stone2083

2008年11月5日 #

ibatis支持枚举类型

很多应用中,数据库表结构都会存在一些状态字段。在关系性数据库中,一般会用VARCHAR类型。使用ibatis的应用,传统做法,往往会使用String的属性,与之对应。
例如一张member表,结构设计如下:

其中status为状态字段。

ibatis中,使用class MemberPO 与之mapping,设计往往如下:
public class MemberPO implements Serializable {
    
private Integer id;
    
private String loginId;
    
private String password;
    
private String name;
    
private String profile;
    
private Date gmtCreated;
    
private Date gmtModified;
    
private String status;

   
//getter/setters

缺点:
1)不直观,没人会知道status具体有哪些值。在缺乏文档,并且历史悠久的系统中,只能使用“select distinct(status) from member”,才能得到想要的数据。如果是在千万级数据中,代价太大了;
2)类型不安全,如果有人不小心拼写错误,将会导致错误状态。假设上面列子中,status只允许ENABLED/DISABLED,如果一不小心,memberPO.setStatus("ENABLEDD"),那么将会造成脏数据。

既然jdk5之后,引入了enum,是否可以让ibatis支持enum类型呢?事实上,最新的ibatis版本,已经支持enum类型(本文使用的是2.3.4.726版本--mvn repsitory上最新的版本)。
以上代码可以修改成:
1)Status类:
public enum Status {

    
/** enabled */
    ENABLED,

    
/** disabled */
    DISABLED;

2)MemberPO类:
public class MemberPO implements Serializable {
    
private Integer id;
    
private String loginId;
    
private String password;
    
private String name;
    
private String profile;
    
private Date gmtCreated;
    
private Date gmtModified;
    
private Status status;

    
//getter/setters

除此之外,其他均无需改动。
为什么呢?ibatis如何知道VARCHAR/Enum的mapping呢?
看过ibatis源码的同学,知道,ibatis是通过jdbcType/javaType得到对应的TypeHandler做mapping处理的。ibatis有基本类型的TypeHandler,比如StringTypeHandler,IntegerTypeHandler等等。在最新版本中,为了支持enum,增加了一个EnumTypeHandler。

并且在TypeHandlerFactory中,加了对enum类型的判断,请看:
public TypeHandler getTypeHandler(Class type, String jdbcType) {
    Map jdbcHandlerMap 
= (Map) typeHandlerMap.get(type);
    TypeHandler handler 
= null;
    
if (jdbcHandlerMap != null) {
      handler 
= (TypeHandler) jdbcHandlerMap.get(jdbcType);
      
if (handler == null) {
        handler 
= (TypeHandler) jdbcHandlerMap.get(null);
      }
    }
    
if (handler == null && type != null && Enum.class.isAssignableFrom(type)) {
      handler 
= new EnumTypeHandler(type);
    }
    
return handler;
  }
ibatis使用了取巧的方法,当取不到基本类型的handler时候,判断javaType是否是Enum类型--Enum.class.isAssignableFrom(type),如果是,则使用 EnumTypeHandler进行mapping处理。

为什么说它取巧,原因是早期ibatis设计过程中,自定义的接口无法得到具体的java class type。故早期的ibatis中,要实现对enmu的支持,非常苦难。而新版本中,为了达到这个功能,作者直接修改了TypeHandlerFactory的实现,打了一个补丁,如下:
if (handler == null && type != null && Enum.class.isAssignableFrom(type)) {
      handler 
= new EnumTypeHandler(type);
}
这个设计有悖于和早前的设计思想。早期,TypeHandler都是通过public void register(Class type, String jdbcType, TypeHandler handler)方式事先注册到factory中的,而这次,是在运行期,通过new方法动态得到EnumTypeHandler。
当然,新版本ibatis能支持enum,已经是一件开心的事情了。

Status枚举类除了描述状态,就足够了吗?回想起很多应用,我是做web开发的,在view层(velocity,jsp,等),见多了类似这样的代码:
#if($member.getStatus()==Status.ENABLED)开通#elseif($member.getStatus()==Status.DISABLED)关闭#end

<select>
  
<option value="ENABLED" #if($member.getStatus()==Status.ENABLED) selected="selected"#end >开通</option>
  
<option value="DISABLED" #if($member.getStatus()==Status.DISABLED) selected="selected"#end >关闭</option>
</select>

web层需要多少个页面,就需要维护多少份这样的代码;以后每添加/删除一种状态,多个地方都需要修改,还要担心逻辑不一致。

而事实上,关于状态的信息描述,按照职责分,就应该由枚举类来维护:
1)制定一个接口,EnumDescription.java
public interface EnumDescription {

    
public String getDescription();

}
2)写一个ResourceBundleUtil.java,通过Properties文件得到描述信息:
public class ResourceBundleUtil {

    
private ResourceBundle resourceBundle;

    
public ResourceBundleUtil(String resource) {
    
this.resourceBundle = ResourceBundle.getBundle(resource);
    }

    
public ResourceBundleUtil(String resource, Locale locale) {
    
this.resourceBundle = ResourceBundle.getBundle(resource, locale);
    }

    
public String getProperty(String key) {
    
return resourceBundle.getString(key);
    }

}
3)Status等枚举类实现EnumDescription:
public enum Status implements EnumDescription {

    
/** enabled */
    ENABLED,

    
/** disabled */
    DISABLED;

    
private static ResourceBundleUtil util = new ResourceBundleUtil(Status.class.getName());

    
public String getDescription() {
       
return util.getProperty(this.toString());
    }

}

这样,有什么好处:
1)通过Properties文件,支持国际化。
2)描述信息统一由自己来维护,方便维护,并且显示层逻辑简化,如:
$member.getStatus().getDescription()

<select>
  #foreach($status in $Status.values())
    
<option value="$status" #if($member.getStatus()==$status)selected="selected"#end >$status.getDescription()</option>
  #end
</select>

##############################################################################
那么使用老版本ibatis的客户怎么办呢?就像我们公司使用ibatis 2.3.0,难道只能眼馋着?解决方案:
1)升级到最新版本。 :)
2)ibatis提供了TypeHandler/TypeHandlerCallback接口,针对每种枚举类型,写相应的TypeHandler/TypeHandlerCallback的接口实现即可--工作量大,重复的劳动力。
主要是早期ibatis TypeHandler无法得到javaType类型,无法从jdbc value转成对应的枚举。在我看来,TypeHandler是作mapping用的,它至少有权知道javaType。
3)实现伪枚举类型(允许继承)来实现状态类型安全,而抛弃jdk5的方式--不方便日后升级。


不知道大家是否还有更好的方案?

本文涉及演示代码如下:
演示代码
workspace file encoding:utf-8
build tool: maven
repository:spring/2.5.5;ibatis/2.3.4.726

posted @ 2008-11-05 23:08 stone2083 阅读(925) | 评论 (0)编辑 收藏

2008年10月12日 #

感冒后记

撑了一年多之后,终于又彻底感冒了一次。
周四白天,身体还是好好的,原本还打算晚上跟随部门一起活动--打羽毛球,但是下班前的三小时,突然发觉双腿酸痛,头脑酸涨,全力无力--感冒来得如此的突然。
晚上,好歹强迫着自己吃了点饭--实在没什么胃口,稀里糊涂地吃了点药--家里没备什么特效药,早早地就睡下了。看看手机,发现才20点多,似乎08年里,是睡得最早的一次了。
长久不生病,难得生病,似乎特别得猛烈,凌晨1点多--身体滚烫,发起了高烧;脑袋昏沉,疼得厉害;难受使我惊醒,让女友帮忙准备了热茶,喝了一杯,又逼迫自己睡下,身体裹起被子,想出点恶汗,第二天能退烧。
醒后要想再睡着,似乎特别困难,而且还特别的折磨人,迷迷糊糊却有睡不着。可笑的是,在1点到2点这个似睡非睡阶段,脑袋中居然一起在考虑白天公司项目中cookie/session memecahed管理的设计方案和实现思路--无奈中感慨自己似乎特别敬业。
2点左右,再考虑完设计方案后,终于迷迷糊糊的睡去了。过程中,醒过几次,又睡下,直到凌晨7点,终于醒起,再也睡不着。起床,洗刷一下,体验自己的身体情况--烧似乎已经退下了,就算还未正常,至少也仅仅是低烧;头还是那么地疼;全身骨头也来凑热闹,一起闹酸痛。
女友让我请假,说这样的身体情况又如何能工作。其实,我也想,何况,去年的年休,我还剩下3天--到今年的11月就要过期了。无奈时不待我--项目经理刚好出差去外省招聘了,这段时间内,需要我负责起项目情况。其实,我也知道,就算我请假一天,甚至更多,单位的兄弟姐妹们同样能把手头上的事情处理的井井有条。但是想到他们在为项目拼搏,我一人在家休息,心里也不好受。
俗话说,身体有时候也可以靠精神支柱,此话不假,打起精神,出门上班,和平常似乎也没什么两样。一天下来,终于把原先写得差不多的cookie/session store manage完成了,下午还结对做了code review--在传授业务逻辑,代码风格给新人的同时,希望不要把感冒也传染给他。
一天终于结束了,庆幸的是,第二天就是周末,可以彻底放松自己,休养生息了。对于周末,用来休息,原本一直觉得是浪费时间,事实上,自从进入公司以后,周末就是用来学习技术的时间--平时太忙了,总是在项目中奔波,只能利用周末实现学习技术。此次的感冒,为我的彻底休息提供了很多理由(当然,也可以说是借口)。
周末两天,懒得跟猪一样,衣食起居,都有女友照顾着,心中也不再想着公司的项目,不再想着如何提高自己的技术能力,除了吃就是睡,心无旁骛。唯一和猪不一样的地方,就是除了吃和睡之外,还拿起了尘封已久的武侠小说《小李飞刀》,两天看了半本。此等悠闲的感觉,自从进入公司之后,还没有享受过。
其实,回想起这次生病,主要还是和工作比较累,心里压力比较大有一些关系:
1)最近,回退了一个实习生。回退的态度是坚决的,但是内心是不好受的。本来很想好好将他带起来,但是事与愿违,总觉得大学的学习方式和企业接轨还是差太多了,很多大学生抱守着大学的一套学习方法来适应公司的学习。自己总结了很多,也和自己学校取得一些联系,希望能为在校大学生做点什么;
2)自从去年起,公司项目一个接着一个,还没有停过。最近一个项目,也开始承担起技术经理的职责。做系统设计,做底层代码编写,感觉压力比较大。不像原先紧紧做模块开发,只要关注技术本身就可以。当然,这种压力,我很乐意接受;
3)项目很忙,原先和主管一起制定的q3季度的行动计划,几乎没多少进展,除了项目中成长外,没有多少时间学习其他技术,原定于q3要通读ibatis源码,而事实上,紧紧读了sqlmap相关的一些代码。想学习,但是没时间,是很痛苦的;
4)项目中,我这边没对产品需求把好关,导致需求逻辑复杂,混乱,甚至和原有逻辑矛盾,但是在项目后期,明知矛盾还是得进行下去,很无奈;
5)最近,女友的妈妈也身体不好,住院了,心里很无助,帮不上任何忙,也痛苦;
。。。
这些杂碎的事情,一起袭来的时候,发现自己也挡不住,也导致时隔一年,高烧再次光顾我身体。
不过,生生小病也好,给我一个彻底休息的理由,周末时间好好放松,调整好心态,以积极的状态,投入到之后的工作和生活中。

posted @ 2008-10-12 21:01 stone2083 阅读(19) | 评论 (0)编辑 收藏

2008年9月21日 #

ajax原理简介以及简单demo演示

如今web应用上,ajax技术是大行其道。
ajax框架层出不穷,prototype,dojo,jquery,mootools,dwr,buffalo,ext,yui,spry。。。
ajax框架的出现,在提升开发生产效率的同时,也让不少同学不明其内在原理,仅仅成为了某些框架的使用者。
(对于产品生产是好事,对于技术追求是坏事)

本文不涉及任何ajax框架的使用,本文仅通过一个模拟需求,在不使用任何ajax框架的前提下,以demo演示的方式,
向大家介绍ajax的原理以及应用场景。

ajax全称是:Asynchronous JavaScript And XML。
其本意是,通过javascript技术(JavaScript),通过异步http请求方式(Asynchronous),得到XML文本内容(XML)之后,通过javascript技术局部刷新web页面内容。
从广义的概念看,只要符合“异步请求,局部刷新web页面”的技术,都可以成为ajax。
未必一定要使用javascript,一般情况下,大多数client端脚本代码都可以;返回内容也未必一定要是xml,目前json格式,更为流行。

如何异步请求内容呢?
以javascript代码作演示,如下:
function xmlhttpPost(url,func) {
    
var xmlHttpReq = false;
    
var self = this;
    
// Mozilla/Safari
    if (window.XMLHttpRequest) {
        self.xmlHttpReq 
= new XMLHttpRequest();
    }
    
// IE
    else if (window.ActiveXObject) {
        self.xmlHttpReq 
= new ActiveXObject("Microsoft.XMLHTTP");
    }
    self.xmlHttpReq.open('POST', url, 
true);
    self.xmlHttpReq.setRequestHeader('Content
-Type', 'application/x-www-form-urlencoded');
    self.xmlHttpReq.onreadystatechange 
= function() {
        
if (self.xmlHttpReq.readyState == 4) {
            func(self.xmlHttpReq.responseText);
        }
    }
    self.xmlHttpReq.send(
null);
  }
参数一,url:表明异步请求的资源地址
参数二,func:表明请求结束后,采用什么函数对请求结果内容进行回调处理

其实,这一个js代码,就诠释了ajax的全部含义--异步请求资源,将得到的资源内容,使用指定的function进行处理。
所以,ajax很简单,大家千万别被如今层出不穷的ajax框架给吓怕了。要了解ajax的原理,就只要参看这段代码即可。
如今的一些框架,仅仅在此基础上,是封装了一些公用的函数,方便开发人员调用。(当然,说说简单,其实所谓的这些函数,大大方便了开发人员使用ajax技术。具体请参看ajax framework的官方介绍。)

特别说明:这个xmlhttpost方法改进了simple-ajax。在原基础上,将回调方法作为参数传递。


解释了原理性的内容之后,接下来,以一个模拟的应用场景,demo说明ajax的使用,以及它的主要应用场景。
模拟场景:
目录选择,即当选择一个目录的时候,需要显示这个目录下的所有子目录。

首先,我们来虚拟一个目录结构,如下:


那么,要实现目录选择,有三个方式:
1)页面初始化的时候,服务端将所有的目录信息都put到页面中。
   优点:选择操作简单,有了全部的目录信息,做选择操作,都可以使用js完成,无需和服务端进行交互
   缺点:当目录信息很大的时候,比如有上万个节点,整个目录信息有1m左右大小,那么要渲染这个页面,估计得20秒左右(视网速)
并且,很可能用户仅仅只要选择有限的几个节点就可以,比如上万个节点中选择6-7个节点,那么浪费太大了;
2)页面初始化的时候,服务端将当前需要的节点信息put到页面上,一旦有选择操作,重新刷新页面。
   优点:选择操作简单,对于节点信息,每次取需要的内容,不存在浪费现象
   缺点:每次都要刷新整个页面,除节点信息外,其他不变的东西都需要重新从服务端取,增加无谓的消耗。
3)页面初始化的时候,服务端将当前需要的节点信息put到页面上,一旦有选择操作,只刷新节点相关的内容;
   优点:每次只load需要的信息,局部刷新页面内容,不存在任何浪费现象
   缺点:需要异步请求数据,每次请求都需要和服务器交互,选择操作稍显复杂(异步请求,局部刷新)

通过这三种方式做对比,发现ajax主要适用的场景如下:
1)整体内容量大(几百k,几m,甚至几十m),而页面只需要其中一小部分信息即可;
2)数据显示,只涉及一个页面中部分数据信息的变动;

特别说明:至于使用ajax性能如何,需要对1,3两个情况做性能测试,权衡使用。

针对第三种方案,
首先需要一个取节点资源的url,
演示代码中,为了演示方便,使用php语言,而非使用主要语言java;
tree_node.php
<?php
$id =  $_GET['id']; 
if("1" == $id) {
  
echo("{\"id\":1,\"parentId\":-1,\"name\":\"1-1\",\"children\":[{\"id\":2,\"name\":\"2-1\"},{\"id\":3,\"name\":\"2-2\"},{\"id\":4,\"name\":\"2-3\"}]}");
else if("2" == $id) {
  
echo("{\"id\":2,\"parentId\":1,\"name\":\"2-1\",\"children\":[]}");
else if("3" == $id) {
  
echo("{\"id\":3,\"parentId\":1,\"name\":\"2-2\",\"children\":[]}");
else if("4" == $id) {
  
echo("{\"id\":4,\"parentId\":1,\"name\":\"2-3\",\"children\":[{\"id\":5,\"name\":\"3-1\"},{\"id\":6,\"name\":\"3-2\"}]}");
else if("5" == $id) {
  
echo("{\"id\":5,\"parentId\":4,\"name\":\"3-1\",\"children\":[]}");
else if("6" == $id) {
  
echo("{\"id\":6,\"parentId\":4,\"name\":\"3-2\",\"children\":[{\"id\":7,\"name\":\"4-1\"}]}");
else if("7" == $id) {
  
echo("{\"id\":7,\"parentId\":6,\"name\":\"4-1\",\"children\":[]}");
else {
  
echo("");
}
?>
该文件中,写死了目录结构(一般情况下,往往根据树对象,动态取得需要的节点)。


通过js,动态请求节点信息,部分刷新页面内容:
 <script type="text/javascript">
    
//模拟需求js
    var nodeSelect = function(text) {
      
var tree = toJsonObje(text);
      
var options = document.getElementById("tree").options;
      options.length 
= 0;
      options.add(
new Option("请选择","-1"));
      
if(tree == null) {
        
return;
      } 
else {
        
var children = tree.children;
        
for(i = 0; i < children.length; i++) {
          
var child = children[i];
          options.add(
new Option(child.name,child.id));
        }
        
if(tree.parentId != "-1") {
          options.add(
new Option("上一级",tree.parentId));
        }
      }
      document.getElementById(
"l").innerHTML = "当前位置:" + tree.name;
    }

    
function nodeSelectAjax(id) {
      
var TREE_NODE_URL = "tree_node.php";
      
var url = TREE_NODE_URL + "?id=" + id;
      xmlhttpPost(url,nodeSelect);
    }   
  
</script>
nodeSelectAjax,异步请求节点资源
nodeSelect,回调函数,根据请求信息,局部刷新页面


至于请求资源信息格式,任何方式都可以,只要client端能解析就行。
目前json格式,比较流行。
最后,附上java使用json库,生成json格式的方法:
JSONObject node = new JSONObject();
node.put(
"id"1);
node.put(
"parentId"-1);
node.put(
"name""1-1");
JSONArray children 
= new JSONArray();
JSONObject c1 
= new JSONObject();
c1.put(
"id"2);
c1.put(
"name""2-1");
JSONObject c2 
= new JSONObject();
c2.put(
"id"3);
c2.put(
"name""2-2");
children.put(c1);
children.put(c2);
node.put(
"children", children);
System.out.println(node.toString());



ajax demo
工程文件编码:utf-8
工程运行:http server with php supported
ubuntu firefox下测试通过


其他:
不知道是不是ie的bug,居然不支持 select.innerHTML = value的方式
只能通过select.options.add(new Option("content","value") 动态往select中添加选项。

posted @ 2008-09-21 19:05 stone2083 阅读(35) | 评论 (0)编辑 收藏

2008年7月20日 #

部门合作 和 数学公式

万物之间都有联系,这句话一点都没错,一组数学公式恰好可以反应常见的几种部门合作情况。

假设:
1) 一部门按质量交付产品的权值为 1
2) 质量每提升一分,权值加0.1
3) 质量每降低一分,权值减0.1
4) 某公司产品线需要4个部门合作

结果:
情况A:每个部门,都按质量完成自己的工作,则
产品总体质量 = 1 * 1 * 1 * 1 = 1
符合公司产品质量要求

情况B:每个部门交付的产品,均只有要求的9成,则
产品总体质量 = 0.9 * 0.9  * 0.9 * 0.9 = 0.6561
产品质量仅仅为要求的6成,刚好达到及格线而已
每个部门完成9分,似乎并不是很差,但是整体产品,却只达到及格而已

情况C:每个部门交付的产品,均超质量1分,则
产品总体质量 = 1.1 * 1.1 * 1.1 * 1.1 = 1.4641
产品质量为要求的1.4641倍,优质产品.
每个部门多完成1.分,似乎额外工作量并不是很大,但是最终产品却能成为优质产品

情况D:两个部门交付的产品,只有要求的9成,另两个部门为了弥补产品的缺陷,努力做到超质量1分,则
产品总体质量 = 0.9 * 0.9 * 1.1 * 1.1 = 0.9801
和情况A同样的工作强度,结果还是没有达到产品质量要求

情况E:两个部门交付的产品,只有要求的8成,另两个部门为了弥补产品的缺陷,努力做到超质量2分,则
产品总体质量 = 0.8 * 0.8 * 1.2 * 1.2 = 0.9216
和合情D同样的工作强度,结果总体质量比情况D更差

总结:
一个公司,只要每个部门对工作懈怠一点,公司产品就会和产品要求差很多,部门越多,差距越大
一个公司,只要每个部门对工作要求更严格一点,公司产品质量却远远高于产品要求,部门越多,质量越高
一个公司,如果有部门A对工作不到位,与其让后续其他部门加强工作强度,弥补部门A的产品缺陷,还不如加强对部门A的教育和培训,让其交付符合质量要求的产品.


公司产品开发历经PD(产品设计规划部门),RA(需求分析),Developer(开发工程师),Test(测试部门)四个环节.

在自己经历的一些项目中,因为时间关系等原因,往往出现:
PD提交的需求逻辑流程有点问题;提交的demo不符合要求;RA UC中仅仅考虑主流程,遗忘一些分支流程;开发对原有代码不敢做重构,搭积木似的添加功能,埋地雷等等...几个环节下来,试问最终产品质量如何??

我仅仅是一名普通的程序员,原先在面对逻辑不完善,demo不符合等等情况,都试图通过自己最大的努力,来弥补这些缺陷,但是当明白上述公式之后,我越来越希望每个部门都能尽自己最大的努力,来交付高质量的产品.

一个成功的公司,不可能仪仗个人英雄,而是需要各个部门协同工作.

posted @ 2008-07-20 20:46 stone2083 阅读(21) | 评论 (0)编辑 收藏

2008年7月17日 #

单元测试分享

最近,在小组内部做了一次关于“单元测试”的分享。把自己两年来做单元测试遇到的问题和对单元测试的认识做了一次总结和讨论。

本文不会详细地讲述分享的内容,仅仅是ppt的大纲显示:

使用单元测试前提:
最小的成本,换来最大的收益

单元测试目的

1)测试代码错误(?) -- 不是主要目的
2)便于重构时的测试
3)改善既有代码的设计

分享核心
1)如何脱离“webx“--做隔离测试
2)dal/biz/web 层如何做单元测试
3)如何通过改善代码设计,更方便测试

dal层(数据库访问层)特点:
1)独立,逻辑单一,对表做操作
2)业务相对比较稳定
3)采用ibatis,写sql的方式

dal层测试方式
1)压根儿就不需要测试
2)仅仅配置spring bean,通过日志打印的方式(无法达到自检)
3) 自检方式 -- AbstractTransactionalDataSourceSpringContextTests (高成本,不轻易使用)
需要权衡

biz(业务层)测试方式--分BO和AO:
BO:(即所谓的Service/Manager)
AO:(一个UseCase对应的业务流)
隔离 + 设计 (主要通过代码演示--见附件)

单元测试的缺点
专注于单一业务测试,衔接点容易出错

解决方案
接口输入输出明确
集成测试

web层:
集成测试的入口

分享文档和演示代码 (ppt是在ubuntu下制作,可能效果并不是很好)

备注:
自己对单元测试了解也比较肤浅,欢迎一起探讨

posted @ 2008-07-17 22:04 stone2083 阅读(36) | 评论 (0)编辑 收藏

2008年6月28日 #

久违的同学,久违的篮球

今天中午,刚在电脑旁边坑次坑次的赶着"单元测试"分享PPT的时候,接到大学同学的电话,约我下午去工大打篮球.

我已经大半年时间没有碰过篮球了,甚至可以说,大半年没有进行过像样的运动--想当年标准的身材,略显结实的肌肉,因为它主人长期坐在电脑前工作,没有足够时间的运动,都裹上了一层厚厚的脂肪.

原先同学也约过我几次打球,但是往往因为我回绍兴老家,或者忙着公司的工作,被我狠心拒绝了.

这次,由于单元测试分享ppt的优先级不是很高,加上对篮球的渴望,对老同学的怀念,一口答应下来.
做好出发的准备工作,穿上刚买不久的篮球鞋,以及刚买不久的篮球,匆匆乘车赶往工大.

2点--4点,与同学相聚,闲聊,打了4个小时的篮球,过程中,由于与篮球久违了半年时间,技术动作已不再到位,运球突破的时候脚步移动也与手部动作脱节,并且原先还算得上精准的投篮技术也不复存在--整一个门外汉.呜呼.... :)
4个小时的运动量,收获颇丰--出了一身的臭汗;腰酸背痛腿抽筋,左脚微微扭伤.
但是,心情是异常的舒畅,舒展--长时间忙于公司的工作,被繁重的任务压抑的感觉一扫而空--身体的疲惫换来心情的愉悦.

之后,我做东,请老同学在工大北门吃饭,去了我们以前常去的那家,点了相似的菜.边吃边聊自己以及其他同学的工作情况.其实大伙儿都混得不错,要么在事业单位高就,要么已在技术公司做上了技术骨干,都有着比较不过的薪水待遇.替他们高兴.

挥手分别,各自赶回自己的家之前,约定以后有时间,常聚聚,打打球,运动运动,聊聊天...

我也为自己定了一个新口号:健脑,健身--为了更好地工作,为了更好地生活.

晚上回到家里,一边听着钢琴曲,一边惬意地写下此文,聊以怀念久违的篮球,怀念久违的老同学.

posted @ 2008-06-28 22:00 stone2083 阅读(43) | 评论 (1)编辑 收藏

2008年6月26日 #

ibatis--部分更新表记录字段的方法

使用ibatis,如果要更新表记录,一般常用的做法就是,查找出记录,然后修改部分字段,进行update操作.
以member表为例:
MemberDO member = memberDAO.findById(1);
member.setName(
"stone");
memberDAO.update(member);

这种是最常用的方法.不错,在很多应用场景下,这么干,完全没有问题.
但是(往往存在但是),如果member表中存在一个或者多个text(或者blob)字段.难道仅仅为了更新一个name字段,需要重新update那些本不需要更新的text/blob字段吗?

于是乎,人们又想出了一个办法,参数采用map,把需要更新的字段put到map中,
演示代码(省略ibatis的sqlmap文件):
Map<String,Object> map = new HashMap<String,Object>();
map.put(
"name","stone");
memberDAO.update(map);

没错,这种方法不错.需要更新哪些字段,只需要动态put到map中就可以.
但是,对于这种方法,需要调用更新的地方,需要手工维护数据库的字段名,如果在put的时候,一不小心拼错字段名,那么更新操作肯定和你预计的会有差别.
比如上面的代码:
Map<String,Object> map = new HashMap<String,Object>();
map.put(
"nama","stone");
memberDAO.update(map);
不小心把name拼成了nama,那么新的name字段就无法保存到数据库中.试想一下,任何需要更新字段的地方,都存在拼写错误的风险.

于是乎,人们又想到了参数类,比如就把MemberDO当成参数类:
MemberDO memberParam = new MemberDO();
memberParam.setName(
"stone");
memberDAO.update(memberParam);
sqlmap.xml如下:
update member
set gmt_modified = current_date
<dynamic>
<isNotNull property="loginId",prepend=",">
login_id = #loginId#
</isNotNull>
   
<isNotNull property="name",prepend=",">
name = #name#
</isNotNull>
   
</dynamic>
where id = #id# 
这方法貌似不错,不会存在字段名拼写错误的风险.并且需要更新哪些字段,动态set一下就可以.
但是,如果要把某个字段设置为null,那怎么办?那没辙咯...(sqlmap中约定,只有不为null的时候,才更新).

那...那...那怎么办呢?
貌似只有Map才能满足需求嘛...因为sqlmap中有个
"isPropertyAvailable"和"isNull"属性支持.只要配合这两个属性,就能区分需要更新为null,还是不更新保持原字段内容.
sqlmap文件演示:
<isPropertyAvailable property="loginId" prepend=",">
        
<isNotNull property="loginId">
          
<![CDATA[
            login_id = #loginId#
          
]]>
        
</isNotNull>
        
<isNull property="loginId">
          
<![CDATA[
            login_id = null
          
]]>
        
</isNull>
</isPropertyAvailable>
只要map不put loginId,那么更新的时候,就不会更新这个字段,如果map.put("loginId",null),那么就会把loginId更新为null.
看来只有map能胜任.

不是说,使用map,维护字段内容很麻烦嘛.但是好像又只能使用它?
于是乎,又想到了一种思路(也是本文要介绍的一个方法)
通过方法拦截,在设置参数类的时候,把设置的属性值put到map中.(cglib是很胜任这样的场合的)

首先,需要一个BaseDO.java DataObject的基类,仅仅用于维护一份Map对象.
BaseDO.java:
public class BaseDO implements Serializable {

    
private static final long serialVersionUID = -315506079592557582L;

    
private Map<String, Object> setterMap;

    
public synchronized void initSetterMap() {
    
if (setterMap == null) {
        setterMap 
= new HashMap<String, Object>();
    }
    }

    
public Map<String, Object> getSetterMap() {
    
return setterMap;
    }

}

采用Cglib,写一个对set方法的拦截器:
SetterInterceptor.java 用于对截获set操作,把set的对象put到map中
public class SetterInterceptor implements MethodInterceptor {

    
private static final String SET_METHOD = "set";

    @Override
    
public Object intercept(Object obj, Method method, Object[] args,
        MethodProxy proxy) 
throws Throwable {
    
// 拦截DataObject中所有的set方法,把set的属性放入到map中
    if (method.getName().startsWith(SET_METHOD)) {
        
if (obj instanceof BaseDO) {
        BaseDO baseDO 
= (BaseDO) obj;
        baseDO.initSetterMap();
        String attribute 
= StringUtils.substring(method.getName(),
            SET_METHOD.length());
        attribute 
= StringUtils.uncapitalize(attribute);
        
if (args != null && args.length == 1) {
            baseDO.getSetterMap().put(attribute, args[
0]);
        }
        }
    }
    
return proxy.invokeSuper(obj, args);
    }

}

写一个创建Setter的工厂类,用于创建带方法拦截的DataObject对象
public class SetterFactory {

    
private static final SetterInterceptor setterInterceptor = new SetterInterceptor();

    @SuppressWarnings(
"unchecked")
    
public static <extends BaseDO> T getSetterInstance(Class<T> clazz) {
    Enhancer enhancer 
= new Enhancer();
    enhancer.setSuperclass(clazz);
    enhancer.setCallback(setterInterceptor);
    
return (T) enhancer.create();
    }

}

那么对于client调用,就非常简单了.
如:
public class Client {

    
private static final Log log = LogFactory.getLog(Client.class);

    
private static final String APP_CONFIG_FILE = "cn/zeroall/javalab/ibatis/app.xml";

    
public static void main(String[] args) {
    ApplicationContext ctx 
= new ClassPathXmlApplicationContext(
        APP_CONFIG_FILE);
    MemberDAO memberDAO 
= (MemberDAO) ctx.getBean("memberDAO");

    MemberDO setter 
= SetterFactory.getSetterInstance(MemberDO.class);
    setter.setId(
1);
    setter.setLoginId(
"stone1");
    setter.setName(
"stone1");
    memberDAO.updateById(setter);

    MemberDO member 
= memberDAO.findById(1);
    log.info(member.getLoginId());

    }
}

sqlmap文件如下:
<update id="update-by-id" parameterClass="java.util.Map">
    
<![CDATA[
      update member
      set gmt_modified = current_date
    
]]>
    
<dynamic>
      
<isPropertyAvailable property="loginId" prepend=",">
        
<isNotNull property="loginId">
          
<![CDATA[
            login_id = #loginId#
          
]]>
        
</isNotNull>
        
<isNull property="loginId">
          
<![CDATA[
            login_id = null
          
]]>
        
</isNull>
      
</isPropertyAvailable>
      
<isPropertyAvailable property="password" prepend=",">
        
<isNotNull property="password">
          
<![CDATA[
            password = #password#
          
]]>
        
</isNotNull>
        
<isNull property="password">
          
<![CDATA[
            password = null
          
]]>
        
</isNull>
      
</isPropertyAvailable>
      
<isPropertyAvailable property="name" prepend=",">
        
<isNotNull property="name">
          
<![CDATA[
            name = #name#
          
]]>
        
</isNotNull>
        
<isNull property="name">
          
<![CDATA[
            name = null
          
]]>
        
</isNull>
      
</isPropertyAvailable>
      
<isPropertyAvailable property="profile" prepend=",">
        
<isNotNull property="profile">
          
<![CDATA[
            profile = #profile#
          
]]>
        
</isNotNull>
        
<isNull property="profile">
          
<![CDATA[
            profile = null
          
]]>
        
</isNull>
      
</isPropertyAvailable>
    
</dynamic>
    
<![CDATA[
        where id = #id#
    
]]>
  
</update>


一旦采用了Setter对象,那么对于表记录的更新操作,仅仅需要一个sql,就能解决.比较方便.

附件中,把整个演示代码附上,有兴趣的朋友,可以了解下:
采用maven构建,workspace编码采用utf-8.数据库采用pgsql

demo附件

备注:
member表创建sql如下:
-- Table: member

-- DROP TABLE member;

CREATE TABLE member
(
  id serial 
NOT NULL,
  login_id 
character varying(16),
  "password" 
character varying(16),
  "name" 
character varying(32),
  profile 
text,
  gmt_created 
timestamp without time zone,
  gmt_modified 
timestamp without time zone,
  
CONSTRAINT member_pkey PRIMARY KEY (id)
)
WITH (OIDS=FALSE);
ALTER TABLE member OWNER TO javalab;


特别说明:
此方法原创作者为公司同事,本文只是盗用了他的创意.


posted @ 2008-06-26 22:46 stone2083 阅读(232) | 评论 (0)编辑 收藏

2008年6月1日 #

活用Srping AOP

总有那么一些代码,在测试环境下,是不能轻易被调用的。
比如:
1)发送系统任务邮件到客户邮箱,可能一不小心,就把测试邮件发送给了真实客户的邮箱里;
2)调用跨公司的系统接口,而对方系统没有测试环境,每调用一次接口,就会在对方系统产生垃圾数据;
3)调用的代码可能需要大量的cpu运算,占用大量的内存空间,消耗大量的资源;
等等。。。

为了解决这样的需求,
1)在代码中,到处充斥着这样的代码:
1 if(在测试环境下) {
2     打印日志;
3 else {
4     调用真实的业务逻辑;
5 }
于是乎,需要到处维护这样的代码,一旦增加此类需求,就需要编写同样的代码

2)部分懒惰的程序员,连这样的if...else...也不愿意写,仅仅在注释中说明下在测试环境中调用方法的危害性。
于是,在测试阶段,一旦和测试部门沟通不足,导致代码还是经常被调用到,如果是在作压力,性能测试,那么危害性可想而已。
曾发生过,压力测试某个功能,结果把大量的测试邮件,发送给了客户,影响很差。


那么,如何解决这样的需求场景呢?
没错,采用proxy模式,可以搞定。考虑到现在很多企业都使用Spring作为IOC容器,本文就简单介绍,如何采用spring aop来解决问题。

以发送邮件的需求作为虚拟场景。
现在有个Service,专门负责邮件的发送。
1. MyService.java
1 public class MyService {
2     public void sendMailSafely() {
3     System.out.println("send mail successfully.");
4     }
5 }

如果这个sendMailSafely被客户端调用,那么毫无疑问,邮件不管任何环境下,都会被成功发送。
需要有个方法拦截器,对这个方法做拦截。
2. MyInterceptor.java
 1 public class MyInterceptor implements MethodInterceptor {
 2 
 3     private boolean isProduction = false;
 4 
 5     @Override
 6     public Object invoke(MethodInvocation invocation)