BlogJava | 首页 | 发新随笔 | 发新文章 | 联系 | 聚合 | 管理 随笔:48 文章:0 评论:18 引用:0

级别: 初级

Sean C. Sullivan , 软件工程师

2003 年 10 月 15 日

J2EE 开发人员使用数据访问对象(Data Access Object DAO)设计模式,以便将低级别的数据访问逻辑与高级别的业务逻辑分离。实现 DAO 模式涉及比编写数据访问代码更多的内容。在本文中,Java 开发人员 Sean C. Sullivan 讨论了 DAO 编程中三个常常被忽略的方面:事务界定、异常处理和日志记录。

在过去 18 个月中,我参加了一个由有才华的软件工程师组成的小组,构建定制的、基于 Web 的供应链管理应用程序。我们的应用程序访问范围广泛的持久性数据,包括配送状态、供应链衡量(metrics)、库存、货运发票、项目管理数据和用户信息。我们用 JDBC API 连接到我们公司的不同数据库平台上,并在整个应用程序中使用 DAO 设计模式。

图 1 显示了应用程序和数据源之间的关系:


图 1. 应用程序和数据源
应用程序和数据源

在整个应用程序中使用数据访问对象(DAO)使我们可以将底层数据访问逻辑与业务逻辑分离开来。我们构建了为每一个数据源提供 GRUD (创建、读取、更新、删除)操作的 DAO 类。

在本文中,我将为您介绍构建更好的 DAO 类的 DAO 实现策略和技术。更确切地说,我将讨论日志、异常处理和事务界定。您将学到如何将这三者结合到自己的 DAO 类中。本文假定您熟悉 JDBC API、SQL 和关系数据库编程。

我们将以对 DAO 设计模式和数据访问对象的概述开始。

DAO基础

DAO 模式是标准 J2EE 设计模式之一。开发人员用这种模式将底层数据访问操作与高层业务逻辑分离开。一个典型的 DAO 实现有以下组件:

  • 一个 DAO 工厂类
  • 一个 DAO 接口
  • 一个实现了 DAO 接口的具体类
  • 数据传输对象(有时称为值对象)

具体的 DAO 类包含访问特定数据源的数据的逻辑。在下面一节中您将学习设计和实现数据访问对象的技术。有关 DAO 设计模式的更多内容请参阅 参考资料





回页首


事务界定

关于 DAO 要记住的重要一点是它们是事务性对象。由 DAO 所执行的每一个操作 -- 如创建、更新或者删除数据 -- 都与一个事务相关联。因此, 事务界定的概念就变得特别重要了。

事务界定是定义事务边界的方式。J2EE 规范描述了两种事务界定的模型:编程式(programmatic)和声明式(declarative)。表 1 分析了这两种模型:

表 1. 两种事务界定的模型

声明式事务界定 编程式事务界定
程序员用 EJB 部署描述符声明事务属性。 程序员负责编写事务逻辑。
运行时环境(EJB 容器)用这些属性自动管理事务。 应用程序通过一个 API 控制事务。

我们将侧重于编程式事务界定。

设计考虑

如前所述,DAO 是事务性对象。一个典型的 DAO 执行像创建、更新和删除这样的事务性操作。在设计 DAO 时,首先要问自己以下问题:

  • 事务要如何开始?
  • 事务应如何结束?
  • 哪一个对象将负责开始一个事务?
  • 哪一个对象将负责结束一个事务?
  • DAO 是否要负责事务的开始和结束?
  • 应用程序是否需要通过多个 DAO 访问数据?
  • 事务涉及到一个 DAO 还是多个 DAO?
  • 一个 DAO 是否调用另一个 DAO 的方法?

了解上述问题的答案将有助于您选择最适合的 DAO 的事务界定策略。在 DAO 中有两种主要的界定事务的策略。一种方式是让 DAO 负责界定事务,另一种将事务界定交给调用这个 DAO 方法的对象处理。如果选择了前一种方式,那么就将事务代码嵌入到 DAO 中。如果选择后一种方式,那么事务界定代码就是在 DAO 类外面。我们将使用简单的代码示例帮助您更好理解每一种方式是如何工作的。

清单 1 显示了一个有两种数据操作的 DAO:创建和更新:


清单 1. DAO 方法
												
														       public void createWarehouseProfile(WHProfile profile);
       public void updateWarehouseStatus(WHIdentifier id, StatusInfo status);

												
										

清单 2 显示了一个简单的事务。事务界定在 DAO 类外面。注意在这个例子中调用者是如何在一个事务中结合多个 DAO 操作的。


清单 2. 调用者管理的事务
												
														      tx.begin();    // start the transaction
      dao.createWarehouseProfile(profile);
      dao.updateWarehouseStatus(id1, status1);
      dao.updateWarehouseStatus(id2, status2);
      tx.commit();   // end the transaction

												
										

这种事务界定策略对于需要在一个事务中访问多个 DAO 的应用程序特别有用。

可以用 JDBC API 或者 Java 事务 API(Java Transaction API JTA)实现事务界定。 JDBC 事务界定比 JTA 事务界定要简单,但是 JTA 提供了更多的灵活性。在下面一节中我将更深入地分析事务界定的机制。





回页首


用 JDBC 进行事务界定

JDBC 事务是用 Connection 对象控制的。JDBC Connection 接口( java.sql.Connection )提供了两种事务模式:自动提交和手工提交。 java.sql.Connection 提供了以下控制事务的方法:

  • public void setAutoCommit(boolean)
  • public boolean getAutoCommit()
  • public void commit()
  • public void rollback()

清单 3 显示了如何用 JDBC API 界定一个事务:


清单 3. 用 JDBC API 进行事务界定
												
														      import java.sql.*;
      import javax.sql.*;

      // ...
      DataSource ds = obtainDataSource();
      Connection conn = ds.getConnection();
      conn.setAutoCommit(false);
      // ...
      pstmt = conn.prepareStatement("UPDATE MOVIES ...");
      pstmt.setString(1, "The Great Escape");
      pstmt.executeUpdate();
      // ...
      conn.commit();
      // ...

												
										

使用 JDBC 事务界定时,您可以将多个 SQL 语句结合到一个事务中。JDBC 事务的一个缺点是事务的范围局限于一个数据库连接。一个 JDBC 事务不能跨越多个数据库。在下面,我们将看一下如何用 JTA 进行事务界定。因为 JTA 不像 JDBC 那样有名,所以我们首先做一个简介。





回页首


JTA简介

Java 事务 API(JTA) 及其同门兄弟 Java 事务服务(Java Transaction Service JTS)为 J2EE 平台提供了分布式事务服务。一个 分布式的事务涉及一个事务管理器和一个或者多个资源管理器。一个 资源管理器是任何类型的持久性的数据存储。事务管理器负责协调所有事务参与者之间的通信。事务管理器与资源管理器之间的关系如图 2 所示:


图 2. 一个事务管理器和资源管理器
一个事务管理器和资源管理器

JTA 事务比 JDBC 事务功能更强。JDBC 事务局限为一个数据库连接,而 JTA 事务可以有多个参与者。所有下列 Java 平台组件都可以参与 JTA 事务:

  • JDBC 连接
  • JDO PersistenceManager 对象
  • JMS 队列
  • JMS 主题
  • 企业 JavaBeans
  • 符合 J2EE 连接体系结构(J2EE Connector Architecture)规范的资源适配器




回页首


使用 JTA 的事务界定

要用 JTA 进行事务界定,应用程序要调用 javax.transaction.UserTransaction 接口中的方法。清单 4 显示了对 UserTransaction 对象的典型 JNDI 查询:


清单 4. 一个对 UserTransaction 对象的 JDNI 查询
												
														      import javax.transaction.*;
      import javax.naming.*;
      // ...
      InitialContext ctx = new InitialContext();
      Object txObj = ctx.lookup("java:comp/UserTransaction");
      UserTransaction utx = (UserTransaction) txObj;


												
										

当应用程序找到了 UserTransaction 对象后,就可以开始事务了,如清单 5 所示:


清单 5. 用 JTA 开始一个事务
												
														      utx.begin();
      // ...
      DataSource ds = obtainXADataSource();
      Connection conn = ds.getConnection();
      pstmt = conn.prepareStatement("UPDATE MOVIES ...");
      pstmt.setString(1, "Spinal Tap");
      pstmt.executeUpdate();
      // ...
      utx.commit();
      // ...


												
										

当应用程序调用 commit() 时,事务管理器用一个两阶段的提交协议结束事务。





回页首


控制事务的 JTA 方法

javax.transaction.UserTransaction 接口提供了以下事务控制方法:

  • public void begin()
  • public void commit()
  • public void rollback()
  • public int getStatus()
  • public void setRollbackOnly()
  • public void setTransactionTimeout(int)

应用程序调用 begin() 开始事务。应用程序调用 commit() 或者 rollback() 结束事务。参阅 参考资料以了解更多关于用 JTA 进行事务管理的内容。





回页首


使用 JTA 和 JDBC

开发人员通常在 DAO 类中用 JDBC 进行底层数据操作。如果计划用 JTA 界定事务,那么就需要有一个实现 javax.sql.XADataSourcejavax.sql.XAConnection javax.sql.XAResource 接口的 JDBC 驱动程序。一个实现了这些接口的驱动程序将可以参与 JTA 事务。一个 XADataSource 对象就是一个 XAConnection 对象的工厂。 XAConnection s 是参与 JTA 事务的 JDBC 连接。

您将需要用应用服务器的管理工具设置 XADataSource 。从应用服务器和 JDBC 驱动程序的文档中可以了解到相关的指导。

J2EE 应用程序用 JNDI 查询数据源。一旦应用程序找到了数据源对象,它就调用 javax.sql.DataSource.getConnection() 以获得到数据库的连接。

XA 连接与非 XA 连接不同。一定要记住 XA 连接参与了 JTA 事务。这意味着 XA 连接不支持 JDBC 的自动提交功能。同时,应用程序一定不要对 XA 连接调用 java.sql.Connection.commit() 或者 java.sql.Connection.rollback() 。相反,应用程序应该使用 UserTransaction.begin()、UserTransaction.commit()serTransaction.rollback()





回页首


选择最好的方式

我们讨论了如何用 JDBC 和 JTA 界定事务。每一种方式都有其优点,您需要决定哪一种最适合于您的应用程序。

在最近的许多项目中,我们小组是用 JDBC API 进事务界定来构建 DAO 类的。这些 DAO 类可以总结如下:

  • 事务界定代码嵌入在 DAO 类中。
  • DAO 类使用 JDBC API 进行事务界定。
  • 调用者不能界定事务。
  • 事务范围局限于单个 JDBC 连接。

JDBC 事务并不总是适合复杂的企业应用程序。如果您的事务要跨越多个 DAO 或者多个数据库,那么下列实现策略也许更合适:

  • 事务用 JTA 界定。
  • 事务界定代码从 DAO 中分离出来。
  • 调用者负责界定事务。
  • DAO 加入一个全局事务。

JDBC 方式由于其简单性而具有吸引力,JTA 方式提供了更大的灵活性。您所选择的实现将取决于应用程序的特定需求。





回页首


日志记录和 DAO

一个良好实现的 DAO 类将使用日志记录来捕捉有关其运行时行为的细节。您可以选择记录异常、配置信息、连接状态、JDBC 驱动程序元数据、或者查询参数。日志对于开发的所有阶段都很有用。我经常在开发时、测试时和生产中分析应用程序日志。

在本节,我将展示一个显示如何将 Jakarta Commons Logging 加入到 DAO 中的代码示例。在这之前,让我们回顾一下一些基本知识。

选择日志库

许多开发人员使用一种原始格式进行日志记录: System.out.printlnSystem.err.printlnPrintln 语句速度快且使用方便,但是它们没有提供全功能的日志记录系统所具有的功能。表 2 列出了 Java 平台的日志库:

表 2. Java 平台的日志库

日志库 开放源代码? URL
java.util.logging 不是 http://java.sun.com/j2se/
Jakarta Log4j http://jakarta.apache.org/log4j/
Jakarta Commons Logging http://jakarta.apache.org/commons/logging.html

Jakarta Commons Logging 可以与 java.util.logging 或者 Jakarta Log4j 一同使用。Commons Logging 是一个日志抽象层,它隔离了应用程序与底层日志实现。使用 Commons Logging,您可以通过改变配置文件更换底层日志实现。Commons Logging 在 Jakarta Struts 1.1 和 Jakarta HttpClient 2.0 中使用。

一个日志记录示例

清单 7 显示了如何在 DAO 类中使用 Jakarta Commons Logging:


清单 7. DAO 类中的 Jakarta Commons Logging
												
														import org.apache.commons.logging.*;

class DocumentDAOImpl implements DocumentDAO
{
      static private final Log log = LogFactory.getLog(DocumentDAOImpl.class);

      public void deleteDocument(String id)
      {
          // ...
          log.debug("deleting document: " + id);
          // ...
          try
          {
              // ... data operations ...
          }
          catch (SomeException ex)
          {
              log.error("Unable to delete document", ex);
              // ... handle the exception ...
	}
      }
}

												
										

日志记录是所有任务关键型应用程序的重要部分。如果在 DAO 中遇到故障,那么日志通常可以提供判断出错位置的最好信息。将日志加入到 DAO 可以保证您有机会进行调试和故障排除。





回页首


DAO 中的异常处理

我们讨论过了事务界定和日志,现在对于如何在数据访问对象上应用它们有了更深入的理解。我们的第三个和最后一个讨论议题是异常处理。遵从几个简单的异常处理指导可以使您的 DAO 更容易使用、更健壮及更易于维护。

在实现 DAO 模式时,考虑以下问题:

  • DAO 的公共接口中的方法是否抛出检查过的异常?
  • 如果是的话,抛出何种检查过的异常?
  • 在 DAO 实现类中如何处理异常?

在使用 DAO 模式的过程中,我们的小组开发了一些处理异常的原则。遵从这些原则可以极大地改进您的 DAO:

  • DAO 方法应该抛出有意义的异常。
  • DAO 方法不应该抛出 java.lang.Exceptionjava.lang.Exception 太一般化了。它不传递关于底层问题的任何信息。
  • DAO 方法不应该抛出 java.sql.SQLException 。SQLException 是一个低级别的 JDBC 异常。一个 DAO 应该力争封装 JDBC 而不是将 JDBC 公开给应用程序的其余部分。
  • 只有在可以合理地预期调用者可以处理异常时,DAO 接口中的方法才应该抛出检查过的异常。如果调用者不能以有意义的方式处理这个异常,那么考虑抛出一个未检查的(运行时)异常。
  • 如果数据访问代码捕获了一个异常,不要忽略它。忽略捕获的异常的 DAO 是很难进行故障诊断的。
  • 使用链接的异常将低级别的异常转化为高级别的异常。
  • 考虑定义标准 DAO 异常类。Spring Framework (参阅 参考资料)提供了很好的一套预定义的 DAO 异常类。

有关异常和异常处理技术的更多信息参阅 参考资料





回页首


实现实例: MovieDAO

MovieDAO 是一个展示本文中讨论的所有技术的 DAO:事务界定、日志和异常处理。您可以在 参考资料一节中找到 MovieDAO 源代码。代码分为三个包:

  • daoexamples.exception
  • daoexamples.movie
  • daoexamples.moviedemo

DAO 模式的这个实现包含下面列出的类和接口:

  • daoexamples.movie.MovieDAOFactory
  • daoexamples.movie.MovieDAO
  • daoexamples.movie.MovieDAOImpl
  • daoexamples.movie.MovieDAOImplJTA
  • daoexamples.movie.Movie
  • daoexamples.movie.MovieImpl
  • daoexamples.movie.MovieNotFoundException
  • daoexamples.movie.MovieUtil

MovieDAO 接口定义了 DAO 的数据操作。这个接口有五个方法,如下所示:

  • public Movie findMovieById(String id)
  • public java.util.Collection findMoviesByYear(String year)
  • public void deleteMovie(String id)
  • public Movie createMovie(String rating, String year, String, title)
  • public void updateMovie(String id, String rating, String year, String title)

daoexamples.movie 包包含 MovieDAO 接口的两个实现。每一个实现使用一种不同的方式进行事务界定,如表 3 所示:

表 3. MovieDAO 实现

MovieDAOImpl MovieDAOImplJTA
实现 MovieDAO 接口?
通过 JNDI 获得 DataSource?
从 DataSource 获得 java.sql.Connection 对象?
DAO 在内部界定事务?
使用 JDBC 事务?
使用一个 XA DataSource?
参与 JTA 事务?

MovieDAO 演示应用程序

这个演示应用程序是一个名为 daoexamples.moviedemo.DemoServlet 的 servlet 类。 DemoServlet 使用这两个 Movie DAO 查询和更新表中的电影数据。

这个 servlet 展示了如何将支持 JTA 的 MovieDAO 和 Java 消息服务(Java Message Service)结合到一个事务中,如清单 8 所示。


清单 8. 将 MovieDAO 和 JMS 代码结合到一个事务中
												
															UserTransaction utx = MovieUtil.getUserTransaction();
	utx.begin();
	batman = dao.createMovie("R",
			"2008",
			"Batman Reloaded");
	publisher = new MessagePublisher();
	publisher.publishTextMessage("I'll be back");
	dao.updateMovie(topgun.getId(),
			"PG-13",
			topgun.getReleaseYear(),
			topgun.getTitle());
	dao.deleteMovie(legallyblonde.getId());
	utx.commit();

												
										
posted @ 2006-08-31 23:16 rendong 阅读(167) | 评论 (0)编辑 收藏
 

 当前位置: 首页 >> 数据库 >> Oracle >> 我的权限控制(JBX + struts + hibernate + ORACLE)
 
我的权限控制(JBX + struts + hibernate + ORACLE) 

--------------------------------------------------------------------------------
 
作者::     来源:     发表时间:2006-06-08     浏览次数:18    字号:大  中  小
  
通过过滤器判断用户权限.
第一步:建立UserPermissionFilter类.


import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.util.*;

import test.system.SysUserApi;
import test.vo.SysUserVO;
import test.system.dao.SysUserDao;
import test.Const;

public class UserPermissionFilter extends HttpServlet implements Filter {

  protected FilterConfig filterConfig = null;

  public void destroy() {
    this.filterConfig = null;
  }


  public void doFilter(
      ServletRequest request,
      ServletResponse response,
      FilterChain filterChain) throws IOException, ServletException {
    try {
      HttpServletRequest req = (HttpServletRequest) request;
      SysUserVO userSession = new SysUserVO();
      userSession = (SysUserVO)req.getSession().getAttribute(Const.SESSION_USER);
      if (userSession == null) {
        HttpServletResponse rep = (HttpServletResponse) response;

        rep.sendRedirect("/admin/login.jsp");
             }else{
        filterChain.doFilter(request, response);
      }
    }
    catch (Exception e) {}
  }
  public void init(FilterConfig filterConfig) throws ServletException {
    this.filterConfig = filterConfig;
  }
  public FilterConfig getFilterConfig() {
    return filterConfig;
  }

  public void setFilterConfig(FilterConfig filterConfig) {
    this.filterConfig = filterConfig;
  }

}


第二步:配置WEB.xml文件
设置过滤器:
  <filter>
    <filter-name>userpermission</filter-name>
    <filter-class>sports.tools.UserPermissionFilter</filter-class>
  </filter>
设置过滤器映射,因为过滤器不能过滤全部的程序,所以可以用列表的形式来增加需要过滤的文件.如下.一个过滤器可以过滤多个映射文件.
  <filter-mapping>
    <filter-name>userpermission</filter-name>
    <url-pattern>/admin/index.jsp</url-pattern>
  </filter-mapping>

  <filter-mapping>
    <filter-name>userpermission</filter-name>
    <url-pattern>/admin/edit/*</url-pattern>
  </filter-mapping>
 
 
 
======================================
http://www.itwenzhai.com/data/2006/0608/article_22958.htm
=========================================
http://www.itwenzhai.com/data/2006/0626/article_25178.htm
=========================================


  不重复DAO
===============

 

由于 Java™ 5 泛型的采用,有关泛型类型安全 Data Access Object (DAO) 实现的想法变得切实可行。在本文中,系统架构师 Per Mellqvist 展示了基于 Hibernate 的泛型 DAO 实现类。然后展示如何使用 Spring AOP introductions 将类型安全接口添加到类中以便于查询执行。
对于大多数开发人员,为系统中的每个 DAO 编写几乎相同的代码到目前为止已经成为一种习惯。虽然所有人都将这种重复标识为 “代码味道”,但我们大多数都已经学会忍受它。其实有解决方案。可以使用许多 ORM 工具来避免代码重复。例如,使用 Hibernate,您可以简单地为所有的持久域对象直接使用会话操作。这种方法的缺点是损失了类型安全。

为什么您要为数据访问代码提供类型安全接口?我会争辩说,当它与现代 IDE 工具一起使用时,会减少编程错误并提高生产率。首先,类型安全接口清楚地指明哪些域对象具有可用的持久存储。其次,它消除了易出错的类型强制转换的需要(这是一个在查询操作中比在 CRUD 中更常见的问题)。最后,它有效利用了今天大多数 IDE 具备的自动完成特性。使用自动完成是记住什么查询可用于特定域类的快捷方法。

在本文中,我将为您展示如何避免再三地重复 DAO 代码,而仍保留类型安全接口的优点。事实上,您需要为每个新 DAO 编写的只是 Hibernate 映射文件、无格式旧 Java 接口以及 Spring 配置文件中的 10 行。

DAO 实现

DAO 模式对任何企业 Java 开发人员来说都应该很熟悉。但是模式的实现各不相同,所以我们来澄清一下本文提供的 DAO 实现背后的假设:

系统中的所有数据库访问都通过 DAO 进行以实现封装。
每个 DAO 实例负责一个主要域对象或实体。如果域对象具有独立生命周期,它应具有自己的 DAO。
DAO 负责域对象的创建、读取(按主键)、更新和删除(creations, reads, updates, and deletions,CRUD)。
DAO 可允许基于除主键之外的标准进行查询。我将之称为查找器方法 或查找器。查找器的返回值通常是 DAO 负责的域对象集合。
DAO 不负责处理事务、会话或连接。这些不由 DAO 处理是为了实现灵活性。
泛型 DAO 接口

泛型 DAO 的基础是其 CRUD 操作。下面的接口定义泛型 DAO 的方法:


清单 1. 泛型 DAO 接口
public interface GenericDao <T, PK extends Serializable> {

    /** Persist the newInstance object into database */
    PK create(T newInstance);

    /** Retrieve an object that was previously persisted to the database using
     *   the indicated id as primary key
     */
    T read(PK id);

    /** Save changes made to a persistent object.  */
    void update(T transientObject);

    /** Remove an object from persistent storage in the database */
    void delete(T persistentObject);
}

 


实现接口

用 Hibernate 实现清单 1 中的接口十分简单,如清单 2 所示。它只需调用底层 Hibernate 方法和添加强制类型转换。Spring 负责会话和事务管理。(当然,我假设这些函数已做了适当的设置,但该主题在 Hibernate 和 Springt 手册中有详细介绍。)


清单 2. 第一个泛型 DAO 实现
public class GenericDaoHibernateImpl <T, PK extends Serializable>
    implements GenericDao<T, PK>, FinderExecutor {
    private Class<T> type;

    public GenericDaoHibernateImpl(Class<T> type) {
        this.type = type;
    }

    public PK create(T o) {
        return (PK) getSession().save(o);
    }

    public T read(PK id) {
        return (T) getSession().get(type, id);
    }

    public void update(T o) {
        getSession().update(o);
    }

    public void delete(T o) {
        getSession().delete(o);
    }

    // Not showing implementations of getSession() and setSessionFactory()
            }
 


Spring 配置

最后,在 Spring 配置中,我创建了 GenericDaoHibernateImpl 的一个实例。必须告诉 GenericDaoHibernateImpl 的构造函数 DAO 实例将负责哪个域类。只有这样,Hibernate 才能在运行时知道由 DAO 管理的对象类型。在清单 3 中,我将域类 Person 从示例应用程序传递给构造函数,并将先前配置的 Hibernate 会话工厂设置为已实例化的 DAO 的参数:


清单 3. 配置 DAO
<bean id="personDao" class="genericdao.impl.GenericDaoHibernateImpl">
        <constructor-arg>
            <value>genericdaotest.domain.Person</value>
        </constructor-arg>
        <property name="sessionFactory">
            <ref bean="sessionFactory"/>
        </property>
</bean>
        
2。
我还没有完成,但我所完成的确实已经可以使用了。在清单 4 中,可以看到原封不动使用该泛型 DAO 的示例:


清单 4. 使用 DAO
public void someMethodCreatingAPerson() {
    ...
    GenericDao dao = (GenericDao)
     beanFactory.getBean("personDao"); // This should normally be injected

    Person p = new Person("Per", 90);
    dao.create(p);
}
        

 

现在,我有一个能够进行类型安全 CRUD 操作的泛型 DAO。让子类 GenericDaoHibernateImpl 为每个域对象添加查询能力将非常合理。因为本文的目的在于展示如何不为每个查询编写显式的 Java 代码来实现查询,但是,我将使用其他两个工具将查询引入 DAO,也就是 Spring AOP 和 Hibernate 命名的查询。

Spring AOP introductions

可以使用 Spring AOP 中的 introductions 将功能添加到现有对象,方法是将功能包装在代理中,定义应实现的接口,并将所有先前未支持的方法指派到单个处理程序。在我的 DAO 实现中,我使用 introductions 将许多查找器方法添加到现有泛型 DAO 类中。因为查找器方法是特定于每个域对象的,因此将其应用于泛型 DAO 的类型化接口。

Spring 配置如清单 5 所示:


清单 5. FinderIntroductionAdvisor 的 Spring 配置
<bean id="finderIntroductionAdvisor" class="genericdao.impl.FinderIntroductionAdvisor"/>

<bean id="abstractDaoTarget"
        class="genericdao.impl.GenericDaoHibernateImpl" abstract="true">
        <property name="sessionFactory">
            <ref bean="sessionFactory"/>
        </property>
</bean>

<bean id="abstractDao"
        class="org.springframework.aop.framework.ProxyFactoryBean" abstract="true">
        <property name="interceptorNames">
            <list>
                <value>finderIntroductionAdvisor</value>
            </list>
        </property>
</bean>
        

 

在清单 5 的配置文件中,我定义了三个 Spring bean。第一个 bean 是 FinderIntroductionAdvisor,它处理引入到 DAO 的所有方法,这些方法在 GenericDaoHibernateImpl 类中不可用。我稍后将详细介绍 Advisor bean。

第二个 bean 是 “抽象的”。在 Spring 中,这意味着该 bean 可在其他 bean 定义中被重用,但不被实例化。除了抽象特性之外,该 bean 定义只指出我想要 GenericDaoHibernateImpl 的实例以及该实例需要对 SessionFactory 的引用。注意,GenericDaoHibernateImpl 类仅定义一个构造函数,该构造函数接受域类作为其参数。因为该 bean 定义是抽象的,所以我将来可以无数次地重用该定义,并将构造函数参数设置为合适的域类。

最后,第三个也是最有趣的 bean 将 GenericDaoHibernateImpl 的 vanilla 实例包装在代理中,赋予其执行查找器方法的能力。该 bean 定义也是抽象的,不指定希望引入到 vanilla DAO 的接口。该接口对于每个具体的实例是不同的。注意,清单 5 显示的整个配置仅定义一次。

3。

扩展 GenericDAO

当然,每个 DAO 的接口都基于 GenericDao 接口。我只需使该接口适应特定的域类并扩展该接口以包括查找器方法。在清单 6 中,可以看到为特定目的扩展的 GenericDao 接口示例:


清单 6. PersonDao 接口
public interface PersonDao extends GenericDao<Person, Long> {
    List<Person> findByName(String name);
}

 

 

很明显,清单 6 中定义的方法旨在按名称查找 Person。必需的 Java 实现代码全部是泛型代码,在添加更多 DAO 时不需要任何更新。

配置 PersonDao

因为 Spring 配置依赖于先前定义的 “抽象” bean,因此它变得相当简洁。我需要指出 DAO 负责哪个域类,并且需要告诉 Springs 该 DAO 应实现哪个接口(一些方法是直接使用,一些方法则是通过使用 introductions 来使用)。清单 7 展示了 PersonDAO 的 Spring 配置文件:


清单 7. PersonDao 的 Spring 配置
<bean id="personDao" parent="abstractDao">
    <property name="proxyInterfaces">
        <value>genericdaotest.dao.PersonDao</value>
    </property>
    <property name="target">
        <bean parent="abstractDaoTarget">
            <constructor-arg>
                <value>genericdaotest.domain.Person</value>
            </constructor-arg>
        </bean>
    </property>
</bean>
        

 

在清单 8 中,可以看到使用了这个更新后的 DAO 版本:


清单 8. 使用类型安全接口
public void someMethodCreatingAPerson() {
    ...
    PersonDao dao = (PersonDao)
     beanFactory.getBean("personDao"); // This should normally be injected

    Person p = new Person("Per", 90);
    dao.create(p);

    List<Person> result = dao.findByName("Per"); // Runtime exception
}
        

 

虽然清单 8 中的代码是使用类型安全 PersonDao 接口的正确方法,但 DAO 的实现并不完整。调用 findByName() 会导致运行时异常。问题在于我还没有实现调用 findByName() 所必需的查询。剩下要做的就是指定查询。为更正该问题,我使用了 Hibernate 命名查询。


 

Hibernate 命名查询

使用 Hibernate,可以在 Hibernate 映射文件 (hbm.xml) 中定义 HQL 查询并为其命名。稍后可以通过简单地引用给定名称来在 Java 代码中使用该查询。该方法的优点之一是能够在部署时优化查询,而无需更改代码。您一会将会看到,另一个优点是无需编写任何新 Java 实现代码,就可以实现 “完整的” DAO。清单 9 是带有命名查询的映射文件的示例:


清单 9. 带有命名查询的映射文件
 <hibernate-mapping package="genericdaotest.domain">
     <class name="Person">
         <id name="id">
             <generator class="native"/>
         </id>
         <property name="name" />
         <property name="weight" />
     </class>

     <query name="Person.findByName">
         <![CDATA[select p from Person p where p.name = ? ]]>
     </query>
 </hibernate-mapping>
        

 

清单 9 定义了域类 Person 的 Hibernate 映射,该域类具有两个属性:name 和 weight。Person 是具有上述属性的简单 POJO。该文件还包含一个在数据库中查找 Person 所有实例的查询,其中 “name” 等于提供的参数。Hibernate 不为命名查询提供任何真正的名称空间功能。出于讨论目的,我为所有查询名称都加了域类的短(非限定)名称作为前缀。在现实世界中,使用包括包名称的完全类名可能是更好的主意。


 

逐步概述

您已经看到了为任何域对象创建和配置新 DAO 所必需的全部步骤。三个简单的步骤是:

定义一个接口,它扩展 GenericDao 并包含所需的任何查找器方法。
将每个查找器的命名查询添加到域对象的 hbm.xml 映射文件。
为 DAO 添加 10 行 Spring 配置文件。
查看执行查找器方法的代码(只编写了一次!)来结束我的讨论。

4。

可重用的 DAO 类

使用的 Spring advisor 和 interceptor 很简单,事实上它们的工作是向后引用 GenericDaoHibernateImplClass。方法名以 “find” 打头的所有调用都传递给 DAO 和单个方法 executeFinder()。


清单 10. FinderIntroductionAdvisor 的实现
public class FinderIntroductionAdvisor extends DefaultIntroductionAdvisor {
    public FinderIntroductionAdvisor() {
        super(new FinderIntroductionInterceptor());
    }
}

public class FinderIntroductionInterceptor implements IntroductionInterceptor {

    public Object invoke(MethodInvocation methodInvocation) throws Throwable {

        FinderExecutor genericDao = (FinderExecutor) methodInvocation.getThis();

        String methodName = methodInvocation.getMethod().getName();
        if (methodName.startsWith("find")) {
            Object[] arguments = methodInvocation.getArguments();
            return genericDao.executeFinder(methodInvocation.getMethod(), arguments);
        } else {
            return methodInvocation.proceed();
        }
    }

    public boolean implementsInterface(Class intf) {
        return intf.isInterface() && FinderExecutor.class.isAssignableFrom(intf);
    }
}
 

 

executeFinder() 方法

清单 10 的实现中惟一缺少的是 executeFinder() 实现。该代码查看调用的类和方法的名称,并使用配置上的约定将其与 Hibernate 查询的名称相匹配。还可以使用 FinderNamingStrategy 来支持其他命名查询的方法。默认实现查找叫做 “ClassName.methodName” 的查询,其中 ClassName 是不带包的短名称。清单 11 完成了泛型类型安全 DAO 实现:


清单 11. executeFinder() 的实现
public List<T> executeFinder(Method method, final Object[] queryArgs) {
     final String queryName = queryNameFromMethod(method);
     final Query namedQuery = getSession().getNamedQuery(queryName);
     String[] namedParameters = namedQuery.getNamedParameters();
     for(int i = 0; i < queryArgs.length; i++) {
             Object arg = queryArgs[i];
             Type argType =  namedQuery.setParameter(i, arg);
      }
      return (List<T>) namedQuery.list();
 }

 public String queryNameFromMethod(Method finderMethod) {
     return type.getSimpleName() + "." + finderMethod.getName();
 }
 

 

结束语

在 Java 5 之前,该语言不支持编写既类型安全又 泛型的代码,您必须只能选择其中之一。在本文中,您已经看到一个结合使用 Java 5 泛型与 Spring 和 Hibernate(以及 AOP)等工具来提高生产率的示例。泛型类型安全 DAO 类相当容易编写 —— 您只需要单个接口、一些命名查询和为 Spring 配置添加的 10 行代码 —— 而且可以极大地减少错误并节省时间。

几乎本文的所有代码都是可重用的。尽管您的 DAO 类可能包含此处没有实现的查询和操作类型(比如,批操作),但使用我所展示的技术,您至少应该能够实现其中的一部分。参阅 参考资料 了解其他泛型类型安全 DAO 类实现。

致谢

自 Java 语言中出现泛型以来,单个泛型类型安全 DAO 的概念已经成为主题。我曾在 JavaOne 2004 中与 Don Smith 简要讨论了泛型 DAO 的灵活性。本文使用的 DAO 实现类旨在作为示例实现,实际上还存在其他实现。例如,Christian Bauer 已经发布了带有 CRUD 操作和标准搜索的实现,Eric Burke 也在该领域做出了工作。我确信将会有更多的实现出现。我要额外感谢 Christian,他目睹了我编写泛型类型安全 DAO 的第一次尝试并提出改进建议。最后,我要感谢 Ramnivas Laddad 的无价帮助,他审阅了本文。

 

posted @ 2006-08-31 22:36 rendong 阅读(301) | 评论 (0)编辑 收藏
 
     摘要: http://www.blogjava.net/zqli/archive/2006/08/29/66394.aspx 生成有4个随机数字和杂乱背景的图片,数字和背景颜色会改变,服务器端刷新(用history.go(-1)也会变)原型参考ALIBABA  http://china.alibaba.com/member/showimage------------产生验证码图片的文件---...  阅读全文
posted @ 2006-08-29 23:44 rendong 阅读(437) | 评论 (1)编辑 收藏
 
做为一名大四的学生,我面试过不少的单位,有成功的也有失败的,但是对我来说所有的失败在某种意义上都是一种成功,特别是我下面写的这些,写这篇文章的时,我已经签了南京的一家软件公司,但是想起今年2月21日我面试苏州台湾的IT公司的经历联想到我们现在学习编程的一些情况我真的深有感触,这次面试使我深深的体会到了失败但也收获了很多。  
<a href='http://www.best-code.com'>www.best-code.com</a>

        我要说的将分成三部分,
        1.是我面试的具体经过
        2.是由面试想到的
        3.现今我应该做的。
        当然这些话很大程度上是我个人的意见,不可能完全得到大家的赞同,所以  在某些观点上如果哪位朋友觉得跟我的有很大出入,请不要介意,也不要对我攻击,就当我  没有说过,欢迎和我联系共同探讨这些问题!我的EMAIL:wutao8@263.net  

1.面试经过  
大约在年前我接到了台湾瑞晟(Realtek)苏州公司的面试通知,通知我2月21日到苏州工业园区面试,接到面试后的几天我把一些专业课温习了一遍,特别是C++和数据结构,由于大学几年里,我一直专研这些方面,加上通过了高级程序员的考试,对于一些常用的算法我差不多也  达到了烂熟于胸的地步,当时的感觉是如果问了我这些方面的问题我应该是没有问题的!

21日那天我被安排在4:30面试,由一位技术人员单独给我面试,在问了一些简单的问题之后  ,他给我出了一道编程题目,题目是这样的:

(由于具体面试的题目比较烦琐,我将其核心思想提取出来分解成……(乱码)  

1)  写一个函数计算当参数为n(n很大)时的值  1-2+3-4+5-6+7......+n  
哼,我的心里冷笑一声!没想到这么简单,我有点紧张的心情顿时放松起来!  
于是很快我给出我的解法:  

long  fn(long  n)  
{  
    long  temp=0;  
    int  i,flag=1;  
    if(n<=0)  
    {  
            printf("error:  n  must  >  0);  
            exit(1);  
        }  
    for(i=1;i<=n;i++)  
    {  
            temp=temp+flag*i;  
            flag=(-1)*flag;  
    }  
            return  temp;  
}

        搞定!当我用期待的目光看着面试官的时候,他微笑着跟我说,执行结果肯定是没有问题!  但当n很大的时候我这个程序执行效率很低,在嵌入式系统的开发中,程序的运行效率很重要  ,能让CPU少执行一条指令都是好的,他让我看看这个程序还有什么可以修改的地方,把程序  优化一下!听了这些话,我的心情当时变的有点沉重,没想到他的要求很严格,之后我对程序  进行了严格的分析,给出了改进了的方案!  

long  fn(long  n)  
{  
        long  temp=0;  
        int  j=1,i=1,flag=1;  
        if(n<=0)  
    {  
            printf("error:  n  must  >  0);  
            exit(1);  
        }  
        while(j<=n)  
        {  
            temp=temp+i;  
            i=-i;  
            i>0?i++:i--;  
            j++;  
        }  
        return  temp;  
}  

        虽然我不敢保证我这个算法是最优的,但是比起上一个程序,我将所有涉及到乘法指令的语  句改为执行加法指令,既达到要题目的要求而且运算时间上缩短了很多!而代价仅仅是增加了  一个整型变量!但是我现在的信心已经受了一点打击,我将信将疑的看者面试官,他还是微笑  着跟我说:“不错,这个程序确实在效率上有了很大的提高!”我心里一阵暗喜!但他接着说这个程序仍然不能达到他的要求,要我给出更优的方案!天啊!还有优化!我当时真的有点崩  溃了,想了一会后,我请求他给出他的方案!然后他很爽快的给出了他的程序!  

long  fn(long  n)  
{  
        if(n<=0)  
    {  
            printf("error:  n  must  >  0);  
            exit(1);  
        }  
    if(0==n%2)  
    return  (n/2)*(-1);  
    else  
    return  (n/2)*(-1)+n;  
}  

        搞笑,当时我目瞪口呆,没想到他是这个意思,这么简单的代码我真的不会写吗,但是我为  什么没有往那方面上想呢!他说的没有错,在n很大很大的时候这三个程序运行时间的差别简  直是天壤之别!当我刚想开口说点什么的时候,他却先开口了:“不要认为CPU运算速度快就  把所有的问题都推给它去做,程序员应该将代码优化再优化,我们自己能做的决不要让CPU做  ,因为CPU是为用户服务的,不是为我们程序员服务的!”多么精辟的语言,我已经不想再说  什么了!接着是第二个问题:  
他要求我用一种技巧性的编程方法来用一个函数实现两个函数的功能n为如:

fn1(n)=n/2!+n/3!+n/4!+n/5!+n/6!  
fn2(n)=n/5!+n/6!+n/7!+n/8!+n/9!  

现在用一个函数fn(int  n,int  flag)实现,当flag为0时  ,实现fn1功能,如果flag为1时实现fn2功能!他的要求还是效率,效率,效率!说实在话,  如果我心情好的话我应该能给出一种比较好的算法,但我那时真的没有什么心思再想了,
我在  纸上胡乱画了一些诸如6!=6*5!的公式后直截了当的跟他说要他给出他的答案!面试官也没有  说什么,给出了他的思路:
定义一个二维数组  float  t[2][5]存入[2!,3!,4!,5!,6!},{5!  ,6!  ,7!,8!,9!]然后给出一个循环:  

for(i=0;i<6;i++)  
{  
    temp=temp+n/t[flag][i];  
}  

最后得到计算值!呵呵,典型的空间换时间的算法!  
这些总共花了50分钟的时间,还有十分钟我就跟他很随意的聊聊天,聊了一些编程以及生活  的问题,那时的我已经很放松了,因为我知道这次面试结果只有一个:失败。5:30的时候面试官要我等通知,于是我离开了他们公司。这就是面试的整个经过!  

2.由面试想到的  

真的是很失败啊!我记得那天下好大的雨,气温也很低,我边走边想,从5:30一直走到7:30  ,全身都湿透了,又冷又饿,但是我只是一直走,脑子里面充满了疑惑,我也想让雨把自己淋  醒!看到这里有些朋友可能觉得那些面试题目不算什么如果让自己做的话肯定能全部答对,我  肯定相信你,因为我从未怀疑过中国程序员的能力,我认为中国有世界上最好的程序员,我也  从未认为自己是高手,所以我做不出来不代表中国程序员比台湾或者别的地方的程序员差,所  
以我就从我的角度,我的所见所想来谈一些感想:  

不错全世界都有优秀的程序员,中国也不例外,但是我疑惑的是:到底中国和台湾或者国外  的优秀的程序员的比例到底是多少?台湾我不知道,中国100个程序员里有几个是优秀的呢?我  根本算不上,从上面的表现就足以说明一切了!是1个?5个?10个?50个?这个数字我不敢乱  猜,恐遭网友一顿痛骂,那么我们国内有多少人学习计算机呢?拿我们学校来说,计算机97级  4个班,98级5个班,99级10个班,2000级17个班,人多了,老师怎么办?我们学校的做法是让  研究生上课,然后呢?补考一抓一大把,大把大把的补考费落入了学校的口袋,还说现在的学  
生素质低!真是好笑,我都不知道学校这么做是为了什么,为国内培养大量的程序员吗?学生  们能真正学到计算机知识吗?好了,我敢讲,在我们学校学习编程学生和优秀程序员(注意我  指的是优秀,只会编几个糟烂程序的人算不上)的比例应该是100:0.1  在这种比例下虽然我们中国学习编程的人铺天盖地,但是想想有多少个人能真正为中国软件  业发展作出贡献,有多少人能真正写出优秀的程序名扬海外!  

我从学习编程以来,不管是自学还是老师指导,从来都是解决问题就好,编出程序来就行,  我的疑惑是:我们有真正的强调过程序的效率,程序的质量吗?我们有仔细分析过我们写的东  西,看看有没有可以改进的地方,看看有没有简单的方法来达到同样的目的呢?我问心自问,  我发现,我从来没有对我写出来的程序进行过优化,最多就是进行详细的测试,然后Debug,  但是这就足够了吗?这些天我偶尔发现我曾经写过的一个游戏,那是一年做为  其中一员时候,感觉应该拿点东西出来,然后花了一个星期的时间写出来的!程序不算复杂,  
但是用到了不少数据结构的东西,也用到了一些精彩的算法,加上windows的界面和游戏的可  玩性,写完后受到了不少好评,我当时真的很佩服自己!但是现在看呢:没有一句注释,好多  丑陋的函数名比如:void  chushihua(),好多没有必要的变量,可以用简单语句完成工作的我  使用华丽的算法,大量使用全局变量.....,说不好听的话,六百多行的程序除了能运行之外就  
是一陀屎!如果一年前我能听到一些反面意见的话,大概我能早一点觉悟,但是自原代码在  网站发布以来听到的都是赞美之词,没有一个人向我提出程序改进的意见,这又说明了一个什  么问题呢?很值得思考啊!  

还有一个疑惑是:我们说的和做的真的一样吗?我在学校的时候曾经受学院指派承办过一个  计算机大赛,请了一个老师出决赛的题目,主要是一些算法题目,这个老师可能是我上大学以  来唯一敬佩的老师了,从程序调试到打分,对于每个程序都仔细分析其时间效率和空间效率,  然后综合打分,四十个人的卷子,老师从下午三点一直调试到晚上十点,在有些写的精彩的语  句后还加上批注。我真是高兴很遇到这样的老师并且和他做深入的交流,但在事后,却发生了  一件不愉快的事,在比赛中获得第二名的学生找到我,说他程序全部调试成功应该给他满分,  并且应该得第一,我说不过他,最后调出了他的原程序和第一名的原程序对比,错,两个程  序都运行的很好,这时,那个同学开口了:“我的程序写的十分简捷明了,仅仅数行就完成了  题目要求,而他的却写了一大堆,为什么给他的分多过给我的分。”我当时很是气愤,如果不  是老师负责的话,那么现在第一名和第二名的位置真的要互调了,拜托,不是程序的行数越少  程序的质量就越高,我记得我跟他大谈这方面的道理,最后说服他了!哈哈,但是我,只能说  说而已,我不知道还有多少人一样,说起来头头是道,但心里却压根就从未重视过它!  


3.我打算做的!  
其实那天我想到的远不止上面那么多,但是我不想再说了,因为我猜想看这篇文章的网友大  概都有一肚子的感想,一肚子的抱怨,借用这篇文章发泄可不是我想达到的目的,在上面我把  自己骂的一文不值也不是妄自菲薄,但是在某些方面我真的做错了,或者说是偏离了正确方向  ,现在是矫正方向和重整旗鼓的时候了,就象我前面说过的,我相信中国有世界上最好的程序  员,我也相信我的水平不会一直保持现状,我现在就收拾起牢骚真正的实干起来!  真的很巧,就写到这里的时候我在网上偶尔发现了这篇手册,我不知道这预示着什么,但是  我想如果我照下面这个基本原则一直踏实做下去,我一定会实现我的理想---一名优秀的软件设计师!  

(下面这些文字不是我的原创,是我偶尔在网上发现的,我真的很幸运能看到这些,这篇文  章也随着下面的文字而结束,我真心的希望您能从这篇文章中得到启发,这篇文章欢迎大家随  意转载!)  

作者:金蝶中间件公司CTO袁红岗  

不知不觉做软件已经做了十年,有成功的喜悦,也有失败的痛苦,但总不敢称自己是高手,  因为和我心目中真正的高手们比起来,还差的太远。世界上并没有成为高手的捷径,但一些基  本原则是可以遵循的。  

  1.  扎实的基础。数据结构、离散数学、编译原理,这些是所有计算机科学的基础,如果  不掌握他们,很难写出高水平的程序。据我的观察,学计算机专业的人比学其他专业的人更能  写出高质量的软件。程序人人都会写,但当你发现写到一定程度很难再提高的时候,就应该想  想是不是要回过头来学学这些最基本的理论。不要一开始就去学OOP,即使你再精通OOP,遇到  一些基本算法的时候可能也会束手无策。  

  2.  丰富的想象力。不要拘泥于固定的思维方式,遇到问题的时候要多想几种解决问题的  方案,试试别人从没想过的方法。丰富的想象力是建立在丰富的知识的基础上,除计算机以外  ,多涉猎其他的学科,比如天文、物理、数学等等。另外,多看科幻电影也是一个很好的途径  。  

  3.  最简单的是最好的。这也许是所有科学都遵循的一条准则,如此复杂的质能互换原理  在爱因斯坦眼里不过是一个简单得不能再简单的公式:E=mc^2。简单的方法更容易被人理解,  更容易实现,也更容易维护。遇到问题时要优先考虑最简单的方案,只有简单方案不能满足要  求时再考虑复杂的方案。  

  4.  不钻牛角尖。当你遇到障碍的时候,不妨暂时远离电脑,看看窗外的风景,听听轻音  乐,和朋友聊聊天。当我遇到难题的时候会去玩游戏,而且是那种极暴力的打斗类游戏,当负  责游戏的那部分大脑细胞极度亢奋的时候,负责编程的那部分大脑细胞就得到了充分的休息。  当重新开始工作的时候,我会发现那些难题现在竟然可以迎刃而解。  

  5.  对答案的渴求。人类自然科学的发展史就是一个渴求得到答案的过程,即使只能知道  答案的一小部分也值得我们去付出。只要你坚定信念,一定要找到问题的答案,你才会付出精  力去探索,即使最后没有得到答案,在过程中你也会学到很多东西。  

  6.  多与别人交流。三人行必有我师,也许在一次和别人不经意的谈话中,就可以迸出灵  感的火花。多上上网,看看别人对同一问题的看法,会给你很大的启发。  

  7.  良好的编程风格。注意养成良好的习惯,代码的缩进编排,变量的命名规则要始终保  持一致。大家都知道如何排除代码中错误,却往往忽视了对注释的排错。注释是程序的一个重  要组成部分,它可以使你的代码更容易理解,而如果代码已经清楚地表达了你的思想,就不必  再加注释了,如果注释和代码不一致,那就更加糟糕。  
www.best-code.com
  8.  韧性和毅力。这也许是"高手"和一般程序员最大的区别。A  good  programming  is  99  weat  and  1ffee。高手们并不是天才,他们是在无数个日日夜夜中磨练出来的。成功能给  我们带来无比的喜悦,但过程却是无比的枯燥乏味。你不妨做个测试,找个10000以内的素数  表,把它们全都抄下来,然后再检查三遍,如果能够不间断地完成这一工作,你就可以满足这  一条。  
   
  这些是我这几年程序员生涯的一点体会,希望能够给大家有所帮助。
posted @ 2006-08-05 15:10 rendong 阅读(427) | 评论 (2)编辑 收藏
 
作者: 车东 Email: chedongATbigfoot.com/chedongATchedong.com

写于:2003/03 最后更新:

版权声明:可以任意转载,转载时请务必以超链接形式标明文章原始出处和作者信息及本声明
http://www.chedong.com/tech/cms.html

关键词:"content manage system" cms path_info cgi php cache squid 内容管理 mod_rewrite url rewrite 系统 缓存 Cacheable "Search engine friendly"

内容摘要:
你完全不必耐心的看到最后,本文主要说明的是在设计CMS时以下2点注意事项:

  1. 搜索引擎友好(Search engine Friendly):基于PATH_INFO的参数解析使得动态网页在链接(URI)形式上更像静态的目录结构,大大方便网站内容被搜索引擎收录;
  2. 可缓存性(Cache Friendly):CMS本身不要过多考虑“效率”问题,只要页面输出设计的比较Cacheable,效率问题可以通过更前端专业的缓存服务器解决。

后面附有一个简单的利用PATH_INFO机制 + SquidWEB加速模式实现PHP动态网页的静态发布的例子,比起那些能导出静态页面的大型发布系统这种轻量级的动态缓存解决方案只需对原有的动态发布系统做少量的修改,从而大大提高了原有内容发布系统的重用度。

网站内容静态发布的重要性:Cacheable / Search Engine Friendly

由于一个动态页面的速度往往会比静态页面慢2-10倍,因此对于一个访问量逐步向百万级发展的网站来说,访问速度很快成为一个瓶颈。除了优化内容发布系统的应用本身外,如果能把更新频率比较低的动态页面转存成静态网页来发布,速度上的提升效果将是显著的,而静态网页如果能被缓存在内存里,访问速度更会比原有动态网页有2-3个数量级的提高。

在国外内容管理系统(CMS)已经是一个非常成熟的行业,能够真正支撑大访问的系统中静态页面输出和缓存系统几乎是必须的。
<a href='http://www.best-code.com'>www.best-code.com</a>
此外随着互联网上的内容以惊人速度的增长也越来越突出了搜索引擎的重要性,如果网站想更好地被搜索引擎收录,网站设计除了面向用户友好(User Friendly)外,面向搜索引擎友好的设计也是非常重要的。链接地址相对固定的静态网页比较适合搜索引擎索引,动态网页后面跟的参数灵活度很大,因此很多搜索引擎都往往会忽略动态页面,比如:对于news.php?day=22&month=03&year=2003,很多搜索引擎可能只索引news.php这个页面一次,更多其他参数的页面可能都会当成相似内容滤掉;我个人一直怀疑在搜索引擎中:即使是同样内容,静态页面往往也比动态网页的PageRank高。

因此,将动态页面转换成静态页面,无论从效率上还是面向搜索引擎友好上,都是一个门户级内容发布系统必须面对的问题。

静态缓存和动态缓存的比较

静态页面的缓存可能有2种形式:

  1. 静态缓存:是在新内容发布的同时就立刻生成相应内容的静态页面,比如:2003年3月22日,管理员通过后台内容管理界面录入一篇新闻后,就立刻生成http://www.chedong.com/tech/2003/03/22/001.html这个静态页面,并同步更新http://www.chedong.com/tech/index.html这个静态页面上的相关链接。
  2. 动态缓存:是在新内容发布以后,并不预先生成相应的静态页面,直到对相应内容发出请求时,如果前台缓存服务器找不到相应缓存,就向后台内容管理服务器发出请求,后台系统会生成相应内容的静态页面,用户第一次访问页面时可能会慢一点,但是以后就是直接访问缓存了。
    如果去ZDNet等国外网站会发现他们使用的基于Vignette内容管理系统都有这样的页面名称:0,22342566,300458.html。其实这里的0,22342566,300458就是用逗号分割开的多个参数:
    第一次访问找不到页面后,相当于会在服务器端产生一个doc_type=0&doc_id=22342566&doc_template=300458的查询,
    而查询结果会生成的缓存的静态页面:0,22342566,300458.html

静态缓存的缺点:

  • 复杂的触发更新机制:这两种机制在内容管理系统比较简单的时候都是非常适用的。但对于一个关系比较复杂的网站来说,页面之间的逻辑引用关系就成为一个非常非常复杂的问题。最典型的例子就是一条新闻要同时出现在新闻首页和相关的3个新闻专题中,在静态缓存模式中,每发一篇新文章,除了这篇新闻内容本身的页面外,还需要系统通过触发器生成多个新的相关静态页面,这些相关逻辑的触发也往往就会成为内容管理系统中最复杂的部分之一。
  • 旧内容的批量更新: 通过静态缓存发布的内容,对于以前生成的静态页面的内容很难修改,这样用户访问旧页面时,新的模板根本无法生效。

在动态缓存模式中,内容管理系统只需要关心各个页面自身,而相关的其他页面链接能自动更新,从而大大减少了设计触发器设计的需要。

VIGNETTE的动态缓存虽然很好,但是一个系统如果设计得太全面其实也是有很大危险的:如果一个频道下文章很多:比如达到十万时,如果生成的静态页面都在一个目录下,对系统文件系统是一个极大的危害,因为一个目录下文件个数超过3000效率就会非常差,甚至连rm *时都会出现too many arguments错误。

简单的说,一个好的内容管理系统应该包括:
  1. 输入:方便的内容录入,所见即所得的编辑界面,权限控制等……
  2. 输出:方便的模板管理,缓存发布等……
设计或寻找这样一个系统如果考虑功能全面和高集成度,你会发现只有那些几十万$以上的专业内容发布系统才能你满足所有的需求。

以前做应用的时候也用过一些方式:应用首次访问以后将生成的内容存成一个缓存文件,下次请求时从缓存目录中读取缓存文件,内容更新时,应用把内容从缓存目录中删掉,从而减少对数据库的访问。虽然这样做也能承载比较大的负载,但这样的内容管理和缓存一体的系统是很难分离的。

如果换一个思路:通过一定的分工现内容管理和缓存机制2者的分离,你会发现无论哪一方面可选的余地都是非常大的。甚至有可能利用目前的已经是“功能”比较全面的内容管理系统,而让所有“效率”问题都由前台更专业,而且是很容易分布的缓存服务器解决:可以是通过开放源代码的SQUID做反相代理的WEB加速,可以是专门的缓存硬件设备,甚至是专业的缓存服务商。

动态缓存必须有一个基于静态链接本身的参数解析过程,很多专业内容管理系统系统都是将参数解析机制做成了WEB服务器的模块实现的。

我们可以把以前的HTTP/GET方式的?key=value改为直接用/value1/value2的方式来传递,从而实现了动态页面的静态URL形式。而缓存只需要在前端加上一层CACHE服务器,比如:Squid。网站动态内容的动态缓存发布就可以实现了。

按照这个机制实现的发布系统很好地体现了应用系统的分工:把复杂地内容管理系统分解成:内容输入和缓存这2个相对简单的系统实现。而中间的内容发布通过URL REWRITE或PATH_INFO解决动态页面的参数传递:

  • 后台:内容管理系统,专心的将内容发布做好,比如:复杂的工作流管理,复杂的模板规则等……
  • 前台:页面的缓存管理则可以使用缓存软件(比如前台80端口使用SQUID对后台8080的内容发布管理系统进行缓存),缓存硬件,甚至交给缓存服务商。
______________________             ___________________
|Squid Software cache| |F5 Hardware cache|
---------------------- -------------------
\ /
\ ________________ /
|ASP |JSP |PHP |
PATH_INFO Based Content Manage System
----------------

    把URI地址用作参数传递:URL REWRITE和PATH_INFO

    最近看到很多关于面向搜索引擎URL设计优化(URI Pretty)的文章,提到了很多利用一定机制将动态网页参数变成像静态网页的形式:
    比如可以将:http://www.chedong.com/phpMan.php?mode=man&parameter=ls
    变成:http://www.chedong.com/phpMan.php/man/ls

    最简单的是基于各种WEB服务器中的URL重写转向(Rewrite)模块的URL转换:这样几乎可以不修改程序的实现将news.asp?id=234的映射成news/234.html

    Apache上有一个模块(非缺省):mod_rewrite:当然URL REWRITE的强大功能还远远不止于此。

    当我需要将将news.asp?id=234的映射成news/234.html时:只需设置:
    RewriteRule /news/(\d+)\.html /news\.asp\?id=$1 [N,I]
    这样就把 /news/234.html 映射成了 /news.asp?id=234
    当有对/news/234.html的请求时:web服务器会把实际请求转发给/news.asp?id=234

    而在IIS也有相应的REWRITE模块:比如ISAPI REWRITEIIS REWRITE,语法都是基于正则表达式,因此语法是几乎相同的:

    比对于某一个简单应用可以是:
    RewriteRule /news/(\d+)? /news\.asp\?id=$1 [N,I]
    这样就把 /news/234 映射到了 /news.asp?id=234

    如我需要把 http://www.myhost.com/foo.php?a=A&b=B&c=C 表现成 http://www.myhost.com/foo.php/a/A/b/B/c/C。而一个更通用的能够将所有的动态页面进行参数映射的表达式是:
    RewriteRule (.*?\.php)(\?[^/]*)?/([^/]*)/([^/]*)(.+?)? $1(?2$2&:\?)$3=$4?5$5: [N,I]

    通过URL REWRITE还有一个好处就是隐藏后台实现:
    比如我们需要将应用从news.asp?id=234迁移成news.php?query=234时,前台的表现可以一直保持为news/234.html。从实现应用和前台表现的分离:保持了URL的稳定性,在实现后台应用平台的迁移时非常有用。使用mod_rewrite甚至可以把请求转发到其他后台服务器上:


    另外一个方式就是基于PATH_INFO:
    PATH_INFO是一个CGI 1.1的标准,所有直接跟在CGI或动态页面app.cgi后面的"/value_1/value_2"就是PATH_INFO参数:
    比如http://www.chedong.com/phpMan.php/man/ls,中:$PATH_INFO = "/man/ls"

    PATH_INFO是CGI标准,因此PHP Servlet等都有比较好的支持。比如Servlet中就有request.getPathInfo()方法。
    注意:/myapp/servlet/Hello/foo的getPathInfo()返回的是/foo,而/myapp/dir/hello.jsp/foo的getPathInfo()将返回的/hello.jsp,从这里你也可以知道jsp其实就是一个Servlet的PATH_INFO参数。ASP不支持PATH_INFO,

    PHP中基于PATH_INFO的参数解析的例子如下:
    //注意:第一个参数是空的,参数按"/"分割
    if ( isset($_SERVER["PATH_INFO"]) ) {
        list($nothing, $day, $id) = explode('/', $_SERVER["PATH_INFO"]);
    }

    如何隐蔽应用:例如.php,的扩展名:
    在APACHE中这样配置:
    <FilesMatch "^app_name$">
        ForceType application/x-httpd-php
    </FilesMatch>

    如何更像静态页面:app_name/my/app.html
    解析的PATH_INFO参数的时候,把最后一个参数的最后5个字符“.html”截断即可。
    注意:APACHE2中缺省是不允许PATH_INFO的,需要设置
    AcceptPathInfo on

    特别是针对使用虚拟主机用户,无权安装和配置mod_rewrite的时候,PATH_INFO往往就成了唯一的选择。

    虽然通过修改设置SQUID也可以对带?的动态页面进行缓存,但为了方便搜索引擎收录索引,还是将参数改成PATH_INFO比较好。

    OK,这样以后看见类似于http://www.example.com/article/234这样的网页你就知道其实是article/show.php?id=234这个php程序生成的动态网页,很多站点表面看上去可能有很多静态目录,其实很有可能都是使用1,2个程序实现的内容发布。比如很多WIKIWIKI系统都使用了这个机制:整个系统就一个简单的wiki程序,而看上去的目录其实都是这个应用拿后面的地址作为参数的查询结果。

    利用基于MOD_REWRITE/PATH_INFO + CACHE服务器的解决方案对原有的动态发布系统进行改造,也可以大大降低旧有系统升级到新的内容管理系统的成本。

    面向缓存的页面设计

    让页面能够比较好的被缓存服务器缓存,必须在产生内容的WEB服务器上设置,让返回内容的HTTP HEADER中加入"Last-Modified"和"Expires"声明,比如:
    Last-Modified: Wed, 14 May 2003 13:06:17 GMT
    Expires: Fri, 13 Jun 2003 13:06:17 GMT
    以允许前端SQUID服务器缓存:
    • 页面必须包含Last-Modified: 标记,一般纯静态页面本身都会有Last-Modified信息,动态页面需要通过函数强制加上,比如PHP中:
          // always modified now
          header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");

    • 必须有Expires或Cache-Control: max-age标记设置页面的过期时间:
      对于静态页面,通过apache的mod_expires根据页面的MIME类型设置缓存周期:比如图片缺省是1个月,HTML页面缺省是2天等。
      <IfModule mod_expires.c>
          ExpiresActive on
          ExpiresByType image/gif "access plus 1 month"
          ExpiresByType text/css "now plus 2 day"
          ExpiresDefault "now plus 1 day"
      </IfModule>

      对于动态页面,则可以直接通过写入HTTP返回的头信息,比如对于新闻首页index.php可以是20分钟,而对于具体的一条新闻页面可能是1天后过期。比如:在php中加入了1个月后过期:
          // Expires one month later
          header("Expires: " .gmdate ("D, d M Y H:i:s", time() + 3600 * 24 * 30). " GMT");

    • 如果服务器端有基于HTTP的认证,必须有Cache-Control: public标记
    ASP应用的缓存设计:
    首先在公用的包含文件中(比如include.asp)加入以下公用函数:
    <%
    ' Converts date (19991022 11:08:38) to http form (Fri, 22 Oct 1999 12:08:38 GMT)
    Function DateToHTTPDate(ByVal OleDATE)
      Const GMTdiff = #08:00:00#
      OleDATE = OleDATE - GMTdiff
      DateToHTTPDate = engWeekDayName(OleDATE) & _
        ", " & Right("0" & Day(OleDATE),2) & " " & engMonthName(OleDATE) & _
        " " & Year(OleDATE) & " " & Right("0" & Hour(OleDATE),2) & _
        ":" & Right("0" & Minute(OleDATE),2) & ":" & Right("0" & Second(OleDATE),2) & " GMT"
    End Function

    Function engWeekDayName(dt)
        Dim Out
        Select Case WeekDay(dt,1)
            Case 1:Out="Sun"
            Case 2:Out="Mon"
            Case 3:Out="Tue"
            Case 4:Out="Wed"
            Case 5:Out="Thu"
            Case 6:Out="Fri"
            Case 7:Out="Sat"
        End Select
        engWeekDayName = Out
    End Function

    Function engMonthName(dt)
        Dim Out
        Select Case Month(dt)
            Case 1:Out="Jan"
            Case 2:Out="Feb"
            Case 3:Out="Mar"
            Case 4:Out="Apr"
            Case 5:Out="May"
            Case 6:Out="Jun"
            Case 7:Out="Jul"
            Case 8:Out="Aug"
            Case 9:Out="Sep"
            Case 10:Out="Oct"
            Case 11:Out="Nov"
            Case 12:Out="Dec"
        End Select
        engMonthName = Out
    End Function
    %>

    然后在具体的页面中,比如index.asp和news.asp的“最上面”加入以下代码:HTTP Header

    <!--#include file="../include.asp"-->
    <%
    ' set Page Last-Modified Header:
    ' Converts date (19991022 11:08:38) to http form (Fri, 22 Oct 1999 12:08:38 GMT)
    Response.AddHeader "Last-Modified", DateToHTTPDate(Now())

    ' The Page Expires in Minutes
    Response.Expires = 60

    ' Set cache control to externel applications
    Response.CacheControl = "public"
    %>

    其中Response.Expires 是设置页面过期时间的:单位是分钟

    如何检查目前站点页面的可缓存性(Cacheablility)呢?可以参考以下2个站点上的工具:
    http://www.ircache.net/cgi-bin/cacheability.py

    面向缓存的站点规划

    一个利用SQUID的Transparent对多个站点进行做WEB加速http acceleration方案:

    原先一个站点的规划可能是这样的:
    200.200.200.207 www.chedong.com
    200.200.200.208 news.chedong.com
    200.200.200.209 bbs.chedong.com
    200.200.200.205 images.chedong.com

    在SQUID模式下:所有站点都通过外部DNS指向到同一个IP:200.200.200.200/201这2台SQUID缓存服务器上(使用2台是为了冗余备份)
                              _____________________   ________
    www.chedong.com 请求 \ | Squid cache box | | | / 192.168.0.4 www.chedong.com
    news.chedong.com 请求 -| 200.200.200.200/201 |-|firewall| - 192.168.0.4 news.chedong.com
    bbs.chedong.com 请求 / | /etc/hosts | | box | \ 192.168.0.3 bbs.chedong.com
    --------------------- --------
    编译和配置过程:
    1. ./configure --enable-referer-log --disable-internal-dns
      --disable-internal-dns:禁用SQUID的DNS解析
      --enable-referer-log:便于APACHE COMBINED格式日志生成

    2. 配置:
      http_port 80
      httpd_accel_host virtual
      httpd_accel_port 8000
      httpd_accel_uses_host_header on

      # accelerater my domain only
      acl acceleratedHosts dstdom_regex chedong.com
      # accelerater http protocol on port 80
      acl acceleratedProtocol protocol HTTP
      acl acceleratedPort port 80
      # access arc
      acl all src 0.0.0.0/0.0.0.0

      # Allow requests when they are to the accelerated machine AND to the
      # right port with right protocol
      http_access allow acceleratedProtocol acceleratedPort acceleratedHosts
      http_access allow all
    在/etc/hosts中:加入内部的DNS解析,比如:
    192.168.0.4 www.chedong.com
    192.168.0.4 news.chedong.com
    192.168.0.3 bbs.chedong.com

    工作原理:
    SQUID服务器上关闭了DNS解析,这样,请求外部过来时,设置SQUID根据/etc/hosts文件进行内部DNS解析。这样,服务器请求就可以转发到我们指定的内部地址上。

    使用SQUID的反相代理加速,我们不仅可以得到性能上的提升,而且还能获得额外的安全性和配置的灵活度:
    • 配置灵活性提高:可以自己在内部服务器上控制后台服务器的DNS解析,当需要在服务器之间做迁移调整时,就不用大量修改外部DNS配置了,只需要修改内部DNS实现服务的调整。
    • 数据安全性增加:所有后台服务器可以很方便的被保护在防火墙内。
    • 后台应用设计复杂程度降低:原先为了效率常常需要建立专门的图片服务器images.chedong.com和负载比较高的应用服务器bbs.chedong.com分离,在SQUID加速模式中,所有前台请求都通过SQUID服务器:实际上就都是静态页面,这样,应用设计时就不用考虑图片和应用本身分离了,也大大降低了后台内容发布系统设计的复杂程度,由于数据和应用都存放在一起,也方便了文件系统的维护和管理。
    小节:
    • 大访问量的网站应尽可能将动态网页生成静态页面作为缓存发布,甚至对于搜索引擎这样的动态应用来说,缓存机制也是非常非常重要的。
    • 利用PATH_INFO机制进行解析参数,实现动态网页链接的美化,方便搜索引擎的索引;
    • 在动态页面中利用HTTP Header定义缓存更新策略。
    • 利用缓存服务器获得额外的配置和安全性
    • 日志非常重要:SQUID日志缺省不支持COMBINED日志,但REFERER日志对于站点分析非常重要,在GNU/Linux可以用以下方式生成:
      pr -mJt access.log referer.log | awk '{print $1" "$2" "$3" "$4" "$5" "$6" "$7" "$8" "$9" "$10" \x22"$14"\x22 \x22"$11"\x22"}'  > combined.log
          -m merge
          -J join line
          -t omit header and footer

    附1:SQUID性能测试试验

    phpMan.php是一个基于php的man page server,每个man page需要调用后台的man命令和很多页面格式化工具,系统负载比较高,提供了Cache Friendly的URL,以下是针对同样的页面的性能测试资料:
    测试环境:Redhat 8 on Cyrix 266 / 192M Mem
    测试程序:使用apache的ab(apache benchmark):
    测试条件:请求50次,并发50个连接
    测试项目:直接通过apache 1.3 (80端口) vs squid 2.5(8000端口:加速80端口)

    测试1:无CACHE的80端口动态输出:
    ab -n 100 -c 10 http://www.chedong.com:81/phpMan.php/man/kill/1
    This is ApacheBench, Version 1.3d <$Revision: 1.58 $> apache-1.3
    Copyright (c) 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
    Copyright (c) 1998-2001 The Apache Group, http://www.apache.org/

    Benchmarking localhost (be patient).....done
    Server Software:        Apache/1.3.23                                     
    Server Hostname:        localhost
    Server Port:            80

    Document Path:          /phpMan.php/man/kill/1
    Document Length:        4655 bytes

    Concurrency Level:      5
    Time taken for tests:   63.164 seconds
    Complete requests:      50
    Failed requests:        0
    Broken pipe errors:     0
    Total transferred:      245900 bytes
    HTML transferred:       232750 bytes
    Requests per second:    0.79 [#/sec] (mean)
    Time per request:       6316.40 [ms] (mean)
    Time per request:       1263.28 [ms] (mean, across all concurrent requests)
    Transfer rate:          3.89 [Kbytes/sec] received

    Connnection Times (ms)
                  min  mean[+/-sd] median   max
    Connect:        0    29  106.1      0   553
    Processing:  2942  6016 1845.4   6227 10796
    Waiting:     2941  5999 1850.7   6226 10795
    Total:       2942  6045 1825.9   6227 10796

    Percentage of the requests served within a certain time (ms)
      50%   6227
      66%   7069
      75%   7190
      80%   7474
      90%   8195
      95%   8898
      98%   9721
      99%  10796
     100%  10796 (last request)

    测试2:SQUID缓存输出
    /home/apache/bin/ab -n50 -c5 "http://localhost:8000/phpMan.php/man/kill/1"
    This is ApacheBench, Version 1.3d <$Revision: 1.58 $> apache-1.3
    Copyright (c) 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
    Copyright (c) 1998-2001 The Apache Group, http://www.apache.org/

    Benchmarking localhost (be patient).....done
    Server Software:        Apache/1.3.23                                     
    Server Hostname:        localhost
    Server Port:            8000

    Document Path:          /phpMan.php/man/kill/1
    Document Length:        4655 bytes

    Concurrency Level:      5
    Time taken for tests:   4.265 seconds
    Complete requests:      50
    Failed requests:        0
    Broken pipe errors:     0
    Total transferred:      248043 bytes
    HTML transferred:       232750 bytes
    Requests per second:    11.72 [#/sec] (mean)
    Time per request:       426.50 [ms] (mean)
    Time per request:       85.30 [ms] (mean, across all concurrent requests)
    Transfer rate:          58.16 [Kbytes/sec] received

    Connnection Times (ms)
                  min  mean[+/-sd] median   max
    Connect:        0     1    9.5      0    68
    Processing:     7    83  537.4      7  3808
    Waiting:        5    81  529.1      6  3748
    Total:          7    84  547.0      7  3876

    Percentage of the requests served within a certain time (ms)
      50%      7
      66%      7
      75%      7
      80%      7
      90%      7
      95%      7
      98%      8
      99%   3876
     100%   3876 (last request)

    结论:No Cache / Cache = 6045 / 84 = 70
    结论:对于可能被缓存请求的页面,服务器速度可以有2个数量级的提高,因为SQUID是把缓存页面放在内存里的(因此几乎没有硬盘I/O操作)。

    附2:一个CACHE多主机APACHE服务的SQUID安装配置:

    squid的编译:
    ./configure --enable-useragent-log  --enable-referer-log --enable-default-err-language=Simplify_Chinese --enable-err-languages="Simplify_Chinese English" --disable-internal-dns 
    make
    #make install
    #cd /usr/local/squid
    make dir cache
    chown squid.squid *
    vi /usr/local/squid/etc/squid.conf

    ---------------------cut here----------------------------------
    # visible name
    visible_hostname cache.example.com

    # cache config: space use 1G and memory use 256M
    cache_dir ufs /usr/local/squid/cache 1024 16 256
    cache_mem 256 MB
    cache_effective_user squid
    cache_effective_group squid


    http_port 80
    httpd_accel_host virtual
    httpd_accel_single_host off
    httpd_accel_port 80
    httpd_accel_uses_host_header on
    httpd_accel_with_proxy on

    # accelerater my domain only
    acl acceleratedHostA dstdomain .example1.com
    acl acceleratedHostB dstdomain .example2.com
    acl acceleratedHostC dstdomain .example3.com
    # accelerater http protocol on port 80
    acl acceleratedProtocol protocol HTTP
    acl acceleratedPort port 80
    # access arc
    acl all src 0.0.0.0/0.0.0.0

    # Allow requests when they are to the accelerated machine AND to the
    # right port with right protocol
    http_access allow acceleratedProtocol acceleratedPort acceleratedHostA
    http_access allow acceleratedProtocol acceleratedPort acceleratedHostB
    http_access allow acceleratedProtocol acceleratedPort acceleratedHostC

    # logging
    emulate_httpd_log on
    referer_log /usr/local/squid/var/logs/referer.log
    useragent_log /usr/local/squid/var/logs/agent.log


    ----------------------cut here---------------------------------

    创建缓存目录:
    /usr/local/squid/sbin/squid -z

    启动squid
    /usr/local/squid/sbin/squid

    停止squid:
    /usr/local/squid/sbin/squid -k shutdown

    启用新配置:
    /usr/local/squid/sbin/squid -k reconfig

    通过crontab每天0点截断/轮循日志:
    0 0 * * * (/usr/local/squid/sbin/squid -k rotate)

    附3:如何在IIS上利用PHP支持PATH_INFO

    PHP的ISAPI模式安装备忘:只试成 php-4.2.3-Win32

    解包目录
    ========
    php-4.2.3-Win32.zip c:\php

    PHP.INI初始化文件
    =================
    复制:c:\php\php.ini-dist 到 c:\winnt\php.ini

    配置文件关联
    ============
    按照install.txt中的说明配置文件关联

    运行库文件
    ==========
    复制 c:\php\php4ts.dll 到 c:\winnt\system32\php4ts.dll

    这样运行后:会发现php把PATH_INFO映射到了物理路径上
    Warning: Unknown(C:\CheDong\Downloads\ariadne\www\test.php\path): failed to create stream: No such file or directory in Unknown on line 0

    Warning: Unknown(): Failed opening 'C:\CheDong\Downloads\ariadne\www\test.php\path' for inclusion (include_path='.;c:\php4\pear') in Unknown on line 0

    安装ariadne的PATCH
    ==================
    停止IIS服务
    net stop iisadmin
    ftp://ftp.muze.nl/pub/ariadne/win/iis/php-4.2.3/php4isapi.dll
    覆盖原有的c:\php\sapi\php4isapi.dll

    注:ariadne是一个基于PATH_INFO的内容发布系统

    PHP 4.3.2 RC2中CGI模式的PATH_INFO已经修正,照常安装即可。
    www.best-code.com

    参考资料:

    CMS行业观察
    http://www.cmswatch.com

    CMS讨论邮件列表
    http://www.cms-list.org

    一个基于PATH_INFO的开源内容管理系统
    http://typo3.com/

    商业的和开源CMS项目列表:
    http://directory.google.com/Top/Computers/Software/Internet/Site_Management/Content_Management/

    搜索引擎友好的URL设计
    http://www.sitepoint.com/article/485
    说不定这个URL原来就是articel.php?id=485

    HTTP代理缓存
    http://vancouver-webpages.com/proxy.html

    可缓存的页面设计
    http://linux.oreillynet.com/pub/a/linux/2002/02/28/cachefriendly.html

    相关RFC文档:

    可缓存性检查:
    http://www.web-caching.com/cacheability.html

    URL Rewrite文档:
    http://www.isapirewrite.com/docs/
    http://httpd.apache.org/docs/mod/mod_rewrite.html
    http://httpd.apache.org/docs-2.0/mod/mod_rewrite.html

    原文出处:<a href="http://www.chedong.com/tech/cms.html">http://www.chedong.com/tech/cms.html</a>

    posted @ 2006-08-05 15:02 rendong 阅读(541) | 评论 (1)编辑 收藏
     
    DOCTYPE(文档类型)DOCTYPE是document  type(文档类型)的简写,用来说明你用的XHTML或者HTML是什么版本。  
    <a href='http://www.best-code.com'>www.best-code.com</a>
    他们是什么和他们为什么是重要的?
    所有的HTML和XHTML文档必须有一个有效的doctype声明。  

    Doctype规定了文档使用的HTML或XHTML的版本。  

    当校验的时候doctype被校验器使用,WEB浏览器通过它来决定那种渲染模式被使用。  

    Doctype影响设备渲染web页面的方式。  

    如果文档使用了正确的doctype,一些浏览器将切换到标准模式,那意味着浏览器会遵守更多的CSS规则。  

    如果文档使用了正确的doctype,文档会渲染更快,因为浏览器不需要花时间去思考怎样正确地去渲染HTML。  
    主要的几种DOCTYPE
    HTML  4.01  Strict  
    <!DOCTYPE  HTML  PUBLIC  "-//W3C//DTD  HTML  4.01//EN"  "http://www.w3.org/TR/html4/strict.dtd">  

    在上面的声明中,声明了文档的根元素是html,它在公共标识符为"-//W3C//DTD  HTML  4.01//EN"的DTD中进行了定义。浏览器将明白如何寻找匹配此公共标识符的DTD。如果找不到,浏览器将使用公共标识符后面的URL作为寻找DTD的位置。

    HTML  4.01  Transitional
    <!DOCTYPE  HTML  PUBLIC  "-//W3C//DTD  HTML  4.01  Transitional//EN"  "http://www.w3.org/TR/html4/loose.dtd">

    HTML  4.01  Frameset  
    <!DOCTYPE  HTML  PUBLIC  "-//W3C//DTD  HTML  4.01  Frameset//EN"  "http://www.w3.org/TR/html4/frameset.dtd">  

    XHTML  1.0  Strict  
    <!DOCTYPE  html  PUBLIC  "-//W3C//DTD  XHTML  1.0  Strict//EN"  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">  

    XHTML  1.0  Transitional  
    <!DOCTYPE  html  PUBLIC  "-//W3C//DTD  XHTML  1.0  Transitional//EN"  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

    XHTML  1.0  Frameset  
    <!DOCTYPE  html  PUBLIC  "-//W3C//DTD  XHTML  1.0  Frameset//EN"  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">  

    XHTML  1.1  
    <!DOCTYPE  html  PUBLIC  "-//W3C//DTD  XHTML  1.1//EN"  "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">  

    Standards  compliant  (or  strict)  and  quirks  modes
    大多数浏览器有两种模式:怪异模式(quirks  mode  使用老的规则)和严格模式(strict  mode遵守标准)。Mozilla,  Safari,  Opera,  Mac/Explorer,  Windows/Explorer  6实现了这两种模式。Windows/Explorer  5和老的浏览器像Netscape  4执行的是怪异模式。

    使用严格模式(strict  mode  或者说遵守标准的模式)有一些非常重要的原因。例如,Windows/Explorer  6  将会使用正确的盒模型(box  model)当使用strict  mode  时,而使用quirks  mode  时会执行错误的盒模型。另外在quirks  mode下,一些现代的浏览器将不允许fonts被继承。

    XML声明需要吗?
    W3C推荐在XHTML文档里使用XML声明,但也不是必须要这么做。所以这就要看开发者本人来决定了。如果有了这个声明,Windows/IE6将会忽略任何使用的doctype  而执行quirks  mode。XML声明看起来如下示:
    <?xml  version="1.0"  encoding="utf-8"?>  
    posted @ 2006-08-05 15:00 rendong 阅读(243) | 评论 (0)编辑 收藏
     
    head区是指首页HTML代码的<head>和</head>之间的内容。  
    必须加入的标签  
    <a href='http://www.best-code.com'>www.best-code.com</a>
    1.公司版权注释  
    <!---  The  site  is  designed  by  Maketown,Inc  06/2000  --->  

    2.网页显示字符集  
    简体中文:<META  HTTP-EQUIV="Content-Type"  CONTENT="text/html;  charset=gb2312">  
    繁体中文:<META  HTTP-EQUIV="Content-Type"  CONTENT="text/html;  charset=BIG5">  
    英  语:<META  HTTP-EQUIV="Content-Type"  CONTENT="text/html;  charset=iso-8859-1">  

    3.网页制作者信息  
    <META  name="author"  content="hhlong.com">  

    4.网站简介  
    <META  NAME="DESCRIPTION"  CONTENT="xxxxxxxxxxxxxxxxxxxxxxxxxx">  

    5.搜索关键字  
    <META  NAME="keywords"  CONTENT="xxxx,xxxx,xxx,xxxxx,xxxx,">  

    6.网页的css规范  
    <LINK  href="style/style.css"  rel="stylesheet"  type="text/css">  
    (参见目录及命名规范)  

    7.网页标题  
    <title>xxxxxxxxxxxxxxxxxx</title>  

    .可以选择加入的标签  

    1.设定网页的到期时间。一旦网页过期,必须到服务器上重新调阅。  
    <META  HTTP-EQUIV="expires"  CONTENT="Wed,  26  Feb  1997  08:21:57  GMT">  

    2.禁止浏览器从本地机的缓存中调阅页面内容。  
    <META  HTTP-EQUIV="Pragma"  CONTENT="no-cache">  

    3.用来防止别人在框架里调用你的页面。  
    <META  HTTP-EQUIV="Window-target"  CONTENT="_top">  

    4.自动跳转。  
    <META  HTTP-EQUIV="Refresh"  CONTENT="5;URL=http://www.hhlong.com">  
    5指时间停留5秒。  

    5.网页搜索机器人向导.用来告诉搜索机器人哪些页面需要索引,哪些页面不需要索引。  
    <META  NAME="robots"  CONTENT="none">  
    CONTENT的参数有all,none,index,noindex,follow,nofollow。默认是all。  

    6.收藏夹图标  
    <link  rel  =  "Shortcut  Icon"  href="favicon.ico">  

    所有的javascript的调用尽量采取外部调用.  
    <SCRIPT  LANGUAGE="javascript"  SRC="script/xxxxx.js"></SCRIPT>  
    www.best-code.com
    附<body>标签:  
    <body>标签不属于head区,这里强调一下,为了保证浏览器的兼容性,必须设置页面背景<body  bgcolor="#FFFFFF">  
    posted @ 2006-08-05 14:58 rendong 阅读(165) | 评论 (0)编辑 收藏
     
    想表达的意图 处理方式
    (回车换行) <br>
     (空格符) &nbsp;
    &(AND符号) &amp;
    <(左尖括号、小于号) &lt;
    >(右尖括号、大于号) &gt;
    °(度) &deg;
    •(间隔符) &#149;
    ´ &acute;
    " &quot;
    &ldquo;
    &rdquo;
    &permil;
    &larr;
    &uarr;
    &rarr;
    &darr;
    &harr;
    &radic;
    &prop;
    &infin;
    &ang;
    &and;
    &or;
    &cap;
    &cup;
    Ø &Oslash;
    &int;
    &there4;
    &asymp;
    ¥(RMB) &yen;

    <a href='http://www.best-code.com'>www.best-code.com</a>
    想表达的意图 处理方式
    &ne;
    &equiv;
    &le;
    &ge;
    &oplus;
    λ &lambda;
    μ &mu;
    ν &nu;
    ξ &xi;
    ν &nu;
    ξ &xi;
    &prod;
    &sum;
    ¥ &yen;
    &#133;
    ¹(一次方符号) &sup1;
    ²(平方符号) &sup2;
    ³(立方符号) &sup3;
    nr(上标情形) n<sup>r</sup>
    Ci(下标情形) C<sub>i</sub>
    加粗 <b>加粗</b>
    斜体 <i>斜体</i>
    下划线 <u>下划线</u>
    ±(加减符号) &plusmn;
    ×(乘法符号) &times;
    ÷(除法符号) &divide;
    ©(版权所有) &copy;
    ®(注册商标) &reg;
    ™(商标符号) &trade;
    —(破折号) &#151;
    posted @ 2006-08-05 14:56 rendong 阅读(499) | 评论 (0)编辑 收藏
     
    1.  什么是robots.txt文件?
            搜索引擎通过一种程序robot(又称spider),自动访问互联网上的网页并获取网页信息。
            您可以在您的网站中创建一个纯文本文件robots.txt,在这个文件中声明该网站中不想被robot访问的部分,这样,该网站的部分或全部内容就可以不被搜索引擎收录了,或者指定搜索引擎只收录指定的内容。
    <a href='http://www.best-code.com'>www.best-code.com</a>
            
        2.  robots.txt文件放在哪里?
            robots.txt文件应该放在网站根目录下。举例来说,当  robots访问一个网站(比如http://www.abc.com)时,首先会检查该网站中是否存在http:  //www.abc.com/robots.txt这个文件,如果机器人找到这个文件,它就会根据这个文件的内容,来确定它访问权限的范围。
            网站  URL        相应的  robots.txt的  URL
            http://www.w3.org/        http://www.w3.org/robots.txt
            http://www.w3.org:80/        http://www.w3.org:80/robots.txt
            http://www.w3.org:1234/        http://www.w3.org:1234/robots.txt
            http://w3.org/        http://w3.org/robots.txt

            
        3.  robots.txt文件的格式
            "robots.txt"文件包含一条或更多的记录,这些记录通过空行分开(以CR,CR/NL,  or  NL作为结束符),每一条记录的格式如下所示:
            "<field>:<optionalspace><value><optionalspace>"。

            在该文件中可以使用#进行注解,具体使用方法和UNIX中的惯例一样。该文件中的记录通常以一行或多行User-agent开始,后面加上若干Disallow行,详细情况如下:

            User-agent:
              该项的值用于描述搜索引擎robot的名字,在"robots.txt"文件中,如果有多条User-agent记录说明有多个robot会受到该协议的限制,对该文件来说,至少要有一条User-agent记录。如果该项的值设为*,则该协议对任何机器人均有效,在"robots.txt"文件中,  "User-agent:*"这样的记录只能有一条。

            Disallow:
              该项的值用于描述不希望被访问到的一个URL,这个  URL可以是一条完整的路径,也可以是部分的,任何以Disallow开头的URL均不会被robot访问到。例如"Disallow:/help"对  /help.html  和/help/index.html都不允许搜索引擎访问,而"Disallow:/help/"则允许robot访问/help.html,而不能访问  /help/index.html。任何一条Disallow记录为空,说明该网站的所有部分都允许被访问,在"/robots.txt"文件中,至少要有一条Disallow记录。如果"/robots.txt"是一个空文件,则对于所有的搜索引擎robot,该网站都是开放的。

        4.  robots.txt文件用法举例
            例1.  禁止所有搜索引擎访问网站的任何部分        User-agent:  *
            Disallow:  /
            例2.  允许所有的robot访问  (或者也可以建一个空文件  "/robots.txt"  file)        User-agent:  *
            Disallow:
            例3.  禁止某个搜索引擎的访问        User-agent:  BadBot
            Disallow:  /
            例4.  允许某个搜索引擎的访问        User-agent:  baiduspider
            Disallow:

            User-agent:  *
            Disallow:  /
            例5.一个简单例子
              在这个例子中,该网站有三个目录对搜索引擎的访问做了限制,即搜索引擎不会访问这三个目录。
              需要注意的是对每一个目录必须分开声明,而不要写成  "Disallow:  /cgi-bin/  /tmp/"。
              User-agent:后的*具有特殊的含义,代表"any  robot",所以在该文件中不能有"Disallow:  /tmp/*"  or  "Disallow:*.gif"这样的记录出现.        User-agent:  *
            Disallow:  /cgi-bin/
            Disallow:  /tmp/
            Disallow:  /~joe/
            

        5.  robots.txt文件参考资料
            robots.txt文件的更具体设置,请参看以下链接:
            ·  Web  Server  Administrator's  Guide  to  the  Robots  Exclusion  Protocol
            ·  HTML  Author's  Guide  to  the  Robots  Exclusion  Protocol
            ·  The  original  1994  protocol  description,  as  currently  deployed
            ·  The  revised  Internet-Draft  specification,  which  is  not  yet  completed  or  implemented
        6.  各搜索引擎的robot  
            Google:Crawled  by  Googlebot/2.1  (+http://www.google.com/bot.html)
            
            Baidu:Crawled  by  Baiduspider+(+http://www.baidu.com/search/spider.htm)

            Yahoo:Crawled  by  Mozilla/5.0  (compatible;  Yahoo!  Slurp  China

            MSN:  Crawled  by  msnbot/1.0  (+http://search.msn.com/msnbot.htm)

            Sogou:  Crawled  by  sogou  spider

            中搜:Crawled  by  User-Agent:  Mozilla/4.0  (compatible;  MSIE  5.5;  Windows  NT  5.0)
    www.best-code.com
            sina:Crawled  by  Mozilla/4.0(compatible;MSIE  6.0;Windows  NT  5.0;.NET  CLR  1.1.432)
    posted @ 2006-08-05 14:54 rendong 阅读(259) | 评论 (0)编辑 收藏
     
    关于Meta的用法
    <a href='http://www.best-code.com'>www.best-code.com</a>
    meta是用来在HTML文档中模拟HTTP协议的响应头报文。meta  标签用于网页的<head>与</head>中,meta  标签的用处很多。meta  的属性有两种:name和http-equiv。name属性主要用于描述网页,对应于content(网页内容),以便于搜索引擎机器人查找、分类(目前几乎所有的搜索引擎都使用网上机器人自动查找meta值来给网页分类)。这其中最重要的是description(站点在搜索引擎上的描述)和keywords(分类关键词),所以应该给每页加一个meta值。比较常用的有以下几个:

    name  属性

        1、<meta  name="generator"  contect="">用以说明生成工具(如Microsoft  FrontPage  4.0)等;

        2、<meta  name="keywords"  contect="">向搜索引擎说明你的网页的关键词;

        3、<meta  name="description"  contect="">告诉搜索引擎你的站点的主要内容;

        4、<meta  name="author"  contect="你的姓名">告诉搜索引擎你的站点的制作的作者;

        5、<meta  name="robots"  contect="all|none|index|noindex|follow|nofollow">

      其中的属性说明如下:

      设定为all:文件将被检索,且页面上的链接可以被查询;

      设定为none:文件将不被检索,且页面上的链接不可以被查询;

      设定为index:文件将被检索;

      设定为follow:页面上的链接可以被查询;

      设定为noindex:文件将不被检索,但页面上的链接可以被查询;

      设定为nofollow:文件将不被检索,页面上的链接可以被查询。

    http-equiv属性

        1、<meta  http-equiv="Content-Type"  contect="text/html";charset=gb_2312-80">

      和  <meta  http-equiv="Content-Language"  contect="zh-CN">用以说明主页制作所使用的文字以及语言;

      又如英文是ISO-8859-1字符集,还有BIG5、utf-8、shift-Jis、Euc、Koi8-2等字符集;

        2、<meta  http-equiv="Refresh"  contect="n;url=http://yourlink">定时让网页在指定的时间n内,跳转到页面http;//yourlink;

        3、<meta  http-equiv="Expires"  contect="Mon,12  May  2001  00:20:00  GMT">可以用于设定网页的到期时间,一旦过期则必须到服务器上重新调用。需要注意的是必须使用GMT时间格式;

        4、<meta  http-equiv="Pragma"  contect="no-cache">是用于设定禁止浏览器从本地机的缓存中调阅页面内容,设定后一旦离开网页就无法从Cache中再调出;

        5、<meta  http-equiv="set-cookie"  contect="Mon,12  May  2001  00:20:00  GMT">cookie设定,如果网页过期,存盘的cookie将被删除。需要注意的也是必须使用GMT时间格式;

        6、<meta  http-equiv="Pics-label"  contect="">网页等级评定,在IE的internet选项中有一项内容设置,可以防止浏览一些受限制的网站,而网站的限制级别就是通过meta属性来设置的;

        7、<meta  http-equiv="windows-Target"  contect="_top">强制页面在当前窗口中以独立页面显示,可以防止自己的网页被别人当作一个frame页调用;

        8、<meta  http-equiv="Page-Enter"  contect="revealTrans(duration=10,transtion=50)">和<meta  http-equiv="Page-Exit"  contect="revealTrans(duration=20,transtion=6)">设定进入和离开页面时的特殊效果,这个功能即FrontPage中的“格式/网页过渡”,不过所加的页面不能够是一个frame页面。

    关于robots.txt的讲解

    1.什么是robots.txt文件?  
    搜索引擎通过一种程序robot(又称spider),自动访问互联网上的网页并获取网页信息。  
    您可以在您的网站中创建一个纯文本文件robots.txt,在这个文件中声明该网站中不想被robot访问的部分,这样,该网站的部分或全部内容就可以不被搜索引擎收录了,或者指定搜索引擎只收录指定的内容。

    2.robots.txt文件放在哪里?  
    robots.txt文件应该放在网站根目录下。举例来说,当robots访问一个网站(比如http://www.abc.com)时,首先会检查该网站中是否存在http://www.abc.com/robots.txt  这个文件,如果机器人找到这个文件,它就会根据这个文件的内容,来确定它访问权限的范围。


    1.robots.txt文件的格式?  

    "robots.txt"文件包含一条或更多的记录,这些记录通过空行分开(以CR,CR/NL,  or  NL作为结束符),每一条记录的格式如下所示:
    "<field>lt;optionalspace><value><optionalspace>"。

    在该文件中可以使用#进行注解,具体使用方法和UNIX中的惯例一样。该文件中的记录通常以一行或多行User-agent开始,后面加上若干Disallow行,详细情况如下:

    User-agent:
      该项的值用于描述搜索引擎robot的名字,在"robots.txt"文件中,如果有多条User-agent记录说明有多个robot会受到该协议的限制,对该文件来说,至少要有一条User-agent记录。如果该项的值设为*,则该协议对任何机器人均有效,在"robots.txt"文件中,"User-agent:*"这样的记录只能有一条。
    www.best-code.com
    Disallow:
      该项的值用于描述不希望被访问到的一个URL,这个URL可以是一条完整的路径,也可以是部分的,任何以Disallow开头的URL均不会被robot访问到。例如"Disallow:/help"对/help.html  和/help/index.html都不允许搜索引擎访问,而"Disallow:/help/"则允许robot访问/help.html,而不能访问/help/index.html。任何一条Disallow记录为空,说明该网站的所有部分都允许被访问,在"/robots.txt"文件中,至少要有一条Disallow记录。如果"/robots.txt"是一个空文件,则对于所有的搜索引擎robot,该网站都是开放的。
    posted @ 2006-08-05 14:52 rendong 阅读(376) | 评论 (0)编辑 收藏
     
    使用robots.txt的注意事项
    <a href='http://www.best-code.com'>www.best-code.com</a>
    robots.txt的创建很简单,只需设置User-agent与Disallow两项内容,其中User-agent项设置特定的搜索引擎Spider,Disallow项设定不允许Spider抓取和索引的内容。尽管如此,笔者却常常见一些设置不当的例子,在此把robots.txt有关的注意事项介绍一下:

    robots.txt文件
      robots.txt只能存放于网站的根目录下,置于除此之外的任何地方均不会被Spider发现。

      每个网站,或每个域名(包括子域名),只能有一个robots.txt。

      文件名“robots.txt”为小写字母,其他如Robots.txt或robots.Txt是不正确的,命名错误将会被Spider忽略。

      正如上篇文章中介绍的,Spider在网站内找不到robots.txt时将会被重定向到404  错误页面,这便有可能阻碍Spider抓取和收录页面。虽然这并不一定会发生,但很多时候我们没必要冒这样的风险,一般来说,即使我们对网站的所有内容都没有限制,对所有的搜索引擎Spider  都欢迎,最好也在根目录下创建一个robots.txt文件:

    User-agent:  *
    Disallow:

    robots.txt的语法规则
      在Disallow项中使用小写字母,即文件名和目录名使用小写字母,特别在对大小写敏感的Unix下更要注意。

      robots.txt惟一支持的通配符是在User-agent使用的“*”,其代表所有的Spider。除此之外,别的通配符均不可用。这方面的错误常见于在文件名或目录名中使用通配符。

      robots.txt的限定项

      在User-agent和Disallow项的设定中,每行只允许有一个设定值,同时,注意不要有空行。至于行数,则没有限制,理论上说可以根据需要创建具有无数行的robots.txt。

      下面即是一个错误的例子

    User-agent:  *
    Disallow:  /dir1/  /dir2/  /dir3/

      正确设置应为:

    User-agent:  *
    Disallow:  /dir1/
    Disallow:  /dir2/
    Disallow:  /dir3/

      robots.txt中的文件与目录

      既定某个文件拒绝索引时,格式为文件名(包括扩展名),其后无“/”,而限定目录时,则需在目录名后加“/”。如下面的示例:

    User-agent:  *
    Disallow:  /file.html
    Disallow:  /dir/

      特别注意的是,不要省略掉目录名后的“/”,不然,Spider便极有可能误读相应的设置。

      robots.txt中限定项的顺序

      请看下方的示例:

    User-agent:  *
    Disallow:  /
    User-agent:  Googlebot
    Disallow:

      该设定本意是想允许Google访问所有页面,同时禁止其他Spider的访问。但在这样的设置下,Googlebot在读取前2行后便会离开网站,后面对其的“解禁”完全失去了意义。正确的格式应为:

    User-agent:  Googlebot
    Disallow:
    User-agent:  *
    Disallow:  /

      robots.txt中的注释

      尽管在robots.txt的标准中,可以在限定项的后面使用“#”添加注释,如下面的例子

    User-agent:  Googlebot  #这是对Google的设置
    Disallow:

      但很多研究与测试表明,不少Spider对这样格式的解读存在问题。为确保其能更好地工作,最好采用如下设置:
    www.best-code.com
    #这是对Google的设置
    User-agent:  Googlebot
    Disallow:
    posted @ 2006-08-05 14:50 rendong 阅读(228) | 评论 (0)编辑 收藏
     
    h1>如何提高网站在Google中的排名
        ——面向搜索引擎的网站设计

    作者: 车东 Email: chedongATbigfoot.com/chedongATchedong.com

    写于:2003/01 最后更新: 08/05/2006 14:47:05

    版权声明:可以任意转载,转载时请务必以超链接形式标明文章原始出处和作者信息及本声明
    http://www.chedong.com/tech/google.html

    关键词:Google PageRank "link popularity" "website promotion" "optimization for search engine" 

    内容摘要:(注意:这个网站设计本身就利用了其中一些方法)。
    网站设计中面向搜索引擎的优化注意事项:

    谁不想自己的网站在Google的搜索结果重排名靠前呢,但你的网站设计是Search Engine Ready的吗?

    什么是PageRank

    Google等新一带搜索引擎的优势之一在于不仅索引量很大,而且还将最好的结果排在搜索结果的最前面,具体的原理可以参考Google の秘密 - PageRank 徹底解説一文,PageRank简单的说类似于科技论文中的引用机制:谁的论文被引用次数多,谁就是权威。在互联网上PageRank就是基于网页中相互链接关系的分析得出的,由此引出第一个要点:

    链接就是一切

    在互联网的海洋中,最重要的就是互联互通,不被其他网站引用的网站就是“信息孤岛”。“酒好也怕巷子深”,也许这话说起来有点像垃圾邮件广告,但事实就是这样。所以如果做网站的目的不是孤芳自赏,就需要积极的推广自己的网站。通过搜索引擎推广自己需要注意以下几个方面:

    1. 以量取胜:不一定加入大型网站的分类目录才是网站推广,来自其他网站的任何反相链接都是有用的
      网站推广比较经典的方式就是加入比较大型门户网站的分类目录,比如:Yahoo!dmoz.org等。其实这里有一个误区:不一定非要加入大型网站的分类目录才是网站推广,因为现在搜索引擎已经不再只是网站目录的索引,而是更全面的网页索引,所以无论来自其他网站任何地方的反相链接都是非常有价值的,哪怕是出现在新闻报道,论坛,邮件列表归档中。因此在往很多大型站点的邮件列表发邮件时,一定注意在自己的签名中加上自己网站的地址。
      Blogger(Weblog的简称)们也许最深刻地理解了“链接就是一切”这句话的含义,由于Blog的内容之间有大量的相互链接,因此最经常被引用的Blog页面在搜索引擎中的排名往往比一些大型商业网站的页面还要高。
    2. 以质取胜:被PageRank高的网站引用能更快地提高PageRank
      数量只是关键因素之一,来自PageRank高的页面的链接还能更快的提高被链接目标的PageRank,以我的个人网站为例:我没有加入任何分类目录,只是将一些文章投稿在了ZDNet中国上,由于页面上有文章出处链接,相应网页和网站整体的PageRank过了一段时间后就有了很大的提升。有时候被什么样的网站引用有时候比引用次数多更重要。这里我要特别感谢的是,当时ZDNet中国是唯一遵循了我的版权声明的要求表明了文章出处,并且有反相链接的网站。
    3. 了解搜索引擎的"价值观":
      Lucene简介这篇文章被Jakarta.apache.org的lucene项目引用以后,这篇文章就成为了所有页面中PageRank最高的页面(在Google工具栏上显示是6/10),而Google深厚的学院气氛让我一直怀疑他们对 .edu等非功利站点有特别加分 :-),毕竟.org .edu才代表了互联网精神的实质:知识的共享。
      但更合理的解释是:.org很多都是开放技术平台的开发者,他们会在首页这样的地方加入Powered By Apache, Power by FreeBSD之类的链接表示对其他开源平台的尊重,所以象Apache, PHP, FreeBSD这样的开源站点在GOOGLE中都有非常高的PageRank。而在.edu这些站点中,很多都是学术性比较强的文档,以超链接形式标明参考文献的出处已经成为一种习惯,而这也无疑正是PageRank最好的依据。
      注意:千万不要通过Link Farm提高自身的站点排名:Google会惩罚那些主动链接到Link Farm站点以提高自身排名站点,相应站点的页面将不会被收入到索引中。但如果你的页面被别的Link Farm链接了也不必担心,因为这种被动的链接是不会被惩罚的。

    另外在推广自己网站之前也许首先需要了解自己网站目前在一些搜索引擎中的知名度,这里我做了个小工具可以用于这一目的:
    http://www.chedong.com/linkPopCheck.php
    原理非常简单,可以参考如何评价网站的人气:http://www.chedong.com/tech/link_pop_check.html一文。

    网站推广只是手段,如何突出内容、让需要相关信息的用户能够尽快的找到你的网站才是目的,PageRank高并不代表像Yahoo!这样的门户站点就能,因为搜索引擎的结果是搜索关键词在页面中的匹配度和页面的PageRank相结合的排名结果。因此第二个要点:

    如何突出关键词

    1. 不要空着标题:空着<title></title>无异于浪费了最有价值的一块阵地;
      传统的页面中,HTML页面中会有类似以下的隐含信息,用于说明当前网页的主要内容关键字:
      <header>
          <meta name="keyword" content="mp3 download music...">
      </header>
      后来由于这种人工添加关键词的方式被滥用,大量网页中为了提高被搜索引擎命中的概率,经常添加一些和实际网页内容无关的热门关键比如:“music mp3 download”等,所以新一代的搜索引擎已经不再关心页面头文件中的人工meta keyword声明,而页面标题在搜索引擎的关键词的命中命中过程中往往有着更高的比重,如果一个关键词在标题中命中会比在页面中命中有更高的得分,从而在相应的搜索结果排名中更靠前。
    2. 标题长度和内容:不要过长,一般在40个字符以内,并充分突出关键词的比重;
      如果更长的标题搜索引擎一般会忽略掉,所以要尽可能将主要关键词放在标题靠前的位置。省略掉不必要的形容词吧,毕竟用户主要通过名词来找到需要的内容。标题内容:尽量用一些别人可以通过关键词找到的字眼(也别太过头,如果标题中的字眼超过1半内容中都没有,有可能被搜索引擎排除出索引),因此基于web日志中来自其他搜索引擎的关键词查询统计非常必要。
    3. 如果网页很多的话,尽量使用不同的网页标题,争取让自己网站的内容更多的进入搜索引擎索引范围;
      因为搜索引擎会根据页面内容的相似度把一些内容当成重复页面排除出索引范围;
      http://www.chedong.com/phpMan.php是我的网站上的一个小应用:一个web界面的unix命令手册(man page),在以前的设计中所有动态页面使用的都是同样的标题:"phpMan: man page /perldoc /info page web interface" ,Google索引了大约3000多个页面,后来我将页面标题改成了"phpMan:  [命令名]"这样的格式,比如:"phpMan: ls",这样大部分动态页面的标题就都不一样了,一个月后Google从这个页面入口索引了大约6000个页面。因此,如果网站中很多网页都使用相同的标题,比如:“新闻频道”,“论坛”,这些页面中很大一部分就会被排重机制忽略掉。
    4. 除了<title></title>外,还可以用<h1></h1>标题行突出内容主题,加强标题的效果;
      在我的网站设计中:我会把用<h1>[标题]</h1>这样的模板把标题突出显示,而不是通过改变字体的方式突出标题。

    其他网站设计提示

    1. 尽量使用静态网页:目前能够像Google一样对动态网页进行索引的搜索引擎还比较少,而同样内容的动态网页其权重比静态网页也要低很多。因此无论从效率上讲还是方便搜索引擎收录,使用内容发布系统将网站内容发布成静态网页都是非常必要的。
      比如:http://www.chedong.com/phpMan.php/man/intro/3
      肯定比 http://www.chedong.com/phpMan.php?mode=man&parameter=intro&section=3
      更容易进入搜索引擎的索引。而且在URL中的命中有时候比在标题中还能突出关键词。
    2. 表现和内容的分离:“绿色”网页
      网页中的javascript和css尽可能和网页分离,一方面提高代码重用度(也方便页面缓存),另外一方面,由于有效内容占网页长度的百分比高,也能提高相关关键词在页面中的比重。总之,应该鼓励遵循w3c的规范,使用更规范的XHTML和XML作为显示格式便于内容更长时间的保存。
    3. 让所有的页面都有能够快速入口:站点地图,方便网页爬虫(spider)快速遍历网站所有需要发布的内容。如果首页就是用Flash或图片进入的话,无异于将搜索引擎拒之门外,除了UI设计的用户友好外,spider friendly也是非常重要的。
    4. 保持网站自身的健康:经常利用坏链检查工具检查网站中是否有死链。
    5. 保持网页内容/链接的稳定性和持久性:在搜索引擎索引中网页存在的历史也是一个比较重要的因素,而且历史比较久的网页被链接的几率越高。为了保证自己网页能够被比较持久的被其他网站的页面引用,如果自己网页中有链接更新时,最好能保留旧的页面并做好链接转向,以保持内容的连续性。要知道,把一个网站和内容在搜索引擎中的排名“培养”的很高是一件非常不容易的事情,谁都不希望好不容易自己的内容被别人找到了,点击却是“404 页面不存在”吧,因此站点管理员对自身站点error.log的分析也是非常必要的。
    6. 文件类型因素:Google有对PDF, Word(Power Point, Excel), PS文档的索引能力,由于这种文档的内容比一般的HTML经过了更多的整理,学术价值一般比较高,所以这些类型的文档天生就比一般的HTML类型的文档PageRank要高。因此,对于比较重要的文档:技术白皮书,FAQ,安装文档等建议使用PDF PS等高级格式存取,这样在搜索结果中也能获得比较靠前的位置。
    7. “一人得道,鸡犬升天”:常常能发现门户站点的一条新闻往往比其他站点的首页排名还要靠前。因此一个站点总体PageRank提高了以后,往往自身一些并不重要的内容也会被同那些高PageRank的内容一起带入被搜索引擎优先查询的列表中。这点有些不是很合理,因为这样经常造成很多大站点的邮件列表归档往往比其他站点的首页PageRank还要高。

    知己知彼——站点访问统计/日志分析挖掘的重要性

    网站设计不仅仅只是被动的迎合搜索引擎的索引,更重要是充分利用搜索引擎带来的流量进行更深层次的用户行为分析。目前,来自搜索引擎关键词统计几乎是各种WEB日志分析工具的标准功能,相信商业日志统计工具在这方面应该会有更强化的实现。WEB日志统计这个功能如此重要,以至于新的RedHat 8中已经将日志分析工具webalizer作为标准的服务器配置应用之一。

    以Apache/webalizer为例,具体的做法如下:
    1. 记录访问来源:
      在Apache配置文件中设置日志格式为combined格式,这样的日志中会包含扩展信息:其中有一个字段就是相应访问的转向来源:HTTP_REFERER,如果用户是从某个搜索引擎的搜索结果中找到了你的网页并点击过来,日志中记录的HTTP_REFERER就是用户在搜索引擎结果页面的URL,这个URL中包含了用户查询的关键词。
    2. 在webalizer中缺省配置针对搜索引擎的统计:如何提取HTTP_REFERER中的关键词
      webalizer中缺省有针对yahoo, google等国际流行搜索引擎的查询格式:这里我增加了针对国内门户站点的搜索引擎参数设置
      SearchEngine yahoo.com p=
      SearchEngine altavista.com q=
      SearchEngine google.com q=
      SearchEngine    sina.com.cn word=
      SearchEngine    baidu.com   word=
      SearchEngine    sohu.com    word=
      SearchEngine    163.com q=

    通过这样设置webalizer统计时就会将HTTP_REFERER中来自搜索引擎的URL中的keyword提取出来,比如:所有来自google.com链接中,参数q的值都将被作为关键词统计下来:,从汇总统计结果中,就可以发现用户是根据什么关键词找到你的次数,以及找到你的用户最感兴趣的是那些关键词等,进一步的,在webalizer中有设置还可以将统计结果倒出成CSV格式的日志,便于以后导入数据库进行历史统计,做更深层次的数据挖掘等。

    以前通过WEB日志的用户分析主要是简单的基于日志中的访问时间/IP地址来源等,很明显,基于搜索引擎关键词的统计能得到的分析结果更丰富、更直观。因此,搜索引擎服务的潜在商业价值几乎是不言而喻的,也许这也是Yahoo!Altavista等传统搜索引擎网站在门户模式后重新开始重视搜索引擎市场的原因,看看Google的年度关键词统计就知道了,在互联网上有谁比搜索引擎更了解用户对什么更感兴趣呢?

    请看本站的反相链接统计:http://www.chedong.com/log/2003_2.log
    需要注意的是:由于Google针对Windows 2000中的IE使用的是UTF-8方式的编码,因此很多统计有时候需要在UTF-8方式下查看才是正确字符显示。从统计中能够感受到:在使用水平比较高的IT开发人员中Google已经成为最常用的搜索引擎。而使用百度的用户也已经大大超过了传统的搜狐,新浪等门户站点,因此传统门户网站在搜索引擎上的优势将是非常脆弱的。而从技术的发展趋势来看,以后还会有更多的利用互联网媒体做更深层次数据挖掘的服务模式出现:

    转载自cnblog.org——“突发”文字可能揭示社会趋势

    在“新科学家”(New Scientist)在线杂志上,公布了康奈尔大学的一个新研究成果,引人注目,也许与Google 收购Pyra 的动机有关。

    这所大学的计算机科学家 Jon Klenberg 开发了一个计算机算法,能够识别一篇文章中某些文字的“突发”增长,而且他发现,这些“突发”增长的文字可以用来快速识别最新的趋势和热点问题,因此能够更有效地筛选重要信息。过去很多搜索技术都采用了简单计算文字/词组出现频率的方法,却忽略了文字使用增加的速率。

    Jon 特别指出,这种方法可以应用到大量Weblog上,以跟踪社会趋势,这对商业应用也很有潜力。例如,广告商可以从成千上万的个人Blog 中快速找到潜在的需求风尚。而且只要Blog 覆盖话题范围足够大(实际上发展趋势确实如此),这项技术对政治、社会、文化和经济等领域也都会有实际意义了。

    虽然Google 新闻的内部算法至今没有公开,但是人们猜测这种完全由机器所搜集的头条新闻应当不是Google搜索引擎中惯用的鸽子算法,很可能与这种“突发”判断算法有关。如此说来,Google收购Blog工具供应商的举动确实还有更深层次的远见了。

    - NewScientist.com news, Word 'bursts' may reveal online trends
    - 还没有写完这些介绍,在 SlashDot 上也看到了很多有关这个发现的讨论
    <a href='http://www.best-code.com'>www.best-code.com</a>

    参考资料:

    面向Google搜索引擎的网站设计优化
    http://www.google-search-engine-optimization.com/

    关于Google的十个神话:
    http://www.promotionbase.com/printTemplate.php?aid=971

    如何评价一个网站的人气
    http://www.chedong.com/tech/link_pop_check.html

    如何提高网站在Google中的排名——面向搜索引擎的广告模式
    http://www.chedong.com/tech/google_ads.html

    Measuring Link Popularity
    http://searchenginewatch.com/webmasters/popularity.html

    Google の秘密 - PageRank 徹底解説
    http://www.kusastro.kyoto-u.ac.jp/~baba/wais/pagerank.html
    这篇文章是在查"Google PageRank"的时候查到的,这篇文章不仅有一个算法说明,也是一个Google的weblog,记录了很多关于Google的新闻和一些市场动态信息。

    Google的海量处理机制:鸽子系统
    http://www.google.com/technology/pigeonrank.html

    WEB日值统计工具Webalizer
    http://www.webalizer.org

    Robots的说明:
    http://bar.baidu.com/robots/
    http://www.google.com/bot.html
    搜索引擎通过一种程序robot(又称spider),自动访问互联网上的网页并获取网页信息。您可以在您的网站中创建一个纯文本文件robots.txt,在这个文件中声明该网站中哪些内容可以被robot访问,哪些不可以。
    www.best-code.com
    原文出处:<a href="http://www.chedong.com/tech/google.html">http://www.chedong.com/tech/google.html</a>

    posted @ 2006-08-05 14:49 rendong 阅读(288) | 评论 (0)编辑 收藏
     
    1.  oncontextmenu="window.event.returnValue=false"  将彻底屏蔽鼠标右键
    <table  border  oncontextmenu=return(false)><td>no</table>  可用于Table
    <a href='http://www.best-code.com'>www.best-code.com</a>
    2.  <body  onselectstart="return  false">  取消选取、防止复制

    3.  onpaste="return  false"  不准粘贴

    4.  oncopy="return  false;"  oncut="return  false;"  防止复制

    5.  <link  rel="Shortcut  Icon"  href="favicon.ico">  IE地址栏前换成自己的图标

    6.  <link  rel="Bookmark"  href="favicon.ico">  可以在收藏夹中显示出你的图标

    7.  <input  style="ime-mode:disabled">  关闭输入法

    8.  永远都会带着框架
    <script  language="JavaScript"><!--
    if  (window  ==  top)top.location.href  =  "frames.htm";  //frames.htm为框架网页
    //  --></script>

    9.  防止被人frame
    <SCRIPT  LANGUAGE=JAVASCRIPT><!--  
    if  (top.location  !=  self.location)top.location=self.location;
    //  --></SCRIPT>

    10.  网页将不能被另存为
    <noscript><iframe  src=*.html></iframe></noscript>  

    11.  <input  type=button  value=查看网页源代码  
    onclick="window.location  =  "view-source:"+  "http://www.pconline.com.cn"">

    12.删除时确认
    <a  href="javascript:if(confirm("确实要删除吗?"))location="boos.asp?&areyou=删除&page=1"">删除</a>  

    13.  取得控件的绝对位置
    //Javascript
    <script  language="Javascript">
    function  getIE(e){
    var  t=e.offsetTop;
    var  l=e.offsetLeft;
    while(e=e.offsetParent){
    t+=e.offsetTop;
    l+=e.offsetLeft;
    }
    alert("top="+t+"/nleft="+l);
    }
    </script>

    //VBScript
    <script  language="VBScript"><!--
    function  getIE()
    dim  t,l,a,b
    set  a=document.all.img1
    t=document.all.img1.offsetTop
    l=document.all.img1.offsetLeft
    while  a.tagName<>"BODY"
    set  a  =  a.offsetParent
    t=t+a.offsetTop
    l=l+a.offsetLeft
    wend
    msgbox  "top="&t&chr(13)&"left="&l,64,"得到控件的位置"
    end  function
    --></script>

    14.  光标是停在文本框文字的最后
    <script  language="javascript">
    function  cc()
    {
    var  e  =  event.srcElement;
    var  r  =e.createTextRange();
    r.moveStart("character",e.value.length);
    r.collapse(true);
    r.select();
    }
    </script>
    <input  type=text  name=text1  value="123"  onfocus="cc()">

    15.  判断上一页的来源
    javascript:
    document.referrer

    16.  最小化、最大化、关闭窗口
    <object  id=hh1  classid="clsid:ADB880A6-D8FF-11CF-9377-00AA003B7A11">  
    <param  name="Command"  value="Minimize"></object>
    <object  id=hh2  classid="clsid:ADB880A6-D8FF-11CF-9377-00AA003B7A11">  
    <param  name="Command"  value="Maximize"></object>
    <OBJECT  id=hh3  classid="clsid:adb880a6-d8ff-11cf-9377-00aa003b7a11">
    <PARAM  NAME="Command"  VALUE="Close"></OBJECT>
    <input  type=button  value=最小化  onclick=hh1.Click()>
    <input  type=button  value=最大化  onclick=hh2.Click()>7R0癳  
    <input  type=button  value=关闭  onclick=hh3.Click()>
    本例适用于IE

    17.屏蔽功能键Shift,Alt,Ctrl
    <script>
    function  look(){  
    if(event.shiftKey)  
    alert("禁止按Shift键!");  //可以换成ALT CTRL
    }  
    document.onkeydown=look;  
    </script>

    18.  网页不会被缓存
    <META  HTTP-EQUIV="pragma"  CONTENT="no-cache">
    <META  HTTP-EQUIV="Cache-Control"  CONTENT="no-cache,  must-revalidate">
    <META  HTTP-EQUIV="expires"  CONTENT="Wed,  26  Feb  1997  08:21:57  GMT">
    或者<META  HTTP-EQUIV="expires"  CONTENT="0">

    19.怎样让表单没有凹凸感?
    <input  type=text  style="border:1  solid  #000000">  

    <input  type=text  style="border-left:none;  border-right:none;  border-top:none;  border-bottom:  

    1  solid  #000000"></textarea>

    20.<div><span>&<layer>的区别?  
    <div>(division)用来定义大段的页面元素,会产生转行  
    <span>用来定义同一行内的元素,跟<div>的唯一区别是不产生转行  
    <layer>是ns的标记,ie不支持,相当于<div>

    21.让弹出窗口总是在最上面:
    <body  onblur="this.focus();">

    22.不要滚动条?  
    让竖条没有:  
    <body  style="overflow:scroll;overflow-y:hidden">  
    </body>  
    让横条没有:  
    <body  style="overflow:scroll;overflow-x:hidden">  
    </body>  
    两个都去掉?更简单了  
    <body  scroll="no">  
    </body>  

    23.怎样去掉图片链接点击后,图片周围的虚线?
    <a  href="#"  onFocus="this.blur()"><img  src="logo.jpg"  border=0></a>

    24.电子邮件处理提交表单
    <form  name="form1"  method="post"  action="mailto:****@***.com"  enctype="text/plain">  
    <input  type=submit>
    </form>

    25.在打开的子窗口刷新父窗口的代码里如何写?
    window.opener.location.reload()

    26.如何设定打开页面的大小
    <body  onload="top.resizeTo(300,200);">
    打开页面的位置<body  onload="top.moveBy(300,200);">

    27.在页面中如何加入不是满铺的背景图片,拉动页面时背景图不动  
    <STYLE>  
    body  
    {background-image:url(logo.gif);  background-repeat:no-repeat;  
    background-position:center;background-attachment:  fixed}  
    </STYLE>  

    28.  检查一段字符串是否全由数字组成
    <script  language="Javascript"><!--
    function  checkNum(str){return  str.match(//D/)==null}
    alert(checkNum("1232142141"))
    alert(checkNum("123214214a1"))
    //  --></script>

    29.  获得一个窗口的大小
    document.body.clientWidth;  document.body.clientHeight

    30.  怎么判断是否是字符
    if  (/[^/x00-/xff]/g.test(s))  alere  t("含有汉字");
    else  alert("全是字符");

    31.TEXTAREA自适应文字行数的多少
    <textarea  rows=1  name=s1  cols=27  onpropertychange="this.style.posHeight=this.scrollHeight">
    </textarea>
    32.  日期减去天数等于第二个日期
    <script  language=Javascript>
    function  cc(dd,dadd)
    {
    //可以加上错误处理
    var  a  =  new  Date(dd)
    a  =  a.valueOf()
    a  =  a  -  dadd  *  24  *  60  *  60  *  1000
    a  =  new  Date(a)
    alert(a.getFullYear()  +  "年"  +  (a.getMonth()  +  1)  +  "月"  +  a.getDate()  +  "日")
    }
    cc("12/23/2002",2)
    </script>

    33.  选择了哪一个Radio
    <HTML><script  language="vbscript">
    function  checkme()
    for  each  ob  in  radio1
    if  ob.checked  then  window.alert  ob.value
    next
    end  function
    </script><BODY>
    <INPUT  name="radio1"  type="radio"  value="style"  checked>Style
    <INPUT  name="radio1"  type="radio"  value="barcode">Barcode
    <INPUT  type="button"  value="check"  onclick="checkme()">
    </BODY></HTML>

    34.脚本永不出错
    <SCRIPT  LANGUAGE="JavaScript">  
    <!--  Hide  
    function  killErrors()  {  
    return  true;  
    }  
    window.onerror  =  killErrors;  
    //  -->  
    </SCRIPT>

    35.ENTER键可以让光标移到下一个输入框
    <input  onkeydown="if(event.keyCode==13)event.keyCode=9">

    36.  检测某个网站的链接速度:
    把如下代码加入<body>区域中:
    <script  language=Javascript>
    tim=1
    setInterval("tim++",100)
    b=1
    var  autourl=new  Array()
    autourl[1]="www.njcatv.net"
    autourl[2]="javacool.3322.net"
    autourl[3]="www.sina.com.cn"
    autourl[4]="www.nuaa.edu.cn"
    autourl[5]="www.cctv.com"
    function  butt(){
    document.write("<form  name=autof>")
    for(var  i=1;i<autourl.length;i++)
    document.write("<input  type=text  name=txt"+i+"  size=10  value=测试中……>  =》<input  type=text  
    name=url"+i+"  size=40>  =》<input  type=button  value=GO  

    onclick=window.open(this.form.url"+i+".value)><br>")
    document.write("<input  type=submit  value=刷新></form>")
    }
    butt()
    function  auto(url){
    document.forms[0]["url"+b].value=url
    if(tim>200)
    {document.forms[0]["txt"+b].value="链接超时"}
    else
    {document.forms[0]["txt"+b].value="时间"+tim/10+"秒"}
    b++
    }
    function  run(){for(var  i=1;i<autourl.length;i++)document.write("<img  src=http://"+autourl+"/"+Math.random()+"  width=1  height=1  

    onerror=auto("http://"+autourl+"")>")}
    run()</script>

    37.  各种样式的光标
    auto  :标准光标
    default  :标准箭头
    hand  :手形光标
    wait  :等待光标
    text  :I形光标
    vertical-text  :水平I形光标
    no-drop  :不可拖动光标
    not-allowed  :无效光标
    help  :?帮助光标
    all-scroll  :三角方向标
    move  :移动标
    crosshair  :十字标
    e-resize
    n-resize
    nw-resize
    w-resize
    s-resize
    se-resize
    sw-resize

    38.页面进入和退出的特效
    进入t页alere  面<meta  http-equiv="Page-Enter"  content="revealTrans(duration=x,  transition=y)">
    推出页面<meta  http-equiv="Page-Exit"  content="revealTrans(duration=x,  transition=y)">  
    这个是页面被载入和调出时的一些特效。duration表示特效的持续时间,以秒为单位。transition表示使用哪种特效,取值为1-23:
      0  矩形缩小  
      1  矩形扩大  
      2  圆形缩小
      3  圆形扩大  
      4  下到上刷新  
      5  上到下刷新
      6  左到右刷新  
      7  右到左刷新  
      8  竖百叶窗
      9  横百叶窗  
      10  错位横百叶窗  
      11  错位竖百叶窗
      12  点扩散  
      13  左右到中间刷新  
      14  中间到左右刷新
      15  中间到上下
      16  上下到中间  
      17  右下到左上
      18  右上到左下  
      19  左上到右下  
      20  左下到右上
      21  横条  
      22  竖条  
      23  以上22种随机选择一种

    39.在规定时间内跳转
    <META  http-equiv=V="REFRESH"  content="5;URL=http://www.51js.com">  
    www.best-code.com
    40.网页是否被检索
    <meta  name="ROBOTS"  content="属性值">
      其中属性值有以下一些:
      属性值为"all":  文件将被检索,且页上链接可被查询;
      属性值为"none":  文件不被检索,而且不查询页上的链接;
      属性值为"index":  文件将被检索;
      属性值为"follow":  查询页上的链接;
      属性值为"noindex":  文件不检索,但可被查询链接;
      属性值为"nofollow":  文件不被检索,但可查询页上的链接。
    41用JavaScript获取客户端各种高宽
    网页可见区域宽:  document.body.clientWidth
    <a href='http://www.best-code.com'>www.best-code.com</a>网页可见区域高:  document.body.clientHeight
    网页可见区域宽(包括边线和滚动条的宽):  document.body.offsetWidth    
    网页可见区域高(包括边线的宽):  document.body.offsetHeight  
    网页正文全文宽:  document.body.scrollWidth
    网页正文全文高:  document.body.scrollHeight
    网页被卷去的高:  document.body.scrollTop
    网页被卷去的左:  document.body.scrollLeft
    网页正文部分上:  window.screenTop
    网页正文部分左:  window.screenLeft
    屏幕分辨率的高:  window.screen.height
    屏幕分辨率的宽:  window.screen.width
    屏幕可用工作区高度:  window.screen.availHeight
    屏幕可用工作区宽度:  window.screen.availWidth
    屏幕颜色深度:    window.screen.colorDepth  
    你的屏幕分辨率(像素/英寸):  window.screen.deviceXDPI  
    posted @ 2006-08-05 14:45 rendong 阅读(402) | 评论 (0)编辑 收藏
     
    开发中经常遇到,字符串过长,无法完全显示的问题

    这时候就需要截取我们所需要的长度,后面显示省略号或其他字符。

    由于中文字符占两个字节,而英文字符占用一个字节,所以,单纯地判断字符数,效果往往不尽如人意

    下面的方法通过判断字符的类型来进行截取,效果还算可以:)


    如果大家有其他的解决方法欢迎贴出来,共同学习:)
    **********************************************************************
    private String str;
    private int counterOfDoubleByte;
    private byte b[];
    /**
    * 设置需要被限制长度的字符串
    * @param str 需要被限制长度的字符串
    */
    public void setLimitLengthString(String str){
      this.str = str;
    }
    /**
    * @param len 需要显示的长度(<font color="red">注意:长度是以byte为单位的,一个汉字是2个byte</font>)
    * @param symbol 用于表示省略的信息的字符,如“...”,“>>>”等。
    * @return 返回处理后的字符串
    */
    public String getLimitLengthString(int len, String symbol) throws UnsupportedEncodingException {
      counterOfDoubleByte = 0;
      b = str.getBytes("GBK");
      if(b.length <= len)
        return str;
      for(int i = 0; i < len; i++){
        if(b[i] < 0)
          counterOfDoubleByte++;
      }

      if(counterOfDoubleByte % 2 == 0)
        return new String(b, 0, len, "GBK") + symbol;
      else
        return new String(b, 0, len - 1, "GBK") + symbol;
    }


    本文转贴自网友:focus2004 的文章
    posted on 2005-08-12 15:39 Rosen 阅读(963) 评论(6)  编辑 收藏收藏至365Key 所属分类: J2EE 技术

    评论

    #  re: 精确截取字符串(转载) 2005-08-19 08:27 ivan
    if(b<0) 编译会出错。  回复
      

    #  re: 精确截取字符串(转载) 2005-08-19 20:39 Rosen
    马上修改一下代码,去年转贴的时候一直忘记修改了。
    是 if(b[i] < 0),谢谢 ivan 指正。  回复
      

    #  re: 精确截取字符串(转载) 2006-01-28 18:24 tdg
    大作拜读,有一点愚见,特抛砖引玉:
    1。字符串除了可以基于byte[]操作外,还可以基于char[]操作。看老大你的意图是想截取字符串的前几个字符然后加上省略符号最后输出而已,完全不必考虑用byte[]数组操作啊 。而且好像开发中更注重语义上的第几个字符而不是你说的这种情况哦。
    2。以下是拙作,请斧正:
    /**
    * 字符串截取函数
    * @param str String 要处理的字符串
    * @param length int 需要显示的长度
    * @param symbol String 用于表示省略的信息的字符,如“...”,“>>>”等
    * @return String 返回处理后的字符串
    * @throws UnsupportedEncodingException
    */
    public String getLimitLengthString(String str, int length, String symbol) throws
    UnsupportedEncodingException {
    assert str != null;
    assert length > 0;
    assert symbol != null;
    //如果字符串的位数小于等于要截取的位数,附加上表示省略的信息的字符串后返回
    if (str.length() <= length) {
    return str + symbol;
    //从零开始,截取length个字符,附加上表示省略的信息的字符串后返回
    } else {
    str = new String(str.getBytes("GBK"));
    char[] charArray = str.toCharArray();
    char[] charArrayDesc = new char[length];
    System.arraycopy(charArray, 0, charArrayDesc, 0, length);
    return new String(charArrayDesc) + symbol;
    }
    }  回复
      

    #  re: 精确截取字符串(转载) 2006-01-28 20:03 Rosen
    呵呵 tdg 兄很认真喔,谈不上斧正。主要是这个问题,用 char 处理,如果是字母或者数字,实际上截取出来的会比汉字少占用一半的空间,所以截取出来后,还是不能对齐。而实际上 char 数组中,不管是字母、数字还是汉字,它们都只代表一个单元。但是 byte 则不同,字母、数字只占用一个字节,而汉字占用两个字节(都是GBK编码)。  回复
      

    #  re: 精确截取字符串(转载) 2006-03-24 10:49 istarliu
    您好!
    你的文章让我受益不少,有个问题想向您确认一下:
    在代码中
    b = str.getBytes("GBK");
    if(b.length <= len)
    return str;
    for(int i = 0; i < len; i++){
    if(b[i] < 0)
    counterOfDoubleByte++;
    }
    是不是如果只要是中文汉字,在b[i]对应的值都是小于0的,
    也就是说,在汉字代表的两个字节中,这两个汉字分别转化为整数值时,是不是一定小于0,但值范围不能小于-127的。做过测试,不能肯定。:)
    posted @ 2006-08-05 00:56 rendong 阅读(417) | 评论 (0)编辑 收藏
     
         摘要: ajax确实是个很好的技术,在提高客户的体验度上面能做很多以前不能做或者不好做的事情。出现提示页面就是一个很好的示例。需要制作提示页面的地方其实很多,但以前大多是要求用户点击相关信息进入详细信息页面察看,然后返回,再点击其他的信息察看详细信息页面。这样就降低了客户的体验度,在没有ajax的时候,我们是劝导客户只能这么做。现在用ajax就可以很轻松的解决这个问题了。我的平台仍然是struts+spr...  阅读全文
    posted @ 2006-08-05 00:48 rendong 阅读(434) | 评论 (1)编辑 收藏
     
    最近由于需要用到ThreadLocal,在网上搜索了一些相关资料,发现对ThreadLocal经常会有下面几种误解

     一、ThreadLocal是java线程的一个实现
          ThreadLocal的确是和java线程有关,不过它并不是java线程的一个实现,它只是用来维护本地变量。针对每个线程,提供自己的变量版本,主要是为了避免线程冲突,每个线程维护自己的版本。彼此独立,修改不会影响到对方。

     二、ThreadLocal是相对于每个session的

            ThreadLocal顾名思义,是针对线程。在java web编程上,每个用户从开始到会话结束,都有自己的一个session标识。但是ThreadLocal并不是在会话层上。其实,Threadlocal是独立于用户session的。它是一种服务器端行为,当服务器每生成一个新的线程时,就会维护自己的ThreadLocal。对于这个误解,个人认为应该是开发人员在本地基于一些应用服务器测试的结果。众所周知,一般的应用服务器都会维护一套线程池,也就是说,对于每次访问,并不一定就新生成一个线程。而是自己有一个线程缓存池。对于访问,先从缓存池里面找到已有的线程,如果已经用光,才去新生成新的线程。所以,由于开发人员自己在测试时,一般只有他自己在测,这样服务器的负担很小,这样导致每次访问可能是共用同样一个线程,导致会有这样的误解:每个session有一个ThreadLocal

     三、ThreadLocal是相对于每个线程的,用户每次访问会有新的ThreadLocal

      理论上来说,ThreadLocal是的确是相对于每个线程,每个线程会有自己的ThreadLocal。但是上面已经讲到,一般的应用服务器都会维护一套线程池。因此,不同用户访问,可能会接受到同样的线程。因此,在做基于TheadLocal时,需要谨慎,避免出现ThreadLocal变量的缓存,导致其他线程访问到本线程变量

     四、对每个用户访问,ThreadLocal可以多用
            可以说,ThreadLocal是一把双刃剑,用得来的话可以起到非常好的效果。但是,ThreadLocal如果用得不好,就会跟全局变量一样。代码不能重用,不能独立测试。因为,一些本来可以重用的类,现在依赖于ThreadLocal变量。如果在其他没有ThreadLocal场合,这些类就变得不可用了。个人觉得ThreadLocal用得很好的几个应用场合,值得参考

      1、存放当前session用户:quake want的jert

      2、存放一些context变量,比如webwork的ActionContext

      3、存放session,比如Spring hibernate orm的session

    posted @ 2006-08-05 00:45 rendong 阅读(207) | 评论 (0)编辑 收藏
     
             究竟Spring在何时调用destroy-method="close" 这个方法close()呢?终于借助JavaEye找到了答案,原来如果Spring不在Web Container或是EJB Container中的时候,这个方法还是需要我们自己来调用的,具体就是调用BeanFactory的destroySingletons()方法,文档上的“自动调用”这几个字真是害我不浅呀,原来自动也是通过Web Container或是EJB Container才可以自动,具体做法就是要实现ServletContextListener这个接口,Spring中已经有具体的实现了:
     
    publicclass ContextLoaderListener implements ServletContextListener { 
            private ContextLoader contextLoader; 
           
    /**
            * Initialize the root web application context.
            */

            publicvoid contextInitialized(ServletContextEvent event){
                    this.contextLoader = createContextLoader();
                    this.contextLoader.initWebApplicationContext(event.getServletContext());
            } 
           
    /**
            * Create the ContextLoader to use. Can be overridden in subclasses.
            * @return the new ContextLoader
            */

            protected ContextLoader createContextLoader(){
                    returnnew ContextLoader();
            } 
           
    /**
            * Return the ContextLoader used by this listener.
            */

            public ContextLoader getContextLoader(){
                    return contextLoader;
            } 
           
    /**
            * Close the root web application context.
            */

            publicvoid contextDestroyed(ServletContextEvent event){
                    this.contextLoader.closeWebApplicationContext(event.getServletContext());
            }

    }
    当tomcat关闭的时候会自动调用contextDestroyed(ServletContextEvent event)这个方法。在看一下contextLoader的closeWebApplicationContext方法:
     
    publicvoid closeWebApplicationContext(ServletContext servletContext)throws ApplicationContextException {
                    servletContext.log("Closing root WebApplicationContext");
                    Object wac = servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
                    if(wac instanceof ConfigurableApplicationContext){
                            ((ConfigurableApplicationContext) wac).close();
                    }
            }
    AbstractApplicationContext.Close这个方法是要你自己调用的,在程序要结束的时候保证调用这个close方法,在这里的话就是由Listener来保证tomcat退出的时候调用close方法。
    AbstractApplicationContext.Close的代码 :
     
    publicvoid close(){
                    logger.info("Closing application context [" + getDisplayName() + "]");

                    // Destroy all cached singletons in this context,
                    // invoking DisposableBean.destroy and/or "destroy-method".
                    getBeanFactory().destroySingletons()
                    // publish corresponding event
                    publishEvent(new ContextClosedEvent(this));
            }

    最终还是调用到了getBeanFactory().destroySingletons(); 看来,没有容器,我们还是需要自己来搞定这个方法的调用的 !

    posted @ 2006-08-04 17:14 rendong 阅读(1532) | 评论 (0)编辑 收藏
     
    eclipse3.2上部署jsp
    danielhuf 发表于 2006-7-15 13:29:00

    一.下载
    J2SDK:jdk-1_5_0_04-windows-i586-p.exe
    ECLIPSE:eclipse-SDK-3.2-win32.zip
    ECLIPSE 插件:
    (1)中文包 NLpack1-eclipse-SDK-3.2-win32.zip
    (2)Tomcat插件 tomcatPluginV31.zip
    (3)LOMBAT:lomboz-wtp-emf-gef-jem-3.1.2.zip
    TOMCAT:apache-tomcat-5.5.17.exe
    MYSQL:Mysql-5.0.15.rar
    二.安装
    1.安装j2sdk
    安装路径是:c:\java\jdk,c:\java\jre
    2.安装eclipse
    将eclipse压缩包解压到:d:\eclipse
    3.安装tomcat
    安装的路径是:c:\java\Tomcat5.5
    4.安装eclipse插件
    (1)中文包
    将压缩包NLpack-eclipse-SDK-3.2-win32内features和plugins两文件夹内的内容解压到eclipse文件夹内同名文件夹
    (2)Tomcat插件
    将压缩包tomcatPluginV3.1解压,把com.sysdeo.eclipse.tomcat_3.1.0文件夹拷到d:\eclipse\plugins下
    (3)LOMBAT插件
    将压缩包lomboz-wtp-emf-gef-jem-3.1.2.zip内的features和plugins两文件夹内的内容解压到eclipse文件夹内同名文件夹
    将压缩里的configuration/config.xml里下面的一段文字

     

     #Product Runtime Configuration File

     osgi.splashPath=platform:/base/plugins/org.objectweb.lomboz.product,platform:/base/plugins/org.eclipse.platform
     eclipse.product=org.objectweb.lomboz.product.lomboz
     osgi.bundles.defaultStartLevel=4

    拷贝粘贴到d:/eclipse/configuration/config.xml中的

    # End of file marker - must be here

    前。
    5.安装Mysql
    解压.rar文件,点击setup.exe,安装路径为c:\mysql。注,编码选为gbk。将.rar解压后得到数据库驱动文件mysql-connector-java-3.1.11-bin.jar拷贝到C:\java\Tomcat 5.5\common\lib下
    三.环境配置
    1.系统环境变量配置
    右击“我的电脑”->高级->环境变量,


    2.eclipse中java运行环境的配置
    在eclipse主窗口中,“窗口”->首选项->Java->已安装的JRE,位置设置为 c:\java\jdk
    3.配置Sysdeo Tomcat 插件
    在eclipse主窗口中,“窗口”->首选项->Tomcat,Tomcat version 选 version 5.x(我们安装的版本),Tomcat home 处填安装tomcat的路径,这里就是c:\java\Tomcat5.5。切换到Advanced选项,在tomcat base处再填 c:\java\Tomcat5.5 。最后按应用按钮,然后再确定。
    4.配置LOMBAT插件
    重新启动eclipse,启动画面变为橙黄色,即表示插件安装成功。在“窗口-〉定制透视图-〉快捷方式“中选定EJB,J2EE,WEB。同样在“命令“标签下也选中这三项即可。确定后调整工具栏。
    5.检查配置
    检查配置是否成功,只需在eclipse主窗口中,按tomcat运行按钮,然后在浏览器里的地址栏里输入http://localhost:8080(主机和端口与Tomcat5.5的/conf/server.xml中的设定一致),如果出现tomcat 的页面,那就证明配置成功了。
    四.编写程序
    1.创建一个Tomcat项目
    右击“包资源管理器”窗口中的空白处,新建->项目->Java->Tomcat Project, 在项目名中填写项目名字,在这里我填tomcat_test,点击“完成”即可。
    2.创建一个JSP页面
    在包资源管理器中,右击“tomcat_test”,新建->文件, 在文件名中填写test.jsp(扩展名不能缺),在文本编辑窗口输入如下代码:

    //test.jsp
    <%@ page contentType="text/html;charset=gb2312"%>
    <%@ page import="java.sql.*"%>
    <html>
    <body>
    <%
    Class.forName("com.mysql.jdbc.Driver").newInstance();
    String url ="jdbc:mysql://localhost/test?user=root&password=1qaz&useUnicode=true&characterEncoding=gbk" ;
    //假设test是你的数据库
    Connection conn= DriverManager.getConnection(url);
    Statement stmt=conn.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE,ResultSet.CONCUR_UPDATABLE);
    String sql="select * from dept;";
    //从test表读数据
    ResultSet rs=stmt.executeQuery(sql);
    while(rs.next())
    {
    %>
    <BR>
    您的第一个字段内容为:<%=rs.getString(1)%> <BR>
    您的第二个字段内容为:<%=rs.getString(2)%> <BR>
    <%
    }
    %>
    <BR>
    <%
    out.print("数据库操作成功,恭喜你");%>
    <%
    rs.close();
    stmt.close();
    conn.close();
    %>
    </body>
    </html>

    标签将加亮显示,按ctrl+s保存。
    3.创建测试数据库
    start->run输入cmd,回车。到达console界面,输入mysql -u root -p1qaz,回车。
    输入一下sql语句:

    use test;

    create table DEPT(
    DEPTNO INT NOT NULL,
    DNAME VARCHAR(18) NOT NULL
    );

    --insert DEPT
    insert into DEPT
    VALUES(8,'EE');
    insert into DEPT
    VALUES(9,'cs');
    insert into DEPT
    VALUES(10,'AUTO');
    insert into DEPT
    VALUES(20,'MATH');

    4.测试
    只需在eclipse主窗口中,按tomcat重新启动按钮。在浏览器中地址栏中输入http://localhost:8080/tomcat_test/test.jsp浏览器显示如下:



    posted @ 2006-08-04 16:11 rendong 阅读(1611) | 评论 (10)编辑 收藏
     
    Struts Tiles
    我很喜欢 struts ,这是我目前最熟悉的 MVC Framework ,但是 struts 的 template Engine 和 Turbine(jakarta 另外一个 mvc framework,还有一个 tapestry )使 用的 Velocity 有异曲同工之妙,另外如果你们在 Mail List 看到 Craig R. McClanahan 这号人物, 他就是“神”的代言人!
    MVCII Framework
    Cotroller是指由 Servlet 所主导,Model 为 JavaBean所开发, 最后以 JSP 做 View 端的呈现,最后 将资料返回到客户端. 而今天我要讨论的就是客户端的 Template Engine -- Tiles.

    View (Template Engine)-Tiles
    Tiles是由Cedric Dumoulin老大所开发的 Template Engine , 什么叫做 Template Engine呢, 他是一个版面切割控制的处理中心.通常我们在早古时代大约 ( 1995 ~ 2000 )年间 , 设计网页大多以 Frame 为切割网页的方式 , 因为当时网络带宽不足, 加上开发工具短缺,所以我们那时候对于版面的控制大 概也只是这样, 但随着宽带网络的普及化,造就了网页的复杂功能, HTML 4.0 包含了 Layer的功能,问题 来了, Layer 无法跨过 Frame变成一个浮动的控制小窗口,所以 Frame渐渐被淘汰,变成整个网页由 Table 的切割来组合而成, 但是, Table 的设计大多属于网页美工的工作,你要他们懂得如何写动态程序, 大概只有 1/10 的美工可以做到,所以我们建议是各师其职,让网页视觉大师的工作就单纯只是网页设计, 所以 Template Engine就应运而生,那比较有名的有, Velocity, Tiles, FreeMaker等等. 而 Struts 是使用 Tiles的,这次我就针对 Tiles 做初级的介绍.

    基本上, 你在撰写 JSP的时候, 如果 /WEB-INF/lib/之下有放struts.jar那就代表说, 你的 JSP 可以 import struts 的组件进来, 而 struts-tiles.tld我通常会放在 /WEB-INF/tlds/目录之下,所以你在 JSP 的开始的地方就要写
    <%@ taglib uri="/WEB-INF/tlds/struts-tiles.tld" prefix="tiles" %> 这意思就是说你这个网页将会通过 Struts-Tiles 这个 TagLib去调用 Tiles Template Engine , 你可以自 己打开 struts-tiles.tld 这个文件看看, 里面的定义就是说,当你调用到其中的 tag时候,他需要去调 用哪一个程序来执行你想得到的结果.

    完全战略首部曲--建立模板 (template.jsp)
    建立一个 template.jsp, 你先规划书面需要切割成为各个区块,本范例是切成上方标题区(top),左方主选单 (menu),右方主画面再切割上下区域各为 main 及 copyright :


    <%@ page contentType="text/html;charset=BIG5" %>
    <%@ taglib uri="/WEB-INF/tlds/struts-tiles.tld" prefix="tiles" %>

    <BODY leftmargin="0" marginheight="0" marginwidth="0" topmargin="0" bgcolor="#FFFFFF"

    link="#660000">
    <table border=\'0\' cellpadding=\'0\' cellspacing=\'0\' width=\'100%\'>
     <!-- 上方标题区 -->
     <tr>
     <td colspan=\'2\'>
      <img src="<%=request.getContextPath()%>/images/top.gif" border="0">
     </td>
     <!-- 左方主选单 -->
     <tr valign=\'top\'>

     <td width=\'120\' bgcolor=\'#FFFFFF\' align=\'center\'>
      <tiles:insert attribute="menu"/>
     </td>

     <!-- 右方主画面 -->
     <td width=\'680\'>
      <table border=\'0\' cellpadding=\'0\' cellspacing=\'0\' width=\'100%\'>
      <tr>
       <td  bgcolor=\'ffffff\'>
       <tiles:insert attribute="main"/>
       </td>
      </tr>

      </table>
     </td>
     <tr>
     <td colspan=\'2\'>
      <tiles:insert attribute="copyright"/>
     </td>
    </table>




    完全战略二部曲--定义 definations.xml
    根据 template.jsp 定义的 InsertTag 属性名称 ( attribute )给予一个 jsp/html来显示


     <definition name="test.screen" path="/admin/template.jsp">
      <put name="menu" value="/menu.jsp"/>
      <put name="main" value="/index.jsp"/>
      <put name="copyright" value="/copyright.jsp"/>
     </definition>

    完全战略三部曲--制作 ScreenServlet.java (WARN:copyrights are reserved by Softleader Copr.)
    编译以下之程序(ScreenServlet.class)放到 /WEB-INF/classes/com/softleader/system/init/之下


    package com.softleader.system.init;

    import java.util.StringTokenizer;
    import java.util.HashMap;
    import java.util.Locale;
    import java.util.Map;

    import java.io.IOException;
    import java.io.PrintWriter;

    import java.net.URL;

    import javax.servlet.*;
    import javax.servlet.ServletException;
    import javax.servlet.ServletConfig;
    import javax.servlet.ServletContext;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import javax.servlet.http.HttpSession;

    import javax.naming.InitialContext;
    import javax.naming.NamingException;

    import org.apache.struts.action.Action;
    import org.apache.struts.action.ActionForm;
    import org.apache.struts.action.ActionForward;
    import org.apache.struts.action.ActionMapping;
    import org.apache.struts.tiles.*;
    import org.apache.struts.tiles.TilesUtil;

    public class ScreenServlet extends HttpServlet {

        private ServletContext context;
        /** Debug flag */
        public static final boolean debug = true;
        /** Associated definition factory */
        protected DefinitionsFactory definitionFactory;
        protected ComponentDefinition definition;
        private TilesRequestProcessor trp;

        public void init()  throws ServletException {
        }


        public void doPost(HttpServletRequest request, HttpServletResponse response)
        throws IOException, ServletException {
            process(request, response);
        }

        public void doGet(HttpServletRequest request, HttpServletResponse  response)
        throws IOException, ServletException {
            process(request, response);

        }

        public void process(HttpServletRequest request, HttpServletResponse  response)
        throws IOException, ServletException {
            // init screen
            String screenName = null;
            String selectedUrl = request.getRequestURI();

            // get the screen name
            int lastPathSeparator = selectedUrl.lastIndexOf("/") + 1;
            int lastDot = selectedUrl.lastIndexOf(".");
            if (lastPathSeparator != -1 && lastDot != -1 && lastDot > lastPathSeparator) {
                screenName = selectedUrl.substring(lastPathSeparator);
            }

            try {
                // Read definition from factory, but we can create it here.
                //ComponentDefinition definition = DefinitionsUtil.getDefinition( screenName,

    request, this.getServletContext() );
                //System.out.println("get Definition " + definition );
                //DefinitionsUtil.setActionDefinition( request, definition);
                //DefinitionsFactory definitionsFactory =

    DefinitionsUtil.getDefinitionsFactory(getServletContext());
                DefinitionsFactory definitionsFactory = TilesUtil.getDefinitionsFactory(request,

    getServletContext());

                String uri="";
                Controller controller;
                ComponentContext tileContext = null;

                if( definitionsFactory != null ) {
                    // Get definition of tiles/component corresponding to uri.
                    ComponentDefinition definition
                        = definitionsFactory.getDefinition(screenName, request, getServletContext());


                    if( definition != null ){
                        // We have a definition.
                        // We use it to complete missing attribute in context.
                        // We also get uri, controller.
                        uri = definition.getPath();
                        controller = definition.getOrCreateController();

                        if( tileContext == null ) {

                            tileContext = new ComponentContext( definition.getAttributes() );
                            ComponentContext.setContext( tileContext, request);

                        }
                        else
                            tileContext.addMissing( definition.getAttributes() );
                    } // end if
                } // end if


                RequestDispatcher rd = getServletContext().getRequestDispatcher(uri);

                rd.forward(request, response);


            }  catch( Exception ex ) {
            }
        }

    }

    并且设定 web.xml增加一个 ScreenServlet

      <servlet>
        <servlet-name>ScreenServlet</servlet-name>
        <display-name>ScreenServlet</display-name>
        <servlet-class>com.softleader.system.init.ScreenServlet</servlet-class>
        <load-on-startup>3</load-on-startup>
      </servlet>

    测试网页呈现
    当然,你需要自己建立相关定义在 definations.xml 的 jsp文件, 接着重新启动 tomcat, 你就可以看到 http://localhost:8080/test.screen是一个整合起来的画面了

    1. 设定相关的 compile 环境, 基本上,可以直接使用 struts source 的 libs 和 sources
    2. 设定相关的 properties 及 xml,如果不太了解, 请直接查阅 oreilly 所出的 Struts
    3. 请尊重知识产权,本文章之原始文件不得用于商业用途,需要时请于本公司联络.
    4. Struts 网站: http://jakarta.apache.org/struts/
    5. Tiles网站: http://www.lifl.fr/~dumoulin/tiles/
    6. Tomcat 网站: http://jakarta.apache.org/tomcat/
    7. 以上程序都在 Tomcat 4.1.x以上以及 Sun JDK 1.4.x以上测试完成

    上周告诉大家使用 tile 的基本方法,当然也有更基本的, 相关的文件有 Manning 出的 Struts in Action 和 Oreilly 出的 Programming Jakarta Struts 里面都有详尽的解释. 不过今天要介绍的时更高阶的技术-- Tile Layout ,书上 都没有提到,呵呵!!

    单独使用 Tiles
    把 tiles.jar 放到 WEB-INF/lib/
    把 tiles.tld 放到 WEB-INF/
    把 commons-digester.jar,commons-collections.jar,commons-beanutils.jar 放到 WEB-INF/lib/ 下
    把 jakarta commons *.tld 放到 WEB-INF/ 下

    接着在 WEB-INF/web.xml 中增加


    <servlet>
     <servlet-name>action</servlet-name>
     <servlet-class>org.apache.struts.titles.TilesServlet</servlet-class>


     <init-param>
      <param-name>definitions-config</param-name>
      <param-value>/WEB-INF/tiles/tiles-definitions.xml</param-value>
     </init-param>
     <init-param>
      <param-name>definitions-parser-validate</param-name>
      <param-value>true</param-value>
     </init-param>
    </servlet>

    使用 <putList> 及 <add>
    简单来说, 上一篇介绍的 tiles definitions 的方法是一对一, tiles:insert 会去找 definitions 中的 put 值, 把指向的 jsp 抓进来, 一起包装成一个网页送到客户端的浏览器, 但是, 如果我希望在 template 中一次 加入多笔的页面该怎么做呢, 哪就得用 <putList> 接着使用 iterate 把他一个一个取出来显示.


    <titles:insert page="/template.jsp">
     <tiles:putList name="items">
      <tiles:add value="home"/>
      <tiles:add><img

    src="<%=request.getContextPath()%>/images/logo.gif"></titles:add>
      <tiles:add value="documentation"/>
     </titles:putList>
    </titles:insert>

     在 view 端 jsp 中要写


    <tiles:importAttribute/>
    <table>
     <logic:iterate id="item" name="items">
     <tr><td><%=item%></td></tr>
     </logic:iterate>
    </table>

    RssChannel
    所谓的 RssData, 是一个 webservice 的格式, 相关的介绍有
    XML.com RSS 的介绍
    Oreilly RSS 研究中心
    RSS 教学手册
    RSS 最新消息
    基本上有几个好处

    • 可能放到各个不同的 tiles channel 中 .
    • 在同一个 page 可能放到好几个不同 channel .
    • 可以简单的重新绘出 channel 画面.
    • 可能符合好几个 channel , 每一个都可以各自重绘.

    首先 我们先定义 tiles-definition.xml , 最重要的, 是 controllerUrl 需要设定 , 此外, 还需要得到 rss 的格式.



    <definition name="examples.rssChannel.body" path="/examples/tiles/rssChannels.jsp"
     controllerUrl="/examples/controller/rssChannel.do">
     <putList name="urls">
      <add value="http://newsforge.com/newsforge.rss"/>
      <add value="http://xmlhack.com/rss.php"/>
      <add value="http://lwn.net/headlines/rss"/>
     </putList>
    </definition>

    在 strut-config.xml 中定义


     <action path="/examples/controller/rssChannel"
       type="org.apache.struts.example.tiles.rssChannel.RssChannelsAction">
     </action>

    接着建立一个 RssChannelsAction 的 Class


     public final class RssChannelsAction extends TilesAction {
      public static final String CHANNELS_KEY = "CHANNELS";

      public static final String CHANNEL_URLS_KEY= "urls";

      public ActionForward doExecute(ActionMapping mapping,
        ActionForm form, HttpServletRequest request,
        HttpServletResponse response)
      throws IOException, ServletException, Exception {
       org.apache.commons.digester.rss.Channel channel = null ;


       List channels = (List)context.getAttribute(CHANNEL_URLS_KEY);
       List channelBeans = new ArrayList(channels.size());

       for ( int i=0 ; i < channels.size(); i++ ) {
        RSSDigester digester = new RSSDigester();
        String url = (String)channels.get(i);

        Channel obj = (Channel) digester.parse(url);
        channelBeans.add(obj);
       }
       context.putAttribute(CHANNELS_KEY,channelBeans);
       return null;
      }  

     }

    最后, 在 view 端 jsp 这样就可以看到 rssChannel 的资料啦


    <%@ page language="java" %>
    <%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean" %>
    <%@ taglib uri="/WEB-INF/struts-logic.tld" prefix="logic" %>
    <%@ taglib uri="/WEB-INF/struts-tiles.tld" prefix="tiles" %>

    <div align="center"><font size="+1"><b>

    <tiles:importAttribute name="CHANNELS" scope="page"/>

    <logic:iterate name="CHANNELS" id="CHANNEL" >
    <TABLE border="0" cellspacing="0" cellpadding="4" width="100%" align="center" >
    <TR>
    <TD class="spanhd" ><logic:present name="CHANNEL" property="image">
      <a href="<bean:write name="CHANNEL" property="link"/>">
        <img src="<bean:write name="CHANNEL"

    property="image.URL"/>"></logic:present></a>
    </TD>
    <TD class="spanhd" width="100%"><bean:write name="CHANNEL" property="title"/>
    <a href="<bean:write name="CHANNEL" property="link"/>">[home]</a></TD>
    </TR>
    <TD class="yellow" colspan="2"><bean:write name="CHANNEL"

    property="description"/></TD>
    </TR>

    <TR>
    <TD class="datagrey" colspan="2">
    <logic:iterate name="CHANNEL" property="items" id="ITEM">
    <br><b><bean:write name="ITEM" property="title"/></b>
    <br><bean:write name="ITEM" property="description"/>
    <br>  [ <a href="<bean:write name="ITEM"

    property="link"/>">more</a> ]
    <br>
    </logic:iterate>
    </TD>
    </TR>
    </TABLE>
    <br>
    </logic:iterate>

    </b></font></div>

    Layouts
    目前 tiles-example 有提供几种不同的 layout 可以参考

    Layout NameParametersUse
    Class Layout
    1. title
    2. header
    3. menu
    4. body
    5. fotter
    使用 <tiles:getAsString attribute="title"> 取得标题外,
    其余使用 <tiles:insert attribute="menu">
    Menu Layout
    1. title
    2. items
    使用 <tiles:getAsString attribute="title"> 取得标题外,
    其余使用 org.apache.struts.tiles.beans.MenuItem  iterate
    VBox or VStack Layout
    1. list
    使用 <tiles:useAttribute classname="java.util.List" name="list" id="list">
    Multi-columns Layout
    1. numCols
    2. list1
    3. list2 [optional]
    4. list3 [optional]
    5. listn [optional]
    使用 <tiles:useAttribute classname="java.util.String" name="numCols" id="numColsStr"> 接着使用 <tiles:insert> 和 <tiles:put> 将资料放进来
    Center Layout
    1. header
    2. right
    3. body
    4. left
    5. footer
    使用 <tiles:insert> 和 <tiles:put> 将资料放进来
    Tabs Layout
    1. tabList
    2. selectedIndex
    3. parameterName
    这个几乎以上用到的观念都会用到
    当然, 你也可以建立自己的 Layout , 我们希望你能建立符合 MVC 观念的 Layout!!

    posted @ 2006-08-02 23:11 rendong 阅读(2299) | 评论 (0)编辑 收藏
     

    一:说明
    Struts1.1以后增加了Tiles包使得struts在页面的处理方面多了一种选择.并且更容易实现代码的重用。Tiles中对页面的划分有点象jakarta的另外一个项目Turbine中的TDK。增加了layout的概念.其实就是把一个页面划分为几块。通常的来说一个页面大概可以划分为如下几块:head页面头部:存放一个运用的公共信息:logo等,如果是网站可能是最上面的一块.menu页面菜单:放置一个运用中需要使用的菜单,或者在每一个页面都使用的连接.footer页面尾部:如版权信息等.body页面主题内容:每个页面相对独立的内容.如果按上面的划分那对每一个页面我们只要写body里面的内容,其他的就可以共享重用.如果大多数页面的布局基本相同我们甚至可以使用一个jsp文件根据不同的参数调用不同的body.

    二:Tiles配置和基本配置文件介绍
    Tiles有一个配置文件:tiles-defs.xml
    tiles-defs.xml定义了每一个页面的组成元素和形式。
    下面我将说明如下所示的一个tiles-defs.xml文件
    tiles-defs.xml
    -----------------------------------------------
    <tiles-definitions>
    <!--定义/layouts/classicLayout.jsp的组成名称为site.mainLayout-->
    <!--后面将附/layouts/classicLayout.jsp的内容-->
    <definition name="site.mainLayout" path="/layouts/classicLayout.jsp">
    <put name="title" value="Tiles Blank Site" />
    <put name="header" value="/tiles/common/header.jsp" />
    <put name="menu" value="site.menu.bar" />
    <!--menu的组成为site.menu.bar对应的页面-->
    <put name="footer" value="/tiles/common/footer.jsp" />
    <put name="body" value="/tiles/body.jsp" />
    </definition>
    <!--定义site.index.page,继承site.mainLayout-->
    <definition name="site.index.page" extends="site.mainLayout" >
    <put name="title" value="Tiles Blank Site Index" />
    <put name="body" value="/tiles/body.jsp" />
    <!--以上两个元素将替换site.mainLayout中的元素-->
    </definition>

    <definition name="site.menu.bar" path="/layouts/vboxLayout.jsp" >
    <putList name="list" >
    <add value="site.menu.links" />
    <add value="site.menu.documentation" />
    </putList>
    </definition>
    </tiles-definitions>

    附:/layouts/classicLayout.jsp
    --------------------------------
    <html>
    <head>
    <title><tiles:getAsString name="title"/>
    </title>
    </head>

    <body bgcolor="#ffffff" text="#000000" link="#023264" alink="#023264" vlink="#023264">
    <table border="0" width="100%" cellspacing="5">
    <tr>
    <td colspan="2"><tiles:insert attribute="header" /></td>
    </tr>
    <tr>
    <td width="140" valign="top">
    <tiles:insert attribute='menu'/>
    </td>
    <td valign="top" align="left">
    <tiles:insert attribute='body' />
    </td>
    </tr>
    <tr>
    <td colspan="2">
    <tiles:insert attribute="footer" />
    </td>
    </tr>
    </table>
    </body>
    </html>

    在web.xml里面配置tiles,配置完后对应struts action servlet的配置如下:
    web.xml
    -----------------
    <!-- Action Servlet Configuration -->
    <servlet>
    <servlet-name>action</servlet-name>
    <!-- Specify servlet class to use:
    - Struts1.0.x: ActionComponentServlet
    - Struts1.1: ActionServlet
    - no Struts: TilesServlet
    -->
    <servlet-class>org.apache.struts.action.ActionServlet</servlet-class>

    <!-- Tiles Servlet parameter
    Specify configuration file names. There can be several comma
    separated file names
    -->
    <init-param>
    <param-name>definitions-config</param-name>
    <param-value>/WEB-INF/tiles-defs.xml</param-value>
    </init-param>

    <!-- Tiles Servlet parameter
    Specify Tiles debug level.
    O : no debug information
    1 : debug information
    2 : more debug information
    -->
    <init-param>
    <param-name>definitions-debug</param-name>
    <param-value>1</param-value>
    </init-param>

    <!-- Tiles Servlet parameter
    Specify Digester debug level. This value is passed to Digester
    O : no debug information
    1 : debug information
    2 : more debug information
    -->
    <init-param>
    <param-name>definitions-parser-details</param-name>
    <param-value>0</param-value>
    </init-param>

    <!-- Tiles Servlet parameter
    Specify if xml parser should validate the Tiles configuration file.
    true : validate. DTD should be specified in file header.
    false : no validation
    -->
    <init-param>
    <param-name>definitions-parser-validate</param-name>
    <param-value>true</param-value>
    </init-param>

    <!-- Struts configuration, if Struts is used -->
    <init-param>
    <param-name>config</param-name>
    <param-value>/WEB-INF/struts-config.xml</param-value>
    </init-param>
    <init-param>
    <param-name>validate</param-name>
    <param-value>true</param-value>
    </init-param>
    <init-param>
    <param-name>debug</param-name>
    <param-value>2</param-value>
    </init-param>
    <init-param>
    <param-name>detail</param-name>
    <param-value>2</param-value>
    </init-param>

    <load-on-startup>2</load-on-startup>
    </servlet>

    三:使用Tiles
    如果已经配置好tiels-defs.xml,接下来就可以在jsp文件中使用这些定义了。
    有如下的方式使用tiles
    3.1:
    <tiles:insert definition="site.mainLayout" flush="true" />
    插入site.mainLayout标记的一页

    3.2:
    <tiles:insert template="/tutorial/basic/myFramesetLayout.jsp" >
    <tiles:put name="title" content="My first frameset page" direct="true" />
    <tiles:put name="header" content="/tutorial/common/header.jsp" direct="true"/>
    <tiles:put name="footer" content="/tutorial/common/footer.jsp" direct="true"/>
    <tiles:put name="menu" content="/tutorial/basic/menu.jsp" direct="true"/>
    <tiles:put name="body" content="/tutorial/basic/helloBody.jsp" direct="true"/>
    </tiles:insert>

    /tutorial/basic/myFramesetLayout.jsp
    ---------------------------------
    <html>
    <head>
    <title><tiles:get name="title"/></title>
    </head>

    <frameset rows="73, *, 73">
    <frame src="<%=request.getContextPath()%><tiles:get name="header" />" name="header" >
    <frame src="<%=request.getContextPath()%><tiles:get name="body" />" name="body" >
    <frame src="<%=request.getContextPath()%><tiles:get name="footer" />" name="footer" >
    </frameset>

    </html>
    插入/tutorial/basic/myFramesetLayout.jsp
    并把title的值设定为:My first frameset page
    header设定为/tutorial/common/header.jsp

    四:后记
    Tiles的使用在他的文档里面写的比较详细。以上是一些简单和基本的使用。具体的文档可以看Struts里面的一个tiles-documentation.war的包。但即使是这个包也不是很全。可以通过上的的连接到作者的主页上去找。个人觉得使用Tiles在做企业运用的时候可能不如在做网站那样更能体现优越性。但在系统开始设计的时候考虑并规划好整个UI,那在修改和维护的时候将节省不少的工作量,因为通常UI的确定在代码编写结束和完成,所有尽可能的使用多个子页面构成一个页面,后面的美化和维护就比直接维护一个很大的页面容易

    posted @ 2006-08-02 23:09 rendong 阅读(456) | 评论 (0)编辑 收藏
     
    摘要

    Hibernate和struts是当前市面上几个最流行的开源的库之一。它们很有效率,是程序员在开发Java企业应用,挑选几个竞争的库的首选。虽然它们经常被一起应用,但是Hibernate的设计目标并不是和Struts一起使用,而Struts在Hibernate诞生好多年之前就发布了。为了让它们在一起工作,仍然有很多挑战。这篇文章点明了Struts和Hibernate之间的一些鸿沟,尤其关系到面向对象建模方面。文章也描述了如何在两者间搭起桥梁,给出了一个基于扩展Struts的解决方案。所有的基于Struts和Hibernate构建的Web应用都能从这个通用的扩展中获益。

    在Hibernate in Action(Manning,2004十月)这本书里,作者Christian Bauer和Gavin King揭示了面向对象世界的模型和关系数据模型,两个世界的范例是不一致的。Hibernate非常成功地在存储层(persistence Layer)将两者粘合在一起。但是领域模型(domain model)(也就是Model-View-Controller的model layer)和HTML页面(MVC的View Layer)仍然存在不一致。在这篇文章中,我们将检查这种不一致,并且探索解决的方案。

    范例不一致的再发现

    让我们先看一个经典的parent-child关系例子(看下面的代码):product和category。Category类定义了一个类型为long的标示符id和一个类型为String的属性name。Product类也有一个类型为long的标示符id和一个类型为Category的属性category,表示了多对一的关系(也就是说很多product可以属于一个Category)

    						/**
    * @hibernate.class table="CATEGORY"
    */
    public class Category {
       private Long id;

       private String name;

       /**
        * @hibernate.id generator-class="native" column="CATEGORY_ID"
        */
       public Long getId() {
          return id;
       }

       public void setId(Long id) {
          this.id = id;
       }

       /**
        * @hibernate.property column="NAME"
        */
       public String getName() {
          return name;
       }

       public void setName(Long name) {
          this.name = name;
       }
    }

    /**
    * @hibernate.class table="PRODUCT"
    */
    public class Product {
       private Long id;
       private Category category;

       /**
        * @hibernate.id generator-class="native" column="PRODUCT_ID"
        */
       public Long getId() {
          return id;
       }

       public void setId(Long id) {
          this.id = id;
       }

       /**
        * @hibernate.many-to-one
        * column="CATEGORY_ID"
        * class="Category"
        * cascade="none"
        * not-null="false"
        */
       public Category getCategory() {
          return category;
       }

       public void setCategory(Category category) {
          this.category = category;
       }
    }



    我们希望一个product可以被更改category,所以我们的HTML提供了一个下拉框列出所有Category。

    						<select name="categoryId">
       <option value="">No Category</option>
       <option value="1">Category 1</option>
       <option value="2">Category 2</option>
       <option value="3">Category 3</option>
    </select>



    这里我们看出了两者的不一致:在Product领域对象里,category属性是Category类型,但是ProductForm只有一个类型为long的categoryId。这种不匹配不但增加了不一致,而且导致了不必要的代码进行primitive type的标示符和对应的对象之间的转换。

    这种不一致部分是由于HTML Form自己引起的:它只代表了一种关系模型,不能代表面向对象的模型。面向对象和关系模型的不一致在存储层由对象关系映射(O/RM)解决。但是类似的问题在表示层(view layer)仍然存在。解决的关键是让他们一起无缝地工作。

    Struts的功能和局限

    幸运的是,Struts能够生成和解释内嵌的对象属性。Category下拉框可以用Struts page-construction(html) tag library:

    						<html:select property="category.id">
       <option value="">No Category</option>
       <html:options collection="categories" property="id" labelProperty="name"/>
    </html:select>


    我们假设categories是Category对象的一个list。所以现在我们要修改ProductForm,让它变得更加“面向对象”,我们要修改ProductForm的categoryId,改成类型为Category的category。这种改变会导致在Product和ProductForm之间复制属性的工作更加繁琐,因为两者有相同的属性。

    						public class ProductForm extends ActionForm {
         private Long id;
         private Category category;
         ...
    }



    当我们完成剩余的Struts Action, configuration, validator, jsp, hibernate层后,开始测试,我们马上在访问ProductForm.category.id时遇到了NullPointerException。这是预料中的!因为ProductForm.category还没有被设置,同时,Hibernate也会将多对一所联系的对象引用设为空(如果database field为空指)(译者:这里指Hiberate将product.category为Null,如果该Product没有联系到任何category)。Struts要求所有的对象在显示(生成HTML Form)和传播(提交HTML FORM)之前被建立。

    让我们看看如何用ActionForm.reset()来架起桥梁。

    (并非如此)臭名昭著的Struts ActionForm

    在我第一个星期接触Struts的时候,我最大的一个疑问就是:为什么我必须为Properties, getter方法, setter方法保持几乎完全相同的两份copy, 一份在ActionForm Bean, 一份在DomainObject。这个繁琐的步骤成了Struts社区最主要的抱怨之一。

    以我的观点,ActionForm存在有原因的。首先,它们可以区别于Domain Object因为他们但当了不同的角色。在MVC模式下,Domain Object是Model层的一个部分,ActionForm是View层的。因为Webpage的Field和Database的Field可能不一样,某些特制的转换是常见的。第二,ActionForm.validate()方法可以定义非常好用的验证规则。第三,可能有其他的,特定的View行为,但是又不想在domain layer实现,特别当persistence framework来管理domain object的时候。

    提交Form

    让我们来用ActionForm内有的方法-reset()-来解决view和model之间的不一致。这个reset()方法是在ActionForm在被Struts Controller Servlet处理request时候复制ActionForm属性之前调用的。这个方法最常见的使用是:checkbox必须被显式地设为false,让没有被选中的checkbox被正确识别。Reset()也是一个初始化用于view rending对象的合适地方。代码看起来是这样的:

    						public class ProductForm extends ActionForm {
         private Long id;
         private Category category;
         ...
         public void reset(ActionMapping mapping, HttpServletRequest request)
         {
            super.reset( mapping, request );
            if ( category == null ) { category = new Category(); }
         }
    }



    Struts在使用用户提交的值填写ProductForm之前,Struts会调用reset(),这样category属性将会被初始化。请注意,你必须检查category看它是不是null,后面我们会讨论这个。

    编辑Form

    到目前为止,我们已经解决了form提交时候的问题。但是当我们在生成form页面的时候呢?Html:select tag也希望有一个非空的引用,所以我们将在form生成页面之前调用reset()。我们在action类里加入了一行:

    						public class EditProductAction extends Action {
         public final ActionForward execute( ActionMapping mapping, ActionForm form,
            HttpServletRequest request, HttpServletResponse response ) throws Exception
         {
            ...
            Product product = createOrLoadProduct();
            ProductForm productForm = (ProductForm)form;
            PropertyUtils.copyProperties( productForm, product );
            productForm.reset( mapping, request );
            ...
         }
    }



    我假设读者已经对action类和Jakarta commons Beanutils包非常熟悉了。CreateOrLoadProduct()建立了一个新的Product实例或者从数据库里载入一个已有的实例,具体取决于这个action是建立或者修改Product的。ProductForm被赋值后(译者:也就是调用PropertyUtils.copyProperties后),productForm.category已经从Product.category复制过来了(译者:实际上只是复制了category对象引用,并没有开销),然后,ProductForm就能用来生成页面了。我们同时也必须保证:不覆盖已经被Hibernate载入的对象,所以我们必须检查(category)是不是为null。

    因为reset()方法是在ActionForm中定义的,我们可以把上述代码放入一个superclass,比如CommonEditAction,来处理这些事情:
        
    						      Product product = createOrLoadProduct();
            PropertyUtils.copyProperties( form, product );
            form.reset( mapping, request );


    如果你需要一个只读的Form, 你有两个选择: 第一检查所联系的jsp对象是不是null, 第二复制domain对象到ActionForm之后调用Reset()

    保存domain对象

    我们解决了提交Form和生成Form页面的问题, 所以Struts可以满足了。但是Hibernate呢?当用户选择了一个null ID option – 在我们的例子中“no category”option- 并且提交form, productForm.category指向一个新建立的hibernate对象,id为null。当category属性从ProductForm复制到Hibernate控制的Product对象并且存储时,Hibernate会抱怨product.category是一个临时对象,需要在Product存储前先被存储。当然,我们知道它是Null,并且不需要被存储。所以我们需要将product.category置为Null,然后Hibernate就能存储Product了(译者:在这种情况下,数据库product.category被设成空值)。我们也不希望改变Hibernate的工作方式,所以我们选择在复制到Domain对象之前清理这些临时对象,我们在ProductForm中加了一个方法:

    						public class ProductForm extends ActionForm {
         private Long id;
         private Category category;
         ...
         public void reset(ActionMapping mapping, HttpServletRequest request) {
            super.reset( mapping, request );
            if ( category == null ) { category = new Category(); }
         }

         public void cleanupEmptyObjects() {
            if ( category.getId() == null ) { category = null; }
         }
    }



    我们在copyProperties之前清理掉这些临时对象,所以如果ProductForm.category只是用来放Null的,则将ProductForm.category置为Null。然后Domain对象的category也会被设成null:

    						public class SaveProductAction extends Action {
         public final ActionForward execute( ActionMapping mapping, ActionForm form,
            HttpServletRequest request, HttpServletResponse response ) throws Exception
         {
            ...
            Product product = new Product();
            ((ProductForm)form).cleanupEmptyObjects();
            PropertyUtils.copyProperties( product, form );
            SaveProduct( product );
            ...
         }
    }



    一对多关系

    我还没有解决Category到Product的一对多关系。我们把它加入到Category的Metadata中:

    						public class Category {
         ...
         private Set products;
         ...

         /**
          * @hibernate.set
          * table="PRODUCT"
          * lazy="true"
          * outer-join="auto"
          * inverse="true"
          * cascade="all-delete-orphan"
          *
          * @hibernate.collection-key
          * column="CATEGORY_ID"
          *
          * @hibernate.collection-one-to-many
          * class="Product"
          */
         public Set getProducts() {
            return products;
         }

         public void setProducts(Set products) {
            this.products = products;
         }
    }


    注意:Hibernate的cascade属性为all-delete-orphan表明:Hibernate需要在存储包含的Category对象时候,自动存储Product对象。和parent对象一起存储child对象的情况并不常见,常见的是:分别控制child的存储和parent的存储。在我们的例子中,我们可以容易地做到这一点,如果我们允许用户在同一个html page编辑Category和ProductS。用set表示Products是非常直观的:

    						public class CategoryForm extends ActionForm {
         private Set productForms;
         ...
         public void reset(ActionMapping mapping, HttpServletRequest request) {
            super.reset( mapping, request );

            for ( int i = 0; i < MAX_PRODUCT_NUM_ON_PAGE; i++ ) {
               ProductForm productForm = new ProductForm();
               productForm.reset( mapping, request );
               productForms.add( productForm );
            }
         }

         public void cleanupEmptyObjects() {
            for ( Iterator i = productForms.iterator(); i.hasNext(); ) {
               ProductForm productForm = (ProductForm) i.next();
               productForm.cleanupEmptyObjects();
            }
         }
    }



    更进一步
    我们已经可以察看,编辑,提交forms,并且存储相关的objects,但是为所有的ActionForm类定义CleanupEmptyObjects()和reset()方法是个累赘。我们将用一个抽象的ActionForm来完成协助完成这些工作。

    作为通用的实现,我们必须遍历所有的Hibernate管理的domain对象,发现他们的identifier,并且测试id值。幸运的是:org.hibernate.metadata包已经有两个Utility类能取出domain对象的元数据。我们用ClassMetadata类检查这个object是不是Hibernate管理的。如果是:我们把它们的id Value取出来。我们用了Jakarta Commons Beanutils包来协助JavaBean元数据的操作。

    						import java.beans.PropertyDescriptor;
    import org.apache.commons.beanutils.PropertyUtils;
    import org.hibernate.metadata.ClassMetadata;

    public abstract class AbstractForm extends ActionForm {
       public void reset(ActionMapping mapping, HttpServletRequest request) {
          super.reset( mapping, request );

          // Get PropertyDescriptor of all bean properties
          PropertyDescriptor descriptors[] =
             PropertyUtils.getPropertyDescriptors( this );

          for ( int i = 0; i < descriptors.length; i++ ) {
             Class propClass = descriptors.getPropertyType();

             ClassMetadata classMetadata = HibernateUtil.getSessionFactory()
                .getClassMetadata( propClass );

             if ( classMetadata != null ) {   // This is a Hibernate object
                String propName = descriptors.getName();
                Object propValue = PropertyUtils.getProperty( this, propName );

                // Evaluate property, create new instance if it is null
                if ( propValue == null ) {
                   PropertyUtils.setProperty( this, propName, propClass.newInstance() );
                }
             }
          }
       }

       public void cleanupEmptyObjects() {
          // Get PropertyDescriptor of all bean properties
          PropertyDescriptor descriptors[] =
          PropertyUtils.getPropertyDescriptors( this );

          for ( int i = 0; i < descriptors.length; i++ ) {
             Class propClass = descriptors.getPropertyType();
             ClassMetadata classMetadata = HibernateUtil.getSessionFactory()
                .getClassMetadata( propClass );

             if ( classMetadata != null ) {   // This is a Hibernate object
                Serializable id = classMetadata.getIdentifier( this, EntityMode.POJO );

                // If the object id has not been set, release the object.
                // Define application specific rules of not-set id here,
                // e.g. id == null, id == 0, etc.
                if ( id == null ) {
                   String propName = descriptors.getName();
                   PropertyUtils.setProperty( this, propName, null );
                }


             }
          }
       }
    }


    为了让代码可读,我们省略了Exception的处理代码。

    我们的新AbstractForm类从Struts的ActionForm类继承,并且提供了通用行为:reset和cleanup多对一关联对象。当这个关系是相反的话(也就是一对多关系),那么每个例子将会有所不同,类似在Abstract类里实现是比较好的办法。

    总结

    Struts和Hibernate是非常流行和强大的框架,他们可以有效地相互合作,并且弥补domain模型和MVC视图(view)之间的差别。这篇文章讨论一个解决Struts/Hibernate Project的通用的方案,并且不需要大量修改已经有的代码。
    posted @ 2006-08-02 01:01 rendong 阅读(206) | 评论 (0)编辑 收藏
     
    人人都爱Spring加Hibernate。
        但Spring MVC+hibernate的Sample如Appfuse的代码却算不得最简洁优美好读,如果在自己的项目中继续发挥我们最擅长的依样画葫芦大法,美好愿望未必会实现。
         所以,Pramatic精神不灭。这个系列就是探寻最适合自己的Spring+Hibernate模式。
        
                                  I-配置文件简化

         我厌倦一切配置文件繁重的框架。
         最好的情况是,框架提供极端灵活复杂的配置方式,但只在你需要的时候
     
         Spring提供了三种可能来简化XML。随着国内用户水平的提高,这些基本的简化技巧大家都已掌握。
         大家可以直接看第3,第4点--Spring 1.2, Spring 2.0的后继改进。

    1.1.autowire="byName" /"byType"

         假设Controller有一个属性名为customerDAO,Spring就会在配置文件里查找有没有名字为CustomerDAO的bean, 自动为Controller注入。
         如果bean有两个属性,一个想默认注入,一个想自定义,只要设定了autowire,然后显式的声明那个想自定义的,就可以达到要求。这就应了需求,在需要特别配置的时候就提供配置,否则给我一个默认注入。

         还有一个更懒的地方,在最最根部的<beans>节点写一句default-autovwrie="byName",可以让文件里的所有bean 都默认autowrie。
        不过Rod认为开发期可以这样,但Production Server上不应该使用Autowire。而我觉得那些自定义一次的地方比如TranscationManager应该详细定义,而Dao,Controller这种大量重复定义的bean就可以偷点懒了。

    1.2.<bean>节点之间抽象公共定义和 Inner Bean

        这太方便懒人了,想不到两个独立的XML节点都可以玩继承和派生,子节点拥有父节点的全部属性。
        最好用的地方就是那个Transtion Proxy的定义。先定义一个又长又冗的父类,然后用子类去继承它。
       
        另外,还有一个Inner Bean的机制,可以把DAO写成Proxy的内部类。为什么要写成内部类?为了让Proxy冒名顶替它去让Controller Autowire。(详见后面的示例)

    1.3. 宽松的配置, To XML or Not to XML 
        据说Spring比Struts的配置宽松了很多,这就给人把东西从配置文件中撤回原码中的机会。
        不赞成什么都往配置文件里晒,造成了Rich Information的配置文件,修改或者查看的时候,要同时打开配置文件和原码才能清楚一切。
        而我希望配置文件就集中做一些整体的配置,还有框架必须的、无需管理的冗余代码。而一些细节的变化不大的配置和逻辑,就尽量别往里塞了。因此,Success/Fail View 的配置,不建议放在里面。

    2.简化后的配置文件

    1.Controller只剩下一句

    <bean name="customerController" class="org.springside.bookstore.web.CustomerController" autowire="byName"/>

    2.DAO也只剩一句

    <bean id="customerDAO" class="org.springside.bookstore.dao.CustomerDao"/>

    3.Service类只剩下5行

      <bean id="customerManager" parent="baseTxService">
            
    <property name="target">
                
    <bean class="org.springside.bookstore.service.CustomerManager"/>
            
    </property>
        
    </bean>

    3.Spring 1.2后xml语法简化

     
    最主要的简化是把属性值和引用bean从子节点变回了属性值,对不喜欢autowire的兄弟比较有用。
     当然,如果value要CDATA的时候还是要用子节点。另外,list的值可以用空格隔开也比较实用。

    1.属性值

      <property name="foo">
         
    <value>fooValue</value>
      </property>
      简化为
      <property name="foo" value="fooValue"/>

    2.引用 bean
    <property name="foo">
       
    <ref bean="fooBean">
    </property>
    简化为
    <property name="foo" ref="fooBean"/>


    3. list可以简化为空格分开的字符串
     
    <property name="myFriendList">
      
    <list>
         
    <value>gigix</value>
            <value>wuyu</value>
      
    </list>
    </property>
    简化为
    <property name="myFriendList" value="gigix wuyu"/>
       
      
    4.Spring 2.0来了
       如果没什么外力刺激,spring xml 可能就这样不会变了。但现在xml成了过街老鼠,被ror的默认配置和JDK5的annotation逼得不行,当然就要继续求变。
       比如有好事者认为,节点名必须以bean打头,附加一个属性id来表示bean名;属性值必须搞一个property子节点,子节点上有个属性name来表示属性名,是给机器看的很不直观的东西。
    <bean id="customerDAO" class="org.springside...CustomerDAO">
     
    <property name="maxCount" value="10">
    </bean>

    给人看的东西应该就写成
    <customerDAO class="org.springside....CustomerDAO" maxCount="10"/>

    Spring 2.0正用schema实现类似的语法,具体请看它的JPetStore sample。

    5.使用Spring自带的DTD使编辑器Smart.

        如果没有用Eclipse的Spring插件,那至少也要使用spring自带的dtd使XML编辑器smart一些,能够自动为你生成属性,判断节点/属性名称有没有拼错等。

    6.还有更变态的简化配置方法
        比如autoproxy,不过我觉得更简化就不可控了,所以没有采用。

    因为Spring自带的sample离我们的实际项目很远,所以官方一点的model层模式展现就靠Appfuse了。
        但Appfuse的model层总共有一个DAO接口、一个DAOImpl类、一个Service接口、一个ServiceImpl类、一个DataObject.....大概只有受惯了虐待的人才会欣然接受吧。
        另外,Domain-Driven逢初一、十五也会被拿出来讨论一遍。

        其实无论什么模式,都不过是一种人为的划分、抽象和封装。只要在团队里理解一致,自我感觉优雅就行了。
         我的建议是,一开始DO和Manager一生一旦包演全场,DO作为纯数据载体,而Manager类放置商业方法,用getHibernateTemplate()直接访问数据库,不强制基于接口编程。当某天系统复杂到你直觉上需要将DAO层和Service层分开时,再分开就好了。

        1.DataObject类
        
    好听点也可以叫Domain Object。Domain Driven  Development虽然诱人,但因为Java下的ORM框架都是基于Data Mapper模式的,没有Ruby On Rails中那种Active Recorder的模式。所以,还是压下了这个欲望,Data Object纯粹作一个数据载体,而把数据库访问与商业逻辑操作统一放到Manager类中。

        2.Manager类
        我的Manager类是Appfuse中DAO类与Service类的结合体,因为:

        2.1 不想使用纯DAO
         以往的DAO是为了透明不同数据库间的差异,而现在Hibernate已经做的很好。所以目前纯DAO的更大作用是为了将来可以切换到别的ORM方案比如iBatis,但一个Pragmaic的程序员显然不会无聊到为了这个机会不大的理由,现在就去做一个纯DAO层,项目又不是Appfuse那样为了demo各种ORM方案而存在。

        2.2 也不使用纯的薄Service层
        在JPetStore里有一个很薄的Service层,Fascade了一堆DAO类,把这些DAO类的所有方法都僵硬的重复了一遍。而我认为Fascade的意义在二:
        一是Controller调用Manager甲的时候,总会伴随着调用Manager乙的某些方法。使用Fascade可以避免Controller零散的调用一堆Manager类。
        二是一个商业过程里可能需要同时调用DAO甲乙丙丁的方法。 

         这些时候,Fascade都是合理的。但我讨厌类膨胀,所以我宁愿在甲乙丙丁中挑一个来充当Fascade的角色。有耦合的问题吗?对一个不是死搬书的Designer来说,组件边界之内的类之间的耦合并不是耦合。

        3.去除不必要的基于接口编程
        众所周知,Spring是提倡基于接口编程的。
        但有些Manager类,比如SaleOrderManager ,只有5%的机会再有另一个Impl实现。95%时间里这两兄弟站一起,就像C++里的.h和.cpp,徒增维护的繁琐(经常要同步两个文件的函数声明),和代码浏览跳转时的不便(比如从Controler类跟踪到Service类时,只能跳转到接口类的相应函数,还要再按一次复杂的热键才跳转到实现类)
        连Martin Flower都说,强制每个类都分离接口和实现是过犹不及。只在有多个独立实现,或者需要消除对实现类的依赖时,才需要分离接口。

        3.1 DAO被强制用接口的原因
        Spring IOC本身是不会强制基于接口的,但DAO类一般要使用Spring的声明式事务机制,而声明式的事务机制是使用Spring AOP来实现的。Spring AOP的实现机制包括动态代理和Cgilib2,其中Spring AOP默认使用的Java动态代理是必须基于接口,所以就要求基于接口了。
        
        3.2 解决方法
        那就让Spring AOP改用CGLib2,生成目标类的子类吧,我们只要指定使用声明式事务的FactoryBean使用CGLib的方式来实现AOP,就可以不基于接口编程了。
        指定的方式为设置proxyTargetClass为true。如下:

    <bean class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"
    id
    ="baseService"   abstract="true">
      
    <property name="transactionManager" ref="transactionManager"/>
      
    <property name="proxyTargetClass" value="true"/>

    </bean>


         又因为这些Service Bean都是单例,效率应该不受影响。

        4.总结
        对比Appfuse里面的5个类,我的Model层里只有VO作为纯数据载体,Manager类放商业方法。有人说这样太简单了,但一个应用,要划成几个JSP,一个Controller,一个Manager,一个VO,对我来说已经足够复杂,再要往上架墙叠屋,恕不奉陪,起码在我的项目范围里不需要。(但有很多项目是需要的,神佑世人)

        后记:迫于世人的压力,SpringSide还是把DAO和Service层分开了,但依然坚持不搞那么多接口。

    Struts与Webwork的扇子请跳过本篇。

        MVC不就是把M、V、C分开么?至唯物朴素的做法是两个JSP一个负责View,一个负责Controller,再加一个负责Model的Java Bean,已经可以工作得很好,那时候一切都很简单。
        而现在为了一些不是本质的功能,冒出这么多非标准的Web框架,实在让人一阵郁闷。像Ruby On Rails那样简捷开发,可用可不用,而且没有太多的限制需要学习的,比如Webwork这型还可以考虑。但像Struts那样越用框架越麻烦,或者像Tapestry那样有严重自闭倾向,额上凿着"高手专用玩具"的,用在团队里就是不负责任的行为了。

        so,我的MVC方案是使用Spring MVC的Controller接口,写最普通的JavaBean作为Controller,本质就和当年拿JSP作Controller差不多,但拥有了Spring IOC的特性。
        之所以用这么消极的选择标准,是因为觉得这一代MVC框架离重回RAD时代的标准还很远,注定了只是一段短暂的,过渡的技术,不值得投资太多精力和团队学习成本。

    1. 原理
         Spring MVC按植物分类学属于Martin Flower〈企业应用模式〉里的静态配置型Front Controler,使用DispatchServlet截获所有*.do的请求,按照xml文件的配置,调用对应的Command对象的handleRequest(request,response)函数,同时进行依赖对象的注入。
         我们的Controller层,就是实现handleRequest(request,response)函数的普通JavaBean。

    2. 优势
        
    Spring MVC与struts相比的优势:
         一是它的Controller有着从松到紧的类层次结构,用户可以选择实现只有一个HandleRequest()函数的接口,也可以使用它有很多回调函数的SimpleFormController类。
         二是不需要Form Bean,也不需要Tapestry那所谓面向对象的页面对象,对于深怕类膨胀,改一个东西要动N个地方的人最适合不过。
         三是不需要强XML配置文件,宣告式编程是好的,但如果强制成框架,什么都要在xml里面宣告,写的时候繁琐,看的时候也要代码配置两边看才能明白就比较麻烦了。
     
         那Webwork呢?没有实战过,不过因为对MVC框架所求就不多,单用Spring MVC的Controller已经可以满足需求,就不多搞一套Webwork来给团队设坎,还有给日后维护,spring,ww2之间的版本升级添麻烦了。真有什么需要添加的,Spring MVC源代码量很少,很容易掌控和扩展。
     
    3.化简
    3.1. 直接implement Controller,实现handleRequest()函数
          首先,simple form controller非我所好,一点都不simple。所以有时我会直接implement Controller接口。这个接口的唯一函数是供Front Controller调用的handleRequest(request,response)。
          如果需要application对象,比如想用application.getRealPath()时,就要extends webApplicationObjectSupport。

    3.2.每个Controler负责一组相关的action
           我是坚决支持一个Controler负责多个action的,一个Controler一个action就像一个function一个类一样无聊。所以我用最传统的方式,用URL参数如msg="insert"把一组相关action交给一个Controler控制。ROR与制作中的Groovy On Rails都是这种模式,Spring也有MultiActionController支持。
           以上三者都是把URL参数直接反射为Controller的函数,而Stripes的设计可用annotation标注url action到响应函数的映射。
          
    3.3.xml宣告式编程的取舍 
        我的取舍很简单,反正Spring没有任何强制,我只在可能需要不重新编译而改变某些东西的时候,才把东西放在xml里动态注入。jsp路径之类的就统统收回到controller里面定义.
     
    3.4.Data Binder
           Data Binder是Controller的必有环节,对于Spring提供的DataBinder,照理完全可用,唯一不爽是对象如果有内嵌对象,如订单对象里面包含了Customer对象,Spring需要你先自行创建了Customer对象并把它赋给了Order对象,才可能实现order.customer.customer_no这样的绑定。我偷懒,又拿Jakarta BeanUtils出来自己做了一个Binder。

    3.5.提取基类
          最后还是忍不住提取了一个基类,负责MultiAction和其他一些简便的方法。Sprnig的MultiActionController做得太死,规定所有函数的第1,2个参数必须是request和response,不懂动态的,温柔的进行参数注入。

          
          经过化简再化简,已经是很简单一个Java Bean ,任谁都可以轻松上手,即使某年某月技术的大潮把现在所有MVC框架都淹没了,也不至于没人识得维护。
    人生像个舞台,请良家少女离开。
        同样的,Freemarker和Velocity爱好者请跳过本篇。与弃用webwork而单用Spring MVC Controller接口的理由一样,Freemarker本来是一样好东西,还跨界支持jsp 的taglib,而且得到了WebWork的全力支持,但为了它的非标准化,用户数量与IDE的缺乏,在View层我们还是使用了保守但人人会用,IDE友好的JSP2.0 配合JSTL。

       
    对于B/S结构的企业应用软件来说,基本的页面不外两种,一种是填Form的,一种是DataGrid 数据列表管理的,再配合一些css, js, ajax的效果,就是View层要关注的东西了。

    1. JSP 2.0的EL代替<c:out>
    JSP2.0可以直接把EL写在html部分,而不必动用<c:out>节点后,老实说,JSP2.0+JSTL达到的页面效果,已不比Velocity相差多少了。
    <p>{goods.name}</p> 
    代替
    <p><c:out value="{goods.name}"/></p>

    (除了EL里面不能调用goods的函数,sun那帮老顽固始终坚持JSTL只能用于数据显示,不能进行数据操作,所以不能调用bean的get/set外的方法)

     2. 最懒的form 数据绑定

        Spring少得可怜的几个tag基本上是鸡肋,完全可以不要。 而Spring开发中的那些Simple Form tag又还没有发布。Spring的Tag主要用来把VO的值绑到input框上。但是,和Struts一样,需要逐个Input框绑定,而且语法极度冗长,遇到select框还要自己进行处理.....典型的Spring Sample页面让人一阵头晕.

        而jodd的form tag给了我们懒人一个懒得多的方法,只要在<form>两头用<jodd:form bean="myVO"></jodd:form>包住,里面的所有input框,select框,checkBox...统统自动被绑定了,这么简单的事情,真不明白struts,spring为什么不用,为了不必要的灵活性么?

    <form>
    <jodd:form bean="human">
    <input type="text" name="name">
    <input type="radiobox" name="sex" value="man">
    <select name="age">
      
    <option value="20">20</option>
      
    <option value="30">30</option>
    </select>
    </jodd:form>
    </form> 


    不过,jodd有个致命弱点是不能绑定内嵌对象的值。比如Order(订单)对象里有个Customer(顾客)对象,jodd就不能像 struts,spring一样用如下语法绑定:

    <input name="customer.customerNo">

    这是因为它的beanUtils比Jakata Common弱,用了一个错误的思路的缘故。 动用beanUtils修改一下就可以了,修改后的源码可以在这里下载

    3. DataGrid数据列表

    DisplayTag和ValueList都属于这种形式的Tag Library。但最近出现的Extreme Table是真正的killer,他本身功能强大不说,而且从一开始就想着如何让别人进行扩展重载,比如Extend Attributes机制就是DisplayTag这样的让千人一面者不会预留。


    4.css, java script, ajax
    天下纷扰,没有什么特别想讲想推荐的,爱谁谁吧。Buffalo, DWR, Scriptaculous, Prototype, AjaxTags, AjaxAnywhere, Rico, Dojo, JSON-RPC,看着名字就头痛。

    posted @ 2006-08-02 00:57 rendong 阅读(709) | 评论 (0)编辑 收藏
     
         摘要: 优化Spring启动速度 spring的容器是提供了lazy-load,缺省设置是bean没有lazy-load,该属性处于false状态。 ...  阅读全文
    posted @ 2006-08-02 00:26 rendong| 编辑 收藏
     
         摘要: 一.spring+struts1.加载springContext      通过struts-config.xml中增加plug-in插件来加载springContext    <plug-in className="org.springframework.web.struts.ContextLoaderPlugIn">     <set-property proper...  阅读全文
    posted @ 2006-08-01 15:06 rendong 阅读(1395) | 评论 (0)编辑 收藏
     
    今天有一个朋友问了我一个问题,他使用的是Hibernate/Spring/Struts架构,配置使用Spring的OpenSessionInView Filter,但是发现不生效,lazy的集合属性在页面访问的时候仍然报session已经关闭的错误。我和他一起检查了所有的配置和相关的代码,但是没有发现任何问题。经过调试发现,应用程序使用的Session和OpenSessionInView Filter打开的Session不是同一个,所以OpenSessionInView模式没有生效,但是为什么他们不使用同一个Session呢?
    检查了一遍Spring的相关源代码,发现了问题的根源:
    通常在Web应用中初始化Spring的配置,我们会在web.xml里面配置一个Listener,即: 
       
    xml代码: 
    <listener> 
       <listener-class>
         org.springframework.web.context.ContextLoaderListener
       </listener-class> 
    </listener>
    如果使用Struts,那么需要在Struts的配置文件struts-config.xml里面配置一个Spring的plugin:ContextLoaderPlugIn。

    实际上ContextLoaderListener和ContextLoaderPlugIn的功能是重叠的,他们都是进行Spring配置的初始化工作的。因此,如果你不打算使用OpenSessionInView,那么你并不需要在web.xml里面配置ContextLoaderListener。

    好了,但是你现在既需要Struts集成Spring,又需要OpenSessionInView模式,问题就来了!

    由于ContextLoaderListener和ContextLoaderPlugIn功能重叠,都是初始化Spring,你不应该进行两次初始化,所以你不应该同时使用这两者,只能选择一个,因为你现在需要集成Struts,所以你只能使用ContextLoaderPlugIn。

    但是令人困惑的是,ContextLoaderListener和ContextLoaderPlugIn有一个非常矛盾的地方!

    ContextLoaderListener初始化spring配置,然后把它放在ServletContext对象里面保存:

    java代码: 

    servletContext.setAttribute(
                                            WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);

    请注意,保存的对象的key是WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE!
    但是ContextLoaderPlugIn初始化spring配置,然后把它放在ServletContext对象里面保存:

    java代码: 


    String attrName = getServletContextAttributeName();
    getServletContext().setAttribute(attrName, wac);

    这个attrName和WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE名字是不一样的!

    如果仅仅是名字不一样,问题还不大,你仍然可以放心使用ContextLoaderPlugIn,但是当你使用OpenSessionInView的时候,OpenSessionInViewFilter是使用哪个key取得spring配置的呢?

    java代码: 

    WebApplicationContext wac =
                                    WebApplicationContextUtils.getRequiredWebApplicationContext(getServletContext());


    显然,OpenSessionInViewFilter是按照WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE这个key去拿spring配置的!

    我们整理一下思路:

    ContextLoaderPlugIn保存spring配置的名字叫做attrName;
    ,ContextLoaderListener保存spring配置的名字叫做WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE;
    而OpenSessionInView是按照WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE这个名字去取得spring配置的!
    而你的应用程序却是按照attrName去取得spring的配置的!

    所以,OpenSessionInView模式失效!

    解决办法:
    修改ContextLoaderPlugIn代码,在getServletContext().setAttribute(attrName, wac);这个地方加上一行代码:
    getServletContext().setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, wac);

    或者修改OpenSessionInViewFilter,让它按照attrName去取得spring配置。



    我原来用struts/spring/hibernate的时候同样使用OpenSessionInView,但是似乎没有robbin所说的问题啊。而且我在使用的时候,是ContextLoaderListener和ContextLoaderPlugIn一起用的。整个配置如下:
    1.首先是web.xml
    java代码: 


            <filter>
            <filter-name>OpenSessionInViewFilter</filter-name>
            <filter-class>org.springframework.orm.hibernate.support.OpenSessionInViewFilter</filter-class>
        </filter>
       
        <filter-mapping>
            <filter-name>OpenSessionInViewFilter</filter-name>
            <url-pattern>/*</url-pattern>
        </filter-mapping>
       
        <listener>
                    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
            </listener>

    ......



    2. 然后是struts-config.xml:
    java代码: 


    <plug-in className="org.springframework.web.struts.ContextLoaderPlugIn">
            <set-property property="contextConfigLocation"
                                      value="/WEB-INF/action-servlet.xml"
            />
    </plug-in>



    其余部分省略。

    在上述配置下,使用OpenSessionInView似乎没有问题。

    不知道robbin所说的ContextLoaderListener和ContextLoaderPlugIn不应该同时使用是不是做得是如下的配置:(struts-config.xml)

    java代码: 


    <plug-in
    className="org.springframework.web.struts.ContextLoaderPlugIn">
    <set-property property="contextConfigLocation"
    value="/WEB-INF/applicationContext.xml,
    /WEB-INF/action-servlet.xml"/>
    </plug-in>



    我尝试了一下,用这种配置时,OpenSessionInView的确失效了。

    我猜想,原因大概是这样:struts的这个plugIn,可能只是为了整合一个action-servlet.xml,将action-servlet.xml中的定义当作Spring的bean来使用,因此,在保存时,只要有action-servlet.xml的配置,就被保存到robbin所提到的那个attrName中,而不是WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE中,所以,OpenSessionInView是取不到这个配置的。

    那么这个配置什么时候被取到呢?直觉告诉我,可能是和Action的Proxy有关。于是,查看了org.springframework.web.struts.DelegatingActionProxy的源码,果然:
    java代码: 


    /**
            * Return the delegate Action for the given mapping.
            * <p>The default implementation determines a bean name from the
            * given ActionMapping and looks up the corresponding bean in the
            * WebApplicationContext.
            * @param mapping the Struts ActionMapping
            * @return the delegate Action
            * @throws BeansException if thrown by WebApplicationContext methods
            * @see #determineActionBeanName
            */

            protectedAction getDelegateAction(ActionMapping mapping)throws BeansException {
                    WebApplicationContext wac = getWebApplicationContext(getServlet(), mapping.getModuleConfig());
                    String beanName = determineActionBeanName(mapping);
                    return(Action) wac.getBean(beanName, Action.class);
            }

            /**
            * Fetch ContextLoaderPlugIn's WebApplicationContext from the
            * ServletContext, containing the Struts Action beans to delegate to.
            * @param actionServlet the associated ActionServlet
            * @param moduleConfig the associated ModuleConfig
            * @return the WebApplicationContext
            * @throws IllegalStateException if no WebApplicationContext could be found
            * @see DelegatingActionUtils#getRequiredWebApplicationContext
            * @see ContextLoaderPlugIn#SERVLET_CONTEXT_PREFIX
            */

            protected WebApplicationContext getWebApplicationContext(
                            ActionServlet actionServlet, ModuleConfig moduleConfig)throwsIllegalStateException{
                    return DelegatingActionUtils.getRequiredWebApplicationContext(actionServlet, moduleConfig);
            }



    仔细看其中的取wac的代码,它并不是从WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE取的wac。

    由此,我相信,除了robbin讲的修改源码以外,同时使用ContextLoaderListener和ContextLoaderPlugIn,但是不要在ContextLoaderPlugIn里面加入applicationContext.xml,只要加入你的action-servlet.xml,我相信,同样也可以非常流畅的使用OpenSessionInView



    我也遇到了上面说的openSessionInView不起作用的问题(web.xml既定义了listener,也定义了struts plugin),我想问一下,上面提到的action-servlet.xml到底是什么内容?
    在我的应用里spring的配置文件是application-context.xml,它本身是空的,引用spring-data.xml,sping-security.xml等等和存放对应struts action的spring 配置文件spring-struts-action.xml。
    struts的配置文件是struts-config.xml,里面定义了所有的action,它们的class都是org.springframework.web.struts.DelegatingActionProxy。最后的plug-in是
    java代码: 


    <plug-in
    className="org.springframework.web.struts.ContextLoaderPlugIn">
    <set-property property="contextConfigLocation"
    value="/WEB-INF/applicationContext.xml"/>
    </plug-in>



    结果也遇到了openSessionInView不起作用的问题
    在我的应用里都没有出现过action-servlet.xml,我想问下它到底是什么?是对应于我的spring-struts-action.xml还是struts-config.xml引用的一部分?

    通俗的说,这个action-servlet.xml到底是spring配置文件还是struts的配置文件?







    我仔细想了一下,那个action-servlet.xml应该是spring配置的一部分,也就是说对应我的spring-struts-action.xml(明确的说,这个里面的xml语法是spring配置文件的),应该是这样的吧?不过按照这个理解下去,我又产生了问题。
    我的理解时这样的,spring里面的listener会在web.xml里加载spring的容器,struts ActionServlet初始化时又会根据struts-config.xml里的spring plugin配置再初始化一个spring容器,所以原则上说只要一个就可以了,如果2处都配了,会初始化2个spring容器,在和struts结合的用法里,实际有效的是stuts配置里面那个plugin初始化的容器,因为用户操作的入口都是struts的action。那么二楼提供的方法其实就是所有的bean都由那个listener初始化的,存在于第一个spring容器中,然后stuts只初始化那些和struts action关联的action bean,存在第二个容器里(这两个容器的区分就在于robbin提到的他们的名字不同)但是问题就是:
    为什么在二楼的的方法中,用户通过action访问spring bean,那么应该只是访问的第二个容器里的action bean,而service bean在第一个容器里,那第二个容器里的action bean是怎么会可以访问到第一个容器里的service bean和其他所有spring bean的呢?实在是费解








    感谢搂主的分析,spring的struts plugin确实有上述描述的问题
    如果根据原来的方法,context会初始化2次,看了plugin的源码以后我对它作了小小的修改,首先检查context是不是被初始化过,如果有则直接从attribute中获取,如果没有初始化,则根据plugin的配置初始化,同时保证了context只被初始化一次。
    原来的意图是屏蔽web.xml中的context监听,直接用plugin初始化context,但启动失败,于是作了上述修改
    java代码: 


    //read the application context from the aplication attribute
                    WebApplicationContext wac = null;
                    String attrName = getServletContextAttributeName();
                    if(getServletContext().getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE)!=null){                       
                            wac = (WebApplicationContext) getServletContext().getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
                            logger.info("Using the context listener's context "+wac);
                    }
                    else {
                            logger.info("Load the context plugin application context ");
                            WebApplicationContext parent = WebApplicationContextUtils.getWebApplicationContext(getServletContext());

                            wac = createWebApplicationContext(parent);
                            if (logger.isInfoEnabled()) {
                                    logger.info("Using context class '
    " + wac.getClass().getName() + "' for servlet '" + getServletName() + "'");
                            }                       
                            //set to attribute to spring listener
                            getServletContext().setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, wac);                
                    }
                   
                    // Publish the context as a servlet context attribute.
                    getServletContext().setAttribute(attrName, wac);



    PS. 有个疑问,如果说spirng中的bean只有一个实例,应该说无论初始化多少次都应该获得的是同一个实例啊?



    strutspugin.rar
     描述:
    根据spirng 1.2.7 重新编译的ContextLoaderPlugin

    下载
     文件名: strutspugin.rar
     文件大小: 4.16 KB
     下载过的: 文件被下载或查看 194 次






    我觉得这个根本就是大家对Spring的理解的问题。

    如果这真是一个严重的问题,以至于需要修改源码来修正,Spring的team不会到现在没有发现,到现在还没有修正。为什么Spring的context分成了多个文件?为什么用applicationContext.xml了,还有xxx-servlet.xml?


    如果大家监听ContextRefreshedEvent的话,会发现一个web app至少会有两个这样的event,下面是我的现在的应用打印出的context及其所包含的beans:
    org.springframework.web.context.support.XmlWebApplicationContext: display name [Root WebApplicationContext]; startup date
    [Wed May 10 17:30:13 CST 2006]; child of [org.springframework.context.support.ClassPathXmlApplicationContext: display name [org.springframework.context.support.ClassPathXmlApplicationContext;hashCode=3736840]; startup date [Wed May 10 17:30:09 CST
    2006]; root of context hierarchy]; config locations
    [/WEB-INF/webApplicationContext.xml,/WEB-INF/webApplicationContext-*.xml,/WEB-INF/standardJspApplicationContext.xml,classpath
    *:config/spring/app-context-base.xml,classpath*:config/spring/app-context-hibernate.xml,classpath*:config/spring/app-context-
    ibatis.xml,classpath*:config/spring/app-context-integration.xml,classpath*:config/spring/app-context-biz.xml]

    [messageSource, localeResolver, exposeSpringBeanDefinition, dataListOfTerminalInfoForm, dataListOfPointsSpecialOfferForm,
    dataListOfSearchTerminalForm, pointsSpecialOfferForm1, terminalInfoForm1, searchTerminalForm1,
    dataListOfSearchCardAccountDetailForm, dataListOfSearchPhysicalCardInfoForm, dataListOfSearchCardApplicationInfoForm,
    dataListOfSearchTransactionForm, searchCardAccountDetailForm1, searchCardAccountDetailForm2, searchCardAccountDetailForm3,
    searchPhysicalCardInfoForm1, searchPhysicalCardInfoForm2, searchPhysicalCardInfoForm3, searchTransactionForm1, searchTransactionForm2, searchTransactionForm3, displayTransactionFormForTest, dataListOfReplaceCardForm, dataListOfSearchCardInfoForm, cardInfoFormForTest, cardAccountForm1, cardAccountForm2, cardAccountForm3, searchCardInfoForm1, searchCardInfoForm2, searchCardInfoForm3, replaceCardForm1, replaceCardForm2, replaceCardForm3,
    searchCardApplicationInfoForm1, searchCardApplicationInfoForm2, searchCardApplicationInfoForm3,
    displayCardApplicationInfoFormForTest, displayCardApplicationInfoFormForTest1, csvDisplayProvider, excelDisplayProvider,
    classicLook, simpleLook, microsoftLook, dacHandler, integer0, integer1, integer2, integer3, integer4, integer5, integer6, integer7, integer8, integer9, sessionFactory, transactionManager, hibernateTemplate, abstractHibernateDao, abstractDacHolderHibernateDao, ageLevelDefinitionDao, auditLogDao, bankDao, bankBranchDao, binRangeDao, cardDao,
    cardAccountDao, cardAccountDetailDao, cardApplicationDao, cardSalesAgentDao, cardTypeDefinitionDao, centerDao,
    centerAccountDao, centerAccountDetailDao, corporationDao, corporationTypeDefinitionDao, csaAccountDao, csaAccountDetailDao, csaBillsDao, csaTypeDefinitionDao, educationLevelDefinitionDao, generalLedgerDao, generalLedgerDetailDao, generalLedgerTypeDefinitionDao, identificationTypeDefinitionDao, incomeLevelDefinitionDao, industryDao, journalDao, journalBackupDao, maritalStatusDefinitionDao, merchantDao, merchantAccountDao, merchantAccountDetailDao, merchantApplicationDao, merchantBillsDao, merchantTypeDefinitionDao, occupationTypeDefinitionDao, outboundEmailDao, permissionDao, physicalCardDao, pointsSpecialOfferDao, residentialTypeDefinitionDao, roleDefinitionDao, rolePermissionDao,
    systemConfigDao, terminalDao, terminalConfigurationDao, terminalModelDefinitionDao, transactionSummaryDao,
    transactionTypeDefinitionDao, userDao, userCreditRatingDao, userLevelDefinitionDao, userRoleDao, sqlMapClient,
    abstractIbatisValueListAdapter, valueListHandler, propertyConfigurer, dataSource, voidTransactionTemplate,
    inquiryBalanceTransactionTemplate, definitionBizFacade, facadeHolder, pointsTransactionTemplate, emailBizObject,
    clsSpringEventListener, balanceBizFacadeTarget, kernelBizObject, printBizFacadeTarget, pointsSpecialOfferBizFacadeTarget,
    settlementBizObject, addPointsTransactionTemplate, inquiryMerchantAccountInfoTransactionTemplate,
    addMerchantPointsTransactionTemplate, emailBizFacadeTarget, centerBizFacadeTarget, monitorBizFacadeTarget,
    endOfDayReportTransactionTemplate, cardBizFacadeTarget, pointsCalculator, balanceBizObject, pointsSpecialOfferBizObject,
    auditLogBizFacadeTarget, terminalBizFacadeTarget, terminalBizObject, templateHolder, settlementBizFacadeTarget,
    merchantBizObject, userBizObject, changePinTransactionTemplate, centerPurchasePointsBackTransactionTemplate,
    definitionBizObject, monitorBizObject, auditLogBizObject, merchantBizFacadeTarget, userBizFacadeTarget,
    responseMessageDataFactoryBean, tradingBizObject, printBizObject, csaBizObject, csaBizFacadeTarget, kernelBizFacadeTarget,
    cardBizObject, centerBizObject, tradingBizFacadeTarget, downloadParametersTransactionTemplate, baseTransactionProxy,
    abstractDataFacade, balanceBizFacade, printBizFacade, settlementBizFacade, emailBizFacade, kernelBizFacade,
    auditLogBizFacade, pointsSpecialOfferBizFacade, terminalBizFacade, userBizFacade, merchantBizFacade, csaBizFacade,
    monitorBizFacade, cardBizFacade, centerBizFacade, tradingBizFacade, pointsConverter, transactionTypeHelperBean, integer100, integer106, integer110, integer111, integer120, integer180, integer181, integer182, integer200, integer220, integer221]



    ---------------------------------------------------------------context2
    org.springframework.web.context.support.XmlWebApplicationContext: display name [WebApplicationContext for namespace
    'action-servlet']; startup date [Wed May 10 17:31:01 CST 2006]; child of
    [org.springframework.web.context.support.XmlWebApplicationContext: display name [Root WebApplicationContext]; startup date
    [Wed May 10 17:30:13 CST 2006]; child of [org.springframework.context.support.ClassPathXmlApplicationContext: display name
    [org.springframework.context.support.ClassPathXmlApplicationContext;hashCode=3736840]; startup date [Wed May 10 17:30:09 CST
    2006]; root of context hierarchy]; config locations
    [/WEB-INF/webApplicationContext.xml,/WEB-INF/webApplicationContext-*.xml,/WEB-INF/standardJspApplicationContext.xml,classpath
    *:config/spring/app-context-base.xml,classpath*:config/spring/app-context-hibernate.xml,classpath*:config/spring/app-context-
    ibatis.xml,classpath*:config/spring/app-context-integration.xml,classpath*:config/spring/app-context-biz.xml]]; config
    locations [/WEB-INF/action-servlet.xml]

    [/EditCurrentUserInfoAction, /FakeLoginAction, /SaveCardInfoAction, /LoginAction, /EditOperatorPswAction,
    /SavePointsSpecialOfferAction, /DisplayCurrentUserInfoAction, /DisplayOperatorApplicationAction, /EditEmailAction,
    /RegisterCardAction, /IssueCardsAction, /SearchEmailAction, /RegisterCsaAction, /EditTerminalInfoAction,
    /ReleaseTerminalAction, /ReleaseCsaAction, /SaveTerminalInfoAction, /SearchTransactionAction, /SearchOperatorInfoAction,
    /ProcessCardApplicationAction, /EditTerminalConfigurationAction, /EditGenericUserByIdAction, /SearchMerchantInfoAction,
    /SearchTerminalAction, /SaveCsaPswAction, /SaveCsaInfoAction, /SaveCardTypeAction, /RegisterPointsSpecialOfferAction,
    /SearchCardInfoAction, /EditMerchantInfoAction, /SearchCardAccountDetailAction, /SearchCardApplicationInfoAction,
    /DisplayTransactionStatisticsAction, /DisplayRegisterCardInfoAction, /SaveEmailAction, /EditCardInfoByIdAction,
    /MoniterSystemLogAction, /ReleasePointsSpecialOfferAction, /SearchMerchantAccountDetailAction, /EditMerchantPswAction,
    /ReleaseMerchantAction, /ListCardTypeAction, /StockCardsAction, /ProcessOperatorApplicationAction,
    /SearchPhysicalCardInfoAction, /SearchCsaInfoAction, /SearchOperatorApplicationAction, /ReleaseOperatorAction,
    /DisplayCardApplicationInfoAction, searchEmailValueListBuilder, /SearchCsaApplicationAction, /RegisterCardTypeAction,
    /MonitorTerminalStatusAction, /SearchCsaAccountDetailAction, /SearchUserAction, /ReleaseCardTypeAction, /ReleaseUserAction,
    /ReleaseEmailAction, /CreateBlankCardsAction, /RegisterBulkCardsAction, /SaveMerchantPswAction,
    /SearchPointsSpecialOfferAction, /EditPswAction, /SearchMerchantApplicationAction, /DisplayCsaApplicationAction,
    /EndOfDayAction, /EditPointsSpecialOfferAction, /DisplayMerchantApplicationAction, /RegisterEmailAction, /EditCsaPswAction,
    /ProcessMerchantApplicationAction, /EditCardTypeDefinitionAction, /SaveOperatorInfoAction, /SaveMerchantInfoAction,
    /SaveOperatorPswAction, /EditOperatorInfoAction, /RegisterMerchantAction, /EditCsaInfoAction, /ChangeSystemStatusAction,
    /ProcessSystemLogAction, /RegisterOperatorAction, /RegisterTerminalAction, /DisplayTransanctionAction,
    /ProcessCsaApplicationAction, /MerchantPointsRedeemTransactionReportAction, /SaveTerminalConfigurationAction,
    autoProxyCreator, profilingAdvice, profilingAdvisor, strutsActionAdvice, baseSearchAction, userTypeBasedSelector,
    valueListBuilder]



    ------------------------------------------------------------------------context3
    org.springframework.web.context.support.XmlWebApplicationContext: display name [WebApplicationContext for namespace
    'trading-servlet']; startup date [Wed May 10 17:31:08 CST 2006]; child of
    [org.springframework.web.context.support.XmlWebApplicationContext: display name [Root WebApplicationContext]; startup date
    [Wed May 10 17:30:13 CST 2006]; child of [org.springframework.context.support.ClassPathXmlApplicationContext: display name
    [org.springframework.context.support.ClassPathXmlApplicationContext;hashCode=3736840]; startup date [Wed May 10 17:30:09 CST
    2006]; root of context hierarchy]; config locations
    [/WEB-INF/webApplicationContext.xml,/WEB-INF/webApplicationContext-*.xml,/WEB-INF/standardJspApplicationContext.xml,classpath
    *:config/spring/app-context-base.xml,classpath*:config/spring/app-context-hibernate.xml,classpath*:config/spring/app-context-
    ibatis.xml,classpath*:config/spring/app-context-integration.xml,classpath*:config/spring/app-context-biz.xml]]; config locations [/WEB-INF/trading-servlet.xml]
    [clsRawTagElementParser, transactionProcessor, clsTradingServlet, rawTagElementParser]



    如果照大家所说的方法去改源代码,那么后启动的servlet的context会覆盖前面一个启动的servlet的context,对于我的应用来说,那种方式会导致action-servlet丢失。开始robbin提出的错误,是因为你在strutsPlugin里多配置了appContext,导致实际上有2分appContext的beans存在,child在自己的context里就可以找到所需要的bean,也就不会去parent里找了。StrutsPlugin里的attrName是正确合理的。

    当然你可以把所有所有的bean全部放到root context里,这也行的通,不过本人极力反对这种方式,bean的组织太乱。

    Spring的context是分层次的:不要把在写contextConfigLocation的时候,把你的xxx-servlet.xml路径也加进去;不要在写xxx-servlet.xml的context的时候把applicationContext的路径也配进去;不要在parent的context里引用children里的bean,不要在你的appContext里引用xxx-servlet的bean。

    总之,就是要求你合理的、有层次的组织你的bena,而不是一陀摆出来。





    applicationContext.xml如果不引用action-servlet.xml路径的话,那么action如何来引用bo;
    java代码: 


    <bean name="/test" class="com.xy.action.TestAction">
      <property name="testBo"><ref bean="testBoProxy"/></property>
    </bean>



    如果bo在applicationContext.xml中的话;
    服务器会报错,找不到bo





    okokok 写道:
    applicationContext.xml如果不引用action-servlet.xml路径的话,那么action如何来引用bo;
    java代码: 


    <bean name="/test" class="com.xy.action.TestAction">
      <property name="testBo"><ref bean="testBoProxy"/></property>
    </bean>



    如果bo在applicationContext.xml中的话;
    服务器会报错,找不到bo


    我不太清楚你的bean的组织,在我的系统里,BO是在applicationContext之类的基础context里定义,而且工作很正常。

    另外你需要搞清楚的是:对于Spring的BeanFactory(ApplicationContext),如果它在自己的context里找不到bean,会去parent里找。

    java代码: 


    // Check if bean definition exists in this factory.
                            if(getParentBeanFactory() != null && !containsBeanDefinition(beanName)){
                                    // Not found -> check parent.
                                    if(getParentBeanFactory() instanceof AbstractBeanFactory){
                                            // Delegation to parent with args only possible for AbstractBeanFactory.
                                            return((AbstractBeanFactory) getParentBeanFactory()).getBean(name, requiredType, args);
                                    }
                                    elseif(args == null){
                                            // No args -> delegate to standard getBean method.
                                            return getParentBeanFactory().getBean(name, requiredType);
                                    }
                                    else{
                                            throw new NoSuchBeanDefinitionException(beanName,
                                                            "Cannot delegate to parent BeanFactory because it does not supported passed-in arguments");
                                    }
                            }



    所以无论如何,只要你在applicationContext里定义了BO,那么webApp的context一定找得到这个bean,因为applicationContext是webApp的context的parent。






    奇了怪了,昨天一直报找不到bo的错,今天居然没报错;服务器有问题?
    还有个问题,既然web.xml里可以用listener来加载applicationContext.xml,为什么还要在struts-config.xml里再用plug-in?我觉得在applicationContext.xml里按模块放置每个模块的action,bo,dao的xml文件的路径是个不错方法,比如:
    java代码: 


    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
    <beans>
            <import resource="SpringConfig/module1.xml" />
                    <import resource="SpringConfig/module2.xml" />
                    <import resource="SpringConfig/module3.xml" />

            <bean id="dataSource"
                    class="org.apache.commons.dbcp.BasicDataSource">
                    <property name="driverClassName">
                            <value>org.gjt.mm.mysql.Driver</value>
                    </property>
                    <property name="url">
                            <value>jdbc:mysql://localhost/airline</value>
                    </property>
                    <property name="username">
                            <value>root</value>
                    </property>
                    <property name="password">
                            <value>123456</value>
                    </property>
            </bean>
             ...
    </beans>


    而module1.xml的内容是:

    java代码: 


    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
    <beans>
    <!-- 数据访问层 -->
    <bean id="testDao" class="com.xy.dao.TestDao">
      <property name="sessionFactory"><ref bean="sessionFactory"/></property>
    </bean>
    <!-- 业务罗基层 -->
    <bean id="testBo" class="com.xy.bo.TestBo">
      <property name="testDao"><ref bean="testDao"/></property>
      <!-- <property name="transactionManager"><ref bean="transactionManager"/></property> -->
    </bean>
    <!-- Action层 -->
    <bean name="/test" class="com.xy.action.TestAction">
      <property name="testBo"><ref bean="testBoProxy"/></property>
    </bean>
    </beans>


    这样的话不用在struts-config.xml里配置plug-in了吧





    struts-config.xml里配置plug-in是要配的,关键在于你Spring的配置文件的合理分层,如果像你那样什么东西都放在一个applictionContext里,那么就有可能出现象这样OpenSessioInView失效的这样“意想不到”的问题。而且会导致你错误地理解Spring,比如Spring的ApplicationEvent,本身web层的context里的Listener是听不到root层的Event的,但是你这样的配置(也是这个topic的配置)会导致Event混淆,也违背了Spring本身的设计意图。

    PS:关于Event的问题可以看我blog,http://spaces.msn.com/sweetriver/blog/cns!367370EB9A9B2807!129.entry

    在我的配置里,各个层次都有属于自己的配置文件,messageSource同样应该有分层,为了图方便而简单吧所有bean罗列在一个配置里是不可取的,而且会导致某些设计与实现上的问题(开始我的messageSource是没有分层的,但是后来这样导致了一些非常痛苦的问题与抉择,结果还是改回分层的messageSource)。





    applicationContext是有层次的,那样的方式会让struts plugIn中配置的sessionFactory比root中的sessionFactory占优,这个struts plugIn中的bean都是访问这里配置的sessionFactory。



    opensession的最大问题还是长链接的时候无法释放session的问题,在对外系统中问题尤为突出。springMVC可以仗着先天优势用interecptor,但是webwork可就没有这样的优势了,尝试自己做一个适合webwork的,但是发现一旦有安全框架介入,session作用的切面就变得难以把握。到现在还无果~~~~
    posted @ 2006-08-01 14:31 rendong 阅读(1244) | 评论 (0)编辑 收藏
     
    组合公式为 "((r1c1+r1c2)/r1c3)"
            1. 组合的公式是一个字符串.
            2. 字符串元素包括"rc0123456789+-*/()"
            3. r1c1=7,r1c2=2,r1c3=4
            4. 求组合成的公式的值是多少.

    解决思路:
            1.对公式进行字符合法性检查.
            2.对公式中出现的变量进行取值替换.
            3.检查语句的合法性.(组合规则)
            4.检查运算的合法性.(除数为0的情况)
            5.求值.


    具体解决方法:
            str1 = "(r1c1+r1c2)/r1c3)"
            str1 = replace(str1," ","")  '去除公式中间出现的空格
            1.  对公式进行字符合法性检查.

                  bool1 = getDataOperationCharCheck(str1)

                  if bool1 = false then
                          Response.write "公式不合法.有异常字符出现.<br>"
                  else
                          Response.write "公式检查通过!<br>"
                  end if

            2.对公式中出现的变量进行取值替换

                    RCstr = getdataoperation(str1)

            3.检查语句的合法性.(组合规则)

                    bool2 = getDataOperationSyntaxCheck(RCstr)

                          if bool2 = false then
                                     Response.write "运算公式语法不合法,有组合异常字符出现.<br>"
                          else
                                     Response.Write "运算公式语法检查通过!<br>"
                          end if

              4.检查运算的合法性.(除数为0的情况)

                       bool3 = getDataOperationRunCheck(RCstr)

                            if bool3 = false then
                                    Response.write "运算公式运算不合法,有除数为0出现.<br>"
                            else
                                    Response.write "运算公式运算合法性检查通过!<br>"  
                            end if

               5.求值.
     
                            intValue = getRunSearch(RCstr)

               6.运算结果:

                            (((7*1.0)+(2*1.0))/(4*1.0)) = 2.25

    '1.=============================================================

    '对原始公式进行字符检查,是否有不合法的字符

    function getDataOperationCharCheck(datastr)
           datastr = replace(datastr," ","")
            sumchar = "rc0123456789+-*/()"
            strlen = len(datastr)
            strreturn = true
            for i = 1 to strlen
                    singlechar = mid(datastr,i,1)
                    if instr(sumchar,singlechar) = 0 then
                            strreturn = false
                            exit for
                    end if
            next
            getDataOperationCharCheck = strreturn
    end function

    '2.==============================================================

    '对原始计划公式进行取值替换.
    '实现方法:对原始字符串进行单个字符检查,
    '在出现 "+-*/()" 中的任何一个字符时,对已经组合生成的变量进行取值.
    '在取到值的同时对其进行 double 转换 ,实现方法是 (intvalue * 1.0)

    function getdataoperation(datastr)
            strlen = len(datastr)  
            sunstr = ""
            strID = ""
            intvalue = 0
            singlechar = ""
            operationstr="()+-*/"
            for i=1 to strlen
                    'Response.write mid(datastr,i,1) & "<br>" 
                    singlechar = mid(datastr,i,1)
                    if instr(operationstr,singlechar) > 0 then
       
                            if strID <> "" then
                                    intvalue = getValue(strID)
                                    sunstr = sunstr & "(" & intvalue & "*1.0)"   '(1)     
                                    intvalue = 0
                                    strID = ""          
                            end if
                            sunstr = sunstr & singlechar
                            singlechar = ""
                    else
                            strID = strID & singlechar
                    end if     
            next
            getdataoperation = sunstr
    end function

    '变量取值函数.
    '下列数据是为测试使用.
    '
    function getValue(strRC)
            select case strRC
                    case "r1c1"
                            getValue = 2
                    case "r1c2"
                            getValue = 7
                    case "r1c3"
                            getValue = 2
                   end select
    end function

    '3.==============================================================

    '对公式进行语法合法性检查.
    'eg.检查 (),--,++,**,//,(/,*) 等 是否成对出现.
    '检查是否有


    function getDataOperationSyntaxCheck(datastr)
            strreturn = true
            datastr = replace(datastr," ","")  '去除所有的空格
            strlen = len(datastr)
            num1 = 0                           '记录 括号的 个数  采用 有 (  加1, 有 ) 减1
            upsinglechar = ""                  '相对本次的字符的上一个字符
            singlechar = ""
            operationstr1="()+-*/"
            operationstr2="(+-*/"              '相对 在 (  左边出现的运的符号是正确的.
     
            for i = 1 to strlen
                    singlechar = mid(datastr,i,1)
                    select case singlechar
                            case "("
                                    num1 = num1 + 1
                                    if upsinglechar <> "" then
                                            if instr(operationstr2,upsinglechar) = 0 then   '在左括号的左边若不为空,必需出现 "(+-*/" 中的一个.
                                                    strreturn = false
                                                    exit for
                                            end if
                                    end if
       
                            case ")"
                                    num1 = num1 - 1
                                    if num1 < 0 then
                                            strreturn = false
                                            exit for
                                    end if
                                    if instr(operationstr2,upsinglechar) > 0 then       '在右括号的左边若不为空,必需不能出现 "(+-*/" 中的一个
                                            strreturn = false
                                            exit for
                                    end if
       
                            case "+"
                                    if instr(operationstr2,upsinglechar) > 0 then       '在加号的左边若不空,必需不能出现 "(+-*/" 中的一个
                                            strreturn = false
                                            exit for
                                    end if
       
                            case "-"
                                     if instr(operationstr2,upsinglechar) > 0 then      '在减号的左边若不空,必需不能出现 "(+-*/" 中的一个
                                            strreturn = false
                                            exit for
                                    end if
                            case "*"
                                     if instr(operationstr2,upsinglechar) > 0 then      '在乘号的左边若不空,必需不能出现 "(+-*/" 中的一个
                                            strreturn = false
                                            exit for
                                    end if
                            case "/"
                                     if instr(operationstr2,upsinglechar) > 0 then      '在除号的左边若不空,必需不能出现 "(+-*/" 中的一个
                                            strreturn = false
                                            exit for
                                    end if
                    end select
                    upsinglechar = singlechar  
                    singlechar = ""
            next
            getDataOperationSyntaxCheck = strreturn
    end function

    '4.==============================================================
     
    '对组合公式进行运算合法性的检查
    '首选查找有没有 "/0"出现.
    '其次查找类似 "/(****)" = /0 出现

    function getDataOperationRunCheck(datastr)
            strreturn = true
            if instr(datastr,"/")>0 then
                    if instr(datastr,"/0") > 0 then    
                            strreturn = false
                    else

                            '对/ 后面出现的()内的数据进行运算,取值是否会有0出现.
                            '首先计算 "/" 出现的次数
                            '再次判断 "/(" 出现的次数
                            '若 "/(" 出现的次为0 则安全.

                            intnum1 = getInstrNum(datastr,"/")
                            intnum2 = getInstrNum(datastr,"/(")
                            if intnum2 > 0 then
                                    for j = intnum2 to 1 step -1
                                            intpoint1 = getInstrPoint(datastr,"/(",j)
                                            if intpoint1 > 0 then
                                                    sumpoint = getRunCheck(datastr,intpoint1)
                                                    if  CDbl(sumpoint) = CDbl(0) then
                                                            strreturn = false
                                                            exit for
                                                    end if
                                            end if
                                    next     
                            end if
                    end if  
            end if
            getDataOperationRunCheck= strreturn
    end function


    '检查字符运行的合法性.
    '主要是对/()出现的字公式进行计算是否会等于0

    function getRunCheck(datastr,intpoint1)
            strlen = len(datastr)
            intpoint = intpoint1 + 1
            intnum = 0
            singlechar = ""
            rcsearch = ""
            intreturn = 0
            for m = intpoint to strlen
                    singlechar = mid(datastr,m,1)
                    if singlechar = "(" then
                            intnum = intnum + 1
                    end if
                    if singlechar = ")" then
                            intnum = intnum - 1
                    end if
                    rcsearch = rcsearch & singlechar
                    if intnum = 0 then    
                            intreturn  = getRunSearch(rcsearch)
                            exit for
                    end if   
            next
            getRunCheck = intreturn
    end function

    '5.==============================================================

    '求值.
    function getRunSearch(strrcsearch)
             sql = "select " & strrcsearch & " as rcvalue "
             Set rs = conn.execute(sql)
             'Response.write "<br>" & strrcsearch & "=" & rs("rcvalue") & "<br>"
             getRunSearch = rs("rcvalue")
    end function

    '公共函数==============================================================

    '返回substr  在 str1 中出现的次数

    function getInstrNum(str1,substr)
            strlen = len(str1)
            substrlen = len(substr)
            singlechar = ""
            intreturn = 0
            for i = 1 to strlen
                    singlechar = mid(str1,i,substrlen)
                    if singlechar = substr then
                            intreturn = intreturn + 1
                    end if
            next
            getInstrNum = intreturn
    end function

    '返回substr 在 str1 中 第 intnum 次出现的位置
    'intnum 必需是大于0的正整数

    function getInstrPoint(str1,substr,intnum)
            intreturn = 0
            strlen = len(str1)
            substrlen = len(substr)
            singlechar = ""
            intcount = 0
            for i = 1 to strlen
                    singlechar = mid(str1,i,substrlen)
                    if singlechar = substr then
                            intcount = intcount + 1
                    end if
                    if intcount = intnum then
                            intreturn = i
                            exit for
                    end if
            next
            getInstrPoint = intreturn
    end function

    posted @ 2006-08-01 13:56 rendong 阅读(363) | 评论 (0)编辑 收藏
     
    Spring的轻量级的bean容器为业务对象(business objects)、DAO对象和资源(如:JDBC数据源或者Hibernate SessionFactorie等)对象提供了IoC类型的装配能力。Spring使用一个xml格式的应用配置文件为开发者提供了一种通过解析定制的属性文件来手动管理单实例对象或者工厂对象的选择性。由于Spring将非入侵性做为一个重要的目标,因此可以由Spring配置管理的bean对象均不需要依赖Spring自有的接口和类就可以通过它们的bean属性完成配置。这个概念可以被应用到任何环境中,无论你开发的是一个J2EE的web应用还是一个桌面应用甚至只是一个applet都可以。

        在使用Hibernate的应用中, Spring的对DAO对象通常的事务管理特别应该引起关注。它的目的就是分离数据访问和事务处理,使事务性业务对象不与任何特殊的数据访问或者事务策略绑在一起,从而不影响业务对象的可复用性。这种划分既可以经由事务模板(TransactionTemplate)用编程的方式实现,也可以经由面向方面(AOP)事务拦截器(TransactionTemplate)用声明的方式实现。无论是本地的Hibernate / JDBC事务,还是JTA事务都支持对象外的事务策略,这对于本地的无状态会话Bean(Stateless Session Beans)是一个非常有用的选择。

        Spring的HibernateTemplate类提供了一个简单的方式实现了Hibernate-based DAO对象而不必关心如何获得Hibernate的Session实例,也不必关心多方参与的事务处理。无需使用try-catch块,也无需进行事务检查。一个简单的Hibernate访问方法就完全解决了些麻烦! 无论是在多个DAO接口还是在多方事务的情况下,Spring使得多种DAO对象无缝地协同工作。例如:某些DAO对象可能是基于plain JDBC的实现,更适合于经由Spring的JdbcTemplate来避免手动的异常处理。

          你可以单独地使用许多Spring特性,因为Spring的所有对象都是设计成可复用的JavaBean对象的集合。也不要因为Spring可以提供一个完整的应该框架而气馁!使用其他的Spring特性时,应用配置概念是一个附加的特性,并不是一个必须的特性。无论如何,当你要决定去构建一个象Spring这样的内在的基础架构的时候,在使用Spring的路途上没有什么范围上的限制。

    1. 介绍: 资源管理

           典型的业务应用系统常常由于重复的资源管理代码而导致混乱。许多项目试着用自己的方法来解决这个问题,有时要为此付出失败的代价,Spring针对适当的资源管理提倡了一种引人注目的简单方法:即经由模板来倒置控制(Inversion of control),例如:基础类使用回调接口,或者应用AOP拦截器。其基础核心是适当的资源处理和将特殊的API异常转换为一个unchecked的基础异常。

           Spring引入了一个DAO异常层适用于任何数据访问策略。对于直接的JDBC,JdbcTemplate类关注于连接处理,并且关注于对SQLException转换为适当的DataAccessException,包括对特殊的数据库SQL错误转换为有意义的异常。 经由不同的事务管理对象,Spring支持JTA和JDBC事务。Spring 也提供对Hibernate和JDO的支持,它的这种支持由与JdbcTemplate类的作用相类似的HibernateTemplate类和JdoTemplate类, 以及HibernateInterceptor类、JdoInterceptor类,还有Hibernate、JDO 事务管理类组成。

           最主要的目的是要使应用的层次分明,为此将数据访问和事务处理同应用对象分离开来。所有的业务对象都不再依赖数据访问或者事务策略。不再有硬编码的资源查找代码,不再有难以替换的单例对象,也不再需要定制服务注册。

          所有的单独的数据访问特性均无需依赖于Spring,可以单独使用,无需让Spring知道,同时也可以通过Spring的应用配置(提供基于XML的配置和对普通JavaBean实例的交叉引用)来进行装配。在一个典型的Spring应用中,大部分重要的对象都是普通的JavaBean:数据访问模板对象(data access templates)、数据访问对象(使用数据访问模板对象的对象)、事务管理对象及业务对象(使用数据访问对象和事务对象的对象),web表示分解对象、web控制对象(使用业务对象的对象)等等。

    2. 应用配置中的资源定义

        为了避免应用对象将资源查找的代码硬编码,Spring允许在应用配置中将一个如JDBC DataSource或者Hibernate SessionFactory定义为一个Bean。应用对象如果需要访问资源只需要通过Bean引用(DAO定义在下一部分说明)接受先前定义的实例的引用。以下的内容引用自一个应用配置定义,显示了如何建立一个JDBC DataSource和一个Hibernate的SessionFactory:

     <beans>

    <bean id="myDataSource" class="org.springframework.jndi

    .JndiObjectFactoryBean">
    <property name="jndiName">
    <value>jdbc/myds</value>
    </property>
    </bean>

    <bean id="mySessionFactory" class="org.springframework.orm.hibernate

    .LocalSessionFactoryBean">
    <property name="mappingResources">
    <list>
    <value>product.hbm.xml</value>
    </list>
    </property>
    <property name="hibernateProperties">
    <props>
    <prop key="hibernate.dialect">net.sf.hibernate.dialect

    .MySQLDialect</prop>
    </props>
    </property>
    <property name="dataSource">
    <ref bean="myDataSource"/>
    </property>
    </bean>

    ...

    </beans>

          注意选择是用JNDI来定位数据源还是从一个象Jakarta Commons DBCP BasicDataSource这样的本地定义取得一个数据源,只是一个改变配置的事:

    <bean id="myDataSource"
          class="org.apache.commons

    .dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName">
    <value>org.hsqldb.jdbcDriver</value>
    </property>
    <property name="url">
    <value>jdbc:hsqldb:hsql://localhost:9001</value>
    </property>
    <property name="username">
    <value>sa</value>
    </property>
    <property name="password">
    <value></value>
    </property>
    </bean>

           你也可以使用一个JNDI查找SessionFactory,但是通常对于EJB环境之外的应用来说并不是需要的(参考"container resources vs local resources"部分的讨论)。

    3. 倒置控制(Inversion of Control): 模板和回调

        模板的基本编程模式就象你将在下面看到那样,至于方法就如同任何定制的数据访问对象或者业务的对象的方法一样。除了需要向其提供一个Hibernate的SessionFactory之外,再没有对周围执行对象的信赖的限制。虽然最好是从一个Spring的应用配置中经由一个简单setSessionFactory bean的属性设置使用Bean引用来获得它,但随后你可以从任何地方获得它。随后的引用片段包括一段在Spring应用配置中对DAO定义的配置,其中引用了在其前面定义的SessionFactory,和一段DAO方法的实现的例子。

    <beans>
    
    <bean id="myProductDao" class="product.ProductDaoImpl">
    <property name="sessionFactory">
    <ref bean="mySessionFactory"/>
    </property>
    </bean>
    
    ...
    
    </beans>

    public class ProductDaoImpl implements ProductDao {
    
    private SessionFactory sessionFactory;
    
    public void setSessionFactory(SessionFactory 
       sessionFactory) {
    this.sessionFactory = sessionFactory;
    }
    
    public List loadProductsByCategory(final String 
      category) {
    HibernateTemplate hibernateTemplate =
    new HibernateTemplate(this.sessionFactory);
    
    return (List) hibernateTemplate.execute(
    new HibernateCallback() {
    public Object doInHibernate(Session session) throws
     HibernateException {
    List result = session.find(
    "from test.Product product where product.category=?",
    category, Hibernate.STRING);
    // do some further stuff with the result list
    return result;
    }
    }
    );
    }
    }

           一个回调的实现可以被有效地用在任何Hibernate数据访问中。在任何情况下都由HibernateTemplate来管理Session的开闭和自动的多方事务。模板实例是线程安全和可重用的,因此它们可以做为其他类的变量。


           对于简单的单步的动作,象find, load, saveOrUpdate或者delete的调用,HibernateTemplate提供更为便利的选择以代替象一行的回调的执行。此外,Spring提供了一个方便的基本类,就是HibernateDaoSupport类,它提供了setSessionFactory方法来接受一个SessionFactory,同时提供了getSessionFactory和getHibernateTemplate方法供其继承类使用。将这些结合起来,允许对于典型的需求给出了非常简单的DAO实现:

    public class ProductDaoImpl extends HibernateDaoSupport implements
     ProductDao {
    
    public List loadProductsByCategory(String category) {
    return getHibernateTemplate().find(
    "from test.Product product where product.category=?", category,
    Hibernate.STRING);
    }
    }

    4. 应用一个AOP拦截器代替一个模板

       除使用HibernateTemplate之外的另一个选择就是使用Spring的AOP HibernateInterceptor。用直接在一个委托的try/catch块中编写Hibernate代码,配合相应的在应用配置中分别的拦截器配置来代替执行回调。下面的片段显示了一个Spring应用配置中的DAO, interceptor和proxy的各自的定义,同时给出了一个DAO方法实现的例子:

    <beans>
    
    ...
    
    <bean id="myHibernateInterceptor"
          class="org.springframework.orm.hibernate
    .HibernateInterceptor">
    <property name="sessionFactory">
    <ref bean="mySessionFactory"/>
    </property>
    </bean>
    
    <bean id="myProductDaoTarget" class="product.ProductDaoImpl">
    <property name="sessionFactory">
    <ref bean="mySessionFactory"/>
    </property>
    </bean>
    
    <bean id="myProductDao" class="org.springframework.aop
    .framework.ProxyFactoryBean">
    <property name="proxyInterfaces">
    <value>product.ProductDao</value>
    </property>
    <property name="interceptorNames">
    <list>
    <value>myHibernateInterceptor</value>
    <value>myProductDaoTarget</value>
    </list>
    </property>
    </bean>
    
    ...
    
    </beans>

     
    public class ProductDaoImpl extends HibernateDaoSupport 
       implements ProductDao {
    
    public List loadProductsByCategory(final String category)
     throws MyException {
    Session session = SessionFactoryUtils
    .getSession(getSessionFactory(), false);
    try {
    List result = session.find(
    "from test.Product product where product.category=?",
    category, Hibernate.STRING);
    if (result == null) {
    throw new MyException("invalid search result");
    }
    return result;
    }
    catch (HibernateException ex) {
    throw SessionFactoryUtils.convertHibernateAccessException(ex);
    }
    }
    }
    

            这个方法将只在有一个与它配合的HibernateInterceptor时才能正常工作,HibernateInterceptor为它负责在方法调用前线程绑定Session的开启和方法调用后的关闭。getSession方法调用中的"false"标志是要确认Session必须是已经存在的,如果没有发现任何一个Session,SessionFactoryUtils将会为其创建一个。如果已经有一个Session句柄绑定在本线程上,比如是由一个HibernateTransactionManager事务绑定的,在任何情况下SessionFactoryUtils会自动接入这个Session。HibernateTemplate在底层也使用SessionFactoryUtils,与以上说的方式基本是一样的。

           HibernateInterceptor的主要益处是它允许在数据访问代码中抛出checked application exception,而HibernateTemplate由于受限于回调只能在其中抛出unchecked exceptions。注意到这点我们可以推迟各自的检验,同时在回调后抛出应用异常。拦截方式的主要缺点是它需要在配置中进行特殊的配置。HibernateTemplate在大多数情况下都是一种简单好用的方法。

    5. 程序事务划分

       在这种底层的数据访问服务之上,事务处理可以在更高的应用层被划分 ,形成一些操作。这里除了需要一个Spring的PlatformTransactionManager对象外,对于周围运行的业务对象也没有任何限制。同样的,其后你可以从任何地方获得它们,但是经由Bean引用的方式通过setTransactionManage方法获得更为适合,象productDAO要经由一个setProductDao方法获得一样。下面的引用片段显示了在一个Spring应用配置中的事务管理对象和业务对象的定义,并且还提供了一个业务方法实现的例子:

    <beans>
    
    ...
    
    <bean id="myTransactionManager"
          class="org.springframework.orm.hibernate
    .HibernateTransactionManager">
    <property name="sessionFactory">
    <ref bean="mySessionFactory"/>
    </property>
    </bean>
    
    <bean id="myProductService" class="product.ProductServiceImpl">
    <property name="transactionManager">
    <ref bean="myTransactionManager"/>
    </property>
    <property name="productDao">
    <ref bean="myProductDao"/>
    </property>
    </bean>
    
    </beans>

     
    public class ProductServiceImpl implements ProductService {
    
    private PlatformTransactionManager transactionManager;
    private ProductDao productDao;
    
    public void setTransactionManager(PlatformTransactionManager 
      transactionManager) {
    this.transactionManager = transactionManager;
    }
    
    public void setProductDao(ProductDao productDao) {
    this.productDao = productDao;
    }
    
    public void increasePriceOfAllProductsInCategory(final String 
      category) {
    TransactionTemplate transactionTemplate =
    new TransactionTemplate(this.transactionManager);
    transactionTemplate.setPropagationBehavior(TransactionDefinition
        .PROPAGATION_REQUIRED);
    transactionTemplate.execute(
    new TransactionCallbackWithoutResult() {
    public void doInTransactionWithoutResult(TransactionStatus 
      status) {
    List productsToChange = productDAO.loadProductsByCategory(category);
    ...
    }
    }
    );
    }
    }

    6. 声明性事务划分

           我们还可以选择使用Spring的AOP TransactionInterceptor通过在应用配置中定义拦截器配置来代替事务划分代码的事务处理方式。这允许我们保持业务对象独立于每个业务对象中重复的事务划分代码。此外,事务行为和隔离层次的变化可以通过一个配置文件来改变而不需要对业务对象的实现造成影响。

    <beans>
    
    ...
    
    <bean id="myTransactionManager"
          class="org.springframework.orm.hibernate
    .HibernateTransactionManager">
    <property name="sessionFactory">
    <ref bean="mySessionFactory"/>
    </property>
    </bean>
    
    <bean id="myTransactionInterceptor"
          class="org.springframework.transaction
    .interceptor.TransactionInterceptor">
    <property name="transactionManager">
    <ref bean="myTransactionManager"/>
    </property>
    <property name="transactionAttributeSource">
    <value>
    product.ProductService.increasePrice*=PROPAGATION_REQUIRED
    product.ProductService.someOtherBusinessMethod=
    PROPAGATION_MANDATORY
    </value>
    </property>
    </bean>
    
    <bean id="myProductServiceTarget" class="product
    .ProductServiceImpl">
    <property name="productDao">
    <ref bean="myProductDao"/>
    </property>
    </bean>
    
    <bean id="myProductService" class="org.springframework.aop
    .framework.ProxyFactoryBean">
    <property name="proxyInterfaces">
    <value>product.ProductService</value>
    </property>
    <property name="interceptorNames">
    <list>
    <value>myTransactionInterceptor</value>
    <value>myProductServiceTarget</value>
    </list>
    </property>
    </bean>
    
    </beans>

     
    public class ProductServiceImpl implements ProductService {
    
    private ProductDao productDao;
    
    public void setProductDao(ProductDao productDao) {
    this.productDao = productDao;
    }
    
    public void increasePriceOfAllProductsInCategory(final String
     category) {
    List productsToChange = this.productDAO
    .loadProductsByCategory(category);
    ...
    }
    }

          如同使用HibernateInterceptor一样,TransactionInterceptor允许任何checked application exception从回调代码中抛出,而TransactionTemplate受回调限制在其内部抛出unchecked exceptions,在出现一个unchecked application exception的情况时,TransactionTemplate将引发一个回滚或者这个事务由应用(通过事务状态)标记为回滚。TransactionInterceptor默认情况也是同样的行为,但是允许为每一个方法制定回滚策略。
           建立声明性事务的一个便利的方式是使用TransactionProxyFactoryBean,特别是如果没有其他AOP拦截器的话,TransactionProxyFactoryBean将联合定义为代理的自身与一个特殊的目标Bean的事务配置。这将减少一个代理Bean对应一个目标Bean的配置情况。此外,你不必指定哪个接口或者哪个类必须定义事务方法。

    <beans>
    
    ...
    
    <bean id="myTransactionManager"
          class="org.springframework.orm.hibernate
    .HibernateTransactionManager">
    <property name="sessionFactory">
    <ref bean="mySessionFactory"/>
    </property>
    </bean>
    
    <bean id="myProductServiceTarget" class="product
    .ProductServiceImpl">
    <property name="productDao">
    <ref bean="myProductDao"/>
    </property>
    </bean>
    
    <bean id="myProductService"
          class="org.springframework.transaction.interceptor
    .TransactionProxyFactoryBean">
    <property name="transactionManager">
    <ref bean="myTransactionManager"/>
    </property>
    <property name="target">
    <ref bean="myProductServiceTarget"/>
    </property>
    <property name="transactionAttributes">
    <props>
    <prop key="increasePrice*">PROPAGATION_REQUIRED</prop>
    <prop key="someOtherBusinessMethod">PROPAGATION_MANDATORY</prop>
    </props>
    </property>
    </bean>
    
    </beans>
    

    7. 事务管理策略

              对于Hibernate应用来说,无论是TransactionTemplate还是TransactionInterceptor都是委托验实际的事务处理给PlatformTransactionManager实例,可以是一个HibernateTransactionManager(由一个单一的Hibernate的SessionFactory,使用一个ThreadLocal Session)或者可以是一个JtaTransactionManager(代理容器的JTA子系统)。甚至你可以使用一个自定义的PlatformTransactionManager实现。
           如果选择从本地Hibernate事务管理转为由JTA来进行事务管理,例如:当你的应用的部署面对分布的事务需求时,也仅仅是改变一下配置的事。只要简单地将Hibernate的事务管理换为JTA事务实现即可。所有的事务划分和数据访问无需做任何变动仍可以继续工作,因为他们使用的都是普通的事务管理API。
           对于分布式的事务会跨越多个Hibernate的session factories,仅仅是联合JtaTransactionManager与多个LocalSessionFactoryBean定义作为事务策略。你的每一个DAO将通过它们各自的Bean属性得到一个特殊的SessionFactory的引用。如果这一切都是在下面的JDBC数据源是事务容器,一个业务对象可以划分事务跨越很多DAO和很多session factories而无需做特别的处理,对于使用JtaTransactionManager做为事务策略也是一样的。

     
    <beans>
    
    <bean id="myDataSource1" class="org.springframework.jndi
    .JndiObjectFactoryBean">
    <property name="jndiName">
    <value>jdbc/myds1</value>
    </property>
    </bean>
    
    <bean id="myDataSource2" class="org.springframework.jndi.
    JndiObjectFactoryBean">
    <property name="jndiName">
    <value>jdbc/myds2</value>
    </property>
    </bean>
    
    <bean id="mySessionFactory1"
          class="org.springframework.orm.hibernate.
    LocalSessionFactoryBean">
    <property name="mappingResources">
    <list>
    <value>product.hbm.xml</value>
    </list>
    </property>
    <property name="hibernateProperties">
    <props>
    <prop key="hibernate.dialect">net.sf.hibernate.dialect.
    MySQLDialect</prop>
    </props>
    </property>
    <property name="dataSource">
    <ref bean="myDataSource1"/>
    </property>
    </bean>
    
    <bean id="mySessionFactory2"
          class="org.springframework.orm.hibernate.
    LocalSessionFactoryBean">
    <property name="mappingResources">
    <list>
    <value>inventory.hbm.xml</value>
    </list>
    </property>
    <property name="hibernateProperties">
    <props>
    <prop key="hibernate.dialect">net.sf.hibernate.
    dialect.OracleDialect</prop>
    </props>
    </property>
    <property name="dataSource">
    <ref bean="myDataSource2"/>
    </property>
    </bean>
    
    <bean id="myTransactionManager"
          class="org.springframework.transaction.jta.
    JtaTransactionManager"/>
    
    <bean id="myProductDao" class="product.ProductDaoImpl">
    <property name="sessionFactory">
    <ref bean="mySessionFactory1"/>
    </property>
    </bean>
    
    <bean id="myInventoryDao" class="product.InventoryDaoImpl">
    <property name="sessionFactory">
    <ref bean="mySessionFactory2"/>
    </property>
    </bean>
    
    <bean id="myProductServiceTarget" class="product.
    ProductServiceImpl">
    <property name="productDao">
    <ref bean="myProductDao"/>
    </property>
    <property name="inventoryDao">
    <ref bean="myInventoryDao"/>
    </property>
    </bean>
    
    <bean id="myProductService"
          class="org.springframework.transaction.interceptor.
    TransactionProxyFactoryBean">
    <property name="transactionManager">
    <ref bean="myTransactionManager"/>
    </property>
    <property name="target">
    <ref bean="myProductServiceTarget"/>
    </property>
    <property name="transactionAttributes">
    <props>
    <prop key="increasePrice*">PROPAGATION_REQUIRED</prop>
    <prop key="someOtherBusinessMethod">PROPAGATION_MANDATORY</prop>
    </props>
    </property>
    </bean>
    
    </beans>

           无论是HibernateTransactionManager还是JtaTransactionManager允许适当的对Hibernate的在JVM层次的缓存处理-不需要容器-提供特殊的事务查找或者JCA连接器(只要不使用EJB发起事务)。另外,HibernateTransactionManager能输出JDBC连接供通常的JDBC访问代码使用。这样就允许在高层次上的事务划分是混合了Hibernate与JDBC而不要JTA的,只要只是访问一个数据库就可以!

    8. 使用Spring管理应用的Bean

            一个Spring应用配置定义可以被多种配置实现所加载,从FileSystemXmlApplicationContext和ClassPathXmlApplicationContext到XmlWebApplicationContext。这就允许在各种环境下重用Spring管理的数据访问和业务对象。默认情况下,一个Web应用将有它自己的定义在“WEB-INF/applicationContext.xml”中的根配置。
           在任何一个Spring应用中,一个应用配置定义在一个XML格式的文件中用来对应用的所有有关的Bean进行装配,从Hibernate的session factory到自定义的数据访问和业务对象(象上面所有的Bean那样)。他们中的大多数不需要Spring容器知道他们,甚至即使是与其他Bean合作时也一样,因为他们只是简单的JavaBean之间的协作。下面的Bean定义可能是一个Spring Web 的MVC配置中用来访问业务对象的配置的一部分。

     
    <bean id="myProductList" class="product.ProductListController">
    <property name="productService">
    <ref bean="myProductService"/>
    </property>
    </bean>

          Spring的Web控制器经由Bean引用拥有它们需要的所有的业务和数据访问对象,因此它们无需在应用配置中做任何手工的Bean查找。但是当使用Spring管理的Beans用于Struts或者是在EJB实现,或者一个applet中时常常是需要必须手工查找一个Bean的。因此Spring的Bean可以被用在任何地方。也许只是需要是一应用配置的引用,或者经由一个web容器的Servlet配置属性,或者从一个文件中或者类路径的资源中创建它。

    ApplicationContext context =WebApplicationContextUtils.
    getWebApplicationContext(servletContext);
    ProductService productService =
    (ProductService) context.getBean("myProductService");

     ApplicationContext context =
    new FileSystemXmlApplicationContext("C:/myContext.xml");
    ProductService productService =
    (ProductService) context.getBean("myProductService");
    

     ApplicationContext context =
    new ClassPathXmlApplicationContext("myContext.xml");
    ProductService productService =
    (ProductService) context.getBean("myProductService");

    9. 容器资源VS本地资源

           Spring的资源管理允许简单地在一个JNDI SessionFactory和一个本地SessionFactory间做选择,同样允许在一个JNDI DataSource与本地DataSource间做选择,而无需改变应用的一行代码。在容器中保存资源定义还是在应用本地保存,主要是一个事务策略方面的事。比较一个Spring定义的本地SessionFactory与一个手工注册的JNDI SessionFactory没有任何益处。如果经由Hibernate的JCA连接器注册,才会有加入JTA事务的明显益处,特别是对EJB。

           一个重要的Spring事务提供的好处是它不与任何容器绑定。定义包括JTA在内的策略,它都可以独立工作或者在一个试验环境中工作。特别是对典型的一个数据库的事务来说,对于JTA这是一个非常轻量的和强大的选择。当使用本地EJB SLSB的事务时,你将同时依赖EJB容器和JTA-即使你只是访问一个数据库,即使只是使用SLSBs经由CMT来声明事务。选择使用 JTA编程也需要一个J2EE环境。

           就JTA自身和JNDI数据源来说JTA不只是包括容器依赖。对于不使用Spring的JTA驱动的Hibernate事务,你必须使用HibernateJCA连接器或者在合适的JVM缓冲层专门写Hibernate的事务代码配置JTA事务。在只访问一个数据库的情况下,Spring驱动的事务可以与一个本地定义的Hibernate的SessionFactory配合良好,就如同与一个本地JDBC数据源相配合一样。因此当面对分布的事务需求时,你只需要转换为Spring的JTA事务策略即可。

           要注意一个JCA连接器需要特别的容器的部署步骤,并且显然首先得支持JCA。这比使用本地资源定义和Spring驱动事务来部署一个简单的Web应用有更多的争议。而且你常常需要企业版本的容器支持,象WebLogic Express就不提供JCA。一个只用一个数据库的使用本地资源和事务的Spring应用可以在任何J2EE的Web容器中工作,Web容器不必支持JTA, JCA和EJB,如:Tomcat, Resin甚至最小的Jetty。另外,这样一个中间层就可以很容易地在桌面应用或者在测试套件中被重用。

           所有考虑过的事情包括:如果你不使用EJB,坚持使用本地SessionFactory,使用SpringHibernateTransactionManager或者JtaTransactionManager,你将获得包括适当处理的JVM层的缓存和分布事务的所有益处,而无需引起任何关于容器部署的争论。经由JCA连接器的一个Hibernate的SessionFactory的JNDI注册只是在使用EJB的情况中才会有明显的附加值。


    10. Skeletons和例子

          配置使用Spring和HIbernate的一个J2EE的Web应用的注释和细节最好去看看在Spring Framework的例子中的“典型的Web应用”Skeletons,它给出了适合于JDBC 和 Hibernate应用的多种数据源及事务管理的配置项,仔细看一下事务拦截器的配置,它也同样向你展示了如何配置AOP拦截器。

          在Spring的1.0 M2版中,例子Petclinic提供了JDBC和Hibernate的DAO实现和应用配置的选择。Petclinic
    可以作为一个可工作的简单应用说明如何在一个Spring web 应用中使用Hibernate,同样也包括根据不同的事务策略来声明事务划分。

    Links

    Spring Framework website

    Spring Framework documentation

    posted @ 2006-08-01 13:54 rendong 阅读(221) | 评论 (0)编辑 收藏
     

    Ant指南

    1. Ant是什么?
    2. 安装Ant
    3. 运行Ant
    4. 编写build.xml
    5. 内置task(internet)
    6. EAR task(internet)
    7. WAR task(internet)
    8. JUnit task(internet)


    1. Ant是什么?

    Ant是一种基于Java的build工具。理论上来说,它有些类似于(Unix)C中的make ,但没有make的缺陷。

    既然我们已经有了make, gnumake, nmake, jam以及其他的build工具为什么还要要一种新的build工具呢?因为Ant的原作者在多种(硬件)平台上开发软件时,无法忍受这些工具的限制和不便。类似于make的工具本质上是基于shell(语言)的:他们计算依赖关系,然后执行命令(这些命令与你在命令行敲的命令没太大区别)。这就意味着你可以很容易地通过使用OS特有的或编写新的(命令)程序扩展该工具;然而,这也意味着你将自己限制在了特定的OS,或特定的OS类型上,如Unix。

    Makefile也很可恶。任何使用过他们的人都碰到过可恶的tab问题。Ant的原作者经常这样问自己:“是否我的命令不执行只是因为在我的tab前有一个空格?!!”。类似于jam的工具很好地处理了这类问题,但是(用户)必须记住和使用一种新的格式。

    Ant就不同了。与基于shell命令的扩展模式不同,Ant用Java的类来扩展。(用户)不必编写shell命令,配置文件是基于XML的,通过调用target树,就可执行各种task。每个task由实现了一个实现了特定Task接口的对象来运行。(如果你对Ant一点概念都没有的话,可能看不懂这一节,没有关系,后面会对target,task做详细的介绍。你如果没有太多的时间甚至可以略过这一节,然后再回来浏览一下这里的介绍,那时你就会看懂了。同样,如果你对make之类的工具不熟悉也没关系,下面的介绍根本不会用到make中的概念。)

    必须承认,这样做,在构造shell命令时会失去一些特有的表达能力。如`find . -name foo -exec rm {}`,但却给了你跨平台的能力-你可以在任何地方工作。如果你真的需要执行一些shell命令,Ant有一个<exec> task,这个task允许执行特定OS上的命令。

    返回
    2. 安装Ant
    由于Ant是一个Open Source的软件,所以有两种安装Ant的方式,一种是用已编译好的binary 文件安装Ant,另一种是用源代码自己build Ant。

    binary 形式的Ant可以从http://jakarta.apache.org/builds/ant/release/v1.4.1/bin下载。如果你希望你能自己编译Ant,则可从 http://jakarta.apache.org/builds/ant/release/v1.4.1/src。注意所列出的连接都是最新发行版的Ant。如果你读到此文时,发现已经有了更新的版本,那么请用新版本。如果你是一个疯狂的技术追求者,你也可以从Ant CVS repository下载最新版本的Ant。

    系统需求

    要想自己build Ant。你需要一个JAXP兼容的XML解析器(parser)放在你的CLASSPATH系统变量中。

    binary 形式的Ant包括最新版的Apache Crimson XML解析器。你可以从http://java.sun.com/xml/ 得到更多的关于JAXP的信息。如果你希望使用其他的JAXP兼容的解析器。你要从Ant的lib目录中删掉jaxp.jar以及crimson.jar。然后你可将你心爱的解析器的jar文件放到Ant的lib目录中或放在你的CLASSPATH系统变量中。

    对于当前版本的Ant,需要你的系统中有JDK,1.1版或更高。未来的Ant版本会要求使用JDK 1.2或更高版本。

    安装Ant

    binary 版的Ant包括三个目录:bin, docs 和lib。只有bin和lib目录是运行Ant所需的。要想安装Ant,选择一个目录并将发行版的文件拷贝到该目录下。这个目录被称作ANT_HOME。

    在你运行Ant之前需要做一些配置工作。
    • 将bin目录加入PATH环境变量。
    • 设定ANT_HOME环境变量,指向你安装Ant的目录。在一些OS上,Ant的脚本可以猜测ANT_HOME(Unix和Windos NT/2000)-但最好不要依赖这一特性。
    • 可选地,设定JAVA_HOME环境变量(参考下面的高级小节),该变量应该指向你安装JDK的目录。
    注意:不要将Ant的ant.jar文件放到JDK/JRE的lib/ext目录下。Ant是个应用程序,而lib/ext目录是为JDK扩展使用的(如JCE,JSSE扩展)。而且通过扩展装入的类会有安全方面的限制。

    可选Task

    Ant支持一些可选task。一个可选task一般需要额外的库才能工作。可选task与Ant的内置task分开,单独打包。这个可选包可以从你下载Ant的同一个地方下载。目前包含可选task的jar文件名叫jakarta-ant-1.4.1-optional.jar。这个jar文件应该放到Ant安装目录的lib目录下。

    每个可选task所需的外部库可参看依赖库小节。这些外部库可以放到Ant的lib目录下,这样Ant就能自动装入,或者将其放入环境变量中。

    Windows

    假定Ant安装在c:\ant\目录下。下面是设定环境的命令:
    set ANT_HOME=c:\ant
    set JAVA_HOME=c:\jdk1.2.2
    set PATH=%PATH%;%ANT_HOME%\bin
    Unix (bash)

    假定Ant安装在/usr/local/ant目录下。下面是设定环境的命令:
    export ANT_HOME=/usr/local/ant
    export JAVA_HOME=/usr/local/jdk-1.2.2
    export PATH=${PATH}:${ANT_HOME}/bin
    高级

    要想运行Ant必须使用很多的变量。你至少参考需要下面的内容:
    • Ant的CLASSPATH必须包含ant.jar以及你所选的JAXP兼容的XML解析器的jar文件。
    • 当你需要JDK的功能(如javac或rmic task)时,对于JDK 1.1,JDK的classes.zip文件必须放入CLASSPATH中;对于JDK 1.2或JDK 1.3,则必须加入tools.jar。如果设定了正确的JAVA_HOME环境变量,Ant所带的脚本,在bin目录下,会自动加入所需的JDK类。
    • 当你执行特定平台的程序(如exec task或cvs task)时,必须设定ant.home属性指向Ant的安装目录。同样,Ant所带的脚本利用ANT_HOME环境变量自动设置该属性。
    Building Ant

    要想从源代码build Ant,你要先安装Ant源代码发行版或从CVS中checkout jakarta-ant模块。

    安装好源代码后,进入安装目录。

    设定JAVA_HOME环境变量指向JDK的安装目录。要想知道怎么做请参看安装Ant小节。

    确保你已下载了任何辅助jar文件,以便build你所感兴趣的task。这些jar文件可以放在CLASSPATH中,也可以放在lib/optional目录下。参看依赖库小节可知不同的task需要那些jar文件。注意这些jar文件只是用作build Ant之用。要想运行Ant,你还要像安装Ant小节中所做的那样设定这些jar文件。

    现在你可以build Ant了:
    build -Ddist.dir=<directory_to_contain_Ant_distribution> dist (Windows)
    build.sh -Ddist.dir=<directory_to_contain_Ant_distribution> dist (Unix)
    这样就可你指定的目录中创建一个binary版本。

    上面的命令执行下面的动作:
    • 如果有必要可以bootstrap Ant的代码。bootstrap 包括手工编辑一些Ant代码以便运行Ant。bootstrap 用于下面的build步骤。
    • 向build脚本传递参数以调用bootstrap Ant。参数定义了Ant的属性值并指定了Ant自己的build.xml文件的"dist" target。
    大多数情况下,你不必直接bootstrap Ant,因为build脚本为你完成这一切。运行bootstrap.bat (Windows) 或 bootstrap.sh (UNIX) 可以build一个新的bootstrap版Ant。

    如果你希望将Ant安装到ANT_HOME目录下,你可以使用:
    build install (Windows)
    build.sh install (Unix)
    如果你希望跳过冗长的Javadoc步骤,可以用:
    build install-lite (Windows)
    build.sh install-lite (Unix)
    这样就只会安装bin和lib目录。

    注意install和install-lite都会覆盖ANT_HOME中的当前Ant版本。

    依赖库

    如果你需要执行特定的task,你需要将对应的库放入CLASSPATH或放到Ant安装目录的lib目录下。注意使用mapper时只需要一个regexp库。同时,你也要安装Ant的可选jar包,它包含了task的定义。参考上面的安装Ant小节。

    Jar NameNeeded ForAvailable At
    An XSL transformer like Xalan or XSL:Pstyle taskhttp://xml.apache.org/xalan-j/index.html or http://www.clc-marketing.com/xslp/
    jakarta-regexp-1.2.jarregexp type with mappersjakarta.apache.org/regexp/
    jakarta-oro-2.0.1.jarregexp type with mappers and the perforce tasksjakarta.apache.org/oro/
    junit.jarjunit taskswww.junit.org
    stylebook.jarstylebook taskCVS repository of xml.apache.org
    testlet.jartest taskjava.apache.org/framework
    antlr.jarantlr taskwww.antlr.org
    bsf.jarscript taskoss.software.ibm.com/developerworks/projects/bsf
    netrexx.jarnetrexx taskwww2.hursley.ibm.com/netrexx
    rhino.jarjavascript with script taskwww.mozilla.org
    jpython.jarpython with script taskwww.jpython.org
    netcomponents.jarftp and telnet taskswww.savarese.org/oro/downloads

    返回
    3. 运行Ant
    运行Ant非常简单,当你正确地安装Ant后,只要输入ant就可以了。

    没有指定任何参数时,Ant会在当前目录下查询build.xml文件。如果找到了就用该文件作为buildfile。如果你用 -find 选项。Ant就会在上级目录中寻找buildfile,直至到达文件系统的根。要想让Ant使用其他的buildfile,可以用参数 -buildfile file,这里file指定了你想使用的buildfile。

    你也可以设定一些属性,以覆盖buildfile中指定的属性值(参看property task)。可以用 -Dproperty=value 选项,这里property是指属性的名称,而value则是指属性的值。也可以用这种办法来指定一些环境变量的值。你也可以用property task来存取环境变量。只要将 -DMYVAR=%MYVAR% (Windows)-DMYVAR=$MYVAR (Unix) 传递给Ant -你就可以在你的buildfile中用${MYVAR}来存取这些环境变量。

    还有两个选项 -quite,告诉Ant运行时只输出少量的必要信息。而 -verbose,告诉Ant运行时要输出更多的信息。

    可以指定执行一个或多个target。当省略target时,Ant使用标签<project>的default属性所指定的target。

    如果有的话,-projecthelp 选项输出项目的描述信息和项目target的列表。先列出那些有描述的,然后是没有描述的target。

    命令行选项总结:
    ant [options] [target [target2 [target3] ...]]
    Options:
    -help print this message
    -projecthelp print project help information
    -version print the version information and exit
    -quiet be extra quiet
    -verbose be extra verbose
    -debug print debugging information
    -emacs produce logging information without adornments
    -logfile file use given file for log output
    -logger classname the class that is to perform logging
    -listener classname add an instance of class as a project listener
    -buildfile file use specified buildfile
    -find file search for buildfile towards the root of the filesystem and use the first one found
    -Dproperty=value set property to value
    例子
    ant
    使用当前目录下的build.xml运行Ant,执行缺省的target。
    ant -buildfile test.xml
    使用当前目录下的test.xml运行Ant,执行缺省的target。
    ant -buildfile test.xml dist
    使用当前目录下的test.xml运行Ant,执行一个叫做dist的target。
    ant -buildfile test.xml -Dbuild=build/classes dist
    使用当前目录下的test.xml运行Ant,执行一个叫做dist的target,并设定build属性的值为build/classes。

    文件


    在Unix上,Ant的执行脚本在做任何事之前都会source(读并计算值)~/.antrc 文件;在Windows上,Ant的批处理文件会在开始时调用%HOME%\antrc_pre.bat,在结束时调用%HOME%\antrc_post.bat。你可以用这些文件配置或取消一些只有在运行Ant时才需要的环境变量。看下面的例子。

    环境变量

    包裹脚本(wrapper scripts)使用下面的环境变量(如果有的话):
    • JAVACMD Java可执行文件的绝对路径。用这个值可以指定一个不同于JAVA_HOME/bin/java(.exe)的JVM。
    • ANT_OPTS 传递给JVM的命令行变量-例如,你可以定义属性或设定Java堆的最大值
    手工运行Ant

    如果你自己动手安装(DIY)Ant,你可以用下面的命令启动Ant:
    java -Dant.home=c:\ant org.apache.tools.ant.Main [options] [target]
    这个命令与前面的ant命令一样。选项和target也和用ant命令时一样。这个例子假定你的CLASSPATH包含:
    • ant.jar
    • jars/classes for your XML parser
    • the JDK's required jar/zip files
    返回
    4. 编写build.xml
    Ant的buildfile是用XML写的。每个buildfile含有一个project。

    buildfile中每个task元素可以有一个id属性,可以用这个id值引用指定的任务。这个值必须是唯一的。(详情请参考下面的Task小节)

    Projects

    project有下面的属性:

    AttributeDescriptionRequired
    name项目名称.No
    default当没有指定target时使用的缺省targetYes
    basedir用于计算所有其他路径的基路径。该属性可以被basedir property覆盖。当覆盖时,该属性被忽略。如果属性和basedir property都没有设定,就使用buildfile文件的父目录。No
    项目的描述以一个顶级的<description>元素的形式出现(参看description小节)。

    一个项目可以定义一个或多个target。一个target是一系列你想要执行的。执行Ant时,你可以选择执行那个target。当没有给定target时,使用project的default属性所确定的target。

    Targets

    一个target可以依赖于其他的target。例如,你可能会有一个target用于编译程序,一个target用于生成可执行文件。你在生成可执行文件之前必须先编译通过,所以生成可执行文件的target依赖于编译target。Ant会处理这种依赖关系。

    然而,应当注意到,Ant的depends属性只指定了target应该被执行的顺序-如果被依赖的target无法运行,这种depends对于指定了依赖关系的target就没有影响。

    Ant会依照depends属性中target出现的顺序(从左到右)依次执行每个target。然而,要记住的是只要某个target依赖于一个target,后者就会被先执行。
    <target name="A"/>
    <target name="B" depends="A"/>
    <target name="C" depends="B"/>
    <target name="D" depends="C,B,A"/>
    假定我们要执行target D。从它的依赖属性来看,你可能认为先执行C,然后B,最后A被执行。错了,C依赖于B,B依赖于A,所以先执行A,然后B,然后C,最后D被执行。

    一个target只能被执行一次,即时有多个target依赖于它(看上面的例子)。

    如果(或如果不)某些属性被设定,才执行某个target。这样,允许根据系统的状态(java version, OS, 命令行属性定义等等)来更好地控制build的过程。要想让一个target这样做,你就应该在target元素中,加入if(或unless)属性,带上target因该有所判断的属性。例如:
    <target name="build-module-A" if="module-A-present"/>
    <target name="build-own-fake-module-A" unless="module-A-present"/>
    如果没有if或unless属性,target总会被执行。

    可选的description属性可用来提供关于target的一行描述,这些描述可由-projecthelp命令行选项输出。

    将你的tstamp task在一个所谓的初始化target是很好的做法,其他的target依赖这个初始化target。要确保初始化target是出现在其他target依赖表中的第一个target。在本手册中大多数的初始化target的名字是"init"。

    target有下面的属性:

    AttributeDescriptionRequired
    nametarget的名字Yes
    depends用逗号分隔的target的名字列表,也就是依赖表。No
    if执行target所需要设定的属性名。No
    unless执行target需要清除设定的属性名。No
    description关于target功能的简短描述。No

    Tasks

    一个task是一段可执行的代码。

    一个task可以有多个属性(如果你愿意的话,可以将其称之为变量)。属性只可能包含对property的引用。这些引用会在task执行前被解析。

    下面是Task的一般构造形式:
    <name attribute1="value1" attribute2="value2" ... />
    这里name是task的名字,attributeN是属性名,valueN是属性值。

    有一套内置的(built-in)task,以及一些可选task,但你也可以编写自己的task。

    所有的task都有一个task名字属性。Ant用属性值来产生日志信息。

    可以给task赋一个id属性:
    <taskname id="taskID" ... />
    这里taskname是task的名字,而taskID是这个task的唯一标识符。通过这个标识符,你可以在脚本中引用相应的task。例如,在脚本中你可以这样:
    <script ... >
    task1.setFoo("bar");
    </script>
    设定某个task实例的foo属性。在另一个task中(用java编写),你可以利用下面的语句存取相应的实例。
    project.getReference("task1").
    注意1:如果task1还没有运行,就不会被生效(例如:不设定属性),如果你在随后配置它,你所作的一切都会被覆盖。

    注意2:未来的Ant版本可能不会兼容这里所提的属性,因为很有可能根本没有task实例,只有proxies。

    Properties

    一个project可以有很多的properties。可以在buildfile中用property task来设定,或在Ant之外设定。一个property有一个名字和一个值。property可用于task的属性值。这是通过将属性名放在"${"和"}"之间并放在属性值的位置来实现的。例如如果有一个property builddir的值是"build",这个property就可用于属性值:${builddir}/classes。这个值就可被解析为build/classes。

    内置属性

    如果你使用了<property> task 定义了所有的系统属性,Ant允许你使用这些属性。例如,${os.name}对应操作系统的名字。

    要想得到系统属性的列表可参考the Javadoc of System.getProperties

    除了Java的系统属性,Ant还定义了一些自己的内置属性:
     basedir   project基目录的绝对路径 (与<project>的basedir属性一样)。
     ant.file   buildfile的绝对路径。
     ant.version  Ant的版本。
     ant.project.name  当前执行的project的名字;由<project>的name属性设定.
     ant.java.version  Ant检测到的JVM的版本; 目前的值有"1.1", "1.2", "1.3" and "1.4".
        
    例子
    <project name="MyProject" default="dist" basedir="."> 
    
     <!-- set global properties for this build -->
     <property name="src" value="."/>
     <property name="build" value="build"/>
     <property name="dist" value="dist"/> 
        
     <target name="init">
      <!-- Create the time stamp -->
      <tstamp/>
      <!-- Create the build directory structure used by compile -->
      <mkdir dir="${build}"/>
     </target>
       
     <target name="compile" depends="init">
      <!-- Compile the java code from ${src} into ${build} -->
      <javac srcdir="${src}" destdir="${build}"/>
     </target>
     
     <target name="dist" depends="compile">
      <!-- Create the distribution directory -->
      <mkdir dir="${dist}/lib"/>
      <!-- Put everything in ${build} into the MyProject-${DSTAMP}.jar file -->
      <jar jarfile="${dist}/lib/MyProject-${DSTAMP}.jar" basedir="${build}"/>
     </target>
     
     <target name="clean">
      <!-- Delete the ${build} and ${dist} directory trees -->
      <delete dir="${build}"/>
      <delete dir="${dist}"/>
     </target>
     
    </project>
    
    Token Filters

    一个project可以有很多tokens,这些tokens在文件拷贝时会被自动扩展,这要求在支持这一行为的task中选择过滤拷贝功能。这一功能可用filter task在buildfile中设定。

    既然这很可能是一个有危害的行为,文件中的tokens必须采取@token@的形式,这里token是filter task中设定的token名。这种token语法与其他build系统执行类似filtering的语法相同,而且与大多数的编程和脚本语言以及文档系统并不冲突,

    注意:如果在一个文件中发现了一个@token@形式的token,但没有filter与这个token关连,则不会发生任何事;因此,没有转义方法-但只要你为token选择合适的名字,就不会产生问题。

    警告:如果你在拷贝binary文件时打开filtering功能,你有可能破坏文件。这个功能只针对文本文件。

    Path-like Structures
    你可以用":"和";"作为分隔符,指定类似PATH和CLASSPATH的引用。Ant会把分隔符转换为当前系统所用的分隔符。

    当需要指定类似路径的值时,可以使用嵌套元素。一般的形式是
     <classpath>
      <pathelement path="${classpath}"/>
      <pathelement location="lib/helper.jar"/>
     </classpath>
    location属性指定了相对于project基目录的一个文件和目录,而path属性接受逗号或分号分隔的一个位置列表。path属性一般用作预定义的路径--其他情况下,应该用多个location属性。

    为简洁起见,classpath标签支持自己的path和location属性。所以:
     <classpath>
      <pathelement path="${classpath}"/>
     </classpath>
    可以被简写作:
     <classpath path="${classpath}"/>
    也可通过<fileset>元素指定路径。构成一个fileset的多个文件加入path-like structure的顺序是未定的。
     <classpath>
      <pathelement path="${classpath}"/>
      <fileset dir="lib">
       <include name="**/*.jar"/>
      </fileset>
      <pathelement location="classes"/>
     </classpath>
    上面的例子构造了一个路径值包括:${classpath}的路径,跟着lib目录下的所有jar文件,接着是classes目录。

    如果你想在多个task中使用相同的path-like structure,你可以用<path>元素定义他们(与target同级),然后通过id属性引用--参考Referencs例子。

    path-like structure可能包括对另一个path-like structurede的引用(通过嵌套<path>元素):
     <path id="base.path">
      <pathelement path="${classpath}"/>
      <fileset dir="lib">
       <include name="**/*.jar"/>
      </fileset>
     <pathelement location="classes"/>
     </path>
      <path id="tests.path">
      <path refid="base.path"/>
      <pathelement location="testclasses"/>
     </path>
    
    前面所提的关于<classpath>的简洁写法对于<path>也是有效的,如:
     <path id="tests.path">
        <path refid="base.path"/>
      <pathelement location="testclasses"/>
     </path>
    可写成:
     <path id="base.path" path="${classpath}"/>
    命令行变量

    有些task可接受参数,并将其传递给另一个进程。为了能在变量中包含空格字符,可使用嵌套的arg元素。

    AttributeDescriptionRequired
    value一个命令行变量;可包含空格字符。只能用一个
    line空格分隔的命令行变量列表。
    file作为命令行变量的文件名;会被文件的绝对名替代。
    path一个作为单个命令行变量的path-like的字符串;或作为分隔符,Ant会将其转变为特定平台的分隔符。

    例子
     <arg value="-l -a"/>
    是一个含有空格的单个的命令行变量。
     <arg line="-l -a"/>
    是两个空格分隔的命令行变量。
     <arg path="/dir;/dir2:\dir3"/>
    是一个命令行变量,其值在DOS系统上为\dir;\dir2;\dir3;在Unix系统上为/dir:/dir2:/dir3 。

    References

    buildfile元素的id属性可用来引用这些元素。如果你需要一遍遍的复制相同的XML代码块,这一属性就很有用--如多次使用<classpath>结构。

    下面的例子:
     <project ... >
      <target ... >    
       <rmic ...>      
        <classpath>        
         <pathelement location="lib/"/>        
         <pathelement path="${java.class.path}/"/>        
         <pathelement path="${additional.path}"/>      
        </classpath>    
       </rmic>  
      </target>
      <target ... >
       <javac ...>
        <classpath>
         <pathelement location="lib/"/>
         <pathelement path="${java.class.path}/"/>
         <pathelement path="${additional.path}"/>
        </classpath>
       </javac>
      </target>
     </project>
    可以写成如下形式:
     <project ... > 
      <path id="project.class.path">  
       <pathelement location="lib/"/>
       <pathelement path="${java.class.path}/"/>   
       <pathelement path="${additional.path}"/> 
      </path>
      <target ... >
       <rmic ...>
        <classpath refid="project.class.path"/>
       </rmic>
      </target>
      <target ... > 
       <javac ...>
        <classpath refid="project.class.path"/>
       </javac>
      </target>
     </project>
    所有使用PatternSets, FileSets 或 path-like structures嵌套元素的task也接受这种类型的引用。
    posted @ 2006-08-01 13:52 rendong 阅读(218) | 评论 (0)编辑 收藏
     
    在 struts+ hibernate 这种结构中,是不应该把Hibernate产生的PO直接传递给JSP的,不管他是Iterator,还是List,这是一个设计错误。

    我来谈谈在J2EE架构中各层的数据表示方法:

    Web层的数据表示是FormBean,数据来源于HTML Form POST
    业务层的数据表示是VO
    持久层的数据表示是PO,其数据来源于数据库,持久层的数据表示例如CMP

    在一个规范的J2EE架构中,不同层的数据表示应该被限制在层内,而不应该扩散到其它层,这样可以降低层间的耦合性,提高J2EE架构整体的可维护性和可扩展性。比如说Web层的逻辑进行了修改,那么只需要修改FormBean的结构,而不需要触动业务层和持久层的代码修改。同样滴,当数据库表进行了小的调整,那么也只需要修改持久层数据表示,而不需要触动业务层代码和Web层代码。

    不过由于Hibernate的强大功能,例如动态生成PO,PO的状态管理可以脱离Session,使得在应用了Hibernate的J2EE框架中,PO完全可以充当VO,因此我们下面把PO和VO合并,统称为PO。

    先来谈谈ActionFormBean和持久层的PO之间的重大区别。

    在简单的应用中,ActionFormBean和PO几乎是没有区别,所以很多人干脆就是用ActionFormBean来充当PO,于是ActionFormBean从JSP页面到Servlet控制层再到业务层,然后穿过持久层,最后一直映射到数据库表。真是一竿子捅到了底!

    但是在复杂的应用中,ActionFormBean和PO是分离的,他们也不可能一样。ActionFormBean是和网页里面的Form表单一一对应的,Form里面有什么元素,Bean里面就有什么属性。而PO和数据库表对应,因此如果数据库表不修改,那么PO也不会修改,如果页面的流程和数据库表字段对应关系不一致,那么你又如何能够使用ActionFormBean来取代PO呢?

    比如说吧,用户注册页面要求注册用户的基本信息,因此HTML Form里面包含了基本信息属性,于是你需要一个ActionFormBean来一一对应(注意:是一一对应),每个Bean属性对应一个文本框或者选择框什么的。

    而用户这个持久对象呢?他的属性和ActionFormBean有什么明显不同呢?他会有一些ActionFormBean所没有的集合属性,比如说用户的权限属性,用户的组属性,用户的帖子等等。另外还有可能的是在ActionFormBean里面有3个属性,分别是用户的First Name, Middle Name, Last Name,而在我的User这个持久对象中就是一个 Name 对象属性。

    假设我的注册页面原来只要你提供First Name,那么ActionFormBean就这一个属性,后来我要你提供全名,你要改ActionFormBean,加两个属性。但是这个时候PO是不应该修改滴,因为数据库没有改。

    那么在一个完整的J2EE系统中应该如何进行合理的设计呢?

    JSP(View) ---> ActionFormBean(Module) ---> Action(Control)

    ActionFormBean是Web层的数据表示,它和HTML页面Form对应,只要Web页面的操作流程发生改变,它就要相应的进行修改,它不应该也不能被传递到业务层和持久层,否则一旦页面修改,会一直牵连到业务层和持久层的大面积的代码进行修改,对于软件的可维护性和可扩展性而言,是一个灾难,Actiont就是他的边界,到此为止!

    Action(Web Control) ---> Business Bean ---> DAO ---> ORM --->DB

    而PO则是业务层和持久层的数据表示,它在业务层和持久层之间进行流动,他不应该也不能被传递到Web层的View中去,而ActionServlet就是他的边界,到此为止!

    然后来看一看整个架构的流程:

    当用户通过浏览器访问网页,提交了一个页面。于是Action拿到了这个FormBean,他会把FormBean属性读出来,然后构造一个PO对象,再调用业务层的Bean类,完成了注册操作,重定向到成功页面。而业务层Bean收到这个PO对象之后,调用DAO接口方法,进行持久对象的持久化操作。

    当用户查询某个会员的信息的时候,他用全名进行查询,于是Action得到一个UserNameFormBean包括了3个属性,分别是first name, middle name, last name,然后Action把UserNameFormBean的3个属性读出来,构造Name对象,再调用业务Bean,把Name对象传递给业务Bean,进行查询。

    业务Bean取得Name(注意: Name对象只是User的一个属性)对象之后调用DAO接口,返回一个User的PO对象,注意这个User不同于在Web层使用的UserFormBean,他有很多集合属性滴。然后业务Bean把User对象返回给Action。

    Action拿到User之后,把User的基本属性取出(集合属性如果不需要就免了),构造UserFormBean,然后把UserFormBean request.setAttribute(...),然后重定向到查询结果页面。

    查询页面拿到request对象里面的ActionFormBean,自动调用tag显示之。

    总结:

    FormBean是Web层的数据表示,他不能被传递到业务层;PO是持久层的数据表示,在特定情况下,例如Hibernate中,他可以取代VO出现在业务层,但是不管PO还是VO都必须限制在业务层内使用,最多到达Web层的Control,绝不能被扩散到View去。

    FormBean和PO之间的数据转化是在Action中进行滴。

    BTW:

    JDO1.x还不能像Hibernate功能这样强大,PO不能脱离持久层,所以必须在业务层使用VO,因此必须在业务层进行大量的VO和PO的转化操作,相对于Hibernate来说,编程比较烦琐。

    当然咯,理论是一回事,实际操作也不一定非要这样干,你可以自行取舍,在实际项目中灵活一点,增加一点bad smell,提高开发效率。只不过在大型项目中最好还是严丝合缝,不然的话,改版的时候会痛苦的很滴。

    posted @ 2006-08-01 13:49 rendong 阅读(185) | 评论 (0)编辑 收藏
     

    摘要:

    这篇文章提供了一个对J2EE的简化,展示了如何消除应用服务器的消耗和限制。特别地,这篇文章提到了:许多应用程序实际上并不需要运行应用服务器。
    尽管J2EE平台(应用程序服务器)及其编程模型(企业JAVA组件,简称EJB)拥有的众所周知的复杂性,但是基于J2EE的应用程序仍然在企业领域里变得非常成功.我们要感谢应用于轻量级容器的控制反转(IoC)和面向方面编程(AOP),比如Spring框架. 我们能够更简单地设计更大型的编程模型。然而,即使有了这些工具,应用服务器仍然是复杂度和消耗的一个重要瓶颈。这篇文章提供了一个对J2EE的简化,展示了如何消除应用服务器的消耗和限制。特别地,这篇文章提到了:许多应用程序实际上并不需要运行应用服务器。这样,J2EE应用组件将会变得:
    ·        开发更容易:不再需要EJB运行代码;
    ·        更简单: 继承不需要EJB类或接口;
    ·        测试更容易:你的应用程序及测试能在你的开发环境(IDE)中直接运行;
    ·        更少的资源消耗:你只需要你的对象,不需要应用服务器,更不需要应用服务器的对象;
    ·        安装更容易:没有运行应用服务专门的安装软件, 没有加载额外的XML文件;
    ·        维护更容易:所有的过程都更简单,因此维护也更容易。

    J2EE不必要的复杂度已经成为一个阻碍。今天,这种复杂度能够通过在这篇文章中提到的方法来避免。另外,程序还能够保留事务和安全这些典型的服务。J2EE程序从来没有比这更有趣过。

    版权声明:任何获得Matrix授权的网站,转载时请务必保留以下作者信息和链接
    作者:Guy Pardon;chmei83(作者的blog:http://blog.matrix.org.cn/page/chmei83)
    原文:http://www.onjava.com/pub/a/onjava/2006/02/08/j2ee-without-application-server.html
    译文:http://www.matrix.org.cn/resource/article/44/44250_J2ee+Application+Server.html
    关键字:J2ee;Application;Server

    例子:消息驱动Bank

    为了阐述我们的观点,我们将开发和安装一个完整的样板应用程序:一个消息驱动的银行系统. 通过(幸亏有Spring)改进的基于POJOs的编程模型和保留相同的事务,我们可以不需要EJB或者一个应用服务器来实现这个系统。在下一个部分,我们将从消息驱动架构产生到另一个架构.就像基于WEB的架构一样.图1展示我们的样本应用程序的架构.  

    image
    Figure 1. Architecture of the message-driven bank

    在我们的例子中,我们将处理来自Java消息服务队列的银行定单.一张定单的处理包括通过JDBC来更新当前帐户的数据库.为了避免信息的丢失和重复,我们将使用JTA和JTA/XA事务来配合更新:处理信息和更新数据库将发生在一个原子事务里.资源部分可得到JTA/XA的更多信息.

    编写应用程序代码

    该应用程序将由两个JAVA类组成: Bank(一个DAO)和MessageDrivenBank.如图2.

    image
    Figure 2. Classes for the message-driven bank

    Bank是一个数据访问对象,这个对象封装数据库访问。MessageDrivenBank是一个消息驱动fa&ccedil;ade并且是DAO的委托.与典型的J2EE方法不同,这个应用程序不包括EJB类.

    第一步:编写Bank DAO

    如下, Bank源代码是很直接和简单的JDBC操作.

    package jdbc;
    import javax.sql.*;
    import java.sql.*;
    public class Bank
    {
      private DataSource dataSource;
      public Bank() {}
      public void setDataSource ( DataSource dataSource )
      {
        this.dataSource = dataSource;
      }

    private DataSource getDataSource()
      {
        return this.dataSource;
      }

      private Connection getConnection()
      throws SQLException
      {
        Connection ret = null;
        if ( getDataSource() != null ) {
            ret = getDataSource().
                  getConnection();
        }
        return ret;
      }

      private void closeConnection ( Connection c )
      throws SQLException
      {
        if ( c != null ) c.close();
      }
        
      public void checkTables()
      throws SQLException
      {
            
        Connection conn = null;
        try {
          conn = getConnection();
          Statement s = conn.createStatement();
          try {
            s.executeQuery (
            "select * from Accounts" );
          }
          catch ( SQLException ex ) {
            //table not there => create it
            s.executeUpdate (
            "create table Accounts ( " +
            "account VARCHAR ( 20 ), " +
            "owner VARCHAR(300), " +
            "balance DECIMAL (19,0) )" );
            for ( int i = 0; i < 100 ; i++ ){
              s.executeUpdate (
              "insert into Accounts values ( " +
              "'account"+i +"' , 'owner"+i +"', 10000 )"
              );
            }
          }
          s.close();
          }
          finally {
            closeConnection ( conn );

          }

          //That concludes setup
      }

        
      //
      //Business methods are below
      //

      public long getBalance ( int account )
      throws SQLException
      {
            
        long res = -1;
        Connection conn = null;

        try {
          conn = getConnection();
          Statement s = conn.createStatement();
          
          String query =
          "select balance from Accounts where account='"+
          "account" + account +"'";
          
          ResultSet rs = s.executeQuery ( query );
          if ( rs == null || !rs.next() )
            throw new SQLException (
            "Account not found: " + account );
          res = rs.getLong ( 1 );
          s.close();
        }
        finally {
            closeConnection ( conn );
        }
        return res;
            
      }

      public void withdraw ( int account , int amount )
      throws Exception
      {
        Connection conn = null;

        try {
          conn = getConnection();
          Statement s = conn.createStatement();

          String sql =
          "update Accounts set balance = balance - "+
          amount + " where account ='account"+
          account+"'";
          
          s.executeUpdate ( sql );
          s.close();
        
        }
        finally {
            closeConnection ( conn );

        }
      }
    }


    注意:代码并没有依赖EJB或任何专门的应用程序服务器.实际上,这是一个纯JAVA代码,这个JAVA代码是能在任何J2SE环境下运行的.
    你同时应注意:我们使用了来自JDBC的DataSource接口.这意味着我们的类是独立于目前JDBC供应商提供的类. 你可能会疑惑,这怎么能与特定的数据管理系统(DBMS)提供商的JDBC实现紧密结合呢? 这里就是Spring框架帮你实现的. 这个技术被称为依赖注入:在我们的应用程序的启动期间,通过调用setDataSource方法,Spring为我们提供了相应的datasource对象.在后面几部分我们会更多地提到Spring.如果我们在以前使用应用程序服务器,我们将不得不借助于JAVA命名绑定接口(JNDI)查询.

    除了直接使用JDBC,我们也可以使用Hibernate或者一个JDO工具来实现我们的持久层.这同样不需要任何的EJB代码.

    第二步:配置BankDAO

    我们会将便用Spring框架来配置我们的应用程序.Spring不是必需的,但是使用Spring的好处是我们将可以简单的添加服务,如:我们JAVA对象的事务和安全.这类似于应用服务器为EJB提供的东西,只是在我们的例子中Spring将变得更容易.
    Spring也允许我们把我们的类从目前的JDBC驱动实现中分离出来:Spring能够配置Driver(基于我们的XML配置数据)并把它提供给BankDAO对象(依赖注入原理).这样可以保持我们的JAVA代码的清淅和集中.这步的Spring配置文件如下:

    <?xml version="1.0" encoding="UTF-8"?>


    <beans>

    <bean id="datasource"
    class="com.atomikos.jdbc.nonxa.NonXADataSourceBean">

        <property name="user">
            <value>sa</value>
        </property>
        <property name="url">
            <value>jdbc:hsqldb:SpringNonXADB
            </value>
        </property>
        <property name="driverClassName">
            <value>org.hsqldb.jdbcDriver</value>
        </property>
        <property name="poolSize">
            <value>1</value>
        </property>
        <property name="connectionTimeout">
            <value>60</value>
        </property>
    </bean>

    <bean id="bank" class="jdbc.Bank">
            <property name="dataSource">
                <ref bean="datasource"/>
            </property>
    </bean>

    </beans>


    这个XML文件包括两个对象的配置:访问数据库的DataSource和使用这个DataSource的Bank对象.下面是由Spring维护的一些基本任务.
    ·        创建应用程序(例: Bank和DataSource)需要的对象(“beans”).在XML文件中给出了这些对象的类名,并且在我们的例子中,这些对象需要有一个公共的无参数constructor (Spring也允许参数,但是配置语法上有所不同).这些对象都被命名(XML中的id属性),所以我们后面能够引用这些对象. id也允许我们的应用程序找回它需要的已配置对象.
    ·        这些对象的初始化是通过在XML文件中的properties的值实现. 在XML文件中这些properties名 应与对应的类中的setXXX方法相对应.
    ·        将对象连接在一起 :一个property可能是另一个对象(例如:在我们例子中的数据源)的引用,引用可以通过id创建.

    注意:在我们下一步中, 我们将选择配置一个JTA-enabled的数据源(由Atomikos Transactions提供,可用于企业和J2SE的JTA产品,我们将应用于我们的应用程序). 简单起见,我们将使用HypersonicSQLDB,这个DBMS不需要专门的安装步骤—它是在.jar文件里,就像JTA和Spring.

    但是,考虑到渐增的可靠性需求,强列推荐你使用XA-capable的DBMS和JDBC驱动.没有XA的支持, 在crash或重启之后你的应用程序将不能恢复原有数据. 资源部分有链接到关于事务和XA的信息和一些例子.

    作为一个练习,你可以试试从HypersonicSQLDB转换到FirstSQL,一个易安装XA-compliant的DBMS.换句话说,任何其他为企业准备的和XA-capable的DBMS也会做得很好.

    第三步:测试BankDAO

    让我们来测试我们的代码,(使用极限编程的程序员会首先写测试,但因开始不是很清淅,所以我们直到现在才开始写测试.)下面是一个简单的单元测试.这个测试可在你的的应用程序里运行:它通过Spring获得一个BANK对象来进行测试(这在setUp方法中实现).注意:这个测试使用清楚的事务划分:每一个测试开始之前开始一个事务,每个测试结束时强制进行事务回滚.这是通过手工的方式来减少测试对数据库数据的影响.

    package jdbc;
    import com.atomikos.icatch.jta.UserTransactionImp;
    import junit.framework.TestCase;
    import java.io.FileInputStream;
    import java.io.InputStream;
    import org.springframework.beans.factory.xml.XmlBeanFactory;

    public class BankTest extends TestCase
    {

        private UserTransactionImp utx;

        private Bank bank;

        public BankTest ( String name )
        {
            super ( name );
            utx = new UserTransactionImp();
            

        }

        protected void setUp()
            throws Exception
        {
            //start a new transaction
            //so we can rollback the
            //effects of each test
            //in teardown!
            utx.begin();
            
            //open bean XML file
            InputStream is =
                new FileInputStream("config.xml");

            //the factory is Spring's entry point
            //for retrieving the configured
            //objects from the XML file

            XmlBeanFactory factory =
                new XmlBeanFactory(is);

            bank = ( Bank ) factory.getBean ( "bank" );
            bank.checkTables();
        }

        protected void tearDown()
            throws Exception
        {
            //rollback all DBMS effects
            //of testing
            utx.rollback();
        }

        public void testBank()
        throws Exception
        {
            int accNo = 10;
            long initialBalance = bank.getBalance ( accNo );
            bank.withdraw ( accNo , 100 );
            long newBalance = bank.getBalance ( accNo );
            if ( ( initialBalance - newBalance ) != 100 )
                fail ( "Wrong balance after withdraw: " +
                       newBalance );
        }
        
    }


    我们将需要JTA事务来确保JMS和JDBC都是原子操作.一般来说,当经常都是两个或多个连接的时候,你应考虑一下JTA/XA。例如,在我们例子中的JMS和JDBC. Spring本身不提供JTA事务;它需要一个JTA实现或者委派一个应用服务器来处理这个事务.在这里,我们使用了一个JTA实现,这个实现可以在任何J2SE平台上工作.
    最终架构如下面图3.白色方框代表我们的应用程序代码.

    image
    Figure 3. Architecture for the test

    如你所看到的,当我们执行我们的测试,将会发生下面的情况:
    1.        BankTest开始一个新事务.
    2.        然后,这个test在Spring运行期间获得一个BANK对象.这步触发Sping的创建和初始化过程.
    3.        这个test调用BANK的方法.
    4.        BANK调用datasource对象,通过它自己的setDataSource 方法从Spring 获取这个对像.
    5.        这个数据源是JTA-enabled,并且与JTA实现交互来注册当前事务.
    6.        JDBC statements和帐户数据库交互.
    7.        当方法返回时, test调用事务回滚.
    8.        JTA记得住datasource对象,会命令它进行回滚.

    第四步:添加声明式事务管理

    Spring允许添加声明式事务管理来管理java对象.假设我们想确认bank总是和一个有效的事务上下文一起被调用.我们通过在实际对象的上部配置一个proxy对象. Proxy和实际对象有相同接口,所以客户通过完全相同的方式使用它. 配置Proxy wrap每个BankDAO方法到事务中.结果配置文件如下. 不要被XML的庞大吓倒—大多数内容能通过复制和粘贴到你自己的工程中再使用.

    <?xml version="1.0" encoding="UTF-8"?>

    <beans>
        <!--
            Use a JTA-aware DataSource
            to access the DB transactionally
        -->
        <bean id="datasource"
            class="com.atomikos.jdbc.nonxa.NonXADataSourceBean">
            <property name="user">
                <value>sa</value>
            </property>
            <property name="url">
                <value>jdbc:hsqldb:SpringNonXADB</value>
            </property>
            <property name="driverClassName">
                <value>org.hsqldb.jdbcDriver</value>
            </property>
            <property name="poolSize">
                <value>1</value>
            </property>
            <property name="connectionTimeout">
                <value>60</value>
            </property>
        </bean>
        <!--
        Construct a TransactionManager,
        needed to configure Spring
        -->
        <bean id="jtaTransactionManager"
            class="com.atomikos.icatch.jta.UserTransactionManager"/>
        <!--
        Also configure a UserTransaction,
        needed to configure Spring  
        -->
        
        <bean id="jtaUserTransaction"
            class="com.atomikos.icatch.jta.UserTransactionImp"/>
        <!--
        Configure the Spring framework to use
        JTA transactions from the JTA provider
        -->
        <bean id="springTransactionManager"
        class="org.springframework.transaction.jta.JtaTransactionManager">
            <property name="transactionManager">
                <ref bean="jtaTransactionManager"/>
            </property>
            <property name="userTransaction">
                <ref bean="jtaUserTransaction"/>
            </property>
        </bean>
        <!-- Configure the bank to use our datasource -->
        <bean id="bankTarget" class="jdbc.Bank">
            <property name="dataSource">
                <ref bean="datasource"/>
            </property>
        </bean>
        <!--
        Configure Spring to insert
        JTA transaction logic for all methods
        -->
        <bean id="bank"
        class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
            <property name="transactionManager">
                <ref bean="springTransactionManager"/>
            </property>
            <property name="target">
                <ref bean="bankTarget"/>
            </property>
            <property name="transactionAttributes">
                <props>
                    <prop key="*">
                        PROPAGATION_REQUIRED, -Exception
                    </prop>
                </props>
            </property>
        </bean>
    </beans>


    这个XML文件告诉Spring去配置下面的对象:
    1.        需要通过JDBC连接的datasource.
    2.         添加jtaTransactionManager和jtaUserTransaction用于为JTA事务的Spring配置作准备.
    3.        springTransactionManager用于告诉Spring需要使用JTA.
    4.         BankDAO被重命名为bankTarget (因如下解释的原因).
    5.        bank对象被添加用于包装事务和bankTarget的所有方法.我们通过配置bank对象来使用

    springTransactionManager,这意味着所有事务将是JTA事务. 每个事务都被设置为PROPAGATION_REQUIRED,这将在任何异常下出现强制回滚.

    对这些对象包含的内容,你都可以很容易的复制和粘贴jtaTransactionManager, jtaUserTransaction, springTransactionManager到其他工程.其他的是应用程序相关的对象:datasource, bankTarget, bank. Bank对象很有趣:事实上对于bankTarget它是一个proxy;他们拥有相同的接口. Trick如下:当我们的应用程序请求Spring去配置和返回bank对象,Spring实际上将返回proxy(看起来和我们的应用程序完全相同),随后这个proxy将为我们开始/结束事务.这样,应用程序和Bank类本身都不需要知道JTA!图4阐述了在这步我们所得到的.  

    image
    Figure 4. Architecture with declarative JTA transactions in Spring

    现在的工作如下:
    1.        应用程序调用bank对象.这将触发Spring的初始化处理和返回proxy对象. 对应用程序而言,这个proxy行为和我们的Bank是一样的.
    2.        当bank的一个方法被调用, 这个调用将会通过proxy进行.
    3.        proxy使用springTransactionManager创建一个新事务.
    4.        springTransactionManager被配置为使用JTA,因些它委派到JTA.
    5.        调用被forward到Bank的实际对象,bankTarget.
    6.        bankTarget使用从Spring中得到的datasource.
    7.        datasource对事务进行注册.
    8.        通过规则的JDBC访问数据库.
    9.        在返回时, proxy终止事务:如果在先前的序列中没有发生异常,那么将会提交终止指令.否则,它将会被回滚.
    10.        transaction 管理器与数据库配合进行提交和回滚.

    在这步中的测试怎样进行?我们可重用BankTest 和它清晰的事务划分:因为PROPAGATION_REQUIRED, proxy将和在BankTest中创建的事务上下文一起执行.

    第五步:编写PROPAGATION_REQUIRED

    在这步,我们将添加JMS处理逻辑.为了做到这样,我们主要需要实现JMS MessageListener接口.我们也会添加公共的setBank方法使Spring的依赖注入起作用.源代码如下:

    package jms;
    import jdbc.Bank;
    import javax.jms.Message;
    import javax.jms.MapMessage;
    import javax.jms.MessageListener;

    public class MessageDrivenBank
    implements MessageListener
    {
        private Bank bank;

        public void setBank ( Bank bank )
        {
            this.bank = bank;
        }

        //this method can be private
        //since it is only needed within
        //this class
        private Bank getBank()
        {
            return this.bank;
        }

        public void onMessage ( Message msg )
        {
            try {
              MapMessage m = ( MapMessage ) msg;
              int account = m.getIntProperty ( "account" );
              int amount = m.getIntProperty ( "amount" );
              bank.withdraw ( account , amount );
              System.out.println ( "Withdraw of " +
              amount + " from account " + account );
            }
            catch ( Exception e ) {
              e.printStackTrace();
                
              //force rollback
              throw new RuntimeException (
              e.getMessage() );
            }
        }
        
    }



    第六步:配置MessageDrivenBank

    这里我们配置MessageDrivenBank去监听事务的QueueReceiverSessionPool.这样给我们可以实现和EJB(没有丢失信息和冗余信息)类似的消息机制,但在这里我们是用简单的POJO对象实现.当向pool中插入一个MessageListener,这个会话池将确保用JTA/XA事务接收到消息.结合JTA/XA-capable 的JDBC数据源,我们可以实现可靠的消息机制.  Spring的配置如下:

    <?xml version="1.0" encoding="UTF-8"?>


    <!--
            NOTE: no explicit transaction manager bean
            is necessary
            because the QueueReceiverSessionPool will
            start transactions by itself.
    -->
    <beans>
        <bean id="datasource"
            class="com.atomikos.jdbc.nonxa.NonXADataSourceBean">
            <property name="user">
                <value>sa</value>
            </property>
            <property name="url">
                <value>jdbc:hsqldb:SpringNonXADB</value>
            </property>
            <property name="driverClassName">
                <value>org.hsqldb.jdbcDriver</value>
            </property>
            <property name="poolSize">
                <value>1</value>
            </property>
            <property name="connectionTimeout">
                <value>60</value>
            </property>
        </bean>
        <bean id="xaFactory"
            class="org.activemq.ActiveMQXAConnectionFactory">
            <property name="brokerURL">
                <value>tcp://localhost:61616</value>
            </property>
        </bean>
        <bean id="queue"
            class="org.activemq.message.ActiveMQQueue">
            <property name="physicalName">
                <value>BANK_QUEUE</value>
            </property>
        </bean>
        <bean id="bank" class="jdbc.Bank">
            <property name="dataSource">
                <ref bean="datasource"/>
            </property>
        </bean>
        <bean id="messageDrivenBank"
            class="jms.MessageDrivenBank">
            <property name="bank">
                <ref bean="bank"/>
            </property>
        </bean>
        <bean id="queueConnectionFactoryBean"
            class="com.atomikos.jms.QueueConnectionFactoryBean">
            <property name="resourceName">
                <value>QUEUE_BROKER</value>
            </property>
            <property name="xaQueueConnectionFactory">
                <ref bean="xaFactory"/>
            </property>
        </bean>
        <bean id="queueReceiverSessionPool"
            class="com.atomikos.jms.QueueReceiverSessionPool"
            init-method="start">
            
            <property name="queueConnectionFactoryBean">
                <ref bean="queueConnectionFactoryBean"/>
            </property>
            <property name="transactionTimeout">
                <value>120</value>
            </property>
            <!--
            default license allows only limited
            concurrency so keep pool small
            -->
            <property name="poolSize">
                <value>1</value>
            </property>
            <property name="queue">
                <ref bean="queue"/>
            </property>
            <property name="messageListener">
                <ref bean="messageDrivenBank"/>
            </property>
        </bean>
    </beans>


    因为这篇文章需要一个便于安装的JMS服务,所以这里我们使用ActiveMQ.如果你正在使用另一个JMS实现,那么你将仍然能使用这部分提出的技术.接下来除了datasource和bank对象,我们将增加下面的对象定义:

    ·        xaFactory: 为建立JMS连接的connection工厂.
    ·        queue: queue代表我们将使用的JMS队列, 这个队列被配置成ActiveMQ要求的形式.
    ·        queueConnectionFactoryBean:一个JTA-aware的JMS连接器.
    ·        A queueReceiverSessionPool for JTA-enabled message consumption:注意:我们同时指定了用来调用的初始化方法(例:start);这是Spring的另一个特性. Start方法在session pool类里定义,它是在Spring配置文件中进行配置的.
    ·        messageDrivenBank:负责处理消息.

    你可以问问自己事务管理是在哪里进行的.事实上, 在先前部分被添加的对象已消失.为什么呢?因为我们现在使用QueueReceiverSessionPool来接收来自JMS的消息,并且这个类也为每次接收启动一个JTA事务.我们也可以保留JTA配置,另外添加JMS配置, 但是这样可能会使XML文件更长. 现在session pool类将担当事务管理角色.它和proxy方法的工作相似; 只是这个类需要JMS  MessageListener 为之添加事务. 通过这样配置,在每个消息收接之前程序将启动一个新事务 无论何时, 当我们的消息实例正常返回时, 这个事务将提交. 如果出现RuntimeException, 那么这个事务将回滚. 结构如下面图5(可以清淅地看到一些JMS对象).

    image
    Figure 5. Architecture for message-driven applications in Spring

    现在该架构工作如下:
    1.        应用程序调用bank对象和初始化数据库表.
    2.        应用程序queueReceiverSessionPool, 因此触发一个start方法的调用去监听到达的消息.
    3.        queueReceiverSessionPool在队列中侦察一个新消息.
    4.        queueReceiverSessionPool开始一个新事务,并且注册这个事务.
    5.        queueReceiverSessionPool调用已注册的MessageListener (messageDrivenBank).
    6.        这将触发对bank 对象的调用.
    7.        bank 对象通过datasource访问数据库.
    8.        datasource注册事务.
    9.        通过JDBC访问数据库.
    10.        当处理完成时, queueReceiverSessionPool会终止这个事务。然后进行commint(除非发生RuntimeException).
    11.        transaction manager开始消息队列的两阶段提交.
    12.        transaction manager开始数据库的两阶段提交.

    第七步:编写应用程序
    因为我们没有使用容器,我们仅仅提供一个Java应用程序就可以启动整个银行系统.我们的Java应用程序是非常简单: 它有能力找回配置的对象(Spring通过XML文件将他们放到一起). 这个应用程序能在任何兼容的JDK(Java Development Kit)上运行,并且不需要应用服务器.

    package jms;
    import java.io.FileInputStream;
    import java.io.InputStream;
    import org.springframework.beans.factory.xml.XmlBeanFactory;
    import com.atomikos.jms.QueueReceiverSessionPool;
    import jdbc.Bank;

    public class StartBank
    {
      public static void main ( String[] args )
      throws Exception
      {
        //open bean XML file
        InputStream is =
        new FileInputStream(args[0]);
        
        //the factory is Spring's entry point
        //for retrieving the configured
        //objects from the XML file
        XmlBeanFactory factory =
            new XmlBeanFactory(is);
        
        //retrieve the bank to initialize
        //alternatively, this could be done
        //in the XML configuration too
        Bank bank =
            ( Bank ) factory.getBean ( "bank" );
        
        //initialize the bank if needed
        bank.checkTables();

        //retrieve the pool;
        //this will also start the pool
        //as specified in the beans XML file
        //by the init-method attribute!

        QueueReceiverSessionPool pool  =
            ( QueueReceiverSessionPool )
            factory.getBean (
            "queueReceiverSessionPool" );

        //Alternatively, start pool here
        //(if not done in XML)
        //pool.start();

        System.out.println (
            "Bank is listening for messages..." );
            
      }
    }


    这就是J2EE!是不是认为J2EE也很容易呢?

    对通用性的考虑
    这部分里我们看看更多的概念,这些概念在许多J2EE应用程序中是很重要的.我们同样将看到对这些概念来说,一个应用服务器并不是必须的.

    集群和可扩展性
    健壮的企业应用程序需要集群来分流负担. 在消息驱动应用程序的例子中,这很容易:我们自动地从JMS应用程序继承得来处理能力.如果我们需要更强大的处理能力,那么我们只需增加更多连接相同JMS服务器的进程.一个对服务性能有效的衡量标准是在队列中停留的消息的数量. 在其他情况下,如基于web的 架构(如下)我们能很容易地使用web环境下的集群能力.

    方法级别的安全
    一个典型的观点是认为EJB能增加方法级的安全性.虽然并没有在这篇文章中提到,但是在Sping中配置方法级别的安全是可能的.这种配置类似于我们增加方法级的事务划分的方式.

    对非消息驱动的应用程序的通用性
    在不改变源代码的情况下(除了主应用程序类),我们使用的平台能很容易地被整合到任何J2EE web 应用服务器. 换句话说 , 通过JMS进行后台处理;这使得web服务器在面对后台处理的延迟问题上更可靠和更独立.在任何情况下, 为了实现容器管理的事务或容器管理的安全性,我们都不再需要依靠EJB容器来实现.

    关于容器管理持久化?
    存在并被很多开发者检验过的技术例如:JDO或者Hibernate 都不一定需要一个应用服务器. 另外,这些工具已经占据了持久化市场.

    结论
    今天,不需要应用服务器的J2EE已经成为可能,也很容易. 有人或许会说,没有应用程序服务器,有一些应用程序仍不能实现:例如,如果你需要一般的JCA(Java Connectivity API) 功能性, 那么我们上面提供的平台是不够的. 但是,这可能会发生改变,因为不用使用一个应用程序服务器进行开发,测式和部署的好处实在是太大. 人们越来越相信: 将来得J2EE是一个模块化的”选择你所需要”架构. 这与我们之前的完全基于应用服务器的方法相反. 在这样的情况下,J2EE开发者将从应用服务器和EJB中解放出来.

    资源:
    ·        代码下载
    ·        Guy Pardon's presentation on Transactions in Spring published at TheServerSide
    ·        More information on Atomikos Transactions and message-driven functionality without EJB
    ·        The home page of Spring
    ·        More information on JUnit
    ·        FirstSQL is an easy-to-install, XA-compliant DBMS
    ·        More information on HSQLDB
    ·        More information on ActiveMQ
    posted @ 2006-08-01 10:11 rendong 阅读(336) | 评论 (0)编辑 收藏