SpringBoot基础篇AOP之高级使用技能

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 前面一篇博文 190301-SpringBoot基础篇AOP之基本使用姿势小结 介绍了aop的简单使用方式,在文章最后,抛出了几个问题待解决,本篇博文则将针对前面的问题,看下更多关于AOP的使用说明

前面一篇博文 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的执行顺序



相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
4月前
|
SQL 监控 Java
在IDEA 、springboot中使用切面aop实现日志信息的记录到数据库
这篇文章介绍了如何在IDEA和Spring Boot中使用AOP技术实现日志信息的记录到数据库的详细步骤和代码示例。
在IDEA 、springboot中使用切面aop实现日志信息的记录到数据库
|
26天前
|
监控 安全 Java
什么是AOP?如何与Spring Boot一起使用?
什么是AOP?如何与Spring Boot一起使用?
53 5
|
1月前
|
JSON Java 数据库
SpringBoot项目使用AOP及自定义注解保存操作日志
SpringBoot项目使用AOP及自定义注解保存操作日志
53 1
|
5月前
|
Java Spring
在Spring Boot中使用AOP实现日志切面
在Spring Boot中使用AOP实现日志切面
|
2月前
|
Java Spring
【SpringBoot】技能一之修改端口与banner样式
【SpringBoot】技能一之修改端口与banner样式
33 5
|
2月前
|
缓存 NoSQL Java
Springboot自定义注解+aop实现redis自动清除缓存功能
通过上述步骤,我们不仅实现了一个高度灵活的缓存管理机制,还保证了代码的整洁与可维护性。自定义注解与AOP的结合,让缓存清除逻辑与业务逻辑分离,便于未来的扩展和修改。这种设计模式非常适合需要频繁更新缓存的应用场景,大大提高了开发效率和系统的响应速度。
88 2
|
4月前
|
Java Spring 容器
SpringBoot整合AOP实现打印方法执行时间切面
SpringBoot整合AOP实现打印方法执行时间切面
55 1
|
4月前
|
Java API Spring
Spring Boot 中的 AOP 处理
对 Spring Boot 中的切面 AOP 做了详细的讲解,主要介绍了 Spring Boot 中 AOP 的引入,常用注解的使用,参数的使用,以及常用 api 的介绍。AOP 在实际项目中很有用,对切面方法执行前后都可以根据具体的业务,做相应的预处理或者增强处理,同时也可以用作异常捕获处理,可以根据具体业务场景,合理去使用 AOP。
|
2月前
|
JavaScript 安全 Java
如何使用 Spring Boot 和 Ant Design Pro Vue 实现动态路由和菜单功能,快速搭建前后端分离的应用框架
本文介绍了如何使用 Spring Boot 和 Ant Design Pro Vue 实现动态路由和菜单功能,快速搭建前后端分离的应用框架。首先,确保开发环境已安装必要的工具,然后创建并配置 Spring Boot 项目,包括添加依赖和配置 Spring Security。接着,创建后端 API 和前端项目,配置动态路由和菜单。最后,运行项目并分享实践心得,包括版本兼容性、安全性、性能调优等方面。
186 1
|
1月前
|
JavaScript 安全 Java
如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个具有动态路由和菜单功能的前后端分离应用。
本文介绍了如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个具有动态路由和菜单功能的前后端分离应用。首先,创建并配置 Spring Boot 项目,实现后端 API;然后,使用 Ant Design Pro Vue 创建前端项目,配置动态路由和菜单。通过具体案例,展示了如何快速搭建高效、易维护的项目框架。
118 62