目录
Springboot中集成GraphQL
1.前言
最近项目需求要使用GraphQL做为后端服务,提供API接口,奈何资料较少,在此记录一下搭建GraphQL服务的过程
2.工程环境
工程包含主要插件
JDK 1.8
Springboot 2.1.3
MySQL
MyBatis
Graphql-java
fastjson
Lombok
maven
3.创建Springboot工程
这里不在描述,使用IDEA或者Eclipse创建一个springboot工程或者导入一个Springboot工程都行,这里使用2.1.3.RELEASE版本
4.集成开发环境
4.1 配置pom.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.mx.server</groupId>
<artifactId>spring-boot-graphql</artifactId>
<version>1.0.0-RELEASE</version>
<name>spring-boot-graphql</name>
<description>easy graphql for spring boot</description>
<properties>
<java.version>1.8</java.version>
<kotlin.version>1.3.70</kotlin.version>
<mybatis.version>2.0.0</mybatis.version>
<c3p0.version>0.9.5.2</c3p0.version>
<fastjson.version>1.2.5</fastjson.version>
<junit.jupiter.version>5.6.2</junit.jupiter.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<!-- 使用Junit5,排除spring-test自带的junit4 -->
<exclusions>
<exclusion>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- GraphQL -->
<dependency>
<groupId>com.graphql-java-kickstart</groupId>
<artifactId>graphql-spring-boot-starter</artifactId>
<version>7.0.1</version>
</dependency>
<dependency>
<groupId>com.graphql-java-kickstart</groupId>
<artifactId>graphql-java-tools</artifactId>
<version>6.0.2</version>
</dependency>
<!-- GraphiQL tool -->
<dependency>
<groupId>com.graphql-java-kickstart</groupId>
<artifactId>graphiql-spring-boot-starter</artifactId>
<version>7.0.1</version>
<scope>runtime</scope>
</dependency>
<!-- Log4J2 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<!-- JDBC -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- MySQL -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- Mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.version}</version>
</dependency>
<!-- c3p0 -->
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>${c3p0.version}</version>
</dependency>
<!-- FastJSON -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- Junit5 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.jupiter.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
其中GraphQL关键依赖参考GraphQL Java Kickstart官网
4.2 配置application.yml文件
Springboot的配置文件为applicaion.properties或者applicaion.yml格式,根据自己使用习惯选择即可,这里使用application.yml风格
#Server
server:
port: 8080
#Mysql
spring:
datasource:
c3p0:
driverClass: com.mysql.cj.jdbc.Driver
jdbcUrl: jdbc:mysql://localhost:3306/graphql_test?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf-8&useSSL=false
user: mysql
password: mxnavi
#Mybatis
mybatis:
config-location: classpath:mybatis/mybatis-config.xml
mapper-locations: classpath*:mybatis/mapper/**/*.xml
#GraphQL
graphql:
servlet:
mapping: /graphql
enabled: true
corsEnabled: true
cors:
allowed-origins: http://some.domain.com
# if you want to @ExceptionHandler annotation for custom GraphQLErrors
exception-handlers-enabled: true
contextSetting: PER_REQUEST_WITH_INSTRUMENTATION
tools:
schema-location-pattern: "**/*.graphqls"
# Enable or disable the introspection query. Disabling it puts your server in contravention of the GraphQL
# specification and expectations of most clients, so use this option with caution
#GraphiQL Tool
graphiql:
mapping: /graphiql
endpoint:
graphql: /graphql
subscriptions:
timeout: 30
reconnect: false
static:
basePath: /
enabled: true
pageTitle: GraphiQL
props:
resources:
query: /graphql/types.graphqls
variables: /graphql/types.graphqls
variables:
editorTheme: "solarized light"
headers:
Authorization: "Bearer <your-token>"
主要配置包括
Server部分:配置一些服务的基础,可定义端口号,服务名等
MySQL部分:配置连接MySQL数据库相关参数
MyBatis部分:配置MyBatis相关参数
GraphQL部分:配置GraphQL相关参数
GraphiQL Tool部分:配置GraphiQL工具相关参数,可访问图形化界面
4.3 创建graphqls文件
在resource目录下创建graphql文件夹,日后所有的graphqls文件都存放再此,创建types.graphqls文件
注意:此文件types.graphqls与applicaion.yml中配置有对应关系,如果修改,请记得一起修改,否则启动报错
types.graphqls文件内容如下
type Query {
user(nickname: String!): User
users: [User]
}
type Mutation {
addUser(mail: String!, nickname: String!, password: String!, description: String): Result
deleteUser(id: String!): Result
updateUser(id: String!, mail: String!, nickname: String!, description: String): User
addUserByInput(input: AddUserInput): User
}
type User {
id: String!
mail: String!
nickname: String!
password: String!
description: String
}
type Result {
respCode: Int!
msg: String
}
input AddUserInput {
mail: String!
nickname: String!
password: String!
description: String
}
其中Query下定义的是查询相关接口,Mutation下定义的是修改相关接口
Query中user和users可以理解为controller中的接口名,后续需要和Resolver中方法名对应
关于graphqls文件中写法,具体参考GraphQL官网介绍GraphQL中文网
4.4 创建graphqls文件对应的Java对象
User.java
package com.mx.server.entity;
import lombok.Data;
@Data
public class User {
private String id;
private String nickname;
private String mail;
private String password;
private String description;
private String updateTime;
private String createTime;
}
Result.java
package com.mx.server.entity.response;
import lombok.Data;
@Data
public class Result {
private Integer respCode;
private String msg;
}
AddUserInput.java
package com.mx.server.entity.input;
import lombok.Data;
@Data
public class AddUserInput {
private String nickname;
private String mail;
private String password;
private String description;
}
4.5 创建graphqls文件对应的Resolver对象
想实现graphqls文件中Query中的方法,就实现GraphQLQueryResolver
想实现graphqls文件中Mutation的中的方法,就实现GraphQLMutationResolver
这里为了区分Query和Mutation,分成2个Resolver来写,具体在项目中应用时根据实际情况来修改。
QueryResolver.Java
package com.mx.server.resolvers;
import com.mx.server.entity.User;
import com.mx.server.service.UserService;
import graphql.kickstart.tools.GraphQLQueryResolver;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.List;
@Component
public class QueryResolver implements GraphQLQueryResolver {
private static final Logger logger = LogManager.getLogger(QueryResolver.class);
@Resource
private UserService userService;
public User user(String nickname) {
logger.info("Query Resolver ==> user");
logger.info("params: nickname:{}", nickname);
return userService.getUserByNickname(nickname);
}
public List<User> users() {
logger.info("Query Resolver ==> users");
return userService.listUsers();
}
}
MutationResolver.java
package com.mx.server.resolvers;
import com.mx.server.entity.User;
import com.mx.server.entity.input.AddUserInput;
import com.mx.server.entity.response.ResponseBuilder;
import com.mx.server.entity.response.Result;
import com.mx.server.service.UserService;
import graphql.kickstart.tools.GraphQLMutationResolver;
import graphql.kickstart.tools.GraphQLQueryResolver;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
@Component
public class MutationResolver implements GraphQLQueryResolver, GraphQLMutationResolver {
private static final Logger logger = LogManager.getLogger(MutationResolver.class);
@Resource
private UserService userService;
public Result addUser(String mail, String nickname, String password, String description) {
logger.info("Mutation Resolver ==> addUser");
logger.info("params: mail:{}, nickname:{}, password:{}, description:{}",
mail, nickname, password, description);
return userService.addUser(mail, nickname, password, description);
}
public Result deleteUser(String id) {
if(StringUtils.isAnyBlank(id)){
return ResponseBuilder.getRespCodeMsg(-101, "参数缺失");
}
logger.info("Mutation Resolver ==> deleteUser");
logger.info("params: id:{}", id);
return userService.deleteUser(id);
}
public User updateUser(String id, String mail, String nickname, String description) {
logger.info("Mutation Resolver ==> updateUser");
logger.info("params: id:{}, mail:{}, nickname:{}, description:{}",
id, mail, nickname, description);
return userService.updateUser(id, mail, nickname, description);
}
public User addUserByInput(AddUserInput addUserInput){
logger.info("Mutation Resolver ==> addUserByInput");
logger.info("params: {}", addUserInput);
return userService.addUserInput(addUserInput);
}
}
注意:Resolver中的方法,入参及返回值类型,必须和graphqls文件中定义的一致,否则启动报错。
至此与GraphQL相关的配置及代码都已创建完成,接下来把工程中其他的类都一一实现,比如Server,Dao等。
4.6 创建Service,Dao等实现类
UserService.java -> Service类,通过Resolver调用,简单加了一些验证。
package com.mx.server.service;
import com.mx.server.dao.UserMapper;
import com.mx.server.entity.User;
import com.mx.server.entity.input.AddUserInput;
import com.mx.server.entity.response.ResponseBuilder;
import com.mx.server.entity.response.Result;
import com.mx.server.util.CommonUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
@Service
public class UserService {
@Resource
private UserMapper userMapper;
private static final Logger logger = LogManager.getLogger(UserService.class.getName());
public User getUserByNickname(String nickname){
logger.info("Service ==> getUserByNickname");
return userMapper.getUserByNickname(nickname);
}
public List<User> listUsers(){
logger.info("Service ==> listUsers");
return userMapper.listUsers();
}
public Result addUser(String mail, String nickname, String password, String description){
logger.info("Service ==> getUser");
User userDb = userMapper.getUserByNickname(nickname);
if (null != userDb){
return ResponseBuilder.getRespCodeMsg(-110, "用户昵称存在");
}
User addUser = new User();
addUser.setId(CommonUtils.getUUID());
addUser.setMail(mail);
addUser.setNickname(nickname);
addUser.setPassword(password);
addUser.setDescription(description);
userMapper.addUser(addUser);
return ResponseBuilder.getRespCodeMsg(100, "Success");
}
public Result deleteUser(String id){
logger.info("Service ==> deleteUser");
User user = userMapper.getUserById(id);
if (null == user){
return ResponseBuilder.getRespCodeMsg(-500, "数据不存在");
}
userMapper.deleteUser(id);
return ResponseBuilder.getRespCodeMsg(100, "Success");
}
public User updateUser(String id,String mail, String nickname, String description){
logger.info("Service ==> updateUser");
User updateUser = new User();
updateUser.setId(id);
updateUser.setMail(mail);
updateUser.setNickname(nickname);
updateUser.setDescription(description);
userMapper.updateUser(updateUser);
return updateUser;
}
public User addUserInput(AddUserInput addUserInput){
logger.info("Service ==> addUserInput");
User addUser = new User();
addUser.setId(CommonUtils.getUUID());
addUser.setMail(addUserInput.getMail());
addUser.setNickname(addUserInput.getNickname());
addUser.setPassword(addUserInput.getPassword());
addUser.setDescription(addUserInput.getDescription());
userMapper.addUser(addUser);
return addUser;
}
}
UserMapper.java -> Mapper接口类,定义调用数据库的方法,由Service调用。
package com.mx.server.dao;
import com.mx.server.entity.User;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface UserMapper {
User getUserById(String id);
User getUserByNickname(String nickname);
List<User> listUsers();
void addUser(User user);
void deleteUser(String id);
void updateUser(User user);
}
CommonUtils.java -> 工具类,定义一些常用公共方法
package com.mx.server.util;
import java.util.UUID;
public final class CommonUtils {
private CommonUtils() {
}
public static String getUUID() {
return UUID.randomUUID().toString().replaceAll("-", "");
}
}
DataSourceConfig -> 定义数据源
package com.mx.server.config;
import javax.sql.DataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
import com.mchange.v2.c3p0.ComboPooledDataSource;
@Configuration
// 声明为配置类,相当于<beans>标签
@Component
public class DataSourceConfig {
@Bean(name = "dataSource")
// 对象及名称,相当于<bean>标签
@Primary
// 主要的候选者
// 配置属性,prefix : 前缀 spring.datasource固定
@ConfigurationProperties(prefix = "spring.datasource.c3p0")
public DataSource createDataSource() {
return DataSourceBuilder.create() // 创建数据源构建对象
.type(ComboPooledDataSource.class) // 设置数据源类型
.build(); // 构建数据源对象
}
}
4.7 MyBatis配置
本工程使用了MyBatis,上面已经定义好了Mapper类,下面就来创建mapper对应的xml文件实现对MySql的CRUD操作。
applicaion.yml文件中的MyBatis配置如下:
#Mybatis
mybatis:
config-location: classpath:mybatis/mybatis-config.xml
mapper-locations: classpath*:mybatis/mapper/**/*.xml
在resource目录下创建mybatis文件夹,放入mybatis-config.xml,其实没什么有用的内容,简单定义一些返回类型的别名。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<typeAliases>
<typeAlias alias="Integer" type="java.lang.Integer" />
<typeAlias alias="Long" type="java.lang.Long" />
<typeAlias alias="Boolean" type="java.lang.Boolean" />
<typeAlias alias="HashMap" type="java.util.HashMap" />
<typeAlias alias="LinkedHashMap" type="java.util.LinkedHashMap" />
<typeAlias alias="ArrayList" type="java.util.ArrayList" />
<typeAlias alias="LinkedList" type="java.util.LinkedList" />
</typeAliases>
</configuration>
在mybatis文件夹下创建mapper文件夹,用来存放具体实现mapper的xml文件
userMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mx.server.dao.UserMapper">
<select id="getUserByNickname" resultType="com.mx.server.entity.User">
SELECT
id,
nickname,
mail,
description,
DATE_FORMAT(update_time,'%Y-%m-%d %H:%i') updateTime,
DATE_FORMAT(create_time,'%Y-%m-%d %H:%i') createTime
FROM
user
WHERE
nickname = #{nickname}
</select>
<select id="getUserById" resultType="com.mx.server.entity.User">
SELECT
id,
nickname,
mail,
description,
DATE_FORMAT(update_time,'%Y-%m-%d %H:%i') updateTime,
DATE_FORMAT(create_time,'%Y-%m-%d %H:%i') createTime
FROM
user
WHERE
id = #{id}
</select>
<select id="listUsers" resultType="com.mx.server.entity.User">
SELECT
id id,
nickname nickname,
mail mail,
description description,
DATE_FORMAT(update_time,'%Y-%m-%d %H:%i:%s') updateTime,
DATE_FORMAT(create_time,'%Y-%m-%d %H:%i:%s') createTime
FROM
user
</select>
<insert id="addUser" parameterType="com.mx.server.entity.User"
keyProperty="id" useGeneratedKeys="true">
INSERT INTO
user
(
id,
nickname,
password,
mail,
description,
update_time,
create_time
)
VALUES
(
#{id},
#{nickname},
#{password},
#{mail},
#{description},
now(),
now()
)
</insert>
<update id="updateUser">
UPDATE
user
SET
nickname = #{nickname},
mail = #{mail},
description = #{description},
update_time = now()
WHERE
id = #{id}
</update>
<delete id="deleteUser">
DELETE FROM
user
WHERE
id = #{id}
</delete>
</mapper>
user表结构
CREATE TABLE `user` (
`id` varchar(32) NOT NULL,
`nickname` varchar(255) NOT NULL,
`mail` varchar(255) NOT NULL,
`password` varchar(255) NOT NULL,
`description` varchar(255) DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
最终工程目录结构如下
5.启动和测试执行
使用GraphiQL Tool进行API调用测试,启动Application,成功后,打开浏览器访问 http://localhost:8080/graphiql
默认显示工程里的graphqls文件内容,删除后,可在右上角Docs菜单查看
5.1 Query类型调用
调用查询接口users,输入调用内容,点击执行按钮,查看结果
# query 查询关键字
# ListUsers 别名
# users 接口名
# id,nickname 查询后,想要获取的返回值
query ListUsers{
users {
id
nickname
}
}
执行结果
5.2 Mutation类型调用
调用创建接口addUser,输入调用内容,点击执行按钮,查看结果
# mutation 变更关键字
# AddUser 别名
# addUser 接口名,后面接入参,接口中有!定义的变量是必填的,其他随意
# respCode,msg 想要获取的返回值
mutation AddUser{
addUser(mail:"11", nickname: "testuser", password: "111", description: "test") {
respCode
msg
}
}
执行结果
5.3 使用变量Variables
拿根据昵称查询用户做示例
# QueryUser 别名
# query 关键字后面的 $nickname 为声明变量
# user 后面的 $nickname 为使用变量,变量名要与上面声明的一致
query QueryUser($nickname: String!){
user(nickname: $nickname) {
id
nickname
}
}
在Variables中写入变量具体的值,格式为JSON格式,这里输入刚刚创建的用户,昵称为testuser
{
"nickname": "testuser"
}
执行结果
5.4 input输入类型的使用
在实际应用时,如果入参过多,逐个填写,显示过于太长了,使用起来也不方便,下面来演示使用input类型,将入参封装成对象传入,接口列表中定义了addUserByInput方法,入参类型为input
# mutation 变更关键字
# AddUserByInput 别名
# mutation后$userinput,声明userinput变量,类型为AddUserInput
# 变量的类型AddUserInput,已经在graphqls文件中定义,input开头那个
# addUserByInput 接口名
# addUserByInput后input 入参类型
# addUserByInput后$userinput 入参变量引用
mutation AddUserByInput($userinput: AddUserInput) {
addUserByInput(input: $userinput) {
id
nickname
mail
}
}
在Variables中写入变量userinput具体的值,格式为JSON格式
{
"userinput": {
"mail": "testuser2@test.com",
"nickname": "testuser2",
"password": "123456"
}
}
执行结果
到此基本功能演示结束,好了,接下就根据官网探索更多的使用方式吧!