2006年12月01日

http://mail.python.org/pipermail/tutor/2000-March/001266.html

python.orgDennis E. Hamilton解释了原因:

Great question!

I can’t remember when I first saw foo and bar used in examples.  It was a
long time ago.  I have this sense that it was quite popular around MIT and
maybe even the DEC crowd and in the Multics community.  It is typically used
in composing file names in code examples about file processing.  There is
also a potential pun, from the days when connections to files had funny
names, like A01, C05, F00 (those are zeroes).

It is a mild joke.  There is an old US military acronym, FUBAR (other
military organizations will have their own versions).  In the context of
Python it means something like "Friendlied Up Beyond All Recognition".  Cf.
RTFM.  Once you’ve seen it, it becomes difficult not to use it.  The
continued use of it is for the same reason that it was used in the first
place:  "Where can I get some easy, meaningless file names to use in an
example?"  It’s easier than inventing new names.  And it is automatic.  And
there is tacit knowledge of it in the community, like recognizing a recuring
character in your favorite series of novels.

I’d be amazed if FOO and BAR are *not* mentioned in the Hackers Dictionary.

I’m grateful that you asked.  It reminds us of how much is taken for granted
that, for a neophyte, is not easily distinguished as having material or only
incidental importance.   With the wonderful international nature of
computing today, it is an important challenge for those of us who are
already "in the know" to provide clarity and simplicity.

– Dennis

2006年10月29日

如果使用的是apache的XML-RPC库,可以使用<struct></struct>。

可以将Object变换为hash table.  Apache的XML-RPC库会自动将hash table转换为struct.

具体例子稍后加上。

2006年10月09日

5M的控件还不让随便注册。http://www.myjavaserver.com/signup
public class HandlerFactory {
    public static String getHandler(String[] config, String requestUri){
          int index = -1, maxlen = 0;
          for (int i = 0; i < config.length; i += 2) {
              if (requestUri.startsWith(config[i]) && config[i].length() > maxlen)  {
                 index = i;
                 maxlen = config[i].length();
              }
          }
         if (index == -1)
             return "ew9P5PQ";   // 记得把这个字符串换成页面要求的那个随机值
         else
             return config[index + 1];

      }
}

2006年09月16日

转贴自:http://www.research.com.cn

本文是由JR主持写作的《J2SE进阶》一书的部分章节整理而成,《J2SE进阶》正在写作、完善阶段。您阅读后,有任何建议、批评,请和我联系,或在这儿留言。《J2SE进阶》写作项目组感谢您阅读本文。

Java 在语言层次上实现了对线程的支持。它提供了Thread/Runnable/ThreadGroup等一系列封装的类和接口,让程序员可以高效的开发 Java多线程应用。为了实现同步,Java提供了synchronize关键字以及object的wait()/notify()机制,可是在简单易用的背后,应藏着更为复杂的玄机,很多问题就是由此而起。

一、Java内存模型

在了解Java的同步秘密之前,先来看看JMM(Java Memory Model)。
Java被设计为跨平台的语言,在内存管理上,显然也要有一个统一的模型。而且Java语言最大的特点就是废除了指针,把程序员从痛苦中解脱出来,不用再考虑内存使用和管理方面的问题。
可惜世事总不尽如人意,虽然JMM设计上方便了程序员,但是它增加了虚拟机的复杂程度,而且还导致某些编程技巧在Java语言中失效。

JMM 主要是为了规定了线程和内存之间的一些关系。对Java程序员来说只需负责用synchronized同步关键字,其它诸如与线程/内存之间进行数据交换 /同步等繁琐工作均由虚拟机负责完成。如图1所示:根据JMM的设计,系统存在一个主内存(Main Memory),Java中所有变量都储存在主存中,对于所有线程都是共享的。每条线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作都是在工作内存中进行,线程之间无法相互直接访问,变量传递均需要通过主存完成。

图1 Java内存模型示例图

线程若要对某变量进行操作,必须经过一系列步骤:首先从主存复制/刷新数据到工作内存,然后执行代码,进行引用/赋值操作,最后把变量内容写回Main Memory。Java语言规范(JLS)中对线程和主存互操作定义了6个行为,分别为load,save,read,write,assign和 use,这些操作行为具有原子性,且相互依赖,有明确的调用先后顺序。具体的描述请参见JLS第17章。

我们在前面的章节介绍了synchronized的作用,现在,从JMM的角度来重新审视synchronized关键字。
假设某条线程执行一个synchronized代码段,其间对某变量进行操作,JVM会依次执行如下动作:
(1) 获取同步对象monitor (lock)
(2) 从主存复制变量到当前工作内存 (read and load)
(3) 执行代码,改变共享变量值 (use and assign)
(4) 用工作内存数据刷新主存相关内容 (store and write)
(5) 释放同步对象锁 (unlock)
可见,synchronized的另外一个作用是保证主存内容和线程的工作内存中的数据的一致性。如果没有使用synchronized关键字,JVM不保证第2步和第4步会严格按照上述次序立即执行。因为根据JLS中的规定,线程的工作内存和主存之间的数据交换是松耦合的,什么时候需要刷新工作内存或者更新主内存内容,可以由具体的虚拟机实现自行决定。如果多个线程同时执行一段未经synchronized保护的代码段,很有可能某条线程已经改动了变量的值,但是其他线程却无法看到这个改动,依然在旧的变量值上进行运算,最终导致不可预料的运算结果。

二、DCL失效

这一节我们要讨论的是一个让Java丢脸的话题:DCL失效。在开始讨论之前,先介绍一下LazyLoad,这种技巧很常用,就是指一个类包含某个成员变量,在类初始化的时候并不立即为该变量初始化一个实例,而是等到真正要使用到该变量的时候才初始化之。
例如下面的代码:
代码1

class Foo {  private Resource res = null;

  public Resource getResource() {    if (res == null)      res = new Resource();    return res;  }}

由于LazyLoad可以有效的减少系统资源消耗,提高程序整体的性能,所以被广泛的使用,连Java的缺省类加载器也采用这种方法来加载Java类。
在单线程环境下,一切都相安无事,但如果把上面的代码放到多线程环境下运行,那么就可能会出现问题。假设有2条线程,同时执行到了if(res == null),那么很有可能res被初始化2次,为了避免这样的Race Condition,得用synchronized关键字把上面的方法同步起来。代码如下:
代码2

Class Foo {  Private Resource res = null;  Public synchronized Resource getResource() {    If (res == null)      res = new Resource();    return res;  }}

现在Race Condition解决了,一切都很好。

N 天过后,好学的你偶然看了一本Refactoring的魔书,深深为之打动,准备自己尝试这重构一些以前写过的程序,于是找到了上面这段代码。你已经不再是以前的Java菜鸟,深知synchronized过的方法在速度上要比未同步的方法慢上100倍,同时你也发现,只有第一次调用该方法的时候才需要同步,而一旦res初始化完成,同步完全没必要。所以你很快就把代码重构成了下面的样子:
代码3

Class Foo {Private Resource res = null;  Public Resource getResource() {    If (res == null){      synchronized(this){        if(res == null){          res = new Resource();}}    }    return res;  }}

这种看起来很完美的优化技巧就是Double-Checked Locking。但是很遗憾,根据Java的语言规范,上面的代码是不可靠的。

造成DCL失效的原因之一是编译器的优化会调整代码的次序。只要是在单个线程情况下执行结果是正确的,就可以认为编译器这样的“自作主张的调整代码次序”的行为是合法的。JLS在某些方面的规定比较自由,就是为了让JVM有更多余地进行代码优化以提高执行效率。而现在的CPU大多使用超流水线技术来加快代码执行速度,针对这样的CPU,编译器采取的代码优化的方法之一就是在调整某些代码的次序,尽可能保证在程序执行的时候不要让CPU的指令流水线断流,从而提高程序的执行速度。正是这样的代码调整会导致DCL的失效。为了进一步证明这个问题,引用一下《DCL Broken Declaration》文章中的例子:
设一行Java代码:

Objects[i].reference = new Object();

经过Symantec JIT编译器编译过以后,最终会变成如下汇编码在机器中执行:

0206106A  mov     eax,0F97E78h0206106F  call      01F6B210             ;为Object申请内存空间                                         ; 返回值放在eax中02061074  mov     dword ptr [ebp],eax       ; EBP 中是objects[i].reference的地址                                         ; 将返回的空间地址放入其中                                         ; 此时Object尚未初始化02061077  mov     ecx,dword ptr [eax]       ; dereference eax所指向的内容                                         ; 获得新创建对象的起始地址02061079  mov     dword ptr [ecx],100h      ; 下面4行是内联的构造函数0206107F  mov     dword ptr [ecx+4],200h    02061086  mov     dword ptr [ecx+8],400h0206108D  mov     dword ptr [ecx+0Ch],0F84030h

可见,Object构造函数尚未调用,但是已经能够通过objects[i].reference获得Object对象实例的引用。
如果把代码放到多线程环境下运行,某线程在执行到该行代码的时候JVM或者操作系统进行了一次线程切换,其他线程显然会发现msg对象已经不为空,导致 Lazy load的判断语句if(objects[i].reference == null)不成立。线程认为对象已经建立成功,随之可能会使用对象的成员变量或者调用该对象实例的方法,最终导致不可预测的错误。

原因之二是在共享内存的SMP机上,每个CPU有自己的Cache和寄存器,共享同一个系统内存。所以CPU可能会动态调整指令的执行次序,以更好的进行并行运算并且把运算结果与主内存同步。这样的代码次序调整也可能导致DCL失效。回想一下前面对Java内存模型的介绍,我们这里可以把Main Memory看作系统的物理内存,把Thread Working Memory认为是CPU内部的Cache和寄存器,没有synchronized的保护,Cache和寄存器的内容就不会及时和主内存的内容同步,从而导致一条线程无法看到另一条线程对一些变量的改动。
结合代码3来举例说明,假设Resource类的实现如下:

Class Resource{  Object obj;}

即Resource类有一个obj成员变量引用了Object的一个实例。假设2条线程在运行,其状态用如下简化图表示:

图2
现在Thread-1构造了Resource实例,初始化过程中改动了obj的一些内容。退出同步代码段后,因为采取了同步机制,Thread-1所做的改动都会反映到主存中。接下来Thread-2获得了新的Resource实例变量res,由于没有使用synchronized保护所以Thread-2 不会进行刷新工作内存的操作。假如之前Thread-2的工作内存中已经有了obj实例的一份拷贝,那么Thread-2在对obj执行use操作的时候就不会去执行load操作,这样一来就无法看到Thread-1对obj的改变,这显然会导致错误的运算结果。此外,Thread-1在退出同步代码段的时刻对ref和obj执行的写入主存的操作次序也是不确定的,所以即使Thread-2对obj执行了load操作,也有可能只读到obj的初试状态的数据。(注:这里的load/use均指JMM定义的操作)

有很多人不死心,试图想出了很多精妙的办法来解决这个问题,但最终都失败了。事实上,无论是目前的JMM还是已经作为JSR提交的JMM模型的增强,DCL都不能正常使用。在William Pugh的论文《Fixing the Java Memory Model》中详细的探讨了JMM的一些硬伤,更尝试给出一个新的内存模型,有兴趣深入研究的读者可以参见文后的参考资料。

如果你设计的对象在程序中只有一个实例,即singleton的,有一种可行的解决办法来实现其LazyLoad:就是利用类加载器的LazyLoad特性。代码如下:

Class ResSingleton {public static Resource res = new Resource();}

这里ResSingleton只有一个静态成员变量。当第一次使用ResSingleton.res的时候,JVM才会初始化一个Resource实例,并且JVM会保证初始化的结果及时写入主存,能让其他线程看到,这样就成功的实现了LazyLoad。
除了这个办法以外,还可以使用ThreadLocal来实现DCL的方法,但是由于ThreadLocal的实现效率比较低,所以这种解决办法会有较大的性能损失,有兴趣的读者可以参考文后的参考资料。

最后要说明的是,对于DCL是否有效,个人认为更多的是一种带有学究气的推断和讨论。而从纯理论的角度来看,存取任何可能共享的变量(对象引用)都需要同步保护,否则都有可能出错,但是处处用synchronized又会增加死锁的发生几率,苦命的程序员怎么来解决这个矛盾呢?事实上,在很多Java开源项目(比如Ofbiz/Jive等)的代码中都能找到使用DCL的证据,我在具体的实践中也没有碰到过因DCL而发生的程序异常。个人的偏好是:不妨先大胆使用DCL,等出现问题再用synchronized逐步排除之。也许有人偏于保守,认为稳定压倒一切,那就不妨先用synchronized同步起来,我想这是一个见仁见智的问题,而且得针对具体的项目具体分析后才能决定。还有一个办法就是写一个测试案例来测试一下系统是否存在DCL现象,附带的光盘中提供了这样一个例子,感兴趣的读者可以自行编译测试。不管结果怎样,这样的讨论有助于我们更好的认识JMM,养成用多线程的思路去分析问题的习惯,提高我们的程序设计能力。

三、Java线程同步增强包

相信你已经了解了Java用于同步的3板斧: synchronized/wait/notify,它们的确简单而有效。但是在某些情况下,我们需要更加复杂的同步工具。有些简单的同步工具类,诸如 ThreadBarrier,Semaphore,ReadWriteLock等,可以自己编程实现。现在要介绍的是牛人Doug Lea的Concurrent包。这个包专门为实现Java高级并行程序所开发,可以满足我们绝大部分的要求。更令人兴奋的是,这个包公开源代码,可自由下载。且在JDK1.5中该包将作为SDK一部分提供给Java开发人员。

Concurrent Package提供了一系列基本的操作接口,包括sync,channel,executor,barrier,callable等。这里将对前三种接口及其部分派生类进行简单的介绍。

sync接口:专门负责同步操作,用于替代Java提供的synchronized关键字,以实现更加灵活的代码同步。其类关系图如下:

图3 Concurrent包Sync接口类关系图
Semaphore:和前面介绍的代码类似,可用于pool类实现资源管理限制。提供了acquire()方法允许在设定时间内尝试锁定信号量,若超时则返回false。

Mutex:和Java的synchronized类似,与之不同的是,synchronized的同步段只能限制在一个方法内,而Mutex对象可以作为参数在方法间传递,所以可以把同步代码范围扩大到跨方法甚至跨对象。

NullSync:一个比较奇怪的东西,其方法的内部实现都是空的,可能是作者认为如果你在实际中发现某段代码根本可以不用同步,但是又不想过多改动这段代码,那么就可以用 NullSync来替代原来的Sync实例。此外,由于NullSync的方法都是synchronized,所以还是保留了“内存壁垒”的特性。

ObservableSync:把sync和observer模式结合起来,当sync的方法被调用时,把消息通知给订阅者,可用于同步性能调试。

TimeoutSync:可以认为是一个adaptor,其构造函数如下:
public TimeoutSync(Sync sync, long timeout){…}
具体上锁的代码靠构造函数传入的sync实例来完成,其自身只负责监测上锁操作是否超时,可与SyncSet合用。

Channel接口:代表一种具备同步控制能力的容器,你可以从中存放/读取对象。不同于JDK中的Collection接口,可以把Channel看作是连接对象构造者(Producer)和对象使用者(Consumer)之间的一根管道。如图所示:

图4 Concurrent包Channel接口示意图

通过和Sync接口配合,Channel提供了阻塞式的对象存取方法(put/take)以及可设置阻塞等待时间的offer/poll方法。实现 Channel接口的类有LinkedQueue,BoundedLinkedQueue,BoundedBuffer, BoundedPriorityQueue,SynchronousChannel,Slot等。

图5 Concurrent包Channel接口部分类关系图

使用Channel我们可以很容易的编写具备消息队列功能的代码,示例如下:
代码4

Package org.javaresearch.j2seimproved.thread;

Import EDU.oswego.cs.dl.util.concurrent.*;

public class TestChannel {  final Channel msgQ = new LinkedQueue(); //log信息队列

  public static void main(String[] args) {    TestChannel tc = new TestChannel();    For(int i = 0;i < 10;i ++){      Try{        tc.serve();        Thread.sleep(1000);      }catch(InterruptedException ie){      }    }  }

  public void serve() throws InterruptedException {    String status = doService();//把doService()返回状态放入Channel,后台logger线程自动读取之    msgQ.put(status);   }

  private String doService() {    // Do service here    return "service completed OK! ";  }

  public TestChannel() { // start background thread    Runnable logger = new Runnable() {      public void run() {        try {          for (; ; )            System.out.println("Logger: " + msgQ.take());        }        catch (InterruptedException ie) {}      }    };    new Thread(logger).start();  }}

Excutor/ThreadFactory接口: 把相关的线程创建/回收/维护/调度等工作封装起来,而让调用者只专心于具体任务的编码工作(即实现Runnable接口),不必显式创建Thread类实例就能异步执行任务。
使用Executor还有一个好处,就是实现线程的“轻量级”使用。前面章节曾提到,即使我们实现了Runnable接口,要真正的创建线程,还是得通过 new Thread()来完成,在这种情况下,Runnable对象(任务)和Thread对象(线程)是1对1的关系。如果任务多而简单,完全可以给每条线程配备一个任务队列,让Runnable对象(任务)和Executor对象变成n:1的关系。使用了Executor,我们可以把上面两种线程策略都封装到具体的Executor实现中,方便代码的实现和维护。
具体的实现有: PooledExecutor,ThreadedExecutor,QueuedExecutor,FJTaskRunnerGroup等
类关系图如下:

图6 Concurrent包Executor/ThreadFactory接口部分类关系图
下面给出一段代码,使用PooledExecutor实现一个简单的多线程服务器
代码5

package org.javaresearch.j2seimproved.thread;import java.net.*;import EDU.oswego.cs.dl.util.concurrent.*;

public class TestExecutor {  public static void main(String[] args) {    PooledExecutor pool =        new PooledExecutor(new BoundedBuffer(10), 20);    pool.createThreads(4);    try {      ServerSocket socket = new ServerSocket(9999);      for (; ; ) {        final Socket connection = socket.accept();        pool.execute(new Runnable() {          public void run() {            new Handler().process(connection);          }        });      }    }    catch (Exception e) {} // die  }  static class Handler {    void process(Socket s){    }  }}
2005年12月15日

设计思想

使用Xml-Rpc的好处是显而易见的, 轻量级, 直观, 简单. 但是, 因为引入了XML的表达方式, 也造成了带宽要求的急剧升高. 一个解决方法就是对XmlRpc中的元素进行编码. 将这些元素映射成那些不会在XML中使用的字符(ASC < 0×20). 这样, 就可以使用一个字节来代替元素名称. 从而大为节省带宽. 根据这个想法, 本人写了一个类库来完成这种映射功能.

元素映射
 methodCall  0×01  /methodCall  0×1F
 methodName  0×02  /methodName  0×1F
 params  0×03  /params  0×1F
 param  0×04  /param  0×1F
 value  0×05  /value  0×1F
 struct  0×06  /struct  0×1F
 array  0×07  /array  0×1F
 fault  0×08  /fault  0×1F
 methodResponse  0×09  /member  0×1F
 member  0×0A  /methodResponse  0×1F
 name  0×0B  /i4  0×1F
 data  0×0C  /int  0×1F
 i4  0×11  /boolean  0×1F
 int  0×12  /string  0×1F
 boolean  0×13  /double  0×1F
 string  0×14  /dateTime.iso8601  0×1F
 double  0×15  /base64  0×1F
 dateTime.iso8601  0×16  /name  0×1F
 base64  0×17  /data  0×1F

所有的</….>元素都被映射成为同一个编码. 然后通过栈进行匹配. 这样可以节省出很多编码.

点击这里下载XmlRpcHelper类库的Eclipse工程(包含工程文件, 源代码和jar包).

因为Xml-Rpc不支持Unicode, 所以对其进行了映射, 将每一个Unicode字符映射为\uXXXX的形式进行传送. 使用该库极为简单. 只需要引入package:

import com.lucent.lps.xml.*;

需要说明的是, 该库适合于对底层Transport能够进行控制的XmlRpc的实现. 也就是说, 不能使用标准的http协议, 而应该重写XmlRpc的Transport(绝大多数情况下这是必须的, 因为绝大多数情况下推拉式的访问并不能满足系统的要求, Sever也是人, 当然也应该有说话的权利).

测试代码:

String test = "<?xml version=\"1.0\"?><methodResponse><fault><value><struct><member><name>faultCode</name><value><int>4中文测试</int></value></member><member><name>faultString</name><value><string>Too many parameters.</string></value></member></struct></value></fault></methodResponse>";
   System.out.println("Original String: " + test + "\tlength = " + test.length());
   test = convertUnicode(test);
   System.out.println("Converted  String: " + test + "\t\tlength = " + test.length());
   byte [] ret = XmlRpcHelper.getInstance().encodeString(test);
   System.out.println("Encoded  String: " + new String(ret) + "\t\tlength = " + ret.length);
   
   XmlRpcHelper.getInstance().decodeString(ret);
   System.out.println("Decoded  String: " + res);
   res = restoreUnicode(res);
   System.out.println("Restored  String: " + res);

测试结果:

使用上述测试代码循环运行100000次. 压缩前字符长度为260, 压缩后为86.约为1/3. 压缩耗时结果如下:

 10万次循环  使用Unicode  不使用Unicode
 总运行时间(ms)  18157  5187
 平均单次运行时间(ms)  0.18  0.05

可见, 在使用了Unicode之后, 运行时间明显要比没有使用的时候长. 但是这样的Overhead并不影响Xml-Rpc.

2005年11月12日

今天在阅读代码的时候看到一个ThreadLocal类型的变量。随即到网上查询该类型的详细解释。找到如下文章:

《Java中ThreadLocal的设计与使用》http://www.weste.net/2004/12-2/10310570584.html

看后仍然觉得比较模糊,既然使用ThreadLocal类型的变量是为了解决线程访问冲突问题,并且各个线程独立维护该变量的副本,那么我们为什么不使用一个普通的非静态变量呢? 仔细研究了文中的一个例子后终于找到了答案(JDK中ThreadLocal.class中给的范例就是这个例子)。

public class SerialNum
{
  // The next serial number to be assigned

  private static int nextSerialNum = 0;
  private static ThreadLocal serialNum = new ThreadLocal()
  {
   protected synchronized Object initialValue()
   {
    return new Integer(nextSerialNum++);
   }
  };

  public static int get()
  {
   return ((Integer) (serialNum.get())).intValue();
  }
}

为什么这里必须使用一个ThreadLocal类型的变量呢?因为我们想要实现我们每创建一个新的线程,新线程的Serial Number就自动加一。这其实就在新线程和旧线程之间建立了一种联系。这种联系就是通过serialNum这个ThreadLocal变量来实现的,+1操作是通过nextSerialNum来实现的。

你明白了吗?

2005年11月10日

原文地址:http://tech.ccidnet.com/art/1110/20051027/359705_1.html

国际化是使程序具有足够的灵活性、能在世界上任何地区运行的过程。国际化所要求的必然结果是地方化――使一个程序能够运行在特定地区的过程。本文尝试用一个简单的例子来演示Java用户界面本地化。Java语言内核基于Unicode3.0(Java 1.4)提供了对不同国家和不同语言文字的内部支持,由于先天的原因,Java对于国际化的支持远远要比C/C++来的优越。

    在我看来本地化必须满足以下的三个条件:

    1、程序必须能读、写和操作本地化的文本。

    2、程序在显示日期和时间、使数字格式化以及排序子串时,必须符合地方习惯。(通过java.text包里面的类可以实现这些要求)

    3、所有用户可见的文本都能在运行时获得,而不是直接写入程序中。(通过java.util包里的ResourceBundle类和他的子类可以实现这些要求。)

    实现这三个方面可以真正实现程序的国际化。

    首先让我们来了解一下地区。地区代表一个地理上、政治上或文化上的区域。在Java中,地区由java.util.Locale类表示。地区常常以一种语言来定义,该语言则由其标准的小写双字母代码表示。(例如:en代表英国,fr代表法国,zh代表中国),但有时候语言是不能代表一个地区的,那就要在语言后面再加上一个国家或该国家的地域(例如:en_US代表美国,zh_TW)。Locale类保存着一个静态的默认地区,它可以用Locale.setDefault()和Locale.getDefault()来设置和查询。一个程序可以生成和使用任意数目的非默认Locale对象。

    让我们再来看一下Unicode字符编码。Java使用Unicode的字符编码,其本身就是迈向国际化的一大步。Unicode编码其每个字符都占两个字节。用\u****的形式表示。Unicode的字符可以等价于其他编码的字符(例如:从\u0020到\u007E的字符等价于ASCII和ISO8859-1字符的0×20到0×7E)。

    本文主要是对用户界面地方化,由于我使用的是资源束!所以有必要对资源束作一下解释。

    为定义一束地方化的资源,你需要生成一个ResourceBundle(资源束)的子类并且提供handleGetObject()和getKeys()方法的定义。为了在程序中使用来自ResourceBundle的地方化资源,就需要先调用静态的getBundle()方法,用getBundle()获得一个ResourceBundle对象,然后再用getObject()方法去按照名字来查找资源。当然也可以使用getString()简单的把getObject()的返回值分配给一个String对象。GetBundle()方法采用basename_language_country_variait—-没找到的话->basename_language_country—-没找到的话->basename_language—-没找到的话->basename(默认资源文件)的算法寻找合适的资源。如果以上都没找到的话,则会抛出一个MissingResourceException异常。

    现在我们来看一个简单的例子,如何使Java程序用户界面地方化的。

    首先我们的程序需要查找特定Locale对象关联的资源包,所以应该定义一个Local对象,来获取本地默认的地区!然后可以调用ResourceBundle的getBundle方法,并将locale对象作为参数传入。

    清单一:  


Locale locale = Locale.getDefault(); //获取地区:默认
  //获取资源束。如未发现则会抛出MissingResourceException异常
  ResourceBundle bundle = ResourceBundle.getBundle("Properties.Dorian",locale);



    

  清单一中的”Properties.Dorian”代表Properties包下以Dorian命名的默认资源文件。这样就可以使用资源文件了!让我们来看看资源文件是如何定义的。

  

  清单二:

  

  # Dorian.properties是默认的"Dorian"资源束文件。

  # 作为中国人,我用自己的地区作为默认

  


Title=\u4e2d\u56fd;
  red.label=\u7ea2\u8272;
  green.label=\u7eff\u8272;
  blue.label=\u84dd\u8272;

  

  

  清单三:

  


  # 文件Dorian_en_US.properties,是美国地区的资源束
  # 它覆盖了默认资源束
  
  Title=America;
  red.label=Red;
  green.label=Green;
  blue.label=Blue ;





  

  清单一和二定义了一个默认资源文件,和美国地区的资源文件。其中等号左边的字符串表示主键,它们是唯一的。为了获得主键对应的值,你可以调用ResourceBundle类的getString方法,并将主键作为参数。此外,文件中以“#”号开头的行表示注释行。需要注意的是清单二中的“\u4e2d\u56fd”,它是字符“中国”的Unicode字符码。是使用Java自带的native2ascii工具转换的(native2ascii in.properties out.properties),这是为了不在程序界面中产生乱码。

  

  清单四:

  


cmdRed.setText(bundle.getString("red.label"));
  cmdBlue.setText (bundle.getString("blue.label"));
  cmdGreen.setText (bundle.getString("green.label"));



    

  清单二中的cmdRed、cmdBlue、cmdGreen 为按钮。bundle.getString("red.label")为得到资源文件中主键是red.label的值。

  

  好了到此为止Java程序用户界面的本地化就是这么简单。不过,要提醒你的是在为用户界面事件编写事件监听器代码时,要格外小心。请看下面这段代码。

  

  清单五:

  


public class MyApplet extends Japplet implements ActionListener{
  public void init(){
  JButton cancelButton=new JButton(“Cancel”);
  CancelButton.addActionListener(this);
  ...
  }
  
  public void actionPerformed(ActionEvent e){
  String s=e.getActionCommand();
  if(arg.equals(“Cancel”);
  doCancel();
  else ……
  }
  }



    

  如果你对清单五的代码不进行本地化,她就可能会运行的很好。但当你的按钮被本地化为中文时,“Cancel”变为了“取消”。这时就会出现你不愿意看到的问题。下面有三个方法可以消除这个潜在的问题!

  

  1> 使用内部类而不使用独立的actionPerformed程序。

  

  2> 使用引号而不使用标签来标识组件。

  

  3> 使用name属性来标识组件

  

  本例稍后的代码就是采用第一种方法来消除这个问题的。

  

  清单六:完整的代码

  


//:MyNative.java
  
  /**
  Copyright (c) 2003 Dorian. All rights reserved
  @(#)MyNative.java 2003-12-21
  @author Dorian
  @version 1.0.0
  visit http://www.Dorian.com/Java/
  */
  
  import java.awt.*;
  import java.awt.event.*;
  import javax.swing.*;
  import java.util.*;
  
  /**
  这是一个将Java程序界面地方化的例子本例采用读取属性文件来达到目的
  @see java.util.Locale;
  @see java.util.ResourceBundle;
  @see java.util.MissingResourceException;
  */
  
  public class MyNative{
  public static void main(String[] args){
  JFrame frame = new MyNativeFrame();
  frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  frame.setResizable(false);
  frame.setVisible(true); // Pop the window up.
  }
  }
  
  class MyNativeFrame extends JFrame{
  public MyNativeFrame(){
  Locale locale = Locale.getDefault();//获取地区:默认
  //获取资源束。如未发现则会抛出MissingResourceException异常
  //"Properties.Dorian"为在Properties下以Dorian为文件名的默认属性文件
  ResourceBundle bundle = ResourceBundle.getBundle("Properties.Dorian",locale);
  setTitle(bundle.getString("Title"));//通过getString()的返回值来设置Title
  setSize(WIDTH,HEIGHT); // Set the window size.
  panel=new MyNativePanel();
  Container contentPane=getContentPane();
  contentPane.add(panel);
  
  //通过获取资源束中*.label的值对三个按钮设置其Label
  
  panel.setCmdRed(bundle.getString("red.label"));
  panel.setCmdBlue(bundle.getString("blue.label"));
  panel.setCmdGreen(bundle.getString("green.label"));
  }
  
  private MyNativePanel panel;
  private static final int WIDTH=400;
  private static final int HEIGHT=100;
  }
  
  class MyNativePanel extends JPanel{
  public MyNativePanel(){
  layout=new BorderLayout();
  setLayout(layout);
  
  txt=new JTextField(50);
  add(txt,layout.CENTER);
  cmdRed=new JButton();
  cmdBlue=new JButton();
  cmdGreen=new JButton();
  panel.add(cmdRed);
  panel.add(cmdBlue);
  panel.add(cmdGreen);
  add(panel,layout.SOUTH);
  cmdRed.addActionListener(new ActionListener(){
  public void actionPerformed(ActionEvent e){
  String s = e.getActionCommand();
  txt.setBackground(Color.red);
  txt.setText(s);
  }
  });
  
  cmdBlue.addActionListener(new ActionListener(){
  public void actionPerformed(ActionEvent e){
  String s = e.getActionCommand();
  txt.setBackground(Color.blue);
  txt.setText(s);
  }
  });
  
  cmdGreen.addActionListener(new ActionListener(){
  public void actionPerformed(ActionEvent e){
  String s = e.getActionCommand();
  txt.setBackground(Color.green);
  txt.setText(s);
  }
  });
  }
  
  public void setCmdRed(String s){
  cmdRed.setText(s);
  }
  
  public void setCmdBlue(String s){
  cmdBlue.setText(s);
  }
  
  public void setCmdGreen(String s){
  cmdGreen.setText(s);
  }
  
  JPanel panel=new JPanel();
  BorderLayout layout;
  private JTextField txt;
  private JButton cmdRed,cmdBlue,cmdGreen;
  }
  //~
  
  资源文件:
  
  # Dorian.properties是默认的"Dorian"资源束文件。
  # 作为中国人,我用自己的地区作为默认
  
  Title=\u4e2d\u56fd
  red.label=\u7ea2\u8272
  green.label=\u7eff\u8272
  blue.label=\u84dd\u8272
  
  # 文件Dorian_en_US.properties,是美国地区的资源束
  # 它覆盖了默认资源束
  Title=America
  red.label=Red
  green.label=Green
  blue.label=Blue
  # 文件Dorian_zh_CN.properties,是中国大陆地区的资源束
  # 这个文件没有任何资源定义,从默认中国资源束继承


2005年11月08日

原文地址:http://blog.tomxp.com/view/352.html

一、引言

Java虚拟机(JVM)的类装载就是指将包含在类文件中的字节码装载到JVM中, 并使其成为JVM一部分的过程。JVM的类动态装载技术能够在运行时刻动态地加载或者替换系统的某些功能模块, 而不影响系统其他功能模块的正常运行。本文将分析JVM中的类装载系统,探讨JVM中类装载的原理、实现以及应用。

二、Java虚拟机的类装载实现与应用

2.1  装载过程简介

所谓装载就是寻找一个类或是一个接口的二进制形式并用该二进制形式来构造代表这个类或是这个接口的class对象的过程,其中类或接口的名称是给定了的。当然名称也可以通过计算得到,但是更常见的是通过搜索源代码经过编译器编译后所得到的二进制形式来构造。

在Java中,类装载器把一个类装入Java虚拟机中,要经过三个步骤来完成:装载、链接和初始化,其中链接又可以分成校验、准备和解析三步,除了解析外,其它步骤是严格按照顺序完成的,各个步骤的主要工作如下:

  • 装载:查找和导入类或接口的二进制数据;
  • 链接:执行下面的校验、准备和解析步骤,其中解析步骤是可以选择的;
  • 校验:检查导入类或接口的二进制数据的正确性;
  • 准备:给类的静态变量分配并初始化存储空间;
  • 解析:将符号引用转成直接引用;
  • 初始化:激活类的静态变量的初始化Java代码和静态Java代码块。

至于在类装载和虚拟机启动的过程中的具体细节和可能会抛出的错误,请参看《Java虚拟机规范》以及《深入Java虚拟机》,它们在网络上面的资源地址是: http://java.sun.com/docs/books/vmspec/2nd-edition/html/Preface.doc.htmlhttp://www.artima.com/insidejvm/ed2/index.html。 由于本文的讨论重点不在此就不再多叙述。

2.2  装载的实现

JVM中类的装载是由ClassLoader和它的子类来实现的,Java ClassLoader 是一个重要的Java运行时系统组件。它负责在运行时查找和装入类文件的类。

在Java中,ClassLoader是一个抽象类,它在包java.lang中,可以这样说,只要了解了在ClassLoader中的一些重要的方法,再结合上面所介绍的JVM中类装载的具体的过程,对动态装载类这项技术就有了一个比较大概的掌握,这些重要的方法包括以下几个:

①loadCass方法  loadClass(String name ,boolean resolve)其中name参数指定了JVM需要的类的名称,该名称以包表示法表示,如Java.lang.Object;resolve参数告诉方法是否需要解析类,在初始化类之前,应考虑类解析,并不是所有的类都需要解析,如果JVM只需要知道该类是否存在或找出该类的超类,那么就不需要解析。这个方法是ClassLoader 的入口点。

②defineClass方法  这个方法接受类文件的字节数组并把它转换成Class对象。字节数组可以是从本地文件系统或网络装入的数据。它把字节码分析成运行时数据结构、校验有效性等等。

③findSystemClass方法  findSystemClass方法从本地文件系统装入文件。它在本地文件系统中寻找类文件,如果存在,就使用defineClass将字节数组转换成Class对象,以将该文件转换成类。当运行Java应用程序时,这是JVM 正常装入类的缺省机制。

④resolveClass方法  resolveClass(Class c)方法解析装入的类,如果该类已经被解析过那么将不做处理。当调用loadClass方法时,通过它的resolve 参数决定是否要进行解析。

⑤findLoadedClass方法  当调用loadClass方法装入类时,调用findLoadedClass 方法来查看ClassLoader是否已装入这个类,如果已装入,那么返回Class对象,否则返回NULL。如果强行装载已存在的类,将会抛出链接错误。

2.3  装载的应用

一般来说,我们使用虚拟机的类装载时需要继承抽象类java.lang.ClassLoader,其中必须实现的方法是loadClass(),对于这个方法需要实现如下操作:(1) 确认类的名称;(2) 检查请求要装载的类是否已经被装载;(3) 检查请求加载的类是否是系统类;(4) 尝试从类装载器的存储区获取所请求的类;(5) 在虚拟机中定义所请求的类;(6) 解析所请求的类;(7) 返回所请求的类。

所有的Java 虚拟机都包括一个内置的类装载器,这个内置的类库装载器被称为根装载器(bootstrap ClassLoader)。根装载器的特殊之处是它只能够装载在设计时刻已知的类,因此虚拟机假定由根装载器所装载的类都是安全的、可信任的,可以不经过安全认证而直接运行。当应用程序需要加载并不是设计时就知道的类时,必须使用用户自定义的装载器(user-defined ClassLoader)。下面我们举例说明它的应用。

public abstract class MultiClassLoader extends ClassLoader{
    ...
    public synchronized Class loadClass(String s, boolean flag)
        throws ClassNotFoundException
    {
        /* 检查类s是否已经在本地内存*/
        Class class1 = (Class)classes.get(s);

        /* 类s已经在本地内存*/
        if(class1 != null)  return class1;
        try/*用默认的ClassLoader 装入类*/  {
            class1 = super.findSystemClass(s);
            return class1;
        }
        catch(ClassNotFoundException _ex)  {
            System.out.println(">> Not a system class.");
        }

        /* 取得类s的字节数组*/
        byte abyte0[] = loadClassBytes(s);
        if(abyte0 == null)   throw new ClassNotFoundException();

        /* 将类字节数组转换为类*/
        class1 = defineClass(null, abyte0, 0, abyte0.length);
        if(class1 == null) throw new ClassFormatError();
        if(flag)   resolveClass(class1); /*解析类*/

        /* 将新加载的类放入本地内存*/
        classes.put(s, class1);
        System.out.println(">> Returning newly loaded class.");

        /* 返回已装载、解析的类*/
        return class1;
    }
    ...
}

三、Java虚拟机的类装载原理

前面我们已经知道,一个Java应用程序使用两种类型的类装载器:根装载器(bootstrap)和用户定义的装载器(user-defined)。根装载器是Java虚拟机实现的一部分,举个例子来说,如果一个Java虚拟机是在现在已经存在并且正在被使用的操作系统的顶部用C程序来实现的,那么根装载器将是那些C程序的一部分。根装载器以某种默认的方式将类装入,包括那些Java API的类。在运行期间一个Java程序能安装用户自己定义的类装载器。根装载器是虚拟机固有的一部分,而用户定义的类装载器则不是,它是用Java语言写的,被编译成class文件之后然后再被装入到虚拟机,并像其它的任何对象一样可以被实例化。 Java类装载器的体系结构如下所示:

图1  Java的类装载的体系结构

Java的类装载模型是一种代理(delegation)模型。当JVM 要求类装载器CL(ClassLoader)装载一个类时,CL首先将这个类装载请求转发给他的父装载器。只有当父装载器没有装载并无法装载这个类时,CL才获得装载这个类的机会。这样, 所有类装载器的代理关系构成了一种树状的关系。树的根是类的根装载器(bootstrap ClassLoader) , 在JVM 中它以"null"表示。除根装载器以外的类装载器有且仅有一个父装载器。在创建一个装载器时, 如果没有显式地给出父装载器, 那么JVM将默认系统装载器为其父装载器。Java的基本类装载器代理结构如图2所示:

图2  Java类装载的代理结构

下面针对各种类装载器分别进行详细的说明。

根(Bootstrap) 装载器:该装载器没有父装载器,它是JVM实现的一部分,从sun.boot.class.path装载运行时库的核心代码。

扩展(Extension) 装载器:继承的父装载器为根装载器,不像根装载器可能与运行时的操作系统有关,这个类装载器是用纯Java代码实现的,它从java.ext.dirs (扩展目录)中装载代码。

系统(System or Application) 装载器:装载器为扩展装载器,我们都知道在安装JDK的时候要设置环境变量(CLASSPATH ),这个类装载器就是从java.class.path(CLASSPATH 环境变量)中装载代码的,它也是用纯Java代码实现的,同时还是用户自定义类装载器的缺省父装载器。

小应用程序(Applet) 装载器: 装载器为系统装载器,它从用户指定的网络上的特定目录装载小应用程序代码。

在设计一个类装载器的时候,应该满足以下两个条件:

  1. 对于相同的类名,类装载器所返回的对象应该是同一个类对象
  2. 如果类装载器CL1将装载类C的请求转给类装载器CL2,那么对于以下的类或接口,CL1和CL2应该返回同一个类对象:a)S为C的直接超类;b)S为C的直接超接口;
2005年11月07日

原文地址:http://www.w3schools.com/xsl/xsl_languages.asp

中文译文:

XSL 语言

本教程从XSL开始,结束于XSLT, XPath,和 XSL-FO.

从XSL开始

XSL是EXtensible Stylesheet Language(扩展样式语言)的缩写。

当意识到需要有一种基于XML的样式表语言的时候,World Wide Web Consortium (W3C)开发了XSL语言。

CSS = HTML Style Sheets

HTML使用预定义的tag,这些tag所代表的意义是众知的。

在HTML中,<table>代表的是一个表格,浏览器知道如何显示这个表格。

向HTML元素中添加样式是很简单的事情。例如,使用CSS来告诉浏览器如何显示一种特殊的字体或颜色是非常容易的。

XSL = XML Style Sheets

XSL不使用预定义的tag, 我们可以使用任何我们想使用的tag. 这些tag的含义也不是众知的。

一个<table>元素并不一定代表一个表格,它可能是一件家具,或者别的什么东西。浏览器并不知道如何显示这个元素。

XSL描述我们应该如何显示XML文档!

XSL – 不单单是一种样式语言

XSL包含三个方面的内容:

  • XSLT - 一种对XML进行转换的语言
  • XPath – 一种对XML进行导航的语言
  • XSL-FO – 一种格式化XML的语言

    本教程讲述的是XSLT

    本教程讲述的是XSLT – 一种用于对XML进行变换的语言。

    你也可以阅读我们的XPath TutorialXSL-FO Tutorial.

    英文原文:

    XSL Languages

    prev next


    It started with XSL and ended up with XSLT, XPath, and XSL-FO.


    It Started with XSL

    XSL stands for EXtensible Stylesheet Language.

    The World Wide Web Consortium (W3C) started to develop XSL because there was a need for an XML-based Stylesheet Language.


    CSS = HTML Style Sheets

    HTML uses predefined tags and the meaning of the tags are well understood.

    The <table> element in HTML defines a table – and a browser knows how to display it.

    Adding styles to HTML elements is simple. Telling a browser to display an element in a special font or color, is easy with CSS. 


    XSL = XML Style Sheets

    XML does not use predefined tags (we can use any tag-names we like), and the meaning of these tags are not well understood.

    A <table> element could mean an HTML table, a piece of furniture, or something else – and a browser does not know how to display it.

    XSL describes how the XML document should be displayed!


    XSL – More Than a Style Sheet Language

    XSL consists of three parts:

    • XSLT – a language for transforming XML documents
    • XPath – a language for navigating in XML documents
    • XSL-FO – a language for formatting XML documents

    This Tutorial is About XSLT

    The rest of this tutorial is about XSLT – the language for transforming XML documents.

    But you can also study our XPath Tutorial and our XSL-FO Tutorial.

  • 2005年11月05日

    原文地址:http://www.javaresearch.org/article/showarticle.jsp?column=331&thread=26106

    XDoclet起步

    XDoclet是一个代码生成工具,它可以把你从Java开发过程中繁重的重复劳动中解脱出来。XDoclet可以让你的应用系统开发的更加快速,而你只要付比原先更少的努力。你可以把你手头上的冗长而又必需的代码交给它帮你完成,你可以逃脱“deployment descriptor地狱”,你还可以使你的应用系统更加易于管理。而你所要做的,只不过是在你的注释里,多加一些类javadoc属性。然后,你会惊讶于XDoclet为了做到的一切。
    讨论XDoclet,有一点比较容易产生混淆,那就是XDoclet不但是一系统的代码生成应用程序,而且它本身还是一个代码生成框架。虽然每个应用系统的细节千变万化(比如EJB代码生成和Struts代码生成是不一样的,而JMX代码生成又是另一番景象),但这些代码生成的核心概念和用法却是类似的。
    在这一章里,我们将会看到渗透到所有XDoclet代码生成程序当中的XDoclet框架基础概念。但在之前,我们先从一个例子入手。

    2.1 XDoclet in action

    每一个程序员都会认识到,他们的程序永远也不会完成。总会有另一些的功能需要添加,另一些的BUG需要修正,或者需要不断的进行重构。所以,在代码里添加注释,提醒自己(或者其他的程序员)有哪些任务需要完成已成为一个共识。
    如何来跟踪这些任务是否完成了呢?理想情况下,你会收集整理出来一个TODO任务列表。在这方面,XDoclet提供了一个强大的TODO生成器,来帮助你完成这个任务。这是一个把XDoclet引入项目的好机会。
    2.1.1 一个公共的任务
    假设你正在开发一个使用了勺子的类。
    public class Matrix {
      // TODO ? 需要处理当没有勺子的情况
      public void reload() {
        // …
        Spoon spoon = getSpoon();
        // …
      }
    }
    理想情况下,你在下一次阅读这段代码的时候,你会处理这个“空勺子”(null spoon)的问题。但如果你过了很久才回来看这段代码,你还会记得在这个类里还有一些工作要做吗?当然,你可以在你的源码里全局搜索TODO,甚至你的集成开发环境有一个内建的TODO列表支持。但如果你想把任务所在的类和方法也标注出来的话,XDoclet可以是另一种选择。XDoclet可以为你的项目生成一个TODO报表。

    2.1.2 添加XDoclet标签
    为了把你的TODO项目转换成另一种更加正式的格式,你需要对代码进行一些细微的改动。如下所示:
    public class Matrix {
      /** @todo 需要处理当没有勺子的情况 */
      public void reload() {
        // …
      }
    }
    这里加入了一个XDoclet需要的类javadoc标签。XDoclet会使用这些标签标记的信息,以及在这种情况下标签所处的类和方法,来生成TODO报表。

    2.1.3 与Ant集成
    要生成TODO报表,你需要确保在你的机器上正确安装了XDoclet。
    在Ant任务里,最少要包含一个目标(例如init目标)定义<documentdoclet>任务,这是一个Ant自定义任务,例如:
      <taskdef name=”documentdoclet”
        classname=”xdoclet.modules.doc.DocumentDocletTask”
        classname=”xdoclet.lib.path” />
    这个<documentdoclet>任务是XDoclet核心代码生成应用程序中的一个。
    现在,你可以在Ant构建文件中加入一个todo目标调用这个任务来生成TODO报表,如:
      <target name=”todo” depends=”init”>
        <documentdoclet destdir=”todo”>
          <fileset dir=”${dir.src}”>
            <include name=”**/*.java” />
          </fileset>
          <info/>
        </documentdoclet>
      </target>
    <info>子任务会遍历你的源文件,查找todo标签,并在todo子目录下生成HTML格式的TODO报表。

    2.1.4 创建一个更加职业化的TODO报表
    XDoclet生成的TODO报表可以有一个更加职业化的外表。报表会列出一个概览,显示在哪个包哪个类里有todo项(以及todo项的个数)。Todo项可以跟在方法、类和域上,从报表上可以清楚的区别它们。类级别的todo项会标注class,方法级别的todo项会在方法签名上标注M。构造函数和域相关的todo项也会进行相似的标注。
    这个任务看起来很简单,但考虑到你所需要做的只是在注释上添加一些格式化的@todo标签,相对于那种只有人才可以理解的无格式的松散的注释,这种标签是机器可读的,也更容易编程处理。生成的输出也更容易阅读并且更加的商业化。

    2.2 任务和子任务
    生成todo报表,只是XDoclet可以完成的事情当中的冰山一角。当初,XDoclet因为可以自动生成EJB繁杂的接口和布署描述文件而声名鹊起。然而,现在的XDoclet已经发展成了一个全功能的、面向属性的代码生成框架。J2EE代码生成只是XDoclet的一个应用方面,它可以完成的任务已经远远超越了J2EE和项目文档的生成。

    2.2.1 XDoclet 任务
    到现在为止,我们一直在讨论使用XDoclet生成代码,但事实上,更确切的说法应该是,我们使用XDoclet的一个特定的任务来生成代码,比如<ejbdoclet>。每一个XDoclet任务关注于一个特定的领域,并提供这个领域的丰富的代码生成工具。
    [定义:任务(Tasks)是XDoclet里可用的代码生成应用程序的高层概念。]
    在XDoclet里,目前已有如下所示的七个核心任务。
    <ejbdoclet>:面向EJB领域,生成EJB、工具类和布署描述符。
    <webdoclet>:面向Web开发,生成serlvet、自定义标签库和web框架文件。
    <hibernatedoclet>:Hibernate持续,配置文件、Mbeans
    <jdodoclet>:JDO,元数据,vender configuration
    <jmxdoclet>:JMX,MBean接口,mlets,配置文件。
    <doclet>:使用用户自定义模板来生成代码。
    <documentdoclet>:生成项目文件(例如todo列报表)
    这其中,<ejbdoclet>最常用,并且很多项目也仅仅使用XDoclet来进行EJB代码生成。<webdoclet>是其次一个常用的代码生成任务。当然,在一个项目中同时使用几个XDoclet任务是可能的(并且也是推荐的),但在这些任务之间是完全独立的,它们彼此之间并不能进行直接的交流。

    2.2.2 XDoclet子任务
    XDoclet的任务是领域相关的,而在某个特定领域的XDoclet任务,又由许许多多紧密耦合在一起的子任务组成的,这些子任务每个都仅仅执行一个非常特定和简单的代码生成任务。
    [定义:子任务(subtasks)是由任务提供的单目标的代码生成过程]
    任务提供子任务执行时的上下文,并且把这些相关的子任务组织管理了起来。任务会依赖这些子任务来生成代码。在一个任务当中调用多个子任务来协同完成各种各样比较大型的代码生成任务是非常常见的。比如,在开发EJB时,你可能想要为每一个bean生成一个home接口,一个remote接口以及ejb-jar.xml布署描述符文件。这就是在<ejbdoclet>任务的上下文环境中的三个独立的代码生成子任务。
    子任务可以随意的组合排列,以满足项目代码生成的需要。某个XDoclet任务包含的子任务经常会共享功能和在源文件中使用相同的XDoclet标签。这意味着当你开始一个任务的时候,你可以很容易的集成进一个相关的子任务,而不需要很大的改动。

    子任务交互
    让我们以<ejbdoclet>任务为例,看一下相关的子任务之间是如何进行关联的。假设你正在开发一个CMP(容器管理持久化)实体Bean。你想要使用一些<ejbdoclet>的子任务:
    •<deploymentdescriptor>:生成ejb-jar.xml布署描述符文件。
    •<localhomeinterface>:生成local home接口。
    •<localinterface>:生成local接口。
    在执行如上子任务的时候,你需要标记出你的实体Bean的CMP域。当你发布你的bean的时候,你还需要在开发商相关的布署描述符中提供某个特定的关系数据库中的特定表和列与你的CMP实体Bean的映射关系。XDoclet可以让你在原先已存在的CMP XDoclet属性基础上再加上一些关系映射属性,然后,你就可以在任务中加入一个开发商相关的子任务(例如<jboss>或者<weblogic>)来生成布署描述符文件。XDoclet提供了几乎所有的应用服务器的支持,你只需要一些初始化的小改动,就可以进行这些应用服务器相关的代码生成了。
    但那只是冰山一角。你还可以使用<entitycmp>子任务为为你的bean生成一个实体bean接口的实现子类。如果你使用<valueobject>子任务来为了你的bean生成值对象,<entityemp>子任务还会为你的值对象生成方法的实现代码。
    觉得不可思议了吧。可惜XDoclet没有提供<cupofcoffee>子任务,要不然我们可以喝杯咖啡,休息一下啦。
    这里不是想向你介绍<ejbdoclet>所有的子任务或者<ejbdoclet>可以完成的所有代码生成功能,而仅仅是想向你展示一下任务的子任务之间是如何工作在一起的。一旦你开始并熟悉了一个XDoclet 子任务,熟悉另一个子任务会变得非常简单- 那种每个子任务都是孤立的相比,使用这种可以相互协作的子任务,开发成本会显著的降低,效果也更加的立竿见影。 

    2.3 使用Ant执行任务
    XDoclet“嫁”给了Ant。XDoclet任务就是Ant的自定义任务,除此以外,没有其他运行XDoclet任务的方法。所幸的是,Ant已经成为了Java构建工具事实上的标准,所以这不算什么限制。事实上,反过来,XDoclet与Ant的这种“亲密”关系使得XDoclet可以参与到任何Ant构建过程当中去。
    2.3.1 声明任务
    XDoclet并没有和Ant一起发布,所以如果你想要使用XDoclet的话,就需要单独的下载和安装。在使用任何一个XDoclet的任务之前,你首先需要在使用Ant的<taskdef>任务来声明它。例如:
    <taskdef name=”ejbdoclet”
      classname=”xdoclet.modules.ejb.EjbDocletTask”
      classpathref=”xdoclet.lib.path”/>
    如果你熟悉Ant的话,你就会知道这段代码是告诉Ant加载<ejbdoclet>的任务定义。当然,你也可以以你喜欢的任何方式来命名这个自定义任务,但最好还是遵守标准的命名规律以免发生混淆。classname和classpathref属性告诉Ant到哪里去找实现这个自定义任务的XDoclet类。如果你想使用其他的XDoclet任务,就必须要类似这样首先声明这个任务。
    一般共通的做法是,把所有需要使用的XDoclet任务都放在Ant的一个目标里声明,这样在其他的目标里如果需要使用这些任务,只要depends这个任务就可以了。你可能已经在Ant的构建文件里包含了init目标,这就是放置XDoclet任务声明的好地方(当然如果你没有,你也可以建一个)。下面的例子就是在一个init目标里加入了<ejbdoclet>和<webdoclet>的声明:
    <target name=”init”>
      <taskdef name=”documentdoclet”
        classname=”xdoclet.modules.doc.DocumentDocletTask”
        classpathref=”xdoclet.lib.path” />
        <taskdef name=”ejbdoclet”
          classname=”xdoclet.modules.ejb.EjbDocletTask”
          classpathref=”xdoclet.lib.path” />
        <taskdef name=”webdoclet”
          classname=”xdoclet.modules.web.WebDocletTask”
          classpathref=”xdoclet.lib.path” />
    </target>
    现在,任务声明好了,XDoclet“整装待发”。

    2.3.2 使用任务
    你可以在任何目标里使用声明好的任务。在任务的上下文环境里,可以调动相关的子任务。让我们看一个例子,这个例子调用了<ejbdoclet>任务。不要担心看不懂语法的细节,现在你只需要关心一些基础概念就可以了。
    <target name=”generateEjb” depends=”init”>
      <ejbdoclet destdir=”${gen.src.dir}”>
        <fileset dir=”${src.dir}”>
          <include name=”**/*Bean.java”/>
        </fileset>
      <deploymentdescriptor destdir=”${ejb.deployment.dir}”/>
      <homeinterface/>
      <remoteinterface/>
      <localinterface/>
       <localhomeinterface/>
      </ejbdoclet>
    </target>
    把任务想像成一个子程序运行时需要的一个配置环境(记住,子任务才是真正进行代码生成工作的)。当调用一个子任务时,子任务从任务继承上下文环境,当然,你也可以根据需要随意的覆盖这些值。在上面的例子里,因为<deploymentdescriptor>子任务生成的布署描述符文件和其他生成各种接口的子任务生成的Java源文件需要放在不同的位置,所以覆盖了destdir的属性值。布署描述符文件需要放在一个在打包EJB JAR文件的时候可以容易包含进来的地方,而生成的Java代码则需要放置在一个可以调用Java编译器进行编译的地方。需要这些子任务之间是紧密关联的,但只要你需要,你可以有足够的自主权控制任务的生成环境。
    <fileset>属性同样被应用到所有的子任务。这是一个Ant的复杂类型(相对于文本和数值的简单类型),所以以子元素的方式在任务中声明。不要把它和子任务混为一谈。当然,如果你想在某个子任务中另外指定一个不同的输入文件集,你也可以在这个子任务中放置一个<fileset>子元素来覆盖它。
    子任务的可配置选项远远不止这些。我们会在下一章继续介绍所有的任务和子任务,以及常用的配置选项。

    2.4 用属性标注你的代码
    可重用的代码生成系统需要输入来生成感兴趣的输出。一个解析器生成器也需要一个语言描述来解析生成解析器。一个商务对象代码生成器需要领域模型来知道要生成哪些商务对象。XDoclet则需要Java源文件做为输出来生成相关的类或者布署/配置文件。
    然而,源文件可能并没有提供代码生成所需要的所有信息。考虑一个基于servlet的应用,当你想生成web.xml文件的时候,servlet源文件仅可以提供类名和适当的servlet接口方法。其他的信息比如URI pattern映射、servlet需要的初始化参数等信息并没有涵盖。显而易见,如果class并没有提供这些信息给你,你就需要自己手动在web.xml文件时填写这些信息。
    XDoclet当然也不会知道这些信息。幸运的是,解决方法很简单。如果所需信息在源文件时没有提供,那就提供它,做法就是在源文件里加入一些XDoclet属性。XDoclet解析源文件,提取这些属性,并把它们传递给模板,模板使用这些信息生成代码。

    2.4.1 剖析属性
    XDoclet属性其实就是javadoc的扩展。它们在外表上和使用上都有javadoc属性一样,可以放置在javadoc文档注释里。文档注释以/**开始,*/结尾。下面是一个简单的例子:
    /**
    * 这是一段javadoc注释。
    * 注释可以被分解成多行,每一行都以星号开始。
    */
    在注释里的所有文本都被视为javadoc注释,并且都能够被XDoclet访问到。注释块一般都与Java源文件中的某个实体有关,并紧跟在这个实体的前面。没有紧跟实体的注释块将不会被处理。类(或者接口)可以有注释块,方法和域也可以有自己的注释块,比如:
    /**
    * 类注释块
    */
    public class SomeClass {
      /** 域注释块 */
      private int id;
      /**
    * 构造函数注释块
    */
      public SomeClass() {
        // …
      }
      /**
       * 方法注释块
       */
      public int getId() {
        return id;
      }
    }
    注释块分成两部分:描述部分和标签部分。当遇到第一个javadoc标签时,标签部分开始。Javadoc标签也分成两部分:标签名和标签描述。标签描述是可选的,并且可以多行。例如:
    /**
    * 这是描述部分
    * @tag1 标签部分从这里开始
    * @tag2
    * @tag3 前面一个标签没有标签描述。
    * 这个标签有多行标签描述。
    */
    XDoclet使用参数化标签扩展了javadoc标签。在XDoclet里,你可以在javadoc标签的标签描述部分加入name=”value”参数。这个微小的改动大大增强了javadoc标签的表达能力,使得javadoc标签可以用来描述复杂的元数据。下面的代码显示了使用XDoclet属性描述实体Bean方法:
    /**
    * @ejb.interface-method
    * @ejb.relation
    * name=”blog-entries”
    * role-name=”blog-has-entries”
    * @ejb.value-object
    * compose=”com.xdocletbook.blog.value.EntryValue”
    * compose-name=”Entry”
    * members=”com.xdocletbook.blog.interfaces.EntryLocal”
    * members-name=”Entries”
    * relation=”external”
    * type=”java.util.Set”
    */
    public abstract Set getEntries();
    参数化标签允许组合逻辑上相关联的属性。你可以加入描述这个类的元信息,使得这个类的信息足够生成代码。另外,程序员借由阅读这样的元信息,可以很快的理解这个类是如何使用的。(如果这个例子上的元信息你看不懂,不要担心,在第4章里,我们会学到EJB相关的标签以及它们的涵意。)
    另外,请注意上面的例子中,所有的标签名都以ejb开头。XDoclet使用namespace.tagname的方式给标签提供了一个命名空间。这样做除了可以跟javadoc区别开来以外,还可以把任务相关的标签组织起来,以免任务之间的标签产生混淆。 

    2.5 代码生成模式
    XDoclet是一种基于模板的代码生成引擎。从高层视图上来看,输出文件其实就是由解析执行各式各样的模板生成出来的。如果你理解了模板以及它所执行的上下文环境,就可以确切的认识到,XDoclet可以生成什么,不可以生成什么。如果你正在评估XDoclet平台,理解这些概念是非常重要的。要不然,你可能会错过XDoclet的许多强大的功能,也可能会被XDoclet的一些限制感到迷惑。
    XDoclet运行在在Ant构建文件环境中,它提供了Ant自定义任务和子任务来与XDoclet引擎交互。任务是子任务的容器,子任务负责执行代码生成。子任务调用模板。模板提供了你将生成代码的饼干模子。XDoclet解析输入的源文件,提取出源文件中的XDoclet属性元数据,再把这些数据提供给模板,驱动模板执行。除此之外,模板还可以提供合并点(merge points),允许用户插入一些模板片断(合并文件merge files)来根据需要定制代码生成。
    2.5.1 模板基础
    XDoclet使用代码模板来生成代码。模板(template)是你想生成文件的原型。模板里使用一些XML标签来指导模板引擎如何根据输入类以及它们的元数据来调整代码的生成。
    [定义:模板(template)是生成代码或描述文件的抽象模视图。当模板被解析的时候,指定的细节信息会被填入。]
    模板一般情况下会有一个执行环境。模板可能应用在一个类环境(转换生成transform generation),也有可能应用在一个全局环境(聚集生成aggregate generation)。转换生成和聚集生成是XDoclet的两种类型的任务模式,理解它们之间的区别对于理解XDoclet是非常重要的。
    当你使用XDoclet生成布置描述符文件时,你使用的是聚集生成。布置描述符文件并不仅仅只与一个类相关,相反,它需要从多个类里聚集信息到一个输入文件。在这种生成模式里,解析一次模板只会生成一个输出文件,不管有多少个输入文件。
    在转换生成模式里,模板遇到每一个源文件就会解析一次,根据该文件类的上下文环境生成输出。这种生成模式会为每一个输入文件生成一个输出文件。
    转换生成模式的一个很好的例子是生成EJB的local和remote接口。显然,接口是和Bean类一一相关的。从每一个类里提取信息(类以及它的方法、域、接口以及XDoclet属性等信息)转换出接口。除此以外,不需要其他的信息。
    从实现里提取出接口似乎有点反向。如果你手写程序的话,一般来说会先定义一个接口,然后再写一个类来关现它。但XDoclet做不到,XDoclet不可能帮你实现一个已有接口,因为它不可能帮你生成你的业务逻辑。当然,如果业务逻辑可以从接口本身得到(比如JavaBean的get/set访问器)或者使用XDoclet属性声明好,那么生成业务逻辑代码来实现一个接口也不是不可能。但一般情况下,这样做不太现实。相比而言,提供一个实现,并描述接口与这个实现之间的关联就容易多了。
    聚集生成和转换生成主要区别在它们的环境信息上。即使一个代码生成任务中生成一个Java文件,一般也不常用聚集生成,因为生成一个Java类还需要一些重要信息如类所处的包以及你想生成的类名,在这种环境下是无法提供的。如果一定要使用聚集生成的话,那就需要在另一个单独的地方提供好配置信息了。
    2.5.2 模板标签
    在还没见到模板长啥样子之前,我们已经比较深入的认识它了。那模板文件究竟长啥样子呢?它有点像JSP文件。它们都包含文件和XML标签,生成输出文件时XML标签会被解析,然后生成文本并显示在XML标签所处的位置上。除了以XDt为命名空间打头的XML标签会被XDoclet引擎解析以外,其余的XML标签XDoclet会忽略不管。下面的代码片断显示了XDoclet模板的“经典造型”:
      public class
        <XDtClass:classOf><XDtEjbFacade:remoteFacadeClass/></XDtClass:classOf>
        Extends Observabe {
        static <XDtClass:classOf><XDtEjbFacade:remoteFacadeClass/></XDtClass:classOf>
          _instance = null;
        public static <XDtClass:classOf><XDtEjbFacade:remoteFacadeClass/></XDtClassOf>
          getInstance() {
          if (_instance == null) {
            _instance =
    new <XDtClass:classOf><XDtEjbFacade:remoteFacadeClass/>
              </XDtClass:classOf>();
          }
          return _instance;
    }
    }
    研究一下这个模板,你会发现,它生成的是一个类定义。这个类里定义了一个静态变量instance,并且使用一个静态方法来控制这个静态文件的访问。借助Java语法,你可以很容易的推断出那些XDoclet模板标签的目录是生成类名,虽然对于这个标签如何工作你还并不是很了解。
    即使你从没打算过要自己写模板,但理解模板是如何被解析运行的还是很有必要的。迟早你会调用到一个运行失败的XDoclet任务,没有产生你所期望的输出,那么最快捷的找出原因的方法就是直接检查模板文件,看看是哪里出了问题。
    让我们看一下生成静态域定义的片断:
    static <XDtClass:classOf><XDtEjbFacade:remoteFacadeClass/></XDtClass:classOf>
        _instance = null;
    在XDoclet的眼里,这段模板代码很简单,那就是:
      static <tag/> _instance = null;
    XDoclet解析执行标签,如果有输出的话,把输入置回到文本里去。有些标签会执行一些运算,把输出放回到一个流里。这样的标签称之为内容标签(content tags),因为它们产生内容。
    另一种类型的标签称之为BODY标签。BODY标签在开始和结束标签之间存在文本。而BODY标签强大就强大在这些文本自己也可以是一断可以由外围标签解析的模板片断。比如在上面的例子里,XDtClass:classOf标签,它们的内容就是模板片断:
      <XDtEjbFacade:remoteFacadeClass/>
    classOf标签解析这段模板,提取出全限制的内容,然后剃除前面的包面,只输出类名。BODY标签并不总是会解析它的内容,在做这件事之前,它们会事先检查一些外部判断条件(比如检查检查你正在生成的是一个接口还是一个类)。这里标标签称之为条件标签(conditional tags)。还有一些BODY标签提供类似迭代的功能,它的内容会被解析多次。比如一个标签针对类里的每一个方法解析一次内容。
    XDoclet标签提供了许多高层次的代码生成功能,但是有时候,它们可能显得不够灵活,或者表达能力满足不了你的需要。这时候,相对于另外开发一套通用功能的模板引擎相比,你可以选择扩展XDoclet模板引擎。你可以使用更具表述能力、功能更加强大的Java平台开发你自己的一套标签。 

    2.6 使用合并定制
    代码生成系统之所以使用的不多,主要原因就在于它们往往只能生成一些死板的、不够灵活的代码。大多数代码生成系统不允许你改动它们生成的代码;如果,如果这个系统不够灵活,你所能做到的最好的扩展就是应用继承扩展生成的代码,或者使用一些共通的设计模式(比如Proxy和Adaptor)来满足你的需要。无论如此,这都不是产生你想生成的代码的好办法。代码生成器最好能做到所生成即所得WYGIWYG(what you generate is what you get),来取代你需要花费大量的时间来粉饰生成出来的并不满足要求的代码。所以,对于代码生成器来说,支持灵活的定制,是生成能够完全满足要求的代码的前提条件。
    XDoclet通过合并点(merge points)支持定制??合并点是在模板文件定义里允许运行时插入定制代码的地方。有时候,合并点甚至可以影响到全局代码的生成,不但允许你添加一些定制内容,还可以从根本上改变将要生成出来的东西。
    [定义:合并点(Merge points)是模板预先定义的允许你在代码生成的运行时加入定制内容的扩展点]
    让我们研究一段从XDoclet源代码里摘取出来的模板代码。在为实体Bean生成主键的模板末尾,定义了这样一个合并点:
      <XDtMerge:merge file=”entitypk-custom.xdt”></XDtMerge:merge>
    如果你在你的merge目录下创建了一个名为entitypk-custom.xdt文件,那么这个模板文件的内容将会在这个合并点被包含进来。你的定制可以执行高层模板可以执行的所有强大功能,可以进行所有模板可以进行的运算(包括定义自定义标签,定义它们自己的合并点)。
    上面的这种合并点,为所有的类环境使用了同一个文件。当然,也可以为每一个类环境使用不同的合并文件。如果你不想定制全部的类文件,或者你不想为了某些改动而重写模板的时候,这会很有用。不管动机是什么,逐类的合并点很容易识别出来:他们会在名字里包含一个XDoclet的逐类标记{0}。这里有一个生成ejb-jar.xml文件里的安全角色引用的例子:
      <XDtMerge:merge file=”ejb-sec-rolerefs-{0}.xml”>
        <XDtClass:forAllClassTags tagName=”ejb:security-role-ref”>
          <security-role-ref>
            <role-name>
    <XDtClass:classTagValue
    tagName=”ejb:security-roleref”
    paramName=”role-name”/>
            </role-name>
            <role-link>
              <XDtClass:classTagValue
                tagName=”ejb:security-roleref”
                paramName=”role-link”/>
            </role-link>
          </security-role-ref>
        </XDtClass:forAllClassTags>
      </XDtMerge:merge>
    这段模板会遍历工程里的所有Bean。对于每一个Bean,XDoclet先从Bean的文件名里提取出Bean名,然后替换{0},再根据替换后的文件名去寻找合并文件。例如,如果你有一个名为BlogFacadeBean的Bean,XDoclet会尝试寻找一个名为ejb-src-rolerefs-BlogFacade.xml的合并文件。
    如果找不到这个合并文件,则这个<merge>标签的内容模板会被解析。这意味着合并点不仅可以提供定制内容,还可以在一个模板文件里定义一个替换点,当定制内容不存在的时候使用替换点里的内容。不是所有的XDoclet任务都提供了有替换内容的合并点,一般来说,它们更倾向于只提供一个简单的合并点,仅仅当合并文件存在的时候解析并导入合并文件的内容。这取决于任务的开发者觉得哪种合并点更符合他的要求。
    还有一点没有介绍到的是XDoclet如何定位合并文件,每一个XDoclet任务或者子任务都会提供一个mergeDir属性,这个属性用于设置你存放合并文件的目录。