【设计模式】代理模式的实现方式与使用场景

1. 概述

代理模式是一种结构型设计模式,它通过创建一个代理对象来控制对另一个对象的访问,代理对象在客户端和目标对象之间充当了中介的角色,客户端不再直接访问目标对象,而是通过代理对象间接访问目标对象。

那在中间加一层代理的作用是什么呢?
有了中间这一层代理,我们就可以在目标对象方法调用前、调用后添加上一些额外的代码逻辑,在不改变目标对象的情况下,实现对目标对象的访问控制、功能增强、提高系统性能等功能。


代理模式按不同的实现方式分为静态代理动态代理

  • 静态代理:在代码编写时显式的编写的代理类。
  • 动态代理:在运行时动态生成代理类。

我们平时使用的更多的是动态代理,但不管是静态代理还是动态代理,最终生成的类关系是一致的,只是手工编写代码和框架生成代码的区别,所以在下面的实现方式内容中会优先讲到静态代理。

2.实现方式

代理模式有两种实现方式,一种是代理类与目标类实现相同的接口,另一种的代理类继承目标类并重写目标类的方法,两种方法没有太大的优劣之分,往往是互补的。

如果我们通过面向接口编程的原则设计的功能,就可以通过“实现接口”的方式来处理代理类,类关系图如下:
【设计模式】代理模式的实现方式与使用场景_第1张图片
在上图中,代理类和目标类都实现了抽象的接口,代理对象通过关联关系持有了目标对象的引用,客户端同样通过关联持有了代理对象的引用。客户端可以向代理对象发起请求,代理对象收到请求后转发到目标对象中,同时在转发前后可以做一定的功能增强。

同样的,如果是我们在使用一些第三方的jar包,在使用到这些包的有可能目标类并没有实现一个具体的接口,这时候就可以通过继承的方式来实现。
【设计模式】代理模式的实现方式与使用场景_第2张图片
在这种实现方式中,代理对象可以直接通过super调用目标对象的方法,看起来结构更为简单。


需要注意的是,由于代理对象需要请求目标对象的方法,所以代理对象一定要有目标对象方法的访问权限。

第一种方式中,代理对象通过关联的方式持有了目标对象,但是代理对象与目标对象不是父类与子类的关系、也可能并不在同一个包中,所以目标对象中被代理的方法一定是public方法,当然,目标对象实现了interface中的方法也只可能是public方法。

在第二种方式,由于是通过继承实现的,需要注意继承的权限,即:

  • 父类不能用final修饰
  • 父类中需要重写的方法不能用final修饰,不能是static方法,也不同由private修饰

总结一下,代理对象能代理目标对象的方法如下表:

实现方式 支持代理public 支持protected 支持default 支持private
接口实现
继承实现

熟悉Spring的同学可能已经发现了,这个表格和Spring AOP中的JDK代理与CGLIB代理支持的方法作用域是一致的。通过上面的分析,相信大家也理解了为什么Spring中的bean对象中的方法在非public修饰时,可能会导致AOP失效。

2.1.静态代理

接下来就是如何用代码来实现代理模式,用一个简单demo来体验一下静态代理,现在有一个UserSevice,我们需要在插入用户前开启事务,在插入完成后提交事务,代码如下:

  • 用户接口和实现:
    public interface UserService {
        void insertUser();
    }
    
    public class UserServiceImpl implements UserService {
        @Override
        public void insertUser() {
            System.out.println("查询用户");
        }
    }
    
  • 代理类:
public class UserServiceProxy implements UserService{

    private UserService userService;

    public UserServiceProxy(UserService userService) {
        this.userService = userService;
    }

    @Override
    public void insertUser() {
        System.out.println("插入用户前开启事务");
        userService.insertUser();
        System.out.println("插入用户后提交事务");
    }

}

然后我们模拟一下客户端,做一个测试:

public static void main(String[] args) {
    UserService userService = new UserServiceImpl();
    UserServiceProxy userServiceProxy = new UserServiceProxy(userService);
    userServiceProxy.insertUser();
}

插入用户前开启事务
插入用户
插入用户后提交事务


相信大家发现了,在客户端中既要创建用户服务的实例,也要创建代理对象实例,而更多的时候客户端可能并不关心请求的是代理对象还是实际的目标对象,这种情况可以结合工厂模式来处理。

以简单工厂为例(如果对简单工厂不熟悉可以看一下我的上一篇博客《什么场景可以考虑使用简单工厂模式》),写一个工厂:

public class UserServiceFactory {
    /**
     * 默认返回代理对象
     */
    public static UserService getInstance() {
        return new UserServiceProxy(new UserServiceImpl());
    }
}

测试代码修改为:

public static void main(String[] args) {
    UserService userService = UserServiceFactory.getInstance();
    userService.insertUser();
}

插入用户前开启事务
插入用户
插入用户后提交事务

两次测试结果一致,且使用工厂后对客户端屏蔽了实现细节,写到这里相信大家已经有了熟悉感,没错,SpringIOC容器底层就是一个工厂,它创建出的bean对象也是一个个的代理对象

通过继承来实现代理模式也比较简单,这里就不过多的赘述了。


上面的代码看起来很容易就增强了目标对象中的方法,但如果需要代理的方法数量开始膨胀,需要代理的类也开始膨胀,例如我们有几十个向数据库插入数据的方法,每个方法我都得去写一遍代理逻辑,这就会导致开发和维护的成本成倍的上升,而且所有的代码都是高度类似的

我希望把这些高度类似的代码都抽取出去,像模板一样,需要使用的地方就直接把模板套进去使用而不需要编写大量重复的代码,下面要说到的动态代理就能解决这个的问题

2.2.动态代理

我们梳理一下上面的静态代理类,这个代理类实现的功能是Insert方法进行增强,自动开启事务和提交事务,我们在这个基础上将增强的逻辑提取出来做一个抽象,将匹配UserService抽象为匹配所有Insert操作。

然后是代码实现,在Java中可以使用JDKCGLIB两种方式来实现动态代理,我们先看JDK实现的方式。

2.2.1. JDK的实现方式

JDK的代理方式要求目标对象一定是实现了某个interface,它是通过反射的方式来创建的代理对象,在java.lang.reflect有两个关键的类(接口):

  • InvocationHandler:定义了一个invoke方法,在这个方法中编写目标对象方法的增强逻辑
  • Proxy:可以通过目标对象与InvocationHandler创建一个代理对象。

按照上面所说的抽象方式,抽象出的事务处理器代码如下:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class TransactionHandlerByJdk implements InvocationHandler {

    /**
     * 目标对象
     */
    private Object target;

    /**
     * 获取代理对象的方法
     *
     * @param target 目标对象
     * @return 代理对象
     */
    public Object getInstance(Object target) {
        this.target = target;
        Class<?> clazz = target.getClass();
        return Proxy.newProxyInstance(clazz.getClassLoader(), clazz.getInterfaces(), this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) {
        before();

        Object invoke = null;
        try {
            invoke = method.invoke(this.target, args);
        } catch (Exception e) {
            afterThrowing();
        }

        after();

        return invoke;
    }

    private void before() {
        System.out.println("开启事务");
    }

    private void after() {
        System.out.println("提交事务");
    }

    private void afterThrowing() {
        System.out.println("回滚事务");
    }

}

写一个单元测试验证一下结果:

@Test
public void testTransaction() {
    UserService userService = (UserService) new TransactionHandlerByJdk().getInstance(new UserServiceImpl());
    userService.insertUser();
}

开启事务
插入用户
提交事务

在执行insert之前打一个断点可以观察到对应的userService是一个代理对象,在所有的框架执行过程中,只要我们看到$Proxy就可以断定它是一个代理对象。
【设计模式】代理模式的实现方式与使用场景_第3张图片

2.2.2.CGLIB代理

如果目标类没有实现interface,可以考虑使用CGLIB来做动态代理,实现的方式也是类似的,在net.sf.cglib.proxy包下面也有两个重要的类(接口):

  • MethodInterceptor:定义了一个intercept方法,在这个方法中编写目标对象方法的增强逻辑。

  • Enhancer:可以通过目标对象与MethodInterceptor创建一个代理对象。

  • 引入依赖:

    <dependency>
        <groupId>cglibgroupId>
        <artifactId>cglibartifactId>
        <version>3.3.0version>
    dependency>
    
  • 编写抽象事务处理器

    import net.sf.cglib.proxy.Enhancer;
    import net.sf.cglib.proxy.MethodInterceptor;
    import net.sf.cglib.proxy.MethodProxy;
    
    import java.lang.reflect.Method;
    
    public class TransactionHandlerByCglib implements MethodInterceptor {
    
        @SuppressWarnings("unchecked")
        public <T> T getInstance(Class<T> target) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(target);
            enhancer.setCallback(this);
            return (T) enhancer.create();
        }
    
        @Override
        public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
    
            before();
    
            Object obj = null;
            try {
                obj = methodProxy.invokeSuper(o, objects);
            } catch (Exception e) {
                afterThrowing();
            }
    
            after();
            return obj;
        }
    
        private void before() {
            System.out.println("开启事务");
        }
    
        private void after() {
            System.out.println("提交事务");
        }
    
        private void afterThrowing() {
            System.out.println("回滚事务");
        }
    }
    
  • 测试

    @Test
    public void testTransactionCglib() {
        UserService userService = new TransactionHandlerByCglib().getInstance(UserServiceImpl.class);
        userService.insertUser();
    }
    

    最终测试的结果也是一样的:

    开启事务
    插入用户
    提交事务


相对于静态代理而言,动态代理的优势在于只需要编写一些代理对象的处理器就可以动态的生成各种各样的代理对象。
在上面的例子中,如果又新增了一个部门服务,只需要在客户端传入对应的目标对象(部门服务对象)就可以享受到自动管理事务的待遇了,不需要修改代理相关的任何的代码,这是静态代理所不具备的优势,这也是我们经常遇到的代理模式是动态代理的原因。

3.使用场景

由于静态代理在使用的时候,需要针对每个对象都创建一个对应的代理对象, 非常繁琐,在实际的项目中运用的并不是太多,一般都是选择使用动态代理模式,主要考虑两种场景:

  • 需要将业务代码与非业务代码的分离。
  • 多个方法都有相同的操作时,做统一处理。

实际上在大多数时候是同时满足两种场景的,例如下面这些场景:

  • 鉴权:例如针对需要用户权限验证的接口,每个接口在调用前都需要验证当前登录人信息。
  • 监控:例如在重要流程接口运行时出现了异常,需要在异常出现时给维护人员发送告警消息。
  • 日志:例如接口请求日志,在关键接口中需要记录访问人、访问IP、请求参数、响应参数等信息。
  • 统计:例如接口的访问次数统计。
  • 事务:数据库的事务开启、提交、回滚操作与业务代码分离。
  • ……

4.总结

本篇主要讲述的是代理模式的实现方式与使用场景,先介绍了代理模式的概念和作用,然后从静态代理开始讲述了代理模式的实现方式,其中静态代理的使用频率并不高,动态代理则相反,使用频率非常高,需要重点掌握。

之所以花了一部分篇幅讲解静态代理,主要是能够直观的感受到代理模式的类结构,后续动态代理生成的代码与静态代理的也大同小异。

我们在鉴权、监控、统计、日志、事务等多种场景中都可以使用动态代理模式。在使用代理模式的时候需要注意接口实现继承实现两种方式的区别及注意事项,重点是下面这个表:

实现方式 支持代理public 支持protected 支持default 支持private
接口实现
继承实现

至于性能上,两种实现动态代理的方式在性能上可能有细微的差异,但在实际应用中并不明显,在选择动态代理方式时,应该根据具体的需求和场景来决定使用哪种方式。

你可能感兴趣的:(#,设计模式,架构与设计,设计模式,代理模式)