代理

这篇文章主要是想介绍 23 种常用设计模式中的代理模式,我们在这里实现相关代码时也是使用 Java 来实现,掌握了代理之后,对我们后面学习 Spring 等框架也会很有帮助,可以让我们更好地理解这些框架的设计与实现,因此掌握好这些基础就相当于是打地基,下面具体来看。

1.代理定义

代理模式是指为其它调用对象提供一种代理以控制对目标对象的访问,某些情况下,一个对象不适合或者不能直接引用另一个对象,这时候代理对象就可以在客户端和目标对象之间起到中介的作用。

2.代理组成

代理模式由三个角色组成,分别为:抽象角色,代理角色和目标角色。三种角色的具体职责如下:

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

3.代理代码案例

下面我们通过一个简单的代码案例来演示上面所说的代理中三种角色,需要注意的一点是,代理角色和真实角色必须要实现同一接口,也就是抽象角色,下面看具体代码:

抽象角色:

public interface ITarget {

    public void show();
}

真实角色:

public class Target implements ITarget {

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

代理角色:

public class TargetProxy implements ITarget {

    private ITarget target;

    public TargetProxy(ITarget target) {
        this.target = target;
    }

    public void show() {
        // 在代理中通过目标对象调用真是行为
        // 1.判断是否有权限调用目标行为
        // 2.进行日志操作
        // 3.性能监控
        target.show();
    }
}

我们现在就可以来看一下使用代理与不使用代理时,相关调用代理有何不同:

不使用代理:

@Test
public void test() {
    ITarget target = new Target();
    target.show();
}

上面的代码我们是很熟悉的,直接创建目标对象,然后调用其中的具体方法,就可以达到我们想要的目的了。

使用代理:

@Test
public void test2() {
    ITarget target = new Target();
    TargetProxy proxy = new TargetProxy(target);
    proxy.show();
}

使用代理时,我们是先创建目标对象,然后在创建代理对象时将目标对象的引用传递进去,最后代理对象调用具体方法时其实还是调用的目标对象中的方法。这时候可能会有一个疑问,本来可以直接使用目标对象调用具体方法,为什么还要使用代理呢?其实使用代理是为了大家的职责更清晰,比如目标对象就只需要实现具体的业务逻辑,而其它不是具体业务逻辑的事情,比如判断当前用户是否有权限调用当前代码,或者对具体业务逻辑方法前后增加日志打印,这些都可以放到代理类中去完成,而目标对象只要不是具体业务逻辑发生改变都是不需要改变的,这样减少核心业务逻辑代码的更改,也使得程序更加安全。

4.代理优点

使用代理的优点主要有以下 3 点:

1.职责清晰。真实角色只需要实现实际的业务逻辑,不用关心其他非本职责的事务,通过后期的代理完成一些其它事务,这样的结果就是编程简洁,职责清晰。
2.代理对象可以在客户端和目标对象之间起到中介的作用和保护了目标对象的作用。
3.高扩展性。

5.代理分类

代理可以分为两类,一类是静态代理,另一类就是动态代理了,两类代理具体定义如下:

静态代理:是由程序员创建或工具生成代理类的源码,再编译代理类。所谓静态也就是在程序运行前就已经存在代理类的字节码文件,代理类和目标类的关系在运行前就确定了。
动态代理:是在实现阶段不用关心代理类,而在运行阶段才指定创建代理类。

上面写的代理代码案例中展示的便是静态代理了,因为程序运行前代理类就被我们创建好了,而且代理类和目标类之间的关系也已经确定了,那掌握了静态代理之后,我们下面再来看动态代理。

6.动态代理

JDK 为我们提供的 api 中我们可以使用 Proxy 来实现动态代理,它的类全称为 java.lang.reflect.Proxy,需要注意的是,当我们使用该类来为目标对象创建动态代理对象时,目标对象所属的类必须是实现了接口的。动态代理对象是在程序运行期间才被创建的,因此动态代理其实是在内存中生成的代理对象。

Proxy 类中具体生成动态代理对象的方法为:

public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException

我们先不具体看该方法的参数等细节,而是先看如何使用该方法为目标对象创建代理对象,使用方法为:

@Test
public void test3() {
    // 1.创建目标对象
    final ITarget target = new Target();

    // 2.使用Proxy的newProxyInstance方法完成代理对象的创建
    ITarget proxy = (ITarget) Proxy.newProxyInstance(target.getClass().getClassLoader(),
            target.getClass().getInterfaces(), new InvocationHandler() {

                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    return method.invoke(target, args);
                }
            });

    proxy.show();
}

7.Proxy详解

我们先看一下 Proxy 类中 newProxyInstance() 方法的各个参数,第 1 个参数 ClassLoader loader 为目标对象的类加载器,第 2 个参数 Class<?>[] interfaces 为目标对象实现接口的 Class 数组,第 3 个参数 InvocationHandler h 为代理对象调用目标对象所实现的接口。前两个参数主要是帮我们在内存中构建目标对象的代理对象,而最后一个参数 InvocationHandler 则是让我们能监听代理对象调用方法,帮助我们调用目标对象。

我们在使用 Proxy 类中 newProxyInstance() 方法来创建代理对象时,其中的第三个参数我们需要传入 InvocationHandler 接口的实现类,而在 InvocationHandler 接口中有一个 invoke() 方法,我们在实现该接口时也需要实现该方法,方法具体信息如下:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable

其中的第 1 个参数 Object proxy 则是代理对象本身,第 2 个参数 Method method 则是代理对象调用目标对象中的方法对象,第 3 个参数 Object[] args 则是代理对象调用目标对象中的方法所传递的参数数组,下面我们也通过一个例子来进行说明。

抽象角色:

public interface IUserService {

    public void addUser(String username, String password);
}

真实角色:

public class UserServiceImpl implements IUserService {

    public void addUser(String username, String password) {
        System.out.println("添加用户:" + username + "   " + password);
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

动态代理创建工具类:

public class ProxyBuilder {

    private Object target;

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

    public Object createProxy() {
        ClassLoader classLoader = target.getClass().getClassLoader();
        Class<?>[] interfaces = target.getClass().getInterfaces();
        return Proxy.newProxyInstance(classLoader, interfaces, new InvocationHandler() {

            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//                System.out.println(proxy);
                System.out.println(method);
                System.out.println(args[0] + "   " + args[1]);
                Object returnValue = method.invoke(target, args);
                return returnValue;
            }
        });
    }
}

使用代码:

@Test
public void test() {
    IUserService userService = new UserServiceImpl();

    ProxyBuilder proxyBuilder = new ProxyBuilder(userService);
    IUserService proxy = (IUserService) proxyBuilder.createProxy();
    proxy.addUser("tom", "123");
}

在动态代理创建工具类中,实现 InvocationHandler 接口中的 invoke() 方法时,我们没有打印 proxy 代理对象的值出来,这是因为如果放开代码注释的话,会导致堆栈内存溢出出错,因为这里打印 proxy 对象,其实是打印 proxy 对象调用 toString() 方法后返回的值,而 proxy 代理对象调用 toString() 方法,其实是调用目标对象的 toString() 方法,在代理对象调用目标对象的 toString() 方法时,又会触发 InvocationHandler 接口实现类中的 invoke() 方法,这样的话就会形成一个死循环,导致内存溢出。

8.性能监控案例

为了监控程序功能的运行时间,我们可以在代理对象中查看程序运行的时间,而不需要在目标对象中增加与业务逻辑无关的代码,下面是具体代码实现:

public class ProxyBuilder {

    private Object target;

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

    public Object createProxy() {
        ClassLoader classLoader = target.getClass().getClassLoader();
        Class<?>[] interfaces = target.getClass().getInterfaces();
        return Proxy.newProxyInstance(classLoader, interfaces, new InvocationHandler() {

            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//                System.out.println(proxy);
                System.out.println(method);
                System.out.println(args[0] + "   " + args[1]);
                long t1 = System.currentTimeMillis();
                Object returnValue = method.invoke(target, args);
                long t2 = System.currentTimeMillis();
                System.out.println(method.getName() + "方法耗时时间:" + (t2 - t1) + "ms");
                return returnValue;
            }
        });
    }
}

这样的话我们就可以知道代理对象调用目标对象中的各个方法都耗费了多长时间,也就实现了我们想要查看性能情况的想法。

9.总结

掌握 Java 中的动态代理技术,对于我们后面学习各个框架是十分有帮助的,可以让我们更好地理解框架是如何实现的,因此现在的每一分积累都是为了后面更好地成长,掌握好这些基础知识也能帮我们更好地理解框架的设计,因此踏实积累吧。

坚持原创技术分享,您的支持将鼓励我继续创作!