zoukankan      html  css  js  c++  java
  • 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;
    		}
    	}
    }
    

      

  • 相关阅读:
    Stream中的map
    项目中的process.bpmn的读-过程
    windows10打开switchHost,提示无修改权限
    Windows10安装node.js
    工作中的小发现
    启动redis
    call apply bin 的区别
    利用promise 让 函数按序执行
    uni-app 小程序
    插件 Generate css tree
  • 原文地址:https://www.cnblogs.com/yanqin/p/9178675.html
Copyright © 2011-2022 走看看