知識庫 / Spring / Spring Boot RSS 訂閱

使用 GraphQL 在 Java 上傳文件

Spring Boot
HongKong
4
11:05 AM · Dec 06 ,2025

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。需要三個鍵值對:operationsmap 和帶有鍵名的文件變量,其值由 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 進行文件上傳是必要的。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.