Java工程师成长之路——Spring框架分析Spring AOP详解

Spring AOP

简述

AOP(Aspect Oriented Programming),意为:面向切面编程(也称面向方面),通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

原理

AOP实现的关键,在于AOP框架自动创建的AOP代理,AOP代理主要分为静态代理和动态代理,静态代理的代表为AspectJ;而动态代理则以Spring AOP为代表。

名词解释

  • Aspect(切面):Aspect 声明类似于 Java 中的类声明,在 Aspect 中会包含着一些 Pointcut 以及相应的 Advice。
  • Joint point(连接点):表示在程序中明确定义的点,典型的包括方法调用,对类成员的访问以及异常处理程序块的执行等等,它自身还可以嵌套其它 joint point。
  • Pointcut(切点):表示一组 joint point,这些 joint point 或是通过逻辑关系组合起来,或是通过通配、正则表达式等方式集中起来,它定义了相应的 Advice 将要发生的地方。
  • Advice(增强):Advice 定义了在 Pointcut 里面定义的程序点具体要做的操作,它通过 before、after 和 around 来区别是在每个 joint point 之前、之后还是代替执行的代码。
  • Target(目标对象):织入 Advice 的目标对象。
  • Weaving(织入):将 Aspect 和其他对象连接起来, 并创建Adviced object 的过程。
  1. 使用AspectJ的编译时增强,实现AOP。

之前提到,AspectJ是静态代理的增强。所谓的静态代理,就是AOP框架会在编译阶段生成AOP代理类,因此也称为编译时增强。
示例程序

/**
 * @author gxl
 */
public class Test {

  void sayHello() {
    System.out.println("hello");
  }

  public static void main(String[] args) {
    Test test = new Test();
    test.sayHello();
  }

}
/**
 * @author gxl
 */
public aspect TestAspectJ {
  void around():call(void Test.sayHello()){
    System.out.println("开始事务。。。");
    proceed();
    System.out.println("结束事务。。。");
  }
}

编译:ajc -d . Test.java TestAspectJ.aj
运行:java Test
运行结果:

开始事务。。。
hello
结束事务。。。

可以看到,AOP已经生效了。这就是AspectJ的静态代理,它会在编译阶段将Aspect织入Java字节码中,运行的时候就是经过增强之后的AOP对象。proceed方法就是回调执行被代理类中的方法。

  1. Spring AOP运行时增强,实现AOP
  • 与AspectJ的静态代理不同,Spring AOP使用的是动态代理。所谓的动态代理,就是说AOP框架不会去修改字节码,而是在内存中临时为方法生成一个AOP对象,这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。
  • Spring AOP中的动态代理,主要有两种方式:JDK动态代理和CGLIB动态代理。JDK动态代理通过“反射”来接收被代理的类,并且要求被代理的类必须实现一个接口。JDK动态代理的核心是InvocationHandler接口和Proxy类。如果目标类没有实现接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。
  • CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态地生成某个类的子类。注意,CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。

实现

上面讲到Spring AOP是通过动态代理来实现的,并且主要有两种形式的动态代理——JDK动态代理和CGLIB动态代理,下面就这两种动态代理方式进行详细介绍:

  1. JDK动态代理

为了方便,本示例采用springboot实现,首先定义注解Timer,用于在指定方法处设置切入点:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author gxl
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Timer {
}

然后新建接口Person用于测试接口的动态代理(即JDK动态代理):

/**
 * @author gxl
 */
public interface Person {

  /**
   * 打招呼
   *
   * @param name 名称
   */
  void sayHello(String name);

}

然后新建实现类Chinese:

import org.springframework.stereotype.Component;

/**
 * @author gxl
 */
@Component
public class Chinese implements Person {

  @Timer
  @Override
  public void sayHello(String name) {
    System.out.println("Hello," + name);
  }

  /**
   * 吃东西
   *
   * @param food 食物
   */
  public void eat(String food) {
    System.out.println("我正在吃:" + food);
  }

}

可以看到实现类除了实现了Person接口定义的sayHello()方法外,还定义了一个eat()方法,此方法用于测试CGLIB动态代理。

接下来定义切面AspectTest:

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.aop.BeforeAdvice;
import org.springframework.stereotype.Component;

/**
 * @author gxl
 */
@Aspect
@Component
public class AdviceTest implements BeforeAdvice {

  @Pointcut("@annotation(com.spring.aop.Timer)")
  public void pointCut() {
  }

  @Before("pointCut()")
  public void before(JoinPoint joinPoint) {
    System.out.println("===before执行了===:" + joinPoint.getClass());
  }

  @After("pointCut()")
  public void after(JoinPoint joinPoint) {
    System.out.println("===after执行了===:" + joinPoint.getClass());
  }

}

使用 @Aspect 注解表名这是一个切面类,@Pointcut 注解设置切入点,这里我指定了上面定义的Timer为切入点,@Before@After 注解顾名思义就是指定在执行对应方法之前及之后需要进行的操作。

最后修改AopApplication:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author gxl
 */
@RestController
@SpringBootApplication
public class AopApplication {

  private final Person person;

  @Autowired
  public AopApplication(Person person) {
    this.person = person;
  }

  @GetMapping("/test")
  public void test() {
    person.sayHello("Spring AOP");
    System.out.println(person.getClass());
  }

  public static void main(String[] args) {
    SpringApplication.run(AopApplication.class, args);
  }

}

在AopApplication类上加 @RestController 注解,使用接口请求的方式测试,注入 Person 类,定义test()方法,打印输出person的类信息;启动服务,调用接口服务,观察控制台输出,输出如下:

===before执行了===:class org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint
Hello,Spring AOP
===after执行了===:class org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint
class com.sun.proxy.$Proxy72

可以看到类型是class com.sun.proxy.$Proxy72,也就是前面提到的Proxy类,因此这里Spring AOP使用了JDK的动态代理,并且切入操作也顺利执行了。

  1. CGLIB动态代理

修改上面的Chinese实现类:

import org.springframework.stereotype.Component;

/**
 * @author gxl
 */
@Component
public class Chinese implements Person {

  @Timer
  @Override
  public void sayHello(String name) {
    System.out.println("Hello," + name);
  }

  /**
   * 吃东西
   *
   * @param food 食物
   */
  @Timer
  public void eat(String food) {
    System.out.println("我正在吃:" + food);
  }

}

这里只是在eat()方法上添加了 @Timer 注解设置切入点操作。

然后修改AopApplication类:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author gxl
 */
@RestController
@SpringBootApplication
public class AopApplication {

  private final Chinese chinese;

  @Autowired
  public AopApplication(Chinese chinese) {
    this.chinese = chinese;
  }

  @GetMapping("/test")
  public void test() {
    chinese.eat("面");
    System.out.println(chinese.getClass());
  }

  public static void main(String[] args) {
    SpringApplication.run(AopApplication.class, args);
  }

}

这里修改了注入的类,直接注入Chinese类,不再注入Person接口,然后调用eat()方法,输出chinese的类信息,启动服务,调用test接口,观察控制台输出,输出如下:

===before执行了===:class org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint
我正在吃:面
===after执行了===:class org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint
class com.spring.aop.Chinese$$EnhancerBySpringCGLIB$$d7d8054c

可以看到类被CGLIB增强了,也就是动态代理。根据打印的类信息可以看到这里的CGLIB代理就是Spring AOP的代理,这个类也就是所谓的AOP代理,AOP代理类在切点动态地织入了增强处理。

  1. 附录
  • 从Spring 3.2以后不再将CGLIB放在项目的classpath下,而是将CGLIB类打包放在spring-core下面的org.springframework中。这个就意味着基于CGLIB的动态代理与JDK的动态代理在支持“just works”就一样了。
  • 在Spring 4.0中,因为CGLIB代理实例是通过Objenesis创建的,所以代理对象的构造器不再有两次调用。
  • 在 Spring Boot 2.0 中,Spring Boot现在默认使用CGLIB动态代理(基于类的动态代理), 包括AOP。如果需要基于接口的动态代理(JDK基于接口的动态代理) , 需要设置spring.aop.proxy-target-class属性为false,如下修改application.yaml文件(根据配置文件的类型不同,设置方式有差异,这里只是举个例子):
spring:
  aop:
    proxy-target-class: false

调试代码的时候,发现如果将spring.aop.proxy-target-class属性设置为false时,这时注入Chinese类,测试CGLIB动态代理会启动项目失败,需要将其设置为true时才能正常启动成功。提示的错误信息:

***************************
APPLICATION FAILED TO START
***************************

Description:

The bean 'chinese' could not be injected as a 'com.spring.aop.Chinese' because it is a JDK dynamic proxy that implements:
	com.spring.aop.Person


Action:

Consider injecting the bean as one of its interfaces or forcing the use of CGLib-based proxies by setting proxyTargetClass=true on @EnableAsync and/or @EnableCaching.

其实提示说的很明确,不能注入设置为JDK动态代理的类。至于具体原因,有兴趣的可以深入研究一下,这里就不赘述了。

应用

  • 日志记录,性能统计,安全控制,事务处理,异常处理等等。

将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。

补充知识点

AOP中的Joinpoint可以有多种类型:构造方法调用,字段的设置和获取,方法的调用,方法的执行,异常的处理执行,类的初始化。也就是说在AOP的概念中我们可以在上面的这些Joinpoint上织入我们自定义的Advice,但是在Spring AOP中却没有实现上面所有的joinpoint,确切的说,Spring只支持方法执行类型的Joinpoint。

Advice 的类型:

  • before advice: 在 join point 前被执行的 advice. 虽然 before advice 是在 join point 前被执行, 但是它并不能够阻止 join point 的执行, 除非发生了异常(即我们在 before advice 代码中, 不能人为地决定是否继续执行 join point 中的代码)。
  • after return advice:在一个 join point 正常返回后执行的 advice。
  • after throwing advice:当一个 join point 抛出异常后执行的 advice。
  • after(final) advice:无论一个 join point 是正常退出还是发生了异常, 都会被执行的 advice。
  • around advice:在 join point 前和 joint point 退出后都执行的 advice,这个是最常用的 advice。
  • introduction:introduction可以为原有的对象增加新的属性和方法。

切入点表达式:

定义切入点的时候需要一个包含名字和任意参数的签名,还有一个切入点表达式,如:

execution(public * com.example.aop...(..))

切入点表达式的格式:

execution( [可见性] 返回类型 [声明类型] . 方法名 (参数) [异常] )

其中[]内的是可选的,其它的还支持通配符的使用:

  1. “ * ”:匹配所有字符
  2. “..”:一般用于匹配多个包,多个参数
  3. “ + ”:表示类及其子类
  4. 运算符:“ && ”,“ || ”,“ ! ”

关键词使用示例

  1. execution:用于匹配子表达式。
//匹配com.example.model包及其子包中所有类中的所有方法,返回类型任意,方法参数任意
@Pointcut("execution(* com.example.model...(..))")
public void before(){}
  1. within:用于匹配连接点所在的Java类或者包。
//匹配Person类中的所有方法
@Pointcut("within(com.example.model.Person)")
public void before(){}

//匹配com.example包及其子包中所有类中的所有方法
@Pointcut("within(com.example..*)")
public void before(){}
  1. this:用于向通知方法中传入代理对象的引用。
@Before("before() && this(proxy)")
public void beforeAdvice(JoinPoint point, Object proxy){
	//TODO 处理逻辑
}
  1. target:用于向通知方法中传入目标对象的引用。
@Before("before() && target(target)")
public void beforeAdvice(JoinPoint point, Object proxy){
	//处理逻辑
}
  1. args:用于将参数传入到通知方法中。
@Before("before() && args(age,username)")
public void beforeAdvice(JoinPoint point, int age, String username){
	//处理逻辑
}
  1. @within :用于匹配在类一级使用了参数确定的注解的类,其所有方法都将被匹配。
//所有被@AdviceAnnotation标注的类都将匹配
@Pointcut("@within(com.example.annotation.AdviceAnnotation)")
public void before(){}
  1. @target :和@within的功能类似,但必须要指定注解接口的保留策略为RUNTIME。
@Pointcut("@target(com.example.annotation.AdviceAnnotation)")
public void before(){}
  1. @args :传入连接点的对象对应的Java类必须被@args指定的Annotation注解标注。
@Before("@args(com.example.annotation.AdviceAnnotation)")
public void beforeAdvice(JoinPoint point){
	//处理逻辑
}
  1. @annotation :匹配连接点被它参数指定的Annotation注解的方法。也就是说,所有被指定注解标注的方法都将匹配。
@Pointcut("@annotation(com.example.annotation.AdviceAnnotation)")
public void before(){}
  1. bean:通过受管Bean的名字来限定连接点所在的Bean。该关键词是Spring2.5新增的。
@Pointcut("bean(person)")
public void before(){}

持续更新中…

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值