Spring Cloud与Docker微服务架构实战(五)—— 使用Feign实现声明式REST调用

介绍

Feign是Netflix开发的声明式、模版化的HTTP客户端。

Feign使用简单,创建一个接口,并加上一些注解,代码就完成了。Feign支持多种注解,如自带的注解或JAX-RS注解等

为服务消费者整合Feign

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调用,还实现了客户端侧的负载均衡

自定义Feign配置

在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):

  • Decoder feignDecoder: ResponseEntityDecoder(which wraps a SpringDecoder)
  • Encoder feignEncoder: SpringEncoder
  • Logger feignLogger: Slf4jLogger
  • Contract feignContract: SpringMvcContract
  • Feign.Builder feignBuilder: HystrixFeign.Builder
  • Client feignClient: if Ribbon is enabled it is a LoadBalancerFeignClient, otherwise the default feign client is used

虽然Netflix没有为feign提供以下beans,但仍然可以从应用程序上下文中提取这些beans来创建client 客户端:

  • Logger.Level
  • Retryer
  • ErrorDecoder
  • Request.Options
  • Collection

在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无法满足需求,需要使用 Feign Builder API手动创建Feign

  • 用户微服务的接口需要登录后才能调用,并且对于相同的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支持继承。使用继承,可以将一些公共操作分组到一些父接口中,简化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的压缩功能

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客户端指定日志记录策略,每个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,可以看到日志信息,可以相应的修改日志级别,能看到不同的日志展示情况。

  • NONE:不记录,默认值
  • BASIC:记录请求方法,URL,响应状态吗和执行时间
  • HEADERS:记录BASIC级别基础上,记录请求和响应的header
  • FULL:记录请求和响应的header,body和元数据

使用Feign构造多参数请求

1.GET请求多参数的URL

@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);
}

POST请求包含多个参数

假设服务提供者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);
}

你可能感兴趣的:(Spring,Cloud,微服务实战,Spring,Cloud,微服务,Feign,restful)