Feign 的使用

Feign 是 Netflix 开发的声明式、模板化的 HTTP 客户端,它可以帮助我们更加便捷、优雅地调用 HTTP API

前言
  • 本文中涉及到的 Spring Cloud 内容,可以查看我的相关博客

  • 使用的 Feign 版本为 1.4.3

  • 服务端指 Eureka Server 所在微服务,客户端指提供数据的微服务,消费端指获取数据的微服务
1、Eureka 整合 Feign

添加 Feign 依赖

compile('org.springframework.cloud:spring-cloud-starter-feign')

创建 Feign 接口,添加 @FeignClient 注解

//name是客户端的虚拟主机名
@FeignClient(name = "microservice-provider-user")
@Service
public interface UserFeignClient {

    //这里写客户端的访问路径
    @GetMapping("/{id}")
    User findById(@PathVariable("id") Long id);
}

其中的 microservice-provider-user 是任意一个客户端的虚拟主机名,用于创建 Ribbon 负载均衡器

下面修改 Controller 代码

@RestController
public class BaseController {
//    @Autowired
//    private RestTemplate restTemplate;

    @Autowired
    private UserFeignClient userFeignClient;

    //之前我们是使用 RestTemplate 调用,需要拼接字符串
//    @GetMapping("/user/{id}")
//    public User findById(@PathVariable Long id){
//        return this.restTemplate.getForObject("http://microservice-provider-user/"+id,User.class);
//    }


    //相比于 RestTemplate ,Feign 明显地更加简洁
    @GetMapping("/user/{id}")
    public User findById_feign(@PathVariable Long id){
        return this.userFeignClient.findById(id);
    }
}

启动类上添加注解

@EnableFeignClients

这样我们就可以使用 Feign 来调用微服务的 API ,取代使用拼接方式访问的 RestTemplate

2、自定义 Feign 配置

创建 Feign 配置类


/**
 * !!不能在主应用程序的上下文的@Component中,即不能在启动类所在包中
 */
@Configuration
public class FeignConfiguration {

    /** 
     * 将契约改为feign原生的默认契约,这样可以使用feign自带的注解。
     * !!修改为默认契约后,启动应用时下面接口会报错,所以建议不要使用
     */
    @Bean
    public Contract feignContract(){
        return new Contract.Default();
    }
}

修改 Feign 接口如下

//使用 configuration 属性指定配置类
@FeignClient(name = "microservice-provider-user",configuration = FeignConfiguration.class)
@Service
public interface UserFeignClient {

    /**
     * 经过测试,如果启用上面的默认契约,这里在启动应用时会报错
     * 下面两种方式都是可以的,RequestLine 是 Feign 的自带注解
     */
//    @RequestLine("GET/{id}")
//    User findById(@Param("id") Long id);

    @GetMapping("{id}")
    User findById(@PathVariable("id") Long id);
}

类似地可以自定义 Feign 的编码器、解码器等(这些未实验)。例如调用需要 HTTP Basic 认证的接口,配置类 FeignConfiguration 中加入:

//过滤器 Http Basic 认证
@Bean
public BasicAuthorizationInterceptor basicAuthorizationInterceptor(){
    return new BasicAuthorizationInterceptor("user","password");
}
3、自建 Feign

在某些场景下,自定义的 Feign 满足不了需求,此时可用 Feign Builder API 手动创建 Feign

首先,在 客户端 微服务上建立 Spring Security 配置

导入 Spring Security 依赖

compile( 'org.springframework.boot:spring-boot-starter-security')

创建配置类(可以全部copy)

@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 CustomUserDetailsService userDetailService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception{
        auth.userDetailsService(this.userDetailService).passwordEncoder(this.passwordEncoder());
    }

    @Component
    class CustomUserDetailsService implements UserDetailsService{
        /**
         * 模拟两个账户
         * ① 账号 user,密码123,角色是user-role
         * ② 账号 admin,密码123,角色是admin-role
         */
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            if("user".equals(username)) {
                return new SecurityUser("user", "123", "user-role");
            }
            else if("admin".equals(username)) {
                return new SecurityUser("admin", "123", "admin-role");
            }
            else
                return null;
        }
    }
    class SecurityUser implements UserDetails {
        private static final long serialVersionUID = 1L;

        public SecurityUser(String username, String password, String role) {
            super();
            this.username = username;
            this.password = password;
            this.role = role;
        }

        public SecurityUser() {
        }

        private Long id;
        private String username;
        private String password;
        private String role;

        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            Collection<GrantedAuthority> authorities = new ArrayList<>();
            SimpleGrantedAuthority authority = new SimpleGrantedAuthority(this.role);
            authorities.add(authority);
            return authorities;
        }
        //关键点:以下四个方法返回值返回true
        @Override
        public boolean isAccountNonExpired() {return true;}

        @Override
        public boolean isAccountNonLocked() {return true;}

        @Override
        public boolean isCredentialsNonExpired() {return true;}

        @Override
        public boolean isEnabled() { return true;}

        @Override
        public String getPassword() {
            return this.password;
        }

        @Override
        public String getUsername() {
            return this.username;
        }
    }
}

修改 Controller,打印当前登录的用户信息

@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){
            BaseController.LOGGER.info("用户:{},角色:{}",user.getUsername(),c.getAuthority());
        }
    }

    //这里从数据库获取 User,我用的是 MyBatis
    return userMapper.findById(id);
}

启动服务端和客户端测试,会弹出登录对话框

分别使用 user / 123 和 admin / 123 登录,会输出类似以下的日志:

2018-03-19 15:53:43.605  INFO 4404 --- [nio-8001-exec-3] c.i.port.controller.BaseController       : 用户:user,角色:user-role
2018-03-19 15:53:43.605  INFO 4404 --- [nio-8001-exec-3] c.i.port.controller.BaseController       : 用户:admin,角色:admin-role

用户信息是保存在了 Session 中,所以注销用户的方法就是重启浏览器

现在修改消费端微服务
  • 去掉 Feign 接口 UserFeignClient 上的 @FeignClient 注解

  • 去掉启动类上的 @EnableFeignClients 注解

修改 Controller 如下:

@Import(FeignClientsConfiguration.class)
@RestController
public class BaseController {

    private UserFeignClient userFeignClient;

    private UserFeignClient adminFeignClient;

    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
    @Autowired
    public BaseController(Decoder decoder, Encoder encoder, Client client, Contract contract){
        this.userFeignClient= Feign.builder().client(client).encoder(encoder).decoder(decoder).contract(contract)
                .requestInterceptor(new BasicAuthRequestInterceptor("user","123"))
                .target(UserFeignClient.class,"http://microservice-provider-user/");
        this.adminFeignClient= Feign.builder().client(client).encoder(encoder).decoder(decoder).contract(contract)
                .requestInterceptor(new BasicAuthRequestInterceptor("admin","123"))
                .target(UserFeignClient.class,"http://microservice-provider-user/");        
    }

    @GetMapping("/user-user/{id}")
    public User findById_user(@PathVariable Long id){
        return this.userFeignClient.findById(id);
    }

    @GetMapping("/user-admin/{id}")
    public User findById_admin(@PathVariable Long id){
        return this.userFeignClient.findById(id);
    }
}

其中,@Import 导入的是 Spring Cloud 为 Feign 默认提供的配置类。两个方法各司其职,分别登录 user 和 admin,使用同一个接口 UserFeignClient

启动服务端、客户端、消费端(端口号8010),访问http://localhost:8010/user-user/1http://localhost:8010/user-admin/1,可以看到客户端微服务打印登录信息

3、Feign 的其他支持
对继承的支持

使用继承,可以将一些公共操作分组到一些父接口中,从而简化 Feign 的开发

创建基础接口 : UserService.java

public interface UserService{
    @RequestMapping(method = RequestMethod.GET,value = "/user/{id}")
    User getUser(@PathVariable("id") long id);
}

服务提供者 Controller : UserResource.java

@RestController
public class UserResource implements UserService{
    //...
}

服务消费者 : UserClient.java

@FeignClient("user")
public interface UserClient extends UserService{
}
对压缩的支持

一些场景下,可能需要对请求后响应进行压缩,此时可使用以下的属性启用 Feign 的压缩功能

feign.compression.request.enabled=true
feign.compression.response.enabled=true

更详细的配置

feign.compression.request.enabled=true
feign.compression.request.mime-types=text/xml,application/xml,application/json
feign.compression.request.min-request-size=2048

其中
- feign.compression.request.mime-types 用于支持的媒体类型列表,默认是 text/xml、application/xml 以及 application/json
- feign.compression.request.min-request-size 用于设置请求的最小阈值,默认是2048

4、Feign 的日志

把项目回归到 第2条( 自定义 Feign 配置 ) 的状态:( 如果你拒绝,可以直接跳到下面黑体字 )
- 加上启动类注解
- 加上 Feign 接口的注解
- UserFeignClient 使用 @Autowire 自动导入

配置类 FeignConfiguration 中加入:

@Bean
public Logger.Level feignLoggerLevel(){
    //设置为输出详细信息
    return Logger.Level.FULL;
}

application.xml 中添加如下:

logging:
  level:
    # 将Feign接口的日志级别设置为DEBUG
    cn.itscloudy.consumer.feign.UserFeignClient: DEBUG

其中 cn.itscloudy.consumer.feign.UserFeignClient 是你 Feign 接口的路径

然后启动服务端、客户端以及消费端,测试可以发现 Feign 请求的各种细节非常详细地记录了下来
DEBUG LOGGER
把上面方法返回值设为 Logger.Level.BASIC,再次测试,控制台只打印了请求方法、请求的 URL 等

如果,项目是自建 Feign 的状态,即第3条的状态,需要以下步骤

首先,自建 MyLogger 类继承 feing.Logger.ErrorLogger (因为经过测试,只有 ErrorLogger 才能输出信息)

public class MyLogger extends feign.Logger.ErrorLogger{
    @Override
    protected void log(String configKey, String format, Object... args) {
        //所有关键信息都在 args 里了,可以自己输出看下,然后自定义格式输出所需信息
        for(Object o:args){
            System.out.println(o.toString());
        }
//        super.log(configKey,format,args);
    }
}

构建 UserFeignClinet 时加上 .logger 指定 Logger ,加上 .logLevel 指定输出级别。

@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
@Autowired
public BaseController(Decoder decoder, Encoder encoder, Client client, Contract contract){
    this.userFeignClient= Feign.builder().client(client).encoder(encoder).decoder(decoder).contract(contract)
            .logger(new MyLogger())
            .logLevel(feign.Logger.Level.FULL)
            .requestInterceptor(new BasicAuthRequestInterceptor("user","123"))
            .target(UserFeignClient.class,"http://microservice-provider-user/");
}

这里的 Logger 使用 MyLogger,当然也可以直接使用 ErrorLogger ,但是输出格式是全红
全红输出
然后,application.yml 也还需要有所添加,添加内容上面有叙述,不再赘述。
( 如果针对这一点有更好解决方法,欢迎告知 )

5、构造 Feign 多参数请求

GET

@FeignClient(name = "microservice-provider-user")
@Service
public interface UserFeignClient {
    //方法一
//    @GetMapping("/")
//    User findUser(@RequestParam("id") Long id),@RequestParam("username") username);

    //方法二
    @GetMapping("/")
    User findUser(@RequestParm Map<String,Object> map);
}

使用方法二调用,可使用以下类似方法

public User getUser(int id,String username){
    HashMap<String,Object> map=new HashMap<>();
    map.put("id",id);
    map.put("username",username);
    return this.userFeignClient.findUser(map);
}

POST

@FeignClient(name = "microservice-provider-user")
@Service
public interface UserFeignClient {
    //使用@RequestBody注解
    @PostMapping("/")
    User findUser(@RequestBody User user);
}

后记

以上代码大部分经过了我的测试

引用的内容源自《Spring Cloud与Docker微服务架构实战》周立/著

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值