代理模式

5,957 阅读9分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第7天,点击查看活动详情

1.情景引入

1.1 创建接口

假如我们要创建一个计算器,那我们首先要做的第一件事情便是创建接口,在接口里面定义好计算器需要的方法。现在声明一个Calculator接口

public interface Calculator {

    int add(int i, int j);
    int sub(int i, int j);
    int mul(int i, int j);
    int div(int i, int j);

}

1.2 创建实现类

现在可以根据接口来实现对应的计算器类了。

public class CalculatorImpl implements Calculator{
    @Override
    public int add(int i, int j) {
        int result = i + j;
        System.out.println("方法内部:result: " + result);
        return result;
    }

    @Override
    public int sub(int i, int j) {
        int result = i - j;
        System.out.println("方法内部:result: " + result);
        return result;
    }

    @Override
    public int mul(int i, int j) {
        int result = i * j;
        System.out.println("方法内部:result: " + result);
        return result;
    }

    @Override
    public int div(int i, int j) {
        int result = i / j;
        System.out.println("方法内部:result: " + result);
        return result;
    }
}

1.3 创建带日志的计算器类

上面的计算器类如果需要添加日志的话,我们通常直接在代码里面添加日志代码

public class CalculatorImpl implements Calculator{
    @Override
    public int add(int i, int j) {
        System.out.println("日志,方法:add,参数:"+i+","+j);
        int result = i + j;
        System.out.println("方法内部:result: " + result);
        System.out.println("日志,方法:add,结果:"+result);
        return result;
    }

    @Override
    public int sub(int i, int j) {
        System.out.println("日志,方法:sub,参数:"+i+","+j);
        int result = i - j;
        System.out.println("方法内部:result: " + result);
        System.out.println("日志,方法:sub,结果:"+result);
        return result;
    }

    @Override
    public int mul(int i, int j) {
        System.out.println("日志,方法:mul,参数:"+i+","+j);
        int result = i * j;
        System.out.println("方法内部:result: " + result);
        System.out.println("日志,方法:mul,结果:"+result);
        return result;
    }

    @Override
    public int div(int i, int j) {
        System.out.println("日志,方法:div,参数:"+i+","+j);
        int result = i / j;
        System.out.println("方法内部:result: " + result);
        System.out.println("日志,方法:div,结果:"+result);
        return result;
    }
}

1.4 问题提出

现有问题

我们发现我们添加日志的代码其实和主业务代码的关系不大,我们主业务的代码其实是进行加减乘除,我们添加的日志代码还分散在不同的业务功能中不利于维护。

解决问题的思路

其实说白了就是解耦。我们将和主业务无关的代码抽取出来。

困难

我们发现日志的代码是分散在业务功能方法里面的,我们无法像抽取jdbc代码一样,将日志代码抽取成一个类。为此我们需要引入新的技术,即代理模式。

2.代理模式

**概念:**代理模式就是不直接去调用目标方法而是通过创建一个代理类,由代理类去调用目标方法。**可能大家会觉得这样做有点多余,为什么不直接调用目标方法?**我们创建代理类的目的是可以将跟主业务代码无关的代码放到代理类里面,这样有利于维护代码。其实代理模式在日常生活中很常见,比如租房,我们自己找房子签合同比较麻烦。但是可以通过中介来租房,中介就相当于代理类,签合同就是目标方法,而中介不仅可以帮我们签合同,还可以帮我们做别的事情,这就是代理类别的作用除了调用目标方法,还可以增加别的功能。

实现代理模式具体步骤:

  1. 定义一个接口。这个接口里面定义我们主要业务需要的方法
  2. 定义一个目标类。让这个类去实现定义好的接口,这个类就是用来实现主要业务功能的
  3. 创建代理类。因为代理类要求调用目标方法,所以代理类也要实现接口,实现接口的方法,在方法里调用目标类的方法。在调用目标类方法的前后,还可以增加别的代码,这样子就能够让业务核心代码和非核心代码进行分离。

2.1 静态代理

创建接口

public interface Calculator {

    int add(int i, int j);
    int sub(int i, int j);
    int mul(int i, int j);
    int div(int i, int j);

}

创建目标类

public class CalculatorImpl implements Calculator{
    @Override
    public int add(int i, int j) {
        int result = i + j;
        System.out.println("方法内部:result: " + result);
        return result;
    }

    @Override
    public int sub(int i, int j) {
        int result = i - j;
        System.out.println("方法内部:result: " + result);
        return result;
    }

    @Override
    public int mul(int i, int j) {
        int result = i * j;
        System.out.println("方法内部:result: " + result);
        return result;
    }

    @Override
    public int div(int i, int j) {
        int result = i / j;
        System.out.println("方法内部:result: " + result);
        return result;
    }
}

创建代理类

说明:因为我们需要调用目标类的方法,所以我们需要在代理类里面创建一个构造方法,这个构造方法的参数是目标类对象。有了目标类对象之后,我们就可以通过它去调用目标方法。大家看下面的代码,可以发现我们可以在目标方法上面或者下面添加别的代码而不对目标方法造成任何影响,如果有必要,还可以用try-cath-finally将目标代码包起来,解决可能出现的异常。

public class CalculatorStaticProxy  implements Calculator{

    private CalculatorImpl target;

    public CalculatorStaticProxy(CalculatorImpl target) {
        this.target = target;
    }

    @Override
    public int add(int i, int j) {
        int result = 0;
        try {
            System.out.println("日志,方法:add,参数:"+i+","+j);
            result = target.add(i, j);
            System.out.println("日志,方法:add,结果:"+result);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
        }
        return result;
    }

    @Override
    public int sub(int i, int j) {
        System.out.println("日志,方法:sub,参数:"+i+","+j);
        int result = target.add(i, j);
        System.out.println("日志,方法:sub,结果:"+result);
        return result;
    }

    @Override
    public int mul(int i, int j) {
        System.out.println("日志,方法:mul,参数:"+i+","+j);
        int result = target.add(i, j);
        System.out.println("日志,方法:mul,结果:"+result);
        return result;
    }

    @Override
    public int div(int i, int j) {
        System.out.println("日志,方法:div,参数:"+i+","+j);
        int result = target.add(i, j);
        System.out.println("日志,方法:div,结果:"+result);
        return result;
    }
}

测试

public class ProxyTest {

    @Test
    public void testProxy(){
        CalculatorStaticProxy proxy = new CalculatorStaticProxy(new CalculatorImpl());
        proxy.add(1,2);
    }

}

结果

日志,方法:add,参数:1,2 方法内部:result: 3 日志,方法:add,结果:3

新问题:

大家有没有发现,如果我们有多个类也需要代理类,那么我们就会出现这样的局面。有很多接口,很多目标类,以及很多代理类。代理模式下,需要一个接口,而且目标类和代理类都需要去实现这个接口,所以就会造成代码很多的局面,那么有解决办法吗?答案是有,我们可以用jdk自带的api来帮我们动态创建代理类,不用我们自己去创建代理类。

2.2 动态代理

要想实现动态代理,我们需要解决两个问题:

  1. 如何根据加载到内存中的被代理类,动态创建一个代理类对象
  2. 当通过代理类的对象调用方法时,如果动态的去调用被代理类中的同名方法

生产代理对象的工厂

创建一个工厂类,有这个类借助jdk的APi帮我们动态创建代理类。好处是这个工厂类只需创建一次,以后需要什么代理类只需传入被代理类即可。

动态代理执行过程:

  1. 我们创建了一个工厂类,这个类里面创建了一个方法getProxyInstance,由这个方法帮我们获取代理对象
  2. 当我们获取代理对象之后,我们便可以调用被代理类里面的方法
  3. 当我们调用被代理类的方法时,本质上是先跳转到我们实现了InvocationHandler接口的类里面的invoke方法,然后在invoke方法里通过method对象调用invoke方法实现被代理类方法的调用

动态代理代码细节:

  1. 获取代理类的代码是通过Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)方法获得的,这方法的三个参数可以详见下面代码
  2. 我们传递给工厂类的被代理对象(例子里的obj),本质上是为了将被代理对象传递给实现了InvocationHandler接口的类。
public class ProxyFactory2 {

    /**
     * 调用此方法返回一个代理类对象
     * @param obj 被代理对象
     * @return
     */
    public static Object getProxyInstance(Object obj){
        MyInvocationHandler handler = new MyInvocationHandler();
        handler.bind(obj);

        /**
         * 返回代理类对象 第一个参数:获取类加载器 第二个参数:获取代理类实现的接口
         * 第三个参数需要我们实现InvocationHandler接口的类,这个类的invoke方法可以动态调用被代理对象的方法,解决问题2
         */
        return Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(),handler);
    }

}

class MyInvocationHandler implements InvocationHandler {

    private Object obj;//被代理类对象,我们要给它赋值,然后用它调用目标方法

    public void bind(Object obj){
        this.obj = obj;
    }

    /**
     * 当我们调用代理类里和被代理类相同名字的方法时,会调用下面的方法。将被代理类要执行的方法的功能声明在invoke()中
     * @param proxy 被代理类对象
     * @param method 代理类对象所调用的方法,此方法也是被代理类对象被调用的方法
     * @param args 目标方法所携带的参数
     * @return
     * @throws Throwable
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //如果目标方法有返回值,那么returnValue就会接收到,这也是为什么下面要return这个返回值的原因
        Object returnValue = method.invoke(obj,args);
        return returnValue;
    }
}

测试

@Test
public void testProxy2(){
    CalculatorImpl calculator = new CalculatorImpl();
    /**
     * 我们知道代理类和被代理类实现的是同样的接口,所以这里在获取代理类的时候直接强转成代理类实现的接口的类型,
     * 不然你获取到的是一个Object类型的代理类,无法调用目标方法
     */
    Calculator proxyInstance = (Calculator) ProxyFactory2.getProxyInstance(calculator);
    int result = proxyInstance.add(1, 1);
    System.out.println(result);
}

结果

方法内部:result: 2 2

动态代理工厂实现类的另一种写法

这种写法本质上和上面一样,只是这样写看起来代码简洁一点。比如将实现接口的类写成匿名内部类。顺便演示了增强代码,即在调用目标方法的前后增加其他业务功能。

public class ProxyFactory {

    private Object target;

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

    public Object getProxy(){
        ClassLoader classLoader = this.getClass().getClassLoader();
        Class<?>[] interfaces = target.getClass().getInterfaces();
        InvocationHandler h = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                Object result = null;
                try {
                    System.out.println("日志,方法:"+method.getName()+",参数:"+ Arrays.toString(args));
                    result = method.invoke(target, args);
                    System.out.println("日志,方法:"+method.getName()+",结果:"+ result);
                } catch (Exception e) {
                    e.printStackTrace();
                    System.out.println("日志,方法:"+method.getName()+",异常:"+ e);
                }  finally {
                    System.out.println("日志,方法:"+method.getName()+",方法执行完毕");
                }
                return result;
            }
        };
        return Proxy.newProxyInstance(classLoader,interfaces,h);
    }
}

2.3 总结

静态代理搞懂了其实就了解了代理模式。动态代理本质上和静态代理一样,只是我们借助了jdk的api,所以不用我们手动来创建代理类,而是通过被代理类创建出代理类。