MiniSpring框架学习笔记-拦截器 :如何在方法前后进行拦截?

MiniSpring框架学习笔记-拦截器 :如何在方法前后进行拦截?

教程: https://github.com/YaleGuo/minis

极客时间: 手把手带你写一个 MiniSpring

前言:MiniSpring 将 Java 面向对象的设计思想体现得非常充分:需要提供某种能力,就先抽象成接口;需要创建某类对象,就通过工厂负责创建。整体结构不断分层细化,上层工厂负责组织流程和组装组件,下层工厂负责创建具体实现,从而降低模块之间的耦合。以动态代理为例,ProxyFactoryBean 负责准备 targetAdvisorAopProxyFactory 负责选择代理实现,JdkDynamicAopProxy 最终调用 JDK API 创建代理对象。每一层只关注自己的职责,既便于扩展,也便于替换具体实现。

18. 拦截器:如何在方法前后进行拦截?

上一节我们已经用 JDK 动态代理给 doAction() 加上了日志。不过,增强逻辑还是直接写在代理类里:

java 复制代码
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if ("doAction".equals(method.getName())) {
        System.out.println("-----before call real object, dynamic proxy........");
    }
    return method.invoke(this.target, args);
}

这段代码能工作,但代理类同时负责了两件事:

  1. 创建并调用代理对象;
  2. 编写日志、事务等具体增强逻辑。

以后每增加一种增强,都要继续修改代理类。为了解耦,本节把增强逻辑抽成独立的 AdviceMethodInterceptor,代理类只负责在合适的位置调用它们。

一、先看最终使用方式

业务代码仍然只从 IoC 容器中获取 IAction

java 复制代码
IAction action = (IAction) context.getBean("action");
action.doAction();

调用方看不到代理创建过程。它只知道自己拿到了一个 IAction,但这个对象实际上是 ProxyFactoryBean 创建的 JDK 动态代理。

项目当前的 beans.xml 配置如下:

xml 复制代码
<bean id="realaction" class="com.chenhai.test.Action1"/>

<bean id="myBeforeAdvice" class="com.chenhai.test.MyBeforeAdvice"/>
<bean id="myAfterAdvice" class="com.chenhai.test.MyAfterAdvice"/>
<bean id="tracingInterceptor" class="com.chenhai.test.TracingInterceptor"/>

<bean id="action" class="com.chenhai.aop.ProxyFactoryBean">
    <property type="java.lang.Object" name="target" ref="realaction"/>
    <property type="String" name="interceptorName" value="myAfterAdvice"/>
</bean>

这里有三个关键对象:

  • realaction:真实业务对象;
  • myAfterAdvice:需要插入的增强逻辑;
  • action:代理工厂,对外返回代理对象。

整个过程可以先记成一条主线:

text 复制代码
context.getBean("action")
        ↓
容器发现 action 是 ProxyFactoryBean
        ↓
ProxyFactoryBean 找到 target 和 interceptorName
        ↓
创建 JdkDynamicAopProxy,再生成 JDK 代理对象
        ↓
调用 action.doAction()
        ↓
MethodInterceptor.invoke(...)
        ↓
MethodInvocation.proceed()
        ↓
反射调用 realaction.doAction()

下面再从上往下拆开这条流程。

二、把增强统一成 MethodInterceptor

先定义三个接口:

java 复制代码
package com.chenhai.aop;

/**
 * AOP 通知的顶层标记接口。
 */
public interface Advice {
}
java 复制代码
package com.chenhai.aop;

/**
 * 拦截器类型的通知。
 */
public interface Interceptor extends Advice {
}
java 复制代码
package com.chenhai.aop;

/**
 * 环绕一次方法调用的拦截器。
 */
public interface MethodInterceptor extends Interceptor {

    Object invoke(MethodInvocation invocation) throws Throwable;
}

它们的关系很简单:

text 复制代码
Advice
  └─ Interceptor
       └─ MethodInterceptor

Advice 表示"这是一段通知",本身不定义执行方式;Interceptor 表示这段通知会参与调用过程;MethodInterceptor 进一步约定,方法增强统一从 invoke() 进入。

MethodInterceptor 不仅能在真实方法执行前后加逻辑,还能决定是否调用 proceed()。如果不调用,真实方法就不会继续执行。因此它表达的是一种环绕调用能力。

三、用 MethodInvocation 表示一次方法调用

拦截器需要知道当前调用了哪个方法、参数是什么、目标对象是谁,还需要一种方式继续执行真实方法。于是我们把这些信息封装进 MethodInvocation

java 复制代码
package com.chenhai.aop;

import java.lang.reflect.Method;

/**
 * 一次被代理方法调用的上下文。
 */
public interface MethodInvocation {

    Method getMethod();

    Object[] getArguments();

    Object getThis();

    Object proceed() throws Throwable;
}

四个方法分别表示:

方法 含义
getMethod() 当前被调用的方法
getArguments() 调用参数
getThis() 当前项目中返回真实目标对象 target
proceed() 继续执行调用,最终进入真实方法

当前教学版使用反射实现 proceed()。构造时还会把无参方法可能传入的 null 参数统一成空数组:

java 复制代码
protected ReflectiveMethodInvocation(Object proxy, Object target, Method method,
                                     Object[] arguments, Class<?> targetClass) {
    this.proxy = proxy;
    this.target = target;
    this.method = method;
    this.arguments = arguments == null ? new Object[0] : arguments;
    this.targetClass = targetClass;
}

@Override
public Object proceed() throws Throwable {
    try {
        return this.method.invoke(this.target, this.arguments);
    } catch (InvocationTargetException e) {
        // 反射会包一层 InvocationTargetException,这里还原真实业务异常。
        throw e.getTargetException();
    }
}

其余 getter 只是返回构造时保存的方法、参数和目标对象。当前版本只有一个拦截器,所以 proceed() 会直接反射调用目标方法。以后扩展成多个拦截器时,proceed() 还可以表示"继续调用下一个拦截器"。

四、把具体增强逻辑搬出代理类

有了统一接口,日志逻辑就可以写成独立的 TracingInterceptor

java 复制代码
package com.chenhai.test;

import com.chenhai.aop.MethodInterceptor;
import com.chenhai.aop.MethodInvocation;

import java.util.Arrays;

/**
 * 环绕通知示例:在真实方法前后都插入日志。
 */
public class TracingInterceptor implements MethodInterceptor {

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        System.out.println("method " + invocation.getMethod() + " is called on "
                + invocation.getThis() + " with args "
                + Arrays.toString(invocation.getArguments()));

        // 继续执行真实方法。
        Object ret = invocation.proceed();

        System.out.println("method " + invocation.getMethod() + " returns " + ret);
        return ret;
    }
}

proceed() 的位置决定了增强逻辑的执行位置:

text 复制代码
proceed() 前面的代码
        ↓
真实业务方法
        ↓
proceed() 后面的代码

这里没有用 finally,因此真实方法抛出异常时,proceed() 后面的日志不会执行。这种写法表达的是"正常返回后增强",不是"无论成功失败都执行"。

五、把前置、后置通知适配成统一拦截器

除了直接实现 MethodInterceptor,项目还提供了更专门的前置通知和后置返回通知:

java 复制代码
// MethodBeforeAdvice.java
public interface MethodBeforeAdvice extends BeforeAdvice {

    void before(Method method, Object[] args, Object target) throws Throwable;
}
java 复制代码
// AfterReturningAdvice.java
public interface AfterReturningAdvice extends AfterAdvice {

    void afterReturning(Object returnValue, Method method,
                        Object[] args, Object target) throws Throwable;
}

这两个接口的调用方式不同,代理类如果分别处理它们,就又会出现很多分支。因此项目使用适配器,把它们统一转换成 MethodInterceptor

java 复制代码
// MethodBeforeAdviceInterceptor:先执行前置通知,再继续真实调用。
@Override
public Object invoke(MethodInvocation mi) throws Throwable {
    this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis());
    return mi.proceed();
}
java 复制代码
// AfterReturningAdviceInterceptor:真实方法正常返回后,再执行后置通知。
@Override
public Object invoke(MethodInvocation mi) throws Throwable {
    Object retVal = mi.proceed();
    this.advice.afterReturning(
            retVal, mi.getMethod(), mi.getArguments(), mi.getThis());
    return retVal;
}

适配之后,动态代理只需要认识 MethodInterceptor,不用关心配置的是前置通知、后置通知还是环绕拦截器。

六、用 Advisor 保存代理需要的拦截器

Advisor 负责把代理配置和通知连接起来。当前教学版只保存一个 MethodInterceptor

java 复制代码
package com.chenhai.aop;

public interface Advisor {

    MethodInterceptor getMethodInterceptor();

    void setMethodInterceptor(MethodInterceptor methodInterceptor);
}

DefaultAdvisor 是它的最小实现,内部只有一个 MethodInterceptor 字段和对应的 getter、setter。在完整 Spring 中,能够按方法匹配的 Advisor(例如 PointcutAdvisor)还会关联切点,用来判断哪些类、哪些方法需要增强。当前项目还没有实现 Pointcut,所以方法匹配暂时放在 JdkDynamicAopProxy 中。

七、ProxyFactoryBean 组装代理

XML 里只配置了 interceptorNameProxyFactoryBean 还要回到容器中找到对应的通知 Bean。为此,它实现了 BeanFactoryAware

BeanFactoryAware 只定义一个 setBeanFactory(BeanFactory beanFactory) 方法。容器完成普通属性注入后,会把自己回调给实现了该接口的 Bean:

java 复制代码
private void invokeAwareMethods(Object bean) throws BeansException {
    if (bean instanceof BeanFactoryAware) {
        ((BeanFactoryAware) bean).setBeanFactory(this);
    }
}

于是 ProxyFactoryBean 可以根据名称取得通知,并把它适配成统一的 MethodInterceptor

java 复制代码
private synchronized void initializeAdvisor() {
    if (this.advisor != null
            || this.interceptorName == null
            || this.interceptorName.trim().isEmpty()) {
        return;
    }
    if (this.beanFactory == null) {
        throw new IllegalStateException(
                "BeanFactory is required to resolve interceptorName");
    }

    Object advice;
    try {
        advice = this.beanFactory.getBean(this.interceptorName);
    } catch (BeansException e) {
        throw new IllegalStateException(
                "Resolve interceptor failed: " + this.interceptorName, e);
    }

    MethodInterceptor methodInterceptor = adaptAdvice(advice);
    DefaultAdvisor defaultAdvisor = new DefaultAdvisor();
    defaultAdvisor.setMethodInterceptor(methodInterceptor);
    this.advisor = defaultAdvisor;
}

private MethodInterceptor adaptAdvice(Object advice) {
    if (advice instanceof MethodBeforeAdvice) {
        return new MethodBeforeAdviceInterceptor((MethodBeforeAdvice) advice);
    }
    if (advice instanceof AfterReturningAdvice) {
        return new AfterReturningAdviceInterceptor((AfterReturningAdvice) advice);
    }
    if (advice instanceof MethodInterceptor) {
        return (MethodInterceptor) advice;
    }
    throw new IllegalArgumentException("Unsupported advice type: " + advice);
}

准备好 Advisor 之后,再把目标对象和 Advisor 一起交给代理工厂:

java 复制代码
protected AopProxy createAopProxy() {
    if (this.target == null) {
        throw new IllegalStateException("Property 'target' is required");
    }
    initializeAdvisor();
    return getAopProxyFactory().createAopProxy(this.target, this.advisor);
}

这里几层对象的职责不要混在一起:

对象 职责
ProxyFactoryBean 从容器取配置、适配通知、组织代理创建
AopProxyFactory 选择并创建一种 AopProxy 实现
JdkDynamicAopProxy 使用 JDK API 生成代理对象并处理方法调用

当前 DefaultAopProxyFactory 只选择 JDK 动态代理:

java 复制代码
package com.chenhai.aop;

public class DefaultAopProxyFactory implements AopProxyFactory {

    @Override
    public AopProxy createAopProxy(Object target, Advisor advisor) {
        return new JdkDynamicAopProxy(target, advisor);
    }
}

八、JDK 动态代理执行拦截器

JdkDynamicAopProxy.getProxy() 负责生成代理对象。目标类必须实现接口,这是 JDK 动态代理的限制:

java 复制代码
@Override
public Object getProxy() {
    Class<?>[] interfaces = this.target.getClass().getInterfaces();
    if (interfaces.length == 0) {
        throw new IllegalStateException(
                "JDK dynamic proxy requires target to implement an interface");
    }

    return Proxy.newProxyInstance(
            JdkDynamicAopProxy.class.getClassLoader(),
            interfaces,
            this);
}

最后,代理对象的方法调用会进入 InvocationHandler.invoke()

java 复制代码
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    MethodInterceptor interceptor = getMethodInterceptor();
    if ("doAction".equals(method.getName()) && interceptor != null) {
        Class<?> targetClass =
                this.target == null ? null : this.target.getClass();
        MethodInvocation invocation =
                new ReflectiveMethodInvocation(
                        proxy, this.target, method, args, targetClass);
        return interceptor.invoke(invocation);
    }

    // 本章只增强 doAction,其他方法仍然转发给真实对象。
    return invokeTarget(method, args);
}

private MethodInterceptor getMethodInterceptor() {
    return this.advisor == null
            ? null
            : this.advisor.getMethodInterceptor();
}

private Object invokeTarget(Method method, Object[] args) throws Throwable {
    try {
        return method.invoke(this.target, args);
    } catch (InvocationTargetException e) {
        throw e.getTargetException();
    }
}

这里不能让其他方法直接返回 null。代理对象上的 toString()equals() 等调用同样会进入 invoke(),不需要增强的方法也应该正常转发给真实对象。

目前用方法名硬编码 "doAction" 只是为了聚焦本节主题。完整做法应该由切点统一判断目标类和目标方法,不能一直把匹配规则写在动态代理里。

九、容器为什么返回代理对象

XML 中注册的 action 实际上是 ProxyFactoryBean。容器取 Bean 时,会先判断它是不是 FactoryBean

java 复制代码
protected Object getObjectForBeanInstance(
        Object beanInstance, String beanName) {
    if (!(beanInstance instanceof FactoryBean)) {
        return beanInstance;
    }

    FactoryBean<?> factory = (FactoryBean<?>) beanInstance;
    return getObjectFromFactoryBean(factory, beanName);
}

如果是,就调用它的 getObject()ProxyFactoryBean 在这里创建并缓存代理对象:

java 复制代码
@Override
public Object getObject() {
    return getSingletonInstance();
}

private synchronized Object getSingletonInstance() {
    if (this.singletonInstance == null) {
        this.singletonInstance = getProxy(createAopProxy());
    }
    return this.singletonInstance;
}

protected Object getProxy(AopProxy aopProxy) {
    return aopProxy.getProxy();
}

因此,容器内部管理的是 ProxyFactoryBean,业务代码通过 getBean("action") 拿到的却是它生产的代理对象。这就是代理创建过程可以对业务代码隐藏起来的原因。

十、运行结果

项目当前配置的是:

xml 复制代码
<property type="String" name="interceptorName" value="myAfterAdvice"/>

所以调用 action.doAction() 时,真实方法先执行,后置返回通知再执行:

text 复制代码
real action doAction........
----------my interceptor after method call----------

也可以切换 interceptorName 来观察不同执行顺序:

配置值 执行顺序
myBeforeAdvice 前置通知 → 真实方法
myAfterAdvice 真实方法 → 后置返回通知
tracingInterceptor 调用日志 → 真实方法 → 返回日志

当前版本一次只能配置一个通知,还没有形成多拦截器调用链。

项目中的 AopAdviceTest 验证的是前置通知。如果 beans.xml 仍配置为 myAfterAdvice,这个测试会因为找不到前置通知输出而失败;运行该测试前,需要把配置值切换为 myBeforeAdvice,或者把测试目标改成验证后置通知。

小结

这一节完成的核心变化,是把具体增强逻辑从动态代理中抽了出来:

text 复制代码
动态代理负责拦截方法
        ↓
Advisor 提供 MethodInterceptor
        ↓
MethodInterceptor 负责增强
        ↓
MethodInvocation.proceed() 调用真实方法

这样一来,代理创建逻辑保持稳定,日志、前置通知和后置通知可以独立编写。业务代码仍然只面向 IAction,代理对象则由 IoC 容器通过 ProxyFactoryBean 自动创建。

相关推荐
景同学4 小时前
把 AI 用到线上运维:可行、有效,前提是喂足信息——一次 Full GC 排障实录
java·人工智能·后端
寒月小酒4 小时前
第五章 生成集成 和第六章 RAG评估(all-in-rag学习)
数据库·学习
摇滚侠4 小时前
《RocketMQ 官网》阅读笔记 RocketMQ 消息队列 MessageQueue 消息 Messagege
笔记·rocketmq
C++、Java和Python的菜鸟4 小时前
第7章 后端Web实战(Tlias系统)
java
魔城烟雨4 小时前
从零开始学习betaflight《5-坐标系》
学习
笔夏4 小时前
【移芯平台CAT1模组】烧写固件
stm32·单片机·学习
憧憬成为java架构高手的小白4 小时前
黑马八股--spring框架学习
java·学习·spring
Ztt6666666664 小时前
【无标题】
经验分享·笔记·其他·百度·微信公众平台
唐青枫5 小时前
Java Picocli 实战详解:用注解写出好用的命令行工具
java
依然范特东5 小时前
强化学习笔记4--TD算法
笔记