继上次使用动态代理构建GraphQL的客户端之后,本次将使用纯粹的代码生成实现(也就是本次会为resolver接口在编译期间生成自己的实现,而不是通过代理来运行时构造实现)。
众所周知,动态代理有大量反射的使用,这可能存在潜在的性能影响(主要是指resolver 接口的动态代理调用),并且Builder模式虽然一定程度上简化了复杂对象的构造,但是对使用客户端的用户来说,过多的参数需要使用setXX方法,这同样增加了使用的难度。
所以这次将摒弃动态代理的方式,将全部代码使用类库生成。当然,这样也有一些缺点,包括
Jackson
。GraphQLRequest graphQLRequest = new GraphQLRequest(request, projection);
)。GraphQLRequest
对象就可以以HTTP POST的方式调用graphql server api了。这与动态代理的调用逻辑是相同的,可以参考上一篇。为了解决这个问题,在这里,我们在源库上引入了CodegenContext
来记录graphql document之间的映射关系。GrowingIOConfig
来为客户端使用者提供对外的一些配置信息。假设现有resolver接口AclsQueryResolver
:
public interface AclsQueryResolver {
java.util.List<UserAccessCtrlDto> acls(String resourceType) throws Exception;
}
现在,我们想要使用类库生成默认的实现类,生成的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。
//在动态代理方案时,我们这里使用反射获取方法参数的名称列表,其实是不够准确的
//因为参数名可能是经过处理的,比如,参数名是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 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 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 [email protected]
* @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