【深入浅出Java】代理

372 阅读6分钟

代理简介

代理是一种设计模式,通过代理对象访问目标对象,在不改变目标对象的前提下,拓展或增强目标对象的功能。

image.png

Java代理

一个类从编写,到运行时调用,中间大概会经过这几个步骤 image.png

所以生成代理可以有三个思路:

  1. 在编译期修改源代码;
  2. 在字节码加载前修改字节码;
  3. 在字节码加载后动态创建代理类的字节码

根据代理生成方式的不同,代理可以分为静态代理和动态代理,静态代理是在编译前修改源码,动态代理是在被代理对象加载之后动态生成代理类的字节码

静态代理

静态代理需要写一个代理类,代理类与被代理类要实现一样的接口,在代理类中调用目标对象的方法。

静态代理容易理解和编码,但是比较繁琐,扩展性不强,每个被代理对象都需要编码实现一个代理对象,接口增加方法,被代理对象与代理对象都要修改。

下面是一个静态代理的一个例子,我们有一个“交通工具”接口,实现类是“汽车”,代理对象是ProxyCar,ProxyCar 在“汽车”实现的方法前后输出日志。

// 接口
public interface Vehicle {
    void doSomething();
}
// 实现
public class Car implements Vehicle{
    @Override
    public void doSomething() {
        System.out.println("运行在公路上");
    }
}
// 代理对象 
public class ProxyCar implements Vehicle{
    private Car car;

    public ProxyCar(Car car){
        this.car = car;
    }

    @Override
    public void doSomething() {
        System.out.println("before car doSomething");
        car.doSomething();
        System.out.println("after car doSomething");
    }

    // 测试
    public static void main(String[] args) {
        Car car = new Car();
        ProxyCar proxyCar = new ProxyCar(car);
        proxyCar.doSomething();
    }
}

测试结果:

image.png

动态代理

动态代理是代理模式的一种,与静态代理不同,静态代理需要我们手动编码代理类,动态代理在内存中构建代理对象。在Spring框架中,常用的动态有JDK动态代理和CGLIB动态代理。

JDK动态代理

JDK动态代理原理

  1. 通过获取被代理对象的类加载器(getClassLoade()方法)和接口(getInterfaces()方法),实现InvocationHandler接口。
  2. 生成代理类字节码文件,使用类加载器将代理类对象动态加载到JVM中。
  3. 其中,代理类与被代理类实现同样的接口,实现接口的时候利用反射调用InvocationHandler接口的invoke方法。

demo

我们有一个“交通工具”接口,实现类是“汽车”,使用JDK动态代理生成代理对象,在“汽车”实现的方法前后输出日志。

// 接口
public interface Vehicle {
    void doSomething();
}
// 实现类
public class Car implements Vehicle{
    @Override
    public void doSomething() {
        System.out.println("运行在公路上");
    }
}
// JDK动态代理
public class JDKProxyFactory {
    private Object target;

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

    public Object getProxyInstance(){
        // get classLoader and interface by reflection
        ClassLoader classLoader = target.getClass().getClassLoader();
        Class<?>[] interfaces = target.getClass().getInterfaces();

        // generate proxy object
        Object proxyObject = Proxy.newProxyInstance(classLoader,interfaces,(proxy,method,args)->{
            System.out.println("开启事务");
            // 执行目标对象方法
            Object returnValue = method.invoke(target, args);
            System.out.println("提交事务");
            return null;
        });
        return proxyObject;
    }

    // test
    public static void main(String[] args) throws IOException {
        Vehicle car = new Car();
        System.out.println(car.getClass());
        Vehicle proxy = (Vehicle)new JDKProxyFactory(car).getProxyInstance();
        System.out.println(proxy.getClass());
        proxy.doSomething();
        // print proxy class file
        byte[] proxyBytes = ProxyGenerator.generateProxyClass("$Proxy0",new Class[]{proxy.getClass()});
        FileOutputStream os = new FileOutputStream("Proxy0.class");
        os.write(proxyBytes);
        os.close();
    }
}

下面是打印出来的字节码文件的部分内容

public final class $Proxy0 extends Proxy implements Proxy0 {
    private static Method m1;
    private static Method m7;
    private static Method m3;
    private static Method m2;
    private static Method m6;
    private static Method m11;
    private static Method m13;
    private static Method m0;
    private static Method m10;
    private static Method m12;
    private static Method m5;
    private static Method m9;
    private static Method m4;
    private static Method m8;
............
............
// 这里是代理类实现的被代理对象的接口的相同方法
public final void doSomething() throws  {
        try {
	   // super.h 对应的是父类的h变量,他就是Proxy.newProxyInstance方法中的InvocationHandler参数
           // 所以这里实际上就是使用了我们自己写的InvocationHandler实现类的invoke方法
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }
...........
...........
// 在静态构造块中,代理类通过反射获取了被代理类的详细信息
static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m7 = Class.forName("com.sun.proxy.$Proxy0").getMethod("getInvocationHandler", Class.forName("java.lang.Object"));
            m3 = Class.forName("com.sun.proxy.$Proxy0").getMethod("doSomething");
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m6 = Class.forName("com.sun.proxy.$Proxy0").getMethod("getProxyClass", Class.forName("java.lang.ClassLoader"), Class.forName("[Ljava.lang.Class;"));
            m11 = Class.forName("com.sun.proxy.$Proxy0").getMethod("getClass");
            m13 = Class.forName("com.sun.proxy.$Proxy0").getMethod("notifyAll");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
            m10 = Class.forName("com.sun.proxy.$Proxy0").getMethod("wait");
            m12 = Class.forName("com.sun.proxy.$Proxy0").getMethod("notify");
            m5 = Class.forName("com.sun.proxy.$Proxy0").getMethod("newProxyInstance", Class.forName("java.lang.ClassLoader"), Class.forName("[Ljava.lang.Class;"), Class.forName("java.lang.reflect.InvocationHandler"));
            m9 = Class.forName("com.sun.proxy.$Proxy0").getMethod("wait", Long.TYPE);
            m4 = Class.forName("com.sun.proxy.$Proxy0").getMethod("isProxyClass", Class.forName("java.lang.Class"));
            m8 = Class.forName("com.sun.proxy.$Proxy0").getMethod("wait", Long.TYPE, Integer.TYPE);
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

观察代理类的class文件,可以得到以下信息:

  1. 在最下面的静态构造块中,通过反射获取被代理对象的类信息。
  2. 代理对象与被代理对象实现相同的接口。
  3. 生成代理对象时,我们传入了一个 InvocationHandler 对象,实现了invoke方法。代理对象实现接口时,通过反射调用了InvocationHandler对象的invoke方法。

CGLIB动态代理

CGLIB(Code Generation Library)是一个第三方代码生成类库,CGLIB 动态代理通过在运行时在内存中动态生成一个子类对象从而实现对被代理对象功能的扩展,底层依靠ASM(开源的java字节码编辑类库)操作字节码实现的。

由于CGLIB是通过继承来实现动态代理,因此被代理类不能是final,同时目标类的方法不能是final,否则代理类就会直接调用目标类的方法。

demo

我们有一个被代理对象"飞行器",同时还实现了一个拦截器 AirCraftInterceptor,通过CGLIB动态代理生成代理对象,将代理对象打印出来

// 被代理对象
public class AirCraft {
    public void doSomething() {
        System.out.println("上天");
    }
}
// interceptor
public class AirCraftInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("before:"+method.getName());
        Object object = proxy.invokeSuper(obj, args);
        System.out.println("after:"+method.getName());
        return object;
    }

    // 测试用例
    public static void main(String[] args) {
	System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "/Users/******/Desktop/proxyInfo");
        Enhancer enhancer = new Enhancer();
        // 继承被代理类
        enhancer.setSuperclass(AirCraft.class);
        // 设置回调
        enhancer.setCallback(new AirCraftInterceptor());
        // 生成代理了对象
        AirCraft airCraft =  (AirCraft)enhancer.create();
        // 调用代理类的方法会被我们实现的方法拦截器进行拦截
        airCraft.doSomething();
    }
}

测试用例输出

CGLIB debugging enabled, writing to '/Users/*****/Desktop/proxyInfo'
before:doSomething
上天
after:doSomething

Process finished with exit code 0

下面是CGLIB生成的字节码文件的部分内容

public class AirCraft$$EnhancerByCGLIB$$13502f9c extends AirCraft implements Factory {
    private boolean CGLIB$BOUND;
    private static final ThreadLocal CGLIB$THREAD_CALLBACKS;
    private static final Callback[] CGLIB$STATIC_CALLBACKS;
    private MethodInterceptor CGLIB$CALLBACK_0;
    private static final Method CGLIB$doSomething$0$Method;
    private static final MethodProxy CGLIB$doSomething$0$Proxy;
    private static final Object[] CGLIB$emptyArgs;
    private static final Method CGLIB$finalize$1$Method;
    private static final MethodProxy CGLIB$finalize$1$Proxy;
    private static final Method CGLIB$equals$2$Method;
    private static final MethodProxy CGLIB$equals$2$Proxy;
    private static final Method CGLIB$toString$3$Method;
    private static final MethodProxy CGLIB$toString$3$Proxy;
    private static final Method CGLIB$hashCode$4$Method;
    private static final MethodProxy CGLIB$hashCode$4$Proxy;
    private static final Method CGLIB$clone$5$Method;
    private static final MethodProxy CGLIB$clone$5$Proxy;
..............
// 重写父类方法
public final void doSomething() {
        MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
        if (var10000 == null) {
            CGLIB$BIND_CALLBACKS(this);
            var10000 = this.CGLIB$CALLBACK_0;
        }

        if (var10000 != null) {
            var10000.intercept(this, CGLIB$doSomething$0$Method, CGLIB$emptyArgs, CGLIB$doSomething$0$Proxy);
        } else {
            super.doSomething();
        }
    }
...............
}

观察代理类的class文件,可以得到以下信息:

  1. 代理类继承了被代理类,重写了被代理类的方法
  2. 在重写的方法中,会先判断是否实现了MethodInterceptor的intercept方法,也就是是否实现了拦截器
  3. 如果实现了拦截器,则会调用我们实现的intercept方法。
  4. 没有实现拦截器,直接调用父类的方法

Spring AOP 与动态代理

Spring AOP基于动态代理实现,根据 Spring Framework 5.x 文档。Spring AOP 默认使用 JDK 动态代理,如果对象没有实现接口,则使用 CGLIB 代理。当然,也可以强制使用 CGLIB 代理。

Spring Boot 动态代理类型

你如果使用Spring Boot 1.5.x,你会发现默认是使用JDK动态代理,而在Spring Boot 2.x 中,你会发现不管对象有没有实现接口,都会使用CGLIB代理。关于这种行为的具体控制,总结来说是受到@AopAutoConfiguration注解的控制,尤其是配置项 proxy-target-class的取值。有兴趣的话可以看一下下面的分析。

image.png

一般在Spring Boot项目的配置类中会有@SpringBootApplication 这个注解, @SpringBootApplication是一个组合注解,该注解中使用@EnableAutoConfiguration实现了大量的自动装配。

image.png EnableAutoConfiguration也是一个组合注解,在该注解上被标志了@Import注入了AutoConfigurationImportSelector.

image.png AutoConfigurationImportSelector实现了DeferredImportSelector接口。

image.png

在 Spring Framework 4.x 版本中,这是一个空接口,它仅仅是继承了ImportSelector接口而已。而在 5.x 版本中拓展了DeferredImportSelector接口,增加了一个getImportGroup方法。

image.png

在这个方法中返回了 AutoConfigurationGroup

image.png AutoConfigurationGroup是AutoConfigurationImportSelector中的一个内部类,他实现了DeferredImportSelector.Group接口。

image.png 在 SpringBoot 2.x 版本中,就是通过AutoConfigurationImportSelector.AutoConfigurationGroupd的process方法来导入自动配置类的。执行this.autoConfigurationEntries.add(autoConfigurationEntry)的时候,加入的配置就包括 org.springframework.boot.autoconfigure.aop.AopAutoConfiguration

image.png AopAutoConfiguration中,在没有proxy-target-class配置项的情况下,默认使用CglibAutoProxyConfiguration。

所以,在 SpringBoot2.x 版本中,通过AopAutoConfiguration来自动装配 AOP。默认情况下,没有spring.aop.proxy-target-class这个配置项,此时默认使用 CGLIB 来实现动态。

image.png 这里我们贴一下SpringBoot 1.5.x 的AopAutoConfiguration,可以看到,在 SpringBoot 1.5.x 版本中,默认还是使用 JDK 动态代理的。

proxy-taget-class 选项配置

主要有两种配置方式:

配置方式一:

// 在application.properties文件中通过spring.aop.proxy-target-class来配置
spring.aop.proxy-target-class=false

配置方式二:

// 在配置类中使用注解
@EnableAspectJAutoProxy(proxyTargetClass = true)

为何 2.x 默认使用CGLIB

Spring Boot 2.x 默认使用 CGLIB,是为了防止在自动注入的时候出现代理问题。

一般情况下,我们的编码习惯都是面向接口编程,但是如果对象没有实现接口且默认使用JDK动态代理,或者我们的引用类型是实现类而不是接口,会导致注入失败。

假设,我们有一个 UserServiceImpl 类,这个类没有实现任何接口,我们通过下面的方式注入

@Autowired
UserServiceImpl userService;

如果我们使用的是JDK 动态代理,那在启动时就会报错。

总结

  1. 代理是一种设计模式,实现方式上分为静态代理和动态代理
  2. Spring AOP的原理就是动态代理,有 JDK动态代理 和 CGLIB动态代理 两种方式
  3. JDK动态代理基于接口实现,CGLIB动态代理基于继承生成子类实现。