【基础系列】AOP之高级使用技能

文章目录
  1. I. 高级技能
    1. 1. 注解拦截方式
    2. 2. 多个advice拦截
    3. 3. 嵌套拦截
      1. a. 调用方法不满足拦截规则,调用本类中其他满足拦截条件的方法
      2. b. 调用方法不满足拦截规则,调用其他类中满足拦截条件的方法
      3. c. 调用方法满足切面拦截条件,又调用其他满足切面拦截条件的方法
    4. 4. AOP拦截方法作用域
    5. 5. 小结
  2. II. 其他
    1. 0. 项目
    2. 1. 一灰灰Blog
    3. 2. 声明
    4. 3. 扫描关注

前面一篇博文 190301-SpringBoot基础篇AOP之基本使用姿势小结 介绍了aop的简单使用方式,在文章最后,抛出了几个问题待解决,本篇博文则将针对前面的问题,看下更多关于AOP的使用说明

I. 高级技能

1. 注解拦截方式

前面一文,主要介绍的是根据正则表达式来拦截对应的方法,接下来演示下如何通过注解的方式来拦截目标方法,实现也比较简单

首先创建注解

1
2
3
4
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AnoDot {
}

接着在目标方法上添加注解,这里借助前面博文中工程进行说明,新建一个com.git.hui.boot.aop.demo2.AnoDemoBean,注意这个包路径,是不会被前文的AnoAspect定义的Advice拦截的,这里新建一个包路径的目的就是为了尽可能的减少干扰项

1
2
3
4
5
6
7
8
9
10
11
12
@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中添加(不知道前文的也没关系,下面贴出相关的代码类,前文的类容与本节内容无关)

1
2
3
4
5
6
7
8
@Aspect
@Component
public class AnoAspect {
@Before("@annotation(AnoDot)")
public void anoBefore() {
System.out.println("AnoAspect ");
}
}

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@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中的逻辑

1
2
3
4
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

1
2
3
4
5
6
7
8
9
10
11
12
@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!");
}
}
}

对应的测试切面内容如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@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");
}
}

测试代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@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,都会被拦截到

1
2
3
4
5
6
7
8
9
10
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类中,新增一个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@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!");
}
}
}

对应的切面为

1
2
3
4
5
6
7
8
9
10
11
12
@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是否会被切面拦截住

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@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);
}
}

输出结果如下,没有切面的日志,表明这种场景下,不会被拦截

1
2
3
4
5
in randUUID start!
in genUUID before process!
in genUUID finally!
in randUUID finally!
>>>>>>>0c6a5ccf-30c0-4ac0-97f2-3dc063580f3d|1551522176035

b. 调用方法不满足拦截规则,调用其他类中满足拦截条件的方法

依然使用前面的例子进行说明,不过是稍稍改一下AnoDemoBean,调用第二节中的DemoBean的方法

DemoBean的代码如下

1
2
3
4
5
6
7
8
9
@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的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@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!");
}
}
}

测试代码和前面完全一致,接下来看下输出

1
2
3
4
5
6
7
8
9
10
11
12
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方法上添加注解,然后执行即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@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!");
}
}
}

输出结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
## 最外层的切面拦截的是 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作用域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@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方法的切面都走到了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-------- 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. 项目

1. 一灰灰Blog

一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

2. 声明

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

3. 扫描关注

一灰灰blog

QrCode

知识星球

goals


打赏 如果觉得我的文章对您有帮助,请随意打赏。
分享到