Spring中AOP相关的API及源码解析,原来AOP是这样子的(1)

本文涉及的产品
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
简介: Spring中AOP相关的API及源码解析,原来AOP是这样子的(1)

前言


之所以写这么一篇文章主要是因为下篇文章将结束Spring启动整个流程的分析,从解析配置到创建对象再到属性注入最后再将创建好的对象初始化成为一个真正意义上的Bean。因为下篇文章会设计到AOP,所以提前单独将AOP的相关API及源码做一次解读,这样可以降低阅读源码的障碍,话不多说,我们进入正文!


一个使用API创建代理的例子


在进入API分析前,我们先通过两个例子体会下如何使用API的方式来创建一个代理对象,对应示例如下:


1.定义通知

public class DmzAfterReturnAdvice implements AfterReturningAdvice {
  @Override
  public void afterReturning(@Nullable Object returnValue, Method method, Object[] args, @Nullable Object target) throws Throwable {
    System.out.println("after invoke method [" + method.getName() + "],aop afterReturning logic invoked");
  }
}
public class DmzAroundAdvice implements MethodInterceptor {
  @Override
  public Object invoke(MethodInvocation invocation) throws Throwable {
    System.out.println("aroundAdvice invoked");
    return invocation.proceed();
  }
}
public class DmzBeforeAdvice implements MethodBeforeAdvice {
  @Override
  public void before(Method method, Object[] args, Object target) throws Throwable {
    System.out.println("before invoke method [" + method.getName() + "],aop before logic invoked");
  }
}
public class DmzIntroductionAdvice extends DelegatingIntroductionInterceptor implements Runnable {
  @Override
  public void run() {
    System.out.println("running!!!!");
  }
}

2.切点

public class DmzPointcut implements Pointcut {
  @Override
  @NonNull
  public ClassFilter getClassFilter() {
    // 在类级别上不进行拦截
    return ClassFilter.TRUE;
  }
  @Override
  @NonNull
  public MethodMatcher getMethodMatcher() {
    return new StaticMethodMatcherPointcut() {
      @Override
      public boolean matches(@NonNull Method method, Class<?> targetClass) {
        // 对于toString方法不进行拦截
        return !method.getName().equals("toString");
      }
    };
  }
}

3.目标类

public class DmzService {
  @Override
  public String toString() {
    System.out.println("dmzService toString invoke");
    return "dmzService";
  }
  public void testAop(){
    System.out.println("testAop invoke");
  }
}

4.测试代码

public class Main {
  public static void main(String[] args) {
    ProxyFactory proxyFactory = new ProxyFactory();
    // 一个Advisor代表的是一个已经跟指定切点绑定了的通知
        // 在这个例子中意味着环绕通知不会作用到toString方法上
    Advisor advisor = new DefaultPointcutAdvisor(new DmzPointcut(), new DmzAroundAdvice());
    // 添加一个绑定了指定切点的环绕通知
    proxyFactory.addAdvisor(advisor);
    // 添加一个返回后的通知
    proxyFactory.addAdvice(new DmzAfterReturnAdvice());
    // 添加一个方法执行前的通知
    proxyFactory.addAdvice(new DmzBeforeAdvice());
    // 为代理类引入一个新的需要实现的接口--Runnable
    proxyFactory.addAdvice(new DmzIntroductionAdvice());
    // 设置目标类
    proxyFactory.setTarget(new DmzService());
    // 因为要测试代理对象自己定义的方法,所以这里启用cglib代理
    proxyFactory.setProxyTargetClass(true);
    // 创建代理对象
    Object proxy = proxyFactory.getProxy();
    // 调用代理类的toString方法,通过控制台查看代理逻辑的执行情况
    proxy.toString();
    if (proxy instanceof DmzService) {
      ((DmzService) proxy).testAop();
    }
    // 判断引入是否成功,并执行引入的逻辑
    if (proxy instanceof Runnable) {
      ((Runnable) proxy).run();
    }
  }
}

这里我就不将测试结果放出来了,大家可以先自行思考这段程序将输出什么。接下来我们就来分析上面这段程序中所涉及到的API,通过这些API的学习相信大家可以彻底理解上面这段代码。


API介绍


Pointcut(切点)


对应接口定义如下:

public interface Pointcut {
    // ClassFilter,在类级别进行过滤
  ClassFilter getClassFilter();
  // MethodMatcher,在方法级别进行过滤
  MethodMatcher getMethodMatcher();
    // 一个单例对象,默认匹配所有
  Pointcut TRUE = TruePointcut.INSTANCE;
}

切点的主要作用是定义通知所要应用到的类跟方法,上面的接口定义也很明显的体现了这一点,我们可以将其拆分成为两个部分

  • ClassFilter,接口定义如下:
public interface ClassFilter {
  boolean matches(Class<?> clazz);
  ClassFilter TRUE = TrueClassFilter.INSTANCE;
}

ClassFilter的主要作用是在类级别上对通知的应用进行一次过滤,如果它的match方法对任意的类都返回true的话,说明在类级别上我们不需要过滤,这种情况下,通知的应用,就完全依赖MethodMatcher的匹配结果。

  • MethodMatcher,接口定义如下:
public interface MethodMatcher {
  boolean matches(Method method, @Nullable Class<?> targetClass);
  boolean isRuntime();
  boolean matches(Method method, @Nullable Class<?> targetClass, Object... args);
  MethodMatcher TRUE = TrueMethodMatcher.INSTANCE;
}

MethodMatcher中一共有三个核心方法


  • matches(Method method, @Nullable Class<?> targetClass),这个方法用来判断当前定义的切点跟目标类中的指定方法是否匹配,它可以在创建代理的时候就被调用,从而决定是否需要进行代理,这样就可以避免每次方法执行的时候再去做判断
  • isRuntime(),如果这个方法返回true的话,意味着每次执行方法时还需要做一次匹配
  • matches(Method method, @Nullable Class<?> targetClass, Object... args),当之前的isRuntime方法返回true时,会调用这个方法再次进行一次判断,返回false的话,意味这个不对这个方法应用通知


Advice(通知)


环绕通知(Interception Around Advice)


接口定义如下:

public interface MethodInterceptor extends Interceptor {
    Object invoke(MethodInvocation invocation) throws Throwable;
}

在上面接口定义的invoke方法中,MethodInvocation就是当前执行的方法,当我们调用invocation.proceed就是在执行当前的这个方法,基于此,我们可以在方法的执行前后去插入我们自定义的逻辑,比如下面这样

// 执行前的逻辑
doSomeThingBefore();
Object var = invocation.proceed;
doSomeThingAfter();
// 执行后的逻辑
retrun var;

前置通知(Before Advice)


public interface MethodBeforeAdvice extends BeforeAdvice {
    void before(Method m, Object[] args, Object target) throws Throwable;
}

跟环绕通知不同的是,这个接口中定义的方法的返回值是void,所以前置通知是无法修改方法的返回值的。

如果在前置通知中发生了异常,那么会直接终止目标方法的执行以及打断整个拦截器链的执行


后置通知(After Returning Advice)

public interface AfterReturningAdvice extends Advice {
    void afterReturning(Object returnValue, Method m, Object[] args, Object target)
            throws Throwable;
}

后置通知相比较于前置通知,主要有以下几点不同

  • 后置通知可以访问目标方法的返回值,但是不能修改
  • 后置通知是在方法执行完成后执行


异常通知(Throws Advice)

public interface ThrowsAdvice extends AfterAdvice {
}

异常通知中没有定义任何方法,它更像一个标记接口。我们在定义异常通知时需要实现这个接口,同时方法的签名也有要求


1.方法名称必须是afterThrowing

2.方法的参数个数必须是1个或者4个,如下

public class OneParamThrowsAdvice implements ThrowsAdvice {
    // 如果只有一个参数,那么这个参数必须是要进行处理的异常
    public void afterThrowing(RemoteException ex) throws Throwable {
        // Do something with remote exception
    }
}
public class FourParamThrowsAdvice implements ThrowsAdvice {
    // 如果定义了四个参数,那么这四个参数分别是
    // 1.m:目标方法
    // 2.args:执行目标方法所需要的参数
    // 3.target:目标对象
    // 4.ex:具体要处理的异常
    // 并且参数类型必须按照这个顺序定义
    public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {
        // Do something with all arguments
    }
}

我们可以在一个异常通知中定义多个方法,在后续的源码分析中我们会发现,这些方法最终会被注册成对应的异常的handler,像下面这样

public static class CombinedThrowsAdvice implements ThrowsAdvice {
    public void afterThrowing(RemoteException ex) throws Throwable {
        // Do something with remote exception
    }
    public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {
        // Do something with all arguments
    }
}

引入通知(Introduction Advice)


引入通知的主要作用是可以让生成的代理类实现额外的接口。例如在上面的例子中,我们为DmzService创建一个代理对象,同时为其定义了一个引入通知

public class DmzIntroductionAdvice extends DelegatingIntroductionInterceptor implements Runnable {
  @Override
  public void run() {
    System.out.println("running!!!!");
  }
}

在这个引入通知中,我们为其引入了一个新的需要实现的接口Runnable,同时通知本身作为这个接口的实现类。


通过这个引入通知,我们可以将生成的代理类强转成Runnable类型然后执行其run方法,同时,run方法也会被前面定义的前置通知,后置通知等拦截。


为了更好的了解引入通知,我们来需要了解下DelegatingIntroductionInterceptor这个类。见名知意,这个类就是一个委托引入拦截器,因为我们要为代理类引入新的接口,因为着我们要提供具体的实现的逻辑,而具体的实现的逻辑就可以被委托给这个DelegatingIntroductionInterceptor。


我们可以看看它的源码

public class DelegatingIntroductionInterceptor extends IntroductionInfoSupport
    implements IntroductionInterceptor {
    // 实际实现了引入逻辑的类
  @Nullable
  private Object delegate;
    // 对外提供了一个带参的构造函数,通过这个构造函数我们可以传入一个
    // 具体的实现类
  public DelegatingIntroductionInterceptor(Object delegate) {
    init(delegate);
  }
    // 对子类暴露了一个空参的构造函数,默认将自身作为实现了引入逻辑的委托类
    // 我们上面的例子中就是使用的这种方法
  protected DelegatingIntroductionInterceptor() {
    init(this);
  }
    // 对这个类进行初始化,要通过实际的实现类来找到具体要实现的接口
  private void init(Object delegate) {
    Assert.notNull(delegate, "Delegate must not be null");
    this.delegate = delegate;
        // 找到delegate所有实现的接口
    implementInterfacesOnObject(delegate);
        // 因为我们可能会将DelegatingIntroductionInterceptor本身作为委托者
        // Spring的设计就是不对外暴露这两个接口
        // 如果将其暴露,意味着我们可以将代理类强转成这种类型
    suppressInterface(IntroductionInterceptor.class);
    suppressInterface(DynamicIntroductionAdvice.class);
  }
  // 引入通知本身也是基于拦截器实现的,当执行一个方法时需要判断这个方法
    // 是不是被引入的接口中定义的方法,如果是的话,那么不能调用目标类的方法
    // 而要调用委托类的方法
  public Object invoke(MethodInvocation mi) throws Throwable {
    if (isMethodOnIntroducedInterface(mi)) {
      Object retVal = AopUtils.invokeJoinpointUsingReflection(this.delegate, mi.getMethod(), mi.getArguments());
      // 这里是处理一种特殊情况,方法的返回值是this的时候
            // 这里应该返回代理类
      if (retVal == this.delegate && mi instanceof ProxyMethodInvocation) {
        Object proxy = ((ProxyMethodInvocation) mi).getProxy();
        if (mi.getMethod().getReturnType().isInstance(proxy)) {
          retVal = proxy;
        }
      }
            // 其余情况下直接将委托类的执行结果返回
      return retVal;
    }
        // 执行到这里说明不是引入的方法,这是Spring提供了一个扩展逻辑
        // 正常来说这个类只会处理引入的逻辑,通过这个方法可以对目标类中的方法做拦截
        // 不常用
    return doProceed(mi);
  }
  protected Object doProceed(MethodInvocation mi) throws Throwable {
    return mi.proceed();
  }
}

通过查看这个类的源码我们可以发现,所谓的引入其实就是在方法执行的时候加了一层拦截,当判断这个方法是被引入的接口提供的方法的时候,那么就执行委托类中的逻辑而不是目标类中的方法

相关文章
|
15天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
45 2
|
2月前
|
搜索推荐 Java Spring
Spring Filter深度解析
【10月更文挑战第21天】Spring Filter 是 Spring 框架中非常重要的一部分,它为请求处理提供了灵活的控制和扩展机制。通过合理配置和使用 Filter,可以实现各种个性化的功能,提升应用的安全性、可靠性和性能。还可以结合具体的代码示例和实际应用案例,进一步深入探讨 Spring Filter 的具体应用和优化技巧,使对它的理解更加全面和深入。
|
15天前
|
存储 安全 Linux
Golang的GMP调度模型与源码解析
【11月更文挑战第11天】GMP 调度模型是 Go 语言运行时系统的核心部分,用于高效管理和调度大量协程(goroutine)。它通过少量的操作系统线程(M)和逻辑处理器(P)来调度大量的轻量级协程(G),从而实现高性能的并发处理。GMP 模型通过本地队列和全局队列来减少锁竞争,提高调度效率。在 Go 源码中,`runtime.h` 文件定义了关键数据结构,`schedule()` 和 `findrunnable()` 函数实现了核心调度逻辑。通过深入研究 GMP 模型,可以更好地理解 Go 语言的并发机制。
|
16天前
|
API 数据安全/隐私保护
抖音视频,图集无水印直链解析免费API接口教程
该接口用于解析抖音视频和图集的无水印直链地址。请求地址为 `https://cn.apihz.cn/api/fun/douyin.php`,支持POST或GET请求。请求参数包括用户ID、用户KEY和视频或图集地址。返回参数包括状态码、信息提示、作者昵称、标题、视频地址、封面、图集和类型。示例请求和返回数据详见文档。
|
28天前
|
消息中间件 缓存 安全
Future与FutureTask源码解析,接口阻塞问题及解决方案
【11月更文挑战第5天】在Java开发中,多线程编程是提高系统并发性能和资源利用率的重要手段。然而,多线程编程也带来了诸如线程安全、死锁、接口阻塞等一系列复杂问题。本文将深度剖析多线程优化技巧、Future与FutureTask的源码、接口阻塞问题及解决方案,并通过具体业务场景和Java代码示例进行实战演示。
43 3
|
3月前
Micronaut AOP与代理机制:实现应用功能增强,无需侵入式编程的秘诀
AOP(面向切面编程)能够帮助我们在不修改现有代码的前提下,为应用程序添加新的功能或行为。Micronaut框架中的AOP模块通过动态代理机制实现了这一目标。AOP将横切关注点(如日志记录、事务管理等)从业务逻辑中分离出来,提高模块化程度。在Micronaut中,带有特定注解的类会在启动时生成代理对象,在运行时拦截方法调用并执行额外逻辑。例如,可以通过创建切面类并在目标类上添加注解来记录方法调用信息,从而在不侵入原有代码的情况下增强应用功能,提高代码的可维护性和可扩展性。
64 1
|
27天前
|
安全 Java 编译器
什么是AOP面向切面编程?怎么简单理解?
本文介绍了面向切面编程(AOP)的基本概念和原理,解释了如何通过分离横切关注点(如日志、事务管理等)来增强代码的模块化和可维护性。AOP的核心概念包括切面、连接点、切入点、通知和织入。文章还提供了一个使用Spring AOP的简单示例,展示了如何定义和应用切面。
72 1
什么是AOP面向切面编程?怎么简单理解?
|
1月前
|
XML Java 开发者
论面向方面的编程技术及其应用(AOP)
【11月更文挑战第2天】随着软件系统的规模和复杂度不断增加,传统的面向过程编程和面向对象编程(OOP)在应对横切关注点(如日志记录、事务管理、安全性检查等)时显得力不从心。面向方面的编程(Aspect-Oriented Programming,简称AOP)作为一种新的编程范式,通过将横切关注点与业务逻辑分离,提高了代码的可维护性、可重用性和可读性。本文首先概述了AOP的基本概念和技术原理,然后结合一个实际项目,详细阐述了在项目实践中使用AOP技术开发的具体步骤,最后分析了使用AOP的原因、开发过程中存在的问题及所使用的技术带来的实际应用效果。
57 5
|
2月前
|
Java 容器
AOP面向切面编程
AOP面向切面编程
43 0
|
3月前
Micronaut AOP与代理机制:实现应用功能增强,无需侵入式编程的秘诀
【9月更文挑战第9天】AOP(面向切面编程)通过分离横切关注点提高模块化程度,如日志记录、事务管理等。Micronaut AOP基于动态代理机制,在应用启动时为带有特定注解的类生成代理对象,实现在运行时拦截方法调用并执行额外逻辑。通过简单示例展示了如何在不修改 `CalculatorService` 类的情况下记录 `add` 方法的参数和结果,仅需添加 `@Loggable` 注解即可。这不仅提高了代码的可维护性和可扩展性,还降低了引入新错误的风险。
48 13