GHawk

#

EJB 异常处理的最佳做法

http://www-128.ibm.com/developerworks/cn/java/j-ejbexcept/

EJB 异常处理的最佳做法

学习在基于 EJB 的系统上编写可以更快解决问题的代码

developerWorks
文档选项
将此页作为电子邮件发送

将此页作为电子邮件发送

未显示需要 JavaScript 的文档选项


对此页的评价

帮助我们改进这些内容


级别: 初级

Srikanth Shenoy, J2EE 顾问

2002 年 5 月 05 日

随着 J2EE 成为企业开发平台之选,越来越多基于 J2EE 的应用程序将投入生产。J2EE 平台的重要组件之一是 Enterprise JavaBean(EJB)API。J2EE 和 EJB 技术一起提供了许多优点,但随之而来的还有一些新的挑战。特别是企业系统,其中的任何问题都必须快速得到解决。在本文中,企业 Java 编程老手 Srikanth Shenoy 展现了他在 EJB 异常处理方面的最佳做法,这些做法可以更快解决问题。

在 hello-world 情形中,异常处理非常简单。每当碰到某个方法的异常时,就捕获该异常并打印堆栈跟踪或者声明这个方法抛出异常。不幸的是,这种办法不足以处理现实中出现的各种类型的异常。在生产系统中,当有异常抛出时,很可能是最终用户无法处理他或她的请求。当发生这样的异常时,最终用户通常希望能这样:

  • 有一条清楚的消息表明已经发生了一个错误
  • 有一个唯一的错误号,他可以据此访问可方便获得的客户支持系统
  • 问题快速得到解决,并且可以确信他的请求已经得到处理,或者将在设定的时间段内得到处理

理想情况下,企业级系统将不仅为客户提供这些基本的服务,还将准备好一些必要的后端机制。举例来说,客户服务小组应该收到即时的错误通知,以便在客户打电话求助之前服务代表就能意识到问题。此外,服务代表应该能够交叉引用用户的唯一错误号和产品日志,从而快速识别问题 ― 最好是能把问题定位到确切的行号或确切的方法。为了给最终用户和支持小组提供他们需要的工具和服务,在构建一个系统时,您就必须对系统被部署后可能出问题的所有地方心中有数。

在本文中,我们将谈谈基于 EJB 的系统中的异常处理。我们将从回顾异常处理的基础知识开始,包括日志实用程序的使用,然后,很快就转入对 EJB 技术如何定义和管理不同类型的异常进行更详细的讨论。此后,我们将通过一些代码示例来研究一些常见的异常处理解决方案的优缺点,我还将展示我自己在充分利用 EJB 异常处理方面的最佳做法。

请注意,本文假设您熟悉 J2EE 和 EJB 技术。您应理解实体 bean 和会话 bean 的差异。如果您对 bean 管理的持久性(bean-managed persistence(BMP))和容器管理的持久性(container-managed persistence(CMP))在实体 bean 上下文中是什么意思稍有了解,也是有帮助的。请参阅 参考资料部分了解关于 J2EE 和 EJB 技术的更多信息。

异常处理基础知识

解决系统错误的第一步是建立一个与生产系统具有相同构造的测试系统,然后跟踪导致抛出异常的所有代码,以及代码中的所有不同分支。在分布式应用程序中,很可能是调试器不工作了,所以,您可能将用 System.out.println() 方法跟踪异常。 System.out.println 尽管很方便,但开销巨大。在磁盘 I/O 期间, System.out.println 对 I/O 处理进行同步,这极大降低了吞吐量。在缺省情况下,堆栈跟踪被记录到控制台。但是,在生产系统中,浏览控制台以查看异常跟踪是行不通的。而且,不能保证堆栈跟踪会显示在生产系统中,因为,在 NT 上,系统管理员可以把 System.outSystem.err 映射到 ' ' ,在 UNIX 上,可以映射到 dev/null 。此外,如果您把 J2EE 应用程序服务器作为 NT 服务运行,甚至不会有控制台。即使您把控制台日志重定向到一个输出文件,当产品 J2EE 应用程序服务器重新启动时,这个文件很可能也将被重写。

异常处理的原则

以下是一些普遍接受的异常处理原则:

  1. 如果无法处理某个异常,那就不要捕获它。
  2. 如果捕获了一个异常,请不要胡乱处理它。
  3. 尽量在靠近异常被抛出的地方捕获异常。
  4. 在捕获异常的地方将它记录到日志中,除非您打算将它重新抛出。
  5. 按照您的异常处理必须多精细来构造您的方法。
  6. 需要用几种类型的异常就用几种,尤其是对于应用程序异常。

第 1 点显然与第 3 点相抵触。实际的解决方案是以下两者的折衷:您在距异常被抛出多近的地方将它捕获;在完全丢失原始异常的意图或内容之前,您可以让异常落在多远的地方。

:尽管这些原则的应用遍及所有 EJB 异常处理机制,但它们并不是特别针对 EJB 异常处理的。

由于以上这些原因,把代码组装成产品并同时包含 System.out.println 并不是一种选择。在测试期间使用 System.out.println ,然后在形成产品之前除去 System.out.println 也不是上策,因为这样做意味着您的产品代码与测试代码运行得不尽相同。您需要的是一种声明控制日志机制,以使您的测试代码和产品代码相同,并且当记录日志以声明方式关闭时,给产品带来的性能开销最小。

这里的解决方案显然是使用一个日志实用程序。采用恰当的编码约定,日志实用程序将负责精确地记录下任何类型的消息,不论是系统错误还是一些警告。所以,我们将在进一步讲述之前谈谈日志实用程序。

日志领域:鸟瞰

每个大型应用程序在开发、测试及产品周期中都使用日志实用程序。在今天的日志领域中,有几个角逐者,其中有两个广为人知。一个是 Log4J,它是来自 Apache 的 Jakarta 的一个开放源代码的项目。另一个是 J2SE 1.4 捆绑提供的,它是最近刚加入到这个行列的。我们将使用 Log4J 说明本文所讨论的最佳做法;但是,这些最佳做法并不特别依赖于 Log4J。

Log4J 有三个主要组件:layout、appender 和 category。 Layou代表消息被记录到日志中的格式。 appender是消息将被记录到的物理位置的别名。而 category则是有名称的实体:您可以把它当作是日志的句柄。layout 和 appender 在 XML 配置文件中声明。每个 category 带有它自己的 layout 和 appender 定义。当您获取了一个 category 并把消息记录到它那里时,消息在与该 category 相关联的各个 appender 处结束,并且所有这些消息都将以 XML 配置文件中指定的 layout 格式表示。

Log4J 给消息指定四种优先级:它们是 ERROR、WARN、INFO 和 DEBUG。为便于本文的讨论,所有异常都以具有 ERROR 优先级记录。当记录本文中的一个异常时,我们将能够找到获取 category(使用 Category.getInstance(String name) 方法)的代码,然后调用方法 category.error() (它与具有 ERROR 优先级的消息相对应)。

尽管日志实用程序能帮助我们把消息记录到适当的持久位置,但它们并不能根除问题。它们不能从产品日志中精确找出某个客户的问题报告;这一便利技术留给您把它构建到您正在开发的系统中。

要了解关于 Log4J 日志实用程序或 J2SE 所带的日志实用程序的更多信息,请参阅 参考资料部分。

异常的类别

异常的分类有不同方式。这里,我们将讨论从 EJB 的角度如何对异常进行分类。EJB 规范将异常大致分成三类:

  • JVM 异常:这种类型的异常由 JVM 抛出。 OutOfMemoryError 就是 JVM 异常的一个常见示例。对 JVM 异常您无能为力。它们表明一种致命的情况。唯一得体的退出办法是停止应用程序服务器(可能要增加硬件资源),然后重新启动系统。
  • 应用程序异常:应用程序异常是一种定制异常,由应用程序或第三方的库抛出。这些本质上是受查异常(checked exception);它们预示了业务逻辑中的某个条件尚未满足。在这样的情况下,EJB 方法的调用者可以得体地处理这种局面并采用另一条备用途径。
  • 系统异常:在大多数情况下,系统异常由 JVM 作为 RuntimeException 的子类抛出。例如, NullPointerExceptionArrayOutOfBoundsException 将因代码中的错误而被抛出。另一种类型的系统异常在系统碰到配置不当的资源(例如,拼写错误的 JNDI 查找(JNDI lookup))时发生。在这种情况下,系统就将抛出一个受查异常。捕获这些受查系统异常并将它们作为非受查异常(unchecked exception)抛出颇有意义。最重要的规则是,如果您对某个异常无能为力,那么它就是一个系统异常并且应当作为非受查异常抛出。

受查异常是一个作为 java.lang.Exception 的子类的 Java 类。通过从 java.lang.Exception 派生子类,就强制您在编译时捕获这个异常。相反地, 非受查异常则是一个作为 java.lang.RuntimeException 的子类的 Java 类。从 java.lang.RuntimeException 派生子类确保了编译器不会强制您捕获这个异常。



回页首


EJB 容器怎样处理异常

EJB 容器拦截 EJB 组件上的每一个方法调用。结果,方法调用中发生的每一个异常也被 EJB 容器拦截到。EJB 规范只处理两种类型的异常:应用程序异常和系统异常。

EJB 规范把 应用程序异常定义为在远程接口中的方法说明上声明的任何异常(而不是 RemoteException )。应用程序异常是业务工作流中的一种特殊情形。当这种类型的异常被抛出时,客户机会得到一个恢复选项,这个选项通常是要求以一种不同的方式处理请求。不过,这并不意味着任何在远程接口方法的 throws 子句中声明的非受查异常都会被当作应用程序异常对待。EJB 规范明确指出,应用程序异常不应继承 RuntimeException 或它的子类。

当发生应用程序异常时,除非被显式要求(通过调用关联的 EJBContext 对象的 setRollbackOnly() 方法)回滚事务,否则 EJB 容器就不会这样做。事实上,应用程序异常被保证以它原本的状态传送给客户机:EJB 容器绝不会以任何方式包装或修改异常。

系统异常被定义为受查异常或非受查异常,EJB 方法不能从这种异常恢复。当 EJB 容器拦截到非受查异常时,它会回滚事务并执行任何必要的清理工作。接着,它把该非受查异常包装到 RemoteException 中,然后抛给客户机。这样,EJB 容器就把所有非受查异常作为 RemoteException (或者作为其子类,例如 TransactionRolledbackException )提供给客户机。

对于受查异常的情况,容器并不会自动执行上面所描述的内务处理。要使用 EJB 容器的内部内务处理,您将必须把受查异常作为非受查异常抛出。每当发生受查系统异常(如 NamingException )时,您都应该通过包装原始的异常抛出 javax.ejb.EJBException 或其子类。因为 EJBException 本身是非受查异常,所以不需要在方法的 throws 子句中声明它。EJB 容器捕获 EJBException 或其子类,把它包装到 RemoteException 中,然后把 RemoteException 抛给客户机。

虽然系统异常由应用程序服务器记录(这是 EJB 规范规定的),但记录格式将因应用程序服务器的不同而异。为了访问所需的统计信息,企业常常需要对所生成的日志运行 shell/Perl 脚本。为了确保记录格式的统一,在您的代码中记录异常会更好些。

:EJB 1.0 规范要求把受查系统异常作为 RemoteException 抛出。从 EJB 1.1 规范起规定 EJB 实现类绝不应抛出 RemoteException



回页首


常见的异常处理策略

如果没有异常处理策略,项目小组的不同开发者很可能会编写以不同方式处理异常的代码。由于同一个异常在系统的不同地方可能以不同的方式被描述和处理,所以,这至少会使产品支持小组感到迷惑。缺乏策略还会导致在整个系统的多个地方都有记录。日志应该集中起来或者分成几个可管理的单元。理想的情况是,应在尽可能少的地方记录异常日志,同时不损失内容。在这一部分及其后的几个部分,我将展示可以在整个企业系统中以统一的方式实现的编码策略。您可以从 参考资料部分下载本文开发的实用程序类。

清单 1 显示了来自会话 EJB 组件的一个方法。这个方法删除某个客户在特定日期前所下的全部订单。首先,它获取 OrderEJB 的 Home 接口。接着,它取回某个特定客户的所有订单。当它碰到在某个特定日期之前所下的订单时,就删除所订购的商品,然后删除订单本身。请注意,抛出了三个异常,显示了三种常见的异常处理做法。(为简单起见,假设编译器优化未被使用。)


清单 1. 三种常见的异常处理做法

100  try {
101    OrderHome homeObj = EJBHomeFactory.getInstance().getOrderHome();
102    Collection orderCollection = homeObj.findByCustomerId(id);
103    iterator orderItter = orderCollection.iterator();
104    while (orderIter.hasNext()) {
105      Order orderRemote = (OrderRemote) orderIter.getNext();
106      OrderValue orderVal = orderRemote.getValue();
107      if (orderVal.getDate() < "mm/dd/yyyy") {
108        OrderItemHome itemHome = 
              EJBHomeFactory.getInstance().getItemHome();
109        Collection itemCol = itemHome.findByOrderId(orderId)
110        Iterator itemIter = itemCol.iterator();
111        while (itemIter.hasNext()) {
112          OrderItem item = (OrderItem) itemIter.getNext();
113          item.remove();
114        }
115        orderRemote.remove();
116      }
117    }
118  } catch (NamingException ne) {
119    throw new EJBException("Naming Exception occurred");
120  } catch (FinderException fe) {
121    fe.printStackTrace();
122    throw new EJBException("Finder Exception occurred");
123  } catch (RemoteException re) {
124    re.printStackTrace();
125    //Some code to log the message
126    throw new EJBException(re);
127  }

现在,让我们用上面所示的代码来研究一下所展示的三种异常处理做法的缺点。

抛出/重抛出带有出错消息的异常
NamingException 可能发生在行 101 或行 108。当发生 NamingException 时,这个方法的调用者就得到 RemoteException 并向后跟踪该异常到行 119。调用者并不能告知 NamingException 实际是发生在行 101 还是行 108。由于异常内容要直到被记录了才能得到保护,所以,这个问题的根源很难查出。在这种情形下,我们就说异常的内容被“吞掉”了。正如这个示例所示,抛出或重抛出一个带有消息的异常并不是一种好的异常处理解决办法。

记录到控制台并抛出一个异常
FinderException 可能发生在行 102 或 109。不过,由于异常被记录到控制台,所以仅当控制台可用时调用者才能向后跟踪到行 102 或 109。这显然不可行,所以异常只能被向后跟踪到行 122。这里的推理同上。

包装原始的异常以保护其内容
RemoteException 可能发生在行 102、106、109、113 或 115。它在行 123 的 catch 块被捕获。接着,这个异常被包装到 EJBException 中,所以,不论调用者在哪里记录它,它都能保持完整。这种办法比前面两种办法更好,同时演示了没有日志策略的情况。如果 deleteOldOrders() 方法的调用者记录该异常,那么将导致重复记录。而且,尽管有了日志记录,但当客户报告某个问题时,产品日志或控制台并不能被交叉引用。



回页首


EJB 异常处理探试法

EJB 组件应抛出哪些异常?您应将它们记录到系统中的什么地方?这两个问题盘根错结、相互联系,应该一起解决。解决办法取决于以下因素:

  • 您的 EJB 系统设计:在良好的 EJB 设计中,客户机绝不调用实体 EJB 组件上的方法。多数实体 EJB 方法调用发生在会话 EJB 组件中。如果您的设计遵循这些准则,则您应该用会话 EJB 组件来记录异常。如果客户机直接调用了实体 EJB 方法,则您还应该把消息记录到实体 EJB 组件中。然而,存在一个难题:相同的实体 EJB 方法可能也会被会话 EJB 组件调用。在这种情形下,如何避免重复记录呢?类似地,当一个会话 EJB 组件调用其它实体 EJB 方法时,您如何避免重复记录呢?很快我们就将探讨一种处理这两种情况的通用解决方案。(请注意,EJB 1.1 并未从体系结构上阻止客户机调用实体 EJB 组件上的方法。在 EJB 2.0 中,您可以通过为实体 EJB 组件定义本地接口规定这种限制。)

  • 计划的代码重用范围:这里的问题是您是打算把日志代码添加到多个地方,还是打算重新设计、重新构造代码来减少日志代码。

  • 您要为之服务的客户机的类型:考虑您是将为 J2EE Web 层、单机 Java 应用程序、PDA 还是将为其它客户机服务是很重要的。Web 层设计有各种形状和大小。如果您在使用命令(Command)模式,在这个模式中,Web 层通过每次传入一个不同的命令调用 EJB 层中的相同方法,那么,把异常记录到命令在其中执行的 EJB 组件中是很有用的。在多数其它的 Web 层设计中,把异常记录到 Web 层本身要更容易,也更好,因为您需要把异常日志代码添加到更少的地方。如果您的 Web 层和 EJB 层在同一地方并且不需要支持任何其它类型的客户机,那么就应该考虑后一种选择。

  • 您将处理的异常的类型(应用程序或系统):处理应用程序异常与处理系统异常有很大不同。系统异常的发生不受 EJB 开发者意图的控制。因为系统异常的含义不清楚,所以内容应指明异常的上下文。您已经看到了,通过对原始异常进行包装使这个问题得到了最好的处理。另一方面,应用程序异常是由 EJB 开发者显式抛出的,通常包装有一条消息。因为应用程序异常的含义清楚,所以没有理由要保护它的上下文。这种类型的异常不必记录到 EJB 层或客户机层;它应该以一种有意义的方式提供给最终用户,带上指向所提供的解决方案的另一条备用途径。系统异常消息没必要对最终用户很有意义。


回页首


处理应用程序异常

在这一部分及其后的几个部分中,我们将更仔细地研究用 EJB 异常处理应用程序异常和系统异常,以及 Web 层设计。作为这个讨论的一部分,我们将探讨处理从会话和实体 EJB 组件抛出的异常的不同方式。

实体 EJB 组件中的应用程序异常
清单 2 显示了实体 EJB 的一个 ejbCreate() 方法。这个方法的调用者传入一个 OrderItemValue 并请求创建一个 OrderItem 实体。因为 OrderItemValue 没有名称,所以抛出了 CreateException


清单 2. 实体 EJB 组件中的样本 ejbCreate() 方法

public Integer ejbCreate(OrderItemValue value) throws CreateException {
    if (value.getItemName() == null) {
      throw new CreateException("Cannot create Order without a name");
    }
    ..
    ..
    return null;
}

清单 2 显示了 CreateException 的一个很典型的用法。类似地,如果方法的输入参数的值不正确,则查找程序方法将抛出 FinderException

然而,如果您在使用容器管理的持久性(CMP),则开发者无法控制查找程序方法,从而 FinderException 永远不会被 CMP 实现抛出。尽管如此,在 Home 接口的查找程序方法的 throws 子句中声明 FinderException 还是要更好一些。 RemoveException 是另一个应用程序异常,它在实体被删除时被抛出。

从实体 EJB 组件抛出的应用程序异常基本上限定为这三种类型( CreateExceptionFinderExceptionRemoveException )及它们的子类。多数应用程序异常都来源于会话 EJB 组件,因为那里是作出智能决策的地方。实体 EJB 组件一般是哑类,它们的唯一职责就是创建和取回数据。

会话 EJB 组件中的应用程序异常
清单 3 显示了来自会话 EJB 组件的一个方法。这个方法的调用者设法订购 n 件某特定类型的某商品。 SessionEJB() 方法计算出仓库中的数量不够,于是抛出 NotEnoughStockExceptionNotEnoughStockException 适用于特定于业务的场合;当抛出了这个异常时,调用者会得到采用另一个备用途径的建议,让他订购更少数量的商品。


清单 3. 会话 EJB 组件中的样本容器回调方法

public ItemValueObject[] placeOrder(int n, ItemType itemType) throws
NotEnoughStockException {

    //Check Inventory.
    Collection orders = ItemHome.findByItemType(itemType);
    if (orders.size() < n) {
      throw NotEnoughStockException("Insufficient stock for " + itemType);
    }
}



回页首


处理系统异常

系统异常处理是比应用程序异常处理更为复杂的论题。由于会话 EJB 组件和实体 EJB 组件处理系统异常的方式相似,所以,对于本部分的所有示例,我们都将着重于实体 EJB 组件,不过请记住,其中的大部分示例也适用于处理会话 EJB 组件。

当引用其它 EJB 远程接口时,实体 EJB 组件会碰到 RemoteException ,而查找其它 EJB 组件时,则会碰到 NamingException ,如果使用 bean 管理的持久性(BMP),则会碰到 SQLException 。与这些类似的受查系统异常应该被捕获并作为 EJBException 或它的一个子类抛出。原始的异常应被包装起来。清单 4 显示了一种处理系统异常的办法,这种办法与处理系统异常的 EJB 容器的行为一致。通过包装原始的异常并在实体 EJB 组件中将它重新抛出,您就确保了能够在想记录它的时候访问该异常。


清单 4. 处理系统异常的一种常见方式

try {
    OrderHome orderHome = EJBHomeFactory.getInstance().getOrderHome();
    Order order = orderHome.findByPrimaryKey(Integer id);
} catch (NamingException ne) {
    throw new EJBException(ne);
} catch (SQLException se) {
    throw new EJBException(se);
} catch (RemoteException re) {
    throw new EJBException(re);
}

避免重复记录

通常,异常记录发生在会话 EJB 组件中。但如果直接从 EJB 层外部访问实体 EJB 组件,又会怎么样呢?要是这样,您就不得不在实体 EJB 组件中记录异常并抛出它。这里的问题是,调用者没办法知道异常是否已经被记录,因而很可能再次记录它,从而导致重复记录。更重要的是,调用者没办法访问初始记录时所生成的唯一的标识。任何没有交叉引用机制的记录都是毫无用处的。

请考虑这种最糟糕的情形:单机 Java 应用程序访问了实体 EJB 组件中的一个方法 foo() 。在一个名为 bar() 的会话 EJB 方法中也访问了同一个方法。一个 Web 层客户机调用会话 EJB 组件的方法 bar() 并也记录了该异常。如果当从 Web 层调用会话 EJB 方法 bar() 时在实体 EJB 方法 foo() 中发生了一个异常,则该异常将被记录到三个地方:先是在实体 EJB 组件,然后是在会话 EJB 组件,最后是在 Web 层。而且,没有一个堆栈跟踪可以被交叉引用!

幸运的是,解决这些问题用常规办法就可以很容易地做到。您所需要的只是一种机制,使调用者能够:

  • 访问唯一的标识
  • 查明异常是否已经被记录了

您可以派生 EJBException 的子类来存储这样的信息。清单 5 显示了 LoggableEJBException 子类:


清单 5. LoggableEJBException ― EJBException 的一个子类

public class LoggableEJBException extends EJBException {
    protected boolean isLogged;
    protected String uniqueID;

    public LoggableEJBException(Exception exc) {
	super(exc);
	isLogged = false;
	uniqueID = ExceptionIDGenerator.getExceptionID();
    }

	..
	..
}

LoggableEJBException 有一个指示符标志( isLogged ),用于检查异常是否已经被记录了。每当捕获一个 LoggableEJBException 时,看一下该异常是否已经被记录了( isLogged == false )。如果 isLogged 为 false,则记录该异常并把标志设置为 true

ExceptionIDGenerator 类用当前时间和机器的主机名为异常生成唯一的标识。如果您喜欢,也可以用有想象力的算法来生成这个唯一的标识。如果您在实体 EJB 组件中记录了异常,则这个异常将不会在别的地方被记录。如果您没有记录就在实体 EJB 组件中抛出了 LoggableEJBException ,则这个异常将被记录到会话 EJB 组件中,但不记录到 Web 层中。

单 6 显示了使用这一技术重写后的清单 4。您还可以继承 LoggableException 以适合于您的需要(通过给异常指定错误代码等)。


清单 6. 使用 LoggableEJBException 的异常处理

try {
    OrderHome orderHome = EJBHomeFactory.getInstance().getOrderHome();
    Order order = orderHome.findByPrimaryKey(Integer id);
} catch (NamingException ne) {
    throw new LoggableEJBException(ne);
} catch (SQLException se) {
    throw new LoggableEJBException(se);
} catch (RemoteException re) {
    Throwable t = re.detail;
     if (t != null && t instanceof Exception) {
       throw new LoggableEJBException((Exception) re.detail);
     }  else {
       throw new LoggableEJBException(re);
     }
}

记录 RemoteException

从清单 6 中,您可以看到 naming 和 SQL 异常在被抛出前被包装到了 LoggableEJBException 中。但 RemoteException 是以一种稍有不同 ― 而且要稍微花点气力 ― 的方式处理的。
会话 EJB 组件中的系统异常

如果您决定记录会话 EJB 异常,请使用 清单 7所示的记录代码;否则,请抛出异常,如 清单 6所示。您应该注意到,会话 EJB 组件处理异常可有一种与实体 EJB 组件不同的方式:因为大多数 EJB 系统都只能从 Web 层访问,而且会话 EJB 可以作为 EJB 层的虚包,所以,把会话 EJB 异常的记录推迟到 Web 层实际上是有可能做到的。

它之所以不同,是因为在 RemoteException 中,实际的异常将被存储到一个称为 detail (它是 Throwable 类型的)的公共属性中。在大多数情况下,这个公共属性保存有一个异常。如果您调用 RemoteExceptionprintStackTrace ,则除打印 detail 的堆栈跟踪之外,它还会打印异常本身的堆栈跟踪。您不需要像这样的 RemoteException 的堆栈跟踪。

为了把您的应用程序代码从错综复杂的代码(例如 RemoteException 的代码)中分离出来,这些行被重新构造成一个称为 ExceptionLogUtil 的类。有了这个类,您所要做的只是每当需要创建 LoggableEJBException 时调用 ExceptionLogUtil.createLoggableEJBException(e) 。请注意,在清单 6 中,实体 EJB 组件并没有记录异常;不过,即便您决定在实体 EJB 组件中记录异常,这个解决方案仍然行得通。清单 7 显示了实体 EJB 组件中的异常记录:


清单 7. 实体 EJB 组件中的异常记录

try {
    OrderHome orderHome = EJBHomeFactory.getInstance().getOrderHome();
    Order order = orderHome.findByPrimaryKey(Integer id);
} catch (RemoteException re) {
    LoggableEJBException le = 
       ExceptionLogUtil.createLoggableEJBException(re);
    String traceStr = StackTraceUtil.getStackTrace(le);
    Category.getInstance(getClass().getName()).error(le.getUniqueID() +
":" + traceStr);
    le.setLogged(true);
    throw le;
}

您在清单 7 中看到的是一个非常简单明了的异常记录机制。一旦捕获受查系统异常就创建一个新的 LoggableEJBException 。接着,使用类 StackTraceUtil 获取 LoggableEJBException 的堆栈跟踪,把它作为一个字符串。然后,使用 Log4J category 把该字符串作为一个错误加以记录。

StackTraceUtil 类的工作原理

在清单 7 中,您看到了一个新的称为 StackTraceUtil 的类。因为 Log4J 只能记录 String 消息,所以这个类负责解决把堆栈跟踪转换成 String 的问题。清单 8 说明了 StackTraceUtil 类的工作原理:


清单 8. StackTraceUtil 类


public class StackTraceUtil {

public static String getStackTrace(Exception e)
      {
          StringWriter sw = new StringWriter();
          PrintWriter pw = new PrintWriter(sw);
          return sw.toString();
      }
      ..
      ..
}

java.lang.Throwable 中缺省的 printStackTrace() 方法把出错消息记录到 System.errThrowable 还有一个重载的 printStackTrace() 方法,它把出错消息记录到 PrintWriterPrintStream 。上面的 StackTraceUtil 中的方法把 StringWriter 包装到 PrintWriter 中。当 PrintWriter 包含有堆栈跟踪时,它只是调用 StringWritertoString() ,以获取该堆栈跟踪的 String 表示。



回页首


Web 层的 EJB 异常处理

在 Web 层设计中,把异常记录机制放到客户机端往往更容易也更高效。要能做到这一点,Web 层就必须是 EJB 层的唯一客户机。此外,Web 层必须建立在以下模式或框架之一的基础上:

  • 模式:业务委派(Business Delegate)、FrontController 或拦截过滤器(Intercepting Filter)
  • 框架:Struts 或任何包含层次结构的类似于 MVC 框架的框架

为什么异常记录应该在客户机端上发生呢?嗯,首先,控制尚未传到应用程序服务器之外。所谓的客户机层在 J2EE 应用程序服务器本身上运行,它由 JSP 页、servlet 或它们的助手类组成。其次,在设计良好的 Web 层中的类有一个层次结构(例如:在业务委派(Business Delegate)类、拦截过滤器(Intercepting Filter)类、http 请求处理程序(http request handler)类和 JSP 基类(JSP base class)中,或者在 Struts Action 类中),或者 FrontController servlet 形式的单点调用。这些层次结构的基类或者 Controller 类中的中央点可能包含有异常记录代码。对于基于会话 EJB 记录的情况,EJB 组件中的每一个方法都必须具有记录代码。随着业务逻辑的增加,会话 EJB 方法的数量也会增加,记录代码的数量也会增加。Web 层系统将需要更少的记录代码。如果您的 Web 层和 EJB 层在同一地方并且不需要支持任何其它类型的客户机,那么您应该考虑这一备用方案。不管怎样,记录机制不会改变;您可以使用与前面的部分所描述的相同技术。



回页首


真实世界的复杂性

到现在为止,您已经看到了简单情形的会话和实体 EJB 组件的异常处理技术。然而,应用程序异常的某些组合可能会更令人费解,并且有多种解释。清单 9 显示了一个示例。 OrderEJBejbCreate() 方法试图获取 CustomerEJB 的一个远程引用,这会导致 FinderExceptionOrderEJBCustomerEJB 都是实体 EJB 组件。您应该如何解释 ejbCreate() 中的这个 FinderException 呢?是把它当作应用程序异常对待呢(因为 EJB 规范把它定义为标准应用程序异常),还是当作系统异常对待?


清单 9. ejbCreate() 方法中的 FinderException

public Object ejbCreate(OrderValue val) throws CreateException {
     try {
        if (value.getItemName() == null) {
          throw new CreateException("Cannot create Order without a name");
        }
        String custId = val.getCustomerId();
        Customer cust = customerHome.fingByPrimaryKey(custId);
        this.customer = cust;
     } catch (FinderException ne) {
     	  //How do you handle this Exception ?
     } catch (RemoteException re) {
	  //This is clearly a System Exception
	  throw ExceptionLogUtil.createLoggableEJBException(re);
     }
     return null;
}

虽然没有什么东西阻止您把 FinderException 当应用程序异常对待,但把它当系统异常对待会更好。原因是:EJB 客户机倾向于把 EJB 组件当黑箱对待。如果 createOrder() 方法的调用者获得了一个 FinderException ,这对调用者并没有任何意义。 OrderEJB 正试图设置客户远程引用这件事对调用者来说是透明的。从客户机的角度看,失败仅仅意味着该订单无法创建。

这类情形的另一个示例是,会话 EJB 组件试图创建另一个会话 EJB,因而导致了一个 CreateException 。一种类似的情形是,实体 EJB 方法试图创建一个会话 EJB 组件,因而导致了一个 CreateException 。这两个异常都应该当作系统异常对待。

另一个可能碰到的挑战是会话 EJB 组件在它的某个容器回调方法中获得了一个 FinderException 。您必须逐例处理这类情况。您可能要决定是把 FinderException 当应用程序异常还是系统异常对待。请考虑清单 1 的情况,其中调用者调用了会话 EJB 组件的 deleteOldOrder 方法。如果我们不是捕获 FinderException ,而是将它抛出,会怎么样呢?在这一特定情况中,把 FinderException 当系统异常对待似乎是符合逻辑的。这里的理由是,会话 EJB 组件倾向于在它们的方法中做许多工作,因为它们处理工作流情形,并且它们对调用者而言是黑箱。

另一方面,请考虑会话 EJB 正在处理下订单的情形。要下一个订单,用户必须有一个简档 ― 但这个特定用户却还没有。业务逻辑可能希望会话 EJB 显式地通知用户她的简档丢失了。丢失的简档很可能表现为会话 EJB 组件中的 javax.ejb.ObjectNotFoundExceptionFinderException 的一个子类)。在这种情况下,最好的办法是在会话 EJB 组件中捕获 ObjectNotFoundException 并抛出一个应用程序异常,让用户知道她的简档丢失了。

即使是有了很好的异常处理策略,另一个问题还是经常会在测试中出现,而且在产品中也更加重要。编译器和运行时优化会改变一个类的整体结构,这会限制您使用堆栈跟踪实用程序来跟踪异常的能力。这就是您需要代码重构的帮助的地方。您应该把大的方法调用分割为更小的、更易于管理的块。而且,只要有可能,异常类型需要多少就划分为多少;每次您捕获一个异常,都应该捕获已规定好类型的异常,而不是捕获所有类型的异常。



回页首


结束语

我们已经在本文讨论了很多东西,您可能想知道我们已经讨论的主要设计是否都物有所值。我的经验是,即便是在中小型项目中,在开发周期中,您的付出就已经能看到回报,更不用说测试和产品周期了。此外,在宕机对业务具有毁灭性影响的生产系统中,良好的异常处理体系结构的重要性再怎么强调也不过分。

我希望本文所展示的最佳做法对您有益。要深入理解这里提供的某些信息,请参看 参考资料部分中的清单。



回页首


参考资料

  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文.

  • 单击本文顶部或底部的 讨论参加本文的 讨论论坛


  • 下载本文所讨论的 实用程序类


  • 您可以阅读 Sun Microsystems 的 EJB 规范了解关于 EJB 体系结构的更多信息。


  • Apache 的 Jakarta 项目有几个珍品。 Log4J 框架即是其中之一。


  • Struts 框架是 Jakarta 项目的另一个珍品。Struts 建立在 MVC 体系结构的基础上,提供了一个彻底的分离,它把系统的表示层从系统的业务逻辑层中分离出来。


  • 要详细了解 Struts,请阅读 Malcom Davis 所写的讲述这个主题的很受欢迎的文章“ Struts, an open-source MVC implementation”( developerWorks,2001 年 2 月)。请注意:有一篇由 Wellie Chao 撰写的最新文章定于 2002 年夏季发表。


  • 您可以通过阅读相关的 J2SE 文档了解关于新的 Java Logging API(java.util.logging)的更多信息。


  • 刚接触 J2EE?来自“WebSphere 开发者园地”的这篇文章告诉您如何 用 WebSphere Studio Application Developer 开发和测试 J2EE 应用程序(2001 年 10 月)。


  • 如果您想更多了解关于测试基于 EJB 的系统的知识,请从最近的 developerWorks文章“ Test flexibly with AspectJ and mock objects”(2002 年 5 月)开始。


  • 如果您不满足于单元测试,还想了解企业级系统测试的知识,请看看 IBM Performance Management, Testing, and Scalability Services企业级测试库提供了什么。


  • Sun 的 J2EE 模式Web 站点着重于使用 J2EE 技术的模式、最佳做法、设计策略以及经验证的解决方案。


  • 您可以在 developerWorks Java 技术专区找到数以百计关于 Java 编程的方方面面的文章。


回页首


关于作者

Srikanth Shenoy 的照片

Srikanth Shenoy 专门从事大型 J2EE 和 EAI 项目的体系结构、设计、开发和部署工作。他在 Java 平台一出现时就迷上了它,从此便全心投入。Srikanth 已经帮他的制造业、物流业和金融业客户实现了 Java 平台“一次编写,随处运行”的梦想。您可以通过 srikanth@srikanth.org与他联系。

posted @ 2005-12-10 11:25 GHawk 阅读(335) | 评论 (0)编辑 收藏

正则表达式之道[转]

http://net.pku.edu.cn/~yhf/tao_regexps_zh.html#Regular%20Expressions%20Syntax

原著:Steve Mansour
sman@scruznet.com
Revised: June 5, 1999
(copied by jm /at/ jmason.org from http://www.scruz.net/%7esman/regexp.htm, after the original disappeared! )

翻译:Neo Lee
什么是正则表达式

一个正则表达式,就是用某种模式去匹配一类字符串的一个公式。很多人因为它们看上去比较古怪而且复杂所以不敢去使用——很不幸,这篇文章也不能够改变这一点,不过,经过一点点练习之后我就开始觉得这些复杂的表达式其实写起来还是相当简单的,而且,一旦你弄懂它们,你就能把数小时辛苦而且易错的文本处理工作压缩在几分钟(甚至几秒钟)内完成。正则表达式被各种文本编辑软件、类库(例如Rogue Wave的tools.h++)、脚本工具(像awk/grep/sed)广泛的支持,而且像Microsoft的Visual C++这种交互式IDE也开始支持它了。

我们将在如下的章节中利用一些例子来解释正则表达式的用法,绝大部分的例子是基于vi中的文本替换命令和grep文件搜索命令来书写的,不过它们都是比较典型的例子,其中的概念可以在sed、awk、perl和其他支持正则表达式的编程语言中使用。你可以看看不同工具中的正则表达式这一节,其中有一些在别的工具中使用正则表达式的例子。还有一个关于vi中文本替换命令(s)的简单说明附在文后供参考。

正则表达式基础

正则表达式由一些普通字符和一些元字符(metacharacters)组成。普通字符包括大小写的字母和数字,而元字符则具有特殊的含义,我们下面会给予解释。

在最简单的情况下,一个正则表达式看上去就是一个普通的查找串。例如,正则表达式"testing"中没有包含任何元字符,,它可以匹配"testing"和"123testing"等字符串,但是不能匹配"Testing"。

要想真正的用好正则表达式,正确的理解元字符是最重要的事情。下表列出了所有的元字符和对它们的一个简短的描述。

元字符   描述


.
匹配任何单个字符。例如正则表达式r.t匹配这些字符串:ratrutr t,但是不匹配root。 
$
匹配行结束符。例如正则表达式weasel$ 能够匹配字符串"He's a weasel"的末尾,但是不能匹配字符串"They are a bunch of weasels."。 
^
匹配一行的开始。例如正则表达式^When in能够匹配字符串"When in the course of human events"的开始,但是不能匹配"What and When in the"。
*
匹配0或多个正好在它之前的那个字符。例如正则表达式.*意味着能够匹配任意数量的任何字符。
\
这是引用府,用来将这里列出的这些元字符当作普通的字符来进行匹配。例如正则表达式\$被用来匹配美元符号,而不是行尾,类似的,正则表达式\.用来匹配点字符,而不是任何字符的通配符。
[ ] 
[c1-c2]
[^c1-c2]
匹配括号中的任何一个字符。例如正则表达式r[aou]t匹配ratrotrut,但是不匹配ret。可以在括号中使用连字符-来指定字符的区间,例如正则表达式[0-9]可以匹配任何数字字符;还可以制定多个区间,例如正则表达式[A-Za-z]可以匹配任何大小写字母。另一个重要的用法是“排除”,要想匹配除了指定区间之外的字符——也就是所谓的补集——在左边的括号和第一个字符之间使用^字符,例如正则表达式[^269A-Z] 将匹配除了2、6、9和所有大写字母之外的任何字符。
\< \>
匹配词(word)的开始(\<)和结束(\>)。例如正则表达式\<the能够匹配字符串"for the wise"中的"the",但是不能匹配字符串"otherwise"中的"the"。注意:这个元字符不是所有的软件都支持的。
\( \)
将 \( 和 \) 之间的表达式定义为“组”(group),并且将匹配这个表达式的字符保存到一个临时区域(一个正则表达式中最多可以保存9个),它们可以用 \1\9 的符号来引用。
|
将两个匹配条件进行逻辑“或”(Or)运算。例如正则表达式(him|her) 匹配"it belongs to him"和"it belongs to her",但是不能匹配"it belongs to them."。注意:这个元字符不是所有的软件都支持的。
+
匹配1或多个正好在它之前的那个字符。例如正则表达式9+匹配9、99、999等。注意:这个元字符不是所有的软件都支持的。
?
匹配0或1个正好在它之前的那个字符。注意:这个元字符不是所有的软件都支持的。
\{i\}
\{i,j\}
匹配指定数目的字符,这些字符是在它之前的表达式定义的。例如正则表达式A[0-9]\{3\} 能够匹配字符"A"后面跟着正好3个数字字符的串,例如A123、A348等,但是不匹配A1234。而正则表达式[0-9]\{4,6\} 匹配连续的任意4个、5个或者6个数字字符。注意:这个元字符不是所有的软件都支持的。


最简单的元字符是点,它能够匹配任何单个字符(注意包括新行符)。假定有个文件test.txt包含以下几行内容:

    he is a rat
    he is in a rut
    the food is Rotten
    I like root beer

我们可以使用grep命令来测试我们的正则表达式,grep命令使用正则表达式去尝试匹配指定文件的每一行,并将至少有一处匹配表达式的所有行显示出来。命令

    grep r.t test.txt

在test.txt文件中的每一行中搜索正则表达式r.t,并打印输出匹配的行。正则表达式r.t匹配一个r接着任何一个字符再接着一个t。所以它将匹配文件中的ratrut,而不能匹配Rotten中的Rot,因为正则表达式是大小写敏感的。要想同时匹配大写和小写字母,应该使用字符区间元字符(方括号)。正则表达式[Rr]能够同时匹配Rr。所以,要想匹配一个大写或者小写的r接着任何一个字符再接着一个t就要使用这个表达式:[Rr].t

要想匹配行首的字符要使用抑扬字符(^)——又是也被叫做插入符。例如,想找到text.txt中行首"he"打头的行,你可能会先用简单表达式he,但是这会匹配第三行的the,所以要使用正则表达式^he,它只匹配在行首出现的h

有时候指定“除了×××都匹配”会比较容易达到目的,当抑扬字符(^)出现在方括号中是,它表示“排除”,例如要匹配he ,但是排除前面是t or s的情性(也就是theshe),可以使用:[^st]he

可以使用方括号来指定多个字符区间。例如正则表达式[A-Za-z]匹配任何字母,包括大写和小写的;正则表达式[A-Za-z][A-Za-z]* 匹配一个字母后面接着0或者多个字母(大写或者小写)。当然我们也可以用元字符+做到同样的事情,也就是:[A-Za-z]+ ,和[A-Za-z][A-Za-z]*完全等价。但是要注意元字符+ 并不是所有支持正则表达式的程序都支持的。关于这一点可以参考后面的正则表达式语法支持情况

要指定特定数量的匹配,要使用大括号(注意必须使用反斜杠来转义)。想匹配所有1001000的实例而排除1010000,可以使用:10\{2,3\},这个正则表达式匹配数字1后面跟着2或者3个0的模式。在这个元字符的使用中一个有用的变化是忽略第二个数字,例如正则表达式0\{3,\} 将匹配至少3个连续的0。

简单的例子

这里有一些有代表性的、比较简单的例子。

vi 命令 作用


:%s/ */ /g 把一个或者多个空格替换为一个空格。
:%s/ *$// 去掉行尾的所有空格。
:%s/^/ / 在每一行头上加入一个空格。
:%s/^[0-9][0-9]* // 去掉行首的所有数字字符。
:%s/b[aeio]g/bug/g 将所有的bagbegbigbog改为bug。 
:%s/t\([aou]\)g/h\1t/g 将所有tagtogtug分别改为hathothug(注意用group的用法和使用\1引用前面被匹配的字符)。

中级的例子(神奇的咒语)

例1

将所有方法foo(a,b,c)的实例改为foo(b,a,c)。这里a、b和c可以是任何提供给方法foo()的参数。也就是说我们要实现这样的转换:

之前   之后
foo(10,7,2) foo(7,10,2)
foo(x+13,y-2,10) foo(y-2,x+13,10)
foo( bar(8), x+y+z, 5) foo( x+y+z, bar(8), 5)

下面这条替换命令能够实现这一魔法:

    :%s/foo(\([^,]*\),\([^,]*\),\([^)]*\))/foo(\2,\1,\3)/g

现在让我们把它打散来加以分析。写出这个表达式的基本思路是找出foo()和它的括号中的三个参数的位置。第一个参数是用这个表达式来识别的::\([^,]*\),我们可以从里向外来分析它: 

[^,]   除了逗号之外的任何字符
[^,]* 0或者多个非逗号字符
\([^,]*\) 将这些非逗号字符标记为\1,这样可以在之后的替换模式表达式中引用它
\([^,]*\), 我们必须找到0或者多个非逗号字符后面跟着一个逗号,并且非逗号字符那部分要标记出来以备后用。

现在正是指出一个使用正则表达式常见错误的最佳时机。为什么我们要使用[^,]*这样的一个表达式,而不是更加简单直接的写法,例如:.*,来匹配第一个参数呢?设想我们使用模式.*来匹配字符串"10,7,2",它应该匹配"10,"还是"10,7,"?为了解决这个两义性(ambiguity),正则表达式规定一律按照最长的串来,在上面的例子中就是"10,7,",显然这样就找出了两个参数而不是我们期望的一个。所以,我们要使用[^,]*来强制取出第一个逗号之前的部分。

这个表达式我们已经分析到了:foo(\([^,]*\),这一段可以简单的翻译为“当你找到foo(就把其后直到第一个逗号之前的部分标记为\1”。然后我们使用同样的办法标记第二个参数为\2。对第三个参数的标记方法也是一样,只是我们要搜索所有的字符直到右括号。我们并没有必要去搜索第三个参数,因为我们不需要调整它的位置,但是这样的模式能够保证我们只去替换那些有三个参数的foo()方法调用,在foo()是一个重载(overoading)方法时这种明确的模式往往是比较保险的。然后,在替换部分,我们找到foo()的对应实例,然后利用标记好的部分进行替换,是的第一和第二个参数交换位置。

例2

假设有一个CSV(comma separated value)文件,里面有一些我们需要的信息,但是格式却有问题,目前数据的列顺序是:姓名,公司名,州名缩写,邮政编码,现在我们希望讲这些数据重新组织,以便在我们的某个软件中使用,需要的格式为:姓名,州名缩写-邮政编码,公司名。也就是说,我们要调整列顺序,还要合并两个列来构成一个新列。另外,我们的软件不能接受逗号前后面有任何空格(包括空格和制表符)所以我们还必须要去掉逗号前后的所有空格。

这里有几行我们现在的数据:

    Bill Jones,     HI-TEK Corporation ,  CA, 95011
    Sharon Lee Smith,  Design Works Incorporated,  CA, 95012
    B. Amos   ,  Hill Street Cafe,  CA, 95013
    Alexander Weatherworth,  The Crafts Store,  CA, 95014
    ...

我们希望把它变成这个样子:

    Bill Jones,CA 95011,HI-TEK Corporation
    Sharon Lee Smith,CA 95012,Design Works Incorporated
    B. Amos,CA 95013,Hill Street Cafe
    Alexander Weatherworth,CA 95014,The Crafts Store
    ...

我们将用两个正则表达式来解决这个问题。第一个移动列和合并列,第二个用来去掉空格。

下面就是第一个替换命令:

    :%s/\([^,]*\),\([^,]*\),\([^,]*\),\(.*\)/\1,\3 \4,\2/

这里的方法跟例1基本一样,第一个列(姓名)用这个表达式来匹配:\([^,]*\),即第一个逗号之前的所有字符,而姓名内容被用\1标记下来。公司名和州名缩写字段用同样的方法标记为\2\3,而最后一个字段用\(.*\)来匹配("匹配所有字符直到行末")。替换部分则引用上面标记的那些内容来进行构造。

下面这个替换命令则用来去除空格:

    :%s/[ \t]*,[ \t]*/,/g

我们还是分解来看:[ \t]匹配空格/制表符,[ \t]* 匹配0或多个空格/制表符,[ \t]*,匹配0或多个空格/制表符后面再加一个逗号,最后,[ \t]*,[ \t]*匹配0或多个空格/制表符接着一个逗号再接着0或多个空格/制表符。在替换部分,我们简单的我们找到的所有东西替换成一个逗号。这里我们使用了结尾的可选的g参数,这表示在每行中对所有匹配的串执行替换(而不是缺省的只替换第一个匹配串)。

例3

假设有一个多字符的片断重复出现,例如:

Billy tried really hard
Sally tried really really hard
Timmy tried really really really hard
Johnny tried really really really really hard

而你想把"really"、"really really",以及任意数量连续出现的"really"字符串换成一个简单的"very"(simple is good!),那么以下命令:

:%s/\(really \)\(really \)*/very /

就会把上述的文本变成:

Billy tried very hard
Sally tried very hard
Timmy tried very hard
Johnny tried very hard

表达式\(really \)*匹配0或多个连续的"really "(注意结尾有个空格),而\(really \)\(really \)* 匹配1个或多个连续的"really "实例。

困难的例子(不可思议的象形文字)

Coming soon.


不同工具中的正则表达式

OK,你已经准备使用RE(regular expressions,正则表达式),但是你并准备使用vi。所以,在这里我们给出一些在其他工具中使用RE的例子。另外,我还会总结一下你在不同程序之间使用RE可能发现的区别。

当然,你也可以在Visual C++编辑器中使用RE。选择Edit->Replace,然后选择"Regular expression"选择框,Find What输入框对应上面介绍的vi命令:%s/pat1/pat2/g中的pat1部分,而Replace输入框对应pat2部分。但是,为了得到vi的执行范围和g选项,你要使用Replace All或者适当的手工Find Next and Replace(译者按:知道为啥有人骂微软弱智了吧,虽然VC中可以选中一个范围的文本,然后在其中执行替换,但是总之不够vi那么灵活和典雅)。

sed

Sed是Stream EDitor的缩写,是Unix下常用的基于文件和管道的编辑工具,可以在手册中得到关于sed的详细信息。

这里是一些有趣的sed脚本,假定我们正在处理一个叫做price.txt的文件。注意这些编辑并不会改变源文件,sed只是处理源文件的每一行并把结果显示在标准输出中(当然很容易使用重定向来定制):

sed脚本   描述


sed 's/^$/d' price.txt 删除所有空行
sed 's/^[ \t]*$/d' price.txt 删除所有只包含空格或者制表符的行
sed 's/"//g' price.txt 删除所有引号

awk

awk是一种编程语言,可以用来对文本数据进行复杂的分析和处理。可以在手册中得到关于awk的详细信息。这个古怪的名字是它作者们的姓的缩写(Aho,Weinberger和Kernighan)。

在Aho,Weinberger和Kernighan的书The AWK Programming Language中有很多很好的awk的例子,请不要让下面这些微不足道的脚本例子限制你对awk强大能力的理解。我们同样假定我们针对price.txt文件进行处理,跟sed一样,awk也只是把结果显示在终端上。 

awk脚本   描述


awk '$0 !~ /^$/' price.txt 删除所有空行
awk 'NF > 0' price.txt awk中一个更好的删除所有行的办法
awk '$2 ~ /^[JT]/ {print $3}' price.txt 打印所有第二个字段是'J'或者'T'打头的行中的第三个字段
awk '$2 !~ /[Mm]isc/ {print $3 + $4}' price.txt 针对所有第二个字段不包含'Misc'或者'misc'的行,打印第3和第4列的和(假定为数字)
awk '$3 !~ /^[0-9]+\.[0-9]*$/ {print $0}' price.txt 打印所有第三个字段不是数字的行,这里数字是指d.d或者d这样的形式,其中d是0到9的任何数字
awk '$2 ~ /John|Fred/ {print $0}' price.txt 如果第二个字段包含'John'或者'Fred'则打印整行

grep

grep是一个用来在一个或者多个文件或者输入流中使用RE进行查找的程序。它的name编程语言可以用来针对文件和管道进行处理。可以在手册中得到关于grep的完整信息。这个同样古怪的名字来源于vi的一个命令,g/re/p,意思是global regular expression print。

下面的例子中我们假定在文件phone.txt中包含以下的文本,——其格式是姓加一个逗号,然后是名,然后是一个制表符,然后是电话号码:

    Francis, John           5-3871
    Wong, Fred              4-4123
    Jones, Thomas           1-4122
    Salazar, Richard        5-2522

grep命令   描述


grep '\t5-...1' phone.txt 把所有电话号码以5开头以1结束的行打印出来,注意制表符是用\t表示的
grep '^S[^ ]* R' phone.txt 打印所有姓以S打头和名以R打头的行
grep '^[JW]' phone.txt 打印所有姓开头是J或者W的行
grep ', ....\t' phone.txt 打印所有姓是4个字符的行,注意制表符是用\t表示的
grep -v '^[JW]' phone.txt 打印所有不以J或者W开头的行
grep '^[M-Z]' phone.txt 打印所有姓的开头是M到Z之间任一字符的行
grep '^[M-Z].*[12]' phone.txt 打印所有姓的开头是M到Z之间任一字符,并且点号号码结尾是1或者2的行

egrep

egrep是grep的一个扩展版本,它在它的正则表达式中支持更多的元字符。下面的例子中我们假定在文件phone.txt中包含以下的文本,——其格式是姓加一个逗号,然后是名,然后是一个制表符,然后是电话号码:

    Francis, John           5-3871
    Wong, Fred              4-4123
    Jones, Thomas           1-4122
    Salazar, Richard        5-2522

egrep command   Description


egrep '(John|Fred)' phone.txt 打印所有包含名字John或者Fred的行
egrep 'John|22$|^W' phone.txt 打印所有包含John 或者以22结束或者以W的行
egrep 'net(work)?s' report.txt 从report.txt中找到所有包含networks或者nets的行


正则表达式语法支持情况

命令或环境 . [ ] ^ $ \( \) \{ \} ? + | ( )
vi  X   X   X   X   X           
Visual C++  X   X   X   X   X           
awk  X   X   X   X       X   X   X   X 
sed  X   X   X   X   X   X         
Tcl  X   X   X   X   X     X   X   X   X 
ex  X   X   X   X   X   X         
grep  X   X   X   X   X   X         
egrep  X   X  X   X   X     X   X   X   X 
fgrep  X   X   X   X   X           
perl  X  X  X  X  X    X  X  X  X

 


vi替换命令简介

Vi的替换命令:

    :ranges/pat1/pat2/g

其中

    : 这是Vi的命令执行界面。
    range 是命令执行范围的指定,可以使用百分号(%)表示所有行,使用点(.)表示当前行,使用美元符号($)表示最后一行。你还可以使用行号,例如10,20表示第10到20行,.,$表示当前行到最后一行,.+2,$-5表示当前行后两行直到全文的倒数第五行,等等。

    s 表示其后是一个替换命令。

    pat1 这是要查找的一个正则表达式,这篇文章中有一大堆例子。

    pat2 这是希望把匹配串变成的模式的正则表达式,这篇文章中有一大堆例子。

    g 可选标志,带这个标志表示替换将针对行中每个匹配的串进行,否则则只替换行中第一个匹配串。

网上有很多vi的在线手册,你可以访问他们以获得更加完整的信息。

posted @ 2005-12-06 22:10 GHawk 阅读(258) | 评论 (0)编辑 收藏

运用Jakarta Struts的七大实战心法

当作者 Chuck Cavaness(著有《Programming Jakarta Struts》一书)所在的网络公司决定采用Struts框架之后,Chuck曾经花费了好几个月来研究如何用它来构建公司的应用系统。本文叙述的正是作者在运用Struts过程中来之不易的若干经验和心得。如果你是个负责通过jsp和servlet开发Web应用的Java程序员,并且也正在考虑采用基于Struts的构建方法的话,那么你会在这里发现很多颇有见地同时也很有价值的信息。

  1. 只在必要的时候才考虑扩展Struts框架

  一个好的framework有很多优点,首先,它必须能够满足用户的可预见的需求。为此 Struts为Web 应用提供了一个通用的架构,这样开发人员可以把精力集中在如何解决实际业务问题上。其次,一个好的framework还必须能够在适当的地方提供扩展接口,以便应用程序能扩展该框架来更好的适应使用者的实际需要。

  如果Struts framework在任何场合,任何项目中都能很好的满足需求,那真是太棒了。但是实际上,没有一个框架声称能做到这一点。一定会有一些特定的应用需求是框架的开发者们无法预见到的。因此,最好的办法就是提供足够的扩展接口,使得开发工程师能够调整struts来更好的符合他们的特殊要求。

  在Struts framework中有很多地方可供扩展和定制。几乎所有的配置类都能被替换为某个用户定制的版本,这只要简单的修改一下Struts的配置文件就可以做到。

  其他组件如ActionServlet和 RequestProcessor 也能用自定义的版本代替. 甚至连Struts 1.1里才有的新特性也是按照扩展的原则来设计的。例如,在异常处理机制中就允许用户定制异常处理的句柄,以便更好的对应用系统发生的错误做出响应。

  作为框架的这种可调整特性在它更适合你的应用的同时也在很大的程度上影响了项目开发的效果。首先,由于您的应用是基于一个现有的成熟的、稳定的framework如Struts,测试过程中发现的错误数量将会大大减少,同时也能缩短开发时间和减少资源的投入。因为你不再需要投入开发力量用于编写基础框架的代码了。

  然而, 实现更多的功能是要花费更大的代价的。我们必须小心避免不必要的滥用扩展性能, Struts是由核心包加上很多工具包构成的,它们已经提供了很多已经实现的功能。因此不要盲目的扩展Struts框架,要先确定能不能采用其他方法使用现有的功能来实现。 在决定编写扩展代码前务必要确认Struts的确没有实现你要的功能。否则重复的功能会导致混乱将来还得花费额外的精力清除它。

  2. 使用异常处理声明

  要定义应用程序的逻辑流程,成熟的经验是推荐在代码之外,用配置的方法来实现,而不是写死在程序代码中的。在J2EE中,这样的例子比比皆是。从实现EJB的安全性和事务性行为到描述JMS消息和目的地之间的关系,很多运行时的处理流程都是可以在程序之外定义的。

  Struts 创建者从一开始就采用这种方法,通过配置Struts的配置文件来定制应用系统运行时的各个方面。这一点在版本1.1的新特性上得到延续,包括新的异常处理功能。在Struts framework以前的版本中,开发人员不得不自己处理Struts应用中发生的错误情况。在最新的版本中,情况大大的改观了,Struts Framework提供了内置的一个称为 ExceptionHandler 的类, 用于系统缺省处理action类运行中产生的错误。这也是在上一个技巧中我们提到的framework许多可扩展接口之一。

  Struts缺省的 ExceptionHandler类会生成一个ActionError对象并保存在适当的范围(scope)对象中。这样就允许JSP页面使用错误类来提醒用户出现什么问题。如果你认为这不能满足你的需求,那么可以很方便的实现你自己的ExcepionHandler类。

  具体定制异常处理的方法和机制

  要定制自己的异常处理机制,第一步是继承org.apache.struts.action.ExceptionHandler类。这个类有2个方法可以覆盖,一个是excute()另外一个是storeException(). 在多数情况下,只需要覆盖其中的excute()方法。下面是ExceptionHandler类的excute()方法声明:
  正如你看到的,该方法有好几个参数,其中包括原始的异常。方法返回一个ActionForward对象,用于异常处理结束后将controller类带到请求必须转发的地方去。

  当然您可以实现任何处理,但一般而言,我们必须检查抛出的异常,并针对该类型的异常进行特定的处理。缺省的,系统的异常处理功能是创建一个出错信息,同时把请求转发到配置文件中指定的地方去。 定制异常处理的一个常见的例子是处理嵌套异常。假设该异常包含有嵌套异常,这些嵌套异常又包含了其他异常,因此我们必须覆盖原来的execute()方法,对每个异常编写出错信息。

  一旦你创建了自己的ExceptionHandler 类,就应该在Struts配置文件中的部分声明这个类,以便让Struts知道改用你自定义的异常处理取代缺省的异常处理.

  可以配置你自己的ExceptionHandler 类是用于Action Mapping特定的部分还是所有的Action对象。如果是用于Action Mapping特定的部分就在元素中配置。如果想让这个类可用于所有的Action对象,可以在 元素中指定。例如,假设我们创建了异常处理类CustomizedExceptionHandler用于所有的Action类, 元素定义如下所示:

  在元素中可以对很多属性进行设置。在本文中,最重要的属性莫过于handler属性, handler属性的值就是自定义的继承了ExceptionHandler类的子类的全名。 假如该属性没有定义,Struts会采用自己的缺省值。当然,其他的属性也很重要,但如果想覆盖缺省的异常处理的话,handler无疑是最重要的属性。

  最后必须指出的一点是,你可以有不同的异常处理类来处理不同的异常。在上面的例子中,CustomizedExceptionHandler用来处理任何java.lang.Exception的子类. 其实,你也可以定义多个异常处理类,每一个专门处理不同的异常树。下面的XML片断解释了如何配置以实现这一点。

  在这里,一旦有异常抛出,struts framework将试图在配置文件中找到ExceptionHandler,如果没有找到,那么struts将沿着该异常的父类链一层层往上找直到发现匹配的为止。因此,我们可以定义一个层次型的异常处理关系结构,在配置文件中已经体现了这一点。

  3. 使用应用模块(Application Modules)

  Struts 1.1的一个新特性是应用模块的概念。应用模块允许将单个Struts应用划分成几个模块,每个模块有自己的Struts配置文件,JSP页面,Action等等。这个新特性是为了解决大中型的开发队伍抱怨最多的一个问题,即为了更好的支持并行开发允许多个配置文件而不是单个配置文件。

  注:在早期的beta版本中,该特性被称为子应用(sub-applications),最近的改名目的是为了更多地反映它们在逻辑上的分工。

  显然,当很多开发人员一起参加一个项目时,单个的Struts配置文件很容易引起资源冲突。应用模块允许Struts按照功能要求进行划分,许多情况已经证明这样更贴近实际。例如,假设我们要开发一个典型的商店应用程序。可以将组成部分划分成模块比如catalog(商品目录), customer(顾客), customer service(顾客服务), order(订单)等。每个模块可以分布到不同的目录下,这样各部分的资源很容易定位,有助于开发和部署。图1 显示了该应用的目录结构。

  图 1. 一个典型的商店应用程序的目录结构
  

  注:如果你无需将项目划分成多个模块,Struts框架支持一个缺省的应用模块。这就使得应用程序也可以在1.0版本下创建,具有可移植性,因为应用程序会自动作为缺省的应用模块。

  为了使用多应用模块功能,必须执行以下几个准备步骤:

  • 为每个应用模块创建独立的Struts配置文件。

  • 配置Web 部署描述符 Web.xml文件。

  • 使用org.apache.struts.actions.SwitchAction 来实现程序在模块之间的跳转.

  创建独立的Struts配置文件

  每个Struts应用模块必须拥有自己的配置文件。允许创建自己的独立于其他模块的Action,ActionForm,异常处理甚至更多。

  继续以上面的商店应用程序为例,我们可以创建以下的配置文件:一个文件名为struts-config-catalog.xml,包含catalog(商品目录)、items(商品清单)、和其它与库存相关的功能的配置信息;另一个文件名为struts- config-order.xml, 包含对order(订单)和order tracking(订单跟踪)的设置。第三个配置文件是struts-config.xml,其中含有属于缺省的应用模块中的一般性的功能。

  配置Web部署描述符

  在Struts的早期版本中,我们在Web.xml中指定Struts配置文件的路径。好在这点没变,有助于向后兼容。但对于多个应用模块,我们需要在Web部署描述符中增加新的配置文件的设定。

  对于缺省的应用(包括Struts的早期版本),Struts framework 在Web.xml文件中查找带有config的元素,用于载入Action mapping 和其它的应用程序设定。作为例子,以下的XML片断展现一个典型的元素:

  注:如果在现有的元素中找不到"config"关键字,Struts framework将缺省地使用/WEB/struts-config.xml

  为了支持多个应用模块(Struts 1.1的新特性),必须增加附加的元素。与缺省的元素不同的是,附加的元素与每个应用模块对应,必须以config/xxx的形式命名,其中字符串xxx代表该模块唯一的名字。例如,在商店应用程序的例子中,元素可定义如下(注意粗体字部分):

  第一个 元素对应缺省的应用模块。第二和第三个元素分别代表非缺省应用模块catalog 和 order。

  当Struts载入应用程序时,它首先载入缺省应用模块的配置文件。然后查找带有字符串config/xxx 形式的附加的初始化参数。对每个附加的配置文件也进行解析并载入内存。这一步完成后,用户就可以很随意地用config/后面的字符串也就是名字来调用相应的应用模块。

  多个应用模块之间调用Action类

  在为每个应用模块创建独立的配置文件之后,我们就有可能需要调用不同的模块中Action。为此必须使用Struts框架提供的SwitchAction类。Struts 会自动将应用模块的名字添加到URL,就如Struts 自动添加应用程序的名字加到URL一样。应用模块是对框架的一个新的扩充,有助于进行并行的团队开发。如果你的团队很小那就没必要用到这个特性,不必进行模块化。当然,就算是只有一个模块,系统还是一样的运作。

  4. 把JSP放到WEB-INF后以保护JSP源代码

  为了更好地保护你的JSP避免未经授权的访问和窥视, 一个好办法是将页面文件存放在Web应用的WEB-INF目录下。

  通常JSP开发人员会把他们的页面文件存放在Web应用相应的子目录下。一个典型的商店应用程序的目录结构如图2所示。跟catalog (商品目录)相关的JSP被保存在catalog子目录下。跟customer相关的JSP,跟订单相关的JSP等都按照这种方法存放。
  
  这种方法的问题是这些页面文件容易被偷看到源代码,或被直接调用。某些场合下这可能不是个大问题,可是在特定情形中却可能构成安全隐患。用户可以绕过Struts的controller直接调用JSP同样也是个问题。

  为了减少风险,可以把这些页面文件移到WEB-INF 目录下。基于Servlet的声明,WEB-INF不作为Web应用的公共文档树的一部分。因此,WEB-INF 目录下的资源不是为客户直接服务的。我们仍然可以使用WEB-INF目录下的JSP页面来提供视图给客户,客户却不能直接请求访问JSP。

  采用前面的例子,图3显示将JSP页面移到WEB-INF 目录下后的目录结构

  如果把这些JSP页面文件移到WEB-INF 目录下,在调用页面的时候就必须把"WEB-INF"添加到URL中。例如,在一个Struts配置文件中为一个logoff action写一个Action mapping。其中JSP的路径必须以"WEB-INF"开头。如下所示:请注意粗体部分.

  这个方法在任何情况下都不失为Struts实践中的一个好方法。是唯一要注意的技巧是你必须把JSP和一个Struts action联系起来。即使该Action只是一个很基本的很简单JSP,也总是要调用一个Action,再由它调用JSP。

  最后要说明的是,并不是所有的容器都能支持这个特性。WebLogic早期的版本不能解释Servlet声明,因此无法提供支持,据报道在新版本中已经改进了。总之使用之前先检查一下你的Servlet容器。

  5. 使用 Prebuilt Action类提升开发效率

  Struts framework带有好几个prebuilt Action类,使用它们可以大大节省开发时间。其中最有用的是org.apache.struts.actions.ForwardAction 和 org.apache.struts.actions.DispatchAction.

  使用 ForwardAction

  在应用程序中,可能会经常出现只要将Action对象转发到某个JSP的情况。在上一点中曾提到总是由Action调用JSP是个好习惯。如果我们不必在Action中执行任何业务逻辑,却又想遵循从Action访问页面的话,就可以使用ForwardAction,它可以使你免去创建许多空的Action类。运用ForwardAction的好处是不必创建自己的Action类,你需要做的仅仅是在Struts配置文件中配置一个Action mapping。

  举个例子,假定你有一个JSP文件index.jsp ,而且不能直接调用该页面,必须让程序通过一个Action类调用,那么,你可以建立以下的Action mapping来实现这一点:

  正如你看到的,当 /home 被调用时, 就会调用ForwardAction 并把请求转发到 index.jsp 页面.

  再讨论一下不通过一个Action类直接转发到某个页面的情况,必须注意我们仍然使用元素中的forward属性来实现转发的目标。这时元素定义如下:

  以上两种方法都可以节省你的时间,并有助于减少一个应用所需的文件数。

  使用 DispatchAction

  DispatchAction是Struts包含的另一个能大量节省开发时间的Action类。与其它Action类仅提供单个execute()方法实现单个业务不同,DispatchAction允许你在单个Action类中编写多个与业务相关的方法。这样可以减少Action类的数量,并且把相关的业务方法集合在一起使得维护起来更容易。

  要使用DispatchAction的功能,需要自己创建一个类,通过继承抽象的DispatchAction得到。对每个要提供的业务方法必须有特定的方法signature。例如,我们想要提供一个方法来实现对购物车添加商品清单,创建了一个类ShoppingCartDispatchAction提供以下的方法:

  那么,这个类很可能还需要一个deleteItem()方法从客户的购物车中删除商品清单,还有clearCart()方法清除购物车等等。这时我们就可以把这些方法集合在单个Action类,不用为每个方法都提供一个Action类。

  在调用ShoppingCartDispatchAction里的某个方法时,只需在URL中提供方法名作为参数值。就是说,调用addItem()方法的 URL看起来可能类似于:

  http://myhost/storefront/action/cart?method=addItem

  其中method参数指定ShoppingCartDispatchAction中要调用的方法。参数的名称可以任意配置,这里使用的"method"只是一个例子。参数的名称可以在Struts配置文件中自行设定。

  6.使用动态ActionForm

  在Struts framework中,ActionForm对象用来包装HTML表格数据(包括请求),并返回返回动态显示给用户的数据。它们必须是完全的JavaBean,并继承.Struts 里面的ActionForm类,同时,用户可以有选择地覆盖两个缺省方法。

  该特性能节省很多时间,因为它可以协助进行自动的表现层的验证。ActionForm的唯一缺点是必须为不同的HTML表格生成多个ActionForm 类以保存数据。例如,如果有一个页面含有用户的注册信息,另一个页面则含有用户的介绍人的信息,那么就需要有两个不同的ActionForm类。这在大的应用系统中就会导致过多的ActionForm类。Struts 1.1对此做出了很好的改进,引入了动态ActionForm类概念

  通过Struts framework中的DynaActionForm类及其子类可以实现动态的ActionForm ,动态的ActionForm允许你通过Struts的配置文件完成ActionForm的全部配置;再也没有必要在应用程序中创建具体的ActionForm类。具体配置方法是:在Struts的配置文件通过增加一个元素,将type属性设定成DynaActionForm或它的某个子类的全名。下面的例子创建了一个动态的ActionForm名为logonForm,它包含两个实例变量:username 和 password.

  动态的ActionForm可以用于Action类和JSP,使用方法跟普通的ActionForm相同,只有一个小差别。如果使用普通的ActionForm对象则需要提供get 和 set方法取得和设置数据。以上面的例子而言,我们需要提供getUsername() 和 setUsername()方法取得和设置username变量,同样地有一对方法用于取得和设置password变量.

  这里我们使用的是DynaActionForm,它将变量保存在一个Map类对象中,所以必须使用DynaActionForm 类中的get(name) 和 set(name)方法,其中参数name是要访问的实例变量名。例如要访问DynaActionForm中username的值,可以采用类似的代码:

  String username = (String)form.get("username");

  由于值存放在一个Map对象,所以要记得对get()方法返回的Object对象做强制性类型转换。

  DynaActionForm有好几个很有用的子类。其中最重要的是DynaValidatorForm ,这个动态的ActionForm和Validator 一起利用公共的Validator包来提供自动验证。这个特性使你得以在程序代码之外指定验证规则。将两个特性结合使用对开发人员来说将非常有吸引力。

  7. 使用可视化工具

  自从Struts 1.0 分布以来,就出现了不少可视化工具用于协助创建,修改和维护Struts的配置文件。配置文件本身是基于XML格式,在大中型的开发应用中会增大变得很笨拙。为了更方便的管理这些文件,一旦文件大到你无法一目了然的时候,建议试着采用其中的一种GUI 工具协助开发。商业性的和开放源代码的工具都有不少,表1列出了可用的工具和其相关链接,从那里可以获取更多信息。

  表 1. Struts GUI 工具
  应用程序 性质 网址
  Adalon 商业软件 http://www.synthis.com/products/adalon
  Easy Struts 开放源码 http://easystruts.sourceforge.net/
  Struts Console 免费 http://www.jamesholmes.com/struts/console
  JForms 商业软件 http://www.solanasoft.com/
  Camino 商业软件 http://www.scioworks.com/scioworks_camino.html
  Struts Builder 开放源码 http://sourceforge.net/projects/rivernorth/
  StrutsGUI 免费 http://www.alien-factory.co.uk/struts/struts-index.html

  相关资源

  要获取更为全面的Struts GUI 工具列表 (包括免费的和商业性的), 请访问 Struts resource page.

posted @ 2005-12-02 13:37 GHawk 阅读(422) | 评论 (1)编辑 收藏

我应该使用哪种样式的 WSDL 呢? (From IBM developerWorks)

我应该使用哪种样式的 WSDL 呢?
内容:
引言
RPC/编码
RPC/文字
文档/编码
文档/文字
文档/文字包装模式
为什么不始终采用文档/文字包装的样式
SOAP 响应消息
结束语
参考资料
关于作者
对本文的评价
相关内容:
Web services with WSDL
Handle namespaces in SOAP messages you create by hand
订阅:
developerWorks 时事通讯

级别: 高级

Russell Butek
Web 服务顾问, IBM
2003 年 10 月 31 日
2005 年 6 月 29 日 更新

WSDL 绑定样式可以是 RPC 样式或文档样式。用法可以是编码的,也可以是文字的。您如何决定使用哪一种样式/用法的组合呢?本文将帮助您解决这个问题。

引言
Web 服务是通过 WSDL 文档来描述的。WSDL 绑定描述了如何把服务绑定到消息传递协议(特别是 SOAP 消息传递协议)。WSDL SOAP 绑定可以是 RPC 样式的绑定,也可以是文档样式的绑定。同样,SOAP 绑定可以有编码的用法,也可以有文字的用法。这给我们提供了四种样式/用法模型:

  1. RPC/编码
  2. RPC/文字
  3. 文档/编码
  4. 文档/文字

除了这些样式之外,还有一种样式也很常见,它称为文档/文字包装的样式,算上这一种,在创建 WSDL 文件时您就有了五种绑定样式可以从中选择。您应该选择哪一种呢?

在我进一步讨论以前,让我阐明一些容易混淆的地方。这里,这些术语是非常不合适的:RPC 与文档。这些术语意味着 RPC 样式应该用于 RPC 编程模型,文档样式应该用于文档或消息编程模型。 但事实完全不是这样。样式对于编程模型没有任何意义。它只是指明了如何将 WSDL 绑定转化为 SOAP 消息。其他就没什么了。你可以将任一种样式用于任何编程模型。

同样,术语编码文字只对于 WSDL 到 SOAP 映射有意义,可是,至少这里,这两个单词的字面意思更容易理解一些。

对于这篇讨论,让我们从清单 1 中的 Java 方法开始,并且应用 JAX-RPC Java-to-WSDL 规则(参阅参考资料查看 JAX-RPC 1.1 规范)。

清单 1. Java 方法
public void myMethod(int x, float y);

RPC/编码
采用清单 1 中的方法并且使用你喜欢的 Java-to-WSDL 工具来运行,指定您想让它生成 RPC/编码的 WSDL。您最后应该得到如清单 2 所示的 WSDL 片断。

清单 2. 用于 myMethod 的 RPC/编码的 WSDL
<message name="myMethodRequest">
    <part name="x" type="xsd:int"/>
    <part name="y" type="xsd:float"/>
</message>
<message name="empty"/>

<portType name="PT">
    <operation name="myMethod">
        <input message="myMethodRequest"/>
        <output message="empty"/>
    </operation>
</portType>

<binding .../>  
<!-- I won't bother with the details, just assume it's RPC/encoded. -->

现在用“5”作为参数 x 的值,“5.0”作为参数 y 的值来调用这个方法。发送一个如清单 3 所示的SOAP 消息。

清单 3. 用于 myMethod 的 RPC/编码的 SOAP 消息
<soap:envelope>
    <soap:body>
        <myMethod>
            <x xsi:type="xsd:int">5</x>
            <y xsi:type="xsd:float">5.0</y>
        </myMethod>
    </soap:body>
</soap:envelope>

关于前缀和命名空间的注意事项
为了简单起见,在本文的大部分 XML 示例中,我省略了命名空间和前缀。不过,我还是使用了少数前缀,您可以假定它们是用下列名称空间进行定义的:

  • xmlns:xsd="http://www.w3.org/2001/XMLSchema"
  • xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  • xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"

关于命名空间和 WSDL-to-SOAP 映射的讨论,请参考文章“Handle namespaces in SOAP messages you create by hand”(参阅 参考资料)。

关于 RPC/编码例子中的 WSDL 和 SOAP 消息有一些需要注意的地方:

优点

  • WSDL 尽可能的简单明了。
  • 操作名出现在消息中,因此接收者可以很容易的将消息分派到操作的实现。



缺点

遵照 WS-I
各种 Web 服务规范有时候是不一致和不明确的。WS-I 组织成立用来解决这些规范上的问题。它已经定义了许多概要,指明了你应该如何编写 Web 服务来实现互操作性。要获取 WS-I 的更多信息,请参阅参考资料中的 WS-I 链接。

  • 类型编码信息(xsi:type="xsd:int")通常就是降低吞吐量性能的开销。
  • 你不能很容易的验证这个消息的有效性,因为只有 <x ...>5</x><y ...>5.0</y> 行包含 Schema 中定义的内容;soap:body 内容的其余部分来自于 WSDL 定义。
  • 虽然它是合法的 WSDL,但 RPC/encoded 是不遵守 WS-I 的。

有没有一种方法可以取其精华,弃其糟粕呢?可能有。让我们看一下 RPC/文字样式。

RPC/文字
用于这个方法的 RPC/文字样式的 WSDL 看起来与 RPC/编码的 WSDL(清单 4)几乎一样。绑定的用法从 编码 变为 文字。仅此而已。

清单 4. 用于 myMethod 的 RPC/文字样式的 WSDL
<message name="myMethodRequest">
    <part name="x" type="xsd:int"/>
    <part name="y" type="xsd:float"/%gt;
</message>
<message name="empty"/>

<portType name="PT">
    <operation name="myMethod">
        <input message="myMethodRequest"/>
        <output message="empty"/>
    </operation>
</portType>

<binding .../>  
<!-- I won't bother with the details, just assume it's RPC/literal. -->

RPC/文字的 SOAP 消息又是怎样的呢(参阅清单 5)?这里的更改要多一点。去掉了类型编码。

清单 5. 用于 myMethod 的 RPC/literal SOAP 消息
<soap:envelope>
    <soap:body>
        <myMethod>
            <x>5</x>
            <y>5.0</y>
        </myMethod>
    </soap:body>
</soap:envelope>

关于 xsi:type 和文字用法的注意事项
虽然在一般情况下,xsi:type 没有出现在文字 WSDL 的 SOAP 消息中,但是仍然有一些情况,类型信息是必须的,并且它将以多种形式出现。如果 API 期望一个基础类型,并且发送一个扩展实例,则必须提供这个实例的类型以便正确的反序列化该对象。

这里是这种方法的优点和缺点:

优点

  • WSDL 尽可能的简单明了。
  • 操作名仍然出现在消息中。
  • 去掉了类型编码。
  • RPC/文字是遵循 WS-I 的。

缺点

  • 你仍然不能很容易的验证这个消息的有效性,因为只有 <x ...>5</x><y ...>5.0</y> 行中包含定义在 Schema 中的内容;soap:body 内容的其余部分来自于 WSDL 定义。

文档样式如何呢?它们能够帮助克服这些困难吗?

文档/编码
没有人使用这个样式。它不遵循 WS-I。因此此处略过。

文档/文字
文档/文字的 WSDL 在 RPC/文字的 WSDL 基础上做了一些修改。其不同点已经在清单 6 中指出。

清单 6. 用于 myMethod 的文档/文字 WSDL
<types>
    <schema>
        <element name="xElement" type="xsd:int"/>
        <element name="yElement" type="xsd:float"/>
    </schema>
</types>

<message name="myMethodRequest">
    <part name="x" element="xElement"/>
    <part name="y" element="yElement"/>
</message>
<message name="empty"/>

<portType name="PT">
    <operation name="myMethod">
        <input message="myMethodRequest"/>
        <output message="empty"/>
    </operation>
</portType>

<binding .../>  
<!-- I won't bother with the details, just assume it's document/literal. -->

用于这个 WSDL 的 SOAP 消息如清单 7 所示:

清单 7. 用于 myMethod 的文档/文字 SOAP 消息
<soap:envelope>
    <soap:body>
        <xElement>5</xElement>
        <yElement>5.0</yElement>
    </soap:body>
</soap:envelope>

关于消息组成部分的注意事项
我本来可以只更改绑定,就像我从 RPC/编码转到 RPC/所做的那样。它将是合法的 WSDL。然而,WS-I 基本概要(WS-I Basic Profile)规定文档/文字的消息的组成部分引用元素而不是类型,所以我遵循了 WS-I(并且此处使用元素部分可以很好地把我们带到关于文档/文字包装的样式的讨论)。

下面是这种方法的优点和缺点:

优点

  • 没有类型编码信息。
  • 您可以在最后用任何 XML 检验器检验此消息的有效性。soap:body 里面的所有内容都定义在 schema 中。
  • 文档/文字是遵循 WS-I 的,但是有限制(参阅缺点)。

缺点

  • WSDL 有一点复杂。不过,这是一个非常小的缺点,因为 WSDL 并没有打算由人来读取。
  • SOAP 消息中缺少操作名。而如果没有操作名,发送就可能比较困难,并且有时变得不可能。
  • WS-I 仅仅允许 SOAP 消息中 soap:body 的一个子元素。正如你在清单 7 中所见的那样,该消息的 soap:body 有两个子元素。

文档/文字样式似乎只是重新排列了一下 RPC/文字模型中的优点和缺点。你可以验证该消息,但是你已经失去了操作名。有没有什么办法可以改进这一点呢?是的,它就是文档/文字包装模式。

文档/文字包装模式
在我描述文档/文字包装模式的规则之前,让我先向您展示 WSDL 和 SOAP 消息,如清单 8清单 9 所示。

清单 8. 用于 myMethod 的文档/文字封装的 WSDL。
<types>
    <schema>
        <element name="myMethod">
            <complexType>
                <sequence>
                    <element name="x" type="xsd:int"/>
                    <element name="y" type="xsd:float"/>
                </sequence>
            </complexType>
        </element>
        <element name="myMethodResponse">
            <complexType/>
        </element>
    </schema>
</types>
<message name="myMethodRequest">
    <part name="parameters" element="myMethod"/>
</message>
<message name="empty">
    <part name="parameters" element="myMethodResponse"/>
</message>

<portType name="PT">
    <operation name="myMethod">
        <input message="myMethodRequest"/>
        <output message="empty"/>
    </operation>
</portType>

<binding .../>  
<!-- I won't bother with the details, just assume it's document/literal. -->

WSDL Schema 现在把参数放在包装中(参阅清单 9)。

清单 9. 用于 myMethod 的文档/文字包装的 SOAP 消息
<soap:envelope>
    <soap:body>
        <myMethod>
            <x>5</x>
            <y>5.0</y>
        </myMethod>
    </soap:body>
</soap:envelope>

注意这个 SOAP 消息同 RPC/文字的 SOAP 消息(清单 5)非常相似。您可能会说,它看起来与 RPC/文字的 SOAP 消息是完全一样的,不过,这两种消息之间存在着微妙的区别。在 RPC/文字的 SOAP 消息中,<soap:body><myMethod> 子句是操作的名称。在文档/文字包装的 SOAP 消息中,<myMethod> 子句是单个输入消息的组成部分引用的元素的名称。因此,包装的样式具有这样的一个特征,输入元素的名称与操作的名称是相同的。此样式是把操作名放入 SOAP 消息的一种巧妙方式。

文档/文字包装的样式的特征有:

  • 输入消息只有一个组成部分。
  • 该部分是一个元素。
  • 该元素同操作有相同的名称。
  • 该元素的复杂类型没有属性。

下面是该种方法的优缺点:

优点

  • 没有类型编码信息。
  • soap:body 中出现的所有内容都定义在 schema 中,所以您可以很容易地检验此消息的有效性。
  • 方法名又出现在 SOAP 消息中。
  • 文档/文字是遵守 WS-I 的,并且包装模式符合了 WS-I 的限制,即 SOAP 消息的 soap:body 只有一个子元素。

缺点

  • WSDL 更加复杂。

文档/文字包装的样式还是有一些缺点,不过与优点比起来,它们都显得微不足道。

RPC/文字包装?
从 WSDL 的角度来考虑,没有理由只是把把包装的样式和文档/文字绑定联系在一起。它可以很容易地应用于 RPC/文字绑定。但是这样做是相当不明智的。SOAP 将包含操作的一个 myMethod 元素和元素名称的子 myMethod 元素。另外,即使它是一个合法的 WSDL,RPC/文字元素部分也不遵循 WS-I。

文档/文字的样式在哪里定义?
这种包装类型来源于 Microsoft?。并没有任何规范来定义这个类型;因此虽然这个类型是一个好东西,但不幸的是,为了与 Microsoft 和其他公司的实现进行互操作,现在惟一的选择就是根据 Microsoft WSDL 的输出来猜测它是如何工作的。该模式已经出现了一段时间,并且业界也很好的理解了它,虽然该模式在例子中是非常明显的,但是也有一些内容不够清晰。我们希望一个独立的组织比如 WS-I 来帮助稳定和标准化这一模式。

为什么不始终采用文档/文字包装的样式
至此,本文已经给了您这样的一个印象,文档/文字包装的样式是最好的方法。而实际的情况往往确实如此。不过,仍然存在着一些情况,在这些情况下,您最好是换一种别的样式。

采用文档/文字非包装的样式的理由
如果您已经重载了操作,就不能采用文档/文字包装的样式。

想象一下,除了我们一直在使用的方法之外,还有另一种方法,请参见清单 10

清单 10. 用于文档/文字包装的有问题的方法
public void myMethod(int x, float y);
public void myMethod(int x);

关于重载的操作的注意事项
WSDL 2.0 不会允许重载的操作。这对于一些允许该操作的语言(比如 Java)来说是不幸的。一些规范(比如 JAX-RPC)将不得不定义一个名称转换模式(name mangling scheme)来将重载的方法映射到 WSDL 中。WSDL 2.0 只不过将问题从 WSDL-to-SOAP 映射转移到 WSDL-to-language 映射中。

WSDL 允许重载的操作。但是当你向 WSDL 上添加包装模式的时候,需要元素有与操作相同的名称,并且在 XML 中不能有两个名称相同的元素。所以您必须采用文档/文字非包装的样式或某种 RPC 样式。

采用 RPC/文字的样式的理由
由于文档/文字非包装的样式没有提供操作名,所以在有些情况下,您将需要采用某种 RPC 样式。比如说清单 11 中的一组方法。





清单 11. 用于文档/文字非包装的样式的问题方法
public void myMethod(int x, float y);
public void myMethod(int x);
public void someOtherMethod(int x, float y);

现在假设你的服务器接收到了文档/文字的 SOAP 消息,你可以回头看一下清单 7。服务器应该发送哪一种方法呢?所有您能确切知道的就是,它一定不是 myMethod(int x),因为消息有两个参数,而这种方法只需要一个参数。它可能是其他两种方法中的一种。采用文档/文字的样式,您没有办法知道是哪一种方法。

假定服务器接收到一个 RPC/文字的消息,而不是文档/文字的消息,如清单 5 所示。对于这种消息,服务器很容易决定把它发送到哪一种方法。你知道该操作名称是 myMethod,并且你知道你有两个参数,因此肯定是 myMethod(int x, float y)

采用 RPC/编码的理由
使用 RPC/编码样式最重要的原因是为了数据图表。设想你有一个二进制树,如清单 12 所示。

清单 12. 二进制树节点 schema
<complexType name="Node">
    <sequence>
        <element name="name" type="xsd:string"/>
        <element name="left" type="Node" xsd:nillable="true"/>
        <element name="right" type="Node" xsd:nillable="true"/>
    </sequence>
</complexType>

根据这种节点定义,你可以构建一个树,其根节点 -- A -- 通过左/右链接指向节点 B(参阅图 1)。

图 1. 编码树。
编码树

发送数据图表的标准方式是使用 href 标签,它是 RPC/编码的样式(清单 13)的一部分。

清单 13. RPC/编码的二进制树
<A>
    <name>A</name>
    <left href="12345"/>
    <right href="12345"/>
</A>
<B id="12345">
    <name>B</name>
    <left xsi:nil="true"/>
    <right xsi:nil="true"/>
</B>

在任何文字样式中,href 属性都是不可用的,这样图形链接就不再起作用了(参阅清单 14图 2)。你仍然有一个根节点 A,其指向左边的节点 B和右边的另一个节点 B。这些节点 B 都是一样的,但是它们不是相同的节点。数据被复制而不是引用两次。

清单 14. 文字二进制树
<A>
    <name>A</name>
    <left>
        <name>B</name>
        <left xsi:nil="true"/>
        <right xsi:nil="true"/>
    </left>
    <right>
        <name>B</name>
        <left xsi:nil="true"/>
        <right xsi:nil="true"/>
    </right>
</A>

图 2. 文字树
文字树

在文字样式中,您可以通过各种方法构造图表,但是却没有标准的方法;所以您做的任何事情很可能不能与网络中其他端点上的服务进行互操作。

SOAP 响应消息
到目前为止我已经讨论了请求消息。但是响应消息呢?它们是怎样的呢?现在你应该很清楚一个文档/文字消息的响应消息应该是怎样的。soap:body 的内容是由 schema 定义的,因此你所需要做的就是查看该 schema 来了解响应消息的内容。比如,参考清单 15 来查看清单 8 中的 WSDL 文件的响应消息。

清单 15. 用于 myMethod 的文档/文字包装的响应 SOAP 消息
<soap:envelope>
    <soap:body>
        <myMethodResponse/>
    </soap:body>
</soap:envelope>

但是用于 RPC 样式响应的 soap:body 的子元素是什么呢?WSDL 1.1 规范并不是很清楚。但是 WS-I 解决了这个问题。WS-I 的 Basic Profile 指明了在 RPC/文字响应消息中,soap:body 子元素的名称是“... 相应的 wsdl:operation 名称加上字符串 'Response' 作为后缀。”奇怪!这正是常规包装模式的响应元素的名称。因此清单 15 可以应用到 RPC/文字消息和文档/文字包装的消息。(因为 RPC/编码并不是遵守 WS-I 的,WS-I Basic Profile 并不关心 RPC/编码的响应是怎样的,但是你可以假设应用在这里的约定也可以应用在其他任何地方。)因此响应消息的内容并不神秘。

结束语
这里有四种绑定样式(其实是五个,但是文档/编码的样式是没有意义的)。虽然每种样式都有自己的用处,但是在大多数情况下,最好的样式是文档/文字包装的样式。

参考资料

关于作者
Russell Butek 是 IBM 的一名 Web 服务顾问。他是 IBM WebSphere Web 服务引擎的开发人员之一。他也是 JAX-RPC Java Specification Request (JSR) 专家组的 IBM 代表。他从事 Apache 的 AXIS SOAP 引擎的实现方面的研究,推动了 AXIS 1.0 遵循 JAX-RPC 1.0。以前,他是 IBM CORBA ORB 的开发人员和许多 OMG 特别工作组的 IBM 代表:包括可移植拦截器特别工作组(他是这个特别工作组的主席)、核心特别工作组以及互操作性特别工作组。你可以通过 butek@us.ibm.com 与他联系。

posted @ 2005-12-02 13:36 GHawk 阅读(665) | 评论 (0)编辑 收藏

FrameBuffer & BootSplash

http://www.chinaitpower.com/2005September/2005-09-13/192844.html

posted @ 2005-11-18 11:18 GHawk 阅读(317) | 评论 (0)编辑 收藏

Windows 和 Linux 实现相同功能的软件对照表

http://cathayan.org/equivalentsoft-zh-cn.html

posted @ 2005-11-18 10:58 GHawk 阅读(446) | 评论 (0)编辑 收藏

安装图形界面(X11)中鼠标主题 (转)

From: http://www.gnome-cn.org/documents/howto/install-mouse-theme

安装图形界面(X11)中鼠标主题。
  1. 如果没有 ~/.icons 目录,创建它。
  2. 如果没有 ~/.icons/default 目录,创建它。
  3. 把鼠标主题解压到 ~/.icons 下:
       > ls ~/.icons/
       DeepSky  default
    
  4. 创建文件 ~/.icons/default/index.theme , 内容如下:
       [Icon Theme]
       Inherits=DeepSky
    
  5. 注消后重新登陆即可看到新鼠标主题。

posted @ 2005-11-09 10:50 GHawk 阅读(726) | 评论 (1)编辑 收藏

搬家

CSDN那边总是不太可靠,决定搬过来了:)

posted @ 2005-11-04 11:15 GHawk 阅读(157) | 评论 (0)编辑 收藏

仅列出标题
共3页: 上一页 1 2 3