天天看點

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";
    }
}
           

繼續閱讀