概述
背景
项目组定位成了业务中台,主要做接口组装和透传。调用后端的代码在整个项目中占了不少的比例。为了提高开发效率,借鉴了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 -> {
AttributeInfo copy = attributeInfo.copy(ctClass.getClassFile().getConstPool(), null);
ctClass.getClassFile().addAttribute(copy);
});
}
/**
* 使用接口类的方法创建出实现类的方法,这样很多信息可以直接带过来,不用一个一个的去设置
* 注:
* 1.需要手工设置返回参数的泛型信息,因为泛型信息会被擦除
* 2.注解信息不会自动带过去,需要手工拷贝
*/
private void addMethod(CtClass ctClass, CtMethod methodOfInterface)
throws NotFoundException, CannotCompileException {
CtClass returnType = methodOfInterface.getReturnType();
String methodName = methodOfInterface.getName();
CtClass[] parameters = methodOfInterface.getParameterTypes();
CtMethod impl = new CtMethod(returnType, methodName, parameters, ctClass);
methodOfInterface.getMethodInfo()
.getAttributes()
.stream()
.forEach(attributeInfo -> {
AttributeInfo copy = attributeInfo.copy(ctClass.getClassFile().getConstPool(), null);
impl.getMethodInfo().addAttribute(copy);
});
// 实现接口的abstract方法
if (CtClass.voidType.equals(returnType)) {
impl.setBody("return ;");
} else {
impl.setBody("return null;");
}
ctClass.addMethod(impl);
}
/**
* 获取需要拷贝到实现中的方法,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";
}
}
最后
以上就是愉快雪碧为你收集整理的feign源码分析背景feign源码分析Feign组件改进思路的全部内容,希望文章能够帮你解决feign源码分析背景feign源码分析Feign组件改进思路所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复