导读:对于业务系统而言,系统的架构决定了系统的可扩展性,应用的技术的决定了开发的效率,是否能够快速迭代,是决定产品是否能够占领市场的一个很重要因素。很多公司在进行技术升级时,都会或多或少遇到一些问题,本文主要讲解老项目接入OpenFeign组件时重构的思想、遇到的问题、解决的方案,以及为什么要这样做,其他人遇到类似问题也可以做参考,需要了解OpenFeign API的同学可以去github官网。
课件系统构建初期正好是微服务概念阶段,当时还没有 Spring Boot 这套框架,我们内部自己搭建了一套前后端分离,微服务化的系统。根据开发人员的使用习惯,系统中存在两种 http 请求的方式:1. 用 Apache HttpClient 写了一个工具类封装了 GET、POST 等各种请求并覆盖了不同的使用场景, 2. 使用 Spring 自带的 RestTemplate。较原生的东西都存在一个很大的优点:扩展方便,但同时也存在一个很大的缺点:应用时会书写大量的代码,不利于后期维护。
2020 年来业务快速增长,面对层出不穷的客户需求,不单单为了目前的快速迭代,从系统可扩展性与稳定性考虑,从长远角度考虑,技术必须要做升级,引入 OpenFeign 就是计划之一。
...
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 了。
@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,所以有了下面的方案。
@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,是否可以自动注入呢?后期还要引入服务注册中心,这种方式也不太适合。所以这种看似比较完美的方案也必须放弃。
如何把 Feign interface 自动注入成 Bean?这里有两种方案:
> 1. 动态代理生成bean,直接放到BeanFactory中
> 2. 创建BeanDefinition,添加到BeanDefinitionRegistry里面,让Spring容器自己去创建Bean
若使用第一种方案,为防止项目启动时找不到依赖,需要在创建项目中 Bean 之前,把 Feign 的 Bean 注入到容器中,还是按照标准的方法出牌吧,选择了第二种。
FeignClient
,两个属性url和name就满足需求了public @interface FeignClient {
String url();
String name();
}
ClassPathScanningCandidateComponentProvider
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);
}
经过对 OpenFeign 与 Spring Framework 的一步步抛析,最终方案落地。
徐海兴,好未来软件开发工程师,专注于系统架构设计。
领取专属 10元无门槛券
私享最新 技术干货