好未来课件系统技术升级实践—引入OpenFeign

2020 年 11 月 24 日

好未来课件系统技术升级实践—引入OpenFeign

导读:对于业务系统而言,系统的架构决定了系统的可扩展性,应用的技术的决定了开发的效率,是否能够快速迭代,是决定产品是否能够占领市场的一个很重要因素。很多公司在进行技术升级时,都会或多或少遇到一些问题,本文主要讲解老项目接入OpenFeign组件时重构的思想、遇到的问题、解决的方案,以及为什么要这样做,其他人遇到类似问题也可以做参考,需要了解OpenFeign API的同学可以去github官网。

1. 背景


课件系统构建初期正好是微服务概念阶段,当时还没有 Spring Boot 这套框架,我们内部自己搭建了一套前后端分离,微服务化的系统。根据开发人员的使用习惯,系统中存在两种 http 请求的方式:1. 用 Apache HttpClient 写了一个工具类封装了 GET、POST 等各种请求并覆盖了不同的使用场景, 2. 使用 Spring 自带的 RestTemplate。较原生的东西都存在一个很大的优点:扩展方便,但同时也存在一个很大的缺点:应用时会书写大量的代码,不利于后期维护。


2020 年来业务快速增长,面对层出不穷的客户需求,不单单为了目前的快速迭代,从系统可扩展性与稳定性考虑,从长远角度考虑,技术必须要做升级,引入 OpenFeign 就是计划之一。

2. 为什么选用 OpenFeign


  1. 面向接口编程

  2. 扩展性好

  3. 支持熔段与负载均衡

  4. 支持自定义序列化与反序列化机制

  5. 可以自己集成OkHttp, Apache HttpClient等

  6. 支持各种日志组件

  7. 很方便的注入拦截器

...

  1. Spring Cloud也用的OpenFeign

3. 落地过程

3.1 开始实践


  1. 因为此项目用的是Struts2+Spring的架构,所以直接引入spring-cloud-starter-openfeign的方式行不通,同时未避免引入其它问题,也不能强上,所以要自己另找门路了。


最开始按照官方 demo 重构了一个接口,抽象出来如下:


    interface BankFeign {      @RequestLine("POST /account/{id}")      Account getAccountInfo(@Param("id") String id);    }
public class BankService { public void Service() { BankFeign bankFeign = Feign.builder().logger(BankFeign.class).logLevel(Logger.Level.FULL) .options(new Request.Options(2000, 5000)) .encoder(new JacksonEncoder()) .decoder(new JacksonDecoder()) .target(BankFeign.class, "https://api.examplebank.com"); Account account = bankFeign.getAccountInfo("1234"); } }
复制代码


这里有一个很明显的问题,每次请求其他服务都要 new 一个 Builder,这明显是框架级别统一的配置不符合系统设计的原则,当代码写完,这种写法就被 pass 了。


  1. 把Builder当作bean注入


@Bean       public Feign.Builder getFeignBuilder(){           Feign.Builder feignBuilder = Feign.builder().logger(new Slf4jLogger()).logLevel(Logger.Level.FULL)               .options(new Request.Options(2000, 5000))               .encoder(new JacksonEncoder())               .decoder(new JacksonDecoder());           return feignBuilder;       }              public void Service() {           BankFeign bankFeign = feignBuilder.target(BankFeign.class, "https://api.examplebank.com");           Account account = bankFeign.getAccountInfo("1234");       }
复制代码


看似比较完美了,但是会造成频繁生成 Feign 接口代理类对象,同时会造成 gc 频繁,所以也不可取。参考下 Spring Cloud,是以 api 接口的维度注入的 bean,所以有了下面的方案。


  1. 以接口维度注入


@Bean       public BankFeign getFeignBuilder(){           Feign.Builder feignBuilder = Feign.builder().logger(new Slf4jLogger()).logLevel(Logger.Level.FULL)               .options(new Request.Options(2000, 5000))               .encoder(new JacksonEncoder())               .decoder(new JacksonDecoder());           return feignBuilder.target(BankFeign.class, "https://api.examplebank.com");       }
复制代码


这里会产生一个疑问,在使用 BankFeign 的时候会不会有线程安全问题,通过跟踪源码,不会产生线程安全问题。


这里又产生了一个新的问题,此时 bean 是以域名的维度注入的,每写一个 Api 接口,就需要手动注入一个 Feign 客户端 Bean,是否可以自动注入呢?后期还要引入服务注册中心,这种方式也不太适合。所以这种看似比较完美的方案也必须放弃。

3.2 挑战

如何把 Feign interface 自动注入成 Bean?这里有两种方案:


> 1. 动态代理生成bean,直接放到BeanFactory中> 2. 创建BeanDefinition,添加到BeanDefinitionRegistry里面,让Spring容器自己去创建Bean
复制代码


若使用第一种方案,为防止项目启动时找不到依赖,需要在创建项目中 Bean 之前,把 Feign 的 Bean 注入到容器中,还是按照标准的方法出牌吧,选择了第二种。


  1. 自定义注解 FeignClient,两个属性url和name就满足需求了


public @interface FeignClient {          String url();          String name();      }
复制代码


  1. 重写扫描类ClassPathScanningCandidateComponentProvider


  1. 实现ImportBeanDefinitionRegistrar,注入Feign接口的BeanDefinition信息。这里会有一个问题,接口是没办法变成Definition注入到容器中的,因为接口根本不能new


解决方案:


​重写FactoryBean,把 Feign 接口的Definition注入到FactoryBean中,实际向容器中注入FactoryBean,通过getObject方法返回实际的FeignClient对象。


@Override           public Object getObject() throws Exception {               Feign.Builder builder = feign();               return builder.target(type,this.url);           }
复制代码


3.3 落地成功


经过对 OpenFeign 与 Spring Framework 的一步步抛析,最终方案落地。

4. 总结


  1. 遵循系统设计的原则

  2. 把握好Spring启动时的各个阶段

  3. 向优秀者学习


关于作者

徐海兴,好未来软件开发工程师,专注于系统架构设计。

2020 年 11 月 24 日 15:441356

评论

发布
暂无评论
发现更多内容
好未来课件系统技术升级实践—引入OpenFeign-InfoQ