博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Java拾遗:007 - 代理模式与动态代理
阅读量:7210 次
发布时间:2019-06-29

本文共 7370 字,大约阅读时间需要 24 分钟。

hot3.png

代理模式

在日常开发中我们可以会接手一些老的项目,有时连源码都没有,或者有时候我会需要对业务逻辑做一定增强(功能扩展,如:日志、事务等),这时候我们通常不能或者不建议直接修改源码(可能根本没有源码)。在设计模式中有一种模式叫代理模式可以很好的应对上述场景,也符合了,实现代码解耦(扩展部分不污染原有业务代码)。 所谓代理模式就是当我们需要增强业务逻辑时,创建一个增强的代理类,在代理类中调用原有业务逻辑实现代码,并对其作增强。 假设一场景,我们有一个业务接口

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 final 
T 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 final 
T 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等,抛开这些不说,即使日常开发中我们程序员自己也经常会用得到,所以推荐理解和掌握。

代码仓库:

转载于:https://my.oschina.net/zhanglikun/blog/1922631

你可能感兴趣的文章
vue2.0 与 bootstrap datetimepicker的结合使用
查看>>
ant design后台模板-1.前端环境搭建
查看>>
什么是npm ?
查看>>
php 中continue break exit return 的区别
查看>>
敏捷爽畅模型及其演变——Diana Larsen专访
查看>>
Bootstrap 4 正式发布,却可能生不逢时
查看>>
PWA即将推向所有Chrome平台
查看>>
修复.NET的HttpClient
查看>>
服务应该去版本化,不管是微服务还是SOA
查看>>
GitHub Draft Pull请求支持新的协作流程
查看>>
JShell:Java REPL综合指南
查看>>
为你的组织设计自己的障碍消除流程
查看>>
为什么你写的代码糟透了?
查看>>
使用Flutter一年后,这是我得到的经验
查看>>
滴滴进入寒冬期,将裁员2000人
查看>>
被批伪开源!刚刚融资6千万美元的Redis怎么了?
查看>>
专访刘刚:360手机卫士的性能监控与优化
查看>>
去哪儿网消息队列设计与实现
查看>>
MySQL 5.7中的更多改进,包括计算列
查看>>
书评与访谈:Scrum for Managers
查看>>