Spring 学习笔记
基于spring-framework-4.3.6.RELEASE
相关代码存放于码云上的hellospring4工程下:
https://git.oschina.net/laideju/hellospring4 以及 https://git.oschina.net/laideju/spring-struts
一、搭建Spring开发环境
1. 拷贝相关 jar 包。
Spring的架构如下图所示:
所以,基本的Spring开发环境搭建包括,添加 spring-framework-4.3.6.RELEASE\libs 目录下的
- spring-beans-4.3.6.RELEASE.jar
- spring-context-4.3.6.RELEASE.jar
- spring-core-4.3.6.RELEASE.jar
- spring-expression-4.3.6.RELEASE.jar
- commons-logging-1.2.jar
到Spring工程的Classpath下。
2. 配置
一个典型的 Spring 项目需要创建一个或多个 Bean 配置文件,这些配置文件用于在 Spring IOC 容器里配置 Bean。Bean 的配置文件可以放在 Classpath 下,也可以放在其它目录下。
二、配置Bean
配置形式:基于 XML 文件的方式;基于注解的方式。下文主要是基于 XML 的方式来配置Bean。
<bean id="helloWorld" class="hellospring.stepone.HelloWorld"> <property name="name" value="laideju"></property> </bean>
其中 id 指明了 Bean 的名称:在 IOC 容器中必须是唯一的;若 id 没有指定,Spring 自动将类的完全限定名作为 Bean 的名字;id 可以指定多个名字,名字之间可用英文的逗号、分号、或空格分隔。
三、Spring 容器
在 Spring IOC 容器读取 Bean 配置创建 Bean 实例之前,必须对它进行实例化。只有在容器实例化后,才可以从 IOC 容器里获取 Bean 实例并使用。
Spring 提供了两种类型的 IOC 容器实现:
- BeanFactory:IOC 容器的基本实现。
- ApplicationContext:提供了更多的高级特性。 是 BeanFactory 的子接口。
BeanFactory 是 Spring 框架的基础设施,面向 Spring 本身;ApplicationContext 面向使用 Spring 框架的开发者,几乎所有的应用场合都直接使用 ApplicationContext 而非底层的BeanFactory。无论使用何种方式,配置文件是相同的。
1. ApplicationContext
ApplicationContext 的主要实现类:
①. ClassPathXmlApplicationContext,从 类路径下加载配置文件;
②. FileSystemXmlApplicationContext,从文件系统中加载配置文件。
ConfigurableApplicationContext 扩展于 ApplicationContext,新增加两个主要方法:refresh() 和 close(), 让 ApplicationContext 具有启动、刷新和关闭上下文的能力。
ApplicationContext 在初始化上下文时就实例化所有单例的 Bean。
WebApplicationContext 是专门为 WEB 应用而准备的,它允许从相对于 WEB 根目录的路径中完成初始化工作。
2. 从 IOC 容器中获取 Bean
调用 ApplicationContext 的 getBean() 方法。
3. 依赖注入的方式
Spring 支持 3 种依赖注入的方式:属性注入、构造器注入、工厂方法注入(很少使用,不推荐)。
3.1. 属性注入
属性注入即通过 setter 方法注入Bean 的属性值或依赖的对象。属性注入使用 <property> 元素,使用 name 属性指定 Bean 的属性名称,value 属性或 <value> 子节点指定属性值。属性注入是实际应用中最常用的注入方式。
3.2. 构造器注入
通过构造方法注入Bean 的属性值或依赖的对象,它保证了 Bean 实例在实例化后就可以使用。
构造器注入在 <constructor-arg> 元素里声明属性,<constructor-arg> 中没有 name 属性。 需要注意的是,在 Bean 中必须有对应的构造器。
public class Car { private String company; private String brand; private int maxSpeed; private double price; @Override public String toString() { return "Car [company=" + company + ", brand=" + brand + ", maxSpeed=" + maxSpeed + ", price=" + price + "]"; } public Car(String brand) { // 给对应的属性赋值 } public Car(String brand, String company, double price) { // 给对应的属性赋值 } public Car(String brand, String company, int maxSpeed) { // 给对应的属性赋值 } }
<bean id="car1" class="hellospring.stepone.Car"> <constructor-arg value="Audi" /> </bean>
上面的Bean配置中将调用 Car 的只有一个参数的构造器来实例化 car1。若一个 bean 有多个构造器,可以根据 <constructor-arg>的 index(按索引匹配入参) 和 type(按类型匹配入参) 属性进行更加精确的定位。例如下面的配置将匹配到构造器:Car(String, String, double)
<bean id="car2" class="hellospring.stepone.Car"> <constructor-arg value="上海公司" index="1" /> <constructor-arg value="大众" index="0" /> <constructor-arg value="230000" type="double" /> </bean>
而下面的配置将匹配到构造器: Car(String, String, int)
<bean id="car3" class="hellospring.stepone.Car"> <constructor-arg value="上海公司" index="1" /> <constructor-arg value="大众" index="0" /> <constructor-arg value="240" index="2" /> </bean>
3.3. 注入配置的一些细节
(1). 字面值
可用字符串表示的值称之为字面值。基本数据类型及其封装类、String 等类型都可以采取字面值注入的方式。字面值可以通过 <value> 元素标签或 value 属性进行注入,若字面值中包含特殊字符,可以使用 <![CDATA[]]> 把字面值包裹起来。
<bean id="car4" class="hellospring.stepone.Car"> <constructor-arg index="2"> <!-- 属性值可以通过value子节点进行设置 --> <value>240</value> </constructor-arg> <constructor-arg index="1"> <!-- 若字面值中包含特殊字符,则可以使用 DCDATA 来进行赋值 --> <value><![CDATA[<武汉公司>]]></value> </constructor-arg> <constructor-arg value="大众" index="0" /> </bean>
(2). 引用其它 Bean、内部Bean
在 Bean 的配置文件中,可以通过 <ref> 标签或 ref 属性为 Bean 的属性或构造器参数指定对 Bean 的引用。
public class Person { private String name; private int age; private Car car; @Override public String toString() { return "Person [name=" + name + ", age=" + age + ", car=" + car + "]"; } // 默认构造器 public Person() {} public Person(String name, int age, Car car) { // 给对应的属性赋值 } }
<bean id="person1" class="hellospring.stepone.Person"> <property name="name" value="Tom" /> <property name="age" value="26" /> <!-- 通过 ref 属性值指定当前属性引用的 bean --> <!-- <property name="car" ref="car4"/> --> <!-- 或者也可以通过ref子标签来指明所引用的bean --> <property name="car"> <ref bean="car4"/> </property> </bean>
也可以在属性(<property>标签)或构造器(<constructor-arg>标签)里包含 Bean 的声明,这样的 Bean 称为内部 Bean。内部 Bean 仅供特定的属性或构造器使用,不能在这之外引用,所以不需为其设置 id 或 name 属性(即使设置了也没什么作用)。
<bean id="person2" class="hellospring.stepone.Person"> <constructor-arg value="laideju"/> <constructor-arg value="27"/> <constructor-arg> <bean class="hellospring.stepone.Car"> <constructor-arg value="哈弗" /> <constructor-arg value="长安汽车有限公司" /> <constructor-arg value="120000" type="double" /> </bean> </constructor-arg> </bean>
(3). null 值
可以使用Spring专用的 <null/> 标签为 Bean 的字符串或其它对象类型的属性注入 null 值。
<bean id="person2" class="hellospring.stepone.Person"> <constructor-arg value="laideju"/> <constructor-arg value="27"/> <constructor-arg> <!-- 测试赋值为 null --> <null /> </constructor-arg> </bean>
(4). 级联属性
Spring支持级联属性(即属性的属性),但需要注意的是,需要先为上级属性赋值才能为级联属性赋值,否则会报空引用异常(而Struts2则不会,它会自动创建上级属性)。
<bean id="person2" class="hellospring.stepone.Person"> <constructor-arg value="laideju"/> <constructor-arg value="27"/> <constructor-arg> <bean id="car3" class="hellospring.stepone.Car"> <constructor-arg value="哈弗" /> <constructor-arg value="长安汽车有限公司" /> <constructor-arg value="120000" type="double" /> </bean> </constructor-arg> <!-- 测试级联属性,需要先为car属性赋值,再才能为car的maxSpeed属性赋值 --> <property name="car.maxSpeed" value="230"></property> </bean>
(5). 集合属性
在 Spring中可以通过一组内置的 xml 标签(例如: <list>, <set> 或 <map>)来配置集合属性。
<list>标签
配置 java.util.List 类型的属性,需要指定 <list> 标签,在标签里包含一些子标签。这些子标签可以:
- 通过 <value> 指定简单的常量值,通过 <ref> 指定对其他 Bean 的引用;
- 通过<bean> 指定内置 Bean 定义;
- 通过 <null/> 指定空元素。 甚至可以内嵌其他集合。
数组的定义和 List 一样,都使用 <list>。配置 java.util.Set 需要使用 <set> 标签,定义元素的方法与 List 一样。
<bean id="person3" class="hellospring.beancollection.Person"> <property name="name" value="lijingyu"></property> <property name="age" value="26"></property> <!-- cars是Person类中的类型为 List的属性 --> <property name="cars"> <list> <ref bean="car1" /> <bean class="hellospring.stepone.Car"> <constructor-arg value="神牛25" /> <constructor-arg value="中国铁机" /> <constructor-arg value="120" type="int" /> </bean> <null/> </list> </property> </bean>
<map>标签
Java.util.Map 通过 <map> 标签定义,<map> 标签里可以使用多个 <entry> 作为子标签。
每个条目包含一个键和一个值。必须在 <key> 标签里定义键。因为键和值的类型没有限制,所以可以自由地为它们指定 <value>,<ref>,<bean> 或 <null> 元素。
可以将 Map 的键和值作为 <entry> 的属性定义:简单常量使用 key 和 value 来定义;Bean 引用通过 key-ref 和 value-ref 属性定义。
<bean id="personWithMap" class="hellospring.beancollection.PersonWithMap"> <property name="name" value="ldj007"></property> <property name="age" value="27"></property> <property name="cars"> <map> <entry key="AA" value-ref="car" /> <entry key="BB"> <bean class="hellospring.stepone.Car"> <property name="company" value="CN-01" /> <property name="brand" value="BRAND-01" /> <property name="price" value="120000" /> <property name="maxSpeed" value="120" /> </bean> </entry> </map> </property> </bean>
PersonWithMap的定义如下:
public class PersonWithMap { private String name; private int age; private Map<String, Car> cars; // getters, setters and override toString() }
<props>标签
使用 <props> 定义 java.util.Properties,该标签使用多个 <prop> 作为子标签。每个 <prop> 标签必须定义 key 属性。
<bean id="dataSource" class="hellospring.beancollection.DataSource"> <property name="properties"> <props> <prop key="user">root</prop> <prop key="password">root</prop> <prop key="jdbcUrl">jdbc:mysql:///test</prop> <prop key="driverClass">com.mysql.jdbc.Driver</prop> </props> </property> </bean>
DataSource的定义如下:
public class DataSource { private Properties properties; // getters, setters and override toString() }
(6). 使用 utility scheme 定义集合
使用基本的集合标签定义集合时,不能将集合作为独立的 Bean 定义,导致其他 Bean 无法引用该集合,所以无法在不同 Bean 之间共享集合。
可以使用 util schema 里的集合标签定义独立的集合 Bean。 需要注意的是,必须在 <beans> 根元素里添加 util schema 定义:xmlns:util="http://www.springframework.org/schema/util"
定义独立集合Bean并引用的实例如下:
<!-- 定义独立的集合Bean,以供其他 Bean 引用 --> <util:list id="common-cars"> <ref bean="car1"/> <bean class="hellospring.stepone.Car"> <property name="company" value="AOA11" /> <property name="brand" value="POLO" /> <property name="price" value="340000" /> <property name="maxSpeed" value="300" /> </bean> </util:list> <bean id="person4" class="hellospring.beancollection.Person"> <property name="name" value="laijibo" /> <property name="age" value="28"/> <!-- 引用独立定义的公共集合Bean --> <property name="cars" ref="common-cars" /> </bean>
(7). 使用 p 命名空间
为了简化 XML 文件的配置,越来越多的 XML 文件采用属性而非子元素配置信息。Spring 从 2.5 版本开始引入了一个新的 p 命名空间,可以通过 <bean> 元素属性的方式配置 Bean 的属性。
需要注意的是,必须在 <beans> 根元素里添加 util schema 定义:xmlns:p="http://www.springframework.org/schema/p"。使用p命名空间的实例如下:
<!-- 使用p命名空间简化配置Bean的属性 --> <bean id="person5" class="hellospring.beancollection.Person" p:name="laijibo2" p:age="29" p:cars-ref="common-cars" />
3.4. Bean 自动装配
Spring IOC 容器可以自动装配 Bean,即只声明 bean,而把 bean 之间的关系交给 IOC 容器来完成。需要做的仅仅是在 <bean> 的 autowire 属性里指定自动装配的模式:
- byType,根据类型进行自动装配,但要求 IOC 容器中只有一个类型对应的 bean,若有多个则无法完成自动装配;
- byName,若 Bean 的某个属性名和某一个 <bean> 的 id 值一致,即可完成自动装配,若没有 id 一致的,则无法完成自动装配;
- constructor,通过构造器自动装配,当 Bean 中存在多个构造器时,此种自动装配方式将会很复杂(不推荐使用)。// 自动装配Bean
public class Address { private String city; private String street; // getters, setters and override toString() } public class Car { private String brand; private int price; // getters, setters and override toString() } public class Person { private String name; private Address address; private Car car; // getters, setters and override toString() }
<!-- <bean id="address" class="hellospring.autowire.Address" p:city="Wuhan" p:street="XiongChu" /> --> <bean id="address1" class="hellospring.autowire.Address" p:city="Wuhan" p:street="XiongChu" /> <bean id="car" class="hellospring.autowire.Car" p:brand="Audi" p:price="300000" /> <bean id="person" class="hellospring.autowire.Person" p:name="laideju" autowire="byName"/> <!-- <bean id="person" class="hellospring.autowire.Person" p:name="laideju" autowire="byType"/> -->
自动装配的缺点:
①. 在 Bean 配置文件里设置 autowire 属性进行自动装配将会装配 Bean 的所有属性。然而,若只希望装配个别属性时,autowire 属性就不够灵活了。
②. autowire 属性要么根据类型自动装配,要么根据名称自动装配,不能两者兼而有之。
③. 在使用 XML 配置时,自动转配用的不多,但在基于注解的配置时,自动装配使用的较多。
3.5. bean之间的关系:继承、依赖
继承 Bean 配置
Spring 允许继承 <bean> 的配置,被继承的 <bean> 称为父 <bean>,进行继承的 <bean> 称为子 <bean>。
以下涉及到的 JavaBean 参考 3.4. Bean 自动装配
(1). 通过parent属性实现<bean>之间的继承,子 <bean> 从父 <bean> 中继承配置,包括父 <bean> 的属性配置。
<bean id="address" class="hellospring.autowire.Address" p:city="WuHan" p:street="XiongChu" /> <!-- 通过设置 parent 属性为当前bean指明其需要继承的父bean,继承之后当前bean将获得父bean的所有可继承的属性 --> <bean id="address2" class="hellospring.autowire.Address" parent="address" />
(2). 子 <bean> 也可以覆盖从父 <bean> 继承过来的配置。
<!-- 当前bean的street配置将被覆盖 --> <bean id="address2" class="hellospring.autowire.Address" p:street="GuanShan" parent="address" />
(3). 父 <bean> 可以作为配置模板,也可以作为 <bean> 实例。若只想把父 <bean> 作为模板,可以设置 <bean> 的 abstract 属性为 true, IOC容器将不会实例化这样的 <bean>(称之为抽象bean)。
<bean id="address" class="hellospring.autowire.Address" p:city="WuHan" p:street="XiongChu" abstract="true" />
试图引用id=address的Bean将会抛出异常:
(4). 可以忽略父 <bean> 的 class 属性,让子 <bean> 指定自己的类,而共享相同的属性配置,但此时 abstract 必须设为 true。
<!-- 抽象bean可以不指定其class属性 --> <bean id="address" p:city="WuHan" p:street="XiongChu" abstract="true" /> <bean id="address2" class="hellospring.autowire.Address" parent="address" />
(5). 并不是 <bean> 元素里的所有属性都会被继承。比如: autowire,abstract 等不会被继承。
依赖 Bean 配置
Spring 允许用户通过 depends-on 属性设定 <bean> 前置依赖的<bean>,前置依赖的 <bean> 会在当前 <bean> 实例化之前创建好。如果前置依赖于多个 <bean>,则可以通过逗号、空格方式配置 依赖 bean 的名称。
<bean id="person" class="hellospring.autowire.Person" p:name="ldj" p:address-ref="address2" depends-on="car1"/> <bean id="car1" class="hellospring.autowire.Car" p:brand="ABC" p:price="120000"/>
注意:虽然在<bean>配置中指明了person依赖于car1,但是这并非等同于 person.car=car1,而是通知 IOC 容器在创建 person 前先创建 car :
3.6. bean的作用域
使用<bean>的scope属性可以设置 Bean实例的作用域。
- singleton:是scope的默认值,表示在整个IOC容器的生命周期中只会创建一个该Bean的实例,而且是在IOC初始化时即创建该Bean的实例。
- prototype:表示在每次请求(例如调用容器的getBean方法)时才创建一个该Bean的实例。
- request:每次http请求都会创建一个新的Bean实例,该作用域仅适用于WebApplicationContext环境。
- session:同一个http session共享一个Bean实例,不同的http session使用不同的Bean,该作用域仅适用于WebApplicationContext环境。
3.7. 使用外部属性文件
在配置文件里配置 Bean 时,有时需要在 Bean 的配置里混入系统部署的细节信息(例如文件路径,数据源配置信息等)。而这些部署细节实际上需要和 Bean 配置相分离。Spring 提供了一个 PropertyPlaceholderConfigurer 的 BeanFactory 后置处理器,这个处理器允许用户将 Bean 配置的部分内容外移到属性文件中。可以在 Bean 配置文件里使用形式为 ${var} 的变量,PropertyPlaceholderConfigurer 从属性文件里加载属性,并使用这些属性来替换变量。Spring 还允许在属性文件中使用 ${propName},以实现属性之间的相互引用。
使用步骤:
定义所需的properties文件:
user=root
password=ldj
jdbcUrl=jdbc://mysql///test
driverClass=com.mysql.jdbc.Driver
在bean配置文件中引入context命名空间:
xmlns:context="http://www.springframework.org/schema/context"
在bean配置文件中进行配置:
<context:property-placeholder location="classpath:db.properties"/> <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="user" value="${user}"></property> <property name="password" value="${password}"></property> <property name="jdbcUrl" value="${jdbcUrl}"></property> <property name="driverClass" value="${driverClass}"></property> </bean>
3.8. 使用SpEL
Spring 表达式语言(简称SpEL),是一个支持运行时查询和操作对象图的表达式语言。语法类似于 EL,SpEL 使用 #{…} 作为定界符,所有在大框号中的字符都将被认为是 SpEL。SpEL 为 <bean> 的属性进行动态赋值提供了便利。
本节讨论中将用到的JavaBean定义如下:
public class Address { private String city; private String street; // getters, setters, toString method and default contructor } public class Car { private String brand; private int price; private double tyrePerimeter; // getters, setters, toString method and default contructor } public class Person { private String name; private Car car; private String city; // 当car.price > 30w 时为金领,否则为白领 private String info; // getters, setters, toString method and default contructor }
(1). SpEL 字面量的表示
整数:<property name="count" value="#{5}"/>
小数:<property name="frequency" value="#{89.7}"/>
科学计数法:<property name="capacity" value="#{1e4}"/>
String 可以使用单引号或者双引号作为字符串的定界符号:<property name="name" value="#{'Chuck'}"/> 或 <property name='name' value='#{"Chuck"}'/>Boolean 值:<property name="enabled" value="#{false}"/>
(2). 引用 Bean、属性和方法
为属性赋值:
<bean id="address" class="hellospring.spel.Address"> <property name="street" value="雄楚大道" /> <!-- 使用SpEL为属性赋一个字面值 --> <property name="city" value="#{'武汉市'}" /> </bean>
调用静态方法或静态属性:通过 T() 调用一个类的静态方法,它将返回一个 Class Object,然后再调用相应的方法或属性:
<bean id="car" class="hellospring.spel.Car"> <property name="brand" value="Audi"/> <property name="price" value="450000" /> <!-- 使用 SpEL 引用类的静态属性 --> <property name="tyrePerimeter" value="#{T(java.lang.Math).PI * 80}" /> </bean>
引用 Bean、属性和方法:
<bean id="person" class="hellospring.spel.Person"> <property name="name" value="Tom" /> <!-- 使用SpEL引用其它Bean --> <property name="car" value="#{car}" /> <!-- 使用SpEL引用其它Bean的属性 --> <property name="city" value="#{address.city}" /> <!-- 在SpEL中使用运算符 --> <property name="info" value="#{car.price > 300000 ? '金领' : '白领'}"></property> </bean>
(3). SpEL支持的运算符号
算数运算符:+, -, *, /, %, ^:
加号还可以用作字符串连接:
比较运算符: <, >, ==, <=, >=, lt, gt, eq, le, ge
逻辑运算符号: and, or, not, |
if-else 运算符:?: (ternary), ?: (Elvis)
if-else 的变体
正则表达式:matches
3.9. IOC 容器中 Bean 的生命周期
Spring IOC 容器可以管理 Bean 的生命周期,在<bean>里通过设置 init-method 和 destroy-method 属性为当前Bean指定初始化和销毁方法。
示例如下:
// JavaBean public class Cat { private String name; public void setName(String name) { System.out.println("name setter called..."); this.name = name; } public String getName() { return name; } public Cat() { System.out.println("ctor called..."); } public String toString() { return "Cat [name=" + name + "]"; } public void init2(){ System.out.println("init-method called..."); } public void destroy1(){ System.out.println("destroy-method called..."); } }
<bean id="cat" class="hellospring.lifecycle.Cat" init-method="init2" destroy-method="destroy1"> <property name="name" value="Tom cat"></property> </bean>
注意:这里的初始化和销毁方法不是JavaBean本身的,而是 IOC 容器在管理该Bean实例时用到的。例如容器在对外提供当前Bean实例之前会先调用为该Bean设置的init方法进行初始化,或者容器在关闭前调用为该Bean设置的destroy方法进行销毁。
在引用配置的 Cat Bean 后,输出效果如下:
public static void main(String[] args) { ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("beans-lifecycle.xml"); Cat cat = (Cat) ctx.getBean("cat"); // 关闭 IOC 容器 ctx.close(); } ctor called... name setter called... init-method called... destroy-method called...
另一方面,Spring 还提供了Bean的后置处理器机制,允许为Bean配置的初始化方法在被调用的前后进行额外的处理。
Bean 后置处理器会对 IOC 容器里的所有 Bean 实例逐一处理,而非单一实例。其典型应用是:检查 Bean 属性的正确性或根据特定的标准更改 Bean 的属性。
通过实现 org.springframework.beans.factory.config.BeanPostProcessor 接口可以定义自己的Bean后置处理器。在初始化方法被调用前后,Spring 将把每个 Bean 实例分别传递给上述接口的以下两个方法:
- Object postProcessAfterInitialization(Object bean, String beanName); // init-method 之前被调用
- Object postProcessBeforeInitialization(Object bean, String beanName); // init-method 之后被调用
在上述两个方法中,形参 bean 是指当前被前置处理器处理的Bean实例本身;beanName是指当前被处理的Bean在<bean>配置中的id属性的值。这两个方法的返回值是实际上返回给请求Bean的方法(例如getBean()方法)的值,可以在这两个方法中修改返回的Bean,甚至返回一个新的Bean。后置处理器的配置:类似普通<bean>的配置,只是不需要id属性,因为 IOC 容器会自行识别。
一个应用自定义后置处理器的示例如下:
// 自定义 Bean 前置处理器 public class MyPostBeanProcessor implements BeanPostProcessor { @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { System.out.println("postProcessAfterInitialization: " + beanName + ", " + bean); if("cat".equals(beanName)){ Cat cat = (Cat) bean; cat.setName("LDJ"); } return bean; } @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { System.out.println("postProcessBeforeInitialization: " + beanName + ", " + bean); return bean; } }
<!-- 配置自定义的Bean后置处理器 --> <bean class="hellospring.lifecycle.MyPostBeanProcessor"/>
// 引用配置了前置处理器的Bean实例 public static void main(String[] args) { ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("beans-lifecycle.xml"); Cat cat = (Cat) ctx.getBean("cat"); System.out.println(cat); // 关闭 IOC 容器 ctx.close(); }
输出结果如下:
总的来说,IOC 容器对于Bean的生命周期管理的过程:
- 通过构造器或工厂方法创建 Bean 实例;
- 为 Bean 的属性设置值和对其它 Bean 的引用;
- 将 Bean 实例传递给 Bean 后置处理器的 postProcessBeforeInitialization 方法;
- 调用 Bean 的初始化方法;
- 将 Bean 实例传递给 Bean 后置处理器的 postProcessAfterInitialization方法;
- Bean 可以使用了;
- 当容器关闭时, 调用 Bean 的销毁方法。
3.10. 再论Bean的配置方式
前文所述中,对于Bean的配置,是通过全类名(<bean>中的class属性)基于反射的方式进行的。下面将学习通过工厂方法(静态工厂方法和实例工厂方法)、FactoryBean等方式来配置Bean。
用到的JavaBean:
public class Car { private String brand; private int price; // getters setters and toString method public Car() {} public Car(String brand, int price) { this.brand = brand; this.price = price; } }
3.10.1 静态工厂方法
所谓“静态工厂方法”是指,通过调用某一个类的静态方法即可返回一个Bean实例的方法。例如下面的 StaticCarFactory.getCar():
public class StaticCarFactory { private static Map<String, Car> cars = new HashMap<>(); static{ cars.put("Audi", new Car("Audi", 300000)); cars.put("Ford", new Car("Ford", 330000)); } // 静态工厂方法 public static Car getCar(String name){ return cars.get(name); } }
要声明通过静态方法创建的Bean,需要在<bean>的class属性里指定拥有该工厂方法的类,同时在factory-method属性里指定工厂方法的名称,最后使用<constrctor-arg>子标签为该静态工厂方法传递所需参数。
<bean id="car1" class="hellospring.factory.StaticCarFactory" factory-method="getCar"> <constructor-arg value="Ford" /> </bean>
3.10.2 实例工厂方法
所谓“实例工厂方法”,是指先实例化工厂本身,在通过工厂实例的相关方法来返回Bean实例的方法。
public class InstanceCarFactory { private Map<String, Car> cars; public InstanceCarFactory(){ cars = new HashMap<>(); cars.put("ford", new Car("Ford", 340000)); cars.put("audi", new Car("Audi", 360000)); } // 实例工厂方法 public Car getCar(String name){ return cars.get(name); } }
要声明通过实例工厂方法创建的Bean,需要:
(1). 先声明一个工厂实例的Bean;
(2). 再声明需要通过实例工厂产生的Bean,包括在 bean 的 factory-bean 属性里指定拥有该工厂方法的Bean,然后在 factory-method 属性里指定该实例工厂方法的名称,最后使用<construtor-arg>子标签为实例工厂方法传递方法参数。
<bean id="instanceCarFactory" class="hellospring.factory.InstanceCarFactory" /> <bean id="car2" factory-bean="instanceCarFactory" factory-method="getCar"> <constructor-arg value="audi"/> </bean>
3.10.3 实现FactoryBean接口在Spring IOC容器中配置Bean
Spring中有两种类型的Bean,一种是普通Bean,另一种是工厂Bean,即FactoryBean。工厂Bean跟普通Bean不同,其返回的对象不是指定类的一个实例,而是该工厂Bean的getObject方法所返回的对象。
import org.springframework.beans.factory.FactoryBean; public class MyBeanFactory implements FactoryBean<Car>{ private String brand; public String getBrand() { return brand; } public void setBrand(String brand) { this.brand = brand; } // 返回Bean的对象 @Override public Car getObject() throws Exception { Car car = new Car(); car.setBrand(brand); return car; } // 返回的Bean的对象的类型 @Override public Class<?> getObjectType() { return Car.class; } // 决定返回的Bean实例是否是单例的 @Override public boolean isSingleton() { return true; } }
通过FactoryBean来配置Bean实例,其中class属性指定某个实现了FactoryBean接口的类,property子标签配置该类的属性(而不是待返回的Bean实例自身的属性)。
<bean id="car1" class="hellospring.beanfactory.MyBeanFactory"> <!-- 注意,这里设置的property是MyBeanFactory的而非Car自身的 --> <property name="brand" value="BMW"></property> </bean>
3.11 基于注解的方式配置Bean
1. 在classpath中扫描组件
组件扫描(component scanning):Spring能够从classpath下自动扫描,侦测和实例化具有特定注解的组件(类)。特定组件包括:
- @Component,基本注解,标识了一个受Spring管理的组件
- @Repository,标识持久层组件
- @Service,标识服务层(业务层)组件
- @Controller,标识表现层组件
注意:以上四个注解其实可以混用,即同一个组件既可以用@Service标注也可以用@Repository标注,因为Spring并不能识别出某个组件是否真的是某个层面的。
import org.springframework.stereotype.Component; import org.springframework.stereotype.Repository; import org.springframework.stereotype.Controller; import org.springframework.stereotype.Service; @Component public class TestObject { // empty class } public interface UserRespository { void save(); } @Repository("userRepository") public class UserRespositoryImpl implements UserRespository { @Override public void save() { System.out.println("UserRespository save..."); } } @Controller public class UserController { public void execute(){ System.out.println("UserController execute..."); } } @Service public class UserService { public void add(){ System.out.println("UserService add..."); } }
当在组件类上使用了特定的注解之后,还需要在Spring的配置文件中声明<context:component-scan>(注意要导入 aop 对应的jar包)。该配置节中base-package属性指定一个需要扫描的基类包,Spring容器将会扫描这个基类包里及其子包中的所有类(当需要扫描多个包时,可以使用逗号分隔)。
<!-- 配置自动扫描的包,需要加入 aop 对应的 jar 包 --> <context:component-scan base-package="hellospring.annotation"></context:component-scan>
对于扫描到的组件,Spring 有默认的命名策略:使用非限定类名且第一个字母小写。也可以在注解中通过value属性值标识组件的名称。
public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext("beans-annotation.xml"); TestObject testObject = (TestObject) ctx.getBean("testObject"); System.out.println(testObject); UserController userController = (UserController) ctx.getBean("userController"); System.out.println(userController); UserRespository useRepository = (UserRespository) ctx.getBean("userRepository"); System.out.println(useRepository); UserService userService = (UserService) ctx.getBean("userService"); System.out.println(userService); }
如果仅希望扫描特定的类而非基包下的所有类,可使用 resource-pattern 属性过滤特定的类,示例:
<!-- 如下配置将只会扫描 hellospring.annotation.controller 包下的所有类 --> <context:component-scan base-package="hellospring.annotation" resource-pattern="controller/*.class"> </context:component-scan> <context:component-scan>还可以包含若干个以下的子节点:<context:include-filter>子节点表示要包含的目标类;<context:exclude-filter>子节点表示要排除在外的目标类。 <context:component-scan base-package="hellospring.annotation"> <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Repository"/> </context:component-scan> <context:component-scan base-package="hellospring.annotation" use-default-filters="false"> <!-- 指定要包含的目标类,需指定 <context:component-scan> 的 use-default-filters=false --> <context:include-filter type="annotation" expression="org.springframework.stereotype.Repository"/> </context:component-scan>
<context:include-filter>和<context:exclude-filter>子节点支持多种类型的过滤表达式:
类别 | 示例 | 说明 |
annotation | hellospring.XxxAnnotation | 所有标注了XxxAnnotation的类。该类型采用目标类是否标注了某个注解进行过滤。 |
assignable | hellospring.XxxService | 所有继承或扩展XxxService的类。该类型采用目标类是否继承或扩展某个特定类进行过滤。 |
aspectj | hellospring.*Service+ | 所有类名以Service结束的类及继承或扩展它们的类。该类型采用AspectJ表达式进行过滤。 |
regex | hellospring.\xxx\.anno\..* | 所有hellospring.xxx.anno包下的类。该类型采用正则表达式根据类的类名进行过滤。 |
custom | hellospring.XxxTypeFilter | 采用XxxTypeFilter通过代码的方式定义过滤规则。该类必须实现org.springframework.core.type.filter.TypeFilter接口 |
注:上表中蓝色标注的,是最常用的两种类型。
2. 组件装配
<context:component-scan>元素还会自动注册AutowiredAnnotationBeanPostProcessor实例,该实例可以自动装配具有@Autowired、@Resource、@Inject注解的属性。
2.1 使用@Autowired自动装配Bean
@Autowired注解自动装配具有兼容类型的单个Bean属性。
(1). 构造器,普通字段(即使是非public),一切具有参数的方法都可以应用@Autowired注解。
@Controller public class UserController { // @Autowired private UserService userService; @Autowired public void confUserService(UserService userService) { this.userService = userService; } public void execute(){ System.out.println("UserController execute..."); userService.add(); } }
(2). 默认情况下,所有使用@Autowired注解的属性都需要被设置。当Spring找不到匹配的Bean装配属性时,会抛出异常,若某一属性允许不被设置,可以设置@Autowired注解的required属性为false。
(3). 默认情况下,当 IOC 容器里存在多个类型兼容的Bean时,通过类型的自动装配将无法工作。此时可以在@Qualifier注解里提供Bean的名称。Spring允许对方法的入参标注@Qualifiter以指定注入Bean的名称。
@Repository public class UserJdbcRepositoryImpl implements UserRespository { @Override public void save() { System.out.println("UserJdbcRepositoryImpl save..."); } } @Service public class UserService { @Autowired // @Qualifier("userRespositoryImpl") @Qualifier("userJdbcRepositoryImpl") private UserRespository userRepository; public void add(){ System.out.println("UserService add..."); userRepository.save(); } }
(4). @Autowired注解也可以应用在数组类型的属性上,此时Spring将会把所有匹配的Bean进行自动装配。
(5). @Autowired注解也可以应用在集合属性上,此时Spring读取该集合的类型信息,然后自动装配所有与之兼容的Bean。
(6). @Autowired注解用在java.util.Map上时,若该Map的键为String,那么Spring将自动装配与之Map值类型兼容的Bean,此时Bean的名称作为键值。
2.2 使用@Resource或@Inject自动装配Bean
Spring还支持@Resource和@Inject注解,这两个注解和@Autowired注解的功用类似。
(1). @Resource注解要求提供一个Bean名称的属性,若该属性为空,则自动采用标注处的变量或方法名作为Bean的名称。
(2). @Inject和@Autowired注解一样也是按类型匹配注入的Bean,但没有reqired属性。
建议使用@Autowired注解。
3.12 泛型依赖注入
Spring 4.x 中可以为子类注入子类对应的泛型类型的成员变量的引用。
public class BaseService<T> { // 在此处指明自动装配,以使得子类能继承得到 @Autowired protected BaseRepository<T> repository; public void add(){ System.out.println("BaseService add..."); System.out.println(repository); } } public class BaseRepository<T> { public void save(){ System.out.println("BaseRepository save..."); } } public class User { // nothing more... } @Service public class UserService extends BaseService<User>{ // nothing more... } @Repository public class UserRepository extends BaseRepository<User>{ // nothing more... } public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext("beans-generic-di.xml"); UserService userService = (UserService) ctx.getBean("userService"); userService.add(); }
beans-generic-di.xml中的配置如下:(仅仅是指明了自动装配)
<context:component-scan base-package="hellospring.generic.di"></context:component-scan>
3.13 整合多个配置文件
Spring允许通过<import>将多个配置文件引入到一个文件中,进行配置文件的集成。这样在启动Spring容器时,仅需要指定这个合并好的配置文件即可。
<import>标签的resource属性支持Spring的标准的路径资源,如下表所示:
地址前缀 | 示例 | 对应资源类型 |
classpath: | classpath:spring-mvc.xml | 从类路径下加载资源,classpath:和classpath:/是等价的 |
file: | file:/conf/security/spring-shiro.xml | 从文件系统目录中加载资源,可采用绝对或相对路径 |
http:// | http://www.xxx.com/resource/beans.xml | 从WEB服务器中加载资源 |
ftp:// | ftp://www.xxx.com/resource/beans.xml | 从FTP服务器中加载资源 |
四、Spring AOP
1. 使用动态代理实现 AOP 编程思想
假设有如下的Java类
现在为了实现如下两个功能:
需求1-日志,在程序执行期间追踪正在发生的活动;
需求2-验证,希望计算器只能处理正数的运算。
需要对代码做修改,如下所示:
如此一来,会多出如下的问题。
代码混乱:越来越多的非业务需求(日志和验证等)加入后,原有的业务方法急剧膨胀。每个方法在处理核心逻辑的同时还必须兼顾其他多个关注点。
代码分散:以日志需求为例,只是为了满足这个单一需求,就不得不在多个模块(方法)里多次重复相同的日志代码。如果日志需求发生变化,必须修改所有模块。
为了解决上面的问题,可以使用动态代理(设计模式)。其原理是,使用一个代理将对象包装起来,然后用该代理对象取代原始对象,任何对原始对象的调用都要通过代理,代理对象决定是否以及何时将方法调用转到原始对象上。
具体实现和测试代码如下。
public interface ArithmeticCalculator { int add(int i, int j); int sub(int i, int j); int mul(int i, int j); int div(int i, int j); } public class ArithmeticCalculatorImpl implements ArithmeticCalculator { @Override public int add(int i, int j) { return i+j; } ... } public class ArithmeticCalculatorLoggingProxy { // 要代理的对象 private ArithmeticCalculator target; public ArithmeticCalculatorLoggingProxy(ArithmeticCalculator target) { this.target = target; } public ArithmeticCalculator getLoggingProxy(){ ArithmeticCalculator proxy = null; // 负责加载被代理对象的类加载器 ClassLoader loader = target.getClass().getClassLoader(); // 代理对象的类型,即其中所包含的方法 Class[] interfaces = new Class[]{ArithmeticCalculator.class}; // 当代理对象中的方法被调用时,额外执行的代码 InvocationHandler h = new InvocationHandler() { /* * proxy: 正在返回的代理对象,一般情况下,在invoke方法中都不使用该对象。 * method:正在被调用的方法 * args: 调用方法时,传入的参数 */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String methodName=method.getName(); // logging goes here System.out.println("The method " + methodName + " begins with " + Arrays.asList(args)); try{ // 前置通知执行 Object result = method.invoke(target, args); // 返回通知执行 }catch(Exceptoin e){ // 异常通知执行 } // 后置通知执行 System.out.println("The method " + methodName + " ends"); return result; } }; proxy = (ArithmeticCalculator) Proxy.newProxyInstance(loader, interfaces, h); return proxy; } } public static void main(String[] args) { ArithmeticCalculator target = new ArithmeticCalculatorImpl(); ArithmeticCalculator proxy = new ArithmeticCalculatorLoggingProxy(target).getLoggingProxy(); int result = proxy.add(1, 2); System.out.println("-->"+result); result = proxy.div(4, 2); System.out.println("-->"+result); }
2. AOP 简介
AOP(Aspect-Oriented Programming,面向切面编程):是一种新的方法论,是对传统 OOP(Object-Oriented Programming,面向对象编程)的补充。AOP的主要编程对象是切面(aspect),而切面即模块化横切关注点。
在应用AOP编程时,仍然需要定义公共功能,但可以明确的定义这个功能在哪里,以什么方式应用,并且不必修改受影响的类。这样一来横切关注点就被模块化到特殊的对象(切面)里。AOP的好处:每个事物逻辑位于一个位置,代码不分散,便于维护和升级;业务模块更简洁,只包含核心业务代码。
AOP相关的术语
- 切面(Aspect):横切关注点(跨越应用程序多个模块的功能)被模块化的特殊对象。
- 通知(Advice):切面必须要完成的工作。
- 目标(Target):被通知的对象。
- 代理(Proxy):向目标对象应用通知之后创建的对象。
- 连接点(Joinpoint):程序执行的某个特定位置,如类某个方法调用前、调用后、方法抛出异常后等。连接点由两个信息确定:方法表示的程序执行点;相对点表示的方位。例如 ArithmethicCalculator.add() 方法执行前的连接点,执行点为 ArithmethicCalculator.add(),方位为该方法执行前的位置。
- 切点(pointcut):每个类都拥有多个连接点,例如 ArithmethicCalculator 的所有方法实际上都是连接点,即连接点是程序类中客观存在的事务。AOP 通过切点定位到特定的连接点。类比:连接点相当于数据库中的记录,切点相当于查询条件。切点和连接点不是一对一的关系,一个切点匹配多个连接点,切点通过org.springframework.aop.Pointcut接口进行描述,它使用类和方法作为连接点的查询条件。
3. Spring中的AOP
推荐使用AspectJ,它是Java社区里最完整最流行的AOP框架。在Spring2.0以上版本中,可以使用基于AspectJ注解或基于XML配置的 AOP。
3.1 在Spring中启用AspectJ注解支持
要在Spring应用中使用AspectJ注解,必须:
①. 在类路径下包含AspectJ类库:aopalliance.jar,aspectj.weaver.jar,spring-aspects.jar;
②. 将AOP Schema添加到<beans>根元素中;
③. 启用AspectJ注解支持,在Bean配置文件中定义一个空的XML元素:<aop:aspectj-autoproxy />。
当Spring IOC容器侦测到Bean配置文件中的<aop:aspectj-autoproxy>元素时,会自动为与AspectJ切面匹配的Bean创建代理。
3.2 用 AspectJ 注解声明切面
要在Spring中声明AspectJ切面,只需要在IOC容器中将切面声明为Bean实例。当在Spring IOC容器中初始化AspectJ切面之后,Spring IOC容器就会为那些与AspectJ切面相匹配的Bean创建代理。
在AspectJ注解中,切面只是一个带有@Aspect注解的Java类。通知是标注有某种注解的简单的Java方法,AspectJ支持5种类型的通知注解:
- @Before,前置通知,在方法执行之前执行。
- @After,后置通知,在方法执行之后执行而不管目标方法是否正常执行完,所以在该通知中无法访问目标方法的返回值。
- @AfterRunning,返回通知,在方法返回结果之后执行,在该通知中可以访问到返回值。
- @AfterThrowing,异常通知,在方法抛出异常之后
- @Around,环绕通知,围绕着方法执行
- 引入通知,不常用
示例如下
定义日志切面 LoggingAspect 类,为了使其被 IOC 容器管理,需要添加 @Component 注解,而要让其成为一个切面,需要添加 @Aspect 注解。
package hellospring.aop.annotation; import java.util.Arrays; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.AfterThrowing; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; /* * 定义日志切面 LoggingAspect 类,为了使其被 IOC 容器管理,需要添加 @Component 注解, * 而要让其成为一个切面,需要添加 @Aspect 注解。 */ @Component @Aspect public class LoggingAspect { // 前置通知,logBefore称为通知方法,通过其形参JoinPoint可以访问到目标方法的细节,如方法名、参数、返回值等。 @Before("execution(int hellospring.aop.annotation.ArithmeticCalculator.*(int, int))") public void logBefore(JoinPoint jp){ String methodName = jp.getSignature().getName(); System.out.println("The method " + methodName + " starts with " + Arrays.asList(jp.getArgs())); } // 后置通知 @After("execution(int hellospring.aop.annotation.ArithmeticCalculator.*(int, int))") public void logAfter(JoinPoint jp){ String methodName = jp.getSignature().getName(); System.out.println("The method " + methodName + " ends"); } // 返回通知,returning参数指明目标方法返回值对象的名字(引用),需要和通知方法中的形参对应,以便在通知方法中可以访问到目标方法的返回值对象 @AfterReturning(value="execution(int hellospring.aop.annotation.ArithmeticCalculator.*(int, int))",returning="result") public void logAfterReturning(JoinPoint jp, Object result){ String methodName = jp.getSignature().getName(); System.out.println("The method " + methodName + " ends with " + result); } // 异常通知,throwing参数指明目标方法中抛出的异常对象的名字(引用),需要注意的是: // 1. throwing参数的值需要同通知方法中形参名一致,以便在通知方法中访问到目标方法抛出的异常; // 2. 通知方法形参中,异常的类型需要兼容目标方法中实际抛出异常的类型,否则无法初始化该形参。 @AfterThrowing(value="execution(int hellospring.aop.annotation.ArithmeticCalculator.*(int, int))", throwing="ex") public void logException(JoinPoint jp, Exception ex){ String methodName = jp.getSignature().getName(); System.out.println("The method " + methodName + " occurs exception: " + ex); } /* 环绕通知,需携带ProceedingJoinPoint类型参数 * 环绕通知类似于动态代理的全过程: ProceedingJoinPoint 类型的参数可以决定是否执行目标方法。 * 且环绕通知必须有返回值,否则会出现空指针异常,返回值即为目标方法的返回值。 */ /* @Around(value="execution(* hellospring.aop.annotation.ArithmeticCalculator.*(..))") public Object around(ProceedingJoinPoint pjd){ Object result = null; String methodName = pjd.getSignature().getName(); try { //前置通知 System.out.println("The method " + methodName + " starts with " + Arrays.asList(pjd.getArgs())); //执行目标方法 result = pjd.proceed(); //返回通知 System.out.println("The method " + methodName + " ends with" + result); } catch (Throwable e) { //异常通知 System.out.println("The method " + methodName + " occurs exception: " + e); throw new RuntimeException(e); } //后置通知 System.out.println("The method " + methodName + " ends"); return result; }*/ }
配置文件中:
<context:component-scan base-package="hellospring.aop.annotation"></context:component-scan> <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
3.2.1 切入点表达式
在LogginAspect类中,形如“execution(int hellospring.aop.annotation.ArithmeticCalculator.*(int, int))”称为切入点表达式。在这个示例中,切入点表达式是根据方法的签名来匹配各种方法的:
①. execution(* hellospring.aop.annotation.ArithmeticCalculator.*(..)),匹配hellospring.aop.annotation.ArithmeticCalculator中声明的所有方法,第一个*代表任意修饰符以及任意返回值,第二个*代表任意方法,而“..”匹配任意数量任意类型的参数。如果目标类、接口与该切面类在同一个包中,则可以省略包名。
②. execution(public * ArithmeticCalculator.*(..)),匹配ArithmeticCalculator接口中的所有公共方法。
③. execution(public double ArithmeticCalculator.*(..)),匹配ArithmeticCalculator接口中的所有返回double类型的公共方法。
④. execution(public double ArithmeticCalculator.*(double, ..)),匹配ArithmeticCalculator接口中的所有第一个参数为double型,且返回值为double型的公共方法,“..”匹配任意数量任意类型的参数。
⑤. execution(public double ArithmeticCalculator.*(double, double)),匹配ArithmeticCalculator接口中所有只有两个double类型参数且返回值为double型的公共方法。
可以使用操作符&&、||、!将多个切入点表达式结合起来:
@Before("execution(* *.add(int, ..)) || execution(* *.sub(int, ..))") public void logBefore(JoinPoint jp){ //... }
3.2.2 指定切面的优先级
在同一个连接点上应用不止一个切面时,除非明确指定,否则它们的优先级是不确定的。切面的优先级可以通过实现 Ordered 接口或利用 @Order 注解指定。实现 Ordered 接口,getOrder() 方法的返回值越小,优先级越高。若使用 @Order 注解,序号出现在注解中,值越小优先级越高。
@Order(1) @Aspect @Component public class ValidatorAspect { @Before("execution(* hellospring.aop.annotation.ArithmeticCalculator.*(..))") public void validate(JoinPoint jp){ System.out.println("--> validate: " + Arrays.asList(jp.getArgs())); } }
3.2.3 重用切入点定义
在编写 AspectJ 切面时,可以直接在通知注解中书写切入点表达式。但同一个切点表达式可能会在多个通知中重复出现,为了重复利用切点表达式,可以通过 @Pointcut 注解将一个切入点声明成简单的方法。该切入点的方法体通常是空的,因为将切入点定义与应用程序逻辑混在一起是不合理的。
切入点方法的访问控制符同时也控制着这个切入点的可见性。如果切入点要在多个切面中共用,最好将它们集中在一个公共的类中。在这种情况下,它们必须被声明为 public。在引入这个切入点时,必须将类名也包括在内。如果类没有与这个切面放在同一个包中,还必须包含包名。其他通知可以通过方法名称引入该切入点。
@Order(2) @Component @Aspect public class LoggingAspect { /** * 定义一个方法,用于声明切入点表达式。 一般地, 该方法中再不需要添入其他的代码。 * 使用 @Pointcut 来声明切入点表达式。 * 后面的其他通知直接使用方法名来引用当前的切入点表达式。 */ @Pointcut("execution(int hellospring.aop.annotation.ArithmeticCalculator.*(..))") public void declareJointPointExpression(){} /* * 同一个类中复用切入点方法 */ @Before("declareJointPointExpression()") public void logBefore(JoinPoint jp){ String methodName = jp.getSignature().getName(); System.out.println("The method " + methodName + " starts with " + Arrays.asList(jp.getArgs())); } // ... } @Order(1) @Aspect @Component public class ValidatorAspect { // 不同类总复用切入点方法 @Before("hellospring.aop.annotation.LoggingAspect.declareJointPointExpression()") public void validate(JoinPoint jp){ System.out.println("--> validate: " + Arrays.asList(jp.getArgs())); } }
3.3 基于 XML 的配置声明切面
通过Spring Bean的配置文件也可以实现切面的声明。但是,通过 AspectJ 注解,切面可以与 AspectJ 兼容,而基于 XML 配置的声明则是 Spring 专有。由于 AspectJ 得到越来越多的 AOP 框架支持,所以以注解风格编写的切面将会有更多重用的机会。正常情况下,基于注解的声明要优先于基于 XML 的声明。当使用XML声明切面时,需要在 <beans> 根元素中导入 aop Schema:xmlns:aop="http://www.springframework.org/schema/aop" 。
在 Bean 配置文件中,所有的 Spring AOP 配置都必须定义在 <aop:config> 元素内部。对于每个切面而言,都要创建一个 <aop:aspect> 元素来为具体的切面实现引用后端 Bean 实例。切面 Bean 必须有一个标示符,供 <aop:aspect> 元素引用。
<!-- 配置Bean --> <bean id="arithmeticCalculator" class="hellospring.aop.xml.ArithmeticCalculatorImpl"></bean> <!-- 配置切面Bean --> <bean id="loggingAspect" class="hellospring.aop.xml.LoggingAspect" /> <bean id="validatorAspect" class="hellospring.aop.xml.ValidatorAspect" /> <!-- 配置AOP --> <aop:config> <!-- 配置切入点表达式 --> <aop:pointcut expression="execution(int hellospring.aop.xml.ArithmeticCalculator.*(..))" id="pointcut1"/> <!-- 配置切面及通知 --> <aop:aspect ref="loggingAspect" order="2"> <!-- 指定各个通知的细节 --> <aop:before method="logBefore" pointcut-ref="pointcut1"/> <aop:after method="logAfter" pointcut-ref="pointcut1"/> <aop:after-returning method="logAfterReturning" pointcut-ref="pointcut1" returning="result"/> <aop:after-throwing method="logException" pointcut-ref="pointcut1" throwing="e"/> <!-- <aop:around method="around" pointcut-ref="pointcut1" /> --> </aop:aspect> <aop:aspect ref="validatorAspect" order="1"> <aop:before method="validate" pointcut-ref="pointcut1"/> </aop:aspect> </aop:config>
需要注意的是:
(1)切入点(<aop:pointcut>)必须定义在 <aop:aspect> 元素下,或者直接定义在 <aop:config> 元素下,定义在 <aop:aspect> 元素下则只对当前切面有效,定义在 <aop:config> 元素下是对所有切面都有效;
(2)基于 XML 的 AOP 配置不允许在切入点表达式中用名称引用其他切入点。
五、Spring对JDBC的支持
1. JdbcTemplate
为了使 JDBC 更加易于使用,Spring 在 JDBC API 上定义了一个抽象层,以此建立一个 JDBC 存取框架。作为 Spring JDBC 框架的核心,JDBC 模板的设计目的是为不同类型的 JDBC 操作提供模板方法。每个模板方法都能控制整个过程,并允许覆盖过程中的特定任务。通过这种方式,可以在尽可能保留灵活性的情况下,将数据库存取的工作量降到最低。
JdbcTemplate 类被设计成为线程安全的,所以可以在IOC容器中声明它的单个实例,并将这个实例注入到所有的 DAO 实例中。
实例,使用JdbcTemplate
配置
<!-- 导入资源文件 --> <context:property-placeholder location="classpath:db.properties"/> <!-- 配置c3p0数据源 --> <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="user" value="${jdbc.user}" /> <property name="password" value="${jdbc.password}" /> <property name="jdbcUrl" value="${jdbc.url}" /> <property name="driverClass" value="${jdbc.driverClass}" /> <property name="initialPoolSize" value="5"/> <property name="maxPoolSize" value="10"/> </bean> <!-- 配置Spring的JdbcTemplate --> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate" > <property name="dataSource" ref="dataSource" /> </bean>
db.properties中的内容如下:
jdbc.user=root
jdbc.password=123
jdbc.url=jdbc:mysql:///hellospring
jdbc.driverClass=com.mysql.jdbc.Driver
jdbc.initialPoolSize=5
jdbc.maxPoolSize=5
实体类:
public class Employee { private Integer id; private String lastName; private String email; // ... } public class Department { private Integer id; private String name; // ... }
Junit 测试类:
public class SpJdbcTest { private ApplicationContext ctx = null; private JdbcTemplate jdbcTemplate = null; { ctx = new ClassPathXmlApplicationContext("spring-jdbc-test.xml"); jdbcTemplate = (JdbcTemplate) ctx.getBean("jdbcTemplate"); } @Test public void testDataSource() throws SQLException { DataSource ds = (DataSource) ctx.getBean(DataSource.class); System.out.println(ds.getConnection()); } /** * 执行 INSERT, UPDATE, DELETE */ @Test public void testUpdate(){ String sql = "UPDATE employees SET last_name = ? WHERE id = ?"; jdbcTemplate.update(sql, "Laideju", 1); } /** * 执行批量更新: 批量的 INSERT, UPDATE, DELETE * 最后一个参数是 Object[] 的 List 类型: 因为修改一条记录需要一个 Object 的数组, 那么多条不就需要多个 Object 的数组吗 */ @Test public void testBatchUpdate(){ String sql = "INSERT INTO employees(last_name, email) values(?,?)"; List<Object[]> batchArgs = new ArrayList<>(); batchArgs.add(new Object[]{"AA", "aa@sina.com"}); batchArgs.add(new Object[]{"BB", "bb@sina.com"}); batchArgs.add(new Object[]{"CC", "cc@sina.com"}); batchArgs.add(new Object[]{"DD", "dd@sina.com"}); batchArgs.add(new Object[]{"EE", "ee@sina.com"}); jdbcTemplate.batchUpdate(sql, batchArgs); } /** * 单行查询 * 从数据库中获取一条记录,实际得到对应的一个对象。 * 注意不是调用 queryForObject(String sql, Class<Employee> requiredType, Object... args) 方法! * 而需要调用 queryForObject(String sql, RowMapper<Employee> rowMapper, Object... args) * 1. 其中的 RowMapper 指定如何去映射结果集的行,常用的实现类为 BeanPropertyRowMapper * 2. 使用 SQL 中列的别名完成列名和类的属性名的映射,例如 last_name lastName * 3. 不支持级联属性,JdbcTemplate 到底是一个 JDBC 的小工具,而不是 ORM 框架 */ @Test public void testQueryForOneRow(){ String sql = "SELECT id, last_name lastName, email FROM employees WHERE id = ?"; RowMapper<Employee> rowMapper = new BeanPropertyRowMapper<>(Employee.class); Employee emp = jdbcTemplate.queryForObject(sql, rowMapper, 1); System.out.println(emp); } /** * 获取单个列的值,或做统计查询 * 使用 queryForObject(String sql, Class<Long> requiredType) */ @Test public void testQueryForField(){ String sql = "SELECT count(id) FROM employees"; long count = jdbcTemplate.queryForObject(sql, Long.class); System.out.println(count); } /** * 查到实体类的集合 * 注意调用的不是 queryForList 方法 */ @Test public void testQueryForList(){ String sql = "SELECT id, last_name lastName, email FROM employees WHERE id > ?"; RowMapper<Employee> rowMapper = new BeanPropertyRowMapper<>(Employee.class); List<Employee> emps = jdbcTemplate.query(sql, rowMapper, 8); System.out.println(emps); } @Test public void testQueryForList2(){ String sql = "SELECT email FROM employees WHERE id > ?"; List<String> emails = jdbcTemplate.queryForList(sql, String.class, 8); System.out.println(emails); } }
Spring JDBC 框架还提供了一个 JdbcDaoSupport 类来简化 DAO 实现。该类声明了 jdbcTemplate 属性,它可以从 IOC 容器中注入,或者自动从数据源中创建。但用的不多!
1.1 在 JDBC 模板中使用命名参数
在经典的 JDBC 用法中,SQL 参数是用占位符 ? 表示,并且受到位置的限制。定位参数的问题在于,一旦参数的顺序发生变化,就必须改变参数绑定。在 Spring JDBC 框架中,绑定 SQL 参数的另一种选择是使用命名参数。
命名参数:SQL 按名称(以冒号开头)而不是按位置进行指定。命名参数更易于维护,也提升了可读性。命名参数由框架类在运行时用占位符取代。命名参数只在 NamedParameterJdbcTemplate 中得到支持。
在 SQL 语句中使用命名参数时,可以在一个 Map 中提供参数值,参数名为键。
@Test public void testNamedParameterJdbcTemplate(){ String sql = "INSERT INTO employees(last_name, email) VALUES(:ln, :email)"; Map<String, Object> paramMap = new HashMap<>(); paramMap.put("ln", "FF"); paramMap.put("email", "ff@sina.com"); // namedParameterJdbcTemplate 已在Spring Bean配置文件中定义 namedParameterJdbcTemplate.update(sql, paramMap); }
上例中namedParameterJdbcTemplate事前已在Spring Bean配置文件中定义:
<!-- 配置 NamedParameterJdbcTemplate,该对象可以使用具名参数,其没有无参数的构造器,所以必须为其构造器指定参数 --> <bean id="namedParameterJdbcTemplate" class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate"> <constructor-arg ref="dataSource" /> </bean>
使用命名参数时也可以使用SqlParameterSource参数。
@Test public void testNamedParameterJdbcTemplate2(){ String sql = "INSERT INTO employees(last_name, email) VALUES(:lastName, :email)"; Employee employee = new Employee(); employee.setLastName("XYZ"); employee.setEmail("xyz@sina.com"); SqlParameterSource paramSource = new BeanPropertySqlParameterSource(employee); namedParameterJdbcTemplate.update(sql, paramSource); }
使用SqlParameterSource参数时,需要注意:
①. SQL 语句中的参数名和类的属性名要一致;
②. 使用 SqlParameterSource 的 BeanPropertySqlParameterSource 实现类作为参数。
批量更新时可以提供 Map 或 SqlParameterSource 的数组。
2. Spring中的事务管理
Spring 既支持编程式事务管理,也支持声明式的事务管理。
编程式事务管理:将事务管理代码嵌入到业务方法中来控制事务的提交和回滚。在编程式管理事务时,必须在每个事务操作中包含额外的事务管理代码。
声明式事务管理:大多数情况下比编程式事务管理更好用。它将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理。事务管理作为一种横切关注点,可以通过 AOP 方法模块化。Spring 通过 Spring AOP 框架支持声明式事务管理。
Spring 声明式事务管理支持注解和基于 XML 配置的两种方式来设置声明式事务。
2.1 基于注解的方式
(1). 在Spring配置文件中配置事务管理器
<!-- 配置事务管理器 --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"></property> </bean>
注意:①. class属性指定的类应根据事务底层的具体实现来选取,例如使用JDBC原生的事务管理class应指定DataSourceTransactionManager,而使用Hibernate的事务管理则应将class设置为HibernateTransactionManager,如果使用JPA的事务管理则class应指定为JpaTransactionManager,等。②. 需要为事务管理器Bean配置必须的dataSource属性。
(2). 为Spring应用启用事务注解
<!-- 启用事务注解 --> <tx:annotation-driven transaction-manager="transactionManager"/>
(3). 使用事务注解
在需要添加事务的方法上添加@Transactional注解:
@Transactional(propagation=Propagation.REQUIRES_NEW, isolation=Isolation.READ_COMMITTED) @Override public void purchase(String username, String isbn) { // 1. 获取书的单价 int price = bookShopDao.findBookPriceByIsbn(isbn); // 2. 更新数的库存 bookShopDao.updateBookStock(isbn); // 3. 更新用户余额 bookShopDao.updateUserAccount(username, price); }
@Transactional注解的可选参数如下:
- propagation:指定事务的传播行为,即当前的事务方法被另外一个事务方法调用时如何使用事务。默认取值为 REQUIRED,即使用调用方法的事务,也常使用REQUIRES_NEW,即使用当前事务方法自身的事务,此时调用方法的事务会被挂起。
- isolation:指定事务的隔离级别,最常用的取值为 READ_COMMITTED 。
- noRollbackFor:指定对哪些异常不进行回滚,默认情况下Spring的声明式事务对所有的运行时异常进行回滚。通常情况下取默认值即可。
- rollbackFor:指定对对哪些异常必须进行回滚。noRollbackFor 和 rollbackFor都是Class[]类型的属性,所以可以同时指定多个异常类。
- readOnly:指定事务是否为只读,表示这个事务只读取数据但不更新数据,这样可以帮助数据库引擎优化事务。若真的是一个只读取数据库值的方法,应设置readOnly=true 。
- timeout:指定强制回滚之前事务可以占用的时间,以秒为单位。
关于事务的传播行为
假设有如下的调用:
@Transactional public void checkout(String username, List<String> isbns) { for(String isbn : isbns){ bookShopService.purchase(username, isbn); } }
其中bookShopService.purchase()方法即上例中的purchase()方法。当purchase()使用“propagation=REQUIRED”时,事务的传播示意图如下所示:
当purchase()使用“propagation=REQUIRED”时,事务的传播示意图如下所示:
2.2 基于XML配置文件的方式
不同于注解的方式,基于XML配置的方式只需要在Spring配置文件中进行恰当配置即可,示例如下:
<!-- 1.配置Spring的事务管理器 --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"></property> </bean> <!-- 2.配置事务属性,需要事务管理器 --> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <!-- 根据方法名指定事务的属性 --> <tx:method name="purchase" propagation="REQUIRES_NEW"/> <!-- 以get和find开头的方法一般是只读取数据库的方法 --> <tx:method name="get*" read-only="true"/> <tx:method name="find*" read-only="true"/> <!-- 其余方法采用默认值 --> <tx:method name="*"/> </tx:attributes> </tx:advice> <!-- 3.配置事务切入点,以及把事务切入点和事务属性关联起来 --> <aop:config> <aop:pointcut expression="execution(* hellospring.tx.xmlway.services.*.*(..))" id="txPointCut"/> <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut"/> </aop:config>
事务的诸多属性,如isolation、timeout、rollbackFor等等,可以在<tx:method>节点上进行配置。
六、Spring整合Hibernate
Spring支持大多数流行的ORM框架,包括Hibernate JDO、TopLink、Mybatis和JPA。并且Spring对这些ORM框架的支持是一致的,因此可以把和Hibernate整合技术应用到其他ORM框架上。Spring 2.0同时支持Hibernate 2.x 和 3.x,但 Spring 2.5只支持Hibernate 3.1或更高版本。Spring整合Hibernate的目的,即让IOC容器来管理Hibernate的SessionFactory,让Hibernate使用上Spring的声明式事务。
1.整合的步骤
(1). 加入Hibernate
①. 导入Hibernate的jar包。
②. 添加Hibernate的配置文件:hibernate.cfg.xml。因为Hibernate中使用到的DataSource对象也将由Spring IOC进行管理,所以在hibernate的配置文件中不再需要关于数据源的配置。
<hibernate-configuration> <session-factory> <!-- 配置 hibernate 的基本属性 --> <!-- 1. 数据源需配置到 IOC 容器中, 所以在此处不再需要配置数据源 --> <!-- 2. 关联的 .hbm.xml 也在 IOC 容器配置 SessionFactory 实例时再进行配置 --> <!-- 3. 配置 hibernate 的基本属性: 方言, SQL 显示及格式化, 生成数据表的策略以及二级缓存等. --> <property name="hibernate.dialect">org.hibernate.dialect.MySQL5InnoDBDialect</property> <property name="hibernate.show_sql">true</property> <property name="hibernate.format_sql">true</property> <property name="hibernate.hbm2ddl.auto">update</property> </session-factory> </hibernate-configuration>
③. 编写了持久化类对应的“.hbm.xml”文件。
(2). 加入Spring
①. 导入Spring相关的jar包。
②. 加入Spring的配置文件。
(3). 整合:在Srping的配置文件中进行配置。
<!-- 配置自动扫描的包 --> <context:component-scan base-package="hellospring.hibernate"></context:component-scan> <!-- 配置数据源 --> <!-- 导入资源文件 --> <context:property-placeholder location="classpath:db-spring-hibernate.properties"/> <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="user" value="${jdbc.user}"></property> <property name="password" value="${jdbc.password}"></property> <property name="driverClass" value="${jdbc.driverClass}"></property> <property name="jdbcUrl" value="${jdbc.jdbcUrl}"></property> <property name="initialPoolSize" value="${jdbc.initialPoolSize}"></property> <property name="maxPoolSize" value="${jdbc.maxPoolSize}"></property> </bean> <!-- 配置 Hibernate 的 SessionFactory 实例: 通过 Spring 提供的 LocalSessionFactoryBean 进行配置 --> <bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean"> <!-- 配置数据源属性 --> <property name="dataSource" ref="dataSource" /> <!-- 配置 hibernate 配置文件的位置及名称 --> <property name="configLocation" value="classpath:hibernate.cfg.xml"></property> <!-- 也可以使用 hibernateProperties 来配置 Hibernate 原生的属性 --> <!-- <property name="hibernateProperties"> <props> <prop key="hibernate.dialect">org.hibernate.dialect.MySQL5InnoDBDialect</prop> <prop key="hibernate.show_sql">true</prop> <prop key="hibernate.format_sql">true</prop> <prop key="hibernate.hbm2ddl.auto">update</prop> </props> </property> --> <!-- 配置 hibernate 映射文件的位置及名称, 可以使用通配符 --> <property name="mappingLocations" value="classpath:hellospring/hibernate/entities/*.hbm.xml" /> </bean> <!-- 配置 Spring 的声明式事务 --> <bean id="transactionManager" class="org.springframework.orm.hibernate5.HibernateTransactionManager"> <property name="sessionFactory" ref="sessionFactory"></property> </bean> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <tx:method name="get*" read-only="true"/> <tx:method name="purchase" propagation="REQUIRES_NEW"/> <tx:method name="*"/> </tx:attributes> </tx:advice> <aop:config> <aop:pointcut expression="execution(* hellospring.hibernate.services.*.*(..))" id="txPointCut"/> <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut"/> </aop:config>
2. 注意小项
Spring hibernate 事务的流程
(1)在方法开始之前:
①. 获取Session;
②. 把Session和当前线程绑定,这样就可以在Dao中使用SessionFactory的getCurrentSession()方法来获取Session了;
③. 开启事务。
(2)若方法正常结束,即没有出现异常,则:
①. 提交事务;
②. 使和当前线程绑定的Session解除绑定;
③. 关闭Session。
(3)若方法出现异常,则:
①. 回滚事务;
②. 使和当前线程绑定的Session解除绑定;
③. 关闭Session。
不推荐使用 HibernateTemplate 和 HibernateDaoSupport,因为这样会导致 Dao 和 Spring 的 API 进行耦合,可以移植性变差。
七、Spring整合Struts2
Spring整合Struts2实际上涉及到另外一个问题:如何在Web环境下使用Spring。下面将先解释这个问题,整合Struts2本质上与将Spring应用到Web环境中并无不同。
1. 在WEB应用中使用Spring
(1). 为Spring加入WEB环境必须的jar包:spring-web-4.0.0.RELEASE.jar,spring-webmvc-4.0.0.RELEASE.jar。
(2). WEB环境下Spring的配置文件与普通的Spring应用的配置文件一致。
(3). 创建IOC容器。普通的Spring应用可以直接在main方法中创建容器,而对于WEB应用来说,一般是在WEB应用被服务器加载时创建容器,具体而言是在ServletContextListener.contextInitialized(ServletContextEvent sce)方法中创建。为了使创建IOC容器的监听器生效,还需在web.xml中进行如下配置:
<!-- 启动 IOC 容器的 ServletContextListener --> <listener> <listener-class>springstruts2.listeners.SpringServletContextListener</listener-class> </listener>
(4). 在WEB应用的其他组件中访问IOC容器:在监听器中创建IOC容器后,就把其放入到ServletContext(即application域)的一个属性中。实际上,Spring 配置文件的名字和位置应该也是可配置的,将其配置到当前 WEB 应用的初始化参数中较为合适。
<!-- 配置Spring配置文件的名字和位置 --> <context-param> <param-name>configLocation</param-name> <param-value>applicationContext.xml</param-value> </context-param>
public class SpringServletContextListener implements ServletContextListener { public void contextInitialized(ServletContextEvent arg0) { //1. 获取 Spring 配置文件的名称. ServletContext servletContext = arg0.getServletContext(); String config = servletContext.getInitParameter("configLocation"); //1. 创建 IOC 容器 ApplicationContext ctx = new ClassPathXmlApplicationContext(config); //2. 把 IOC 容器放在 ServletContext 的一个属性中. servletContext.setAttribute("ApplicationContext", ctx); } public void contextDestroyed(ServletContextEvent arg0) { } public SpringServletContextListener() { } }
事实上,Spring已经提供相关的组件实现了上述思想,直接配置使用即可从而避免再次编写监听器。
<!-- 配置 Spring 配置文件的名称和位置 --> <context-param> <param-name>contextConfigLocation</param-name> <!-- 因为是给Spring内部组件使用,所以要使用classpath前缀 --> <param-value>classpath:applicationContext.xml</param-value> </context-param> <!-- 启动 IOC 容器的 ServletContextListener --> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener>
2. Spring整合Struts2
整合目的:使IOC容器来管理Struts2的Action。
整合步骤:
(1). 正常加入Struts2,即导入其所需的jar包。
(2). 在Spring的IOC容器中配置Struts2的Action。注意:在IOC容器中配置Struts2的Action时,需要配置scope属性,其值必须为prototype,否则Action就成为单例的了。
<bean id="personAction" class="springstruts2.actions.PersonAction" scope="prototype"> <property name="personService" ref="personService"></property> </bean>
(3). 配置Struts2的配置文件:action节点的class属性需要指向IOC容器中该bean的id。
<!-- Spring 整合 Struts2 时, 在 Struts2 中配置的 Spring 的 Action 的 class 需要指向 IOC 容器中该 bean 的 id --> <action name="person-save" class="personAction"> <result>/success.jsp</result> </action>
(4). 加入struts2-spring-plugin-2.3.15.3.jar。
整合原理:通过添加struts2-spring-plugin-2.3.15.3.jar后,Struts2会先从IOC容器中获取Action的实例。
SPRING 学习笔记 END at 2017-02-18 12:21:00.