1. 引言
GraphQL 已經改變了開發者與 API 交互的方式,它提供了一種比傳統 REST 方法更簡潔、更強大的替代方案。
然而,在 Java 中使用 GraphQL 處理文件上傳,尤其是在 Spring Boot 應用程序中,由於 GraphQL 處理二進制數據的特性,需要進行一些配置。在本教程中,我們將介紹如何在 Spring Boot 應用程序中使用 GraphQL 進行文件上傳。
2. 使用 GraphQL 與 HTTP 進行文件上傳
在 Spring Boot 中開發 GraphQL API 時,遵循最佳實踐通常涉及使用標準 HTTP 請求來處理文件上傳。
通過使用專用 HTTP 端點管理文件上傳,然後使用 URL 或 ID 等標識符將這些上傳與 GraphQL mutating 關聯,開發人員可以有效地減少與在 GraphQL 查詢中嵌入文件上傳相關的複雜性和處理開銷。 這種方法不僅簡化了上傳過程,也有助於避免與文件大小限制和序列化需求相關的潛在問題,從而有助於構建更流線型和可擴展的應用程序架構。
儘管如此,在某些情況下,必須在 GraphQL 查詢中直接包含文件上傳。 在這種情況下,將文件上傳功能集成到 GraphQL API 中需要一種仔細平衡用户體驗與應用程序性能的定製策略。 因此,我們需要為處理上傳定義一個專門的標量類型。 此外,這種方法還涉及部署用於驗證輸入和將上傳的文件映射到 GraphQL 操作中正確變量的特定機制。 此外,上傳文件需要使用請求主體中 multipart/form-data 內容類型,因此我們需要實現一個自定義 HttpHandler。
3. 在 GraphQL 中實現文件上傳
本節概述瞭如何使用 Spring Boot 在 GraphQL API 中集成文件上傳功能的方法。通過一系列步驟,我們將探索如何創建和配置關鍵組件,以便直接通過 GraphQL 查詢處理文件上傳。
在本指南中,我們將使用專門的 啓動包,以便在 Spring Boot 應用程序中啓用 GraphQL 支持:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-graphql</artifactId>
<version>3.3.0</version>
</dependency>3.1. 自定義 上傳標量類型
首先,我們在 GraphQL 模式中定義一個自定義標量類型 Upload。引入 Upload 標量類型擴展了 GraphQL 處理二進制文件數據的能力,使 API 能夠接受文件上傳。 自定義 標量作為客户端文件上傳請求與服務器處理邏輯之間的橋樑,確保了文件上傳在類型安全和結構化方面。
以下是在 src/main/resources/file-upload/graphql/upload.graphqls文件中定義的:
scalar Upload
type Mutation {
uploadFile(file: Upload!, description: String!): String
}
type Query {
getFile: String
}在上述定義中,我們還具有描述參數,用於説明如何將附加數據與文件一起傳遞。
3.2. 上傳轉換 實現
在 GraphQL 的上下文中,轉換是指將值從一種類型轉換為另一種類型的過程。 這在處理自定義 標量 類型,例如我們的 Upload 類型時尤其重要。 在這種情況下,我們需要定義與此類型關聯的值的解析(從輸入轉換)和序列化(轉換為輸出)方式。
UploadCoercing 實現對於以與 GraphQL API 中文件上傳的運營要求一致的方式管理這些轉換至關重要。
讓我們定義 UploadCoercing 類以正確處理 Upload 類型:
public class UploadCoercing implements Coercing<MultipartFile, Void> {
@Override
public Void serialize(Object dataFetcherResult) {
throw new CoercingSerializeException("Upload is an input-only type and cannot be serialized");
}
@Override
public MultipartFile parseValue(Object input) {
if (input instanceof MultipartFile) {
return (MultipartFile) input;
}
throw new CoercingParseValueException("Expected type MultipartFile but was " + input.getClass().getName());
}
@Override
public MultipartFile parseLiteral(Object input) {
throw new CoercingParseLiteralException("Upload is an input-only type and cannot be parsed from literals");
}
}如我們所見,這涉及到將輸入值(來自查詢或 mutation)轉換為我們的應用程序可以理解和使用的 Java 類型。對於 Upload scalar,這意味着從客户端獲取文件輸入,並確保它在我們的服務端代碼中正確地表示為 MultipartFile。
3.3. <em>MultipartGraphQlHttpHandler</em>: 處理多部分請求
GraphQL 在其標準規範中,旨在處理 JSON 格式的請求。這種格式對於典型的 CRUD 操作非常有效,但當處理文件上傳時,由於文件本質上是二進制數據,且不適合用 JSON 表示,因此效果不佳。 <em>multipart/form-data</em> 是一種用於通過 HTTP 提交表單和上傳文件(特別是當內容類型為多部分表單數據)的標準內容類型,但處理這些請求需要與標準 GraphQL 請求不同地解析請求體。
默認情況下,GraphQL 服務器不理解或直接處理多部分請求,這通常會導致此類請求返回 <em>404 Not Found</em> 錯誤。因此,我們需要實現一個處理程序,以彌合這一差距,並確保我們的應用程序正確處理 <em>multipart/form-data</em> 內容類型。
讓我們來實現這個類:
public ServerResponse handleMultipartRequest(ServerRequest serverRequest) throws ServletException {
HttpServletRequest httpServletRequest = serverRequest.servletRequest();
Map<String, Object> inputQuery = Optional.ofNullable(this.<Map<String, Object>>deserializePart(httpServletRequest, "operations", MAP_PARAMETERIZED_TYPE_REF.getType())).orElse(new HashMap<>());
final Map<String, Object> queryVariables = getFromMapOrEmpty(inputQuery, "variables");
final Map<String, Object> extensions = getFromMapOrEmpty(inputQuery, "extensions");
Map<String, MultipartFile> fileParams = readMultipartFiles(httpServletRequest);
Map<String, List<String>> fileMappings = Optional.ofNullable(this.<Map<String, List<String>>>deserializePart(httpServletRequest, "map", LIST_PARAMETERIZED_TYPE_REF.getType())).orElse(new HashMap<>());
fileMappings.forEach((String fileKey, List<String> objectPaths) -> {
MultipartFile file = fileParams.get(fileKey);
if (file != null) {
objectPaths.forEach((String objectPath) -> {
MultipartVariableMapper.mapVariable(objectPath, queryVariables, file);
});
}
});
String query = (String) inputQuery.get("query");
String opName = (String) inputQuery.get("operationName");
Map<String, Object> body = new HashMap<>();
body.put("query", query);
body.put("operationName", StringUtils.hasText(opName) ? opName : "");
body.put("variables", queryVariables);
body.put("extensions", extensions);
WebGraphQlRequest graphQlRequest = new WebGraphQlRequest(serverRequest.uri(), serverRequest.headers().asHttpHeaders(), body, this.idGenerator.generateId().toString(), LocaleContextHolder.getLocale());
if (logger.isDebugEnabled()) {
logger.debug("Executing: " + graphQlRequest);
}
Mono<ServerResponse> responseMono = this.graphQlHandler.handleRequest(graphQlRequest).map(response -> {
if (logger.isDebugEnabled()) {
logger.debug("Execution complete");
}
ServerResponse.BodyBuilder builder = ServerResponse.ok();
builder.headers(headers -> headers.putAll(response.getResponseHeaders()));
builder.contentType(selectResponseMediaType(serverRequest));
return builder.body(response.toMap());
});
return ServerResponse.async(responseMono);
}handleMultipartRequest 方法位於 MultipartGraphQlHttpHandler 類中,處理 multipart/form-data 請求。首先,我們從服務器請求對象中提取 HTTP 請求,從而可以訪問請求中包含的 multipart 文件和其他表單數據。然後,我們嘗試反序列化請求中的“operations” 部分,其中包含 GraphQL 查詢或 mutation,以及“map” 部分,它指定如何將文件映射到 GraphQL 操作中的變量。
反序列化這些部分後,該方法繼續從請求中讀取實際的文件上傳,使用“map” 部分中定義的映射來將每個上傳的文件與 GraphQL 操作中的正確變量關聯。
3.4. 實現文件上傳 DataFetcher
由於我們擁有 uploadFile 變體用於上傳文件,因此我們需要實現特定的邏輯來接受客户端的文件和附加元數據,並保存文件。
在 GraphQL 中,schema 中的每個字段都與 DataFetcher 相關聯,一個組件負責檢索與該字段關聯的數據。
雖然某些字段可能需要特定的 DataFetcher 實現,這些實現能夠從數據庫或其他持久存儲系統檢索數據,但許多字段只是從內存對象中提取數據。 這種提取通常依賴於字段名稱,並利用標準 Java 對象模式來訪問所需的數據。
讓我們實現我們的 DataFetcher 接口實現:
@Component
public class FileUploadDataFetcher implements DataFetcher<String> {
private final FileStorageService fileStorageService;
public FileUploadDataFetcher(FileStorageService fileStorageService) {
this.fileStorageService = fileStorageService;
}
@Override
public String get(DataFetchingEnvironment environment) {
MultipartFile file = environment.getArgument("file");
String description = environment.getArgument("description");
String storedFilePath = fileStorageService.store(file, description);
return String.format("File stored at: %s, Description: %s", storedFilePath, description);
}
}當此數據獲取器中的 get 方法由 GraphQL 框架調用時,它會從 mutation 的參數中檢索文件和可選描述信息。然後,它會調用 FileStorageService 來存儲文件,並將文件及其描述信息傳遞給它。
4. 使用 Spring Boot 配置支持 GraphQL 上傳
將文件上傳集成到 GraphQL API 中,使用 Spring Boot 是一項多方面的過程,需要配置多個關鍵組件。
讓我們根據我們的實現來定義配置:
@Configuration
public class MultipartGraphQlWebMvcAutoconfiguration {
private final FileUploadDataFetcher fileUploadDataFetcher;
public MultipartGraphQlWebMvcAutoconfiguration(FileUploadDataFetcher fileUploadDataFetcher) {
this.fileUploadDataFetcher = fileUploadDataFetcher;
}
@Bean
public RuntimeWiringConfigurer runtimeWiringConfigurer() {
return (builder) -> builder
.type(newTypeWiring("Mutation").dataFetcher("uploadFile", fileUploadDataFetcher))
.scalar(GraphQLScalarType.newScalar()
.name("Upload")
.coercing(new UploadCoercing())
.build());
}
@Bean
@Order(1)
public RouterFunction<ServerResponse> graphQlMultipartRouterFunction(
GraphQlProperties properties,
WebGraphQlHandler webGraphQlHandler,
ObjectMapper objectMapper
) {
String path = properties.getPath();
RouterFunctions.Builder builder = RouterFunctions.route();
MultipartGraphQlHttpHandler graphqlMultipartHandler = new MultipartGraphQlHttpHandler(webGraphQlHandler, new MappingJackson2HttpMessageConverter(objectMapper));
builder = builder.POST(path, RequestPredicates.contentType(MULTIPART_FORM_DATA)
.and(RequestPredicates.accept(SUPPORTED_MEDIA_TYPES.toArray(new MediaType[]{}))), graphqlMultipartHandler::handleMultipartRequest);
return builder.build();
}
}運行時配置器 在該設置中發揮着關鍵作用,使我們能夠將 GraphQL 模式的操作(如 Mutation 和查詢)與相應的查詢器鏈接起來。 這種鏈接對於 uploadFile Mutation 至關重要,我們應用 FileUploadDataFetcher 來處理文件上傳過程。
此外,運行時配置器 也是定義和集成自定義 Upload 標量 類型到 GraphQL 模式中的關鍵。 這種 標量 類型,與 UploadCoercing 相關聯,使 GraphQL API 能夠理解和正確處理文件數據,從而確保文件在上傳過程中正確序列化和反序列化。
為了處理傳入的請求,特別是攜帶 multipart/form-data 內容類型的文件上傳請求,我們使用 RouterFunction Bean 定義。該函數擅長攔截這些特定類型的請求,從而使我們能夠通過 MultipartGraphQlHttpHandler 處理它們。該處理程序是解析 multipart 請求、提取文件並將它們映射到 GraphQL 操作中適當變量的關鍵,從而促進文件上傳 Mutation 的執行。我們還通過使用 @Order(1) 註解來確保正確的順序。
5. 使用 Postman 測試文件上傳
測試 GraphQL API 中的文件上傳功能需要採用非標準方法,因為內置的 GraphQL 負載格式不支持 multipart/form-data 請求,而 multipart/form-data 請求對於上傳文件至關重要。相反,我們必須手動構建 multipart 請求,模擬客户端上傳文件與 GraphQL 變體一起的方式。
在 Body 選項卡中,選擇應設置為 form-data。需要三個鍵值對:operations、map 和帶有鍵名的文件變量,其值由 map 確定。
對於 operations 鍵,值應為包含 GraphQL 查詢和變量的 JSON 對象,文件部分用 null 表示作為佔位符。該部分的類型保持為 Text。
{"query": "mutation UploadFile($file: Upload!, $description: String!) { uploadFile(file: $file, description: $description) }","variables": {"file": null,"description": "Sample file description"}}接下來,map 鍵需要一個值為另一個 JSON 對象。此操作將文件變量映射到包含文件的表單字段。如果將文件附加到鍵 0,則 map 將明確地將此鍵與 GraphQL 變量中的文件變量關聯,從而確保服務器正確地解釋表單數據中包含文件的部分。該值也具有 文本 類型。
{"0": ["variables.file"]}最後,我們為文件本身添加一個鍵,該鍵與 map對象中的引用匹配。 在我們的例子中,我們使用 0 作為該值的鍵。 與先前文本值不同,此部分的類型為 File。
執行請求後,我們應該收到一個 JSON 響應:
{
"data": {
"uploadFile": "File stored at: File uploaded successfully: C:\\Development\\TutorialsBaeldung\\tutorials\\uploads\\2023-06-21_14-22.bmp with description: Sample file description, Description: Sample file description"
}
}6. 結論
在本文中,我們探討了如何使用 Spring Boot 將文件上傳功能添加到 GraphQL API 中。我們首先介紹了自定義標量類型 Upload,該類型處理 GraphQL mutating 中的文件數據。
然後,我們實現了 MultipartGraphQlHttpHandler 類,用於管理 multipart/form-data 請求,這對於通過 GraphQL mutating 上傳文件是必需的。 與標準 GraphQL 請求使用 JSON 不同,文件上傳需要使用 multipart 請求來處理二進制文件數據。
FileUploadDataFetcher 類處理 uploadFile mutating。它提取和存儲上傳的文件,並向客户端發送清晰的響應,説明文件上傳狀態。
通常,對於文件上傳,使用簡單的 HTTP 請求並通過 GraphQL 查詢傳遞結果 ID 更有效。但是,有時直接使用 GraphQL 進行文件上傳是必要的。