ssh我爱编程

Spring基础(一)

2018-04-25  本文已影响181人  此鱼不得水

本来是准备看一看Spring源码的。然后在知乎上看到来一个帖子,说有一群**自己连Spring官方文档都没有完全读过就嚷嚷着怎么学习Spring源码,这句话戳中了我的心呐,这不就是说我的嘛。然后自己自己想了一下,之前确实了为了源码而去读源码,所以这次就打算仔细读一个Spring的官方文档先。

第二章 Spring框架概述

Spring框架的各个特性被组织成20个模块。这些模块被分组成Core Container(核心容器), Data Access/Integration(数据访问/集成), Web(网络端), AOP (Aspect Oriented Programming,切面编程), Instrumentation, Messaging(消息),和Test(测试), 以下图片显示的就是Spring的各个模块:


image

2.2.1 核心容器

核心容器 包含了 spring-core, spring-beans, spring-context, and spring-expression (Spring表达式语言) 四个模块。

spring-core和spring-beans模块提供了整个框架最基础的部分, 包括了IoC(控制反转)和Dependency Injection(依赖注入)特性。 BeanFactory实现了工厂模式。
Context (spring-context)模块建立在Core and Beans模块提供的基础之上: 它提供了框架式访问对象的方式,类似于JNDI注册。 Context模块从Beans模块中继承了它的特性并且为国际化(例如使用资源包), 事件传播, 资源加载和创建上下文,例如Servlet容器。 Context模块也支持Java EE特性,例如EJB, JMX,和基础远程. ApplicationContext接口是Context模块的焦点所在。
spring-expression模块提供了一种强大的用于在运行时查询操作对象的表达式语言。他是对于在JSP2.1规范中所声明的unified expression语言(统一表达式语言)的扩展。 这种语言支持对属性值, 属性参数, 方法调用, 获得数组内容, 收集器和索引, 算术和逻辑运算, 变量命名和从Spring IoC容器中根据名称获得对象。它也为列表映射和选择提供了支持,就像常见的列表操作一样。

分离的spring-aspects模块集成了AspectJ。

2.2.2 AOP

spring-aop模块提供了AOP面向切面的编程实现,允许你定义,例如, 将拦截器方法和切入点的代码完全分离开来。 利用源码中的元数据, 你可以将行为信息加入到你的代码中, 一定程度上类似于.NET属性。

2.2.3 Messaging

Spring 4框架包含了spring-messaging模块,包含了 Spring Integration项目的高度抽象,我个人理解的messaging就是Spring实现了自己的消息队列。

2.2.4 Data Access/Integration

Spring的集成层面主要包括对于JDBC(spring-jdbc),ORM(spring-orm)和事务(spring-tx)的集成.

2.2.5 Web

Spring的Web模块主要包括了spring-web, spring-webmvc和spring-websocket。
spring-web模块主要是包括了例如文件上传,通过Servlet Listener初始化IOC容器等等web的基础功能。
spring-webmvc提供了Spring自己的MVC实现,Spring的MVN模块在Model 层和Web的表单层面做了一个很明确的分割。

2.2.6 Test

spring-test模块支持JUint和TestNG等集成测试环境。支持统一加载ApplicationContext和缓存这些对戏那个。而且spring-test还提供了mock功能可以帮助你在隔离的环境中测试你的代码~

2.3.1 Spring依赖管理

我们在项目中经常遇到Spring多版本的情况下,如果我们依赖的其他工程中使用了Spring的其他版本,那么在我们的依赖中就会出现多个版本,在spring 版本比较多的情况下就有能出现一些因为版本不兼容的异常,为了解决这些问题,Spring推出了spring-framework-bom,我们可以这样子配置bom文件在我们的pom文件中:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-framework-bom</artifactId>
            <version>4.1.3.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

这样配置之后我们只需要在后面的依赖中添加spring的artifactId和groupId就可以,版本会默认使用上面指定的版本。

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-web</artifactId>
    </dependency>
<dependencies>

2.3.2 Logging

日志记录非常重要在,主要有一下几个原因:

spring默认依赖的日志框架是commons-logging,但是在大多数的使用场景中我们都会提供自己的logging实现,如果要替换掉commons-logging的话,一般需要这两个步骤:

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-core</artifactId>
        <version>4.1.3.RELEASE</version>
        <exclusions>
            <exclusion>
                <groupId>commons-logging</groupId>
                <artifactId>commons-logging</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
</dependencies>
<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-core</artifactId>
        <version>4.1.3.RELEASE</version>
        <exclusions>
            <exclusion>
                <groupId>commons-logging</groupId>
                <artifactId>commons-logging</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <!--将spring 对于jcl规则的实现导向slf4j-->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>jcl-over-slf4j</artifactId>
        <version>1.5.8</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>1.5.8</version>
    </dependency>
    <!--使用log4j来作为slf4j的具体实现-->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>1.5.8</version>
    </dependency>
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.14</version>
    </dependency>
</dependencies>

如果使用lokback的话就不需要这么复杂,因为logback直接实现了log4j,所以只需要添加jcl-over-slf4j and logback包就可以了。

5. IoC 容器

5.1 Spring IoC容器和Bean概述

org.springframework.beans和org.springframework.context包是Spring框架IoC容器的基础。 BeanFactory接口提供了一个先进的配置机制能够管理任何类型的对象。 ApplicationContext(应用上下文) 是BeanFactory的一个子接口。它增加了更方便的集成Spring的AOP功能、消息资源处理(使用国际化)、事件发布和特定的应用层,如在web应用层中使用的WebApplicationContext。在Spring中,被Spring IoC 容器 管理的这些来自于应用主干的这些对象称作 beans 。bean是一个由Spring IoC容器进行实例化、装配和管理的对象。此外,bean只是你应用中许多对象中的一个。Beans以及他们之间的 依赖关系 是通过容器使用 配置元数据 反应出来。

5.2 容器概述

org.springframework.context.ApplicationContext接口代表了Spring IoC容器,并且负责上面提到的Beans的实例化、配置和装配。容器通过读取配置元数据获取对象如何实例化、配置和装配的指示。配置元数据可以用XML、Java注解或Java代码来描述。它允许你表示组成你应用的对象,以及对象间丰富的依赖关系。

Spring提供了几个开箱即用的ApplicationContext接口的实现。在独立的应用程序中,通常创建 ClassPathXmlApplicationContext 或 FileSystemXmlApplicationContext的实例。 虽然XML是定义配置元数据的传统格式,但是你可以指示容器使用Java注解或者代码作为元数据格式,你需要通过提供少量XML配置声明支持这些额外的元数据格式。

5.2.1 配置元数据

Spring IoC容器使用了一种 配置元数据 的形式,这些配置元数据代表了你作为一个应用开发者告诉Spring容器如何去实例化、配置和装备你应用中的对象。基于XML配置的元数据 不是 唯一允许用来配置元数据的一种形式。Spring IoC容器本身是 完全 和元数据配置书写的形式解耦的。
Spring配置包括至少一个且通常多个由容器管理的bean定义。在基于XML配置的元数据中,这些beans配置成一个<bean/>元素,这些<bean/>元素定义在顶级元素<beans/>的里面。在Java配置中通常在一个@Configuration注解的类中,在方法上使用@Bean注解。
下面的例子演示了基于XML的配置元数据的基础结构:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="..." class="...">
        <!-- 在这里写 bean 的配置和相关引用 -->
    </bean>

    <bean id="..." class="...">
        <!-- 在这里写 bean 的配置和相关引用 -->
    </bean>

    <!-- 更多bean的定义写在这里 -->

</beans>
5.2.2 实例化容器

实例化Spring IoC容器很容易。将一个或多个位置路径提供给ApplicationContext的构造方法就可以让容器加载配制元数据,可以从多种外部资源进行获取,例如文件系统、Java的CLASSPATH等等。

ApplicationContext context =
    new ClassPathXmlApplicationContext(new String[] {"services.xml", "daos.xml"});

bean定义可以跨越多个XML文件是非常有用的。通常每个独立的XML配置文件表示一个逻辑层或者是你架构中的一个模块。

你可以使用应用上下文的构造方法从多个XML片段中加载bean的定义。像上面例子中出现过的一样,构造方法可以接收多个Resource位置。或者可以在bean定义中使用一个或多个<import/>从其他的配置文件引入bean定义。例如:

<beans>
    <import resource="services.xml"/>
    <import resource="resources/messageSource.xml"/>
    <import resource="/resources/themeSource.xml"/>

    <bean id="bean1" class="..."/>
    <bean id="bean2" class="..."/>
</beans>

上面的例子中,外部的bean定义从services.xml、messageSource.xml和themeSource.xml这三个文件中加载。所有的位置路径都是相对于定义执行导入的文件,所以 services.xml必须和当前定义导入的文件在相同的路径下。而messageSource.xml和themeSource.xml必须在当前定义导入的文件路径下的resources路径下。你可以看到,这里忽略了反斜杠,由于这里的路径是相对的,因此建议 不使用反斜杠。这些被引入文件的内容会被导入进来,包含顶层的<beans/>元素,它必须是一个符合Spring架构的有效的XML bean定义。

5.2.3 使用容器

ApplicationContext是智能的工厂接口,它能够维护注册不同beans和它们的依赖。通过使用 T getBean(String name, Class<T> requiredType) 方法,你可以取得这些beans的实例。

// 创建并配置beans
ApplicationContext context =
    new ClassPathXmlApplicationContext(new String[] {"services.xml", "daos.xml"});

// 取得配置的实例
PetStoreService service = context.getBean("petStore", PetStoreService.class);

// 使用实例
List<String> userList = service.getUsernameList();

使用getBean()来获取您beans的实例,ApplicationContext接口还有几个其他的可以获取beans的方法,但是理想情况下,你最好不要使用这些方法。 事实上,您的应用程序代码不应调用getBean()所有方法,因而不会和Spring的接口产生依赖。

5.3 Bean概述

一个Spring IoC容器管理了一个或者多个 beans。这些beans通过你提供给容器的配置元数据进行创建,例如通过XML形式的<bean/>定义。
在容器内本身,这些bean定义表示为BeanDefinition对象,它包含了如下的元数据:

ApplicationContext实现还允许由用户在容器外创建注册现有的对象。 这是通过访问ApplicationContext的工厂方法,通过getBeanFactory()返回DefaultListableBeanFactory工厂方法的实现。 DefaultListableBeanFactory支持通过registerSingleton(..)方法和registerBeanDefinition(..)方法进行注册。 然而,典型的应用程序的工作仅仅通过元数据定义的bean定义beans。

5.3.1 bean的命名

每个bean都有一个或多个标识符,这些bean的标识符在它所在的容器中必须唯一。 一个bean通常只有一个标识符,但如果它有一个以上的id标识符,多余的标识符将被认为是别名。

<bean id="clientService"
    class="examples.ClientService"
    factory-method="createInstance"/>
    
public class ClientService {
    private static ClientService clientService = new ClientService();
    private ClientService() {}

    public static ClientService createInstance() {
        return clientService;
    }
}
<!-- 工厂bean,包含createInstance()方法 -->
<bean id="serviceLocator" class="examples.DefaultServiceLocator">
    <!-- 其他需要注入的依赖项 -->
</bean>

<!-- 通过工厂bean创建的ben -->
<bean id="clientService"
    factory-bean="serviceLocator"
    factory-method="createClientServiceInstance"/>
public class DefaultServiceLocator {

    private static ClientService clientService = new ClientServiceImpl();
    private DefaultServiceLocator() {}

    public ClientService createClientServiceInstance() {
        return clientService;
    }
}

5.4 依赖

5.4.1 依赖注入

构造器参数解析 构造器参数通过参数类型进行匹配。如果构造器参数的类型定义没有潜在的歧义,那么bean被实例化的时候,bean定义中构造器参数的定义顺序就是这些参数的顺序并依次进行匹配。看下面的代码:

package x.y;

public class Foo {

    public Foo(Bar bar, Baz baz) {
        // ...
    }
}

<beans>
    <bean id="foo" class="x.y.Foo">
        <constructor-arg ref="bar"/>
        <constructor-arg ref="baz"/>
    </bean>

    <bean id="bar" class="x.y.Bar"/>

    <bean id="baz" class="x.y.Baz"/>
</beans>

当另一个bean被引用,它的类型是已知的,并且匹配也没问题(跟前面的例子一样)。当我们使用简单类型,比如<value>true</value>。Spring并不能知道该值的类型,不借助其他帮助Spring将不能通过类型进行匹配。


public class ExampleBean {

    // Number of years to calculate the Ultimate Answer
    private int years;

    // The Answer to Life, the Universe, and Everything
    private String ultimateAnswer;

    public ExampleBean(int years, String ultimateAnswer) {
        this.years = years;
        this.ultimateAnswer = ultimateAnswer;
    }
}

针对上面的场景可以使用type属性来显式指定那些简单类型那个的构造参数类型,比如:
<bean id="exampleBean" class="examples.ExampleBean">
    <constructor-arg type="int" value="7500000"/>
    <constructor-arg type="java.lang.String" value="42"/>
</bean>

使用index属性来显式指定构造参数的索引,比如:
<bean id="exampleBean" class="examples.ExampleBean">
    <constructor-arg index="0" value="7500000"/>
    <constructor-arg index="1" value="42"/>
</bean>

你也可以使用构造器参数命名来指定值的类型:
<bean id="exampleBean" class="examples.ExampleBean">
    <constructor-arg name="years" value="7500000"/>
    <constructor-arg name="ultimateAnswer" value="42"/>
</bean>
请记住为了使这个起作用,你的代码编译时要打开编译模式,这样Spring可以检查构造方法的参数。如果你不打开调试模式(或者不想打开),也可以使用 @ConstructorProperties JDK注解明确指出构造函数的参数。下面是简单的例子:

public class ExampleBean {
    // Fields omitted
    @ConstructorProperties({"years", "ultimateAnswer"})
    public ExampleBean(int years, String ultimateAnswer) {
        this.years = years;
        this.ultimateAnswer = ultimateAnswer;
    }

}

ApplicationContext所管理的beans支持构造函数注入和setter注入,在一些依赖已经使用构造器注入之后它还支持setter注入。你可以以BeanDefinition的形式配置依赖,它能根据指定的PropertyEditor实现将属性从一种格式转化为另外一种格式。但是,大多数Spring的使用者不会直接使用这些类(也就是通过编程的形式),而是采用XML配置这些bean,注解的组件(即用@Component,@Controller等注解类),或者基于@Configuration类的@Bean方法。本质上这些资源会转换成BeanDefinition的实例并且用于加载整个Spring IoC容器实例。

因为你可以混合使用构造器注入和setter注入, 强制性依赖关系 时使用构造器注入, 可选的依赖关系 时使用setter方法或者配置方法是比较好的经验法则。
通常你可以信赖Spring。在容器加载时Spring会检查配置,比如不存在的bean和循环依赖。当bean创建时,Spring尽可能迟得设置属性和依赖关系。这意味着即使Spring正常加载,在你需要一个存在问题或者它的依赖存在问题的对象时,Spring会报出异常。举个例子,bean因设置缺少或者无效的属性会抛出一个异常。因为一些配置问题存在将会导致潜在的可见性被延迟,所以默认ApplicationContext的实现bean采用提前实例化的单例模式。在实际需要之前创建这些bean会带来时间和内存的开销,当ApplicationContext创建完成时你会发现配置问题,而不是之后。你也可以重写默认的行为使得单例bean延迟实例化而不是提前实例化。

5.4.2 依赖配置详解

<bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <!-- results in a setDriverClassName(String) call -->
    <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql://localhost:3306/mydb"/>
    <property name="username" value="root"/>
    <property name="password" value="masterkaoli"/>
</bean>

接下来的例子使用p 命名空间简化XML配置
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource"
        destroy-method="close"
        p:driverClassName="com.mysql.jdbc.Driver"
        p:url="jdbc:mysql://localhost:3306/mydb"
        p:username="root"
        p:password="masterkaoli"/>

</beans>

你也可以配置java.util.Properties实例,就像这样:
<bean id="mappings"
    class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">

    <!-- typed as a java.util.Properties -->
    <property name="properties">
        <value>
            jdbc.driver.className=com.mysql.jdbc.Driver
            jdbc.url=jdbc:mysql://localhost:3306/mydb
        </value>
    </property>
</bean>
idref元素用来将容器内其他bean的id(值是字符串-不是引用)传给元素<constructor-arg/> 或者 <property/>

<bean id="theTargetBean" class="..."/>

<bean id="theClientBean" class="...">
    <property name="targetName">
        <idref bean="theTargetBean" />
    </property>
</bean>
上面的bean定义片段完全等同于(在运行时)下面片段:

<bean id="theTargetBean" class="..." />

<bean id="client" class="...">
    <property name="targetName" value="theTargetBean" />
</bean>

第一种形式比第二种形式更好,因为使用idref标签允许容器在部署时验证引用的bean是否存在。 在第二种形式中,传给 client bean中属性targetName的值并没有被验证。 只有当 client bean完全实例化的时候错误才会被发现(可能伴随着致命的结果)。
idref和ref的区别就是idref只能够引用其他bean的id,而ref可以引用if或者name

<beans>
    <bean id="parent" abstract="true" class="example.ComplexObject">
        <property name="adminEmails">
            <props>
                <prop key="administrator">administrator@example.com</prop>
                <prop key="support">support@example.com</prop>
            </props>
        </property>
    </bean>
    <bean id="child" parent="parent">
        <property name="adminEmails">
            <!-- the merge is specified on the child collection definition -->
            <props merge="true">
                <prop key="sales">sales@example.com</prop>
                <prop key="support">support@example.co.uk</prop>
            </props>
        </property>
    </bean>
<beans>
<bean class="ExampleBean">
    <property name="email" value=""/>
</bean>
先前的例子等同于以下Java代码:
exampleBean.setEmail("")

<null/>元素处理null值,例如:
<bean class="ExampleBean">
    <property name="email">
        <null/>
    </property>
</bean>
上面的配置等同于下面的Java代码:
exampleBean.setEmail(null)
5.4.3 使用 depends-on

如果一个bean是另外一个bean的依赖,这通常意味着这个bean可以设置成为另外一个bean的属性。当前bean初始化之前显式地强制一个或多个bean被初始化。下面的例子中使用了depends-on属性来指定一个bean的依赖。
为了实现多个bean的依赖,你可以在depends-on中将指定的多个bean名字用分隔符进行分隔,分隔符可以是逗号,空格以及分号等。

<bean id="beanOne" class="ExampleBean" depends-on="manager,accountDao">
    <property name="manager" ref="manager" />
</bean>

<bean id="manager" class="ManagerBean" />
<bean id="accountDao" class="x.y.jdbc.JdbcAccountDao" />
5.4.4 延迟初始化bean

ApplicationContext实现的默认行为就是再启动时将所有singleton bean提前进行实例化。 通常这样的提前实例化方式是好事,因为配置中或者运行环境的错误就会被立刻发现,否则可能要花几个小时甚至几天。如果你不想 这样,你可以将单例bean定义为延迟加载防止它提前实例化。

<bean id="lazy" class="com.foo.ExpensiveToCreateBean" lazy-init="true"/>

但是当一个延迟加载的bean是单例bean的依赖,但这个单例bean又不是 延迟加载,ApplicationContext在启动时创建了延迟加载 的bean,因为它必须满足单例bean的依赖。因此延迟加载的bean会被注入单例bean,然而在其他地方它不会延迟加载。

5.4.6 方法注入

在大部分的应用场景中,容器中的大部分bean是singletons类型的。当一个单例bean需要和另外一个单例bean, 协作时,或者一个费单例bean要引用另外一个非单例bean时,通常情况下将一个bean定义为另外一个bean的属性值就行了。不过对于具有不同生命周期的bean 来说这样做就会有问题了,比如在调用一个单例类型bean A的某个方法,需要引用另一个非单例(prototype)类型bean B,对于bean A来说,容器只会创建一次,这样就没法 在需要的时候每次让容器为bean A提供一个新的bean B实例。
上面问题的一个解决方法是放弃控制反转,你可以实现ApplicationContextAware接口来让bean A感知到容器, 并且在需要的时候通过使用使用getBean("B")向容器请求一个(新的)bean B实例。下面的例子使用了这个方法:

public class CommandManager implements ApplicationContextAware {

    private ApplicationContext applicationContext;

    public Object process(Map commandState) {
        // grab a new instance of the appropriate Command
        Command command = createCommand();
        // set the state on the (hopefully brand new) Command instance
        command.setState(commandState);
        return command.execute();
    }

    protected Command createCommand() {
        // notice the Spring API dependency!
        return this.applicationContext.getBean("command", Command.class);
    }

    public void setApplicationContext(
            ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

Lookup 方法注入 Lookup方法具有使容器覆盖受容器管理的bean方法的能力,从而返回指定名字的bean实例。在上述场景中,Lookup方法注入适用于原型bean。 Lookup方法注入的内部机制是Spring利用了CGLIB库在运行时生成二进制代码的功能,通过动态创建Lookup方法bean的子类从而达到复写Lookup方法的目的.

public abstract class CommandManager {

    public Object process(Object commandState) {
        // grab a new instance of the appropriate Command interface
        Command command = createCommand();
        // set the state on the (hopefully brand new) Command instance
        command.setState(commandState);
        return command.execute();
    }

    // okay... but where is the implementation of this method?
    protected abstract Command createCommand();
}

<!-- a stateful bean deployed as a prototype (non-singleton) -->
<bean id="command" class="fiona.apple.AsyncCommand" scope="prototype">
    <!-- inject dependencies here as required -->
</bean>

<!-- commandProcessor uses statefulCommandHelper -->
<bean id="commandManager" class="fiona.apple.CommandManager">
    <lookup-method name="createCommand" bean="command"/>
</bean>
标识为commandManager的bean在需要一个新的command bean实例时会调用createCommand()方法。你必须将`command`bean部署为 原型(prototype)类型,如果这是实际需要的话。如果部署为singleton。那么每次将返回相同的 `command`bean。

5.5 Bean作用域

当你创建一个 bean 的定义,实际上是创建了一个产生真实实例的配方(recipe)。bean 定义是一个配方(recipe)这种概念是很重要的,它的意思是指,和class一样,你可以从一个配方(recipe)创建多个对象实例。

5.5.1 单例作用域

仅管理一个单例 bean 的共享实例,并且所有通过 id 或者 ids 获得 bean 定义的请求,都会从 Spring 容器中得到同一个特定的 bean 实例。

5.5.2 原型作用域

bean使用原型作用域而不是单例作用域的话,会在每次请求该bean,也就是bean被注入至另一个bean、 或通过调用Spring容器的 getBean() 方法时,创建一个新的bean实例 。 通常,对于有状态的bean使用原型作用域,无状态的bean则使用单例作用域。
与其他作用域不同的是,Spring容器不会管理原型域bean的完整生命周期:Spring容器会初始化、 配置,亦或者组装原型域的bean对象,然后交给客户端,之后就再也不会管这个bean对象了。 因此,对于bean的生命周期方法来说,尽管所有作用域的 初始化方法 都会被调用, 但是原型域bean的 销毁方法 不会 被Spring容器调用。客户端代码要自己负责销毁原型域bean 以及和bean相关的资源(特别是开销大的资源)。如果想让Spring负责这些事(销毁bean、释放资源), 就得自定义bean的后处理器 bean post-processor ,它会持用原型域bean的引用。

5.5.3 依赖原型bean的单例bean

如果你的单例bean依赖了原型bean,谨记这些依赖(的原型bean) 只在初始化时解析 。 因此,假如你将原型bean依赖注入至单例bean,在注入时会初始化一个新的原型bean实例, 这个被注入的原型bean实例是一个独立的实例。

5.5.4 请求作用域、会话作用域和全局会话作用域

仅当 你使用web相关的Spring ApplicationContext(例如 XmlWebApplicationContext)时, 请求 request 、会话 session 和全局会话 global session 作用域才会起作用。如果你在普通的Spring IoC 容器(例如 ClassPathXmlApplicationContext)中使用这几个作用域,会抛出异常 IllegalStateException ,告知你这是一个未知的bean作用域

5.6 定制bean特性

5.6.1 生命周期回调

Spring提供了几个标志接口(marker interface),这些接口用来改变容器中bean的行为;它们包括InitializingBean和DisposableBean。 实现这两个接口的bean在初始化和析构时容器会调用前者的afterPropertiesSet()方法,以及后者的destroy()方法。
在现代的Spring应用中,The JSR-250 @PostConstruct and @PreDestroy 接口一般认为是接收生命周期回调的最佳做法。 使用这些注解意味着bean没有耦合到Spring具体的接口。
Spring在内部使用 BeanPostProcessor 实现来处理它能找到的任何回调接口并调用相应的方法。如果你需要自定义特性或者生命周期行为,你可以实现自己的 BeanPostProcessor 。

初始化回调函数

实现 org.springframework.beans.factory.InitializingBean 接口,允许容器在设置好bean的所有必要属性后,执行初始化事宜。通常,要避免使用 InitializingBean 接口并且不鼓励使用该接口,因为这样会将代码和Spring耦合起来。 使用@PostConstruct注解或者指定一个POJO的初始化方法。 在XML配置元数据的情况下,使用 init-method 属性去指定方法名,并且该方法无参数签名。

析构回调函数

实现 org.springframework.beans.factory.DisposableBean 接口,允许一个bean当容器需要其销毁时获得一次回调。建议不使用 DisposableBean 回调接口,因为会与Spring耦合。使用@PreDestroy 注解或者指定一个普通的方法,但能由bean定义支持。基于XML配置的元数据,使用 <bean/> 的 destroy-method 属性。

组合生命周期机制

截至 Spring 2.5,有三种选择控制bean生命周期行为:InitializingBean 和 DisposableBean 回调接口;自定义init() 和 destroy() 方法; @PostConstruct and @PreDestroy。如果bean存在多种的生命周期机制配置并且每种机制都配置为不同的方法名, 那所有配置的方法将会按照上面的顺利执行。顺序依次是:注解->afterPropertiesSet()->init()方法。

5.6.2 ApplicationContextAware and BeanNameAware

当 ApplicationContext 创建一个实现 org.springframework.context.ApplicationContextAware 接口的对象的实例, 该实例提供一个参考,ApplicationContext。bean可以通过编程方式操纵 ApplicationContext 来创建,通过 ApplicationContext 接口,或者通过向这个接口的一个已知的子类的引用, 如 ConfigurableApplicationContext ,公开附加功能。截止Spring 2.5,自动装配是获取 ApplicationContext 引用传统的 constructor 和 byType自动模式(在 Section 5.4.5, “自动装配协作者”中描述)可以分别为 ApplicationContext 类型的构造函数参数或setter方法参数提供依赖。比较方便的做法直接通过@Autowired将属性注入进来。,但是基于xml的配置也是完全可以的~
当 ApplicationContext 创建一个实现 org.springframework.beans.factory.BeanNameAware 接口的类,该类为 定义在其相关对象定义中的名称提供一个索引。实现方式和上面的ApplicationContextAware一致~

5.6.3 其他 Aware 接口

除了上述的 ApplicationContextAware 和 BeanNameAware ,Spring提供一系列的 Aware 接口,允许bean表示他们需要一定基础设施依赖的容器。 最重要的 Aware 接口概括如下,作为一般规则,这个名称是依赖类型的一个很好的指示:
再次说明,这些接口的使用将您的代码耦合到Spring API,并且不遵循反转控制方式。

5.7 Bean定义的继承

在一个bean定义中,可以包含配置信息,包括构造器参数,属性值,以及容器的特定信息,比如初始化方法, 静态工厂方法名等等。子bean定义,从它的父bean定义继承配置数据;子bean定义可以覆盖一些值, 或者根据需要添加一些其他值。使用父子bean定义,可以避免很多配置填写。事实上,这是一种模板设计模式。

<bean id="inheritedTestBean" abstract="true"
        class="org.springframework.beans.TestBean">
    <property name="name" value="parent"/>
    <property name="age" value="1"/>
</bean>

<bean id="inheritsWithDifferentClass"
        class="org.springframework.beans.DerivedTestBean"
        parent="inheritedTestBean" init-method="initialize">
    <property name="name" value="override"/>
    <!--age属性的值1,将会从父类继承-->
</bean>

下面的例子,通过使用abstract属性,明确地标明这个父类bean定义是抽象的。如果,父类bean定义 没有明确地指出所属的类,那么标记父bean定义为为abstract是必须的,如下:

<bean id="inheritedTestBeanWithoutClass" abstract="true">
    <property name="name" value="parent"/>
    <property name="age" value="1"/>
</bean>

<bean id="inheritsWithClass" class="org.springframework.beans.DerivedTestBean"
        parent="inheritedTestBeanWithoutClass" init-method="initialize">
    <property name="name" value="override"/>
    <!--age属性的值1,将会从父类继承-->
</bean>

这个父bean不能自主实例化,因为它是不完整的,同时它也明确地被标注为abstract;像这样, 一个bean定义为abstract 的,它只能作为一个纯粹的bean模板,为子bean定义,充当父bean定义。 尝试独立地使用这样一个abstract的父bean,把他作为另一个bean 的引用,或者根据这个父bean的id显式调用getBean()方法, 将会返回一个错误。类似地,容器内部的preInstantiateSingletons() 方法,也忽略定义为抽象的bean定义。

5.8 容器拓展点

5.8.1 使用BeanPostProcessor自定义beans

BeanPostProcessor 定义了回调方法,通过实现这个回调方法,你可以提供你自己的(或者重写容器默认的) 实例化逻辑,依赖分析逻辑等等。如果你想在Spring容器完成实例化配置,实例化一个bean之后,实现一些自定义逻辑 你可以插入一个或多个 BeanPostProcessor 的实现。

你可以配置多个BeanPostProcessor实例,同时你也能通过设置 order 属性来控制这些BeanPostProcessors 的执行顺序。只有BeanPostProcessor实现了Ordered 接口,你才可以设置 order 属性。如果,你编写了自己的BeanPostProcessor 也应当考虑实现 Ordered 接口。欲知详情,请参考BeanPostProcessor 和 Ordered接口的javadoc。
BeanPostProcessors作用范围是每一个容器。这仅仅和你正在使用容器有关。如果你在一个容器中定义了一个BeanPostProcessor ,它将 仅仅 后置处理那个容器中的beans。换言之,一个容器中的beans不会被另一个,容器中的BeanPostProcessor处理,即使这两个容器,具有相同的父类。
一个ApplicationContext,自动地检测所有定义在配置元文件中,并实现了BeanPostProcessor接口的bean。 该ApplicationContext注册这些beans作为后置处理器,使他们可以在bean创建完成之后,被调用。 bean后置处理器可以像其他bean一样部署到容器中。

BeanPostProcessor使用的实例
public class InstantiationTracingBeanPostProcessor implements BeanPostProcessor {

    // simply return the instantiated bean as-is
    public Object postProcessBeforeInitialization(Object bean,
            String beanName) throws BeansException {
        return bean; // we could potentially return any object reference here...
    }

    public Object postProcessAfterInitialization(Object bean,
            String beanName) throws BeansException {
        System.out.println("Bean '" + beanName + "' created : " + bean.toString());
        return bean;
    }

}
5.8.2 通过BeanFactoryPostProcessor来处理元数据

BeanFactoryPostProcessor和BeanPostProcessor非常像,但还有一个明显的区别:BeanFactoryPostProcessor主要处理的是bean的定义元数据,Spring允许BeanFactoryPostProcessor读取bean定义的元数据并在bean的实例化之前改变其中的某些元素。它的作用域和BeanPostProcessor保持一致,一个容器中的BeanFactoryPostProcessor只会在当前元素中生效,我们可以通过实现Orderd接口来决定BeanFactoryPostProcessor的生效顺序。Spring在内部已经生成了很多BeanFactoryPostProcessor的实现类,比较常见的就是PropertyPlaceholderConfigurer,用来进行占位符的替换操作。

PropertyPlaceholderConfigurer例子
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
    <property name="locations" value="classpath:com/foo/jdbc.properties"/>
</bean>

<bean id="dataSource" destroy-method="close"
        class="org.apache.commons.dbcp.BasicDataSource">
    <property name="driverClassName" value="${jdbc.driverClassName}"/>
    <property name="url" value="${jdbc.url}"/>
    <property name="username" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>
</bean>

通过对于PropertyPlaceholderConfigurer的声明,我们可以使用jdbc.proerties文件中的属性替换占位符对象。在Spring2.5之后更是提供了注解形式的写法

<context:property-placeholder location="classpath:com/foo/jdbc.properties"/>

Spring并不只会读取priperties文件,还会在没有读取到配置信息的时候进一步读取System配置信息。我们可以通过systemPropertiesMode来控制对于Java System元素的读取优先级:

PropertyOverrideConfigurer使用样例

PropertyOverrideConfigurer可以用来设置bean的属性值,如果配置文件中没有包含对应bean的属性值的话,默认的属性值就会生效。

//db.proerties
dataSource.driverClassName=com.mysql.jdbc.Driver
dataSource.url=jdbc:mysql:mydb

foo.fred.bob.sammy=123

<context:property-override location="classpath:db.properties"/>

然后,名为dataSource的bean的driverClassName属性和url属性都会被替换为系统中的值,嵌套的属性值也会被替换掉,例如:foo.fred.bob.sammy

5.9 注解配置

注解和XML那个更屌?一千个人眼中有一千个哈姆雷特,针对这个问题大家的看法也都不一致。但是我常用给的做法是这样子:一些对于第三方依赖配置我会使用xml配置,例如db层,(kafka)队列介入等。对于内部使用的Bean,例如Service,Dao,Conmopnet等等都是通过注解来实现的,特别是对于Bean属性的注入等等都是通过注解的。总结的话使用比例大概在7:3,部分简单的配置也会直接用@Configuration替换xml配置(偷懒)。但是注解的缺点就是与Spring代码严重耦合,如果注解多的情况下代码看起来可能会比较乱,维护的地方比较分散,不像xml那样基本只会分布在resource目录的。

注解是在xml配置生效前生效的,所以如果注解和xml同时都配置来某个信息,xml的配置会覆盖注解的。
通常情况下,如果需要使Spring注解生效的话,一般会在xml中使用以下配置:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

    <context:annotation-config/>

</beans>

这个配置主要告诉Spring激活AutowiredAnnotationBeanPostProcessor, CommonAnnotationBeanPostProcessor, PersistenceAnnotationBeanPostProcessor和RequiredAnnotationBeanPostProcessor.

public class MovieRecommender {

    @Autowired
    private MovieCatalog[] movieCatalogs;
}

public class MovieRecommender {

    private Set<MovieCatalog> movieCatalogs;
    @Autowired
    public void setMovieCatalogs(Set<MovieCatalog> movieCatalogs) {
        this.movieCatalogs = movieCatalogs;
    }
}
public class MovieRecommender {

    private MovieCatalog movieCatalog;

    private CustomerPreferenceDao customerPreferenceDao;

    @Autowired
    public void prepare(@Qualifier("main")MovieCatalog movieCatalog,
            CustomerPreferenceDao customerPreferenceDao) {
        this.movieCatalog = movieCatalog;
        this.customerPreferenceDao = customerPreferenceDao;
    }

    // ...

}
@Configuration
@ComponentScan(basePackages = "org.example",
        includeFilters = @Filter(type = FilterType.REGEX, pattern = ".*Stub.*Repository"),
        excludeFilters = @Filter(Repository.class))
public class AppConfig {
    ...
}
通过components来定义Bean metadata

Spring还可以通过components或者@Configuration来生成bean definition,你可以在要生成bean的方法上标注@Bean注解。下面是个例子:

@Component
public class FactoryMethodComponent {
    @Bean
    @Qualifier("public")
    public TestBean publicInstance() {
        return new TestBean("publicInstance");
    }
}

这个例子中就通过@Bean来生成一个bean definition。

@Component
public class FactoryMethodComponent {

    private static int i;

    @Bean
    @Qualifier("public")
    public TestBean publicInstance() {
        return new TestBean("publicInstance");
    }

    @Bean
    protected TestBean protectedInstance(
            @Qualifier("public") TestBean spouse,
            @Value("#{privateInstance.age}") String country) {
        TestBean tb = new TestBean("protectedInstance", 1);
        tb.setSpouse(spouse);
        tb.setCountry(country);
        return tb;
    }

    @Bean
    @Scope(BeanDefinition.SCOPE_SINGLETON)
    private TestBean privateInstance() {
        return new TestBean("privateInstance", i++);
    }

    @Bean
    @Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
    public TestBean requestScopedInstance() {
        return new TestBean("requestScopedInstance", 3);
    }

}

Spring组件中的@Bean方法与Spring @Configuration类中的对应方法不同。不同之处是,@Component类没有通过CGLIB增强,以拦截方法和字段的调用。CGLIB代理是在@Configuration类中的@Bean方法中调用方法或字段的方法,它为协作对象创建bean元数据引用;这些方法不是用普通的Java语义调用的。相反,在@Component类中调用@Bean方法中的方法或字段具有标准的Java语义。
当Spring在监测到一个Component的时候,如果注解上没有定义名称的话,Spring则会模式给该bean生成一个name,生成的规则还是采用首字母小写的形式。 如果你想定义自己的默认名字生成规则,可以这个做:
1.实现ScopeMetadataResolver接口
2.配置时候指定自己定义的ScopeMetadataResolver

@Configuration
@ComponentScan(basePackages = "org.example", scopeResolver = MyScopeResolver.class)
public class AppConfig {
    ...
}

5.12 基于Java(Java-based)的容器配置

5.12.1 基本概念: @Bean and @Configuration

Spring新的Java配置支持中核心构件是 @Configuration 注释类和 @Bean 注释方法。

@Bean 注释是用来表示一个方法实例化,配置和初始化一个由Spring IoC容器管理的新的对象。对于那些熟悉Spring的 <beans/> XML 配置, @Bean 注释和 <bean/> 一样起着同样的作用。你可以使用 @Bean 注释任何Spring @Component 的方法,但是, 最经常使用的是 @Configuration(@Configuration就是一种被@Component标示的注解) 注释bean。

用 @Configuration 注释一个类表明它的主要目的是作为bean定义的来源。此外, @Configuration 注释的类允许inter-bean依赖关系 在相同的类中,通过简单地调用其他 @Bean 方法被定义。最简单的 @Configuration 类定义如下:

@Configuration
public class AppConfig {
    @Bean
    public MyService myService() {
        return new MyServiceImpl();
    }
}

在使用@Bean注解的时候也可以做到Bean的生命周期管理,如下:

public class Foo {
    public void init() {
        // initialization logic
    }
}

public class Bar {
    public void cleanup() {
        // destruction logic
    }
}

@Configuration
public class AppConfig {

    @Bean(initMethod = "init")
    @Scope("prototype")
    public Foo foo() {
        return new Foo();
    }

    @Bean(name = "testBean", destroyMethod = "cleanup")
    public Bar bar() {
        return new Bar();
    }
    
    //只有在使用@Configuration的时候,内部@Bean才可以使用
    @Bean
    public Demo demo(){
        retrun new Demo(bar());
    }

}
@Configuration
public class ConfigA {
     @Bean
    public A a() {
        return new A();
    }
}

@Configuration
@Import(ConfigA.class)
public class ConfigB {

    @Bean
    public B b() {
        return new B();
    }

}

我们在@Configuration生效的时候也可以进行一定的定制,比如在测试环境生效,线上环境不生效这种操作,这时候可以依赖另外一个注解@Conditional,做法就是实现Conditinal,然后在@Conditional的注解上指定其实现类就可以做到有条件的激活。

5.13 Environment抽象

Environment 是集成在容器中的抽象,他包含了两个两个方面:profiles 和 properties.

profile是一个命名,是一组逻辑上bean定义的组,只有相应的profile被激活的情况下才会起作用。可以通过XML或者注解将bean分配给一个profile,Environment对象在profile中的角色是判断哪一个profile应该在当前激活和哪一个profile应该在默认情况下激活。

属性在几乎所有应用中都扮演了非常重要的角色,并且可能来源于各种各样的资源:属性文件,JVM系统属性,系统环境变量,JNDI,servlet上下文参数,点对点的属性对象,映射等等。Environment对象在属性中的角色是提供一个方便的服务接口来配置属性资源和解决它们的属性。

5.13.1 Bean定义配置文件

Beand定义配置文件是核心容器的一种机制,它允许为不同的bean在不同的环境中注册。 environment这个词可以意味着不同的事情不同的用户,而且这个功能可以帮助很多用例,包括:

在工作中使用内存数据源并且在QA和生产环境中通过JDNI查找相同的数据源
只有当部署应用到一个性能测试环境时注册监视工具
给客户A注册定制的bean实现而不需要给客户B时

@Profile 注解用于当一个或多个配置文件激活的时候,用来指定组件是否有资格注册。使用上面的例子,我们可以按如下方式重写dataSource配置:

@Configuration
@Profile("dev")
public class StandaloneDataConfig {

    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.HSQL)
            .addScript("classpath:com/bank/config/sql/schema.sql")
            .addScript("classpath:com/bank/config/sql/test-data.sql")
            .build();
    }
}

如果带@Configuration的类被标记了@Profile,那么只有当这个配置是激活状态的时候,这个类中标记@Bean的方法和@Import关联的类才有效,否则就会被忽略。 如果一个@Component或@Configuration类标记了@Profile({"p1", "p2"}),这样的类只有当p1和(或)p2激活的时候才有效。如果一个配置使用了!前缀,只有当这个配置不激活的时候才有效。例如@Profile({"p1", "!p2"}),只有当p1激活,p2不激活的时候才有效。

启用配置文件

现在我们已经更新了我们的配置,我们还需要指示那个配置文件处于激活状态。如果我们现在启动我们的示例程序,我们会看到抛出NoSuchBeanDefinitionException异常,因为我们的容器找不到名为dataSource的bean对象。

激活配置文件可以采取多种方式,但是最直接的方式就是以编程的方式使用ApplicationContext API:

AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("dev");
ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class);
ctx.refresh();

配置文件还可以以声明的方式通过spring.profiles.active属性来激活,可以通过系统环境变量,JVM系统属性。启动Tomcat的时候只要简单的加一条启动参数-Dspring.profiles.active="profile1,profile2"就可以激活目标profile。另外需要注意一点的是,如果一个profile的name配置了“default”的话,是默认激活的项。

5.15.2 Spring标准的和用户自定义事件

Spring本身也是支持事件监听机制的,其基本的事件和事件监听器主要是ApplicationEvent和ApplicationListener接口,如果有ApplicationEvent发布的话,对应的监听器类就会触发自己监听操作,下面介绍一些Spring自己的事件。

我们也可以定义自己的事件机制,下面简单介绍一下:

//定义事件
public class BlackListEvent extends ApplicationEvent {

    private final String address;
    private final String test;

    public BlackListEvent(Object source, String address, String test) {
        super(source);
        this.address = address;
        this.test = test;
    }
}

public class EmailService implements ApplicationEventPublisherAware {

    private List<String> blackList;
    //事件发布者
    private ApplicationEventPublisher publisher;

    public void setBlackList(List<String> blackList) {
        this.blackList = blackList;
    }

    public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }

    public void sendEmail(String address, String text) {
        if (blackList.contains(address)) {
            BlackListEvent event = new BlackListEvent(this, address, text);
            publisher.publishEvent(event);
            return;
        }
    }
}

//特殊事件监听器(通过事件类型区分)
public class BlackListNotifier implements ApplicationListener<BlackListEvent> {

    private String notificationAddress;
    public void setNotificationAddress(String notificationAddress) {
        this.notificationAddress = notificationAddress;
    }

    public void onApplicationEvent(BlackListEvent event) {
        // notify appropriate parties via notificationAddress...
    }
}
上一篇下一篇

猜你喜欢

热点阅读