Springcloud微服务架构-使用 Feign实现声明式 R
书接上文,之前的代码使用字符串拼接的方式构造我们调用的 URL,目前这个 URL 有一个参数,如果有很多参数,我们就需要构造一个哈希表,URL 上面挂满了&参数连接符
Feign 是 Netflix 开发的声明式、模板化的 HTTP 客户端,帮助我们更加优雅的调用 HTTP API。
集成 Feign
我们再 movie 微服务里面集成 Feign,首先添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
<version>1.3.5.RELEASE</version>
</dependency>
之后我们需要构造一个接口,这个接口是专门处理掉 HTTP API 的
为了方便,这里建一个 feign 包,在这个包下面建立一个接口UserFeignClient
,用来调用用户接口的 feign REST 接口
package cn.ts.ms.movie.feign;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import cn.ts.ms.movie.model.User;
@FeignClient(name="MS-SIMPLE-PROVIDER-USER")
public interface UserFeignClient {
@RequestMapping(value="/user/find?id={id}",method=RequestMethod.GET)
public User findById(@PathVariable("id") Integer id);
}
说明:
@FeignClient(name="MS-SIMPLE-PROVIDER-USER")
标明是向 user 这个未付发起请求的;
在 requestMapping 里面构造了请求路径,合在一起就是 user 微服务的 HTTP 调用;这里的 REST 格式不正确,姑且先走通程序。
接下来在 Controller 里面调用 feign 来进行数据请求
@RestController
public class MovieController {
@Autowired
private RestTemplate restTemplate;
@Autowired
private LoadBalancerClient loadBalancerClient;
@Autowired
private UserFeignClient userFeignClient;
@GetMapping("/user/{id}")
public User findByUserId(@PathVariable Integer id){
//return restTemplate.getForObject("http://MS-SIMPLE-PROVIDER-USER/user/find?id="+id, User.class);
return userFeignClient.findById(id);
}
@GetMapping("/log")
public void printLog(){
ServiceInstance instance = loadBalancerClient.choose("MS-SIMPLE-PROVIDER-USER");
System.out.println("Now:"+instance.getServiceId()+"---"+instance.getHost()+":"+instance.getPort());
}
}
接下来修改启动类,增加@EnableFeignClients 注解
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class MsSimpleConsumerMovieApplication {
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(MsSimpleConsumerMovieApplication.class, args);
}
}
测试和启动
启动 eureka1和 eureka2以及两个 user,最后启动 movie
从浏览器里面访问
不仅仅如此,我们的请求是分别随机请求到 user 的两个微服务,也就是实现的负载均衡
自定义 Feign
实际应用开发过程中,上面的例子是不能使用的,用户接口需要账号才能访问,而且不同用户访问结果不一样才行
下面就来修改之前的微服务达到这个目的
首先修改用户微服务
增加security 的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
接下来我们就需要配置 security 的配置类,关于 Spring security将在后续介绍,这里直接引入他人的基本配置
package cn.ts.ms.user;
import java.util.ArrayList;
import java.util.Collection;
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;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter{
@Override
protected void configure(HttpSecurity http) throws Exception {
//所有请求都需要经过 HTTP Basic 认证
http.authorizeRequests().anyRequest().authenticated().and().httpBasic();
}
@Bean
public PasswordEncoder passwordEncoder(){
return NoOpPasswordEncoder.getInstance();
}
@Autowired
private CustomerUserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(this.passwordEncoder());
}
@Component
class CustomerUserDetailsService implements UserDetailsService {
//构造用户,可以查询数据库来完成
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if("user".equals(username)){
return new SecurityUser("user","123","role");
}else if("admin".equals(username)){
return new SecurityUser("admin","123","admin");
}
return null;
}
}
class SecurityUser implements UserDetails{
private static final long serialVersionUID = 1L;
public SecurityUser() {}
public SecurityUser(String name,String password,String role) {
this.name=name;
this.password=password;
this.role=role;
}
private Integer id;
private String name;
private String password;
private String role;
//手机用户的角色列表
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities=new ArrayList<>();
SimpleGrantedAuthority role=new SimpleGrantedAuthority(this.role);
authorities.add(role);
return authorities;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.name;
}
@Override
public boolean isAccountNonExpired() {//账号没有过期
return true;
}
@Override
public boolean isAccountNonLocked() {//账号没有被锁
return true;
}
@Override
public boolean isCredentialsNonExpired() {//凭证没有失效
return true;
}
@Override
public boolean isEnabled() {//开启状态
return true;
}
}
}
接下来在控制层修改一下代码
package cn.ts.ms.user.controller;
import java.util.Collection;
import javax.annotation.Resource;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
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.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import cn.ts.ms.user.mapper.UserMapper;
import cn.ts.ms.user.model.User;
@RestController
@RequestMapping("/user")
@EnableAutoConfiguration
public class UserController {
@Resource
private UserMapper userMapper;
@RequestMapping("/find")
public User findUserById(@RequestParam String id){
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if(principal instanceof UserDetails){
UserDetails userDetails=(UserDetails)principal;
Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
for(GrantedAuthority auth:authorities){
System.out.println("当前用户:"+userDetails.getUsername()+" 拥有角色"+auth.getAuthority());
}
}
System.out.println("==================================");
return userMapper.findUserById(id);
}
}
在浏览器里面访问 user 微服务接口,看到需要登录
输入 user/123 或者 admin/123 可以进入正常浏览
接下来修改电影微服务
去掉UserFeignClient的注解;
去掉启动类的 EnableFeign的注解
修改 Controller 类如下
package cn.ts.ms.movie.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.cloud.netflix.feign.FeignClientsConfiguration;
import org.springframework.context.annotation.Import;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import cn.ts.ms.movie.feign.UserFeignClient;
import cn.ts.ms.movie.model.User;
import feign.Client;
import feign.Contract;
import feign.Feign;
import feign.auth.BasicAuthRequestInterceptor;
import feign.codec.Decoder;
import feign.codec.Encoder;
@Import(FeignClientsConfiguration.class)
@RestController
public class MovieController {
@Autowired
private RestTemplate restTemplate;
@Autowired
private LoadBalancerClient loadBalancerClient;
// @Autowired
// private UserFeignClient userFeignClient;
private UserFeignClient userUserFeignClient;
private UserFeignClient adminUserFeignClient;
@Autowired
public MovieController(Decoder decoder,Encoder encoder,Client client,Contract contract){
this.userUserFeignClient=Feign.builder().client(client).encoder(encoder).decoder(decoder).contract(contract)
.requestInterceptor(new BasicAuthRequestInterceptor("user","123"))
.target(UserFeignClient.class,"http://MS-SIMPLE-PROVIDER-USER/");
this.adminUserFeignClient=Feign.builder().client(client).encoder(encoder).decoder(decoder).contract(contract)
.requestInterceptor(new BasicAuthRequestInterceptor("admin","123"))
.target(UserFeignClient.class,"http://MS-SIMPLE-PROVIDER-USER/");
}
@GetMapping("/user-user/{id}")
public User fingByUserIdWithUser(@PathVariable Integer id){
return this.userUserFeignClient.findById(id);
}
@GetMapping("/user-admin/{id}")
public User fingByUserIdWithAdmin(@PathVariable Integer id){
return this.adminUserFeignClient.findById(id);
}
// @GetMapping("/user/{id}")
// public User findByUserId(@PathVariable Integer id){
// //return restTemplate.getForObject("http://MS-SIMPLE-PROVIDER-USER/user/find?id="+id, User.class);
// return userFeignClient.findById(id);
// }
@GetMapping("/log")
public void printLog(){
ServiceInstance instance = loadBalancerClient.choose("MS-SIMPLE-PROVIDER-USER");
System.out.println("Now:"+instance.getServiceId()+"---"+instance.getHost()+":"+instance.getPort());
}
}
再次访问 movie 的接口,用不同的 URL,可以发现在user 微服务打印的日志不一样
http://127.0.0.1:8010/user-user/1
对应的日志当前用户:user 拥有角色role
http://127.0.0.1:8010/user-admin/1
对应的日志当前用户:admin 拥有角色admin
Feign 支持压缩功能,在 yml 配置里面增加对应的配置即可,版本不同配置不一样貌似。
Feign 的多参数问题
1、在接口入参的地方,每个接口参数对应一个请求参数;
2、配置 map 表传递到接口
Post 请求多参数,直接使用@RequestBody 注解类来进行,Feign 接口也同样使用即可