代理模式
在日常开发中我们可以会接手一些老的项目,有时连源码都没有,或者有时候我会需要对业务逻辑做一定增强(功能扩展,如:日志、事务等),这时候我们通常不能或者不建议直接修改源码(可能根本没有源码)。在设计模式中有一种模式叫代理模式可以很好的应对上述场景,也符合了,实现代码解耦(扩展部分不污染原有业务代码)。 所谓代理模式就是当我们需要增强业务逻辑时,创建一个增强的代理类,在代理类中调用原有业务逻辑实现代码,并对其作增强。 假设一场景,我们有一个业务接口
public interface HelloService { /** * 业务:用户说一句话 * @param name * @param message * @return */ String say(String name, String message);}
并且有一个对应的实现类
public class HelloServiceImpl implements HelloService { @Override public String say(String name, String message) { // 使程序休眠[0, 200]毫秒,模拟代码执行过程 try { Thread.sleep(new Random().nextLong() & 200); } catch (InterruptedException e) { e.printStackTrace(); } return String.format("%s : %s", name, message); }}
我们测试一下这个场景
private HelloService service; @Test public void proxy() { // 模拟业务接口实现类实例注入 service = new HelloServiceImpl(); // I、业务接口调用 String r = service.say("Ashe", "今天没吃早饭"); // Ashe : 今天没吃早饭 System.out.println(r); }
现在需求调整了,要求接口调用都必须加日志,记录接口调用行为以及执行耗时。为了实现这一需求,我们定义一个日志代理类,来代理原有业务逻辑实现
public class HelloServiceLogProxy implements HelloService { private HelloService service; public HelloServiceLogProxy(HelloService service) { this.service = service; } @Override public String say(String name, String message) { // 需求一:打印调用日志 System.out.printf("调用了HelloService#say(%s, %s)方法!%n", name, message); // 需求二:计算程序执行耗时 long time = System.currentTimeMillis(); String r = this.service.say(name, message); System.out.printf("程序执行耗时:%d毫秒!%n", System.currentTimeMillis() - time); return r; }}
测试一下代理的效果
private HelloService service; @Test public void proxy() { // II、需求微调,接口调用都必须加日志,记录接口调用的时间以及执行耗时 // 如果直接在业务代码中实现日志逻辑,那么耦合就太重了,而且如果其它业务有同样需求,不利用代码复用 // 设计模式中的代理模式刚好应对这个场景,实现一个日志代理来增强业务接口,这样就不用修改业务逻辑了 // 模拟代理类实例注入 service = new HelloServiceLogProxy(new HelloServiceImpl()); // 调用了HelloService#say(Peter, 我一天都没吃饭了)方法! // 程序执行耗时:194毫秒! r = service.say("Peter", "我一天都没吃饭了"); // Peter : 我一天都没吃饭了 System.out.println(r); }
这就是代理模式的作用,在不修改原有业务代码的基础上对功能作增强。 有人说调用的地方不是要修改实现类,还不是要修改,实际上在企业开发中并不建议直接用new的方式在业务代码中创建类实例,比如在Spring中我们通过IoC技术实现接口与实现类解耦,即使不使用Spring,我们也会使用工厂模式或者SPI等技术实现接口与实现类的解耦。
JDK动态代理
在上述案例中(静态代理),存在一个很明显的问题,当我们的需求二在应用到其它业务接口时,案例中提供的代理类无法增强其它业务接口,没有实现代码复用。针对这一点JDK提供了动态代理机制(基于反射技术),可以实现一个代理类,但与具体某个接口无关,也就是可以应用于任意接口。 我们先来实现一个动态代理类
public class LogProxyInvocationHandler implements InvocationHandler { private Object target; public LogProxyInvocationHandler(Object target) { this.target = target; } /** * 动态代理核心逻辑 * @param proxy 不知道有啥用,不能使用 * @param method 代理方法对象 * @param args 代理方法参数 * @return * @throws Throwable */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 需求一:打印调用日志(简化逻辑,忽略参数日志) System.out.printf("%s#%s方法!%n", target.getClass().getSimpleName(), method.getName()); // 需求二:计算程序执行耗时 long time = System.currentTimeMillis(); Object r = method.invoke(target, args); System.out.printf("程序执行耗时:%d毫秒!%n", System.currentTimeMillis() - time); return r; }}
实现InvocationHandler
接口即可定义一个动态代理类,下面测试一下这个代理类
private HelloService service; @Test public void dynamicProxy() { // III、上例看似完美,但实际上存在一个问题,如果其它业务也需要实现需求二,那么日志代理因为实现了HelloService接口, // 所以是无法直接给其它业务使用的,也就是说无法代码复用 // 解决这一问题的办法是使用动态代理,代理类实现需求二,但不绑定固定接口,也就是可以和任意接口配合使用,从而实现代码复用 HelloServiceImpl logic = new HelloServiceImpl(); service = (HelloService) Proxy.newProxyInstance(logic.getClass().getClassLoader(), logic.getClass().getInterfaces(), new LogProxyInvocationHandler(logic)); // HelloServiceImpl#say方法! // 程序执行耗时:211毫秒! String r = service.say("Peter", "我一天都没吃饭了"); // Peter : 我一天都没吃饭了 System.out.println(r); // 检查一个对象是否是代理对象 assertTrue(Proxy.isProxyClass(service.getClass())); // class com.sun.proxy.$Proxy4 System.out.println(service.getClass()); }
通过Proxy.newProxyInstance()
方法可以生成一个动态代理类,该方法接收三个参数,分别是:代理类的ClassLoader、代理接口(数组)、InvocationHandler实例(该实例通常会要求传入代理对象)。 通过测试代码,可以看出动态代理并不固定绑定在某一个接口上,这意味着可以代理任意接口,从而实现了代码复用。 当然上面的代码只是为了测试,实际我们并不会在业务逻辑里写这么一大段生成动态代理的代码,较好的做法是封装一个工厂类
public class JdkLogProxyFactory { public static finalT createProxyInstance(T t) { return (T) Proxy.newProxyInstance(t.getClass().getClassLoader(), t.getClass().getInterfaces(), new LogProxyInvocationHandler(t)); }}
来隐藏创建动态代理类的细节
private HelloService service; @Test public void dynamicProxy2() { // IV、上面的动态代理还需要封装一下,否则实际应用过程中写这么一大段代码并不合适 // 定义一个动态代理对象生成工厂,隐藏动态代理创建过程 service = JdkLogProxyFactory.createProxyInstance(new HelloServiceImpl()); // HelloServiceImpl#say方法! // 程序执行耗时:193毫秒! String r = service.say("Peter", "我一天都没吃饭了"); // Peter : 我一天都没吃饭了 System.out.println(r); }
CGLib动态代理
日常开发中,JDK提供的动态代理技术已足够使用,我们也提倡面向接口开发,但在有些场景下我们会需要对类进行代理,而JDK只支持对接口实现动态代理,对类做代理,我们需要使用CGLib库来实现
public class CglibLogProxyFactory { public static finalT createProxyInstance(final T target) { // 这是一个工具类 Enhancer enhancer = new Enhancer(); // 设置父类(用于动态生成一个子类) enhancer.setSuperclass(target.getClass()); // 设置回调函数 enhancer.setCallback(new MethodInterceptor() { /** * 动态代理核心方法 * @param o 和JDK中的一样,不知道意义但不能使用 * @param method 代理方法对象 * @param args 代理方法参数列表 * @param methodProxy * @return * @throws Throwable */ @Override public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { System.out.println(methodProxy); // 需求一:打印调用日志(简化逻辑,忽略参数日志) System.out.printf("%s#%s方法!%n", target.getClass().getSimpleName(), method.getName()); // 需求二:计算程序执行耗时 long time = System.currentTimeMillis(); Object r = method.invoke(target, args); System.out.printf("程序执行耗时:%d毫秒!%n", System.currentTimeMillis() - time); return r; } }); // 创建子类(代理对象) return (T) enhancer.create(); }}
实际上和JDK动态代理的代码非常相像,但CGLib可以同时对接口和类进行代理(据说性能好优于JDK的实现,这点倒是可以理解,前者直接通过字节码实现,后者通过反射实现)。 下面是使用CGLib实现的动态代理对一个未继承任何接口的类进行动态代理
@Test public void dynamicProxy3() { // V、以上动态代理实现是JDK提供实现,存在的问题是只能代理接口,如果要代理类,需要使用CGLib库 // 实际测试中发现即使方法使用protected和默认访问权限也能成功被代理 UserService service = CglibLogProxyFactory.createProxyInstance(new UserService()); // UserService#create方法! // 创建一个用户:account = Peter, password = 123456 // 程序执行耗时:0毫秒! Long id = service.create("Peter", "123456"); // 1 System.out.println(id); // CGLib的代理类不能使用Proxy.isProxyClass()方法检测 // class com.zlikun.jee.j007.UserService$$EnhancerByCGLIB$$7947b053 System.out.println(service.getClass()); }
结语
代理模式和动态代理技术是一种非常实用的模式(技术),在很多热门框架和库中都用应到,典型的像Spring、Struts等,抛开这些不说,即使日常开发中我们程序员自己也经常会用得到,所以推荐理解和掌握。
代码仓库: