整合 GrpahQL

GraphQL 简介

GraphQL 提供了一种灵活、高效的数据查询方式,广泛应用于现代 web 和移动应用开发中。

GraphQL 是一种用于 API 的查询语言以及服务器端的运行环境,它通过给予客户端请求所需数据的能力来提供更高效、灵活的交互。GraphQL 由 Facebook 于 2012 年开发,并于 2015 年开源。与传统的 REST API 不同,GraphQL 允许客户端明确指定需要的字段,从而避免了过多的数据传输。

主要特点

  1. 单一端点:

    • GraphQL 使用一个单一的 URL 进行所有请求,而不是像 REST API 那样有多个端点。
  2. 明确请求:

    • 客户端可以明确请求所需的数据结构,只获取需要的数据,不多也不少。
  3. 实时更新:

    • GraphQL 支持订阅(subscriptions),允许客户端在数据变化时实时接收更新。
  4. 强类型系统:

    • GraphQL 使用强类型系统定义 API,Schema 明确了可查询的类型及其关系。
  5. 灵活的查询:

    • 客户端可以在一次请求中查询多个资源,而不是像 REST API 那样可能需要多次请求。

GraphQL 基本概念

  1. Schema:

    • GraphQL Schema 定义了 API 的结构,包括类型(types)、查询(queries)、变更(mutations)以及订阅(subscriptions)。它是整个 GraphQL 服务的核心。
  2. Queries:

    • Queries 是用于读取数据的请求。在 GraphQL 中,查询类似于 REST 的 GET 请求,但更为灵活。
    query {
      user(id: "1") {
        id
        name
        email
      }
    }
    
  3. Mutations:

    • Mutations 是用于修改数据的请求,与 REST 的 POST、PUT、DELETE 请求类似。
    mutation {
      createUser(name: "John Doe", email: "john@example.com") {
        id
        name
      }
    }
    
  4. Subscriptions:

    • Subscriptions 允许客户端订阅特定事件的变化,适用于实时更新。
    subscription {
      newUser {
        id
        name
      }
    }
    
  5. Resolvers:

    • Resolvers 是服务器端的函数,负责处理查询或变更请求,并返回相应的数据。每个字段都有一个对应的 resolver 函数。

整合示例

添加依赖

<ApiTable.version>1.3.0</ApiTable.version>

<dependency>
  <groupId>com.litongjava</groupId>
  <artifactId>ApiTable</artifactId>
  <version>${ApiTable.version}</version>
</dependency>

<dependency>
  <groupId>com.graphql-java</groupId>
  <artifactId>graphql-java</artifactId>
  <version>17.3</version>
</dependency>

实体类 User

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
  private String id;
  private String name;
  private String email;
}

这是一个简单的 Java 类,表示用户对象。它具有 id、name 和 email 属性,并使用 Lombok 库自动生成 getter 和 setter 方法。

UserResolver

public class UserResolver {
  public User getUser(String id) {
    // 在这里实现数据获取逻辑
    return new User(id, "John Doe", "john@example.com");
  }
}

UserResolver 类包含了一个方法 getUser,它接受一个 id 参数并返回一个 User 对象。在实际应用中,你需要实现数据获取逻辑,可能会从数据库或其他数据源中获取用户信息。

GraphQLHandler

import java.util.Map;

import com.litongjava.data.utils.TioRequestParamUtils;
import com.litongjava.jfinal.plugin.graphql.GQL;
import com.litongjava.tio.boot.http.TioControllerContext;
import com.litongjava.tio.http.common.HttpRequest;
import com.litongjava.tio.http.common.HttpResponse;
import com.litongjava.tio.utils.resp.Resp;

import graphql.ExecutionInput;
import graphql.ExecutionResult;

public class GraphQLHandler {

  public HttpResponse graphql(HttpRequest httpRequest) {
    // 解析请求参数
    Map<String, Object> requestMap = TioRequestParamUtils.getRequestMap(httpRequest);
    String query = (String) requestMap.get("query");
    Map<String, Object> variables = (Map<String, Object>) requestMap.get("variables");

    // 构建GraphQL执行输入
    ExecutionInput executionInput = ExecutionInput.newExecutionInput().query(query).variables(variables).build();

    // 执行GraphQL查询
    ExecutionResult executionResult = GQL.execute(executionInput);
    Map<String, Object> specification = executionResult.toSpecification();

    // 构建响应
    Resp resp = null;
    Object errors = specification.get("errors");
    if (errors != null) {
      resp = Resp.fail(errors);
    } else {
      Object data = specification.get("data");
      resp = Resp.ok(data);
    }

    //响应json数据
    HttpResponse response = TioControllerContext.getResponse();
    return response.setJson(resp);
  }

}

GraphQLHandler 类包含一个方法 graphql,它接受一个 HttpRequest 对象并处理 GraphQL 查询。它解析请求中的查询和变量,然后使用 GQL.execute 方法执行 GraphQL 查询,并将结果转换为符合预期的响应格式。

schema.graphqls

type Query {
    user(id: ID!): User
}

type User {
    id: ID!
    name: String!
    email: String!
}

schema.graphqls 文件中的定义是 GraphQL 的模式定义语言(Schema Definition Language,SDL),用于定义 GraphQL 的数据模式(schema)。这些定义描述了数据类型、查询字段、输入参数等,为客户端和服务器之间的数据交互提供了结构化的约定。

示例中的 schema.graphqls 文件定义了一个 Query 类型,其中包含一个名为 user 的查询字段,该字段接受一个 ID 类型的参数 id,并返回一个 User 类型的对象。User 类型包含了 idnameemail 字段,分别对应用户的标识、姓名和电子邮件。

这些定义为客户端提供了一种清晰地了解服务器支持的数据类型和操作的方式。客户端可以根据这些定义构建有效的查询,并与服务器进行交互。

GraphQLConfig

import java.io.IOException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.function.UnaryOperator;

import com.litongjava.jfinal.plugin.graphql.GQL;
import com.litongjava.tio.boot.demo.graphql.resolver.UserResolver;
import com.litongjava.tio.utils.hutool.FileUtil;
import com.litongjava.tio.utils.hutool.ResourceUtil;

import graphql.GraphQL;
import graphql.schema.DataFetcher;
import graphql.schema.GraphQLSchema;
import graphql.schema.idl.RuntimeWiring;
import graphql.schema.idl.SchemaGenerator;
import graphql.schema.idl.SchemaParser;
import graphql.schema.idl.TypeDefinitionRegistry;
import graphql.schema.idl.TypeRuntimeWiring;

public class GraphQLConfig {

  public void graphQL() throws IOException {
    // 读取GraphQL模式定义文件
    URL resource = ResourceUtil.getResource("schema.graphqls");
    StringBuilder text = FileUtil.readTextFromURL(resource);
    String sdl = text.toString();

    // 解析GraphQL模式定义文件并构建GraphQLSchema
    TypeDefinitionRegistry typeRegistry = new SchemaParser().parse(sdl);
    RuntimeWiring wiring = buildWiring();
    GraphQLSchema schema = new SchemaGenerator().makeExecutableSchema(typeRegistry, wiring);

    // 创建GraphQL实例并设置到GQL类中
    GraphQL grphql = GraphQL.newGraphQL(schema).build();
    GQL.setGraphQL(grphql);
  }

  private RuntimeWiring buildWiring() {
    // 创建数据获取器映射
    Map<String, DataFetcher> dataFetchersMap = new HashMap<>();
    DataFetcher<?> userFetcher = env -> new UserResolver().getUser(env.getArgument("id"));
    dataFetchersMap.put("user", userFetcher);

    // 创建类型运行时绑定
    UnaryOperator<TypeRuntimeWiring.Builder> builderFunction = typeWiring -> {
      return typeWiring.dataFetchers(dataFetchersMap);
    };

    // 创建运行时绑定并返回
    graphql.schema.idl.RuntimeWiring.Builder runtimeWiring = RuntimeWiring.newRuntimeWiring();
    return runtimeWiring.type("Query", builderFunction).build();
  }
}

GraphQLConfig 类负责配置 GraphQL。它从文件中读取 GraphQL 模式定义,解析该定义并构建 GraphQLSchema。同时,它构建了数据获取器映射,将每个查询字段与相应的数据获取器关联起来。

HttpRequestHanlderConfig

import com.litongjava.tio.boot.demo.graphql.handler.GraphQLHandler;
import com.litongjava.tio.boot.server.TioBootServer;
import com.litongjava.tio.http.server.router.HttpReqeustSimpleHandlerRoute;
public class HttpRequestHanlderConfig {

  public void config() {
    // 获取router
    HttpReqeustSimpleHandlerRoute r = TioBootServer.me().getHttpReqeustSimpleHandlerRoute();
    GraphQLHandler graphQLHandler = new GraphQLHandler();

    // 将GraphQLHandler注册到路由中
    r.add("/graphql", graphQLHandler::graphql);
  }

}

HttpRequestHanlderConfig 类用于配置 HTTP 请求处理程序。它将 GraphQLHandler 注册到路由中,使得可以通过 /graphql 路径访问 GraphQL 查询。

GraphqlServerConfig

import java.io.IOException;

import com.litongjava.tio.boot.context.TioBootConfiguration;
public class GraphqlServerConfig implements TioBootConfiguration {

  @Override
  public void config() throws IOException {
    // 配置HTTP请求处理程序和GraphQL
    new HttpRequestHanlderConfig().config();
    new GraphQLConfig().graphQL();
  }
}

GraphqlServerConfig 类实现了 TioBootConfiguration 接口,负责配置 GraphQL 服务器。在 config 方法中,它依次配置了 HTTP 请求处理程序和 GraphQL。

启动类 GraphqlServer

import com.litongjava.jfinal.aop.annotation.AComponentScan;
import com.litongjava.tio.boot.TioApplication;
import com.litongjava.tio.boot.demo.graphql.config.GraphqlServerConfig;

@AComponentScan
public class GraphqlServer {

  public static void main(String[] args) {
    // 启动GraphQL服务器
    long start = System.currentTimeMillis();
    TioApplication.run(GraphqlServer.class, new GraphqlServerConfig(), args);
    long end = System.currentTimeMillis();
    System.out.println((end - start) + "ms");
  }
}

GraphqlServer 类是 GraphQL 服务器的启动类。它使用 TioApplication.run 方法启动服务器,并输出启动时间。

测试请求

请求数据

http://localhost/graphql

{
  "query": "query GetUser($userId: ID!) { user(id: $userId) { id name email }}",
  "variables": {
    "userId": "1"
  }
}

响应数据

成功示例

{
  "data": {
    "user": {
      "id": "1",
      "name": "John Doe",
      "email": "john@example.com"
    }
  },
  "code": null,
  "msg": null,
  "ok": true
}

这个查询字符串是一个 GraphQL 查询,用于获取特定用户的信息。让我们逐步解释每个部分的含义:

  1. "query": 这是一个关键字,用于指示这是一个查询操作。在 GraphQL 中,"query" 关键字用于指示客户端要从服务器获取数据。

  2. "GetUser": 这是查询的名称,你可以为查询定义一个名称以便在代码中引用它。在这里,查询名称是 "GetUser"。

  3. "($userId: ID!)": 这是查询的参数列表。在这里,我们定义了一个名为 "userId" 的参数,它的类型是 ID,表示一个唯一的标识符。"!" 表示这个参数是非空的,也就是说在执行查询时必须提供一个有效的 ID 值。

  4. "{ user(id: $userId) { id name email }}": 这是实际的查询操作。在这里,我们调用了一个名为 "user" 的查询字段,并传入了一个参数 "id",这个参数的值来自于变量中的 "userId"。然后,我们指定了要从用户对象中获取的字段,包括 "id"、"name" 和 "email"。

  5. "variables": 这是一个包含变量值的对象。在这里,我们为查询中的 "userId" 参数提供了一个值为 "1" 的变量。这个变量值将在查询执行时被替换到相应的位置上。

综上所述,这个查询的意义是:执行一个名为 "GetUser" 的查询,传入一个 "userId" 参数,并从服务器获取对应用户的 ID、姓名和电子邮件。

失败示例

{
  "data": [
    {
      "message": "Invalid Syntax : offending token '}' at line 1 column 30",
      "locations": [
        {
          "line": 1,
          "column": 30
        }
      ],
      "extensions": {
        "classification": "InvalidSyntax"
      }
    }
  ],
  "ok": false,
  "code": null,
  "msg": null
}

请求和处理过程

发送这些数据会执行 UserResolver 中的 getUser 方法是因为在 GraphQL 的配置中,已经将 user 查询字段与对应的数据获取器(DataFetcher)关联起来了。

在 GraphQL 的配置中,有一个 RuntimeWiring 对象,它定义了如何将查询字段映射到具体的数据获取器。在你的代码中,buildWiring 方法创建了一个 RuntimeWiring 对象,并将 user 查询字段与 userFetcher 数据获取器关联起来。这样,当收到一个包含 user 查询的 GraphQL 请求时,GraphQL 引擎会使用 userFetcher 来获取相应的数据。

工作流大致如下:

  1. 当收到一个包含 user 查询的 GraphQL 请求时,GraphQLHandler 中的 graphql 方法被调用。
  2. graphql 方法中,解析请求的参数,并构建一个 ExecutionInput 对象,其中包含查询字符串和变量。
  3. 使用 GQL.execute 方法执行查询。
  4. 在执行过程中,GraphQL 引擎根据配置中的 RuntimeWiring 将查询字段映射到对应的数据获取器。
  5. 对于 user 查询字段,引擎会调用 userFetcher 数据获取器,并传递相应的参数(这里是用户的 id)。
  6. userFetcher 中的逻辑会根据传入的 id 获取对应的用户信息,并返回一个 User 对象。
  7. 引擎将获取到的数据包装成响应,发送给客户端。

这样,就实现了将 GraphQL 查询映射到具体的数据获取逻辑的过程。