歡迎查看上一篇博客:SpringCloud專題之一:Eureka 。
OpenFeign是一種聲明式的webservice客戶端調用框架。你只需要聲明接口和一些簡單的注解,就能像使用普通的Bean一樣調用遠程服務,Ribbon 和 OpenFeign 都可以實現服務調用和實現負載均衡.OpenFeign 也內置了Ribbon.
OpenFeign是在feign的基礎上做了一些修改封裝,增加了對Spring Mvc注解的支持.
OpenFiegn注解講解
一般我們會使用@GetMapping和@PostMapping兩種方式來調用Rest服務。
使用@RequestParam和@RequestBody來獲取參數
@RequestBody只能用在Post請求中,並且一個Post請求只能有一個@RequestBody,@RequestBody的參數可以包含復雜類型。
@RequestParam可以用在Post和Get請求中,但是要注意,@RequestParam的參數只能是基本類型或者是Enum,或者是List和Map(List和Map里只能是基本類型),所以@RequestParam可以和@RequestBody一起使用。
如果是Get請求,但是有時復合類型怎么辦呢?比如我們想傳遞一個User對象,User對象里面只有普通的兩個String屬性,就可以使用@SpringQueryMap。@SpringQueryMap的參數是鞥你是普通的POJO,不能是復合類型,否則解析不了,如果必須使用復合類型,那么使用@RequestBody吧。
多個FeignClient使用同一個name的問題
在eureka的客戶端添加一個類:
/**
* @className: Hello1Controller
* @description: 測試多個feign使用相同的name的問題
* @author: charon
* @create: 2021-06-06 09:35
*/
@RestController
public class Hello1Controller {
/**
* 日志記錄類
*/
private final Logger logger = LoggerFactory.getLogger(getClass());
@Value("${server.port}")
private String host;
@Value("${spring.application.name}")
private String instanceName;
@RequestMapping("/sayHello1")
public String sayHello1(@RequestParam("name") String name){
logger.info("你好,服務名:{},端口為:{},接收到的參數為:{}",instanceName,host,name);
return "你好,服務名:"+instanceName+",端口為:"+host+",接收到的參數為:"+name;
}
}
eureka消費者端的controller里添加sayHello1(String name)方法:
/**
* @className: CustomerController
* @description:
* @author: charon
* @create: 2021-05-19 22:56
*/
@RestController
public class CustomerController {
@Autowired
private CustomerSerivce serivce;
@RequestMapping("/sayHello")
public String invokeSayHello(){
return serivce.invokeSayHello();
}
@RequestMapping("/sayHello1")
public String invokeSayHello1(String name){
return serivce.invokeSayHello1(name);
}
}
接口及實現類:
/**
* @className: CustomerSerivce
* @description:
* @author: charon
* @create: 2021-05-19 22:56
*/
public interface CustomerSerivce {
String invokeSayHello();
String invokeSayHello1(String name);
}
@Service
public class CustomerServiceImpl implements CustomerSerivce {
@Autowired
private CustomerFeign feign;
@Autowired
private Customer1Feign feign1;
@Override
public String invokeSayHello() {
return feign.sayHello();
}
@Override
public String invokeSayHello1(String name) {
return feign1.sayHello1(name);
}
}
feignClient:
/**
* @className: CustomerFeign
* @description: @FeignClient使用的value參數,表示從HELLO-SERVER這個服務中調用服務
* @author: charon
* @create: 2021-05-19 23:01
*/
@FeignClient("HELLO-SERVER")
public interface CustomerFeign {
/**
* 要求:
* 返回值要對應,方法名隨意,參數值要對應
* 方法上添加SpringMVC的注解
* @return
*/
@RequestMapping("/sayHello")
String sayHello();
}
/**
* @className: Customer1Feign
* @description: 測試多個feign使用相同的name的問題
* @author: charon
* @create: 2021-06-06 09:42
*/
@FeignClient("HELLO-SERVER")
public interface Customer1Feign {
/**
* 要求:
* 必須要指定RequestParam屬性的value值,同時RequestMethod的method也需要指定
* 方法上添加SpringMVC的注解
* @return
*/
@RequestMapping(value = "/sayHello1",method = RequestMethod.GET)
String sayHello1(@RequestParam("name") String name);
}
如上圖所示,運行時候就會報錯。 原因是兩個FeignClient使用了同一個value,對於同一個service-id
只能使用一個配置類,如果有多個@FeignClient
注解使用了相同的name
屬性,則注解的configuration
參數會被覆蓋。至於誰覆蓋誰要看Spring容器初始化Bean的順序。
改動:
# 設置為true,表示后發現的bean會覆蓋之前相同名稱的bean
spring.main.allow-bean-definition-overriding=true
源碼解讀
openfeign的自動配置
@EnableFeignClients開啟openfeign
首先,我們從@EnableFeignClients這個注解開始了解。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
}
這個注解導入了一個類FeignClientsRegistrar,這個類實現了ImportBeanDefinitionRegistrar接口,該接口用於向Bean容器中注冊添加BeanDefinition。
跟進FeignClientsRegistrar的registerBeanDefinitions方法,看看它注冊了哪些東西。
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
// 注冊默認配置,會讀取@EnableFeignClients接口的屬性,如果存在自定義配置類那么就會被注冊到容器中
registerDefaultConfiguration(metadata, registry);
// 注冊FeignClient接口的Bean,會掃描所有注解了@FeignClient的接口,然后像spring本地Bean一樣地注冊到容器中。
registerFeignClients(metadata, registry);
}
下面重點看看,registerFeignClients方法,這個方法的核心邏輯就是掃描類路徑,獲取BeanDefinition,然后遍歷進行注冊。
public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
LinkedHashSet<BeanDefinition> candidateComponents = new LinkedHashSet<>();
Map<String, Object> attrs = metadata
.getAnnotationAttributes(EnableFeignClients.class.getName());
final Class<?>[] clients = attrs == null ? null
: (Class<?>[]) attrs.get("clients");
if (clients == null || clients.length == 0) {
ClassPathScanningCandidateComponentProvider scanner = getScanner();
scanner.setResourceLoader(this.resourceLoader);
scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class));
// 掃描所有路徑,默認情況下掃描啟動類下的路徑
Set<String> basePackages = getBasePackages(metadata);
for (String basePackage : basePackages) {
// 將所有 @FeignClient 的接口的BeanDefinition拿到
candidateComponents.addAll(scanner.findCandidateComponents(basePackage));
}
}
else {
for (Class<?> clazz : clients) {
candidateComponents.add(new AnnotatedGenericBeanDefinition(clazz));
}
}
// 遍歷掃描到的FeignClient的Bean
for (BeanDefinition candidateComponent : candidateComponents) {
if (candidateComponent instanceof AnnotatedBeanDefinition) {
// verify annotated class is an interface
AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
Assert.isTrue(annotationMetadata.isInterface(),
"@FeignClient can only be specified on an interface");
Map<String, Object> attributes = annotationMetadata
.getAnnotationAttributes(FeignClient.class.getCanonicalName());
String name = getClientName(attributes);
// 注冊FeignClient的配置
registerClientConfiguration(registry, name,attributes.get("configuration"));
// 注冊FeignClient
registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
下面來看看注冊FeignClient的方法:
private void registerFeignClient(BeanDefinitionRegistry registry,
AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
String className = annotationMetadata.getClassName();
Class clazz = ClassUtils.resolveClassName(className, null);
ConfigurableBeanFactory beanFactory = registry instanceof ConfigurableBeanFactory
? (ConfigurableBeanFactory) registry : null;
String contextId = getContextId(beanFactory, attributes);
String name = getName(attributes);
FeignClientFactoryBean factoryBean = new FeignClientFactoryBean();
factoryBean.setBeanFactory(beanFactory);
factoryBean.setName(name);
factoryBean.setContextId(contextId);
factoryBean.setType(clazz);
// 使用FactoryBean,將Bean的具體生成過程收攏到FeignClientFactoryBean之中
BeanDefinitionBuilder definition = BeanDefinitionBuilder
.genericBeanDefinition(clazz, () -> {
factoryBean.setUrl(getUrl(beanFactory, attributes));
factoryBean.setPath(getPath(beanFactory, attributes));
factoryBean.setDecode404(Boolean
.parseBoolean(String.valueOf(attributes.get("decode404"))));
Object fallback = attributes.get("fallback");
if (fallback != null) {
factoryBean.setFallback(fallback instanceof Class
? (Class<?>) fallback
: ClassUtils.resolveClassName(fallback.toString(), null));
}
Object fallbackFactory = attributes.get("fallbackFactory");
if (fallbackFactory != null) {
factoryBean.setFallbackFactory(fallbackFactory instanceof Class
? (Class<?>) fallbackFactory
: ClassUtils.resolveClassName(fallbackFactory.toString(),
null));
}
return factoryBean.getObject();
});
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
definition.setLazyInit(true);
validate(attributes);
AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className);
beanDefinition.setAttribute("feignClientsRegistrarFactoryBean", factoryBean);
// has a default, won't be null
boolean primary = (Boolean) attributes.get("primary");
beanDefinition.setPrimary(primary);
String[] qualifiers = getQualifiers(attributes);
if (ObjectUtils.isEmpty(qualifiers)) {
qualifiers = new String[] { contextId + "FeignClient" };
}
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, qualifiers);
// 將這個使用了 @FeignClient 的接口的工廠Bean的 BeanDefinition 注冊到Spring容器中
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}
這里值得注意的是genericBeanDefinition方法最終生成的其實是FeignClientFactoryBean,而registerBeanDefinition方法注冊進容器的也是FeignClientFactoryBean。而FeignClientFactoryBean是FactoryBean的實現類。FactoryBean接口是spring開放出來的,用於自定義Bean的生成過程。也就是說,spring將會通過調用FeignClientFactoryBean的getObject來獲取@FeignClient注解的接口對應的Bean對象。
openfeign生成並調用客戶端動態代理對象
從FeignClientFactoryBean的getObject()方法開始,看看代理對象的生成。getObject()方法調用了一個getTarget()方法,該方法做了一些預處理。獲取了一個上下文以及Feign的構造器,沒有URL的情況下拼接了一個。
@Override
public Object getObject() {
return getTarget();
}
<T> T getTarget() {
// 獲取上下文,FeignContext是在FeignAutoConfiguration被解析的時候成為Bean.
FeignContext context = beanFactory != null ? beanFactory.getBean(FeignContext.class)
: applicationContext.getBean(FeignContext.class);
// feign用於構建代理對象,builder將會構建feign
Feign.Builder builder = feign(context);
if (!StringUtils.hasText(url)) {
// 然后在沒有url 的情況下是按照服務名進行處理,拼接url 屬性為http://服務名稱。
// 如果有URL會按照URL的方式進行處理,並且如果URL沒有加http:// 會在這里加上,也就是URL可以只寫域名加端口
if (!name.startsWith("http")) {
url = "http://" + name;
}
else {
url = name;
}
url += cleanPath();
// HardCodedTarget 對象,實際上就是一個記錄的功能,記錄了接口類型,服務名稱,地址信息
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 LoadBalancerFeignClient) {
client = ((LoadBalancerFeignClient) client).getDelegate();
}
if (client instanceof FeignBlockingLoadBalancerClient) {
client = ((FeignBlockingLoadBalancerClient) client).getDelegate();
}
if (client instanceof RetryableFeignBlockingLoadBalancerClient) {
client = ((RetryableFeignBlockingLoadBalancerClient) client)
.getDelegate();
}
builder.client(client);
}
Targeter targeter = get(context, Targeter.class);
return (T) targeter.target(this, builder, context,
new HardCodedTarget<>(type, name, url));
}
用org.springframework.cloud.openfeign.FeignClientFactoryBean#loadBalance 方法:
protected <T> T loadBalance(Feign.Builder builder, FeignContext context,
HardCodedTarget<T> target) {
// 獲取執行HTTP請求的client對象
Client client = getOptional(context, Client.class);
if (client != null) {
builder.client(client);
// 獲取Target對象,默認為HystrixTargeter
Targeter targeter = get(context, Targeter.class);
// 創建代理對象
return targeter.target(this, builder, context, target);
}
}
跟進HystrixTargeter的target方法:
@Override
public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign,
FeignContext context, Target.HardCodedTarget<T> target) {
if (!(feign instanceof feign.hystrix.HystrixFeign.Builder)) {
return feign.target(target);
}
feign.hystrix.HystrixFeign.Builder builder = (feign.hystrix.HystrixFeign.Builder) feign;
String name = StringUtils.isEmpty(factory.getContextId()) ? factory.getName()
: factory.getContextId();
SetterFactory setterFactory = getOptional(name, context, SetterFactory.class);
if (setterFactory != null) {
builder.setterFactory(setterFactory);
}
Class<?> fallback = factory.getFallback();
if (fallback != void.class) {
return targetWithFallback(name, context, target, builder, fallback);
}
Class<?> fallbackFactory = factory.getFallbackFactory();
if (fallbackFactory != void.class) {
return targetWithFallbackFactory(name, context, target, builder,
fallbackFactory);
}
return feign.target(target);
}
HystrixTargeter的target方法里,最后調用了feign.target(target);方法,feign實現了構造代理對象的過程,所以這里將會回調feign的構造過程方法,在feign的target方法中,將會構造出一個Feign對象,並返回對象。
public <T> T target(Target<T> target) {
return build().newInstance(target);
}
public Feign build() {
// ...
SynchronousMethodHandler.Factory synchronousMethodHandlerFactory =
new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger,
logLevel, decode404, closeAfterDecode, propagationPolicy, forceDecoding);
ParseHandlersByName handlersByName =
new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder,
errorDecoder, synchronousMethodHandlerFactory);
return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder);
}
跟進ReflectiveFeign#newInstance方法,主要是通過JDK的動態代理構建代理對象:
public <T> T newInstance(Target<T> target) {
Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();
for (Method method : target.type().getMethods()) {
if (method.getDeclaringClass() == Object.class) {
continue;
} else if (Util.isDefault(method)) {
DefaultMethodHandler handler = new DefaultMethodHandler(method);
defaultMethodHandlers.add(handler);
methodToHandler.put(method, handler);
} else {
methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
}
}
InvocationHandler handler = factory.create(target, methodToHandler);
T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
new Class<?>[] {target.type()}, handler);
for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
defaultMethodHandler.bindTo(proxy);
}
return proxy;
}
代理對象的構建主要由3個內容組成:
- 構建Method到MethodHandler的映射關系,后面調用代理對象的時候將會根據Method找到MethodHandler,然后調用MethodHandler的invoke方法,而MethodHandler包含發起HTTP請求的實現。
- jdk動態代理需要提供InvocationHandler。而InvocationHandler將由InvocationHandlerFactory的create方法實現。
- 通過Proxy.newProxyInstance方法,生成proxy對象。
調用proxy對象發起HTTP請求
我們都知道,JDK的動態代理將會調用FeignInvocationHandler(ReflectiveFeign的靜態內部類)的invoke方法.
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// equals、toString、hashcode方法特殊處理
return dispatch.get(method).invoke(args);
}
在前面構建代理對象的時候,構建了Method到MethodHandler的映射關系.所以在這里就是根據method來獲取到MethodHandler,在調用invoke方法的.
進入到invoke方法里,MethodHandler接口的默認實現類為SynchronousMethodHandler:
@Override
public Object invoke(Object[] argv) throws Throwable {
// 根據上面創建對象過程中解析出來的RequestTemplate克隆一個RequestTemplate
RequestTemplate template = buildTemplateFromArgs.create(argv);
Options options = findOptions(argv);
Retryer retryer = this.retryer.clone();
while (true) {
try {
// executeAndDecode將會負責發起http請求
return executeAndDecode(template, options);
} catch (RetryableException e) {
try {
retryer.continueOrPropagate(e);
} catch (RetryableException th) {
// ...
}
continue;
}
}
}
Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
// 對FeignInteceptor 攔截器做處理,並將信息封裝到feign.Request 類中
Request request = targetRequest(template);
if (logLevel != Logger.Level.NONE) {
logger.logRequest(metadata.configKey(), logLevel, request);
}
Response response;
long start = System.nanoTime();
try {
// 執行HTTP請求
response = client.execute(request, options);
// ensure the request is set. TODO: remove in Feign 12
response = response.toBuilder()
.request(request)
.requestTemplate(template)
.build();
} catch (IOException e) {
if (logLevel != Logger.Level.NONE) {
logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start));
}
throw errorExecuting(request, e);
}
long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
// 解碼結果
if (decoder != null)
return decoder.decode(response, metadata.returnType());
CompletableFuture<Object> resultFuture = new CompletableFuture<>();
asyncResponseHandler.handleResponse(resultFuture, metadata.configKey(), response,
metadata.returnType(),
elapsedTime);
try {
if (!resultFuture.isDone())
throw new IllegalStateException("Response handling not done");
return resultFuture.join();
} catch (CompletionException e) {
Throwable cause = e.getCause();
if (cause != null)
throw cause;
throw e;
}
}
總結
openFeign生成@FeignClient注解的接口的代理對象是從FeignClientFactoryBean的getObject方法開始的,生成proxy對象主要由ReflectiveFeign對象來實現。動態代理方法由jdk原生的動態代理支持。
調用proxy對象,其實就是發起http請求,請求結果將被解碼並返回。
所以,正如Feign本身的意義一樣,http遠程調用被偽裝成了本地調用一樣簡單的代理對象,對於使用者來說就是調用本地接口一樣簡單
參考文章:
https://zhuanlan.zhihu.com/p/133378040