网络传输架构之GraphQL讲解

文章目录

  • [1 GraphQL架构风格](#1 GraphQL架构风格)
    • [1.1 简介](#1.1 简介)
    • [1.2 GraphQL执行流程](#1.2 GraphQL执行流程)
    • [1.3 相关语法](#1.3 相关语法)
      • [1.3.1 Query(查询)](#1.3.1 Query(查询))
      • [1.3.2 Mutation(变更)](#1.3.2 Mutation(变更))
      • [1.3.3 Subscription(订阅)](#1.3.3 Subscription(订阅))
      • [1.3.4 变量系统(Variable)](#1.3.4 变量系统(Variable))
      • [1.3.5 指令(Directive)](#1.3.5 指令(Directive))
      • [1.3.6 Introspection(自省)元字段](#1.3.6 Introspection(自省)元字段)
      • [1.3.7 错误与响应格式](#1.3.7 错误与响应格式)
    • [1.4 服务端操作](#1.4 服务端操作)
      • [1.4.1 配置](#1.4.1 配置)
        • [1.4.1.1 pom.xml](#1.4.1.1 pom.xml)
        • [1.4.1.2 yml配置](#1.4.1.2 yml配置)
        • [1.4.1.1 Schema](#1.4.1.1 Schema)
      • [1.4.2 业务操作](#1.4.2 业务操作)
      • [1.4.3 测试示例](#1.4.3 测试示例)
      • [1.4.4 原生操作](#1.4.4 原生操作)
        • [1.4.4.1 配置](#1.4.4.1 配置)
        • [1.4.4.2 解析配置](#1.4.4.2 解析配置)
        • [1.4.4.3 注册servlet](#1.4.4.3 注册servlet)
        • [1.4.4.4 测试](#1.4.4.4 测试)

1 GraphQL架构风格

1.1 简介

有些小伙伴在工作中可能遇到过这样的场景:移动端只需要用户的姓名和邮箱,但REST API返回了用户的所有信息,造成数据传输浪费。

GraphQL正是为了解决这个问题而生的。

GraphQL包含三个核心组件:

  • Schema定义:强类型系统描述API能力
  • 查询语言:客户端精确请求需要的数据
  • 执行引擎:解析查询并返回结果

GraphQL的优缺点分析

  • 优点:
    精确的数据获取,避免过度获取
    单一端点,减少HTTP连接开销
    强类型系统,自动生成文档
    前端主导数据需求
  • 缺点:
    查询复杂度控制困难
    缓存实现复杂(HTTP缓存失效)
    N+1查询问题需要额外处理
    学习曲线相对陡峭

1.2 GraphQL执行流程

1.3 相关语法

GraphQL 操作语法其实只有 3 种顶层类型:QueryMutationSubscription,但写进纸面的"语法单元"有 10 来种。

可到此处进行验证语法:https://countries.trevorblades.com/

3 种根操作(Operation):

  • Query:只读
  • Mutation:写操作
  • Subscription:实时推送(基于 WebSocket)

语法模板,方括号代表可省

java 复制代码
operationType [operationName] ( [variableDefinitions] ) {
  selectionSet
}

1.3.1 Query(查询)

最简单字段列表

js 复制代码
{
  user(id: 4) {
    name
    avatar
  }
}

起个操作名(方便调试/日志)

js 复制代码
query GetUser {
  user(id: 4) {
    name
  }
}

传变量(推荐,避免字符串拼接)

js 复制代码
query GetUser($uid: ID!) {   # 变量定义
  user(id: $uid) {
    name
    posts {                  # 嵌套对象
      title
      comments(first: 3) {   # 分页实参
        body
      }
    }
  }
}

此处ID表示是类型,加感叹号!区别:

写法 含义 举例
ID 可以是 null 的 ID "user-123"null
ID! 绝对不能为 null 的 ID 必须传 "user-123"传 null 或干脆不传都会报错

对于$使用变量声明时才用

位置 写法 角色
形参列表 ($uid: ID!) 变量声明------告诉 GraphQL"等会儿外部会传个变量叫 uid"
查询体内 user(id: $uid) 变量使用------把刚才声明的那个变量插进来

别名(同一字段查两次)

js 复制代码
{
  smallPic: user(id: 4) { avatar(size: 64)  }
  bigPic:   user(id: 4) { avatar(size: 512) }
}

Fragment(复用片段)

js 复制代码
//必须指向 on User 表示 这套字段只能展开在 User 对象上
fragment AvatarInfo on User {
  name
  avatar
}

query {
  u1: user(id: 4) { ...AvatarInfo }
  u2: user(id: 5) { ...AvatarInfo }
}

内联片段(接口/联合类型),... on Type { fields } inline fragment(内联片段),按类型挑选字段

js 复制代码
{
  search(keyword: "hero") {
  // GraphQL 内置字段,任何对象里都能拿,返回当前对象的真实类型名(字符串)
    __typename 
    ... on Movie { title }
    ... on Book  { author }
  }
}

1.3.2 Mutation(变更)

创建 + 返回结果

js 复制代码
//mutation  声明"我要改"  CreatePost 本次操作的命名
mutation CreatePost($input: PostInput!) {
  // schema 里定义的 mutation 字段
  createPost(input: $input) { 
  	// 要返回什么字段
    id
    title
    author { name }
  }
}

多个写操作顺序执行(GraphQL 保证串行)

js 复制代码
mutation {
  addComment(postId: 1, body: "nice") { id }
  updatePost(id: 1, views: +1) { views }
}

1.3.3 Subscription(订阅)

语法与 Query 相同,但由服务器推回

js 复制代码
subscription MessageAdded($room: ID!) {
  messageAdded(roomId: $room) {
    from { name }
    content
    createdAt
  }
}

客户端通过 WebSocket 发送该帧,服务器在有人发言时回相同结构数据。

1.3.4 变量系统(Variable)

声明:$var: Type = defaultValue,类型后加 ! 代表非空。
List / InputObject

js 复制代码
query($ids: [ID!]!) {
  users(ids: $ids) { name }
}

1.3.5 指令(Directive)

@skip(if: Boolean):如果为 true,就跳过这块,@include(if: Boolean):只有为 true 才保留这块,同时参数 if 必须是 Boolean!(非空布尔),变量、字面量都可以

js 复制代码
query GetUser($skipAvatar: Boolean!) {
  user(id: 4) {
    name
    avatar @skip(if: $skipAvatar)
  }
}

1.3.6 Introspection(自省)元字段

Meta-field(元字段),带 __ 的字段不会出现在要写的 schema 里,但任何对象都能查询它们:

  • __typename:运行期真实类型,内置字段,任何对象里都能拿
  • __schema:整个 schema 的"根目录",挂在 最顶层的 Query 隐式字段
  • __type(name: "User"): 按名取类型详情,同样挂在顶层,用来查询某一个具体类型 的字段、枚举值、可能的接口实现等
js 复制代码
{
  __schema {
    types {
      name
      kind
      fields { name type { name } }
    }
  }
}

1.3.7 错误与响应格式

GraphQL 总是 HTTP 200,错误放在 errors 数组:

json 复制代码
{
  "data": { "user": null },
  "errors": [{
    "message": "User not found",
    "path": ["user"],
    "extensions": { "code": "USER_404" }
  }]
}

1.4 服务端操作

1.4.1 配置

1.4.1.1 pom.xml

springboot 2.x 主要是 接口方式 比如 QueryResolver

xml 复制代码
<dependency>
  <groupId>com.graphql-java-kickstart</groupId>
   <artifactId>graphql-spring-boot-starter</artifactId>
   <version>11.1.0</version>
</dependency>
<!-- 测试客户端:GraphiQL(浏览器调试工具) -->
<dependency>
    <groupId>com.graphql-java-kickstart</groupId>
    <artifactId>graphiql-spring-boot-starter</artifactId>
    <version>11.1.0</version>
</dependency>

spring 3.x 自带graphql 主要使用注解方式@SchemaMapping、@QueryMapping 注解方式而不是 QueryResolver

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-graphql</artifactId>
</dependency>
1.4.1.2 yml配置
yml 复制代码
graphql:
  servlet:
  	# graphql 访问地址
    mapping: /graphql
    auto-register: true
  tools:
  	# graphql 一般存放resource 下的 graphql 文件夹以 .graphqls 结尾
    schema-location-pattern: graphql/**/schema*.graphqls

# 浏览器内使用 GraphiQL 启用(默认 true)
graphiql:
  enabled: true
  mapping: /graphiql
1.4.1.1 Schema
js 复制代码
type Query { //服务端定义而非客户端的 query 定义
  userById(id: ID!): User
  users: [User]
}

type User {
  id: ID
  userName: String
  phoneNumer: String
  remark: String
}


type Mutation {
  addUser(userName: String!, phoneNumer: String!): Boolean  # 添加
}

1.4.2 业务操作

java 复制代码
//查询类
@Component
public class UserQuery implements GraphQLQueryResolver {

    @Autowired
    private UserService userService;

    public List<UserEntity> users() {
        return userService.list();
    }

    public UserEntity userById(String id) {
        return userService.getById(id);
    }
}

//新增类
@Component
public class UserMutation implements GraphQLMutationResolver {
    @Autowired
    private UserService userService;


    public Boolean addUser(String userName, String phoneNumer) {
        UserEntity book = new UserEntity();
        book.setUserName(userName);
        book.setPhoneNumer(phoneNumer);
        return userService.save(book);
    }
}

1.4.3 测试示例

查询graphql schema定义:

js 复制代码
query userList{
  users{
    id
    userName
    phoneNumer
    remark
  }
}

结果:
{
  "data": {
    "users": [
      {
        "id": "1",
        "userName": "哈哈",
        "phoneNumer": "1234556789",
        "remark": "1"
      }
    ]
  }
}

新增类示例:

js 复制代码
mutation addUser{
  addUser(userName: "小明", phoneNumer: "123456879465")
}
结果:
{
  "data": {
    "addUser": true
  }
}

1.4.4 原生操作

1.4.4.1 配置

pom.xml

xml 复制代码
<dependency>
   <groupId>com.graphql-java</groupId>
    <artifactId>graphql-java</artifactId>
    <version>24.3</version>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

schema如下

js 复制代码
type Query {
  userById(id: ID!): User
  users: [User]
}
type User {
  id: ID
  userName: String
  phoneNumer: String
  remark: String
}
type Mutation {
  addUser(userName: String!, phoneNumer: String!): Boolean  # 添加
}
1.4.4.2 解析配置
java 复制代码
@Configuration
public class GraphQLProvider {
    private GraphQL graphQL;

    @Autowired
    private UserService userService;   // 你的业务层

    @Bean
    public GraphQL graphQL() {
        return graphQL;
    }

    @PostConstruct
    public void init() throws IOException {
        // 1. 自动加载 resources/graphql 目录下所有 .graphqls 文件
        SchemaParser schemaParser = new SchemaParser();
        TypeDefinitionRegistry typeRegistry = new TypeDefinitionRegistry();

        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        Resource[] resources = resolver.getResources("classpath:graphql/**/*.graphqls");

        for (Resource resource : resources) {
            String sdl = StreamUtils.copyToString(resource.getInputStream(), StandardCharsets.UTF_8);
            typeRegistry.merge(schemaParser.parse(sdl));
        }

        // 2. 绑定 DataFetcher(这就是替代 GraphQLQueryResolver 的方式)
        RuntimeWiring runtimeWiring = RuntimeWiring.newRuntimeWiring()
                .type("Query", builder -> builder
                        .dataFetcher("users", env -> userService.list())
                        .dataFetcher("userById", env -> {
                            String id = env.getArgument("id");
                            return userService.getById(id);
                        })
                )
                .type("Mutation", builder -> builder
                        .dataFetcher("addUser", env -> {
                            String userName = env.getArgument("userName");
                            String phoneNumer = env.getArgument("phoneNumer");
                            UserEntity user = new UserEntity();
                            user.setUserName(userName);
                            user.setPhoneNumer(phoneNumer);
                            return userService.save(user);
                        })
                )
                .build();

        // 3. 生成可执行 schema
        SchemaGenerator schemaGenerator = new SchemaGenerator();
        GraphQLSchema graphQLSchema = schemaGenerator.makeExecutableSchema(
                SchemaGenerator.Options.defaultOptions(), // 可自定义严格校验等
                typeRegistry,
                runtimeWiring
        );

        // 4. 创建 GraphQL 实例
        this.graphQL = GraphQL.newGraphQL(graphQLSchema).build();
    }

}
1.4.4.3 注册servlet
java 复制代码
@WebServlet(urlPatterns = "/graphql")
public class GraphQLServletConfig extends HttpServlet {

    private final GraphQL graphQL;  // 从 GraphQLProvider 注入,或静态持有

    public GraphQLServletConfig(GraphQL graphQL) {
        this.graphQL = graphQL;
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        // 1. 解析请求 body(GraphQL query JSON)
        String requestBody = req.getReader().lines().collect(Collectors.joining());
        Map<String, Object> input = new ObjectMapper().readValue(requestBody, Map.class);
        String query = (String) input.get("query");
        Map<String, Object> variables = (Map<String, Object>) input.getOrDefault("variables", Collections.emptyMap());

        // 2. 执行 GraphQL
        ExecutionInput executionInput = ExecutionInput.newExecutionInput()
                .query(query)
                .variables(variables)
                .build();
        ExecutionResult executionResult = graphQL.execute(executionInput);

        // 3. 返回 JSON 响应
        resp.setContentType("application/json");
        resp.setCharacterEncoding("UTF-8");
        resp.getWriter().write(new ObjectMapper().writeValueAsString(executionResult.toSpecification()));
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        // 支持 GET(可选,GraphQL 规范允许)
        doPost(req, resp);
    }
}

注意:启动类别忘了@ServletComponentScan

1.4.4.4 测试

用bruno 模拟测试

js 复制代码
查询示例
query {
  users {
    id
    userName
    phoneNumer
    remark
  }
}

修改示例
mutation {
  addUser(
    userName:"小王"
    phoneNumer:"987654321"
  )  
}
相关推荐
稚辉君.MCA_P8_Java3 小时前
Gemini永久会员 containerd部署java项目 kubernetes集群
后端·spring cloud·云原生·容器·kubernetes
yihuiComeOn3 小时前
[源码系列:手写Spring] AOP第二节:JDK动态代理 - 当AOP遇见动态代理的浪漫邂逅
java·后端·spring
e***71674 小时前
Spring Boot项目接收前端参数的11种方式
前端·spring boot·后端
程序猿小蒜4 小时前
基于springboot的的学生干部管理系统开发与设计
java·前端·spring boot·后端·spring
q***56385 小时前
Spring容器初始化扩展点:ApplicationContextInitializer
java·后端·spring
菜鸟‍5 小时前
【后端学习】MySQL数据库
数据库·后端·学习·mysql
Curvatureflight5 小时前
GPT-4o Realtime 之后:全双工语音大模型如何改变下一代人机交互?
人工智能·语言模型·架构·人机交互
Codebee6 小时前
30 分钟落地全栈交互:OneCode CLI+SVG 排课表实战
后端
TechTrek6 小时前
Spring Boot 4.0正式发布了
java·spring boot·后端·spring boot 4.0