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 的源码:

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

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

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

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

Spring Boot GraphQL 实战 03_分页、全局异常处理和异步加载的更多相关文章

  1. Spring Boot GraphQL 实战 02_增删改查和自定义标量

    hello,大叫好,我是小黑,又和大家见面啦~ 今天我们来继续学习 Spring Boot GraphQL 实战,我们使用的框架是 https://github.com/graphql-java-ki ...

  2. Spring Boot 2.X(十一):全局异常处理

    前言 在 Java Web 系统开发中,不管是 Controller 层.Service 层还是 Dao 层,都有可能抛出异常.如果在每个方法中加上各种 try catch 的异常处理代码,那样会使代 ...

  3. Spring Boot GraphQL 实战 01_快速入门

    hello,大家好,我是小黑,又和大家见面啦~ 新开一个专题是关于 GraphQL 的相关内容,主要是通过 Spring Boot 来快速开发 GraphQL 应用,希望对刚接触 GraphQL 的同 ...

  4. Spring Boot 2.4.0正式发布,全新的配置文件加载机制(不向下兼容)

    千里之行,始于足下.关注公众号[BAT的乌托邦],有Spring技术栈.MyBatis.JVM.中间件等小而美的原创专栏供以免费学习.分享.成长,拒绝浅尝辄止.本文已被 https://www.you ...

  5. 【Bug档案01】Spring Boot的控制器+thymeleaf模板 -使用中出现静态资源加载路径不当的问题 -解决时间:3h

    总结 - thymeleaf的模板解析规则不清楚,或者忘了; - 出现bug时,瞎调试, 没有打开NETWORK 进行查看资源的加载情况 - 控制器中的其他代码,可以先注释掉,这样就可以迅速屏蔽掉其他 ...

  6. Spring Boot 项目实战(五)集成 Dubbo

    一.前言 上篇介绍了 Redis 的集成过程,可用于解决热点数据访问的性能问题.随着业务复杂度的提高,单体应用越来越庞大,就好比一个类的代码行数越来越多,分而治之,切成多个类应该是更好的解决方法,所以 ...

  7. Spring Boot:实现MyBatis分页

    综合概述 想必大家都有过这样的体验,在使用Mybatis时,最头痛的就是写分页了,需要先写一个查询count的select语句,然后再写一个真正分页查询的语句,当查询条件多了之后,会发现真的不想花双倍 ...

  8. “Spring Boot+Marklogic实战应用(1)”

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议.本文链接:http://www.blbk.info Spring Boot+Marklogic应用 摘要: 在前一节的介绍,相信 ...

  9. spring boot插件开发实战和原理

    本文转载自spring boot插件开发实战和原理 实战:编写spring boot插件 为什么要编写boot插件 因为我们在开发的时候需要提供一些共同的功能,所以我们编写个共同的jar包.开发人员在 ...

随机推荐

  1. 逆袭故事|从“点点点”到月薪17K,这个女孩子做到啦

    姓:XXX,XX VIP 学员,一名自动化测试工程师!之前在某大型培训机构转行学习软件测试时就是木子老师的学生,她也一直关注木子老师的动态,后来知道木子老师打算开设测试开发高级提升班,等开了班就报名了 ...

  2. 全栈工程师-史上最强VSCODE插件-提高开发效率

    当你点进来的时候 ,你可能是被标题吸引进来的,也有可能是 偶然间,看到的,首先恭喜你,已经准备好向全栈开发工程师靠近 ,那我们不说废话,直接开始,咱们先从安装步骤开始讲起 ,因为有些人连插件在哪都不知 ...

  3. Kubernetes应用管理器OpenKruise之CloneSet

    OpenKruise OpenKruise 是 Kubernetes 的一个标准扩展,它可以配合原生 Kubernetes 使用,并为管理应用容器.sidecar.镜像分发等方面提供更加强大和高效的能 ...

  4. MySQL 触发器、存储过程 练习

    创建表并插入数据: ```        字段名 数据类型 主键 外键 非空 唯一 自增        id INT    是 否 是  是 否        name VARCHAR(50) 否 否 ...

  5. Json转换值类型字段为空字符串时报错问题

    问题 在写Webservices时,碰到的问题. 定义的类 public class User { public string sID { get; set; } public int? iAge { ...

  6. Error:(18) error: '#FFFF782' is incompatible with attribute android:endColor (attr) color. --Android

    android  studio 编译是报如下错误: Error:(18) error: '#FFFF782' is incompatible with attribute android:endCol ...

  7. 算法竞赛入门经典第二版第二章习题-(练习Java和C++语法)

    习题2-1水仙花数(daffodil) 输出1000-999中所有的水仙花数.若三位数ABC满足ABC = A3+B3+C3,则称其为水仙花数. Java: package suanfa; publi ...

  8. 痞子衡嵌入式:MCUXpresso IDE下SDK工程导入与workspace管理机制

    大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家介绍的是MCUXpresso IDE下SDK工程导入与workspace管理机制. MCUXpresso IDE是恩智浦软件团队倾注很大心血研发 ...

  9. spring的事物传递

    Propagation.REQUIRED:默认也是常用的事物级别,在当前事物中执行,不存在事物,则创建新事物执行. Propagation.SUPPORTS:支持使用当前事物,当前事物不存爱,则不使用 ...

  10. IntelliJ IDEA如何用maven命令打jar包

    IntelliJ IDEA如何用maven命令打jar包?下面给大家详细介绍一下具体步骤及说明. 工具/原料   IntelliJ IDEA maven 方法/步骤     第一步在CMD命令窗口输入 ...