在 Java 中使用 GraphQL 上傳文件
一、簡介
GraphQL 改變了開發人員與 API 互動的方式,為傳統 REST 方法提供了簡化、強大的替代方案。
然而,由於 GraphQL 處理二進位資料的性質,在 Java 中使用 GraphQL 處理檔案上傳(尤其是在 Spring Boot 應用程式中)需要進行一些設定。在本教學中,我們將在 Spring Boot 應用程式中使用 GraphQL 設定檔上傳。
2. 使用 GraphQL 與 HTTP 上傳文件
在使用 Spring Boot 開發 GraphQL API 領域,遵守最佳實踐通常涉及利用標準 HTTP 請求來處理文件上傳。
透過專用 HTTP 端點管理檔案上傳,然後透過 URL 或 ID 等識別碼將這些上傳連結到 GraphQL 突變,開發人員可以有效地最小化通常與直接在 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.自訂Upload
Scalar
類型
首先,我們在 GraphQL 模式中定義一個自訂scalar
類型Upload
。 Upload scalar
類型的引入擴展了 GraphQL 處理二進位檔案資料的能力,使 API 能夠接受檔案上傳。自訂scalar
作為客戶端文件上傳請求和伺服器處理邏輯之間的橋樑,確保使用類型安全且結構化的方法來處理文件上傳。
讓我們在src/main/resources/file-upload/graphql/upload.graphqls
檔案中定義它:
scalar Upload
type Mutation {
uploadFile(file: Upload!, description: String!): String
}
type Query {
getFile: String
}
在上面的定義中,我們也有描述參數來說明如何隨文件一起傳遞附加資料。
3.2. UploadCoercing
實施
在 GraphQL 上下文中,強制是指將值從一種類型轉換為另一種類型的過程。在處理自訂scalar
類型(例如我們的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");
}
}
正如我們所看到的,這涉及將輸入值(來自查詢或突變)轉換為我們的應用程式可以理解和使用的 Java 類型。對於Upload
scalar
,這表示從客戶端取得檔案輸入並確保它在我們的伺服器端程式碼中正確表示為MultipartFile
。
3.3. MultipartGraphQlHttpHandler
:處理多部分請求
GraphQL 在其標準規範中旨在處理 JSON 格式的請求。這種格式適用於典型的CRUD操作,但在處理檔案上傳時效果不佳,檔案上傳本質上是二進位數據,不容易用 JSON 表示。 multipart/form-data
內容類型是透過 HTTP 提交表單和上傳檔案的標準,但處理這些請求需要以與標準 GraphQL 請求不同的方式解析請求正文。
預設情況下,GraphQL 伺服器不直接瞭解或處理多部分請求,通常會導致此類請求出現404 Not Found
回應。因此,我們需要實作一個處理程序來彌補這一差距,確保我們的應用程式正確處理multipart/form-data
內容類型。
我們來實作這個類別:
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);
}
MultipartGraphQlHttpHandler
類別中的handleMultipartRequest
方法處理multipart/form-data
請求。首先,我們從伺服器請求物件中提取 HTTP 請求,這允許存取請求中包含的多部分檔案和其他表單資料。然後,我們嘗試反序列化請求的「操作」部分(其中包含 GraphQL 查詢或突變)以及「映射」部分(指定如何將檔案對應到 GraphQL 操作中的變數)。
反序列化這些部分後,該方法繼續從請求中讀取實際的檔案上傳,使用「map」中定義的映射將每個上傳的檔案與 GraphQL 操作中的正確變數關聯起來。
3.4.實作檔案上傳DataFetcher
由於我們有用於上傳檔案的uploadFile
突變,因此我們需要實作特定的邏輯來接受來自客戶端的檔案和附加元資料並保存檔案。
在 GraphQL 中,架構中的每個欄位都連結到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);
}
}
當 GraphQL 框架呼叫此資料擷取器的get
方法時,它會從突變的參數中檢索檔案和可選描述。然後,它會呼叫FileStorageService
來儲存文件,並傳遞文件及其描述。
4. GraphQL 上傳支援的 Spring Boot 配置
使用 Spring Boot 將文件上傳整合到 GraphQL API 是一個多方面的過程,需要配置多個關鍵元件。
讓我們根據我們的實作來定義配置:
@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();
}
}
RuntimeWiringConfigurer
在此設定中發揮關鍵作用,使我們能夠將 GraphQL 模式的操作(例如突變和查詢)與相應的資料擷取器連結。這種連結對於uploadFile
突變至關重要,我們應用FileUploadDataFetcher
來處理檔案上傳過程。
此外, RuntimeWiringConfigurer
有助於在 GraphQL 模式中定義和整合自訂Upload
scalar
類型。這種scalar
類型與UploadCoercing
相關聯,使 GraphQL API 能夠理解並正確處理檔案數據,確保檔案在上傳過程中正確序列化和反序列化。
為了處理傳入請求,特別是那些攜帶檔案上傳所需的multipart/form-data
內容類型的請求,我們採用RouterFunction
bean 定義。該函數擅長攔截這些特定類型的請求,允許我們透過MultipartGraphQlHttpHandler
處理它們。此處理程序是解析多部分請求、提取檔案並將其對應到 GraphQL 操作中適當變數的關鍵,從而促進檔案上傳突變的執行。我們也使用@Order(1)
註釋來應用正確的順序。
5. 使用Postman測試檔上傳
透過 Postman 在 GraphQL API 中測試檔案上傳功能需要採用非標準方法,因為內建 GraphQL 有效負載格式不直接支援multipart/form-data
請求,而這對於上傳檔案至關重要。相反,我們必須手動建構一個多部分請求,模仿客戶端上傳檔案以及 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 變數中的檔案變數關聯起來,確保伺服器正確解釋表單資料的哪一部分包含該檔案。該值也具有Text
類型。
{"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"
}
}
六,結論
在本文中,我們探討如何使用 Spring Boot 將檔案上傳功能新增至 GraphQL API。我們首先引入一個名為Upload
的自訂標量類型,它處理 GraphQL 突變中的檔案資料。
然後,我們實作了MultipartGraphQlHttpHandler
類別來管理多部分/表單資料請求,這是透過 GraphQL 突變上傳檔案所必需的。與使用 JSON 的標準 GraphQL 請求不同,檔案上傳需要分段請求來處理二進位檔案資料。
FileUploadDataFetcher
類別處理uploadFile
突變。它提取並儲存上傳的文件,並向客戶端發送有關文件上傳狀態的明確回應。
通常,使用純 HTTP 請求進行檔案上傳並透過 GraphQL 查詢傳遞結果 ID 會更有效。然而,有時直接使用 GraphQL 進行檔案上傳是必要的。
與往常一樣,程式碼片段可以 在 GitHub 上取得。