Morning@Weblog

7/7/2005

善用JUnit assert*

Filed under: — site admin @ 11:39 am

昨天看到一位同事的unit test是这么写的:

if (…) {
 assertTrue(false);
}
assertTrue(true);

抛开assertTrue(true)这个“无效”语句不说,怎么看怎么觉得别扭。在JUnit中,assert有很多“变体”,灵活使用将会给TestCase的结果验证带来很好的表达能力。它们包括:

assertTrue
assertFalse
assertEquals
assertNotNull
assertNull
assertSame
assertNotSame
fail

其中,每个assert*还有一个带有message的重载方法,比如:

assertEquals(String msg, Object arg1, Object arg2);

这表示,当assert失败时,会附带一段用户友好的说明性文字。

由此,对于上述的unit test,更好的书写方式应该是:

if (…) {
 fail("…");
}

而最好应该直接写成assertTrue(…) 或者assertFalse(…)。

想不到一个小小的assert也会引出问题来。

6/19/2005

JDK各版本发布时间及代号

Filed under: — site admin @ 5:42 pm

基本来自gigix的blog,并做了修改。

版本号 名称 中文名 发布日期
JDK 1.1.4 Sparkler 宝石 1997-09-12
JDK 1.1.5 Pumpkin 南瓜 1997-12-13
JDK 1.1.6 Abigail 阿比盖尔–女子名 1998-04-24
JDK 1.1.7 Brutus 布鲁图–古罗马政治家和将军 1998-09-28
JDK 1.1.8 Chelsea 切尔西–城市名 1999-04-08
J2SE 1.2 Playground 运动场 1998-12-04
J2SE 1.2.1 none 1999-03-30
J2SE 1.2.2 Cricket 蟋蟀 1999-07-08
J2SE 1.3 Kestrel 美洲红隼 2000-05-08
J2SE 1.3.1 Ladybird 瓢虫 2001-05-17
J2SE 1.4.0 Merlin 灰背隼 2002-02-13
J2SE 1.4.1 grasshopper 蚱蜢 2002-09-16
J2SE 1.4.2 Mantis 螳螂 2003-06-26
J2SE 5.0 (1.5.0) Tiger 老虎 2004-09-30
J2SE 5.1 (1.5.1) Dragonfly 蜻蜓 未知
J2SE 6.0 (1.6.0) Mustang 野马 预计2006年
J2SE 7.0 (1.7.0) Dolphin 海豚 未知

Sun JDK的发展历程

Filed under: — site admin @ 1:13 am

这几天忙于为《程序员》杂志的下一期专题“Java 10周年”撰文。查阅资料之余到是对Java的发展史,产生了浓厚的兴趣。包括早期Java诞生的传奇故事,以及Sun JDK的发展历程,……,一个个线索,一段段记忆的碎片,衔接在一起还真是别有一番意味在其中。

关于Sun JDK,大致可以分作如下几个阶段:
*1996年1月,JDK 1.0,JDK的首个版本。
*1996年12月,JDK 1.1,重写AWT(引入新的事件模型),JavaBean组件规范,inner class,math包
*1998年,JDK 1.2,Swing,Java 2D,Collection Framework
*1998-1999,JDK被从Java Development Kit更名为Java 2 SDK,Java技术被进一步分为J2SE,J2EE,J2ME三个方向
*2000年,J2SE 1.3,引入HotSpot技术
*2002年2月,J2SE 1.4,首个JCP参与的J2SE发布版本,assert,Java Plug-in,Web Start,DOM[level 2],SAX 2.0,JDBC 3.0
*2004年9月30日,J2SE 5.0,泛型,元数据,其他语法特性,线程库,JDBC等
*2006年,J2SE 6.0,(未知)

这里是每个时期的参考资料:
[JDK 1.0/1.1]
JDK 1.0 to JDK 1.1: Making the Transition Listening for Events
JDK 1.0 and Beyond: Making the Transition
Trail: JDK™ 1.1 — And Beyond!

[JDK 1.2]
JDK 1.2 Roadmap: New Features and Functionality
JDK 1.2 Roadmap: All Things New with JDK 1.2

[JDK 1.3]
Java 2 SDK, Standard Edition, version 1.3 Summary of New Features and Enhancements
Improving on Excellence

[JDK 1.4]
Java 2 Platform, Standard Edition, Version 1.4 Overview
Java 2 Platform, Standard Edition (J2SE), Version 1.4.1 Preview
A Roadmap for Java 2 Platform, Standard Edition (J2SE) 1.4.2 and 1.5

[JDK 1.5]
Tiger Roars:an Interview with Sun Microsystems Vice President Graham Hamilton
Tiger and Beyond, the Future of the Java Platform
From Mantis to Tiger
The All-New Java 2 Platform, Standard Edition (J2SE) 5.0 Platform:Programming with the New Language Features in J2SE 5.0

[Misc]
J2SE Naming and Versioning
J2SE 5.0 Name and Version Change
Sun’s Java HotSpot Performance Engine Hits the Spot
Trail Map Your guide to The Java Tutorial
DYNAMIC GENERATION FOR THE WEB: JavaServer Pages 1.0 Technology

如果要研究Java的历史,发现最好的地方是java的官方网站,尤其是Feature Stories About Java Technology
,上面记录了差不多自Java诞生以来,每年所发生的重大事件。

6/17/2005

Sitemesh Tutorial(1)

Filed under: — site admin @ 3:08 pm

为团队做的简单的Sitemesh培训提纲。

Introduction

sitemesh是opensymphony的一个子项目,旨在提高页面可维护性和复用性。它依据经典的Decorator Pattern,利用filter拦截http request/response,再为其加上“装饰”页面,从而达到基于模板结构的多页面复合视图的效果。与“JSP include”的区别在于,我们不需要在每个JSP页面中加入对页面模板布局的表达,被装饰页面所反应的内容正是它自己所关注的内容。

Hello World

1.WEB-INF/web.xml中加入filter定义:

<filter>
 <filter-name>sitemesh</filter-name>
 <filter-class>com.opensymphony.module.sitemesh.filter.PageFilter</filter-class>
</filter>
<filter-mapping>
 <filter-name>sitemesh</filter-name>
 <url-pattern>/*</url-pattern>
</filter-mapping>

2.WEB-INF/web.xml中加入sitemesh的tag lib定义:

<taglib>
 <taglib-uri>sitemesh-decorator</taglib-uri>
 <taglib-location>/WEB-INF/sitemesh-decorator.tld</taglib-location>
</taglib>
<taglib>
 <taglib-uri>sitemesh-page</taglib-uri>
 <taglib-location>/WEB-INF/sitemesh-page.tld</taglib-location>
</taglib>

3.将必需的jar, tld文件拷贝至相应目录:

sitemesh.jar,WEB-INF/lib
sitemesh-decorator.tld ,WEB-INF
sitemesh-page.tld,WEB-INF

4.创建WEB-INF/decorators.xml,配置装饰页面:

<decorators defaultdir="/decorators">
 <decorator name="main” page="main.jsp">
  <pattern>*</pattern>
 </decorator>
</decorators>

5.创建装饰页面:

<%@ page contentType="text/html; charset=GBK"%>
<%@ taglib uri="sitemesh-decorator” prefix="decorator” %>
<html>
 <head>
  <title><decorator:title /></title>
  <decorator:head />
 </head>
 <body>
  Hello World
  <hr />
  <decorator:body />
 </body>
</html>

6.创建被装饰页面:

<%@ page contentType="text/html; charset=GBK"%>
<html>
 <head>
  <title>Hello World</title>
 </head>
 <body>
  <p>Decorated page goes here.</p>
 </body>
</html>

7.部署至tomcat并启动,访问备装饰页面。

5/18/2005

ibatis使用心得——返回Map的Map

Filed under: — site admin @ 10:56 pm

一般而言,ibatis的sql map是通过JavaBean的属性与数据库表字段的映射来完成一些数据库存取的。而有些场合下,比如数据表本身较为简单,我们并不希望为此单独构造一个JavaBean而“污染”对象系统。此时,我们可以利用Map作为返回结果来代替JavaBean对象。见下面的sql map片段:

<resultMap id="getItemsResult” class="java.util.HashMap">
<result property="itemName” column="item_name” />
<result property="itemValue” column="item_value” />
</resultMap>
<select id="getItems” resultMap="getItemsResult">
select item_name, item_value from item_table
</select>

这里显式指定了一个resultMap,利用SqlMapClient的queryForList将会返回一个HashMap List,每个List元素都将是一个Map对象。

有时候,我们并不希望返回的是Object List,而希望返回某种形式的Map。无须更改sql map,我们可以利用SqlMapClient的queryForMap达到这一目的。此时,需要指定Map的key和value,见下面的代码片段:

getSqlMapClientTemplate().queryForMap("getItems", null, “itemName", “itemValue");

该函数的返回结果将是一个HashMap Map。这里,指定了key和value分别为itemName和itemValue,根据sql map中的定义,实际对应于item_name和item_value字段。也可以定义key为某个字段,而value为整个HashMap对象(或者JavaBean对象):

getSqlMapClientTemplate().queryForMap("getItems", null, “itemName");

此外,sql map还有提供一种隐式的resultMap(详见reference),但是经过试验,这种隐式resultMap在返回HashMap Map的时候,并没有得到正确结果,但显式的resultMap声明是没有问题的。

4/10/2005

Rod Johnson对Java领域未来关注的技术的看法

Filed under: — site admin @ 5:14 pm

Rod Johnson对Java领域未来关注的技术的看法:
- Inversion of Control and dependency injection design patterns;
- unit testing and TDD (test-driven development);
- O/R (object/relational) mapping;
- post struts 1.x Web technologies such as JavaServer Faces, Spring Model-View-Controller and Tapestry, and value-add Web technologies such as Apache Beehive;
- the rich client space;

http://www.eweek.com/article2/0,1759,1773154,00.asp

4/6/2005

一个并非无关痛痒的异常类的引入

Filed under: — site admin @ 9:27 pm

故事的起因是,最近,在我所负责的模块中时常于运行期间抛出NullPointException异常。经过仔细排查后,发现错误的源头位于某个业务层的controller中:

int public doOperation(int modelId) {
 Model model = dao.getModelById(modelId);
 model.getSomething();
 …
}

由于dao在根据给定的modelId查找数据库时没有发现相应记录,因此model取值为null,导致后续调用失败。进而发现,该dao对应于后台数据库中的一张表,该表从属于另一张主表。而当主表插入某条记录后(以modelId为主键),相应的,从表也会插入一条记录,且其主键即为modelId。从现象可以推得,两张表的一致性出现了问题——这是一个程序bug。

果不出所料,在涉及记录插入的相关代码中发现了导致数据不一致的漏洞——保持主从表一致的程序逻辑并未涵盖所有情况。

修改后的程序运行正常。但是,事情并没有结束。我依然对于整个系统心存疑虑,担心日后还会出现类似的数据不一致情形。是否应该考虑在dao获取model之后加上一个null值判断呢?很快,我打消了这个危险的念头。因为:首先,这是一个bug(数据不一致);其次,如果贸然吞掉异常,恐怕以后有关数据不一致的bug将会很难查找,这不是controller的责任;最后,如果一旦检测到null,doOperation方法的返回值将是很难定义的。因而,我还是选择了让异常抛出。不过,为了使异常更具informative,我将其做了转义,即:定义一个名为ModelNotFoundException的运行期异常。修改后的代码如下:

int public doOperation(int modelId) {
 Model model = dao.getModelById(modelId);
 if (null==model)
  throw new ModelNotFoundException(ErrorMessage);
 …
}

4/5/2005

Spring JPetStore学习小结(6)

Filed under: — site admin @ 9:39 am

※ 配置

以下是Spring JPetStore中使用的配置文件:

- web.xml

主要包含了如下内容:

定义log4j配置文件所在路径:

<context-param>
 <param-name>log4jConfigLocation</param-name>
 <param-value>/WEB-INF/log4j.properties</param-value>
</context-param>

定义application context配置文件的所在路径:

<context-param>
 <param-name>contextConfigLocation</param-name>
 <param-value>
  /WEB-INF/dataAccessContext-local.xml /WEB-INF/applicationContext.xml
 </param-value>
 <!–
 <param-value>
  /WEB-INF/dataAccessContext-jta.xml /WEB-INF/applicationContext.xml
 </param-value>
 –>
</context-param>

几个重要的servlet,以及servlet-mapping定义:

<servlet>
 <servlet-name>context</servlet-name>
 <servlet-class>org.springframework.web.context.ContextLoaderServlet</servlet-class>
 <load-on-startup>1</load-on-startup>
</servlet>
<servlet>
 <servlet-name>petstore</servlet-name>
 <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
 <load-on-startup>2</load-on-startup>
</servlet>
<servlet>
 <servlet-name>action</servlet-name>
 <servlet-class>org.apache.struts.action.ActionServlet</servlet-class>
 <load-on-startup>3</load-on-startup>
</servlet>
<servlet-mapping>
 <servlet-name>petstore</servlet-name>
 <!–
 <servlet-name>action</servlet-name>
 –>
 <url-pattern>*.do</url-pattern>
</servlet-mapping>

- applicationContext.xml

与应用相关的context配置信息,包含bean定义,以及对email和remoting的配置,主要涉及中间层,也是其他servlet-specific context的root。在代码中可以通过WebApplicationContextUtils.getWebApplicationContext()访问到:

在该文件以及后面的dataAccessContext-local中都引入了形如“${}”的属性,这些属性是单独定义在外部属性文件中的,需要利用PropertyPlaceholderConfigurer来读入,该bean也在applicationContext.xml中定义。

<bean id="propertyConfigurer” class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
 <property name="locations">
  <list>
   <value>WEB-INF/mail.properties</value>
   <value>WEB-INF/jdbc.properties</value>
  </list>
 </property>
</bean>

在声明事务时,applicationContext.xml文件中首先定义了一个名为baseTransactionProxy的bean,其中包含了有关事务的基本配置(对所有的insert方法和update方法使用PROPAGATION_REQUIRED,对其他方法使用PROPAGATION_REQUIRED,readOnly)。其abstract属性为true,作为parent bean被petStore bean所继承,并得到扩展。

<bean id="baseTransactionProxy” class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean”
 abstract="true">
 <property name="transactionManager"><ref bean="transactionManager"/></property>
 <property name="transactionAttributes">
  <props>
   <prop key="insert*">PROPAGATION_REQUIRED</prop>
   <prop key="update*">PROPAGATION_REQUIRED</prop>
   <prop key="*">PROPAGATION_REQUIRED,readOnly</prop>
  </props>
 </property>
</bean>
<bean id="petStore” parent="baseTransactionProxy">
 <property name="target">
  <bean class="org.springframework.samples.jpetstore.domain.logic.PetStoreImpl">
   <property name="accountDao"><ref bean="accountDao"/></property>
   <property name="categoryDao"><ref bean="categoryDao"/></property>
   <property name="productDao"><ref bean="productDao"/></property>
   <property name="itemDao"><ref bean="itemDao"/></property>
   <property name="orderDao"><ref bean="orderDao"/></property>
  </bean>
 </property>
</bean>

petSotre bean即对应于中间层的PetStoreImpl类,在其bean reference中所出现的Dao bean定义于dataAccessContext-local.xml中。

- dataAccessContext-local.xml/dataAccessContext-jta.xml

Spring JPetStore将对Dao bean的定义单独放到了一个文件中,这种按层分文件来描述context的方式值得借鉴。local后缀的文件用于单一数据库场景,jta后缀的文件用于多数据库场景。以dataAccessContext-local为例,除了Dao bean定义,该文件中还包括了dataSource的定义,所用的是apache的DBCP:

<bean id="dataSource” class="org.apache.commons.dbcp.BasicDataSource” destroy-method="close">
 <property name="driverClassName"><value>${jdbc.driverClassName}</value></property>
 <property name="url"><value>${jdbc.url}</value></property>
 <property name="username"><value>${jdbc.username}</value></property>
 <property name="password"><value>${jdbc.password}</value></property>
</bean>

另外,还有关于ibatis sql map的配置信息:

<bean id="sqlMapClient” class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
 <property name="configLocation"><value>WEB-INF/sql-map-config.xml</value></property>
 <property name="dataSource"><ref local="dataSource"/></property>
</bean>

这里的sql-map-config.xml文件中包含了所有ibatis sql map文件所在的物理位置。

- petstore-servlet.xml

Spring web MVC的Web层context配置文件,依然是有关bean的定义,主要是Controller,定义了url与class的映射,以及一些属性,包括:对petStore bean的引用,successView,validator,viewName等。另外,viewResolver bean定义了view所在的位置和扩展名,以及对应的viewClass:

<bean id="viewResolver” class="org.springframework.web.servlet.view.InternalResourceViewResolver">
 <property name="viewClass"><value>org.springframework.web.servlet.view.JstlView</value></property>
 <property name="prefix"><value>/WEB-INF/jsp/spring/</value></property>
 <property name="suffix"><value>.jsp</value></property>
</bean>

4/4/2005

Spring JPetStore学习小结(5)

Filed under: — site admin @ 9:34 am

※ Web层(续2)——分页机制

在Spring JPetStore中,为列表提供了简单的分页处理,这里使用了Spring的helper class:PagedListHolder。它虽是用于Web UI,不过实际上是针对bean list的维护,因此位于org.springframework.beans.support包中。在Web层中,PagedListHolder的实例会被当作session属性传递,然后在jsp页面中以model获取。

PagedListHolder itemList = new PagedListHolder(this.petStore.getItemListByProduct(productId));
itemList.setPageSize(4);
Product product = this.petStore.getProduct(productId);
request.getSession().setAttribute("ViewProductAction_itemList", itemList);
request.getSession().setAttribute("ViewProductAction_product", product);
model.put("itemList", itemList);
model.put("product", product);

在PageListHolder中提供了很多分页时经常用到的方法:isFirstPage、isLastPage、previousPage、nextPage、getPageList等。通过设置其成员变量sort,PageListHolder也支持排序,该成员变量是一个SortDefinition接口的实现类,缺省使用的是MutableSortDefinition。实际上MutableSortDefinition只不过维护了有关排序的一些状态信息,比如:以那个属性排序、降序还是升序、是否忽略大小写等。而真正的排序则是由PropertyComparator完成的,该类实现了java.util.Comparator接口,用于根据指定的bean property来比较两个bean的先后顺序。在PageListHolder的resort方法中调用了PropertyComparator的静态方法sort。

PagedHolderList提供了对不可更新的bean list的分页支持,如果需要处理可更新的bean list,可以使用RefreshablePagedListHolder。RefreshablePagedListHolder是PagedListHolder的子类,具备reloading功能。调用其refresh方法,能够根据Locale和filter的更新情况自动实现数据的reloading。为了让RefreshablePagedListHolder能够成功reload数据,我们还需要编写一个PagedListSourceProvider接口的实现类,因为RefreshablePagedListHolder会调用该接口的loadList方法。

Spring的org.springframework.beans.support包为Web分页机制提供了统一的解决方案,使用起来十分方便。不过,需要指出的是,使用PagedHolderList/RefreshablePagedHolderList的隐含前提是,你需要将后台数据表中的所有数据悉数全部取出来,然后再交由它们进行分页处理。

4/1/2005

Spring JPetStore学习小结(4)

Filed under: — site admin @ 10:11 am

※ Web层(续)

在众多Controller当中,有两个Controller比较特殊,它们涉及表单处理,分别是OrderFormController和AccountFormController,前者派生自AbstractWizardFormController,后者派生自SimpleFormController,而它们的父类则同为AbstractFormController。Spring Web MVC对一般的Form处理,从流程(workflow)的角度做了分类(form workflow),并以抽象类的形式封装与框架代码中。像OrderFormController和AccountFormController即是从这些抽象类中扩展派生而来,它们对部分callback方法做了覆盖。这是一个典型的Template Method Pattern的运用。当然,这也使得Web层的应用代码严重依赖于Spring Web MVC框架本身。

以SimpleFormController为例,从其核心方法之一processFormSubmission的实现代码中可以看出大致的处理流程:

protected ModelAndView processFormSubmission(
  HttpServletRequest request, HttpServletResponse response,
  Object command, BindException errors)
  throws Exception {
 if (errors.hasErrors() || isFormChangeRequest(request)) {
  return showForm(request, response, errors);
 }
 else {
  return onSubmit(request, response, command, errors);
 }
}

在表单验证之后,如果用户输入有误,或者表单需要再次刷新,则会重新导向当前表单页面,否则说明一切正常,调用onSubmit方法。onSubmit最终会调用doSubmitAction方法,然后调用getSuccessView获得后继视图的标识,配合errors.getModel(),组装成ModelAndView返回。而这里的doSubmitAction则是一个需要派生类覆盖的protected方法。

与上述Controller配套的还有两个Form Object:OrderForm,AccountForm,其所包含的实例变量对应于表单字段。

另一个值得一提的是拦截器SignOnInterceptor,它对部分用户操作实施了认证保护。代码实现逻辑大致如下:

public boolean preHandle(
  HttpServletRequest request, HttpServletResponse response, Object handler)
  throws Exception {
 UserSession userSession =
  (UserSession) WebUtils.getSessionAttribute(request, “userSession");
 if (userSession == null) {
  ModelAndView modelAndView = new ModelAndView("SignonForm");
  throw new ModelAndViewDefiningException(modelAndView);
 }
 else {
  return true;
 }
}

这里的UserSession是一个封装了包括帐号在内的用户信息的helper class。它被当作session属性在Web层的不同对象之间传递。在petstore-servlets.xml文件中有如下一段配置信息:

<bean id="secureHandlerMapping”
 class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
 <property name="interceptors">
  <list>
   <ref bean="signonInterceptor"/>
  </list>
 </property>
 <property name="urlMap">
  <map>
   <entry key="/shop/editAccount.do"><ref local="secure_editAccount"/></entry>
   …
  </map>
 </property>
</bean>
<bean id="signonInterceptor”
 class="org.springframework.samples.jpetstore.web.spring.SignonInterceptor"/>
<bean id="secure_editAccount”
 class="org.springframework.samples.jpetstore.web.spring.AccountFormController">
 …
</bean>

由此,我们可以看到,SignonInterceptor的preHandle方法,将会在AccountFormController执行之前被调用。而当userSession为空时,则会被当作非法操作抛出ModelAndViewDefiningException异常,否则AccountFormController将会顺利执行。至于如何处理ModelAndViewDefiningException,则要“上溯”到Spring Web MVC的核心请求处理类:org.springframework.web.servlet.DispatcherServlet。在该Servlet的doDispatch方法中有如下一段代码:

try {
 …
}
catch (ModelAndViewDefiningException ex) {
 mv = ex.getModelAndView();
}
if (mv != null && !mv.isEmpty()) {
 render(mv, processedRequest, response);
}

由于ModelAndViewDefiningException中包含了出错以后的后继视图标识,因此最终将会导向该视图(在本例中,它导向用户登录页面)。

3/31/2005

Spring JPetStore学习小结(3)

Filed under: — site admin @ 10:19 am

※ Web层

Web层是Spring JPetStore相对最为复杂的地方。Spring JPetStore同时支持了两种Web Framework:Struts和Spring Web MVC,分别位于org.springframework.samples.jpetstore.web.struts和org.springframework.samples.jpetstore.web.spring这两个包内。两者的切换只需要修改一下web.xml文件的相应配置即可。如果选择Spring Web MVC,则:

<servlet>
<servlet-name>petstore</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>2</load-on-startup>
</servlet>

DispatcherServlet是Spring Web MVC中负责请求调度的核心引擎,通过内嵌的<init-param>节点可以为其配置名为“contextConfigLocation”的
参数,该参数指定了Spring专属的Application Context配置文件的位置。如果忽略此设定,则默认为“/WEB-INF/<servlet name>-servlet.xml”,其中<servlet name>以此处的Servlet名替换(比如:petstore-servlet.xml)

在org.springframework.samples.jpetstore.web.spring包中,绝大部分类属于Controller,它们均维护了一个PetStoreFacade的实例变量,通过PetStoreFacade来访问业务层的功能。利用Spring的配置文件petstore-servlets.xml,可以将PetStoreFacade的实现类(PetStoreImpl)通过reference bean的方式“注入”到各个Controller中。

这些Controller几乎都实现自org.springframework.web.servlet.mvc.Controller接口,该接口只有一个抽象方法:
ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response);
handleRequest的处理逻辑大致是:
- 从request中获取信息(并没有什么特别的,一切数据都是通过request以及session在众多jsp页面和controller间传递的)
- 调用相应的petStore业务方法(处理Cart相关的Controller例外)
- 装配并返回ModelAndView实例
- 由于可以在handleRequest中直接操纵response,因此另一种返回形式是调用reponse.sendRedirect

从中可以看出,这是与Servlet API紧密耦合的。

3/29/2005

Spring JPetStore学习小结(2)

Filed under: — site admin @ 9:51 am

※ 数据访问层

在这一层里,所有的Dao类由于使用了ibatis,所以代码显得格外干净。实际上,绝大部分逻辑都被搬到了外部的sql map文件里,这些文件位于org.springframework.samples.jpetstore.dao.ibatis.maps包下。值得一提的是,JPetStore对于Sequence的处理。JPetStore专门定义了一个Sequence的sql map,里面分别针对普通情况和Oracle数据库做了单独配置。

通常情况下是在数据库中单独维护一张代表sequence的表,使用时首先利用“getSequence”获取对应name的nextid,然后代码实现累加1,完成后再利用“updateSequence”更新sequence表。以下是Sequence.xml中的相关定义:

<select id="getSequence” resultMap="result">
select name, nextid from sequence where name = #name#
</select>
<update id="updateSequence">
update sequence set nextid = #nextId# where name = #name#
</update>

在SqlMapSequenceDao中的getNextId方法实现了上述的代码处理逻辑:

public int getNextId(String name) throws DataAccessException {
Sequence sequence = new Sequence(name, -1);
sequence =
(Sequence) getSqlMapClientTemplate().queryForObject("getSequence", sequence);
// …
Object parameterObject = new Sequence(name, sequence.getNextId() + 1);
getSqlMapClientTemplate().update("updateSequence", parameterObject, 1);
return sequence.getNextId();
}

而对于Oracle,则直接利用其sequence功能,相应的sql map定义如下:

<select id="oracleSequence” resultMap="result">
select ‘$name$’ as name, $name$.nextval as nextid from dual
</select>

另有一个OracleSequenceDao实现,其getNextId方法如下:

public int getNextId(String name) throws DataAccessException {
Sequence sequence = new Sequence();
sequence.setName(name);
sequence = (Sequence) getSqlMapClientTemplate().queryForObject("oracleSequence", sequence);
return sequence.getNextId();
}

包括SqlMapSequenceDao在内的所有Dao类都使用了SqlMapClientDaoSupport作为基类,同时,多数Dao类实现相应的Dao接口。Dao实现类中会调用SqlMapClientDaoSupport提供了getSqlMapClientTemplate方法。这样就可以很方便的从sql map文件中读取sql配置,而无需将sql语句hard code到代码中了。spring为ibatis提供了一层简单的封装,然后将自底层抛出的异常转义为spring框架所定义的runtime异常DataAccessException,可以在上层视情况决定是否catch该异常。

所有的Dao接口都位于org.springframework.samples.jpetstore.dao包中,而所有的ibatis实现类都位于org.springframework.samples.jpetstore.dao.ibatis包中。

3/28/2005

Spring JPetStore学习小结(1)

Filed under: — site admin @ 11:23 am

※ 业务层

主要的类和接口位于org.springframework.samples.jpetstore.domain.logic包中。业务层其实很简单,主要是一个PetStoreFacade接口,该接口在JPetStore中只有一个唯一的实现类PetStoreImpl,它提供了很多供Web层调用的方法,而绝大多数方法都只是简单的调用了数据访问层的Dao类所提供的方法。

domain.logic包中还有两个供Web层访问的validator,AccountValidator和OrderValidator,它们实现自spring的Validator接口,由于使用了spring提供的helper class ValidationUtils,因此看起来也十分简洁。通过在servlet配置文件中为AccountFormController和AccountFormController指定validator属性,从而为Account和Order提供了验证功能。不过在这里,这些Validator无疑是依赖于spring框架的。

<bean name="/shop/newAccount.do”
class="org.springframework.samples.jpetstore.web.spring.AccountFormController">
<property name="petStore"><ref bean="petStore"/></property>
<property name="validator"><ref bean="accountValidator"/></property>
<property name="successView"><value>index</value></property>
</bean>
<bean id="secure_editAccount”
class="org.springframework.samples.jpetstore.web.spring.AccountFormController">
<property name="petStore"><ref bean="petStore"/></property>
<property name="validator"><ref bean="accountValidator"/></property>
<property name="successView"><value>index</value></property>
</bean>

另外,还有一个AOP advice,SendOrderConfirmationEmailAdvice,用于在完成一条order的数据库插入之后,向用户发送一封确认邮件,相应的配置位于applicationContext.xml中。

<bean id="mailSender”
class="org.springframework.mail.javamail.JavaMailSenderImpl">
<property name="host"><value>${mail.host}</value></property>
</bean>
<bean id="emailAdvice”
class="org.springframework.samples.jpetstore.domain.logic.SendOrderConfirmationEmailAdvice">
<property name="mailSender"><ref local="mailSender"/></property>
</bean>
<bean id="emailAdvisor”
class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
<constructor-arg><ref local="emailAdvice"/></constructor-arg>
<property name="pattern"><value>.*insertOrder.*</value></property>
</bean>

在另一个包org.springframework.samples.jpetstore.domain中,包含了在整个JPetStore各个层中都会使用到的domain object。这些基本的model类,除了Cart,在数据访问层均有对应的Dao类,而由于Cart仅作为在Web层内部传递的model,因此不需要持久化。

3/11/2005

基于Spring template的dao及其单元测试的一般模式

Filed under: — site admin @ 11:14 am

利用Spring的template,比如:JdbcTemplate,可以大大简化dao代码的实现。不过template要求数据源作为传入参数,因此可以将data source做为dao类的member,然后使用setter由外部传入:

public XXXDbDaoImpl implements XXXDao {
DataSource dataSource;
public void setDataSource(DataSource dataSource)
{
this.dataSource = dataSource;
}
// dao methods …
}

这样的dao实现类简洁清晰,同时也有助于以后进一步利用IoC来完成容器管理的data source配置。不过此前,可以先定义一个dao的factory作为过度,由factory来完成对data source设置的封装。相应的,单元测试可以利用spring的DriverManagerDataSource来手工配置data source:

public class XXXDaoTest extends TestCase {
protected void setUp() throws Exception {
DriverManagerDataSource ds = new DriverManagerDataSource();
ds.setDriverClassName(TestConfig.getDbDriver());
ds.setUrl(TestConfig.getDbUrl());
ds.setUsername(TestConfig.getDbUsername());
ds.setPassword(TestConfig.getDbPassword());
dao.setDataSource(ds);
// other setup …
}
// test methods …
}

1/17/2005

当DBUnit遇到了Oracle Clob

Filed under: — site admin @ 3:03 pm

在使用DBUnit实现数据库数据初始化的时候遇到了一个问题。在向Oracle的数据库表空间中插入记录时由于存在Clob字段,而报出类似如下的SQLException:数据大小超出此类型的最大值:***。

查看DBUnit的源代码之后发现,这是由于DBUnit在做数据库插入时,缺省使用的是其org.dbunit.dataset.datatype包中的ClobDataType,而该类使用的是“一般性”方法:调用PreparedStatement的setObject,并给定SQL类型为LONGVARCHAR。但是这样的方法在遇到Oracle的Clob时是行不通的。

后来发现DBUnit在其扩展包org.dbunit.ext.oracle中提供了一组类,以支持Oracle的Clob/Blob类型。对于Clob类型的处理,其基本思路是:
- 利用oracle.sql.CLOB的createTemporary方法先创建一个临时Clob对象
- 以可读写模式打开临时对象,并利用getCharacterOutputStream获取Writer实例
- 把值写入临时对象中
- 关闭Writer实例和临时对象
- 利用PreparedStatement的setObject将临时对象“塞”入数据库
使用时需要将原先的DatabaseConnection替换成OracleConnection。并且由于使用了Java反射机制,在编译test case时,并不需要Oracle的jdbc driver。

可我在实际使用时却又遇到了新的问题,test case在运行期间报出了createTemporary方法的NoSuchMethodException。去Oracle的CLOB类中一查,发现确实没有此方法,于是恍然大悟:由于我用的是Oracle 8的thin driver,应该是没有createTemporary的。这就只能使用传统的处理Clob数据的方法了:
- 插入一条Clob数据为空值的记录(用empty_clob函数)
- 在将auto commit设置为false之后,通过select for update选出插入的记录
- 为选出的结果集中的clob设置取值,然后update

原本想修改DBUnit的实现代码,不过后来发现这种“三步走”的方法与DBUnit外围的框架代码是向背的,希望通过效仿org.dbunit.ext.oracle的扩展方法来达到同样的目的是行不通的,根本原因在于OracleClobDataType类中可做的扩展及其有限,包括插入数据在内的大部分事情都由外围框架代劳了(并且是一个批操作)。因此,最后决定放弃这种无谓的努力,要么换成Oracle 9i,或者使用其他的数据库,比如mysql。

12/14/2004

JspWiki的小波折

Filed under: — site admin @ 10:23 am

上个星期差点被JspWiki折腾的不行,主要是由于某些原因导致的JVM资源耗尽,使得后台报出OutOfMemory错误。其实以前也有过类似现象,用着用着浏览器就变得特慢,甚至没反映了,只是当时没怎么在意,觉得重启一下tomcat就可以了。

虽然现在还不能肯定,但最有可能的原因是cache造成的。由于,JspWiki缺省使用的是文件系统管理wiki的,因此为了提高性能,默认会将访问的页面作为缓存置于内存当中。但是随着wiki规模的不断壮大,缓存所占空间就会越来越大(team所使用的wiki服务器只有区区256M)。实际上,每次重新启动tomcat的时候,首次访问wiki总是很慢,虽然这部分代码我还没有仔细看过,不过我猜想应该就是在“重建”缓存信息,包括页面的交叉引用,JspWiki会将某些临时信息放到temp目录下,包括lucene的index信息,这样即使系统重启了,信息依然可以再次被加载。

尽管上回曾经为了这个,把JAVA_OPTS调成了:-Xms128m -Xmx256m,可总觉得不牢靠,果然没过多久就又出问题了。另外,不知道采用VersioningFileProvider是否也有关系,因为我有个“坏”毛病,总喜欢不断的写不断的保存,结果文件目录下面就会产生无数中间结果,不知道这是否也是导致wiki运行变慢的原因。

总之,为了不致再次OutOfMemory,我在目前力所能及的范围内做了最大的努力,比如:通过jspwiki.properties的相关配置关掉了cache,去除了文件历史跟踪记录,取消某些耗时的操作,等等。经过一番折腾,现在wiki到还能基本正常可用,访问起来似乎并不慢。也许最初就应该用数据库存储方式。

唯一的遗憾是,在备份数据文件时,由于一个小小的失误,以前大家写的wiki页面文件的时间信息都丢失了。主要是由于linux下的文件拷贝会将副本文件的时间改为拷贝的时间,而不是原文件的时间,只有mv命令才会保留原有文件时间。这一点似乎与windows不同。

P.S. 前段时间研究和修改JspWiki还是花了些时间的,本来可以写点心得的,只可惜当时不在状态,现在就更没这个兴致去写这些东东了。

12/13/2004

Webwork解读之一

Filed under: — site admin @ 9:52 pm

ServletDispacher>>

众所周知,作为webwork2中的MVC架构的controller,ServletDispacher自然是一个十分重要的类,它负责将到达的request转译成相应的action。其核心方法是service(),当一个http请求到达时,它将依序做如下处理:

- 将multipart request包装成一个单一的HttpServletRequest
- 为调用serviceAction方法准备传入的参数:
 - 利用getNamespaceFromServletPath从servlet path中获得namespace,比如:从/foo/bar/MyAction.action中得到/foo/bar。(getNameSpace)
 - 利用getAction从servlet path中获得action的名称,比如:从MyAction.action中得到MyAction。(getActionName)
 - 将HttpServletRequest的attribute-value对封装成一个RequestMap对象,该对象保存了一个hash set,可以通过其entrySet方法进行访问。(getRequestMap)
 - 将HttpServletRequest的session包装成一个SessionMap对象,该对象保存了一个包含key-value对的hash set,可以通过其entrySet方法进行访问。( getSessionMap)
 - 简单的调用HttpServletRequest的getParameterMap方法。(getParameterMap)
 - 返回一个ApplicationMap对象,该类的entrySet方法返回一个包含了全部app. attr.的hash set(getApplicationMap)
- 调用serviceAction,加载并执行action:
 - 利用createContextMap,将前面的那些参数(包含了request、response、parameters、session、application properties),还有一些其他属性,设置到一个hash map中,以创建一个action context(extraContext),并把它自己也设置进去:

extraContext.put(SERLVET_DISPATCHER, this);

 - 创建ActionProxy,并传入namespace、action name、extra context,并执行ActionProxy的execute方法。

ActionProxy是xwork的一个部件,它对action做了wrap,根据xwork.xml配置文件,它会找到相应的action class,并调用actoin的excute方法。

此外,在ServletDispacher的init方法中还做了一些初始化工作,主要包含对velocity引擎的初始化,以及三个参数的初始化,它们都可以在webwork.properties文件中指定,由com.opensymphony.webwork.config.Configuration
加载:
webwork.configuration.xml.reload,若为true,包括action定义、interceptor定义的xml配置文件,将会在每次request请求时都加载,适合于调试阶段;
webwork.multipart.saveDir,上传文件的临时路径,缺省为javax.servlet.context.tempdir;
webwork.multipart.maxSize,上传文件的最大字节数,缺省为java.lang.Integer.MAX_VALUE;

从ServletDispacher的实现,我们还可以得出两点结论:
*针对每次request的响应,新的Action对象都将会被创建。
*为什么缺省时,webwork要求以*.action作为url pattern。

12/7/2004

JBoss和Tomcat的端口冲突

Filed under: — site admin @ 4:37 pm

一直在困惑于奇怪的JBoss和Tomcat的端口冲突问题。不得已,今天终于决定不改动Jboss的设置了,因为它配起来比Tomcat麻烦。于是,我开始拿Tomcat的server.xml开刀,经过修改的Tomcat端口包括:AJP port(8009->8010);Shutdown port(8005->8006);HTTP port(8080->8000)。目前使用无任何异常,希望不会再有问题。

P.S. 这回修改端口的时候,发现Tomcat的8005是用来帧听shutdown的,而JBoss好像也拿它来干这个事情。于是联想到以前总是遇到Jboss不能正常关闭的现象,估计与此有关。也许经过这次调整,就不用强行kill Jboss的线程了,不过这个还有待验证。

10/19/2004

一个Jsp Tag的错误

Filed under: — site admin @ 10:39 am

Tomcat在解析Jsp页面时报错如下:
According to TLD or attribute directive in tag file, attribute page does not accept any expressions

说的是对应tag的属性不支持表达式传入。解决的办法是,在tld文件的相应tag的相应属性中加上rtexprvalue属性并设置为true,比如:

<tag>
 <name>CustomTag</name>
 ……
 <attribute>
  <name>AttName</name>
  <rtexprvalue>true</rtexprvalue>
 </attribute>
</tag>

网上说这个和Tomcat的版本也有关系(5.x),我用的恰好是5.0.27,不过这个没有证实过。

8/26/2004

成功实现了Eclipse+lomboz+Jboss的EJB/Servlet开发

Filed under: — site admin @ 11:56 am

终于试验通过了在Eclipse下用lomboz结合Jboss的EJB和Servlet开发。

换了一个Eclispe3.0还真是很管用,以前在2.1上配置时,一直存在classpath找不到的错误,如今也迎刃而解了。由此也可看到,Eclipse与其插件以及服务器的版本不兼容问题还是很麻烦的。

现在我用的版本分别是:
- JBoss 3.2.5
- Eclispe 3.0.o
- Lomboz 3.0.1
(more…)

8/23/2004

UML view of Java archieves

Filed under: — site admin @ 4:08 pm

这是一个摘自《Java Development with Ant》的Java archieves的UML图,很直观,也很贴切:WAR文件和JAR文件是JAR文件的子类,而JAR文件自身则是ZIP文件的子类。WAR文件可以包含JAR文件,EAR则可以包含JAR和WAR。JAR文件通常包含一个manifest文件,以及一些编译过了的class文件。图中省略的一点实事是,Zip和经过gzip的tar文件通常也被用来发布JAR、WAR和EAR文件。 (more…)

8/18/2004

原来蚂蚁也是庞然大物

Filed under: — site admin @ 2:22 pm

乍一看,ant的target/task概念十分的简洁,可是当你开始深入了解其背后的“语法”时,那简直是一幅让人发怵的情景——一个何其庞大的体系。

想想三年前的ant,是多么的简洁(或者说简陋更合适些)。可现如今,随着不同的tasks,不同的datatypes,不同的properties被引入,在build.xml的表达能力极度丰富的同时,其语法体系也极度膨胀了。有时候,尤其对一个不十分熟练的newbie而言,dir、srcdir、destdir的幻化有如移形大法般让人摸不着头脑,更不用说那些刚刚引入不久的新特性了。

不明白这种复杂化是否真的有必要。诚然,功能是强大了,可学习的门槛又提高了(这让我想起了struts里的tag library);而与此同时,在多数情况下或许我们对ant的使用就仅仅是日常部分罢了。换句话说,我们对ant的80%的使用只涵盖了其功能集的20%。这是一个老话题了。一个典型的例子是所谓的“regexp mapper”,引入了貌似功能强大正则表达式,可这种mapper的使用会带来极大的复杂性,而现实中又有多少可以真正让regexp mapper一展身手的机会呢?

8/12/2004

顺利实现Eclipse+Tomcat下Jsp的调试

Filed under: — site admin @ 10:14 pm

终于可以在Eclipse+Tomcat下顺利调试Jsp了。下午顺利实现Tomcat上的特定配置,即:定义一个workDir为j2src的Context:

<Context path="/helloWorld" debug="0" reloadable="true"
  docBase="C:/eclipse/workspace/helloWorldJsp/helloWorld"
  workDir="C:/eclipse/workspace/helloWorldJsp/j2src">
</Context>

(more…)

jdocs - TSS上的一则新闻

Filed under: — site admin @ 11:05 am

http://www.theserverside.com/news/thread.tss?thread_id=27998

http://www.jdocs.com,作为javadoc APIs的网上仓库,看起来确实是个不错的主意。目前已经有了50个大大小小的流行的Projects了,还在继续中。

除了有待完善的APIs搜索外,还提供了用户提交Comments的功能,这个很像wiki,每个人都可以为这个网上仓库贡献自己的绵薄之力。另一个不错的建议是:提供配套的Samples的搜索,以及网上运行Samples的功能。

也许这是未来的Java Community的Kowledge Center,不知道能热乎多久,拭目以待。

8/11/2004

又一个Eclipse+Tomcat的问题

Filed under: — site admin @ 11:47 pm

有是一个莫名其妙的错误,在eclipse中成功启动了Tomcat,可是在Browser里输入http://localhost:8080之后,IDE中的Console蹦出了一串异常:

java.lang.NoClassDefFoundError: org/apache/commons/el/ExpressionEvaluatorImpl
at org.apache.jasper.runtime.JspFactoryImpl.internalGetPageContext(JspFactoryImpl.java:144)


于是效仿几天前的做法,在网上搜索了一番,果然没有让我失望,也有人曾经遇到和我一样的困惑,虽然所提供的解决方法,在我的环境里并没有成功,但这启发了我。既然是NoClassDefFoundError,那我就顺藤摸瓜,找到了该问题类所在的文件:common/lib下的commons-el.jar。然后在Windows->Preference->Lomboz->Server Definitions的对应处添加了该jar包。果然,问题解决了。

8/10/2004

How to get column name in Java?

Filed under: — site admin @ 5:10 pm

同学问我如何获取数据库中字段的名称,结果还是他自己找到了答案。后来在网上查了资料,现把它诸于此,以备后查:

// sample codes: display the whole column names of a result set
ResultSet rs = stmt.getResultSet()
ResultSetMetaData rsmd = rs.getMetaData();

int columeCount = rsmd.getColumnCount() for (i = 1; i < = columnCount; i ++) { System.out.println(rsmd.getColumnName()) }

8/8/2004

Eclipse中启动Tomcat失败的问题

Filed under: — site admin @ 11:11 pm

周末一直在为一件事情苦恼,本想试验一下在Eclispe中调试Jsp,却遇到了Tomcat启动失败的错误。在命令行下运行一切正常,可在IDE下就是不行。虽然搜索了很多网上介绍用Eclispe结合Tomcat的文章,可是大都泛泛而谈,隔靴搔痒,走个过场也就结束了。另外,这些文章所使用的环境版本也不尽相同,我所使用的是:Eclispe 2.1 + Lomboz 2.1 + Tomcat 5.0.27。

好在今天突然想到用错误信息作为google的搜索关键字,结果问题迎刃而解,原来是Tomcat版本的问题,如果使用5.0.24就不会出现类似错误,不过另一种稍微高明一点的办法是修改lobmoz的plugins下的tomcat50x.server文件,把该文件中的startWorkingDirectory和stopWorkingDirectory一项中的-Djava.endorsed.dirs调整为:"${serverRootDirectory}/common/endorsed"。这个解决办法的思路来自于Tomcat的bin目录下的启动脚本catalina.bat

完整的解决方法可以在objectweb的论坛里找到。

看来搜索的技巧也是很关键的:)

7/29/2004

ant学习的一点收获

Filed under: — site admin @ 3:02 pm

记得三年前就听说过ant,当初还曾简单的使用过,不过时隔3年,印象已然不深。昨天重新学习ant,在huihoo找到了一篇介绍ant使用方法的文章(可惜年代久远),在对ant的使用有所了解之余,还有另一番收获。 (more…)

7/28/2004

A very stupid mistake

Filed under: — site admin @ 3:09 pm

一直声称“要转向java领域”,不过今天算是碰了一个不大不小的钉子,让我汗颜了一把。

这大概可以算是我n年来运行的第一个java程序了(以前也摆弄过一段时间的java),我还记得要准确设置JAVA_HOME和classpath的,不过编译通过后,却遇到了一个莫名其妙的java.lang.NoClassDefFoundError,好在网络无极限,google一搜就可以找到解决方法。

原来缺省时classpath的取值为“.”,亦即当前目录,在手工设置classpath之后,务必不要忘记在所有路径之前添加“.;”,比如:
.;%JAVA_HOME%lib;%JAVA_HOME%libtools.jar;
否则,即使.class就在你眼皮底下java也依然会熟视无睹的。

That’s my mistake, a very stupid mistake.

Powered by WordPress