Feign 是 Netflix 开发的声明式、模板化的 HTTP 客户端,它可以帮助我们更加便捷、优雅地调用 HTTP API
前言
本文中涉及到的 Spring Cloud 内容,可以查看我的相关博客
使用的 Feign 版本为 1.4.3
- 服务端指 Eureka Server 所在微服务,客户端指提供数据的微服务,消费端指获取数据的微服务
1、Eureka 整合 Feign
添加 Feign 依赖
compile('org.springframework.cloud:spring-cloud-starter-feign')
创建 Feign 接口,添加 @FeignClient 注解
//name是客户端的虚拟主机名
@FeignClient(name = "microservice-provider-user")
@Service
public interface UserFeignClient {
//这里写客户端的访问路径
@GetMapping("/{id}")
User findById(@PathVariable("id") Long id);
}
其中的 microservice-provider-user 是任意一个客户端的虚拟主机名,用于创建 Ribbon 负载均衡器
下面修改 Controller 代码
@RestController
public class BaseController {
// @Autowired
// private RestTemplate restTemplate;
@Autowired
private UserFeignClient userFeignClient;
//之前我们是使用 RestTemplate 调用,需要拼接字符串
// @GetMapping("/user/{id}")
// public User findById(@PathVariable Long id){
// return this.restTemplate.getForObject("http://microservice-provider-user/"+id,User.class);
// }
//相比于 RestTemplate ,Feign 明显地更加简洁
@GetMapping("/user/{id}")
public User findById_feign(@PathVariable Long id){
return this.userFeignClient.findById(id);
}
}
启动类上添加注解
@EnableFeignClients
这样我们就可以使用 Feign 来调用微服务的 API ,取代使用拼接方式访问的 RestTemplate
2、自定义 Feign 配置
创建 Feign 配置类
/**
* !!不能在主应用程序的上下文的@Component中,即不能在启动类所在包中
*/
@Configuration
public class FeignConfiguration {
/**
* 将契约改为feign原生的默认契约,这样可以使用feign自带的注解。
* !!修改为默认契约后,启动应用时下面接口会报错,所以建议不要使用
*/
@Bean
public Contract feignContract(){
return new Contract.Default();
}
}
修改 Feign 接口如下
//使用 configuration 属性指定配置类
@FeignClient(name = "microservice-provider-user",configuration = FeignConfiguration.class)
@Service
public interface UserFeignClient {
/**
* 经过测试,如果启用上面的默认契约,这里在启动应用时会报错
* 下面两种方式都是可以的,RequestLine 是 Feign 的自带注解
*/
// @RequestLine("GET/{id}")
// User findById(@Param("id") Long id);
@GetMapping("{id}")
User findById(@PathVariable("id") Long id);
}
类似地可以自定义 Feign 的编码器、解码器等(这些未实验)。例如调用需要 HTTP Basic 认证的接口,配置类 FeignConfiguration 中加入:
//过滤器 Http Basic 认证
@Bean
public BasicAuthorizationInterceptor basicAuthorizationInterceptor(){
return new BasicAuthorizationInterceptor("user","password");
}
3、自建 Feign
在某些场景下,自定义的 Feign 满足不了需求,此时可用 Feign Builder API 手动创建 Feign
首先,在 客户端 微服务上建立 Spring Security 配置
导入 Spring Security 依赖
compile( 'org.springframework.boot:spring-boot-starter-security')
创建配置类(可以全部copy)
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
@Override
protected void configure(HttpSecurity http) throws Exception{
//所有的请求,都需要经过HTTP basic认证
http.authorizeRequests().anyRequest().authenticated().and().httpBasic();
}
@Bean
public PasswordEncoder passwordEncoder(){
//明文编码器。这是一个不做任何操作的密码编码器,是Spring提供给我们做明文测试的
return NoOpPasswordEncoder.getInstance();
}
//在下面
@Autowired
private CustomUserDetailsService userDetailService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception{
auth.userDetailsService(this.userDetailService).passwordEncoder(this.passwordEncoder());
}
@Component
class CustomUserDetailsService implements UserDetailsService{
/**
* 模拟两个账户
* ① 账号 user,密码123,角色是user-role
* ② 账号 admin,密码123,角色是admin-role
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if("user".equals(username)) {
return new SecurityUser("user", "123", "user-role");
}
else if("admin".equals(username)) {
return new SecurityUser("admin", "123", "admin-role");
}
else
return null;
}
}
class SecurityUser implements UserDetails {
private static final long serialVersionUID = 1L;
public SecurityUser(String username, String password, String role) {
super();
this.username = username;
this.password = password;
this.role = role;
}
public SecurityUser() {
}
private Long id;
private String username;
private String password;
private String role;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(this.role);
authorities.add(authority);
return authorities;
}
//关键点:以下四个方法返回值返回true
@Override
public boolean isAccountNonExpired() {return true;}
@Override
public boolean isAccountNonLocked() {return true;}
@Override
public boolean isCredentialsNonExpired() {return true;}
@Override
public boolean isEnabled() { return true;}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
}
}
修改 Controller,打印当前登录的用户信息
@GetMapping("{id}")
public User findById(@PathVariable long id){
Object principal= SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if(principal instanceof UserDetails){
UserDetails user=(UserDetails) principal;
Collection<? extends GrantedAuthority> collection=user.getAuthorities();
//打印当前用户信息
for(GrantedAuthority c:collection){
BaseController.LOGGER.info("用户:{},角色:{}",user.getUsername(),c.getAuthority());
}
}
//这里从数据库获取 User,我用的是 MyBatis
return userMapper.findById(id);
}
启动服务端和客户端测试,会弹出登录对话框
分别使用 user / 123 和 admin / 123 登录,会输出类似以下的日志:
2018-03-19 15:53:43.605 INFO 4404 --- [nio-8001-exec-3] c.i.port.controller.BaseController : 用户:user,角色:user-role
2018-03-19 15:53:43.605 INFO 4404 --- [nio-8001-exec-3] c.i.port.controller.BaseController : 用户:admin,角色:admin-role
用户信息是保存在了 Session 中,所以注销用户的方法就是重启浏览器
现在修改消费端微服务
去掉 Feign 接口 UserFeignClient 上的 @FeignClient 注解
去掉启动类上的 @EnableFeignClients 注解
修改 Controller 如下:
@Import(FeignClientsConfiguration.class)
@RestController
public class BaseController {
private UserFeignClient userFeignClient;
private UserFeignClient adminFeignClient;
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
@Autowired
public BaseController(Decoder decoder, Encoder encoder, Client client, Contract contract){
this.userFeignClient= Feign.builder().client(client).encoder(encoder).decoder(decoder).contract(contract)
.requestInterceptor(new BasicAuthRequestInterceptor("user","123"))
.target(UserFeignClient.class,"http://microservice-provider-user/");
this.adminFeignClient= Feign.builder().client(client).encoder(encoder).decoder(decoder).contract(contract)
.requestInterceptor(new BasicAuthRequestInterceptor("admin","123"))
.target(UserFeignClient.class,"http://microservice-provider-user/");
}
@GetMapping("/user-user/{id}")
public User findById_user(@PathVariable Long id){
return this.userFeignClient.findById(id);
}
@GetMapping("/user-admin/{id}")
public User findById_admin(@PathVariable Long id){
return this.userFeignClient.findById(id);
}
}
其中,@Import 导入的是 Spring Cloud 为 Feign 默认提供的配置类。两个方法各司其职,分别登录 user 和 admin,使用同一个接口 UserFeignClient
启动服务端、客户端、消费端(端口号8010),访问http://localhost:8010/user-user/1 和http://localhost:8010/user-admin/1,可以看到客户端微服务打印登录信息
3、Feign 的其他支持
对继承的支持
使用继承,可以将一些公共操作分组到一些父接口中,从而简化 Feign 的开发
创建基础接口 : UserService.java
public interface UserService{
@RequestMapping(method = RequestMethod.GET,value = "/user/{id}")
User getUser(@PathVariable("id") long id);
}
服务提供者 Controller : UserResource.java
@RestController
public class UserResource implements UserService{
//...
}
服务消费者 : UserClient.java
@FeignClient("user")
public interface UserClient extends UserService{
}
对压缩的支持
一些场景下,可能需要对请求后响应进行压缩,此时可使用以下的属性启用 Feign 的压缩功能
feign.compression.request.enabled=true
feign.compression.response.enabled=true
更详细的配置
feign.compression.request.enabled=true
feign.compression.request.mime-types=text/xml,application/xml,application/json
feign.compression.request.min-request-size=2048
其中
- feign.compression.request.mime-types 用于支持的媒体类型列表,默认是 text/xml、application/xml 以及 application/json
- feign.compression.request.min-request-size 用于设置请求的最小阈值,默认是2048
4、Feign 的日志
把项目回归到 第2条( 自定义 Feign 配置 ) 的状态:( 如果你拒绝,可以直接跳到下面黑体字 )
- 加上启动类注解
- 加上 Feign 接口的注解
- UserFeignClient 使用 @Autowire 自动导入
配置类 FeignConfiguration 中加入:
@Bean
public Logger.Level feignLoggerLevel(){
//设置为输出详细信息
return Logger.Level.FULL;
}
application.xml 中添加如下:
logging:
level:
# 将Feign接口的日志级别设置为DEBUG
cn.itscloudy.consumer.feign.UserFeignClient: DEBUG
其中 cn.itscloudy.consumer.feign.UserFeignClient 是你 Feign 接口的路径
然后启动服务端、客户端以及消费端,测试可以发现 Feign 请求的各种细节非常详细地记录了下来
把上面方法返回值设为 Logger.Level.BASIC,再次测试,控制台只打印了请求方法、请求的 URL 等
如果,项目是自建 Feign 的状态,即第3条的状态,需要以下步骤
首先,自建 MyLogger 类继承 feing.Logger.ErrorLogger (因为经过测试,只有 ErrorLogger 才能输出信息)
public class MyLogger extends feign.Logger.ErrorLogger{
@Override
protected void log(String configKey, String format, Object... args) {
//所有关键信息都在 args 里了,可以自己输出看下,然后自定义格式输出所需信息
for(Object o:args){
System.out.println(o.toString());
}
// super.log(configKey,format,args);
}
}
构建 UserFeignClinet 时加上 .logger 指定 Logger ,加上 .logLevel 指定输出级别。
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
@Autowired
public BaseController(Decoder decoder, Encoder encoder, Client client, Contract contract){
this.userFeignClient= Feign.builder().client(client).encoder(encoder).decoder(decoder).contract(contract)
.logger(new MyLogger())
.logLevel(feign.Logger.Level.FULL)
.requestInterceptor(new BasicAuthRequestInterceptor("user","123"))
.target(UserFeignClient.class,"http://microservice-provider-user/");
}
这里的 Logger 使用 MyLogger,当然也可以直接使用 ErrorLogger ,但是输出格式是全红
然后,application.yml 也还需要有所添加,添加内容上面有叙述,不再赘述。
( 如果针对这一点有更好解决方法,欢迎告知 )
5、构造 Feign 多参数请求
GET
@FeignClient(name = "microservice-provider-user")
@Service
public interface UserFeignClient {
//方法一
// @GetMapping("/")
// User findUser(@RequestParam("id") Long id),@RequestParam("username") username);
//方法二
@GetMapping("/")
User findUser(@RequestParm Map<String,Object> map);
}
使用方法二调用,可使用以下类似方法
public User getUser(int id,String username){
HashMap<String,Object> map=new HashMap<>();
map.put("id",id);
map.put("username",username);
return this.userFeignClient.findUser(map);
}
POST
@FeignClient(name = "microservice-provider-user")
@Service
public interface UserFeignClient {
//使用@RequestBody注解
@PostMapping("/")
User findUser(@RequestBody User user);
}
后记
以上代码大部分经过了我的测试
引用的内容源自《Spring Cloud与Docker微服务架构实战》周立/著