Java 中的 GraphQL 上傳檔案

banq發表於2024-07-10

GraphQL改變了開發人員與 API 互動的方式,為傳統REST方法提供了一種簡化且強大的替代方案。

但是,由於 GraphQL 處理二進位制資料的性質,在 Java 中使用 GraphQL 處理檔案上傳(特別是在 Spring Boot 應用程式中)需要進行一些設定。在本教程中,我們將介紹如何在 Spring Boot應用程式中使用 GraphQL 設定檔案上傳。

使用 GraphQL 與 HTTP 上傳檔案
在使用 Spring Boot 開發 GraphQL API 領域中,遵守最佳實踐通常涉及利用標準HTTP請求來處理檔案上傳。

透過專用 HTTP 端點管理檔案上傳,然後透過 URL 或 ID 等識別符號將這些上傳連結到 GraphQL 變更,開發人員可以有效地最大限度地降低通常與將檔案上傳直接嵌入 GraphQL 查詢相關的複雜性和處理開銷。這種方法不僅簡化了上傳過程,還有助於避免與檔案大小限制和序列化需求相關的潛在問題,從而有助於實現更精簡和可擴充套件的應用程式結構。

儘管如此,某些情況下還是需要將檔案上傳直接合併到 GraphQL 查詢中。在這種情況下,將檔案上傳功能整合到 GraphQL API 中需要一種量身定製的策略,以仔細平衡使用者體驗和應用程式效能。因此,我們需要定義一個專門的標量型別來處理上傳。此外,此方法涉及部署特定機制來驗證輸入並將上傳的檔案對映到 GraphQL 操作中的正確變數。此外,上傳檔案需要請求主體的multipart/form-data內容型別,因此我們需要實現自定義HttpHandler。

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>

自定義上傳 標量型別
首先,我們在 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
}

在上面的定義中,我們還有描述引數來說明如何隨檔案傳遞附加資料。

UploadCoercing實現
在 GraphQL 上下文中,強制轉換是指將值從一種型別轉換為另一種型別的過程。這在處理自定義標量型別(如我們的Upload型別)時尤其重要。在這種情況下,我們需要定義如何解析(從輸入轉換)和序列化(轉換為輸出)與此型別關聯的值。

UploadCoercing實現對於以符合 GraphQL API 中檔案上傳的操作要求的方式管理這些轉換至關重要。

讓我們定義UploadCoercing類來正確處理Upload型別:

public class UploadCoercing implements Coercing<MultipartFile, Void> {
    @Override
    public Void serialize(Object dataFetcherResult) {
        throw new CoercingSerializeException(<font>"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 標量,這意味著從客戶端獲取檔案輸入並確保它在我們的伺服器端程式碼中正確表示為 MultipartFile 。

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, <font>"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 操作中的正確變數關聯起來。

實現檔案上傳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(<font>"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來儲存檔案,並傳遞檔案及其描述。

Spring Boot 配置 GraphQL 上傳支援
使用 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(<font>"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 標量型別。此標量型別與UploadCoercing相關聯,使 GraphQL API 能夠理解並正確處理檔案資料,確保檔案在上傳過程中正確序列化和反序列化。

為了處理傳入請求,特別是那些攜帶檔案上傳所需的multipart/form-data內容型別的請求,我們使用RouterFunction bean 定義。此函式擅長攔截這些特定型別的請求,使我們能夠透過 MultipartGraphQlHttpHandler 處理它們。此處理程式是解析多部分請求、提取檔案並將它們對映到 GraphQL 操作中的適當變數的關鍵,從而促進檔案上傳突變的執行。我們還使用 @Order (1)註釋應用正確的順序。

5.使用Postman測試檔案上傳
透過Postman測試 GraphQL API 中的檔案上傳功能需要採用非標準方法,因為內建的 GraphQL 有效負載格式不直接支援多部分/表單資料請求,而這對於上傳檔案至關重要。相反,我們必須手動構建多部分請求,模仿客戶端與 GraphQL 突變一起上傳檔案的方式。
在Body選項卡中,應將選擇設定為form-data。需要三個鍵值對:operations、map和具有根據map值命名的鍵名的檔案變數。

對於operations鍵,其值應為封裝 GraphQL 查詢和變數的 JSON 物件,其中檔案部分以 null 表示,作為佔位符。此部分的型別仍為Text。

{<font>"query": "mutation UploadFile($file: Upload!, $description: String!) { uploadFile(file: $file, description: $description) }","variables": {"file": null,"description": "Sample file description"}}


接下來,對映鍵需要一個值,該值是另一個 JSON 物件。這次,將檔案變數對映到包含檔案的表單欄位。如果我們將檔案附加到鍵0,則對映會將此鍵與 GraphQL 變數中的檔案變數明確關聯,確保伺服器正確解釋表單資料的哪一部分包含該檔案。此值也具有Text型別。

{<font>"0": ["variables.file"]}

最後,我們新增一個檔案本身,其鍵與map物件中的引用相匹配。在我們的例子中,我們使用0作為此值的鍵。與之前的文字值不同,此部分的型別為File。

執行請求後,我們應該得到一個 JSON 響應:

{
    <font>"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類來管理 multipart/form-data 請求,這是透過 GraphQL 突變上傳檔案所必需的。與使用 JSON 的標準 GraphQL 請求不同,檔案上傳需要多部分請求來處理二進位制檔案資料。

FileUploadDataFetcher類處理uploadFile突變。它提取並儲存上傳的檔案,並向客戶端傳送有關檔案上傳狀態的明確響應。

通常,使用純 HTTP 請求進行檔案上傳並透過 GraphQL 查詢傳遞結果 ID 會更有效。但是,有時直接使用 GraphQL 進行檔案上傳也是必要的。

相關文章