RestTemplate OR Spring Cloud Feign 上传文件
SpringBoot,通过RestTemplate 或者 Spring Cloud Feign,上传文件(支持多文件上传),服务端接口是MultipartFile接收。
将文件的字节流,放入ByteArrayResource中,并重写getFilename方法。
然后将ByteArrayResource放入MultiValueMap中(如果是Feign调用,方法里传参就是MultiValueMap),
然后进行上传时,Spring会自动识别到Map中的文件数据,然后通过FormHttpMessageConverter,将数据转成form表单型的multipart/formdata请求。
这里有个坑!
Spring web 4里面的FormHttpMessageConverter在将文件转成formdata时,会将文件名转成Byte[],但是使用的编码却是写死 US-ASCII,该编码不支持中文,使用该编码转换后,中文变成?号,是无法转回来的。
我想到的解决方法:
1.将spring版本升到5,Spring5里面,该编码是可以传入修改的。Springboot,默认UTF8
2.客户端进行一次编码,比如URLEncoder。然后服务端进行Decoder。
贴部分代码:
Feign
调用方,使用Spring 的MultiValueMap类,将文件File 转成 Resource,如果多个文件,则可以循环 用 add 方法,放入一个key下。
MultiValueMap是允许一key多值的。
或者,将多个Resource放入list,然后将list put 进 map中。
接收方
接收,可以用
(MultiValueMap map)
如果有其他的 值。
则是(MultiValueMap map,String XXX,String AAA)
多文件,则是
(MultiValueMap[] map,String XXX,String AAA)
或者用对象接收,也可以,不需要 @RequestBody 注解,这个注解是接收 http body里的json的。
(Bean bean),bean对象里,则是 MultiValueMap[] map,String XXX,String AAA
====================== 分割线 =================================
我在查询Feign上传文件时,还查到了另一种方式,就是专门给Feign方式提供的feign form相关Jar包,
引入Jar包后,然后进行相关配置,便可以在Feign方法中,参数直接传递MultipartFile。
该方法,或许也可以解决Spring4的编码问题。
=================== 分隔线:2018-11-8补充 关于 feign form用法 ====================
先引入相关jar:
<dependency> <groupId>io.github.openfeign.form</groupId> <artifactId>feign-form</artifactId> <version>3.2.2</version> </dependency> <dependency> <groupId>io.github.openfeign.form</groupId> <artifactId>feign-form-spring</artifactId> <version>3.2.2</version> </dependency>
@Bean public Logger.Level feignLoggerLevel() { return Logger.Level.FULL; } @Bean public Encoder feignFormEncoder() { return new SpringFormEncoder(); }
feign 调用方法 写法 :
save(@RequestPart MultipartFile file,@RequestParam("khbh") String khbh)
但是如果,参数多时,一个一个写较为麻烦,可以用
save(Map<String,?> param)
但是,经过测试,发现如果 map中value是null,会出现异常。(原因好像是因为,在将 值写入 formdata时,没有null判断)
========== 上面的用的 feign form
下面 有从网络上查到的,是类似于 feign form的解决方式:
http://b-l-east.iteye.com/blog/2373462
糞坑-SpringCloud中使用Feign的坑 示例如下: @FeignClient("service-resource") //@RequestMapping("/api/test") public interface TestResourceItg { @RequestMapping(value = "/api/test/raw", method = RequestMethod.POST, consumes = "application/x-www-form-urlencoded") public String raw1(@PathVariable("subject") String subject, // 标题 @RequestParam("content") String content); // 内容 } 说明: *使用RequestMapping中的consumes指定生成的请求的Content-Type *RequestParam指定的参数会拼接在URL之后,如: ?name=xxx&age=18 *PathVariable指定的参数会放到一个LinkedHashMap<String, ?>传入到feign的Encoder中进行处理,而在Spring中实现了该接口的Encoder为SpringEncoder,而该实现又会使用Spring中的HttpMessageConverter进行请求体的写入。 坑: *不要在接口类名上使用RequestMapping,虽然可以使用,但同时SpringMVC会把该接口的实例当作Controller开放出去,这个可以在启动的Mapping日志中查看到 *使用默认的SpringEncoder,在不指定consumes时,PathVariable中的参数会生成JSON字符串发送,且默认情况下不支持Form表单的生成方式,原因为:FormHttpMessageConverter只能处理MultiValueMap,而使用PathVariable参数被放在了HashMap中。默认更不支持文件上传。其实已经有支持处理各种情况的HttpMessageConverter存在。 填坑: *支持Form表单提交:只需要编写一个支持Map的FormHttpMessageConverter即可,内部可调用FormHttpMessageConverter的方法简化操作。 *支持文件上传:只需要把要上传的文件封装成一个Resource(该Resource一定要实现filename接口,这个是把请求参数解析成文件的标识),使用默认的ResourceHttpMessageConverter处理即可。 *支持处理MultipartFile参数:编写一个支持MultipartFile的MultipartFileHttpMessageConverter即可,内部可调用ResourceHttpMessageConverter实现,同时注意需要将其添加至FormHttpMessageConverter的Parts中,并重写FormHttpMessageConverter的getFilename方法支持从MultipartFile中获取filename *所有的HttpMessageConverter直接以@Bean的方式生成即可,spring会自动识别添加 完美支持表单和文件上传: 方案一: 使用附件中的MapFormHttpMessageConverter.java和MultipartFileHttpMessageConverter.java 在Spring中进行如下配置即可 @Bean public MapFormHttpMessageConverter mapFormHttpMessageConverter(MultipartFileHttpMessageConverter multipartFileHttpMessageConverter) { MapFormHttpMessageConverter mapFormHttpMessageConverter = new MapFormHttpMessageConverter(); mapFormHttpMessageConverter.addPartConverter(multipartFileHttpMessageConverter); return mapFormHttpMessageConverter; } @Bean public MultipartFileHttpMessageConverter multipartFileHttpMessageConverter() { return new MultipartFileHttpMessageConverter(); } 方案二: 使用FeignSpringFormEncoder.java 在Spring中配置如下: @Bean public Encoder feignEncoder(ObjectFactory<HttpMessageConverters> messageConverters) { return new FeignSpringFormEncoder(messageConverters); } 推荐使用方案一 方案二为参考https://github.com/pcan/feign-client-test而来,未测
上面方案中所用代码,贴在下面:
package com.access.service.saas.cmpt.utl; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collections; import java.util.List; import org.springframework.core.io.InputStreamResource; import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpOutputMessage; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotWritableException; import org.springframework.http.converter.ResourceHttpMessageConverter; import org.springframework.web.multipart.MultipartFile; /** * @author elvis.xu * @since 2017-05-09 11:17 */ public class MultipartFileHttpMessageConverter implements HttpMessageConverter<MultipartFile> { protected List<MediaType> supportedMediaTypes = new ArrayList<MediaType>(); protected ResourceHttpMessageConverter resourceHttpMessageConverter; public MultipartFileHttpMessageConverter() { supportedMediaTypes.add(MediaType.APPLICATION_OCTET_STREAM); resourceHttpMessageConverter = new ResourceHttpMessageConverter(); } public void setSupportedMediaTypes(List<MediaType> supportedMediaTypes) { this.supportedMediaTypes = supportedMediaTypes; } @Override public List<MediaType> getSupportedMediaTypes() { return Collections.unmodifiableList(this.supportedMediaTypes); } @Override public boolean canRead(Class<?> clazz, MediaType mediaType) { return false; } @Override public boolean canWrite(Class<?> clazz, MediaType mediaType) { if (!MultipartFile.class.isAssignableFrom(clazz)) { return false; } if (mediaType == null || MediaType.ALL.equals(mediaType)) { return true; } for (MediaType supportedMT : getSupportedMediaTypes()) { if (supportedMT.isCompatibleWith(mediaType)) { return true; } } return false; } @Override public MultipartFile read(Class<? extends MultipartFile> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { return null; } @Override public void write(MultipartFile file, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { MultipartFileResource multipartFileResource = new MultipartFileResource(file); resourceHttpMessageConverter.write(multipartFileResource, contentType, outputMessage); } public static class MultipartFileResource extends InputStreamResource { private final String filename; private final long size; public MultipartFileResource(MultipartFile multipartFile) throws IOException { this(multipartFile.getOriginalFilename(), multipartFile.getSize(), multipartFile.getInputStream()); } public MultipartFileResource(String filename, long size, InputStream inputStream) { super(inputStream); this.size = size; this.filename = filename; } @Override public String getFilename() { return this.filename; } @Override public InputStream getInputStream() throws IOException, IllegalStateException { return super.getInputStream(); //To change body of generated methods, choose Tools | Templates. } @Override public long contentLength() throws IOException { return size; } } }
package com.access.service.saas.cmpt.utl; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.reflect.Type; import java.nio.charset.Charset; import java.util.Arrays; import java.util.List; import java.util.Map; import feign.RequestTemplate; import feign.codec.EncodeException; import org.springframework.beans.factory.ObjectFactory; import org.springframework.boot.autoconfigure.web.HttpMessageConverters; import org.springframework.cloud.netflix.feign.support.SpringEncoder; import org.springframework.core.io.InputStreamResource; import org.springframework.core.io.Resource; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpOutputMessage; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.util.LinkedMultiValueMap; import org.springframework.web.multipart.MultipartFile; /** * @author elvis.xu * @since 2017-04-11 15:33 */ public class FeignSpringFormEncoder extends SpringEncoder { protected ObjectFactory<HttpMessageConverters> messageConverters; protected HttpHeaders multipartHeaders = new HttpHeaders(); public static final Charset UTF_8 = Charset.forName("UTF-8"); public FeignSpringFormEncoder(ObjectFactory<HttpMessageConverters> messageConverters) { super(messageConverters); this.messageConverters = messageConverters; multipartHeaders.setContentType(MediaType.MULTIPART_FORM_DATA); } protected static boolean isFormRequest(Type type) { return MAP_STRING_WILDCARD.equals(type); } protected static boolean isMultipart(Object body, Type bodyType) { if (isFormRequest(bodyType)) { Map<String, ?> map = (Map<String, ?>) body; for (Map.Entry<String, ?> entry : map.entrySet()) { Object value = entry.getValue(); if (isMultipartFile(value) || isMultipartFileArray(value)) { return true; } } } return false; } protected static boolean isMultipartFile(Object obj) { return obj instanceof MultipartFile; } protected static boolean isMultipartFileArray(Object o) { return o != null && o.getClass().isArray() && MultipartFile.class.isAssignableFrom(o.getClass().getComponentType()); } @Override public void encode(Object requestBody, Type bodyType, RequestTemplate request) throws EncodeException { if (isMultipart(requestBody, bodyType)) { encodeMultipartFormRequest((Map<String, ?>) requestBody, request); } else { super.encode(requestBody, bodyType, request); } } /** * Encodes the request as a multipart form. It can detect a single {@link MultipartFile}, an * array of {@link MultipartFile}s, or POJOs (that are converted to JSON). * * @param formMap * @param template * @throws EncodeException */ private void encodeMultipartFormRequest(Map<String, ?> formMap, RequestTemplate template) throws EncodeException { if (formMap == null) { throw new EncodeException("Cannot encode request with null form."); } LinkedMultiValueMap<String, Object> map = new LinkedMultiValueMap<>(); for (Map.Entry<String, ?> entry : formMap.entrySet()) { Object value = entry.getValue(); if (isMultipartFile(value)) { map.add(entry.getKey(), encodeMultipartFile((MultipartFile) value)); } else if (isMultipartFileArray(value)) { encodeMultipartFiles(map, entry.getKey(), Arrays.asList((MultipartFile[]) value)); } else { map.add(entry.getKey(), encodeJsonObject(value)); } } encodeRequest(map, multipartHeaders, template); } /** * Wraps a single {@link MultipartFile} into a {@link HttpEntity} and sets the * {@code Content-type} header to {@code application/octet-stream} * * @param file * @return */ private HttpEntity<?> encodeMultipartFile(MultipartFile file) { HttpHeaders filePartHeaders = new HttpHeaders(); filePartHeaders.setContentType(MediaType.APPLICATION_OCTET_STREAM); try { Resource multipartFileResource = new MultipartFileResource(file.getOriginalFilename(), file.getSize(), file.getInputStream()); return new HttpEntity<>(multipartFileResource, filePartHeaders); } catch (IOException ex) { throw new EncodeException("Cannot encode request.", ex); } } /** * Fills the request map with {@link HttpEntity}s containing the given {@link MultipartFile}s. * Sets the {@code Content-type} header to {@code application/octet-stream} for each file. * * @param map the current request map. * @param name the name of the array field in the multipart form. * @param files */ private void encodeMultipartFiles(LinkedMultiValueMap<String, Object> map, String name, List<? extends MultipartFile> files) { HttpHeaders filePartHeaders = new HttpHeaders(); filePartHeaders.setContentType(MediaType.APPLICATION_OCTET_STREAM); try { for (MultipartFile file : files) { Resource multipartFileResource = new MultipartFileResource(file.getOriginalFilename(), file.getSize(), file.getInputStream()); map.add(name, new HttpEntity<>(multipartFileResource, filePartHeaders)); } } catch (IOException ex) { throw new EncodeException("Cannot encode request.", ex); } } /** * Wraps an object into a {@link HttpEntity} and sets the {@code Content-type} header to * {@code application/json} * * @param o * @return */ private HttpEntity<?> encodeJsonObject(Object o) { HttpHeaders jsonPartHeaders = new HttpHeaders(); jsonPartHeaders.setContentType(MediaType.APPLICATION_JSON); return new HttpEntity<>(o, jsonPartHeaders); } /** * Calls the conversion chain actually used by * {@link org.springframework.web.client.RestTemplate}, filling the body of the request * template. * * @param value * @param requestHeaders * @param template * @throws EncodeException */ private void encodeRequest(Object value, HttpHeaders requestHeaders, RequestTemplate template) throws EncodeException { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); HttpOutputMessage dummyRequest = new HttpOutputMessageImpl(outputStream, requestHeaders); try { Class<?> requestType = value.getClass(); MediaType requestContentType = requestHeaders.getContentType(); for (HttpMessageConverter<?> messageConverter : messageConverters.getObject().getConverters()) { if (messageConverter.canWrite(requestType, requestContentType)) { ((HttpMessageConverter<Object>) messageConverter).write(value, requestContentType, dummyRequest); break; } } } catch (IOException ex) { throw new EncodeException("Cannot encode request.", ex); } HttpHeaders headers = dummyRequest.getHeaders(); if (headers != null) { for (Map.Entry<String, List<String>> entry : headers.entrySet()) { template.header(entry.getKey(), entry.getValue()); } } /* we should use a template output stream... this will cause issues if files are too big, since the whole request will be in memory. */ template.body(outputStream.toByteArray(), UTF_8); } /** * Dummy resource class. Wraps file content and its original name. */ static class MultipartFileResource extends InputStreamResource { private final String filename; private final long size; public MultipartFileResource(String filename, long size, InputStream inputStream) { super(inputStream); this.size = size; this.filename = filename; } @Override public String getFilename() { return this.filename; } @Override public InputStream getInputStream() throws IOException, IllegalStateException { return super.getInputStream(); //To change body of generated methods, choose Tools | Templates. } @Override public long contentLength() throws IOException { return size; } } /** * Minimal implementation of {@link org.springframework.http.HttpOutputMessage}. It's needed to * provide the request body output stream to * {@link org.springframework.http.converter.HttpMessageConverter}s */ private class HttpOutputMessageImpl implements HttpOutputMessage { private final OutputStream body; private final HttpHeaders headers; public HttpOutputMessageImpl(OutputStream body, HttpHeaders headers) { this.body = body; this.headers = headers; } @Override public OutputStream getBody() throws IOException { return body; } @Override public HttpHeaders getHeaders() { return headers; } } }
package com.access.service.saas.cmpt.utl; import java.io.IOException; import java.util.List; import java.util.Map; import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpOutputMessage; import org.springframework.http.MediaType; import org.springframework.http.converter.FormHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotWritableException; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.multipart.MultipartFile; /** * @author elvis.xu * @since 2017-05-09 10:58 */ public class MapFormHttpMessageConverter implements HttpMessageConverter<Map<String, ?>> { protected FormHttpMessageConverter formHttpMessageConverter; public MapFormHttpMessageConverter() { this.formHttpMessageConverter = new MultipartFormHttpMessageConverter(); } public void addPartConverter(HttpMessageConverter<?> partConverter) { this.formHttpMessageConverter.addPartConverter(partConverter); } @Override public boolean canRead(Class<?> clazz, MediaType mediaType) { return formHttpMessageConverter.canRead(clazz, mediaType); } @Override public List<MediaType> getSupportedMediaTypes() { return formHttpMessageConverter.getSupportedMediaTypes(); } @Override public boolean canWrite(Class<?> clazz, MediaType mediaType) { if (!Map.class.isAssignableFrom(clazz)) { return false; } if (mediaType == null || MediaType.ALL.equals(mediaType)) { return true; } for (MediaType supportedMediaType : getSupportedMediaTypes()) { if (supportedMediaType.isCompatibleWith(mediaType)) { return true; } } return false; } @Override public Map<String, ?> read(Class<? extends Map<String, ?>> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { return formHttpMessageConverter.read(null, inputMessage); } public void write(Map<String, ?> map, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { MultiValueMap<String, Object> multiMap = null; if (map != null) { if (map instanceof MultiValueMap) { multiMap = (MultiValueMap<String, Object>) map; } else { multiMap = new LinkedMultiValueMap<>(); for (Map.Entry<String, ?> entry : map.entrySet()) { multiMap.add(entry.getKey(), entry.getValue()); } } } formHttpMessageConverter.write(multiMap, contentType, outputMessage); } public static class MultipartFormHttpMessageConverter extends FormHttpMessageConverter { @Override protected String getFilename(Object part) { String rt = super.getFilename(part); if (rt == null && part instanceof MultipartFile) { return ((MultipartFile) part).getOriginalFilename(); } return null; } } }