1 GraphQL架構風格
1.1 簡介
有些小伙伴在工作中可能遇到過這樣的場景:移動端只需要用戶的姓名和郵箱,但REST API返回了用戶的所有信息,造成數據傳輸浪費。
GraphQL正是為了解決這個問題而生的。
GraphQL包含三個核心組件:
-
Schema定義:強類型系統(tǒng)描述API能力 -
查詢語言:客戶端精確請求需要的數據 -
執(zhí)行引擎:解析查詢并返回結果
GraphQL的優(yōu)缺點分析
- 優(yōu)點:
精確的數據獲取,避免過度獲取
單一端點,減少HTTP連接開銷
強類型系統(tǒng),自動生成文檔
前端主導數據需求 - 缺點:
查詢復雜度控制困難
緩存實現復雜(HTTP緩存失效)
N+1查詢問題需要額外處理
學習曲線相對陡峭
1.2 GraphQL執(zhí)行流程

1.3 相關語法
GraphQL 操作語法其實只有 3 種頂層類型:Query、Mutation、Subscription,但寫進紙面的“語法單元”有 10 來種。
可到此處進行驗證語法:https://countries.trevorblades.com/
3 種根操作(Operation):
-
Query:只讀 -
Mutation:寫操作 -
Subscription:實時推送(基于 WebSocket)
語法模板,方括號代表可省
operationType [operationName] ( [variableDefinitions] ) {
selectionSet
}
1.3.1 Query(查詢)
最簡單字段列表
{
user(id: 4) {
name
avatar
}
}
起個操作名(方便調試/日志)
query GetUser {
user(id: 4) {
name
}
}
傳變量(推薦,避免字符串拼接)
query GetUser($uid: ID!) { # 變量定義
user(id: $uid) {
name
posts { # 嵌套對象
title
comments(first: 3) { # 分頁實參
body
}
}
}
}
此處ID表示是類型,加感嘆號!區(qū)別:
| 寫法 | 含義 | 舉例 |
|---|---|---|
ID |
可以是 null 的 ID |
"user-123" 或 null
|
ID! |
絕對不能為 null 的 ID | 必須傳 "user-123",傳 null 或干脆不傳都會報錯
|
對于$使用變量聲明時才用
| 位置 | 寫法 | 角色 |
|---|---|---|
| 形參列表 | ($uid: ID!) |
變量聲明——告訴 GraphQL“等會兒外部會傳個變量叫 uid” |
| 查詢體內 | user(id: $uid) |
變量使用——把剛才聲明的那個變量插進來 |
別名(同一字段查兩次)
{
smallPic: user(id: 4) { avatar(size: 64) }
bigPic: user(id: 4) { avatar(size: 512) }
}
Fragment(復用片段)
//必須指向 on User 表示 這套字段只能展開在 User 對象上
fragment AvatarInfo on User {
name
avatar
}
query {
u1: user(id: 4) { ...AvatarInfo }
u2: user(id: 5) { ...AvatarInfo }
}
內聯片段(接口/聯合類型),... on Type { fields }叫 inline fragment(內聯片段),按類型挑選字段
{
search(keyword: "hero") {
// GraphQL 內置字段,任何對象里都能拿,返回當前對象的真實類型名(字符串)
__typename
... on Movie { title }
... on Book { author }
}
}
1.3.2 Mutation(變更)
創(chuàng)建 + 返回結果
//mutation 聲明“我要改” CreatePost 本次操作的命名
mutation CreatePost($input: PostInput!) {
// schema 里定義的 mutation 字段
createPost(input: $input) {
// 要返回什么字段
id
title
author { name }
}
}
多個寫操作順序執(zhí)行(GraphQL 保證串行)
mutation {
addComment(postId: 1, body: "nice") { id }
updatePost(id: 1, views: +1) { views }
}
1.3.3 Subscription(訂閱)
語法與 Query 相同,但由服務器推回
subscription MessageAdded($room: ID!) {
messageAdded(roomId: $room) {
from { name }
content
createdAt
}
}
客戶端通過 WebSocket 發(fā)送該幀,服務器在有人發(fā)言時回相同結構數據。
1.3.4 變量系統(tǒng)(Variable)
聲明:$var: Type = defaultValue,類型后加 ! 代表非空。
List / InputObject
query($ids: [ID!]!) {
users(ids: $ids) { name }
}
1.3.5 指令(Directive)
@skip(if: Boolean):如果為 true,就跳過這塊,@include(if: Boolean):只有為 true 才保留這塊,同時參數 if 必須是 Boolean!(非空布爾),變量、字面量都可以
query GetUser($skipAvatar: Boolean!) {
user(id: 4) {
name
avatar @skip(if: $skipAvatar)
}
}
1.3.6 Introspection(自省)元字段
Meta-field(元字段),帶 __ 的字段不會出現在要寫的 schema 里,但任何對象都能查詢它們:
-
__typename:運行期真實類型,內置字段,任何對象里都能拿 -
__schema:整個 schema 的“根目錄”,掛在最頂層的 Query隱式字段 -
__type(name: "User"): 按名取類型詳情,同樣掛在頂層,用來查詢某一個具體類型 的字段、枚舉值、可能的接口實現等
{
__schema {
types {
name
kind
fields { name type { name } }
}
}
}
1.3.7 錯誤與響應格式
GraphQL 總是 HTTP 200,錯誤放在 errors 數組:
{
"data": { "user": null },
"errors": [{
"message": "User not found",
"path": ["user"],
"extensions": { "code": "USER_404" }
}]
}
1.4 服務端操作
1.4.1 配置
1.4.1.1 pom.xml
springboot 2.x 主要是 接口方式 比如 QueryResolver
<dependency>
<groupId>com.graphql-java-kickstart</groupId>
<artifactId>graphql-spring-boot-starter</artifactId>
<version>11.1.0</version>
</dependency>
<!-- 測試客戶端:GraphiQL(瀏覽器調試工具) -->
<dependency>
<groupId>com.graphql-java-kickstart</groupId>
<artifactId>graphiql-spring-boot-starter</artifactId>
<version>11.1.0</version>
</dependency>
spring 3.x 自帶graphql 主要使用注解方式@SchemaMapping、@QueryMapping 注解方式而不是 QueryResolver
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-graphql</artifactId>
</dependency>
1.4.1.2 yml配置
graphql:
servlet:
# graphql 訪問地址
mapping: /graphql
auto-register: true
tools:
# graphql 一般存放resource 下的 graphql 文件夾以 .graphqls 結尾
schema-location-pattern: graphql/**/schema*.graphqls
# 瀏覽器內使用 GraphiQL 啟用(默認 true)
graphiql:
enabled: true
mapping: /graphiql
1.4.1.1 Schema
type Query { //服務端定義而非客戶端的 query 定義
userById(id: ID!): User
users: [User]
}
type User {
id: ID
userName: String
phoneNumer: String
remark: String
}
type Mutation {
addUser(userName: String!, phoneNumer: String!): Boolean # 添加
}
1.4.2 業(yè)務操作
//查詢類
@Component
public class UserQuery implements GraphQLQueryResolver {
@Autowired
private UserService userService;
public List<UserEntity> users() {
return userService.list();
}
public UserEntity userById(String id) {
return userService.getById(id);
}
}
//新增類
@Component
public class UserMutation implements GraphQLMutationResolver {
@Autowired
private UserService userService;
public Boolean addUser(String userName, String phoneNumer) {
UserEntity book = new UserEntity();
book.setUserName(userName);
book.setPhoneNumer(phoneNumer);
return userService.save(book);
}
}
1.4.3 測試示例
查詢graphql schema定義:
query userList{
users{
id
userName
phoneNumer
remark
}
}
結果:
{
"data": {
"users": [
{
"id": "1",
"userName": "哈哈",
"phoneNumer": "1234556789",
"remark": "1"
}
]
}
}
新增類示例:
mutation addUser{
addUser(userName: "小明", phoneNumer: "123456879465")
}
結果:
{
"data": {
"addUser": true
}
}
1.4.4 原生操作
1.4.4.1 配置
pom.xml
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-java</artifactId>
<version>24.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
schema如下
type Query {
userById(id: ID!): User
users: [User]
}
type User {
id: ID
userName: String
phoneNumer: String
remark: String
}
type Mutation {
addUser(userName: String!, phoneNumer: String!): Boolean # 添加
}
1.4.4.2 解析配置
@Configuration
public class GraphQLProvider {
private GraphQL graphQL;
@Autowired
private UserService userService; // 你的業(yè)務層
@Bean
public GraphQL graphQL() {
return graphQL;
}
@PostConstruct
public void init() throws IOException {
// 1. 自動加載 resources/graphql 目錄下所有 .graphqls 文件
SchemaParser schemaParser = new SchemaParser();
TypeDefinitionRegistry typeRegistry = new TypeDefinitionRegistry();
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
Resource[] resources = resolver.getResources("classpath:graphql/**/*.graphqls");
for (Resource resource : resources) {
String sdl = StreamUtils.copyToString(resource.getInputStream(), StandardCharsets.UTF_8);
typeRegistry.merge(schemaParser.parse(sdl));
}
// 2. 綁定 DataFetcher(這就是替代 GraphQLQueryResolver 的方式)
RuntimeWiring runtimeWiring = RuntimeWiring.newRuntimeWiring()
.type("Query", builder -> builder
.dataFetcher("users", env -> userService.list())
.dataFetcher("userById", env -> {
String id = env.getArgument("id");
return userService.getById(id);
})
)
.type("Mutation", builder -> builder
.dataFetcher("addUser", env -> {
String userName = env.getArgument("userName");
String phoneNumer = env.getArgument("phoneNumer");
UserEntity user = new UserEntity();
user.setUserName(userName);
user.setPhoneNumer(phoneNumer);
return userService.save(user);
})
)
.build();
// 3. 生成可執(zhí)行 schema
SchemaGenerator schemaGenerator = new SchemaGenerator();
GraphQLSchema graphQLSchema = schemaGenerator.makeExecutableSchema(
SchemaGenerator.Options.defaultOptions(), // 可自定義嚴格校驗等
typeRegistry,
runtimeWiring
);
// 4. 創(chuàng)建 GraphQL 實例
this.graphQL = GraphQL.newGraphQL(graphQLSchema).build();
}
}
1.4.4.3 注冊servlet
@WebServlet(urlPatterns = "/graphql")
public class GraphQLServletConfig extends HttpServlet {
private final GraphQL graphQL; // 從 GraphQLProvider 注入,或靜態(tài)持有
public GraphQLServletConfig(GraphQL graphQL) {
this.graphQL = graphQL;
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
// 1. 解析請求 body(GraphQL query JSON)
String requestBody = req.getReader().lines().collect(Collectors.joining());
Map<String, Object> input = new ObjectMapper().readValue(requestBody, Map.class);
String query = (String) input.get("query");
Map<String, Object> variables = (Map<String, Object>) input.getOrDefault("variables", Collections.emptyMap());
// 2. 執(zhí)行 GraphQL
ExecutionInput executionInput = ExecutionInput.newExecutionInput()
.query(query)
.variables(variables)
.build();
ExecutionResult executionResult = graphQL.execute(executionInput);
// 3. 返回 JSON 響應
resp.setContentType("application/json");
resp.setCharacterEncoding("UTF-8");
resp.getWriter().write(new ObjectMapper().writeValueAsString(executionResult.toSpecification()));
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
// 支持 GET(可選,GraphQL 規(guī)范允許)
doPost(req, resp);
}
}
注意:啟動類別忘了@ServletComponentScan
1.4.4.4 測試
用bruno 模擬測試
查詢示例
query {
users {
id
userName
phoneNumer
remark
}
}
修改示例
mutation {
addUser(
userName:"小王"
phoneNumer:"987654321"
)
}