Feign
Feign是Netflix开源一种负载均衡的声明式Http客户端,使用Feign调用APi就像调用本地方法一样,避免了调用目标服务时,需要不断的解析/封装JSON数据的麻烦。Feign致力于编写Java的http客户端更加简便。
为什么要使用Feign
在我们微服务环境中,服务发现使用nacos实现,负载均衡使用ribbon实现,但是现有技术体系下的服务间调用存在以下问题,也是为什么我们需要使用Feign的原因:
- 代码可读性差
- 复杂的URL难以维护
- 难以响应需求的变化,在快速迭代的过程中很痛苦
- 编程体验不统一
Feign实现http调用
-
加依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> 复制代码
-
写注解
在启动类添加 @EnableFeignClients(basePackages = "com.samir.contentcenter.feignclient") 注解(basePackages为feign接口所在包路径)。
-
写配置
-
改写代码
-
编写feign接口
package com.samir.contentcenter.feignclient; import com.samir.contentcenter.domian.dto.user.UserDTO; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; @FeignClient(name = "user-center", path = "/users") public interface UserCenterClient { /** * http://user-center/users/{id} * @param id * @return */ @GetMapping(value = "/{id}") UserDTO findById(@RequestParam("id") Integer id); } 复制代码
-
修改原有代码调用
package com.samir.contentcenter.service.content.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.samir.contentcenter.domian.dto.content.ShareDTO; import com.samir.contentcenter.domian.dto.user.UserDTO; import com.samir.contentcenter.domian.entity.content.Share; import com.samir.contentcenter.dao.content.ShareDao; import com.samir.contentcenter.feignclient.UserCenterClient; import com.samir.contentcenter.service.content.ShareService; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import java.util.List; import java.util.concurrent.ThreadLocalRandom; import java.util.stream.Collectors; /** * Auto created by codeAppend plugin */ @Service public class ShareServiceImpl extends ServiceImpl<ShareDao, Share> implements ShareService { @Autowired private RestTemplate restTemplate; @Autowired private DiscoveryClient discoveryClient; @Autowired private UserCenterClient userCenterClient; @Override public ShareDTO findById(Integer id) { // 获取分享详情 Share share = baseMapper.selectById(id); // 获取发布人id Integer userId = share.getUserId(); // List<ServiceInstance> instances = discoveryClient.getInstances("user-center"); // List<String> urls = instances.stream().map(instance -> instance.getUri().toString() + "/users/{id}").collect(Collectors.toList()); // // // 随机算法 // int i = ThreadLocalRandom.current().nextInt(urls.size()); // // 远程调用用户中心服务接口 // UserDTO userDTO = restTemplate.getForObject(urls.get(i), UserDTO.class, userId); // 使用feign调用用户中心接口 UserDTO userDTO = userCenterClient.findById(userId); // 消息的装配 ShareDTO shareDTO = ShareDTO.builder() .wxNickname(userDTO.getWxNickname()) .build(); BeanUtils.copyProperties(share, shareDTO); return shareDTO; } } 复制代码
-
Feign的组成
接口 | 作用 | 默认值 |
---|---|---|
Feign.Builder | Feign的入口 | Feign.Builder |
Client | Feign底层用什么取样请求 | 和Ribbo配合时:LoadBalancerFeignClient;不配合时:feign.Client.Default |
Contract | 契约,注解支持 | SpringMvcContract |
Encoder | 编码器,将对象转换成http请求消息体 | SpringEncoder |
Decoder | 解码器,将响应消息体转换成对象 | ResponseEntityDecoder |
Logger | 日志管理器 | Slf4jLogger |
RequestInterceptor | 用于为每个请求添加通用逻辑 | 无 |
自定义Feign日志级别
级别 | 打印内容 |
---|---|
NONE(默认值) | 不记录任何日志 |
BASIC | 仅记录请求方法、URL、响应状态代码以及执行时间 |
HEADERS | 记录BASIC级别的基础上,记录请求和响应的header |
FULL | 记录请求和响应的header、body和元数据(适用于开发环境) |
细粒度配置(日志级别)
Java代码方式
-
编写Feign配置类
package com.samir.contentcenter.configuration; import feign.Logger; import org.springframework.context.annotation.Bean; // 注意,如果这里加上了@Configuration注解,就得避免父子上文的问题,不然就是全局生效;不写就是最佳实现 public class UserCenterFeignConfigurantion { @Bean public Logger.Level level() { return Logger.Level.FULL; } } 复制代码
-
在Feign客户端引入配置类
package com.samir.contentcenter.feignclient; import com.samir.contentcenter.configuration.UserCenterFeignConfigurantion; import com.samir.contentcenter.domian.dto.user.UserDTO; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; @FeignClient(name = "user-center", path = "/users", configuration = UserCenterFeignConfigurantion.class) public interface UserCenterClient { /** * http://user-center/users/{id} * @param id * @return */ @GetMapping(value = "/{id}") UserDTO findById(@RequestParam("id") Integer id); } 复制代码
-
在配置文件配置Feign客户端的日志级别为debug
logging: level: com.samir.contentcenter.feignclient.UserCenterClient: debug 复制代码
配置属性方式
feign.client.config..loggerLevel: 日志级别
feign:
client:
config:
user-center:
loggerLevel: full
复制代码
全局配置
Java代码方式
-
方式一:
让父子上下文ComponentSacn重叠(强烈不建议使用) -
方式二:@EnableFeignClients(defaultConfiguration=xxx.class)
package com.samir.contentcenter; import com.samir.contentcenter.configuration.GlobalFeignConfiguration; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; @SpringBootApplication @MapperScan("com.samir.contentcenter.dao") @EnableDiscoveryClient // 只需要在启动类这里设置feign的配置就行了 @EnableFeignClients(basePackages = "com.samir.contentcenter.feignclient", defaultConfiguration = GlobalFeignConfiguration.class) public class ContentCenterApplication { public static void main(String[] args) { SpringApplication.run(ContentCenterApplication.class, args); } @Bean @LoadBalanced RestTemplate restTemplate() { return new RestTemplate(); } } 复制代码
配置属性方式
feign.client.config.default.loggerLevel: 日志级别
feign:
client:
config:
default: # 这里是default就是全局的配置
loggerLevel: full
复制代码
支持的配置项
代码方式
配置项 | 作用 |
---|---|
Feign.Builder | Feign的入口 |
Client | Feign底层用什么去请求 |
Contract | 契约,注解支持 |
Encoder | 编码器,用于将对象转换成http请求消息体 |
Decoder | 解码器,用于将响应消息体转换为对象 |
Logger | 日志管理器 |
属性方式
使用 **feign.client.config..属性 ** 的方式。如下属性:
- connectTimeout: 5000 # 连接超时时间
- readTimeout: 5000 # 读取超时时间
- loggerLevel: full # 日志级别
- errorDecoder: com.example.SimpleErrorDecoder # 错误解码器
- retryer: com.example.SimpleRetryer # 重试策略
- requestInterceptors: com.example.FooRequestInterceptor # 拦截器
- decode404: false # 是否对404错误码解码(处理逻辑见 feign.SynchronousMethodHandler#executeAndDecode)
- encoder: com.example.SimpleEncoder # 编码器
- decoder: com.example.SimpleDecoder # 解码器
- contract: com.example.SimpleContract # 契约
配置最佳实践
Ribbon配置 vs Feign配置
方式 | 粒度 | Ribbon | Feign |
---|---|---|---|
代码方式 | 局部 | @RibbonClient(name = "user-center", configuration = xxx.class);xxx.class必须使用@Configuration并且不能父子上下文重叠 | @FeignClient(name = "user-center", path = "/users", configuration = xxx.class);xxx.class非必须使用@Configuration,如果使用不能父子上下文重叠 |
代码方式 | 全局 | @RibbonClients(defaultConfiguration) | @EnableFeignClients(defaultConfiguration) |
属性方式 | 局部 | .ribbon.NFLoadBalancerRuleClassName = 规则的全路径 | feign.client.config..loggerLevel: 日志级别 |
属性方式 | 全局 | - | feign.client.config.default.loggerLevel: 日志级别 |
Feign代码方式 vs 属性方式
配置方式 | 优点 | 缺点 |
---|---|---|
代码配置 | 基于代码,更加灵活 | 注意父子上下文的问题;线上修改需要重新打包发布 |
属性配置 | 易上手;配置简洁直观;线上修改无需重新打包发布(配置配置中心);优先级更高 | 极端场景下没有代码配置方式灵活 |
优先级:全局代码配置 < 全局属性配置 < 细粒度代码配置 < 细粒度属性配置
最佳实现
- 尽量使用属性配置,属性方式实现不了的时候再考虑代码配置。
- 在同一个微服务中尽量保持单一性,不要两种方式混用,增加定位代码的复杂性。简单就是美。
Feign的继承
以user-center服务为例,user-center提供的接口与content-center调用user-center服务的Feign客户端接口基本上是一样的,那么我们就可以考虑将其接口独立出来,以一个maven独立api模块管理。但是官方不建议这样使用,而现在很多企业在使用,故需要根据自身情况决定是否适应继承。
多参数请求构造
-
GET
-
方式一
@GetMapping(value = "/find") UserDTO find(@RequestParam("id") Integer id, @RequestParam("name") String name); 复制代码
-
方式二
@GetMapping(value = "/find") UserDTO find(@StringQueryMap User user); 复制代码
-
方式三(不建议使用)
@GetMapping(value = "/find") UserDTO find(@RequestParam Map<String, Object> map); 复制代码
-
-
POST
-
方式一
@PostMapping(value = "/find") UserDTO find(@RequestBody User user); 复制代码
-
方式二(推荐)
@PostMapping(value = "/find", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE) UserDTO find(User user); 复制代码
-
Feign脱离Ribbon使用
使用feign调用未在注册中心注册的服务,例:www.baidu.com
package com.samir.contentcenter.feignclient;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
@FeignClient(name = "baidu", url = "http://www.baidu.com") // 使用url属性完成
public interface TestBaiduFeignClient {
@GetMapping("")
public String index();
}
复制代码
RestTemplate vs Feign
原则:尽量使用Feign,杜绝使用RestTemplate;但是出现Feign真的解决不了的问题,再考虑RestTemplate。
角度 | RestTemplate | Feign |
---|---|---|
可读性、可维护性 | 一般 | 极佳 |
开发体验 | 欠佳 | 极佳 |
性能 | 很好 | 中等(RestTemplate的50%左右) |
灵活性 | 极佳 | 中等(内置功能可满足绝大多数场景) |
Feign性能优化
为feign配置连接池【性能提升15%左右】
feign的底层默认使用UrlConnection请求,是没有连接池的。而Feign支持Apache的httpclient和okhttp,这两种http请求是支持连接池的,所有我们需要集成集中一种到我们的项目,配置。
-
httpclient
-
加依赖
<dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-httpclient</artifactId> </dependency> 复制代码
-
写配置
feign: httpclient: enabled: true # 让feign使用apache httpclient 做请求,而不是使用默认的urlconnection # 通过压测的结果配置最优的连接池大小 max-connections: 200 # feign的最大连接数 max-connections-per-route: 50 # feign单个路径的最大连接数 复制代码
-
-
okhttp
-
加依赖
<dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-okhttp</artifactId> <version>10.4.0</version> </dependency> 复制代码
-
写配置
feign: okhttp: enabled: true # 让feign使用okhttp 做请求,而不是使用默认的urlconnection httpclient: # 通过压测的结果配置最优的连接池大小 max-connections: 200 # feign的最大连接数 max-connections-per-route: 50 # feign单个路径的最大连接数 复制代码
-
日志级别
若生产环境需要日志,建议将生产环境日志级别设置为Basic
近期评论