【Java】Java 动态代理(Dynamic Proxy)

Posted by 西维蜀黍 on 2019-03-28, Last Modified on 2021-09-21

代理

我们大家都知道微商代理,简单地说就是代替厂家卖商品,厂家“委托”代理为其销售商品。

关于微商代理,首先我们从他们那里买东西时通常不知道背后的厂家究竟是谁,也就是说,“委托者”对我们来说是不可见的;其次,微商代理主要以朋友圈的人为目标客户,这就相当于为厂家做了一次对客户群体的“过滤”。

我们把微商代理和厂家进一步抽象,前者可抽象为代理类,后者可抽象为委托类(被代理类)。通过使用代理,通常有两个优点,并且能够分别与我们提到的微商代理的两个特点对应起来:

优点一:可以隐藏委托类的实现;

优点二:可以实现客户与委托类间的解耦,在不修改委托类代码的情况下能够做一些额外的处理。

静态代理(Static Proxy)

若代理类在程序运行前就已经存在,那么这种代理方式被成为静态代理(Static Proxy) ,这种情况下的代理类通常都是我们在Java代码中定义的。

通常情况下, 静态代理中的代理类和委托类会实现同一接口或是派生自相同的父类。

代理模式

静态代理对应设计模式中的代理模式。

定义:给某个对象提供一个代理对象,并由代理对象控制对于原对象的访问,即客户不直接操控原对象,而是通过代理对象间接地操控原对象。

生活中的代理模式

要理解代理模式很简单,其实生活当中就存在代理模式:

我们购买火车票可以去火车站买,但是也可以去火车票代售处买,此处的火车票代售处就是火车站购票的代理,即我们在代售点发出买票请求,代售点会把请求发给火车站,火车站把购买成功响应发给代售点,代售点再告诉你。

例子

下面我们用Vendor类代表生产厂家,BusinessAgent类代表微商代理,来介绍下静态代理的简单实现,委托类和代理类都实现了Sell接口。

Sell接口的定义如下:

/**
 * 委托类和代理类都实现了Sell接口
 */
public interface Sell { 
    void sell(); 
    void ad(); 
} 

Vendor类的定义如下:

/**
 * 生产厂家
 */
public class Vendor implements Sell { 
    public void sell() { 
        System.out.println("In sell method"); 
    } 
    
    public void ad() { 
        System.out.println("ad method");
    }
} 

代理类BusinessAgent的定义如下:

/**
 * 代理类
 */
public class BusinessAgent implements Sell {
    private Sell vendor;
    
    public BusinessAgent(Sell vendor){
        this.vendor = vendor;
    }

    public void sell() { 
        vendor.sell();
    } 
    
    public void ad() {
        vendor.ad();
    }
} 

从BusinessAgent类的定义我们可以了解到,静态代理可以通过聚合来实现,让代理类持有一个委托类的引用即可。

下面我们考虑一下这个需求:给Vendor类增加一个过滤功能,只卖货给大学生。通过静态代理,我们无需修改Vendor类的代码就可以实现,只需在BusinessAgent类中的sell方法中添加一个判断即可如下所示:

/**
 * 代理类
 */
public class BusinessAgent(){ implements Sell {
    private Sell vendor;
    
    public BusinessAgent(Sell vendor){
        this.vendor = vendor;
    }

    public void sell() {
        if (isCollegeStudent()) {
            vendor.sell();
        }
    } 
    
    public void ad() {
        vendor.ad();
    }
} 

这对应着我们上面提到的使用代理的第二个优点:可以实现客户与委托类间的解耦,在不修改委托类代码的情况下能够做一些额外的处理。静态代理的局限在于运行前必须编写好代理类,

下面我们来介绍下运行时生成代理类的动态代理方式。

动态代理(Dynamic Proxy)

什么是动态代理

代理类在程序运行时创建的代理方式被成为动态代理(Dynamic Proxy)

也就是说,这种情况下,代理类并不是在Java代码中定义的,而是在运行时根据我们在Java代码中的“指示”动态生成的。

相比于静态代理, 动态代理的优势在于可以很方便的对代理类的函数进行统一的处理,而不用修改每个代理类的函数。

动态代理在Java中有着广泛的应用,比如Spring AOP,Hibernate数据查询、测试框架的后端mock、RPC,Java注解对象获取等。静态代理的代理关系在编译时就确定了,而动态代理的代理关系是在运行时确定的。静态代理实现简单,适合于代理类较少且确定的情况,而动态代理则给我们提供了更大的灵活性。

动态代理的实现方式

Java中,实现动态代理有两种方式:

**JDK动态代理:**java.lang.reflect 包中的Proxy类和InvocationHandler接口提供了生成动态代理类的能力。

**Cglib动态代理:**Cglib (Code Generation Library)是一个第三方代码生成类库,运行时在内存中动态生成一个子类对象从而实现对目标对象功能的扩展。

例子

下面我们结合一个实例来介绍一下动态代理的这个优势是怎么体现的。

现在,假设我们要实现这样一个需求:我们希望在执行委托类中的方法之前先输出“before”,在执行完毕后输出“after”。

我们还是以上面例子中的Vendor类作为委托类,BusinessAgent类作为代理类来进行介绍。

使用静态代理实现

首先我们来使用静态代理来实现这一需求,代理类的实现代码如下:

public class BusinessAgent implements Sell {
    private Vendor mVendor; 
 
    public BusinessAgent(Vendor vendor) {
        this.mVendor = vendor; 
    } 
 
    public void sell() {
        System.out.println("before"); 
        mVendor.sell(); 
        System.out.println("after"); 
    } 
 
    public void ad() {
        System.out.println("before"); 
        mVendor.ad(); 
        System.out.println("after"); 
    }
} 

通过静态代理实现我们的需求,需要我们在每个方法中都添加相应的打印逻辑(System.out.println("before"); System.out.println("after"); )。

在BusinessAgent类中,由于只存在sell() 和 ad()两个方法,所以工作量还不算大。

但事实上,这样的实现并不是特别优美。

而且,假如Sell接口中包含上百个方法呢?这时候使用静态代理,就不得不插入大量冗余代码。

通过使用动态代理,我们可以做一个“统一指示”,从而对所有代理类的方法进行统一处理,而不用逐一修改每个方法。

下面我们来介绍下如何使用动态代理方式实现我们的需求。

使用JDK动态代理实现

InvocationHandler接口

在使用动态代理时,我们需要定义一个位于代理类与委托类之间的中介类,这个中介类需要实现InvocationHandler接口。

在Java中,InvocationHandler接口的定义如下:

/**
 * 调用处理程序
 */
public interface InvocationHandler { 
    Object invoke(Object proxy, Method method, Object[] args); 
} 

因此,我们的中介类要实现InvocationHandler接口,则需要实现invoke方法。

实现InvocationHandler接口目的:当我们调用代理类对象的方法时,这个“调用”会转送到invoke方法中,代理类对象作为proxy参数传入,参数method标识了我们具体调用的是代理类的哪个方法,args为这个方法的参数。

这样一来,我们对代理类中的所有方法的调用都会变为对invoke的调用,这样我们可以在invoke方法中添加统一的处理逻辑(也可以根据method参数对不同的代理类方法做不同的处理)。

因此,我们只需在中介类的invoke方法实现中输出“before”,然后调用委托类的invoke方法,再输出“after”。下面我们来一步一步具体实现它。

委托类的定义

动态代理方式下,要求委托类必须实现某个接口,这里我们实现的是Sell接口。委托类Vendor类的定义如下:

public class Vendor implements Sell { 
    public void sell() { 
        System.out.println("In sell method"); 
    }

    public void ad() {
        System.out.println("ad method");
    }
} 

中介类

上面我们提到过,中介类必须实现InvocationHandler接口,作为调用处理器”拦截“对代理类方法的调用。

中介类的定义如下:

public class DynamicProxy implements InvocationHandler { 
    //obj为委托类对象; 
    private Object obj; 
 
    public DynamicProxy(Object obj) {
        this.obj = obj;
    } 
 
    @Override 
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 
      	// 插入的逻辑
        System.out.println("before"); 
      
        Object result = method.invoke(obj, args); 
        // 插入的逻辑    
        System.out.println("after"); 
        return result; 
    }
} 

从以上代码中我们可以看到,中介类持有一个委托类对象引用,在invoke方法中调用了委托类对象的相应方法,看到这里是不是觉得似曾相识?

通过聚合方式持有委托类对象引用,把外部对invoke的调用最终都转为对委托类对象的调用。这不就是我们上面介绍的静态代理的一种实现方式吗?

实际上,

  • 中介类与委托类构成了静态代理关系,在这个关系中,中介类是代理类,委托类就是委托类;
  • 代理类与中介类也构成一个静态代理关系,在这个关系中,中介类是委托类,代理类是代理类。

也就是说,动态代理关系由两组静态代理关系组成,这就是动态代理的原理

下面我们来介绍一下如何”指示“以动态生成代理类。

动态生成代理类

动态生成代理类的相关代码如下:

public class Main { 
    public static void main(String[] args) {
        //创建中介类实例 
        DynamicProxy inter = new DynamicProxy(new Vendor()); 
        //加上这句将会产生一个$Proxy0.class文件,这个文件即为动态生成的代理类文件
        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true"); 

        //动态生成代理类实例 
        Sell sell = (Sell)(Proxy.newProxyInstance(Sell.class.getClassLoader(), new Class[] {Sell.class}, inter)); 
 
        //通过代理类对象调用代理类方法,实际上会转到invoke方法调用 
        sell.sell(); 
        sell.ad(); 
    }
} 

在以上代码中,我们调用Proxy类的newProxyInstance方法来获取一个代理类实例。这个代理类实现了我们指定的接口并且会把方法调用分发到指定的调用处理器。

Cglib动态代理

前面曾经提到过,实现动态代理有两种方式:JDK动态代理Cglib动态代理(Cglib (Code Generation Library )。

使用 JDK 生成的动态代理的前提是目标类必须有实现的接口。但这里又引入一个问题,如果某个类没有实现任何接口,就不能使用 JDK 动态代理。所以 CGLIB 代理就是解决这个问题的。

  • Cglib是一个强大的高性能的代码生成包,它可以在运行期扩展Java类与实现java接口。它广泛的被许多AOP的框架使用,例如Spring AOP和synaop,为他们提供方法的interception(拦截)
  • Cglib包的底层是通过使用字节码处理框架ASM来转换字节码并生成新的子类。

CGLIB 是以动态生成的子类继承目标的方式实现,在运行期动态的在内存中构建一个子类,如下:

public class UserDao{} // CGLIB 是以动态生成的子类继承目标的方式实现,程序执行时,隐藏了下面的过程public class $Cglib_Proxy_class  extends UserDao{}

CGLIB 使用的前提是目标类不能被 final 修饰。因为 final 修饰的类不能被继承。

通过Cglib实现动态代理

委托类 - Vendor

还是回到上面的例子,我们假设委托类Vendor类并没有实现任何接口,定义如下:

public class Vendor { 
    public void sell() { 
        System.out.println("In sell method"); 
    }

    public void ad() {
        System.out.println("ad method");
    }
} 

Cglib代理工厂 - ProxyFactory

/**
 * Cglib子类代理工厂
* 对UserDao在内存中动态构建一个子类对象
*/
public class ProxyFactory implements MethodInterceptor{
     //维护目标对象
    private Object target;

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

   //给目标对象创建一个代理对象
    public Object getProxyInstance(){
        //1.工具类
        Enhancer en = new Enhancer();
       //2.设置父类
        en.setSuperclass(target.getClass());
        //3.设置回调函数
        en.setCallback(this);
        //4.创建子类(代理对象)
        return en.create();
    }

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        // 插入的逻辑
        System.out.println("before"); 
      
        Object result = method.invoke(obj, args);  
        //执行目标对象的方法
        Object returnValue = method.invoke(target, args);
        
      	// 插入的逻辑    
        System.out.println("after"); 
      
        return returnValue;
    }
}

测试类

public class App {

    @Test
    public void test(){
        //目标对象
        Vendor vendor = new Vendor();

        //代理对象
        UserDao proxy = (Vendor)new ProxyFactory(vendor).getProxyInstance();

        //执行代理对象的方法
        proxy.sell();
    }
}

Spring中的AOP(Aspect Oriented Programming)

实际上,在Spring框架中,就是通过动态代理实现**面向切面编程(Aspect Oriented Programming,AOP)**的。

将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,可以让被代理的对象专注于完成自己的本职工作(业务逻辑),而代理对象专注于进行业务逻辑执行前后的日志记录,安全控制等附加的功能。

就像插了两个刀到这个被代理的对象前后。所以形象的叫做面向切面编程。

通过对非业务逻辑行为的分离,我们就可以将它们独立到特定的方法中,进而,当需要改变这些行为的时候,不影响业务逻辑的代码,并实现两者的解耦。

在Spring的AOP编程中:

  • 如果加入容器的目标对象有实现接口,用JDK代理;
  • 如果目标对象没有实现接口,用Cglib代理。

Reference