文章目录
前言
这几天针对一些功能性接口的替换,涉及多个微服务,基本都在同一个eureka且使用的都是springboot+springcloud的架构,所以把各自系统请求统一调用某个基础功能服务。feign调用调试时发现些问题(ContentType=multipart/form-data),总结问题如下:
- feign不支持使用多个
@RequestPart MutiValueMap传递文件参数不支持MultiparFile类型编码MutiValueMap传递文件参数,中文文件名乱码(springboot1.5+对应的springcloud)
因此,索性总结下针对目前主流ContentType的web请求,springMvc和feign调用的处理方式。以下总结和demo适用版本(springboot1.5.x+springcloud-Dalston和springboot2.0.x+springcloud-Finchley),推荐使用2.0版本
一、ContentType
在 Web 请求中,Content-Type 是 HTTP 请求头(Request Header)的一个核心字段,用于告诉服务器当前请求体(Request Body)中数据的格式类型,以便服务器能够正确解析请求内容
application/x-www-form-urlencoded
简单表单提交,请求体中的数据会被编码为「键值对字符串」,格式为
key1=value1&key2=value2
![[urlEncoded.png]]
application/json
当下最常用请求体类型,将数据体json序列化传递

application/xml
一些政府机构或老服务常用请求体类型,与json处理相似

multipart/form-data
文本或文件等复杂表单请求体类型,建议有文件参数时使用

查询参数
非请求体参数(无ContentType),用
&直接拼接在url之后,GET参数传值,在此补充

二、SpringMVC实践
针对不同的ContentType的参数解析,SpringMVC有提供对应的标注注解,有些情况下不使用注解spring也可以自行推断处理但不建议,一来是便于spring精确处理避免不必要的性能损耗,二来也方便自己区分,不留歧义。
核心注解总览
请求处理
@RequestMapping标注控制器的处理方法- value 请求路径
- method 请求方式
- consumes 解析的ContentType
- produces 返回的ContentType
请求体处理
@RequestParam【标注简单表单参数和查询参数】@RequestBody【标注json参数、xml参数】@RequestPart【标注复杂表单参数如文件】
必要配置
服务端需要限制文件大小
spring:
# 2.0版本配置
servlet:
multipart:
enabled: true
max-file-size: 30MB # 单个文件最大大小
max-request-size: 30MB # 整个请求最大大小
## 1.5版本配置
# http:
# multipart:
# enabled: true
# max-file-size: 30MB # 单个文件最大大小
# max-request-size: 30MB # 整个请求最大大小
application/x-www-form-urlencoded
- 核心注解
@RequestParam标注简单表单参数
@PostMapping(value = "/urlEncoded", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public String simplyFormData(@RequestParam("name")String name,
@RequestParam("age")Integer age) {
return name+": "+age;
}
application/json
- 核心注解:
@RequestBody标注请求体反序列化的java对象
@PostMapping(value = "/json", consumes = MediaType.APPLICATION_JSON_VALUE)
public String json(@RequestBody User user) {
return JSONUtil.toJsonStr(user);
}
application/xml
- 核心注解:
@RequestBody标注请求体反序列化的java对象
- 额外的xml支持
- springboot1.5 自动配置xml转换器
Jaxb2RootElementHttpMessageConverter - springboot2.0 不再自动配置xml转换器,需要自行导入相应的xml依赖,并且推荐使用jackson族的xml转换器
- springboot1.5 自动配置xml转换器
xml-jackson依赖
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<!-- 版本由 Spring Boot 父工程管理,不要手动指定 -->
</dependency>
@PostMapping(value = "/xml",
consumes = MediaType.APPLICATION_XML_VALUE,
produces = MediaType.APPLICATION_XML_VALUE)
public XmlUser xml(@RequestBody XmlUser xmlUser) {
xmlUser.setName("Welcome " + xmlUser.getName());
return xmlUser;
}
@JacksonXmlRootElement(localName = "xuser")//指定根元素名称
@Data
public class XmlUser {
private String name;
private Integer age;
public XmlUser() {}
public XmlUser(String name, Integer age) {
this.name = name;
this.age = age;
}
}
multipart/form-data
- 核心注解
@RequestPart标注复杂表单参数如文件,当然也可以标注简单表单参数@RequestParam简单表单参数
//单文件表单
@PostMapping(value = "/multiFormData", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public String multipartFormData(@RequestPart("file")MultipartFile file,
@RequestParam("name")String name) {
StringBuilder sb = new StringBuilder();
if (file != null) {
sb.append("fileName: ").append(file.getOriginalFilename()).append("fileSize: ").append(file.getSize()).append("\n");
}
sb.append("username: ").append(name);
return sb.toString();
}
//多文件表单
@PostMapping(value = "/manyMultiFormData", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public String manyMultipartFormData(@RequestPart("files") MultipartFile[] files,
@RequestParam("name")String name) {
StringBuilder sb = new StringBuilder();
if (files != null) {
for (MultipartFile file : files) {
sb.append("fileName: ").append(file.getOriginalFilename()).append("fileSize: ").append(file.getSize()).append("\n");
}
}
sb.append("username: ").append(name);
return sb.toString();
}
查询参数请求
核心注解
@RequestParam标注指定的key
@GetMapping(value = "/queryParams")
public String urlEncoded(@RequestParam("name") String name,
@RequestParam("age") Integer age) {
return name+": "+age;
}
混合参数请求
文件(Multipart)+文本(text)+json
对于这种混合的复杂请求体,
ContentType=multipart/form-data,对于文件和json一定要使用@RequestPart标注
核心注解
@RequestPart标注复杂表单参数@RequestParam标注简单表单参数
@PostMapping(value = "/mixMultiFormData", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public String mixRequestBody(@RequestPart("file")MultipartFile file,
@RequestParam("expireTime")String expireTime,
@RequestPart("user") String user) {
StringBuilder sb = new StringBuilder();
if (file != null) {
sb.append("fileName: ").append(file.getOriginalFilename()).append("fileSize: ").append(file.getSize()).append("\n");
}
sb.append("expireTime: ").append(expireTime).append("\n");
sb.append("user: ").append(JSONUtil.toJsonStr(user));
return sb.toString();
}
三、feign实践
作为声明式http请求,feign调用的核心部分:mvc注解/feign注解+编码器,习惯了springboot的开箱即用,本着一个能用配置的就不用自定义的原则,给出以下两套主流请求方式。
必要依赖
注意
springboot与springcloud版本一定要对应(这个可以查看官网或者问AI)boot1.5+cloud Dalston+feignboot2.0+cloud Finchley+openfeign
feign-form与feign-form-spring也要与springcloud版本适配,不然会有编码异常等问题- 3.0.3版本可以适配上面的
feign和openfeign
- 3.0.3版本可以适配上面的
- 在实践过程中遇到问题特别是框架相关的,依赖冲突与不适配是主要问题所在
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<!-- <version>1.5.2.RELEASE</version>-->
<version>2.0.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<!-- <spring.cloud.version>Dalston.RELEASE</spring.cloud.version>-->
<spring.cloud.version>Finchley.SR2</spring.cloud.version>
</properties>
<!-- springmvc -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- XML 处理核心依赖(必须) -->
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
<!-- 一定要与springcloud版本对应 -->
<!-- <dependency>-->
<!-- <groupId>org.springframework.cloud</groupId>-->
<!-- <artifactId>spring-cloud-starter-feign</artifactId>-->
<!-- </dependency>-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- 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>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring.cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
必要配置
feign-mvc契约
注解与SpringMVC一致,但是有几个注意的地方
- feign不允许使用多个
@RequestPart,这意味这无法发送多个文件MultipartFile(如果你想用一个对象封装多个file或者标注一个MultipartFile[]数组,那你需要自定义编码器,依赖提供的编码器是不支持的) @RequestParam标注的参数会被当成查询参数拼接在url之后- 解决“1-2”问题,再不自定义编码器的前提下使用原生的
MultiValueMap来发送简单表单和复杂表单数据 - 使用
MultiValueMap传递文件时,若是springboot1.5版本会出现中文名乱码问题,2.0框架已解决这个问题 - form-data表单请求体编码,需要引入相关依赖来配置相应的编码器(注意版本适配问题)
配置
开放日志
logging:
level:
org.example.feign: DEBUG
开启feign功能
@EnableFeignClients(basePackages = "org.example.feign")
@SpringBootApplication
public class FeignClient {
public static void main(String[] args) {
SpringApplication.run(FeignClient.class);
}
}
配置编码器
public class FeignDefaultConfig {
//feign编码器
@Bean
public Encoder feignEncoder(ObjectFactory<HttpMessageConverters> messageConverters) {
return new SpringFormEncoder(new SpringEncoder(messageConverters));
}
//feign日志输出
@Bean
public Logger.Level feignLoggerLevel() {
return Logger.Level.HEADERS;
}
}
FeignClient demo
FeignClient
package org.example.feign;
import org.example.entity.User;
import org.example.entity.XmlUser;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.MediaType;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@FeignClient(name = "feign-server", path = "/test", configuration = FeignDefaultConfig.class)
public interface UserFeignClientV1 {
//查询参数
@GetMapping(value = "/queryParams")
String urlEncoded(@RequestParam("name") String name,
@RequestParam("age") Integer age);
//json
@PostMapping(value = "/json", consumes = MediaType.APPLICATION_JSON_VALUE)
String json(@RequestBody User user);
//xml
@PostMapping(value = "/xml", consumes = MediaType.APPLICATION_XML_VALUE, produces = MediaType.APPLICATION_XML_VALUE)
String xml(@RequestBody XmlUser xmlUser);
//普通表单
@PostMapping(value = "/ulrEncoded", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
String simplyFormData(MultiValueMap<String,Object> map);
//单文件,这里的@RequestParam标注的key会被拼接到url后面,只是为了演示注解可用,实际对于表单参数来说还是用MultiValueMap
@PostMapping(value = "/multiFormData", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
String multipartFormData(@RequestPart("file") MultipartFile file,
@RequestParam("name")String username);
//多文件
@PostMapping(value = "/manyMultiFormData", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
String manyMultipartFormData(MultiValueMap<String,Object> map);
//混合请求体
@PostMapping(value = "/mixMultiFormData",consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
String mixRequestBody(MultiValueMap<String,Object> map);
}
测试demo
package feign;
import org.example.FeignClient;
import org.example.entity.User;
import org.example.entity.XmlUser;
import org.example.feign.UserFeignClientV1;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.io.FileSystemResource;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@SpringBootTest(classes = FeignClient.class)
@RunWith(SpringRunner.class)
public class UserFeignClientV1Test {
@Autowired
private UserFeignClientV1 userFeignClientV1;
@Test
public void test001() {
String resp = userFeignClientV1.urlEncoded("赵大", 1);
System.out.println("urlEncoded返回值========\n"+resp);
}
@Test
public void test002() {
User user = new User("钱二", 2);
String resp = userFeignClientV1.json(user);
System.out.println("json返回值========\n"+resp);
}
@Test
public void test003() {
XmlUser xmlUser = new XmlUser("孙三", 3);
String resp = userFeignClientV1.xml(xmlUser);
System.out.println("xml返回值========\n"+resp);
}
@Test
public void test004() {
MultiValueMap<String, Object> multiValueMap = new LinkedMultiValueMap<>();
multiValueMap.add("name","李四");
multiValueMap.add("age","4");//普通文本必须用String
String resp = userFeignClientV1.simplyFormData(multiValueMap);
System.out.println("simplyFormData返回值========\n"+resp);
}
@Test
public void test005() throws IOException {
Path path = Paths.get("/Users/images/老达奇.jpg");
MultipartFile file = new MockMultipartFile(
"file", path.getFileName().toString(), Files.probeContentType(path), Files.newInputStream(path)
);
String resp = userFeignClientV1.multipartFormData(file, "周五");
System.out.println("multipartFormData返回值========\n"+resp);
}
@Test
public void test006() throws IOException {
Path path1 = Paths.get("/Users/images/rdr2_arthur_morgan.jpg");
Path path2 = Paths.get("/Users/images/老达奇.jpg");
MultiValueMap<String,Object> map = new LinkedMultiValueMap<>();
map.add("files",new FileSystemResource(path1.toFile()));
map.add("files",new FileSystemResource(path2.toFile()));
map.add("name","吴六");
String resp = userFeignClientV1.manyMultipartFormData(map);
System.out.println("manyMultipartFormData返回值========\n"+resp);
}
//
@Test
public void test007() throws IOException {
Path path = Paths.get("/Users/images/老达奇.jpg");
User user = new User("郑七", 7);
MultiValueMap<String,Object> map = new LinkedMultiValueMap<>();
map.add("file",new FileSystemResource(path.toFile()));
map.add("expireTime","2099-12-12 00:00:00");
map.add("user",user);
String resp = userFeignClientV1.mixRequestBody(map);
System.out.println("mixRequestBody返回值========\n"+resp);
}
}
feign-默认契约
通过配置开启feign的底层注解
核心注解总览
@RequestLine- 请求处理注解,类比于
RequestMapping @RequestLine("POST /json")【请求方法+请求路径】
- 请求处理注解,类比于
@Headers- 显示指定请求头信息
@Headers({"Content-Type: application/json"})
@Param- 标注各类型请求体参数
@Param("file") MultipartFile file
配置
开放日志
logging:
level:
org.example.feign: DEBUG
开启feign功能
@EnableFeignClients(basePackages = "org.example.feign")
@SpringBootApplication
public class FeignClient {
public static void main(String[] args) {
SpringApplication.run(FeignClient.class);
}
}
配置编码器
package org.example.feign;
import feign.Contract;
import feign.Logger;
import feign.codec.Encoder;
import feign.form.spring.SpringFormEncoder;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.cloud.openfeign.support.SpringEncoder;
import org.springframework.context.annotation.Bean;
public class FeignFormDataConfig {
/**
* 此配置使 Feign 使用原生注解(@RequestLine, @Param)替代 Spring MVC 注解
* @return
*/
@Bean
public Contract feignContract() {
return new feign.Contract.Default();
}
@Bean
public Encoder feignEncoder(ObjectFactory<HttpMessageConverters> messageConverters) {
return new SpringFormEncoder(new SpringEncoder(messageConverters));
}
@Bean
public Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}
FeignClient demo
FeignClient
package org.example.feign;
import feign.Headers;
import feign.Param;
import feign.RequestLine;
import org.example.entity.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.multipart.MultipartFile;
@FeignClient(name = "feign-server",path = "/test",configuration = FeignFormDataConfig.class)
public interface UserFiegnClientV2 {
//查询参数 /users?name={name}&age={age}
@RequestLine("GET /queryParams?name={name}&age={age}")
String urlEncoded(@Param("name") String name,
@Param("age")Integer age);
//json
@RequestLine("POST /json")
@Headers({"Content-Type: application/json"})
String json(User user);
//普通表单
@RequestLine("POST /urlEncoded")
@Headers({"Content-Type: application/x-www-form-urlencoded"})
String simplyFormData(@Param("name")String name,@Param("age")Integer age);
//单文件
@RequestLine("POST /multiFormData")
@Headers({"Content-Type: multipart/form-data"})
String multipartFormData(@Param("file") MultipartFile file,
@Param("name")String name);
//多文件
@RequestLine("POST /manyMultiFormData")
@Headers({"Content-Type: multipart/form-data"})
String manyMultipartFormData(@Param("files") MultipartFile[] file,
@Param("name")String name);
//混合请求体
@RequestLine("POST /mixMultiFormData")
@Headers({"Content-Type: multipart/form-data"})
String mixRequestBody(@Param("file") MultipartFile file,
@Param("expireTime") String expireTime,
@Param("user") String user);//这里user的contentType会变成text/plain,服务端无法用@RequestPart反序列化成对象
}
测试demo
package feign;
import cn.hutool.json.JSONUtil;
import org.example.FeignClient;
import org.example.entity.User;
import org.example.feign.UserFiegnClientV2;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* @author yu
* @version 1.0
* @date 2025/7/29 14:07
* @Description
*/
@SpringBootTest(classes = FeignClient.class)
@RunWith(SpringRunner.class)
public class UserFeignClientV2Test {
@Autowired
private UserFiegnClientV2 userFeignClientV2;
@Test
public void test001() {
String resp = userFeignClientV2.urlEncoded("赵大", 1);
System.out.println("urlEncoded返回值========\n"+resp);
}
@Test
public void test002() {
User user = new User("钱二", 2);
String resp = userFeignClientV2.json(user);
System.out.println("json返回值========\n"+resp);
}
@Test
public void test004() {
String resp = userFeignClientV2.simplyFormData("孙三", 3);
System.out.println("simplyFormData返回值========\n"+resp);
}
@Test
public void test005() throws IOException {
Path path = Paths.get("/Users/images/老达奇.jpg");
MultipartFile file = new MockMultipartFile(
"file", path.getFileName().toString(), Files.probeContentType(path), Files.newInputStream(path)
);
String resp = userFeignClientV2.multipartFormData(file, "周五");
System.out.println("multipartFormData返回值========\n"+resp);
}
@Test
public void test006() throws IOException {
Path path1 = Paths.get("/Users/images/rdr2_arthur_morgan.jpg");
Path path2 = Paths.get("/Users/images/老达奇.jpg");
MultipartFile file1 = new MockMultipartFile(
"files", path1.getFileName().toString(), Files.probeContentType(path1), Files.newInputStream(path1)
);
MultipartFile file2 = new MockMultipartFile(
"files", path2.getFileName().toString(), Files.probeContentType(path2), Files.newInputStream(path2)
);
MultipartFile[] files = new MultipartFile[2];
files[0] = file1;
files[1] = file2;
String resp = userFeignClientV2.manyMultipartFormData(files,"吴六");
System.out.println("manyMultipartFormData返回值========\n"+resp);
}
@Test
public void test007() throws IOException {
Path path = Paths.get("/Users/images/老达奇.jpg");
MultipartFile file = new MockMultipartFile(
"file", path.getFileName().toString(), Files.probeContentType(path), Files.newInputStream(path)
);
User user = new User("郑七", 7);
String resp = userFeignClientV2.mixRequestBody(file, "2099-12-12 00:00:00", JSONUtil.toJsonStr(user));
System.out.println("mixRequestBody返回值========\n"+resp);
}
}
四、feign调用总结
注意点
- 注意版本的兼容性,建议老版本升级为springboot2.0+openfeign能解决不少问题
- 能用feign提供的编码器就别自定义,默认契约和feign契约基本能解决问题
- 尽量加标识注解,不让框架推断,避免出现意料之外的问题或歧义
- 可以debug看下springMVC的处理流程流程
DispatcherServlet,无需深入每个处理方法,只过主要流程;还有feign的编码器处理,如上面的SpringFormEncoder
问题
现在回过头来看下前言中提到的问题
- feign不支持使用多个
@RequestPart- 默认契约MVC是不支持的,可以用
MutiValueMap代替或者直接使用feign契约
- 默认契约MVC是不支持的,可以用
MutiValueMap传递文件参数不支持MultiparFile类型编码- 编码器不支持,可以自定义编码器或者使用
FileSystemResource对象传递
- 编码器不支持,可以自定义编码器或者使用
MutiValueMap传递文件参数,中文文件名乱码(springboot1.5+对应的springcloud)- 升级boot2.0+openfeign
- 使用feign契约传递MultipartFile
- 新增一个参数存放中文文件名,在服务端重命名
- 自定义编码器处理中文文件名
五、SpringMVC处理流程图
springboot1.5

springboot2.0

总结
建议还是使用springboot2.0+openfeign(开启feign契约),能省下不少麻烦,若非同一个eureka那使用feign调用意义不大可以使用Apache的HttpClient或其他的http客户端调用,自己封装工具类,方便以后升级或改造。

1398

被折叠的 条评论
为什么被折叠?



