vlambda博客
学习文章列表

动态代理与Scala反射/Java反射在Java、Scala、Kotlin中的使用

一、Java反射

被代理对象

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

本文代码来自本人为graphql-java-codegen编写的集成测试客户端github-graphql-client,该仓库以github的graphql api为基准实现,针对三种语言进行测试。
https://github.com/jxnu-liguobin/github-graphql-client

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

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]}

使用

由于我们使用了Manifest和ScalaObjectMapper,我们可以直接在调用时传递返回类型,这样就不需要使用反射获取方法的返回类型,减少一步操作,也更符合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()) }}