Spring容器加载Bean源码分析

关于面向对象中接口的作用

spring的核心就是控制反转与依赖注入容器,整个容器内对象的依赖关系被容器管理,而这种具体的依赖关系可以通过很多方式,面向对象设计的设计就是面向接口编程,因为接口是定义规范,接口的具体实现不关心,给调用方使用的只是一个黑盒,而这个黑盒的具体表现就是一个接口,就像我们平时使用的物理接口,我们并不关心接口内的电路构造,我只关心这是哪种接口,我的设备是否可以使用而已。

关于spring容器解决的问题

spring控制反转就是生成的对象的方式被反转,原先我们使用对象是手工的去new一个对象,但是容器通过工厂模式,让我们不再关心对象是怎样构造出来了,让开发者直接使用,早期我接触项目的时候看到很多时候我们都是从容器里面直接取一个接口,因为我们是面向接口编程,但是具体的实现类我并不知道,虽然实际情况中一般实现类不多,但是如果哪天替换了实现类,调用方不用做任何修改,因为容器做了一切,这种具体类与具体类的依赖关系由容器管理,容器如何知道这一切呢。例如:

interface A {
    public void hello();
}
class B {
    @Inject  // 容器注入a
    A a;
    public void doSomething() {
        a.hello();
    }
}

B使用了A接口,如果A的实现有多种,我们如何知道这一切是怎样的。B的确是通过接口与A的背后实现者沟通,如果A是一个人可以说不同的语言,B需要哪种语言这个是需要我们人为指定,这个指定就是Spring容器需要知道的事情,人为的去告诉容器,告诉的方式有三种。

  • 第一种
    xml配置的方式,相信有过使用spring开发经验的开发者都会明白,我们通常会使用一个ServletContext的文件xml文件用于管理这种依赖信息,以数据库连接的DataSource为例:
<bean id="driver" class="com.mysql.jdbc.Driver"></bean>
    <bean id="dataSource"
        class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
        <property name="username" value="${com.xxxx.database.username}" />
        <property name="password" value="${com.xxxx.database.password}" />
        <property name="url" value="${com.xxxx.database.url}" />
        <property name="driver" ref="driver" />
    </bean>

如果在代码中我需要向容器使用DataSource的 Bean,容器就会给我这个具体的实现类,我们通过接口获取了具体的对象,哪天我们想换一个数据源不用改代码,直接改配置文件即可。这里还可以初始化这个bean的属性。

  • 基于注解的bean定义
    注解是在3.0被大量引入,由于大量的业务类需要被容器管理,可能每个都去手动配置,这样会导致配置文件的爆炸不好维护,也显得不够合理,所以基于注解的方式给bean定义一个新的选择,只要在容器上加上@Compoent注解即可:
@Compoent
class User {
    private String id;
    private String name;
    // setter getter
}

注解只是一个原数据,它本身不会起什么作用,而是被某个注解处理器感知到了然后通过一些机制把它标注的对象放到了容器里,这样就会指定被容器管理了,而扫描的过程就是spring加载的xml配置文件中:

<context:component-scan base-package="com" />

这个注解处理器就是ComponentScanBeanDefinitionParser。这个调用关系比较复杂,我们可以来梳理一下。

这个就是故事开始的地方,容器会根据这个配置会去处理,我们可以来层层拨开这些调用,找到处理的地方,spring容器,定义了一个接口Resource抽象各种具体的配置资源:
《Spring容器加载Bean源码分析》
这样抽象资源之后得到,ResourceLoader就是加载方式的抽象,这里采用了设计模式之桥梁模式与策略模式,从桥梁模式来看Resource是一个维度,ResourceLoader是一个维度,从策略模式角度,Resource是算法的抽象,ResourceLoader是策略环境类。
容器实现了ResourceLoader并且使用ResourcePatternResolver(桥梁模式第二个维度)解析对应的Resource。
这个时候我们获取了Resource,再调用BeanDefinitionReader从Resource读取BeanDefinition, 这里值得一提的是BeanDefinition就是所有Bean的配置信息,无论是xml还是注解方式还是编程方式定义的Bean最终都会映射成容器的内部BeanDefinition,当然我们这里探究注解方式的解析处理方式,那我们就从容器调用BeanDefinitionReader的函数讲起:

public interface BeanDefinitionReader {
    // 省略部分方法 
    // 这里就是从Resouce加载BeanDefinition
    int loadBeanDefinitions(Resource... resources) throws BeanDefinitionStoreException;


    int loadBeanDefinitions(String location) throws BeanDefinitionStoreException;


    int loadBeanDefinitions(String... locations) throws BeanDefinitionStoreException;

}

这个默认实现是DefaultBeanDefinitionDocumentReader,容器的记载会调用parseBeanDefinitions,这个函数的定义:

/** * Parse the elements at the root level in the document: * "import", "alias", "bean". * @param root the DOM root element of the document */
    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;
                    // 是否是http://www.springframework.org/schema/beans下定义标签
                    if (delegate.isDefaultNamespace(ele)) {
                        parseDefaultElement(ele, delegate);
                    }
                    else { // 注解扫描bean入口
                        delegate.parseCustomElement(ele);
                    }
                }
            }
        }
        else {
            delegate.parseCustomElement(root);
        }
    }

Javadoc写的比较清晰,转换这个根元素的import alias bean等属性.delegate.parseCustomElement(root)通过命名空间拿到对应的NamespaceHandler会调用BeanDefinition org.springframework.beans.factory.xml.NamespaceHandlerSupport.parse(Element element, ParserContext parserContext),定义方法如下:

public BeanDefinition parse(Element element, ParserContext parserContext) {
    // 拿到具体转换器并调用转换器parse方法
        return findParserForElement(element, parserContext).parse(element, parserContext);
    }

    /** * Locates the {@link BeanDefinitionParser} from the register implementations using * the local name of the supplied {@link Element}. */
    private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) {
        String localName = parserContext.getDelegate().getLocalName(element);
        // 根据本地名称去parser缓存拿对应的parser
        BeanDefinitionParser parser = this.parsers.get(localName);
        if (parser == null) {
            parserContext.getReaderContext().fatal(
                    "Cannot locate BeanDefinitionParser for element [" + localName + "]", element);
        }
        return parser;
    }

转换器类型:
《Spring容器加载Bean源码分析》
这么BeanDefinitionParser是通过元素的名称去缓存拿,这个缓存是什么时候加载呢,就是在delegate.parseCustomElement(root);DefaultBeanDefinitionDocumentReader的parseBeanDefinitions方法触发的NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);这里每个xml元素会获取到命名空间并且加载对应的parser,我们在使用<context:component-scan />一定要指定命名空间,否则会抛出这样的异常

元素 "context:component-scan" 的前缀 "context" 未绑定

添加上标签就会注入下面的这些标签BeanDefinitionParser

/** * {@link org.springframework.beans.factory.xml.NamespaceHandler} * for the '{@code context}' namespace. * * @author Mark Fisher * @author Juergen Hoeller * @since 2.5 */
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());
    }

}

其中就包含我们扫描包的:component-scan
这样我们就会调用ComponentScanBeanDefinitionParser的parse方法:

@Override
    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);

        // Actually scan for bean definitions and register them.
        // 这里处理注解并注册为bean
        ClassPathBeanDefinitionScanner scanner = configureScanner(parserContext, element);
        Set<BeanDefinitionHolder> beanDefinitions = scanner.doScan(basePackages);
        registerComponents(parserContext.getReaderContext(), beanDefinitions, element);

        return null;
    }

这里转换成BeanDefinition注册到org.springframework.beans.factory.support.BeanDefinitionRegistry,然后供容器后续使用。
至此容器加载注解配置bean定义的源码分析就差不多了。

  • 代码定义
    xml与注解提供了两种方式定义bean,还有一种方式用的比较少,但在一些特殊的情况下也是一种方案,那就是代码定义。我们先上代码:
/** * @author mapc * @date 2017年6月10日 */
@Configuration
@ImportResource("classpath:*.xml")
public class ConfigBeanDefined {

    // 配置Bean的方式有三种,第一种是xml方式,第二种注解的方式,但是注解怎么初始化属性可以采用代码配置的方式
    @Bean
    public User user() {
        User user = new User();
        user.setName("micro");
        user.setPassword("test_micro");
        return user;
    }

    public static void main(String[] args) {
        AnnotationConfigWebApplicationContext annotationConfigWebApplicationContext = new AnnotationConfigWebApplicationContext();
        annotationConfigWebApplicationContext.setConfigLocation("com.micro.demo.spring");
        annotationConfigWebApplicationContext.refresh();
        User user = (User)annotationConfigWebApplicationContext.getBean("user");
        System.out.println(user.getName());
    }

}

@Configuration标记这是一个BeanDefinition定义类,@ImportResource它可以结合其他容器配置文件一起为容器的定义添砖加瓦。这里@Bean就定义了一个函数方法名的Bean,并且给这个Bean初始值,AnnotationConfigWebApplicationContext的设计结构如下:

public class AnnotationConfigWebApplicationContext extends AbstractRefreshableWebApplicationContext implements AnnotationConfigRegistry 

它在注解容器的基本功能的基础上实现了能够注解配置Registry的能力,看下AnnotationConfigRegistry的源码:

public interface AnnotationConfigRegistry {

    /** * Register one or more annotated classes to be processed. * <p>Calls to {@code register} are idempotent; adding the same * annotated class more than once has no additional effect. * @param annotatedClasses one or more annotated classes, * e.g. {@link Configuration @Configuration} classes */
    void register(Class<?>... annotatedClasses);

    /** * Perform a scan within the specified base packages. * @param basePackages the packages to check for annotated classes */
    void scan(String... basePackages);

}

然后容器的预加载会最终会调用到loadBeanDefinitions,AnnotationConfigWebApplicationContext的reader.register(clazz);完成注解Bean到BeanDefinition的映射关系。这里不展开解析。如果很多时候对象的构造比较复杂,如果使用xml的方式,因为最终属性的定义都是要到基本类型,可能对象引用比较深,A -> B -> C -> D这样的引用链,如果都是对象类型,这样的配置会显得很麻烦,这个时候使用代码配置定义Bean会轻松很多。

总结

xml 配置适合一些组建,因为这些类是属于jar,不能加注解,xml也适合初始化一些bean的属性。

注解配置适合大量无状态的业务类。这样可以减少配置文件大小。更高的自动化。

类的配置方式适合比较多的引用链的时候初始化对象的构造比较复杂。

这三种方式搭建起了spring容器内部数据结构对用户定义bean的映射框架,数据结构的转换是spring容器进行一系列高级特性的基础。因此此文深入分析了三种配置方式的具体实现源码以及各种配置方式的适用场景,我们完全可以在整个项目中采用多种方式结合达到最优的配置,实际项目中也的确这样做的。

还有一些感兴趣的点值得探索,例如xml是如何被解析的。反射在加载bean信息当中的应用。在web环境中如何做多种配置方式的结合等等读者可以自己去探索这里不展开,后续会继续提供更多spring源码的解析,欢迎与博主进行探讨,微博:-超威半导体-。

    原文作者:Spring Boot
    原文地址: https://blog.csdn.net/micro_hz/article/details/73015226
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞