-->
侧边栏壁纸
博主头像
断钩鱼 博主等级

行动起来,活在当下

  • 累计撰写 28 篇文章
  • 累计创建 34 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

Spring Graphql 初体验

halt
2022-01-05 / 0 评论 / 0 点赞 / 3107 阅读 / 0 字

Spring Graphql 初体验

代码地址:https://duangouyu.coding.net/p/demo/d/spring-graphql/git/tree/webflux

Graphql 是什么

中文官网:https://graphql.cn/learn/

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

SpringGraphql 是什么

官网:https://spring.io/projects/spring-graphql

Spring GraphQL 是 GraphQL Java 团队的 GraphQL Java Spring 项目的继承者。它将成为所有 Spring、GraphQL 应用程序的基础。

搭建运行环境

SpringGraphql 正式版 对于 SpringBoot 的要求大于 2.7.0.m1

image-20220105105438503

如果小于这个版本的springboot 可以使用 实验版本

<dependency>
 <groupId>org.springframework.experimental</groupId>
 <artifactId>graphql-spring-boot-starter</artifactId>
 <version>1.0.0-M4</version>
</dependency>

build.gradle

plugins {
    id 'org.springframework.boot' version '2.7.0-SNAPSHOT'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
}

group = 'top.mengshuo'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenLocal()
    mavenCentral()
    maven { url 'https://repo.spring.io/milestone' }
    maven { url 'https://repo.spring.io/snapshot' }
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-r2dbc'
    implementation 'org.springframework.boot:spring-boot-starter-graphql'
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'com.h2database:h2'
    runtimeOnly 'io.r2dbc:r2dbc-h2'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework:spring-webflux'
    testImplementation 'org.springframework.graphql:spring-graphql-test'
    testImplementation 'io.projectreactor:reactor-test'

}

test {
    useJUnitPlatform()
}

schema.graphqls

在 resources 目录下创建 graphql 目录 添加 schema.graphqls 文件

此文件用于定义与客户端交互的接口信息 具体规范参考:https://graphql.cn/learn/schema/

graphql 一共有三种请求类型

query:查询

mutation:更改

subscription:订阅

# Query 定义查询操作
type Query {
    userList: [User]
    byId( id: ID!): User
}

# Mutation 定义更新操作
type Mutation {
    addUser(user: InUser!) :User
}

# Subscription 订阅类型
type Subscription {
    sub: User
}

# 定义输出的数据类型
type User {
    id: ID!
    name: String!
    phone: String!
    # 返回的数据中不包含这个 所以会子查询
    tags: [Tag]
}

type Tag {
    id: ID!
    name: String!
}

# 定义输入的数据类型
input InUser {
    name: String!
    phone: String!
}

application.yml

server:
  port: 8081
spring:
  graphql:
    # 指定 websocket 端点
    websocket:
      path: /graphql
    # 默认访问端点
    path: /graphql
    # 网页测试
    graphiql:
      # 是否启用
      enabled: true
      # 访问地址
      path: /graphiql
    # 打印请求内容
    schema:
      printer:
        enabled: true
  datasource:
    driver-class-name: org.h2.Driver
    url: r2dbc:h2:file:./h2/db/graphql;MODE=MySQL
    username: root
    password: root
  h2:
    console:
      # 是否允许网页访问,默认false
      enabled: true
      # h2数据库的访问路径:http://localhost:8080/h2(默认为/h2-console)
      path: /h2
      settings:
        # 是否允许从其他地方访问,还是只允许从本地访问
        web-allow-others: true
  main:
    allow-bean-definition-overriding: true

controller

完整代码请查看 git

package top.mengshuo.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.graphql.data.method.annotation.*;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import top.mengshuo.entity.Tag;
import top.mengshuo.entity.User;
import top.mengshuo.entity.UserTag;
import top.mengshuo.repository.TagRepository;
import top.mengshuo.repository.UserRepository;
import top.mengshuo.repository.UserTagRepository;

import java.time.Duration;

/**
 * graphql query
 *
 * @author mengshuo
 * @since 2022-01-03
 */
@Controller
public class GraphqlController {

    @Autowired
    private UserRepository userRepository;
    @Autowired
    private TagRepository tagRepository;
    @Autowired
    private UserTagRepository userTagRepository;


    /**
     * 查询用户列表
     * <p>
     * - @QueryMapping 属于 @SchemaMapping 的派生注解 = @SchemaMapping(typeName = "Query")
     *
     * @return 用户列表
     */
    @PreAuthorize("isAuthenticated()")
    @QueryMapping(name = "userList")
    public Flux<User> userList() {
        return this.userRepository.findAll();
    }

    /**
     * 根据id查询用户信息
     * - @Argument  指定参数名称
     *
     * @param id 用户id
     * @return res
     */
    @PreAuthorize("hasAuthority('test:select')")
    @QueryMapping(name = "byId")
    public Mono<User> byId(@Argument Integer id) {
        return this.userRepository.findById(id);
    }


    /**
     * type User 中 tags 的子查询 <br/>
     * typeName = "User" 指定父类型名称 field = "tags" 指定查询名称 <br/>
     * 也就是 schema 中 User 的 tags 字段
     *
     * @param user 用户信息
     * @return tags
     */
    @SchemaMapping(typeName = "User", field = "tags")
    public Flux<Tag> tags(User user) {

        Flux<UserTag> userTags = this.userTagRepository.findByUid(user.getId());
        // 通过 map 转换为 Flux<Integer> 类型数据 再进行查询
        return this.tagRepository.findAllById(userTags.map(UserTag::getId));

    }

    /**
     * - @MutationMapping 变更类型 和 @QueryMapping 一样都是派生注解
     *
     * @param user 用户信息
     * @return res
     */
    @MutationMapping(name = "addUser")
    public Mono<User> addUser(@Argument User user) {

        return this.userRepository.save(user);
    }


    /**
     * 订阅数据 查询全部user数据每秒返回一个数据
     *
     * @return user
     */
    @SubscriptionMapping(name = "sub")
    public Flux<User> subscription() {

        return this.userRepository.findAll().delayElements(Duration.ofSeconds(1));
    }


}

认证授权处理

使用security

@PreAuthorize("isAuthenticated()") 必须认证 授权与其同理 添加权限表达式即可

    /**
     * 根据id查询用户信息
     * - @Argument  指定参数名称
     *
     * @param id 用户id
     * @return res
     */
    @PreAuthorize("hasAuthority('test:select')")
    @QueryMapping(name = "byId")
    public Mono<User> byId(@Argument Integer id) {
        return this.userRepository.findById(id);
    }

自定义异常处理

自定义异常处理 集成 DataFetcherExceptionResolverAdapter

重写 resolveToMultipleErrors 解决多个异常 或者 resolveToSingleError 解决单个异常 的方法

org.springframework.graphql.execution.DataFetcherExceptionResolver 这是接口

这并不能处理全部异常,从注释文档来看只能处理 graphql.schema.DataFetcher 产生的异常

我理解的是graphql查询语句通过解析后的异常,也就是如果前端的查询语句与 schema.graphqls 中的不匹配则属于graphql解析的异常,比如查询的参数不符合要求,或者请求的字段不存在等,此时这个处理器并不会进行处理,只有通过解析后,产生的异常才会处理

package top.mengshuo.exception;

import graphql.GraphQLError;
import graphql.GraphqlErrorBuilder;
import graphql.schema.DataFetchingEnvironment;
import org.springframework.cglib.proxy.UndeclaredThrowableException;
import org.springframework.graphql.execution.DataFetcherExceptionResolverAdapter;
import org.springframework.graphql.security.ReactiveSecurityDataFetcherExceptionResolver;
import org.springframework.graphql.security.SecurityDataFetcherExceptionResolver;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

/**
 * graphql 异常处理 <br/>
 * {@link GraphQLError} 的实现类包含了graphql 的默认处理方法
 * {@link ReactiveSecurityDataFetcherExceptionResolver} security 适配 webflux 的异常处理
 * {@link SecurityDataFetcherExceptionResolver} security 适配 webmvc 的异常处理
 *
 * @author mengshuo
 * @since 2022-01-05
 */
@Component
public class GraphqlExceptionResolver extends DataFetcherExceptionResolverAdapter {


    private static final Map<Class<? extends Throwable>, ErrorType> ERROR_TYPE_MAP =
            new HashMap<>();

    /*
    初始化已知异常 也可以将信息放入到 异常枚举类 由枚举类提供查找方法 但是有耦合
     */
    static {
        ERROR_TYPE_MAP.put(AuthenticationException.class, ErrorType.UNAUTHORIZED);
    }

    /**
     * 覆盖此方法以解决单个 GraphQL 错误的异常。
     *
     * @param ex  要解决的异常
     * @param env 被调用的DataFetcher的环境
     * @return 已解决的错误 或 null 如果未解决
     */
    @Override
    protected GraphQLError resolveToSingleError(Throwable ex, DataFetchingEnvironment env) {

        ErrorType errorType;
        if (ex instanceof UndeclaredThrowableException) {
            ex = ((UndeclaredThrowableException) ex).getUndeclaredThrowable();
        }
        /* web flux 不能这样处理
        // 特殊处理的放入到 if 无需特殊处理的放入map减少代码冗余
        if (ex instanceof AccessDeniedException) {
            // 权限不足 如果是匿名用户 也就是没有登陆的情况下 返回 未登录 只能处理webmvc的security异常
            // 因为 @PreAuthorize("isAuthenticated()") 也是 AccessDeniedException 异常,
            // 因此就需要判定是否登录了 登录了就是没权限 没登陆就返回没登陆 这样前端就可以根据提示进行操作了
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            if (authentication != null) {
                errorType = authentication.isAuthenticated() ? ErrorType.UNAUTHORIZED : ErrorType.FORBIDDEN;
            } else {
                errorType = ErrorType.UNAUTHORIZED;
            }
        } else {
            // 非特殊类型直接获取并返回
            errorType = ERROR_TYPE_MAP.get(ex.getClass());
        }
        */
        // 非特殊类型直接获取并返回
        errorType = ERROR_TYPE_MAP.get(ex.getClass());
        // 兜底处理 如果这不是最后一个处理器 如果返回null 则会继续寻找处理方法
        if (errorType == null) {
            return null;
        }
        return GraphqlErrorBuilder.newError(env).errorType(errorType).message(errorType.getMsg()).build();
    }

}
package top.mengshuo.exception;

import graphql.GraphQLError;
import graphql.GraphqlErrorBuilder;
import graphql.schema.DataFetchingEnvironment;
import org.springframework.graphql.execution.DataFetcherExceptionResolver;
import org.springframework.graphql.security.ReactiveSecurityDataFetcherExceptionResolver;
import org.springframework.graphql.security.SecurityDataFetcherExceptionResolver;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

import java.util.Collections;
import java.util.List;

/**
 * 针对于webflux security的异常处理
 * {@link ReactiveSecurityDataFetcherExceptionResolver} security 适配 webflux 的异常处理
 * {@link SecurityDataFetcherExceptionResolver} security 适配 webmvc 的异常处理
 *
 * @author mengshuo
 * @since 2022-01-09
 */
@Component
public class WebFluxSecurityExceptionResolver implements DataFetcherExceptionResolver {
    /**
     * 参考{@link ReactiveSecurityDataFetcherExceptionResolver#resolveException(java.lang.Throwable, graphql.schema.DataFetchingEnvironment)}
     *
     * @param exception   异常
     * @param environment 数据解析上下文
     * @return res
     */
    @Override
    public Mono<List<GraphQLError>> resolveException(Throwable exception, DataFetchingEnvironment environment) {
        return ReactiveSecurityContextHolder.getContext()
                .map(context ->
                        Collections.singletonList(GraphqlErrorBuilder.newError().errorType(ErrorType.FORBIDDEN).message(ErrorType.FORBIDDEN.getMsg()).build())
                )
                .switchIfEmpty(Mono.fromCallable(() ->
                        Collections.singletonList(GraphqlErrorBuilder.newError().errorType(ErrorType.UNAUTHORIZED).message(ErrorType.UNAUTHORIZED.getMsg()).build())
                ));
    }
}
package top.mengshuo.exception;

import graphql.ErrorClassification;
import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * 错误类型 实现 {@link ErrorClassification}
 *
 * @author mengshuo
 * @since 2022-01-05
 */
@Getter
@AllArgsConstructor
public enum ErrorType implements ErrorClassification {

    /**
     * 默认error
     */
    BAD_REQUEST("客户端错误"),
    UNAUTHORIZED("未登录或登录失效"),
    FORBIDDEN("未经授权,禁止访问"),
    NOT_FOUND("未定义"),
    INTERNAL_ERROR("系统内部错误, 请进行排查"),
    ;

    private final String msg;
}

测试

可以访问:http://127.0.0.1:8081/graphiql

也可以安装浏览器插件 或者 postman

浏览器插件:https://chrome.google.com/webstore/detail/kjhjcgclphafojaeeickcokfbhlegecd

  1. 访问须认证的端点

    POST /graphql HTTP/1.1
    Host: 127.0.0.1:8081
    Content-Type: application/json
    Content-Length: 73
    
    {"query":"{ userList{  id name phone tags{ id name } } }","variables":{}}
    

    无登录 访问 userList 端点

  2. 访问需要权限的端点

    使用 http basic 进行认证

    POST /graphql HTTP/1.1
    Host: 127.0.0.1:8081
    Authorization: Basic bWVuZ3NodW86bWVuZ3NodW8=
    Content-Type: application/json
    Content-Length: 96
    
    {"query":"query ($id: ID! ){ byId(id: $id) { id name tags { id name } } }","variables":{"id":1}}
    

    需要权限

  3. 多个端点同时调用 聚合数据

    因为 byId 端点无权限所以只返回了 userList 端点

    POST /graphql HTTP/1.1
    Host: 127.0.0.1:8081
    Authorization: Basic bWVuZ3NodW86bWVuZ3NodW8=
    Content-Type: application/json
    Content-Length: 101
    
    {"query":"query ($id: ID! ){ byId(id: $id) { id name } userList{ id name}}\r\n","variables":{"id":1}}
    

    多个端点

  4. subscription 类型

    订阅端点

总结

Rest api 和 Graphql 并不冲突,rest 调用更加简单 而 grapqhl 则更加灵活,查询聚合数据时 rest可以通过增加接口来避免前端的多次调用,但是如果我只需要几个字段,那么多余的数据返回势必造成返回数据的冗余,而且增加了数据传输的内容

Graphql 是在Rest基础上进行改进的,设计是向前迭代的

0
博主关闭了所有页面的评论