Feign是Netflix开发的声明式、模版化的HTTP客户端。
Feign使用简单,创建一个接口,并加上一些注解,代码就完成了。Feign支持多种注解,如自带的注解或JAX-RS注解等
1.复制项目microservice-simple-consumer-movie,将artifactId修改为microservice-simple-consumer-movie-feign
2.添加Feign依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
<version>2.2.5.RELEASEversion>
dependency>
3.创建一个Feign接口,并添加@FeignClient注解
import com.lzy.cloud.microservicesimpleconsumermovie.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@FeignClient(name = "microservice-simple-provider-user")
public interface UserFeignClient {
@RequestMapping(value = "/{id}", method = RequestMethod.GET)
public User findById(@PathVariable("id") Long id);
}
@FeignClient注解中的microservice-provider-user是一个任意的客户端名称,用于创建Ribbon负载均衡器。本例中,由于使用了Eureka,所以Ribbon会把microservice-provider-user解析成Eureka Server服务注册表中的服务。当然,如果不想使用Eureka,可使用service.ribbon.listOfServers属性配置服务器列表(见上一篇)
还可以使用url属性指定请求的URL(URL可以是完整的URL或主机名),如:
@FeignClient(name = "microservice-simple-provider-user", url = "http://localhost:8000/")
4.修改Controller代码,让其调用Feign接口
@RestController
public class MovieController {
@Autowired
private UserFeignClient userFeignClient;
@GetMapping("/user/{id}")
public User findById(@PathVariable Long id){
return this.userFeignClient.findById(id);
}
}
5.修改启动类,为其添加@EnableFeignClient注解如下
@EnableDiscoveryClient
@SpringBootApplication
@EnableFeignClients
public class MicroserviceSimpleConsumerMovieApplication {
public static void main(String[] args) {
SpringApplication.run(MicroserviceSimpleConsumerMovieApplication.class, args);
}
}
启动eureka注册中心,启动多个用户微服务,启动feign调用的电影消费者微服务
多次访问http://localhost:8010/user/3,返回结果如下
{
"id": 3,
"username": "account3",
"name": "王五",
"age": 32,
"balance": 280
}
不但实现了声明式REST API调用,还实现了客户端侧的负载均衡
在Spring Cloud中,Feign的默认配置类是FeignClientsConfiguration,该类定义了Feign默认使用的编码、解码器和所使用的契约等。
Spring Cloud允许通过注解@FeignClient的configuration属性自定义Feign的配置,自定义配置的优先级高于 FeignClientsConfiguration。
下面是Spring Cloud文档中描述的默认配置:
Spring Cloud Netflix provides the following beans by default for feign(BeanType beanName: ClassName):
虽然Netflix没有为feign提供以下beans,但仍然可以从应用程序上下文中提取这些beans来创建client 客户端:
在Spring Cloud中,Feign默认使用的契约是SpringMvcContract,因此它可以使用Spring MVC注解。下面来自定义Feign的配置,让它使用Feign自带的注解进行工作。
1.复制项目microservice-simple-consumer-movie-feign,将artifactID改为microservice-simple-consumer-movie-feign-customizing
2.创建Feign的配置类
import feign.Contract;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/*
* 该类不应该在 应用程序上下文的 @ComponentScan中
*/
@Configuration
public class FeignConfiguration {
@Bean
public Contract feignContract() {
return new Contract.Default();
}
}
3.Feign接口修改如下,使用@FeignClient的configuration属性指定配置类,同时,将findById上的Spring MVC注解修改为Feign自带的注解。
@FeignClient(name = "microservice-simple-provider-user", configuration = FeignConfiguration.class)
public interface UserFeignClient {
//使用Feign自带的注解 RequestLine
@RequestLine("GET /{id}")
public User findById(@Param("id") Long id);
}
类似的,还可以自定义Feign的编码器、解码器、日志打印,甚至为Feign添加拦截器。例如,一些接口需要进行基于Http Basic 的认证后才能调用,配置类可以这样写:
@Configuration
public class FooConfiguration {
@Bean
public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
return new BasicAuthRequestInterceptor("user","password");
}
}
启动eureka服务注册发现中心,启动用户微服务,启动自定义的Feign电影微服务。
访问 http://localhost:8010/user/1,得到如下结果
{
"id": 1,
"username": "account1",
"name": "张三",
"age": 20,
"balance": 100
}
注意:记得将 配置文件中的属性fetch-registry
改为true,否则无法拉取服务列表
有时候自定义Feign无法满足需求,需要使用 Feign Builder API手动创建Feign
1.复制项目microservice-simple-provider-user,修改artifactId 为 microservice-simple-provider-user-with-auth
2.为项目添加以下依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
<version>2.3.4.RELEASEversion>
dependency>
3.创建Spring Security的配置类
package com.lzy.cloud.microservicesimpleprovideruser.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Collection;
@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 CustomUserDetailService userDetailService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(this.userDetailService).passwordEncoder(this.passwordEncoder());
}
@Component
class CustomUserDetailService implements UserDetailsService{
/*
* 模拟两个账户:
* 1.账号是user,密码是password1,角色是user-role
* 2.账号是admin,密码是password2,角色是admin-role
*/
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
if("user".equals(userName)){
return new SecurityUser("user","password1","user-role");
}else if("admin".equals(userName)){
return new SecurityUser("admin","password2","admin-role");
}else{
return null;
}
}
}
class SecurityUser implements UserDetails {
private static final long serialVersionUID = 1L;
private Long id;
private String userName;
private String password;
private String role;
public SecurityUser(String userName, String password, String role){
super();
this.userName = userName;
this.password = password;
this.role = role;
}
public SecurityUser(){
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList();
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(this.role);
authorities.add(authority);
return authorities;
}
@Override
public String getPassword() {
return null;
}
@Override
public String getUsername() {
return null;
}
@Override
public boolean isAccountNonExpired() {
return false;
}
@Override
public boolean isAccountNonLocked() {
return false;
}
@Override
public boolean isCredentialsNonExpired() {
return false;
}
@Override
public boolean isEnabled() {
return false;
}
}
}
模拟两个账户:
1.账号是user,密码是password1,角色是user-role
2.账号是admin,密码是password2,角色是admin-role
4.修改Controller ,在其中打印当前登录用户信息
package com.lzy.cloud.microservicesimpleprovideruser.controller;
import com.lzy.cloud.microservicesimpleprovideruser.dao.UserDao;
import com.lzy.cloud.microservicesimpleprovideruser.po.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import java.util.Collection;
import java.util.Optional;
@RestController
public class UserController {
@Autowired
private UserDao userDao;
private static final Logger LOGGER = LoggerFactory.getLogger(UserController.class);
@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){
//打印当前登录用户的信息
UserController.LOGGER.info("当前用户是{},角色是{}",user.getUsername(),c.getAuthority());
}
}else{
//do other things
}
Optional<User> findOne = userDao.findById(id);
User result = findOne.get();
return result;
}
}
启动eureka服务发现中心,启动用户微服务,访问localhost:8000/1,弹出对话框,输入身份验证即可登录
使用user/password1登录,可以看到打印当前用户是 user,角色是user-role
Feign支持继承。使用继承,可以将一些公共操作分组到一些父接口中,简化Feign的开发。简单示例如下:
基础接口 :UserService.java
public interface UserService {
@RequestMapping(method = RequestMethod.GET, value = "/users/{id}")
User getUser(@PathVariable("id") long id);
}
服务提供者 Controller : UserResource.java
@RestController
public class UserResource implements UserService {
//...
}
服务消费者 : UserClient.java
@FeignClient("users")
public interface UserClient extends UserService{
}
注意:官方指出,不建议在服务端和客户端之间共享接口,因为会造成服务端与客户端代码的紧耦合,且Feign本身不使用 Spring MVC的工作机制(方法参数映射不被继承)
有些场景可能需要对请求或响应进行压缩,可以使用以下属性启用Feign的压缩功能
feign.compression.request.enable=true
feign.compression.response.enable=true
对于请求的压缩,Feign还提供了更加详细的设置,如:
feign.compression.request.enable=true
//设置支持的媒体类型类表
feign.compression.request.mime-types=text/xml,application/xml,application/json
//设置请求最小阈值
feign.compression.request.min-request-size=2048
Feign日志处理很灵活,可以为每个Feign客户端指定日志记录策略,每个Feign客户端都会创建一个logger。Feign的日志打印只会对DEBUG级别作出响应。
1.为feign微服务添加配置类
import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FeignLogConfiguration {
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}
2.修改Feign的接口,指定配置类
@FeignClient(name = "microservice-simple-provider-user", configuration = FeignLogConfiguration.class)
public interface UserFeignClient {
@RequestMapping(value = "/{id}", method = RequestMethod.GET)
public User findById(@PathVariable("id") Long id);
}
3.在application.yml 添加如下内容,指定Feign接口的日志级别为DEBUG:
logging:
level:
com.lzy.cloud.microservicesimpleconsumermovie.feign.UserFeignClient: DEBUG
注意,这里的level里要填自己feignclient的完整路径
启动注册中心,用户微服务,消费者微服务,访问用户信息接口 localhost:8010/user/1,可以看到日志信息,可以相应的修改日志级别,能看到不同的日志展示情况。
@FeignClient(name = "microservice-provider-user")
public interface UserFeignClient {
@RequestMapping(value = "/get", method = RequestMethod.GET)
public User get1(@RequestParam("id") Long id, @RequestParam("username") String username);
}
URL有几个参数,Feign接口方法中就有几个参数,使用@RequestParam注解指定请求参数是什么
多参数的URL也可以使用Map来构建。当目标URL参数非常多时,可以使用这种方式简化Feign接口编写。
@FeignClient(name = "microservice-provider-user")
public interface UserFeignClient {
@RequestMapping(value = "/get", method = RequestMethod.GET)
public User get2(@RequestParam Map<String, Object> map);
}
调用时,使用类似以下的代码
public User get(String username, String password){
HashMap<String, Object> map = Maps.newHashMap();
map.put("id","1");
map.put("username","张三");
return this.userFeignClient.get2(map);
}
假设服务提供者Controller如下编写:
@RestController
public class UserController {
@PostMapping("/post")
public User post(@RequestBody User user) {
//...
}
}
用Feign去请求,示例如下:
@FeignClient(name = "microservice-provider-user")
public interface UserFeignClient{
@RequestMapping(value = "/post", method = RequestMethod.POST)
public User post(@RequestBody User user);
}