InvocationHandler 与动态代理综合案例 | JavaSE

InvocationHandler 与动态代理综合案例

一、学习目标

完成本章后,你应该能够:

  • 能够解释 InvocationHandler 在 JDK 动态代理中的职责。
  • 能够准确解释 invoke(proxy, method, args) 三个参数。
  • 能够理解代理方法调用为什么会自动进入 InvocationHandler.invoke()
  • 能够使用 Method.invoke() 调用真实目标对象。
  • 能够正确处理方法参数和返回值。
  • 能够编写通用 ProxyUtil
  • 能够使用动态代理实现日志、耗时统计和异常记录。
  • 能够分析动态代理中的递归调用、类型转换和异常传播问题。

二、核心知识

2.1 InvocationHandler 是什么

InvocationHandler 位于:

java.lang.reflect

包下。

它是一个接口:

public interface InvocationHandler

核心抽象方法:

Object invoke(
        Object proxy,
        Method method,
        Object[] args
) throws Throwable;

每一个 JDK 动态代理实例都会关联一个:

InvocationHandler

当代理对象上的接口方法被调用时,对应的方法调用会被分派给:

invoke(...)

处理。

因此:

InvocationHandler 是代理对象的方法调用处理器。


2.2 invoke() 到底是谁调用的

这是本章最重要的问题之一。

假设:

UserService proxy = ...

客户端:

proxy.login(
        "admin",
        "123456"
);

我们并没有自己写:

handler.invoke(...)

但它仍然会执行。

调用关系:

客户端调用
proxy.login(...)
↓
运行时生成的代理类接收到调用
↓
代理机制将这次调用封装
↓
自动分派到 InvocationHandler.invoke(...)

也就是说:

invoke() 通常不是业务程序员直接调用的,而是代理对象的方法被调用时,由 JDK 动态代理机制自动调用。


2.3 proxy 参数

第一个参数:

Object proxy

表示:

当前被调用的代理对象本身。

注意:

它不是:

target

真实业务对象。

必须明确区分:

proxy
↓
JDK 创建出来的代理对象

target
↓
真正完成业务的真实对象

例如:

UserService target =
        new UserServiceImpl();

UserService proxy =
        createProxy(target);

进入:

invoke(
    Object proxy,
    Method method,
    Object[] args
)

第一个:

proxy

对应外面的动态代理对象。


2.4 method 参数

第二个参数:

Method method

表示:

客户端当前调用的接口方法对应的反射 Method 对象。

例如:

proxy.login(
        "admin",
        "123456"
);

那么:

method.getName()

可以得到:

login

还可以获得:

method.getParameterTypes();

method.getReturnType();

method.getDeclaringClass();

因此代理处理器甚至不需要提前知道:

这次调用到底是哪一个方法

只需要统一读取:

method

即可。

这就是动态代理能够实现:

通用方法增强

的重要原因。


2.5 args 参数

第三个参数:

Object[] args

表示:

客户端调用方法时传入的实参。

例如:

proxy.login(
        "admin",
        "123456"
);

那么逻辑上:

args[0] = "admin"
args[1] = "123456"

可以:

System.out.println(
        Arrays.deepToString(args)
);

输出参数。

需要特别注意:

如果接口方法没有参数,args 允许为 null

因此通用代码不要假设:

args.length

永远安全。


2.6 invoke() 的返回值

invoke() 返回:

Object

这个返回值会进一步成为:

代理方法调用返回给客户端的结果。

例如接口:

String login(
        String username,
        String password
);

真实调用:

Object result =
        method.invoke(
                target,
                args
        );

应该:

return result;

最终客户端:

String result =
        proxy.login(
                "admin",
                "123456"
        );

就能得到真实业务的结果。

调用链:

target.login()
↓
返回 "SUCCESS"
↓
method.invoke()
↓
result
↓
InvocationHandler.invoke() return
↓
proxy.login() return
↓
客户端得到 "SUCCESS"

因此:

有返回值的方法必须认真处理 invoke() 的返回结果。


三、使用方法

3.1 一个完整的 UserService

接口:

public interface UserService {

    String login(
            String username,
            String password
    );

    void deleteUsers(int... ids);
}

真实实现:

import java.util.Arrays;

public class UserServiceImpl
        implements UserService {

    @Override
    public String login(
            String username,
            String password
    ) {

        if ("admin".equals(username)
                && "123456".equals(password)) {

            return "SUCCESS";
        }

        return "FAIL";
    }

    @Override
    public void deleteUsers(int... ids) {

        System.out.println(
                "删除用户:"
                        + Arrays.toString(ids)
        );
    }
}

3.2 编写最基础的 ProxyUtil

import java.lang.reflect.Proxy;

public class ProxyUtil {

    public static UserService createProxy(
            UserService target
    ) {

        return (UserService)
                Proxy.newProxyInstance(

                        target.getClass()
                                .getClassLoader(),

                        target.getClass()
                                .getInterfaces(),

                        (proxy, method, args) -> {

                            return method.invoke(
                                    target,
                                    args
                            );
                        }
                );
    }
}

这已经是一个可以工作的动态代理。

但它目前:

只是转发

还没有真正增强。


3.3 增加方法日志

return (UserService)
        Proxy.newProxyInstance(
                target.getClass()
                        .getClassLoader(),

                target.getClass()
                        .getInterfaces(),

                (proxy, method, args) -> {

                    System.out.println(
                            "调用方法:"
                                    + method.getName()
                    );

                    Object result =
                            method.invoke(
                                    target,
                                    args
                            );

                    return result;
                }
        );

于是所有代理方法都可以统一输出:

login
deleteUsers
...

而不需要:

if ("login".equals(...))

才能记录基本日志。


3.4 输出参数

import java.util.Arrays;

System.out.println(
        "参数:"
                + Arrays.deepToString(args)
);

完整一点:

String argsText =
        args == null
                ? "[]"
                : Arrays.deepToString(args);

System.out.println(
        "参数:" + argsText
);

为什么推荐:

Arrays.deepToString(args)

因为参数本身还可能包含数组。

例如:

deleteUsers(1, 2, 3);

对应的可变参数在反射层面可能表现为一个数组参数。


3.5 记录返回值

Object result =
        method.invoke(
                target,
                args
        );

System.out.println(
        "返回值:" + result
);

return result;

如果真实方法:

void deleteUsers(...)

则:

Method.invoke(...)

返回:

null

这是正常现象。


3.6 增加耗时统计

推荐使用:

System.nanoTime()

测量一段代码执行所经过的时间。

long start =
        System.nanoTime();

Object result =
        method.invoke(
                target,
                args
        );

long end =
        System.nanoTime();

System.out.println(
        "耗时:"
                + (end - start)
                + " ns"
);

return result;

比起:

currentTimeMillis()

这里关注的是:

持续时间(elapsed time)

而不是当前墙上时间,因此 nanoTime() 更符合这个语义。


3.7 为什么应该使用 finally 统计耗时

下面代码:

long start =
        System.nanoTime();

Object result =
        method.invoke(target, args);

long end =
        System.nanoTime();

存在问题。

如果:

method.invoke(...)

过程中出现异常:

long end

后面的代码根本不会执行。

更稳妥:

long start =
        System.nanoTime();

try {

    return method.invoke(
            target,
            args
    );

} finally {

    long cost =
            System.nanoTime() - start;

    System.out.println(
            "耗时:" + cost + " ns"
    );
}

这样无论:

成功
还是
异常

都能记录耗时。


3.8 Method.invoke 的异常包装

这里是动态代理一个非常重要的 Debug 点。

假设真实方法:

public void work() {

    throw new IllegalArgumentException(
            "业务参数错误"
    );
}

我们通过:

method.invoke(target, args);

反射调用时,目标方法抛出的异常会被:

InvocationTargetException

包装。

也就是:

真实业务异常
IllegalArgumentException
        ↓
Method.invoke()
        ↓
InvocationTargetException

因此如果希望代理继续传播真实业务异常,可以:

catch (InvocationTargetException e) {

    throw e.getCause();
}

这样可以拿到原始异常。


3.9 一个完整的通用 ProxyUtil

下面设计一个更通用的代理工具。

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.Objects;

public final class ProxyUtil {

    private ProxyUtil() {
    }

    public static <T> T createProxy(
            Class<T> interfaceType,
            T target
    ) {

        Objects.requireNonNull(
                interfaceType,
                "interfaceType"
        );

        Objects.requireNonNull(
                target,
                "target"
        );

        if (!interfaceType.isInterface()) {

            throw new IllegalArgumentException(
                    "interfaceType 必须是接口"
            );
        }

        if (!interfaceType.isInstance(target)) {

            throw new IllegalArgumentException(
                    "target 必须实现指定接口"
            );
        }

        Object proxy =
                Proxy.newProxyInstance(

                        target.getClass()
                                .getClassLoader(),

                        new Class<?>[]{
                                interfaceType
                        },

                        (proxyObject,
                         method,
                         args) -> {

                            long start =
                                    System.nanoTime();

                            String argsText =
                                    args == null
                                            ? "[]"
                                            : Arrays.deepToString(
                                                    args
                                            );

                            System.out.println(
                                    "[CALL] "
                                            + method.getName()
                                            + " args="
                                            + argsText
                            );

                            try {

                                Object result =
                                        method.invoke(
                                                target,
                                                args
                                        );

                                System.out.println(
                                        "[RETURN] "
                                                + method.getName()
                                                + " result="
                                                + result
                                );

                                return result;

                            } catch (
                                    InvocationTargetException e
                            ) {

                                Throwable cause =
                                        e.getCause();

                                System.out.println(
                                        "[ERROR] "
                                                + method.getName()
                                                + " -> "
                                                + cause
                                );

                                throw cause;

                            } finally {

                                long cost =
                                        System.nanoTime()
                                                - start;

                                System.out.println(
                                        "[COST] "
                                                + method.getName()
                                                + " "
                                                + cost
                                                + " ns"
                                );
                            }
                        }
                );

        return interfaceType.cast(proxy);
    }
}

使用:

public class Demo {

    public static void main(String[] args) {

        UserService target =
                new UserServiceImpl();

        UserService proxy =
                ProxyUtil.createProxy(
                        UserService.class,
                        target
                );

        String result =
                proxy.login(
                        "admin",
                        "123456"
                );

        System.out.println(result);

        proxy.deleteUsers(
                1,
                2,
                3
        );
    }
}

调用链:

proxy.login(...)
↓
InvocationHandler.invoke()
↓
记录方法名
↓
记录参数
↓
开始计时
↓
method.invoke(target, args)
↓
UserServiceImpl.login(...)
↓
得到结果
↓
记录返回值
↓
finally 记录耗时
↓
结果返回客户端

这已经是一个真正具有:

通用代理增强

意味的工具了。


四、原理与进阶

4.1 为什么一个 invoke 可以处理所有方法

静态代理中:

login()
deleteUser()
updateUser()

通常都需要分别重写。

而动态代理只需要:

invoke(
        proxy,
        method,
        args
)

因为调用的差异已经被抽象成:

method
+
args

例如:

login("admin", "123456")
↓
method = login
args = ["admin", "123456"]

另一次:

deleteUser(1001)
↓
method = deleteUser
args = [1001]

因此同一个:

InvocationHandler

可以处理大量不同方法。

这就是:

将“具体方法调用”抽象成数据。

这是反射和动态代理非常重要的编程思想。


4.2 method.invoke(target, args) 与代理递归

错误代码:

method.invoke(
        proxy,
        args
);

调用链可能变成:

proxy.login()
↓
handler.invoke()
↓
method.invoke(proxy)
↓
proxy.login()
↓
handler.invoke()
↓
method.invoke(proxy)
↓
...

不断递归。

最终可能导致:

StackOverflowError

所以通常应该:

method.invoke(
        target,
        args
);

即:

代理负责拦截,目标对象负责真正执行。


4.3 Object 方法也需要注意

代理对象本身仍然是:

Object

所以业务中可能调用:

proxy.toString();

proxy.hashCode();

proxy.equals(other);

对于 JDK 动态代理,这类方法也需要纳入 InvocationHandler 的设计考虑。

因此在真正通用的代理处理器中,不要默认:

进入 invoke() 的一定是业务接口中最核心的业务方法。

必要时可以判断:

method.getDeclaringClass()

或者:

method.getName()

决定是否增强。


4.4 异常传播为什么必须认真处理

InvocationHandler.invoke() 可以:

throws Throwable

但接口方法本身可能只声明:

void work();

如果代理处理器抛出一个:

接口方法没有声明的受检异常

代理调用方可能看到:

UndeclaredThrowableException

因此动态代理中的异常处理必须考虑两层:

真实业务异常
↓
Method.invoke 的包装
↓
InvocationTargetException

以及

handler 最终抛出的异常
↓
是否符合接口方法 throws 契约

这也是成熟代理框架比简单 Demo 复杂很多的原因之一。


4.5 原课程中的泛型 ProxyUtil

原课程常见写法:

public static <T> T createProxy(T obj) {

    T proxy =
            (T) Proxy.newProxyInstance(
                    ProxyUtil.class
                            .getClassLoader(),

                    obj.getClass()
                            .getInterfaces(),

                    handler
            );

    return proxy;
}

这种写法在教学阶段能够很好地表达:

一个工具可以接收不同接口的业务对象。

但是必须理解:

泛型 T 并不会让 JDK 代理对象突然变成目标实现类。

例如最好:

UserService target =
        new UserServiceImpl();

UserService proxy =
        ProxyUtil.createProxy(target);

T 表达接口类型。

而不要形成错误认识:

UserServiceImpl proxy =
        ProxyUtil.createProxy(
                new UserServiceImpl()
        );

代理对象仍然不是:

UserServiceImpl

本章前面提供:

createProxy(
    Class<T> interfaceType,
    T target
)

是为了让接口类型约束更加明确。


五、实践应用

5.1 统一调用日志

可以输出:

[CALL] login args=[admin, 123456]
[RETURN] login result=SUCCESS
[COST] login 325100 ns

于是每个业务类不需要自己重复记录。


5.2 统一性能监控

所有 Service 方法
↓
代理
↓
统一记录开始时间
↓
真实调用
↓
统一记录结束时间

可以快速发现:

哪些业务方法耗时异常

5.3 权限检查

例如:

if ("deleteUsers".equals(
        method.getName()
)) {

    checkAdminPermission();
}

然后:

method.invoke(
        target,
        args
);

这只是一个简化思想。

真正系统通常不会大量依赖字符串方法名硬编码,而会结合:

注解
+
反射
+
代理

形成更通用的规则。

这正好把前几章串起来:

注解
↓
声明规则

反射
↓
读取结构和元数据

动态代理
↓
拦截方法调用

InvocationHandler
↓
执行通用增强

5.4 注解 + 反射 + 动态代理

例如概念上:

@Log
public void login() {
}

代理收到调用:

method
↓
检查 method 上有没有 @Log
↓
有
↓
输出日志
↓
执行真实方法

这就是 Java 框架能力开始真正组合起来的地方。


六、常见问题

6.1 args 一定不是 null 吗?

不是。

无参方法:

void work();

调用时:

args

可以是:

null

所以应安全处理。


6.2 void 方法的反射调用返回什么?

例如:

void deleteUser();

通过:

method.invoke(...)

调用后返回:

null

这是正常的。


6.3 为什么代理方法有返回值,却得到 null?

检查:

Object result =
        method.invoke(
                target,
                args
        );

之后有没有:

return result;

如果写成:

method.invoke(
        target,
        args
);

return null;

代理方法当然无法正常返回真实业务结果。

如果接口返回基本类型,错误返回 null 甚至可能进一步导致运行时异常。


6.4 为什么出现 StackOverflowError?

首先检查是否写成:

method.invoke(
        proxy,
        args
);

这可能重新调用代理自身,再次进入:

invoke()

形成递归。

通常应该:

method.invoke(
        target,
        args
);

6.5 为什么业务明明抛 IllegalArgumentException,我看到的却是 InvocationTargetException?

因为:

Method.invoke()

是反射调用。

目标方法抛出的异常会被:

InvocationTargetException

包装。

可以:

catch (InvocationTargetException e) {

    Throwable cause =
            e.getCause();
}

获取真实原因。


6.6 为什么出现 UndeclaredThrowableException?

一种典型情况是:

InvocationHandler.invoke()
↓
抛出某个受检异常
↓
接口方法的 throws 并没有声明这种异常

代理无法直接按照该接口方法的异常契约把它交给调用方,因此可能包装成:

UndeclaredThrowableException

所以通用代理中的异常传播不能随便处理。


6.7 泛型 ProxyUtil 为什么仍然可能类型转换失败?

因为:

<T>

只是编译期泛型抽象。

JDK 动态代理真正产生的对象:

实现接口,但并不是你的具体业务实现类。

因此变量类型应该围绕接口设计。


七、练习与验收

7.1 知识问答

  1. 什么是 InvocationHandler
  2. invoke() 是谁调用的?
  3. proxy 参数代表什么?
  4. method 参数代表什么?
  5. args 参数代表什么?
  6. 无参方法的 args 可能是什么?
  7. invoke() 的返回值最终去了哪里?
  8. 为什么通常使用 method.invoke(target, args)
  9. 为什么不能随意使用 method.invoke(proxy, args)
  10. InvocationTargetException 是什么?
  11. UndeclaredThrowableException 可能在什么情况下出现?
  12. 为什么一个 InvocationHandler 可以增强多个不同方法?

7.2 代码阅读

阅读:

InvocationHandler handler =
        (proxy, method, args) -> {

            long start =
                    System.nanoTime();

            try {

                Object result =
                        method.invoke(
                                target,
                                args
                        );

                return result;

            } finally {

                System.out.println(
                        System.nanoTime()
                                - start
                );
            }
        };

回答:

  1. proxy 是谁?
  2. method 是谁?
  3. args 是谁?
  4. 核心业务由哪个对象执行?
  5. 为什么耗时统计放在 finally 中?
  6. 如果目标方法有返回值,如何返回给客户端?

7.3 手写代码

编写通用动态代理工具:

ProxyUtil

要求:

  • 接收一个实现接口的目标对象。
  • 创建 JDK 动态代理。
  • 输出方法名。
  • 输出参数。
  • 输出返回值。
  • 记录执行耗时。
  • 发生异常时记录异常。
  • 将真实异常继续传播。

7.4 Debug

下面代码存在什么问题?

return Proxy.newProxyInstance(
        loader,
        interfaces,
        (proxy, method, args) -> {

            System.out.println("before");

            return method.invoke(
                    proxy,
                    args
            );
        }
);

请:

  1. 分析调用链。
  2. 判断为什么可能无限递归。
  3. 修改为调用真实目标对象。

再分析:

(proxy, method, args) -> {

    method.invoke(
            target,
            args
    );

    return null;
}

如果被代理的方法是:

String login();

会有什么问题?

7.5 综合训练

实现:

UserService

方法包括:

String login(
    String username,
    String password
);

void deleteUsers(
    int... ids
);

要求使用一个通用动态代理完成:

调用前:
方法名
参数

调用:
真实业务

调用后:
返回值
耗时

异常:
异常类型
异常信息

并保证:

  • 一个代理处理器能够处理两个不同方法。
  • 正确处理无返回值方法。
  • 正确处理数组参数。
  • 正确传播真实业务异常。

7.6 本章验收

能够完整画出:

客户端
↓
proxy.login(...)
↓
JDK 动态代理类
↓
InvocationHandler.invoke(
    proxy,
    method,
    args
)
↓
前置增强
↓
method.invoke(
    target,
    args
)
↓
真实业务对象
↓
返回值 / 异常
↓
后置增强
↓
返回客户端

并能够口述:

InvocationHandler 是 JDK 动态代理的方法调用处理器。代理对象的接口方法被调用后,JDK 会把这次调用分派给 invoke(proxy, method, args)。其中 proxy 表示代理对象,method 表示当前被调用的方法,args 表示实参。处理器可以在调用 method.invoke(target, args) 前后统一加入日志、权限、耗时统计等增强,从而实现通用动态代理。