# 前言 🚀本章代码示例
# 关于 OOP 的概念
OOP
是面向对象的程序设计,在谈到OOP
程序设计前,我们不得不了解一下POP
即面向过程的程序设计,POP
它是以功能为中心进行思考和组织的一种编程方式,强调的是系统数据被加工处理的过程,而OOP
则注重封装,强调整体性的概念,以对象为中心,将对象内部组织与外部环境区分开来。
至于
OOP
的出现对POP
存在很多颠覆性的,不能说POP
没有了价值,只是不同时代的产物,从方法论来讲,更喜欢将面向对象与面向过程看做是事物的两个方面,那就是局部与整体 (必须注意局部与整体是相对的),在实际应用中,两者方法都同样重要。
在
Java
程序设计过程中,我们享受到了OOP
设计思想带来的甜头,以至于在这个一切皆对象众生平等的世界里欢乐不已,而OOP
确实也遵循自身的宗旨,即将数据及数据的操作行为放在一起相互依存,是不可分割的整体,利用该定义对于相同类型的对象进行分类并抽象后得出共同的特性,从而行成了类,在Java
程序设计中这些类对象就是class
,它使程序设计更简单,更易于维护。但是随着应用程序规模的增加,慢慢地OOP
程序设计也开始暴露出了一些问题。
# 关于 AOP 的概念
# Aop 领跑者 - AspectJ
在进行下方测试时请务必先在
IntelliJ IDEA
中配置AspectJ
,不然将无法进行后续操作,AspectJ 详细配置🚀
先看下方案例,首先编写一个
HelloWord
类,然后通过AspectJ
的技术切入该类并执行相应操作。
package top.rem.rain.aspect; | |
/** | |
* @Author: LightRain | |
* @Description: 使用 Aspect 进行事务围绕 | |
* @DateTime: 2024-01-04 00:34 | |
* @Version:1.0 | |
**/ | |
public class HelloWord { | |
private void helloWord(){ | |
System.out.println("Hello Word!"); | |
} | |
public static void main(String[] args) { | |
HelloWord helloWord = new HelloWord(); | |
helloWord.helloWord(); | |
/* | |
执行结果: | |
helloWord () 方法执行前验证权限 | |
Hello Word! | |
helloWord () 方法执行后记录日志 | |
*/ | |
} | |
} |
下面将来编写
AspectJ
类,注意关键字aspect
, 而不是class
,其后缀名称为.aj
代表它是属于AspectJ
类,含义与class
相同,即定义一个AspectJ
类。
package top.rem.rain.aspect; | |
public aspect HelloWordAspect { | |
/* | |
定义切点,日志记录切点 | |
*/ | |
pointcut recordLog():call(* HelloWord.helloWord(..)); | |
/* | |
定义切点,权限验证 (实际开发中日志和权限一般会放在不同的切面中,这里仅为方便演示) | |
*/ | |
pointcut authCheck():call(* HelloWord.helloWord(..)); | |
/* | |
定义前置通知! | |
*/ | |
before():authCheck(){ | |
System.out.println("helloWord()方法执行前验证权限"); | |
} | |
/* | |
定义后置通知 | |
*/ | |
after():recordLog(){ | |
System.out.println("helloWord()方法执行后记录日志"); | |
} | |
} |
下面是运行
main
方法后的执行结果:
helloWord () 方法执行前验证权限 | |
Hello Word! | |
helloWord () 方法执行后记录日志 |
执行结果是意料之中的,我们可以发现明明只运行了
main
方法,却在helloWord()
方法前后分别进行了权限验证和日志记录,这就是AspectJ
的功劳了,AspectJ
是一个AOP
框架,它能对Java
代码进行编译,让Java
代码具有AspectJ
的AOP
功能,AspectJ
是目前实现AOP
最成熟的框架,AspectJ
与Java
程序完全兼容。
使用
aspect
关键字来定义一个切面类,它可以是单独的日志切面、权限切面等,在切面内部使用pointcut
定义了两个切点,一个用于权限验证,一个用于日志记录,而所谓的切点就是那些需要应用切面的方法,如:需要在helloWord()
方法执行前后进行权限验证和日志记录,那么就需要捕捉该方法,而pointcut
就是定义这些需要捕捉的方法,这些方法也被称为目标方法,最后定义两个通知,通知就是那些需要在目标方法前后执行的函数,如before()
即前置通知,会在目标方法之前执行,after()
是后置通知,会在目标方法执行之后再执行此方法。
切点定义语法:使用关键字
pointcut
定义切点,后面跟函数名称,最后编写匹配方式,函数一般使用call()
或execution()
进行匹配。
pointcut 函数名称:匹配表达式 |
在上述示例中的
recordLog()
是自定义函数名称,其中*
表示任意返回值,紧接着就是需要拦截目标函数了,helloWord(..)
表示任意参数类型。
pointcut recordLog():call(* HelloWord.helloWord(..)); |
- 关于通知定义语法有以下五种方式:
before()
:在目标方法执行前,执行前置通知。after()
:在目标方法执行后,执行后置通知。after() returning
:在目标方法返回时,执行后置返回通知。after() throwing
:在目标方法抛出异常时,执行异常通知。around()
:在目标方法执行中执行,可控制目标方法是否执行的环绕通知,即目标方法的前后。
[返回值类型] 通知函数名称 (参数)[returning/throwing]: 连接点函数 (切点函数){} | |
如:after () returning:recordLog (){} |
示例代码如下:注意其中
around
通知即环绕通知,可以通过proceed()
方法来控制目标函数是否执行。
package top.rem.rain.aspect; | |
public aspect HelloWordAspect { | |
/* | |
定义前置通知 | |
before (参数): 连接点函数 { | |
函数体 | |
} | |
*/ | |
before():authCheck(){ | |
System.out.println("sayHello方法执行前验证权限"); | |
} | |
/* | |
定义后置通知 | |
after (参数): 连接点函数 { | |
函数体 | |
} | |
*/ | |
after():recordLog(){ | |
System.out.println("sayHello方法执行后记录日志"); | |
} | |
/* | |
定义后置通知带返回值 | |
after (参数) returning (返回值类型): 连接点函数 { | |
函数体 | |
} | |
*/ | |
after()returning(int x): get(){ | |
System.out.println("返回值为:"+x); | |
} | |
/* | |
异常通知 | |
after (参数) throwing (返回值类型): 连接点函数 { | |
函数体 | |
} | |
*/ | |
after() throwing(Exception e):helloWord(){ | |
System.out.println("抛出异常:"+e.toString()); | |
} | |
/* | |
环绕通知 可通过 proceed () 控制目标函数是否执行 | |
Object around (参数): 连接点函数 { | |
函数体 | |
Object result=proceed ();// 执行目标函数 | |
return result; | |
} | |
*/ | |
Object around():aroundAdvice(){ | |
System.out.println("sayAround 执行前执行"); | |
Object result=proceed();// 执行目标函数 | |
System.out.println("sayAround 执行后执行"); | |
return result; | |
} | |
} |
现在已经有了对
pointcut(切入点)
和advice(通知)
的概念,而切面则是调用切入点和通知的组合如上述使用的aspect
关键字定义的HelloWordAspect
类,把切面应用到目标函数的过程称为weaving(织入)
。
public class UserServiceImpl{ | |
public void addUser(){} | |
public void deleteUser(){} | |
} | |
/* | |
joinPoint (连接点):指的是哪些目标函数可以被拦截如:addUser ()、deleteUser () | |
pointcut (切入点):指的是对 joinPoint 中哪些目标函数进行切入 | |
advice (通知):在某个特定的切入点上执行操作,如:日志、权限验证等具体要应用于切入点的代码 | |
weaving (织入):把切面的代码应用到目标函数的过程被称为织入 | |
Aspect (切面):由切点和通知结合而成,定义通知应用到哪些切入点上 | |
*/ |
# Aspect 织入原理
我们需要了解
AspectJ
是如何应用到Java
代码中的,其中这个过程被称为织入,对于这个过程分为动态织入和静态织入,动态织入是在运行期时动态将要增强的代码织入到目标类中,这样的操作都是通过动态代理来完成的,如:Proxy
、CGLIB
。
ApectJ
采用的是静态织入方式,它是在编译期间进行织入,在这个期间使用AspectJ
的ajc
编译器把aspect
类编译成class
字节码后,在Java
目标类编译时织入,即先编译aspect
类再编译目标类。
ajc
编译器它是一种可以识别aspect
语法的编译器,采用Java
语言编写的,由于javac
编译器并不能识别aspect
语法,便有了ajc
编译器,ajc
编译器是可以编译Java
文件的。
为了更佳直观了解
aspect
织入方式,现在打开刚才已编译完成的HelloWord.class
文件,反编译后的代码如下:
// | |
// Source code recreated from a .class file by IntelliJ IDEA | |
// (powered by FernFlower decompiler) | |
// | |
package top.rem.rain.aspect; | |
public class HelloWord { | |
public HelloWord() { | |
} | |
private void helloWord() { | |
System.out.println("Hello Word!"); | |
} | |
public static void main(String[] args) { | |
HelloWord helloWord = new HelloWord(); | |
HelloWord var10000 = helloWord; | |
try { | |
// 切面类前置通知织入 | |
HelloWordAspect.aspectOf().ajc$before$top_rem_rain_HelloWordAspect$1$22c5541(); | |
// 目标函数的调用 | |
var10000.helloWord(); | |
} catch (Throwable var3) { | |
HelloWordAspect.aspectOf().ajc$after$top_rem_rain_HelloWordAspect$2$4d789574(); | |
throw var3; | |
} | |
// 切面类后置通知织入 | |
HelloWordAspect.aspectOf().ajc$after$top_rem_rain_HelloWordAspect$2$4d789574(); | |
} | |
} |
当然除了编译期织入,还存在链接期 (编译后) 织入,即:将
aspect
类和java
目标类同时编译成字节码文件后,再进行织入处理,这种方式有助于已编译好的第三方jar
和class
文件进行织入操作。
# SpringAop-AspectJ
SpringAOP
与AspectJ
目的是一致的,它们都是为了统一处理横切业务,但与AspectJ
不同的是SpringAOP
更注重的是与SpringIOC
容器结合,利用该优势来处理横切业务问题,因此在AOP
功能完善方面来讲AspectJ
更具有优势。
此时
Spring
注意到了AspectJ
在实现AOP
的方式上依赖于特殊编译器(ajc编译器)
,因此Spring
采用了动态代理技术来构建SpringAOP
内部机制,其SpringAOP(动态织入)
与AspectJ(静态织入)
最大的区别就是动态织入和静态织入。
在
AspectJ 1.5
之后引入了@Aspect
注解开发,Spring
也很快的跟进了这种方式,因此Spring 2.0
后便使用了与AspectJ
相同名称的注解,注意:Spring
只是使用了与AspectJ 5
相同的注解,并没有使用AspectJ
的编译器,其底层实现依旧是动态代理技术,Spring 官方描述。
# SpringAop 快速入门
首先在普通
Maven
项目中添加以下依赖。
<properties> | |
<spring.version>6.1.0</spring.version> | |
</properties> | |
<dependencies> | |
<!-- 添加 Aspectj 依赖 --> | |
<dependency> | |
<groupId>org.aspectj</groupId> | |
<artifactId>aspectjrt</artifactId> | |
<version>1.8.14</version> | |
</dependency> | |
<dependency> | |
<groupId>org.aspectj</groupId> | |
<artifactId>aspectjtools</artifactId> | |
<version>1.8.14</version> | |
</dependency> | |
<!-- 添加 Spring 依赖 --> | |
<dependency> | |
<groupId>org.springframework</groupId> | |
<artifactId>spring-context</artifactId> | |
<version>${spring.version}</version> | |
</dependency> | |
<dependency> | |
<groupId>org.springframework</groupId> | |
<artifactId>spring-aop</artifactId> | |
<version>${spring.version}</version> | |
</dependency> | |
<!-- 添加 junit 单元测试 --> | |
<dependency> | |
<groupId>junit</groupId> | |
<artifactId>junit</artifactId> | |
<version>4.13.2</version> | |
<scope>compile</scope> | |
</dependency> | |
<!-- 添加 Spring 测试 --> | |
<dependency> | |
<groupId>org.springframework</groupId> | |
<artifactId>spring-test</artifactId> | |
<version>6.0.6</version> | |
</dependency> | |
</dependencies> |
定义
UserDao
接口。
package top.rem.rain.spring.aop; | |
/** | |
* @author LightRain | |
*/ | |
public interface UserDao { | |
/** | |
* 添加用户 | |
*/ | |
String addUser(); | |
/** | |
* 删除用户 | |
*/ | |
void deleteUser(); | |
} |
定义
UserDao
接口的实现类。
package top.rem.rain.spring.aop; | |
import org.springframework.stereotype.Repository; | |
/** | |
* @Author: LightRain | |
* @Description: 实现类 | |
* @DateTime: 2024-01-04 21:17 | |
* @Version:1.0 | |
**/ | |
@Repository | |
public class UserDaoImpl implements UserDao{ | |
/** | |
* 添加用户 | |
* | |
* @return | |
*/ | |
@Override | |
public String addUser() { | |
System.out.println("添加了一条用户记录"); | |
return "成功添加一条用户记录"; | |
} | |
/** | |
* 删除用户 | |
*/ | |
@Override | |
public void deleteUser() { | |
System.out.println("删除了一条用户记录"); | |
} | |
} |
定义
SpringAop
的aspect
类
package top.rem.rain.spring.aop; | |
import org.aspectj.lang.ProceedingJoinPoint; | |
import org.aspectj.lang.annotation.*; | |
import org.springframework.context.annotation.EnableAspectJAutoProxy; | |
/** | |
* @Author: LightRain | |
* @Description: SpringAop 切面类 | |
* @DateTime: 2024-01-04 21:28 | |
* @Version:1.0 | |
**/ | |
@Aspect // 使用 AspectJ 的 @Aspect 注解来表示此类是一个切面类 | |
@EnableAspectJAutoProxy // 使用 Spring 启用 AspectJ 自动代理 | |
public class UserDaoAspect { | |
/** | |
* 前置通知 | |
*/ | |
@Before("execution(* top.rem.rain.spring.aop.UserDao.addUser(..))") | |
public void before() { | |
System.out.println("前置通知...."); | |
} | |
/** | |
* 后置通知 | |
* returnVal, 切点方法执行后的返回值 | |
*/ | |
@AfterReturning(value = "execution(* top.rem.rain.spring.aop.UserDao.addUser(..))", returning = "returnVal") | |
public void afterReturning(Object returnVal) { | |
System.out.println("后置通知:" + returnVal); | |
} | |
/** | |
* 环绕通知 | |
* | |
* @param joinPoint 可用于执行切点的类 | |
* @return | |
* @throws Throwable | |
*/ | |
@Around("execution(* top.rem.rain.spring.aop.UserDao.addUser(..))") | |
public Object around(ProceedingJoinPoint joinPoint) throws Throwable { | |
System.out.println("环绕通知前...."); | |
Object obj = (Object) joinPoint.proceed(); | |
System.out.println("环绕通知后...."); | |
return obj; | |
} | |
/** | |
* 抛出通知 | |
* | |
* @param e | |
*/ | |
@AfterThrowing(value = "execution(* top.rem.rain.spring.aop.UserDao.addUser(..))", throwing = "e") | |
public void afterThrowable(Throwable e) { | |
System.out.println("出现异常:msg=" + e.getMessage()); | |
} | |
/** | |
* 无论在什么情况下都会执行的方法 | |
*/ | |
@After(value = "execution(* top.rem.rain.spring.aop.UserDao.addUser(..))") | |
public void after() { | |
System.out.println("最终通知...."); | |
} | |
} |
编写配置文件并交由
SpringIOC
来管理。
<?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:aop="http://www.springframework.org/schema/p" | |
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> | |
<!-- 定义目标对象 --> | |
<bean id="userDao" class="top.rem.rain.spring.aop.UserDaoImpl" /> | |
<!-- 定义 aspect 类 --> | |
<bean name="userDaoAspect" class="top.rem.rain.spring.aop.UserDaoAspect"/> | |
</beans> |
定义一个测试类来测试是否可以正确切入,测试类代码如下:
package top.rem.rain.spring.aop; | |
import org.junit.Test; | |
import org.junit.runner.RunWith; | |
import org.springframework.beans.factory.annotation.Autowired; | |
import org.springframework.test.context.ContextConfiguration; | |
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; | |
/** | |
* @Author: LightRain | |
* @Description: 测试类 | |
* @DateTime: 2024-01-04 21:54 | |
* @Version:1.0 | |
**/ | |
@RunWith(SpringJUnit4ClassRunner.class) | |
@ContextConfiguration(locations= "classpath:spring-aop-aspectj.xml") | |
public class UserDaoTest { | |
@Autowired | |
private UserDao userDao; | |
@Test | |
public void testUserDao(){ | |
userDao.addUser(); | |
/* | |
执行结果: | |
环绕通知前.... | |
前置通知.... | |
添加了一条用户记录 | |
后置通知:成功添加一条用户记录 | |
最终通知.... | |
环绕通知后.... | |
*/ | |
} | |
} |
在此说明一下
UserDaoAspect
类中的定义,在该类中编写了五种注解类型的通知方法,分别是@Before(前置通知)
、@AfterReturning(后置通知)
、@Around(环绕通知)
、@AfterThrowing(异常通知)
、@After(最终通知)
,这五种通知与AspectJ
通知类型几乎是一样的,并在注解通知上使用execution
关键字来定义切点表达式,即指明该通知要应用到哪个目标方法,当只有一个execution
参数时value
属性可以省略,当含有两个或以上参数时value
属性就不可以省略了。
当然除了把切点表达式直接传递给注解通知类型外,还可以使用
@pointcut
来定义切点表达式,这个与AspectJ
使用的关键字pointcut
是一样的,在将目标类和aspect
类定义完成后,还需在XML
配置文件中进行配置并将它们交由SpringIOC
来管理。
# 关于 SpringAop 术语
SpringAop
实现是遵循的AOP
规范,特别是以AspectJ
为参考可以与Java
无缝整合,因此在AOP
术语的概念上与AspectJ
的AOP
术语是一样的。
如:
pointcut(切点)
定义需要应用通知的目标方法,通知则是那些需要应用到目标方法而编写的方法体,Aspect(切面)
则是通知与切点的结合,weaving(织入)
则是将aspect
类应用到目标方法的过程,只不过SpringAop
底层是通过动态代理技术来实现的。
# SpringAop - 注解开发
# 定义切点方法
在上述案例中,定义过滤切点方法时,是直接把
execution
已定义匹配表达式作为值传递给通知类型的。
public class UserDaoAspect { | |
@After(value = "execution(* top.rem.rain.spring.aop.UserDao.addUser(..))") | |
public void after() { | |
System.out.println("最终通知...."); | |
} | |
} |
在除了上述方式外,还可以采用与
AspectJ
中使用pointcut
关键字类似的定义切点表达式,使用@Pointcut
注解,代码如下:
public class UserDaoAspect { | |
/** | |
* 使用 @Pointcut 注解定义切点 | |
*/ | |
@Pointcut("execution(* top.rem.rain.spring.aop.UserDao.addUser(..))") | |
private void customPointcut(){} | |
/** | |
* 应用切入点方法 | |
*/ | |
@After(value="customPointcut()") | |
public void afterDemo(){ | |
System.out.println("最终通知...."); | |
} | |
} |
使用
@Pointcut
注解进行定义并应用到通知方法afterDemo
时直接传递切点表达式的方法名称即可。
# 切入点指示符
为了使方法通知应用到相应过滤的目标方法上,
SpringAop
提供了匹配表达式,这些表达式也被称为切入点指示符,Spring 官方文档。
# 方法签名表达式
如果想要根据方法签名进行过滤使用关键字
execution
即可,语法表达式如下:
//scope:方法作用域,如 public,private,protect | |
//returnt-type:方法返回值类型 | |
//fully-qualified-class-name:表方法所在类的完全限定名称 | |
//parameters:方法参数 | |
execution (<scope> <return-type> <fully-qualified-class-name>.*(parameters)) |
对于给定的作用域、返回值类型、完全限定名以及参数匹配的方法将会应用切点函数指定的通知,示例如下:
// 匹配 UserDaoImpl 类中的所有方法 | |
@Pointcut("execution(* top.rem.rain.spring.aop.UserDaoImpl.*(..))") | |
// 匹配 UserDaoImpl 类中的所有公共方法 | |
@Pointcut("execution(public * top.rem.rain.spring.aop.UserDaoImpl.*(..))") | |
// 匹配 UserDaoImpl 类中的所有公共方法并且返回值为 String 类型 | |
@Pointcut("execution(public String top.rem.rain.spring.aop.UserDaoImpl.*(..))") | |
// 匹配 UserDaoImpl 类中第一个参数为 int 类型的所有公共方法 | |
@Pointcut("execution(public * top.rem.rain.spring.aop.UserDaoImpl.*(String, ..))") |
# 类型签名表达式
为了方便过滤类型方法如:包名、类名、接口,
SpringAop
提供了within
关键字,语法表达式如下:
within(<type name>) |
示例如下:
// 匹配 top.rem.rain.spring.aop.dao 包及其子包中所有类中的所有方法 | |
@Pointcut("within(top.rem.rain.spring.aop.dao..*)") | |
// 匹配 UserDaoImpl 类中所有方法 | |
@Pointcut("within(top.rem.rain.spring.aop.UserDaoImpl)") | |
// 匹配 UserDaoImpl 类及其子类中所有方法 | |
@Pointcut("within(top.rem.rain.spring.aop.UserDaoImpl+)") | |
// 匹配所有实现 UserDao 接口类的所有方法 | |
@Pointcut("within(top.rem.rain.spring.aop.UserDao+)") |
# 通配符
当在定义匹配表达式是通配符可以说是随处可见的,如
*
、..
、+
它们代表的含义如下:*
:匹配任意数量的字符。
// 匹配 top.rem.rain.spring.aop.service 包及其子包中所有类的所有方法
within(top.rem.rain.spring.aop.service..*)
// 匹配以 set 开头,参数为 int 类型,任意返回值的方法
execution(* set*(int))
+
:匹配给定类的任意子类。
// 匹配实现了 DaoUser 接口的所有子类的方法
within(top.rem.rain.spring.aop.dao.DaoUser+)
..
:匹配方法定义中的任意数量的参数,此外还可以匹配类定义中的任意数量包。
// 任意返回值,任意名称,任意参数的公共方法
execution(public * *(..))
// 匹配 top.rem.rain.spring.aop.dao 包及其子包中所有类中的所有方法
within(top.rem.rain.spring.aop.dao..*)
# 其它标识符
bean
:SpringAop
扩展的,在AspectJ
中没有对应的标识符,它用于匹配特定名称的bean
对象的执行方法。
// 匹配名称中带有后缀 Service 的 Bean。 | |
@Pointcut("bean(*Service)") | |
private void customPointcut1(){} |
this
:它用于匹配当前AOP
代理对象类型的执行方法,注意:是AOP
代理对象的类型匹配。
// 匹配任意实现 UserDao 接口的代理对象的方法进行过滤 | |
@Pointcut("this(top.rem.rain.spring.aop.UserDao)") | |
private void customPointcut2(){} |
target
:它用于匹配当前目标对象类型的执行方法。
// 匹配任意实现 UserDao 接口的目标对象的方法进行过滤 | |
@Pointcut("target(top.rem.rain.spring.aop.UserDao)") | |
private void customPointcut3(){} |
@within
:它用于匹配所持有指定注解类型内的方法,注意:它与within
是有区别的,within
是用于匹配指定类型内的方法。
// 匹配使用 Transactional 注解的类 | |
@Pointcut("@within(org.springframework.transaction.annotation.Transactional)") | |
private void customPointcut4(){} |
@annotation
:根据所应用的注解进行方法过滤。
// 匹配使用 Transactional 注解的方法 | |
@Pointcut("@annotation(org.springframework.transaction.annotation.Transactional)") | |
private void customPointcut5(){} |
# 五种通知类型
通知类型主要分为五种,在
Spring
与AspectJ
中一样,分别是@Before(前置通知)
、@AfterReturning(后置通知)
、@Around(环绕通知)
、@AfterThrowing(异常通知)
、@After(最终通知)
。
# @Before (前置通知)
前置通知通过使用
@Before
注解进行标注,可直接传入切点表达式的值,该通知在目标方法执行前执行,通过JoinPoint
参数可以获取,目标对象的信息,如:类名称、方法名称、方法参数等,该参数可选。
package top.rem.rain.spring.aop; | |
import org.aspectj.lang.JoinPoint; | |
import org.aspectj.lang.ProceedingJoinPoint; | |
import org.aspectj.lang.annotation.*; | |
import org.springframework.context.annotation.EnableAspectJAutoProxy; | |
/** | |
* @Author: LightRain | |
* @Description: SpringAop 切面类 | |
* @DateTime: 2024-01-04 21:28 | |
* @Version:1.0 | |
**/ | |
@Aspect // 使用 AspectJ 的 @Aspect 注解来表示此类是一个切面类 | |
@EnableAspectJAutoProxy // 使用 Spring 启用 AspectJ 自动代理 | |
public class UserDaoAspect { | |
/** | |
* 前置通知 | |
* @param joinPoint 该参数可以获取目标对象的信息,如类名称,方法参数,方法名称等 | |
*/ | |
@Before("execution(* top.rem.rain.spring.aop.UserDao.addUser(..))") | |
public void before(JoinPoint joinPoint) { | |
System.out.println("前置通知...."); | |
} | |
} |
# @AfterReturning (后置通知)
后置通知通过使用
@AfterReturning
注解进行标注,该通知在目标方法执行完成后执行,可以获取到目标方法最终返回值returnVal
, 当目标方法没有返回值时returnVal
将返回null
, 注意:必须通过returning = "returnVal"
注明参数并且必须与通知方法参数名称相同,该参数可选。
package top.rem.rain.spring.aop; | |
import org.aspectj.lang.JoinPoint; | |
import org.aspectj.lang.ProceedingJoinPoint; | |
import org.aspectj.lang.annotation.*; | |
import org.springframework.context.annotation.EnableAspectJAutoProxy; | |
/** | |
* @Author: LightRain | |
* @Description: SpringAop 切面类 | |
* @DateTime: 2024-01-04 21:28 | |
* @Version:1.0 | |
**/ | |
@Aspect // 使用 AspectJ 的 @Aspect 注解来表示此类是一个切面类 | |
@EnableAspectJAutoProxy // 使用 Spring 启用 AspectJ 自动代理 | |
public class UserDaoAspect { | |
/** | |
* 后置通知 | |
* returnVal, 切点方法执行后的返回值 | |
*/ | |
@AfterReturning(value = "execution(* top.rem.rain.spring.aop.UserDao.addUser(..))", returning = "returnVal") | |
public void afterReturning(Object returnVal) { | |
System.out.println("后置通知:" + returnVal); | |
} | |
} |
# @Around (环绕通知)
环绕通知通过使用
@Around
注解进行标注,它可以在目标方法前执行也可以在目标方法后执行,更重要的是环绕通知可以控制方法是否执行,即使如此我们也应该尽量以最简单的方式满足需求,在仅需要在目标方法前执行时应该采用前置通知而并非环绕通知。
环绕通知的第一个参数必须为
ProceedingJoinPoint
, 通过该对象的proceed()
方法来执行目标方法,proceed()
的返回值就是环绕通知的返回值,ProceedingJoinPoint
它同样可以获取目标对象的信息,如:类名称、方法名称、方法参数等。
package top.rem.rain.spring.aop; | |
import org.aspectj.lang.JoinPoint; | |
import org.aspectj.lang.ProceedingJoinPoint; | |
import org.aspectj.lang.annotation.*; | |
import org.springframework.context.annotation.EnableAspectJAutoProxy; | |
/** | |
* @Author: LightRain | |
* @Description: SpringAop 切面类 | |
* @DateTime: 2024-01-04 21:28 | |
* @Version:1.0 | |
**/ | |
@Aspect // 使用 AspectJ 的 @Aspect 注解来表示此类是一个切面类 | |
@EnableAspectJAutoProxy // 使用 Spring 启用 AspectJ 自动代理 | |
public class UserDaoAspect { | |
/** | |
* 环绕通知 | |
* | |
* @param joinPoint 可用于执行切点的类 | |
* @return | |
* @throws Throwable | |
*/ | |
@Around("execution(* top.rem.rain.spring.aop.UserDao.addUser(..))") | |
public Object around(ProceedingJoinPoint joinPoint) throws Throwable { | |
System.out.println("环绕通知前...."); | |
Object obj = (Object) joinPoint.proceed(); | |
System.out.println("环绕通知后...."); | |
return obj; | |
} | |
} |
# @AfterThrowing (异常通知)
该通知只有在异常时才会被触发,并由
throwing
来声明一个接收异常信息的变量,同样异常通知也可用于JoinPoint
参数,需要时加上即可。
package top.rem.rain.spring.aop; | |
import org.aspectj.lang.JoinPoint; | |
import org.aspectj.lang.ProceedingJoinPoint; | |
import org.aspectj.lang.annotation.*; | |
import org.springframework.context.annotation.EnableAspectJAutoProxy; | |
/** | |
* @Author: LightRain | |
* @Description: SpringAop 切面类 | |
* @DateTime: 2024-01-04 21:28 | |
* @Version:1.0 | |
**/ | |
@Aspect // 使用 AspectJ 的 @Aspect 注解来表示此类是一个切面类 | |
@EnableAspectJAutoProxy // 使用 Spring 启用 AspectJ 自动代理 | |
public class UserDaoAspect { | |
/** | |
* 异常通知 | |
* | |
* @param e 抛出异常的信息 | |
*/ | |
@AfterThrowing(value = "execution(* top.rem.rain.spring.aop.UserDao.addUser(..))", throwing = "e") | |
public void afterThrowable(Throwable e) { | |
System.out.println("出现异常:msg=" + e.getMessage()); | |
} | |
} |
# @After (最终通知)
最终通知类似于
finally
代码块,只要应用了目标方法时不管在什么情况下都会执行。
package top.rem.rain.spring.aop; | |
import org.aspectj.lang.JoinPoint; | |
import org.aspectj.lang.ProceedingJoinPoint; | |
import org.aspectj.lang.annotation.*; | |
import org.springframework.context.annotation.EnableAspectJAutoProxy; | |
/** | |
* @Author: LightRain | |
* @Description: SpringAop 切面类 | |
* @DateTime: 2024-01-04 21:28 | |
* @Version:1.0 | |
**/ | |
@Aspect // 使用 AspectJ 的 @Aspect 注解来表示此类是一个切面类 | |
@EnableAspectJAutoProxy // 使用 Spring 启用 AspectJ 自动代理 | |
public class UserDaoAspect { | |
/** | |
* 无论在什么情况下都会执行的方法 | |
*/ | |
@After(value = "execution(* top.rem.rain.spring.aop.UserDao.addUser(..))") | |
public void after() { | |
System.out.println("最终通知...."); | |
} | |
} |
# 传递通知参数
在
SpringAop
中,除了execution
和bean
指示符不可以传递参数给通知方法,其它指示符都可以将匹配方法相应参数传递给通知方法,获取到匹配方法参数后通过argNames
属性指定参数名,注意args
指示符,argNames
参数名称必须与before()
方法中的参数名称保持一致。
package top.rem.rain.spring.aop; | |
import org.aspectj.lang.JoinPoint; | |
import org.aspectj.lang.ProceedingJoinPoint; | |
import org.aspectj.lang.annotation.*; | |
import org.springframework.context.annotation.EnableAspectJAutoProxy; | |
/** | |
* @Author: LightRain | |
* @Description: SpringAop 切面类 | |
* @DateTime: 2024-01-04 21:28 | |
* @Version:1.0 | |
**/ | |
@Aspect // 使用 AspectJ 的 @Aspect 注解来表示此类是一个切面类 | |
@EnableAspectJAutoProxy // 使用 Spring 启用 AspectJ 自动代理 | |
public class UserDaoAspect { | |
@Before(value="args(param)", argNames="param") // 明确指定了 | |
public void before(String param) { | |
System.out.println("param:" + param); | |
} | |
} |
当然也可以直接使用
args
指示符不带argNames
参数。
package top.rem.rain.spring.aop; | |
import org.aspectj.lang.JoinPoint; | |
import org.aspectj.lang.ProceedingJoinPoint; | |
import org.aspectj.lang.annotation.*; | |
import org.springframework.context.annotation.EnableAspectJAutoProxy; | |
/** | |
* @Author: LightRain | |
* @Description: SpringAop 切面类 | |
* @DateTime: 2024-01-04 21:28 | |
* @Version:1.0 | |
**/ | |
@Aspect // 使用 AspectJ 的 @Aspect 注解来表示此类是一个切面类 | |
@EnableAspectJAutoProxy // 使用 Spring 启用 AspectJ 自动代理 | |
public class UserDaoAspect { | |
@Before("execution(public * top.rem.rain.spring.aop..*.addUser(..)) && args(userId,..)") | |
public void before(int userId) { | |
// 当调用 addUser 方法时如果与 addUser 的参数匹配成功则会将参数值传递进来 | |
System.out.println("userId:" + userId); | |
} | |
} |
# 关于 Aspect 的优先级
在不同的切面中,如果有多个通知需要在同一个切点方法指定的过滤目标方法上执行,那么那些在目标方法前执行的通知属于最高优先级,将会优先执行,在执行目标方法后执行的通知将会在最后执行,而对于在同一个切面定义的通知方法会根据在类中声明的顺序执行。
package top.rem.rain.spring.aop; | |
import org.aspectj.lang.annotation.*; | |
import org.springframework.context.annotation.EnableAspectJAutoProxy; | |
/** | |
* @Author: LightRain | |
* @Description: SpringAop 切面类 | |
* @DateTime: 2024-01-04 21:28 | |
* @Version:1.0 | |
**/ | |
@Aspect // 使用 AspectJ 的 @Aspect 注解来表示此类是一个切面类 | |
@EnableAspectJAutoProxy // 使用 Spring 启用 AspectJ 自动代理 | |
public class UserDaoAspect2 { | |
/** | |
* Pointcut 定义切点函数 | |
*/ | |
@Pointcut("execution(* top.rem.rain.spring.aop.UserDao.deleteUser(..))") | |
private void customPointcut(){} | |
@Before("customPointcut()") | |
public void beforeOne(){ | |
System.out.println("前置通知....执行顺序1"); | |
} | |
@Before("customPointcut()") | |
public void beforeTwo(){ | |
System.out.println("前置通知....执行顺序2"); | |
} | |
@AfterReturning(value = "customPointcut()") | |
public void afterReturningThree(){ | |
System.out.println("后置通知....执行顺序3"); | |
} | |
@AfterReturning(value = "customPointcut()") | |
public void afterReturningFour(){ | |
System.out.println("后置通知....执行顺序4"); | |
} | |
} |
在
xml
中配置一条bean
信息并交由SpringIOC
管理。
<bean name="userDaoAspect2" class="top.rem.rain.spring.aop.UserDaoAspect2"/> |
测试方法如下:
package top.rem.rain.spring.aop; | |
import org.junit.Test; | |
import org.junit.runner.RunWith; | |
import org.springframework.beans.factory.annotation.Autowired; | |
import org.springframework.test.context.ContextConfiguration; | |
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; | |
/** | |
* @Author: LightRain | |
* @Description: 测试类 | |
* @DateTime: 2024-01-04 21:54 | |
* @Version:1.0 | |
**/ | |
@RunWith(SpringJUnit4ClassRunner.class) | |
@ContextConfiguration(locations= "classpath:spring-aop-aspectj.xml") | |
public class UserDaoTest { | |
@Autowired | |
private UserDao userDao; | |
@Test | |
public void testUserDao2() { | |
userDao.deleteUser(); | |
/* | |
执行结果: | |
前置通知.... 执行顺序 1 | |
前置通知.... 执行顺序 2 | |
删除了一条用户记录 | |
后置通知.... 执行顺序 3 | |
后置通知.... 执行顺序 4 | |
*/ | |
} | |
} |
由此可以看出执行结果,在同一个切面中定义多个通知来处理同一个切点方法,确实会根据在类中的声明顺序执行。
前置通知.... 执行顺序 1 | |
前置通知.... 执行顺序 2 | |
删除了一条用户记录 | |
后置通知.... 执行顺序 3 | |
后置通知.... 执行顺序 4 |
如果在不同切面中定义多个通知来处理同一个切点,进入时则优先级高的切面类中的通知方法优先执行,退出时则最后执行。
如下定义了两个类分别是
UserDaoAspect3
和UserDaoAspect4
切面类并且实现了Ordered
接口,该接口用于控制切面类的优先级,同时重写getOrder()
方法,返回值越小优先级越高。其中UserDaoAspect3
的返回值是0
,而UserDaoAspect4
的优先级是1
, 显然UserDaoAspect3
的优先级更高。
package top.rem.rain.spring.aop; | |
import org.aspectj.lang.annotation.AfterReturning; | |
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.core.Ordered; | |
/** | |
* @Author: LightRain | |
* @Description: SpringAop 切面类 | |
* @DateTime: 2024-01-04 21:28 | |
* @Version:1.0 | |
**/ | |
@Aspect // 使用 AspectJ 的 @Aspect 注解来表示此类是一个切面类 | |
@EnableAspectJAutoProxy // 使用 Spring 启用 AspectJ 自动代理 | |
public class UserDaoAspect3 implements Ordered { | |
/** | |
* Pointcut 定义切点函数 | |
*/ | |
@Pointcut("execution(* top.rem.rain.spring.aop.UserDao.deleteUser(..))") | |
private void customPointcut() { | |
} | |
@Before("customPointcut()") | |
public void beforeOne() { | |
System.out.println("前置通知..UserDaoAspect3..执行顺序1"); | |
} | |
@Before("customPointcut()") | |
public void beforeTwo() { | |
System.out.println("前置通知..UserDaoAspect3..执行顺序2"); | |
} | |
@AfterReturning(value = "customPointcut()") | |
public void afterReturningThree() { | |
System.out.println("后置通知..UserDaoAspect3..执行顺序3"); | |
} | |
@AfterReturning(value = "customPointcut()") | |
public void afterReturningFour() { | |
System.out.println("后置通知..UserDaoAspect3..执行顺序4"); | |
} | |
/** | |
* 定义优先级,值越低,优先级越高 | |
* @return int | |
*/ | |
@Override | |
public int getOrder() { | |
return 0; | |
} | |
} |
package top.rem.rain.spring.aop; | |
import org.aspectj.lang.annotation.AfterReturning; | |
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.core.Ordered; | |
/** | |
* @Author: LightRain | |
* @Description: SpringAop 切面类 | |
* @DateTime: 2024-01-04 21:28 | |
* @Version:1.0 | |
**/ | |
@Aspect // 使用 AspectJ 的 @Aspect 注解来表示此类是一个切面类 | |
@EnableAspectJAutoProxy // 使用 Spring 启用 AspectJ 自动代理 | |
public class UserDaoAspect4 implements Ordered { | |
/** | |
* Pointcut 定义切点函数 | |
*/ | |
@Pointcut("execution(* top.rem.rain.spring.aop.UserDao.deleteUser(..))") | |
private void customPointcut() { | |
} | |
@Before("customPointcut()") | |
public void beforeOne() { | |
System.out.println("前置通知..UserDaoAspect4..执行顺序1"); | |
} | |
@Before("customPointcut()") | |
public void beforeTwo() { | |
System.out.println("前置通知..UserDaoAspect4..执行顺序2"); | |
} | |
@AfterReturning(value = "customPointcut()") | |
public void afterReturningThree() { | |
System.out.println("后置通知..UserDaoAspect4..执行顺序3"); | |
} | |
@AfterReturning(value = "customPointcut()") | |
public void afterReturningFour() { | |
System.out.println("后置通知..UserDaoAspect4..执行顺序4"); | |
} | |
/** | |
* 定义优先级,值越低,优先级越高 | |
* | |
* @return int | |
*/ | |
@Override | |
public int getOrder() { | |
return 1; | |
} | |
} |
在
xml
添加如下配置:
<?xml version="1.0" encoding="UTF-8"?> | |
<beans xmlns="http://www.springframework.org/schema/beans" | |
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | |
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> | |
<!-- 定义目标对象 --> | |
<bean id="userDao" class="top.rem.rain.spring.aop.UserDaoImpl" /> | |
<!-- 定义 aspect 类 --> | |
<!-- <bean name="userDaoAspect" class="top.rem.rain.spring.aop.UserDaoAspect"/>--> | |
<!-- <bean name="userDaoAspect2" class="top.rem.rain.spring.aop.UserDaoAspect2"/>--> | |
<bean name="userDaoAspect3" class="top.rem.rain.spring.aop.UserDaoAspect3"/> | |
<bean name="userDaoAspect4" class="top.rem.rain.spring.aop.UserDaoAspect4"/> | |
</beans> |
测试方法和执行结果如下:
package top.rem.rain.spring.aop; | |
import org.junit.Test; | |
import org.junit.runner.RunWith; | |
import org.springframework.beans.factory.annotation.Autowired; | |
import org.springframework.test.context.ContextConfiguration; | |
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; | |
/** | |
* @Author: LightRain | |
* @Description: 测试类 | |
* @DateTime: 2024-01-04 21:54 | |
* @Version:1.0 | |
**/ | |
@RunWith(SpringJUnit4ClassRunner.class) | |
@ContextConfiguration(locations= "classpath:spring-aop-aspectj.xml") | |
public class UserDaoTest { | |
@Autowired | |
private UserDao userDao; | |
@Test | |
public void testUserDao3() { | |
userDao.deleteUser(); | |
/* | |
执行结果: | |
前置通知..UserDaoAspect3.. 执行顺序 1 | |
前置通知..UserDaoAspect3.. 执行顺序 2 | |
前置通知..UserDaoAspect4.. 执行顺序 1 | |
前置通知..UserDaoAspect4.. 执行顺序 2 | |
删除了一条用户记录 | |
后置通知..UserDaoAspect4.. 执行顺序 3 | |
后置通知..UserDaoAspect4.. 执行顺序 4 | |
后置通知..UserDaoAspect3.. 执行顺序 3 | |
后置通知..UserDaoAspect3.. 执行顺序 4 | |
*/ | |
} | |
} |
# SpringAop-XML 开发
下面是基于
XML
的方式来使用SpringAop
。
package top.rem.rain.spring.aop; | |
import org.aspectj.lang.ProceedingJoinPoint; | |
import org.aspectj.lang.annotation.AfterReturning; | |
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.core.Ordered; | |
/** | |
* @Author: LightRain | |
* @Description: SpringAop 切面类 - XML 方式 | |
* @DateTime: 2024-01-05 22:18 | |
* @Version:1.0 | |
**/ | |
public class UserDaoAspectXML { | |
public void before() { | |
System.out.println("UserDaoAspectXML ==== 前置通知"); | |
} | |
public void afterReturn(Object returnVal) { | |
System.out.println("后置通知 --> 返回值:" + returnVal); | |
} | |
public Object around(ProceedingJoinPoint joinPoint) throws Throwable { | |
System.out.println("UserDaoAspectXML ===== 环绕通知前"); | |
Object object = joinPoint.proceed(); | |
System.out.println("UserDaoAspectXML ===== 环绕通知后"); | |
return object; | |
} | |
public void afterThrowing(Throwable throwable) { | |
System.out.println("UserDaoAspectXML ====== 异常通知:" + throwable.getMessage()); | |
} | |
public void after() { | |
System.out.println("UserDaoAspectXML ===== 最终通知..来了"); | |
} | |
} |
通过配置文件的方式声明如下:
<?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: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/aop https://www.springframework.org/schema/aop/spring-aop.xsd"> | |
<!-- 定义目标对象 --> | |
<bean id="userDao" class="top.rem.rain.spring.aop.UserDaoImpl" /> | |
<!-- 定义 aspect 类 --> | |
<bean name="userDaoAspectXML" class="top.rem.rain.spring.aop.UserDaoAspectXML"/> | |
<!-- 配置 AOP 切面 --> | |
<aop:config> | |
<!-- 定义切点函数 --> | |
<aop:pointcut id="pointcut" expression="execution(* top.rem.rain.spring.aop.UserDao.addUser(..))" /> | |
<!-- 定义通知 order 定义优先级,值越小优先级越大 --> | |
<aop:aspect ref="userDaoAspectXML" order="0"> | |
<!-- 定义通知 | |
method 指定通知方法名,必须与 userDaoAspectXML 中的相同 | |
pointcut 指定切点函数 | |
--> | |
<aop:before method="before" pointcut-ref="pointcut" /> | |
<!-- 后置通知 returning="returnVal" 定义返回值 必须与类中声明的名称一样 --> | |
<aop:after-returning method="afterReturn" pointcut-ref="pointcut" returning="returnVal" /> | |
<!-- 环绕通知 --> | |
<aop:around method="around" pointcut-ref="pointcut" /> | |
<!-- 异常通知 throwing="throwable" 指定异常通知错误信息变量,必须与类中声明的名称一样 --> | |
<aop:after-throwing method="afterThrowing" pointcut-ref="pointcut" throwing="throwable"/> | |
<!-- | |
method : 通知的方法 (最终通知) | |
pointcut-ref : 通知应用到的切点方法 | |
--> | |
<aop:after method="after" pointcut-ref="pointcut"/> | |
</aop:aspect> | |
</aop:config> | |
</beans> |
测试方法和执行结果如下:
package top.rem.rain.spring.aop; | |
import org.junit.Test; | |
import org.junit.runner.RunWith; | |
import org.springframework.beans.factory.annotation.Autowired; | |
import org.springframework.test.context.ContextConfiguration; | |
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; | |
/** | |
* @Author: LightRain | |
* @Description: 测试类 | |
* @DateTime: 2024-01-04 21:54 | |
* @Version:1.0 | |
**/ | |
@RunWith(SpringJUnit4ClassRunner.class) | |
@ContextConfiguration(locations= "classpath:spring-aop-aspectj.xml") | |
public class UserDaoTest { | |
@Autowired | |
private UserDao userDao; | |
@Test | |
public void testUserDao4(){ | |
userDao.addUser(); | |
/* | |
执行结果: | |
UserDaoAspectXML ==== 前置通知 | |
UserDaoAspectXML ===== 环绕通知前 | |
添加了一条用户记录 | |
UserDaoAspectXML ===== 最终通知.. 来了 | |
UserDaoAspectXML ===== 环绕通知后 | |
后置通知 --> 返回值:成功添加一条用户记录 | |
*/ | |
} | |
} |
# SpringAop 应用场景
下面是两种开发中常用到的场景,注意:这些场景的代码并不一定适合所有情况,但它们确实可以运用到实际开发中。
# 性能监控
对于成熟的系统,性能监控是不可或缺的,以下代码将展示程序的消耗时间,而对于计算消耗时间的代码可能会存在于各个模块中。
// 获取当前毫秒值 | |
long start = System.currentTimeMillis(); | |
// 获取当前毫秒值并减去之前的毫秒值计算这段代码的运行耗时 | |
long time = System.currentTimeMillis() - start; |
而这种操作就可以使用
AOP
作为横切点来执行并减少模块中的重复代码。
为了方便演示此处只创建了一个
Controller
并未创建Service
, 其中的for
表示要执行的业务代码的耗时。
package top.rem.rain.controller; | |
import org.springframework.stereotype.Controller; | |
/** | |
* @Author: LightRain | |
* @Description: User 控制器 | |
* @DateTime: 2024-01-05 22:58 | |
* @Version:1.0 | |
**/ | |
@Controller | |
public class UserController { | |
public Object[] addUser(String userName,int age,String sex){ | |
// 此处本该是调用 service 层来处理响应的业务信息,这里为了简单演示就不创建 service 层了,就直接在 Controller 层写代码了 | |
for (int i = 0; i < 2000000000; i++) {} | |
return new Object[]{userName,age,sex}; | |
} | |
} |
package top.rem.rain.monitoring.performance.bean; | |
import java.util.Date; | |
/** | |
* @Author: LightRain | |
* @Description: 性能监控信息 | |
* @DateTime: 2024-01-05 22:50 | |
* @Version:1.0 | |
**/ | |
public class PerformanceMonitoringInfo { | |
/** | |
* 类名 | |
*/ | |
private String className; | |
/** | |
* 方法名 | |
*/ | |
private String methodName; | |
/** | |
* 日志时间 | |
*/ | |
private Date logTime; | |
/** | |
* 耗时时间 | |
*/ | |
private long timeConsuming; | |
public String getClassName() { | |
return className; | |
} | |
public void setClassName(String className) { | |
this.className = className; | |
} | |
public String getMethodName() { | |
return methodName; | |
} | |
public void setMethodName(String methodName) { | |
this.methodName = methodName; | |
} | |
public Date getLogTime() { | |
return logTime; | |
} | |
public void setLogTime(Date logTime) { | |
this.logTime = logTime; | |
} | |
public long getTimeConsuming() { | |
return timeConsuming; | |
} | |
public void setTimeConsuming(long timeConsuming) { | |
this.timeConsuming = timeConsuming; | |
} | |
} |
package top.rem.rain.monitoring.performance.aspect; | |
import org.aspectj.lang.ProceedingJoinPoint; | |
import org.aspectj.lang.annotation.Around; | |
import org.aspectj.lang.annotation.Aspect; | |
import org.aspectj.lang.annotation.Pointcut; | |
import org.springframework.context.annotation.EnableAspectJAutoProxy; | |
import top.rem.rain.monitoring.performance.bean.PerformanceMonitoringInfo; | |
import java.util.Date; | |
/** | |
* @Author: LightRain | |
* @Description: 监控切面类 | |
* @DateTime: 2024-01-05 23:09 | |
* @Version:1.0 | |
**/ | |
@Aspect // 这是一个 AspectJ 切面类 | |
@EnableAspectJAutoProxy // 启用 AspectJ 自动代理 | |
public class PerformanceAspect { | |
/** | |
* 定义切点函数,过滤 controller 包下的名称以 Controller 结尾的类所有方法 | |
*/ | |
@Pointcut("execution(* top.rem.rain.controller.*Controller.*(..))") | |
public void timer() { | |
} | |
@Around("timer()") | |
public Object logTimer(ProceedingJoinPoint thisJoinPoint) throws Throwable { | |
PerformanceMonitoringInfo performanceMonitoring = new PerformanceMonitoringInfo(); | |
// 获取目标类名称 | |
String clazzName = thisJoinPoint.getTarget().getClass().getName(); | |
// 获取目标类方法名称 | |
String methodName = thisJoinPoint.getSignature().getName(); | |
// 记录类名称 | |
performanceMonitoring.setClassName(clazzName); | |
// 记录对应方法名称 | |
performanceMonitoring.setMethodName(methodName); | |
// 记录时间 | |
performanceMonitoring.setLogTime(new Date()); | |
// 计时并调用目标函数 | |
long start = System.currentTimeMillis(); | |
Object result = thisJoinPoint.proceed(); | |
long time = System.currentTimeMillis() - start; | |
// 设置消耗时间 | |
performanceMonitoring.setTimeConsuming(time); | |
System.out.println("消耗时间 = " + time + "毫秒"); | |
// 如果要把 performanceMonitoring 记录的信息上传给监控系统,这里并没有实现,需要自行实现即可 | |
//MonitoruUtils.report(performanceMonitoring) | |
return result; | |
} | |
} |
配置
XML
由SpringIOC
进行管理。
<?xml version="1.0" encoding="UTF-8"?> | |
<beans xmlns="http://www.springframework.org/schema/beans" | |
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | |
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> | |
<bean name="userController" class="top.rem.rain.controller.UserController"/> | |
<bean name="performanceAspect" class="top.rem.rain.monitoring.performance.aspect.PerformanceAspect"/> | |
</beans> |
测试方法如下:
package top.rem.rain.monitoring.performance.test; | |
import org.junit.Test; | |
import org.junit.runner.RunWith; | |
import org.springframework.beans.factory.annotation.Autowired; | |
import org.springframework.test.context.ContextConfiguration; | |
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; | |
import top.rem.rain.controller.UserController; | |
import java.util.Arrays; | |
/** | |
* @Author: LightRain | |
* @Description: 测试类 | |
* @DateTime: 2024-01-05 23:18 | |
* @Version:1.0 | |
**/ | |
@RunWith(SpringJUnit4ClassRunner.class) | |
@ContextConfiguration(locations= "classpath:performance-monitoring.xml") | |
public class UserControllerTest { | |
@Autowired | |
private UserController controller; | |
@Test | |
public void testUser(){ | |
System.out.println(Arrays.toString(controller.addUser("ZeroTwo", 17, "女"))); | |
/* | |
执行结果: | |
消耗时间 = 4 毫秒 | |
[ZeroTwo, 17, 女] | |
*/ | |
} | |
} |
在每次访问切点方法时就可以计算出接口消耗的时间了,以此来作为访问接口的性能指标。
# 异常处理
为了避免到处编写
try/catch
将统一处理异常信息,这也是AOP
可以做的事情,可以在切面类中使用环绕通知来统一处理异常信息并上报给日志系统。
package top.rem.rain.monitoring.abnormal.bean; | |
import java.util.Date; | |
/** | |
* @Author: LightRain | |
* @Description: 异常信息类 | |
* @DateTime: 2024-01-06 01:17 | |
* @Version:1.0 | |
**/ | |
public class ExceptionInfo { | |
/** | |
* 类名 | |
*/ | |
private String className; | |
/** | |
* 方法名 | |
*/ | |
private String methodName; | |
/** | |
* 异常时间 | |
*/ | |
private Date logTime; | |
/** | |
* 异常信息 | |
*/ | |
private String message; | |
public String getClassName() { | |
return className; | |
} | |
public void setClassName(String className) { | |
this.className = className; | |
} | |
public String getMethodName() { | |
return methodName; | |
} | |
public void setMethodName(String methodName) { | |
this.methodName = methodName; | |
} | |
public Date getLogTime() { | |
return logTime; | |
} | |
public void setLogTime(Date logTime) { | |
this.logTime = logTime; | |
} | |
public String getMessage() { | |
return message; | |
} | |
public void setMessage(String message) { | |
this.message = message; | |
} | |
} |
package top.rem.rain.monitoring.abnormal.aspect; | |
import org.aspectj.lang.ProceedingJoinPoint; | |
import org.aspectj.lang.annotation.Around; | |
import org.aspectj.lang.annotation.Aspect; | |
import org.aspectj.lang.annotation.Pointcut; | |
import org.springframework.context.annotation.EnableAspectJAutoProxy; | |
import top.rem.rain.monitoring.abnormal.bean.ExceptionInfo; | |
import java.util.Date; | |
/** | |
* @Author: LightRain | |
* @Description: 异常切面类 | |
* @DateTime: 2024-01-06 01:19 | |
* @Version:1.0 | |
**/ | |
@Aspect | |
@EnableAspectJAutoProxy | |
public class ExceptionAspect { | |
/** | |
* 定义异常监控类 | |
*/ | |
@Pointcut("execution(top.rem.rain *(..))") | |
void exceptionMethod() { | |
} | |
@Around("exceptionMethod()") | |
public Object monitorMethods(ProceedingJoinPoint thisJoinPoint) { | |
try { | |
return thisJoinPoint.proceed(); | |
} catch (Throwable e) { | |
ExceptionInfo exceptionInfo=new ExceptionInfo(); | |
// 异常类记录 | |
exceptionInfo.setClassName(thisJoinPoint.getTarget().getClass().getName()); | |
exceptionInfo.setMethodName(thisJoinPoint.getSignature().getName()); | |
exceptionInfo.setLogTime(new Date()); | |
exceptionInfo.setMessage(e.toString()); | |
// 上传日志系统,自行完善 | |
// ExceptionReportUtils.report(info); | |
return null; | |
} | |
} | |
} |
这里仅简单演示代码实现,在实际开发中根据业务需求修改即可。
AOP
的应用远不止这两种,诸如缓存,权限验证、内容处理、事务控制等都可以使用AOP
实现,其中事务控制Spring
提供了专门的处理方式。
# SpringAop 实现原理
SpringAop
的实现原理是基于动态织入的动态代理技术,而AspectJ
则是静态织入,而动态代理技术又分为Java JDK
动态代理和CGLIB
动态代理,前者是基于反射技术的实现,后者是基于继承的机制实现,关于代理可以查看 23 种设计模式中的代理模式。