<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>润物无声 &#187; Java</title>
	<atom:link href="http://blog.zhourunsheng.com/tag/java/feed/" rel="self" type="application/rss+xml" />
	<link>http://blog.zhourunsheng.com</link>
	<description>天空一朵雨做的云</description>
	<lastBuildDate>Sat, 08 May 2021 05:17:21 +0000</lastBuildDate>
	<language>zh-CN</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>https://wordpress.org/?v=4.1.41</generator>
	<item>
		<title>Java开发内存优化导读</title>
		<link>http://blog.zhourunsheng.com/2013/08/java%e5%bc%80%e5%8f%91%e5%86%85%e5%ad%98%e4%bc%98%e5%8c%96%e5%af%bc%e8%af%bb/</link>
		<comments>http://blog.zhourunsheng.com/2013/08/java%e5%bc%80%e5%8f%91%e5%86%85%e5%ad%98%e4%bc%98%e5%8c%96%e5%af%bc%e8%af%bb/#comments</comments>
		<pubDate>Tue, 13 Aug 2013 02:32:05 +0000</pubDate>
		<dc:creator><![CDATA[润物无声]]></dc:creator>
				<category><![CDATA[程序设计]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[Memory]]></category>

		<guid isPermaLink="false">http://blog.zhourunsheng.com/?p=1752</guid>
		<description><![CDATA[<p>在开发Java的过程中，怎么样合理的利用Object，资源的申请与释放，每种类型的Object占用的内存大小是 [&#8230;]</p>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2013/08/java%e5%bc%80%e5%8f%91%e5%86%85%e5%ad%98%e4%bc%98%e5%8c%96%e5%af%bc%e8%af%bb/">Java开发内存优化导读</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></description>
				<content:encoded><![CDATA[<p>在开发Java的过程中，怎么样合理的利用<strong>Object</strong>，资源的申请与释放，每种类型的<strong>Object</strong>占用的内存大小是多少，有没有可以替换的lib可以使用。鉴于效率的考量和内存占用的考量，我们应该选择什么样的数据结构，那么如下的文章将会找到答案。</p>
<p>This article will give you the general advices on memory consumption optimization in Java.</p>
<p>Memory usage optimization is the most important type of optimization in Java. <a href="http://www.amazon.com/gp/product/0123797519/ref=as_li_ss_tl?ie=UTF8&amp;camp=1789&amp;creative=390957&amp;creativeASIN=0123797519&amp;linkCode=as2&amp;tag=javaperfor07e-20" rel="nofollow">Current systems are limited by memory access times rather than by CPU frequency</a><img alt="" src="http://www.assoc-amazon.com/e/ir?t=javaperfor07e-20&amp;l=as2&amp;o=1&amp;a=0123797519" width="1" height="1" border="0" /> (otherwise, why CPU producers are implementing all these L1s, L2s and L3s?). It means that by reducing your application memory footprint you will most likely improve your program data processing speed by making your CPU to wait for smaller amount of data. Now let’s get back to Java.<span id="more-1752"></span></p>
<h2>General Java memory layout information</h2>
<p>First of all, we have to <a href="http://java-performance.info/memory-introspection-using-sun-misc-unsafe-and-reflection/">revise the memory layout of Java objects</a>: any Java <strong>Object</strong> occupies at least 16 bytes, 12 out of which are occupied by a Java object header. Besides that, all Java objects are aligned by 8 bytes boundary. It means that, for example, an object containing 2 fields: <strong>int</strong> and <strong>byte</strong> will occupy not 17 (12 + 4 + 1), but 24 bytes (17 aligned by 8 bytes).</p>
<p>Each <strong>Object</strong> reference occupies 4 bytes if the Java heap is under 32G and<strong>XX:+UseCompressedOops</strong> is turned on (it is turned on by default in the recent Oracle JVM versions). Otherwise, <strong>Object</strong> references occupy 8 bytes.</p>
<p>All primitive data types occupy their exact byte size:</p>
<table border="1">
<tbody>
<tr>
<td>byte, boolean</td>
<td>1 byte</td>
</tr>
<tr>
<td>short, char</td>
<td>2 bytes</td>
</tr>
<tr>
<td>int, float</td>
<td>4 bytes</td>
</tr>
<tr>
<td>long, double</td>
<td>8 bytes</td>
</tr>
</tbody>
</table>
<p>In essence, this information is sufficient for Java memory optimization. But it will be more convenient if you will be aware of arrays / <strong>String</strong> / numeric wrappers memory consumption.</p>
<h2>Most common Java types memory consumption</h2>
<p>Arrays consume 12 bytes plus their length multiplied by their element size (and, of course, rounded by 8 bytes alignment).</p>
<p><a href="http://java-performance.info/changes-to-string-java-1-7-0_06/">From Java 7 build 06</a> <strong>String</strong> contains 3 fields – a <strong>char[]</strong> with the string data plus 2 <strong>int</strong> fields with 2 hash codes calculated by the different algorithms. It means that a <strong>String</strong> itself needs 12 (header) + 4 (<strong>char[]</strong> reference) + 4 * 2 (int) = 24 bytes (as you can see, it exactly fits in 8 byte alignment). Besides that, a <strong>char[]</strong> with the <strong>String</strong> data occupies 12 + length * 2 bytes (plus alignment). It means that a <strong>String</strong> occupies 36 + length*2 bytes aligned by 8 bytes (which is, by the way, 8 byte less than a <strong>String</strong> memory consumption prior to <a href="http://java-performance.info/changes-to-string-java-1-7-0_06/">Java 7 build 06</a>).</p>
<p>Numeric wrappers occupy 12 bytes plus size of the underlying type.<a href="http://java-performance.info/java-lang-byte-short-integer-long-character-boxing-and-unboxing/">Byte/Short/Character/Integer/Long are cached by JDK</a>, so the actual memory consumption may be smaller for values in the range [-128; 127]. Anyway, these type may be the source of serious memory overhead in the collection-based applications:</p>
<table border="1">
<tbody>
<tr>
<td>Byte, Boolean</td>
<td>16 bytes</td>
</tr>
<tr>
<td>Short, Character</td>
<td>16 bytes</td>
</tr>
<tr>
<td>Integer, Float</td>
<td>16 bytes</td>
</tr>
<tr>
<td>Long, Double</td>
<td>24 bytes</td>
</tr>
</tbody>
</table>
<h2>General Java memory optimization tips</h2>
<p>With all this knowledge at hand it is not difficult to give the general Java memory optimization tips:</p>
<ul>
<li>Prefer primitive types to their <strong>Object</strong> wrappers. The main cause of wrapper types usage are JDK collections, so consider using one of primitive type collection frameworks like<a href="http://java-performance.info/primitive-types-collections-trove-library/">Trove</a>.</li>
<li>Try to minimize number of <strong>Object</strong>s you have. For example, prefer array-based structures like <strong><a href="http://java-performance.info/arraylist-performance/">ArrayList</a>/<a href="http://java-performance.info/linkedlist-performance/">ArrayDeque</a></strong> to pointer based structures like <strong><a href="http://java-performance.info/linkedlist-performance/">LinkedList</a></strong>.</li>
</ul>
<h2>Java memory optimization example</h2>
<p>Here is an example. Suppose you have to create a map from <strong>int</strong> to 20 character long strings. The size of this map is equal to one million and all mappings are static and predefined (saved in some dictionary, for example).</p>
<p>The first approach is to use a <strong>Map&lt;Integer, String&gt;</strong> from the standard JDK. Let’s roughly estimate the memory consumption of this structure. Each <strong>Integer</strong> occupies 16 bytes plus 4 bytes for an <strong>Integer</strong> reference from a map. Each 20 character long <strong>String</strong> occupies 36 + 20*2 = 76 bytes (see <strong>String</strong> description above), which are aligned to 80 bytes. Plus 4 bytes for a reference. The total memory consumption will be roughly <b>(16 + 4 + 80 + 4) * 1M = 104M</b>.</p>
<p>The better approach will be replacing <strong>String</strong> with a <strong>byte[]</strong> in UTF-8 encoding as described in<a href="http://java-performance.info/string-packing-converting-characters-to-bytes/">String packing part 1: converting characters to bytes</a> article. Our map will be <strong>Map&lt;Integer, byte[]&gt;</strong>. Assume that all string characters belong to ASCII set (0-127), which is true in most of English-speaking countries. A <strong>byte[20]</strong> occupies 12 (header) + 20*1 = 32 bytes, which conveniently fit into 8 bytes alignment. The whole map will now occupy <b>(16 + 4 + 32 + 4) * 1M = 56M</b>, which is 2 times less than the previous example.</p>
<p>Now let’s use <a href="http://java-performance.info/primitive-types-collections-trove-library/">Trove</a> <strong>TIntObjectMap&lt;byte[]&gt;</strong>. It stores keys as normal <strong>int[]</strong> compared to wrapper types in JDK collections. Each key will now occupy exactly 4 bytes. The total memory consumption will go down to <b>(4 + 32 + 4) * 1M = 40M</b>.</p>
<p>The final structure will be more complicated. All <strong>String</strong> values will be stored in the single<strong>byte[]</strong> one after another with <strong>(byte)0</strong> as a separator (we still assume we have a text-based ASCII strings). The whole <strong>byte[]</strong> will occupy (20 + 1) * 1M = 21M. Our map will store an offset of the string in the large <strong>byte[]</strong> instead of the string itself. We will use Trove <strong>TIntIntMap</strong> for this purpose. It will consume (4 + 4) * 1M = 8M. The total memory consumption in this example will be <b>8M + 21M = 29M</b>. By the way, this is the first example relying on the immutability of this dataset.</p>
<p>Can we achieve the better result? Yes, we can, but at a cost of CPU consumption. The obvious ‘optimization’ is to sort the whole dataset by keys before storing values into a large <strong>byte[]</strong>. Now we may store the keys in the <strong>int[]</strong> and use a binary search to look for a key. If a key is found, its index multiplied by 21 (remember, all strings have the same length) will give us the offset of a value in the <strong>byte[]</strong>. This structure occupies ‘only’ <b>21M + 4M (for <strong>int[]</strong>) = 25M</b>at a price of <strong>O(log N)</strong> lookups compared to <strong>O(1)</strong> lookups in case of a hash map.</p>
<p>Is this the best we can do? No! We have forgotten that all values are 20 characters long, so we don’t actually need the separators in the <strong>byte[]</strong>. It means that we can store our ‘map’ using <b>24M memory</b> if we agree on <strong>O( log N )</strong> lookups. No overhead at all compared to theoretical data size and <b>nearly 4.5 times less than required by the original solution</b> (<strong>Map&lt;Integer, String&gt;</strong> )! Who told you that Java programs are memory hungry?</p>
<p>In <a href="http://java-performance.info/memory-consumption-of-java-data-types-1/">second part of this article</a> we will look at a few commonly used Java types memory consumption.</p>
<h2>Summary</h2>
<ul>
<li>Prefer primitive types to their <strong>Object</strong> wrappers. The main cause of wrapper types usage are JDK collections, so consider using one of primitive type collection frameworks like<a href="http://java-performance.info/primitive-types-collections-trove-library/">Trove</a>.</li>
<li>Try to minimize number of <strong>Object</strong>s you have. For example, prefer array-based structures like <strong><a href="http://java-performance.info/arraylist-performance/">ArrayList</a>/<a href="http://java-performance.info/linkedlist-performance/">ArrayDeque</a></strong> to pointer based structures like <strong><a href="http://java-performance.info/linkedlist-performance/">LinkedList</a></strong>.</li>
</ul>
<h2>Recommended reading</h2>
<p>If you want to find out more about the clever data compression algorithms, it worth to read<a href="http://www.amazon.com/gp/product/0201657880/ref=as_li_ss_tl?ie=UTF8&amp;camp=1789&amp;creative=390957&amp;creativeASIN=0201657880&amp;linkCode=as2&amp;tag=javaperfor07e-20" rel="nofollow">“Programming Pearls” (second edition) by Jon Bentley</a><img alt="" src="http://www.assoc-amazon.com/e/ir?t=javaperfor07e-20&amp;l=as2&amp;o=1&amp;a=0201657880" width="1" height="1" border="0" />. This is a wonderful collection of rather unexpected algorithms. For example, in column 13.8 author has described how Doug McIlroy managed to fit a 75,000 word spellchecker just in 64 kilobytes of RAM. That spellchecker kept all required information in that tiny amount of memory and did not use disks! It may also be noticed that <a href="http://www.amazon.com/gp/product/0201657880/ref=as_li_ss_tl?ie=UTF8&amp;camp=1789&amp;creative=390957&amp;creativeASIN=0201657880&amp;linkCode=as2&amp;tag=javaperfor07e-20" rel="nofollow">“Programming Pearls”</a><img alt="" src="http://www.assoc-amazon.com/e/ir?t=javaperfor07e-20&amp;l=as2&amp;o=1&amp;a=0201657880" width="1" height="1" border="0" /> is one of recommended preparation books for Google SRE interviews.</p>
<p>节选文章：</p>
<ul>
<li><a href="http://java-performance.info/overview-of-memory-saving-techniques-java/">An overview of memory saving techniques in Java</a></li>
<li><a href="http://java-performance.info/memory-consumption-of-java-data-types-1/">Memory consumption of popular Java data types – part 1</a></li>
<li><a href="http://java-performance.info/memory-consumption-of-java-data-types-2/">Memory consumption of java data types 2</a></li>
<li><a href="http://java-performance.info/java-collections-overview/">Java collections overview</a></li>
<li><a href="http://java-performance.info/a-few-more-memory-saving-techniques-in-java/">A few more memory saving techniques in Java</a></li>
</ul>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2013/08/java%e5%bc%80%e5%8f%91%e5%86%85%e5%ad%98%e4%bc%98%e5%8c%96%e5%af%bc%e8%af%bb/">Java开发内存优化导读</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://blog.zhourunsheng.com/2013/08/java%e5%bc%80%e5%8f%91%e5%86%85%e5%ad%98%e4%bc%98%e5%8c%96%e5%af%bc%e8%af%bb/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Java开发内存占用监测</title>
		<link>http://blog.zhourunsheng.com/2013/08/java%e5%bc%80%e5%8f%91%e5%86%85%e5%ad%98%e5%8d%a0%e7%94%a8%e7%9b%91%e6%b5%8b/</link>
		<comments>http://blog.zhourunsheng.com/2013/08/java%e5%bc%80%e5%8f%91%e5%86%85%e5%ad%98%e5%8d%a0%e7%94%a8%e7%9b%91%e6%b5%8b/#comments</comments>
		<pubDate>Mon, 05 Aug 2013 13:15:20 +0000</pubDate>
		<dc:creator><![CDATA[润物无声]]></dc:creator>
				<category><![CDATA[程序设计]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[Memory]]></category>

		<guid isPermaLink="false">http://blog.zhourunsheng.com/?p=1729</guid>
		<description><![CDATA[<p>在开发Java的过程中，怎么样来监测一个对象占用的内存大小，对于调试程序，优化程序来说，有很大的帮助，本文介绍 [&#8230;]</p>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2013/08/java%e5%bc%80%e5%8f%91%e5%86%85%e5%ad%98%e5%8d%a0%e7%94%a8%e7%9b%91%e6%b5%8b/">Java开发内存占用监测</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></description>
				<content:encoded><![CDATA[<p>在开发Java的过程中，怎么样来监测一个对象占用的内存大小，对于调试程序，优化程序来说，有很大的帮助，本文介绍了一个开发包，JAMM (Java Agent for Memory Measurements)，可以很方便的在程序的运行过程中动态监测对象的内存占用情况。<br />
This short article shows how to measure java object memory size at runtime with <a href="https://github.com/jbellis/jamm" target="_blank">JAMM</a>, a java agent dedicated to measure actual object memory use including JVM overhead.<br />
JAMM uses the <a href="http://docs.oracle.com/javase/6/docs/api/java/lang/instrument/Instrumentation.html" target="_blank">Instrumentation </a>implementation provided by the JVM to compute memory usage of a given object by calling the <a href="http://docs.oracle.com/javase/6/docs/api/java/lang/instrument/Instrumentation.html#getObjectSize(java.lang.Object)" target="_blank">getObjectSize(..)</a> method.<span id="more-1729"></span></p>
<p>It is quite simple to use, as explained by the author:</p>
<div>
<pre>        
MemoryMeter meter = new MemoryMeter();
meter.measure(object);
meter.measureDeep(object);
meter.countChildren(object);</pre>
</div>
<div>The only constraint is to attach JAMM java agent to the JVM before using it to measure memory usage, by starting the JVM with the <b>-javaagent</b> option pointing to the JAMM jar.</div>
<div></div>
<div>So, we are going to write a JUnit that shows how to use JAMM, but before we need to setup maven to achieve this:</div>
<div>
<pre>        
&lt; dependencies &gt; 
      &lt; dependency &gt; 
          &lt; groupid &gt;junit&lt;/ groupid &gt; 
          &lt; artifactid &gt;junit&lt;/ artifactid &gt; 
          &lt; version &gt;4.11&lt;/ version &gt; 
          &lt; scope &gt;test&lt;/ scope &gt; 
      &lt;/ dependency &gt; 

      &lt;!-- jamm java agent --&gt; 
      &lt; dependency &gt; 
          &lt; groupid &gt;com.github.stephenc&lt;/ groupid &gt; 
          &lt; artifactid &gt;jamm&lt;/ artifactid &gt; 
          &lt; version &gt;0.2.5&lt;/ version &gt; 
          &lt; scope &gt;test&lt;/ scope &gt; 
      &lt;/ dependency &gt; 

 &lt;/ dependencies &gt; 

 &lt; build &gt; 
      &lt; plugins &gt; 

          &lt;!-- copies java agent dependency into the target directory --&gt; 
          &lt; plugin &gt; 
              &lt; groupid &gt;org.apache.maven.plugins&lt;/ groupid &gt; 
              &lt; artifactid &gt;maven-dependency-plugin&lt;/ artifactid &gt; 
              &lt; version &gt;2.8&lt;/ version &gt; 
              &lt; executions &gt; 
                  &lt; execution &gt; 
                      &lt; id &gt;copy-dependencies&lt;/ id &gt; 
                      &lt; phase &gt;generate-test-resources&lt;/ phase &gt; 
                      &lt; goals &gt; 
                          &lt; goal &gt;copy&lt;/ goal &gt; 
                      &lt;/ goals &gt; 
                      &lt; configuration &gt; 
                          &lt; artifactitems &gt; 
                              &lt; artifactitem &gt; 
                                  &lt; groupid &gt;com.github.stephenc&lt;/ groupid &gt; 
                                  &lt; artifactid &gt;jamm&lt;/ artifactid &gt; 
                                  &lt; version &gt;0.2.5&lt;/ version &gt; 
                                  &lt; type &gt;jar&lt;/ type &gt; 
                                  &lt; outputdirectory &gt;${project.build.directory}&lt;/ outputdirectory &gt; 
                                  &lt; destfilename &gt;jamm.jar&lt;/ destfilename &gt; 
                              &lt;/ artifactitem &gt; 
                          &lt;/ artifactitems &gt; 
                      &lt;/ configuration &gt; 
                  &lt;/ execution &gt; 
              &lt;/ executions &gt; 
          &lt;/ plugin &gt; 

          &lt;!-- executes test with java agent options of the JVM --&gt; 
          &lt; plugin &gt; 
              &lt; groupid &gt;org.apache.maven.plugins&lt;/ groupid &gt; 
              &lt; artifactid &gt;maven-surefire-plugin&lt;/ artifactid &gt; 
              &lt; version &gt;2.14&lt;/ version &gt; 
              &lt; configuration &gt; 
                  &lt; argline &gt;-javaagent:${project.build.directory}/jamm.jar&lt;/ argline &gt; 
              &lt;/ configuration &gt; 
          &lt;/ plugin &gt; 

      &lt;/ plugins &gt; 
 &lt;/ build &gt;</pre>
</div>
<p>Next, write the following JUnit that explores the JAMM features:</p>
<div>
<pre> package org.javabenchmark.memory; 

 import java.util.ArrayList; 
 import java.util.List; 
 import org.github.jamm.MemoryMeter; 
 import org.junit.Test; 

 public class MemoryMeterTest { 

      private MemoryMeter meter = new MemoryMeter(); 

      @Test 
      public void shouldMeasureMemoryUsage() { 

          String st1 = "This is the string #1" ; 
          measure(st1); 

          String st2 = "This is the string #2 and it has more chars." ; 
          measure(st2); 

          List aList = new ArrayList( 0 ); 
          measure(aList); 

          aList.add(st1); 
          measure(aList); 

          aList.add(st2); 
          measure(aList); 
      } 

      private void measure(Object anObject) { 
          System.out.println( "-----------------------------------" ); 
          System.out.printf( "size: %d bytes\n" , meter.measure(anObject)); 
          System.out.printf( "retained size: %d bytes\n" , meter.measureDeep(anObject)); 
          System.out.printf( "inner object count: %d\n" , meter.countChildren(anObject)); 
      } 
 }</pre>
</div>
<p>Running the test produces the following output on my computer:</p>
<div>
<pre> ----------------------------------- 
 size: 24 bytes 
 retained size: 88 bytes 
 inner object count: 2 
 ----------------------------------- 
 size: 24 bytes 
 retained size: 128 bytes 
 inner object count: 2 
 ----------------------------------- 
 size: 24 bytes 
 retained size: 40 bytes 
 inner object count: 2 
 ----------------------------------- 
 size: 24 bytes 
 retained size: 136 bytes 
 inner object count: 4 
 ----------------------------------- 
 size: 24 bytes 
 retained size: 264 bytes 
 inner object count: 6</pre>
</div>
<p>To conclude, you can see how it is easy to monitor the memory usage of your objects. It is very handy when dealing with huge collections, or when using caches such as the ones provided by Guava or EHCache. That way you can setup trigger that alert when memory consumption is excessive.</p>
<p><strong>注意：</strong></p>
<p>如果在调试的过程中，发现内存不足的情况，请调整JVM的启动参数。</p>
<p>JVM options: <b>-Xms256m -Xmx256m</b>.</p>
<p>文章选自：<a href="http://blog.javabenchmark.org/2013/07/compute-java-object-memory-footprint-at.html">compute-java-object-memory-footprint-at.html</a></p>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2013/08/java%e5%bc%80%e5%8f%91%e5%86%85%e5%ad%98%e5%8d%a0%e7%94%a8%e7%9b%91%e6%b5%8b/">Java开发内存占用监测</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://blog.zhourunsheng.com/2013/08/java%e5%bc%80%e5%8f%91%e5%86%85%e5%ad%98%e5%8d%a0%e7%94%a8%e7%9b%91%e6%b5%8b/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Java语言开发之SQL语句改善</title>
		<link>http://blog.zhourunsheng.com/2013/08/java%e8%af%ad%e8%a8%80%e5%bc%80%e5%8f%91%e4%b9%8bsql%e8%af%ad%e5%8f%a5%e6%94%b9%e5%96%84/</link>
		<comments>http://blog.zhourunsheng.com/2013/08/java%e8%af%ad%e8%a8%80%e5%bc%80%e5%8f%91%e4%b9%8bsql%e8%af%ad%e5%8f%a5%e6%94%b9%e5%96%84/#comments</comments>
		<pubDate>Mon, 05 Aug 2013 10:57:15 +0000</pubDate>
		<dc:creator><![CDATA[润物无声]]></dc:creator>
				<category><![CDATA[程序设计]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[SQL]]></category>

		<guid isPermaLink="false">http://blog.zhourunsheng.com/?p=1727</guid>
		<description><![CDATA[<p>在基于Java语言的开发过程中，DB相关的开发相信大家都接触过SQL语句，增删改查作为基本的操作，比较熟悉，那 [&#8230;]</p>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2013/08/java%e8%af%ad%e8%a8%80%e5%bc%80%e5%8f%91%e4%b9%8bsql%e8%af%ad%e5%8f%a5%e6%94%b9%e5%96%84/">Java语言开发之SQL语句改善</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></description>
				<content:encoded><![CDATA[<p>在基于Java语言的开发过程中，DB相关的开发相信大家都接触过SQL语句，增删改查作为基本的操作，比较熟悉，那么JOIN，UNION, MERGE, DISTINCT这些高级的操作使用过没有呢？使用的过程中有哪些误用呢？有没有可能改善现在的SQL语句来提高程序的性能。本文介绍了几种常见的SQL误用场合，有语法方面的，改善效率方面的，下次，自己再做开发的时候，想想当前的SQL处理语句有没有可以完善的情况。</p>
<p>Java developers mix object-oriented thinking with imperative thinking, depending on their levels of:</p>
<ul>
<li>Skill (anyone can code imperatively)</li>
<li>Dogma (some use the “Pattern-Pattern”, i.e. the pattern of applying patterns everywhere and giving them names)</li>
<li>Mood (true OO is more clumsy to write than imperative code. At first)</li>
</ul>
<p>But when Java developers write SQL, everything changes. SQL is a declarative language that has nothing to do with either object-oriented or imperative thinking. It is very easy to express a query in SQL. It is not so easy to express it optimally or correctly. Not only do developers need to re-think their programming paradigm, they also need to think in terms of set theory.</p>
<p>Here are common mistakes that a Java developer makes when writing SQL (in no particular order):<span id="more-1727"></span></p>
<h3>1. Forgetting about NULL</h3>
<p>Misunderstanding NULL is probably the biggest mistake a Java developer can make when writing SQL. This is also (but not exclusively) due to the fact that NULL is also called UNKNOWN. If it were only called UNKNOWN, it would be easier to understand. Another reason is that JDBC maps SQL NULL to Java null when fetching data or when binding variables. This may lead to thinking that NULL = NULL (SQL) would behave the same way as null == null (Java)</p>
<p>One of the crazier examples of misunderstanding NULL is when <a title="Row value expressions and the NULL predicate" href="http://blog.jooq.org/2012/12/24/row-value-expressions-and-the-null-predicate/">NULL predicates are used with row value expressions</a>.</p>
<p>Another, subtle problem appears when misunderstanding the meaning of<a title="NOT IN vs. NOT EXISTS vs. LEFT JOIN / IS NULL: MySQL" href="http://blog.jooq.org/2012/07/27/not-in-vs-not-exists-vs-left-join-is-null-mysql/">NULL in NOT IN anti-joins</a>.</p>
<p><strong>The Cure:</strong></p>
<p>Train yourself. There’s nothing but explicitly thinking about NULL, every time you write SQL:</p>
<ul>
<li>Is this predicate correct with respect to NULL?</li>
<li>Does NULL affect the result of this function?</li>
</ul>
<h3>2. Processing data in Java memory</h3>
<p>Few Java developers know SQL very well. The occasional JOIN, the odd UNION, fine. But window functions? Grouping sets? A lot of Java developers load SQL data into memory, transform the data into some appropriate collection type, execute nasty maths on that collection with verbose loop structures (at least, before <a href="http://blog.informatech.cr/2013/03/24/java-streams-preview-vs-net-linq/">Java 8′s Collection improvements</a>).</p>
<p>But some SQL databases support advanced (and SQL standard!) OLAP features that tend to perform a lot better and are much easier to write. A (non-standard) example is <a href="http://stackoverflow.com/a/12245888/521799">Oracle’s awesome MODEL clause</a>. Just let the database do the processing and fetch only the results into Java memory. Because after all some very smart guys have optimised these expensive products. So in fact, by moving OLAP to the database, you gain two things:</p>
<ul>
<li>Simplicity. It’s probably easier to write correctly in SQL than in Java</li>
<li>Performance. The database will probably be faster than your algorithm. And more importantly, you don’t have to transmit millions of records over the wire.</li>
</ul>
<p><strong>The Cure:</strong></p>
<p>Every time you implement a data-centric algorithm in Java, ask yourself: Is there a way to let the database perform that work for me?</p>
<h3>3. Using UNION instead of UNION ALL</h3>
<p>It’s a shame that UNION ALL needs an extra keyword compared to UNION. It would be much better if the SQL standard had been defined to support:</p>
<ul>
<li>UNION (allowing duplicates)</li>
<li>UNION DISTINCT (removing duplicates)</li>
</ul>
<p>Not only is the removal of duplicates rarely needed (or sometimes even wrong), it is also quite slow for large result sets with many columns, as the two subselects need to be ordered, and each tuple needs to be compared with its subsequent tuple.</p>
<p>Note that even if the SQL standard specifies INTERSECT ALL and EXCEPT ALL, hardly any database implements these less useful set operations.</p>
<p><strong>The Cure:</strong></p>
<p>Every time you write a UNION, think if you actually wanted to write UNION ALL.</p>
<h3>4. Using JDBC Paging to page large results</h3>
<p>Most databases support some way of paging ordered results through LIMIT .. OFFSET, TOP .. START AT, OFFSET .. FETCH clauses. In the absence of support for these clauses, there is still the possibility for <a href="http://stackoverflow.com/q/6033080/521799">ROWNUM (Oracle)</a>or <a href="http://stackoverflow.com/q/7073263/521799">ROW_NUMBER() OVER() filtering (DB2, SQL Server 2008 and less)</a>, which is much faster than paging in memory. This is specifically true for large offsets!</p>
<p><strong>The Cure:</strong></p>
<p>Just use those clauses, or a tool (such as <a href="http://www.jooq.org/">jOOQ</a>) that can simulate those clauses for you.</p>
<h3>5. Joining data in Java memory</h3>
<p>From early days of SQL, some developers still have an uneasy feeling when expressing JOINs in their SQL. There is an inherent fear of JOIN being slow. This can be true if a cost-based optimiser chooses to perform a nested loop, possibly loading complete tables into database memory, before creating a joined table source. But that happens rarely. With appropriate predicates, constraints and indexes, MERGE JOIN and HASH JOIN operations are extremely fast. It’s all about the correct metadata <a title="How schema meta data impacts Oracle query transformations" href="http://blog.jooq.org/2011/11/25/how-schema-meta-data-impacts-oracle-query-transformations/">(I cannot cite Tom Kyte often enough for this)</a>. Nonetheless, there are probably still quite a few Java developers who will load two tables from separate queries into maps and join them in Java memory in one way or another.</p>
<p><strong>The Cure:</strong></p>
<p>If you’re selecting from various tables in various steps, think again to see if you cannot express your query in a single statement.</p>
<h3>6. Using DISTINCT or UNION to remove duplicates from an accidental cartesian product</h3>
<p>With heavy joining, one can loose track of all the relations that are playing a role in a SQL statement. Specifically, if multi-column foreign key relationships are involved, it is possible to forget to add the relevant predicates in JOIN .. ON clauses. This might result in duplicate records, but maybe only in exceptional cases. Some developers may then choose to use DISTINCT to remove those duplicates again. This is wrong in three ways:</p>
<ul>
<li>It (may) solve the symptoms but not the problem. It may as well not solve the symptoms in edge-cases.</li>
<li>It is slow for large result sets with many columns. DISTINCT performs an ORDER BY operation to remove duplicates.</li>
<li>It is slow for large cartesian products, which will still load lots of data into memory</li>
</ul>
<p><strong>The Cure:</strong></p>
<p>As a rule of thumb, when you get unwanted duplicates, always review your JOIN predicates. There’s probably a subtle cartesian product in there somewhere.</p>
<h3>7. Not using the MERGE statement</h3>
<p>This isn’t really a mistake, but probably some lack of knowledge or some fear towards the <a title="Arcane magic with the SQL:2003 MERGE statement" href="http://blog.jooq.org/2011/11/29/arcane-magic-with-the-sql2003-merge-statement/">powerful MERGE statement</a>. Some databases know other forms of UPSERT statements, e.g. MySQL’s ON DUPLICATE KEY UPDATE clause. But MERGE is really so powerful, most importantly in databases that heavily extend the SQL standard, <a href="http://msdn.microsoft.com/de-de/library/bb510625.aspx">such as SQL Server</a>.</p>
<p><strong>The Cure:</strong></p>
<p>If you’re UPSERTING by chaining INSERT and UPDATE or by chaining SELECT .. FOR UPDATE and then INSERT or UPDATE, think again. Apart from risking race conditions, you might be able to express a simpler MERGE statement.</p>
<h3>8. Using aggregate functions instead of window functions</h3>
<p>Before the introduction of window functions, the only means to aggregate data in SQL was by using a GROUP BY clause along with aggregate functions in the projection. This works well in many cases, and if aggregation data needed to be enriched with regular data, the grouped query can be pushed down into a joined subquery.</p>
<p>But SQL:2003 defined window functions, which are implemented by many popular database vendors. Window functions can aggregate data on result sets that are not grouped. In fact, each window function supports its own, independent PARTITION BY clause, which is an awesome tool for reporting.</p>
<p>Using window functions will:</p>
<ul>
<li>Lead to more readable SQL (less dedicated GROUP BY clauses in subqueries)</li>
<li>Improve performance, as a RDBMS is likely to optimise window functions more easily</li>
</ul>
<p><strong>The Cure:</strong></p>
<p>When you write a GROUP BY clause in a subquery, think again if this cannot be done with a window function.</p>
<h3>9. Using in-memory sorting for sort indirections</h3>
<p>The SQL ORDER BY clause supports many types of expressions, including CASE statements, which can be very useful for sort indirections. You should probably never sort data in Java memory because you think that</p>
<ul>
<li>SQL sorting is too slow</li>
<li>SQL sorting cannot do it</li>
</ul>
<p><strong>The Cure:</strong></p>
<p>If you sort any SQL data in memory, think again if you cannot push sorting into your database. This goes along well with pushing paging into the database.</p>
<h3>10. Inserting lots of records one by one</h3>
<p>JDBC knows batching, and you should use it. Do not INSERT thousands of records one by one, re-creating a new PreparedStatement every time. If all of your records go to the same table, create a batch INSERT statement with a single SQL statement and multiple bind value sets. Depending on your database and database configuration, you may need to commit after a certain amount of inserted records, in order to keep the UNDO log slim.</p>
<p><strong>The Cure:</strong></p>
<p>Always batch-insert large sets of data.</p>
<h3>Some interesting books</h3>
<p>Some very interesting books on similar topics are</p>
<ul>
<li><a href="http://pragprog.com/book/bksqla/sql-antipatterns">SQL Antipatterns by Bill Karwin</a></li>
<li><a href="http://winand.at/book/sql-performance-explained">SQL Performance Explained by Markus Winand</a></li>
</ul>
<p>本文选自：<a href="http://blog.jooq.org/2013/07/30/10-common-mistakes-java-developers-make-when-writing-sql/">10-common-mistakes-java-developers-make-when-writing-sql</a></p>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2013/08/java%e8%af%ad%e8%a8%80%e5%bc%80%e5%8f%91%e4%b9%8bsql%e8%af%ad%e5%8f%a5%e6%94%b9%e5%96%84/">Java语言开发之SQL语句改善</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://blog.zhourunsheng.com/2013/08/java%e8%af%ad%e8%a8%80%e5%bc%80%e5%8f%91%e4%b9%8bsql%e8%af%ad%e5%8f%a5%e6%94%b9%e5%96%84/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Java PHP Json Mysql 中文乱码问题之解决</title>
		<link>http://blog.zhourunsheng.com/2012/06/java-php-json-mysql-%e4%b8%ad%e6%96%87%e4%b9%b1%e7%a0%81%e9%97%ae%e9%a2%98%e4%b9%8b%e8%a7%a3%e5%86%b3/</link>
		<comments>http://blog.zhourunsheng.com/2012/06/java-php-json-mysql-%e4%b8%ad%e6%96%87%e4%b9%b1%e7%a0%81%e9%97%ae%e9%a2%98%e4%b9%8b%e8%a7%a3%e5%86%b3/#comments</comments>
		<pubDate>Fri, 01 Jun 2012 03:15:55 +0000</pubDate>
		<dc:creator><![CDATA[润物无声]]></dc:creator>
				<category><![CDATA[程序设计]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[Json]]></category>
		<category><![CDATA[Mysql]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[中文乱码]]></category>

		<guid isPermaLink="false">http://blog.zhourunsheng.com/?p=1619</guid>
		<description><![CDATA[<p>问题 客户端采用Java语言编写，服务器端采用PHP语言编写，数据库采用Mysql存储，客户端和服务器之间的交 [&#8230;]</p>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2012/06/java-php-json-mysql-%e4%b8%ad%e6%96%87%e4%b9%b1%e7%a0%81%e9%97%ae%e9%a2%98%e4%b9%8b%e8%a7%a3%e5%86%b3/">Java PHP Json Mysql 中文乱码问题之解决</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></description>
				<content:encoded><![CDATA[<h2>问题</h2>
<p>客户端采用Java语言编写，服务器端采用PHP语言编写，数据库采用Mysql存储，客户端和服务器之间的交互采用Json，在传递英文数据的时候没有问题，当传递中文数据数据的时候，就会出现中文乱码问题，mysql里面的中文全部变成问号了。</p>
<h2>解决方案</h2>
<ul>
<li>Mysql数据库，数据表，数据字段采用统一编码UTF-8, 如 utf8_general_ci</li>
<li>客户端Java字符串转成json格式的时候先进行urlencode处理
<pre>JSONObject jo = new JSONObject();
//jo.accumulate("note", note);
jo.accumulate("note", URLEncoder.encode(note));</pre>
</li>
<li>服务器端PHP转换json格式后，插入数据库前先进行urldecode处理
<pre>$data = array(
    'meta_key' =&gt; 'note',
    //'meta_value' =&gt; $params['note'],
    'meta_value' =&gt; urldecode($params['note']),
);</pre>
</li>
<li>经过以上的步骤处理，可完美解决中文乱码问题</li>
</ul>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2012/06/java-php-json-mysql-%e4%b8%ad%e6%96%87%e4%b9%b1%e7%a0%81%e9%97%ae%e9%a2%98%e4%b9%8b%e8%a7%a3%e5%86%b3/">Java PHP Json Mysql 中文乱码问题之解决</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://blog.zhourunsheng.com/2012/06/java-php-json-mysql-%e4%b8%ad%e6%96%87%e4%b9%b1%e7%a0%81%e9%97%ae%e9%a2%98%e4%b9%8b%e8%a7%a3%e5%86%b3/feed/</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
		<item>
		<title>关于Java程序自动发布文章到百度空间的程序更新</title>
		<link>http://blog.zhourunsheng.com/2012/02/%e5%85%b3%e4%ba%8ejava%e7%a8%8b%e5%ba%8f%e8%87%aa%e5%8a%a8%e5%8f%91%e5%b8%83%e6%96%87%e7%ab%a0%e5%88%b0%e7%99%be%e5%ba%a6%e7%a9%ba%e9%97%b4%e7%9a%84%e7%a8%8b%e5%ba%8f%e6%9b%b4%e6%96%b0/</link>
		<comments>http://blog.zhourunsheng.com/2012/02/%e5%85%b3%e4%ba%8ejava%e7%a8%8b%e5%ba%8f%e8%87%aa%e5%8a%a8%e5%8f%91%e5%b8%83%e6%96%87%e7%ab%a0%e5%88%b0%e7%99%be%e5%ba%a6%e7%a9%ba%e9%97%b4%e7%9a%84%e7%a8%8b%e5%ba%8f%e6%9b%b4%e6%96%b0/#comments</comments>
		<pubDate>Sun, 05 Feb 2012 05:21:09 +0000</pubDate>
		<dc:creator><![CDATA[润物无声]]></dc:creator>
				<category><![CDATA[程序设计]]></category>
		<category><![CDATA[Baidu]]></category>
		<category><![CDATA[Java]]></category>

		<guid isPermaLink="false">http://blog.zhourunsheng.com/?p=1313</guid>
		<description><![CDATA[<p>利用Java程序自动发送博客文章到百度空间，可以参照我的博文《Java程序自动发布文章到百度空间》，通过修改程 [&#8230;]</p>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2012/02/%e5%85%b3%e4%ba%8ejava%e7%a8%8b%e5%ba%8f%e8%87%aa%e5%8a%a8%e5%8f%91%e5%b8%83%e6%96%87%e7%ab%a0%e5%88%b0%e7%99%be%e5%ba%a6%e7%a9%ba%e9%97%b4%e7%9a%84%e7%a8%8b%e5%ba%8f%e6%9b%b4%e6%96%b0/">关于Java程序自动发布文章到百度空间的程序更新</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></description>
				<content:encoded><![CDATA[<p>利用Java程序自动发送博客文章到百度空间，可以参照我的博文《<a href="http://blog.zhourunsheng.com/2011/07/java%E7%A8%8B%E5%BA%8F%E8%87%AA%E5%8A%A8%E5%8F%91%E5%B8%83%E6%96%87%E7%AB%A0%E5%88%B0%E7%99%BE%E5%BA%A6%E7%A9%BA%E9%97%B4/">Java程序自动发布文章到百度空间</a>》，通过修改程序的源码，主要是登录账户的信息就可以进行代码测试。</p>
<p>2012年开始，百度空间的博文发布系统进行了一些调整，主要是变更了一些请求的API参数和URL地址，以及增加跨域检测，提高了安全性，所以旧的代码无法正常工作，更新过的可以正常工作的代码在本文的最后有下载链接。</p>
<p><strong>主要变更点如下：</strong></p>
<ol>
<li>百度空间的登录
<p>params.put("mem_pass", "on");</p>
</li>
<li>添加博文类别
<p>params.put("spIsBlogCatAdd", "1");</p>
</li>
<li>博文发布
<p>参数变更：params.put("previewImg", "");</p>
<p>提交地址变更：private static final String SUBMIT_CREATBLOG_URL = "http://hi.baidu.com/" + Config.USERNAME + "/blog/submit/createblog";</p>
</li>
<li>跨域检测
<p>httpPost.setRequestHeader("Referer", refURL);</p>
</li>
</ol>
<p><strong>代码下载</strong></p>
<p>程序源码 <a href="http://carey-blog-image.googlecode.com/files/BaiduCreateBlog%2820120205%29.zip">BaiduCreateBlog(20120205)</a></p>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2012/02/%e5%85%b3%e4%ba%8ejava%e7%a8%8b%e5%ba%8f%e8%87%aa%e5%8a%a8%e5%8f%91%e5%b8%83%e6%96%87%e7%ab%a0%e5%88%b0%e7%99%be%e5%ba%a6%e7%a9%ba%e9%97%b4%e7%9a%84%e7%a8%8b%e5%ba%8f%e6%9b%b4%e6%96%b0/">关于Java程序自动发布文章到百度空间的程序更新</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://blog.zhourunsheng.com/2012/02/%e5%85%b3%e4%ba%8ejava%e7%a8%8b%e5%ba%8f%e8%87%aa%e5%8a%a8%e5%8f%91%e5%b8%83%e6%96%87%e7%ab%a0%e5%88%b0%e7%99%be%e5%ba%a6%e7%a9%ba%e9%97%b4%e7%9a%84%e7%a8%8b%e5%ba%8f%e6%9b%b4%e6%96%b0/feed/</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
		<item>
		<title>Heroku 上的 Play Framework（Java）</title>
		<link>http://blog.zhourunsheng.com/2011/09/heroku-%e4%b8%8a%e7%9a%84-play-framework%ef%bc%88java%ef%bc%89/</link>
		<comments>http://blog.zhourunsheng.com/2011/09/heroku-%e4%b8%8a%e7%9a%84-play-framework%ef%bc%88java%ef%bc%89/#comments</comments>
		<pubDate>Sun, 04 Sep 2011 03:06:06 +0000</pubDate>
		<dc:creator><![CDATA[润物无声]]></dc:creator>
				<category><![CDATA[云计算]]></category>
		<category><![CDATA[Heroku]]></category>
		<category><![CDATA[Java]]></category>

		<guid isPermaLink="false">http://blog.zhourunsheng.com/?p=706</guid>
		<description><![CDATA[<p>上周Heroku开始支持Java程序设计，具体的请参见我的上一篇博文《Heroku 上的 Java 程序设计》 [&#8230;]</p>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2011/09/heroku-%e4%b8%8a%e7%9a%84-play-framework%ef%bc%88java%ef%bc%89/">Heroku 上的 Play Framework（Java）</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></description>
				<content:encoded><![CDATA[<p>上周Heroku开始支持Java程序设计，具体的请参见我的上一篇博文《<a title="Heroku 上的 Java 程序设计" href="http://blog.zhourunsheng.com/2011/08/heroku-%e4%b8%8a%e7%9a%84-java-%e7%a8%8b%e5%ba%8f%e8%ae%be%e8%ae%a1/">Heroku 上的 Java 程序设计</a>》，本周Heroku开始支持Play框架，本文就带领大家熟悉一下基于play框架的程序开发流程。</p>
<h3>Play是什么</h3>
<p>Play是 <a href="http://www.infoq.com/news/2011/07/play-composition">一个Java Web框架</a> 。针对Web开发，Play采用了“净室”方法，不会强加约束，例如：</p>
<blockquote><p>兼容Servlet容器、支持JSP、兼容标准Java Web App布局、顺应Java和OO原则。</p></blockquote>
<p>Play遵循Ruby on Rails的“Built-and-Deploy”模型，而不是更传统的“Package-and-Distribute”模型。</p>
<blockquote><p>不需要公式化的类或XML配置文件。框架采用了全新的打包惯例，在适当的地方使用了静态代码。举例来说，控制器入口点是无状态的，它面向HTTP而非面向对象，因此可以用静态方法来实现。</p></blockquote>
<p>Play基于无容器的PaaS模型。Play应用可以运行于本地，也可以无缝地部署到生产环境。这样一来就可以简化部署工作流，消除由环境差异导致的问题。<br />
从架构角度来看，Play使用了<a href="http://www.jboss.org/netty">Netty</a>，这是一个由<a href="http://www.jboss.org/about.html">JBoss团队</a>构建的非阻塞I/O协议库，它使用基于Continuation的编程模型，可以支持请求的异步处理。Play还实现了<a href="http://www.infoq.com/presentations/Horizontal-Scalability">Share-Nothing</a>模型，可以很方便地通过添加节点对应于程序进行水平扩展，有状态会话是无法做到这点的。<span id="more-706"></span></p>
<h3>怎样使用Play</h3>
<ol>
<li>安装 heroku 客户端开发环境，参见  <a href="http://toolbelt.herokuapp.com/linux/readme">Linux</a>, <a href="http://toolbelt.herokuapp.com/osx/download">Mac</a>,  <a href="http://toolbelt.herokuapp.com/windows/download">Windows</a>.</li>
<li>安装 <a href="http://git-scm.com/">git</a>  客户端软件和配置 ssh key，参见 <a href="http://help.github.com/mac-key-setup/">Mac</a>，<a href="http://help.github.com/msysgit-key-setup/">Windows</a> 和 <a href="http://help.github.com/linux-key-setup/">Linux</a></li>
<li>安装 <a href="http://www.playframework.org/download">Play! version 1.2.3</a></li>
<li>登录 Heroku :
<pre>heroku auth:login</pre>
</li>
<li>创建一个 Play! app:
<pre>play new play_hello_carey
cd play_hello_carey</pre>
<p><img class="alignnone size-medium wp-image-708" title="new_play_hello_carey" src="http://blog.zhourunsheng.com/wp-content/uploads/2011/09/new_play_hello_carey-300x173.png" alt="" width="300" height="173" /></li>
<li>本地运行 app :
<pre>play run --%production</pre>
<p><img class="alignnone size-medium wp-image-712" title="play_run_production" src="http://blog.zhourunsheng.com/wp-content/uploads/2011/09/play_run_production-300x173.png" alt="" width="300" height="173" /></li>
<li>建立 git repo:
<pre>git init
git add app conf lib public test
git commit -m init</pre>
</li>
<li>在 Heroku 上建立一个新的 app :
<pre>heroku create -s cedar</pre>
<p><img class="alignnone size-medium wp-image-713" title="create_new_app" src="http://blog.zhourunsheng.com/wp-content/uploads/2011/09/create_new_app-300x42.png" alt="" width="300" height="42" /></li>
<li>上传 play_hello_carey app 到 Heroku:
<pre>git push heroku master</pre>
</li>
<li>运行 app，浏览器中访问app网址 :
<pre>heroku open</pre>
<p><img class="alignnone size-medium wp-image-714" title="run_app" src="http://blog.zhourunsheng.com/wp-content/uploads/2011/09/run_app-300x194.png" alt="" width="300" height="194" /></li>
</ol>
<p>通过这个流程，相信大家可以知道怎样在Heroku上面部署安装基于Play框架的Java程序了。</p>
<h4>参考文献</h4>
<ul>
<li><a href="http://www.jamesward.com/2011/08/29/getting-started-with-play-framework-on-heroku">getting-started-with-play-framework-on-heroku</a></li>
<li><a href="http://www.infoq.com/cn/news/2011/09/play-heroku">play-heroku</a></li>
</ul>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2011/09/heroku-%e4%b8%8a%e7%9a%84-play-framework%ef%bc%88java%ef%bc%89/">Heroku 上的 Play Framework（Java）</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://blog.zhourunsheng.com/2011/09/heroku-%e4%b8%8a%e7%9a%84-play-framework%ef%bc%88java%ef%bc%89/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Heroku 上的 Java 程序设计</title>
		<link>http://blog.zhourunsheng.com/2011/08/heroku-%e4%b8%8a%e7%9a%84-java-%e7%a8%8b%e5%ba%8f%e8%ae%be%e8%ae%a1/</link>
		<comments>http://blog.zhourunsheng.com/2011/08/heroku-%e4%b8%8a%e7%9a%84-java-%e7%a8%8b%e5%ba%8f%e8%ae%be%e8%ae%a1/#comments</comments>
		<pubDate>Sat, 27 Aug 2011 02:07:57 +0000</pubDate>
		<dc:creator><![CDATA[润物无声]]></dc:creator>
				<category><![CDATA[云计算]]></category>
		<category><![CDATA[Heroku]]></category>
		<category><![CDATA[Java]]></category>

		<guid isPermaLink="false">http://blog.zhourunsheng.com/?p=698</guid>
		<description><![CDATA[<p>Heroku简介 Heroku is a Polyglot Cloud Application Platfor [&#8230;]</p>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2011/08/heroku-%e4%b8%8a%e7%9a%84-java-%e7%a8%8b%e5%ba%8f%e8%ae%be%e8%ae%a1/">Heroku 上的 Java 程序设计</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></description>
				<content:encoded><![CDATA[<h3>Heroku简介</h3>
<p>Heroku is a <a href="http://blog.heroku.com/archives/2011/8/3/polyglot_platform/">Polyglot Cloud Application Platform</a>. Heroku provides us a way to run Ruby, Node.js, Clojure, and Java applications on a managed, scalable, and multi-tenant system. Heroku also provides <a href="http://addons.heroku.com/">numerous add-ons</a> that help us make the <a href="http://www.jamesward.com/2011/07/12/architectural-evolution-from-middleware-to-the-cloud">shift from monolithic middleware to Cloud Components</a>. Another way to say it is:</p>
<blockquote><p>Heroku = Polyglot + Platform as a Service (PaaS) + Cloud Components</p></blockquote>
<p>Heroku是一个支持<a href="http://blog.heroku.com/archives/2011/8/3/polyglot_platform/">多语言的云应用平台</a>。 它为我们提供了一个支撑的环境来运行Ruby，Node.js，Clojure，和Java应用程序，并且是可扩展，多租户的系统。 Heroku还提供了众多的<a href="http://addons.heroku.com/">组件</a>，帮助我们从<a href="http://www.jamesward.com/2011/07/12/architectural-evolution-from-middleware-to-the-cloud">单片中间件转移到云组件</a>。另一种说法是：</p>
<blockquote><p>Heroku = 多语种 + 平台作为服务（PaaS）+ 云组件</p></blockquote>
<p><span id="more-698"></span></p>
<h3>Heroku 上运行Java程序</h3>
<p>Heroku can run any Java app that runs in OpenJDK 6. Today Heroku uses Maven to create a “<a href="http://devcenter.heroku.com/articles/slug-compiler">slug</a>” for Java apps. That slug can then be loaded onto one or more “<a href="http://devcenter.heroku.com/articles/dynos">dynos</a>“. You can tell a dyno to execute / start a Java app from the command line and you can also use a “<a href="http://devcenter.heroku.com/articles/procfile">Procfile</a>” to provide a command that will auto-start for each instance of a specific dyno type. Web dynos are able to listen on a port and will receive HTTP traffic through a load balancer that is automatically setup for each app. With that background knowledge, lets dive into code!</p>
<p>Heroku 的Java运行时环境为OpenJDK 6，通过用Maven来编译程序，然后将程序上传到云端，用命令行方式来启动执行Java应用程序，云端会根据HTTP流量自动调节负载平衡，并启动一个运行实例。</p>
<p>具体步骤如下：</p>
<ol>
<li>安装 heroku 客户端软件，参见  <a href="http://toolbelt.herokuapp.com/linux/readme">Linux</a>，<a href="http://toolbelt.herokuapp.com/osx/download">Mac</a> 和  <a href="http://toolbelt.herokuapp.com/windows/download">Windows</a></li>
<li>安装 <a href="http://git-scm.com/">git</a>  客户端软件和配置 ssh key，参见 <a href="http://help.github.com/mac-key-setup/">Mac</a>，<a href="http://help.github.com/msysgit-key-setup/">Windows</a> 和 <a href="http://help.github.com/linux-key-setup/">Linux</a></li>
<li>安装 <a href="http://maven.apache.org/">Maven</a></li>
<li>从命令行登入 Heroku :
<div>
<div>
<pre>heroku auth:login
依次输入Email和Password，如果是第一次登录的话会要求上传SSH key文件，选择yes就行，然后heroku会将这些信息保存在本地，以后登录就不需要再次输入了。</pre>
</div>
</div>
</li>
<li>建立一个新的工程目录:
<div>
<div>
<pre>mkdir herokuhellojava
cd herokuhellojava</pre>
</div>
</div>
</li>
<li>新建 Maven 配置文件 <code>pom.xml</code>:
<div>
<div>
<pre>&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"&gt;
    &lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt;
    &lt;groupId&gt;foo&lt;/groupId&gt;
    &lt;version&gt;1.0-SNAPSHOT&lt;/version&gt;
    &lt;name&gt;herokuhellojava&lt;/name&gt;
    &lt;artifactId&gt;herokuhellojava&lt;/artifactId&gt;
&lt;/project&gt;</pre>
</div>
</div>
</li>
<li>建立Java source 目录:
<div>
<div>
<pre>mkdir -p src/main/java</pre>
</div>
</div>
</li>
<li>在 <code>src/main/java</code> 目录中新建立一个Java 源文件 <code>Hello.java</code> :
<div>
<div>
<pre>public class Hello
{
  public static void main(String[] args)
  {
    System.out.println("hello, Carey");
  }
}</pre>
</div>
</div>
</li>
<li>编译工程项目:
<div>
<div>
<pre>mvn compile</pre>
</div>
</div>
</li>
<li>本地运行Java程序:
<div>
<div>
<pre>java -cp target/classes Hello
程序输出 hello, Carey</pre>
</div>
</div>
</li>
<li>建立git本地repo，然后将 <code>pom.xml</code> 和 <code>src</code> 文件夹加入进去:
<div>
<div>
<pre>git init
git add pom.xml src
git commit -m init</pre>
</div>
</div>
</li>
<li>利用cedar栈在Heroku上建立一个新的APP:
<div>
<div>
<pre>heroku create -s cedar
程序输出新建立的APP的信息，比如 stark-lightning-113</pre>
</div>
</div>
</li>
<li>把本地的java程序上传到Heroku:
<div>
<div>
<pre>git push heroku master</pre>
</div>
</div>
<p>Heroku 会自动建立一个 与此APP对应的 slug .</li>
<li>在 Heroku上远程运行java程序:
<div>
<div>
<pre>heroku run "java -cp target/classes Hello"</pre>
</div>
</div>
<p>Heroku 会新建立一个 dyno 环境，装载此APP的 slug，然后运行该APP</p>
<p><img title="herokuhellojava" src="http://blog.zhourunsheng.com/wp-content/uploads/2011/08/herokuhellojava-300x173.jpg" alt="" width="300" height="173" /></li>
</ol>
<p>到目前为止，你已经开始在云端运行Java程序了，虽然只是个简单的Demo程序，但是至少知道了整个部署和执行的过程，接下来要学习的东西还有很多很多。</p>
<h3>下一步工作</h3>
<ul>
<li>阅读书籍 <a href="http://github.com/heroku/java-workbook">Heroku for Java Workbook</a></li>
<li>阅读文章 <a href="http://devcenter.heroku.com/articles/java">Heroku Dev Center</a></li>
<li>问题讨论 <a href="http://stackoverflow.com/">StackOverflow</a></li>
</ul>
<h3>参考文献</h3>
<p><a href="http://www.jamesward.com/2011/08/25/heroku-adds-java-support">heroku-adds-java-support</a></p>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2011/08/heroku-%e4%b8%8a%e7%9a%84-java-%e7%a8%8b%e5%ba%8f%e8%ae%be%e8%ae%a1/">Heroku 上的 Java 程序设计</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://blog.zhourunsheng.com/2011/08/heroku-%e4%b8%8a%e7%9a%84-java-%e7%a8%8b%e5%ba%8f%e8%ae%be%e8%ae%a1/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>电话归属地查询之Android解决方案</title>
		<link>http://blog.zhourunsheng.com/2011/08/%e7%94%b5%e8%af%9d%e5%bd%92%e5%b1%9e%e5%9c%b0%e6%9f%a5%e8%af%a2%e4%b9%8bandroid%e8%a7%a3%e5%86%b3%e6%96%b9%e6%a1%88/</link>
		<comments>http://blog.zhourunsheng.com/2011/08/%e7%94%b5%e8%af%9d%e5%bd%92%e5%b1%9e%e5%9c%b0%e6%9f%a5%e8%af%a2%e4%b9%8bandroid%e8%a7%a3%e5%86%b3%e6%96%b9%e6%a1%88/#comments</comments>
		<pubDate>Sun, 14 Aug 2011 02:19:06 +0000</pubDate>
		<dc:creator><![CDATA[润物无声]]></dc:creator>
				<category><![CDATA[移动开发]]></category>
		<category><![CDATA[Android]]></category>
		<category><![CDATA[Java]]></category>

		<guid isPermaLink="false">http://blog.zhourunsheng.com/?p=682</guid>
		<description><![CDATA[<p>最近接触的一个项目中，其中有一个功能需求就是号码归属地的查询，乍一看确实挺简单，反正数据库也都有了，只不过就是 [&#8230;]</p>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2011/08/%e7%94%b5%e8%af%9d%e5%bd%92%e5%b1%9e%e5%9c%b0%e6%9f%a5%e8%af%a2%e4%b9%8bandroid%e8%a7%a3%e5%86%b3%e6%96%b9%e6%a1%88/">电话归属地查询之Android解决方案</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></description>
				<content:encoded><![CDATA[<p>最近接触的一个项目中，其中有一个功能需求就是号码归属地的查询，乍一看确实挺简单，反正数据库也都有了，只不过就是查询一下而已嘛！到了实际程序设计的时候才发现，5M的数据库光要加载起来就得1分多钟，放在android手机上跑的太慢了，没办法，只好另辟蹊径了！！！</p>
<p>本文的基本思路如下：</p>
<p>1.  先把数据进行分组，即每一个地区一个组，例如</p>
<p>1898742 1898743 1898744 :云南曲靖</p>
<p>1894380 1894381 1894382 :吉林松原</p>
<p>2.  把电话号码进行排序，目的就是为了找到电话号码的区间，例如</p>
<p>1894815 --&gt; 1899819 :广东珠海，</p>
<p>找到了一个区段，这样就不用把每个电话号码读存储下来， 只要存储一个区间就好，</p>
<p>这样可以大大节省存储空间</p>
<p>3. 设计新的存储格式，本文的程序采用如下方式存储</p>
<table align="center">
<tbody>
<tr align="center">
<td>第一条电话记录在文件中的位置偏移</td>
<td>最后一条电话记录在文件中的位置偏移</td>
</tr>
<tr align="center">
<td colspan="2">电话归属地的字符串（例如：辽宁大连，湖北武汉，广东珠海，广东深圳..., 字符串之间以逗号分隔）</td>
</tr>
<tr>
<td colspan="2">第一条电话记录 （例如：1894815{代表号码段起始值} 5{代表连续的号码个数} 2{代表该归属地字符串在所有归属地字符串中的偏移量}）</td>
</tr>
<tr align="center">
<td colspan="2">第二条电话记录</td>
</tr>
<tr align="center">
<td colspan="2">...</td>
</tr>
<tr align="center">
<td colspan="2">最后一条电话记录</td>
</tr>
</tbody>
</table>
<p>4. 归属地查询</p>
<p>根据用户输入的电话号码，利用二分查找法可快速定位到该记录在文件中的位置偏移，读出该记录中位置字符串的偏移值，进而查表即可找到归属地</p>
<p><span id="more-682"></span></p>
<p>程序设计，源码如下：</p>
<pre>package com.carey.tel;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;

public class JavaTelArea {
     private static JavaTelArea jta = null;
     private static final String INDEXDATAFILE = "tel.bin";
     private static Hearder cacheHearder = null;
     private static AreaCode cacheAreaCode = null;
     private static String cacheIndexFilePath = null;

     public static void main(String[] args) {
          JavaTelArea ctc = JavaTelArea.getInstance();
          //ctc.genIndexFile("e:\", "e:\telphone.txt");
          String indexPath = "e:\";

          long startTime = new Date().getTime();
          String searchNum = "13889650920";
          String res = ctc.searchTel(indexPath, searchNum);
          System.out.println(searchNum + " : " + res);
          System.out.println(System.currentTimeMillis() - startTime + "ms");

          startTime = new Date().getTime();
          searchNum = "+8613659867583";
          res = ctc.searchTel(indexPath, searchNum);
          System.out.println(searchNum + " : " + res);
          System.out.println(System.currentTimeMillis() - startTime + "ms");

          startTime = new Date().getTime();
          searchNum = "1301815";
          res = ctc.searchTel(indexPath, searchNum);
          System.out.println(searchNum + " : " + res);
          System.out.println(System.currentTimeMillis() - startTime + "ms");

          startTime = new Date().getTime();
          searchNum = "1301816";
          res = ctc.searchTel(indexPath, searchNum);
          System.out.println("没有预测");
          System.out.println(searchNum + " : " + res);
          System.out.println(System.currentTimeMillis() - startTime + "ms");

          startTime = new Date().getTime();
          searchNum = "1301816";
	  res = ctc.searchTel(indexPath, searchNum, true);
	  System.out.println("根据号码连贯性原理预测");
          System.out.println(searchNum + " : " + res);
          System.out.println(System.currentTimeMillis() - startTime + "ms");

          startTime = new Date().getTime();
          searchNum = "1301817";
          res = ctc.searchTel(indexPath, searchNum);
          System.out.println(searchNum + " : " + res);
          System.out.println(System.currentTimeMillis() - startTime + "ms");
    }

    private HashMap&lt;Long, String&gt; generateTestData() {
          HashMap&lt;Long, String&gt;
          telToAreaCode = new HashMap&lt;Long, String&gt;();
          telToAreaCode.put(1310944l, "新疆伊犁州");
          telToAreaCode.put(1301263l, "新疆伊犁州");
          telToAreaCode.put(1301264l, "新疆伊犁州");
          telToAreaCode.put(1301260l, "新疆伊犁州");
          telToAreaCode.put(955L, "海南");
          telToAreaCode.put(1320955l, "海南");
          telToAreaCode.put(1320957l, "海南");
          telToAreaCode.put(1300561L, "陕西商州");
          telToAreaCode.put(1300562L, "陕西商州");
          return telToAreaCode;
    }

    public static synchronized JavaTelArea getInstance() {
         if (jta == null) {
               jta = new JavaTelArea();
         }
         return jta;
    }

   /**	 * Generate Index File (tel.bin)	 * */
   private void genIndexFile(String indexFilePath, String souceFile) {
         ArrayList&lt;String&gt; strs = readFileToList(souceFile);
         HashMap&lt;Long, String&gt;
         telToArea = createTel2AreaHashMap(strs);
         writeDate(indexFilePath + INDEXDATAFILE, telToArea);
   }

   /**	 * read file content to String array list, every line one string.	 * */
   private ArrayList&lt;String&gt; readFileToList(String filePath) {
        final ArrayList&lt;String&gt;
        strLists = new ArrayList&lt;String&gt;();

        BufferedReader bReader = null;
        try {
              bReader = new BufferedReader(new InputStreamReader(new FileInputStream(filePath)));
              String str = bReader.readLine();
              while (str != null) {
                   strLists.add(str);
                   str = bReader.readLine();
              }
         } catch (Exception e) {
             e.printStackTrace();
         } finally {
           if (bReader != null) {
               try {
                     bReader.close();
               } catch (IOException e) {
                    e.printStackTrace();
               }
          }
       }
       return strLists;
    }

     /**	 * create telephone number to area hash map.	 * */
     HashMap&lt;Long, String&gt; createTel2AreaHashMap(ArrayList&lt;String&gt; strs) {
           final HashMap&lt;Long, String&gt;
           telToArea = new HashMap&lt;Long, String&gt;();

           String[] tels = null;
           int len = 0;
           String strArea = null;
           for (String string : strs) {
               tels = string.split(" ");
               len = tels.length;
               strArea = tels[len - 1].substring(1);

               for (int i = 0; i &lt; len - 1; i++) {
                    telToArea.put(Long.valueOf(tels[i]), strArea);
               }
           }
         return telToArea;
    }

    /**	 * combined the adjacent Records.	 * */
    private void combinedRecords(ArrayList&lt;Record&gt; records, Record newRecord) {
          int size = records.size();
          if (size &gt; 0&amp;&amp; records.get(size - 1).areacodeIndex == newRecord.areacodeIndex) {
               // combined
              Record lastRecord = records.get(size - 1);
              lastRecord.numCnt = (int) (newRecord.baseTelNum - lastRecord.baseTelNum) + newRecord.numCnt;
         } else {
              records.add(newRecord);
         }
    }

   /**	 * write index info to file.	 * */
   private void writeDate(String filePath, HashMap&lt;Long, String&gt; telToAreaCode) {
       // 1. get all area info
       ArrayList&lt;String&gt; tmpAreaCodes = new ArrayList&lt;String&gt;(telToAreaCode.values());
       ArrayList&lt;String&gt; strAreaCodes = new ArrayList&lt;String&gt;();
       for (String str : tmpAreaCodes) {
            if (!strAreaCodes.contains(str)) {
                    strAreaCodes.add(str);
            }
       }
       tmpAreaCodes.clear();
       tmpAreaCodes = null;

       StringBuffer sb = new StringBuffer();
       for (String str : strAreaCodes) {
                 sb.append(str + ",");
       }
       sb.deleteCharAt(sb.length() - 1);

       AreaCode areaCode = new AreaCode(sb.toString());
       areaCode.print();

       // 2. Sort HashMap and combined the adjacent telephone number
       ArrayList&lt;Long&gt; telNunms = new ArrayList&lt;Long&gt;(telToAreaCode.keySet());
       Collections.sort(telNunms);
       ArrayList&lt;Record&gt; records = new ArrayList&lt;Record&gt;();
       long baseNum = 0;
       String baseArea = null;
       int numCnt = 0;
       for (Long tm : telNunms) {
             if (numCnt == 0) {
                    baseNum = tm;
                    baseArea = telToAreaCode.get(tm);
                    numCnt = 1;
             } else {
                    if (tm == baseNum + numCnt &amp;&amp; baseArea.equals(telToAreaCode.get(tm))) {
			  numCnt++;
                    } else {
                         combinedRecords(records, new Record(baseNum, numCnt,	strAreaCodes.indexOf(baseArea)));
                         baseNum = tm;
                         baseArea = telToAreaCode.get(tm);
                         numCnt = 1;
                    }
             }
          }
         combinedRecords(records,	new Record(baseNum, numCnt, strAreaCodes.indexOf(baseArea)));		

         // for (Record record : records) {
               // record.print();
         // }

         // 3. Write data to the file
         RandomAccessFile raf = null;
         try {
                raf = new RandomAccessFile(filePath, "rw");
                raf.seek(0);

                Hearder hearder = new Hearder();
                hearder.firstRecordOffset = hearder.Size() + areaCode.Size();
                hearder.lastRecordOffset = hearder.firstRecordOffset	+ (records.size() - 1) * records.get(0).Size();
                hearder.print();
                hearder.write(raf);

                areaCode.write(raf);

                for (Record record : records) {
                       record.write(raf);
                }
          } catch (Exception e) {
              e.printStackTrace();
          } finally {
              if (raf != null) {
                    try {
                           raf.close();
                     } catch (IOException e) {
                            e.printStackTrace();
                     }
              }
          }
    }

   class Hearder {
         int firstRecordOffset;
         int lastRecordOffset;

         public int Size() {
                 return (Integer.SIZE + Integer.SIZE) / Byte.SIZE;
         }

         public void write(RandomAccessFile raf) throws IOException {
                raf.writeInt(this.firstRecordOffset);
                raf.writeInt(this.lastRecordOffset);
         }

        public void read(RandomAccessFile raf) throws IOException {
               this.firstRecordOffset = raf.readInt();
               this.lastRecordOffset = raf.readInt();
        }

        public void print() {
                System.out.println("===== Hearder ===== ");
                System.out.println("[" + firstRecordOffset + " , "	 + lastRecordOffset + "]");
        }
    }

   class AreaCode {
          private String areacode;
          private String[] codes;

          public AreaCode() {
               this("");
          }

          public AreaCode(String areacode) {
               this.areacode = areacode;
               this.codes = null;
          }

          public int Size() {
                 return areacode.getBytes().length + (Integer.SIZE / Byte.SIZE);
          }

          public void print() {
                System.out.println("===== AreaCode ===== ");
                System.out.println("[" + areacode.getBytes().length + "]" + areacode);
          }

         public void write(RandomAccessFile raf) throws IOException {
		raf.writeInt(areacode.getBytes().length);
                raf.write(this.areacode.getBytes());
         }

         public void read(RandomAccessFile raf) throws IOException {
                byte[] bytes = new byte[raf.readInt()];
                raf.read(bytes);
                this.areacode = new String(bytes);
         }

         public String getCodeByIndex(int index) {
             if (this.codes == null) {
                   this.codes = this.areacode.split(",");
             }
             return (index &lt; 0 || this.codes == null || index &gt;= this.codes.length) ? null	 : this.codes[index];
        }
}

class Record {
       long baseTelNum;
       int numCnt;
       int areacodeIndex;

       public Record() {
            this(0, 0, 0);
       }

       public Record(long baseTelNum, int numCnt, int areacodeIndex) {
             this.baseTelNum = baseTelNum;
             this.numCnt = numCnt;
             this.areacodeIndex = areacodeIndex;
       }

       public void print() {
             System.out.println("===== Record ===== ");
             System.out.println("&lt;" + baseTelNum + "&gt; &lt;" + numCnt + "&gt; &lt;" + areacodeIndex + "&gt;");
       }

      public int Size() {
             return (Long.SIZE + Integer.SIZE) / Byte.SIZE;
      }

      public void write(RandomAccessFile raf) throws IOException {
           raf.writeLong(this.baseTelNum);
           int tmp = this.numCnt &lt;&lt; 16;
           tmp += 0xFFFF &amp; this.areacodeIndex;
           raf.writeInt(tmp);
      }

     public void read(RandomAccessFile raf) throws IOException {
          this.baseTelNum = raf.readLong();
          int tmp = raf.readInt();
          this.numCnt = tmp &gt;&gt; 16;
          this.areacodeIndex = 0xFFFF &amp; tmp;
     }

     public int inWhich(long telNum) {
         if (telNum &lt; this.baseTelNum) {
               return -1;
         } else if (telNum &gt;= this.baseTelNum + this.numCnt) {
               return 1;
         } else {
               return 0;
         }
    }
 }

  public String searchTel(String indexFilePath, String telNum) {
          return searchTel(indexFilePath, telNum, false);
  }

  /**	 * search	 * */
  public String searchTel(String indexFilePath, String telNum, boolean forecast) {
	StringBuffer sb = new StringBuffer(telNum);

	// +
	if (sb.charAt(0) == '+') {
		sb.deleteCharAt(0);
	}

	// 86
	if (sb.charAt(0) == '8' &amp;&amp; sb.charAt(1) == '6') {
		sb.delete(0, 2);
	}

	// 以0开头，是区号
	if (sb.charAt(0) == '0') {
		sb.deleteCharAt(0);
		// 首先按照4位区号查询，若查询为空，再按3位区号查询
		if (sb.length() &gt;= 3) {
			sb.delete(3, sb.length());
		} 

		String dial = searchTel(indexFilePath, Long.valueOf(sb.toString()),false);
			
		if (dial != null) {
			return dial;
		}
			
		if (sb.length() &gt;= 2) {
			sb.delete(2, sb.length());
		}
	}
	// 以1开头，是手机号或者服务行业号码
	else if (sb.charAt(0) == '1') {
		// 首先按照手机号码查询，若查询为空，再按特殊号码查询
			
		if (sb.length() &gt; 7) {
			String dial = searchTel(indexFilePath, Long.valueOf(sb.substring(0, 8)),false);
				
			if (dial != null) {
				return dial;
			}
				
			dial = searchTel(indexFilePath, Long.valueOf(sb.toString()),false);
				
			if (dial != null) {
				return dial;
			}
				
			// 只需要保留7位号码就ok了，多余的删掉
			if (sb.length() &gt; 7) {
				sb.delete(7, sb.length());
			}
		} else {
			//小于7位，最有可能是服务号码
			//do nothing.
		}
	}
	// 以其他数字开头，这也不知道是啥号码了
	else {
		//do nothing.
	}

	return searchTel(indexFilePath, Long.valueOf(sb.toString()), forecast);
  }

 private String searchTel(String indexFilePath, long telNum, boolean forecast) {
          RandomAccessFile raf = null;
          try {
             raf = new RandomAccessFile(indexFilePath + INDEXDATAFILE, "r");
             if (cacheIndexFilePath == null || !cacheIndexFilePath.equals(indexFilePath)) {
                cacheIndexFilePath = indexFilePath;
                cacheHearder = new Hearder();
                cacheHearder.read(raf);
                cacheHearder.print();

                cacheAreaCode = new AreaCode();
                cacheAreaCode.read(raf);
                cacheAreaCode.print();
             }

            int index = lookUP(raf, cacheHearder.firstRecordOffset, cacheHearder.lastRecordOffset, telNum, forecast);
            return cacheAreaCode.getCodeByIndex(index);
         } catch (Exception e) {
              e.printStackTrace();
         } finally {
              if (raf != null) {
                  try {
                         raf.close();
                  } catch (IOException e) {
                       e.printStackTrace();
                  }
               }
         }

      return null;
  }

  private int lookUP(RandomAccessFile raf, long startpos, long endpos, long looknum, boolean forecast) throws IOException {
      Record record = new Record();
      long seekpos = 0;

      do {
            seekpos = startpos + (endpos - startpos) / record.Size() / 2 * record.Size();
            raf.seek(seekpos);
            record.read(raf);

           if (record.inWhich(looknum) &gt; 0) {
                 startpos = seekpos + record.Size();
           } else if (record.inWhich(looknum) &lt; 0) {
                 endpos = seekpos - record.Size();
           } else {
                 return record.areacodeIndex;
           }
      } while (startpos &lt;= endpos);

     if (forecast) {
            return record.areacodeIndex;
     } else {
           return -1;
     }
  }
}</pre>
<p>程序运行情况如下：</p>
<pre>==== Hearder =====
[4554 , 605622]
===== AreaCode ===== 
[4542]北福建南平,福建三明,海果洛,青海海南,...
13889650920 : 辽宁大连
20ms
+8613659867583 : 湖北武汉
2ms
1301815 : 四川泸州
2ms
没有预测1301816 : null
2ms
根据号码连贯性原理预测1301816 : 四川泸州
1ms
1301817 : 四川宜宾
2ms</pre>
<p>可以看到，除了第一次查询的时候要加载索引文件大约耗时20ms，以后的查询基本都在1ms，速度非常快了！！！</p>
<p>本程序的测试data文件下载<a href="http://blog.zhourunsheng.com/wp-content/uploads/2011/08/telinfo.zip">telinfo</a></p>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2011/08/%e7%94%b5%e8%af%9d%e5%bd%92%e5%b1%9e%e5%9c%b0%e6%9f%a5%e8%af%a2%e4%b9%8bandroid%e8%a7%a3%e5%86%b3%e6%96%b9%e6%a1%88/">电话归属地查询之Android解决方案</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://blog.zhourunsheng.com/2011/08/%e7%94%b5%e8%af%9d%e5%bd%92%e5%b1%9e%e5%9c%b0%e6%9f%a5%e8%af%a2%e4%b9%8bandroid%e8%a7%a3%e5%86%b3%e6%96%b9%e6%a1%88/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>Java深度解析之安全与对象序列化RMI</title>
		<link>http://blog.zhourunsheng.com/2011/08/java%e6%b7%b1%e5%ba%a6%e8%a7%a3%e6%9e%90%e4%b9%8b%e5%ae%89%e5%85%a8%e4%b8%8e%e5%af%b9%e8%b1%a1%e5%ba%8f%e5%88%97%e5%8c%96rmi/</link>
		<comments>http://blog.zhourunsheng.com/2011/08/java%e6%b7%b1%e5%ba%a6%e8%a7%a3%e6%9e%90%e4%b9%8b%e5%ae%89%e5%85%a8%e4%b8%8e%e5%af%b9%e8%b1%a1%e5%ba%8f%e5%88%97%e5%8c%96rmi/#comments</comments>
		<pubDate>Tue, 02 Aug 2011 08:32:02 +0000</pubDate>
		<dc:creator><![CDATA[润物无声]]></dc:creator>
				<category><![CDATA[程序设计]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[RMI]]></category>

		<guid isPermaLink="false">http://blog.zhourunsheng.com/?p=632</guid>
		<description><![CDATA[<p>本系列的文章转载自infoq专栏合集《Java深度历险》，10篇文章分别由浅到深描述了Java的多个细节，具体 [&#8230;]</p>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2011/08/java%e6%b7%b1%e5%ba%a6%e8%a7%a3%e6%9e%90%e4%b9%8b%e5%ae%89%e5%85%a8%e4%b8%8e%e5%af%b9%e8%b1%a1%e5%ba%8f%e5%88%97%e5%8c%96rmi/">Java深度解析之安全与对象序列化RMI</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></description>
				<content:encoded><![CDATA[<p>本系列的文章转载自<a href="http://www.infoq.com/cn/minibooks/java-explore">infoq专栏合集《Java深度历险》</a>，10篇文章分别由浅到深描述了Java的多个细节，具体包括<br />
1. Java 字节代码的操纵    2. Java类的加载、链接和初始化  3. Java线程:基本概念、可见性与同步<br />
4. Java垃圾回收机制与引用类型  5. Java泛型   6. Java注解  7. Java反射与动态代理<br />
8. Java I/O   9. Java安全  10. Java对象序列化与RMI，<br />
相信通过十个主题的学习，读者能对Java的这几个方面有较深入的理解。enjoy it！</p>
<h3>Java 安全</h3>
<p>安全性是Java应用程序的非功能性需求的重要组成部分，如同其它的非功能性需求一样，安全性很容易被开发人员所忽略。当然，对于Java EE的开发人员来说，安全性的话题可能没那么陌生，用户认证和授权可能是绝大部分Web应用都有的功能。类似<a href="http://static.springsource.org/spring-security/site/">Spring Security</a>这样的框架，也使得开发变得更加简单。本文并不会讨论Web应用的安全性，而是介绍Java安全一些底层和基本的内容。<br />
<span id="more-632"></span><br />
<strong>认证</strong><br />
用户认证是应用安全性的重要组成部分，其目的是确保应用的使用者具有合法的身份。<br />
Java安全中使用术语主体（<a href="http://download.oracle.com/javase/6/docs/api/javax/security/auth/Subject.html">Subject</a>）来表示访问请求的来源。一个主体可以是任何的实体。一个主体可以有多个不同的身份标识（<a href="http://download.oracle.com/javase/6/docs/api/java/security/Principal.html">Principal</a>）。比如一个应用的用户这类主体，就可以有用户名、身份证号码和手机号码等多种身份标识。除了身份标识之外，一个主体还可以有公开或是私有的安全相关的凭证（<a href="http://en.wikipedia.org/wiki/Credential">Credential</a>），包括密码和密钥等。</p>
<p>典型的用户认证过程是通过登录操作来完成的。在登录成功之后，一个主体中就具备了相应的身份标识。Java提供了一个可扩展的登录框架，使得应用开发人员可以很容易的定制和扩展与登录相关的逻辑。登录的过程由<a href="http://download.oracle.com/javase/6/docs/api/javax/security/auth/login/LoginContext.html">LoginContext</a>启动。在创建LoginContext的时候需要指定一个登录配置（<a href="http://download.oracle.com/javase/6/docs/api/javax/security/auth/login/Configuration.html">Configuration</a>）的名称。该登录配置中包含了登录所需的多个<a href="http://download.oracle.com/javase/6/docs/api/javax/security/auth/spi/LoginModule.html">LoginModule</a>的信息。每个LoginModule实现了一种登录方式。当调用LoginContext的<a href="http://download.oracle.com/javase/6/docs/api/javax/security/auth/login/LoginContext.html#login%28%29">login</a>方法的时候，所配置的每个LoginModule会被调用来执行登录操作。如果整个登录过程成功，则通过<a href="http://download.oracle.com/javase/6/docs/api/javax/security/auth/login/LoginContext.html#getSubject%28%29">getSubject</a>方法就可以获取到包含了身份标识信息的主体。开发人员可以实现自己的LoginModule来定制不同的登录逻辑。</p>
<p>每个LoginModule的登录方式由两个阶段组成。第一个阶段是在<a href="http://download.oracle.com/javase/6/docs/api/javax/security/auth/spi/LoginModule.html#login%28%29">login</a>方法的实现中。这个阶段用来进行必要的身份认证，可能需要获取用户的输入，以及通过数据库、网络操作或其它方式来完成认证。当认证成功之后，把必要的信息保存起来。如果认证失败，则抛出相关的异常。第二阶段是在<a href="http://download.oracle.com/javase/6/docs/api/javax/security/auth/spi/LoginModule.html#commit%28%29">commit</a>或<a href="http://download.oracle.com/javase/6/docs/api/javax/security/auth/spi/LoginModule.html#abort%28%29">abort</a>方法中。由于一个登录过程可能涉及到多个LoginModule。LoginContext会根据每个LoginModule的认证结果以及相关的配置信息来确定本次登录是否成功。LoginContext用来判断的依据是每个LoginModule对整个登录过程的必要性，分成必需、必要、充分和可选这四种情况。如果登录成功，则每个LoginModule的commit方法会被调用，用来把身份标识关联到主体上。如果登录失败，则LoginModule 的abort方法会被调用，用来清除之前保存的认证相关信息。</p>
<p>在LoginModule进行认证的过程中，如果需要获取用户的输入，可以通过<a href="http://download.oracle.com/javase/6/docs/api/javax/security/auth/callback/CallbackHandler.html">CallbackHandler</a>和对应的Callback来完成。每个<a href="http://download.oracle.com/javase/6/docs/api/javax/security/auth/callback/Callback.html">Callback</a>可以用来进行必要的数据传递。典型的启动登录的过程如下：</p>
<pre>public Subject login() throws LoginException {    
    TextInputCallbackHandler callbackHandler = new TextInputCallbackHandler();    
    LoginContext lc = new LoginContext("SmsApp", callbackHandler);    
    lc.login();    
    return lc.getSubject();
}</pre>
<p>这里的SmsApp是登录配置的名称，可以在配置文件中找到。该配置文件的内容也很简单。</p>
<pre>SmsApp {    
    security.login.SmsLoginModule required;
};</pre>
<p>这里声明了使用security.login.SmsLoginModule这个登录模块，而且该模块是必需的。配置文件可以通过启动程序时的参数java.security.auth.login.config来指定，或修改JVM的默认设置。下面看看SmsLoginModule的核心方法login和commit。</p>
<pre>public boolean login() throws LoginException {    
    TextInputCallback phoneInputCallback = new TextInputCallback("Phone number: ");    
    TextInputCallback smsInputCallback = new TextInputCallback("Code: ");    
    try {        
        handler.handle(new Callback[] {phoneInputCallback, smsInputCallback});    
    } catch (Exception e) {        
        throw new LoginException(e.getMessage());    
    }     
    String code = smsInputCallback.getText();    
    boolean isValid = code.length() &gt; 3; //此处只是简单的进行验证。   
    if (isValid) {        
        phoneNumber = phoneInputCallback.getText();    
    }    
    return isValid;
}
public boolean commit() throws LoginException {    
    if (phoneNumber != null) {        
    subject.getPrincipals().add(new PhonePrincipal(phoneNumber));       
    return true;    
}    
    return false;
}</pre>
<p>这里使用了两个<a href="http://download.oracle.com/javase/6/docs/api/javax/security/auth/callback/TextInputCallback.html">TextInputCallback</a>来获取用户的输入。当用户输入的编码有效的时候，就把相关的信息记录下来，此处是用户的手机号码。在commit方法中，就把该手机号码作为用户的身份标识与主体关联起来。</p>
<p><strong>权限控制</strong><br />
在验证了访问请求来源的合法身份之后，另一项工作是验证其是否具有相应的权限。权限由<a href="http://download.oracle.com/javase/6/docs/api/java/security/Permission.html">Permission</a>及其子类来表示。每个权限都有一个名称，该名称的含义与权限类型相关。某些权限有与之对应的动作列表。比较典型的是文件操作权限<a href="http://download.oracle.com/javase/6/docs/api/java/io/FilePermission.html">FilePermission</a>，它的名称是文件的路径，而它的动作列表则包括读取、写入和执行等。Permission类中最重要的是<a href="http://download.oracle.com/javase/6/docs/api/java/security/Permission.html#implies%28java.security.Permission%29">implies</a>方法，它定义了权限之间的包含关系，是进行验证的基础。</p>
<p>权限控制包括管理和验证两个部分。管理指的是定义应用中的权限控制策略，而验证指的则是在运行时刻根据策略来判断某次请求是否合法。策略可以与主体关联，也可以没有关联。策略由<a href="http://download.oracle.com/javase/6/docs/api/java/security/Policy.html">Policy</a>来表示，JDK提供了基于文件存储的基本实现。开发人员也可以提供自己的实现。在应用运行过程中，只可能有一个Policy处于生效的状态。验证部分的具体执行者是<a href="http://download.oracle.com/javase/6/docs/api/java/security/AccessController.html">AccessController</a>，其中的<a href="http://download.oracle.com/javase/6/docs/api/java/security/AccessController.html#checkPermission%28java.security.Permission%29">checkPermission</a>方法用来验证给定的权限是否被允许。在应用中执行相关的访问请求之前，都需要调用checkPermission方法来进行验证。如果验证失败的话，该方法会抛出<a href="http://download.oracle.com/javase/6/docs/api/java/security/AccessControlException.html">AccessControlException</a>异常。 JVM中内置提供了一些对访问关键部分内容的访问控制检查，不过只有在启动应用的时通过参数-Djava.security.manager启用了安全管理器之后才能生效，并与策略相配合。</p>
<p>与访问控制相关的另外一个概念是特权动作。特权动作只关心动作本身所要求的权限是否具备，而并不关心调用者是谁。比如一个写入文件的特权动作，它只要求对该文件有写入权限即可，并不关心是谁要求它执行这样的动作。特权动作根据是否抛出受检异常，分为<a href="http://download.oracle.com/javase/6/docs/api/java/security/PrivilegedAction.html">PrivilegedAction</a>和<a href="http://download.oracle.com/javase/6/docs/api/java/security/PrivilegedExceptionAction.html">PrivilegedExceptionAction</a>。这两个接口都只有一个run方法用来执行相关的动作，也可以向调用者返回结果。通过AccessController的doPrivileged方法就可以执行特权动作。</p>
<p>Java安全使用了保护域的概念。每个保护域都包含一组类、身份标识和权限，其意义是在当访问请求的来源是这些身份标识的时候，这些类的实例就自动具有给定的这些权限。保护域的权限既可以是固定，也可以根据策略来动态变化。<a href="http://download.oracle.com/javase/6/docs/api/java/security/ProtectionDomain.html">ProtectionDomain</a>类用来表示保护域，它的两个构造方法分别用来支持静态和动态的权限。一般来说，应用程序通常会涉及到系统保护域和应用保护域。不少的方法调用可能会跨越多个保护域的边界。因此，在AccessController进行访问控制验证的时候，需要考虑当前操作的调用上下文，主要指的是方法调用栈上不同方法所属于的不同保护域。这个调用上下文一般是与当前线程绑定在一起的。通过AccessController的<a href="http://download.oracle.com/javase/6/docs/api/java/security/AccessController.html#getContext%28%29">getContext</a>方法可以获取到表示调用上下文的<a href="http://download.oracle.com/javase/6/docs/api/java/security/AccessControlContext.html">AccessControlContext</a>对象，相当于访问控制验证所需的调用栈的一个快照。在有些情况下，会需要传递此对象以方便在其它线程中进行访问控制验证。</p>
<p>考虑下面的权限验证代码：</p>
<pre>Subject subject = new Subject();
ViewerPrincipal principal = new ViewerPrincipal("Alex");
subject.getPrincipals().add(principal);
Subject.doAsPrivileged(subject, new PrivilegedAction&lt;Object&gt;() {    
    public Object run() {       
        new Viewer().view();        
        return null;   
    }
}, null);</pre>
<p>这里创建了一个新的Subject对象并关联上身份标识。通常来说，这个过程是由登录操作来完成的。通过Subject的doAsPrivileged方法就可以执行一个特权动作。Viewer对象的view方法会使用AccessController来检查是否具有相应的权限。策略配置文件的内容也比较简单，在启动程序的时候通过参数java.security.auth.policy指定文件路径即可。</p>
<pre>grant Principal security.access.ViewerPrincipal "Alex" {
    permission security.access.ViewPermission "CONFIDENTIAL";
}; //这里把名称为CONFIDENTIAL的ViewPermission授权给了身份标识为Alex的主体。</pre>
<p><strong>加密、解密与签名</strong><br />
构建安全的Java应用离不开加密和解密。Java的密码框架采用了常见的服务提供者架构，以提供所需的可扩展性和互操作性。该密码框架提供了一系列常用的服务，包括加密、数字签名和报文摘要等。这些服务都有服务提供者接口（<a href="http://en.wikipedia.org/wiki/Service_provider_interface">SPI</a>），服务的实现者只需要实现这些接口，并注册到密码框架中即可。比如加密服务<a href="http://download.oracle.com/javase/6/docs/api/javax/crypto/Cipher.html">Cipher</a>的SPI接口就是<a href="http://download.oracle.com/javase/6/docs/api/javax/crypto/CipherSpi.html">CipherSpi</a>。每个服务都可以有不同的算法来实现。密码框架也提供了相应的工厂方法用来获取到服务的实例。比如想使用采用MD5算法的报文摘要服务，只需要调用MessageDigest.getInstance("MD5")即可。</p>
<p>加密和解密过程中并不可少的就是密钥（<a href="http://download.oracle.com/javase/6/docs/api/java/security/Key.html">Key</a>）。加密算法一般分成对称和非对称两种。<a href="http://baike.baidu.com/view/119320.htm">对称加密算法</a>使用同一个密钥进行加密和解密；而<a href="http://baike.baidu.com/view/7595.htm">非对称加密算法</a>使用一对公钥和私钥，一个加密的时候，另外一个就用来解密。不同的加密算法，有不同的密钥。对称加密算法使用的是<a href="http://download.oracle.com/javase/6/docs/api/javax/crypto/SecretKey.html">SecretKey</a>，而非对称加密算法则使用<a href="http://download.oracle.com/javase/6/docs/api/java/security/PublicKey.html">PublicKey</a>和<a href="http://download.oracle.com/javase/6/docs/api/java/security/PrivateKey.html">PrivateKey</a>。与密钥Key对应的另一个接口是<a href="http://download.oracle.com/javase/6/docs/api/java/security/spec/KeySpec.html">KeySpec</a>，用来描述不同算法的密钥的具体内容。比如一个典型的使用对称加密的方式如下：</p>
<pre>KeyGenerator generator = KeyGenerator.getInstance("DES");
SecretKey key = generator.generateKey();
saveFile("key.data", key.getEncoded());
Cipher cipher = Cipher.getInstance("DES");
cipher.init(Cipher.ENCRYPT_MODE, key);
String text = "Hello World";
byte[] encrypted = cipher.doFinal(text.getBytes());
saveFile("encrypted.bin", encrypted);</pre>
<p>加密的时候首先要生成一个密钥，再由Cipher服务来完成。可以把密钥的内容保存起来，方便传递给需要解密的程序。</p>
<pre>byte[] keyData = getData("key.data");
SecretKeySpec keySpec = new SecretKeySpec(keyData, "DES");
Cipher cipher = Cipher.getInstance("DES");
cipher.init(Cipher.DECRYPT_MODE, keySpec);
byte[] data = getData("encrypted.bin");
byte[] result = cipher.doFinal(data);</pre>
<p>解密的时候先从保存的文件中得到密钥编码之后的内容，再通过<a href="http://download.oracle.com/javase/6/docs/api/javax/crypto/spec/SecretKeySpec.html">SecretKeySpec</a>获取到密钥本身的内容，再进行解密。</p>
<p><a href="http://baike.baidu.com/view/4971320.htm">报文摘要</a>的目的在于防止信息被有意或无意的修改。通过对原始数据应用某些算法，可以得到一个校验码。当收到数据之后，只需要应用同样的算法，再比较校验码是否一致，就可以判断数据是否被修改过。相对原始数据来说，校验码长度更小，更容易进行比较。消息认证码（<a href="http://en.wikipedia.org/wiki/Message_authentication_code">Message Authentication Code</a>）与报文摘要类似，不同的是计算的过程中加入了密钥，只有掌握了密钥的接收者才能验证数据的完整性。</p>
<p>使用公钥和私钥就可以实现数字签名的功能。某个发送者使用私钥对消息进行加密，接收者使用公钥进行解密。由于私钥只有发送者知道，当接收者使用公钥解密成功之后，就可以判定消息的来源肯定是特定的发送者。这就相当于发送者对消息进行了签名。数字签名由<a href="http://download.oracle.com/javase/6/docs/api/java/security/Signature.html">Signature</a>服务提供，签名和验证的过程都比较直接。</p>
<pre>Signature signature = Signature.getInstance("SHA1withDSA");
KeyPairGenerator keyGenerator = KeyPairGenerator.getInstance("DSA");
KeyPair keyPair = keyGenerator.generateKeyPair();
PrivateKey privateKey = keyPair.getPrivate();
signature.initSign(privateKey);
byte[] data = "Hello World".getBytes();
signature.update(data);
byte[] signatureData = signature.sign(); //得到签名
PublicKey publicKey = keyPair.getPublic();
signature.initVerify(publicKey);
signature.update(data);
boolean result = signature.verify(signatureData); //进行验证</pre>
<p>验证数字签名使用的公钥可以通过文件或证书的方式来进行发布。</p>
<p><strong>安全套接字连接</strong><br />
在各种数据传输方式中，网络传输目前使用较广，但是安全隐患也更多。安全套接字连接指的是对套接字连接进行加密。加密的时候可以选择对称加密算法。但是如何在发送者和接收者之间安全的共享密钥，是个很麻烦的问题。如果再用加密算法来加密密钥，则成为了一个循环问题。非对称加密算法则适合于这种情况。私钥自己保管，公钥则公开出去。发送数据的时候，用私钥加密，接收者用公开的公钥解密；接收数据的时候，则正好相反。这种做法解决了共享密钥的问题，但是另外的一个问题是如何确保接收者所得到的公钥确实来自所声明的发送者，而不是伪造的。为此，又引入了证书的概念。证书中包含了身份标识和对应的公钥。证书由用户所信任的机构签发，并用该机构的私钥来加密。在有些情况下，某个证书签发机构的真实性会需要由另外一个机构的证书来证明。通过这种证明关系，会形成一个证书的链条。而链条的根则是公认的值得信任的机构。只有当证书链条上的所有证书都被信任的时候，才能信任证书中所给出的公钥。</p>
<p>日常开发中比较常接触的就是<a href="http://en.wikipedia.org/wiki/HTTP_Secure">HTTPS</a>，即安全的HTTP连接。大部分用Java程序访问采用HTTPS网站时出现的错误都与证书链条相关。有些网站采用的不是由正规安全机构签发的证书，或是证书已经过期。如果必须访问这样的HTTPS网站的话，可以提供自己的套接字工厂和主机名验证类来绕过去。另外一种做法是通过<a href="http://download.oracle.com/javase/6/docs/technotes/tools/solaris/keytool.html">keytool</a>工具把证书导入到系统的信任证书库之中。</p>
<pre>URL url = new URL("https://localhost:8443");
SSLContext context = SSLContext.getInstance("TLS");
context.init(new KeyManager[] {}, new TrustManager[] {new MyTrustManager()}, new SecureRandom());HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
connection.setSSLSocketFactory(context.getSocketFactory());
connection.setHostnameVerifier(new MyHostnameVerifier());</pre>
<p>这里的MyTrustManager实现了<a href="http://download.oracle.com/javase/6/docs/api/javax/net/ssl/X509TrustManager.html">X509TrustManager</a>接口，但是所有方法都是默认实现。而MyHostnameVerifier实现了<a href="http://download.oracle.com/javase/6/docs/api/javax/net/ssl/HostnameVerifier.html">HostnameVerifier</a>接口，其中的verify方法总是返回true。</p>
<p><strong>参考资料</strong></p>
<ul>
<li><a href="http://download.oracle.com/javase/6/docs/technotes/guides/security/spec/security-spec.doc.html">Java安全体系结构</a></li>
<li><a href="http://download.oracle.com/javase/6/docs/technotes/guides/security/crypto/CryptoSpec.html">Java密码框架（JCA）参考指南</a></li>
<li><a href="http://download.oracle.com/javase/6/docs/technotes/guides/security/jaas/JAASRefGuide.html">Java认证和授权服务（JAAS）参考指南</a></li>
<li><a href="http://download.oracle.com/javase/6/docs/technotes/guides/security/jsse/JSSERefGuide.html">Java安全套接字扩展（JSSE）参考指南</a></li>
</ul>
<h3>Java对象序列化与RMI</h3>
<p>对于一个存在于Java虚拟机中的对象来说，其内部的状态只保持在内存中。JVM停止之后，这些状态就丢失了。在很多情况下，对象的内部状态是需要被持久化下来的。提到持久化，最直接的做法是保存到文件系统或是数据库之中。这种做法一般涉及到自定义存储格式以及繁琐的数据转换。<a href="http://en.wikipedia.org/wiki/Object-relational_mapping">对象关系映射</a>（Object-relational mapping）是一种典型的用关系数据库来持久化对象的方式，也存在很多直接存储对象的<a href="http://en.wikipedia.org/wiki/Object_database">对象数据库</a>。对象序列化机制（object serialization）是Java语言内建的一种对象持久化方式，可以很容易的在JVM中的活动对象和字节数组（流）之间进行转换。除了可以很简单的实现持久化之外，序列化机制的另外一个重要用途是在远程方法调用中，用来对开发人员屏蔽底层实现细节。</p>
<p><strong>基本的对象序列化</strong><br />
由于Java提供了良好的默认支持，实现基本的对象序列化是件比较简单的事。待序列化的Java类只需要实现<a href="http://download.oracle.com/javase/6/docs/api/java/io/Serializable.html">Serializable</a>接口即可。Serializable仅是一个标记接口，并不包含任何需要实现的具体方法。实现该接口只是为了声明该Java类的对象是可以被序列化的。实际的序列化和反序列化工作是通过<a href="http://download.oracle.com/javase/6/docs/api/java/io/ObjectOutputStream.html">ObjectOuputStream</a>和<a href="http://download.oracle.com/javase/6/docs/api/java/io/ObjectInputStream.html">ObjectInputStream</a>来完成的。ObjectOutputStream的<a href="http://download.oracle.com/javase/6/docs/api/java/io/ObjectOutputStream.html#writeObject%28java.lang.Object%29">writeObject</a>方法可以把一个Java对象写入到流中，ObjectInputStream的<a href="http://download.oracle.com/javase/6/docs/api/java/io/ObjectInputStream.html#readObject%28%29">readObject</a>方法可以从流中读取一个Java对象。在写入和读取的时候，虽然用的参数或返回值是单个对象，但实际上操纵的是一个对象图，包括该对象所引用的其它对象，以及这些对象所引用的另外的对象。Java会自动帮你遍历对象图并逐个序列化。除了对象之外，Java中的基本类型和数组也是可以通过 ObjectOutputStream和ObjectInputStream来序列化的。</p>
<pre>try {
    User user = new User("Alex", "Cheng");
    ObjectOutputStream output = new ObjectOutputStream(new FileOutputStream("user.bin"));
    output.writeObject(user);
    output.close();
} catch (IOException e) {
    e.printStackTrace();
}

try {
    ObjectInputStream input = new ObjectInputStream(new FileInputStream("user.bin"));
    User user = (User) input.readObject();
    System.out.println(user);
} catch (Exception e) {
    e.printStackTrace();
}</pre>
<p>上面的代码给出了典型的把Java对象序列化之后保存到磁盘上，以及从磁盘上读取的基本方式。 User类只是声明了实现Serializable接口。</p>
<p>在默认的序列化实现中，Java对象中的非静态和非瞬时域都会被包括进来，而与域的可见性声明没有关系。这可能会导致某些不应该出现的域被包含在序列化之后的字节数组中，比如密码等隐私信息。由于Java对象序列化之后的格式是固定的，其它人可以很容易的从中分析出其中的各种信息。对于这种情况，一种解决办法是把域声明为瞬时的，即使用<a href="http://en.wikibooks.org/wiki/Java_Programming/Keywords/transient">transient</a>关键词。另外一种做法是添加一个serialPersistentFields? 域来声明序列化时要包含的域。从这里可以看到在Java序列化机制中的这种仅在书面层次上定义的契约。声明序列化的域必须使用固定的名称和类型。在后面还可以看到其它类似这样的契约。虽然Serializable只是一个标记接口，但它其实是包含有不少隐含的要求。下面的代码给出了 serialPersistentFields的声明示例，即只有firstName这个域是要被序列化的。</p>
<pre>private static final ObjectStreamField[] serialPersistentFields = { 
    new ObjectStreamField("firstName", String.class) 
};</pre>
<p><strong>自定义对象序列化</strong><br />
基本的对象序列化机制让开发人员可以在包含哪些域上进行定制。如果想对序列化的过程进行更加细粒度的控制，就需要在类中添加writeObject和对应的 readObject方法。这两个方法属于前面提到的序列化机制的隐含契约的一部分。在通过ObjectOutputStream的 writeObject方法写入对象的时候，如果这个对象的类中定义了writeObject方法，就会调用该方法，并把当前 ObjectOutputStream对象作为参数传递进去。writeObject方法中一般会包含自定义的序列化逻辑，比如在写入之前修改域的值，或是写入额外的数据等。对于writeObject中添加的逻辑，在对应的readObject中都需要反转过来，与之对应。</p>
<p>在添加自己的逻辑之前，推荐的做法是先调用Java的默认实现。在writeObject方法中通过ObjectOutputStream的<a href="http://download.oracle.com/javase/6/docs/api/java/io/ObjectOutputStream.html#defaultWriteObject%28%29">defaultWriteObject</a>来完成，在readObject方法则通过ObjectInputStream的<a href="http://download.oracle.com/javase/6/docs/api/java/io/ObjectInputStream.html#defaultReadObject%28%29">defaultReadObject</a>来实现。下面的代码在对象的序列化流中写入了一个额外的字符串。</p>
<pre>private void writeObject(ObjectOutputStream output) throws IOException {
    output.defaultWriteObject();
    output.writeUTF("Hello World");
}
private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException {
    input.defaultReadObject();
    String value = input.readUTF();
    System.out.println(value);
}</pre>
<p><strong>序列化时的对象替换</strong><br />
在有些情况下，可能会希望在序列化的时候使用另外一个对象来代替当前对象。其中的动机可能是当前对象中包含了一些不希望被序列化的域，比如这些域都是从另外一个域派生而来的；也可能是希望隐藏实际的类层次结构；还有可能是添加自定义的对象管理逻辑，如保证某个类在JVM中只有一个实例。相对于把无关的域都设成transient来说，使用对象替换是一个更好的选择，提供了更多的灵活性。替换对象的作用类似于Java EE中会使用到的<a href="http://java.sun.com/blueprints/patterns/TransferObject.html">传输对象</a>（Transfer Object）。</p>
<p>考虑下面的例子，一个订单系统中需要把订单的相关信息序列化之后，通过网络来传输。订单类Order引用了客户类Customer。在默认序列化的情况下，Order类对象被序列化的时候，其引用的Customer类对象也会被序列化，这可能会造成用户信息的泄露。对于这种情况，可以创建一个另外的对象来在序列化的时候替换当前的Order类的对象，并把用户信息隐藏起来。</p>
<pre>private static class OrderReplace implements Serializable {
    private static final long serialVersionUID = 4654546423735192613L;
    private String orderId;
    public OrderReplace(Order order) {
        this.orderId = order.getId();
    }
    private Object readResolve() throws ObjectStreamException {
        //根据orderId查找Order对象并返回
    }
}</pre>
<p>这个替换对象类OrderReplace只保存了Order的ID。在Order类的writeReplace方法中返回了一个OrderReplace对象。这个对象会被作为替代写入到流中。同样的，需要在OrderReplace类中定义一个readResolve方法，用来在读取的时候再转换回 Order类对象。这样对调用者来说，替换对象的存在就是透明的。</p>
<pre>private Object writeReplace() throws ObjectStreamException {
    return new OrderReplace(this);
}</pre>
<p><strong>序列化与对象创建</strong><br />
在通过ObjectInputStream的readObject方法读取到一个对象之后，这个对象是一个新的实例，但是其构造方法是没有被调用的，其中的域的初始化代码也没有被执行。对于那些没有被序列化的域，在新创建出来的对象中的值都是默认的。也就是说，这个对象从某种角度上来说是不完备的。这有可能会造成一些隐含的错误。调用者并不知道对象是通过一般的new操作符来创建的，还是通过反序列化所得到的。解决的办法就是在类的readObject方法里面，再执行所需的对象初始化逻辑。对于一般的Java类来说，构造方法中包含了初始化的逻辑。可以把这些逻辑提取到一个方法中，在readObject方法中调用此方法。</p>
<p><strong>版本更新</strong><br />
把一个Java对象序列化之后，所得到的字节数组一般会保存在磁盘或数据库之中。在保存完成之后，有可能原来的Java类有了更新，比如添加了额外的域。这个时候从兼容性的角度出发，要求仍然能够读取旧版本的序列化数据。在读取的过程中，当ObjectInputStream发现一个对象的定义的时候，会尝试在当前JVM中查找其Java类定义。这个查找过程不能仅根据Java类的全名来判断，因为当前JVM中可能存在名称相同，但是含义完全不同的Java 类。这个对应关系是通过一个全局惟一标识符serialVersionUID来实现的。通过在实现了Serializable接口的类中定义该域，就声明了该Java类的一个惟一的序列化版本号。JVM会比对从字节数组中得出的类的版本号，与JVM中查找到的类的版本号是否一致，来决定两个类是否是兼容的。对于开发人员来说，需要记得的就是在实现了Serializable接口的类中定义这样的一个域，并在版本更新过程中保持该值不变。当然，如果不希望维持这种向后兼容性，换一个版本号即可。该域的值一般是综合Java类的各个特性而计算出来的一个哈希值，可以通过Java提供的<a href="http://download.oracle.com/javase/1.4.2/docs/tooldocs/solaris/serialver.html">serialver</a>命令来生成。在Eclipse中，如果Java类实现了Serializable接口，Eclipse会提示并帮你生成这个serialVersionUID。</p>
<p>在类版本更新的过程中，某些操作会破坏向后兼容性。如果希望维持这种向后兼容性，就需要格外的注意。一般来说，在新的版本中添加东西不会产生什么问题，而去掉一些域则是不行的。</p>
<p><strong>序列化安全性</strong><br />
前面提到，Java对象序列化之后的内容格式是<a href="http://download.oracle.com/javase/6/docs/platform/serialization/spec/protocol.html">公开的</a>。所以可以很容易的从中提取出各种信息。从实现的角度来说，可以从不同的层次来加强序列化的安全性。</p>
<ul>
<li>对序列化之后的流进行加密。这可以通过<a href="http://download.oracle.com/javase/6/docs/api/javax/crypto/CipherOutputStream.html">CipherOutputStream</a>来实现。</li>
<li>实现自己的writeObject和readObject方法，在调用defaultWriteObject之前，先对要序列化的域的值进行加密处理。</li>
<li>使用一个<a href="http://download.oracle.com/javase/6/docs/api/java/security/SignedObject.html">SignedObject</a>或<a href="http://download.oracle.com/javase/6/docs/api/javax/crypto/SealedObject.html">SealedObject</a>来封装当前对象，用SignedObject或SealedObject进行序列化。</li>
<li>在从流中进行反序列化的时候，可以通过ObjectInputStream的<a href="http://download.oracle.com/javase/6/docs/api/java/io/ObjectInputStream.html#registerValidation%28java.io.ObjectInputValidation,%20int%29">registerValidation</a>方法添加<a href="http://download.oracle.com/javase/6/docs/api/java/io/ObjectInputValidation.html">ObjectInputValidation</a>接口的实现，用来验证反序列化之后得到的对象是否合法。</li>
</ul>
<p><strong>RMI</strong><br />
RMI（Remote Method Invocation）是Java中的<a href="http://baike.baidu.com/view/32726.htm">远程过程调用</a>（Remote Procedure Call，RPC）实现，是一种分布式Java应用的实现方式。它的目的在于对开发人员屏蔽横跨不同JVM和网络连接等细节，使得分布在不同JVM上的对象像是存在于一个统一的JVM中一样，可以很方便的互相通讯。之所以在介绍对象序列化之后来介绍RMI，主要是因为对象序列化机制使得RMI非常简单。调用一个远程服务器上的方法并不是一件困难的事情。开发人员可以基于<a href="http://mina.apache.org/">Apache MINA</a>或是<a href="http://www.jboss.org/netty">Netty</a>这样的框架来写自己的网络服务器，亦或是可以采用<a href="http://baike.baidu.com/view/1077487.htm">REST架构风格</a>来编写HTTP服务。但这些解决方案中，不可回避的一个部分就是数据的编排和解排（marshal/unmarshal）。需要在Java对象和传输格式之间进行互相转换，而且这一部分逻辑是开发人员无法回避的。RMI的优势在于依靠Java序列化机制，对开发人员屏蔽了数据编排和解排的细节，要做的事情非常少。JDK 5之后，RMI通过动态代理机制去掉了早期版本中需要通过工具进行代码生成的繁琐方式，使用起来更加简单。</p>
<p>RMI采用的是典型的客户端-服务器端架构。首先需要定义的是服务器端的远程接口，这一步是设计好服务器端需要提供什么样的服务。对远程接口的要求很简单，只需要继承自RMI中的<a href="http://download.oracle.com/javase/6/docs/api/java/rmi/Remote.html">Remote</a>接口即可。Remote和Serializable一样，也是标记接口。远程接口中的方法需要抛出<a href="http://download.oracle.com/javase/6/docs/api/java/rmi/RemoteException.html">RemoteException</a>。定义好远程接口之后，实现该接口即可。如下面的Calculator是一个简单的远程接口。</p>
<pre>public interface Calculator extends Remote {
    String calculate(String expr) throws RemoteException;
}</pre>
<p>实现了远程接口的类的实例称为远程对象。创建出远程对象之后，需要把它注册到一个注册表之中。这是为了客户端能够找到该远程对象并调用。</p>
<pre>public class CalculatorServer implements Calculator {
    public String calculate(String expr) throws RemoteException {
        return expr;
    }
    public void start() throws RemoteException, AlreadyBoundException {
        Calculator stub = (Calculator) UnicastRemoteObject.exportObject(this, 0);
        Registry registry = LocateRegistry.getRegistry();
        registry.rebind("Calculator", stub);
    }
}</pre>
<p>CalculatorServer是远程对象的Java类。在它的start方法中通过<a href="http://download.oracle.com/javase/6/docs/api/java/rmi/server/UnicastRemoteObject.html">UnicastRemoteObject</a>的<a href="http://download.oracle.com/javase/6/docs/api/java/rmi/server/UnicastRemoteObject.html#exportObject%28java.rmi.Remote,%20int%29">exportObject</a>把当前对象暴露出来，使得它可以接收来自客户端的调用请求。再通过<a href="http://download.oracle.com/javase/6/docs/api/java/rmi/registry/Registry.html">Registry</a>的<a href="http://download.oracle.com/javase/6/docs/api/java/rmi/registry/Registry.html#rebind%28java.lang.String,%20java.rmi.Remote%29">rebind</a>方法进行注册，使得客户端可以查找到。</p>
<p>客户端的实现就是首先从注册表中查找到远程接口的实现对象，再调用相应的方法即可。实际的调用虽然是在服务器端完成的，但是在客户端看来，这个接口中的方法就好像是在当前JVM中一样。这就是RMI的强大之处。</p>
<pre>public class CalculatorClient {
    public void calculate(String expr) {
        try {
            Registry registry = LocateRegistry.getRegistry("localhost");
            Calculator calculator = (Calculator) registry.lookup("Calculator");
            String result = calculator.calculate(expr);
            System.out.println(result);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}</pre>
<p>在运行的时候，需要首先通过rmiregistry命令来启动RMI中用到的注册表服务器。</p>
<p>为了通过Java的序列化机制来进行传输，远程接口中的方法的参数和返回值，要么是Java的基本类型，要么是远程对象，要么是实现了 Serializable接口的Java类。当客户端通过RMI注册表找到一个远程接口的时候，所得到的其实是远程接口的一个动态代理对象。当客户端调用其中的方法的时候，方法的参数对象会在序列化之后，传输到服务器端。服务器端接收到之后，进行反序列化得到参数对象。并使用这些参数对象，在服务器端调用实际的方法。调用的返回值Java对象经过序列化之后，再发送回客户端。客户端再经过反序列化之后得到Java对象，返回给调用者。这中间的序列化过程对于使用者来说是透明的，由动态代理对象自动完成。除了序列化之外，RMI还使用了<a href="http://download.oracle.com/javase/6/docs/technotes/guides/rmi/codebase.html">动态类加载</a>技术。当需要进行反序列化的时候，如果该对象的类定义在当前JVM中没有找到，RMI会尝试从远端下载所需的类文件定义。可以在RMI程序启动的时候，通过JVM参数java.rmi.server.codebase来指定动态下载Java类文件的URL。</p>
<p><strong>参考资料</strong></p>
<ul>
<li><a href="http://download.oracle.com/javase/6/docs/platform/serialization/spec/serialTOC.html">Java对象序列化规范</a></li>
<li><a href="http://download.oracle.com/javase/6/docs/platform/rmi/spec/rmiTOC.html">RMI规范</a></li>
</ul>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2011/08/java%e6%b7%b1%e5%ba%a6%e8%a7%a3%e6%9e%90%e4%b9%8b%e5%ae%89%e5%85%a8%e4%b8%8e%e5%af%b9%e8%b1%a1%e5%ba%8f%e5%88%97%e5%8c%96rmi/">Java深度解析之安全与对象序列化RMI</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://blog.zhourunsheng.com/2011/08/java%e6%b7%b1%e5%ba%a6%e8%a7%a3%e6%9e%90%e4%b9%8b%e5%ae%89%e5%85%a8%e4%b8%8e%e5%af%b9%e8%b1%a1%e5%ba%8f%e5%88%97%e5%8c%96rmi/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Java深度解析之反射机制动态代理与I/O</title>
		<link>http://blog.zhourunsheng.com/2011/08/java%e6%b7%b1%e5%ba%a6%e8%a7%a3%e6%9e%90%e4%b9%8b%e5%8f%8d%e5%b0%84%e6%9c%ba%e5%88%b6%e5%8a%a8%e6%80%81%e4%bb%a3%e7%90%86%e4%b8%8eio/</link>
		<comments>http://blog.zhourunsheng.com/2011/08/java%e6%b7%b1%e5%ba%a6%e8%a7%a3%e6%9e%90%e4%b9%8b%e5%8f%8d%e5%b0%84%e6%9c%ba%e5%88%b6%e5%8a%a8%e6%80%81%e4%bb%a3%e7%90%86%e4%b8%8eio/#comments</comments>
		<pubDate>Tue, 02 Aug 2011 08:18:47 +0000</pubDate>
		<dc:creator><![CDATA[润物无声]]></dc:creator>
				<category><![CDATA[程序设计]]></category>
		<category><![CDATA[Java]]></category>

		<guid isPermaLink="false">http://blog.zhourunsheng.com/?p=627</guid>
		<description><![CDATA[<p>本系列的文章转载自infoq专栏合集《Java深度历险》，10篇文章分别由浅到深描述了Java的多个细节，具体 [&#8230;]</p>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2011/08/java%e6%b7%b1%e5%ba%a6%e8%a7%a3%e6%9e%90%e4%b9%8b%e5%8f%8d%e5%b0%84%e6%9c%ba%e5%88%b6%e5%8a%a8%e6%80%81%e4%bb%a3%e7%90%86%e4%b8%8eio/">Java深度解析之反射机制动态代理与I/O</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></description>
				<content:encoded><![CDATA[<p>本系列的文章转载自<a href="http://www.infoq.com/cn/minibooks/java-explore">infoq专栏合集《Java深度历险》</a>，10篇文章分别由浅到深描述了Java的多个细节，具体包括<br />
1. Java 字节代码的操纵    2. Java类的加载、链接和初始化  3. Java线程:基本概念、可见性与同步<br />
4. Java垃圾回收机制与引用类型  5. Java泛型   6. Java注解  7. Java反射与动态代理<br />
8. Java I/O   9. Java安全  10. Java对象序列化与RMI，<br />
相信通过十个主题的学习，读者能对Java的这几个方面有较深入的理解。enjoy it！</p>
<h3>Java反射与动态代理</h3>
<p>以前文章中介绍Java注解的时候，多次提到了Java的反射API。与<a href="http://download.oracle.com/javase/6/docs/api/javax/lang/model/package-summary.html">javax.lang.model</a>不同的是，通过反射API可以获取程序在运行时刻的内部结构。反射API中提供的动态代理也是非常强大的功能，可以原生实现<a href="http://en.wikipedia.org/wiki/Aspect-oriented_programming">AOP</a>中 的方法拦截功能。正如英文单词reflection的含义一样，使用反射API的时候就好像在看一个Java类在水中的倒影一样。知道了Java类的内部 结构之后，就可以与它进行交互，包括创建新的对象和调用对象中的方法等。这种交互方式与直接在源代码中使用的效果是相同的，但是又额外提供了运行时刻的灵活性。使用反射的一个最大的弊端是<a href="http://stackoverflow.com/questions/435553/java-reflection-performance">性能比较差</a>。相同的操作，用反射API所需的时间大概比直接的使用要慢一两个数量级。不过现在的JVM实现中，反射操作的性能已经有了<a href="http://java.sun.com/j2se/1.4.2/performance.guide.html">很大的提升</a>。在灵活性与性能之间，总是需要进行权衡的。应用可以在适当的时机来使用反射API。<br />
<span id="more-627"></span></p>
<p><strong>基本用法</strong><br />
Java 反射API的第一个主要作用是获取程序在运行时刻的内部结构。这对于程序的检查工具和调试器来说，是非常实用的功能。只需要短短的十几行代码，就可以遍历出来一个Java类的内部结构，包括其中的构造方法、声明的域和定义的方法等。这不得不说是一个很强大的能力。只要有了java.lang.Class类 的对象，就可以通过其中的方法来获取到该类中的构造方法、域和方法。对应的方法分别是<a href="http://download.oracle.com/javase/6/docs/api/java/lang/Class.html#getConstructor%28java.lang.Class...%29">getConstructor</a>、<a href="http://download.oracle.com/javase/6/docs/api/java/lang/Class.html#getField%28java.lang.String%29">getField</a>和<a href="http://download.oracle.com/javase/6/docs/api/java/lang/Class.html#getMethod%28java.lang.String,%20java.lang.Class...%29">getMethod</a>。这三个方法还有相应的getDeclaredXXX版本，区别在于getDeclaredXXX版本的方法只会获取该类自身所声明的元素，而不会考虑继承下来的。<a href="http://download.oracle.com/javase/6/docs/api/java/lang/reflect/Constructor.html">Constructor</a>、<a href="http://download.oracle.com/javase/6/docs/api/java/lang/reflect/Field.html">Field</a>和<a href="http://download.oracle.com/javase/6/docs/api/java/lang/reflect/Method.html">Method</a>这三个类分别表示类中的构造方法、域和方法。这些类中的方法可以获取到所对应结构的元数据。</p>
<p>反射API的另外一个作用是在运行时刻对一个Java对象进行操作。 这些操作包括动态创建一个Java类的对象，获取某个域的值以及调用某个方法。在Java源代码中编写的对类和对象的操作，都可以在运行时刻通过反射API来实现。考虑下面一个简单的Java类。</p>
<pre>class MyClass {
    public int count;
    public MyClass(int start) {
        count = start;
    }
    public void increase(int step) {
        count = count + step;
    }
}</pre>
<p>使用一般做法和反射API都非常简单。</p>
<pre>MyClass myClass = new MyClass(0); //一般做法
myClass.increase(2);
System.out.println("Normal -&gt; " + myClass.count);
try {
    Constructor constructor = MyClass.class.getConstructor(int.class); //获取构造方法
    MyClass myClassReflect = constructor.newInstance(10); //创建对象
    Method method = MyClass.class.getMethod("increase", int.class);  //获取方法
    method.invoke(myClassReflect, 5); //调用方法
    Field field = MyClass.class.getField("count"); //获取域
    System.out.println("Reflect -&gt; " + field.getInt(myClassReflect)); //获取域的值
} catch (Exception e) {
    e.printStackTrace();
}</pre>
<p>由于数组的特殊性，<a href="http://download.oracle.com/javase/6/docs/api/java/lang/reflect/Array.html">Array</a>类提供了一系列的静态方法用来创建数组和对数组中的元素进行访问和操作。</p>
<pre>Object array = Array.newInstance(String.class, 10); //等价于 new String[10]
Array.set(array, 0, "Hello");  //等价于array[0] = "Hello"
Array.set(array, 1, "World");  //等价于array[1] = "World"
System.out.println(Array.get(array, 0));  //等价于array[0]</pre>
<p>使用Java反射API的时候可以绕过Java默认的访问控制检查，比如可以直接获取到对象的私有域的值或是调用私有方法。只需要在获取到Constructor、Field和Method类的对象之后，调用<a href="http://download.oracle.com/javase/6/docs/api/java/lang/reflect/AccessibleObject.html#setAccessible%28boolean%29">setAccessible</a>方法并设为true即可。有了这种机制，就可以很方便的在运行时刻获取到程序的内部状态。</p>
<p><strong>处理泛型</strong><br />
Java 5中引入了泛型的概念之后，Java反射API也做了相应的修改，以提供对泛型的支持。由于类型擦除机制的存在，泛型类中的类型参数等信息，在运行时刻是不存在的。JVM看到的都是原始类型。对此，Java 5对Java类文件的格式做了<a href="http://java.sun.com/docs/books/jvms/second_edition/ClassFileFormat-Java5.pdf">修订</a>，添加了Signature属性，用来包含不在JVM类型系统中的类型信息。比如以java.util.List接口为例，在其类文件中的Signature属性的声明是&lt;E:Ljava/lang/Object;&gt;Ljava/lang/Object;Ljava/util/Collection&lt;TE;&gt;;; ，这就说明List接口有一个类型参数E。在运行时刻，JVM会读取Signature属性的内容并提供给反射API来使用。</p>
<p>比如在代码中声明了一个域是List&lt;String&gt;类型的，虽然在运行时刻其类型会变成原始类型List，但是仍然可以通过反射来获取到所用的实际的类型参数。</p>
<pre>Field field = Pair.class.getDeclaredField("myList"); //myList的类型是List
Type type = field.getGenericType();
if (type instanceof ParameterizedType) {
    ParameterizedType paramType = (ParameterizedType) type;
    Type[] actualTypes = paramType.getActualTypeArguments();
    for (Type aType : actualTypes) {
        if (aType instanceof Class) {
            Class clz = (Class) aType;
            System.out.println(clz.getName()); //输出java.lang.String
        }
    }
}</pre>
<p><strong>动态代理</strong><br />
熟悉设计模式的人对于<a href="http://sourcemaking.com/design_patterns/proxy">代理模式</a>可 能都不陌生。 代理对象和被代理对象一般实现相同的接口，调用者与代理对象进行交互。代理的存在对于调用者来说是透明的，调用者看到的只是接口。代理对象则可以封装一些内部的处理逻辑，如访问控制、远程通信、日志、缓存等。比如一个对象访问代理就可以在普通的访问机制之上添加缓存的支持。这种模式在<a href="http://www.oracle.com/technetwork/java/javase/tech/index-jsp-136424.html">RMI</a>和<a href="http://www.oracle.com/technetwork/java/javaee/ejb/index.html">EJB</a>中都得到了广泛的使用。传统的代理模式的实现，需要在源代码中添加一些附加的类。这些类一般是手写或是通过工具来自动生成。JDK 5引入的动态代理机制，允许开发人员在运行时刻动态的创建出代理类及其对象。在运行时刻，可以动态创建出一个实现了多个接口的代理类。每个代理类的对象都会关联一个表示内部处理逻辑的<a href="http://download.oracle.com/javase/6/docs/api/java/lang/reflect/InvocationHandler.html">InvocationHandler</a>接 口的实现。当使用者调用了代理对象所代理的接口中的方法的时候，这个调用的信息会被传递给InvocationHandler的invoke方法。在 invoke方法的参数中可以获取到代理对象、方法对应的Method对象和调用的实际参数。invoke方法的返回值被返回给使用者。这种做法实际上相 当于对方法调用进行了拦截。熟悉AOP的人对这种使用模式应该不陌生。但是这种方式不需要依赖<a href="http://www.eclipse.org/aspectj/">AspectJ</a>等AOP框架。</p>
<p>下面的代码用来代理一个实现了List接口的对象。所实现的功能也非常简单，那就是禁止使用List接口中的add方法。如果在getList中传入一个实现List接口的对象，那么返回的实际就是一个代理对象，尝试在该对象上调用add方法就会抛出来异常。</p>
<pre>public List getList(final List list) {
    return (List) Proxy.newProxyInstance(DummyProxy.class.getClassLoader(), new Class[] { List.class },
        new InvocationHandler() {
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                if ("add".equals(method.getName())) {
                    throw new UnsupportedOperationException();
                }
                else {
                    return method.invoke(list, args);
                }
            }
        });
 }</pre>
<p>这里的实际流程是，当代理对象的add方法被调用的时候，InvocationHandler中的invoke方法会被调用。参数method就包含了调用的基本信息。因为方法名称是add，所以会抛出相关的异常。如果调用的是其它方法的话，则执行原来的逻辑。</p>
<p><strong>使用案例</strong><br />
Java 反射API的存在，为Java语言添加了一定程度上的动态性，可以实现某些动态语言中的功能。比如在JavaScript的代码中，可以通过 obj["set" + propName]()来根据变量propName的值找到对应的方法进行调用。虽然在Java源代码中不能这么写，但是通过反射API同样可以实现类似 的功能。这对于处理某些遗留代码来说是有帮助的。比如所需要使用的类有多个版本，每个版本所提供的方法名称和参数不尽相同。而调用代码又必须与这些不同的版本都能协同工作，就可以通过反射API来依次检查实际的类中是否包含某个方法来选择性的调用。</p>
<p>Java 反射API实际上定义了一种相对于编译时刻而言更加松散的契约。如果被调用的Java对象中并不包含某个方法，而在调用者代码中进行引用的话，在编译时刻就会出现错误。而反射API则可以把这样的检查推迟到运行时刻来完成。通过把Java中的字节代码增强、类加载器和反射API结合起来，可以处理一些对灵 活性要求很高的场景。</p>
<p>在 有些情况下，可能会需要从远端加载一个Java类来执行。比如一个客户端Java程序可以通过网络从服务器端下载Java类来执行，从而可以实现自动更新 的机制。当代码逻辑需要更新的时候，只需要部署一个新的Java类到服务器端即可。一般的做法是通过自定义类加载器下载了类字节代码之后，定义出 Class类的对象，再通过newInstance方法就可以创建出实例了。不过这种做法要求客户端和服务器端都具有某个接口的定义，从服务器端下载的是 这个接口的实现。这样的话才能在客户端进行所需的类型转换，并通过接口来使用这个对象实例。如果希望客户端和服务器端采用更加松散的契约的话，使用反射API就可以了。两者之间的契约只需要在方法的名称和参数这个级别就足够了。服务器端Java类并不需要实现特定的接口，可以是一般的Java类。</p>
<p>动态代理的使用场景就更加广泛了。需要使用AOP中的方法拦截功能的地方都可以用到动态代理。Spring框架的<a href="http://static.springsource.org/spring/docs/2.5.x/reference/aop.html">AOP实现</a>默认也使用动态代理。不过JDK中的动态代理只支持对接口的代理，不能对一个普通的Java类提供代理。不过这种实现在大部分的时候已经够用了。</p>
<p><strong>参考资料</strong></p>
<ul>
<li>Classworking toolkit: Reflecting  generics</li>
<li><a href="http://www.ibm.com/developerworks/java/library/j-jtp08305.html">D?ecorating with dynamic proxies</a></li>
</ul>
<h3>Java I/O</h3>
<p>在应用程序中，通常会涉及到两种类型的计算：CPU计算和I/O计算。对于大多数应用来说，花费在等待I/O上的时间是占较大比重的。通常需要等待速度较慢的磁盘或是网络连接完成I/O请求，才能继续后面的CPU计算任务。因此提高I/O操作的效率对应用的性能有较大的帮助。本文将介绍Java语言中与I/O操作相关的内容，包括基本的Java I/O和Java NIO，着重于基本概念和最佳实践。</p>
<p><strong>流</strong><br />
Java语言提供了多个层次不同的概念来对I/O操作进行抽象。Java I/O中最早的概念是流，包括输入流和输出流，早在JDK 1.0中就存在了。简单的来说，流是一个连续的字节的序列。输入流是用来读取这个序列，而输出流则构建这个序列。<a href="http://download.oracle.com/javase/6/docs/api/java/io/InputStream.html">InputStream</a>和<a href="http://download.oracle.com/javase/6/docs/api/java/io/OutputStream.html">OutputStream</a>所操纵的基本单元就是字节。每次读取和写入单个字节或是字节数组。如果从字节的层次来处理数据类型的话，操作会非常繁琐。可以用更易使用的流实现来包装基本的字节流。如果想读取或输出Java的基本数据类型，可以使用<a href="http://download.oracle.com/javase/6/docs/api/java/io/DataInputStream.html">DataInputStream</a>和<a href="http://download.oracle.com/javase/6/docs/api/java/io/DataOutputStream.html">DataOutputStream</a>。它们所提供的类似readFloat和writeDouble这样的方法，会让处理基本数据类型变得很简单。如果希望读取或写入的是Java中的对象的话，可以使用<a href="http://download.oracle.com/javase/6/docs/api/java/io/ObjectInputStream.html">ObjectInputStream</a>和<a href="http://download.oracle.com/javase/6/docs/api/java/io/ObjectOutputStream.html">ObjectOutputStream</a>。它们与对象的<a href="http://download.oracle.com/javase/6/docs/technotes/guides/serialization/index.html">序列化</a>机制一起，可以实现Java对象状态的持久化和数据传递。基本流所提供的对于输入和输出的控制比较弱。InputStream只提供了顺序读取、跳过部分字节和标记/重置的支持，而OutputStream则只能顺序输出。</p>
<p><strong>流的使用</strong><br />
由于I/O操作所对应的实体在系统中都是有限的资源，需要妥善的进行管理。每个打开的流都需要被正确的关闭以释放资源。所遵循的原则是谁打开谁释放。如果一个流只在某个方法体内使用，则通过finally语句或是JDK 7中的try-with-resources语句来确保在方法返回之前，流被正确的关闭。如果一个方法只是作为流的使用者，就不需要考虑流的关闭问题。典型的情况是在servlet实现中并不需要关闭HttpServletResponse中的输出流。如果你的代码需要负责打开一个流，并且需要在不同的对象之间进行传递的话，可以考虑使用<a href="http://c2.com/cgi/wiki?ExecuteAroundMethod">Execute Around Method</a>模式。如下面的代码所示：</p>
<pre>public void use(StreamUser user) {
    InputStream input = null;
    try {
        input = open();
        user.use(input);
    } catch(IOException e) {
        user.onError(e);
    } finally {
        if (input != null) {
            try {
                input.close();
            } catch (IOException e) {
                user.onError(e);
            }
        }
    }
 }</pre>
<p>如上述代码中所看到的一样，由专门的类负责流的打开和关闭。流的使用者StreamUser并不需要关心资源释放的细节，只需要对流进行操作即可。</p>
<p>在使用输入流的过程中，经常会遇到需要复用一个输入流的情况，即多次读取一个输入流中的内容。比如通过URL.openConnection方法打开了一个远端站点连接的输入流，希望对其中的内容进行多次处理。这就需要把一个InputStream对象在多个对象中传递。为了保证每个使用流的对象都能获取到正确的内容，需要对流进行一定的处理。通常有两种解决的办法，一种是利用InputStream的标记支持。如果一个流支持标记的话（通过<a href="http://download.oracle.com/javase/6/docs/api/java/io/InputStream.html#markSupported%28%29">markSupported</a>方法判断），就可以在流开始的地方通过<a href="http://download.oracle.com/javase/6/docs/api/java/io/InputStream.html#mark%28int%29">mark</a>方法添加一个标记，当完成一次对流的使用之后，通过<a href="http://download.oracle.com/javase/6/docs/api/java/io/InputStream.html#reset%28%29">reset</a>方法就可以把流的读取位置重置到上次标记的位置，即流开始的地方。如此反复，就可以复用这个输入流。大部分输入流的实现是不支持标记的。可以通过<a href="http://download.oracle.com/javase/6/docs/api/java/io/BufferedInputStream.html">BufferedInputStream</a>进行包装来支持标记。</p>
<pre>private InputStream prepareStream(InputStream ins) {
    BufferedInputStream buffered = new BufferedInputStream(ins);
    buffered.mark(Integer.MAX_VALUE);
    return buffered;
}
private void resetStream(InputStream ins) throws IOException {
    ins.reset();
    ins.mark(Integer.MAX_VALUE);
}</pre>
<p>如上面的代码所示，通过prepareStream方法可以用一个BufferedInputStream来包装基本的InputStream。通过 mark方法在流开始的时候添加一个标记，允许读入Integer.MAX_VALUE个字节。每次流使用完成之后，通过resetStream方法重置即可。</p>
<p>另外一种做法是把输入流的内容转换成字节数组，进而转换成输入流的另外一个实现<a href="http://download.oracle.com/javase/6/docs/api/java/io/ByteArrayInputStream.html">ByteArrayInputStream</a>。这样做的好处是使用字节数组作为参数传递的格式要比输入流简单很多，可以不需要考虑资源相关的问题。另外也可以尽早的关闭原始的输入流，而无需等待所有使用流的操作完成。这两种做法的思路其实是相似的。BufferedInputStream在内部也创建了一个字节数组来保存从原始输入流中读入的内容。</p>
<pre>private byte[] saveStream(InputStream input) throws IOException {
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    ReadableByteChannel readChannel = Channels.newChannel(input);
    ByteArrayOutputStream output = new ByteArrayOutputStream(32 * 1024);
    WritableByteChannel writeChannel = Channels.newChannel(output);
    while ((readChannel.read(buffer)) &gt; 0 || buffer.position() != 0) {
        buffer.flip();
        writeChannel.write(buffer);
        buffer.compact();
    }
    return output.toByteArray();
}</pre>
<p>上面的代码中saveStream方法把一个InputStream保存为字节数组。</p>
<p><strong>缓冲区</strong><br />
由于流背后的数据有可能比较大，在实际的操作中，通常会使用缓冲区来提高性能。传统的缓冲区的实现是使用数组来完成。比如经典的从InputStream到OutputStream的复制的<a href="http://ventrix.nsdc.gr/code_folds/?p=87">实现</a>，就是使用一个字节数组作为中间的缓冲区。NIO中引入的<a href="http://download.oracle.com/javase/6/docs/api/java/nio/Buffer.html">Buffer</a>类及其子类，可以很方便的用来创建各种基本数据类型的缓冲区。相对于数组而言，Buffer类及其子类提供了更加丰富的方法来对其中的数据进行操作。后面会提到的通道也使用Buffer类进行数据传递。</p>
<p>在Buffer上进行的元素添加和删除操作，都围绕3个属性<a href="http://download.oracle.com/javase/6/docs/api/java/nio/Buffer.html#position%28%29">position</a>、<a href="http://download.oracle.com/javase/6/docs/api/java/nio/Buffer.html#limit%28%29">limit</a>和<a href="http://download.oracle.com/javase/6/docs/api/java/nio/Buffer.html#capacity%28%29">capacity</a>展开，分别表示Buffer当前的读写位置、可用的读写范围和容量限制。容量限制是在创建的时候指定的。Buffer提供的get/put方法都有相对和绝对两种形式。相对读写时的位置是相对于position的值，而绝对读写则需要指定起始的序号。在使用Buffer的常见错误就是在读写操作时没有考虑到这3个元素的值，因为大多数时候都是使用的是相对读写操作，而position的值可能早就发生了变化。一些应该注意的地方包括：将数据读入缓冲区之前，需要调用<a href="http://download.oracle.com/javase/6/docs/api/java/nio/Buffer.html#clear%28%29">clear</a>方法；将缓冲区中的数据输出之前，需要调用<a href="http://download.oracle.com/javase/6/docs/api/java/nio/Buffer.html#flip%28%29">flip</a>方法。</p>
<pre>ByteBuffer buffer = ByteBuffer.allocate(32);
CharBuffer charBuffer = buffer.asCharBuffer();
String content = charBuffer.put("Hello ").put("World").flip().toString();
System.out.println(content);</pre>
<p>上面的代码展示了Buffer子类的使用。首先可以在已有的ByteBuffer上面创建出其它数据类型的缓冲区视图，其次Buffer子类的很多方法是可以级联的，最后是要注意flip方法的使用。</p>
<p><strong>字符与编码</strong><br />
在程序中，总是免不了与字符打交道，毕竟字符是用户直接可见的信息。而与字符处理直接相关的就是编码。相信不少人都曾经为了程序中的乱码问题而困扰。要弄清楚这个问题，就需要理解字符集和编码的概念。字符集，顾名思义，就是字符的集合。一个字符集中所包含的字符通常与地区和语言有关。字符集中的每个字符通常会有一个整数编码与其对应。常见的字符集有ASCII、ISO-8859-1和Unicode等。对于字符集中的每个字符，为了在计算机中表示，都需要转换某种字节的序列，即该字符的编码。同一个字符集可以有不同的编码方式。如果某种编码格式产生的字节序列，用另外一种编码格式来解码的话，就可能会得到错误的字符，从而产生乱码的情况。所以将一个字节序列转换成字符串的时候，需要知道正确的编码格式。</p>
<p>NIO中的<a href="http://download.oracle.com/javase/6/docs/api/java/nio/charset/package-frame.html">java.nio.charset</a>包提供了与字符集相关的类，可以用来进行编码和解码。其中的<a href="http://download.oracle.com/javase/6/docs/api/java/nio/charset/CharsetEncoder.html">CharsetEncoder</a>和<a href="http://download.oracle.com/javase/6/docs/api/java/nio/charset/CharsetDecoder.html">CharsetDecoder</a>允许对编码和解码过程进行精细的控制，如处理非法的输入以及字符集中无法识别的字符等。通过这两个类可以实现字符内容的过滤。比如应用程序在设计的时候就只支持某种字符集，如果用户输入了其它字符集中的内容，在界面显示的时候就是乱码。对于这种情况，可以在解码的时候忽略掉无法识别的内容。</p>
<pre>String input = "你123好";
Charset charset = Charset.forName("ISO-8859-1");
CharsetEncoder encoder = charset.newEncoder();
encoder.onUnmappableCharacter(CodingErrorAction.IGNORE);
CharsetDecoder decoder = charset.newDecoder();
CharBuffer buffer = CharBuffer.allocate(32);
buffer.put(input);
buffer.flip();
try {
    ByteBuffer byteBuffer = encoder.encode(buffer);
    CharBuffer cbuf = decoder.decode(byteBuffer);
    System.out.println(cbuf);  //输出123
} catch (CharacterCodingException e) {
    e.printStackTrace();
}</pre>
<p>上面的代码中，通过使用ISO-8859-1字符集的编码和解码器，就可以过滤掉字符串中不在此字符集中的字符。</p>
<p>Java I/O在处理字节流字之外，还提供了处理字符流的类，即<a href="http://download.oracle.com/javase/6/docs/api/java/io/Reader.html">Reader</a>/<a href="http://download.oracle.com/javase/6/docs/api/java/io/Writer.html">Writer</a>类及其子类，它们所操纵的基本单位是char类型。在字节和字符之间的桥梁就是编码格式。通过编码器来完成这两者之间的转换。在创建Reader/Writer子类实例的时候，总是应该使用两个参数的构造方法，即显式指定使用的字符集或编码解码器。如果不显式指定，使用的是JVM的默认字符集，有可能在其它平台上产生错误。</p>
<p><strong>通道</strong><br />
通道作为NIO中的核心概念，在设计上比之前的流要好不少。通道相关的很多实现都是接口而不是抽象类。通道本身的抽象层次也更加合理。通道表示的是对支持I/O操作的实体的一个连接。一旦通道被打开之后，就可以执行读取和写入操作，而不需要像流那样由输入流或输出流来分别进行处理。与流相比，通道的操作使用的是Buffer而不是数组，使用更加方便灵活。通道的引入提升了I/O操作的灵活性和性能，主要体现在文件操作和网络操作上。</p>
<p><strong>文件通道</strong><br />
对文件操作方面，文件通道<a href="http://download.oracle.com/javase/6/docs/api/java/nio/channels/FileChannel.html">FileChannel</a>提供了与其它通道之间高效传输数据的能力，比传统的基于流和字节数组作为缓冲区的做法，要来得简单和快速。比如下面的把一个网页的内容保存到本地文件的实现。</p>
<pre>FileOutputStream output = new FileOutputStream("baidu.txt");
FileChannel channel = output.getChannel();
URL url = new URL("http://www.baidu.com");
InputStream input = url.openStream();
ReadableByteChannel readChannel = Channels.newChannel(input);
channel.transferFrom(readChannel, 0, Integer.MAX_VALUE);</pre>
<p>文件通道的另外一个功能是对文件的部分片段进行加锁。当在一个文件上的某个片段加上了排它锁之后，其它进程必须等待这个锁释放之后，才能访问该文件的这个片段。文件通道上的锁是由JVM所持有的，因此适合于与其它应用程序协同时使用。比如当多个应用程序共享某个配置文件的时候，如果Java程序需要更新此文件，则可以首先获取该文件上的一个排它锁，接着进行更新操作，再释放锁即可。这样可以保证文件更新过程中不会受到其它程序的影响。</p>
<p>另外一个在性能方面有很大提升的功能是<a href="http://baike.baidu.com/view/394293.htm">内存映射文件</a>的支持。通过FileChannel的<a href="http://download.oracle.com/javase/6/docs/api/java/nio/channels/FileChannel.html#map%28java.nio.channels.FileChannel.MapMode,%20long,%20long%29">map</a>方法可以创建出一个<a href="http://download.oracle.com/javase/6/docs/api/java/nio/MappedByteBuffer.html">MappedByteBuffer</a>对象，对这个缓冲区的操作都会直接反映到文件内容上。这点尤其适合对大文件进行读写操作。</p>
<p><strong>套接字通道</strong><br />
在套接字通道方面的改进是提供了对非阻塞I/O和多路复用I/O的支持。传统的流的I/O操作是阻塞式的。在进行I/O操作的时候，线程会处于阻塞状态等待操作完成。NIO中引入了非阻塞I/O的支持，不过只限于套接字I/O操作。所有继承自<a href="http://download.oracle.com/javase/6/docs/api/java/nio/channels/SelectableChannel.html">SelectableChannel</a>的通道类都可以通过<a href="http://download.oracle.com/javase/6/docs/api/java/nio/channels/SelectableChannel.html#configureBlocking%28boolean%29">configureBlocking</a>方法来设置是否采用非阻塞模式。在非阻塞模式下，程序可以在适当的时候查询是否有数据可供读取。一般是通过定期的轮询来实现的。</p>
<p>多路复用I/O是一种新的I/O编程模型。传统的套接字服务器的处理方式是对于每一个客户端套接字连接，都新创建一个线程来进行处理。创建线程是很耗时的操作，而有的实现会采用线程池。不过一个请求一个线程的处理模型并不是很理想。原因在于耗费时间创建的线程，在大部分时间可能处于等待的状态。而多路复用I/O的基本做法是由一个线程来管理多个套接字连接。该线程会负责根据连接的状态，来进行相应的处理。多路复用I/O依靠操作系统提供的<a href="http://en.wikipedia.org/wiki/Asynchronous_I/O#Select.28.2Fpoll.29_loops">select</a>或相似系统调用的支持，选择那些已经就绪的套接字连接来处理。可以把多个非阻塞I/O通道注册在某个<a href="http://download.oracle.com/javase/6/docs/api/java/nio/channels/Selector.html">Selector</a>上，并声明所感兴趣的操作类型。每次调用Selector的<a href="http://download.oracle.com/javase/6/docs/api/java/nio/channels/Selector.html#select%28%29">select</a>方法，就可以选择到某些感兴趣的操作已经就绪的通道的集合，从而可以进行相应的处理。如果要执行的处理比较复杂，可以把处理转发给其它的线程来执行。</p>
<p>下面是一个简单的使用多路复用I/O的服务器实现。当有客户端连接上的时候，服务器会返回一个Hello World作为响应。</p>
<pre>private static class IOWorker implements Runnable {
    public void run() {
        try {
            Selector selector = Selector.open();
            ServerSocketChannel channel = ServerSocketChannel.open();
            channel.configureBlocking(false);
            ServerSocket socket = channel.socket();
            socket.bind(new InetSocketAddress("localhost", 10800));
            channel.register(selector, channel.validOps());
            while (true) {
                selector.select();
                Iterator iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    iterator.remove();
                    if (!key.isValid()) {
                        continue;
                    }
                    if (key.isAcceptable()) {
                        ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
                        SocketChannel sc = ssc.accept();
                        sc.configureBlocking(false);
                        sc.register(selector, sc.validOps());
                    }
                    if (key.isWritable()) {
                        SocketChannel client = (SocketChannel) key.channel();
                        Charset charset = Charset.forName("UTF-8");
                        CharsetEncoder encoder = charset.newEncoder();
                        CharBuffer charBuffer = CharBuffer.allocate(32);
                        charBuffer.put("Hello World");
                        charBuffer.flip();
                        ByteBuffer content = encoder.encode(charBuffer);
                        client.write(content);
                        key.cancel();
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}</pre>
<p>上面的代码给出的只是非常简单的示例程序，只是展示了多路复用I/O的基本使用方式。在开发复杂网络应用程序的时候，使用一些Java NIO网络应用框架会让你事半功倍。目前来说最流行的两个框架是<a href="http://mina.apache.org/">Apache MINA</a>和<a href="http://www.jboss.org/netty">Netty</a>。在使用了Netty之后，Twitter的<a href="http://engineering.twitter.com/2011/04/twitter-search-is-now-3x-faster_1656.html">搜索功能速度提升达到了3倍之多</a>。网络应用开发人员都可以使用这两个开源的优秀框架。</p>
<p><strong>参考资料</strong></p>
<ul>
<li><a href="http://download.oracle.com/javase/6/docs/technotes/guides/io/index.html">Java 6 I/O-related APIs &amp; Developer Guides</a></li>
<li><a href="http://onjava.com/pub/a/onjava/2002/10/02/javanio.html">Top Ten New Things You Can Do with NIO</a></li>
<li>Building Highly Scalable Servers with Java NIO</li>
</ul>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2011/08/java%e6%b7%b1%e5%ba%a6%e8%a7%a3%e6%9e%90%e4%b9%8b%e5%8f%8d%e5%b0%84%e6%9c%ba%e5%88%b6%e5%8a%a8%e6%80%81%e4%bb%a3%e7%90%86%e4%b8%8eio/">Java深度解析之反射机制动态代理与I/O</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://blog.zhourunsheng.com/2011/08/java%e6%b7%b1%e5%ba%a6%e8%a7%a3%e6%9e%90%e4%b9%8b%e5%8f%8d%e5%b0%84%e6%9c%ba%e5%88%b6%e5%8a%a8%e6%80%81%e4%bb%a3%e7%90%86%e4%b8%8eio/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
