sys.sysprocesses (Transact-SQL)

含正在 SQL Server 实例上运行的进程的相关信息。这些进程可以是客户端进程或系统进程。若要访问 sysprocesses,您必须位于 master 数据库上下文中,或者必须使用由三部分构成的名称 master.dbo.sysprocesses。

列名

数据类型

说明

spid

smallint

SQL Server 会话 ID。

kpid

smallint

Windows 线程 ID。

blocked

smallint

正在阻塞请求的会话的 ID。如果此列为 NULL,则表示请求未被阻塞,或锁定会话的会话信息不可用(或无法进行标识)。 -2 = 阻塞资源由孤立的分布式事务拥有。 -3 = 阻塞资源由延迟的恢复事务拥有。 -4 = 由于内部闩锁状态转换而无法确定阻塞闩锁所有者的会话 ID。

waittype

binary(2)

保留。

waittime

bigint

当前等待时间(毫秒)。 0 = 进程不等待。

lastwaittype

nchar(32)

指示上次或当前等待类型名称的字符串。

waitresource

nchar(256)

锁资源的文本化表示法。

dbid

smallint

当前正由进程使用的数据库 ID。

uid

smallint

执行命令的用户 ID。如果用户数和角色数超过 32,767,则发生溢出或返回 NULL。有关详细信息,请参阅查询 SQL Server 系统目录。

cpu

int

进程的累计 CPU 时间。无论 SET STATISTICS TIME 选项是 ON 还是 OFF,都为所有进程更新该项。

physical_io

int

进程的累计磁盘读取和写入。

memusage

int

当前为此进程分配的过程缓存中的页数。一个负数,表示进程正在释放由另一个进程分配的内存。

login_time

datetime

客户端进程登录到服务器的时间。对于系统进程,将存储 SQL Server 的启动时间。

last_batch

datetime

客户端进程上次执行远程存储过程调用或 EXECUTE 语句的时间。对于系统进程,将存储 SQL Server 的启动时间。

ecid

smallint

用于唯一标识代表单个进程进行操作的子线程的执行上下文 ID。

open_tran

smallint

进程的打开事务数。

status

nchar(30)

进程 ID 状态。可能的值有: dormant = SQL Server 正在重置会话。 running = 会话正在运行一个或多个批。多个活动的结果集 (MARS) 启用后,会话可以运行多个批。有关详细信息,请参阅使用多个活动的结果集 (MARS)。 background = 会话正在运行一个后台任务,例如死锁检测。 rollback = 会话具有正在处理的事务回滚。 pending = 会话正在等待工作线程变为可用。 runnable = 会话中的任务在等待获取时间量程时位于计划程序的可执行队列中。 spinloop = 会话中的任务正在等待调节锁变为可用。 suspended = 会话正在等待事件(如 I/O)完成。

sid

binary(86)

用户的全局唯一标识符 (GUID)。

hostname

nchar(128)

工作站的名称。

program_name

nchar(128)

应用程序的名称。

hostprocess

nchar(10)

工作站进程 ID 号。

cmd

nchar(16)

当前正在执行的命令。

nt_domain

nchar(128)

客户端的 Windows 域(如果使用 Windows 身份验证)或可信连接的 Windows 域。

nt_username

nchar(128)

进程的 Windows 用户名(如果使用 Windows 身份验证)或可信连接的 Windows 用户名。

net_address

nchar(12)

为每个用户工作站上的网络适配器分配的唯一标识符。当用户登录时,该标识符插入 net_address 列。

net_library

nchar(12)

用于存储客户端网络库的列。每个客户端进程都在网络连接上进入。网络连接有一个与这些进程关联的网络库,该网络库使得这些进程可以建立连接。有关详细信息,请参阅网络协议和 TDS 端点。

loginame

nchar(128)

登录名。

context_info

binary(128)

使用 SET CONTEXT_INFO 语句存储在批中的数据。

sql_handle

binary(20)

表示当前正在执行的批或对象。 注意 此值是从对象的批或内存地址派生的。通过使用基于 SQL Server 哈希的算法无法计算此值。

stmt_start

int

为指定 sql_handle 运行当前 SQL 语句的起始偏移量。

stmt_end

int

所指定 sql_handle 的当前 SQL 语句的结束偏移量。 -1 指出当前语句为指定的 sql_handle 运行到 fn_get_sql 函数返回结果的结尾。

request_id

int

请求 ID。用于标识在特定会话中运行的请求。

SQL Server 使用快照事务隔离避免死锁

使用基于行版本控制的隔离级别:2005中支持快照事务隔离和指定READ_COMMITTED隔离级别的事务使用行版本控制,可以将读与写操作之间发生的死锁几率降至最低: SET ALLOW_SNAPSHOT_ISOLATION ON –事务可以指定 SNAPSHOT 事务隔离级别; SET READ_COMMITTED_SNAPSHOT ON –指定 READ_COMMITTED 隔离级别的事务将使用行版本控制而不是锁定。默认情况下(没有开启此选项,没有加with nolock提示),SELECT语句会对请求的资源加S锁(共享锁);而开启了此选项后,SELECT不会对请求的资源加S锁。 在数据库中设置READ COMMITTED SNAPSHOT 或 ALLOW SNAPSHOT ISOLATIONON ON时,查询数据时不再使用请求共享锁,如果请求的行正被锁定(例如正在被更新),SQL_Server会从行版本存储区返回最早的关于该行的记录(SQL_server会在更新时将之前的行数据在tempdb库中形成一个链接列表。

select name,user_access,user_access_desc,
snapshot_isolation_state,snapshot_isolation_state_desc,
is_read_committed_snapshot_on
from sys.databases;

ALTER DATABASE DBName SET SINGLE_USER WITH ROLLBACK IMMEDIATE
ALTER DATABASE DBName SET ALLOW_SNAPSHOT_ISOLATION ON
ALTER DATABASE DBName SET READ_COMMITTED_SNAPSHOT ON
ALTER DATABASE DBName SET MULTI_USER

注意:设置 READ_COMMITTED_SNAPSHOT选项时,数据库中只允许存在执行 ALTER DATABASE命令的连接。在 ALTER DATABASE完成之前,数据库中决不能有其他打开的连接。数据库不必一定要处于单用户模式中。 参考:使用快照隔离 https://msdn.microsoft.com/zh-cn/library/tcbchxcb(v=vs.80).aspx.aspx)

需要我们了解的SQL Server阻塞原因与解决方法

上篇说SQL Server应用模式之OLTP系统性能分析。五种角度分析sql性能问题。本章依然是SQL性能 五种角度其一“阻塞与死锁”

这里通过连接在sysprocesses里字段值的组合来分析阻塞源头,可以把阻塞分为以下5种常见的类型(见表)。waittype,open_tran,status,都是sysprocesses里的值,“自我修复?”列的意思,就是指阻塞能不能自动消失。 5种常见的阻塞类型

类型

waittype

open_tran

status

自我修复

原因/其他特征

1

不为0

=0

runnable

是的,当语句运行结束后

语句运行的时间比较长,运行时需等待某些系统资源(如硬盘读写、CPU或内存等)。

2

0x0000

0

sleeping

不能,但是如果运行 KILL语句,这个链接能够很容易被终止

可能客户端遇到了一个语句执行超时,或者主动取消了上一语句的执行,但是没有回滚开启的事务,在SQL Trace里能够看到一个Attention事件

3

0x0000 0x0800 0x0063

=0

runnable

不能。知道客户端吧所有结果都主动取走,或者主动断开连接,可以运行KILL语句去终止它,但是可能要花长达30秒

客户端没有及时把所有结果都取走,这时可能open_tran=0,事务隔离级别也为默认(READ COMMITTED),但这个连接还会持有锁资源

4

0x0000

0

rollback

是的

在SQL Trace里能够看到这个SPID已经发来了一个Attention事件,说明客户端已经遇到了超时,或者主动要求回滚事务

5

各种值都有可能

=0

runnable

不能,直到客户端取消语句运行或者主动断开连接。可以运行KILL语句终止它,但是可能要花长达30秒

应用程序运行中产生死锁,在SQL Server中以阻塞形式体现。Sysprocesses里阻塞和被阻塞的连接hostname值是一样的

下面详细介绍这些类型产生的原因,以及解决方法

类型1:由于语句运行时间太长而导致的阻塞,语句本身在正常运行中,只须等待某些系统资源。

解决方法: 要解决这一类阻塞,数据库管理员需要和数据库应用设计人员合作,共同解决以下问题。

  1. 语句本身有没有可优化的空间? 这里包括修改语句本身降低复杂度、修改表格设计、调整索引等。
  2. SQL Server整体性能如何?是不是有资源瓶颈影响了语句执行速度? 当SQL Server 遇到诸如内存、硬盘读写、CPU等资源瓶颈是,原来能很快完成的语句有可能会花很长时间。
  3. 如果语句天生就很复杂,无法调优(很多处理报表的语句就是这样),就须考虑怎样把这一类应用(一般就是数据仓库应用)从OLTP系统中隔离出来。

类型2:由于一个未按预期提交的事务导致的阻塞

这一类阻塞的特征,就是问题连接早就进入了空闲状态(sysprocesses.status=’sleeping’和sysprocesses.cmd=’AWAITING COMMAND’),但是,如果检查sysprocesses.open_tran,就会发现它不为0,以及事务没有提交。这类问题很多都是因为应用端遇到一个执行超时,或者其他原因,当时执行的语句被提前终止了,但是连接还保留着。应用没有跟随发来的事务提交或回滚指令,导致一个事务被遗留在SQL Server里。 遇到这类问题,许多使用者会误以为是SQL Server端什么地方没有处理好。其实,执行超时(command timeout)完全是一个客户端的行为。当客户端应用向SQL Server发来语句执行请求时,自己会有一个执行超时设置。一般ADO或ADO.NET的连接超时时限是30秒。如果30秒以内SQL Server没有完成语句返回任何结果,客户端就会发送一个Attention的消息给SQL Server,告诉SQL Server它不想继续等下去了。SQL Server收到这个消息后,会终止当前正在运行的语句(或批处理)。但是,为了维护客户端的逻辑,SQL Server默认不会自动回滚或提交这个连接已经打开的事务,而是等待客户端的后续决定。如果客户端不发来回滚或提交指令,SQL Server会永远的把这个事务保持下去,直到客户端断开连接为止。 这里可以用下面这个实验来模拟这个问题。在Management Studio里创建一个连接到SQL Server,运行下面的批处理语句:

复制代码

use sqlnexus
go
BEGIN TRAN
SELECT
FROM ReadTrace.tblInterestingEvents
WITH(HOLDLOCK) SELECT
\

FROM sysobjects s1,sysobjects
s2 COMMIT TRAN

复制代码

由于使用了HOLDLOCK参数,第一句SELECT会在运行结束后,在表格上维持一个TAB的S锁。如果批处理全部完成,这个锁会在提交事务的时候释放。但是第二句的SELECT会执行很久。请在等待3~4秒钟以后取消执行。然后运行下面的语句,检查open_tran和锁的情况。

SELECT @@TRANCOUNT
GO sp_lock GO

通过结果(见图)可以得知: (1) 批处理被取消的时候,“COMMIT TRAN”这条语句没有被执行到。SQL Server没有对“BEGIN TRAN”开启的那个事务做任何处理,只保持其活动的状态。 (2) 第一句SELECT带来的锁由于事务没有结束,所以锁还保持着(objID=85575343, Type=TAB, Mode=IS)。 现在,如果有其他连接要修改ReadTrace.tblInterestingEvents这张表,就会被阻塞住。 解决办法: 1. 应用程序本身必须意识到审核语句都有可能遇到意外终止情况,做好错误处理工作。这些工作包括 a) 在做SQL Server调用的时候,必须加上错误捕捉和处理语句 SQL Server客户端驱动程序(包括ODBC和OLE DB)当语句执行遇到意外终止(包括超时)的时候,都会向应用返回错误信息。客户端在捕捉到错误信息时。除了做记录以外(这对问题定位非常有帮助),还要运行下面这句话,把没有提交的事务回滚掉。

IF @@TRANCOUNT>0 ROLLBACK TRAN

有些程序员会问,我在T-SQL批处理里已经写了T-SQL层面的错误捕捉和处理语句(IF @@ERROR<>0 ROLLBACK TRAN),还有必要让应用程序再做一遍么?需要意识到的是,有些异常(比如超时)终止的是整个T-SQL批处理的执行,而不仅仅是当前语句。所以当这些异常发生的时候,T-SQL层面错误捕捉和处理语句很可能也一起被取消了。它们不能发挥想象中的作用。在应用程序里的错误捕捉和处理语句是必不可少的。 b) 设置连接属性“SET SACT_ABORT ON” 当SET SACT_ABORT为ON时,如果执行T-SQL语句产生运行错误,整个事务将会终止并回滚 当SET SACT_ABORT为OFF时,处理方法不是唯一的。有时只回滚产生错误的T-SQL语句,而事务将继续进行处理。如果错误很严重,及时SET SACT_ABORT 为OFF,也可能回滚整个事务。OFF是默认设置。 如果没有办法很快规范应用程序的错误捕捉和处理语句,一个最快的方法就是在每个连接建立以后,或者是容易出问题的存储过程的开头,运行“SET XACT_ABORT ON”,让SQL Server帮助应用程序回滚事务。 c) 考虑是否要关闭连接池 一般的SQL Server应用都会使用连接池来得到良好的性能。如果有一个连接忘记把事务关闭就推出连接,那么这个连接会被交还给连接池,但是这个时候事务不会被清理。客户端驱动程序会在这个连接下一次被重用的时候(又有新的用户要建立连接),发一句sp_reset_connection命令清理当前连接上次遗留下来的所有对象,包括回滚未提交的事务。如果连接交还给连接池以后很久都没有被重用,那它的事务就会持续长时间,引起阻塞。有些Java程序使用的驱动程序,提供连接池功能,但是不提供连接重用时的事务清理功能。这样的连接池对应用开发质量要求很高,比较容易发生阻塞。 如果不能很快的实施建议a)和b),把连接池关闭能缩短食事务持续时间,也能从一定程度上缓解阻塞问题。 2. 分析为什么连接会遇到异常终止 这里又得谈到错误信息记录了。有了错误信息,就可以判定是超时问题,还是其他SQL Server错误。如果是超时问题,可按照第一种阻塞进行处理。 还有一种孤儿事务的来源,是连接开启了隐式事务(implicit transaction)而没有加入及时提交事务的机制。如果连接处于隐式事务模式(SET IMPLICIT_TRANSACTIONS ON),并且连接当前不再事务中,则执行下列任何一条语句都会开启一个新的事务。

ALTER TABLE

FETCH

REVOKE

CREATE

GRANT

SELECT

DELETE

INSERT

TRUNCATE_TABLE

DROP

OPEN

UPDATE

对于因为此设置为ON而自动打开的事务,SQL Server会自动帮你打开事务,但是不会自动帮你提交。用户必须在该事务结束后将其显式提交或回滚。否则,当用户断开连接时,事务及其包含的所有数据更改将被回滚。事务提交后,执行上述任意一条语句又会启动一个新事务。隐式事务模式将始终生效,知道连接执行SET IMPLICIT_TRANSACTIONS OFF语句使连接恢复为自动提交模式。在自动提交模式下,所有单个语句在成功完成时将被提交,不会有事务遗留。 为什么会有连接要开启隐式事务呢?除了程序员有意为之以外,很多是客户端数据库连接驱动,或者空间为了实现它的事务功能(注意不是SQL Server通过T-SQL语句直接提供的)而选用这个机制。如果应用程序出现意外,或者脚本没有处理好,会有应用层事务未提交的现象。在SQL Server里也体现为一个孤儿事务。严格约束应用层对事务的使用,直接使用SQL Server里面的事务,是避免这种问题出现的好方法。

类型3:由于客户端没有及时把结果集取出而导致的语句长时间运行。

语句在SQL Server内执行总时间不仅包含SQL Server的执行时间,还包含把结果集发给客户端的时间。如果结果集比较大,SQL Server会分几次打包发出,每发一次,都要等待客户端的确认。只有确认以后,SQL Server才会发送下一个结果集包。所有结果都发完以后,SQL Server才认为语句执行完毕,释放执行申请的资源(包括锁资源)。 如果处于某种原因,客户端应用处理结果非常缓慢甚至没有相应,或者干脆不理睬SQL Server发送结果集的请求,则SQL Server会耐心的等待,因此会导致语句长时间执行而发生阻塞。 解决方法:

  1. 在设计程序时,一定要慎重返回大结果集。这种行为不仅会对SQL Server和网络带来很大负担,对应用程序本身来讲,也要花很多资源去处理结果集。如果最终用户只需要部分结果集就可以,则在发送SQL Server指令的时候就要指定好。要避免居于不管三七二十一所有数据都要,而结果集只取走开头一部分去展示这样的行为发生。
  2. 如果应用程序的确须返回大结果集,例如一些报表系统,则要考虑报表数据库和生产数据库分开。
  3. 如果1和2在短期内不能实现,可以和最终用户协商,返回大结果集的连接使用READ UNCOMMITTED事务隔离级别。这样查询语句就不会申请S锁了。

类型4:阻塞的源头连接一直处于rollback状态。

这种情况常是由第一类情况衍生来的。有时候数据库管理员发现一个连接阻塞住了别人,为了解决问题,会让连接主动退出或强制退出(轻质退出应用,或者直接在SQL Server端KILL连接)。对于大部分情况,这些措施会消除阻塞。但是要记住的是,不管是在客户端退出,还是要服务器端KILL,为了维护数据库事务的一致性,SQL Server都会对连接还没有来得及完成提交的事务做回滚动作。SQL Server要找到所有当前事务修改过的记录,把它们改回原来的状态。所以,如果一个DELETE、INSERT或UPDATE已经运行了一个小时,可能回滚也需要一个小时,在这个过程中,阻塞还会延续,我们只能等待。 有些用户可能等不及,直接重启SQL Server。当SQL Server关闭的时候,回滚动作会被中断,SQL Server会被很快关掉,但是这个回滚动作在下次SQL Server重启的时候会重新开始(数据库做恢复的时候)。重启的时候如果回滚不能很快结束,整个数据库都不可用,可能会带来更严重的后果。 解决方法: 最好的方法是在工作时间尽量不要做这种大的修改操作。这些操作尽量安排在半夜或者周末的时间完成。如果操作已经做了很久,最好耐心等它做完。如果一定要在有工作负荷的时候做,最好把一个大操作分成若干小操作分步完成。

类型5:应用程序运行中产生死锁,在SQL Server中以阻塞形式体现。

一个客户端的应用在运行过程中会使用到许多资源,包括线程资源,信号量资源,内存资源,IO资源等,SQL Server也是资源之一。如果发生死锁的两端不全是SQL Server,SQL Server的死锁判断机制可能不起作用。这时如果应用端没有处理好,可能会永远等下去。而SQL Server内部的表现可能仅仅是一个阻塞。但是这个阻塞不会自动消除。这样的阻塞对SQL Server的性能会产生很大影响。 下面我们举两个这种应用端死锁的例子。 1) 在应用的一个线程中开启不止一个数据库连接而产生的死锁(见图)。 假设应用有一个线程有这样的逻辑: ● 开始运行 ● 建立数据库连接A,调用存储过程ProcA。打开结果集A。 ● 建立数据库连接B,调用存储过程ProcB。打开结果集B。 ● 轮流读取结果集A、B,整合输出最终结果。 ● 关闭结果集A、B,关机连接A、B。 ● 结束运行 在正常情况下这样的设计看上去没有问题,但是实际上很脆弱。因为在线程内部,这个逻辑是线程执行的。假设存储过程ProcA是一个事务,在返回结果集之前因为一些操作申请了一些排他锁,而ProcB为了返回结果又要用到这些锁,那会发生什么情况呢? 发生的情况会是连接A在等线程把连接B上的结果读出来,再来处理结果集A,而连接B等待连接A完成事务后再释放锁。双方相互等待,产生思索。 1) 两个线程间的死锁(见图)。 如果应用有两个线程,每个线程各开一个数据库连接,那上面的逻辑不会出问题。因为运行ProcA的那个线程会先做完,释放阻塞住连接B的锁,让B也能够接着跑完。但是假设有下列逻辑: 线程A:建立数据库连接A,不断读取表格A,按条取出记录,做一定处理后发给线程B的输入缓存。 线程B:建立数据库连接B,从输入缓存读取数据,依据收到的记录对表格A进行修改。 这个逻辑会产生什么问题呢?我们知道表格修改会在表上申请一些排他锁。如果线程A正在读取这条记录,修改动作会被阻塞住。这个时候线程B就会进入等待状态。但是线程A需要线程B输入缓存清空后才能写入。如果线程B还没来得及清空,它也不得不等待,这时候也会产生死锁(在SQL Server里是一个阻塞)。 解决方法: 复杂的程序还可能会出现其他的死锁形式。为了避免这种死锁,要在应用调用SQL Server的时候设置执行超时,并写好错误处理机制(参见阻塞原因2)。一旦死锁发生,SQL Server的操作在等待一段时间后会因为超时而放弃,并释放出SQL Server内部的资源,解决死锁。 小结:应更多从程序设计着手解决阻塞问题 很多用户有一种误解,认为阻塞是一个数据库问题。当阻塞问题发生的时候,都希望从数据库层面找到方法,一劳永逸地解决问题。可是,阻塞本身是为了完成事务的隔离,是应用程序向SQL Server提出的要求。所以很多时候,光从数据库端努力是不能解决阻塞问题的。在应用程序层面也要做很多工作。例如应用在做连接的时候选择什么样的隔离级别,事务开始和结束的时间点选择,连接的建立和回收机制,指令复杂度的控制等。应用程序还应该考虑到控制结果集大小,并及时从SQL Server端取走数据。还要考虑SQL Server指令执行时间长短控制,以及发生超时或其他意外后的错误处理机制等。尤其是对高并发量、高响应要求的关键业务系统,在设计应用时必须要考虑好上面这些关键因素。对于关键的业务逻辑,必须逐个审查,保证应用选择的是能够满足业务需求的最低隔离级别,事务的大小已经控制到了最小的粒度。而运行的语句,也要有良好的数据库设计,保证它不会随着数据库的增大和用户量的增多,占用更多的资源和运行时间。如果做不到这几点,就会容易发生应用在用户量比较少,或者数据库比较小的初始阶段性能不错,但是当用户量增长或数据量增大以后性能越来越慢的问题。

启动SQL Server Profiler,创建Trace(跟踪)

启动SQL Server Profiler,创建Trace(跟踪). 启动SQL Server Profiler工具(在Microsoft SQL Server Management Studio的工具菜单上就发现它),创建一个Trace,Trace属性选择主要是包含: Deadlock graph Lock: Deadlock Lock: Deadlock Chain RPC:Completed SP:StmtCompleted SQL:BatchCompleted SQL:BatchStarting 分析死锁 如下图,我们可以看到第一个会话在SPID 54,第二个会话在SPID 55,一旦SQL Server发现死锁,它就会确定一个优胜者,可成功执行,和另一个作为牺牲品,要回滚。 可以到看到EventClass列中,两条SQL:BatchCompleted事件紧跟在Lock:DealLock后面,其中一条,它就是作为牺牲品,它会被回滚.而另一条SQL:BatchCompleted将会是优胜者,成功执行。 那么,谁是优胜者,谁是牺牲品呢? 不用着急,通过DealLock graph事件,所返回来的信息,我们可以知道结果。 我们虽然不能明白DealLock graph图示的含义,但通过图中描述的关系,我们知道一些有用的信息。图中左右两旁椭圆形相当一个处理节点(Process Node),当鼠标移动到上面的时候,可以看到内部执行的代码,如Insert,UPdate,Delete.有打叉的左边椭圆形就是牺牲者,没有打叉的右边椭圆形是优胜者。中间两个长方形就是一个资源节点(Resource Node),描述数据库中的对象,如一个表、一行或一个索引。在我们当前的实例中,资源节点描述的是,在聚集索引请求获得排它锁(X)。椭圆形与长方形之间,带箭头的连线表示,处理节点与资源节点的关系,包含描述锁的模式. 接下来我们更详细的看图里面的数据说明。 先看右边作为优胜者的这椭圆形,我们可以看到内容包含有: 服务器进程 ID: 服务器进程标识符 (SPID),即服务器给拥有锁的进程分配的标识符。 服务器批 ID: 服务器批标识符 (SBID)。 执行上下文 ID: 执行上下文标识符 (ECID)。与指定 SPID 相关联的给定线程的执行上下文 ID。ECID = {0,1,2,3, …n},其中 0 始终表示主线程或父线程,并且 {1,2,3, …n} 表示子线程。 死锁优先级: 进程的死锁优先级有关可能值的详细信息,请参阅 SET DEADLOCK_PRIORITY (Transact-SQL)。 已用日志: 进程所使用的日志空间量。 所有者 ID: 正在使用事务并且当前正在等待锁的进程的事务 ID。 事务描述符: 指向描述事务状态的事务描述符的指针。 这些数据描述,对于我们理解死锁,只需要知道其中的一些就够,除非我们在专门SQL Server机构工作,才可能要深入理解它们。 下面我们来看左边作为牺牲品的这椭圆形处理节点,它告诉我们以下信息: 它是一个失败的事务。(蓝色的交叉表示) 它是作为牺牲品的T-SQL代码。 它对右下方的资源节点有一个排它锁(X). 它对右上方的资源节点请求 一个排它锁(X). 我们再来看中间两个长方形的资源节点,两个处理节点对它们各自都使用权,来执行它们各自的代码,同时又有对对方使用资源请求的动作,从而发生了资源的竞争。 这也就让我们明白死锁发生的原因。 这里说明下资源节点的一些信息: HoBT:  堆或 B 树。 用于保护没有聚集索引的表中的 B 树(索引)或堆数据页的锁 associated objid: 关联的对象ID,这里只是索引关联的对象ID. Index name:索引名 让我们再对SQL Server Profiler监视到的数据,作一次整理: 回顾图: 在第3行SQL:BatchStarting, SPID 54 (第一个会话启动),在索引PK__DealLock__3214EC274222D4EF获得一个排它锁,再处理等待状态,(因为在这个实例中我设置了Waitfor Delay ‘00:00:05’) 在第6行SQL:BatchStarting, SPID 55 (第二个会话启动),在索引PK__DealLock__3214EC2745F365D3获得一个排它锁,再处理等待状态,(因为在这个实例中我设置了Waitfor Delay ‘00:00:05’) 两个进程都各自获得一个排它锁(X),几秒过去,它们就开始请求排它锁。 SPID 54 (第一个会话),先对PK__DealLock__3214EC2745F365D3请求一个排它锁(X),但PK__DealLock__3214EC2745F365D3当前已经给SPID 55 (第二个会话)获得。SPID 54要于等待。 同时, SPID 55 (第二个会话),开始对PK__DealLock__3214EC274222D4EF请求一个排它锁(X),但PK__DealLock__3214EC274222D4EF当前已经给SPID 54 (第一个会话)获得。SPID 55要等待。 这里就出现了进程阻塞,从而发生死锁。 SQL Server 检查到这两个进程(第一个&第二个会话)发生死锁,并对占用资源比较少的进程,列入牺牲品名单,将它终止(Kill)。通过左右椭圆形进程节点显示,可以发现已用日志最少的是左边的进程节点。 SPID 54 (第一个会话)被回滚(Rollback),SPID 55 (第二个会话)执行成功。 到这里我们已算完成了,对死锁的监视和分析。

并发事务成败皆归于锁——锁定

在多用户都用事务同时访问同一个数据资源的情况下,就会造成以下几种数据错误。

  • 更新丢失:多个用户同时对一个数据资源进行更新,必定会产生被覆盖的数据,造成数据读写异常。
  • 不可重复读:如果一个用户在一个事务中多次读取一条数据,而另外一个用户则同时更新啦这条数据,造成第一个用户多次读取数据不一致。
  • 脏读:第一个事务读取第二个事务正在更新的数据表,如果第二个事务还没有更新完成,那么第一个事务读取的数据将是一半为更新过的,一半还没更新过的数据,这样的数据毫无意义。
  • 幻读:第一个事务读取一个结果集后,第二个事务,对这个结果集经行增删操作,然而第一个事务中再次对这个结果集进行查询时,数据发现丢失或新增。

然而锁定,就是为解决这些问题所生的,他的存在使得一个事务对他自己的数据块进行操作的时候,而另外一个事务则不能插足这些数据块。这就是所谓的锁定。 锁定从数据库系统的角度大致可以分为6种:

  • 共享锁(S):还可以叫他读锁。可以并发读取数据,但不能修改数据。也就是说当数据资源上存在共享锁的时候,所有的事务都不能对这个资源进行修改,直到数据读取完成,共享锁释放。
  • 排它锁(X):还可以叫他独占锁、写锁。就是如果你对数据资源进行增删改操作时,不允许其它任何事务操作这块资源,直到排它锁被释放,防止同时对同一资源进行多重操作。
  • 更新锁(U):防止出现死锁的锁模式,两个事务对一个数据资源进行先读取在修改的情况下,使用共享锁和排它锁有时会出现死锁现象,而使用更新锁则可以避免死锁的出现。资源的更新锁一次只能分配给一个事务,如果需要对资源进行修改,更新锁会变成排他锁,否则变为共享锁。
  • 意向锁:SQL Server需要在层次结构中的底层资源上(如行,列)获取共享锁,排它锁,更新锁。例如表级放置了意向共享锁,就表示事务要对表的页或行上使用共享锁。在表的某一行上上放置意向锁,可以防止其它事务获取其它不兼容的的锁。意向锁可以提高性能,因为数据引擎不需要检测资源的每一列每一行,就能判断是否可以获取到该资源的兼容锁。意向锁包括三种类型:意向共享锁(IS),意向排他锁(IX),意向排他共享锁(SIX)。
  • 架构锁:防止修改表结构时,并发访问的锁。
  • 大容量更新锁:允许多个线程将大容量数据并发的插入到同一个表中,在加载的同时,不允许其它进程访问该表。

这些锁之间的相互兼容性,也就是,是否可以同时存在。

现有的授权模式

请求的模式

IS

S

U

IX

SIX

X

意向共享 (IS)

共享 (S)

更新 (U)

意向排他 (IX)

意向排他共享 (SIX)

排他 (X)

锁兼容性具体参见:http://msdn.microsoft.com/zh-cn/library/ms186396.aspx 锁粒度和层次结构参见:http://msdn.microsoft.com/zh-cn/library/ms189849(v=sql.105).aspx

Hibernate的基本配置

第1**章 准备源代码阅读环境与Hibernate的基本配置**

1.1 概述

本书将通过深度解读Hibernate源代码来分析Hibernate的架构设计与实现原理,所以在开始之前,我们应该拥有一份Hibernate的源代码,并对源代码有一个大概的了解。这样,我们就可以很方便地结合Hibernate源代码阅读后面的章节。本章主要讲解如何搭建源代码阅读环境,以及介绍Hibernate项目的基本配置和详细的实体映射配置等内容,详细的Hibernate项目配置将在第2章讲解。

1.**2 建立源代码阅读环境**

1.2.1**安装**JDK

JDK是Java程序开发包,是我们开发运行Java程序的基础。接下来我们安装的Eclipse也是运行在Java虚拟机之上的。Java虚拟机也有多个实现,我们选用原Sun公司的虚拟机,随着Oracle收购Sun的成功,现在已经成为Oracle的产品了。我们准备安装6.x版本的JDK。下载地址如下:http://java.sun.com/javase/downloads/widget/jdk6.jsp。我们选择下载Windows版本的JDK。

运行刚才下载的JDK安装文件。进入安装初始化界面。这时您要等待一会,具体时间视机器的速度而定。在此期间,安装向导程序会收集机器的一些配置信息,为以后的安装提供依据。

图1-1是JDK功能安装选项。你可以安装开发工具、演示程序及样例、源代码、公共JRE、JavaDB等。其中,开发工具、演示程序及样例是必须安装的,其他几项是可以选择安装的,如果你不需要,可以点击其前的下拉图标选择“现在不安装此功能”即可。开发工具中包含我们必须用到的编译环境和运行Java程序需要的虚拟机。演示程序及样例中有一些小应用程序的源码,可以帮助我们学习Java的相关技术。源代码中包含Java公共API(Application Programming Interface,应用程序编程接口)的类,安装这项后,如果我们想了解Java中的某个类的内部实现方式,就可以在此找到其源代码。公共JRE是一个独立的虚拟机运行环境。Java DB是一个附带的数据库,这里可以不用安装。
JDK功能安装选项
图1-1 JDK功能安装选项

在安装JRE时,你可以更改安装路径,安装到其他位置。在此,我们保持默认的安装路径。如果点击“取消”,将不会安装独立的JRE。

1.2.2 安装**Eclipse**及相关插件

Eclipse是一个Java集成开发环境,由IBM在1998年11月开始开发,在2001年11月开源。此后IBM和其他8个组织一起建立了Eclipse协会和eclipse.org网站,共同推动Eclipse的发展。目前Eclipse作为一个开源的集成开发环境被全球IT开发人员广泛使用。其下载地址是:http://www.eclipse.org/downloads/。因为以后我们讲解Hibernate的时候会与J2EE项目相结合。所以我们下载时选择Eclipse IDE for Java EE Developers版本的Eclipse,并且是Windows版本。下载完成解压后,即可运行使用。

Eclipse主窗口称为工作台,包含菜单栏、工具栏、编辑器和视图等内容。工具栏下方放置编辑器和其他视图的区域,称为工作台页面。此页面包含界面的大部分可见部分:编辑器和视图。

在今后我们阅读分析Hibernate源代码的过程中,Eclipse将会为我们提供十分便捷的帮助。其中,Hierarchy视图可以方便地帮助我们查看类或接口继承关系。我们可以在视图或编辑器中点击右键,选择要查看的类或接口,选择Open Type Hierarchy或者按快捷键F4,就可以打开该类或接口的继承关系视图了,如图1-2。

图1-2 Eclipse的Hierarchy视图

另外用快捷键Ctrl+T可以快速地弹出一个类或接口的继承关系列表窗口。同样地,Call Hierarchy视图可以帮助我们查看一个方法被其他方法调用的情况。我们在一个方法上点击右键,选择要查看的方法,选择Open Call Hierarchy或用快捷键Ctrl+Alt+H,就可以打开该方法的调用关系视图,如图1-3所示。

图1-3 Eclipse的Call Hierarchy视图

1.2.3 安装**SVN**插件

2010年11月9日,Hibernate开发团队宣布使用Gif控制Hibernate的源代码。在迁移到Git中时,放弃一些较老的版本。考虑到目前国内使用SVN的读者居多,有必要讲解一下SVN的使用方法。如果有读者想了解Gif的使用方法可到本书的附录中查阅。

SVN(subversion)是近年来崛起的版本管理工具,目前,绝大多数开源软件都使用SVN作为代码版本管理软件,其最新版本的下载地址是:http://subclipse.tigris.org/update_1.6.x。适用于3.2以上版本的Eclipse。在Eclipse中,通过自带的插件安装工具,可以十分方便地下载安装这个插件。具体操作步骤是:菜单Help→Software Updates→Find and Install…→Search for new features to install。如图1-4。

图1-4创建SVN插件下载站点

新增一个更新站点后保存。点击完成后进入选择更新界面,如图1-5。

图1-5 选择SVN插件安装项目

在此我们更新所有的Subclipse包,之后重新启动Eclipse即可。那么怎么知道我们的SVN是否安装成功呢?我们可以在Eclipse菜单Windows→Show View→Other中看到SVN的视图。如图1-6所示。

图1-6 视图列表中的SVN视图

1.2.4 安装**Hibernate**插件

这里介绍两款Hibernate的Eclipse插件:Synchronizer和Hibernate Tools。

Synchronizer是一款开源Hibernate的Eclipse插件,能帮助我们自动生成一些与Hibernate框架相关的Java代码和配置文件。并且当Hibernate映射文件发生变化后,Synchronizer能自动更新Java代码。它能生成以下对象:数据类、代理接口、复合主键、枚举类、复合对象、子类、DAO。

目前,Synchronizer安装地址如下:

http://hibernatesynch.sourceforge.net/

通过Eclipse的插件更新工具,输入上面的一个地址即可安装,步骤与安装SVN插件基本相同,在此不再详细说明。读者可试着自己安装该插件。

Hibernter Tools是另一款优秀的Eclipse下的Hibernate插件,它是Hibernate 3的一个完整的工具集,其中包含一个Hibernate自带的插件。Hibernate Tools是JBoss Tools的一个核心组件,因此也是JBoss Developer Studio的一部分,如图1-7 。

可以使用Eclipse的安装程序安装这一插件。地址是:http://download.jboss.org/jbosstools/updates/stable

图1-7 JBoss Tools中的Hibernate Tools

选择其中的Hibernate Tools安装即可。安装后重新启动Eclipse,在视图列表中会有Hibernate项目,如图1-8所示。

图1-8 视图列表中的Hibernate视图

1.2.5 安装**MySQL**数据库

在MySQL官方网站可以下载MySQL的安装程序。下载地址为:http://dev.mysql.com/downloads/。下载符合你的计算机配置的版本(要注意操作系统和处理器位数)。下载后运行安装程序如图1-9。

图1-9 MySQL的安装界面

图1-10 选择MySQL Server安装类型

如图1-10所示,MySQL Server安装类型有三种:典型、完全、自定义。如果你想指定安装路径或选择安装组件,可以采用自定义的安装方式。我们选择典型安装方式。点击Next就进入安装进度界面,如图1-11。

图1-11 安装进度

安装完成后会有一个配置向导。根据向导的提示可以一步一步地完成MySQL的配置。

1.3**获取Hibernate源代码**

Hibernate于2010年10月4日发布了3.6.0最终版本。在这个版本中有以下改进:

不再支持JDK1.4;

将Hibernate-annotations和Hibernate-jmx整合到Hibernate核心代码中来;

改进了类型支持;

修改了DTD的URL;

改进了文档,增加了新手指南;

强化了注释对discriminators,字段级别的读/写表达式和时间戳版本的支持;

改进了对Envers的支持;

我们有三种方式获取Hibernate源代码:

1)第一种方式:到SourceForge官方网站上获取。网址为:http://sourceforge.net/projects/hibernate/files/hibernate3/。

2)第二种方式:到Jobss官方网站上获取,网址为:http://repository.jboss.org/maven2/org/hibernate/。

3)第三种方式:通过Gif客户端下载。下载路径:https://github.com/hibernate

下载Hibernate源码后,解压加入到一个新的工程中,其目录结构如图1-14所示


图1-14 Hibernate3.5.0-Final的目录结构图

注意当把以上包加入到工程中后,Eclipse就会自动编译,之后会出现一些错误提示,如:下载后的Hibernate源码文件中org.hibernate.hql.antlr是空的,编译时会提示源文件丢失。细心的读者会发现,该包中只有一个HTML文件,打开后有以下提示:

A special package for ANTLR-generated parser classes.

NOTE: The classes in this package are generated from the ANTLR grammar files, do not register them into version control.

即,这个包是一个特殊的包,该包中的类是由ANTLR解析器根据脚本文件生成的。生成步骤如下:

1、将antlr.jar配置到环境变量CLASSPATH中,或拷到jdk的lib下面。

2、打开命令行,进入源码的grammar文件夹(coresrcmainantlr)。可以看到以下文件:hql.g、hql-sql.g、order-by.g、order-by-render.g、sql-gen.g。

3、依次运行上述脚本java antlr.Tool hql.g;java antlr.Tool hql-sql.g;java antlr.Tool sql-gen.g。生成源代码。

4、把生成的java源文件考到org.hibernate.hql.antlr包中。

1.4 Hibernate**源代码的组织结构**

下载完Hibernate源代码后,在Eclipse中Package explorer的结构如图1-15所示。下面我们对Hibernate源代码包结构做一下简单的说明。以便大家对Hibernate有一个大致的了解。

Hibernate源代码主要包括:核心包core、注解包annotations、缓存包cache-ehcache, cache-infinispan, cache-jbosscache, cache-oscache,cache-swarmcache、连接池包connection-c3p0, connection-proxool、ejb实体管理包entitymanager、持久化类审查包envers、Java管理方案的扩展包jmx,还有一些存放测试用例的包和Hibernate自带的实例包等。Hibernate3.5.0-final版将bernate-annotations, hibernate-entitymanager and hibernate-envers整合到核心包中来。

图1-15 Hibernate核心包结构

接下来我们看看每一类别中所包含有哪些包和主要的类以及它们的用途等。Hibernate核心包结构包含以下几个大类。

1.4.1 核心**API**

org.hibernate包存放的是Hibernate核心接口,如SessionFactory、Session、Transaction、Query、Criteria等。还有一些常用的异常类:HibernateException、SessionException、QueryException等。

org.hibernate.cfg包存入的是配置hibernate的相关API和类,如:Configuration、SettingsFactory、Settings、Mappings。

org.hibernate.criterion包提供了一组查询API的实现。我们可以使用它们组装出复杂的查询,以及一些常用的统计查询。

org.hibernate.mapping包定义了Hibernate配置时的元模型。包含了与实体配置文档相关的类,如:Table、Property、ManyToOne等。

org.hibernate.metadata包定义了一组API,用于访问Hibernate运行时的数据元模型。

org.hibernate.classic包向后兼容了Hibernate2.1的一些API,而这些API在Hibernate3.5中已过时。

org.hibernte.stat包暴露了有关Hibernate运行时的一些统计数据。

1.4.2 扩展的**SPI**

org.hibernate.cache定义了Hibernate二级缓存的一些API/SPI,以及对其的实现。

org.hibernate.connection实现了获得JDBC连接的机制。

org.hibernate.collection为集合类封装类定义了框架。

org.hibernate.dialect实现了底层数据库的SQL方言。

org.hibernate.event为Hibernate定义了一个事件框架。

org.hibernate.id为Hibernate提供了不同的实体Id生成机制。

org.hibernate.jdbc实现了分发SQL语句到数据库的机制,并实现了与JDBC的互动。

org.hibernate.loader实现了处理JDBC结果集的功能。

org.hibernate.persister包实现持久对象和表之间的映射,是Hibernate的核心包。它还有两个子包,collection实现了集合的持久化机制,负责持久化集合的对象。Entity实现了实体的持久化机制,并定义了Hibernate运行负责单个实体的持久化。

org.hibernate.proxy定义了延迟加载代理实体的框架。

org.hibernate.transaction实现了底层事务机制(JTA,JDBC),并提供了获得应用服务器事务管理器的策略。

org.hibernate.tuple为实体在对象级别上定义了一个运行时的元模型,并实现了各式各样实体类型的差异化。

org.hibernate.usertype用户自己定义类型的接口。

org.hibernate.type处理Java属性类型与JDBC字段类型的应射。

1.4.3 Bytecode

org.hibernate.bytecode包含一些bytecode库的插件,便于在Hibernate中使用。。Hibernate使用bytecode有三种情形:1、为了优化反射机制:提高访问POJO实体和组件的构造方法或属性的速度。2、为了生成代理:在运行过程中创建用于延迟加载的代理;3、属性级别的拦截:在延迟加载和脏数据跟踪时,为拦截属性级别访问构建实体类的说明。

org.hibernate.intercept这个包实现了基于CGLIB字节码的延迟加载属性的拦截机制。

1.4.4 Infinispan包

Infinispan是JBoss Cache的后续项目,是一个数据网格平台,是一个分布式的缓存系统。Hibernate为了整合Infinispan提供了相应的接口,它们存放在包org.hibernate.cache.infinispan,这样在Hibernte中就使用Infinispan作为缓存系统了。

1.4.5 JBossCache包

JBoss Cache是Hibernate推荐使用的缓存。在Hibernate中整合JBoss Cache的程序存放在包org.hibernate.cache.jbc中。

1.4.6 其他的包

org.hibernate.impl包存放的是hibernate的核心接口的实现类,如SessionFactoryImpl、SessionImpl、QueryImpl等。这些类是Hibernate具体实现,包含了ORM的一些具体算法,也体现出许多Hibernate的设计思想。也是我们今后分析Hibernate运行的主要对象。

org.hibernate.engine包中的类比较分散,多是由其他包“共享”而来的,并且实现了一些关键的算法。

其他的包就不一一介绍了,如果想了解可去查看API文档资料。

1.5 创建一个简单的**Hibernate**项目

通过前几个小节,我们配置好了开发环境,接下来我们建立一个Web工程。初步认识一下Hibernate。

在建立项目之前,我们先明确一下这个项目的内容。该项目是一个简单的学生信息管理系统。其中有学生实体、班级实体、课程实体。一个班级包含零个或多个学生,一个学生可以修多门课程。一门课程可以被多个学生选择。这个项目名称为students,包含一个一对多的关系:班级与学生;也包含一个多对多的关系:学生与课程。这个项目使用MySQL数据库存储数据。

1.5.1**MySQL中创建表**

根据前面的分析,我们需要创建4张表:班级表、学生表、课程表、学生课程映射表。表结构图如下:

表1-1 班级表clazz

名称

字段名

类型

长度

备注

ID

id

int

自动增长

班级名称

name

varchar

100

表1-2学生表student

名称

字段名

类型

长度

备注

ID

id

int

自动增长

学号

number

varchar

50

姓名

name

varchar

20

年龄

age

int

班级ID

clazz_id

int

表1-3课程表course

名称

字段名

类型

长度

备注

ID

id

int

自动增长

课程名称

name

varchar

100

表1-4学生课程表std_cor

名称

字段名

类型

长度

备注

ID

id

int

自动增长

课程ID

course_id

int

学生ID

std_id

int

1.5.2**创建一个**Web Project

使用Eclipse的创建工程向导建立一个Web工程。在工程名称中输入students,作为我们的工程名。其余选项采用默认设置。如图1-16 。

图1-16 创建Java工程向导

把以下包加入到WebContent/WEB-INFO/lib下,并加入到ClASSPATH中:antlr-2.7.6.jar,c3p0-0.9.1.jar、cglib-2.2.jar、commons-collections-3.1.jar、commons-logging-1.1.jar、dom4j-1.6.1.jar、ehcache-1.5.0.jar、hibernate-jpa-2.0-api-1.0.0.Final.jar、hibernate-testing.jar、hibernate3.jar、infinispan-core-4.0.0.FINAL.jar、javaee.jar、javassist-3.9.0.GA.jar、jbosscache-core-3.2.1.GA.jar、jta-1.1.jar、log4j-1.2.14.jar、mysql-connector-java-5.0.8-bin.jar、oscache-2.1.jar、proxool-0.8.3.jar、slf4j-api-1.5.8.jar、slf4j-log4j12-1.4.2.jar、swarmcache-1.0RC2.jar。

创建存放pojo的包org.st.pojo,存放dao的包org.st.dao。

1.5.3 配置**Hibernate**

首先用向导创建一个Hibernate Configuration File(cfg.xml),如图1-17。

图1-17 创建Hibernate配置文档

选择Hibernate配置文件的存放路径。我们将其存放在students工程的源代码目录中,即在src目录中。点击Next进入下一步:

图1-18 选择Hibernate配置文件的存放路径

图1-19 设置Hibernate配置文件

图1-20创建并配置Hibernate Console

图1-21 配置Hibernate Console的classpath

创建并设置好Hibernate的配置文件后。将Eclipse切换到Hibernate视图。在Hibernate Configurations窗口中可以看到刚才配置的students,以及数据库中的表信息,如图1-22。说明刚才的配置是正确的。如果不能看到数据库中的表,请检查您的配置文件,看看什么地方设置错了。如果没有看到Hibernate Configurations窗口,可以选择Windows-Show View-Other打开该窗口。

图1-22 Hibernate Configurations窗口

1.5.4 反向工程

接下来将我们介绍如何使用Hibernate Code Generation产生Hibernate映射文件和Java类文件。首先将Eclipse切换到Hibernate视图,在工具栏中可以看到如图1-23的下拉按钮,点击后会有Hibernate Code Generation Configurations项目。

图1-23 反向工程按在工具栏上的位置

选择该项。弹出如图1-24的窗口。点击左侧窗口的添加按钮,增加一个Hibernate反向工程配置文档。在Main选项卡中设置好名称、工程名称、生成的Hibernate文档及Java文件存放路径,包名等。

图1-24配置反向工程

点击Setup按钮设置reveng.xml项目,选择好存放路径。如果Database schema中为空,可以点击其下面的Refresh按钮刷新。然后选中其中的表,点击Include按钮将其导入到Table filters中。如果点击Exclude,在反向工程时会将其排除在外,不对其反向。点击“Finish”完成设置。如图1-25。

图1-25 反向工程过滤设置

切换到Exporters选项卡。如图1-26 所示。

图1-26 设置导出对象

选择导出项目,这里我们选择Domain Code、Hibernate XML Mappings和DAO code,这样一来我们在运行反向工程时,只会生成与表相对应的pojo Java类文件、Hibernate映射文件和DAO类文件。

点击Apply保存以上设置。点击Run运行反向工程。运行成功后,工程的源代码目录中会生成如图1-27 所示的文件:

图1-27 反向工程后生成的文件

其中Student.java等文件是POJO对象,StudentHome.java等是DAO,Student.hbm.xml等是实体映射文件,hibernate.cfg.xml是Hibernate的配置文件,里面包含数据库连接等参数。其中POJO、DAO、映射文件等名称的生成规则可以通过指定命名策略来改变。为了便于查看代码,另外建立一个包org.st.dao专门存放DAO。将反向工程生成的StudentHome.java放入到这个包中来。在反向工程时,学生与课程的多对多的关系被分成了两个一对多的关系。我们可以手工修改一下Student.hbm.xml和Course.hbm.xml文件。修改成多对多的关系。如代码清单1-1和代码清单1-2。

代码清单1-1 实体Student的映射文件

`










`

代码清单1-2 实体Course的映射文件

`







`
代码清单1-2 实体Course的映射文件

下面我们创建一个SessionFactory。用来初始化Hibernate,并给DAO提供Session。如代码清单1-3所示。

代码清单1-3 SessionFactory

public class HibernateSessionFactory {

`

private static String resource = “/hibernate.cfg.xml”;

private static SessionFactory sessionFactory;

static {

try {

sessionFactory = new Configuration().configure(resource).buildSessionFactory();

} catch (Exception e) {

System.err.println(“%%%% Error Creating sessionFactory %%%%”);

e.printStackTrace();

}

}

private HibernateSessionFactory() {

}

public static SessionFactory getSessionFactory() {

return sessionFactory;

}

`

}
在用Hibernate插件反向工程生成的DAO中,我们可以看到其获得SessionFactory的代码如代码清单1-4所示。SessionFactory是从容器的上下文中获得的。所以,当程序运行时,我们要向容器的上下文中放入一个SessionFactory。

代码清单1-4 DAO是获得SessionFactory的方法
protected SessionFactory getSessionFactory() {

`

try {

return (SessionFactory) new InitialContext()

.lookup(“SessionFactory”);

} catch (Exception e) {

log.error(“Could not locate SessionFactory in JNDI”, e);

throw new IllegalStateException(“Could not locate SessionFactory in JNDI”);

}

`

}

为了向容器的上下文中放入一个SessionFactory。我们可以在Web应用中注册一个监听器。在应用启动时由监听器向容器中放入一个SessionFactory。监听器配置见代码清单1-5。监听器代码如代码清单1-6 所示。

代码清单1-5 配置加载Hibernate的监听器

org.st.HibernateContextLoaderListener

代码清单1-6 Hibernate监听器

public class HibernateContextLoaderListener implements ServletContextListener{

`

private InitialContext initialContext;

/**

  • Default constructor.

  • @throws NamingException

*/

public HibernateContextLoaderListener() throws NamingException {

initialContext = new InitialContext();

}

@Override

public void contextDestroyed(ServletContextEvent arg0) {

try {

initialContext.destroySubcontext(“SessionFactory”);

} catch (NamingException e) {

e.printStackTrace();

}

}

@Override

public void contextInitialized(ServletContextEvent arg0) {

try {

initialContext.bind(“SessionFactory”, HibernateSessionFactory.getSessionFactory());

} catch (NamingException e) {

e.printStackTrace();

}

}

`

}

接下来我们创建几个jsp页面。使用刚才生成的DAO,保存我的实体类。具体代码请查看随书光盘Students项目。

项目中包含一个一对多的关系——班级对学生和一个多对多的关系——学生对课程。从liststudents.jsp中保存实体的代码如代码清单1-7所示。一个学生属于一个班级,所以对一个学生要创建一个班级实体对象clazz,此处是根据班级的id从数据库中查找出来的。一个学生可以选择多门课程,所以要为一个学生创建一个课程集合courses,然后根据课程的id从数据库中查找出课程放入到courses集合中。最后是调用studentHome保存学生实体。

代码清单1-7 保存Student的代码

//创建学生实体

`

Student s = new Student();

s.setNumber(number);

s.setName(name);

s.setAge(Integer.parseInt(age));

//创建班级实体。班级实体与学生实体是一对多的关系。

Clazz clazz = null;

if(clazzid != null && !””.equals(clazzid)){

clazz = clazzHome.findById(Integer.parseInt(clazzid));

s.setClazz(clazz);

}

//创建多个课程实体,课程实体与学生实体是多对多的关系

if(courseids != null){

Set courses = s.getCourses();

for(String courseid : courseids){

courses.add(courseHome.findById(Integer.parseInt(courseid)));

}

}

//保存学生实体

`

studentHome.persist(s);

1.6 创建一个简单的SSH**项目**

SSH项目是Struts+Spring+Hibernate的有机整合。利用三框架将J2EE项目分成表现层、业务层、持久层等。为了便于结合着Struts2.0和Spring讲解Hibernate。我们需将第1.5节的students例子改造一下,加入Struts和Spring框架,使之成为一个SSH项目。

首先向项目中加入Struts包和Spring包。并将其加入到CLASSPATH中。

1.6.1 配置**Struts**

在web.xml中增加Struts过滤器FilterDispatcher,如代码清单1-8。

代码清单1-8 配置Struts过滤器

`

struts

org.apache.struts2.dispatcher.FilterDispatcher

struts

*.action
`
增加完过滤器后,在src源代码目录中创建一个struts的配置文件,代码清单1-9。内容如下:

其中struts.objectFactory的值设置为spring,这样一来Struts配置文件中的action可交于Spring来管理,也就是说Struts可以引用Spring管理的Bean。

代码清单1-9 Struts的配置文件

`<?xml version=”1.0” encoding=”UTF-8”?>
<!DOCTYPE struts PUBLIC
“-//Apache Software Foundation//DTD Struts Configuration 2.0//EN”
http://struts.apache.org/dtds/struts-2.0.dtd">



`

1.6.2 配置**Spring**

配置Spring主要是配置一个监听器,当应用启动后,初始化Spring的上下文。其中需要一个上下文参数contextConfigLocation,用以指明Spring配置文档所在位置。这里采用classpath的方式。我们将applicationContext.xml放在src目录下。如代码清单1-10。

代码清单1-10 加载Spring的监听器

`

contextConfigLocation


classpath:/applicationContext*.xml

org.springframework.web.context.ContextLoaderListener

org.springframework.web.context.request.RequestContextListener

`

1.6.3**配置**Hibernate

有了Spring框架,我们可以将Hibernate的SessionFactory放入到Spring配置文件中进行管理。Spring为了集成Hibernate,提供了一些现成的SessionFactory。如:LocalSessionFactoryBean,AnnotationSessionFactoryBean等。现在我们采用AnnotationSessionFactoryBean配置Hibernate的SessionFactory,代码如清单1-11所示。使用AnnotationSessionFactoryBean必须有一个数据库连接池,我们在此使用开源的数据源数据库连接池,配置数据源如代码清单,代码如清单1-12所示。

代码清单1-11 在Spring配置SessionFactory

`



<!—指定映射文件存放路径 –>

classpath:/org/st/pojo





<!—使用的数据库方言 –>


org.hibernate.dialect.MySQL5Dialect

<!—是否使用二级缓存 –>

false


`

代码清单1-12 在Spring中配置数据源

`








`

至此,Struts+Spring+Hibernate框架整合完毕。接下来就可以将原来的代码拆分到各个不同的功能层次中去,使代码显得层次分明、结构条理、易于维护。具体的修改过程在此就不一一介绍了。需要的读者可查看本书附带的源码。

1.7 小结

本章主要讲述了如何搭建一个学习和开发Hibernate的环境,并讲述了Hibernate的结构。然后在此基础上创建一个Hibernate工程students。在此工程中展示了如何使用Hibernate。最后又对students工程进行完善,整合Struts框架和Spring框架。至此,students成为一个完整的SSH项目。

新企业初始化

declare @blue varchar(40); declare @uid decimal(10,0); declare @eid int; declare cur_blue cursor for select EID,DeviceType+’$’+Bluetooth from [EnterpriseBluetooth]; open cur_blue fetch next from cur_blue into @eid,@blue; WHILE @@FETCH_STATUS = 0 BEGIN declare cur_user cursor for select U_ID from [UserEx] where BluetoothAddr like ‘%‘+@blue+’%’; open cur_user fetch next from cur_user into @uid; WHILE @@FETCH_STATUS = 0 BEGIN update [User] set Enterprise_ID=@eid where ID=@uid and (Enterprise_ID is null or Enterprise_ID =0); fetch next from cur_user into @uid; END close cur_user deallocate cur_user fetch next from cur_blue into @eid,@blue; END close cur_blue deallocate cur_blue

Java中常用的加密方法(JDK)

加密,是以某种特殊的算法改变原有的信息数据,使得未授权的用户即使获得了已加密的信息,但因不知解密的方法,仍然无法了解信息的内容。大体上分为双向加密单向加密,而双向加密又分为对称加密非对称加密(有些资料将加密直接分为对称加密和非对称加密)。 双向加密大体意思就是明文加密后形成密文,可以通过算法还原成明文。而单向加密只是对信息进行了摘要计算,不能通过算法生成明文,单向加密从严格意思上说不能算是加密的一种,应该算是摘要算法吧。具体区分可以参考: (本人解释不清呢 …… ) http://security.group.iteye.com/group/wiki/1710-one-way-encryption-algorithm 一、双向加密 (一)、对称加密 采用单钥密码系统的加密方法,同一个密钥可以同时用作信息的加密和解密,这种加密方法称为对称加密,也称为单密钥加密。 需要对加密和解密使用相同密钥的加密算法。由于其速度,对称性加密通常在消息发送方需要加密大量数据时使用。对称性加密也称为密钥加密。 所谓对称,就是采用这种加密方法的双方使用方式用同样的密钥进行加密和解密。密钥是控制加密及解密过程的指令。 算法是一组规则,规定如何进行加密和解密。因此对称式加密本身不是安全的。 常用的对称加密有:DES、IDEA、RC2、RC4、SKIPJACK、RC5、AES算法等 对称加密一般java类中中定义成员

Java代码 收藏代码

  1. //KeyGenerator 提供对称密钥生成器的功能,支持各种算法
  2. private KeyGenerator keygen;
  3. //SecretKey 负责保存对称密钥
  4. private SecretKey deskey;
  5. //Cipher负责完成加密或解密工作
  6. private Cipher c;
  7. //该字节数组负责保存加密的结果
  8. private byte[] cipherByte;

在构造函数中初始化

Java代码 收藏代码

  1. Security.addProvider(new com.sun.crypto.provider.SunJCE());
  2. //实例化支持DES算法的密钥生成器(算法名称命名需按规定,否则抛出异常)
  3. keygen = KeyGenerator.getInstance(“DES”);//
  4. //生成密钥
  5. deskey = keygen.generateKey();
  6. //生成Cipher对象,指定其支持的DES算法
  7. c = Cipher.getInstance(“DES”);

1. DES算法为密码体制中的对称密码体制,又被成为美国数据加密标准,是1972年美国IBM公司研制的对称密码体制加密算法。 明文按64位进行分组, 密钥长64位,密钥事实上是56位参与DES运算(第8、16、24、32、40、48、56、64位是校验位, 使得每个密钥都有奇数个1)分组后的明文组和56位的密钥按位替代或交换的方法形成密文组的加密方法。

Java代码 收藏代码

  1. import java.security.InvalidKeyException;
  2. import java.security.NoSuchAlgorithmException;
  3. import java.security.Security;

  4. import javax.crypto.BadPaddingException;

  5. import javax.crypto.Cipher;
  6. import javax.crypto.IllegalBlockSizeException;
  7. import javax.crypto.KeyGenerator;
  8. import javax.crypto.NoSuchPaddingException;
  9. import javax.crypto.SecretKey;

  10. public class EncrypDES {

  11. //KeyGenerator 提供对称密钥生成器的功能,支持各种算法

  12. private KeyGenerator keygen;
  13. //SecretKey 负责保存对称密钥
  14. private SecretKey deskey;
  15. //Cipher负责完成加密或解密工作
  16. private Cipher c;
  17. //该字节数组负责保存加密的结果
  18. private byte[] cipherByte;

  19. public EncrypDES() throws NoSuchAlgorithmException, NoSuchPaddingException{

  20. Security.addProvider(new com.sun.crypto.provider.SunJCE());
  21. //实例化支持DES算法的密钥生成器(算法名称命名需按规定,否则抛出异常)
  22. keygen = KeyGenerator.getInstance(“DES”);
  23. //生成密钥
  24. deskey = keygen.generateKey();
  25. //生成Cipher对象,指定其支持的DES算法
  26. c = Cipher.getInstance(“DES”);
  27. }

  28. /**

    • 对字符串加密
  29. *
    • @param str
    • @return
    • @throws InvalidKeyException
    • @throws IllegalBlockSizeException
    • @throws BadPaddingException
  30. */
  31. public byte[] Encrytor(String str) throws InvalidKeyException,
  32. IllegalBlockSizeException, BadPaddingException {
  33. // 根据密钥,对Cipher对象进行初始化,ENCRYPT_MODE表示加密模式
  34. c.init(Cipher.ENCRYPT_MODE, deskey);
  35. byte[] src = str.getBytes();
  36. // 加密,结果保存进cipherByte
  37. cipherByte = c.doFinal(src);
  38. return cipherByte;
  39. }

  40. /**

    • 对字符串解密
  41. *
    • @param buff
    • @return
    • @throws InvalidKeyException
    • @throws IllegalBlockSizeException
    • @throws BadPaddingException
  42. */
  43. public byte[] Decryptor(byte[] buff) throws InvalidKeyException,
  44. IllegalBlockSizeException, BadPaddingException {
  45. // 根据密钥,对Cipher对象进行初始化,DECRYPT_MODE表示加密模式
  46. c.init(Cipher.DECRYPT_MODE, deskey);
  47. cipherByte = c.doFinal(buff);
  48. return cipherByte;
  49. }

  50. /**

    • @param args
    • @throws NoSuchPaddingException
    • @throws NoSuchAlgorithmException
    • @throws BadPaddingException
    • @throws IllegalBlockSizeException
    • @throws InvalidKeyException
  51. */
  52. public static void main(String[] args) throws Exception {
  53. EncrypDES de1 = new EncrypDES();
  54. String msg =”郭XX-搞笑相声全集”;
  55. byte[] encontent = de1.Encrytor(msg);
  56. byte[] decontent = de1.Decryptor(encontent);
  57. System.out.println(“明文是:” + msg);
  58. System.out.println(“加密后:” + new String(encontent));
  59. System.out.println(“解密后:” + new String(decontent));
  60. }

  61. }

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)))

Java代码 收藏代码

  1. import java.security.InvalidKeyException;
  2. import java.security.NoSuchAlgorithmException;
  3. import java.security.Security;

  4. import javax.crypto.BadPaddingException;

  5. import javax.crypto.Cipher;
  6. import javax.crypto.IllegalBlockSizeException;
  7. import javax.crypto.KeyGenerator;
  8. import javax.crypto.NoSuchPaddingException;
  9. import javax.crypto.SecretKey;

  10. public class EncrypDES3 {

  11. // KeyGenerator 提供对称密钥生成器的功能,支持各种算法

  12. private KeyGenerator keygen;
  13. // SecretKey 负责保存对称密钥
  14. private SecretKey deskey;
  15. // Cipher负责完成加密或解密工作
  16. private Cipher c;
  17. // 该字节数组负责保存加密的结果
  18. private byte[] cipherByte;

  19. public EncrypDES3() throws NoSuchAlgorithmException, NoSuchPaddingException {

  20. Security.addProvider(new com.sun.crypto.provider.SunJCE());
  21. // 实例化支持DES算法的密钥生成器(算法名称命名需按规定,否则抛出异常)
  22. keygen = KeyGenerator.getInstance(“DESede”);
  23. // 生成密钥
  24. deskey = keygen.generateKey();
  25. // 生成Cipher对象,指定其支持的DES算法
  26. c = Cipher.getInstance(“DESede”);
  27. }

  28. /**

    • 对字符串加密
  29. *
    • @param str
    • @return
    • @throws InvalidKeyException
    • @throws IllegalBlockSizeException
    • @throws BadPaddingException
  30. */
  31. public byte[] Encrytor(String str) throws InvalidKeyException,
  32. IllegalBlockSizeException, BadPaddingException {
  33. // 根据密钥,对Cipher对象进行初始化,ENCRYPT_MODE表示加密模式
  34. c.init(Cipher.ENCRYPT_MODE, deskey);
  35. byte[] src = str.getBytes();
  36. // 加密,结果保存进cipherByte
  37. cipherByte = c.doFinal(src);
  38. return cipherByte;
  39. }

  40. /**

    • 对字符串解密
  41. *
    • @param buff
    • @return
    • @throws InvalidKeyException
    • @throws IllegalBlockSizeException
    • @throws BadPaddingException
  42. */
  43. public byte[] Decryptor(byte[] buff) throws InvalidKeyException,
  44. IllegalBlockSizeException, BadPaddingException {
  45. // 根据密钥,对Cipher对象进行初始化,DECRYPT_MODE表示加密模式
  46. c.init(Cipher.DECRYPT_MODE, deskey);
  47. cipherByte = c.doFinal(buff);
  48. return cipherByte;
  49. }

  50. /**

    • @param args
    • @throws NoSuchPaddingException
    • @throws NoSuchAlgorithmException
    • @throws BadPaddingException
    • @throws IllegalBlockSizeException
    • @throws InvalidKeyException
  51. */
  52. public static void main(String[] args) throws Exception {
  53. EncrypDES3 des = new EncrypDES3();
  54. String msg =”郭XX-搞笑相声全集”;
  55. byte[] encontent = des.Encrytor(msg);
  56. byte[] decontent = des.Decryptor(encontent);
  57. System.out.println(“明文是:” + msg);
  58. System.out.println(“加密后:” + new String(encontent));
  59. System.out.println(“解密后:” + new String(decontent));

  60. }

  61. }

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”。)

Java代码 收藏代码

  1. import java.security.InvalidKeyException;
  2. import java.security.NoSuchAlgorithmException;
  3. import java.security.Security;

  4. import javax.crypto.BadPaddingException;

  5. import javax.crypto.Cipher;
  6. import javax.crypto.IllegalBlockSizeException;
  7. import javax.crypto.KeyGenerator;
  8. import javax.crypto.NoSuchPaddingException;
  9. import javax.crypto.SecretKey;

  10. public class EncrypAES {

  11. //KeyGenerator 提供对称密钥生成器的功能,支持各种算法

  12. private KeyGenerator keygen;
  13. //SecretKey 负责保存对称密钥
  14. private SecretKey deskey;
  15. //Cipher负责完成加密或解密工作
  16. private Cipher c;
  17. //该字节数组负责保存加密的结果
  18. private byte[] cipherByte;

  19. public EncrypAES() throws NoSuchAlgorithmException, NoSuchPaddingException{

  20. Security.addProvider(new com.sun.crypto.provider.SunJCE());
  21. //实例化支持DES算法的密钥生成器(算法名称命名需按规定,否则抛出异常)
  22. keygen = KeyGenerator.getInstance(“AES”);
  23. //生成密钥
  24. deskey = keygen.generateKey();
  25. //生成Cipher对象,指定其支持的DES算法
  26. c = Cipher.getInstance(“AES”);
  27. }

  28. /**

    • 对字符串加密
  29. *
    • @param str
    • @return
    • @throws InvalidKeyException
    • @throws IllegalBlockSizeException
    • @throws BadPaddingException
  30. */
  31. public byte[] Encrytor(String str) throws InvalidKeyException,
  32. IllegalBlockSizeException, BadPaddingException {
  33. // 根据密钥,对Cipher对象进行初始化,ENCRYPT_MODE表示加密模式
  34. c.init(Cipher.ENCRYPT_MODE, deskey);
  35. byte[] src = str.getBytes();
  36. // 加密,结果保存进cipherByte
  37. cipherByte = c.doFinal(src);
  38. return cipherByte;
  39. }

  40. /**

    • 对字符串解密
  41. *
    • @param buff
    • @return
    • @throws InvalidKeyException
    • @throws IllegalBlockSizeException
    • @throws BadPaddingException
  42. */
  43. public byte[] Decryptor(byte[] buff) throws InvalidKeyException,
  44. IllegalBlockSizeException, BadPaddingException {
  45. // 根据密钥,对Cipher对象进行初始化,DECRYPT_MODE表示加密模式
  46. c.init(Cipher.DECRYPT_MODE, deskey);
  47. cipherByte = c.doFinal(buff);
  48. return cipherByte;
  49. }

  50. /**

    • @param args
    • @throws NoSuchPaddingException
    • @throws NoSuchAlgorithmException
    • @throws BadPaddingException
    • @throws IllegalBlockSizeException
    • @throws InvalidKeyException
  51. */
  52. public static void main(String[] args) throws Exception {
  53. EncrypAES de1 = new EncrypAES();
  54. String msg =”郭XX-搞笑相声全集”;
  55. byte[] encontent = de1.Encrytor(msg);
  56. byte[] decontent = de1.Decryptor(encontent);
  57. System.out.println(“明文是:” + msg);
  58. System.out.println(“加密后:” + new String(encontent));
  59. System.out.println(“解密后:” + new String(decontent));
  60. }

  61. }

(二)、非对称加密 1976年,美国学者Dime和Henman为解决信息公开传送和密钥管理问题,提出一种新的密钥交换协议,允许在不安全的媒体上的通讯双方交换信息,安全地达成一致的密钥,这就是“公开密钥系统”。相对于“对称加密算法”这种方法也叫做“非对称加密算法”。 与对称加密算法不同,非对称加密算法需要两个密钥:公开密钥(publickey)和私有密钥 (privatekey)。公开密钥与私有密钥是一对,如果用公开密钥对数据进行加密,只有用对应的私有密钥才能解密;如果用私有密钥对数据进行加密,那么只有用对应的公开密钥才能解密。因为加密和解密使用的是两个不同的密钥,所以这种算法叫作非对称加密算法。 1. RSA 公钥加密算法是1977年由Ron Rivest、Adi Shamirh和LenAdleman在(美国麻省理工学院)开发的。RSA取名来自开发他们三者的名字。RSA是目前最有影响力的公钥加密算法,它能够抵抗到目前为止已知的所有密码攻击,已被ISO推荐为公钥数据加密标准。RSA算法基于一个十分简单的数论事实:将两个大素数相乘十分容易,但那时想要对其乘积进行因式分解却极其困难,因此可以将乘积公开作为加密密钥。

Java代码 收藏代码

  1. import java.security.InvalidKeyException;
  2. import java.security.KeyPair;
  3. import java.security.KeyPairGenerator;
  4. import java.security.NoSuchAlgorithmException;
  5. import java.security.interfaces.RSAPrivateKey;
  6. import java.security.interfaces.RSAPublicKey;

  7. import javax.crypto.BadPaddingException;

  8. import javax.crypto.Cipher;
  9. import javax.crypto.IllegalBlockSizeException;
  10. import javax.crypto.NoSuchPaddingException;

  11. public class EncrypRSA {

  12. /**

    • 加密
    • @param publicKey
    • @param srcBytes
    • @return
    • @throws NoSuchAlgorithmException
    • @throws NoSuchPaddingException
    • @throws InvalidKeyException
    • @throws IllegalBlockSizeException
    • @throws BadPaddingException
  13. */
  14. protected byte[] encrypt(RSAPublicKey publicKey,byte[] srcBytes) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException{
  15. if(publicKey!=null){
  16. //Cipher负责完成加密或解密工作,基于RSA
  17. Cipher cipher = Cipher.getInstance(“RSA”);
  18. //根据公钥,对Cipher对象进行初始化
  19. cipher.init(Cipher.ENCRYPT_MODE, publicKey);
  20. byte[] resultBytes = cipher.doFinal(srcBytes);
  21. return resultBytes;
  22. }
  23. return null;
  24. }

  25. /**

    • 解密
    • @param privateKey
    • @param srcBytes
    • @return
    • @throws NoSuchAlgorithmException
    • @throws NoSuchPaddingException
    • @throws InvalidKeyException
    • @throws IllegalBlockSizeException
    • @throws BadPaddingException
  26. */
  27. protected byte[] decrypt(RSAPrivateKey privateKey,byte[] srcBytes) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException{
  28. if(privateKey!=null){
  29. //Cipher负责完成加密或解密工作,基于RSA
  30. Cipher cipher = Cipher.getInstance(“RSA”);
  31. //根据公钥,对Cipher对象进行初始化
  32. cipher.init(Cipher.DECRYPT_MODE, privateKey);
  33. byte[] resultBytes = cipher.doFinal(srcBytes);
  34. return resultBytes;
  35. }
  36. return null;
  37. }

  38. /**

    • @param args
    • @throws NoSuchAlgorithmException
    • @throws BadPaddingException
    • @throws IllegalBlockSizeException
    • @throws NoSuchPaddingException
    • @throws InvalidKeyException
  39. */
  40. public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeyException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException {
  41. EncrypRSA rsa = new EncrypRSA();
  42. String msg = “郭XX-精品相声”;
  43. //KeyPairGenerator类用于生成公钥和私钥对,基于RSA算法生成对象
  44. KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(“RSA”);
  45. //初始化密钥对生成器,密钥大小为1024位
  46. keyPairGen.initialize(1024);
  47. //生成一个密钥对,保存在keyPair中
  48. KeyPair keyPair = keyPairGen.generateKeyPair();
  49. //得到私钥
  50. RSAPrivateKey privateKey = (RSAPrivateKey)keyPair.getPrivate();
  51. //得到公钥
  52. RSAPublicKey publicKey = (RSAPublicKey)keyPair.getPublic();

  53. //用公钥加密

  54. byte[] srcBytes = msg.getBytes();
  55. byte[] resultBytes = rsa.encrypt(publicKey, srcBytes);

  56. //用私钥解密

  57. byte[] decBytes = rsa.decrypt(privateKey, resultBytes);

  58. System.out.println(“明文是:” + msg);

  59. System.out.println(“加密后是:” + new String(resultBytes));
  60. System.out.println(“解密后是:” + new String(decBytes));
  61. }

  62. }

2. DSA Digital Signature Algorithm (DSA)是Schnorr和ElGamal签名算法的变种,被美国NIST作为DSS(DigitalSignature Standard)。(感觉有点复杂,没有附代码) 详见http://63938525.iteye.com/blog/1051565 (三)、题外话 MySQL加密解密函数 MySQL有两个函数来支持这种类型的加密,分别叫做ENCODE()和DECODE()。 下面是一个简单的实例:

Mysql代码 收藏代码

  1. mysql> INSERT INTO users (username,password) VALUES (‘joe’,ENCODE(‘guessme’,’abr’));

  2. Query OK, 1 row affected (0.14 sec)

其中,Joe的密码是guessme,它通过密钥abracadabra被加密。要注意的是,加密完的结果是一个二进制字符串,如下所示: 提示:虽然ENCODE()和DECODE()这两个函数能够满足大多数的要求,但是有的时候您希望使用强度更高的加密手段。在这种情况下,您可以使用AES_ENCRYPT()和AES_DECRYPT()函数,它们的工作方式是相同的,但是加密强度更高。 单向加密与双向加密不同,一旦数据被加密就没有办法颠倒这一过程。因此密码的验证包括对用户输入内容的重新加密,并将它与保存的密文进行比对,看是否匹配。一种简单的单向加密方式是MD5校验码。MySQL的MD5()函数会为您的数据创建一个“指纹”并将它保存起来,供验证测试使用。下面就是如何使用它的一个简单例子:

Mysql代码 收藏代码

  1. mysql> INSERT INTO users (username,password) VALUES (‘joe’,MD5(‘guessme’));

  2. Query OK, 1 row affected (0.00 sec)

或者,您考虑一下使用ENCRYPT()函数,它使用系统底层的crypt()系统调用来完成加密。这个函数有两个参数:一个是要被加密的字符串,另一个是双(或者多)字符的“salt”。它然后会用salt加密字符串;这个salt然后可以被用来再次加密用户输入的内容,并将它与先前加密的字符串进行比对。下面一个例子说明了如何使用它:

Mysql代码 收藏代码

  1. mysql> INSERT INTO users (username,password) VALUES(‘joe’, ENCRYPT(‘guessme’,’ab’));

  2. Query OK, 1 row affected (0.00 sec)

提示:ENCRYPT()只能用在UNIX、LINIX系统上,因为它需要用到底层的crypt()库。 二、单向加密(信息摘要) Java一般需要获取对象MessageDigest来实现单项加密(信息摘要)。 1. MD5 即Message-Digest Algorithm 5(信息-摘要算法 5),用于确保信息传输完整一致。是计算机广泛使用的杂凑算法之一(又译摘要算法、哈希算法),主流编程语言普遍已有MD5实现。将数据(如汉字)运算为另一固定长度值,是杂凑算法的基础原理,MD5的前身有MD2、MD3和MD4。MD5的作用是让大容量信息在用数字签名软件签署私人密钥前被”压缩”成一种保密的格式(就是把一个任意长度的字节串变换成一定长的十六进制数字串)。 除了MD5以外,其中比较有名的还有sha-1、RIPEMD以及Haval等

Java代码 收藏代码

  1. import java.security.MessageDigest;
  2. import java.security.NoSuchAlgorithmException;

  3. public class EncrypMD5 {

  4. public byte[] eccrypt(String info) throws NoSuchAlgorithmException{

  5. //根据MD5算法生成MessageDigest对象
  6. MessageDigest md5 = MessageDigest.getInstance(“MD5”);
  7. byte[] srcBytes = info.getBytes();
  8. //使用srcBytes更新摘要
  9. md5.update(srcBytes);
  10. //完成哈希计算,得到result
  11. byte[] resultBytes = md5.digest();
  12. return resultBytes;
  13. }

  14. public static void main(String args[]) throws NoSuchAlgorithmException{

  15. String msg = “郭XX-精品相声技术”;
  16. EncrypMD5 md5 = new EncrypMD5();
  17. byte[] resultBytes = md5.eccrypt(msg);

  18. System.out.println(“密文是:” + new String(resultBytes));

  19. System.out.println(“明文是:” + msg);
  20. }

  21. }

2. SHA 是一种数据加密算法,该算法经过加密专家多年来的发展和改进已日益完善,现在已成为公认的最安全的散列算法之一,并被广泛使用。该算法的思想是接收一段明文,然后以一种不可逆的方式将它转换成一段(通常更小)密文,也可以简单的理解为取一串输入码(称为预映射或信息),并把它们转化为长度较短、位数固定的输出序列即散列值(也称为信息摘要或信息认证代码)的过程。散列函数值可以说时对明文的一种“指纹”或是“摘要”所以对散列值的数字签名就可以视为对此明文的数字签名。

Java代码 收藏代码

  1. import java.security.MessageDigest;
  2. import java.security.NoSuchAlgorithmException;

  3. public class EncrypSHA {

  4. public byte[] eccrypt(String info) throws NoSuchAlgorithmException{

  5. MessageDigest md5 = MessageDigest.getInstance(“SHA”);
  6. byte[] srcBytes = info.getBytes();
  7. //使用srcBytes更新摘要
  8. md5.update(srcBytes);
  9. //完成哈希计算,得到result
  10. byte[] resultBytes = md5.digest();
  11. return resultBytes;
  12. }

  13. /**

    • @param args
    • @throws NoSuchAlgorithmException
  14. */
  15. public static void main(String[] args) throws NoSuchAlgorithmException {
  16. String msg = “郭XX-精品相声技术”;
  17. EncrypSHA sha = new EncrypSHA();
  18. byte[] resultBytes = sha.eccrypt(msg);
  19. System.out.println(“明文是:” + msg);
  20. System.out.println(“密文是:” + new String(resultBytes));

  21. }

  22. }

附件中是以上几种的源代码,附带额外的两种使用方式。 增加一种关于文件的哈希算法源代码:

Java代码 收藏代码

  1. import java.io.FileInputStream;
  2. import java.io.InputStream;
  3. import java.security.MessageDigest;

  4. public class FileHashUtil {

  5. public static final char[] hexChar = {

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

  8. public void MD5File(String fileName) throws Exception{

  9. //String fileName = args[0];
  10. System.out.println(“需要获取hash的文件为: “ + fileName);
  11. java.util.List mds = new java.util.ArrayList();
  12. for (String hashType : hashTypes) {
  13. MessageDigest md = MessageDigest.getInstance(hashType);
  14. mds.add(md);
  15. }
  16. InputStream fis = null;
  17. try {
  18. fis = new FileInputStream(fileName);
  19. byte[] buffer = new byte[1024];
  20. int numRead = 0;
  21. while ((numRead = fis.read(buffer)) > 0) {
  22. for (MessageDigest md : mds) {
  23. md.update(buffer, 0, numRead);
  24. }
  25. }
  26. } catch (Exception ex) {
  27. ex.printStackTrace();
  28. } finally {
  29. if (fis != null) {
  30. fis.close();
  31. }
  32. }
  33. for (MessageDigest md : mds) {
  34. System.out.println(md.getAlgorithm() + “ == “ + toHexString(md.digest()));
  35. }
  36. }

  37. public static void main(String[] args) throws Exception {

  38. String[] fileName = new String[] {“D:/hapfish/ShellFolder.java”,”D:/hapfish/ShellFolder - 副本.java”,
  39. “E:/ShellFolder - 副本.java”,”E:/ShellFolder.txt”,”D:/hapfish/ShellFolder.jpg”,
  40. “E:/ShellFolder增加字符.txt”,”D:/hapfish/birosoft.jar”};
  41. FileHashUtil files = new FileHashUtil();
  42. for(int i=0;i<fileName.length;i++){
  43. files.MD5File(fileName[i]);
  44. }

  45. }

  46. public static String toHexString(byte[] b) {

  47. StringBuilder sb = new StringBuilder(b.length * 2);
  48. for (int i = 0; i < b.length; i++) {
  49. sb.append(hexChar[(b[i] & 0xf0) >>> 4]);
  50. sb.append(hexChar[b[i] & 0x0f]);
  51. }
  52. return sb.toString();
  53. }

  54. }

运行说明

说明代码 收藏代码

  1. “D:/hapfish/ShellFolder.java”,
  2. “D:/hapfish/ShellFolder - 副本.java”,
  3. “E:/ShellFolder - 副本.java”,
  4. “E:/ShellFolder.txt”,
  5. “D:/hapfish/ShellFolder.jpg”,
  6. 以上五个文件是同一文件经过复制、改扩展名的,最后计算哈希结果是一致的。

  7. “E:/ShellFolder增加字符.txt” 增加了几个字符串,就不一样了

  8. “D:/hapfish/birosoft.jar” 完全不相关的另外一个文件

运行结果:

Java代码 收藏代码

  1. 需要获取hash的文件为: D:/hapfish/ShellFolder.java
  2. MD2 == 3a755a99c5e407005cd45ebd856b4649
  3. MD5 == 5d08d440fa911d1e418c69a90b83cd86
  4. SHA1 == 522c8c4f4ff1dd669e251c2ab854c3033a51ca63
  5. SHA-256 == d1feb0c73c10a759e88bd240cb9d56d0598b4ff83a0704c6679f7ba12f6c4d99
  6. SHA-384 == 8f8c9da4cd7241c58af3c52b49199033f2dcf3d67f421753999f87511618d9ea2d738e8c16b9b68a7572d06108ff10f6
  7. SHA-512 == 4711579daee3ddacbaea189310348956cb43bcaaf0099f3be047b06f16c1a20a6b71ee3a4ee018128d647e9f2ef0d644747672238e49a8da3d0cd26dfe597458
  8. 需要获取hash的文件为: D:/hapfish/ShellFolder - 副本.java
  9. MD2 == 3a755a99c5e407005cd45ebd856b4649
  10. MD5 == 5d08d440fa911d1e418c69a90b83cd86
  11. SHA1 == 522c8c4f4ff1dd669e251c2ab854c3033a51ca63
  12. SHA-256 == d1feb0c73c10a759e88bd240cb9d56d0598b4ff83a0704c6679f7ba12f6c4d99
  13. SHA-384 == 8f8c9da4cd7241c58af3c52b49199033f2dcf3d67f421753999f87511618d9ea2d738e8c16b9b68a7572d06108ff10f6
  14. SHA-512 == 4711579daee3ddacbaea189310348956cb43bcaaf0099f3be047b06f16c1a20a6b71ee3a4ee018128d647e9f2ef0d644747672238e49a8da3d0cd26dfe597458
  15. 需要获取hash的文件为: E:/ShellFolder - 副本.java
  16. MD2 == 3a755a99c5e407005cd45ebd856b4649
  17. MD5 == 5d08d440fa911d1e418c69a90b83cd86
  18. SHA1 == 522c8c4f4ff1dd669e251c2ab854c3033a51ca63
  19. SHA-256 == d1feb0c73c10a759e88bd240cb9d56d0598b4ff83a0704c6679f7ba12f6c4d99
  20. SHA-384 == 8f8c9da4cd7241c58af3c52b49199033f2dcf3d67f421753999f87511618d9ea2d738e8c16b9b68a7572d06108ff10f6
  21. SHA-512 == 4711579daee3ddacbaea189310348956cb43bcaaf0099f3be047b06f16c1a20a6b71ee3a4ee018128d647e9f2ef0d644747672238e49a8da3d0cd26dfe597458
  22. 需要获取hash的文件为: E:/ShellFolder.txt
  23. MD2 == 3a755a99c5e407005cd45ebd856b4649
  24. MD5 == 5d08d440fa911d1e418c69a90b83cd86
  25. SHA1 == 522c8c4f4ff1dd669e251c2ab854c3033a51ca63
  26. SHA-256 == d1feb0c73c10a759e88bd240cb9d56d0598b4ff83a0704c6679f7ba12f6c4d99
  27. SHA-384 == 8f8c9da4cd7241c58af3c52b49199033f2dcf3d67f421753999f87511618d9ea2d738e8c16b9b68a7572d06108ff10f6
  28. SHA-512 == 4711579daee3ddacbaea189310348956cb43bcaaf0099f3be047b06f16c1a20a6b71ee3a4ee018128d647e9f2ef0d644747672238e49a8da3d0cd26dfe597458
  29. 需要获取hash的文件为: D:/hapfish/ShellFolder.jpg
  30. MD2 == 3a755a99c5e407005cd45ebd856b4649
  31. MD5 == 5d08d440fa911d1e418c69a90b83cd86
  32. SHA1 == 522c8c4f4ff1dd669e251c2ab854c3033a51ca63
  33. SHA-256 == d1feb0c73c10a759e88bd240cb9d56d0598b4ff83a0704c6679f7ba12f6c4d99
  34. SHA-384 == 8f8c9da4cd7241c58af3c52b49199033f2dcf3d67f421753999f87511618d9ea2d738e8c16b9b68a7572d06108ff10f6
  35. SHA-512 == 4711579daee3ddacbaea189310348956cb43bcaaf0099f3be047b06f16c1a20a6b71ee3a4ee018128d647e9f2ef0d644747672238e49a8da3d0cd26dfe597458
  36. 需要获取hash的文件为: E:/ShellFolder增加字符.txt
  37. MD2 == f2717c24c6c0e110457bd17221c9ca6c
  38. MD5 == c49e353a7c4c26bd7ccb5e90917c230f
  39. SHA1 == 477c8a9e465bfaa4be42d35c032a17f7e6b42b97
  40. SHA-256 == 9fa18adaf242ebcdc6563922d84c2a163c82e1a24db2eb2b73978ed1f354a8a3
  41. SHA-384 == 4eee8f8e6d64d21c15dc01fa049f4d12a3b8e1d94d87763fe0bea75ab5ea8432fa8251289ece45ee39fe3d36b3c3020c
  42. SHA-512 == e852ec0ff77250be497389d2f5a1818c18bb66106b9905c4ee26fe0d256eb3b77e0ce9a28a84e4b67e4332ba37ec3aa7518148e3a682318c0fc34c391f45c201
  43. 需要获取hash的文件为: D:/hapfish/birosoft.jar
  44. MD2 == 38c5e1404718916dec59c33cafc909b3
  45. MD5 == dc3e2cc4fb3949cf3660e0f5f8c3fba3
  46. SHA1 == cde3dc25498afc5a563af0bb0eb54dc45f71bb28
  47. SHA-256 == adf6a961c70c6ea677dff066fc5d896fb0beb4dd442ca0eb619ae1d1b04291e5
  48. SHA-384 == fe7c6b754893c53ebd82bb53703fb5cc32115c9a38f98072f73def90729b271ee3c5c78e258bd9ff5ee5476193c2178b
  49. SHA-512 == a15376f327256a6e049dfbdc5c2ad3a98bffccc6fa92ee01ff53db6b04471ca0f45ca28f76ff4a6911b57825afa046671299141f2499d71f1dac618c92385491

最后,把运行结果贴出来有点占空间,主要为了说明表述自己的猜想。一般来说同一哈希算法对同一文件(镜像、扩展名被修改)所产生的结果应该是一致的。 因此有个猜想,在baidu文库、腾讯的群共享上传时,先会判断是否有相同文件,从某种可能上来说也采用了对文件的哈希算法,毕竟从本地运算一个哈希算法后获得的数值要比把整个文件传过去比较实惠得多。而且字符串的比较也是很方便的。 对于某一种哈希算法,存在一种可能:就是两个不同的文件,计算出来的哈希值可能是一样的。当然为了保险,可以用两种甚至更多的哈希算法,只有在每种算法获得的哈希值都相同时,才能判断是同一个文件。 如果我们也对用户上传的文件进行哈希计算的话,就可以节省资源,同样的文件按理说可以减少上传次数……

Quartz任务调度快速入门

概述

了解Quartz体系结构 Quartz对任务调度的领域问题进行了高度的抽象,提出了调度器、任务和触发器这3个核心的概念,并在org.quartz通过接口和类对重要的这些核心概念进行描述: ●Job:是一个接口,只有一个方法void execute(JobExecutionContext context),开发者实现该接口定义运行任务,JobExecutionContext类提供了调度上下文的各种信息。Job运行时的信息保存在JobDataMap实例中; ●JobDetail:Quartz在每次执行Job时,都重新创建一个Job实例,所以它不直接接受一个Job的实例,相反它接收一个Job实现类,以便运行时通过newInstance()的反射机制实例化Job。因此需要通过一个类来描述Job的实现类及其它相关的静态信息,如Job名字、描述、关联监听器等信息,JobDetail承担了这一角色。 通过该类的构造函数可以更具体地了解它的功用:JobDetail(java.lang.String name, java.lang.String group, java.lang.Class jobClass),该构造函数要求指定Job的实现类,以及任务在Scheduler中的组名和Job名称; ●Trigger:是一个类,描述触发Job执行的时间触发规则。主要有SimpleTrigger和CronTrigger这两个子类。当仅需触发一次或者以固定时间间隔周期执行,SimpleTrigger是最适合的选择;而CronTrigger则可以通过Cron表达式定义出各种复杂时间规则的调度方案:如每早晨9:00执行,周一、周三、周五下午5:00执行等; ●Calendar:org.quartz.Calendar和java.util.Calendar不同,它是一些日历特定时间点的集合(可以简单地将org.quartz.Calendar看作java.util.Calendar的集合——java.util.Calendar代表一个日历时间点,无特殊说明后面的Calendar即指org.quartz.Calendar)。一个Trigger可以和多个Calendar关联,以便排除或包含某些时间点。 假设,我们安排每周星期一早上10:00执行任务,但是如果碰到法定的节日,任务则不执行,这时就需要在Trigger触发机制的基础上使用Calendar进行定点排除。针对不同时间段类型,Quartz在org.quartz.impl.calendar包下提供了若干个Calendar的实现类,如AnnualCalendar、MonthlyCalendar、WeeklyCalendar分别针对每年、每月和每周进行定义; ●Scheduler:代表一个Quartz的独立运行容器,Trigger和JobDetail可以注册到Scheduler中,两者在Scheduler中拥有各自的组及名称,组及名称是Scheduler查找定位容器中某一对象的依据,Trigger的组及名称必须唯一,JobDetail的组和名称也必须唯一(但可以和Trigger的组和名称相同,因为它们是不同类型的)。Scheduler定义了多个接口方法,允许外部通过组及名称访问和控制容器中Trigger和JobDetail。 Scheduler可以将Trigger绑定到某一JobDetail中,这样当Trigger触发时,对应的Job就被执行。一个Job可以对应多个Trigger,但一个Trigger只能对应一个Job。可以通过SchedulerFactory创建一个Scheduler实例。Scheduler拥有一个SchedulerContext,它类似于ServletContext,保存着Scheduler上下文信息,Job和Trigger都可以访问SchedulerContext内的信息。SchedulerContext内部通过一个Map,以键值对的方式维护这些上下文数据,SchedulerContext为保存和获取数据提供了多个put()和getXxx()的方法。可以通过Scheduler# getContext()获取对应的SchedulerContext实例; ●ThreadPool:Scheduler使用一个线程池作为任务运行的基础设施,任务通过共享线程池中的线程提高运行效率。 Job有一个StatefulJob子接口,代表有状态的任务,该接口是一个没有方法的标签接口,其目的是让Quartz知道任务的类型,以便采用不同的执行方案。无状态任务在执行时拥有自己的JobDataMap拷贝,对JobDataMap的更改不会影响下次的执行。而有状态任务共享共享同一个JobDataMap实例,每次任务执行对JobDataMap所做的更改会保存下来,后面的执行可以看到这个更改,也即每次执行任务后都会对后面的执行发生影响。 正因为这个原因,无状态的Job可以并发执行,而有状态的StatefulJob不能并发执行,这意味着如果前次的StatefulJob还没有执行完毕,下一次的任务将阻塞等待,直到前次任务执行完毕。有状态任务比无状态任务需要考虑更多的因素,程序往往拥有更高的复杂度,因此除非必要,应该尽量使用无状态的Job。 如果Quartz使用了数据库持久化任务调度信息,无状态的JobDataMap仅会在Scheduler注册任务时保持一次,而有状态任务对应的JobDataMap在每次执行任务后都会进行保存。 Trigger自身也可以拥有一个JobDataMap,其关联的Job可以通过JobExecutionContext#getTrigger().getJobDataMap()获取Trigger中的JobDataMap。不管是有状态还是无状态的任务,在任务执行期间对Trigger的JobDataMap所做的更改都不会进行持久,也即不会对下次的执行产生影响。 Quartz拥有完善的事件和监听体系,大部分组件都拥有事件,如任务执行前事件、任务执行后事件、触发器触发前事件、触发后事件、调度器开始事件、关闭事件等等,可以注册相应的监听器处理感兴趣的事件。 图1描述了Scheduler的内部组件结构,SchedulerContext提供Scheduler全局可见的上下文信息,每一个任务都对应一个JobDataMap,虚线表达的JobDataMap表示对应有状态的任务:

500){this.resized=true;this.style.width=500;}” align=center>

图1 Scheduler结构图 一个Scheduler可以拥有多个Triger组和多个JobDetail组,注册Trigger和JobDetail时,如果不显式指定所属的组,Scheduler将放入到默认组中,默认组的组名为Scheduler.DEFAULT_GROUP。组名和名称组成了对象的全名,同一类型对象的全名不能相同。 Scheduler本身就是一个容器,它维护着Quartz的各种组件并实施调度的规则。Scheduler还拥有一个线程池,线程池为任务提供执行线程——这比执行任务时简单地创建一个新线程要拥有更高的效率,同时通过共享节约资源的占用。通过线程池组件的支持,对于繁忙度高、压力大的任务调度,Quartz将可以提供良好的伸缩性。 提示: Quartz完整下载包examples目录下拥有10多个实例,它们是快速掌握Quartz应用很好的实例。

使用SimpleTrigger

SimpleTrigger拥有多个重载的构造函数,用以在不同场合下构造出对应的实例: ●SimpleTrigger(String name, String group):通过该构造函数指定Trigger所属组和名称; ●SimpleTrigger(String name, String group, Date startTime):除指定Trigger所属组和名称外,还可以指定触发的开发时间; ●SimpleTrigger(String name, String group, Date startTime, Date endTime, int repeatCount, long repeatInterval):除指定以上信息外,还可以指定结束时间、重复执行次数、时间间隔等参数; ●SimpleTrigger(String name, String group, String jobName, String jobGroup, Date startTime, Date endTime, int repeatCount, long repeatInterval):这是最复杂的一个构造函数,在指定触发参数的同时,还通过jobGroup和jobName,让该Trigger和Scheduler中的某个任务关联起来。 通过实现 org.quartz..Job 接口,可以使 Java 类化身为可调度的任务。代码清单1提供了 Quartz 任务的一个示例: 代码清单1 SimpleJob:简单的Job实现类

package com.baobaotao.basic.quartz; import java.util.Date; import org.quartz.Job; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; public class SimpleJob implements Job { ①实例Job接口方法 public void execute(JobExecutionContext jobCtx)throws JobExecutionException { System.out.println(jobCtx.getTrigger().getName()+ “ triggered. time is:” + (new Date())); } }

这个类用一条非常简单的输出语句实现了Job接口的execute(JobExecutionContext context) 方法,这个方法可以包含想要执行的任何代码。下面,我们通过SimpleTrigger对SimpleJob进行调度: 代码清单2 SimpleTriggerRunner:使用SimpleTrigger进行调度

package com.baobaotao.basic.quartz; import java.util.Date; import org.quartz.JobDetail; import org.quartz.Scheduler; import org.quartz.SchedulerFactory; import org.quartz.SimpleTrigger; import org.quartz.impl.StdSchedulerFactory; public class SimpleTriggerRunner { public static void main(String args[]) { try { ①创建一个JobDetail实例,指定SimpleJob JobDetail jobDetail = new JobDetail(“job1_1”,”jGroup1”, SimpleJob.class); ②通过SimpleTrigger定义调度规则:马上启动,每2秒运行一次,共运行100次 SimpleTrigger simpleTrigger = new SimpleTrigger(“trigger1_1”,”tgroup1”); simpleTrigger.setStartTime(new Date()); simpleTrigger.setRepeatInterval(2000); simpleTrigger.setRepeatCount(100); ③通过SchedulerFactory获取一个调度器实例 SchedulerFactory schedulerFactory = new StdSchedulerFactory(); Scheduler scheduler = schedulerFactory.getScheduler(); scheduler.scheduleJob(jobDetail, simpleTrigger);④ 注册并进行调度 scheduler.start();⑤调度启动 } catch (Exception e) { e.printStackTrace(); } } }

首先在①处通过JobDetail封装SimpleJob,同时指定Job在Scheduler中所属组及名称,这里,组名为jGroup1,而名称为job1_1。 在②处创建一个SimpleTrigger实例,指定该Trigger在Scheduler中所属组及名称。接着设置调度的时间规则。 最后,需要创建Scheduler实例,并将JobDetail和Trigger实例注册到Scheduler中。这里,我们通过StdSchedulerFactory获取一个Scheduler实例,并通过scheduleJob(JobDetail jobDetail, Trigger trigger)完成两件事: 1)将JobDetail和Trigger注册到Scheduler中; 2)将Trigger指派给JobDetail,将两者关联起来。 当Scheduler启动后,Trigger将定期触发并执行SimpleJob的execute(JobExecutionContext jobCtx)方法,然后每 10 秒重复一次,直到任务被执行 100 次后停止。 还可以通过SimpleTrigger的setStartTime(java.util.Date startTime)和setEndTime(java.util.Date endTime)指定运行的时间范围,当运行次数和时间范围冲突时,超过时间范围的任务运行不被执行。如可以通过simpleTrigger.setStartTime(new Date(System.currentTimeMillis() + 60000L))指定60秒钟以后开始。 除了通过scheduleJob(jobDetail, simpleTrigger)建立Trigger和JobDetail的关联,还有另外一种关联Trigger和JobDetail的方式:

JobDetail jobDetail = new JobDetail(“job1_1”,”jGroup1”, SimpleJob.class); SimpleTrigger simpleTrigger = new SimpleTrigger(“trigger1_1”,”tgroup1”); simpleTrigger.setJobGroup(“jGroup1”);①-1:指定关联的Job组名 simpleTrigger.setJobName(“job1_1”);①-2:指定关联的Job名称 scheduler.addJob(jobDetail, true);② 注册JobDetail scheduler.scheduleJob(simpleTrigger);③ 注册指定了关联JobDetail的Trigger

在这种方式中,Trigger通过指定Job所属组及Job名称,然后使用Scheduler的scheduleJob(Trigger trigger)方法注册Trigger。有两个值得注意的地方: 通过这种方式注册的Trigger实例必须已经指定Job组和Job名称,否则调用注册Trigger的方法将抛出异常; 引用的JobDetail对象必须已经存在于Scheduler中。也即,代码中①、②和③的先后顺序不能互换。 在构造Trigger实例时,可以考虑使用org.quartz.TriggerUtils工具类,该工具类不但提供了众多获取特定时间的方法,还拥有众多获取常见Trigger的方法,如makeSecondlyTrigger(String trigName)方法将创建一个每秒执行一次的Trigger,而makeWeeklyTrigger(String trigName, int dayOfWeek, int hour, int minute)将创建一个每星期某一特定时间点执行一次的Trigger。而getEvenMinuteDate(Date date)方法将返回某一时间点一分钟以后的时间。

使用CronTrigger

CronTrigger 能够提供比 SimpleTrigger 更有具体实际意义的调度方案,调度规则基于 Cron 表达式,CronTrigger 支持日历相关的重复时间间隔(比如每月第一个周一执行),而不是简单的周期时间间隔。因此,相对于SimpleTrigger而言,CronTrigger在使用上也要复杂一些。

Cron表达式

Quartz使用类似于Linux下的Cron表达式定义时间规则,Cron表达式由6或7个由空格分隔的时间字段组成,如表1所示: 表1 Cron表达式时间字段

位置

时间域名

允许值

允许的特殊字符

1

0-59

, - * /

2

分钟

0-59

, - * /

3

小时

0-23

, - * /

4

日期

1-31

, - * ? / L W C

5

月份

1-12

, - * /

6

星期

1-7

, - * ? / L C #

7

年(可选)

空值1970-2099

, - * /

Cron表达式的时间字段除允许设置数值外,还可使用一些特殊的字符,提供列表、范围、通配符等功能,细说如下: ●星号():可用在所有字段中,表示对应时间域的每一个时刻,例如,在分钟字段时,表示“每分钟”; ●问号(?):该字符只在日期和星期字段中使用,它通常指定为“无意义的值”,相当于点位符; ●减号(-):表达一个范围,如在小时字段中使用“10-12”,则表示从10到12点,即10,11,12; ●逗号(,):表达一个列表值,如在星期字段中使用“MON,WED,FRI”,则表示星期一,星期三和星期五; ●斜杠(/):x/y表达一个等步长序列,x为起始值,y为增量步长值。如在分钟字段中使用0/15,则表示为0,15,30和45秒,而5/15在分钟字段中表示5,20,35,50,你也可以使用*/y,它等同于0/y; ●L:该字符只在日期和星期字段中使用,代表“Last”的意思,但它在两个字段中意思不同。L在日期字段中,表示这个月份的最后一天,如一月的31号,非闰年二月的28号;如果L用在星期中,则表示星期六,等同于7。但是,如果L出现在星期字段里,而且在前面有一个数值X,则表示“这个月的最后X天”,例如,6L表示该月的最后星期五; ●W:该字符只能出现在日期字段里,是对前导日期的修饰,表示离该日期最近的工作日。例如15W表示离该月15号最近的工作日,如果该月15号是星期六,则匹配14号星期五;如果15日是星期日,则匹配16号星期一;如果15号是星期二,那结果就是15号星期二。但必须注意关联的匹配日期不能够跨月,如你指定1W,如果1号是星期六,结果匹配的是3号星期一,而非上个月最后的那天。W字符串只能指定单一日期,而不能指定日期范围; ●LW组合:在日期字段可以组合使用LW,它的意思是当月的最后一个工作日; ●井号(#):该字符只能在星期字段中使用,表示当月某个工作日。如6#3表示当月的第三个星期五(6表示星期五,#3表示当前的第三个),而4#5表示当月的第五个星期三,假设当月没有第五个星期三,忽略不触发; ● C:该字符只在日期和星期字段中使用,代表“Calendar”的意思。它的意思是计划所关联的日期,如果日期没有被关联,则相当于日历中所有日期。例如5C在日期字段中就相当于日历5日以后的第一天。1C在星期字段中相当于星期日后的第一天。 Cron表达式对特殊字符的大小写不敏感,对代表星期的缩写英文大小写也不敏感。 表2下面给出一些完整的Cron表示式的实例: 表2 Cron表示式示例

表示式

说明

“0 0 12 ? “

每天12点运行

“0 15 10 ?

每天10:15运行

“0 15 10 ?”

每天10:15运行

“0 15 10 ? *”

每天10:15运行

“0 15 10 ? 2008”

在2008年的每天10:15运行

“0 14 * ?”

每天14点到15点之间每分钟运行一次,开始于14:00,结束于14:59。

“0 0/5 14 ?”

每天14点到15点每5分钟运行一次,开始于14:00,结束于14:55。

“0 0/5 14,18 ?”

每天14点到15点每5分钟运行一次,此外每天18点到19点每5钟也运行一次。

“0 0-5 14 ?”

每天14:00点到14:05,每分钟运行一次。

“0 10,44 14 ? 3 WED”

3月每周三的14:10分到14:44,每分钟运行一次。

“0 15 10 ? * MON-FRI”

每周一,二,三,四,五的10:15分运行。

“0 15 10 15 * ?”

每月15日10:15分运行。

“0 15 10 L * ?”

每月最后一天10:15分运行。

“0 15 10 ? * 6L”

每月最后一个星期五10:15分运行。

“0 15 10 ? * 6L 2007-2009”

在2007,2008,2009年每个月的最后一个星期五的10:15分运行。

“0 15 10 ? * 6#3”

每月第三个星期五的10:15分运行。

CronTrigger实例

下面,我们使用CronTrigger对SimpleJob进行调度,通过Cron表达式制定调度规则,让它每5秒钟运行一次: 代码清单3 CronTriggerRunner:使用CronTrigger进行调度

package com.baobaotao.basic.quartz; import org.quartz.CronExpression; import org.quartz.CronTrigger; import org.quartz.JobDetail; import org.quartz.Scheduler; import org.quartz.SchedulerFactory; import org.quartz.impl.StdSchedulerFactory; public class CronTriggerRunner { public static void main(String args[]) { try { JobDetail jobDetail = new JobDetail(“job1_2”, “jGroup1”,SimpleJob.class); ①-1:创建CronTrigger,指定组及名称 CronTrigger cronTrigger = new CronTrigger(“trigger1_2”, “tgroup1”); CronExpression cexp = new CronExpression(“0/5 ?”);①-2:定义Cron表达式 cronTrigger.setCronExpression(cexp);①-3:设置Cron表达式 SchedulerFactory schedulerFactory = new StdSchedulerFactory(); Scheduler scheduler = schedulerFactory.getScheduler(); scheduler.scheduleJob(jobDetail, cronTrigger); scheduler.start(); //② } catch (Exception e) { e.printStackTrace(); } } }

运行CronTriggerRunner,每5秒钟将触发运行SimpleJob一次。默认情况下Cron表达式对应当前的时区,可以通过CronTriggerRunner的setTimeZone(java.util.TimeZone timeZone)方法显式指定时区。你还也可以通过setStartTime(java.util.Date startTime)和setEndTime(java.util.Date endTime)指定开始和结束的时间。 在代码清单3的②处需要通过Thread.currentThread.sleep()的方式让主线程睡眠,以便调度器可以继续工作执行任务调度。否则在调度器启动后,因为主线程马上退出,也将同时引起调度器关闭,调度器中的任务都将相应销毁,这将导致看不到实际的运行效果。在单元测试的时候,让主线程睡眠经常使用的办法。对于某些长周期任务调度的测试,你可以简单地调整操作系统时间进行模拟。

使用Calendar

在实际任务调度中,我们不可能一成不变地按照某个周期性的调度规则运行任务,必须考虑到实现生活中日历上特定日期,就象习惯了大男人作风的人在2月14号也会有不同表现一样。 下面,我们安排一个任务,每小时运行一次,并将五一节和国际节排除在外,其代码如代码清单4所示: 代码清单4 CalendarExample:使用Calendar

package com.baobaotao.basic.quartz; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import org.quartz.impl.calendar.AnnualCalendar; import org.quartz.TriggerUtils; public class CalendarExample { public static void main(String[] args) throws Exception { SchedulerFactory sf = new StdSchedulerFactory(); Scheduler scheduler = sf.getScheduler(); ①法定节日是以每年为周期的,所以使用AnnualCalendar AnnualCalendar holidays = new AnnualCalendar(); ②五一劳动节 Calendar laborDay = new GregorianCalendar(); laborDay.add(Calendar.MONTH,5); laborDay.add(Calendar.DATE,1); holidays.setDayExcluded(laborDay, true); ②-1:排除的日期,如果设置为false则为包含 ③国庆节 Calendar nationalDay = new GregorianCalendar(); nationalDay.add(Calendar.MONTH,10); nationalDay.add(Calendar.DATE,1); holidays.setDayExcluded(nationalDay, true);③-1:排除该日期 scheduler.addCalendar(“holidays”, holidays, false, false);④向Scheduler注册日历 Date runDate = TriggerUtils.getDateOf(0,0, 10, 1, 4);⑤4月1号 上午10点 JobDetail job = new JobDetail(“job1”, “group1”, SimpleJob.class); SimpleTrigger trigger = new SimpleTrigger(“trigger1”, “group1”, runDate, null, SimpleTrigger.REPEAT_INDEFINITELY, 60L 60L 1000L); trigger.setCalendarName(“holidays”);⑥让Trigger应用指定的日历规则 scheduler.scheduleJob(job, trigger); scheduler.start(); //实际应用中主线程不能停止,否则Scheduler得不到执行,此处从略 } }

由于节日是每年重复的,所以使用org.quartz.Calendar的AnnualCalendar实现类,通过②、③的代码,指定五一和国庆两个节日并通过AnnualCalendar#setDayExcluded(Calendar day, boolean exclude)方法添加这两个日期。exclude为true时表示排除指定的日期,如果为false时表示包含指定的日期。 在定制好org.quartz.Calendar后,还需要通过Scheduler#addCalendar(String calName, Calendar calendar, boolean replace, boolean updateTriggers)进行注册,如果updateTriggers为true,Scheduler中已引用Calendar的Trigger将得到更新,如④所示。 在⑥处,我们让一个Trigger指定使用Scheduler中代表节日的Calendar,这样Trigger就会避开五一和国庆这两个特殊日子了。

任务调度信息存储

在默认情况下Quartz将任务调度的运行信息保存在内存中,这种方法提供了最佳的性能,因为内存中数据访问最快。不足之处是缺乏数据的持久性,当程序路途停止或系统崩溃时,所有运行的信息都会丢失。 比如我们希望安排一个执行100次的任务,如果执行到50次时系统崩溃了,系统重启时任务的执行计数器将从0开始。在大多数实际的应用中,我们往往并不需要保存任务调度的现场数据,因为很少需要规划一个指定执行次数的任务。 对于仅执行一次的任务来说,其执行条件信息本身应该是已经持久化的业务数据(如锁定到期解锁任务,解锁的时间应该是业务数据),当执行完成后,条件信息也会相应改变。当然调度现场信息不仅仅是记录运行次数,还包括调度规则、JobDataMap中的数据等等。 如果确实需要持久化任务调度信息,Quartz允许你通过调整其属性文件,将这些信息保存到数据库中。使用数据库保存任务调度信息后,即使系统崩溃后重新启动,任务的调度信息将得到恢复。如前面所说的例子,执行50次崩溃后重新运行,计数器将从51开始计数。使用了数据库保存信息的任务称为持久化任务。

通过配置文件调整任务调度信息的保存策略

其实Quartz JAR文件的org.quartz包下就包含了一个quartz.properties属性配置文件并提供了默认设置。如果需要调整默认配置,可以在类路径下建立一个新的quartz.properties,它将自动被Quartz加载并覆盖默认的设置。 先来了解一下Quartz的默认属性配置文件: 代码清单5 quartz.properties:默认配置 ①集群的配置,这里不使用集群 org.quartz.scheduler.instanceName = DefaultQuartzScheduler org.quartz.scheduler.rmi.export = false org.quartz.scheduler.rmi.proxy = false org.quartz.scheduler.wrapJobExecutionInUserTransaction = false ②配置调度器的线程池 org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool org.quartz.threadPool.threadCount = 10 org.quartz.threadPool.threadPriority = 5 org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = true ③配置任务调度现场数据保存机制 org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore Quartz的属性配置文件主要包括三方面的信息: 1)集群信息; 2)调度器线程池; 3)任务调度现场数据的保存。 如果任务数目很大时,可以通过增大线程池的大小得到更好的性能。默认情况下,Quartz采用org.quartz.simpl.RAMJobStore保存任务的现场数据,顾名思义,信息保存在RAM内存中,我们可以通过以下设置将任务调度现场数据保存到数据库中: 代码清单6 quartz.properties:使用数据库保存任务调度现场数据 … org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX org.quartz.jobStore.tablePrefix = QRTZ_①数据表前缀 org.quartz.jobStore.dataSource = qzDS②数据源名称 ③定义数据源的具体属性 org.quartz.dataSource.qzDS.driver = oracle.jdbc.driver.OracleDriver org.quartz.dataSource.qzDS.URL = jdbc:oracle:thin:@localhost:1521:ora9i org.quartz.dataSource.qzDS.user = stamen org.quartz.dataSource.qzDS.password = abc org.quartz.dataSource.qzDS.maxConnections = 10 要将任务调度数据保存到数据库中,就必须使用org.quartz.impl.jdbcjobstore.JobStoreTX代替原来的org.quartz.simpl.RAMJobStore并提供相应的数据库配置信息。首先①处指定了Quartz数据库表的前缀,在②处定义了一个数据源,在③处具体定义这个数据源的连接信息。 你必须事先在相应的数据库中创建Quartz的数据表(共8张),在Quartz的完整发布包的docs/dbTables目录下拥有对应不同数据库的SQL脚本。

查询数据库中的运行信息

任务的现场保存对于上层的Quartz程序来说是完全透明的,我们在src目录下编写一个如代码清单6所示的quartz.properties文件后,重新运行代码清单2或代码清单3的程序,在数据库表中将可以看到对应的持久化信息。当调度程序运行过程中途停止后,任务调度的现场数据将记录在数据表中,在系统重启时就可以在此基础上继续进行任务的调度。 代码清单7 JDBCJobStoreRunner:从数据库中恢复任务的调度

package com.baobaotao.basic.quartz; import org.quartz.Scheduler; import org.quartz.SchedulerFactory; import org.quartz.SimpleTrigger; import org.quartz.Trigger; import org.quartz.impl.StdSchedulerFactory; public class JDBCJobStoreRunner { public static void main(String args[]) { try { SchedulerFactory schedulerFactory = new StdSchedulerFactory(); Scheduler scheduler = schedulerFactory.getScheduler(); ①获取调度器中所有的触发器组 String[] triggerGroups = scheduler.getTriggerGroupNames(); ②重新恢复在tgroup1组中,名为trigger1_1触发器的运行 for (int i = 0; i < triggerGroups.length; i++) { String[] triggers = scheduler.getTriggerNames(triggerGroups[i]); for (int j = 0; j < triggers.length; j++) { Trigger tg = scheduler.getTrigger(triggers[j],triggerGroups[i]); if (tg instanceof SimpleTrigger && tg.getFullName().equals(“tgroup1.trigger1_1”)) {②-1:根据名称判断 ②-1:恢复运行 scheduler.rescheduleJob(triggers[j], triggerGroups[i],tg); } } } scheduler.start(); } catch (Exception e) { e.printStackTrace(); } } }

当代码清单2中的SimpleTriggerRunner执行到一段时间后非正常退出,我们就可以通过这个JDBCJobStoreRunner根据记录在数据库中的现场数据恢复任务的调度。Scheduler中的所有Trigger以及JobDetail的运行信息都会保存在数据库中,这里我们仅恢复tgroup1组中名称为trigger1_1的触发器,这可以通过如②-1所示的代码进行过滤,触发器的采用GROUP.TRIGGER_NAME的全名格式。通过Scheduler#rescheduleJob(String triggerName,String groupName,Trigger newTrigger)即可重新调度关联某个Trigger的任务。 下面我们来观察一下不同时期qrtz_simple_triggers表的数据: 1.运行代码清单2的SimpleTriggerRunner一小段时间后退出:

500){this.resized=true;this.style.width=500;}” align=center resized=”true”>

REPEAT_COUNT表示需要运行的总次数,而TIMES_TRIGGER表示已经运行的次数。 2.运行代码清单7的JDBCJobStoreRunner恢复trigger1_1的触发器,运行一段时间后退出,这时qrtz_simple_triggers中的数据如下:

500){this.resized=true;this.style.width=500;}” align=center resized=”true”>

首先Quartz会将原REPEAT_COUNT-TIMES_TRIGGER得到新的REPEAT_COUNT值,并记录已经运行的次数(重新从0开始计算)。 3.重新启动JDBCJobStoreRunner运行后,数据又将发生相应的变化:

500){this.resized=true;this.style.width=500;}” align=center resized=”true”>

4.继续运行直至完成所有剩余的次数,再次查询qrtz_simple_triggers表:

500){this.resized=true;this.style.width=500;}” align=center resized=”true”>

这时,该表中的记录已经变空。 值得注意的是,如果你使用JDBC保存任务调度数据时,当你运行代码清单2的SimpleTriggerRunner然后退出,当再次希望运行SimpleTriggerRunner时,系统将抛出JobDetail重名的异常: Unable to store Job with name: ‘job1_1’ and group: ‘jGroup1’, because one already exists with this identification. 因为每次调用Scheduler#scheduleJob()时,Quartz都会将JobDetail和Trigger的信息保存到数据库中,如果数据表中已经同名的JobDetail或Trigger,异常就产生了。 本文使用quartz 1.6版本,我们发现当后台数据库使用MySql时,数据保存不成功,该错误是Quartz的一个Bug,相信会在高版本中得到修复。因为HSQLDB不支持SELECT * FROM TABLE_NAME FOR UPDATE的语法,所以不能使用HSQLDB数据库。

小结

Quartz提供了最为丰富的任务调度功能,不但可以制定周期性运行的任务调度方案,还可以让你按照日历相关的方式进行任务调度。Quartz框架的重要组件包括Job、JobDetail、Trigger、Scheduler以及辅助性的JobDataMap和SchedulerContext。 Quartz拥有一个线程池,通过线程池为任务提供执行线程,你可以通过配置文件对线程池进行参数定制。Quartz的另一个重要功能是可将任务调度信息持久化到数据库中,以便系统重启时能够恢复已经安排的任务。此外,Quartz还拥有完善的事件体系,允许你注册各种事件的

cron表达式详解

Cron表达式是一个字符串,字符串以5或6个空格隔开,分为6或7个域,每一个域代表一个含义,Cron有如下两种语法格式: Seconds Minutes Hours DayofMonth Month DayofWeek Year或 Seconds Minutes Hours DayofMonth Month DayofWeek 每一个域可出现的字符如下: Seconds:可出现”, - /“四个字符,有效范围为0-59的整数 Minutes:可出现”, - /“四个字符,有效范围为0-59的整数 Hours:可出现”, - /“四个字符,有效范围为0-23的整数 DayofMonth:可出现”, - / ? L W C”八个字符,有效范围为0-31的整数 Month:可出现”, - /“四个字符,有效范围为1-12的整数或JAN-DEc DayofWeek:可出现”, - / ? L C #”四个字符,有效范围为1-7的整数或SUN-SAT两个范围。1表示星期天,2表示星期一, 依次类推 Year:可出现”, - /“四个字符,有效范围为1970-2099年 每一个域都使用数字,但还可以出现如下特殊字符,它们的含义是: (1):表示匹配该域的任意值,假如在Minutes域使用, 即表示每分钟都会触发事件。 (2)?:只能用在DayofMonth和DayofWeek两个域。它也匹配域的任意值,但实际不会。因为DayofMonth和DayofWeek会相互影响。例如想在每月的20日触发调度,不管20日到底是星期几,则只能使用如下写法: 13 13 15 20 ?, 其中最后一位只能用?,而不能使用,如果使用表示不管星期几都会触发,实际上并不是这样。 (3)-:表示范围,例如在Minutes域使用5-20,表示从5分到20分钟每分钟触发一次 (4)/:表示起始时间开始触发,然后每隔固定时间触发一次,例如在Minutes域使用5/20,则意味着5分钟触发一次,而25,45等分别触发一次. (5),:表示列出枚举值值。例如:在Minutes域使用5,20,则意味着在5和20分每分钟触发一次。 (6)L:表示最后,只能出现在DayofWeek和DayofMonth域,如果在DayofWeek域使用5L,意味着在最后的一个星期四触发。 (7)W:表示有效工作日(周一到周五),只能出现在DayofMonth域,系统将在离指定日期的最近的有效工作日触发事件。例如:在 DayofMonth使用5W,如果5日是星期六,则将在最近的工作日:星期五,即4日触发。如果5日是星期天,则在6日(周一)触发;如果5日在星期一到星期五中的一天,则就在5日触发。另外一点,W的最近寻找不会跨过月份 (8)LW:这两个字符可以连用,表示在某个月最后一个工作日,即最后一个星期五。 (9)#:用于确定每个月第几个星期几,只能出现在DayofMonth域。例如在4#2,表示某月的第二个星期三。 举几个例子: 0 0 2 1 ? 表示在每月的1日的凌晨2点调度任务 0 15 10 ? MON-FRI 表示周一到周五每天上午10:15执行作业 0 15 10 ? 6L 2002-2006 表示2002-2006年的每个月的最后一个星期五上午10:15执行作 一个cron表达式有至少6个(也可能7个)有空格分隔的时间元素。 按顺序依次为 秒(0~59) 分钟(0~59) 小时(0~23) 天(月)(0~31,但是你需要考虑你月的天数) 月(0~11) 天(星期)(1~7 1=SUN 或 SUN,MON,TUE,WED,THU,FRI,SAT) 年份(1970-2099) 其中每个元素可以是一个值(如6),一个连续区间(9-12),一个间隔时间(8-18/4)(/表示每隔4小时),一个列表(1,3,5),通配符。由于”月份中的日期”和”星期中的日期”这两个元素互斥的,必须要对其中一个设置? 0 0 10,14,16 ? 每天上午10点,下午2点,4点 0 0/30 9-17 ? 朝九晚五工作时间内每半小时 0 0 12 ? WED 表示每个星期三中午12点 “0 0 12 ?” 每天中午12点触发 “0 15 10 ? “ 每天上午10:15触发 “0 15 10 ?” 每天上午10:15触发 “0 15 10 ? “ 每天上午10:15触发 “0 15 10 ? 2005” 2005年的每天上午10:15触发 “0 14 ?” 在每天下午2点到下午2:59期间的每1分钟触发 “0 0/5 14 ?” 在每天下午2点到下午2:55期间的每5分钟触发 “0 0/5 14,18 ?” 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发 “0 0-5 14 ?” 在每天下午2点到下午2:05期间的每1分钟触发 “0 10,44 14 ? 3 WED” 每年三月的星期三的下午2:10和2:44触发 “0 15 10 ? MON-FRI” 周一至周五的上午10:15触发 “0 15 10 15 ?” 每月15日上午10:15触发 “0 15 10 L ?” 每月最后一日的上午10:15触发 “0 15 10 ? 6L” 每月的最后一个星期五上午10:15触发 “0 15 10 ? 6L 2002-2005” 2002年至2005年的每月的最后一个星期五上午10:15触发 “0 15 10 ? 6#3” 每月的第三个星期五上午10:15触发 有些子表达式能包含一些范围或列表 例如:子表达式(天(星期))可以为 “MON-FRI”,“MON,WED,FRI”,“MON-WED,SAT” “”字符代表所有可能的值 因此,“”在子表达式(月)里表示每个月的含义,“”在子表达式(天(星期))表示星期的每一天 “/”字符用来指定数值的增量 例如:在子表达式(分钟)里的“0/15”表示从第0分钟开始,每15分钟 在子表达式(分钟)里的“3/20”表示从第3分钟开始,每20分钟(它和“3,23,43”)的含义一样 “?”字符仅被用于天(月)和天(星期)两个子表达式,表示不指定值 当2个子表达式其中之一被指定了值以后,为了避免冲突,需要将另一个子表达式的值设为“?” “L” 字符仅被用于天(月)和天(星期)两个子表达式,它是单词“last”的缩写 但是它在两个子表达式里的含义是不同的。 在天(月)子表达式中,“L”表示一个月的最后一天 在天(星期)自表达式中,“L”表示一个星期的最后一天,也就是SAT 如果在“L”前有具体的内容,它就具有其他的含义了 例如:“6L”表示这个月的倒数第6天,“FRIL”表示这个月的最一个星期五 注意:在使用“L”参数时,不要指定列表或范围,因为这会导致问题 字段 允许值 允许的特殊字符 秒 0-59 , - / 分 0-59 , - / 小时 0-23 , - / 日期 1-31 , - ? / L W C 月份 1-12 或者 JAN-DEC , - / 星期 1-7 或者 SUN-SAT , - ? / L C # 年(可选) 留空, 1970-2099 , - /