Springcloud微服务架构-使用 Feign实现声明式 REST 调用

书接上文,之前的代码使用字符串拼接的方式构造我们调用的 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

《Springcloud微服务架构-使用 Feign实现声明式 REST 调用》

从浏览器里面访问

《Springcloud微服务架构-使用 Feign实现声明式 REST 调用》

不仅仅如此,我们的请求是分别随机请求到 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 微服务接口,看到需要登录

《Springcloud微服务架构-使用 Feign实现声明式 REST 调用》

输入 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 接口也同样使用即可

    原文作者:breezedancer
    原文地址: https://www.jianshu.com/p/04218c285a73
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞