概述
Spring基于注解的Bean注入过程分析
使用Spring基于注解的方式配置Bean,只需要在类名称上加上@Bean注解就可以了,Spring是如何实现的呢。
1. 写一个简单的测试工程
先建立一个简单的工程,写一段简单的程序,工程代码如下:
maven工程代码目录结构:
spring-study
├── pom.xml
└── src
├── main
│ └── java
│ └── com
│ └── zk
│ └── spring
│ ├── Main.java
│ └── Person.java
Main.java
package com.zk.spring;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
applicationContext.scan("com.zk.spring");
applicationContext.refresh();
Person p = (Person) applicationContext.getBean("person");
System.out.println(p.getName());
}
}
Person.java
package com.zk.spring;
import org.springframework.stereotype.Component;
@Component
public class Person {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Person(){
this.name = "zk";
}
}
运行Main类的main函数,程序输出了 “zk”。
2. 代码主流程分析
代码第9行扫描了包“com.zk.spring",AnnotationConfigApplicationContext类的scan函数代码如下:
public void scan(String... basePackages) {
Assert.notEmpty(basePackages, "At least one base package must be specified");
this.scanner.scan(basePackages);
}
可以看出,实际上是调用了AnnotationConfigApplicationContext实例的的scanner属性的scan方法。scanner在AnnotationConfigApplicationContext类的构造函数宗初始化:
public AnnotationConfigApplicationContext() {
this.reader = new AnnotatedBeanDefinitionReader(this);
this.scanner = new ClassPathBeanDefinitionScanner(this);
}
ClassPathBeanDefinitionScanner类的scan方法代码如下:
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);
}
该方法先记录了开始扫描之前已经注册的Bean个数,然后调用doScan方法,最后返回了新增注册的Bean个数。
doScan方法的代码如下:
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;
}
扫描Bean的过程发生在上面代码块的第5行findCandidateComponents方法内,后续的代码是对Bean注入的Post处理以及验证。
findCandidateComponents方法的代码如下:
public Set<BeanDefinition> findCandidateComponents(String basePackage) {
if (this.componentsIndex != null && indexSupportsIncludeFilters()) {
return addCandidateComponentsFromIndex(this.componentsIndex, basePackage);
}
else {
return scanCandidateComponents(basePackage);
}
}
对于我们的简单工程,上面的代码会进入else分支,也就是调用scanCandidateComponents方法。该方法的代码如下:
private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
Set<BeanDefinition> candidates = new LinkedHashSet<>();
try {
String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
resolveBasePackage(basePackage) + '/' + this.resourcePattern;
Resource[] resources = getResourcePatternResolver().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 = getMetadataReaderFactory().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;
}
对于每一个basepackage,上面代码块的第6行,将会得到一个Resource数组。Resource接口是Spring底层用来表示一个资源的基础类。如果是一个文件类型的资源,通过Resource实例我们可以知道文件的路径,文件是否可读,文件的大小等关于该文件的元信息。对于我们的测试代码,当运行到这里的时候,Resource的运行时类型是ClassPathResource,它有一个classLoader属性:
@Nullable
private ClassLoader classLoader;
对于我们的测试代码,传入的basepackage变量是“com.zk.spring”,将会被解析成对应的路径“com/zk/spring",得到的Resource数组实际上就是代表了2个class文件:
- com/zk/spring/Main.class
- com/zk/spring/Person.class
到这里其实大致的过程我们就应该知道了,知道了class文件的路径,就能读取其字节码内容,调用ClassLoader的方法加载该类。
那么Spring加载类的代码是如何实现的呢。
MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
这里又涉及到一个新的接口MetadataReader,故名思义,它是一个用来读取类的元数据的Reader。在Spring中有一个实现类SimpleMetadataReader。
SimpleMetadataReader类的代码并不多,整个类的定义如下(去掉了注释):
package org.springframework.core.type.classreading;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import org.springframework.asm.ClassReader;
import org.springframework.core.NestedIOException;
import org.springframework.core.io.Resource;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.ClassMetadata;
import org.springframework.lang.Nullable;
final class SimpleMetadataReader implements MetadataReader {
private final Resource resource;
private final ClassMetadata classMetadata;
private final AnnotationMetadata annotationMetadata;
SimpleMetadataReader(Resource resource, @Nullable ClassLoader classLoader) throws IOException {
InputStream is = new BufferedInputStream(resource.getInputStream());
ClassReader classReader;
try {
classReader = new ClassReader(is);
}
catch (IllegalArgumentException ex) {
throw new NestedIOException("ASM ClassReader failed to parse class file - " +
"probably due to a new Java class file version that isn't supported yet: " + resource, ex);
}
finally {
is.close();
}
AnnotationMetadataReadingVisitor visitor = new AnnotationMetadataReadingVisitor(classLoader);
classReader.accept(visitor, ClassReader.SKIP_DEBUG);
this.annotationMetadata = visitor;
// (since AnnotationMetadataReadingVisitor extends ClassMetadataReadingVisitor)
this.classMetadata = visitor;
this.resource = resource;
}
@Override
public Resource getResource() {
return this.resource;
}
@Override
public ClassMetadata getClassMetadata() {
return this.classMetadata;
}
@Override
public AnnotationMetadata getAnnotationMetadata() {
return this.annotationMetadata;
}
}
这个类主要干的事情就是调用Resource接口的getInputStream方法,得到了一个InputStream,可以用来读取字节码,用这个字节码实例化了一个ClassReader实例,ClassReader根据JVM规范中class文件的结构,分析出了该字节码所表示的类的方法,属性,注解等等所有的类的元信息。得到了这些元信息,就可以判断有没有@Component注解,有的话就加载该类,并实例化后注入到Spring的容器中。
这里有一点值得注意的是,Spring是自己分析了字节码,从而解析出的元信息,然后再决定要不要加载该字节码到虚拟机,在分析的过程中字节码就是普通的byte数组。其实我们想一想的话,要分析出类是否有被@Component注解,有很直接的方式,根据配置的basepackage,得到目录下所有类的全类名,然后使用Class.forName方法就可以通过反射的方式得到类的元信息了。但是这样的话就会导致给定目录下的全部类都会在初始化Spring容器的时候就被加载了,这显然会有启动性能问题,也没有必要。
3. 总结
总结一下,Spring的包扫描过程如下:
- 通过配置的basepackage,将它转化为对应的类路径的目录,遍历该目录下的class文件,得到每个class文件的路径,实例化为Resource接口的实例。
- ClassReader通过分析字节码的方式而不是加载字节码的方式,得到了每个class文件所代表的类的元信息。这样Spring就知道了每个类都有什么注解。
- Spring有个过滤器,过滤出需要加载的类,实例化Bean,注入到Spring容器。
转载于:https://my.oschina.net/u/1393056/blog/1633676
最后
以上就是现实信封为你收集整理的Spring基于注解的Bean注入过程分析Spring基于注解的Bean注入过程分析的全部内容,希望文章能够帮你解决Spring基于注解的Bean注入过程分析Spring基于注解的Bean注入过程分析所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复