代理模式JDK动态代理案例

wen java案例 2

JDK动态代理从源码到高并发场景的全解析

目录导读

  1. 代理模式的本质与分类 – 为什么静态代理会被淘汰?
  2. JDK动态代理核心机制 – 你必须搞懂的InvocationHandler与Proxy
  3. 手写一个可运行的JDK动态代理案例 – 从0到1的完整代码
  4. 源码级揭秘:JDK动态代理到底在字节码层面做了什么?
  5. 代理模式 vs 装饰器模式 – 面试官最爱挖的坑
  6. 高频问答 – 关于JDK动态代理的10个灵魂拷问

代理模式:不是简单的“中间层”

代理模式(Proxy Pattern)的核心思想是:为其他对象提供一种代理以控制对这个对象的访问,在GoF设计模式中,它被归类为结构型模式。

代理模式JDK动态代理案例

1 为什么需要代理?

  • 访问控制:比如权限校验(只有VIP才能调用的方法)
  • 增强功能:日志记录、性能监控、事务管理
  • 延迟加载:真正需要时才创建目标对象(虚拟代理)

2 静态代理的痛点

静态代理在编译期就确定了代理类,每个目标类都需要手工编写一个代理类,假设有100个Service,就要写100个代理类——代码爆炸,而JDK动态代理在运行时动态生成代理类,一个InvocationHandler就能服务所有接口。


核心机制:一接口、一处理器、一代理对象

JDK动态代理依赖于三个核心组件:

组件 作用 必须条件
InvocationHandler 处理器接口,定义invoke方法 实现该接口
Proxy 静态工厂方法newProxyInstance 传入ClassLoader+接口数组+处理器
目标接口 代理类必须实现的接口 没有接口则无法使用JDK动态代理

关键约束:JDK动态代理只能代理接口,不能代理类(这是面试高频考点)。


手写实战:一个带性能监测的代理案例

我们模拟一个电商下单场景,对OrderService添加耗时统计和异常日志。

1 定义目标接口

public interface OrderService {
    void createOrder(String orderId, double amount);
    String queryOrder(String orderId);
}

2 真实目标类(被代理对象)

public class OrderServiceImpl implements OrderService {
    @Override
    public void createOrder(String orderId, double amount) {
        // 模拟耗时操作
        try { Thread.sleep(200); } catch (InterruptedException e) {}
        System.out.println("订单 " + orderId + " 创建成功,金额:" + amount);
    }
    @Override
    public String queryOrder(String orderId) {
        try { Thread.sleep(100); } catch (InterruptedException e) {}
        return "订单详情:" + orderId;
    }
}

3 实现InvocationHandler

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class LogPerformanceHandler implements InvocationHandler {
    private final Object target;
    public LogPerformanceHandler(Object target) {
        this.target = target;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        long start = System.nanoTime();
        Object result = null;
        try {
            System.out.println("【前置通知】方法开始:" + method.getName());
            result = method.invoke(target, args);
            System.out.println("【后置通知】方法结束:" + method.getName());
        } catch (Exception e) {
            System.out.println("【异常通知】方法异常:" + e.getMessage());
            throw e;
        } finally {
            long cost = (System.nanoTime() - start) / 1_000_000;
            System.out.println("【性能监控】方法耗时:" + cost + " ms");
        }
        return result;
    }
    // 返回代理对象
    @SuppressWarnings("unchecked")
    public <T> T getProxy() {
        return (T) Proxy.newProxyInstance(
            target.getClass().getClassLoader(),
            target.getClass().getInterfaces(),
            this
        );
    }
}

4 客户端调用

public class Client {
    public static void main(String[] args) {
        // 创建真实对象
        OrderService realService = new OrderServiceImpl();
        // 创建代理对象
        LogPerformanceHandler handler = new LogPerformanceHandler(realService);
        OrderService proxyService = handler.getProxy();
        // 通过代理调用
        proxyService.createOrder("D1001", 299.00);
        String detail = proxyService.queryOrder("D1001");
        System.out.println(detail);
        // 特别说明:打印代理的类名,看清本质
        System.out.println("代理类的真实类型:" + proxyService.getClass());
    }
}

运行输出示例:

【前置通知】方法开始:createOrder
订单 D1001 创建成功,金额:299.0
【后置通知】方法结束:createOrder
【性能监控】方法耗时:201 ms
【前置通知】方法开始:queryOrder
【后置通知】方法结束:queryOrder
【性能监控】方法耗时:101 ms
订单详情:D1001
代理类的真实类型:class com.sun.proxy.$Proxy0

源码级解密:$Proxy0是怎么来的?

当调用Proxy.newProxyInstance时,JVM内部做了三件大事:

  1. 生成代理类字节码:根据传入的接口动态生成一个class $Proxy0,它继承了Proxy并实现了所有接口(注意:Java是单继承,所以只能靠接口扩展)。
  2. 编译并加载类:使用sun.misc.ProxyGenerator生成字节码,通过ClassLoader加载到JVM。
  3. 创建代理实例:调用无参构造器(底层是(InvocationHandler)构造器),把handler绑到父类Proxy上。

invoke方法里,method.invoke(target, args)这一步是反射调用,这也是为什么JDK动态代理比CGLIB慢一点的原因(CGLIB直接字节码增强,不反射)。

小技巧:在main方法中加入System.getProperties().put("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true"); 可以反编译生成的$Proxy0.class。


代理模式 vs 装饰器模式:别再傻傻分不清

对比维度 代理模式 装饰器模式
目的 控制访问(权限/懒加载/日志) 增强功能(加糖/加奶)
创建主动权 客户端持有代理,代理决定何时调真实 客户端创建被包装对象,按需包装
结构 代理和真实实现同一接口 装饰器和被装饰者同一抽象类型
是否持有真实对象 是,但外部不可见 是,外部可见并传入

面试官常问:“如果我用装饰器模式做日志,和代理有什么区别?”——关键回答:代理关心的不是功能叠加,而是控制,装饰器是透明包装,代理是管控入口。


高频问答(面试官视角)

Q1:JDK动态代理为什么要求目标必须是接口? JVM的类继承是单根结构,生成的$Proxy0已经继承了Proxy类,如果目标还是类就冲突了,接口可以多实现,所以只能代理接口。

Q2:JDK动态代理和CGLIB如何选择?

  • 目标有接口 → 优先JDK动态代理(无需额外依赖,性能在JDK8+已接近CGLIB)
  • 目标无接口(纯类)→ 必须用CGLIB(或Spring的AOP自动选择)
  • 高并发下JDK代理反射成本略高,可考虑CGLIB的FastClass机制。

Q3:代理对象每次调用invoke都会走反射吗? 是的,每次方法调用都会经过invokemethod.invoke,但JDK8以后引入了MethodHandle优化,性能差距已缩小到5%以内。

Q4:如果被代理的方法声明为final会怎样? JDK动态代理不关心方法是否final(反射可调用),但CGLIB无法重写final方法,会导致无法增强。

Q5:InvocationHandler里的proxy参数有什么用? 它代表生成的代理对象本身,通常不会在invoke里使用它(否则会无限递归),但可以用来判断是否为代理对象,或做链式调用。

Q6:如何保证代理对象不被重复创建? 在Spring中,单例Bean的代理对象在IOC容器启动时创建一次并缓存,所有getBean返回的都是同一个代理。

Q7:代理对象能否用于多线程环境? 可以,但注意InvocationHandler实现要保证线程安全(比如不要把共享变量设为非volatile),因为多个线程会同时进入invoke方法。

Q8:JDK动态代理的类加载生命周期? 代理类由系统类加载器(AppClassLoader)加载,如果目标接口在自定义类加载器中,需传入对应ClassLoader,否则会类转换异常。

Q9:如何用动态代理实现一个简单的AOP切面? 定义一个BeforeAdviceAfterAdvice,在invoke里按顺序调用,这正是Spring AOP的雏形。

Q10:代理对象和真实对象equals/hashCode会一样吗? 不会,因为代理类没有重写equals/hashCode,它们继承自Object,所以proxy.equals(real)返回false,如果需要值比较,必须在接口中显式定义。


JDK动态代理是Java反射的极致应用,也是Spring AOP的基石,掌握它的源码机制、手写能力以及应用边界,是进阶Java高级工程师的必备技能,建议你在实际项目中,试着用代理模式解决跨切面问题(如日志、事务、缓存),对比静态代理的重构痛点,你会真正体会到动态代理“一处代码,全局复用”的威力。

抱歉,评论功能暂时关闭!