Vincent

Vicent's blog
随笔 - 74, 文章 - 0, 评论 - 5, 引用 - 0
数据加载中……

WebWork教程- Interceptor(拦截器)

     摘要: Interceptor (拦截器)将 Action 共用的行为独立出来,在 Action 执行前后运行。这也就是我们所说的 AOP ( Aspect Oriented Programming ,面向切面编程),它是分散关注的编程方法,它将通用需求功能从不相关类之中分离出来;同时,能够使得很多类共享一个行为,一...  阅读全文

posted @ 2006-09-01 13:39 Binary 阅读(597) | 评论 (0)编辑 收藏

WebWork介绍-Action篇

     摘要: Action 简介 Action 在 MVC 模式中担任控制部分的角色 , 在 WebWork 中使用的最多 , 用于接收页面参数,起到对 HttpRequest 判断处理作用。每个请求的动作都对应于一个相应的 ...  阅读全文

posted @ 2006-09-01 13:38 Binary 阅读(215) | 评论 (0)编辑 收藏

Log4J学习笔记

一、简介
  在程序中输出信息的目的有三:一是监视程序运行情况;一是将程序的运行情况记录到日志文件中,以备将来查看;一是做为调试器。但信息输出的手段不仅限于System.out.println()或System.out.print(),还有日志记录工具可以选择。与System.out.pringln()和System.out.print()相比,日志记录工具可以控制输出级别,并且可以在配置文件中对输出级别进行设置,这样开发阶段的信息在程序发布后就可以通过设置输出级别来消除掉,而无须对代码进行修正了。现在流行的日志记录工具很多, Log4J就是其中的佼佼者。
  Log4J是由著名开源组织Apache推出的一款日志记录工具,供Java编码人员做日志输出之用,可以从网站http://logging.apache.org/log4j上免费获得,最新版本1.2.11。获得logging-log4j-1.2.11.zip文件后,解压缩,需要的是其中的log4j-1.2.11.jar文件,将该文件放到特定的文件夹中备用,我放到了我机器的G:\YPJCCK\Log4J\lib文件夹中。
  这里选择的IDE是Eclipse和JBuilder。Eclipse用的是3.0.1加语言包,可以到www.eclipse.org网站上下载;JBuilder用的是JBuilder 2005。
二、配置类库
  下面打开Eclipse或JBuilder。
  如果使用的是Eclipse,那么在Eclipse打开后,点击菜单"文件"->"新建"->"项目",打开"新建项目"对话框:

请选中"Java项目",点击"下一步",进入"新建Java项目"对话框:

在这个对话框中需要设置项目的名称以及项目所在目录,我为自己的项目起名为Log4JTest,目录为G:\YPJCCK\Log4J\Eclipse\ Log4JTest。设置好后点击"下一步",进入下一个窗口。在这个窗口中选择名为"库"的选项卡,然后点击"添加外部JAR"按钮,将保存于特定文件夹中的log4j-1.2.11.jar文件引用进来。

设置好后,点击"完成",至此,已经具备了在Eclipse下使用Log4J的环境。
  如果使用的是JBuilder,那么在JBuilder打开后,点击菜单"Tools"->"Configure" ->"Libraries",打开"Configure Libraries"对话框:

点击"New"按钮,打开"New Library Wizard"对话框:

使用"Add"按钮将保存于特定文件夹中的log4j-1.2.11.jar文件引用进来,并设置Name,即该类库的名字,我将Name设置为 Log4J。设置好后点击"OK"按钮,回到"Configure Libraries"对话框,再点击"OK"按钮,则JUnit类库已经被添加到JBuilder当中。
  下面继续,在JBuilder中创建项目。点击菜单"File"->"New Project",打开"Project Wizard"对话框:

在这个窗口中设置项目名称及存放目录,我的项目名称仍为Log4JTest,路径为G:/YPJCCK/log4J/JBuilder/Log4JTest。点击"Next"进入下一个窗口:

在这个窗口中选择"Required Libraries"选项卡,点击"Add"按钮,将刚才设置的JUnit库引用进来。然后点击"Next"按钮,进入下一个窗口:

在这个窗口中用鼠标点击Encoding下拉列表框,然后按一下"G"键,选中相应选项,此时该项目的字符集就被设置成GBK了。如果做的是国内项目,这绝对是个好习惯。最后点击"Finish",项目创建完成。
三、编写一个简单的示例
  在了解Log4J的使用方法之前,先编写一个简单的示例,以对Log4J有个感性认识。
如果使用的是Eclipse,请点击"文件"->"新建"->"类",打开"新建Java类"对话框,设置包为 piv.zheng.log4j.test,名称为Test,并确保"public static void main(String[] args)"选项选中;如果使用的是JBuilder,请点击"File"->"New Class",打开"Class Wizard"对话框,设置Package为piv.zheng.log4j.test,Class name为Test,并确保"Generate main method"选项选中。设置完成后,点击"OK"。代码如下:
  package piv.zheng.log4j.test;
  
  import org.apache.log4j.Logger;
  import org.apache.log4j.Level;
  import org.apache.log4j.SimpleLayout;
  import org.apache.log4j.ConsoleAppender;
  
  public class Test {
    
    public static void main(String[] args) {
      SimpleLayout layout = new SimpleLayout();
      
      ConsoleAppender appender = new ConsoleAppender(layout);
      
      Logger log = Logger.getLogger(Test.class);
      log.addAppender(appender);
      log.setLevel(Level.FATAL);
      
      log.debug("Here is DEBUG");
      log.info("Here is INFO");
      log.warn("Here is WARN");
      log.error("Here is ERROR");
      log.fatal("Here is FATAL");
    }
  }
至此,示例编写完成。请点击运行按钮旁边的倒三角,选择"运行为"->"2 Java应用程序"(Eclipse),或者在Test类的选项卡上点击鼠标右键,在调出的快捷菜单中点击"Run using defaults"(JBuilder),运行程序,观察从控制台输出的信息。
四、Log4J入门
  看过程序的运行效果后可能会奇怪,为何控制台只输出了"FATAL - Here is FATAL"这样一条信息,而程序代码中的log.debug()、log.info()等方法也都设置了类似的内容,却没有被输出?其实答案很简单,但在公布之前,先来了解一下Log4J的使用。
  请先看前边的示例代码,会发现,示例中用到了Logger、Level、 ConsoleAppender、SimpleLayout等四个类。其中Logger类使用最多,甚至输出的信息也是在其对象log的fatal方法中设置的,那么Logger究竟是做什么的呢?其实Logger就是传说中的日志记录器(在Log4J中称为Category),创建方法有三:
  1.根Category,默认创建,获取方法:

Logger log = Logger.getRootLogger();

  2.用户创建的Category,方法:

Logger log = Logger.getLogger("test");

其中字符串test是为Category设定的名称。Category的名称允许使用任何字符,但区分大小写,例如:

Logger l1 = Logger.getLogger("x");
Logger l2 = Logger.getLogger("X");

l1和l2就是两个Category;而如果名称完全相同,例如:

Logger l1 = Logger.getLogger("x");
Logger l2 = Logger.getLogger("x");

l1和l2就是同一个Category。此外,符号"."在Category的名称中有特殊作用,这一点将在后边介绍。
  3.与方法2类似,只是参数由字符串换成了类对象,其目的是通过类对象获取类的全名。这个方法比较常用,示例中使用的就是这个方法。
  那么Category是如何输出信息的呢?其实示例中用到的debug、info、warn、error、fatal等五个方法都是用来输出信息的。什么,怎么这么多?原因很简单,Log4J支持分级输出。Log4J的输出级别有五个,由低到高依次是DEBUG(调试)、INFO(信息)、WARN(警告)、ERROR(错误)和FATAL(致命),分别与以上方法对应。当输出级别设置为DEBUG时,以上方法都能够输出信息,当输出级别设置为INFO 时,则只有debug方法将不能再输出信息,依此类推,当输出级别设置为FATAL时,就只有fatal方法可以输出信息了。现在再回头看前边的问题,为何只有设置给fatal方法的信息被输出就不难理解了,示例中有这样一行代码:

log.setLevel(Level.FATAL);

正是这行代码将log对象的输出级别设成了FATAL。在为log对象设置输出级别时用到了Level类,该类中定义了DEBUG、INFO、WARN、 ERROR、FATAL等五个静态对象,与五个输出级别相对应。此外,Level还有两个特殊的静态对象ALL和OFF,前者允许所有的方法输出信息,其级别其实比DEBUG还低;后者则会禁止所有的方法输出信息,其级别比FATAL要高。除前边示例中用到的五个方法,Logger还提供了这五个方法的重载,以在输出信息的同时抛出异常,以fatal方法为例:

log.fatal("Here is FATAL", new Exception("Exception"));

执行后输出信息:
  FATAL - Here is FATAL
  java.lang.Exception: Exception
    at piv.zheng.log4j.test.Test.main(Test.java:24)
其他方法类似。此外,Logger还提供了log方法,该方法不针对任何输出级别,需要在调用时设置,例如:

log.log(Level.FATAL, "Here is FATAL");
log.log(Level.FATAL, "Here is FATAL", new Exception("Exception"));

虽然一般情况下log方法不如其它方法方便,但由于允许设置级别,因此log方法在很多时候反而比其它方法更灵活,甚至可以在输出级别为OFF时输出信息。不过log方法主要是给用户自定义的输出级别用的,而且设立OFF输出级别的目的也为了不输出任何信息,因此请不要在log方法中使用OFF来输出信息。
  此外,Category的输出级别并非必须,若未设置,子Category会默认使用其父Category的输出级别,若父Category也没设置,就使用再上一级Category的设置,直到根Category为止。根Category默认输出级别为DEBUG,因此在示例中,若将 "log.setLevel(Level.FATAL);"一行注释掉,则所有方法都会输出信息。
  下面简单介绍一下Log4J中 Category的继承关系。其实在Log4J中Category之间是存在继承关系的,根Category默认创建,是级别最高的Category,用户创建的Category均继承自它。而用户创建的Category之间也存在继承关系,例如:

Logger lx = Logger.getLogger("x");
Logger lxy = Logger.getLogger("xy");
Logger lx_y = Logger.getLogger("x.y");
Logger lx_z = Logger.getLogger("x.z");
Logger lx_y_z = Logger.getLogger("x.y.z");

其中的lx_y、lx_z就是lx的子Category,而lx_y_z是lx_y的子Category。但lxy并不是lx的子Category。也许有点乱,下面来一个一个看。首先看与lx_y、lx_z对应的Category的名称"x.y"和"x.z","."前边的是什么,"x",这说明与名称为 "x"的Category对应lx就是它们的父Category;而与lx_y_z对应的Category的名称"x.y.z",最后一个"."前边的是什么,"x.y",这说明lx_y是lx_y_z的父Category;至于lxy,由于与之对应的Category名称"xy"之间没有".",因此它是一个与lx同级的Category,其父Category就是根Category器。此外还有一种情况,例如有一个名称为"a.b"的 Category,如果没有名称为"a"的Category,那么它的父Category也是根Category。前边说过,"."在Category名称中有特殊作用,其实它的作用就是继承。至此,为何使用类对象来创建Category也就不难理解了。
  可是,仅有Category是无法完成信息输出的,还需要为Category添加Appender,即Category的输出源。前边的例子使用的是ConsoleAppender,即指定 Category将信息输出到控制台。其实Log4J提供的Appender有很多,这里选择几常用的进行介绍。
  1.org.apache.log4j.WriterAppender,可以根据用户选择将信息输出到Writer或OutputStream。
  示例代码:
    SimpleLayout layout = new SimpleLayout ();
    
    //向文件中输出信息,OutputStream示例
    WriterAppender appender1 = null;
    try {
      appender1 = new WriterAppender(layout, new FileOutputStream("test.txt"));
    }
    catch(Exception ex) {}
    
    //向控制台输出信息,Writer示例
    WriterAppender appender2 = null;
    try {
      appender2 = new WriterAppender(layout, new OutputStreamWriter(System.out));
    }
    catch(Exception ex) {}
    
    //Category支持同时向多个目标输出信息
    Logger log = Logger.getLogger(Test.class);
    log.addAppender(appender1);
    log.addAppender(appender2);
    
    log.debug("output");
这个示例由第一个示例修改而来,没有设置输出级别,而且向Category中添加了两个输出源,运行后会在控制台中输出"DEBUG - output",并在工程目录下生成test.txt文件,该文件中也记录着"DEBUG - output"。若要将test.txt文件放到其它路径下,例如f:,则将"test.txt"改为"f:/test.txt",又如e:下的temp 文件夹,就改为"e:/temp/test.txt"。后边FileAppender、RollingFileAppender以及 DailyRollingFileAppender设置目标文件时也都可以这样来写。
  2.org.apache.log4j.ConsoleAppender,向控制台输出信息,继承了WriterAppender,前边的示例使用的就是它。
  3.org.apache.log4j.FileAppender,向文件输出信息,也继承了WriterAppender。
  示例代码:
    SimpleLayout layout = new SimpleLayout();
    
    //若文件不存在则创建文件,若文件已存在则向文件中追加信息
    FileAppender appender = null;
    try {
      appender = new FileAppender(layout, "test.txt");
    } catch(Exception e) {}
    
    Logger log = Logger.getLogger(Test.class);
    log.addAppender(appender);
    log.debug("output");
这个示例也由第一个示例修改而来,运行后会在工程目录下生成test.txt文件,该文件中记录着"DEBUG - output"。再次运行程序,查看文件,则"DEBUG - output"有两行。
  另外,FileAppender还有一个构造:

FileAppender(Layout layout, String filename, boolean append)

与示例的类似,只是多了一个boolean型的参数append。append参数是个开关,用来设置当程序重启,而目标文件已存在时,是向目标文件追加信息还是覆盖原来的信息,当值为true时就追加,这是FileAppender默认的,当值为false时则覆盖。此外,FileAppender还提供了setAppend方法来设置append开关。
  4.org.apache.log4j.RollingFileAppender,继承了 FileAppender,也是向文件输出信息,但文件大小可以限制。当文件大小超过限制时,该文件会被转为备份文件或删除,然后重新生成。文件的转换或删除与设置的备份文件最大数量有关,当数量大于0时就转为备份文件,否则(小于等于0)删除,默认的备份文件数量是1。转换备份文件非常简单,就是修改文件名,在原文件名之后加上".1",例如文件test.txt,转为备份文件后文件名为"test.txt.1"。但若同名的备份文件已存在,则会先将该备份文件删除或更名,这也与设置的备份文件最大数量有关,若达到最大数量就删除,否则更名。若备份文件更名时也遇到同样情况,则使用同样的处理方法,依此类推,直到达到设置的备份文件最大数量。备份文件更名也很简单,就是将扩展名加1,例如test.txt.1文件更名后变为test.txt.2, test.txt.2文件更名后变为test.txt.3。
  示例代码:
    SimpleLayout layout = new SimpleLayout();
    
    //若文件不存在则创建文件,若文件已存在则向文件中追加内容
    RollingFileAppender appender = null;
    try {
      appender = new RollingFileAppender(layout, "test.txt");
    } catch(Exception e) {}
    //限制备份文件的数量,本例为2个
    appender.setMaxBackupIndex(2);
    //限制目标文件的大小,单位字节,本例为10字节
    appender.setMaximumFileSize(10);
    
    Logger log = Logger.getLogger(Test.class);
    log.addAppender(appender);
    
    log.debug("output0");
    log.debug("output1");
    log.debug("output2");
程序运行后,会在工程目录下生成test.txt、test.txt.1和test.txt.2三个文件,其中test.txt内容为空,而后两个文件则分别记录着"DEBUG - output2"和"DEBUG - output1",这是怎么回事?原来由于目标文件大小被限制为10字节,而三次使用log.debug方法输出的信息都超过了10字节,这样就导致了三次备份文件转换,所以test.txt内容为空。而备份文件最大数量被设为2,因此第一次转换的备份文件就被删掉了,而后两次的则保存下来。此外,由于 test.txt转换备份文件时是先转为test.txt.1,再转为test.txt.2,因此最后test.txt.2的内容是"DEBUG - output1",而test.txt.1是"DEBUG - output2",这点千万别弄混了。
  另外,RollingFileAppender还提供了两个方法:
  (1)setMaxFileSize,功能与setMaximumFileSize一样,但参数是字符串,有两种情况:一是仅由数字组成,默认单位为字节,例如"100",即表示限制文件大小为100字节;一是由数字及存储单位组成,例如"1KB"、"1MB"、"1GB",其中单位不区分大小写,分别表示限制文件大小为1K、1M、1G。
  (2)rollOver,手动将目标文件转换为备份文件,使用起来较灵活,适用于复杂情况。
  示例代码:
    SimpleLayout layout = new SimpleLayout();
    
    RollingFileAppender appender = null;
    try {
      appender = new RollingFileAppender(layout, "test.txt");
    } catch(Exception e) {}
    appender.setMaxBackupIndex(2);

    Logger log = Logger.getLogger(Test.class);
    log.addAppender(appender);
    
    log.debug("output0");
    appender.rollOver();
    log.debug("output1");
    appender.rollOver();
    log.debug("output2");
    appender.rollOver();
这里没限制目标文件大小,但程序运行后,效果与上例相同。
  5.org.apache.log4j.DailyRollingFileAppender,也继承了FileAppender,并且也是向文件输出信息,但会根据设定的时间频率生成备份文件。
  时间频率格式简介:
  '.'yyyy-MM,按月生成,生成时间为每月最后一天午夜过后,例如test.txt在2005年7月31日午夜过后会被更名为test.txt.2005-07,然后重新生成。
  '.'yyyy-ww,按周生成,生成时间为每周六午夜过后,例如test.txt在2005年8月13日午夜过后会被更名为test.txt.2005-33,33表示当年第33周。
  '.'yyyy-MM-dd,按天生成,生成时间为每天午夜过后,例如2005年8月16日午夜过后,test.txt会被更名为test.txt.2005-08-16。
  '.'yyyy-MM-dd-a,也是按天生成,但每天会生成两次,中午12:00过后一次,午夜过后一次,例如test.txt在2005年8月16 日12:00过后会被更名为test.txt.2005-8-16-上午,午夜过后会被更名为test.txt.2005-8-16-下午。
  '.'yyyy-MM-dd-HH,按小时生成,例如test.txt在2005年8月16日12:00过后会被更名为test.txt.2005-8-16-11。
  '.'yyyy-MM-dd-HH-mm,按分钟生成,例如test.txt在2005年8月16日12:00过后会被更名为test.txt.2005-8-16-11-59。
  示例代码:
    SimpleLayout layout = new SimpleLayout();
    
    DailyRollingFileAppender appender = null;
    try {
      appender = new DailyRollingFileAppender(layout, "test.txt", "'.'yyyy-MM-dd-HH-mm");
    } catch(Exception e) {}
    
    Logger log = Logger.getLogger(Test.class);
    log.addAppender(appender);
    log.debug("output");
编码完成后运行程序,等一分钟后再次运行,由于我是在2005年8月17日15:42分第一次运行程序的,因此工程目录下最终有两个文件test.txt和test.txt.2005-08-17-15-42。
  6.org.apache.log4j.AsyncAppender,用于管理不同类型的Appender,也能实现同时向多个源输出信息,但其执行是异步的。
  示例代码:
    SimpleLayout layout = new SimpleLayout();
    
    //向控制台输出
    ConsoleAppender appender1 = null;
    try {
      appender1 = new ConsoleAppender(layout);
    } catch(Exception e) {}
    
    //向文件输出
    FileAppender appender2 = null;
    try {
      appender2 = new FileAppender(layout, "test.txt");
    } catch(Exception e) {}
    
    //使用AsyncAppender实现同时向多个目标输出信息
    AsyncAppender appender = new AsyncAppender();
    appender.addAppender(appender1);
    appender.addAppender(appender2);
    
    Logger log = Logger.getLogger(Test.class);
    log.addAppender(appender);
    log.debug("output");
此外,AsyncAppender和Logger都提供了更多的方法来管理Appender,例如getAppender、 getAllAppenders、removeAppender和removeAllAppenders,分别用来获取指定的Appender、获取全部 Appender、移除指定的Appender以及移除全部Appender。
  7.org.apache.log4j.jdbc.JDBCAppender,将信息输出到数据库。
  示例代码:
    JDBCAppender appender = new JDBCAppender();
    appender.setDriver("com.mysql.jdbc.Driver");
    appender.setURL("jdbc:mysql://localhost:3306/zheng");
    appender.setUser("root");
    appender.setPassword("11111111");
    appender.setSql("insert into log4j (msg) values ('%m')");
    
    Logger log = Logger.getLogger(Test.class);
    log.addAppender(appender);
    log.debug("output");
这里使用的数据库是MySQL 5.0.4beta,用户名root,密码11111111,我在其中建了一个库zheng,包含表log4j,该表只有一个字段msg,类型为varchar(300)。此外,本例用到的JDBC驱动可以从http://dev.mysql.com/downloads/connector/j/3.1.html下载,版本3.1.8a,下载mysql-connector-java-3.1.8a.zip文件后解压缩,需要其中的mysql-connector- java-3.1.8-bin.jar文件。下面再来看代码。由于JDBCAppender内部默认使用PatternLayout格式化输出信息,因此这里没用到SimpleLayout,而appender.setSql所设置的SQL语句就是PatternLayout所需的格式化字符串,故此其中才有"%m"这样的字符,有关PatternLayout的具体内容后边介绍。执行后,表log4j增加一条记录,内容为"output"。
  8.org.apache.log4j.nt.NTEventLogAppender,向Windows NT系统日志输出信息。
  示例代码:
    SimpleLayout layout = new SimpleLayout();
    
    NTEventLogAppender appender = new NTEventLogAppender("Java", layout);

    Logger log = Logger.getLogger(Test.class);
    log.addAppender(appender);
    log.debug("output");
注意,要完成此示例,还需向C:\WINNT\system32文件夹(我的操作系统装在了C:\)中复制一个名为 NTEventLogAppender.dll的文件。如果跟我一样用的是Log4J 1.2.11,实在对不住,Log4J 1.2.11并未提供该文件。虽然logging-log4j-1.2.11.zip文件解压缩后,其下的src\java\org\apache\ log4j\nt文件夹中有一个make.bat文件执行后可以编译出该文件,但还需要配置,很麻烦。还好,条条大道通罗马,1.2.11不行,就换 1.2.9,可以从http://apache.justdn.org/logging/log4j/1.2.9下载,下载后解压缩logging-log4j-1.2.9.zip文件,在其下的src\java\org\apache\log4j\nt文件夹中找到 NTEventLogAppender.dll,复制过去就可以了。程序执行后,打开"事件查看器",选择"应用程序日志",其中有一条来源为Java的记录,这条记录就是刚才输出的信息了。
  9.org.apache.log4j.lf5.LF5Appender,执行时会弹出一个窗口,信息在该窗口中以表格的形式显示。
  示例代码:
    LF5Appender appender = new LF5Appender();
    Logger log = Logger.getLogger(Test.class);
    log.addAppender(appender);
    log.debug("output");
由于LF5Appender不需要Layout格式化输出信息,因此这里没有设置。此外LF5Appender还提供了一个setMaxNumberOfRecords方法,用来限制信息在表格中显示的行数。
  10.org.apache.log4j.net.SocketAppender,以套接字方式向服务器发送日志,然后由服务器将信息输出。
  示例代码:
  //指定要连接的服务器地址及端口,这里使用的是本机9090端口
  SocketAppender appender = new SocketAppender("localhost", 9090);
  Logger log = Logger.getLogger(Test.class);
  log.addAppender(appender);
  log.debug("output");
SocketAppender不需要设置Layout,因为SocketAppender不负责输出信息。那么如何看到信息输出的效果呢?这就需要SocketServer和SimpleSocketServer了。
  示例代码1:
    package piv.zheng.log4j.test;
    
    import org.apache.log4j.net.SocketServer;
    
    public class TestServer {
      public static void main(String[] args) {
        SocketServer.main(new String[]{"9090", "test.properties", "G:/YPJCCK/Log4J"});
      }
    }
这是SocketServer的示例。SocketServer只有一个静态方法main,该方法意味着SocketServer不仅可以在代码中被调用,也可以用java命令执行。main方法只有一个参数,是个字符串数组,但要求必须有三个元素:元素一用来指定端口,本例为9090;元素二用来指定输出信息时需要的配置文件,该文件放在工程目录下,本例使用的test.properties内容如下:
  log4j.rootLogger=, console
  log4j.appender.console =org.apache.log4j.ConsoleAppender
  log4j.appender.console.layout=org.apache.log4j.SimpleLayout
该配置指定SocketServer使用ConsoleAppender以SimpleLayout格式输出信息;元素三用来指定一个路径,以存放.lcf 文件,我指定的是本机的G:/YPJCCK/Log4J文件夹。.lcf文件也是输出信息时使用的配置文件,格式与元素二所指定的配置文件一样,但 test.properties是默认配置文件,即当.lcf文件找不到时才使用。那么.lcf文件如何命名呢?其实.lcf文件的名称并不是随意起的,当SocketAppender与SocketServer建立连接时,SocketServer就会获得SocketAppender所在计算机的IP 地址与网络ID,并将其格式化成"网络ID/IP地址"这样的字符串,然后获取其中的网络ID作为.lcf文件的主名,例如 "zhengyp/127.0.0.1",其中的"zhengyp"就是主文件名,而后再根据这个文件名来调用相应的.lcf文件。这意味着对不同的计算机可以提供不同的配置文件,使信息输出时有不同的效果。此外,SocketServer还默认了一个名为generic.lcf的文件,用于处理网络ID 获取不到或其他情况,本例是用的就是这个文件,内容如下:
  log4j.rootLogger=, console
  log4j.appender.console =org.apache.log4j.ConsoleAppender
  log4j.appender.console.layout=org.apache.log4j.PatternLayout
  log4j.appender.console.layout.ConversionPattern=%m%n
该配置指定SocketServer使用ConsoleAppender以PatternLayout格式输出信息。运行程序时请先运行 SocketServer,再运行SocketAppender。SocketAppender运行结束后,就可以从SocketServer的控制台看到输出的信息了。
  示例代码2:
    package piv.zheng.log4j.test;
    
    import org.apache.log4j.net.SimpleSocketServer;

    public class TestServer {
      public static void main(String[] args) {
        SimpleSocketServer.main(new String[]{"9090", "test.properties"});
      }
    }
这是SimpleSocketServer的示例,与SocketServer相比,只允许指定一个默认的配置文件,而无法对不同计算机使用不同的配置文件。
  11.org.apache.log4j.net.SocketHubAppender,也是以套接字方式发送日志,但与SocketAppender相反,SocketHubAppender是服务器端,而不是客户端。
  示例代码:
    //指定服务器端口,这里使用的是本机9090端口
    SocketHubAppender appender = new SocketHubAppender(9090);
    
    Logger log = Logger.getLogger(Test.class);
    log.addAppender(appender);
    while (true) {
      Thread.sleep(1000);
      log.debug("output"); //输出信息
    }
由于SocketHubAppender一旦运行就开始发送消息,而无论有无接收者,因此这里使用了while语句并将条件设为true以保证程序持续运行。不过为了保证性能,这里还使用了Thread.sleep(1000),这样程序每循环一次都休眠1秒,如果机器性能不好,还可以将值设的再大些。此外,由于SocketHubAppender也不负责输出信息,因此同样不需要设置Layout。那么如何看到信息输出的效果呢?这里我自己写了个客户端程序,代码如下:
  package piv.zheng.log4j.test;
  
  import java.net.Socket;
  import java.lang.Thread;
  import org.apache.log4j.LogManager;
  import org.apache.log4j.PropertyConfigurator;
  import org.apache.log4j.net.SocketNode;
  
  public class TestClient {
    public static void main(String[] args) throws Exception {
      //创建客户端套接字对象
      Socket s = new Socket("localhost", 9090);
      //调用配置文件
      PropertyConfigurator.configure("test.properties");
      //从套接字中恢复Logger,并输出信息
      new Thread(new SocketNode(s, LogManager.getLoggerRepository())).start();
    }
  }
由于SocketHubAppender与SocketAppender一样,发送的也是SocketNode对象,因此编写该程序时参考了 SocketServer的源码。此外,这里的配置文件直接使用了上例的test.properties文件。运行程序时请先运行 SocketHubAppender,再运行客户端程序,然后从客户端的控制台就可以看到效果了。
  13.org.apache.log4j.net.TelnetAppender,与SocketHubAppender有些类似,也是作为服务器发送信息,但TelnetAppender发送的不是SocketNode对象,而是Category输出的结果。
  示例代码:
    SimpleLayout layout = new SimpleLayout();
    
    TelnetAppender appender = new TelnetAppender();
    appender.setLayout(layout); //设置Layout
    appender.setPort(9090); //设置端口号
    appender.activateOptions(); //应用设置
    
    Logger log = Logger.getLogger(Test.class);
    log.addAppender(appender);
    
    while (true) {
      java.lang.Thread.sleep(1000);
      log.debug("output"); //输出信息
    }
    //appender.close();
注意最后一行被注释掉的代码,若该行代码执行,则TelnetAppender的资源会被清理,从而导致TelnetAppender无法继续运行。那么如何看到信息输出的效果呢?这里提供两种方法:方法一,使用Telnet工具,我使用的就是Windows自带的Telnet。运行 TelnetAppender程序后,点击[开始]菜单->[运行],在"运行"框中输入"telnet",回车,telnet客户端弹出,这是一个命令行程序,输入命令"open localhost 9090",回车,然后就可以看到效果了。方法二,自己写程序,代码如下:
  package piv.zheng.log4j.test;
  
  import java.net.*;
  import java.io.*;
  
  public class TestClient {
    public static void main(String[] args) throws Exception {
      //创建客户端套接字对象
      Socket s = new Socket("localhost", 9090);
      //将BufferedReader与Socket绑定,以输出Socket获得的信息
      BufferedReader in = new BufferedReader(new InputStreamReader(s.getInputStream()));
      //获得信息并输出
      String line = in.readLine();
      while (line != null) {
        System.out.println(line);
        line = in.readLine();
      }
    }
  }
  13.org.apache.log4j.net.SMTPAppender,向指定的电子邮件发送信息,但只能发送ERROR和FATAL级别的信息,而且还没提供身份验证功能。
  示例代码:
  SimpleLayout loyout = new SimpleLayout();
  
  SMTPAppender appender = new SMTPAppender();
  appender.setLayout(loyout); //设置Layout
  appender.setFrom("zhengyp@126.com"); //设置发件人
  appender.setSMTPHost("smtp.126.com"); //设置发送邮件服务器
  appender.setTo("zhengyp@126.com"); //设置收件人
  appender.setSubject("Log4J Test"); //设置邮件标题
  appender.activateOptions(); //应用设置
  
  Logger log = Logger.getLogger(Test.class);
  log.addAppender(appender);
  log.debug("Here is DEBUG");
  log.info("Here is INFO");
  log.warn("Here is WARN");
  log.error("Here is ERROR");
  log.fatal("Here is FATAL");
要运行此示例,还需要JavaMail 和JAF,前者是Sun推出的电子邮件类库,可以从http://java.sun.com/products/javamail/downloads/index.html下载,最新版本1.3.3,下载javamail-1_3_3-ea.zip压缩包后需要其中的mail.jar文件;后者全称是JavaBeans Activation Framework,提供了对输入任意数据块的支持,并能相应地对其进行处理,可以从http://www.sun.com/download中找到,最新版本1.1,下载jaf-1_1-ea.zip压缩包后需要其中的activation.jar文件。不过,程序运行后会抛出两次异常,分别是log.error和log.fatal方法导致的,失败的原因很简单,我用的邮件服务器需要身份验证。
  14.piv.zheng.log4j.test.SMTPAppender,自定义的,依照Log4J提供的SMTPAppender修改而来,增加了身份验证功能,并去掉了对级别的限制。由于代码太长,所以放到了另一篇文章《自定义SMTPAppender的源码》中,有兴趣的请自行去查看。
  示例代码:
    SimpleLayout layout = new SimpleLayout();
    
    SMTPAppender appender = new SMTPAppender(layout);
    appender.setFrom("zhengyp@126.com"); //发件人
    appender.setSMTPHost("smtp.126.com"); //发送邮件服务器
    appender.setTo("zhengyp@126.com"); //收件人
    appender.setSubject("Log4J Test"); //邮件标题
    appender.setAuth("true"); //身份验证标识
    appender.setUsername("zhengyp"); //用户名
    appender.setPassword("1111111"); //密码
    appender.activateOptions(); //应用设置
    
    Logger log = Logger.getLogger(Test.class);
    log.addAppender(appender);
    log.debug("output");
同样需要JavaMail 和JAF。程序运行后会发送一封邮件,快去查看一下自己的邮箱吧^_^
  此外,Log4J还提供了SyslogAppender、JMSAppender(均在org.apache.log4j.net包下)以及更多的 Appender,或者用来向Unix操作系统的syslogd服务发送信息,或者通过JMS方式发送信息,或者以其他方式发送信息。由于条件有现,就不再介绍了。
  不过,在前边的示例中还使用了SimpleLayout和PatternLayout来格式化输出的信息,这里也简单介绍一下。
  1.org.apache.log4j.SimpleLayout,一直用的就是它,输出的格式比较简单,就是"级别 - 信息"。
  2.org.apache.log4j.HTMLLayout,以HTML格式输出信息。
  示例代码:
    HTMLLayout layout = new HTMLLayout();
    layout.setTitle("Log4J Test"); //HTML页标题
    
    FileAppender appender = null;
    try {
      appender = new FileAppender(layout, "test.html");
    } catch(Exception e) {}
    
    Logger log = Logger.getLogger(Test.class);
    log.addAppender(appender);
    log.debug("output");
程序运行后会在工程目录下生成一个HTML页,可以用浏览器来查看。
  3.org.apache.log4j.xml.XMLLayout,以XML格式输出信息。
  示例代码:
    XMLLayout layout = new XMLLayout();
    
    FileAppender appender = null;
    try {
      appender = new FileAppender(layout, "test.xml");
    } catch(Exception e) {}
    
    Logger log = Logger.getLogger(Test.class);
    log.addAppender(appender);
    log.debug("output");
程序运行后会在工程目录下生成一个test.xml文件。
  4.org.apache.log4j.TTCCLayout,输出信息的同时输出日志产生时间、相关线程及Category等信息。
  示例代码:
    TTCCLayout layout = new TTCCLayout();
    //是否打印与TTCCLayout关联的Category的名称,默认为true,表示打印
    layout.setCategoryPrefixing(true);
    //是否打印当前线程,默认为true,表示打印
    layout.setThreadPrinting(true);
    //是否打印输出和当前线程相关的NDC信息,默认为true,表示打印
    layout.setContextPrinting(true);
    //设置日期时间格式
    layout.setDateFormat("iso8601");
    //设置时区
    layout.setTimeZone("GMT+8:00");
    //设置时区后需要调用此方法应用设置
    layout.activateOptions();
    
    ConsoleAppender appender = new ConsoleAppender(layout);
    
    Logger log = Logger.getLogger(Test.class);
    log.addAppender(appender);
    log.debug("output");
注意,TTCCLayout输出的时间格式及时区是可以设置的:
  (1)setDateFormat,设置日期时间格式,有五个常用值:"NULL",表示不输出;"RELATIVE",输出信息所用的时间,以毫秒为单位,默认使用该值;"ABSOLUTE",仅输出时间部分;"DATE",按当前所在地区显示日期和时间;"ISO8601",按ISO8601标准显示日期和时间。这些字符串不区分大小写。此外,还可以使用时间模式字符来格式化日期时间,详细内容请参考J2SE文档中的 java.text.SimpleDateFormat类。
  (2)setTimeZone,设置时区,详细内容请参考J2SE文档中的java.util.TimeZone类和java.util.SimpleTimeZone类。但请注意,当日期格式为"RELATIVE"时,设置时区会造成冲突。
  5.org.apache.log4j.PatternLayout,用模式字符灵活指定信息输出的格式。
  示例代码:
    String pattern = "Logger: %c %n"
        + "Date: %d{DATE} %n"
        + "Message: %m %n";
    PatternLayout layout = new PatternLayout(pattern);
    
    ConsoleAppender appender = new ConsoleAppender(layout);
    
    Logger log = Logger.getLogger(Test.class);
    log.addAppender(appender);
    log.debug("output");
模式字符串简介:
  %c:Category名称。还可以使用%c{n}的格式输出Category的部分名称,其中n为正整数,输出时会从Category名称的右侧起查 n个".",然后截取第n个"."右侧的部分输出,例如Category的名称为"x.y.z",指定格式为"%c{2}",则输出"y.z"。
  %C:输出信息时Category所在类的名称,也可以使用%C{n}的格式输出。
  %d:输出信息的时间,也可以用%d{FormatString}的格式输出,其中FormatString的值请参考TTCCLayout的setDateFormat方法,但NULL和RELATIVE在%d中无法使用。
  %F:输出信息时Category所在类文件的名称。
  %l:输出信息时Category所在的位置,使用"%C.%M(%F:%L)"可以产生同样的效果。
  %L:输出信息时Category在类文件中的行号。
  %m:信息本身。
  %M:输出信息时Category所在的方法。
  %n:换行符,可以理解成回车。
  %p:日志级别。
  %r:输出信息所用的时间,以毫秒为单位。
  %t:当前线程。
  %x:输出和当前线程相关的NDC信息。
  %X:输出与当前现成相关的MDC信息。
  %%:输出%。
此外,还可以在%与模式字符之间加上修饰符来设置输出时的最小宽度、最大宽度及文本对齐方式,例如:
  %30d{DATE}:按当前所在地区显示日期和时间,并指定最小宽度为30,当输出信息少于30个字符时会补以空格并右对齐。
  %-30d{DATE}:也是按当前所在地区显示日期和时间,指定最小宽度为30,并在字符少于30时补以空格,但由于使用了"-",因此对齐方式为左对齐,与默认情况一样。
  %.40d{DATE}:也是按当前所在地区显示日期和时间,但指定最大宽度为40,当输出信息多于40个字符时会将左边多出的字符截掉。此外,最大宽度只支持默认的左对齐方式,而不支持右对齐。
  %30.40d{DATE}:如果输出信息少于30个字符就补空格并右对齐,如果多于40个字符,就将左边多出的字符截掉。
  %-30.40d{DATE}:如果输出信息少于30个字符就补空格并左对齐,如果多于40个字符,就将左边多出的字符截掉。
五、Log4J进阶
  了解以上内容后,就已经初步掌握Log4J了,但要想灵活使用Log4J,则还需要了解其配置功能。这里简单介绍一下。
  1.org.apache.log4j.BasicConfigurator,默认使用ConsoleAppender以PatternLayout (使用PatternLayout.TTCC_CONVERSION_PATTERN,即"%r [%t] %p %c %x - %m%n"格式)输出信息。
  示例代码:
    BasicConfigurator.configure();
    Logger log = Logger.getLogger(Test.class);
    log.debug("output");
注意,BasicConfigurator及其它Configurator其实都只对根Category进行配置,但由于用户创建的Category会继承根Category的特性(声明,许多资料介绍Category继承关系时都主要在讨论输出级别,而事实上,Category间继承的不仅是输出级别,所有特性都可以继承),因此输出时仍会显示BasicConfigurator配置的效果。此外,还可以使用configure方法指定Appender,以自定义输出。BasicConfigurator允许同时指定多个Appender。
  示例代码:
    SimpleLayout layout1 = new SimpleLayout();
    ConsoleAppender appender1 = new ConsoleAppender(layout1);
    BasicConfigurator.configure(appender1);
    
    String pattern = "Logger: %c %n"
        + "Date: %d{DATE} %n"
        + "Message: %m %n";
    PatternLayout layout2 = new PatternLayout(pattern);
    FileAppender appender2 = null;
    try {
      appender2 = new FileAppender(layout2, "test.log", false);
    }
    catch(Exception e){}
    BasicConfigurator.configure(appender2);
    
    Logger log = Logger.getLogger(Test.class);
    log.debug("output");
这里用BasicConfigurator指定了两个Appender,即ConsoleAppender和FileAppender,程序运行后信息会在以SimpleLayout输出到控制台的同时以PatternLayout输出到test.log文件。若要清除这些Appender,可以调用 BasicConfigurator的resetConfiguration方法。
  2. org.apache.log4j.PropertyConfigurator,调用文本配置文件输出信息,通常使用.properties文件。配置文件以"键=值"的形式保存数据,注释以"#"开头。PropertyConfigurator和配置文件在介绍SocketAppender和 SocketHubAppender时曾提到过。使用PropertyConfigurator可以避免硬编码。
  示例代码:
    PropertyConfigurator.configure("test.properties");
    Logger log = Logger.getLogger(Test.class);
    log.debug("output");
要完成该示例,还需要在工程目录下创建一个test.properties文件,内容如下:
  ##设置根Category,其值由输出级别和指定的Appender两部分组成
  #这里设置输出级别为DEBUG
  log4j.rootLogger=DEBUG,appender
  ##输出信息到控制台
  #创建一个名为appender的Appender,类型为ConsoleAppender
  log4j.appender.appender=org.apache.log4j.ConsoleAppender
  #设置appender以SimpleLayout输出
  log4j.appender.appender.layout=org.apache.log4j.SimpleLayout
此外,PropertyConfigurator也允许同时指定多个Appender,例如:
  #这里没有设置输出级别,但指定了两个Appender
  log4j.rootLogger=,appender1,appender2
  #输出信息到控制台
  log4j.appender.appender1=org.apache.log4j.ConsoleAppender
  log4j.appender.appender1.layout=org.apache.log4j.SimpleLayout
  #输出信息到文件
  log4j.appender.appender2=org.apache.log4j.FileAppender
  log4j.appender.appender2.File=test.log
  log4j.appender.appender2.Append=false
  log4j.appender.appender2.layout=org.apache.log4j.PatternLayout
  log4j.appender.appender2.layout.ConversionPattern=Logger: %c %nDate: %d{DATE} %nMessage: %m %n
关于更多配置,网上示例很多,这里不再赘述。但要说明一件事,就是配置文件中的键是怎么来的。参照后一个示例,查看 PropertyConfigurator源码,会发现"log4j.rootLogger"是定义好的,只能照写;而"log4j.appender" 字样也可以找到,与指定的Appender名称appender1、appender2联系起来,log4j.appender.appender1和 log4j.appender.appender2也就不难理解了;再看下去,还能找到"prefix + ".layout"",这样log4j.appender.appender1.layout也有了;可是 log4j.appender.appender2.File 和log4j.appender.appender2.Append呢?还记得前边介绍FileAppender时曾提到的setAppend方法吗?其实FileAppender还有个getAppend方法,这说明FileAppender具有Append属性。那么File呢?当然也是 FileAppender的属性了。至于log4j.appender.appender2.layout.ConversionPattern也一样,只不过FileAppender换成了PatternLayout。其实别的Appender和Layout的属性也都是这样定义成键来进行设置的。此外,定义键时,属性的首字母不区分大小写,例如"File",也可以写成"file"。
  3. org.apache.log4j.xml.DOMConfigurator,调用XML配置文件输出信息。其定义文档是log4j- 1.2.11.jar中org\apache\log4j\xml包下的log4j.dtd文件。与PropertyConfigurator相比, DOMConfigurator似乎是趋势。
  示例代码:
    DOMConfigurator.configure("test.xml");
    Logger log = Logger.getLogger(Test.class);
    log.debug("output");
要完成该示例,也需要在工程目录下创建一个test.xml文件,内容如下:
  <?xml version="1.0" encoding="UTF-8" ?>
  <!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
  <log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
    <!-- 输出信息到控制台
    创建一个名为appender的Appender,类型为ConsoleAppender -->
    <appender name="appender" class="org.apache.log4j.ConsoleAppender">
      <!-- 设置appender以SimpleLayout输出 -->
      <layout class="org.apache.log4j.SimpleLayout"/>
    </appender>
    <!-- 设置根Category,其值由输出级别和指定的Appender两部分组成
    这里设置输出级别为DEBUG -->
    <root>
      <priority value ="debug" />
      <appender-ref ref="appender"/>
    </root>
  </log4j:configuration>
此外,DOMConfigurator也允许同时指定多个Appender,例如:
  <?xml version="1.0" encoding="UTF-8" ?>
  <!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
  <log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
    <!-- 输出信息到控制台 -->
    <appender name="appender1" class="org.apache.log4j.ConsoleAppender">
      <layout class="org.apache.log4j.SimpleLayout"/>
    </appender>
    <!-- 输出信息到文件 -->
    <appender name="appender2" class="org.apache.log4j.FileAppender">
      <param name="File" value="test.log"/>
      <param name="Append" value="false"/>
      <layout class="org.apache.log4j.PatternLayout">
        <param name="ConversionPattern" value="Logger: %c %nDate: %d{DATE} %nMessage: %m %n"/>
      </layout>
    </appender>
    <!-- 这里没有设置输出级别,但指定了两个Appender -->
    <root>
      <appender-ref ref="appender1"/>
      <appender-ref ref="appender2"/>
    </root>
  </log4j:configuration>
由于以上两个示例是在PropertyConfigurator的两个示例基础上改的,而且也写了注释,因此这里只简单介绍一下<param> 标记。<param>标记有两个属性,name和value,前者的值也是Appender或Layout的属性名,作用与 log4j.appender.appender2.File这样的键一样。设置时,首字母同样不区分大小写,例如"File"也可以写成"file"。此外还请注意,使用这两段XML代码时应将中文注释去掉,或者把<?xml version="1.0" encoding="UTF-8" ?>中的UTF-8改成GBK或GB2312,否则会导致错误。这里使用的UTF-8是XML默认的字符集。
  4. org.apache.log4j.lf5.DefaultLF5Configurator,默认使用LF5Appender来输出信息,需要调用 log4j-1.2.11.jar中org\apache\log4j\lf5\config包下的defaultconfig.properties文件。
  示例代码:
    try {
      DefaultLF5Configurator.configure();
    }
    catch(Exception e){}
    Logger log = Logger.getLogger(Test.class);
    log.debug("output");
  下面讨论另外一个话题:Diagnostic Context。Diagnostic Context意为诊断环境,针对于多用户并发环境,在这种环境下,通常需要对每个客户端提供独立的线程以处理其请求,此时若要在日志信息中对客户端加以区分,为每个线程分别创建Category是个办法。但这样做并不高效,反而会导致大量资源被占用。Diagnostic Context所要解决的就是这个问题。Diagnostic Context会为当前线程提供一定空间,然后将信息保存到该空间供Category调用。与创建一个Category相比,这点信息所占的资源自然要少得多。
  1.org.apache.log4j.NDC。NDC是Nested Diagnostic Context的简写,意为嵌套诊断环境,使用时提供一个堆栈对象来保存信息。堆栈的特点是数据后进先出、先进后出,即清理堆栈时,后保存的数据会被先清掉,而先保存的数据则被后清掉。
  示例代码:
    PatternLayout layout = new PatternLayout("%m %x%n");
    ConsoleAppender appender = new ConsoleAppender(layout);
    Logger log = Logger.getLogger(Test.class);
    log.addAppender(appender);
    
    String tmp = "zhengyp"; //模拟从客户端获取的信息
    log.debug("Start");
    NDC.push(tmp); //添加信息到堆栈中
    log.debug("Before");
    NDC.pop(); //将信息从堆栈中移除
    log.debug("After");
    NDC.remove(); //将当前线程移除,退出NDC环境
    log.debug("End");
这里使用了PatternLayout来格式化信息,其模式字符%x就是用来输出NDC信息的。程序运行后会输出如下内容:
  Start
  Before zhengyp
  After
  End
可以看到,第二行输出时由于已向堆栈中添加了信息,因此"zhengyp"也会同时输出;而第三行输出时由于信息已被移除,因此就没再输出"zhengyp"。不过这个示例仅简单演示了NDC的用法,而没有显示出NDC的堆栈特性,所以下面再提供一个示例,代码如下:
  TTCCLayout layout = new TTCCLayout();
  ConsoleAppender appender = new ConsoleAppender(layout);
  Logger log = Logger.getLogger(Test.class);
  log.addAppender(appender);
  
  log.debug("Start");
  NDC.push("zhengyp"); //添加信息到堆栈中
  log.debug("Test1");
  NDC.push("192.168.0.1"); //向堆栈中追加信息
  log.debug("Test2");
  NDC.pop(); //从堆栈中移除信息,但移除的只是最后的信息
  log.debug("Test3");
  NDC.pop(); //再次从堆栈中移除信息
  log.debug("Test4");   
  log.debug("End");
这里格式化输出信息使用的是TTCCLayout,还记得其setContextPrinting方法吗?程序运行后,从输出的信息就可以看到效果了。此外,NDC还提供了其他方法:
  (1)get,获取堆栈中的全部信息。以上例为例,当输出Test2时,使用该方法会获得"zhengyp 192.168.0.1"。
  (2)peek,获取堆栈中最后的信息。仍以上例为例,当输出Test1时会获得"zhengyp",Test2时为"192.168.0.1",而当输出Test3时由于"192.168.0.1"已被移除,"zhengyp"又成了最后的信息,因此获得的仍是"zhengyp"。
  (3)clear,清空堆栈中的全部信息。
  (4)setMaxDepth,设置堆栈的最大深度,即当前的信息可以保留多少,对之后追加的信息没有影响。当需要一次清掉多条信息时,使用setMaxDepth会比多次调用pop方便。
  2.org.apache.log4j.MDC。MDC是Mapped Diagnostic Context的简写,意为映射诊断环境,提供了一个Map对象来保存信息。Map对象使用Key、Value的形式保存值。
  示例代码:
    PatternLayout layout = new PatternLayout("%m %X{name} %X{ip}%n");
    ConsoleAppender appender = new ConsoleAppender(layout);
    Logger log = Logger.getLogger(Test.class);
    log.addAppender(appender);
    
    log.debug("Start");
    //添加信息到Map中
    MDC.put("name", "zhengyp1");
    MDC.put("ip", "192.168.1.1");
    log.debug("Test1");
    
    //添加信息到Map中,若Key重复,则覆盖之前的值
    MDC.put("name", "zhengyp2");
    MDC.put("ip", "192.168.1.2");
    log.debug("Test2");
    
    //将信息从Map中移除,此时信息不再输出
    MDC.remove("name");
    MDC.remove("ip");
    log.debug("End");
这个示例演示了MDC的基本用法,格式化信息用的也是PatternLayout,模式字符为"%X",其格式必须为"%X{Key}"。其中Key就是向 Map对象添加信息时put方法所用的Key,这里为name和ip。由于可以使用"%X{Key}"输出信息,因此MDC使用起来会比NDC更灵活。此外,MDC还提供了get方法来获取指定Key的信息。
六、小结
  用了近半个月,终于大概掌握了Log4J。由于本文是边学边写的,目的是将Log4J的用法记录下来,而非提供一份中文参考,因此内容并不细致,但尽量提供了示例。不过到最后才发现,示例存在问题,其实Logger做为类的static成员比较恰当,而我为了图方便,竟直接写到了main方法中,这一点还请注意。
  此外,这里再推荐一下《The Complete log4j Manual》,是对Log4J较详细的介绍,在网上可以找到,只不过是英文的。


posted @ 2006-09-01 13:25 Binary 阅读(417) | 评论 (0)编辑 收藏

Java 技术: 使您轻松地进行多线程应用程序编程

产者-消费者方案是多线程应用程序开发中最常用的构造之一 ― 因此困难也在于此。因为在一个应用程序中可以多次重复生产者-消费者行为,其代码也可以如此。软件开发人员 Ze'ev Bubis 和 Saffi Hartal 创建了 Consumer 类,该类通过在一些多线程应用程序中促进代码重用以及简化代码调试和维护来解决这个问题。请通过单击本文顶部或底部的 讨论来参与本文的 论坛,与作者和其他读者分享您的想法。

多线程应用程序通常利用生产者-消费者编程方案,其中由生产者线程创建重复性作业,将其传递给作业队列,然后由消费者线程处理作业。虽然这种编程方法很有用,但是它通常导致重复的代码,这对于调试和维护可能是真正的问题。

为了解决这个问题并促进代码重用,我们创建了 Consumer 类。 Consumer 类包含所有用于作业队列和消费者线程的代码,以及使这两者能够结合在一起的逻辑。这使我们可以专注于业务逻辑 ― 关于应该如何处理作业的细节 ― 而不是专注于编写大量冗余的代码。同时,它还使得调试多线程应用程序的任务变得更为容易。

在本文中,我们将简单观察一下多线程应用程序开发中公共线程用法,同时,解释一下生产者-消费者编程方案,并研究一个实际的示例来向您演示 Consumer 类是如何工作的。请注意,对于多线程应用程序开发或消费者-生产者方案,本文不作深入介绍;有关那些主题,请参阅 参考资料获取文章的清单。

多线程基础知识

多线程是一种使应用程序能同时处理多个操作的编程技术。通常有两种不同类型的多线程操作使用多个线程:

  • 适时事件,当作业必须在特定的时间或在特定的间隔内调度执行时
  • 后台处理,当后台事件必须与当前执行流并行处理或执行时

适时事件的示例包括程序提醒、超时事件以及诸如轮询和刷新之类的重复性操作。后台处理的示例包括等待发送的包或等待处理的已接收的消息。





回页首


生产者-消费者关系

生产者-消费者方案很适合于后台处理类别的情况。这些情况通常围绕一个作业“生产者”方和一个作业“消费者”方。当然,关于作业并行执行还有其它考虑事项。在大多数情况下,对于使用同一资源的作业,应以“先来先服务”的方式按顺序处理,这可以通过使用单线程的消费者轻松实现。通过使用这种方法,我们使用单个线程来访问单个资源,而不是用多个线程来访问单个资源。

要启用标准消费者,当作业到来时创建一个作业队列来存储所有作业。生产者线程通过将新对象添加到消费者队列来交付这个要处理的新对象。然后消费者线程从队列取出每个对象,并依次处理。当队列为空时,消费者进入休眠。当新的对象添加到空队列时,消费者会醒来并处理该对象。因为大多数应用程序喜欢顺序处理方式,所以消费者通常是单线程的。





回页首


问题:代码重复

因为生产者-消费者方案很常用,所以在构建应用程序时它可能会出现几次,这导致了代码重复。我们认识到,这显示了在应用程序开发过程期间多次使用了生产者-消费者方案的问题。

当第一次需要生产者-消费者行为时,通过编写一个采用一个线程和一个队列的类来实现该行为。当第二次需要这种行为时,我们着手从头开始实现它,但是接着认识到以前已经做过这件事了。我们复制了代码并修改了处理对象的方式。当第三次在该应用程序中实现生产者-消费者行为时,很明显我们复制了太多代码。我们决定,需要一个适用的 Consumer 类,它将处理我们所有的生产者-消费者方案。





回页首


我们的解决方案:Consumer 类

我们创建 Consumer 类的目的是:在我们的应用程序中,消除这种代码重复 ― 为每个生产者-消费者实例编写一个新作业队列和消费者线程来解决这个问题。有了适当的 Consumer 类,我们所必须做的只是编写专门用于作业处理(业务逻辑)的代码。这使得我们的代码更清晰、更易于维护以及更改起来更灵活。

我们对 Consumer 类有如下需求:

  • 重用:我们希望这个类包括所有东西。一个线程、一个队列以及使这两者结合在一起的所有逻辑。这将使我们只须编写队列中“消费”特定作业的代码。(因而,例如,程序员使用 Consumer 类时,将重载 onConsume(ObjectjobToBeConsumed) 方法。)
  • 队列选项:我们希望能够设置将由 Consumer 对象使用的队列实现。但是,这意味着我们必须确保队列是线程安全的或使用一个不会与消费操作冲突的单线程生产者。无论使用哪种方法,都必须将队列设计成允许不同的进程能访问其方法。
  • Consumer 线程优先级:我们希望能够设置 Consumer 线程运行的优先级。
  • Consumer 线程命名:线程拥有一个有意义的名称会比较方便,当然这的确有助于调试。例如,如果您向 Java 虚拟机发送了一个信号,它将生成一个完整的线程转储 ― 所有线程及其相应堆栈跟踪的快照。要在 Windows 平台上生成这个线程转储,您必须在 Java 程序运行的窗口中按下键序列 <ctrl><break> ,或者单击窗口上的“关闭”按钮。有关如何使用完整的线程转储来诊断 Java 软件问题的更多信息,请参阅 参考资料




回页首


类代码

getThread() 方法中,我们使用“惰性创建”来创建 Consumer 的线程,如清单 1 所示:


清单 1. 创建 Consumer 的线程
     /**
       * Lazy creation of the Consumer's thread.
       *
       * @return  the Consumer's thread
       */
      private Thread getThread()
      {
         if (_thread==null)
         {
            _thread = new Thread()
            {
               public void run()
               {
                  Consumer.this.run();
               }
            };
         }
         return _thread;

该线程的 run() 方法运行 Consumerrun() 方法,它是主消费者循环,如清单 2 所示:


清单 2. run() 方法是主 Consumer 循环
     /**
       *  Main Consumer's thread method.
       */
      private void run()
      {
         while (!_isTerminated)
         {
            // job handling loop
      while (true)
            {
               Object o;
               synchronized (_queue)
               {
                  if (_queue.isEmpty())
          break;
                  o = _queue.remove();
               }
               if (o == null)
          break;
               onConsume(o);
            }

            // if we are not terminated and the queue is still empty
            // then wait until new jobs arrive.

            synchronized(_waitForJobsMonitor)
            {
               if (_isTerminated)
          break;
               if(_queue.isEmpty())
               {
        try
                  {
                     _waitForJobsMonitor.wait();
                  }
                  catch (InterruptedException ex)
                  {
                  }
               }
            }
         }
}// run()

基本上, Consumer 的线程一直运行,直到队列中不再有等待的作业为止。然后它进入休眠,只在第一次调用 add(Object) 时醒来,该方法向队列添加一个新作业并“踢”醒该线程。

使用 wait()notify() 机制来完成“睡眠”和“踢”。实际的消费者工作由 OnConsume(Object) 方法处理,如清单 3 所示:


清单 3. 唤醒和通知 Consumer
     /**
      * Add an object to the Consumer.
      * This is the entry point for the producer.
      * After the item is added, the Consumer's thread
      * will be notified.
      *
      * @param  the object to be 'consumed' by this consumer
      */
      public void add(Object o)
      {
         _queue.add(o);
         kickThread();
      }

      /**
       * Wake up the thread (without adding new stuff to consume)
       *
       */
      public void kickThread()
      {
         if (!this._thread.isInterrupted())
         {
            synchronized(_waitForJobsMonitor)
            {
               _waitForJobsMonitor.notify();
            }
         }
      }





回页首


示例:MessagesProcessor

为了向您展示 Consumer 类是如何工作的,我们将使用一个简单示例。 MessagesProcessor 类以异步方式处理进入的消息(也就是说,不干扰调用线程)。其工作是在每个消息到来时打印它。 MessagesProcessor 具有一个处理到来的消息作业的内部 Consumer 。当新作业进入空队列时, Consumer 调用 processMessage(String) 方法来处理它,如清单 4 所示:


清单 4. MessagesProcessor 类
      class MessagesProcessor
      {
         String _name;
         // anonymous inner class that supplies the consumer
         // capabilities for the MessagesProcessor
         private Consumer _consumer = new Consumer()
         {
            // that method is called on each event retrieved
            protected void onConsume(Object o)
            {
               if (!(o instanceof String))
               {
                  System.out.println("illegal use, ignoring");
                  return;
               }
               MessagesProcesser.this.processMessage((String)o);
            }
         }.setName("MessagesProcessor").init();

         public void gotMessageEvent(String s)
         {
            _consumer.add(s);
         }
         private void processMessage(String s)
         {
            System.out.println(_name+" processed message: "+s);
         }

         private void terminate()
         {
           _consumer.terminateWait();
           _name = null;
         }

         MessagesProcessor()
         {
            _name = "Example Consumer";
         }
      }

正如您可以从上面的代码中所看到的,定制 Consumer 相当简单。我们使用了一个匿名内部类来继承 Consumer 类,并重载抽象方法 onConsume() 。因此,在我们的示例中,只需调用 processMessage





回页首


Consumer 类的高级特性

除了开始时提出的基本需求以外,我们还为 Consumer 类提供了一些我们觉得有用的高级特性。

事件通知

  • onThreadTerminate():只在终止 Consumer 前调用该方法。我们出于调试目的覆盖了这个方法。
  • goingToRest():只在 Consumer 线程进入休眠前调用该方法(也就是说,只在调用 _waitForJobsMonitor.wait() 之前调用)。只在需要消费者在进入休眠之前处理一批已处理工作的复杂情况中,可能需要这种通知。

终止

  • terminate():Consumer 线程的异步终止。
  • terminateWait():设置调用线程一直等待,直到消费者线程实际终止为止。

在我们的示例中,如果使用 terminate() 而不是 terminateWait() ,那么将会出现问题,因为在将 _name 设置成空值之后调用 onConsume() 方法。这将导致执行 processMessage 的线程抛出一个 NullPointerException





回页首


结束语:Consumer 类的好处

可在 参考资料一节下载 Consumer 类的源代码。请自由使用源代码,并按照您的需要扩展它。我们发现将这个类用于多线程应用程序开发有许多好处:

  • 代码重用/重复代码的消除:如果您有 Consumer 类,就不必为您应用程序中的每个实例编写一个新的消费者。如果在应用程序开发中频繁使用生产者-消费者方案,这可以很大程度地节省时间。另外,请牢记重复代码是滋生错误的沃土。它还使基本代码的维护更为困难。
  • 更少错误:使用验证过的代码是一种防止错误的好实践,尤其是处理多线程应用程序时。因为 Consumer 类已经被调试过,所以它更安全。消费者还通过在线程和资源之间担任安全中介来防止与线程相关的错误。消费者可以代表其它线程以顺序的方式访问资源。
  • 漂亮、清晰的代码:使用 Consumer 类有助于我们编写出更简单的代码,这样的代码更容易理解和维护。如果我们不使用 Consumer 类,就必须编写代码来处理两种不同的功能:消费逻辑(队列和线程管理、同步等)和指定消费者的用法或功能的代码。

posted @ 2006-08-24 17:53 Binary 阅读(237) | 评论 (0)编辑 收藏

Java 理论和实践: 理解 JTS ― 幕后魔术

虽然用 Java™ 语言编写的程序在理论上是不会出现“内存泄漏”的,但是有时对象在不再作为程序的逻辑状态的一部分之后仍然不被垃圾收集。本月,负责保障应用程序健康的工程师 Brian Goetz 探讨了无意识的对象保留的常见原因,并展示了如何用弱引用堵住泄漏。

要让垃圾收集(GC)回收程序不再使用的对象,对象的逻辑 生命周期(应用程序使用它的时间)和对该对象拥有的引用的实际 生命周期必须是相同的。在大多数时候,好的软件工程技术保证这是自动实现的,不用我们对对象生命周期问题花费过多心思。但是偶尔我们会创建一个引用,它在内存中包含对象的时间比我们预期的要长得多,这种情况称为无意识的对象保留(unintentional object retention)

全局 Map 造成的内存泄漏

无意识对象保留最常见的原因是使用 Map 将元数据与临时对象(transient object)相关联。假定一个对象具有中等生命周期,比分配它的那个方法调用的生命周期长,但是比应用程序的生命周期短,如客户机的套接字连接。需要将一些元数据与这个套接字关联,如生成连接的用户的标识。在创建 Socket 时是不知道这些信息的,并且不能将数据添加到 Socket 对象上,因为不能控制 Socket 类或者它的子类。这时,典型的方法就是在一个全局 Map 中存储这些信息,如清单 1 中的 SocketManager 类所示:


清单 1. 使用一个全局 Map 将元数据关联到一个对象
												
														public class SocketManager {
    private Map<Socket,User> m = new HashMap<Socket,User>();
    
    public void setUser(Socket s, User u) {
        m.put(s, u);
    }
    public User getUser(Socket s) {
        return m.get(s);
    }
    public void removeUser(Socket s) {
        m.remove(s);
    }
}

SocketManager socketManager;
...
socketManager.setUser(socket, user);

												
										

这种方法的问题是元数据的生命周期需要与套接字的生命周期挂钩,但是除非准确地知道什么时候程序不再需要这个套接字,并记住从 Map 中删除相应的映射,否则,SocketUser 对象将会永远留在 Map 中,远远超过响应了请求和关闭套接字的时间。这会阻止 SocketUser 对象被垃圾收集,即使应用程序不会再使用它们。这些对象留下来不受控制,很容易造成程序在长时间运行后内存爆满。除了最简单的情况,在几乎所有情况下找出什么时候 Socket 不再被程序使用是一件很烦人和容易出错的任务,需要人工对内存进行管理。





回页首


找出内存泄漏

程序有内存泄漏的第一个迹象通常是它抛出一个 OutOfMemoryError,或者因为频繁的垃圾收集而表现出糟糕的性能。幸运的是,垃圾收集可以提供能够用来诊断内存泄漏的大量信息。如果以 -verbose:gc 或者 -Xloggc 选项调用 JVM,那么每次 GC 运行时在控制台上或者日志文件中会打印出一个诊断信息,包括它所花费的时间、当前堆使用情况以及恢复了多少内存。记录 GC 使用情况并不具有干扰性,因此如果需要分析内存问题或者调优垃圾收集器,在生产环境中默认启用 GC 日志是值得的。

有工具可以利用 GC 日志输出并以图形方式将它显示出来,JTune 就是这样的一种工具(请参阅 参考资料)。观察 GC 之后堆大小的图,可以看到程序内存使用的趋势。对于大多数程序来说,可以将内存使用分为两部分:baseline 使用和 current load 使用。对于服务器应用程序,baseline 使用就是应用程序在没有任何负荷、但是已经准备好接受请求时的内存使用,current load 使用是在处理请求过程中使用的、但是在请求处理完成后会释放的内存。只要负荷大体上是恒定的,应用程序通常会很快达到一个稳定的内存使用水平。如果在应用程序已经完成了其初始化并且负荷没有增加的情况下,内存使用持续增加,那么程序就可能在处理前面的请求时保留了生成的对象。

清单 2 展示了一个有内存泄漏的程序。MapLeaker 在线程池中处理任务,并在一个 Map 中记录每一项任务的状态。不幸的是,在任务完成后它不会删除那一项,因此状态项和任务对象(以及它们的内部状态)会不断地积累。


清单 2. 具有基于 Map 的内存泄漏的程序
												
														public class MapLeaker {
    public ExecutorService exec = Executors.newFixedThreadPool(5);
    public Map<Task, TaskStatus> taskStatus 
        = Collections.synchronizedMap(new HashMap<Task, TaskStatus>());
    private Random random = new Random();

    private enum TaskStatus { NOT_STARTED, STARTED, FINISHED };

    private class Task implements Runnable {
        private int[] numbers = new int[random.nextInt(200)];

        public void run() {
            int[] temp = new int[random.nextInt(10000)];
            taskStatus.put(this, TaskStatus.STARTED);
            doSomeWork();
            taskStatus.put(this, TaskStatus.FINISHED);
        }
    }

    public Task newTask() {
        Task t = new Task();
        taskStatus.put(t, TaskStatus.NOT_STARTED);
        exec.execute(t);
        return t;
    }
}

												
										

图 1 显示 MapLeaker GC 之后应用程序堆大小随着时间的变化图。上升趋势是存在内存泄漏的警示信号。(在真实的应用程序中,坡度不会这么大,但是在收集了足够长时间的 GC 数据后,上升趋势通常会表现得很明显。)


图 1. 持续上升的内存使用趋势

确信有了内存泄漏后,下一步就是找出哪种对象造成了这个问题。所有内存分析器都可以生成按照对象类进行分解的堆快照。有一些很好的商业堆分析工具,但是找出内存泄漏不一定要花钱买这些工具 —— 内置的 hprof 工具也可完成这项工作。要使用 hprof 并让它跟踪内存使用,需要以 -Xrunhprof:heap=sites 选项调用 JVM。

清单 3 显示分解了应用程序内存使用的 hprof 输出的相关部分。(hprof 工具在应用程序退出时,或者用 kill -3 或在 Windows 中按 Ctrl+Break 时生成使用分解。)注意两次快照相比,Map.EntryTaskint[] 对象有了显著增加。

请参阅 清单 3

清单 4 展示了 hprof 输出的另一部分,给出了 Map.Entry 对象的分配点的调用堆栈信息。这个输出告诉我们哪些调用链生成了 Map.Entry 对象,并带有一些程序分析,找出内存泄漏来源一般来说是相当容易的。


清单 4. HPROF 输出,显示 Map.Entry 对象的分配点
												
														TRACE 300446:
	java.util.HashMap$Entry.<init>(<Unknown Source>:Unknown line)
	java.util.HashMap.addEntry(<Unknown Source>:Unknown line)
	java.util.HashMap.put(<Unknown Source>:Unknown line)
	java.util.Collections$SynchronizedMap.put(<Unknown Source>:Unknown line)
	com.quiotix.dummy.MapLeaker.newTask(MapLeaker.java:48)
	com.quiotix.dummy.MapLeaker.main(MapLeaker.java:64)

												
										





回页首


弱引用来救援了

SocketManager 的问题是 Socket-User 映射的生命周期应当与 Socket 的生命周期相匹配,但是语言没有提供任何容易的方法实施这项规则。这使得程序不得不使用人工内存管理的老技术。幸运的是,从 JDK 1.2 开始,垃圾收集器提供了一种声明这种对象生命周期依赖性的方法,这样垃圾收集器就可以帮助我们防止这种内存泄漏 —— 利用弱引用

弱引用是对一个对象(称为 referent)的引用的持有者。使用弱引用后,可以维持对 referent 的引用,而不会阻止它被垃圾收集。当垃圾收集器跟踪堆的时候,如果对一个对象的引用只有弱引用,那么这个 referent 就会成为垃圾收集的候选对象,就像没有任何剩余的引用一样,而且所有剩余的弱引用都被清除。(只有弱引用的对象称为弱可及(weakly reachable)。)

WeakReference 的 referent 是在构造时设置的,在没有被清除之前,可以用 get() 获取它的值。如果弱引用被清除了(不管是 referent 已经被垃圾收集了,还是有人调用了 WeakReference.clear()),get() 会返回 null。相应地,在使用其结果之前,应当总是检查 get() 是否返回一个非 null 值,因为 referent 最终总是会被垃圾收集的。

用一个普通的(强)引用拷贝一个对象引用时,限制 referent 的生命周期至少与被拷贝的引用的生命周期一样长。如果不小心,那么它可能就与程序的生命周期一样 —— 如果将一个对象放入一个全局集合中的话。另一方面,在创建对一个对象的弱引用时,完全没有扩展 referent 的生命周期,只是在对象仍然存活的时候,保持另一种到达它的方法。

弱引用对于构造弱集合最有用,如那些在应用程序的其余部分使用对象期间存储关于这些对象的元数据的集合 —— 这就是 SocketManager 类所要做的工作。因为这是弱引用最常见的用法,WeakHashMap 也被添加到 JDK 1.2 的类库中,它对键(而不是对值)使用弱引用。如果在一个普通 HashMap 中用一个对象作为键,那么这个对象在映射从 Map 中删除之前不能被回收,WeakHashMap 使您可以用一个对象作为 Map 键,同时不会阻止这个对象被垃圾收集。清单 5 给出了 WeakHashMapget() 方法的一种可能实现,它展示了弱引用的使用:


清单 5. WeakReference.get() 的一种可能实现
												
														public class WeakHashMap<K,V> implements Map<K,V> {

    private static class Entry<K,V> extends WeakReference<K> 
      implements Map.Entry<K,V> {
        private V value;
        private final int hash;
        private Entry<K,V> next;
        ...
    }

    public V get(Object key) {
        int hash = getHash(key);
        Entry<K,V> e = getChain(hash);
        while (e != null) {
            K eKey= e.get();
            if (e.hash == hash && (key == eKey || key.equals(eKey)))
                return e.value;
            e = e.next;
        }
        return null;
    }

												
										

调用 WeakReference.get() 时,它返回一个对 referent 的强引用(如果它仍然存活的话),因此不需要担心映射在 while 循环体中消失,因为强引用会防止它被垃圾收集。WeakHashMap 的实现展示了弱引用的一种常见用法 —— 一些内部对象扩展 WeakReference。其原因在下面一节讨论引用队列时会得到解释。

在向 WeakHashMap 中添加映射时,请记住映射可能会在以后“脱离”,因为键被垃圾收集了。在这种情况下,get() 返回 null,这使得测试 get() 的返回值是否为 null 变得比平时更重要了。

用 WeakHashMap 堵住泄漏

SocketManager 中防止泄漏很容易,只要用 WeakHashMap 代替 HashMap 就行了,如清单 6 所示。(如果 SocketManager 需要线程安全,那么可以用 Collections.synchronizedMap() 包装 WeakHashMap)。当映射的生命周期必须与键的生命周期联系在一起时,可以使用这种方法。不过,应当小心不滥用这种技术,大多数时候还是应当使用普通的 HashMap 作为 Map 的实现。


清单 6. 用 WeakHashMap 修复 SocketManager
												
														public class SocketManager {
    private Map<Socket,User> m = new WeakHashMap<Socket,User>();
    
    public void setUser(Socket s, User u) {
        m.put(s, u);
    }
    public User getUser(Socket s) {
        return m.get(s);
    }
}

												
										

引用队列

WeakHashMap 用弱引用承载映射键,这使得应用程序不再使用键对象时它们可以被垃圾收集,get() 实现可以根据 WeakReference.get() 是否返回 null 来区分死的映射和活的映射。但是这只是防止 Map 的内存消耗在应用程序的生命周期中不断增加所需要做的工作的一半,还需要做一些工作以便在键对象被收集后从 Map 中删除死项。否则,Map 会充满对应于死键的项。虽然这对于应用程序是不可见的,但是它仍然会造成应用程序耗尽内存,因为即使键被收集了,Map.Entry 和值对象也不会被收集。

可以通过周期性地扫描 Map,对每一个弱引用调用 get(),并在 get() 返回 null 时删除那个映射而消除死映射。但是如果 Map 有许多活的项,那么这种方法的效率很低。如果有一种方法可以在弱引用的 referent 被垃圾收集时发出通知就好了,这就是引用队列 的作用。

引用队列是垃圾收集器向应用程序返回关于对象生命周期的信息的主要方法。弱引用有两个构造函数:一个只取 referent 作为参数,另一个还取引用队列作为参数。如果用关联的引用队列创建弱引用,在 referent 成为 GC 候选对象时,这个引用对象(不是 referent)就在引用清除后加入 到引用队列中。之后,应用程序从引用队列提取引用并了解到它的 referent 已被收集,因此可以进行相应的清理活动,如去掉已不在弱集合中的对象的项。(引用队列提供了与 BlockingQueue 同样的出列模式 —— polled、timed blocking 和 untimed blocking。)

WeakHashMap 有一个名为 expungeStaleEntries() 的私有方法,大多数 Map 操作中会调用它,它去掉引用队列中所有失效的引用,并删除关联的映射。清单 7 展示了 expungeStaleEntries() 的一种可能实现。用于存储键-值映射的 Entry 类型扩展了 WeakReference,因此当 expungeStaleEntries() 要求下一个失效的弱引用时,它得到一个 Entry。用引用队列代替定期扫描内容的方法来清理 Map 更有效,因为清理过程不会触及活的项,只有在有实际加入队列的引用时它才工作。


清单 7. WeakHashMap.expungeStaleEntries() 的可能实现
												
														    private void expungeStaleEntries() {
	Entry<K,V> e;
        while ( (e = (Entry<K,V>) queue.poll()) != null) {
            int hash = e.hash;

            Entry<K,V> prev = getChain(hash);
            Entry<K,V> cur = prev;
            while (cur != null) {
                Entry<K,V> next = cur.next;
                if (cur == e) {
                    if (prev == e)
                        setChain(hash, next);
                    else
                        prev.next = next;
                    break;
                }
                prev = cur;
                cur = next;
            }
        }
    }

												
										





回页首


结束语

弱引用和弱集合是对堆进行管理的强大工具,使得应用程序可以使用更复杂的可及性方案,而不只是由普通(强)引用所提供的“要么全部要么没有”可及性。下个月,我们将分析与弱引用有关的软引用,将分析在使用弱引用和软引用时,垃圾收集器的行为。

posted @ 2006-08-24 17:51 Binary 阅读(199) | 评论 (0)编辑 收藏

Java 理论与实践: 您的小数点到哪里去了?

许多程序员在其整个开发生涯中都不曾使用定点或浮点数,可能的例外是,偶尔在计时测试或基准测试程序中会用到。Java语言和类库支持两类非整数类型 ― IEEE 754 浮点( floatdouble ,包装类(wrapper class)为 FloatDouble ),以及任意精度的小数( java.math.BigDecimal )。在本月的 Java 理论和实践中,Brian Goetz 探讨了在 Java 程序中使用非整数类型时一些常碰到的陷阱和“gotcha”。请在本文的 论坛上提出您对本文的想法,以飨笔者和其他读者。(您也可以单击本文顶部或底部的讨论来访问论坛)。

虽然几乎每种处理器和编程语言都支持浮点运算,但大多数程序员很少注意它。这容易理解 ― 我们中大多数很少需要使用非整数类型。除了科学计算和偶尔的计时测试或基准测试程序,其它情况下几乎都用不着它。同样,大多数开发人员也容易忽略 java.math.BigDecimal 所提供的任意精度的小数 ― 大多数应用程序不使用它们。然而,在以整数为主的程序中有时确实会出人意料地需要表示非整型数据。例如,JDBC 使用 BigDecimal 作为 SQL DECIMAL 列的首选互换格式。

IEEE 浮点

Java 语言支持两种基本的浮点类型: floatdouble ,以及与它们对应的包装类 FloatDouble 。它们都依据 IEEE 754 标准,该标准为 32 位浮点和 64 位双精度浮点二进制小数定义了二进制标准。

IEEE 754 用科学记数法以底数为 2 的小数来表示浮点数。IEEE 浮点数用 1 位表示数字的符号,用 8 位来表示指数,用 23 位来表示尾数,即小数部分。作为有符号整数的指数可以有正负之分。小数部分用二进制(底数 2)小数来表示,这意味着最高位对应着值 ?(2 -1),第二位对应着 ?(2 -2),依此类推。对于双精度浮点数,用 11 位表示指数,52 位表示尾数。IEEE 浮点值的格式如图 1 所示。


图 1. IEEE 754 浮点数的格式
图 1. IEEE 754 浮点数的格式

因为用科学记数法可以有多种方式来表示给定数字,所以要规范化浮点数,以便用底数为 2 并且小数点左边为 1 的小数来表示,按照需要调节指数就可以得到所需的数字。所以,例如,数 1.25 可以表示为尾数为 1.01,指数为 0: (-1) 0*1.01 2*2 0

数 10.0 可以表示为尾数为 1.01,指数为 3: (-1) 0*1.01 2*2 3

特殊数字

除了编码所允许的值的标准范围(对于 float ,从 1.4e-45 到 3.4028235e+38),还有一些表示无穷大、负无穷大、 -0 和 NaN(它代表“不是一个数字”)的特殊值。这些值的存在是为了在出现错误条件(譬如算术溢出,给负数开平方根,除以 0 等)下,可以用浮点值集合中的数字来表示所产生的结果。

这些特殊的数字有一些不寻常的特征。例如, 0-0 是不同值,但在比较它们是否相等时,被认为是相等的。用一个非零数去除以无穷大的数,结果等于 0 。特殊数字 NaN 是无序的;使用 ==<> 运算符将 NaN 与其它浮点值比较时,结果为 false 。如果 f 为 NaN,则即使 (f == f) 也会得到 false 。如果想将浮点值与 NaN 进行比较,则使用 Float.isNaN() 方法。表 1 显示了无穷大和 NaN 的一些属性。

表 1. 特殊浮点值的属性

表达式 结果
Math.sqrt(-1.0) -> NaN
0.0 / 0.0 -> NaN
1.0 / 0.0 -> 无穷大
-1.0 / 0.0 -> 负无穷大
NaN + 1.0 -> NaN
无穷大 + 1.0 -> 无穷大
无穷大 + 无穷大 -> 无穷大
NaN > 1.0 -> false
NaN == 1.0 -> false
NaN < 1.0 -> false
NaN == NaN -> false
0.0 == -0.01 -> true

基本浮点类型和包装类浮点有不同的比较行为

使事情更糟的是,在基本 float 类型和包装类 Float 之间,用于比较 NaN 和 -0 的规则是不同的。对于 float 值,比较两个 NaN 值是否相等将会得到 false ,而使用 Float.equals() 来比较两个 NaN Float 对象会得到 true 。造成这种现象的原因是,如果不这样的话,就不可能将 NaN Float 对象用作 HashMap 中的键。类似的,虽然 0-0 在表示为浮点值时,被认为是相等的,但使用 Float.compareTo() 来比较作为 Float 对象的 0-0 时,会显示 -0 小于 0





回页首


浮点中的危险

由于无穷大、NaN 和 0 的特殊行为,当应用浮点数时,可能看似无害的转换和优化实际上是不正确的。例如,虽然好象 0.0-f 很明显等于 -f ,但当 f0 时,这是不正确的。还有其它类似的 gotcha,表 2 显示了其中一些 gotcha。

表 2. 无效的浮点假定

这个表达式…… 不一定等于…… 当……
0.0 - f -f f 为 0
f < g ! (f >= g) f 或 g 为 NaN
f == f true f 为 NaN
f + g - g f g 为无穷大或 NaN

舍入误差

浮点运算很少是精确的。虽然一些数字(譬如 0.5 )可以精确地表示为二进制(底数 2)小数(因为 0.5 等于 2 -1),但其它一些数字(譬如 0.1 )就不能精确的表示。因此,浮点运算可能导致舍入误差,产生的结果接近 ― 但不等于 ― 您可能希望的结果。例如,下面这个简单的计算将得到 2.600000000000001 ,而不是 2.6


												
														 double s=0;
  for (int i=0; i<26; i++)
    s += 0.1;
  System.out.println(s);

												
										


类似的, .1*26 相乘所产生的结果不等于 .1 自身加 26 次所得到的结果。当将浮点数强制转换成整数时,产生的舍入误差甚至更严重,因为强制转换成整数类型会舍弃非整数部分,甚至对于那些“看上去似乎”应该得到整数值的计算,也存在此类问题。例如,下面这些语句:


												
														 double d = 29.0 * 0.01;
  System.out.println(d);
  System.out.println((int) (d * 100));

												
										


将得到以下输出:


												
														 0.29
  28

												
										

这可能不是您起初所期望的。





回页首


浮点数比较指南

由于存在 NaN 的不寻常比较行为和在几乎所有浮点计算中都不可避免地会出现舍入误差,解释浮点值的比较运算符的结果比较麻烦。

最好完全避免使用浮点数比较。当然,这并不总是可能的,但您应该意识到要限制浮点数比较。如果必须比较浮点数来看它们是否相等,则应该将它们差的绝对值同一些预先选定的小正数进行比较,这样您所做的就是测试它们是否“足够接近”。(如果不知道基本的计算范围,可以使用测试“abs(a/b - 1) < epsilon”,这种方法比简单地比较两者之差要更准确)。甚至测试看一个值是比零大还是比零小也存在危险 ―“以为”会生成比零略大值的计算事实上可能由于积累的舍入误差会生成略微比零小的数字。

NaN 的无序性质使得在比较浮点数时更容易发生错误。当比较浮点数时,围绕无穷大和 NaN 问题,一种避免 gotcha 的经验法则是显式地测试值的有效性,而不是试图排除无效值。在清单 1 中,有两个可能的用于特性的 setter 的实现,该特性只能接受非负数值。第一个实现会接受 NaN,第二个不会。第二种形式比较好,因为它显式地检测了您认为有效的值的范围。


清单 1. 需要非负浮点值的较好办法和较差办法
												
														   // Trying to test by exclusion -- this doesn't catch NaN or infinity
    public void setFoo(float foo) {
      if (foo < 0)
          throw new IllegalArgumentException(Float.toString(f));
        this.foo = foo;
    }
    // Testing by inclusion -- this does catch NaN
    public void setFoo(float foo) {
      if (foo >= 0 && foo < Float.INFINITY)
        this.foo = foo;
  else
        throw new IllegalArgumentException(Float.toString(f));
    }

												
										

不要用浮点值表示精确值

一些非整数值(如几美元和几美分这样的小数)需要很精确。浮点数不是精确值,所以使用它们会导致舍入误差。因此,使用浮点数来试图表示象货币量这样的精确数量不是一个好的想法。使用浮点数来进行美元和美分计算会得到灾难性的后果。浮点数最好用来表示象测量值这类数值,这类值从一开始就不怎么精确。





回页首


用于较小数的 BigDecimal

从 JDK 1.3 起,Java 开发人员就有了另一种数值表示法来表示非整数: BigDecimalBigDecimal 是标准的类,在编译器中不需要特殊支持,它可以表示任意精度的小数,并对它们进行计算。在内部,可以用任意精度任何范围的值和一个换算因子来表示 BigDecimal ,换算因子表示左移小数点多少位,从而得到所期望范围内的值。因此,用 BigDecimal 表示的数的形式为 unscaledValue*10 -scale

用于加、减、乘和除的方法给 BigDecimal 值提供了算术运算。由于 BigDecimal 对象是不可变的,这些方法中的每一个都会产生新的 BigDecimal 对象。因此,因为创建对象的开销, BigDecimal 不适合于大量的数学计算,但设计它的目的是用来精确地表示小数。如果您正在寻找一种能精确表示如货币量这样的数值,则 BigDecimal 可以很好地胜任该任务。

所有的 equals 方法都不能真正测试相等

如浮点类型一样, BigDecimal 也有一些令人奇怪的行为。尤其在使用 equals() 方法来检测数值之间是否相等时要小心。 equals() 方法认为,两个表示同一个数但换算值不同(例如, 100.00100.000 )的 BigDecimal 值是不相等的。然而, compareTo() 方法会认为这两个数是相等的,所以在从数值上比较两个 BigDecimal 值时,应该使用 compareTo() 而不是 equals()

另外还有一些情形,任意精度的小数运算仍不能表示精确结果。例如, 1 除以 9 会产生无限循环的小数 .111111... 。出于这个原因,在进行除法运算时, BigDecimal 可以让您显式地控制舍入。 movePointLeft() 方法支持 10 的幂次方的精确除法。

使用 BigDecimal 作为互换类型

SQL-92 包括 DECIMAL 数据类型,它是用于表示定点小数的精确数字类型,它可以对小数进行基本的算术运算。一些 SQL 语言喜欢称此类型为 NUMERIC 类型,其它一些 SQL 语言则引入了 MONEY 数据类型,MONEY 数据类型被定义为小数点右侧带有两位的小数。

如果希望将数字存储到数据库中的 DECIMAL 字段,或从 DECIMAL 字段检索值,则如何确保精确地转换该数字?您可能不希望使用由 JDBC PreparedStatementResultSet 类所提供的 setFloat()getFloat() 方法,因为浮点数与小数之间的转换可能会丧失精确性。相反,请使用 PreparedStatementResultSetsetBigDecimal()getBigDecimal() 方法。

对于 BigDecimal ,有几个可用的构造函数。其中一个构造函数以双精度浮点数作为输入,另一个以整数和换算因子作为输入,还有一个以小数的 String 表示作为输入。要小心使用 BigDecimal(double) 构造函数,因为如果不了解它,会在计算过程中产生舍入误差。请使用基于整数或 String 的构造函数。

构造 BigDecimal 数

对于 BigDecimal ,有几个可用的构造函数。其中一个构造函数以双精度浮点数作为输入,另一个以整数和换算因子作为输入,还有一个以小数的 String 表示作为输入。要小心使用 BigDecimal(double) 构造函数,因为如果不了解它,会在计算过程中产生舍入误差。请使用基于整数或 String 的构造函数。

如果使用 BigDecimal(double) 构造函数不恰当,在传递给 JDBC setBigDecimal() 方法时,会造成似乎很奇怪的 JDBC 驱动程序中的异常。例如,考虑以下 JDBC 代码,该代码希望将数字 0.01 存储到小数字段:

												
														 PreparedStatement ps =
    connection.prepareStatement("INSERT INTO Foo SET name=?, value=?");
  ps.setString(1, "penny");
  ps.setBigDecimal(2, new BigDecimal(0.01));
  ps.executeUpdate();

												
										

在执行这段似乎无害的代码时会抛出一些令人迷惑不解的异常(这取决于具体的 JDBC 驱动程序),因为 0.01 的双精度近似值会导致大的换算值,这可能会使 JDBC 驱动程序或数据库感到迷惑。JDBC 驱动程序会产生异常,但可能不会说明代码实际上错在哪里,除非意识到二进制浮点数的局限性。相反,使用 BigDecimal("0.01")BigDecimal(1, 2) 构造 BigDecimal 来避免这类问题,因为这两种方法都可以精确地表示小数。





回页首


结束语

在 Java 程序中使用浮点数和小数充满着陷阱。浮点数和小数不象整数一样“循规蹈矩”,不能假定浮点计算一定产生整型或精确的结果,虽然它们的确“应该”那样做。最好将浮点运算保留用作计算本来就不精确的数值,譬如测量。如果需要表示定点数(譬如,几美元和几美分),则使用 BigDecimal

posted @ 2006-08-24 17:51 Binary 阅读(237) | 评论 (0)编辑 收藏

Java 理论与实践: 变还是不变?

不变对象具有许多能更方便地使用它们的特性,包括不严格的同步需求和不必考虑数据讹误就能自由地共享和高速缓存对象引用。尽管不变性可能未必对于所有类都有意义,但大多数程序中至少有一些类将受益于不可变。在本月的 Java 理论与实践中,Brian Goetz 说明了不变性的一些长处和构造不变类的一些准则。请在附带的 论坛中与作者和其他读者分享您关于本文的心得。(也可以单击文章顶部或底部的“讨论”来访问论坛。)

不变对象是指在实例化后其外部可见状态无法更改的对象。Java 类库中的 StringIntegerBigDecimal 类就是不变对象的示例 ― 它们表示在对象的生命期内无法更改的单个值。

不变性的长处

如果正确使用不变类,它们会极大地简化编程。因为它们只能处于一种状态,所以只要正确构造了它们,就决不会陷入不一致的状态。您不必复制或克隆不变对象,就能自由地共享和高速缓存对它们的引用;您可以高速缓存它们的字段或其方法的结果,而不用担心值会不会变成失效的或与对象的其它状态不一致。不变类通常产生最好的映射键。而且,它们本来就是线程安全的,所以不必在线程间同步对它们的访问。

自由高速缓存

因为不变对象的值没有更改的危险,所以可以自由地高速缓存对它们的引用,而且可以肯定以后的引用仍将引用同一个值。同样地,因为它们的特性无法更改,所以您可以高速缓存它们的字段和其方法的结果。

如果对象是可变的,就必须在存储对其的引用时引起注意。请考虑清单 1 中的代码,其中排列了两个由调度程序执行的任务。目的是:现在启动第一个任务,而在某一天启动第二个任务。


清单 1. 可变的 Date 对象的潜在问题
												
														 Date d = new Date();
  Scheduler.scheduleTask(task1, d);
  d.setTime(d.getTime() + ONE_DAY);
  scheduler.scheduleTask(task2, d);

												
										

因为 Date 是可变的,所以 scheduleTask 方法必须小心地用防范措施将日期参数复制(可能通过 clone() )到它的内部数据结构中。不然, task1task2 可能都在明天执行,这可不是所期望的。更糟的是,任务调度程序所用的内部数据结构会变成讹误。在编写象 scheduleTask() 这样的方法时,极其容易忘记用防范措施复制日期参数。如果忘记这样做,您就制造了一个难以捕捉的错误,这个错误不会马上显现出来,而且当它暴露时人们要花较长的时间才会捕捉到。不变的 Date 类不可能发生这类错误。

固有的线程安全

大多数的线程安全问题发生在当多个线程正在试图并发地修改一个对象的状态(写-写冲突)时,或当一个线程正试图访问一个对象的状态,而另一个线程正在修改它(读-写冲突)时。要防止这样的冲突,必须同步对共享对象的访问,以便在对象处于不一致状态时其它线程不能访问它们。正确地做到这一点会很难,需要大量文档来确保正确地扩展程序,还可能对性能产生不利后果。只要正确构造了不变对象(这意味着不让对象引用从构造函数中转义),就使它们免除了同步访问的要求,因为无法更改它们的状态,从而就不可能存在写-写冲突或读-写冲突。

不用同步就能自由地在线程间共享对不变对象的引用,可以极大地简化编写并发程序的过程,并减少程序可能存在的潜在并发错误的数量。

在恶意运行的代码面前是安全的

把对象当作参数的方法不应变更那些对象的状态,除非文档明确说明可以这样做,或者实际上这些方法具有该对象的所有权。当我们将一个对象传递给普通方法时,通常不希望对象返回时已被更改。但是,使用可变对象时,完全会是这样的。如果将 java.awt.Point 传递给诸如 Component.setLocation() 的方法,根本不会阻止 setLocation 修改我们传入的 Point 的位置,也不会阻止 setLocation 存储对该点的引用并稍后在另一个方法中更改它。(当然, Component 不这样做,因为它不鲁莽,但是并不是所有类都那么客气。)现在, Point 的状态已在我们不知道的情况下更改了,其结果具有潜在危险 ― 当点实际上在另一个位置时,我们仍认为它在原来的位置。然而,如果 Point 是不变的,那么这种恶意的代码就不能以如此令人混乱而危险的方法修改我们的程序状态了。

良好的键

不变对象产生最好的 HashMapHashSet 键。有些可变对象根据其状态会更改它们的 hashCode() 值(如清单 2 中的 StringHolder 示例类)。如果使用这种可变对象作为 HashSet 键,然后对象更改了其状态,那么就会对 HashSet 实现引起混乱 ― 如果枚举集合,该对象仍将出现,但如果用 contains() 查询集合,它就可能不出现。无需多说,这会引起某些混乱的行为。说明这一情况的清单 2 中的代码将打印“false”、“1”和“moo”。


清单 2. 可变 StringHolder 类,不适合用作键
												
														   public class StringHolder {
        private String string;
        public StringHolder(String s) {
            this.string = s;
        }
        public String getString() {
            return string;
        }
        public void setString(String string) {
            this.string = string;
        }
        public boolean equals(Object o) {
            if (this == o)
                return true;
            else if (o == null || !(o instanceof StringHolder))
                return false;
            else {
                final StringHolder other = (StringHolder) o;
                if (string == null)
                    return (other.string == null);
                else
                    return string.equals(other.string);
            }
        }
        public int hashCode() {
            return (string != null ? string.hashCode() : 0);
        }
        public String toString() {
            return string;
        }
        ...
        StringHolder sh = new StringHolder("blert");
        HashSet h = new HashSet();
        h.add(sh);
        sh.setString("moo");
        System.out.println(h.contains(sh));
        System.out.println(h.size());
        System.out.println(h.iterator().next());
    }

												
										





回页首


何时使用不变类

不变类最适合表示抽象数据类型(如数字、枚举类型或颜色)的值。Java 类库中的基本数字类(如 IntegerLongFloat )都是不变的,其它标准数字类型(如 BigIntegerBigDecimal )也是不变的。表示复数或精度任意的有理数的类将比较适合于不变性。甚至包含许多离散值的抽象类型(如向量或矩阵)也很适合实现为不变类,这取决于您的应用程序。

Flyweight 模式

不变性启用了 Flyweight 模式,该模式利用共享使得用对象有效地表示大量细颗粒度的对象变得容易。例如,您可能希望用一个对象来表示字处理文档中的每个字符或图像中的每个像素,但这一策略的幼稚实现将会对内存使用和内存管理开销产生高得惊人的花费。Flyweight 模式采用工厂方法来分配对不变的细颗粒度对象的引用,并通过仅使一个对象实例与字母“a”对应来利用共享缩减对象数。有关 Flyweight 模式的更多信息,请参阅经典书籍 Design Patterns(Gamma 等著;请参阅 参考资料)。

Java 类库中不变性的另一个不错的示例是 java.awt.Color 。在某些颜色表示法(如 RGB、HSB 或 CMYK)中,颜色通常表示为一组有序的数字值,但把一种颜色当作颜色空间中的一个特异值,而不是一组有序的独立可寻址的值更有意义,因此将 Color 作为不变类实现是有道理的。

如果要表示的对象是多个基本值的容器(如:点、向量、矩阵或 RGB 颜色),是用可变对象还是用不变对象表示?答案是……要看情况而定。要如何使用它们?它们主要用来表示多维值(如像素的颜色),还是仅仅用作其它对象的一组相关特性集合(如窗口的高度和宽度)的容器?这些特性多久更改一次?如果更改它们,那么各个组件值在应用程序中是否有其自己的含义呢?

事件是另一个适合用不变类实现的好示例。事件的生命期较短,而且常常会在创建它们的线程以外的线程中消耗,所以使它们成为不变的是利大于弊。大多数 AWT 事件类都没有作为严格的不变类来实现,而是可以有小小的修改。同样地,在使用一定形式的消息传递以在组件间通信的系统中,使消息对象成为不变的或许是明智的。





回页首


编写不变类的准则

编写不变类很容易。如果以下几点都为真,那么类就是不变的:

  • 它的所有字段都是 final
  • 该类声明为 final
  • 不允许 this 引用在构造期间转义
  • 任何包含对可变对象(如数组、集合或类似 Date 的可变类)引用的字段:
    • 是私有的
    • 从不被返回,也不以其它方式公开给调用程序
    • 是对它们所引用对象的唯一引用
    • 构造后不会更改被引用对象的状态

最后一组要求似乎挺复杂的,但其基本上意味着如果要存储对数组或其它可变对象的引用,就必须确保您的类对该可变对象拥有独占访问权(因为不然的话,其它类能够更改其状态),而且在构造后您不修改其状态。为允许不变对象存储对数组的引用,这种复杂性是必要的,因为 Java 语言没有办法强制不对 final 数组的元素进行修改。注:如果从传递给构造函数的参数中初始化数组引用或其它可变字段,您必须用防范措施将调用程序提供的参数或您无法确保具有独占访问权的其它信息复制到数组。否则,调用程序会在调用构造函数之后,修改数组的状态。清单 3 显示了编写一个存储调用程序提供的数组的不变对象的构造函数的正确方法(和错误方法)。


清单 3. 对不变对象编码的正确和错误方法
												
														class ImmutableArrayHolder {
  private final int[] theArray;
  // Right way to write a constructor -- copy the array
  public ImmutableArrayHolder(int[] anArray) {
    this.theArray = (int[]) anArray.clone();
  }
  // Wrong way to write a constructor -- copy the reference
  // The caller could change the array after the call to the constructor
  public ImmutableArrayHolder(int[] anArray) {
    this.theArray = anArray;
  }
  // Right way to write an accessor -- don't expose the array reference
  public int getArrayLength() { return theArray.length }
  public int getArray(int n)  { return theArray[n]; }
  // Right way to write an accessor -- use clone()
  public int[] getArray()       { return (int[]) theArray.clone(); }
  // Wrong way to write an accessor -- expose the array reference
  // A caller could get the array reference and then change the contents
  public int[] getArray()       { return theArray }
}

												
										

通过一些其它工作,可以编写使用一些非 final 字段的不变类(例如, String 的标准实现使用 hashCode 值的惰性计算),这样可能比严格的 final 类执行得更好。如果类表示抽象类型(如数字类型或颜色)的值,那么您还会想实现 hashCode()equals() 方法,这样对象将作为 HashMapHashSet 中的一个键工作良好。要保持线程安全,不允许 this 引用从构造函数中转义是很重要的。





回页首


偶尔更改的数据

有些数据项在程序生命期中一直保持常量,而有些会频繁更改。常量数据显然符合不变性,而状态复杂且频繁更改的对象通常不适合用不变类来实现。那么有时会更改,但更改又不太频繁的数据呢?有什么方法能让 有时更改的数据获得不变性的便利和线程安全的长处呢?

util.concurrent 包中的 CopyOnWriteArrayList 类是如何既利用不变性的能力,又仍允许偶尔修改的一个良好示例。它最适合于支持事件监听程序的类(如用户界面组件)使用。虽然事件监听程序的列表可以更改,但通常它更改的频繁性要比事件的生成少得多。

除了在修改列表时, CopyOnWriteArrayList 并不变更基本数组,而是创建新数组且废弃旧数组之外,它的行为与 ArrayList 类非常相似。这意味着当调用程序获得迭代器(迭代器在内部保存对基本数组的引用)时,迭代器引用的数组实际上是不变的,从而可以无需同步或冒并发修改的风险进行遍历。这消除了在遍历前克隆列表或在遍历期间对列表进行同步的需要,这两个操作都很麻烦、易于出错,而且完全使性能恶化。如果遍历比插入或除去更加频繁(这在某些情况下是常有的事), CopyOnWriteArrayList 会提供更佳的性能和更方便的访问。





回页首


结束语

使用不变对象比使用可变对象要容易得多。它们只能处于一种状态,所以始终是一致的,它们本来就是线程安全的,可以被自由地共享。使用不变对象可以彻底消除许多容易发生但难以检测的编程错误,如无法在线程间同步访问或在存储对数组或对象的引用前无法克隆该数组或对象。在编写类时,问问自己这个类是否可以作为不变类有效地实现,总是值得的。您可能会对回答常常是肯定的而感到吃惊。

posted @ 2006-08-24 17:50 Binary 阅读(195) | 评论 (0)编辑 收藏

JDBC 查询日志变得简单

JDBC java.sql.PreparedStatement接口的简单扩展可以使查询记录更少犯错,同时整理您的代码。在本文中,IBM电子商务顾问Jens Wyke向您介绍如何应用基本的封装技术(“通过封装来实现扩展”也称为Decorator设计模式)来获得最满意的结果。

在大多数情况下,JDBC PreparedStatements 使执行数据库查询更简便并可以显著提升您整体应用程序的性能。当谈到日志查询语句时 PreparedStatement 接口就显得有些不足了。 PreparedStatement 的优势在于其可变性,但是一个好的日志条目必须正确描述如何将SQL发送到数据库,它将密切关注用实际的参数值来替换所有参数占位符。虽然有多种方法可以解决这一难题,但没有任何一种易于大规模实施并且大部分将扰乱您的程序代码。

在本文中,您将了解到如何扩展JDBC PreparedStatement 接口来进行查询日志。 LoggableStatement 类实现 PreparedStatement 接口,但添加用于获得查询字符串的方法,使用一种适用于记录的格式。使用 LoggableStatement 类可以减少日志代码中发生错误的几率,生成简单且易于管理的代码。

注意:本文假设您有丰富的JDBC和 PreparedStatement 类经验。

典型日志解决方案

表1介绍了数据库查询时通常是如何使用 PreparedStatement (虽然忽略了初始化和错误处理)。在本文中,我们将使用SQL query SELECT 做为例子,但讨论使用其它类型的SQL语句,如 DELETEUPDATEINSERT


表1:一个典型的SQL数据库查询
												
														String sql = "select foo, bar from foobar where foo < ? and bar = ?";
    String fooValue = new Long(99);
    String barValue = "christmas";

    Connection conn = dataSource.getConnection();
    PreparedStatement pstmt = conn.prepareStatement(sql);

    pstmt.setLong(1,fooValue);
    pstmt.setString(2,barValue);

    ResultSet rs = pstmt.executeQuery();

    // parse result...


												
										

表1中一个好的查询日志条目看起来应与下面有几分类似:

												
														Executing query: select foo,bar from foobar where foo < 99 and 
bar='christmas'

												
										

下面是查询的日志代码的一个例子。注意:表1中的问号已经被每个参数的值替换。

												
														System.out.println("Executing query: select foo, bar from foobar where foo
< "+fooValue+" and bar = '+barValue+"'")

												
										

一种更好的方法是创建方法,我们称之为 replaceFirstQuestionMark ,它读取查询字符串并用参数值替换问号,如表2所示。这类方法的使用无需创建复制的字符串来描述SQL语句。


表 2:使用replaceFirstQuestionMark来进行字符串替换
												
														      // listing 1 goes here

     sql = replaceFirstQuestionMark(sql, fooValue);
     sql = replaceFirstQuestionMark(sql, barValue);
     System.out.println("Executing query: "+sql);

												
										

虽然这些解决方案都易于实施,但没有一种是完美的。问题是在更改SQL模板的同时也必须更改日志代码。您将在某一点上犯错几乎是不可避免的。查询将更改但您忘记了更新日志代码,您将结束与将发送到数据库的查询不匹配的日志条目 -- 调试恶梦。

我们真正需要的是一种使我们能够一次性使用每个参数变量(在我们的实例中为 fooValuebarValue )的设计方案。我们希望有一种方法,它使我们能够获得查询字符串,并用实际的参数值替换参数占位符。由于 java.sql.PreparedStatement 没有此类方法,我们必须自己实现。





回页首


定制解决方案

我们的 PreparedStatement 定制实施将做为围绕JDBC驱动器提供的“真实语句(real statement)”的封装器(Wrapper)。封装器语句将转发所有方法调用(例如 setLong(int, long)setString(int,String) ) 到“真实语句”。在这样做之前它将保存相关的参数值,从而它们可以用于生成日志输出结果。

表3介绍了 LoggableStatement 类如何实现 java.sql.PreparedStatement ,以及它如何使用JDBC连接和SQL模板作为输入来构建。


表3:LoggableStatement实现java.sql.PreparedStatement
												
														  public class LoggableStatement implements java.sql.PreparedStatement {

     // used for storing parameter values needed
      // for producing log
     private ArrayList parameterValues;     
          
     // the query string with question marks as  
     // parameter placeholders
     private String sqlTemplate;       
               
     // a statement created from a real database     
     // connection                                       
     private PreparedStatement wrappedStatement; 
                                                 

    public LoggableStatement(Connection connection, String sql) 
      throws SQLException {
      // use connection to make a prepared statement
      wrappedStatement = connection.prepareStatement(sql);
      sqlTemplate = sql;
      parameterValues = new ArrayList();
    }
     }

												
										





回页首


LoggableStatement如何工作

表4介绍了 LoggableStatement 如何向 saveQueryParamValue() 方法添加一个调用,以及在方法 setLongsetString 的“真实语句”上调用相应的方法。我们采用与用于参数设置的所有方法(例如 setCharsetLongsetRefsetObj )相同的方式来增加 saveQueryParamValue() 调用。表4还显示了在不调用 saveQueryParamValue() 的情况下如何封装方法 executeQuery ,因为它不是一个“参数设置”方法。


表4:LoggableStatement 方法
												
														     public void setLong(int parameterIndex, long x) 
         throws java.sql.SQLException {
      wrappedStatement.setLong(parameterIndex, x);
      saveQueryParamValue(parameterIndex, new Long(x));
   }

   public void setString(int parameterIndex, String x) 
       throws java.sql.SQLException {
      wrappedStatement.setString(parameterIndex, x);
      saveQueryParamValue(parameterIndex, x);
   }

  public ResultSet executeQuery() throws java.sql.SQLException {
     return wrappedStatement.executeQuery();
   }

												
										

表5中显示了 saveQueryParamValue() 方法。它把每个参数值转换成 String 表示,保存以便 getQueryString 方法日后使用。缺省情况下,一个对象使用其 toString 方法将被转换成 String ,但如果对象是 StringDate ,它将用单引号('')表示。 getQueryString() 方法使您能够从日志复制大多数查询并进行粘贴,无需修改交互式SQL处理器就可进行测试和调试。您可以根据需要修订该方法来转换其它类的参数值。


表5:saveQueryParamValue()方法
												
														  private void saveQueryParamValue(int position, Object obj) {
      String strValue;
      if (obj instanceof String || obj instanceof Date) {
           // if we have a String, include '' in the saved value
           strValue = "'" + obj + "'";
      } else {
           if (obj == null) {
                // convert null to the string null
                 strValue = "null";
           } else {
                // unknown object (includes all Numbers), just call toString
                strValue = obj.toString();
           }
      }
      // if we are setting a position larger than current size of 
      // parameterValues, first make it larger
      while (position >= parameterValues.size()) {
           parameterValues.add(null);
      }
      // save the parameter
      parameterValues.set(position, strValue);
 }

												
										

当我们使用标准方法来设置所有参数时,我们在 LoggableStatement 中简单调用 getQueryString() 方法来获得查询字符串。所有问号都将被真正的参数值替换,它准备输出到我们选定的日志目的地。





回页首


使用LoggableStatement

表6显示如何更改表1和表2中的代码来使用 LoggableStatement 。将 LoggableStatement 引入到我们的应用程序代码中可以解决复制的参数变量问题。如果改变了SQL模板,我们只需更新 PreparedStatement 上的参数设置调用(例如添加一个 pstmt.setString(3,"new-param-value") )。这一更改将在日志输出结果中反映出,无需任何记录代码的手工更新。


表6:使用LoggableStatement&#160
												
														    String sql = "select foo, bar from foobar where foo < ? and bar = ?";
    long fooValue = 99;
    String barValue = "christmas";

    Connection conn = dataSource.getConnection();
    PreparedStatement pstmt;

    if(logEnabled) // use a switch to toggle logging.
        pstmt = new LoggableStatement(conn,sql);
    else
        pstmt = conn.prepareStatement(sql);

    pstmt.setLong(1,fooValue);
    pstmt.setString(2,barValue);

    if(logEnabled)
       System.out.println("Executing query: "+
         ((LoggableStatement)pstmt).getQueryString());

    ResultSet rs = pstmt.executeQuery();

												
										





回页首


结束语

使用本文介绍的非常简单的步骤,您可以为查询记录扩展JDBC PreparedStatement 接口。我们在此处使用的技术可以被视为“通过封装来实现扩展”,或作为Decorator设计模式的一个实例(见 参考资料)。通过封装来实现扩展在当您必须扩展API但subclassing不是一项可选功能时极其有用。

posted @ 2006-08-24 17:50 Binary 阅读(229) | 评论 (0)编辑 收藏

使用Jakarta Commons Pool处理对象池化

     摘要: 恰当地使用对象池化技术,可以有效地减少对象生成和初始化时的消耗,提高系统的运行效率。Jakarta Commons Pool组件提供了一整套用于实现对象池化的框架,以及若干种各具特色的对象池实现,可以有效地减少处理对象池化时的工作量,为其它重要的工作留下更多的精力和时间。 创建新的对象并初始化的操作,可能会消耗很多的时间。在这种对象的初始化工作包含了一些费时的操作(例...  阅读全文

posted @ 2006-08-24 17:49 Binary 阅读(227) | 评论 (0)编辑 收藏

Java 理论和实践: 理解 JTS ― 幕后魔术

在这个系列的 第 1 部分,我们讨论了事务并研究了它们的基本属性 ― 原子性(atomicity)、一致性(consistency)、孤立性(isolation)和持久性(durability)。事务是企业应用程序的基本构件;没有它们,几乎不可能构建有容错能力的企业应用程序。幸运的是,Java 事务服务(Java Transaction Service,JTS)和 J2EE 容器自动为您做了大量的事务管理工作,这样您就不必将事务意识直接集成到组件代码中。结果简直是一种魔术 ― 通过遵守几条简单的规则,J2EE 应用程序就可以自动获得事务性语义,只需极少或根本不需要额外的组件代码。本文旨在通过展示事务管理如何发生,以及发生在何处来揭开这个魔术的神秘面纱。

什么是 JTS?

JTS 是一个 组件事务监视器(component transaction monitor)。这是什么意思?在第 1 部分,我们介绍了 事务处理监视器(TPM)这个概念,TPM 是一个程序,它代表应用程序协调分布式事务的执行。TPM 与数据库出现的时间长短差不多;在 60 年代后期,IBM 首先开发了 CICS,至今人们仍在使用。经典的(或者说 程序化)TPM 管理被程序化定义为针对事务性资源(比如数据库)的操作序列的事务。随着分布式对象协议,如 CORBA、DCOM 和 RMI 的出现,人们希望看到事务更面向对象的前景。将事务性语义告知面向对象的组件要求对 TPM 模型进行扩展 ― 在这个模型中事务是按照事务性对象的调用方法定义的。JTS 只是一个组件事务监视器(有时也称为 对象事务监视器(object transaction monitor)),或称为 CTM。

JTS 和 J2EE 的事务支持设计受 CORBA 对象事务服务(CORBA Object Transaction Service,OTS)的影响很大。实际上,JTS 实现 OTS 并充当 Java 事务 API(Java Transaction API)― 一种用来定义事务边界的低级 API ― 和 OTS 之间的接口。使用 OTS 代替创建一个新对象事务协议遵循了现有标准,并使 J2EE 和 CORBA 能够互相兼容。

乍一看,从程序化事务监视器到 CTM 的转变好像只是术语名称改变了一下。然而,差别不止这一点。当 CTM 中的事务提交或回滚时,与事务相关的对象所做的全部更改都一起被提交或取消。但 CTM 怎么知道对象在事务期间做了什么事?象 EJB 组件之类的事务性组件并没有 commit()rollback() 方法,它们也没向事务监视器注册自己做了什么事。那么 J2EE 组件执行的操作如何变成事务的一部分呢?





回页首


透明的资源征用

当应用程序状态被组件操纵时,它仍然存储在事务性资源管理器(例如,数据库和消息队列服务器)中,这些事务性资源管理器可以注册为分布式事务中的资源管理器。在第 1 部分中,我们讨论了如何在单个事务中征用多个资源管理器,事务管理器如何协调这些资源管理器。资源管理器知道如何把应用程序状态中的变化与特定的事务关联起来。

但这只是把问题的焦点从组件转移到了资源管理器 ― 容器如何断定什么资源与该事务有关,可以供它征用?请考虑下面的代码,在典型的 EJB 会话 bean 中您可能会发现这样的代码:


清单 1. bean 管理的事务的透明资源征用
												
														  InitialContext ic = new InitialContext();
  UserTransaction ut = ejbContext.getUserTransaction();
  ut.begin();
  DataSource db1 = (DataSource) ic.lookup("java:comp/env/OrdersDB");
  DataSource db2 = (DataSource) ic.lookup("java:comp/env/InventoryDB");
  Connection con1 = db1.getConnection();
  Connection con2 = db2.getConnection();
  // perform updates to OrdersDB using connection con1
  // perform updates to InventoryDB using connection con2
  ut.commit();

												
										

注意,这个示例中没有征用当前事务中 JDBC 连接的代码 ― 容器会为我们完成这个任务。我们来看一下它是如何发生的。

资源管理器的三种类型

当一个 EJB 组件想访问数据库、消息队列服务器或者其它一些事务性资源时,它需要到资源管理器的连接(通常是使用 JNDI)。而且,J2EE 规范只认可三种类型的事务性资源 ― JDBC 数据库、JMS 消息队列服务器和“其它通过 JCA 访问的事务性服务”。后面一种服务(比如 ERP 系统)必须通过 JCA(J2EE Connector Architecture,J2EE 连接器体系结构)访问。对于这些类型资源中的每一种,容器或提供者都会帮我们把资源征调到事务中。

在清单 1 中, con1con2 好象是普通的 JDBC 连接,比如那些从 DriverManager.getConnection() 返回的连接。我们从一个 JDBC DataSource 得到这些连接,JDBC DataSource 可以通过查找 JNDI 中的数据源名称得到。EJB 组件中被用来查找数据源( java:comp/env/OrdersDB )的名称是特定于组件的;组件的部署描述符的 resource-ref 部分将其映射为容器管理的一些应用程序级 DataSource 的 JNDI 名称。

隐藏的 JDBC 驱动器

每个 J2EE 容器都可以创建有事务意识的池态 DataSource 对象,但 J2EE 规范并不向您展示如何创建,因为这不在 J2EE 规范内。浏览 J2EE 文档时,您找不到任何关于如何创建 JDBC 数据源的内容。相反,您不得不为您的容器查阅该文档。创建一个数据源可能需要向属性或配置文件添加一个数据源定义,或者也可以通过 GUI 管理工具完成,这取决于您的容器。

每个容器(或连接池管理器,如 PoolMan)都提供它自己的创建 DataSource 机制,JTA 魔术就隐藏在这个机制中。连接池管理器从指定的 JDBC 驱动器得到一个 Connection ,但在将它返回到应用程序之前,将它与一个也实现 Connection 的虚包包在一起,将自己置入应用程序和底层连接之间。当创建连接或者执行 JDBC 操作时,包装器询问事务管理器当前线程是不是正在事务的上下文中执行,如果事务中有 Connection 的话,就自动征用它。

其它类型的事务性资源,JMS 消息队列和 JCA 连接器,依靠相似的机制将资源征用隐藏起来,使用户看不到。如果要使 JMS 队列在部署时对 J2EE 应用程序可用,您就要再次使用特定于提供者的机制来创建受管 JMS 对象(队列连接工厂和目标),然后在 JNDI 名称空间内发布这些对象。提供者创建的受管对象包含与 JDBC 包装器(由容器提供的连接池管理器添加)相似的自动征用代码。





回页首


透明的事务控制

两种类型的 J2EE 事务 ― 容器管理的和 bean 管理的 ― 在如何启动和结束事务上是不同的。事务启动和结束的地方被称为 事务划分(transaction demarcation)。清单 1 中的示例代码演示了 bean 管理的事务(有时也称为 编程(programmatic)事务)。Bean 管理的事务是由组件使用 UserTransaction 类显式启动和结束的。通过 ejbContext 使 UserTransaction 对 EJB 组件可用,通过 JNDI 使其对其它 J2EE 组件可用。

容器根据组件的部署描述符中的事务属性代表应用程序透明地启动和结束容器管理的事务(或称为 宣告式事务(declarative transaction))。通过将 transaction-type 属性设置为 ContainerBean 您可以指出 EJB 组件是使用 bean 管理的事务性支持还是容器管理的事务性支持。

使用容器管理的事务,您可以在 EJB 类或方法级别上指定事务性属性;您可以为 EJB 类指定缺省的事务性属性,如果不同的方法会有不同的事务性语义,您还可以为每个方法指定属性。这些事务性属性在装配描述符(assembly descriptor)的 container-transaction 部分被指定。清单 2 显示了一个装配描述符示例。 trans-attribute 的受支持的值有:

  • Supports
  • Required
  • RequiresNew
  • Mandatory
  • NotSupported
  • Never

trans-attribute 决定方法是否支持在事务内部执行、当在事务内部调用方法时容器会执行什么操作以及在事务外部调用方法时容器会执行什么操作。最常用的容器管理的事务属性是 Required 。如果设置了 Required ,过程中的事务将在该事务中征用您的 bean,但如果没有正在运行的事务,容器将为您启动一个。在这个系列的第 3 部分,当您可能想使用每个事务属性时,我们将研究各个事务属性之间的区别。


清单 2. EJB 装配描述符样本
												
														<assembly-descriptor>
  ...
  <container-transaction>
    <method>
      <ejb-name>MyBean</ejb-name>
      <method-name>*</method-name>
    </method>
    <trans-attribute>Required</trans-attribute>
  </container-transaction>
  <container-transaction>
    <method>
      <ejb-name>MyBean</ejb-name>
      <method-name>updateName</method-name>
      </method>
   <trans-attribute>RequiresNew</trans-attribute>
  </container-transaction>
  ...
</assembly-descriptor>

												
										

功能强大,但很危险

与清单 1 中的示例不同,由于有宣告式事务划分,这段组件代码中没有事务管理代码。这不仅使结果组件代码更加易读(因为它不与事务管理代码混在一起),而且它还有另一个更重要的优点 ― 不必修改,甚至不必访问组件的源代码,就可以在应用程序装配时改变组件的事务性语义。

尽管能够指定与代码分开的事务划分是一种非常强大的功能,但在装配时做出不好的决定会使应用程序变得不稳定,或者严重影响它的性能。对容器管理的事务进行正确分界的责任由组件开发者和应用程序装配人员共同担当。组件开发者需要提供足够的文档说明组件是做什么的,这样应用程序部署者就能够明智地决定如何构建应用程序的事务。应用程序装配人员需要理解应用程序中的组件是怎样相互作用的,这样就可以用一种既强制应用程序保持一致又不削弱性能的方法对事务进行分界。在这个系列的第 3 部分中我们将讨论这些问题。





回页首


透明的事务传播

在任何类型的事务中,资源征用都是透明的;容器自动将事务过程中使用的任意事务性资源征调到当前事务中。这个过程不仅扩展到事务性方法使用的资源(比如在清单 1 中获得的数据库连接),还扩展到它调用的方法(甚至远程方法)使用的资源。我们来看一下这是如何发生的。

容器用线程与事务相关联

我们假设对象 AmethodA() 启动一个事务,然后调用对象 BmethodB() (对象 B 将得到一个 JDBC 连接并更新数据库)。 B 获得的连接将被自动征调到 A 创建的事务中。容器怎么知道要做这件事?

当事务启动时,事务上下文与执行线程关联在一起。当 A 创建事务时, A 在其中执行的线程与该事务关联在一起。由于本地方法调用与主调程序(caller)在同一个线程内执行,所以 A 调用的每个方法也都在该事务的上下文中。

橱中骸骨

如果对象 B 其实是在另一个线程,甚至另一个 JVM 中执行的 EJB 组件的存根,情况会怎样?令人吃惊的是,远程对象 B 访问的资源仍将在当前事务中被征用。EJB 对象存根(在主调程序的上下文中执行的那部分)、EJB 协议(IIOP 上的 RMI)和远端的骨架对象协力要使其透明地发生。存根确定调用者是不是正在执行一个事务。如果是,事务标识,或者说 Xid,被作为 IIOP 调用的一部分与方法参数一起传播到远程对象。(IIOP 是 CORBA 远程-调用协议,它为传播执行上下文(比如事务上下文和安全性上下文)的各种元素而备;关于 RMI over IIOP 的更多信息,请参阅 参考资料。)如果调用是事务的一部分,那么远程系统上的骨架对象自动设置远程线程的事务上下文,这样,当调用实际的远程方法时,它已经是事务的一部分了。(存根和骨架对象还负责开始和提交容器管理的事务。)

事务可以由任何 J2EE 组件来启动 ― 一个 EJB 组件、一个 servlet 或者一个 JSP 页面(如果容器支持的话,还可以是一个应用程序客户机)。这意味着,应用程序可以在请求到达时在 servlet 或者 JSP 页面中启动事务、在 servlet 或者 JSP 页面中执行一些处理、作为页面逻辑的一部分访问多个服务器上的实体 bean 和会话 bean 并使所有这些工作透明地成为一个事务的一部分。图 1 演示了事务上下文怎样遵守从 servlet 到 EJB,再到 EJB 的执行路径。


图 1.单个事务中的多个组件
单个事务中的多个组件




回页首


最优化

让容器来管理事务允许容器为我们做出某些最优化决定。在图 1 中,我们看到一个 servlet 和多个 EJB 组件在单个事务的上下文中访问一个数据库。每个组件都获得到数据库的 Connection ;很可能每个组件都在访问同一个数据库。即使多个连接是从不同的组件到同一个资源,JTS 也可以检测出多个资源是否和事务有关,并最优化该事务的执行。您可以从第 1 部分回忆起来,单个事务要包含多个资源管理器需要使用两阶段提交协议,这比单个资源管理器使用的单阶段提交代价要高。JTS 能够确定事务中是不是只征用了一个资源管理器。如果它检测出所有与事务相关的资源都一样,它可以跳过两阶段提交并让资源管理器自己来处理事务。





回页首


结束语

这个虑及透明事务控制、资源征用和透明传播的魔术不是 JTS 的一部分,而是 J2EE 容器如何在幕后代表 J2EE 应用程序使用 JTA 和 JTS 服务的一部分。在幕后有许多实体合力使这个魔术透明地发生;EJB 存根和骨架、容器厂商提供的 JDBC 驱动器包装器、数据库厂商提供的 JDBC 驱动器、JMS 提供器和 JCA 连接器。所有这些实体都与事务管理器进行交互,于是应用程序代码就不必与之交互了。

在第 3 部分,我们将看一下关于管理 J2EE 上下文中事务的一些实际问题 ― 事务划分和孤立 ― 以及它们对应用程序一致性、稳定性和性能的影响。

posted @ 2006-08-24 17:47 Binary 阅读(236) | 评论 (0)编辑 收藏

仅列出标题
共8页: 上一页 1 2 3 4 5 6 7 8 下一页