OpenFeign实战

2023年 7月 12日 70.8k 0

  OpenFeign 是Netflix开发的声明式、模板化的http请求客户端,作用和RestTemplate差不多,只不过OpenFeign可以更加便捷、优雅地调用http api
  OpenFeign可以将提供者提供的http接口伪装为Java接口进行消费,消费者只需使用 接口 + 注解 的方式便可直接调用提供者提供的http接口,而无需再使用RestTemplate

  • OpenFeignFeign
  • Spring Cloud Dalston 版及之前的版本使用的是 Feign,而该项目现已更新为了 OpenFeign,新版本中的依赖也发生了变化。

    
        org.springframework.cloud
        spring-cloud-starter-openfeign
    
    

    Feign本身不支持Spring MVC的注解,它有一套自己的注解;OpenFeignSpring CloudFeign的基础上支持了Spring MVC的注解,如@RequesMapping等等。 OpenFeign@FeignClient可以解析SpringMVC@RequestMapping注解下的接口, 并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。

  • OpenFeignRibbon
  • RibbonNetflix的一个开源的负载均衡项目,是一个客户端负载均衡器,运行在消费者端。OpenFeign也是运行在消费者端的,并且使用Ribbon进行负载均衡,所以OpenFeign直接内置了Ribbon,即在导入 OpenFeign依赖后,无需再导入Ribbon依赖了。

    一、OpenFeign项目搭建

    1. 提供者应用

    image.png

    • pom.xml文件
    
    
        4.0.0
        
            org.springframework.boot
            spring-boot-starter-parent
            2.3.10.RELEASE
            
        
    
        com.example
        user-provider
        0.0.1-SNAPSHOT
    
        
            8
            Hoxton.SR12
        
    
        
            
                
                    org.springframework.cloud
                    spring-cloud-dependencies
                    ${spring-cloud.version}
                    pom
                    import
                
            
        
    
        
    
            
                org.springframework.boot
                spring-boot-starter-actuator
            
    
            
                org.springframework.cloud
                spring-cloud-starter-netflix-eureka-client
            
    
            
                org.springframework.boot
                spring-boot-starter-web
            
    
            
                com.alibaba
                fastjson
                1.2.83
            
    
        
    
        
            
                
                    org.springframework.boot
                    spring-boot-maven-plugin
                
            
        
    
    
    
    • application.properties
    server.port=8081
    #
    eureka.client.service-url.defaultZone=http://admin:123@ek1.com:7901/eureka/,http://admin:123@ek2.com:7902/eureka/
    #
    # 客户端在注册中心中的名称
    eureka.instance.instance-id=user-provider-8081
    #
    # 设置当前 client 每5秒向 server 发送一次心跳,默认 30s
    eureka.instance.lease-renewal-interval-in-seconds=5
    #
    # 表示eureka server至上一次收到client的心跳之后,等待下一次心跳的超时时间,在这个时间内若没收到下一次心跳,则将移除该instance,默认为90秒
    eureka.instance.lease-expiration-duration-in-seconds=90
    #
    # 当前服务名称
    spring.application.name=user-provider
    #
    # 表示将自己的ip注册到Eureka Server上。不配置,表示将操作系统的 hostname 注册到server
    eureka.instance.prefer-ip-address=true
    #
    # eureka 服务名,默认值 unknown;如果没有配置,则取 spring.application.name
    #eureka.instance.appname=user-provider
    #
    # 实例的虚拟主机名称,默认值 unknown;如果没有配置,则取 spring.application.name
    #eureka.instance.virtual-host-name=user-provider
    #
    # 对外开放所有监控端点
    management.endpoints.web.exposure.include=*
    #
    # 是否将自己注册到其他Eureka Server,默认为true
    eureka.client.register-with-eureka=true
    #
    # 是否从eureka server获取注册信息, 需要
    eureka.client.fetch-registry=true
    
    • 实体bean
    public class User implements Serializable {
        private String idCard;
        private String username;
    
        public User() {
        }
    
        public User(String idCard, String username) {
            this.idCard = idCard;
            this.username = username;
        }
    
        public String getUsername() {
            return username;
        }
    
        public void setUsername(String username) {
            this.username = username;
        }
    
        public String getIdCard() {
            return idCard;
        }
    
        public void setIdCard(String idCard) {
            this.idCard = idCard;
        }
    
        @Override
        public String toString() {
            return "User{" +
                    "idCard='" + idCard + ''' +
                    ", username='" + username + ''' +
                    '}';
        }
    }
    
    • controller接口
    @RestController
    @RequestMapping("/user")
    public class UserController {
    
        private final static Logger log = LoggerFactory.getLogger(UserController.class);
    
        @PostMapping("/save")
        public Boolean saveUser(@RequestBody User user) {
            log.info("save user success : {}", JSON.toJSONString(user));
            return Boolean.TRUE;
        }
    
        @DeleteMapping("/del/{id}")
        public Boolean deleteUser(@PathVariable("id") Long id) {
            log.info("delete user success, user id : {}", id);
            return Boolean.TRUE;
        }
    
        @GetMapping("/list")
        public List getUserList() {
            User user = new User("110", "大宝");
            log.info("getUserList result : " + user);
            return Lists.newArrayList(user);
        }
    
        @GetMapping("/get")
        public User getUserById(@RequestParam(value = "id", required = true) Long id) {
            User user = new User("111", "大宝");
            log.info("getUserById result : {} , id : {}", user, id);
            return user;
        }
    }
    
    • 启动类
    @SpringBootApplication
    @EnableDiscoveryClient
    public class UserServiceProviderApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(UserServiceProviderApplication.class, args);
        }
    
    }
    

    2. 消费者应用

    image.png

    • pom.xml
    
    
    
       4.0.0
       
          org.springframework.boot
          spring-boot-starter-parent
          2.3.10.RELEASE
          
       
    
       com.example
       open-feign-consumer
       0.0.1-SNAPSHOT
       open-feign-consumer
       open-feign-consumer
    
       
          8
          Hoxton.SR12
       
    
       
          
             org.springframework.boot
             spring-boot-starter-web
          
    
          
             org.springframework.cloud
             spring-cloud-starter-netflix-eureka-client
          
    
          
             org.springframework.cloud
             spring-cloud-starter-openfeign
          
    
       
    
       
          
             
                org.springframework.cloud
                spring-cloud-dependencies
                ${spring-cloud.version}
                pom
                import
             
          
       
    
       
          
             
                org.springframework.boot
                spring-boot-maven-plugin
             
          
       
    
    
    
    • application.properties
    server.port=7072
    # 注册中心地址
    eureka.client.service-url.defaultZone=http://admin:123@ek1.com:7901/eureka/,http://admin:123@ek2.com:7902/eureka/
    #
    # 客户端在注册中心中的名称
    eureka.instance.instance-id=open-feign-consumer-7072
    #
    # 当前服务对外暴露的名称
    spring.application.name=open-feign-consumer
    #
    # 指定 feign 从请求到获取提供者响应的超时时间
    feign.client.config.default.read-timeout=5000
    #
    # 指定 feign 连接提供者的超时时间
    feign.client.config.default.connect-timeout=5000
    #
    # 设置当前 client 每5秒向 server 发送一次心跳,默认 30s
    eureka.instance.lease-renewal-interval-in-seconds=5
    #
    # 表示eureka server至上一次收到client的心跳之后,等待下一次心跳的超时时间,在这个时间内若没收到下一次心跳,则将移除该instance,默认为90秒
    eureka.instance.lease-expiration-duration-in-seconds=90
    #
    # 表示将自己的ip注册到Eureka Server上。不配置,表示将操作系统的 hostname 注册到 server
    eureka.instance.prefer-ip-address=true
    #
    # 是否将自己注册到其他Eureka Server
    eureka.client.register-with-eureka=true
    #
    # 是否从eureka server获取注册信息
    eureka.client.fetch-registry=true
    #
    # 表示eureka client间隔多久去拉取服务注册信息,默认为30秒,如果要迅速获取服务注册状态,可以缩小该值,比如5秒
    eureka.client.registry-fetch-interval-seconds=5
    
    • 创建实体
    public class User implements Serializable {
    
        private String idCard;
        private String username;
    
        public User() {
        }
    
        public User(String idCard, String username) {
            this.idCard = idCard;
            this.username = username;
        }
    
        public String getUsername() {
            return username;
        }
    
        public void setUsername(String username) {
            this.username = username;
        }
    
        public String getIdCard() {
            return idCard;
        }
    
        public void setIdCard(String idCard) {
            this.idCard = idCard;
        }
    
        @Override
        public String toString() {
            return "User{" +
                    "idCard='" + idCard + ''' +
                    ", username='" + username + ''' +
                    '}';
        }
    }
    
    public class ResultBody implements Serializable {
    
        private boolean status;
        // 响应码
        private String code;
        // 响应描述信息
        private String message;
        // 响应数据
        private T data;
    
        public ResultBody() {
        }
    
        private ResultBody(T data) {
            this.data = data;
        }
    
        private ResultBody(String code, String msg) {
            this.code = code;
            this.message = msg;
        }
    
        public static  ResultBody success() {
            ResultBody result = new ResultBody();
            result.setCode("1");
            result.setStatus(Boolean.TRUE);
            result.setMessage("成功");
            return result;
        }
    
        public static  ResultBody success(T data) {
            ResultBody result = new ResultBody();
            result.setCode("1");
            result.setStatus(Boolean.TRUE);
            result.setMessage("成功");
            result.setData(data);
            return result;
        }
    
        public static  ResultBody error(String code, String message) {
            ResultBody result = new ResultBody(code, message);
            result.setStatus(Boolean.FALSE);
            return result;
        }
    
        public boolean isStatus() {
            return status;
        }
    
        public void setStatus(boolean status) {
            this.status = status;
        }
    
        public String getCode() {
            return code;
        }
    
        public void setCode(String code) {
            this.code = code;
        }
    
        public String getMessage() {
            return message;
        }
    
        public void setMessage(String message) {
            this.message = message;
        }
    
        public T getData() {
            return data;
        }
    
        public void setData(T data) {
            this.data = data;
        }
    }
    
    • @FeignClient定义接口
    /**
     * 定义http请求接口
     */
    @FeignClient(value = "user-provider/user")
    public interface OpenFeignUserService {
    
        @PostMapping("/save")
        Boolean saveUser(@RequestBody User user);
    
        @DeleteMapping("/del/{id}")
        public Boolean deleteUser(@PathVariable("id") Long id);
    
        @GetMapping("/list")
        List getUserList();
    
        @GetMapping("/get")
        User getUserById(@RequestParam(value = "id", required = true) Long id);
    }
    

    注意: 如果这里你要使用@RequestMapping注解的时候,你必须说明请求方式,例如:@RequestMapping(value = "/save", method = RequestMethod.POST)

    • controller定义
    @RestController
    @RequestMapping("/open-feign")
    public class OpenFeignController {
        @Resource
        private OpenFeignUserService userService;
    
        @PostMapping("/save")
        public ResultBody saveUser(@RequestBody User user) {
            Boolean result = userService.saveUser(user);
            return ResultBody.success(result);
        }
    
        @DeleteMapping("/del/{id}")
        public ResultBody deleteUser(@PathVariable("id") Long id) {
            Boolean result = userService.deleteUser(id);
            return ResultBody.success(result);
        }
    
        @GetMapping("/list")
        public ResultBody getUserList() {
            List userList = userService.getUserList();
            return ResultBody.success(userList);
        }
    
        @GetMapping("/get")
        public ResultBody getUserById(@RequestParam(value = "id", required = true) Long id) {
            User user = userService.getUserById(id);
            return ResultBody.success(user);
        }
    }
    
    • 启动类定义, @EnableFeignClients启用 OpenFeign
    @SpringBootApplication
    @EnableDiscoveryClient
    // 开启 Feign 客户端,指定service接口所在的包
    @EnableFeignClients(basePackages = "com.example.openfeign.consumer.service")
    public class OpenFeignConsumerApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(OpenFeignConsumerApplication.class, args);
        }
    }
    
    • IDEAHttpClient 测试文件TestUser.http
    ###
    POST http://127.0.0.1:7072/open-feign/save
    Content-Type: application/json
    
    {
       "idCard":"123",
       "username":"Kate"
    }
    
    ###
    DELETE http://127.0.0.1:7072/open-feign/del/110
    
    ###
    GET http://127.0.0.1:7072/open-feign/list
    
    ###
    GET http://127.0.0.1:7072/open-feign/get?id=111
    

    二、 超时和重试

    OpenFeign默认支持RibbonRibbon的重试机制和OpenFeign的重试机制有冲突,所以源码中默认关闭了OpenFeign的重试机制,使用Ribbon重试机制。

    1. 超时

    • 服务提供者接口
    private AtomicLong atomicLong = new AtomicLong();
    
    @GetMapping("/retry")
    public User retryUser() {
    
        try {
            log.info("超时模拟 ...");
            Thread.sleep(6000);
        } catch (Exception e) {
            log.info("执行异常");
        }
    
        long i = atomicLong.getAndIncrement();
        log.info("retryUser 接口第 {} 次调用", i);
        User user = new User("111", "大宝");
        return user;
    }
    
    • 客户端设置业务超时时间
    # 业务超时时间
    ribbon.ReadTimeout=2000
    
    @FeignClient(value = "user-provider/user")
    public interface OpenFeignUserService {
        @GetMapping("/retry")
        User retryUser();
    }
    
    @RestController
    @RequestMapping("/open-feign")
    public class OpenFeignController { 
        @GetMapping("/retry")
        public ResultBody retryUser() {
            User user = userService.retryUser();
            return ResultBody.success(user);
        }
    }
    

    image.png

    1. 重试

    客户端设置重试次数

    # 同一台实例最大重试次数,不包括首次调用
    ribbon.MaxAutoRetries=3
    #
    # 重试负载均衡其他实例最大此时,不包括首次调用
    ribbon.MaxAutoRetriesNextServer=3
    #
    # 是否所有操作都重试
    ribbon.okToRetryOnAllOperations=false
    

    三、日志配置

    properties文件中配置日志级别,方便本地调试,参考官方文档:docs.spring.io/spring-clou…

    # none:不记录任何日志,默认值;
    # basic:仅记录请求方法,url,响应状态码,执行时间;
    # headers:在basic基础上,记录header信息;
    # full:记录请求和响应的header,body,元数据
    #
    feign.client.config.default.logger-level=full
    #
    # logger-level只对 debug 级别日志做出响应
    logging.level.com.example.openfeign.consumer=debug
    

    如果不想通过feign.client.config.default.logger-level的方式配置,也可通过Java代码的方式来配置

    @Configuration
    public class FeiginConfig {
    	@Bean
    	Logger.Level logLevel(){	
    		return Logger.Level.FULL;
    	}
    }
    

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论