跳至主要內容

代理模式

AruNi_Lu设计模式设计模式与范式约 2751 字大约 9 分钟

本文内容

前言

上几章中学习了几种 创建型 设计模式,它们主要解决 对象的创建问题

而从本章开始,将学习 结构型 设计模式,主要总结了一些 类或对象组合在一起的经典结构,这些结构可以适用于特定的应用场景。

结构型设计模式包括:代理模式、桥接模式、装饰器模式、适配器模式、门面模式、组合模式、享元模式。

1. 什么是代理模式

代理模式能在不改变原始类(被代理类)代码的情况下,通过引入代理类实现附加功能

举个例子,假设我们要为业务代理提供一个性能统计的功能。我们初步的做法是在业务方法开始和结束的位置记录两个时间,最后相减即可得到该业务功能的耗时。

接口:

public interface UserService {
    String login(String loginName , String passWord) ;
    void selectUsers();
}

实现类:

public class UserServiceImpl implements UserService{
    @Override
    public String login(String loginName, String passWord)  {
        long startTimer = System.currentTimeMillis();
        try {
            Thread.sleep(1000);
            if("admin".equals(loginName) && "1234".equals(passWord)) {
                return "success";
            }
            return "登录名和密码可能有误";
        } catch (Exception e) {
            e.printStackTrace();
            return "error";
        }finally {
            long endTimer = System.currentTimeMillis();
            System.out.println("login方法耗时:" + (endTimer - startTimer) / 1000.0 + "s");
        }
    }

    @Override
    public void selectUsers() {
        long startTimer = System.currentTimeMillis();
        System.out.println("查询了100个用户数据!");
        try {
            Thread.sleep(2000);
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            long endTimer = System.currentTimeMillis();
            System.out.println("selectUsers方法耗时:" + (endTimer - startTimer) / 1000.0 + "s");
        }
    }
    
}

测试类:

public class Test {
    public static void main(String[] args) {
        UserService userService = new UserServiceImpl();
        System.out.println(userService.login("admin", "1234"));
        userService.selectUsers();
    }
}

/* 输出:
login方法耗时:1.0s
success
查询了100个用户数据!
selectUsers方法耗时:2.001s
*/

上面的例子有很明显的两个问题:

  • 性能统计代理入侵到业务代码中,跟业务代码高度耦合
  • 每个需要性能统计的业务方法都需要编写同样的统计逻辑,存在大量重复代码

为了 将性能统计这个功能与业务代码解耦代理模式 就派上用场了。

我们新定义一个代理类 UserServiceProxy,也实现 UserService 接口。让 UserServiceImpl 只负责业务功能,代理类 UserServiceProxy 负责在业务代理前后附加性能统计的代码,中间通过委托的方式调用业务代码。

UserServiceImpl 只负责业务逻辑:

public class UserServiceImpl implements UserService{
    @Override
    public String login(String loginName, String passWord)  {
        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        if("admin".equals(loginName) && "1234".equals(passWord)) {
            return "success";
        }
        return "登录名和密码可能有误";
    }

    @Override
    public void selectUsers() {
        System.out.println("查询了100个用户数据!");
        try {
            Thread.sleep(2000);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

UserServiceProxy 负责性能统计,通过委托的方式调用业务代码:

public class UserServiceProxy implements UserService {
    private UserServiceImpl userService;
    
    // 通过依赖注入的方式将业务功能的实现类 UserServiceImpl 注入进来
    public UserServiceProxy(UserService userService) {
        this.userService = userService;
    }
    
    @Override
    public String login(String loginName, String passWord)  {
        long startTimer = System.currentTimeMillis();
        // 委托
        userService.login(loginName, passWord);
        long endTimer = System.currentTimeMillis();
        System.out.println("selectUsers方法耗时:" + (endTimer - startTimer) / 1000.0 + "s");
    }

    @Override
    public void selectUsers() {
        long startTimer = System.currentTimeMillis();
        // 委托
        userService.selectUsers();
        long endTimer = System.currentTimeMillis();
        System.out.println("selectUsers方法耗时:" + (endTimer - startTimer) / 1000.0 + "s");
    }  
}

测试:

public class Test {
    public static void main(String[] args) {
        // 使用 UserServiceProxy 类
        UserService userService = new UserServiceProxy(new UserUserviceImpl());
        System.out.println(userService.login("admin", "1234"));
        userService.selectUsers();
    }
}

/* 输出:
login方法耗时:1.0s
success
查询了100个用户数据!
selectUsers方法耗时:2.001s
*/

上面的做法虽然解耦了,但是基于接口来完成的,如果原始类没有定义接口,并且原始类代码并不是我们开发维护的(比如第三方类库),此时也没法给原始类重新定义一个接口。不过可以通过继承来解决,让代理类继承原始类,就可以调用父类的方法了。

不过还是 没有解决重复代码 的问题,而且 如果我们需要给很多类都提供附加逻辑,那么就需要为这些类都创建一个对应的代理类,增加了不必要的开发成本。

其实上面的做法是代理的第一种实现方式,成为静态代理。想要解决上面的两个问题,就需要使用动态代理。

2. 动态代理

所谓 动态代理,就是 不事先为每个原始类编写代理类,而是在运行时,动态地创建原始类对应的代理类,在系统中使用代理类替换掉原始类

在 Java 中提供了动态代理的语法(实际上,动态代理的低层就是反射),使用方法如下:

  • 代理类必须实现 InvocationHandler 接口,表明该类是动态代理的执行类;
  • 代理类需要重写 InvocationHandler 接口中的 invoke(Object proxy, Method method, Object[] args) 方法;
  • 使用 Proxy.newProxyInstance(Clas loader, Class[] interfaces, InvocationHandler h) 方法去获取代理对象。

具体实现代码如下:

public class TimeCollectionProxy {

    public static Object getProxy(Object proxyObj) {
        Class<?>[] interfaces = proxyObj.getClass().getInterfaces();
        DynamicProxyHandler handler = new DynamicProxyHandler(proxyObj);

        /* Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h):
            参数一:类加载器,负责加载代理类到内存中使用。
            参数二:获取被代理对象实现的全部接口。代理要为全部接口的全部方法进行代理
            参数三:代理的核心处理逻辑
         */
        return Proxy.newProxyInstance(proxyObj.getClass().getClassLoader(), interfaces, handler);
    }

    private static class DynamicProxyHandler implements InvocationHandler {
        private Object proxiedObj;

        public DynamicProxyHandler(Object proxiedObj) {
            this.proxiedObj = proxiedObj;
        }

        /**
         * @param proxy 代理对象本身。一般不管
         * @param method 正在被代理的方法
         * @param args 被代理方法,应该传入的参数
         * @return 被代理方法的执行结果
         */
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            long startTimer = System.currentTimeMillis();

            // 马上触发方法的真正执行 (触发真正的业务功能)
            Object result = method.invoke(proxiedObj, args);

            long endTimer = System.currentTimeMillis();
            System.out.println(method.getName() + " 方法耗时:" + (endTimer - startTimer) / 1000.0 + "s");

            // 把业务功能方法执行的结果返回给调用者
            return result;
        }
    }
}

这样,某个业务方法需要性能统计时,使用代理类创建的代理对象调用业务方法即可

public class Test {
    public static void main(String[] args) {
        // 把业务对象直接做成一个代理对象返回,代理对象的类型也是 UserService 类型
        UserService userService = (UserService) TimeCollectionProxy.getProxy(new UserServiceImpl());
        // 一调用方法就走代理
        System.out.println(userService.login("admin", "1234"));
        System.out.println(userService.deleteUsers());
    }
}

上面的代理类还有一种简便的写法,使用匿名内部类,就无需手动实现 InvocationHandler 了:

public class TimeCollectionProxy {

    /**
     * 参数写成泛型,任何类型的对象都能通用,返回类型为参数类型 T
     * 这样在外部使用时,就无需进行类型的强转了
     */
    public static <T> T getProxy(T proxyObj) {
        // 直接使用匿名内部类的写法
        return (T) Proxy.newProxyInstance(
                proxyObj.getClass().getClassLoader(),
                proxyObj.getClass().getInterfaces(),
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        long startTimer = System .currentTimeMillis();

                        Object result = method.invoke(proxyObj, args);

                        long endTimer = System.currentTimeMillis();
                        System.out.println(method.getName() + "方法耗时:" + (endTimer - startTimer) / 1000.0 + "s");

                        return result;
                    }
                });
    }
}

3. 代理模式的应用场景

代理模式的应用场景有很多,下面列举一些常用的。

3.1 业务系统的非功能性需求开发

这是代理模式最常用的一个应用场景,比如,我们在业务系统中会做一些 监控、统计、鉴权、限流、日志 等非功能性需求。这时候使用 代理模式 就可以很好的 将这些附加功能与业务功能解耦,统一放到代理类中处理

我们在使用 Spring 框架开发时,上面的功能大多都是使用 Spring AOP 来实现。其实,Spring AOP 的低层实现就是基于动态代理。简单概括就是:Spring 为这些类创建动态代理对象,并在 JVM 中替代原始类对象。原本在代码中执行的原始类的方法,被换作执行代理类的方法,也就实现了给原始类添加附加功能的目的。

3.2 代理模式在 RCP、缓存中的应用

实际上,RPC 框架也可以看作一种代理模式,可以称之为 远程代理。通过远程代理,将网络通信、数据编解码等细节隐藏起来。这样做有两个好处:

  • 客户端在使用 RPC 服务时,就像使用本地函数一样,无需了解跟服务器的交互细节;
  • RPC 服务的开发者也只需要关注业务逻辑,就像开发本地函数一样,无需关注跟客户端的交互细节。

再来看看 代理模式在缓存中的应用。假设现在要为某查询提供两种方式,一个支持缓存,一个支持实时查询。最简单的实现方式就是为需要支持缓存的查询都开发两个不同的接口,一个支持缓存,一个支持实时查询。但这样做明显增加了开发成本,而且会让代码看起来非常臃肿(接口个数成倍增加),也不方便缓存接口的集中管理(增加、删除缓存接口)、集中配置(比如配置每个接口缓存过期时间)。

这时,就可以使用动态代理了,如果使用 Spring 框架,就可以 在 AOP 切面中完成接口缓存的功能。在程序启动时,从配置文件中加载需要支持缓存的接口,以及对应的缓存策略等。当请求到来时,在 AOP 切面中拦截请求,如果请求中带有支持缓存的字段(如:http://...?...&cached=true),则从缓存中获取数据直接返回;否则就去执行实时查询的逻辑。

4. 总结

代理模式,即在不改变原始类(被代理类)代码的情况下,通过引入代理类实现附加功能。一般情况下会让代理类和原始类实现同样的接口(组合方式)。但是如果原始类并没有定义接口,并且原始类代码并不是我们开发维护的。此时可以通过让代理类 继承 原始类的方法来实现代理模式。

静态代理需要为每个类都创建一个代理类,而且代理类中的每个方法中的附加功能代码都是重复的,因此增加了许多无效的开发和维护成本,此时就需要使用动态代理了。

所谓 动态代理,就是 不事先为每个原始类编写代理类,而是在运行时,动态地创建原始类对应的代理类,在系统中使用代理类替换掉原始类

代理模式的应用场景非常多,场景的有:

  • 在业务系统中做一些 监控、统计、鉴权、限流、日志 等非功能性需求;
  • 在 RPC、缓存等应用场景。
上次编辑于: