基于模式优先构建GraphQL的客户端

前言

继上次使用动态代理构建GraphQL的客户端之后,本次将使用纯粹的代码生成实现(也就是本次会为resolver接口在编译期间生成自己的实现,而不是通过代理来运行时构造实现)。

众所周知,动态代理有大量反射的使用,这可能存在潜在的性能影响(主要是指resolver 接口的动态代理调用),并且Builder模式虽然一定程度上简化了复杂对象的构造,但是对使用客户端的用户来说,过多的参数需要使用setXX方法,这同样增加了使用的难度。

所以这次将摒弃动态代理的方式,将全部代码使用类库生成。当然,这样也有一些缺点,包括

  1. 由类库实现的代码生成必然会涉及到对服务端响应的结果的反序列化,这些依赖于第三方类库,如何能支持客户端使用者动态替换反序列化类库可能是个问题,目前没有解决,固定使用Jackson
  2. 虽然已经尽量尝试与原作者讨论,但是,作者觉得,该功能不应该属于类库本身的逻辑,并且可能带来过度设计、维护难的问题。感觉作者不太能理解我们的应用场景。其实我们的目的是简化graphql的使用,并且尽可能保留graphql的功能,以及不带来太多额外的性能损失。虽然类库本身只是Java平台的,但是其总体逻辑应与其他语言平台类型是类似的。这么一来最大的影响是,我们得自己维护类库,并跟进原库上的一些bug fix和features。
  3. 其实代码生成类库本身并不在意模式的具体含义,其只是将schema转化为document,并在内存中进行转化。所以它不存在上下文的概念,它对每个schema都是单独的处理,但是对于resolver的实现来说,其必须有request和response projection,才能构造GraphQLRequest(GraphQLRequest graphQLRequest = new GraphQLRequest(request, projection);)。
    有了GraphQLRequest对象就可以以HTTP POST的方式调用graphql server api了。这与动态代理的调用逻辑是相同的,可以参考上一篇。为了解决这个问题,在这里,我们在源库上引入了CodegenContext来记录graphql document之间的映射关系。
  4. 既然把实现都生成了,那么很多配置比如服务端地址,鉴权自然是需要支持的,由此可见,在类库上支持resolver的实现,确实会加重类库的责任。在这里为了支持配置,我们引入了GrowingIOConfig来为客户端使用者提供对外的一些配置信息。

实例

resolver接口

假设现有resolver接口AclsQueryResolver

public interface AclsQueryResolver {
    java.util.List<UserAccessCtrlDto> acls(String resourceType) throws Exception;
}

resolver实现类

现在,我们想要使用类库生成默认的实现类,生成的DefaultAclsQueryResolver代码如下:

 final public class DefaultAclsQueryResolver implements AclsQueryResolver {

    private GrowingIOConfig growingIOConfig;

    public DefaultAclsQueryResolver(GrowingIOConfig growingIOConfig) {
        this.growingIOConfig = growingIOConfig;
    }

    private DefaultAclsQueryResolver() {}

    @Override
    public java.util.List<UserAccessCtrlDto> acls(String resourceType) throws Exception {
        AclsQueryRequest request = new AclsQueryRequest();
        List<String> keys = Arrays.asList("resourceType");
        List<?> values = Arrays.asList(resourceType);
        //参数其实是存储在map中的,通过 keys zip values,可以组成map<k, v>。
        //在动态代理方案时,我们这里使用反射获取方法参数的名称列表,其实是不够准确的
        //因为参数名可能是经过处理的,比如,参数名是int时(Java关键字),生成的实际参数是处理后的Int,但是graphql 的请求参数依然需要int,这可能导致参数名匹配不上,请求失败的问题。
        //但是我们在这里,keys不是resolver的参数名列表,而是graphql schema 字段的原始名称:也就是int,后面看模板就清楚了。
        Map<String, ?> parameters = JavaCollectionUtils.listToMap(keys, values);
        request.getInput().putAll(parameters);
        UserAccessCtrlResponseProjection projection = new UserAccessCtrlResponseProjection().all$(growingIOConfig.getResponseProjectionMaxDepth());
        GraphQLRequest graphQLRequest = new GraphQLRequest(request, projection);
        AclsQueryResponse result = OkHttpUtils.executeGraphQLRemote(growingIOConfig, graphQLRequest, AclsQueryResponse.class);
        return result.acls();
    }

}

executeGraphQLRemote方法主要包含HTTP调用和反序列化的逻辑,如下

    public static <T> T executeGraphQLRemote(final GrowingIOConfig growingIOConfig, final GraphQLRequest graphQLRequest, final Class<T> javaClass) throws Exception {

        if (growingIOConfig == null) {
            throw new Exception("exception in OkHttpUtils, GrowingIOConfig must be not equals to null");
        }

        if (growingIOConfig.getGraphQLServerHost() == null) {
            throw new Exception("exception in OkHttpUtils, graphQLServerHost must be not equals to null");
        }

        Request.Builder request = new Request.Builder()
                .url(growingIOConfig.getGraphQLServerHost())
                .post(RequestBody.create(graphQLRequest.toHttpJsonBody(), MediaType.parse(DEFAULT_MEDIA_TYPE)));

        Map<String, String> headers = growingIOConfig.getHeaders();

        if (!headers.isEmpty()) {
            for (String header : headers.keySet()) {
                request.addHeader(header, headers.get(header));
            }
        }
        Response response = getInstance().newCall(request.build()).execute();
        if (response.code() == 200 && response.body() != null) {
            T ret = Jackson.mapper().readValue(response.body().string(), javaClass);
            return ret;
        } else {
            throw new Exception("exception in OkHttpUtils, response body is: " + response.toString());
        }

    }

模板

简单来说,想要实现上面的DefaultAclsQueryResolver,只需构造模板并填充参数即可。

模板如下:

<#if package?has_content>
package ${package};

</#if>
import com.kobylynskyi.graphql.codegen.extension.GrowingIOConfig;
import com.kobylynskyi.graphql.codegen.extension.utils.JavaCollectionUtils;
import com.kobylynskyi.graphql.codegen.extension.utils.OkHttpUtils;
import com.kobylynskyi.graphql.codegen.model.graphql.GraphQLRequest;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
<#list imports as import>
<#if import?has_content>
import ${import}.*;
</#if>
</#list>

<#if javaDoc?has_content>
/**
<#list javaDoc as javaDocLine>
 * ${javaDocLine}
</#list>
 */
</#if>
<#if generatedInfo.getGeneratedType()?has_content>
@${generatedInfo.getGeneratedType()}(
    value = "com.kobylynskyi.graphql.codegen.GraphQLCodegen",
    date = "${generatedInfo.getDateTime()}"
)
</#if>
final public class ${className} implements ${className?substring(defaultResolverImplPrefix?length, className?length)} {

    private GrowingIOConfig growingIOConfig;

    public ${className}(GrowingIOConfig growingIOConfig) {
        this.growingIOConfig = growingIOConfig;
    }

    private ${className}() {}

<#list operations as operation>
<#if operation.javaDoc?has_content>
    /**
    <#list operation.javaDoc as javaDocLine>
     * ${javaDocLine}
    </#list>
     */
</#if>
<#if operation.deprecated>
    @Deprecated
</#if>
<#list operation.annotations as annotation>
    @${annotation}
</#list>
    @Override
    public ${operation.type} ${operation.name}(<#list operation.parameters as param>${param.type} ${param.name}<#if param_has_next>, </#if></#list>) throws Exception {
        ${operateNameRequestName} request = new ${operateNameRequestName}();
        <#if operation.parameters?? && (operation.parameters?size > 0) >
        List<String> keys = Arrays.asList(<#list operation.parameters as param>"${param.originalName}"<#if param_has_next>, </#if></#list>);
        List<?> values = Arrays.asList(<#list operation.parameters as param>${param.name}<#if param_has_next>, </#if></#list>);
        Map<String, ?> parameters = JavaCollectionUtils.listToMap(keys, values);
        request.getInput().putAll(parameters);
        </#if>
        <#if operateNameProjectionName?has_content>
        ${operateNameProjectionName} projection = new ${operateNameProjectionName}().all$(growingIOConfig.getResponseProjectionMaxDepth());
        GraphQLRequest graphQLRequest = new GraphQLRequest(request, projection);
        <#else>
        GraphQLRequest graphQLRequest = new GraphQLRequest(request, null);
        </#if>
        ${operateNameResponseName} result = OkHttpUtils.executeGraphQLRemote(growingIOConfig, graphQLRequest, ${operateNameResponseName}.class);
        return result.${operation.name}();
    }

</#list>
}

外部使用

这里是Scala写的一个简单包装,和Java没什么差异,应该很容易看懂。

package io.growing.graphql.api

import java.util
import java.util.Collections

import com.kobylynskyi.graphql.codegen.extension.GrowingIOConfig
import io.growing.graphql.model._
import io.growing.graphql.resolver.impl._

/**
 * @author liguobin@growingio.com
 * @version 1.0,2020/10/23
 */
class GrowingioApi(url: String) { //构造函数1,仅url,无鉴权

  private var headers: util.Map[String, String] = _

  /**
   * 构造函数2,只有url和token
   * graphql-java-codegen底层也是把 authKey -> aValue 放到HTTP的请求头中,这里与下面分开仅是为了方便调用
   *
   * @param url
   * @param authKey
   * @param authValue
   */
  def this(url: String, authKey: String, authValue: String) {
    this(url)
    headers = Collections.singletonMap(authKey, authValue)
  }

  /**
   * 构造函数3,通用的headers,token也放在headers中
   *
   * @param url
   * @param headers
   */
  def this(url: String, headers: util.Map[String, String]) {
    this(url)
    this.headers = headers
  }

  private lazy val conf: GrowingIOConfig = new GrowingIOConfig(url, headers)

  def submitTagUserExportJob(tagId: String, properties: util.List[String], charset: String, detailExport: Boolean): TagUserExportJobDto = {
    val resolver = new DefaultSubmitTagUserExportJobMutationResolver(conf)
    resolver.submitTagUserExportJob(tagId, properties, charset, detailExport)
  }

  def submitSegmentUserExportJob(segmentId: String, tags: util.List[String], properties: util.List[String], charset: String): SegmentUserExportJobDto = {
    val resolver = new DefaultSubmitSegmentUserExportJobMutationResolver(conf)
    resolver.submitSegmentUserExportJob(segmentId, tags, properties, charset)
  }

  def jobResult(id: String): JobResultDto = {
    val resolver = new DefaultJobResultQueryResolver(conf)
    resolver.jobResult(id)
  }

  def userProfile(userId: String, tags: util.List[String], properties: util.List[String]): UserProfileDto = {
    val resolver = new DefaultUserProfileQueryResolver(conf)
    resolver.userProfile(userId, tags, properties)
  }

  def tags(): util.List[TagDto] = {
    val resolver = new DefaultTagsQueryResolver(conf)
    resolver.tags()
  }

  def segments(): util.List[SegmentDto] = {
    val resolver = new DefaultSegmentsQueryResolver(conf)
    resolver.segments()

  }

}

本文提供了一种使用纯粹的代码生成来构建graphql java 客户端的方案,但在编码实现和可拓展性上可能还有很多不足,仅供参考。

我们的SDK目前还处于开发阶段,可能后续还会有些更改。https://github.com/growingio/growingio-graphql-javasdk
https://github.com/growingio/graphql-java-codegen
https://github.com/kobylynskyi/graphql-java-codegen

graphql-java 是 GraphQLJava 实现。这个库的目标是用于真实的生产环境。graphql-java 解析和执行查询 GraphQL 。它并不真正获取任何数据的:数据来源于执行回调或提供静态数据。graphql-java 的 "hello world":import graphql.schema.GraphQLObjectType; import graphql.schema.GraphQLSchema; import static graphql.Scalars.GraphQLString; import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition; import static graphql.schema.GraphQLObjectType.newObject; public class HelloWorld {     public static void main(String[] args) {         GraphQLObjectType queryType = newObject()                         .name("helloWorldQuery")                         .field(newFieldDefinition()                                 .type(GraphQLString)                                 .name("hello")                                 .staticValue("world")                                 .build())                         .build();         GraphQLSchema schema = GraphQLSchema.newSchema()                         .query(queryType)                         .build();         Map result = new GraphQL(schema).execute("{hello}").getData();         System.out.println(result);         // Prints: {hello=world}     } } 标签:graphql
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值