装饰器与动态代理_用动态代理装饰

动态代理提供了一种替代的动态机制,用于实现许多常见的设计模式,包括外观,网桥,拦截器,装饰器,代理(包括远程和虚拟代理)和适配器模式。 尽管可以使用普通类而不是动态代理轻松实现所有这些模式,但在许多情况下,动态代理方法更方便,更紧凑,并且可以消除许多手写或生成的类。

代理模式

代理模式涉及创建“存根”或“代理”对象,其目的是接受请求并将请求转发给实际工作的另一个对象。 远程方法调用(RMI)使用代理模式来使在另一个JVM中执行的对象看起来像本地对象。 通过Enterprise JavaBeans(EJB)添加远程调用,安全性和事务划分; 并通过JAX-RPC Web服务使远程服务显示为本地对象。 在每种情况下,潜在的远程对象的行为都是由接口定义的,该接口本质上允许多种实现。 调用者不能(大部分情况下)知道它们仅持有对存根的引用,而不是真实对象的引用,因为它们都实现相同的接口。 存根将完成以下工作:查找真实对象,将参数编组,将其发送给真实对象,将返回值解组并将其返回给调用者。 代理可用于提供远程处理(如在RMI,EJB和JAX-RPC中),使用安全策略(EJB)包装对象,为昂贵的对象提供懒加载(EJB实体Bean)或添加诸如日志记录之类的工具。

在5.0之前的JDK中,RMI存根(及其对应的骨架)是由RMI编译器(rmic)在编译时生成的类,它是JDK工具集的一部分。 对于每个远程接口,都会生成一个存根(代理)类,该类模拟了远程对象,还生成了一个骨架对象,该对象完成了远程JVM中存根的相反工作-取消编组参数并调用实数目的。 同样,用于Web服务的JAX-RPC工具为远程Web服务生成代理类,使它们看起来像本地对象。

不管生成的存根类是作为源代码还是字节码生成,由于类似名称的类的激增,代码生成仍会增加编译过程的额外步骤,并可能造成混淆。 另一方面,动态代理机制允许在运行时创建代理对象,而无需在编译时生成存根类。 在JDK 5.0和更高版本中,RMI工具使用动态代理而不是生成的存根,结果是RMI变得更易于使用。 许多J2EE容器还使用动态代理来实现EJB。 EJB技术在很大程度上依赖于使用拦截来实现安全性和事务划分。 动态代理通过为接口上调用的所有方法提供中央控制流路径来简化拦截的实现。

动态代理机制

动态代理机制的核心是InvocationHandler接口,如清单1所示。调用处理程序的工作实际上是代表动态代理执行请求的方法调用。 调用处理程序被传递给Method对象(来自java.lang.reflect包)和要传递给方法的参数列表; 在最简单的情况下,它可以简单地调用反射方法Method.invoke()并返回结果。

清单1. InvocationHandler接口
public interface InvocationHandler {
    Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
}

每个代理都有一个关联的调用处理程序,只要调用该代理的一种方法,就会调用该处理程序。 与接口用于定义类型和类用于定义实现的一般设计原则一致,代理对象可以实现一个或多个接口,但不能实现类。 因为代理类没有可访问的名称,所以它们不能具有构造函数,因此必须由工厂创建。 清单2显示了动态代理的最简单的可能实现,该代理实现Set接口,并将所有Set方法(以及所有Object方法)分派给封装的Set实例。

清单2.包装Set的简单动态代理
public class SetProxyFactory {

    public static Set getSetProxy(final Set s) {
        return (Set) Proxy.newProxyInstance
          (s.getClass().getClassLoader(),
                new Class[] { Set.class },
                new InvocationHandler() {
                    public Object invoke(Object proxy, Method method, 
                      Object[] args) throws Throwable {
                        return method.invoke(s, args);
                    }
                });
    }
}

SetProxyFactory类包含一个静态工厂方法getSetProxy() ,该方法返回实现Set的动态代理。 代理对象确实实现了Set -调用者无法告诉(通过反射除外)返回的对象是动态代理。 SetProxyFactory返回的代理除了将方法分派到传递给工厂方法的Set实例外,不执行任何其他操作。 虽然反射代码通常很难看懂,但这里进行的工作很少,以至于很难控制流程-每当在Set代理上调用方法时,它就会分派给调用处理程序,该处理程序只是反射地调用所需的处理程序基础包装对象上的方法。 当然,完全不做任何事情的代理人会很愚蠢-还是会呢?

虚无适配器

实际上,什么都不做包装器(如SetProxyFactory )有很好的用途-它可以用于安全地将对象引用范围缩小到特定接口(或一组接口),以使调用者无法向上传递引用,从而将对象引用传递给不受信任的代码(例如插件或回调)会更安全。 清单3包含一组实现典型回调方案的类定义。 您将看到动态代理如何更方便地替换通常由手工(或由IDE提供的代码生成向导)实现的适配器模式。

清单3.典型的回调方案
public interface ServiceCallback {
    public void doCallback();
}

public interface Service {
    public void serviceMethod(ServiceCallback callback);
}

public class ServiceConsumer implements ServiceCallback {
    private Service service;

    ...
    public void someMethod() {
        ...
        service.serviceMethod(this);
    }
}

ServiceConsumer类实现ServiceCallback (这通常是支持回调的便捷方法),并将this引用传递给serviceMethod()作为回调引用。 这种方法的问题在于,没有什么可以阻止Service实现将ServiceCallback上载到ServiceConsumer并调用ServiceConsumer不希望该Service调用的方法。 有时您并不关心这种风险,但有时您会担心。 如果你这样做,你可以把回调对象的内部类,或者写一个什么也不做的适配器类(见ServiceCallbackAdapter清单4中),敷ServiceConsumerServiceCallbackAdapterServiceCallbackAdapter防止ServiceServiceCallback上播到ServiceConsumer

清单4.适配器类可以将对象安全地缩小到接口,因此它不会被恶意代码篡改
public class ServiceCallbackAdapter implements ServiceCallback {
    private final ServiceCallback cb;

    public ServiceCallbackAdapter(ServiceCallback cb) {
        this.cb = cb;
    }

    public void doCallback() {
        cb.doCallback();
    }
}

编写适配器类(例如ServiceCallbackAdapter很简单,但很乏味。 您必须为包装的接口中的每个方法编写一个转发方法。 就ServiceCallback ,只有一种方法可以实现,但是某些接口(例如Collections或JDBC接口)包含许多方法。 现代的IDE通过提供“代理方法”向导来减少编写适配器类的工作量,但是您仍然必须为要包装的每个接口编写一个适配器类,并且对于仅包含生成的类的类有些不满意码。 似乎应该有一种方法可以更紧凑地表达“不做任何事情变窄适配器模式”。

通用适配器类

清单2中SetProxyFactory类肯定比Set接口的等效适配器类更紧凑,但是它仍然仅适用于以下一个接口: Set 。 但是,通过使用泛型,您可以轻松地创建一个泛型代理工厂,该工厂可以对任何接口执行相同的操作,如清单5所示。它几乎与SetProxyFactory相同,但是可以对任何接口使用。 现在,您不必再编写狭窄的适配器类! 如果要创建一个可以安全地将对象缩小到接口T的代理对象,只需调用getProxy(T.class,object) ,您就已经拥有了一个,而没有一堆适配器类的额外getProxy(T.class,object)

清单5.通用缩小适配器工厂类
public class GenericProxyFactory {

    public static<T> T getProxy(Class<T> intf, 
      final T obj) {
        return (T) 
          Proxy.newProxyInstance(obj.getClass().getClassLoader(),
                new Class[] { intf },
                new InvocationHandler() {
                    public Object invoke(Object proxy, Method method, 
                      Object[] args) throws Throwable {
                        return method.invoke(obj, args);
                    }
                });
    }
}

作为代理的动态代理

当然,动态代理功能不仅可以将对象的类型缩小到特定的接口,还可以做更多的事情。 从清单2清单5中的简单缩小适配器到Decorator模式仅一步之遥,在该模式下,代理使用其他功能(例如安全性检查或日志记录)来包装调用。 清单6显示了一个日志记录InvocationHandler ,它除了简单地在所需的目标对象上调用该方法外,还写入一条日志消息,其中显示了调用的方法,传递的参数和返回值。 除了反射性invoke()调用之外,这里的所有代码只是生成调试消息的一部分-仍然没有那么多。 代理工厂方法的代码与GenericProxyFactory几乎相同,除了它使用LoggingInvocationHandler而不是匿名调用处理程序。

清单6.基于代理的Decorator为每个方法调用生成调试日志记录
private static class LoggingInvocationHandler<T> 
      implements InvocationHandler {
        final T underlying;

        public LoggingHandler(T underlying) {
            this.underlying = underlying;
        }

        public Object invoke(Object proxy, Method method, 
          Object[] args) throws Throwable {
            StringBuffer sb = new StringBuffer();
            sb.append(method.getName()); sb.append("(");
            for (int i=0; args != null && i<args.length; i++) {
                if (i != 0)
                    sb.append(", ");
                sb.append(args[i]);
            }
            sb.append(")");
            Object ret = method.invoke(underlying, args);
            if (ret != null) {
                sb.append(" -> "); sb.append(ret);
            }
            System.out.println(sb);
            return ret;
        }
    }

如果用日志记录代理包装HashSet并执行以下简单测试程序:

Set s = newLoggingProxy(Set.class, new HashSet());
    s.add("three");
    if (!s.contains("four"))
        s.add("four");
    System.out.println(s);

您将获得以下输出:

add(three) -> true
  contains(four) -> false
  add(four) -> true
  toString() -> [four, three]
  [four, three]

这种方法是一种在对象周围添加调试包装的好方法。 当然,这比生成委托类和手动创建许多println()语句要容易得多(并且更加通用)。 我可以进一步采用这种方法。 代替无条件生成调试输出,代理可以改为查询动态配置存储(从配置文件初始化,并且可以由JMX MBean动态修改)以确定是否实际上生成调试语句,甚至在类上也可以。类或实例的基础上。

在这一点上,我希望观众中的AOP粉丝几乎能听到“但这就是AOP的优点!” 是的,但是有多种方法可以解决任何给定的问题-仅仅因为技术可以解决问题,并不意味着它是最佳解决方案。 无论如何,动态代理方法的优点是完全在“纯Java”的范围内工作,而不是每个商店都使用(或应该使用)AOP。

动态代理作为适配器

代理还可以用作真正的适配器,从而提供对象的视图,该对象导出的接口与基础对象实现的接口不同。 调用处理程序不必将每个方法调用都分派到相同的基础对象。 它可以检查名称并将不同的方法分配给不同的对象。 作为示例,假设您有一组JavaBeans接口用于表示为属性指定getter和setter的持久性实体( PersonCompanyPurchaseOrder ),并且您正在编写一个持久化层,该层将数据库记录映射到实现这些接口的对象。 您可能没有一个通用的JavaBeans样式的代理类,而是在一个Map中存储属性,而不是为每个接口编写或生成一个类。

清单7显示了一个动态代理,该代理检查被调用方法的名称,并通过查询或修改属性映射直接实现getter和setter方法。 这个代理类现在可以实现多个JavaBeans风格的接口的对象。

清单7.将getter和setter调度到Map的动态代理类
public class JavaBeanProxyFactory {
    private static class JavaBeanProxy implements InvocationHandler {
        Map<String, Object> properties = new HashMap<String, 
          Object>();

        public JavaBeanProxy(Map<String, Object> properties) {
            this.properties.putAll(properties);
        }

        public Object invoke(Object proxy, Method method, 
          Object[] args) 
          throws Throwable {
            String meth = method.getName();
            if (meth.startsWith("get")) {
                String prop = meth.substring(3);
                Object o = properties.get(prop);
                if (o != null && !method.getReturnType().isInstance(o))
                    throw new ClassCastException(o.getClass().getName() + 
                      " is not a " + method.getReturnType().getName());
                return o;
            }
            else if (meth.startsWith("set")) {
                // Dispatch setters similarly
            }
            else if (meth.startsWith("is")) {
                // Alternate version of get for boolean properties
            }
            else {
                // Can dispatch non get/set/is methods as desired
            }
        }
    }

    public static<T> T getProxy(Class<T> intf,
      Map<String, Object> values) {
        return (T) Proxy.newProxyInstance
          (JavaBeanProxyFactory.class.getClassLoader(),
                new Class[] { intf }, new JavaBeanProxy(values));
    }
}

尽管由于反射是根据Object工作的,所以可能会损失类型安全性,但是JavaBeanProxyFactory中的getter处理“加入”了一些所需的额外类型检查,就像我在isInstance()检查getter一样。

绩效成本

如您所见,动态代理具有简化大量代码的潜力-不仅可以替换大量生成的代码,而且一个代理类可以替换多个手写或生成的代码类。 费用是多少? 由于反射性地分配方法而不是使用内置的虚拟方法分配,可能会有一些性能成本。 在早期的JDK中,反射性能很差(早期JDK中几乎所有其他产品的性能也是如此),但是反射在过去10年中变得更快。

在不涉及基准测试构建的主题的情况下,我编写了一个简单,不科学的测试程序,该程序循环,将数据填充到Set ,随机插入,查找和从Set删除元素。 我使用三个Set实现来运行它:一个没有修饰的HashSet ,一个手写的Set适配器,它仅将所有方法转发到一个基础HashSet ;以及一个基于代理的Set适配器,它也将所有方法转发到一个基础HashSet 。 每次循环迭代都会生成几个随机数,并执行一个或多个Set操作。 与原始HashSet相比,手写适配器仅产生了百分之几的性能开销(大概是由于JVM级别的有效内联缓存和硬件级别的分支预测); 代理适配器的速度明显要比原始HashSet慢,但是开销却不到两倍。

我从该实验得出的结论是,在大多数情况下,代理方法甚至对于轻量级方法也表现良好,并且随着代理操作变得更加繁重(例如远程方法调用或使用序列化,执行IO或从数据库中提取数据),代理的开销将有效地接近零。 尽管在某些情况下代理方法会带来不可接受的性能开销,但这些情况可能只占少数。

结论

动态代理是实现许多设计模式(包括Proxy,Decorator和Adapter)的功能强大且未充分利用的工具。 这些模式的基于代理的实现易于编写,更难以出错,并具有更大的通用性。 在许多情况下,一个动态代理类可以充当所有接口的装饰器或代理,而不必为每个接口编写静态类。 对于除性能要求最高的所有应用程序外,动态代理方法可能比手写或机器生成的存根方法更可取。


翻译自: https://www.ibm.com/developerworks/java/library/j-jtp08305/index.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值