天天看点

feign源码分析背景feign源码分析Feign组件改进思路

背景

项目组定位成了业务中台,主要做接口组装和透传。调用后端的代码在整个项目中占了不少的比例。为了提高开发效率,借鉴了feign的思想,只定义接口,自动生成实例。

在自研之前对feign做了一些分析,发现feign有以下缺点:

  1. feign无法支持复杂的鉴权,我们系统对接十几个后端,每个后端都有不同的鉴权方式。有些鉴权模式feign难以实现。
  2. 部分接口我们需要直接透传给前端,因此定义的接口生成的实例类,要支持直接发布成服务
  3. 因为接口生成的实例直接对外发布,所以接口上需要添加鉴权注解,被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";
    }
}
           

继续阅读