动态代理与反射在GraphQL客户端的应用
反射与动态代理实践
简介
文章稍长,代码较多,使用电脑看比较合适。
目前需要使用 graphql 对外提供 open api,准备开发一个 sdk,使得尽可能的自动实现 graphql 的调用。也就是绝大部分代码使用代码生成。
代码生成主要针对 Java,其他 JVM 语言都可以使用,对于其他语言也可以参考类似的设计方法。使用的库是 graphql-java-codegen(https://github.com/kobylynskyi/graphql-java-codegen),该工具参考了 swagger-api 。
熟悉 graphql 的人应该知道,graphql 具有复杂的数据类型定义和更强的功能支持,如果要使用代码生成,但是编程语言与该类型不存在一一对应的关系,则可能会增加设计工具的复杂性。好在 Java 支持绝大多数的 graphql 类型,我们使用的 graphql-java-codegen 也很好的支持了代码生成这一步。
总的来说,对 graphql-java-codegen 而言,会抽象出四个主要的模型:
Request 用于抽象 graphql 的请求参数和请求类型,目前在 graphql 中没有 HTTP 的 GET/POST/DELETE/PUT,只有 query/mutation/subscription
Response 用于抽象 graphql 的返回数据结构,众所周知,graphql 是强类型的,类似 protobuffer。
Response Projection 有了返回的数据结构我们还需要选择返回哪些字段,这是 graphql 的特性之一,通常我们只返回我们需要的字段。
Resolver 用于抽象 graphql 的接口,每个 resolver 都是一个 graphql 中的一个方法,对应一个 HTTP 调用。需要注意的是,对一个资源的操作,CRUD 可以生成四个 resolver,每个仅有一个方法;也可以仅生成一个 resolver 接口包含有四个方法。这取决于代码生成策略。
有了上面基础,我们在看如何使用生成的实现 graphql client。
我们的 schema 如下:
type Query {: Episode) : Character: String) : Humanhumans: [Human]: ID!) : Droid}interface Character {id: ID!name: String!friends: [Character]appearsIn: [Episode]!secretBackstory : String @deprecated(reason : "We have decided that this is not canon")}type Human implements Character {id: ID!name: String!friends: [Character]appearsIn: [Episode]!homePlanet: StringsecretBackstory : String @deprecated(reason : "We have decided that this is not canon")email: Email!}type Droid implements Character {id: ID!name: String!friends: [Character]appearsIn: [Episode]!primaryFunction: StringsecretBackstory : String @deprecated(reason : "We have decided that this is not canon")}
普通方式
我们使用测试代码生成了一个 resolver,它包含了四个方法
@javax.annotation.Generated(value = "com.kobylynskyi.graphql.codegen.GraphQLCodegen",date = "2020-08-10T15:37:50+0800")public interface QueryResolver {@com.fasterxml.jackson.annotation.JsonTypeInfo(use=com.fasterxml.jackson.annotation.JsonTypeInfo.Id.NAME, include=com.fasterxml.jackson.annotation.JsonTypeInfo.As.PROPERTY,property = "__typename")@com.fasterxml.jackson.annotation.JsonSubTypes(value = {@com.fasterxml.jackson.annotation.JsonSubTypes.Type(value = HumanDO.class, name = "Human"),@com.fasterxml.jackson.annotation.JsonSubTypes.Type(value = DroidDO.class, name = "Droid")})CharacterDO hero(EpisodeDO episode) throws Exception;HumanDO human(String id) throws Exception;java.util.List<HumanDO> humans() throws Exception;DroidDO droid(String id) throws Exception;}
正常情况下,我们应该实现这个接口,然后使用IOC注入这个接口或者 new 接口的实现类,调用 graphql 服务端。下面是一个使用 Scala 实现的 QueryResolverImpl
class QueryResolverImpl extends QueryResolver {[Exception]def hero(episode: EpisodeDO): CharacterDO = {val heroQueryRequest = new HeroQueryRequestheroQueryRequest.setEpisode(episode)//must use typename, and add jackson annotation to support, since v2.4val characterResponseProjection = new CharacterResponseProjection().id().name().typename().friends(new CharacterResponseProjection().id().name().typename()).appearsIn()val graphQLRequest = new GraphQLRequest(heroQueryRequest, characterResponseProjection)val retFuture = OkHttp.executeRequest(graphQLRequest, new TypeReference[CharacterDO] {})val ret = Await.result(retFuture, Duration.Inf)ret}[Exception]def human(id: String): HumanDO = {val humanQueryRequest = new HumanQueryRequesthumanQueryRequest.setId(id)//must use typename, and add jackson annotation to support, since v2.4val humanResponseProjection = new HumanResponseProjection().id().name().typename()val graphQLRequest = new GraphQLRequest(humanQueryRequest, humanResponseProjection)val retFuture = OkHttp.executeRequest(graphQLRequest, new TypeReference[HumanDO] {})val ret = Await.result(retFuture, Duration.Inf)ret}[Exception]def humans: util.List[HumanDO] = {import scala.collection.JavaConverters._val humanQueryRequest = new HumansQueryRequest//must use typename, and add jackson annotation to support, since v2.4val humanResponseProjection = new HumanResponseProjection().id().name().typename()val graphQLRequest = new GraphQLRequest(humanQueryRequest, humanResponseProjection)val retFuture = OkHttp.executeRequest(graphQLRequest, new TypeReference[List[HumanDO]] {})val ret = Await.result(retFuture, Duration.Inf)ret.asJava}[Exception]def droid(id: String): DroidDO = {val productByIdQueryRequest = new DroidQueryRequestproductByIdQueryRequest.setId(id)//must use typename, and add jackson annotation to support, since v2.4val droidResponseProjection = new DroidResponseProjection().id().name().typename()val graphQLRequest = new GraphQLRequest(productByIdQueryRequest, droidResponseProjection)val retFuture = OkHttp.executeRequest(graphQLRequest, new TypeReference[DroidDO] {})val ret = Await.result(retFuture, Duration.Inf)ret}}
可以看到,不仅需要为每个 resolver 编写一个实现类,而且每个 request 都需要去 set 传进来的参数,并且需要实例化 projection 指定返回的字段(调用 projection 的方法,将其添加到 Map 中);如果是嵌套的字段,还需要 typename。很显然,所有 resolver 的实现,也都是这个流程,通过构造 request 和 projection 来实例化 GraphQLRequest,生成 graphql query,调用服务端接口。所以我们可以使用动态代理来优化这个调用过程。
动态代理 & 反射
首先,我们当然需要创建一个代理对象,我们定义一个 Java 类,叫做 ResolverImplClient
import com.kobylynskyi.graphql.codegen.model.graphql.GraphQLOperationRequest;import com.kobylynskyi.graphql.codegen.model.graphql.GraphQLResponseProjection;import java.lang.reflect.Proxy;/*** invoker for proxy** @author [email protected]* @version 1.0, 2020/7/28*/final public class ResolverImplClient {private Class<?> resolver;private GraphQLResponseProjection projection;private GraphQLOperationRequest request;private int maxDepth;private Object getResolver() {DynamicProxy invocationHandler = new DynamicProxy(projection, request, maxDepth);return Proxy.newProxyInstance(resolver.getClassLoader(), new Class[]{resolver}, invocationHandler);}private void setResolver(Class<?> resolver) {this.resolver = resolver;}private void setRequest(GraphQLOperationRequest request) {this.request = request;}private void setMaxDepth(int maxDepth) {this.maxDepth = maxDepth;}private void setProjection(GraphQLResponseProjection projection) {this.projection = projection;}public static class ResolverImplClientBuilder {private GraphQLResponseProjection projection;private GraphQLOperationRequest request;private int maxDepth = 3;private ResolverImplClientBuilder() {}public ResolverImplClientBuilder setRequest(GraphQLOperationRequest request) {this.request = request;return this;}public ResolverImplClientBuilder setMaxDepth(int maxDepth) {this.maxDepth = maxDepth;return this;}public ResolverImplClientBuilder setProjection(GraphQLResponseProjection projection) {this.projection = projection;return this;}public static ResolverImplClientBuilder newBuilder() {return new ResolverImplClientBuilder();}/*** Resolver is generic type, if we do not want to cast to real resolver on the user side, we need set resolver when invoker builder method,* although this is not in line with the conventional builder model** @return R resolver which need for invoke graphql*/(value = "unchecked")public <R> R build(Class<R> resolver) {ResolverImplClient invoke = new ResolverImplClient();assert (projection != null);assert (resolver != null);assert (request != null);invoke.setProjection(projection);invoke.setResolver(resolver);invoke.setMaxDepth(maxDepth);invoke.setRequest(request);return (R) invoke.getResolver();}}}
这里我们将泛型放到了 build 方法上,是因为基于构建者模式,外部类的泛型在内部类 new 时会丢失则得到的代理对象需要强转,而强转增加了使用的复杂性,所以将泛型放到了最后的 build 方法。当然,如果是 Scala,可以使用 runtimeclass,因为目的是想 JVM 语言都支持,所以这里都使用 Java 实现了。
知道如何构造代理对象,我们还需要定义处理函数,在 Java 中,我们只需要实现 InvocationHandler 接口(JDK动态代理)
import com.kobylynskyi.graphql.codegen.model.graphql.GraphQLOperationRequest;import com.kobylynskyi.graphql.codegen.model.graphql.GraphQLResponseProjection;import java.lang.reflect.*;import java.util.Arrays;import java.util.LinkedHashMap;import java.util.List;import java.util.Map;import java.util.stream.Collectors;/*** dynamic proxy for create request* <p>* this is a experimental implement** @author liguobin@growingio.com* @version 1.0, 2020/7/28*/final public class DynamicProxy implements InvocationHandler, ExecutionGraphql {private GraphQLResponseProjection projection;private GraphQLOperationRequest request;/*** should limit max depth when invoke method on projection.*/private int maxDepth;DynamicProxy(GraphQLResponseProjection projection, GraphQLOperationRequest request, int maxDepth) {this.projection = projection;this.request = request;this.maxDepth = maxDepth;}public Object invoke(Object proxy, Method method, Object[] args) {System.out.println("before Invoking");if (Object.class.equals(method.getDeclaringClass())) {try {System.out.println("invoking by resolver implement");return method.invoke(this, args);} catch (Throwable t) {t.printStackTrace();return null;}} else {System.out.println("invoking by dynamic proxy");return proxyInvoke(method, args);}}/*** 在处理投影(projection)时,我们使用反射直接调用所有方法(不处理alias),而在处理请求(需要参数)时,我们使用反射来获取 input 字段,这是基于 setXXX 方法的内部实现来实现的。** @param parentProjection* @param parentMethod* @param currentDepth*/private void invokeOnProjection(GraphQLResponseProjection parentProjection, Method parentMethod, int currentDepth) {try {//对于无参方法,我们使用了 `parentMethod.invoke(parentProjection, null);`,这里的 null 不能使用空数组替代。if (parentMethod.getParameterCount() == 0) {System.out.println("method <" + parentMethod.getName() + ">");parentMethod.invoke(parentProjection, null);return;}//if this method have parameters, eg: name(String alias) or friends(CharacterResponseProjection subProjection), friends(CharacterResponseProjection subProjection, String alias),//we only handle do not have any alias, like: friends(CharacterResponseProjection subProjection)for (Class<?> parameterClazz : parentMethod.getParameterTypes()) {//处理只有一个参数的方法调用if (parentMethod.getParameterCount() == 1) {if (GraphQLResponseProjection.class.isAssignableFrom(parameterClazz)) {currentDepth++;GraphQLResponseProjection subProjection = (GraphQLResponseProjection) parameterClazz.newInstance();//at now,not support `..on`List<Method> methods = Arrays.stream(parameterClazz.getDeclaredMethods()).filter(m -> !m.getName().startsWith("on")).collect(Collectors.toList());for (Method subProjectionMethod : methods) {//if this method have no parameterif (subProjectionMethod.getParameterCount() == 0) {String t = "->";for (int i = 0; i < currentDepth; i++) {t = t.concat("->");}System.out.println(t + " method <" + subProjectionMethod.getName() + ">");subProjectionMethod.invoke(subProjection, null);} else if (subProjectionMethod.getParameterCount() == 1 && GraphQLResponseProjection.class.isAssignableFrom(subProjectionMethod.getParameterTypes()[0])) {//if this method have one parameter and type is GraphQLResponseProjection sub class//recursive continuation callif (currentDepth < maxDepth) {invokeOnProjection(subProjection, subProjectionMethod, currentDepth);}} else {//TODO getParameterCount == 2, (GraphQLResponseProjection sub and String alias)}}parentMethod.invoke(parentProjection, subProjection);}} else {//TODO getParameterCount == 2, (GraphQLResponseProjection sub and String alias)}}} catch (IllegalAccessException | InvocationTargetException | InstantiationException e) {e.printStackTrace();}}private Object proxyInvoke(Method method, Object[] args) {int i = 0;String entityClazzName;try {//对于集合类型,目前只处理了 List,我们需要拿到它的泛型参数,才能正确反序列化。所以使用 `method.getGenericReturnType()` 获取 resolver 接口的方法的返回类型。//如果返回类型是一个 参数化类型,我们还需要继续获取其中的值。`Type[] parameterizedType = ((ParameterizedType) type).getActualTypeArguments();` 帮助我们获取该类型的参数化类型的列表,//由于我们只处理 List 类型,很显然,它只有一个参数化类型,所以我们使用 `entityClazzName = parameterizedType[0].getTypeName();`。//而当 resolver 接口的方法的返回类型是一个普通 entity 时,我们只需要直接获取它的类名即可。//使用 `type.getTypeName();`Type type = method.getGenericReturnType();if (type instanceof ParameterizedType) {Type[] parameterizedType = ((ParameterizedType) type).getActualTypeArguments();entityClazzName = parameterizedType[0].getTypeName();} else {entityClazzName = type.getTypeName();}List<Parameter> parameters = Arrays.stream(method.getParameters()).collect(Collectors.toList());//if this method have no parameter, then do not need invoke on request instance//other wise, we need append parameters to request field which use hashmap store paramsif (!parameters.isEmpty()) {Field input = request.getClass().getDeclaredField("input");input.setAccessible(true);Map<String, Object> params = new LinkedHashMap<>();for (Parameter parameter : parameters) {Object argsCopy = args[i++];//我们使用反射获取参数名称,同时更改 request 的 input 字段值,达到了等同调用 set 方法的目的。(因为set方法最终也是将参数和参数值放到 input 中)//因为我们必须在反射时动态获取参数值,所以我们应该使用 java8的编译参数 -parameters,以便字节码中保留参数名称,否则参数名都是 var0 var1,没办法获取真实的参数名params.put(parameter.getName(), argsCopy);System.out.println("request parameter <" + parameter.getName() + "> and parameter type <" + parameter.getType().getName() + ">");}input.set(request, params);input.setAccessible(false);}//newInstance GraphQLResponseProjection and GraphQLOperationRequest//获取自己定义的方法,因为很显然,projection 是提供本 response 的字段选择功能,而父类继承过来的方法,我们根本不需要。for (Method parentMethod : projection.getClass().getDeclaredMethods()) {invokeOnProjection(projection, parentMethod, 1);}return executeByHttp(entityClazzName, request, projection);} catch (IllegalAccessException | NoSuchFieldException e) {e.printStackTrace();}return null;}}
这里有比较关键的几点,我们使用动态代理时,没有办法知道需要返回哪些字段,所以一律返回所有字段。我们不考虑使用传递参数的形式指定字段,这对于使用客户端的用户来说,不够友好,用户只需要得到数据。
且动态代理是自动构建 projection,所以也没有办法知道需要对嵌套查询如何终止,所以这里采用限制递归深度,每次仅查询最大深度为3的的子结构。
如果我们不递归调用子类型的 projection,无法返回嵌套结构。在 graphql 中,想要返回该数据中的字段的字段,则需要提供字段本身的详细结构。
如果我们不递归,friends 就不知道返回哪些数据,此时可以干脆去掉 friends()(此时返回结构中就没有 friends 的内容了),也可以向最上面普通方式一样:
val characterResponseProjection = new CharacterResponseProjection().id().name().typename().friends(new CharacterResponseProjection().id().name().typename()).appearsIn() //向 friends 传入 CharacterResponseProjection 查询 friends 的详细内容。
现在我们可以使用动态代理来发起调用:使用Java
import io.github.dreamylost.api.HumanQueryResolver;import io.github.dreamylost.api.HumansQueryResolver;import io.github.dreamylost.model.HumanDO;import io.github.dreamylost.model.HumanQueryRequest;import io.github.dreamylost.model.HumanResponseProjection;import io.github.dreamylost.model.HumansQueryRequest;import java.util.List;/*** @author [email protected]* @version 1.0, 2020/7/29*/public class HumanResolverJavaApp {public static void main(String[] args) {ResolverImplClient.ResolverImplClientBuilder humanInvokerBuilder = ResolverImplClient.ResolverImplClientBuilder.newBuilder().setProjection(new HumanResponseProjection());//Set your own request and resolver for each requesttry {System.out.println("======human========");HumanDO humanDO = humanInvokerBuilder.setRequest(new HumanQueryRequest()).build(HumanQueryResolver.class).human("1001");assert humanDO.getEmail() == "[email protected]";System.out.println(humanDO);System.out.println("======humans========");List<HumanDO> humanDOs = humanInvokerBuilder.setRequest(new HumansQueryRequest()).build(HumansQueryResolver.class).humans();assert humanDOs.size() == 4;System.out.println(humanDOs);} catch (Exception e) {e.printStackTrace();}//......}}
使用Scala
import io.github.dreamylost.api.{ HumanQueryResolver, HumansQueryResolver }import io.github.dreamylost.model.{ HumanQueryRequest, HumanResponseProjection, HumansQueryRequest }import scala.collection.JavaConverters._/*** use invoke by proxy in scala** @author [email protected]* @version 1.0,2020/7/28*/object HumanResolverScalaApp extends App {//For a model, the projection and maximum depth fields can be commonval humanInvokerBuilder = ResolverImplClient.ResolverImplClientBuilder.newBuilder().setProjection(new HumanResponseProjection())//Set your own request and resolver for each requestprintln("======human========")val human = humanInvokerBuilder.setRequest(new HumanQueryRequest).build(classOf[HumanQueryResolver]).human("1001")println(human)println("======humans========")val humans = humanInvokerBuilder.setRequest(new HumansQueryRequest).build(classOf[HumansQueryResolver]).humans().asScalafor (human <- humans) {println("->" + human)}//......}
可以看到,我们实际上只需要一个链式调用即可,无需编写 resolver 的实现类。也就是,我们只需要使用代码生成工具,生成代码,就可以使用代码调用服务端了。
