6. Spring AOP 应用

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

6.1 相关术语

1. 业务主线

在讲解AOP术语之前,我们先来看一下下面这两张图,它们就是第三部分案例需求的扩展(针对这些扩 展的需求,只进行分析,在此基础上去进一步回顾AOP,不进行实现)

上图描述的就是未采用AOP思想设计的程序,当我们红色框中圈定的方法时,会带来大量的重复劳动。 程序中充斥着大量的重复代码,使我们程序的独立性很差。而下图中是采用了AOP思想设计的程序,它把红框部分的代码抽取出来的同时,运用动态代理技术,在运行期对需要使用的业务逻辑方法进行增强。

2. AOP术语

  • 连接点:方法开始时、结束时、正常运行完毕时、方法异常时等这些特殊的时机点,我们称之为连接点,项目中每个方法都有连接点,连接点是一种候选点
  • 切入点:指定AOP思想想要影响的具体方法是哪些,描述感兴趣的方法
  • Advice增强
    • 第一个层次:指的是横切逻辑
    • 第二个层次:方位点(在某一些连接点上加入横切逻辑,那么这些连接点就叫做方位点,描述的是具体的特殊时机)
  • Aspect切面:切面概念是对上述概念的一个综合
    • Aspect切面 = 切入点+增强 = 切入点(锁定方法) + 方位点(锁定方法中的特殊时机) + 横切逻辑

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

6.2 代理选择

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

6.3 AOP的配置方式

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

  • 第一类:使用XML配置
  • 第二类:使用XML+注解组合配置
  • 第三类:使用纯注解配置

6.4 AOP实现

需求:横切逻辑代码是打印日志,希望把打印日志的逻辑织入到目标方法的特定位置(service层transfer方法)

1. xml模式

引入依赖

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.1.12.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>

核心配置

<!-- 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配置步骤:
第一步:把通知Bean交给Spring管理
第二步:使用aop:config开始aop的配置
第三步:使用aop:aspect配置切面 第四步:使用对应的标签配置通知的类型
入⻔案例采用前置通知,标签为aop:before
-->


<!--进行aop相关的xml配置,配置aop的过程其实就是把aop相关术语落地-->
<!--横切逻辑bean-->
<bean id="logUtils" class="com.lagou.edu.utils.LogUtils"></bean>
<!--使用config标签表明开始aop配置,在内部配置切面aspect-->

<!--aspect = 切入点(锁定方法) + 方位点(锁定方法中的特殊时机)+ 横切逻辑 -->
<aop:config>
<aop:aspect id="logAspect" ref="logUtils">

<!--切入点锁定我们感兴趣的方法,使用aspectj语法表达式-->
<!--<aop:pointcut id="pt1" expression="execution(* *..*.*(..))"/>-->
<aop:pointcut id="pt1" expression="execution(* com.lagou.edu.service.impl.TransferServiceImpl.*(..))"/>


<!--方位信息,pointcut-ref关联切入点-->
<!--aop:before前置通知/增强-->
<aop:before method="beforeMethod" pointcut-ref="pt1"/>
<!--aop:after,最终通知,无论如何都执行-->
<!--aop:after-returnning,正常执行通知-->
<!-- <aop:after-returning method="successMethod" returning="retVal"/>-->
<!--aop:after-throwing,异常通知-->

<aop:around method="arroundMethod" pointcut-ref="pt1"/>

</aop:aspect>
</aop:config>

**切入点表达式 **

上述配置实现了对 TransferServiceImpl 的 updateAccountByCardNo 方法进行增强,在其执行之前,输出了记录日志的语句。这里面,我们接触了一个比较陌生的名称:切入点表达式,它是做什么的呢?我们往下看。

  • 概念及作用 切入点表达式,也称之为AspectJ切入点表达式,指的是遵循特定语法结构的字符串,其作用是用于对符合语法格式的连接点进行增强。它是AspectJ表达式的一部分。
  • 关于AspectJ
    AspectJ是一个基于Java语言的AOP框架,Spring框架从2.0版本之后集成了AspectJ框架中切入点表达式的部分,开始支持AspectJ切入点表达式。
  • 切入点表达式使用示例
 全限定方法名 访问修饰符 返回值 包名.包名.包名.类名.方法名(参数列表) 全匹配方式:
public void com.lagou.service.impl.TransferServiceImpl.updateAccountByCardNo(com.lagou.pojo.Account)
访问修饰符可以省略
void com.lagou.service.impl.TransferServiceImpl.updateAccountByCardNo(com.lagou.pojo.Account)
返回值可以使用*,表示任意返回值
* com.lagou.service.impl.TransferServiceImpl.updateAccountByCardNo(com.lagou.pojo.Account)
包名可以使用.表示任意包,但是有几级包,必须写几个*
* *.*.*.*.TransferServiceImpl.updateAccountByCardNo(com.lagou.pojo.Account)
包名可以使用..表示当前包及其子包
* *..*.TransferServiceImpl.updateAccountByCardNo(com.lagou.pojo.Account)
类名和方法名,都可以使用.表示任意类,任意方法
* *..*.*(com.lagou.pojo.Account)
参数列表,可以使用具体类型 基本类型直接写类型名称 : int
引用类型必须写全限定类名:java.lang.String
参数列表可以使用*,表示任意参数类型,但是必须有参数
* *..*.*(*)
参数列表可以使用..,表示有无参数均可。有参数可以是任意类型
* *..*.*(..)
全通配方式:
* *..*.*(..)

2. xml + 注解

使用注解配置切面:

@Component
@Aspect
public class LogUtils {

@Pointcut("execution(* com.lagou.edu.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 {
System.out.println("环绕通知中的beforemethod....");

Object result = null;
try{
// 控制原有业务逻辑是否执行
// result = proceedingJoinPoint.proceed(proceedingJoinPoint.getArgs());
}catch(Exception e) {
System.out.println("环绕通知中的exceptionmethod....");
}finally {
System.out.println("环绕通知中的after method....");
}

return result;
}

}

开启aop注解驱动:

<!--开启aop注解驱动  proxy-target-class:true强制使用cglib--> 
<aop:aspectj-autoproxy/>

3. 纯注解模式

只需要在xml + 注解模式的基础下,定义一个配置类或者在已有配置类的基础上加上一个注解@EnableAspectJAutoProxy即可。基础下,定义一个配置类或者在已有配置类的基础上加上一个注解@EnableAspectJAutoProxy即可。