Spring AOP应用
一、aop概念
名词 | 解释 |
---|---|
Joinpoint 连接点 | 它指的是那些可以⽤于把增强代码加⼊到业务主线中的点,那么由上图中我们可以看出,这些点指的就是⽅法。在⽅法执⾏的前后通过动态代理技术加⼊增强的代码。在Spring框架AOP思想的技术实现中,也只⽀持⽅法类型的连接点。 |
Pointcut 切入点 | 它指的是那些已经把增强代码加⼊到业务主线进来之后的连接点。由上图中,我们看出表现层 transfer⽅法就只是连接点,因为判断访问权限的功能并没有对其增强。 |
Advice 通知/增加 | 它指的是切⾯类中⽤于提供增强功能的⽅法。并且不同的⽅法增强的时机是不⼀样的。⽐如,开启事务肯定要在业务⽅法执⾏之前执⾏;提交事务要在业务⽅法正常执⾏之后执⾏,⽽回滚事务要在业务⽅法执⾏产⽣异常之后执⾏等等。那么这些就是通知的类型。其分类有:前置通知 后置通知 异常通知 最终通知 环绕通知。 |
Target 目标对象 | 它指的是代理的⽬标对象。即被代理对象。 |
Proxy 代理 | 它指的是⼀个类被AOP织⼊增强后,产⽣的代理类。即代理对象。 |
Weaving 织入 | 它指的是把增强应⽤到⽬标对象来创建新的代理对象的过程。spring采⽤动态代理织⼊,⽽AspectJ采⽤编译期织⼊和类装载期织⼊。 |
Aspect 切面 | 它指定是增强的代码所关注的⽅⾯,把这些相关的增强代码定义到⼀个类中,这个类就是切⾯类。例如,事务切⾯,它⾥⾯定义的⽅法就是和事务相关的,像开启事务,提交事务,回滚事务等等,不会定义其他与事务⽆关的⽅法。我们前⾯的案例中 TrasnactionManager就是⼀个切⾯。 |
连接点:⽅法开始时、结束时、正常运⾏完毕时、⽅法异常时等这些特殊的时机点,我们称之为连接点,项⽬中每个⽅法都有连接点,连接点是⼀种候选点
切⼊点:指定AOP思想想要影响的具体⽅法是哪些,描述感兴趣的⽅法
Advice增强:
第⼀个层次:指的是横切逻辑
第⼆个层次:⽅位点(在某⼀些连接点上加⼊横切逻辑,那么这些连接点就叫做⽅位点,描述的是具体的特殊时机)
Aspect切⾯:切⾯概念是对上述概念的⼀个综合
Aspect切⾯= 切⼊点+增强= 切⼊点(锁定⽅法) + ⽅位点(锁定⽅法中的特殊时机)+ 横切逻辑
众多的概念,⽬的就是为了锁定要在哪个地⽅插⼊什么横切逻辑代码
二、代理的选择
spring根据代理对象实现接口情况选择,当代理对象:
-
未实现接口:cglib代理
-
实现接口:jdk代理
Tips: 可通过配置强制使用cglib
三、aop配置方式
- xml配置
- xml+注解组合
- 纯注解
四、Spring 中 aop实现
1.XML模式
-
开启aop支持
<!--开启spring对注解aop的⽀持--> <aop:aspectj-autoproxy/>
-
添加约束头
xmlns:aop="http://www.springframework.org/schema/aop" http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd
-
配置步骤(由于xml现在已经不主流,此处仅做简单示例,不加demo代码了)
<!--把通知bean交给spring来管理--> <bean id="logUtil" class="com.example.demo.utils.LogUtil"></bean> <!--开始aop的配置--> <aop:config> <!--配置切⾯--> <aop:aspect id="logAdvice" ref="logUtil"> <!--配置前置通知--> <aop:before method="printLog" pointcut="execution(public * com.lagou.service.impl.TransferServiceImpl .updateAccountByCardNo(com.lagou.pojo.Account))"> </aop:before> </aop:aspect> </aop:config>
-
细节
1) 关于切入点表达式
全限定⽅法名 访问修饰符 返回值 包名.包名.包名.类名.⽅法名(参数列表) 1.全匹配方式 public void com.demo.service.impl.TransferServiceImpl.updateAccountByCardNo(com.demo.pojo.Account) 2.访问修饰符可以省略 void com.demo.service.impl.TransferServiceImpl.updateAccountByCardNo(com.demo.pojo.Account) 3.返回值可以使⽤*,表示任意返回值 * com.demo.service.impl.TransferServiceImpl.updateAccountByCardNo(com.demo.pojo.Account) 4.包名可以使⽤.表示任意包,但是有⼏级包,必须写⼏个 * ....TransferServiceImpl.updateAccountByCardNo(com.demo.pojo.Account) 5.包名可以使⽤..表示当前包及其⼦包 * ..TransferServiceImpl.updateAccountByCardNo(com.demo.pojo.Account) 6.类名和⽅法名,都可以使⽤.表示任意类,任意⽅法 * ...updateAccountByCardNo(com.demo.pojo.Account) 7.参数列表,可以使⽤具体类型 基本类型直接写类型名称 如: int 引⽤类型必须写全限定类名,如:java.lang.String 8.参数列表可以使⽤*,表示任意参数类型,但是必须有参数 * *..*.*(*) 9.参数列表可以使⽤..,表示有⽆参数均可。有参数可以是任意类型 * *..*.*(..) 10.全通配⽅式: * *..*.*(..)
2)改变代理的方式
<!--使⽤aop:config标签配置--> <aop:config proxy-target-class="true"> <!--使⽤aop:aspectj-autoproxy标签配置--> <!--此标签是基于XML和注解组合配置AOP时的必备标签,表示Spring开启注解配置AOP的⽀持--> <aop:aspectj-autoproxy proxy-target-class="true"></aop:aspectjautoproxy>
3)五种通知类型
1.前置通知:aop:before标签,前置通知可以获取切⼊点⽅法的参数,并对其进⾏增强,属性: method:⽤于指定前置通知的⽅法名称 pointcut:⽤于指定切⼊点表达式 pointcut-ref:⽤于指定切⼊点表达式的引⽤ 2.正常执⾏时通知:aop:after-returning标签 3.异常通知:op:after-throwing标签,异常通知不仅可以获取切⼊点⽅法执⾏的参数,也可以获取切⼊点⽅法执⾏产 ⽣的异常信息 4.最终通知:aop:after 最终通知的执⾏时机是在切⼊点⽅法(业务核⼼⽅法)执⾏完成之后,切⼊点⽅法返回之前执⾏。 换句话说,⽆论切⼊点⽅法执⾏是否产⽣异常,它都会在返回之前执⾏ 5.环绕通知:aop:around 环绕通知,它是有别于前⾯四种通知类型外的特殊通知。前⾯四种通知(前置,后置,异常和最终) 它们都是指定何时增强的通知类型。⽽环绕通知,它是Spring框架为我们提供的⼀种可以通过编码的 ⽅式,控制增强代码何时执⾏的通知类型。它⾥⾯借助的ProceedingJoinPoint接⼝及其实现类, 实现⼿动触发切⼊点⽅法的调⽤
2.组合模式
-
开启支持-很有必要
<!--开启spring对注解aop的⽀持--> <aop:aspectj-autoproxy/>
-
示例代码
/** * @author zhaojianqiang */ @Component @Aspect public class LogUtils { @Pointcut("execution(* com.example.demo.service.impl.TransferServiceImpl.*(..))") public void pt1() { } /** * 业务逻辑开始之前执行 */ @Before("pt1()") public void beforeMethod(JoinPoint joinPoint) { Object[] args = joinPoint.getArgs(); for (int i = 0; i < args.length; i++) { Object arg = args[i]; System.out.println(arg); } System.out.println("业务逻辑开始执行之前执行......."); } /** * 业务逻辑结束时执行(无论异常与否) */ @After("pt1()") public void afterMethod() { System.out.println("业务逻辑结束时执行,无论异常与否都执行......."); } /** * 异常时时执行 */ @AfterThrowing("pt1()") public void exceptionMethod() { System.out.println("异常时执行......."); } /** * 业务逻辑正常时执行 */ @AfterReturning(value = "pt1()", returning = "retVal") public void successMethod(Object retVal) { System.out.println("业务逻辑正常时执行......."); } /** * 环绕通知 */ /*@Around("pt1()")*/ public Object arroundMethod(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { Object result = null; try { System.out.println("环绕通知中的 beforeMethod...."); Object[] args = proceedingJoinPoint.getArgs(); result = proceedingJoinPoint.proceed(args); System.out.println("环绕通知中的 afterMethod...."); } catch (Exception e) { System.out.println("环绕通知中的 exceptionMethod...."); } finally { System.out.println("环绕通知中的 successMethod...."); } return result; } }
3.注解模式
在使⽤注解驱动开发aop时,我们要明确的就是,是注解替换掉配置⽂件中的下⾯这⾏配置:
<!--开启spring对注解aop的⽀持-->
<aop:aspectj-autoproxy/>
@Configuration
@ComponentScan("com.example.demo")
@EnableAspectJAutoProxy //开启spring对注解AOP的⽀持
public class SpringConfiguration {
}
五、声明式事务的支持
1. 事务回顾
1.1 事务概念
事务指逻辑上的⼀组操作,组成这组操作的各个单元,要么全部成功,要么全部不成功。从⽽确保了数
据的准确与安全。
1.2 事务四大特性
-
原⼦性(Atomicity)
原⼦性是指事务是⼀个不可分割的⼯作单位,事务中的操作要么都发⽣,要么都不发⽣。从操作的⻆度来描述,事务中的各个操作要么都成功要么都失败
-
⼀致性(Consistency)
事务必须使数据库从⼀个⼀致性状态变换到另外⼀个⼀致性状态。例如转账前A有1000,B有1000。转账后A+B也得是2000。⼀致性是从数据的⻆度来说的,(1000,1000) (900,1100),不应该出现(900,1000)
-
隔离性(Isolation)
事务的隔离性是多个⽤户并发访问数据库时,数据库为每⼀个⽤户开启的事务,每个事务不能被其他事务的操作数据所⼲扰,多个并发事务之间要相互隔离。
⽐如:事务1给员⼯涨⼯资2000,但是事务1尚未被提交,员⼯发起事务2查询⼯资,发现⼯资涨了2000块钱,读到了事务1尚未提交的数据(脏读)
-
持久性(Durability)
持久性是指⼀个事务⼀旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发⽣故障也不应该对其有任何影响。
1.3 事务隔离级别
不考虑隔离级别,会出现以下情况:(以下情况全是错误的),也即为隔离级别在解决事务并发问题
-
脏读:⼀个线程中的事务读到了另外⼀个线程中未提交的数据。
-
不可重复读:⼀个线程中的事务读到了另外⼀个线程中已经提交的update的数据(前后内容不⼀样)
场景:
员⼯A发起事务1,查询⼯资,⼯资为1w,此时事务1尚未关闭
财务⼈员发起了事务2,给员⼯A张了2000块钱,并且提交了事务
员⼯A通过事务1再次发起查询请求,发现⼯资为1.2w,原来读出来1w读不到了,叫做不可重复读
-
虚读(幻读):⼀个线程中的事务读到了另外⼀个线程中已经提交的insert或者delete的数据(前后条数不⼀样)
场景:
事务1查询所有⼯资为1w的员⼯的总数,查询出来了10个⼈,此时事务尚未关闭
事务2财务⼈员发起,新来员⼯,⼯资1w,向表中插⼊了2条数据,并且提交了事务
事务1再次查询⼯资为1w的员⼯个数,发现有12个⼈,⻅了⻤了
数据库共定义了四种隔离级别:
-
Serializable(串⾏化):可避免脏读、不可重复读、虚读情况的发⽣。(串⾏化) 最⾼
-
Repeatable read(可重复读):可避免脏读、不可重复读情况的发⽣。(幻读有可能发⽣) 第⼆
该机制下会对要update的⾏进⾏加锁
-
Read committed(读已提交):可避免脏读情况发⽣。不可重复读和幻读⼀定会发⽣。 第三
-
Read uncommitted(读未提交):最低级别,以上情况均⽆法保证。(读未提交) 最低
注意:级别依次升⾼,效率依次降低
MySQL的默认隔离级别是:REPEATABLE READ
查询当前使⽤的隔离级别: select @@tx_isolation;
设置MySQL事务的隔离级别: set session transaction isolation level xxx; (设置的是当前mysql连接会话的,并不是永久改变的)
1.4 事务传播行为
事务往往在service层进⾏控制,如果出现service层⽅法A调⽤了另外⼀个service层⽅法B,A和B⽅法本身都已经被添加了事务控制,那么A调⽤B的时候,就需要进⾏事务的⼀些协商,这就叫做事务的传播⾏为。
A调⽤B,我们站在B的⻆度来观察来定义事务的传播⾏为
PROPAGATION_REQUIRED | 如果当前没有事务,就新建⼀个事务,如果已经存在⼀个事务中,加⼊到这个事务中。这是最常⻅的选择。 |
---|---|
PROPAGATION_SUPPORTS | ⽀持当前事务,如果当前没有事务,就以⾮事务⽅式执⾏。 |
PROPAGATION_MANDATORY | 使⽤当前的事务,如果当前没有事务,就抛出异常。 |
PROPAGATION_REQUIRES_NEW | 新建事务,如果当前存在事务,把当前事务挂起。 |
PROPAGATION_NOT_SUPPORTED | 以⾮事务⽅式执⾏操作,如果当前存在事务,就把当前事务挂起。 |
PROPAGATION_NEVER | 以⾮事务⽅式执⾏,如果当前存在事务,则抛出异常。 |
PROPAGATION_NESTED | 如果当前存在事务,则在嵌套事务内执⾏。如果当前没有事务,则执⾏与PROPAGATION_REQUIRED类似的操作。 |
2. Spring中事务API
- mybatis: sqlSession.commit();
- hibernate: session.commit();
PlatformTransactionManager
package org.springframework.transaction;
import org.springframework.lang.Nullable;
public interface PlatformTransactionManager {
TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
throws TransactionException;
void commit(TransactionStatus status) throws TransactionException;
void rollback(TransactionStatus status) throws TransactionException;
}
此接⼝是Spring的事务管理器核⼼接⼝。Spring本身并不⽀持事务实现,只是负责提供标准,应⽤底层
⽀持什么样的事务,需要提供具体实现类。
此处也是策略模式的具体应⽤。在Spring框架中,也为我们内置了⼀些具体策略
如:DataSourceTransactionManager(mybatis) , HibernateTransactionManager(hibernate)
DataSourceTransactionManager 归根结底是横切逻辑代码,声明式事务要做的就是使⽤Aop(动态代
理)来将事务控制逻辑织⼊到业务代码
3. Spring声明式事务配置
3.1 纯xml
此种方式目前很少使用,我们不做代码示例
-
依赖
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.1.12.RELEASE</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.4</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.1.12.RELEASE</version>
-
xml配置
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"></property> </bean> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <!--定制事务细节,传播⾏为、隔离级别等--> <tx:attributes> <!--⼀般性配置--> <tx:method name="*" read-only="false" propagation="REQUIRED" isolation="DEFAULT" timeout="-1"/> <!--针对查询的覆盖性配置--> <tx:method name="query*" read-only="true" propagation="SUPPORTS"/> </tx:attributes> </tx:advice> <aop:config> <!--advice-ref指向增强=横切逻辑+⽅位--> <aop:advisor advice-ref="txAdvice" pointcut="execution(* com.example.demo.service.impl.TransferServiceImpl.*(*))"/> </aop:config>
3.2 xml+注解
-
xml配置
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"></property> </bean> <!--开启spring对注解事务的⽀持--> <tx:annotation-driven transaction-manager="transactionManager"/>
-
在接口、类或者方法上添加@Transactional注解
//注解参数可自定义 @Transactional(readOnly = true,propagation = Propagation.SUPPORTS, rollbackFor = Exception.class)
3.3 纯注解
Spring基于注解驱动开发的事务控制配置,只需要把 xml 配置部分改为注解实现。只是需要⼀个注解替换掉xml配置⽂件中的<tx:annotation-driven transactionmanager=“transactionManager”/> 配置。
在注解类上配置如下即可:
@EnableTransactionManagement//开启spring注解事务的⽀持
public class SpringConfiguration {
}