Spring AOP

By | 2021年12月30日

1 基于注解的Spring AOP的配置和使用

Spring的一个核心功能是IOC,就是将Bean初始化加载到容器中,Bean是如何加载到容器的,可以使用Spring注解方式或者Spring XML配置方式。我们现在做的一些非业务,如:日志、事务、安全等都会写在业务代码中(也即是说,这些非业务类横切于业务类),但这些代码往往是重复,复制——粘贴式的代码会给程序的维护带来不便,AOP就实现了把这些业务需求与系统需求分开来做。这种解决的方式也称代理机制。
先来了解一下AOP的相关概念:

  • 切面(Aspect):官方的抽象定义为“一个关注点的模块化,这个关注点可能会横切多个对象”,在本例中,“切面”就是类TestAspect所关注的具体行为,例如,AServiceImpl.barA()的调用就是切面TestAspect所关注的行为之一。“切面”在ApplicationContext中<aop:aspect>来配置。
  • 连接点(Joinpoint) :程序执行过程中的某一行为,例如,UserService.get的调用或者UserService.delete抛出异常等行为。
  • 通知(Advice) :“切面”对于某个“连接点”所产生的动作,例如,TestAspect中对com.spring.service包下所有类的方法进行日志记录的动作就是一个Advice。其中,一个“切面”可以包含多个“Advice”,例如ServiceAspect。
  • 切入点(Pointcut) :匹配连接点的断言,在AOP中通知和一个切入点表达式关联。例如,TestAspect中的所有通知所关注的连接点,都由切入点表达式execution(* com.spring.service.*.*(..))来决定。
  • 目标对象(Target Object) :被一个或者多个切面所通知的对象。例如,AServcieImpl和BServiceImpl,当然在实际运行时,Spring AOP采用代理实现,实际AOP操作的是TargetObject的代理对象。
  • AOP代理(AOP Proxy) :在Spring AOP中有两种代理方式,JDK动态代理和CGLIB代理。默认情况下,TargetObject实现了接口时,则采用JDK动态代理,例如,AServiceImpl;反之,采用CGLIB代理,例如,BServiceImpl。强制使用CGLIB代理需要将 <aop:config>的 proxy-target-class属性设为true。

通知(Advice)类型:

  • 前置通知(Before advice):在某连接点(JoinPoint)之前执行的通知,但这个通知不能阻止连接点前的执行。ApplicationContext中在<aop:aspect>里面使用<aop:before>元素进行声明。例如,TestAspect中的doBefore方法。
  • 后置通知(After advice):当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)。ApplicationContext中在<aop:aspect>里面使用<aop:after>元素进行声明。例如,ServiceAspect中的returnAfter方法,所以Teser中调用UserService.delete抛出异常时,returnAfter方法仍然执行。
  • 返回后通知(After return advice):在某连接点正常完成后执行的通知,不包括抛出异常的情况。ApplicationContext中在<aop:aspect>里面使用<after-returning>元素进行声明。
  • 环绕通知(Around advice):包围一个连接点的通知,类似Web中Servlet规范中的Filter的doFilter方法。可以在方法的调用前后完成自定义的行为,也可以选择不执行。ApplicationContext中在<aop:aspect>里面使用<aop:around>元素进行声明。例如,ServiceAspect中的around方法。
  • 抛出异常后通知(After throwing advice):在方法抛出异常退出时执行的通知。ApplicationContext中在<aop:aspect>里面使用<aop:after-throwing>元素进行声明。例如,ServiceAspect中的returnThrow方法。

注:可以将多个通知应用到一个目标对象上,即可以将多个切面织入到同一目标对象。
使用Spring AOP可以基于两种方式,一种是比较方便和强大的注解方式,另一种则是中规中矩的xml配置方式。

先说注解,使用注解配置Spring AOP总体分为两步,第一步是在xml文件中声明激活自动扫描组件功能,同时激活自动代理功能(同时在xml中添加一个UserService的普通服务层组件,来测试AOP的注解功能):

<?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"
  xsi:schemaLocation="http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.1.xsd
    http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">

  <!-- 激活组件扫描功能,在包cn.ysh.studio.spring.aop及其子包下面自动扫描通过注解配置的组件 -->
  <context:component-scan base-package="cn.ysh.studio.spring.aop"/>
  <!-- 激活自动代理功能 -->
  <aop:aspectj-autoproxy proxy-target-class="true"/>
  
  <!-- 用户服务对象 -->
  <bean id="userService" class="cn.ysh.studio.spring.aop.service.UserService" />
</beans>

第二步是为Aspect切面类添加注解:

package cn.ysh.studio.spring.aop.aspect;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
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.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

/**
 * 系统服务组件Aspect切面Bean
 */
//声明这是一个组件
@Component
//声明这是一个切面Bean
@Aspect
public class ServiceAspect {

  private final static Log log = LogFactory.getLog(ServiceAspect.class);
  
  //配置切入点,该方法无方法体,主要为方便同类中其他方法使用此处配置的切入点
  @Pointcut("execution(* cn.ysh.studio.spring.aop.service..*(..))")
  public void aspect(){	}
  
  /*
   * 配置前置通知,使用在方法aspect()上注册的切入点
   * 同时接受JoinPoint切入点对象,可以没有该参数
   */
  @Before("aspect()")
  public void before(JoinPoint joinPoint){
    if(log.isInfoEnabled()){
      log.info("before " + joinPoint);
    }
  }
  
  //配置后置通知,使用在方法aspect()上注册的切入点
  @After("aspect()")
  public void after(JoinPoint joinPoint){
    if(log.isInfoEnabled()){
      log.info("after " + joinPoint);
    }
  }
  
  //配置环绕通知,使用在方法aspect()上注册的切入点
  @Around("aspect()")
  public void around(JoinPoint joinPoint){
    long start = System.currentTimeMillis();
    try {
      ((ProceedingJoinPoint) joinPoint).proceed();
      long end = System.currentTimeMillis();
      if(log.isInfoEnabled()){
        log.info("around " + joinPoint + "\tUse time : " + (end - start) + " ms!");
      }
    } catch (Throwable e) {
      long end = System.currentTimeMillis();
      if(log.isInfoEnabled()){
        log.info("around " + joinPoint + "\tUse time : " + (end - start) + " ms with exception : " + e.getMessage());
      }
    }
  }
  
  //配置后置返回通知,使用在方法aspect()上注册的切入点
  @AfterReturning("aspect()")
  public void afterReturn(JoinPoint joinPoint){
    if(log.isInfoEnabled()){
      log.info("afterReturn " + joinPoint);
    }
  }
  
  //配置抛出异常后通知,使用在方法aspect()上注册的切入点
  @AfterThrowing(pointcut="aspect()", throwing="ex")
  public void afterThrow(JoinPoint joinPoint, Exception ex){
    if(log.isInfoEnabled()){
      log.info("afterThrow " + joinPoint + "\t" + ex.getMessage());
    }
  }
  
}

测试代码:

package cn.ysh.studio.spring.aop;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import cn.ysh.studio.spring.aop.service.UserService;
import cn.ysh.studio.spring.mvc.bean.User;

/**
 * Spring AOP测试
 */
public class Tester {
  private final static Log log = LogFactory.getLog(Tester.class);
  public static void main(String[] args) {
    //启动Spring容器
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    //获取service组件
    UserService service = (UserService) context.getBean("userService");
    //以普通的方式调用UserService对象的三个方法
    User user = service.get(1L);
    service.save(user);
    try {
      service.delete(1L);
    } catch (Exception e) {
      if(log.isWarnEnabled()){
        log.warn("Delete user : " + e.getMessage());
      }
    }
  }
}

控制台输出如下:

INFO [spring.aop.aspect.ServiceAspect:40] before execution(User cn.ysh.studio.spring.aop.service.UserService.get(long))
INFO [spring.aop.service.UserService:19] getUser method . . .
INFO [spring.aop.aspect.ServiceAspect:60] around execution(User cn.ysh.studio.spring.aop.service.UserService.get(long))	Use time : 42 ms!
INFO [spring.aop.aspect.ServiceAspect:48] after execution(User cn.ysh.studio.spring.aop.service.UserService.get(long))
INFO [spring.aop.aspect.ServiceAspect:74] afterReturn execution(User cn.ysh.studio.spring.aop.service.UserService.get(long))
INFO [spring.aop.aspect.ServiceAspect:40] before execution(void cn.ysh.studio.spring.aop.service.UserService.save(User))
INFO [spring.aop.service.UserService:26] saveUser method . . .
INFO [spring.aop.aspect.ServiceAspect:60] around execution(void cn.ysh.studio.spring.aop.service.UserService.save(User))	Use time : 2 ms!
INFO [spring.aop.aspect.ServiceAspect:48] after execution(void cn.ysh.studio.spring.aop.service.UserService.save(User))
INFO [spring.aop.aspect.ServiceAspect:74] afterReturn execution(void cn.ysh.studio.spring.aop.service.UserService.save(User))
INFO [spring.aop.aspect.ServiceAspect:40] before execution(boolean cn.ysh.studio.spring.aop.service.UserService.delete(long))
INFO [spring.aop.service.UserService:32] delete method . . .
INFO [spring.aop.aspect.ServiceAspect:65] around execution(boolean cn.ysh.studio.spring.aop.service.UserService.delete(long))	Use time : 5 ms with exception : spring aop ThrowAdvice演示
INFO [spring.aop.aspect.ServiceAspect:48] after execution(boolean cn.ysh.studio.spring.aop.service.UserService.delete(long))
INFO [spring.aop.aspect.ServiceAspect:74] afterReturn execution(boolean cn.ysh.studio.spring.aop.service.UserService.delete(long))
WARN [studio.spring.aop.Tester:32] Delete user : Null return value from advice does not match primitive return type for: public boolean cn.ysh.studio.spring.aop.service.UserService.delete(long) throws java.lang.Exception

可以看到,正如我们预期的那样,虽然我们并没有对UserSerivce类包括其调用方式做任何改变,但是Spring仍然拦截到了其中方法的调用,或许这正是AOP的魔力所在。

再简单说一下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:aop="http://www.springframework.org/schema/aop"
  xsi:schemaLocation="http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.1.xsd
    http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">

  <!-- 系统服务组件的切面Bean -->
  <bean id="serviceAspect" class="cn.ysh.studio.spring.aop.aspect.ServiceAspect"/>
  <!-- AOP配置 -->
  <aop:config>
    <!-- 声明一个切面,并注入切面Bean,相当于@Aspect -->
    <aop:aspect id="simpleAspect" ref="serviceAspect">
      <!-- 配置一个切入点,相当于@Pointcut -->
      <aop:pointcut expression="execution(* cn.ysh.studio.spring.aop.service..*(..))" id="simplePointcut"/>
      <!-- 配置通知,相当于@Before、@After、@AfterReturn、@Around、@AfterThrowing -->
      <aop:before pointcut-ref="simplePointcut" method="before"/>
      <aop:after pointcut-ref="simplePointcut" method="after"/>
      <aop:after-returning pointcut-ref="simplePointcut" method="afterReturn"/>
      <aop:after-throwing pointcut-ref="simplePointcut" method="afterThrow" throwing="ex"/>
    </aop:aspect>
  </aop:config>
</beans>

个人觉得不如注解灵活和强大,你可以不同意这个观点,但是不知道如下的代码会不会让你的想法有所改善:

//配置前置通知,拦截返回值为cn.ysh.studio.spring.mvc.bean.User的方法
@Before("execution(cn.ysh.studio.spring.mvc.bean.User cn.ysh.studio.spring.aop.service..*(..))")
public void beforeReturnUser(JoinPoint joinPoint){
  if(log.isInfoEnabled()){
    log.info("beforeReturnUser " + joinPoint);
  }
}

//配置前置通知,拦截参数为cn.ysh.studio.spring.mvc.bean.User的方法
@Before("execution(* cn.ysh.studio.spring.aop.service..*(cn.ysh.studio.spring.mvc.bean.User))")
public void beforeArgUser(JoinPoint joinPoint){
  if(log.isInfoEnabled()){
    log.info("beforeArgUser " + joinPoint);
  }
}

//配置前置通知,拦截含有long类型参数的方法,并将参数值注入到当前方法的形参id中
@Before("aspect()&&args(id)")
public void beforeArgId(JoinPoint joinPoint, long id){
  if(log.isInfoEnabled()){
    log.info("beforeArgId " + joinPoint + "\tID:" + id);
  }
}

附上UserService的代码(其实很简单):

package cn.ysh.studio.spring.aop.service;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import cn.ysh.studio.spring.mvc.bean.User;

/**
 * 用户服务模型
 */
public class UserService {

  private final static Log log = LogFactory.getLog(UserService.class);
  
  public User get(long id){
    if(log.isInfoEnabled()){
      log.info("getUser method . . .");
    }
    return new User();
  }
  
  public void save(User user){
    if(log.isInfoEnabled()){
      log.info("saveUser method . . .");
    }
  }
  
  public boolean delete(long id) throws Exception{
    if(log.isInfoEnabled()){
      log.info("delete method . . .");
      throw new Exception("spring aop ThrowAdvice演示");
    }
    return false;
  }
}

应该说学习Spring AOP有两个难点,第一点在于理解AOP的理念和相关概念,第二点在于灵活掌握和使用切入点表达式。概念的理解通常不在一朝一夕,慢慢浸泡的时间长了,自然就明白了,下面我们简单地介绍一下切入点表达式的配置规则吧。通常情况下,表达式中使用”execution“就可以满足大部分的要求。

最后说一下通知参数

可以通过args来绑定参数,这样就可以在通知(Advice)中访问具体参数了。例如,<aop:aspect>配置如下:

<aop:config>
  <aop:aspect id="TestAspect" ref="aspectBean">
   <aop:pointcut id="businessService"
    expression="execution(* com.spring.service.*.*(String,..)) and args(msg,..)" />
    <aop:after pointcut-ref="businessService" method="doAfter"/>
  </aop:aspect>
</aop:config>

上面的代码args(msg,..)是指将切入点方法上的第一个String类型参数添加到参数名为msg的通知的入参上,这样就可以直接使用该参数啦。

访问当前的连接点

在上面的Aspect切面Bean中已经看到了,每个通知方法第一个参数都是JoinPoint。其实,在Spring中,任何通知(Advice)方法都可以将第一个参数定义为 org.aspectj.lang.JoinPoint类型用以接受当前连接点对象。JoinPoint接口提供了一系列有用的方法, 比如 getArgs() (返回方法参数)、getThis() (返回代理对象)、getTarget() (返回目标)、getSignature() (返回正在被通知的方法相关信息)和 toString() (打印出正在被通知的方法的有用信息)。

章节1来源:http://ntzrj513.blog.163.com/blog/static/27945612201362232315

2 拦截器与Filter的区别

AOP为Aspect Oriented Programming的缩写,意为:面向切面编程。Spring的拦截器与Servlet的Filter有相似之处,比如二者都是AOP编程思想的体现,都能实现权限检查、日志记录等。不同的是:

  1. 使用范围不同:Filter是Servlet规范规定的,只能用于Web程序中。而拦截器既可以用于Web程序,也可以用于Application、Swing程序中。
  2. 规范不同:Filter是在Servlet规范中定义的,是Servlet容器支持的。而拦截器是在Spring容器内的,是Spring框架支持的。
  3. 使用的资源不同:同其他的代码块一样,拦截器也是一个Spring的组件,归Spring管理,配置在Spring文件中,因此能使用Spring里的任何资源、对象,例如Service对象、数据源、事务管理等,通过IoC注入到拦截器即可;而Filter则不能。
  4. 深度不同:Filter只在Servlet前后起作用。而拦截器能够深入到方法前后、异常抛出前后等,因此拦截器的使用具有更大的弹性。所以在Spring构架的程序中,要优先使用拦截器。

3 切入点表达式

下面这个切面配置,针对Service包上所有方法

<aop:config> 
    <aop:pointcut id="myPointcut" expression="execution(* *..service*..*(..))"/> 
    <aop:advisor pointcut-ref="myPointcut" advice-ref="myAdvice"/> 
</aop:config>

切面配置好后,接着我们来控制切面上的事务,即使用AOP事物

<tx:advice id="myAdvice" transaction-manager="transactionManager"> 
    <tx:attributes> 
        <tx:method name="insert*" rollback-for="Exception"/> 
        <tx:method name="update*" rollback-for="Exception"/> 
        <tx:method name="delete*" rollback-for="Exception"/> 
        <tx:method name="*" read-only="true" rollback-for="Exception"/> 
    </tx:attributes> 
</tx:advice>

通过切面和通知的配置,就为所有的insert、update、delete开头的方法添加上了write事务,对其他方法添加上了只读事务。

以下文档来自Spring中文开发指南2.5文档,由满江红开源组织翻译:

Spring AOP 用户可能会经常使用 execution切入点指示符。执行表达式的格式如下:

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)throws-pattern?)
  • modifiers-pattern:方法的操作权限
  • ret-type-pattern:返回值
  • declaring-type-pattern:方法所在的包
  • name-pattern:方法名
  • parm-pattern:参数名
  • throws-pattern:异常

其中,除ret-type-pattern和name-pattern之外,其他都是可选的。上例中,execution(* com.spring.service.*.*(..))表示com.spring.service包下,返回值为任意类型;方法名任意;参数不作限制的所有方法。除了返回类型模式(上面代码片断中的ret-type-pattern),名字模式和参数模式以外, 所有的部分都是可选的。返回类型模式决定了方法的返回类型必须依次匹配一个连接点。 你会使用的最频繁的返回类型模式是*,它代表了匹配任意的返回类型。 一个全限定的类型名将只会匹配返回给定类型的方法。名字模式匹配的是方法名。 你可以使用*通配符作为所有或者部分命名模式。 参数模式稍微有点复杂:()匹配了一个不接受任何参数的方法, 而(..)匹配了一个接受任意数量参数的方法(零或者更多)。 模式(*)匹配了一个接受一个任何类型的参数的方法。 模式(*,String)匹配了一个接受两个参数的方法,第一个可以是任意类型, 第二个则必须是String类型。更多的信息请参阅AspectJ编程指南中 语言语义的部分。
下面给出一些通用切入点表达式的例子。

  • 任意公共方法的执行
    execution(public * *(..))
  • 任何一个名字以“set”开始的方法的执行
    execution(* set*(..))
  • AccountService接口定义的任意方法的执行
    execution(* com.xyz.service.AccountService.*(..))
  • 在service包中定义的任意方法的执行
    execution(* com.xyz.service.*.*(..))
  • 在service包或其子包中定义的任意方法的执行:
    execution(* com.xyz.service..*.*(..))
  • 在service包中的任意连接点(在Spring AOP中只是方法执行):
    within(com.xyz.service.*)
  • 在service包或其子包中的任意连接点(在Spring AOP中只是方法执行):
    within(com.xyz.service..*)
  • 实现了AccountService接口的代理对象的任意连接点 (在Spring AOP中只是方法执行):
    this(com.xyz.service.AccountService)
    ‘this’在绑定表单中更加常用:- 请参见后面的通知一节中了解如何使得代理对象在通知体内可用。
  • 实现AccountService接口的目标对象的任意连接点 (在Spring AOP中只是方法执行):
    target(com.xyz.service.AccountService)
    ‘target’在绑定表单中更加常用:- 请参见后面的通知一节中了解如何使得目标对象在通知体内可用。
  • 任何一个只接受一个参数,并且运行时所传入的参数是Serializable 接口的连接点(在Spring AOP中只是方法执行)
    args(java.io.Serializable)
    ‘args’在绑定表单中更加常用:- 请参见后面的通知一节中了解如何使得方法参数在通知体内可用。
    请注意在例子中给出的切入点不同于 execution(* *(java.io.Serializable)): args版本只有在动态运行时候传入参数是Serializable时才匹配,而execution版本在方法签名中声明只有一个 Serializable类型的参数时候匹配。
  • 目标对象中有一个 @Transactional 注解的任意连接点 (在Spring AOP中只是方法执行)@target(org.springframework.transaction.annotation.Transactional)
    ‘@target’在绑定表单中更加常用:- 请参见后面的通知一节中了解如何使得注解对象在通知体内可用。
  • 任何一个目标对象声明的类型有一个 @Transactional 注解的连接点 (在Spring AOP中只是方法执行):
    @within(org.springframework.transaction.annotation.Transactional)
    ‘@within’在绑定表单中更加常用:- 请参见后面的通知一节中了解如何使得注解对象在通知体内可用。
  • 任何一个执行的方法有一个 @Transactional 注解的连接点 (在Spring AOP中只是方法执行)@annotation(org.springframework.transaction.annotation.Transactional)
    ‘@annotation’在绑定表单中更加常用:- 请参见后面的通知一节中了解如何使得注解对象在通知体内可用。
  • 任何一个只接受一个参数,并且运行时所传入的参数类型具有@Classified 注解的连接点(在Spring AOP中只是方法执行)
    @args(com.xyz.security.Classified)
    ‘@args’在绑定表单中更加常用:- 请参见后面的通知一节中了解如何使得注解对象在通知体内可用。
  • 任何一个在名为’tradeService’的Spring bean之上的连接点 (在Spring AOP中只是方法执行):
    bean(tradeService)
  • 任何一个在名字匹配通配符表达式’*Service’的Spring bean之上的连接点 (在Spring AOP中只是方法执行):
    bean(*Service)

4 component-scan

使用”<context:component-scan base-package=”xxx”/>”可以自动扫描以下注解:

  • @Controller 声明Action组件
  • @Service 声明Service组件 @Service(“myMovieLister”)
  • @Repository 声明Dao组件
  • @Component 泛指组件, 当不好归类时.
  • @Resource 用于注入,( j2ee提供的 ) 默认按名称装配,@Resource(name=”beanName”)
  • @Autowired 用于注入,(srping提供的) 默认按类型装配
  • @Scope(“prototype”) 设定bean的作用域
  • @PostConstruct
  • @PreDestroy
  • @ResponseBody
  • @RequestMapping(“/menu”) 请求映射

5 Spring注解详解

在很多情况下,注释配置比 XML 配置更受欢迎,本节我们将向您讲述使用注释进行 Bean 定义和依赖注入的内容。

5.1 @Autowired

使用 @Autowired 注释的 Boss.java:

package com.baobaotao;
import org.springframework.beans.factory.annotation.Autowired;

public class Boss {

    @Autowired
    private Car car;

    @Autowired
    private Office office;

    …
}

Spring 通过一个 BeanPostProcessor 对 @Autowired 进行解析,所以要让@Autowired 起作用必须事先在 Spring 容器中声明AutowiredAnnotationBeanPostProcessor Bean。

让 @Autowired 注释工作起来:

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

    <!-- 该 BeanPostProcessor 将自动起作用,对标注 @Autowired 的 Bean 进行自动注入 -->
    <bean class="org.springframework.beans.factory.annotation.
        AutowiredAnnotationBeanPostProcessor"/>

    <!-- 移除 boss Bean 的属性注入配置的信息 -->
    <bean id="boss" class="com.baobaotao.Boss"/>
 
    <bean id="office" class="com.baobaotao.Office">
        <property name="officeNo" value="001"/>
    </bean>
    <bean id="car" class="com.baobaotao.Car" scope="singleton">
        <property name="brand" value=" 红旗 CA72"/>
        <property name="price" value="2000"/>
    </bean>
</beans>

这样,当 Spring 容器启动时,AutowiredAnnotationBeanPostProcessor 将扫描 Spring 容器中所有 Bean,当发现 Bean 中拥有@Autowired 注释时就找到和其匹配(默认按类型匹配)的 Bean,并注入到对应的地方中去。

按照上面的配置,Spring 将直接采用 Java 反射机制对 Boss 中的 car 和 office 这两个私有成员变量进行自动注入。所以对成员变量使用@Autowired 后,您大可将它们的 setter 方法(setCar() 和 setOffice())从 Boss 中删除。

当然,您也可以通过 @Autowired 对方法或构造函数进行标注,来看下面的代码:

将 @Autowired 注释标注在 Setter 方法上:

package com.baobaotao;

public class Boss {
    private Car car;
    private Office office;

     @Autowired
    public void setCar(Car car) {
        this.car = car;
    }
 
    @Autowired
    public void setOffice(Office office) {
        this.office = office;
    }
    …
}

这时,@Autowired 将查找被标注的方法的入参类型的 Bean,并调用方法自动注入这些 Bean。而下面的使用方法则对构造函数进行标注:

将 @Autowired 注释标注在构造函数上:

package com.baobaotao;

public class Boss {
    private Car car;
    private Office office;
 
    @Autowired
    public Boss(Car car ,Office office){
        this.car = car;
        this.office = office ;
    }
 
    …
}

由于 Boss() 构造函数有两个入参,分别是 car 和 office,@Autowired 将分别寻找和它们类型匹配的 Bean,将它们作为Boss(Car car ,Office office)的入参来创建 Boss Bean。

当候选 Bean 数目不为 1 时的应对方法

在默认情况下使用 @Autowired 注释进行自动注入时,Spring 容器中匹配的候选 Bean 数目必须有且仅有一个。当找不到一个匹配的 Bean 时,Spring 容器将抛出BeanCreationException 异常,并指出必须至少拥有一个匹配的 Bean。我们可以来做一个实验:

候选 Bean 数目为 0 时

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

    <bean id="boss" class="com.baobaotao.Boss"/>

    <!-- 将 office Bean 注释掉 -->
    <!-- <bean id="office" class="com.baobaotao.Office">
    <property name="officeNo" value="001"/>
    </bean>-->

    <bean id="car" class="com.baobaotao.Car" scope="singleton">
        <property name="brand" value=" 红旗 CA72"/>
        <property name="price" value="2000"/>
    </bean>
</beans>

由于 office Bean 被注释掉了,所以 Spring 容器中将没有类型为 Office 的 Bean 了,而 Boss 的office 属性标注了 @Autowired,当启动 Spring 容器时,异常就产生了。

当不能确定 Spring 容器中一定拥有某个类的 Bean 时,可以在需要自动注入该类 Bean 的地方可以使用 @Autowired(required = false),这等于告诉 Spring:在找不到匹配 Bean 时也不报错。来看一下具体的例子:

使用 @Autowired(required = false)

package com.baobaotao;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Required;

public class Boss {

    private Car car;
    private Office office;

    @Autowired
    public void setCar(Car car) {
        this.car = car;
    }
    @Autowired(required = false)
    public void setOffice(Office office) {
        this.office = office;
    }
    …
}

当然,一般情况下,使用 @Autowired 的地方都是需要注入 Bean 的,使用了自动注入而又允许不注入的情况一般仅会在开发期或测试期碰到(如为了快速启动 Spring 容器,仅引入一些模块的 Spring 配置文件),所以@Autowired(required = false) 会很少用到。

和找不到一个类型匹配 Bean 相反的一个错误是:如果 Spring 容器中拥有多个候选 Bean,Spring 容器在启动时也会抛出 BeanCreationException 异常。来看下面的例子:

在 beans.xml 中配置两个 Office 类型的 Bean:

… 
<bean id="office" class="com.baobaotao.Office">
    <property name="officeNo" value="001"/>
</bean>
<bean id="office2" class="com.baobaotao.Office">
    <property name="officeNo" value="001"/>
</bean>
…

我们在 Spring 容器中配置了两个类型为 Office 类型的 Bean,当对 Boss 的 office 成员变量进行自动注入时,Spring 容器将无法确定到底要用哪一个 Bean,因此异常发生了。

Spring 允许我们通过 @Qualifier 注释指定注入 Bean 的名称,这样歧义就消除了,可以通过下面的方法解决异常:

使用 @Qualifier 注释指定注入 Bean 的名称:

@Autowired
public void setOffice(@Qualifier("office")Office office) {
    this.office = office;
}

@Qualifier(“office”) 中的 office 是 Bean 的名称,所以 @Autowired 和@Qualifier 结合使用时,自动注入的策略就从 byType 转变成 byName 了。@Autowired 可以对成员变量、方法以及构造函数进行注释,而@Qualifier 的标注对象是成员变量、方法入参、构造函数入参。正是由于注释对象的不同,所以 Spring 不将 @Autowired 和@Qualifier 统一成一个注释类。下面是对成员变量和构造函数入参进行注释的代码:

对成员变量进行注释:

对成员变量使用 @Qualifier 注释:

public class Boss {
    @Autowired
    private Car car;
 
    @Autowired
    @Qualifier("office")
    private Office office;
    …
}

对构造函数入参进行注释:

对构造函数变量使用 @Qualifier 注释:

public class Boss {
    private Car car;
    private Office office;

    @Autowired
    public Boss(Car car , @Qualifier("office")Office office){
        this.car = car;
        this.office = office ;
    }
}

@Qualifier 只能和 @Autowired 结合使用,是对 @Autowired 有益的补充。一般来讲,@Qualifier 对方法签名中入参进行注释会降低代码的可读性,而对成员变量注释则相对好一些。

使用 JSR-250 的注释

Spring 不但支持自己定义的 @Autowired 的注释,还支持几个由 JSR-250 规范定义的注释,它们分别是 @Resource、@PostConstruct 以及 @PreDestroy。

5.2 @Resource

@Resource 的作用相当于 @Autowired,只不过 @Autowired 按 byType 自动注入,面@Resource 默认按 byName 自动注入罢了。@Resource 有两个属性是比较重要的,分别是 name 和 type,Spring 将@Resource 注释的 name 属性解析为 Bean 的名字,而 type 属性则解析为 Bean 的类型。所以如果使用 name 属性,则使用 byName 的自动注入策略,而使用 type 属性时则使用 byType 自动注入策略。如果既不指定 name 也不指定 type 属性,这时将通过反射机制使用 byName 自动注入策略。

Resource 注释类位于 Spring 发布包的 lib/j2ee/common-annotations.jar 类包中,因此在使用之前必须将其加入到项目的类库中。来看一个使用@Resource的例子:

使用 @Resource 注释的 Boss.java:

package com.baobaotao;

import javax.annotation.Resource;

public class Boss {
    // 自动注入类型为 Car 的 Bean
    @Resource
    private Car car;

    // 自动注入 bean 名称为 office 的 Bean
    @Resource(name = "office")
    private Office office;
}

一般情况下,我们无需使用类似于 @Resource(type=Car.class) 的注释方式,因为 Bean 的类型信息可以通过 Java 反射从代码中获取。要让 JSR-250 的注释生效,除了在 Bean 类中标注这些注释外,还需要在 Spring 容器中注册一个负责处理这些注释的 BeanPostProcessor:<bean class="org.springframework.context.annotation.CommonAnnotationBeanPostProcessor"/>
CommonAnnotationBeanPostProcessor 实现了 BeanPostProcessor 接口,它负责扫描使用了 JSR-250 注释的 Bean,并对它们进行相应的操作。

5.3 @PostConstruct 和 @PreDestroy

Spring 容器中的 Bean 是有生命周期的,Spring 允许在 Bean 在初始化完成后以及 Bean 销毁前执行特定的操作,您既可以通过实现 InitializingBean/DisposableBean 接口来定制初始化之后 / 销毁之前的操作方法,也可以通过 <bean> 元素的 init-method/destroy-method 属性指定初始化之后 / 销毁之前调用的操作方法。关于 Spring 的生命周期,笔者在《精通 Spring 2.x—企业应用开发精解》第 3 章进行了详细的描述,有兴趣的读者可以查阅。

JSR-250 为初始化之后/销毁之前方法的指定定义了两个注释类,分别是 @PostConstruct 和 @PreDestroy,这两个注释只能应用于方法上。标注了 @PostConstruct 注释的方法将在类实例化后调用,而标注了 @PreDestroy 的方法将在类销毁之前调用。

使用 @PostConstruct 和 @PreDestroy 注释的 Boss.java:

package com.baobaotao;

import javax.annotation.Resource;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

public class Boss {
    @Resource
    private Car car;

    @Resource(name = "office")
    private Office office;

    @PostConstruct
    public void postConstruct1(){
        System.out.println("postConstruct1");
    }

    @PreDestroy
    public void preDestroy1(){
        System.out.println("preDestroy1"); 
    }
    …
}

您只需要在方法前标注 @PostConstruct 或 @PreDestroy,这些方法就会在 Bean 初始化后或销毁之前被 Spring 容器执行了。

我们知道,不管是通过实现 InitializingBean/DisposableBean 接口,还是通过 <bean> 元素的init-method/destroy-method 属性进行配置,都只能为 Bean 指定一个初始化 / 销毁的方法。但是使用 @PostConstruct 和 @PreDestroy 注释却可以指定多个初始化 / 销毁方法,那些被标注 @PostConstruct 或@PreDestroy 注释的方法都会在初始化 / 销毁时被执行。

通过以下的测试代码,您将可以看到 Bean 的初始化 / 销毁方法是如何被执行的:

测试类代码:

package com.baobaotao;

import org.springframework.context.support.ClassPathXmlApplicationContext;

public class AnnoIoCTest {

    public static void main(String[] args) {
        String[] locations = {"beans.xml"};
        ClassPathXmlApplicationContext ctx = 
            new ClassPathXmlApplicationContext(locations);
        Boss boss = (Boss) ctx.getBean("boss");
        System.out.println(boss);
        ctx.destroy();// 关闭 Spring 容器,以触发 Bean 销毁方法的执行
    }
}

这时,您将看到标注了 @PostConstruct 的 postConstruct1() 方法将在 Spring 容器启动时,创建Boss Bean 的时候被触发执行,而标注了 @PreDestroy注释的 preDestroy1() 方法将在 Spring 容器关闭前销毁Boss Bean 的时候被触发执行。

使用 <context:annotation-config/> 简化配置

Spring 2.1 添加了一个新的 context 的 Schema 命名空间,该命名空间对注释驱动、属性文件引入、加载期织入等功能提供了便捷的配置。我们知道注释本身是不会做任何事情的,它仅提供元数据信息。要使元数据信息真正起作用,必须让负责处理这些元数据的处理器工作起来。

而我们前面所介绍的 AutowiredAnnotationBeanPostProcessor 和 CommonAnnotationBeanPostProcessor 就是处理这些注释元数据的处理器。但是直接在 Spring 配置文件中定义这些 Bean 显得比较笨拙。Spring 为我们提供了一种方便的注册这些BeanPostProcessor 的方式,这就是 <context:annotation-config/>。请看下面的配置:

调整 beans.xml 配置文件:

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

    <bean id="boss" class="com.baobaotao.Boss"/>
    <bean id="office" class="com.baobaotao.Office">
        <property name="officeNo" value="001"/>
    </bean>
    <bean id="car" class="com.baobaotao.Car" scope="singleton">
        <property name="brand" value=" 红旗 CA72"/>
        <property name="price" value="2000"/>
    </bean>
</beans>

<context:annotationconfig/> 将隐式地向 Spring 容器注册AutowiredAnnotationBeanPostProcessor、CommonAnnotationBeanPostProcessor、PersistenceAnnotationBeanPostProcessor 以及equiredAnnotationBeanPostProcessor 这 4 个 BeanPostProcessor。

在配置文件中使用 context 命名空间之前,必须在 <beans> 元素中声明 context 命名空间。

5.4 @Component

虽然我们可以通过 @Autowired 或 @Resource 在 Bean 类中使用自动注入功能,但是 Bean 还是在 XML 文件中通过 <bean> 进行定义 —— 也就是说,在 XML 配置文件中定义 Bean,通过@Autowired 或 @Resource 为 Bean 的成员变量、方法入参或构造函数入参提供自动注入的功能。能否也通过注释定义 Bean,从 XML 配置文件中完全移除 Bean 定义的配置呢?答案是肯定的,我们通过 Spring 2.5 提供的@Component 注释就可以达到这个目标了。

为什么 @Repository 只能标注在 DAO 类上呢?这是因为该注解的作用不只是将类识别为 Bean,同时它还能将所标注的类中抛出的数据访问异常封装为 Spring 的数据访问异常类型。 Spring 本身提供了一个丰富的并且是与具体的数据访问技术无关的数据访问异常结构,用于封装不同的持久层框架抛出的异常,使得异常独立于底层的框架。

Spring 2.5 在 @Repository 的基础上增加了功能类似的额外三个注解:@Component、@Service、@Constroller,它们分别用于软件系统的不同层次:

  • @Component 是一个泛化的概念,仅仅表示一个组件 (Bean) ,可以作用在任何层次。
  • @Service 通常作用在业务层,但是目前该功能与 @Component 相同。
  • @Constroller 通常作用在控制层,但是目前该功能与 @Component 相同。

通过在类上使用 @Repository、@Component、@Service 和 @Constroller 注解,Spring 会自动创建相应的 BeanDefinition 对象,并注册到 ApplicationContext 中。这些类就成了 Spring 受管组件。这三个注解除了作用于不同软件层次的类,其使用方式与 @Repository 是完全相同的。

下面,我们完全使用注释定义 Bean 并完成 Bean 之间装配:

使用 @Component 注释的 Car.java:

package com.baobaotao;

import org.springframework.stereotype.Component;

@Component
public class Car {
    …
}

仅需要在类定义处,使用 @Component 注释就可以将一个类定义了 Spring 容器中的 Bean。下面的代码将 Office 定义为一个 Bean:

使用 @Component 注释的 Office.java:

package com.baobaotao;

import org.springframework.stereotype.Component;

@Component
public class Office {
    private String officeNo = "001";
    …
}

这样,我们就可以在 Boss 类中通过 @Autowired 注入前面定义的 Car 和 Office Bean 了。

使用 @Component 注释的 Boss.java:

package com.baobaotao;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

@Component("boss")
public class Boss {
    @Autowired
    private Car car;

    @Autowired
    private Office office;
    …
}

@Component 有一个可选的入参,用于指定 Bean 的名称,在 Boss 中,我们就将 Bean 名称定义为“boss”。一般情况下,Bean 都是 singleton 的,需要注入 Bean 的地方仅需要通过 byType 策略就可以自动注入了,所以大可不必指定 Bean 的名称。

在使用 @Component 注释后,Spring 容器必须启用类扫描机制以启用注释驱动 Bean 定义和注释驱动 Bean 自动注入的策略。Spring 2.5 对 context 命名空间进行了扩展,提供了这一功能,请看下面的配置:

简化版的 beans.xml:

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

这里,所有通过 <bean> 元素定义 Bean 的配置内容已经被移除,仅需要添加一行 <context:component-scan/> 配置就解决所有问题了——Spring XML 配置文件得到了极致的简化(当然配置元数据还是需要的,只不过以注释形式存在罢了)。<context:component-scan/> 的 base-package 属性指定了需要扫描的类包,类包及其递归子包中所有的类都会被处理。

<context:component-scan/> 还允许定义过滤器将基包下的某些类纳入或排除。Spring 支持以下 4 种类型的过滤方式,通过下表说明:

扫描过滤方式

过滤器类型 说明
注释 假如 com.baobaotao.SomeAnnotation 是一个注释类,我们可以将使用该注释的类过滤出来。
类名指定 通过全限定类名进行过滤,如您可以指定将 com.baobaotao.Boss 纳入扫描,而将 com.baobaotao.Car 排除在外。
正则表达式 通过正则表达式定义过滤的类,如下所示: com\.baobaotao\.Default.*
AspectJ 表达式 通过 AspectJ 表达式定义过滤的类,如下所示: com. baobaotao..*Service+

下面是一个简单的例子:

<context:component-scan base-package="com.baobaotao">
    <context:include-filter type="regex" 
        expression="com\.baobaotao\.service\..*"/>
    <context:exclude-filter type="aspectj" 
        expression="com.baobaotao.util..*"/>
</context:component-scan>

值得注意的是 <context:component-scan/> 配置项不但启用了对类包进行扫描以实施注释驱动 Bean 定义的功能,同时还启用了注释驱动自动注入的功能(即还隐式地在内部注册了AutowiredAnnotationBeanPostProcessor 和 CommonAnnotationBeanPostProcessor),因此当使用 <context:component-scan/> 后,就可以将 <context:annotation-config/> 移除了。

默认情况下通过 @Component 定义的 Bean 都是 singleton 的,如果需要使用其它作用范围的 Bean,可以通过@Scope 注释来达到目标,如以下代码所示:

5.5 @Scope

通过 @Scope 指定 Bean 的作用范围

package com.baobaotao;
import org.springframework.context.annotation.Scope;
…
@Scope("prototype")
@Component("boss")
public class Boss {
    …
}

这样,当从 Spring 容器中获取 boss Bean 时,每次返回的都是新的实例了。

采用具有特殊语义的注释

Spring 2.5 中除了提供 @Component 注释外,还定义了几个拥有特殊语义的注释,它们分别是:@Repository@Service 和@Controller。在目前的 Spring 版本中,这 3 个注释和 @Component 是等效的,但是从注释类的命名上,很容易看出这 3 个注释分别和持久层、业务层和控制层(Web 层)相对应。虽然目前这 3 个注释和@Component 相比没有什么新意,但 Spring 将在以后的版本中为它们添加特殊的功能。所以,如果 Web 应用程序采用了经典的三层分层结构的话,最好在持久层、业务层和控制层分别采用@Repository@Service 和 @Controller 对分层中的类进行注释,而用@Component 对那些比较中立的类进行注释。

注释配置和 XML 配置的适用场合

是否有了这些 IOC 注释,我们就可以完全摒除原来 XML 配置的方式呢?答案是否定的。有以下几点原因:

  • 注释配置不一定在先天上优于 XML 配置。如果 Bean 的依赖关系是固定的,(如 Service 使用了哪几个 DAO 类),这种配置信息不会在部署时发生调整,那么注释配置优于 XML 配置;反之如果这种依赖关系会在部署时发生调整,XML 配置显然又优于注释配置,因为注释是对 Java 源代码的调整,您需要重新改写源代码并重新编译才可以实施调整。
  • 如果 Bean 不是自己编写的类(如 JdbcTemplate、SessionFactoryBean 等),注释配置将无法实施,此时 XML 配置是唯一可用的方式。
  • 注释配置往往是类级别的,而 XML 配置则可以表现得更加灵活。比如相比于 @Transaction 事务注释,使用 aop/tx 命名空间的事务配置更加灵活和简单。

所以在实现应用中,我们往往需要同时使用注释配置和 XML 配置,对于类级别且不会发生变动的配置可以优先考虑注释配置;而对于那些第三方类以及容易发生调整的配置则应优先考虑使用 XML 配置。Spring 会在具体实施 Bean 创建和 Bean 注入之前将这两种配置方式的元信息融合在一起。

本章引用自:https://blog.csdn.net/bbewx/article/details/38462675

6 AOP事物

6.1 Spring事物管理

众所周知的ACID属性: 
原子性(atomicity)、一致性(consistency)、隔离性(isolation)以及持久性(durability)。我们无法控制一致性、原子性以及持久性,但可以控制超时,设置事务的只读性以指定隔离级别。Spring在TransactionDefinition接口封装了所有这些设置。

package org.springframework.transaction;  
  
public interface TransactionDefinition {  
    int getPropagationBehavior();  
    int getIsolationLevel();  
    int getTimeout();  
    boolean isReadOnly();  
    String getName();  
}

getTimeout:返回一个事务必须完成的时间限制。

isReadOnly:表示事务是否只读。

getIsolationLevel:他对其他事务所看到的数据变化进行控制。隔离级别 说明

  • ISOLATION_DEFAULT 默认级别(对大多数数据库来说就是ISOLATION_READ_COMMITTED)
  • ISOLATION_READ_UNCOMMITTED 最低的隔离级别。事实上我们不应该隔离级别,因为在事务完成前,其他事务可以看到该事务所修改的数据。而在其他事务提交前,该事务也可以看到其他事务所做的修改。
  • ISOLATION_READ_COMMITTED 大多数数据库的默认级别。在事务完成前,其他事务无法看到该事务所修改的数据。遗憾的是,在该事务提交后,你就可以查看其他事务插入活更新的数据。这意味着在事务的不同点上,如果其他事务修改数据,你会看到不同的数据。
  • ISOLATION_REPEATABLE_READ 该隔离级别确保如果在事务中查询了某个数据集,你至少还能再次查询到相同的数据集,即使其他事务修改了所查询的数据。然而如果其他事务插入了新数据,你就可以查询到该新插入的数据。
  • ISOLATION_SERIALIZABLE 代价最大、可靠性最高的隔离级别,所有的事务都是俺顺序一个接一个的执行。

getPropagationBehavior:指定了当代码请求一个新的事务时Spring所做的事情。 传播行为 说明 

  • PROPAGATION_REQUIRED 当前如果有事务,Spring就会使用该事务;否则会开始一个新事务。 
  • PROPAGATION_SUPPORTS 当前如果有事务,Spring就会使用该事务;否则不会开启一个新事务。 
  • PROPAGATION_MANDATORY 当前如果有事务,Spring就会使用该事务;否则会抛出异常。 
  • PROPAGATION_REQUIRES_NEW Spring总会开始一个新事务。如果当前有事务,则该事务挂起。 
  • PROPAGATION_NOT_SUPPORTED Spring不会执行事务中的代码。代码总是在非事务环境下执行,如果当期有事务,则该事务挂起。 
  • PROPAGATION_NEVER 即使当前有事务,Spring也会在飞事务环境下执行。如果当前有事务,则抛出异常。 
  • PROPAGATION_NESTED 如果当前有事务,则在嵌套事务中执行。如果没有,那么执行情况与PROPAGATION_REQUIRED一样。

使用TransactionStatus接口:

package org.springframework.transaction;  
  
public interface TransactionStatus extends SavepointManager {  
    boolean isNewTransaction();  
    boolean hasSavepoint();  
    void setRollbackOnly();  
    boolean isRollbackOnly();  
    boolean isCompleted();  
}

setRollbackOnly:将一个事务表示为不可提交的。 

PlatformTransactionManager的实现: 
使用TransactionDefinition和TransactionStatus接口,创建并管理事务。 
DataSourceTransactionManager控制着从DataSource中获得的JDBC Connection上的事务执行; 
HibernateTransactionManager控制着Hibernate session上的事务执行; 
JdoTransactionManager管理着JDO事务; 
JtaTransactionManager将事务管理委托给JTA。 

例如: 
JDBC:

<!-- 声明事务处理器 -->  
<bean id="transactionManager"  
    class="org.springframework.jdbc.datasource.DataSourceTransactionManager">  
    <property name="dataSource" ref="dataSource"></property>  
</bean>  
  
<!-- 声明事务通知 -->  
<tx:advice id="bookShopTx"  
    transaction-manager="transactionManager">  
    <tx:attributes>  
        <tx:method name="purchase"   
            propagation="REQUIRES_NEW"  
            isolation="READ_COMMITTED"  
            rollback-for="java.lang.ArithmeticException"/>  
    </tx:attributes>  
</tx:advice>  
  
<!-- 声明事务通知需要通知哪些类的那些方法, 即: 那些方法受事务管理 -->  
<aop:config>  
    <!-- 声明切入点 -->  
    <aop:pointcut expression="execution(* cn.partner4java.spring.transaction.BookShopService.*(..))"   
        id="txPointCut"/>  
          
    <!-- 把切入点和事务通知联系起来: 既声明一个增强器 -->  
    <aop:advisor advice-ref="bookShopTx" pointcut-ref="txPointCut"/>  
</aop:config>

Hibernate:

<bean id="sessionFactory"  
    class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">  
    <property name="configLocation" value="classpath:hibernate.cfg.xml"></property>  
    <property name="dataSource" ref="dataSource"></property>      
</bean>  
  
<bean id="transactionManager"  
    class="org.springframework.orm.hibernate3.HibernateTransactionManager">  
    <property name="sessionFactory" ref="sessionFactory"></property>      
</bean>  
  
<!-- 事务通知 -->  
<tx:advice id="txAdvice" transaction-manager="transactionManager">  
    <tx:attributes>  
        <tx:method name="new*" propagation="REQUIRED" isolation="DEFAULT" />  
        <tx:method name="save*" propagation="REQUIRED" isolation="DEFAULT" />  
        <tx:method name="update*" propagation="REQUIRED" isolation="DEFAULT" />  
        <tx:method name="delete*" propagation="REQUIRED" isolation="DEFAULT" />  
        <tx:method name="bulk*" propagation="REQUIRED" isolation="DEFAULT" />  
        <tx:method name="load*" propagation="REQUIRED" isolation="DEFAULT" read-only="true"/>  
        <tx:method name="get*" propagation="REQUIRED" isolation="DEFAULT" read-only="true"/>  
        <tx:method name="query*" propagation="REQUIRED" isolation="DEFAULT" read-only="true"/>  
        <tx:method name="find*" propagation="REQUIRED" isolation="DEFAULT" read-only="true"/>  
        <tx:method name="is*" propagation="REQUIRED" isolation="DEFAULT" read-only="true"/>  
          
        <tx:method name="*" propagation="SUPPORTS" isolation="DEFAULT" />  
    </tx:attributes>  
</tx:advice>  
<aop:config>  
        <aop:advisor pointcut="execution(* *..*service*.*(..))" advice-ref="txAdvice" />  
</aop:config>   
  
<context:component-scan base-package="com.bytter"></context:component-scan>  
  
<tx:annotation-driven/>

JPA:

<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">  
    <property name="dataSource" ref="dataSource" />  
    <property name="persistenceXmlLocation" value="classpath:META-INF/persistence.xml" />  
    <property name="loadTimeWeaver">  
          <bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver"/>  
    </property>  
</bean>  
      
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">  
    <property name="entityManagerFactory" ref="entityManagerFactory"/>  
</bean>  

<tx:annotation-driven transaction-manager="transactionManager"/>

3对一个事务管理示例的探索
使用事务操作方式有3种基本方式: 

可以使用声明式事务,只需声明某个方法需要事务就行了; 
可以使用源码级的元数据来说明某个方法需要一个事务; 
还可以用编写事务代码的方式来实现。 

AOP事务管理 

1、使用基于注解的AOP事务管理 
<tx:annotation-driven transaction-manager=”transactionManager”/> 
<aop:aspectj-autoproxy /> 

探索tx:annotation-driven标签: 
<tx:annotation-driven/>标签是注解驱动的事务管理支持的核心。 

<tx:annotation-driven/>标签的属性: 
transaction-manager:指定到现有的PlatformTransactionManager bean的引用,通知会使用该引用。default=”transactionManager” 
mode:指定Spring事务管理框架创建通知bean的方式。可用的值有proxy和aspectj。前者是默认值,表示通知对象是个JDK代理;后者表示Spring AOP会使用AspectJ创建代理。 
order:指定创建的切面的顺序。只要目标对象有多个通知就可以使用该属性。 
proxy-target-class:该属性如果为true就表示你想要代理目标类而不是bean所实现的所有接口。default=”false” 

探索@Transactional注解: 
你可以指定传播、隔离级别、超时以及允许和不允许的异常。 
@Transactional注解的属性: 
propagation:指定事务定义中使用的传播 
isolation:设定事务的隔离级别 
timeout:指定事务的超市(秒) 
readOnly:指定事务的超时 
noRollbackFor:目标方法可抛出的异常所构成的数组,但通知仍会提交事务 
rollbackFor:异常所构成的数组,如果目标方法抛出了这些异常,通知就会回滚事务 

基于注解的事务管理小结: 
如果定义在类上,那么所有的方法都使用相同的方式,有些read就会抱怨给太多的东西了。 
如果在每个方法上都定义注解,那么就会很麻烦。 
(可以使用XML AOP事务管理能更好的处理这种情况)

2、使用XML AOP事务管理
<tx:advice/>标签,该标签会创建一个事务处理通知。

<tx:advice id="txAdvice" transaction-manager="transactionManager">  
    <tx:attributes>  
        <tx:method name="bulk*" propagation="REQUIRED" isolation="DEFAULT" />  
        <tx:method name="load*" propagation="REQUIRED" isolation="DEFAULT" read-only="true"/>  
    </tx:attributes>  
</tx:advice>  
<aop:config>  
        <aop:advisor pointcut="execution(* *..*Service*.*(..))" advice-ref="txAdvice" />  
</aop:config>  
  
或  
  
<aop:config>  
    <aop:pointcut id="allServiceMethods"  
                  expression="execution(* com.apress.prospring2.ch16.services.*.*(..))"/>  
    <aop:advisor advice-ref="defaultTransactionAdvice"  
                 pointcut-ref="allServiceMethods"/>  
</aop:config>  
  
<tx:advice id="defaultTransactionAdvice" transaction-manager="transactionManager">  
    <tx:attributes>  
        <tx:method  
                name="*"  
                isolation="DEFAULT"  
                propagation="REQUIRED"  
                no-rollback-for="java.lang.RuntimeException"  
                timeout="100"/>  
        <tx:method  
                name="get*"  
                read-only="true"/>  
    </tx:attributes>  
</tx:advice>

3、tx:advice标签简介 
id是该advice bean的标识,而transaction-manager则必须引用一个PlatformTransactionManager bean。 
还可以通过<tx:attributes>标签定制<tx:advice>标签所创建的通知的行为。 

<tx:method/>标签的属性: 
name:方法名的匹配模式,通知根据该模式寻找匹配的方法。 
propagation:设定事务定义所用的传播级别。 
isolation:设置事务的隔离级别。 
timeout:指定事务的超时(秒)。 
read-only:该属性为true指示事务是只读的 
no-rollback-for:以逗号分隔的异常类的列表,目标方法可以跑出这些异常而不会导致通知执行回滚 
rollback-for:以逗号分隔的异常类的列表,当目标方法跑出这些异常时会导致通知执行回滚。默认情况下,该列表为空,因此不在no-rollback-for列表中的任何运行时异常都会导致回滚。

8实现你自己的事务同步 

将介绍如何实现你自己的事务同步,只要是活动的事务状态发生变化就会收到TransactionSynchronizationManager的回调。 

书中的demo: 
使用TransactionSynchronizationManager注册了TransactionSynchronization回调,同时MyTransactionSynchronizationAdapter会根据事务的完成状态去调用MySession.beginTransaction()、MySession.commit()或MySession.rollback()方法。 

模拟一个session类:

package cn.partner4java.myptm;  
  
import java.io.Serializable;  
  
/** 
* 模拟一个session类 
* @author partner4java 
* 
*/  
public class MySession {  
    /** 用来标识一个session */  
    private Long sessionId;  
  
    public void save(Serializable entity){  
        System.out.println(sessionId + ":save");  
    }  
      
    public void beginTransaction(){  
        System.out.println(sessionId + ":beginTransaction");  
    }  
      
    public void commit(){  
        System.out.println(sessionId + ":commit");  
    }  
      
    public void rollback(){  
        System.out.println(sessionId + ":rollback");  
    }  
      
    public Long getSessionId() {  
        return sessionId;  
    }  
  
    public void setSessionId(Long sessionId) {  
        this.sessionId = sessionId;  
    }  
  
    @Override  
    public String toString() {  
        return "MySession [sessionId=" + sessionId + "]";  
    }
}

简单模拟SessionFactory:

package cn.partner4java.myptm;  
  
import org.springframework.transaction.support.TransactionSynchronization;  
import org.springframework.transaction.support.TransactionSynchronizationManager;  
  
  
/** 
* 简单模拟SessionFactory<br/> 
* 通判传递的类都为MySessionFactory而不是MySession,通过MySessionFactory获得当前线程的MySession或者开启一个新的MySession 
* @author partner4java 
* 
*/  
public class MySessionFactory {  
      
    /** 
     * 如果当前线程存在MySession,就使用该MySession,否者开启一个新的MySession 
     * @return 
     */  
    public MySession getSession(){  
        //传入this,是因为,我们以当前factory类作为键保存的MySession  
        if(TransactionSynchronizationManager.hasResource(this)){  
            return getCurrentSession();  
        }else{  
            return openSession();  
        }  
          
    }  
  
      
    /** 
     * 开启一个新MySession 
     * @return 
     */  
    private MySession openSession() {  
        MySession mySession = new MySession();  
        mySession.setSessionId(System.currentTimeMillis());  
          
        //注册进当前线程管理一个Synchronization  
        TransactionSynchronization transactionSynchronization = new MyTransactionSynchronizationAdapter(this);  
        TransactionSynchronizationManager.registerSynchronization(transactionSynchronization);  
          
        //绑定新开启的一个MySession进当前线程事务管理器  
        TransactionSynchronizationManager.bindResource(this, mySession);  
          
        return mySession;  
    }  
  
    /** 
     * 获取当前线程的MySession 
     * @return 
     */  
    private MySession getCurrentSession() {  
        MySession mySession = (MySession) TransactionSynchronizationManager.getResource(this);  
        return mySession;  
    }  
}

核心事务同步适配器:

package cn.partner4java.myptm;  
  
import org.springframework.transaction.support.TransactionSynchronizationAdapter;  
import org.springframework.transaction.support.TransactionSynchronizationManager;  
  
/** 
* 核心事务同步适配器<br/> 
* 当方法上面定义了@Transactional注解,那么当每次状态发生时就会调用本同步适配器 
* for transaction synchronization callbacks 
* @author partner4java 
* 
*/  
public class MyTransactionSynchronizationAdapter extends  
        TransactionSynchronizationAdapter {  
    private MySessionFactory mySessionFactory;  
  
    public MyTransactionSynchronizationAdapter(MySessionFactory mySessionFactory) {  
        this.mySessionFactory = mySessionFactory;  
    }  
  
    @Override  
    public void beforeCommit(boolean readOnly) {  
        //readOnly标识是否是一个只读线程  
        if(!readOnly){  
            MySession mySession = (MySession) TransactionSynchronizationManager.getResource(mySessionFactory);  
            mySession.beginTransaction();  
        }  
    }  
  
    @Override  
    public void afterCompletion(int status) {  
        MySession mySession = (MySession) TransactionSynchronizationManager.getResource(mySessionFactory);  
        if (STATUS_COMMITTED == status) {  
            mySession.commit();  
        }  
        //当然,你还可以定义回滚方法  
    }  
}

调用起的DAO:

package cn.partner4java.dao;  
  
public interface HelloDao {  
    public void saveHello();  
}
package cn.partner4java.dao;  
  
import org.springframework.jdbc.core.support.JdbcDaoSupport;  
import org.springframework.transaction.annotation.Transactional;  
  
import cn.partner4java.myptm.MySessionFactory;  
  
/** 
* 一个hello world dao,起到模拟调用自定义事务同步的作用 
* @author partner4java 
* 
*/  
public class HelloDaoImpl extends JdbcDaoSupport implements HelloDao {  
    private MySessionFactory mySessionFactory;  
  
    public void setMySessionFactory(MySessionFactory mySessionFactory) {  
        this.mySessionFactory = mySessionFactory;  
    }  
      
    @Transactional  
    public void saveHello(){  
        mySessionFactory.getSession().save(null);  
        this.getJdbcTemplate().execute("select * from user");  
    }  
}

配置文件:

<?xml version="1.0" encoding="UTF-8"?>  
<beans xmlns="http://www.springframework.org/schema/beans"  
       xmlns:aop="http://www.springframework.org/schema/aop"  
       xmlns:tx="http://www.springframework.org/schema/tx"  
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
       xsi:schemaLocation="  
            http://www.springframework.org/schema/beans  
            http://www.springframework.org/schema/beans/spring-beans.xsd  
            http://www.springframework.org/schema/tx  
            http://www.springframework.org/schema/tx/spring-tx.xsd  
            http://www.springframework.org/schema/aop  
            http://www.springframework.org/schema/aop/spring-aop.xsd">  
                  
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource" destroy-method="close">  
        <property name="driverClassName" value="com.mysql.jdbc.Driver"/>  
        <property name="url" value="jdbc:mysql://localhost:3306/springdb"/>  
        <property name="username" value="root"/>  
        <property name="password" value="123456"/>  
    </bean>  
  
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">  
        <property name="dataSource" ref="dataSource"/>  
    </bean>  
      
    <tx:annotation-driven transaction-manager="transactionManager"/>  
                  
    <aop:aspectj-autoproxy />  
      
    <bean id="mySessionFactory" class="cn.partner4java.myptm.MySessionFactory"/>  
      
    <bean id="helloDao" class="cn.partner4java.dao.HelloDaoImpl">  
        <property name="dataSource" ref="dataSource"/>  
        <property name="mySessionFactory" ref="mySessionFactory"></property>  
    </bean>  
  
</beans>

测试:

         ApplicationContext ac = new ClassPathXmlApplicationContext("/META-INF/spring/myptm.xml");  
           
         HelloDao helloDao = (HelloDao) ac.getBean("helloDao");  
         helloDao.saveHello();  
//       后台打印:  
//       1322395163008:save  
//       1322395163008:beginTransaction  
//       1322395163008:commit

总结:有两个核心的Spring类支持了这个功能,TransactionSynchronization接口,TransactionSynchronizationManager类。 
TransactionSynchronizationManager负责管理当前线程在资源,资源可以主动绑定到TransactionSynchronizationManager中。 
TransactionSynchronization提供了同步调用,当方法上面定义了@Transactional注解,那么当每次状态发生时就会调用本同步适配器。 

PlatformTransactionManager的各种实现也是借助了上面这两个类,你可以查阅一下源码。所以,我们自然而然的也可以自己实现一个PlatformTransactionManager,来管理真正的sessionFactory,然后像其他实现一样,交给Spring,然后再给他声明事务。

6.2 配置事物注意事项

  • 若要支持 @Transactional 注解需要在 applicationContext.xml 上添加如下配置:
    <tx:annotation-driven transaction-manager="transactionManager" />
    transactionManager 是 bean 的ID:
    <bean id="transactionManager" class="org.springframework.orm.hibernate4.HibernateTransactionManager">
  • @Transactional 的事务开启,或者是基于接口的或者是基于类的代理被创建。所以在同一个类中一个方法调用另一个方法有事务的方法,事务是不会起作用的。
  • 在需要事务管理的地方加 @Transactional 注解。此注解可以被应用于接口定义和接口方法、类定义和类的 public 方法上。
  • 此注解只能应用到 public 可见度的方法上。 如果你在 protected、private 或者 package-visible 的方法上使用 @Transactional 注解,它也不会报错, 但是这个被注解的方法将不会展示已配置的事务设置。
  • 注解@Transactional cglib与java动态代理最大区别是代理目标对象不用实现接口,那么注解要是写到接口方法上,要是使用cglib代理,注解事物就失效了,为了保持兼容(即cglib与java动态代理都可用)注解最好都写到实现类方法上。Spring团队建议在具体的类(或类的方法)上使用 @Transactional 注解,而不要使用在类所要实现的任何接口上。因为注解是不能继承 的,这就意味着如果正在使用基于类的代理时,那么事务的设置将不能被基于类的代理所识别,而且对象也将不会被事务代理所包装。
  • Spring使用声明式事务处理,默认情况下,如果被注解的数据库操作方法中发生了unchecked异常,所有的数据库操作将rollback;如果发生的异常是checked异常,默认情况下数据库操作还是会提交的。unchecked异常:这类异常都是RuntimeException的子类 ,虽然RuntimeException同样也是Exception的子类,但是它们是特殊的,它们不能通过客户端代码来试图解决。 checked异常:这类异常都是Exception的子类 。异常的向上抛出机制进行处理,如果子类可能产生A异常,那么在父类中也必须throws A异常。可能导致的问题:代码效率低,耦合度过高。C#中就没有使用这种异常机制。)
    一:unchecked异常:

    @Transactional //发生了unchecked异常,事务会回滚,staff不会被保存。
    public void add(staff){ 
        dao.create(staff);
        throw new RuntimeException("我是unchecked异常"); 
    }

    二:checked异常:

    @Transactional //发生了checked异常,事务不回滚,即 staff 仍能被创建。
    public void add(staff) throws Exception{ 
        dao.create(staff);
        throw new Exception("我是checked异常"); 
    }
  • checked异常是不会被回滚的,如果我们需要它进行事务回滚,可以这样配置:
    @Transactional(rollbackFor=Exception.class)
  • 如果将 @Transactional 放在类上,但类的某些方法不需要使用事物,则可以这些方法上使用 @Transactional(propagation=Propagation.NOT_SUPPORTED)
    @Transactional 
    public class PersonServiceBean implements IPersonService { 
        @Transactional(propagation=Propagation.NOT_SUPPORTED)
        public Person getPerson(Integer personid){} //调用此方法不会开启事物。
    }

6.3 AOP事务与异常

6.4 事物不能在Controller层开启

发表评论

您的电子邮箱地址不会被公开。