帶註解的控制器
Spring for GraphQL 提供了一種基於註解的程式設計模型,其中 @Controller
元件使用註解宣告具有靈活方法簽名的處理方法,用於為特定 GraphQL 欄位獲取資料。例如:
@Controller
public class GreetingController {
@QueryMapping (1)
public String hello() { (2)
return "Hello, world!";
}
}
1 | 將此方法繫結到查詢,即 Query 型別下的欄位。 |
2 | 如果未在註解中宣告,則從方法名稱確定查詢。 |
Spring for GraphQL 使用 RuntimeWiring.Builder
將上述處理方法註冊為名為 "hello" 的查詢的 graphql.schema.DataFetcher
。
宣告
你可以將 @Controller
beans 定義為標準的 Spring bean 定義。@Controller
構造型(stereotype)允許自動檢測,與 Spring 對類路徑上檢測 @Controller
和 @Component
類併為其自動註冊 bean 定義的通用支援保持一致。它也充當帶註解類的構造型,表示其在 GraphQL 應用中作為資料獲取元件的作用。
AnnotatedControllerConfigurer
檢測 @Controller
beans 並透過 RuntimeWiring.Builder
將其帶註解的處理方法註冊為 DataFetcher
。它是 RuntimeWiringConfigurer
的實現,可以新增到 GraphQlSource.Builder
。 Boot Starter 會自動將 AnnotatedControllerConfigurer
宣告為一個 bean,並將所有 RuntimeWiringConfigurer
beans 新增到 GraphQlSource.Builder
,從而啟用對帶註解的 DataFetcher
的支援,請參閱 Boot starter 文件中的 GraphQL RuntimeWiring 部分。
@SchemaMapping
@SchemaMapping
註解將一個處理方法對映到 GraphQL schema 中的一個欄位,並宣告該方法是該欄位的 DataFetcher
。該註解可以指定父型別名稱和欄位名稱。
@Controller
public class BookController {
@SchemaMapping(typeName="Book", field="author")
public Author getAuthor(Book book) {
// ...
}
}
@SchemaMapping
註解也可以省略這些屬性,在這種情況下,欄位名預設為方法名,而型別名預設為注入到方法中的源/父物件的簡單類名。例如,以下預設對映到 "Book" 型別和 "author" 欄位:
@Controller
public class BookController {
@SchemaMapping
public Author author(Book book) {
// ...
}
}
@SchemaMapping
註解可以宣告在類級別,用於為該類中的所有處理方法指定一個預設的型別名。
@Controller
@SchemaMapping(typeName="Book")
public class BookController {
// @SchemaMapping methods for fields of the "Book" type
}
@QueryMapping
、@MutationMapping
和 @SubscriptionMapping
是元註解,它們本身都帶有 @SchemaMapping
註解,並且 typeName
分別預設為 Query
、Mutation
或 Subscription
。實際上,這些是 Query、Mutation 和 Subscription 型別下欄位的快捷註解。例如:
@Controller
public class BookController {
@QueryMapping
public Book bookById(@Argument Long id) {
// ...
}
@MutationMapping
public Book addBook(@Argument BookInput bookInput) {
// ...
}
@SubscriptionMapping
public Flux<Book> newPublications() {
// ...
}
}
@SchemaMapping
處理方法具有靈活的簽名,並且可以選擇多種方法引數和返回值。
方法引數
Schema 對映處理方法可以有以下任何方法引數:
方法引數 | 描述 |
---|---|
|
用於訪問繫結到更高階、型別化物件的命名欄位引數。 參見 |
|
用於訪問原始引數值。 參見 |
|
用於訪問繫結到更高階、型別化物件的命名欄位引數,並帶有指示輸入引數是被省略還是被設定為 參見 |
|
用於訪問繫結到更高階、型別化物件的所有欄位引數。 參見 |
|
用於訪問原始引數對映。 |
|
透過投影介面訪問欄位引數。 |
"源" |
用於訪問欄位的源(即父/容器)例項。 參見 源。 |
|
用於訪問分頁引數。 |
|
用於訪問排序詳情。 |
|
用於訪問 參見 |
|
用於從 |
|
用於從 |
|
用於從 |
|
如果可用,從 Spring Security 上下文獲取。 |
|
用於從 Spring Security 上下文訪問 |
|
透過 |
|
用於從 |
|
用於直接訪問底層的 |
返回值
Schema 對映處理方法可以返回:
-
任何型別的解析值。
-
Mono
和Flux
用於非同步值。支援控制器方法和任何DataFetcher
,如 響應式DataFetcher
中所述。 -
Kotlin coroutine 和
Flow
被適配為Mono
和Flux
。 -
java.util.concurrent.Callable
用於非同步生成值。為此,AnnotatedControllerConfigurer
必須配置一個Executor
。
在 Java 21+ 上,當 AnnotatedControllerConfigurer
配置了 Executor
後,具有阻塞方法簽名的控制器方法將非同步呼叫。預設情況下,如果控制器方法不返回 Flux
、Mono
、CompletableFuture
等非同步型別,並且也不是 Kotlin suspending 函式,則被視為阻塞方法。你可以在 AnnotatedControllerConfigurer
上配置一個阻塞控制器方法 Predicate
,以幫助確定哪些方法被視為阻塞方法。
Spring for GraphQL 的 Spring Boot starter 在設定屬性 spring.threads.virtual.enabled 時,會自動為 AnnotatedControllerConfigurer 配置一個用於虛擬執行緒的 Executor 。 |
介面 Schema 對映
當控制器方法對映到 schema 介面欄位時,預設情況下該對映會被替換為多個對映,每個實現該介面的 schema 物件型別對應一個對映。這允許對所有子型別使用同一個控制器方法。
例如,給定:
type Query {
activities: [Activity!]!
}
interface Activity {
id: ID!
coordinator: User!
}
type FooActivity implements Activity {
id: ID!
coordinator: User!
}
type BarActivity implements Activity {
id: ID!
coordinator: User!
}
type User {
name: String!
}
你可以像這樣編寫一個控制器:
@Controller
public class BookController {
@QueryMapping
public List<Activity> activities() {
// ...
}
@SchemaMapping
public User coordinator(Activity activity) {
// Called for any Activity subtype
}
}
如果需要,你可以為單個子型別接管對映:
@Controller
public class BookController {
@QueryMapping
public List<Activity> activities() {
// ...
}
@SchemaMapping
public User coordinator(Activity activity) {
// Called for any Activity subtype except FooActivity
}
@SchemaMapping
public User coordinator(FooActivity activity) {
// ...
}
}
@Argument
在 GraphQL Java 中,DataFetchingEnvironment
提供對特定欄位引數值對映的訪問。值可以是簡單的標量值(例如 String、Long),一個用於更復雜輸入的 Map
,或者一個 List
。
使用 @Argument
註解可以將引數繫結到目標物件並注入到處理方法中。繫結透過將引數值對映到預期方法引數型別的主資料建構函式來執行,或者使用預設建構函式建立物件,然後將引數值對映到其屬性。這個過程會遞迴重複,使用所有巢狀的引數值並相應地建立巢狀的目標物件。例如:
@Controller
public class BookController {
@QueryMapping
public Book bookById(@Argument Long id) {
// ...
}
@MutationMapping
public Book addBook(@Argument BookInput bookInput) {
// ...
}
}
如果目標物件沒有 setter,並且你無法更改它,你可以在 AnnotatedControllerConfigurer 上使用一個屬性來允許回退到透過直接欄位訪問進行繫結。 |
預設情況下,如果方法引數名可用(Java 8+ 需要 -parameters
編譯器標誌,或者需要編譯器的除錯資訊),它將被用於查詢引數。如果需要,你可以透過註解自定義名稱,例如 @Argument("bookInput")
。
@Argument 註解沒有 "required" 標誌,也沒有指定預設值的選項。這兩者都可以在 GraphQL schema 級別指定,並由 GraphQL Java enforced。 |
如果繫結失敗,將丟擲 BindException
,其中包含繫結問題作為欄位錯誤,每個錯誤的 field
是問題發生的引數路徑。
你可以將 @Argument
與 Map<String, Object>
引數一起使用,以獲取引數的原始值。例如:
@Controller
public class BookController {
@MutationMapping
public Book addBook(@Argument Map<String, Object> bookInput) {
// ...
}
}
在 1.2 版本之前,如果註解未指定名稱,@Argument Map<String, Object> 返回完整的引數對映。在 1.2 版本之後,@Argument 與 Map<String, Object> 總是返回原始引數值,與註解中指定的名稱或引數名稱匹配。要訪問完整的引數對映,請改用 @Arguments 。 |
ArgumentValue
預設情況下,GraphQL 中的輸入引數是可為空且可選的,這意味著引數可以設定為 null
字面量,或者根本不提供。這種區別對於使用 mutation 進行部分更新非常有用,因為基礎資料可能因此被設定為 null
或根本不改變。當使用 @Argument
時,無法區分這兩種情況,因為在兩種情況下你都會獲得 null
或空的 Optional
。
如果你想知道一個值是否根本沒有提供,你可以宣告一個 ArgumentValue
方法引數,它是一個包含結果值的簡單容器,並帶有一個標誌指示輸入引數是否完全被省略。你可以使用它代替 @Argument
,在這種情況下,引數名稱由方法引數名稱確定;或者與 @Argument
一起使用以指定引數名稱。
例如:
@Controller
public class BookController {
@MutationMapping
public void addBook(ArgumentValue<BookInput> bookInput) {
if (!bookInput.isOmitted()) {
BookInput value = bookInput.value();
// ...
}
}
}
ArgumentValue
也支援作為 @Argument
方法引數物件結構中的欄位,無論是透過建構函式引數還是透過 setter 初始化,包括作為任何巢狀級別低於頂級物件的欄位。
@Arguments
如果你想將完整的引數對映繫結到一個單一目標物件上,請使用 @Arguments
註解,這與繫結特定命名引數的 @Argument
不同。
例如,@Argument BookInput bookInput
使用引數 "bookInput" 的值來初始化 BookInput
,而 @Arguments
使用完整的引數對映,在這種情況下,頂級引數被繫結到 BookInput
的屬性。
你可以將 @Arguments
與 Map<String, Object>
引數一起使用,以獲取所有引數值的原始對映。
@ProjectedPayload
介面
作為使用帶有 @Argument
的完整物件的替代方案,你也可以使用投影介面透過明確定義的最小介面訪問 GraphQL 請求引數。當 Spring Data 在類路徑上時,引數投影由 Spring Data 的介面投影提供。
要利用這一點,建立一個帶有 @ProjectedPayload
註解的介面,並將其宣告為控制器方法的引數。如果引數帶有 @Argument
註解,則它應用於 DataFetchingEnvironment.getArguments()
map 中的單個引數。如果未宣告 @Argument
,則投影作用於完整引數 map 中的頂級引數。
例如:
@Controller
public class BookController {
@QueryMapping
public Book bookById(BookIdProjection bookId) {
// ...
}
@MutationMapping
public Book addBook(@Argument BookInputProjection bookInput) {
// ...
}
}
@ProjectedPayload
interface BookIdProjection {
Long getId();
}
@ProjectedPayload
interface BookInputProjection {
String getName();
@Value("#{target.author + ' ' + target.name}")
String getAuthorAndName();
}
源
在 GraphQL Java 中,DataFetchingEnvironment
提供對欄位源(即父/容器)例項的訪問。要訪問它,只需宣告預期目標型別的方法引數即可。
@Controller
public class BookController {
@SchemaMapping
public Author author(Book book) {
// ...
}
}
源方法引數也有助於確定對映的型別名稱。如果 Java 類的簡單名稱與 GraphQL 型別匹配,則無需在 @SchemaMapping
註解中明確指定型別名稱。
給定源/父書籍物件列表, |
Subrange
當 Spring 配置中存在 CursorStrategy
bean 時,控制器方法支援 Subrange<P>
引數,其中 <P>
是從遊標轉換而來的相對位置。對於 Spring Data,ScrollSubrange
暴露了 ScrollPosition
。例如:
@Controller
public class BookController {
@QueryMapping
public Window<Book> books(ScrollSubrange subrange) {
ScrollPosition position = subrange.position().orElse(ScrollPosition.offset());
int count = subrange.count().orElse(20);
// ...
}
}
有關分頁和內建機制的概述,請參閱 分頁。
Sort
當 Spring 配置中存在 SortStrategy
bean 時,控制器方法支援將 Sort
作為方法引數。例如:
@Controller
public class BookController {
@QueryMapping
public Window<Book> books(Optional<Sort> optionalSort) {
Sort sort = optionalSort.orElse(Sort.by(..));
}
}
DataLoader
當你為實體註冊批次載入函式時,如 批次載入 中所述,你可以透過宣告型別為 DataLoader
的方法引數來訪問實體的 DataLoader
,並使用它來載入實體:
@Controller
public class BookController {
public BookController(BatchLoaderRegistry registry) {
registry.forTypePair(Long.class, Author.class).registerMappedBatchLoader((authorIds, env) -> {
// return Map<Long, Author>
});
}
@SchemaMapping
public CompletableFuture<Author> author(Book book, DataLoader<Long, Author> loader) {
return loader.load(book.getAuthorId());
}
}
預設情況下,BatchLoaderRegistry
使用值型別的完整類名(例如,Author
的類名)作為註冊的鍵,因此只需宣告帶有泛型型別的 DataLoader
方法引數就足以在 DataLoaderRegistry
中找到它。作為回退,DataLoader
方法引數解析器也會嘗試使用方法引數名作為鍵,但通常不需要這樣做。
請注意,對於許多載入相關實體的情況,其中 @SchemaMapping
只是委託給 DataLoader
,你可以透過使用下一節中描述的 @BatchMapping 方法來減少樣板程式碼。
驗證
當找到 javax.validation.Validator
bean 時,AnnotatedControllerConfigurer
啟用對帶註解控制器方法的 Bean 驗證 支援。通常,該 bean 的型別是 LocalValidatorFactoryBean
。
Bean 驗證允許你宣告型別上的約束:
public class BookInput {
@NotNull
private String title;
@NotNull
@Size(max=13)
private String isbn;
}
然後,你可以使用 @Valid
註解控制器方法引數,以便在方法呼叫之前對其進行驗證:
@Controller
public class BookController {
@MutationMapping
public Book addBook(@Argument @Valid BookInput bookInput) {
// ...
}
}
如果在驗證期間發生錯誤,將丟擲 ConstraintViolationException
。你可以使用 異常 鏈來決定如何將其呈現給客戶端,方法是將其轉換為包含在 GraphQL 響應中的錯誤。
除了 @Valid 之外,你還可以使用 Spring 的 @Validated ,它允許指定驗證組。 |
Bean 驗證對於 @Argument
、@Arguments
和 @ProjectedPayload 方法引數很有用,但更普遍地適用於任何方法引數。
驗證與 Kotlin 協程
Hibernate Validator 與 Kotlin 協程方法不相容,並且在內省其方法引數時會失敗。有關相關問題和建議的解決方法,請參閱 spring-projects/spring-graphql#344 (comment)。 |
@BatchMapping
批次載入 透過使用 org.dataloader.DataLoader
延遲載入單個實體例項來解決 N+1 查詢問題,以便它們可以一起載入。例如:
@Controller
public class BookController {
public BookController(BatchLoaderRegistry registry) {
registry.forTypePair(Long.class, Author.class).registerMappedBatchLoader((authorIds, env) -> {
// return Map<Long, Author>
});
}
@SchemaMapping
public CompletableFuture<Author> author(Book book, DataLoader<Long, Author> loader) {
return loader.load(book.getAuthorId());
}
}
對於上面所示的載入關聯實體的直接情況,@SchemaMapping
方法除了委託給 DataLoader
之外什麼都沒做。這是可以使用 @BatchMapping
方法避免的樣板程式碼。例如:
@Controller
public class BookController {
@BatchMapping
public Mono<Map<Book, Author>> author(List<Book> books) {
// ...
}
}
上面變成 BatchLoaderRegistry
中的一個批次載入函式,其中鍵是 Book
例項,載入的值是它們的作者。此外,一個 DataFetcher
也透明地繫結到 Book
型別的 author
欄位,它只是將載入作者的任務委託給 DataLoader
,並提供其源/父 Book
例項。
作為唯一鍵使用時, |
預設情況下,欄位名預設為方法名,而型別名預設為輸入 List
元素型別的簡單類名。兩者都可以透過註解屬性自定義。型別名也可以從類級別的 @SchemaMapping
繼承。
方法引數
批次對映方法支援以下引數:
方法引數 | 描述 |
---|---|
|
源/父物件。 |
|
如果可用,從 Spring Security 上下文獲取。 |
|
用於從 |
|
用於從 |
|
在 GraphQL Java 中可用於
|
返回值
批次對映方法可以返回:
返回型別 | 描述 |
---|---|
|
一個以父物件為鍵,批次載入物件為值的 Map。 |
|
批次載入物件的序列,必須與傳遞給方法的源/父物件的順序相同。 |
|
命令式變體,例如不需要進行遠端呼叫。 |
|
要非同步呼叫的命令式變體。為此, |
帶有 |
適配為 |
在 Java 21+ 上,當 AnnotatedControllerConfigurer
配置了 Executor
後,具有阻塞方法簽名的控制器方法將非同步呼叫。預設情況下,如果控制器方法不返回 Flux
、Mono
、CompletableFuture
等非同步型別,並且也不是 Kotlin suspending 函式,則被視為阻塞方法。你可以在 AnnotatedControllerConfigurer
上配置一個阻塞控制器方法 Predicate
,以幫助確定哪些方法被視為阻塞方法。
Spring for GraphQL 的 Spring Boot starter 在設定屬性 spring.threads.virtual.enabled 時,會自動為 AnnotatedControllerConfigurer 配置一個用於虛擬執行緒的 Executor 。 |
介面批次對映
與 介面 Schema 對映 的情況一樣,當批次對映方法對映到 schema 介面欄位時,該對映會被替換為多個對映,每個實現該介面的 schema 物件型別對應一個對映。
這意味著,給定以下內容:
type Query {
activities: [Activity!]!
}
interface Activity {
id: ID!
coordinator: User!
}
type FooActivity implements Activity {
id: ID!
coordinator: User!
}
type BarActivity implements Activity {
id: ID!
coordinator: User!
}
type User {
name: String!
}
你可以像這樣編寫一個控制器:
@Controller
public class BookController {
@QueryMapping
public List<Activity> activities() {
// ...
}
@BatchMapping
Map<Activity, User> coordinator(List<Activity> activities) {
// Called for all Activity subtypes
}
}
如果需要,你可以為單個子型別接管對映:
@Controller
public class BookController {
@QueryMapping
public List<Activity> activities() {
// ...
}
@BatchMapping
Map<Activity, User> coordinator(List<Activity> activities) {
// Called for all Activity subtypes
}
@BatchMapping(field = "coordinator")
Map<Activity, User> fooCoordinator(List<FooActivity> activities) {
// ...
}
}
@GraphQlExceptionHandler
使用 @GraphQlExceptionHandler
方法以靈活的方法簽名處理資料獲取中的異常。當在控制器中宣告時,異常處理方法適用於同一控制器的異常。
@Controller
public class BookController {
@QueryMapping
public Book bookById(@Argument Long id) {
// ...
}
@GraphQlExceptionHandler
public GraphQLError handle(BindException ex) {
return GraphQLError.newError().errorType(ErrorType.BAD_REQUEST).message("...").build();
}
}
當在 @ControllerAdvice
中宣告時,異常處理方法適用於跨控制器的異常。
@ControllerAdvice
public class GlobalExceptionHandler {
@GraphQlExceptionHandler
public GraphQLError handle(BindException ex) {
return GraphQLError.newError().errorType(ErrorType.BAD_REQUEST).message("...").build();
}
}
透過 @GraphQlExceptionHandler
方法的異常處理會自動應用於控制器呼叫。要處理來自其他非基於控制器方法的 graphql.schema.DataFetcher
實現的異常,請從 AnnotatedControllerConfigurer
獲取 DataFetcherExceptionResolver
,並在 GraphQlSource.Builder
中將其註冊為 DataFetcherExceptionResolver。
方法簽名
異常處理方法支援靈活的方法簽名,其方法引數從 DataFetchingEnvironment
解析,並與 @SchemaMapping 方法的引數匹配。
支援的返回型別如下:
返回型別 | 描述 |
---|---|
|
將異常解析為單個欄位錯誤。 |
|
將異常解析為多個欄位錯誤。 |
|
解析異常但不包含響應錯誤。 |
|
將異常解析為單個錯誤、多個錯誤或沒有錯誤。返回值必須是 |
|
用於非同步解析,其中 |
名稱空間
在 schema 級別,查詢和 mutation 操作直接在 Query
和 Mutation
型別下定義。豐富的 GraphQL API 可以在這些型別下定義數十個操作,這使得探索 API 和分離關注點變得更加困難。你可以選擇在 GraphQL schema 中定義名稱空間。雖然這種方法有一些需要注意的地方,但你可以使用 Spring for GraphQL 帶註解的控制器實現這種模式。
透過名稱空間,你的 GraphQL schema 可以例如將查詢操作巢狀在頂級型別下,而不是直接列在 Query
下。這裡,我們將定義 MusicQueries
和 UserQueries
型別並將它們在 Query
下可用:
type Query {
music: MusicQueries
users: UserQueries
}
type MusicQueries {
album(id: ID!): Album
searchForArtist(name: String!): [Artist]
}
type Album {
id: ID!
title: String!
}
type Artist {
id: ID!
name: String!
}
type UserQueries {
user(login: String): User
}
type User {
id: ID!
login: String!
}
一個 GraphQL 客戶端會像這樣使用 album
查詢:
{
music {
album(id: 42) {
id
title
}
}
}
並得到以下響應:
{
"data": {
"music": {
"album": {
"id": "42",
"title": "Spring for GraphQL"
}
}
}
}
這可以在 @Controller
中使用以下模式實現:
import java.util.List;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.graphql.data.method.annotation.SchemaMapping;
import org.springframework.stereotype.Controller;
@Controller
@SchemaMapping(typeName = "MusicQueries") (1)
public class MusicController {
@QueryMapping (2)
public MusicQueries music() {
return new MusicQueries();
}
(3)
public record MusicQueries() {
}
@SchemaMapping (4)
public Album album(@Argument String id) {
return new Album(id, "Spring GraphQL");
}
@SchemaMapping
public List<Artist> searchForArtist(@Argument String name) {
return List.of(new Artist("100", "the Spring team"));
}
}
1 | 使用 @SchemaMapping 和 typeName 屬性註解控制器,以避免在方法上重複: |
2 | 為 "music" 名稱空間定義一個 @QueryMapping : |
3 | “music” 查詢返回一個“空”記錄,但也可以返回一個空 map: |
4 | 查詢現在被宣告為 "MusicQueries" 型別下的欄位。 |
你可以選擇使用 Spring Boot 透過 GraphQlSourceBuilderCustomizer
在執行時配置中配置它們,而不是在控制器中顯式宣告包裝型別("MusicQueries"、"UserQueries"):
import java.util.Collections;
import java.util.List;
import org.springframework.boot.autoconfigure.graphql.GraphQlSourceBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class NamespaceConfiguration {
@Bean
public GraphQlSourceBuilderCustomizer customizer() {
List<String> queryWrappers = List.of("music", "users"); (1)
return (sourceBuilder) -> sourceBuilder.configureRuntimeWiring((wiringBuilder) ->
queryWrappers.forEach((field) -> wiringBuilder.type("Query",
(builder) -> builder.dataFetcher(field, (env) -> Collections.emptyMap()))) (2)
);
}
}
1 | 列出 "Query" 型別的所有包裝型別: |
2 | 手動為每個包裝型別宣告資料獲取器,並返回一個空 Map: |