1. AOP的基本概念
AOP:面向切面编程(Aspect Oriented Programming),通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术
AOP的作用:
利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发效率
没有AOP技术的时候如何实现方法增强,常用的三种方式:
-
在方法内部添加功能
-
添加一个子类,通过子类扩展增加父类的方法
-
在新的方法中调用旧方法,在新方法中增强功能
public class UserServiceImpl implements IUserService {
/**
* find user list.
*
* @return user list
*/
@Override
public List<User> findUserList() {
System.out.println("execute method: findUserList");
return Collections.singletonList(new User("pdai", 18));
}
/**
* add user
*/
@Override
public void addUser() {
System.out.println("execute method: addUser");
// do something
}
}
AOP的操作
通过注解/配置文件的方式,不修改源代码,直接增强方法功能
面向对象编程(OOP):针对业务处理过程的实体,及其属性和行为进行抽象封装,以获得更加清晰高效的逻辑单元划分。
面向切面编程(AOP):AOP则是针对业务处理过程中的切面进行提取,用于处理某个步骤或阶段,以获得逻辑过程中各部分之间低耦合的隔离效果
1.1 AOP术语
连接点(joinpoint):表示需要在程序中插入横切关注点的扩展点(需要被增强的方法【在哪里干】),这些方法就称为连接点(连接点为切入准备点),连接点可以做类初始化、方法执行、方法调用、字段调用或处理异常等,Spring只支持方法作为连接点
切点(pointcut):选择一组相关连接点的模式,即连接点的集合,通俗理解为:哪些方法真正被增强了,真正增强的方法被称为切点(切点为实际切入点),Spring支持perl5正则表达式和AspectJ切入点模式
-
切点表达式
切入点表达式作用:知道对哪个类里面的哪个方法进行增强,有多种表达式,并且表达式之间可以组合使用
-
Execution表达式
精确匹配指定包/指定类/指定方法/指定权限修饰符等
语法结构:execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)
exection([权限修饰符][返回类型][类全路径].方法名称(形式参数) 异常)
-
modifiers-pattern:表示权限修饰符,*表示所有权限修饰符,public表示只匹配public修饰的方法
//匹配UserDaoImpl类的add方法,该方法必须为public修饰 execution(public * com.carl.dao.impl.UserDaoImpl.add(..)) //匹配UserDaoImpl类的所有方法,且不限制权限修饰符 execution(* com.carl.dao.impl.UserDaoImpl.*(..)) //匹配所有public修饰的方法 execution(public * *(..))
-
ret-type-pattern:表示返回值类型,*表示所有返回值类型,void表示返回值为void的,int表示返回值为int的依此推断
//对com.carl.dao.impl.UserDaoImpl类里的add方法进行增强,不限制返回值 exection(* com.carl.dao.impl.UserDaoImpl.add(..)) //对com.carl.dao.impl.UserDaoImpl类里的所有返回值为int类型的方法进行增强 exection(int com.carl.dao.impl.UserDaoImpl.*(..)) //对com.carl.dao包里的所有类里返回值类型为void的方法进行增强 exection(void com.carl.dao.*.*(..))
-
declaring-type-pattern:表示类路径,例如com.carl.dao.impl.UserDaoImpl表示匹配com.carl.dao.impl包下的UserDaoImpl类,如果要匹配com.carl.dao.impl包下的所有类,则使用com.carl.dao.impl.*
//对com.carl.dao.impl.UserDaoImpl类里的add方法进行增强 exection(* com.carl.dao.impl.UserDaoImpl.add(..)) //对com.carl.dao.impl.UserDaoImpl类里的所有方法进行增强 exection(* com.carl.dao.impl.UserDaoImpl.*(..)) //对com.carl.dao包里的所有类里的方法进行增强 exection(* com.carl.dao.*.*(..))
-
name-pattern:表示方法名,
*
表示所有方法名,紧跟在declaring-type-pattern后面(没有空格),例如com.carl.dao.impl.UserDaoImpl.*表示UserDaoImpl类下的所有方法// 任何一个名字以“set”开始的方法的执行: execution(* set*(..)) //对com.carl.dao.impl包下所有类里的add方法进行增强 exection(* com.carl.dao.impl.*.add(..)) //对com.carl.dao.impl包下所有类里抛出Execption及子异常的add方法进行增强 exection(* com.carl.dao.impl.*.add(..) throws Exception)
-
param-pattern:表示方法的形式参数,()表示没有参数,()表示任意类型和数量的参数,(…)同()
//对com.carl.dao.impl.UserDaoImpl类里的add方法进行增强 exection(* com.carl.dao.impl.UserDaoImpl.add(..)) //对com.carl.dao.impl.UserDaoImpl类里的add方法进行增强,add方法必须有两个形式参数,第一个为int,第二个为String exection(* com.carl.dao.impl.UserDaoImpl.add(int,String)) //对com.carl.dao.impl.UserDaoImpl类里的空参add方法进行增强 exection(* com.carl.dao.impl.UserDaoImpl.add())
-
throws-pattern:表示异常信息,如Exception表示抛出Exception或者它的子类
//对com.carl.dao.impl包下所有类里抛出Execption及子异常的add方法进行增强 exection(* com.carl.dao.impl.*.add(..) throws Exception)
-
-
Within表达式
模糊匹配某个包内的所有类和方法,语法结构:within(type-pattern)
within(包路径匹配)
//匹配controller包下的所有方法 within(com.example.demo.controller.*) //匹配controller包或其子包下的所有方法 within(com.example.demo.controller..*)
-
This表达式
模糊匹配某个类或其子类的所有方法
语法结构:this(type)this(类路径匹配)
//匹配UserService及其子类中的所有方法 this(com.example.demo.service.UserService)
-
Target表达式
模糊匹配某个类或其子类的所有方法
语法结构:target(type)target(类路径匹配)
//匹配UserService及其子类中的所有方法 target(com.example.demo.service.UserService)
-
Args表达式
用于匹配当前执行的方法参数类型与指定类型相匹配的所有方法
语法结构:args(type-pattern)args(String)
//限制目标方法必须有两个形参 args(param1,param2) //使用方式: // 下面的args(arg0,arg1)会限制目标方法必须有2个形参 // 此处access方法指定arg0、arg1为String类型,则args(arg0,arg1)还要求目标方法的两个形参都是String类型 @AfterReturning(returning="rvt" , pointcut="execution(* org.crazyit.app.service.impl.*.*(..)) && args(arg0,arg1)") public void access(Object rvt, String arg0 , String arg1){ System.out.println("调用目标方法第1个参数为:" + arg0); System.out.println("调用目标方法第2个参数为:" + arg1); System.out.println("获取目标方法返回值:" + rvt); System.out.println("模拟记录日志功能..."); }
-
@annotation表达式
用于匹配被指定注解标注的方法
语法结构:@annotation(type)@annotation(注解类型)
//匹配所有被MyAnnotation注解标注的方法 @annotation(com.example.demo.annotation.MyAnnotation)
组合表达式的逻辑运算符
&&:要求连接点同时匹配两个切入点表达式
||:要求连接点匹配任意个切入点表达式
!:要求连接点不匹配指定的切入点表达式
-
通知(增强)(advice):在连接点上执行的逻辑部分,提供了在切入点所选择的连接点处扩展现有方法的手段。实际增强的逻辑部分称为通知或增强(通知为切入逻辑)
-
通知的类型:
-
前置通知(before)
前置通知,即在目标方法调用之前执行,注意,无论方法是否遇到异常都会执行
-
后置通知(after returning)
在目标方法执行后执行,前提是目标方法没有遇到异常,如果有异常则不执行该通知
-
环绕通知(around)
可以控制目标方法的执行(ProceedingJoinPoint.proceed()),可以在目标方法前后实现增强
-
异常通知(after throwing)
在目标方法抛出异常时执行,可以获取异常信息
-
最终通知(after)
在目标方法执行后执行,无论是否有异常都会执行
-
切面(aspectJ):由切点和通知组成,即包括切点的逻辑定义和连接点的定义,在Spring中可以使用Schema和@AspectJ方式进行组织实现
引入(inter-type declaration):也称为内部类型声明,为已有的类添加额外新的字段或方法,Spring允许引入新的接口(必须对应一个实现类)到所有被代理的对象
目标对象(Target Object):需要被织入横切面的对象,即改对象是切入点选择的对象,需要被通知的对象,也称为被通知对象;由于SpringAOP采用代理模式实现,因此该对象就是被代理对象
织入(wearing):将增强添加到目标类的具体连接点的过程
AOP代理(AOP proxy):AOP框架使用代理模式创建的对象。从而实现在连接点处插入通知,就是通过代理来对目标对象应用切面。在Spring中,AOP代理可以用JDK动态代理或CGLIB代理实现,而通过拦截器模型应用到切面
Spring框架基于AspectJ实现AOP操作
AspectJ不是Spring的组成部分,是独立的AOP框架,一般把AspectJ和Spring框架一起使用,进行AOP操作
2. Spring AOP和AspectJ的区别
AspectJ是Java实现的AOP框架,能够对Java代码进行AOP编译,让Java代码具有AspectJ的AOP功能(需要特殊的编译器)
AspectJ是目前实现AOP框架中最成熟,功能最丰富的语言,更幸运的是,AspectJ与Java程序完全兼容
Spring中的AOP注解几乎都是来源于AspectJ
Spring2.0开始,就已经开始采用AspectJ 5一样的注解,并使用AspectJ来做切入点解析和匹配,但是,AOP在运行时仍然是纯的Sping AOP,并不依赖于AspectJ的编译器或者织入器
上述提到的织入的概念:是aspect(切面)应用到目标类/方法的过程。这个过程一般分为动态织入和静态织入
-
动态织入:在运行时动态将要增强的代码织入到目标类中,这样往往是通过动态代理技术实现的,例如:JDK的动态代理(底层反射)或者CGLIB的动态代理(底层通过继承实现),Spring AOP采用的就是基于运行时增强的代理技术
-
静态织入:AspectJ采用的就是静态织入的方式,即在编译期织入,在这个期间,使用AspectJ的acj编译器(类似javac)把aspect类编译成class字节码后,在Java目标类编译时织入,即先编译aspect再编译目标类
下表总结了 Spring AOP 和 AspectJ 之间的关键区别:
Spring AOP | AspectJ |
---|---|
在纯 Java 中实现 | 使用 Java 编程语言的扩展实现 |
不需要单独的编译过程 | 除非设置 LTW,否则需要 AspectJ 编译器 (ajc) |
只能使用运行时织入 | 运行时织入不可用。支持编译时、编译后和加载时织入 |
功能不强-仅支持方法级编织 | 更强大 - 可以编织字段、方法、构造函数、静态初始值设定项、最终类/方法等 |
只能在由 Spring 容器管理的 bean 上实现 | 可以在所有域对象上实现 |
仅支持方法执行切入点 | 支持所有切入点 |
代理是由目标对象创建的, 并且切面应用在这些代理上 | 在执行应用程序之前 (在运行时) 前, 各方面直接在代码中进行织入 |
比 AspectJ 慢多了 | 更好的性能 |
易于学习和应用 | 相对于 Spring AOP 来说更复杂 |
3. Spring AOP的底层原理
Spring AOP的底层使用的动态代理
有两种动态代理的情况
-
有接口的动态代理(JDK动态代理)
接口使用JDK动态代理
定义接口IJdkProxyService
public interface IJdkProxyService { void doMethod1(); String doMethod2(); String doMethod3() throws Exception; }
定义接口实现类JdkProxyDemoServiceImpl
@Service public class JdkProxyDemoServiceImpl implements IJdkProxyService { @Override public void doMethod1() { System.out.println("JdkProxyServiceImpl.doMethod1()"); } @Override public String doMethod2() { System.out.println("JdkProxyServiceImpl.doMethod2()"); return "hello world"; } @Override public String doMethod3() throws Exception { System.out.println("JdkProxyServiceImpl.doMethod3()"); throw new Exception("some exception"); } }
定义切面LogAspect
package tech.pdai.springframework.aspect; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.AfterThrowing; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.springframework.context.annotation.EnableAspectJAutoProxy; import org.springframework.stereotype.Component; @EnableAspectJAutoProxy @Component @Aspect public class LogAspect { /** * define point cut. */ @Pointcut("execution(* tech.pdai.springframework.service.*.*(..))") private void pointCutMethod() { } /** * 环绕通知. * * @param pjp pjp * @return obj * @throws Throwable exception */ @Around("pointCutMethod()") public Object doAround(ProceedingJoinPoint pjp) throws Throwable { System.out.println("-----------------------"); System.out.println("环绕通知: 进入方法"); Object o = pjp.proceed(); System.out.println("环绕通知: 退出方法"); return o; } /** * 前置通知. */ @Before("pointCutMethod()") public void doBefore() { System.out.println("前置通知"); } /** * 后置通知. * * @param result return val */ @AfterReturning(pointcut = "pointCutMethod()", returning = "result") public void doAfterReturning(String result) { System.out.println("后置通知, 返回值: " + result); } /** * 异常通知. * * @param e exception */ @AfterThrowing(pointcut = "pointCutMethod()", throwing = "e") public void doAfterThrowing(Exception e) { System.out.println("异常通知, 异常: " + e.getMessage()); } /** * 最终通知. */ @After("pointCutMethod()") public void doAfter() { System.out.println("最终通知"); } }
测试结果
----------------------- 环绕通知: 进入方法 前置通知 JdkProxyServiceImpl.doMethod1() 最终通知 环绕通知: 退出方法 ----------------------- 环绕通知: 进入方法 前置通知 JdkProxyServiceImpl.doMethod2() 后置通知, 返回值: hello world 最终通知 环绕通知: 退出方法 ----------------------- 环绕通知: 进入方法 前置通知 JdkProxyServiceImpl.doMethod3() 异常通知, 异常: some exception 最终通知
-
无接口的动态代理(GCLib动态代理)
非接口使用Cglib代理
类定义CglibProxyDemoServiceImpl
@Service public class CglibProxyDemoServiceImpl { public void doMethod1() { System.out.println("CglibProxyDemoServiceImpl.doMethod1()"); } public String doMethod2() { System.out.println("CglibProxyDemoServiceImpl.doMethod2()"); return "hello world"; } public String doMethod3() throws Exception { System.out.println("CglibProxyDemoServiceImpl.doMethod3()"); throw new Exception("some exception"); } }
切面定义和定义切面LogAspect相同
测试结果----------------------- 环绕通知: 进入方法 前置通知 CglibProxyDemoServiceImpl.doMethod1() 最终通知 环绕通知: 退出方法 ----------------------- 环绕通知: 进入方法 前置通知 CglibProxyDemoServiceImpl.doMethod2() 后置通知, 返回值: hello world 最终通知 环绕通知: 退出方法 ----------------------- 环绕通知: 进入方法 前置通知 CglibProxyDemoServiceImpl.doMethod3() 异常通知, 异常: some exception 最终通知
4. AOP注解详解
注解名称 | 解释 |
---|---|
@Aspect | 用于定义一个切面 |
@pointcut | 用于定义切入点表达式,在使用时还需要定义一个包含名字和任意参数的方法签名来表示切入点名称,这个方法签名就是一个返回值为void,且方法体为空的普通方法 |
@Before | 用于定义前置通知,在使用时,通常需要指定一个value属性值,该属性值用于指定一个切入点表达式,也可以是已有的切入点 |
@AfterReturning | 用于定义后置通知,在使用时可以指定pointcut/value和returning属性,其中pointcut/value两个属性的作用一样,都用于指定切入点表达式 |
@Around | 用于定义环绕通知,在使用时需要指定一个value属性,该属性用于指定该通知被织入的切入点 |
@After-Throwing | 用于定义异常通知来处理程序中未处理的异常,在使用时可指定pointcut/value和throwing属性,其中throwing属性值用于指定一个形参名(形参名用于访问目标方法抛出的异常) |
@After | 用于定义最终final通知,不管是否异常,该通知都会执行,使用时需要指定value属性 |
@DeclareParents | 用于定义引介通知(几乎不使用) |
5. 基于AspectJ实现AOP切面
在项目工程中引入AOP相关依赖
在上述包依赖的基础上增加spring-aspects-5.3.9.jar
,以及一个外部依赖包
aspectjweaver-1.9.9.1.jar
以下实现前提:
package com.carl.entity;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;
/**
* @Version 1.0.0
*
* @author carl蔡先生
* @Date 2022/10/04
* @Description 使用者
*/
@Component
public class User{
/**
* @see String
* 姓名
*/
private String name;
public void setName(String name) {
this.name = name;
}
public Integer add(int a, int b){
int i = a / b;
System.out.println("User Add Result:"+a / b);
return i;
}
}
基于xml配置实现
-
定义切面类UserProxy
package com.carl.aop; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.Signature; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; import java.util.Arrays; /** * @Version 1.0.0 * @author carl蔡先生 * @Date 2022/10/04 * @Description 用户代理 */ @Component(value = "diy") public class UserProxy { /** * 前置通知 */ public void before(JoinPoint joinPoint){ Object[] args = joinPoint.getArgs(); Signature signature = joinPoint.getSignature(); System.out.println("[before] [前置通知:"+ signature.getName()+"],参数为:"+ Arrays.asList(args)); } /** * 异常通知 */ public void afterThrowing(){ System.out.println("afterThrowing"); } /** * 后置通知 */ public void afterReturning(JoinPoint joinPoint){ System.out.println("afterReturning:后置通知:"); } /** * 最终通知 */ public void after(JoinPoint joinPoint){ System.out.println("[after] [最终通知:"+ joinPoint.getSignature().getName()+"]"); } /** * 环绕通知 */ public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { System.out.println("around1:前环绕通知"); proceedingJoinPoint.proceed(); System.out.println("around2:后环绕通知"); } }
-
配置xml的aop代理
添加aop的命名空间—红色部分<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
添加aop-config配置
<!--注解方式就使用自动装配咯,如果不使用注解,使用bean标签也是可以的啦--> <context:component-scan base-package="com.carl"/> <!-- 注意事项: 1.aop:aspect标签的ref属性对应@Component注解中的value值,即bean实例中的id属性值 2.aop:pointcut标签表示切点,使用表达式定义具体的增强方法 3.aop:after-returning~aop:after-throwing这五个标签,可以通过调整顺序,改变执行顺序 4.pointcut-ref属性,如果定义了aop:pointcut标签,就需要使用该属性关联,否则aop:pointcut不生效 也可以使用pointcut属性,单独定义切点表达式:<aop:after method="after" pointcut="execution(Integer com.carl.entity.User.add(..))"/> 5.一个aop:config标签可以定义多个切面aop:aspect --> <aop:config> <aop:aspect ref="diy"> <aop:pointcut id="point" expression="execution(Integer com.carl.entity.User.add(..))"/> <aop:after-returning method="afterReturning" pointcut-ref="point"/> <aop:around method="around" pointcut-ref="point"/> <aop:before method="before" pointcut-ref="point"/> <aop:after method="after" pointcut-ref="point"/> <aop:after-throwing method="afterThrowing" pointcut-ref="point"/> </aop:aspect> </aop:config>
-
测试
@Test public void testAop(){ ApplicationContext context = new FileSystemXmlApplicationContext("/resource/bean1.xml"); User user = context.getBean("user", User.class); user.add(1, 2); }
基于注解方式实现
-
定义切面类
UserProxypackage com.carl.aop; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.Signature; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; import java.util.Arrays; /** * @Version 1.0.0 * @author carl蔡先生 * @Date 2022/10/04 * @Description 用户代理 */ @Component(value = "diy") @Aspect//1.添加注解@Aspect,标注UserProxy为切面类 public class UserProxy { /** * 2.定义切点方法和表达式 */ @Pointcut("execution(Integer com.carl.entity.User.add(..))") public void pointcut(){} /** * 3.实现通知方法,使用注解标注通知的类型,设置value值为切点方法相当于使用切点方法上的表达式 * 前置通知 * 等价于 @Before("execution(Integer com.carl.entity.User.add(..))") * 等价于 @Before(value = "execution(Integer com.carl.entity.User.add(..))") */ @Before(value = "pointcut()") public void before(JoinPoint joinPoint){ Object[] args = joinPoint.getArgs(); Signature signature = joinPoint.getSignature(); System.out.println("[before] [前置通知:"+ signature.getName()+"],参数为:"+ Arrays.asList(args)); } /** * 异常通知 */ @AfterThrowing(value = "pointcut()") public void afterThrowing(){ System.out.println("afterThrowing"); } /** * 后置通知 */ @AfterReturning("pointcut()") public void afterReturning(JoinPoint joinPoint){ System.out.println("afterReturning:后置通知:"); } /** * 最终通知 */ @After("pointcut()") public void after(JoinPoint joinPoint){ System.out.println("[after] [最终通知:"+ joinPoint.getSignature().getName()+"]"); } /** * 环绕通知 */ @Around("pointcut()") public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { System.out.println("around1:前环绕通知"); proceedingJoinPoint.proceed(); System.out.println("around2:后环绕通知"); } }
-
配置xml文件
添加aop的命名空间—红色部分<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
添加aspectJ自动代理对象
<aop:aspectj-autoproxy/>
6. AOP通知的执行顺序(重点)
Spring AOP遵循AspectJ的优先规则来确定通知的执行顺序。
在连接点开始时执行的通知,最高优先级的通知会先执行,即给定的两个前置通知中,优先级高的那个会先执行
在连接点结束时执行的通知,最高优先级的通知会后执行,即给定的两个后置通知中,优先级高的那个会最后执行
当定义在相同切面的多个通知,需要在同一个相同连接点中运行,执行的顺序是未知的(因为没有方法通过反射javac编译的类来获取声明的顺序)。不过可以考虑在每个切面类中按连接点压缩这些通知方法到一个通知方法,或者重构通知的片段到各自的切面类中–因为可以在切面类上做排序操作
当定义在不同切面的多个通知,需要在同一个相同连接点中运行,执行的顺序是未知的,除非你实现org.springframework.core.Ordered 接口或者使用@Order注解(@Order(数值类型))进行优先级排序,多个切面通过Orderd.getValue方法返回值(或@Order注解值)较低的那个会拥有更高的优先级
@Order(1)