第6章 使用Feign实现声明式REST调用

8 篇文章 0 订阅
6 篇文章 0 订阅

第六章 使用Feign实现声明式REST调用

  • 在之前的示例中是使用RestTemplate实现RESTAPI调用的,代码如下。

    • public User findById(Long id) {
      	return this.restTemplate.getForObject("http://microservice-provider-user"/+id,User.class);
      }
      
  • 上面的方式使用的是拼接字符串的方式构造的URL的,该URL只有一个参数,然而在现实中,URL往往有多个参数,这个时候使用上面的方式就显得很低效,难以维护,比如请求的URL如下。

    • http://localhost:8010/search?name=张三&username=李四&age=24
      
  • 对其进行字符串的拼接方式构建URL,如下:

    • public User[] fingfById(String name,String username,Integer age) {
      	Map<String,Object> paramMap = new HashMap();
          paramMap.put("name",name);
          paramMap.put("username",username);
          paranMap.put("age",age);
          return this.restTemplate.getForObject("http://microservice-provider-user/search?name={name}&username={username}&age={age}",User[].class,paramMap);
      }
      
  • 当参数越来越多,URL会变得越来越复杂,同时代码会变得难以维护。

Feign简介

  • FeignNetflix开发的声明式,模块化的HTTP客户端,Feign可更加便捷,优雅地调用HTTP API
  • Spring CloudFeign进行了增强,使Feign支持Spring MVC注解,并整合了RibbonEureka

服务消费者整合Feign

  • 创建Maven项目(基于microservice-consumer-movie二次构建)

在这里插入图片描述

  • pom中添加Feign依赖

    • <dependency>
      	<groupId>org.springframwork.cloud</groupId>
          <artifactId>spring-cloud-starter-openfeign</artifactId>
      </dependency>
      
  • 创建接口UserFeignClient

    • @FeignClient(name = "microservice-provider-user")
      public interface UserFeignClient {
          @RequestMapping("/{id}")
          public User findById(@PathVariable("id")Long id);
      }
      
    • @FeignClient注解中的name是一个任意客户端名称,用于创建Ribbon的负载均衡器,但由于在项目中使用了Eureka,所以Ribbon会把name的值(microservice-provider-user)解析成Eureka Server服务注册表中的服务。

    • 如果没有使用Eureka可以在配置中(application.yml)配置。

      • service:
        	ribbon:
        		listOfServers: 服务器列表
        
    • 或者使用URL属性指定请求的URL(URL可以是完整的URL或者是主机名)

      • @FeignClient(name = "microservice-provider-user",url = "http://localhost:8000/")
        
  • 修改MovieController,让其调用Feign接口

    • @RestController
      public class MovieController {
          @Autowired
          private UserFeignClient userFeignClient;
          @GetMapping("/user/{id}")
          public User findByUser(@PathVariable Long id) {
              return userFeignClient.findById(id);
          }
      }
      
      
  • 修改启动类添加**@EnableFeignClients **注解

    • @SpringBootApplication
      @EnableFeignClients
      public class ConsumerMovieApplication {
          @Bean
          public RestTemplate restTemplate() {
              return new RestTemplate();
          }
          public static void main(String[] args) {
              SpringApplication.run(ConsumerMovieApplication.class, args);
          }
      }
      
  • 测试

    • 启动microservice-discovery-eureka
    • 启动多个microservice-provider-user
    • 启动microservice-consumer-movie-feign
    • 多次访问http://localhost:8010/user/1。
    • 观察用户微服务控制台,发现不但实现了声明式的REST API调用,同时还实现了客户端的负载均衡。

自定义Feign配置

  • 在某些场景下,我们需要自定义Feign的配置,如配置日志级别定义拦截器等。Spring Cloud Edgware同时提供了Java代码配置属性自定义Feign的配置,两种方式是等价的

使用Java代码自定义Feign配置

  • Feign的默认配置类是FeignClientsConfiguration,该类中定义了Feign默认使用的编码器解码器,所使用的契约等。
  • Spring Cloud允许通过注解**@FeignClientconfiguration属性自定义Feign的配置,==自定义配置的优先级比FeignClientsConfiguration**要高==。
配置指定名称的Feign Client
  • Spring Cloud中,Feign默认使用的契约是SpringMvcContract,它可以使用Spring MVC的注解。
  • 创建Maven项目,让其使用Feign自带的注解进行工作(基于microservice-consumer-movie-feign二次构建)。

在这里插入图片描述

  • 创建配置类FeignConfiguration

    • /**
       * 该类为Feign的配置类,
       * 注:该类可以不写@Configuration注解,
       * 如果加了该注解,那么该类就不能放在主应用程序上下文@ComponentScan所扫描的包中了。
       */
      @Configuration
      public class FeignConfiguration {
          /**
           * 将契约改为feign原生的默认契约,这样就可以使用feign自带的注解了
           * @return 默认的feign契约
           */
          @Bean
          public Contract feignContract() {
              return new feign.Contract.Default();
          }
      }
      
    • 如果将带有**@Configuration的配置类存放在主应用程序上下文@ComponentScan所扫描的包中,那么该类中的配置的feign.Decoder**,feign.Encoder,feign.Contract等配置就会被所有的**@FeignClient**共享。

  • 修改接口UserFeignClient

    • /**
       * 使用@FeignClient的configuration属性指定配置类,
       * 同时,将findById上的Spring MVC注解修改为Feign自带的注解@RequestLine
       */
      @FeignClient(name = "microservice-provider-user",configuration = FeignConfiguration.class)
      public interface UserFeignClient {
          //"GET /{id}"之间一定要有一个空格否则会报错
          @RequestLine("GET /{id}")
          public User findById(@Param("id") Long id);
      }
      
  • 启动microservice-discovery-eureka

  • 启动microservice-provider-user

  • 启动microservice-consumer-movie-feign-customizing

  • 访问http://localhost:8010/user/1,访问成功说明已经实现Feign配置的自定义。

在这里插入图片描述

全局配置
  • 注解**@EnableFeignClients提供了defaultConfiguration**属性,用来指定默认的配置类。如

    • @EnableFeignClients(defaultConfiguration = DefaultRibbonConfig.class)
      

使用属性自定义Feign配置

  • Spring Cloud Netflix 1.4.0开始,Feign支持使用属性自定义。这种方式比Java代码配置更加方便。
配置指定名称的Feign Client
  • 对于指定名称的Feign Client(本例中Feign Client的名称为feignName),配置如下:

    • feign: 
      	client:	
      		config:	
      			feignName:	
      				#相当于Request.Options
      				connectTimeout: 5000
      				#相当于Request.Options
      				readTimeout: 5000
      				#配置Feign的日志级别,相当于代码配置方式中的Logger
      				loggerLevel: full
      				#Feign的错误解码器,相当于代码配置中的ErrorDecoder
      				errorDecoder: com.example.SimpleErrorDecoder
      				#配置重试,相当于代码配置方式中的Retryer
      				retryer: com.example.SimpleRetryer
      				#配置拦截器,相当于代码配置方式中的RequestInterceptor
      				requestInterceptors:
      					- com.example.FooRequestInterceptor
      					- com.example.BarRequestInterceptor
      				decode404: false
      
通用配置
  • 如果想配置所有的Feign Client,该如何配置。配置如下。

    • feign:	
      	client:	
      		config:
      			default:
      				connectTimeout: 5000
      				readTimeout: 5000
      				loggerLevel: basic
      
  • 注:属性配置的方式比Java代码配置的优先级更高,如果想让Java代码配置方式优先于属性,可配置属性:feign.client.default-to-properties=false

手动创建Feign

  • 某些场景下,自定义Feign已经不能满足需求,此时可以使用Feign Builder API手动创建Feign。如下场景:
    • 用户微服务的接口需要登陆后才能调用,并且对于相同的API,不同角色的用户有不同的行为。
    • 让电影微服务中的同一个Feign接口使用不同的账号登录,并调用用户微服务的接口。

修改用户微服务

  • 创建Maven项目(基于microservice-provider-user)

在这里插入图片描述

  • 添加security依赖

    •  <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
       </dependency>
      
  • 创建Spring Security配置类

    • @Configuration
      @EnableWebSecurity
      @EnableGlobalMethodSecurity(prePostEnabled = true)
      /**
       * @EnableGlobalMethodSecurity(securedEnabled=true) 开启@Secured 注解过滤权限
       * @EnableGlobalMethodSecurity(jsr250Enabled=true)开启@RolesAllowed 注解过滤权限
       * @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 userDetailsService;
      
          @Override
          protected void configure(AuthenticationManagerBuilder auth) throws Exception {
              auth.userDetailsService(this.userDetailsService).passwordEncoder(this.passwordEncoder());
          }
      
          @Component
          class CustomUserDetailsService implements UserDetailsService {
              /**
               * 模拟两个账号:
               * 账号user,密码是password1.角色是user-role
               * 账号admin,密码是password2,角色是admin-role
               * @param s
               * @return
               * @throws UsernameNotFoundException
               */
              @Override
              public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
                  if ("user".equals(s)) {
                      return new SecurityUser("user", "password1", "user-role");
                  } else if ("admin".equals(s)) {
                      return new SecurityUser("admin", "password2", "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<GrantedAuthority>();
                  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;
              }
      
              @Override
              public String getPassword() {
                  return this.password;
              }
      
              @Override
              public String getUsername() {
                  return this.username;
              }
      
      
      
              public Long getId() {
                  return id;
              }
      
              public void setId(Long id) {
                  this.id = id;
              }
      
              public void setUsername(String username) {
                  this.username = username;
              }
      
              public void setPassword(String password) {
                  this.password = password;
              }
      
              public String getRole() {
                  return role;
              }
      
              public void setRole(String role) {
                  this.role = role;
              }
          }
      }
      
    • 上面定义了两个账号,useradmin,密码分别为password1,password2,角色本别为user-roleadmin-role

  • 修改Controller,在其中打印当前登录的用户信息。

    • @RestController
      public class UserController {
          private static final Logger LOGGER = LoggerFactory.getLogger(UserController.class);
          @Autowired
          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> collection = user.getAuthorities();
                  for (GrantedAuthority c : collection) {
                      //打印当前登录用户的信息
                      UserController.LOGGER.info("当前用户为{},角色为{}",user.getUsername(),c.getAuthority());
                  }
              }else {
                  //do other things
              }
              User user = userRepository.findOne(id);
              return user;
          }
      }
      
  • 测试

    • 启动microservice-discovery-eureka
    • 启动microservice-provider-user-with-auth
    • 访问http://localhost:8000/1,弹出如下窗口

    在这里插入图片描述

    • 分别使用user/password1admin/password2登录查看控制台打印信息。

    在这里插入图片描述

    在这里插入图片描述

修改电影微服务

  • 创建Maven项目(基于microservice-consumer-movie-feign)

在这里插入图片描述

  • 去除UserFeignClient接口上的**@FeignClient**注解

  • 去除启动类的**@EnableFeignClients**注解

  • 修改MovieController

    • @Import(FeignClientsConfiguration.class)
      @RestController
      public class MovieController {
          //下面两个属性分别对应user账号,和admin账号
         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","password1"))
                     .target(UserFeignClient.class,"http://microservice-provider-user/");
             this.adminUserFeignClient = Feign.builder().client(client).encoder(encoder)
                     .decoder(decoder).contract(contract)
                     .requestInterceptor(new BasicAuthRequestInterceptor("admin","password2"))
                     .target(UserFeignClient.class,"http://microservice-provider-user/");
         }
          @GetMapping("/user-user/{id}")
          public User findByIdUser(@PathVariable Long id) {
              return this.userUserFeignClient.findById(id);
          }
          @GetMapping("/user-admin/{id}")
          public User findByIdAdmin(@PathVariable Long id) {
              return this.adminUserFeignClient.findById(id);
          }
      }
      
    • @Import导入的FeignClientsConfigurationSpring CloudFeign默认提供的配置类。

    • userUserFeignClient登录账号user,adminUserFeignClient登录账号admin,但是他们使用的是同一个接口。

  • 测试

    • 启动microservice-discovery-eureka
    • 启动microservice-provider-user-with-auth
    • 启动microservice-consumer-movie-feign-manual
    • 访问http://localhost:8010/user-user/1,控制台输出如下。

    在这里插入图片描述

    • 访问http://localhost:8010/user-amdin/1,控制台输出如下。

    在这里插入图片描述

Feign对继承的支持

  • Feign支持继承,使用继承后,可以将一些公共操作分组到一些父接口中,从而简化Feign,如下

  • 基础接口:UserService.java

    • public interface UserService {
          @RequestMapping(method = RequestMethod.GET,value = "/users/{id}")
          User getUser(@PathVariable("id") long id);
      }
      
  • 服务提供者:UserResource.java

    • @RestController
      public class UserResource implements UserService {
          //...
      }
      
  • 服务消费者:UserClient.java

    • @FeignClient("users")
      public interface UserClient extends UserService {
          
      }
      
  • 注:尽管Feign的继承可帮助我们进一步简化Feign开发,但是Spring Cloud官方,不建议在服务器端与客户端之间共享接口,因为这种方式造成了服务器端与客户端代码的紧耦合。并且,Feign本身并不使用Spring MVC的工作机制(方法参数映射不被继承)

Feign对压缩的支持

  • 在某些场景下,可能需要对请求或响应进行压缩,可使用以下属性启用Feign的压缩功能。

    • feign: 
      	compression: 
      		request: 
      			enabled: true
      		response:
              	enabled: true
      
  • 对于请求的压缩,Feign还提供了更为详细的设置,如:

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

Feign的日志

  • Feign对日志的处理非常灵活,可为每个Feign客户端指定日志记录策略,每个Feign客户端都会创建一个logger。默认情况下,logger的名称是Feign接口的完整类名,Feign的日志打印只会对DEBUG级别做出响应。
  • 可为每个Feign客户端配置各自的Logger.Level对象,告诉Feign记录那些日志。Logger.Level的值有以下选择。
    • NONE:不记录任何日志(默认值)
    • BASIC:仅记录请求方法,URL,响应状态代码以及执行时间
    • HEADERS:记录BASIC级别的基础上,记录请求和响应的header
    • FULL:记录请求和响应的headerbody元数据

编码方式设置日志级别

  • 创建Maven项目(基于microservice-consumer-movie-feign)

在这里插入图片描述

  • 编写Feign配置类

    • @Configuration
      public class FeignConfiguration {
          @Bean
          Logger.Level feignLoggerLevel() {
              return Logger.Level.FULL;
          }
      }
      
  • 修改UserFeignClient接口。

    • @FeignClient(name = "microservice-provider-user",configuration = FeignConfiguration.class)
      //使用configuration属性配置自定义日志类
      public interface UserFeignClient {
          @RequestMapping("/{id}")
          public User findById(@PathVariable("id")Long id);
      }
      
  • application.yml中添加信息

    • logging:
        level:
      #    将Feign接口的日志级别设置为DEBUG,因为Feign的Logger.Lever只对DEBUG做出响应
          com.ym.cloud.study.feign.UserFeignClient: DEBUG
      
  • 测试

    • 启动microservice-discovery-eureka

    • 启动microservice-provider-user

    • 启动microservice-consumer-movie-feign-logging

    • 访问http://localhost:8010/user/1,观察控制台可看到如下日志:

    •   2020-02-09 16:40:00.787 DEBUG 13220 --- [nio-8010-exec-3] c.ym.cloud.study.feign.UserFeignClient   : [UserFeignClient#findById] ---> GET http://microservice-provider-user/1 HTTP/1.1
        2020-02-09 16:40:00.787 DEBUG 13220 --- [nio-8010-exec-3] c.ym.cloud.study.feign.UserFeignClient   : [UserFeignClient#findById] ---> END HTTP (0-byte body)
        2020-02-09 16:40:00.787 DEBUG 13220 --- [nio-8010-exec-3] c.ym.cloud.study.feign.UserFeignClient   : [UserFeignClient#findById] <--- HTTP/1.1 200 (7ms)
        2020-02-09 16:40:00.787 DEBUG 13220 --- [nio-8010-exec-3] c.ym.cloud.study.feign.UserFeignClient   : [UserFeignClient#findById] content-type: application/json;charset=UTF-8
        2020-02-09 16:40:00.787 DEBUG 13220 --- [nio-8010-exec-3] c.ym.cloud.study.feign.UserFeignClient   : [UserFeignClient#findById] date: Sun, 09 Feb 2020 08:40:00 GMT
        2020-02-09 16:40:00.787 DEBUG 13220 --- [nio-8010-exec-3] c.ym.cloud.study.feign.UserFeignClient   : [UserFeignClient#findById] transfer-encoding: chunked
        2020-02-09 16:40:00.787 DEBUG 13220 --- [nio-8010-exec-3] c.ym.cloud.study.feign.UserFeignClient   : [UserFeignClient#findById] x-application-context: microservice-provider-user:8001
        2020-02-09 16:40:00.787 DEBUG 13220 --- [nio-8010-exec-3] c.ym.cloud.study.feign.UserFeignClient   : [UserFeignClient#findById] 
        2020-02-09 16:40:00.787 DEBUG 13220 --- [nio-8010-exec-3] c.ym.cloud.study.feign.UserFeignClient   : [UserFeignClient#findById] {"id":1,"username":"account1","name":"张三","age":20,"balance":100.00}
        2020-02-09 16:40:00.787 DEBUG 13220 --- [nio-8010-exec-3] c.ym.cloud.study.feign.UserFeignClient   : [UserFeignClient#findById] <--- END HTTP (72-byte body)
        
      

使用属性配置日志级别

  • Spring Cloud Edgware开始,可以使用配置属性直接定义Feign的日志级别,如下

    • feign:
        client:
          config:
            microservice-provider-user:
              loggerLevel: full
      logging:
        level:
          com.ym.cloud.study.feign.UserFeignClient: DEBUG
      

使用Feign构造多参数请求

创建提供者

  • 创建Maven项目(基于microservice-provider-user)

在这里插入图片描述

  • 修改UserController,此处添加get/post请求演示

    • @GetMapping("/get")
          public User getUser(User user) {
              return user;
          }
          @PostMapping("post")
          public User postUser(@RequestBody User user) {
              return user;
          }
      

创建消费者

  • 创建Maven项目(基于microservice-consumer-movie-feign)

在这里插入图片描述

  • 修改MovieController

    •  /**
           * 测试路径为http://localhost:8010/get/user?id=xxx&username=xxx
           * @param user
           * @return用户信息
           */
          @GetMapping("/get/user")
          public User getUser(User user) {
              return this.userFeignClient.get1(user.getId(),user.getUsername());
          }
          /**
           * 当请求参数比较多时,可以使用Map来构建
           * 测试路径为http://localhost:8010/get/user2?id=xxx&username=xxx
           * @param user
           * @return用户信息
           */
          @GetMapping("get/user2")
          public User getUserMap(User user) {
              Map<String,Object> userMap = new HashMap<>();
              userMap.put("id",user.getId());
              userMap.put("username",user.getUsername());
              return userFeignClient.get2(userMap);
          }
          /**
           * 测试路径为http://localhost:8010/post?id=xxx&username=xxx
           * @param user
           * @return用户信息
           */
          @GetMapping("/post")
          public User postUser(User user) {
              return userFeignClient.postUser(user);
          }
      
  • 修改UserFeignClient

    • @FeignClient(name = "microservice-provider-user")
      public interface UserFeignClient {
          @RequestMapping("/{id}")
          public User findById(@PathVariable("id")Long id);
      //下面注解的值在@FeignClient的name属性对应的服务中要能找到,否则会报错
          @GetMapping("/get")
          public User get1(@RequestParam("id") Long id,@RequestParam("username") String username);
          @GetMapping("/get")
          public User get2(@RequestParam Map<String,Object> userMap);
          @PostMapping("/post")
          public User postUser(@RequestBody User user);
      }
      

ost")
public User postUser(User user) {
return userFeignClient.postUser(user);
}
```

  • 修改UserFeignClient

    • @FeignClient(name = "microservice-provider-user")
      public interface UserFeignClient {
          @RequestMapping("/{id}")
          public User findById(@PathVariable("id")Long id);
      //下面注解的值在@FeignClient的name属性对应的服务中要能找到,否则会报错
          @GetMapping("/get")
          public User get1(@RequestParam("id") Long id,@RequestParam("username") String username);
          @GetMapping("/get")
          public User get2(@RequestParam Map<String,Object> userMap);
          @PostMapping("/post")
          public User postUser(@RequestBody User user);
      }
      
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值