何为feign
直接套用官网的话
Feign is a Java to HTTP client binder inspired by Retrofit, JAXRS-2.0, and WebSocket. Feign's first goal was reducing the complexity of binding Denominator uniformly to HTTP APIs regardless of ReSTfulness.
官网使用文档开篇第一句话
Feign允许我们通过注解的方式实现http客户端的功能,Feign能用最小的性能开销,让我们调用web服务器上基于文本的接口。同时允许我们自定义编码器、解码器和错误处理器等等
因为本篇主要是介绍feign的一些功能扩展,具体入门可查看如下文章,本篇就不再论述
https://github.com/OpenFeign/feign
有使用过feign的小伙伴大概都知道,feign post提交的时候可以使用bean传输,不需要每个参数注解@Param,feign会把这个bean的内容写入到http的 body中去,且指定contentType为application/json 。controller接收时,需要在接口对应的bean上注解@RequestBody,从body中读取这个bean的内容。
如下示例
@RequestMapping(value = "/saveOrUpdateUser")
public Long saveOrUpdateUser(@RequestBody UserDTO userDTO){
log.info("{}",userDTO);
return userService.saveOrUpdateUser(userDTO);
}
@RequestLine("POST /u/saveOrUpdateUser")
@Headers("Content-Type: application/json")
Long saveOrUpdateUser(UserDTO userDTO);
但当接收方不是用body接收复杂对象,上面的方案就失效了
出现上面情况,假如我们又不想进行额外扩展,我们可以采用如下方案解决
1、使用@Param注解
@RequestMapping(value = "/saveOrUpdateUser")
public Long saveOrUpdateUser( UserDTO userDTO){
log.info("{}",userDTO);
return userService.saveOrUpdateUser(userDTO);
}
@RequestLine("POST /u/saveOrUpdateUser?id={id}&name={name}&age={age}")
@Headers("Content-Type: application/x-www-form-urlencoded")
Long saveOrUpdateUser(@Param("id")Long id,@Param("name")String name,@Param("age") int age);
2、使用@QueryMap
@RequestLine("POST /u/saveOrUpdateUser")
@Headers("Content-Type: application/x-www-form-urlencoded")
Long saveOrUpdateUser(@QueryMap Map<String,Object> param);
虽然使用上面两种方案可以解决,但存在不优雅的地方,比如参数太多,用map语意不直观。那有没有其他方案,答案是有的,feign很贴心的提供了feign-form,这玩儿意可以同时支持json和表单。具体文档可以查看如下链接
https://github.com/OpenFeign/feign-form
不过目前的版本并没提供,比如接口提供方有个字段属性名称叫做order-items,或者bean里面又嵌套bean的解决方案。为了解决这些问题,我就在feign-form的基础进行了再次扩展
1、pom.xml
<properties>
<java.version>1.8</java.version>
<feign.version>10.1.0</feign.version>
<feign.form.version>3.5.0</feign.form.version>
<findbugs.version>3.0.1</findbugs.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-core</artifactId>
<version>${feign.version}</version>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-jackson</artifactId>
<version>${feign.version}</version>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
<version>${feign.version}</version>
</dependency>
<dependency>
<groupId>io.github.openfeign.form</groupId>
<artifactId>feign-form</artifactId>
<version>${feign.form.version}</version>
</dependency>
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>annotations</artifactId>
<version>${findbugs.version}</version>
</dependency>
</dependencies>
注意点:
all feign-form releases before 3.5.0 works with OpenFeign 9.* versions; starting from feign-form's version 3.5.0, the module works with OpenFeign 10.1.0 versions and greater.
2、核心代码类
public class CustomFormEncoder implements Encoder {
private static final String CONTENT_TYPE_HEADER = "Content-Type";
private static final Pattern CHARSET_PATTERN = Pattern.compile("(?<=charset=)([\\w\\-]+)");
private final Encoder delegate;
private final Map<ContentType, ContentProcessor> processors;
public CustomFormEncoder() {
this(new Default());
}
public CustomFormEncoder(Encoder delegate) {
this.delegate = delegate;
List<ContentProcessor> list = Arrays.asList(new MultipartFormContentProcessor(delegate), new UrlencodedFormContentProcessor());
this.processors = new HashMap(list.size(), 1.0F);
Iterator iterator = list.iterator();
while(iterator.hasNext()) {
ContentProcessor processor = (ContentProcessor)iterator.next();
this.processors.put(processor.getSupportedContentType(), processor);
}
}
public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException {
String contentTypeValue = this.getContentTypeValue(template.headers());
ContentType contentType = ContentType.of(contentTypeValue);
if (!this.processors.containsKey(contentType)) {
this.delegate.encode(object, bodyType, template);
} else {
Map data;
if (MAP_STRING_WILDCARD.equals(bodyType)) {
data = (Map)object;
} else {
if (!PojoUtils.isUserPojo(object)) {
this.delegate.encode(object, bodyType, template);
return;
}
data = PojoUtils.toMap(object);
}
Charset charset = this.getCharset(contentTypeValue);
((ContentProcessor)this.processors.get(contentType)).process(template, charset, data);
}
}
public final ContentProcessor getContentProcessor(ContentType type) {
return (ContentProcessor)this.processors.get(type);
}
private String getContentTypeValue(Map<String, Collection<String>> headers) {
Iterator iterator = headers.entrySet().iterator();
while(true) {
Map.Entry entry;
do {
if (!iterator.hasNext()) {
return null;
}
entry = (Map.Entry)iterator.next();
} while(!((String)entry.getKey()).equalsIgnoreCase(CONTENT_TYPE_HEADER));
Iterator var = ((Collection)entry.getValue()).iterator();
while(var.hasNext()) {
String contentTypeValue = (String)var.next();
if (contentTypeValue != null) {
return contentTypeValue;
}
}
}
}
private Charset getCharset(String contentTypeValue) {
Matcher matcher = CHARSET_PATTERN.matcher(contentTypeValue);
return matcher.find() ? Charset.forName(matcher.group(1)) : CharsetUtil.UTF_8;
}
}
1、创建示例bean
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserPageDTO {
@FieldAlias(isPojo = true)
private UserDTO userDTO;
private int pageSize;
@FieldAlias("pageIndex")
private int pageNo;
}
@FieldAlias 为自定义注解,用来标注接口提供方的特殊字段属性名,其中还有一个属性isPojo来指定字段是不是复杂对象,当使用isPojo对象里面又包含特殊字段属性名,则该特殊字段属性名上方要加上@JsonProperty注解,比如@JsonProperty("user.name")
private String userName
2、示例接口提供方
@RequestMapping(value = "/listPage") public List<User> listPage( UserDTO userDTO, int pageIndex, int pageSize){
log.info("userDTO:{},pageIndex:{},pageSize:{}",userDTO,pageIndex,pageSize);
return userService.listPage(userDTO,pageIndex,pageSize);
}
3、示例客户端调用
@CustomFeignClient(url="http://localhost:8080")
public interface UserHttpClient {
@RequestLine("POST /u/listPage")
@Headers("Content-Type: application/x-www-form-urlencoded")
List<User> listPage(UserPageDTO userPageDTO);
}
当客户端接口上有CustomFeignClient注解,且springboot启动类上面有配置EnableCustomFeignClients注解时,程序会自动把接口注册到spring容器中,如果不在springboot启动类上加EnableCustomFeignClients,则可以额外编写一个配置类。配置类上加入EnableCustomFeignClients注解并指定扫描的类包,比如@EnableCustomFeignClients(basePackages = "abc.com")
如果不和spring集成,也可以单独使用,如下
UseUserHttpClient userHttpClient = FeignClientUtils.getInstance().getClient(UserHttpClient.class, Constant.REMOTE_URL);
4、测试
@SpringBootTest
@RunWith(SpringRunner.class)
public class UserHttpClientSpringTest {
@Autowired
private UserHttpClient userHttpClient;
@Test
public void testListPage(){
UserDTO userDTO = new UserDTO();
// userDTO.setAge(25);
// userDTO.setId(2L);
// userDTO.setName("李四");
userDTO.setSex("男");
UserPageDTO userPageDTO = UserPageDTO.builder().userDTO(userDTO).pageNo(1).pageSize(5).build();
List<User> users = userHttpClient.listPage(userPageDTO);
users.forEach(user-> System.out.println(user));
}
}
User(id=1, name=张三, age=20, sex=男, password=zhangsan)
User(id=2, name=李四, age=25, sex=男, password=lisi)
User(id=4, name=王五, age=30, sex=男, password=wangwu)
https://github.com/lyb-geek/feign-complex-demo
feign是一个挺好用的http客户端类库,其通过注解加接口的实现方式确实比传统用apache的HttpComponents方便很多,且性能也比较优越。之前接单,为了简化http的客户端代码的编写,我也造了一个类似的接口+注解http客户端,在maven的中央仓库就可以搜到,感兴趣的小伙伴可以蛮看一下,后面有机会会介绍一下
https://mvnrepository.com/artifact/com.github.easilyuse/easily-http