如何用 GraphQL+JavaPoet 把前端逼成瓶颈 (AKA: 如何炒了自己)

本贴最后更新于 1228 天前,其中的信息可能已经时过境迁

上篇文章中提到《DDD 探险——基于 GraphQL+Dgraph 实践》了一种假设

如果前端能通过领域模型进行数据操作,能否通过 GraphQL Schema 同时描述领域模型、API 接口以及数据库结构?

基于此假设提供了 Arc 框架,同时采用 Dgraph 作为数据存储。
既然我们已经有了 Schema,能否基于这个描述自动生成系统实现?通过降低开发成本、提升效率,达成快速迭代、试错领域模型的目的。

尝试通过代码生成器来完成这个假设。

代码已开源: https://github.com/YituHealthcare/Arc

1. GraphQL Schema -> Dgraphql Schema

1.1 概念:

  1. Dgraph Schema:Dgraph 数据库结构化语句,类似 mysql 中的 DDL
  2. predicate:Dgraph 的数据库字段,一个 predicate 可以被多个 type 使用
  3. domainClass:Arc 框架中定义 javaBean 类型,用于反序列化

1.2 方案

解析 GraphQL Schema 后,根据转换逻辑生成 Dgraph Schema

generator0dgraphpipline.png

注意 Arc 框架限制

  1. DB 结构中拥有框架依赖通用 predicate,如 domainClass
  2. 为了解决 predicatetype 定义问题,dgraph 中的 predicate 命名增加 type前缀

1.3 注意

  1. 需要框架提供根据 DgraphSchema 自动初始化数据库的能力

2. GraphQL Schema -> JavaCode

2.1 JavaPoet

JavaPoet is a Java API for generating .java source files.

通过 JavaPoet 可以方便、详细的描述 Java 源文件并生成。

看官方 issue 发现很多人希望提供快速生成 gettersetter 方法的方式,官方并未采纳,给出的回复是

Basically we're a tool for generating exactly what you tell us and not a tool for inferring code to generate. You can write a static method or helper class which can produce a field, getter, and setting in one shot onto a TypeSpec.Builder.

这个回复也明确了 JavaPoet 的定位。其实生成相关方法的方式非常简单

public class FieldSpecGenSetter implements Function<FieldSpec, MethodSpec> {

    @Override
    public MethodSpec apply(FieldSpec fieldSpec) {
        return MethodSpec.methodBuilder("set" + StringUtils.capitalize(fieldSpec.name))
                .addModifiers(Modifier.PUBLIC)
                .addParameter(fieldSpec.type, fieldSpec.name)
                .addStatement("this.$L = $L", fieldSpec.name, fieldSpec.name)
                .build();
    }

}

public class FieldSpecGenGetter implements Function<FieldSpec, MethodSpec> {

    @Override
    public MethodSpec apply(FieldSpec fieldSpec) {
        return MethodSpec.methodBuilder(GenSpecUtil.getGetterPrefix(fieldSpec) + StringUtils.capitalize(fieldSpec.name))
                .addModifiers(Modifier.PUBLIC)
                .addStatement("return $L", fieldSpec.name)
                .returns(fieldSpec.type)
                .build();
    }

}

提供一个生成 Builder 的示例,基本涵盖了全部定义

public class TypeSpecGenBuilder implements UnaryOperator<TypeSpec> {

    @Override
    public TypeSpec apply(TypeSpec source) {
        if (CollectionUtils.isEmpty(source.fieldSpecs)) {
            return source;
        } else {
            TypeSpec target = TypeSpec.classBuilder(source.name + "Builder").build();
            return source.toBuilder()
                    .addMethod(getAllArgsConstructor(source.fieldSpecs))
                    .addMethod(getNoArgsConstructor())
                    .addMethod(getBuilderInstanceMethod(target))
                    .addType(getBuilderType(source, target))
                    .build();
        }
    }

    private MethodSpec getAllArgsConstructor(List<FieldSpec> fieldSpecs) {
        MethodSpec.Builder builder = MethodSpec.constructorBuilder()
                .addModifiers(Modifier.PUBLIC)
                .addParameters(fieldSpecs.stream().map(f -> ParameterSpec.builder(f.type, f.name).build()).collect(Collectors.toList()));
        fieldSpecs.forEach(fieldSpec -> builder.addStatement("this.$L = $L", fieldSpec.name, fieldSpec.name));
        return builder.build();
    }

    private MethodSpec getNoArgsConstructor() {
        return MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC).build();
    }

    private MethodSpec getBuilderInstanceMethod(TypeSpec target) {
        return MethodSpec.methodBuilder("builder")
                .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                .returns(getType(target))
                .addStatement("return new $T()", getType(target))
                .build();
    }

    private TypeSpec getBuilderType(TypeSpec source, TypeSpec target) {
        return target.toBuilder()
                .addFields(source.fieldSpecs.stream().map(it -> FieldSpec.builder(it.type, it.name, Modifier.PRIVATE).build()).collect(Collectors.toList()))
                .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                .addMethod(MethodSpec.constructorBuilder().addModifiers(Modifier.PRIVATE).build())
                .addMethods(getBuilderSetMethodSpecList(source, target))
                .addMethod(getSourceInstanceMethod(source))
                .build();
    }

    private List<MethodSpec> getBuilderSetMethodSpecList(TypeSpec source, TypeSpec target) {
        return source.fieldSpecs.stream()
                .map(fieldSpec -> MethodSpec.methodBuilder(fieldSpec.name)
                        .addModifiers(Modifier.PUBLIC)
                        .returns(getType(target))
                        .addParameter(ParameterSpec.builder(fieldSpec.type, fieldSpec.name).build())
                        .addStatement("this.$L = $L", fieldSpec.name, fieldSpec.name)
                        .addStatement("return this")
                        .build()).collect(Collectors.toList());
    }

    private MethodSpec getSourceInstanceMethod(TypeSpec source) {
        return MethodSpec.methodBuilder("build")
                .addStatement("return new $L($L)", source.name, source.fieldSpecs.stream().map(f -> f.name).collect(Collectors.joining(",")))
                .addModifiers(Modifier.PUBLIC)
                .returns(ClassName.get("", source.name))
                .build();
    }

    private TypeName getType(TypeSpec typeSpec) {
        return ClassName.get("", typeSpec.name);
    }

}

2.2 实现

基于 graphql-java 解析 GraphqlSchema,通过 javapoet 按照 Arc 约束生成相关 java 文件

  • Graphql Enum ->dictionary
  • Graphql input ->input
  • Graphql type ->typeapirepository

2.3 注意

  1. 通过只提供 interface 方式,避免修改生成的代码。保障每次 schema 变更后都可以重新生成。
  2. 如果存在需要修改生成代码的场景,通过配置方式跳过相关 Java 源文件生成。

3. Maven Plugin

生成逻辑通过上述定义完成后,如何触发生成的动作?Maven 插件是一个不错的选择。
只需要继承 AbstractMojoOverride execute() 中调用相关生成方法即可。这里暂不展开如何开发 Maven 插件

@Mojo(name = "generate")
public class GeneratorMojo extends AbstractMojo {

    @Parameter(defaultValue = "${project}", readonly = true, required = true)
    private MavenProject project;

    @Parameter(defaultValue = "${basedir}/src/main/resources/arc-generator.json")
    private File configJson;
    @Parameter(defaultValue = "${basedir}/src/main/resources/graphql/schema.graphqls")
    private File schemaPath;
    @Parameter(property = "target", defaultValue = "all")
    private String target;

    private static final String TARGET_ALL = "all";
    private static final String TARGET_JAVA = "java";
    private static final String TARGET_DGRAPH = "dgraph";

    @Override
    public void execute() throws MojoExecutionException, MojoFailureException {
        final CodeGenConfig config = getConfig();
        if (TARGET_ALL.equalsIgnoreCase(target) || TARGET_JAVA.equalsIgnoreCase(target)) {
            generateJava(config);
        }
        if (TARGET_ALL.equalsIgnoreCase(target) || TARGET_DGRAPH.equalsIgnoreCase(target)) {
            generateDgraph(config);
        }
    }
}

然后再命令行执行

mvn arc:generate

即可按照配置及规则生成相关代码。也可以通过参数决定只生成 java 代码,不生成 Dgraph Schema

mvn arc:generate -Dtarget=java

4. 效果

4.0 添加 Maven 插件依赖

<build>
    <plugins>
        <plugin>
            <groupId>com.github.yituhealthcare</groupId>
            <artifactId>arc-maven-plugin</artifactId>
            <version>1.4.0</version>
        </plugin>
    </plugins>
</build>

4.1 创建 schema. 默认路径为 resources:graphql/schema.graphqls

scalar DateTime

schema{
    query: Query,
    mutation: Mutation
}

type Query{
    project(
        id: String
    ): Project
    users: [User]
}

type Mutation{
    createProject(
        payload: ProjectInput
    ): Project
    createMilestone(
        payload: MilestoneInput
    ): Milestone
}

"""
项目分类
"""
enum ProjectCategory {
    """
    示例项目
    """
    DEMO
    """
    生产项目
    """
    PRODUCTION
}

"""
名称
为了达到某个产品迭代、产品模块开发、或者科研调研等目的所做的工作.
"""
type Project{
    id: String!
    name: String!
    description: String!
    category: ProjectCategory
    createTime: DateTime!
    milestones(
        status: MilestoneStatus
    ): [Milestone]
}
"""
里程碑
表述一个Project的某个时间阶段及阶段性目标. 一个Project可以同时拥有多个处于相同或者不同阶段的Milestone.
"""
type Milestone{
    id: String!
    name: String!
    status: MilestoneStatus
}

type User {
    name: String!
}

"""
里程碑状态
"""
enum MilestoneStatus{
    """
    未开始
    """
    NOT_STARTED,
    """
    进行中
    """
    DOING,
    """
    发布
    """
    RELEASE,
    """
    关闭
    """
    CLOSE
}

input ProjectInput{
    name: String!
    description: String!
    vendorBranches: [String!]!
    category: ProjectCategory!
}


input MilestoneInput{
    projectId: String!
    name: String!
}

4.2 新建配置文件, 默认路径为 resources:arc-generator.json

{
  "basePackage": "com.github.yituhealthcare.arcgeneratorsample",
  "dropAll": false,
  "genStrategies": [
    {
      "codeGenOperation": "SKIP_IF_EXISTED",
      "codeGenType": "REPO"
    },
    {
      "codeGenOperation": "OVERRIDE",
      "codeGenType": "API"
    }
  ],
  "ignoreJavaFileNames": [
    "User"
  ],
  "dgraphPath": "dgraph/schema.dgraph"
}

4.3 执行命令行

generator6terminal.jpeg

4.4 代码生成

generator7generated.jpeg

在聊清楚 schema 后,只需要执行一行命令,然后编写一个实现类即可完成接口服务的开发。在 schema 修改后,重复执行这个过程。

5. 缺陷

5.1 GraphQL 描述力不够。只描述来数据结构与类型,没有描述数据存储可能会用到的主键、索引、缓存等信息,以及 DDD 中的限界上下文(BoundedContext)、聚合根(AggregateRoot)等概念

可以通过自定义 directive 实现相关扩展定义,比如:

scalar DateTime
directive @Cache on FIELD_DEFINITION
directive @Alloftext on FIELD_DEFINITION
directive @AggregateRoot on OBJECT

type Project @AggregateRoot {
    id: String!
    name: String! @Cache @Alloftext
    description: String!
    createTime: DateTime!
}

但是这个时候 QL 已经变成了 DSL,需要考虑额外的学习、推广成本

5.2 生成的代码和实现代码混合,如何进行 code review?

schema 和生成的代码先提交一次 PR,对 schema 进行 review。之后的实现代码再单独提交 PR

5.3 Dgraph 更新时数据如何处理?

已经上线后无法自动迭代。数据版本迁移不应属于开发过程

5.4 仍需编写实现类(生成的代码是否允许编辑?)

理想情况是完全通过代码生成完成相关实现。但是实际业务并不是简单的 crud,如果通过 schema 描述复杂业务,就会发现本质上是通过 schema 来编写另一种 java 代码。所以只生成 interface,需要开发者补全相关实现。让 schema 专注于描述 DDD。不排除提供生成简单 crud 实现的功能,在修改了相关实现类后,再通过配置 SKIP_IF_EXISTED 跳过相关实现

5.5 自动化生成后发现无事可做,不可替代性如何保障?

-_-#

  • DDD

    领域驱动设计。

    20 引用 • 2 回帖 • 2 关注
  • GraphQL

    GraphQL 是一个用于 API 的查询语言,是一个使用基于类型系统来执行查询的服务端运行时(类型系统由你的数据定义)。GraphQL 并没有和任何特定数据库或者存储引擎绑定,而是依靠你现有的代码和数据支撑。

    4 引用 • 3 回帖 • 22 关注
  • Dgraph
    2 引用 • 1 回帖
  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3169 引用 • 8207 回帖 • 1 关注
2 操作
crick77 在 2020-12-21 10:33:33 置顶了该帖
crick77 在 2020-12-21 00:14:19 更新了该帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...