聊透Spring-2-聊透Xml配置和注解的混用以及其原理

先看看AnnotationConfigApplicationContext

上篇博客我们讲了一下ClassPathXmlApplicationContext解析Xml的总体流程,没有深入到细节。ClassPathXmlApplicationContext负责解析Xml,对应Xml的spring启动方式,我们可以这样启动spring:

ApplicationContext context = new ClassPathXmlApplicationContext(“applicationContext.xml”);

相对应的AnnotationConfigApplicationContext代表的是以纯注解的方式启动spring:

ApplicationContext context = new AnnotationConfigApplicationContext(“com.zrj”);

这里传入的参数是扫描的Root包,spring会扫描这个包下的所有Class。

###AnnotationConfigApplicationContext启动spring

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// AnnotationConfigApplicationContext.java
public class AnnotationConfigApplicationContext extends GenericApplicationContext implements AnnotationConfigRegistry {

private final AnnotatedBeanDefinitionReader reader;
private final ClassPathBeanDefinitionScanner scanner;

public AnnotationConfigApplicationContext() {
this.reader = new AnnotatedBeanDefinitionReader(this);
this.scanner = new ClassPathBeanDefinitionScanner(this);
}

public AnnotationConfigApplicationContext(String... basePackages) {
this();
scan(basePackages);
refresh();
}

// ...
}

new AnnotationConfigApplicationContext(“com.nowcoder.spring”);

这样创建的AnnotationConfigApplicationContext调用的是上面的构造函数(其他的构造函数不列举了,都差不多),这样构造出来后,创建了一个AnnotatedBeanDefinitionReader和一个ClassPathBeanDefinitionScanner,这个两个类非常的重要,AnnotationConfigApplicationContext所有的工作都交给了这两个类去完成。

看一下这个构造函数,首先调用this()初始化reader、scanner,然后调用scan,这是关键的一步,最终调用了ClassPathBeanDefinitionScanner#doScan方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
Assert.notEmpty(basePackages, "At least one base package must be specified");
Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
for (String basePackage : basePackages) {
Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
for (BeanDefinition candidate : candidates) {
ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
candidate.setScope(scopeMetadata.getScopeName());
String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
if (candidate instanceof AbstractBeanDefinition) {
postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
}
if (candidate instanceof AnnotatedBeanDefinition) {
AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
}
if (checkCandidate(beanName, candidate)) {
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
definitionHolder =
AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
beanDefinitions.add(definitionHolder);
registerBeanDefinition(definitionHolder, this.registry);
}
}
}
return beanDefinitions;
}

首先调用findCandidateComponents找到basePackage下所有的BeanDefinition,这个过程比较复杂,是通过ASM实现的,通过读取Class文件的字节码,拿到类的元信息。这里Spring可以说是一丝不苟,通过读取Class字节码的方式,让自己的扫描元信息的工作坚决不影响类的加载周期,换句话说这里完全不用对类进行任何的加载工作就能拿到类的元信息了。

找到所有的BeanDefinition后,工作就完成了一大半,Spring启动简单来说分两步走,第一步构建BeanDefinition作为Bean的候选人,第二步就是将BeanDefinition创建成一个个的Bean对象。

最后将BeanDefinition注册进BeanFactory中,等待Bean被创建。

在构造函数最后调用了refresh方法,这个方法和上篇博客中的ClassPathXmlApplicationContext里面调用的refresh是同一个方法,这里不再赘述了。

传送门:透Spring-1-ClassPathXmlApplicationContext

注意一点,AbstractRefreshableApplicationContext#refreshBeanFactory方法会调用loadBeanDefinitions,这是一个模板方法,在ClassPathXmlApplicationContext是有实现的,但是在AnnotationConfigApplicationContext是没有实现的,因为AnnotationConfigApplicationContext中BeanDefinition的扫描是交给ClassPathBeanDefinitionScanner去做的。

调用完doScan之后,调用了AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry):

1
2
3
4
5
6
7
8
9
10
11
12
13
// ClassPathBeanDefinitionScanner.java
public int scan(String... basePackages) {
int beanCountAtScanStart = this.registry.getBeanDefinitionCount();

doScan(basePackages);

// Register annotation config processors, if necessary.
if (this.includeAnnotationConfig) {
AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
}

return (this.registry.getBeanDefinitionCount() - beanCountAtScanStart);
}

这个方法用于开启对注解的解析,这部分代码后面会讲解,我们还会遇到它的。includeAnnotationConfig这个配置默认是true。

Xml和注解配置Spring混用是怎么实现的?

如果我们纯粹的使用Xml作为Spring的启动方式的话,那么全部bean的组装是有Xml文件控制的,类中定义的@Autowired、@Component等等注解是不会起作用的。

但是绝大多数情况下,我们既需要使用Xml配置Bean,同时也需要使用Annotation配置或组装Bean。这是怎么做到的呢?就是使用context扩展标签(除了spring-beans.xsd中定义的标签以外的所有标签都是扩展标签):

<context:component-scan base-package=”xxx.xxx” />

这个标签的命名空间是http://www.springframework.org/schema/context

通过使用<context:component-scan base-package=”xxx.xxx” />标签,spring会去扫描base-package指定的包下去扫描所有的Class文件,寻找潜在的Bean(被@Component标注的类),然后转换为BeanDefinition注册进BeanFactory。当使用component-scan扩展标签的时候,就默认开启了注解的解析功能,这时候,工程中所有的注解就会被spring识别并处理了(比如@Autowired、@Configuration、@Resource等)。

这里一旦使用了component-scan标签,那么不管是通过Xml标签解得来的Bean还是通过component-scan扫描注解得来Bean都会开启对Spring注解(例如@Autowired)的识别(不单单是base-package中扫描得到的Bean),因为component-scan会使得BeanFactory注册一系类解析注解的BeanPostProcessor,这些BeanPostProcessor对于不管是Xml定义的Bean还是注解配置的Bean都会生效。这部分稍后会分析代码。

这样就实现了spring中Xml和注解混用。

扩展标签

在context:component-scan中,context是命名空间,真正的标签是component-scan。

与component-scan相似的扩展标签还有很多,具体的作用不解释了,我们主要关心的是扩展标签是如何被spring处理的,部分扩展标签如下:

<context:annotation-config />

<task:scheduled-tasks />

<cache:annotation-driven mode=”aspectj”/>

<aop:aspectj-autoproxy />

每个扩展标签(这里的context、task、cache、aop就是扩展标签的命名空间,”:”后面的是扩展标签)都有各自的命名空间,例如context标签的命名空间就是http://www.springframework.org/schema/context。

那么扩展标签(或者说扩展命名空间,spring中是以命名空间为单位进行处理的而不是单个的标签)在spring中是如何被处理的呢?关键就在spring.handler配置文件中。

spring.handler配置文件

首先,我们关注一下Spring中的一个配置文件,spring.handler:

1
2
3
4
5
http\://www.springframework.org/schema/context=org.springframework.context.config.ContextNamespaceHandler
http\://www.springframework.org/schema/jee=org.springframework.ejb.config.JeeNamespaceHandler
http\://www.springframework.org/schema/lang=org.springframework.scripting.config.LangNamespaceHandler
http\://www.springframework.org/schema/task=org.springframework.scheduling.config.TaskNamespaceHandler
http\://www.springframework.org/schema/cache=org.springframework.cache.config.CacheNamespaceHandler

这是一个property文件,由几组key-value组成,现在先看一下这个配置文件中的value,value比较明显都是一个全限定类名。很容易就能找到上面我们使用的context的命名空间的处理类:ContextNamespaceHandler。我们的context命名空间就是在这个类里面被处理的了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ContextNamespaceHandler extends NamespaceHandlerSupport {

@Override
public void init() {
registerBeanDefinitionParser("property-placeholder", new PropertyPlaceholderBeanDefinitionParser());
registerBeanDefinitionParser("property-override", new PropertyOverrideBeanDefinitionParser());
registerBeanDefinitionParser("annotation-config", new AnnotationConfigBeanDefinitionParser());
registerBeanDefinitionParser("component-scan", new ComponentScanBeanDefinitionParser());
registerBeanDefinitionParser("load-time-weaver", new LoadTimeWeaverBeanDefinitionParser());
registerBeanDefinitionParser("spring-configured", new SpringConfiguredBeanDefinitionParser());
registerBeanDefinitionParser("mbean-export", new MBeanExportBeanDefinitionParser());
registerBeanDefinitionParser("mbean-server", new MBeanServerBeanDefinitionParser());
}
}

可以看到ContextNamespaceHandler可以处理context命名空间中的如下扩展标签:

  • property-placeholder
  • property-override
  • annotation-config
  • component-scan
  • load-time-weaver
  • spring-configured
  • mbean-export
  • mbean-server

每个标签又有一个专门的类去解析,这里我们用上面举过的例子component-scan,看看它的处理类:ComponentScanBeanDefinitionParser。

在哪里解析扩展标签

这里要回顾上一篇博客中的ClassPathXmlApplicationContext类了,上一节我们讲到了Xml的解析流程交给了DefaultBeanDefinitionDocumentReader去控制,这个类会遍历Xml中的每一个节点,根据节点的类型执行不同的逻辑。关键的方法在这里(类:DefaultBeanDefinitionDocumentReader.java):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// DefaultBeanDefinitionDocumentReader.java
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
if (delegate.isDefaultNamespace(root)) {
NodeList nl = root.getChildNodes();
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
if (node instanceof Element) {
Element ele = (Element) node;
if (delegate.isDefaultNamespace(ele)) {
parseDefaultElement(ele, delegate);
}
else {
delegate.parseCustomElement(ele);
}
}
}
}
else {
delegate.parseCustomElement(root);
}
}

方法遍历了root(代表Xml配置文件的根节点),然后遍历每一个子节点。关键方法是delegate.isDefaultNamespace这个方法,这个方法用来判断当前节点是否是beans命名空间(也是spring的Xml配置文件的默认命名空间),如果是,则继续对\<bean>标签等进行解析,这个步骤在上篇博客详细讲过了(传送门:透Spring-1-ClassPathXmlApplicationContext),如果不是,则是扩展标签,需要对扩展命名空间内的标签进行特殊处理,也就是调用delegate.parseCustomElement(ele)方法。

1
2
3
4
5
6
7
8
// BeanDefinitionParserDelegate.java
// 判断当前标签是否属于默认(beans)命名空间的方法

public static final String BEANS_NAMESPACE_URI = "http://www.springframework.org/schema/beans";

public boolean isDefaultNamespace(@Nullable String namespaceUri) {
return (!StringUtils.hasLength(namespaceUri) || BEANS_NAMESPACE_URI.equals(namespaceUri));
}

好了我们接着看parseCustomElement方法,同样在BeanDefinitionParserDelegate类中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// BeanDefinitionParserDelegate.java

public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) {
String namespaceUri = getNamespaceURI(ele);
if (namespaceUri == null) {
return null;
}
NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
if (handler == null) {
error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele);
return null;
}
return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
}

这里通过节点的namespaceUri找到对应处理当前节点标签的命名空间的NamespaceHandler,然后调用parse方法去解析。

嗯~我们终于看到NamespaceHandler的身影了,还记得上面的ContextNamespaceHandler吗?快接上了,咱们继续往下看。

这里getNamespaceHandlerResolver是获取的什么类呢?获取的是接口NamespaceHandlerResolver的实现类,这个接口的实现类只有一个,就是DefaultNamespaceHandlerResolver。

DefaultNamespaceHandlerResolver

1
public static final String DEFAULT_HANDLER_MAPPINGS_LOCATION = "META-INF/spring.handlers";

这是这个类上来的第一句话,是不是特别特别的亲切!(前提是你好好看了这篇博客上面的内容)

这里定义了一个字符串常量表示spring.handlers文件的位置,你是不是立马就意识到了这个类是用来干嘛的,没错,就是用来加载spring.handlers配置的,不光可以加载配置,还可以决定一个namespaceUri对应的是哪个NamespaceHandler。看看具体代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// DefaultNamespaceHandlerResolver.java
public NamespaceHandler resolve(String namespaceUri) {
Map<String, Object> handlerMappings = getHandlerMappings();
Object handlerOrClassName = handlerMappings.get(namespaceUri);
if (handlerOrClassName == null) {
return null;
}
else if (handlerOrClassName instanceof NamespaceHandler) {
return (NamespaceHandler) handlerOrClassName;
}
else {
String className = (String) handlerOrClassName;
try {
Class<?> handlerClass = ClassUtils.forName(className, this.classLoader);
if (!NamespaceHandler.class.isAssignableFrom(handlerClass)) {
throw new FatalBeanException("Class [" + className + "] for namespace [" + namespaceUri +
"] does not implement the [" + NamespaceHandler.class.getName() + "] interface");
}
NamespaceHandler namespaceHandler = (NamespaceHandler) BeanUtils.instantiateClass(handlerClass);
namespaceHandler.init();
handlerMappings.put(namespaceUri, namespaceHandler);
return namespaceHandler;
}
catch (ClassNotFoundException ex) {
throw new FatalBeanException("Could not find NamespaceHandler class [" + className +
"] for namespace [" + namespaceUri + "]", ex);
}
catch (LinkageError err) {
throw new FatalBeanException("Unresolvable class definition for NamespaceHandler class [" +
className + "] for namespace [" + namespaceUri + "]", err);
}
}
}

这里的handlerMappings最初是一个字符串对字符串的map,因为最初加载的spring.handlers还是一个property文件。最后一个else里面,使用反射区拿到对应的NamespaceHandler,并保存在了handlerMappings中。这样,就加载完了spring.handlers中的所有命名空间对应的NamespaceHandler实例了。

这里是不是有点SPI的味道~

当spring扫描Xml扫描到<context:component-scan base-package="xxx.xxx" />标签时,发现不是默认命名空间,就去查找context的命名空间,就是http://www.springframework.org/schema/context,然后通过DefaultNamespaceHandlerResolver查找这个命名空间对应的NamespaceHandler,这里就找到了ContextNamespaceHandler。

接下来的解析工作就委托给了ContextNamespaceHandler,所以我们接着看ContextNamespaceHandler。

ContextNamespaceHandler

这个类只是实现了init方法,注册进了一批标签和处理类,component-scan的处理类是ComponentScanBeanDefinitionParser,我们重点看看这个类,这个类实现了BeanDefinitionParser接口:

1
2
3
public interface BeanDefinitionParser {
BeanDefinition parse(Element element, ParserContext parserContext);
}

ComponentScanBeanDefinitionParser的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
// ComponentScanBeanDefinitionParser.java
public BeanDefinition parse(Element element, ParserContext parserContext) {
String basePackage = element.getAttribute(BASE_PACKAGE_ATTRIBUTE);
basePackage = parserContext.getReaderContext().getEnvironment().resolvePlaceholders(basePackage);
String[] basePackages = StringUtils.tokenizeToStringArray(basePackage,
ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);

ClassPathBeanDefinitionScanner scanner = configureScanner(parserContext, element);
Set<BeanDefinitionHolder> beanDefinitions = scanner.doScan(basePackages);
registerComponents(parserContext.getReaderContext(), beanDefinitions, element);

return null;
}

流程非常清晰:

  • 从Element中找到base-package属性,设置待扫描包。
  • 创建ClassPathBeanDefinitionScanner
  • 使用ClassPathBeanDefinitionScanner扫描base-package包获得BeanDefinitions
  • 将扫描到的BeanDefinitions注册进BeanFactory

这里有个熟悉的朋友:ClassPathBeanDefinitionScanner,在这篇博客的开头讲AnnotationConfigApplicationContext的时候就提到了ClassPathBeanDefinitionScanner,里面有一个重要的方法叫doScan,用来将Class文件转换为BeanDefinition。

处理注解

这里还没完,还有一个重要的功能没有看到,就是在什么地方注册的处理注解的BeanPostProcessor。

上面parse的最后调用了registerComponents方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// ComponentScanBeanDefinitionParser.java
protected void registerComponents(
XmlReaderContext readerContext, Set<BeanDefinitionHolder> beanDefinitions, Element element) {

Object source = readerContext.extractSource(element);
CompositeComponentDefinition compositeDef = new CompositeComponentDefinition(element.getTagName(), source);

for (BeanDefinitionHolder beanDefHolder : beanDefinitions) {
compositeDef.addNestedComponent(new BeanComponentDefinition(beanDefHolder));
}

// Register annotation config processors, if necessary.
boolean annotationConfig = true;
if (element.hasAttribute(ANNOTATION_CONFIG_ATTRIBUTE)) {
annotationConfig = Boolean.valueOf(element.getAttribute(ANNOTATION_CONFIG_ATTRIBUTE));
}
if (annotationConfig) {
Set<BeanDefinitionHolder> processorDefinitions = AnnotationConfigUtils.registerAnnotationConfigProcessors(readerContext.getRegistry(), source);
for (BeanDefinitionHolder processorDefinition : processorDefinitions) {
compositeDef.addNestedComponent(new BeanComponentDefinition(processorDefinition));
}
}

readerContext.fireComponentRegistered(compositeDef);
}

里面调用了

1
AnnotationConfigUtils.registerAnnotationConfigProcessors(readerContext.getRegistry(), source);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// AnnotationConfigUtils.java
public static Set<BeanDefinitionHolder> registerAnnotationConfigProcessors(
BeanDefinitionRegistry registry, @Nullable Object source) {

DefaultListableBeanFactory beanFactory = unwrapDefaultListableBeanFactory(registry);
if (beanFactory != null) {
if (!(beanFactory.getDependencyComparator() instanceof AnnotationAwareOrderComparator)) {
beanFactory.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE);
}
if (!(beanFactory.getAutowireCandidateResolver() instanceof ContextAnnotationAutowireCandidateResolver)) {
beanFactory.setAutowireCandidateResolver(new ContextAnnotationAutowireCandidateResolver());
}
}

Set<BeanDefinitionHolder> beanDefs = new LinkedHashSet<>(4);

if (!registry.containsBeanDefinition(CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME)) {
RootBeanDefinition def = new RootBeanDefinition(ConfigurationClassPostProcessor.class);
def.setSource(source);
beanDefs.add(registerPostProcessor(registry, def, CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME));
}

if (!registry.containsBeanDefinition(AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME)) {
RootBeanDefinition def = new RootBeanDefinition(AutowiredAnnotationBeanPostProcessor.class);
def.setSource(source);
beanDefs.add(registerPostProcessor(registry, def, AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME));
}

// Check for JSR-250 support, and if present add the CommonAnnotationBeanPostProcessor.
if (jsr250Present && !registry.containsBeanDefinition(COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)) {
RootBeanDefinition def = new RootBeanDefinition(CommonAnnotationBeanPostProcessor.class);
def.setSource(source);
beanDefs.add(registerPostProcessor(registry, def, COMMON_ANNOTATION_PROCESSOR_BEAN_NAME));
}

// Check for JPA support, and if present add the PersistenceAnnotationBeanPostProcessor.
if (jpaPresent && !registry.containsBeanDefinition(PERSISTENCE_ANNOTATION_PROCESSOR_BEAN_NAME)) {
RootBeanDefinition def = new RootBeanDefinition();
try {
def.setBeanClass(ClassUtils.forName(PERSISTENCE_ANNOTATION_PROCESSOR_CLASS_NAME,
AnnotationConfigUtils.class.getClassLoader()));
}
catch (ClassNotFoundException ex) {
throw new IllegalStateException(
"Cannot load optional framework class: " + PERSISTENCE_ANNOTATION_PROCESSOR_CLASS_NAME, ex);
}
def.setSource(source);
beanDefs.add(registerPostProcessor(registry, def, PERSISTENCE_ANNOTATION_PROCESSOR_BEAN_NAME));
}

if (!registry.containsBeanDefinition(EVENT_LISTENER_PROCESSOR_BEAN_NAME)) {
RootBeanDefinition def = new RootBeanDefinition(EventListenerMethodProcessor.class);
def.setSource(source);
beanDefs.add(registerPostProcessor(registry, def, EVENT_LISTENER_PROCESSOR_BEAN_NAME));
}
if (!registry.containsBeanDefinition(EVENT_LISTENER_FACTORY_BEAN_NAME)) {
RootBeanDefinition def = new RootBeanDefinition(DefaultEventListenerFactory.class);
def.setSource(source);
beanDefs.add(registerPostProcessor(registry, def, EVENT_LISTENER_FACTORY_BEAN_NAME));
}

return beanDefs;
}

里面注册了一堆BeanPostProcessor:

  • ConfigurationClassPostProcessor,处理@Configuration和@Bean
  • AutowiredAnnotationBeanPostProcessor,处理@Autowired,@Value等相关注解
  • CommonAnnotationBeanPostProcessor,支持JSR-250标签,@PostConstruct、@PreDestroy、@Resource
  • EventListenerMethodProcessor,处理@EventListener

这些BeanPostProcessor就是处理各种注解的关键了。

这里也印证了上面说到的,由于注解的处理是借助BeanPostProcessor实现的,所以对所有的BeanDefinition都会生效,即使是从Xml解析过来的BeanDefinition。

要注意的是,BeanPostProcessor有很多子接口,不同的子类是在不同的生命周期中执行的,这个咱们之后会用一篇博客聊透Bean的生命周期。

我不喝咖啡,但是我相信知识有价。