<?xml version='1.0' encoding='utf-8'?>
<rss version='2.0' xmlns:content='http://purl.org/rss/1.0/modules/content/' xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:slash='http://purl.org/rss/1.0/modules/slash/'>
<channel><title>廖雪峰的官方网站</title><link>http://www.liaoxuefeng.com/feed.xml</link><description>廖雪峰的官方网站</description><pubDate>Sun, 07 Feb 2010 20:56:59 +0800</pubDate><generator>expressme.org</generator><language>en</language><item><title>Amazon Kindle 2中文升级攻略</title><link>http://www.liaoxuefeng.com/it-4b605cbeadcf42318ca5d902acb2d442-1</link><dc:creator>廖雪峰</dc:creator><pubDate>Sun, 07 Feb 2010 20:56:59 +0800</pubDate><description><![CDATA[<p>托朋友从米国终于带回了Kindle 2，加上皮套一共才2000 RMB，比国内各大品牌的阅读器都要便宜很多，而且可以直接免费使用3G网络，自带的浏览器能直接浏览网站（估计是联通WCDMA信号？确实不知道为啥能免费使用）。</p>
<p>不过Kindle 2仍然不支持中文，需要经过hack才能支持。在国内搜索了半天，发现中文支持最后都是从<a target="_blank" href="http://blogkindle.com/">Blog Kindle</a>这个国外牛人搞出来的升级包，在<a target="_blank" href="http://blogkindle.com/unicode-fonts-hack/">Unicode Font Hack</a>一文里有详细的升级步骤和所有升级包下载，大概读了一下，基本步骤如下：</p>
<p>1.首先，根据Kindle 2的型号（从Kindle机器后面的序列号看，Kindle 2是B002开头，Kindle 2国际版是B003开头，Kindle 2 DX是B004开通，Kindle 2 DX国际版是B005开头）下载对应的升级包，作者已经做了好几种字体的升级包，对于中文用户，如果你不想自己从Windows上搞微软雅黑字体，推荐直接使用Droid字体的升级包，这是Android手机默认的中文字体，显示效果很不错，完全够用。例如，Kindle 2国际版的升级包就是：</p>
<p><a target="_blank" href="http://blogkindle.com/wp-content/uploads/2009/11/update_ufh_droid_install-k2i.bin">http://blogkindle.com/wp-content/uploads/2009/11/update_ufh_droid_install-k2i.bin</a></p>
<p>2.把下载的update_uth_xxx.bin通过USB复制到Kindle的根目录，然后依次按Home，Menu，Settings，Menu，最后选择&ldquo;Update Your Kindle&rdquo;开始升级。</p>
<p>3.升级过程大概一分钟，然后Kindle会自动重启，重启后，复制几本中文PDF或TXT进去，能正常显示中文就大功告成！</p>
<p><img width="500" height="686" alt="" src="/upload/36/140/25/1e9a8f59aee142b4ac8938f73fc6ecf4.jpg" /></p>
<p><span style="color: rgb(255, 0, 0); ">升级注意要点</span>：确保下载的bin文件和你的Kindle型号对应，升级前确保电池有足够的电量，升级过程中不要做任何操作。</p>
<p>用Kindle看电子书效果非常出色，和普通LCD屏幕相比更有纸质书的感觉。Kindle 2最新版本已经能直接支持PDF格式，不过，由于Kindle 2的屏幕还较小（Kindle 2 DX好点），而且不支持PDF重排和字体大小调整，而一般的PDF都是按照A4纸的尺寸排版的，即使是横屏看PDF也觉得字体太小，而中文的TXT格式我发现Kindle会有乱码和不能翻页的问题，估计是一些特殊字符造成的。Kindle支持的最佳格式是AZW，也就是Amazon自家的电子书格式，不但字体可缩放，而且翻页速度极快，因此，在Kindle上看电子书最好的办法就是把PDF和TXT等格式转换为AZW格式，如何批量转换？下次再继续讨论。</p>]]></description></item>
<item><title>为本地Subversion库链接外部资源库</title><link>http://www.liaoxuefeng.com/it-42eae668c6ab48a4966a1062ecd29d32-1</link><dc:creator>廖雪峰</dc:creator><pubDate>Tue, 19 Jan 2010 15:39:26 +0800</pubDate><description><![CDATA[<p>在使用Subversion进行版本管理时，有时候需要将其他资源库的一部分链接到我们自己资源库中，例如，本地已经存在的Project已经被Subversion管理了，但是，需要引入gdata等第三方库，当然可以直接将第三方发布的lib放到本地，但是，无法做到实时更新。由于Subversion支持external形式的外链，我们就可以将外部库的一部分当作本地Project的一部分。</p>
<p>以gdata为例，我们需要在本地的src目录下引入gdata的两个目录：</p>
<ul>
    <li>atom：http://gdata-python-client.googlecode.com/svn/trunk/src/atom</li>
    <li>gdata：http://gdata-python-client.googlecode.com/svn/trunk/src/gdata</li>
</ul>
<p>则需要通过svn命令实现。首先切换到src所在目录的父目录，新建文本文件externals.txt，内容如下：</p>
<pre class="brush:text">
atom http://gdata-python-client.googlecode.com/svn/trunk/src/atom
gdata http://gdata-python-client.googlecode.com/svn/trunk/src/gdata
</pre>
<p>给src目录加上svn:externals属性，使用以下命令：</p>
<pre class="brush:text">
C:\projects\my_project&gt; svn propset svn:externals -F externals.txt src
</pre>
<p>-F参数告诉SVN属性&quot;svn:externals&quot;的内容从文件externals.txt中读取，然后，对src目录做update操作，就会看到类似如下的输出：</p>
<pre class="brush:text">
Updating external location at: C:/projects/my_project/src/atom
  ...
Updating external location at: C:/projects/my_project/src/gdata
  ...
</pre>
<p>最后，在src目录下，可以看到，自动创建了引入的两个外部资源库的目录：atom和gdata。</p>]]></description></item>
<item><title>设计REST风格的MVC框架</title><link>http://www.liaoxuefeng.com/it-3299ae65e9f64b209e34532d3452818a-1</link><dc:creator>廖雪峰</dc:creator><pubDate>Fri, 08 Jan 2010 10:54:20 +0800</pubDate><description><![CDATA[<p style="color: rgb(255, 102, 0); ">本文最早发表于IBM developerWorks：</p>
<p><a target="_blank" href="http://www.ibm.com/developerworks/cn/java/j-lo-restmvc/">http://www.ibm.com/developerworks/cn/java/j-lo-restmvc/</a></p>
<p>传统的JavaEE MVC框架如Struts等都是基于Action设计的后缀式映射，然而，流行的Web趋势是REST风格的架构。尽管使用Filter或者Apache mod_rewrite能够通过URL重写实现REST风格的URL，为什么不直接设计一个全新的REST风格的MVC框架呢？本文将讲述如何从头设计一个基于REST风格的Java MVC框架，配合Annotation，最大限度地简化Web应用的开发，您甚至编写一行代码就可以实现&ldquo;Hello, world&rdquo;。</p>
<p>Java开发者对MVC框架一定不陌生，从Struts到WebWork，Java MVC框架层出不穷。我们已经习惯了处理*.do或*.action风格的URL，为每一个URL编写一个控制器，并继承一个Action或者Controller接口。然而，流行的Web趋势是使用更加简单，对用户和搜索引擎更加友好的REST风格的URL。例如，来自豆瓣的一本书的链接是<a target="_blank" href="http://www.douban.com/subject/2129650/">http://www.douban.com/subject/2129650/</a>，而非http://www.douban.com/subject.do?id=2129650。</p>
<p>有经验的 Java Web 开发人员会使用 URL 重写的方式来实现类似的URL，例如，为前端Apache服务器配置mod_rewrite模块，并依次为每个需要实现URL重写的地址编写负责转换的正则表达式，或者，通过一个自定义的 RewriteFilter，使用Java Web服务器提供的Filter和请求转发（Forward）功能实现URL重写，不过，仍需要为每个地址编写正则表达式。</p>
<p>既然URL重写如此繁琐，为何不直接设计一个原生支持REST风格的MVC框架呢？</p>
<p>要设计并实现这样一个MVC框架并不困难，下面，我们从零开始，仔细研究如何实现REST风格的URL映射，并与常见的IoC容器如Spring框架集成。这个全新的MVC框架暂命名为 WebWind。</p>
<h3 style="color: red">术语</h3>
<p>MVC：Model-View-Controller，是一种常见的UI架构模式，通过分离Model（模型）、View（视图）和Controller（控制器），可以更容易实现易于扩展的UI。在Web应用程序中，Model 指后台返回的数据；View指需要渲染的页面，通常是JSP或者其他模板页面，渲染后的结果通常是HTML；Controller 指 Web 开发人员编写的处理不同URL的控制器（在Struts中被称之为 Action），而 MVC 框架本身还有一个前置控制器，用于接收所有的 URL 请求，并根据 URL 地址分发到 Web 开发人员编写的Controller中。 IoC：Invertion-of-Control，控制反转，是目前流行的管理所有组件生命周期和复杂依赖关系的容器，例如 Spring 容器。</p>
<p>Template：模板，通过渲染，模板中的变量将被Model的实际数据所替换，然后，生成的内容即是用户在浏览器中看到的 HTML。模板也能实现判断、循环等简单逻辑。本质上，JSP页面也是一种模板。此外，还有许多第三方模板引擎，如Velocity，FreeMarker等。</p>
<h3 style="color: red">设计目标</h3>
<p>和传统的Struts等MVC框架完全不同，为了支持REST风格的URL，我们并不把一个URL映射到一个Controller类（或者Struts的Action），而是直接把一个URL映射到一个方法，这样，Web开发人员就可以将多个功能类似的方法放到一个Controller中，并且，Controller没有强制要求必须实现某个接口。一个Controller通常拥有多个方法，每个方法负责处理一个URL。例如，一个管理Blog的Controller 定义起来就像清单1所示。</p>
<pre class="brush:java">
// 清单 1. 管理 Blog 的 Controller 定义

public class Blog {
    @Mapping(&quot;/create/$1&quot;)
    Public void create(int userId) { ... }

    @Mapping(&quot;/display/$1/$2&quot;)
    Public void display(int userId, int postId) { ... }

    @Mapping(&quot;/edit/$1/$2&quot;)
    Public void edit(int userId, int postId) { ... }

    @Mapping(&quot;/delete/$1/$2&quot;)
    Public String delete(int userId, int postId) { ... }
}
</pre>
<p>@Mapping()注解指示了这是一个处理URL映射的方法，URL 中的参数 $1、$2 &hellip;&hellip;则将作为方法参数传入。对于一个&ldquo;/blog/1234/5678&rdquo;的URL，对应的方法将自动获得参数 userId=1234 和 postId=5678。同时，也无需任何与URL映射相关的XML配置文件。</p>
<p>使用$1、$2 &hellip;&hellip;来定义URL中的可变参数要比正则表达式更简单，我们需要在MVC框架内部将其转化为正则表达式，以便匹配 URL。</p>
<p>此外，对于方法返回值，也未作强制要求。</p>
<h3 style="color: red">集成 IoC</h3>
<p>当接收到来自浏览器的请求，并匹配到合适的URL时，应该转发给某个Controller实例的某个标记有@Mapping的方法，这需要持有所有Controller的实例。不过，让一个MVC框架去管理这些组件并不是一个好的设计，这些组件可以很容易地被IoC容器管理，MVC 框架需要做的仅仅是向IoC容器请求并获取这些组件的实例。</p>
<p>为了解耦一种特定的IoC容器，我们通过ContainerFactory来获取所有Controller组件的实例，如清单2所示。</p>
<pre class="brush:java">
// 清单 2. 定义 ContainerFactory

public interface ContainerFactory {

    void init(Config config);

    List&lt;Object&gt; findAllBeans();

    void destroy();
}
</pre>
<p>其中，关键方法findAllBeans()返回IoC容器管理的所有Bean，然后，扫描每一个Bean的所有public方法，并引用那些标记有@Mapping的方法实例。</p>
<p>我们设计目标是支持Spring和Guice这两种容器，对于Spring容器，可以通过ApplicationContext获得所有的Bean引用，代码见清单3。</p>
<pre class="brush:java">
// 清单 3. 定义 SpringContainerFactory

public class SpringContainerFactory implements ContainerFactory {
    private ApplicationContext appContext;

    public List&lt;Object&gt; findAllBeans() {
        String[] beanNames = appContext.getBeanDefinitionNames();
        List&lt;Object&gt; beans = new ArrayList&lt;Object&gt;(beanNames.length);
        for (int i=0; i&lt;beanNames.length; i++) {
            beans.add(appContext.getBean(beanNames[i]));
        }
        return beans;
    }
    ...
}
</pre>
<p>对于Guice容器，通过Injector实例可以返回所有绑定对象的实例，代码见清单4。</p>
<pre class="brush:java">
// 清单 4. 定义 GuiceContainerFactory

public class GuiceContainerFactory implements ContainerFactory {
    private Injector injector;

    public List&lt;Object&gt; findAllBeans() {
        Map&lt;Key&lt;?&gt;, Binding&lt;?&gt;&gt; map = injector.getBindings();
        Set&lt;Key&lt;?&gt;&gt; keys = map.keySet();
        List&lt;Object&gt; list = new ArrayList&lt;Object&gt;(keys.size());
        for (Key&lt;?&gt; key : keys) {
            Object bean = injector.getInstance(key);
            list.add(bean);
        }
        return list;
    }
    ...
}
</pre>
<p>类似的，通过扩展ContainerFactory，就可以支持更多的IoC容器，如PicoContainer。</p>
<p>出于效率的考虑，我们缓存所有来自IoC的Controller实例，无论其在IoC中配置为Singleton还是Prototype类型。当然，也可以修改代码，每次都从IoC容器中重新请求实例。</p>
<h3 style="color: red">设计请求转发</h3>
<p>和Struts等常见MVC框架一样，我们也需要实现一个前置控制器，通常命名为DispatcherServlet，用于接收所有的请求，并作出合适的转发。在Servlet规范中，有以下几种常见的URL匹配模式：</p>
<ul>
    <li>/abc：精确匹配，通常用于映射自定义的 Servlet；</li>
    <li>*.do：后缀模式匹配，常见的 MVC 框架都采用这种模式；</li>
    <li>/app/*：前缀模式匹配，这要求 URL 必须以固定前缀开头；</li>
    <li>/：匹配默认的 Servlet，当一个 URL 没有匹配到任何 Servlet 时，就匹配默认的 Servlet。一个 Web 应用程序如果没有映射默认的 Servlet，Web 服务器会自动为 Web 应用程序添加一个默认的 Servlet。</li>
</ul>
<p>REST风格的URL一般不含后缀，我们只能将DispatcherServlet映射到&ldquo;/&rdquo;，使之变为一个默认的Servlet，这样，就可以对任意的URL进行处理。</p>
<p>由于无法像Struts等传统的MVC框架根据后缀直接将一个URL映射到一个Controller，我们必须依次匹配每个有能力处理HTTP请求的@Mapping方法。完整的HTTP请求处理流程如下图所示。</p>
<p><img width="506" height="294" alt="" src="/upload/161/51/203/98adff9f1f0642c2b29a634ef54851dc.png" /></p>
<p>当扫描到标记有@Mapping注解的方法时，需要首先检查URL与方法参数是否匹配，UrlMatcher用于将@Mapping中包含$1、$2&hellip;&hellip;的字符串变为正则表达式，进行预编译，并检查参数个数是否符合方法参数，代码见清单5。</p>
<pre class="brush:java">
// 清单 5. 定义 UrlMatcher

final class UrlMatcher {
    final String url;
    int[] orders;
    Pattern pattern;

    public UrlMatcher(String url) {
        ...
    }
}
</pre>
<p>将@Mapping中包含$1、$2 &hellip;&hellip;的字符串变为正则表达式的转换规则是，依次将每个$n替换为([^\\/]*)，其余部分作精确匹配。例如，&ldquo;/blog/$1/$2&rdquo;变化后的正则表达式为： ^\\/blog\\/([^\\/]*)\\/([^\\/]*)$</p>
<p>请注意，Java字符串需要两个连续的&ldquo;\\&rdquo;表示正则表达式中的转义字符&ldquo;\&rdquo;。将&ldquo;/&rdquo;排除在变量匹配之外可以避免很多歧义。</p>
<p>调用一个实例方法则由Action类表示，它持有类实例、方法引用和方法参数类型，代码见清单6。</p>
<pre class="brush:java">
// 清单 6. 定义 Action

class Action {
    public final Object instance;
    public final Method method;
    public final Class[] arguments;

    public Action(Object instance, Method method) {
        this.instance = instance;
        this.method = method;
        this.arguments = method.getParameterTypes();
    }
}
</pre>
<p>负责请求转发的Dispatcher通过关联UrlMatcher与Action，就可以匹配到合适的URL，并转发给相应的Action，代码见清单7。</p>
<pre class="brush:java">
// 清单 7. 定义 Dispatcher

class Dispatcher  {
    private UrlMatcher[] urlMatchers;
    private Map&lt;UrlMatcher, Action&gt; urlMap = new HashMap&lt;UrlMatcher, Action&gt;();
    ....
}
</pre>
<p>当Dispatcher接收到一个URL请求时，遍历所有的UrlMatcher，找到第一个匹配URL的UrlMatcher，并从URL中提取方法参数，代码见清单8。</p>
<pre class="brush:java">
// 清单 8. 匹配并从 URL 中提取参数

final class UrlMatcher {
    ...

    /**
     * 根据正则表达式匹配 URL，若匹配成功，返回从 URL 中提取的参数
     * 若匹配失败，返回 null
     */
    public String[] getMatchedParameters(String url) {
        Matcher m = pattern.matcher(url);
        if (!m.matches())
            return null;
        if (orders.length==0)
            return EMPTY_STRINGS;
        String[] params = new String[orders.length];
        for (int i=0; i&lt;orders.length; i++) {
            params[orders[i]] = m.group(i+1);
        }
        return params;
    }
}
</pre>
<p>根据URL找到匹配的Action后，就可以构造一个Execution对象，并根据方法签名将URL中的String转换为合适的方法参数类型，准备好全部参数，代码见清单9。</p>
<pre class="brush:java">
// 清单 9. 构造 Exectuion

public class Execution {
    public final HttpServletRequest request;
    public final HttpServletResponse response;
    private final Action action;
    private final Object[] args;
    ...

    public Object execute() throws Exception {
        try {
            return action.method.invoke(action.instance, args);
        }
        catch (InvocationTargetException e) {
            Throwable t = e.getCause();
            if (t!=null &amp;&amp; t instanceof Exception)
                throw (Exception) t;
            throw e;
        }
    }
}
</pre>
<p>调用execute()方法就可以执行目标方法，并返回一个结果。请注意，当通过反射调用方法失败时，我们通过查找InvocationTargetException的根异常并将其抛出，这样，客户端就能捕获正确的原始异常。</p>
<p>为了最大限度地增加灵活性，我们并不强制要求URL的处理方法返回某一种类型。我们设计支持以下返回值：</p>
<ul>
    <li>String：当返回一个String时，自动将其作为HTML写入HttpServletResponse；</li>
    <li>void：当返回void时，不做任何操作；</li>
    <li>Renderer：当返回Renderer对象时，将调用Renderer对象的render方法渲染HTML页面。</li>
</ul>
<p>最后需要考虑的是，由于我们将DispatcherServlet映射为&ldquo;/&rdquo;，即默认的Servlet，则所有的未匹配成功的URL都将由DispatcherServlet处理，包括所有静态文件，因此，当未匹配到任何Controller的@Mapping方法后，DispatcherServlet将试图按URL查找对应的静态文件，我们用StaticFileHandler封装，主要代码见清单10。</p>
<pre class="brush:java">
// 清单 10. 处理静态文件

class StaticFileHandler {
    ...
    public void handle(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException {
        String url = request.getRequestURI();
        String path = request.getServletPath();
        url = url.substring(path.length());
        if (url.toUpperCase().startsWith(&quot;/WEB-INF/&quot;)) {
            response.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        int n = url.indexOf('?');
        if (n!=(-1))
            url = url.substring(0, n);
        n = url.indexOf('#');
        if (n!=(-1))
            url = url.substring(0, n);
        File f = new File(servletContext.getRealPath(url));
        if (! f.isFile()) {
            response.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        long ifModifiedSince = request.getDateHeader(&quot;If-Modified-Since&quot;);
        long lastModified = f.lastModified();
        if (ifModifiedSince!=(-1) &amp;&amp; ifModifiedSince&gt;=lastModified) {
            response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
            return;
        }
        response.setDateHeader(&quot;Last-Modified&quot;, lastModified);
        response.setContentLength((int)f.length());
        response.setContentType(getMimeType(f));
        sendFile(f, response.getOutputStream());
    }
}
</pre>
<p>处理静态文件时要过滤/WEB-INF/目录，否则将造成安全漏洞。</p>
<h3 style="color: red">集成模板引擎</h3>
<p>作为示例，返回一个&ldquo;&lt;h1&gt;Hello, world!&lt;/h1&gt;&rdquo;作为HTML页面非常容易。然而，实际应用的页面通常是极其复杂的，需要一个模板引擎来渲染出HTML。可以把JSP看作是一种模板，只要不在JSP页面中编写复杂的Java代码。我们的设计目标是实现对JSP和Velocity这两种模板的支持。</p>
<p>和集成IoC框架类似，我们需要解耦MVC与模板系统，因此，TemplateFactory用于初始化模板引擎，并返回Template模板对象。TemplateFactory定义见清单11。</p>
<pre class="brush:java">
// 清单 11. 定义 TemplateFactory

public abstract class TemplateFactory {
    private static TemplateFactory instance;
    public static TemplateFactory getTemplateFactory() {
        return instance;
    }

    public abstract Template loadTemplate(String path) throws Exception;
}
</pre>
<p>Template接口则实现真正的渲染任务。定义见清单12。</p>
<pre class="brush:java">
// 清单 12. 定义 Template

public interface Template {
    void render(HttpServletRequest request, HttpServletResponse response,
        Map&lt;String, Object&gt; model) throws Exception;
}
</pre>
<p>以JSP为例，实现JspTemplateFactory非常容易。代码见清单13。</p>
<pre class="brush:java">
// 清单 13. 定义 JspTemplateFactory

public class JspTemplateFactory extends TemplateFactory {
    private Log log = LogFactory.getLog(getClass());

    public Template loadTemplate(String path) throws Exception {
        if (log.isDebugEnabled())
            log.debug(&quot;Load JSP template '&quot; + path + &quot;'.&quot;);
        return new JspTemplate(path);
    }

    public void init(Config config) {
        log.info(&quot;JspTemplateFactory init ok.&quot;);
    }
}
</pre>
<p>JspTemplate用于渲染页面，只需要传入JSP的路径，将Model绑定到HttpServletRequest，就可以调用Servlet规范的forward方法将请求转发给指定的JSP页面并渲染。代码见清单14。</p>
<pre class="brush:java">
// 清单 14. 定义 JspTemplate

public class JspTemplate implements Template {
    private String path;

    public JspTemplate(String path) {
        this.path = path;
    }

    public void render(HttpServletRequest request, HttpServletResponse response,
            Map&lt;String, Object&gt; model) throws Exception {
        Set&lt;String&gt; keys = model.keySet();
        for (String key : keys) {
            request.setAttribute(key, model.get(key));
        }
        request.getRequestDispatcher(path).forward(request, response);
    }
}
</pre>
<p>另一种比JSP更加简单且灵活的模板引擎是Velocity，它使用更简洁的语法来渲染页面，对页面设计人员更加友好，并且完全阻止了开发人员试图在页面中编写Java代码的可能性。使用Velocity编写的页面示例如清单15所示。</p>
<pre class="brush:html">
清单15. Velocity 模板页面

&lt;html&gt;
    &lt;head&gt;&lt;title&gt;${title}&lt;/title&gt;&lt;/head&gt;
    &lt;body&gt;&lt;h1&gt;Hello, ${name}!&lt;/body&gt;
&lt;/html&gt;
</pre>
<p>通过VelocityTemplateFactory和VelocityTemplate就可以实现对Velocity的集成。不过，从Web开发人员看来，并不需要知道具体使用的模板，客户端仅需要提供模板路径和一个由Map</p>
<pre class="brush:java">
// 清单16. 定义 TemplateRenderer

public class TemplateRenderer extends Renderer {
    private String path;
    private Map&lt;String, Object&gt; model;

    public TemplateRenderer(String path, Map&lt;String, Object&gt; model) {
        this.path = path;
        this.model = model;
    }

    @Override
    public void render(ServletContext context, HttpServletRequest request,
            HttpServletResponse response) throws Exception {
        TemplateFactory.getTemplateFactory()
                .loadTemplate(path)
                .render(request, response, model);
    }
}
</pre>
<p>TemplateRenderer通过简单地调用render方法就实现了页面渲染。为了指定Jsp或Velocity，需要在web.xml中配置DispatcherServlet的初始参数。配置示例请参考清单17。</p>
<pre class="brush:xml">
清单 17. 配置 Velocity 作为模板引擎

&lt;servlet&gt;
    &lt;servlet-name&gt;dispatcher&lt;/servlet-name&gt;
    &lt;servlet-class&gt;org.expressme.webwind.DispatcherServlet&lt;/servlet-class&gt;
    &lt;init-param&gt;
        &lt;param-name&gt;template&lt;/param-name&gt;
        &lt;param-value&gt;Velocity&lt;/param-value&gt;
    &lt;/init-param&gt;
&lt;/servlet&gt;
</pre>
<p>如果没有该缺省参数，那就使用默认的Jsp。</p>
<p>类似的，通过扩展TemplateFactory和Template，就可以添加更多的模板支持，例如FreeMarker。</p>
<h3 style="color: red">设计拦截器</h3>
<p>拦截器和Servlet规范中的Filter非常类似，不过Filter的作用范围是整个HttpServletRequest的处理过程，而拦截器仅作用于Controller，不涉及到View的渲染，在大多数情况下，使用拦截器比Filter速度要快，尤其是绑定数据库事务时，拦截器能缩短数据库事务开启的时间。</p>
<p>拦截器接口Interceptor定义如清单18所示。</p>
<pre class="brush:java">
// 清单 18. 定义 Interceptor

public interface Interceptor {
    void intercept(Execution execution, InterceptorChain chain) throws Exception;
}
</pre>
<p>和Filter类似，InterceptorChain代表拦截器链。InterceptorChain定义如清单19所示。</p>
<pre class="brush:java">
// 清单 19. 定义 InterceptorChain

public interface InterceptorChain {
    void doInterceptor(Execution execution) throws Exception;
}
</pre>
<p>实现InterceptorChain要比实现FilterChain简单，因为Filter需要处理Request、Forward、Include和Error这4种请求转发的情况，而Interceptor仅拦截Request。当MVC框架处理一个请求时，先初始化一个拦截器链，然后，依次调用链上的每个拦截器。请参考清单20所示的代码。</p>
<pre class="brush:java">
// 清单 20. 实现 InterceptorChain 接口

class InterceptorChainImpl implements InterceptorChain {
    private final Interceptor[] interceptors;
    private int index = 0;
    private Object result = null;

    InterceptorChainImpl(Interceptor[] interceptors) {
        this.interceptors = interceptors;
    }

    Object getResult() {
        return result;
    }

    public void doInterceptor(Execution execution) throws Exception {
        if(index==interceptors.length)
            result = execution.execute();
        else {
            // must update index first, otherwise will cause stack overflow:
            index++;
            interceptors[index-1].intercept(execution, this);
        }
    }
}
</pre>
<p>成员变量index表示当前链上的第N个拦截器，当最后一个拦截器被调用后，InterceptorChain才真正调用Execution对象的execute()方法，并保存其返回结果，整个请求处理过程结束，进入渲染阶段。清单21演示了如何调用拦截器链的代码。</p>
<pre class="brush:java">
// 清单 21. 调用拦截器链

class Dispatcher  {
    ...
    private Interceptor[] interceptors;
    void handleExecution(Execution execution, HttpServletRequest request,
        HttpServletResponse response) throws ServletException, IOException {
        InterceptorChainImpl chains = new InterceptorChainImpl(interceptors);
        chains.doInterceptor(execution);
        handleResult(request, response, chains.getResult());
    }
}
</pre>
<p>当Controller方法被调用完毕后，handleResult()方法用于处理执行结果。</p>
<h3 style="color: red">渲染</h3>
<p>由于我们没有强制HTTP处理方法的返回类型，因此，handleResult()方法针对不同的返回值将做不同的处理。代码如清单22所示。</p>
<pre class="brush:java">
// 清单 22. 处理返回值

class Dispatcher  {
    ...
    void handleResult(HttpServletRequest request, HttpServletResponse response,
            Object result) throws Exception {
        if (result==null)
            return;
        if (result instanceof Renderer) {
            Renderer r = (Renderer) result;
            r.render(this.servletContext, request, response);
            return;
        }
        if (result instanceof String) {
            String s = (String) result;
            if (s.startsWith(&quot;redirect:&quot;)) {
                response.sendRedirect(s.substring(9));
                return;
            }
            new TextRenderer(s).render(servletContext, request, response);
            return;
        }
        throw new ServletException(&quot;Cannot handle result with type '&quot;
                + result.getClass().getName() + &quot;'.&quot;);
    }
}
</pre>
<p>如果返回null，则认为HTTP请求已处理完成，不做任何处理；如果返回Renderer，则调用Renderer对象的render()方法渲染视图；如果返回String，则根据前缀是否有&ldquo;redirect:&rdquo;判断是重定向还是作为HTML返回给浏览器。这样，客户端可以不必访问HttpServletResponse对象就可以非常方便地实现重定向。代码如清单23所示。</p>
<pre class="brush:java">
// 清单 23. 重定向

@Mapping(&quot;/register&quot;)
String register() {
    ...
    if (success)
        return &quot;redirect:/reg/success&quot;;
    return &quot;redirect:/reg/failed&quot;;
}
</pre>
<p>扩展Renderer还可以处理更多的格式，例如，向浏览器返回JavaScript代码等。</p>
<h3 style="color: red">扩展</h3>
<p>以下是对MVC框架核心功能的扩展。</p>
<h3 style="color: red">使用Filter转发</h3>
<p>对于请求转发，除了使用DispatcherServlet外，还可以使用Filter来拦截所有请求，并直接在Filter内实现请求转发和处理。使用Filter的一个好处是如果URL没有被任何Controller的映射方法匹配到，则可以简单地调用FilterChain.doFilter()将HTTP请求传递给下一个Filter，这样，我们就不必自己处理静态文件，而由Web服务器提供的默认Servlet处理，效率更高。和DispatcherServlet类似，我们编写一个DispatcherFilter作为前置处理器，负责转发请求，代码见清单24。</p>
<pre class="brush:java">
// 清单 24. 定义 DispatcherFilter

public class DispatcherFilter implements Filter {
    ...
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
    throws IOException, ServletException {
        HttpServletRequest httpReq = (HttpServletRequest) req;
        HttpServletResponse httpResp = (HttpServletResponse) resp;
        String method = httpReq.getMethod();
        if (&quot;GET&quot;.equals(method) || &quot;POST&quot;.equals(method)) {
            if (!dispatcher.service(httpReq, httpResp))
                chain.doFilter(req, resp);
            return;
        }
        httpResp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
    }
}
</pre>
<p>如果用DispatcherFilter代替DispatcherServlet，则我们需要过滤&ldquo;/*&rdquo;，在web.xml中添加声明如清单25所示。</p>
<pre class="brush:xml">
清单25. 声明 DispatcherFilter

&lt;filter&gt;
    &lt;filter-name&gt;dispatcher&lt;/servlet-name&gt;
    &lt;filter-class&gt;org.expressme.webwind.DispatcherFilter&lt;/servlet-class&gt;
&lt;/filter&gt;
&lt;filter-mapping&gt;
    &lt;filter-name&gt;dispatcher&lt;/servlet-name&gt;
    &lt;url-pattern&gt;/*&lt;/url-pattern&gt;
&lt;/filter-mapping&gt;
</pre>
<h3 style="color: red">访问Request和Response对象</h3>
<p>如何在@Mapping方法中访问Servlet对象？如HttpServletRequest，HttpServletResponse，HttpSession和ServletContext。ThreadLocal是一个最简单有效的解决方案。我们编写一个ActionContext，通过ThreadLocal来封装对Request等对象的访问。代码见清单26。</p>
<pre class="brush:java">
// 清单 26. 定义 ActionContext

public final class ActionContext {
    private static final ThreadLocal&lt;ActionContext&gt; actionContextThreadLocal
            = new ThreadLocal&lt;ActionContext&gt;();

    private ServletContext context;
    private HttpServletRequest request;
    private HttpServletResponse response;

    public ServletContext getServletContext() {
        return context;
    }

    public HttpServletRequest getHttpServletRequest() {
        return request;
    }

    public HttpServletResponse getHttpServletResponse() {
        return response;
    }

    public HttpSession getHttpSession() {
        return request.getSession();
    }

    public static ActionContext getActionContext() {
        return actionContextThreadLocal.get();
    }

    static void setActionContext(ServletContext context,
            HttpServletRequest request, HttpServletResponse response) {
        ActionContext ctx = new ActionContext();
        ctx.context = context;
        ctx.request = request;
        ctx.response = response;
        actionContextThreadLocal.set(ctx);
    }

    static void removeActionContext() {
        actionContextThreadLocal.remove();
    }
}
</pre>
<p>在Dispatcher的handleExecution()方法中，初始化ActionContext，并在finally中移除所有已绑定变量，代码见清单27。</p>
<pre class="brush:java">
// 清单 27. 初始化 ActionContext

class Dispatcher {
    ...
    void handleExecution(Execution execution, HttpServletRequest request,
    HttpServletResponse response) throws ServletException, IOException {
        ActionContext.setActionContext(servletContext, request, response);
        try {
            InterceptorChainImpl chains = new InterceptorChainImpl(interceptors);
            chains.doInterceptor(execution);
            handleResult(request, response, chains.getResult());
        }
        catch (Exception e) {
            handleException(request, response, e);
        }
        finally {
            ActionContext.removeActionContext();
        }
    }
}
</pre>
<p>这样，在@Mapping方法内部，可以随时获得需要的Request、Response、Session和ServletContext对象。</p>
<h3 style="color: red">处理文件上传</h3>
<p>Servlet API本身并没有提供对文件上传的支持，要处理文件上传，我们需要使用Commons FileUpload之类的第三方扩展包。考虑到Commons FileUpload是使用最广泛的文件上传包，我们希望能集成Commons FileUpload，但是，不要暴露Commons FileUpload的任何API给MVC的客户端，客户端应该可以直接从一个普通的HttpServletRequest对象中获取上传文件。</p>
<p>要让MVC客户端直接使用HttpServletRequest，我们可以用自定义的MultipartHttpServletRequest替换原始的HttpServletRequest，这样，客户端代码可以通过instanceof判断是否是一个Multipart格式的Request，如果是，就强制转型为MultipartHttpServletRequest，然后，获取上传的文件流。</p>
<p>核心思想是从HttpServletRequestWrapper派生MultipartHttpServletRequest，这样，MultipartHttpServletRequest具有HttpServletRequest接口。MultipartHttpServletRequest的定义如清单28所示。</p>
<pre class="brush:java">
// 清单 28. 定义 MultipartHttpServletRequest

public class MultipartHttpServletRequest extends HttpServletRequestWrapper {
    final HttpServletRequest target;
    final Map&lt;String, List&lt;FileItemStream&gt;&gt; fileItems;
    final Map&lt;String, List&lt;String&gt;&gt; formItems;

    public MultipartHttpServletRequest(HttpServletRequest request, long maxFileSize)
    throws IOException {
        super(request);
        this.target = request;
        this.fileItems = new HashMap&lt;String, List&lt;FileItemStream&gt;&gt;();
        this.formItems = new HashMap&lt;String, List&lt;String&gt;&gt;();
        ServletFileUpload upload = new ServletFileUpload();
        upload.setFileSizeMax(maxFileSize);
        try {
           ...解析Multipart ...
        }
        catch (FileUploadException e) {
            throw new IOException(e);
        }
    }

    public InputStream getFileInputStream(String fieldName) throws IOException {
        List&lt;FileItemStream&gt; list = fileItems.get(fieldName);
        if (list==null)
            throw new IOException(&quot;No file item with name '&quot; + fieldName + &quot;'.&quot;);
        return list.get(0).openStream();
    };
}
</pre>
<p>对于正常的Field参数，保存在成员变量Map&lt;String, List&lt;String&gt;&gt; formItems中，通过覆写getParameter()、getParameters()等方法，就可以让客户端把MultipartHttpServletRequest也当作一个普通的Request来操作，代码见清单29。</p>
<pre class="brush:java">
// 清单 29. 覆写 getParameter

public class MultipartHttpServletRequest extends HttpServletRequestWrapper {
    ...
    @Override
    public String getParameter(String name) {
        List&lt;String&gt; list = formItems.get(name);
        if (list==null)
            return null;
        return list.get(0);
    }

    @Override
    @SuppressWarnings(&quot;unchecked&quot;)
    public Map getParameterMap() {
        Map&lt;String, String[]&gt; map = new HashMap&lt;String, String[]&gt;();
        Set&lt;String&gt; keys = formItems.keySet();
        for (String key : keys) {
            List&lt;String&gt; list = formItems.get(key);
            map.put(key, list.toArray(new String[list.size()]));
        }
        return Collections.unmodifiableMap(map);
    }

    @Override
    @SuppressWarnings(&quot;unchecked&quot;)
    public Enumeration getParameterNames() {
        return Collections.enumeration(formItems.keySet());
    }

    @Override
    public String[] getParameterValues(String name) {
        List&lt;String&gt; list = formItems.get(name);
        if (list==null)
            return null;
        return list.toArray(new String[list.size()]);
    }
}
</pre>
<p>为了简化配置，在Web应用程序启动的时候，自动检测当前ClassPath下是否有Commons FileUpload，如果存在，文件上传功能就自动开启，如果不存在，文件上传功能就不可用，这样，客户端只需要简单地把Commons FileUpload的jar包放入/WEB-INF/lib/，不需任何配置就可以直接使用。核心代码见清单30。</p>
<pre class="brush:java">
// 清单 30. 检测 Commons FileUpload
				
class Dispatcher {
    private boolean multipartSupport = false;
    ...
    void initAll(Config config) throws Exception {
        try {
            Class.forName(&quot;org.apache.commons.fileupload.servlet.ServletFileUpload&quot;);
            this.multipartSupport = true;
        }
        catch (ClassNotFoundException e) {
            log.info(&quot;CommonsFileUpload not found.&quot;);
        }
        ...
    }

    void handleExecution(Execution execution, HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
        if (this.multipartSupport) {
            if (MultipartHttpServletRequest.isMultipartRequest(request)) {
                request = new MultipartHttpServletRequest(request, maxFileSize);
            }
        }
        ...
    }
    ...
}
</pre>
<h3 style="color: red">小结</h3>
<p>要从头设计并实现一个MVC框架其实并不困难，设计WebWind的目标是改善Web应用程序的URL结构，并通过自动提取和映射URL中的参数，简化控制器的编写。WebWind适合那些从头构造的新的互联网应用，以便天生支持REST风格的URL。但是，它不适合改造已有的企业应用程序，企业应用的页面不需要搜索引擎的索引，其用户对URL地址的友好程度通常也并不关心。</p>
<h3 style="color: red">参考资料</h3>
<p>参考Servlet 2.4规范：<a target="_blank" href="http://jcp.org/aboutJava/communityprocess/final/jsr154/index.html">http://jcp.org/aboutJava/communityprocess/final/jsr154/index.html</a></p>
<p>参考Spring框架：<a target="_blank" href="http://www.springsource.org/">http://www.springsource.org/</a></p>
<p>参考Guice框架：<a target="_blank" href="http://code.google.com/p/google-guice/">http://code.google.com/p/google-guice/</a></p>
<p>参考Velocity引擎：<a target="_blank" href="http://velocity.apache.org/">http://velocity.apache.org/</a></p>
<p>参考Commons FileUpload：<a target="_blank" href="http://commons.apache.org/fileupload/">http://commons.apache.org/fileupload/</a></p>
<h3 style="color: red">下载</h3>
<p>下载WebWind：<a target="_blank" href="http://code.google.com/p/webwind/downloads/list">http://code.google.com/p/webwind/downloads/list</a></p>
<p>下载WebWind SVN源码：<a target="_blank" href="http://webwind.googlecode.com/svn/trunk/">http://webwind.googlecode.com/svn/trunk/</a></p>
<h3 style="color: red">关于作者</h3>
<p>廖雪峰，精通Java/Java EE/Java ME/Android/Python/C#/Visual Basic，对开源框架有深入研究，著有《<a target="_blank" href="http://www.china-pub.com/34820">Spring 2.0核心技术与最佳实践</a>》一书，创建有开源框架<a target="_blank" href="http://code.google.com/p/jopenid/">JOpenID</a>，其官方博客是<a target="_blank" href="http://www.liaoxuefeng.com/">http://www.liaoxuefeng.com/</a>和<a target="_blank" href="http://michael-liao.appspot.com/">http://michael-liao.appspot.com/</a>。</p>]]></description></item>
<item><title>在Debian上可以安装Google Chrome了！</title><link>http://www.liaoxuefeng.com/it-3ac3dbfdc87441a1a2bed2def93a9437-1</link><dc:creator>廖雪峰</dc:creator><pubDate>Thu, 26 Nov 2009 15:19:31 +0800</pubDate><description><![CDATA[<p>现在，Google Chrome浏览器已经有了unstable的Linux版本，而且是deb格式的！</p>
<p>从<a target="_blank" href="http://dev.chromium.org/getting-involved/dev-channel">dev.chromium.org/getting-involved/dev-channel</a>这里下载32位或64位版本，我选择的是32位：</p>
<p><a target="_blank" href="http://www.google.com/chrome/intl/en/eula_dev.html?dl=unstable_i386_deb">http://www.google.com/chrome/intl/en/eula_dev.html?dl=unstable_i386_deb</a></p>
<p>用dpkg -i xxx.deb安装，然后就可以直接启动google-chrome了：</p>
<p><img width="546" height="362" alt="" src="/upload/65/252/51/1a0be190e01840f0a3d8618b90faa9cd.png" /></p>
<p>显示的版本号是4.0.x：</p>
<p><img width="563" height="350" alt="" src="/upload/43/91/228/52cb8fc945d443cfa70afcc9dce1f0df.png" /></p>]]></description></item>
<item><title>如何卸载Eclipse中已安装的插件</title><link>http://www.liaoxuefeng.com/it-e60293f53b264d35a7d50a54694dc7f9-1</link><dc:creator>廖雪峰</dc:creator><pubDate>Thu, 05 Nov 2009 10:05:09 +0800</pubDate><description><![CDATA[<p>最近才知道原来Eclipse还可以自己卸载已经安装的插件，方法是点击菜单&ldquo;Help&rdquo;，&ldquo;Install New Software...&rdquo;，在弹出的对话框中选择那个非常隐蔽的&ldquo;already installed&rdquo;链接：</p>
<p><img width="464" height="489" alt="" src="/upload/255/165/45/6202624d3ff847b2ba8d9eb593ca7b42.png" /></p>
<p>然后就显示已经安装的插件：</p>
<p><img width="506" height="413" alt="" src="/upload/253/219/132/163e715c79db4302a61322fd9530de0e.png" /></p>
<p>现在就可以选择要卸载的插件，然后点&ldquo;Uninstall...&rdquo;把它卸载掉。</p>
<p>这个方法对Eclipse Galileo (3.5)有效，其他版本你需要自己试一下。</p>]]></description></item>
<item><title>高效使用JDBC</title><link>http://www.liaoxuefeng.com/it-a40938739b684156af88ee8c2801262d-1</link><dc:creator>廖雪峰</dc:creator><pubDate>Tue, 15 Sep 2009 16:57:16 +0800</pubDate><description><![CDATA[<p>在JavaEE应用中，使用ORM操作数据库虽然简单快捷（参考&ldquo;<a target="_blank" href="http://www.liaoxuefeng.com/it-b913d28602a04a2880fc61d895791da1-1">高效使用JavaEE ORM</a>&rdquo;），但是毕竟是对JDBC的封装，很多时候，ORM还是不能满足我们的需求，主要是两个问题：</p>
<p>1. 速度不如JDBC，毕竟是封装JDBC，有额外的开销；</p>
<p>2. ORM提供的xQL很多时候无法满足需求，还需要数据库相关的SQL，这时，必须使用JDBC。</p>
<p>使用JDBC虽然麻烦点，但是，按照软件设计的思想，一步一步封装必要的代码，还是可以做到性能与开发效率并存。</p>
<p>首先，要坚决避免的就是不断重复编写try... catch... finally...。对于查询、更新、插入和删除操作，每一种操作只允许编写一次try... catch... finally...。如何实现？有两个方法。</p>
<p>第一个办法是找一个现成的封装了这些JDBC操作的框架，最好的方案当然就是Spring的JDBC框架了，顺便可以参考JdbcTemplate的源码，以便提升自己的JavaEE功力。</p>
<p>如果不使用Spring，那就采用第二个办法，自己造轮子，封装一个JDBC框架。</p>
<p>很多人反对自己造轮子，原因不外乎费事。不过很多时候，造轮子并不麻烦，而且可以满足特定的需求。今天要造的轮子就是一个封装JDBC的框架：<a target="_blank" href="http://code.google.com/p/express-me/wiki/ExpressPersist">Express-Persist</a>。</p>
<p>Express-Persist是<a target="_blank" href="http://express-me.googlecode.com">ExpressMe</a>的持久化子项目，目标是封装JDBC并提供简单的数据库操作接口。</p>
<p>为什么不使用Spring JDBC呢？主要原因只有一个：Spring的JDBC目前还是1.4兼容的，不支持1.5的泛型。Express-Persist要提供的接口除了基本的数据库操作外，还要实现：</p>
<p>1. 简单的ORM映射，注意是简单的，没有Hibernate那样完整而强悍，本质上就是把ResultSet的每一条记录变成一个JavaBean，可以参考Spring JDBC的RowMapper，实现非常容易；</p>
<p>2. 充分利用Java 5泛型支持，都是类型安全的参数和返回值，不用做强制转化；</p>
<p>3. 利用Java 5的注解（Annotation）把SQL标记在接口方法上，比如：</p>
<pre class="brush:java">
@Query(&quot;select * from User where u.id=:id&quot;)
public User get(String id);
</pre>
<p>4. 最后，最重要的，只编写接口，没有实现类！</p>
<p>没有实现类，那JDBC代码写在哪？当然由Express-Persist框架自动生成了。如何自动生成？运行一个命令自动生成Java类？在&ldquo;<a target="_blank" href="http://www.liaoxuefeng.com/it-b913d28602a04a2880fc61d895791da1-1">高效使用JavaEE ORM</a>&rdquo;一文中我们已经对JDO的这种静态增强方式表示了强烈的鄙视和唾弃，因此绝不可重蹈覆辙。Express-Persist会在启动时根据接口动态创建出类，不过我们不采用Hibernate使用的CGLIB库，而是直接通过JDK的动态代理功能实现动态类。</p>
<h3 style="color: red;">如何绑定SQL参数</h3>
<p>DAO接口的方法参数要自动绑定到SQL参数中，由于方法参数的顺序与SQL参数的顺序可能不一致，因此，只能使用命名参数来绑定，即：SQL参数定义为:xxx，对应的方法参数用@Param(&quot;xxx&quot;)标记。</p>
<p>当SQL参数很多的时候（尤其是INSERT语句），方法参数也非常多，调用起来非常不方便，比如：</p>
<pre class="brush:java">
@Update(&quot;insert into User values(:id, :email, :password, :name)&quot;)
public int create(@Param(&quot;id&quot;) String id, @Param(&quot;email&quot;) String email, @Param(&quot;password&quot;) String password, @Param(&quot;name&quot;) String name);
</pre>
<p>而且都是String类型，调用起来容易出错。</p>
<p>因此，Express-Persist允许使用JavaBean绑定，把上述代码变为：</p>
<pre class="brush:java">
@Update(&quot;insert into User values(:u.id, :u.email, :u.password, :u.name)&quot;)
public int create(@Param(&quot;u&quot;) User u);
</pre>
<p>这样，调用起来只需要传入一个User对象即可，简单且不易出错。</p>
<h3 style="color: red; ">如何分页查询</h3>
<p>绝大多数数据库支持分页查询，但语法各不相同。如果让开发者自己写分页SQL语句，难度较大，而且不易复用。因此，Express-Persist仿照Hibernate的做法，为每一种数据库定义一个Dialect，处理分页，这样，无需考虑数据库的特定分页语法，只需额外添加@FirstResult和@MaxResults这两个注解，以便传入分页参数：</p>
<pre class="brush:java">
@Query(&quot;select * from User u order by id&quot;)
List&lt;User&gt; queryAll(@FirstResult int first, @MaxResults int max);
</pre>
<p>Express-Persist已经内置HSQLDB、MySQL和Oracle的Dialect支持，也可以编写其他数据库的Dialect，只需实现Dialect接口即可。</p>
<h3 style="color: red; ">如何把ResultSet映射为Java对象</h3>
<p>要把ResultSet映射为Java对象，我们采用Spring JDBC使用的RowMapper方案，改进之处在于采用了泛型，并且，提供一个BeanRowMapper，实现ResultSet到JavaBean的转换，因为大部分的转换都是到JavaBean。</p>
<p>利用Java 5的泛型支持，可以非常容易地生成一个BeanRowMapper，而无需编写任何方法：</p>
<pre class="brush:java">
public class UserRowMapper extends BeanRowMapper&lt;User&gt; {}
</pre>
<p>@MappedBy用于告诉Express-Persist如何映射ResultSet：</p>
<pre class="brush:java">
@MappedBy(UserRowMapper.class)
@Query(&quot;select * from User u order by id&quot;)
List&lt;User&gt; queryAll(@FirstResult int first, @MaxResults int max);
</pre>
<p>如果返回结果仅有一个，例如根据主键查询，则必须加上一个@Unique注解，这样，Express-Persist将自动检查返回的记录数，如果不为1，则抛出异常：</p>
<pre class="brush:java">
@Unique
@MappedBy(UserRowMapper.class)
@Query(&quot;select * from User u where id=:id&quot;)
User queryById(@Param(&quot;id&quot;) String id);
</pre>
<p>如果返回结果允许多个，则返回值应该定义为泛型List，如List&lt;User&gt;。</p>
<h3 style="color: red; ">Batch支持</h3>
<p>批量插入或修改时，使用和不使用JDBC Batch，其性能将有数量级的差距。Express-Persist提供Batch支持，通过继承BatchSupport接口：</p>
<pre class="brush:java">
public class UserDao extends BatchSupport {
    @Update(&quot;update User set name=:name where id=:id&quot;)
    void updateUserName(@Param(&quot;id&quot;) String id, @Param(&quot;name&quot;) String name);
}
</pre>
<p>Batch操作的代码稍微复杂一点，必须用try... finally执行，以便正确释放资源：</p>
<pre class="brush:java">
try {
    dao.prepareBatch();
    // now the batch prepared:
    dao.updateUserName(&quot;id-1&quot;, &quot;change A's name&quot;);
    dao.updateUserName(&quot;id-2&quot;, &quot;change B's name&quot;);
    dao.updateUserName(&quot;id-3&quot;, &quot;change C's name&quot;);
    // execute:
    int[] results = dao.executeBatch();
}
finally {
    dao.closeBatch();
}
</pre>
<h3 style="color: red; ">事务控制</h3>
<p>Express-Persist仅支持JDBC事务，因此无法远程传播事务。事务代码通常写在Web应用程序的Filter或Interceptor中，只需编写一次：</p>
<pre class="brush:java">
TransactionManager txManager = ...;
Transaction tx = txManager.beginTransaction();
try {
    // TODO: DAO operations here...
    tx.commit();
}
catch (Exception e) {
    tx.rollback();
}
</pre>
<p>如果你想体验一下Express-Persist带来的全新Java持久化方案，可以从<a target="_blank" href="http://express-me.googlecode.com/files/express-persist.jar">http://express-me.googlecode.com/files/express-persist.jar</a>下载Jar包（含源代码）。完整的文档请参考<a target="_blank" href="http://code.google.com/p/express-me/wiki/ExpressPersist">http://code.google.com/p/express-me/wiki/ExpressPersist</a>。</p>]]></description></item>
<item><title>高效使用JavaEE ORM框架</title><link>http://www.liaoxuefeng.com/it-b913d28602a04a2880fc61d895791da1-1</link><dc:creator>廖雪峰</dc:creator><pubDate>Mon, 14 Sep 2009 19:54:15 +0800</pubDate><description><![CDATA[<p>虽然Java领域有无数的ORM框架，如<a target="_blank" href="http://www.hibernate.org">Hibernate</a>，<a target="_blank" href="http://ibatis.apache.org">iBatis</a>，TopLink，JDO，JPA&hellip;&hellip;但是这些ORM框架基本上大同小异。很多初学者对JDBC的复杂性望而却步，就简单认为使用ORM就会省时省力，结果恰恰相反，任何好的框架都是给专家准备的，任何急功近利试图偷懒的方法往往适得其反。要正确使用ORM还真不是一件简单的事情。本文仅简单整理一下ORM的原理，基本用法，以及如何避免各种陷阱的基本编程原则。</p>
<h3 style="color: red">ORM的原理</h3>
<p>先说ORM的实现原理。其实，要实现JavaBean的属性到数据库表的字段的映射，任何ORM框架不外乎是读某个配置文件把JavaBean的属性和数据库表的字段自动关联起来，当从数据库Query时，自动把字段的值塞进JavaBean的对应属性里，当做INSERT或UPDATE时，自动把JavaBean的属性值绑定到SQL语句中。但是，几乎所有的ORM都提供&ldquo;按需读取&rdquo;的功能，比如一个User有id，name，email和address这4个字段，但是address字段很少用，于是ORM只读取前3个字段，直到调用User的getAddress()方法时，才去数据库中读取address的值。这个功能显然不能通过User的get/set完成，因此，ORM需要采用某种方式生成一个User类的子类，并且覆写get/set方法，这样，才能在调用get方法时有机会从数据库中读取。类似的对User的修改检测也是这样实现的。</p>
<h3 style="color: red">两种增强的方式</h3>
<p>ORM为我们自己的JavaBean实现子类的方法很多，这个过程简单称之为&ldquo;增强&rdquo;，基本上有两种方法：Hibernate使用CGLIB在加载我们的User类时动态创建了一个子类，而JDO则要求编译完User类后再利用它提供的工具对User类进行改造，以便实现JDO需要的各种接口。<strong><span style="color: #ff0000">请注意</span></strong>：就是这种极其变态的设计导致了使用JDO的极大困难，在我们编译完源码后，还需要额外执行一个增强命令，或者额外编写Ant任务，极大地影响了快速开发和单元测试，所以，凡是采用静态生成持久类的ORM，要在第一时间直接排除，切记！</p>
<h3 style="color: red">理解持久和非持久状态</h3>
<p>所有的ORM框架都有持久和非持久的概念。简单地说，当我们new一个User实例时，它是非持久对象，当我们调用ORM的save()之类的方法后，这个实例就变成持久对象了。当我们通过ORM从数据库读取到一个User对象时，这个对象是持久对象，当关闭当前的事务后，这个对象变成非持久对象。</p>
<p>虽然这个过程很容易理解，但是，难点在于当我们设计一个方法时，我们必须准确地知道当前操作的对象是持久对象还是非持久对象，否则，各种灵异事件会接踵而来，比如插入了重复记录等等。举例说明，当我们需要创建一个User对象时，save(User)方法必须传入非持久对象，当我们需要更新一个User对象时，update(User)方法必须传入一个持久对象，有些ORM比如Hibernate，为了方便用户，提供了saveOrUpdate()方法，自动判断是否是持久对象，是则更新，否则创建。我的建议是永远不要使用这些看上去很简单的方法，否则将很难判断ORM到底做了什么操作，也就很难追踪到逻辑错误。</p>
<h3 style="color: red">正确使用CRUD</h3>
<p>正因为我们需要时刻区分一个对象的持久化状态，所以，编写CRUD（Create，Retrieve，Update，Delete的缩写）要严格遵循以下原则：</p>
<p><span style="color: #ff0000"><strong>Create</strong></span>：对于Create操作，传入的永远是非持久化对象，一旦调用了create方法，就变成持久化对象；</p>
<p><span style="color: #ff0000"><strong>Retrieve</strong></span>：所有通过ORM从数据库读取的对象都是持久化对象，直到当前会话结束；</p>
<p><span style="color: #ff0000"><strong>Update</strong></span>：对于Update操作，传入的必须是持久化对象，而通常需要更新的对象是从页面获得的，因此，编写Update方法要按照以下步骤：</p>
<p>从Web页面中获得了一个User对象（包含ID），这个对象肯定是非持久化对象；</p>
<p>当得到该User对象时，千万不可直接做Update操作，因为从Web页面得到的数据都是不可信任的，修改HTTP请求非常简单，有经验的开发人员利用一个FireFox插件就能完成。正确的做法是根据该User对象的ID从数据库中查询到持久化的User对象，然后把待修改的属性复制到持久化的User对象中，最后Update该持久化的User对象，简单的代码如下：</p>
<pre class="brush:java">
void update(User u) {
    // 从数据库读取User：
    User p = load(User.class, u.getId());
    // 检查当前用户有无权限修改：
    // TODO: ...
    // 复制属性：
    p.setName(u.getName());
    p.setAddress(u.getAddress());
    // 不允许修改的属性例如email就不要复制了，然后更新：
    update(p);
}
</pre>
<p><span style="color: #ff0000"><strong>Delete</strong></span>：对于Delete操作，ORM通常只关心映射到主键的ID属性，不过，正确的做法仍然是根据ID先通过ORM读取，然后判断权限，最后删除。简单的代码如下：</p>
<pre class="brush:java">
void delete(String id) {
    // 从数据库读取User：
    User p = load(User.class, u.getId());
    // 检查当前用户有无权限修改：
    // TODO: ...
    // 删除：
    delete(p);
}
</pre>
<p>严格按照正确的方法做CRUD操作，使用ORM才能事半功倍。</p>
<h3 style="color: red">级联读取</h3>
<p>数据库表支持外键关联，因此，ORM也可以把多个JavaBean按照数据库的外键关系联系起来，比如可以在读User对象时把其关联的Profile对象也一并读出来，即所谓级联读取。这又是一个使用起来要非常小心谨慎的功能。</p>
<p>首先，我的建议是级联读取的层次最好是0或1，一般不要超过3，千万不可设为无限，否则，一个简单的查询可能就读取了上万条记录，在开发时由于并发用户只有1，往往看不出问题，等到部署了发现服务器经常内存溢出，其实是级联读取太多导致的。</p>
<p>其次，级联有一对多和多对一两种（一对一可以并入第二种），要非常小心地使用一对多，除非有十分的把握确定&ldquo;多&rdquo;的一端只有不超过100条记录。比如设计论坛时读取&ldquo;版面&rdquo;Board时如果有一对多顺便把&ldquo;话题&rdquo;Topic一并读入了，随着Topic越来越多，每次读取Board的内存占用也越来越多，直到最后内存溢出。因此，我的建议是最好不用一对多，凡是有一对多的需求全部采用分页查询的方式解决。</p>
<p>最后，大多数ORM对级联读取都是采用join的方式，在数据量很大的情况时，join操作很慢，而且无法水平分割数据库表。对读取要求很高的应用，最好不要设置级联读取。</p>
<h3 style="color: red">缓存</h3>
<p>绝大多数ORM都会提供缓存，通常还分为一级缓存和二级缓存。一级缓存只在当前会话内有效，当我们在一个会话里反复读取同一个对象时，只有第一次ORM会从数据库中读取，后续请求会直接从缓存中读取，例如：</p>
<pre class="brush:java">
int id=12345;
User u1 = load(User.class, id); // 从数据库读
User u2 = load(User.class, id); // 从一级缓存读
System.out.println(u1==u2); // True
</pre>
<p>实际上，很少有人会写出这样的代码，所以，一级缓存几乎没有什么作用。</p>
<p>而二级缓存就作用于整个应用。不过，当数据量很小的时候，通过增大数据库服务器的内存就和使用缓存没什么区别，当数据量非常大的时候，二级缓存的命中率很低，原因当然是缓存大小不够，因此，针对海量数据通常都要自己动手，用memcached做专门的缓存服务器来提高性能。所以，二级缓存不开也罢。</p>
<h3 style="color: red">确定事务范围，小心使用Lasy Loading</h3>
<p>使用ORM也需要对数据库事务有一定了解，通常ORM的一个会话对应一个数据库事务，如果事务持续时间长，占用的数据库资源就长，数据库并发处理能力就会降低，所以，事务范围越短越好。对于Web应用，把事务限制在Controller中就比限制在Controller+View中要短，通过MVC框架提供的各种拦截器可以很方便地开启和关闭事务。当事务限制在Controller时，到了View渲染的时候，就无法使用LasyLoading功能了。Lasy Loading虽然简单，但不当使用也会造成严重的性能问题，所以还是不用为妙。</p>
<p>以上对ORM框架的使用做了一个简单的概括，实际应用中还需要通过大量实践慢慢探索。</p>]]></description></item>
<item><title>用Linux替代Windows</title><link>http://www.liaoxuefeng.com/it-06e04bfd00514aabad6cc495582a00d9-1</link><dc:creator>廖雪峰</dc:creator><pubDate>Tue, 01 Sep 2009 10:00:29 +0800</pubDate><description><![CDATA[<p>Windows虽然有简单易用的特点，不过，作为一名专业的软件开发人员，使用Linux作为开发平台，还是有很大的优越性的。</p>
<p>首先，Windows下的软件大多是收费的，虽然网上的破解也不少，不过，在公司使用说不定哪天就有麻烦了，而Linux下的软件基本上都是免费而且开源的，虽然公开源代码对我等只用不改造的用户来说意义不大，不过，免费却是实实在在的。</p>
<p>其次，作为一名软件开发人员，许多服务器软件只能在Linux下运行，有的虽然已经移植到了Windows上，运行效率和稳定性却要大打折扣。</p>
<p>不过，和Windows用户的担心一样，使用Linux，如果不能听MP3，不能看大片，仅仅在Linux上工作也不太爽，毕竟要劳逸结合嘛。好在Linux下的多媒体软件已经今非昔比了，今天，我们就一步一步打造一个工作＋娱乐一体的Linux环境。</p>
<h3 style="color: Red;">选择什么Linux？</h3>
<p>Linux发行版众多，有商业公司支持的，也有开源社区支持的，不同的Linux发行版侧重也不同，有的Linux比如Gentoo，完全面向Linux发烧级用户的，从源代码编译开始。对于普通开发者而言，入门容易，安装软件快捷简便是最重要的两点。我的选择是Debian Linux，最新版本是5。相对于其他Linux版本，Debian的最大的优势就是软件众多，安装极为简单。有许多用户可能用过或听说过Red Hat的RPM软件包，不过，和Debian的DEB相比，RPM就差远了！</p>
<p>另外，Debian是一个社区维护的Linux发行版，和倾向于提供傻瓜式操作的Ubuntu相比，Debian显得更加&ldquo;专业&rdquo;一点，动手能力要求更高一点，便于和普通的Windows用户拉开更大的差距。</p>
<h3 style="color: Red;">安装Debian Linux</h3>
<p>闲话少说，要安装Debian Linux，先去Debian官方网站下载刻盘，根据计算机类型选相应的ISO，通常是i386，用AMD64处理器的可以选amd64，建议以HTTP/FTP方式下载第一张ISO光盘，比如i386对应的CD：</p>
<p><a target="_blank" href="http://cdimage.debian.org/debian-cd/5.0.2a/i386/iso-cd/">http://cdimage.debian.org/debian-cd/5.0.2a/i386/iso-cd/</a></p>
<p>选择netinst方式的ISO虽然下载较快，但是安装过程中需要联网，毕竟麻烦。</p>
<p>下载后刻盘，从光盘启动，安装过程很简单，主要是分区要注意，最好手动分区，然后把引导区安装到MBR上，Debian会自动发现已安装的Windows，双系统启动没有问题。</p>
<p>安装时会要求输入APT源，就是将来安装软件的下载地址了，我通常选择ftp.us.debian.org，速度那是非常地快。</p>
<p>安装过程中可以安装桌面，也可以不装。如果没有安装，用root登录后手动安装：</p>
<pre class="brush:bash">
# apt-get install gnome</pre>
<p>然后桌面就搞定了。</p>
<h3 style="color: Red;">中文支持</h3>
<p>使用Linux的第一个大问题就是要搞定中文。虽然Linux实际上完美支持各种语言，不过还是要稍微配置一下。在Debian中配置中文是相对简单的，运行命令</p>
<pre class="brush:bash">
# dpkg-reconfigure locales</pre>
<p>把以下的编码选中：</p>
<ul>
    <li>en_US.UTF-8 UTF-8</li>
    <li>zh_CN GB2312</li>
    <li>zh_CN.GB18030 GB18030</li>
    <li>zh_CN.GBK GBK</li>
    <li>zh_CN.UTF-8 UTF-8</li>
</ul>
<p>也可以顺便把BIG5编码选上：</p>
<ul>
    <li>zh_TW BIG5</li>
    <li>zh_TW.UTF-8 UTF-8</li>
</ul>
<p>下一步是安装中文字体。Linux自带几种中文字体，输入以下命令安装Debian Linux默认的中文字体：</p>
<pre class="brush:bash">
# apt-get install ttf-arphic-bkai00mp ttf-arphic-bsmi00lp ttf-arphic-gbsn00lp ttf-arphic-gbsn00lp</pre>
<p>重启XWindow，中文应该可以显示了，不过，效果当然没有Windows那么好了，怎么办？其实字体并不依赖平台，所以，把Windows的字体copy到Linux下使用，效果和Windows一样，哈哈！</p>
<p>首先，进入默认的True Type字体目录：</p>
<pre class="brush:bash">
# pwd/usr/share/fonts/truetype</pre>
<p>新建一个windows目录（其实名字无所谓）：</p>
<pre class="brush:bash">
# mkdir windows</pre>
<p>然后，从另一台Windows Vista或Windows 7的机器上把以下几个字体文件拷出来：</p>
<p>Tohoma：Tahoma是Windows默认的英文字体，适合英文字体；</p>
<p>微软雅黑：微软雅黑是Windows Vista和Windows 7默认的中文字体，显示效果当然非常好了。</p>
<p>把这几个字体文件复制到刚才创建的windows目录下：</p>
<pre class="brush:bash">
# lsmsyhbd.ttf&nbsp; msyh.ttf&nbsp; tahomabd.ttf&nbsp; tahoma.ttf</pre>
<p>其他Windows字体根据个人爱好复制。注意字体也是有版权的，仅限个人使用，千万不要传到网上去自找麻烦。</p>
<p>确保已经安装了xfstt以支持TrueType字体：</p>
<pre class="brush:bash">
# apt-get install xfstt</pre>
<p>然后，告诉系统要刷新所有字体：</p>
<pre class="brush:bash">
# /etc/init.d/xfstt force-reload</pre>
<p>重启XWindow后就可以设置喜欢的字体了。</p>
<h3 style="color: Red;">安装中文输入法</h3>
<p>能显示中文还不够，怎么也得能输入中文吧。要在Debian下安装中文输入法也很简单，把下面几个包装上：</p>
<pre class="brush:bash">
# apt-get install scim scim-chinese scim-tables-zh</pre>
<p>然后，新建一个文件/etc/X11/Xsession.d/95xinput，输入以下内容：</p>
<pre class="brush:bash">
/usr/bin/scim -dXMODIFIERS=&quot;@im=SCIM&quot;export XMODIFIERS</pre>
<p>重启XWindow后生效。</p>
<h3 style="color: Red;">访问Windows的NTFS分区</h3>
<p>有了Windows/Linux双系统后，由于主要的文件还是存放在Windows分区下的，所以，经常需要在Linux下访问Windows的NTFS分区。</p>
<p>首先安装ntfs-3g：</p>
<pre class="brush:bash">
# apt-get install ntfs-3g</pre>
<p>然后，在/media下新建一个空目录用于挂载Windows的NTFS分区，比如storage：</p>
<pre class="brush:bash">
# mkdir /media/storage</pre>
<p>现在就可以挂载NTFS分区了，不过首先要知道NTFS分区的位置，用命令fdisk -l查看：</p>
<pre class="brush:bash">
# fdisk -lDisk /dev/sda: 320.0 GB, 320072933376 bytes255 heads, 63 sectors/track, 38913 cylindersUnits = cylinders of 16065 * 512 = 8225280 bytesDisk identifier: 0x60997b83   Device Boot      Start        End      Blocks   Id  System/dev/sda1    *          1       4700    37748736    7  HPFS/NTFS/dev/sda2            4700      14099    75497472   83  Linux/dev/sda3           14099      29764   125829120    7  HPFS/NTFS/dev/sda4           29764      38914   73492480+    f  W95 Ext'd (LBA)/dev/sda5           29764      38391    69298176    7  HPFS/NTFS/dev/sda6           38392      38914     4193280   82  Linux swap / Solaris</pre>
<p>可以看到，我的硬盘类型为sda，一共有6个分区，sda1是安装有Windows 7的NTFS分区，sda2是Linux的/分区，sda3和sda5也是NTFS分区，而sda6是Linux的swap分区。</p>
<p>现在，我打算把sda3分区挂载到/media/storage下，用命令：</p>
<pre class="brush:bash">
# mount -t ntfs-3g /dev/sda3 /media/storage -o umask=0,nls=utf8</pre>
<p>如果每次开机都想自动挂载该NTFS分区，就编辑/etc/fstab，追加一行：</p>
<pre class="brush:bash">
/dev/sda3 /media/storage ntfs-3g umask=0,nls=utf8 0 0</pre>
<p>查看/etc/fstab的内容如下：</p>
<pre class="brush:bash">
# more /etc/fstab# /etc/fstab: static file system information.## &lt;file system&gt; &lt;mount point&gt;   &lt;type&gt;  &lt;options&gt;       &lt;dump&gt;  &lt;pass&gt;proc            /proc           proc    defaults          0      0/dev/sda2       /               ext3    errors=remount-ro 0      1/dev/sda6       none            swap    sw                0      0/dev/hda        /media/cdrom0   udf,iso9660 user,noauto   0      0/dev/sda3       /media/storage  ntfs-3g umask=0,nls=utf8  0      0</pre>
<p>要挂载多个NTFS分区，就重复上述步骤。</p>
<p>现在，在Debian下就可以自由访问Windows的文件了！</p>
<p>注意，使用ntfs-3g时，NTFS的权限控制完全失效，可以任意修改文件，所以要特别小心！如果只需要读取，可以用ntfs，这样就不能写入。</p>
<h3 style="color: Red;">播放MP3</h3>
<p>要在Linux下播放MP3，强烈推荐安装Audacious。在Gnome桌面上，不需要apt-get命令了，直接打开&ldquo;Add/Remove Applications&rdquo;，在&ldquo;Sound &amp; Video&rdquo;中选择&ldquo;Audacious&rdquo;，安装后即可播放许多格式的音乐，包括MP3、ACC、WMA等等。</p>
<p>解决中文歌曲乱码问题：右键弹出菜单中选择Preferences，打开选项对话框，选择Appearance：去掉&ldquo;Use Bitmap fonts if available&rdquo;，选择Playlist，在&ldquo;Auto character encoding detector for:&rdquo;选择&ldquo;Chinese&rdquo;，然后在&ldquo;Fallback character encodings:&rdquo;中输入&ldquo;GBK&rdquo;，现在中文显示一切正常！</p>
<h3 style="color: Red;">播放RMVB</h3>
<p>Windows下许多用户都会装一个暴风影音，一个播放器搞定所有格式的视频。在Linux下，其实也有类似软件。</p>
<p>首先通过&ldquo;Add/Remove Applications&rdquo;安装MPlayer，安装后由于不带RMVB等解码器，所以还需要安装一个&ldquo;万能&rdquo;解码器。之所以不能通过MPlayer自动安装，是因为这些解码器都是有版权的，而且在Debian的源里也不会有，所以就需要我们自己动手下载一个：</p>
<pre class="brush:bash">
# wget http://www.debian-multimedia.org/pool/main/w/w32codecs/w32codecs_20071007-0.2_i386.deb</pre>
<p>或者用浏览器下载。</p>
<p>然后，用以下命令安装：</p>
<pre class="brush:bash">
# dpkg -i w32codecs_20071007-0.2_i386.deb</pre>
<p>安装完毕后，打开MPlayer就可以播放RMVB了！</p>
<p>为啥Debian官方的源不提供解码器？因为这些解码器其实都是有版权的，Debian是不会把这些解码器放到源里的，所以，要发扬一下自力更生的Google精神，好在老外都是活雷锋，早就把这些解码器打包成deb包了（当然也有版权问题啦，不过咱仅限自己使用）。</p>
<h3 style="color: Red;">常用软件安装</h3>
<p>默认的浏览器不爽，Firefox哪去了？在&ldquo;Add/Remove Applications&rdquo;里找个遍也找不到，其实，搜索Iceweasel就找到了，原来Debian把Firefox名字改成了Iceweasel，再重新打包发布，原因是Firefox的几个Logo据说是Mozilla的注册商标，不能以GPL发布。</p>
<p>要用MSN咋办？安装一个Pidgin就能搞定MSN，Yahoo，ICQ。</p>
<p>Java开发者可以安装Open JDK，不过，如果你更信赖SUN的官方JDK怎么办？修改一下Debian的APT源，把non-free加上，就可以安装SUN的JDK了：</p>
<pre class="brush:bash">
# more /etc/apt/sources.listdeb http://ftp.us.debian.org/debian/ lenny main non-freedeb-src http://ftp.us.debian.org/debian/ lenny maindeb http://security.debian.org/ lenny/updates main non-freedeb-src http://security.debian.org/ lenny/updates maindeb http://volatile.debian.org/debian-volatile lenny/volatile maindeb-src http://volatile.debian.org/debian-volatile lenny/volatile main# apt-get install sun-java6-jdk</pre>
<p>Python安装更简单，直接apt-get install python，需要其他框架的也直接通过apt-get安装。</p>
<p>Office软件有OpenOffice，不过我早就转向Google Docs了，在线版真的非常方便，只要不是搞出版的，免费的Google Docs足以满足绝大多数人的需求，而且，也不用把文件到处copy，直接发布，然后告诉对方一个地址让他自己去看得了。</p>]]></description></item>
<item><title>解决Windows 7的中文乱码问题</title><link>http://www.liaoxuefeng.com/it-131caf35f4824d54b780b522b37f35e6-1</link><dc:creator>廖雪峰</dc:creator><pubDate>Mon, 24 Aug 2009 19:24:43 +0800</pubDate><description><![CDATA[<p>安装了Windows 7 Ultimate英文正式版后（注意是英文版！！！），发现很多中文软件显示为乱码，例如招商银行个人专业版，联系了他们的客服后，非常肯定地告诉我，目前专业版不支持Windows 7，!!! -_- !!!</p>
<p>然后在Google中搜索Windows 7乱码问题，发现好多网站把一篇改注册表的文章转来转去，结果我也被严重误导了！改完重启一点作用没有，最后终于找到简单而正确的方法：</p>
<p>打开Control Panel，Clock, Language and Region，选择Administrative：</p>
<p><img width="477" height="550" alt="" src="/upload/55/175/134/f7c63f41865b4c2eaf6847ef9d785b01.png" /></p>
<p>选择Change system locale...按钮，把当前语言改为中文，重启即可：</p>
<p><img width="457" height="226" alt="" src="/upload/143/225/20/cc3d28104a1143fc9a151cb012f03e6b.png" /></p>
<p>所有中文软件均运行正常！</p>]]></description></item>
<item><title>将Flex集成到Java EE应用程序的最佳实践</title><link>http://www.liaoxuefeng.com/it-83fb2c5d8ca94023a7b8501b8c2a3a60-1</link><dc:creator>廖雪峰</dc:creator><pubDate>Wed, 12 Aug 2009 13:21:56 +0800</pubDate><description><![CDATA[<p><span style="color: #ff6600">本文最早发表于IBM developerWorks：</span></p>
<p><a target="_blank" href="http://www.ibm.com/developerworks/cn/java/j-lo-jeeflex/">http://www.ibm.com/developerworks/cn/java/j-lo-jeeflex/</a></p>
<p>传统的Java EE应用程序通常使用某种MVC框架（例如，<a target="_blank" href="http://struts.apache.org/">Struts</a>）作为前端用户界面，随着Flex的兴起，基于RIA的客户端能够给用户带来更酷的界面，更短的响应时间，以及更接近于桌面应用程序的体验。本文将讲述如何将Flex集成至一个现有的Java EE应用程序中，以及如何应用最佳实践高效率地并行开发Java EE和Flex。</p>
<h3 style="color: red">开发环境</h3>
<p>本文的开发环境为Windows 7 Ultimate，Eclipse 3.4，Flex Builder 3。Java EE服务器使用<a target="_blank" href="http://www.caucho.com/">Resin</a> 3.2，当然，您也可以使用<a target="_blank" href="http://tomcat.apache.org/">Tomcat</a>等其他Java EE服务器。</p>
<h3 style="color: red">现有的Java EE应用</h3>
<p>假定我们已经拥有了一个管理雇员信息的Java EE应用，名为EmployeeMgmt-Server，结构如图所示：</p>
<p><img alt="" width="301" height="557" src="/upload/215/1/124/c96545a22e984eedb35b05c63223bc84.png" /></p>
<p>这是一个典型的Java EE应用，使用了流行的Spring框架。为了简化数据库操作，我们使用了内存数据库<a target="_blank" href="http://hsqldb.org/">HSQLDB</a>。对这个简单的应用，省略了DAO，直接在Fa&ccedil;ade中通过Spring的JdbcTemplate操作数据库。最后，EmployeeMgmt应用通过Servlet和JSP页面为用户提供前端界面：</p>
<p><img alt="" width="572" height="410" src="/upload/4/90/74/4126806cd5f54abbbcbd10d3e9a32563.png" /></p>
<p>该界面为传统的HTML页面，用户每次点击某个链接都需要刷新页面。由于Employee Management系统更接近于传统的桌面应用程序，因此，用Flex重新编写界面会带来更好的用户体验。</p>
<h3 style="color: red">集成BlazeDS</h3>
<p>如何将Flex集成至该Java EE应用呢？现在，我们希望用Flex替换掉原有的Servlet和JSP页面，就需要让Flex和Java EE后端通信。Flex支持多种远程调用方式，包括HTTP，Web Services和AMF。不过，针对Java EE开发的服务器端应用，可以通过集成BlazeDS，充分利用AMF协议并能轻易与Flex前端交换数据，这种方式是JavaEE应用程序集成Flex的首选。</p>
<p>BlazeDS是Adobe LifeCycle Data Services的开源版本，遵循LGPL v3授权，可以免费使用。BlazeDS为Flex提供了基于AMF二进制协议的远程调用支持，其作用相当于Java的RMI。有了BlazeDS，通过简单的配置，一个Java接口就可以作为服务暴露给Flex，供其远程调用。</p>
<p>尽管现有的EmployeeMgmt应用程序已经有了Fa&ccedil;ade接口，但这个接口是暴露给Servlet使用的，最好能再为Flex定义另一个接口FlexService，并隐藏Java语言的特定对象：</p>
<pre class="brush:java">
public interface FlexService {
    Employee createEmployee(String name, String title, boolean gender, Date birth);
    void deleteEmployee(String id);
    Employee[] queryByName(String name);
    Employee[] queryAll();
}
</pre>
<p>现在，Java EE后端与Flex前端的接口已经定义好了，要完成Java EE后端的接口实现类非常容易，利用Spring强大的依赖注入功能，可以通过几行简单的代码完成：</p>
<pre class="brush:java">
public class FlexServiceImpl implements FlexService {
    private static final Employee[] EMPTY_EMPLOYEE_ARRAY = new Employee[0];
    private Facade facade;

    public void setFacade(Facade facade) {
        this.facade = facade;
    }

    public Employee createEmployee(String name, String title, boolean gender, Date birth) {
        return facade.createEmployee(name, title, gender, birth);
    }

    public void deleteEmployee(String id) {
        facade.deleteEmployee(id);
    }

    public Employee[] queryAll() {
        return facade.queryAll().toArray(EMPTY_EMPLOYEE_ARRAY);
    }

    public Employee[] queryByName(String name) {
        return facade.queryByName(name).toArray(EMPTY_EMPLOYEE_ARRAY);
    }
}
</pre>
<p>然后，我们将BlazeDS所需的jar包放至/WEB-INF/lib/。BlazeDS需要如下的jar：</p>
<ul>
    <li>backport-util-concurrent.jar</li>
    <li>commons-httpclient.jar</li>
    <li>commons-logging.jar</li>
    <li>flex-messaging-common.jar</li>
    <li>flex-messaging-core.jar</li>
    <li>flex-messaging-proxy.jar</li>
    <li>flex-messaging-remoting.jar</li>
</ul>
<p>在web.xml中添加HttpFlexSession和Servlet映射。HttpFlexSession是BlazeDS提供的一个Listener，负责监听Flex远程调用请求，并进行一些初始化设置：</p>
<pre class="brush:xml">
&lt;listener&gt;
    &lt;listener-class&gt;flex.messaging.HttpFlexSession&lt;/listener-class&gt;
&lt;/listener&gt;
</pre>
<p>MessageBrokerServlet是真正处理Flex远程调用请求的Servlet，我们需要将其映射到指定的URL：</p>
<pre class="brush:xml">
&lt;servlet&gt;
    &lt;servlet-name&gt;messageBroker&lt;/servlet-name&gt;
    &lt;servlet-class&gt;flex.messaging.MessageBrokerServlet&lt;/servlet-class&gt;
    &lt;init-param&gt;
        &lt;param-name&gt;services.configuration.file&lt;/param-name&gt;
        &lt;param-value&gt;/WEB-INF/flex/services-config.xml&lt;/param-value&gt;
    &lt;/init-param&gt;
    &lt;load-on-startup&gt;0&lt;/load-on-startup&gt;
&lt;/servlet&gt;

&lt;servlet-mapping&gt;
    &lt;servlet-name&gt;messageBroker&lt;/servlet-name&gt;
    &lt;url-pattern&gt;/messagebroker/*&lt;/url-pattern&gt;
&lt;/servlet-mapping&gt;
</pre>
<p>BlazeDS所需的所有配置文件均放在/WEB-INF/flex/目录下。BlazeDS将读取services-config.xml配置文件，该配置文件又引用了remoting-config.xml、proxy-config.xml和messaging-config.xml这3个配置文件，所以，一共需要4个配置文件。</p>
<p>由于BlazeDS需要将Java接口FlexService暴露给Flex前端，因此，我们在配置文件remoting-config.xml中将FlexService接口声明为一个服务：</p>
<pre class="brush:xml">
&lt;destination id=&quot;flexService&quot;&gt;
    &lt;properties&gt;
        &lt;source&gt;org.expressme.employee.mgmt.flex.FlexServiceImpl&lt;/source&gt;
        &lt;scope&gt;application&lt;/scope&gt;
    &lt;/properties&gt;
&lt;/destination&gt;
</pre>
<p>服务名称通过destination的id属性指定，Flex前端通过该服务名称来进行远程调用。scope指定为application，表示该对象是一个全局对象。</p>
<p>然而，按照默认的声明，BlazeDS会去实例化FlexService对象。对于一个Java EE应用来说，通常这些服务对象都是被容器管理的（例如，Spring容器或EJB容器），更合适的方法是查找该服务对象而非直接实例化。因此，需要告诉BlazeDS通过Factory来查找指定的FlexService对象，修改配置如下：</p>
<pre class="brush:xml">
&lt;destination id=&quot;flexService&quot;&gt;
    &lt;properties&gt;
        &lt;factory&gt;flexFactory&lt;/factory&gt;
        &lt;source&gt;flexService&lt;/source&gt;
        &lt;scope&gt;application&lt;/scope&gt;
    &lt;/properties&gt;
&lt;/destination&gt;
</pre>
<p>现在，Flex如何才能通过BlazeDS调用FlexService接口呢？由于FlexService对象已经被Spring管理，因此，我们需要编写一个FlexFactory告诉BlazeDS如何找到Spring管理的FlexService的实例。flexFactory在services-config.xml中指定：</p>
<pre class="brush:xml">
&lt;factories&gt;
    &lt;factory id=&quot;flexFactory&quot; class=&quot;org.expressme.employee.mgmt.flex.FlexFactoryImpl&quot;/&gt;
&lt;/factories&gt;
</pre>
<p>FlexFactoryImpl实现了FlexFactory接口，该接口完成两件事情：</p>
<ol>
    <li>创建 FactoryInstance 对象；</li>
    <li>通过 FactoryInstance 对象查找我们需要的FlexService。</li>
</ol>
<p>因此，需要一个FactoryInstance的实现类，我们编写一个SpringFactoryInstance，以便从Spring的容器中查找FlexService：</p>
<pre class="brush:java">
class SpringFactoryInstance extends FactoryInstance {
    private Log log = LogFactory.getLog(getClass());

    SpringFactoryInstance(FlexFactory factory, String id, ConfigMap properties) {
        super(factory, id, properties);
    }

    public Object lookup() {
        ApplicationContext appContext = WebApplicationContextUtils.
                getRequiredWebApplicationContext(
                    FlexContext.getServletConfig().getServletContext()
        );
        String beanName = getSource();
        try {
            log.info(&quot;Lookup bean from Spring ApplicationContext: &quot; + beanName);
            return appContext.getBean(beanName);
        }
        catch (NoSuchBeanDefinitionException nex) {
            ...
        }
        catch (BeansException bex) {
            ...
        }
        catch (Exception ex) {
            ...
        }
    }
}
</pre>
<p>FlexFactoryImpl负责实例化SpringFactoryInstance并通过SpringFactoryInstance的lookup()方法查找FlexService接口对象：</p>
<pre class="brush:java">
public class FlexFactoryImpl implements FlexFactory {
    private Log log = LogFactory.getLog(getClass());

    public FactoryInstance createFactoryInstance(String id, ConfigMap properties) {
        log.info(&quot;Create FactoryInstance.&quot;);
        SpringFactoryInstance instance = new SpringFactoryInstance(this, id, properties);
        instance.setSource(properties.getPropertyAsString(SOURCE, instance.getId()));
        return instance;
    }

    public Object lookup(FactoryInstance instanceInfo) {
        log.info(&quot;Lookup service object.&quot;);
        return instanceInfo.lookup();
    }

    public void initialize(String id, ConfigMap configMap) {
    }
}
</pre>
<p>以下是BlazeDS查找FlexService接口的过程：</p>
<ol>
    <li>BlazeDS将首先创建FlexFactory的实例&mdash;&mdash;FlexFactoryImpl；</li>
    <li>当接收到Flex前端的远程调用请求时，BlazeDS通过FlexFactory创建FactoryInstance对象，并传入请求的Service ID。在这个应用程序中，被创建的FactoryInstance实际对象是SpringFactoryInstance；</li>
    <li>FactoryInstance的lookup()方法被调用，在SpringFactoryInstance中，首先查找Spring容器，然后，通过Bean的ID查找Bean，最终，FlexService接口的实例被返回。</li>
</ol>
<p>注意到destination的id并没有写死在代码中，而是通过以下语句获得的：</p>
<pre class="brush:java">
properties.getPropertyAsString(SOURCE, instance.getId())
</pre>
<p>Property的SOURCE属性由BlazeDS读取XML配置文件获得：</p>
<pre class="brush:xml">
&lt;destination id=&quot;flexService&quot;&gt;
    &lt;properties&gt;
        &lt;factory&gt;flexFactory&lt;/factory&gt;
        &lt;source&gt;flexService&lt;/source&gt;
        &lt;scope&gt;application&lt;/scope&gt;
    &lt;/properties&gt;
&lt;/destination&gt;
</pre>
<p>如果您没有使用Spring框架，也不要紧，只需修改FactoryInstance的lookup()方法。例如，对于一个EJB来说，lookup()方法应该通过JNDI查找返回远程接口。无论应用程序结构如何，我们的最终目标是向BlazeDS返回一个FlexService的实例对象。</p>
<h3 style="color: red">开发Flex客户端</h3>
<p>首先安装Flex Builder 3，可以在Adobe的官方网站获得30天免费试用版。然后，打开Flex Builder 3，创建一个新的Flex Project，命名为EmployeeMgmt-Flex：</p>
<p><img alt="" width="456" height="590" src="/upload/156/246/14/2133e47cbe2f461cb3f4ebf1f2efc107.png" /></p>
<p>Flex Project需要指定Server端的配置文件地址：</p>
<p><img alt="" width="467" height="536" src="/upload/246/57/200/b460ed99c82d4c9993b0e4b067072b8b.png" /></p>
<p>因此，需要填入EmployeeMgmt-Server项目的web根目录，该目录下必须要存在/WEB-INF/flex/。点击&ldquo;Validate Configuration&rdquo;验证配置文件是否正确，只有通过验证后，才能继续。默认地，Flex Builder将会把生成的Flash文件放到EmployeeMgmt-Server项目的web/EmployeeMgmt-Flex-debug目录下。</p>
<p>一个Flex Project的目录结构如下：</p>
<p><img alt="" width="332" height="291" src="/upload/101/123/167/0755c480b9564090ab83bf40570e61ca.png" /></p>
<p>用Flex Builder做出漂亮的用户界面非常容易。Flex Builder提供了一个可视化的编辑器，通过简单的拖拽，一个毫无经验的开发人员也能够设计出漂亮的布局。如果熟悉一点XML的知识，编辑MXML也并非难事。我们设计的Employee Management系统界面的最终效果如下：</p>
<p><img alt="" width="572" height="364" src="/upload/141/207/141/79bdf2e757214346a001ec1a0079fbf3.png" /></p>
<p>本文不打算讨论如何编写Flex界面，而是把重点放在如何实现远程调用。</p>
<p>为了能在Flex中实现远程调用，我们需要定义一个RemoteObject对象。可以通过ActionScript编码创建该对象，也可以直接在MXML中定义一个RemoteObject对象，并列出其所有的方法：</p>
<pre class="brush:xml">
&lt;mx:RemoteObject id=&quot;flexServiceRO&quot; destination=&quot;flexService&quot;&gt;
    &lt;mx:method name=&quot;queryAll&quot; result=&quot;handleQueryAll(result : ResultEvent)&quot;/&gt;
&lt;/mx:RemoteObject&gt;
</pre>
<p>现在，就可以调用这个名为flexServiceRO的RemoteObject对象的方法了：</p>
<pre class="brush:as3">
flexServiceRO.queryAll(function(result : ResultEvent) {
    var employees = result.result as Array;
});
</pre>
<p>运行该Flex Application，雇员信息已经被正确获取了：</p>
<p><img alt="" width="572" height="481" src="/upload/181/186/127/e46ebc4661a2446bae8c4260e4266dec.png" /></p>
<h3 style="color: red">增强RemoteObject对象</h3>
<p>通过RemoteObject进行调用虽然简单，但存在不少问题：首先，RemoteObject是一个Dynamic Class，Flex Builder的编译器无法替我们检查参数类型和参数个数，这样，在编写ActionScript代码时极易出错。此外，接口变动时（这种情况常常发生），需要重新修改RemoteObject的定义。此外，Flex团队需要一份随时修订的完整的FlexService接口文档才能工作。</p>
<p>因此，最好能使用强类型的RemoteObject接口，让Flex Builder的编译器及早发现错误。这个强类型的RemoteObject最好能通过Java EE应用的FlexService接口自动生成，这样，就无需再维护RemoteObject的定义。</p>
<p>为了能完成自动生成RemoteObject对象，我编写了一个Java2ActionScript的Ant任务来自动转换FlexService接口以及相关的所有JavaBean。JavaInterface2RemoteObjectTask完成一个Java接口对象到RemoteObject对象的转换。使用如下的Ant脚本：</p>
<pre class="brush:xml">
&lt;taskdef name=&quot;genactionscript&quot; classname=&quot;org.expressme.ant.JavaBean2ActionScriptTask&quot;&gt;
    &lt;classpath refid=&quot;build-classpath&quot; /&gt;
&lt;/taskdef&gt;
&lt;taskdef name=&quot;genremoteobject&quot;
    classname=&quot;org.expressme.ant.JavaInterface2RemoteObjectTask&quot;&gt;
    &lt;classpath refid=&quot;build-classpath&quot; /&gt;
&lt;/taskdef&gt;
&lt;genactionscript
    packageName=&quot;org.expressme.employee.mgmt&quot;
    includes=&quot;Employee&quot;
    orderByName=&quot;true&quot;
    encoding=&quot;UTF-8&quot;
    outputDir=&quot;${gen.dir}&quot;
/&gt;
&lt;genremoteobject
    interfaceClass=&quot;org.expressme.employee.mgmt.flex.FlexService&quot;
    encoding=&quot;UTF-8&quot;
    outputDir=&quot;${gen.dir}&quot;
    destination=&quot;flexService&quot;
/&gt;
</pre>
<p>转换后的FlexServiceRO类拥有Java接口对应的所有方法，每个方法均为强类型签名，并添加额外的两个可选的函数处理result和fault事件。例如，queryByName方法：</p>
<pre class="brush:as3">
public function queryByName(arg1 : String, result : Function = null, fault : Function = null) : void {
    var op : AbstractOperation = ro.getOperation(&quot;queryByName&quot;);
    if (result!=null) {
        op.addEventListener(ResultEvent.RESULT, result);
    }
    if (fault!=null) {
        op.addEventListener(FaultEvent.FAULT, fault);
    }
    var f : Function = function() : void {
        op.removeEventListener(ResultEvent.RESULT, f);
        op.removeEventListener(FaultEvent.FAULT, f);
        if (result!=null) {
            op.removeEventListener(ResultEvent.RESULT, result);
        }
        if (fault!=null) {
            op.addEventListener(FaultEvent.FAULT, fault);
        }
    }
    op.addEventListener(ResultEvent.RESULT, f);
    op.addEventListener(FaultEvent.FAULT, f);
    op.send(arg1);
}
</pre>
<p>转换Java接口是通过Interface.as和InterfaceMethod.as两个模板文件完成的，此外，所有在Java EE后端和Flex之间传递的JavaBean对象也通过JavaBean2ActionScriptTask自动转换成对应的ActionScript类，这是通过Bean.as模板完成的。</p>
<p>有了Java类到ActionScript的自动转换，我们在编写ActionScript时，就能享受到编译器检查和ActionScript类方法的自动提示了：</p>
<p><img alt="" width="556" height="283" src="/upload/1/61/123/4eeda2035ad94fc89776c27370005f63.png" /></p>
<p>唯一的缺憾是通过反射读取FlexService接口时，我们失去了方法的参数名称，因此，FlexServiceRO的方法参数名只能变成arg1，arg2&hellip;&hellip; 等，要读取FlexService接口的方法参数名，只能通过解析Java源代码实现。</p>
<p>现在，Java EE后端开发团队和Flex前端开发团队只需协商定义好FlexService接口，然后，利用Java2ActionScript，Flex团队就得到了强类型的FlexServiceRO类，而Java EE团队则只需集中精力实现FlexService接口。</p>
<p>在开发的前期，甚至可以用硬编码的FlexService的实现类。每当FlexService变动时，只需再次运行Ant脚本，就可以获得最新的FlexServiceRO类。这样，两个团队都可以立刻开始工作，仅需要通过FlexService接口就可以完美地协同开发。</p>
<h3 style="color: red">下载</h3>
<p>Java EE工程源码：<a target="_blank" href="http://express-me.googlecode.com/files/EmployeeMgmt-Server.zip">EmployeeMgmt-Server.zip</a></p>
<p>Flex工程源码：<a target="_blank" href="http://express-me.googlecode.com/files/EmployeeMgmt-Flex.zip">EmployeeMgmt-Flex.zip</a></p>
<p>Java2ActionScript工程源码：<a target="_blank" href="http://express-me.googlecode.com/files/Java2ActionScript.zip">Java2ActionScript.zip</a></p>
<h3 style="color: red">参考资料</h3>
<p>&ldquo;<a target="_blank" href="http://www.adobe.com/support/documentation/en/flex/">Adobe Flex参考资料</a>&rdquo;：查看Adobe Flex的文档集。</p>
<p>&ldquo;<a target="_blank" href="http://www.ibm.com/developerworks/cn/web/wa-lo-flexdev/">Flex开发入门</a>&rdquo;（developerWorks，2009 年 1 月）：本文介绍Flex开发的基础知识：包括如何搭建开发环境，如何调试，以及如何建立和部署简单的Flex项目。</p>
<p>&ldquo;<a target="_blank" href="http://www.ibm.com/developerworks/cn/web/wa-aj-flex/">集成Flex与Ajax应用程序</a>&rdquo;（developerWorks，2008年7月）：本文将介绍Adobe Flex Ajax Bridge (FABridge)，这是让您可以采用轻松而一致的方法集成Ajax与Flex内容的代码库。</p>
<p>&ldquo;<a target="_blank" href="http://www.ibm.com/developerworks/cn/web/wa-lo-flexgoogle/">用Flex开发Google Map应用程序</a>&rdquo;（developerWorks，2009年3月）：介绍如何用Google Maps API for Flash来开发基于Flash的地图应用程序。</p>
<h3 style="color: red">获得产品和技术</h3>
<p><a target="_blank" href="http://www.adobe.com/products/flex/">访问Flex产品页面</a>。</p>
<p><a target="_blank" href="http://opensource.adobe.com/wiki/display/blazeds/BlazeDS">访问Adobe BlazeDS项目站点</a>。</p>
<p><a target="_blank" href="http://www.eclipse.org/downloads/">下载Eclipse</a>。</p>
<p><a target="_blank" href="http://www.adobe.com/cfusion/entitlement/index.cfm?e=flexbuilder3">下载Flex Builder</a>。</p>
<h3 style="color: red">关于作者</h3>
<p>廖雪峰，5年Java EE开发经验，对开源框架有深入研究，著有《<a target="_blank" href="http://www.livebookstore.net">Spring 2.0核心技术与最佳实践</a>》一书，其官方博客是<a target="_blank" href="http://www.liaoxuefeng.com">http://www.liaoxuefeng.com</a>，可以通过<a href="mailto:askxuefeng@gmail.com">askxuefeng@gmail.com</a>与之联系。</p>]]></description></item>
<item><title>J2ME陷阱一例</title><link>http://www.liaoxuefeng.com/it-15f2f1dc66104bca9e431aacdc16d9e9-1</link><dc:creator>廖雪峰</dc:creator><pubDate>Mon, 03 Aug 2009 10:16:16 +0800</pubDate><description><![CDATA[<p>用WTK 2.5开发MIDP应用时，自己写了个冒泡排序，模拟器运行正常，真机上报NoClassDefFoundError，原来是没有java.lang.Comparable这个接口，但是WTK编译居然通过了！校验器也没验出任何问题。</p>
<p>解决办法：</p>
<p>自定义一个IsComparable接口，将要排序的类实现此接口：</p>
<pre class="brush:java">
public static void sort(Vector v) {
    int size = v.size();
    for (int i=0; i&lt;size; i++) {
        for (int j=i+1; j&lt;size; j++) {
            IsComparable o1 = (IsComparable) v.elementAt(i);
            Object o2 = v.elementAt(j);
            if (o1.compareTo(o2) &gt; 0) {
                // swap:
                v.setElementAt(o1, j);
                v.setElementAt(o2, i);
            }
        }
    }
}
</pre>]]></description></item>
<item><title>快速排序算法</title><link>http://www.liaoxuefeng.com/it-5f0c97c3f7cf4b4d8d47d9c2f792d0a4-1</link><dc:creator>廖雪峰</dc:creator><pubDate>Fri, 31 Jul 2009 19:35:30 +0800</pubDate><description><![CDATA[<p>快速排序是一种基于分治的算法，其基本思想是将一个大数组按照一个基准数分成左右两份，左边的部份都不大于基准数，右边的部分都不小于基准数。然后，对这两份再分别应用快速排序，直到分到只剩2个数为止。</p>
<p>快速排序在通常情况下是最快的排序算法，以下是用Python实现的一个例子：</p>
<pre class="brush:python">
'''
qsort.py

Quick sort

Created on Jun 18, 2009

@author: Liao
'''

from random import Random

def quick_sort(arr):
    if len(arr) &gt; 1:
        qsort(arr, 0, len(arr) - 1)

def qsort(arr, start, end):
    base = arr[start]
    pl = start
    pr = end
    while pl &lt; pr:
        while pl &lt; pr and arr[pr] &gt;= base:
            pr -= 1
        if pl == pr:
            break
        else:
            arr[pl], arr[pr] = arr[pr], arr[pl]

        while pl &lt; pr and arr[pl] &lt;= base:
            pl += 1
        if pl == pr:
            break
        else:
            arr[pl], arr[pr] = arr[pr], arr[pl]
    # now pl == pr
    if pl - 1 &gt; start:
        qsort(arr, start, pl - 1)
    if pr + 1 &lt; end:
        qsort(arr, pr + 1, end)

r = Random()
a = []
for i in range(20):
    a.append(r.randint(0, 100))

print a
quick_sort(a)
print a
</pre>
<p>快速排序是一种不稳定排序，而冒泡排序则是稳定排序。</p>
<p>稳定排序是指如果排序前有两个相同的数，比如对[a=10, b=10, c=2]排序，a和b相等，排序前a在b的前面，稳定排序后结果为[c, a, b]，a仍然在b的前面，而不稳定排序则不保证相等的两个数位置不会交换，排序结果可能变为[c, b, a]。</p>]]></description></item>
<item><title>在您的网站或博客中添加中华诗词</title><link>http://www.liaoxuefeng.com/it-744320040e9b4c0a918262c7e3ab3deb-1</link><dc:creator>廖雪峰</dc:creator><pubDate>Wed, 29 Jul 2009 11:13:31 +0800</pubDate><description><![CDATA[<p>中华诗词（<a target="_blank" href="http://www.shi-ci.com/">http://www.shi-ci.com</a>）免费提供上至诗经下至当代共计6万余首诗词，供广大诗词爱好者在线阅读和搜索。此外，中华诗词还提供了一段免费的代码让您的网站或博客能每天都自动显示不同的诗词，只需在网站或博客的合适位置插入以下JavaScript代码即可：</p>
<pre class="brush:javascript">
&lt;script language=&quot;javascript&quot; charset=&quot;UTF-8&quot; src=&quot;http://www.shi-ci.com/embeded.do&quot;&gt;&lt;/script&gt;
</pre>
<p>此外，如果您的博客是<a target="_blank" href="http://blog.sohu.com/">sohu博客</a>，可以更方便地向您的博客添加&ldquo;中华诗词&rdquo;模块，在您的sohu博客中每天自动显示一首中华诗词：</p>
<p><a target="_blank" href="http://ow.blog.sohu.com/widget/771"><img width="444" height="336" alt="" src="/upload/227/255/141/7c699db8d86447f7bfbf3b936e4766d8.png" /></a></p>
<p>地址：<a target="_blank" href="http://ow.blog.sohu.com/widget/771">http://ow.blog.sohu.com/widget/771</a></p>
<p>或者，您还可以添加&ldquo;唐诗300首&rdquo;模块，在您的sohu博客中每天自动显示唐诗300首中的一首：</p>
<p><a target="_blank" href="http://ow.blog.sohu.com/widget/782"><img width="443" height="335" alt="" src="/upload/46/79/131/11a935a113fb4fc99dcd5ce14edb88fd.png" /></a></p>
<p>地址：<a target="_blank" href="http://ow.blog.sohu.com/widget/782">http://ow.blog.sohu.com/widget/782</a></p>
<p>或者，您还可以添加&ldquo;毛主席诗词&rdquo;模块，在您的sohu博客中每天自动显示一首毛主席诗词：</p>
<p><a target="_blank" href="http://ow.blog.sohu.com/widget/785"><img width="441" height="333" alt="" src="/upload/139/149/131/6810844ac9bd478e9c850f56921588a7.png" /></a></p>
<p>地址：<a target="_blank" href="http://ow.blog.sohu.com/widget/785">http://ow.blog.sohu.com/widget/785</a></p>
<p>添加后的博客效果如下：</p>
<p><img width="620" height="962" alt="" src="/upload/53/21/75/d82c13faa9894d28a516236633aaf9f3.png" /></p>
<p>根据您的sohu博客主题，显示效果会有所不同。</p>]]></description></item>
<item><title>在Eclipse中快速浏览源代码</title><link>http://www.liaoxuefeng.com/it-981c9e3d44fd4ee595bd3d6e55689ccb-1</link><dc:creator>廖雪峰</dc:creator><pubDate>Tue, 28 Jul 2009 11:14:20 +0800</pubDate><description><![CDATA[<p>在Eclipse中，只需随时按住Ctrl并点击某个类名或方法名，即可跳转到相应的代码中。然而，如果引用一个开源的jar包，则会直 接打开其class的二进制码，这对于调试或研究代码内部流程颇为不便，尽管可以在Build Path中为每个jar指定源代码位置，但这样一来，对于同一个jar（例如spring.jar），每个工程都要指定，比较麻烦。</p>
<p>另一种更简单的方式是直接用WinZip或WinRAR之类的工具解开jar，再把源码也放进去，注意路径要正确，同一个Xxx.class和 Xxx.java应该在同一目录下，再用zip打包成jar包（jar格式其实就是zip格式），以后无论在哪个工程引用该jar包，Eclipse都可以直接从jar包中读出其对应的源代码，不必在Build Path中配置源代码位置，对于开源组件来说，大大方便了代码的跟踪和测试。</p>]]></description></item>
<item><title>向Google博客搜索发出Ping通知</title><link>http://www.liaoxuefeng.com/it-41cfef3489aa4a86b1789c3d67dba988-1</link><dc:creator>廖雪峰</dc:creator><pubDate>Fri, 24 Jul 2009 12:43:17 +0800</pubDate><description><![CDATA[<p>通过Google&ldquo;博客搜索&rdquo;Ping API, 用户可以程序化的方式将博客内容的更新通知给Google&ldquo;博客搜索&rdquo;引擎。这对于经常更新博客内容的用户尤其有用。博客服务提供商的管理人员也可以利用此API将其平台上的博客内容变化向Google通告，以便Google&ldquo;博客搜索&rdquo;及时抓取来自这一服务提供商的最新内容。</p>
<p>Google&ldquo;博客搜索&rdquo;支持XML-RPC客户端和REST客户端。使用XML-RPC时，需要构造一个XML，然后将其POST到Google的指定地址，比较麻烦，而REST则既简单又方便。</p>
<p>使用REST时，只需构造一个如下URL：</p>
<p>http://blogsearch.google.com/ping?name=xxx&amp;url=xxx&amp;changesURL=xxx</p>
<p>然后以GET发送，成功后会返回字符串&ldquo;Thanks for the ping.&rdquo;。</p>
<p>Google会根据url参数抓取blog页面并在最短的时间内索引。</p>]]></description></item>
<item><title>关于Jetty锁定静态文件的问题解决办法</title><link>http://www.liaoxuefeng.com/it-18357918ad55451198d5e128ced71531-1</link><dc:creator>廖雪峰</dc:creator><pubDate>Fri, 24 Jul 2009 12:40:58 +0800</pubDate><description><![CDATA[<p>Jetty是一个优秀的Web服务器，最大的特点是可嵌入应用程序，因此作为调试服务器非常方便，就像跟踪普通的main()方法一样可以在Eclipse中直接调试Web应用而无需远程连接。但是使用Jetty发现一个问题，即Windows上启动后Jetty会锁定已访问的静态文件，如HTML，CSS等，这给页面设计带来了不便。</p>
<p>其实Jetty官方站点对此问题已有回答，锁定文件据说是为了提高性能，但我觉得缓存也不一定需要长时间锁定文件：<a target="_blank" href="http://docs.codehaus.org/display/JETTY/Files+locked+on+Windows">http://docs.codehaus.org/display/JETTY/Files+locked+on+Windows</a></p>
<p>其实可以修改Jetty默认的配置文件，在jetty-6.1.5.jar中找到org/mortbay/jetty/webapp/webdefault.xml，搜索useFileMappedBuffer：</p>
<pre class="brush:xml">
&lt;init-param&gt;
    &lt;param-name&gt;useFileMappedBuffer&lt;/param-name&gt;
    &lt;param-value&gt;true&lt;/param-value&gt;
&lt;/init-param&gt;
</pre>
<p>将param-value从true改为false即可。可以直接修改jar包内的这个文件，但是修改发行包毕竟不好，可以将此文件复制一份，在启动Jetty时用自己的这个webdefault.xml覆盖Jetty的设置即可。加上：</p>
<pre class="brush:java">
WebAppContext webapp = new WebAppContext();
webapp.setDefaultsDescriptor(&quot;./webdefault.xml&quot;);
</pre>
<p>重新启动后问题解决。</p>]]></description></item>
<item><title>控制WebLogic解压war包</title><link>http://www.liaoxuefeng.com/it-5418319b12cd4249a11d751407af50c0-1</link><dc:creator>廖雪峰</dc:creator><pubDate>Fri, 24 Jul 2009 12:37:09 +0800</pubDate><description><![CDATA[<p>在开发web应用时，如果通过weblogic的控制台部署war包，则weblogic默认在运行期不会解压war，这对于调试jsp颇为不便。其实，只需一个简单的设置就可以强迫weblogic解开war，并且编辑jsp后weblogic会重新加载，方便调试。</p>
<p>以8.1 sp4为例，打开bea/user_projects/domains/&lt;my-domain&gt;/config.xml，找到相应的war包：</p>
<pre class="brush:xml">
&lt;Application Name=&quot;test&quot;
    Path=&quot;C:\java\bea\user_projects\domains\mydomain\applications\test.war&quot;
    StagingMode=&quot;nostage&quot; TwoPhase=&quot;true&quot;&gt;
</pre>
<p>将StagingMode由nostage改为stage，重启weblogic即可。解压后的目录在myserver目录下。</p>
<p>需要注意的是，一旦war包需要重新部署，除了更新war包外，还要删除bea/user_projects/domains/&lt;my-domain&gt;/myserver目录下的.wlnotdelete和stage目录，以便强迫weblogic重新解开最新的war包，否则将继续使用原来已解压的目录。</p>]]></description></item>
<item><title>Subclipse入门指南</title><link>http://www.liaoxuefeng.com/it-38cd7066c4014e63a1c7302043b58ae0-1</link><dc:creator>廖雪峰</dc:creator><pubDate>Fri, 24 Jul 2009 12:33:51 +0800</pubDate><description><![CDATA[<p>Subversion是新一代的开源版本控制系统，和CVS相比，Subversion最大的特点是支持事务，可以确保一个提交是原子操作。此外，Subversion还支持更多的协议，包括HTTP访问。在Eclipse中，使用Subverison和CVS一样简单，只需安装Subclipse插件就可以了。</p>
<p>本文以Eclipse 3.3为例，安装Subclipse非常容易，打开Eclipse，选择菜单Help-&gt;Software Updates-&gt;Find and Install&hellip;，在弹出的对话框中选择&ldquo;Search for new features to install&rdquo;，然后点击&ldquo;New Remote Site&hellip;&rdquo;，填入Subclipse的在线安装的URL：</p>
<p><img alt="" width="356" height="154" src="/upload/173/139/130/cdd7db96bdf54f28bf7878acea453844.jpg" /></p>
<p>按照提示安装完毕后，我们就可以打开Subversion的资源库了。选择Eclipse菜单Window-&gt;Show View-&gt;Other&hellip;，选择SVN-&gt;SVN Repository，然后添加一个新的资源库，例如<a href="http://livebookstore.googlecode.com/svn/trunk">http://livebookstore.googlecode.com/svn/trunk</a>：</p>
<p><img alt="" width="401" height="252" src="/upload/210/118/243/c85d2d1527c54ce9b29d1aedb51d8c15.jpg" /></p>
<p>添加完毕后，即可直接浏览SVN库的目录结构，然后通过右键菜单Checkout&hellip;检出为一个工程：</p>
<p><img alt="" width="288" height="435" src="/upload/207/111/123/b250b2e7a90d4469a9d8b52a2be76e22.jpg" /></p>
<p>Eclipse提示将目录检出为一个工程：</p>
<p><img width="417" height="410" alt="" src="/upload/217/73/217/62de2d70b9d542a2bf804be0a449933f.jpg" /></p>
<p>点击Finish即完成。</p>]]></description></item>
<item><title>Java多线程设计模式</title><link>http://www.liaoxuefeng.com/it-d225a33ad6e947cea997cc02b1826e7f-1</link><dc:creator>廖雪峰</dc:creator><pubDate>Tue, 21 Jul 2009 17:40:37 +0800</pubDate><description><![CDATA[<h3 style="color: red">线程的创建和启动</h3>
<p>Java语言已经内置了多线程支持，所有实现Runnable接口的类都可被启动一个新线程，新线程会执行该实例的run()方法，当run()方法执行完毕后，线程就结束了。一旦一个线程执行完毕，这个实例就不能再重新启动，只能重新生成一个新实例，再启动一个新线程。</p>
<p>Thread类是实现了Runnable接口的一个实例，它代表一个线程的实例，并且，启动线程的唯一方法就是通过Thread类的start()实例方法：</p>
<pre class="brush:java">
Thread t = new Thread();
t.start();
</pre>
<p>start()方法是一个native方法，它将启动一个新线程，并执行run()方法。Thread类默认的run()方法什么也不做就退出了。注意：直接调用run()方法并不会启动一个新线程，它和调用一个普通的java方法没有什么区别。</p>
<p>因此，有两个方法可以实现自己的线程：</p>
<p>方法1：自己的类extend Thread，并复写run()方法，就可以启动新线程并执行自己定义的run()方法。例如：</p>
<pre class="brush:java">
public class MyThread extends Thread {
    public run() {
        System.out.println(&quot;MyThread.run()&quot;);
    }
}
</pre>
<p>在合适的地方启动线程：new MyThread().start();</p>
<p>方法2：如果自己的类已经extends另一个类，就无法直接extends Thread，此时，必须实现一个Runnable接口：</p>
<pre class="brush:java">
public class MyThread extends OtherClass implements Runnable {
    public run() {
        System.out.println(&quot;MyThread.run()&quot;);
    }
}
</pre>
<p>为了启动MyThread，需要首先实例化一个Thread，并传入自己的MyThread实例：</p>
<pre class="brush:java">
MyThread myt = new MyThread();
Thread t = new Thread(myt);
t.start();
</pre>
<p>事实上，当传入一个Runnable target参数给Thread后，Thread的run()方法就会调用target.run()，参考JDK源代码：</p>
<pre class="brush:java">
public void run() {
    if (target != null) {
        target.run();
    }
}
</pre>
<p>线程还有一些Name, ThreadGroup, isDaemon等设置，由于和线程设计模式关联很少，这里就不多说了。</p>
<h3 style="color: red">线程的同步</h3>
<p>由于同一进程内的多个线程共享内存空间，在Java中，就是共享实例，当多个线程试图同时修改某个实例的内容时，就会造成冲突，因此，线程必须实现共享互斥，使多线程同步。</p>
<p>最简单的同步是将一个方法标记为synchronized，对同一个实例来说，任一时刻只能有一个synchronized方法在执行。当一个方法正在执行某个synchronized方法时，其他线程如果想要执行这个实例的任意一个synchronized方法，都必须等待当前执行 synchronized方法的线程退出此方法后，才能依次执行。</p>
<p>但是，非synchronized方法不受影响，不管当前有没有执行synchronized方法，非synchronized方法都可以被多个线程同时执行。</p>
<p>此外，必须注意，只有同一实例的synchronized方法同一时间只能被一个线程执行，不同实例的synchronized方法是可以并发的。例如，class A定义了synchronized方法sync()，则不同实例a1.sync()和a2.sync()可以同时由两个线程来执行。</p>
<h3 style="color: red">Java锁机制</h3>
<p>多线程同步的实现最终依赖锁机制。我们可以想象某一共享资源是一间屋子，每个人都是一个线程。当A希望进入房间时，他必须获得门锁，一旦A获得门锁，他进去后就立刻将门锁上，于是B,C,D...就不得不在门外等待，直到A释放锁出来后，B,C,D...中的某一人抢到了该锁（具体抢法依赖于 JVM的实现，可以先到先得，也可以随机挑选），然后进屋又将门锁上。这样，任一时刻最多有一人在屋内（使用共享资源）。</p>
<p>Java语言规范内置了对多线程的支持。对于Java程序来说，每一个对象实例都有一把&ldquo;锁&rdquo;，一旦某个线程获得了该锁，别的线程如果希望获得该锁，只能等待这个线程释放锁之后。获得锁的方法只有一个，就是synchronized关键字。例如：</p>
<pre class="brush:java">
public class SharedResource {
    private int count = 0;

    public int getCount() { return count; }

    public synchronized void setCount(int count) { this.count = count; }
}
</pre>
<p>注意，如果将synchronized关键字标记在方法上，例如上面的：</p>
<pre class="brush:java">
public synchronized void setCount(int count) { ... }
</pre>
<p>那么，锁住的是哪个对象呢？答案是<span style="color: #ff0000"><strong>this</strong></span>对象，因此，以上方法事实上完全等同于下面的写法：</p>
<pre class="brush:java">
public void setCount(int count) {
    synchronized(this) { // 在此获得this锁
         this.count = count;
    } // 在此释放this锁
}
</pre>
<p>synchronized {}括号内的部分表示需要同步的代码段，该区域为&ldquo;危险区域&rdquo;，如果两个以上的线程同时执行，会引发冲突，因此，要更改SharedResource的内部状态，必须先获得SharedResource实例的锁。</p>
<p>退出synchronized块时，线程拥有的锁自动释放，于是，别的线程又可以获取该锁了。</p>
<p>为了提高性能，不一定要锁定this，例如，SharedResource有两个独立变化的变量：</p>
<pre class="brush:java">
public class SharedResouce {
    private int a = 0;
    private int b = 0;

    public synchronized void setA(int a) { this.a = a; }
    public synchronized void setB(int b) { this.b = b; }
}
</pre>
<p>若同步整个方法，则setA()的时候无法setB()，setB()时无法setA()。为了提高性能，可以使用不同对象的锁：</p>
<pre class="brush:java">
public class SharedResouce {
    private int a = 0;
    private int b = 0;
    private Object sync_a = new Object();
    private Object sync_b = new Object();

    public void setA(int a) {
        synchronized(sync_a) {
            this.a = a;
        }
    }

    public synchronized void setB(int b) {
        synchronized(sync_b) {
            this.b = b;
        }
    }
}
</pre>
<p>如果将synchronized关键字标记在静态方法上，由于静态方法不可能访问this实例，那么，锁住的是哪个对象呢？答案是<strong><span style="color: #ff0000">当前类的Class对象</span></strong>，原因是每个对象的Class实例是唯一且不可变的。比如：</p>
<pre class="brush:java">
public synchronized static void sync() { ... }
</pre>
<p>事实上完全等同于下面的写法：</p>
<pre class="brush:java">
public static void sync() {
    synchronized(SharedResource.class) {
         ...
    }
}
</pre>
<h3 style="color: red">wait/notify机制</h3>
<p>通常，多线程之间需要协调工作。例如，浏览器的一个显示图片的线程displayThread想要执行显示图片的任务，必须等待下载线程 downloadThread将该图片下载完毕。如果图片还没有下载完，displayThread可以暂停，当downloadThread完成了任务后，再通知displayThread&ldquo;图片准备完毕，可以显示了&rdquo;，这时，displayThread继续执行。</p>
<p>以上逻辑简单的说就是：如果条件不满足，则等待。当条件满足时，等待该条件的线程将被唤醒。在Java中，这个机制的实现依赖于wait/notify。等待机制与锁机制是密切关联的。例如：</p>
<pre class="brush:java">
synchronized(obj) {
    while(!condition) {
        obj.wait();
    }
    obj.doSomething();
}
</pre>
<p>当线程A获得了obj锁后，发现条件condition不满足，无法继续下一处理，于是线程A就wait()。</p>
<p>在另一线程B中，如果B更改了某些条件，使得线程A的condition条件满足了，就可以唤醒线程A：</p>
<pre class="brush:java">
synchronized(obj) {
    condition = true;
    obj.notify();
}
</pre>
<p>需要注意的概念是：</p>
<p># 调用obj的wait(), notify()方法前，必须获得obj锁，也就是必须写在synchronized(obj)&nbsp;{...} 代码段内。</p>
<p># 调用obj.wait()后，线程A就释放了obj的锁，否则线程B无法获得obj锁，也就无法在synchronized(obj) {...} 代码段内唤醒A。</p>
<p># 当obj.wait()方法返回后，线程A需要再次获得obj锁，才能继续执行。</p>
<p># 如果A1,A2,A3都在obj.wait()，则B调用obj.notify()只能唤醒A1,A2,A3中的一个（具体哪一个由JVM决定）。</p>
<p># obj.notifyAll()则能全部唤醒A1,A2,A3，但是要继续执行obj.wait()的下一条语句，必须获得obj锁，因此，A1,A2,A3只有一个有机会获得锁继续执行，例如A1，其余的需要等待A1释放obj锁之后才能继续执行。</p>
<p># 当B调用obj.notify/notifyAll的时候，B正持有obj锁，因此，A1,A2,A3虽被唤醒，但是仍无法获得obj锁。直到B退出synchronized块，释放obj锁后，A1,A2,A3中的一个才有机会获得锁继续执行。</p>
<h3 style="color: red">wait/sleep的区别</h3>
<p>Thread还有一个sleep()静态方法，它也能使线程暂停一段时间。sleep与wait的不同点是：sleep并不释放锁，并且sleep的暂停和wait暂停是不一样的。obj.wait会使线程进入obj对象的等待集合中并等待唤醒。</p>
<p>但是wait()和sleep()都可以通过interrupt()方法打断线程的暂停状态，从而使线程立刻抛出InterruptedException。</p>
<p>如果线程A希望立即结束线程B，则可以对线程B对应的Thread实例调用interrupt方法。如果此刻线程B正在 wait/sleep/join，则线程B会立刻抛出InterruptedException，在catch() {} 中直接return即可安全地结束线程。</p>
<p>需要注意的是，InterruptedException是线程自己从内部抛出的，并不是interrupt()方法抛出的。对某一线程调用 interrupt()时，如果该线程正在执行普通的代码，那么该线程根本就不会抛出InterruptedException。但是，一旦该线程进入到 wait()/sleep()/join()后，就会立刻抛出InterruptedException。</p>
<h3 style="color: red">Worker Pattern</h3>
<p>前面谈了多线程应用程序能极大地改善用户相应。例如对于一个Web应用程序，每当一个用户请求服务器连接时，服务器就可以启动一个新线程为用户服务。</p>
<p>然而，创建和销毁线程本身就有一定的开销，如果频繁创建和销毁线程，CPU和内存开销就不可忽略，垃圾收集器还必须负担更多的工作。因此，线程池就是为了避免频繁创建和销毁线程。</p>
<p>每当服务器接受了一个新的请求后，服务器就从线程池中挑选一个等待的线程并执行请求处理。处理完毕后，线程并不结束，而是转为阻塞状态再次被放入线程池中。这样就避免了频繁创建和销毁线程。</p>
<p>Worker Pattern实现了类似线程池的功能。首先定义Task接口：</p>
<pre class="brush:java">
public interface Task {
    void execute();
}
</pre>
<p>线程将负责执行execute()方法。注意到任务是由子类通过实现execute()方法实现的，线程本身并不知道自己执行的任务。它只负责运行一个耗时的execute()方法。</p>
<p>具体任务由子类实现，我们定义了一个CalculateTask和一个TimerTask：</p>
<pre class="brush:java">
// CalculateTask.java
public class CalculateTask implements Task {
    private static int count = 0;
    private int num = count;
    public CalculateTask() {
        count++;
    }
    public void execute() {
        System.out.println(&quot;[CalculateTask &quot; + num + &quot;] start...&quot;);
        try {
            Thread.sleep(3000);
        }
        catch(InterruptedException ie) {}
        System.out.println(&quot;[CalculateTask &quot; + num + &quot;] done.&quot;);
    }
}

// TimerTask.java
public class TimerTask implements Task {
    private static int count = 0;
    private int num = count;
    public TimerTask() {
        count++;
    }
    public void execute() {
        System.out.println(&quot;[TimerTask &quot; + num + &quot;] start...&quot;);
        try {
            Thread.sleep(2000);
        }
        catch(InterruptedException ie) {}
        System.out.println(&quot;[TimerTask &quot; + num + &quot;] done.&quot;);
    }
}
</pre>
<p>以上任务均简单的sleep若干秒。</p>
<p>TaskQueue实现了一个队列，客户端可以将请求放入队列，服务器线程可以从队列中取出任务：</p>
<pre class="brush:java">
import java.util.*;

public class TaskQueue {
    private List queue = new LinkedList();
    public synchronized Task getTask() {
        while(queue.size()==0) {
            try {
                this.wait();
            }
            catch(InterruptedException ie) {
                return null;
            }
        }
        return (Task)queue.remove(0);
    }
    public synchronized void putTask(Task task) {
        queue.add(task);
        this.notifyAll();
    }
}
</pre>
<p>终于到了真正的WorkerThread，这是真正执行任务的服务器线程：</p>
<pre class="brush:java">
public class WorkerThread extends Thread {
    private static int count = 0;
    private boolean busy = false;
    private boolean stop = false;
    private TaskQueue queue;
    public WorkerThread(ThreadGroup group, TaskQueue queue) {
        super(group, &quot;worker-&quot; + count);
        count++;
        this.queue = queue;
    }
    public void shutdown() {
        stop = true;
        this.interrupt();
        try {
            this.join();
        }
        catch(InterruptedException ie) {}
    }
    public boolean isIdle() {
        return !busy;
    }
    public void run() {
        System.out.println(getName() + &quot; start.&quot;);        
        while(!stop) {
            Task task = queue.getTask();
            if(task!=null) {
                busy = true;
                task.execute();
                busy = false;
            }
        }
        System.out.println(getName() + &quot; end.&quot;);
    }
}
</pre>
<p>前面已经讲过，queue.getTask()是一个阻塞方法，服务器线程可能在此wait()一段时间。此外，WorkerThread还有一个shutdown方法，用于安全结束线程。</p>
<p>最后是ThreadPool，负责管理所有的服务器线程，还可以动态增加和减少线程数：</p>
<pre class="brush:java">
import java.util.*;

public class ThreadPool extends ThreadGroup {
    private List threads = new LinkedList();
    private TaskQueue queue;
    public ThreadPool(TaskQueue queue) {
        super(&quot;Thread-Pool&quot;);
        this.queue = queue;
    }
    public synchronized void addWorkerThread() {
        Thread t = new WorkerThread(this, queue);
        threads.add(t);
        t.start();
    }
    public synchronized void removeWorkerThread() {
        if(threads.size()&gt;0) {
            WorkerThread t = (WorkerThread)threads.remove(0);
            t.shutdown();
        }
    }
    public synchronized void currentStatus() {
        System.out.println(&quot;-----------------------------------------------&quot;);
        System.out.println(&quot;Thread count = &quot; + threads.size());
        Iterator it = threads.iterator();
        while(it.hasNext()) {
            WorkerThread t = (WorkerThread)it.next();
            System.out.println(t.getName() + &quot;: &quot; + (t.isIdle() ? &quot;idle&quot; : &quot;busy&quot;));
        }
        System.out.println(&quot;-----------------------------------------------&quot;);
    }
}
</pre>
<p>currentStatus()方法是为了方便调试，打印出所有线程的当前状态。</p>
<p>最后，Main负责完成main()方法：</p>
<pre class="brush:java">
public class Main {
    public static void main(String[] args) {
        TaskQueue queue = new TaskQueue();
        ThreadPool pool = new ThreadPool(queue);
        for(int i=0; i&lt;10; i++) {
            queue.putTask(new CalculateTask());
            queue.putTask(new TimerTask());
        }
        pool.addWorkerThread();
        pool.addWorkerThread();
        doSleep(8000);
        pool.currentStatus();
        pool.addWorkerThread();
        pool.addWorkerThread();
        pool.addWorkerThread();
        pool.addWorkerThread();
        pool.addWorkerThread();
        doSleep(5000);
        pool.currentStatus();
    }
    private static void doSleep(long ms) {
        try {
            Thread.sleep(ms);
        }
        catch(InterruptedException ie) {}
    }
}
</pre>
<p>main()一开始放入了20个Task，然后动态添加了一些服务线程，并定期打印线程状态，运行结果如下：</p>
<pre>
worker-0 start.
[CalculateTask 0] start...
worker-1 start.
[TimerTask 0] start...
[TimerTask 0] done.
[CalculateTask 1] start...
[CalculateTask 0] done.
[TimerTask 1] start...
[CalculateTask 1] done.
[CalculateTask 2] start...
[TimerTask 1] done.
[TimerTask 2] start...
[TimerTask 2] done.
[CalculateTask 3] start...
-----------------------------------------------
Thread count = 2
worker-0: busy
worker-1: busy
-----------------------------------------------
[CalculateTask 2] done.
[TimerTask 3] start...
worker-2 start.
[CalculateTask 4] start...
worker-3 start.
[TimerTask 4] start...
worker-4 start.
[CalculateTask 5] start...
worker-5 start.
[TimerTask 5] start...
worker-6 start.
[CalculateTask 6] start...
[CalculateTask 3] done.
[TimerTask 6] start...
[TimerTask 3] done.
[CalculateTask 7] start...
[TimerTask 4] done.
[TimerTask 7] start...
[TimerTask 5] done.
[CalculateTask 8] start...
[CalculateTask 4] done.
[TimerTask 8] start...
[CalculateTask 5] done.
[CalculateTask 9] start...
[CalculateTask 6] done.
[TimerTask 9] start...
[TimerTask 6] done.
[TimerTask 7] done.
-----------------------------------------------
Thread count = 7
worker-0: idle
worker-1: busy
worker-2: busy
worker-3: idle
worker-4: busy
worker-5: busy
worker-6: busy
-----------------------------------------------
[CalculateTask 7] done.
[CalculateTask 8] done.
[TimerTask 8] done.
[TimerTask 9] done.
[CalculateTask 9] done.
</pre>
<p>仔细观察：一开始只有两个服务器线程，因此线程状态都是忙，后来线程数增多，7个线程中的两个状态变成idle，说明处于wait()状态。</p>
<p>思考：本例的线程调度算法其实根本没有，因为这个应用是围绕TaskQueue设计的，不是以Thread Pool为中心设计的。因此，Task调度取决于TaskQueue的getTask()方法，你可以改进这个方法，例如使用优先队列，使优先级高的任务先被执行。</p>
<p>如果所有的服务器线程都处于busy状态，则说明任务繁忙，TaskQueue的队列越来越长，最终会导致服务器内存耗尽。因此，可以限制 TaskQueue的等待任务数，超过最大长度就拒绝处理。许多Web服务器在用户请求繁忙时就会拒绝用户：HTTP 503 SERVICE UNAVAILABLE</p>
<p>从JDK 5开始，java.util.concurrent包已经内置了Worker线程模式（即java.util.concurrent.Executors），无需我们手动编写上述代码。不过，理解Worker模式的原理非常重要。</p>
<h3 style="color: red">ReadWriteLock模式</h3>
<p>多线程读写同一个对象的数据是很普遍的，通常，要避免读写冲突，必须保证任何时候仅有一个线程在写入，有线程正在读取的时候，写入操作就必须等待。简单说，就是要避免&ldquo;写-写&rdquo;冲突和&ldquo;读-写&rdquo;冲突。但是同时读是允许的，因为&ldquo;读-读&rdquo;不冲突，而且很安全。</p>
<p>要实现以上的ReadWriteLock，简单的使用synchronized就不行，我们必须自己设计一个ReadWriteLock类，在读之前，必须先获得&ldquo;读锁&rdquo;，写之前，必须先获得&ldquo;写锁&rdquo;。举例说明：</p>
<p>DataHandler对象保存了一个可读写的char[]数组：</p>
<pre class="brush:java">
public class DataHandler {
    // store data:
    private char[] buffer = &quot;AAAAAAAAAA&quot;.toCharArray();

    private char[] doRead() {
        char[] ret = new char[buffer.length];
        for(int i=0; i&lt;buffer.length; i++) {
            ret[i] = buffer[i];
            sleep(3);
        }
        return ret;
    }

    private void doWrite(char[] data) {
        if(data!=null) {
            buffer = new char[data.length];
            for(int i=0; i&lt;buffer.length; i++) {
                buffer[i] = data[i];
                sleep(10);
            }
        }
    }

    private void sleep(int ms) {
        try {
            Thread.sleep(ms);
        }
        catch(InterruptedException ie) {}
    }
}
</pre>
<p>doRead()和doWrite()方法是非线程安全的读写方法。为了演示，加入了sleep()，并设置读的速度大约是写的3倍，这符合通常的情况。</p>
<p>为了让多线程能安全读写，我们设计了一个ReadWriteLock：</p>
<pre class="brush:java">
public class ReadWriteLock {
    private int readingThreads = 0;
    private int writingThreads = 0;
    private int waitingThreads = 0; // waiting for write
    private boolean preferWrite = true;

    public synchronized void readLock() throws InterruptedException {
        while(writingThreads&gt;0 || (preferWrite &amp;&amp; waitingThreads&gt;0))
            this.wait();
        readingThreads++;
    }

    public synchronized void readUnlock() {
        readingThreads--;
        preferWrite = true;
        notifyAll();
    }

    public synchronized void writeLock() throws InterruptedException {
        waitingThreads++;
        try {
            while(readingThreads&gt;0 || writingThreads&gt;0)
                this.wait();
        }
        finally {
            waitingThreads--;
        }
        writingThreads++;
    }

    public synchronized void writeUnlock() {
        writingThreads--;
        preferWrite = false;
        notifyAll();
    }
}
</pre>
<p>readLock()用于获得读锁，readUnlock()释放读锁，writeLock()和writeUnlock()一样。由于锁用完必须释放，因此，必须保证lock和unlock匹配。我们修改DataHandler，加入ReadWriteLock：</p>
<pre class="brush:java">
public class DataHandler {
    // store data:
    private char[] buffer = &quot;AAAAAAAAAA&quot;.toCharArray();
    // lock:
    private ReadWriteLock lock = new ReadWriteLock();

    public char[] read(String name) throws InterruptedException {
        System.out.println(name + &quot; waiting for read...&quot;);
        lock.readLock();
        try {
            char[] data = doRead();
            System.out.println(name + &quot; reads data: &quot; + new String(data));
            return data;
        }
        finally {
            lock.readUnlock();
        }
    }

    public void write(String name, char[] data) throws InterruptedException {
        System.out.println(name + &quot; waiting for write...&quot;);
        lock.writeLock();
        try {
            System.out.println(name + &quot; wrote data: &quot; + new String(data));
            doWrite(data);
        }
        finally {
            lock.writeUnlock();
        }
    }

    private char[] doRead() {
        char[] ret = new char[buffer.length];
        for(int i=0; i&lt;buffer.length; i++) {
            ret[i] = buffer[i];
            sleep(3);
        }
        return ret;
    }
    private void doWrite(char[] data) {
        if(data!=null) {
            buffer = new char[data.length];
            for(int i=0; i&lt;buffer.length; i++) {
                buffer[i] = data[i];
                sleep(10);
            }
        }
    }
    private void sleep(int ms) {
        try {
            Thread.sleep(ms);
        }
        catch(InterruptedException ie) {}
    }
}
</pre>
<p>public方法read()和write()完全封装了底层的ReadWriteLock，因此，多线程可以安全地调用这两个方法：</p>
<pre class="brush:java">
// ReadingThread不断读取数据：
public class ReadingThread extends Thread {
    private DataHandler handler;
    public ReadingThread(DataHandler handler) {
        this.handler = handler;
    }
    public void run() {
        for(;;) {
            try {
                char[] data = handler.read(getName());
                Thread.sleep((long)(Math.random()*1000+100));
            }
            catch(InterruptedException ie) {
                break;
            }
        }
    }
}

// WritingThread不断写入数据，每次写入的都是10个相同的字符：
public class WritingThread extends Thread {
    private DataHandler handler;
    public WritingThread(DataHandler handler) {
        this.handler = handler;
    }
    public void run() {
        char[] data = new char[10];
        for(;;) {
            try {
                fill(data);
                handler.write(getName(), data);
                Thread.sleep((long)(Math.random()*1000+100));
            }
            catch(InterruptedException ie) {
                break;
            }
        }
    }
    // 产生一个A-Z随机字符，填入char[10]:
    private void fill(char[] data) {
        char c = (char)(Math.random()*26+'A');
        for(int i=0; i&lt;data.length; i++)
            data[i] = c;
    }
}
</pre>
<p>最后Main负责启动这些线程：</p>
<pre class="brush:java">
public class Main {
    public static void main(String[] args) {
        DataHandler handler = new DataHandler();
        Thread[] ts = new Thread[] {
                new ReadingThread(handler),
                new ReadingThread(handler),
                new ReadingThread(handler),
                new ReadingThread(handler),
                new ReadingThread(handler),
                new WritingThread(handler),
                new WritingThread(handler)
        };
        for(int i=0; i&lt;ts.length; i++) {
            ts[i].start();
        }
    }
}
</pre>
<p>我们启动了5个读线程和2个写线程，运行结果如下：</p>
<pre>
Thread-0 waiting for read...
Thread-1 waiting for read...
Thread-2 waiting for read...
Thread-3 waiting for read...
Thread-4 waiting for read...
Thread-5 waiting for write...
Thread-6 waiting for write...
Thread-4 reads data: AAAAAAAAAA
Thread-3 reads data: AAAAAAAAAA
Thread-2 reads data: AAAAAAAAAA
Thread-1 reads data: AAAAAAAAAA
Thread-0 reads data: AAAAAAAAAA
Thread-5 wrote data: EEEEEEEEEE
Thread-6 wrote data: MMMMMMMMMM
Thread-1 waiting for read...
Thread-4 waiting for read...
Thread-1 reads data: MMMMMMMMMM
Thread-4 reads data: MMMMMMMMMM
Thread-2 waiting for read...
Thread-2 reads data: MMMMMMMMMM
Thread-0 waiting for read...
Thread-0 reads data: MMMMMMMMMM
Thread-4 waiting for read...
Thread-4 reads data: MMMMMMMMMM
Thread-2 waiting for read...
Thread-5 waiting for write...
Thread-2 reads data: MMMMMMMMMM
Thread-5 wrote data: GGGGGGGGGG
Thread-6 waiting for write...
Thread-6 wrote data: AAAAAAAAAA
Thread-3 waiting for read...
Thread-3 reads data: AAAAAAAAAA
......
</pre>
<p>可以看到，每次读/写都是完整的原子操作，因为我们每次写入的都是10个相同字符。并且，每次读出的都是最近一次写入的内容。</p>
<p>如果去掉ReadWriteLock：</p>
<pre class="brush:java">
public class DataHandler {

    // store data:
    private char[] buffer = &quot;AAAAAAAAAA&quot;.toCharArray();

    public char[] read(String name) throws InterruptedException {
        char[] data = doRead();
        System.out.println(name + &quot; reads data: &quot; + new String(data));
        return data;
    }
    public void write(String name, char[] data) throws InterruptedException {
        System.out.println(name + &quot; wrote data: &quot; + new String(data));
        doWrite(data);
    }

    private char[] doRead() {
        char[] ret = new char[10];
        for(int i=0; i&lt;10; i++) {
            ret[i] = buffer[i];
            sleep(3);
        }
        return ret;
    }
    private void doWrite(char[] data) {
        for(int i=0; i&lt;10; i++) {
            buffer[i] = data[i];
            sleep(10);
        }
    }
    private void sleep(int ms) {
        try {
            Thread.sleep(ms);
        }
        catch(InterruptedException ie) {}
    }
}
</pre>
<p>运行结果如下：</p>
<pre>
Thread-5 wrote data: AAAAAAAAAA
Thread-6 wrote data: MMMMMMMMMM
Thread-0 reads data: AAAAAAAAAA
Thread-1 reads data: AAAAAAAAAA
Thread-2 reads data: AAAAAAAAAA
Thread-3 reads data: AAAAAAAAAA
Thread-4 reads data: AAAAAAAAAA
Thread-2 reads data: MAAAAAAAAA
Thread-3 reads data: MAAAAAAAAA
Thread-5 wrote data: CCCCCCCCCC
Thread-1 reads data: MAAAAAAAAA
Thread-0 reads data: MAAAAAAAAA
Thread-4 reads data: MAAAAAAAAA
Thread-6 wrote data: EEEEEEEEEE
Thread-3 reads data: EEEEECCCCC
Thread-4 reads data: EEEEEEEEEC
Thread-1 reads data: EEEEEEEEEE
</pre>
<p>从最后4行可以看到在Thread-6写入EEEEEEEEEE的过程中，3个线程读取的内容是不同的。</p>
<p><strong>思考</strong></p>
<p>java的synchronized提供了最底层的物理锁，要在synchronized的基础上，实现自己的逻辑锁，就必须仔细设计ReadWriteLock。</p>
<p>Q: lock.readLock()为什么不放入try { }内？</p>
<p>A: 因为readLock()会抛出InterruptedException，导致readingThreads++不执行，而readUnlock()在 finally { }中，导致readingThreads--执行，从而使readingThread状态出错。writeLock()也是类似的。</p>
<p>Q: preferWrite有用吗？</p>
<p>A: 如果去掉preferWrite，线程安全不受影响。但是，如果读取线程很多，上一个线程还没有读取完，下一个线程又开始读了，就导致写入线程长时间无法获得writeLock；如果写入线程等待的很多，一个接一个写，也会导致读取线程长时间无法获得readLock。preferWrite的作用是让读/写交替执行，避免由于读线程繁忙导致写无法进行和由于写线程繁忙导致读无法进行。</p>
<p>Q: notifyAll()换成notify()行不行？</p>
<p>A: 不可以。由于preferWrite的存在，如果一个线程刚读取完毕，此时preferWrite=true，再notify()，若恰好唤醒的是一个读线程，则while(writingThreads&amp;gt;0 || (preferWrite &amp;&amp; waitingThreads&amp;gt;0))可能为true导致该读线程继续等待，而等待写入的线程也处于wait()中，结果所有线程都处于wait ()状态，谁也无法唤醒谁。因此，notifyAll()比notify()要来得安全。程序验证notify()带来的死锁：</p>
<pre>
Thread-0 waiting for read...
Thread-1 waiting for read...
Thread-2 waiting for read...
Thread-3 waiting for read...
Thread-4 waiting for read...
Thread-5 waiting for write...
Thread-6 waiting for write...
Thread-0 reads data: AAAAAAAAAA
Thread-4 reads data: AAAAAAAAAA
Thread-3 reads data: AAAAAAAAAA
Thread-2 reads data: AAAAAAAAAA
Thread-1 reads data: AAAAAAAAAA
Thread-5 wrote data: CCCCCCCCCC
Thread-2 waiting for read...
Thread-1 waiting for read...
Thread-3 waiting for read...
Thread-0 waiting for read...
Thread-4 waiting for read...
Thread-6 wrote data: LLLLLLLLLL
Thread-5 waiting for write...
Thread-6 waiting for write...
Thread-2 reads data: LLLLLLLLLL
Thread-2 waiting for read...
（运行到此不动了）
</pre>
<p>注意到这种死锁是由于所有线程都在等待别的线程唤醒自己，结果都无法醒过来。这和两个线程希望获得对方已有的锁造成死锁不同。因此多线程设计的难度远远高于单线程应用。</p>
<p>从JDK 5开始，java.util.concurrent包就已经包含了ReadWriteLock，使用更简单，无需我们自行实现上述代码。但是，理解ReadWriteLock的原理仍非常重要。</p>]]></description></item>
<item><title>J2ME最佳实践</title><link>http://www.liaoxuefeng.com/it-63331a36a9c54d418fd094a3329198ab-1</link><dc:creator>廖雪峰</dc:creator><pubDate>Tue, 21 Jul 2009 16:29:25 +0800</pubDate><description><![CDATA[<h3 style="color: red">概述</h3>
<p>J2ME是Sun发布的运行在小型设备上的微型版Java的一系列标准，其中，最重要的标准便是运行在手机上的MIDP应用程序了。到目前为止，MIDP一共发布了两个版本：MIDP 1.0（JSR37）和MIDP 2.0（JSR118），2.0版本可以向后兼容1.0版本，也就是说，支持MIDP 2.0的手机可以同时运行MIDP 1.0和MIDP 2.0的应用程序。本文将重点讲述开发MIDP应用程序时非常有用的一些设计模式，开发技巧以及如何调试、优化J2ME应用程序。</p>
<p>本文将讨论J2ME开发的以下内容：</p>
<ul>
    <li>如何自动适应用户手机配置</li>
    <li>如何在屏幕间导航</li>
    <li>如何实现一个灵活的联网应用</li>
    <li>如何实现一个灵活的RMS应用</li>
    <li>如何调试并优化J2ME程序</li>
</ul>
<h3 style="color: red">避免OutOfMemoryError</h3>
<p>对于MIDP应用程序来说，由于手机设备上的资源非常有限，较弱的CPU计算能力，有限的内存（从几十KB到几百KB，虽然少数高端手机拥有超过1M的动态内存），很小的屏幕尺寸，因此，为了让一个MIDP应用程序能够不加改动地在多种不同手机上运行，程序必须有能力根据系统配置自动调整运行时的参数。比如，对于内存非常小的手机，如果从网络下载一幅较大的图像，需要分配巨大的缓冲区，就可能导致OutOfMemoryError错误，使应用程序直接终止，这会使用户感到不知所措，或者丢失用户的重要数据。因此，在试图分配一块大内存之前，首先使用System.gc()尝试让垃圾收集器释放无用对象占用的内存，然后，使用Runtime.getRuntime().freeMemory()方法获得可用的内存空间。如果可用空间太小，给用户一个&ldquo;内存不足，无法完成操作&rdquo;的Alert提示，从而尽可能地避免OutOfMemoryError错误。</p>
<pre class="brush:java">
// 示例代码：
System.gc();
int max_size = 102400; // 100KB
int free_size = (int)Runtime.getRuntime().freeMemory();
if(max_size&gt;free_size*2/3) {
    // TODO: Alert!
}
else {
    byte[] buffer = new byte[max_size];
    // TODO: Download image...
}
</pre>
<h3 style="color: red">减少图片以减小JAR文件大小</h3>
<p>许多手机会因为JAR文件太大而无法运行MIDP应用程序，而减小JAR文件尺寸的有效方法之一是减少不必要的图片，例如，启动时的LOGO图片可以用文字来代替，列表项可以只显示文字而不显示图片。为了能适应不同配置的手机，我们的代码就应该编写得更加灵活。例如，从JAR包中加载图片时：</p>
<pre class="brush:java">
Image image = null;
try {
    image = Image.createImage(&quot;/logo.png&quot;);
}
catch(Exception ioe) {}
if(image==null) {
    g.setColor(0);
    g.drawString(&quot;info&quot;, getWidth()/2, getHeight()/2, Graphics.HCENTER|Graphics.BASELINE);
}
else {
    g.drawImage(image, getWidth()/2, getHeight()/2, Graphics.HCENTER|Graphics.VCENTER);
}
</pre>
<p>如果加载失败，程序会以文字方式显示，这样，对于低配置的手机，只需要把美化界面的图片删除掉，再重新打包即可得到一个可发布的尺寸较小的JAR包，同时应用程序的代码并没有改动。</p>
<p>类似的，在加载List之类的UI组件时：</p>
<pre class="brush:java">
Image image = null;
try {
    image = Image.createImage(&quot;/logo.png&quot;);
}
catch(Exception ioe) {}
append(&quot;label&quot;, image);
</pre>
<p>这使得有无图片仅仅影响界面美观，而不影响应用程序的功能。</p>
<h3 style="color: red">获取设备支持的可选API</h3>
<p>J2ME规范包括了许多可选包，如支持多媒体功能的MMAPI，支持消息接收和发送的WMA，支持3D游戏的M3G API。如果某一款手机支持某个可选API，MIDP应用程序就可以使用它。但是，让用户回答&ldquo;本机是否支持MMAPI&rdquo;是不友好的，发布几个不同版本不但增加了开发的工作量，也让用户难以选择。因此，应用程序应该自己检测手机是否支持某一API，从而在运行期决定是否可以使用此API。</p>
<p>MIDP 1.0和2.0应用程序都可以通过System.getProperty(String key)检测某一个属性的信息。如果该属性有效，将返回对应的字符串，否则，返回null，表示系统不支持此功能。</p>
<p>例如，System.getProperty(&quot;microedition.profiles&quot;)可能的返回值是&quot;MIDP-1.0&quot;或&quot;MIDP-2.0&quot;。</p>
<p>以下是常见的系统属性和可选API的属性，右侧列出了可能的返回值：</p>
<table border="1" cellspacing="1" cellpadding="1" width="558" style="width: 558px; height: 463px">
    <tbody>
        <tr>
            <td><strong>系统信息</strong></td>
            <td>&nbsp;</td>
        </tr>
        <tr>
            <td>microedition.platform</td>
            <td>平台名称，如j2me</td>
        </tr>
        <tr>
            <td>microedition.configuration</td>
            <td>CLDC或CDC版本，如CLDC-1.0</td>
        </tr>
        <tr>
            <td>microedition.profiles</td>
            <td>MIDP版本，如MIDP-1.0</td>
        </tr>
        <tr>
            <td>microedition.encoding</td>
            <td>默认的系统编码，如GBK</td>
        </tr>
        <tr>
            <td>microedition.locale</td>
            <td>默认的区域设置，如zh-CN</td>
        </tr>
        <tr>
            <td><strong>MMAPI相关</strong></td>
            <td>&nbsp;</td>
        </tr>
        <tr>
            <td>microedition.media.version</td>
            <td>MMAPI的版本，如1.1</td>
        </tr>
        <tr>
            <td>supports.mixing</td>
            <td>是否支持混音，如true</td>
        </tr>
        <tr>
            <td>supports.audio.capture</td>
            <td>是否支持音频捕获，如true</td>
        </tr>
        <tr>
            <td>supports.video.capture</td>
            <td>是否支持视频捕获，如true</td>
        </tr>
        <tr>
            <td>supports.recording</td>
            <td>是否支持录音，如true</td>
        </tr>
        <tr>
            <td>audio.encodings</td>
            <td>音频编码格式，如encoding=pcm encoding=pcm&amp;rate=8000&amp;bits=8&amp;channels=1</td>
        </tr>
        <tr>
            <td>video.snapshot.encodings</td>
            <td>拍摄图片的编码格式，如encoding=jpeg encoding=png</td>
        </tr>
        <tr>
            <td>streamable.contents</td>
            <td>支持的流媒体格式，如audio/x-wav</td>
        </tr>
        <tr>
            <td><strong>WMA相关</strong></td>
            <td>&nbsp;</td>
        </tr>
        <tr>
            <td>wireless.messaging.sms.smsc</td>
            <td>返回SMS的服务中心，如+8613800010000</td>
        </tr>
        <tr>
            <td>wireless.messaging.mms.mmsc</td>
            <td>返回MMS的服务中心，如http://mmsc.monternet.com</td>
        </tr>
        <tr>
            <td><strong>其他</strong></td>
            <td>&nbsp;</td>
        </tr>
        <tr>
            <td>microedition.m3g.version</td>
            <td>返回Mobile 3D的版本，如1.0</td>
        </tr>
        <tr>
            <td>bluetooth.api.version</td>
            <td>返回蓝牙API的版本，如1.0</td>
        </tr>
        <tr>
            <td>microedition.io.file.FileConnection.version</td>
            <td>返回FileConnection的版本，如1.0</td>
        </tr>
        <tr>
            <td>microedition.pim.version</td>
            <td>返回PIM的版本，如1.0</td>
        </tr>
    </tbody>
</table>
<p>例如，如果用户的手机内置了数码相机，并且支持MMAPI，我们就可以在MIDP程序中拍摄照片。因此，在应用程序启动时就应该判断是否启用拍照功能以及用户手机支持的图片编码格式：</p>
<pre class="brush:java">
boolean supports_take_photo = false;
boolean supports_jpeg_encoding = false;
boolean supports_png_encoding = false;
boolean supports_gif_encoding = false;
if(System.getProperty(&quot;microedition.media.version&quot;)!=null) {
    if(&quot;true&quot;.equals(System.getProperty(&quot;supports.video.capture&quot;)))
        supports_take_photo = true;
        String all_encoding = System.getProperty(&quot;video.snapshot.encodings&quot;);
        if(all_encoding!=null) {
            if(all_encoding.indexOf(&quot;jpeg&quot;)!=(-1))
                supports_jpeg_encoding = true;
            if(all_encoding.indexOf(&quot;png&quot;)!=(-1))
                supports_png_encoding = true;
            if(all_encoding.indexOf(&quot;gif&quot;)!=(-1))
                supports_gif_encoding = true;
        }
    }
}
</pre>
<h3 style="color: red">屏幕导航</h3>
<p>除了游戏程序，在通常的MIDP应用程序中，通常会有很多个Screen或Canvas，这些屏幕一般靠命令来实现切换，比如用户点击&ldquo;Next&rdquo;应该跳到下一屏，点击&ldquo;Back&rdquo;应该返回到上一屏。当屏幕数量相当可观时，如何在各个屏幕之间导航就值得好好考虑了。</p>
<p>经典的MVC模式可用于屏幕导航，Model用于存储应用程序数据，而View则是各个Displayable对象，Controller需要单独的一个类实现。由于MIDlet类本身在生命周期内就只有一个实例，因此MIDlet类就非常适合作为Controller。SUN在blueprints示例程序SmartTicket中应用了非常复杂的MVC，完全可以满足MIDP应用程序的导航需要，但是可以看出，缺点是很明显的：</p>
<p>一是每一个事件都需要一个唯一标识，switch-case语句会随着屏幕的增加而增加，Controller变得难以维护。二是Controller引用了所有的View，这些View在程序启动时就被初始化导致很大的内存开销，而不管它们是否会被显示。三是大量的Model对象以及异常处理都使得整个应用程序的逻辑大大复杂。</p>
<p>实际上，MIDP应用程序的很多屏幕并不需要复杂的Controller和Model，我们的目标是满足基本的灵活性的同时保持结构简单。因此，另外两种导航方法是用二叉树和堆栈实现，这里我们只讨论用堆栈实现的MIDP导航框架，其基本思想是：每当前进到下一个屏幕时，先将下一个屏幕压栈，然后再显示；当返回到上一个屏幕时，先从堆栈中弹出当前屏幕，再从堆栈中取出上一个屏幕并显示。因此，每个屏幕只需要指定要显示的下一个屏幕，而不需记住上一个屏幕。这种堆栈导航模型特别适合有规律的&ldquo;前进&rdquo;、&ldquo;后退&rdquo;屏幕。</p>
<p>由于MIDlet类运行期只有一个实例，因此，使用MIDlet类作为控制器相当合适。此外，我们在一个静态变量中保存了MIDlet实例，使得访问MIDlet更加方便：</p>
<pre class="brush:java">
public class ControllerMIDlet extends MIDlet {
    private static ControllerMIDlet instance = null;

    private Display display = null;
    private Stack ui = new Stack();

    public ControllerMIDlet() { instance = this; }

    protected void startApp() {}
    protected void pauseApp() {}
    protected void destroyApp(boolean unconditional) {}

    public static void goBack() {
        instance.ui.pop();
        Object obj = instance.ui.peek();
        instance.display.setCurrent((Displayable)obj);
    }

    public static void forward(Displayable next) {
        instance.ui.push(next);
        instance.display.setCurrent(next);
    }
}
</pre>
<p>让我们更详细地研究一下实际的应用程序可能出现的几种屏幕跳转情况。最简单的情况是，从一个屏幕前进到另一个屏幕，且返回时仍回到原先的屏幕，这种情况完全符合堆栈的FIFO特点，可以直接调用ControllerMIDlet的forward和goBack方法即可。例如，要显示一个帮助屏幕：</p>
<p><img alt="" width="477" height="141" src="/upload/2/68/111/441c94060208475d83580d4a990b21f6.jpg" /></p>
<p>对于一个联网的应用程序，另一种情况是有一个暂时的等待屏幕。下面是一个在线浏览图片的屏幕：</p>
<p><img alt="" width="478" height="201" src="/upload/27/8/238/226b3eb3cdc24c7d9de03f845225cc3a.jpg" /></p>
<p>与上面的情况所不同的是，如果用户在屏幕3选择&ldquo;返回&rdquo;，则应当回到屏幕1而不是屏幕2，因此，对于屏幕2到屏幕3的切换，就不能forward，我们使用replace，抛弃屏幕2，从而实现屏幕3直接可以goBack到屏幕1：</p>
<pre class="brush:java">
public static void replace(Displayable next) {
    instance.ui.pop();
    instance.ui.push(next);
    instance.display.setCurrent(next);
}
</pre>
<p>堆栈的变化如下：</p>
<p><img alt="" width="322" height="94" src="/upload/207/145/113/0d6970fcfe704db7b01f4d121bb2ad5d.jpg" /></p>
<p>对于某些更为复杂的情况，例如，登录过程，如果允许用户选择自动登录，则屏幕跳转如下：</p>
<p><img alt="" width="521" height="220" src="/upload/109/71/231/a07f53fd124b4d85952a99c6c53acf5e.jpg" /></p>
<p>如果用户不选择自动登录，则屏幕跳转如下：</p>
<p><img alt="" width="546" height="163" src="/upload/149/18/158/af967917033941c2bfdfd45fd9bb7934.jpg" /></p>
<p>对于这种情况，解决方案是，即使用户选择了自动登录，LoginUI屏幕也要被压入堆栈中，但是不显示出来，因此，我们定义了另一个forward(Displayable d1, Displayable d2)方法，它将d1和d2依次压入堆栈，但只显示d2。在返回时，如果用户取消，则返回到LoginUI。总之，通过定义多个导航方法，就可以实现各种操作。</p>
<p>这种基于堆栈的导航模型非常适用于有规律的&ldquo;前进&rdquo;，&ldquo;后退&rdquo;屏幕，而且只在需要的时候生成新的屏幕。无需关心屏幕状态，因为返回时上一个屏幕的状态被完整地保存在堆栈中。</p>
<p>堆栈模型的缺点是数据由不同的屏幕处理，对于一些流程而言，可能需要将每个屏幕的数据依次传递给下一个屏幕，越往后的屏幕其构造方法的参数可能也越多。</p>
<p>对于联网操作等涉及到多线程等待屏幕的情况，我们将在后面给出一个完整的解决方案，并集成到堆栈导航框架中，使应用程序本身完全不用涉及到多线程联网操作，只需专注于自身逻辑。</p>
<h3 style="color: red">编写反应灵敏的联网提示界面</h3>
<p>由于无线设备所能支持的网络协议非常有限，仅限于HTTP，Socket，UDP等几种协议，不同的厂家可能还支持其他网络协议，但是，MIDP 1.0规范规定，HTTP协议是必须实现的协议，而其他协议的实现都是可选的。因此，为了能在不同类型的手机上移植，我们尽量采用HTTP作为网络连接的首选协议，这样还能重用服务器端的代码。但是，由于HTTP是一个基于文本的效率较低的协议，因此，必须仔细考虑手机和服务器端的通信内容，尽可能地提高效率。</p>
<p>对于MIDP应用程序，应当尽量做到：</p>
<ol>
    <li>发送请求时，附加一个User-Agent头，传入MIDP和自身版本号，以便服务器能识别此请求来自MIDP应用程序，并且根据版本号发送相应的相应。</li>
    <li>连接服务器时，显示一个下载进度条使用户能看到下载进度，并能随时中断连接。</li>
    <li>由于无线网络连接速度还很慢，因此有必要将某些数据缓存起来，可以存储在内存中，也可以放到RMS中。</li>
</ol>
<p>对于服务器端而言，其输出响应应当尽量做到：</p>
<ol>
    <li>明确设置Content-Length字段，以便MIDP应用程序能读取HTTP头并判断自身是否有能力处理此长度的数据，如果不能，可以直接关闭连接而不必继续读取HTTP正文。</li>
    <li>服务器不应当发送HTML内容，因为MIDP应用程序很难解析HTML，XML虽然能够解析，但是耗费CPU和内存资源，因此，应当发送紧凑的二进制内容，用DataOutputStream直接写入并设置Content-Type为application/octet-stream。</li>
    <li>尽量不要重定向URL，这样会导致MIDP应用程序再次连接服务器，增加了用户的等待时间和网络流量。</li>
    <li>如果发生异常，例如请求的资源未找到，或者身份验证失败，通常，服务器会向浏览器发送一个显示出错的页面，可能还包括一个用户登录的Form，但是，向MIDP发送错误页面毫无意义，应当直接发送一个404或401错误，这样MIDP应用程序就可以直接读取HTTP头的响应码获取错误信息而不必继续读取相应内容。</li>
    <li>由于服务器的计算能力远远超过手机客户端，因此，针对不同客户端版本发送不同响应的任务应该在服务器端完成。例如，根据客户端传送的User-Agent头确定客户端版本。这样，低版本的客户端不必升级也能继续使用。</li>
</ol>
<p>MIDP的联网框架定义了多种协议的网络连接，但是每个厂商都必须实现HTTP连接，在MIDP 2.0中还增加了必须实现的HTTPS连接。因此，要保证MIDP应用程序能在不同厂商的手机平台上移植，最好只使用HTTP连接。虽然HTTP是一个基于文本的效率较低的协议，但是由于使用特别广泛，大多数服务器应用的前端都是基于HTTP的Web页面，因此能最大限度地复用服务器端的代码。只要控制好缓存，仍然有不错的速度。</p>
<p>SUN的MIDP库提供了javax.microediton.io包，能非常容易地实现HTTP连接。但是要注意，由于网络有很大的延时，必须把联网操作放入一个单独的线程中，以避免主线程阻塞导致用户界面停止响应。事实上，MIDP运行环境根本就不允许在主线程中操作网络连接。因此，我们必须实现一个灵活的HTTP联网模块，能让用户非常直观地看到当前上传和下载的进度，并且能够随时取消连接。</p>
<p>一个完整的HTTP连接为：用户通过某个命令发起连接请求，然后系统给出一个等待屏幕提示正在连接，当连接正常结束后，前进到下一个屏幕并处理下载的数据。如果连接过程出现异常，将给用户提示并返回到前一个屏幕。用户在等待过程中能够随时取消并返回前一个屏幕。</p>
<p>我们设计一个HttpThread线程类负责在后台连接服务器，HttpListener接口实现Observer（观察者）模式，以便HttpThread能提示观察者下载开始、下载结束、更新进度条等。HttpListener接口如下：</p>
<pre class="brush:java">
public interface HttpListener {
    void onSetSize(int size);
    void onFinish(byte[] data, int size);
    void onProgress(int percent);
    void onError(int code, String message);
}
</pre>
<p>实现HttpListener接口的是继承自Form的一个HttpWaitUI屏幕，它显示一个进度条和一些提示信息，并允许用户随时中断连接：</p>
<pre class="brush:java">
public class HttpWaitUI extends Form implements CommandListener, HttpListener {
    private Gauge gauge;
    private Command cancel;
    private HttpThread downloader;
    private Displayable displayable;
    public HttpWaitUI(String url, Displayable displayable) {
        super(&quot;Connecting&quot;);
        this.gauge = new Gauge(&quot;Progress&quot;, false, 100, 0);
        this.cancel = new Command(&quot;Cancel&quot;, Command.CANCEL, 0);
        append(gauge);
        addCommand(cancel);
        setCommandListener(this);
        downloader = new HttpThread(url, this);
        downloader.start();
    }
    public void commandAction(Command c, Displayable d) {
        if(c==cancel) {
            downloader.cancel();
            ControllerMIDlet.goBack();
        }
    }
    public void onFinish(byte[] buffer, int size) { &hellip; }
    public void onError(int code, String message) { &hellip; }
    public void onProgress(int percent) { &hellip; }
    public void onSetSize(int size) { &hellip; }
}
</pre>
<p>HttpThread是负责处理Http连接的线程类，它接受一个URL和HttpListener：</p>
<pre class="brush:java">
class HttpThread extends Thread {
    private static final int MAX_LENGTH = 20 * 1024; // 20K
    private boolean cancel = false;
    private String url;
    private byte[] buffer = null;
    private HttpListener listener;
    public HttpThread(String url, HttpListener listener) {
        this.url = url;
        this.listener = listener;
    }
    public void cancel() { cancel = true; }
}
</pre>
<h3 style="color: red">使用GET获取内容</h3>
<p>我们先讨论最简单的GET请求。GET请求只需向服务器发送一个URL，然后取得服务器响应即可。在HttpThread的run()方法中实现如下：</p>
<pre class="brush:java">
public void run() {
    HttpConnection hc = null;
    InputStream input = null;
    try {
        hc = (HttpConnection)Connector.open(url);
        hc.setRequestMethod(HttpConnection.GET); // 默认即为GET
        hc.setRequestProperty(&quot;User-Agent&quot;, USER_AGENT);
        // get response code:
        int code = hc.getResponseCode();
        if(code!=HttpConnection.HTTP_OK) {
            listener.onError(code, hc.getResponseMessage());
            return;
        }
        // get size:
        int size = (int)hc.getLength(); // 返回响应大小，或者-1如果大小无法确定
        listener.onSetSize(size);
        // 开始读响应：
        input = hc.openInputStream();
        int percent = 0; // percentage
        int tmp_percent = 0;
        int index = 0; // buffer index
        int reads; // each byte
        if(size!=(-1))
            buffer = new byte[size]; // 响应大小已知，确定缓冲区大小
        else
            buffer = new byte[MAX_LENGTH]; // 响应大小未知，设定一个固定大小的缓冲区
        while(!cancel) {
            int len = buffer.length - index;
            len = len&gt;128 ? 128 : len;
            reads = input.read(buffer, index, len);
            if(reads&lt;=0)
                break;
            index += reads;
            if(size&gt;0) { // 更新进度
                tmp_percent = index * 100 / size;
                if(tmp_percent!=percent) {
                    percent = tmp_percent;
                    listener.onProgress(percent);
                }
            }
        }
        if(!cancel &amp;&amp; input.available()&gt;0) // 缓冲区已满，无法继续读取
            listener.onError(601, &quot;Buffer overflow.&quot;);
        if(!cancel) {
            if(size!=(-1) &amp;&amp; index!=size)
                listener.onError(102, &quot;Content-Length does not match.&quot;);
            else
                listener.onFinish(buffer, index);
        }
    }
    catch(IOException ioe) {
        listener.onError(101, &quot;IOException: &quot; + ioe.getMessage());
    }
    finally { // 清理资源
        if(input!=null)
            try { input.close(); } catch(IOException ioe) {}
        if(hc!=null)
            try { hc.close(); } catch(IOException ioe) {}
    }
}
</pre>
<p>当下载完毕后，HttpWaitUI就获得了来自服务器的数据，要传递给下一个屏幕处理，HttpWaitUI必须包含对此屏幕的引用并通过一个setData(DataInputStream input)方法让下一个屏幕能非常方便地读取数据。因此，定义一个DataHandler接口：</p>
<pre class="brush:java">
public interface DataHandler {
    void setData(DataInputStream input) throws IOException;
}
</pre>
<p>HttpWaitUI响应HttpThread的onFinish事件并调用下一个屏幕的setData方法将数据传递给它并显示下一个屏幕：</p>
<pre class="brush:java">
public void onFinish(byte[] buffer, int size) {
    byte[] data = buffer;
    if(size!=buffer.length) {
        data = new byte[size];
        System.arraycopy(data, 0, buffer, 0, size);
    }
    DataInputStream input = null;
    try {
        input = new DataInputStream(new ByteArrayInputStream(data));
        if(displayable instanceof DataHandler)
            ((DataHandler)displayable).setData(input);
        else
            System.err.println(&quot;[WARNING] Displayable object cannot handle data.&quot;);
        ControllerMIDlet.replace(displayable);
    }
    catch(IOException ioe) { &hellip; }
}
</pre>
<p>以下载一则新闻为例，一个完整的HTTP GET请求过程如下：</p>
<p>首先，用户通过点击某个屏幕的命令希望阅读指定的一则新闻，在commandAction事件中，我们初始化HttpWaitUI和显示数据的NewsUI屏幕：</p>
<pre class="brush:java">
public void commandAction(Command c, Displayable d) {
    HttpWaitUI wait = new HttpWaitUI(&quot;http://192.168.0.1/news.do?id=1&quot;, new NewsUI());
    ControllerMIDlet.forward(wait);
}
</pre>
<p>NewsUI实现DataHandler接口并负责显示下载的数据：</p>
<pre class="brush:java">
public class NewsUI extends Form implements DataHandler {
    public void setData(DataInputStream input) throws IOException {
        String title = input.readUTF();
        Date date = new Date(input.readLong());
        String text = input.readUTF();
        append(new StringItem(&quot;Title&quot;, title));
        append(new StringItem(&quot;Date&quot;, date.toString()));
        append(text);
    }
}
</pre>
<p>服务器端只要以String, long, String的顺序依次写入DataOutputStream，MIDP客户端就可以通过DataInputStream依次取得相应的数据，完全不需要解析XML之类的文本，非常高效而且方便。</p>
<p>需要获得联网数据的屏幕只需实现DataHandler接口，并向HttpWaitUI传入一个URL即可复用上述代码，无须关心如何连接网络以及如何处理用户中断连接。</p>
<h3 style="color: red">使用POST发送数据</h3>
<p>以POST方式发送数据主要是为了向服务器发送较大量的客户端的数据，它不受URL的长度限制。POST请求将数据以URL编码的形式放在HTTP正文中，字段形式为fieldname=value，用&amp;分隔每个字段。注意所有的字段都被作为字符串处理。实际上我们要做的就是模拟浏览器POST一个表单。以下是IE发送一个登陆表单的POST请求：</p>
<pre>
POST http://127.0.0.1/login.do HTTP/1.0
Accept: image/gif, image/jpeg, image/pjpeg, */*
Accept-Language: en-us,zh-cn;q=0.5
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)
Content-Length: 28
\r\n
username=admin&amp;password=1234
</pre>
<p>要在MIDP应用程序中模拟浏览器发送这个POST请求，首先设置HttpConnection的请求方式为POST：</p>
<pre class="brush:java">
hc.setRequestMethod(HttpConnection.POST);
</pre>
<p>然后构造出HTTP正文：</p>
<pre class="brush:java">
byte[] data = &quot;username=admin&amp;password=1234&quot;.getBytes();
</pre>
<p>并计算正文长度，填入Content-Type和Content-Length：</p>
<pre class="brush:java">
hc.setRequestProperty(&quot;Content-Type&quot;, &quot;application/x-www-form-urlencoded&quot;);
hc.setRequestProperty(&quot;Content-Length&quot;, String.valueOf(data.length));
</pre>
<p>然后打开OutputStream将正文写入：</p>
<pre class="brush:java">
OutputStream output = hc.openOutputStream();
output.write(data);
</pre>
<p>需要注意的是，数据仍需要以URL编码格式编码，由于MIDP库中没有J2SE中与之对应的URLEncoder类，因此，需要自己动手编写这个encode()方法，可以参考java.net.URLEncoder.java的源码。剩下的便是读取服务器响应，代码与GET一致，这里就不再详述。</p>
<h3 style="color: red">使用multipart/form-data发送文件</h3>
<p>如果要在MIDP客户端向服务器上传文件，我们就必须模拟一个POST multipart/form-data类型的请求，Content-Type必须是multipart/form-data。</p>
<p>以multipart/form-data编码的POST请求格式与application/x-www-form-urlencoded完全不同，multipart/form-data需要首先在HTTP请求头设置一个分隔符，例如ABCD：</p>
<pre class="brush:java">
hc.setRequestProperty(&quot;Content-Type&quot;, &quot;multipart/form-data; boundary=ABCD&quot;);
</pre>
<p>然后，将每个字段用&ldquo;--分隔符&rdquo;分隔，最后一个&ldquo;--分隔符--&rdquo;表示结束。例如，要上传一个title字段&quot;Today&quot;和一个文件C:\1.txt，HTTP正文如下：</p>
<pre>
--ABCD
Content-Disposition: form-data; name=&quot;title&quot;
\r\n
Today
--ABCD
Content-Disposition: form-data; name=&quot;1.txt&quot;; filename=&quot;C:\1.txt&quot;
Content-Type: text/plain
\r\n
&lt;这里是1.txt文件的内容&gt;
--ABCD--
\r\n
</pre>
<p>请注意，每一行都必须以\r\n结束，包括最后一行。如果用Sniffer程序检测IE发送的POST请求，可以发现IE的分隔符类似于---------------------------7d4a6d158c9，这是IE产生的一个随机数，目的是防止上传文件中出现分隔符导致服务器无法正确识别文件起始位置。我们可以写一个固定的分隔符，只要足够复杂即可。</p>
<p>发送文件的POST代码如下：</p>
<pre class="brush:java">
String[] props = ... // 字段名
String[] values = ... // 字段值
byte[] file = ... // 文件内容
String BOUNDARY = &quot;---------------------------7d4a6d158c9&quot;; // 分隔符
StringBuffer sb = new StringBuffer();
// 发送每个字段:
for(int i=0; i&lt;props.length; i++) {
    sb = sb.append(&quot;--&quot;);
    sb = sb.append(BOUNDARY);
    sb = sb.append(&quot;\r\n&quot;);
    sb = sb.append(&quot;Content-Disposition: form-data; name=\&quot;&quot;+ props[i] + &quot;\&quot;\r\n\r\n&quot;);
    sb = sb.append(URLEncoder.encode(values[i]));
    sb = sb.append(&quot;\r\n&quot;);
}
// 发送文件:
sb = sb.append(&quot;--&quot;);
sb = sb.append(BOUNDARY);
sb = sb.append(&quot;\r\n&quot;);
sb = sb.append(&quot;Content-Disposition: form-data; name=\&quot;1\&quot;; filename=\&quot;1.txt\&quot;\r\n&quot;);
sb = sb.append(&quot;Content-Type: application/octet-stream\r\n\r\n&quot;);
byte[] data = sb.toString().getBytes();
byte[] end_data = (&quot;\r\n--&quot; + BOUNDARY + &quot;--\r\n&quot;).getBytes();
// 设置HTTP头:
hc.setRequestProperty(&quot;Content-Type&quot;, MULTIPART_FORM_DATA + &quot;; boundary=&quot; + BOUNDARY);
hc.setRequestProperty(&quot;Content-Length&quot;, String.valueOf(data.length + file.length + end_data.length));
// 输出:
output = hc.openOutputStream();
output.write(data);
output.write(file);
output.write(end_data);
// 读取服务器响应：
// TODO...
</pre>
<h3 style="color: red">使用Cookie保持Session</h3>
<p>通常服务器使用Session来跟踪会话。Session的简单实现就是利用Cookie。当客户端第一次连接服务器时，服务器检测到客户端没有相应的Cookie字段，就发送一个包含一个识别码的Set-Cookie字段。在此后的会话过程中，客户端发送的请求都包含这个Cookie，因此服务器能够识别出客户端曾经连接过服务器。</p>
<p>要实现与浏览器一样的效果，MIDP应用程序必须也能识别Cookie，并在每个请求头中包含此Cookie。</p>
<p>在处理每次连接的响应中，我们都检查是否有Set-Cookie这个头，如果有，则是服务器第一次发送的Session ID，或者服务器认为会话超时，需要重新生成一个Session ID。如果检测到Set-Cookie头，就将其保存，并在随后的每次请求中附加它：</p>
<pre class="brush:java">
String session = null;
String cookie = hc.getHeaderField(&quot;Set-Cookie&quot;);
if(cookie!=null) {
    int n = cookie.indexOf(';');
    session = cookie.substring(0, n);
}
</pre>
<p>使用Sniffer程序可以捕获到不同的Web服务器发送的Session。WebLogic Server 7.0返回的Session如下：</p>
<p>Set-Cookie: JSESSIONID=CxP4FMwOJB06XCByBWfwZBQ0IfkroKO2W7FZpkLbmWsnERuN5u2L!-1200402410; path=/</p>
<p>而Resin 2.1返回的Session则是：</p>
<p>Set-Cookie: JSESSIONID= aTMCmwe9F5j9; path=/</p>
<p>运行ASP.Net的IIS返回的Session：</p>
<p>Set-Cookie: ASPSESSIONIDQATSASQB=GNGEEJIDMDFCMOOFLEAKDGGP; path=/</p>
<p>我们无须关心Session ID的内容，服务器自己会识别它。我们只需在随后的请求中附加上这个Session ID即可：</p>
<pre class="brush:java">
if(session!=null)
    hc.setRequestProperty(&quot;Cookie&quot;, session);
</pre>
<p>对于URL重写来保持Session的方法，在PC客户端可能很有用，但是，由于MIDP程序很难分析出URL中有用的Session信息，因此，不推荐使用这种方法。</p>
<h3 style="color: red">编写灵活的RMS应用</h3>
<p>MIDP应用程序的标准持久化方案就是使用RMS。RMS类似于一个小型数据库，RecordStore相当于数据库的表，每个&ldquo;表&rdquo;由若干记录（Record）构成，一条记录就是一个用int表示的记录号RecordID和用byte[]表示的内容。记录号可以看作是&ldquo;主键&rdquo;，byte[]数组存储内容。</p>
<p>RMS提供的记录操作可以实现根据ID直接获得记录，或者枚举出一个表中的所有记录。</p>
<p>枚举记录是非常低效的，因为只能比较byte[]数据来确定该记录是否是所需的记录。通过ID获得记录是高效而方便的，类似于SQL语句&ldquo;SELECT byteArrayData FROM recordStoreName WHERE RecordID=?&rdquo;。然而，通常应用程序很难知道某条记录的ID号，而RMS记录的&ldquo;主键&rdquo;又仅限于int类型，无法使用其他类型如String作为&ldquo;主键&rdquo;来查找。因此，对于需要存取不同类型对象的应用程序而言，就需要一个灵活的RMS操作框架。</p>
<p>我们的基本设想是，如果能使用String作为&ldquo;主键&rdquo;来查找记录，就能非常方便地获得所需的内容。例如，应用程序设置可以通过&quot;sys.settings&quot;获得byte[]数组，并依次读取出设置，用户登录信息可以通过&quot;user.info&quot;获得byte[]数组，再分解出用户名和口令。</p>
<p>因此，我们实现一个StorageHandler类，提供唯一的RMS访问接口，使得其他类完全不必考虑底层的RMS操作，只需提供能标识自身的一个String即可。</p>
<p>如果我们能实现一种类似于数据库索引的查找表，就能根据String关键字查找某条记录。因此，我们使用一个名为&quot;index&quot;的RecordStore来存储所有的索引，每一条索引都指向某一条具体记录的ID，设计一个IndexEntry表示一条索引：</p>
<pre class="brush:java">
class IndexEntry {
    private int selfId;   // IndexEntry的ID
    private int recordId; // 对应记录的ID
    private String key;   // 访问记录的Key
}
</pre>
<p>根据索引查找，分3步进行：</p>
<p>在名为&quot;index&quot;的RecordStore中根据String查找对应的IndexEntry。</p>
<p>取出IndexEntry，获得记录ID号。</p>
<p>根据ID号获得另一个RecordStore的记录，然后就可以读取、更新和删除该记录。</p>
<p>如下图所示：</p>
<p><img alt="" width="377" height="183" src="/upload/197/245/193/b472b31373914ca3a5afa5ca929b0241.jpg" /></p>
<p>由于IndexEntry保存的数据很少，为了加快查找速度，可以在应用程序启动时，把所有的IndexEntry读入一个Vector，在后面的操作中更新这个Vector并与RecordStore保持同步。</p>
<p>为了处理不同类型的数据，所有可通过StorageHandler存取的类都必须实现一个Storable接口：</p>
<pre class="brush:java">
public interface Storable {
    String getKey();
    void getData(DataOutputStream output) throws IOException;
    void setData(DataInputStream input) throws IOException;
}
</pre>
<p>前面已经提到，在MIDP应用程序中，序列化一个类的最佳方法是使用DataInputStream和DataOutputStream。因此，需要持久化的类可以通过getData()和setData()方法非常方便地存取。假定应用程序的类UserInfo保存了用户的登录名、口令和是否自动登录的信息：</p>
<pre class="brush:java">
public class UserInfo {
    String username;
    String password;
    boolean autoLogin;
}
</pre>
<p>为了能将UserInfo存入RMS，需要实现Storable接口：</p>
<pre class="brush:java">
class UserInfo implements Storable {
    String username;
    String password;
    boolean autoLogin;
    public String getKey() { return &quot;user.info&quot;; } // 提供一个唯一标识符即可
    public void getData(DataOutputStream output) throws IOException {
        output.writeUTF(username);
        output.writeUTF(password);
        output.writeBoolean(autoLogin);
    }
    public void setData(DataInputStream input) throws IOException {
        username = input.readUTF();
        password = input.readUTF();
        autoLogin = input.readBoolean();
    }
    // getters here...
}
</pre>
<p>要保存UserInfo，只需调用StorageHandler的保存方法：</p>
<pre class="brush:java">
StorageHandler.storeOrUpdate(userinfo);
</pre>
<p>要读取UserInfo，调用StorageHandler的读取方法：</p>
<pre class="brush:java">
UserInfo userinfo = new UserInfo();
StorageHandler.load(userinfo);
</pre>
<p>这样，需要读取或保存数据的类完全不必涉及底层的RMS操作，大大简化了应用程序的设计，增强了源代码的可复用性与可维护性。</p>]]></description></item>
<item><title>如何用Eclipse在Resin中调试Web应用程序</title><link>http://www.liaoxuefeng.com/it-c84318802792429ba6a6d03f0683b4c0-1</link><dc:creator>廖雪峰</dc:creator><pubDate>Mon, 20 Jul 2009 09:59:41 +0800</pubDate><description><![CDATA[<p>本文介绍如何在Resin中调试Web应用程序。测试环境为Windows&nbsp;7 / Resin 3.2 / Eclipse 3.3</p>
<p>在Resin的resin.conf中找到&lt;server-default&gt;并添加加以下参数：</p>
<pre class="brush:xml">
&lt;resin xmlns=&quot;http://caucho.com/ns/resin&quot;
       xmlns:resin=&quot;http://caucho.com/ns/resin/core&quot;&gt;
    &lt;log name=&quot;&quot; level=&quot;info&quot; path=&quot;stdout:&quot;/&gt;
    &lt;cluster id=&quot;&quot;&gt;
        &lt;root-directory&gt;.&lt;/root-directory&gt;
        &lt;server-default&gt;
            &lt;http server-id=&quot;&quot; host=&quot;*&quot; port=&quot;80&quot;/&gt;
            &lt;jvm-arg&gt;-Xmx128m&lt;/jvm-arg&gt;
            &lt;jvm-arg&gt;-Xss1m&lt;/jvm-arg&gt;
            &lt;jvm-arg&gt;-Xdebug&lt;/jvm-arg&gt;
            &lt;jvm-arg&gt;-Xnoagent&lt;/jvm-arg&gt;
            &lt;jvm-arg&gt;-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=12345&lt;/jvm-arg&gt;
</pre>
<p>启动Resin后，打开Eclipse项目，选择 Run -&gt; Debug... -&gt; Remote Java Application -&gt; New</p>
<p>新建一个Remote Java Application，填入Host: 127.0.0.1, Port: <span style="color: #ff0000">12345</span>, 注意这个Port就是Resin启动的address参数。</p>
<p>现在，就可以利用Eclipse强大而方便的调试界面对Web App断点调试并跟踪了！</p>]]></description></item>
<item><title>在当前ClassPath中搜索类</title><link>http://www.liaoxuefeng.com/it-4a2970243aec4e61bba4ad0ac1c1b7be-1</link><dc:creator>廖雪峰</dc:creator><pubDate>Fri, 17 Jul 2009 10:14:37 +0800</pubDate><description><![CDATA[<p>Spring 2.5提供了自动在当前ClassPath搜索被标注有特定注解的类，这个特性非常有用，跟踪了一下源码，发现其实核心代码就是利用ClassLoader的方法：</p>
<pre class="brush:java">
public Enumeration&lt;URL&gt; getResources(String name)
</pre>
<p>于是自己动手，也写了一个能在ClassPath下搜索特定类的Scanner：</p>
<pre class="brush:java">
package com.liaoxuefeng.util;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

public class ClassPathScanner {

    private static final String PROTOCOL_FILE = &quot;file&quot;;
    private static final String PROTOCOL_JAR = &quot;jar&quot;;

    private static final String PREFIX_FILE = &quot;file:&quot;;

    private static final String JAR_URL_SEPERATOR = &quot;!/&quot;;
    private static final String CLASS_FILE = &quot;.class&quot;;

    private final String packageName;
    private final ClassFilter filter;

    public ClassPathScanner(String packageName) {
        this(packageName, null);
    }

    public ClassPathScanner(String packageName, ClassFilter filter) {
        this.packageName = packageName;
        this.filter = filter;
    }

    public List&lt;Class&lt;?&gt;&gt; scan() {
        List&lt;Class&lt;?&gt;&gt; list = new ArrayList&lt;Class&lt;?&gt;&gt;();
        Enumeration&lt;URL&gt; en = null;
        try {
            en = getClass().getClassLoader().getResources(dotToPath(packageName));
        }
        catch(IOException e) {
            e.printStackTrace();
        }
        while (en.hasMoreElements()) {
            URL url = en.nextElement();
            if (PROTOCOL_FILE.equals(url.getProtocol())) {
                File root = new File(url.getFile());
                findInDirectory(list, root, root, packageName);
            }
            else if (PROTOCOL_JAR.equals(url.getProtocol())) {
                findInJar(list, getJarFile(url), packageName);
            }
        }
        return list;
    }

    public File getJarFile(URL url) {
        String file = url.getFile();
        if (file.startsWith(PREFIX_FILE))
            file = file.substring(PREFIX_FILE.length());
        int end = file.indexOf(JAR_URL_SEPERATOR);
        if (end!=(-1))
            file = file.substring(0, end);
        return new File(file);
    }

    void findInJar(List&lt;Class&lt;?&gt;&gt; results, File file, String packageName) {
        JarFile jarFile = null;
        String packagePath = dotToPath(packageName) + &quot;/&quot;;
        try {
            jarFile = new JarFile(file);
            Enumeration&lt;JarEntry&gt; en = jarFile.entries();
            while (en.hasMoreElements()) {
                JarEntry je = en.nextElement();
                String name = je.getName();
                if (name.startsWith(packagePath) &amp;&amp; name.endsWith(CLASS_FILE)) {
                    String className = name.substring(0, name.length() - CLASS_FILE.length());
                    add(results, pathToDot(className));
                }
            }
        }
        catch(IOException e) {
            e.printStackTrace();
        }
        finally {
            if (jarFile!=null) {
                try {
                    jarFile.close();
                }
                catch(IOException e) {}
            }
        }
    }

    void findInDirectory(List&lt;Class&lt;?&gt;&gt; results, File rootDir, File dir, String packageName) {
        File[] files = dir.listFiles();
        String rootPath = rootDir.getPath();
        for (File file : files) {
            if (file.isFile()) {
                String classFileName = file.getPath();
                if (classFileName.endsWith(CLASS_FILE)) {
                    String className = classFileName.substring(rootPath.length() - packageName.length(), classFileName.length() - CLASS_FILE.length());
                    add(results, pathToDot(className));
                }
            }
            else if (file.isDirectory()) {
                findInDirectory(results, rootDir, file, packageName);
            }
        }
    }

    void add(List&lt;Class&lt;?&gt;&gt; results, String className) {
        Class&lt;?&gt; clazz = null;
        try {
            clazz = Class.forName(className);
        }
        catch(ClassNotFoundException e) {
            return;
        }
        if (filter==null || filter.accept(clazz))
            results.add(clazz);
    }

    String dotToPath(String s) {
        return s.replace('.', '/');
    }

    String pathToDot(String s) {
        return s.replace('/', '.').replace('\\', '.');
    }
}
</pre>
<p>ClassFilter用来过滤是否要返回这一个类：</p>
<pre class="brush:java">
public interface ClassFilter {
    boolean accept(Class&lt;?&gt; clazz);
}
</pre>
<p>一个完整的示例：</p>
<pre class="brush:java">
public static void main(String[] args) {
    List&lt;Class&lt;?&gt;&gt; list = new ClassPathScanner(
            &quot;org.springframework.core.io&quot;,
            new ClassFilter() {
                 public boolean accept(Class&lt;?&gt; clazz) {
                    return clazz.isInterface(); // 返回接口类
                }
            }
    ).scan();
    for (Class&lt;?&gt; clazz : list) {
        System.out.println(clazz);
    }
}
</pre>]]></description></item>
<item><title>Spring集成Velocity的中文解决方案</title><link>http://www.liaoxuefeng.com/it-14a6ed76865242bf84a751c85c41bbf9-1</link><dc:creator>廖雪峰</dc:creator><pubDate>Thu, 16 Jul 2009 12:38:30 +0800</pubDate><description><![CDATA[<p>在Spring framework中使用Velocity是非常方便的，只需在spring配置文件中申明：</p>
<pre class="brush:xml">
&lt;bean id=&quot;viewResolver&quot; class=&quot;org.springframework.web.servlet.view.velocity.VelocityViewResolver&quot;&gt;
&lt;/bean&gt;
</pre>
<p>即可在spring mvc框架中直接返回new ModelAndView(&quot;velocity模板&quot;, map)，但是中文一直为乱码。</p>
<p>为了解决中文问题，首先，考虑到国际化，将所有web页面都用UTF-8编码，然后在/WEB-INF/velocity.properties文件中覆盖velocity的默认编码ISO-8859-1：</p>
<pre>
input.encoding = UTF-8
output.encoding = UTF-8
</pre>
<p>最后，在spring配置文件中设置：</p>
<pre class="brush:xml">
&lt;bean id=&quot;viewResolver&quot; class=&quot;org.springframework.web.servlet.view.velocity.VelocityViewResolver&quot;&gt;
    &lt;property name=&quot;contentType&quot;&gt;&lt;value&gt;text/html;charset=UTF-8&lt;/value&gt;&lt;/property&gt;
&lt;/bean&gt;
</pre>
<p>启动Web服务器，可以看到中文显示正常，输入也正常。你也可以使用GBK，但是不利于多语言移植。</p>
<h3 style="color: red">附：Velocity简介</h3>
<p>Velocity是apache的一个开放源代码项目，它实现了可替代JSP的View层，并且以很直观的方式来编写View。编写一个Velocity View就和编写一个纯HTML文件没有什么区别，完全可以在Dreamwaver中可视化编写，只需将数据部分用$xxx替换即可。</p>
<p>例如，要显示一个用户信息，Model传入的是一个Map，包含&quot;username&quot;，&quot;email&quot;和&quot;address&quot;三个Key：</p>
<pre class="brush:html">
&lt;html&gt;
&lt;head&gt;
    &lt;title&gt;User: $username&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;p&gt;Email: $email&lt;/p&gt;
    &lt;p&gt;Address: $address&lt;/p&gt;
&lt;/body&gt;
&lt;/html&gt;
</pre>
<p>这样你就完全不必担心嵌套的JSP标签在Dreamwaver中造成的语法错误。</p>]]></description></item>
<item><title>在Debian Etch上安装Windows</title><link>http://www.liaoxuefeng.com/it-f8aa96ef5cbf466ca8a0917d1a063427-1</link><dc:creator>廖雪峰</dc:creator><pubDate>Thu, 16 Jul 2009 10:12:27 +0800</pubDate><description><![CDATA[<p>上午通过3个小时奋战，终于成功在Debian Etch上通过xen成功安装Windows XP！</p>
<p>这是安装界面：</p>
<p><a href="/upload/219/92/11/3cc715e1067d4f79a0ae377387b9d5b8.gif" target="_blank"><img title="点击查看原图" alt="" width="75%" src="/upload/219/92/11/3cc715e1067d4f79a0ae377387b9d5b8.gif" /></a></p>
<p>安装成功后进入Windows的界面：</p>
<p><a href="/upload/96/139/92/0e89f9fbd07247d9a011a7518deb103b.gif" target="_blank"><img title="点击查看原图" alt="" width="75%" src="/upload/96/139/92/0e89f9fbd07247d9a011a7518deb103b.gif" /></a></p>
<p>要在Linux上按装Windows，除了VMWare这种通过软件全虚拟的方式，还可以通过xen实现硬件支持的虚拟。现在，各主要发行版都内置了xen，原本打算在Redhat Enterprise 5上试试，不过配置太麻烦，最好是通过安装包一次搞定，最终决定用Debian Etch，几条apt-get就搞定了，非常方便。</p>
<p>安装Windows的必须条件：</p>
<p>CPU必须支持Intel VT或AMD PT虚拟化技术，没有的话就不用考虑了。要检查CPU是否支持，用命令：</p>
<pre class="brush:bash">
grep vmx /proc/cpuinfo
</pre>
<p>如果是AMD的CPU用：</p>
<pre class="brush:bash">
grep svm /proc/cpuinfo
</pre>
<p>我的硬件配置：Intel Core2 Duo T7200 2GHz，2G RAM</p>
<p>先安装好Debian Etch和Gnome桌面，第一步是安装xen支持的内核，注意版本要和当前Linux内核一致。用apt-get安装：</p>
<ul>
    <li>xen-linux-system-2.6.18-5-xen-686</li>
    <li>xen-tools</li>
    <li>libc6-xen</li>
    <li>xen-ioemu-3.0.3-1</li>
    <li>xen-hypervisor-3.0.3-1-i386-pae</li>
    <li>bridge-utils</li>
</ul>
<p>安装完毕后重启系统，在GRUB就可以看到带xen的内核，启动后发现无线网卡不工作，需要再安装一个ipw3945-modules-2.6.18-5-686，重启后网卡工作正常。</p>
<p>第二步是安装准备，先创建一个4G的文件作为Windows的虚拟硬盘：</p>
<pre class="brush:bash">
dd if=/dev/zero of=/home/xuefeng/xen/winxp/winxp.img bs=1M count=4096
</pre>
<p>准备好Windows XP的ISO文件，我放在/home/xuefeng/xen/winxp/winxp.iso。</p>
<p>编写配置文件/home/xuefeng/xen/winxp/winxp.cfg，以下是我的配置文件：</p>
<pre class="brush:bash">
name='winxp'
kernel='/usr/lib/xen-3.0.3-1/boot/hvmloader'
device_model='/usr/lib/xen-3.0.3-1/bin/qemu-dm'
builder='hvm'
# 内存大小：
memory=1024
pae=1
# 配置一个硬盘和一个光盘：
disk=['file:/home/xuefeng/xen/winxp/winxp.img,ioemu:hda,w', 'file:/home/xuefeng/xen/winxp/winxp.iso,hdc:cdrom,r']
# 网络启动失败，暂时注释掉：
#vif=['type=ioemu,bridge=xenbr0']
# 先设置从d启动，等安装结束后改为c就可以直接从硬盘启动：
boot='d'
vcpus=2
# 设置VNC：
vnc=1
vnclisten='127.0.0.1'
vncviewer=1
on_poweroff='destroy'
on_reboot='restart'
</pre>
<p>然后通过以下命令启动虚拟机：</p>
<pre class="brush:bash">
sudo xm create /home/xuefeng/xen/winxp/winxp.cfg
</pre>
<p>启动完毕后，可以通过xm list查看当前所有的虚拟机。最后一步是通过VNC客户端连接到winxp的虚拟机，如果没有先安装xvncviewer，安装完毕后启动：</p>
<pre class="brush:bash">
xvncviewer 127.0.0.1
</pre>
<p>即可看见安装界面。</p>]]></description></item>
<item><title>删除Linux后如何恢复XP启动</title><link>http://www.liaoxuefeng.com/it-b7ac3cebe9dc480f92b18a6e6cceafb6-1</link><dc:creator>廖雪峰</dc:creator><pubDate>Thu, 16 Jul 2009 09:52:44 +0800</pubDate><description><![CDATA[<p>安装Linux时，如果将GRUB安装在主引导扇区，则可以正常引导Linux和Windows XP，但同时也破坏了原Windows的主引导信息。当删除Linux后，GRUB无法正常引导，因为GRUB需要读取Linux的/boot信息，但已不存在。此时，可以通过Windows XP的安装光盘恢复。</p>
<p>用XP安装盘启动后，选择R进入修复模式，输入管理员口令后，可以使用命令<span style="color: #ff0000"><strong>fixmbr</strong></span>修复主引导扇区的信息，然后重启，自动进入到Windows XP的启动菜单。</p>
<p>此法同样可用于Windows 2000 / 2003 / Vista等。</p>]]></description></item>
<item><title>JSON入门指南</title><link>http://www.liaoxuefeng.com/it-b5121593edd34710878cbc90713b91f0-1</link><dc:creator>廖雪峰</dc:creator><pubDate>Wed, 15 Jul 2009 13:46:42 +0800</pubDate><description><![CDATA[<p><span style="color: #ff6600">本文最早发表于IBM developerWorks：</span></p>
<p><a target="_blank" href="http://www.ibm.com/developerworks/cn/web/wa-lo-json/">http://www.ibm.com/developerworks/cn/web/wa-lo-json/</a></p>
<p>JSON即JavaScript Object Notation，它是一种轻量级的数据交换格式，非常适合服务器与JavaScript的交互。</p>
<p>尽管有许多宣传关于XML如何拥有跨平台，跨语言的优势，然而，除非应用于Web Services，否则，在普通的Web应用中，开发者经常为XML的解析伤透了脑筋，无论是服务器端生成或处理XML，还是客户端用JavaScript解析XML，都常常导致复杂的代码，极低的开发效率。实际上，对于大多数Web应用来说，他们根本不需要复杂的XML来传输数据，XML的扩展性很少具有优势，许多AJAX应用甚至直接返回HTML片段来构建动态Web页面。和返回XML并解析它相比，返回HTML片段大大降低了系统的复杂性，但同时缺少了一定的灵活性。</p>
<p>现在，JSON为Web应用开发者提供了另一种数据交换格式。让我们来看看JSON到底是什么，同XML或HTML片段相比，JSON提供了更好的简单性和灵活性。</p>
<h3 style="color: red">JSON数据格式解析</h3>
<p>和XML一样，JSON也是基于纯文本的数据格式。由于JSON天生是为JavaScript准备的，因此，JSON的数据格式非常简单，你可以用JSON传输一个简单的String，Number，Boolean，也可以传输一个数组，或者一个复杂的Object对象。</p>
<p>String，Number和Boolean用JSON表示非常简单。例如，用JSON表示一个简单的String&ldquo;abc&rdquo;，其格式为：</p>
<pre class="brush:javascript">
&quot;abc&quot;
</pre>
<p>除了字符&quot;，\，/和一些控制符（\b，\f，\n，\r，\t）需要编码外，其他Unicode字符可以直接输出。下图是一个String的完整表示结构：</p>
<p><img alt="" width="598" height="413" src="/upload/132/66/243/5cb90b10df7d4524b2c8d79da3d81ca1.gif" /></p>
<p>一个Number可以根据整型或浮点数表示如下：</p>
<p><img alt="" width="598" height="266" src="/upload/92/78/239/d676e796f278435bb6483dab65015f50.gif" /></p>
<p>这与绝大多数编程语言的表示方法一致，例如：</p>
<pre>
12345（整数）
-3.9e10（浮点数）
</pre>
<p>Boolean类型表示为true或false。此外，JavaScript中的null被表示为null，注意，true、false和null都没有双引号，否则将被视为一个String。</p>
<p>JSON还可以表示一个数组对象，使用[]包含所有元素，每个元素用逗号分隔，元素可以是任意的Value，例如，以下数组包含了一个String，Number，Boolean和一个null：</p>
<pre class="brush:javascript">
[&quot;abc&quot;,12345,false,null]
</pre>
<p>Object对象在JSON中是用{}包含一系列无序的Key-Value键值对表示的，实际上此处的Object相当于Java中的Map&lt;String, Object&gt;，而不是Java的Class。注意Key只能用String表示。</p>
<p>例如，一个Address对象包含如下Key-Value：</p>
<pre>
city:Beijing
street:Chaoyang Road
postcode:100025（整数）
</pre>
<p>用JSON表示如下：</p>
<pre class="brush:javascript">
{
    &quot;city&quot; : &quot;Beijing&quot;,
    &quot;street&quot; : &quot;Chaoyang Road&quot;,
    &quot;postcode&quot; : 100025
}
</pre>
<p>其中Value也可以是另一个Object或者数组，因此，复杂的Object可以嵌套表示，例如，一个Person对象包含name和address对象，可以表示如下：</p>
<pre class="brush:javascript">
{
    &quot;name&quot; : &quot;Michael&quot;,
    &quot;address&quot; : {
        &quot;city&quot; : &quot;Beijing&quot;,
        &quot;street&quot; : &quot;Chaoyang Road&quot;,
        &quot;postcode&quot; : 100025
    }
}
</pre>
<h3 style="color: red">用JavaScript处理JSON数据</h3>
<p>上面介绍了如何用JSON表示数据，接下来，我们还要解决如何在服务器端生成JSON格式的数据以便发送到客户端，以及客户端如何使用JavaScript处理JSON格式的数据。</p>
<p>我们先讨论如何在Web页面中用JavaScript处理JSON数据。我们通过一个简单的JavaScript方法就能看到客户端如何将JSON数据表示给用户：</p>
<pre class="brush:javascript">
function handleJson() {
    var j={
        &quot;name&quot; : &quot;Michael&quot;,
        &quot;address&quot; : {
            &quot;city&quot; : &quot;Beijing&quot;,
            &quot;street&quot; : &quot;Chaoyang Road&quot;,
            &quot;postcode&quot; : 100025
        }
    };
    document.write(j.name);
    document.write(j.address.city);
}
</pre>
<p>假定服务器返回的JSON数据是上文的：</p>
<pre class="brush:javascript">
{
    &quot;name&quot; : &quot;Michael&quot;,
    &quot;address&quot; : {
        &quot;city&quot; : &quot;Beijing&quot;,
        &quot;street&quot; : &quot;Chaoyang Road&quot;,
        &quot;postcode&quot; : 100025
    }
}
</pre>
<p>只需将其赋值给一个JavaScript变量，就可以立刻使用该变量并更新页面中的信息了，相比XML需要从DOM中读取各种节点而言，JSON的使用非常容易。我们需要做的仅仅是发送一个AJAX请求，然后将服务器返回的JSON数据赋值给一个变量即可。有许多AJAX框架早已包含了处理JSON数据的能力，例如Prototype（一个流行的JavaScript库：http://prototypejs.org）提供了evalJSON()方法，能直接将服务器返回的JSON文本变成一个JavaScript变量：</p>
<pre class="brush:javascript">
new Ajax.Request(&quot;http://url&quot;, {
    method: &quot;get&quot;,
    onSuccess: function(transport) {
        var json = transport.responseText.evalJSON();
        // TODO: document.write(json.xxx);
    }
});
</pre>
<h3 style="color: red">服务器端输出JSON格式数据</h3>
<p>下面我们讨论如何在服务器端输出JSON格式的数据。以Java为例，我们将演示将一个Java对象编码为JSON格式的文本。</p>
<p>将String对象编码为JSON格式时，只需处理好特殊字符即可。另外，必须用(&quot;)而非(')表示字符串：</p>
<pre class="brush:java">
static String string2Json(String s) {
    StringBuilder sb = new StringBuilder(s.length()+20);
    sb.append('\&quot;');
    for (int i=0; i&lt;s.length(); i++) {
        char c = s.charAt(i);
        switch (c) {
        case '\&quot;':
            sb.append(&quot;\\\&quot;&quot;);
            break;
        case '\\':
            sb.append(&quot;\\\\&quot;);
            break;
        case '/':
            sb.append(&quot;\\/&quot;);
            break;
        case '\b':
            sb.append(&quot;\\b&quot;);
            break;
        case '\f':
            sb.append(&quot;\\f&quot;);
            break;
        case '\n':
            sb.append(&quot;\\n&quot;);
            break;
        case '\r':
            sb.append(&quot;\\r&quot;);
            break;
        case '\t':
            sb.append(&quot;\\t&quot;);
            break;
        default:
            sb.append(c);
        }
    }
    sb.append('\&quot;');
    return sb.toString();
}
</pre>
<p>将Number表示为JSON就容易得多，利用Java的多态，我们可以处理Integer，Long，Float等多种Number格式：</p>
<pre class="brush:java">
static String number2Json(Number number) {
    return number.toString();
}
</pre>
<p>Boolean类型也可以直接通过toString()方法得到JSON的表示：</p>
<pre class="brush:java">
static String boolean2Json(Boolean bool) {
    return bool.toString();
}
</pre>
<p>要将数组编码为JSON格式，可以通过循环将每一个元素编码出来：</p>
<pre class="brush:java">
static String array2Json(Object[] array) {
    if (array.length==0)
        return &quot;[]&quot;;
    StringBuilder sb = new StringBuilder(array.length &lt;&lt; 4);
    sb.append('[');
    for (Object o : array) {
        sb.append(toJson(o));
        sb.append(',');
    }
    // 将最后添加的','变为']':
    sb.setCharAt(sb.length()-1, ']');
    return sb.toString();
}
</pre>
<p>最后，我们需要将Map&lt;String, Object&gt;编码为JSON格式，因为JavaScript的Object实际上对应的是Java的Map&lt;String, Object&gt;。该方法如下：</p>
<pre class="brush:java">
static String map2Json(Map&lt;String, Object&gt; map) {
    if (map.isEmpty())
        return &quot;{}&quot;;
    StringBuilder sb = new StringBuilder(map.size() &lt;&lt; 4);
    sb.append('{');
    Set&lt;String&gt; keys = map.keySet();
    for (String key : keys) {
        Object value = map.get(key);
        sb.append('\&quot;');
        sb.append(key);
        sb.append('\&quot;');
        sb.append(':');
        sb.append(toJson(value));
        sb.append(',');
    }
    // 将最后的','变为'}':
    sb.setCharAt(sb.length()-1, '}');
    return sb.toString();
}
</pre>
<p>为了统一处理任意的Java对象，我们编写一个入口方法toJson(Object)，能够将任意的Java对象编码为JSON格式：</p>
<pre class="brush:java">
public static String toJson(Object o) {
    if (o==null)
        return &quot;null&quot;;
    if (o instanceof String)
        return string2Json((String)o);
    if (o instanceof Boolean)
        return boolean2Json((Boolean)o);
    if (o instanceof Number)
        return number2Json((Number)o);
    if (o instanceof Map)
        return map2Json((Map&lt;String, Object&gt;)o);
    if (o instanceof Object[])
        return array2Json((Object[])o);
    throw new RuntimeException(&quot;Unsupported type: &quot; + o.getClass().getName());
}
</pre>
<p>我们并未对Java对象作严格的检查。不被支持的对象（例如List）将直接抛出RuntimeException。此外，为了保证输出的JSON是有效的，Map&lt;String, Object&gt;对象的Key也不能包含特殊字符。细心的读者可能还会发现循环引用的对象会引发无限递归，例如，精心构造一个循环引用的Map，就可以检测到StackOverflowException：</p>
<pre class="brush:java">
@Test(expected=StackOverflowError.class)
public void testRecurrsiveMap2Json() {
    Map&lt;String, Object&gt; map = new HashMap&lt;String, Object&gt;();
    map.put(&quot;key&quot;, map);
    JsonUtil.map2Json(map);
}
</pre>
<p>好在服务器处理的JSON数据最终都应该转化为简单的JavaScript对象，因此，递归引用的可能性很小。</p>
<p>最后，通过Servlet或MVC框架输出JSON时，需要设置正确的MIME类型（application/json）和字符编码。假定服务器使用UTF-8编码，则可以使用以下代码输出编码后的JSON文本：</p>
<pre class="brush:java">
response.setContentType(&quot;application/json;charset=UTF-8&quot;);
response.setCharacterEncoding(&quot;UTF-8&quot;);
PrintWriter pw = response.getWriter();
pw.write(JsonUtil.toJson(obj));
pw.flush();
</pre>
<p>JSON已经是JavaScript标准的一部分。目前，主流的浏览器对JSON支持都非常完善。应用JSON，我们可以从XML的解析中摆脱出来，对那些应用AJAX的Web 2.0网站来说，JSON确实是目前最灵活的轻量级方案。</p>
<p>本文完整的源代码可以从以下地址下载：</p>
<p><a target="_blank" href="http://javaeedev.googlecode.com/files/jeedev-util.zip">http://javaeedev.googlecode.com/files/jeedev-util.zip</a></p>
<h3 style="color: red">参考</h3>
<p>Introducing JSON: <a target="_blank" href="http://json.org/">http://json.org/</a></p>]]></description></item>
<item><title>将ReadWriteLock应用于缓存设计</title><link>http://www.liaoxuefeng.com/it-85a28556f3cd49d097ecfbb059a705b8-1</link><dc:creator>廖雪峰</dc:creator><pubDate>Wed, 15 Jul 2009 13:20:04 +0800</pubDate><description><![CDATA[<p><span style="color: #ff6600">本文最早发表于BEA dev2dev</span></p>
<p>&mdash;&mdash;针对缓慢变化的小数据的缓存实现模型</p>
<p>在JavaEEdev站点（<a target="_blank" href="http://www.javaeedev.com">http://www.javaeedev.com</a>）的设计中，有几类数据是极少变化的，如ArticleCategory（文档分类），ResourceCategory（资源分类），Board（论坛版面）。在对应的DAO实现中，总是一次性取出所有的数据，例如：</p>
<pre class="brush:java">
List&lt;ArticleCategory&gt; getArticleCategories();
</pre>
<p>此类数据的特点是：数据量很小，读取非常频繁，变化却极慢（几天甚至几十天才变化一次），如果每次通过DAO从数据库获取数据，则增加了数据库服务器的压力。为了在不影响整个系统结构的情况下透明地缓存这些数据，可以在Facade一层通过Proxy模式配合ReadWriteLock实现缓存，而客户端和后台的DAO数据访问对象都不受影响：</p>
<p><img alt="" src="/upload/167/62/153/42eeef5b79054a6fadbce86d9c53b707.png" /></p>
<p>首先，现有的中间层是由Facade接口和一个FacadeImpl具体实现构成的。对ArticleCategory的相关操作在FacadeImpl中实现如下：</p>
<pre class="brush:java">
public class FacadeImpl implements Facade {
    protected CategoryDao categoryDao;
    public void setCategoryDao(CategoryDao categoryDao) {
        this.categoryDao = categoryDao;
    }
    // 读操作:
    public ArticleCategory queryArticleCategory(Serializable id) {
        return categoryDao.queryArticleCategory(id);
    }
    // 读操作:
    public List&lt;ArticleCategory&gt; queryArticleCategories() {
        return categoryDao.queryArticleCategories();
    }
    // 写操作:
    public void createArticleCategory(ArticleCategory category) {
        categoryDao.create(category);
    }
    // 写操作:
    public void deleteArticleCategory(ArticleCategory category) {
        categoryDao.delete(category);
    }
    // 写操作:
    public void updateArticleCategory(ArticleCategory category) {
        categoryDao.update(category);
    }
    // 其他方法省略...
}
</pre>
<p>设计代理类FacadeCacheProxy，让其实现缓存ArticleCategory的功能：</p>
<pre class="brush:java">
public class FacadeCacheProxy implements Facade {
    private Facade target;
    public void setFacadeTarget(Facade target) {
        this.target = target;
    }

    // 定义缓存对象:
    private FullCache&lt;ArticleCategory&gt; cache = new FullCache&lt;ArticleCategory&gt;() {
        // how to get real data when cache is unavailable:
        protected List&lt;ArticleCategory&gt; doGetList() {
            return target.queryArticleCategories();
        }
    };

    // 从缓存返回数据:
    public List&lt;ArticleCategory&gt; queryArticleCategories() {
        return cache.getCachedList();
    }

    // 创建新的ArticleCategory后，让缓存失效:
    public void createArticleCategory(ArticleCategory category) {
        target.createArticleCategory(category);
        cache.clearCache();
    }

    // 更新某个ArticleCategory后，让缓存失效:
    public void updateArticleCategory(ArticleCategory category) {
        target.updateArticleCategory(category);
        cache.clearCache();
    }

    // 删除某个ArticleCategory后，让缓存失效:
    public void deleteArticleCategory(ArticleCategory category) {
        target.deleteArticleCategory(category);
        cache.clearCache();
    }
}
</pre>
<p>该代理类的核心是调用读方法getArticleCategories()时，直接从缓存对象FullCache中返回结果，当调用写方法（create，update和delete）时，除了调用target对象的相应方法外，再将缓存对象清空。</p>
<p>FullCache便是实现缓存的关键类。为了实现强类型的缓存，采用泛型实现FullCache：</p>
<pre class="brush:java">
public abstract class FullCache&lt;T extends AbstractId&gt; {
    ...
}
</pre>
<p>AbstractId是所有Domain Object的超类，目的是提供一个String类型的主键，同时便于在Hibernate或其他ORM框架中只需要配置一次JPA注解：</p>
<pre class="brush:java">
@MappedSuperclass
public abstract class AbstractId {
    protected String id;

    @Id
    @Column(nullable=false, updatable=false, length=32)
    @GeneratedValue(generator=&quot;system-uuid&quot;)
    @GenericGenerator(name=&quot;system-uuid&quot;, strategy=&quot;uuid&quot;)
    public String getId() { return id; }
    public void setId(String id) { this.id = id; }
}
</pre>
<p>FullCache实现以下2个功能：</p>
<ol>
    <li>List&lt;T&gt; getCachedList()：获取整个缓存的List&lt;T&gt;</li>
    <li>clearCache()：清除所有缓存</li>
</ol>
<p>此外，FullCache在缓存失效的情况下，必须从真正的数据源获得数据，因此，抽象方法：</p>
<pre class="brush:java">
protected abstract List&lt;T&gt; doGetList()
</pre>
<p>负责获取真正的数据。</p>
<p>下面，用ReadWriteLock实现该缓存模型。</p>
<p>Java 5平台新增了java.util.concurrent包，该包包含了许多非常有用的多线程应用类，例如ReadWriteLock，这使得开发人员不必自己封装就可以直接使用这些健壮的多线程类。</p>
<p>ReadWriteLock是一种常见的多线程设计模式。当多个线程同时访问同一资源时，通常，并行读取是允许的，但是，任一时刻只允许最多一个线程写入，从而保证了读写操作的完整性。下图很好地说明了ReadWriteLock的读写并发模型：</p>
<table border="1" cellspacing="1" cellpadding="1" width="227" style="width: 227px; height: 61px">
    <tbody>
        <tr>
            <td>&nbsp;</td>
            <td>读</td>
            <td>写</td>
        </tr>
        <tr>
            <td>读</td>
            <td>允许</td>
            <td>不允许</td>
        </tr>
        <tr>
            <td>写</td>
            <td>不允许</td>
            <td>不允许</td>
        </tr>
    </tbody>
</table>
<p>当读线程远多于写线程时，使用ReadWriteLock来取代synchronized同步会显著地提高性能，因为大多数时候，并发的多个读线程不需要等待。</p>
<p>Java 5的ReadWriteLock接口仅定义了如何获取ReadLock和WriteLock的方法，对于具体的ReadWriteLock的实现模式并没有规定，例如，Read优先还是Write优先，是否允许在等待写锁的时候获取读锁，是否允许将一个写锁降级为读锁，等等。</p>
<p>Java 5自身提供的一个ReadWriteLock的实现是ReentrantReadWriteLock，该ReadWriteLock实现能满足绝大多数的多线程环境，有如下特点：</p>
<ul>
    <li>支持两种优先级模式，以时间顺序获取锁和以读、写交替优先获取锁的模式；</li>
    <li>当获得了读锁或写锁后，还可重复获取读锁或写锁，即ReentrantLock；</li>
    <li>获得写锁后还可获得读锁，但获得读锁后不可获得写锁；</li>
    <li>支持将写锁降级为读锁，但反之不行；</li>
    <li>支持在等待锁的过程中中断；</li>
    <li>对写锁支持Condition（用于取代wait，notify和notifyAll）；</li>
    <li>支持锁的状态检测，但仅仅用于监控系统状态而并非同步控制；</li>
</ul>
<p>FullCache采用ReentrantReadWriteLock实现读写同步：</p>
<pre class="brush:java">
public abstract class FullCache&lt;T extends AbstractId&gt; {
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    private final Lock readLock = lock.readLock(); // 读锁
    private final Lock writeLock = lock.writeLock(); // 写锁

    private List&lt;T&gt; cachedList = null; // 持有缓存的数据，若为null，表示缓存失效
}
</pre>
<p>对于clearCache()方法，由于其是一个写操作，故定义如下：</p>
<pre class="brush:java">
public void clearCache() {
    writeLock.lock();
    cachedList = null;
    writeLock.unlock();
}
</pre>
<p>对于get方法，由于是读操作，同时要考虑在缓存失效的情况下更新数据，其实现就稍微复杂一点：</p>
<pre class="brush:java">
public List&lt;T&gt; getCachedList() {
    // 获得读锁:
    readLock.lock();
    try {
        if(cachedList==null) {
            // 在获得写锁前，必须先释放读锁:
            readLock.unlock();
            writeLock.lock();
            try {
                cachedList = doGetList(); // 获取真正的数据
            }
            finally {
                // 在释放写锁前，先获得读锁:
                readLock.lock();
                writeLock.unlock();
            }
        }
        return cachedList;
    }
    finally {
        // 确保读锁在方法返回前被释放:
        readLock.unlock();
    }
}
</pre>
<p>通过适当的装配（例如在Spring IoC容器中），让客户端持有FacadeCacheProxy的引用，就在中间层完全实现了透明的缓存，客户端代码一行也不用更改。</p>
<p>考虑到多线程模型远比单线程复杂，为了确保FullCache实现的健壮性，编写一个FullCacheTest来执行单元测试：</p>
<pre class="brush:java">
public class FullCacheTest {
    // count how many hits:
    class Hit {
        private AtomicInteger total = new AtomicInteger(0);
        private AtomicInteger notHit = new AtomicInteger(0);

        public void total() {
            total.incrementAndGet();
        }

        public void notHit() {
            notHit.incrementAndGet();
        }

        public void debug() {
            System.err.println(&quot;Total get: &quot; + total.intValue());
            System.err.println(&quot;Not hit: &quot; + notHit.intValue());
            System.err.println(&quot;Hits: &quot; + ((total.intValue()-notHit.intValue()) * 100 / total.intValue()) + &quot;%&quot;);
        }
    }

    private static final int DATA_OPERATION = 10;
    private static final int MAX = 10;
    private static String[] ids = new String[MAX];

    static {
        for(int i=0; i&lt;MAX; i++) {
            ids[i] = UUID.randomUUID().toString();
        }
    }

    private Hit hit;
    private FullCache&lt;ArticleCategory&gt; cache;

    @Before
    public void setUp() {
        hit = new Hit();
        cache = new FullCache&lt;ArticleCategory&gt;() {
            @Override
            protected List&lt;ArticleCategory&gt; doGetList() {
                hit.notHit();
                List&lt;ArticleCategory&gt; list = new ArrayList&lt;ArticleCategory&gt;();
                for(int i=0; i&lt;MAX; i++) {
                    ArticleCategory obj = new ArticleCategory();
                    obj.setId(ids[i]);
                    list.add(obj);
                }
                doSleep(DATA_OPERATION);
                return list;
            }

            @Override
            public List&lt;ArticleCategory&gt; getCachedList() {
                hit.total();
                return super.getCachedList();
            }
        };
    }

    @Test
    public void testMultiThread() {
        final int THREADS = 100;
        final int LOOP_PER_THREAD = 100000;
        List&lt;Thread&gt; threads = new ArrayList&lt;Thread&gt;(THREADS);
        // test FullCache.getCachedList(id):
        for(int i=0; i&lt;THREADS; i++) {
            threads.add(
                    new Thread() {
                        public void run() {
                            for(int j=0; j&lt;LOOP_PER_THREAD; j++) {
                                List&lt;ArticleCategory&gt; list = cache.getCachedList();
                                for(int k=0; k&lt;MAX; k++) {
                                    assertEquals(ids[k], list.get(k).getId());
                                }
                            }
                        }
                    }
            );
        }
        // test FullCache.clearCache():
        Thread clearThread = new Thread() {
                public void run() {
                    for(;;) {
                        cache.clearCache();
                        try {
                            Thread.sleep(DATA_OPERATION * 2);
                        }
                        catch(InterruptedException e) {
                            break;
                        }
                    }
                }
        };
        // start all threads:
        clearThread.start();
        for(Thread t : threads) {
            t.start();
        }
        // wait for all threads:
        for(Thread t : threads) {
            try {
                t.join();
            }
            catch(InterruptedException e) {}
        }
        clearThread.interrupt();
        try {
            clearThread.join();
        }
        catch(InterruptedException e) {}
        // statistics:
        hit.debug();
    }

    private static void doSleep(long n) {
        try {
            Thread.sleep(n);
        }
        catch(InterruptedException e) {}
    }
}
</pre>
<p>反复运行JUnit测试，均未报错。统计结果如下：</p>
<pre>
Total get: 10000000
Not hit: 7
Hits: 99%
</pre>
<p>执行时间3.9秒。如果用synchronized取代ReadWriteLock，执行时间为204秒，可见性能差异巨大。</p>
<h3 style="color: red">总结</h3>
<p>接口和实现的分离是必要的，否则难以实现Proxy模式。</p>
<p>Facade模式和DAO模式都是必要的，否则，一旦数据访问分散在各个Servlet或JSP中，将难以控制缓存读写。</p>
<p>本文完整的源代码可以从以下地址获取：</p>
<p><a target="_blank" href="http://javaeedev.googlecode.com/files/FullCache.zip">http://javaeedev.googlecode.com/files/FullCache.zip</a></p>]]></description></item>
<item><title>Spring入门</title><link>http://www.liaoxuefeng.com/it-51fc67c487c7471db8a1b9115da4cae4-1</link><dc:creator>廖雪峰</dc:creator><pubDate>Wed, 15 Jul 2009 13:03:54 +0800</pubDate><description><![CDATA[<p>Spring是一个非常优秀的轻量级框架，通过Spring的IoC容器，我们的关注点便放到了需要实现的业务逻辑上。对AOP的支持则能让我们动态增强业务方法。编写普通的业务逻辑Bean是非常容易而且易于测试的，因为它能脱离J2EE容器（如Servlet，JSP环境）单独进行单元测试。最后的一步便是在Spring框架中将这些业务Bean以XML配置文件的方式组织起来，它们就按照我们预定的目标正常工作了！非常容易！</p>
<p>本文将给出一个基本的Spring入门示例，并演示如何使用Spring的AOP将复杂的业务逻辑分离到每个方面中。</p>
<h3 style="color: red">开发环境配置</h3>
<p>首先，需要正确配置Java环境。推荐安装JDK1.4.2，并正确配置环境变量：</p>
<pre class="brush:bash">
JAVA_HOME=(JDK安装目录)
CLASSPATH=.
Path=%JAVA_HOME%\bin;...
</pre>
<p>我们将使用免费的Eclipse 3.1作为IDE。新建一个Java Project，将Spring的发布包spring.jar以及commons-logging-1.0.4.jar复制到Project目录下，并在Project &gt; Properties中配置好Java Build Path：</p>
<p><img alt="" src="/upload/18/0/1/987b03b1357742be88033e4cd481c701.png" /></p>
<h3 style="color: red">编写Bean接口及其实现</h3>
<p>我们实现一个管理用户的业务Bean。首先定义一个ServiceBean接口，声明一些业务方法：</p>
<pre class="brush:java">
package com.crackj2ee.example.spring;

/**
 * Interface of service facade.
 * 
 * @author Xuefeng
 */
public interface ServiceBean {
    void addUser(String username, String password);
    void deleteUser(String username);
    boolean findUser(String username);
    String getPassword(String username);
}
</pre>
<p>然后在MyServiceBean中实现接口：</p>
<pre class="brush:java">
package com.crackj2ee.example.spring;

import java.util.*;

public class MyServiceBean implements ServiceBean {

    private String dir;
    private Map map = new HashMap();

    public void setUserDir(String dir) {
        this.dir = dir;
        System.out.println(&quot;Set user dir to: &quot; + dir);
    }

    public void addUser(String username, String password) {
        if(!map.containsKey(username))
            map.put(username, password);
        else
            throw new RuntimeException(&quot;User already exist.&quot;);
    }

    public void deleteUser(String username) {
        if(map.remove(username)==null)
            throw new RuntimeException(&quot;User not exist.&quot;);
    }

    public boolean findUser(String username) {
        return map.containsKey(username);
    }

    public String getPassword(String username) {
        return (String)map.get(username);
    }
}
</pre>
<p>为了简化逻辑，我们使用一个Map保存用户名和口令。</p>
<p>现在，我们已经有了一个业务Bean。要测试它非常容易，因为到目前为止，我们还没有涉及到Spring容器，也没有涉及到任何Web容器（假定这是一个Web应用程序关于用户管理的业务Bean）。完全可以直接进行Unit测试，或者，简单地写个main方法测试：</p>
<pre class="brush:java">
package com.crackj2ee.example.spring;

public class Main {

    public static void main(String[] args) throws Exception {
        ServiceBean service = new MyServiceBean();
        service.addUser(&quot;bill&quot;, &quot;hello&quot;);
        service.addUser(&quot;tom&quot;, &quot;goodbye&quot;);
        service.addUser(&quot;tracy&quot;, &quot;morning&quot;);
        System.out.println(&quot;tom's password is: &quot; + service.getPassword(&quot;tom&quot;));
        if(service.findUser(&quot;tom&quot;)) {
            service.deleteUser(&quot;tom&quot;);
        }
    }
}
</pre>
<p>执行结果：</p>
<p><img alt="" width="566" height="184" src="/upload/114/77/39/b40375e27b344b67a5a786c8270a1280.png" /></p>
<h3 style="color: red">在Spring中配置Bean并获得Bean的实例</h3>
<p>我们已经在一个main方法中实现了业务，不过，将对象的生命周期交给容器管理是更好的办法，我们就不必为初始化对象和销毁对象进行硬编码，从而获得更大的灵活性和可测试性。</p>
<p>想要把ServiceBean交给Spring来管理，我们需要一个XML配置文件。新建一个beans.xml，放到src目录下，确保在classpath中能找到此配置文件，输入以下内容：</p>
<pre class="brush:xml">
&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&lt;!DOCTYPE beans PUBLIC &quot;-//SPRING//DTD BEAN//EN&quot; 
&quot;http://www.springframework.org/dtd/spring-beans.dtd&quot;&gt;
&lt;beans&gt;
    &lt;bean id=&quot;service&quot; class=&quot;com.crackj2ee.example.spring.MyServiceBean&quot; /&gt;
&lt;/beans&gt;
</pre>
<p>以上XML声明了一个id为service的Bean，默认地，Spring为每个声明的Bean仅创建一个实例，并通过id来引用这个Bean。下面，我们修改main方法，让Spring来管理业务Bean：</p>
<pre class="brush:java">
package com.crackj2ee.example.spring;

import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;

public class Main {

    public static void main(String[] args) throws Exception {
        // init factory:
        XmlBeanFactory factory = new XmlBeanFactory(new ClassPathResource(&quot;beans.xml&quot;));
        // use service bean:
        ServiceBean service = (ServiceBean)factory.getBean(&quot;service&quot;);
        service.addUser(&quot;bill&quot;, &quot;hello&quot;);
        service.addUser(&quot;tom&quot;, &quot;goodbye&quot;);
        service.addUser(&quot;tracy&quot;, &quot;morning&quot;);
        System.out.println(&quot;tom's password is \&quot;&quot; + service.getPassword(&quot;tom&quot;) + &quot;\&quot;&quot;);
        if(service.findUser(&quot;tom&quot;)) {
            service.deleteUser(&quot;tom&quot;);
        }
        // close factory:
        factory.destroySingletons();
    }
}
</pre>
<p>执行结果：</p>
<p><img alt="" width="566" height="183" src="/upload/136/118/152/722808196e88448abc993a50e317af6b.png" /></p>
<p>由于我们要通过main方法启动Spring环境，因此，首先需要初始化一个BeanFactory。红色部分是初始化Spring的BeanFactory的典型代码，只需要保证beans.xml文件位于classpath中。</p>
<p>然后，在BeanFactory中通过id查找，即可获得相应的Bean的实例，并将其适当转型为合适的接口。</p>
<p>接着，实现一系列业务操作，在应用程序结束前，让Spring销毁所有的Bean实例。</p>
<p>对比上一个版本的Main，可以看出，最大的变化是不需要自己管理Bean的生命周期。另一个好处是在不更改实现类的前提下，动态地为应用程序增加功能。</p>
<h3 style="color: red">编写Advisor以增强ServiceBean</h3>
<p>所谓AOP即是将分散在各个方法处的公共代码提取到一处，并通过类似拦截器的机制实现代码的动态织入。可以简单地想象成，在某个方法的调用前、返回前、调用后和抛出异常时，动态插入自己的代码。在弄清楚Pointcut、Advice之类的术语前，不如编写一个最简单的AOP应用来体验一下。</p>
<p>考虑一下通常的Web应用程序都会有日志记录，我们来编写一个LogAdvisor，对每个业务方法调用前都作一个记录：</p>
<pre class="brush:java">
package com.crackj2ee.example.spring;

import java.lang.reflect.Method;
import org.springframework.aop.MethodBeforeAdvice;

public class LogAdvisor implements MethodBeforeAdvice {
    public void before(Method m, Object[] args, Object target) throws Throwable {
        System.out.println(&quot;[Log] &quot; + target.getClass().getName() + &quot;.&quot; + m.getName() + &quot;()&quot;);
    }
}
</pre>
<p>然后，修改beans.xml：</p>
<pre class="brush:java">
&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&lt;!DOCTYPE beans PUBLIC &quot;-//SPRING//DTD BEAN//EN&quot; 
&quot;http://www.springframework.org/dtd/spring-beans.dtd&quot;&gt;

&lt;beans&gt;
    &lt;bean id=&quot;serviceTarget&quot; class=&quot;com.crackj2ee.example.spring.MyServiceBean&quot; /&gt;

    &lt;bean id=&quot;logAdvisor&quot; class=&quot;com.crackj2ee.example.spring.LogAdvisor&quot; /&gt;

    &lt;bean id=&quot;service&quot; class=&quot;org.springframework.aop.framework.ProxyFactoryBean&quot;&gt;
        &lt;property name=&quot;proxyInterfaces&quot;&gt;&lt;value&gt;com.crackj2ee.example.spring.ServiceBean&lt;/value&gt;&lt;/property&gt;
        &lt;property name=&quot;target&quot;&gt;&lt;ref local=&quot;serviceTarget&quot;/&gt;&lt;/property&gt;
        &lt;property name=&quot;interceptorNames&quot;&gt;
            &lt;list&gt;
                &lt;value&gt;logAdvisor&lt;/value&gt;
            &lt;/list&gt;
        &lt;/property&gt;
    &lt;/bean&gt;
&lt;/beans&gt;
</pre>
<p>注意观察修改后的配置文件，我们使用了一个ProxyFactoryBean作为service来与客户端打交道，而真正的业务Bean即MyServiceBean被声明为serviceTarget并作为参数对象传递给ProxyFactoryBean，proxyInterfaces指定了返回的接口类型。对于客户端而言，将感觉不出任何变化，但却动态加入了LogAdvisor，关系如下：</p>
<p><img alt="" width="382" height="106" src="/upload/216/144/247/a1b2fc6046f04764b552475ffdf5efde.png" /></p>
<p>运行结果如下，可以很容易看到调用了哪些方法：</p>
<p><img alt="" width="566" height="330" src="/upload/233/96/118/8699599748f8464a8c5d0c5ef911919b.png" /></p>
<p>要截获指定的某些方法也是可以的。下面的例子将修改getPassword()方法的返回值：</p>
<pre class="brush:java">
package com.crackj2ee.example.spring;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

public class PasswordAdvisor implements MethodInterceptor {
    public Object invoke(MethodInvocation invocation) throws Throwable {
        Object ret = invocation.proceed();
        if(ret==null)
            return null;
        String password = (String)ret;
        StringBuffer encrypt = new StringBuffer(password.length());
        for(int i=0; i&lt;password.length(); i++)
            encrypt.append('*');
        return encrypt.toString();
    }
}
</pre>
<p>这个PasswordAdvisor将截获ServiceBean的getPassword()方法的返回值，并将其改为&quot;***&quot;。继续修改beans.xml：</p>
<pre class="brush:xml">
&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&lt;!DOCTYPE beans PUBLIC &quot;-//SPRING//DTD BEAN//EN&quot; 
&quot;http://www.springframework.org/dtd/spring-beans.dtd&quot;&gt;
&lt;beans&gt;
    &lt;bean id=&quot;serviceTarget&quot; class=&quot;com.crackj2ee.example.spring.MyServiceBean&quot; /&gt;

    &lt;bean id=&quot;logAdvisor&quot; class=&quot;com.crackj2ee.example.spring.LogAdvisor&quot; /&gt;

    &lt;bean id=&quot;passwordAdvisorTarget&quot; class=&quot;com.crackj2ee.example.spring.PasswordAdvisor&quot; /&gt;

    &lt;bean id=&quot;passwordAdvisor&quot; class=&quot;org.springframework.aop.support.RegexpMethodPointcutAdvisor&quot;&gt;
        &lt;property name=&quot;advice&quot;&gt;
            &lt;ref local=&quot;passwordAdvisorTarget&quot;/&gt;
        &lt;/property&gt;
        &lt;property name=&quot;patterns&quot;&gt;
            &lt;list&gt;
                &lt;value&gt;.*getPassword&lt;/value&gt;
            &lt;/list&gt;
        &lt;/property&gt;
    &lt;/bean&gt;

    &lt;bean id=&quot;service&quot; class=&quot;org.springframework.aop.framework.ProxyFactoryBean&quot;&gt;
        &lt;property name=&quot;proxyInterfaces&quot;&gt;&lt;value&gt;com.crackj2ee.example.spring.ServiceBean&lt;/value&gt;&lt;/property&gt;
        &lt;property name=&quot;target&quot;&gt;&lt;ref local=&quot;serviceTarget&quot;/&gt;&lt;/property&gt;
        &lt;property name=&quot;interceptorNames&quot;&gt;
            &lt;list&gt;
                &lt;value&gt;logAdvisor&lt;/value&gt;
                &lt;value&gt;passwordAdvisor&lt;/value&gt;
            &lt;/list&gt;
        &lt;/property&gt;
    &lt;/bean&gt;
&lt;/beans&gt;
</pre>
<p>利用Spring提供的一个RegexMethodPointcutAdvisor可以非常容易地指定要截获的方法。运行结果如下，可以看到返回结果变为&quot;******&quot;：</p>
<p><img alt="" src="/upload/139/200/254/aa366cb663d44230ba47b03446e6dee2.png" /></p>
<p>还需要继续增强ServiceBean？我们编写一个ExceptionAdvisor，在业务方法抛出异常时能做一些处理：</p>
<pre class="brush:java">
package com.crackj2ee.example.spring;

import org.springframework.aop.ThrowsAdvice;

public class ExceptionAdvisor implements ThrowsAdvice {
    public void afterThrowing(RuntimeException re) throws Throwable {
        System.out.println(&quot;[Exception] &quot; + re.getMessage());
    }
}
</pre>
<p>将此Advice添加到beans.xml中，然后在业务Bean中删除一个不存在的用户，故意抛出异常：</p>
<pre class="brush:java">
service.deleteUser(&quot;not-exist&quot;);
</pre>
<p>再次运行，注意到ExceptionAdvisor记录下了异常：</p>
<p><img alt="" width="566" height="223" src="/upload/122/169/28/b22d05b1a4b6463a86d744b393ccbb1d.png" /></p>
<h3 style="color: red">总结</h3>
<p>利用Spring非常强大的IoC容器和AOP功能，我们能实现非常灵活的应用，让Spring容器管理业务对象的生命周期，利用AOP增强功能，却不影响业务接口，从而避免更改客户端代码。</p>
<p>为了实现这一目标，必须始终牢记：面向接口编程。而Spring默认的AOP代理也是通过Java的代理接口实现的。虽然Spring也可以用CGLIB实现对普通类的代理，但是，业务对象只要没有接口，就会变得难以扩展、维护和测试。</p>
<p>源代码下载：<a target="_blank" href="http://javaeedev.googlecode.com/files/SpringBasic.zip">http://javaeedev.googlecode.com/files/SpringBasic.zip</a></p>]]></description></item>
<item><title>使用EasyMock使单元测试更加容易</title><link>http://www.liaoxuefeng.com/it-995eee59cc064de38d9c2d3f112bf2d8-1</link><dc:creator>廖雪峰</dc:creator><pubDate>Tue, 14 Jul 2009 17:33:40 +0800</pubDate><description><![CDATA[<p>单元测试是XP极力推荐的测试驱动开发模式，是保证软件质量的重要方法。尽管如此，对许多类的单元测试仍然是极其困难的，例如，对数据库操作的类进行测试，如果不准备好数据库环境以及相关测试数据，是很难进行单元测试的；再例如，对需要运行在容器内的Servlet或EJB组件，脱离了容器也难于测试。</p>
<p>幸运的是，Mock Object可以用来模拟一些我们需要的类，这些对象被称之为模仿对象，在单元测试中它们特别有价值。</p>
<p>Mock Object用于模仿真实对象的方法调用，从而使得测试不需要真正的依赖对象。Mock Object只为某个特定的测试用例的场景提供刚好满足需要的最少功能。它们还可以模拟错误的条件，例如抛出指定的异常等。</p>
<p>目前，有许多可用的Mock类库可供我们选择。一些Mock库提供了常见的模仿对象，例如：HttpServletRequest，而另一些Mock库则提供了动态生成模仿对象的功能，本文将讨论使用EasyMock动态生成模仿对象以便应用于单元测试。</p>
<p>到目前为止，EasyMock提供了1.2版本和2.0版本，2.0版本仅支持Java SE 5.0，本例中，我们选择EasyMock 1.2 for Java 1.3版本进行测试，可以从<a target="_blank" href="http://www.easymock.org">http://www.easymock.org</a>下载合适的版本。</p>
<p>我们首先来看一个用户验证的LoginServlet类：</p>
<pre class="brush:java">
package com.javaeedev.test.mock;

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class LoginServlet extends HttpServlet {

    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String username = request.getParameter(&quot;username&quot;);
        String password = request.getParameter(&quot;password&quot;);
        // check username &amp; password:
        if(&quot;admin&quot;.equals(username) &amp;&amp; &quot;123456&quot;.equals(password)) {
            ServletContext context = getServletContext();
            RequestDispatcher dispatcher = context.getNamedDispatcher(&quot;dispatcher&quot;);
            dispatcher.forward(request, response);
        }
        else {
            throw new RuntimeException(&quot;Login failed.&quot;);
        }
    }
}
</pre>
<p>这个Servlet实现简单的用户验证的功能，若用户名和口令匹配&ldquo;admin&rdquo;和&ldquo;123456&rdquo;，则请求被转发到指定的dispatcher上，否则，直接抛出RuntimeException。</p>
<p>为了测试doPost()方法，我们需要模拟HttpServletRequest，ServletContext和RequestDispatcher对象，以便脱离J2EE容器来测试这个Servlet。我们建立TestCase，名为LoginServletTest：</p>
<pre class="brush:java">
public class LoginServletTest extends TestCase {
}
</pre>
<p>我们首先测试当用户名和口令验证失败的情形，演示如何使用EasyMock来模拟HttpServletRequest对象：</p>
<pre class="brush:java">
public void testLoginFailed() throws Exception {
    MockControl mc = MockControl.createControl(HttpServletRequest.class);
    HttpServletRequest request = (HttpServletRequest)mc.getMock();
    // set Mock Object behavior:
    request.getParameter(&quot;username&quot;);
    mc.setReturnValue(&quot;admin&quot;, 1);
    request.getParameter(&quot;password&quot;);
    mc.setReturnValue(&quot;1234&quot;, 1);
    // ok, all behaviors are set!
    mc.replay();
    // now start test:
    LoginServlet servlet = new LoginServlet();
    try {
        servlet.doPost(request, null);
        fail(&quot;Not caught exception!&quot;);
    }
    catch(RuntimeException re) {
        assertEquals(&quot;Login failed.&quot;, re.getMessage());
    }
    // verify:
    mc.verify();
}
</pre>
<p>仔细观察测试代码，使用EasyMock来创建一个Mock对象需要首先创建一个MockControl：</p>
<pre class="brush:java">
MockControl mc = MockControl.createControl(HttpServletRequest.class);
</pre>
<p>然后，即可获得MockControl创建的Mock对象：</p>
<pre class="brush:java">
HttpServletRequest request = (HttpServletRequest)mc.getMock();
</pre>
<p>下一步，我们需要&ldquo;录制&rdquo;Mock对象的预期行为。在LoginServlet中，先后调用了request.getParameter(&quot;username&quot;)和request.getParameter(&quot;password&quot;)两个方法，因此，需要在MockControl中设置这两次调用后的指定返回值。我们期望返回的值为&ldquo;admin&rdquo;和&ldquo;1234&rdquo;：</p>
<pre class="brush:java">
request.getParameter(&quot;username&quot;); // 期望下面的测试将调用此方法，参数为&quot;username&quot;
mc.setReturnValue(&quot;admin&quot;, 1); // 期望返回值为&quot;admin&quot;，仅调用1次
request.getParameter(&quot;password&quot;); // 期望下面的测试将调用此方法，参数为&quot; password&quot;
mc.setReturnValue(&quot;1234&quot;, 1); // 期望返回值为&quot;1234&quot;，仅调用1次
</pre>
<p>紧接着，调用mc.replay()，表示Mock对象&ldquo;录制&rdquo;完毕，可以开始按照我们设定的方式运行，我们对LoginServlet进行测试，并预期会产生一个RuntimeException：</p>
<pre class="brush:java">
LoginServlet servlet = new LoginServlet();
try {
    servlet.doPost(request, null);
    fail(&quot;Not caught exception!&quot;);
}
catch(RuntimeException re) {
    assertEquals(&quot;Login failed.&quot;, re.getMessage());
}
</pre>
<p>由于本次测试的目的是检查当用户名和口令验证失败后，LoginServlet是否会抛出RuntimeException，因此，response对象对测试没有影响，我们不需要模拟它，仅仅传入null即可。</p>
<p>最后，调用mc.verify()检查Mock对象是否按照预期的方法调用正常运行了。</p>
<p>运行JUnit，测试通过！表示我们的Mock对象正确工作了！</p>
<p>下一步，我们来测试当用户名和口令匹配时，LoginServlet应当把请求转发给指定的RequestDispatcher。在这个测试用例中，我们除了需要HttpServletRequest Mock对象外，还需要模拟ServletContext和RequestDispatcher对象：</p>
<pre class="brush:java">
MockControl requestCtrl = MockControl.createControl(HttpServletRequest.class);
HttpServletRequest requestObj = (HttpServletRequest)requestCtrl.getMock();
MockControl contextCtrl = MockControl.createControl(ServletContext.class);
final ServletContext contextObj = (ServletContext)contextCtrl.getMock();
MockControl dispatcherCtrl = MockControl.createControl(RequestDispatcher.class);
RequestDispatcher dispatcherObj = (RequestDispatcher)dispatcherCtrl.getMock();
</pre>
<p>按照doPost()的语句顺序，我们设定Mock对象指定的行为：</p>
<pre class="brush:java">
requestObj.getParameter(&quot;username&quot;);
requestCtrl.setReturnValue(&quot;admin&quot;, 1);
requestObj.getParameter(&quot;password&quot;);
requestCtrl.setReturnValue(&quot;123456&quot;, 1);
contextObj.getNamedDispatcher(&quot;dispatcher&quot;);
contextCtrl.setReturnValue(dispatcherObj, 1);
dispatcherObj.forward(requestObj, null);
dispatcherCtrl.setVoidCallable(1);
requestCtrl.replay();
contextCtrl.replay();
dispatcherCtrl.replay();
</pre>
<p>然后，测试doPost()方法，这里，为了让getServletContext()方法返回我们创建的ServletContext Mock对象，我们定义一个匿名类并覆写getServletContext()方法：</p>
<pre class="brush:java">
LoginServlet servlet = new LoginServlet() {
    public ServletContext getServletContext() {
        return contextObj;
    }
};
servlet.doPost(requestObj, null);
</pre>
<p>最后，检查所有Mock对象的状态：</p>
<pre class="brush:java">
requestCtrl.verify();
contextCtrl.verify();
dispatcherCtrl.verify();
</pre>
<p>运行JUnit，测试通过！</p>
<p>倘若LoginServlet的代码有误，例如，将context.getNamedDispatcher(&quot;dispatcher&quot;)误写为 context.getNamedDispatcher(&quot;dispatcher2&quot;)，则测试失败，JUnit报告：</p>
<pre class="brush:java">
junit.framework.AssertionFailedError:
  Unexpected method call getNamedDispatcher(&quot;dispatcher2&quot;):
    getNamedDispatcher(&quot;dispatcher2&quot;): expected: 0, actual: 1
    getNamedDispatcher(&quot;dispatcher&quot;): expected: 1, actual: 0
      at ...
</pre>
<p>完整的LoginServletTest代码如下：</p>
<pre class="brush:java">
package com.javaeedev.test.mock;

import javax.servlet.*;
import javax.servlet.http.*;

import org.easymock.*;

import junit.framework.TestCase;

public class LoginServletTest extends TestCase {

    public void testLoginFailed() throws Exception {
        MockControl mc = MockControl.createControl(HttpServletRequest.class);
        HttpServletRequest request = (HttpServletRequest)mc.getMock();
        // set Mock Object behavior:
        request.getParameter(&quot;username&quot;);
        mc.setReturnValue(&quot;admin&quot;, 1);
        request.getParameter(&quot;password&quot;);
        mc.setReturnValue(&quot;1234&quot;, 1);
        // ok, all behaviors are set!
        mc.replay();
        // now start test:
        LoginServlet servlet = new LoginServlet();
        try {
            servlet.doPost(request, null);
            fail(&quot;Not caught exception!&quot;);
        }
        catch(RuntimeException re) {
            assertEquals(&quot;Login failed.&quot;, re.getMessage());
        }
        // verify:
        mc.verify();
    }

    public void testLoginOK() throws Exception {
        // create mock:
        MockControl requestCtrl = MockControl.createControl(HttpServletRequest.class);
        HttpServletRequest requestObj = (HttpServletRequest)requestCtrl.getMock();
        MockControl contextCtrl = MockControl.createControl(ServletContext.class);
        final ServletContext contextObj = (ServletContext)contextCtrl.getMock();
        MockControl dispatcherCtrl = MockControl.createControl(RequestDispatcher.class);
        RequestDispatcher dispatcherObj = (RequestDispatcher)dispatcherCtrl.getMock();
        // set behavior:
        requestObj.getParameter(&quot;username&quot;);
        requestCtrl.setReturnValue(&quot;admin&quot;, 1);
        requestObj.getParameter(&quot;password&quot;);
        requestCtrl.setReturnValue(&quot;123456&quot;, 1);
        contextObj.getNamedDispatcher(&quot;dispatcher&quot;);
        contextCtrl.setReturnValue(dispatcherObj, 1);
        dispatcherObj.forward(requestObj, null);
        dispatcherCtrl.setVoidCallable(1);
        // done!
        requestCtrl.replay();
        contextCtrl.replay();
        dispatcherCtrl.replay();
        // test:
        LoginServlet servlet = new LoginServlet() {
            public ServletContext getServletContext() {
                return contextObj;
            }
        };
        servlet.doPost(requestObj, null);
        // verify:
        requestCtrl.verify();
        contextCtrl.verify();
        dispatcherCtrl.verify();
    }
}
</pre>
<p>虽然EasyMock可以用来模仿依赖对象，但是，它只能动态模仿接口，无法模仿具体类。这一限制正好要求我们遵循&ldquo;针对接口编程&rdquo;的原则：如果不针对接口，则测试难于进行。应当把单元测试看作是运行时代码的最好运用，如果代码在单元测试中难于应用，则它在真实环境中也将难于应用。总之，创建尽可能容易测试的代码就是创建高质量的代码。</p>]]></description></item>
<item><title>J2ME概念解析</title><link>http://www.liaoxuefeng.com/it-f2ddc1bac8d747f088cf88a96763d4ed-1</link><dc:creator>廖雪峰</dc:creator><pubDate>Tue, 14 Jul 2009 17:32:08 +0800</pubDate><description><![CDATA[<p>J2ME，即Java 2 Micro Edition，是SUN公司推出的在移动设备上运行的微型版Java平台，常见的移动设备有手机，PDA，电子词典，以及各式各样的信息终端如机顶盒等等。</p>
<p>由于移动终端的类型成千上万，而且计算能力差异非常大，不可能像桌面系统那样仅仅两三个版本的JVM即可满足Windows，Linux和Unix系统，因此，J2ME不是一个简单的微型版的JVM。为了满足千差万别的移动设备的需求，SUN定义了一系列的针对不同类型设备的规范，因此，J2ME平台便是由许多的规范组成的集合。</p>
<p>最重要的移动终端当然是手机了，因此，我们主要讨论手机相关的J2ME规范。</p>
<h3 style="color: red">Configuration</h3>
<p>SUN把不同的设备按照计算能力分为<span style="color: #ff0000">CLDC</span>（Connected Limited Device Configuration）和<span style="color: #ff0000">CDC</span>（Connected Device Configuration）两大类，这两个Configuration是针对设备软硬件环境严格定义的，比如CLDC1.0定义了内存大小为64-512k，任何设备如果支持CLDC1.0，就必须严格满足定义，不能有可选的或者含糊的功能。</p>
<p>CLDC1.0是针对计算能力非常有限的设备定义的，只支持整数运算，不支持浮点运算，早期的Java手机大部分都支持CLDC1.0，如Nokia 3650，Siemens 6688i。</p>
<p>CLDC1.1则增加了浮点运算，因此，在支持CLDC1.1的设备上，可以使用float和double类型的变量。现在的Java手机很多都能支持CLDC1.1，如Nokia 9500，Siemens S65。</p>
<p>CDC则是针对计算能力比较强的设备定义的，如PPC等，CDC平台的JVM基本上和桌面的JVM很接近了，只是可以使用的Package大大少于J2SE的包。支持CDC的非常高端的Java手机也会很快上市。</p>
<h3 style="color: red">Profile</h3>
<p>和Configuration相比，Profile更多是针对软件的定义，Profile定义有必须实现的，也有可选的功能，因此，Profile更灵活。</p>
<p>最重要的Profile当然是<span style="color: #ff0000">MIDP</span>（Micro Information Device Profile），MIDP定义了能在Java手机上运行的Java程序的规范，符合MIDP规范的Java小程序被称为MIDlet，可以直接通过无线网络下载到手机并运行。</p>
<p>早期的MIDP1.0规范使我们能在手机上运行有UI界面的Java程序，但是MIDP1.0对游戏的支持不够，必须自己实现许多必须的代码，因此，MIDP2.0规范大大加强了对游戏开发的支持，使开发者编写更少的代码来创建游戏。</p>
<p>MIDP规范的图形界面基本上都是独立于J2SE的AWT和Swing组件，因为目前手机的计算能力还比较有限，但是，随着手机的CPU越来越快，使得AWT和Swing移植到手机上也将成为可能，因此，最新的PBP 1.0（Personal Basic Profile）和PP 1.0（Personal Profile）规范提供了部分AWT和Swing的支持，目前，部分高端PDA已经可以运行PBP和PP的Java程序了。可以预见，将来大部分的AWT和Swing组件都能移植到手机上。</p>
<p>前面已经说过，和Configuration相比，Profile有许多可选包，比较实用的Profile还有在<span style="color: #ff0000">JSR135</span>定义的<span style="color: #ff0000">MMAPI</span>（Mobile Media API），实现多媒体播放功能；在<span style="color: #ff0000">JSR184</span>定义的<span style="color: #ff0000">M3G API</span>（Mobile 3D Graphics API），实现3D功能；在<span style="color: #ff0000">JSR120</span>定义的<span style="color: #ff0000">WMA</span>（Wireless Message API），实现短消息收发。如果你的手机支持某一Profile，如M3G，那么便可以在MIDlet中使用M3G的3D API实现3D游戏。</p>
<p>Profile虽然定义了Java API接口，但是底层如何实现是由各厂商自己决定的，如M3G定义了3D接口，但是底层实现既可以使用硬件加速，也可以由C程序模拟，或者部分由硬件实现，部分由软件实现。</p>
<p>比J2ME更精简的Java平台被SUN称为<span style="color: #ff0000">JavaCard</span>，运行在信用卡等芯片中，实现电子支付等功能，目前SUN还没有把JavaCard并入J2ME平台。</p>]]></description></item>
<item><title>3分钟安装配置Postfix邮件服务器</title><link>http://www.liaoxuefeng.com/it-ef5f8ce39bce4d4c862c1cc4fc2972c5-1</link><dc:creator>廖雪峰</dc:creator><pubDate>Tue, 14 Jul 2009 10:55:16 +0800</pubDate><description><![CDATA[<p>Linux邮件服务器通常使用Sendmail，在网上Google了Sendmail的教程后，我决定知难而退，改用Postfix。</p>
<p>Postfix是用来替代Sendmail的，它的配置文件比Sendmail简单得多，配置相当容易。</p>
<p>在配置邮件服务器之前，先解释几个概念。</p>
<p>我们通常使用Email都很容易，但是Internet的邮件系统是通过几个复杂的部分连接而成的，对于最终用户而言，我们熟悉的Outlook，Foxmail等都是用来收信和发信的，称之为<strong><span style="color: #ff0000">MUA</span></strong>：Mail User Agent，邮件用户代理。</p>
<p>MUA并非直接将邮件发送至收件人手中，而是通过<strong><span style="color: #ff0000">MTA</span></strong>：Mail Transfer Agent，邮件传输代理代为传递，Sendmail和Postfix就是扮演MTA的角色。</p>
<p>一封邮件从MUA发出后，可能通过一个或多个MTA传递，最终到达<strong><span style="color: #ff0000">MDA</span></strong>：Mail Delivery Agent，邮件投递代理，邮件到达MDA后，就存放在某个文件或特殊的数据库里，我们将这个长期保存邮件的地方称之为邮箱。</p>
<p>一旦邮件到达邮箱，就原地不动了，等用户再通过MUA将其取走，就是用Outlook，Foxmail等软件收信的过程。</p>
<p>所以一封邮件的流程是：</p>
<p>发件人：MUA --发送--&gt; MTA -&gt; 若干个MTA... -&gt; MTA -&gt; MDA &lt;--收取-- MUA：收件人</p>
<p>MUA到MTA，以及MTA到MTA之间使用的协议就是SMTP协议，而收邮件时，MUA到MDA之间使用的协议最常用的是POP3或IMAP。</p>
<p>需要注意的是，专业邮件服务商都有大量的机器来为用户服务，所以通常MTA和MDA并不是同一台服务器，因此，在Outlook等软件里，我们需要分别填写SMTP发送服务器的地址和POP3接收服务器的地址。</p>
<p>下面开始用Postfix搭建Linux下的邮件服务器。目标服务器是RedHat Enterprise Linux 4，首先需要停止Sendmail：</p>
<pre class="brush:bash">
# /etc/init.d/sendmail stop
</pre>
<p>并从启动组里移除：</p>
<pre class="brush:bash">
# chkconfig sendmail off
</pre>
<p>然后，通过rpm包安装Postfix：</p>
<pre class="brush:bash">
# rpm -Uvh postfix-2.x.x.xxx.rpm
</pre>
<p>Postfix只有一个/etc/postfix/main.cf需要修改，其他配置文件可以直接使用默认设置。</p>
<p>第一个需要修改的参数是myhostname，指向真正的域名，例如：</p>
<pre class="brush:bash">
myhostname = mail.example.com
</pre>
<p>mydomain参数指向根域：</p>
<pre class="brush:bash">
mydomain = example.com
</pre>
<p>myorigin和mydestination都可以指向mydomain：</p>
<pre class="brush:bash">
myorigin = $mydomain
mydestination = $mydomain
</pre>
<p>Postfix默认只监听本地地址，如果要与外界通信，就需要监听网卡的所有IP：</p>
<pre class="brush:bash">
inet_interfaces = all
</pre>
<p>Postfix默认将子网内的机器设置为可信任机器，如果只信任本机，就设置为host：</p>
<pre class="brush:bash">
mynetworks_style = host
</pre>
<p>配置哪些地址的邮件能够被Postfix转发，当然是mydomain的才能转发，否则其他人都可以用这台邮件服务器转发垃圾邮件了：</p>
<pre class="brush:bash">
relay_domains = $mydomain
</pre>
<p>现在，Postfix已经基本配置完成，我们需要对邮件的发送进行控制：</p>
<ol>
    <li>对于外域到本域的邮件，必须接收，否则，收不到任何来自外部的邮件；</li>
    <li>对于本域到外域的邮件，只允许从本机发出，否则，其他人通过伪造本域地址就可以向外域发信；</li>
    <li>对于外域到外域的邮件，直接拒绝，否则我们的邮件服务器就是Open Relay，将被视为垃圾邮件服务器。</li>
</ol>
<p>先设置发件人的规则：</p>
<pre class="brush:bash">
smtpd_sender_restrictions = permit_mynetworks, check_sender_access hash:/etc/postfix/sender_access, permit
</pre>
<p>以上规则先判断是否是本域地址，如果是，允许，然后再从sender_access文件里检查发件人是否存在，拒绝存在的发件人，最后允许其他发件人</p>
<p>然后设置收件人规则：</p>
<pre class="brush:bash">
smtpd_recipient_restrictions = permit_mynetworks, check_recipient_access hash:/etc/postfix/recipient_access, reject
</pre>
<p>以上规则先判断是否是本域地址，如果是，允许，然后再从recipient_access文件里检查收件人是否存在，允许存在的收件人，最后拒绝其他收件人。</p>
<p>/etc/postfix/sender_access的内容：</p>
<pre class="brush:bash">
example.com REJECT
</pre>
<p>目的是防止其他用户从外部以xxx@example.com身份发送邮件，但登录到本机再发送则不受影响，因为第一条规则permit_mynetworks允许本机登录用户发送邮件。</p>
<p>/etc/postfix/recipient_access的内容：</p>
<pre class="brush:bash">
postmaster@example.com OK
webmaster@example.com OK
</pre>
<p>因此，外域只能发送给以上两个Email地址，其他任何地址都将被拒绝。但本机到本机发送不受影响。</p>
<p>最后用postmap生成hash格式的文件：</p>
<pre class="brush:bash">
# postmap sender_access
# postmap recipient_access
</pre>
<p>启动Postfix：</p>
<pre class="brush:bash">
# /etc/init.d/postfix start
</pre>
<p>设置到启动组里：</p>
<pre class="brush:bash">
# chkconfig postfix on
</pre>
<p>现在就可以通过telnet来测试了：（红色是输入的命令）</p>
<p><span style="font-family: Courier New">220 mail.example.com ESMTP Postfix<br />
<span style="color: #ff0000">helo localhost</span><br />
250 mail.example.com<br />
<span style="color: #ff0000">mail from:test@gmail.com</span><br />
250 Ok<br />
<span style="color: #ff0000">rcpt to:webmaster@example.com</span><br />
250 Ok<br />
<span style="color: #ff0000">data</span><br />
354 End data with &lt;CR&gt;&lt;LF&gt;.&lt;CR&gt;&lt;LF&gt;<br />
<span style="color: #ff0000">hello!!!!!!<br />
.</span><br />
250 Ok: queued as D68E41407D0<br />
<span style="color: #ff0000">mail from:test@gmail.com</span><br />
250 Ok<br />
<span style="color: #ff0000">rcpt to:haha@example.com<br />
</span>554 &lt;haha@example.com&gt;: Recipient address rejected: Access denied<br />
<span style="color: #ff0000">quit</span><br />
221 Bye</span></p>
<p>如果配置了SMTP认证，就可以让用户远程发送时能通过认证后再发送邮件，目前还没有配置，准备继续研究后再配置。需要注意的是，配置SMTP认证后，设置规则应该是：</p>
<ol>
    <li>外域-&gt;本域：不需认证，允许，否则将接受不到任何外部邮件；</li>
    <li>本域-&gt;外域：需要认证，否则拒绝。</li>
</ol>
<p>因为我们作为发送服务器的MTA和转发的MTA实际上是由一个MTA完成的，所以需要以上规则。对于大型邮件服务商，发送服务器的MTA和转发的MTA是分别部署的，例如，sina的发送服务器是smtp.sina.com，需要经过用户认证，而转发服务器是mx???.sina.com，不需要认证，否则无法转发邮件。</p>
<p>最后不要忘了在DNS的MX记录中将域名mail.example.com添上。</p>]]></description></item>
<item><title>使用NetBeans开发国际化MIDP应用</title><link>http://www.liaoxuefeng.com/it-f76d273947884411a1779cf00553d85d-1</link><dc:creator>廖雪峰</dc:creator><pubDate>Tue, 14 Jul 2009 09:58:11 +0800</pubDate><description><![CDATA[<p>J2ME并未提供类似J2SE的ResourceBuddle类，因此，为了实现国际化，我们只能根据类似的思路，自己实现一个ResourceBuddle。</p>
<p>首先，要准备一个资源文件，保存所有需要用到的语言字符串，在MIDP应用程序运行期，根据用户手机当前的语言设置自动读取相应的资源。</p>
<p>为了得到用户手机当前的区域设置，使用System.getProperty(&ldquo;<strong>microedition.locale</strong>&rdquo;)，例如，返回zh-CN表示中国。</p>
<p>NetBeans的示例程序提供了一个Localization Support Example，如果安装了Mobility Pack，就可以直接打开这个示例工程，然后在此基础上开发自己的国际化程序。</p>
<p>实现国际化的主要类就是LocalizationSupport类，其思路就是读入messages.properties文件，然后根据locale和key取得对应的字符串。要添加一个新的Locale，只需选中messages.properties文件，右键点击，选择Add Locale：</p>
<p><img width="279" height="406" alt="" src="/upload/0/207/155/3543ac340be84c578709f95e0ccdd8e3.png" /><br />
&nbsp;<br />
即可添加新的语言。</p>]]></description></item>
<item><title>对DAO编写单元测试</title><link>http://www.liaoxuefeng.com/it-6fd35fa6390e43ee972921eb2ca99a53-1</link><dc:creator>廖雪峰</dc:creator><pubDate>Mon, 13 Jul 2009 21:12:04 +0800</pubDate><description><![CDATA[<p><span style="color: #ff6600">本文最早发表于BEA dev2dev</span></p>
<p>单元测试作为保证软件质量及重构的基础，早已获得广大开发人员的认可。单元测试是一种细粒度的测试，越来越多的开发人员在提交功能模块时也同时提交相应的单元测试。对于大多数开发人员来讲，编写单元测试已经成为开发过程中必须的流程和最佳实践。</p>
<p>对普通的逻辑组件编写单元测试是一件容易的事情，由于逻辑组件通常只需要内存资源，因此，设置好输入输出即可编写有效的单元测试。对于稍微复杂一点的组件，例如Servlet，我们可以自行编写模拟对象，以便模拟HttpRequest和HttpResponse等对象，或者，使用EasyMock之类的动态模拟库，可以对任意接口实现相应的模拟对象，从而对依赖接口的组件进行有效的单元测试。</p>
<p>在J2EE开发中，对DAO组件编写单元测试往往是一件非常复杂的任务。和其他组件不通，DAO组件通常依赖于底层数据库，以及JDBC接口或者某个ORM框架（如Hibernate），对DAO组件的测试往往还需引入事务，这更增加了编写单元测试的复杂性。虽然使用EasyMock也可以模拟出任意的JDBC接口对象，或者ORM框架的主要接口，但其复杂性往往非常高，需要编写大量的模拟代码，且代码复用度很低，甚至不如直接在真实的数据库环境下测试。不过，使用真实数据库环境也有一个明显的弊端，我们需要准备数据库环境，准备初始数据，并且每次运行单元测试后，其数据库现有的数据将直接影响到下一次测试，难以实现&ldquo;即时运行，反复运行&rdquo;单元测试的良好实践。</p>
<p>本文针对DAO组件给出一种较为合适的单元测试的编写策略。在JavaEE开发网（<a href="http://www.javaeedev.com/">http://www.javaeedev.com</a>）的开发过程中，为了对DAO组件进行有效的单元测试，我们采用HSQLDB这一小巧的纯Java数据库作为测试时期的数据库环境，配合Ant，实现了自动生成数据库脚本，测试前自动初始化数据库，极大地简化了DAO组件的单元测试的编写。</p>
<p>在Java领域，JUnit作为第一个单元测试框架已经获得了最广泛的应用，无可争议地成为Java领域单元测试的标准框架。本文以最新的JUnit 4版本为例，演示如何创建对DAO组件的单元测试用例。</p>
<p>JavaEEdev的持久层使用Hibernate 3.2，底层数据库为MySQL。为了演示如何对DAO进行单元测试，我们将其简化为一个DAOTest工程：</p>
<p><img alt="" src="/upload/181/89/193/3d198f31c68f4cae9500c8af8e404a84.png" /></p>
<p>由于将Hibernate的Transaction绑定在Thread上，因此，HibernateUtil类负责初始化SessionFactory以及获取当前的Session：</p>
<pre class="brush:java">
public class HibernateUtil {
    private static final SessionFactory sessionFactory;
    static {
        try {
            sessionFactory = new AnnotationConfiguration()
                                 .configure()
                                 .buildSessionFactory();
        }
        catch(Exception e) {
            throw new ExceptionInInitializerError(e);
        }
    }

    public static Session getCurrentSession() {
        return sessionFactory.getCurrentSession();
    }
}
</pre>
<p>HibernateUtil还包含了一些辅助方法，如：</p>
<pre class="brush:java">
public static Object query(Class clazz, Serializable id);
public static void createEntity(Object entity);
public static Object queryForObject(String hql, Object[] params);
public static List queryForList(String hql, Object[] params);
</pre>
<p>在此不再多述。</p>
<p>实体类User使用JPA注解，代表一个用户：</p>
<pre class="brush:java">
@Entity
@Table(name=&quot;T_USER&quot;)
public class User {
    public static final String REGEX_USERNAME = &quot;[a-z0-9][a-z0-9\\-]{1,18}[a-z0-9]&quot;;
    public static final String REGEX_PASSWORD = &quot;[a-f0-9]{32}&quot;;
    public static final String REGEX_EMAIL = &quot;([0-9a-zA-Z]([-.\\w]*[0-9a-zA-Z])*@([0-9a-zA-Z][-\\w]*[0-9a-zA-Z]\\.)+[a-zA-Z]{2,9})&quot;;

    private String username;     // 用户名
    private String password;     // MD5口令
    private boolean admin;       // 是否是管理员
    private String email;        // 电子邮件
    private int emailValidation; // 电子邮件验证码
    private long createdDate;    // 创建时间
    private long lockDate;       // 锁定时间

    public User() {}

    public User(String username, String password, boolean admin, long lastSignOnDate) {
        this.username = username;
        this.password = password;
        this.admin = admin;
    }

    @Id
    @Column(updatable=false, length=20)
    @Pattern(regex=REGEX_USERNAME)
    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }

    @Column(nullable=false, length=32)
    @Pattern(regex=REGEX_PASSWORD)
    public String getPassword() { return password; }
    public void setPassword(String password) { this.password = password; }

    @Column(nullable=false, length=50)
    @Pattern(regex=REGEX_EMAIL)
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }

    @Column(nullable=false)
    public boolean getAdmin() { return admin; }
    public void setAdmin(boolean admin) { this.admin = admin; }

    @Column(nullable=false, updatable=false)
    public long getCreatedDate() { return createdDate; }
    public void setCreatedDate(long createdDate) { this.createdDate = createdDate; }

    @Column(nullable=false)
    public int getEmailValidation() { return emailValidation; }
    public void setEmailValidation(int emailValidation) { this.emailValidation = emailValidation; }

    @Column(nullable=false)
    public long getLockDate() { return lockDate; }
    public void setLockDate(long lockDate) { this.lockDate = lockDate; }

    @Transient
    public boolean getEmailValidated() { return emailValidation==0; }

    @Transient
    public boolean getLocked() {
        return !admin &amp;&amp; lockDate&gt;0 &amp;&amp; lockDate&gt;System.currentTimeMillis();
    }
}
</pre>
<p>&nbsp;实体类PasswordTicket代表一个重置口令的请求：</p>
<pre class="brush:java">
@Entity
@Table(name=&quot;T_PWDT&quot;)
public class PasswordTicket {
    private String id;
    private User user;
    private String ticket;
    private long createdDate;

    @Id
    @Column(nullable=false, updatable=false, length=32)
    @GeneratedValue(generator=&quot;system-uuid&quot;)
    @GenericGenerator(name=&quot;system-uuid&quot;, strategy=&quot;uuid&quot;)
    public String getId() { return id; }
    protected void setId(String id) { this.id = id; }

    @ManyToOne
    @JoinColumn(nullable=false, updatable=false)
    public User getUser() { return user; }
    public void setUser(User user) { this.user = user; }

    @Column(nullable=false, updatable=false, length=32)
    public String getTicket() { return ticket; }
    public void setTicket(String ticket) { this.ticket = ticket; }

    @Column(nullable=false, updatable=false)
    public long getCreatedDate() { return createdDate; }
    public void setCreatedDate(long createdDate) { this.createdDate = createdDate; }
}
</pre>
<p>UserDao接口定义了对用户的相关操作：</p>
<pre class="brush:java">
public interface UserDao {
    User queryForSignOn(String username);
    User queryUser(String username);
    void createUser(User user);
    void updateUser(User user);
    boolean updateEmailValidation(String username, int ticket);
    String createPasswordTicket(User user);
    boolean updatePassword(String username, String oldPassword, String newPassword);
    boolean queryResetPassword(User user, String ticket);
    boolean updateResetPassword(User user, String ticket, String password);
    void updateLock(User user, long lockTime);
    void updateUnlock(User user);
}
</pre>
<p>UserDaoImpl是其实现类：</p>
<pre class="brush:java">
public class UserDaoImpl implements UserDao {
    public User queryForSignOn(String username) {
        User user = queryUser(username);
        if(user.getLocked())
            throw new LockException(user.getLockDate());
        return user;
    }

    public User queryUser(String username) {
        return (User) HibernateUtil.query(User.class, username);
    }

    public void createUser(User user) {
        user.setEmailValidation((int)(Math.random() * 1000000) + 0xf);
        HibernateUtil.createEntity(user);
    }
    // 其余方法略
    ...
}
</pre>
<p>由于将Hibernate事务绑定在Thread上，因此，实际的客户端调用DAO组件时，还必须加入事务代码：</p>
<pre class="brush:java">
Transaction tx = HibernateUtil.getCurrentSession().beginTransaction();
try {
    dao.xxx();
    tx.commit();
}
catch(Exception e) {
    tx.rollback();
    throw e;
}
</pre>
<p>下面，我们开始对DAO组件编写单元测试。前面提到了HSQLDB这一小巧的纯Java数据库。HSQLDB除了提供完整的JDBC驱动以及事务支持外，HSQLDB还提供了进程外模式（与普通数据库类似）和进程内模式（In-Process），以及文件和内存两种存储模式。我们将HSQLDB设定为进程内模式及仅使用内存存储，这样，在运行JUnit测试时，可以直接在测试代码中启动HSQLDB。测试完毕后，由于测试数据并没有保存在文件上，因此，不必清理数据库。</p>
<p>此外，为了执行批量测试，在每个独立的DAO单元测试运行前，我们都执行一个初始化脚本，重新建立所有的表。该初始化脚本是通过HibernateTool自动生成的，稍后我们还会讨论。下图是单元测试的执行顺序：</p>
<p><img alt="" src="/upload/4/24/57/f1426b2228aa49458e5c5854d250c6dc.png" /></p>
<p>在编写测试类之前，我们首先准备了一个TransactionCallback抽象类，该类通过Template模式将DAO调用代码通过事务包装起来：</p>
<pre class="brush:java">
public abstract class TransactionCallback {
    public final Object execute() throws Exception {
        Transaction tx = HibernateUtil.getCurrentSession().beginTransaction();
        try {
            Object r = doInTransaction();
            tx.commit();
            return r;
        }
        catch(Exception e) {
            tx.rollback();
            throw e;
        }
    }
    // 模板方法：
    protected abstract Object doInTransaction() throws Exception;
}
</pre>
<p>其原理是使用JDK提供的动态代理。由于JDK的动态代理只能对接口代理，因此，要求DAO组件必须实现接口。如果只有具体的实现类，则只能考虑CGLIB之类的第三方库，在此我们不作更多讨论。</p>
<p>下面我们需要编写DatabaseFixture，负责启动HSQLDB数据库，并在@Before方法中初始化数据库表。该DatabaseFixture可以在所有的DAO组件的单元测试类中复用：</p>
<pre class="brush:java">
public class DatabaseFixture {
    private static Server server = null; // 持有HSQLDB的实例
    private static final String DATABASE_NAME = &quot;javaeedev&quot;; // 数据库名称
    private static final String SCHEMA_FILE = &quot;schema.sql&quot;; // 数据库初始化脚本
    private static final List&lt;String&gt; initSqls = new ArrayList&lt;String&gt;();

    @BeforeClass // 启动HSQLDB数据库
    public static void startDatabase() throws Exception {
        if(server!=null)
            return;
        server = new Server();
        server.setDatabaseName(0, DATABASE_NAME);
        server.setDatabasePath(0, &quot;mem:&quot; + DATABASE_NAME);
        server.setSilent(true);
        server.start();
        try {
            Class.forName(&quot;org.hsqldb.jdbcDriver&quot;);
        }
        catch(ClassNotFoundException cnfe) {
            throw new RuntimeException(cnfe);
        }
        LineNumberReader reader = null;
        try {
            reader = new LineNumberReader(new InputStreamReader(DatabaseFixture.class.getClassLoader().getResourceAsStream(SCHEMA_FILE)));
            for(;;) {
                String line = reader.readLine();
                if(line==null) break;
                // 将text类型的字段改为varchar(2000)，因为HSQLDB不支持text：
                line = line.trim().replace(&quot; text &quot;, &quot; varchar(2000) &quot;).replace(&quot; text,&quot;, &quot; varchar(2000),&quot;);
                if(!line.equals(&quot;&quot;))
                    initSqls.add(line);
            }
        }
        catch(IOException e) {
            throw new RuntimeException(e);
        }
        finally {
            if(reader!=null) {
                try { reader.close(); } catch(IOException e) {}
            }
        }
    }

    @Before // 执行初始化脚本
    public void initTables() {
        for(String sql : initSqls) {
            executeSQL(sql);
        }
    }

    static Connection getConnection() throws SQLException {
        return DriverManager.getConnection(&quot;jdbc:hsqldb:mem:&quot; + DATABASE_NAME, &quot;sa&quot;, &quot;&quot;);
    }

    static void close(Statement stmt) {
        if(stmt!=null) {
            try {
                stmt.close();
            }
            catch(SQLException e) {}
        }
    }

    static void close(Connection conn) {
        if(conn!=null) {
            try {
                conn.close();
            }
            catch(SQLException e) {}
        }
    }

    static void executeSQL(String sql) {
        Connection conn = null;
        Statement stmt = null;
        try {
            conn = getConnection();
            boolean autoCommit = conn.getAutoCommit();
            conn.setAutoCommit(true);
            stmt = conn.createStatement();
            stmt.execute(sql);
            conn.setAutoCommit(autoCommit);
        }
        catch(SQLException e) {
            log.warn(&quot;Execute failed: &quot; + sql + &quot;\nException: &quot; + e.getMessage());
        }
        finally {
            close(stmt);
            close(conn);
        }
    }

    public static Object createProxy(final Object target) {
        return Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new InvocationHandler() {
                    public Object invoke(Object proxy, final Method method, final Object[] args) throws Throwable {
                        return new TransactionCallback() {
                            @Override
                            protected Object doInTransaction() throws Exception {
                                return method.invoke(target, args);
                            }
                        }.execute();
                    }
                }
        );
    }
}
</pre>
<p>注意DatabaseFixture的createProxy()方法，它将一个普通的DAO对象包装为在事务范围内执行的代理对象，即对于一个普通的DAO对象的方法调用前后，自动地开启事务并根据异常情况提交或回滚事务。</p>
<p>下面是UserDaoImpl的单元测试类：</p>
<pre class="brush:java">
public class UserDaoImplTest extends DatabaseFixture {
    private UserDao userDao = new UserDaoImpl();
    private UserDao proxy = (UserDao)createProxy(userDao);

    @Test
    public void testQueryUser() {
        User user = newUser(&quot;test&quot;);
        proxy.createUser(user);
        User t = proxy.queryUser(&quot;test&quot;);
        assertEquals(user.getEmail(), t.getEmail());
    }
}
</pre>
<p>注意到UserDaoImplTest持有两个UserDao引用，userDao是普通的UserDaoImpl对象，而proxy则是将userDao进行了事务封装的对象。</p>
<p>由于UserDaoImplTest从DatabaseFixture继承，因此，@Before方法在每个@Test方法调用前自动调用，这样，每个@Test方法执行前，数据库都是一个经过初始化的&ldquo;干净&rdquo;的表。</p>
<p>对于普通的测试，如UserDao.queryUser()方法，直接调用proxy.queryUser()即可在事务内执行查询，获得返回结果。</p>
<p>对于异常测试，例如期待一个ResourceNotFoundException，就不能直接调用proxy.queryUser()方法，否则，将得到一个UndeclaredThrowableException：</p>
<p>&nbsp;<img alt="" src="/upload/219/221/153/e462101ebf5344789b87b0e65bc59e17.png" /></p>
<p>这是因为通过反射调用抛出的异常被代理类包装为UndeclaredThrowableException，因此，对于异常测试，只能使用原始的userDao对象配合TransactionCallback实现：</p>
<pre class="brush:java">
@Test(expected=ResourceNotFoundException.class)
public void testQueryNonExistUser() throws Exception {
    new TransactionCallback() {
        protected Object doInTransaction() throws Exception {
            userDao.queryUser(&quot;nonexist&quot;);
            return null;
        }
    }.execute();
}
</pre>
<p>到此为止，对DAO组件的单元测试已经实现完毕。下一步，我们需要使用HibernateTool自动生成数据库脚本，免去维护SQL语句的麻烦。相关的Ant脚本片段如下：</p>
<pre class="brush:xml">
&lt;target name=&quot;make-schema&quot; depends=&quot;build&quot; description=&quot;create schema&quot;&gt;
    &lt;taskdef name=&quot;hibernatetool&quot; classname=&quot;org.hibernate.tool.ant.HibernateToolTask&quot;&gt;
        &lt;classpath refid=&quot;build-classpath&quot;/&gt;
    &lt;/taskdef&gt;
    &lt;taskdef name=&quot;annotationconfiguration&quot; classname=&quot;org.hibernate.tool.ant.AnnotationConfigurationTask&quot;&gt;
        &lt;classpath refid=&quot;build-classpath&quot;/&gt;
    &lt;/taskdef&gt;
    &lt;annotationconfiguration configurationfile=&quot;${src.dir}/hibernate.cfg.xml&quot;/&gt;
    &lt;hibernatetool destdir=&quot;${gen.dir}&quot;&gt;
        &lt;classpath refid=&quot;build-classpath&quot;/&gt;
        &lt;annotationconfiguration configurationfile=&quot;${src.dir}/hibernate.cfg.xml&quot;/&gt;
        &lt;hbm2ddl
            export=&quot;false&quot;
            drop=&quot;true&quot;
            create=&quot;true&quot;
            delimiter=&quot;;&quot;
            outputfilename=&quot;schema.sql&quot;
            destdir=&quot;${src.dir}&quot;
        /&gt;
    &lt;/hibernatetool&gt;
&lt;/target&gt;
</pre>
<p>完整的Ant脚本以及Hibernate配置文件请参考项目工程源代码。</p>
<p>利用HSQLDB，我们已经成功地简化了对DAO组件进行单元测试。我发现这种方式能够找出许多常见的bug：</p>
<ul>
    <li>HQL语句的语法错误，包括SQL关键字和实体类属性的错误拼写，反复运行单元测试就可以不断地修复许多这类错误，而不需要等到通过Web页面请求而调用DAO时才发现问题；</li>
    <li>传入了不一致或者顺序错误的HQL参数数组，导致Hibernate在运行期报错；</li>
    <li>一些逻辑错误，包括不允许的null属性（常常由于忘记设置实体类的属性），更新实体时引发的数据逻辑状态不一致。</li>
</ul>
<p>总之，单元测试需要根据被测试类的实际情况，编写最简单最有效的测试用例。本文旨在给出一种编写DAO组件单元测试的有效方法。</p>
<p>本文的全部源代码可以在此下载：<a target="_blank" href="http://javaeedev.googlecode.com/files/DAOTest.zip">http://javaeedev.googlecode.com/files/DAOTest.zip</a></p>]]></description></item>
<item><title>经济学十大原理</title><link>http://www.liaoxuefeng.com/it-9621b06afbc644fd8b4f1b7bd3def015-1</link><dc:creator>廖雪峰</dc:creator><pubDate>Mon, 13 Jul 2009 13:42:08 +0800</pubDate><description><![CDATA[<h3 style="color: red">一：人们面临交替关系</h3>
<p>&ldquo;天下没有白吃的午餐。&rdquo;为了得到一件东西，通常不得不放弃另一件东西。作出决策要求我们在一个目标与另一个目标之间有所取舍。</p>
<p>学生面临如何分配学习时间的交替，父母在购物，旅游和储蓄间面临交替，社会面临效率与平等的交替。</p>
<p>[名词解释]</p>
<p>效率：社会能从其稀缺资源中得到最多东西的特性。</p>
<p>平等：经济成果在社会成员中公平分配的特性。</p>
<h3 style="color: red">二：某种东西的成本是为了得到它而放弃的东西</h3>
<p>很多情况下，某种行动的成本并不像乍看时那么明显。</p>
<p>一种东西的机会成本是为了得到这种东西所放弃的东西。</p>
<p>考虑上大学的决策，成本不是住房和伙食，因为即使不上大学，也要租房和吃饭。最大的成本是时间，如果把上大学的时间用于工作，能赚到的工资就是上大学最大的单项成本。</p>
<p>因此，很多正值上大学年龄的职业运动员如果放弃运动而上大学，可能每年少赚几百万美元，因此他们上大学的成本比普通人高得多。这也是为什么许多职业运动员一定要退役后才去上大学的原因。</p>
<p>[名词解释]</p>
<p>机会成本：为了得到某种东西所必须放弃的东西。</p>
<h3 style="color: red">三：理性人考虑边际量</h3>
<p>许多决策涉及到对现有行动计划进行微小的增量调整，经济学家把这些调整称为边际变动。</p>
<p>假设一架200个座位的飞机飞一次的成本是10万美元，每个座位的成本是500美元，有人会说：票价决不应低于500美元。但是当飞机即将起飞时仍有10个空座，在登机口等退票的乘客愿意支付300美元买一张票，应该卖给他吗？当然应该。如果飞机有空座，多增加一位乘客的成本微乎其微。虽然一位乘客飞行的平均成本是500美元，但是边际成本仅仅是这位额外的乘客将消费的一包花生米和一杯饮料而已。</p>
<p>只有一种行动的边际收益大于边际成本，一个理性决策者才会采取这项行动。</p>
<p>[名词解释]</p>
<p>边际变动：对行动计划小的增量调整。</p>
<h3 style="color: red">四：人们会对激励作出反应</h3>
<p>由于人们通过比较成本与收益作出决策，所以，当成本或收益变动时，人们的行为也会改变。这就是说，人们会对激励作出反应。</p>
<p>例如，当苹果价格上涨时，人们就决定少吃苹果多吃梨，因为成本高了。同时，苹果园主决定雇用更多工人并多摘苹果，因为收益也高了。</p>
<p>通过立法要求汽车公司必须为汽车配备安全带带来的后果：安全带降低了驾驶员的车祸代价，因此驾驶员的反应是更快更放肆地开车，结果是更多的车祸次数，但是每次车祸中驾驶员死亡的概率降低了。但是对行人有不利的影响，他们遇上了更多的车祸但没有安全带。因此，汽车安全法导致的结果是：驾驶员死亡人数变动很小，行人的死亡人数增加了。</p>
<p>在分析任何一种决策时，不仅应该考虑直接影响，而且还应该考虑激励发生作用的间接影响。</p>
<h3 style="color: red">五：贸易能使每个人状况更好</h3>
<p>贸易使每个人可以专门从事自己最擅长的活动。通过与他人交易，人们可以按较低的价格买到各种各样的物品与劳务。</p>
<p>经济中每个家庭都与其他所有家庭竞争，但是把你的家庭与所有其他家庭隔绝开来并不会过得更好，如果是这样的话，你的家庭就必须自己种粮食，做衣服，盖房子。</p>
<p>国家和家庭一样也能从相互交易中获益。</p>
<h3 style="color: red">六：市场通常是组织经济活动的一种好办法</h3>
<p>在一个市场经济中，中央计划者的决策被千百万企业和家庭的决策所取代。这些企业和家庭在市场上相互交易，价格和个人利益引导着他们的决策，他们仿佛被一只&ldquo;看不见的手&rdquo;所指引，引起了合意的市场结果。</p>
<p>价格指引这些个别决策者在大多数情况下实现了整个社会福利最大化的结果。</p>
<p>[名词解释]</p>
<p>市场经济：当许多企业和家庭在物品与劳务市场上相互交易时通过他们的分散决策配置资源的经济。</p>
<h3 style="color: red">七：政府有时可以改善市场结果</h3>
<p>政府干预经济的原因有两类：促进效率和促进平等。</p>
<p>经济学家用市场失灵这个词来指市场本身不能有效配置资源的情况。</p>
<p>市场失灵的一个可能原因是外部性。污染的例子：如果一家化工厂不承担排放烟尘的全部成本，它就会大量排放。</p>
<p>另一个可能原因是市场势力。假设镇里只有一口井，这口井的所有者对水的销售就有市场势力。</p>
<p>政府有时可以改善市场结果并不意味着它总能这样。</p>
<p>[名词解释]</p>
<p>市场失灵：市场本身不能有效配置资源的情况。</p>
<p>外部性：一个人的行动对旁观者福利的影响。</p>
<p>市场势力：一个经济活动者对市场价格有显著影响的能力。</p>
<h3 style="color: red">八：一国的生活水平取决于它生产物品与劳务的能力</h3>
<p>用什么来解释各国和不同时期中生活水平的巨大差别呢？答案之简单出人意料之外。几乎所有生活水平的变动都可以归因于各国生产率的差别：一个工人一小时所生产的物品与劳务量的差别。同样，一国的生产率增长率决定了平均收入增长率。</p>
<p>[名词解释]</p>
<p>生产率：一个工人一小时所生产的物品与劳务量。</p>
<h3 style="color: red">九：当政府发行了过多货币时，物价上升</h3>
<p>通货膨胀是经济中物价总水平的上升。</p>
<p>什么引起了通货膨胀？在大多数严重或持续的通货膨胀情况下，罪魁祸首结果总是相同的：货币量的增长。当一个政府创造了大量本国货币时，货币的价值下降了。</p>
<p>[名词解释]</p>
<p>通货膨胀：经济中物价总水平的上升。</p>
<h3 style="color: red">十：社会面临通货膨胀与失业之间的短期交替关系</h3>
<p>人们通常认为降低通货膨胀会引起失业暂时增加。通货膨胀与失业之间的这种交替关系被称为菲利普斯曲线。</p>
<p>当政府减少货币量时，它就减少了人们支出的数量。较低的支出与居高不下的价格结合在一起就减少了企业销售的物品与劳务量。销售量减少又引起企业解雇工人，就暂时增加了失业。</p>
<p>[名词解释]</p>
<p>菲利普斯曲线：通货膨胀与失业之间的短期交替关系。</p>]]></description></item>
<item><title>Java源码分析：深入探讨Iterator模式</title><link>http://www.liaoxuefeng.com/it-2e8a60a0d8954cfc9f9e40d22e7fe482-1</link><dc:creator>廖雪峰</dc:creator><pubDate>Mon, 13 Jul 2009 10:26:48 +0800</pubDate><description><![CDATA[<p style="color: #ff6600">本文最早发表于Sun技术社区：</p>
<p><a target="_blank" href="http://gceclub.sun.com.cn/yuanchuang/week-14/iterator.html">http://gceclub.sun.com.cn/yuanchuang/week-14/iterator.html</a></p>
<p>java.util包中包含了一系列重要的集合类。本文将从分析源码入手，深入研究一个集合类的内部结构，以及遍历集合的迭代模式的源码实现内幕。</p>
<p>下面我们先简单讨论一个根接口Collection，然后分析一个抽象类AbstractList和它的对应Iterator接口，并仔细研究迭代子模式的实现原理。</p>
<p>本文讨论的源代码版本是JDK 1.4.2，因为JDK 1.5在java.util中使用了很多泛型代码，为了简化问题，所以我们还是讨论1.4版本的代码。</p>
<h3 style="color: red">集合类的根接口Collection</h3>
<p>Collection接口是所有集合类的根类型。它的一个主要的接口方法是：</p>
<pre class="brush:java">
boolean add(Object c)
</pre>
<p>add()方法将添加一个新元素。注意这个方法会返回一个boolean，但是返回值不是表示添加成功与否。仔细阅读doc可以看到，Collection规定：如果一个集合拒绝添加这个元素，无论任何原因，都必须抛出异常。这个返回值表示的意义是add()方法执行后，集合的内容是否改变了（就是元素有无数量，位置等变化），这是由具体类实现的。即：如果方法出错，总会抛出异常；返回值仅仅表示该方法执行后这个Collection的内容有无变化。</p>
<p>类似的还有：</p>
<pre class="brush:java">
boolean addAll(Collection c);
boolean remove(Object o);
boolean removeAll(Collection c);
boolean remainAll(Collection c);
</pre>
<p>Object[] toArray()方法很简单，把集合转换成数组返回。Object[] toArray(Object[] a)方法就有点复杂了，首先，返回的Object[]仍然是把集合的所有元素变成的数组，但是类型和参数a的类型是相同的，比如执行：</p>
<pre class="brush:java">
String[] o = (String[])c.toArray(new String[0]);
</pre>
<p>得到的o实际类型是String[]。</p>
<p>其次，如果参数a的大小装不下集合的所有元素，返回的将是一个新的数组。如果参数a的大小能装下集合的所有元素，则返回的还是a，但a的内容用集合的元素来填充。尤其要注意的是，如果a的大小比集合元素的个数还多，a后面的部分全部被置为null。</p>
<p>最后一个最重要的方法是iterator()，返回一个Iterator（迭代子），用于遍历集合的所有元素。</p>
<h3 style="color: red">用Iterator模式实现遍历集合</h3>
<p>Iterator模式是用于遍历集合类的标准访问方法。它可以把访问逻辑从不同类型的集合类中抽象出来，从而避免向客户端暴露集合的内部结构。</p>
<p>例如，如果没有使用Iterator，遍历一个数组的方法是使用索引：</p>
<pre class="brush:java">
for(int i=0; i&lt;array.size(); i++) {
    // TODO: get(i) ...
}
</pre>
<p>而访问一个链表（LinkedList）又必须使用while循环：</p>
<pre class="brush:java">
while((e=e.next())!=null) {
    // TODO: e.data() ...
}
</pre>
<p>以上两种方法客户端都必须事先知道集合的内部结构，访问代码和集合本身是紧耦合，无法将访问逻辑从集合类和客户端代码中分离出来，每一种集合对应一种遍历方法，客户端代码无法复用。</p>
<p>更恐怖的是，如果以后需要把ArrayList更换为LinkedList，则原来的客户端代码必须全部重写。</p>
<p>为解决以上问题，Iterator模式总是用同一种逻辑来遍历集合：</p>
<pre class="brush:java">
for(Iterator it = c.iterater(); it.hasNext(); ) { ... }
</pre>
<p>奥秘在于客户端自身不维护遍历集合的&quot;指针&quot;，所有的内部状态（如当前元素位置，是否有下一个元素）都由Iterator来维护，而这个Iterator由集合类通过工厂方法生成，因此，它知道如何遍历整个集合。</p>
<p>客户端从不直接和集合类打交道，它总是控制Iterator，向它发送&quot;向前&quot;，&quot;向后&quot;，&quot;取当前元素&quot;的命令，就可以间接遍历整个集合。</p>
<p>首先看看java.util.Iterator接口的定义：</p>
<pre class="brush:java">
public interface Iterator {
    boolean hasNext();
    Object next();
    void remove();
}
</pre>
<p>依赖前两个方法就能完成遍历，典型的代码如下：</p>
<pre class="brush:java">
for(Iterator it = c.iterator(); it.hasNext(); ) {
    Object o = it.next();
    // 对o的操作...
}
</pre>
<p>在JDK1.5中，还对上面的代码在语法上作了简化：</p>
<pre class="brush:java">
// Type是具体的类型，如String。
for(Type t : c) {
    // 对t的操作...
}
</pre>
<p>每一种集合类返回的Iterator具体类型可能不同，Array可能返回ArrayIterator，Set可能返回SetIterator，Tree可能返回TreeIterator，但是它们都实现了Iterator接口，因此，客户端不关心到底是哪种Iterator，它只需要获得这个Iterator接口即可，这就是面向对象的威力。</p>
<h3 style="color: red">Iterator源码剖析</h3>
<p>让我们来看看AbstracyList如何创建Iterator。首先AbstractList定义了一个内部类（inner class）：</p>
<pre class="brush:java">
private class Itr implements Iterator {
    ...
}
</pre>
<p>而iterator()方法的定义是：</p>
<pre class="brush:java">
public Iterator iterator() {
    return new Itr();
}
</pre>
<p>因此客户端不知道它通过Iterator it = a.iterator();所获得的Iterator的真正类型。</p>
<p>现在我们关心的是这个申明为private的Itr类是如何实现遍历AbstractList的：</p>
<pre class="brush:java">
private class Itr implements Iterator {
    int cursor = 0;
    int lastRet = -1;
    int expectedModCount = modCount;
}
</pre>
<p>Itr类依靠3个int变量（还有一个隐含的AbstractList的引用）来实现遍历，cursor是下一次next()调用时元素的位置，第一次调用next()将返回索引为0的元素。lastRet记录上一次游标所在位置，因此它总是比cursor少1。</p>
<p>变量cursor和集合的元素个数决定hasNext()：</p>
<pre class="brush:java">
public boolean hasNext() {
    return cursor != size();
}
</pre>
<p>方法next()返回的是索引为cursor的元素，然后修改cursor和lastRet的值：</p>
<pre class="brush:java">
public Object next() {
    checkForComodification();
    try {
        Object next = get(cursor);
        lastRet = cursor++;
        return next;
    } catch(IndexOutOfBoundsException e) {
        checkForComodification();
        throw new NoSuchElementException();
    }
}
</pre>
<p>expectedModCount表示期待的modCount值，用来判断在遍历过程中集合是否被修改过。AbstractList包含一个modCount变量，它的初始值是0，当集合每被修改一次时（调用add，remove等方法），modCount加1。因此，modCount如果不变，表示集合内容未被修改。</p>
<p>Itr初始化时用expectedModCount记录集合的modCount变量，此后在必要的地方它会检测modCount的值：</p>
<pre class="brush:java">
final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}
</pre>
<p>如果modCount与一开始记录在expectedModeCount中的值不等，说明集合内容被修改过，此时会抛出ConcurrentModificationException。</p>
<p>这个ConcurrentModificationException是RuntimeException，不要在客户端捕获它。如果发生此异常，说明程序代码的编写有问题，应该仔细检查代码而不是在catch中忽略它。</p>
<p>但是调用Iterator自身的remove()方法删除当前元素是完全没有问题的，因为在这个方法中会自动同步expectedModCount和modCount的值：</p>
<pre class="brush:java">
public void remove() {
    ...
    AbstractList.this.remove(lastRet);
    ...
    // 在调用了集合的remove()方法之后重新设置了expectedModCount：
    expectedModCount = modCount;
    ...
}
</pre>
<p>要确保遍历过程顺利完成，必须保证遍历过程中不更改集合的内容（Iterator的remove()方法除外），因此，确保遍历可靠的原则是只在一个线程中使用这个集合，或者在多线程中对遍历代码进行同步。</p>
<p>最后给个完整的示例：</p>
<pre class="brush:java">
Collection c = new ArrayList();
c.add(&quot;abc&quot;);
c.add(&quot;xyz&quot;);
for(Iterator it = c.iterator(); it.hasNext(); ) {
    String s = (String)it.next();
    System.out.println(s);
}
</pre>
<p>如果你把第一行代码的ArrayList换成LinkedList或Vector，剩下的代码不用改动一行就能编译，而且功能不变，这就是针对抽象编程的原则：对具体类的依赖性最小。</p>]]></description></item>
<item><title>用于MIDP的URLEncoder类</title><link>http://www.liaoxuefeng.com/it-806bd479c5c340d5b57ac76334c3ff9c-1</link><dc:creator>廖雪峰</dc:creator><pubDate>Sun, 12 Jul 2009 22:25:15 +0800</pubDate><description><![CDATA[<p>由于MIDP没有J2SE对应的java.net.URLEncoder类，因此，要向服务器发送HTTP请求，必须自己进行URL编码，参考JDK1.4.2的src代码，将其改为一个能用在MIDP环境中的URLEncoder类，源码如下：</p>
<pre class="brush:java">
package com.liaoxuefeng.javame.util;

import java.io.*;

/**
 * Encode url, just like java.net.URLEncoder.encode() in J2SE.
 * NOTE: This class is modified from java.net.URLEncoder class in J2SE 1.4.
 */
public class URLEncoder {

    private static final int MAX_BYTES_PER_CHAR = 10; // rather arbitrary limit, but safe for now

    private static boolean[] dontNeedEncoding;
    private static final int caseDiff = ('a' - 'A');

    static {
        dontNeedEncoding = new boolean[256];
        for (int i='a'; i&lt;='z'; i++) {
            dontNeedEncoding[i] = true;
        }
        for (int i='A'; i&lt;='Z'; i++) {
            dontNeedEncoding[i] = true;
        }
        for (int i='0'; i&lt;='9'; i++) {
            dontNeedEncoding[i] = true;
        }
        dontNeedEncoding[' '] = true;
        dontNeedEncoding['-'] = true;
        dontNeedEncoding['_'] = true;
        dontNeedEncoding['.'] = true;
        dontNeedEncoding['*'] = true;
    }

    private URLEncoder() {}

    public static String encode(String s) {
        boolean wroteUnencodedChar = false;

        StringBuffer out = new StringBuffer(s.length());
        ByteArrayOutputStream buf = new ByteArrayOutputStream(MAX_BYTES_PER_CHAR);
        OutputStreamWriter writer = new OutputStreamWriter(buf);

        for (int i = 0; i &lt; s.length(); i++) {
            int c = (int) s.charAt(i);
            if (c&lt;256 &amp;&amp; dontNeedEncoding[c]) {
                out.append((char) (c==' ' ? '+' : c));
                wroteUnencodedChar = true;
            } else {
                // convert to external encoding before hex conversion
                try {
                    if (wroteUnencodedChar) {
                        writer = new OutputStreamWriter(buf);
                        wroteUnencodedChar = false;
                    }
                    writer.write(c);
                    /*
                     * If this character represents the start of a Unicode
                     * surrogate pair, then pass in two characters. It's not
                     * clear what should be done if a bytes reserved in the
                     * surrogate pairs range occurs outside of a legal surrogate
                     * pair. For now, just treat it as if it were any other
                     * character.
                     */
                    if (c &gt;= 0xD800 &amp;&amp; c &lt;= 0xDBFF) {
                        if ((i + 1) &lt; s.length()) {
                            int d = (int) s.charAt(i + 1);
                            if (d &gt;= 0xDC00 &amp;&amp; d &lt;= 0xDFFF) {
                                writer.write(d);
                                i++;
                            }
                        }
                    }
                    writer.flush();
                } catch (IOException e) {
                    buf.reset();
                    continue;
                }
                byte[] ba = buf.toByteArray();
                for (int j = 0; j &lt; ba.length; j++) {
                    out.append('%');
                    char ch = toHex((ba[j] &gt;&gt; 4) &amp; 0xF);
                    // converting to use uppercase letter as part of
                    // the hex value if ch is a letter.
                    if (isLetter(ch)) {
                        ch -= caseDiff;
                    }
                    out.append(ch);
                    ch = toHex(ba[j] &amp; 0xF);
                    if (isLetter(ch)) {
                        ch -= caseDiff;
                    }
                    out.append(ch);
                }
                buf.reset();
            }
        }
        return out.toString();
    }

    private static char toHex(int digit) {
        if ((digit &gt;= 16) || (digit &lt; 0)) {
            return '\0';
        }
        if (digit &lt; 10) {
            return (char)('0' + digit);
        }
        return (char)('a' - 10 + digit);
    }

    private static boolean isLetter(char c) {
        return (c&gt;='a' &amp;&amp; c&lt;='z');
    }
}
</pre>
<p>J2SE的URLEncoder依赖于java.lang.Character的许多特性，将其全部剔除并改造为基本运算，即可在MIDP环境中使用，中文测试也一切正常。</p>]]></description></item>
<item><title>一个拼图游戏的开发</title><link>http://www.liaoxuefeng.com/it-732b1015bf774660818e25f768b9eda2-1</link><dc:creator>廖雪峰</dc:creator><pubDate>Sun, 12 Jul 2009 21:47:41 +0800</pubDate><description><![CDATA[<p>MIDP规范的出现使得我们在手机上开发Java游戏成为可能。今天我们要实现的是一个简单的拼图游戏。这个拼图游戏是一个3x3的拼图，由9个分割的小图片构成。这样，在手机上，就可以用按键1-9对应每个图片。需要移动某个图片时，用户只需要按下对应的数字键即可，非常方便。当然，对于键盘设计不规则的手机来说，就只能委屈了。当用户按下0键时，则显示整个原始图片。</p>
<p>虽然MIDP提供了许多高级和低级的UI API接口，但是整个MIDP应用程序的结构设计仍然至关重要，一个灵活的框架能大大降低游戏开发的复杂度。</p>
<p>MVC模式几乎是UI应用程序开发的标准模式了，通过Model-View-Controller的分工合作，使得整个应用程序的不同功能部分被分离开来，从而降低开发难度。</p>
<p>MVC有MVC1和MVC2两种模式，其不同之处在于Model能否主动通知View。在普通的Windows窗口程序中，Model可以主动通知View是否需要Update，因此应使用MVC1；在Web程序中，由于HTTP协议的限制，服务器端的Model无法主动通知View（如JSP页面），因此只能使用MVC2，由Controller取得Model并渲染View。</p>
<p>在窗口应用程序中，View通常仅有一个，但Model可能有很多；而在Web程序中，Model通常被放在服务器端，每一个JSP页面都是一个View，因此View有很多个。</p>
<p>微软的MFC框架也是一个基于MVC模式的框架，其View-Document框架是专门针对桌面应用程序设计的，因此，我们在MIDP程序中也可借鉴其思想。</p>
<p>在MIDP程序中，MIDlet起着Controller的作用，每个Screen或者Canvas就是一个View，而Model可以用一个单独的类来表示，用于存储程序运行中的数据。对于这个拼图游戏来说，设计以下几个类：</p>
<ul>
    <li>PuzzleMIDlet：控制整个游戏的生命周期，也是应用程序的入口；</li>
    <li>MainCanvas：绘制游戏的主屏幕，完成所有绘图操作；</li>
    <li>Document：存储游戏运行过程中的数据，并负责通知屏幕更新。</li>
</ul>
<p>当用户通过MainCanvas输入命令后（例如，按下0-9的某个键），将可能引起Document数据的更新，如果需要更新屏幕，则Document应通知View更新显示，这是一个Observer模式的典型应用。</p>
<p>由于这个拼图游戏不需要频繁地更新画面，因此，连多线程也不必使用了，这样就大大简化了游戏逻辑的设计。下面是这个拼图游戏运行在真实手机上的效果图：</p>
<p><img alt="" width="256" height="192" src="/upload/178/16/65/31f16ca95e1e4d5bbd26b91c21111874.jpg" /></p>
<p>由于公司的手机还停留在CF62 / MIDP1.0的水平，因此，只好用MIDP1.0来编写这个拼图游戏了。不过好在我们的重点不是在如何绘制Canvas上，因此，MIDP2.0中提供的新的Game API绝大部分都用不上。</p>
<p>下面，我们开始设计每个类，并实现整个完整的游戏逻辑。</p>
<h3 style="color: red">设计Document类</h3>
<p>Document类需要保存游戏运行中所有的状态数据，对于这个拼图游戏来说，我们设计以下成员变量：</p>
<pre class="brush:java">
Updatable updatable;
int state;
Image[] images = new Image[9];
int[][] current = new int[3][3];
int hiddenX, hiddenY;
int steps; // 移动的步数
</pre>
<p>MainCanvas需要实现Updatable接口，因此，Document保存了一个View的引用，在恰当的时候，Document可以调用updatable.update()方法通知View需要重绘。这样，MainCanvas和Document就实现了Observer模式。</p>
<p>游戏中，state用于存储游戏状态，一共有3种状态：</p>
<ul>
    <li>PUZZLE_STATE：表示用户正在进行拼图中；</li>
    <li>IMAGE_STATE：表示用户正在查看原始图片；</li>
    <li>FINISH_STATE：表示用户已经完成拼图。</li>
</ul>
<p>images数组按次序存储原始图片，我们把这个90x90大小的原始图片&amp;lt;image&amp;gt;切割成9个30x30的小图片，并依次编号0-8：</p>
<p><img alt="" width="288" height="46" src="/upload/169/24/80/a8e279f93be1432dbe13990a6245a9ee.jpg" /></p>
<p>current[3][3]是一个二维数组，存储Image在images[]数组中的索引号，这样就可以从current[][]中获得对应的Image对象。</p>
<p>hiddenX和hiddenY用来标识空白方格的位置。仅当位于(hiddenX, hiddenY)上下左右的方格可以移动。</p>
<h3 style="color: red">初始化current</h3>
<p>为了打乱一个拼好的方格，我们需要一个算法来随机打乱9个方格。在我们想出这个算法前，最简单的方法便是用一个可拼好的数据来写死current[][]，使得我们能集中精力先把游戏的框架搭起来：</p>
<pre class="brush:java">
current = new int[][] {
    {2, 7, 5},
    {1, 0, 6},
    {4, 3, 8}
}
</pre>
<p>然后设定hiddenX=2, hiddenY=2，使得右下角current[2][2]的方格被隐藏。</p>
<p>要取得某个方格对应的Image对象，我们用</p>
<pre class="brush:java">
public Image getCurrentImage(int x, int y) {
    if( (x==hiddenX) &amp;&amp; (y==hiddenY) )
        return null;
    return images[current[x][y]];
}
</pre>
<p>对于位于(hiddenX, hiddenY)位置的方格，返回null表示不显示该方格。</p>
<p>如何判断拼图是否完成？当current[][]数组的内容按照{0, 1, 2}, {3, 4, 5}, {6, 7, 8}排列时，表示该拼图已经拼好，因此，判断代码非常简单：</p>
<pre class="brush:java">
public boolean isFinish() {
    for(int i=0; i&lt;3; i++) {
        for(int j=0; j&lt;3; j++) {
            if(current[i][j]!=(i*3+j))
                return false;
        }
    }
    return true;
}
</pre>
<p>当用户移动某个方格时，Document接收方格位置(x, y)并负责判断能否移动，如果能，更新current[][]的数据和hiddenX, hiddenY，并返回true表示数据已更新，否则返回false表示不可移动。</p>
<pre class="brush:java">
public boolean move(int x, int y) {
    // 如果用户试图移动隐藏方格，直接返回false：
    if(hiddenX==x &amp;&amp; hiddenY==y)
        return false;
    // 如果方格位于(hiddexX, hiddenY)的相邻位置，
    // 交换该方格(x, y)和(hiddenX, hiddenY)的相关数据：
    boolean moved = false;
    if( ((x-1)==hiddenX) &amp;&amp; (y==hiddenY) ) {
        sweep(x, y);
        moved = true;
    }
    if( ((x+1)==hiddenX) &amp;&amp; (y==hiddenY) ) {
        sweep(x, y);
        moved = true;
    }
    if( (x==hiddenX) &amp;&amp; ((y-1)==hiddenY) ) {
        sweep(x, y);
        moved = true;
    }
    if( (x==hiddenX) &amp;&amp; ((y+1)==hiddenY) ) {
        sweep(x, y);
        moved = true;
    }
    if(moved) {
        steps++;
        if(isFinish()) {
            // TODO...
        }
        updatable.update();
    }
}

private void sweep(int x, int y) {
    int temp = current[x][y];
    current[x][y] = current[hiddenX][hiddenY];
    current[hiddenX][hiddenY] = temp;
    hiddenX = x;
    hiddenY = y;
}
</pre>
<p>至此，Document类基本完成。Document不涉及任何显示功能，仅仅存储和更新数据，并在恰当的时候通知View更新显示。</p>
<h3 style="color: red">实现View</h3>
<p>在MIDP中，View就是Screen或者Canvas，在这个游戏中，我们应该使用Canvas，定义：</p>
<pre class="brush:java">
public class MainCanvas extends Canvas implements CommandListener, Updatable { ... }
</pre>
<p>在构造方法中，初始化Document：</p>
<pre class="brush:java">
public MainCanvas(String imageName) {
    // 读图像：
    Image[] images = new Image[9];
    for(int i=0; i&lt;9; i++) {
        try {
            images[i] = Image.createImage(&quot;/image/&quot; + i + &quot;.png&quot;);
        }
        catch(IOException ioe) {}
    }
    document = new Document(this, images, 2, 2);
}
</pre>
<p>在paint()方法中，MainCanvas从Document中获得数据，然后更新画面：</p>
<pre class="brush:java">
protected void paint(Graphics g) {
    g.fillRect(0,0,getWidth(),getHeight());
    // 获得当前状态：
    int state = document.getState();
    if(state==Document.PUZZLE_STATE) {
        for(int x=0; x&lt;3; x++) {
            for(int y=0; y&lt;3; y++) {
                Image image = document.getImage(x, y);
                if(image!=null) {
                    g.drawImage(image, y*IMAGE_WIDTH, x*IMAGE_WIDTH, Graphics.LEFT|Graphics.TOP);
                }
                else {
                    g.setColor(0x000000);
                    g.fillRect(y*IMAGE_WIDTH, x*IMAGE_WIDTH, IMAGE_WIDTH, IMAGE_WIDTH);
                }
            }
        }
        // draw line:
        g.setColor(0xffffff);
        for(int i=0; i&lt;=3; i++) {
            g.drawLine(0, i*IMAGE_WIDTH, 3*IMAGE_WIDTH, i*IMAGE_WIDTH);
            g.drawLine(i*IMAGE_WIDTH, 0, i*IMAGE_WIDTH, 3*IMAGE_WIDTH);
        }
    }
    else {
        // TODO...
    }
}
</pre>
<p>当用户按下某个键时，MainCanvas的keyPressed()方法被执行，然后将用户输入数据传递给Document：</p>
<pre class="brush:java">
protected void keyPressed(int keyCode) {
    switch(keyCode) {
    case KEY_NUM1:
        document.move(0,0);
        break;
    case KEY_NUM2:
        document.move(0,1);
        break;
    case KEY_NUM3:
        document.move(0,2);
        break;
    // TODO: case KEY_NUM4, 5, 6...
    }
}
</pre>
<p>然后，Document可能更新自身内部状态，如果需要重绘画面，Document将调用update()回调方法来通知View更新画面。因此，MainCanvas必须实现Updatable接口的update()回调方法：</p>
<pre class="brush:java">
public void update() {
    repaint();
}
</pre>
<p>至此，View已基本实现，我们再添加一个用作启动的MIDlet，即可实现整个游戏的基本框架。</p>
<p>游戏源码和二进制包下载：</p>
<p><a target="_blank" href="http://javaeedev.googlecode.com/files/Puzzle.zip">http://javaeedev.googlecode.com/files/Puzzle.zip</a></p>]]></description></item>
<item><title>JDK源码分析：java.lang.Boolean</title><link>http://www.liaoxuefeng.com/it-f0693632954847f49f35548f917dee29-1</link><dc:creator>廖雪峰</dc:creator><pubDate>Sun, 12 Jul 2009 21:24:22 +0800</pubDate><description><![CDATA[<p>闲来无事，开始研究JDK源码（JDK 1.5），先找了一个最简单的java.lang.Boolean开始解剖。</p>
<p>首先我们剔除所有的方法和静态变量，Boolean的核心代码如下：</p>
<pre class="brush:java">
public final class Boolean implements java.io.Serializable,Comparable {
    private final boolean value;
}
</pre>
<p>很明显，凡是成员变量都是final类型的，一定是immutable class，这个Boolean和String一样，一旦构造函数执行完毕，实例的状态就不能再改变了。</p>
<p>Boolean的构造方法有两个：</p>
<pre class="brush:java">
public Boolean(boolean value) {
    this.value = value;
}

public Boolean(String s) {
    this(toBoolean(s));
}
</pre>
<p>另外注意到Boolean类实际上只有两种不同状态的实例：一个包装true，一个包装false，Boolean又是immutable class，所以在内存中相同状态的Boolean实例完全可以共享，不必用new创建很多实例。因此Boolean class还提供两个静态变量：</p>
<pre class="brush:java">
public static final Boolean TRUE = new Boolean(true);
public static final Boolean FALSE = new Boolean(false);
</pre>
<p>这两个变量在Class Loader装载时就被实例化，并且申明为final，不能再指向其他实例。</p>
<p>提供这两个静态变量是为了让开发者直接使用这两个变量而不是每次都new一个Boolean，这样既节省内存又避免了创建一个新实例的时间开销。</p>
<p>因此，用</p>
<pre class="brush:java">
Boolean b = Boolean.TRUE;
</pre>
<p>比</p>
<pre class="brush:java">
Boolean b = new Boolean(true);
</pre>
<p>要好得多。</p>
<p>如果遇到下面的情况：</p>
<pre class="brush:java">
Boolean b = new Boolean(var);
</pre>
<p>一定要根据一个boolean变量来创建Boolean实例怎么办？推荐使用Boolean提供的静态工厂方法：</p>
<pre class="brush:java">
Boolean b = Boolean.valueOf(var);
</pre>
<p>这样就可以避免创建新的实例。看看valueOf()静态方法：</p>
<pre class="brush:java">
public static Boolean valueOf(boolean b) {
    return (b ? TRUE : FALSE);
}
</pre>
<p>这个静态工厂方法返回的仍然是两个静态变量TRUE和FALSE之一，而不是new一个Boolean出来。虽然Boolean非常简单，占用的内存也很少，但是一个复杂的类用new创建实例的开销可能非常大，而且，使用工厂方法可以方便的实现缓存实例，这对客户端是透明的。所以，能用工厂方法就不要使用new。</p>
<p>和Boolean只有两种状态不同，Integer也是immutable class，但是状态上亿种，不可能用静态实例缓存所有状态。不过，SUN的工程师还是作了一点优化，Integer类缓存了-128到127这256个状态的Integer，如果使用Integer.valueOf(int i)，传入的int范围正好在此内，就返回静态实例。</p>
<p>hashCode()方法很奇怪，两种Boolean的hash code分别是1231和1237。估计写Boolean.java的人对这两个数字有特别偏好：</p>
<pre class="brush:java">
public int hashCode() {
    return value ? 1231 : 1237;
}
</pre>
<p>equals()方法也很简单，只有Boolean类型的Object并且value相等才返true：</p>
<pre class="brush:java">
public boolean equals(Object obj) {
    if (obj instanceof Boolean) {<br />
        return value == ((Boolean)obj).booleanValue();
    }
    return false;
}
</pre>
<p>很多人写equals()总是在第一行写一个null判断：</p>
<pre class="brush:java">
if (obj==null)
    return false;
</pre>
<p>其实完全没有必要，因为如果obj==null，下一行的</p>
<pre class="brush:java">
if (obj instanceof Type)
</pre>
<p>就肯定返回false，因为(null instanceof AnyType)永远是false。</p>
<p>详细内容请参考《Effective Java》第7条：Obey the general contract when overriding equals。</p>
<h3 style="color: red">总结</h3>
<p>如果一个类只有有限的几种状态，考虑用几个final的静态变量来表示不同状态的实例。例如编写一个Weekday类，状态只有7个，就不要让用户写new Weekday(1)，直接提供Weekday.MONDAY即可。</p>
<p>要防止用户使用new生成实例，就取消public构造方法，用户要获得静态实例的引用有两个方法：如果申明了public static var，就可以直接访问，比如Boolean.TRUE，第二个方法是通过静态工厂方法：Boolean.valueOf(?)。</p>
<p>如果不提供public构造方法，让用户只能通过上面的方法获得静态变量的引用，还可以大大简化equals()方法：</p>
<pre class="brush:java">
public boolean equals(Object obj) {
    return this==obj;
}
</pre>]]></description></item>
<item><title>使用FileUpload组件实现文件上传</title><link>http://www.liaoxuefeng.com/it-c8de7bf6ed1a4118a3e644f2f8af29eb-1</link><dc:creator>廖雪峰</dc:creator><pubDate>Fri, 10 Jul 2009 22:29:38 +0800</pubDate><description><![CDATA[<p>文件上传在Web应用中非常普遍，要在Java Web环境中实现文件上传功能非常容易，因为网上已经有许多用Java开发的组件用于文件上传，本文以使用最普遍的commons-fileupload组件为例，演示如何为Java Web应用添加文件上传功能。</p>
<p>commons-fileupload组件是Apache的一个开源项目之一，可以从<a target="_blank" href="http://commons.apache.org/fileupload/">http://commons.apache.org/fileupload/</a>下载。该组件简单易用，可实现一次上传一个或多个文件，并可限制文件大小。</p>
<p>下载后解压zip包，将commons-fileupload-1.x.jar复制到tomcat的webapps/你的webapp/WEB-INF/lib/下，如果目录不存在请自建目录。</p>
<p>新建一个UploadServlet.java用于文件上传：</p>
<pre class="brush:java">
package com.liaoxuefeng.web;

public class FileUploadServlet extends HttpServlet {
    private String uploadDir = &quot;C:\\temp&quot;;
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException
    {
        // TODO:
    }
}
</pre>
<p>当servlet收到浏览器发出的Post请求后，在doPost()方法中实现文件上传，我们需要遍历FileItemIterator，获得每一个FileItemStream：</p>
<pre class="brush:java">
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException
{
    try {
        ServletFileUpload upload = new ServletFileUpload();
        // set max file size to 1 MB:
        upload.setFileSizeMax(1024 * 1024);
        FileItemIterator it = upload.getItemIterator(req);
        // handle with each file:
        while (it.hasNext()) {
            FileItemStream item = it.next();
            if (! item.isFormField()) {
                // it is a file upload:
                handleFileItem(item);
            }
        }
        req.getRequestDispatcher(&quot;success.jsp&quot;).forward(req, resp);
    }
    catch(FileUploadException e) {
        throw new ServletException(&quot;Cannot upload file.&quot;, e);
    }
}
</pre>
<p>在handleFileItem()方法中读取上传文件的输入流，然后写入到uploadDir中，文件名通过UUID随机生成：</p>
<pre class="brush:java">
void handleFileItem(FileItemStream item) throws IOException {
    System.out.println(&quot;upload file: &quot; + item.getName());
    File newUploadFile = new File(uploadDir + &quot;/&quot; + UUID.randomUUID().toString());
    byte[] buffer = new byte[4096];
    InputStream input = null;
    OutputStream output = null;
    try {
        input = item.openStream();
        output = new BufferedOutputStream(new FileOutputStream(newUploadFile));
        for (;;) {
            int n = input.read(buffer);
            if (n==(-1))
                break;
            output.write(buffer, 0, n);
        }
    }
    finally {
        if (input!=null) {
            try {
                input.close();
            }
            catch (IOException e) {}
        }
        if (output!=null) {
            try {
                output.close();
            }
            catch (IOException e) {}
        }
    }
}
</pre>
<p>如果要在web.xml配置文件中读取指定的上传文件夹，可以在init()方法中初始化：</p>
<pre class="brush:java">
@Override
public void init(ServletConfig config) throws ServletException {
    super.init(config);
    this.uploaddir = config.getInitParameter("dir");
}
</pre>
<p>最后在web.xml中配置Servlet：</p>
<pre class="brush:xml">
&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&lt;!DOCTYPE web-app
    PUBLIC &quot;-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN&quot;
    &quot;http://java.sun.com/dtd/web-app_2_3.dtd&quot;&gt;
&lt;web-app&gt;
    &lt;servlet&gt;
        &lt;servlet-name&gt;UploadServlet&lt;/servlet-name&gt;
        &lt;servlet-class&gt;com.liaoxuefeng.web.FileUploadServlet&lt;/servlet-class&gt;
    &lt;/servlet&gt;
    &lt;servlet-mapping&gt;
        &lt;servlet-name&gt;UploadServlet&lt;/servlet-name&gt;
        &lt;url-pattern&gt;/upload&lt;/url-pattern&gt;
    &lt;/servlet-mapping&gt;
&lt;/web-app&gt;
</pre>
<p>配置好Servlet后，启动Tomcat或Resin，写一个简单的index.htm测试：</p>
<pre class="brush:html">
&lt;html&gt;
&lt;body&gt;
&lt;p&gt;FileUploadServlet Demo&lt;/p&gt;
&lt;form name=&quot;form1&quot; action=&quot;upload&quot; method=&quot;post&quot; enctype=&quot;multipart/form-data&quot;&gt;
    &lt;input type=&quot;file&quot; name=&quot;file&quot; /&gt;
    &lt;input type=&quot;submit&quot; name=&quot;button&quot; value=&quot;Submit&quot; /&gt;
&lt;/form&gt;
&lt;/body&gt;
&lt;/html&gt;
</pre>
<p>注意action=&quot;upload&quot;指定了处理上传文件的FileUploadServlet的映射URL。</p>
<p>当上传成功后，显示success.jsp，否则，抛出异常。如果上传的文件大小超过了我们设定的1MB，就会得到一个FileSizeLimitExceededException。</p>
<p>下载完整的源代码：<a href="http://javaeedev.googlecode.com/files/FileUpload.zip">http://javaeedev.googlecode.com/files/FileUpload.zip</a></p>]]></description></item>
<item><title>Spring 2.0核心技术与最佳实践</title><link>http://www.liaoxuefeng.com/it-d51d136777c74d2e85e99bab8280b0c9-1</link><dc:creator>廖雪峰</dc:creator><pubDate>Wed, 08 Jul 2009 22:13:55 +0800</pubDate><description><![CDATA[<p><img width="350" height="490" alt="" src="/upload/49/170/176/b2cd49cbb27c4386a56b85967ae37fbc.jpg" /></p>
<p>书名：Spring 2.0核心技术与最佳实践</p>
<p>作者：<a target="_blank" href="http://www.liaoxuefeng.com">廖雪峰</a></p>
<p>出版社：电子工业出版社</p>
<p>书号：9787121042621</p>
<p>出版日期：2007年6月</p>
<p>内容简介：</p>
<p>本书注重实践而又深入理论，由浅入深且详细介绍了Spring 2.0框架的几乎全部的内容，并重点突出2.0版本的新特性。本书将为读者展示如何应用Spring 2.0框架创建灵活高效的JavaEE应用，并提供了一个真正可直接部署的完整的Web应用程序&mdash;&mdash;<a target="_blank" href="http://www.livebookstore.net">Live在线书店</a>。</p>
<p>在介绍Spring框架的同时，本书还介绍了与Spring相关的大量第三方框架，涉及领域全面，实用性强。本书另一大特色是实用性强，易于上手，以实际项目为出发点，介绍项目开发中应遵循的最佳开发模式。</p>
<p>本书还介绍了大量实践性极强的例子，并给出了完整的配置步骤，几乎覆盖了Spring 2.0版本的所有新特性。</p>
<p>本书适合有一定Java基础的读者，对JavaEE开发人员特别有帮助。本书既可以作为Spring 2.0的学习指南，也可以作为实际项目开发的参考手册。</p>
<p>编辑推荐：被China-Pub会员评为&ldquo;<a target="_blank" href="http://www.china-pub.com/STATIC07/0801/zh_px_080116.asp">2007年我最喜爱的十大技术图书</a>&rdquo;之一。</p>]]></description></item>
<item><title>Symbian OS J2ME编程指南</title><link>http://www.liaoxuefeng.com/it-ba84bf6348a04814b4a5cd05c217043f-1</link><dc:creator>廖雪峰</dc:creator><pubDate>Wed, 08 Jul 2009 22:01:23 +0800</pubDate><description><![CDATA[<p><img alt="" width="350" height="442" src="/upload/70/220/191/4781c345a9054fa3a6ade7d17274bf31.jpg" /></p>
<p>书名：Symbian OS J2ME编程指南</p>
<p>原书名：Programming Java 2 Micro Edition for Symbian OS</p>
<p>作者：Martin de Jode</p>
<p>译者：<a target="_blank" href="http://www.j2medev.com">詹建飞</a> <a target="_blank" href="http://www.liaoxuefeng.com">廖雪峰</a></p>
<p>出版社：人民邮电出版社</p>
<p>书号：7115136866</p>
<p>出版日期：2005年10月</p>
<p>内容简介：</p>
<p>本书介绍在Symbian操作系统上的J2ME编程，尤其是针对MIDP 2.0的编程。</p>
<p>全书共分3个部分，5个附录。第一部分包括第5章，介绍J2ME配置和简表的意义，然后集中说明新一代Symbian操作系统手机上构成Java平台的MIDP和附加API。第二部分包括第6章和第7章，研究编写高质量代码在设计和实现中的考虑。第三部分是第8章，介绍Java对无线生态系统的战略，并对Java在Symbian操作系统上的发展方向给出大概的描述。附录部分分别介绍了CLDC核心库、MIDP库、使用Wireless Toolkit的命令行工具、开发者资源和参考文献，以及Symbian系统手机规范。</p>
<p>本书适合于Symbian系统下进行J2ME应用开发的人员阅读，它能为开发者展示如何最大限度地发挥新一代Symbian操作系统手机的功能。本书也可作为Symbian系统下J2ME编程的教材和参考书。</p>]]></description></item>
<item><title>培训介绍</title><link>http://www.liaoxuefeng.com/it-92d3b12cd8234d45b311c86ac615e967-1</link><dc:creator>廖雪峰</dc:creator><pubDate>Thu, 01 Jan 2009 22:23:18 +0800</pubDate><description><![CDATA[<p>长期为企业提供软件开发类培训，包括：</p>
<h3 style="color: red; ">JavaEE平台：</h3>
<p><span style="color: rgb(255, 0, 0); "><strong>Spring培训</strong></span>：覆盖Spring 2.x/3.0的基础（IoC，AOP），Web开发（集成Struts，WebWork，Velocity等常见框架），访问和发布Web Services，第三方组件集成（任务调度，邮件服务，消息服务等），缓存设计，分层模型设计与最佳实践。</p>
<p><span style="color: rgb(255, 0, 0); "><strong>Hibernate培训</strong></span>：覆盖Hibernate 3.x的基础（ORM映射），各种复杂映射，JPA Annotation应用，Sharding设计，JDBC集成，性能分析与优化以及最佳实践。</p>
<p><span style="color: rgb(255, 0, 0); "><strong>JUnit培训</strong></span>：覆盖JUnit 3.x/4.x的基础测试，数据库测试，Web测试，代码覆盖率测试，以及Ant集成和自动化测试的最佳实践。</p>
<p><span style="color: rgb(255, 0, 0); "><strong>Lucene培训</strong></span>：讲解全文搜索原理，覆盖Lucene 3.0/Compass的基础，如何快速实现站内搜索以及搜索优化。</p>
<p><span style="color: rgb(255, 0, 0); "><strong>Android培训</strong></span>：讲解Android平台基础，UI设计，多线程，网络连接，XML读写，SQLite数据库操作，多媒体操作，传感器访问，游戏设计基础，Android组件生命周期与设计，以及最佳实践。</p>
<p>&nbsp;</p>
<h3 style="color: red; ">Python培训：</h3>
<p>覆盖Python2.5/2.6基础，和静态语言相比较的特点，Web开发，数据库开发，AppEngine开发以及动态语言的最佳实践。</p>
<p>&nbsp;</p>
<p>实际课程内容可根据客户需求定制，有意请联系：<a target="_blank" href="mailto:askxuefeng@gmail.com?subject=About%20Training">askxuefeng@gmail.com</a></p>]]></description></item>
<item><title>About Michael Liao</title><link>http://www.liaoxuefeng.com/it-4c67ceed5dab4a3198de630de7847c5b-1</link><dc:creator>廖雪峰</dc:creator><pubDate>Thu, 01 Jan 2009 10:25:18 +0800</pubDate><description><![CDATA[<p>Hi, I'm Michael Liao, a software engineer in China.</p>
<p>I have 5 years experiences in software development, especially Java and Python.</p>
<table width="100%" cellspacing="0" cellpadding="3" border="0">
    <tbody>
        <tr>
            <td width="20%">&nbsp;</td>
            <td width="80%">&nbsp;</td>
        </tr>
        <tr>
            <td colspan="2">
            <h3 style="color: Red;">Profile</h3>
            </td>
        </tr>
        <tr>
            <td style="text-align: right;">Chinese Name:</td>
            <td>Liao Xuefeng</td>
        </tr>
        <tr>
            <td style="text-align: right;">English Name:</td>
            <td>Michael Liao</td>
        </tr>
        <tr>
            <td style="text-align: right;">Email:</td>
            <td>(hidden)</td>
        </tr>
        <tr>
            <td style="text-align: right;">Mobile:</td>
            <td>(hidden)</td>
        </tr>
        <tr>
            <td style="text-align: right;">Chinese Blog:</td>
            <td><a target="_blank" href="http://www.liaoxuefeng.com">http://www.liaoxuefeng.com</a></td>
        </tr>
        <tr>
            <td style="text-align: right;">English Blog:</td>
            <td><a target="_blank" href="http://michael.liaoxuefeng.com">http://michael.liaoxuefeng.com</a></td>
        </tr>
        <tr>
            <td>&nbsp;</td>
            <td>&nbsp;</td>
        </tr>
        <tr>
            <td colspan="2">
            <h3 style="color: Red;">Education</h3>
            </td>
        </tr>
        <tr>
            <td style="text-align: right;">2000.9 - 2004.7</td>
            <td><a target="_blank" href="http://www.bupt.edu.cn"><strong>Beijing University of Posts &amp; Telecommunications</strong></a></td>
        </tr>
        <tr>
            <td>&nbsp;</td>
            <td>Bachelor Degree, major in Information Engineering.</td>
        </tr>
        <tr>
            <td>&nbsp;</td>
            <td>&nbsp;</td>
        </tr>
        <tr>
            <td colspan="2">
            <h3 style="color: Red;">Software Development Skills</h3>
            </td>
        </tr>
        <tr>
            <td style="text-align: right;">Java / J2EE:</td>
            <td><img width="16" height="16" alt="" src="/upload/248/65/221/1d6ff70f81bc4629acaf77832c4a48f2.gif" /><img width="16" height="16" alt="" src="/upload/248/65/221/1d6ff70f81bc4629acaf77832c4a48f2.gif" /><img width="16" height="16" alt="" src="/upload/248/65/221/1d6ff70f81bc4629acaf77832c4a48f2.gif" /><img width="16" height="16" alt="" src="/upload/248/65/221/1d6ff70f81bc4629acaf77832c4a48f2.gif" /><img width="16" height="16" alt="" src="/upload/248/65/221/1d6ff70f81bc4629acaf77832c4a48f2.gif" /></td>
        </tr>
        <tr>
            <td style="text-align: right;">Android / J2ME:</td>
            <td><img width="16" height="16" alt="" src="/upload/248/65/221/1d6ff70f81bc4629acaf77832c4a48f2.gif" /><img width="16" height="16" alt="" src="/upload/248/65/221/1d6ff70f81bc4629acaf77832c4a48f2.gif" /><img width="16" height="16" alt="" src="/upload/248/65/221/1d6ff70f81bc4629acaf77832c4a48f2.gif" /><img width="16" height="16" alt="" src="/upload/248/65/221/1d6ff70f81bc4629acaf77832c4a48f2.gif" /><img width="16" height="16" alt="" src="/upload/248/65/221/1d6ff70f81bc4629acaf77832c4a48f2.gif" /></td>
        </tr>
        <tr>
            <td style="text-align: right;">Python:</td>
            <td><img width="16" height="16" alt="" src="/upload/248/65/221/1d6ff70f81bc4629acaf77832c4a48f2.gif" /><img width="16" height="16" alt="" src="/upload/248/65/221/1d6ff70f81bc4629acaf77832c4a48f2.gif" /><img width="16" height="16" alt="" src="/upload/248/65/221/1d6ff70f81bc4629acaf77832c4a48f2.gif" /><img width="16" height="16" alt="" src="/upload/248/65/221/1d6ff70f81bc4629acaf77832c4a48f2.gif" /><img width="16" height="16" alt="" src="/upload/248/65/221/1d6ff70f81bc4629acaf77832c4a48f2.gif" /></td>
        </tr>
        <tr>
            <td style="text-align: right;">Visual Basic:</td>
            <td><img width="16" height="16" alt="" src="/upload/248/65/221/1d6ff70f81bc4629acaf77832c4a48f2.gif" /><img width="16" height="16" alt="" src="/upload/248/65/221/1d6ff70f81bc4629acaf77832c4a48f2.gif" /><img width="16" height="16" alt="" src="/upload/248/65/221/1d6ff70f81bc4629acaf77832c4a48f2.gif" /><img width="16" height="16" alt="" src="/upload/248/65/221/1d6ff70f81bc4629acaf77832c4a48f2.gif" /><img width="16" height="16" alt="" src="/upload/248/65/221/1d6ff70f81bc4629acaf77832c4a48f2.gif" /></td>
        </tr>
        <tr>
            <td style="text-align: right;">CSS / JavaScript:</td>
            <td><img width="16" height="16" alt="" src="/upload/248/65/221/1d6ff70f81bc4629acaf77832c4a48f2.gif" /><img width="16" height="16" alt="" src="/upload/248/65/221/1d6ff70f81bc4629acaf77832c4a48f2.gif" /><img width="16" height="16" alt="" src="/upload/248/65/221/1d6ff70f81bc4629acaf77832c4a48f2.gif" /></td>
        </tr>
        <tr>
            <td style="text-align: right;">C# / .Net:</td>
            <td><img width="16" height="16" alt="" src="/upload/248/65/221/1d6ff70f81bc4629acaf77832c4a48f2.gif" /><img width="16" height="16" alt="" src="/upload/248/65/221/1d6ff70f81bc4629acaf77832c4a48f2.gif" /><img width="16" height="16" alt="" src="/upload/248/65/221/1d6ff70f81bc4629acaf77832c4a48f2.gif" /></td>
        </tr>
        <tr>
            <td style="text-align: right;">&nbsp;C / C++:</td>
            <td><img width="16" height="16" alt="" src="/upload/248/65/221/1d6ff70f81bc4629acaf77832c4a48f2.gif" /><img width="16" height="16" alt="" src="/upload/248/65/221/1d6ff70f81bc4629acaf77832c4a48f2.gif" /></td>
        </tr>
        <tr>
            <td>&nbsp;</td>
            <td>&nbsp;</td>
        </tr>
        <tr>
            <td colspan="2">
            <h3 style="color: Red;">Project Experiences</h3>
            </td>
        </tr>
        <tr>
            <td>&nbsp;TODO...</td>
            <td>&nbsp;</td>
        </tr>
        <tr>
            <td>&nbsp;</td>
            <td>&nbsp;</td>
        </tr>
        <tr>
            <td colspan="2">
            <h3 style="color: Red;">Books &amp; Articles</h3>
            </td>
        </tr>
        <tr>
            <td rowspan="3" style="text-align: right;"><img width="100" height="140" border="1" alt="" src="/upload/49/170/176/b2cd49cbb27c4386a56b85967ae37fbc.jpg" /></td>
            <td><a target="_blank" href="http://www.livebookstore.net"><strong>Spring 2.0 Core Technology &amp; Best Practice</strong></a></td>
        </tr>
        <tr>
            <td><em>One of the most popular Java books in China!</em></td>
        </tr>
        <tr>
            <td>This practical guide will introduce Spring 2.0 framework and explain the principle of Spring 2.0 design patterns and best practice. A complete example 'live bookstore' demonstrates how to create a lightweight JavaEE application, with illustration of key functionality of Spring 2.0, and lots of open source solutions including Hibernate, Lucene, Velocity, etc.</td>
        </tr>
        <tr>
            <td rowspan="2" style="text-align: right;"><img width="100" border="1" height="126" alt="" src="/upload/70/220/191/4781c345a9054fa3a6ade7d17274bf31.jpg" /></td>
            <td><a target="_blank" href="http://www.china-pub.com/25538"><strong>Programming Java 2 Micro Edition for Symbian OS</strong></a> (Translation)</td>
        </tr>
        <tr>
            <td>Hands-on information to help you fully exploit the capabilities of MIDP 2.0 on Symbian OS (including MMA, WMA and Bluetooth). This practical guide will walk you through developing example applications illustrating key functionality and explain how to install these applications onto real devices.</td>
        </tr>
        <tr>
            <td>&nbsp;</td>
            <td><a target="_blank" href="http://www.ibm.com/developerworks/cn/java/j-lo-jeeflex/"><strong>Flex Integration with JavaEE: Best Practice</strong></a> (IBM developerWorks)</td>
        </tr>
        <tr>
            <td>&nbsp;</td>
            <td>This article describes how to integrate Flex into an existing JavaEE application, and best practices to develop JavaEE and Flex efficiently and paralleled.</td>
        </tr>
        <tr>
            <td>&nbsp;</td>
            <td><a target="_blank" href="http://www.ibm.com/developerworks/cn/web/wa-lo-json/"><strong>Introduce to JSON</strong></a> (IBM developerWorks)</td>
        </tr>
        <tr>
            <td>&nbsp;</td>
            <td>This article describes what is JSON, how to use JSON to transfer data between server and client, and how to build a fast but simple Java library to handle JSON data.</td>
        </tr>
        <tr>
            <td>&nbsp;</td>
            <td>&nbsp;</td>
        </tr>
        <tr>
            <td colspan="2">
            <h3 style="color: Red;">Open Source Contribution</h3>
            </td>
        </tr>
        <tr>
            <td style="text-align: right;"><a target="_blank" href="http://code.google.com/p/express-me/">ExpressMe</a>:</td>
            <td>Blog / CMS site management system which runs on standard JavaEE server or Google AppEngine. Anyone without programming knowledges can build his/her web site in 5 minutes.</td>
        </tr>
        <tr>
            <td style="text-align: right;"><a target="_blank" href="http://code.google.com/p/express-me/wiki/ExpressWind">Express-Wind</a>:</td>
            <td>Java REST-URL web MVC framework that greatly simplify the web development.</td>
        </tr>
        <tr>
            <td style="text-align: right;"><a target="_blank" href="http://code.google.com/p/express-me/wiki/ExpressPersist">Express-Persist</a>:</td>
            <td>Java JDBC framework that enables you write DAO without any JDBC code.</td>
        </tr>
        <tr>
            <td style="text-align: right;"><a target="_blank" href="http://code.google.com/p/express-me/wiki/ExpressSearch">Express-Search</a>:</td>
            <td>Java search API based on well-known Lucene search engine, provides simple API.&nbsp;</td>
        </tr>
        <tr>
            <td style="text-align: right;">JexiEditor:</td>
            <td>&nbsp;</td>
        </tr>
        <tr>
            <td style="text-align: right;"><a target="_blank" href="http://code.google.com/p/jopenid/">JOpenID</a>:</td>
            <td>Pure Java implementation of OpenID 2.0, compatible with Java 5 or above.</td>
        </tr>
    </tbody>
</table>
<p>&nbsp;</p>]]></description></item>
</channel></rss>