feign 基于參數(shù)動(dòng)態(tài)指定路由主機(jī)
背景
項(xiàng)目上最近有需求:通過(guò)一個(gè)公共基礎(chǔ)實(shí)體定義一個(gè)主機(jī)地址字段 , feign
遠(yuǎn)程調(diào)用時(shí)候根據(jù)具體值動(dòng)態(tài)改變進(jìn)行調(diào)用。
官方解決方案
第一種方案
官方支持動(dòng)態(tài)指定
URI
Overriding the Request Line
If there is a need to target a request to a different host then the one supplied when the Feign client was created, or
you want to supply a target host for each request, include ajava.net.URI
parameter and Feign will use that value
as the request target.@RequestLine("POST /repos/{owner}/{repo}/issues") void createIssue(URI host, Issue issue, @Param("owner") String owner, @Param("repo") String repo);
根據(jù)文檔的相關(guān)指引 , 需要提供一個(gè) URI
參數(shù)就可以動(dòng)態(tài)指定目標(biāo)主機(jī) , 可以實(shí)現(xiàn)動(dòng)態(tài)路由放典。
URI 方式源碼分析
官方
URI
動(dòng)態(tài)改變主機(jī)源碼解析:
Contract
類是 feign
用于提取有效信息到元信息存儲(chǔ)
在 feign.Contract.BaseContract.parseAndValidateMetadata(java.lang.Class<?>, java.lang.reflect.Method)
方法解析元數(shù)據(jù)時(shí)候 , 判斷參數(shù)類型是否為 URI
類型 , 然后記錄下參數(shù)位置
if(parameterTypes[i]==URI.class){
data.urlIndex(i);
}
在 feign.ReflectiveFeign.BuildTemplateByResolvingArgs.create
方法執(zhí)行初始化 RequestTemplate
時(shí)候 , 根據(jù) urlIndex()
是否為 null
, 直接設(shè)置 feign.RequestTemplate.target
方法改變最終目標(biāo)地址
private static class BuildTemplateByResolvingArgs implements RequestTemplate.Factory {
// ...
@Override
public RequestTemplate create(Object[] argv) {
RequestTemplate mutable = RequestTemplate.from(metadata.template());
mutable.feignTarget(target);
if (metadata.urlIndex() != null) {
int urlIndex = metadata.urlIndex();
checkArgument(argv[urlIndex] != null, "URI parameter %s was null", urlIndex);
mutable.target(String.valueOf(argv[urlIndex]));
}
// ...
}
}
URI 方式優(yōu)缺點(diǎn)
優(yōu)點(diǎn):直接 , 直接傳入目標(biāo)主機(jī)地址可以直接實(shí)現(xiàn)動(dòng)態(tài)路由
缺點(diǎn):如果是普通三方調(diào)用接口形式的話 , 使用起來(lái)問(wèn)題不大;但是我們?nèi)绻俏⒎?wù)的模式 , 我們經(jīng)常會(huì)定義一個(gè) API
接口 , FeignClient
客戶端和 Controller
層同時(shí)實(shí)現(xiàn) , 如果多一個(gè) URI
參數(shù)情況下 , 需要遠(yuǎn)程調(diào)用又不需要改變路由 , 會(huì)導(dǎo)致我們需要多填寫(xiě)一個(gè)參數(shù),請(qǐng)看下面的案例:
API 接口
public interface AccountApi {
@PostMapping(value = "/accounts")
Result<AccountCreateDTO> saveAccount(@RequestBody BaseCloudReq<AccountCreateReq> req);
}
FeignClient
@FeignClient(value = "app-server-name", contextId = "AccountClient")
public interface AccountClient extends AccountApi {
}
Controller
@RequestMapping("/accounts")
public class AccountController implements AccountApi {
@PostMapping
@Override
public Result<AccountCreateDTO> saveAccount(@RequestBody BaseCloudReq<AccountCreateReq> req) {
// ...
return Result.success(accountService.saveAccount(request));
}
}
上面案例會(huì)有以下問(wèn)題:
- 我需要改變
@FeignClient
注解的value
值 , 只能通過(guò)參數(shù)URI
指定 , 需要加一個(gè) URI 參數(shù) - 如果根據(jù)上面第一點(diǎn)是微服務(wù)互相調(diào)用情況 , 我不需要?jiǎng)討B(tài)路由的話 , 這個(gè)參數(shù)只能填寫(xiě)
null
而且必須填寫(xiě)參數(shù)岂膳。
指定 Target
根據(jù) FeignClientBuilder
手工創(chuàng)建 feign
實(shí)例,直接指定 FeignClientFactoryBean
的 name
屬性 , 從而達(dá)到動(dòng)態(tài)指定 URI
@Component
public class DynamicProcessFeignBuilder {
private FeignClientBuilder feignClientBuilder;
public DynamicProcessFeignBuilder(@Autowired ApplicationContext appContext) {
this.feignClientBuilder = new FeignClientBuilder(appContext);
}
public <T> T build(String serviceId, Class<T> tClass) {
return this.feignClientBuilder.forType(tClass, serviceId).build();
}
}
上面操作如何達(dá)到動(dòng)態(tài)指定 URI , 進(jìn)行源碼分析
org.springframework.cloud.openfeign.FeignClientBuilder
是建造者模式構(gòu)造 Feign 使用的
org.springframework.cloud.openfeign.FeignClientBuilder.forType(java.lang.Class<T>, java.lang.String)
方法直接構(gòu)造
feignClientFactoryBean
在 org.springframework.cloud.openfeign.FeignClientBuilder.Builder.Builder( org.springframework.context.ApplicationContext, org.springframework.cloud.openfeign.FeignClientFactoryBean, java.lang.Class<T>, java.lang.String)
方法里面設(shè)置 feignClientFactoryBean
的 name
/ contextId
等屬性
調(diào)用方法 org.springframework.cloud.openfeign.FeignClientBuilder.Builder.build
最終在 org.springframework.cloud.openfeign.FeignClientFactoryBean.getTarget
方法中賦值 構(gòu)造最終目標(biāo) Target
類和對(duì)應(yīng) Host
地址屬性
public class FeignClientFactoryBean
implements FactoryBean<Object>, InitializingBean, ApplicationContextAware, BeanFactoryAware {
// 省略部分門源代碼
<T> T getTarget() {
FeignContext context = beanFactory != null ? beanFactory.getBean(FeignContext.class)
: applicationContext.getBean(FeignContext.class);
Feign.Builder builder = feign(context);
if (!StringUtils.hasText(url)) {
if (LOG.isInfoEnabled()) {
LOG.info("For '" + name + "' URL not provided. Will try picking an instance via load-balancing.");
}
if (!name.startsWith("http")) {
url = "http://" + name;
} else {
url = name;
}
url += cleanPath();
return (T) loadBalance(builder, context, new HardCodedTarget<>(type, name, url));
}
if (StringUtils.hasText(url) && !url.startsWith("http")) {
url = "http://" + url;
}
String url = this.url + cleanPath();
Client client = getOptional(context, Client.class);
if (client != null) {
if (client instanceof FeignBlockingLoadBalancerClient) {
// not load balancing because we have a url,
// but Spring Cloud LoadBalancer is on the classpath, so unwrap
client = ((FeignBlockingLoadBalancerClient) client).getDelegate();
}
if (client instanceof RetryableFeignBlockingLoadBalancerClient) {
// not load balancing because we have a url,
// but Spring Cloud LoadBalancer is on the classpath, so unwrap
client = ((RetryableFeignBlockingLoadBalancerClient) client).getDelegate();
}
builder.client(client);
}
applyBuildCustomizers(context, builder);
Targeter targeter = get(context, Targeter.class);
return (T) targeter.target(this, builder, context, new HardCodedTarget<>(type, name, url));
}
// 省略部分門源代碼
}
核心問(wèn)題
1.能否通過(guò)調(diào)用時(shí)候動(dòng)態(tài)解析某些實(shí)體參數(shù)進(jìn)行動(dòng)態(tài)指定主機(jī)地址
2.feign
可以在創(chuàng)建實(shí)例時(shí)候使用不同的 feign.Target
類去指定和改變最終目的主機(jī)地址 , 能否有入口動(dòng)態(tài)改變 feign.Target
從而達(dá)到動(dòng)態(tài)路由的效果
結(jié)合 Capability / Encoder / RequestInterceptor 進(jìn)行動(dòng)態(tài)主機(jī)地址路由
自己通過(guò)另一種實(shí)現(xiàn)方式 , 但是不算優(yōu)雅 , 分享一下 , Capability
接口 相當(dāng)于 我們?cè)O(shè)計(jì)模式上的裝飾者模式 , 我們可以裝飾已經(jīng)存在的 Encoder
重新提取包裝數(shù)據(jù)
實(shí)現(xiàn)思路:
- 我們需要攔截請(qǐng)求參數(shù)去自定義解析,提取對(duì)應(yīng)的主機(jī)
Host
地址,根據(jù)官方文檔,能獲取原始參數(shù)的方法一般在Encoder
或Contract
(這兩個(gè)接口的實(shí)現(xiàn)只能是一個(gè),不能使用多個(gè)粒竖,所以才考慮是使用Capability
重新裝飾), 本文是通過(guò)Encoder
重新包裝實(shí)現(xiàn) - 提取出來(lái)自定義主機(jī)
Host
地址以后,通過(guò)自定義RequestInterceptor
請(qǐng)求攔截器直接動(dòng)態(tài)指定主機(jī) Host 地址
源碼實(shí)現(xiàn)
動(dòng)態(tài)路由參數(shù)接口
import java.util.Optional;
public interface ICloudReq<C, D, ID> {
ID getServerId();
C setServerId(ID serverId);
D getData();
C setData(D data);
default C self() {
return (C) this;
}
default Optional<D> data() {
return Optional.of(this).map(ICloudReq::getData);
}
}
實(shí)現(xiàn)自定義 Encoder
import cn.hutool.core.util.StrUtil;
import com.e.cloudapi.pojo.param.req.ICloudReq;
import feign.RequestTemplate;
import feign.Target;
import feign.codec.EncodeException;
import feign.codec.Encoder;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.Type;
import java.util.Objects;
@Slf4j
public class FeignCloudReqEncoderDecorator implements Encoder {
public static final String HEADER_DYNAMIC_CLIENT_NAME = "CLOUD_DYNAMIC_CLIENT";
private final Encoder encoder;
public FeignCloudReqEncoderDecorator(Encoder encoder) {
Objects.requireNonNull(encoder);
this.encoder = encoder;
}
@Override
public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException {
log.debug("[{}] encode {}", encoder.getClass().getSimpleName(), bodyType);
// 原來(lái)的邏輯繼續(xù)走
encoder.encode(object, bodyType, template);
log.debug("[{}] encode {}", getClass().getSimpleName(), bodyType);
// 新邏輯
extractTargetUrlHeader(object, bodyType, template);
}
private void extractTargetUrlHeader(Object object, Type bodyType, RequestTemplate template) {
if (object == null) {
return;
}
if (!(object instanceof ICloudReq)) {
return;
}
// 判斷參數(shù)類型几于,如果匹配蕊苗,直接提取相應(yīng)的主機(jī)路由地址
ICloudReq<?, ?, ?> req = (ICloudReq<?, ?, ?>) object;
Object o = req.getServerId();
if (Objects.isNull(o)) {
return;
}
String serverId = o.toString();
if (StrUtil.isBlank(serverId)) {
log.warn("{} contains empty server id,not inject dynamic client name", object.getClass().getSimpleName());
return;
}
Target<?> target = template.feignTarget();
String name = target.name();
// 提取出來(lái)的參數(shù)往 RequestTemplate 請(qǐng)求頭添加
template.header(HEADER_DYNAMIC_CLIENT_NAME, serverId);
log.debug("inject dynamic client name header [{}]:[{}]->[{}]", HEADER_DYNAMIC_CLIENT_NAME, name, serverId);
}
}
實(shí)現(xiàn)自定義 RequestInterceptor
import cn.hutool.core.util.StrUtil;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import feign.Target;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import java.util.Collection;
import java.util.Map;
import static com.e.cmdb.feign.FeignCloudReqEncoderDecorator.HEADER_DYNAMIC_CLIENT_NAME;
@Slf4j
@Configuration
public class FeignCloudReqInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
Map<String, Collection<String>> headers = template.headers();
if (!headers.containsKey(HEADER_DYNAMIC_CLIENT_NAME)) {
return;
}
// 獲取請(qǐng)求頭
headers.get(HEADER_DYNAMIC_CLIENT_NAME)
.stream()
.findFirst()
.filter(StrUtil::isNotBlank)
.ifPresent(serverName -> injectClientNameHeader(template, serverName));
}
private static void injectClientNameHeader(RequestTemplate template, String serverName) {
// 提取原來(lái)的 Target 信息
Target<?> target = template.feignTarget();
String url = target.url();
if (StrUtil.isBlank(url)) {
return;
}
// 替換成新的路由地址
String targetUrl = StrUtil.replaceFirst(url, target.name(), serverName);
log.debug("Rewrite template target:{},url:[{}]->[{}]", serverName, url, targetUrl);
// 直接設(shè)置目標(biāo)路由
template.target(targetUrl);
// 移除 RequestTemplate 剛才填充的請(qǐng)求頭,因?yàn)檎?qǐng)求不需要發(fā)送
template.removeHeader(HEADER_DYNAMIC_CLIENT_NAME);
}
}
實(shí)現(xiàn)自定義 Capability
import feign.Capability;
import feign.codec.Encoder;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FeignCloudReqCapability implements Capability {
@Override
public Encoder enrich(Encoder encoder) {
// 裝飾者模式沿彭,附加功能
return new FeignCloudReqEncoderDecorator(encoder);
}
}
總結(jié)
- 可以通過(guò)參數(shù)內(nèi)容動(dòng)態(tài)改變主機(jī)路由地址
- 暫時(shí)沒(méi)發(fā)現(xiàn)其他的入口可以做目標(biāo)路由的替換朽砰,只能以這一種方式實(shí)現(xiàn),在原有基礎(chǔ)上不要做太大的改動(dòng)就可以實(shí)現(xiàn)功能