设计模式 - 代理模式

276

实习期间,用到了 Spring AOP ,对以前的 AOP 理解地也更为深刻些,正好借这个机会总结整理一下 静态代理与动态代理,以及 Spring AOP 的动态代理实战

一、思想

在我们所做的很多事情,是不想要自己做的,如此,我们就可以把一些事情让其他人来做

  • 明星有经纪人可以帮他处理各种杂事,明星专注于自己的演唱事业即可,不用自己和第三方打交道了
  • 房产中介可以帮助我们找出合适的房子,我们专注于挑选房子即可,不用自己走路到处尝试了

所以,在这两个场景中,经纪人是一个代理,而房产中介也是一个代理

回到 Wiki 中,对代理模式的比较官方的定义

Use of the proxy can simply be forwarding to the real object, or can provide additional logic. In the proxy, extra functionality can be provided, for example caching when operations on the real object are resource intensive, or checking preconditions before operations on the real object are invoked. For the client, usage of a proxy object is similar to using the real object, because both implement the same interface.

👉意思就是,使用代理的时候,是可以很轻易地将请求转发给真正的处理对象去处理,或者代理对象可以在这个原基础上添加额外的逻辑功能。在代理模式中,可以提供额外的功能,比如,某对象的操作是资源密集型的,也就是频繁地使用该对象,那么可以对该对象缓存;或者调用对象前需要进行一些前置操作,此时你使用缓存或者代理对象都比原对象好,因为无论是代理对象或者原对象,他们都是同一接口的实现,或者说功能目的上,他们是一致的

二、实现

当然,简单地分类,代理还分静态代理和动态代理两种,静态代理就是自己要去创建代理对象,而动态代理就是通过一些声明式的调用,代理对象由第三方帮我们生成,我们只需专注于自己的逻辑开发即可

2.1 静态代理

拿租房子的例子来看,静态代理的实现如下:

提供租房服务的房子接口

public interface House {
    // 房子接口提供租房服务
    void renting();
}

想要租房的房客

public class Consumer implements House {
    // 房客实现接口,要租房子
    @Override
    public void renting() {
        System.out.println("我是房客,我要租房子!");
    }
}

帮房客租房的租房中介

public class HouseProxy implements House {
    
    private Consumer consumer;
    
    public HouseProxy(Consumer consumer) {
        this.consumer = consumer;
    }
    
    @Override
    public void renting() {
        System.out.println("我是租房中介,我带你看房子");
        consumer.renting();
        System.out.println("我是租房中介,房子租完了,给我中介费")
    }
}

测试类

class Main {
    public static void main(String[] args) {
        // 创建一个房子中介
        House proxy = new HouseProxy(new Consumer());
        // 让中介帮忙租房
        proxy.renting();
    }
}

HouseProxy 使用 Consumer 来代理 Consumer 想做的事情,他们都是 House 的实现,能够实现 House 相要的功能

2.2 动态代理

  • 静态代理:一个接口实现类必须得写一个该接口实现类对应的代理类才能做相应的逻辑
  • 动态代理:不用写接口实现类的代理类,但需要实现一个调用处理器,通过指定一个想要代理的实现类给调用处理器,让调用处理器来处理其实现方法,也就是说一个调用处理器可以完成所有同一接口实现类的代理!

2.2.1 JDK 方式实现

调用处理器,它是一个实现了 JDK 的调用接口处理器 InvocationHandler 的类,实现这个接口的任意代理类,都可以代理它想做的逻辑

实现卖房子的调用处理器,调用处理器在这里其实就是实现了上边的中介的作用

public class HouseHandler implements InvocationHandler {

    private Object invokeTarget;

    public HouseHandler(Object target) {
        this.invokeTarget = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("我是租房中介,我带你看房子");
        Object oj = method.invoke(invokeTarget, args);
        System.out.println("我是租房中介,房子租完了,给我中介费");
        return oj;
    }

}

在测试类将要代理的目标实现类和调用处理器传递给一个代理对象,让 JDK 通过反射动态生成,完成代理

public class Main {
	public static void main(String[] args) {
        // 要代理的目标类
        House consumer = new Consumer();
        
        // 该接口(House)对应的调用处理器,实例化一个 consumer handler
        HouseHandler houseHandler = new HouseHandler(consumer);
        
        // 让 JDK 动态生成一个代理类,去执行 houseHandler 的方法
        House houseProxy = (House) Proxy.newProxyInstance(
            consumer.getClass().getClassLoader(),
            consumer.getClass().getInterfaces(),
            houseHandler);
        
        houseProxy.renting();
    }
}

关系图如下

  • HouseProxy 依赖于 HouseHandler
  • HouseHandler 依赖于 Consumer
  • Consumer 实现了 House

拦截调用流程:

  • houseProxy 调用 consumer 的同名方法 renting() 时,会被 houseHandler 拦截
  • 调用 houseHandler 的 invoke() 方法
  • 从而完成代理调用

有了这个 HouseHandler 不管后面出现什么类型的 House 实现类,直接委托给这个 handler 即可实现对应的代理,而它生成的代理类都是 JDK 动态临时生成的!

2.2.2 CGLIB 方式实现

引入 CGLIB 包,实现 CGLIB 方法拦截器

public class Cglib implements MethodInterceptor {
    public Object intercept(
        Object o,
        Method method,
        Object[] objects,
        MethodProxy methodProxy) throws Throwable {
        
        System.out.println("我是租房中介,我带你看房子");
        Object invokeSuper = methodProxy.invokeSuper(o, objects);
        System.out.println("我是租房中介,房子租完了,给我中介费");
        return invokeSuper;
        
    }
}

测试类

public class Main {
    public static void main(String[] args) {
        
        // 实例化运行时代码增强和 CGLIB 组件
        Cglib cglib = new Cglib();
        Enhancer enhancer = new Enhancer();
        
        // 设置要拦截处理的类
        enhancer.setSuperclass(Consumer.class);
        enhancer.setCallback(cglib);
        
        // 动态代理生成代理类 
        House houseProxy = (House) enhancer.create();
        houseProxy.renting();
        
    }
}

2.2.3 比较

过程比较:

  • JDK 反射生成的动态代理,在生成的代理类过程比较高效
  • CGLIB 借助 ASM 的动态代理,在代理类执行的过程比较高效

限制比较:

  • JDK 动态代理目标类必须基于同一接口的实现
  • CGLIB 动态代理没这个限制,因为是基于字节码控制的原理

三、作用

3.1 延迟加载

客户端根据用户请求去数据库查询数据,查询之前,要进行数据库连接,这个过程可能涉及到了很多 XML 解析、实例化其他 SQL 连接类的各种操作,是比较耗时的

而如果用户打开客户端,客户端就进行各种初始化,那么整个过程下来会很卡顿,体验很不好,比如一些功能的初始化是暂时用不到的,比如数据库的连接查询的各种组件,可能用户只是浏览上一次缓存的数据,这一次不用数据库连接,那么每次启动就连接是会很耗时的

所以,此时就可以通过代理模式来延迟加载各种暂时用不到的功能组件,因为代理类是比较轻量级的,所以每次初始化代理类即可,如果碰到了想用的功能,就初始化对应的功能组件

实现延迟加载数据库的连接操作,如下

3.1.1 静态代理 - 实现

定义一个数据库查询接口

public interface IDBQuery {
 String request();
}

定义一个数据库查询组件

public class DBQuery implements IDBQuery {
    public DBQuery() {
        try {
            // 假设数据库连接等耗时操作
            Thread.sleep(1000);
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
    }

    @Override
    public String request() {
        return "request string";
    }

}

定义一个数据库查询的代理组件

public class DBQueryProxy implements IDBQuery {
    
    private DBQuery real = null;

    @Override
    public String request() {
        // 在真正需要的时候才能创建真实对象,创建过程可能很慢
        if (real == null) {
            real = new DBQuery();
        }
        return real.request();
    }

}

客户端调用

public class Main {
    public static void main(String[] args) {
        // 使用代里对象
        IDBQuery q = new DBQueryProxy(); 
        // 在真正需要使用时才创建真实对象
        q.request(); 
    }
}

3.1.2 动态代理 - 实现

基于 JDK 实现处理方法调用 Handler 的方式实现

定义调用方法的 Handler

public class DBQueryHandler implements InvocationHandler {

    /**
     * 定义接口实现类的要代理调用的目标对象
     */
    private IDBQuery realQuery = null;

    /**
     * 代理对象拦截真实对象的方法后,要调用的代理方法
     *
     * @param proxy
     * @param method
     * @param args
     * @return
     * @throws Throwable
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 如果第一次调用,则初始化
        if (realQuery == null) {
            realQuery = new DBQuery();
        }
        // 返回目标对象的真实操作
        return realQuery.query();
    }


    /**
     * 创建代理对象,代理对象调用方法的时候,会去调用目标对象实例的 invoke()
     *
     * @return 返回一个代理对象
     */
    public static IDBQuery createProxy() {
        IDBQuery proxy = (IDBQuery) Proxy.newProxyInstance(
                ClassLoader.getSystemClassLoader(),
                new Class[]{IDBQuery.class},
                new DBQueryHandler());
        return proxy;
    }
}

客户端调用

public class Client {
    public static void main(String[] args) {
        // 创建一个代理对象
        IDBQuery idbQuery = DBQueryHandler.createProxy();
        // 需要的时候就才实例化目标对象,并调用
        idbQuery.query();
    }
}

但这样写其实局限性挺大的,因为是在代理类中直接指定要实例化的目标类,这样的耦合度太高了,如果再来个别的目标类想延迟加载,又得改代码了

更好的实现方式,考虑暴漏一个接口,传入 Class 对象,然后实例化对应的 Class 的实例,再让调用处理器去执行对应的 Classs 的延迟加载的逻辑即可

3.2 代理增强

比如现在有个需求需要打印调用方法的入参和出参,就可以用代理来实现了,事实上,现在的 AOP 技术,拦截实现打印日志参数之类的,也是基于代理模式的,如下示例

代理调用处理器

public class DBQueryHandler implements InvocationHandler {
    
    public static final Logger LOGGER = LoggerFactory.getLogger(InvocationHandler.class);
    
    // 要代理的目标对象
    private Object targetInvocationObj;

    public DBQueryHandler (Object targetInvocationObj) {
        this.targetInvocationObj = targetInvocationObj;
    }


    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 调用目标对象的方法, 并返回方法返回值
        LOGGER.info("DBQueryHandler 代理 invoke 的方法入参");
        Object result = method.invoke(targetInvocationObj,args);
        LOGGER.info("DBQueryHandler 代理 invoke 的方法出参");
        return 
    }

}

客户端调用

public class Client {
    public static void main(String[] args) {
        // 创建要调用的目标实例
        IDBQuery idbQuery = new DBQuery();
        // 让调用处理器代理调用要调用的目标实例
        DBQueryHandler dbQueryHandler = new DBQueryHandler(idbQuery);
        // 动态生成代理类
        IDBQuery idbQueryProxy = (IDBQuery) Proxy.newProxyInstance(
                idbQuery.getClass().getClassLoader(),
                idbQuery.getClass().getInterfaces(),
                dbQueryHandler);

        idbQueryProxy.query();
    }
}

如此边完成了一个代理,在原基础上增强了可以打印方法出参和入参的功能

四、参考