臭名昭著aTrust隔离计划1.臭名昭著aTrust隔离计划2.VPN原理与aTrust隔离网络实践3.docker-easyconnect到底做了什么4.TUN(tunnel-隧道-虚拟网卡)模式
个人项目
personal(个人展示)GitHub Actions 自动部署个人展示项目
基于Quartz搭建的个人博客1.Quartz个人博客使用教程2.使用 rsync 增量部署 Quartz 博客3.使用 GitHub Actions 自动部署 Quartz 博客5.域名绑定6.CDN加速7.Google 搜索收录排查:为什么关键词搜得到别人,却搜不到我的网站?Obsidian、双向链接与知识图谱
架构设计
分布式分布式事务RPC原理
旅行
行程攻略青岛三日游行程攻略
前端
NginxNginx配置与反向代理入门
Nodenpm与npx的区别
数据库
mysql01.MySQL基础语法与约束02.MySQL数据库类型03.MySQL多表查询04.MySQL 索引05.MySQL 事务06.MySQL 隔离级别07.MySQL MVCC08.MySQL 锁09.EXPLAIN10.慢查询与 SQL 优化
mysql面试题01.索引问题02.事务与隔离级别03.锁机制04.存储引擎与性能优化05.用户权限、字符集与备份恢复
mysql实战01.EXPLAIN实战02.测试环境菜单误删排查与恢复03.MySQL IN 详解
网络
网络安全1.无线监听网卡苹果M系列虚拟机安装Kali Linux教程3.kali怎么使用RTL8812AU4.kali基础命令
网络基础进程之间怎样通过网络传递数据
网址常用网址
中间键
esElasticsearch 入门
mq
RabbitMQRabbitMQ高级RabbitMQ基础
1. MQ 基础2. MQ 核心角色与代码对应3. MQ Consumer Group 与消费模式4. SOFAMQ 账户变更代码实践5. MQ 可靠性问题6. 消息幂等
redis缓存击穿、穿透与雪崩Redis 分布式锁Redis 缓存问题Redis 数据结构
AI
使用说明
第三方插件&技能简介Archify使用与安装指南geekgeekRun使用OfferFlow使用指南Ponytail使用指南Skills Manager使用指南
CodexCodex CLI与IDE区别及使用指南Codex Hook单独配置与提交通知Codex MCP安装与使用指南Codex第三方插件安装与使用指南
Agent开发0-学习路线1.LLM、Token、上下文窗口与模型参数2.大模型 API、请求参数与响应结构3.Spring AI ChatClient4.Prompt、System Prompt、Prompt 模板6.结构化输出、JSON Schema7.SSE 流式响应8.会话 ID、聊天记录、Redis9.超时、重试、限流、降级10.*完成可运行聊天接口11.Function Calling、Tool Calling 原理12.用 Java 方法定义 Tool13.工具参数设计和校验14.实现订单查询工具15.实现物流查询工具16.实现库存查询工具17.多个工具自动选择18.工具异常、超时、重试
diy主机
主机零件学习1.CPU2.主板3.内存4.显卡5.硬盘6.散热7.风扇8.机箱热门型号价格表
捡垃圾diy-13600kf+5060diy-14600kf+5070
English高频词汇英文基础语法简单句子音标
GitGit常用命令Sublime Merge 使用指南
Java
工具库Caffeine本地缓存Reactor教程
工作实践如何使用@Async线程池使用与排查线上服务 CPU 使用率过高排查压测下RPC接口大量失败排查记录
基础1.HashMap2.ConcurrentHashMap3.HashSet4.final5.static6.volatile7.AtomicInteger8.String动态代理反射锁与线程同步线程注解
面试题1.HashMap 存储在方法内是线程安全的吗2.ArrayList参数传递与引用重新赋值3.== 和 equals 的区别、hashcode4.重载和重写的区别5.String、StringBuffer、StringBuilder区别及使用场景6.接口和抽象类7.ArrayList 与 LinkedList 的区别和扩容机制8.HashMap 和 Hashtable 的区别及底层实现9.Object 和 Objects 的区别10.ThreadLocal的原理和使用场景11.字节码是什么及作用12.Java类加载器有哪些13.双亲委派模型14.Java异常体系15.GC是什么16.GC如何判断对象可以被回收
面试总结0.HR面试模板1.面试一2.面试二3.面试三笔试
网络与 IO(黑马程序员-纯理论)01-网络编程基础02-IO模型与零拷贝03-BIO04-NIO基础与缓冲区05-NIO通道与选择器06-AIO
juc00-目录01-进程与线程02-线程同步与通信03-并发设计模式04-Java内存模型05-CAS与原子类06-ThreadLocal07-阻塞队列08-线程池使用与原理09-任务调度10-ForkJoin与享元模式11-AQS12-ReentrantLock13-读写锁14-并发协作工具15-ConcurrentHashMap16-CopyOnWrite与同步容器17-SkipListMap18-非阻塞队列
juc(工作常见)Java并发编程三个重点拆解
jvm1. JVM运行时数据区
Spring1.Spring IoC、DI与构造方法注入CGLIB、Byte Buddy 与 Spring AOPSpring 常用注解详解Spring Bean 生命周期
Spring MVC异常处理机制
linux常用命令
mac快捷键终端常用操作
python
基础01.Python是什么,为什么要安装Python02.Python项目中.venv是什么03.Python基础语法
基础实战sql替换
HTTPXHRMS员工详情接口调用(Python HTTPX)
windowswindows激活

一、💡 一句话理解

核心结论

Java 动态代理会在运行时生成一个代理对象。调用代理对象的方法时,调用会先进入 InvocationHandler.invoke(...),我们就可以在真实方法前后统一增加日志、权限校验、事务等公共逻辑。

可以把它理解成“给真实对象加一个会自动拦截请求的中间人”:调用方只认识接口,不需要知道中间人内部如何记录日志或执行检查。

二、🧭 理论:它是什么

2.1 什么是代理

代理(Proxy)不是业务对象本身,而是一个代替业务对象接收调用的对象。

普通调用是:

调用方 ──────────────> 真实对象

加入代理后变成:

调用方 ──> 代理对象 ──> 公共逻辑 ──> 真实对象

例如,调用 userService.findUser("小明") 前后,代理可以自动完成:

  • 记录方法名和参数;
  • 检查当前用户是否有权限;
  • 开启、提交或回滚事务;
  • 统计方法执行耗时;
  • 统一处理异常。

代理的关键点是:调用方仍然按照原来的接口调用,不需要修改业务代码。

2.2 什么是动态代理

静态代理需要我们提前编写代理类的源代码;动态代理则由程序在运行时根据接口生成代理类和代理对象,不需要提前写出具体的代理类文件。

大白话理解

可以把代理想成“代购”。静态代理就是你提前雇好一个代购,还要亲自写好这名代购的工作流程;动态代理则是你只告诉系统“商品接口是什么、统一处理规则是什么”,系统在程序运行时临时生成一个代购。

所以,静态代理是“代理类提前手写好”,动态代理是“只写处理规则,代理类运行时自动生成”。动态代理并不是完全不写代码,而是不需要提前写出具体的 UserServiceProxy 类。

例如,静态代理需要自己编写类似下面的代理类:

// 这是理解静态代理的简化示意,不是单独可运行的完整程序。
class UserServiceProxy implements UserService {
 
    private final UserService target;
 
    UserServiceProxy(UserService target) {
        this.target = target;
    }
 
    @Override
    public String findUser(String name) {
        // 代理类中手动添加公共逻辑。
        System.out.println("记录日志");
 
        // 再手动调用真实对象。
        return target.findUser(name);
    }
}

动态代理则不需要编写 UserServiceProxy 这个类,只需要写统一的处理规则:

// 这是理解动态代理的简化示意;完整可运行代码见 4.3 节。
public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable {
    System.out.println("记录日志");
    return method.invoke(target, args);
}

调用方最终都可以这样调用:

proxy.findUser("小明");

调用方式没有变化,区别只在于:静态代理的代理类是我们提前写好的,动态代理的代理类是 Java 在运行时帮我们生成的。

JDK 动态代理的正式定义是:在运行时根据一组接口生成代理类,代理对象上的接口方法调用会被编码后分发给同一个调用处理器。Oracle 官方资料将这个处理器称为 InvocationHandler。Oracle:Dynamic Proxy Classes

2.3 JDK 动态代理的三个核心角色

角色白话理解主要职责
业务接口调用方和业务对象之间的约定声明可以调用哪些方法
真实对象真正完成业务的对象实现接口并执行核心业务
InvocationHandler代理对象背后的总开关接收方法调用,决定前置逻辑、真实调用和后置逻辑
ProxyJDK 提供的代理工厂根据接口和处理器创建代理对象

最容易混淆的一点是:Proxy 负责“生成代理对象”,InvocationHandler 负责“处理代理对象收到的每一次方法调用”。

2.4 JDK 动态代理的限制

JDK 动态代理要求代理目标通过接口暴露方法。也就是说,目标对象通常需要实现一个或多个接口,代理对象也会实现这些接口。

UserService target = new UserServiceImpl();
UserService proxy = ...;

调用方只需要持有 UserService 类型的引用,不需要依赖 UserServiceImpl 的具体类型。

如果目标类没有接口,JDK 动态代理不能直接代理这个类;这时通常会考虑基于子类的代理工具,例如 CGLIB 或 Byte Buddy。它们属于其他库,本篇先不展开。

这句话可以这样理解:JDK 动态代理只能代理接口,不能直接代理普通类。

下面这个类没有实现任何接口,所以不能直接使用 JDK 动态代理:

// 这是一个没有接口的普通类。
class UserService {
 
    public void findUser() {
        System.out.println("查询用户");
    }
}

JDK 动态代理的工作方式是“生成一个实现指定接口的代理对象”。因此,目标对象通常需要先实现接口:

// 接口:规定调用方可以使用哪些方法。
interface UserService {
    void findUser();
}
 
// 真实对象:实现接口并完成真正的业务。
class UserServiceImpl implements UserService {
 
    @Override
    public void findUser() {
        System.out.println("查询用户");
    }
}

此时就可以让 JDK 动态代理实现 UserService 接口:

UserService 接口
        ↓
JDK 动态生成一个实现 UserService 的代理对象
        ↓
调用代理对象的方法
        ↓
进入 InvocationHandler

如果目标类没有接口,CGLIB、Byte Buddy 等工具会采用另一种方式:在运行时生成目标类的子类,并重写可以重写的方法。

注意:下面是另一个独立示例。这里的 PlainUserService 是没有实现接口的普通类,不要和前面示例中的 UserService 接口混淆。

// 这是帮助理解的简化示意,不是 CGLIB 或 Byte Buddy 的实际用法。
// PlainUserService 是没有接口的普通类。
class PlainUserService {
 
    public void findUser() {
        System.out.println("查询用户");
    }
}
 
// 代理类通过继承普通类,再重写其中可以重写的方法。
class PlainUserServiceProxy extends PlainUserService {
 
    @Override
    public void findUser() {
        System.out.println("记录日志");
        super.findUser();
    }
}

因此可以这样记:

代理方式代理依据能否代理没有接口的普通类
JDK 动态代理生成接口的实现类不能直接代理
CGLIB、Byte Buddy生成目标类的子类通常可以代理

这里的 ## 只是 Markdown 的标题符号,不是 Java 代码;本篇的下一节标题“## 三、理论:它是怎么工作的”表示新的章节开始。

三、⚙️ 理论:它是怎么工作的

3.1 创建代理对象的三个参数

JDK 动态代理最常用的入口是:

在看下面的代码之前,先说明 UserService 是什么:它是本篇为了演示动态代理而自己定义的业务接口,不是 Java 或 JDK 自带的类。它代表“用户服务”,里面声明了调用方可以使用的方法。

// 业务接口:只规定可以做什么,不负责具体实现。
interface UserService {
 
    // 声明一个查询用户的方法。
    String findUser(String username);
}

后面会再写一个 UserServiceImpl 来实现这个接口,真正完成查询业务;动态代理则会生成另一个实现 UserService 的代理对象。

Proxy.newProxyInstance(classLoader, interfaces, invocationHandler)

三个参数分别是:

参数含义
classLoader用来加载接口和生成代理类的类加载器,通常使用接口或目标对象的类加载器
interfaces代理对象需要实现的接口数组
invocationHandler代理对象收到方法调用后要执行的处理器

代理对象创建后,可以转换成接口类型:

UserService proxy = (UserService) Proxy.newProxyInstance(
        UserService.class.getClassLoader(),
        new Class<?>[]{UserService.class},
        handler
);

这里的 handler 是后面定义的调用处理器对象,暂时可以先理解成“代理收到方法调用后要执行的规则”。

3.1.1 类加载器是什么

ClassLoader(类加载器)属于 JVM 基础知识,具体属于“JVM 的类加载机制”。它的作用是把编译后的 .class 文件加载到 JVM 中,让 Java 程序可以使用这个类。

可以把它理解成“把类送进 JVM 的加载员”:

UserService.class
        ↓
ClassLoader 加载
        ↓
JVM 可以使用 UserService

动态代理同样需要一个代理类。代理类是在运行时生成的,生成之后也要被加载到 JVM 中,所以 Proxy.newProxyInstance(...) 需要一个类加载器。

UserService.class.getClassLoader()

这句话的意思是:

取出加载 UserService 接口的那个类加载器,用它来加载动态生成的代理类。

因此,创建代理对象的三个参数可以先这样记:

类加载器:把代理类加载进 JVM
接口:规定代理对象能调用什么方法
handler:规定调用方法时要做什么

类加载器不是动态代理最核心的难点。初学动态代理时,可以先把下面这句当作固定写法:

UserService.class.getClassLoader()

以后学习 JVM 时,再专门理解类加载过程、类加载器类型和双亲委派机制。

3.1.2 new Class<?>[]{UserService.class} 是什么

这段代码看起来复杂,我们按“由简入难”的顺序拆开。数组可以先理解成一排有编号的盒子,编号从 0 开始。

第一步,先看最简单的数组:

// 创建一个能存放 3 个整数的数组。
int[] numbers = new int[3];
 
// 把数字放进数组的第 0、1、2 个位置。
numbers[0] = 10;
numbers[1] = 20;
numbers[2] = 30;

第二步,创建数组时直接放入元素:

// 创建整数数组,并直接放入 90、80、70。
int[] scores = new int[]{90, 80, 70};

上面的写法还可以简写成:

int[] ages = {18, 20, 22};

第三步,先理解数组的通用写法:

new 类型[]{元素1, 元素2}

有时为了讲解,会把它进一步简写成:

new ... {}

这里的 ... 只是“待替换内容”,不是可以直接运行的 Java 代码。把它一步步还原,就是:

// 第一步:先写出创建数组的结构,... 仍然只是占位符。
new ... {}
 
// 第二步:把 ... 替换成具体的数组类型 Class<?>[]。
new Class<?>[]{}
 
// 第三步:在大括号中放入数组元素 UserService.class。
new Class<?>[]{UserService.class}

第四步,理解上面代码中的各个部分:

写法大白话
Class<?>表示一个类或接口的信息
[]表示这是一个数组
UserService.class获取 UserService 接口的信息,不是创建 UserService 对象
new Class<?>[]{...}创建 Class 信息数组,并在创建时直接放入元素

这里的 UserService.class 和下面的 new UserServiceImpl() 不是一回事:

UserService.class       // 获取接口的描述信息
new UserServiceImpl()   // 创建真实业务对象

为了看清数组的创建过程,new Class<?>[]{UserService.class} 还可以拆成两步:

// 先创建一个长度为 1 的 Class 信息数组。
Class<?>[] interfaces = new Class<?>[1];
 
// 再把 UserService 接口的信息放到数组的第 0 个位置。
interfaces[0] = UserService.class;

Proxy.newProxyInstance(...) 的第二个参数要求传入“代理对象需要实现哪些接口”。所以:

new Class<?>[]{UserService.class}

就是在告诉 Java:

请生成一个实现 UserService 接口的代理对象。

虽然当前示例只有一个接口,但这个参数可以放多个接口:

Class<?>[] interfaces = new Class<?>[]{
        UserService.class,
        OrderService.class
};

简单记忆:

int[]                   :数组中存放整数
Class<?>[]              :数组中存放 Class 信息
UserService.class       :拿到 UserService 接口信息
new Class<?>[]{...}     :创建 Class 数组并放入接口信息
Proxy                   :根据这些接口生成代理对象

3.2 一次方法调用发生了什么

调用:

String result = proxy.findUser("小明");

大致会经历以下过程:

1. 调用方调用 proxy.findUser("小明")
2. 代理对象把方法信息封装成 Method
3. 代理对象把 proxy、Method、参数数组传给 handler.invoke(...)
4. handler 执行前置逻辑
5. handler 使用 method.invoke(target, args) 调用真实对象
6. handler 执行后置逻辑并返回结果
7. 结果回到调用方

InvocationHandler.invoke 的三个参数如下:

Object invoke(Object proxy, Method method, Object[] args) throws Throwable
  • proxy:当前收到调用的代理对象;
  • method:本次被调用的方法描述对象;
  • args:调用参数数组。无参数方法时可能为 null;
  • 返回值:必须与接口方法声明的返回类型兼容。

Oracle 的 InvocationHandler 文档明确说明:每个代理对象都关联一个调用处理器,代理方法调用会分发到处理器的 invoke 方法。Oracle:InvocationHandler

3.3 代理和反射的关系

动态代理本身依靠反射 API 完成方法分发:

  • Proxy.newProxyInstance(...) 负责创建代理对象;
  • Method 描述当前调用的方法;
  • Method.invoke(...) 负责在真实对象上执行方法。

因此,动态代理可以看作是“反射加上一层统一的调用拦截”。如果想先理解 Class、Method 和 Method.invoke,可以先阅读 反射。

3.4 为什么要解包 InvocationTargetException

“解包”可以理解成“拆开包装盒”。InvocationTargetException 是反射帮真正异常套上的外层包装,真正的业务异常在它里面。

InvocationTargetException   ← 外面的异常包装盒
        ↓
真正的业务异常              ← 盒子里面的异常

当处理器通过反射调用真实方法时:

method.invoke(target, args);

如果真实方法内部抛出异常,反射通常会用 InvocationTargetException 包装它。

例如,真实方法内部发生了业务错误:

// 真实业务方法抛出的异常。
public void pay() {
    throw new RuntimeException("余额不足");
}

代理处理器捕获到的可能不是直接的“余额不足”,而是外层的 InvocationTargetException。因此需要调用 getTargetException() 取出里面真正的异常,再继续抛出:

try {
    return method.invoke(target, args);
} catch (InvocationTargetException exception) {
    // 取出真实方法抛出的异常,而不是继续抛出外层包装异常。
    throw exception.getTargetException();
}

如果不解包,调用方看到的只是:

InvocationTargetException

解包之后,调用方才能看到更有用的真实错误:

RuntimeException: 余额不足

所以,代理处理器一般应该解包异常,让调用方更容易定位真正的业务问题。

四、🚀 实践:从准备到验证

4.1 前置准备

4.1.1 环境

  • JDK 8 或更高版本;
  • 一个空目录;
  • 新建文件 DynamicProxyDemo.java。

本例只使用 JDK 自带的 java.lang.reflect.Proxy、InvocationHandler 和 Method,不需要添加第三方依赖。

4.2 可以拿来干什么

4.2.1 统一记录日志和耗时

前置条件:真实对象实现业务接口,代理处理器持有真实对象。

long start = System.currentTimeMillis();
System.out.println("开始调用:" + method.getName());
 
Object result = method.invoke(target, args);
 
long elapsed = System.currentTimeMillis() - start;
System.out.println("调用结束,耗时:" + elapsed + " ms");
return result;

输入是接口方法调用和参数,处理器负责记录方法信息,再把调用转发给真实对象,最后返回真实结果。它适合给多个业务方法增加统一日志,而不把日志代码复制到每个业务类中。

4.2.2 调用前做权限校验

前置条件:处理器可以获得当前用户或权限服务,并能根据 method 判断当前方法需要什么权限。

if (method.getName().equals("deleteUser") && !currentUserIsAdmin()) {
    throw new SecurityException("没有删除用户的权限");
}
 
return method.invoke(target, args);

输入是方法名、参数和当前登录状态;处理器先判断是否允许调用,只有通过检查后才执行真实方法。更复杂的项目也可以结合 注解,给方法加上 @RequirePermission,再由处理器通过反射读取注解。

4.2.3 统计执行结果

前置条件:处理器需要在调用前后分别记录时间,并在异常分支记录失败原因。

try {
    Object result = method.invoke(target, args);
    successCounter.increment();
    return result;
} catch (InvocationTargetException exception) {
    failureCounter.increment();
    throw exception.getTargetException();
}

这个用途适合统计成功次数、失败次数和耗时,但不应该把业务逻辑本身塞进通用处理器,否则代理会逐渐变成难以维护的“万能类”。

4.3 完整实践:创建一个日志代理

下面的程序模拟一个用户服务:UserServiceImpl 负责真正查询和删除用户;LoggingHandler 负责在调用前打印参数、调用后打印耗时;调用方只依赖 UserService 接口。

4.3.1 完整代码

文件位置:DynamicProxyDemo.java。

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
 
public class DynamicProxyDemo {
 
    /**
     * 业务接口:调用方只依赖这个接口,不直接依赖实现类。
     */
    interface UserService {
 
        /**
         * 根据用户名查询用户。
         */
        String findUser(String username);
 
        /**
         * 删除指定编号的用户。
         */
        void deleteUser(long userId);
    }
 
    /**
     * 真实业务对象:真正执行查询和删除操作。
     */
    static class UserServiceImpl implements UserService {
 
        @Override
        public String findUser(String username) {
            // 这里用字符串模拟数据库查询结果。
            return "用户:" + username;
        }
 
        @Override
        public void deleteUser(long userId) {
            // 这里用打印语句模拟删除数据库记录。
            System.out.println("已删除用户,编号:" + userId);
        }
    }
 
    /**
     * 调用处理器:代理对象收到任何接口方法调用后,都会进入 invoke 方法。
     */
    static class LoggingHandler implements InvocationHandler {
 
        // handler 必须保存真实对象,后续才能把调用转发给真实对象。
        private final Object target;
 
        LoggingHandler(Object target) {
            this.target = target;
        }
 
        @Override
        public Object invoke(Object proxy, Method method, Object[] args)
                throws Throwable {
            long startTime = System.currentTimeMillis();
 
            // args 可能为 null,所以打印时使用空数组避免阅读和处理上的歧义。
            Object[] actualArgs = args == null ? new Object[0] : args;
            System.out.println("[前置日志] 方法:" + method.getName()
                    + ",参数:" + Arrays.toString(actualArgs));
 
            try {
                // 使用反射在真实对象上执行同一个方法。
                Object result = method.invoke(target, args);
 
                System.out.println("[返回结果] " + result);
                return result;
            } catch (InvocationTargetException exception) {
                // 真实方法抛出的异常会被反射包装,这里还原真实异常再向外抛出。
                throw exception.getTargetException();
            } finally {
                long elapsed = System.currentTimeMillis() - startTime;
                System.out.println("[后置日志] 方法:" + method.getName()
                        + ",耗时:" + elapsed + " ms");
            }
        }
    }
 
    public static void main(String[] args) {
        // 创建真实对象。它负责业务,不负责统一日志。
        UserService target = new UserServiceImpl();
 
        // 通过 JDK 动态代理创建代理对象。
        UserService proxy = (UserService) Proxy.newProxyInstance(
                UserService.class.getClassLoader(),
                new Class<?>[]{UserService.class},
                new LoggingHandler(target)
        );
 
        // 调用方使用接口调用,表面上和调用真实对象没有区别。
        System.out.println(proxy.findUser("小明"));
        proxy.deleteUser(1001L);
 
        // 验证:JDK 确实为该对象生成了动态代理类。
        System.out.println("是否为代理类:"
                + Proxy.isProxyClass(proxy.getClass()));
        System.out.println("代理类名:" + proxy.getClass().getName());
    }
}

代码中的调用链是:

proxy.findUser("小明")
        ↓
LoggingHandler.invoke(...)
        ↓
method.invoke(target, args)
        ↓
UserServiceImpl.findUser("小明")

4.3.2 编译和启动

在 DynamicProxyDemo.java 所在目录执行:

javac DynamicProxyDemo.java
java DynamicProxyDemo

4.3.3 预期结果

具体耗时和生成的代理类名称可能不同,但输出结构类似:

[前置日志] 方法:findUser,参数:[小明]
[返回结果] 用户:小明
[后置日志] 方法:findUser,耗时:0 ms
用户:小明
[前置日志] 方法:deleteUser,参数:[1001]
已删除用户,编号:1001
[返回结果] null
[后置日志] 方法:deleteUser,耗时:0 ms
是否为代理类:true
代理类名:com.sun.proxy.$Proxy0

在较新的 JDK 中,代理类名的具体格式可能不同;只要最后的 是否为代理类:true 出现,就说明代理对象创建成功。

4.3.4 关键代码复盘

代码作用
new LoggingHandler(target)让处理器保存真实对象
Proxy.newProxyInstance(...)根据接口和处理器创建代理对象
UserService proxy让调用方通过接口使用代理
invoke(...)接收代理对象的每一次方法调用
method.invoke(target, args)把调用转发给真实对象
finally无论成功或失败都执行耗时统计

4.4 常见报错排查

4.4.1 IllegalArgumentException: ... is not an interface

原因:传给 Proxy.newProxyInstance 的类型不是接口,或者把实现类写进了接口数组。

检查:

new Class<?>[]{UserService.class}

这里应该放 UserService 接口,而不是 UserServiceImpl.class。

4.4.2 ClassCastException

原因:代理对象没有实现你转换目标的接口,或者转换成了错误的类型。

检查接口数组和强制转换是否一致:

new Class<?>[]{UserService.class}
UserService proxy = (UserService) ...;

4.4.3 NullPointerException 或返回类型错误

原因:invoke 返回的对象必须兼容接口方法的返回类型。接口方法返回基本类型时,也不能返回 null。

例如:

int count();

对应的 invoke 必须返回 Integer,不能返回 null 或字符串。

4.4.4 业务异常变成 InvocationTargetException

原因:处理器直接把反射异常向外抛出,没有取出真实异常。

处理方式:

catch (InvocationTargetException exception) {
    throw exception.getTargetException();
}

五、📌 总结

  • JDK 动态代理在运行时生成代理对象,不需要手写具体代理类。
  • JDK 动态代理主要面向接口,代理对象实现创建时传入的接口。
  • Proxy 负责创建代理对象,InvocationHandler 负责接收方法调用。
  • Method 描述当前调用的方法,method.invoke(target, args) 将调用转发给真实对象。
  • 日志、权限、事务、耗时统计等横切逻辑适合放进代理处理器。
  • 真实方法抛出的异常可能被 InvocationTargetException 包装,通常需要解包后再抛出。

记忆句:接口定义调用方式,Proxy 生成代理对象,Handler 拦截调用,Method 反射执行真实方法。

六、📚 官方资料