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;@Autowiredprivate 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自带的注解。* !!修改为默认契约后,启动应用时下面接口会报错,所以建议不要使用*/@Beanpublic 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{
@Overrideprotected void configure(HttpSecurity http) throws Exception{//所有的请求,都需要经过HTTP basic认证http.authorizeRequests().anyRequest().authenticated().and().httpBasic();}@Beanpublic PasswordEncoder passwordEncoder(){//明文编码器。这是一个不做任何操作的密码编码器,是Spring提供给我们做明文测试的return NoOpPasswordEncoder.getInstance();}//在下面@Autowiredprivate CustomUserDetailsService userDetailService;@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception{auth.userDetailsService(this.userDetailService).passwordEncoder(this.passwordEncoder());}@Componentclass CustomUserDetailsService implements UserDetailsService{/*** 模拟两个账户* ① 账号 user,密码123,角色是user-role* ② 账号 admin,密码123,角色是admin-role*/@Overridepublic 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");}elsereturn 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;@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {Collection<GrantedAuthority> authorities = new ArrayList<>();SimpleGrantedAuthority authority = new SimpleGrantedAuthority(this.role);authorities.add(authority);return authorities;}//关键点:以下四个方法返回值返回true@Overridepublic boolean isAccountNonExpired() {
return true;}@Overridepublic boolean isAccountNonLocked() {
return true;}@Overridepublic boolean isCredentialsNonExpired() {
return true;}@Overridepublic boolean isEnabled() { return true;}@Overridepublic String getPassword() {return this.password;}@Overridepublic 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,我用的是 MyBatisreturn 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")@Autowiredpublic 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接口的日志级别设置为DEBUGcn.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{
@Overrideprotected 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微服务架构实战》周立/著