5

Spring Boot GraphQL 实战 03_分页、全局异常处理和异步加载

 3 years ago
source link: https://www.daqianduan.com/17562.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

hello,大家好,我是小黑,又和大家见面啦~

今天我们来继续学习 Spring Boot GraphQL 实战,我们使用的框架是 https://github.com/graphql-java-kickstart/graphql-spring-boot

本期,我们将使用 H2 和 Spring Data JPA 来构建数据库和简单的查询,不熟悉的同学可以自行去网上查阅相关资料学习。

完整项目 github 地址: https://github.com/shenjianeng/graphql-spring-boot-example

分页查询

基于偏移量的分页

基于偏移量的分页,即通过 SQL 的 limit 来实现分页。

优点是实现简单,使用成本低。缺点是在数据量过大时,进行大翻页时可能会有性能问题。

先来编写 graphqls 文件:

type PageResult{
    items:[Student]!
    pageNo:Int!
    pageSize:Int!
    totalCount:Int!
}

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

type Query{
    findAll(pageNo:Int!,pageSize:Int!):PageResult!
}

对应的 Java Bean 就不在这里赘述了,读者感兴趣的话可以自行查询小黑同学上传在 github 上的源码。

其中,最主要的 StudentGraphQLQueryResolver 源码如下:

@Component
@RequiredArgsConstructor
public class StudentGraphQLQueryResolver implements GraphQLQueryResolver {

    private final StudentRepository studentRepository;


    public PageResult<Student> findAll(int pageNo, int pageSize) {
        Page<Student> page = studentRepository.findAll(PageRequest.of(pageNo - 1, pageSize));
        PageResult<Student> pageResult = new PageResult<>();
        pageResult.setItems(page.getContent());
        pageResult.setPageNo(pageNo);
        pageResult.setPageSize(page.getSize());
        pageResult.setTotalCount((int) page.getTotalElements());
        return pageResult;
    }
}

启动应用,测试结果如下图:

RJruMf.png

基于游标的分页

基于游标的分页,即通过游标来跟踪数据获取的位置。

游标的选取有时候可以非常简单,例如可以将所获得数据的最后一个对象的 ID 作为游标。

GraphQL 游标分页是 Relay 风格式的,更多规范信息可以查阅: https://relay.dev/graphql/connections.htm

Connection 对象

在 Relay 分页查询中,分页结果需要返回 Connection 对象。

先来简单看一下 Connection 的默认实现 graphql.relay.DefaultConnection 的源码:

2AR7ni.png

PageInfo 中保存了和分页相关的一些信息:

mQ7Bry.png

编写 graphqls 文件

Relay 式分页中定义了一些规范:

  • 向前分页,在向前分页中,有两个必要参数: firstafter

    first
    after
    
  • 向后分页,在向后分页中,也有两个必要参数:

    • last :指定取游标前的多少个数据

    • before :与 last 搭配使用,用来指定游标位置

type Query{
    students(first: Int, after: String): StudentConnection @connection(for: "Student")
}

实现分页方法

对应 StudentGraphQLQueryResolver 源码如下:

public Connection<Student> students(int first, String after) {
    String afterToUsed = StringUtils.defaultIfEmpty(after, "0");

    Integer minId = studentRepository.findMinId();
    Integer maxId = studentRepository.findMaxId();

    // 从 after 游标开始,取 first 个数据
    // 这里故意取 first + 1 个数,用来判断是否还有下一页数据
    List<Student> students =
            studentRepository.findByIdGreaterThan(Integer.valueOf(afterToUsed), PageRequest.of(0, first + 1));

    List<Edge<Student>> edges = students.stream()
            .limit(first)
            .map(student -> new DefaultEdge<>(student, new DefaultConnectionCursor(String.valueOf(student.getId()))))
            .collect(Collectors.toList());

    PageInfo pageInfo =
            new DefaultPageInfo(
                    new DefaultConnectionCursor(String.valueOf(minId)),
                    new DefaultConnectionCursor(String.valueOf(maxId)),
                    Integer.parseInt(afterToUsed) > minId,
                    students.size() > first);

    return new DefaultConnection<>(edges, pageInfo);
}

eAF7fa.png

更多参考资料: https://www.graphql-java-kickstart.com/tools/relay/

使用 validation 校验参数

在 SpringMVC 中, javax.validation 的一系列注解可以帮我们完成参数校验,那在 GraphQL 中能否也使用 javax.validation 来进行参数合法性校验呢?答案是可行的。

下面,我们就构建一个简单的案例来尝试一下。

type Teacher{
    id:ID!
    name:String!
    age:Int
}

type Mutation{
    createTeacher(teacherInput:TeacherInput!):Teacher
}

input TeacherInput{
    id:ID!
    name:String!
    age:Int!
}
@Data
public class Teacher {
    private int id;
    private String name;
    private int age;
}

@Data
public class TeacherInput {

    @Min(value = 1, message = "id错误")
    private int id;

    @Length(min = 2, max = 10, message = "名称过长")
    private String name;

    @Range(min = 1, max = 100, message = "年龄不正确")
    private int age;
}

@Validated
@Component
public class TeacherGraphQLMutationResolver implements GraphQLMutationResolver {

    public Teacher createTeacher(@Valid TeacherInput input) {
        Teacher teacher = new Teacher();
        teacher.setId(input.getId());
        teacher.setName(input.getName());
        teacher.setAge(input.getAge());
        return teacher;
    }
}

iUzQBr.png

nE7rQr.png

可以看到,当客户端输入非法的参数时,服务端参数校验失败,但此时客户端看到的错误信息并不友好。那这个应该如何解决呢?

想想我们在 Spring MVC 中是怎么解决这个问题的?一般,这种情况下,我们会自定义全局异常处理器,然后由这些全局异常处理器来处理这些参数校验失败的异常,同时返回给客户端更友好的提示。

那现在我们是不是也可以这样做呢?我们当前使用的 graphql-spring-boot 框架支不支持全局异常处理呢?

全局异常处理

使用 @ExceptionHandler

Spring MVC 允许我们使用 @ExceptionHandler 来自定义 HTTP 错误响应。

在 graphql-spring-boot 框架中也添加了对该注释的支持,用于以将异常转换为有效的 GraphQLError 对象。

要使用 @ExceptionHandler 注解的方法签名必须满足以下要求:

public GraphQLError singleError(Exception e);

public GraphQLError singleError(Exception e, ErrorContext ctx);

public Collection<GraphQLError> multipleErrors(Exception e);

public Collection<GraphQLError> multipleErrors(Exception e, ErrorContext ctx);

下面,我们就来简单尝试一下。

@Component
public class CustomExceptionHandler {

    @ExceptionHandler(ConstraintViolationException.class)
    public GraphQLError constraintViolationExceptionHandler(ConstraintViolationException ex, ErrorContext ctx) {
        return GraphqlErrorBuilder.newError()
                .message(ex.getMessage())
                .locations(ctx.getLocations())
                .path(ctx.getPath())
                .build();
    }
}

ARB7Nv.png

aiUv2i.png

自定义 GraphQLErrorHandler

第二种处理方式:可以通过实现 graphql.kickstart.execution.error.GraphQLErrorHandler 接口来自定义异常处理器。

需要注意的是,一旦系统中自定义了 GraphQLErrorHandler 组件,那么 @ExceptionHandler 的处理方式就会失效。

@Slf4j
@Component
public class CustomGraphQLErrorHandler implements GraphQLErrorHandler {

    @Override
    public List<GraphQLError> processErrors(List<GraphQLError> errors) {
        log.info("Handle errors: {}", errors);
        return Collections.singletonList(new GenericGraphQLError("系统异常,请稍后尝试"));
    }
}

异步 Resolver

异步加载的实现其实也很简单,直接使用 CompletableFuture 作为 Resolver 的返回对象即可。

type Query{
    getTeachers:[Teacher]
}
@Slf4j
@Component
public class TeacherGraphQLQueryResolver implements GraphQLQueryResolver {

    private final ExecutorService executor =
            Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

    @PreDestroy
    public void destroy() {
        executor.shutdown();
    }

    public CompletableFuture<Collection<Teacher>> getTeachers() {
        log.info("start getTeachers...");
        CompletableFuture<Collection<Teacher>> future = CompletableFuture.supplyAsync(() -> {
            log.info("invoke getTeachers...");
            sleep();
            Teacher teacher = new Teacher();
            teacher.setId(666);
            teacher.setName("coder小黑");
            teacher.setAge(17);
            return Collections.singletonList(teacher);
        }, executor);

        log.info("end getTeachers...");
        return future;
    }

    private void sleep() {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

当客户端发起请求时,让我们来一起看一下后台的日志输出,注意看日志输出的先后顺序和执行线程名:

FRBnAn.png

#感谢您访问本站#
#本文转载自互联网,若侵权,请联系删除,谢谢!657271#qq.com#

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK