关于作者

姓名:

性别:其他

出生日期:--

地区:

联系电话:

QQ:--

婚否:保密
用户名:mahaixing
笔名:海星
地区:
行业:其他

日历  

快速登录

+ 用户名:
+ 密 码:

在线留言



访问统计:
文章个数:14
评论个数:5
留言条数:1




Powered by BlogDriver 2.1

老马的博客

 

文章

使用数据泵进行数据导入和导出

摘自:http://www.oracle.com/global/cn/oramag/oracle/04-sep/o54data.html

找到将数据从仓库迁移到集市的最快方法。

Lora是Acme银行的数据库管理员,她现在在该银行高层管理团队高级会议上成了大家最关注的核心人物。这次会议的目的是确定一些方法,来使最终用户能够详细分析公司主数据仓库中的数据。会上提出的一种想法是创建几个小型数据集市--每个集市根据一个特定的职能范围存储数据--这样每个数据集市就可以由专门的团队来使用。

为了有效地实现数据集市的方法,数据专家必须能将数据快速、有效地放入数据集市中。该团队面临的挑战就是解决如何用数据仓库中的数据快速刷新数据集市中的数据,而这些数据集市又运行在各个结构不同的平台上。这就是Lora为什么出席会议的原因。她会为移动数据提出哪些可供选择的方法呢?

作为一名经验丰富、知识渊博的数据库管理员,Lora向与会者提供了三种可能的方法,分别是:

  • 使用可移动表空间
  • 使用数据泵(导入和导出)
  • 拖出表空间

本文介绍Lora对这三种可选方法的解释,包括它们的实施细节和优缺点。

可移动表空间

Lora从可移动表空方法开始介绍。把整个表空间移动到目标系统的最快速方法是用FTP(文件传输协议)或rcp(远程复制)来简单地转移表空间的基本文件。但是,仅仅复制Oracle数据文件还不够,目标数据库必须识别出并导入文件以及相应的表空间,最终用户才能使用表空间数据。使用可移动表空间包括复制表空间文件和使它们中的数据在目标数据库中可用。

在考虑该方法之前必须进行一些审查。首先,对于要转移到目标系统的表空间TS1,它必须是自含式的(self-contained)。也就是说,在该表空间中表的所有索引、分区及其他从属于该表的各数据段都必须在该表空间内部。Lora解释说,如果一个表空间集合包含所有从属的数据段,那么就认为这个集合是自含式的。例如,如果表空间TS1和TS2要作为一个集合进行转移,TS1中的一个表在TS2中有一个索引,则这个表空间集合就是自含式的。但是,如果TS1中的一个表另一个索引在表空间TS3中,则该表空间集合 (TS1, TS2)就不是自含式的。

要移动表空间,Lora提议使用Oracle数据库10g中的数据泵导出(Data Pump Export)工具。数据泵是Oracle的新一代数据转移工具,它替换了早期的Oracle Export (EXP)和Import (IMP)工具。这些老的工具使用正则SQL来提取和插入数据,而数据泵则与它们不同,它使用能绕过SQL缓冲区的专用API,从而使操作过程速度变得极快。此外,数据泵可以提取特定的对象,如特定的存储过程或特定表空间的表集合。 数据泵的导出和导入可以由作业控制,数据库管理员可以随时暂停、重启或终止这些作业。

开会前Lora运行了一项测试,看看数据泵能否解决Acme的要求。Lora进行的测试是转移TS1和TS2表空间,步骤如下:

1.检查TS1和TS2这个表空间集合是否是自含式的。执行下面的命令:

BEGIN 
  SYS.DBMS_TTS.TRANSPORT_SET_CHECK ('TS1','TS2'); 
END;

2.确定所有不可移动的集合。如果没有选择任何行,则该表空间是自含式的:

SELECT * FROM TRANSPORT_SET_VIOLATIONS;

no rows selected

3.确保该表空间是只读的:

SELECT STATUS
FROM DBA_TABLESPACES
WHERE TABLESPACE_NAME IN ('TS1','TS2');

STATUS
---------
READ ONLY
READ ONLY

4.使用传输机制,如FTP或rcp,将每个表空间中的数据文件移到远程系统,放到/u01/oradata目录下。

5.在目标数据库中,创建一个到源数据库的数据库链接(在下面的命令行中命名为srcdb)。

CREATE DATABASE LINK srcdb 
USING 'srcdb';

6.在目标数据库中,使用数据泵导入工具将该表空间导入到该数据库中。

impdp lora/lora123  
TRANSPORT_DATAFILES=
"'/u01/oradata/ts1_1.dbf',                      
'/u01/oradata/ts2_1.dbf'" 
NETWORK_LINK='srcdb' 
TRANSPORT_TABLESPACES=\(TS1,TS2\) 
NOLOGFILE=Y

这一步就使TS1和TS2表空间以及它们的数据可以在目标数据库中可用。

请注意,Lora并没有从源数据库导出元数据。她只是在上面的impdp命令中指定参数NETWORK_LINK的值为srcdb,即到源数据库的数据库链接。数据泵导入工具通过数据库链接从源数据库中获得所需的元数据,并在目标数据库中重新创建它们。

7. 最后,使源数据库中的TS1和TS2表空间成为可读写。

ALTER TABLESPACE TS1 READ WRITE;
ALTER TABLESPACE TS2 READ WRITE;

这一步使TS1和TS2表空间在源数据库中对用户立即可用。

请注意,在前面讲的所有步骤中,最费时的是第4步,在这一步中要跨各系统移动数据文件。

跨不同平台的挑战

Lora知道,数据复制过程中的复杂因素之一就是源平台和目标平台经常不一致。例如,在Acme的环境中,当前数据仓库位于运行Tru64 UNIX的HP服务器上,而建议的数据集市将部署在运行Linux和Windows的Intel硬件上。

在Oracle数据库10g出现之前,在Acme使用可移动表空间不是一个切实可行的方法。你不能移动表空间,除非源数据库和目标数据库运行在同一个平台上。

有了Oracle数据库10g,这一限制得到了极大的缓解。在移动表空间时,数据文件可以跨各操作系统任意复制。在前面的例子中,数据文件可以从Tru64 UNIX复制到Linux或Windows,而目标数据库仍能识别它们。

但是,管理团队必须考虑另一种限制。默认情况下,只有两个操作系统具有相同的字节顺序(也被称为"endian-ness")时才可能进行跨操作系统复制。在Acme的情况下,Tru64 UNIX、基于Intel的Linux和Windows都使用little-endian字节顺序(低位在前),因此在它们之间进行文件复制是可能的。但是,有的团队成员提出了在有关数据仓库/数据集市的提案中使用Solaris操作系统的想法。Solaris使用big-endian字节顺序(高位在前),这使得在数据库之间进行简单的文件复制成为不可能。

Lora解释说,Oracle提供了使用Oracle Recovery Manager (RMAN)进行字节顺序转换的解决方案。她描述了针对RMAN解决方案的一个测试,该测试根据前面讲到的移动表空间解决方案的7个步骤,并稍作修改来适应Solaris到Intel/Linux的环境。除了在第5步之前或之后额外增加一步以外,所有操作步骤都一样。在源(Solaris)数据库服务器上执行的额外这一步如代码清单1中所示。

用这些代码,可以从Solaris格式的/u01/oradata/ts1_01.dbf文件创建Intel Linux格式的文件/u01/tts/ TS1_34。注意最初的文件并没有被破坏;只是创建了一个可以被导入和传输到Linux上的目标数据库中的新文件。

然后Lora讨论了对RMAN解决方案的一些修改。第一,为了提高性能,她可以指定PARALLELISM=<degree>子句来提高执行线程的数目。第二,她可以指定在不同的目录下以相同的文件名创建数据文件。这些修改如下:

RMAN> CONVERT TABLESPACE TS1
2> TO PLATFORM 'Linux IA (32-bit)'
3> DB_FILE_NAME_CONVERT 
4> '/u01/oradata','/u01/tts'	
5> PARALLELISM=4
6> ;

这条命令用相同的文件名ts1_01.dbf但在/u01/tts目录下根据原始文件/u01/oradata/ts1_01.dbf创建一个转换后的数据文件。这种方法将所有转换后的文件放到一个位置,这样确定转移哪个文件就更简单了。

Lora还可以在目标(Linux)数据库服务器而不是在源服务器上执行变换。在这种情况下,将在Linux服务器上执行转换,如下所示:

RMAN> CONVERT DATAFILE
2> '/u01/oradata/ts1_1.dbf',
'/u01/oradata/ts2_1.dbf'
3> TO PLATFORM='Linux IA (32-bit)'
4> FROM PLATFORM='Solaris[tm] OE 
(64-bit)'
5> DB_FILE_NAME_CONVERT="ts","tslinux"
6> ;

这条命令通过用tslinux代替ts(换句话说,将ts1_1.dbf转换到Linux上的文件格式,新文件名为tslinux1_1.dbf)来创建文件。在数据库内,文件将用这个文件名。

Lora怎么知道哪个平台用哪种字节顺序呢?在数据字典视图上执行以下查询就会给出答案:

SELECT * FROM V$TRANSPORTABLE_PLATFORM ORDER BY PLATFORM_ID;

数据泵导出与导入工具

选择可移动表空间的限制之一是在转移文件时源表空间必须是只读模式。在现实世界中,并不总是能满足这一要求。例如,在OLTP数据库中,可能对表要经常进行读写操作。

Lora提出的另一种方法是使用Oracle数据库10g中的数据泵实用工具来转移表空间。她对这种方法进行的测试包括移动TS1和TS2表空间内容的以下步骤:

1.创建一个目录对象来存放转储的文件。

CREATE DIRECTORY dump_dir AS '/u01/dumps';

2.用数据泵导出工具导出数据。

expdp lora/lora123 TABLESPACES=\(ts1,ts2\) DUMPFILE=ts1_ts2.dmp DIRECTORY=dump_dir

这一步创建一个包含TS1和TS2表空间内容的文件/u01/dumps/ ts1_ts2.dmp。

3.将文件ts1_ts2.dmp转移到远程系统中,放在目录/u01/dumps下(用文件传输的方法如FTP或rcp)。

4.在目标数据库中创建一个目录对象。

CREATE DIRECTORY dump_dir 
AS '/u01/dumps';

5.使用数据泵导入工具将该文件导入到该数据库中。

impdp lora/lora123 DIRECTORY=dump_dir DUMPFILE=ts1_ts2.dmp

如果表空间内的数据量相对较小,则Lora可以只用一条命令执行上面的所有步骤:

impdp lora/lora123 DIRECTORY=dump_dir NETWORK_LINK='srcdb' TABLESPACES=\(ts1,ts2\) 

这条命令使用数据泵导入工具将通过数据库链接srcdb(在以前的章节中已讨论过)检索到的数据加载到表中。但是,由于网络带宽通常是受到限制的,因此这种方法可能比使用导出/传输/导入周期方法要慢一些。

如果只需将特定的表或表集合进行转移,那么Lora可以在expdp命令中使用TABLES=<tablelist>子句来只下载特定的表或表集合。

拖出表空间

作为第三种选择,Lora建议使用Oracle数据库10g中的新工具,它简化了可移动表空间的移动方法,因此只涉及执行一个打包过程。在这种方法中,用户利用所提供的DBMS_STREAMS_TABLESPACE_ADM包从源系统中"拖?quot;表空间。这个包使用数据泵转移表空间并将数据文件转换成目标系统的格式。 它还自动执行任何所需的字节顺序变换。

下面给出在最简单的情况下使用这种方法的过程--涉及单个简单表空间(更复杂的情况在下一节介绍)。 如果一个表空间只有一个数据文件,则这个表空间称为简单表空间。Lora演示了DBMS_STREAMS_TABLESPACE_ADM包中PULL_SIMPLE_TABLESPACE过程的使用方法:

1.在存放数据文件的目录所在的(远程)数据仓库数据库中创建一个目录对象。

CREATE DIRECTORY dbf_dir AS '/u01/oradata/dw';

2.设置远程数据库中的表空间TS1为只读。

ALTER TABLESPACE TS1 READ ONLY;

剩下的一些步骤在本地(数据集市)数据库中完成。

3.创建一个连接到远程(数据仓库)数据库(在Lora的例子中是dwdb)的数据库链接。

CREATE DATABASE LINK dwdb USING 'dwdb';

4.创建一个数据文件将被转移到其中的目录对象。

CREATE DIRECTORY dbf_dir AS '/u01/oradata/mart';

5.从远程数据库中拖出表空间。

BEGIN
  DBMS_STREAMS_TABLESPACE_ADM
    .PULL_SIMPLE_TABLESPACE (
     tablespace_name     => 'TS1',
     database_link       => 'dwdb',
     directory_object    => 'DBF_DIR',
     conversion_extension => 'linux'
);
END;

该操作在后台完成了许多步骤:设置源表空间为只读;用数据泵导出工具进行一次表空间的元数据转储;用DBMS_FILE_TRANSFER包移动数据文件和转储的文件;把源表空间恢复到其最初的读写状态;使用数据泵导入工具将表空间插入到本地数据库中。由于源数据库运行在Linux上,而目标数据库运行在Solaris上,因此这一操作首先复制原始数据文件(Linux的文件格式),然后将它转换到目标平台上(Solaris)的文件格式。复制过程保持最初被转移的文件,而创建一个新文件用于转换。新文件与最初的文件同名,但具有CONVERSION_EXTENSION参数指定的linux扩展名。在目标数据库中创建的表空间为只读表空间。

该操作还在与数据文件相同的目录下创建一个名为ts1_01.plg的日志文件。如果执行该过程返回错误信息,则检查该文件的内容可能有助于找到错误的原因。

拖出多个表空间

上面的例子针对的是单个简单表空间的情况。但如果Lora想移动一组表空间,或者一些表空间的数据文件多于一个,该怎么办呢?在这种情况下,她可以使用同一个包中的另一个过程PULL_TABLESPACES。代码清单2给出的例子说明Lora如何转移两个表空间TS7和TS8,而不管它们有多少个数据文件。

该过程要求以VARCHAR2数据类型给出表空间名和目录名。代码清单2中第2行到第5行展示出了这些变量的声明,第10行到第13行展示出这些变量被赋值给相应的表空间名和目录名。 由于定义了两个目录,因此第一个文件在第一个目录中创建,下一个文件在第二个目录中创建,第三个文件再次在第一个目录中创建,如此等等。这些操作通过数据泵作业来执行,作业名在第17行指定。如果需要的话,源系统字节顺序的数据文件会自动转换为目标系统的字节顺序。在目标数据库中创建的新文件获得linux扩展名,如第21行代码所示。处理过程记录在由目录对象LOG_DIR指定的目录中的ts7_ts8.log日志文件中(第14行)。

这种方法的优点显而易见。从一个系统把一个表空间转移到另一个系统所需的所有操作任务都封装在一个程序单元中,并且细节对用户完全透明。甚至把文件从源系统转移到目标系统的工作也在这个过程中通过所提供的DBMS_FILE_TRANSFER包来完成。用户简单地用表空间名调用该过程,表空间就会在本地数据库中被刷新。他们不必操心底层的细节(如操作系统),因为文件转移过程自动转换文件。这种方法有什么不好的地方吗?会上Lora讲到,它的主要缺点就是将各个功能封装在一个单一的过程内,这可能会掩盖某一步产生的错误,使问题的诊断变得很复杂。手工转移一个表空间的方法要求各条命令都是透明的,因而其好处是用户能够看到每一步操作的结果。

会议结束

针对Acme的数据仓库/数据集市体系结构,Lora提出了几种移动数据的可选方法。

第一种可选方法是使用可移动表空间,它能移动完整的表空间集合(不仅包括表,还包括索引、物化视图和其他对象)。通常它还是这三种方法中最快的一种。但是,它的一个主要缺点是对指定的表空间必须在复制文件时设置为只读。

第二种方法是使用数据泵,它对表空间是否为只读没有要求。当只需要移动指定的表而不是整个表空间时,这种方法很有用。

最后一种方法是拖出表空间,该方法把可移动表空间方法的所有步骤组合成一步操作。用这种方法复制数据非常简单,但要想调整每个具体步骤以便进行性能优化时,它为数据库管理员提供的灵活性太少。

在会议结束时,高级管理层对Lora表示感谢,而Lora也感谢高级管理层对Acme银行迁移到Oracle数据库10g的支持,因为这使得不同的数据移动方法都成为可行的。

- 作者: 海星 2006年04月10日, 星期一 14:40  回复(0) |  引用(0) 加入博采

权限系统概要

转贴:http://www.cnblogs.com/xspin/articles/31395.html

前言:
权限往往是一个极其复杂的问题,但也可简单表述为这样的逻辑表达式:判断"Who对What(Which)进行How的操作"的逻辑表达式是否为真。针对不同的应用,需要根据项目的实际情况和具体架构,在维护性、灵活性、完整性等N多个方案之间比较权衡,选择符合的方案。


目标:
直观,因为系统最终会由最终用户来维护,权限分配的直观和容易理解,显得比较重要,系统不辞劳苦的实现了组的继承,除了功能的必须,更主要的就是因为它足够直观。
简单,包括概念数量上的简单和意义上的简单还有功能上的简单。想用一个权限系统解决所有的权限问题是不现实的。设计中将常常变化的"定制"特点比较强的部分判断为业务逻辑,而将常常相同的"通用"特点比较强的部分判断为权限逻辑就是基于这样的思路。
扩展,采用可继承在扩展上的困难。的Group概念在支持权限以组方式定义的同时有效避免了重定义时
现状:
对于在企业环境中的访问控制方法,一般有三种:
1.自主型访问控制方法。目前在我国的大多数的信息系统中的访问控制模块中基本是借助于自主型访问控制方法中的访问控制列表(ACLs)。
2.强制型访问控制方法。用于多层次安全级别的军事应用。
3.基于角色的访问控制方法(RBAC)。是目前公认的解决大型企业的统一资源访问控制的有效方法。其显著的两大特征是:1.减小授权管理的复杂性,降低管理开销。2.灵活地支持企业的安全策略,并对企业的变化有很大的伸缩性。
名词:
粗粒度:表示类别级,即仅考虑对象的类别(the type of object),不考虑对象的某个特
定实例。比如,用户管理中,创建、删除,对所有的用户都一视同仁,并不区分操作的具体对象实例。
细粒度:表示实例级,即需要考虑具体对象的实例(the instance of object),当然,细
粒度是在考虑粗粒度的对象类别之后才再考虑特定实例。比如,合同管理中,列表、删除,需要区分该合同实例是否为当前用户所创建。
原则:
权限逻辑配合业务逻辑。即权限系统以为业务逻辑提供服务为目标。相当多细粒度的权限问题因其极其独特而不具通用意义,它们也能被理解为是"业务逻辑"的一部分。比如,要求:"合同资源只能被它的创建者删除,与创建者同组的用户可以修改,所有的用户能够浏览"。这既可以认为是一个细粒度的权限问题,也可以认为是一个业务逻辑问题。在这里它是业务逻辑问题,在整个权限系统的架构设计之中不予过多考虑。当然,权限系统的架构也必须要能支持这样的控制判断。或者说,系统提供足够多但不是完全的控制能力。即,设计原则归结为:"系统只提供粗粒度的权限,细粒度的权限被认为是业务逻辑的职责"。
需要再次强调的是,这里表述的权限系统仅是一个"不完全"的权限系统,即,它不提供所有关于权限的问题的解决方法。它提供一个基础,并解决那些具有"共性"的(或者说粗粒度的)部分。在这个基础之上,根据"业务逻辑"的独特权限需求,编码实现剩余部分(或者说细粒度的)部分,才算完整。回到权限的问题公式,通用的设计仅解决了Who+What+How 的问题,其他的权限问题留给业务逻辑解决。
概念:
Who:权限的拥用者或主体(Principal、User、Group、Role、Actor等等)
What:权限针对的对象或资源(Resource、Class)。
How:具体的权限(Privilege, 正向授权与负向授权)。
Role:是角色,拥有一定数量的权限。
Operator:操作。表明对What的How 操作。
说明:
User:与 Role 相关,用户仅仅是纯粹的用户,权限是被分离出去了的。User是不能与 Privilege 直接相关的,User 要拥有对某种资源的权限,必须通过Role去关联。解决 Who 的问题。
Resource:就是系统的资源,比如部门新闻,文档等各种可以被提供给用户访问的对象。资源可以反向包含自身,即树状结构,每一个资源节点可以与若干指定权限类别相关可定义是否将其权限应用于子节点。
Privilege:是Resource Related的权限。就是指,这个权限是绑定在特定的资源实例上的。比如说部门新闻的发布权限,叫做"部门新闻发布权限"。这就表明,该Privilege是一个发布权限,而且是针对部门新闻这种资源的一种发布权限。Privilege是由Creator在做开发时就确定的。权限,包括系统定义权限和用户自定义权限用户自定义权限之间可以指定排斥和包含关系(如:读取,修改,管理三个权限,管理 权限 包含 前两种权限)。Privilege 如"删除" 是一个抽象的名词,当它不与任何具体的 Object 或 Resource 绑定在一起时是没有任何意义的。拿新闻发布来说,发布是一种权限,但是只说发布它是毫无意义的。因为不知道发布可以操作的对象是什么。只有当发布与新闻结合在一起时,才会产生真正的 Privilege。这就是 Privilege Instance。权限系统根据需求的不同可以延伸生很多不同的版本。
Role:是粗粒度和细粒度(业务逻辑)的接口,一个基于粗粒度控制的权限框架软件,对外的接口应该是Role,具体业务实现可以直接继承或拓展丰富Role的内容,Role不是如同User或Group的具体实体,它是接口概念,抽象的通称。
Group:用户组,权限分配的单位与载体。权限不考虑分配给特定的用户。组可以包括组(以实现权限的继承)。组可以包含用户,组内用户继承组的权限。Group要实现继承。即在创建时必须要指定该Group的Parent是什么Group。在粗粒度控制上,可以认为,只要某用户直接或者间接的属于某个Group那么它就具备这个Group的所有操作许可。细粒度控制上,在业务逻辑的判断中,User仅应关注其直接属于的Group,用来判断是否"同组" 。Group是可继承的,对于一个分级的权限实现,某个Group通过"继承"就已经直接获得了其父Group所拥有的所有"权限集合",对这个Group而言,需要与权限建立直接关联的,仅是它比起其父Group需要"扩展"的那部分权限。子组继承父组的所有权限,规则来得更简单,同时意味着管理更容易。为了更进一步实现权限的继承,最直接的就是在Group上引入"父子关系"。
User与Group是多对多的关系。即一个User可以属于多个Group之中,一个Group可以包括多个User。子Group与父Group是多对一的关系。Operator某种意义上类似于Resource + Privilege概念,但这里的Resource仅包括Resource Type不表示Resource Instance。Group 可以直接映射组织结构,Role 可以直接映射组织结构中的业务角色,比较直观,而且也足够灵活。Role对系统的贡献实质上就是提供了一个比较粗颗粒的分配单位。
Group与Operator是多对多的关系。各概念的关系图示如下:
解释:
Operator的定义包括了Resource Type和Method概念。即,What和How的概念。之所以将What和How绑定在一起作为一个Operator概念而不是分开建模再建立关联,这是因为很多的How对于某What才有意义。比如,发布操作对新闻对象才有意义,对用户对象则没有意义。
How本身的意义也有所不同,具体来说,对于每一个What可以定义N种操作。比如,对于合同这类对象,可以定义创建操作、提交操作、检查冲突操作等。可以认为,How概念对应于每一个商业方法。其中,与具体用户身份相关的操作既可以定义在操作的业务逻辑之中,也可以定义在操作级别。比如,创建者的浏览视图与普通用户的浏览视图要求内容不同。既可以在外部定义两个操作方法,也可以在一个操作方法的内部根据具体逻辑进行处理。具体应用哪一种方式应依据实际情况进行处理。
这样的架构,应能在易于理解和管理的情况下,满足绝大部分粗粒度权限控制的功能需要。但是除了粗粒度权限,系统中必然还会包括无数对具体Instance的细粒度权限。这些问题,被留给业务逻辑来解决,这样的考虑基于以下两点:
一方面,细粒度的权限判断必须要在资源上建模权限分配的支持信息才可能得以实现。比如,如果要求创建者和普通用户看到不同的信息内容,那么,资源本身应该有其创建者的信息。另一方面,细粒度的权限常常具有相当大的业务逻辑相关性。对不同的业务逻辑,常常意味着完全不同的权限判定原则和策略。相比之下,粗粒度的权限更具通用性,将其实现为一个架构,更有重用价值;而将细粒度的权限判断实现为一个架构级别的东西就显得繁琐,而且不是那么的有必要,用定制的代码来实现就更简洁,更灵活。
所以细粒度控制应该在底层解决,Resource在实例化的时候,必需指定Owner和GroupPrivilege在对Resource进行操作时也必然会确定约束类型:究竟是OwnerOK还是GroupOK还是AllOK。Group应和Role严格分离User和Group是多对多的关系,Group只用于对用户分类,不包含任何Role的意义;Role只授予User,而不是Group。如果用户需要还没有的多种Privilege的组合,必须新增Role。Privilege必须能够访问Resource,同时带User参数,这样权限控制就完备了。
思想:
权限系统的核心由以下三部分构成:1.创造权限,2.分配权限,3.使用权限,然后,系统各部分的主要参与者对照如下:1.创造权限 - Creator创造,2.分配权限 - Administrator 分配,3.使用权限 - User:
1. Creator 创造 Privilege, Creator 在设计和实现系统时会划分,一个子系统或称为模块,应该有哪些权限。这里完成的是 Privilege 与 Resource 的对象声明,并没有真正将 Privilege 与具体Resource 实例联系在一起,形成Operator。
2. Administrator 指定 Privilege 与 Resource Instance 的关联。在这一步, 权限真正与资源实例联系到了一起, 产生了Operator(Privilege Instance)。Administrator利用Operator这个基本元素,来创造他理想中的权限模型。如,创建角色,创建用户组,给用户组分配用户,将用户组与角色关联等等...这些操作都是由 Administrator 来完成的。
3. User 使用 Administrator 分配给的权限去使用各个子系统。Administrator 是用户,在他的心目中有一个比较适合他管理和维护的权限模型。于是,程序员只要回答一个问题,就是什么权限可以访问什么资源,也就是前面说的 Operator。程序员提供 Operator 就意味着给系统穿上了盔甲。Administrator 就可以按照他的意愿来建立他所希望的权限框架可以自行增加,删除,管理Resource和Privilege之间关系。可以自行设定用户User和角色Role的对应关系。(如果将 Creator看作是 Basic 的发明者, Administrator 就是 Basic 的使用者,他可以做一些脚本式的编程) Operator是这个系统中最关键的部分,它是一个纽带,一个系在Programmer,Administrator,User之间的纽带。
用一个功能模块来举例子。
一.建立角色功能并做分配:
1.如果现在要做一个员工管理的模块(即Resources),这个模块有三个功能,分别是:增加,修改,删除。给这三个功能各自分配一个ID,这个ID叫做功能代号:
Emp_addEmp,Emp_deleteEmp,Emp_updateEmp。
2.建立一个角色(Role),把上面的功能代码加到这个角色拥有的权限中,并保存到数据库中。角色包括系统管理员,测试人员等。
3.建立一个员工的账号,并把一种或几种角色赋给这个员工。比如说这个员工既可以是公司管理人员,也可以是测试人员等。这样他登录到系统中将会只看到他拥有权限的那些模块。
二.把身份信息加到Session中。
登录时,先到数据库中查找是否存在这个员工,如果存在,再根据员工的sn查找员工的权限信息,把员工所有的权限信息都入到一个Hashmap中,比如就把上面的Emp_addEmp等放到这个Hashmap中。然后把Hashmap保存在一个UserInfoBean中。最后把这个UserInfoBean放到Session中,这样在整个程序的运行过程中,系统随时都可以取得这个用户的身份信息。
三.根据用户的权限做出不同的显示。
可以对比当前员工的权限和给这个菜单分配的"功能ID"判断当前用户是否有打开这个菜单的权限。例如:如果保存员工权限的Hashmap中没有这三个ID的任何一个,那这个菜单就不会显示,如果员工的Hashmap中有任何一个ID,那这个菜单都会显示。
对于一个新闻系统(Resouce),假设它有这样的功能(Privilege):查看,发布,删除,修改;假设对于删除,有"新闻系统管理者只能删除一月前发布的,而超级管理员可删除所有的这样的限制,这属于业务逻辑(Business logic),而不属于用户权限范围。也就是说权限负责有没有删除的Permission,至于能删除哪些内容应该根据UserRole or UserGroup来决定(当然给UserRole or UserGroup分配权限时就应该包含上面两条业务逻辑)。
一个用户可以拥有多种角色,但同一时刻用户只能用一种角色进入系统。角色的划分方法可以根据实际情况划分,按部门或机构进行划分的,至于角色拥有多少权限,这就看系统管理员赋给他多少的权限了。用户—角色—权限的关键是角色。用户登录时是以用户和角色两种属性进行登录的(因为一个用户可以拥有多种角色,但同一时刻只能扮演一种角色),根据角色得到用户的权限,登录后进行初始化。这其中的技巧是同一时刻某一用户只能用一种角色进行登录。
针对不同的"角色"动态的建立不同的组,每个项目建立一个单独的Group,对于新的项目,建立新的 Group 即可。在权限判断部分,应在商业方法上予以控制。比如:不同用户的"操作能力"是不同的(粗粒度的控制应能满足要求),不同用户的"可视区域"是不同的(体现在对被操作的对象的权限数据,是否允许当前用户访问,这需要对业务数据建模的时候考虑权限控制需要)。
扩展性:
有了用户/权限管理的基本框架,Who(User/Group)的概念是不会经常需要扩展的。变化的可能是系统中引入新的 What (新的Resource类型)或者新的How(新的操作方式)。那在三个基本概念中,仅在Permission上进行扩展是不够的。这样的设计中Permission实质上解决了How 的问题,即表示了"怎样"的操作。那么这个"怎样"是在哪一个层次上的定义呢?将Permission定义在"商业方法"级别比较合适。比如,发布、购买、取消。每一个商业方法可以意味着用户进行的一个"动作"。定义在商业逻辑的层次上,一方面保证了数据访问代码的"纯洁性",另一方面在功能上也是"足够"的。也就是说,对更低层次,能自由的访问数据,对更高层次,也能比较精细的控制权限。
确定了Permission定义的合适层次,更进一步,能够发现Permission实际上还隐含了What的概念。也就是说,对于What的How操作才会是一个完整的Operator。比如,"发布"操作,隐含了"信息"的"发布"概念,而对于"商品"而言发布操作是没有意义的。同样的,"购买"操作,隐含了"商品"的"购买"概念。这里的绑定还体现在大量通用的同名的操作上,比如,需要区分"商品的删除"与"信息的删除"这两个同名为"删除"的不同操作。
提供权限系统的扩展能力是在Operator (Resource + Permission)的概念上进行扩展。Proxy 模式是一个非常合适的实现方式。实现大致如下:在业务逻辑层(EJB Session Facade [Stateful SessionBean]中),取得该商业方法的Methodname,再根据Classname和 Methodname 检索Operator 数据,然后依据这个Operator信息和Stateful中保存的User信息判断当前用户是否具备该方法的操作权限。
应用在 EJB 模式下,可以定义一个很明确的 Business层次,而一个Business 可能意味着不同的视图,当多个视图都对应于一个业务逻辑的时候,比如,Swing Client以及 Jsp Client 访问的是同一个 EJB 实现的 Business。在 Business 层上应用权限较能提供集中的控制能力。实际上,如果权限系统提供了查询能力,那么会发现,在视图层次已经可以不去理解权限,它只需要根据查询结果控制界面就可以了。
灵活性:
Group和Role,只是一种辅助实现的手段,不是必需的。如果系统的Role很多,逐个授权违背了"简单,方便"的目的,那就引入Group,将权限相同的Role组成一个Group进行集中授权。Role也一样,是某一类Operator的集合,是为了简化针对多个Operator的操作。
Role把具体的用户和组从权限中解放出来。一个用户可以承担不同的角色,从而实现授权的灵活性。当然,Group也可以实现类似的功能。但实际业务中,Group划分多以行政组织结构或业务功能划分;如果为了权限管理强行将一个用户加入不同的组,会导致管理的复杂性。
Domain的应用。为了授权更灵活,可以将Where或者Scope抽象出来,称之为Domain,真正的授权是在Domain的范围内进行,具体的Resource将分属于不同的Domain。比如:一个新闻机构有国内与国外两大分支,两大分支内又都有不同的资源(体育类、生活类、时事政治类)。假如所有国内新闻的权限规则都是一样的,所有国外新闻的权限规则也相同。则可以建立两个域,分别授权,然后只要将各类新闻与不同的域关联,受域上的权限控制,从而使之简化。
权限系统还应该考虑将功能性的授权与资源性的授权分开。很多系统都只有对系统中的数据(资源)的维护有权限控制,但没有对系统功能的权限控制。
权限系统最好是可以分层管理而不是集中管理。大多客户希望不同的部门能且仅能管理其部门内部的事务,而不是什么都需要一个集中的Administrator或Administrators组来管理。虽然你可以将不同部门的人都加入Administrators组,但他们的权限过大,可以管理整个系统资源而不是该部门资源。
正向授权与负向授权:正向授权在开始时假定主体没有任何权限,然后根据需要授予权限,适合于权限要求严格的系统。负向授权在开始时假定主体有所有权限,然后将某些特殊权限收回。
权限计算策略:系统中User,Group,Role都可以授权,权限可以有正负向之分,在计算用户的净权限时定义一套策略。
系统中应该有一个集中管理权限的AccessService,负责权限的维护(业务管理员、安全管理模块)与使用(最终用户、各功能模块),该AccessService在实现时要同时考虑一般权限与特殊权限。虽然在具体实现上可以有很多,比如用Proxy模式,但应该使这些Proxy依赖于AccessService。各模块功能中调用AccessService来检查是否有相应的权限。所以说,权限管理不是安全管理模块自己一个人的事情,而是与系统各功能模块都有关系。每个功能模块的开发人员都应该熟悉安全管理模块,当然,也要从业务上熟悉本模块的安全规则。
技术实现:
1.表单式认证,这是常用的,但用户到达一个不被授权访问的资源时,Web容器就发
出一个html页面,要求输入用户名和密码。
2.一个基于Servlet Sign in/Sign out来集中处理所有的Request,缺点是必须由应用程序自己来处理。
3.用Filter防止用户访问一些未被授权的资源,Filter会截取所有Request/Response,
然后放置一个验证通过的标识在用户的Session中,然后Filter每次依靠这个标识来决定是否放行Response。
这个模式分为:
Gatekeeper :采取Filter或统一Servlet的方式。
Authenticator: 在Web中使用JAAS自己来实现。
用户资格存储LDAP或数据库:
1. Gatekeeper拦截检查每个到达受保护的资源。首先检查这个用户是否有已经创建
好的Login Session,如果没有,Gatekeeper 检查是否有一个全局的和Authenticator相关的session?
2. 如果没有全局的session,这个用户被导向到Authenticator的Sign-on 页面,
要求提供用户名和密码。
3. Authenticator接受用户名和密码,通过用户的资格系统验证用户。
4. 如果验证成功,Authenticator将创建一个全局Login session,并且导向Gatekeeper
来为这个用户在他的web应用中创建一个Login Session。
5. Authenticator和Gatekeepers联合分享Cookie,或者使用Tokens在Query字符里。

- 作者: mahaixing 2005年03月21日, 星期一 11:26  回复(0) |  引用(0) 加入博采

对Swing线程的再思索 (上)
原文:
http://today.java.net/pub/a/today/2003/10/24/swing.html?page=1
http://blog.csdn.net/chensheng913/archive/2004/08/29/88359.aspx

不正确的Swing线程是运行缓慢、无响应和不稳定的Swing应用的主要原因之一。这是许多原因造成的,从开发人员对Swing单线程模型的误解,到保证正确的线程执行的困难。即使对Swing线程进行了很多努力,应用线程逻辑也是很难理解和维护的。本文阐述了如何在开发Swing应用中使用事件驱动编程,以大大简化开发、维护,并提供高灵活性。

背景

既然我们是要简化Swing应用的线程,首先让我们来看看Swing线程是怎么工作的,为什么它是必须的。Swing API是围绕单线程模型设计的。这意味着Swing组件必须总是通过同一个线程来修改和操纵。为什么采用单线程模型,这有很多原因,包括开发成本和同步Swing的复杂性--这都会造成一个迟钝的API。为了达到单线程模型,有一个专门的线程用于和Swing组件交互。这个线程就是大家熟知的Swing线程,AWT(有时也发音为"ought")线程,或者事件分派线程。在本文的下面的部分,我选用Swing线程的叫法。
既然Swing线程是和Swing组件进行交互的唯一的线程,它就被赋予了很多责任。所有的绘制和图形,鼠标事件,组件事件,按钮事件,和所有其它事件都发生在Swing线程。因为Swing线程的工作已经非常沉重了,当太多其它工作在Swing线程中进行处理时就会发生问题。会引起这个问题的最常见的位置是在非Swing处理的地方,像发生在一个事件监听器方法中,比如JButton的ActionListener,的数据库查找。既然ActionListener的actionPerformed()方法自动在Swing线程中执行,那么,数据库查找也将在Swing线程中执行。这将占用了Swing的工作,阻止它处理它的其它任务--像绘制,响应鼠标移动,处理按钮事件,和应用的缩放。用户以为应用死掉了,但实际上并不是这样。在适当的线程中执行代码对确保系统正常地执行非常重要。

既然我们已经看到了在适当的线程中执行Swing应用的代码是多么重要,现在让我们如何实现这些线程。我们看看将代码放入和移出Swing线程的标准机制。在讲述过程中,我将突出几个和标准机制有关的问题和难点。正如我们看到的,大部分的问题都来自于企图在异步的Swing线程模型上实现同步的代码模型。从那儿,我们将看到如何修改我们的例子到事件驱动--移植整个方式到异步模型。


通用Swing线程解决方案

让我们以一个最常用的Swing线程错误开始。我们将企图使用标准的技术来修正这个问题。在这个过程中,我们将看到实现正确的Swing线程的复杂性和常见困难。并且,注意在修正这个Swing线程问题中,许多中间的例子也是不能工作的。在例子中,我在代码失败的地方以//broken开头标出。好了,现在,让我们进入我们的例子吧。
假设我们在执行图书查找。我们有一个简单的用户界面,包括一个查找文本域,一个查找按钮,和一个输出的文本区域。这个接口如图1所示。不要批评我的UI设计,这个确实很丑陋,我承认。

图 1. 基本查询用户界面
用户输入书的标题,作者或者其它条件,然后显示一个结果的列表。下面的代码例子演示了按钮的ActionListener在同一个线程中调用lookup()方法。在这些例子中,我使用了thread.sleep()休眠5秒来作为一个占位的外部查找。线程休眠的结果等同于一个耗时5秒的同步的服务器调用。
private void searchButton_actionPerformed() {
    outputTA.setText("Searching for: " +
                     searchTF.getText());
    //Broken!! Too much work in the Swing thread
    String[] results = lookup(searchTF.getText());
    outputTA.setText("");
    for (int i = 0; i < results.length; i++) {
        String result = results[i];
        outputTA.setText(outputTA.getText() +
                        '\n' + result);
    }
}

如果你运行这段代码(完整的代码可以在这儿下载),你会立即发现存在一些问题。图2显示了查找运行中的一个屏幕截图。

图 2. 在Swing线程中进行查找
注意Go按钮看起来是被按下了。这是因为actionPerformed方法通知了按钮绘制为非按下外观,但是还没有返回。你也会发现要查找的字串"abcde"并没有出现在文本区域中。searchButton_actionPerformed的第1行代码将文本区域设置为要查找的字串。但是,注意Swing重画并不是立即执行的。而是把重画请求放置到Swing事件队列中等待Swing线程处理。但是这儿,我们因查找处理占用了Swing线程,所以,它还不能马上进行重画。

要修正这些问题,让我们把查找操作移入非Swing线程中。我们第一个想到的就是让整个方法在一个新的线程中执行。这样作的问题是Swing组件,本例中的文本区域,只能从Swing线程中进行编辑。下面是修改后的searchButton_actionPerformed方法:
private void searchButton_actionPerformed() {
    outputTA.setText("Searching for: " +
                     searchTF.getText());
    //the String[][] is used to allow access to
    // setting the results from an inner class
    final String[][] results = new String[1][1];
    new Thread(){
        public void run() {
           results[0] = lookup(searchTF.getText());
        }
    }.start();
    outputTA.setText("");
    for (int i = 0; i < results[0].length; i++) {
        String result = results[0][i];
        outputTA.setText(outputTA.getText() +
                        '\n' + result);
    }
}

这种方法有很多问题。注意final String[][] 。这是一个处理匿名内部类和作用域的不得已的替代。基本上,在匿名内部类中使用的,但在外部环绕类作用域中定义的任何变量都需要定义为final。你可以通过创建一个数组来持有变量解决这个问题。这样的话,你可以创建数组为final的,修改数组中的元素,而不是数组的引用自身。既然我们已经解决这个问题,让我们进入真正的问题所在吧。图3显示了这段代码运行时发生的情况:

 

图 3. 在Swing线程外部进行查找
界面显示了一个null,因为显示代码在查找代码完成前被处理了。这是因为一旦新的线程启动了,代码块继续执行,而不是等待线程执行完毕。这是那些奇怪的并发代码块中的一个,下面将把它编写到一个方法中使其能够真正执行。
在SwingUtilities类中有两个方法可以帮助我们解决这些问题:invokerLater()和invokeAndWait()。每一个方法都以一个Runnable作为参数,并在Swing线程中执行它。invokeAndWait()方法阻塞直到Runnnable执行完毕;invokeLater()异步地执行Runnable。invokeAndWait()一般不赞成使用,因为它可能导致严重的线程死锁,对你的应用造成严重的破坏。所以,让我们把它放置一边,使用invokeLater()方法。
要修正最后一个变量变量scooping和执行顺序的问题,我们必须将文本区域的getText()和setText()方法调用移入一个Runnable,只有在查询结果返回后再执行它,并且在Swing线程中执行。我们可以这样作,创建一个匿名Runnable传递给invokeLater(),包括在新线程的Runnable后的文本区域操作。这保证了Swing代码不会在查找结束之前执行。下面是修正后的代码:
private void searchButton_actionPerformed() {
    outputTA.setText("Searching for: " +
                     searchTF.getText());
    final String[][] results = new String[1][1];
    new Thread() {
        public void run() {
            //get results.
            results[0] = lookup(searchTF.getText());
            // send runnable to the Swing thread
            // the runnable is queued after the
            // results are returned
            SwingUtilities.invokeLater(
                new Runnable() {
                    public void run() {
                        // Now we're in the Swing thread
                        outputTA.setText("");
                        for (int i = 0;
                             i < results[0].length;
                             i++) {
                            String result = results[0][i];
                            outputTA.setText(
                                outputTA.getText() +
                                '\n' + result);
                        }
                    }
                }
            );
        }
    }.start();
}

这可以工作,但是这样做令人非常头痛。我们不得不对通过匿名线程执行的顺序,我们还不得不处理困难的scooping问题。问题并不少见,并且,这只是一个非常简单的例子,我们已经遇到了作用域,变量传递,和执行顺序等一系列问题。相像一个更复杂的问题,包含了几层嵌套,共享的引用和指定的执行顺序。这种方法很快就失控了。

问题

我们在企图强制通过异步模型进行同步执行--企图将一个方形的螺栓放到一个圆形的空中。只有我们尝试这样做,我们就会不断地遭遇这些问题。从我的经验,可以告诉你这些代码很难阅读,很难维护,并且易于出错。
这看起来是一个常见的问题,所以一定有标准的方式来解决,对吗?出现了一些框架用于管理Swing的复杂性,所以让我们来快速预览一下它们可以做什么。
一个可以得到的解决方案是Foxtrot,一个由Biorn Steedom写的框架,可以在SourceForge上获取。它使用一个叫做Worker的对象来控制非Swing任务在非Swing线程中的执行,阻塞直到非Swing任务执行完毕。它简化了Swing线程,允许你编写同步代码,并在Swing线程和非Swing线程直接切换。下面是来自它的站点的一个例子:
public void actionPerformed(ActionEvent e)
{
button.setText("Sleeping...");

    String text = null;
    try
    {
        text = (String)Worker.post(new Task()
           {
              public Object run() throws Exception
              {
                 Thread.sleep(10000);
                 return "Slept !";
              }
           });
    }
    catch (Exception x) ...

    button.setText(text);

    somethingElse();
}


注意它是如何解决上面的那些问题的。我们能够非常容易地在Swing线程中传入传出变量。并且,代码块看起来也很正确--先编写的先执行。但是仍然有一些问题障碍阻止使用从准同步异步解决方案。Foxtrot中的一个问题是异常管理。使用Foxtrot,每次调用Worker必须捕获Exception。这是将执行代理给Worker来解决同步对异步问题的一个产物。
同样以非常相似的方式,我此前也创建了一个框架,我称它为链接运行引擎(Chained Runnable Engine) ,同样也遭受来自类似同步对异步问题的困扰。使用这个框架,你将创建一个将被引擎执行的Runnable的集合。每一个Runnable都有一个指示器告诉引擎是否应该在Swing线程或者另外的线程中执行。引擎也保证Runnable以正确的顺序执行。所以Runnable #2将不会放入队列直到Runnable #1执行完毕。并且,它支持变量以HashMap的形式从Runnable到Runnable传递。
表面上,它看起来解决了我们的主要问题。但是当你深入进去后,同样的问题又冒出来了。本质上,我们并没有改变上面描述的任何东西--我们只是将复杂性隐藏在引擎的后面。因为指数级增长的Runnable而使代码编写将变得非常枯燥,也很复杂,并且这些Runnable常常相互耦合。Runnable之间的非类型的HashMap变量传递变得难于管理。问题的列表还有很多。
在编写这个框架之后,我意识到这需要一个完全不同的解决方案。这让我重新审视了问题,看别人是怎么解决类似的问题的,并深入的研究了Swing的源代码。

 

 

- 作者: mahaixing 2005年03月11日, 星期五 11:03  回复(1) |  引用(0) 加入博采

对Swing线程的再思索 (下)

原文:http://today.java.net/pub/a/today/2003/10/24/swing.html?page=2

        http://blog.csdn.net/chensheng913/archive/2004/08/29/88360.aspx 

解决方案:事件驱动编程


所有前面的这些解决方案都存在一个共同的致命缺陷--企图在持续地改变线程的同时表示一个任务的功能集。但是改变线程需要异步的模型,而线程异步地处理Runnable。问题的部分原因是我们在企图在一个异步的线程模型之上实现一个同步的模型。这是所有Runnable之间的链和依赖,执行顺序和内部类scooping问题的根源。如果我们可以构建真正的异步,我们就可以解决我们的问题并极大地简化Swing线程。
在这之前,让我们先列举一下我们要解决的问题:

1.    在适当的线程中执行代码
2.    使用SwingUtilities.invokeLater()异步地执行.

异步地执行导致了下面的问题:

1.    互相耦合的组件
2.    变量传递的困难
3.    执行的顺序

让我们考虑一下像Java消息服务(JMS)这样的基于消息的系统,因为它们提供了在异步环境中功能组件之间的松散耦合。消息系统触发异步事件,正如在Enterprise Integration Patterns 中描述的。感兴趣的参与者监听该事件,并对事件做成响应--通常通过执行它们自己的一些代码。结果是一组模块化的,松散耦合的组件,组件可以添加到或者从系统中去除而不影响到其它组件。更重要的,组件之间的依赖被最小化了,而每一个组件都是良好定义的和封装的--每一个都仅对自己的工作负责。它们简单地触发消息,其它一些组件将响应这个消息,并对其它组件触发的消息进行响应。
现在,我们先忽略线程问题,将组件解耦并移植到异步环境中。在我们解决了异步问题后,我们将回过头来看看线程问题。正如我们所将要看到的,那时解决这个问题将非常容易。
让我们还拿前面引入的例子,并把它移植到基于事件的模型。首先,我们把lookup调用抽象到一个叫LookupManager的类中。这将允许我们将所有UI类中的数据库逻辑移出,并最终允许我们完全将这两者脱耦。下面是LookupManager类的代码:
class LookupManager {
   private String[] lookup(String text) {
      String[] results = ...
      // database lookup code
      return results
   }
}


现在我们开始向异步模型转换。为了使这个调用异步化,我们需要抽象调用的返回。换句话,方法不能返回任何值。我们将以分辨什么相关的动作是其它类所希望知道的开始。在我们这个例子中最明显的事件是搜索结束事件。所以让我们创建一个监听器接口来响应这些事件。该接口含有单个方法lookupCompleted()。下面是接口的定义:
interface LookupListener {
    public void lookupCompleted(Iterator results);
}


遵守Java的标准,我们创建另外一个称作LookupEvent的类包含结果字串数组,而不是到处直接传递字串数组。这将允许我们在不改变LookupListener接口的情况下传递其它信息。例如,我们可以在LookupEvent中同时包括查找的字串和结果。下面是LookupEvent类:
public class LookupEvent {
    String searchText;
    String[] results;

    public LookupEvent(String searchText) {
        this.searchText = searchText;
    }

    public LookupEvent(String searchText,
                       String[] results) {
        this.searchText = searchText;
        this.results = results;
    }

    public String getSearchText() {
        return searchText;
    }

    public String[] getResults() {
        return results;
    }

}


注意LookupEvent类是不可变的。这是很重要的,因为我们并不知道在传递过程中谁将处理这些事件。除非我们创建事件的保护拷贝来传递给每一个监听者,我们需要把事件做成不可变的。如果不这样,一个监听者可能会无意或者恶意地修订事件对象,并破坏系统。
现在我们需要在LookupManager上调用lookupComplete()事件。我们首先要在LookupManager上添加一个LookupListener的集合:
List listeners = new ArrayList();

并提供在LookupManager上添加和去除LookupListener的方法:
public void addLookupListener(LookupListener listener){
    listeners.add(listener);
}

public void removeLookupListener(LookupListener listener){
    listeners.remove(listener);
}


当动作发生时,我们需要调用监听者的代码。在我们的例子中,我们将在查找返回时触发一个lookupCompleted()事件。这意味着在监听者集合上迭代,并使用一个LookupEvent事件对象调用它们的lookupCompleted()方法。
我喜欢把这些代码析取到一个独立的方法fire[event-method-name] ,其中构造一个事件对象,在监听器集合上迭代,并调用每一个监听器上的适当的方法。这有助于隔离主要逻辑代码和调用监听器的代码。下面是我们的fireLookupCompleted方法:
private void fireLookupCompleted(String searchText,
                                 String[] results){
    LookupEvent event =
        new LookupEvent(searchText, results);
    Iterator iter =
        new ArrayList(listeners).iterator();
    while (iter.hasNext()) {
        LookupListener listener =
            (LookupListener) iter.next();
        listener.lookupCompleted(event);
    }
}


第2行代码创建了一个新的集合,传入原监听器集合。这在监听器响应事件后决定在LookupManager中去除自己时将发挥作用。如果我们不是安全地拷贝集合,在一些监听器应该 被调用而没有被调用时发生令人厌烦的错误。
下面,我们将在动作完成时调用fireLookupCompleted辅助方法。这是lookup方法的返回查询结果的结束处。所以我们可以改变lookup方法使其触发一个事件而不是返回字串数组本身。下面是新的lookup方法:
public void lookup(String text) {
    //mimic the server call delay...
    try {
        Thread.sleep(5000);
    } catch (Exception e){
        e.printStackTrace();
    }
    //imagine we got this from a server
    String[] results =
        new String[]{"Book one",
                     "Book two",
                     "Book three"};
    fireLookupCompleted(text, results);
}


现在让我们把监听器添加到LookupManager。我们希望当查找返回时更新文本区域。以前,我们只是直接调用setText()方法。因为文本区域是和数据库调用一起都在UI中执行的。既然我们已经将查找逻辑从UI中抽象出来了,我们将把UI类作为一个到LookupManager的监听器,监听lookup事件并相应地更新自己。首先我们将在类定义中实现监听器接口:
public class FixedFrame implements LookupListener

接着我们实现接口方法:
public void lookupCompleted(final LookupEvent e) {
outputTA.setText("");
    String[] results = e.getResults();
    for (int i = 0; i < results.length; i++) {
        String result = results[i];
                outputTA.setText(outputTA.getText() +
                                "\n" + result);
    }
}


最后,我们将它注册为LookupManager的一个监听器:
public FixedFrame() {
    lookupManager = new LookupManager();
    //here we register the listener
    lookupManager.addListener(this);
    initComponents();
    layoutComponents();
}


为了简化,我在类的构造器中将它添加为监听器。这在大多数系统上都允许良好。当系统变得更加复杂时,你可能会重构、从构造器中提炼出监听器注册代码,以允许更大的灵活性和扩展性。
到现在为止,你看到了所有组件之间的连接,注意职责的分离。用户界面类负责信息的显示--并且仅负责信息的显示。另一方面,LookupManager类负责所有的lookup连接和逻辑。并且,LookupManager负责在它变化时通知监听器--而不是当变化发生时应该具体做什么。这允许你连接任意多的监听器。
为了演示如何添加新的事件,让我们回头添加一个lookup开始的事件。我们可以添加一个称作lookupStarted()的事件到LookupListener,我们将在查找开始执行前触发它。我们也创建一个fireLookupStarted()事件调用所有LookupListener的lookupStarted()。现在lookup方法如下:
public void lookup(String text) {
    fireLookupStarted(text);

    //mimic the server call delay...
    try {
        Thread.sleep(5000);
    } catch (Exception e){
        e.printStackTrace();
    }
    //imagine we got this from a server
    String[] results =
        new String[]{"Book one",
                     "Book two",
                     "Book three"};

    fireLookupCompleted(text, results);
}

我们也添加新的触发方法fireLookupStarted()。这个方法等同于fireLookupCompleted()方法,除了我们调用监听器上的lookupStarted()方法,并且该事件也不包含结果集。下面是代码:
private void fireLookupStarted(String searchText){
    LookupEvent event =
        new LookupEvent(searchText);
    Iterator iter =
        new ArrayList(listeners).iterator();
    while (iter.hasNext()) {
        LookupListener listener =
            (LookupListener) iter.next();
        listener.lookupStarted(event);
    }
}


最后,我们在UI类上实现lookupStarted()方法,设置文本区域提示当前搜索的字符串。
public void lookupStarted(final LookupEvent e) {
    outputTA.setText("Searching for: " +
                    e.getSearchText());
}


这个例子展示了添加新的事件是多么容易。现在,让我们看看展示事件驱动脱耦的灵活性。我们将通过创建一个日志类,当一个搜索开始和结束时在命令行中输出信息来演示。我们称这个类为Logger。下面是它的代码:
public class Logger implements LookupListener {

    public void lookupStarted(LookupEvent e) {
        System.out.println("Lookup started: " +
                           e.getSearchText());
    }

    public void lookupCompleted(LookupEvent e) {
        System.out.println("Lookup completed: " +
                           e.getSearchText() +
                           " " +
                           e.getResults());
    }

}


现在,我们添加Logger作为在FixedFrame构造方法中的LookupManager的一个监听器。
public FixedFrame() {
    lookupManager = new LookupManager();
    lookupManager.addListener(this);
    lookupManager.addListener(new Logger());
    initComponents();
    layoutComponents();
}


现在你已经看到了添加新的事件、创建新的监听器--向您展示了事件驱动方案的灵活性和扩展性。你会发现随着你更多地开发事件集中的程序,你会更加娴熟地在你的应用中创建通用动作。像其它所有事情一样,这只需要时间和经验。看起来在事件模型上已经做了很多研究,但是你还是需要把它和其它替代方案相比较。考虑开发时间成本;最重要的,这是一次性成本。一旦你创建好了监听器模型和它们的动作,以后向你的应用中添加监听器将是小菜一蝶。

线程

到现在,我们已经解决了上面的异步问题;通过监听器使组件脱耦,通过事件对象传递变量,通过事件产生和监听器的注册的组合决定执行的顺序。让我们回到线程问题,因为正是它把我们带到了这儿。实际上非常容易:因为我们已经有了异步功能的监听器,我们可以简单地让监听器自己决定它们应该在哪个线程中执行。考虑UI类和LookupManager的分离。UI类基于事件,决定需要什么处理。并且,该类也是Swing,而日志类不是。所以让UI类负责决定它应该在什么线程中执行将更加有意义。所以,让我们再次看看UI类。下面是没有线程的lookupCompleted()方法:
public void lookupCompleted(final LookupEvent e) {
outputTA.setText("");
        String[] results = e.getResults();
        for (int i = 0; i < results.length; i++) {
            String result = results[i];
                    outputTA.setText(outputTA.getText() +
                                    "\n" + result);
    }
}


我们知道这将在非Swing线程中调用,因为该事件是直接在LookupManager中触发的,这将不是在Swing线程中执行。因为所有的代码功能上都是异步的(我们不必等待监听器方法允许结束后才调用其它代码),我们可以通过SwingUtilities.invokeLater()将这些代码改道到Swing线程。下面是新的方法,传入一个匿名Runnable到SwingUtilities.invokeLater():
public void lookupCompleted(final LookupEvent e) {
  //notice the threading
  SwingUtilities.invokeLater(
        new Runnable() {
            public void run() {
                outputTA.setText("");
                String[] results = e.getResults();
                for (int i = 0;
                     i < results.length;
                     i++) {
                    String result = results[i];
                    outputTA.setText(outputTA.getText() +
                                    "\n" + result);
                }
            }
        }
    );
}


如果任何LookupListener不是在Swing线程中执行,我们可以在调用线程中执行监听器代码。作为一个原则,我们希望所有的监听器都迅速地接到通知。所以,如果你有一个监听器需要很多时间来处理自己的功能,你应该创建一个新的线程或者把耗时代码放入ThreadPool中等待执行。

最后的步骤是让LookupManager在非Swing线程中执行lookup。当前,LookupManager是在JButton的ActionListener的Swing线程中被调用的。现在是我们做出决定的时候,或者我们在JButton的ActionListener中引入一个新的线程,或者我们可以保证lookup自己在非Swing线程中执行,自己开始一个新的线程。我选择尽可能和Swing类贴近地管理Swing线程。这有助于把所有Swing逻辑封装在一起。如果我们把Swing线程逻辑添加到LookupManager,我们将引入了一层不必要的依赖。并且,对于LookupManager在非Swing线程环境中孵化自己的线程是完全没有必要的,比如一个非绘图的用户界面,在我们的例子中,就是Logger。产生不必要的新线程将损害到你应用的性能,而不是提高性能。LookupManager执行的很好,不管Swing线程与否--所以,我喜欢把代码集中在那儿。
现在我们需要将JButton的ActionListener执行lookup的代码放在一个非Swing线程中。我们创建一个匿名的Thread,使用一个匿名的Runnable执行这个lookup。
private void searchButton_actionPerformed() {
    new Thread(){
        public void run() {
            lookupManager.lookup(searchTF.getText());
        }
    }.start();
}


这就完成了我们的Swing线程。简单地在actionPerformed()方法中添加线程,确保监听器在新的线程中执行照顾到了整个线程问题。注意,我们不用处理像第一个例子那样的任何问题。通过把时间花费在定义一个事件驱动的体系,我们在和Swing线程相关处理上节约了更多的时间。

结论

如果你需要在同一个方法中执行大量的Swing代码和非Swing代码,很容易将某些代码放错位置。事件驱动的方式将迫使你将代码放在它应该在的地方--它仅应该在的地方。如果你在同一个方法中执行数据库调用和更新UI组件,那么你就在一个类中写入了太多的逻辑。分析你系统中的事件,创建底层的事件模型将迫使你将代码放到正确的地方。将费时的数据库调用代码放在非UI类中,也不要在非UI组件中更新UI组件。采用事件驱动的体系,UI负责UI更新,数据库管理类负责数据库调用。在这一点上,每一个封装的类都只用关心自己的线程,不用担心系统其它部分如何动作。当然,设计、构建一个事件驱动的客户端也很有用,但是需要花费的时间代价远超过带来的结果系统的灵活性和可维护性的提高。

- 作者: mahaixing 2005年03月11日, 星期五 11:00  回复(0) |  引用(0) 加入博采

Swing线程的最后讨论 -- 利用异步模型

转贴自:

http://www.softhouse.com.cn/html/200410/2004102610471400001367.html


作者:Joseph Bowbeer

本文并不属于任何系列,但它是The Swing Connection中发表的第三篇关于在Swing中使用线程的文章。
第一篇文章《线程与Swing》,解释了Swing的单线程规则。这篇文章现在可以在The Swing Connection Archive找到。
第二篇文章《使用Swing Worker线程》,演示了如何使用SwingWorker线程工具类。它也可以在存档中找到。
本文介绍了修订过的SwingWorker类,并演示了和基于模型的组件(model-based components)如JTable和JTree同时使用线程。
要理解本文呈现的材料,熟悉SwingWorker和JTable和JTree组件会有所帮助。你可以在存档索引中找到关于JTable和JTree的文章。
本文分为下面几个主要部分:
介绍和回顾
动态树
远程树
SwingWorker修订版
下载
结论
参考资料
关于作者
---------------------------------------------------------------------------

介绍与回顾


在钻研树、表和异步模型之前,我首先回顾一下Swing的单线程规则(single-thread rule)并检验它的含义。
Swing的单线程规则是说,Swing组件在某一时刻仅能被一个线程访问。这个规则对gets和sets都有效,并且"一个线程"通常指的都是事件派发线程。
单线程规则应用于UI组件是非常相称的,因为UI组件往往都以单线程方式使用,并由用户发起大多数动作。构建线程安全的组件是困难而冗长乏味的:如果能避免则最好不过。但除了它的好处,单线程规则有更多的含义。
Swing组件所有的事件都在事件派发线程(event-dispatch thread)中发送和接收,除此之外才不遵守单线程规则。例如,属性变更事件(property-change events)应该在事件派发线程中发送,模型变更事件(model-change events)应该在事件派发线程中接收。
对于基于模型的组件如JTable和JTree来说,单线程规则意味着仅能在事件派发线程中访问模型本身。出于这个理由,模型中的方法必须执行得很快并且决不能阻塞,否则整个用户界面会响应迟钝。
但想象一下你有一个带TreeModel的JTree要访问一个远程服务器?你会发现当服务器不可用或过于繁忙时,你对的TreeModel的调用会阻塞,并使得整个用户界面被冻结。你能做些什么来改进界面的响应能力?
再假设你有一个带TableModel的JTable管理着网络上的一个设备?你会发现当被管理的设备繁忙或网络拥挤时,界面变得迟钝。你该怎么办?
这些困难的情况在召唤线程。当需求浮现,我们还是有几种途径在Swing中使用线程,尽管存在单线程规则。
本文展现了两种方法来使用线程访问缓慢的、远程的或其他异步的模型,并显示了如何在JTree和JTable组件中使用。(不仅仅由事件派发线程访问的模型被称为"异步"模型。)
-----------------------------------------------------------------------------

动态树

假设你有一个带TreeModel的JTree要访问远程服务器,但服务器又是缓慢或不可靠的,你该怎么办?
DynamicTree演示了如何使用后台线程动态展开JTree的节点。
图1是正在处理节点展开的DynamicTree。

图1

分裂模型(Split-model)设计

DynamicTree基于一种分裂模型设计(split-model design)。在这个设计中,真正的模型是一个异步模型,它可能是缓慢或不可靠的。JTree模型使用一个标准的同步模型来维持一个真正的模型的snapshot。(异步模型可能位于远程服务器,出于这个原因我通常称之为远程模型,并且我把Swing组件的模型称为本地模型。)
使用本地模型来影射或缓存远程模型有助于提供一个在所有时刻都可靠的、高响应能力的Swing组件。但这种方法的一个缺点,也是必要的代价是,模型数据不得不重复。另一个问题是,两个模型并非总是同步的,而且它们之间不得不用某种方法来互相协调。

在展开时更新:动态浏览

模型间的一种协调方法是仅在需要数据时更新本地模型,而非在此之前。这种技术在远程模型很慢或很大时是有用的,但这种技术要求模型基本上是静态的。DynamicTree用这种方法来浏览一个缓慢的静态树模型。
DynamicTree开始时是一个未展开的根节点,同时有一个展开监听器(expansion listener)等待用户输入来展开节点。当节点展开后,展开监听器启动一个SwingWorker线程。这个worker的construct()方法访问远程模型并返回新的子节点。然后worker的finished()方法会在事件派发线程中执行,将子节点添加到本地模型。
为简单起见,在同一时刻仅允许一个worker运行。如果有任何节点被折叠,当前活动的任何worker将被中断。(程序并不检查折叠的节点是否是worker正在展开的节点的祖先。)

执行顺序

图2 DynamicTree执行顺序图
本节的余下部分将讨论类结构和实现。你可以也跳过下面的远程模型演示程序。后面的"下载"一节解释了如何下载和运行该演示程序。

实现

图3是一个概略的类结构图。

图3 DynamicTree各类
本地模型是一个包含DefaultMutableTreeNode节点的DefaultTreeModel。节点必须是可变的,以便能够动态地添加子节点。可变节点的userObject域可以用来指向远程模型。

TreeNodeFactory

远程模型实现了TreeNodeFactory接口:

public interface TreeNodeFactory {
  DefaultMutableTreeNode[] createChildren(Object userObject)
      throws Exception;
}

createChildren()在worker线程中被调用。类似于大多数异步和远程方法,它会抛出一个异常(一般是一个RemoteException或InterruptedException)。userObject参数是最近展开的节点指回远程模型的一个链接。一次返回一个包括所有子节点的数组比单独返回每个子节点更高效,还可以避免面对部分失败的情况。
在初始化时,每个子节点都要被设置allowsChildren属性和一个指回远程模型的链接。(如果远程节点是叶子节点,则allowsChildren属性设为false;否则allowsChildren被设为true以表明该节点可被展开。)
DefaultTreeNodeFactory是一个Adapter(请参考GOF的《Design Patterns》)。它将任意TreeModel配接成TreeNodeFactory接口。演示程序中用的是缺省树模型。在main方法中的一些注释掉的代码演示了如何安装一个FileSystemNodeFactory。SlowTreeNodeFactory是演示用的一个包装类;它在运行中插入随机的延迟。

未来的工作

我已经努力使DynamicTree保持简单。节点中除了节点的名字别无其他内容。在节点中需要填充内容的情况下,如果能异步地加载内容会好一些,比如你可能用一个树选择监听器(tree selection listener)来初始化加载过程。
下一节的远程表演示程序会更实用一些。
-------------------------------------------------------------------------------

远程表

假设你有一个带TableModel的JTable管理着一个远程设备,但接口在设备繁忙时慢如龟爬,你该怎么办?
下面的远程表演示程序使用后台线程和异步的回调操作(asynchronous callbacks)来显示或修改一个远程表模型。
图4是一个远程表编辑器正在向服务器发送新的值。未处理的单元格在更新完成前会保持黄色。

图4

RemoteTable bean

远程表演示程序用RMI实现,它由一个服务器和两个客户端(一个编辑器和一个查看器)组成。查看器实际上只是一个禁止了编辑功能的编辑器。
客户端使用一个RemoteTable组件bean,RemoteTable是设计成于远程表模型协作的JTable的子类。图4所示的编辑器由一个RemoteTable组件、一个状态标签、一个简单的活动计量器(右下角)和一些用于定位服务器的代码组成。

得到通知时更新

DynamicTree仅在需要数据时更新它的本地模型,与此不同的是,RemoteTable组件在一开始就获取所有的数据,然后监听此后的变化。基于通知(notification-based)的技术可以和动态模型一起工作,且在模型比较小时工作的很好。
对单元格的修改驱动通知。当用户完成对单元格的编辑,RemoteTable把编辑过的单元格标记为未处理的(黄色突出显示)并且安排一项SwingWorker任务。该woker的construct()方法会把新的值发送到远程模型。
当远程模型接收到新的值,它把变化通知给监听器。Worker的finished()方法的唯一功能是确认任务已完成;在来自远程模型的通知接受并处理完后,黄色的未处理单元格变回普通单元格。

任务队列

RemoteTable用一个QueuedExecutor调度它的SwingWorker任务,QueuedExecutor在单个线程中顺序执行所有的任务。(QueuedExecutor是Doug Lea的util.concurrent包的一部分,请参见后面的"参考资料"部分。)远程模型用RMI回调操作通知它的监听器。
为了支持可视的反馈,RemoteTable发送Task事件给注册的Task监听器。在任务进入调度时,监听器的taskStarted()被调用,taskEnded()在任务完成时被调用。客户端演示程序使用这些事件来启动或停止一个小动画并更新状态来。

执行次序

图5表示的是单元格的更新过程。执行过程的开始和结束都位于左边的事件派发线程。SwingWorker任务则在右边的executor的线程中执行。Worker的finished()方法的执行过程没有表示出来。

图5 RemoteTable执行顺序图

简化

简单起见,远程模型并没有对互相冲突的编辑提供保护。所以在同一时刻仅能有一个编辑器在运行。(并发编辑可以用添加请求ID(request IDs)的方法来实现。)
所作的另一项简化决定是客户端和服务器必须预先对表的列结构协商好。换句话说,服务器性客户端提供行数据,而客户端必须已经知道他们要处理的是什么表。演示用的客户端用DefaultModelTemplate来预先定义了各列的名称和类,来确定哪个单元格可以被编辑。(在演示程序中,前两列是不可编辑的。)
本节的余下部分阐述了类结构和实现。如果不想了解这个演示程序中所用的修订版的SwingWorker,你可以跳过去。"下载"一节解释了如何下载并运行这个演示程序。

实现

图6是一个概略的类结构图。

图6 RemoteTable各类
远程模型实现了RemoteTableModel接口,这个接口和AbstractTableModel很相似,除了它的所有方法都会抛出异常。要启动一个客户端,远程表模型发送一个完全更新事件给客户端已注册的的监听器。
RemoteTableModelAdapter配接任意的TableModel到一个RemoteTableModel。演示程序中的表模型取自The Java Tutorial,但插入了一些延迟来模拟实际情况。远程表模型事件包含已更新的单元格的值。
RemoteTable组件用一个DefaultRemoteTableModelListener来接受来自远程模型的回调。这个监听器会在事件派发线程中更新本地模型。因为远程模型可能要通知插入或删除某些行,所以监听器要求本地模型支持插入和删除操作,DefaultTableModel满足这个要求。

-------------------------------------------------------------------------------

SwingWorker修订版


演示程序用SwingWorker在后台执行费时的操作,然后更新UI。
这个演示程序所用的SwingWorker是基于《使用SwingWorker线程》文中提出的SwingWorker类,但重新实现了它以修正一处竞态条件,添加超时支持,和改进了异常处理。
这个新的实现还基于Doug Lea的util.concurrent包的FutureResult类(参见"参考资料"一节)。由于大量依赖了FutureResult所做的工作,SwingWorker类的实现是简单而灵活的。
本节的余下部分更详细地描述了实现的细节,请继续往下看或直接跳到后面下载源码。

Runnable FutureResult


FutureResult,正如它的名字所暗示的,它是用来保持某动作的结果的。它被设计成和一个Callable共同使用,Callable是一个会返回结果的runnable动作:

public interface Callable {
  Object call() throws Exception;
}

新的SwingWorker是一个Runnable FutureResult。在运行时,它把结果设成construct()的返回值,然后在事件派发线程中调用finished()方法。(注意:SwingWorker是一个抽象类;你要子类化它并实现construct()和finished()。)
下面的代码来自SwingWorker的run()方法:

  Callable function = new Callable() {
    public Object call() throws Exception {
      return construct();
    }
  };

  Runnable doFinished = new Runnable() {
    public void run() {
      finished();
    }
  };

  setter(function).run();
  SwingUtilities.invokeLater(doFinished);

第一段把construct()转换成一个Callable动作,第二段把finished()转换成作为Runnable的doFinished。然后setter(function)被运行,doFinished被调用。

setter(function)


上面缺少的部分是setter(function)。它创建一个刻板的Runnable。在运行时,这个Runnable调用参数指定的function,然后给结果设置返回值。下面是来自FutureResult的代码:

  public Runnable setter(final Callable function) {
    return new Runnable() {
      public void run() {
        try {
          set(function.call());
        }
        catch(Throwable ex) {
          setException(ex);
        }
      }
    };
  }

注意try-catch块所作的防护。如果construct()抛出任何东西(Exception、Error等等),都会被捕捉并记录下来。

不要抢跑:先construct,再start


调用start()来启动worker线程。这是修订版的SwingWorker和原来版本的一个重要区别。
在原来的版本中,SwingWorker()构造器自动启动线程,这种做法带来了一个线程和子类构造器竞争的危险:当SwingWorker()构造器已启动了线程,而子类的构造器还没完成。弥补方法是,先构造SwingWorker,然后再调用start()。
顺便一提,RemoteTable并不调用start()。正确来说,SwingWorker是作为一个Runnable被QueuedExecutor执行的。

超时支持


新的SwingWorker支持超时,这是通过覆盖getTimeout()方法已返回一个非零值来实现的。当超出超时时间,worker线程会被中断。
如果想查看使用超时的例子,请参阅注释版的getTimeout()方法和DynamicTree如何处理TimeoutException。
超时功能是用TimedCallable来实现的,其中使用了FutureResult的timedGet()方法。

增强的异常处理


construct()方法抛出的任何东西都会被记录。除了死循环和死锁,新的异常处理确保了SwingWorker处于"准备好"的状态。也就是说,它要么得到一个正确的结果,要么得到一个异常。
下面的get()方法用来取出结果。这个方法继承自FutureResult:

public Object get()
    throws InvocationTargetException, InterruptedException

如果construct()抛出一个Exception,get()方法就会抛出InvocationTargetException。要获得construct()方法实际上抛出的异常,可以调用getTargetException()。
如果取结果的线程在等待结果的过程中被中断,get()方法会抛出InterruptedException——但这种情况对SwingWorker来说很罕见,因为取结果的线程通常都是事件派发线程,并且在finished()会被调用以前,结果总是已经准备好的。

更多调用工具


SwingWorker的实现在jozart.swingutils包中。在同一个包里,你还能找到InvokeUtils类,这个类还提供了几个invokeXXX()方法。后台线程可以用这些方法来在事件派发线程中获取值和用户输入,再把结果返回到后台线程。

-------------------------------------------------------------------------------

下载


所有演示程序的源码,还有编译好的类文件和资源,都包括在这个zip文件中:threads3 demos.zip
threads3_demos.zip文件包括以下内容:
•    jozart/ 
         o    dynamictree/ - DynamicTree 演示程序。 
         o    remotetable/ - RemoteTable bean 源码。 
         o    remotetabledemo/ - RemoteTable演示程序。 
         o    swingutils/ - SwingWorker. 
•    EDU/ - util.concurrent 包(只包括了用到的类)。 
•    java.policy - RMI 安全策略文件(security policy)。 
•    remotetable.jar - RemoteTable bean (供各IDE使用) 
注意:来自util.concurrent的类是从v1.2.3版选出的。只包括了演示程序中用到的类。
注意:运行这些演示程序需要Java 2。(util.concurrent包有几处用到了Collections。)
运行演示程序,首先请解压threads3_demos.zip到一个空文件夹,注意不要改变里面文件夹的名字。然后换到你把文件解压到的那个文件夹。

运行DynamicTree


将DynamicTree作为applet运行:
  > appletviewer jozart/dynamictree/DynamicTree.html
将DynamicTree作为application运行:
  > java jozart.dynamictree.DynamicTree

运行RemoteTableDemo


远程表服务器和客户端是设计成用RMI分别运行的。但RMI可能难以设置。RMI要求一个RMI registry、一个HTTP服务器和一个安全策略文件。幸运的是,另一个演示程序不那么麻烦,我会先解释怎样运行它。
RemoteTableDemo把一个编辑器、一个查看器和一个服务器都包到了一起。
将RemoteTableDemo作为applet运行:
  > appletviewer jozart/remotetabledemo/RemoteTableDemo.html
将RemoteTableDemo作为application运行:
  > java jozart.remotetabledemo.RemoteTableDemo

分别运行服务器和客户端


要分别运行服务器和客户端,需要执行以下步骤:
1. 确定rmiregistry正在运行:
  > start rmiregistry
2. 确定有一个http服务器正在运行:
  > start httpd
3. 换到演示程序的目录。
  > cd threads3
4. 启动服务器和客户端:
  > java -Djava.security.policy=java.policy
      -Djava.rmi.server.codebase=http://host/threads3/
      jozart.remotetabledemo.RemoteTableServer
  > java -Djava.security.policy=java.policy
      -Djava.rmi.server.codebase=http://host/threads3/
      jozart.remotetabledemo.RemoteTableEditor
  > java -Djava.security.policy=java.policy
      -Djava.rmi.server.codebase=http://host/threads3/
      jozart.remotetabledemo.RemoteTableViewer
你需要在codebase设置中设置你的正确的主机名。你还应该检查java.policy没有给出过多的权限。
更多关于RMI的介绍请参阅The Java Tutorial(参见"参考资料"一节)。

------------------------------------------------------------------------------

结论


本文演示了两种共同使用线程和基于模型的组件(如JTable和JTree)的方法。并提供了一个修订过的SwingWorker工具类。
在一开头,我说明了构建线程安全的组件是困难的。Java在语言级别提供了对线程和锁的支持,但这并不能使得并发编程突然变得容易起来。Swing的单线程规则使得程序员们的日常任务从线程安全的复杂性中解脱出来,但当确实需要线程时却成了阻碍。
为了使得我自己和我的代码的未来使用者在并发编程上的工作更容易些,我尽可能地使用了工具包、证明有效的设计模式和广泛接受的约定。
在文章的最后,我要给任何有兴趣扩展SwingWorker或者开发他们自己的线程工具的读者提个建议:弄清楚你的内存模型。
关于共享资源的访问控制,我还要再说一下。Java的synchronized语句确保了在线程间传递值的安全。这个重要的细节常常被程序员们忽略。关于Java内存模型的更多信息可以参考Doug Lea对《Concurrent Programming in Java》一书的在线补充。这个参考资料和其他参考资料的链接都列在了下一节。

-------------------------------------------------------------------------------

参考资料


1.    Threads and Swing - Hans Muller and Kathy Walrath. 
2.    Using a Swing Worker Thread - Hans Muller and Kathy Walrath. 
3.    Understanding the TreeModel - Eric Armstrong, Tom Santos, Steve Wilson. 
4.    The JTable Class is DB-Aware - Philip Milne and Mark Andrews. 
5.    The Java Tutorial on RMI - Ann Wollrath and Jim Waldo. 
6.    Concurrent Programming in Java - Doug Lea. 
7.    util.concurrent package - Doug Lea. 

-------------------------------------------------------------------------------

关于作者


Joseph Bowbeer是住在西雅图的一个Java顾问。他对并发编程的着迷使他找准了自己的方向。他要感谢Doug Lea、Tom May、David Holmes和Joshua Bloch,感谢他们分享他们的知识。文中的任何错误和疏忽都归咎于他自己。

 

- 作者: mahaixing 2005年03月10日, 星期四 00:20  回复(0) |  引用(0) 加入博采

使用Swing Worker线程 --执行后台任务的新方法

转贴自:

http://www.softhouse.com.cn/html/200410/2004102610530400001371.html


本文给出了一些使用SwingWorker类的例子。SwingWorker类的目的是实现一个后台线程,让你可以用它来执行一些费时的操作,而不影响你的程序的GUI的性能。关于SwingWorker类的一些基本信息,请参阅《线程和Swing》。
注意:在2000年9月我们修改了这篇文章和它的例子以适用于一个更新版本的SwingWorker类。SwingWorker类的这个版本修正了一些微妙的线程bug。
对执行一些费时或可阻塞操作的Swing程序来说,线程是基本的解决之道。例如,如果你的应用程序要根据用户选择的菜单项发出一个数据库请求或加载一个文件,那么你应该在一个单独的线程中完成这些工作。本文阐述了在一个分离的worker线程中完成上述工作的途径。
本文包括以下主要内容:
  • SwingWorker类:这一部分告诉你怎样下载SwingWorker类并描述了SwingWorker类的用途。介绍了SwingWorker类的interrupt()方法。
  • 引入Worker线程的例子:演示一个运用SwingWorker类的应用程序。
  • 例一:中断一个Swing Worker线程:解释如何运用interrupt()方法来中断worker线程。
  • 例二:从Swing Worker线程反馈给用户:例一的增强,添加了一个模式对话框以提示用户输入。

概览:SwingWorker类

因为SwingWorker类并不是Java发行版的一部分,你需要下载和编译它才能使用。它的源代码在这里:
SwingWorker.java
SwingWorker类简介
SwingWorker类可以简单且方便地用于在一个新的线程中计算一个数值。要使用这个类,你只要创建一个SwingWorker的子类并覆盖SwingWorker.construct()方法来执行计算。然后你实例化它,并在这个新实例上调用start()方法。
例如,下面的代码片断中产生了一个线程,其中构造了一个字符串。然后,片断中使用了get()方法来取得前面由construct方法所返回的值,并且在必要时将等待。最后,在显示器上显示出字符串的值。

SwingWorker worker = new SwingWorker() {
   public Object construct() {
      return "Hello" + " " + "World";
   }
};
worker.start();
System.out.println(worker.get().toString());

在实际的应用程序中,construct方法会做些更有用(但可能很费时)的事情。比如,它可能做下列工作之一:
  • 执行大量的运算
  • 执行可能导致大量的类被装载的代码
  • 为网络或磁盘I/O阻塞
  • 等待其他资源

在上面的代码片断中没有展示的一个SwingWorker类的特性是,当construct()返回后,SwingWorker可以让你在事件派发线程中执行一些代码。你可以通过覆盖SwingWorker的finished()方法来做到这一点。典型地,你可以用finished()来显示刚刚构造的一些组件或设置组件上显示的数据。
原始版本的SwingWorker类的一个局限是,一旦一个worker线程开始运行,你无法中断它。(译注:本文、本文的例子及SwingWorker类曾被更新。)不管怎么说,对于一个交互应用程序来说,让用户在工作线程完成前一直等待是相当糟糕的风格。如果用户希望终止一项正在执行中的操作,执行此操作的线程应该能够尽快中止。
使用interrupt()方法

在第二版的SwingWorker类中加入了一个interrupt()方法以允许中断一个线程。你的线程应该以下面两种途径之一得到中断的通知:
  • 正在执行诸如sleep()或wait()方法的线程在interrupt()被调用时会抛出一个InterruptedException。
  • 线程可以显式询问它是否已被中断,通过形如以下代码:
    Thread.interrupted()
     

使用sleep()或wait()方法的工作线程(如后面例子中的线程)一般不需要显式检查是否被中断。通常让sleep()或wait()方法抛出InterruptedException就足够了。
不过,如果你希望能够中断一个不含定时循环的SwingWorker,还是需要用interrupted()方法来显式检查中断。

引入Worker线程的例子


本文余下的部分讨论一个包含两个worker线程的例子的程序。下图是程序主窗口的截图,和它弹出的一个对话框:

例子的源代码由以下文件组成:

  • SwingWorker.java 
  • ThreadsExample.java 
  • Example1.java 
  • Example2.java 


你可以下载一个包含上面所有文件的zip文件。在此zip文件中还包含一个带有HTML格式的说明的例子,这个版本的例子更能说明问题(但也更复杂)。
运行此例子前要先编译:
/usr/local/java/jdk1.3.0/bin/javac ThreadsExample.java
用以下命令行运行此例子:
/usr/local/java/jdk1.3.0/bin/java ThreadsExample
当你按下"Start"按钮,相应例子的worker线程将被创建。你可以在进度条中查看它的进度。你可以按下"Cancel"按钮来中断worker线程。在启动例子二稍等几秒,你会看到一个对话框提示你确认是否执行。稍后我们再详述这个。

例子一:中断Swing Worker线程

接着我们要讨论的例子是Example1.java。这个例子中的worker线程包含一个执行100次的循环,并在两次循环之间睡眠半秒。
//progressBar maximum is NUMLOOPS (100) progressBar的最大值是NUMLOOPS (100)
for(int i = 0; i < NUMLOOPS; i++) {
   updateStatus(i);
   ...
   Thread.sleep(500);
}

为了向你展示如何使用interrupt()方法,我们的例子程序可以让你启动一个SwingWorker然后等待它完成或者中断它。程序中的SwingWorker子类的construct()方法所作的唯一一件事就是调用Example1的doWork()方法。doWork()方法的完整源码列在下面。这个例子在处理worker线程时会重置进度条并把标签设为"Interrupted"。因为中断可能发生在sleep()方法调用之外,所以代码中在调用sleep()方法之前要先检查中断与否。

Object doWork() {
   try {
      for(int i = 0; i < NUMLOOPS; i++) {
         updateStatus(i);
         if (Thread.interrupted()) {
            throw new InterruptedException();
         }
         Thread.sleep(500);
      }
   }
   catch (InterruptedException e) {
      updateStatus(0);
      return "Interrupted";  
   }
   return "All Done"
}

在此方法中执行的费时操作应该:周期性地让用户知道它有所进展。updateStatus()方法会为事件派发线程排队Runnable对象(记住:不要在其他线程中执行GUI工作)。一旦按下"Start"按钮,动作监听器(action listener)会创建SwingWorker,使得worker线程被创建。Worker线程启动后,它将执行它的construct()方法,该方法将调用doWork()(如下面的代码所示)。下面的代码例示了Example1实现的SwingWorker的子类。

worker = new SwingWorker() {
   public Object construct() {
      return doWork();
   }
   public void finished() {
      startButton.setEnabled(true);
      interruptButton.setEnabled(false);
      statusField.setText(get().toString());
   }
};

finished()方法在construct()方法返回后执行(即worker线程完成后)。它的任务只是简单地重新使"Start"按钮有效,同时使"Cancel"按钮无效,并将状态域显示的值设置成worker的计算结果。记住finished()方法是在事件派发线程中执行的,所以它可以安全地直接更新GUI。

例子二:从Worker线程提示用户

这个例子实现为Example1的子类。唯一的区别是,在worker线程执行了大约两秒后,它将阻塞直到用户响应一个Continue/Cancel模态对话框。如果用户选择的不是"Continue",我们就退出doWork()循环。
这个例子演示了一个在许多worker线程中应用的惯用法:如果worker执行中到达一个不期望的状态,它将阻塞起来直到用户被提醒或用一个模态对话框从用户那里收集到了更多信息。这种做法有一点复杂,因为对话框的显示需要放到事件派发线程中,且worker线程需要被阻塞直到用户解除了该模式对话框。
我们使用SwingUtilities的invokeAndWait()方法来在事件派发线程中弹出对话框。与invokeLater()不同,invokeAndWait()会阻塞起来直到它的Runnable对象返回。在我们的例子中,Runnable对象直到对话框被解除才返回。我们创建一个内部Runnable类DoShowDialog,来完成弹出对话框。一个实例变量DoShowDialog.proceedConfirmed,被用来记录用户的选择:

class DoShowDialog implements Runnable {
   boolean proceedConfirmed;
   public void run() {
      Object[] options = {"Continue""Cancel"};
         int n = JOptionPane.showOptionDialog
         (Example2.this,
         "Example2: Continue?",
         "Example2",
         JOptionPane.YES_NO_OPTION,
         OptionPane.QUESTION_MESSAGE,
            null,
            options,
            "Continue");
         proceedConfirmed =
            (n == JOptionPane.YES_OPTION);
   }
}

因为showConfirmDialog()方法弹出一个模态对话框,调用会阻塞直到用户解除该对话框。
为了显示对话框并阻塞调用线程(worker线程)直到对话框被解除,worker线程调用invokeAndWait()方法,定义一个DoShowDialog的实例:

DoShowDialog doShowDialog = new DoShowDialog();
try {
   SwingUtilities.invokeAndWait(doShowDialog);
}
catch 
   (java.lang.reflect.
      InvocationTargetException e) {
      e.printStackTrace();
}

代码中捕获的InvocationTargetException是调试DoShowDialog的run()方法的残留。当invokeAndWait()方法返回后,worker线程可以读取doShowDialog.proceedConfirmed来获得用户的响应。

我们要感谢Doug Lea(《Concurrent Programming in Java》的作者)、Joseph Bowbeer和其他给我们的线程文章和SwingWorker类做出回馈的读者。
-- Hans Muller and Kathy Walrath

 

 
 出处: Java原创社区 FooSleeper 翻译    日期:2004-10-26

- 作者: mahaixing 2005年03月10日, 星期四 00:16  回复(0) |  引用(0) 加入博采

线程与Swing

转贴自:

http://www.softhouse.com.cn/html/200410/2004102610560200001372.html


本文关于Swing中的多线程,发表于1998年4月。一个月后,我们发表了另一篇文章《使用Swing Worker线程》,该文更深入地讨论了这一主题。要更好地了解多线程在Swing中如何工作,我们建议你把这两篇文章都看一下。
    注意:在2000年9月我们修改了这篇文章和它的例子以适用于一个更新版本的SwingWorker类。SwingWorker类的这个版本修正了一些微妙的线程bug。

    Swing API的设计目标是强大、灵活和易用。特别地,我们希望能让程序员们方便地建立新的Swing组件,不论是从头开始还是通过扩展我们所提供的一些组件。
    出于这个目的,我们不要求Swing组件支持多线程访问。相反,我们向组件发送请求并在单一线程中执行请求。
    本文讨论线程和Swing组件。目的不仅是为了帮助你以线程安全的方式使用Swing API,而且解释了我们为什么会选择现在这样的线程方案。
    本文包括以下内容:
  • 单线程规则:Swing线程在同一时刻仅能被一个线程所访问。一般来说,这个线程是事件派发线程(event-dispatching thread)。
  • 规则的例外:有些操作保证是线程安全的。
  • 事件分发:如果你需要从事件处理(event-handling)或绘制代码以外的地方访问UI,那么你可以使用SwingUtilities类的invokeLater()或invokeAndWait()方法。
  • 创建线程:如果你需要创建一个线程——比如用来处理一些耗费大量计算能力或受I/O能力限制的工作——你可以使用一个线程工具类如SwingWorker或Timer。
  • 为什么我们这样实现Swing:我们用一些关于Swing的线程安全的背景资料来结束这篇文章。
Swing的规则是:
    一旦Swing组件被具现化(realized),所有可能影响或依赖于组件状态的代码都应该在事件派发线程中执行。

    这个规则可能听起来有点吓人,但对许多简单的程序来说,你用不着为线程问题操心。在我们深入如何撰写Swing代码之前,让我们先来定义两个术语:具现化(realized)和事件派发线程(event-dispatching thread)。
    具现化的意思是组建的paint()方法已经或可能会被调用。一个作为顶级窗口的Swing组件当调用以下方法时将被具现化:setVisible(true)、show()或(可能令你惊奇)pack()。当一个窗口被具现化,它包含的所有组件都被具现化。另一个具现化一个组件的方法是将它放入到一个已经具现化的容器中。稍后你会看到一些对组件具现化的例子。
    事件派发线程是执行绘制和事件处理的线程。例如,paint()和actionPerformed()方法会自动在事件派发线程中执行。另一个将代码放到事件派发线程中执行的方法是使用SwingUtilities类的invokeLater()方法。
    所有可能影响一个已具现化的Swing组件的代码都必须在事件派发线程中执行。但这个规则有一些例外:
  • 有些方法是线程安全的:在Swing API的文档中,线程安全的方法用以下文字标记:
    This method is thread safe, although most Swing methods are not. 
    (这个方法是线程安全的,尽管大多数Swing方法都不是。)
  • 一个应用程序的GUI常常可以在主线程中构建和显示:下面的典型代码是安全的,只要没有(Swing或其他)组件被具现化:

    public class MyApplication {
    public static void main(String[] args) {
       JFrame f = new JFrame("Labels");
       // 在这里将各组件 
       // 加入到主框架...... 
       f.pack(); 
       f.show(); 
       // 不要再做任何GUI工作...... 
       } 
    }
    上面所示的代码全部在"main"线程中运行。对f.pack()的调用使得JFrame以下的组件都被具现化。这意味着,f.show()调用是不安全的且应该在事件派发线程中执行。尽管如此,只要程序还没有一个看得到的GUI,JFrame或它的里面的组件就几乎不可能在f.show()返回前收到一个paint()调用。因为在f.show()调用之后不再有任何GUI代码,于是所有GUI工作都从主线程转到了事件派发线程,因此前面所讨论的代码实际上是线程安全的。
  • 一个applet的GUI可以在init()方法中构造和显示:现有的浏览器都不会在一个applet的init()和start()方法被调用前绘制它。因而,在一个applet的init()方法中构造GUI是安全的,只要你不对applet中的对象调用show()或setVisible(true)方法。
    要顺便一提的是,如果applet中使用了Swing组件,就必须实现为JApplet的子类。并且,组件应该添加到的JApplet内容窗格(content pane)中,而不要直接添加到JApplet。对任何applet,你都不应该在init()或start()方法中执行费时的初始化操作;而应该启动一个线程来执行费时的任务。
  • 下述JComponent方法是安全的,可以从任何线程调用:repaint()、revalidate()、和invalidate()。repaint()和revalidate()方法为事件派发线程对请求排队,并分别调用paint()和validate()方法。invalidate()方法只在需要确认时标记一个组件和它的所有直接祖先。
  • 监听者列表可以由任何线程修改:调用addListenerTypeListener()和removeListenerTypeListener()方法总是安全的。对监听者列表的添加/删除操作不会对进行中的事件派发有任何影响。
    注意:revalidate()和旧的validate()方法之间的重要区别是,revalidate()会缓存请求并组合成一次validate()调用。这和repaint()缓存并组合绘制请求类似。
    大多数初始化后的GUI工作自然地发生在事件派发线程。一旦GUI成为可见,大多数程序都是由事件驱动的,如按钮动作或鼠标点击,这些总是在事件派发线程中处理的。
    不过,总有些程序需要在GUI成为可见后执行一些非事件驱动的GUI工作。比如:
  • 在成为可用前需要进行长时间初始化操作的程序:这类程序通常应该在初始化期间就显示出GUI,然后更新或改变GUI。初始化过程不应该在事件派发线程中进行;否则,重绘组件和事件派发会停止。尽管如此,在初始化之后,GUI的更新/改变还是应该在事件派发线程中进行,理由是线程安全。
  • 必须响应非AWT事件来更新GUI的程序:例如,想象一个服务器程序从可能运行在其他机器上的程序得到请求。这些请求可能在任何时刻到达,并且会引起在一些可能未知的线程中对服务器的方法调用。这个方法调用怎样更新GUI呢?在事件派发线程中执行GUI更新代码。
    SwingUtilities类提供了两个方法来帮助你在事件派发线程中执行代码:
  • invokeLater():要求在事件派发线程中执行某些代码。这个方法会立即返回,不会等待代码执行完毕。
  • invokeAndWait():行为与invokeLater()类似,除了这个方法会等待代码执行完毕。一般地,你可以用invokeLater()来代替这个方法。

    下面是一些使用这几个API的例子。请同时参阅《The Java Tutorial》中的"BINGO example",尤其是以下几个类:CardWindow、ControlPane、Player和OverallStatusPane。

使用invokeLater()方法


    你可以从任何线程调用invokeLater()方法以请求事件派发线程运行特定代码。你必须把要运行的代码放到一个Runnable对象的run()方法中,并将此Runnable对象设为invokeLater()的参数。invokeLater()方法会立即返回,不等待事件派发线程执行指定代码。这是一个使用invokeLater()方法的例子:

Runnable doWorkRunnable = new Runnable() {
    public void run() { doWork(); }
};
SwingUtilities.invokeLater(doWorkRunnable);

使用invokeAndWait()方法


    invokeAndWait()方法和invokeLater()方法很相似,除了invokeAndWait()方法会等事件派发线程执行了指定代码才返回。在可能的情况下,你应该尽量用invokeLater()来代替invokeAndWait()。如果你真的要使用invokeAndWait(),请确保调用invokeAndWait()的线程不会在调用期间持有任何其他线程可能需要的锁。
    这是一个使用invokeAndWait()的例子:

void showHelloThereDialog() 
        throws Exception {
    Runnable showModalDialog = new 
      Runnable() {
        public void run() {
            JOptionPane.showMessageDialog(
               myMainFrame, "Hello There");
        }
    };
    SwingUtilities.invokeAndWait
       (showModalDialog);
}

    类似地,假设一个线程需要对GUI的状态进行存取,比如文本域的内容,它的代码可能类似这样:

void printTextField() throws Exception {
    final String[] myStrings = 
       new String[2];

    Runnable getTextFieldText = 
      new Runnable() {
        public void run() {
            myStrings[0] = 
               textField0.getText();
            myStrings[1] = 
               textField1.getText();
        }
    };
    SwingUtilities.invokeAndWait
      (getTextFieldText);

    System.out.println(myStrings[0] 
                       + " " + myStrings[1]);
}

    如果你能避免使用线程,最好这样做。线程可能难于使用,并使得程序的debug更困难。一般来说,对于严格意义下的GUI工作,线程是不必要的,比如对组件属性的更新。
不管怎么说,有时候线程是必要的。下列情况是使用线程的一些典型情况:
  • 执行一项费时的任务而不必将事件派发线程锁定。例子包括执行大量计算的情况,会导致大量类被装载的情况(如初始化),和为网络或磁盘I/O而阻塞的情况。
  • 重复地执行一项操作,通常在两次操作间间隔一个预定的时间周期。
  • 要等待来自客户的消息。

    你可以使用两个类来帮助你实现线程:
  • SwingWorker:创建一个后台线程来执行费时的操作。
  • Timer:创建一个线程来执行或多次执行某些代码,在两次执行间间隔用户定义的延迟。

使用SwingWorker类


    SwingWorker类在SwingWorker.java中实现,这个类并不包含在Java的任何发行版中,所以你必须单独下载它。
    SwingWorker类做了所有实现一个后台线程所需的肮脏工作。虽然许多程序都不需要后台线程,后台线程在执行费时的操作时仍然是很有用的,它能提高程序的性能观感。
    SwingWorker's get() method. Here's an example of using SwingWorker:
    要使用SwingWorker类,你首先要实现它的一个子类。在子类中,你必须实现construct()方法还包含你的长时间操作。当你实例化SwingWorker的子类时,SwingWorker创建一个线程但并不启动它。你要调用你的SwingWorker对象的start()方法来启动线程,然后start()方法会调用你的construct()方法。当你需要construct()方法返回的对象时,可以调用SwingWorker类的get()方法。这是一个使用SwingWorker类的例子:

...// 在main方法中:
    final SwingWorker worker = 
      new SwingWorker() {
        public Object construct() {
            return new 
               expensiveDialogComponent();
        }
    };
    worker.start();

...// 在动作事件处理方法中:
    JOptionPane.showMessageDialog
        (f, worker.get());

    当程序的main()方法调用start()方法,SwingWorker启动一个新的线程来实例化ExpensiveDialogComponent。main()方法还构造了由一个窗口和一个按钮组成的GUI。
当用户点击按钮,程序将阻塞,如果必要,阻塞到ExpensiveDialogComponent创建完成。然后程序显示一个包含ExpensiveDialogComponent的模式对话框。你可以在MyApplication.java找到整个程序。

使用Timer类

    Timer类通过一个ActionListener来执行或多次执行一项操作。你创建定时器的时候可以指定操作执行的频率,并且你可以指定定时器的动作事件的监听者(action listener)。启动定时器后,动作监听者的actionPerformed()方法会被(多次)调用来执行操作。
    定时器动作监听者(action listener)定义的actionPerformed()方法将在事件派发线程中调用。这意味着你不必在其中使用invokeLater()方法。
    这是一个使用Timer类来实现动画循环的例子:

public class AnimatorApplicationTimer 
  extends JFrame implements 
  ActionListener {
    ...//在这里定义实例变量
    Timer timer;

    public AnimatorApplicationTimer(...) {
        ...
        // 创建一个定时器来  
        // 来调用此对象action handler。
        timer = new Timer(delay, this);
        timer.setInitialDelay(0);
        timer.setCoalesce(true);
        ...
    }

    public void startAnimation() {
        if (frozen) {
            // 什么都不做。应用户要求 
            // 停止变换图像。
        } else {
            // 启动(或重启动)动画!
            timer.start();
        }
    }

    public void stopAnimation() {
        // 停止动画线程。
        timer.stop();
    }

    public void actionPerformed
      (ActionEvent e) {
        // 进到下一帧动画。
        frameNumber++;

        // 显示。
        repaint();
    }
    ...
}

    在一个线程中执行所有的用户界面代码有这样一些优点:
  • 组件开发者不必对线程编程有深入的理解:像ViewPoint和Trestle这类工具包中的所有组件都必须完全支持多线程访问,使得扩展非常困难,尤其对不精通线程编程的开发者来说。最近的一些工具包如SubArctic和IFC,都采用和Swing类似的设计。
  • 事件以可预知的次序派发:invokeLater()排队的runnable对象从鼠标和键盘事件、定时器事件、绘制请求的同一个队列派发。在一些组件完全支持多线程访问的工具包中,组件的改变被变化无常的线程调度程序穿插到事件处理过程中。这使得全面测试变得困难甚至不可能。
  • 更低的代价:尝试小心锁住临界区的工具包要花费实足的时间和空间在锁的管理上。每当工具包中调用某个可能在客户代码中实现的方法时(如public类中的任何public和protected方法),工具包都要保存它的状态并释放所有锁,以便客户代码能在必要时获得锁。当控制权交回到工具包,工具包又必须重新抓住它的锁并恢复状态。所有应用程序都不得不负担这一代价,即使大多数应用程序并不需要对GUI的并发访问。
    这是的SubArctic Java Toolkit的作者对在工具包中支持多线程访问的问题的描述:
    我们的基本信条是,当设计和建造多线程应用程序,尤其是那些包括GUI组件的应用程序时,必须保证极端小心。线程的使用可能会很有欺骗性。在许多情况下,它们表现得能够极好的简化编成,使得设计"专注于单一任务的简单自治实体"成为可能。在一些情况下它们的确简化了设计和编码。然而,在几乎所有的情况下,它们都使得调试、测试和维护的困难大大增加甚至成为不可能。无论大多数程序员所受的训练、他们的经验和实践,还是我们用来帮助自己的工具,都不是能够用来对付非决定论的。例如,全面测试(这总是困难的)在bug依赖于时间时是几乎不可能的。尤其对于Java来说,一个程序要运行在许多不同类型的机器的操作系统平台上,并且每个程序都必须在抢先和非抢先式调度下都能正常工作。
    由于这些固有的困难,我们力劝你三思是否绝对有使用线程的必要。尽管如此,有些情况下使用线程是必要的(或者是被其他软件包强加的),所以subArctic提供了一个线程安全的访问机制。本章讨论了这一机制和怎样在一个独立线程中安全地操作交互树。
    他们所说的线程安全机制非常类似于SwingUtilities类提供的invokeLater()和invokeAndWait()方法。
 
 出处: Java原创社区 FooSleeper 翻译    日期:2004-10-26

- 作者: mahaixing 2005年03月10日, 星期四 00:10  回复(0) |  引用(0) 加入博采

使用JasperReport和iReport制作java报表
最近使用了JasperReports和iReport制作了几个报表,这片文章是我的一些总结。

JasperReports是一个开源的java报表制作引擎
http://jasperreports.sourceforge.net

iReport是JasperReports的一个GUI工具,用来生成JasperReports的jrxml文件。
http://ireport.sourceforge.net



首先使用iReport制作报表的模板(我自己起的名词):

    运行iReport,新建一个report。
    在Title band中输入报表的名称,好象使用pageheader band也可以,目前我还没有弄明白2者具体的差别。
    在column header band中放置一些static text,做为报表的列标题。
    在detal band中放入一些textfield,显示报表的详细数据。
    在summary band中放入类似于"合计"之类的textfield,这个band是显示在最后一行数据的下放。
    在LastPageFooter band中放入想在最后一页显示的信息,比如说"审阅人签名"之类的。

这样报表的框架就基本上搭好了,然后compile一下检查是否有错误,如果没有错误那么就可以在程序中使用这个报表模板了。

JasperReports介绍:
   
    使用JasperReports生成报表是非常简单的,仅仅使用net.sf.jasperreports.engine包中的几个类即可完成报表的生成、预览、打印、导出等各个功能。

1.  net.sf.jasperreports.engine.JasperCompileManager类。
    使用这个类的几个静态方法即可完成对报表的编译工作(具体参见api文档)
    编译完成后可以JasperCompileManager有两种处理方式:
       1> 返回一个JasperReport对象
       2> 在.jrxml文件所在的目录生成一个.jasper文件
    这两种方式是由程序员自己选择的。不过我比较倾向使用.jasper文件,毕竟报表的结构不是每天都在改动,所以每次重新编译报表并不是很划算。

2.  net.sf.jasperreports.engine.JasperFillManager类
    这个类的作用是用数据填充报表。它可以使用JasperReport对象也可以使用。jasper文件做为报表模板。
    它同样有2中处理方式:
          1> 返回一个JsaperPrint对象。
          2> 在.jasper文件所在目录生成一个.jrprint文件
    这个类使用net.sf.jasperreports.engine.JRDataSource接口的实现做为数据源。任何实现了JRDataSource的类均可做为数据源使用
    在net.sf.jasperreports.engine.data包中定义了一些数据源,可根据自己的需要选择。这里我使用的是JRTableModelDataSource做为
    数据源(因为我的报表还要显示在Table中)。

3. net.sf.jasperreports.engine.JasperPrintManager和net.sf.jasperreports.engine.JasperExportManager
   者两个类的作用是打印、导出报表
   他们使用 JasperPrint 和 .jrprint文件做为输入。
   可以根据自己的需要使用里面的方法。

例:
       import net.sf.jasperreports.engine.*;
       import net.sf.jasperreports.view.*;
       public class CompileReport {
          public static void main(String args[]) {
             try {
                //编译report.jrxml并在report.jrxml所在的目录中生成report.jasper文件
                JasperCompileManager.compileReportToFile("report.jrxml");
                //填充数据,这里使用的是JREmptyDataSource
                JasperFillManager.fillReportToFile("report.jasper", null, new JREmptyDataSource(50));
                //预览报表,false代表不是使用xml文件。
                JasperViewer view = new JasperViewer("reports.jrprint", false);
                view.pack();
                view.setVisible(true);
             }
             catch (Exception e) {
                e.printStackTrace()
             }
          }
       }

这是我制作报表过程中的一点总结,写得并不详细,留待以后补充。

- 作者: mahaixing 2005年03月7日, 星期一 17:41  回复(2) |  引用(0) 加入博采

使用php实现隐藏文件实际位置的文件下载,
        //检查文件是否存在
        if (!is_file($file)) { die("文件不存在"); }
       
        //取得文件大小
        $len = filesize($file);
        //取得不包含路径信息的文件名
        $filename = basename($file);      
       
        //输出http头信息
        header("Pragma: public");
        header("Expires: 0");
        header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
        header("Cache-Control: public");
        header("Content-Description: File Transfer");
        Header("Content-type: application/application/octet-stream");
        header("Content-Disposition: attachment; filename=". $filename);
        header("Content-Transfer-Encoding: binary");
        header("Content-Length: " . $len);
        //输出文件
        @readfile($file);


这个方法可以很好的实现隐藏文件具体位置。
但是有个问题我一直无法解决,就是如何实现断点续传功能。
在实际使用中如果使用flashget之类的断点续传软件会下载不到文件。


更具体的例子可以参见:
http://cn.php.net/manual/zh/function.header.php
中的note部分。

- 作者: mahaixing 2005年03月7日, 星期一 11:50  回复(0) |  引用(0) 加入博采

Linux下java中文解决办法
摘自:
http://www.chinaunix.net/jh/26/15923.html

linux下swing显示中文的方法

第一步:修改 /etc/profile 
增加一句 export JAVA_FONTS=你安装中文字体后的目录

第二步:修改 /你安装jdk的目录/j2sdk1.4.1/jre/lib/font.properties
用你自己想要用的字体替换所有以下内容(可以用gedit编辑它)
替换前 serif.1=--standard symbols l-medium-r-normal--*-%d-*-*-p-*-urw-fontspecific

替换后 serif.1=你自己安装的字体

可以正常显示中文、日文、BIG5,其它的就没有再试了!

- 作者: mahaixing 2005年03月3日, 星期四 16:45  回复(0) |  引用(0) 加入博采