背景
項目組定位成了業務中台,主要做接口組裝和透傳。調用後端的代碼在整個項目中占了不少的比例。為了提高開發效率,借鑒了feign的思想,隻定義接口,自動生成執行個體。
在自研之前對feign做了一些分析,發現feign有以下缺點:
- feign無法支援複雜的鑒權,我們系統對接十幾個後端,每個後端都有不同的鑒權方式。有些鑒權模式feign難以實作。
- 部分接口我們需要直接透傳給前端,是以定義的接口生成的執行個體類,要支援直接釋出成服務
- 因為接口生成的執行個體直接對外釋出,是以接口上需要添加鑒權注解,被spring AOP攔截,目前feign生成的執行個體類無法從接口上繼承注解,是以本需求也無法滿足。
基于以上三點,我們自己開發一用戶端架構,使用起來與feign一緻,隻需要定義接口就行,執行個體由架構生成。
feign源碼分析
入口@EnableFeignClients
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
String[] value() default {};
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
Class<?>[] defaultConfiguration() default {};
Class<?>[] clients() default {};
}
通過@EnableFeignClients可以開啟Feign功能,然後會自動掃描帶@FeignClient注解的接口
通過源碼看到,可以配置掃描的路徑,及Feign的全局配置
@EnableFeignClients與spring中常見的@EnableXXX原理一樣,通過@Import配置Bean注冊器。當spring容器掃描到@Import注解時,會執行個體化FeignClientsRegistrar,并調用registerBeanDefinitions方法進行Bean定義的注冊。
FeignClientsRegistrar#registerBeanDefinitions
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
# 從注解中擷取全局配置
registerDefaultConfiguration(metadata, registry);
# 掃描帶@FeignClient注解的接口,并注冊Bean定義
registerFeignClients(metadata, registry);
}
FeignClientsRegistrar#registerFeignClients
代碼有删減】
public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
LinkedHashSet<BeanDefinition> candidateComponents = new LinkedHashSet<>();
Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName());
# 使用scanner掃描帶@FeignClient的注解的類或接口
ClassPathScanningCandidateComponentProvider scanner = getScanner();
scanner.setResourceLoader(this.resourceLoader);
scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class));
Set<String> basePackages = getBasePackages(metadata);
for (String basePackage : basePackages) {
candidateComponents.addAll(scanner.findCandidateComponents(basePackage));
}
for (BeanDefinition candidateComponent : candidateComponents) {
if (candidateComponent instanceof AnnotatedBeanDefinition) {
AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
# 要求@FeignClient必須是注解
Assert.isTrue(annotationMetadata.isInterface(), "@FeignClient can only be specified on an interface");
Map<String, Object> attributes = annotationMetadata
.getAnnotationAttributes(FeignClient.class.getCanonicalName());
registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
FeignClientsRegistrar#registerFeignClient
代碼有删減
可以從下面代碼看到,Feign可以從接口上面擷取HTTP調用的所有資訊,然後FeignClientFactoryBean去生成執行個體類。
這樣類似我們通過接口上的注解資訊,告訴Feign調用資訊,剩的模闆代碼由feign幫我們搞定
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 name = getName(attributes);
FeignClientFactoryBean factoryBean = new FeignClientFactoryBean();
factoryBean.setName(name);
factoryBean.setType(clazz);
# 設定BeanDefinition的instanceSupplier字段,進而告訴spring容器如何執行個體化Bean。
BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(clazz, () -> {
# factoryBean生成的執行個體是無法從接口上繼承注解資訊的
factoryBean.setUrl(getUrl(beanFactory, attributes));
factoryBean.setPath(getPath(beanFactory, attributes));
factoryBean.setDecode404(Boolean.parseBoolean(String.valueOf(attributes.get("decode404"))));
return factoryBean.getObject();
});
AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, qualifiers);
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
registerOptionsBeanDefinition(registry, contextId);
}
Feign元件改進思路
由上所述,feign隻是使用接口上的注解資訊生成HTTP用戶端,生成的用戶端執行個體方法并未從原始接口上繼承注解。是以生成的執行個體無法使用spring AOP功能。
為了解決這個問題,我使用了位元組碼工具,去生成類執行個體,并從接口上繼承原始接口的注解資訊。
然後使用AOP去攔截方法調用,然後進行HTTP請求,而不是像feign一樣,生成的執行個體直接進行HTTP調用。
這樣我不僅能夠完成HTTP調用,也能保證其他切面能夠生效。
比如,我想實作緩存功能,我直接在接口定義上面添加@cacheable注解,就能夠內建spring cache的功能。
feign無法做到這一點,feign得自己開發一套緩存功能,是以至今feign還未支援緩存功能。
public class DynamicClassCreator {
private ClassPool pool = ClassPool.getDefault();
{
// 設定classloader,預設的可能會導緻部分類無法讀取
LoaderClassPath loaderClassPath = new LoaderClassPath(DynamicClassCreator.class.getClassLoader());
pool.appendClassPath(loaderClassPath);
}
public Class<?> createClass(Class<?> interfaceClass) throws CannotCompileException, NotFoundException {
return doCreateClass(interfaceClass);
}
private Class<?> doCreateClass(Class<?> interfaceClass) throws NotFoundException, CannotCompileException {
// 生成類名
String newClassName = generateClassName(interfaceClass);
CtClass implementationCtClass = pool.makeClass(newClassName);
// 從接口上拷貝注解資訊到新類中
addClassAnnotation(implementationCtClass, interfaceClass);
// 給實作類設定接口
implementationCtClass.setInterfaces(new CtClass[] {pool.get(interfaceClass.getName())});
// 實作類需要添加的方法
List<CtMethod> methods = getInterfaceMethod(interfaceClass);
for (CtMethod method : methods) {
addMethod(implementationCtClass, method);
}
return implementationCtClass.toClass();
}
/**
* 從接口上拷貝注解資訊到類上
*/
private void addClassAnnotation(CtClass ctClass, Class<?> interfaceClass) throws NotFoundException {
CtClass interfaceCtClass = pool.get(interfaceClass.getName());
List<AttributeInfo> attributeInfos = interfaceCtClass.getClassFile().getAttributes();
attributeInfos.stream()
.filter(attributeInfo -> attributeInfo instanceof AnnotationsAttribute)
.forEach(attributeInfo -> ctClass.getClassFile().getAttributes().add(attributeInfo));
}
/**
* 使用接口類的方法建立出實作類的方法,這樣很多資訊可以直接帶過來,不用一個一個的去設定
* 注:
* 1.需要手工設定傳回參數的泛型資訊,因為泛型資訊會被擦除
* 2.注解資訊不會自動帶過去,需要手工拷貝
*/
private void addMethod(CtClass ctClass, CtMethod methodOfInterface)
throws NotFoundException, CannotCompileException {
CtMethod impl = new CtMethod(methodOfInterface, ctClass, null);
impl.getReturnType().setGenericSignature(methodOfInterface.getReturnType().getGenericSignature());
methodOfInterface.getMethodInfo()
.getAttributes()
.stream()
.filter(attributeInfo -> attributeInfo instanceof AnnotationsAttribute)
.forEach(attributeInfo -> impl.getMethodInfo().getAttributes().add(attributeInfo));
}
/**
* 擷取需要拷貝到實作中的方法,javassit擷取到的方法會包括equal,hashcode這些,需要過濾掉
*/
private List<CtMethod> getInterfaceMethod(Class<?> interfaceClass) throws NotFoundException {
Method[] jdkMethods = interfaceClass.getMethods();
CtClass interfaceCtClass = pool.get(interfaceClass.getName());
return Arrays.stream(interfaceCtClass.getMethods())
.filter(ctMethod -> Arrays.stream(jdkMethods)
.filter(jdkMethod -> jdkMethod.getName().equals(ctMethod.getName()))
.findFirst()
.isPresent())
.collect(Collectors.toList());
}
private String generateClassName(Class<?> interfaceClass) {
return interfaceClass.getName() + "Impl";
}
}