zoukankan      html  css  js  c++  java
  • Spring Boot GraphQL 实战 03_分页、全局异常处理和异步加载

    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;
        }
    }
    

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

    传统分页

    基于游标的分页

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

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

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

    Connection 对象

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

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

    DefaultConnection

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

    PageInfo

    编写 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);
    }
    

    query

    更多参考资料: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;
        }
    }
    

    参数校验错误

    服务端参数校验失败

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

    想想我们在 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();
        }
    }
    

    CustomExceptionHandler

    客户端错误信息

    自定义 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();
            }
        }
    }
    

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

    异步加载日志输出

  • 相关阅读:
    React方法论
    React学习记录
    LeetCode 530. Minimum Absolute Difference in BST
    LeetCode 521. Longest Uncommon Subsequence I
    LeetCode 520. Detect Capital
    LeetCode 516. Longest Palindromic Subsequence
    LeetCode 513. Find Bottom Left Tree Value
    LeetCode 506. Relative Ranks
    LeetCode 504. Base 7
    LeetCode 500. Keyboard Row
  • 原文地址:https://www.cnblogs.com/coderxiaohei/p/14256248.html
Copyright © 2011-2022 走看看