`

Java 理论与实践: 用动态代理进行修饰

    博客分类:
  • JAVA
 
阅读更多

动态代理为实现许多常见设计模式(包括 Facade、Bridge、Interceptor、Decorator、Proxy(包括远程和虚拟代理)和 Adapter 模式)提供了替代的动态机制。虽然这些模式不使用动态代理,只用普通的类就能够实现,但是在许多情况下,动态代理方式更方便、更紧凑,可以清除许多手写或 生成的类。

Proxy 模式

Proxy 模式中要创建“stub”或“surrogate”对象,它们的目的是接受请求并把请求转发到实际执行工作的其他对象。远程方法调用(RMI)利用 Proxy 模式,使得在其他 JVM 中执行的对象就像本地对象一样;企业 JavaBeans (EJB)利用 Proxy 模式添加远程调用、安全性和事务分界;而 JAX-RPC Web 服务则用 Proxy 模式让远程服务表现得像本地对象一样。在每一种情况中,潜在的远程对象的行为是由接口定义的,而接口本质上接受多种实现。调用者(在大多数情况下)不能区 分出它们只是持有一个对 stub 而不是实际对象的引用,因为二者实现了相同的接口;stub 的工作是查找实际的对象、封送参数、把参数发送给实际对象、解除封送返回值、把返回值返回给调用者。代理可以用来提供远程控制(就像在 RMI、EJB 和 JAX-RPC 中那样),用安全性策略包装对象(EJB)、为昂贵的对象(EJB 实体 Bean)提供惰性装入,或者添加检测工具(例如日志记录)。

在 5.0 以前的 JDK 中,RMI stub(以及它对等的 skeleton)是在编译时由 RMI 编译器(rmic)生成的类,RMI 编译器是 JDK 工具集的一部分。对于每个远程接口,都会生成一个 stub(代理)类,它代表远程对象,还生成一个 skeleton 对象,它在远程 JVM 中做与 stub 相反的工作 —— 解除封送参数并调用实际的对象。类似地,用于 Web 服务的 JAX-RPC 工具也为远程 Web 服务生成代理类,从而使远程 Web 服务看起来就像本地对象一样。

不管 stub 类是以源代码还是以字节码生成的,代码生成仍然会向编译过程添加一些额外步骤,而且因为命名相似的类的泛滥,会带来意义模糊的可能性。另一方面,动态代理 机制支持在编译时没有生成 stub 类的情况下,在运行时创建代理对象。在 JDK 5.0 及以后版本中,RMI 工具使用动态代理代替了生成的 stub,结果 RMI 变得更容易使用。许多 J2EE 容器也使用动态代理来实现 EJB。EJB 技术严重地依靠使用拦截(interception)来实现安全性和事务分界;动态代理为接口上调用的所有方法提供了集中的控制流程路径。

动态代理机制

动态代理机制的核心是 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 提供的代码生成向导)实现的 Adapter 模式。


清单 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 调用的方法。有时对这个风险并不关心 —— 但有时却关心。如果关心,那么可以把回调对象作为内部类,或者编写一个什么都不做的适配器类(请参阅清单 4 中的 ServiceCallbackAdapter )并用 ServiceCallbackAdapter 包装 ServiceConsumerServiceCallbackAdapter 防止 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 提供了“Delegate Methods”向导,降低了编写适配器类的工作量,但是仍然必须为每个想要包装的接口编写一个适配器类,而且对于只包含生成的代码的类,也有一些让人不 满意的地方。看起来应当有一种方式可以更紧凑地表示“什么也不做的限制适配器模式”。

通用适配器类

清单 2 中的 SetProxyFactory 类当然比用于 Set 的等价的适配器类更紧凑,但是它仍然只适用于一个接口:Set 。但是通过使用泛型,可以容易地创建通用的代理工厂,由它为任何接口做同样的工作,如清单 5 所示。它几乎与 SetProxyFactory 相同,但是可以适用于任何接口。现在再也不用编写限制适配器类了!如果想创建代理对象安全地把对象限制在接口 T ,只要调用 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);
                    }
                });
    }
}

 

动态代理作为 Decorator

当然,动态代理工具能做的,远不仅仅是把对象类型限制在特定接口上。从 清单 2清单 5 中简单的限制适配器到 Decorator 模式,是一个小的飞跃,在 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 的。

动态代理作为适配器

代理也可以用作真正的适配器,提供了对象的一个视图,导出与底层对象实现的接口不同的接口。调用句柄不需要把每个方法调用都分派给相同的底层对象;它可以检查名称,并把不同的方法分派给不同的对象。例如,假设有一组表示持久实体 (PersonCompanyPurchaseOrder ) 的 JavaBean 接口,指定了属性的 getter 和 setter,而且正在编写一个持久层,把数据库记录映射到实现这些接口的对象上。现在不用为每个接口编写或生成类,可以只用一个 JavaBean 风格的通用代理类,把属性保存在 Map 中。

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


清单 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 ,一个手写的、只是把所有方法转发到底层的 HashSetSet 适配器,还有一个基于代理的、也只是把所有方法转发到底层 HashSetSet 适配器。每次循环迭代都生成若干随机数,并执行一个或多个 Set 操作。手写的适配器比起原始的 HashSet 只产生很少百分比的性能负荷(大概是因为 JVM 级有效的内联缓冲和硬件级的分支预测);代理适配器则明显比原始 HashSet 慢,但是开销要少于两个量级。

我从这个试验得出的结论是:对于大多数情况,代理方式即使对轻量级方法也执行得足够好,而随着被代理的操作变得越来越重量级(例如远程方法调 用,或者使用序列化、执行 IO 或者从数据库检索数据的方法),代理开销就会有效地接近于 0。当然也存在一些代理方式的性能开销无法接受的情况,但是这些通常只是少数情况。

分享到:
评论

相关推荐

    Java理论与实践:用动态代理进行修饰

    Java 动态代理是一种强大的工具,它首次出现在JDK 1.3中,位于`java.lang.reflect`包中。动态代理允许程序创建代理对象,这些对象可以实现一个或多个已知接口,并通过反射来处理方法调用。这种方法使得在运行时能够...

    java笔试认真学习

    九、反射与动态代理 1. 反射:理解反射机制,如何在运行时获取类的信息并进行操作。 2. 动态代理:了解动态代理的概念,以及Java动态代理API的使用。 通过这些基础知识的学习和深入理解,不仅能够应对Java笔试,还...

    JAVA面试大全(适合应届生)

    "Java笔试试题汇总(共150题).doc"这份文档应该包含了上述各个知识点的实践题目,通过解答这些题目,应届生可以巩固理论知识,提高实战能力,为面试做好充分准备。记得结合实际项目经验,理论联系实际,才能在面试...

    java课件完整版

    2. **面向对象编程**:Java是一种纯面向对象的语言,因此对类、对象、接口、抽象类、构造器以及访问修饰符的理解至关重要。同时,深入理解继承、多态性和组合的概念,有助于设计出更加灵活和可维护的代码。 3. **...

    JAVA开发实战经典PPT

    8. **反射机制**:反射是Java的一个强大特性,允许在运行时动态地获取类的信息并操作类的对象,PPT会解释如何使用Class类、Constructor、Method和Field进行反射操作。 9. **Java Swing和JavaFX**:这两部分可能会...

    java 课件 大连理工大学软件学院

    7. **反射**:Java反射机制允许在运行时动态获取类的信息并操作对象,是许多高级功能如插件系统、序列化、动态代理的基础。 8. **Java虚拟机(JVM)**:理解JVM的工作原理,包括内存模型、类加载机制、垃圾回收等,...

    Java编程基础、应用与实例

    11. **反射机制**:介绍如何在运行时动态获取类的信息,创建和调用对象,以及动态代理。 12. **Java API的使用**:包括常用的类库,如Math、Date、Calendar、Collections等。 13. **设计模式**:讲解常见的设计...

    JAVA课程总复习

    此外,"Java知识点总结.doc"是一个Word文档,它可能是对PPT内容的补充或详细解释,可能包括更深入的理论讲解、代码示例、练习题和解题思路,帮助读者巩固理解和实践能力。 总的来说,"JAVA课程总复习"是一个全面的...

    公司Java笔试题真实照片.zip

    - **动态代理**:掌握动态代理的原理和应用场景。 8. **设计模式** - **单例模式**:了解不同实现单例模式的方法,如饿汉式、懒汉式、双重检查锁定等。 - **工厂模式**:理解简单工厂、工厂方法和抽象工厂模式的...

    java基础思维导图

    - **动态代理**:使用Java动态代理实现运行时创建代理对象,提供AOP(面向切面编程)的基础。 8. **Java标准库** - **常用类库**:如String、StringBuilder、Arrays、Collections等,学习如何高效地操作字符串...

    java实验的题目

    14. **设计模式**:了解常见的设计模式,如单例、工厂、观察者、装饰器、代理等,并尝试在实践中应用。 在实验过程中,每个题目都会针对上述一个或多个知识点进行设计,通过实际操作来巩固理论知识。解决这些问题...

    Java初级中级思维导图资源

    Java编程语言是软件开发领域广泛使用的工具,尤其在企业级应用中占据主导地位。这个"Java初级中级思维导图资源"旨在...在学习过程中,结合实际项目实践,将理论知识转化为实际技能,对于成为合格的Java开发者至关重要。

    Java课件(初学者)

    Java是世界上最流行的编程语言之一,尤其在企业级应用开发领域占据主导地位。"Java课件(初学者)"是一套...在实际学习过程中,结合实际编程练习和项目实践,理论与实践相结合,将有助于更好地理解和掌握这些知识点。

    Java华为面试真题

    Java华为面试真题是针对Java开发者在应聘华为公司职位时可能会遇到的问题集合。这些题目涵盖了Java编程语言的基础、进阶以及在实际项目中...在准备面试时,除了理论知识外,实践经验、问题解决能力和项目经验同样重要。

    Java 经典教材课件(清华版)

    7. **Java高级特性**:这包括反射、注解、动态代理等进阶话题。反射允许在运行时检查类的信息,注解可以提供元数据,而动态代理则用于创建代理对象以实现特定功能。 8. **Java EE技术**:虽然标题未明确提及,但...

    《MLDN出品JAVA风暴JAVA学习的终极资料》JAVASE压缩包

    12. **实战项目**:为了帮助学习者巩固理论知识,压缩包可能包含一些小规模的Java SE项目,比如命令行应用程序、简单的桌面应用或游戏,让学习者有机会将所学应用于实践。 通过这份《MLDN出品JAVA风暴JAVA学习的...

    java源码1000例java源码1000例

    Java编程语言是面向对象的、跨平台的编程语言,广泛应用于企业级应用开发、Web服务、移动应用等各个领域。...同时,这也是一种有效的学习方法,通过实例驱动,理论与实践相结合,有助于巩固和拓展编程知识。

    java代码习题.zip

    9. **CH12 - 高级话题**:可能涵盖反射、注解、动态代理、JVM内存模型等Java的高级特性和用法。 10. **CH15 - 网络编程**:讲解Java如何进行网络通信,包括Socket编程、HTTP协议的实现,以及网络服务的创建。 这些...

    java面试题及其答案

    - 代理模式:静态代理和动态代理(JDK动态代理和CGLIB动态代理)。 这些知识点涵盖了Java面试的大部分重点,深入理解和掌握这些内容将有助于你在面试中表现出色。在准备面试时,不仅要理解理论,还要通过编写代码...

Global site tag (gtag.js) - Google Analytics