代理模式

Posted 鲁班ds

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了代理模式相关的知识,希望对你有一定的参考价值。

代理模式,可以分为两种,一种是静态代理,一种是动态代理。

               两种代理从虚拟机加载类的角度来讲,本质上都是一样的,都是在原有类的行为基础上,加入一些多出的行为,甚至完全替换原有的行为。

               静态代理采用的方式就是我们手动的将这些行为换进去,然后让编译器帮我们编译,同时也就将字节码在原有类的基础上加入一些其他的东西或者替换原有的东西,产生一个新的与原有类接口相同却行为不同的类型。

               示例

               我们都知道,数据库连接是很珍贵的资源,频繁的开关数据库连接是非常浪费服务器的CPU资源以及内存的,所以我们一般都是使用数据库连接池来解决这一问题,即创造一堆等待被使用的连接,等到用的时候就从池里取一个,不用了再放回去,数据库连接在整个应用启动期间,几乎是不关闭的,除非是超过了最大闲置时间。

               但是在程序员编写程序的时候,会经常使用connection.close()这样的方法,去关闭数据库连接,而且这样做是对的,所以你并不能告诉程序员们说,你们使用连接都不要关了,去调用一个其他的类似归还给连接池的方法吧。这是不符合程序员的编程思维的,也很勉强,而且具有风险性,因为程序员会忘的。

               解决这一问题的办法就是使用代理模式,因为代理模式可以替代原有类的行为,所以我们要做的就是替换掉connection的close行为。

               下面是connection接口原有的样子,我去掉了很多方法,因为都类似,全贴上来占地方。

技术分享图片
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Wrapper;

public interface Connection  extends Wrapper {
    
    Statement createStatement() throws SQLException;
    
    void close() throws SQLException;
    
}
技术分享图片

              这里只贴了两个方法,但是我们代理的精髓只要两个方法就能掌握,下面使用静态代理,采用静态代理我们通常会使用组合的方式,为了保持对程序猿是透明的,我们实现Connection这个接口, 如下所示。

技术分享图片
import java.sql.SQLException;
import java.sql.Statement;


public class ConnectionProxy implements Connection{
    
    private Connection connection;
    
    public ConnectionProxy(Connection connection) {
        super();
        this.connection = connection;
    }

    public Statement createStatement() throws SQLException{
        return connection.createStatement();
    }
    
    public void close() throws SQLException{
        System.out.println("不真正关闭连接,归还给连接池");
    }

}
技术分享图片

                我们在构造方法中让调用者强行传入一个原有的连接,接下来我们将我们不关心的方法,交给真正的Connection接口去处理,就像createStatement方法一样,而我们将真正关心的close方法用我们自己希望的方式去进行。

                此处为了更形象,给出一个本人写的非常简单的连接池,意图在于表明实现的思路。下面我们来看一下连接池的变化,在里面注明了变化点。

技术分享图片
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.LinkedList;

public class DataSource {
    
    private static LinkedList<Connection> connectionList = new LinkedList<Connection>();
    
    static{
        try {
            Class.forName("com.mysql.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
    
    private static Connection createNewConnection() throws SQLException{
        return DriverManager.getConnection("url","username", "password");
    }
    
    private DataSource(){
        if (connectionList == null || connectionList.size() == 0) {
            for (int i = 0; i < 10; i++) {
                try {
                    connectionList.add(createNewConnection());
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    public Connection getConnection() throws Exception{
        if (connectionList.size() > 0) {
            //return connectionList.remove();  这是原有的方式,直接返回连接,这样可能会被程序员把连接给关闭掉
            //下面是使用代理的方式,程序员再调用close时,就会归还到连接池
            return new ConnectionProxy(connectionList.remove());
        }
        return null;
    }
    
    public void recoveryConnection(Connection connection){
        connectionList.add(connection);
    }
    
    public static DataSource getInstance(){
        return DataSourceInstance.dataSource;
    }
    
    private static class DataSourceInstance{
        
        private static DataSource dataSource = new DataSource();
        
    }
    
}
技术分享图片

               连接池我们把它做成单例,所以假设是上述连接池的话,我们代理中的close方法可以再具体化一点,就像下面这样,用归还给连接池的动作取代关闭连接的动作。

    public void close() throws SQLException{
        DataSource.getInstance().recoveryConnection(connection);
    }

               好了,这下我们的连接池返回的连接全是代理,就算程序员调用了close方法也只会归还给连接池了。

               我们使用代理模式解决了上述问题,从静态代理的使用上来看,我们一般是这么做的。

               1,代理类一般要持有一个被代理的对象的引用。

               2,对于我们不关心的方法,全部委托给被代理的对象处理。

               3,自己处理我们关心的方法。

               这种代理是死的,不会在运行时动态创建,因为我们相当于在编译期,也就是你按下CTRL+S的那一刻,就给被代理的对象生成了一个不可动态改变的代理类。

               静态代理对于这种,被代理的对象很固定,我们只需要去代理一个类或者若干固定的类,数量不是太多的时候,可以使用,而且其实效果比动态代理更好,因为动态代理就是在运行期间动态生成代理类,所以需要消耗的时间会更久一点。就像上述的情况,其实就比较适合使用静态代理。

               下面介绍下动态代理,动态代理是JDK自带的功能,它需要你去实现一个InvocationHandler接口,并且调用Proxy的静态方法去产生代理类。

               接下来我们依然使用上面的示例,但是这次该用动态代理处理,我们来试一下看如何做。

技术分享图片
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;


public class ConnectionProxy implements InvocationHandler{
    
    private Connection connection;
    
    public ConnectionProxy(Connection connection) {
        super();
        this.connection = connection;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //这里判断是Connection接口的close方法的话
        if (Connection.class.isAssignableFrom(proxy.getClass()) && method.getName().equals("close")) {
            //我们不执行真正的close方法
            //method.invoke(connection, args);
            //将连接归还连接池
            DataSource.getInstance().recoveryConnection(connection);
            return null;
        }else {
            return method.invoke(connection, args);
        }
    }
    
    public Connection getConnectionProxy(){
        return (Connection) Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{Connection.class}, this);
    }
    
}
技术分享图片

               上面是我们针对connection写的动态代理,InvocationHandler接口只有一个invoke方法需要实现,这个方法是用来在生成的代理类用回调使用的,关于动态代理的原理一会做详细的分析,这里我们先只关注用法。很显然,动态代理是将每个方法的具体执行过程交给了我们在invoke方法里处理。而具体的使用方法,我们只需要创造一个ConnectionProxy的实例,并且将调用getConnectionProxy方法的返回结果作为数据库连接池返回的连接就可以了。

               上述便是我们针对connection做动态代理的方式,但是我们从中得不到任何好处,除了能少写点代码以外,因为这个动态代理还是只能代理Connection这一个接口,如果我们写出这种动态代理的方式的话,说明我们应该使用静态代理处理这个问题,因为它代表我们其实只希望代理一个类就好。从重构的角度来说,其实更简单点,那就是在你发现你使用静态代理的时候,需要写一大堆重复代码的时候,就请改用动态代理试试吧。

               通常情况下,动态代理的使用是为了解决这样一种问题,就是我们需要代理一系列类的某一些方法,最典型的应用就是我们前段时间讨论过的springAOP,我们需要创造出一批代理类,切入到一系列类当中的某一些方法中。下面给出一个经常使用的动态代理方式。

技术分享图片
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;


public class DynamicProxy implements InvocationHandler{
    
    private Object source;
    
    public DynamicProxy(Object source) {
        super();
        this.source = source;
    }
    
    public void before(){
        System.out.println("在方法前做一些事,比如打开事务");
    }
    
    public void after(){
        System.out.println("在方法返回前做一些事,比如提交事务");
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //假设我们切入toString方法,其他其实也是类似的,一般我们这里大部分是针对特定的方法做事情的,通常不会对类的全部方法切入
        //比如我们常用的事务管理器,我们通常配置的就是对save,update,delete等方法才打开事务
        if (method.getName().equals("toString")) {
            before();
        }
        Object result = method.invoke(source, args);
        if (method.getName().equals("toString")) {
            after();
        }
        return result;
    }
    
    public Object getProxy(){
        return Proxy.newProxyInstance(getClass().getClassLoader(), source.getClass().getInterfaces(), this);
    }
    
    
}
技术分享图片

                上述我做了一些注释,其实已经说明一些问题,这个代理类的作用是可以代理任何类,因为它被传入的对象是Object,而不再是具体的类,比如刚才的Connection,这些产生的代理类在调用toString方法时会被插入before方法和after方法。

                动态代理有一个强制性要求,就是被代理的类必须实现了某一个接口,或者本身就是接口,就像我们的Connection。

                道理其实很简单,这是因为动态代理生成的代理类是继承Proxy类的,并且会实现被你传入newProxyInstance方法的所有接口,所以我们可以将生成的代理强转为任意一个代理的接口或者Proxy去使用,但是Proxy里面几乎全是静态方法,没有实例方法,所以转换成Proxy意义不大,几乎没什么用。假设我们的类没有实现任何接口,那么就意味着你只能将生成的代理类转换成Proxy,那么就算生成了,其实也没什么用,而且就算你传入了接口,可以强转,你也用不了这个没有实现你传入接口的这个类的方法。

               你可能会说,假设有个接口A,那我将接口A传给newProxyInstance方法,并代理一个没实现接口A的类B,但类B与接口A有一样的方法可以吗?

               答案是可以的,并且JDK的动态代理只认你传入的接口,只要你传入,你就可以强转成这个接口,这个一会解释,但是你无法在invoke方法里调用method.invoke方法,也就是说,你只能全部替换A接口的方法,而不能使用类B中原有与接口A方法描述相同的方法,这是因为invoke中传入的Method的class信息是接口A,而类B因为没实现接口A,所以无法执行传入的Method,会抛出非法参数异常。

               下面我贴出测试代码,各位可以自己试一下,具体为何会这样是在后面解释的,这里不再多做解释。

              先是一个普通接口。

技术分享图片
public interface TestInterface {

    void method1();
    
    void method2();
    
    void method3();
}
技术分享图片

            然后是一个类,和接口一模一样的方法,但是就是没实现这个接口。

技术分享图片
public class TestClass{

    public void method1() {
        System.out.println("TestClass.method1");
    }

    public void method2() {
        System.out.println("TestClass.method2");
    }

    public void method3() {
        System.out.println("TestClass.method3");
    }

}
技术分享图片

               下面是测试类。

技术分享图片
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;


public class DynamicProxy implements InvocationHandler{
    
    private Object source;
    
    public DynamicProxy(Object source) {
        super();
        this.source = source;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("接口的方法全部变成这样了");
        //这里source是TestClass,但是我们不能使用反射调用它的方法,像下面这样,放开这一行会抛异常
        //return method.invoke(source, args);
        return null;
    }
    
    public static void main(String[] args) {
        //只要你传入就可以强转成功
        TestInterface object =  (TestInterface) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{TestInterface.class}, new DynamicProxy(new TestClass()));
        object.method1();
        object.method2();
        object.method3();
    }
}
技术分享图片

               上面我们运行就会发现接口的方法全部都只能输出一个很2的字符串了。如果是要继续使用TestClass的方法也不是不行,只要你确认你传入的类包括了所有你传入的接口的方法,只是没实现这些接口而已,那么你可以在invoke中这样使用。

技术分享图片
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("before");
        Method sourceMethod = source.getClass().getDeclaredMethod(method.getName(), method.getParameterTypes());
        sourceMethod.setAccessible(true);
        Object result = sourceMethod.invoke(source, args);
        System.out.println("after");
        return result;
    }
技术分享图片

                这就与你实现接口的表现行为一致了,但是我们本来就只需要一句method.invoke就可以了,就因为没实现接口就要多写两行,所以这种突破JDK动态代理必须实现接口的行为就有点画蛇添足了。因为你本来就实现了该接口的方法,只是差了那一句implements而已。

 

资料参考:左萧龙博客设计模式

 

 

 

           

以上是关于代理模式的主要内容,如果未能解决你的问题,请参考以下文章

scrapy按顺序启动多个爬虫代码片段(python3)

用于从 cloudkit 检索单列的代码模式/片段

java代码实现设计模式之代理模式

代理模式(静态代理动态代理)代码实战(详细)

Java设计模式-代理模式之动态代理(附源代码分析)

代理模式(静态代理)