Java JDK动态代理JDK和CGLIB动态代理

0. 代理

    代理是一种设计模式, 一种不直接访问目标, 通过代理访问目标的设计模式. 好像等于白说?代理其实在很多地方都有使用到, 不只是Java,在nginx中有正向代理反向代理, 生活中有律师代理他的当事人进行变护.用Java代码来解释:

class A {
	public void add()
}

class B {
	private A a;
	public B(A a){
		this.a = a;
	}
	public void add() {
		a.add()
	}
}

    上述代码中想要调用A.add()方法但是不直接通过创建A对象的实例来调用, 而采用创建B对象实例, 传入A对象实例, 通过调用B.add()最终完成了对A的调用这就是代理. 作为使用者我们只调用了new B().add() , 并没有调用a.add(), a.add()是在b.add()内部调用的.

1.0 静态代理

    静态代理的代理类和目标类需要继承相同的父类或者实现相同的接口,通过父类型方法或者接口方法的调用实现代理.

// 接口
interface I {
	void add()
}
// 需要被代理的目标类
class T implements I{
	public void add() {
		System.out.println("T add");
	}
}
// 代理类, 通过此类调用目标类.
class P implements I {
	private I i;
	public P (I i) { this.i = i; }
	public void add(){
		System.out.println("P add")
		i.add();
	}
}

public class Test{
	public static void main(String ...args){
		I i = new T();
		I proxy = new P(i); // 将目标类通过代理类的构造函数传入
		proxy.add(); // 调用代理类实现的add方法, 内部对目标类T实现add方法进行调用.
	}
} // 依次输出 P add 和 T add

    有了静态代理为什么还需要动态代理呢, 又或者说静态代理的"静"在什么地方. 观察以上的代码, 就会发现静态代理的缺陷:

  1. 静态代理的目标类T和代理类P都需要实现相应的接口, 或者继承相应父类
  2. 静态代理就"静"在必须由编程人员手写代理类P, 如果有很多个类需要代理就要写很多个代理类, 当然这也可以使用代理工厂类似的手段做优化, 但是始终逃离不了写代理类. 代理类都是事先写好再生成的, 先有.class文件再有代理对象. 这就是静态代理的"静".

2.0 动态代理

    动态代理的目标就是要解决静态代理存在的各种问题, 目前在Java中存在两种主流的动态代理, 一是JDK自带的动态代理, 好像自JDK1.3开始引入(不确定), 二是CGLIB动态代理, 在spring两种代理兼有, 下面就分别介绍两种代理.

2.1 JDK动态代理

    JDK的动态代理相关的类都在java.lang.reflect包下, 核心的就两个, java.lang.reflect.Proxy类和java.lang.reflect.InvocationHandler接口

  1. Proxy类: 这个类就是动态代理类相当于上一节代码块中写的P类, 这就是Dynamic Proxy中代理类, 相较于之前静态代理我们自己写的P类,JDK动态代理就免去写代理类了.
    (1).public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces,InvocationHandler h), 使用中通过调用这个方法生成代理类实例. 实际这里生成的代理类实例通过将生成的字节码反编译可知这个代理类实际是extends Proxy implements 用户接口 , 这里其实也说明了一个问题, JDK动态代理只能代理接口不能代理类, 就因为动态代理生成的类继承了Proxy, 而Java语言只支持单继承.
    (2). public static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces)这个方法就是生成动态代理类Class对象的关键, 它内部调用了sun.misc.ProxyGenerator.generateProxyClass方法进行了字节码生成.
  2. InvocationHandler接口: 我们自身要实现这个接口的public Object invoke(Object proxy, Method method, Object[] args)方法, 几个参数依次分别是代理类实例, 被调用的方法对象, 方法所需的参数. 当然一般情况下,这个接口的实现类还需要持有被代理的目标类的对象实例.

    接下来看看JDK 动态代理的代码实例:

// 需要被代理的接口
public interface Animal {
	String eat();
	// ... 可以有多个方法
}
// 实际被代理的类实现了Animal接口
public class Dog implements Animal {
	public String eat () {
		System.out.println("dog eat food");
		return "Dog eat finish";
	}
}
// 调用处理类, 对被代理的方法前后添加处理逻辑的实现类, 这个类实现InvocationHandler接口
public class AnimalInvocationhandlerImpl implements InvocationHandler{
	private Animal animal;
	// 通过构造函数持有被代理的类
	public AnimalInvocationhandlerImpl (Animal animal) {
		this.animal = animal;
	}
	// 实现invoke接口
	public Object invoke(Object proxy, Method method, Object[] args) {
		System.out.println("before invoke target method");
		// 调用被代理的目标类的方法, 这是通过反射进行调用的. 
		Object res = method.invoke(animal, args);
		System.out.println("after invoke target method");
		return res;
	}
}

public class Test {
	public static void main(String ... args){
		Animal target = new Dog();
		InvocationHandler handler = new AnimalInvocationhandlerImpl(target);
		// 生成代理类proxy 传入参数分别是类加载器, 被代理的目标类的实现接口, 以及处理类
		Animal proxy = (Animal)Proxy.newProxyInstance(
										Test.class.getClassLoader(),
										target.getClass().getInterfaces(), handler);
		System.out.println(proxy.eat());
	}
} 
// 测试输出
// before invoke target method - dog eat food - after invoke target method - Dog eat finish

    给你们证明一波我说的, 保存生成的proxy对象的字节码到本地文件反编译的代理类代码如下, 如果需要保存生成的class文件,则需要在上面Test.main方法的首行设置环境变量System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");只适用于JDK1.8及其以下的, 1.8以上的ProxyGenerator在jdk.proxy下.删除了部分Object类继承下来的代码留下些重要的说明, 参照注释

// 1. 代理类确实继承了Proxy类, 所以JDK动态代理只能基于接口代理
// 2. 代理类也确实实现了接口
final class $Proxy0 extends Proxy implements Animal {
    public $Proxy0(InvocationHandler var1) throws  {
        super(var1);
    }
    public final String eat() throws  {
        try {
        	// 2. 确实是通过反射调用
            return (String)super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final String toString() throws  {
        try {
            return (String)super.h.invoke(this, m2, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }
    static {
       m3 = Class.forName("com.jinxin.beoneWeb3d.utils.Animal").getMethod("eat");
       m2 = Class.forName("java.lang.Object").getMethod("toString");
    }
}

2.2 CGLIB动态代理.

    CGLIB是基于开源的asm框架也就是字节码增强技术实现的动态代理, 是通过修改被代理对象的字节码成为被代理对象的子类来实现代理功能的, 这一点有别于JDK动态代理, 上一小节JDK动态代理最终生成的代理类是采用的实现目标类相同接口的方式.所以可以说CGLIB是通过继承目标类, 重写被代理的方法实现代理. 所以明显的不会有JDK自带的动态代理目标类必须实现接口的局限性, 但是也就因为继承它也有自身的局限性. 还记得final关键字吗? final修饰的类无法被继承, 所以CGLIB无法代理被final修饰的类. 相关的类或者接口.

  1. MethodInterceptor接口: 拦截器接口, 有一个需要实现的方法public Object intercept(Object obj, java.lang.reflect.Method method, Object[] args, MethodProxy proxy), 代理类会调用这个intercept方法, 那么又在什么时候调用呢? 后面原理的时候会解释
    (1). obj 代理对象由cglib生成
    (2). method 被代理的方法引用.
    (3). 方法参数.
    (4). obj 中的代理方法引用.
  2. Enhancer类: 此类通过无参构造函数创建, 用于创建代理对象

    先看代码, 再说原理

// 需要被代理的类
public class Cat {
	public String eat() {
		System.out.println("cat eat");
		return "cat eat finish";
	}
}
// 方法拦截器
public class CatInterceptor implements MethodInterceptor{
	@Override
	public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) 
										throws Throwable {
		System.out.println("before cat eat");
		Object result = proxy.invokeSuper(obj, args);
		System.out.println("after cat eat");
		return result;
	}
}

public class Test {
	public static void main(String ... args){
		Enhancer enhancer = new Enhancer();
		// cglib通过继承实现的代理, 这里就将目标类Cat设置为代理类的父类
		enhancer.setSuperclass(Cat.class);
		// 设置方法拦截器
		enhancer.setCallback(new CatInterceptor());
		// 创建代理类, 这里的cat实际是cat的子类
		Cat cat = (Cat)enhancer.create();
		System.out.println(cat.eat());
	}	
} // 依次输出 before cat eat - cat eat - after cat eat - cat eat finish

    上面代码需要正常运行需要引入CGLIB相关依赖可以到maven仓库搜索, 或者在spring项目下运行, spring项目带有CGLIB相关依赖.
    CGLIB实际通过继承目标类, 子类重写了所有非final修饰的方法, enhancer.setCallback(new CatInterceptor())设置了回调, 通过方法拦截的方式调用了目标对象. 这样说起来有点拗口, 所以直接看Cat cat = (Cat)enhancer.create();这个cat的字节码反编译生成的eat()方法的代码吧. 笔者用的JD-GUI进行的反编译. CGLIB生成的动态代理的class文件如何保持, 只需要在上述Test.main方法第一句设置一个系统变量即可System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "这个参数为需要保存的路径如D://test);最后会保存到你指定目下的和你类统计的包下.
cglib01
    打开反编译工具查eat方法的代码

public class Cat$$EnhancerByCGLIB$$e693ea13 extends Cat implements Factory {  

	static {
    CGLIB$STATICHOOK1();
  	}
	// 这里的静态代码块后后面的FastClass有关, 已部分删除 123321
   static void CGLIB$STATICHOOK1() {
	    CGLIB$THREAD_CALLBACKS = new ThreadLocal();
	    CGLIB$emptyArgs = new Object[0];
	    Class clazz1 = Class.forName("com.jinxin.beoneWeb3d.utils.Cat$$EnhancerByCGLIB$$e693ea13");
	    Class clazz2;
	    CGLIB$eat$0$Proxy = MethodProxy.create(clazz2, clazz1, "()Ljava/lang/String;", "eat", "CGLIB$eat$0");
	    ReflectUtils.findMethods(new String[] { "eat", "()Ljava/lang/String;" }, 
	    		(clazz2 = Class.forName("com.jinxin.beoneWeb3d.utils.Cat")).getDeclaredMethods());
  }
	// 直接调用父类的eat方法
  final String CGLIB$eat$0() {
    return super.eat();
  }
  public final String eat() {
  	// 首先判断是否有CALLBACK. 上面的代码示例我们enhancer.setCallback(new CatInterceptor());
  	
    if (this.CGLIB$CALLBACK_0 == null)
      CGLIB$BIND_CALLBACKS(this); 
    // 当CALLBACK不为空的时候就会调用它实现的MethodInterceptor的intercept方法, 为空就直接调用父类的eat方法
    return (this.CGLIB$CALLBACK_0 != null) ? (String)this.CGLIB$CALLBACK_0.intercept(this, CGLIB$eat$0$Method, CGLIB$emptyArgs, CGLIB$eat$0$Proxy) : super.eat();
  }
  public final String toString() {
    if (this.CGLIB$CALLBACK_0 == null)
      CGLIB$BIND_CALLBACKS(this); 
    return (this.CGLIB$CALLBACK_0 != null) ? (String)this.CGLIB$CALLBACK_0.intercept(this, CGLIB$toString$2$Method, CGLIB$emptyArgs, CGLIB$toString$2$Proxy) : super.toString();
  }
}

删除了无关紧要的代码

  1. 首先可以看到, 这个代理如上文所述是通过继承实现了代理, 这代理类实现了目标类Cat.
  2. 确实重写了所有目标类的方法, 笔者删除了hashCode等只留下了toString作为示例.
  3. 重点看看这个public final String eat()这个方法是目标类的方法.相关解释写在代码块的注释上.
  4. 上文的截图红圈标注的有三个class文件, 直觉思考,我们需要生成一个代理类,只需要一个class文件, 为什么会有3个呢? 这就涉及到CGLIG动态代理采用的FastClass机制.

    接下来讲讲FastClass机制,简单来说FastClass机制就是为每个方法建立索引, 通过索引直接调用方法 , 然后回答上图中的三个class文件分别是什么:

  • .Cat$$EnhancerByCGLIB$$e693ea13$$FastClassByCGLIB$$f450950f这个是代理的类的FastClass类, 下面贴出部分精简过后的代码加以说明.
// 首先这个类继承自FastCast
public class Cat$$EnhancerByCGLIB$$e693ea13$$FastClassByCGLIB$$f450950f extends FastClass {
  public Cat$$EnhancerByCGLIB$$e693ea13$$FastClassByCGLIB$$f450950f(Class paramClass) {
    super(paramClass);
  }
  // 这个invoke方法就是通过索引调用代理类的eat方法 可以看到索引为8
  // 在示例代码中CatInterceptor 类中proxy.invokeSuper(obj, args)实际上内部就是调用这个FastClass类的invoke方法
  // 通过传入的paramInt = 19从而调用了代理类的CGLIB$eat$0()方法
  // 和直觉思维有点不一样, invokeSuper居然不是直接调用的Cat类的eat方法
  // 真正调用父类的eat方法在CGLIB$eat$0()的内部super.eat()
  // 这些索引怎么来的, 就是上面代理类的代码块里面注释说标注的 ctrl + f搜123321就能看见了. 
  public Object invoke(int paramInt, Object paramObject, Object[] paramArrayOfObject) throws 
      switch (paramInt) {
        case 0:
          return new Boolean(((Cat$$EnhancerByCGLIB$$e693ea13)paramObject).equals(paramArrayOfObject[0]));
        case 1:
          return ((Cat$$EnhancerByCGLIB$$e693ea13)paramObject).toString();
        case 8:
          return ((Cat$$EnhancerByCGLIB$$e693ea13)paramObject).eat();
        case 19:
          return ((Cat$$EnhancerByCGLIB$$e693ea13)paramObject).CGLIB$eat$0();
      } 
  }
  
}
  • Cat$$FastClassByCGLIB$$ac82d240这是目标类的FastClass类, 代码和上面的代码类似就不贴出来了.
  • Cat$$EnhancerByCGLIB$$e693ea13这个是代理类, 上文已经贴过代码了.

3.0 总结

  1. JDK动态代理和CGLIB动态代理相关比较总结:
    1. JDK动态代理只能基于接口进行代理, 而CGLIB通过继承的方式则无此限制. 但是CGLIB无法代理final修饰的类
    2. JDK动态代理最后调用目标方法是通过反射调用的, 而CGLIB是基于FastClass机制直接调用目标对象的方法
    3. JDK动态代理是JDK自带的, CGLIB是需要引入第三方依赖的.
    4. 低版本JDK的动态代理因为反射执行效率的问题, 所以执行速度是不如CGLIB的, 但高版本JDK, 如1.8优化了反射调用的效率.
    5. CGLIB使用动态生成字节码, 所以在创建生成的速度慢.
  2. 各自的适用场景
    1. CGLIB比较适合单例模式对象的创建, 因为创建速度慢.
    2. 如果要多次创建对象则用JDK动态代理, 并且CGLIB的不甚使用还内存泄漏问题.
  3. spring 中的动态代理
    1. spring5.x 以后默认使用的是JDK动态代理, 当需要代理的类没有实现相关接口的时候采用CGLIB代理.
    2. springboot 2.x 则有些不同,springboot 2.x 通过spring.aop.proxy-target-class属性修改了spring5.x 默认使用的JDK动态代理. 如果表示怀疑可以去springboot相关包搜索org.springframework.boot.autoconfigure.aop.AopAutoConfiguration这个类, 顺便提一句可以通过配置文件spring.aop.proxy-target-class此属性修改动态代理的方式.false为jdk,true为CGLIB.这里面关于@EnableAspectJAutoProxy @EnableTransactionManagement还有些奇怪的问题, 读者可以自行尝试.
    3. 全面采用CGLIB动态代理其实可能会带来一些问题, 比如笔者习惯用@autowire的类型为接口类型而不是目标类的类型, 可能就无法注入.
  • 11
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值