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

  • 2020-11-24
  • 本文字数:2377 字

    阅读完需:约 8 分钟

导读:对于业务系统而言,系统的架构决定了系统的可扩展性,应用的技术的决定了开发的效率,是否能够快速迭代,是决定产品是否能够占领市场的一个很重要因素。很多公司在进行技术升级时,都会或多或少遇到一些问题,本文主要讲解老项目接入 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. 向优秀者学习

关于作者

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