| 无心插柳's profilezwchen的共享空间BlogListsNetwork | Help |
zwchen的共享空间September 09 Java线程安全系列(1)――Servlet线程安全(续)现在,我们保证了顺序,但是我们怎么保证Counter字段(不是局部变量!)在每个Servlet的线程下都是独立的呢?也就是说,并发请求时,它们都不相互干扰。 我现在将Servlet代码重构如下: public class SimpleServlet extends HttpServlet { private ThreadLocal counter = new ThreadLocal() { protected synchronized Object initialValue() { return new Integer(0); } };
public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doPost(req, resp); }
public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.getWriter().println("<HTML><BODY>"); resp.getWriter().println(this + "[" + Thread.currentThread() + "]: <br>"); for (int c = 0; c < 10; c++) {
resp.getWriter().println(counter.get() + "<br>"); try { Thread.sleep((long) Math.random() * 1000); int c1 = ((Integer)counter.get()).intValue(); c1++; counter.set(new Integer(c1)); } catch (InterruptedException exc) { }
} resp.getWriter().println("</BODY></HTML>"); } } 现在,我刷新html页面三次,第三次结果如下: com.zwchen.servlet.SimpleServlet@124e935[Thread[http-8081-Processor22,5,main]]:
com.zwchen.servlet.SimpleServlet@124e935[Thread[http-8081-Processor25,5,main]]:
com.zwchen.servlet.SimpleServlet@124e935[Thread[http-8081-Processor23,5,main]]:
从以上结果,我们可以发现: 1、 在该html页面内的并发三次请求中,该Servlet里面的counter字段都不相互干扰 2、 counter字段还是实例字段,并且都会保留状态,不是每次都用0开始 3、 html页面内的三次请求都在不同的线程,但在同一个实例中。 总之,在Java里面,字段(不是局部变量)有三个共享范围:instance field,static field,local thread field,而后者往往在服务器端这种多线程环境必须考虑到的。
在J2EE项目开发过程中,ThreadLocal类有时有非常重要的作用,下面是我碰到的,但可以延伸: 1、 在用Hibernate做web开发的持久化时,有个模式叫做Open Session In View,也就是将session保留到页面中,在response结束后,在OpenSessionInViewFilter中关闭session,这对于延迟加载非常有效,例如,我们在页面上显示User的详细信息,需要显示该user的所属Department的信息; 但是,在list users这种不需要显示department信息的地方,那个user的department信息就不会加载,也就是说加载相关信息是动态的,但不会出现LazyInitializationException,也就是Load on demand。不过,注意慎用该模式。 2、 在工作流开发,例如OSWorflow,每次调用其服务前,都需要将caller对象传入,这样会导致我们的方法非常臃肿,如果我们在调用该方法的上层,如在Servlet里调用它之前,将User对象置于ThreadLocal中,那么可以在工作流方法内通过get()方法获取,而不用传入参数。 3、 为什么Web框架中,Webwork的action中可以有field,但Struts却不能?其实,也就是说,Struts不是线程安全的,而Webwork是线程安全的。大家可以参考Webwork的ActionContext类: public class ActionContext implements Serializable { static ThreadLocal actionContext = new ActionContextThreadLocal(); …………….. 而对于Struts,我们可以从ActionServlet.process() => RequestProcessor. processActionPerform,在RequestProcessor中有字段 protected HashMap actions = new HashMap();我们不难发现,我们所写的action是共享的,那么内部字段必然也是共享。注意,这种共享类似于Servlet里面的字段。
相关文献: http://www.artima.com/designtechniques/threadsafety.html http://www.javaworld.com/javaworld/jw-07-2004/jw-0712-threadsafe.html Java线程安全系列(1)――Servlet线程安全Java线程安全系列(1)――Servlet线程安全
概述 在探讨java线程安全前,让我们先简要介绍一下Java语言。 任何语言,如C++,C#,Java,它们都有相通之处,特别是语法,但如果有人问你,Java语言的核心是什么?类库?关键字?语法?似乎都不是。Java语言的核心,也就是Sun始终不愿意开源的东西:Java虚拟机的实现(不过sun公开了其Java虚拟机规范),也就有了BEA的JRockit,IBM的Jikes,Sun的Hotspot。 Java的核心有两点,Java类加载(Java Class Loader)和Java内存管理,它们具体体现在Java类库的以下几个类: java.lang.ClassLoader(java.lang.Class):我们调用的类,包括其接口和超类,import的类是怎么被Java虚拟机载入的?为什么static的字段在servlet容器里面可以一直生存下去(Spring容器中)? java.lang.Thread(java.lang.ThreadLocal):垃圾回收是怎么进行的(垃圾回收线程)?我们的程序是怎么退出的? java.lang.refelect.Proxy(java.lang.refelect.Method):为什么Tomcat、Tapestry、Webwork、Spring等容器和框架可以通过配置文件来调用我们写的类?Servlet规范、JSF规范、EJB规范、JDBC规范究竟是怎么回事?为什么它们几乎都是一些接口,而不是具体类?
Servlet线程安全 在Java的server side开发过程中,线程安全(Thread Safe)是一个尤为突出的问题。因为容器,如Servlet、EJB等一般都是多线程运行的。虽然在开发过程中,我们一般不考虑这些问题,但诊断问题(Robust),程序优化(Performance),我们必须深入它们。 什么是线程安全? Thread-safe describes a program portion or routine that can be called from multiple programming threads without unwanted interaction between the threads。 在Java里,线程安全一般体现在两个方面: 1、 多个thread对同一个java实例的访问(read和modify)不会相互干扰,它主要体现在关键字synchronized。如ArrayList和Vector,HashMap和Hashtable(后者每个方法前都有synchronized关键字)。如果你在interator一个List对象时,其它线程remove一个element,问题就出现了。 2、 每个线程都有自己的字段,而不会在多个线程之间共享。它主要体现在java.lang.ThreadLocal类,而没有Java关键字支持,如像static、transient那样。
一个普遍的疑问,我们的Servlet中能够像JavaBean那样declare instance或static字段吗?如果不可以?会引发什么问题? 答案是:不可以。我们下面以实例讲解: 首先,我们写一个普通的Servlet,里面有instance字段count:
public class SimpleServlet extends HttpServlet { // A variable that is NOT thread-safe! private int counter = 0;
public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doPost(req, resp); }
public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.getWriter().println("<HTML><BODY>"); resp.getWriter().println(this + " ==> "); resp.getWriter().println(Thread.currentThread() + ": <br>"); for (int c = 0; c < 10; c++) { resp.getWriter().println("Counter = " + counter + "<BR>"); try { Thread.sleep((long) Math.random() * 1000); counter++; } catch (InterruptedException exc) { } } resp.getWriter().println("</BODY></HTML>"); } } 然后,我们通过一个html页面向该servlet发出三次请求: <HTML> <BODY> <TABLE> <TR> <TD><IFRAME src="./SimpleServlet" name="servlet1" height="200%"> </IFRAME></TD> </TR> <TR> <TD><IFRAME src="./SimpleServlet" name="servlet2" height="200%"> </IFRAME></TD> </TR> <TR> <TD><IFRAME src="./SimpleServlet" name="servlet3" height="200%"> </IFRAME></TD> </TR> </TABLE> </BODY> </HTML>
刷新页面几次后,产生的结果为: com.zwchen.servlet.SimpleServlet@11e1bbf
==> Thread[http-8081-Processor23,5,main]:
com.zwchen.servlet.SimpleServlet@11e1bbf
==> Thread[http-8081-Processor22,5,main]:
com.zwchen.servlet.SimpleServlet@11e1bbf
==> Thread[http-8081-Processor24,5,main]: 我们会发现三点: servlet只产生了一个Servlet对象,因为输出this时,其hashcode都一样, servlet在不同的线程(线程池)中运行,如http-8081-Processor22,http-8081-Processor23 Count被这三个doGet方法共享,并且并行修改。
上面的结果,违反了线程安全的两个方面。 那么,我们怎样保证按照我们期望的结果运行呢?首先,我想保证产生的count都是顺序执行的。 我们将Servlet代码重构如下:
public class SimpleServlet extends HttpServlet { //A variable that is NOT thread-safe! private int counter = 0; private String mutex = "";
public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doPost(req, resp); }
public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.getWriter().println("<HTML><BODY>"); resp.getWriter().println(this + ": <br>"); synchronized (mutex) {
for (int c = 0; c < 10; c++) { resp.getWriter().println("Counter = " + counter + "<BR>"); try { Thread.sleep((long) Math.random() * 1000); counter++; } catch (InterruptedException exc) { } } } resp.getWriter().println("</BODY></HTML>"); } }
我们的输出结果为: com.zwchen.servlet.SimpleServlet@109da93:
com.zwchen.servlet.SimpleServlet@109da93:
com.zwchen.servlet.SimpleServlet@109da93:
这符合了我们的要求,输出都是按顺序的,这正式synchronized的含义。 附带说一下,我现在synchronized的是一个字符串变量mutex,不是this对象,这主要是从performance和Scalability考虑。Synchronized用在this对象上,会带来严重的可伸缩性的问题(Scalability),所有的并发请求都要排队!
August 19 是Axis的bug吗?(一次debug的完整过程) 今天下午确实够郁闷的,还是周六,在公司加班。北京那边出差的人告诉我,说我负责的Web Services怎么都部署不到WebSphere(AIX)集群上,不过我确实在公司里的AIX上的WebSphere上以及Windows的WebSphere上都发布成功了。而且用C#客户端测试通过。还说那个Axis下的Web Services在集群下没有问题。所以,现在的解决办法,就是我把web Service移出WebSphere的Web Services引擎,再部署到Axis上,然后传给他们。 于是,我就开始准备了。由于Web Services中包含复杂的对象,而且Axis没有提供相关部署工具,所以手工处理确实非常费劲,而且很容易出差。我看他们都是用JBuilder发布的,于是我安装了一个JBuilder2006,感觉非常不顺手,试了两个小时,无奈,放弃了。 其实,IBM 的RAD(WSAD)和eclipse的Lomboz插件都提供Axis的发布,RAD只支持Axis1.0,Lomboz支持1.2,但RAD那个太旧,放弃。而Lomboz那个Axis插件有bug,死活都生成不了。网上有个Axis2.0的eclipse插件,不过我的axis是1.3版本,太急嘛,所以不敢随便用Axis2.0。 最后,我干脆手工写WSDD配置了,不过有JBuilder生成的那个对照,这个倒简单,一下搞定,而且Tomcat下部署成功,毕竟我当时对Axis专门研究过。 然后我就用C#客户端测试,总是报错(返回的xml文件无效)。于是,我用TCPMonitor拦截SOAP包,发现那个返回(这说明WebServices调用是成功的)的XML文件首行多了5aa,末行多了0,当然无效了。 难道这是axis的bug? 我试着将SOAP的style从RPC改为Document,encoded改为literal和wrapped,都有更多奇怪的错误(排列组合一下我该要尝试多少次?)。 我怀疑是Servlet容器的问题(因为那几个字符产生于xml外面),于是我又试着将Servlet容器从Tomcat换成Resin、WebSphere,那几个字符变了,但还是存在。 我又怀疑是Axis的bug,试着将我的Axis1.3 的jar文件换成了Axis1.2的,问题依旧。 遇到这种问题,我不知道大家是什么心情?这是我的问题吗? 我起身喝了点水,歇了几分钟。 我忽然想到,是不是我的客户端调用的问题?但是,调用正确啊,因为soap包中有返回指,而且是正确的。只是xml文件外面多了几个奇怪字符。 但是,我的客户端的soap头确实多了些信息,因为我的project启用了WSE3.0(Web Services安全包),我把工程的WSE选择禁止掉,然后重新update wsdl地址,重新编译。费了一番周折,让C#客户端跑起来。 我期待以久的结果重于出现了: 测试SystemUser Web Services 看来,客户端SOAP头的一些信息,让Axis迷惑了,出了bug,不过同样的调用,如果用WebSphere的Web Services引擎,是没有任何的问题的。SystemUser truename: 总系统管理员 account: administrator email:admin@hisoft.com address:大连大连大连大连大连大连大连大连大连大连大 遇到这样的问题,看来必须静下心来,多尝试了。 以前一次,用RAD发布的Web Services在它自带的WebSphere Base6.0上总是出错,我都发布了几十遍,花了一天半的时间。同样的代码,我后来突然想到部署在WebSphere ND6.0上,一切都ok了。你说,这种问题怎么去诊断?谁会告诉你是websphere本身的问题?因为我查阅了好几本IBM 的RedBook,以及IBM网站,都没有找到满意的结果。 附那个返回的xml,看到5aa和最后的0了吗: HTTP/1.1 200 OK Date: Sat, 19 Aug 2006 10:04:04 GMT Content-Type: text/xml; charset=utf-8 Content-Language: en-US Transfer-Encoding: chunked 5aa <?xml version="1.0" encoding="utf-8"?> <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <soapenv:Body> <ns1:checkAccountResponse soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:ns1="http://webservice.infocenter.zmd.hisoft.com"> <checkAccountReturn href="#id0"/> </ns1:checkAccountResponse> <multiRef id="id0" soapenc:root="0" soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xsi:type="ns2:SystemUserVO" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:ns2="http://vo.infocenter.zmd.hisoft.com"> <account xsi:type="xsd:string">administrator</account> <address xsi:type="xsd:string">大连大连大连大连大连大连大连大连大连大连大连</address> <deleteflag xsi:type="xsd:int">0</deleteflag> <email xsi:type="xsd:string">admin@hisoft.com</email> <idcard xsi:type="xsd:string"></idcard> <password xsi:type="xsd:string">200ceb26807d6bf99fd6f4f0d1ca54d4</password> <sex xsi:type="xsd:string">M</sex> <stopflag xsi:type="xsd:int">0</stopflag> <telephone xsi:type="xsd:string">1</telephone> <truename xsi:type="xsd:string">总系统管理员</truename> <userid xsi:type="xsd:int">372</userid> </multiRef> </soapenv:Body> </soapenv:Envelope> 0 记下此次经历,下次debug时要灵活些。 August 16 Webwork及框架设计现在这个项目已经做了半年多了,我也是从需求调研时开始加入的,现在正是设计完成后的紧张编码阶段。具体的编码工作我已经做了整整两周。我主要是开发后台,也就是权限管理相关部分。部分后台我们是用webwork+Spring+Hibernate,一种很流行的J2EE开发模式,我现在主要是归纳总结一下。
(一) 表示层(Presentational Layer)Webwork总结 作为一个表示层框架,无论是Struts、Webwork,还是SpringMVC、JSF,它们都是解决同一类问题。 表示层框架,从分层的角度考虑,它的核心是作为一个Front Controller,在Struts里面是ActionSerlvet+RequestProcessor+Action,前两个是框架本身的,只有Action需要我们处理。它的职责也就是协调用户的输入,将请求dispatch给后端business,由后端business系统处理后,将相应的结果返回给Controller,再由Controller处理返回给用户。 对于开发来说,特别需要注意的是职责分配的问题,这是我们开发过程中最容易犯的毛病,也是最需要注意的地方。Front Controller只是一个Controller,也就是Call别的接口,自己本身不应该有处理业务的职责,它应该尽量的thin。如果不遵守这个原则呢?那带来的将是系统难以测试,难以重用,难以维护。 具体来说,我们的业务系统是可以重用的,可以提供Service接口给Web层调用,也可以提供给其它系统通过Web Services接口调用,还可以给分布式client调用。它们的调用方式,一般有三种: 最普通的调用。在web系统中,就是表示层Controller直接调用服务层。 依赖注入式调用(Dependency Injection)。它需要IoC容器支持,如Spring,picoContainer,它对解耦和测试非常有用 依赖查找式调用(Dependency Lookup)。也就是EJB里面的JNDI,它最大的好处是支持分布式开发和组件开发。 我们一定要牢牢记住表示层的职责。表示层框架很容易变,但系统业务还是相对固定,如果你接触过物流系统,你就会明白什么叫业务了,那时候在表示层里处理业务会非常可怕,因为我的业务要同时支持几种客户端:桌面client、Brower、移动client,难道你需要在每种client里面写业务吗?而且,表示层框架的开发人员可以不需要懂太多的业务就可以利用自己的技术优势,来调用业务相关接口。
下面我再具体谈谈表示层需要处理的几个部分: 整体说明 举个例子,我现在想对updateUser.do?userId=19做出请求,然后在一个UpdateUserAction里处理用户预更新,然后将结果导向到updateUser.jsp页面。这大概是我们所希望的最简单的处理方式吧。 上面那三个元素的相互关系在web框架的配置文件里面定义。现在的framework都倾向于config和metadata oriented,因为这样的框架会比较灵活、好维护,试试直接用JSP和Serlvet开发你就知道要做多少重复工作和hard code了。 在webwork里面,是xwork..xml,在Struts里面,是struts-config.xml,在JSF里面,是faces-config.xml。所以说,框架都是相通的。我这里需要特别指出的是,上面那些文件和文件名都是默认的,实际上,为了支持组件开发和团队协作开发,保持开发模块之间的松耦合,我们应该将配置文件分解成一个个的segment。 对于action,也就是前面提到的Front Controller。 在Struts里面,是通过继承基类的Action,在webwork,是通过实现Action接口。 在Struts里面,通过继承方式来实现Action,那么我们代码是和框架紧密耦合的。而且,只支持JSP,不支持模板语言,如Velocity、FreeMaker,而这些可以脱离Servlet容器,最大的好处是,我们的网页设计人员可以专心做html界面。分工就意味着效率和质量。但Struts这种框架就支持不够。 而webwork就很好地做到了这种解耦,action和前端表示完全分离,如表示页面还可以直接发布成pdf文档。 那么,继承基类和实现接口,对于我们开发人员意味着什么呢?灵活性、容易测试,这让我开发更容易一些。 Scenario 1 我们一些简单的CRUD操作,完全可以在一个action里面处理。虽然Struts也可以这样,因为它有一个DispatchAction,但你会发现,项目中,我们经常引入一个BaseAction,而这个BaseAction是继承DispatchAction还是Action,往往很难决择,因为我们这两种需要都有,虽然DispathAction也是继承Action。而webwork的处理就方便多了,你需要验证、国际化、安全等等很多操作,你都可以通过实现一个接口实现。 Scenario 2 我们测试Action的时候,总是要启动容器,启动一般要花将近半分钟,这会让我们很多时间都花在等待之中,由于Action和容器脱离,我们的测试会更迅速和直接。
我现在就按照整个页面流程来分类说明,在一个web框架下开发,或是开发web应用,或者开发Web Framework必须考虑到的各个方面: 1. 表单提取 我们开发web MIS系统,从技术角度考虑,第一步就是在页面输入数据,然后将它们保存到后端数据库。 框架应该自动提取表单数据,将这些String类型的数据转换成对应的数据类型,并封装为值对象(Value Object),提供给Controller,作为Controller的输入。 Struts是通过ActionForm,非常笨拙,而且对类型转换都支持不够。Webwork在这方面做得优雅多了,它将请求参数封装为一个Map对象。其实,请求本来就是key-value对。然后当数据传入到action后,就和Serlvet容器,web脱离了关系,这带来的解耦的好处就是还可以支持多种客户端,支持容器外测试。 具体实现上,Struts通过BeanUtil工具对ActionForm进行反射;webwork是通过一个拦截器Parameters Interceptor来获取,而action实现该接口,然后在配置文件里申明一下就ok了。 2. 表单验证 我们输入数据,一般需要符合一定的规范,如必须输入数字,不能为空,必须为邮政编码等等。 解决这个问题,如果不用framework,我们需要在jsp页面上用javascript验证,还要在servlet里面获取参数,然后验证,对于不合理的数据,需要将之保存,并附上错误说明,然后forword到原来jsp页面显示处理,非常烦琐和机械。 Struts框架原来处理还是挺机械的,主要在ActionForm的validate方法里验证,后来加入一个Validation框架,也就是一个plug-in,非常好用。Webwork也沿用了这一点,两者在验证上都不错。 但我们必须注意的一个问题,那就是验证不通过时,怎么保持原来的数据,特别是下拉框里面的数据,Struts处理很麻烦(难道我没用好?),webwork好用多了,不过有个tips,那就是将select的下拉列表保存在session里,不用每次验证不通过都重新从数据库加载。 另外还有一点,那就是和表单重复提交结合时会出现的问题,struts中,处理不当,就会出现在你验证不通过,修改后再次提交时总是被提示重复提交。 Webwork是通过一个Interceptor截获,处理很自然。不过我建议时提交通过后,用redirect,这样就不存在重复提交的缺陷。你总不希望因为网络慢,你的订单由于刷新而提交两遍,收到两份相通的货物吧。 3. 国际化和resource文件 Web界面是给用户看的。譬如,我们公司内网打算采用一套很知名的blog系统,但它是德国人开发的,但是你总不会希望看到界面是德文吧。这套blog开发商为了全球推广,也不至于为每种语言都开发一套吧,那不累死,维护简直就是恶梦。 我之所以现在要提到国际化,不光是国际化的问题,因为在表单验证时,如果field不通过,都会提示用户,但这些提示语一般都会重用,如create和update用户时“用户名不能为空!”,我们可以在resource文件里定义一个key:error.user.username.required,然后在各处引用。 Webwork和Struts对这个都处理比较好,这也是一个表示层框架必须处理的。我建议不要在Action-validation.xml文件里hard code这些信息,而是将它们分离出来。 4. 请求路由 当我们将user成功提交后,我们让它由谁来处理呢?这就是请求路由的问题。 这种关系在配置文件里面定义,我上文都提到过。为了方便,往往将多个请求提交到一个action的不同方法,这是我们应该好好利用的。而它们在webwork里面都可以很好配置。例如,将saveCreatedUser 路由到UserAction的saveCreatedUser方法里。 5. 请求处理 当请求发生,并且路由到正确的action后,我们的action就要处理它了,包括初始化,处理,清场。 关于Action需要注意的问题,我现在还要重申的是:一定要注意它的职责:不要处理它不应该处理的事情。 例如,注册用户时,用户姓名可以重复,但用户登录账号却不能重复。论坛发帖子就没有这样要求。再例如,会员系统中,用户积分达到100就是可以下载,但什么时候规则变了,需要积分150才能。这些规则在哪儿处理呢?最简单当然是在Action里:没有满足条件,就forward到输入页面。当规则很多时,你就很难维护了。反问一下,它是web层关心的问题吗? 在任何框架设计,包括web框架设计时,一般都有一个非常核心的模式,那就是template模式,它和一种称为callback的调用方式相关,也就是那个著名的好莱坞原则:don’t call me, I’ll call you! 具体到我们的web框架,也就是action,如webwork里面的ActionSupport,它们都是这种处理方式,我们不知道我们的Action是被谁调用的(当然是框架了),但我们这样写,然后在xml一配置,就work well。 我们可以研究一下Struts的ActionSerlvet,特别是ReqeustProcessor,后者就是一个典型的template模式,action是作为里面一个command运行的。Command模式也是用callback调用。关于这两种模式,google一下就清楚了。 Spring就是使用Template和callback模式,对底层实现做了统一封装,其中template负责那些通用的功能,如事务、资源管理、异常处理,callback则完成变化的那部分,如处理具体的数据库statement操作。我们最常用的HibernateCallback和HibernateDaoSupport就体现了这一点,具体可参照其源码。 另外,对于Action,一定要注意线程安全的问题。Struts的action是非线程安全的,大家可以参考一个RequestProcessor中定义的Map类型的actions字段,而Webwork是线程安全的,因为其ActionContext是 ThreadLocal的,也就是说每个线程都有自己的变量,互不干扰。具体体现是Struts的Action里面不能有字段申明,只能有局部变量。而Webwork可以,这对于获取页面表单数据非常方便。 6. 请求应答 在Action执行完毕后,我们应该为其导航到结果页面, 在webwork里面,也就是Result Type。它定义了好几种方式。比较有意思的是它处理象freemaker(html模板),XML/XSLT,PDF这些格式输出。而Struts基本上是很单一JSP。 而且webwork还可以自定义Result类型,可以为我们提供Swing、mobile客户端显示。 7. 异常处理 任何操作都有出错的可能,怎么优雅地处理错误,这也是每个框架都应该考虑的。 有一些异常处理最佳实践,《Effective Java》就专门有一章谈到。在OnJava上也有一篇文章:http://www.onjava.com/pub/a/onjava/2003/11/19/exceptions.html,非常值得一读。 1.选择Checked还是Unchecked 2.Exception的封装 3.如无必要不要创建自己得Exception 4.不要用Exception来作流程控制 5.不要轻易的忽略捕获的Exception 6.不要简单地捕获顶层的Exception Struts和Webwork里面都支持异常处理,webwork是通过Interceptor支持的:ExceptionMappingInterceptor。 在我的项目实践中,我喜欢将业务异常封装在业务层,申明为checked Exception,web的Action必须捕获,获取exception的ErrorCode,此ErrorCode就是resource文件里面的key,也就是说它支持多语言,如ErrorCode.USER_EXISTED=”error.user.userExisted”。这类信息应该是业务层抛出的。如果将它由action处理,这是一种设计上的错误。 8. 灵活性、扩展性、可插入性 作为框架,就应该保证其灵活,支持不同用户的业务需求;容易扩展其功能,插入系统级的功能。 就Struts而言,在可插入性上还是不错的,这得益于它的plug-in支持,实现其PlugIn接口,然后在其struts-config.xml文件中配置一下,如它的验证框架、网页布局Tiles,内存数据库,都是其典型应用。 另外,以前的Struts还可以扩展它的RequestProcessor,实现一些特有的功能,如日志、安全,因为它预留了一个缺省方法processPreprocess()。 Webwork也是高度可扩展的,它主要是通过实现Interceptor接口。 9. 模块、组件化 在大型web项目中,模块、组件这些概念就非常适用了,因为一个大的工程,可能由不同公司开发,或是不同项目组、不同成员协作开发,最后要很容易地集成在一起。另外,还有第三方公司开发的组件插入进来,如漂亮的web界面组件,如Tree、table等。 Tapestry就是一种基于组件的开发框架,不过我没有研究,呵呵。 但webwork确实可以支持,如xwork.xml 配置的package就是支持模块申明,其附带的FreeMaker就可以将开发好的模块打包成jar文件,在Servlet容器外加载。 JSF对组件支持是相当到位的。它所设计的目标,就是在IDE里面,直接拖拽web组件,象table,tree,就可以形成漂亮的界面,真正实现所见即所得。实现自己的组件,只要实现其UIComponent接口就行了。应该说JSF是一种非常有前途的Web层框架,它会把Web开发真正达到easy。因为直到现在,还没有一种框架能够很容易做到,它的界面开发始终没法和微软相提并论,虽然它有界面布局框架Tiles和SiteMesh,以及模板语言Velocity、FreeMaker、Servlet2.4支持的EL。 不过遗憾的是,JSF开发工具还没有达到那么完善,除了Sun自己一直、推广的NetBeans IDE。Sun的官方有其demo,用flash演示,还是蛮cool的。
关于Ajax和请求模式 Ajax其实早在五年前就有了,只是没有被人注意罢了,直到2005年google把它应用在google map和gmail上,才开始被人关注。它也是web2.0时代一种重要的技术,ajax强调用户体验(XP)。因为它的本质就是一个异步调用。而我们的一般web页面请求都是同步的,你必须等到服务器全部处理完成。同步变异步,确实是一种革命,虽然它的技术很简单,只是一个XMLHttpRequest的javascript对象。它让我们的Web版的Rich Client成为可能。Google现在正看到了web office的应用前景,对MS构成巨大威胁,大家可以试试它的Web版的excel和、日历、邮件,不久就会推出正式的web word,很cool。 另外,Ajax更接近实际的web请求,因为一般web请求时,每一次,我们获得的是整个页面内容,无论这个页面有多少数据是冗余的。譬如,我们分页浏览时,整个页面布局是由header,left navigator、footer组成,中间是content,翻页时,应该从服务器返回一个xml的用户列表数据格式,而且不包括html table的tag,这正是ajax另外一个方面:返回的是数据,而不是整个页面。这对于窄带宽,也是非常有益的。 Webwork的2.2.2版本支持ajax,底层是dojo和DWR这两个著名的ajax框架。但是webwork的ajax Tag做得太粗糙,用了ajax后,页面布局,特别是表单位置没法控制,而且对IE浏览器支持不够。我现在几乎都放弃了。 没有好的ajax框架,使得ajax开发起来很困难,也许因为它还不到一周岁吧。 我们现在的web框架,一般都是基于请求的模式,也就是说我们请求时总是会考虑它的目的URL,将提交数据保存在该请求中。这种模式,让我们开发时总在考虑这些所谓的http协议。一种完善的框架,应该尽量去屏蔽这些与底层协议相关的东西。 JSF给我们一种全新的方式,它让我们按照操作桌面应用,如Swing程序的方式处理网络请求,因为它是基于事件的。在Sun的petstore的WAF框架中就有这种原始的思想。基于事件,让我们开发rich client提供了一种思想。 事件驱动有一种核心的模式,那就是Observer模式,所谓的Observer就是listener(只是一个用眼睛,一个用耳朵,呵呵)。Oberver是一种推(push)模式,也就是说事件Event发生时(如model发生改变),它会通知你,而不是你去取(拉模式)。 在Servlet规范中,也引入了listener的概念,如HttpSessionListener,它对于监视用户上线下线非常有帮助,它们是生命周期lifecycle的范畴。 April 30 你的灯亮着吗---发现问题的真正所在这周拜读了温伯格先生的《你的灯亮着吗?》,一本薄薄的册子,才120来面。记得两年前读了他的《程序开发心理学》,也受益匪浅。不过,《程序开发心理学》的部分观点,在《人件》这本书里也有涉及。大学时候,喜欢研读心理学书籍,所以感觉《程序开发心理学》略为粗浅。 问题是什么?这是本书提出的一个问题,温伯格先生说得真是一针见血:问题其实就是你期望的东西和你体验的东西之间的差别。 我查了一下金山词霸的高级汉语词典,里面的解释是:需要解决的疑难、矛盾。是不是还是很抽象?另外,也查了一下美国传统词典里的problem,感觉也深入不到哪里。 第一次读到这句话感觉似曾相识,因为弗洛伊德创立的精神分析学有几个核心概念,其中就有本我和超过。应该说整个弗洛伊德心理学的根基就是本我和超我,由此而来的就是潜意识理论。就像马克思的整个经济学理论是建立在剩余价值理论上的一样。 问题是什么? 似乎很简单吧。在温伯格的一个寓言里,解决起来竟然这么复杂。那个故事大意是,在一栋写字楼里,有一个电梯,一段时间,那个电梯变得特别慢,在电梯里的人都感觉非常漫长。当然,问题也就出来了。 房客有问题了,房东自然也有问题。他们的问题是否一样呢?当然不是,要是这样,问题就好解决了。房客的解决方法是给房东施加压力,房东的解决方法是怎样让房客不感觉到乘电梯时间长,怎样避开房客的侵扰。 问题在哪里?自然在电梯。很简单吧,不过其间的一系列矛盾处理方式你还是不会觉得很滑稽的。 那么,问题最后怎么解决的呢?原来在电梯一周年检修时,电梯厂家发现电梯里面的一条线路被老鼠咬了。解决这个问题后,大家就相安无事了。 大家都没想到吧。 后来,还有更多的解决方案,哪一种都比他们双方原来采取的那种好。 问题是什么?应该说真正的问题是什么?在故事的开始,双方都没有找到,抑或找到了,他们也没有找到根本的解决办法。 从这个故事,我们还体会到,解决一个问题时,我们最先会从自己的角度出发。因为这,我们往往没有发现问题的本质,或没有找到真正的解决办法。 在商业上,如果我们转变一下自己的思路,以顾客为中心;在公司里,以人为本;在项目开发上,以用户为中心(譬如现在的web2.0的设计思想)。也许,这样更能为我们创造价值。微软Windows XP的理念是什么,大家从它的名字一定也能够猜出吧:用户体验(eXPerience), 有笔记本的朋友,用XP时应该深有体会吧。 记得曾经听过一个小故事。一次乘电梯时,有人问为什么电梯里会装上镜子,听者的回答真是五花八门,有人说可以进门时整整仪容啊,有女士人说防止后面有人占便宜啊,总之很多很多,其中一个人的回答很让人深思:因为装了一面镜子,那些坐轮椅上楼的残疾人就不用转身看电梯到达那楼了。这个答案,我们也想到了吗? 温伯格在讲述问题时,给我们分了几个主题: 问题是什么? 什么是真正的问题? 这是谁的问题? 问题从哪里来? 我们真的想解决问题吗? 其实,读了这本书后,我发现这些主题其实都是将的一个主题:什么是最本质的问题?如果回答了这个问题,其它的自然迎刃而解。 大学时,有个很好的异性朋友,她经常告诉我她很苦恼,起先,我总是给滔滔不绝地告诉她应该怎样怎样,后来我发现,她真正需要的,只是一个倾诉对象,所以,我当一个忠实的听众就够了。因为,后来,我发现我的建议并不能解决什么实质性的问题,其实,她也没有什么问题,心情偶尔不好,很正常嘛。
书中很多观点和建议,即使过了20多年,依然还能引起我深深的共鸣: 人们很少知道他们需要什么,直到你给了他们所需要的(在你第一次用QQ聊天时)。 任何一种问题解决方式,都会带来新的问题(当你买了手机后不久)。 你永远都不可能对问题有一个正确的定义,但永远不要放弃对寻求的努力。 当别人能够很好地解决自己的问题时,千万不要越俎代庖。 很多时候别人需要的只是提示,而不是解决办法(想想你见到的交通指示牌)。 …… …… 我极力向大家推荐这本书,为什么呢?因为,生活的每天,其实就是不断的发现问题,再不断地解决问题。很多时候,解决问题是不是也充满乐趣呢?譬如,你一直想的那个心仪已久的人,但是...... ?这难道不是一个问题吗?那么,怎么去解决呢?首先,那个人真正需要的什么,你真的了解吗?
2006年4月29夜
|
|
|||||||||
|
|