基于注解的Spring
<context-param>
<param-name>contextConfigLocationparam-name>
<param-value>
classpath*:/Spring/applicationContext*.xml
param-value>
context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListenerlistener-class>
listener>
<servlet>
<servlet-name>springmvcservlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServletservlet-class>
<init-param>
<param-name>contextConfigLocationparam-name>
<param-value>classpath*:/SpringMVC/applicationContext-mvc.xmlparam-value>
init-param>
<load-on-startup>1load-on-startup>
servlet>
<servlet-mapping>
<servlet-name>springmvcservlet-name>
<url-pattern>*.jhtmlurl-pattern>
servlet-mapping>
<listener>
<listener-class>org.springframework.web.util.IntrospectorCleanupListenerlistener-class>
listener>
<filter>
<filter-name>encodingFilterfilter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilterfilter-class>
<init-param>
<param-name>encodingparam-name>
<param-value>UTF-8param-value>
init-param>
<init-param>
<param-name>forceEncodingparam-name>
<param-value>trueparam-value>
init-param>
filter>
<filter>
<filter-name>encodingConvertFilterfilter-name>
<filter-class>net.sage.filter.EncodingConvertFilterfilter-class>
<init-param>
<param-name>fromEncodingparam-name>
<param-value>ISO-8859-1param-value>
init-param>
<init-param>
<param-name>toEncodingparam-name>
<param-value>UTF-8param-value>
init-param>
filter>
<filter-mapping>
<filter-name>encodingFilterfilter-name>
<url-pattern>*.jspurl-pattern>
filter-mapping>
<filter-mapping>
<filter-name>encodingFilterfilter-name>
<url-pattern>*.jhtmlurl-pattern>
filter-mapping>
<filter-mapping>
<filter-name>encodingConvertFilterfilter-name>
<url-pattern>*.jhtmlurl-pattern>
filter-mapping>
classpath*:/applicationContext*.xml
<filter>
<filter-name>encodingFilterfilter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilterfilter-class>
<init-param>
<param-name>encodingparam-name>
<param-value>UTF-8param-value>
init-param>
<init-param>
<param-name>forceEncodingparam-name>
<param-value>trueparam-value>
init-param>
filter>
<filter>
<filter-name>encodingConvertFilterfilter-name>
<filter-class>net.sage.filter.EncodingConvertFilterfilter-class>
<init-param>
<param-name>fromEncodingparam-name>
<param-value>ISO-8859-1param-value>
init-param>
<init-param>
<param-name>toEncodingparam-name>
<param-value>UTF-8param-value>
init-param>
filter>
<filter-mapping>
<filter-name>encodingFilterfilter-name>
<url-pattern>*.jspurl-pattern>
filter-mapping>
<filter-mapping>
<filter-name>encodingFilterfilter-name>
<url-pattern>*.jhtmlurl-pattern>
filter-mapping>
<filter-mapping>
<filter-name>encodingConvertFilterfilter-name>
<url-pattern>*.jhtmlurl-pattern>
filter-mapping>
<listener>
<listener-class>org.springframework.web.util.IntrospectorCleanupListenerlistener-class>
listener>
<session-config>
<session-timeout>30session-timeout>
session-config>
<welcome-file-list>
<welcome-file>index.htmlwelcome-file>
<welcome-file>index.jspwelcome-file>
welcome-file-list>
<error-page>
<error-code>404error-code>
<location>/common/resource_not_found.jhtmllocation>
error-page>
<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"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">
通常如此配置:
如果仅希望扫描特定的类,而非基包下的所有类,可以使用resource-pattern属性过滤出特定的类:
如果使用resource-pattern属性满足不你的要求,可以使用
<context:component-scan base-package="com.sage">
<context:include-filter type=”regex” expression="com\.sage\.anno\..*" />
<context:exclude-filter type=aspectj expression="com.sage..*Controller+" />
context:component-scan>
过滤表达式
类别 |
示例 |
说明 |
annotation |
com.sage.XxxAnnotation |
所有标注了XxxAnnotation的类。该类型采用目标类是否标注了某个注解进行过滤 |
assignable |
com.sage.XxxService |
所有继承或扩展了XxxService的类。该类型采购目标类是否继承或扩展某个特定类进行过滤。 |
aspectj |
Com.sage..*Service+ |
所有类名以Service结束的类及继承或扩展它们的类。该类型采购AspectJ表达式进行过滤。 |
regex |
Com\.sage\.anno\..* |
所有com.sage.anno类包下的类。该类型采购正则表达式根据目标类名进行过滤。 |
Custom |
com.sage.XxxTypeFilter |
采购XxxTypeFilter通过代码的代工根据过滤规则。该类必须实现org.springframework.core.type.TypeFilter接口。 |
<context:component-scan /> 在application.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"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<context:component-scan base-package="com.sage"/>
<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"
xmlns:aop="http://www.springframework.org/schema/aop”
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
如果使用的是CGLib动态代理,可以将其proxy-target-class属性设置为”true”,如:
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"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
<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"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd"
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">
<context:property-placeholder location="classpath*:/database.properties" ignore-resource-not-found="true" ignore-unresolvable="true" />
<bean id="proxoolDataSource" class="org.logicalcobwebs.proxool.ProxoolDataSource">
<property name="driver">
<value>${driver_class}value>
property>
<property name="driverUrl">
<value>${driver_url}value>
property>
<property name="user" value="${username}"/>
<property name="password" value="${password}"/>
<property name="delegateProperties">
<value>user=${username},password=${password}value>
property>
<property name="maximumConnectionCount" value="50"/>
<property name="minimumConnectionCount" value="5"/>
<property name="houseKeepingSleepTime" value="120000"/>
<property name="prototypeCount" value="3"/>
<property name="alias" value="${username}">property>
<property name="trace" value="true"/>
<property name="houseKeepingTestSql">
<value>select 1 from dualvalue>
property>
bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTrancationManager">
<property name="dataSource" ref="dataSource" />
bean>
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"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd"
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">
<context:property-placeholder location="classpath*:/database.properties" ignore-resource-not-found="true" ignore-unresolvable="true" />
<bean id="proxoolDataSource" class="org.logicalcobwebs.proxool.ProxoolDataSource">
<property name="driver">
<value>${driver_class}value>
property>
<property name="driverUrl">
<value>${driver_url}value>
property>
<property name="user" value="${username}"/>
<property name="password" value="${password}"/>
<property name="delegateProperties">
<value>user=${username},password=${password}value>
property>
<property name="maximumConnectionCount" value="50"/>
<property name="minimumConnectionCount" value="5"/>
<property name="houseKeepingSleepTime" value="120000"/>
<property name="prototypeCount" value="3"/>
<property name="alias" value="${username}">property>
<property name="trace" value="true"/>
<property name="houseKeepingTestSql">
<value>select 1 from dualvalue>
property>
bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTrancationManager">
<property name="dataSource" ref="dataSource" />
bean>
beans>
使用@Component注解在类声明处对类进行标注,它可以和在XML中配置达到同样的效果。
package com.test;
import org.springframework.stereotype.Component;
@Component("a")
public class A {
...
}
等同于
<bean id="a" class="com.test.A" />
除了@Component外,Spring还提供了3个功能基本与之相同的注解,它们分别用于对DAO、Service以及Web层的Controller进行注解,所以也称为注解了Bean的衍型stereotype注解:
之所以要在@Component之外提供这三个特殊的注解,是为了让标注类本身的用途清晰化,你完全可以用@Component替代这三个特殊的注解。但是推荐使用特定的注解标注特定的Bean。Spring在后续版本中可能会分别对这三个特殊的注解功能进行增强。
用于对DAO实现类进行标注;
用于对Service实现类进行标注;
用于对Web层的Controller实现类进行标注;
使用@autoWired注解可以实现Bean的依赖注入。
该注解默认按类型匹配的方法,在容器中查找匹配的Bean,当有且仅有一个匹配的Bean时,Spring将其注入到@AutoWired标注的变量中。
如果窗口中没有一个和标注变量类型匹配的Bean,Spring容器启动时将报NoSuchBeanDefinitionException异常。如果希望Srping即使找不到匹配的Bean完成注入也不要抛出异常,那么可以使用它的required属性:@AutoWired(required=false),默认情况下,required属性为true。
package com.test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component("a")
public class A {
@Autowired(required=false)
private B b;
...
}
对类方法进行标注:
@Autowired可以对类成品变量及方法的入库进行标注,如:
package com.test;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class A {
private Bar bar;
private Car car;
@Autowired
public void setBar(Bar bar){
this.bar = bar;
}
@Autowired
public void int(Bar bar, Car car){
this.bar = bar;
this.car = car;
}
}
对集合类进行标注:
如果对类中集合类的变量或方法入参进行@Autowired标注,Spring会将容器中类型匹配的所有Bean都自动注入进来。如:
package com.test;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class T {
@Autowired
public List
public List
return plugins;
}
}
Spring如果发现变量是一个集合类,它就会将容器中匹配集合元素类型的所有Bean都注入进来。这里,Plugin为一个接口,它拥有两个实现类,分别是APlugin和BPlugin,这两个实现类都通过@Component标注为Bean,则Spring会将这两个Bean都注入到plugins中。
如果容器中有一个以上匹配的Bean时,则可以和将@Autowired注解和@Qualifier注解一起使用:
package com.test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
@Component("a")
public class A {
@Autowired(required=false)
@Qualifier("b")
private B b;
...
}
这时,假设容器有两个类型为com.test.B的Bean,一个名为”b”,另一个名称”bb”,Spring会注入名为”b”的Bean。
进类方法进行标注:
package com.test;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
@Component
public class A {
private Bar bar;
private Car car;
@Autowired
@Qualifier("bar")
public void setBar(Bar bar){
this.bar = bar;
}
@Autowired
public void int(@Qualifier("bar")Bar bar, Car car){
this.bar = bar;
this.car = car;
}
}
Spring还支持JSR-250中定义的@Resource和JSR-330中定义的@Inject注解,这两个注解和@Autowired注解功能类似,都是对类变量及方法入参提供自动注入。
@Resource要求提供一个Bean名称的属性,如果为空,则自动采用标注处的变量名或方法名作为Bean的名称:
package com.test;
import java.util.List;
import javax.annotation.Resource;
import org.springframework.stereotype.Component;
@Component
public class A {
private Bar bar;
@Resource("bar")
public void setBar(Bar bar){
this.bar = bar;
}
}
@Inject注解和@autowired一样也是按类型匹配注入Bean的,只不过它没有required属性。
可见不管是@resource还是@Inject注解,其功能都没有@Autowired丰富,因此除非必要,大可不必在乎这两个注解。
通过入参指定Bean的作用范围,默认为单例(singletion)。可选择的值有:
单据模式。每次获取(注入)都是同一个实例。
原型模式。每一次获取(注入)都是新的实例。
Request作用域的Bean对应一个Http请求和生命周期,这样,每次Http请求调用到Bean时,Spring容器创建一个新的Bean实例,请求处理完毕后,销毁这个实例。
Session作用域的Bean对应一个Session请求和生命周期。Bean实例的作用域横跨整个Http Session,Session中所有Http请求都共享同一个Bean实例。当Http Session结束后,实例才被销毁。
书中所写:globalSession作用域类似于Session作用域,不过仅在Portlet的Web应用中使用。Portlet规范定义了全局Session的概念,它被组成portlet Web应用的所有子Portlet共享。如果不在Portlet Web应用环境下,globalSession自然就等价于session作用域了。
使用Web应用环境相关的Bean作用域
需要在Web容器中进行额外配置:
在高版本的Web窗口中使用Http请求监听器进行配置:
<listener>
listener-class>org.springframework.web.context.request.RequestContextListenerlistener-class>
listener>
在低版本的Web容器中(before Servlet2.3)使用Http请求过滤器进行配置:
<filter>
<filter-name>requestContextFilterfilter-name>
<filter-class>org.springframework.web.filter.RequestContextFilterfilter-class>
filter>
<filter-mapping>
<filter-name>requestContextFilterfilter-name>
<url-pattern>/*url-pattern>
filter-mapping>
@PostConsturct注解是用于标注初始化方法的,相当于xml配置中的init-method属性;@PreDestory注解是用于标注销毁方法的,相当于xml配置中的destory-method属性。
package com.test;
import java.util.List;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.annotation.Resource;
import org.springframework.stereotype.Component;
@Component
public class A {
private Bar bar;
public A(){
System.out.println("construct...");
}
@Resource("bar")
public void setBar(Bar bar){
System.out.println("execute in setBar");
this.bar = bar;
}
@PostConstruct
private void init1(){
System.out.println("execute in init1");
}
@PostConstruct
public void init2(){
System.out.println("execute in init2");
}
@PreDestroy
public void destory1(){
System.out.println("execute in destory1");
}
@PreDestroy
public void destory2(){
System.out.println("execute in destory2");
}
}
运行这个类,将可以在控制台中看到如下输出信息:
construct...
execute in setBar
execute in init1
execute in init2
execute in destory1
execute in destory2
说明Spring先调用com.test.A的构造函数实例化Bean,再执行@Autowired进行自动注入,然后分别执行标注了@PostContruct的方法,容器关闭时,则分别执行标注了@PreDestory的方法。
一般情况下,可以使用@Autowired注解建立对其他Bean的依赖关系,Spring负责管理这些Bean的关系,当实例化一个Bean时,Spring保存该Bean所依赖的其他Bean已经初始化。
但在某些情况下,这种Bean之间的依赖关系并不那么明显。如某个论坛有很多系统参数(如会话过期时间、缓存更新时间),这些参数用于控制系统的运行逻辑。我们使用SystemSetting类表示这些系统参数:
public class SystemSetting{
public static int SESSION_TIMEOUT = 30;
public static int REFRESH_CYCLE = 60;
...
}
在SystemSetting中为每一个系统参数提供了默认值,但一个灵活的论坛必须提供一个管理后台,在管理后台中可以调整这些系统参数并保存到后台数据库中,在系统启动时,初始化程序从数据库后台加载这些系统参数的配置值以覆盖默认的值:
@Component("sysInit")
public class SysInit{
public SysInit(){
SystemSetting.SESSION_TIMEOUT = 10;
SystemSetting.REFRESH_CYCLE = 100;
}
}
假设论坛有一个缓存刷新管理器,它需要根据系统参数SystemSettings.REFERSH_CYCLE创建缓存刷新定时任务:
@Component("cacheManager")
public class CacheManager{
public CacheManager(){
Timer timer = new Timer();
TimerTask task = new CacheTask();
timer.schedule(task, 0, SystemSetting.REFRESH_CYCLE);
}
}
在以上的实例中,CacheManager依赖于SystemSettings,而SystemSettings的值由SysInit负责初始化,虽然CacheManager不直接依赖于SysInit,但从逻辑上看,CacheManager希望在SysInit加载并完成系统参数设置后再启动,以避免调用不到真实的系统参数值。如果这三个Bean都在Spring中定义,我们如何保存SysInit在CacheManager之前进行初始化呢?
Xml配置中,Spring允许用户通过depends-on属性指定Bean前置依赖的Bean,前置依赖的Bean会在本Bean实例化之前创建好。
而在使用注解的配置中,我们使用@DependsOn注解来标注前置依赖,多个前置依赖可以使用数组入参:
@DependsOn({"cacheManager"})
@Component("cacheManager")
public class CacheManager{
public CacheManager(){
Timer timer = new Timer();
TimerTask task = new CacheTask();
timer.schedule(task, 0, SystemSetting.REFRESH_CYCLE);
}
}
通过@Lazy(true)注解表示Bean是否延迟初始化。
Spring的JavaConfig子项目中的类,JavaConfig旨在通过Java害的方式提供Bean的定义信息,该项目早在Spring2.0时就已经发布了1.0版本。Srping3.0基于Java类配置的核心即取材于JavaConfig,JavaConfig经过若干年的努力终于修成正果,成为了Spring3.0的核心功能。
@Configuration、@Bean、@Import、@ImportResource这四个注解都是用于该功能的。普通的POJO类只要标注了@Configruration注解,就可以为Spring容器提供Bean定义的信息了。
Spring支持9个@ApsectJ切点表达式函数,它们用不同的方式描述目标类的连接点,根据描述对象的不同,可以大致分为4种类型:
类别 |
函数 |
入参 |
说明 |
方法切点函数 |
execution() |
方法匹配模式串 |
表示满足某一匹配模式的所有目标类方法连接点。语法: execution(<修饰符模式>?<返回类型模式><方法名模式><参数模式><异常模式>>) 每个模式之间用空格(“ ”)分开,如execution(public * *(..))、execution(* com.sage.Waiter.*(..))、execution(* com.sage..*(..))其中修饰符模式、异常模式是可以选的。这是最常用的切点函数。 支持所有的通配符。 |
@annotation() |
方法注解类名 |
表示标注了特定注解 的目标方法连接点。如@annotation(com.sage.anno.NeedTest)表示匹配任何标注了@NeedTest注解的目标类方法。 不支持任何通配符。 |
|
方法入参切点函数 |
args() |
类名、变量名 |
通过判别目标类方法运行时入参对象的类型指定连接点。如args(com.sage.A)表示所有有且仅有一个按类型匹配于A的入参方法。 仅支持通配符”+”,且默认实现”+”通配符,所以args(com.sage.A)等价于args(com.sage.A+)。 |
@args() |
类型注解类名 |
通过判别目标方法运行时入参对象的类是否标注特定注解来指定连接点。如@args(com.sage.Monitorable)表示任何这样的一个目标方法:它有一个入参且入参对象的类标注@Monitorable注解。 不支持任何通配符。 |
|
目标类切点函数 |
within() |
类名匹配串 |
表示特定域下的所有连接点。如within(com.sage.service.*)表示com.sage.service包中所有连接点,即包中所有类的所有方法,而within(com.sage.service.*Service)表示匹配com.sage.service包中所有以Service结尾的类的所有连接点。 支持所有的通配符。 |
target |
类名 |
假如目标类按类型匹配于指定类,则目标类的所有连接点匹配这个切点。如通过target(com.sage.Waiter)定义的切点,Waiter以及Waiter的实现类及子类中的所有连接点都匹配该切点。 仅支持通配符”+”,且默认实现”+”通配符,所以target(com.sage.A)等价于target(com.sage.A+)。 |
|
@within() |
类型注解类名 |
假如目标类按类型匹配于某个类A,且类A标注了特定注解,则目标类的所有连接点匹配这个切点,如@within(com.sage.Monitorable)定义的切点,假如Waiter类标注了@Monitorable注解,则Waiter以及Waiter的实现类或子类的所有连接点都匹配。 不支持任何通配符。 |
|
@target() |
类型注o解类名 |
目标类标注了特定注解,则目标类所有连接点匹配该切点。如@target(com.sage.Monitorable),假如Waiter类标注了@Monitorable注解,那么只匹配Waiter类,Waiter实现类或其子类并不匹配。 不支持任何通配符。 |
|
代理类切点函数 |
this() |
类名 |
代理类按类型匹配于指定类,则被代理的目标类所有连接点匹配切点。在一般情况下,使用this()和target()两者是等效的;它们的不同不处,我们使用例子来说明:com.sage.NaiveWaiter实现Waiter接口,又通过引介增强引入com.sage.Seller接口中的方法,而this(com.sage.Seller)支持通过引介增强产生的NaiveWaiter代理对象,而target(com.sage.Seller)是不支持的。 仅支持通配符”+”,且默认实现”+”通配符,所以this(com.sage.A)等价于this(com.sage.A+)。 |
在函数入参中使用通配符
匹配任意字符,但它只能匹配上下文中的一个元素;如com.sage.service.*Service,表示com.sage.service包中所有以Service结尾的类。
匹配任意字符,可以匹配上下文中的多个元素,但在表示类时,必须和*联合使用,而在表示入参时则单独使用;如com.sage..*,表示com.sage.service包及其子孙包中所有的类。
表示按类型匹配指定类的所有类(类本身、实现类及子类),必须跟在类名后面,如com.sage.Car+,表示com.sage.Car类本身,以及com.sageCar的实现类和子类。
逻辑运算符
与操作符,相当于切点的次运算;如within(com.sage.*) && args(java.lang.String) 表示com.sage包下(不包含其子孙包)的所有类拥有一个java.lang.String类型入参的方法。
如果在Spring的XML配置文件中使用切点表达款,由于&是XML特殊字符,所以需要使用转义字符&&表示。为了使用方便,Spring提供了一个等效的运算符”and”。如within(com.sage.*) and args(java.lang.String) 。
或操作符,相当于切点的并集运算。如within(com.sage.*) || args(java.lang.String),表示com.sage.包下所有类的方法,或者所有拥有一个java.lang.String类型入参的方法。
within(com.sage.*) or args(java.lang.String)与上述表达式一样。
非操作符,相当于切点的反集运算,如!within(com.sage.*)表示所有不在com.sage包下的方法。
“ not within(com.sage.*)”与述表达式一样,注意:如果表达式是”not within(com.sage.*)”将产生解析错误,这应该是Spring的一个Bug,在表达式开头添加空格后则可以通过:” not within(com.sage.*)”。
前置增强,在目标方法执行之前被调用。Before注解类拥有两个属性:
后置增强,在目标方法执行之后被调用。AfterReturning注解类拥有四个属性:
环绕增强,在目标方法执行之前被调用,需要在增加方法中通过ProceedingJoinPoint.proceed()方法执行目标方法。Around注解类拥有两个属性:
抛出增加,在目标方法抛出指定的异常时被调用。AfterThrowing注解类拥有四个属性:
Final增强,不管是抛出异常或是正常退出,该增强都会得到执行,该增强一般用于释放资源,相当于try{}finally{}控制流。After注解类拥有两个属性:
引介增强。它不是在目标方法周围织入增强,而是为目标类创建新的方法和属性,所以引介增强的连接点是类级别的,而非方法级别的。通过引介增强可以为目标类添加一个接口的实现,即原来目标类未实现某个接口,通过引介增强可以为目标类创建实现某接口的代理。该增强有点难理解,下面会通过一个实例来说明。DeclareParents拥有两个属性:
请看以下两个接口及其实现类:
假设我们希望NaiveWaiter能够同时充当售货员的角色,即通过切面技术为NaiveWaiter新增Seller接口的实现。实现代码如下:
package com.test;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.DeclareParents;
@Aspect
public class EnableSellerAspect {
@DeclareParents(value="com.sage.NaiveWaiter", defaultImpl=SmartSeller.class)
public Seller seller;
}
运行以下测试代码:
package com.test;
import org.springframework.context.ApplicationContext;
public class EnableSellerAspectTest {
public static void main(String[] args) {
...
ApplicationContext ctx = ...;
Waiter waiter = (Waiter)ctx.getBean("waiter");
System.out.println(waiter instanceof Waiter);
Seller seller = (Seller)waiter;
System.out.println(seller instanceof Seller);
}
}
代码成功执行,并输出以下信息:
true
true
可见,NaiveWaiter已经成功地新增了Seller接口的实现。
该函数接受一个注解类的类名,当方法的运行时入参对象标注了指定的注解时,方法匹配切点。如图:
T0、T1、T2、T3具有如图所示的继承关系,假设目标类方法的签名为fun(T1 t),它的入参为T1,而切面的切点定义为@args(M),T2类标注了@M。当fun(T1 t)传入对象是T2或T3时,则方法匹配@args(M)所声明定义的切点。
再看下面的情况,假设方法签名是fun(T1 t),入参为T1,而标注@M注解的类是T0,当fun(T1 t)传入T1、T2、T3的实例时,均不匹配切点@args(M),如图:
该函数匹配任意标注了@M的目标类,不包含其实现类和子类。
如@target(M)切点的匹配规则如图:
该函数匹配任意标注了@M的目标类及其子孙类。
如@within(M)切点的匹配规则如图:
为了方便讲解,我们假设可供被增强的目标类包括7个类,这些类都在com.sage.*包中,如图:
args()、this()、target()、@args()、@within()、@target()、@annotation()这七个函数除了可以指定类名外,还可以指定参数名,将瞟对象连接点的方法入参绑定到增强的方法中。
其中args()用于绑定连接点方法的入参;@annotation()用于绑定连接点方法的注解对象;@args()用于绑定连接点方法入参的注解。来看一个args()绑定参数的实例:
package com.test;
import org.aspectj.lang.annotation.Before;
public class TestAspect {
@Before("target(com.sage.NaiveWaiter)&&args(name,num,..)")
public void bindJoinPointParams(int num, String name){
System.out.println("---bindJoinPointParams()---");
System.out.println("name:"+name);
System.out.println("num:"+num);
System.out.println("---bindJoinPointParams()---");
System.out.println("以下是目标方法的输出:");
}
}
当前args()函数入参为参数名时,共包括两方面的信息:
运行下面的测试代码:
ApplicationContext ctx = new ...;
NaiveWaiter naiveWaiter = ctx.getBean("naiveWaiter");
naiveWaiter.smile("John", 2);
System.out.println("结束!");
将在控制台看到以下输出信息:
---bindJoinPointParams()---
name:John
num:2
---bindJoinPointParams()---
以下是目标方法的输出:
NaiverWaiter:smile to Johe 2 times.
结束!
可见增强方法按预期绑定了NaiveWaiter.smile(String name, int times)方法的运行期入参.
为了保存实例能成功执行,必须启用CGLib动态代码,在XML配置文件中如此配置:
使用this()或target()可绑定被代理对象实例,在通过类实例名绑定对象时,还依然具有原来连接点匹配的功能,只不过类名是通过增强方法中同名入参的类型间接决定的。
这里通过this()函数来了解对象绑定的用法:
package com.test;
import org.aspectj.lang.annotation.Before;
public class TestAspect {
@Before("this(waiter)")
public void bindProxyObj(Waiter waiter){
System.out.println("---bindProxyObj()---");
System.out.println(waiter.getClass().getName());
System.out.println("---bindProxyObj()---");
System.out.println("以下是目标方法的输出:");
}
}
运行下面的测试代码:
ApplicationContext ctx = new ...;
Waiter naiveWaiter = ctx.getBean("naiveWaiter");
naiveWaiter.greetTo("John");
System.out.println("结束!");
将在控制台看到以下输出信息:
---bindProxyObj()---
com.sage.NaiveWaiter$$EnhancerByCGLIB$$6758801b
---bindProxyObj()---
以下是目标方法的输出:
NaiverWaiter:greet to John.
结束!
target()函数的代理对象绑定跟上面一样。
@within()和@target()函数可以将目标类的注解对象绑定到增强方法中,下面通过@within()函数演示注解绑定的操作:
package com.test;
import org.aspectj.lang.annotation.Before;
public class TestAspect {
@Before("@within(m)")
public void bindAnnoObj(Monitorable m){
System.out.println("---bindAnnoObj()---");
System.out.println(m.getClass().getName());
System.out.println("---bindAnnoObj()---");
System.out.println("以下是目标方法的输出:");
}
}
NaiveWaiter类中标注了@Monitorable注解,所有NaiveWaiter Bean匹配切点,其Monitorable注解对象将绑定到增强方法中。运行下面的测试代码:
ApplicationContext ctx = new ...;
Waiter naiveWaiter = ctx.getBean("naiveWaiter");
naiveWaiter.greetTo("John");
System.out.println("结束!");
将在控制台看到以下输出信息:
---bindAnnoObj()---
$Proxy3
---bindAnnoObj()---
以下是目标方法的输出:
NaiverWaiter:greet to John.
结束!
从输出信息中可以看出,使用CGLib代理NaiveWaiter时,其类的注解Monitorable对象也被代理了。
在后置增强中,可以通过returning绑定连接点方法的返回值:
@AfterReturning(value="target(com.sage.SmartSeller)", returning="retVal")
public void bindReturnValue(int retVal){
System.out.println("---bindReturnValue()---");
System.out.println("return value:" + retVal);
System.out.println("---bindReturnValue()---");
System.out.println("以下是目标方法的输出:");
}
Returning属性值的名称必须跟增强方法入参的名称相同,此外,returning属性值的类型必须和连接点方法的返回值类型匹配。运行下面的测试代码:
ApplicationContext ctx = new ...;
SmartSeller seller = ctx.getBean("seller");
seller.sell("John", "beer");
System.out.println("结束!");
可以看到以下输出信息:
---bindReturnValue()---
return value:100
---bindReturnValue()---
以下是目标方法的输出:
SmartSeller:Johe sell to beer.
结束!
和通过切点函数绑定连接点信息不同,连接点抛出的必须使用@AfterThrowing注解的throwing属性进行绑定:
@AfterThrowing(value="target(com.sage.SmartSeller)", throwing="e")
public void bindException(NullPointerException e){
System.out.println("---bindException()---");
System.out.println("exception:" + e.getMessage());
System.out.println("---bindException()---");
System.out.println("以下是目标方法的输出:");
}
在SmartSeller中添加一个抛出异常的方法:
public class SmartSell implements Seller {
public void checkBill(String billId){
if(billId==null) throw new NullPointerException("billId is null.");
}
}
运行下面的测试代码:
ApplicationContext ctx = new ...;
SmartSeller seller = ctx.getBean("seller");
seller.checkBill(null);
System.out.println("结束!");
可以看到以下输出信息:
---bindException()---
exception:billId is null
---bindException()---
以下是目标方法的输出:
SmartSeller:Johe sell to beer.
结束!
一个连接点可以同时匹配多个切点,切点对应的增强在连接点上的织入顺序是如何安排的呢?这个问题需要分3种情况讨论:
我们可以通过下图描述这种织入的规则:
切面类A和B都实现了Ordered接口,A切面类对应序号为1,B切面类对应序号为2,A切面类按顺序定义了3个增强,B切面类按顺序定义两个增强,这5个增强对应的切点都匹配某个目标类的连接点,则增强织入的顺序为上图中虚线所示。
AspectJ使用org.aspectj.lang.JoinPoint接口表示目标类连接点对象,如果是环线增强时,使用org.aspectj.lang.ProceedingJoinPoint表示连接点对象,该类是JoinPoint的子接口。任何一个增强方法都可以通过将第一个入参声明为JoinPoint来访问到连接点上下文的信息。
ProceedingJoinPoint继承JoinPoint子接口,它新增了两个用于执行连接点方法的方法:
看一个具体的实例:
package com.test;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
public class TestAspect {
@Around("execution(* geetTo(..)) && target(com.sage.NaiveWaiter)")
public void joinPointAccess(ProceedingJoinPoint p){
System.out.println("---joinPointAccess()---");
System.out.println("args[0]:" + p.getArgs()[0]);
System.out.println("target class:" + p.getTarget().getClass());
System.out.println("以下是目标方法的输出:");
p.proceed();
System.out.println("---joinPointAccess()---");
}
}
执行以下测试代码:
ApplicationContext ctx = new ...;
Waiter waiter = ctx.getBean("naiveWaiter");
waiter.greetTo("John");
System.out.println("结束!");
可以看到以下输出信息:
---joinPointAccess()---
args[0]:John
target class:com.sage.NaiveWaiter
以下是目标方法的输出:
NaiveWaiter:greet to John.
---joinPointAccess()---
结束!
在前面所举的例子中,切点直接声明在杨增强处,这种切点声明方式称为匿名切点,匿名切点只能在声明处使用。如果希望在其他地方重用一个切点,可以通过@Pointcut注解以及切面类方法对切点进行命名,如下:
package com.test;
import org.aspectj.lang.annotation.Pointcut;
public class TestNamePointcut {
@Pointcut("within(com.sage.*)")
private void inPackage(){}
@Pointcut("execution(* greetTo(..))")
protected void greetTo(){}
@Pointcut("inPackage() && greetTo()")
public void inPkgGreetTo(){}
}
在上述代码中定义了3个命名切点,命名切点的使用类方法作为切点的名称,此外方法的访问修饰符还控制了切点的可引用性,这种可引用性和类方法的可访问性相同,如private的切点只能在本类中引用,public的切点可以在任何类中引用。命名切公利用方法名和访问修饰符的信息,所以习惯上,方法的返回类型为void,并且方法体为空。
通过下图可以更直观的了解命名切点的结构:
在上述代码中inPkgGreetTo()的切点引用了同类中的greetTo()切点,而inPkgGreetTo()切点可以被任何类引用。我们还可以扩展TestNamePointcut类,通过类的继承关系定义更多的切点。
命名切点定义好之后,就可以在定义切面时通过名称引用切点,看下面的实例:
package com.test;
import org.aspectj.lang.annotation.Before;
public class TestAspect {
@Before("TestNamePointcut.inPkgGreetTo()")
public void pkgGreetTo(){
System.out.println("- pkgGreetTo() executed! -");
}
@Before("!target(com.sage.NaiveWaiter) && TestNamePointcut.inPkgGreetTo()")
public void pkgGreetToNotNaiveWaiter(){
System.out.println("- pkgGreetToNotNaiveWaiter() executed! -");
}
}
可以看出,命名切点可以使用复合运算和匿名切点一起使用。
一个数据库可能拥有多个访问客户端,这些客户端都可以以并发方式访问数据库。数据库中的相同数据可能同时被多个事务访问,如果没有采取必要的隔离措施,就会导致各种并发问题,破坏数据的完整性。这些问题可以归结为5类,包括3类数据读问题(脏读、不可重复读、幻象读)以及2类数据更新问题(第一类丢失更新、第二类丢失更新)。下面,我们分别通过实例讲解引发问题的场景。
A事务读取B事务尚未提交的更改数据,并在这个数据的基础上操作。如果恰巧B事务回滚,那么A事务读到的数据根本是不承认的。来看取款事务和转账事务并发时引发的脏读场景:
时间 |
转账事务A |
取款事务B |
T1 |
|
开始事务 |
T2 |
开始事务 |
|
T3 |
|
查询账户余额为1000元 |
T4 |
|
取出500元,把余额改为500元 |
T5 |
查询账户余额为500元(脏读) |
|
T6 |
|
撤销事务,余额恢复为1000元 |
T7 |
汇入100元,把余额改为600元 |
|
T8 |
提交事务 |
|
在这个场景中,B希望取款500元而后又撤销了动作,而A住相同的账户中转账100元,就因为A事务读取了B事务尚未提交的数据,因而造成账户白白丢失了500元。在Oracle数据库中,不会发生脏读的情况。
不可重复读是批A事务读取了B事务已经提交的更改数据。假设A在取款事务的过程中,B往该账户转账100元,A两次读取账户的余额发生不一致:
时间 |
取款事务A |
转账事务B |
T1 |
|
开始事务 |
T2 |
开始事务 |
|
T3 |
|
查询账户余额为1000元 |
T4 |
查询账户余额为1000元 |
|
T5 |
|
取出100元,把余额改为900元 |
T6 |
|
提交事务 |
T7 |
查询账户余额为900元(和T4读取的不一致) |
|
在同一事务中,T4时间点和T7时间点读取账户存款余额不一样。
A事务读取B事务提交的新增数据,这时A事务将出现幻象读的问题。幻象读一般发生在计算统计数据的事务中,举一个例子,假设银行系统在同一个事务中,两次统计存款账户的总金额,在两次统计过程中,刚好新增了一个存款账户,并存入100元,这时,两次统计的总金额将不一致:
时间 |
统计金额事务A |
转账事务B |
T1 |
|
开始事务 |
T2 |
开始事务 |
|
T3 |
统计总存款数为10000元 |
|
T4 |
|
新增一个存款账户,存款为100元 |
T5 |
|
提交事务 |
T6 |
再次统计总存款数为10100元(幻象读) |
|
如果新增数据刚好满足事务的查询条件,这个新数据就进入了事务的视野,因而产生了两个统计不一致的情况。
幻象读和不可重复读是两个容易混淆的概念,前者是指读到了其他已经提交事务的新增数据,而后者是指读到了已经提交事务的更改数据(更改或删除),为了避免这两个情况,采取的对策是不同的,防止读取到更改数据,只需要对操作的数据添加行级锁,阻止操作中的数据发生变化,而防止读取到新新增数据,则往往需要添加表级锁 -- 将整个表锁定,防止新增数据(Oracle使用多版本数据的方式实现)。
A事务撤销时,把已经提交的B事务的更新数据覆盖了。这种错误可以造成很严重的问题,通过下面的账户取款转账就可以看出来:
时间 |
取款事务A |
转账事务B |
T1 |
开始事务 |
|
T2 |
|
开始事务 |
T3 |
查询账户余额为1000元 |
|
T4 |
|
查询账户余额为1000元 |
T5 |
|
汇入100元把余额改为1100元 |
T6 |
|
提交事务 |
T7 |
取出100元把余额改为900元 |
|
T8 |
撤销事务 |
|
T9 |
余额恢复为100元(丢失更新) |
|
A事务在撤销时,“不小心”将B事务已经转入账户的金额给抹去了。
A事务覆盖B事务已经提交的数据,造成B事务所做操作丢失:
时间 |
转账事务A |
取款事务B |
T1 |
|
开始事务 |
T2 |
开始事务 |
|
T3 |
|
查询账户余额为1000元 |
T4 |
查询账户余额为1000元 |
|
T5 |
|
取出100元,把余额改为900元 |
T6 |
|
提交事务 |
T7 |
汇入100元 |
|
T8 |
提交事务 |
|
T9 |
把余额改为1100元(丢失更新) |
|
上面的例子里由于转账事务A覆盖了取款事务对存款余额所做的更新,导致银行最后损失了100元,相反如果转账事务先提交,那么用户账户将损失100元。
数据并发会引发很多问题,在一些场合下有些问题是允许的,但在另外一些场合下可能却是致命的。数据库通过 锁的机制解决并发访问的问题,虽然不同的数据库在实现细节上存在差别,但原理基本上是一样的。
按锁定的对象的不同,一般可以分为表锁定和行锁定,前者对整个表进行锁定,而后者对表中特定行进行锁定。从并发事务锁定的关系上看,可以分为共享锁定和独占锁定。共享锁定会防止独占锁定,但允许其他的共享锁定。而独占锁定 既防止苍的独占锁定,也防止其他的共享锁定。为了更改数据,数据库必须在进行更新的行上施加行独占锁定,INSERT、UPDATE、DELETE和SELECT FOR UPDATE语句都会隐式采用必要的行锁定。下面我们介绍一下Oracle数据库常用的5种锁定:
一般通过SELECT FOR UPDATE语句隐式获得行共享锁定,在Oracle中用户也可以通过LOCK TABLE IN ROW SHARE MODE语句显式获得行共享锁定。行共享锁定并不防止对数据行进行更新的操作,但是可以防止 其他会话获取独点性数据表锁定。允许进行多个并发的行共享和行独占性锁定,还允许进行数据表的共享锁定或者表共享行独占锁定。
通过一条INSERT、UPDATE或DELETE语句隐式获取,或者通过一条LOCK TABLE IN ROW EXCLUSIVE MODE 语句显式获取。这个锁定可以防止其他会话获取一个表共享锁定、行共享锁定、表共享行独占锁定、行独占锁定、表独占锁定。
通过LOCAK TABLE IN SHARE MODE语句显式获得。这种锁定可以防止其他会话获取行独占锁定(INSERT、UPDATE、DELETE),或者防止其他表共享行独占锁定或表独占锁定,它允许在表中拥有多个行共享和表共享锁定。该锁定可以让会话具有对表事务级一致性访问,因为其他会话在用户提交匮乏回溯该事务并释放对该表的锁定之前不能更改这个被锁定的表。
通过LOCK TABLE IN SHARE ROW EXCLUSIVE MODE语句显式获得。这种锁定可以防止其他会话获取一个表共享、行独占或者表独占锁定,这允许其他行共享锁定。这种锁定类似于表共享锁定,只是一次只能对一个表旋转一个表共享行独占锁定。如果A会话拥有该锁定,则B会话可以执行SELECT FOR UPDATE操作,如果B会话试图更新选择的行,则需要等待。
通过LOCK TABLE IN EXCLUSIVE MODE显式获得。这个锁定防止其他会话对该表的任何其他锁定。
尽管数据库为用户提供了锁的DML操作方式,但直接使用锁管理是非常麻烦的,因此数据库为用户提供了自动锁机制。只要用户指定会话的事务隔离级别,数据库就会分析事务中的SQL语句,然后自动为事务操作的数据资源添加上适合的锁。些外数据库还会维护这些锁,当一个资源上的锁数目太多时,自动进行锁升级以提高系统的运行性能,而这一过程对用户来说完全是透明的。
ANSI/ISO SQL 92标准定义了4个等级的事务隔离级别,在相同数据环境下,使用相同的输入,执行相同的工作,根据不同的隔离级别,可以导致不同的结果。不同事务隔离级别能够解决的数据并发问题的能力是不同的,如下所示:
隔离级别 |
脏读 |
不可重复读 |
幻象读 |
第一类丢失更新 |
第二类丢失更新 |
READ UNCOMMITED |
允许 |
允许 |
允许 |
不允许 |
允许 |
READ COMMITED |
不允许 |
允许 |
允许 |
不允许 |
允许 |
REPEATABLE READ |
不允许 |
不允许 |
允许 |
不允许 |
不允许 |
SERIALIZABLE |
不允许 |
不允许 |
不允许 |
不允许 |
不允许 |
事务的隔离级别和数据库并发性是对立的,两者此增彼减。一般来说,使用READ UNCOMMITED隔离级别的数据库拥有最高的并发性和吞吐量,而使用SERIALIZABLE隔离级别的数据库并发性最低。
SQL 92定义READ UNCOMMITED主要是为了提供非阻塞坊的能力,Oracle虽然也支持READ UNCOMMITED,但它不支持脏读,因为Oracle使用多版本机制彻底解决了在非阻塞读到脏数据的问题并保证读的一致性,所以Oracle的READ COMMITED隔离级别就已经满足了SQL 92标准的REFEATABLE READ隔离级别。
SQL 92推荐使用REPEATABLE READ以保证数据的读一致性,不过用户可以根据就用的需要选择适合的隔离等级。
通过JDBC的事务管理知识,我们知道事务只能被提交或回滚(或回滚到某个保存点后提交),Spring高层事务抽象接口org.springframework.transaction.PlatformTransactionManager很好的描述了事务管理的这个概念,以下是其代码:
public interface PlatformTransactionManager {
TransactionStratus getTransaction(TransactionDefinition definition) throws TransactionException;
void commit(TransactionStratus status) throws TransactionException;
void rollback(TransactionStratus status) throws TransactionException;
}
PlatformTransactionManager只定义了3个接口方法,它们是SPI(Service Provider Interface)高层次的接口方法。这些访问都没有和JNDI绑定在一起,可以像Spring容器中普通的Bean一样对待PlatformTransactionManager实现者。下面我们来了解PlatformTransactionManager方法的功能:
该方法根据事务定义信息从事务环境中返回一个已存在的事务,或者创建一个新的事务,并用TransactionStratus描述这个事务的状态。
根据事务的状态提交事务,如果事务状态已经被标识为rollback-only,该方法将执行一个回滚事务的操作。
将事务回滚。当commit()方法抛出异常时,rollback()会被隐式调用。
Spring将事务管理委托给底层具体的持久化实现框架完成。因此,Spring为不同的持久化框架提供了PlatformTransactionManager接口的实现类,如下所示:
事务 |
说明 |
org.springframework.orm.jpa.JpaTransactionManager |
使用JPA进行持久化时,使用该事务管理器 |
Org.springframework.orm.hibernate3.HibernateTransactionManager |
使用Hibernate3.0版本进行持久化,使用该事务管理器 |
org.springframework.jdbc.datasource.DataSourceTransactionManager |
使用Spring JDBC或iBatis等基于DataSource数据源的持久化技术时,使用该事务管理器 |
org.springframework.orm.jdo.JdoTransactionManager |
使用JDO进行持久化时,使用该事务管理器 |
org.springframework.transaction.jta.JtaTransactionManager |
具有多个数据源的全局事务使用该事务管理器(不管理采用何种持久化技术) |
这些事务管理器都是对特定事务实现框架的代理,这样,我们就可以通过Spring所提交的高级抽象对不种类的事务实现使用相同的方式进行管理,而不用关心具体的实现。
要实现事务,首先要在Spring中配置好相应的事务管理器,为事务管理器指定数据资源以及一些其他事务管理控制属性。下面让我们看一个JDBC和iBatis的事务管理器配置:
...
<bean id="dataSource" class="org.logicalcobwebs.proxool.ProxoolDataSource"
p:driver="${driver_class}"
p:driverUrl="${driver_url}"
p:user="${username}"
p:password="${password}"
p:delegateProperties="user=${username},password=${password}"
/>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
p:dataSource-ref="dataSource" />
...
在幕后,DataSourceTransactionManager使用DataSource的Connection的commit()、rollback()等方法管理事务。
Spring将JDBC的Connection、Hibernate的Session等访问数据库的连接或会话对象统称为资源。这些资源在同一时刻是不能多线程共享的,为了让Dao、Service类可能做到Singleton,Spring的事务同步管理器类org.springframework.transaction.support.TransactionSynchronizationManager使用ThreadLocal管理为不同事务线程提供了独立的资源副本,同时维护事务配置的属性和运行状态信息。事务同步管理器是Spring事务管理的基石部分,不管用户使用编程式事务管理,还是声明式事务管理,都离不开事务同步管理器。
Spring框架为不同的持久化技术提供了一套从TransactionSynchronizationManager中获取对应线程绑定资源的工具类,如下所示:
待久化技术 |
线程绑定资源获取工具 |
Spring JDBC或iBatis |
org.springframework.jdbc.datasource.DataSourceUtils |
Hibernate 3.0 |
org.springframework.orm.Hibernate3.SessionFactoryUtils |
JPA |
org.springframework.jpa.EntityManagerFactoryUtils |
JDO |
org.springframework.orm.jdo.PersistenceManagerFactoryUtils |
这些工具类都提供了静态的方法,通过这些当前线程绑定的资源,如果DataSourceUtils.getConnection(DataSource dataSource)可以从数据源中获取和当前线程绑定的Connection,而Hibernate的SessionFactoryUtils.getSession(SessionFactory sessionFactory, boolean allowCreate)则从指定的SessionFactory中获取和当前线程绑定的Session。
当需要脱离模版类,手工操作底层持久技术的原生API时,就需要通过这些工具类获取线程绑定的资源,而不应该直接从DataSource或SessionFactory中获取。因为后者不能获得和本线程相关的资源,因此无法让数据操作参与到本线程相关的事务环境中。
这些工具类还有另外一个重要的用途:将特定异常转换为Spring的DAO异常。
Spring为不同的持久化技术提供了模版类,模板类在内部通过资源获取工具类间接访问TransactionSynchronizationManager中的线程绑定资源。所以如果Dao使用模版类进行持久化操作,这些Dao就可以配置成singleton。如果不使用模板类,也可以直接通过资源获取工具类访问线程相关的资源。
下面,我们就揭开TransactionSynchronizationManager的层层面纱,探寻其中的奥秘:
public abstract class TransactionSynchronizationManager{
//①②③④⑤用于保存每个事务线程对应的Connection或Session等类型的资源
private static final ThreadLocal resources = new ThreadLocal();
//②用于保存每个事务线程对应事务的名称
private static final ThreadLocal currentTransactionName = new ThreadLocal();
//③用于保存每个事务线程对应事务的read-only状态
private static final ThreadLocal currentTransactionReadOnly = new ThreadLocal();
//④用于保存每个事务线程对应事务的隔离级别
private static final ThreadLocal currentTransactionIsolationLevel = new ThreadLocal();
//⑤用于保存每个事务线程对应事务的激活态
private static final ThreadLocal actualTransactionActive = new ThreadLocal();
...
}
TransactionSynchronizationManager将Dao、Service类中影响线程安全的所有“状态”统一抽取到该类中,并用ThreadLocal进行替换,从此Dao(必须基于模板类或资源获取工具类创建的Dao)和Service(必须采用Spring事务管理机制)摘掉了非线程安全的帽子,完成了脱胎换骨式的身份转变。
当我们调用一个基于Spring的Service接口方法(如UserService#addUser())时,它将运行于Spring管理的事务环境中,Service接口方法可能会在内部调用其他的Service接口方法以共同完成一个完整的业务操作,因此就会产生服务接口方法嵌套调用的情况,Spring通过事务传播行为控制当前的事务如何传播到被嵌套调用的目标服务接口方法中。事务传播是Spring进行事务管理的重要概念,其重要性怎么强调都不为过。但是事务传播行为也是被误解最多的地方,接下来我们将详细分析不同事务传播行为的表现形式,掌握它们之间的区别。
Spring在TransactionDefinition接口中规定了7种类型的事务传播行为,它们规定了事务方法和事务方法发生嵌套调用时事务如何进行传播,如下所示:
事务传播行为类型 |
说明 |
PROPAGATION_REQUIRED |
如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。 |
PROPAGATION_SUPPORTS |
支持当前事务,如果当前没有事务,就以非事务方式执行。 |
PROPAGATION_MANDATORY |
使用当前的事务,如果当前没有事务,就抛出异常。 |
PROPAGATION_REQUIRES_NEW |
新建事务,如果当前存在事务,把当前事务挂起。 |
PROPAGATION_NOT_SUPPORTED |
以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。 |
PROPAGATION_NEVER |
以非事务方式执行,如果当前存在事务,则抛出异常。 |
PROPAGATION_NESTED |
如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作 -- 新建一个事务。 |
(关于PROPAGATION_NESTED和PROPAGATION_REQUIRES_NEW区别可以点击http://www.07net01.com/linux/spring_PROPAGATION_REQUIRES_NEW_he_PROPAGATION_NESTED_499731_1373007731.html查看)
具体配置可以看事务配置。
基于@Transactional注解的配置和基于XML的配置方式一样,它拥有一组普适性很强的默认事务属性,我们往往可以直接使用这些默认的属性就可以了:
因为这些默认设置在大多数情况下都是适用的,一般不需要手工设置事务注解的属性。当前,Spring允许我们通过手工设定属性值覆盖默认值。下面就是@Transactional的属性说明:
属性名 |
说明 |
propagation |
事务传播行为,通过以下枚举类提供合法值: ora.springframework.transaction.annotation.Propagation 例如:@Transactional(propagation=Propagation.REQUIRES_NEW) |
isolation |
事务隔离级别,通过以下枚举类提供合法值: org.springframework.transaction.annotation.Isolation 例如:@Transactional(isolation=Isolation.READ_COMMITTED) |
readOnly |
事务读写性,boolean型,例如:@Transactional(readOnly=true) |
timeout |
超时时间,int型,以秒为单位,例如:@Transactional(timeout=10) |
rollbackFor |
一组异常类,遇到时进行回滚,类型为:Class extends Throwable>[],默认为{}。 例如:@Transactional(rollbackFor={SQLException.class}),多个异常之间可用逗号分隔。 |
rollbackForClassName |
一组异常类名,遇到时进行回滚,类型为String[],默认值为{}。例如: @Transactional(rollbackForClassName={“Exception”}) |
noRollbackFor |
一组异常类,遇到时不回滚,类型为:Class extends Throwable>[],默认为 |
noRollbackForCassName |
一组异常类名,遇到时不回滚,类型为String[],默认值为{}。 |
在何必使用@Transactional注解
@Transactional注解可以被应用于接口定义和接口方法、类定义、和类的public方法上。
但Spring建议在业务实现类上使用@Transactional注解,当然我们也可以在业务接口上使用@Transactional注解。但这样会留下一容易被忽视的隐患。因为注解不能被继承,所以业务接口中标注的@Transactional注解不会被实现的业务类继承,如果通过以下的配置启用子类代理:
业务类不会添加事务增强,照样工作在非事务的环境下。举一个具体的实例:如果使用子类代理,假设用户为BbtForum接口标注了@Transactional注解,其实现类BbtForumImpl依旧不会启用事务机制。
因此,Spring建议在具体业务类上使用@Transactional注解。这样不管理
在方法处使用注解
方法处的注解会覆盖类定义处的注解,如果有些方法需要使用特殊的事务属性,则可以在类注解的基础上,提供方法注解:
//①类级的注解,适用于类中所有public的方法
@Transactional
public class BbtForumImpl implements BbtForum {
//②提供额外的注解信息,它将覆盖①处的类级注解
@Transactional(readOnly=true)
public Forum getForum(int forumId){
}
}
②处的方法注解提供了readOnly事务属性的设置,它将覆盖类级注解中默认的readOnly=false设置。
在不同的事务管理器
在一般情况下,一个应用仅需使用到一个事务管理器就可以了。如果希望在不同的地方使用不同的事务管理器,则可以通过如下的方法实现:
public class MultiForumService{
//①使用名为topic的事务管理器
@Transactional("topic")
public void addTopic(Topic topic){
...
}
//②使用名为forum的事务管理器
@Transactional("forum")
public void updateForum(Forum forum){
...
}
}
而topic和forum的事务管理器可以在XML中分别定义,如下所示:
<bean id="forumTxManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
p:dataSource-ref="forumDataSource" >
<qualifier value="forum" />
bean>
<bean id="topicTxManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
p:dataSource-ref="topicDataSource" >
<qualifier value="topic" />
bean>
在①处,为事务管理器指定了一个数据源,每个事务管理器都可以绑定一个独立的数据源。在②处,指定了一个可被@Transactional注解引用的事务管理器标识。
在一两处使用带标识的@Transactional也许挺适合的,但如果到处都使用,则显得比较哆嗦。可以自定义一个绑定到特定事务管理器的注解,然后直接使用这个自定义的注解进行标识:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional("forum")//①绑定到forum的事务管理器中
public @interface ForumTransactional{
}
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional("topic")//②绑定到topic的事务管理器中
public @interface TopicTransactional{
}
完成定义后,就可以用以下的方式对原来的代码进行调整了:
public class MultiForumService{
//①使用名为topic的事务管理器
@TopicTransactional
public void addTopic(Topic topic){
...
}
//②使用名为forum的事务管理器
@ForumTransactional
public void updateForum(Forum forum){
...
}
}
在web.xml中添加以下配置:
<context-param>
<param-name>contextConfigLocationparam-name>
<param-value>classpath*:spring/applicationContext-*.xmlparam-value>
context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListenerlistener-class>
listener>
<servlet>
<servlet-name>ierpservlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServletservlet-class>
servlet>
<servlet-mapping>
<servlet-name>ierpservlet-name>
<url-pattern>/*url-pattern>
servlet-mapping>
在①处,通过contextConfigLocation参数指定业务层Spring容器的配置文件(多个配置文件使用逗号分隔),ContextLoaderListener是一个ServletContextListener,它通过contextConfigLocation参数所指定的Spring配置文件启动”业务层”的Spring容器.
在②处,配置了名为ierp的DispatcherServlet,它默认自动加载/WEB-INF/ierp-servlet.xml(即
在③处通过
我们知道多个Spring容器之间可设置为父子级的关系,实现良好的解耦。在这里,”Web层”Spring容器将作为”业务层”Spring容器的子容器,退”Web层”容器可以引用“业务层”窗口的Bean,而“业务层”容器却访问不到”Web层”容器的Bean。
需要提醒的是,一个web.xml可以配置多个DispatcherServlet,通过其
DispatcherServlet遵循“契约优于配置”的原则,在大多数情况下,你无须进行额外的配置,只需按契约行事即可。
如果你确实要对DispatcherServlet的默认规则进行调整,DispatcherServlet是敞开胸怀的。下面是常用的一些配置参数,可以通过
在Spring Web上下文中配置FreeMarker
配置如下:
<bean class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer"
p:templateLoaderPath="/WEB-INF/template" 1-a
p:defaultEncoding="UTF-8" > 1-b
<property name="freemarkerSettings"> 1-c
<props>
<prop key="classic_compatible">trueprop>
props>
property>
bean>
<bean class="org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver"
p:order="5" 2-a
p:suffix=".ftl" 2-b
p:contentType="text/html; charset=utf-8" /> 2-c
在1处,我通过FreeMarkerConfigurer配置了FreeMarker的环境,首先在1-a处指定了模版文件的存放路径.由于我们的模版文件采用UTF-8编码格式,所以必须显工配置defaultEncoding属性,否则将采用系统默认的编码,造成乱码的现象.
FreeMarker拥有丰富的自定义属性,用户可以通过freemarkerSettings进行统一的设置,需要阅读FreeMarker的参考文档,以获取可配置的属性信息,这些属性名称都采用小写并用”_”连接,1-c处配置的class_compatible属性非常重要,否则当FreeMarker模版碰到值为null的对象属性时,将抛出异常.将classic_compatible设置为true后,FreeMarker将采购类似于JSTL标签的行为处理模型属性数据:返回一个空白字符串,而非抛出系统异常.
在搭建好FreeMarker的环境后,就可以通过FreeMarkerViewResolver视图解析器将”userListFtl”的逻辑视图名解析为一个对应的FreeMarkerView视图对象.
2-a外指定该视图解析器的优先级,它将优先于InteralResourceViewResolver执行(因为InteralResourceViewResolver默认的优先级最低).而2-b处指定了后缀,这新”userListFtl”的逻辑视图名将解析为”/WEB-INF/template/userListFtl.ftl”的视图对象。由于userListFtl.ftl模版最终产生的html,且我们希望使用UTF-8编码格式输出内容,所以需要通过2-c处的contentType属性进行相应配置,如果不指定该属性,输出的HTML将会产生中文乱码的问题。
现在有一个问题:Spring如何将上下文中的SpringMVC组件装配到DispatcherServlet中?通过查看DispatcherServlet的initStrategies()的代码,一切就真相大白了:
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);//①初始化上传文件解析器(直译为多部分解析器)
initLocaleResolver(context);//②初始化本地化解析器
initThemeResolver(context);//③初始化主题解析器
initHandlerMappings(context);//④初始处理器映射器
initHandlerAdapters(context);//⑤初始化处理器适配器
initHandlerExceptionResolvers(context);//⑥初始化处理器异常解析器
initRequestToViewNameTranslator(context);//⑦初始化请求到视图名翻译器
initViewResolvers(context);//⑧初始化视图解析器
}
initStrategies()方法将在WebApplicationContext初始化后自动执行,些时Spring上下文中的Bean已经初始化完毕。该方法的工作是通过反射机制查找并装配Spring容器中用户显式自定义的组件Bean,如果找不到再装配默认的组件实例。
Spring MVC定义了一套默认的组件实现类,也就是说即使在Spring窗口中没有显工定义组件Bean,DispatcherServlet也会装配好一套可用的默认组件。在org.springframework.web.servlet.jar包的org.springframework.web.servlet包路径下拥有一个DispatcherServlet.properties配置文件,该文件指定了DispatcherServletr所使用的默认组件。
##本地化解析器
org.springframework.web.servlet.LocaleResolver=
org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver
##主题解析器
org.springframework.web.servlet.ThemeResolver=
org.springframework.web.servlet.theme.FixedThemeResolver
##处理器映射
org.springframework.web.servlet.HandlerMapping=
org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\
org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping
##处理器适配器
org.springframework.web.servlet.HandlerAdapter=
org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\
org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\
org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter
##异常处理器
org.springframework.web.servlet.HandlerExceptionResolver=
org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerExceptionResolver,\
org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\
org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver
##视图名称翻译器
org.springframework.web.servlet.RequestToViewNameTranslator=
org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator
##视图解析器
org.springframework.web.servlet.ViewResolver=
org.springframework.web.servlet.view.InternalResourceViewResolver
如果用户希望采用非默认类型的组件,则只需在Spring配置文件中配置自定义的组件Bean即可,Spring MVC一旦发现上下文中拥有用户自定义的组件,就不会使用默认的组件。概言之,当DispatcherServlet初始化后,就会自动扫描上下文的Bean,根据名称或类型匹配的机制查找自定义的组件,找不到时则使用DispatcherServlet.properties定义的默认组件。通过下表可以进一步深入了解DispatcherServlet装配每种组件的过程:
组件类型 |
发现机制 |
文件上传解析器 (☆) |
如果用户没有在上下文中显示定义这一类型的组件,DispatcherServlet中将不会拥有该类型的组件 |
本地化解析器 (☆) |
|
主题解析器 (☆) |
|
处理器映射器 (★) |
|
处理器适配器 (★) |
|
处理器异常解析器 (★) |
|
视图名翻译器 (☆) |
|
视图解析器 (★) |
|
有些组件最多只允许一个实例,如MultipartResolver、LocaleResolver等,上表使用☆进行标注。而另一类型的组件允许存在多个,如HandlerMapping、HandlerAdapter等,上表使用★进行标注。同一类型的组件如果存在多个,它们之间的优先级顺序如何确定呢?这些组件都实现了orag.springframework.core.Ordered接口,可通过order属性确定优先顺序,值越小优先级越高。
Spring MVC提供了几个处理XML\JSON格式的请求/响应消息的HttpMessageConverter。MappingJacksonHttpMessageConverter是处理JSON格式的请求或响应消息的,下面我们就JSON格式举个例子,在Spring XML配置文件中添加以下配置:
<bean
class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
<property name="messageConverters">
<list>
1
<bean class="org.springframework.http.converter.BufferedImageHttpMessageConverter" />
<bean class="org.springframework.http.converter.ByteArrayHttpMessageConverter" />
<bean class="org.springframework.http.converter.StringHttpMessageConverter" />
<bean class="org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter" />
2-a
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
<property name="supportedMediaTypes"> 2-b
<list>
<value>application/json;charset=UTF-8value> 2-c
list>
property>
bean>
list>
property>
bean>
1处配置的AnnotationMethodHandlerAdapter默认的加载的HttpMessageConverter,2-a添加处理JSON格式的请求或响应消息。2-b是表示响应的MediaType类型,2-c处添加JSON格式的类型。
装配好处理JSON的HttpMessageConverter后,我们在控制器中编写相应的方法,请看下面。
package com.sage.ierp.controller;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import com.sage.jfianl.model.User;
@Controller
public class JsonController {
@ModelAttribute("user")
public User getUser(){
User user = new User();
user.setName("Saleson Lee");
user.setAge(25);
return user;
}
@RequestMapping("json")
public @ResponseBody User json(@ModelAttribute("user") User user){
return user;
}
@RequestMapping("json2")
public ResponseEntity
return new ResponseEntity
}
}
Ibates
测试