基于注解的AOP~

简介: 基于注解的AOP~

AOP概述:

AOP是一种设计思想,是软件设计领域中的面向切面编程。它是面向对象编程的一种补充和完善,它以通过预编译方式和运行其动态代理方式实现—>在不修改源代码的情况下给程序动态统一添加额外功能的一种技术

横切关注点:

从每个方法中抽取出来的同一类非核心业务[比如:日志功能],在同一个项目中,我们可以使用多个横切关注点对相关方法进行多个不同方面的增强

这个概念不是语法层面天然存在的,而是根据附加功能的逻辑上的需要:有十个附加功能,就有十个横切关注点

横切关注点是对于目标对象来说,抽取出来的非核心业务代码,将横切关注点封装到切面中,而在这个切面中,每个横切关注点都被表示为一个通知方法


通知:

每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫通知方法,也就是说横切关注点是实现额外功能的,该功能是通过通知方法实现的

前置通知:在被代理的目标方法前执行
返回通知:在被代理的目标方法成功结束后执行
异常通知:在被代理的目标方法异常结束后执行
后置通知:在被代理的目标方法最终结束后执行
环绕通知:使用try-catch-finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置

切面:封装通知方法的类

目标:被代理的目标对象

代理:向目标对象应用通知后创建的代理对象

那么AOP到底应该怎么做呢?

要想实现AOP,那么目标对象必须在此之前就已经存在的,我们通过分析之后,产生了目标对象,才需要通过AOP对该目标对象进行功能增强的,代理对象也是不需要我们去创建


那么AOP是做什么呢?


即为从目标对象中将非核心业务代码抽取出来,所抽取出的非核心业务代码叫做横切关注点,而将其代码放入的类叫做切面,切面中去封装横切关注点,每一个横切关注点都是一个方法,而这个方法被叫做通知,但非核心代码不仅仅是被抽取出来就OK了,我们将其抽取出来之后,是需要在目标对象中实现该功能以达到功能增强的目的,我们不仅需要将其抽取出来,还需要将其套到当前的目标对象上,代码是从哪里抽取的,就需要套哪里


连接点:

并不是语法定义的,而是一个纯逻辑概念,把方法排成一排,每一个横切位置看成x轴方向,把方法从上到下执行的顺序看成y轴,x轴与y轴的交叉点就是连接点

切入点:定位连接点的方式


每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物(从逻辑上来说)


如果把连接点看作数据库中的记录,那么切入点就是查询记录的SQL语句


Spring的AOP技术可以通过切入点定位到特定的连接点


切点通过org.springframework.aop.Pointcut接口进行描述,它使用类和方法作为连接点的查询条件


AOP的作用:

简化代码把方法中固定位置的重复的代码抽取出来让被抽取的方法更专注于自身的核心功能提高内聚性

代码增强把特定的功能封装到切面类中,哪里有需要,就往哪里套,被套用了切面逻辑的方法就被切面给增强了

基于注解的AOP:

上面我们说到AOP是一种思想,而该思想是通过Aspect注解层实现的

动态代理:JDK原生的实现方式,需要被代理的目标类必须实现接口,因为这个技术要求代理对象和目标对象实现相同的接口


cglib:通过继承被代理的目标类实现代理,不需要目标类实现接口


Aspectj:本质上是静态代理,将代理逻辑"织入"被代理的目标类编译得到的字节码文件,所以最终效果是动态的,weaver就是织入器,Spring只是借用了Aspectj中的注解


实现基于注解的AOP:

准备工作:

新建模块或者项目:

添加依赖:是在IOC所需依赖基础上再加入下面的依赖:也就是说aop的实现也是需要以IOC作为基础的

<!-- spring-aspects会帮我们传递过来aspectjweaver-->
<dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            <version>5.3.1</version>
</dependency>

加入上述依赖后,点击左边的刷新按钮即可!

打开spring_aop的dependency:

创建接口:

package spring_aop_annotation;
public interface Calculator {
    int add(int i,int j);
    int sub(int i,int j);
    int mul(int i,int j);
    int div(int i,int j);
}

创建其实现类:

package spring_aop_annotation;
@Component
public class CalculatorImpl implements  Calculator{
    @Override
    public int add(int i, int j) {
        int result=i+j;
        System.out.println("方法内部:result,"+result);
        return result;
    }
    @Override
    public int sub(int i, int j) {
        int result=i-j;
        System.out.println("方法内部:result,"+result);
        return result;
    }
    @Override
    public int mul(int i, int j) {
        int result=i*j;
        System.out.println("方法内部:result,"+result);
        return result;
    }
    @Override
    public int div(int i, int j) {
        int result=i/j;
        System.out.println("方法内部:result,"+result);
        return result;
    }
}

创建切面类并配置:

package spring_aop_annotation;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Component
@Aspect //将当前组件标识为切面
public class LoggerAspects {//切面类
}

创建spring-aop-annotation.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" xmlns:context="http://www.springframework.org/schema/c"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
   <!-- 切面类和目标类都需要交给IOC容器管理
   切面类必须通过@Aspects注解标识为一个切面
   -->
    <context:component-scan base-package="spring-aop_annotation"></context:component-scan>
    <!-- 启用 AspectJ 自动代理-->
    <aop:aspectj-autoproxy/>
</beans>

基于注解的AOP之前置通知:在目标对象方法执行之前执行

修改LoggerAspects中的代码如下所示:

package spring_aop_annotation;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Component
@Aspect 
//切面类--->需要通过指定的注解将方法标识为通知方法
public class LoggerAspects {
    //需要在切入点设置切入点表达式,Before用来标识前置通知的注解,该通知在目标方法执行之前执行
    @Before("execution(public int spring_aop_annotation.CalculatorImpl.add(int,int))")//假设将前置通知作用于add方法上
    public void beforeAdviceMethod(){
        System.out.println("LoggerAspects,前置通知");
    }
}

通过测试类测试:

import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import spring_aop_annotation.CalculatorImpl;
public class AopTest {
    @Test
    public void testAopAnnotation(){
        ApplicationContext ioc=new ClassPathXmlApplicationContext("spring-aop-annotation.xml");
        CalculatorImpl calculator=ioc.getBean(CalculatorImpl.class);
        calculator.add(4,10);
    }
}

输出如下所示:

报错的信息为没有找到该bean对象,但我们已经对该类加了注解并且也在XML文件中配置了扫描该类所在的包,那么为什么 IOC容器无法获取该对象呢?原因是:只要我们为目标对象创建了代理类,那么我们无法通过该目标对象直接去访问,而是要通过代理对象去访问,那么代理类是什么呢?我们好像似乎不知道,虽然我们不知道其代理类,但是我们知道它所实现的接口啊,早在之前我们就说过通过IOC容器获取,我们并不需要知道它的类型,只需要知道它所实现的接口或者继承的父类即可


修改测试类代码如下所示:

import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import spring_aop_annotation.Calculator;
public class AopTest {
    @Test
    public void testAopAnnotation(){
        ApplicationContext ioc=new ClassPathXmlApplicationContext("spring-aop-annotation.xml");
        Calculator calculator=ioc.getBean(Calculator.class);
        calculator.add(4,10);
    }
}

测试结果如下所示:

LoggerAspects,前置通知
方法内部:result,14

基于注解的AOP之切入点表达式的语法和重用以及获取连接点的信息:

这是我们上述所写的切入点表达式,其实很多地方不仅可以简化且还可以提高重用度:

@Before("execution(public int spring_aop_annotation.CalculatorImpl.add(int ,int ))")

首先来说简化的过程,其public int是可以直接使用 *代替的,表示任意的访问修饰符和返回值类型,且方法中的参数类型也是可以使用..代替的,表示任意的参数列表,当然类/包我们也可以使用*代替,表示该包下的所有子包或者所有类

@Before("execution(* spring_aop_annotation.CalculatorImpl.add(..))")

提高重用率体现在:

这里我们是指定了CalculatorImpl下的add方法,那么如果当我们在测试类中直接调用目标对象的其他方法,会调用成功吗?

一试便知!

calculator.sub(4,10);

测试如下:

方法内部:result,-6

与上述加法不同的是,这里这输出了关于计算的信息,而并没有输出前置通知中的信息,原因是,在切面类的Before注解中我们指定了是add方法,因此,当我们调用其他方法时,前置方法的信息并不会被输出,这也就说明上述写法的重用率并不高

如下所示,我们不去指定目标对象的类中的方法,而是使用*代替,表示类中任意的方法,如下所示:

@Before("execution(* spring_aop_annotation.CalculatorImpl.*(..))")

此时输出结果如下所示:

LoggerAspects,前置通知
方法内部:result,-6

在之前的动态代理中,目标对象方法执行之前,我们所做的工作是输出方法的方法名和参数列表,但我们上述所使用的这种方式该怎么获取其方法名和参数列表呢?

如下所示:

修改LoggerAspects类中的方法如下所示:

package spring_aop_annotation;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import java.util.Arrays;
@Component
@Aspect
public class LoggerAspects {
    @Before("execution(* spring_aop_annotation.CalculatorImpl.*(..))")
    //获取连接点信息---在通知方法的参数列表处,设置JoinPoint类型的参数,就可以获取连接点所对应方法的信息
    public void beforeAdviceMethod(JoinPoint joinPoint){
        //获取连接点所对应的方法的签名信息[方法的声明部分]
        Signature signature=joinPoint.getSignature();
        //获取连接点所对应方法的参数
        Object[] args=joinPoint.getArgs();
        System.out.println("LoggerAspects,方法:"+signature.getName()+",参数:"+ Arrays.toString(args));
    }
}

输出如下所示:

此时在输出结果中,我们成功获取到了参数名和参数列表

LoggerAspects,方法:sub,参数:[4, 10]
方法内部:result,-6

上述我们已经对其切入点表达式进行了简化,但这似乎还不是最简洁的,如果此时我们有了后置通知,那么在该后置通知的注解上依然要将该表达式再写一遍啊,对此我们可以进行切入点表达式的重用

切入点表达式的重用:

在LoggerAspects类中设置公用的切入点:

@Pointcut("execution(* spring_aop_annotation.CalculatorImpl.*(..))")
    public  void pointCut(){}

在其他方法的注解中直接引用即可

基于注解的AOP之各种通知的使用:

后置通知:在目标对象方法的finally子句中执行

在介绍通知方法时,我们提到,后置通知是在被代理的目标方法最终结束后执行,但方法结束完对应的有两个位置,如下所示:

那么后置通知到底是执行那个呢?

一试便知!

第一步:在LoggerAspects;类中的后置通知中输出信息

 System.out.println("LoggerAspects,方法:后置通知");

第二步:在测试类中人为创建异常

//调用除法运算-->被除数不能为0
calculator.div(4,0);

输出结果如下所示:

即使产生了异常,但后置通知中的信息依然被输出,由此可说明后置通知是在执行方法的finally子句中执行的

通过之前的学习我们知道finally是做善后处理,常用来关闭资源等,那么我们在其中只需要获取执行方法的方法名,再输出执行完毕的信息即可,如下所示:

@After("pointCut()")
    public void afterAdviceMethod(JoinPoint joinPoint){
        Signature signature=joinPoint.getSignature();
        System.out.println("LoggerAspects,方法:"+signature.getName()+",执行完毕");
}

输出如下所示:

返回通知:在目标对象方法返回值之后执行

在LoggerAspects类中创建返回通知,如下所示:

@AfterReturning("pointCut()")
    public void adviceReturnMethod(){//创建返回通知
        System.out.println("LoggerAspects,返回通知");
    }

测试类测试,输出结果如下:

测试结果并没有输出返回通知中的有关信息,原因是返回通知是执行方法成功被调用并且直接结束之后才会执行的,但这里有异常抛出啊,因此返回通知并没有被执行

修改测试类中的代码,如下所示:

calculator.div(4,1);

此时输出结果 如下所示:

LoggerAspects,方法:div,参数:[4, 1]
方法内部:result,4
LoggerAspects,返回通知
LoggerAspects,方法:div,执行完毕

返回通知就是在上述我们介绍后置通知时,方法结束完对应的有两个位置的第一个位置

既然返回通知是在执行方法成功被调用并且直接结束之后才会执行的,那么我们也可通过该通知获取返回值等,如下所示:

 //在返回通知中若要获取目标对象方法的返回值,只需要通过@AfterReturning的returning属性,就可以将通知方法的某个参数指定为接受目标对象方法的返回值的参数
    @AfterReturning(value = "pointCut()",returning = "result")
    public void adviceReturnMethod(JoinPoint joinPoint,Object result){  //创建返回通知
        Signature signature=joinPoint.getSignature();
        System.out.println("LoggerAspects,返回通知");
        System.out.println("LoggerAspects,方法:"+signature.getName()+",执行结果:"+result);
    }

测试结果如下所示:

LoggerAspects,方法:div,参数:[4, 1]
方法内部:result,4
LoggerAspects,返回通知
LoggerAspects,方法:div,执行结果:4
LoggerAspects,方法:div,执行完毕

异常通知:在目标对象方法的catch子句中执行

在LoggerAspects类中创建异常通知,如下所示:

@AfterThrowing("pointCut()")
    public void afterThrowingAdviceMethod(JoinPoint joinPoint){
        Signature signature=joinPoint.getSignature();
        System.out.println("LoggerAspects,方法:"+signature.getName()+",异常通知");
    }

输出如下所示,注意注意:若程序不发生异常,那么异常通知中的信息是不会被输出的

既然是通知,那么仅仅是输出通知是不够的,该通知应该输出具体的异常信息,如下所示:

 //在返回通知中若要获取目标对象方法的异常信息,只需要通过@AfterThrowing的throwing属性,
    // 就可以将通知方法的某个参数指定为接受目标对象方法的异常的参数
@AfterThrowing(value = "pointCut()",throwing = "ex")
    public void afterThrowingAdviceMethod(JoinPoint joinPoint,Throwable ex){
        Signature signature=joinPoint.getSignature();
        System.out.println("LoggerAspects,方法:"+signature.getName()+",异常:"+ex);
    }

输出如下所示:此时异常信息被获取到并且成功输出

关于四个通知的符号说明:

各种通知的执行顺序:

Spring版本5.3.x以前:

前置通知
目标操作
后置通知
返回通知或异常通知

Spring版本5.3.以后:

前置通知
目标操作
返回通知或异常通知
后置通知

基于注解的AOP之环绕通知:

使用@Around注解标识,使用try-catch-finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置

在LoggerAspects类中创建环绕通知,如下所示:

@Around("pointCut()")
//环绕方法的返回值必须和目标对象方法的返回值一致
    public Object aroundAdviceMethod(ProceedingJoinPoint proceedingJoinPoint) {//ProceedingJoinPoint可执行的连接点的对象
            Object result=null;
        try {
            System.out.println("环绕通知-->前置通知");
            result= proceedingJoinPoint.proceed();// 表示目标对象方法的执行,类似于动态代理中的invoke方法
            System.out.println("环绕通知-->返回通知");
        } catch (Throwable throwable) {
                throwable.printStackTrace();
                System.out.println("环绕通知-->异常通知");
        }finally{
                System.out.println("环绕通知-->后置通知");
        }
        return result;
    }

测试结果如下所示:

环绕通知-->前置通知
LoggerAspects,方法:div,参数:[3, 5]
方法内部:result,0
LoggerAspects,返回通知
LoggerAspects,方法:div,执行结果:0
LoggerAspects,方法:div,执行完毕
环绕通知-->返回通知
环绕通知-->后置通知

切面的优先级:

上述我们创建了LoggerAspects切面,它是关于日志功能的,此后我们还会有其他的切面,例如关于事务功能的,关于验证的切面等等

假设此时我们再创建一个关于针对计算器计算的切面,

package spring_aop_annotation;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Component
@Aspect
@Order
public class validateAspects {
    @Before("spring_aop_annotation.LoggerAspects.pointCut()")//访问公共的切入点表达式
    public void beforeMethod(){
        System.out.println("validateAspects,前置通知");
    }
}

输出如下所示:

不同切面之间是存在优先级的,而优先级我们可以通过@Order注解去设置

首先点开@Order注解源码如下所示:

@Order注解的属性值越小,优先级越高,而Integer.MAX_VALUE为切面的默认优先级

package org.springframework.core.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
@Documented
public @interface Order {
    int value() default Integer.MAX_VALUE;
}

设置validateAspects切面的优先级为1:

此时输出结果如下所示:

相关文章
|
19天前
|
Java API 数据安全/隐私保护
(工作经验)优雅实现接口权限校验控制:基于自定义注解、AOP与@ConditionalOnProperty配置开关的通用解决方案
(工作经验)优雅实现接口权限校验控制:基于自定义注解、AOP与@ConditionalOnProperty配置开关的通用解决方案
49 1
|
17天前
|
XML Java 数据格式
使用完全注解的方式进行AOP功能实现(@Aspect+@Configuration+@EnableAspectJAutoProxy+@ComponentScan)
本文介绍了如何使用Spring框架的注解方式实现AOP(面向切面编程)。当目标对象没有实现接口时,Spring会自动采用CGLIB库进行动态代理。文中详细解释了常用的AOP注解,如`@Aspect`、`@Pointcut`、`@Before`等,并提供了完整的示例代码,包括业务逻辑类`User`、配置类`SpringConfiguration`、切面类`LoggingAspect`以及测试类`TestAnnotationConfig`。通过这些示例,展示了如何在方法执行前后添加日志记录等切面逻辑。
47 2
使用完全注解的方式进行AOP功能实现(@Aspect+@Configuration+@EnableAspectJAutoProxy+@ComponentScan)
|
1月前
|
缓存 NoSQL Java
Springboot自定义注解+aop实现redis自动清除缓存功能
通过上述步骤,我们不仅实现了一个高度灵活的缓存管理机制,还保证了代码的整洁与可维护性。自定义注解与AOP的结合,让缓存清除逻辑与业务逻辑分离,便于未来的扩展和修改。这种设计模式非常适合需要频繁更新缓存的应用场景,大大提高了开发效率和系统的响应速度。
44 2
|
3月前
|
XML Java 数据库
Spring5入门到实战------10、操作术语解释--Aspectj注解开发实例。AOP切面编程的实际应用
这篇文章是Spring5框架的实战教程,详细解释了AOP的关键术语,包括连接点、切入点、通知、切面,并展示了如何使用AspectJ注解来开发AOP实例,包括切入点表达式的编写、增强方法的配置、代理对象的创建和优先级设置,以及如何通过注解方式实现完全的AOP配置。
|
5月前
|
XML 安全 Java
Spring高手之路19——Spring AOP注解指南
在本文中,我们将深入探索Spring AOP(面向切面编程)的核心概念及其在现代Spring应用中的实际应用。从基本的注解使用到复杂的切面配置,本文将一步步指导你如何利用Spring AOP提升代码的模块化,帮助你在Spring开发路上更进一步。
72 3
Spring高手之路19——Spring AOP注解指南
|
5月前
|
Java Spring 容器
基于注解的Aop开发,实现aop快速入门,基于注解的AOP开发
基于注解的Aop开发,实现aop快速入门,基于注解的AOP开发
|
4月前
|
分布式计算 Java MaxCompute
详解 Java 限流接口实现问题之在Spring框架中使用AOP来实现基于注解的限流问题如何解决
详解 Java 限流接口实现问题之在Spring框架中使用AOP来实现基于注解的限流问题如何解决
|
4月前
|
容器
springboot-自定义注解拦截ip aop和ioc
springboot-自定义注解拦截ip aop和ioc
|
5月前
|
XML Java 数据格式
Spring5系列学习文章分享---第三篇(AOP概念+原理+动态代理+术语+Aspect+操作案例(注解与配置方式))
Spring5系列学习文章分享---第三篇(AOP概念+原理+动态代理+术语+Aspect+操作案例(注解与配置方式))
48 0
|
5月前
|
Java Maven Spring
Spring中AOP最简单实例-@注解形式
Spring中AOP最简单实例-@注解形式
39 0