教你如何玩转Spring的两大特性之一AOP,掌握工作中的代码整洁之道
AOP专题
- 1: 1.基本使用姿势小结
- 2: 2.高级使用技能
- 3: 3.拦截优先级详解
- 4: 4.AOP实现一个日志插件(应用篇)
- 5: 5.接口上注解AOP拦截不到场景兼容
- 6: 6.AOP结合SpEL实现日志输出的注意事项
1 - 1.基本使用姿势小结
一般来讲,谈到Spring的特性,绕不过去的就是DI(依赖注入)和AOP(切面),在将bean的系列中,说了DI的多种使用姿势;接下来看一下AOP的玩法
I. 背景知识
在实际使用之前有必要了解一下什么是AOP,以及AOP的几个基本概念
1. advice
- before: 在方法执行之前被调用
- after: 在方法执行之后调用
- after returning: 方法执行成功之后
- after throwing: 方法抛出异常之后
- around: 环绕,自己在内部决定方法的执行时机,因此可以在之前之后做一些业务逻辑
2. join point
连接点,比如方法调用,方法执行,字段设置/获取、异常处理执行、类初始化、甚至是 for 循环中的某个点
但 Spring AOP 目前仅支持方法执行 (method execution)
简单来说,Spring AOP中,PointCut就是那个被拦截的方法
3. pointcut
切点,用来描述满足什么规则的方法会被拦截
- 正则表达式 :
@Before("execution(public * com.git.hui.demo.base.bean.*.*(..))")
- 注解拦截方式 :
@Around("@annotation(parameterCheck)")
4. aspect
切面是切点和通知的结合。通知和切点共同定义了关于切面的全部内容,它是什么时候,在何时和何处完成功能
5. introduction
引入允许我们向现有的类添加新的方法或者属性
6. weaving
组装方面来创建一个被通知对象。这可以在编译时完成(例如使用AspectJ编译器),也可以在运行时完成。Spring和其他纯Java AOP框架一样,在运行时完成织入。
简单来讲就是生成一个代理类,在调用被拦截的方法时,实际上执行的是代理类,这个代理类内部执行切面逻辑
II. 使用说明
1. 基本配置
首先是基本环境的搭建, 先贴上必要的xml配置, 使用aop需要引入包: spring-boot-starter-aop
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-cloud.version>Finchley.RELEASE</spring-cloud.version>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</pluginManagement>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
2. 代码准备
首先创建一个被拦截的bean: com.git.hui.boot.aop.demo.DemoBean
,如下
@Component
public class DemoBean {
/**
* 返回随机的字符串
*
* @param time
* @return
*/
public String randUUID(long time) {
try {
System.out.println("in randUUID before process!");
return UUID.randomUUID() + "|" + time;
} finally {
System.out.println("in randUUID finally!");
}
}
}
接着在启动类中,执行
@SpringBootApplication
public class Application {
public Application(DemoBean demoBean) {
String ans = demoBean.randUUID(System.currentTimeMillis());
System.out.println("----- ans: " + ans + "---------");
}
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}
3. AOP使用
在实际使用之前,需要创建一个切面,用@Aspect
声明,其次切面也需要作为bean托付给Spring容器管理
@Aspect
@Component
public class AnoAspcet {
}
a. before
在方法调用之前,需要执行一些操作,这个时候可以使用 @Before
注解来声明before advice
一种可使用姿势如下,我们的切点直接在注解中进行定义,使用正则表达式的方式
@Before("execution(public * com.git.hui.boot.aop.demo.*.*(*))")
public void doBefore(JoinPoint joinPoint) {
System.out.println("do in Aspect before method called! args: " + JSON.toJSONString(joinPoint.getArgs()));
}
b. after
在方法调用完毕之后,再执行一些操作,这个时候after就可以派上用场,为了考虑切点的通用性,我们可以考虑声明一个切点,使用@Pointcut
注解
@Pointcut("execution(public * com.git.hui.boot.aop.demo.*.*(*))")
public void point() {
}
使用pointcut的方式也比较简单,如下
@After("point()")
public void doAfter(JoinPoint joinPoint) {
System.out.println("do in Aspect after method called! args: " + JSON.toJSONString(joinPoint.getArgs()));
}
c. after returning
在正常返回结果之后,再次执行,这个也挺有意思的,通常使用这个advice时,一般希望获取返回结果,那么应该怎么处理呢?
org.aspectj.lang.annotation.AfterReturning#returning
指定返回结果对应参数name- 返回结果作为参数传入,要求类型一致,否则不生效
/**
* 执行完毕之后,通过 args指定参数;通过 returning 指定返回的结果,要求返回值类型匹配
*
* @param time
* @param result
*/
@AfterReturning(value = "point() && args(time)", returning = "result")
public void doAfterReturning(long time, String result) {
System.out.println("do in Aspect after method return! args: " + time + " ans: " + result);
}
d. around
这个也比较常见,在方法执行前后干一些事情,比如常见的耗时统计,日志打印,安全控制等,很多都是基于around advice实现的
使用这个advice需要注意的是传入参数类型为 ProceedingJoinPoint
,需要在方法内部显示执行org.aspectj.lang.ProceedingJoinPoint#proceed()
来表示调用方法
@Around("point()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("do in Aspect around ------ before");
Object ans = joinPoint.proceed();
System.out.println("do in Aspect around ------- over! ans: " + ans);
return ans;
}
e. 输出
执行之后输出如下
do in Aspect around ------ before
do in Aspect before method called! args: [1551433188205]
in randUUID before process!
in randUUID finally!
do in Aspect around ------- over! ans: 6849544b-160e-464c-80bd-641f2651c6c1|1551433188205
do in Aspect after method called! args: [1551433188205]
do in Aspect after method return! args: 1551433188205 ans: 6849544b-160e-464c-80bd-641f2651c6c1|1551433188205
----- ans: 6849544b-160e-464c-80bd-641f2651c6c1|1551433188205---------
从输出结果上,可以看到每个advice的使用范围,当然也带来了一些疑问
- 可以存在多个同类型的advice,拦截同一个目标吗?(如两个around都拦截methodA方法,那么methodA方法被调用时,两个around advice是否都会执行)
- 多个advice之间的优先级怎么定义?
- aop拦截的目标方法有没有限制(对非public的方法可以拦截么?)
- 被拦截的方法中存在相互调用的时候,会怎样?(如methodA,methodB都可以被拦截,且methodA中调用了methodB,那么在执行methodA时,methodB的各种advice是否会被触发?)
- 基于注解的aop方式可以怎样用
以上这些问题留在下一篇进行介绍
III. 其他
0. 项目
2 - 2.高级使用技能
前面一篇博文 190301-SpringBoot基础篇AOP之基本使用姿势小结 介绍了aop的简单使用方式,在文章最后,抛出了几个问题待解决,本篇博文则将针对前面的问题,看下更多关于AOP的使用说明
I. 高级技能
1. 注解拦截方式
前面一文,主要介绍的是根据正则表达式来拦截对应的方法,接下来演示下如何通过注解的方式来拦截目标方法,实现也比较简单
首先创建注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AnoDot {
}
接着在目标方法上添加注解,这里借助前面博文中工程进行说明,新建一个com.git.hui.boot.aop.demo2.AnoDemoBean
,注意这个包路径,是不会被前文的AnoAspect
定义的Advice拦截的,这里新建一个包路径的目的就是为了尽可能的减少干扰项
@Component
public class AnoDemoBean {
@AnoDot
public String genUUID(long time) {
try {
System.out.println("in genUUID before process!");
return UUID.randomUUID() + "|" + time;
} finally {
System.out.println("in genUUID finally!");
}
}
}
接下来定义对应的advice, 直接在前面的AnoAspect
中添加(不知道前文的也没关系,下面贴出相关的代码类,前文的类容与本节内容无关)
@Aspect
@Component
public class AnoAspect {
@Before("@annotation(AnoDot)")
public void anoBefore() {
System.out.println("AnoAspect ");
}
}
测试代码
@SpringBootApplication
public class Application {
private AnoDemoBean anoDemoBean;
public Application(AnoDemoBean anoDemoBean) {
this.anoDemoBean = anoDemoBean;
this.anoDemoBean();
}
private void anoDemoBean() {
System.out.println(">>>>>>>" + anoDemoBean.genUUID(System.currentTimeMillis()));
}
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}
输出结果如下,在执行目标方法之前,会先执行before advice中的逻辑
AnoAspect
in genUUID before process!
in genUUID finally!
>>>>>>>3a5d749d-d94c-4fc0-a7a3-12fd97f3e1fa|1551513443644
2. 多个advice拦截
一个方法执行时,如果有多个advice满足拦截规则,是所有的都会触发么?通过前面一篇博文知道,不同类型的advice是都可以拦截的,如果出现多个相同类型的advice呢?
在前面一篇博文的基础上进行操作,我们扩展下com.git.hui.boot.aop.demo.DemoBean
@Component
public class DemoBean {
@AnoDot
public String genUUID(long time) {
try {
System.out.println("in genUUID before process!");
return UUID.randomUUID() + "|" + time;
} finally {
System.out.println("in genUUID finally!");
}
}
}
对应的测试切面内容如
@Aspect
@Component
public class AnoAspect {
@Before("execution(public * com.git.hui.boot.aop.demo.*.*(*))")
public void doBefore(JoinPoint joinPoint) {
System.out.println("do in Aspect before method called! args: " + JSON.toJSONString(joinPoint.getArgs()));
}
@Pointcut("execution(public * com.git.hui.boot.aop.demo.*.*(*))")
public void point() {
}
@After("point()")
public void doAfter(JoinPoint joinPoint) {
System.out.println("do in Aspect after method called! args: " + JSON.toJSONString(joinPoint.getArgs()));
}
/**
* 执行完毕之后,通过 args指定参数;通过 returning 指定返回的结果,要求返回值类型匹配
*
* @param time
* @param result
*/
@AfterReturning(value = "point() && args(time)", returning = "result")
public void doAfterReturning(long time, String result) {
System.out.println("do in Aspect after method return! args: " + time + " ans: " + result);
}
@Around("point()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("do in Aspect around ------ before");
Object ans = joinPoint.proceed();
System.out.println("do in Aspect around ------- over! ans: " + ans);
return ans;
}
@Before("point()")
public void sameBefore() {
System.out.println("SameAspect");
}
@Before("@annotation(AnoDot)")
public void anoBefore() {
System.out.println("AnoAspect");
}
}
测试代码如下
@SpringBootApplication
public class Application {
private DemoBean demoBean;
public Application(DemoBean demoBean) {
this.demoBean = demoBean;
this.demoBean();
}
private void demoBean() {
System.out.println(">>>>> " + demoBean.genUUID(System.currentTimeMillis()));
}
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}
输出结果如下,所有的切面都执行了,也就是说,只要满足条件的advice,都会被拦截到
do in Aspect around ------ before
AnoAspect
do in Aspect before method called! args: [1551520547268]
SameAspect
in genUUID before process!
in genUUID finally!
do in Aspect around ------- over! ans: 5f6a5616-f558-4ac9-ba4b-b4360d7dc238|1551520547268
do in Aspect after method called! args: [1551520547268]
do in Aspect after method return! args: 1551520547268 ans: 5f6a5616-f558-4ac9-ba4b-b4360d7dc238|1551520547268
>>>>> 5f6a5616-f558-4ac9-ba4b-b4360d7dc238|1551520547268
3. 嵌套拦截
嵌套的方式有几种case,先看第一种
a. 调用方法不满足拦截规则,调用本类中其他满足拦截条件的方法
这里我们借助第一节中的bean来继续模拟, 在AnoDemoBean
类中,新增一个方法
@Component
public class AnoDemoBean {
public String randUUID(long time) {
try {
System.out.println("in randUUID start!");
return genUUID(time);
} finally {
System.out.println("in randUUID finally!");
}
}
@AnoDot
public String genUUID(long time) {
try {
System.out.println("in genUUID before process!");
return UUID.randomUUID() + "|" + time;
} finally {
System.out.println("in genUUID finally!");
}
}
}
对应的切面为
@Aspect
@Component
public class NetAspect {
@Around("@annotation(AnoDot)")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("In NetAspect doAround before!");
Object ans = joinPoint.proceed();
System.out.println("In NetAspect doAround over! ans: " + ans);
return ans;
}
}
然后测试case需要改为直接调用 AnoDemoBean#randUUID
,需要看这个方法内部调用的genUUID
是否会被切面拦截住
@SpringBootApplication
public class Application {
private AnoDemoBean anoDemoBean;
public Application(AnoDemoBean anoDemoBean) {
this.anoDemoBean = anoDemoBean;
this.anoDemoBean();
}
private void anoDemoBean() {
System.out.println(">>>>>>>" + anoDemoBean.randUUID(System.currentTimeMillis()));
}
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}
输出结果如下,没有切面的日志,表明这种场景下,不会被拦截
in randUUID start!
in genUUID before process!
in genUUID finally!
in randUUID finally!
>>>>>>>0c6a5ccf-30c0-4ac0-97f2-3dc063580f3d|1551522176035
b. 调用方法不满足拦截规则,调用其他类中满足拦截条件的方法
依然使用前面的例子进行说明,不过是稍稍改一下AnoDemoBean
,调用第二节中的DemoBean的方法
DemoBean的代码如下
@AnoDot
public String genUUID(long time) {
try {
System.out.println("in DemoBean genUUID before process!");
return UUID.randomUUID() + "|" + time;
} finally {
System.out.println("in DemoBean genUUID finally!");
}
}
然后AnoDemoBean的代码如下
@Component
public class AnoDemoBean {
@Autowired
private DemoBean demoBean;
public String randUUID(long time) {
try {
System.out.println("in AnoDemoBean randUUID start!");
return genUUID(time) + "<<<>>>" + demoBean.genUUID(time);
} finally {
System.out.println("in AnoDemoBean randUUID finally!");
}
}
@AnoDot
public String genUUID(long time) {
try {
System.out.println("in AnoDemoBean genUUID before process!");
return UUID.randomUUID() + "|" + time;
} finally {
System.out.println("in AnoDemoBean genUUID finally!");
}
}
}
测试代码和前面完全一致,接下来看下输出
in AnoDemoBean randUUID start!
in AnoDemoBean genUUID before process!
in AnoDemoBean genUUID finally!
### 上面三行为 anoDemoBean#randUUID方法调用 anoDemoBean#genUUID方法的输出结果,可以看到没有切面执行的日志输出
### 下面的为调用 demoBean#genUUID 方法,可以看到切面(NetAspect#doAround)执行的日志
In NetAspect doAround before!
in DemoBean genUUID before process!
in DemoBean genUUID finally!
In NetAspect doAround over! ans: f35b8878-fbd0-4840-8fbe-5fef8eda5e31|1551522532092
### 最后是收尾
in AnoDemoBean randUUID finally!
>>>>>>>e516a35f-b85a-4cbd-aae0-fa97cdecab47|1551522532092<<<>>>f35b8878-fbd0-4840-8fbe-5fef8eda5e31|1551522532092
从上面的日志分析中,可以明确看出对比,调用本类中,满足被拦截的方法,也不会走切面逻辑;调用其他类中的满足切面拦截的方法,会走切面逻辑
c. 调用方法满足切面拦截条件,又调用其他满足切面拦截条件的方法
这个和两个case有点像,不同的是直接调用的方法也满足被切面拦截的条件,我们主要关注点在于嵌套调用的方法,会不会进入切面逻辑,这里需要修改的地方就很少了,直接把 AnoDemoBean#randUUID
方法上添加注解,然后执行即可
@Component
public class AnoDemoBean {
@Autowired
private DemoBean demoBean;
@AnoDot
public String randUUID(long time) {
try {
System.out.println("in AnoDemoBean randUUID start!");
return genUUID(time) + "<<<>>>" + demoBean.genUUID(time);
} finally {
System.out.println("in AnoDemoBean randUUID finally!");
}
}
@AnoDot
public String genUUID(long time) {
try {
System.out.println("in AnoDemoBean genUUID before process!");
return UUID.randomUUID() + "|" + time;
} finally {
System.out.println("in AnoDemoBean genUUID finally!");
}
}
}
输出结果如下
## 最外层的切面拦截的是 AnoDemoBean#randUUID 方法的执行
In NetAspect doAround before!
in AnoDemoBean randUUID start!
in AnoDemoBean genUUID before process!
in AnoDemoBean genUUID finally!
### 从跟上面三行的输出,可以知道内部调用的 AnoDemoBean#genUUID 即便满足切面拦截规则,也不会再次走切面逻辑
### 下面4行,表明其他类的方法,如果满足切面拦截规则,会进入到切面逻辑
In NetAspect doAround before!
in DemoBean genUUID before process!
in DemoBean genUUID finally!
In NetAspect doAround over! ans: d9df7388-2ef8-4b1a-acb5-6639c47f36ca|1551522969801
in AnoDemoBean randUUID finally!
In NetAspect doAround over! ans: cf350bc2-9a9a-4ef6-b496-c913d297c960|1551522969801<<<>>>d9df7388-2ef8-4b1a-acb5-6639c47f36ca|1551522969801
>>>>>>>cf350bc2-9a9a-4ef6-b496-c913d297c960|1551522969801<<<>>>d9df7388-2ef8-4b1a-acb5-6639c47f36ca|1551522969801
从输出结果进行反推,一个结论是
- 执行的目标方法,如果调用了本类中一个满足切面规则的方法A时,在执行方法A的过程中,不会触发切面逻辑
- 执行的目标方法,如果调用其他类中一个满足切面规则的方法B时,在执行方法B的过程中,将会触发切面逻辑
4. AOP拦截方法作用域
前面测试的被拦截方法都是public,那么是否表明只有public方法才能被拦截呢?
从第三节基本可以看出,private方法首先淘汰出列,为啥?因为private方法正常来讲只能内部调用,而内部调用不会走切面逻辑;所以接下来需要关注的主要放在默认作用域和protected作用域
@Component
public class ScopeDemoBean {
@AnoDot
String defaultRandUUID(long time) {
try {
System.out.println(" in ScopeDemoBean defaultRandUUID before!");
return UUID.randomUUID() + " | default | " + time;
} finally {
System.out.println(" in ScopeDemoBean defaultRandUUID finally!");
}
}
@AnoDot
protected String protectedRandUUID(long time) {
try {
System.out.println(" in ScopeDemoBean protectedRandUUID before!");
return UUID.randomUUID() + " | protected | " + time;
} finally {
System.out.println(" in ScopeDemoBean protectedRandUUID finally!");
}
}
@AnoDot
private String privateRandUUID(long time) {
try {
System.out.println(" in ScopeDemoBean privateRandUUID before!");
return UUID.randomUUID() + " | private | " + time;
} finally {
System.out.println(" in ScopeDemoBean privateRandUUID finally!");
}
}
}
我们不直接使用这个类里面的方法,借助前面的 AnoDemoBean
, 下面给出了通过反射的方式来调用private方法的case
@Component
public class AnoDemoBean {
@Autowired
private ScopeDemoBean scopeDemoBean;
public void scopeUUID(long time) {
try {
System.out.println("-------- default --------");
String defaultAns = scopeDemoBean.defaultRandUUID(time);
System.out.println("-------- default: " + defaultAns + " --------\n");
System.out.println("-------- protected --------");
String protectedAns = scopeDemoBean.protectedRandUUID(time);
System.out.println("-------- protected: " + protectedAns + " --------\n");
System.out.println("-------- private --------");
Method method = ScopeDemoBean.class.getDeclaredMethod("privateRandUUID", long.class);
method.setAccessible(true);
String privateAns = (String) method.invoke(scopeDemoBean, time);
System.out.println("-------- private: " + privateAns + " --------\n");
} catch (Exception e) {
e.printStackTrace();
}
}
}
测试case
@SpringBootApplication
public class Application {
private AnoDemoBean anoDemoBean;
public Application(AnoDemoBean anoDemoBean) {
this.anoDemoBean = anoDemoBean;
this.anoDemoBean();
}
private void anoDemoBean() {
anoDemoBean.scopeUUID(System.currentTimeMillis());
}
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}
输出结果如下,从日志打印来看,protected和default方法的切面都走到了
-------- default --------
In NetAspect doAround before!
in ScopeDemoBean defaultRandUUID before!
in ScopeDemoBean defaultRandUUID finally!
In NetAspect doAround over! ans: 2ad7e509-c62c-4f25-b68f-eb5e0b53196d | default | 1551524311537
-------- default: 2ad7e509-c62c-4f25-b68f-eb5e0b53196d | default | 1551524311537 --------
-------- protected --------
In NetAspect doAround before!
in ScopeDemoBean protectedRandUUID before!
in ScopeDemoBean protectedRandUUID finally!
In NetAspect doAround over! ans: 9eb339f8-9e71-4321-ab83-a8953d1b8ff8 | protected | 1551524311537
-------- protected: 9eb339f8-9e71-4321-ab83-a8953d1b8ff8 | protected | 1551524311537 --------
-------- private --------
in ScopeDemoBean privateRandUUID before!
in ScopeDemoBean privateRandUUID finally!
-------- private: 1826afac-6eca-4dc3-8edc-b4ca7146ce28 | private | 1551524311537 --------
5. 小结
本篇博文篇幅比较长,主要是测试代码比较占用地方,因此有必要简单的小结一下,做一个清晰的归纳,方便不想看细节,只想获取最终结论的小伙伴
注解拦截方式:
- 首先声明注解
- 在目标方法上添加注解
- 切面中,advice的内容形如
@Around("@annotation(AnoDot)")
多advice情况:
- 多个advice满足拦截场景时,全部都会执行
嵌套场景
- 执行的目标方法,如果调用了本类中一个满足切面规则的方法A时,在执行方法A的过程中,不会触发切面逻辑
- 执行的目标方法,如果调用其他类中一个满足切面规则的方法B时,在执行方法B的过程中,将会触发切面逻辑
作用域
- public, protected, default 作用域的方法都可以被拦截
优先级
这个内容因为特别多,所以有必要单独拎出来,其主要的分类如下
- 同一aspect,不同advice的执行顺序
- 不同aspect,advice的执行顺序
- 同一aspect,相同advice的执行顺序
II. 其他
0. 项目
3 - 3.拦截优先级详解
前面两篇分别介绍了AOP的基本使用姿势和一些高级特性,当时还遗留了一个问题没有说明,即不同的advice,拦截同一个目标方法时,优先级是怎样的,本篇博文将进行详细分析
- 同一个切面中,不同类型的advice的优先级
- 同一个切面中,同一种类型的advice优先级
- 不同切面中,同一类型的advice优先级
- 不同切面中,不同类型的advice优先级
I. 统一切面,不同类型ddvice优先级
在不分析源码的前提下,也只能通过实际的case来看优先级问题了,我们现在设计一下使用实例,通过输出结果来看对应的优先级
1. case设计
首先创建被拦截的bean: com.git.hui.boot.aop.order.InnerDemoBean
@Component
public class InnerDemoBean {
public String print() {
try {
System.out.println("in innerDemoBean start!");
String rans = System.currentTimeMillis() + "|" + UUID.randomUUID();
System.out.println(rans);
return rans;
} finally {
System.out.println("in innerDemoBean over!");
}
}
}
接下来写一个切面,里面定义我们常见的各种advice
对于aop的使用,有疑问的可以参考: 190301-SpringBoot基础篇AOP之基本使用姿势小结
@Component
@Aspect
public class OrderAspect {
@Pointcut("execution(public * com.git.hui.boot.aop.order.*.*())")
public void point() {
}
@Before(value = "point()")
public void doBefore(JoinPoint joinPoint) {
System.out.println("do before!");
}
@After(value = "point()")
public void doAfter(JoinPoint joinPoint) {
System.out.println("do after!");
}
@AfterReturning(value = "point()", returning = "ans")
public void doAfterReturning(JoinPoint joinPoint, String ans) {
System.out.println("do after return: " + ans);
}
@Around("point()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
try {
System.out.println("do in around before");
return joinPoint.proceed();
} finally {
System.out.println("do in around over!");
}
}
}
2. 测试
使用SpringBoot的项目进行测试aop,使用还是比较简单的
@SpringBootApplication
public class Application {
private InnerDemoBean innerDemoBean;
public Application(InnerDemoBean innerDemoBean) {
this.innerDemoBean = innerDemoBean;
this.innerDemoBean();
}
private void innerDemoBean() {
System.out.println("result: " + innerDemoBean.print());
}
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}
看下上面执行的输出结果
do in around before
do before!
in innerDemoBean start!
1552219604035|e9a31f44-6a31-4485-806a-834361842ce1
in innerDemoBean over!
do in around over!
do after!
do after return: 1552219604035|e9a31f44-6a31-4485-806a-834361842ce1
result: 1552219604035|e9a31f44-6a31-4485-806a-834361842ce1
从输出结果进行反推,我们可以知道统一切面中,advice执行的先后顺序如下
II. 同一切面,同一类型切面
正常来讲,拦截一个方法时,统一类型的切面逻辑都会写在一起,那这个case有什么分析的必要呢?
在我们实际的使用中,同一类型的advice拦截同一个方法的可能性还是很高的,why? 因为多个advice有自己定义的拦截规则,它们之间并不相同,但可能存在交集,比如我们在上面的切面中,再加一个拦截注解的before advice
1. case设计
依然是上面的InnerDemoBean
,方法上加一个自定义注解
@AnoDot
public String print() {
try {
System.out.println("in innerDemoBean start!");
String rans = System.currentTimeMillis() + "|" + UUID.randomUUID();
System.out.println(rans);
return rans;
} finally {
System.out.println("in innerDemoBean over!");
}
}
然后加一个拦截注解的advice
@Before("@annotation(AnoDot)")
public void doAnoBefore(JoinPoint joinPoint) {
System.out.println("dp AnoBefore");
}
2. 测试
再次执行前面的case,然后看下输出结果如下
In NetAspect doAround before!
do in around before
dp AnoBefore
do before!
in innerDemoBean start!
1552221765322|d92b6d37-0025-43c0-adcc-c4aa7ba639e0
in innerDemoBean over!
do in around over!
do after!
do after return: 1552221765322|d92b6d37-0025-43c0-adcc-c4aa7ba639e0
In NetAspect doAround over! ans: 1552221765322|d92b6d37-0025-43c0-adcc-c4aa7ba639e0
result: 1552221765322|d92b6d37-0025-43c0-adcc-c4aa7ba639e0
我们主要看下两个before,发现 AnoBefore
在前面; 因此这里的一个猜测,顺序就是根据方法命名的顺序来的,比如我们再加一个 doXBefore
,然后我们预估输出结果应该是
do AnoBefore > doBefore > doXBefore
额外添加一个
@Before("@annotation(AnoDot)")
public void doXBefore(JoinPoint joinPoint) {
System.out.println("dp XBefore");
}
接着就是输出结果如下,和我们预期一致
3. Order注解尝试
我们知道有个Order注解可以来定义一些优先级,那么把这个注解放在advice方法上,有效么?实际尝试一下
@Order(1)
@Before(value = "point()")
public void doBefore(JoinPoint joinPoint) {
System.out.println("do before!");
}
@Order(2)
@Before("@annotation(AnoDot)")
public void doAnoBefore(JoinPoint joinPoint) {
System.out.println("dp AnoBefore");
}
@Order(3)
@Before("@annotation(AnoDot)")
public void doXBefore(JoinPoint joinPoint) {
System.out.println("dp XBefore");
}
如果注解有效,我们预期输出结果如下
do Before > do AnoBefore > do XBefore
然后再次执行,看下输出结果是否和我们预期一样
4. 小结
同一个切面中,相同的类型的advice,优先级是根据方法命名来的,加@Order
注解是没有什么鸟用的,目前也没有搜索到可以调整优先级的方式
III. 不同切面,相同类型的advice
如果说上面这种case不太好理解为啥会出现的话,那么这个可能就容易理解多了;毕竟一个切面完成一件事情,出现相同的advice就比较常见了;
比如spring mvc中,我们通常会实现的几个切面
- 一个before advice的切面,实现输出请求日志
- 一个before advice的切面,实现安全校验(这种其实更常见的是放在filter/intercept中)
1. case设计
现在就需要再加一个切面,依然以before advice作为case
@Aspect
@Component
public class AnotherOrderAspect {
@Before("@annotation(AnoDot)")
public void doBefore() {
System.out.println("in AnotherOrderAspect before!");
}
}
2. 测试
接下来看测试输出结果如下图
发现了一个有意思的事情了,AnotherOrderAspect
切面的输出,完全在OrderAspect
切面中所有的advice之前,接着我们再次尝试使用@Order
注解来试试,看下会怎样
@Order(0)
@Component
@Aspect
public class OrderAspect {
}
@Aspect
@Order(10)
@Component
public class AnotherOrderAspect {
}
如果顺序有关,我们预期的输出结果应该是
do AnoBefore > do Before > doXBefore > do AnotherOrderAspect before!
实际测试输出如下,和我们预期一致
3. 小结
从上面的测试来看,不同的切面,默认顺序实际上是根据切面的命令来的;
- A切面中的advice会优先B切面中同类型的advice
- 我们可以通过
Order
注解来解决不同切面的优先级问题,依然是值越小,优先级越高
IV. 不同切面,不同advice顺序
其实前面的case已经可以说明这个问题了,现在稍稍丰富一下AnotherOrderAspect
,看下结果
1. case设计
@Aspect
@Order(10)
@Component
public class AnotherOrderAspect {
@Before("@annotation(AnoDot)")
public void doBefore() {
System.out.println("in AnotherOrderAspect before!");
}
@After("@annotation(AnoDot)")
public void doAfter(JoinPoint joinPoint) {
System.out.println("do AnotherOrderAspect after!");
}
@AfterReturning(value = "@annotation(AnoDot)", returning = "ans")
public void doAfterReturning(JoinPoint joinPoint, String ans) {
System.out.println("do AnotherOrderAspect after return: " + ans);
}
@Around("@annotation(AnoDot)")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
try {
System.out.println("do AnotherOrderAspect in around before");
return joinPoint.proceed();
} finally {
System.out.println("do AnotherOrderAspect in around over!");
}
}
}
2. 测试
看下执行后的输出结果
假设A切面优先级高于B切面,那么我们执行先后顺序如下
V. 小结
本篇内容有点多,针对前面的测试以及结果分析,给出一个小结,方便直接获取最终的答案
1. 不同advice之间的优先级顺序
around 方法执行前代码 > before > 方法执行 > around方法执行后代码 > after > afterReturning/@AfterThrowing
2. 统一切面中相同advice
统一切面中,同类型的advice的优先级根据方法名决定,暂未找到可以控制优先级的使用方式
3. 不同切面优先级
不同切面优先级,推荐使用 @Order
注解来指定,数字越低,优先级越高
4. 不同切面advice执行顺序
优先级高的切面中的advice执行顺序会呈现包围优先级低的advice的情况,更直观的先后顺序,推荐看第四节的顺序图,更加清晰明了
VI. 其他
0. 项目
4 - 4.AOP实现一个日志插件(应用篇)
前面针对AOP的使用姿势和一些疑问进行了说明,这一篇则从应用的角度出发,看下AOP可以实现些什么样的效果
I. AOP实现日志拦截
1. 背景及目标
对于后端服务而言,一个日常的需求就是需要记录一些关键方法调用历史情况,用于分析接口的响应、问题定位排查等,属于比较常见的场景了
因此,我们希望可以针对某些接口,知道传入的参数时什么,谁调用的,返回了啥,耗时多少这些基本信息。显然这些属于公用的普适性需求,与方法本身的业务无关,如果直接在每个方法内部中加这个逻辑,就比较恶心了;为了最少的倾入性和通用性,正好可以使用AOP来实现这么一个功能
- 拦截目标方法的执行
- 打印请求参数,返回结果和执行时间到日志
2. 实现
这个属于比较aop的简单使用场景,因为需要知道返回结果,所有选择 around
或者 afterReturning
advice;此外需要统计方法执行耗时,这样就只能选中 around
了
首先我们支持自定义注解方式,先定义一个注解,只要这个方法上有这个注解,就拦截
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AnoDot {
}
其次,如果想更通用拦截指定包路径下的方法,可以如下定义PointCut;注意下面语句中的||
表示或,只有有一个满足即可
@Pointcut("execution(public * com.git.hui.boot.aop.demo.*.*(..)) || @annotation(AnoDot)")
public void pointcut() {
}
接着就是我们的advice实现了
@Around(value = "pointcut()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
Object res = null;
String req = null;
long start = System.currentTimeMillis();
try {
req = buildReqLog(proceedingJoinPoint);
res = proceedingJoinPoint.proceed();
return res;
} catch (Throwable e) {
res = "Un-Expect-Error";
throw e;
} finally {
long end = System.currentTimeMillis();
System.out.println(req + "" + JSON.toJSONString(res) + SPLIT_SYMBOL + (end - start));
}
}
private String buildReqLog(ProceedingJoinPoint joinPoint) {
// 目标对象
Object target = joinPoint.getTarget();
// 执行的方法
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
// 请求参数
Object[] args = joinPoint.getArgs();
StringBuilder builder = new StringBuilder(target.getClass().getName());
builder.append(SPLIT_SYMBOL).append(method.getName()).append(SPLIT_SYMBOL);
for (Object arg : args) {
builder.append(JSON.toJSONString(arg)).append(",");
}
return builder.substring(0, builder.length() - 1) + SPLIT_SYMBOL;
}
3. 测试
添加下测试代码,我们先创建两个bean
// 这个bean下的方法,演示注解拦截
// com.git.hui.boot.aop.anodemo.AnoDemo
@Component
public class AnoDemo {
@AnoDot
public String gen(String ans) {
return UUID.randomUUID() + "<>" + ans;
}
}
// 这个bean下的方法,演示正则方式的拦截
// 注意前面的参数为..,表示任意参数类型和个数的方法都会拦截
// com.git.hui.boot.aop.demo.PrintDemo
@Component
public class PrintDemo {
public String genRand(int seed, String suffix) {
return seed + UUID.randomUUID().toString() + suffix;
}
}
启动类如下
@SpringBootApplication
public class Application {
public Application(PrintDemo printDemo, AnoDemo anoDemo) {
System.out.println(printDemo.genRand(10, "--一灰灰Blog"));
System.out.println(anoDemo.gen("!23"));
}
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}
输出结果
com.git.hui.boot.aop.demo.PrintDemo|genRand|10,"--一灰灰Blog"|"10521195c0-3c2a-41d0-82f5-a41afad066b0--一灰灰Blog"|240
10521195c0-3c2a-41d0-82f5-a41afad066b0--一灰灰Blog
com.git.hui.boot.aop.anodemo.AnoDemo|gen|"!23"|"1e3438fe-e31f-4f75-8405-4ff7494f9c9c<>!23"|26
1e3438fe-e31f-4f75-8405-4ff7494f9c9c<>!23
II. 其他
0. 项目
5 - 5.接口上注解AOP拦截不到场景兼容
在Java的开发过程中,面向接口的编程可能是大家的常态,切面也是各位大佬使用Spring时,或多或少会使用的一项基本技能;结果这两个碰到一起,有意思的事情就发生了,接口方法上添加注解,面向注解的切面拦截,居然不生效
这就有点奇怪了啊,最开始遇到这个问题时,表示难以相信;事务注解也挺多是写在接口上的,好像也没有遇到这个问题(难道是也不生效,只是自己没有关注到?)
接下来我们好好瞅瞅,这到底是怎么个情况
I. 场景复现
这个场景复现相对而言比较简单了,一个接口,一个实现类;一个注解,一个切面完事
1. 项目环境
采用SpringBoot 2.2.1.RELEASE
+ IDEA
+ maven
进行开发
添加aop依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2. 复现case
声明一个注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AnoDot {
}
拦截切面,下面这段代码来自之前分享的博文 【基础系列】AOP实现一个日志插件(应用篇)
@Aspect
@Component
public class LogAspect {
private static final String SPLIT_SYMBOL = "|";
@Pointcut("execution(public * com.git.hui.boot.aop.demo.*.*(..)) || @annotation(AnoDot)")
public void pointcut() {
}
@Around(value = "pointcut()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
Object res = null;
String req = null;
long start = System.currentTimeMillis();
try {
req = buildReqLog(proceedingJoinPoint);
res = proceedingJoinPoint.proceed();
return res;
} catch (Throwable e) {
res = "Un-Expect-Error";
throw e;
} finally {
long end = System.currentTimeMillis();
System.out.println(req + "" + JSON.toJSONString(res) + SPLIT_SYMBOL + (end - start));
}
}
private String buildReqLog(ProceedingJoinPoint joinPoint) {
// 目标对象
Object target = joinPoint.getTarget();
// 执行的方法
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
// 请求参数
Object[] args = joinPoint.getArgs();
StringBuilder builder = new StringBuilder(target.getClass().getName());
builder.append(SPLIT_SYMBOL).append(method.getName()).append(SPLIT_SYMBOL);
for (Object arg : args) {
builder.append(JSON.toJSONString(arg)).append(",");
}
return builder.substring(0, builder.length() - 1) + SPLIT_SYMBOL;
}
}
然后定义一个接口与实现类,注意下面的两个方法,一个注解在接口上,一个注解在实现类上
public interface BaseApi {
@AnoDot
String print(String obj);
String print2(String obj);
}
@Component
public class BaseApiImpl implements BaseApi {
@Override
public String print(String obj) {
System.out.println("ano in interface:" + obj);
return "return:" + obj;
}
@AnoDot
@Override
public String print2(String obj) {
System.out.println("ano in impl:" + obj);
return "return:" + obj;
}
}
测试case
@SpringBootApplication
public class Application {
public Application(BaseApi baseApi) {
System.out.println(baseApi.print("hello world"));
System.out.println("-----------");
System.out.println(baseApi.print2("hello world"));
}
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}
执行后输出结果如下(有图有真相,别说我骗你🙃)
3. 事务注解测试
上面这个不生效,那我们通常写在接口上的事务注解,会生效么?
添加mysql操作的依赖
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
</dependencies>
数据库配置 application.properties
## DataSource
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2b8
spring.datasource.username=root
spring.datasource.password=
接下来就是我们的接口定义与实现
public interface TransApi {
@Transactional(rollbackFor = Exception.class)
boolean update(int id);
}
@Service
public class TransApiImpl implements TransApi {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public boolean update(int id) {
String sql = "replace into money (id, name, money) values (" + id + ", '事务测试', 200)";
jdbcTemplate.execute(sql);
Object ans = jdbcTemplate.queryForMap("select * from money where id = 111");
System.out.println(ans);
throw new RuntimeException("事务回滚");
}
}
注意上面的update方法,事务注解在接口上,接下来我们需要确认调用之后,是否会回滚
@SpringBootApplication
public class Application {
public Application(TransApiImpl transApi, JdbcTemplate jdbcTemplate) {
try {
transApi.update(111);
} catch (Exception e) {
System.out.println(e.getMessage());
}
System.out.println(jdbcTemplate.queryForList("select * from money where id=111"));
}
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}
回滚了,有木有!!!
果然是没有问题的,吓得我一身冷汗,这要是有问题,那就…(不敢想不敢想)
所以问题来了,为啥第一种方式不生效呢???
II. 接口注解切面拦截实现
暂且按下探寻究竟的欲望,先看下如果想让我们可以拦截接口上的注解,可以怎么做呢?
既然拦截不上,多半是因为子类没有继承父类的注解,所以在进行切点匹配时,匹配不到;既然如此,那就让它在匹配时,找下父类看有没有对应的注解
1. 自定义Pointcut
虽说是自定义,但也没有要求我们直接实现这个接口,我们选择StaticMethodMatcherPointcut
来补全逻辑
import org.springframework.core.annotation.AnnotatedElementUtils;
public static class LogPointCut extends StaticMethodMatcherPointcut {
@SneakyThrows
@Override
public boolean matches(Method method, Class<?> aClass) {
// 直接使用spring工具包,来获取method上的注解(会找父类上的注解)
return AnnotatedElementUtils.hasAnnotation(method, AnoDot.class);
}
}
接下来我们采用声明式来实现切面逻辑
2. 自定义Advice
这个advice就是我们需要执行的切面逻辑,和上面的日志输出差不多,区别在于参数不同
自定义advice实现自接口MethodInterceptor
,顶层接口是Advice
public static class LogAdvice implements MethodInterceptor {
private static final String SPLIT_SYMBOL = "|";
@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
Object res = null;
String req = null;
long start = System.currentTimeMillis();
try {
req = buildReqLog(methodInvocation);
res = methodInvocation.proceed();
return res;
} catch (Throwable e) {
res = "Un-Expect-Error";
throw e;
} finally {
long end = System.currentTimeMillis();
System.out.println("ExtendLogAspect:" + req + "" + JSON.toJSONString(res) + SPLIT_SYMBOL + (end - start));
}
}
private String buildReqLog(MethodInvocation joinPoint) {
// 目标对象
Object target = joinPoint.getThis();
// 执行的方法
Method method = joinPoint.getMethod();
// 请求参数
Object[] args = joinPoint.getArguments();
StringBuilder builder = new StringBuilder(target.getClass().getName());
builder.append(SPLIT_SYMBOL).append(method.getName()).append(SPLIT_SYMBOL);
for (Object arg : args) {
builder.append(JSON.toJSONString(arg)).append(",");
}
return builder.substring(0, builder.length() - 1) + SPLIT_SYMBOL;
}
}
3. 自定义Advisor
将上面自定义的切点pointcut与通知advice整合,实现我们的切面
public static class LogAdvisor extends AbstractBeanFactoryPointcutAdvisor {
@Setter
private Pointcut logPointCut;
@Override
public Pointcut getPointcut() {
return logPointCut;
}
}
4. 最后注册切面
说是注册,实际上就是声明为bean,丢到spring容器中而已
@Bean
public LogAdvisor init() {
LogAdvisor logAdvisor = new LogAdvisor();
// 自定义实现姿势
logAdvisor.setLogPointCut(new LogPointCut());
logAdvisor.setAdvice(new LogAdvice());
return logAdvisor;
}
然后再次执行上面的测试用例,输出如下
接口上的注解也被拦截了,但是最后一个耗时的输出,有点夸张了啊,采用上面这种方式,这个耗时有点夸张了啊,生产环境这么一搞,岂不是分分钟卷铺盖的节奏
- 可以借助 StopWatch 来查看到底是哪里的开销增加了这么多 (关于StopWatch的使用,下篇介绍)
- 单次执行的统计偏差问题,将上面的调用,执行一百遍之后,再看耗时,趋于平衡,如下图
5. 小结
到这里,我们实现了接口上注解的拦截,虽说解决了我们的需求,但是疑惑的地方依然没有答案
- 为啥接口上的注解拦截不到 ?
- 为啥事务注解,放在接口上可以生效,事务注解的实现机制是怎样的?
- 自定义的切点,可以配合我们的注解来玩么?
- 为什么首次执行时,耗时比较多;多次执行之后,则耗时趋于正常?
上面这几个问题,毫无意外,我也没有确切的答案,待我研究一番,后续再来分享
III. 其他
0. 项目
6 - 6.AOP结合SpEL实现日志输出的注意事项
使用AOP来打印日志大家一把都很熟悉了,最近在使用的过程中,发现了几个有意思的问题,一个是SpEL的解析,一个是参数的JSON格式输出
I. 项目环境
1. 项目依赖
本项目借助SpringBoot 2.2.1.RELEASE
+ maven 3.5.3
+ IDEA
进行开发
开一个web服务用于测试
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
II. AOP & SpEL
关于AOP与SpEL的知识点,之前都有过专门的介绍,这里做一个聚合,一个非常简单的日志输出切面,在需要打印日志的方法上,添加注解@Log
,这个注解中定义一个key
,作为日志输出的标记;key支持SpEL表达式
1. AOP切面
注解定义
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
String key();
}
切面逻辑
@Slf4j
@Aspect
@Component
public class AopAspect implements ApplicationContextAware {
private ExpressionParser parser = new SpelExpressionParser();
private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
@Around("@annotation(logAno)")
public Object around(ProceedingJoinPoint joinPoint, Log logAno) throws Throwable {
long start = System.currentTimeMillis();
String key = loadKey(logAno.key(), joinPoint);
try {
return joinPoint.proceed();
} finally {
log.info("key: {}, args: {}, cost: {}", key,
JSONObject.toJSONString(joinPoint.getArgs()),
System.currentTimeMillis() - start);
}
}
private String loadKey(String key, ProceedingJoinPoint joinPoint) {
if (key == null) {
return key;
}
StandardEvaluationContext context = new StandardEvaluationContext();
context.setBeanResolver(new BeanFactoryResolver(applicationContext));
String[] params = parameterNameDiscoverer.getParameterNames(((MethodSignature) joinPoint.getSignature()).getMethod());
Object[] args = joinPoint.getArgs();
for (int i = 0; i < args.length; i++) {
context.setVariable(params[i], args[i]);
}
return parser.parseExpression(key).getValue(context, String.class);
}
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
上面这个逻辑比较简单,和大家熟知的使用姿势没有太大的区别
2. StandardEvaluationContext安全问题
关于StandardEvaluationContext
的注入问题,有兴趣的可以查询一下相关文章;对于安全校验较高的,要求只能使用SimpleEvaluationContext
,使用它的话,SpEL的能力就被限制了
如加一个测试
@Data
@Accessors(chain = true)
public class DemoDo {
private String name;
private Integer age;
}
服务类
@Service
public class HelloService {
@Log(key = "#demo.getName()")
public String say(DemoDo demo, String prefix) {
return prefix + ":" + demo;
}
}
为了验证SimpleEvaluationContext
,我们修改一下上面的loadKeys
方法
private String loadKey(String key, ProceedingJoinPoint joinPoint) {
if (key == null) {
return key;
}
SimpleEvaluationContext context = new SimpleEvaluationContext.Builder().build();
String[] params = parameterNameDiscoverer.getParameterNames(((MethodSignature) joinPoint.getSignature()).getMethod());
Object[] args = joinPoint.getArgs();
for (int i = 0; i < args.length; i++) {
context.setVariable(params[i], args[i]);
}
return parser.parseExpression(key).getValue(context, String.class);
}
启动测试
@SpringBootApplication
public class Application {
public Application(HelloService helloService) {
helloService.say(new DemoDo().setName("一灰灰blog").setAge(18), "welcome");
}
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}
直接提示方法找不到!!!
3. gson序列化问题
上面的case中,使用的FastJson对传参进行序列化,接下来我们采用Gson来做序列化
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
然后新增一个特殊的方法
@Service
public class HelloService {
/**
* 字面量,注意用单引号包裹起来
* @param key
* @return
*/
@Log(key = "'yihuihuiblog'")
public String hello(String key, HelloService helloService) {
return key + "_" + helloService.say(new DemoDo().setName(key).setAge(10), "prefix");
}
}
注意上面方法的第二个参数,非常有意思的是,传参是自己的实例;再次执行
public Application(HelloService helloService) {
helloService.say(new DemoDo().setName("一灰灰blog").setAge(18), "welcome");
String ans = helloService.hello("一灰灰", helloService);
System.out.println(ans);
}
直接抛了异常
这就很尴尬了,一个输出日志的辅助工具,因为序列化直接导致接口不可用,这就不优雅了;而我们作为日志输出的切面,又是没有办法控制这个传参的,没办法要求使用的参数,一定能序列化,这里需要额外注意 (比较好的方式就是简单对象都实现toString,然后输出toString的结果;而不是json串)
4. 小结
虽然上面一大串的内容,总结下来,也就两点
- SpEL若采用的是
SimpleEvaluationContext
,那么注意spel的功能是减弱的,一些特性不支持 - 若将方法参数json序列化输出,那么需要注意某些类在序列化的过程中,可能会抛异常
(看到这里的小伙伴,不妨点个赞,顺手关注下微信公众号”一灰灰blog“,我的公众号已经寂寞的长草了😭)
III. 不能错过的源码和相关知识点
0. 项目
- 工程:https://github.com/liuyueyi/spring-boot-demo
- 源码: https://github.com/liuyueyi/spring-boot-demo/tree/master/spring-boot/014-spel-aop
AOP系列博文