Spring 扩展点系列:BeanFactoryPostProcessor扩展点实战应用

3,009 阅读9分钟

前言

   Spring扩展点较多,本文主要从BeanFactoryPostProcessor作用、解析及是实例化入手,通过源码解读分析自定义BeanFactoryPostProcessor加载流程。同时在Spring单测模块自定义BeanFactoryPostProcessor,屏蔽Dubbo的ServiceBean、定时任务Job及阿里云消息,加速Spring test容器启动流程。后续有关其他扩展点解析会陆续更新。

BeanFactoryPostProcessor扩展点解析

   有关自定义BeanFactoryPostProcessor解析,主要包括BeanDefinition生成和后置处理器方法回调解析这两块。

1.解析生成BeanDefinition

   自定义BeanFactoryPostProcessor主要通过包扫描+@Component注解的方式引入到Spring中。包扫描主要通过ComponentScanBeanDefinitionParser#parse进行,下面分析下该方法的执行。

1.1 解析器生成

  • Spring将xml转换成Document后,遍历节点,当处理到[context:component-scan: null]时,根据节点获取namespaceUri,即 www.springframework.org/schema/cont…
  • 根据namespaceUri获取NameSpaceHandler,即ContextNamespaceHandler,其实Spring自己在META-INF/spring.handlers中维护了namespaceUri,在解析Element时,已经load到内存里放到handlerMappings里了。

  • 解析时,首先调用ContextNamespaceHandler的init方法,初始化elementName对应的解析器;

1.2 包扫描生成BD

   ComponentScanBeanDefinitionParser通过获取包数据,创建一个包扫描器ClassPathBeanDefinitionScanner,然后扫描包里的类。

@Override
public BeanDefinition parse(Element element, ParserContext parserContext) {
    // 获取base-package属性,得到包的字符串,以","分割
    String basePackage = element.getAttribute(BASE_PACKAGE_ATTRIBUTE);
    basePackage = parserContext.getReaderContext().getEnvironment().resolvePlaceholders(basePackage);
    // 切割成字符串数组
    String[] basePackages = StringUtils.tokenizeToStringArray(basePackage,
    		ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
    
    // Actually scan for bean definitions and register them.
    ClassPathBeanDefinitionScanner scanner = configureScanner(parserContext, element);
    // 通过ClassPathBeanDefinitionScanner扫描包
    Set<BeanDefinitionHolder> beanDefinitions = 
    scanner.doScan(basePackages);
    registerComponents(parserContext.getReaderContext(), beanDefinitions, element);
    return null;
}

分析: ClassPathBeanDefinitionScanner并不是使用ApplicationContext中的ClassPathBeanDefinitionScanner,而是自己创建了一个ClassPathBeanDefinitionScanner。

下面简单分析下ClassPathBeanDefinitionScanner#doScan解析流程

protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
    Assert.notEmpty(basePackages, "At least one base package must be specified");
    Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<BeanDefinitionHolder>();
    // 遍历所有的包
    for (String basePackage : basePackages) {
    	// 这个方法主要是先拿出包下所有的类,然后再去匹配,看是否含有@Component注解
    	Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
    	for (BeanDefinition candidate : candidates) {
    	    // 解析scope-proxy,关于scope-proxy下文有补充
    	    ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
    	    candidate.setScope(scopeMetadata.getScopeName());
    	    // 通过beanName生成器生成beanName,先判断注解上有没有显示设置beanName,没有的话,就以类名小写为beanName
    	    String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
    	    if (candidate instanceof AbstractBeanDefinition) {
    		如果这个类是AbstractBeanDefinition类型,则为他设置默认值,比如lazy/init/destroy,
    		通过扫描出来的bd是ScannedGenericBeanDefinition,实现了AbstractBeanDefinition
    		postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
    	    }
    	    if (candidate instanceof AnnotatedBeanDefinition) {
    	        // 把常用注解设置到AnnotationBeanDefinition中
    		AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
    	    }
    	    if (checkCandidate(beanName, candidate)) {
    	        BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
    		definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
    		beanDefinitions.add(definitionHolder);
    		//beanDefinition注册到了DefaultListableBeanFactory中
    		registerBeanDefinition(definitionHolder, this.registry);
    	    }
    	}
    }
    return beanDefinitions;
}

补充: scoped-proxy: scope代理,有三个值选项,no(默认值),interfaces(接口代理),targetClass(类代理),那什么时候需要用到scope代理呢,举个例子,我们知道Bean的作用域scope有singleton,prototype,request,session,那有这么一种情况,当你把一个session或者request的Bean注入到singleton的Bean中时,因为singleton的Bean在容器启动时就会创建A,而session的Bean在用户访问时才会创建B,那么当A中要注入B时,有可能B还未创建,这个时候就会出问题,那么代理的时候来了,B如果是个接口,就用interfaces代理,是个类则用targetClass代理。这个例子出处:www.bubuko.com/infodetail-…

下面简单分析findCandidateComponents方法

public Set<BeanDefinition> findCandidateComponents(String basePackage) {
		Set<BeanDefinition> candidates = new LinkedHashSet<BeanDefinition>();
		try {
			String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
					resolveBasePackage(basePackage) + "/" + this.resourcePattern;
			Resource[] resources = this.resourcePatternResolver.getResources(packageSearchPath);
			boolean traceEnabled = logger.isTraceEnabled();
			boolean debugEnabled = logger.isDebugEnabled();
			for (Resource resource : resources) {
				if (traceEnabled) {
					logger.trace("Scanning " + resource);
				}
				if (resource.isReadable()) {
					try {
						MetadataReader metadataReader = this.metadataReaderFactory.getMetadataReader(resource);
						if (isCandidateComponent(metadataReader)) {
							ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
							sbd.setResource(resource);
							sbd.setSource(resource);
							if (isCandidateComponent(sbd)) {
								if (debugEnabled) {
									logger.debug("Identified candidate component class: " + resource);
								}
								candidates.add(sbd);
							}
							else {
								if (debugEnabled) {
									logger.debug("Ignored because not a concrete top-level class: " + resource);
								}
							}
						}
						else {
							if (traceEnabled) {
								logger.trace("Ignored because not matching any filter: " + resource);
							}
						}
					}
					catch (Throwable ex) {
						throw new BeanDefinitionStoreException(
								"Failed to read candidate component class: " + resource, ex);
					}
				}
				else {
					if (traceEnabled) {
						logger.trace("Ignored because not readable: " + resource);
					}
				}
			}
		}
		catch (IOException ex) {
			throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex);
		}
		return candidates;
	}

   这个主要的作用是扫描包下面所有的文件,然后对每个文件进行判断,看是否存在@Component注解,如果存在,加入到candidates集合里。有个重要的一点是Spring生成的BeanDefinition默认类型都是ScannedGenericBeanDefinition(这点在应用中有使用到)。

2.自定义BeanFactoryPostProcessor回调方法分析

   自定义工厂后置器的解析流程是个非常复杂的过程,其在AbstractApplicationContext#invokeBeanFactoryPostProcessors这个方法中执行,有关这个方法的具体执行逻辑会在其他文章中着重描述,本文只大概说下具体执行代码的逻辑。

public static void invokeBeanFactoryPostProcessors(
			ConfigurableListableBeanFactory beanFactory, List<BeanFactoryPostProcessor> beanFactoryPostProcessors) {
	// Invoke BeanDefinitionRegistryPostProcessors first, if any.
    .........
	// Finally, invoke all other BeanFactoryPostProcessors.
	// 当处理完实现了PriorityOrdered和Ordered后处理我们自定义的bean工厂后置处理器
	List<BeanFactoryPostProcessor> nonOrderedPostProcessors = new ArrayList<BeanFactoryPostProcessor>();
	for (String postProcessorName : nonOrderedPostProcessorNames) {
	    nonOrderedPostProcessors.add(beanFactory.getBean(postProcessorName, BeanFactoryPostProcessor.class));
	}
	invokeBeanFactoryPostProcessors(nonOrderedPostProcessors, beanFactory);
	// Clear cached merged bean definitions since the post-processors might have
	// modified the original metadata, e.g. replacing placeholders in values...
	beanFactory.clearMetadataCache();
}
/**
 * Invoke the given BeanFactoryPostProcessor beans.
 */
private static void invokeBeanFactoryPostProcessors(
		Collection<? extends BeanFactoryPostProcessor> postProcessors, ConfigurableListableBeanFactory beanFactory) {
    for (BeanFactoryPostProcessor postProcessor : postProcessors) {
    	postProcessor.postProcessBeanFactory(beanFactory);
    }
}

三、BeanFactoryPostProcessor扩展点应用

   BeanFactoryPostProcessor可以在Bean实例化之前修改BeanDefinition,干扰Bean生命周期。基于此,作者想到在集成测试中,Spring test启动会加载很多配置项,如对外暴露的服务、定时任务及阿里云消息,这些bean的初始化过程会有较多的网络连接耗时,如果对外提供的接口或者阿里云消息过多,整个项目启动耗时会较长,试想你做一个接口的单测,要花这么长时间,肯定是烦躁无比的。因此,作者通过BeanFactoryPostProcessor这种BeanFactory后置处理器人为排除掉Dubbo ServiceBean、OpJob及OnsProducer和OnsConsumer对应的BeanDefinition,使这些bean不再生成,这样就可以大大缩减项目启动时间。同时mock掉阿里云消息发送,缩短单测时间。

1.自定义BeanFactoryPostProcessor

   在web项目中,在spring test模块自定义BeanFactoryPostProcessor实现BeanFactoryPostProcessor接口,重写postProcessBeanFactory方法,实现扩展逻辑。注意一点,这个自定义的bean工厂后置处理器要交由Spring管理。

/**
 * @author kunlun.tian
 * @since 2019-11-27 14:33 
 */
@Component
public class SpringTestFactoryBeanPostProcessor implements BeanFactoryPostProcessor {
   // 要屏蔽的class
    private static final Set<String> EXCLUDE_BEAN_CLASSES = Sets.newHashSet(
            "com.ooo.finance.vehicle.disposal.core.job.config.OpjobManagerConfig",
            "com.alibaba.dubbo.config.spring.ServiceBean",
            "com.ooo.optimus.mq.aliyunons.ONSConsumerInvoker");

   // 要屏蔽的含有某些注解的bean
   private static final Set<String> ANNOTATION_BEAN_CLASSES = Sets.newHashSet("com.xxx.opjobclient.annotation.JobHandler")
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        DefaultListableBeanFactory defaultBeanFactory = (DefaultListableBeanFactory) beanFactory;
        // 通过beanFactory获取所有BeanDefinition名称
        ArrayList<String> definitionNames = Lists.newArrayList(defaultBeanFactory.getBeanDefinitionNames());
        // 遍历所有definitionName
        for (String definitionName : definitionNames) {
            // 通过DefaultListableBeanFactory中的beanDefinitionMap获取definitionName对应的BeanDefinition
            BeanDefinition beanDefinition = defaultBeanFactory.getBeanDefinition(definitionName);
            String beanClassName = beanDefinition.getBeanClassName();
            
            if (EXCLUDE_BEAN_CLASSES.contains(beanClassName)) {
                //排除掉beanDefinition,即从维护的beanDefinitionMap和beanDefinitionNames中删除掉对应的beanDefintion和beanDefintionName。
                defaultBeanFactory.removeBeanDefinition(definitionName);
            }
            if (beanDefinition instanceof ScannedGenericBeanDefinition) {
                ScannedGenericBeanDefinition sgbd = (ScannedGenericBeanDefinition) beanDefinition;
                AnnotationMetadata metadata = sgbd.getMetadata();
                for (String annotationBeanClass : ANNOTATION_BEAN_CLASSES) {
                     //判断是否含有某些注解
                    if(metadata.hasAnnotation(annotationBeanClass)){
                     // 排除含有注解的beanDefinition
                     defaultBeanFactory.removeBeanDefinition(definitionName);
                    }
                }
            }
            
            // OpjobManagerConfig可以理解成一个配置类,其通过@Bean注解引入OpjobManager,所以将其排除。
            if ("opjobManagerConfig".equals(beanDefinition.getFactoryBeanName())) {
                defaultBeanFactory.removeBeanDefinition(definitionName);
            }
            
            // 替换ONSProducer对应的beanDefinition的class,这样也不会真正地去发消息了。
            if (ONSProducer.class.getName().equals(beanDefinition.getBeanClassName())) {
                beanDefinition.setBeanClassName(MockOnsProducer.class.getName());
            }
        }
    }
}

1.1 分析OpjobManagerConfig做了啥?

@Component
public class OpjobManagerConfig {
    @Bean(initMethod = "start", destroyMethod = "destroy")
    public OpjobManager opjobManager() {
        return new OpjobManager();
    }
}

   通过@Bean的方式引入op-job-client中的OpjobManager,生成bean时调用OpjobManager的start方法。

通过ConfigurationClassPostProcessor去解析

实例化bean时判断factoryMethodName是否为空,不为空,执行其中的method方法

1.2 如何排除加了@Bean注解的bean?

   由上图可以发现有BeanMethod生成的BD,会有一个factoryBeanName,根据factoryBeanName去排除。

自定义BeanFactoryPostProcessor回调方法

   自定义工厂后置器的解析流程是个非常复杂的过程,其在AbstractApplicationContext#invokeBeanFactoryPostProcessors这个方法中执行,有关这个方法的具体执行逻辑会在其他文章中着重描述,本文只大概说下具体执行代码的逻辑。

// OpjobManagerConfig可以理解成一个配置类,其通过@Bean注解引入OpjobManager,所以将其排除。
if ("opjobManagerConfig".equals(beanDefinition.getFactoryBeanName())) {
    defaultBeanFactory.removeBeanDefinition(definitionName);
}

注意: 在一个实体类中如果有多个@Bean注解的方法返回类型一样,则会创建多个类。

1.3排除有关定时任务的Job

   包扫描生成的ScannedGenericBeanDefinition含有元数据metada,其中含有注解信息annotationSet,判断annotationSet中是否含有@JobHandler注解,如果有,排除即可。

if (beanDefinition instanceof ScannedGenericBeanDefinition) {
    ScannedGenericBeanDefinition sgbd = (ScannedGenericBeanDefinition) beanDefinition;
    AnnotationMetadata metadata = sgbd.getMetadata();
    for (String annotationBeanClass : ANNOTATION_BEAN_CLASSES) {
        //判断是否含有某些注解
        if(metadata.hasAnnotation(annotationBeanClass)){
            // 排除含有注解的beanDefinition
            defaultBeanFactory.removeBeanDefinition(definitionName);
        }
    }
}

1.4其他

  • EXCLUDE_BEAN_CLASSES这个集合里存放需要排除的bean的className;
  • ANNOTATION_BEAN_CLASSES这个集合里存放注解的className,这样做的目的,主要是排除含有指定注解的bean;
  • 对于含有某些指定注解的bean,放到EXCLUDE_BEAN_CLASSES中去也是可以的,但每新增指定注解的bean时,要向EXCLUDE_BEAN_CLASSES里手动添加,这样有些麻烦,也不灵活;

2.自定义MockOnsProducer

   自定义ONSProducer集成ONSProducer,重写其中发送消息的方法,然后利用bean工厂后置处理器将ONSProducer对应的BeanDefinition的class替换成MockOnsProducer的class,这样在单测时就不会去真正发送消息了。

public class MockOnsProducer extends ONSProducer {
    public void send(Map<String, Object> body, String keys) {
        System.out.println("mock掉了");
    }
    public void send(Map<String, Object> body, String keys, String tags) {
        System.out.println("mock掉了");
    }
    public void send(Map<String, Object> body, String keys, Long delay) {
        System.out.println("mock掉了");
    }
    public void send(Map<String, Object> body, String keys, String tags, Long delay) {
        System.out.println("mock掉了");
    }
}

1.修改class属性

   class修改后,实例化时就会以替代后的class进行初始化,也就是MockOnsProducer,这样生成的ONSProducer不会进行相关的注册,同时重写发送消息的方法,那么单测时也不会真正发送消息。

// 替换ONSProducer对应的beanDefinition的class,这样也不会真正地去发消息了。
if (ONSProducer.class.getName().equals(beanDefinition.getBeanClassName())) {
    beanDefinition.setBeanClassName(MockOnsProducer.class.getName());
}

2.注意点

  • 通过beanDefinition.setBeanClassName方法将子类的class将父类替换掉;
  • 注意一点,MockOnsProducer这个子类不应该由Spring 管理了,否则spring不知道该用哪一个类了,在启动时会报错。

3.为什么说可以提高单测效率?

  • Dubbo ServiceBean初始时要注册服务到zk,开启netty端口;OpJobManager实例化要注册服务到zk上;同样,屏蔽掉OnsConsumer本身也能减少网络消耗。
  • 由于屏蔽些含有大量网络交互的操作可以大大减少项目启动时间,前后耗时对比如下图

总结

   bean工厂后置处理器主要作用是在bean实例化之前干扰bean的生命周期,我们利用这点去排除不需要加载的beanDefinition或者修改bean的一些属性,如class属性等;