在前文《使用 Ribbon 实现客户端负载均衡》https://mp.weixin.qq.com/s/YwwCsUKoUA1UsaarZWVMHw 中,服务间调用使用的是 RestTemplate:
User entity = restTemplate.getForObject("http://micro-provider-user/user/v1/"+id, User.class);
由代码可知,主要是使用拼接字符串的方式构造 URL 来进行调用,这样会使得代码变得难以维护。
Feign 是 Netflix 开发的声明式 、模板化的 HTTP 客户端,Feign 可帮助我们更加便捷、优雅地调用 HTTP API 。
在 Spring Cloud 中, 使用 Feign 非常简单 – 创建一个接口,并在接口上添加一些注解,代码就完成了。 Feign 支持多种 注解,例如 Feign 自带的注解或者 JAX-RS 注解等。
Spring Cloud 对 Feign 进行了增强 ,使 Feign 支持了 SpringMVC 注解,并整合了 Ribbon 和 Eureka ,从而让 Feign 的使用更加方便。
Spring Cloud Feign 本质上是一种动态代理机制,你只要给出一个 RESTful API 对应的 Java 接口,它就可以在运行期动态的拼装出对应接口的强类型客户端,这个拼装出来的客户端的简化结构和请求响应的流程,如下图:
虽然我们开发出来的服务是弱类型的 RESTful 的服务,但是因为有 Spring Cloud Feign 的支持,我们只要简单的给出一个强类型的 Java API 接口,就自动获得了强类型的客户端。
关于强弱类型接口详情见:《如何基于 Spring Cloud Feign 实现强类型接口调用RESTful服务》https://mp.weixin.qq.com/s/bYZXhf9BfAfL5aC3bSyiHQ
创建项目,复制项目 micro-consumer-movie ,将 artifactId 修改为 micro-consumer-movie-feign 。
添加 Feign 的依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-feignartifactId>
dependency>
@FeignClient
注解。@FeignClient(name = "micro-provider-user")
public interface UserFeignClient {
@RequestMapping(value = "/user/v1/{id}", method = RequestMethod.GET)
User findById(@PathVariable("id") Long id);
}
@FeignClient
注解中的 micro-provider-user 是一个任意的客户端名称,用于 Ribbon 负载均衡器。
在本例中,由于使用了 Eureka ,所以 Ribbon 会把 micro-provider-user 解析成 Eureka Server 服务注册表中的服务。
当然,如果不想使用 Eureka 可使用 service.ribbon.listOfServers 属性配置服务器列表。
还可使用 url 属性指定请求的 URL (URL 可以是完整的 URL 或者主 名),例如 @FeignClient(name = "micro-provider-user", url="http://localhost:8000/")"
。
@RestController
@RequestMapping("/movie/v1")
public class MovieController {
@Resource
private UserFeignClient userFeignClient;
@GetMapping("/{id}")
public User findById(@PathVariable Long id){
User entity = userFeignClient.findById(id);
return entity;
}
}
@EnableFeignClients
注解。@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class MicroConsumerMovieFeignApplication {
public static void main(String[] args) {
SpringApplication.run(MicroConsumerMovieFeignApplication.class, args);
}
}
在某些场景下,可使用 Feign Builder API 手动创建 Feign 。
例如下场景:
一、修改用户微服务,使其需要登录认证通过才可访问
创建项目,复制项目 micro-provider-user ,将 artifactId 修改为 micro-provider-user-with-auth 。
为项目添加依赖。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private UserDetailsService userDetailsService;
@Bean
public PasswordEncoder passwordEncoder(){
// 明文编码器。Spring 提供做明文测试的
return NoOpPasswordEncoder.getInstance();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 所有的请求,都需要经过 HTTP basic 认证
http.authorizeRequests().anyRequest().authenticated().and().httpBasic();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
}
@Component
public class CustomUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if("user".equalsIgnoreCase(username)){
return new SecurityUser("user", "password1", "user-role");
} else if("admin".equalsIgnoreCase(username)){
return new SecurityUser("admin", "password2", "admin-role");
} else {
return null;
}
}
}
public class SecurityUser implements UserDetails {
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;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(this.role);
authorities.add(authority);
return authorities;
}
//...
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
模拟了两个账号: user 和 admin ,它们的密码分别是 password1 和 password2 ,角色分别是 user-role 和 admin-role 。
@RestController
@RequestMapping("/user/v1")
public class UserController {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Resource
private UserRepository userRepository;
@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> authorities = user.getAuthorities();
for(GrantedAuthority authority : authorities){
logger.info("当前用户是:{}, 角色是:{}", user.getUsername(), authority.getAuthority());
}
}
User entity = userRepository.findOne(id);
return entity;
}
}
二、修改电影微服务,使其可实现同一个 Feign 接口使用不同账号登录
复制项目 micro-consumer-movie-feign ,修改 artifactId 为 micro-consumer-movie-feign-manual 。
去掉 Feign 接口 UserFeignClient 的 @FeignClient
注解 。
去掉启动类上的 @EnableFeignClients
注解。
修改 Controller 如下:
@Import(FeignClientsConfiguration.class)
@RestController
@RequestMapping("/movie/v1")
public class MovieController {
private UserFeignClient userFeignClient;
private UserFeignClient adminFeignClient;
@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://micro-provider-user");
this.adminFeignClient = Feign.builder().client(client).encoder(encoder).decoder(decoder).contract(contract)
.requestInterceptor(new BasicAuthRequestInterceptor("admin", "password2"))
.target(UserFeignClient.class, "http://micro-provider-user");
}
@GetMapping("/user-user/{id}")
public User findByIdUser(@PathVariable Long id){
User entity = userFeignClient.findById(id);
return entity;
}
@GetMapping("/user-admin/{id}")
public User findByIdAdmin(@PathVariable Long id){
User entity = adminFeignClient.findById(id);
return entity;
}
}
其中,@Import
导入的 FeignClientsConfiguration 是 Spring Cloud 为 Feign 默认提供的配置类。
userFeignClient 登录账号 user, adminFeignClient 登录账号 admin ,它们使用的是同一个接口 UserFeignClient 。
https://gitee.com/chentian114/spring-cloud-practice
《Spring Cloud 与Docker 微服务架构实战》 周立