由于项目架构微服务SpringCloud的方式部署,独立了文件系统微服务,其他服务需要调到此服务。但是发现调用不是报错就是文件系统接收端接收到的file为空。
先抛下我的pom引用的版本支持:
1.spring-cloud-starter-openfeign 版本为 :2.1.1.RELEASE
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
2.原有依赖的 feign-form-spring 等包,用来支持跨服务间文件传输
<dependency> <groupId>io.github.openfeign.form</groupId> <artifactId>feign-form</artifactId> <version>3.0.3</version> </dependency> <dependency> <groupId>io.github.openfeign.form</groupId> <artifactId>feign-form-spring</artifactId> <version>3.0.3</version> </dependency>
同时为了支持多服务:
3.服务端的文件上传代码如下:
/** * 上传文件 单个 * * @param file * @param sorId * @param fileType * @return * @throws Exception */ @RequestMapping(value = {"/addfilesor","/pturl/addfilesor"}, produces = {MediaType.APPLICATION_JSON_UTF8_VALUE}, consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public R addFileSor(@RequestPart( name ="file", value = "file", required = false) MultipartFile file, @RequestParam("sorId") String sorId, @RequestParam("fileType") String fileType) throws Exception { return sorFileService.addFileSor(file, sorId, fileType, getUser()); }
/** * 上传文件 批量 * * @return * @throws Exception */ @RequestMapping(value = {"/addfilesorbath","/pturl/addfilesorbath"} , produces = {MediaType.APPLICATION_JSON_UTF8_VALUE}, consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public R addfilesorBath(@RequestPart("file") MultipartFile[] file, @RequestParam("sorId") String sorId, @RequestParam("fileType") String fileType) throws Exception { return sorFileService.addfilesorBath(file, sorId, fileType, getUser()); }
其中MultipartFile 需要用@RequestPart 来注解,@RequestPart和@RequestParam的区别请自行百度。
3.消费端代码如下:
3.1 PubAppClient 消费端接口 如下FeignMultipartSupportConfig类是为了支持跨服务文件传输做的配置。下面会讲到
@FeignClient(value = "qboa-pubapp", configuration = FeignMultipartSupportConfig.class) public interface PubAppClient {
/** * 单个文件上传 * * @param file * @param sorId * @param fileType * @return * @throws Exception */ @RequestMapping(value = "/filesor/pturl/addfilesor", produces = {MediaType.APPLICATION_JSON_UTF8_VALUE}, consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public R addFileSor(@RequestPart(name = "file", value = "file", required = false) MultipartFile file, @RequestParam("sorId") String sorId, @RequestParam("fileType") String fileType) throws Exception; /** * 批量文件上传 * * @param file * @param sorId * @param fileType * @return * @throws Exception */ @RequestMapping(value = "/filesor/pturl/addfilesorbath", produces = {MediaType.APPLICATION_JSON_UTF8_VALUE}, consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public R addfilesorBath(@RequestPart("file") MultipartFile[] file, @RequestParam("sorId") String sorId, @RequestParam("fileType") String fileType) throws Exception;
}
3.2 FeignMultipartSupportConfig 中 最重要的一行代码:return new FeignSpringFormEncoder(new SpringEncoder(messageConverters)) 中重写,原生SpringFormEncoder 有个bug没有判断MultipartFile数组类型。
package com.qboa.oa.config.ftp;/** /** *说明: *@auther Luojie *@date 2020/03/24 10:36 */ import feign.codec.Encoder; import feign.form.spring.SpringFormEncoder; import org.springframework.beans.factory.ObjectFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.http.HttpMessageConverters; import org.springframework.cloud.openfeign.support.SpringEncoder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Scope; @Configuration public class FeignMultipartSupportConfig { @Autowired private ObjectFactory<HttpMessageConverters> messageConverters; @Bean @Scope("prototype") @Primary public Encoder feignEncoder() { // return new SpringFormEncoder(new SpringEncoder(messageConverters)); //支持多文件传输 return new FeignSpringFormEncoder(new SpringEncoder(messageConverters)); } }
3.3 FeignSpringFormEncoder 类中重写 支持MultipartFile数组,网上抄的至于抄谁的我已经忘记了。。。原博主不要打死我
package com.qboa.oa.config.ftp; import feign.RequestTemplate; import feign.codec.EncodeException; import feign.codec.Encoder; import feign.form.FormEncoder; import feign.form.MultipartFormContentProcessor; import feign.form.spring.SpringManyMultipartFilesWriter; import feign.form.spring.SpringSingleMultipartFileWriter; import lombok.val; import org.springframework.web.multipart.MultipartFile; import java.lang.reflect.Type; import java.util.Collections; import java.util.Map; import static feign.form.ContentType.MULTIPART; /** * fegin文件解析 */ public class FeignSpringFormEncoder extends FormEncoder { /** * Constructor with the default Feign's encoder as a delegate. */ public FeignSpringFormEncoder() { this(new Encoder.Default()); } /** * Constructor with specified delegate encoder. * * @param delegate delegate encoder, if this encoder couldn't encode object. */ public FeignSpringFormEncoder(Encoder delegate) { super(delegate); val processor = (MultipartFormContentProcessor) getContentProcessor(MULTIPART); processor.addFirstWriter(new SpringSingleMultipartFileWriter()); processor.addFirstWriter(new SpringManyMultipartFilesWriter()); } @Override public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException { if (bodyType.equals(MultipartFile.class)) { MultipartFile file = (MultipartFile) object; Map<String, Object> data = Collections.singletonMap(file.getName(), object); super.encode(data, MAP_STRING_WILDCARD, template); return; } else if (bodyType.equals(MultipartFile[].class)) { MultipartFile[] file = (MultipartFile[]) object; if (file != null) { Map<String, Object> data = Collections.singletonMap(file.length == 0 ? "" : file[0].getName(), object); super.encode(data, MAP_STRING_WILDCARD, template); return; } } super.encode(object, bodyType, template); } private boolean isMultipartFileCollection(Object object) { if (!(object instanceof Iterable)) { return false; } val iterable = (Iterable<?>) object; val iterator = iterable.iterator(); return iterator.hasNext() && iterator.next() instanceof MultipartFile; } }
3.5 写接收界面传递的controller了
@RequestMapping("/saveUpload") public R saveUpload(@RequestPart(value = "file") MultipartFile[] files, @RequestParam("patNo") String patNo, @RequestParam(value = "docId", required = false) String docId, @RequestParam("fileType") String fileType) throws Exception { Long userId = getUserId(); String ret = reqAndTransMangeService.saveUpload(files, patNo, docId, fileType, userId); if (!RCodestants.SUCCESS.equals(ret)) { return R.error(ret); } return R.ok(); }
service
@Override public String saveUpload(MultipartFile[] files, String patNo, String docId, String fileType, Long userId) throws Exception { //业务代码略略略 //调用文件公共方法 单个测试 pubAppClient.addFileSor(files[0], patNo, fileType); //调用文件公共方法 多个测试 pubAppClient.addfilesorBath(files, patNo, fileType); return RCodestants.SUCCESS; }
按照上面配置我们抛下起来出现了以下血案:
血案一:
调用的时候 the request was rejected because no multipart boundary was found
案情分析:该异常的源码抛出地方及原因可以看看
破案: 可以看到是request中boundary的为空,但是我在接受前台的时候有,跨服务的时候丢了(这些可以打印出来分析)
引发1 的Content-Type 中 multipart/form-data 可能在feign调用的时候没传递或传递的时候被更改了
2.未使用@RequestPart (我的排除了,你们的可以检查检查)
3. 就是引入的eign-form-spring 等包 和spring-cloud-starter-openfeign 版本
验证1:启动类中加了FeignClientInterceptor feign拦截器
@SpringBootApplication @EnableFeignClients @EnableScheduling public class QboaOaApplication { public static void main(String[] args) { SpringApplication.run(QboaOaApplication.class, args); } @Bean public FeignClientInterceptor getFeignClientInterceptor() { return new FeignClientInterceptor(); } }
FeignClientInterceptor 拦截器我突发奇想,想把request都传递下去包括原有cont-type
package com.qboa.common.interceptor; import com.alibaba.fastjson.JSONObject; import feign.RequestInterceptor; import feign.RequestTemplate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.Part; import java.util.Collection; import java.util.Enumeration; /** * Feign拦截器 * * @author Administrator * @version 1.0 **/ public class FeignClientInterceptor implements RequestInterceptor { private static final Logger logger = LoggerFactory.getLogger(FeignClientInterceptor.class); @Override public void apply(RequestTemplate requestTemplate) { ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (requestAttributes != null) { HttpServletRequest request = requestAttributes.getRequest(); //取出当前请求的header,找到jwt令牌 Enumeration<String> headerNames = request.getHeaderNames(); if (headerNames != null) { while (headerNames.hasMoreElements()) { String headerName = headerNames.nextElement(); String headerValue = request.getHeader(headerName); //临时解决支付表单改为json // if("/startpay/patTransNotifyUrl" .equals(request.getServletPath()) // && "content-type".equals(headerName)){ // headerValue="application/json;charset=UTF-8"; // } // 将header向下传递 requestTemplate.header(headerName, headerValue); } } } } }
由此从血案1引发了血案2,导致我服务端接收的file 文件一直是null 如果接收端一直file一直是null的可以看看是否是因为这原因引起的。还有接收端的 参数名称也可以检查是否一直,检验方法就是把接收端的request和消费端传递的request中的参数打印出来对比下。
查找了下原有找到了解决方案:
在FeignClientInterceptor 的 requestTemplate.header(headerName, headerValue);之前过滤掉content-type原因可以看引发1中的链接。博主讲得简洁明了!!!
//解决跨服务文件上传 防止请求头Content-Type的boundary被更改 if ("content-type".equals(headerName)) { continue; }
由此血案2 解决,回到血案1,时间节点一天。抱着司马当活马医的心态替换了feign form版本
<!-- Feign进行跨服务传递文件依赖 --> <!--spring feign form 表单提交相关 请不要更换版本否则批量会报错 --> <dependency> <groupId>io.github.openfeign.form</groupId> <artifactId>feign-form</artifactId> <version>3.8.0</version> </dependency> <dependency> <groupId>io.github.openfeign.form</groupId> <artifactId>feign-form-spring</artifactId> <version>3.8.0</version> </dependency> <!-- Feign进行跨服务传递文件依赖结束 -->
然后发现问题居然好了, 唉版本问题重于泰山,不重视的话出来的问题真的难以觉察。本着人人为我,我为人人的心,不忍心看到像我这样的菜鸟们不重复趟这趟雷,含泪写下了这篇总结,以及感谢各位无私博主,特别感谢两位大牛分享。
ps:含泪哭诉,如果正好解决你的问题,请点个赞吧~~~~