由于項(xiàng)目的要求,不能對(duì)所有基于Feign的進(jìn)行攔截辐益,需要對(duì)不同的Feign請(qǐng)求進(jìn)行不同的攔截,經(jīng)過(guò)資料的收集整理以及SpringCloud中對(duì)于Feign的集成的源碼的閱讀,解決了針對(duì)Feign請(qǐng)求的局部攔截
本項(xiàng)目中SpringCloud的版本是Camden.SR6版本
背景說(shuō)明
在既有的項(xiàng)目上進(jìn)行二次開(kāi)發(fā)贴妻,服務(wù)A需要請(qǐng)求服務(wù)B同時(shí)需要將服務(wù)A中請(qǐng)求的消息頭相關(guān)信息傳送給服務(wù)B,但是由于既有項(xiàng)目中的相關(guān)設(shè)計(jì)蝙斜,不支持feign請(qǐng)求的全局?jǐn)r截名惩,只能針對(duì)服務(wù)A請(qǐng)求服務(wù)B的feign請(qǐng)求進(jìn)行攔截,所以開(kāi)發(fā)了如下的方法孕荠;
這里說(shuō)明下娩鹉,之所以采用Feign是由于Feign添加支持負(fù)載均衡攻谁,這點(diǎn)尤為重要。
思路說(shuō)明
既然當(dāng)前SpringCloud的版本不支持Feign請(qǐng)求的攔截弯予,那么只能自己開(kāi)發(fā)攔截的方法來(lái)攔截Feign請(qǐng)求了戚宦,整理資料有如下兩種思路:
- Feign內(nèi)部也是使用Ribbon來(lái)完成支持負(fù)載均衡的,所以拋開(kāi)Feign,直接使用Ribbon也是可以的锈嫩;
為了擴(kuò)展方便受楼,可以采用掃描自定義注解和AOP攔截的方式,然后通過(guò)前置方法將消息頭相關(guān)內(nèi)容存儲(chǔ)到請(qǐng)求中
這個(gè)思路簡(jiǎn)單易用呼寸,而且方法都是自己開(kāi)發(fā)的艳汽,出現(xiàn)問(wèn)題,定位和修改都是很容易的对雪,但是這種方法也相當(dāng)于重新開(kāi)發(fā)了一種新的功能河狐,工作量和代碼量肯定是不小的,
- 還是使用Feign,既然SpringCloud當(dāng)前版本不支持瑟捣,那么就利用原生的Feign來(lái)自己封裝馋艺;
SpringCloud的@FeignClient
也是基于原生的Feign的基礎(chǔ)上進(jìn)行封裝的,所以我們也可以開(kāi)發(fā)新的封裝蝶柿,使之支持目前的需求丈钙,對(duì)Feign的請(qǐng)求進(jìn)行局部攔截
如果想進(jìn)行新的封裝,我們可以借鑒SPringCloud對(duì)Feign的封裝方法交汤,這里我們可以參考 FeignClient源碼深度解析這篇文章雏赦,說(shuō)的很詳細(xì),在這里感謝大佬的分享芙扎。
代碼實(shí)現(xiàn)
廢話不多說(shuō)星岗,為了讓代碼改動(dòng)量小,并且利用Feign的特性:(一個(gè)接口就可以訪問(wèn)其他的項(xiàng)目)戒洼,我們選擇第二種方法來(lái)實(shí)現(xiàn)
在這里對(duì)于SpringCloud支持Feign的封裝思路就顯得比較重要了俏橘,不過(guò)在這之前,我們可以使用原生的Feign來(lái)支持請(qǐng)求的攔截
首先是依賴的支持
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
<version>1.2.6.RELEASE</version>
</dependency>
<dependency>
<groupId>com.netflix.feign</groupId>
<artifactId>feign-ribbon</artifactId>
<version>8.18.0</version>
</dependency>
- 首先我們定義一個(gè)接口圈浇,該接口配置Feign訪問(wèn)其他項(xiàng)目的路徑
/**
* @author: amos
* @Description: 訪問(wèn)其他業(yè)務(wù)的請(qǐng)求
* @date: 2019/12/23 0023 下午 17:39
* @Version: V1.0
*/
public interface BizClient {
@RequestMapping(value = "/biz/list", method = RequestMethod.POST)
Result list(@RequestBody BizDTO dto);
}
注意:該接口上沒(méi)有添加 @FeignClient
注解寥掐,因?yàn)槟壳绊?xiàng)目是支持SpringCloud的Feign使用方式的,如果添加了注解磷蜀,就會(huì)直接走SpringCloud的Feign請(qǐng)求方式
- 原生的Feign使用方式
/**
*
* @author: amos
* @Description: 基于原生的Feign請(qǐng)求來(lái)獲取請(qǐng)求訪問(wèn)對(duì)象
* @date: 2020/2/19 0019 下午 16:09
* @Version: V1.0
*/
@Configuration
public class BasicFeignBuilderConfig {
public Client client;
private HttpMessageConverter jsonConverter;
private ObjectFactory<HttpMessageConverters> converter;
private static final String CLINET_URL = "http://APPLICATION-NAME";
/**
* 初始化client
*/
@PostConstruct
public void initClient() {
this.client = RibbonClient.create();
this.jsonConverter = new MappingJackson2HttpMessageConverter(new ObjectMapper());
this.converter = () -> new HttpMessageConverters(jsonConverter);
}
/**
* 利用Feign來(lái)獲取接口訪問(wèn)對(duì)象
*
* @param clazz
* @param <T>
* @return
*/
public <T> T feignBuilderRequestInterceptor(Class<T> clazz) {
T t = Feign.builder()
.encoder(new SpringEncoder(converter))
.client(client)
.decoder(new SpringDecoder(converter))
.contract(new SpringMvcContract())
.requestInterceptor(new FeignBasicTenantIdRequestInterceptor())
.target(clazz, CLINET_URL);
return t;
}
/**
* 將BizClient注冊(cè)到SpringContext的上下文中
*
* @return
*/
@Bean("bizClient")
public BizClient bizClient() {
return this.feignBuilderRequestInterceptor(BizClient.class);
}
}
至此使用原生的Feign結(jié)束召耘,但是一測(cè)試就報(bào)錯(cuò)
java.lang.RuntimeException: com.netflix.client.ClientException: Load balancer does not have available server for client: APPLICATION-NAME
從網(wǎng)上查詢資料,并進(jìn)行了相關(guān)的依賴和配置項(xiàng) 都沒(méi)有生效
- 閱讀源碼褐隆,了解SpringCloud支持Feign的原理
上面的辦法既然不可行污它,主要的問(wèn)題是Ribbon識(shí)別不了我們的實(shí)例名,也就是代碼中Client有問(wèn)題,但是SpringCloud的Feign卻是可以支持的衫贬,所以這里的關(guān)鍵就是SpringCloud中的Feign是怎么支持的Ribbon的,然后將他支持的方式移到我們目前代碼固惯,解決由于Ribbon造成的負(fù)載均衡的問(wèn)題就可以了梆造。
為此,我們需要閱讀SpringCloud支持Feign方面的相關(guān)的源碼缝呕,源碼的閱讀可以參考上面的博客鏈接澳窑,說(shuō)明的非常詳細(xì)斧散,下面我們主要分析下源碼中的代理工廠的代碼;
FeignClientFactoryBean
這個(gè)類就是FeignClient的代理工廠類供常,我們看下工廠類的入口getObject()
方法:
@Override
public Object getObject() throws Exception {
// 從Spring的ApplicationContext中獲取FeignContext
FeignContext context = applicationContext.getBean(FeignContext.class);
// 利用構(gòu)造器來(lái)構(gòu)造Feign的對(duì)象
Feign.Builder builder = feign(context);
.....
}
這里我們主要看下構(gòu)造器中是怎么獲取Client對(duì)象的,我們需要知道他是怎么處理支持負(fù)載均衡的鸡捐,我們追蹤到對(duì)應(yīng)的代碼:
protected <T> T getOptional(FeignContext context, Class<T> type) {
return context.getInstance(this.name, type);
}
protected <T> T loadBalance(Feign.Builder builder, FeignContext context,
HardCodedTarget<T> target) {
// 這里就是我們需要的client對(duì)象
// 通過(guò)上面的代碼我們知道 FeignContext 中獲取對(duì)應(yīng)的Bean
Client client = getOptional(context, Client.class);
if (client != null) {
builder.client(client);
Targeter targeter = get(context, Targeter.class);
return targeter.target(this, builder, context, target);
}
throw new IllegalStateException(
"No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-ribbon?");
}
至此栈暇,我們知道了Client對(duì)象主要來(lái)源于FeignContext中,而FeignContext是來(lái)源于ApplicationContext中箍镜,到這里就非常請(qǐng)求了源祈,我們需要從ApplicationContext中獲取FeignContext,然后再?gòu)腇eignContext中獲取Client對(duì)象色迂。
所以我們需要改造上面 BasicFeignBuilderConfig
的代碼:
/**
*
* @author: amos
* @Description: 基于原生的Feign請(qǐng)求來(lái)獲取請(qǐng)求訪問(wèn)對(duì)象
* @date: 2020/2/19 0019 下午 16:09
* @Version: V1.0
*/
@Configuration
public class BasicFeignBuilderConfig implements ApplicationContextAware{
public Client client;
private HttpMessageConverter jsonConverter;
private ObjectFactory<HttpMessageConverters> converter;
private static final String CLINET_URL = "http://APPLICATION-NAME";
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
/**
* 初始化client
*/
@PostConstruct
public void initClient() {
this.client = FeignContext context = applicationContext.getBean(FeignContext.class);
this.jsonConverter = new MappingJackson2HttpMessageConverter(new ObjectMapper());
this.converter = () -> new HttpMessageConverters(jsonConverter);
}
/**
* 利用Feign來(lái)獲取接口訪問(wèn)對(duì)象
*
* @param clazz
* @param <T>
* @return
*/
public <T> T feignBuilderRequestInterceptor(Class<T> clazz) {
T t = Feign.builder()
.encoder(new SpringEncoder(converter))
.client(client)
.decoder(new SpringDecoder(converter))
.contract(new SpringMvcContract())
.requestInterceptor(new FeignBasicTenantIdRequestInterceptor())
.target(clazz, CLINET_URL);
return t;
}
/**
* 將BizClient注冊(cè)到SpringContext的上下文中
*
* @return
*/
@Bean("bizClient")
public BizClient bizClient() {
return this.feignBuilderRequestInterceptor(BizClient.class);
}
}
上面的代碼主要是 類實(shí)現(xiàn)ApplicationContextAware
接口來(lái)獲取 ApplicationContext
對(duì)象香缺,然后從ApplicationContext
對(duì)象中獲取FeignContext
對(duì)象,再獲取到我們需要的Client
對(duì)象即可歇僧;
至此已經(jīng)完全結(jié)束了图张,我們可以在業(yè)務(wù)代碼中直接注入 BizClient
直接調(diào)用對(duì)應(yīng)得方法了。
@Autowired
BizClient bizClient
public Result list(BizDTO dto){
return bizClient.list(dto);
}
上面的代碼還可以再進(jìn)行封裝诈悍,如果有多個(gè)BizClient的業(yè)務(wù)請(qǐng)求祸轮,可以通過(guò)自定義注解來(lái)實(shí)現(xiàn)系統(tǒng)在啟動(dòng)的時(shí)候,掃描自定義的注解侥钳,然后同樣利用代理工廠的方法生成實(shí)例對(duì)象适袜,然后注入到Spring的ApplicationContext
中,方便業(yè)務(wù)直接拿來(lái)使用。
邏輯和Spring支持Feign的邏輯是一樣的舷夺,主要依賴ImportBeanDefinitionRegistrar
苦酱、ResourceLoaderAware
、 BeanClassLoaderAware
三個(gè)類给猾。
我會(huì)在下一篇博文中基于該方法來(lái)說(shuō)明疫萤,如何實(shí)現(xiàn)系統(tǒng)啟動(dòng)將自定義注解的bean注入到Spring的ApplicationContext
中