多种JVM语言的代理与反射应用

2021/03/31 其他 共 14246 字,约 41 分钟
梦境迷离

一、Java反射

需要注意的是:本文的代理不是为了给方法添加前置或者后缀逻辑,而是直接替换方法本身实现。

被代理对象

这个也是我使用graphql-java-codegen生成的一个resolver,这里我们需要了解这些,只知道我们有个接口,其中有个方法,需要被动态代理使用即可。

public interface QueryResolver {
    UserTO user(String login) throws Exception;
}

InvocationHandler实现

通常我们在Java中只需实现InvocationHandler接口,本文Scala和Kotlin同样使用该接口实现代理。如下:

final public class JavaResolverProxy implements InvocationHandler, JavaDeserializerAdapter {

    private GraphQLResponseProjection projection;

    private GraphQLOperationRequest request;

    private final ServerConfig config;

    public JavaResolverProxy(ServerConfig config, GraphQLResponseProjection projection, Class<? extends GraphQLOperationRequest> request) {
        this.config = config;
        this.projection = projection;
        try {
            this.request = request.newInstance();
        } catch (InstantiationException | IllegalAccessException e) {
            throw new ExecuteException("newInstance failed: ", e.getLocalizedMessage(), e);
        }
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) {
        if (Object.class.equals(method.getDeclaringClass())) {
            try {
                return method.invoke(this, args);
            } catch (Throwable t) {
                throw new ExecuteException("invoke failed: ", t.getLocalizedMessage(), t);
            }
        } else {
            return proxyInvoke(method, args);
        }
    }

    private Object proxyInvoke(Method method, Object[] args) {
        Field field = null;
        List<GraphQLResponseField> fields;
        Class<?> entityClass;
        Type type = method.getGenericReturnType();
        // 获取方法的返回类型,用于后续的json解析
        if (type instanceof ParameterizedType) {
            Type[] parameterizedType = ((ParameterizedType) type).getActualTypeArguments();
            entityClass = (Class<?>) parameterizedType[0];
        } else {
            entityClass = (Class<?>)type;
        }

        if (isPrimitive(entityClass)) {
            assert(projection == null);
        } else {
            assert(projection != null);
        }
		// 利用Java8的特性,获取参数名列表与参数值组成map,用于发送graphql请求,可以不管
        List<Parameter> parameters = Arrays.stream(method.getParameters()).collect(Collectors.toList());

        if (!parameters.isEmpty()) {
            List<String> parameterNames = parameters.stream().map(Parameter::getName).collect(Collectors.toList());
            List<Object> arguments = Arrays.stream(args).collect(Collectors.toList());
            request.getInput().putAll(CollectionUtils.listToMap(parameterNames, arguments));
        }
        // 使用反射获取父类的字段
        try {
            field = projection.getClass().getSuperclass().getDeclaredField("fields");
            field.setAccessible(true);
            fields = (List<GraphQLResponseField>) field.get(projection);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new ExecuteException("access fields failed: ", e.getLocalizedMessage(), e);
        } finally {
            if (field != null) {
                field.setAccessible(false);
            }
        }

        //if fields not null, use it directly, because user want to select fields
        if (projection != null && (fields == null || fields.isEmpty())) {
            throw new ExecuteException("projection verification failed: ", "fields of projection cannot be empty", null);
        }

        GraphQLRequest graphQLRequest = new GraphQLRequest(request, projection);
        Object ret;
        // 发送graphql请求获取json解析后的对象
        ret = OkHttp.syncRunQuery(config, graphQLRequest, entityClass, buildFunction3());
        return ret;
    }

}

代理实现

有了处理器的实现类,我们还需构造代理对象,如下:

final public class GitHubJavaClient {

    private ServerConfig config;
    private Class<?> resolver;
    private GraphQLResponseProjection projection;
    private Class<? extends GraphQLOperationRequest> request;

    private GitHubJavaClient() {

    }

    private Object getResolver() {
        JavaResolverProxy invocationHandler = new JavaResolverProxy(config, projection, request);
        return Proxy.newProxyInstance(resolver.getClassLoader(), new Class[]{resolver}, invocationHandler);
    }


    private void setConfig(ServerConfig config) {
        this.config = config;
    }

    private void setResolver(Class<?> resolver) {
        this.resolver = resolver;
    }

    private void setRequest(Class<? extends GraphQLOperationRequest> request) {
        this.request = request;
    }

    private void setProjection(GraphQLResponseProjection projection) {
        this.projection = projection;
    }

    public static GitHubJavaClientBuilder newBuilder() {
        return new GitHubJavaClientBuilder();
    }

    public static class GitHubJavaClientBuilder {
        private GraphQLResponseProjection projection;
        private Class<? extends GraphQLOperationRequest> request;
        private ServerConfig config;

        private GitHubJavaClientBuilder() {

        }

        public GitHubJavaClientBuilder setRequest(Class<? extends GraphQLOperationRequest> request) {
            this.request = request;
            return this;
        }

        public GitHubJavaClientBuilder setConfig(ServerConfig config) {
            this.config = config;
            return this;
        }

        public GitHubJavaClientBuilder setProjection(GraphQLResponseProjection projection) {
            this.projection = projection;
            return this;
        }
        
        @SuppressWarnings(value = "unchecked")
        public <Resolver> Resolver build(Class<Resolver> resolver) {
            GitHubJavaClient invoke = new GitHubJavaClient();
            assert (resolver != null);
            assert (request != null);
            invoke.setProjection(projection);
            invoke.setResolver(resolver);
            invoke.setConfig(config);
            invoke.setRequest(request);
            return (Resolver) invoke.getResolver();
        }
    }

}

使用

不需要new,直接调用接口的方法

public class JavaClientExample {
    public static void main(String[] args) throws Exception {

        // 1. Use projection to select the preset returned.
        UserResponseProjection userResponseProjection = new UserResponseProjection().id().avatarUrl().login().resourcePath();

        QueryResolver queryResolver = GitHubJavaClient.newBuilder()
                // 2. Set the service endpoint.
                .setConfig(ServerConfig.apply("https://api.github.com/graphql", Collections.singletonMap("Authorization", "Bearer xx")))
                .setProjection(userResponseProjection)
                // 3. Set the request corresponding to the resolver.
                .setRequest(UserQueryRequest.class)
                // 4. Set the resolver that needs a proxy.
                .build(QueryResolver.class);

        // 5. Use resolver to create a call.
        UserTO userTO = queryResolver.user("jxnu-liguobin"); // projection and request must correspond to the return type of the user method.
        System.out.println(userTO.toString());

    }
}

二、Scala中使用Java反射

被代理对象

trait QueryResolver {
    def user(login: String): UserTO
}

Scala中也可直接使用Java反射操作Java/Scala类,但是代码不如Scala反射优美。同时有些Scala特有类型可能反射不到,此时必须使用Scala反射。

InvocationHandler实现

//这里的Manifest是Scala特有,用于获取运行时类
final class ScalaResolverProxyV1[Request <: GraphQLOperationRequest : Manifest] private
(val config: ServerConfig, val projection: GraphQLResponseProjection) extends InvocationHandler
  with JavaDeserializerAdapter {

  private lazy val request: GraphQLOperationRequest = manifest[Request].runtimeClass.getConstructor(classOf[String]).
    newInstance(null).asInstanceOf[GraphQLOperationRequest]

  override def invoke(proxy: AnyRef, method: Method, args: Array[AnyRef]): Any =
    if (classOf[AnyRef] == method.getDeclaringClass) {
      try {
        method.invoke(this, args)
      } catch {
        case t: Throwable =>
          throw ExecuteException("invoke failed: ", t.getLocalizedMessage, t)
      }
    } else {
      proxyInvoke(method, args)
    }

  private def proxyInvoke(method: Method, args: Array[AnyRef]): Any = {
    val `type` = method.getGenericReturnType
    val entityClass = `type` match {
      case parameterizedType1: ParameterizedType =>
        val parameterizedType = parameterizedType1.getActualTypeArguments
        parameterizedType(0).asInstanceOf[Class[_]]
      case _ => `type`.asInstanceOf[Class[_]]
    }

    if (isPrimitive(entityClass)) {
      assert(projection == null)
    } else {
      assert(projection != null)
    }

    val parameters = method.getParameters.toList
    if (parameters.nonEmpty) {
      val parameterNames = parameters.map(_.getName)
      val arguments = args.toList
      request.getInput.putAll(CollectionUtils.listToMap(parameterNames, arguments))
    }
    // TODO remove reflect
    var field: Field = null
    var fields: util.List[GraphQLResponseField] = null
    try {
      field = projection.getClass.getSuperclass.getDeclaredField("fields")
      field.setAccessible(true)
      fields = field.get(projection).asInstanceOf[util.List[GraphQLResponseField]]
    } catch {
      case e@(_: NoSuchFieldException | _: IllegalAccessException) =>
        throw ExecuteException("access fields failed: ", e.getLocalizedMessage, e)
    } finally if (field != null) field.setAccessible(false)

    if (projection != null && (fields == null || fields.isEmpty)) {
      throw ExecuteException("projection verification failed: ", "fields of projection cannot be empty")
    }

    val graphQLRequest = new GraphQLRequest(request, projection)
    OkHttp.syncRunQuery(config, graphQLRequest, entityClass)(extractData)
  }

}

object ScalaResolverProxyV1 {

  def apply[Request <: GraphQLOperationRequest : Manifest](config: ServerConfig, projection: GraphQLResponseProjection):
  ScalaResolverProxyV1[Request] = new ScalaResolverProxyV1[Request](config, projection)
}

代理实现


class GithubScalaClient {

  private var config: ServerConfig = _
  private var resolver: Class[_] = _
  private var projection: GraphQLResponseProjection = _

  private def getResolverObjectV1[Request <: GraphQLOperationRequest : Manifest]: AnyRef = {
    val invocationHandler: ScalaResolverProxyV1[Request] = ScalaResolverProxyV1[Request](config, projection)
    Proxy.newProxyInstance(resolver.getClassLoader, Array[Class[_]](resolver), invocationHandler)
  }
}

object GithubScalaClient {

  def newBuilder: GitHubScalaClientBuilder = new GitHubScalaClientBuilder()

  class GitHubScalaClientBuilder() {

    private var projection: GraphQLResponseProjection = _
    private var config: ServerConfig = _

    def setConfig(config: ServerConfig): GitHubScalaClientBuilder = {
      this.config = config
      this
    }

    def setProjection(projection: GraphQLResponseProjection): GitHubScalaClientBuilder = {
      this.projection = projection
      this
    }

    def buildV1[Resolver: Manifest, Request <: GraphQLOperationRequest : Manifest]: Resolver = {
      assert(this.config != null)
      val invoke = new GithubScalaClient
      invoke.config = this.config
      invoke.projection = this.projection
      invoke.resolver = manifest[Resolver].runtimeClass
      invoke.getResolverObjectV1[Request].asInstanceOf[Resolver]
    }
  }

}

使用

object ScalaClientExample extends App {

  val userResponseProjection = new UserResponseProjection().id().avatarUrl().login().resourcePath()
  val config = ServerConfig("https://api.github.com/graphql", Map("Authorization" -> "Bearer xx"))
  val queryResolver = GithubScalaClient.newBuilder.setConfig(config).
    setProjection(userResponseProjection).
    buildV1[QueryResolver, UserQueryRequest]

  val userTO = queryResolver.user("jxnu-liguobin")
  println(userTO.id) //tostring failed, because jackson use java Deserializer
}

三、Scala反射

被代理对象

trait QueryResolver {
    def user(login: String): UserTO
}

InvocationHandler实现

// Scala反射的环境
import scala.reflect.runtime.{ universe => ru }
import scala.reflect.runtime.universe._

final class ScalaResolverProxyV2[Request <: GraphQLOperationRequest : Manifest, Out: Manifest] private //使用Manifest传递返回类型,不再需要通过返回获取方法的返回类型,json解析时,需要使用ScalaObjectMapper和CaseClassObjectMapper
(val config: ServerConfig, val projection: GraphQLResponseProjection) extends InvocationHandler
  with ScalaDeserializer {
  // 使用Scala反射获取构造函数 
  private[this] lazy val constructor = getRuntimeMirror.reflectClass(getRequestScalaType.typeSymbol.asClass)
    .reflectConstructor(getRequestScalaType.members.find(_.isConstructor).get.asMethod)

  private val request: GraphQLOperationRequest = constructor.apply(null).asInstanceOf[GraphQLOperationRequest]

  //当前类的运行时环境
  private[this] def getRuntimeMirror: ru.Mirror = runtimeMirror(getClass.getClassLoader)
  //获取Request泛型的运行时类型,通过类型反射出构造函数	
  private[this] def getRequestScalaType: ru.Type = typeOf[Request]

  override def invoke(proxy: AnyRef, method: Method, args: Array[AnyRef]): Any =
    if (classOf[AnyRef] == method.getDeclaringClass) {
      try {
        method.invoke(this, args)
      } catch {
        case t: Throwable =>
          throw ExecuteException("invoke failed: ", t.getLocalizedMessage, t)
      }
    } else {
      proxyInvoke(method, args)
    }

  private def proxyInvoke(method: Method, args: Array[AnyRef]): Any = {
    val `type` = method.getGenericReturnType
    val isCollection = `type` match {
      case _: ParameterizedType => true
      case _ => false
    }

    if (isPrimitive(manifest[Out].runtimeClass)) {
      assert(projection == null)
    } else {
      assert(projection != null)
    }

    val parameters = method.getParameters.toList
    if (parameters.nonEmpty) {
      val parameterNames = parameters.map(_.getName)
      val arguments = args.toList
      request.getInput.putAll(CollectionUtils.listToMap(parameterNames, arguments))
    }

    // use shapeless LabelledGeneric
    // Scala反射获取父类的私有字段
    def fieldValue(name: String): Any = {
      //获取到当前类加载的运行时环境,反射出projection对象父类中的名为“fields”的字段
      //members返回projection类的所有成员,包含继承过来的,所以这里可以拿到父类的“fields”字段
      val im = ru.runtimeMirror(projection.getClass.getClassLoader)
      getRuntimeMirror.classSymbol(projection.getClass).toType.members.filter(!_.isMethod).
        filter(_.name.decodedName.toString.trim.equals(name)).map(s => {
        im.reflect(projection).reflectField(s.asTerm).get
      }).head
    }

    val fields: java.util.List[GraphQLResponseField] = fieldValue("fields").asInstanceOf[java.util.List[GraphQLResponseField]]

    if (projection != null && (fields == null || fields.isEmpty)) {
      throw ExecuteException("projection verification failed: ", "fields of projection cannot be empty")
    }

    val graphQLRequest = new GraphQLRequest(request, projection)
    // extractData是一个解析函数,Out传递过去用于解析json。注意,这里没有entityClass
    OkHttp.syncRunQuery(config, isCollection, graphQLRequest)(extractData[Out])
  }

}

object ScalaResolverProxyV2 {

  def apply[Request <: GraphQLOperationRequest : Manifest, Out: Manifest](config: ServerConfig, projection: GraphQLResponseProjection):
  ScalaResolverProxyV2[Request, Out] = new ScalaResolverProxyV2[Request, Out](config, projection)
}

代理实现

其实只要在上面的GithubScalaClient中添加2个方法即可。

  private def getResolverObjectV2[Request <: GraphQLOperationRequest : Manifest, Out: Manifest]: AnyRef = {
    val invocationHandler: ScalaResolverProxyV2[Request, Out] = ScalaResolverProxyV2[Request, Out](config, projection)
    Proxy.newProxyInstance(resolver.getClassLoader, Array[Class[_]](resolver), invocationHandler)
  }
  def buildV2[Resolver: Manifest, Request <: GraphQLOperationRequest : Manifest, Out: Manifest]: Resolver = {
	assert(this.config != null)
	val invoke = new GithubScalaClient
	invoke.config = this.config
	invoke.projection = this.projection
	invoke.resolver = manifest[Resolver].runtimeClass
	invoke.getResolverObjectV2[Request, Out].asInstanceOf[Resolver]
}

使用

由于我们使用了ManifestScalaObjectMapper,我们可以直接在调用时传递返回类型,这样就不需要使用反射获取方法的返回类型,减少一步操作,也更符合Scala代码风格。不过由于Jackson-scala-module提示不再支持Scala3,所以如果不能使用Manifest,那么就得继续使用entityClass。使用Manifest如下:

  val userResponseProjection1 = new UserResponseProjection().id().avatarUrl().login().resourcePath()
  val queryResolver1 = GithubScalaClient.newBuilder.setConfig(config).
    setProjection(userResponseProjection).
    buildV2[QueryResolver, UserQueryRequest, UserTO]//指定返回类型

  val userTO1 = queryResolver1.user("jxnu-liguobin")
  println(userTO.toString())

当然,动态代理+JSON的最大问题之一是无法处理编译时的类型不匹配,只会在运行时暴露出来错误。本文不考虑性能问题,专注可行性,主要用于测试代码生成库graphql-java-codegen。

四、Kotlin中使用Java反射

被代理对象

interface QueryResolver {
    fun rateLimit(dryRun: Boolean?): RateLimitTO?
}

具体就不写了,坑也很多,三种语言的源码在github-graphql-client 使用如下:

object KotlinClientExample {

    @JvmStatic
    fun main(args: Array<String>) {
        // Since Kotlin has a mandatory non-null for fields, a field-less interface test is used here
        val rateLimitResponseProjection = RateLimitResponseProjection().`all$`(1)
        val queryResolver = GithubKotlinClient.newBuilder()
            .setConfig(
                ServerConfigAdapter(
                    "https://api.github.com/graphql",
                    mapOf(Pair("Authorization", "Bearer xx"))
                )
            )
            .setProjection(rateLimitResponseProjection).build<QueryResolver, RateLimitQueryRequest>()

        val rateLimit = queryResolver.rateLimit(true)
        println(rateLimit.toString())
    }
}

文档信息

Search

    Table of Contents