Spring-AOP

693 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第20天,点击查看活动详情

AOP概念

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

相关术语

横切关注点(日志):

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

通知 :

每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法叫通知方法

切面:

用于封装横切关注点的类(每个横切关注点都表示为一个通知方法)

注: 我们要把横切关注点封装到切面中,而在这个切面中每一个横切关注点都表示一个通知方法

目标

被代理的目标对象(加减乘除功能)

连接点:

表示横切关注点抽出来的位置

切入点:

定位连接点的方式

每一个横切关注点也就是非核心业务方法都会被抽出到切面中,而切面中的每个横切关注点表示一个通知方法,通过切入点就是将通知方法放到连接点处

通知分类

  • 前置通知:在被代理的目标方法前执行
  • 返回通知:在被代理的目标方法成功结束后执行
  • 异常通知:在被代理的目标方法异常结束后执行
  • 后置通知:在被代理的目标方法最终结束后执行
  • 环绕通知:目标方法的前后都可以执行某些代码,用于控制目标方法

AOP作用

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

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

注: AOP依赖于IOC而存在

基于注解的AOP

前置通知

配置文件

  • 切面类和目标类都需要交给IOC容器管理
  • 切面类必须通过@Aspect注解标识为一个切面在spring的配置文件中设置<aop:aspectj-autoproxy/>开启基于注解的AOP
<context:component-scan base-package="com.sentiment.aop"></context:component-scan>
<!--开启基于注解的AOP-->
<aop:aspectj-autoproxy/>

测试类

@Component
@Aspect
public class LogAspect {
    @Before("execution(public int com.sentiment.aop.CalculatorImpl.add(int ,int))")
    public void beforeAdvicedMethdo(){
        System.out.println("前置通知");
    }
}

切入点表达式

  • bean表达式

    bean(bean的id)//没有引号
    
  • within表达式

    //只拦截具体包下的具体类
    within(com.Sentiment.service.User)
    //拦截具体包下的所有类
    within(com.sentiment.service.*)
    //拦截具体包下的所有包类
    within(com.sentiment.service..*)
    //拦截com.任意包.service包下的所有包类
    within(com.*.service..*)
    
  • execution表达式

语法:execution(返回值类型 包名.类名.方法名(参数列表))

//拦截返回值任意的具体方法
execution(* com.sentiment.service.UserServiceImpl.addUser())
//拦截返回值任意,参数列表任意,具体包service所有子包与子类的所有方法
execution(* com.sentiment.service..*.*(..))
//拦截返回值任意,参数列表为两个int类型,具体包service所有子包与子类的add方法
execution(* com.sentiment.service..*.add(int,int))
  • @annoation表达式

定义一个注解类,在需要扩展的方法上加注解

//对被有该注解标明的方法有效(里面填定义注解的位置)
@annotation(com.sentiment.anno.注解)

简化操作

在上边定义了一个add方法的前置通知,但是此时"减乘除"都没有设置,若使用刚才的方式则需再定义三个,代码重复量过高,所以就可以用表达式进行简化

第一个*表示任意的访问修饰符和返回值类型

第二个*表示类中任意的方法

.. 表示任意的参数列表

@Before("execution(* com.sentiment.aop.CalculatorImpl.*(..))")

除此外类的地方也可以用*,表示包下所有的类

重用切入点

此时只定义了前置操作,若在定义后置通知,异常通知等,就会导致execution(* com.sentiment.aop.CalculatorImpl.*(..))出现反复重写问题,所以又引入了一个注解@Pointcut

此时After后边只需要填写对应的pointCut方法即可

@Pointcut("execution(* com.sentiment.aop.CalculatorImpl.*(..))")
public void pointCut(){}
​
@After("pointCut()")
public void after(){
    System.out.println("后置通知");
}

获取连接点信息

在通知方法的参数位置,设置JoinPoint类型的参数,就可以获取连接点对应方法的信息

测试

@Before("pointCut()")
public void beforeAdvicedMethdo(JoinPoint joinPoint){
    //获取连接点对应方法的签名信息
    Signature signature = joinPoint.getSignature();
    //获取连接点对应方法的参数
    Object[] args = joinPoint.getArgs();
    System.out.println("前置通知,方法:"+signature.getName()+"参数:"+ Arrays.toString(args));
}

结果

前置通知,方法:mul参数:[1, 2]
方法内部,result:2

常见通知方式

先用try语句理解一下各个通知的接入点

try {
    //前置通知@Before
    目标方法执行语句..........
    //返回通知@AfterReturning
   }catch(exception e){
    //异常通知@AfterThrowing
} finally{
    //后置通知@After
}

后置通知

@After

接入点相当于finally位置,无论执行是否异常都会执行

image-20220923101636492.png

返回通知

@AfterReturning

接入点在目标方法语句后,若出现异常则不会执行,另外该注释中有一个参数returning,作为为目标函数的返回结果

@AfterReturning(value = "pointCut()",returning = "result")
public void afterReturning(JoinPoint joinPoint,Object result){
    Signature signature = joinPoint.getSignature();
    System.out.println("返回通知,方法:"+signature.getName()+",结果:"+result);
}

异常通知

@AfterThrowing

接入点在catch语句中,若出现异常则会执行,另外该注释中有一个参数throwing,作为为目标函数的异常结果

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

环绕通知

@Around("pointCut()")
public Object around(ProceedingJoinPoint joinPoint){
    Object result=null;
    try {
        System.out.println("环绕通知-->前置通知");
         result = joinPoint.proceed();
        System.out.println("环绕通知-->后置通知");
    }catch (Throwable throwable){
        System.out.println("环绕通知-->异常通知");
    }finally {
        System.out.println("环绕通知-->后置通知");
    }
    return result;
}

切面优先级

再添加一个AOP类,加上前置通知

public class OrderAspect {
    @Before("com.sentiment.aop.LogAspect.pointCut()")
    public void before(){
        System.out.println("前置通知:Order");
    }
}

此时在程序执行后发现,该通知的执行在LogAspect类的前置通知后,所有就引入了一个注解@Order来设置切面执行的优先级

image-20220924121712166.png 此时发现Order的前置通知被执行, @Order填写一个int值即可,值越小优先级越高,而默认值为int类型的最大值2147483647,所以上边值随便给个比这个小的即可

image-20220924121807241.png

基于xml的AOP

将前边的AOP注解都去掉后,可以基于xml实现AOP管理

配置文件

    <context:component-scan base-package="com.sentiment.aop.xml"></context:component-scan>
​
    <aop:config>
        <aop:pointcut id="pointCut" expression="execution(* com.sentiment.aop.xml.CalculatorImpl.*(..))"/>
        <aop:aspect ref="logAspect" >
            <aop:before method="beforeAdvicedMethdo" pointcut-ref="pointCut"></aop:before>
            <aop:after method="after" pointcut-ref="pointCut"></aop:after>
            <aop:after-returning method="afterReturning" returning="result" pointcut-ref="pointCut"></aop:after-returning>
            <aop:after-throwing method="afterThrowing" throwing="e" pointcut-ref="pointCut"></aop:after-throwing>
        </aop:aspect>
        
        <!-- 同样可以通过order修改优先级-->
        <aop:aspect ref="orderAspect" order="1">
            <aop:before method="before" pointcut-ref="pointCut"></aop:before>
        </aop:aspect>
    </aop:config>

测试

@Test
public void test(){
    ClassPathXmlApplicationContext ioc = new ClassPathXmlApplicationContext("aop-xml.xml");
    Calculator bean = ioc.getBean(Calculator.class);
    bean.add(1,0);
}