Java中的代理

280 阅读9分钟

代理技术是指,在不改变原始类(或者叫被代理类)代码的情况下,通过引入代理类来给增强原始类的功能。代理技术常用于日志、计数器等等场景当中,作用是用于实现框架代码跟业务代码解耦。

在Java中,代理技术有静态代理和动态代理之分:

  • 静态代理是指代理类在程序运行前就已经存在,往往是在代码编译的时候就存在的。静态代理类可以通过程序员手写,或者编译时自动生成等方式实现。当我们在实现设计模式中的代理模式时,我们就是在手写静态代理。

  • 动态代理是指代理类在程序运行时被创建,比如Spring的AOP、cglib动态代理等。动态代理的好处是足够灵活。

一、设计模式:Proxy Pattern

代理模式有两种实现方法:

  • 基于接口的实现:原始类和代理类都实现了同一个接口,依据基于接口编程的思想,在使用时创建代理类对象来替代原始类对象

  • 基于继承的实现:代理类通过继承原始类并override原始类方法的方式,来实现对原始类对象的代理

1.1 基于接口实现的代理模式

我们来看一个日志打印的需求,要求是在查询数据库的方法前后,打印出入参和出参的信息。

假设现在有一个查询用户信息的Repository接口及其实现:

public interface UserRepository {
    // 这是一个通过name查询用户地址的方法
    String getAddress(String name);
}
public class UserRepositoryImpl implements UserRepository {
    // 这个就是原始类
    @Override
    public String getAddress(String name) {
        // 这里做一些查询数据库的逻辑
        String address = doGetAddressByNameFromDB(name);
        return address;
    }
}
public class App {
    public static void main() {
        // 基于接口编程的思想
        UserRepository userRepository = new UserRepositoryImpl();
        String userName = "caicai";
        String address = userRepository.getAddress(userName);
        System.out.println(address);
    }
}

当我们没有使用代理模式时,为了实现打印出入参信息的需求,我们一般会这么写:

public class UserRepositoryImpl implements UserRepository {
    // 这个就是原始类
    @Override
    public String getAddress(String name) {
        // 打印入参
        System.out.println("UserRepository#getAddress, args: " + name);
        // 这里做一些查询数据库的逻辑
        String address = doGetAddressByNameFromDB(name);
        // 打印出参
        System.out.println(
            "UserRepository#getAddress, return value: " + address);
        return address;
    }
}

这个时候你会发现,日志这个功能跟业务逻辑是完全耦合在一起的,当你想要修改日志的时候,就必须去业务代码里修改,就比较麻烦。代理模式就可以解决这个问题。

// 我们创建一个代理类,跟原始类实现同一个接口
public UserRepositoryImplProxy implements UserRepository {
    // 原始类的实现被代理,作为代理类的一个字段
    private UserRepository userRepository = new UserRepository();

    @Override
    public String getAddress(String name) {
        // 打印入参
        System.out.println("UserRepository#getAddress, args: " + name);
        // 调用被代理的对象的方法,这时候就实现了业务代码跟这个打日志的代码的解耦
        String address = userRepository.getAddress(userName);
        // 打印出参
        System.out.println(
            "UserRepository#getAddress, return value: " + address);
    }
}
// 使用的时候可以做一些修改,直接使用代理类
public class App {
    public static void main() {
        // 基于接口编程的思想,使用代理类对象替换原始类对象
        UserRepository userRepository = new UserRepositoryImplProxy();
        String userName = "caicai";
        String address = userRepository.getAddress(userName);
        System.out.println(address);
    }
}

基于接口的代理,其前提是必须存在一个接口。但在现实的应用场景中,可能并不存在接口,这时候就可以考虑使用继承的方式来实现代理模式。

1.2 基于继承实现的代理模式

同样还是上面那个例子,只不过这一次没有了接口:

public class UserRepository {
    // 这个就是原始类,没有实现任何接口
    @Override
    public String getAddress(String name) {
        // 这里做一些查询数据库的逻辑
        String address = doGetAddressByNameFromDB(name);
        return address;
    }
}
public class App {
    public static void main() {
        UserRepository userRepository = new UserRepository();
        String userName = "caicai";
        String address = userRepository.getAddress(userName);
        System.out.println(address);
    }
}

我们通过继承来实现代理模式:

public class UserRepositoryProxy extends UserRepository {
    // 代理类直接继承了原始类,通过覆写原始类方法的方式实现代理
    @Override
    public String getAddress(String name) {
        // 打印入参
        System.out.println("UserRepository#getAddress, args: " + name);
        // 调用被代理的对象的方法,实际上就是父类的方法
        // 这时候就实现了业务代码跟这个打日志的代码的解耦
        String address = super.getAddress(userName);
        // 打印出参
        System.out.println(
            "UserRepository#getAddress, return value: " + address);
    }
}
// 使用的时候可以做一些修改,直接使用代理类
public class App {
    public static void main() {
        // 基于接口编程的思想,使用代理类对象替换原始类对象
        UserRepository userRepository = new UserRepositoryProxy();
        String userName = "caicai";
        String address = userRepository.getAddress(userName);
        System.out.println(address);
    }
}

1.3 静态代理的缺点

上文中所述的代理模式都是静态代理,虽然比较容易理解,但我们会发现静态代理其实不够灵活。

一方面,我们需要在代理类中,将原始类的所有方法都重新实现一遍,并且为每个方法增加相似的代码逻辑。比如如果上文例子中的UserRepository有多个方法都需要打印日志的话,就是要在代理类里把每个方法都实现一遍然后加上日志的逻辑的。

另一方面,如果要添加的附加功能的类不止一个,我们就需要针对每个类都创建一个代理类。比如如果上文的例子中,又出现了一个DepartmentRepository需要加日志的功能,我们又得针对他再创建一个代理类,这在现实的项目中,会导致类成倍的增加,且每个代理类中的代码都是类似的,这就增加了很多不必要的开发成本。

动态代理就可以解决这些静态代理多带来的缺点。

二、动态代理

所谓的动态代理,就是我们不必事先为每个原始类编写代理类,而是在运行时,动态的创建原始类所对应的代理类,然后在系统运行时替换掉原始类。

比较常见的动态代理有两种:

  • JDK自带的动态代理:基于Java的反射机制实现

  • cglib、javassist等开源工具:基于修改字节码的机制实现动态代理

2.1 JDK动态代理

还是用上面的那个打日志的例子,不过这一次增加一个DepartmentRepository:

public interface UserRepository {
    String getAddress(String name);
    // 增加了一个方法
    Integer getAge(String name);
}
public class UserRepositoryImpl implements UserRepository {
    @Override
    public String getAddress(String name) {
        return "杭州市";
    }
    @Override
    public Integer getAge(String name) {
        return 10;
    }
}
// 增加了一个接口DepartmentRepository
public interface DepartmentRepository {
    Integer getEmployeeCount(String name);
}

public class DepartmentRepositoryImpl implements DepartmentRepository {
    @Override
    public Integer getEmployeeCount(String name) {
        return 100;
    }
}

如果按照的静态代理的方式,我们需要做如下事情:

  • 创建UserRepositoryImpl的代理类,然后实现两个方法,并且在两个方法中加上打印日志的逻辑

  • 创建DepartmentRepositoryImpl的代理类,然后实现方法,并且在方法中加上打印日志的逻辑

这就非常的繁琐,并且打印日志的逻辑实际上是通用的,但是在静态代理里面无法复用。我们使用JDK动态代理的方式来解决这个问题:

// 这个是一个为了使用JDK代理的工具类
public class JdkProxyHandler implements InvocationHandler {
    // 被代理的对象
    private final Object target;

    public JdkProxyHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 只需要写这一个通用的方法即可,不需要给每一个原始类都创建代理类了,也不需要实现所有的方法
        System.out.printf("ClassName: %s, methodName: %s, args: %s%n",
                target.getClass().getSimpleName(), method.getName(), Arrays.toString(args));
        Object result = method.invoke(target, args);
        System.out.printf("ClassName: %s, methodName: %s, result: %s%n",
                target.getClass().getSimpleName(), method.getName(), String.valueOf(result));
        return result;
    }
}
// 使用的时候就非常的简单了
public class DynamicProxyApp {
    public static void main(String[] args) {

        UserRepository userRepository = (UserRepository) Proxy.newProxyInstance(
                DynamicProxyApp.class.getClassLoader(),
                // 这里就是被代理的接口,所以说JDK代理一定要有接口才能做
                new Class[]{UserRepository.class},
                // 这个就是被代理的对象
                new JdkProxyHandler(new UserRepositoryImpl()));

        userRepository.getAddress("caicai");
        userRepository.getAge("caicai");

        DepartmentRepository departmentRepository = (DepartmentRepository) Proxy.newProxyInstance(
                DynamicProxyApp.class.getClassLoader(),
                new Class[]{DepartmentRepository.class},
                new JdkProxyHandler(new DepartmentRepositoryImpl()));

        departmentRepository.getEmployeeCount("caicai");
    }
}

执行结果如下:

ClassName: UserRepositoryImpl, methodName: getAddress, args: [caicai]
ClassName: UserRepositoryImpl, methodName: getAddress, result: 杭州市

ClassName: UserRepositoryImpl, methodName: getAge, args: [caicai]
ClassName: UserRepositoryImpl, methodName: getAge, result: 10

ClassName: DepartmentRepositoryImpl, methodName: getEmployeeCount, args: [caicai]
ClassName: DepartmentRepositoryImpl, methodName: getEmployeeCount, result: 100

2.1.1 为什么JDK的代理一定是基于接口实现的

在JDK动态代理的过程中,会生成对应的代理类(形如$Proxy0.class这种名字的类),这些匿名类是继承了java.lang.reflect.Proxy类的,并且实现了我们所传入的接口。由于Java不支持多继承,当前的代理类为了能够替换掉原始类,所以就只能通过实现目标接口的方式来对原始类做扩展。

我们只需要看一下所生成的代理类就明白了:

public class DynamicProxyApp {
    public static void main(String[] args) {
        // 加这一行,就能够把JDK生成的代理类保存在本地,在本工程的 jdk目录下
        // 使用Java17是jdk.proxy.ProxyGenerator.saveGeneratedFiles这个参数,
        // 如果使用其他Java版本,可以参考下 java.lang.reflect.ProxyGenerator这个类中的saveGeneratedFiles字段的值
        // 用它的值替换jdk.proxy.ProxyGenerator.saveGeneratedFiles就行
        System.getProperties().put("jdk.proxy.ProxyGenerator.saveGeneratedFiles","true");

        DepartmentRepository departmentRepository = (DepartmentRepository) Proxy.newProxyInstance(
                DynamicProxyApp.class.getClassLoader(),
                new Class[]{DepartmentRepository.class},
                new JdkProxyHandler(new DepartmentRepositoryImpl()));
        departmentRepository.getEmployeeCount("caicai");
    }
}

上面这一段代码执行后,我们就可以在工程目录下的jdk文件夹下找到JDK所生成的代理类:

// 1. 从这边可以看到,这个代理类,继承了Proxy,实现了DepartmentRepository
// 由于Java只支持单继承,为了能够代理DepartmentRepositoryImpl,我们就只能让
// 这个代理类实现DepartmentRepository接口,也就是上面所提到的基于接口实现代理的方式
public final class $Proxy0 extends Proxy implements DepartmentRepository {
    private static final Method m0;
    private static final Method m1;
    private static final Method m2;
    private static final Method m3;

    public $Proxy0(InvocationHandler invocationHandler) {
        super(invocationHandler);
    }
    // 2. 这个就是我们所要被代理的方法
    public final Integer getEmployeeCount(String var1) {
        try {
            // 2.1 这个 super.h 就是我们使用时候传入的 invocationHandler 对象
            // 就是上文中 new JdkProxyHandler(new DepartmentRepositoryImpl())
            // 这个对象
            // 这里我们可以看到,基本上就是通过反射来调用我们在JdkProxyHandler中写的方法
            // 执行我们封装好的逻辑
            return (Integer)super.h.invoke(this, m3, new Object[]{var1});
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        try {
            // 3.静态代码块,用于通过反射来初始化四个方法属性
            // 我们关注的其实就是 m3 这个方法,其他的几个方法是Object自带
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m3 = Class.forName("com.zju.caicai.common.proxy.DepartmentRepository").getMethod("getEmployeeCount", Class.forName("java.lang.String"));
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

2.2 基于字节码修改技术的动态代理

来看一下如何使用cglib实现动态代理,先把cglib的依赖加进来:

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.3.0</version>
</dependency>

然后用cglib做一下动态代理:

public class OrderRepository {
    // 这个是要被代理的类,它并没有实现接口
    public Long getPrice(String orderId) {
        return 100L;
    }
}
public class LogInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object target, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        // 这里其实跟JdkProxyHandler的代理写法差不多,也是个工具类
        System.out.printf("ClassName: %s, methodName: %s, args: %s%n",
                target.getClass().getSimpleName(), method.getName(), Arrays.toString(args));
        // 这里是invokeSuper,即cglib是通过继承的方式来实现动态代理的
        Object result = methodProxy.invokeSuper(target, args);
        System.out.printf("ClassName: %s, methodName: %s, result: %s%n",
                target.getClass().getSimpleName(), method.getName(), String.valueOf(result));
        return result;
    }
}
// 调用的地方需要修改下
public class DynamicProxyApp {
    public static void main(String[] args) {
        // 加这一行,是为了把cglib生成的类保存下来,后面的参数是路径
        System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "/Users/caicai/Documents/Workspace/Codes/common/jdk/proxy1");
        Enhancer enhancer = new Enhancer();
        // superClass是原始类,这里也能看出cglib是用继承实现动态代理的
        enhancer.setSuperclass(OrderRepository.class);
        // 这个是代理类
        enhancer.setCallback(new LogInterceptor());
        // 创建来代理类对象以后,直接调用即可
        OrderRepository orderRepository = (OrderRepository) enhancer.create();
        orderRepository.getPrice("xxxx");
    }
}

由于cglib是基于继承实现的代理,并且java是不允许继承由final修饰的方法和类的,所以这种方式无法代理被final修饰的方法和类。

2.2.1 cglib生成了3个代理类?

当我们运行完上面的代码之后,会发现,cglib居然一下子就给我们生成了3个类:

  • OrderRepository$$EnhancerByCGLIB$$b537f8d3

  • OrderRepository$$EnhancerByCGLIB$$b537f8d3$$FastClassByCGLIB$$a878acf1

  • OrderRepository$$FastClassByCGLIB$$ef66e34d

先来看第一个OrderRepository$$EnhancerByCGLIB$$b537f8d3

// 这个就是代理类,可以看到是继承了OrderRepository
// 本文中的是简化了之后的,真实的代理类比这个更复杂一些,多一些hash、equals等方法
// 而且变量名也更不可读,所以这里做了些优化,但逻辑是完全是一样
public class OrderRepository$$EnhancerByCGLIB$$b537f8d3 extends OrderRepository implements Factory {
    // 拦截器,就是所注册的LogInterceptory
    private MethodInterceptor logInterceptor;
    // 被代理的方法
    private static final Method getPrice;
    // 代理的方法
    private static final MethodProxy getPriceProxy;

    static void CGLIB$STATICHOOK1() {
        CGLIB$THREAD_CALLBACKS = new ThreadLocal();
        CGLIB$emptyArgs = new Object[0];
        // 这个就是生成的代理类
        Class var0 = Class.forName("com.zju.caicai.common.proxy.OrderRepository$$EnhancerByCGLIB$$b537f8d3");
        // 这个是被代理类
        Class var1;
        getPrice = ReflectUtils.findMethods(new String[]{"getPrice", "(Ljava/lang/String;)Ljava/lang/Long;"}, (var1 = Class.forName("com.zju.caicai.common.proxy.OrderRepository")).getDeclaredMethods())[0];
        getPriceProxy = MethodProxy.create(var1, var0, "(Ljava/lang/String;)Ljava/lang/Long;", "getPrice", "CGLIB$getPrice$0");
    }

    final Long CGLIB$getPrice$0(String orderId) {
        // 这个是调用原来的没有被代理的方法
        return super.getPrice(orderId);
    }

    public final Long getPrice(String orderId) {
        这里就是调用被代理的方法
        MethodInterceptor interceptor = this.logInterceptor;
        return interceptor != null ? (Long)interceptor.intercept(this, getPrice, new Object[]{orderId}, getPriceProxy) : super.getPrice(orderId);
    }
}

FastClass机制

cglib多生成的几个类,实际上是它为了优化执行效率所设计的FastClass机制,FastClass机制就是对一个类的方法建立索引,通过索引来直接调用相应的方法。

看一个FastClass的例子,其实原理很简单:

public class InventoryRepository {
    // 被代理类,有两个方法可以被代理,在JDK代理里面,一般都是通过反射执行的
    public Long getInventoryById(String id) {
        return 100L;
    }

    public Long getInventoryByName(String name) {
        return 120L;
    }
}
public class InventoryFastClass {
    // 这个就是被代理类的FaceClass
    public Object invoke(int index, Object obj, Object[] args) {
        InventoryRepository inventoryRepository = (InventoryRepository) obj;
        // 其实就是这里给InventoryRepository的每个方法都做了索引,
        // 这样就可以直接调用,而不需要用反射,这样执行效率就会更高
        switch (index) {
            case 1:
                return inventoryRepository.getInventoryById((String) args[0]);
            case 2:
                return inventoryRepository.getInventoryByName((String) args[0]);
        }
        return null;
    }

    public int getIndex(String name) {
        switch (name) {
            case "getInventoryById":
                return 1;
            case "getInventoryByName":
                return 2;
        }
        return -1;
    }
}
public class FastClassApplication {
    // 使用的时候就很简单了
    public static void main(String[] args) {
        InventoryRepository inventoryRepository = new InventoryRepository();
        InventoryFastClass inventoryFastClass = new InventoryFastClass();
        // 本质上就是对原始类的所有方法,都做了一个索引,以此来绕过反射,直接找到对应的方法执行
        int index = inventoryFastClass.getIndex("getInventoryById");
        inventoryFastClass.invoke(index, inventoryRepository, new Object[]{"caicai"});
    }

}

2.3 动态代理总结

最后我们总结一下JDK动态代理和Gglib动态代理的区别:

  • JDK动态代理是实现了被代理对象的接口,Cglib是继承了被代理对象

  • JDK和Cglib都是在运行期生成字节码,JDK是直接写Class字节码,Cglib使用ASM框架写Class字节码,Cglib代理实现更复杂,生成代理类比JDK效率低。

  • JDK调用代理方法,是通过反射机制调用,Cglib是通过FastClass机制直接调用方法,Cglib执行效率更高。