一、Jvm加载对象

在说Java动态代理之前,还是要说一下Jvm加载对象的过程,这个依旧是理解动态代理的基础性原理:

Java类即源代码程序.java类型文件,经过编译器编译之后就被转换成字节代码.class类型文件,类加载器负责读取字节代码,并转换成java.lang.Class对象,描述类在元数据空间的数据结构,类被实例化时,堆中存储实例化的对象信息,并且通过对象类型数据的指针找到类。

过程描述:源码->.java文件->.class文件->Class对象->实例对象

所以通过New创建对象,独断其背后很多实现细节,理解上述过程之后,再了解一个常用的设计模式,即代理模式。

详细参考我的这篇文章:类加载子系统-东的博客

二、代理模式

1、基本描述

代理模式给某一个(目标)对象提供一个代理对象,并由代理对象持有目标对象的引用。所谓代理,就是一个对象代表另一个对象执行相应的动作程序。而代理对象可以在客户端和目标对象之间起到中介的作用。

动态代理在Java中有着广泛的应用,比如Spring AOP、Hibernate数据查询、测试框架的后端mock、RPC远程调用、Java注解对象获取、日志、用户鉴权、全局性异常处理、性能监控,甚至事务处理等。

在这个模式下存在两个关键角色:

目标对象角色:即代理对象所代表的对象。

代理对象角色:内部含有目标对象的引用,可以操作目标对象;AOP编程就是基于这个思想。

2、静动态模式

  • 静态代理:在程序运行之前确定代理角色,并且明确代理类和目标类的关系。

  • 动态代理:基于Java反射机制,在JVM运行时动态创建和生成代理对象。动态代理又分为JDK动态代理CGLIB动态代理实现。

三、静态代理

基于上述静态代理的概念,用一段代码进行描述实现,基本逻辑如下:

  • 明确目标对象即被代理的对象;

  • 定义代理对象,通过构造器持有目标对象;

  • 代理对象中定义前后置增强方法;

目标对象与前后置增强代码就组成了代理对象,这样就不用直接访问目标对象,像极了电视剧中那句话:我是律师,我的当事人不方便和你对话。

public class Proxy01 {
    public static void main(String[] args) {
        TargetObj targetObj = new TargetObj() ;
        ProxyObj proxyObj = new ProxyObj(targetObj) ;
        proxyObj.invoke();
    }
}
class TargetObj {
    public void execute (){
        System.out.println("目标类方法执行...");
    }
}
class ProxyObj {
    private TargetObj targetObj ;
    /**
     * 持有目标对象
     */
    public ProxyObj (TargetObj targetObj){
        this.targetObj = targetObj ;
    }
    /**
     * 目标对象方法调用
     */
    public void invoke (){
        before () ;
        targetObj.execute();
        after () ;
    }
    /**
     * 前后置处理
     */
    public void before (){
        System.out.println("代理对象前置处理...");
    }
    public void after (){
        System.out.println("代理对象后置处理...");
    }
}

静态代理明确定义了代理对象,即有一个代理对象的.java文件加载到JVM的过程,很显然的一个问题,在实际的开发过程中,不可能为每个目标对象都定义一个代理类,同样也不能让一个代理对象去代理多个目标对象,这两种方式的维护成本都极高。

代理模式的本质是在目标对象的方法前后置入增强操作,但是又不想修改目标类,通过前面反射机制可以知道,在运行的时候可以获取对象的结构信息,基于Class信息去动态创建代理对象,这就是动态代理机制。

顺便说一句:技术的底层实现逻辑不好理解是众所周知,然而基础知识点并不复杂,例如代理模式的基本原理,但是结合到实际的复杂应用中(AOP模式),很难活灵活现的理解到是基于反射和动态代理的方式实现的。

四、动态代理

1、场景描述

基于一个场景来描述动态代理和静态代理的区别,即最近几年很火的概念,海外代购:

在代购刚兴起的初期,是一些常去海外出差的人,会接代购需求,即代理人固定;后来就兴起海外代购平台,海淘等一系列产品,即用户代购需求(目标对象)由代购平台去实现,但是具体谁来操作这个就看即时分配,这个场景与动态代理的原理类似。

2、JDK动态代理

首先看两个核心类,这里简述下概念,看完基本过程再细聊:

  • Proxy-创建代理对象,核心参数:

    • ClassLoader:(目标类)加载器;

    • Interfaces:(目标类)接口数组;

    • InvocationHandler:代理调用机制;

  • InvocationHandler-代理类调用机制:

    • invoke:这个上篇说的反射原理;

    • method:反射类库中的核心API;

目标对象和接口

interface IUser {
    Integer update (String name) ;
}
class UserService implements IUser {
    @Override
    public Integer update(String name) {
        Integer userId = 99 ;
        System.out.println("UserId="+userId+";updateName="+name);
        return userId ;
    }
}
1234567891011

代理对象执行机制

class UserHandler implements InvocationHandler {
    private Object target ;
    public UserHandler (Object target){
        this.target = target ;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("before()...");
        Object result = method.invoke(target, args);
        System.out.println("after()...");
        return result;
    }
}
12345678910111213

具体组合方式

public class Proxy02 {
    public static void main(String[] args) {
        /*
         * 生成$Proxy0的class文件
         */
        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
        /*
         * 目标对象信息
         */
        IUser userService = new UserService();
        ClassLoader classLoader = userService.getClass().getClassLoader();
        Class<?>[] interfaces = UserService.class.getInterfaces() ;
        /*
         * 创建代理对象
         */
        InvocationHandler userHandler = new UserHandler(userService);
        /*
         * 代理类对象名
         * proxyClassName=com.java.proxy.$Proxy0
         */
        String proxyClassName = Proxy.newProxyInstance(classLoader,interfaces,userHandler).getClass().getName();
        System.out.println("proxyClassName="+proxyClassName);
        /*
         * 具体业务实现模拟
         */
        IUser proxyUser1 = (IUser) Proxy.newProxyInstance(classLoader,interfaces,userHandler);
        IUser proxyUser2 = (IUser) Proxy.newProxyInstance(classLoader,interfaces,userHandler);
        proxyUser1.update("cicada") ;
        proxyUser2.update("smile") ;
    }
}

InvocationHandler Proxy 的主要方法介绍如下:

  • java.lang.reflect.InvocationHandler

Object invoke(Object proxy, Method method, Object[] args) 定义了代理对象调用方法时希望执行的动作,用于集中处理在动态代理类对象上的方法调用

  • java.lang.reflect.Proxy

static InvocationHandler getInvocationHandler(Object proxy) 用于获取指定代理对象所关联的调用处理器

static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces) 返回指定接口的代理类

static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) 构造实现指定接口的代理类的一个新实例,所有方法会调用给定处理器对象的 invoke 方法

static boolean isProxyClass(Class<?> cl) 返回 cl 是否为一个代理类

另一种使用方法

public class ProxyUtil {
    public static Star createProxy(SuperStar superStar) {
        /*ClassLoader loader,
        Class<?>[] interfaces,
        InvocationHandler h
        参数1:用于指定一个类加载器,固定写法
        参数2:指定生成的代理对象是怎么样的,也就是有哪些方法,固定写法
        参数3:指定一个调用处理器,也就是代理对象调用方法时,会调用哪个类的invoke方法(指定代理对象要做什么事情)
        */
        Star starProxy = (Star) Proxy.newProxyInstance(ProxyUtil.class.getClassLoader(),
                new Class[]{Star.class},
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        //代理对象要做的事情,会在这里写代码
                        if (method.getName().equals("sing")) {
                            System.out.println("准备话筒,收钱20");
                        } else if (method.getName().equals("dance")) {
                            System.out.println("准备场地,收钱20w");
                        }
                        return method.invoke(superStar, args);
                    }
                });
        return starProxy;
    }
}

3、CGLIB动态代理

maven引入CGLIB包,然后编写一个UserDao类,它没有接口,只有两个方法,select() 和 update()

public class UserDao {
    public void select() {
        System.out.println("UserDao 查询 selectById");
    }
    public void update() {
        System.out.println("UserDao 更新 update");
    }
}

编写一个 LogInterceptor ,继承了 MethodInterceptor,用于方法的拦截回调

import java.lang.reflect.Method;
import java.util.Date;

public class LogInterceptor implements MethodInterceptor {
    /**
     * @param object 表示要进行增强的对象
     * @param method 表示拦截的方法
     * @param objects 数组表示参数列表,基本数据类型需要传入其包装类型,如int-->Integer、long-Long、double-->Double
     * @param methodProxy 表示对方法的代理,invokeSuper方法表示对被代理对象方法的调用
     * @return 执行结果
     * @throws Throwable
     */
    @Override
    public Object intercept(Object object, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        before();
		// 注意这里是调用 invokeSuper 而不是 invoke,否则死循环,methodProxy.invokesuper执行的是原始类的方法,method.invoke执行的是子类的方法
        Object result = methodProxy.invokeSuper(object, objects);   
        after();
        return result;
    }
    private void before() {
        System.out.println(String.format("log start time [%s] ", new Date()));
    }
    private void after() {
        System.out.println(String.format("log end time [%s] ", new Date()));
    }
}

测试

import net.sf.cglib.proxy.Enhancer;

public class CglibTest {
    public static void main(String[] args) {
        LogInterceptor logInterceptor = new LogInterceptor(); 
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(Dao.class);  // 设置超类,cglib是通过继承来实现的
        enhancer.setCallback(logInterceptor);

        Dao dao = (Dao)enhancer.create();   // 创建代理类
        dao.update();
        dao.select();
    }
}

运行结果

log start time [Fri Dec 21 00:06:40 CST 2018] 
UserDao 查询 selectById
log end time [Fri Dec 21 00:06:40 CST 2018] 
log start time [Fri Dec 21 00:06:40 CST 2018] 
UserDao 更新 update
log end time [Fri Dec 21 00:06:40 CST 2018] 

还可以进一步多个 MethodInterceptor 进行过滤筛选

public class LogInterceptor2 implements MethodInterceptor {
    @Override
    public Object intercept(Object object, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        before();
        Object result = methodProxy.invokeSuper(object, objects);
        after();
        return result;
    }
    private void before() {
        System.out.println(String.format("log2 start time [%s] ", new Date()));
    }
    private void after() {
        System.out.println(String.format("log2 end time [%s] ", new Date()));
    }
}

// 回调过滤器: 在CGLib回调时可以设置对不同方法执行不同的回调逻辑,或者根本不执行回调。
public class DaoFilter implements CallbackFilter {
    @Override
    public int accept(Method method) {
        if ("select".equals(method.getName())) {
            return 0;   // Callback 列表第1个拦截器
        }
        return 1;   // Callback 列表第2个拦截器,return 2 则为第3个,以此类推
    }
}

再次测试

public class CglibTest2 {
    public static void main(String[] args) {
        LogInterceptor logInterceptor = new LogInterceptor();
        LogInterceptor2 logInterceptor2 = new LogInterceptor2();
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(UserDao.class);   // 设置超类,cglib是通过继承来实现的
        enhancer.setCallbacks(new Callback[]{logInterceptor, logInterceptor2, NoOp.INSTANCE});   // 设置多个拦截器,NoOp.INSTANCE是一个空拦截器,不做任何处理
        enhancer.setCallbackFilter(new DaoFilter());

        UserDao proxy = (UserDao) enhancer.create();   // 创建代理类
        proxy.select();
        proxy.update();
    }
}

运行结果

log start time [Fri Dec 21 00:22:39 CST 2018] 
UserDao 查询 selectById
log end time [Fri Dec 21 00:22:39 CST 2018] 
log2 start time [Fri Dec 21 00:22:39 CST 2018] 
UserDao 更新 update
log2 end time [Fri Dec 21 00:22:39 CST 2018] 

CGLIB工作于类的字节码层面。它通过ASM(一个Java字节码操作和分析框架)动态生成新的类字节码。CGLIB的工作流程大致如下:

  1. 类扩展:CGLIB动态生成一个目标类的子类。这个子类重写了目标类的所有非final方法。

  2. 方法拦截:在子类中,每个重写的方法都包含了额外的逻辑,通常会调用一个MethodInterceptor。当你调用代理类的方法时,实际上是执行MethodInterceptor中的intercept方法。

  3. 代码生成与加载:生成的类被编译并加载到JVM中,然后可以像普通类一样使用。

4、JDK动态代理与CGLIB动态代理对比

  • JDK动态代理:基于Java反射机制实现,必须要实现了接口的业务类才能用这种办法生成代理对象。

  • cglib动态代理:基于ASM机制实现,通过生成业务类的子类作为代理类。

JDK Proxy 的优势:

  • 最小化依赖关系,减少依赖意味着简化开发和维护,JDK 本身的支持,可能比 cglib 更加可靠。

  • 平滑进行 JDK 版本升级,而字节码类库通常需要进行更新以保证在新版 Java 上能够使用。

  • 代码实现简单。

CGLIB框架的优势:

  • 无需实现接口,达到代理类无侵入

  • 只操作我们关心的类,而不必为其他相关类增加工作量。

  • 功能更强大,灵活性更高。

面试题

来源于网上,用于帮助理解和掌握,欢迎补充

描述动态代理的几种实现方式?分别说出相应的优缺点

代理可以分为 "静态代理" 和 "动态代理",动态代理又分为 "JDK动态代理" 和 "CGLIB动态代理" 实现。

静态代理:代理对象和实际对象都继承了同一个接口,在代理对象中指向的是实际对象的实例,这样对外暴露的是代理对象而真正调用的是 Real Object

  • 优点:可以很好的保护实际对象的业务逻辑对外暴露,从而提高安全性。

  • 缺点:不同的接口要有不同的代理类实现,会很冗余

JDK 动态代理

  • 为了解决静态代理中,生成大量的代理类造成的冗余;

  • JDK 动态代理只需要实现 InvocationHandler 接口,重写 invoke 方法便可以完成代理的实现,

  • jdk的代理是利用反射生成代理类 Proxyxx.class 代理类字节码,并生成对象

  • jdk动态代理之所以只能代理接口是因为代理类本身已经extends了Proxy,而java是不允许多重继承的,但是允许实现多个接口

  • 优点:解决了静态代理中冗余的代理实现类问题。

  • 缺点:JDK 动态代理是基于接口设计实现的,如果没有接口,会抛异常。

CGLIB 代理

  • 由于 JDK 动态代理限制了只能基于接口设计,而对于没有接口的情况,JDK方式解决不了;

  • CGLib 采用了非常底层的字节码技术,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑,来完成动态代理的实现。

  • 实现方式实现 MethodInterceptor 接口,重写 intercept 方法,通过 Enhancer 类的回调方法来实现。

  • 但是CGLib在创建代理对象时所花费的时间却比JDK多得多,所以对于单例的对象,因为无需频繁创建对象,用CGLib合适,反之,使用JDK方式要更为合适一些。

  • 同时,由于CGLib由于是采用动态创建子类的方法,对于final方法,无法进行代理。

  • 优点:没有接口也能实现动态代理,而且采用字节码增强技术,性能也不错。

  • 缺点:技术实现相对难理解些。

参考文章
Java进阶 | Proxy动态代理机制详解-CSDN博客

Java动态代理详解-稀土掘金