Spring AOP 应用

AOP本质:在不改变原有业务逻辑的情况下增强横切逻辑,横切逻辑代码往往应用在权限校验、日志、事务控制、性能监控中。

1 AOP术语

名词解释
Joinpoint(连接点)它指的是那些可以用于把增强代码加入到业务主线中的点,这些点指的就是方法。在方法执行的前后通过动态代理技术假如增强的代码。在Spring框架AOP思想的技术实现中,也只支持方法类型的连接点。
Pointcut(切入点)它指的是那些已经把增强代码加入到业务主线进来之后的连接点。在项目中判断是否有访问权限就是一个连接点
Advice(通知/增强)它指的是切面类中用于提供增强功能的方法,并且不同的方法增强的时机是不一样的。比如,开启事务肯定要在业务方法执行之前执行;提交事务要在业务方法正常执行之后执行,而回滚事务要在业务方法执行产生异常之后执行等等。那么这些就是通知的类型。其分类有:前置通知 后置通知 异常通知 最终通知 环绕通知
Target(目标对象)它指的是代理的目标对象。即被代理对象。
Proxy(代理)它指的是一个类被AOP织如增强后,产生的代理类。即代理对象。
Weaving(织入)它指的是把增强应用到目标对象来创建新的代理对象的过程。Spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入。
Aspect(切面)它指定是增强的代码所关注的方面,把这些相关的增强代码定义到一个类中,这个类就是切面类。例如:事务切面,它里面定义的方法就是和事务相关的,像开启事务,提交事务,回滚事务等等,不会定义其他与事务无关的方法。
连接点:方法开始时、结束时、正常运行完毕时、方法异常时等这些特殊的时机点,我们称之为连接点,项目中每个方法都有连接点,连接点也是一种候选点
切入点: 指定AOP思想想要影响的具体方法时哪些
Advice增强:
     第一个层次:指的是横切逻辑
     第二个层次:方位点(在某一些连接点上加入横切逻辑,那么这些连接点就叫做方位点)
Aspect切面:切面概念是对上述概念的一个综合
     切面 = 切入点 + 增强
          = 切入点(锁定方法)+ 方位点(锁定方法中的特殊时机)+横切逻辑

众多的概念,目的就是为了锁定要在哪个地方插入什么横切逻辑代码

2 Spring中AOP代理选择

Spring实现AOP思想使用的是动态代理技术
默认情况下,Spring会根据被代理对象是否实现接口来选择使用JDK还是CGLIB。当被代理对象没有实现任何接口时,Spring会选择CGLIB。当被代理对象实现了接口,Spring会选择JDK官方的代理技术,不过我们可以通过配置的方式,让Spring强制使用CGLIB。

3 Spring中AOP的配置方式

在Spring的AOP配置中,也和IoC配置一样,支持3类配置方法

  • 使用XML配置
  • 使用XML+注解组合配置
  • 使用纯注解配置

4 Spring中AOP实现

4.1 XML模式

  • 坐标
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>5.3.15</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.7</version>
</dependency>
  • AOP 核心配置
<!--
	Spring基于XML的AOP配置前期准备:在spring的配置文件中加入aop的约束
	xmlns:aop="http://www.springframework.org/schema/aop"
	http://www.springframework.org/schema/aop
	https://www.springframework.org/schema/aop/spring-aop.xsd
	
	Spring基于XML的AOP配置步骤:
	1、把通知Bean交给Spring管理
	2、使用aop:config开始aop的配置
	3、使用aop:aspect配置切面
	4、使用对应的标签配置通知的类型
	入门案例采用前置通知,标签为aop:before
-->
<!--把通知Bean交给Spring来管理-->
<bean id="logUitls" class="com.lxsh.utils.LogUtils"></bean>
<!--开始aop的配置-->
<aop:config>
	<!--配置切面-->
	<aop:aspect id="idAdvice" ref="logUtils">
		<!--配置前置通知-->
		<aop:before method="printLog" pointcut="execution(public * com.lxsh.service.*.*(..))"></aop:before>
	</aop:aspect>
</aop:config>
  • 细节
    关于切入点表达式
    上述配置实现了对service包下任意方法进行增强,在其执行之前,输出了记录日志的语句。在这里,我们接触了一个比较陌生的名称:切入点表达式,它是做什么的呢?我们继续往下看。
    概念及作用
    切入点表达式,也称之为AspectJ切入点表达式,**指的是遵循特定语法结构的字符串,其作用是用于对符合语法格式的连接点进行增强。**他是AspectJ表达式的一部分。
    关于AspectJ
    AspectJ是一个基于Java语言的AOP框架,Spring框架从2.0版本之后集成了AspectJ框架中切入点表达式的部分,开始支持AspectJ切入点表达式。
    切入点表达式的使用示例

    全限定方法名     访问修饰符     返回值     包名.包名.包名.类名.方法名(参数列表)
    全匹配方式
    public void com.lxsh.service.UserService.saveUser(com.lxsh.po.User)
    访问修饰符可以省略
    void com.lxsh.service.UserService.saveUser(com.lxsh.po.User)
    返回值可以用  *   表示任意返回值
    * com.lxsh.service.UserService.saveUser(com.lxsh.po.User)
    包名可以使用  .   表示任意包,但是有几级包,必须写几个
    * ...UserService.saveUser(com.lxsh.po.User)
    包名可以使用  ..   表示当前包及其子包
    * ..UserService.saveUser(com.lxsh.po.User)
    类名和方法名,都可以用 . 表示任意类、任意方法
    * ...(com.lxsh.po.User)
    参数列表,可以使用具体类型
    基本类型直接写类型名称:int
    引入类型必须写全限定类型:java.lang.String
    参数列表可以使用*,表示任务参数类型,但是必须有参数
    * *..*.*(*)
    参数列表也可以使用 ..  表示有无参数均可,有参数可以是任意类型
    * *..*.*(..)
    全通配方式
    * *..*.*(..)
    

    改变代理方式的配置
    在前面我们已经说了,Spring在选择创建代理对象时,会根据被代理对象的实际情况来选择的。被代理对象实现了接口,则采用基于接口的动态代理。当被代理对象没有实现任何接口的时候,Spring会自动切换到基于子类的动态代理方式。
    但是我们都知道,无论被代理对象是否实现接口,只要不是final修饰的类都可以采用CGLIB提供的方式创建代理对象。所以Spring也考虑到了这个情况,提供了配置的方式实现强制使用基于子类的动态代理(即CGLIB的方式),配置方式有两种

<!--使用aop:config标签-->
<aop:config proxy-target-class="true"></aop:config>
<!--使用aop:aspectj-autoproxy标签配置-->
<aop:aspectj-autoproxy proxy-target-class="true"></aop:aspectj-autoproxy>

五种通知类型

  1. 前置通知
    配置方式:aop:before标签
<!--作用:用于配置前置通知。
	出现位置:它只能出现在aop:aspect标签内部。
	属性:
	    method:用于指定前置通知的方法名称
	    pointcut:用于指定切入点表达式
	    pointcut-ref:用于指定切入点表达式的引用
	    arg-names:用于指定通知方式的参数名称,要求表达式中必须有描述args的语句-->
<aop:before method="beforeMethod" pointcut-ref="pt"></aop:before>

执行时机
前置通知永远都会在切入点方法(业务核心方法)执行之前执行。
细节
前置通知可以获取切入点方法的参数,并对其进行增强。

  1. 后置通知
    配置方式:aop:after-returning标签
<!--作用:用于配置后置通知。
	出现位置:它只能出现在aop:aspect标签内部。
	属性:
	    method:用于指定后置通知的方法名称
	    pointcut:用于指定切入点表达式
	    pointcut-ref:用于指定切入点表达式的引用
	    arg-names:用于指定通知方式的参数名称,要求表达式中必须有描述args的语句
	    returning:用于指定返回值类型-->
<aop:after-returning method="returningMethod" pointcut-ref="pt"></aop:after-returning>

执行时机
后置通知的执行时机时在切入点方法正常执行之后执行(业务核心方法),当切入点方法执行产生异常之后,后置通知就不再执行了,而是执行异常通知。
细节
后置通知既可以获取切入点方法的参数,也可以获取到切入点方法的返回值。

  1. 异常通知
    配置方式
<!--作用:用于配置异常通知。
	出现位置:它只能出现在aop:aspect标签内部。
	属性:
	    method:用于指定异常通知的方法名称
	    pointcut:用于指定切入点表达式
	    pointcut-ref:用于指定切入点表达式的引用
	    arg-names:用于指定通知方式的参数名称,要求表达式中必须有描述args的语句
	    throwing:用于指定异常通知中异常的变量名称-->
<aop:after-throwing method="throwingMethod" pointcut-ref="pt"></aop:after-throwing>

执行时机
异常通知的执行时机时在切入点方法(业务核心方法)执行产生异常之后,异常通知执行,如果切入点方法执行没有产生异常,则异常通知不会执行。
细节
异常通知不仅可以获取切入点方法执行的参数,也可以获取切入点方法执行产生的异常信息。

  1. 最终通知
    配置方式
<!--作用:用于配置最终通知。
	出现位置:它只能出现在aop:aspect标签内部。
	属性:
	    method:用于指定后置通知的方法名称
	    pointcut:用于指定切入点表达式
	    pointcut-ref:用于指定切入点表达式的引用
	    arg-names:用于指定通知方式的参数名称,要求表达式中必须有描述args的语句
-->
<aop:after method="afterMethod" pointcut-ref="pt"></aop:after>

执行时机
最终通知的执行时机时在切入点方法正常执行之后执行(业务核心方法),切入点方法返回之前执行,换句话说,无论切入点方法执行是否产生异常,他都会在返回之前执行。
细节
最终通知执行时,可以获取到通知方法的参数,同时它可以做一些清理操作。

  1. 环绕通知
    配置方式
<!--作用:用于配置环绕通知。
	出现位置:它只能出现在aop:aspect标签内部。
	属性:
	    method:用于指定后置通知的方法名称
	    pointcut:用于指定切入点表达式
	    pointcut-ref:用于指定切入点表达式的引用
	    arg-names:用于指定通知方式的参数名称,要求表达式中必须有描述args的语句
-->
<aop:around method="aroundMethod" pointcut-ref="pt"></aop:around>

特别说明
环绕通知,它是有别于前面四种通知类型外的特殊通知,前面四种通知(前置、后置、异常和最终)它们都是指定何时增强的通知类型。而环绕通知,它是Spring框架为我们提供的一种可以通过编码的方式,控制增强代码何时执行的通知类型,它里面借助的ProceedingJoinPoint接口及其实现类,实现手动触发切入点方法的调用。
执行时机
当配置环绕通知之后,在环绕通知里面必须要明确调用业务层的方法,如果不调用,就会出现只出现通知,而不执行方法。其中原理和动态代理是一样的
细节
环绕通知可以代替其他的四个通知,所以环绕通知不可以与其他四种通知共用。

4.2 XML+注解模式

基于xml模式的演进

/**
 * @Aspect注解 开启切面
 */
@Component
@Aspect
public class LogUtils{

	/**
	 * 配置切入点
	 */
	@Pointcut("execution(public * com.lxsh.service.*.*(..))")
	public void pointCut(){
	}

	/**
	 * 业务逻辑开始之前
	 */
	@Before("pointCut()")
	public void beforeMethod(JoinPoint joinPoint){
		//方法参数
		Object[] args = joinPoint.getArgs();
		System.out.println("业务逻辑开始之前执行...")
	}

	/**
	 * 业务逻辑结束时执行(无论异常与否)
	 */
	@After("pointCut()")
	public void afterMethod(JoinPoint joinPoint){
		System.out.println("业务逻辑结束时执行(无论异常与否)...")
	}

	/**
	 * 业务异常时执行
	 */
	@AfterThrowing("pointCut()")
	public void throwingMethod(JoinPoint joinPoint){
		System.out.println("异常时执行...")
	}

	/**
	 * 业务逻辑正常时执行
	 */
	@AfterReturning(value = "pointCut()" returning ="retVal")
	public void sucessMethod(Object retVal){
		System.out.println("业务逻辑正常时执行...")
	}

	/**
	 * 环绕通知
	 */
	@Around("pointCut()")
	public Object aroundMethod(ProceedingJoinPoint proceedingJoinPoint){
		System.out.println("环绕通知中的before method...")
		Object result = null
		try{
			result = proceedingJoinPoint.proceed(proceedingJoinPoint.getArgs());
			System.out.println("环绕通知中的beforemethod...")
		}catch(Exception e){
			System.out.println("环绕通知中的throwing method...")
		}finally{
			System.out.println("环绕通知中的after method...")
		}
		System.out.println("环绕通知中的afterReturning method...")
		return result;
	}
}
<!--开启AOP注解驱动-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>

4.3 纯注解模式

基于xml+注解模式的演进

在启动类/配置类上加入以下注释即可
@EnableAspectJAutoProxy

5 Spring 声明式事务的支持

**编程式事务:**在业务代码中添加事务控制代码,这样的事务控制机制就叫做编程式事务
**声明式事务:**通过XML或者注解配置的方式达到事务控制的目的,叫做声明式事务

5.1 事务回顾

5.1.1 事务的概念

事务指逻辑上的一组操作,组成这组操作的各个单元,要么全部成功,要么全部不成功,从而确保了数据的准确与安全。
例如:A–>B转账,对应如下两条sql语句:

/*转出账户减钱*/
update account set money = money-100 where name = 'A';
/*转入账户加钱*/
update account set money = money+100 where name = 'B';

这两条语句的执行,要么全部成功,要么全部不成功。

5.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)
持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性,接下来即使数据库发生故障也不应该对其有任何影响。

5.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 isloation level xxx;(设置的是当前mysql连接会话的,并不是永久改变的)

5.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类似的操作

5.2 Spring中事务的API

PlatformTransactionManager

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,HibernateTransactionManager等等(HibernateTransactionManager事务管理器在spring-orm-5.1.12.RELEASE.jar中)
SpringJdbcTemplate(数据库操作工具)
DataSourceTransactionManager(Mybatis) 归根结底是横切逻辑代码,声明式事务要做的就是使用AOP(动态代理)将事务控制逻辑织入到业务代码

5.3 Spring基于注解的声明式事务配置

基于XML+注解
- 事务的必要坐标

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.3.15</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.7</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>5.3.15</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-tx</artifactId>
    <version>5.3.15</version>
</dependency>
  • 配置事务
<!--配置事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
	<property name="dataSource" ref="dataSource"></property>
</bean>
<!--开启spring对注解事务的支持-->
<tx:annotation-driven transaction-manager="transactionManager"/>
  • 在接口、类或者方法上添加@Transaction注解
@Transaction(readOnly=true,propagation=Propagation.SUPPORTS)

基于纯注解
Spring基于注解驱动开发的事务控制配置,只需要把xml配置部分改为注解实现,只是需要一个注解替换掉xml配置文件中的<tx:annotation-driven transaction-manager="transactionManager"/>配置。
在Spring配置类上添加@EnableTransactionManagement注解即可

//开启Spring注解事务的支持
@EnableTransactionManagement
public class SpringConfiguration{

}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值