服务调用Feign
1、简介
Feign 的英文表意为“假装,伪装,变形”,是一个http请求调用的轻量级框架(Netflix开发的声明式、模板化的HTTP客户端,其灵感来自Retrofit、JAXRS-2.0以及WebSocket),可以以Java接口注解的方式调用Http请求,而不用像Java中通过封装HTTP请求报文的方式直接调用。Feign通过处理注解,将请求模板化,当实际调用的时候,传入参数,根据参数再应用到请求上,进而转化成真正的请求,这种请求相对而言比较直观。Feign可帮助我们更加便捷、优雅地调用HTTP API。在Spring Cloud中,使用Feign非常简单——只需创建接口,并在接口上添加注解即可。Feign支持多种注解,例如Feign自带的注解或者JAX-RS注解等。Spring Cloud对Feign进行了增强,使其支持Spring MVC注解,另外还整合了Ribbon和Eureka,从而使得Feign的使用更加方便。
2、Feign解决什么问题?
封装了Http调用流程,更适合面向接口化的变成习惯 在服务调用的场景中,我们经常调用基于Http协议的服务,而我们经常使用到的框架可能有HttpURLConnection、Apache HttpComponnets、OkHttp3 、Netty等等,这些框架在基于自身的专注点提供了自身特性。而从角色划分上来看,他们的职能是一致的提供Http调用服务。具体流程如下:

3、Feign是如何设计的?

3.1PHASE 1. 基于面向接口的动态代理方式生成实现类
/*** 非全局关闭Hystrix @FeignClient(value = "ORGHRSERVICE", configuration = DisableHystrixConfiguration.class)* 参数必须加@RequestParam 否则服务端会报找不到参数错误* 如果是url 则必须加@PathVariable* 如果是传递对象,则接收端必须加@RequestBody* */@FeignClient(name = "orghrservice", fallback = OrghrServiceInterfaceHystrix.class)public interface OrghrServiceInterface {@RequestMapping(value = "/orghr/user/findOaIdsByIds", method = RequestMethod.POST)Response findOaUserIdsByIds(@RequestParam("ids") String ids);}
在Feign 底层,通过基于面向接口的动态代理方式生成实现类,将请求调用委托到动态代理实现类,基本原理如下所示:

public class ReflectiveFeign extends Feign{///省略部分代码@Overridepublic <T> T newInstance(Target<T> target) {//根据接口类和Contract协议解析方式,解析接口类上的方法和注解,转换成内部的MethodHandler处理方式Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();for (Method method : target.type().getMethods()) {if (method.getDeclaringClass() == Object.class) {continue;} else if(Util.isDefault(method)) {DefaultMethodHandler handler = new DefaultMethodHandler(method);defaultMethodHandlers.add(handler);methodToHandler.put(method, handler);} else {methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));}}InvocationHandler handler = factory.create(target, methodToHandler);// 基于Proxy.newProxyInstance 为接口类创建动态实现,将所有的请求转换给InvocationHandler 处理。T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class<?>[]{target.type()}, handler);for(DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {defaultMethodHandler.bindTo(proxy);}return proxy;}//省略部分代码
3.2根据Contract协议规则,解析接口类的注解信息,解析成内部表现

List代表有一个Feign客户端的多个方法。
协议解析最终实现的效果就是返回接口调用的定义
GET /repos/foo/myrepo/contributorsHOST XXXX.XXX.XXX
目前的Spring MVC的注解并不是可以完全使用的,有一些注解并不支持,如@GetMapping
,@PutMapping
等,仅支持使用@RequestMapping
等
3.3基于 RequestBean,动态生成Request
根据传入的Bean对象和注解信息,从中提取出相应的值,来构造Http Request 对象

3.4使用Encoder 将Bean转换成 Http报文正文(消息解析和转码逻辑)
Feign 最终会将请求转换成Http 消息发送出去,传入的请求对象最终会解析成消息体

| Encoder/ Decoder 实现 | 说明 |
| JacksonEncoder,JacksonDecoder | 基于 Jackson 格式的持久化转换协议 |
| GsonEncoder,GsonDecoder | 基于Google GSON 格式的持久化转换协议 |
| SaxEncoder,SaxDecoder | 基于XML 格式的Sax 库持久化转换协议 |
| JAXBEncoder,JAXBDecoder | 基于XML 格式的JAXB 库持久化转换协议 |
| ResponseEntityEncoder,ResponseEntityDecoder | Spring MVC 基于 ResponseEntity< T > 返回格式的转换协议 |
| SpringEncoder,SpringDecoder | 基于Spring MVC HttpMessageConverters 一套机制实现的转换协议 ,应用于Spring Cloud 体系中 |
3.5拦截器负责对请求和返回进行装饰处理
比如,如果希望Http消息传递过程中被压缩,可以定义一个请求拦截器:
public class FeignAcceptGzipEncodingInterceptor extends BaseRequestInterceptor {protected FeignAcceptGzipEncodingInterceptor(FeignClientEncodingProperties properties) {super(properties);}@Overridepublic void apply(RequestTemplate template) {// 在Header 头部添加相应的数据信息addHeader(template, HttpEncoding.ACCEPT_ENCODING_HEADER, HttpEncoding.GZIP_ENCODING,HttpEncoding.DEFLATE_ENCODING);}}
@Configurationpublic class FeignInterceptor implements RequestInterceptor {@Overridepublic void apply(RequestTemplate requestTemplate) {requestTemplate.header("token", "10086");}}
/*** 自定义的请求头处理类,处理服务发送时的请求头;将服务接收到的请求头中的token字段取出来,并设置到新的请求头里面去转发给下游服务;* 比如A服务收到一个请求,请求头里面包含token字段,A处理时会使用Feign客户端调用B服务,那么token这个字段就会添加到请求头中一并发给B服务;** @author zhangwy*/@Configurationpublic class FeignHeaderConfig {Logger logger = LoggerFactory.getLogger(getClass());@Beanpublic RequestInterceptor requestInterceptor() {return requestTemplate -> {ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();if (attrs != null) {HttpServletRequest request = attrs.getRequest();Enumeration<String> headerNames = request.getHeaderNames();if (headerNames != null) {while (headerNames.hasMoreElements()) {String name = headerNames.nextElement();String value = request.getHeader(name);/*** 遍历请求头里面的属性字段,将token添加到新的请求头中转发到下游服务*/if ("token".equalsIgnoreCase(name)) {requestTemplate.header(name, value);}}} else {logger.warn("FeignHeaderConfig", "获取请求头失败!");}/*// 转发body参数Enumeration<String> bodyNames = request.getParameterNames();StringBuffer body = new StringBuffer();if (bodyNames != null) {while (bodyNames.hasMoreElements()) {String name = bodyNames.nextElement();String value = request.getParameter(name);body.append(name).append("=").append(value).append("&");}}if (body.length() != 0) {body.deleteCharAt(body.length() - 1);requestTemplate.body(body.toString());}*/}};}}
3.6日志记录
在发送和接收请求的时候,Feign定义了统一的日志门面来输出日志信息 , 并且将日志的输出定义了四个等级:
| 级别 | 说明 |
| NONE | 不做任何记录,性能最佳,适用于生产环境 |
| BASIC | 只记录输出Http 方法名称、请求URL、返回状态码和执行时间,适用于生产环境追踪问题 |
| HEADERS | 记录输出Http 方法名称、请求URL、返回状态码和执行时间 和 Header 信息 |
| FULL | 记录Request 和Response的Header,Body和一些请求元数据(推荐方式) |
Feign的日志打印只会对DEBUG级别做出响应
启用
@Configurationpublic class FeignConfiguration {@BeanLogger.Level feignLoggerLevel() {return Logger.Level.FULL;}}
配置
logging.level.com.sccba.interfaces.feign.* = debug
3.7基于重试器发送HTTP请求
Feign 内置了一个重试器,当HTTP请求出现IO异常时,Feign会有一个最大尝试次数发送请求,以下是Feign核心 代码逻辑:
final class SynchronousMethodHandler implements MethodHandler {// 省略部分代码@Overridepublic Object invoke(Object[] argv) throws Throwable {//根据输入参数,构造Http 请求。RequestTemplate template = buildTemplateFromArgs.create(argv);// 克隆出一份重试器Retryer retryer = this.retryer.clone();// 尝试最大次数,如果中间有结果,直接返回while (true) {try {return executeAndDecode(template);} catch (RetryableException e) {retryer.continueOrPropagate(e);if (logLevel != Logger.Level.NONE) {logger.logRetry(metadata.configKey(), logLevel);}continue;}}}
重试器有如下几个控制参数:
| 重试参数 | 说明 | 默认值 |
| period | 初始重试时间间隔,当请求失败后,重试器将会暂停 初始时间间隔(线程 sleep 的方式)后再开始,避免强刷请求,浪费性能 | 100ms |
| maxPeriod | 当请求连续失败时,重试的时间间隔将按照:long interval = (long) (period * Math.pow(1.5, attempt - 1));计算,按照等比例方式延长,但是最大间隔时间为 maxPeriod, 设置此值能够避免 重试次数过多的情况下执行周期太长 | 1000ms |
| maxAttempts | 最大重试次数 | 5 |
FeignClient的默认超时时间为10s,不会开启重试机制,需要自定义配置。重试机制开启后默认为5次,dubbo为3次。
开启自定义超时和重试机制
@Configurationpublic class FeignConfigure {public static int connectTimeOutMillis = 12000;//连接超时时间public static int readTimeOutMillis = 12000;//读取超时@Beanpublic Request.Options options() {return new Request.Options(connectTimeOutMillis, readTimeOutMillis);}@Beanpublic Retryer feignRetryer() {return new Retryer.Default();}}
自定义重试次数
//自定义重试次数@Beanpublic Retryer feignRetryer(){Retryer retryer = new Retryer.Default(100, 1000, 4);return retryer;}
通过配置修改超时时间
feign:client:config:remote-service: #服务名,填写default为所有服务connectTimeout: 1000readTimeout: 12000
相关源码:https://github.com/OpenFeign/feign/blob/master/core/src/main/java/feign/Retryer.java
3.8发送Http请求
Feign 真正发送HTTP请求是委托给 feign.Client
来做的,默认底层通过JDK 的 java.net.HttpURLConnection
实现了feign.Client
接口类,在每次发送请求的时候,都会创建新的HttpURLConnection 链接,这也就是为什么默认情况下Feign的性能很差的原因。可以通过拓展该接口,使用Apache HttpClient 或者OkHttp3等基于连接池的高性能Http客户端。
引入依赖
<dependency><groupId>io.github.openfeign</groupId><artifactId>feign-okhttp</artifactId><version>10.2.0</version></dependency>
开启配置
feign:httpclient:enabled: falseokhttp:enabled: true
配置类
@Configuration@ConditionalOnClass(Feign.class)@AutoConfigureBefore(FeignAutoConfiguration.class)public class FeignOkHttpConfig {@AutowiredOkHttpLoggingInterceptor okHttpLoggingInterceptor;@Beanpublic okhttp3.OkHttpClient okHttpClient(){return new okhttp3.OkHttpClient.Builder().readTimeout(60, TimeUnit.SECONDS).connectTimeout(60, TimeUnit.SECONDS).writeTimeout(120, TimeUnit.SECONDS).connectionPool(new ConnectionPool())// .addInterceptor();.build();}}
4、使用
4.1引入依赖
<!-- feign support openfeign默认支持ribbon 和 hystrix,然而不引入会报错 --><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency>
4.2启动类
@EnableFeignClients // 开启Feign@EnableFeignClients(basePackages = "com.sccba.api.client")// 如果feign接口和启动类不在一个包名下
4.3客户端
/*** 非全局关闭Hystrix @FeignClient(value = "ORGHRSERVICE", configuration = DisableHystrixConfiguration.class)* 参数必须加@RequestParam 否则服务端会报找不到参数错误* 如果是url中的占位符参数,则必须加@PathVariable* 如果是传递对象,则接收端必须加@RequestBody* */@FeignClient(name = "orghrservice", fallback = OrghrServiceInterfaceHystrix.class)public interface OrghrServiceInterface {@RequestMapping(value = "/orghr/user/findOaIdsByIds", method = RequestMethod.POST)Response findOaUserIdsByIds(@RequestParam("ids") String ids);}
参数:@FeignClient(name = "products", url = "http://localhost:7073/products", value = "product", serviceId = "product-server", path = "/products", decode404 = true, fallback = xxx.class)
•name:指定 FeignClient 的名称,该属性会作为微服务的名称,用于服务发现,如果没有服务注册中心,可以使用url参数定义接口地址•value:同 name 字段互通,注意Sentinel替换Hystrix后好像只能用name而不能用value•serviceId:指定服务ID,每个注册到注册中心上的客户端都会有对应的 serviceId 一般是 spring.application.name,与 name 和 value 互通•url:一般用于调试,可以指定一个详细地址(http://localhost:8080/products)•path:请求统一路径,可以看成 @RequestMapping("/products")•decode404:404 错误时,调用 decoder 进行解码,否则抛出 FeignException•fallback:发生错误时,回调 hystrix 类/方法(后面会详细介绍)
4.4使用客户端
@Autowired注入使用即可
4.5Feign和RestTemplate对比
| 角度 | RestTemplate + Ribbon | Feign(自带Ribbon) |
| 可读性、可维护性 | 欠佳(无法从URL直观了解这个远程调用是干什么的) | 极佳(能在接口上写注释,方法名称也是可读的,能一眼看出这个远程调用是干什么的) |
| 开发体验 | 欠佳 | 极佳 |
| 风格一致性 | 欠佳(本地API调用和RestTemplate调用的代码风格截然不同) | 极佳(完全一致,不点开Feign的接口,根本不会察觉这是一个远程调用而非本地API调用) |
| 性能 | 较好 | 中等(性能是RestTemplate的50%左右;如果为Feign配置连接池,性能可提升15%左右) |
| 灵活性 | 极佳 | 中等(内置功能能满足大多数项目的需求) |
4.6Feign配置自定义(细粒度配置)
4.6.1日志
正常配置需要打印日志的步骤如上面描述的
Feign的日志打印只会对DEBUG级别做出响应
启用
@Configurationpublic class FeignConfiguration {@BeanLogger.Level feignLoggerLevel() {return Logger.Level.FULL;}}
配置
logging.level.com.sccba.interfaces.feign.* = debug
在实际使用过程中第一步我们不同环境希望开启不同级别的日志(生产环境NONE,开发环境FULL),所以自定义参数feign.logger.level,根据参数返回不同的日志级别。
@Configuration@EnableApolloConfig@Component("feignLoggerConfig")@Datapublic class FeignLoggerConfig {@Value("${feign.logger.level}")private String loggerLevel;@BeanLogger.Level feignLoggerLevel(){/** feign的日志级别:* NONE 不做任何记录Default* BASIC 只记录输出Http方法名称、请求URL、返回状态码、执行时间* HEADERS 记录输出Http方法名称、请求URL、返回状态码、执行时间、Header信息* FULL 记录request和response的Header信息。body和一些请求元数据* */if("FULL".equals(loggerLevel)){return Logger.Level.FULL;}if("NONE".equals(loggerLevel)){return Logger.Level.NONE;}if("BASIC".equals(loggerLevel)){return Logger.Level.BASIC;}if("HEADERS".equals(loggerLevel)){return Logger.Level.HEADERS;}return Logger.Level.NONE;}}
完成这一步后我们还希望不配置logging.level(客户端可能路径不一致,产生多个配置),而是直接记录info级别的日志。
重写feign.Logger
自定义一个Logger类,继承Feign.Logger,将代码中的Debug修改成为Info
public class SccbaFeignLogger extends feign.Logger {// 这里的logger指的是slf4j的loggerprivate final Logger logger;public SccbaFeignLogger() {this(feign.Logger.class);}public SccbaFeignLogger(Class<?> clazz) {this(LoggerFactory.getLogger(clazz));}public SccbaFeignLogger(String name) {this(LoggerFactory.getLogger(name));}SccbaFeignLogger(Logger logger) {this.logger = logger;}@Overrideprotected void logRequest(String configKey, Level logLevel, Request request) {if (logger.isInfoEnabled()) {super.logRequest(configKey, logLevel, request);}}@Overrideprotected Response logAndRebufferResponse(String configKey, Level logLevel, Response response, long elapsedTime)throws IOException {if (logger.isInfoEnabled()) {return super.logAndRebufferResponse(configKey, logLevel, response, elapsedTime);}return response;}@Overrideprotected void log(String configKey, String format, Object... args) {// Not using SLF4J's support for parameterized messages (even though it// would be more efficient) because it would// require the incoming message formats to be SLF4J-specific.if (logger.isInfoEnabled()) {logger.info(String.format(methodTag(configKey) + format, args));}}}
自定义Logger类加入配置
@Configurationpublic class FeignConfiguration {@BeanLogger.Level feignLoggerLevel() {return Logger.Level.FULL;}@BeanLogger SccbaFeign(){return new SccbaFeignLogger();}}
4.6.2其他配置
方式一:指定FeignClient注解的configuration属性,并且将配置放到客户端接口类中实现只对当前客户端生效
@FeignClient(name = "microservice-provider-user", configuration = UserFeignConfig.class)/*** 该Feign Client的配置类,注意:* 1. 该类可以独立出去;* 2. 该类上也可添加@Configuration声明是一个配置类;* 配置类上也可添加@Configuration注解,声明这是一个配置类;* 但此时千万别将该放置在主应用程序上下文@ComponentScan所扫描的包中,* 否则,该配置将会被所有Feign Client共享,无法实现细粒度配置!* 个人建议:不加@Configuration注解**/class UserFeignConfig {@Beanpublic Logger.Level logger() {return Logger.Level.FULL;}}
方式二:配置文件
从Spring Cloud Edgware开始,Feign支持使用属性自定义Feign。对于一个指定名称的Feign Client(例如该Feign Client的名称为feignName
),Feign支持如下配置项:
feign:client:config:feignName:connectTimeout: 5000 # 相当于Request.OptionsreadTimeout: 5000 # 相当于Request.Options# 配置Feign的日志级别,相当于代码配置方式中的LoggerloggerLevel: full# Feign的错误解码器,相当于代码配置方式中的ErrorDecodererrorDecoder: com.example.SimpleErrorDecoder# 配置重试,相当于代码配置方式中的Retryerretryer: com.example.SimpleRetryer# 配置拦截器,相当于代码配置方式中的RequestInterceptorrequestInterceptors:- com.example.FooRequestInterceptor- com.example.BarRequestInterceptordecode404: false
不建议配置retryer,因为在ribbon中已经进行时重试配置
通用配置
@EnableFeignClients
注解上有个defaultConfiguration
属性,我们可以将默认配置写成一个类,然后用defaultConfiguration
来引用,例如:
@EnableFeignClients(defaultConfiguration = DefaultRibbonConfig.class)
如果想使用配置属性的方式,只需使用类似如下的写法即可
feign:client:config:default:connectTimeout: 5000readTimeout: 5000loggerLevel: basic
优先级
如果你使用了Java代码配置Feign,同时又使用了配置属性配置Feign,那么使用配置属性的优先级更高。配置属性配置的方式将会覆盖Java代码配置。如果你想修改代码配置方式的优先级,可使用如下属性:feign.client.default-to-properties=false
。
4.6.3继承
4.7替换client底层为okhttp3
参考3.8即可。
4.8超时和重试
参考3.7即可。
4.9OpenFeign集成Protocol Buffer
Protocol Buffer 是Google的一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。它很适合做数据存储或 RPC 数据交换格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。
4.10常见问题
4.10.1FeignClient接口如使用@PathVariable
,必须指定value属性
@FeignClient("microservice-provider-user")public interface UserFeignClient {@RequestMapping(value = "/simple/{id}", method = RequestMethod.GET)public User findById(@PathVariable("id") Long id);...}
4.10.2构造多参数请求问题
1、有几个参数写几个参数。2、利用map。3、利用requestbody传递实体。
4.10.3Feign集成Hystrix Stream
4.10.5首次请求失败
4.10.6Feign的继承
4.10.7@FeignClient中的@RequestMapping也被SpringMVC加载的问题解决
4.10.8Spring Boot和Feign中使用Java 8时间日期API(LocalDate等)的序列化问题
LocalDate
、LocalTime
、LocalDateTime
是Java 8开始提供的时间日期API,主要用来优化Java 8以前对于时间日期的处理操作。然而,我们在使用Spring Boot或使用Spring Cloud Feign的时候,往往会发现使用请求参数或返回结果中有LocalDate
、LocalTime
、LocalDateTime
的时候会发生各种问题。
比如在服务端定义了private LocalDate birthday;这样一个类型,在使用feign调用的时候会出现反序列化失败的错误JSON parse error: Can not construct instance of java.time.LocalDate: no suitable constructor found, can not deserialize from Object value。原因就是实际上默认情况下Spring MVC对于LocalDate
序列化成了一个数组类型,而Feign在调用的时候,还是按照ArrayList
来处理,所以自然无法反序列化为LocalDate
对象了。
解决方法
为了解决上面的问题非常简单,因为jackson也为此提供了一整套的序列化方案,我们只需要在pom.xml
中引入jackson-datatype-jsr310
依赖,具体如下:
<dependency><groupId>com.fasterxml.jackson.datatype</groupId><artifactId>jackson-datatype-jsr310</artifactId></dependency>
注意:在设置了spring boot的parent的情况下不需要指定具体的版本,也不建议指定某个具体版本
在该模块中封装对Java 8的时间日期API序列化的实现,其具体实现在这个类中:com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
(注意:一些较早版本封装在这个类中“com.fasterxml.jackson.datatype.jsr310.JSR310Module
)。在配置了依赖之后,我们只需要在上面的应用主类中增加这个序列化模块,并禁用对日期以时间戳方式输出的特性:
@Beanpublic ObjectMapper serializingObjectMapper() {ObjectMapper objectMapper = new ObjectMapper();objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);objectMapper.registerModule(new JavaTimeModule());return objectMapper;}
4.10.9Feign实现文件上传
服务提供方(接收文件)
服务提供方的实现比较简单,就按Spring MVC的正常实现方式即可,比如:
@RestControllerpublic class UploadController {@PostMapping(value = "/uploadFile", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)public String handleFileUpload(@RequestPart(value = "file") MultipartFile file) {return file.getName();}}
服务消费方(发送文件)
在服务消费方由于会使用Feign客户端,所以在这里需要在引入feign对表单提交的依赖,具体如下:
<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><dependency><groupId>commons-fileupload</groupId><artifactId>commons-fileupload</artifactId><version>1.3.3</version></dependency>
定义文件上传方的应用主类和FeignClient,假设服务提供方的服务名为eureka-feign-upload-server
@FeignClient(value = "upload-server", configuration = UploadService.MultipartSupportConfig.class)public interface UploadService {@PostMapping(value = "/uploadFile", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)String handleFileUpload(@RequestPart(value = "file") MultipartFile file);@Configurationclass MultipartSupportConfig {@Beanpublic Encoder feignFormEncoder() {return new SpringFormEncoder();}}}
在启动了服务提供方之后,尝试在服务消费方编写测试用例来通过上面定义的Feign客户端来传文件,比如:
@Slf4j@RunWith(SpringJUnit4ClassRunner.class)@SpringBootTestpublic class UploadTester {@Autowiredprivate UploadService uploadService;@Test@SneakyThrowspublic void testHandleFileUpload() {File file = new File("upload.txt");DiskFileItem fileItem = (DiskFileItem) new DiskFileItemFactory().createItem("file",MediaType.TEXT_PLAIN_VALUE, true, file.getName());try (InputStream input = new FileInputStream(file); OutputStream os = fileItem.getOutputStream()) {IOUtils.copy(input, os);} catch (Exception e) {throw new IllegalArgumentException("Invalid file: " + e, e);}MultipartFile multi = new CommonsMultipartFile(fileItem);log.info(uploadService.handleFileUpload(multi));}}




