Spring-AOP-注解方式使用介绍长文详解

38次阅读

共计 9383 个字符,预计需要花费 24 分钟才能阅读完成。

前言

之前的源码解析章节,本人讲解了 Spring IOC 的核心部分的源码。如果你熟悉 Spring AOP 的使用的话,在了解 Spring IOC 的核心源码之后,学习 Spring AOP 的源码,应该可以说是水到渠成,不会有什么困难。

但是直接开始讲 Spring AOP 的源码,本人又觉得有点突兀,所以便有了这一章。Spring AOP 的入门使用介绍:包括 Spring AOP 的一些概念性介绍和配置使用方法。

这里先贴一下思维导图。

AOP 是什么

AOP : 面向切面编程(Aspect Oriented Programming)

Aspect 是一种新的模块化机制,用来描述分散在对象、类或函数中的横切关注点(crosscutting concern)。从关注点中分离出横切关注点是面向切面的程序设计的核心概念。分离关注点使解决特定领域问题的代码从业务逻辑中独立出来,业务逻辑的代码中不再含有针对特定领域问题代码的调用,业务逻辑同特定领域问题的关系通过切面来封装、维护,这样原本分散在整个应用程序中的变动就可以很好地管理起来。

最近在看李智慧的《大型网站技术架构》一书中,作者提到,开发低耦合系统是软件设计的终极目标之一。AOP 这种面向切面编程的的方式就体现了这样的理念。将一些重复的、和业务主逻辑不相关的功能性代码(日志记录、安全管理等)通过切面模块化地抽离出来进行封装,实现关注点分离、模块解耦,使得整个系统更易于维护管理。

这样分而治之的设计,让我感觉到了一种美感。

AOP 要实现的是在我们原来写的代码的基础上,进行一定的包装,如在方法执行前、方法返回后、方法抛出异常后等地方进行一定的拦截处理或者叫增强处理。

AOP 的实现并不是因为 Java 提供了什么神奇的钩子,可以把方法的几个生命周期告诉我们,而是我们要实现一个代理,实际运行的实例其实是 生成的代理类的实例

名词概念

前面提到过,Spring AOP 延用了 AspectJ 中的概念,使用了 AspectJ 提供的 jar 包中的注解。也就是 Spring AOP 里面的概念和术语,并不是 Spring 独有的,而是和 AOP 相关的。

概念可以草草看过,在看了之后的章节之后再回来看会对概念理解的更深。

术语 概念
Aspect 切面是 PointcutAdvice的集合,一般单独作为一个类。PointcutAdvice 共同定义了关于切面的全部内容,它是 什么时候,在何时和何处 完成功能。
Joinpoint 这表示你的应用程序中可以插入 AOP 方面的一点。也可以说,这是应用程序中使用 Spring AOP 框架采取操作的实际位置。
Advice 这是在方法执行之前或之后采取的实际操作。这是在 Spring AOP 框架的程序执行期间调用的实际代码片段。
Pointcut 这是一组一个或多个切入点,在切点应该执行Advice。您可以使用表达式或模式指定切入点,后面示例会提到。
Introduction 引用允许我们向现有的类添加新的方法或者属性
Weaving 创建一个被增强对象的过程。这可以在编译时完成(例如使用 AspectJ 编译器),也可以在运行时完成。Spring 和其他纯 Java AOP 框架一样,在运行时完成织入。

PS:在整理概念的时候有个疑问,为什么网上这么多中文文章把 advice 翻译成“通知”呢???概念上说得通吗???我更愿意翻译成“增强”(并发中文网 ifeve.com 也是翻译成增强)

还有一些注解,表示 Advice 的类型,或者说增强的时机,看过之后的示例之后会更加的清楚。

术语 概念
Before 在方法被调用之前执行增强
After 在方法被调用之后执行增强
After-returning 在方法成功执行之后执行增强
After-throwing 在方法抛出指定异常后执行增强
Around 在方法调用的前后执行自定义的增强行为(最灵活的方式)

使用方式

Spring 2.0 之后,Spring AOP 有了两种配置方式。

  1. schema-based:Spring 2.0 以后使用 XML 的方式来配置,使用 命名空间 <aop />
  2. @AspectJ 配置:Spring 2.0 以后提供的注解方式。这里虽然叫做 @AspectJ,但是这个和 AspectJ 其实没啥关系。

PS:个人比较钟情于 @AspectJ 这种方式,使用下来是最方面的。也可能是因为我觉得 XML 方式配置的 Spring Bean 很不简洁、写起来不好看吧,所以有点排斥吧。23333~

本文主要针对注解方式讲解,并且给出对应的 DEMO;之后的源码解析也会以注解的这种方式为范例讲解 Spring AOP 的源码(整个源码解析看完,会对其他方式触类旁通,因为原理都是一样的)

如果对其他配置方式感兴趣的同学可以 google 其他的学习资料。


来一条分割线,正式开始

1. 开启 @AspectJ 注解配置方式

开启 @AspectJ 的注解配置方式,有两种方式

  1. 在 XML 中配置:

    <aop:aspectj-autoproxy/>
  2. 使用 @EnableAspectJAutoProxy 注解

    @Configuration
    @EnableAspectJAutoProxy
    public class Config {}

开启了上述配置之后,所有 在容器中 @AspectJ注解的 bean 都会被 Spring 当做是 AOP 配置类,称为一个 Aspect。

NOTE:这里有个要注意的地方,@AspectJ 注解只能作用于 Spring Bean 上面,所以你用 @Aspect 修饰的类要么是用 @Component 注解修饰,要么是在 XML 中配置过的。

比如下面的写法,

// 有效的 AOP 配置类
@Aspect
@Component
public class MyAspect {//....}

// 如果没有在 XML 配置过,那这个就是无效的 AOP 配置类
@Aspect
public class MyAspect {//....}

2. 配置 Pointcut (增强的切入点)

Pointcut 在大部分地方被翻译成切点,用于定义哪些方法需要被增强或者说需要被拦截。

在 Spring 中,我们可以认为 Pointcut 是用来匹配 Spring 容器中所有满足指定条件的 bean 的方法。

比如下面的写法,

    // 指定的方法
    @Pointcut("execution(* testExecution(..))")
    public void anyTestMethod() {}

下面完整列举一下 Pointcut 的匹配方式:

  1. execution:匹配方法签名

    这个最简单的方式就是上面的例子,"execution(* testExecution(..))"表示的是匹配名为 testExecution 的方法,*代表任意返回值,(..)表示零个或多个任意参数。

  2. within:指定所在类或所在包下面的方法(Spring AOP 独有)

        // service 层
        // ".." 代表包及其子包
        @Pointcut("within(ric.study.demo.aop.svc..*)")
        public void inSvcLayer() {}
  3. @annotation:方法上具有特定的注解

        // 指定注解
        @Pointcut("@annotation(ric.study.demo.aop.HaveAop)")
        public void withAnnotation() {}
  4. bean(idOrNameOfBean):匹配 bean 的名字(Spring AOP 独有)

        // controller 层
        @Pointcut("bean(testController)")
        public void inControllerLayer() {}

上述是日常使用中常见的几种配置方式

有更细的匹配需求的,可以参考这篇文章:https://www.baeldung.com/spri…

关于 Pointcut 的配置,Spring 官方有这么一段建议:

When working with enterprise applications, you often want to refer to modules of the application and particular sets of operations from within several aspects. We recommend defining a “SystemArchitecture” aspect that captures common pointcut expressions for this purpose. A typical such aspect would look as follows:

意思就是,如果你是在开发企业级应用,Spring 建议你使用 SystemArchitecture这种切面配置方式,即将一些公共的 PointCut 配置全部写在这个一个类里面维护。官网文档给的例子像下面这样(它文中使用 XML 配置的,所以没加 @Component 注解)

package com.xyz.someapp;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class SystemArchitecture {

  /**
   * A join point is in the web layer if the method is defined
   * in a type in the com.xyz.someapp.web package or any sub-package
   * under that.
   */
  @Pointcut("within(com.xyz.someapp.web..*)")
  public void inWebLayer() {}

  /**
   * A join point is in the service layer if the method is defined
   * in a type in the com.xyz.someapp.service package or any sub-package
   * under that.
   */
  @Pointcut("within(com.xyz.someapp.service..*)")
  public void inServiceLayer() {}

  /**
   * A join point is in the data access layer if the method is defined
   * in a type in the com.xyz.someapp.dao package or any sub-package
   * under that.
   */
  @Pointcut("within(com.xyz.someapp.dao..*)")
  public void inDataAccessLayer() {}

  /**
   * A business service is the execution of any method defined on a service
   * interface. This definition assumes that interfaces are placed in the
   * "service" package, and that implementation types are in sub-packages.
   * 
   * If you group service interfaces by functional area (for example, 
   * in packages com.xyz.someapp.abc.service and com.xyz.def.service) then
   * the pointcut expression "execution(* com.xyz.someapp..service.*.*(..))"
   * could be used instead.
   */
  @Pointcut("execution(* com.xyz.someapp.service.*.*(..))")
  public void businessService() {}
  
  /**
   * A data access operation is the execution of any method defined on a 
   * dao interface. This definition assumes that interfaces are placed in the
   * "dao" package, and that implementation types are in sub-packages.
   */
  @Pointcut("execution(* com.xyz.someapp.dao.*.*(..))")
  public void dataAccessOperation() {}

}

上面这个 SystemArchitecture 很好理解,该 Aspect 定义了一堆的 Pointcut,随后在任何需要 Pointcut 的地方都可以直接引用。

配置切点,代表着我们想让程序拦截哪一些方法,但程序需要怎么对拦截的方法进行增强,就是后面要介绍的配置 Advice。

3. 配置 Advice

注意,实际开发过程当中,Aspect 类应该遵守单一职责原则,不要把所有的 Advice 配置全部写在一个 Aspect 类里面。

这里是为了演示方便,所以写在了一起。

先直接上示例代码,里面包含了 Advice 的几种配置方式(上文名词概念小节中有提到)。

/**
 * 注:实际开发过程当中,Advice 应遵循单一职责,不应混在一起
 *
 * @author Richard_yyf
 * @version 1.0 2019/10/28
 */
@Aspect
@Component
public class GlobalAopAdvice {@Before("ric.study.demo.aop.SystemArchitecture.dataAccessOperation()")
    public void doAccessCheck() {// ... 实现代码}

    // 实际使用过程当中 可以像这样把 Advice 和 Pointcut 合在一起,直接在 Advice 上面定义切入点
    @Before("execution(* ric.study.demo.dao.*.*(..))")
    public void doAccessCheck() {// ... 实现代码}

    // 在方法
    @AfterReturning("ric.study.demo.aop.SystemArchitecture.dataAccessOperation()")
    public void doAccessCheck() {// ... 实现代码}

    // returnVal 就是相应方法的返回值
    @AfterReturning(pointcut="ric.study.demo.aop.SystemArchitecture.dataAccessOperation()",
        returning="returnVal")
    public void doAccessCheck(Object returnVal) {//  ... 实现代码}

    // 异常返回的时候
    @AfterThrowing("ric.study.demo.aop.SystemArchitecture.dataAccessOperation()")
    public void doRecoveryActions() {// ... 实现代码}

    // 注意理解它和 @AfterReturning 之间的区别,这里会拦截正常返回和异常的情况
    @After("ric.study.demo.aop.SystemArchitecture.dataAccessOperation()")
    public void doReleaseLock() {
        // 通常就像 finally 块一样使用,用来释放资源。// 无论正常返回还是异常退出,都会被拦截到
    }

    // 这种最灵活,既能做 @Before 的事情,也可以做 @AfterReturning 的事情
    @Around("ric.study.demo.aop.SystemArchitecture.businessService()")
    public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
           //  target 方法执行前... 实现代码
        Object retVal = pjp.proceed();
        //  target 方法执行后... 实现代码
        return retVal;
    }
}

在某些场景下,我们想在 @Before 的时候,去获取方法的入参,比如进行一些日志的记录 ,我们可以通过 org.aspectj.lang.JoinPoint 来实现。上文中的 ProceedingJoinPoint 就是其子类。

@Before("...")
public void logArgs(JoinPoint joinPoint) {System.out.println("方法执行前,打印入参:" + Arrays.toString(joinPoint.getArgs()));
}

再举个与之对应的,方法返参打印:

@AfterReturning(pointcut="...", returning="returnVal")
public void logReturnVal(Object returnVal) {System.out.println("方法执行后,打印返参:" + returnVal));
}

快速 Demo

介绍完上述的配置过程之后,我们用一个快速的 Demo 来实际演示一遍。这里把顺序变一下;

1. 编写 目标类

package ric.study.demo.aop.svc;

public interface TestSvc {void process();
}

@Service("testSvc")
public class TestSvcImpl implements TestSvc {
    @Override
    public void process() {System.out.println("test svc is working");
    }
}

public interface DateSvc {void printDate(Date date);
}

@Service("dateSvc")
public class DateSvcImpl implements DateSvc {

    @Override
    public void printDate(Date date) {System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date));
    }
}

2. 配置 Pointcut

@Aspect
@Component
public class PointCutConfig {@Pointcut("within(ric.study.demo.aop.svc..*)")
    public void inSvcLayer() {}   
}

3. 配置 Advice

/**
 * @author Richard_yyf
 * @version 1.0 2019/10/29
 */
@Component
@Aspect
public class ServiceLogAspect {

    // 拦截,打印日志,并且通过 JoinPoint 获取方法参数
    @Before("ric.study.demo.aop.PointCutConfig.inSvcLayer()")
    public void logBeforeSvc(JoinPoint joinPoint) {System.out.println("在 service 方法执行前 打印第 1 次日志");
        System.out.println("拦截的 service 方法的方法签名:" + joinPoint.getSignature());
        System.out.println("拦截的 service 方法的方法入参:" + Arrays.toString(joinPoint.getArgs()));
    }

    // 这里是 Advice 和 Pointcut 合在一起配置的方式
    @Before("within(ric.study.demo.aop.svc..*)")
    public void logBeforeSvc2() {System.out.println("在 service 的方法执行前 打印第 2 次日志");
    }
}

4. 开启 @AspectJ 注解配置方式,并启动

这里为了图方便,把配置类和启动类写在了一起,

/**
 * @author Richard_yyf
 * @version 1.0 2019/10/28
 */
@Configuration
@EnableAspectJAutoProxy
@ComponentScan("ric.study.demo.aop")
public class Boostrap {public static void main(String[] args) {AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Boostrap.class);
        TestSvc svc = (TestSvc) context.getBean("testSvc");
        svc.process();
        System.out.println("==================");
        DateSvc dateSvc = (DateSvc) context.getBean("dateSvc");
        dateSvc.printDate(new Date());
    }
}

5. 输出

在 service 方法执行前 打印第 1 次日志
拦截的 service 方法的方法签名: void ric.study.demo.aop.svc.TestSvcImpl.process()
拦截的 service 方法的方法入参: []
在 service 的方法执行前 打印第 2 次日志
test svc is working
==================
在 service 方法执行前 打印第 1 次日志
拦截的 service 方法的方法签名: void ric.study.demo.aop.svc.DateSvcImpl.printDate(Date)
拦截的 service 方法的方法入参: [Mon Nov 04 18:11:34 CST 2019]
在 service 的方法执行前 打印第 2 次日志
2019-11-04 18:11:34

JDK 动态代理和 Cglib

前面有提到过,Spring AOP 在目标类有实现接口的时候,会使用 JDK 动态代理来生成代理类,我们结合上面的 DEMO 看看,

如果我们想不管是否有实现接口,都是强制使用 Cglib 的方式来实现怎么办?

Spring 提供给了我们对应的配置方式,也就是proxy-target-class.

注解方式://@EnableAspectJAutoProxy(proxyTargetClass = true) // 这样子就是默认使用 CGLIB
XML 方式:<aop:config proxy-target-class="true">

改了之后,

小结

本文详细介绍了 Spring AOP 的起源、名词概念以及基于注解的使用方式。

本文按照作者的写作习惯,是源码解析章节的前置学习章节。在下一章中,我们会以注解方式为入口,介绍 Spring AOP 的源码设计,解读相关核心源码(整个源码解析看完,会对其他方式触类旁通,因为原理都是一样的)。

感兴趣的可以翻到【前言】部分,再看一下思维导图。

如果本文有帮助到你,希望能点个赞,这是对我的最大动力。

本文由博客一文多发平台 OpenWrite 发布!

正文完
 0