顯示具有 轉載 標籤的文章。 顯示所有文章
顯示具有 轉載 標籤的文章。 顯示所有文章
2022-08-01 11:37

[轉載] NLog Variables

轉載自:當條件等于不作業時.net核心Nlog過濾器

我必須從我的啟動類傳遞變數值

LogManager.Configuration.Variables["environment"] = "Development";

我在我的 nlog.config 檔案中添加了以下過濾器

<rules>
    <logger name="*" minlevel="Error" writeTo="logfile">
        <filters>
            <when condition="equals('${var:environment}', 'Development')" action="Ignore" />
        </filters>
    </logger>
</rules>

即使我將值作為 Development 傳遞,該訊息仍會被記錄而不是忽略。

但是,當我對它的作業值進行硬編碼時

您在 NLog中發現了一個錯誤,但如果您這樣做,它應該可以作業(也會更快):

<rules>
    <logger name="*" minlevel="Error" writeTo="logfile">
        <filters defaultAction='log'>
            <when condition="'${var:environment}' == 'Development'" action="Ignore" />
        </filters>
    </logger>
</rules>

您也可以用 minLevel="${var:EnvironmentMinLevel:whenEmpty=Error}" 處理,這比 <filters> 快得多

<rules>
    <logger name="*" minlevel="${var:EnvironmentMinLevel:whenEmpty=Error}" writeTo="logfile" />
</rules>

設定 Variables 要記得呼叫 Reconfig,或者在 config 中設定 autoReload="true"

NLog.LogManager.Configuration.Variables["EnvironmentMinLevel"] = "Off";
NLog.LogManager.ReconfigExistingLoggers();

另請參閱 https://github.com/NLog/NLog/wiki/Filtering-log-messages#semi-dynamic-routing-rules

2022-07-15 12:52

[轉載] X-Y PROBLEM

轉載自:酷壳 X-Y PROBLEM

对于X-Y Problem的意思如下:

  1. 有人想解决问题X
  2. 他觉得Y可能是解决X问题的方法
  3. 但是他不知道Y应该怎么做
  4. 于是他去问别人Y应该怎么做?

简而言之,没有去问怎么解决问题X,而是去问解决方案Y应该怎么去实现和操作。于是乎:

  1. 热心的人们帮助并告诉这个人Y应该怎么搞,但是大家都觉得Y这个方案有点怪异。
  2. 在经过大量地讨论和浪费了大量的时间后,热心的人终于明白了原始的问题X是怎么一回事。
  3. 于是大家都发现,Y根本就不是用来解决X的合适的方案。

X-Y Problem最大的严重的问题就是:在一个根本错误的方向上浪费他人大量的时间和精力

示例

举个两个例子:

Q) 我怎么用Shell取得一个字符串的后3位字符?
A1) 如果这个字符的变量是$foo,你可以这样来 echo ${foo:-3}
A2) 为什么你要取后3位?你想干什么?
Q) 其实我就想取文件的扩展名
A1) 我靠,原来你要干这事,那我的方法不对,文件的扩展名并不保证一定有3位啊。
A1) 如果你的文件必然有扩展名的话,你可以这来样来:echo ${foo##*.}

再来一个示例:

Q)问一下大家,我如何得到一个文件的大小
A1) size = ls -l $file | awk '{print $5}'
Q) 哦,要是这个文件名是个目录呢?
A2) 用du吧
A3) 不好意思,你到底是要文件的大小还是目录的大小?你到底要干什么?
Q)  我想把一个目录下的每个文件的每个块(第一个块有512个字节)拿出来做md5,并且计算他们的大小 ……
A1) 哦,你可以使用dd吧。
A2) dd不行吧。
A3) 你用md5来计算这些块的目的是什么?你究竟想干什么啊?
Q) 其实,我想写一个网盘,对于小文件就直接传输了,对于大文件我想分块做增量同步。
A2) 用rsync啊,你妹!

这里有篇文章说明了X-Y Problem的各种案例说明,我从其中摘出三个来让大家看看:

你试图做X,并想到了用Y方案。所以你去问别人Y,但根本不提X。于是,你可以会错过本来可能有更好更适合的方案,除非你告诉大家X是什么。

— from Re: How do I keep the command line from eating the backslashes? by revdiablo

有些人问怎么做Y,但其它他想做的是X。他问怎么做Y是因为他觉得Y是最好搞定X的方法。 于是大家不断地回答“试试这个,试试那个”来帮助他,而他总是在说“这个有问题,那个有问题,因为……”。基本不同的情况,其它的方案可能会更好。

— from Re: Re: Re: Re: regex to validate e-mail addresses and phone numbers by Limbic~Region

X-Y Problem又叫“过早下结论”:提问者其实并不非常清楚想要解决的X问题,他猜测用Y可以搞定,于是他问大家如何实现Y。

— from <Pine.GHP.4.21.0009061210570.8800-100000@hpplus03.cern.ch> by Alan J. Flavell

其实这个问题在我之前的《你会问问题吗》里提到的那篇How To Ask Questions the Smart Way中的提到过,你可以移步去看一下

所以,我们在寻求别人帮助的时候,最好把我们想解决的问题和整个事情的来龙去脉说清楚。

一些变种

我们不要以为X-Y Problem就像上面那样的简单,我们不会出现,其实我们生活的这个世界有有各种X-Y Problem的变种。下面我个人觉得非常像XY Problem的总是:

  • 其一、大多数人有时候,非常容易把手段当目的,他们会用自己所喜欢的技术和方法来反推用户的需求,于是很有可能就会出现X-Y Problem – 也许解决用户需求最适合的技术方案是PC,但是我们要让他们用手机。
  • 其二、产品经理有时候并不清楚他想解决的用户需求是什么,于是他觉得可能开发Y的功能能够满足用户,于是他提出了Y的需求让技术人员去做,但那根本不是解决X问题的最佳方案。
  • 其三、因为公司或部门的一些战略安排,业务部门设计了相关的业务规划,然后这些业务规划更多的是公司想要的Y,而不是解决用户的X问题。
  • 其四、对于个人的职业发展,X是成长为有更强的技能和能力,这个可以拥有比别人更强的竞争力,从而可以有更好的报酬,但确走向了Y:全身心地追逐KPI。
  • 其五、本来我们想达成的X是做出更好和更有价值的产品,但最终走到了Y:通过各种手段提升安装量,点击量,在线量,用户量来衡量。
  • 其六、很多团队Leader都喜欢制造信息不平等,并不告诉团队某个事情的来由,掩盖X,而直接把要做的Y告诉团队,导致团队并不真正地理解,而产生了很多时间和经历的浪费。

所有的这些,在我心中都是X-Y Problem的变种,这是不是一种刻舟求剑的表现?

参考

2022-07-15 12:38

[轉載] CSS 变量教程

轉載自:阮一峰 CSS 变量教程

今年三月,微软宣布 Edge 浏览器将支持 CSS 变量。

这个重要的 CSS 新功能,所有主要浏览器已经都支持了。本文全面介绍如何使用它,你会发现原生 CSS 从此变得异常强大。

一、变量的声明

声明变量的时候,变量名前面要加两根连词线(--)。

body {
  --foo: #7F583F;
  --bar: #F7EFD2;
}

上面代码中,body选择器里面声明了两个变量:--foo--bar

它们与colorfont-size等正式属性没有什么不同,只是没有默认含义。所以 CSS 变量(CSS variable)又叫做"CSS 自定义属性"(CSS custom properties)。因为变量与自定义的 CSS 属性其实是一回事。

你可能会问,为什么选择两根连词线(--)表示变量?因为$foo被 Sass 用掉了,@foo被 Less 用掉了。为了不产生冲突,官方的 CSS 变量就改用两根连词线了。

各种值都可以放入 CSS 变量。

:root{
  --main-color: #4d4e53;
  --main-bg: rgb(255, 255, 255);
  --logo-border-color: rebeccapurple;

  --header-height: 68px;
  --content-padding: 10px 20px;

  --base-line-height: 1.428571429;
  --transition-duration: .35s;
  --external-link: "external link";
  --margin-top: calc(2vh + 20px);
}

变量名大小写敏感,--header-color--Header-Color是两个不同变量。

二、var() 函数

var()函数用于读取变量。

a {
  color: var(--foo);
  text-decoration-color: var(--bar);
}

var()函数还可以使用第二个参数,表示变量的默认值。如果该变量不存在,就会使用这个默认值。

color: var(--foo, #7F583F);

第二个参数不处理内部的逗号或空格,都视作参数的一部分。

var(--font-stack, "Roboto", "Helvetica");
var(--pad, 10px 15px 20px);

var()函数还可以用在变量的声明。

:root {
  --primary-color: red;
  --logo-text: var(--primary-color);
}

注意,变量值只能用作属性值,不能用作属性名。

.foo {
  --side: margin-top;
  /* 无效 */
  var(--side): 20px;
}

上面代码中,变量--side用作属性名,这是无效的。

三、变量值的类型

如果变量值是一个字符串,可以与其他字符串拼接。

--bar: 'hello';
--foo: var(--bar)' world';

利用这一点,可以 debug(例子)。

body:after {
  content: '--screen-category : 'var(--screen-category);
}

如果变量值是数值,不能与数值单位直接连用。

.foo {
  --gap: 20;
  /* 无效 */
  margin-top: var(--gap)px;
}

上面代码中,数值与单位直接写在一起,这是无效的。必须使用calc()函数,将它们连接。

.foo {
  --gap: 20;
  margin-top: calc(var(--gap) * 1px);
}

如果变量值带有单位,就不能写成字符串。

/* 无效 */
.foo {
  --foo: '20px';
  font-size: var(--foo);
}

/* 有效 */
.foo {
  --foo: 20px;
  font-size: var(--foo);
}

四、作用域

同一个 CSS 变量,可以在多个选择器内声明。读取的时候,优先级最高的声明生效。这与 CSS 的"层叠"(cascade)规则是一致的。

下面是一个例子

<style>
  :root { --color: blue; }
  div { --color: green; }
  #alert { --color: red; }
  * { color: var(--color); }
</style>

<p>蓝色</p>
<div>绿色</div>
<div id="alert">红色</div>

上面代码中,三个选择器都声明了--color变量。不同元素读取这个变量的时候,会采用优先级最高的规则,因此三段文字的颜色是不一样的。

这就是说,变量的作用域就是它所在的选择器的有效范围。

body {
  --foo: #7F583F;
}

.content {
  --bar: #F7EFD2;
}

上面代码中,变量--foo的作用域是body选择器的生效范围,--bar的作用域是.content选择器的生效范围。

由于这个原因,全局的变量通常放在根元素:root里面,确保任何选择器都可以读取它们。

:root {
  --main-color: #06c;
}

五、响应式布局

CSS 是动态的,页面的任何变化,都会导致采用的规则变化。

利用这个特点,可以在响应式布局的media命令里面声明变量,使得不同的屏幕宽度有不同的变量值。

body {
  --primary: #7F583F;
  --secondary: #F7EFD2;
}

a {
  color: var(--primary);
  text-decoration-color: var(--secondary);
}

@media screen and (min-width: 768px) {
  body {
    --primary:  #F7EFD2;
    --secondary: #7F583F;
  }
}

六、兼容性处理

对于不支持 CSS 变量的浏览器,可以采用下面的写法。

a {
  color: #7F583F;
  color: var(--primary);
}

也可以使用@support命令进行检测。

@supports ( (--a: 0)) {
  /* supported */
}

@supports ( not (--a: 0)) {
  /* not supported */
}

七、JavaScript 操作

JavaScript 也可以检测浏览器是否支持 CSS 变量。

const isSupported =
  window.CSS &&
  window.CSS.supports &&
  window.CSS.supports('--a', 0);

if (isSupported) {
  /* supported */
} else {
  /* not supported */
}

JavaScript 操作 CSS 变量的写法如下。

// 设置变量
document.body.style.setProperty('--primary', '#7F583F');

// 读取变量
document.body.style.getPropertyValue('--primary').trim();
// '#7F583F'

// 删除变量
document.body.style.removeProperty('--primary');

这意味着,JavaScript 可以将任意值存入样式表。下面是一个监听事件的例子,事件信息被存入 CSS 变量。

const docStyle = document.documentElement.style;

document.addEventListener('mousemove', (e) => {
  docStyle.setProperty('--mouse-x', e.clientX);
  docStyle.setProperty('--mouse-y', e.clientY);
});

那些对 CSS 无用的信息,也可以放入 CSS 变量。

--foo: if(x > 5) this.width = 10;

上面代码中,--foo的值在 CSS 里面是无效语句,但是可以被 JavaScript 读取。这意味着,可以把样式设置写在 CSS 变量中,让 JavaScript 读取。

所以,CSS 变量提供了 JavaScript 与 CSS 通信的一种途径。

八、参考链接

2022-07-15 11:21

[轉載] 跨域资源共享 CORS 详解

轉載自:阮一峰 跨域资源共享 CORS 详解

CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。

它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。

本文详细介绍CORS的内部机制。

一、简介

CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。

整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。

因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。

二、两种请求

浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。

只要同时满足以下两大条件,就属于简单请求。

(1) 请求方法是以下三种方法之一:
HEAD
GET
POST
(2)HTTP的头信息不超出以下几种字段:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type:只限于三个值
= application/x-www-form-urlencoded
= multipart/form-data
= text/plain

这是为了兼容表单(form),因为历史上表单一直可以发出跨域请求。AJAX 的跨域设计就是,只要表单可以发,AJAX 就可以直接发。

凡是不同时满足上面两个条件,就属于非简单请求。

浏览器对这两种请求的处理,是不一样的。

三、简单请求


3.1 基本流程

对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。

下面是一个例子,浏览器发现这次跨源AJAX请求是简单请求,就自动在头信息之中,添加一个Origin字段。

GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

上面的头信息中,Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。

如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequestonerror回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。

如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。

Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8

上面的头信息之中,有三个与CORS请求相关的字段,都以Access-Control-开头。

(1)Access-Control-Allow-Origin
该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。
(2)Access-Control-Allow-Credentials
该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。
(3)Access-Control-Expose-Headers
该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader('FooBar')可以返回FooBar字段的值。

3.2 withCredentials 属性

上面说到,CORS请求默认不发送Cookie和HTTP认证信息。如果要把Cookie发到服务器,一方面要服务器同意,指定Access-Control-Allow-Credentials字段。

Access-Control-Allow-Credentials: true

另一方面,开发者必须在AJAX请求中打开withCredentials属性。

var xhr = new XMLHttpRequest();
xhr.withCredentials = true;

否则,即使服务器同意发送Cookie,浏览器也不会发送。或者,服务器要求设置Cookie,浏览器也不会处理。

但是,如果省略withCredentials设置,有的浏览器还是会一起发送Cookie。这时,可以显式关闭withCredentials

xhr.withCredentials = false;

需要注意的是,如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。

四、非简单请求


4.1 预检请求

非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUTDELETE,或者Content-Type字段的类型是application/json

非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。

浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。

下面是一段浏览器的JavaScript脚本。

var url = 'http://api.alice.com/cors';
var xhr = new XMLHttpRequest();
xhr.open('PUT', url, true);
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.send();

上面代码中,HTTP请求的方法是PUT,并且发送一个自定义头信息X-Custom-Header

浏览器发现,这是一个非简单请求,就自动发出一个"预检"请求,要求服务器确认可以这样请求。下面是这个"预检"请求的HTTP头信息。

OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

"预检"请求用的请求方法是OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是Origin,表示请求来自哪个源。

除了Origin字段,"预检"请求的头信息包括两个特殊字段。

(1)Access-Control-Request-Method
该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是PUT
(2)Access-Control-Request-Headers
该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是X-Custom-Header

4.2 预检请求的回应

服务器收到"预检"请求以后,检查了OriginAccess-Control-Request-MethodAccess-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应。

HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain

上面的HTTP回应中,关键的是Access-Control-Allow-Origin字段,表示http://api.bob.com可以请求数据。该字段也可以设为星号,表示同意任意跨源请求。

Access-Control-Allow-Origin: *

如果服务器否定了"预检"请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获。控制台会打印出如下的报错信息。

XMLHttpRequest cannot load http://api.alice.com.
Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.

服务器回应的其他CORS相关字段如下。

Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1728000
(1)Access-Control-Allow-Methods
该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求。
(2)Access-Control-Allow-Headers
如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。
(3)Access-Control-Allow-Credentials
该字段与简单请求时的含义相同。
(4)Access-Control-Max-Age
该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。

4.3 浏览器的正常请求和回应

一旦服务器通过了"预检"请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。

下面是"预检"请求之后,浏览器的正常CORS请求。

PUT /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
X-Custom-Header: value
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

上面头信息的Origin字段是浏览器自动添加的。

下面是服务器正常的回应。

Access-Control-Allow-Origin: http://api.bob.com
Content-Type: text/html; charset=utf-8

上面头信息中,Access-Control-Allow-Origin字段是每次回应都必定包含的。

五、与JSONP的比较

CORS与JSONP的使用目的相同,但是比JSONP更强大。

JSONP只支持GET请求,CORS支持所有类型的HTTP请求。JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。

2015-04-27 17:49

[轉載] Spring 使用 Cache、整合 Ehcache

轉載自:Spring使用Cache、整合Ehcache

Spring 使用 Cache


從 3.1 開始,Spring 引入了對 Cache 的支持。其使用方法和原理都類似於 Spring 對事務管理的支持。Spring Cache 是作用在方法上的,其核心思想是這樣的:當我們在調用一個緩存方法時會把該方法參數和返回結果作為一個鍵值對存放在緩存中,等到下次利用同樣的參數來調用該方法時將不再執行該方法,而是直接從緩存中獲取結果進行返回。所以在使用 Spring Cache 的時候我們要保證我們緩存的方法對於相同的方法參數要有相同的返回結果。

使用 Spring Cache 需要我們做兩方面的事:
  • 聲明某些方法使用緩存
  • 配置 Spring 對 Cache 的支持

和 Spring 對事務管理的支持一樣,Spring 對 Cache 的支持也有基於注解和基於 XML 配置兩種方式。下面我們先來看看基於注解的方式。


1 基於注解的支持


Spring 為我們提供了幾個注解來支持 Spring Cache。其核心主要是 @Cacheable@CacheEvict。使用 @Cacheable 標記的方法在執行後 Spring Cache 將緩存其返回結果,而使用 @CacheEvict 標記的方法會在方法執行前或者執行後移除 Spring Cache 中的某些元素。下面我們將來詳細介紹一下 Spring 基於注解對 Cache 的支持所提供的幾個注解。


1.1 @Cacheable


@Cacheable 可以標記在一個方法上,也可以標記在一個類上。當標記在一個方法上時表示該方法是支持緩存的,當標記在一個類上時則表示該類所有的方法都是支持緩存的。對於一個支持緩存的方法,Spring 會在其被調用後將其返回值緩存起來,以保證下次利用同樣的參數來執行該方法時可以直接從緩存中獲取結果,而不需要再次執行該方法。Spring 在緩存方法的返回值時是以鍵值對進行緩存的,值就是方法的返回結果,至於鍵的話,Spring 又支持兩種策略,默認策略和自定義策略,這個稍後會進行說明。需要注意的是當一個支持緩存的方法在對像內部被調用時是不會觸發緩存功能的。@Cacheable 可以指定三個屬性,value、key 和 condition。


1.1.1 value 屬性指定 Cache 名稱


value 屬性是必須指定的,其表示當前方法的返回值是會被緩存在哪個 Cache 上的,對應 Cache 的名稱。其可以是一個 Cache 也可以是多個 Cache,當需要指定多個 Cache 時其是一個陣列。
@Cacheable("cache1") // Cache 是發生在 cache1 上的
public User find(Integer id) {
    return null;
}

@Cacheable({"cache1", "cache2"}) // Cache 是發生在 cache1 和 cache2 上的
public User find(Integer id) {
    return null;
}


1.1.2 使用 key 屬性自定義 key


key 屬性是用來指定 Spring 緩存方法的返回結果時對應的 key 的。該屬性支持 SpringEL 表達式。當我們沒有指定該屬性時,Spring 將使用默認策略生成 key。我們這裡先來看看自定義策略,至於默認策略會在後文單獨介紹。

自定義策略是指我們可以通過 Spring 的 EL 表達式來指定我們的 key。這裡的 EL 表達式可以使用方法參數及它們對應的屬性。使用方法參數時我們可以直接使用 "#參數名" 或者 "#p參數index"。下面是幾個使用參數作為 key 的示例。
@Cacheable(value="users", key="#id")
public User find(Integer id) {
    return null;
}

@Cacheable(value="users", key="#p0")
public User find(Integer id) {
    return null;
}

@Cacheable(value="users", key="#user.id")
public User find(User user) {
    return null;
}

@Cacheable(value="users", key="#p0.id")
public User find(User user) {
    return null;
}

除了上述使用方法參數作為 key 之外,Spring 還為我們提供了一個 root 對像可以用來生成 key。通過該 root 對像我們可以獲取到以下信息。
屬性名稱描述示例
methodName當前方法名#root.methodName
method當前方法#root.method.name
target當前被調用的對像#root.target
targetClass當前被調用的對像的 class#root.targetClass
args當前方法參數組成的陣列#root.args[0]
caches當前被調用的方法使用的 Cache#root.caches[0].name

當我們要使用 root 對像的屬性作為 key 時我們也可以將 "#root" 省略,因為 Spring 默認使用的就是 root 對像的屬性。如:
@Cacheable(value={"users", "xxx"}, key="caches[1].name")
public User find(User user) {
    return null;
}


1.1.3 condition 屬性指定發生的條件


有的時候我們可能並不希望緩存一個方法所有的返回結果。通過 condition 屬性可以實現這一功能。condition 屬性默認為空,表示將緩存所有的調用情形。其值是通過 SpringEL 表達式來指定的,當為 true 時表示進行緩存處理;當為 false 時表示不進行緩存處理,即每次調用該方法時該方法都會執行一次。如下示例表示只有當 user 的 id 為偶數時才會進行緩存。
@Cacheable(value={"users"}, key="#user.id", condition="#user.id%2==0")
public User find(User user) {
    System.out.println("find user by user " + user);
    return user;
}


1.2 @CachePut


在支持 Spring Cache 的環境下,對於使用 @Cacheable 標注的方法,Spring 在每次執行前都會檢查 Cache 中是否存在相同 key 的緩存元素,如果存在就不再執行該方法,而是直接從緩存中獲取結果進行返回,否則才會執行並將返回結果存入指定的緩存中。@CachePut 也可以聲明一個方法支持緩存功能。與 @Cacheable 不同的是使用 @CachePut 標注的方法在執行前不會去檢查緩存中是否存在之前執行過的結果,而是每次都會執行該方法,並將執行結果以鍵值對的形式存入指定的緩存中。

@CachePut 也可以標注在類上和方法上。使用 @CachePut 時我們可以指定的屬性跟 @Cacheable 是一樣的。
@CachePut("users") // 每次都會執行方法,並將結果存入指定的緩存中
public User find(Integer id) {
    return null;
}


1.3 @CacheEvict


@CacheEvict 是用來標注在需要清除緩存元素的方法或類上的。當標記在一個類上時表示其中所有的方法的執行都會觸發緩存的清除操作。@CacheEvict 可以指定的屬性有 value、key、condition、allEntries 和 beforeInvocation。其中 value、key 和 condition 的語義與 @Cacheable 對應的屬性類似。即 value 表示清除操作是發生在哪些 Cache 上的(對應 Cache 的名稱);key 表示需要清除的是哪個 key,如未指定則會使用默認策略生成的 key;condition 表示清除操作發生的條件。下面我們來介紹一下新出現的兩個屬性 allEntries和 beforeInvocation。


1.3.1 allEntries 屬性


allEntries 是 boolean 類型,表示是否需要清除緩存中的所有元素。默認為 false,表示不需要。當指定了 allEntries 為 true 時,Spring Cache 將忽略指定的 key。有的時候我們需要 Cache 一下清除所有的元素,這比一個一個清除元素更有效率。
@CacheEvict(value="users", allEntries=true)
public void delete(Integer id) {
    System.out.println("delete user by id: " + id);
}


1.3.2 beforeInvocation 屬性


清除操作默認是在對應方法成功執行之後觸發的,即方法如果因為拋出異常而未能成功返回時也不會觸發清除操作。使用 beforeInvocation 可以改變觸發清除操作的時間,當我們指定該屬性值為 true 時,Spring 會在調用該方法之前清除緩存中的指定元素。
@CacheEvict(value="users", beforeInvocation=true)
public void delete(Integer id) {
    System.out.println("delete user by id: " + id);
}

其實除了使用 @CacheEvict 清除緩存元素外,當我們使用 Ehcache 作為實現時,我們也可以配置 Ehcache 自身的驅除策略,其是通過 Ehcache 的配置文件來指定的。由於 Ehcache 不是本文描述的重點,這裡就不多贅述了,想了解更多關於 Ehcache 的信息,請查看我關於 Ehcache 的專欄


1.4 @Caching


@Caching 注解可以讓我們在一個方法或者類上同時指定多個 Spring Cache 相關的注解。其擁有三個屬性:cacheable、put 和 evict,分別用於指定 @Cacheable@CachePut@CacheEvict
@Caching(
    cacheable = @Cacheable("users"),
    evict = {
        @CacheEvict("cache2"),
        @CacheEvict(value = "cache3", allEntries = true)
    }
)
public User find(Integer id) {
    return null;
}


1.5 使用自定義注解


Spring 允許我們在配置可緩存的方法時使用自定義的注解,前提是自定義的注解上必須使用對應的注解進行標注。如我們有如下這麼一個使用 @Cacheable 進行標注的自定義注解。
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Cacheable(value="users")
public @interface MyCacheable {
}

那麼在我們需要緩存的方法上使用 @MyCacheable 進行標注也可以達到同樣的效果。
@MyCacheable
public User findById(Integer id) {
    System.out.println("find user by id: " + id);

    User user = new User();
    user.setId(id);
    user.setName("Name" + id);

    return user;
}



2 配置 Spring 對 Cache 的支持


2.1 聲明對Cache的支持


2.1.1 基於注解


配置 Spring 對基於注解的 Cache 的支持,首先我們需要在 Spring 的配置文件中引入 cache 命名空間,其次通過 <cache:annotation-driven/> 就可以啟用 Spring 對基於注解的 Cache 的支持。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:cache="http://www.springframework.org/schema/cache"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/cache
        http://www.springframework.org/schema/cache/spring-cache.xsd"
>
    <cache:annotation-driven/>
</beans>

<cache:annotation-driven/> 有一個 cache-manager 屬性用來指定當前所使用的 CacheManager 對應的 bean 的名稱,默認是 cacheManager,所以當我們的 CacheManager 的 id 為 cacheManager 時我們可以不指定該參數,否則就需要我們指定了。

<cache:annotation-driven/> 還可以指定一個 mode 屬性,可選值有 proxy 和 aspectj。默認是使用 proxy。當 mode 為 proxy 時,只有緩存方法在外部被調用的時候 Spring Cache 才會發生作用,這也就意味著如果一個緩存方法在其聲明對像內部被調用時 Spring Cache 是不會發生作用的。而 mode 為 aspectj 時就不會有這種問題。另外使用 proxy 時,只有 public 方法上的 @Cacheable 等標注才會起作用,如果需要非 public 方法上的方法也可以使用 Spring Cache 時把 mode 設置為 aspectj。

此外,<cache:annotation-driven/> 還可以指定一個 proxy-target-class 屬性,表示是否要代理 class,默認為 false。我們前面提到的 @Cacheable@cacheEvict 等也可以標注在接口上,這對於基於接口的代理來說是沒有什麼問題的,但是需要注意的是當我們設置 proxy-target-class 為 true 或者 mode 為 aspectj 時,是直接基於 class 進行操作的,定義在接口上的 @Cacheable 等 Cache 注解不會被識別到,那對應的 Spring Cache 也不會起作用了。

需要注意的是 <cache:annotation-driven/> 只會去尋找定義在同一個 ApplicationContext 下的 @Cacheable 等緩存注解。


2.1.2 基於 XML 配置


除了使用注解來聲明對 Cache 的支持外,Spring 還支持使用 XML 來聲明對 Cache 的支持。這主要是通過類似於 aop:advice 的 cache:advice 來進行的。在 cache 命名空間下定義了一個 cache:advice 元素用來定義一個對於 Cache 的 advice。其需要指定一個 cache-manager 屬性,默認為 cacheManager。 cache:advice 下面可以指定多個 cache:caching 元素,其有點類似於使用注解時的 @Caching 注解。cache:caching 元素下又可以指定 cache:cacheable、cache:cache-put 和 cache:cache-evict 元素,它們類似於使用注解時的 @Cacheable@CachePut@CacheEvict。下面來看一個示例:
<cache:advice id="cacheAdvice" cache-manager="cacheManager">
    <cache:caching cache="users">
        <cache:cacheable method="findById" key="#p0"/>
        <cache:cacheable method="find" key="#user.id"/>
        <cache:cache-evict method="deleteAll" all-entries="true"/>
    </cache:caching>
</cache:advice>

上面配置定義了一個名為 cacheAdvice 的 cache:advice,其中指定了將緩存 findById 方法和 find 方法到名為 users 的緩存中。這裡的方法還可以使用通配符 "*",比如 "find*" 表示任何以 "find" 開始的方法。

有了cache:advice 之後,我們還需要引入 aop 命名空間,然後通過 aop:config 指定定義好的 cacheAdvice 要應用在哪些 pointcut 上。如:

<aop:config proxy-target-class="false">
    <aop:advisor advice-ref="cacheAdvice" pointcut="execution(* com.xxx.UserService.*(..))"/>
</aop:config>

上面的配置表示在調用 com.xxx.UserService 中任意公共方法時將使用 cacheAdvice 對應的 cache:advice 來進行 Spring Cache 處理。更多關於 Spring AOP 的內容不在本文討論範疇內。


2.2 配置 CacheManager


CacheManager 是 Spring 定義的一個用來管理 Cache 的接口。Spring 自身已經為我們提供了兩種 CacheManager 的實現,一種是基於 Java API 的 ConcurrentMap,另一種是基於第三方 Cache 實現—— Ehcache,如果我們需要使用其它類型的緩存時,我們可以自己來實現 Spring 的 CacheManager 接口或 AbstractCacheManager 抽像類。下面分別來看看 Spring 已經為我們實現好了的兩種 CacheManager 的配置示例。


2.2.1 基於 ConcurrentMap 的配置


<bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager">
    <property name="caches">
        <set>
            <bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="xxx"/>
        </set>
    </property>
</bean>

上面的配置使用的是一個 SimpleCacheManager,其中包含一個名為 "xxx" 的 ConcurrentMapCache。


2.2.2 基於 Ehcache 的配置


<!-- Ehcache 實現 -->
<bean id="ehCacheManagerFactory" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
    <property name="configLocation" value="/WEB-INF/ehcache.xml"/>
</bean>

<bean id="ehCacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager">
    <property name="cacheManager" ref="ehCacheManagerFactory"/>
</bean>

上面的配置使用了一個 Spring 提供的 EhCacheCacheManager 來生成一個 Spring 的 CacheManager,其接收一個 Ehcache 的 CacheManager,因為真正用來存入緩存數據的還是 Ehcache。Ehcache 的 CacheManager 是通過 Spring 提供的 EhCacheManagerFactoryBean 來生成的,其可以通過指定 ehcache 的配置文件位置來生成一個 Ehcache 的 CacheManager。若未指定則將按照 Ehcache 的默認規則取 classpath 根路徑下的 ehcache.xml 文件,若該文件也不存在,則獲取 Ehcache 對應 jar 包中的 ehcache-failsafe.xml 文件作為配置文件。更多關於 Ehcache 的內容這裡就不多說了,它不屬於本文討論的內容,欲了解更多關於 Ehcache 的內容可以參考我之前發布的 Ehcache 系列文章,也可以參考官方文檔等。



3 鍵的生成策略


鍵的生成策略有兩種,一種是默認策略,一種是自定義策略。


3.1 默認策略


默認的 key 生成策略是通過 KeyGenerator 生成的,其默認策略如下:
  • 如果方法沒有參數,則使用 0 作為 key
  • 如果只有一個參數的話則使用該參數作為 key
  • 如果參數多餘一個的話則使用所有參數的 hashCode 作為 key

如果我們需要指定自己的默認策略的話,那麼我們可以實現自己的 KeyGenerator,然後指定我們的 Spring Cache 使用的 KeyGenerator 為我們自己定義的 KeyGenerator。

使用基於注解的配置時是通過 cache:annotation-driven 指定的.
<cache:annotation-driven key-generator="userKeyGenerator"/>
<bean id="userKeyGenerator" class="com.xxx.cache.UserKeyGenerator"/>

而使用基於 XML 配置時是通過 cache:advice 來指定的。
<cache:advice id="cacheAdvice" cache-manager="cacheManager" key-generator="userKeyGenerator">
</cache:advice>

需要注意的是此時我們所有的 Cache 使用的 Key 的默認生成策略都是同一個 KeyGenerator。


3.1 自定義策略


自定義策略是指我們可以通過 Spring 的 EL 表達式來指定我們的 key。這裡的 EL 表達式可以使用方法參數及它們對應的屬性。使用方法參數時我們可以直接使用 "#參數名" 或者 "#p參數index"。下面是幾個使用參數作為 key 的示例。
@Cacheable(value="users", key="#id")
public User find(Integer id) {
    return null;
}

@Cacheable(value="users", key="#p0")
public User find(Integer id) {
    return null;
}

@Cacheable(value="users", key="#user.id")
public User find(User user) {
    return null;
}

@Cacheable(value="users", key="#p0.id")
public User find(User user) {
    return null;
}

除了上述使用方法參數作為 key 之外,Spring 還為我們提供了一個 root 對像可以用來生成 key。通過該 root 對像我們可以獲取到以下信息。
屬性名稱描述示例
methodName當前方法名#root.methodName
method當前方法#root.method.name
target當前被調用的對像#root.target
targetClass當前被調用的對像的 class#root.targetClass
args當前方法參數組成的陣列#root.args[0]
caches當前被調用的方法使用的 Cache#root.caches[0].name

當我們要使用 root 對像的屬性作為 key 時我們也可以將 "#root" 省略,因為 Spring 默認使用的就是 root 對像的屬性。如:
@Cacheable(value={"users", "xxx"}, key="caches[1].name")
public User find(User user) {
    return null;
}



4 Spring 單獨使用 Ehcache


前面介紹的內容是 Spring 內置的對 Cache 的支持,其實我們也可以通過 Spring 自己單獨的使用 Ehcache 的 CacheManager 或 Ehcache 對像。通過在 Application Context 中配置 EhCacheManagerFactoryBean 和 EhCacheFactoryBean,我們就可以把對應的 EhCache 的 CacheManager 和 Ehcache 對像注入到其它的 Spring bean 對像中進行使用。


4.1 EhCacheManagerFactoryBean


EhCacheManagerFactoryBean 是 Spring 內置的一個可以產生 Ehcache 的 CacheManager 對像的 FactoryBean。其可以通過屬性 configLocation 指定用於創建 CacheManager 的 Ehcache 配置文件的路徑,通常是 ehcache.xml文件的路徑。如果沒有指定 configLocation,則將使用默認位置的配置文件創建 CacheManager,這是屬於 Ehcache 自身的邏輯,即如果在 classpath 根路徑下存在 ehcache.xml 文件,則直接使用該文件作為 Ehcache 的配置文件,否則將使用 ehcache-xxx.jar 中的 ehcache-failsafe.xml 文件作為配置文件來創建 Ehcache 的 CacheManager。此外,如果不希望創建的 CacheManager 使用默認的名稱(在 ehcache.xml 文件中定義的,或者是由 CacheManager 內部定義的),則可以通過 cacheManagerName 屬性進行指定。下面是一個配置 EhCacheManagerFactoryBean 的示例。
<!-- 定義 CacheManager -->
<bean id="cacheManager" 
       class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">

    <!-- 指定配置文件的位置 -->
    <property name="configLocation" value="/WEB-INF/config/ehcache.xml"/>

    <!-- 指定新建的 CacheManager 的名稱 -->
    <property name="cacheManagerName" value="cacheManagerName"/>
</bean>


4.2 EhCacheFactoryBean


EhCacheFactoryBean 是用來產生 Ehcache 的 Ehcache 對像的 FactoryBean。定義 EhcacheFactoryBean 時有兩個很重要的屬性我們可以來指定。一個是 cacheManager 屬性,其可以指定將用來獲取或創建 Ehcache 的 CacheManager 對像,若未指定則將通過 CacheManager.create() 獲取或創建默認的 CacheManager。另一個重要屬性是 cacheName,其表示當前 EhCacheFactoryBean 對應的是 CacheManager 中的哪一個 Ehcache 對像,若未指定默認使用 beanName 作為 cacheName。若 CacheManager 中不存在對應 cacheName 的 Ehcache 對像,則將使用 CacheManager 創建一個名為 cacheName 的 Cache 對像。此外我們還可以通過 EhCacheFactoryBean 的 timeToIdle、timeToLive 等屬性指定要創建的 Cache 的對應屬性,注意這些屬性只對 CacheManager 中不存在對應 Cache 時新建的 Cache 才起作用,對已經存在的 Cache 將不起作用,更多屬性設置請參考 Spring 的 API 文檔。此外還有幾個屬性是對不管是已經存在還是新創建的 Cache 都起作用的屬性:statisticsEnabled、sampledStatisticsEnabled、disabled、blocking 和 cacheEventListeners,其中前四個默認都是 false,最後一個表示為當前 Cache 指定 CacheEventListener。下面是一個定義 EhCacheFactoryBean 的示例。
<!-- 定義 CacheManager -->
<bean id="cacheManager" 
       class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">

    <!-- 指定配置文件的位置 -->
    <property name="configLocation" value="/WEB-INF/config/ehcache.xml"/>
    <!-- 指定新建的 CacheManager 的名稱 -->
    <property name="cacheManagerName" value="cacheManagerName"/>
</bean>

<!-- 定義一個 Ehcache -->
<bean id="userCache" 
       class="org.springframework.cache.ehcache.EhCacheFactoryBean">
    <property name="cacheName" value="user"/>
    <property name="cacheManager" ref="cacheManager"/>
</bean>


(注:本文是基於 Spring 3.1.0 所寫)
2015-03-03 13:34

[轉載] Spring Collections (List, Set, Map, and Properties)

轉載自:Spring Collections (List, Set, Map, and Properties) example

Spring examples to show you how to inject values into collections type (List, Set, Map, and Properties). 4 major collection types are supported :
  • List – <list/>
  • Set – <set/>
  • Map – <map/>
  • Properties – <props/>


Spring beans

A Customer object, with four collection properties.
package com.mkyong.common;

import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

public class Customer
{
    private List<Object> lists;
    private Set<Object> sets;
    private Map<Object, Object> maps;
    private Properties pros;

    //...
}
See different code snippets to declare collection in bean configuration file.


1. List example

<property name="lists">
    <list>
        <value>1</value>
        <ref bean="PersonBean" />
        <bean class="com.mkyong.common.Person">
            <property name="name" value="mkyongList" />
            <property name="address" value="address" />
            <property name="age" value="28" />
        </bean>
    </list>
</property>


2. Set example

<property name="sets">
    <set>
        <value>1</value>
        <ref bean="PersonBean" />
        <bean class="com.mkyong.common.Person">
            <property name="name" value="mkyongSet" />
            <property name="address" value="address" />
            <property name="age" value="28" />
        </bean>
    </set>
</property>


3. Map example

<property name="maps">
    <map>
        <entry key="Key 1" value="1" />
        <entry key="Key 2" value-ref="PersonBean" />
        <entry key="Key 3">
            <bean class="com.mkyong.common.Person">
                <property name="name" value="mkyongMap" />
                <property name="address" value="address" />
                <property name="age" value="28" />
            </bean>
        </entry>
    </map>
</property>


4. Properties example

<property name="pros">
    <props>
        <prop key="admin">admin@nospam.com</prop>
        <prop key="support">support@nospam.com</prop>
    </props>
</property>


Full Spring’s bean configuration file.

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">

    <bean id="CustomerBean" class="com.mkyong.common.Customer">

        <!-- java.util.List -->
        <property name="lists">
            <list>
                <value>1</value>
                <ref bean="PersonBean" />
                <bean class="com.mkyong.common.Person">
                    <property name="name" value="mkyongList" />
                    <property name="address" value="address" />
                    <property name="age" value="28" />
                </bean>
            </list>
        </property>

        <!-- java.util.Set -->
        <property name="sets">
            <set>
                <value>1</value>
                <ref bean="PersonBean" />
                <bean class="com.mkyong.common.Person">
                    <property name="name" value="mkyongSet" />
                    <property name="address" value="address" />
                    <property name="age" value="28" />
                </bean>
            </set>
        </property>

        <!-- java.util.Map -->
        <property name="maps">
            <map>
                <entry key="Key 1" value="1" />
                <entry key="Key 2" value-ref="PersonBean" />
                <entry key="Key 3">
                    <bean class="com.mkyong.common.Person">
                        <property name="name" value="mkyongMap" />
                        <property name="address" value="address" />
                        <property name="age" value="28" />
                    </bean>
                </entry>
            </map>
        </property>

        <!-- java.util.Properties -->
        <property name="pros">
            <props>
                <prop key="admin">admin@nospam.com</prop>
                <prop key="support">support@nospam.com</prop>
            </props>
        </property>

    </bean>

    <bean id="PersonBean" class="com.mkyong.common.Person">
        <property name="name" value="mkyong1" />
        <property name="address" value="address 1" />
        <property name="age" value="28" />
    </bean>
</beans>

Run it…
package com.mkyong.common;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class App {

    public static void main( String[] args ) {

        ApplicationContext context 
            = new ClassPathXmlApplicationContext("SpringBeans.xml");

        Customer cust = (Customer)context.getBean("CustomerBean");
        System.out.println(cust);
    }
}

Output
Customer [
    lists=[
        1,
        Person [address=address 1, age=28, name=mkyong1],
        Person [address=address, age=28, name=mkyongList]
    ],

    maps={
        key 1=1,
        key 2=Person [address=address 1, age=28, name=mkyong1],
        key 3=Person [address=address, age=28, name=mkyongMap]
    },

    pros={
        admin=admin@nospam.com,
        support=support@nospam.com
    },

    sets=[
        1,
        Person [address=address 1, age=28, name=mkyong1],
        Person [address=address, age=28, name=mkyongSet]
    ]
]
2015-03-01 18:10

[轉載] Make 命令教程

轉載自:Make 命令教程 - 阮一峰的网络日志

代碼變成可執行文件,叫做編譯(compile);先編譯這個,還是先編譯那個(即編譯的安排),叫做構建(build)。

Make 是最常用的構建工具,誕生於 1977 年,主要用於 C 語言的項目。但是實際上 ,任何只要某個文件有變化,就要重新構建的項目,都可以用 Make 構建。

本文介紹 Make 命令的用法,從簡單的講起,不需要任何基礎,只要會使用命令行,就能看懂。我的參考資料主要是Isaac Schlueter的《Makefile文件教程》《GNU Make手冊》


一、Make 的概念


Make 這個詞,英語的意思是"制作"。Make 命令直接用了這個意思,就是要做出某個文件。比如,要做出文件 a.txt,就可以執行下面的命令。

$ make a.txt

但是,如果你真的輸入這條命令,它並不會起作用。因為 Make 命令本身並不知道,如何做出 a.txt,需要有人告訴它,如何調用其他命令完成這個目標。

比如,假設文件 a.txt 依賴於 b.txt 和 c.txt ,是後面兩個文件連接(cat 命令)的產物。那麼,make 需要知道下面的規則。

a.txt: b.txt c.txt
    cat b.txt c.txt > a.txt

也就是說,make a.txt 這條命令的背後,實際上分成兩步:第一步,確認 b.txt 和 c.txt 必須已經存在,第二步使用 cat 命令 將這個兩個文件合並,輸出為新文件。

像這樣的規則,都寫在一個叫做 Makefile 的文件中,Make 命令依賴這個文件進行構建。Makefile 文件也可以寫為 makefile, 或者用命令行參數指定為其他文件名。

$ make -f rules.txt
# 或者
$ make --file=rules.txt

上面代碼指定 make 命令依據 rules.txt 文件中的規則,進行構建。

總之,make 只是一個根據指定的 Shell 命令進行構建的工具。它的規則很簡單,你規定要構建哪個文件、它依賴哪些源文件,當那些文件有變動時,如何重新構建它。



二、Makefile 文件的格式


構建規則都寫在 Makefile 文件裡面,要學會如何 Make 命令,就必須學會如何編寫 Makefile 文件。


2.1 概述


Makefile 文件由一系列規則(rules)構成。每條規則的形式如下。

<target> : <prerequisites>
[tab]  <commands>

上面第一行冒號前面的部分,叫做"目標"(target),冒號後面的部分叫做"前置條件"(prerequisites);第二行必須由一個tab鍵起首,後面跟著"命令"(commands)。

"目標"是必需的,不可省略;"前置條件"和"命令"都是可選的,但是兩者之中必須至少存在一個。

每條規則就明確兩件事:構建目標的前置條件是什麼,以及如何構建。下面就詳細講解,每條規則的這三個組成部分。


2.2 目標(target)


一個目標(target)就構成一條規則。目標通常是文件名,指明Make命令所要構建的對像,比如上文的 a.txt 。目標可以是一個文件名,也可以是多個文件名,之間用空格分隔。

除了文件名,目標還可以是某個操作的名字,這稱為"偽目標"(phony target)。

clean:
    rm *.o

上面代碼的目標是 clean,它不是文件名,而是一個操作的名字,屬於"偽目標 ",作用是刪除對像文件。

$ make  clean

但是,如果當前目錄中,正好有一個文件叫做 clean,那麼這個命令不會執行。因為 Make 發現 clean 文件已經存在,就認為沒有必要重新構建了,就不會執行指定的 rm 命令。

為了避免這種情況,可以明確聲明 clean 是"偽目標",寫法如下。

.PHONY: clean
clean:
    rm *.o temp

聲明 clean 是"偽目標"之後,make 就不會去檢查是否存在一個叫做 clean 的文件,而是每次運行都執行對應的命令。像 .PHONY 這樣的內置目標名還有不少,可以查看手冊

如果 Make 命令運行時沒有指定目標,默認會執行 Makefile 文件的第一個目標。

$ make

上面代碼執行 Makefile 文件的第一個目標。


2.3 前置條件(prerequisites)


前置條件通常是一組文件名,之間用空格分隔。它指定了"目標"是否重新構建的判斷標准:只要有一個前置文件不存在,或者有過更新(前置文件的 last-modification 時間戳比目標的時間戳新),"目標"就需要重新構建。

result.txt: source.txt
    cp source.txt result.txt

上面代碼中,構建 result.txt 的前置條件是 source.txt 。如果當前目錄中,source.txt 已經存在,那麼 make result.txt 可以正常運行,否則必須再寫一條規則,來生成 source.txt 。

source.txt:
    echo "this is the source" > source.txt

上面代碼中,source.txt 後面沒有前置條件,就意味著它跟其他文件都無關,只要這個文件還不存在,每次調用 make source.txt,它都會生成。

$ make result.txt
$ make result.txt

上面命令連續執行兩次 make result.txt。第一次執行會先新建 source.txt,然後再新建 result.txt。第二次執行,Make 發現 source.txt 沒有變動(時間戳晚於 result.txt),就不會執行任何操作,result.txt 也不會重新生成。

如果需要生成多個文件,往往采用下面的寫法。

source: file1 file2 file3

上面代碼中,source 是一個偽目標,只有三個前置文件,沒有任何對應的命令。

$ make source

執行 make source 命令後,就會一次性生成 file1,file2,file3 三個文件。這比下面的寫法要方便很多。

$ make file1
$ make file2
$ make file3


2.4 命令(commands)


命令(commands)表示如何更新目標文件,由一行或多行的 Shell 命令組成。它是構建"目標"的具體指令,它的運行結果通常就是生成目標文件。

每行命令之前必須有一個 tab 鍵。如果想用其他鍵,可以用內置變量 .RECIPEPREFIX 聲明。

.RECIPEPREFIX = >
all:
> echo Hello, world

上面代碼用 .RECIPEPREFIX 指定,大於號(>)替代 tab 鍵。所以,每一行命令的起首變成了大於號,而不是 tab 鍵。

需要注意的是,每行命令在一個單獨的 shell 中執行。這些 Shell 之間沒有繼承關系。

var-lost:
    export foo=bar
    echo "foo=[$$foo]"

上面代碼執行後(make var-lost),取不到 foo 的值。因為兩行命令在兩個不同的進程執行。一個解決辦法是將兩行命令寫在一行,中間用分號分隔。

var-kept:
    export foo=bar; echo "foo=[$$foo]"

另一個解決辦法是在換行符前加反斜杠轉義。

var-kept:
    export foo=bar; \
    echo "foo=[$$foo]"

最後一個方法是加上 .ONESHELL: 命令。

.ONESHELL:
var-kept:
    export foo=bar;
    echo "foo=[$$foo]"



三、Makefile文件的語法


3.1 注釋


井號(#)在 Makefile 中表示注釋。

# 这是注释
result.txt: source.txt
    # 这是注释
    cp source.txt result.txt # 这也是注释


3.2 回聲(echoing)


正常情況下,make 會打印每條命令,然後再執行,這就叫做回聲(echoing)。

test:
    # 这是测试

執行上面的規則,會得到下面的結果。

$ make test
# 这是测试

在命令的前面加上 @,就可以關閉回聲。

test:
    @# 这是测试

現在再執行 make test,就不會有任何輸出。

由於在構建過程中,需要了解當前在執行哪條命令,所以通常只在注釋和純顯示的 echo 命令前面加上 @

test:
    @# 这是测试
    @echo TODO


3.3 通配符


通配符(wildcard)用來指定一組符合條件的文件名。Makefile 的通配符與 Bash 一致,主要有星號(*)、問號(?)和 [...] 。比如, *.o 表示所有後綴名為 o 的文件。

clean:
    rm -f *.o


3.4 模式匹配


Make 命令允許對文件名,進行類似正則運算的匹配,主要用到的匹配符是 %。比如,假定當前目錄下有 f1.c 和 f2.c 兩個源碼文件,需要將它們編譯為對應的對像文件。

%.o: %.c

等同於下面的寫法。

f1.o: f1.c
f2.o: f2.c

使用匹配符 %,可以將大量同類型的文件,只用一條規則就完成構建。


3.5 變量和賦值符


Makefile 允許使用等號自定義變量。

txt = Hello World
test:
    @echo $(txt)

上面代碼中,變量 txt 等於 Hello World。調用時,變量需要放在 $( ) 之中。

調用Shell變量,需要在美元符號前,再加一個美元符號,這是因為Make命令會對美元符號轉義。

test:
    @echo $$HOME

有時,變量的值可能指向另一個變量。

v1 = $(v2)

上面代碼中,變量 v1 的值是另一個變量 v2。這時會產生一個問題,v1 的值到底在定義時擴展(靜態擴展),還是在運行時擴展(動態擴展)?如果 v2 的值是動態的,這兩種擴展方式的結果可能會差異很大。

為了解決類似問題,Makefile 一共提供了四個賦值運算符 (=、:=、?=、+=),它們的區別請看 StackOverflow

VARIABLE = value
# 在执行时扩展,允许递归扩展。

VARIABLE := value
# 在定义时扩展。

VARIABLE ?= value
# 只有在该变量为空时才设置值。

VARIABLE += value
# 将值追加到变量的尾端。


3.6 內置變量(Implicit Variables)


Make命令提供一系列內置變量,比如,$(CC) 指向當前使用的編譯器,$(MAKE) 指向當前使用的Make工具。這主要是為了跨平台的兼容性,詳細的內置變量清單見手冊

output:
    $(CC) -o output input.c


3.7 自動變量(Automatic Variables)


Make 命令還提供一些自動變量,它們的值與當前規則有關。主要有以下幾個。


(1)$@

$@指代當前目標,就是Make命令當前構建的那個目標。比如,make foo的 $@ 就指代foo。

a.txt b.txt:
    touch $@

等同於下面的寫法。

a.txt:
    touch a.txt
b.txt:
    touch b.txt


(2)$<

$< 指代第一個前置條件。比如,規則為 t: p1 p2,那麼$< 就指代p1。

a.txt: b.txt c.txt
    cp $< $@

等同於下面的寫法。

a.txt: b.txt c.txt
    cp b.txt a.txt


(3)$?

$? 指代比目標更新的所有前置條件,之間以空格分隔。比如,規則為 t: p1 p2,其中 p2 的時間戳比 t 新,$?就指代p2。


(4)$^

$^ 指代所有前置條件,之間以空格分隔。比如,規則為 t: p1 p2,那麼 $^ 就指代 p1 p2 。


(5)$*

$* 指代匹配符 % 匹配的部分, 比如% 匹配 f1.txt 中的f1 ,$* 就表示 f1。


(6)$(@D) 和 $(@F)

$(@D) 和 $(@F) 分別指向 $@ 的目錄名和文件名。比如,$@是 src/input.c,那麼$(@D) 的值為 src ,$(@F) 的值為 input.c。


(7)$(<D) 和 $(<F)

$(<D) 和 $(<F) 分別指向 $< 的目錄名和文件名。

所有的自動變量清單,請看手冊。下面是自動變量的一個例子。

dest/%.txt: src/%.txt
    @[ -d dest ] || mkdir dest
    cp $< $@

上面代碼將 src 目錄下的 txt 文件,拷貝到 dest 目錄下。首先判斷 dest 目錄是否存在,如果不存在就新建,然後,$< 指代前置文件(src/%.txt), $@ 指代目標文件(dest/%.txt)。


3.8 判斷和循環


Makefile使用 Bash 語法,完成判斷和循環。

ifeq ($(CC),gcc)
    libs=$(libs_for_gcc)
else
    libs=$(normal_libs)
endif

上面代碼判斷當前編譯器是否 gcc ,然後指定不同的庫文件。

LIST = one two three
all:
    for i in $(LIST); do \
        echo $$i; \
    done

# 等同于

all:
    for i in one two three; do \
        echo $i; \
    done

上面代碼的運行結果。

one
two
three


3.9 函數


Makefile 還可以使用函數,格式如下。

$(function arguments)
# 或者
${function arguments}

Makefile提供了許多內置函數,可供調用。下面是幾個常用的內置函數。


(1)shell 函數

shell 函數用來執行 shell 命令

srcfiles := $(shell echo src/{00..99}.txt)


(2)wildcard 函數

wildcard 函數用來在 Makefile 中,替換 Bash 的通配符。

srcfiles := $(wildcard src/*.txt)


(3)subst 函數

subst 函數用來文本替換,格式如下。

$(subst from,to,text)

下面的例子將字符串 "feet on the street" 替換成 "fEEt on the strEEt"。

$(subst ee,EE,feet on the street)

下面是一個稍微復雜的例子。

comma:= ,
empty:=
# space变量用两个空变量作为标识符,当中是一个空格
space:= $(empty) $(empty)
foo:= a b c
bar:= $(subst $(space),$(comma),$(foo))
# bar is now `a,b,c'.


(4)patsubst 函數

patsubst 函數用於模式匹配的替換,格式如下。

$(patsubst pattern,replacement,text)

下面的例子將文件名 "x.c.c bar.c",替換成 "x.c.o bar.o"。

$(patsubst %.c,%.o,x.c.c bar.c)


(5)替換後綴名

替換後綴名函數的寫法是:變量名 + 冒號 + 後綴名替換規則。它實際上 patsubst 函數的一種簡寫形式。

min: $(OUTPUT:.js=.min.js)

上面代碼的意思是,將變量 OUTPUT 中的後綴名 .js 全部替換成 .min.js 。


四、Makefile 的實例



(1)執行多個目標

.PHONY: cleanall cleanobj cleandiff

cleanall : cleanobj cleandiff
    rm program

cleanobj :
    rm *.o

cleandiff :
    rm *.diff

上面代碼可以調用不同目標,刪除不同後綴名的文件,也可以調用一個目標(cleanall),刪除所有指定類型的文件。


(2)編譯C語言項目

edit : main.o kbd.o command.o display.o
    cc -o edit main.o kbd.o command.o display.o

main.o : main.c defs.h
    cc -c main.c
kbd.o : kbd.c defs.h command.h
    cc -c kbd.c
command.o : command.c defs.h command.h
    cc -c command.c
display.o : display.c defs.h
    cc -c display.c

clean :
    rm edit main.o kbd.o command.o display.o

.PHONY: edit clean
2015-02-26 16:15

[轉載] Java 內部類別

轉載自:Java Gossip: 內部類別

可以在類別中再定義類別,稱之為內部類別(Inner class),初學者暫時不會使用到內部類別,在這邊先簡單介紹語法,雖然會無聊一些,不過之後章節就會看到相關應用。

內部類別可以定義在類別區塊之中。例如以下程式片段建立了非靜態的內部類別:

class Some {
    class Other {
    }
}

雖然實務上很少看到接下來的寫法,不過要使用 Some 中的 Other 類別,必須先建立實例 Some 實例。例如:

Some s = new Some();
Some.Other o =  s.new Other();

內部類別也可以使用 publicprotectedprivate 宣告。例如:

class Some {
    private class Other {
    }
}

內部類別本身可以存取外部類別的成員,通常非靜態內部類別會宣告為 private,這類內部類別是輔助類別中某些操作而設計,外部不用知道內部類別的存在。

內部類別也可以宣告為 static。例如:

class Some {
    static class Other {
    }
}

一個被宣告為 static 的內部類別,通常是將外部類別當作名稱空間。你可以如下建立類別實例:

Some.Other o = new Some.Other();

被宣告為 static 的內部類別,雖然將外部類別當作名稱空間,但算是個獨立類別,它可以存取外部類別 static 成員,但不可存取外部類別非 static 成員。例如:


方法中也可以宣告類別,這通常是輔助方法中演算之用,方法外無法使用。例如:

class Some {
    public void doSome() {
        class Other {
        }
    }
}

實務上比較少看到在方法中定義具名的內部類別,倒很常看到方法中定義匿名內部類別(Anonymous inner class)並直接實例化,這跟類別繼承或介面實作有關,以下先看一下語法,細節留到談到繼承與介面時再作討論:

Object o = new Object() {
    public String toString() {
        return "無聊的語法示範而已";
    }
};

如果要稍微解釋一下,這個語法定義了一個沒有名稱的類別,它繼承 Object 類別,並重新定義(Override)了 toString() 方法,new 表示實例化這個沒有名稱的類別。匿名類別語法本身,在某些場合有時有些囉嗦,JDK 8 提出了 Lambda,有一部份目的是用來解決匿名類別語法囉嗦的問題,之後會再討論。
2015-02-26 16:07

[轉載] Java 動態代理

轉載自:Java Essence: 動態代理

來看一個最簡單的例子,當您需要在執行某些方法時留下日誌訊息,直覺的,您可能會如下撰寫:
package cc.openhome;

import java.util.logging.*;

public class HelloSpeaker {
    private Logger logger =
            Logger.getLogger(this.getClass().getName());

    public void hello(String name) {
        // 方法執行開始時留下日誌
        logger.log(Level.INFO, "hello method starts....");
        // 程式主要功能
        System.out.println("Hello, " + name);
        // 方法執行完畢前留下日誌
        logger.log(Level.INFO, "hello method ends....");
    }
}


HelloSpeaker 類別中,當執行 hello() 方法時,你希望該方法執行開始與執行完畢時都能留下日誌,最簡單的作法就是如以上的程式設計,在 方法執行的前後加上日誌動作,然而記錄的這幾行程式碼橫切入(Cross-cutting)HelloSpeaker 類別中,對於 HelloSpeaker 來說,日誌的這幾個動作並不屬於 HelloSpeaker 商務邏輯(顯示"Hello"等文字),這使得 HelloSpeaker 增加了額外的職責。

想想如果程式中這種日誌的動作到處都有需求,以上的寫法勢必造成你必須到處撰寫這些日誌動作的程式碼,這將使得維護日誌程式碼的困難度加大。如果需要的服 務(Service)不只有日誌動作,有一些非物件本身職責的相關動作也混入了物件之中(例如權限檢查、交易管理等等),會使得物件的負擔更形加重,甚至 混淆了物件本身該負有的職責,物件本身的職責所佔的程式碼,或許還小於這些與物件職責不相關的動作或服務的程式碼。

另一方面,使用以上的寫法,若你有一日不再需要日誌(或權限檢查、交易管理等)的服務,那麼你將需要修改所有留下日誌動作的程式碼,你無法簡單的就將這些相關服務從即有的程式中移去。

可以使用代理(Proxy)機制來解決這個問題,在這邊討論兩種代理方式:
  • 靜態代理(Static proxy)
  • 動態代理(Dynamic proxy)

在靜態代理的實現中,代理物件與被代理的物件都必須實現同一個介面,在代理物件中可以實現記錄等相關服務,並在需要的時候再呼叫被代理的物件,如此被代理物件當中就可以僅保留商務相關職責。

舉個實際的例子來說,首先定義一個 IHello 介面:
IHello.java
package cc.openhome;

public interface IHello {
    public void hello(String name);
}


然後讓實現商務邏輯的 HelloSpeaker 類別要實現 IHello 介面,例如:
HelloSpeaker.java
package cc.openhome;

public class HelloSpeaker implements IHello {
    public void hello(String name) {
        System.out.println("Hello, " + name);
    }
}


可以看到,在 HelloSpeaker 類別中現在沒有任何日誌的程式碼插入其中,日誌服務的實現將被放至代理物件之中,代理物件同樣也要實現 IHello 介面,例如:
HelloProxy.java
package cc.openhome;

import java.util.logging.*;

public class HelloProxy implements IHello {
    private Logger logger =
            Logger.getLogger(this.getClass().getName());

    private IHello helloObject;

    public HelloProxy(IHello helloObject) {
        this.helloObject = helloObject;
    }

    public void hello(String name) {
        // 日誌服務
        log("hello method starts....");

        // 執行商務邏輯
        helloObject.hello(name);

        // 日誌服務
        log("hello method ends....");
    }

    private void log(String msg) {
        logger.log(Level.INFO, msg);
    }
}


HelloProxy 類別的 hello() 方法中,真正實現商務邏輯前後可以安排記錄服務,可以實際撰寫一個測試程式來看看如何使用代理物件。
ProxyDemo.java
package cc.openhome;

public class ProxyDemo {
    public static void main(String[] args) {
        IHello proxy =
            new HelloProxy(new HelloSpeaker());
        proxy.hello("Justin");
    }
}


程式中呼叫執行的是代理物件,建構代理物件時必須給它一個被代理物件,記得在操作取回的代理物件時,必須轉換操作介面為 IHello 介面。

代理物件 HelloProxy 將代理真正的 HelloSpeaker 來執行 hello(),並在其前後加上日誌的動作,這使得我們的 HelloSpeaker 在撰寫時不必介入日誌動作,HelloSpeaker 可以專心於它的職責。

在 JDK 1.3 之後加入了可協助開發動態代理功能的 API 等相關類別,您不必為特定物件與方法撰寫特定的代理物件,使用動態代理,可以使得一個處理者 (Handler)服務於各個物件,首先,一個處理者的類別設計必須實作 java.lang.reflect.InvocationHandler 介面, 以實例來進行說明,例如設計一個 LogHandler 類別:
LogHandler.java
package cc.openhome;

import java.util.logging.*;
import java.lang.reflect.*;

public class LogHandler implements InvocationHandler {
    private Logger logger =
            Logger.getLogger(this.getClass().getName());

    private Object delegate;

    public Object bind(Object delegate) {
        this.delegate = delegate;
        return Proxy.newProxyInstance(
            delegate.getClass().getClassLoader(),
            delegate.getClass().getInterfaces(),
            this
        );
    }

    public Object invoke(Object proxy, Method method, Object[] args) 
        throws Throwable 
    {
        Object result = null;

        try {
            log("method starts..." + method);

            result = method.invoke(delegate, args);

            logger.log(Level.INFO, "method ends..." + method);
        } catch (Exception e){
            log(e.toString());
        }

        return result;
    }

    private void log(String message) {
        logger.log(Level.INFO, message);
    }
}


主要的概念是使用 Proxy.newProxyInstance() 靜態方法建立一個代理物件(底層會使用 Native 的方式生成代理物件的 Class 實例),建立代理物件時必須告知所要代理的介面,之後您可以操作所 建立的代理物件,在每次操作時會呼叫 InvocationHandlerinvoke() 方法,invoke() 方法會傳入被代理物件的方法名稱與執行 參數,實際上要執行的方法交由 method.invoke(),您在 method.invoke() 前後加上記錄動作,method.invoke() 傳 回的物件是實際方法執行過後的回傳結果。

接下來撰寫一個測試的程式,您要使用 LogHandlerbind() 方法來綁定被代理物件,如下所示:
ProxyDemo.java
package cc.openhome;

public class ProxyDemo {
    public static void main(String[] args) {
        LogHandler logHandler  = new LogHandler();

        IHello helloProxy =
            (IHello) logHandler.bind(new HelloSpeaker());

        helloProxy.hello("Justin");
    }
}

2015-02-26 15:44

[轉載] Java 型態通配字元

轉載自:Java Essence: 我只收這種東西

如果你定義了以下的類別:
class Node<T> {
    T value;
    Node<T> next;

    Node(T value, Node<T> next) {
        this.value = value;
        this.next = next;
    }
}


如果在以下的例子中:
class Fruit {}
class Apple extends Fruit {
    @Override
    public String toString() {
        return "Apple";
    }
}

class Banana extends Fruit {
    @Override
    public String toString() {
        return "Banana";
    }
}


public class Main {
    public static void main(String[] args) {
        Node<Apple> apple = new Node<Apple>(new Apple(), null);
        Node<Fruit> fruit = apple;  // 編譯錯誤,incompatible types
    }
}



在範例中,apple 的型態是 Node<Apple>,而 fruit 的型態為 Node<Fruit>,你將 apple 所參考的物件 給 fruit 參考,那麼 Node<Apple> 該是一種 Node<Fruit> 呢?在上例中編譯器給你的答案為「不是」!

如 果 B 是 A 的子型態,而 Node<B> 被視為一種 Node<A> 型態,則稱 Node 具有共變性(Covariance)或有彈性的(flexible)。如 果 Node<A> 被視為一種 Node<B> 型態,則稱 Node 具有逆變性(Contravariance)。如果不具共變性或逆變性,則 Node 是不可變 的(nonvariant)嚴謹的(rigid)

Java 的泛型不支援共變性,不過可以使用型態通配字元 ?extends 來宣告變數,使其達到類似共變性,例如:
public class Main {
    public static void main(String[] args) {
        Node<Apple> apple = new Node<Apple>(new Apple(), null);
        Node<? extends Fruit> fruit = apple; // 類似共變性效果
    }
}


一個實際應用的例子是:
public class Main {
    public static void main(String[] args) {
        Node<Apple> apple1 = new Node<Apple>(new Apple(), null);
        Node<Apple> apple2 = new Node<Apple>(new Apple(), apple1);
        Node<Apple> apple3 = new Node<Apple>(new Apple(), apple2);

        Node<Banana> banana1 = new Node<Banana>(new Banana(), null);
        Node<Banana> banana2 = new Node<Banana>(new Banana(), banana1);

        show(apple3);
        show(banana2);
    }

    static void show(Node<? extends Fruit> n) {
        Node<? extends Fruit> node = n;
        do {
            System.out.println(node.value);
            node = node.next;
        } while(node != null);
    }
}



你的目的是可以顯示所有的水果節點,由於 show() 方法使用型態通配字元宣告參數,使得 n 具備類似共變性的效果,因此 show() 方法就可以顯示 Node<Apple> 也可以顯示 Node<Banana>

Java 的泛型亦不支援逆變性,不過可以使用型態通配字元 ?super 來宣告變數,使其達到類似逆變性,例如:
public class Main {
    public static void main(String[] args) {
        Node<Fruit> fruit = new Node<Fruit>(new Fruit(), null);
        Node<? super Apple> apple = fruit;
        Node<? super Banana> banana = fruit;
    }
}


一個實際應用的例子如下:
class Fruit {
    int price;
    int weight;
    Fruit(int price, int weight) {
        this.price = price;
        this.weight = weight;
    }
}

class Apple extends Fruit {
     Apple(int price, int weight) {
         super(price, weight);
     }
}

class Banana extends Fruit {
     Banana(int price, int weight) {
         super(price, weight);
     }
}

interface Comparator<T> {
    int compare(T t1, T t2);
}

class Basket<T> {
    private T[] things;
    Basket(T... things) {
        this.things = things;
    }
    void sort(Comparator<? super T> comparator) {
        // 作一些排序
    }
}


籃子(Basket)中可以置放各種物品,並可以傳入一個比較器(Comparator)進行排序。假設你分別在兩個籃子中放置了蘋果(Apple)與香蕉(Banana):
public class Main {
    public static void main(String[] args) {
        Comparator<Fruit> comparator = new Comparator<Fruit>() {
            public int compare(Fruit f1, Fruit f2) {
                return f1.price - f2.price;
            }
        };
        Basket<Apple> b1 = new Basket<Apple>(
            new Apple(20, 100), new Apple(25, 150)
        );
        Basket<Banana> b2 = new Basket<Banana>(
            new Banana(30, 200), new Banana(25, 250)
        );
        b1.sort(comparator);
        b2.sort(comparator);
    }
}


現在 b1 的型態為 Basket<Apple>,而 b2 的型態為 Basket<Banana>,你可以如上實作一個水果(Fruit)比較器,比較水果的價格進行排序,這可以同時適用於 Basket<Apple>Basket<Banana>
2015-02-23 00:00

[轉載] Java 字符串格式化:String.format()方法的使用

轉載自:java字符串格式化:String.format()方法的使用 - kgd1120 - ITeye技术网站

常規類型的格式化


String 類的 format() 方法用於創建格式化的字符串以及連接多個字符串對像。熟悉 C 語言的讀者應該記得 C 語言的 sprintf() 方法,兩者有類似之處。format() 方法有兩種重載形式。

format(String format, Object... args)
該方法使用指定的字符串格式和參數生成格式化的新字符串。 新字符串始終使用本地語言環境。例如當前日期信息在中國語言環境中的表現形式為 “2007-10-27”,但是在其他國家有不同的表現形式。
  • format:字符串格式。
  • args...:字符串格式中由格式說明符引用的參數。如果還有格式說明符以外的參數,則忽略這些額外的參數。參數的數目是可變的,可以為 0。

format(Locale locale, String format, Object... args)
該方法使用指定的語言環境、字符串格式和參數生成一個格式化的新字符串。新字符串始終使用指定的語言環境。
  • locale:指定的語言環境。
  • format:字符串格式。
  • args...:字符串格式中由格式說明符引用的參數。如果還有格式說明符以外的參數,則忽略這些額外的參數。參數的數目是可變的,可以為 0。

format() 方法中的字符串格式參數有很多種轉換符選項,例如:日期、整數、浮點數等:
轉換符說明示例
%s字符串類型"mingrisoft"
%c字符類型'm'
%b布爾類型true
%d整數類型(十進制)99
%x整數類型(十六進制)FF
%o整數類型(八進制)77
%f浮點類型99.99
%a十六進制浮點類型FF.35AE
%e指數類型9.38e+5
%g通用浮點類型(f和e類型中較短的)
%h散列碼
%%百分比類型%
%n換行符
%tx日期與時間類型(x代表不同的日期與時間轉換符)


使用各種轉換符實現不同數據類型到字符串的轉換:
public static void main(String[] args) {

    String str=null;

    // 格式化字符串
    str=String.format("Hi,%s", "飛龍");

    // 輸出字符串變量str的內容
    System.out.println(str);

    System.out.printf("字母a的大寫是:%c %n", 'A');

    System.out.printf("3>7的結果是:%b %n", 3>7);

    System.out.printf("100的一半是:%d %n", 100/2);

    System.out.printf("100的16進制數是:%x %n", 100);

    System.out.printf("100的8進制數是:%o %n", 100);

    System.out.printf("50元的書打8.5折扣是:%f 元%n", 50*0.85);

    System.out.printf("上面價格的16進制數是:%a %n", 50*0.85);

    System.out.printf("上面價格的指數表示:%e %n", 50*0.85);

    System.out.printf(
        "上面價格的指數和浮點數結果的長度較短的是:%g %n", 50*0.85
    );

    System.out.printf("上面的折扣是%d%% %n", 85);

    System.out.printf("字母A的散列碼是:%h %n", 'A');
}

輸出以下結果:
Hi,飛龍

字母a的大寫是:A

3>7的結果是:false

100的一半是:50

100的16進制數是:64

100的8進制數是:144

50元的書打8.5折扣是:42.500000 元

上面價格的16進制數是:0x1.54p5

上面價格的指數表示:4.250000e+01

上面價格的指數和浮點數結果的長度較短的是:42.5000

上面的折扣是85%

字母A的散列碼是:41


這些字符串格式參數不但可以靈活將其他數據類型轉換成字符串,而且可以與各種標誌組合在一起,生成各種格式的字符串:
標誌說明示例結果
+為正數或者負數添加符號("%+d",15)+15
左對齊("%-5d",15)|15 |
0數字前面補0("%04d", 99)0099
空格在整數之前添加指定數量的空格("% 4d", 99)| 99|
,以“,”對數字分組("%,f", 9999.99)9,999.990000
(使用括號包含負數("%(f", -99.99)(99.990000)
#如果是浮點數則包含小數點,
如果是16進制或8進制則添加0x或0
("%#x", 99)
("%#o", 99)
0x63
0143
<格式化前一個轉換符所描述的參數("%f 和 %<3.2f", 99.45)99.450000 和 99.45
$被格式化的參數索引("%1$d,%2$s", 99,"abc")99,abc


使用幾種常用的轉換符組合標誌實現字符串的格式化:
public static void main(String[] args) {

    String str=null;

    // 格式化字符串
    str=String.format("格式參數$的使用:%1$d,%2$s", 99,"abc");

    // 輸出字符串變量
    System.out.println(str);

    System.out.printf("顯示正負數的符號:%+d與%d%n", 99,-99);

    System.out.printf("最牛的編號是:%03d%n", 7);

    System.out.printf("Tab鍵的效果是:% 8d%n", 7);

    System.out.printf("整數分組的效果是:%,d%n", 9989997);

    System.out.printf("一本書的價格是:%2.2f元%n", 49.8);
}

輸出以下結果:
格式參數$的使用:99,abc

顯示正負數的符號:+99與-99

最牛的編號是:007

Tab鍵的效果是:       7

整數分組的效果是:9,989,997

一本書的價格是:49.80元




日期和時間字符串格式化


在程序界面中經常需要顯示時間和日期,但是其顯示的 格式經常不盡人意,需要編寫大量的代碼經過各種算法才得到理想的日期與時間格式。字符串格式中還有%tx轉換符沒有詳細介紹,它是專門用來格式化日期和時 間的。%tx轉換符中的x代表另外的處理日期和時間格式的轉換符,它們的組合能夠將日期和時間格式化成多種格式。


常見日期時間格式化

格式化日期與時間的轉換符定義了各種格式化日期字符串的方式,其中最常用的日期和時間的組合格式如下:
轉換符說明示例
c包括全部日期和時間信息星期六 十月 27 14:21:20 CST 2007
F“年-月-日”格式2007-10-27
D“月/日/年”格式10/27/07
r“HH:MM:SS PM”格式(12時制)02:25:51 下午
T“HH:MM:SS”格式(24時制)14:28:16
R“HH:MM”格式(24時制)14:28


使用轉換符格式化當前日期和時間:
public static void main(String[] args) {

    // 創建日期對像
    Date date=new Date();

    // 格式化輸出日期或時間
    System.out.printf("全部日期和時間信息:%tc%n",date);

    System.out.printf("年-月-日格式:%tF%n",date);

    System.out.printf("月/日/年格式:%tD%n",date);

    System.out.printf("HH:MM:SS PM格式(12時制):%tr%n",date);

    System.out.printf("HH:MM:SS格式(24時制):%tT%n",date);

    System.out.printf("HH:MM格式(24時制):%tR",date);

    System.out.printf("年-月-日 HH:MM:SS格式:%1$tF %1$tT%n",date);
}

輸出以下結果:
全部日期和時間信息:星期日十月28 13:53:24 CST 2007

年-月-日格式:2007-10-28

月/日/年格式:10/28/07

HH:MM:SS PM格式(12時制):01:53:24 下午

HH:MM:SS格式(24時制):13:53:24

HH:MM格式(24時制):13:53

年-月-日 HH:MM:SS格式:2007-10-28 13:53:24


格式化日期字符串

定義日期格式的轉換符可以使日期通過指定的轉換符生成新字符串:
轉換符說明示例
bh月份簡稱中:十月
英:Oct
B月份全稱中:十月
英:October
a星期的簡稱中:星期六
英:Sat
A星期的全稱中:星期六
英:Saturday
C年的前兩位數字(前補0至2位)20
y年的後兩位數字(前補0至2位)07
Y4位數字的年份(前補0至4位)2007
j一年中的天數(即年的第幾天)300
m兩位數字的月份(前補0至2位)10
d兩位數字的日(前補0至2位)27
e月份的日5


使用各種轉換符格式化當前系統的日期:
public static void main(String[] args) {

    // 創建日期對像
    Date date=new Date();

    // 格式化日期字符串
    String str=String.format(Locale.US,"英文月份簡稱:%tb",date);

    // 輸出字符串內容
    System.out.println(str);

    System.out.printf("本地月份簡稱:%tb%n",date);

    str=String.format(Locale.US,"英文月份全稱:%tB",date);

    System.out.println(str);

    System.out.printf("本地月份全稱:%tB%n",date);

    str=String.format(Locale.US,"英文星期的簡稱:%ta",date);

    System.out.println(str);

    System.out.printf("本地星期的簡稱:%tA%n",date);

    System.out.printf("年的前兩位數字(前補0至2位):%tC%n",date);

    System.out.printf("年的後兩位數字(前補0至2位):%ty%n",date);

    System.out.printf("一年中的天數(即年的第幾天):%tj%n",date);

    System.out.printf("兩位數字的月份(前補0至2位):%tm%n",date);

    System.out.printf("兩位數字的日(前補0至2位):%td%n",date);

    System.out.printf("月份的日:%te",date);
}

輸出以下結果:
英文月份簡稱:Oct

本地月份簡稱:十月

英文月份全稱:October

本地月份全稱:十月

英文星期的簡稱:Sun

本地星期的簡稱:星期日

年的前兩位數字(前補0至2位):20

年的後兩位數字(前補0至2位):07

一年中的天數(即年的第幾天):301

兩位數字的月份(前補0至2位):10

兩位數字的日(前補0至2位):28

月份的日:28


格式化時間字符串

和日期格式轉換符相比,時間格式的轉換符要更多、更精確。它可以將時間格式化成時、分、秒甚至時毫秒等單位:
轉換符說明示例
H2位數字24時制的小時(前補0至2位)15
I2位數字12時制的小時(前補0至2位)03
k2位數字24時制的小時15
l2位數字12時制的小時3
M2位數字的分鐘(前補0至2位)03
S2位數字的秒(前補0至2位)09
L3位數字的毫秒(前補0至3位)015
N9位數字的毫秒數(前補0至9位)562000000
p小寫字母的上午或下午標記中:下午
英:pm
z相對於GMT的RFC822時區的偏移量+0800
Z時區縮寫字符串CST
s1970-1-1 00:00:00 到現在所經過的秒數1193468128
Q1970-1-1 00:00:00 到現在所經過的毫秒數1193468128984


使用各種轉換符格式化當前系統的時間:
public static void main(String[] args) {

    // 創建日期對像
    Date date=new Date();

    System.out.printf("2位數字24時制的小時(前補0至2位):%tH%n",date);

    System.out.printf("2位數字12時制的小時(前補0至2位):%tI%n",date);

    System.out.printf("2位數字24時制的小時:%tk%n",date);

    System.out.printf("2位數字12時制的小時:%tl%n",date);

    System.out.printf("2位數字的分鐘(前補0至2位):%tM%n",date);

    System.out.printf("2位數字的秒(前補0至2位):%tS%n",date);

    System.out.printf("3位數字的毫秒(前補0至3位):%tL%n",date);

    System.out.printf("9位數字的毫秒數(前補0至9位):%tN%n",date);

    String str=String.format(
        Locale.US,"小寫字母的上午或下午標記(英):%tp",date
    );

    // 輸出字符串變量str的內容
    System.out.println(str);

    System.out.printf ("小寫字母的上午或下午標記(中):%tp%n",date);

    System.out.printf("相對於GMT的RFC822時區的偏移量:%tz%n",date);

    System.out.printf("時區縮寫字符串:%tZ%n",date);

    System.out.printf("1970-1-1 00:00:00 到現在所經過的秒數:%ts%n",date);

    System.out.printf("1970-1-1 00:00:00 到現在所經過的毫秒數:%tQ%n",date);
}

輸出以下結果:
2位數字24時制的小時(前補0至2位):15

2位數字12時制的小時(前補0至2位):03

2位數字24時制的小時:15

2位數字12時制的小時:3

2位數字的分鐘(前補0至2位):24

2位數字的秒(前補0至2位):56

3位數字的毫秒(前補0至3位):828

9位數字的毫秒數(前補0至9位):828000000

小寫字母的上午或下午標記(英):pm

小寫字母的上午或下午標記(中):下午

相對於GMT的RFC822時區的偏移量:+0800

時區縮寫字符串:CST

1970-1-1 00:00:00到現在所經過的秒數:1193556296

1970-1-1 00:00:00到現在所經過的毫秒數:1193556296828
2015-02-22 21:55

[轉載] Java Gossip: wait()、notify()

轉載自:Java Gossip: wait()、notify()

wait()notify()notifyAll() 是由 Object 所提供的方法,您在定義自己的類別時會繼承下來(記得 Java 中所有的物件最頂層都繼承自 Object),wait()notify() notifyAll() 都被宣告為 "final",所以您無法重新定義它們,透過這三個方法您可以控制執行緒是否為 Runnable 狀態。

您必須在同步化的方法或區塊中呼叫 wait() 方法,當物件的 wait() 方法被調用,目前的執行緒會被放入物件的「等待集合」(Wait set)中, 執行緒會釋放物件的鎖定,其它的執行緒可以競爭鎖定,取得鎖定的執行緒可以執行同步化區塊;被放在等待集中的執行緒將不參與執行緒的排班,wait() 可 以指定等待的時間,如果指定時間的話,則時間到之後執行緒會再度加入排班,如果指定時間 0 或不指定,則執行緒會持續等待,直到有被中斷(interrupt)或是被告知(notify)可以參與排班。

當物件的 notify() 被調用,它會從物件的等待集中選出「一個」執行緒加入排班,被選出的執行緒是隨機的,被選出的執行緒會與其它正在執行的執行緒共 同競爭對物件的鎖定;如果您呼叫 notifyAll(),則「所有」在等待集中的執行緒都會被喚醒,這些執行緒會與其它正在執行的執行緒共同競爭對物件的 鎖定。

簡單的說,當執行緒呼叫到物件的 wait() 方法時,表示它要先讓出物件的被同步區使用權並等待通知,或是等待一段指定的時間,直到被通知或時間到時再從 等待點開始執行,這就好比您要叫某人作事,作到一半時某人叫您等候通知(或等候1分鐘之類的),當您被通知(或時間到時)某人會繼續為您服務。

說明 wait()notify()notifyAll() 的應用最常見的一個例子,就是生產者(Producer)消費者(Consumer)的 例子,如果生產者會將產品交給店員,而消費者從店員處取走產品,店員一次只能持有固定數量產品,如果生產者生產了過多的產品,店員叫生產者等一下 (wait),如果店中有空位放產品了再通知(notify)生產者繼續生產,如果店中沒有產品了,店員會告訴消費者等一下(wait),如果店中有產品 了再通知(notify)消費者來取走產品。


以下舉一個最簡單的:生產者每次生產一個 int 整數交給在店員上,而消費者從店員處取走整數,店員一次只能持有一個整數。


以程式實例來看,首先是生產者:
Producer.java
package onlyfun.caterpillar;

public class Producer implements Runnable {
    private Clerk clerk;

    public Producer(Clerk clerk) {
        this.clerk = clerk;
    }

    public void run() {
        System.out.println("生產者開始生產整數......");

        // 生產1到10的整數
        for(int product = 1; product <= 10; product++) {
            try {
                // 暫停隨機時間
                Thread.sleep((int) (Math.random() * 3000));
            }
            catch(InterruptedException e) {
                e.printStackTrace();
            }

            // 將產品交給店員
            clerk.setProduct(product);
        }
    }
}


再來是消費者:
Consumer.java
package onlyfun.caterpillar;

public class Consumer implements Runnable {
    private Clerk clerk;

    public Consumer(Clerk clerk) {
        this.clerk = clerk;
    }

    public void run() {
        System.out.println("消費者開始消耗整數......");

        // 消耗10個整數
        for(int i = 1; i <= 10; i++) {
            try {
                // 等待隨機時間
                Thread.sleep((int) (Math.random() * 3000));
            }
            catch(InterruptedException e) {
                e.printStackTrace();
            }

            // 從店員處取走整數
            clerk.getProduct();
        }
    }
}


生產者將產品放至店員,而消費者從店員處取走產品,所以店員來決定誰必須等待並等候通知。
Clerk.java
package onlyfun.caterpillar;

public class Clerk {
    // -1 表示目前沒有產品
    private int product = -1;

    // 這個方法由生產者呼叫
    public synchronized void setProduct(int product) {
        while(this.product != -1) {
            try {
                // 目前店員沒有空間收產品,請稍候!
                wait();
            }
            catch(InterruptedException e) {
                e.printStackTrace();
            }
        }

        this.product = product;
        System.out.printf("生產者設定 (%d)%n", this.product);

        // 通知等待區中的一個消費者可以繼續工作了
        notify();
    }

    // 這個方法由消費者呼叫
    public synchronized int getProduct() {
        while(this.product == -1) {
            try {
                // 缺貨了,請稍候!
                wait();
            }
            catch(InterruptedException e) {
                e.printStackTrace();
            }
        }

        int p = this.product;
        System.out.printf(
                  "消費者取走 (%d)%n", this.product);
        this.product = -1;

        // 通知等待區中的一個生產者可以繼續工作了
        notify();

        return p;
    }
}


根據 規格書中所說明 ,執行緒也有可能在未經 notify()interrupt() 或逾時的情況下自動甦醒(spurious wakeup),雖然這種情況實務上很少發生,但應用程式應考量這種情況,你必須持續檢測這種情況,因而 wait() 必須總是在迴圈中執行,例如:
synchronized (obj) {
    while (執行條件不成立時)
        obj.wait(timeout);
    ... // 執行一些動作進行判斷
}


使用這麼一個程式來測試:
WaitNotifyDemo.java
package onlyfun.caterpillar;

public class WaitNotifyDemo {
    public static void main(String[] args) {
        Clerk clerk = new Clerk();

        Thread producerThread = new Thread(
            new Producer(clerk)
        );
        Thread consumerThread = new Thread(
            new Consumer(clerk)
        );

        producerThread.start();
        consumerThread.start();
    }
}


執行結果:
生產者開始生產整數......
消費者開始消耗整數......
生產者設定 (1)
消費者取走 (1)
生產者設定 (2)
消費者取走 (2)
生產者設定 (3)
消費者取走 (3)
生產者設定 (4)
消費者取走 (4)
生產者設定 (5)
消費者取走 (5)
生產者設定 (6)
消費者取走 (6)
生產者設定 (7)
消費者取走 (7)
生產者設定 (8)
消費者取走 (8)
生產者設定 (9)
消費者取走 (9)
生產者設定 (10)
消費者取走 (10)


生產者會生產10個整數,而消費者會消耗10個整數,由於店員處只能放置一個整數,所以每生產一個就消耗一個,其結果如上所示是無誤的。

如果一個執行緒進入物件的等待集中,您可以中斷它的等待,這時將會發生InterruptedException例外物件,interrupt()方法可用來進行這項工作。
2015-02-22 21:32

[轉載] Java 類靜態域,非靜態域,構造函數的初始化順序

轉載自:java类静态域、块,非静态域、块,构造函数的初始化顺序 - 艾妮 - ITeye技术网站

面試的時候,經常會遇到這樣的考題:給你兩個類的程式,它們之間是繼承的關系,每個類裡只有建構子方法和一些變數,建構子裡可能還有一段程式對變數值進行了某種運算,另外還有一些將變數值輸出到控制台的程式,然後讓我們判斷輸出的
結果。這實際上是在考查我們對於繼承情況下類的初始化順序的了解。

我們大家都知道,對於靜態變數、靜態初始化、變數、初始化、建構子,它們的初始化順序以此是:
(靜態變數、靜態初始化) > (變數、初始化) > 建構子

我們也可以通過下面的測試程式來驗證這一點:
public class InitialOrderTest {
    // 靜態變數
    public static String staticField = "靜態變數";

    // 變數
    public String field = "變數";

    // 靜態初始化
    static {
        System.out.println(staticField);
        System.out.println("靜態初始化");
    }

    // 初始化
    {
        System.out.println(field);
        System.out.println("初始化");
    }

    // 建構子
    public InitialOrderTest() {
        System.out.println("建構子");
    }

    public static void main(String[] args) {
        new InitialOrderTest();
    }
}

運行以上程式,我們會得到如下的輸出結果:
靜態變數
靜態初始化
變數
初始化
建構子

這與上文中說的完全符合。



那麼對於繼承情況下又會怎樣呢?我們仍然以一段測試程式來獲取最終結果:
class Parent {
    // 靜態變數
    public static String p_StaticField = "父--靜態變數";

    // 變數
    public String p_Field = "父--變數";

    // 靜態初始化
    static {
        System.out.println(p_StaticField);
        System.out.println("父--靜態初始化");
    }

    // 初始化
    {
        System.out.println(p_Field);
        System.out.println("父--初始化");
    }
    // 建構子
    public Parent() {
        System.out.println("父--建構子");
    }
}


public class SubClass extends Parent {

    // 靜態變數
    public static String s_StaticField = "子--靜態變數";

    // 變數
    public String s_Field = "子--變數";

    // 靜態初始化
    static {
        System.out.println(s_StaticField);
        System.out.println("子--靜態初始化");
    }

    // 初始化
    {
        System.out.println(s_Field);
        System.out.println("子--初始化");
    }

    // 建構子
    public SubClass() {
        System.out.println("子--建構子");
    }


    public static void main(String[] args) {
        new SubClass();
    }
}

運行一下上面的程式,結果馬上呈現在我們的眼前:
父--靜態變數
父--靜態初始化
子--靜態變數
子--靜態初始化
父--變數
父--初始化
父--建構子
子--變數
子--初始化
子--建構子

現在,結果已經不言自明了。大家可能會注意到一點,那就是,並不是父類完全初始化完畢後才進行子類的初始化,實際上子類的靜態變數和靜態初始化的初始化是在父類的變數、初始化和建構子初始化之前就完成了。那麼對於靜態變數和靜態初始化之間、變數和初始化之間的先後順序又是怎樣呢?是否靜態變數總是先於靜態初始化,變數總是先於初始化就被初始化了呢?實際上這取決於它們在類中出現的先後順序。



我們以靜態變數和靜態初始化為例來進行說明。 同樣,我們還是寫一個類來進行測試:
public class TestOrder {
    // 靜態變數
    public static TestA a = new TestA();

    // 靜態初始化
    static {
        System.out.println("靜態初始化");
    }

    // 靜態變數
    public static TestB b = new TestB();

    public static void main(String[] args) {
        new TestOrder();
    }
}

class TestA {
    public TestA() {
        System.out.println("TestA");
    }
}

class TestB {
    public TestB() {
        System.out.println("TestB");
    }
}

運行上面的程式,會得到如下的結果:
TestA
靜態初始化
TestB

大家可以隨意改變變數 a、變數 b 以及靜態初始化的前後位置,就會發現輸出結果隨著它們在類中出現的前後順序而改變,這就說明靜態變數和靜態初始化是依照他們在類中的定義順序進行初始化的。同樣,變數和初始化也遵循這個規律。了解了繼承情況下類的初始化順序之後,如何判斷最終輸出結果就迎刃而解了。



測試函數:
public class TestStaticCon {
    public static int a = 0;

    static {
        a = 10;
        System.out.println("靜態初始化 a = " + a);
    }

    {
        a = 8;
        System.out.println("初始化 a = " + a);
    }

    public TestStaticCon() {
        this(a);
        System.out.println("無參數建構子 a = " + a);
    }
    public TestStaticCon(int n) {
        System.out.println("參數建構子 n = " + n);
        System.out.println("參數建構子 a = " + a);
    }


    public static void main(String[] args) {
        new TestStaticCon();
    }
}

運行結果:
靜態初始化 a = 10
初始化 a = 8
參數建構子 n = 10
參數建構子 a = 8
無參數建構子 a = 8


結論:
靜態初始化是在類加載時自動執行的,非靜態初始化是在創建對像時自動執行的程式,不創建對像不執行該類的非靜態初始化。且執行順序為
靜態初始化 -> 非靜態初始化 -> 建構子


擴展( 靜態初始化 與 靜態方法 ):
一般情況下,如果有些程式必須在項目啟動的時候就執行的時候,需要使用靜態初始化,這種程式是主動執行的。
需要在項目啟動的時候就初始化,在不創建對像的情況下,其他程序來調用的時候,需要使用靜態方法,這種程式是被動執行的。
兩者的區別就是:靜態初始化是自動執行的。靜態方法是被調用的時候才執行的。


作用:
靜態初始化可用來初始化一些項目最常用的變數或類別靜態方法可用作不創建物件也可能需要執行的程式。



阿裡筆試題:
public class Test1 {

    public static int k = 0;
    public static Test1 t1 = new Test1("obj t1");
    public static Test1 t2 = new Test1("obj t2");
    public static int i = print("var I");
    public static int n = 99;
    public int j = print("var J");

    static{
        print("static");
    }

    {
        print("init");
    }

    public Test1(String str){
        print(str);
        ++i;
        ++n;
    }

    public static int print(String str){
        k++;
        System.out.printf("%-3s: %-8s i=%-4d n=%d", k, str, i, n);
        ++n;
        return ++i;
    }

    public static void main(String[] args) {
        new Test1("main");
    }
}

運行結果:
1  : var J    i=0    n=0
2  : init     i=1    n=1
3  : obj t1   i=2    n=2
4  : var J    i=4    n=4
5  : init     i=5    n=5
6  : obj t2   i=6    n=6
7  : var I    i=8    n=8
8  : static   i=9    n=99
9  : var J    i=10   n=100
10 : init     i=11   n=101
11 : main     i=12   n=102
2015-02-21 23:37

[轉載] Java中常用的加密方法

轉載自:Java中常用的加密方法(JDK) - Java - language - ITeye论坛

加密,是以某種特殊的演算法改變原有的信息數據,使得未授權的用戶即使獲得了已加密的信息,但因不知解密的方法,仍然無法了解信息的內容。大體上分為雙向加密和單向加密,而雙向加密又分為對稱加密和非對稱加密(有些資料將加密直接分為對稱加密和非對稱加密)。

雙向加密大體意思就是明文加密後形成密文,可以通過演算法還原成明文。而單向加密只是對信息進行了摘要計算,不能通過演算法生成明文,單向加密從嚴格意思上說不能算是加密的一種,應該算是摘要演算法吧。具體區分可以參考:
(本人解釋不清呢 …… )
http://security.group.iteye.com/group/wiki/1710-one-way-encryption-algorithm


一、雙向加密


(一)、對稱加密


采用單鑰密碼系統的加密方法,同一個密鑰可以同時用作信息的加密和解密,這種加密方法稱為對稱加密,也稱為單密鑰加密。
需要對加密和解密使用相同密鑰的加密演算法。由於其速度,對稱性加密通常在消息發送方需要加密大量數據時使用。對稱性加密也稱為密鑰加密。
所謂對稱,就是采用這種加密方法的雙方使用方式用同樣的密鑰進行加密和解密。密鑰是控制加密及解密過程的指令。

演算法是一組規則,規定如何進行加密和解密。因此對稱式加密本身不是安全的。   
常用的對稱加密有:DES、IDEA、RC2、RC4、SKIPJACK、RC5、AES 演算法等

對稱加密一般 Java 類中中定義成員
// KeyGenerator 提供對稱密鑰生成器的功能,支持各種演算法
private KeyGenerator keygen;
// SecretKey 負責保存對稱密鑰
private SecretKey deskey;
// Cipher負責完成加密或解密工作
private Cipher c;
// 該字節數組負責保存加密的結果
private byte[] cipherByte;


在構造函數中初始化
Security.addProvider(new com.sun.crypto.provider.SunJCE());
// 實例化支持 DES 演算法的密鑰生成器(演算法名稱命名需按規定,否則拋出異常)
keygen = KeyGenerator.getInstance("DES");//
// 生成密鑰
deskey = keygen.generateKey();
// 生成 Cipher 物件,指定其支持的 DES 演算法
c = Cipher.getInstance("DES");


1. DES
演算法為密碼體制中的對稱密碼體制,又被成為美國數據加密標准,是 1972 年美國 IBM 公司研制的對稱密碼體制加密演算法。 明文按 64 位進行分組, 密鑰長 64 位,密鑰事實上是 56 位參與 DES 運算(第 8、16、24、32、40、48、56、64 位是校驗位, 使得每個密鑰都有奇數個 1)分組後的明文組和 56 位的密鑰按位替代或交換的方法形成密文組的加密方法。

import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.Security;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;

public class EncrypDES {

    // KeyGenerator 提供對稱密鑰生成器的功能,支持各種演算法
    private KeyGenerator keygen;
    // SecretKey 負責保存對稱密鑰
    private SecretKey deskey;
    // Cipher 負責完成加密或解密工作
    private Cipher c;
    // 該字節數組負責保存加密的結果
    private byte[] cipherByte;

    public EncrypDES()
        throws NoSuchAlgorithmException, NoSuchPaddingException
    {
        Security.addProvider(new com.sun.crypto.provider.SunJCE());
        // 實例化支持 DES 演算法的密鑰生成器(演算法名稱命名需按規定,否則拋出異常)
        keygen = KeyGenerator.getInstance("DES");
        // 生成密鑰
        deskey = keygen.generateKey();
        // 生成 Cipher 物件,指定其支持的 DES 演算法
        c = Cipher.getInstance("DES");
    }

    /**
     * 對字符串加密
     *
     * @param str
     * @return
     * @throws InvalidKeyException
     * @throws IllegalBlockSizeException
     * @throws BadPaddingException
     */
    public byte[] Encrytor(String str)
        throws InvalidKeyException, IllegalBlockSizeException,
               BadPaddingException
    {
        // 根據密鑰,對 Cipher 物件進行初始化,ENCRYPT_MODE 表示加密模式
        c.init(Cipher.ENCRYPT_MODE, deskey);
        byte[] src = str.getBytes();
        // 加密,結果保存進 cipherByte
        cipherByte = c.doFinal(src);
        return cipherByte;
    }

    /**
     * 對字符串解密
     *
     * @param buff
     * @return
     * @throws InvalidKeyException
     * @throws IllegalBlockSizeException
     * @throws BadPaddingException
     */
    public byte[] Decryptor(byte[] buff)
        throws InvalidKeyException, IllegalBlockSizeException,
               BadPaddingException
    {
        // 根據密鑰,對 Cipher 物件進行初始化,DECRYPT_MODE 表示加密模式
        c.init(Cipher.DECRYPT_MODE, deskey);
        cipherByte = c.doFinal(buff);
        return cipherByte;
    }

    /**
     * @param args
     * @throws NoSuchPaddingException
     * @throws NoSuchAlgorithmException
     * @throws BadPaddingException
     * @throws IllegalBlockSizeException
     * @throws InvalidKeyException
     */
    public static void main(String[] args) throws Exception {
        EncrypDES de1 = new EncrypDES();
        String msg ="郭XX-搞笑相聲全集";
        byte[] encontent = de1.Encrytor(msg);
        byte[] decontent = de1.Decryptor(encontent);
        System.out.println("明文是:" + msg);
        System.out.println("加密後:" + new String(encontent));
        System.out.println("解密後:" + new String(decontent));
    }
}


2. 3DES
又稱 Triple DES,是 DES 加密演算法的一種模式,它使用 3 條 56 位的密鑰對 3DES
數據進行三次加密。數據加密標准(DES)是美國的一種由來已久的加密標准,它使用對稱密鑰加密法,並於 1981 年被 ANSI 組織規範為 ANSI X.3.92。DES 使用 56 位密鑰和密碼塊的方法,而在密碼塊的方法中,文本被分成 64 位大小的文本塊然後再進行加密。比起最初的 DES,3DES 更為安全。   
3DES(即Triple DES)是 DES 向 AES 過渡的加密演算法(1999年,NIST 將 3-DES 指定為過渡的加密標准),是 DES 的一個更安全的變形。它以 DES 為基本模塊,通過組合分組方法設計出分組加密演算法,其具體實現如下:
設 Ek() 和 Dk() 代表 DES 演算法的加密和解密過程,K 代表 DES 演算法使用的密鑰,P 代表明文,C 代表密文,

3DES 加密過程為:C=Ek3(Dk2(Ek1(P)))
3DES 解密過程為:P=Dk1((EK2(Dk3(C)))

import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.Security;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;

public class EncrypDES3 {

    // KeyGenerator 提供對稱密鑰生成器的功能,支持各種演算法
    private KeyGenerator keygen;
    // SecretKey 負責保存對稱密鑰
    private SecretKey deskey;
    // Cipher 負責完成加密或解密工作
    private Cipher c;
    // 該字節數組負責保存加密的結果
    private byte[] cipherByte;

    public EncrypDES3()
        throws NoSuchAlgorithmException, NoSuchPaddingException
    {
        Security.addProvider(new com.sun.crypto.provider.SunJCE());
        // 實例化支持 DESede 演算法的密鑰生成器(演算法名稱命名需按規定,否則拋出異常)
        keygen = KeyGenerator.getInstance("DESede");
        // 生成密鑰
        deskey = keygen.generateKey();
        // 生成 Cipher 物件,指定其支持的 DESede 演算法
        c = Cipher.getInstance("DESede");
    }

    /**
     * 對字符串加密
     *
     * @param str
     * @return
     * @throws InvalidKeyException
     * @throws IllegalBlockSizeException
     * @throws BadPaddingException
     */
    public byte[] Encrytor(String str)
        throws InvalidKeyException, IllegalBlockSizeException,
               BadPaddingException
    {
        // 根據密鑰,對 Cipher 物件進行初始化,ENCRYPT_MODE 表示加密模式
        c.init(Cipher.ENCRYPT_MODE, deskey);
        byte[] src = str.getBytes();
        // 加密,結果保存進 cipherByte
        cipherByte = c.doFinal(src);
        return cipherByte;
    }

    /**
     * 對字符串解密
     *
     * @param buff
     * @return
     * @throws InvalidKeyException
     * @throws IllegalBlockSizeException
     * @throws BadPaddingException
     */
    public byte[] Decryptor(byte[] buff)
        throws InvalidKeyException, IllegalBlockSizeException,
               BadPaddingException
    {
        // 根據密鑰,對 Cipher 物件進行初始化,DECRYPT_MODE 表示加密模式
        c.init(Cipher.DECRYPT_MODE, deskey);
        cipherByte = c.doFinal(buff);
        return cipherByte;
    }

    /**
     * @param args
     * @throws NoSuchPaddingException
     * @throws NoSuchAlgorithmException
     * @throws BadPaddingException
     * @throws IllegalBlockSizeException
     * @throws InvalidKeyException
     */
    public static void main(String[] args) throws Exception {
        EncrypDES3 des = new EncrypDES3();
        String msg ="郭XX-搞笑相聲全集";
        byte[] encontent = des.Encrytor(msg);
        byte[] decontent = des.Decryptor(encontent);
        System.out.println("明文是:" + msg);
        System.out.println("加密後:" + new String(encontent));
        System.out.println("解密後:" + new String(decontent));
    }
}


3. AES
密碼學中的高級加密標准(Advanced Encryption Standard,AES),又稱 高級加密標准
Rijndael 加密法,是美國聯邦政府采用的一種區塊加密標准。這個標准用來替代原先的 DES,已經被多方分析且廣為全世界所使用。經過五年的甄選流程,高級加密標准由美國國家標准與技術研究院(NIST)於 2001 年 11 月 26 日發布於 FIPS PUB 197,並在 2002 年 5 月 26 日成為有效的標准。2006 年高級加密標准已然成為對稱密鑰加密中最流行的演算法之一。
該演算法為比利時密碼學家 Joan Daemen 和 Vincent Rijmen 所設計,結合兩位作者的名字,以 Rijndael 之命名之,投稿高級加密標准的甄選流程。(Rijdael 的發音近於 "Rhinedoll")

import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.Security;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;

public class EncrypAES {

    // KeyGenerator 提供對稱密鑰生成器的功能,支持各種演算法
    private KeyGenerator keygen;
    // SecretKey 負責保存對稱密鑰
    private SecretKey deskey;
    // Cipher 負責完成加密或解密工作
    private Cipher c;
    //該字節數組負責保存加密的結果
    private byte[] cipherByte;

    public EncrypAES()
        throws NoSuchAlgorithmException, NoSuchPaddingException
    {
        Security.addProvider(new com.sun.crypto.provider.SunJCE());
        // 實例化支持 AES 演算法的密鑰生成器(演算法名稱命名需按規定,否則拋出異常)
        keygen = KeyGenerator.getInstance("AES");
        //生成密鑰
        deskey = keygen.generateKey();
        //生成 Cipher 物件,指定其支持的 AES 演算法
        c = Cipher.getInstance("AES");
    }

    /**
     * 對字符串加密
     *
     * @param str
     * @return
     * @throws InvalidKeyException
     * @throws IllegalBlockSizeException
     * @throws BadPaddingException
     */
    public byte[] Encrytor(String str)
        throws InvalidKeyException, IllegalBlockSizeException,
               BadPaddingException
    {
        // 根據密鑰,對 Cipher 物件進行初始化,ENCRYPT_MODE 表示加密模式
        c.init(Cipher.ENCRYPT_MODE, deskey);
        byte[] src = str.getBytes();
        // 加密,結果保存進 cipherByte
        cipherByte = c.doFinal(src);
        return cipherByte;
    }

    /**
     * 對字符串解密
     *
     * @param buff
     * @return
     * @throws InvalidKeyException
     * @throws IllegalBlockSizeException
     * @throws BadPaddingException
     */
    public byte[] Decryptor(byte[] buff)
        throws InvalidKeyException, IllegalBlockSizeException,
               BadPaddingException
    {
        // 根據密鑰,對 Cipher 物件進行初始化,DECRYPT_MODE 表示加密模式
        c.init(Cipher.DECRYPT_MODE, deskey);
        cipherByte = c.doFinal(buff);
        return cipherByte;
    }

    /**
     * @param args
     * @throws NoSuchPaddingException
     * @throws NoSuchAlgorithmException
     * @throws BadPaddingException
     * @throws IllegalBlockSizeException
     * @throws InvalidKeyException
     */
    public static void main(String[] args) throws Exception {
        EncrypAES de1 = new EncrypAES();
        String msg ="郭XX-搞笑相聲全集";
        byte[] encontent = de1.Encrytor(msg);
        byte[] decontent = de1.Decryptor(encontent);
        System.out.println("明文是:" + msg);
        System.out.println("加密後:" + new String(encontent));
        System.out.println("解密後:" + new String(decontent));
    }
}


(二)、非對稱加密


1976 年,美國學者 Dime 和 Henman 為解決信息公開傳送和密鑰管理問題,提出一種新的密鑰交換協議,允許在不安全的媒體上的通訊雙方交換信息,安全地達成一致的密鑰,這就是“公開密鑰系統”。相對於“對稱加密演算法”這種方法也叫做“非對稱加密演算法”。 與對稱加密演算法不同,非對稱加密演算法需要兩個密鑰:公開密鑰(publickey)和私有密鑰(privatekey)。公開密鑰與私有密鑰是一對,如果用公開密鑰對數據進行加密,只有用對應的私有密鑰才能解密;如果用私有密鑰對數據進行加密,那麼只有用對應的公開密鑰才能解密。因為加密和解密使用的是兩個不同的密鑰,所以這種演算法叫作非對稱加密演算法。

1. RSA
公鑰加密演算法是 1977 年由 Ron Rivest、Adi Shamirh 和 LenAdleman 在(美國麻省理工學院)開發的。RSA 取名來自開發他們三者的名字。RSA 是目前最有影響力的公鑰加密演算法,它能夠抵抗到目前為止已知的所有密碼攻擊,已被 ISO 推薦為公鑰數據加密標准。RSA 演算法基於一個十分簡單的數論事實:將兩個大素數相乘十分容易,但那時想要對其乘積進行因式分解卻極其困難,因此可以將乘積公開作為加密密鑰。

import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;

public class EncrypRSA {

    /**
     * 加密
     * @param publicKey
     * @param srcBytes
     * @return
     * @throws NoSuchAlgorithmException
     * @throws NoSuchPaddingException
     * @throws InvalidKeyException
     * @throws IllegalBlockSizeException
     * @throws BadPaddingException
     */
    protected byte[] encrypt(RSAPublicKey publicKey,byte[] srcBytes)
        throws NoSuchAlgorithmException, NoSuchPaddingException,
               InvalidKeyException, IllegalBlockSizeException,
               BadPaddingException
    {
        if(publicKey == null){ return null; }

        // Cipher 負責完成加密或解密工作,基於 RSA
        Cipher cipher = Cipher.getInstance("RSA");
        // 根據公鑰,對 Cipher 物件進行初始化
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);
        byte[] resultBytes = cipher.doFinal(srcBytes);
        return resultBytes;
    }

    /**
     * 解密
     * @param privateKey
     * @param srcBytes
     * @return
     * @throws NoSuchAlgorithmException
     * @throws NoSuchPaddingException
     * @throws InvalidKeyException
     * @throws IllegalBlockSizeException
     * @throws BadPaddingException
     */
    protected byte[] decrypt(RSAPrivateKey privateKey,byte[] srcBytes)
        throws NoSuchAlgorithmException, NoSuchPaddingException,
               InvalidKeyException, IllegalBlockSizeException,
               BadPaddingException
    {
        if(privateKey == null){ return null; }

        // Cipher負責完成加密或解密工作,基於RSA
        Cipher cipher = Cipher.getInstance("RSA");
        // 根據公鑰,對 Cipher 物件進行初始化
        cipher.init(Cipher.DECRYPT_MODE, privateKey);
        byte[] resultBytes = cipher.doFinal(srcBytes);
        return resultBytes;
    }

    /**
     * @param args
     * @throws NoSuchAlgorithmException
     * @throws BadPaddingException
     * @throws IllegalBlockSizeException
     * @throws NoSuchPaddingException
     * @throws InvalidKeyException
     */
    public static void main(String[] args)
        throws NoSuchAlgorithmException, InvalidKeyException,
               NoSuchPaddingException, IllegalBlockSizeException,
               BadPaddingException
    {
        EncrypRSA rsa = new EncrypRSA();
        String msg = "郭XX-精品相聲";

        // KeyPairGenerator 類用於生成公鑰和私鑰對,基於 RSA 演算法生成物件
        KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");
        // 初始化密鑰對生成器,密鑰大小為 1024 位
        keyPairGen.initialize(1024);
        // 生成一個密鑰對,保存在 keyPair 中
        KeyPair keyPair = keyPairGen.generateKeyPair();
        // 得到私鑰
        RSAPrivateKey privateKey = (RSAPrivateKey)keyPair.getPrivate();
        // 得到公鑰
        RSAPublicKey publicKey = (RSAPublicKey)keyPair.getPublic();

        // 用公鑰加密
        byte[] srcBytes = msg.getBytes();
        byte[] resultBytes = rsa.encrypt(publicKey, srcBytes);

        // 用私鑰解密
        byte[] decBytes = rsa.decrypt(privateKey, resultBytes);

        System.out.println("明文是:" + msg);
        System.out.println("加密後是:" + new String(resultBytes));
        System.out.println("解密後是:" + new String(decBytes));
    }

}


2. DSA
Digital Signature Algorithm (DSA)是 Schnorr 和 ElGamal 簽名演算法的變種,被美國 NIST 作為 DSS(DigitalSignature Standard)。(感覺有點復雜,沒有附代碼)
詳見http://63938525.iteye.com/blog/1051565



(三)、題外話 MySQL加密解密函數


MySQL有兩個函數來支持這種類型的加密,分別叫做 ENCODE() 和 DECODE()。
下面是一個簡單的實例:
mysql> INSERT INTO users (username,password) VALUES ('joe',ENCODE('guessme','abr'));

Query OK, 1 row affected (0.14 sec)

其中,Joe 的密碼是 guessme,它通過密鑰 abracadabra 被加密。要注意的是,加密完的結果是一個二進制字符串,如下所示:

提示:雖然 ENCODE() 和DECODE() 這兩個函數能夠滿足大多數的要求,但是有的時候您希望使用強度更高的加密手段。在這種情況下,您可以使用 AES_ENCRYPT() 和 AES_DECRYPT() 函數,它們的工作方式是相同的,但是加密強度更高。

單向加密與雙向加密不同,一旦數據被加密就沒有辦法顛倒這一過程。因此密碼的驗證包括對用戶輸入內容的重新加密,並將它與保存的密文進行比對,看是否匹配。一種簡單的單向加密方式是MD5校驗碼。MySQL 的 MD5() 函數會為您的數據創建一個“指紋”並將它保存起來,供驗證測試使用。下面就是如何使用它的一個簡單例子:
mysql> INSERT INTO users (username,password) VALUES ('joe',MD5('guessme'));

Query OK, 1 row affected (0.00 sec)



或者,您考慮一下使用 ENCRYPT() 函數,它使用系統底層的 crypt() 系統調用來完成加密。這個函數有兩個參數:一個是要被加密的字符串,另一個是雙(或者多)字符的“salt”。它然後會用 salt 加密字符串;這個 salt 然後可以被用來再次加密用戶輸入的內容,並將它與先前加密的字符串進行比對。下面一個例子說明了如何使用它:
mysql> INSERT INTO users (username,password) VALUES('joe', ENCRYPT('guessme','ab'));

Query OK, 1 row affected (0.00 sec)

PS:ENCRYPT() 只能用在 UNIX、LINIX 系統上,因為它需要用到底層的 crypt() 庫。




二、單向加密(信息摘要)


Java 一般需要獲取物件 MessageDigest 來實現單項加密(信息摘要)。

1. MD5
即 Message-Digest Algorithm 5(信息-摘要演算法 5),用於確保信息傳輸完整一致。是計算機廣泛使用的雜湊演算法之一(又譯摘要演算法、哈希演算法),主流編程語言普遍已有MD5實現。將數據(如漢字)運算為另一固定長度值,是雜湊演算法的基礎原理,MD5 的前身有 MD2、MD3 和 MD4。MD5 的作用是讓大容量信息在用數字簽名軟件簽署私人密鑰前被"壓縮"成一種保密的格式(就是把一個任意長度的字節串變換成一定長的十六進制數字串)。
除了 MD5 以外,其中比較有名的還有 sha-1、RIPEMD 以及 Haval 等

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class EncrypMD5 {

    public byte[] eccrypt(String info) throws
        NoSuchAlgorithmException
    {
        // 根據 MD5 演算法生成 MessageDigest 物件
        MessageDigest md5 = MessageDigest.getInstance("MD5");
        byte[] srcBytes = info.getBytes();
        // 使用 srcBytes 更新摘要
        md5.update(srcBytes);
        // 完成哈希計算,得到 result
        byte[] resultBytes = md5.digest();
        return resultBytes;
    }

    public static void main(String args[]) throws
        NoSuchAlgorithmException
    {
        String msg = "郭XX-精品相聲技術";
        EncrypMD5 md5 = new EncrypMD5();
        byte[] resultBytes = md5.eccrypt(msg);

        System.out.println("密文是:" + new String(resultBytes));
        System.out.println("明文是:" + msg);
    }
}


2. SHA
是一種數據加密演算法,該演算法經過加密專家多年來的發展和改進已日益完善,現在已成為公認的最安全的散列演算法之一,並被廣泛使用。該演算法的思想是接收一段明文,然後以一種不可逆的方式將它轉換成一段(通常更小)密文,也可以簡單的理解為取一串輸入碼(稱為預映射或信息),並把它們轉化為長度較短、位數固定的輸出序列即散列值(也稱為信息摘要或信息認證代碼)的過程。散列函數值可以說時對明文的一種“指紋”或是“摘要”所以對散列值的數字簽名就可以視為對此明文的數字簽名。

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class EncrypSHA {

    public byte[] eccrypt(String info) throws
        NoSuchAlgorithmException
    {
        MessageDigest sha = MessageDigest.getInstance("SHA");
        byte[] srcBytes = info.getBytes();
        // 使用 srcBytes 更新摘要
        sha.update(srcBytes);
        // 完成哈希計算,得到 result
        byte[] resultBytes = sha.digest();
        return resultBytes;
    }

    /**
     * @param args
     * @throws NoSuchAlgorithmException
     */
    public static void main(String[] args) throws
        NoSuchAlgorithmException
    {
        String msg = "郭XX-精品相聲技術";
        EncrypSHA sha = new EncrypSHA();
        byte[] resultBytes = sha.eccrypt(msg);
        System.out.println("明文是:" + msg);
        System.out.println("密文是:" + new String(resultBytes));

    }
}


附件中是以上幾種的源代碼,附帶額外的兩種使用方式。

增加一種關於文件的哈希演算法源代碼:

import java.io.FileInputStream;
import java.io.InputStream;
import java.security.MessageDigest;

public class FileHashUtil {

    public static final char[] hexChar = {
        '0', '1', '2', '3', '4', '5', '6', '7',
        '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
    };
    public static final String[] hashTypes = new String[] {
        "MD2", "MD5", "SHA1", "SHA-256", "SHA-384", "SHA-512"
    };

    public void MD5File(String fileName) throws Exception{
        //String fileName = args[0];
        System.out.println("需要獲取 hash 的文件為: " + fileName);

        java.util.List<MessageDigest> mds = new java.util.ArrayList<>();
        for (String hashType : hashTypes) {
            MessageDigest md = MessageDigest.getInstance(hashType);
            mds.add(md);
        }

        InputStream fis = null;
        try {
            fis = new FileInputStream(fileName);
            byte[] buffer = new byte[1024];
            int numRead = 0;
            while ((numRead = fis.read(buffer)) > 0) {
                for (MessageDigest md : mds) {
                    md.update(buffer, 0, numRead);
                }
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        } finally {
            if (fis != null) { fis.close(); }
        }

        for (MessageDigest md : mds) {
            System.out.println(
                md.getAlgorithm() +
                " == " +
                toHexString(md.digest())
            );
        }
    }


    public static void main(String[] args) throws Exception {
        String[] fileName = new String[] {
            "D:/hapfish/ShellFolder.java",
            "D:/hapfish/ShellFolder - 副本.java",
            "E:/ShellFolder - 副本.java",
            "E:/ShellFolder.txt",
            "D:/hapfish/ShellFolder.jpg",
            "E:/ShellFolder增加字符.txt",
            "D:/hapfish/birosoft.jar"
        };

        FileHashUtil files  = new FileHashUtil();
        for(int i=0;i<fileName.length;i++){
            files.MD5File(fileName[i]);
        }
    }

    public static String toHexString(byte[] b) {
        StringBuilder sb = new StringBuilder(b.length * 2);
        for (int i = 0; i < b.length; i++) {
            sb.append(hexChar[(b[i] & 0xf0) >>> 4]);
            sb.append(hexChar[b[i] & 0x0f]);
        }
        return sb.toString();
    }
}


運行說明
"D:/hapfish/ShellFolder.java",
"D:/hapfish/ShellFolder - 副本.java",
"E:/ShellFolder - 副本.java",
"E:/ShellFolder.txt",
"D:/hapfish/ShellFolder.jpg",
以上五個文件是同一文件經過復制、改擴展名的,最後計算哈希結果是一致的。

"E:/ShellFolder增加字符.txt" 增加了幾個字符串,就不一樣了

"D:/hapfish/birosoft.jar" 完全不相關的另外一個文件


運行結果:
需要獲取 hash 的文件為: D:/hapfish/ShellFolder.java
  MD2 == 3a755a99c5e407005cd45ebd856b4649
  MD5 == 5d08d440fa911d1e418c69a90b83cd86
  SHA1 == 522c8c4f4ff1dd669e251c2ab854c3033a51ca63
  SHA-256 == d1feb0c73c10a759e88bd240cb9d56d0598b4ff83a0704c6679f7ba12f6c4d99
  SHA-384 == 8f8c9da4cd7241c58af3c52b49199033f2dcf3d67f421753999f87511618d9ea2d738e8c16b9b68a7572d06108ff10f6
  SHA-512 == 4711579daee3ddacbaea189310348956cb43bcaaf0099f3be047b06f16c1a20a6b71ee3a4ee018128d647e9f2ef0d644747672238e49a8da3d0cd26dfe597458
需要獲取 hash 的文件為: D:/hapfish/ShellFolder - 副本.java
  MD2 == 3a755a99c5e407005cd45ebd856b4649
  MD5 == 5d08d440fa911d1e418c69a90b83cd86
  SHA1 == 522c8c4f4ff1dd669e251c2ab854c3033a51ca63
  SHA-256 == d1feb0c73c10a759e88bd240cb9d56d0598b4ff83a0704c6679f7ba12f6c4d99
  SHA-384 == 8f8c9da4cd7241c58af3c52b49199033f2dcf3d67f421753999f87511618d9ea2d738e8c16b9b68a7572d06108ff10f6
  SHA-512 == 4711579daee3ddacbaea189310348956cb43bcaaf0099f3be047b06f16c1a20a6b71ee3a4ee018128d647e9f2ef0d644747672238e49a8da3d0cd26dfe597458
需要獲取 hash 的文件為: E:/ShellFolder - 副本.java
  MD2 == 3a755a99c5e407005cd45ebd856b4649
  MD5 == 5d08d440fa911d1e418c69a90b83cd86
  SHA1 == 522c8c4f4ff1dd669e251c2ab854c3033a51ca63
  SHA-256 == d1feb0c73c10a759e88bd240cb9d56d0598b4ff83a0704c6679f7ba12f6c4d99
  SHA-384 == 8f8c9da4cd7241c58af3c52b49199033f2dcf3d67f421753999f87511618d9ea2d738e8c16b9b68a7572d06108ff10f6
  SHA-512 == 4711579daee3ddacbaea189310348956cb43bcaaf0099f3be047b06f16c1a20a6b71ee3a4ee018128d647e9f2ef0d644747672238e49a8da3d0cd26dfe597458
需要獲取 hash 的文件為: E:/ShellFolder.txt
  MD2 == 3a755a99c5e407005cd45ebd856b4649
  MD5 == 5d08d440fa911d1e418c69a90b83cd86
  SHA1 == 522c8c4f4ff1dd669e251c2ab854c3033a51ca63
  SHA-256 == d1feb0c73c10a759e88bd240cb9d56d0598b4ff83a0704c6679f7ba12f6c4d99
  SHA-384 == 8f8c9da4cd7241c58af3c52b49199033f2dcf3d67f421753999f87511618d9ea2d738e8c16b9b68a7572d06108ff10f6
  SHA-512 == 4711579daee3ddacbaea189310348956cb43bcaaf0099f3be047b06f16c1a20a6b71ee3a4ee018128d647e9f2ef0d644747672238e49a8da3d0cd26dfe597458
需要獲取 hash 的文件為: D:/hapfish/ShellFolder.jpg
  MD2 == 3a755a99c5e407005cd45ebd856b4649
  MD5 == 5d08d440fa911d1e418c69a90b83cd86
  SHA1 == 522c8c4f4ff1dd669e251c2ab854c3033a51ca63
  SHA-256 == d1feb0c73c10a759e88bd240cb9d56d0598b4ff83a0704c6679f7ba12f6c4d99
  SHA-384 == 8f8c9da4cd7241c58af3c52b49199033f2dcf3d67f421753999f87511618d9ea2d738e8c16b9b68a7572d06108ff10f6
  SHA-512 == 4711579daee3ddacbaea189310348956cb43bcaaf0099f3be047b06f16c1a20a6b71ee3a4ee018128d647e9f2ef0d644747672238e49a8da3d0cd26dfe597458
需要獲取 hash 的文件為: E:/ShellFolder增加字符.txt
  MD2 == f2717c24c6c0e110457bd17221c9ca6c
  MD5 == c49e353a7c4c26bd7ccb5e90917c230f
  SHA1 == 477c8a9e465bfaa4be42d35c032a17f7e6b42b97
  SHA-256 == 9fa18adaf242ebcdc6563922d84c2a163c82e1a24db2eb2b73978ed1f354a8a3
  SHA-384 == 4eee8f8e6d64d21c15dc01fa049f4d12a3b8e1d94d87763fe0bea75ab5ea8432fa8251289ece45ee39fe3d36b3c3020c
  SHA-512 == e852ec0ff77250be497389d2f5a1818c18bb66106b9905c4ee26fe0d256eb3b77e0ce9a28a84e4b67e4332ba37ec3aa7518148e3a682318c0fc34c391f45c201
需要獲取 hash 的文件為: D:/hapfish/birosoft.jar
  MD2 == 38c5e1404718916dec59c33cafc909b3
  MD5 == dc3e2cc4fb3949cf3660e0f5f8c3fba3
  SHA1 == cde3dc25498afc5a563af0bb0eb54dc45f71bb28
  SHA-256 == adf6a961c70c6ea677dff066fc5d896fb0beb4dd442ca0eb619ae1d1b04291e5
  SHA-384 == fe7c6b754893c53ebd82bb53703fb5cc32115c9a38f98072f73def90729b271ee3c5c78e258bd9ff5ee5476193c2178b
  SHA-512 == a15376f327256a6e049dfbdc5c2ad3a98bffccc6fa92ee01ff53db6b04471ca0f45ca28f76ff4a6911b57825afa046671299141f2499d71f1dac618c92385491


最後,把運行結果貼出來有點占空間,主要為了說明表述自己的猜想。一般來說同一哈希演算法對同一文件(鏡像、擴展名被修改)所產生的結果應該是一致的。

因此有個猜想,在 baidu 文庫、騰訊的群共享上傳時,先會判斷是否有相同文件,從某種可能上來說也采用了對文件的哈希演算法,畢竟從本地運算一個哈希演算法後獲得的數值要比把整個文件傳過去比較實惠得多。而且字符串的比較也是很方便的。

對於某一種哈希演算法,存在一種可能:就是兩個不同的文件,計算出來的哈希值可能是一樣的。當然為了保險,可以用兩種甚至更多的哈希演算法,只有在每種演算法獲得的哈希值都相同時,才能判斷是同一個文件。
如果我們也對用戶上傳的文件進行哈希計算的話,就可以節省資源,同樣的文件按理說可以減少上傳次數……