Spring基于注解的Bean注入过程分析

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的包扫描过程如下:

  1. 通过配置的basepackage,将它转化为对应的类路径的目录,遍历该目录下的class文件,得到每个class文件的路径,实例化为Resource接口的实例。
  2. ClassReader通过分析字节码的方式而不是加载字节码的方式,得到了每个class文件所代表的类的元信息。这样Spring就知道了每个类都有什么注解。
  3. Spring有个过滤器,过滤出需要加载的类,实例化Bean,注入到Spring容器。

转载于:https://my.oschina.net/u/1393056/blog/1633676

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值