1. 简介

GraphQL彻底改变了开发者与API交互的方式,为传统的REST架构提供了更高效、更强大的替代方案。

但在Java中(尤其是Spring Boot应用)处理GraphQL文件上传时,由于GraphQL对二进制数据的处理特性,需要额外配置。本教程将详细介绍如何在Spring Boot GraphQL应用中实现文件上传功能。

2. GraphQL vs HTTP文件上传方案对比

在Spring Boot开发GraphQL API时,最佳实践通常建议使用标准HTTP请求处理文件上传。

通过专用HTTP接口管理文件上传,再通过URL或ID等标识符将上传结果关联到GraphQL变更操作,可以有效降低直接在GraphQL查询中嵌入文件上传的复杂度和性能开销。这种方案不仅能简化上传流程,还能规避文件大小限制和序列化问题,构建更精简可扩展的应用架构。

但某些场景下确实需要直接在GraphQL查询中处理文件上传。此时需要平衡用户体验和应用性能,采取以下策略:

  • ✅ 定义专用标量类型处理上传
  • ✅ 实现输入验证机制
  • ✅ 将上传文件映射到GraphQL操作变量
  • ⚠️ 文件上传要求请求体为multipart/form-data类型,需实现自定义HttpHandler

3. GraphQL文件上传实现方案

本节将详细介绍在Spring Boot中集成GraphQL文件上传功能的完整步骤。我们使用专用启动器包启用GraphQL支持:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-graphql</artifactId>
    <version>3.3.0</version>
</dependency>

3.1 自定义Upload标量类型

首先在GraphQL schema中定义自定义标量类型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
}

上述定义中的description参数展示了如何与文件同时传递附加数据

3.2 UploadCoercing实现

在GraphQL中,coercing(强制转换)指将值从一种类型转换为另一种类型的过程。 这对处理自定义标量类型(如Upload)至关重要,需要明确定义:

  • 输入解析(从查询/变更转换)
  • 输出序列化(转换为输出格式)

UploadCoercing实现类负责管理这些转换:

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");
    }
}

核心逻辑是:

  1. 将客户端输入的文件转换为服务端可处理的MultipartFile
  2. 明确禁止序列化操作(Upload仅用于输入)
  3. 拒绝字面量解析(文件必须通过multipart请求上传)

3.3 MultipartGraphQlHttpHandler:处理multipart请求

GraphQL标准规范设计用于处理JSON格式请求,但文件上传的二进制特性使JSON难以胜任。默认情况下,GraphQL服务器无法直接处理multipart请求,常导致404错误。 需要实现专用处理器桥接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);
}

处理流程分解:

  1. 提取HTTP请求中的multipart文件和表单数据
  2. 反序列化operations部分(包含GraphQL查询/变更)
  3. 解析map部分(定义文件到变量的映射关系)
  4. 读取实际上传文件并按映射关联到GraphQL变量
  5. 构建标准GraphQL请求并执行

3.4 实现文件上传DataFetcher

针对uploadFile变更,需要实现专用逻辑接收文件和元数据。在GraphQL中,schema的每个字段都关联一个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);
    }
}

关键步骤:

  1. 从变更参数中提取文件和描述信息
  2. 调用FileStorageService存储文件
  3. 返回包含存储路径和描述的响应

4. Spring Boot GraphQL上传配置

在Spring Boot中集成GraphQL文件上传需要配置多个关键组件:

@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();
    }
}

配置要点:

  1. **RuntimeWiringConfigurer**:
    • uploadFile变更绑定到FileUploadDataFetcher
    • 注册自定义Upload标量类型及UploadCoercing
  2. **RouterFunction**:
    • 拦截multipart/form-data类型请求
    • 通过MultipartGraphQlHttpHandler处理文件上传
    • 使用@Order(1)确保优先处理

5. 使用Postman测试文件上传

通过Postman测试GraphQL文件上传需要手动构造multipart请求,因为内置GraphQL格式不支持multipart/form-data

请求配置步骤:

  1. Body标签页选择form-data
  2. 添加三个键值对:

operations键(类型:Text):

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

map键(类型:Text):

{"0": ["variables.file"]}

文件键(类型:File):

  • 键名:0(与map中定义一致)
  • 选择实际上传文件

执行后返回示例:

{
    "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添加文件上传功能的完整方案:

  1. 引入自定义Upload标量类型处理文件数据
  2. 实现MultipartGraphQlHttpHandler管理multipart请求(不同于标准JSON请求,文件上传必须使用multipart格式
  3. 通过FileUploadDataFetcher处理uploadFile变更,完成文件存储和响应

通常建议使用普通HTTP接口处理文件上传,再通过GraphQL传递文件ID。但在需要统一API的场景下,直接使用GraphQL上传也是可行的解决方案。

完整代码示例可在GitHub获取。


原始标题:Upload Files With GraphQL in Java | Baeldung