Hibernate里的重复记录

前几天APIS爆出了个奇怪的BUG:某个小组关闭任务后,任务自动被duplicate,然后子任务也丢失了。手工查看数据库,发现数据库里居然只有一条Task记录,Hibernate却返回两条。findById(id为主键)居然会返回两条记录,这就奇了怪了。
用Firebug看记录,发现抛出了异常。”More than one row with the given identifier was found”。可是数据库里明明只有一条记录啊。二话不说google发现了这个post,顿时想起来由于关联(association)而产生万恶的outer join。排查了几个关联(出问题的那个类偏偏是最heavily-associated),终于在一个与Part类<one-to-one>上发现了问题。虽然是<one-to-one>关联,却发现另外一方(Part)存在重复的记录,直接影响到Task类。找到原因,assign给Part类的责任人,问题解决。

打算写一个简单的数据库迁移工具

之前做项目一直被数据库schema改动折腾得不行,找了不少工具,比如migrate4j,dbmigrate和liquibase之类的,感觉都不是很满意。其实我的需求很简单,不需要数据库的DSL(比如migrate4j或者db:migrate的那种用Java/Ruby描述DDL),只需要纯的SQL(也就是db:migrate生成的SQL),只需要前向操纵(即不需要回滚),有Eclipse的插件。
现有的migration工具,都是使用分开的文件作为不同版本的内容,这带来了很大的灵活性。但是我的打算是对现有的开发模式有最小的影响。比如现在项目的开发方式是使用一个schema文件外加几个基础数据文件。于是我打算直接使用SVN作为数据库版本的保存工具,利用SVN的特性做版本化。最直接使用的就是SVN的版本号。不过这样的问题在于基本无法做rollback,除非使用两个文件,一个construction,一个destruction,但这要求两个文件一起提交,带来了不少复杂性。现有项目的schema不需要做rollback,正好这么做。工作的方式为:更新SVN,获得最新的schema文件和当前的版本做对比,把增加的部分(要求更新append在最后)执行一遍。就这么简单。
调查了一下午+半个晚上,基本把一些技术方案给调查清楚了。主要的难度在于和subclipse的集成。既然功能简单,就要做到易用。于是我打起了subclipse的主意,主要是subclipse保存的SVN用户名密码。发现subclipse还挺变态的,JavaHL的接口使用HOME目录保存,SVNKit接口使用Eclipse的keyrings文件。
这两天就动手,代码参考较为简单(或者说简陋)的dbmigrate。

使用Cache-Control和gzip提升tomcat应用性能(整理)

这个其实应该是常识,只不过以前做的J2EE应用大部分是内网里跑的东西,所以性能上没什么问题。这次APIS由于有在外面用的可能,加上使用了一些比较大的javascript框架(Ext),所以性能问题瞬间窜了上来。
以前做的J2EE应用没有使用上达500K的框架,最多就是几十K的Prototype,所以没什么问题。一个页面一般也就几十K最多了。但这次还在开发中的APIS,由于还在用debug版本的库,所以单单Ext就膨胀到了一个多M,加上不知道是Struts还是Tomcat默认写入Response的cache-control: no cache,在远程用起来就很慢,一般一个页面需要十多秒种甚至更久,实在无法忍受。前几天集中解决了问题。
首先是Cache-Control的问题,Google了好一阵,没有什么直接配置的方法,只好自己抄了一个一个Filter,通过和web.xml里配置的配合勉强凑合着用。一般就是对*.do实施no-cache政策,其他需要缓存的img, js文件,统统加上长达两周的缓存期限。ETag实在不会用,就先用这个缓存策略吧。
Filter的代码:

  1. public class ResponseHeaderFilter implements Filter {
  2. FilterConfig fc;
  3. public void doFilter(ServletRequest req, ServletResponse res,
  4. FilterChain chain) throws IOException, ServletException {
  5. HttpServletResponse response = (HttpServletResponse) res;
  6. // set the provided HTTP response parameters
  7. for (Enumeration e = fc.getInitParameterNames(); e.hasMoreElements();) {
  8. String headerName = (String) e.nextElement();
  9. response.addHeader(headerName, fc.getInitParameter(headerName));
  10. }
  11. // pass the request/response on
  12. chain.doFilter(req, response);
  13. }
  14. public void init(FilterConfig filterConfig) {
  15. this.fc = filterConfig;
  16. }
  17. public void destroy() {
  18. this.fc = null;
  19. }
  20. }

web.xml里的巧妙配置:

  1. <filter>
  2. <filter-name>NoCache</filter-name>
  3. <filter-class>apis.server.common.util.ResponseHeaderFilter</filter-class>
  4. <init-param>
  5. <param-name>Cache-Control</param-name>
  6. <param-value>no-cache, must-revalidate</param-value>
  7. </init-param>
  8. </filter>
  9. <filter>
  10. <filter-name>CacheForWeek</filter-name>
  11. <filter-class>apis.server.common.util.ResponseHeaderFilter</filter-class>
  12. <init-param>
  13. <param-name>Cache-Control</param-name>
  14. <param-value>max-age=604800, public</param-value>
  15. </init-param>
  16. </filter>
  17. <filter-mapping>
  18. <filter-name>NoCache</filter-name>
  19. <url-pattern>*.do</url-pattern>
  20. </filter-mapping>
  21. <filter-mapping>
  22. <filter-name>CacheForWeek</filter-name>
  23. <url-pattern>/images/*</url-pattern>
  24. </filter-mapping>
  25. <filter-mapping>
  26. <filter-name>CacheForWeek</filter-name>
  27. <url-pattern>/img/*</url-pattern>
  28. </filter-mapping>
  29. <filter-mapping>
  30. <filter-name>CacheForWeek</filter-name>
  31. <url-pattern>/icons/*</url-pattern>
  32. </filter-mapping>
  33. <filter-mapping>
  34. <filter-name>CacheForWeek</filter-name>
  35. <url-pattern>/ext/*</url-pattern>
  36. </filter-mapping>
  37. <filter-mapping>
  38. <filter-name>CacheForWeek</filter-name>
  39. <url-pattern>*.js</url-pattern>
  40. </filter-mapping>
  41. <filter-mapping>
  42. <filter-name>CacheForWeek</filter-name>
  43. <url-pattern>*.css</url-pattern>
  44. </filter-mapping>

(插入一段:在探测这些性能问题的时候,我使用的是一个Firebug的插件,也就是Firefox插件的插件-YSlow,好像是Yahoo的,结合Firebug里XHR的Net这块做Profiling,效果很不错,很容易就知道瓶颈)
还有一个gzip的办法,就是在服务器压缩内容,再传给浏览器。现在主流的浏览器都支持gzip压缩,而且这些html和js文本压缩起来很厉害,基本上可以有40%的压缩率。办法在servel.xml的注释里也有写,就是在Connector元素里加上
compression=”on”
compressionMinSize=”2048″
noCompressionUserAgents=”gozilla,traviata”
compressableMimeType=”text/html,text/xml,text/javascript,text/css,text/plain”
以上的内容大部分都是Google得来,我自己做了一下整理

解决Ubuntu升级8.04 “未能计算更新”问题

其实问题也不难,只要好好看提示就可以了。提示里说查看/var/log/dist-upgrade/里的内容,之前粗粗地看过一遍也没在意,main.log里没问题,term.log是空的,apt.log里东西挺多没仔细看。一直以为是cn99源的问题。换了cn.archive.ubuntu.com也失败。
今天把ubuntu-alternative给下了下来,发现还是这个问题。于是好好看了apt.log,发现有这么一句话:

language-support-cn has broken dep on openoffice.org-l10n-zh-cn language-support-cn has broken dep on openoffice.org-l10n-zh-tw

难道这几个东西之间有冲突?卸掉openoffice l10n的包以后,问题解决。
结论:仔细看日志。

Upgrading to Spring Security 2.0(zz)

原文:http://raibledesigns.com/rd/entry/upgrading_to_spring_security_2
就是appfuse作者的博客,做了一些精简
1. 包变化:org.acegisecurity  => org.springframework.security
2. 依赖变化(略,不用Maven)
3. tag标签的开头authz => security, 然后把taglib的关联项改为

<%@ taglib uri="http://www.springframework.org/security/tags"
    prefix="security" %>

4. web.xml,把<filter-class>改为org.springframework.web.filter.DelegatingFilterProxy,另外还要加上<init-param>标签

    <init-param>
        <param-name>targetBeanName</param-name>
        <param-value>springSecurityFilterChain</param-value>
    </init-param>

5. 修改security.xml,使用新的语法.根据作者的说法,AppFuse的security.xml的长度从177行下降到了33行,因为使用了很多convention over configuration的元素,如<http auto-config=”true”/>。关于语法,还需要一些实践把握。

java diff 及wiki相关

diff的原理在于找两个字符串之间的最大相同子串(Longest Common Subsequence)以及编辑距离,比较有名的实现是UnixLinux上常用的diff(GNU Diff)。

实现

Java里Diff的实现我找了一下,主要是两个,java-diff 和bsmi上的Diff ,前者为LGPL,后一个为GPL。其实代码也都不多,都实现了LCS算法。前一个协议上对我们比较有利,而且文档、测试和例子多一些。
JavaDiff里主要有两个类,Diff和Difference类。前者是算法,后者是差异的表示类。下面讲一下例子:

Object[] a = new Object[] {         "a",         "b",         "c",         "d",         "e"     };     Object[] b = new Object[] {         "a",         "x",         "y",         "b",         "c",         "j",         "e",     };     Difference[] expected = new Difference[] {         new Difference(1-1,  1,  2),         new Difference(3,  3,  5,  5),     };     Diff diff = new Diff(a, b);     List diffOut = diff.diff();

差别有三处,用两个Difference对象表示。一个Difference对象表示替换,增加,删除。Difference的构造函数:

public Difference(int delStart, int delEnd, int addStart, int addEnd)

如果delEnd或者addEnd为-1的话,就代表没有删除或者增加行为。
回到例子,两个字符串之间的差别在于,目标字符串在第1-2行(从0算起)增加了x,y,第3行的d被第5行的j替换。Difference虽然只说明了行号和动作,但我们可以推算出来增加了什么,删除了什么,替换了什么。下面是另一个更长的例子,来自测试用例:

public void testStrings1()     {         Object[] a = new Object[] {             "a",             "b",             "c",             "e",             "h",             "j",             "l",             "m",             "n",             "p"         };         Object[] b = new Object[] {             "b",             "c",             "d",             "e",             "f",             "j",             "k",             "l",             "m",             "r",             "s",             "t"         };         Difference[] expected = new Difference[] {             new Difference(0,  0,  0-1),             new Difference(3-1,  2,  2),             new Difference(4,  4,  4,  4),             new Difference(6-1,  6,  6),             new Difference(8,  9,  911),         };         runDiff(a, b, expected);     }

上面比较的都是一个个字符串的差异,推广一下,把每一行文本当作一个字母,就可以得到文件的差异。在java-diff的etc下有一个FileDiff.java,是一个很好的参考。得到之间的差异之后,我们要把这个差异表示出来,这个需要包装一下,不过难度不大。

版本保存

还有一个wiki版本的保存问题。大的维基引擎如MediaWiki(就是维基百科那个,顺便说一下,维基百科的英文版终于可以访问了)没时间研究,就是 JSPWiki也没来得及看)(JSPWiki连数据库也不用,Web用自己写的框架,可读性可能比较不行)。只研究了trac的wiki实现。trac 的wiki实现很简单,就是把每一个版本都保存在数据库,毕竟都是文本的,还可以接受。每次比较的时候就从数据库里取两个版本出来做一个diff,具体实 现在PYTHON/site-packages/trac/wiki/web_ui.py(_render_diff函数)。trac提供两种形式的 diff结果,一个是tabular的表格形式,就是很直观的对比,还有一个是Unified的形式,也就是经常看见的diff结果。这是通过页面上 javascript读table里的文字转换成Unified格式的diff文本,虽然个人不推荐这种方式。wiki的文本修改又有一个特点,就是每一 行其实内容可能比较多,只改了几个字,这样就要对这一行的两个版本再做一个diff,然后把删除的文本用<del>标签,增加的文本用 <ins>标签展示出来。
最后提一下JSR-170,一个用来管理仓库内容(主要是大型CMS)的API,支持版本控制,存储多元化,很复杂,有两个商业实现和一个Apache JackRabbit的开源实现,这里 是一个参考资料。JSR170也是里面的例子也是把每一个版本都存储下来。

参考资料

看来最好的代码阅读器还是IDE

这两天要研究下Acegi,给人做技术讲座,内容要求和Acegi有关。于是想找个代码阅读器来看代码。
第一个想到的就是SourceInsight,但考虑到SourceInsight还是收费软件,于是转向开源产品。搜了半天,找出一个Code Browser,没想到功能比Notepad++还差一些。于是无奈就用Eclispe看看代码。因为Acegi和Spring结合得很紧密,顺便把Spring IDE的帮助看了一下,第一次用了起来,觉得很好用啊。这么多年开发Spring应用居然都没好好用Spring IDE,真是惭愧。
Attach上Source的Eclispe+Spring IDE看起Acegi代码果然非常方便,按住Ctrl进行智能导航,还有引用查找等。怪不得一直找不到合适的开源代码查看器,原来IDE就已经这么好用了。

Validator原来不可以Render两次

公司有一个简单功能的PrintPanel,这两天却一直用不上,报以下的错误:

[ArgumentException: 已存在具有相同键的条目。]    System.Collections.Specialized.ListDictionary.Add(Object key, Object value) +283    System.Web.UI.ClientScriptManager.RegisterExpandoAttribute(String controlId, String attributeName, String attributeValue, Boolean encode) +237    System.Web.UI.WebControls.BaseValidator.AddExpandoAttribute(Page page, HtmlTextWriter writer, String controlId, String attributeName, String attributeValue, Boolean encode) +105    System.Web.UI.WebControls.BaseValidator.AddAttributesToRender(HtmlTextWriter writer) +188    System.Web.UI.WebControls.RequiredFieldValidator.AddAttributesToRender(HtmlTextWriter writer) +21    System.Web.UI.WebControls.WebControl.RenderBeginTag(HtmlTextWriter writer) +17    System.Web.UI.WebControls.BaseValidator.Render(HtmlTextWriter writer) +459    System.Web.UI.Control.RenderControlInternal(HtmlTextWriter writer, ControlAdapter adapter) +53    System.Web.UI.Control.RenderControl(HtmlTextWriter writer, ControlAdapter adapter) +294    System.Web.UI.Control.RenderControl(HtmlTextWriter writer) +24    System.Web.UI.Control.RenderChildrenInternal(HtmlTextWriter writer, ICollection children) +199    System.Web.UI.Control.RenderChildren(HtmlTextWriter writer) +21    System.Web.UI.WebControls.WebControl.RenderContents(HtmlTextWriter writer) +7    System.Web.UI.WebControls.WebControl.Render(HtmlTextWriter writer) +29

实在是看不懂,而.NET又不像Java一样是开源的,可以一直跟进去看代码。网上又查不到错误。只好还是回到唯一的线索:StackTrace。发现是从BaseValidator.AddExpandoAttribute这个私有方法抛上来的,于是把Validator都去掉了,发现没问题了。研究了一会儿过后,发现应该是ClientScriptManager里把相同的Javascript给注册了两次造成的。把Validator设成Disable没用,只有Visible=false管用。