目录
面向切面编程AOP
AOP的自我介绍
AOP的七大术语
▎通过代码理解术语
▎通过图加深理解术语
切点表达式
使用Spring的AOP
▎什么是AspectJ?
代码实操:AOP基于注解形式
@Aspect的五种通知类型
▎触发异常通知类
切面顺序
@Pointcut通用切点
▎跨类引用通用切点
@JoinPoint 连接点
▎测试信息
JoinPoint 和 ProceedingJoinPoint 区别
AOP基于xml形式回顾
AOP的实际案例:事务处理
AOP的实际案例:安全日志
吐槽一下:如果疯狂星期四的全家桶能跟Spring全家桶一样多就好了!
工作了好多年,发现Spring体系的知识真是到死都学不完,好不容易理解了一个知识点,过一段时间又忘了,脑海里的知识体系虽然在缓慢增长和扩展,但随着时间线拉长,前期加深过的知识点,也是模模糊糊的记忆了.....
今天看到项目里用了AOP切面技术,那就浅浅的水一篇博文来弥补一下断更了大半年的我吧,断更的原因也是因为工作变动和最近压力很大(还有你们看完不点赞!呜呜呜!),杭漂四年,一无所有,留下的只有一身伤疤,并非我脱不下孔乙己的长衫,是我没有长衫。
IOC使软件组件松耦合,AOP让你能够朴拙系统中经常使用的功能,把它转换为组件。
AOP (Aspect Oriented Programming): 面向切面编程,面向方面编程。(AOP是一种编程技术);是对OOP的补充延伸(面向对象编程)。
底层用的是动态代理来实现的
Spring的AOP使用的动态代理是:JDK动态代理 + CGLIB动态代理技术,Spring在这两种动态代理中灵活切换,如果是代理接口,会默认使用JDK动态代理,如果要代理某个类,这个类没有实现接口,就会切换使用CGLIB。当然也可以强制通过配置让Spring只使用CGLIB。
一个系统当中会存在一些系统服务,如:日志、事务管理、安全等。这些服务被称为:交叉业务
这些交叉业务几乎是通用的。如果在每一个业务处理过程中,都掺杂着写交叉业务代码进去,存在两方面问题:
使用AOP可以很轻松的解决以上问题,如下图所示:
▎AOP的优点:
初看这么多术语,一下子都不好接受,慢慢来,很快就会搞懂。
① 连接点JoinPoint:在程序的整个执行流程中,可以织入切面的位置。方法的执行前后,异常抛出之后等位置
② 切点Pointcut:在程序执行流程中,真正织入切面的位置(一个切点对应多个连接点)
③ 通知Advice:通知又叫增强,就是具体你要织入的代码。
④ 切面Aspect:切点+通知 就是切面
⑤ 织入Weaving:把通知应用到目标对象的过程
⑥ 代理对象Proxy:一个目标对象被织入通知后产生的新对象
⑦ 代理对象Target:被织入通知的对象
/**
* @Author wpf
* @Date 2023/3/16 16:29
* @Desc 用户业务
*/
public class UserService {
public void do1(){
System.out.println("do1");
}
public void do2(){
System.out.println("do2");
}
public void do3(){
System.out.println("do3");
}
public void do4(){
System.out.println("do4");
}
// 核心业务方法
public void service(){
try {
// JoinPoint连接点
do1(); // PointCut 切点
// JoinPoint连接点
do2(); // PointCut 切点
// JoinPoint连接点
do4(); // PointCut 切点
// JoinPoint连接点
}catch (Exception e){
// JoinPoint连接点
}
}
/**
1. 连接点(JoinPoint):描述的是位置
2. 切点(PointCut):本质上就是方法(真正织入切面的那个方法叫切点)
3. 通知(Advice):描述的是具体代码,通知又叫做增强。就是具体增强的那个代码。例如具体的事务代码、日志代码等
4. 切面 = 切点+通知
*/
}
切点表达式用来定义通知(Advice)往哪些方法上切入。
切入点表达式语法格式(中括号的都是可选项,不带中括号的都是必填项)
execution([访问控制权限修饰符] 返回值类型 [全限定类名]方法名(形式参数列表)[异常])
① 访问控制权限修饰符:
② 返回值类型
③ 全限定类名
④ 方法名
⑤ 形式参数列表
⑥ 异常
理解以下的切点表达式:
1. service包下的所有的类中以delete开始的代码
execution(public * com.service.*.delete*(..))
2. mail包下所有的类所有的方法
execution(* com.mail..*(..))
3. 所有类的所有方法
execution(* *(..))
Spring对AOP的实现包括以下两种方式
Eclipse组织的一个支持AOP的框架,AOP框架是独立于Spring框架之外的一个框架,Spring框架用了AspectJ
AspectJ项目源于帕洛阿尔托(Palo Alto)研究中心(缩写为PARC),该中心由Xerox集团资助,Gregor Kiczales领导,从1997年开始致力于AspectJ的开发,1998年第一次发布给外部用户,2001年发布1.0.release,为了推动AspectJ技术和社团的发展,PARC在2003年4月正式将AspectJ项目移交给了Eclipse组织,因为AspectJ的发展和受关注程度大大超出了PARC的预期,他们已经无力继续维持它的发展。
不敲代码,光bibi,是流氓行为
1.打开idea,新建一个空项目
2. 在该空项目下,新建一个module
3. 引入spring的相关依赖
org.springframework
spring-context
5.2.8.RELEASE
org.springframework
spring-aspects
5.2.8.RELEASE
junit
junit
RELEASE
test
4. 配置spring组件扫描和定义动态代理类型
有两种方式,一种基于配置类方式,另一种就是傻瓜式自己写spring配置文件
① 基于配置类方式
package com.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
/**
* @Author wpf
* @Date 2023/3/16 15:30
* @Desc spring配置类
*/
@Configuration
@ComponentScan("com.service") // 扫描这个路径
@EnableAspectJAutoProxy(proxyTargetClass = true) //强制使用Cglib动态代理
public class SpringConfig {
}
② 基于spring配置文件:在resources资源目录下新建一个spring.xml配置文件,如下
5. 新建一个UserService类,由于上面指定了底层使用cglib,因此我们要创建的是类而不是接口
package com.service;
import org.springframework.stereotype.Service;
/**
* @Author wpf
* @Date 2023/3/15 10:25
* @Desc 目标类,也就是被增强的对象
* 注意:UserService是一个类,不是接口,表示底层会使用cglib动态代理,不会用jdk
*/
@Service("UserService")
public class UserService {
// 目标方法
public void login(){
System.out.println("系统正在进行身份验证...");
}
}
6. 然后我们就可以创建一个切面类了,如下,记得该类需要被spring容器识别,需要加上@Component注解,如果是注解形式,所有的切面类同样也需要加上@Aspect注解
package com.service;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
/**
* @Author wpf
* @Date 2023/3/15 10:26
* @Desc TODO
*/
@Component("LogAspect")
@Aspect // 切面类是需要使用@Aspect注解进行标注的
public class LogAspect { //切面
// 切面 = 通知 + 切点
// Advice通知就是增强,就是具体的要编写的增强代码
// @Before(切点表达式) 注解标志的方法就是一个前置通知。
// execution(修饰符 返回值类型 全限定类名 方法名(形式参数列表))
@Before("execution(* com.service.UserService.*(..))")
public void beforeAdvice(){
System.out.println("我是一个通知,我是一段增强代码...");
}
/**
execution(* com.service.UserService.*(..) 的意思:
修饰符忽略
返回值类型为*,也就是任意类型
全限定类名 com.service.UserService
.* 该UserService下的所有方法
(..) 形式参数列表为任意
*/
}
7. 最后编写一个测试类,测试效果
执行结果:
写在前面:关于五种通知执行顺序问题,我这里采用的是5.2.7之后的版本,5.2.7之前的版本,其执行顺序不一样,具体就不在这里讨论了,使用前建议测试一下版本不同的区别!
在实际使用中如果需要考虑不同类型通知的执行顺序,一定要弄清楚采用Spring的版本!
环绕通知:
包围一个连接点(JoinPoint)的通知,如方法调用。这是最强大的一种通知类型。 环绕通知可以在方法调用前后完成自定义的行为。它也会选择是否继续执行连接点或直接返回它们自己的返回值或抛出异常来结束执行。
我们通过以下案例来查看下几种通知的执行顺序
1. 新建一个 OrderService 目标类,如下
package com.service;
import org.springframework.stereotype.Service;
// 目标类
@Service("orderService")
public class OrderService {
// 目标方法
public void generate(){
System.out.println("系统正在生成订单...");
}
}
2. 再新建 LogAllAspect 切面类展示五种通知,这里先不展示异常通知,如下
package com.service;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
/**
* @Author wpf
* @Date 2023/3/15 11:41
* @Desc 日志切面:检测5种通知的执行顺序
*/
@Component("logAllAspect")
@Aspect // 切面类是需要使用@Aspect注解进行标注的
public class LogAllAspect {
// 1.前置通知:在目标方法执行之前的通知
@Before("execution(* com.service..*(..))")
public void beforeAdvice(){
System.out.println("@Before前置通知");
}
// 2.后置/最终通知:在方法执行之后执行(不论是正常返回还是异常退出)
@After("execution(* com.service..*(..))")
public void afterAdvice(){
System.out.println("@After后置/最终通知");
}
// 环绕通知特殊,需要增加ProceedingJoinPoint入参,表示目标方法
// 3.环绕通知:最大的通知,在前置通知之前,在后置通知之后
@Around("execution(* com.service..*(..))")
public void aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
// 前面的代码
System.out.println("@Around前环绕");
// 执行目标
joinPoint.proceed(); // proceed方法就是用于启动目标方法执行的
// 后面的代码
System.out.println("@Around后环绕");
}
// 4.后置返回通知:在目标方法正常返回结果之后执行
@AfterReturning("execution(* com.service..*(..))")
public void afterReturningAdvice(){
System.out.println("@AfterReturning后置返回通知");
}
// 5.异常通知:发生异常之后执行的通知
@AfterThrowing("execution(* com.service.OrderService.*(..))")
public void afterThrowingAdvice(){
System.out.println("AfterThrowing异常通知");
}
}
3. 执行结果如下:可以看出正常执行方法的结果,不会触发 @AfterThrowing 通知
在同一 @Aspect 类中定义的需要在同一连接点上运行的通知方法根据其通知类型按以下顺序分配优先级:从最高到最低优先级:@Around、@Before、@After、@AfterReturning、@AfterThrowing。
但是,请注意,@After 通知方法将在同一切面的任何 @AfterReturning 或 @AfterThrowing 通知方法之后有效地调用,遵循 AspectJ 对 @After 的“After finally advice”语义。 其中,@Around 通知包裹了其余四种通知方法。
接下来再增加一个执行异常看看执行效果,在OrderService里增加一个异常抛出,如下:
执行效果如下图:
从结果上可以看出,由于方法异常结束,因此@AfterReturning后置返回通知并没有触发,@Aroud后环绕也没有执行
假设我们有多个切面,那它的顺序是怎么排序的?比如我们有日志切面、事务切面、统计方法执行时长切面等等..
可以通过 @Order(排序号) 进行对切面类的执行顺序排序,数字越小,表示执行顺序越高
1.在原来 LogAllAspect 日志切面类基础上,新增加一个安全切面类 SecurityAspect ,如下:
package com.service;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
/**
* @Author wpf
* @Date 2023/3/15 17:41
* @Desc 安全切面
*/
@Component("securityAspect")
@Aspect
@Order(1) // 排序,数字越小,执行优先级越高
public class SecurityAspect {
// 前置通知:在目标方法执行之前的通知
@Before("execution(* com.service..*(..))")
public void beforeAdvice(){
System.out.println("@Before前置通知:安全。。。。。");
}
}
2.执行效果如下:
在使用基于Aspectj注解的Spring Aop时,我们可以把通过@Pointcut注解定义Pointcut,指定其表达式,然后在需要使用Pointcut表达式的时候直接指定这个Pointcut。
从上述代码可以看到,每个advice都定义了切点表达式execution,表达式都是重复的,可以通过@Pointcut()注解来定义一个通用切点,其他advice只需要引用这个通用切点,就能去除冗余代码
1. 例如我们在 LogAllAspect 日志切面类,通过@Pointcut注解定义一个通用切点,代码如下:
@Component("logAllAspect")
@Aspect
public class LogAllAspect {
// 定义通用切点的表达式
@Pointcut("execution(* com.service..*(..))")
public void generalPointcut(){
// 这个方法只是一个标记,方法名随意,方法体中不需要写任务代码
}
// 前置通知:在目标方法执行之前的通知
//@Before("execution(* com.service..*(..))")
@Before("generalPointcut()") // 引入通用切点
public void beforeAdvice(){
System.out.println("@Before前置通知");
}
// 后置/最终通知:在方法执行之后执行(不论是正常返回还是异常退出)
@After("generalPointcut()") // 引入通用切点
public void afterAdvice(){
System.out.println("@After后置/最终通知");
}
}
2. 运行结果:
➳ 特别说明:一旦定义了一个通用切点,其他通知都能够通过方法名去引入这个切点,从而达到精简代码的目的。在上述代码中,我们是在当前类中定义的通用切点(@Pointcut和advice都在同一个类),所以通过@Pointcut注解定义通用切点的方法名就能够直接引用。
引用其他类的通用切点,可以通过全限定类名.通用切点方法名来达到引用目的
@Component("securityAspect")
@Aspect
@Order(1)
public class SecurityAspect {
// 引入外部类的通用切点格式:全限定类名.方法名()
@Before("com.service.LogAllAspect.generalPointcut()")
public void beforeAdvice(){
System.out.println("@Before前置通知:安全。。。。。");
}
}
运行效果如下:
程序执行过程中明确的点叫连接点,简单的来说就是Java程序执行过程中的方法。这个点可以用来作为AOP切入点。
作用:JointPoint对象则包含了和切入相关的很多信息。比如切入点的对象,方法,属性等。我们可以通过反射的方式获取这些点的状态和信息,用于追踪tracing和记录logging应用信息。
方法名 | 功能 |
---|---|
Signature getSignature(); | 获取方法的签名,签名就是方法的信息,例如修饰符、返回类型、方法名等 |
Object[] getArgs(); | 获取传入目标方法的参数对象 |
Object getTarget(); | 获取目标对象,即被代理的对象 |
Object getThis(); | 获取代理对象 |
实际上我们在每个advice中都自带隐形的传入了一个JoinPoint连接点,这个接点在Spring容器调用这个方法时会自动传过来,如下代码:(可定义,也可不定义)
增强的目标类
// 目标类,也就是被增强的对象
@Service("orderService")
public class OrderService {
// 目标方法
public void generate(){
System.out.println("系统正在生成订单...");
}
}
测试类:
public class SpringAopTest {
// 动态的在程序运行阶段织入这段增强代码,你看不到任何cglib或者是jdk动态代理
@Test
public void testBefore(){
ApplicationContext application = new ClassPathXmlApplicationContext("spring.xml");
OrderService orderService = application.getBean("orderService", OrderService.class);
// 调用方法
orderService.generate();
}
}
切面类:
@Component("logAllAspect")
@Aspect
public class LogAllAspect {
// 前置通知:在目标方法执行之前的通知
@Before("execution(* com.service..*(..))")
public void beforeAdvice(JoinPoint joinPoint){
System.out.println("目标方法名为:" +joinPoint.getSignature().getName());
System.out.println("目标方法所属类的简单类名:" + joinPoint.getSignature().getDeclaringType().getSimpleName());
System.out.println("目标方法所属类的类名:" + joinPoint.getSignature().getDeclaringTypeName());
System.out.println("目标方法声明类型:" + Modifier.toString(joinPoint.getSignature().getModifiers()));
System.out.println("被代理的对象:" + joinPoint.getTarget());
System.out.println("@Before前置通知");
}
}
执行效果:
就像我和你,父与子的区别.... 阿巴阿巴阿巴...
说明一下,ProceedingJoinPoint 只用在@Around切面方法中
▸ JoinPoint 连接点对象封装了SpringAop中切面方法的信息,在切面方法中添加JoinPoint参数,就可以获取到封装了该方法信息的JoinPoint对象,上面已经说过了
▸ 而Proceedingjoinpoint 继承了 JoinPoint。是在JoinPoint的基础上暴露出 proceed 这个方法。proceed很重要,这个是aop代理链执行的方法。
环绕通知 = 前置 + 目标方法执行 + 后置通知 proceed方法就是用于启动目标方法执行的
为什么要暴露proceed方法?因为夏天到了,穿太多容易热。说错了,因为只有暴露proceed,才能支持 aop:around 这种切面(这也是环绕通知和其他通知类型的一个最大区别),决定是否走代理链还是走自己拦截的其他逻辑。建议看一下 JdkDynamicAopProxy的invoke方法,了解一下代理链的执行原理。
上面代码的aop配置基于注解形式,来回顾下spring的xml配置形式,这里直接贴代码了
1.依赖
org.springframework
spring-context
5.2.8.RELEASE
org.springframework
spring-aspects
5.2.8.RELEASE
junit
junit
RELEASE
test
2.service类
public class UserService {
// 目标方法
public void login(){
System.out.println("系统正在进行身份验证...");
}
}
3.切面类
public class TimerAspect {
public void aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("开始计时..");
// 前环绕
long begin = System.currentTimeMillis();
joinPoint.proceed();
// 后环绕
long end = System.currentTimeMillis();
System.out.println("耗时:"+(end-begin)+"毫秒");
}
}
4.Spring配置文件
5.测试类
public class SpringAopTest {
@Test
public void testBefore(){
ApplicationContext application = new ClassPathXmlApplicationContext("spring.xml");
UserService userService = application.getBean("userService", UserService.class);
userService.login();
}
}
6.测试类执行结果
项目中的事务控制是在所难免的。在一个业务流程中,需要多条DML语句共同完成,为了保证数据的安全,这多条DML要么同时成功,要么同时失败,这就需要添加事务了
例如,如下伪代码:
public class 业务类 {
public void 创建订单(){
try {
// 开启事务
commitTrasaction();
// 执行业务核心逻辑
step1();
step2();
...
// 提交事务
commitTrasaction();
}catch (Exception e){
// 回滚事务
rollbackTrasaction();
}
}
...
}
这个控制事务的代码就是和业务逻辑没有关系的 “ 交叉业务 ”
以上伪代码中的这些交叉业务的代码没有得到复用,并且这些交叉业务代码如果要修改,必然要修改多处,难维护,可以采用AOP思想解决,将事务代码作为环绕通知,切入到目标类的方法中。
/**
* @Author wpf
* @Date 2023/3/16 16:22
* @Desc 编程式事务aop解决方式
*/
@Aspect
@Component
public class TransactionAspect {
@Around("execution(* com.service.AccountService.*(..))")
public void aroundAdvice(ProceedingJoinPoint joinPoint){
// 前环绕
System.out.println("start:开始事务");
try {
// 执行目标
joinPoint.proceed();
// 后环绕
System.out.println("commit:提交事务");
} catch (Throwable e) {
System.out.println("rollback:回滚事务");
throw new RuntimeException(e);
}
}
}
项目开发结束,已经上线,客户提出了新需求:凡事在系统中进行修改、删除、新增操作的,都要把这个人记录下来,因为这几个操作属于危险行为。
例如有业务类和业务方法:
/**
* @Author wpf
* @Date 2023/3/16 16:29
* @Desc 用户业务
*/
public class UserService {
public void getUser(){
System.out.println("获取用户信息");
}
public void saveUser(){
System.out.println("保存用户信息");
}
public void updateUser(){
System.out.println("修改用户信息");
}
public void deleteUser(){
System.out.println("删除用户信息");
}
}
AOP切面伪代码:
/**
* @Author wpf
* @Date 2023/3/16 16:30
* @Desc 安全日志切面
*/
@Aspect
@Component
public class SecurityLogAspect {
// save开头的方法
@Pointcut("execution(* com.log..save*(..))")
public void savePointcut(){}
@Pointcut("execution(* com.log..delete*(..))")
public void deletePointcut(){}
@Pointcut("execution(* com.log..update*(..))")
public void updatePointcut(){}
// 前置通知,操作保存、删除、修改记录日志信息
@Before("savePointcut() || deletePointcut() || updatePointcut()")
public void beforeAdvice(JoinPoint point){
// 类名
String className = point.getSignature().getDeclaringTypeName();
// 方法名
String methodName = point.getSignature().getName();
// 输出日志信息
System.out.println(new Date()+"梅花十三"+className+"."+methodName);
}
}