Spring AOP官方文档学习笔记之基于注解的Spring AOP

Posted shame11

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring AOP官方文档学习笔记之基于注解的Spring AOP相关的知识,希望对你有一定的参考价值。

1.@Aspect注解

(1) @Aspect注解用于声明一个切面类,我们可在该类中来自定义切面,早在Spring之前,AspectJ框架中就已经存在了这么一个注解,而Spring为了提供统一的注解风格,因此采用了和AspectJ框架相同的注解方式,这便是@Aspect注解的由来,换句话说,在Spring想做AOP框架之前,AspectJ AOP框架就已经很火了,而直接把AspectJ搬过来又不现实,因此,Spring想了一个折中的方案,即只使用AspectJ框架的声明,写法和定义方式(比如@Aspect注解),而底层由Spring自己实现,这样,就避免了我们程序员从AspectJ AOP切换到Spring AOP后,还要再去学一套新的写法了,也正因为如此,如果想要使用Spring AOP,就必须依赖aspectjweaver.jar包(不然谁来提供写法和定义方式),我们可以通过maven进行导入,如下

<!-- 添加对AspectJ框架的依赖 -->
<dependency>
  <groupId>org.aspectj</groupId>
  <artifactId>aspectjrt</artifactId>
  <version>1.9.5</version>
</dependency>

<!-- 除了上面的方式外,也可以直接使用spring-aspects依赖,它里面包含了对AspectJ的依赖 -->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-aspects</artifactId>
  <version>$spring.framework.version</version>
</dependency>

(2) 同时还需使用@EnableAspectJAutoProxy注解来开启Spring对于AspectJ注解的支持,如下

@Configuration
@EnableAspectJAutoProxy
public class Config 


如果是基于xml的配置,可通过如下标签进行开启

<aop:aspectj-autoproxy/>

2.自定义一个切面类

(1) 在基于注解的配置下,除了使用@Aspect注解外,还需要声明该切面是一个bean,否则,spring在扫描过程中是会忽略掉这个类的,如下

@Aspect
@Component
public class Logger 


(2) 对上面的例子,基于xml配置的写法如下

@Aspect
public class Logger 



<!-- xml配置文件中 -->
<beans ...>
    <!-- 无论何种配置方式,不要忘了将切面类注册为spring的一个bean -->
    <bean id="logger" class="cn.example.spring.boke.Logger"></bean>
</beans>

(3) 由@Aspect注解标注的类,称之为切面类,与普通的类一样,都有成员方法与成员变量,不同的是,切面类还可以包含连接点,通知,引介等与AOP有关的东西

(4) 切面不能再被增强,如果想拿一个切面来增强另一个切面,是不可能的,Spring会将切面类从自动代理(auto-proxying)中排除

3.自定义一个切入点

(1) Spring AOP中的切入点目前只可能是bean中的方法,而对于一个普通类中的方法,是不可能成为切入点的,在Spring中,声明一个切入点主要包括两个部分:一个切入点签名以及一个切入点表达式,如下

//如下定义了一个叫做anyExampleAMethod的切入点,这个切入点会匹配cn.example.spring.boke包下的ExampleA类中的任何方法
//其中,(1)就代表的是切入点表达式,(2)就代表的是切入点签名,注意,这个签名的返回值必须是void
@Pointcut("execution(* cn.example.spring.boke.ExampleA.*(..))")      //(1)
public void anyExampleAMethod()                                    //(2)

(2) Spring AOP的切入点表达式中,支持如下等切入点标识符

  • execution:最为常用,用于匹配某个包,某个类中的方法

  • within:进行类型匹配,用于匹配某个包下所有类的所有方法或某个指定类中的所有方法,如下

//指定了within的类型,这个切入点会匹配cn.example.spring.boke包下ExampleA类中的任何方法
@Pointcut("within(cn.example.spring.boke.ExampleA)")
public void withinDesignator()
  • this:进行类型匹配,用于匹配生成的代理对象的类型是否为指定类型,如下
//此前我们提到过,Spring AOP中的底层实现分为jdk动态代理和cglib动态代理,jdk动态代理基于接口,要求目标对象必须实现某个接口,而cglib动态代理基于继承,因此不同的实现方式下,导致Spring生成的代理对象的类型可能不同,这就是this标识符的基础
//首先定义一个接口
public interface Parent 
    void register();

    void sendEmail();


//让我们的ExampleA类,实现这个接口
@Component
public class ExampleA implements Parent

    public void register() 

    

    public void sendEmail() 

    


//设置@EnableAspectJAutoProxy注解中的proxyTargetClass属性值为false,表示使用jdk动态代理,为true,表示使用cglib动态代理,默认值为false,不过我们这里显式的声明出来
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = false)
@ComponentScan(basePackages = "cn.example.spring.boke")
public class Config 



//切面类,在其中声明一个this标识符,并指定类型为ExampleA
@Aspect
@Component
public class Logger 
    /**
     * this标识符,进行类型匹配,用于匹配代理对象的类型是否为指定类型
     */
    @Pointcut(value = "this(cn.example.spring.boke.ExampleA)")
    public void thisDesignator()

    @Around(value = "thisDesignator()")
    public void around(ProceedingJoinPoint joinPoint) throws Throwable 
        System.out.println(new Date() + " 开始执行...");
        joinPoint.proceed();
        System.out.println(new Date() + " 结束执行...");
    


//执行如下打印方法,可见通知未被执行,原因就是因为我们使用了jdk动态代理,Spring为我们生成的代理对象继承自jdk中的Proxy类并实现了Parent接口,它不属于ExampleA类型,自然而然切入点匹配失败,我们的通知未被执行
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Config.class);
ctx.getBean(Parent.class).register();
ctx.getBean(Parent.class).sendEmail();

//打印一下系统中代理对象的类型是否为ExampleA,结果为false
System.out.println(ctx.getBean(Parent.class) instanceof ExampleA);


//为了能进行匹配,我们可以将@EnableAspectJAutoProxy中的proxyTargetClass属性设置为true,使用cglib动态代理,这时再执行上面的打印方法,通知就会被执行了,原因就是因为使用了cglib动态代理后,Spring为我们生成的代理对象是继承自ExampleA,当然属于ExampleA类型,因此通知会被执行
@EnableAspectJAutoProxy(proxyTargetClass = true)
  • target:进行类型匹配,用于匹配目标对象的类型是否为指定类型,跟上面的this类似

  • args:进行方法参数匹配,用于匹配方法的参数类型是否为指定类型,如下

//ExampleA中的register方法的参数为String
@Component
public class ExampleA

    public void register(String name) 

    

    public void sendEmail() 

    


@Aspect
@Component
public class Logger 
    /**
     * 指定了args参数的类型为String,因此只会与ExampleA中的register方法匹配
     */
    @Pointcut(value = "args(java.lang.String)")
    public void argsDesignator() 

    @Around(value = "argsDesignator()")
    public void around(ProceedingJoinPoint joinPoint) throws Throwable 
        System.out.println(new Date() + " 开始执行...");
        joinPoint.proceed();
        System.out.println(new Date() + " 结束执行...");
    

  • @target:用于匹配目标对象的类上有没有标注指定注解

  • @args:用于匹配方法的参数的所属类上有没有标注指定注解

  • @within:用于匹配某个类上有没有标注指定注解

  • @annotation:最常用,用于匹配某个方法上有没有标注指定注解

(3) Spring的AOP是基于代理实现的,因此,在目标对象中进行内部调用是不会被拦截的(即this指针会导致AOP失效问题),此外,对于jdk动态代理,只能拦截public方法,而对于cglib动态代理,会拦截public和protected方法(package-visible 方法在配置后也能被拦截)

(4) Spring AOP还提供了一个PCD bean,用于按照bean的名称进行切入,它是Spring AOP独有的,如下

//匹配所有beanName以A结尾的bean
@Pointcut("bean(*A)")
public void pcd() 

4.组合切入点表达式

(1) 可以通过 &&,|| 和 !来组合切入点表达式,如下

//切入所有public方法
@Pointcut("execution(public * *(..))")
public void allPublicMethod() 

//切入boke包下所有类中的所有方法
@Pointcut("within(cn.example.spring.boke.*)")
public void methodInBokePackage() 

//使用 && 操作符,将上面两个切入点表达式组合起来,即切入boke包下所有类中的所有public方法
@Pointcut("allPublicMethod() && methodInBokePackage()")
public void allPublicMethodInBokePackage() 

5.常见切入点表达式例子

(1)在实际工作中,我们的切入点表达式的通常形式为:execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?),其中ret-type-pattern表示一个方法的返回类型, 用 * 号可以代表任何类型; name-pattern表示方法名称,用 * 可以进行全部或部分名称匹配; param-pattern表示方法参数,其中用()代表无参方法,用(..)代表任何数量的参数(0个或多个),用()代表1个参数,(, String)代表有两个参数,第一个参数可以是任何类型,而第二个参数只能是String类型; 除此之外,其他带有 ? 的都是选填项,可不填写

(2)常见例子

//匹配任意public方法
 execution(public * *(..))

//匹配任意名称以set开头的方法
execution(* set*(..))

//匹配com.xyz.service包下,AccountService类下的任意方法
execution(* com.xyz.service.AccountService.*(..))

//匹配com.xyz.service包下,任意类下的任意方法
execution(* com.xyz.service.*.*(..))

//匹配com.xyz.service包及其子包下,任意类下的任意方法
execution(* com.xyz.service..*.*(..))

//匹配com.xyz.service包下,任意类下的任意方法
within(com.xyz.service.*)

//匹配com.xyz.service包及其子包下,任意类下的任意方法
within(com.xyz.service..*)

//匹配代理对象的类型为AccountService的类下的任意方法
this(com.xyz.service.AccountService)

//匹配目标对象的类型为AccountService的类下的任意方法
target(com.xyz.service.AccountService)

//匹配方法参数只有一个且参数类型为Serializable的方法,注意它与execution(* *(java.io.Serializable))的一点区别:execution这个例子只能匹配参数类型为Serializable的方法,如果说某个方法的参数类型是Serializable的子类,是不会匹配的,而下面args这个例子可以匹配参数类型为Serializable或其子类的方法
args(java.io.Serializable)

//匹配标注了@Transactional注解的目标对象中的任意方法
@target(org.springframework.transaction.annotation.Transactional)

//匹配标注了@Transactional注解的类中的任意方法
@within(org.springframework.transaction.annotation.Transactional)

//匹配标注了@Transactional注解的任意方法
@annotation(org.springframework.transaction.annotation.Transactional)

//匹配方法的参数有且只有一个且该参数的所属类上标注了@Classified注解的任意方法
@args(com.xyz.security.Classified)

//匹配beanName为tradeService的bean中的任意方法
bean(tradeService)

//匹配所有以Service作为beanName结尾的bean中的任意方法
bean(*Service)

6.编写良好的pointcuts

(1)Spring将切入点标识符分为3大类,分别为:

  • Kinded:类型标识符,如execution, get, set, call等,它们都是根据类型进行选择,比如execution选择的都是可执行方法这一类型,其中除了execution,其他的都是AspectJ框架提供的
  • Scoping:范围标识符,如within;
  • Contextual:上下文标识符,如this, target和@annotation,它们都是根据方法所处的环境(比如在哪个类中)进行选择
    Spring建议一个良好的切入点表达式应该至少包括前两种类型(kinded和scoping,在这两种标识符中scoping又特别重要,因为它的匹配速度非常快,可以快速的排除掉那些不应该被处理的方法),此外在有根据上下文环境的需求时,可以包括contextual标识符

7.声明一个通知

(1)在前面已经提及过通知,它是增强的逻辑,与切入点相关联,会在切入点执行前或执行后执行,在Spring中总共分为5大类,如下

  • Before Advice:使用@Before注解可定义前置通知,它会在切入点执行之前执行
@Aspect
@Component
public class Logger 

    @Before(value = "execution(* cn.example.spring.boke.ExampleA.*(..))")
    public void before() 
        //...
    

  • After Returning Advice:使用@AfterReturning注解可定义返回通知,它会在切入点"正常"执行之后执行
//一个普通的bean ExampleA
@Component
public class ExampleA
    public String doSomething() 
        return "finish";
    


//有时候,我们可能需要访问切入点执行后的返回值,那么我们可以使用@AfterReturning注解中的returning属性来指定返回值的名称,然后再给这个切面方法添加一个形参,这个形参类型即为切入点执行后的返回值类型(或其父类型,但不能完全不一致,否则切面会切入失败),形参名要与刚刚设置过的returning属性值一致,如下例
@Aspect
@Component
public class Logger 

    @AfterReturning(value = "execution(* cn.example.spring.boke.ExampleA.*(..))", returning = "returnVal")
    public void afterReturning(Object returnVal) 
        System.out.println(new Date() + " 开始执行...");
        System.out.println(returnVal);
        System.out.println(new Date() + " 结束执行...");
    

  • After Throwing Advice:使用@AfterThrowing注解可定义异常通知,它会在切入点触发异常之后执行
//同样,我们有时候也期望访问切入点执行过程中抛出的异常,与返回通知一致,例子如下
@Aspect
@Component
public class Logger 

    @AfterThrowing(value = "execution(* cn.example.spring.boke.ExampleA.*(..))", throwing = "throwable")
    public void afterReturning(Throwable throwable) 
        System.out.println(new Date() + " 开始执行...");
        System.out.println(throwable);
        System.out.println(new Date() + " 结束执行...");
    

  • After (Finally) Advice:使用@After注解可定义后置通知,它会在切入点无论以何种方式执行(正常或异常)后执行,常用于释放资源等目的,类似于try-catch语句中的finally块,它与@AfterReturning的区别是,@AfterReturning只适用于切入点正常返回
@Aspect
@Component
public class Logger 

    @After(value = "execution(* cn.example.spring.boke.ExampleA.*(..))")
    public void afterReturning() 
        //...
    

  • Around advice:使用@After注解可定义环绕通知,它既可以在切入点之前执行通知,又可以在切入点之后执行,甚至可以不用执行切入点,是最为灵活强大的通知
@Aspect
@Component
public class Logger 
    //环绕通知方法可不声明形参,但如果要声明形参,第一个形参的类型必须是ProceedingJoinPoint类型,对ProceedingJoinPoint调用proceed方法后会导致切入点真正的执行,此外,proceed方法还有一个重载方法,我们可以对它传递一个Object[],那么当切入点执行时会以这个数组中的值作为方法参数值来执行
    //我们可以调用一次,多次或根本不调用proceed方法
    @Around(value = "execution(* cn.example.spring.boke.ExampleA.*(..))")
    public void afterReturning(ProceedingJoinPoint joinPoint) 
        Object returnVal = null;
        try 
            //...
            returnVal = joinPoint.proceed();
            //...
         catch (Throwable e) 
            //...
            e.printStackTrace();
        
        return returnVal;
    


8.切入点信息获取

(1)有时候,我们期望获取到切入点相关信息,比如它的签名,形参等信息,Spring为我们提供了JoinPoint类型,用于获取相关信息,在前面的环绕通知的例子中,我们就已经使用了JoinPoint的子类型ProceedingJoinPoint,它添加了proceed方法,来显式的调用执行切入点

@Aspect
@Component
public class Logger 
    //除了下面的例子外,使用JoinPoint,还可以获取到切入点的其他一些信息,可参考api文档
    @Before(value = "execution(* cn.example.spring.boke.ExampleA.*(..))")
    public void before(JoinPoint joinPoint) 
        System.out.println("被拦截的类:" + joinPoint.getTarget().getClass().getName());
        System.out.println("被拦截的方法:" + ((MethodSignature) joinPoint.getSignature()).getMethod().getName());
        System.out.println("被拦截的方法参数:" + Arrays.toString(joinPoint.getArgs()));
    

9.通知执行顺序

(1)不同切面类中的通知,在默认情况下,按照所在切面类名的字典序排序,若其排序越高则优先级也就越高,如下

@Configuration
@EnableAspectJAutoProxy
@ComponentScan(basePackages = "cn.example.spring.boke")
public class Config  

@Component
public class ExampleA

    public void doSomething() 
        System.out.println("doSomething...");
    


//声明两个切面类TimerLogger和OperationLogger
@Aspect
@Component
public class TimerLogger 
    @Before(value = "execution(* cn.example.spring.boke.ExampleA.*(..))")
    public void a() 
        System.out.println("timer...");
    



@Aspect
@Component
public class OperationLogger 
    @Before(value = "execution(* cn.example.spring.boke.ExampleA.*(..))")
    public void a() 
        System.out.println("operation...");
    


//启动容器,观察打印结果
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Config.class);
ctx.getBean(ExampleA.class).doSomething();

//打印结果如下,OperationLogger中的前置通知先执行,TimerLogger中的前置通知后执行,就是因为O的字典序列大于T,因此OperationLogger中的通知的优先级高于TimerLogger中的,而对于前置通知而言,优先级越高的越先执行,对于后置通知,优先级越高的越后执行
operation...
timer...
doSomething...


//我们可以将TimerLogger改为ATimerLogger,这样的话它里面的前置通知就会先执行了
@Aspect
@Component
public class ATimerLogger 
    @Before(value = "execution(* cn.example.spring.boke.ExampleA.*(..))")
    public void a() 
        System.out.println("timer...");
    


(2)我们可以对切面类实现Ordered接口或添加@Order注解来显示的指定优先级,其中指定的值越小,优先级越高

//此时TimerLogger的优先级高于OperationLogger
@Aspect
@Component
@Order(1)
public class TimerLogger 
    @Before(value = "execution(* cn.example.spring.boke.ExampleA.*(..))")
    public void a() 
        System.out.println("timer...");
    


@Aspect
@Component
@Order(2)
public class OperationLogger 

    @Before(value = "execution(* cn.example.spring.boke.ExampleA.*(..))")
    public void a() 
        System.out.println("operation...");
    

(3)对于同个切面类中的相同类型的通知,其优先级只与通知方法名字典序的排序有关,排序越高,优先级越高,如下

//Logger切面类中定义了两个前置通知为aPrint和bPrint
@Aspect
@Component
public class Logger 

    @Before(value = "execution(* cn.example.spring.boke.ExampleA.*(..))")
    public void aPrint() 
        System.out.println("a");
    

    @Before(value = "execution(* cn.example.spring.boke.ExampleA.*(..))")
    public void bPrint() 
        System.out.println("b");
    


//启动容器,可见aPrint先于bPrint,这就是因为a的字典序高于b
a
b
doSomething...

//将aPrint改为caPrint,这时bPrint会先执行,因为此时它的字典序高
@Before(value = "execution(* cn.example.spring.boke.ExampleA.*(..))")
publicvoid caPrint() 
    System.out.println("a");


//此外使用@Order注解,无法改变优先级,因为此时显式指定优先级的策略已经失效了,如下面这个例子还是按照之前默认的优先级进行执行
@Aspect
@Component
public class Logger 

    @Before(value = "execution(* cn.example.spring.boke.ExampleA.*(..))")
    @Order(Ordered.LOWEST_PRECEDENCE)
    public void aPrint() 
        System.out.println("a");
    

    @Before(value = "execution(* cn.example.spring.boke.ExampleA.*(..))")
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public void bPrint() 
        System.out.println("b");
    

10.Introductions(引介)

(1)引介能够使指定的对象实现某些接口,并提供对这些接口的实现,以达到向对象中动态添加它所没有方法的目的,例子如下

//我们希望向ExampleA类中增加某些新的方法
@Component
public class ExampleA 

//声明一个接口,这个接口里的方法即为我们希望增加的新的方法
public interface Extention 
    void doSomething();


//新方法的具体实现
public class ExtentionImpl implements Extention

    @Override
    public void doSomething() 
        System.out.println("doSomething...");
    


//定义一个切面
@Component
@Aspect
public class MyAspect 
    //使用@DeclarePrents注解,声明被拦截的类有一个新的父类型,其中value指定拦截哪些类,在下面这个例子中指定拦截cn.example.spring.boke包下的所有类,同时指定它们的父类型均为Extention,具体的实现为ExtentionImpl
    @DeclareParents(value = "cn.example.spring.boke.*", defaultImpl = ExtentionImpl.class)
    public Extention extention;


//开启AOP
@Configuration
@EnableAspectJAutoProxy
@ComponentScan(basePackages = "cn.example.spring.boke")
public class Config  

//启动容器,从容器中获取到exampleA并将其强制转换为Extention,这样我们就能使用向ExampleA中新添加的方法了
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Config.class);
Extention exampleA = (Extention)ctx.getBean("exampleA");
exampleA.doSomething();

Spring AOP官方文档学习笔记之AOP概述

1.AOP简介

(1) Spring的关键组件之一就是AOP框架,它是对Spring IoC的补充(这意味着如果我们的IOC容器不需要AOP的话就不用引入AOP),此外,AOP亦是对OOP的补充,OOP的关注点在于类,而AOP的关注点在于切面,它可以将分散在不同类不同方法中重复的代码逻辑抽取出来,称之为通知(Advice),然后在运行时通过动态代理技术将“通知”组合进原有对象中,这样就能在实现原有预期效果的情况下达到减少代码冗余的目的

(2) 在Spring中,AOP主要用于两大方面,一是提供了声明式服务(比如声明式事物管理:@Transactional注解),二是让用户实现自定义切面,实现代码解偶,用于作为OOP的补充,一个简单的例子如下

//我们想在ExampleA中的每个方法中记录该方法的开始执行时间和结束执行时间
@Component
public class ExampleA 
    //在每个方法业务代码执行前和执行后,都有一个System.out.println用于打印执行时间
    public void register() 
        System.out.println(System.currentTimeMillis() + " 开始执行...");
        //注册相关的业务逻辑代码...
        try 
            Thread.sleep(100);
         catch (InterruptedException e) 
            e.printStackTrace();
        
        System.out.println(System.currentTimeMillis() + " 结束执行...");
    

    public void sendEmail() 
        System.out.println(System.currentTimeMillis() + " 开始执行...");
        //发送邮件相关的业务逻辑代码...
        try 
            Thread.sleep(200);
         catch (InterruptedException e) 
            e.printStackTrace();
        
        System.out.println(System.currentTimeMillis() + " 结束执行...");
    


//现在,我们期望把"记录该方法的开始执行时间和结束执行时间"这一段代码逻辑提取出来,这样,之后再往ExampleA中添加了新的方法后,我们就不需要再手动添加两个 System.out.println 语句了,而被提取的这一段代码逻辑,被称之为通知(Advice),而通知加上要被增强的业务代码(比如ExampleA中的注册,发送邮件相关的业务逻辑代码)就形成了一个切面
//使用@Aspect注解定义切面类,用于声明定义一个个切面,你可能会疑惑为啥这个注解是由org.aspectj.aspectjrt提供的,那是因为虽然名字叫Spring AOP,给人的感觉好像是Spring单独开发的,但其实是Spring整合了AspectJ AOP框架(用AspectJ AOP的写法和定义方式,底层由Spring封装实现)一同实现的这个AOP功能
@Aspect
@Component
public class Logger 
    //1.重复的代码逻辑(System.out.println),即通知的提取:对应(3)和(5),用于记录方法的开始执行时间和结束执行时间
    //2.指出要被增强的业务代码:假如有两个类ExampleA和ExampleB,我们需要记录ExampleA中每个方法的开始执行时间和结束执行时间,而ExampleB类不用,这就是(1)所发挥的作用
    //3.执行要被增强的业务代码:仅仅指出要被增强的业务代码有哪些还不行,我们还需要调用这些业务代码,从而使它真正的被执行,这是(2)和(4)发挥的作用,(2)中的joinPoint称之为切入点,我们可以将它视为要被增强的业务代码(register,sendEmail)的抽象,而(4)joinPoint.proceed() 就代表着业务代码的执行,如同 thread.start() 代表着线程执行一样
    //4.将通知和被增强的业务代码整个组合起来,称之为切面,即下面的recordTime方法,它就代表一个切面
    @Around("execution(public * cn.example.spring.boke.ExampleA.*(..))")           //(1)
    public void recordTime(ProceedingJoinPoint joinPoint) throws Throwable        //(2)
        System.out.println(System.currentTimeMillis() + " 开始执行...");            //(3)
        joinPoint.proceed();                                                       //(4)
        System.out.println(System.currentTimeMillis() + " 结束执行...");            //(5)
    


//之后,使用@EnableAspectJAutoProxy开启注解AOP功能
@Configuration
@EnableAspectJAutoProxy
@ComponentScan("cn.example.spring.boke")
public class Config 



//然后我们就可以去掉ExampleA的方法中的开始和结束时执行时间打印,因为我们已经把它们抽象提取出来了
@Component
public class ExampleA 
    public void register() 
        //注册相关的业务逻辑代码...
        try 
            Thread.sleep(100);
         catch (InterruptedException e) 
            e.printStackTrace();
        
    

    public void sendEmail() 
        //发送邮件相关的业务逻辑代码...
        try 
            Thread.sleep(200);
         catch (InterruptedException e) 
            e.printStackTrace();
        
    


//启动容器,执行register和sendEmail方法,可以看见我们的执行时间日志打印了出来,之后添加进ExampleA类中的新方法,也都会进行时间的打印,而无需我们手动的添加两个System.out.println语句,这便是AOP的强大功能
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(Config.class);
ctx.getBean(ExampleA.class).register();
ctx.getBean(ExampleA.class).sendEmail();

2.AOP术语

(1) Aspect:切面 = 通知 + 切入点,如我们例子中所示的recordTime方法

(2) Join point:连接点,可以理解为类中的一个一个方法

(3) Advice:通知,即某个切面要在某个切入点上所要采取的行动,通常而言就是各个类各个方法中重复逻辑的抽象,比如上面这个例子中开始ExampleA中的2个方法中都有开始和结束时间打印,它们是重复的逻辑,因此我们可将它提取出来,称之为通知

(4) Pointcut:切入点,会被增强的连接点,比如上面的例子中我们对ExampleA中的方法register和sendEmail进行了增强,那么这些被增强的方法就有了一个新的称谓,即切入点,而其它的类中的方法没有被增强,还是一个个普通的方法,而这些普通的方法称之为连接点

(5) Introduction:引介,它的概念同通知差不多,只不过通知是针对切入点所提供的增强的逻辑,而引介是针对Class类,它可以在不修改原有类的代码的前提下,在运行期为原始类动态添加新的属性/方法

(6) Target object:目标对象,即会被增强的方法所属的对象,如上面例子中的ExampleA对象

(7) AOP proxy:AOP代理对象,是在目标对象上被增强了过后所产生的新对象,Spring采用动态代理技术来实现AOP,底层实现为JDK动态代理或CGLIB动态代理

(8) Weaving:织入,它代表一个动作,即将Advice组合进Target object中,从而产生AOP proxy这么的一个过程

3.AOP通知类型

(1) 根据通知与切入点的执行关系,Spring提供了5种通知类型,如下:

  • Before advice:前置通知,即通知在切入点执行之前执行

  • After returning advice:返回通知,即通知在切入点"正常"执行之后执行

  • After throwing advice:异常通知,即通知在切入点触发异常之后执行

  • After (finally) advice:后置通知,即无论切入点以何种方式执行(正常或异常),通知都会执行

  • Around advice:环绕通知,既可以在切入点之前执行通知,又可以在切入点之后执行,甚至可以不用执行切入点,它是最为强大的通知,我们上面例子中的recordTime就使用的是环绕通知

注意:After returning advice与After throwing advice两者是互斥的,因为如果方法调用成功无异常,则会有返回值;如果方法抛出了异常,则不会有返回值,因此这两个通知只会有一个执行

(2) Spring建议能使用具体的通知就去使用具体的通知,比如能用Before advice的情况下就不用去使用Around advice

4.Spring AOP特征

(1) Spring AOP是基于纯java实现的,不需要特殊的编译过程也不需要去控制类加载器的层次结构

(2) Spring AOP目前的切入点类型只能是方法,不能是变量或其它的类型,如果我们想要切入变量,可以使用AspectJ AOP框架,Spring与AspectJ无缝集成

(3) Spring AOP与IOC容器紧密集成,如果我们只是想要单独使用一个AOP框架,那么可以使用AspectJ

(4) Spring AOP支持基于注解的配置,也支持基于xml文件的配置,同IOC一样

(5) Spring AOP默认使用jdk动态代理,因此只要一个类实现了某个接口,那么它就能被代理,但如果我们的某个类没有实现接口,则会采用cglib动态代理来生成代理对象,同时我们也可以强制使用cglib动态代理作为默认选项

以上是关于Spring AOP官方文档学习笔记之基于注解的Spring AOP的主要内容,如果未能解决你的问题,请参考以下文章

Spring IOC官方文档学习笔记之基于注解的容器配置

Spring IOC官方文档学习笔记之基于Java的容器配置

Spring AOP官方文档学习笔记之AOP概述

Spring AOP官方文档学习笔记之Spring AOP的其他知识点

Spring5学习笔记 — “AOP操作—AspectJ注解”

Spring5学习笔记 — “AOP操作—AspectJ注解”