0

Java基础之代理模式

 2 years ago
source link: https://segmentfault.com/a/1190000040632302
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

代理模式是常见的设计模式之一,意图在为指定对象提供一种代理以控制对这个对象的访问。Java中的代理分为动态代理和静态代理,动态代理在Java中的应用比较广泛,比如Spring的AOP实现、远程RPC调用等。静态代理和动态代理的最大区别就是代理类是JVM启动之前还是之后生成。本文会介绍Java的静态代理和动态代理,以及二者之间的对比,重点是介绍动态代理的原理及其实现。

代理模式的定义:为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。比如说:要访问的对象在远程的机器上。在面向对象系统中,有些对象由于某些原因(比如对象创建开销很大,或者某些操作需要安全控制,或者需要进程外的访问),直接访问会给使用者或者系统结构带来很多麻烦,我们可以在访问此对象时加上一个对此对象的访问层。

代理示例图

代理的组成

代理由以下三部分角色组成:

  • 抽象角色:通过接口或抽象类声明真实角色实现的业务方法。
  • 代理角色:实现抽象角色,是真实角色的代理,通过真实角色的业务逻辑方法来实现抽象方法,并可以附加自己的操作。
  • 真实角色:实现抽象角色,定义真实角色所要实现的业务逻辑,供代理角色调用。

代理的优点

  1. 职责清晰:真实的角色就是实现实际业务的逻辑,不用关系非业务的逻辑(如事务管理)。
  2. 隔离作用:代理对象可以在客户端和目标对象之间起到中介作用,目标对象不直接暴露给客户端,从而实现隔离目标对象的作用
  3. 高可扩展性:代理对象可以对目标对象进行灵活的扩展。

代理的例子

我们用一个加载并显示图片的例子来解释代理的工作原理,图片存在磁盘上,每次IO会花费比较多的事件,如果我们需要频繁的显示图片,每次都从磁盘读取会花费比较长的时间。我们通过一个代理来缓存图片,只有第一次读取图片的时候才从磁盘读取,之后都从缓存中读取,源码示例如下:

import java.util.*;
 
interface Image {
    public void displayImage();
}

//on System A 
class RealImage implements Image {
    private String filename;
    public RealImage(String filename) { 
        this.filename = filename;
        loadImageFromDisk();
    }

    private void loadImageFromDisk() {
        System.out.println("Loading   " + filename);
    }

    public void displayImage() { 
        System.out.println("Displaying " + filename); 
    }
}

//on System B 
class ProxyImage implements Image {
    private String filename;
    private Image image;
 
    public ProxyImage(String filename) { 
        this.filename = filename; 
    }
    public void displayImage() {
        if(image == null)
              image = new RealImage(filename);
        image.displayImage();
    }
}
 
class ProxyExample {
    public static void main(String[] args) {
        Image image1 = new ProxyImage("HiRes_10MB_Photo1");
        Image image2 = new ProxyImage("HiRes_10MB_Photo2");     
        
        image1.displayImage(); // loading necessary
        image2.displayImage(); // loading necessary
    }
}

静态代理需要在程序中定义两个类:目标对象类和代理对象类,为了保证二者行为的一致性,目标对象和代理对象实现了相同的接口。代理类的信息在程序运行之前就已经确定,代理对象中会包含目标对象的引用。

举例说明静态代理的使用: 假设我们有一个接口方法用于计算员工工资,有一个实现类实现了具体的逻辑,如果我们需要给计算员工工资的逻辑添加日志应该怎么办呢?直接在计算工资的实现逻辑里面添加会导致引入非业务逻辑,不符合规范。这个时候我们就可以引入一个日志代理,在计算工资前后输出相关的日志信息。

  • 计算员工工资的接口定义如下:
public interface Employee {
    double calculateSalary(int id);
}
  • 计算员工工资的实现类如下:
public class EmployeeImpl {
    public double calculateSalary(int id){
        return 100;
    }
}
  • 带有日志的代理类的实现如下:
public class EmployeeLogProxy implements Employee {

    //代理类需要包含一个目标类的对象引用
    private EmployeeImpl employee;

    //并提供一个带参的构造方法用于指定代理哪个对象
    public EmployeeProxyImpl(EmployeeImpl employee){
        this.employee = employee;
    }

    public double calculateSalary(int id) {

        //在调用目标类的calculateSalary方法之前记录日志
        System.out.println("当前正在计算员工: " + id + "的税后工资");
        double salary = employee.calculateSalary(id);
        System.out.println("计算员工: " + id + "的税后工资结束");
        // 在调用目标类方法之后记录日志
        return salary;
    }
}

动态代理的代理对象类在程序运行时被创建,而静态代理对象类则是在程序编译期就确定好的,这是二者最大的不同之处。动态代理的优势再于不需要开发者手工写很多代理类,比如上面的例子中,如果再来一个Manager类计算工资的逻辑需要日志,那么我们就需要新建一个ManagerLogProxy来代理对象,如果需要代理的对象很多,那么需要写的代理类也会很多。

而使用动态代理则没有这种问题,一种类型的代理只需要写一次,就可以适用于所有的代理对象。比如上文中的EmployeeManager,二者只需要抽象一个计算薪资相关的接口,就可以使用同一套动态代理逻辑实现代理。

动态代理示例

下面我们使用上文中的EmployeeManager计算薪资的逻辑来展示动态代理的用法。

接口的抽象

我们知道EmployeeManager都有计算薪资的逻辑,而且需要对计算薪资的逻辑进行日志记录,所以我们需要抽象一个计算薪资的接口:

public interface SalaryCalculator {
    double calculateSalary(int id);
}

接口的实现

public class EmployeeSalaryCalculator implements SalaryCalculator{
    public double calculateSalary(int id){
        return 100;
    }
}
public class ManagerSalaryCalculator implements SalaryCalculator{
    public double calculateSalary(int id){
        return 1000000;
    }
}

创建动态代理的InvocationHandler

public class SalaryLogProxy implements InvocationHandler {
    private SalaryCalculator calculator;

    public SalaryLogProxy(SalaryCalculator calculator) {
        this.calculator = calculator;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("--------------begin-------------");
        Object invoke = method.invoke(subject, args);
        System.out.println("--------------end-------------");
        return invoke;
    }
}

创建代理对象

public class Main {

    public static void main(String[] args) {
        SalaryCalculator calculator = new ManagerSalaryCalculator();
        InvocationHandler calculatorProxy = new SalaryLogProxy(subject);
        SalaryCalculator proxyInstance = (SalaryCalculator) Proxy.newProxyInstance(calculatorProxy.getClass().getClassLoader(), subject.getClass().getInterfaces(), calculatorProxy);
        proxyInstance.calculateSalary(1);
    }

}

动态代理源码分析

动态代理的流程如下图所示,可以看到动态代理中包含以下内容:

  • 目标对象:我们需要代理的对象,对应上文中的new ManagerSalaryCalculator()
  • 接口:目标对象和代理对象需要共同提供的方法,对应上文中的SalaryCalculator
  • Proxy代理:用于生成代理对象类。
  • 代理对象类:通过代理和对应的参数得到的代理对象。
  • 类加载器:用于加载代理对象类的类加载器,对应上文中的calculatorProxy.getClass().getClassLoader()

动态代理流程

Proxy.newProxyInstance

动态代理的关键代码就是Proxy.newProxyInstance(classLoader, interfaces, handler).

  • 可以看到Proxy.newProxyInstance一共做了两件事情:1.获取代理对象类的构造函数,2:根据构造函数实例化代理对象。
@CallerSensitive
public static Object newProxyInstance(ClassLoader loader,
                Class<?>[] interfaces, InvocationHandler h) {
    Objects.requireNonNull(h);

    final Class<?> caller = System.getSecurityManager() == null
                                    ? null : Reflection.getCallerClass();

    /*
     * Look up or generate the designated proxy class and its constructor.
     */
    // 获取代理对象类的构造函数,里面就包含了代理对象类的构建和加载
    Constructor<?> cons = getProxyConstructor(caller, loader, interfaces);
 
    // 根据构造函数生成代理实例.
    return newProxyInstance(caller, cons, h);
}

代理对象类

通过查看源码,我们可以发现代理对象类都extend了Proxy类并实现了指定接口中的方法。由于java不能多继承,这里已经继承了Proxy类了,不能再继承其他的类。所以JDK的动态代理不支持对实现类的代理,只支持接口的代理。

我是御狐神,欢迎大家关注我的微信公众号

qrcode_for_gh_83670e17bbd7_344-2021-09-04-10-55-16

本文最先发布至微信公众号,版权所有,禁止转载!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK