技术开发 频道

用AspectJ增强设计模式上

    AspectJ 修饰器

    在 Hanneman 和 Kiczales 合著的论文中,他们谈到了以下这点:

    如果使用 AspectJ,那么某些模式的实现就会完全消失,因为 AspectJ 语言的构造直接实现了它们。这适用于 [修饰器]。
查看一下 Gof 书中关于使用修饰器的动机的那一节,然后您就会很清楚为什么会是这种情况了:

    修饰器把请求转发到组件,在转发前后可能执行附加动作(例如绘制边框)。透明性支持递归地嵌套修饰器,所以它支持无限数量的附加功能。
那么所给的建议是什么呢?是透明地把附加“操作”添加到任何操作上的能力?从某种意义上说,AspectJ 支持对任何方法进行修饰。为了看到它在真实系统中的作用,可以研究 AspectJ 实现的输入流读取监视。

    识别被修饰的操作

    为了正确地实现监视,方面必须识别出流上的读取操作。通过编写一个捡取读取取操作的切入点可以做到这一点:
pointcut arrayRead() : call(public int InputStream+.read(..));

    现在可以应用一些下面这样通用格式的通知:

after() returning (int bytesRead) : arrayRead() { updateMonitor(bytesRead); }

    此通知用 returning 格式公开方法调用的返回值。读取的字节数被传递给方面上的一个私有方法:updateMonitor()。该方法负责更新实际的 ProgressMonitor 的细节(稍后会有更多关于这项工作的内容)。

    公开相关上下文

    到目前为止,解决方案都很简单。但是,这表明 ProgressMonitor 类还要求做几件事才能实现它的工作。具体地说,它需要一个 GUI 组件来绑定监视对话模式。在传统的实现中可以看到这个要求:

//this is a JPanel GUI component stream = new ProgressMonitorInputStream(this, "This is gonna take a while", stream);

    要获得需要的 GUI 组件,方面必须把它绑定到切入点,通知才能使用它。清单 3 包含修订后的切入点和通知。注意,fromAComponent() 切入点利用了原始的 cflow() 切入点。切入点实际上是在说“选择作为 JComponent 上面方法执行结果的全部连接点,并公开这个组件,供通知使用。”

    清单 3. 用 cflow 将上下文环境交给监视器

pointcut arrayRead(JComponent component, InputStream is) : call(public int InputStream+.read(..)) && target(is) && fromAComponent(component); pointcut fromAComponent(JComponent component) : cflow(execution(* javax.swing.JComponent+.*(..)) && this(component)); after(JComponent component, InputStream is) returning (int bytesRead) : arrayRead(component, is) { updateMonitor(component, is, bytesRead); }

    维护状态

    为了让方面的应用面更广(也为了准确地模拟其他实现),方面必须维护状态。也就是说,它应当为每个受监视的流弹出一个惟一的进度监视器。AspectJ 提供了几个处理这个问题的选择。对于这个方面来说,非常好的选择可能是用 Map 维护每个对象的状态存储。这种技术在我实现的观察者模式中会再次出现,请注意观察!(将特定状态保存到对象的其他方法中,包括类型间声明和 pertarget/perthis 方面,但是对这些概念的考虑超出了本文的范畴。)

    要实现状态存储,首先要声明一个 WeakHashMap,它用流作为键,把监视器保存为值。可以使用 WeakHashMap,因为如果正常使用中不再需要键,那么 WeakHashMaps 不会阻止将它的键作为垃圾进行收集。这个非常好的实践可以防止由于方面持有对不活动对象的引用而造成的内存泄漏。

    然后 updateMonitor() 方法用 map 消极地初始化一个新的 IncrementMonitor。一旦该方法确定存在监视器,就会用最新的进度(read() 的返回值表示)来更新监视器。在清单 4 中,可以看到实现消极初始化和进度更新的代码,以及 IncrementMonitor 的完整代码:

    清单 4. 每个流监视器的消极初始化

//From the aspect... private void updateMonitor(JComponent component, InputStream is, int amount){ IncrementMonitor monitor = (IncrementMonitor)perStreamMonitor.get(is); if(monitor == null){ monitor = initMonitor(is, component); } monitor.increment(amount); } private IncrementMonitor initMonitor(InputStream is, JComponent component){ try { int size = is.available(); IncrementMonitor monitor = new IncrementMonitor(component, size); perStreamMonitor.put(is, monitor); return monitor; } catch (Exception e) { //...handle } } }//...end aspect public class IncrementMonitor extends ProgressMonitor{ private int counter; public IncrementMonitor(Component component, int size){ super(component, "Some Title", null, 0, size); } public void increment(int amount){ counter += amount; setProgress(counter); } }

    最后,在已经完全读取完流的时候,方面需要释放监视器。如果在这个时候按照方面的思路去思考,您就会认识到这是个机会:InputStream 可以很方便地为将要通知的方面定义一个 close() 方法,如下所示:

before(InputStream is): call(public void InputStream+.close()) && target(is) { System.out.println("Discarding monitor."); perStreamMonitor.remove(is); }

    目前,练习已经完成。但是,如果熟悉 InputStream 实现的话,那么可能会发现,我故意遗漏了一些事。必须用与其他读取方法不同的方法来处理 read() 方法 (没有参数),因为它的返回值不是读取的字节数,而是读取流中的下一个字节。随着本文的示例代码对方面进行扩展,可以处理这个限制,但是我建议您在参阅代码之前,想想自己编写方面时应当如何解决这个问题。

0
相关文章