文章目录
1. AOP
1.1 概念
AOP(Aspect Oriented Programming)面向切面编程),是OOP(Object Oriented Programming )面向对象编程的一种补充和完善。
在传统的面向对象的业务处理代码中,通常会进行事务处理、日志记录等操作,虽然使用OOP可以通过组合或继承的方式来达到代码的重用,但如果想要实现某个功能(如日志记录),相同的代码仍然会分散到各个方法中。这样,如果想要关闭某个功能,或者对其进行修改,就必须修改所有相关的方法。这不但增加了开发量,也提高了代码的重用率。
为了解决上面的问题,AOP思想诞生了,AOP提供一种“横向切面抽取”的机制,将多个对象的公共模块封装成一个可重用模块,并将这个模块整合成为Aspect,即切面。切面就是与具体的业务逻辑无关的,确实许多业务模块共同的特性或职责的一种抽象,其减少了系统中的重复代码,因此降低了模块的耦合度,更加有利于扩展。它的作用是用来在程序运行期间,在不修改程序代码的情况下对已有方法进行增强。
AOP优势:减少重复代码,提高开发效率,维护方便。
实现方式:目前流行的AOP框架有两个,分别为Spring AOP
和AspectJ
。Spring AOP使用纯Java实现,不需要专门的编译器和类加载器,在运行期间通过代理方式向目标类植入增强的代码。AspectJ是一个基于Java语言的AOP框架,从Spring2.0开始,Spring AOP引入了对AspectJ的支持,AspectJ扩展了Java语言,提供了专门的编译器,在编译时提供横向代码的植入。
在Spring AOP中,实现AOP的方式是动态代理。而实现动态代理有两种方式:JDK动态代理和CGLIB动态代理。Spring根据是否实现被代理接口来选择使用JDK动态代理还是CGLIB动态代理。JDK动态代理和CGLIB动态代理
1.2 AOP相关术语
- Aspect(切面):切面通常是指封装的用于横向插入系统功能(如事务、日志等)的类,如果该类想被Spring识别为切面,需要在配置文件中通过< bean>元素指定。
- Joinpoint(连接点):在程序执行过程中的某个阶段点,它实际上是对象的一个操作。在Spring AOP中,连接点就是指方法的调用。
- Pointcut(切入点):指的是要对哪些连接点(Joinpoint)进行拦截的定义,所有的切入点都是连接点,但不是所有的连接点都是增强点。切入点表达式是切入点和连接点匹配的核心。
- Advice(通知):指拦截到Joinpoint之后所要做的事就是通知。通知的类型:前置通知、后置通知、异常通知、最终通知、环绕通知。
- Target Object(目标对象):指所有被通知的对象,也被称为增强对象。如果AOP框架采用的是动态的AOP实现,那么该对象就是一个被代理对象。
- Weaving(织入):是指把增强应用到目标对象来创建新的代理对象的过程。Spring采用动态代理织入。AspectJ采用编译期织入和类装载期织入。
2. AOP实战(基于AspectJ实现)
AspectJ
是一个基于Java语言的AOP框架,从Spring2.0开始,Spring AOP引入了对AspectJ的支持,并允许直接使用AspectJ进行编程,而Spring本身的AOP API也尽量与AspectJ保持一致。新版本的Spring框架建议使用AspectJ来开发AOP。使用时只需要引入AspectJ相关依赖即可。
<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
2.1 基于XML配置AOP
(1)首先引入AspectJ
依赖:
<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
(2)针对特定类型的方法进行增强
<!-- 配置spring的IoC,把Service对象配置进来-->
<bean id="accountService" class="service.impl.AccountServiceImpl"></bean>
<!-- 配置Logger类-->
<bean id="logger" class="utils.Logger"></bean>
<!-- 配置AOP-->
<aop:config>
<!--
配置切入点表达式,id属性用于指定表达式的唯一标识。expression属性用于指定表达式内容。
把它配置在<aop:aspect>标签外部(只能在切面aspect之前)时,所有切面可以用它。
把它配置在<aop:aspect>标签内部时,只有当前切面能用它。
-->
<aop:pointcut id="pt1" expression="execution(* *..*.saveAccount(..))"/>
<!-- 配置切面 -->
<aop:aspect id="logAdvice" ref="logger">
<!-- 配置前置通知,并且建立通知方法和切入点方法的关联 -->
<!-- 前置通知,在切入点方法执行之前执行-->
<aop:before method="beforePrintLog" pointcut-ref="pt1"></aop:before>
<!-- 后置通知,在切入点方法正常执行之后执行-,和异常通知只能执行一个-->
<aop:after-returning method="afterReturningPrintLog" pointcut="execution(* *..*.saveAccount(..))"></aop:after-returning>
<!-- 异常通知,在切入点方法产生异常之后执行-,和后置通知只能执行一个 -->
<aop:after-throwing method="afterThrowingPrintLog" pointcut="execution(* *..*.saveAccount(..))"></aop:after-throwing>
<!-- 最终通知,无论切入点方法是否正常执行,都会执行-->
<aop:after method="afterPrintLog" pointcut="execution(* *..*.saveAccount(..))"></aop:after>
<!-- 配置环绕通知-->
<aop:around method="aroundPrintLog" pointcut-ref="pt1"></aop:around>
</aop:aspect>
</aop:config>
2.2 Spring基于注解配置AOP
通过Aspectj注解的方式实现AOP编程,主要依赖以下注解:
@Aspect
:标识一个切面@Pointcut
:标识切入点@Before
:前置通知@Around
:环绕通知@AfterReturning
:后置通知@After
:最终通知@AfterThrowing
:异常通知
各种实例配置如下:
@Component(value = "logger")
@Aspect//表示当前类是一个切面类
public class Logger {
@Pointcut("execution(* *..*.saveAccount(..))")
private void pt1(){ }
/**
* 前置通知
* 用于打印日志:计划让其在切入点方法执行之前执行
* (切入点就是业务层方法)
*
*/
@Before("pt1()")
public void beforePrintLog(){
System.out.println("前置通知:Logger类中的printLog方法执行了,开始记录日志了");
}
/**
* 后置通知
*/
@AfterReturning("pt1()")
public void afterReturningPrintLog(){
System.out.println("后置通知:Logger类中的afterReturningPrintLog方法执行了,开始记录日志了");
}
/**
* 异常通知
*/
@AfterThrowing("pt1()")
public void afterThrowingPrintLog(){
System.out.println("异常通知:Logger类中的afterThrowingPrintLog方法执行了,开始记录日志了");
}
/**
* 最终通知
*/
@After("pt1()")
public void afterPrintLog(){
System.out.println("最终通知:Logger类中的afterPrintLog方法执行了,开始记录日志了");
}
}
<!-- 配置Spring启动时要扫描的包 -->
<context:component-scan base-package="service.impl,utils"></context:component-scan>
<!-- 配置Spring开启注解AOP的支持 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
实战:统一日志打印
@Aspect
@Component
public class SystemLogAspect {
private static final Logger logger = LoggerFactory.getLogger(SystemLogAspect.class);
/**
* Service层切点
*/
// @Pointcut("execution(* com.jd.jdd.ia.ack.service.*.*(..))")
@Pointcut("execution(* com.xingze.service.*.*(..)) || execution(* com.xingze.rpc.*.*(..))")
public void serviceAspect() {
}
/**
* 打印Service层日志
*/
@Around("serviceAspect()")
public Object doBefore(ProceedingJoinPoint joinPoint) throws Throwable {
//获取方法参数值数组
Object[] args = joinPoint.getArgs();
logger.info("调用[{}],参数=【{}】", joinPoint.getSignature().toShortString(), Optional.ofNullable(args).map(JSON::toJSONString).map(IaUtils::printStrLimit).orElse(""));
Object result = joinPoint.proceed(args);
logger.info("调用[{}],响应结果=【{}】", joinPoint.getSignature().toShortString(), Optional.ofNullable(result).map(JSON::toJSONString).map(IaUtils::printStrLimit).orElse(""));
return result;
}
}
3. AOP的Advice类型与切入点表达式
3.1 Advice类型(重点)
- 前置通知(Before Advice):在切入点方法执行之前执行
- 后置通知(After Advice):在切入点方法正常执行之后执行-,和异常通知只能执行一个
- 环绕通知(Around Advice):包围一个连接点的通知,这是最强大的一种通知类型。 环绕通知可以在方法调用前后完成自定义的行为。它也可以选择是否继续执行连接点或直接返回它们自己的返回值或抛出异常来结束执行。
- 返回后通知(AfterReturning Advice):在连接点正常完成后执行的通知(如果连接点抛出异常,则不执行)
- 抛出异常后通知(AfterThrowing advice):在切入点方法抛出异常之后执行。
3.2 Advice的执行顺序:
没有异常情况下的执行顺序:
- around before advice
- before advice
- target method 执行
- after advice
- around after advice
- afterReturning advice
出现异常情况下的执行顺序:
- around before advice
- before advice
- target method 执行
- after advice
- around after advice
- afterThrowing advice
- java.lang.RuntimeException:异常发生
3.3 切入点表达式的写法
关键字:execution(表达式) 表达式:访问修饰符 返回值 包名.包名.包名…类名.方法名(参数列表)
例如标准的表达式写法:public void com.xingze.service.impl.AccountServiceImpl.saveAccount()
- 访问修饰符可以省略、返回值可以使用通配符,表示任意返回值
* com.xingze.service.impl.AccountServiceImpl.saveAccount()
- 包名可以使用通配符,表示任意包。但是有几级包,就需要写几个*
*.*.*.*.AccountServiceImpl.saveAccount()
- 包名可以使用…表示当前包及其子包
* *..AccountServiceImpl.saveAccount()
- 类名和方法名都可以使用*来实现通配
* *..*.*()
- 全通配写法:
* *..*.*(..)
参数列表:
以直接写数据类型:基本类型直接写名称,引用类型写包名.类名的方式
以使用通配符*表示任意类型,但是必须有参数
以使用…表示有无参数均可,有参数可以是任意类型