一、场景问题
某教务系统想要在用户业务操作前添加权限验证,以及操作过程的日志记录。请设计出一套合理的程序解决方案。
二、解决方案
1、传统解决方案
1.1、实现思路
在业务操作的前面添加权限校验,校验通过才能执行业务操作,在业务操作过程中添加相应的日志记录代码。
-
代码展示
-
权限校验器
public class AccessValidator { /** * 权限校验 * @param role 角色 * @author xianzilei **/ public static boolean validate(String role) { //这里只有admin角色才能校验通过 return "admin".equals(role); } }
-
日志记录器
public class Logger { /** * 普通日志 * @param msg 日志信息 * @author xianzilei **/ public static void info(String msg) { System.out.println("info:" + msg); } /** * 警告日志 * @param msg 日志信息 * @author xianzilei **/ public static void warn(String msg) { System.out.println("warn:" + msg); } /** * 错误日志 * @param msg 日志信息 * @author xianzilei **/ public static void error(String msg) { System.out.println("error:" + msg); } }
-
业务代码接口和实现
//业务操作接口 public interface IBusinessService { /** * 业务操作 * @param role 角色 * @author xianzilei **/ void businessOperation(String role); } //业务操作实现 public class BusinessServiceImpl implements IBusinessService{ Override public void businessOperation(String role) { //开始和结束记录日志 Logger.info("业务操作开始..."); //权限校验 if (!AccessValidator.validate(role)) { System.out.println("角色[" + role + "]没有访问权限!"); Logger.info("业务操作结束!"); return; } System.out.println("业务操作中..."); Logger.info("业务操作结束!"); } }
-
-
客户端测试
public static void main(String[] args) { IBusinessService businessService = new BusinessServiceImpl(); businessService.businessOperation("admin"); System.out.println("----------------------------------"); businessService.businessOperation("xianzilei"); }
输出
info:业务操作开始... 业务操作中... info:业务操作结束! ---------------------------------- info:业务操作开始... 角色[xianzilei]没有访问权限! info:业务操作结束!
1.2、传统方式实现的优缺点
- 优点:实现简单,代码易读。
- 缺点:缺点非常明显,每个业务操作都需要加同样的代码,重复代码巨大,而且非业务代码与业务代码高度耦合。
2、代理模式方案
明白了上面写法的弊端,我们期望业务代码尽可能只包含业务代码。那么我们该如何在不改变原业务代码的基础上,将权限校验和日志记录代码镶嵌到业务代码中,这就可以考虑使用代理模式。核心思想就是创建个代理类来执行业务代码,只不过代理类中调用日志记录和权限校验,以及具体的业务操作。代码如下
2.1、代码实现
-
定义代理类
public class BusinessServiceProxy implements IBusinessService { /** * 被代理的业务操作类 */ private IBusinessService businessService; public BusinessServiceProxy(IBusinessService businessService) { this.businessService = businessService; } @Override public void businessOperation(String role) { Logger.info("业务操作开始..."); //权限校验 if (!AccessValidator.validate(role)) { System.out.println("角色[" + role + "]没有访问权限!"); Logger.info("业务操作结束!"); return; } //业务操作代码(具体的操作) businessService.businessOperation(role); Logger.info("业务操作结束!"); } }
-
原业务代码可以简化
public class BusinessServiceImpl implements IBusinessService { @Override public void businessOperation(String role) { //仅仅放业务操作类 System.out.println("业务操作中..."); } }
-
客户端测试
public static void main(String[] args) { //创建被代理类 IBusinessService businessService = new BusinessServiceImpl(); //创建代理类 IBusinessService proxy = new BusinessServiceProxy(businessService); //代理类执行操作逻辑 proxy.businessOperation("admin"); System.out.println("----------------------------------"); proxy.businessOperation("xianzilei"); }
输出
info:业务操作开始... 业务操作中... info:业务操作结束! ---------------------------------- info:业务操作开始... 角色[xianzilei]没有访问权限! info:业务操作结束!
2.2、方案解析
我们采用组合的模式,将调料作为装饰器,通过动态组合方式动态构建出不同的咖啡口味。如果添加一种调料,只需要新增装饰器的实现类,在需要的时候将装饰器组装进去即可。
三、模式剖析
1、模式定义
装饰者模式(Decorator Pattern):指在不改变现有对象结构的情况下,动态地给该对象增加一些**职责(即增加其额外功能)**的模式。
2、模式结构
装饰者模式一般包含如下角色
-
Subject
:抽象主题角色通过接口或抽象类声明真实主题和代理对象实现的业务方法。
public abstract class Subject { public abstract void operate(); }
-
Real Subject
:真实主题角色实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。
public class RealSubject extends Subject { @Override public void operate() { System.out.println("执行真正的操作..."); } }
-
Proxy
:代理角色提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。
public class ProxySubject extends Subject { private Subject realSubject; @Override public void operate() { if (realSubject == null) { realSubject = new RealSubject(); } preRequest(); realSubject.operate(); postRequest(); } public void preRequest() { System.out.println("访问真实主题之前的预处理..."); } public void postRequest() { System.out.println("访问真实主题之后的后续处理..."); } }
3、模式结构图
4、模式扩展
4.1、动态代理
上面案例中的代理模式采用的是静态代理的方式,这种方式写法简单易懂,但是存在很大的问题。如果我们的业务接口存在10个方法,但是我们只要想要代理其中的一个方法,我们创建的代理类就被迫去实现另外9个方法,增加代码的维护成本。因此我们就可以采用动态代理的方式来实现,即我们不事先为每个类编写代理类,而是在运行的时候动态地创建代理类,然后在程序中替换掉原始类。
熟悉动态代理的小伙伴都知道实现代理有两种方式:JDK
动态代理和cglib
动态代理
(1)JDK
动态代理
上面的案例我们使用JDK
动态代理来实现,代码如下
-
代理工厂类
public class ProxyFactory { /** * 被代理的目标类 */ private Object target; public ProxyFactory(Object target) { this.target = target; } /** * 创建代理类 * @author xianzilei **/ public Object getProxyInstance() { return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), (proxy, method, args) -> { Logger.info("JKD代理开始..."); String role = (String) args[0]; if (!AccessValidator.validate(role)) { System.out.println("角色[" + role + "]没有访问权限!"); Logger.info("JKD代理结束..."); return null; } Object result = method.invoke(target, args); Logger.info("JKD代理结束..."); return result; }); } }
-
客户端代码
public static void main(String[] args) { //创建被代理类 IBusinessService businessService = new BusinessServiceImpl(); //创建代理工厂类 ProxyFactory proxyFactory = new ProxyFactory(businessService); //创建代理类 IBusinessService proxy = (IBusinessService) proxyFactory.getProxyInstance(); //代理类执行操作逻辑 proxy.businessOperation("admin"); System.out.println("----------------------------------"); proxy.businessOperation("xianzilei"); }
输出
info:JKD代理开始... 业务操作中... info:JKD代理结束... ---------------------------------- info:JKD代理开始... 角色[xianzilei]没有访问权限! info:JKD代理结束...
(2)cglib
动态代理
cglib
属于第三方代理框架,需要引入依赖
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.2.4</version>
</dependency>
代码如下
-
自定义拦截器(这里创建
MethodInterceptor
的实现类)public class LogMethodInterceptor implements MethodInterceptor { @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { Logger.info("Cglib动态代理开始..."); String role = (String) args[0]; if (!AccessValidator.validate(role)) { System.out.println("角色[" + role + "]没有访问权限!"); Logger.info("Cglib代理结束..."); return null; } Object object = proxy.invokeSuper(obj, args); Logger.info("Cglib动态代理结束..."); return object; } }
-
代理工厂类
public class ProxyFactory { /** * 被代理的目标类 */ private Object target; public ProxyFactory(Object target) { this.target = target; } /** * 创建代理对象 * @author xianzilei **/ public Object getProxyInstance() { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(target.getClass()); enhancer.setCallback(new LogMethodInterceptor()); return enhancer.create(); } }
-
客户端测试
public static void main(String[] args) { //创建被代理类 IBusinessService businessService = new BusinessServiceImpl(); //创建代理工厂类(注意这个是cglib的ProxyFactory) ProxyFactory proxyFactory = new ProxyFactory(businessService); //创建代理类 IBusinessService proxy = (IBusinessService) proxyFactory.getProxyInstance(); //代理类执行操作逻辑 proxy.businessOperation("admin"); System.out.println("----------------------------------"); proxy.businessOperation("xianzilei"); }
输出
info:Cglib动态代理开始... 业务操作中... info:Cglib动态代理结束... ---------------------------------- info:Cglib动态代理开始... 角色[xianzilei]没有访问权限! info:Cglib代理结束...
4.2、远程代理,虚拟代理,缓冲代理
实际开发中,代理模式往往比上述案例要复杂很多,一般情况下,代理模式根据目的和实现方式主要分为如下几种
-
远程代理
远程代理是一种常用的代理模式,它使得客户端程序可以访问在远程主机上的对象,远程主机可能具有更好的计算性能与处理速度,可以快速响应并处理客户端的请求。远程代理可以将网络的细节隐藏起来,使得客户端不必考虑网络的存在。客户端完全可以认为被代理的远程业务对象是在本地而不是在远程,而远程代理对象承担了大部分的网络通信工作,并负责对远程业务方法的调用
-
虚拟代理
对于一些占用系统资源较多或者加载时间较长的对象,可以给这些对象提供一个虚拟代理。在真实对象创建成功之前虚拟代理扮演真实对象的替身,而当真实对象创建之后,虚拟代理将用户的请求转发给真实对象
-
缓冲代理
缓冲代理它为某一个操作的结果提供临时的缓存存储空间,以便在后续使用中能够共享这些结果,从而可以避免某些方法的重复执行,优化系统性能
5、优缺点
- 优点
- 大大降低代码逻辑的耦合度,开发可以专注于业务代码的开发
- 提高代码逻辑的复用性
- 缺点
- 由于需要增加代理对象,因此会增加类的数量
- 部分代理模式可能会导致请求速度变慢。
6、使用场景
-
当客户端对象需要访问远程主机中的对象时可以使用远程代理。
-
当需要用一个消耗资源较少的对象来代表一个消耗资源较多的对象,从而降低系统开销、缩短运行时间时可以使用虚拟代理,例如一个对象需要很长时间才能完成加载时。
-
当需要为某一个被频繁访问的操作结果提供一个临时存储空间,以供多个客户端共享访问这些结果时可以使用缓冲代理。通过使用缓冲代理,系统无须在客户端每一次访问时都重新执行操作,只需直接从临时缓冲区获取操作结果即可。
-
当需要控制对一个对象的访问,为不同用户提供不同级别的访问权限时可以使用保护代理。
-
当需要为一个对象的访问(引用)提供一些额外的操作时可以使用智能引用代理。