Java拾遗:007 - 代理模式与动态代理

代理模式

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

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> 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> 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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值