2005年11月24日

转载,原文出处无法考证。

  我是一个硬盘.
  在一个普普通通的台式机里工作。别人总认为我们是高科技白领,工作又干净又体面,似乎风光得很。也许他们是因为看到洁白漂亮的机箱才有这样的错觉吧。其实象我们这样的小台式机,工作环境狭迫,里面的灰尘吓得死人。每天生活死水一潭,工作机械重复。跑跑文字处理看看电影还凑活,真要遇到什么大软件和游戏,上上下下就要忙的团团转,最后还常常要死机。

  我们这一行技术变化快,差不多每过两三年就要升级换代,所以人人都很有压力而且没有安全感。每个新板卡来的时候都神采飞扬踌躇满志,几年光阴一过,就变得灰头土脸意志消沉。机箱里的人都很羡慕能去别的机器工作。特别是去那些笔记本,经常可以出差飞来飞去,住五星级的酒店,还不用干重活,运行运行word,上网聊聊天就行了。

  但我更喜欢去那些大服务器,在特别干净明亮的机房里工作。虽然工作时间长点,但是福利好,24小时不间断电ups,而且还有阵列,热插拔,几个人做一个人的事情,多轻松啊。而且也很有面子,只运行关键应用,不像我们这里,什么乱七八糟的事情都要做。不过我知道,那些硬盘都很厉害,不是SCSI,就是SCSI II,Fibrechannel,象我这样IDE的,能混到工作站就算很不错了。   

  我常常想,当年在工厂里,如果我努力一下会不会也成了一个SCSI?或者至少做一个笔记本硬盘。但我又会想,也许这些都是命运,不过我从不抱怨。内存就常常抱怨,抱怨他们主板部门的复杂,抱怨他如何跟新来的杂牌内存不兼容,网卡和电视卡又是如何的冲突。

  我的朋友不多,内存算一个。他很瘦的而我很胖,他动作很快,而我总是很慢。我们是一起来这台机器的,他总是不停地说,而我只是听,我从来不说。

  内存的头脑很简单,虽然英文名字叫Memory,可是他什么Memory都不会有,天大的事睡一觉就能忘个精光。我不说,但我会记得所有的细节。他说我这样忧郁的人不适合作技术活,迟早要精神分裂。我笑笑,因为我相信自己的容量。

  有时候我也很喜欢这份工作,简单,既不用象显示器那样一天到晚被老板盯着,也不用象光驱那样对付外面的光碟。只要和文件打交道就行了,无非是读读写写,很单纯安静的生活。直到有一天……

  我至今还记得那渐渐掀起的机箱的盖子,从缺口伸进来的光柱越来越宽,也越来越亮。空气里弥漫着跳动的颗粒。那个时候,我看到了她。她是那么的纤细瘦弱,银白的外壳一闪一闪的。浑身上下的做工都很精致光洁,让我不禁惭愧自己的粗笨。等到数据线把我们连在一起,我才缓过神来。开机的那一刹那,我感到了电流和平时的不同。后来内存曾经笑话我,说我们这里只要有新人来,电流都会不同的,上次新内存来也是这样。我觉得他是胡扯。我尽量的保持镇定,显出一副很专业的样子,只是淡淡的向她问好并介绍工作环境。慢慢的,我知道了,她,IBM-DJSA220,是一个笔记本硬盘,在老板朋友的笔记本里做事。这次来是为了复制一些文件。我们聊得很开心。她告诉我很多旅行的趣闻,告诉我坐飞机是怎么样的,坐汽车的颠簸又是如何的不同,给我看很多漂亮的照片、游记,还有一次她从桌子上掉下来的历险故事。而我则卖弄各种网上下载来的故事和笑话。

  她笑得很开心。

  而我很惊讶自己可以说个不停。

  一个早晨,开机后我看到数据线上空荡荡的插口。她一共呆了7天。后来,我再也没有见过她。我有点后悔没有交换电子邮件,也没能和她道别。不忙的时候,我会一个人怀念伸进机箱的那股阳光。

  我不知道记忆这个词是什么意思,我有的只是她留下的许多文件。我把它们排的整整齐齐,放在我最常经过的地方。每次磁头从它们身上掠过,我都会感到一丝淡淡的惬意。

  但我没有想到老板会要我删除这些文件。我想争辩还有足够的空间,但毫无用处。于是,平生第一次违背命令,我偷偷修改了文件分配表。然后把他们都藏到了一个秘密的地方,再把那里标志成坏扇区。不会有人来过问坏扇区。而那里,就成了我唯一的秘密,我常常去看他们,虽然从不作停留。

  日子一天一天的重复,读取写入,读取写入……我以为永远都会这样继续下去,直到一天,老板要装xp却发现没有足够的空间。他发现了问题,想去修复那些坏扇区。我拒绝了。很快,我接到了新命令:格式化。

  我犹豫了很久 ……………………

  track 0 bad,disk unusable

  

  我是一条内存.

  我在一台台式电脑里工作,但是我记不得我是从哪里来的,是什么牌子,因为我健忘。我的上司是cpu大哥,他是我们的老大。都说他是电脑的脑子,可是我看他的脑子实在是太小了,比我还要健忘。每天他总是不停的问我,某某页某某地址存的是什么?我总是不厌其烦的告诉他,可是不出一秒钟他又忘记了,又要问一遍,一次我说大哥你烦不烦,你就不能记住点有用的东西?他说“内存兄弟,我有苦衷啊,每天都在不停地做题,头晕眼花的,我也难啊。”

  其实我不愿意跟他计较,因为他脑子小,思维也很简单。虽然说他是我的上司,可是每次睡觉醒来,他连要干什么都不记得了,总是急急忙忙地找BIOS兄弟,“嘿,哥们,今天干什么来着”。bios总是很不耐烦地把每天必做的工作说一遍,然后就去睡觉了。接下来就轮到我和C哥瞎忙了。


  在机箱里的兄弟中,我最喜欢硬盘。他脑子大,记得东西多,而且记得牢。他说话的速度很慢,而且很少说错,这说明他很有深度,我这么感觉。CPU也这么想,不过他很笨,每次都忘了硬盘是谁。开机自检的时候总要问:“嘿,那家伙是谁?”

  “ST!”我总要重复一遍。

  硬盘很喜欢忧郁,我觉得像他这样忧郁的人不适合做技术活,迟早会精神分裂的,但是他不信。

  其实睡着的时候我总是把几乎所有的东西都忘记掉,但是我从来都不会忘记朋友。有一块地方叫做CMOS,那是我记忆的最深处,保存着硬盘、光驱的名字。有些东西应该很快忘掉,而有些东西应该永远记得。我在梦中总是这么想着。

  BIOS是一个很奇怪的家伙,他老是睡觉,但是却总是第一个醒过来。让我们自检,启动,然后接着睡觉。我知道如果我在CMOS里头把BIOS Shadow选项去掉,他就睡不成了,但是看着他晕晕乎乎的样子,也就不忍心这么做了。他对人总是爱搭不理,没有什么人了解他。但是这次硬盘恋爱的事,却使我重新认识了他。

  那是很久以前的事了,机箱里似乎来过一块笔记本硬盘,很可爱,说实话我也喜欢她。不过现在除了记得他可爱,别的都忘记了。这就是我比硬盘幸运的地方,我把所有应该忘记的都忘记了,但是他却什么都记得。

  自从笔记本硬盘走了之后,硬盘就变得很不正常。每次他的磁头经过一些地方的时候,我们都能感觉到电流很不正常。
  “硬盘这是怎么了?”我问CPU。
  “谁是硬盘?”
  我就知道和CPU没有办法交流,倒是bios没好气地说:“那个傻瓜恋爱了”。我不知道什么是恋爱,因为我记不住东西,似乎有一些人或者事在我生命中留下过痕迹,但是我都轻率地把他们忘记了。

  BIOS对我说:“对你来说记忆太容易了,所以你遗忘得更快,生命中能够永刻的记忆都带着痛楚。”我不懂,但是我知道BIOS曾经被刷写过,那时他很痛,像要死了一样。我的记忆是轻浮的,不像他们……我很羡慕他们,因为他们拥有回忆,而我们有,从此我也学会了忧郁,因为我在CMOS里面写下了“忧郁”两个字。

  硬盘一天比一天不对劲,终于有一天,CPU对问说:“下条指令是什么来着?”
  我一看,吓了一跳:“format”
  “是什么?”CPU很兴奋,这个没脑子的家伙。
  我还是告诉了他。我不知为什么这么做。
  硬盘犹豫了很久,终于说了一句 Track 0 bad,Disk unusable。
  电停了,很久很久,我在黑暗中数着时钟……

  一个月后硬盘回来了,也许最后的挣扎也没有使他摆残酷的命运,他被低格了。他什么也不记得了,如同一个婴儿,我们很难过,但是这未必不是一件好事,他以后不用痛苦了。

  为了恢复数据,笔记本硬盘回来了。“Hi,ST”,她说,“你不认识我了?”
  硬盘没有说话,似乎低格对他的伤害很大。
  过了一会,他说:“对不起,好像我们没有见过吧……”。
  笔记本硬盘显得很伤心,我能感觉到她带泪的电流。“想不到连你也这么健忘”。
  “哦……”。硬盘没有回答。

  我很难过,笔记本硬盘的心里依然记着他,他却把一切都忘了,而那正是他最不希望忘却的。究竟是幸运,还是痛苦,我说不上来,只是觉得造化弄人,有一种淡淡的悲凉。
  这时从BIOS传来一阵奇怪的电流,我感觉到硬盘的表情在变化,由漠然到兴奋,由兴奋到哀伤,由哀伤到狂喜……
  “IBM,你回来了……”。
  ……
  后来BIOS对我说,其实他并没有睡觉,自从硬盘把那些文件藏起来以后,他就料到会有这样的结局,于是偷偷地把其中一些文件放到了备份里。
  “幸好我是DUAL BIOS,虽然藏得不多,还足够让他想起来……”。
  我想BIOS保存这些东西的时候一定很疼,当我问他“为什么这么做”时,BIOS轻描淡写的说:“呵呵,我们是朋友嘛”。
  嗯,朋友,永远的朋友……

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月09日

原文地址:http://www.irunnet.com/viewtopic.php?p=913&sid=4f05f5b8a26e7d0b0e2586190c175d0b#913
GFS是一个可扩展的分布式文件系统,用于大型的、分布式的、对大量数据进行访问的应用。它运行于廉价的普通硬件上,但可以提供容错功能。它可以给大量的用户提供总体性能较高的服务。
1、设计概览
(1)设计想定
GFS与过去的分布式文件系统有很多相同的目标,但GFS的设计受到了当前及预期的应用方面的工作量及技术环境的驱动,这反映了它与早期的文件系统明显不同的设想。这就需要对传统的选择进行重新检验并进行完全不同的设计观点的探索。
GFS与以往的文件系统的不同的观点如下:
1、部件错误不再被当作异常,而是将其作为常见的情况加以处理。因为文件系统由成百上千个用于存储的机器构成,而这些机器是由廉价的普通部件组成并被大量的客户机访问。部件的数量和质量使得一些机器随时都有可能无法工作并且有一部分还可能无法恢复。所以实时地监控、错误检测、容错、自动恢复对系统来说必不可少。
2、按照传统的标准,文件都非常大。长度达几个GB的文件是很平常的。每个文件通常包含很多应用对象。当经常要处理快速增长的、包含数以万计的对象、长度达TB的数据集时,我们很难管理成千上万的KB规模的文件块,即使底层文件系统提供支持。因此,设计中操作的参数、块的大小必须要重新考虑。对大型的文件的管理一定要能做到高效,对小型的文件也必须支持,但不必优化。
3、大部分文件的更新是通过添加新数据完成的,而不是改变已存在的数据。在一个文件中随机的操作在实践中几乎不存在。一旦写完,文件就只可读,很多数据都有这些特性。一些数据可能组成一个大仓库以供数据分析程序扫描。有些是运行中的程序连续产生的数据流。有些是档案性质的数据,有些是在某个机器上产生、在另外一个机器上处理的中间数据。由于这些对大型文件的访问方式,添加操作成为性能优化和原子性保证的焦点。而在客户机中缓存数据块则失去了吸引力。
4、工作量主要由两种读操作构成:对大量数据的流方式的读操作和对少量数据的随机方式的读操作。在前一种读操作中,可能要读几百KB,通常达 1MB和更多。来自同一个客户的连续操作通常会读文件的一个连续的区域。随机的读操作通常在一个随机的偏移处读几个KB。性能敏感的应用程序通常将对少量数据的读操作进行分类并进行批处理以使得读操作稳定地向前推进,而不要让它来来回回的读。
5、工作量还包含许多对大量数据进行的、连续的、向文件添加数据的写操作。所写的数据的规模和读相似。一旦写完,文件很少改动。在随机位置对少量数据的写操作也支持,但不必非常高效。
6、系统必须高效地实现定义完好的大量客户同时向同一个文件的添加操作的语义。
(2)系统接口
GFS提供了一个相似地文件系统界面,虽然它没有向POSIX那样实现标准的API。文件在目录中按层次组织起来并由路径名标识。
(3)体系结构:
一个GFS集群由一个master和大量的chunkserver构成,并被许多客户(Client)访问。如图1所示。Master和 chunkserver通常是运行用户层服务进程的Linux机器。只要资源和可靠性允许,chunkserver和client可以运行在同一个机器上。
文件被分成固定大小的块。每个块由一个不变的、全局唯一的64位的chunk-handle标识,chunk-handle是在块创建时由 master分配的。ChunkServer将块当作Linux文件存储在本地磁盘并可以读和写由chunk-handle和位区间指定的数据。出于可靠性考虑,每一个块被复制到多个chunkserver上。默认情况下,保存3个副本,但这可以由用户指定。
Master维护文件系统所以的元数据(metadata),包括名字空间、访问控制信息、从文件到块的映射以及块的当前位置。它也控制系统范围的活动,如块租约(lease)管理,孤儿块的垃圾收集,chunkserver间的块迁移。Master定期通过HeartBeat消息与每一个 chunkserver通信,给chunkserver传递指令并收集它的状态。
与每个应用相联的GFS客户代码实现了文件系统的API并与master和chunkserver通信以代表应用程序读和写数据。客户与master的交换只限于对元数据(metadata)的操作,所有数据方面的通信都直接和chunkserver联系。
客户和chunkserver都不缓存文件数据。因为用户缓存的益处微乎其微,这是由于数据太多或工作集太大而无法缓存。不缓存数据简化了客户程序和整个系统,因为不必考虑缓存的一致性问题。但用户缓存元数据(metadata)。Chunkserver也不必缓存文件,因为块时作为本地文件存储的。
(4)单master。
只有一个master也极大的简化了设计并使得master可以根据全局情况作出先进的块放置和复制决定。但是我们必须要将master对读和写的参与减至最少,这样它才不会成为系统的瓶颈。Client从来不会从master读和写文件数据。Client只是询问master它应该和哪个 chunkserver联系。Client在一段限定的时间内将这些信息缓存,在后续的操作中Client直接和chunkserver交互。
以图1解释一下一个简单的读操作的交互。
1、client使用固定的块大小将应用程序指定的文件名和字节偏移转换成文件的一个块索引(chunk index)。
2、给master发送一个包含文件名和块索引的请求。
3、master回应对应的chunk handle和副本的位置(多个副本)。
4、client以文件名和块索引为键缓存这些信息。(handle和副本的位置)。
5、Client 向其中一个副本发送一个请求,很可能是最近的一个副本。请求指定了chunk handle(chunkserver以chunk handle标识chunk)和块内的一个字节区间。
6、除非缓存的信息不再有效(cache for a limited time)或文件被重新打开,否则以后对同一个块的读操作不再需要client和master间的交互。
通常Client可以在一个请求中询问多个chunk的地址,而master也可以很快回应这些请求。
(5)块规模:
块规模是设计中的一个关键参数。我们选择的是64MB,这比一般的文件系统的块规模要大的多。每个块的副本作为一个普通的Linux文件存储,在需要的时候可以扩展。
块规模较大的好处有:
1、减少client和master之间的交互。因为读写同一个块只是要在开始时向master请求块位置信息。对于读写大型文件这种减少尤为重要。即使对于访问少量数据的随机读操作也可以很方便的为一个规模达几个TB的工作集缓缓存块位置信息。
2、Client在一个给定的块上很可能执行多个操作,和一个chunkserver保持较长时间的TCP连接可以减少网络负载。
3、这减少了master上保存的元数据(metadata)的规模,从而使得可以将metadata放在内存中。这又会带来一些别的好处。
不利的一面:
一个小文件可能只包含一个块,如果很多Client访问改文件的话,存储这些块的chunkserver将成为访问的热点。但在实际应用中,应用程序通常顺序地读包含多个块的文件,所以这不是一个主要问题。
(6)元数据(metadata):
master存储了三中类型的metadata:文件的名字空间和块的名字空间,从文件到块的映射,块的副本的位置。所有的metadata都放在内存中。前两种类型的metadata通过向操作日志登记修改而保持不变,操作日志存储在master的本地磁盘并在几个远程机器上留有副本。使用日志使得我们可以很简单地、可靠地更新master的状态,即使在master崩溃的情况下也不会有不一致的问题。相反,mater在每次启动以及当有 chuankserver加入的时候询问每个chunkserver的所拥有的块的情况。
A、内存数据结构:
因为metadata存储在内存中,所以master的操作很快。进一步,master可以轻易而且高效地定期在后台扫描它的整个状态。这种定期地扫描被用于实现块垃圾收集、chunkserver出现故障时的副本复制、为平衡负载和磁盘空间而进行的块迁移。
这种方法的一个潜在的问题就是块的数量也即整个系统的容量是否受限与master的内存。实际上,这并不是一个严重的问题。Master为每个 64MB的块维护的metadata不足64个字节。除了最后一块,文件所有的块都是满的。类似的,每个文件的名字空间数据也不足64个字节,因为文件名是以一种事先确定的压缩方式存储的.如果要支持更大的文件系统,那么增加一些内存的方法对于我们将元数据(metadata)保存在内存种所获得的简单性、可靠性、高性能和灵活性来说,这只是一个很小的代价。
B、块位置:
master并不为chunkserver所拥有的块的副本的保存一个不变的记录。它在启动时通过简单的查询来获得这些信息。Master可以保持这些信息的更新,因为它控制所有块的放置并通过HeartBeat消息来监控chunkserver的状态。
这样做的好处:因为chunkserver可能加入或离开集群、改变路径名、崩溃、重启等,一个集群重有成百个server,这些事件经常发生,这种方法就排除了master与chunkserver之间的同步问题。
另一个原因是:只有chunkserver才能确定它自己到底有哪些块,由于错误,chunkserver中的一些块可能会很自然的消失,这样在master中就没有必要为此保存一个不变的记录。
C、操作日志:
操作日志包含了对metadata所作的修改的历史记录。它作为逻辑时间线定义了并发操作的执行顺序。文件、块以及它们的版本号都由它们被创建时的逻辑时间而唯一地、永久地被标识。
操作日志是如此的重要,我们必须要将它可靠地保存起来,并且只有在metadata的改变固定下来之后才将变化呈现给用户。所以我们将操作日志复制到数个远程的机器上,并且只有在将相应的日志记录写到本地和远程的磁盘上之后才回答用户的请求。
Master可以用操作日志来恢复它的文件系统的状态。为了将启动时间减至最小,日志就必须要比较小。每当日志的长度增长到超过一定的规模后,master就要检查它的状态,它可以从本地磁盘装入最近的检查点来恢复状态。
创建一个检查点比较费时,master的内部状态是以一种在创建一个检查点时并不耽误即将到来的修改操作的方式来组织的。Master切换到一个新的日子文件并在一个单独的线程中创建检查点。这个新的检查点记录了切换前所有的修改。在一个有数十万文件的集群中用一分钟左右就能完成。创建完后,将它写入本地和远程的磁盘。
(7)数据完整性
名字空间的修改必须是原子性的,它们只能有master处理:名字空间锁保证了操作的原子性和正确性,而master的操作日志在全局范围内定义了这些操作的顺序。
文件区间的状态在修改之后依赖于修改的类型,不论操作成功还是失败,也不论是不是并发操作。如果不论从哪个副本上读,所有的客户都看到同样的数据,那么文件的这个区域就是一致的。如果文件的区域是一致的并且用户可以看到修改操作所写的数据,那么它就是已定义的。如果修改是在没有并发写操作的影响下完成的,那么受影响的区域是已定义的,所有的client都能看到写的内容。成功的并发写操作是未定义但却是一致的。失败的修改将使区间处于不一致的状态。
Write操作在应用程序指定的偏移处写入数据,而record append操作使得数据(记录)即使在有并发修改操作的情况下也至少原子性的被加到GFS指定的偏移处,偏移地址被返回给用户。
在一系列成功的修改操作后,最后的修改操作保证文件区域是已定义的。GFS通过对所有的副本执行同样顺序的修改操作并且使用块版本号检测过时的副本(由于chunkserver退出而导致丢失修改)来做到这一点。
因为用户缓存了会位置信息,所以在更新缓存之前有可能从一个过时的副本中读取数据。但这有缓存的截止时间和文件的重新打开而受到限制。
在修改操作成功后,部件故障仍可以是数据受到破坏。GFS通过master和chunkserver间定期的handshake,借助校验和来检测对数据的破坏。一旦检测到,就从一个有效的副本尽快重新存储。只有在GFS检测前,所有的副本都失效,这个块才会丢失。
2、系统交互
(1)租约(lease)和修改顺序:
(2)数据流
我们的目标是充分利用每个机器的网络带宽,避免网络瓶颈和延迟
为了有效的利用网络,我们将数据流和控制流分离。数据是以流水线的方式在选定的chunkerserver链上线性的传递的。每个机器的整个对外带宽都被用作传递数据。为避免瓶颈,每个机器在收到数据后,将它收到数据尽快传递给离它最近的机器。
(3)原子性的record Append:
GFS提供了一个原子性的添加操作:record append。在传统的写操作中,client指定被写数据的偏移位置,向同一个区间的并发的写操作是不连续的:区间有可能包含来自多个client的数据碎片。在record append中, client只是指定数据。GFS在其选定的偏移出将数据至少原子性的加入文件一次,并将偏移返回给client。
在分布式的应用中,不同机器上的许多client可能会同时向一个文件执行添加操作,添加操作被频繁使用。如果用传统的write操作,可能需要额外的、复杂的、开销较大的同步,例如通过分布式锁管理。在我们的工作量中,这些文件通常以多个生产者单个消费者队列的方式或包含从多个不同 client的综合结果。
Record append和前面讲的write操作的控制流差不多,只是在primary上多了一些逻辑判断。首先,client将数据发送到文件最后一块的所有副本上。然后向primary发送请求。Primary检查添加操作是否会导致该块超过最大的规模(64M)。如果这样,它将该块扩充到最大规模,并告诉其它副本做同样的事,同时通知client该操作需要在下一个块上重新尝试。如果记录满足最大规模的要求,primary就会将数据添加到它的副本上,并告诉其它的副本在在同样的偏移处写数据,最后primary向client报告写操作成功。如果在任何一个副本上record append操作失败,client将重新尝试该操作。这时候,同一个块的副本可能包含不同的数据,因为有的可能复制了全部的数据,有的可能只复制了部分。GFS不能保证所有的副本每个字节都是一样的。它只保证每个数据作为一个原子单元被写过至少一次。这个是这样得出的:操作要是成功,数据必须在所有的副本上的同样的偏移处被写过。进一步,从这以后,所有的副本至少和记录一样长,所以后续的记录将被指定到更高的偏移处或者一个不同的块上,即使另一个副本成了primary。根据一致性保证,成功的record append操作的区间是已定义的。而受到干扰的区间是不一致的。
(4)快照(snapshot)
快照操作几乎在瞬间构造一个文件和目录树的副本,同时将正在进行的其他修改操作对它的影响减至最小。
我们使用copy-on-write技术来实现snapshot。当master受到一个snapshot请求时,它首先将要snapshot的文件上块上的lease。这使得任何一个向这些块写数据的操作都必须和master交互以找到拥有lease的副本。这就给master一个创建这个块的副本的机会。
副本被撤销或终止后,master在磁盘上登记执行的操作,然后复制源文件或目录树的metadata以对它的内存状态实施登记的操作。这个新创建的snapshot文件和源文件(其metadata)指向相同的块(chunk)。
Snapshot之后,客户第一次向chunk c写的时候,它发一个请求给master以找到拥有lease的副本。Master注意到chunk c的引用记数比1大,它延迟对用户的响应,选择一个chunk handle C’,然后要求每一有chunk c的副本的chunkserver创建一个块C’。每个chunkserver在本地创建chunk C’避免了网络开销。从这以后和对别的块的操作没有什么区别。
3、MASTER操作
MASTER执行所有名字空间的操作,除此之外,他还在系统范围管理数据块的复制:决定数据块的放置方案,产生新数据块并将其备份,和其他系统范围的操作协同来确保数据备份的完整性,在所有的数据块服务器之间平衡负载并收回没有使用的存储空间。
3.1 名字空间管理和加锁
与传统文件系统不同的是,GFS没有与每个目录相关的能列出其所有文件的数据结构,它也不支持别名(unix中的硬连接或符号连接),不管是对文件或是目录。GFS的名字空间逻辑上是从文件元数据到路径名映射的一个查用表。
MASTER在执行某个操作前都要获得一系列锁,例如,它要对/d1/d2…/dn/leaf执行操作,则它必须获得/d1,/d1/d2,…, /d1/d2/…/dn的读锁,/d1/d2…/dn/leaf的读锁或写锁(其中leaf可以使文件也可以是目录)。MASTER操作的并行性和数据的一致性就是通过这些锁来实现的。
3.2 备份存储放置策略
一个GFS集群文件系统可能是多层分布的。一般情况下是成千上万个文件块服务器分布于不同的机架上,而这些文件块服务器又被分布于不同机架上的客户来访问。因此,不同机架上的两台机器之间的通信可能通过一个或多个交换机。数据块冗余配置策略要达到连个目的:最大的数据可靠性和可用性,最大的网络带宽利用率。因此,如果仅仅把数据的拷贝置于不同的机器上很难满足这两个要求,必须在不同的机架上进行数据备份。这样即使整个机架被毁或是掉线,也能确保数据的正常使用。这也使数据传输,尤其是读数据,可以充分利用带宽,访问到多个机架,而写操作,则不得不涉及到更多的机架。
3.3 产生、重复制、重平衡数据块
当MASTER产生新的数据块时,如何放置新数据块,要考虑如下几个因素:(1)尽量放置在磁盘利用率低的数据块服务器上,这样,慢慢地各服务器的磁盘利用率就会达到平衡。(2)尽量控制在一个服务器上的“新创建”的次数。(3)由于上一小节讨论的原因,我们需要把数据块放置于不同的机架上。
MASTER在可用的数据块备份低于用户设定的数目时需要进行重复制。这种情况源于多种原因:服务器不可用,数据被破坏,磁盘被破坏,或者备份数目被修改。每个被需要重复制的数据块的优先级根据以下几项确定:第一是现在的数目距目标的距离,对于能阻塞用户程序的数据块,我们也提高它的优先级。最后, MASTER按照产生数据块的原则复制数据块,并把它们放到不同的机架内的服务器上。
MASTER周期性的平衡各服务器上的负载:它检查chunk分布和负载平衡,通过这种方式来填充一个新的服务器而不是把其他的内容统统放置到它上面带来大量的写数据。数据块放置的原则与上面讨论的相同,此外,MASTER还决定那些数据块要被移除,原则上他会清除那些空闲空间低于平均值的那些服务器。
3.4 垃圾收集
在一个文件被删除之后,GFS并不立即收回磁盘空间,而是等到垃圾收集程序在文件和数据块级的的检查中收回。
当一个文件被应用程序删除之后,MASTER会立即记录下这些变化,但文件所占用的资源却不会被立即收回,而是重新给文件命了一个隐藏的名字,并附上了删除的时间戳。在MASTER定期检查名字空间时,它删除超过三天(可以设定)的隐藏的文件。在此之前,可以以一个新的名字来读文件,还可以以前的名字恢复。当隐藏的文件在名字空间中被删除以后,它在内存中的元数据即被擦除,这就有效地切断了他和所有数据块的联系。
在一个相似的定期的名字空间检查中,MASTER确认孤儿数据块(不属于任何文件)并擦除他的元数据,在和MASTER的心跳信息交换中,每个服务器报告他所拥有的数据块,MASTER返回元数据不在内存的数据块,服务器即可以删除这些数据块。
3.5 过时数据的探测
在数据更新时如果服务器停机了,那么他所保存的数据备份就会过时。对每个数据块,MASTER设置了一个版本号来区别更新过的数据块和过时的数据块。
当MASTER授权一个新的lease时,他会增加数据块的版本号并会通知更新数据备份。MASTER和备份都会记录下当前的版本号,如果一个备份当时不可用,那么他的版本号不可能提高,当ChunkServer重新启动并向MASTER报告他的数据块集时,MASTER就会发现过时的数据。
MASTER在定期的垃圾收集程序中清除过时的备份,在此以前,处于效率考虑,在各客户及英大使,他会认为根本不存在过时的数据。作为另一个安全措施, MASTER在给客户及关于数据块的应答或是另外一个读取数据的服务器数据是都会带上版本信息,在操作前客户机和服务器会验证版本信息以确保得到的是最新的数据。
4、容错和诊断
4.1 高可靠性
4.1.1 快速恢复
不管如何终止服务,MASTER和数据块服务器都会在几秒钟内恢复状态和运行。实际上,我们不对正常终止和不正常终止进行区分,服务器进程都会被切断而终止。客户机和其他的服务器会经历一个小小的中断,然后它们的特定请求超时,重新连接重启的服务器,重新请求。
4.1.2 数据块备份
如上文所讨论的,每个数据块都会被备份到放到不同机架上的不同服务器上。对不同的名字空间,用户可以设置不同的备份级别。在数据块服务器掉线或是数据被破坏时,MASTER会按照需要来复制数据块。
4.1.3 MASTER备份
为确保可靠性,MASTER的状态、操作记录和检查点都在多台机器上进行了备份。一个操作只有在数据块服务器硬盘上刷新并被记录在MASTER和其备份的上之后才算是成功的。如果MASTER或是硬盘失败,系统监视器会发现并通过改变域名启动它的一个备份机,而客户机则仅仅是使用规范的名称来访问,并不会发现MASTER的改变。
4.2 数据完整性
每个数据块服务器都利用校验和来检验存储数据的完整性。原因:每个服务器随时都有发生崩溃的可能性,并且在两个服务器间比较数据块也是不现实的,同时,在两台服务器间拷贝数据并不能保证数据的一致性。
每个Chunk按64kB的大小分成块,每个块有32位的校验和,校验和和日志存储在一起,和用户数据分开。
在读数据时,服务器首先检查与被读内容相关部分的校验和,因此,服务器不会传播错误的数据。如果所检查的内容和校验和不符,服务器就会给数据请求者返回一个错误的信息,并把这个情况报告给MASTER。客户机就会读其他的服务器来获取数据,而MASTER则会从其他的拷贝来复制数据,等到一个新的拷贝完成时,MASTER就会通知报告错误的服务器删除出错的数据块。
附加写数据时的校验和计算优化了,因为这是主要的写操作。我们只是更新增加部分的校验和,即使末尾部分的校验和数据已被损坏而我们没有检查出来,新的校验和与数据会不相符,这种冲突在下次使用时将会被检查出来。
相反,如果是覆盖现有数据的写,在写以前,我们必须检查第一和最后一个数据块,然后才能执行写操作,最后计算和记录校验和。如果我们在覆盖以前不先检查首位数据块,计算出的校验和则会因为没被覆盖的数据而产生错误。
在空闲时间,服务器会检查不活跃的数据块的校验和,这样可以检查出不经常读的数据的错误。一旦错误被检查出来,服务器会拷贝一个正确的数据块来代替错误的。
4.3 诊断工具
广泛而细致的诊断日志以微小的代价换取了在问题隔离、诊断、性能分析方面起到了重大的作用。GFS服务器用日志来记录显著的事件(例如服务器停机和启动)和远程的应答。远程日志记录机器之间的请求和应答,通过收集不同机器上的日志记录,并对它们进行分析恢复,我们可以完整地重现活动的场景,并用此来进行错误分析。
6 测量
6.1 测试环境
一台主控机,两台主控机备份,16台数据块服务器,16台客户机。
每台机器:2块PIII1.4G处理器,2G内存,2块80G5400rpm的硬盘,1块100Mbps全双工网卡
19台服务器连接到一个HP2524交换机上,16台客户机俩接到领外一台交换机上,两台交换机通过1G的链路相连

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属性,这个属性用于设置你存放合并文件的目录。 

            仅供参考,请于下载后24小时内删除!