一、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);}}@Overridepublic 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 fieldsif (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 InvocationHandlerwith 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.getGenericReturnTypeval entityClass = `type` match {
case parameterizedType1: ParameterizedType =>val parameterizedType = parameterizedType1.getActualTypeArgumentsparameterizedType(0).asInstanceOf[Class[_]]case _ => `type`.asInstanceOf[Class[_]]}if (isPrimitive(entityClass)) {
assert(projection == null)} else {
assert(projection != null)}val parameters = method.getParameters.toListif (parameters.nonEmpty) {
val parameterNames = parameters.map(_.getName)val arguments = args.toListrequest.getInput.putAll(CollectionUtils.listToMap(parameterNames, arguments))}// TODO remove reflectvar field: Field = nullvar fields: util.List[GraphQLResponseField] = nulltry {
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 = configthis}def setProjection(projection: GraphQLResponseProjection): GitHubScalaClientBuilder = {
this.projection = projectionthis}def buildV1[Resolver: Manifest, Request <: GraphQLOperationRequest : Manifest]: Resolver = {
assert(this.config != null)val invoke = new GithubScalaClientinvoke.config = this.configinvoke.projection = this.projectioninvoke.resolver = manifest[Resolver].runtimeClassinvoke.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 InvocationHandlerwith 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.getGenericReturnTypeval isCollection = `type` match {
case _: ParameterizedType => truecase _ => false}if (isPrimitive(manifest[Out].runtimeClass)) {
assert(projection == null)} else {
assert(projection != null)}val parameters = method.getParameters.toListif (parameters.nonEmpty) {
val parameterNames = parameters.map(_.getName)val arguments = args.toListrequest.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。注意,这里没有entityClassOkHttp.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 GithubScalaClientinvoke.config = this.configinvoke.projection = this.projectioninvoke.resolver = manifest[Resolver].runtimeClassinvoke.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 {
@JvmStaticfun main(args: Array<String>) {
// Since Kotlin has a mandatory non-null for fields, a field-less interface test is used hereval 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())}
}