【问题标题】:How to upload files with graphql-java?如何使用 graphql-java 上传文件?
【发布时间】:2019-12-13 18:48:49
【问题描述】:

如果我使用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,所以我可以得到文件部分。

【问题讨论】:

标签: java file-upload graphql graphql-java graphql-java-tools


【解决方案1】:

由于没有字节的数据类型,我决定使用字符串类型发送 base64 中的数据。我先解释一下架构:

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

弹簧靴:

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 Client 发送者,例如在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)
    

关于java中的base64编码/解码这篇文章很有帮助:https://www.baeldung.com/java-base64-encode-and-decode

【讨论】:

    【解决方案2】:

    只是添加到上面的答案,对于像我这样可以找到 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;
      }
    

    【讨论】:

    • 处理完上传的文件后可以删除tmp文件吗?
    【解决方案3】:
    1. 在我们的模式中定义一个标量类型

      scalar Upload

      我们应该为 Upload 配置 GraphQLScalarType,在下面使用这个:

      @Configuration
      public class GraphqlConfig {
      
         @Bean
         public GraphQLScalarType uploadScalarDefine() {
            return ApolloScalars.Upload;
         } 
      }
      
    2. 然后我们将在 schema 中定义一个突变,并为 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;
        }
      }
      

      为什么我们返回 null?因为List&lt;Part&gt; parts总是为null,所以在resolver的方法中,从DataFetchingEnvironment中获取parts参数;

      environment.getArgument("文件")

    将其注册到 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. 要对此进行测试,请将以下表单数据(我们使用 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
    

    像这样:

    记得选择表单数据选项

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

    【讨论】:

    • @Val Bonn 你可以看看我的解决方案
    • 为了避免全局 ObjectMapper 覆盖(顺便说一句,这不是很好,因为它可能会在您不期望的地方带来副作用 :D )您可以使用相同的配置更好地注册此 bean:@Bean public PerFieldObjectMapperProvider perFieldObjectMapperProvider() {}
    • @SergeiDubinin 现在已经过时了吗?我正在尝试使用PerFieldObjectMapperProvider 来实现这个答案,但我无法让它工作。
    • @stereo 我能够让它工作,但使用它并不是很好,因为 graphql 总是将数据转换为 JSON,反之亦然,我改变了这种方法。我建议您在提交数据之前上传文件,因此在提交您的变异之前,您已经拥有上传文件的 URL。好多了
    • @stereo 这个anwser已经2年了,我不记得一些细节,但我可以告诉你的是我们现在可以使用Netflix DGS Graphql Framework:netflix.github.io/dgs,它已经实现了文件上传用graphql。你可以在这里找到文件上传文档:netflix.github.io/dgs/advanced/file-uploads
    【解决方案4】:

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

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

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

    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 的自定义标量,但返回我们自己的解析器类型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
    }
    

    如果它不适合你,请告诉我:)

    【讨论】:

    • 是的,你的解决方案确实对我有用,但有一个问题:通过graphql上传文件我们无法删除由graphql在tomcat tmp目录中生成的临时文件,我没有解决这个问题问题呢。你遇到过这个问题吗?
    • 很好的调用,但我认为它与 GraphQL 没有直接关系,而是与 Part 和 Java Servlet API 直接相关。我认为这与 InputStream 没有关闭有关。如果是这种情况,您应该能够在声明 byte[] content 变量后立即在标量内关闭它。检查此线程stackoverflow.com/questions/31741477/…
    • 我更新了我的代码示例并添加了part.delete(),还没有运行代码,但应该是对的:) 请告诉我。
    • 我在 try catch finally 块中使用了 part.delete() ,但它不起作用。实际上,part.delete() 并没有为我们删除临时文件。就像你说的那样,原因可能是 InputStream 没有关闭。
    • 3 注意这里 1. 你正在创建一个空数组,应该有byte[] content = inputStream.readAllBytes(); 2. 临时文件即使没有part.delete(); 也会被删除 3. 你不应该一次读取所有字节,但是您应该只将 InputStream 存储在 FileUpload 中(假设 10 GB 文件)
    猜你喜欢
    • 1970-01-01
    • 2022-01-23
    • 2020-03-09
    • 2019-11-22
    • 2021-01-07
    • 2019-10-13
    • 1970-01-01
    • 2021-09-03
    • 2017-03-25
    相关资源
    最近更新 更多