Spring学习笔记:基于注解的Sprig AOP配置的深入学习

发布于:2025-08-14 ⋅ 阅读:(11) ⋅ 点赞:(0)

在上一个在学习刘Java的关于Spring的AOP的学习中,算是对这个Spring的AOP的理解有一个深入。首先不是在对于这个AOP只有一个概念的理解,所谓一种实践方式,人家也提供了ApectJ这些工具或者说是规范。Spring AOP对于AOP是一种扩展和支持。有了XML肯定对应的还会有注解的形式的支持,对应各个标签会有对应的各个注解。

1 注解配置概述

如同IOC支持基于注解的两种基本配置方式,一个是常见的XML配置文件方式,另一个是项目中最常用到的注解方式。什么情况来采用什么配置方式,要区分情况:

XML配置方式,看起来更像“POJO”,xml的配置方式对于代码是“无侵入”的,配置文件和代码是分开的。而且由于是在配置文件中集中,因为集中所以要方便管理配置。

基于注解方式,代码和配置封装在一个类中,和代码在一起会更直观,方便理解,以及模块化。
使用注解还有一个好处就是可以在Spring AOP和Aspectj框架之间无缝切换,方便后续功能升级(如果需要超过Spring AOP的功能,虽然大概率不会出现)
Spring团队推荐基于注解方式。

PS:
Spring AOP 框架 AspectJ框架
这里说一下Spring AOP和AspectJ的AOP框架,两个是单独的两个框架,前面可能我的表达意思不对。

但是Spring AOP在表达式什么之类的使用方式是和AspectJ是相同的,但是在实现的基础上也不一样。Spring AOP是基于动态代理的实现的(JDK的反射/cglib),这个AspectJ框架在编译或者加载的时候代码织入来完成。

2 相关依赖

基于注解所需的依赖和基于XML所需的依赖一致,其中spring-context包括Spring IoC和Spring AOP等核心依赖。
而aspectjweaver则是AspectJ框架的依赖,Spring使用该依赖来解析AspectJ的切入点表达式语法,以及AOP的注解支持。

<properties>
    <spring-framework.version>5.2.8.RELEASE</spring-framework.version>
    <aspectjweaver>1.9.6</aspectjweaver>
</properties>
<dependencies>
    <!--spring 核心组件所需依赖-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>${spring-framework.version}</version>
        <scope>compile</scope>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
    <!--用于解析AspectJ的切入点表达式语法,以及AOP的注解支持-->
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>${aspectjweaver}</version>
    </dependency>
</dependencies>

3 开启AOP注解支持

想使用AOP注解,就需要开启注解支持。开启注解支持可以用XML和Java风格的配置启用注解。

3.1 基于XML的配置

引入AOP相关schema文件,加入AOP的命名空间< aop:aspectj-autoproxy/>标签就可以。Spring将会查找被@AspectJ注解标注的bean。
表明这个bean是一个切面的bean,然后会进行AOP的自动配置,比如里面的通知和切入点表达式解析。

关于代理对象的实现方式选择,对应的属性是proxy-class-target,默认的是false,也就是默认的实现方式是JDK代理方式,如果遇到的是非接口方式,JDK代理没有办法就采用cglib,这样的默认流程。如果选择的是true就是,采用cglib的动态代理方式。

<?xml version="1.0" encoding="UTF-8"?>
<!--加入AOP 相关schema-->
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/aop
       https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--开启注解自动配置-->
    <aop:aspectj-autoproxy/>
</beans>

3.2 基于Java的配置
和Ioc的注解支持,可以使用Java配置开启,AOP的注解支持同样可以使用Java注解配置开启,舍弃XML文件使用Java代码的方式。

/**
 * @author lx
 */
@Configuration
@ComponentScan("com.spring.aop.annotation")
//Java配置开启AOP注解支持
@EnableAspectJAutoProxy
public class MainConfiguration {
}

@Configuration和@ComponentScan,是IoC的Java注解配置,重要的是@EnableAspectJAutoProxy注解,该注解开启Spring AOP的注解自动配置。
它的proxyTargetClass属性用于指定使用哪一种代理,默认为false,表示先使用JDK代理,不符合要求再使用Cglib代理,改成true就表示使用CGlib代理。

4 AOP注解配置第一例

新建maven工程,引入依赖通过注解开启AOP注解支持。

5 @Aspect切面类

@Aspect注解对应XML配置中的< aop:aspect >标签
在开启Spring AOP注解自动配置支持,Spring容器将会在扫描包路径下(@ComponentScan配置的路径)扫描具有@Aspect注解的bean。该类被当作切面类并且用于自动配置Spring AOP。

@Aspect注解不会被Spring组件扫描工具自动检测,所以需要在这个切面类上添加一个@Component(对应的@Component注解扩展@Repository,@service和@controller这些注解也可以。),将这些注册到Spring容器。

切面类可以有自己的字段和方法,这些字段和方法可以包含有pointcut切入点,advice通知和introduction声明,可以进行绑定。

在Spring AOP,切面类本身不能做其他切面通知的目标,类上标注@Aspect注解之后,该类的bean将从AOP自动配的的bean中排除。所以切面类的方法不能被代理。

/**
 * 一个切面类
 *
 * @author lx
 */
@Component
@Aspect
public class AspectMethod1 {
    /**
     * 一个切入点,绑定到方法上,该切入点匹配“任何方法”
     */
    @Pointcut("execution(* *(..))")
    public void pointCut() {
    }

    /**
     * 一个前置通知,绑定到方法上
     */
    @Before("pointCut()")
    public void before() {
        System.out.println("----before advice----");
    }

    /**
     * 切面类的方法,无法被增强
     */
    public void aspectMethod1() {
        System.out.println("aspectMethod1");
    }
}
//-----------------
/**
 * 一个切面类
 *
 * @author lx
 */
@Component
@Aspect
public class AspectMethod2 {
    /**
     * 一个切入点,绑定到方法上,该切入点匹配“任何方法”
     */
    @Pointcut("execution(* *(..))")
    public void pointCut() {
    }

    /**
     * 一个前置通知,绑定到方法上
     */
    @Before("pointCut()")
    public void before() {
        System.out.println("----before advice----");
    }

    /**
     * 切面类的方法,无法被增强
     */
    public void aspectMethod2() {
        System.out.println("aspectMethod2");
    }
}

@Test
public void aspectMethod() {
    //1 获取容器
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(MainConfiguration.class);
    //2 获取切面类bean
    AspectMethod1 am1 = ac.getBean("aspectMethod1", AspectMethod1.class);
    AspectMethod2 am2 = ac.getBean("aspectMethod2", AspectMethod2.class);
    //2 调用切面类方法,并不会被代理
    am1.aspectMethod1();
    am2.aspectMethod2();
}

aspectMethod1
aspectMethod2

返回的结果,没有看到对应匹配的before这些输出,说明切面类的方法没有被加强。

6 @Pointcut切入点

@Pointcut对应着XML配置< aop:pointcut > 标签,都用来定义一个切入点,来控制什么地方,什么情况下执行通知,切入点表达式是一致的。

@Pointcut注解标注一个切面类的方法,该方法名是该切入点的名字。在通知中通过名字引用该切入点,也可以将多个切入点通过名字引用使用&&、||、!组合起来成为一个新的切入点,这是基于XML的配置所不具备的功能。

企业应用程序,模块化把所有切入点定义在一个切面类,该类专门用于存放切入点,其他切面类用于存放通知,切入点在所有切面类中是共享的。

/**
 1. 切面类,专门用于存放切入点定义
 */
@Aspect
@Component
public class AspectPointcut {

    /**
     * 任何方法的切入点
     */
    @Pointcut("execution(* *(..))")
    public void pointcut1() {
    }

    /**
     * AspectPointcutTarget类的target1方法的切入点
     */
    @Pointcut("execution(* *..AspectPointcutTarget.target1(..))")
    public void pointcut2() {
    }

    /**
     * AspectPointcutTarget类的任何方法的切入点
     */
    @Pointcut("within(*..AspectPointcutTarget)")
    public void pointcut3() {
    }

    /**
     * 第一个参数是String类型的任何方法的切入点
     */
    @Pointcut("args(String,..)")
    public void pointcut4() {
    }

    /**
     * 通过切入点名称组合切入点
     * <p>
     * AspectPointcutTarget类的第一个参数是String类型的任何方法的切入点
     */
    @Pointcut("pointcut3() && pointcut4()")
    public void pointcut5() {
    }

    //…………
}

7 注解配置通知

advice通知同样可以使用注解声明,并且绑定到一个方法,一共有5种通知注解。

对应的XML的注解:< aop:before>,< aop:after-returning >,< aop:after-throwing>,< aop:after>,< aop:around>这几个标签,

1 befoer :用于配置前置通知before advice,在切入点方法之前执行。
2 afterReturning:用于配置后置通知after-returning advice,在切入定之后执行
3 afterThrowing:用于配置后置通知after-throwing advice,切入点方法和后置通知抛出异常可能会执行
4 after:配置前置通知 after advice,无论切入点方法是否执行,都会在其后执行
5 around:用于配置环绕通知

/**
 * 切面类
 */
@Aspect
@Component
public class AspectAdvice {
    /**
     * 切入点,用于筛选通知将在哪些方法上执行
     */
    @Pointcut("execution(* AspectAdviceTarget.target())")
    public void pt() {
    }


    //五种通知

    /**
     * 前置通知
     * 可以通过名称引用定义好的切入点
     */
    @Before("pt()")
    public void before() {
        System.out.println("---before---");
    }

    /**
     * 后置通知
     * 也可以定义自己的切入点
     */
    @AfterReturning("execution(* AspectAdviceTarget.target())")
    public void afterReturning() {
        System.out.println("---afterReturning---");
    }

    /**
     * 异常通知
     */
    @AfterThrowing("pt()")
    public void afterThrowing() {
        System.out.println("---afterThrowing---");
    }

    /**
     * 最终通知
     */
    @After("pt()")
    public void after() {
        System.out.println("---after---");
    }

//    /**
//     * 环绕通知
//     *
//     * @param pjp 连接点
//     */
//    @Around("pt()")
//    public Object around(ProceedingJoinPoint pjp) {
//        System.out.println("---around---");
//        try {
//            System.out.println("前置通知");
//            //调用目标方法
//            Object proceed = pjp.proceed();
//            System.out.println("后置通知");
//            return proceed;
//        } catch (Throwable throwable) {
//            System.out.println("异常通知");
//            throwable.printStackTrace();
//            return null;
//        } finally {
//            System.out.println("最终通知");
//        }
//    }
}
//--------------
/**
 * 目标类
 *
 * @author lx
 */
@Component
public class AspectAdviceTarget {

    public int target() {
        System.out.println("target");
        //抛出一个异常
        //int i = 1 / 0;
        return 33;
    }
}

调用方法

@Test
public void aspectAdvice() {
    //1 获取容器
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(MainConfiguration.class);
    //2 获取目标类bean
    AspectAdviceTarget aspectAdviceTarget = ac.getBean("aspectAdviceTarget", AspectAdviceTarget.class);
    //2 调用目标方法
    aspectAdviceTarget.target();
}

返回的结果

---before---
target
---afterReturning---
---after---

几个AOP环绕通知都起效果

7.1 参数绑定

在基于XML的配置中,我们可以将各种参数,返回值,异常信息传递给通知方法,在基于注解可以像xml一样使用。

7.1.1 返回值和异常绑定

@AfterReturning注解中有一个returning属性,它的值是后置通知方法中的参数名,用来将切入点方法返回值绑定到参数上。
@AfterThrowing注解中的throwing属性,它的值是异常通知方法中的参数名,用来将前置通知、切入点方法、后置通知执行过程中抛出的异常绑定到该参数上。另外,这里的最后抛出的异常同样遵循后者优先的覆盖原则,详见基于XML配置的文章。

/**
 * 切面类
 * @author lx
 */
@Component
@Aspect
public class AspectArgument {

    @Pointcut("within(AspectArgumentTarget)")
    public void pt() {
    }

    /**
     * 后置通知,获取切入点方法的返回值作为参数
     *
     * @param date 切入点方法的返回值
     */
    @AfterReturning(value = "pt()", returning = "date")
    public void afterReturning(Date date) {
        System.out.println("----afterReturning----");
        System.out.println("Get the return value : " + date);
        System.out.println("----afterReturning----");
    }

    /**
     * 异常通知,获取抛出的异常作为参数
     *
     * @param e 前置通知、切入点方法、后置通知执行过程中抛出的异常
     */
    @AfterThrowing(value = "pt()", throwing = "e")
    public void afterThrowing(Exception e) {
        System.out.println("----afterThrowing----");
        System.out.println("Get the exception : " + Arrays.toString(e.getStackTrace()));
        System.out.println("----afterThrowing----");
    }
}
//------------------
@Component
public class AspectArgumentTarget {

    public Date target() {
        Date date = new Date();
        System.out.println("target return: " + date);
        //抛出一个异常
        //int i=1/0;
        return date;
    }
}
@Test
public void aspectArgument() {
    //1 获取容器
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(MainConfiguration.class);
    //2 获取目标类bean
    AspectArgumentTarget aspectArgumentTarget = ac.getBean("aspectArgumentTarget", AspectArgumentTarget.class);
    //2 调用目标方法
    Date target = aspectArgumentTarget.target();
    System.out.println(" returned value: "+target);
}

正常情况下:

target return: Fri Sep 18 00:00:41 CST 2020
----afterReturning----
Get the return value : Fri Sep 18 00:00:41 CST 2020
----afterReturning----
 returned value: Fri Sep 18 00:00:41 CST 2020

有异常的情况下:

target return: Fri Sep 18 00:01:33 CST 2020
----afterThrowing----
Get the exception : [com.spring.aop.annotation.aspectargument.AspectArgumentTarget.target(AspectArgumentTarget.java:14)//……………………
----afterThrowing----
//最终抛出的异常
java.lang.ArithmeticException: / by zero
at com.spring.aop.annotation.aspectargument.AspectArgumentTarget.target(AspectArgumentTarget.java:14)
//……………………
7.1.2 其他参数绑定

其他参数绑定,需要使用切入点表达式的语法,包括切入点方法参数args、代理对象this、目标对象target,和相关注解(@within, @target, @annotation, 和 @args)都可以绑定到通知方法的参数中。这些内容我们在基于XML的配置部分已经详细讲解了。

在通知方法的第一个参数同样可以默认设置一个JoinPoint,用来获取当前连接点的信息。
  绑定参数的时候,会通过参数名字注入参数,如果类型不兼容,那么将不会执行该通知。预定义的切入点,如果要绑定参数,那么切入点绑定的方法同样必须有这些参数,并且参数名字同样要对应。

7.2 通知顺序

同一个连接点方法绑定了多个统一类型的通知,有时需要制定执行顺序,在基于XML配置文件我们可以通过order属性指定通知顺序,注解也可以指定执行顺序。

以下是一个代码样例

/**
 * order测试
 * order越小的切面,其内部定义的前置通知越先执行,后置通知越后执行。
 * 相同的order的切面则无法确定它们内部的通知执行顺序,同一个切面内的相同类型的通知也无法确定执行顺序。
 */
@Component
public class AspectOrder {
    /**
     * 默认order为Integer.MAX_VALUE
     */
    @Component
    @Aspect
    public static class AspectOrder1 {

        @Pointcut("execution(* *..AspectOrderTarget.*())")
        public void pt() {
        }

        @Before("pt()")
        public void before() {
            System.out.println("-----Before advice 1-----");
        }

        @AfterReturning("pt()")
        public void afterReturning() {
            System.out.println("-----afterReturning advice 1-----");
        }

        @After("pt()")
        public void after() {
            System.out.println("-----after advice 1-----");
        }
    }

    /**
     * 使用注解
     */
    @Component
    @Aspect
    @Order(Integer.MAX_VALUE - 2)
    public static class AspectOrder2 {

        @Before("com.spring.aop.annotation.aspectorder.AspectOrder.AspectOrder1.pt()")
        public void before() {
            System.out.println("-----Before advice 2-----");
        }

        @AfterReturning("com.spring.aop.annotation.aspectorder.AspectOrder.AspectOrder1.pt()")
        public void afterReturning() {
            System.out.println("-----afterReturning advice 2-----");
        }

        @After("com.spring.aop.annotation.aspectorder.AspectOrder.AspectOrder1.pt()")
        public void after() {
            System.out.println("-----after advice 2-----");
        }
    }

    /**
     * 实现Ordered接口
     */
    @Component
    @Aspect
    public static class AspectOrder3 implements Ordered {

        @Before("com.spring.aop.annotation.aspectorder.AspectOrder.AspectOrder1.pt()")
        public void before() {
            System.out.println("-----Before advice 3-----");
        }

        @AfterReturning("com.spring.aop.annotation.aspectorder.AspectOrder.AspectOrder1.pt()")
        public void afterReturning() {
            System.out.println("-----afterReturning advice 3-----");
        }

        @After("com.spring.aop.annotation.aspectorder.AspectOrder.AspectOrder1.pt()")
        public void after() {
            System.out.println("-----after advice 3-----");
        }

        /**
         * @return 获取order
         */
        public int getOrder() {
            return Integer.MAX_VALUE - 1;
        }
    }
}

//----------------

/**
 * @author lx
 */
@Component
public class AspectOrderTarget {

    public void target() {
        System.out.println("target");
    }
}

8 DeclareParents Intrduction

@DeclareParents 对应基于XML配置中的< aop:declare-parents >标签,即Introduction(引介)。

在不修改代码的前提下,Intrdcuction可以在运行期为类动态添加一些额外的方法或者属性。

@DeclareParents绑定到一个切面类的属性上,该属性的类型就是新增的功能接口,实际上Spring创建的目标类的代理类会同时实现这个接口(无论是JDK还是CGlib),因此可以多一些功能。@DeclareParents还有两个属性:

value:需要增强的类扫描路径,该路径下的被Spring管理的bean被增强
defaultImpl:一个实现了新增的功能接口的类,作为新的增强方法的默认实现类

9 总结

基于注解还是基于XML的都是差不多,只是标签换成了注解。(一个项目可以在同一个工程可以同时使用两种方式来完成AOP,但一般不需要这么混着来。)

在Spring程序中很多地方隐式使用AOP代理机制,比如对于@Configuration标注的类,将默认创建一个cglib生成一个代理类,这样能代理他内部@Bean方法,当@Bean方法被调用时,将会返回同一个代理对象(单例)。这是其他注解@Component注解这些所不具备的特性。

还有像Spring的自动事务管理,传播行为@Transaction注解等也是AOP代理机制来实现的。


网站公告

今日签到

点亮在社区的每一天
去签到