如何使用graphql-java上传文件?

How to upload files with graphql-java?

如果我使用 graphql-java,我不知道如何上传文件,谁能给我演示一下?我将不胜感激!

参考:https://github.com/graphql-java-kickstart/graphql-java-tools/issues/240

我在 springboot 中使用 graphql-java-kickstart graphql-java-tools 试过了,但是没用

@Component
public class FilesUpload implements GraphQLMutationResolver {

    public Boolean testMultiFilesUpload(List<Part> parts, DataFetchingEnvironment env) {
        // get file parts from DataFetchingEnvironment, the parts parameter is not used
        List<Part> attchmentParts = env.getArgument("files");
        System.out.println(attchmentParts);
        return true;
    }
}

这是我的架构

type Mutation {
    testSingleFileUpload(file: Upload): UploadResult
}

我希望这个解析器可以打印attchmentParts,所以我可以得到文件部分。

  1. 在我们的模式中定义一个标量类型

    scalar Upload

    我们应该为上传配置 GraphQLScalarType,在下面使用它:

    @Configuration
    public class GraphqlConfig {
    
       @Bean
       public GraphQLScalarType uploadScalarDefine() {
          return ApolloScalars.Upload;
       } 
    }
    
  2. 然后我们将在模式中定义一个突变,并为 testMultiFilesUpload

    定义一个 GraphQLMutationResolver
    type Mutation {
      testMultiFilesUpload(files: [Upload!]!): Boolean
    }
    

这里是解析器:

public Boolean testMultiFilesUpload(List<Part> parts, DataFetchingEnvironment env) {
    // get file parts from DataFetchingEnvironment, the parts parameter is not use
    List<Part> attachmentParts = env.getArgument("files");
    int i = 1;
    for (Part part : attachmentParts) {
      String uploadName = "copy" + i;
      try {
        part.write("your path:" + uploadName);
      } catch (IOException e) {
        e.printStackTrace();
      }
      i++;
    }
    return true;   
  }
}
  1. javax.servlet.http.Part配置一个jackson反序列化器并将其注册到ObjectMapper

    public class PartDeserializer extends JsonDeserializer<Part> {
    
      @Override
      public Part deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {         
         return null;
      }
    }
    

    为什么我们 return null?因为 List<Part> parts 始终为 null ,在解析器的方法中,从 DataFetchingEnvironment 获取 parts 参数;

    environment.getArgument("files")

将其注册到 ObjectMapper:

@Bean
public ObjectMapper objectMapper() {
  ObjectMapper objectMapper = new ObjectMapper();
  objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
  SimpleModule module = new SimpleModule();
  module.addDeserializer(Part.class, new PartDeserializer());
  objectMapper.registerModule(module);
  return objectMapper;
}
  1. 为了测试这一点,post 将以下表单数据(我们使用 Postman)发送到 GraphQL 端点
operations

{ "query": "mutation($files: [Upload!]!) {testMultiFilesUpload(files:$files)}", "variables": {"files": [null,null] } }

map

{ "file0": ["variables.files.0"] , "file1":["variables.files.1"]}

file0

your file

file1

your file

像这样:

记住 select 表单数据选项

通过这个我们可以上传多个文件

主要问题是 graphql-java-tools 可能无法为包含 ListStringInteger 等非基本类型字段的解析器进行字段映射, Boolean, 等...

我们通过创建基本上类似于 ApolloScalar.Upload 的自定义标量解决了这个问题。但是我们 return 我们自己的解析器类型 FileUpload 不是 returning 类型 Part 的对象,它包含 String 的 contentType 和 String 的 inputStream byte[],然后字段映射工作,我们可以在解析器中读取 byte[]

首先,设置要在解析器中使用的新类型:

public class FileUpload {
    private String contentType;
    private byte[] content;

    public FileUpload(String contentType, byte[] content) {
        this.contentType = contentType;
        this.content = content;
    }

    public String getContentType() {
        return contentType;
    }

    public byte[] getContent() {
        return content;
    }
}

然后我们制作一个看起来很像 ApolloScalars.Upload 的自定义标量,但是 return 是我们自己的解析器类型 FileUpload:

public class MyScalars {
    public static final GraphQLScalarType FileUpload = new GraphQLScalarType(
        "FileUpload",
        "A file part in a multipart request",
        new Coercing<FileUpload, Void>() {

            @Override
            public Void serialize(Object dataFetcherResult) {
                throw new CoercingSerializeException("Upload is an input-only type");
            }

            @Override
            public FileUpload parseValue(Object input) {
                if (input instanceof Part) {
                    Part part = (Part) input;
                    try {
                        String contentType = part.getContentType();
                        byte[] content = new byte[part.getInputStream().available()];
                        part.delete();
                        return new FileUpload(contentType, content);

                    } catch (IOException e) {
                        throw new CoercingParseValueException("Couldn't read content of the uploaded file");
                    }
                } else if (null == input) {
                    return null;
                } else {
                    throw new CoercingParseValueException(
                            "Expected type " + Part.class.getName() + " but was " + input.getClass().getName());
                }
            }

            @Override
            public FileUpload parseLiteral(Object input) {
                throw new CoercingParseLiteralException(
                        "Must use variables to specify Upload values");
            }
    });
}

在解析器中,您现在可以从解析器参数中获取文件:

public class FileUploadResolver implements GraphQLMutationResolver {

    public Boolean uploadFile(FileUpload fileUpload) {

        String fileContentType = fileUpload.getContentType();
        byte[] fileContent = fileUpload.getContent();

        // Do something in order to persist the file :)


        return true;
    }
}

在架构中,您可以这样声明它:

scalar FileUpload

type Mutation {
    uploadFile(fileUpload: FileUpload): Boolean
}

如果它对你不起作用,请告诉我:)

只是为了补充上面的答案,对于像我这样可以找到 0 个使用 GraphQLSchemaGenerator 与模式优先方法上传文件示例的人来说,您只需创建一个 TypeMapper 并将其添加到您的 GraphQLSchemaGenerator:

public class FileUploadMapper implements TypeMapper {

  @Override
  public GraphQLOutputType toGraphQLType(
      final AnnotatedType javaType, final OperationMapper operationMapper,
      final Set<Class<? extends TypeMapper>> mappersToSkip, final BuildContext buildContext) {
    return MyScalars.FileUpload;
  }

  @Override
  public GraphQLInputType toGraphQLInputType(
      final AnnotatedType javaType, final OperationMapper operationMapper,
      final Set<Class<? extends TypeMapper>> mappersToSkip, final BuildContext buildContext) {
    return MyScalars.FileUpload;
  }

  @Override
  public boolean supports(final AnnotatedType type) {
     return type.getType().equals(FileUpload.class); //class of your fileUpload POJO from the previous answer
  }
}

然后在您构建 GraphQLSchema 的 GraphQL @Configuration 文件中:

public GraphQLSchema schema(GraphQLSchemaGenerator schemaGenerator) {
    return schemaGenerator
        .withTypeMappers(new FileUploadMapper()) //add this line
        .generate();
  }

然后在你的突变解析器中

  @GraphQLMutation(name = "fileUpload")
  public void fileUpload(      
      @GraphQLArgument(name = "file") FileUpload fileUpload //type here must be the POJO.class referenced in your TypeMapper
  ) {
    //do something with the byte[] from fileUpload.getContent();
    return;
  }

由于字节没有数据类型,我决定使用 String 类型发送 base64 中的数据。我解释一下,模式优先:

type Mutation{ 
  uploadCSV(filedatabase64: String!): Boolean
}

spring开机:

public DataFetcher<Boolean> uploadCSV() { 
    return dataFetchingEnvironment -> {
        String input= dataFetchingEnvironment.getArgument("filedatabase64");
        byte[] bytes = Base64.getDecoder().decode(input);
        //in my case is textfile:
        String strCSV = new String(bytes);
        //....
        return true;
    };
}

Http 客户端发件人,例如 python3:

import requests
import base64
import json

with open('myfile.csv', 'r',encoding='utf-8') as file:
    content = file.read().rstrip()
file.close()
    
base64data = base64.b64encode(content.encode()).decode()
url = 'https://www.misite/graphql/'
query = "mutation{uploadCSV(filedatabase64:\""+base64data+"\")}"
r = requests.post(url, json={'query': query})
print("response " + r.status_code + " " + r.text)
    

about base64 encoding/decoding in java 这篇文章很有帮助:https://www.baeldung.com/java-base64-encode-and-decode