SSM--day3--Spring(三)--AOP事务(补漏)
(以下内容全部来自上述课程及课件)
(JavaWeb中大致学过,可见:JavaWeb–day12–事务&AOP)
AOP(补充)
1. AOP注解总结
知识点1:@EnableAspectJAutoProxy
知识点2:@Aspect
知识点3:@Pointcut
知识点4:@Before
2. AOP工作流程
2.1 AOP工作流程
由于AOP是基于Spring容器管理的bean做的增强,所以整个工作过程需要从Spring加载bean说起:
2.1.1 流程1:Spring容器启动
- 容器启动就需要去加载bean,哪些类需要被加载呢?
- 需要被增强的类,如:BookServiceImpl
- 通知类,如:MyAdvice
- 注意此时bean对象还没有创建成功
2.1.2 流程2:读取所有切面配置中的切入点
- 上面这个例子中有两个切入点的配置,但是第一个ptx()并没有被使用,所以不会被读取。
2.1.3 流程3:初始化bean
判定bean对应的类中的方法是否匹配到任意切入点
- 注意第1步在容器启动的时候,bean对象还没有被创建成功。
- 要被实例化bean对象的类中的方法和切入点进行匹配
- 匹配失败,创建原始对象,如UserDao
匹配失败说明不需要增强,直接调用原始对象的方法即可。 - 匹配成功,创建原始对象(目标对象)的代理对象,如:BookDao
匹配成功说明需要对其进行增强
对哪个类做增强,这个类对应的对象就叫做目标对象
因为要对目标对象进行功能增强,而采用的技术是动态代理,所以会为其创建一个代理对象
最终运行的是代理对象的方法,在该方法中会对原始方法进行功能增强
2.1.4 流程4:获取bean执行方法
- 获取的bean是原始对象时,调用方法并执行,完成操作
- 获取的bean是代理对象时,根据代理对象的运行模式运行原始方法与增强的内容,完成操作
2.1.5 验证容器中是否为代理对象
为了验证IOC容器中创建的对象和我们刚才所说的结论是否一致,首先先把结论理出来:
- 如果目标对象中的方法会被增强,那么容器中将存入的是目标对象的代理对象
- 如果目标对象中的方法不被增强,那么容器中将存入的是目标对象本身。
验证思路
1.要执行的方法,不被定义的切入点包含,即不要增强,打印当前类的getClass()方法
2.要执行的方法,被定义的切入点包含,即要增强,打印出当前类的getClass()方法
3.观察两次打印的结果
步骤1:修改App类,获取类的类型
public class App {public static void main(String[] args) {ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);BookDao bookDao = ctx.getBean(BookDao.class);System.out.println(bookDao);System.out.println(bookDao.getClass());}}
步骤2:修改MyAdvice类,不增强
因为定义的切入点中,被修改成update1,所以BookDao中的update方法在执行的时候,就不会被增强,
所以容器中的对象应该是目标对象本身。
@Component@Aspectpublic class MyAdvice {@Pointcut("execution(void com.itheima.dao.BookDao.update1())")private void pt(){}@Before("pt()")public void method(){System.out.println(System.currentTimeMillis());}}
步骤3:运行程序
步骤4:修改MyAdvice类,增强
因为定义的切入点中,被修改成update,所以BookDao中的update方法在执行的时候,就会被增强,
所以容器中的对象应该是目标对象的代理对象
@Component@Aspectpublic class MyAdvice {@Pointcut("execution(void com.itheima.dao.BookDao.update())")private void pt(){}@Before("pt()")public void method(){System.out.println(System.currentTimeMillis());}}
步骤5:运行程序
至此对于刚才的结论,我们就得到了验证,这块大家需要注意的是:
不能直接打印对象,从上面两次结果中可以看出,直接打印对象走的是对象的toString方法,不管是
不是代理对象打印的结果都是一样的,原因是内部对toString方法进行了重写。
3. AOP通知类型(补充)
3.1 类型位置
(1)前置通知,追加功能到方法执行前,类似于在代码1或者代码2添加内容
(2)后置通知,追加功能到方法执行后,不管方法执行的过程中有没有抛出异常都会执行,类似于在代
码5添加内容
(3)返回后通知,追加功能到方法执行后,只有方法正常执行结束后才进行,类似于在代码3添加内容,
如果方法执行抛出异常,返回后通知将不会被添加
(4)抛出异常后通知,追加功能到方法抛出异常后,只有方法执行出异常才进行,类似于在代码4添加内
容,只有方法抛出异常后才会被添加
(5)环绕通知,环绕通知功能比较强大,它可以追加功能到方法执行的前后,这也是比较常用的方式,
它可以实现其他四种通知类型的功能
3.2 环绕通知
因为环绕通知是可以控制原始方法执行的,所以我们把增强的代码写在调用原始方法的不同位置就可以实现不同的通知类型的功能,如:
3.3 通知类型总结
3.3.1 知识点1:@After
3.3.2 知识点2:@AfterReturning
3.3.3 知识点3:@AfterThrowing
3.3.4 知识点4:@Around
4. AOP通知获取数据
目前我们写AOP仅仅是在原始方法前后追加一些操作,接下来我们要说说AOP中数据相关的内容,我们将从获取参数、获取返回值和获取异常三个方面来研究切入点的相关信息。
前面我们介绍通知类型的时候总共讲了五种,那么对于这五种类型都会有参数,返回值和异常吗?
我们先来一个个分析下:
- 获取切入点方法的参数,所有的通知类型都可以获取参数
JoinPoint:适用于前置、后置、返回后、抛出异常后通知
ProceedingJoinPoint:适用于环绕通知 - 获取切入点方法返回值,前置和抛出异常后通知是没有返回值,后置通知可有可无,所以不做研究
返回后通知
环绕通知 - 获取切入点方法运行异常信息,前置和返回后通知是不会有,后置通知可有可无,所以不做研究
抛出异常后通知
环绕通知
4.1 环境准备
- 创建一个Maven项目
- pom.xml添加Spring依赖
<dependencies><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>5.2.10.RELEASE</version></dependency><dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId><version>1.9.4</version></dependency></dependencies>
- 添加BookDao和BookDaoImpl类
public interface BookDao {public String findName(int id);}@Repositorypublic class BookDaoImpl implements BookDao {public String findName(int id) {System.out.println("id:"+id);return "itcast";}}
- 创建Spring的配置类
@Configuration@ComponentScan("com.itheima")@EnableAspectJAutoProxypublic class SpringConfig {}
- 编写通知类
@Component@Aspectpublic class MyAdvice {@Pointcut("execution(* com.itheima.dao.BookDao.findName(..))")private void pt(){}@Before("pt()")public void before() {System.out.println("before advice ..." );}@After("pt()")public void after() {System.out.println("after advice ...");}@Around("pt()")public Object around() throws Throwable{Object ret = pjp.proceed();return ret;}@AfterReturning("pt()")public void afterReturning() {System.out.println("afterReturning advice ...");}@AfterThrowing("pt()")public void afterThrowing() {System.out.println("afterThrowing advice ...");}}
- 编写App运行类
public class App {public static void main(String[] args) {ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);BookDao bookDao = ctx.getBean(BookDao.class);String name = bookDao.findName(100);System.out.println(name);}}
最终创建好的项目结构如下:
4.2 获取参数
非环绕通知获取方式
在方法上添加JoinPoint,通过JoinPoint来获取参数
@Component@Aspectpublic class MyAdvice {@Pointcut("execution(* com.itheima.dao.BookDao.findName(..))")private void pt(){}@Before("pt()")public void before(JoinPoint jp) Object[] args = jp.getArgs();System.out.println(Arrays.toString(args));System.out.println("before advice ..." );}//...其他的略
}
运行App类,可以获取如下内容,说明参数100已经被获取
说明:
使用JoinPoint的方式获取参数适用于前置、后置、返回后、抛出异常后通知。剩下的大家自行去验证。
环绕通知获取方式
环绕通知使用的是ProceedingJoinPoint,因为ProceedingJoinPoint是JoinPoint类的子类,所以对于ProceedingJoinPoint类中应该也会有对应的getArgs()方法,我们去验证下:
@Component@Aspectpublic class MyAdvice {@Pointcut("execution(* com.itheima.dao.BookDao.findName(..))")private void pt(){}@Around("pt()")public Object around(ProceedingJoinPoint pjp)throws Throwable {Object[] args = pjp.getArgs();System.out.println(Arrays.toString(args));Object ret = pjp.proceed();return ret;}//其他的略
}
运行App后查看运行结果,说明ProceedingJoinPoint也是可以通过getArgs()获取参数
注意:
- pjp.proceed()方法是有两个构造方法,分别是:
调用无参数的proceed,当原始方法有参数,会在调用的过程中自动传入参数
所以调用这两个方法的任意一个都可以完成功能
但是当需要修改原始方法的参数时,就只能采用带有参数的方法,如下:
@Component@Aspectpublic class MyAdvice {@Pointcut("execution(* com.itheima.dao.BookDao.findName(..))")private void pt(){}@Around("pt()")public Object around(ProceedingJoinPoint pjp) throws Throwable{Object[] args = pjp.getArgs();System.out.println(Arrays.toString(args));args[0] = 666;Object ret = pjp.proceed(args);return ret;}//其他的略
}
有了这个特性后,我们就可以在环绕通知中对原始方法的参数进行拦截过滤,避免由于参数的问题导致程序无法正确运行,保证代码的健壮性。
4.3 获取返回值
对于返回值,只有返回后AfterReturing和环绕Around这两个通知类型可以获取,具体如何获取?
环绕通知获取返回值
@Component@Aspectpublic class MyAdvice {@Pointcut("execution(* com.itheima.dao.BookDao.findName(..))")private void pt(){}@Around("pt()")public Object around(ProceedingJoinPoint pjp) throws Throwable{Object[] args = pjp.getArgs();System.out.println(Arrays.toString(args));args[0] = 666;Object ret = pjp.proceed(args);return ret;}//其他的略
}
上述代码中,ret就是方法的返回值,我们是可以直接获取,不但可以获取,如果需要还可以进行修改。
返回后通知获取返回值
@Component@Aspectpublic class MyAdvice {@Pointcut("execution(* com.itheima.dao.BookDao.findName(..))")private void pt(){}@AfterReturning(value = "pt()",returning = "ret")public void afterReturning(Object ret) {System.out.println("afterReturning advice ..."+ret);}//其他的略}
注意:
(1)参数名的问题
(2)afterReturning方法参数类型的问题
参数类型可以写成String,但是为了能匹配更多的参数类型,建议写成Object类型
(3)afterReturning方法参数的顺序问题
运行App后查看运行结果,说明返回值已经被获取到
4.4 获取异常
对于获取抛出的异常,只有抛出异常后AfterThrowing和环绕Around这两个通知类型可以获取,具体如何获取?
环绕通知获取异常
这块比较简单,以前我们是抛出异常,现在只需要将异常捕获,就可以获取到原始方法的异常信息了
@Component@Aspectpublic class MyAdvice {@Pointcut("execution(* com.itheima.dao.BookDao.findName(..))")private void pt(){}@Around("pt()")public Object around(ProceedingJoinPoint pjp){Object[] args = pjp.getArgs();System.out.println(Arrays.toString(args));args[0] = 666;Object ret = null;try{ret = pjp.proceed(args);}catch(Throwable throwable){t.printStackTrace();}return ret;}//其他的略
}
在catch方法中就可以获取到异常,至于获取到异常以后该如何处理,这个就和你的业务需求有关了。
抛出异常后通知获取异常
@Component@Aspectpublic class MyAdvice {@Pointcut("execution(* com.itheima.dao.BookDao.findName(..))")private void pt(){}@AfterThrowing(value = "pt()",throwing = "t")public void afterThrowing(Throwable t) {System.out.println("afterThrowing advice ..."+t);}//其他的略}
如何让原始方法抛出异常,方式有很多
@Repositorypublic class BookDaoImpl implements BookDao {public String findName(int id,String password) {System.out.println("id:"+id);if(true){throw new NullPointerException();}return "itcast";}}
注意:
运行App后,查看控制台,就能看的异常信息被打印到控制台
至此,AOP通知如何获取数据就已经讲解完了,数据中包含参数、返回值、异常(了解)。
事务(补充)
1. 事务注解总结
1.1 知识点1:@EnableTransactionManagement
1.2 知识点2:@Transactional
2. Spring事务角色
重点要理解两个概念,分别是事务管理员和事务协调员。
- 未开启Spring事务之前:
- AccountDao的outMoney因为是修改操作,会开启一个事务T1
- AccountDao的inMoney因为是修改操作,会开启一个事务T2
- AccountService的transfer没有事务,
运行过程中如果没有抛出异常,则T1和T2都正常提交,数据正确
如果在两个方法中间抛出异常,T1因为执行成功提交事务,T2因为抛异常不会被执行
就会导致数据出现错误
- 开启Spring的事务管理后
- transfer上添加了@Transactional注解,在该方法上就会有一个事务T
- AccountDao的outMoney方法的事务T1加入到transfer的事务T中
- AccountDao的inMoney方法的事务T2加入到transfer的事务T中
- 这样就保证他们在同一个事务中,当业务层中出现异常,整个事务就会回滚,保证数据的准确性。
通过上面例子的分析,我们就可以得到如下概念:
- 事务管理员:发起事务方,在Spring中通常指代业务层开启事务的方法
- 事务协调员:加入事务方,在Spring中通常指代数据层方法,也可以是业务层方法
注意:
目前的事务管理是基于DataSourceTransactionManager和SqlSessionFactoryBean使用的是同一个数据源。
3. Spring事务属性
3.1 事务配置
上面这些属性都可以在@Transactional注解的参数上进行设置。
- readOnly:true只读事务,false读写事务,增删改要设为false,查询设为true。
- timeout:设置超时时间单位秒,在多长时间之内事务没有提交成功就自动回滚,-1表示不设置超时时间。
- rollbackFor:当出现指定异常进行事务回滚
- noRollbackFor:当出现指定异常不进行事务回滚
思考:出现异常事务会自动回滚,这个是我们之前就已经知道的
noRollbackFor是设定对于指定的异常不回滚,这个好理解
rollbackFor是指定回滚异常,对于异常事务不应该都回滚么,为什么还要指定? - 这块需要更正一个知识点,并不是所有的异常都会回滚事务,比如下面的代码就不会回滚
public interface AccountService {/*** 转账操作* @param out 传出方* @param in 转入方* @param money 金额*///配置当前接口方法具有事务public void transfer(String out,String in ,Double money) throws IOException;
}@Service
public class AccountServiceImpl implements AccountService {@Autowiredprivate AccountDao accountDao;@Transactionalpublic void transfer(String out,String in ,Double money) throws IOException{accountDao.outMoney(out,money);//int i = 1/0; //这个异常事务会回滚if(true){throw new IOException(); //这个异常事务就不会回滚}accountDao.inMoney(in,money);}}
- 出现这个问题的原因是,Spring的事务只会对Error异常和RuntimeException异常及其子类进行事务回顾,其他的异常类型是不会回滚的,对应IOException不符合上述条件所以不回滚
- 此时就可以使用rollbackFor属性来设置出现IOException异常不回滚
@Servicepublic class AccountServiceImpl implements AccountService {@Autowiredprivate AccountDao accountDao;@Transactional(rollbackFor = {IOException.class})public void transfer(String out,String in ,Double money) throws
IOException{accountDao.outMoney(out,money);//int i = 1/0; //这个异常事务会回滚if(true){throw new IOException(); //这个异常事务就不会回滚}accountDao.inMoney(in,money);}}
- rollbackForClassName等同于rollbackFor,只不过属性为异常的类全名字符串
- noRollbackForClassName等同于noRollbackFor,只不过属性为异常的类全名字符串
- isolation设置事务的隔离级别
DEFAULT :默认隔离级别, 会采用数据库的隔离级别
READ_UNCOMMITTED : 读未提交
READ_COMMITTED : 读已提交
REPEATABLE_READ : 重复读取
SERIALIZABLE: 串行化
3.2 事务传播行为
所谓的事务传播行为指的是:
事务传播行为:事务协调员对事务管理员所携带事务的处理态度。
需要用到之前我们没有说的propagation属性。
1.修改logService改变事务的传播行为
@Servicepublic class LogServiceImpl implements LogService {@Autowiredprivate LogDao logDao;//propagation设置事务属性:传播行为设置为当前操作需要新事务@Transactional(propagation = Propagation.REQUIRES_NEW)public void log(String out,String in,Double money ) {logDao.log("转账操作由"+out+"到"+in+",金额:"+money);}}
运行后,就能实现我们想要的结果,不管转账是否成功,都会记录日志。
2.事务传播行为的可选值
对于我们开发实际中使用的话,因为默认值需要事务是常态的。根据开发过程选择其他的就可以了,
例如案例中需要新事务就需要手工配置。其实入账和出账操作上也有事务,采用的就是默认值