component-scan标签的use-default-filters属性

SPPan 2019-03-28 Spring SpringMVC 43人已围观

1 引入问题

今天一位同事忽然转过头来问我,spring实例化的bean默认不是单例的吗?为什么我实例化后的bean,注入到controller中的时候不是之前那个了呢?于是我去看了一下他的代码,为了方便,代码只是贴出主要部分。

1.1 被注入的bean

@Component
public class ParamSupportFactory {
    private Map<String, ParamSupport> services = new HashMap<>();
    public void register(String pkey, ParamSupport service) {
        this.services.put(pkey, service);
    }
    public ParamSupport getSupportProcessor(String productKey) throws Exception {
        if (!services.containsKey(productKey)) {
            throw new Exception("unsupported product key: " + productKey);
        }
        return services.get(productKey);
    }
}

1.2 给bean中存入信息

在系统启动的时候,通过register()方法,往单例的ParamSupportFactory的services中存入信息。

@Service
public class XXXSupportServer {
    @Autowired
    private ParamSupportFactory factory;
    @PostConstruct
    public void init() {
        factory.register("xxx", this);
    }
}

1.3 从bean中取出信息使用

在controller中注入bean,通过productKey取出在系统启动的时候注入的XXXSupportServer对象。

@RestController
public class XxxController {
    @Autowired
    private ParamSupportFactory paramSupportFactory;
    @RequestMapping(value = "/{productKey}")
    public AjaxBaseModel submit(@PathVariable String productKey) throws Exception {
        XXXSupportServer processor = paramSupportFactory.getSupportProcessor(productKey);
    }
}

1.4 意外

同事的这段代码看起来是没有什么问题的,其实也确实没什么问题,但是运行起来的时候,只要访问controller,就会抛出异常unsupported product key:xxx。通过打了断点,确定系统启动了以后ParamSupportFactory.services中确实是有自己需要的实例的,为什么controller就是取不到呢。

1.5 发现问题

首先给ParamSupportFactory类写上无参构造函数,然后在无参构造和register方法中都打上断点,重启系统。

会发现启动的时候会停在无参构造函数断点处,证明确实实例化了这个bean。

然后同样会停在register方法中的断点,证明确实也向HashMap中存入了数据。

关键

访问controller,发现程序竟然神奇又停在了无参构造函数的断点上,wtf,不是说好的spring管理的bean默认是单例的吗,为什么会再次实例化了一个呢,这样的话,启动的时候存入的数据肯定不在了,以为controller肯定是使用的后面实例化的bean。

此时,第一反应,IOC父子容器的坑是不是出现了。果然去查springMVC和spring的配置文件,发现如下配置。

spring配置文件

<context:component-scan base-package="com.sogou.socm.cscd">
		<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>

springMVC配置文件

<context:component-scan base-package="com.sogou.socm.cscd.main.web">
		<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller" />
</context:component-scan>

看到这里,曾经处理过spring事物不生效的我果断看出了问题所在。springMVC配置文件正确写法应该是如下所示。

<context:component-scan base-package="com.sogou.socm.cscd.main.web" use-default-filters="false">
		<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller" />
</context:component-scan>

修改以后,重启,再看,果然一切都按照预期的运行。

那么两种配置,就差了一个use-default-filters="false",这个到底是个什么东西呢 ???于是去翻了一趟spring源码。

2 原理解释

2.1 ContextNamespaceHandler

要想弄清楚事情的原因,还得先从component-scan这个配置入手。但是怎么知道处理component-scan配置的位置在哪里呢?通过配置<context:component-scan />可以看出,改配置是属于context命名空间的,于是去查看对应的处理类ContextNamespaceHandler,发现如下代码。

public class ContextNamespaceHandler extends NamespaceHandlerSupport {
	@Override
	public void init() {
		//省略无关信息
		registerBeanDefinitionParser("component-scan", new ComponentScanBeanDefinitionParser());
		//省略无关信息
	}

}

由代码可以得知,负责component-scan的相关处理的类为org.springframework.context.annotation.ComponentScanBeanDefinitionParser

2.2 ComponentScanBeanDefinitionParser

ComponentScanBeanDefinitionParser类为BeanDefinitionParser接口的一个实现,在接口中,只有一个parse方法,由此可知,改类的入口方法就是parse方法。

@Override
@Nullable
public BeanDefinition parse(Element element, ParserContext parserContext) {
  //省略无关信息
	// Actually scan for bean definitions and register them.
	ClassPathBeanDefinitionScanner scanner = configureScanner(parserContext, element);
	//省略无关信息
	return null;
}

然后调用configureScanner方法

protected ClassPathBeanDefinitionScanner configureScanner(ParserContext parserContext, Element element) {
	//省略无关信息
	boolean useDefaultFilters = true;
	if (element.hasAttribute(USE_DEFAULT_FILTERS_ATTRIBUTE)) {
		useDefaultFilters = Boolean.valueOf(element.getAttribute(USE_DEFAULT_FILTERS_ATTRIBUTE));
	}
	// Delegate bean definition registration to scanner class.
	ClassPathBeanDefinitionScanner scanner = createScanner(parserContext.getReaderContext(), useDefaultFilters);
	//省略无关信息
}

然后调用createScanner方法创建了一个ClassPathBeanDefinitionScanner类,给类的构造方法传入了useDefaultFilters参数。

protected ClassPathBeanDefinitionScanner createScanner(XmlReaderContext readerContext, boolean useDefaultFilters) {
	return new ClassPathBeanDefinitionScanner(readerContext.getRegistry(), useDefaultFilters,
			readerContext.getEnvironment(), readerContext.getResourceLoader());
}

2.2 ClassPathBeanDefinitionScanner

ClassPathBeanDefinitionScanner的构造方法

public ClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry, boolean useDefaultFilters,
		Environment environment, @Nullable ResourceLoader resourceLoader) {

	Assert.notNull(registry, "BeanDefinitionRegistry must not be null");
	this.registry = registry;

	if (useDefaultFilters) {
		registerDefaultFilters();
	}
	setEnvironment(environment);
	setResourceLoader(resourceLoader);
}

通过上面构造函数中的代码可以看到,如果useDefaultFilters的属性为true。则执行如下代码。

protected void registerDefaultFilters() {
	this.includeFilters.add(new AnnotationTypeFilter(Component.class));
	ClassLoader cl = ClassPathScanningCandidateComponentProvider.class.getClassLoader();
	try {
		this.includeFilters.add(new AnnotationTypeFilter(
				((Class<? extends Annotation>) ClassUtils.forName("javax.annotation.ManagedBean", cl)), false));
		logger.trace("JSR-250 'javax.annotation.ManagedBean' found and supported for component scanning");
	}
	catch (ClassNotFoundException ex) {
		// JSR-250 1.1 API (as included in Java EE 6) not available - simply skip.
	}
	try {
		this.includeFilters.add(new AnnotationTypeFilter(
				((Class<? extends Annotation>) ClassUtils.forName("javax.inject.Named", cl)), false));
		logger.trace("JSR-330 'javax.inject.Named' annotation found and supported for component scanning");
	}
	catch (ClassNotFoundException ex) {
		// JSR-330 API not available - simply skip.
	}
}

可以看到默认ClassPathBeanDefinitionScanner会自动注册对@Component、@ManagedBean、@Named注解的Bean进行扫描。

在进行扫描时会通过include-filter/exclude-filter来判断你的Bean类是否是合法的,即 首先通过exclude-filter 进行黑名单过滤;然后通过include-filter 进行白名单过滤;否则默认排除。

protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
	for (TypeFilter tf : this.excludeFilters) {
		if (tf.match(metadataReader, getMetadataReaderFactory())) {
			return false;
		}
	}
	for (TypeFilter tf : this.includeFilters) {
		if (tf.match(metadataReader, getMetadataReaderFactory())) {
			return isConditionMatch(metadataReader);
		}
	}
	return false;
}

3 结论

配置<context:component-scan >中,如果use-default-filters属性不配置,默认为true,不仅仅扫描@Controller注解的Bean,而且还扫描了@Component的子注解@Service、@Reposity。所以导致了ParamSupportFactory被重新实例化了一次。

此问题还会导致的一个现象就是spring中事物不生效

吐槽(0)

上一篇:java集合:ArrayList

下一篇:设计模式概述

文章评论

    共有0条评论

文章目录