由於項目的要求,不能對所有基於Feign的進行攔截,需要對不同的Feign請求進行不同的攔截,經過資料的收集整理以及SpringCloud中對於Feign的集成的源碼的閱讀,解決了針對Feign請求的局部攔截
本項目中SpringCloud的版本是Camden.SR6版本
背景說明
在既有的項目上進行二次開發,服務A需要請求服務B同時需要將服務A中請求的消息頭相關信息傳送給服務B,但是由於既有項目中的相關設計,不支持feign請求的全局攔截,只能針對服務A請求服務B的feign請求進行攔截,所以開發瞭如下的方法;
這裏說明下,之所以採用Feign是由於Feign添加支持負載均衡,這點尤爲重要。
思路說明
既然當前SpringCloud的版本不支持Feign請求的攔截,那麼只能自己開發攔截的方法來攔截Feign請求了,整理資料有如下兩種思路:
- Feign內部也是使用Ribbon來完成支持負載均衡的,所以拋開Feign,直接使用Ribbon也是可以的;
爲了擴展方便,可以採用掃描自定義註解和AOP攔截的方式,然後通過前置方法將消息頭相關內容存儲到請求中
這個思路簡單易用,而且方法都是自己開發的,出現問題,定位和修改都是很容易的,但是這種方法也相當於重新開發了一種新的功能,工作量和代碼量肯定是不小的,
- 還是使用Feign,既然SpringCloud當前版本不支持,那麼就利用原生的Feign來自己封裝;
SpringCloud的@FeignClient
也是基於原生的Feign的基礎上進行封裝的,所以我們也可以開發新的封裝,使之支持目前的需求,對Feign的請求進行局部攔截
如果想進行新的封裝,我們可以借鑑SPringCloud對Feign的封裝方法,這裏我們可以參考 FeignClient源碼深度解析這篇文章,說的很詳細,在這裏感謝大佬的分享。
代碼實現
廢話不多說,爲了讓代碼改動量小,並且利用Feign的特性:(一個接口就可以訪問其他的項目),我們選擇第二種方法來實現
在這裏對於SpringCloud支持Feign的封裝思路就顯得比較重要了,不過在這之前,我們可以使用原生的Feign來支持請求的攔截
首先是依賴的支持
<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>
- 首先我們定義一個接口,該接口配置Feign訪問其他項目的路徑
/**
* @author: amos
* @Description: 訪問其他業務的請求
* @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);
}
注意:該接口上沒有添加 @FeignClient
註解,因爲目前項目是支持SpringCloud的Feign使用方式的,如果添加了註解,就會直接走SpringCloud的Feign請求方式
2. 原生的Feign使用方式
/**
*
* @author: amos
* @Description: 基於原生的Feign請求來獲取請求訪問對象
* @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來獲取接口訪問對象
*
* @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註冊到SpringContext的上下文中
*
* @return
*/
@Bean("bizClient")
public BizClient bizClient() {
return this.feignBuilderRequestInterceptor(BizClient.class);
}
}
至此使用原生的Feign結束,但是一測試就報錯
java.lang.RuntimeException: com.netflix.client.ClientException: Load balancer does not have available server for client: APPLICATION-NAME
從網上查詢資料,並進行了相關的依賴和配置項 都沒有生效
- 閱讀源碼,瞭解SpringCloud支持Feign的原理
上面的辦法既然不可行,主要的問題是Ribbon識別不了我們的實例名,也就是代碼中Client有問題,但是SpringCloud的Feign卻是可以支持的,所以這裏的關鍵就是SpringCloud中的Feign是怎麼支持的Ribbon的,然後將他支持的方式移到我們目前代碼,解決由於Ribbon造成的負載均衡的問題就可以了。
爲此,我們需要閱讀SpringCloud支持Feign方面的相關的源碼,源碼的閱讀可以參考上面的博客鏈接,說明的非常詳細,下面我們主要分析下源碼中的代理工廠的代碼;
FeignClientFactoryBean
這個類就是FeignClient的代理工廠類,我們看下工廠類的入口getObject()
方法:
@Override
public Object getObject() throws Exception {
// 從Spring的ApplicationContext中獲取FeignContext
FeignContext context = applicationContext.getBean(FeignContext.class);
// 利用構造器來構造Feign的對象
Feign.Builder builder = feign(context);
.....
}
這裏我們主要看下構造器中是怎麼獲取Client對象的,我們需要知道他是怎麼處理支持負載均衡的,我們追蹤到對應的代碼:
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對象
// 通過上面的代碼我們知道 FeignContext 中獲取對應的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對象主要來源於FeignContext中,而FeignContext是來源於ApplicationContext中,到這裏就非常請求了,我們需要從ApplicationContext中獲取FeignContext,然後再從FeignContext中獲取Client對象。
所以我們需要改造上面 BasicFeignBuilderConfig
的代碼:
/**
*
* @author: amos
* @Description: 基於原生的Feign請求來獲取請求訪問對象
* @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來獲取接口訪問對象
*
* @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註冊到SpringContext的上下文中
*
* @return
*/
@Bean("bizClient")
public BizClient bizClient() {
return this.feignBuilderRequestInterceptor(BizClient.class);
}
}
上面的代碼主要是 類實現ApplicationContextAware
接口來獲取 ApplicationContext
對象,然後從ApplicationContext
對象中獲取FeignContext
對象,再獲取到我們需要的Client
對象即可;
至此已經完全結束了,我們可以在業務代碼中直接注入 BizClient
直接調用對應得方法了。
@Autowired
BizClient bizClient
public Result list(BizDTO dto){
return bizClient.list(dto);
}
上面的代碼還可以再進行封裝,如果有多個BizClient的業務請求,可以通過自定義註解來實現系統在啓動的時候,掃描自定義的註解,然後同樣利用代理工廠的方法生成實例對象,然後注入到Spring的ApplicationContext
中,方便業務直接拿來使用。
邏輯和Spring支持Feign的邏輯是一樣的,主要依賴ImportBeanDefinitionRegistrar
、ResourceLoaderAware
、 BeanClassLoaderAware
三個類。
我會在下一篇博文中基於該方法來說明,如何實現系統啓動將自定義註解的bean注入到Spring的ApplicationContext
中