- Demo 源码下载
- 本案例为源码分支的 feign 分支
简介
- Feign 是声明式、模版化的 HTTP 客户端,可以更加便捷的调用 HTTP API
快速入门
- 通过 RestTemplate 调用 API 大致代码如下
restTemplate.getForObject("http://flim-user/"+id,User.class);
- 但当 URL 有多个参数时,就会变得很低效,如要请求这样的请求 URL: http://localhost:8010/search?name=张三&username=account1&age=20
Map map = new HashMap<>();
map.put("name","张三");
map.put("username","account1");
map.put("age",20);
this.restTemplate.getForObject(" http://localhost:8010/search?name={name}&username={username}&age={age}",User[].class,map);
- 而使用 Feign 非常简单,创建一个接口,并添加一些注解就可以了
服务消费者整合 Feign
org.springframework.cloud
spring-cloud-starter-feign
1.4.4.RELEASE
- 创建一个 Feign 接口,并添加 @FeignClient 注解
@FeignClient(name = "film-user") // 服务提供者名称
public interface UserFeignClient {
@GetMapping("/{id}")
UserEntity findById(@PathVariable("id") int id);
}
- 由于使用 Eureka,film-user 会被解析成 Eureka Server 中注册的微服务,若不想使用 Eureka,可以使用 service.ribbon.listOfServers 属性配置服务列表
- 还可使用 url 属性指定请求 URL
@FeignClient(name = "film-user",url = "http://localhost:8080")
- 修改 Controller 代码,调用其 Feign 接口
@Autowired
private UserFeignClient userFeignClient;
@GetMapping("/user/{id}")
public UserEntity findById(@PathVariable int id){
return userFeignClient.findById(id);
}
- 启动类添加 @EnableFeignClients
自定义 Feign 配置
- Feign 默认配置类是 FeignClientsConfiguration,该类定义了默认解码器、编码器、所用的契约等,Spring Cloud 允许通过 @FeignClient 的 configuration 属性自定义配置,优先级比默认要高,Feign 可使用 SpringMVC 注解进行工作
- 创建 Feign 配置类
@Configuration
public class FeignConfiguration {
//使用Feign自带的注解
@Bean
public Contract feignContract(){
return new feign.Contract.Default();
}
}
- 修改 Feign 接口,使用 @FeignClient 的 configuration 属性指定配置类,同时将 findById 上的 SpringMVC 注解改为 Feign 自带的注解
@FeignClient(name = "film-user",configuration = FeignConfiguration.class)
public interface UserFeignClient {
//使用Feign自带的注解@RequestLine
@RequestLine("GET /{id}")
UserEntity findById(@PathVariable("id") int id);
}
- 还可以定义 Feign 的解码器、编码器、日志打印、拦截器等,一些接口需要 HttpBasic 认证后才能调用
手动创建 Feign
- Feign 默认的方式可能无法满足业务需求,可用 Feign Builder API 手动创建 Feign,例如案例要实现的需求如下:
- 用户微服务的接口需要登录才能调用,不同的用户角色有不同的行为
- 电影微服务中,不同的账号登录,调用不同的微服务接口
修改服务提供者
org.springframework.boot
spring-boot-starter-security
- 创建 SpringSecurity 配置类,添加两个账号 user 和 admin,密码分别是 password1 和 password2,角色分别是 user-role 和 admin-role
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user").password("password1").roles("USER")
.and()
.withUser("admin").password("password2").roles("ADMIN")
.and()
.passwordEncoder(this.passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//所有请求都要经过httpBasic认证
http.authorizeRequests().anyRequest().authenticated().and().httpBasic();
}
@Bean
public PasswordEncoder passwordEncoder(){
//返回一个明文密码器,Spring 提供给我们做明文测试的
return NoOpPasswordEncoder.getInstance();
}
}
- 修改 UserController 打印当前登录的用户信息
@RestController
public class UserController {
@Autowired
private UserRepository userRepository;
private final Logger log = LoggerFactory.getLogger(UserController.class);
@GetMapping("/{id}")
public User findById(@PathVariable int id){
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
org.springframework.security.core.userdetails.User principal = (org.springframework.security.core.userdetails.User) authentication.getPrincipal();
Collection extends GrantedAuthority> authorities = authentication.getAuthorities();
log.info("当前的用户是{},角色是{}",principal.getUsername(),authorities.stream()
.map((GrantedAuthority e) -> e.getAuthority()).collect(Collectors.joining(",")));
User user = userRepository.findOne(id);
return user;
}
}
- 使用 user/password1 登录与 user/password2 登录会看到如下日志
com.linyuan.controller.UserController : 当前的用户是user,角色是ROLE_USER
com.linyuan.controller.UserController : 当前的用户是admin,角色是ROLE_ADMIN
修改服务消费者
- 去掉接口 UserFeignClient 上的 @FeignClient 注解,去掉启动类上的 @EnableFeignClients 注解
- 修改 Controller,通过 @Import 导 FeignClientsConfiguration,FeignClientsConfiguration 是 Spring Cloud 为 Feign 提供的默认配置,这里 userFeignClient 登录账号是 user,adminFeignClient 登录账号是 admin,它们使用的是同一个接口
@Import(FeignClientsConfiguration.class)
@RestController
public class MovieController {
private UserFeignClient adminFeignClient;
private UserFeignClient userFeignClient;
@Autowired
public MovieController(Decoder decoder, Encoder encoder, Client client, Contract contract) {
this.userFeignClient = Feign.builder().client(client).encoder(encoder).decoder(decoder).contract(contract)
.requestInterceptor(new BasicAuthRequestInterceptor("user","password1"))
.target(UserFeignClient.class,"http://film-user/");
this.adminFeignClient = Feign.builder().client(client).encoder(encoder).decoder(decoder).contract(contract)
.requestInterceptor(new BasicAuthRequestInterceptor("admin","password2"))
.target(UserFeignClient.class,"http://film-user/");
}
@GetMapping("/user/{id}")
public UserEntity findByIdUser(@PathVariable int id){
return userFeignClient.findById(id);
}
@GetMapping("/admin/{id}")
public UserEntity findByIdAdmin(@PathVariable int id){
return adminFeignClient.findById(id);
}
}
Feign 对压缩的支持
- 某些场景下,可能需要对请求或响应进行压缩,可通过以下属性启动 Feign 的压缩功能
feign.compression.request.enabled = true
feign.compression.response.enabled = true
- 对于请求的压缩,还可以更详细的配置,比如配置支持的媒体类型表,默认是,请求的最小阈值
feign.compression.request.enabled = true
feign.compression.request.mine-types = text/xml,application/xml,application/json #默认配置
feign.compression.request.min-request-size = 2048 #默认配置
Feign 日志
- Feign 对日志的处理非常灵活,可为每个 Feign 客户端指定日志策略,每个 Feign 客户端都会创建一个 Logger,默认名称为 Feign 接口的完整类名,但 Feign 日志打印只会对 DEBUG 级别做出响应
- 我们可为每个 Feign 客户端配置各自的 Logger.level 对象,告诉 Feign 记录哪些日志,Logger.Level 的值有以下选择:
- NONE:不记录任何日志(默认)
- BASIC:仅记录请求方法、URL、响应状态码以及执行时间
- HEADERS:记录 BASIC 级别的基础上,记录请求和响应的 header
- FULL:记录请求响应的 header、body 和元数据
- 编写 Feign 配置类
@Configuration
public class FeignConfiguration {
@Bean
public Logger.Level feignLoggerLevel(){
return Logger.Level.BASIC;
}
}
@FeignClient(name = "film-user",configuration = FeignConfiguration.class)
public interface UserFeignClient {
@RequestMapping(value = "/{id}", method = RequestMethod.GET)
UserEntity findById(@PathVariable("id") int id);
}
- 添加以下配置到配置文件,指定 Feign 接口日志级别为 DEBUG
logging:
level:
com.linyuan.request.UserFeignClient: DEBUG #Feign的Logger.Level只对DEBUG作出响应/
- 访问 http://localhost:8010/user/1 会输出
2017-12-22 11:41:34.387 DEBUG 7405 --- [nio-8010-exec-3] com.linyuan.request.UserFeignClient : [UserFeignClient#findById] ---> GET http://flim-user/1 HTTP/1.1
2017-12-22 11:41:34.395 DEBUG 7405 --- [nio-8010-exec-3] com.linyuan.request.UserFeignClient : [UserFeignClient#findById] <--- HTTP/1.1 200 (8ms)
2017-12-22 11:41:35.001 DEBUG 7405 --- [nio-8010-exec-5] com.linyuan.request.UserFeignClient : [UserFeignClient#findById] ---> GET http://flim-user/1 HTTP/1.1
2017-12-22 11:41:35.012 DEBUG 7405 --- [nio-8010-exec-5] com.linyuan.request.UserFeignClient : [UserFeignClient#findById] <--- HTTP/1.1 200 (10ms)
2017-12-22 11:41:35.591 DEBUG 7405 --- [nio-8010-exec-4] com.linyuan.request.UserFeignClient : [UserFeignClient#findById] ---> GET http://flim-user/1 HTTP/1.1
2017-12-22 11:41:35.602 DEBUG 7405 --- [nio-8010-exec-4] com.linyuan.request.UserFeignClient : [UserFeignClient#findById] <--- HTTP/1.1 200 (11ms)
Feign 构造多参数请求
GET 请求多参数
- 假设请求 URL 包含多个参数,如:http://flim-user/get?id=1&username=张三,因为 Feign 支持 SpringMVC 注解,则写法可以为:
@FeignClient(name = "film-user")
public interface UserFeignClient {
@RequestMapping(value = "/get", method = RequestMethod.GET)
UserEntity get1(@RequestParam("id") int id,@RequestParam("username")String username);
}
- 方法二,可使用 Map 来构建,在调用时传递 Map 对象
@RequestMapping(value = "/get", method = RequestMethod.GET)
User get2(@RequestParam Map map);
public User get(String username,String password){
HashMap map = Maps.newHashMap();
map.put("id",1);
map.put("username","张三");
return this.userUserFeignClient.get2(map);
}
POST 请求多参数
@RestController
public class UserController{
@PostMapping("/post")
public User post (@RequestBody User user){
...
}
}
@FeignClient(name = "flim-user")
public interface UserFeignClient {
@RequestMapping(value = "/post", method = RequestMethod.POST)
UserEntity post(@RequestBody User user);
}