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小时内删除!

    2005年08月16日

     

    Dialogue (1)

    A: Good morning, Mr. Smith 早上好,史密斯先生

    B: Hello, Mr. Lie. It’s good to see you. How are you? 你好,李先生。 见到你太好了, 你好吗?

    A: Just fine, thanks. And you? 还好,谢谢。 你呢?

    B: I’m doing very well, thank you. 我一切都好, 谢谢。

    A: That’s good. See you later. 那好,回头见。

    B: All right. Bye now. 好,再见。

    Dialogue (2)

    A: I don’t think we’ve met. My name is Tim. 我想我们以前没见过面,我叫提姆

    B: Hello. I’m Scot. Nice to see you. 你好,我叫司考特,非常高兴见到你。

    A: Nice meeting you, too. Where are you from? 见到你我也很高兴。你是哪国人?

    B: I’m from the United States. How about you? 我是美国人,你呢?

    A: Canada. 我是加拿大人。

    我是加拿大人。

    Dialogue (3)

    A: Say, Grace, how are you doing? 我说,格雷司,你怎么样?

    B: Mike! How are you? 迈克!你好吗?

    A: Not bad. Where are you going? 不错。你去哪?

    B: To a McDonald’s. How about you? 去马克唐纳快餐厅,你呢?

    A: Oh, I just go off work. Boy, I’m so hungry I could eat a horse. 哦,我刚刚下班。我饿得能吃下一匹马!

    B: Where are you working now? 你在哪里工作?

    A: J&L Steel. It’s a rail pain. But I guess I shouldn’t complain. Lots of people are out of work these days. 杰爱尔钢厂。这的确是份苦活,但是我想我不该抱怨。现在许多人都失业。

    B: Yeah, that’s truth. Well, I’d better let you go and get some food. 对,这是事实。那么,我最好是让你去吃点东西。

    A: It was great seeing you again. Maybe we could get together sometime. 见到你可真好,也许咱们什么时候再聚一聚。

    B: Sounds good. I’ll give you a call. 好主意。我给你打电话。

    A: Ok. Great. Well, I’ll be seeing you. 好极了。那么再见。

    好极了。那么再见。

    Functions

    Greetings 打招呼

    • How do you do? 您好
    • How are you? 你好
    • Good morning (afternoon, evening, day). 早上好 (中午好,晚上好,你好)
    • How nice to see you! (见到你太好了)
    • How have you been? (你近来怎样?)
    • What’s happening? (情况怎样?)
    • How are you doing? (你的情形如何?)
    • How are things? (一切都好吧?)
    • How are you getting on with…? (你的…好吗?)
    • What’s new? (有新情况吗?)
    • Long time no see. (好久没见了。)

    Responding to greetings 对问候的回答

    • How do you do? 你好
    • Fine, thanks. And you? 很好,谢谢。你呢?
    • Very well, thanks. 很好,谢谢。
    • Good morning (afternoon, evening, day) 早上好 (中午好,晚上好,你好)
    • Pretty good. 很好
    • Couldn’t be better. 太好了
    • Not bad. 还可以
    • Okay. 可以
    • Nothing. 没什么新情况
    • Yeah! Long time no see. 是的,是好久没见了。

    Preclosings 告别前用语

    • Well, I’m afraid I have to be going 哦,恐怕我得走了。
    • It’s been a pleasure talking with you. 我很荣幸能与你交谈。
    • I really must go now. 我真的得走了。
    • Well, it’s getting late. 噢,天已晚了。
    • I know you are busy. 我知道你很忙。
    • Nice to see you again. 再次见到你太好了
    • Thanks for coming. 谢谢你的光临
    • Maybe we could get together sometime. 也许我们什么时候能再聚一聚
    • Great seeing you. 见到你太好了。
    • I’ve really got to go. 我现在真的得走了
    • Got to go now. 现在必须得走了。

    Responding to preclosings 对告别前用语的回答

    • Thanks for coming. 谢谢您的光临
    • Yes, I’ve enjoyed it. 对,我很开心
    • My pleasure, too. 这也是我的荣幸。
    • It’s good to see you. 见到你太好了。
    • Maybe wee can talk again. 也许我们能够再谈
    • It was fun. 这很有趣
    • Sounds good. 好主意
    • Same here. 我也同样
    • Ok, see you. 好吧,再见
    • See you again. 再见

    Closings 告别用语

    • Until next time… 下次再见
    • Good-bye. 再见
    • Have a nice weekend. 祝你周末愉快
    • Talk to you later. 回来再谈
    • See you later. 再见
    • good-bye. 再见

    Responding to closings 对告别用语的回答

    • You too. 你也一样
    • Same to you. 你也一样。
    • So long. Take care. 再见,保重。

    Notes.

        打招呼几乎是公式化的。有许多正确的打招呼和回答的方法。 经常当人们说:“how are you doing?”,他们并非真有兴趣去了解一个人的情况如何。 正象打招呼是无意识那样,回答也是无意识的。 “fine”再加上 “and how are you?”是最常见的回答。有时我们不想过分谈论我们真正的健康状况和感情,我们就用一个中性的回答,如“Okay”,然后就终止话题。

        通常人们不会突然终止谈话,告别然后突然分手。结束一次谈话一般要用一点时间,这涉及要用告别前用语和告别用语。告别前用语表明谈话快要结束而告别用语则明确地结束谈话。在与不熟悉的人或与上司谈话时,我们不应该忽视告别前用语发出的信号。在非正式的场合,年长者,位高者通常发出信号来结束谈话。告别前用语还包括为某事而感谢某人,为自己某一行为找借口,或道歉。告别用语与打招呼一样,一般都没有真正的意义。

    2005年08月11日

    今天把代码重新check了一下,发现了一个BUG。另外,感觉使用类型转换来进行解码可能会带来问题,所以将代码重新修改了一下。主要是decodeString()函数,另外,encodeString()函数对于发现的Bug也进行了修改。最重要的,这次对代码进行了一些必要的优化,使得代码效率有了很大的提高。经过测试,这次的代码效率是上一个版本的10倍左右(具体数值取决于所处理的字符串的构成,如有含有Unicode字符的数量等)。

    import java.util.*;
    import java.lang.*;
    import java.io.*;
    import java.nio.charset.*;
    import java.nio.*;

     /*

     * @author   Franky Bie, Rainer Fischer

     */

    public class DataObject {
        /**
          *  Check to see whether a String represents a Unicode char
          * @params
          *  chopString: The string to be checked
          * @return
          *  True if it is. False otherwise.  
          */
        protected boolean isUnicode(StringBuffer chopString)
        {
            if(chopString.charAt(1) != ‘U’ && chopString.charAt(1) != ‘u’)
             return false;
           
            for(int i = 2; i < 6 ; i ++)
            {
                if(!((chopString.charAt(i) >= ‘0′ && chopString.charAt(i) <= ‘9′) ||
                            (chopString.charAt(i) >= ‘A’ && chopString.charAt(i) <= ‘F’) ||
                            (chopString.charAt(i) >= ‘a’ && chopString.charAt(i) <= ‘f’)))
                 return false;
            }
            return true;
        }
        /**
         *   The function to encode the original string
         * @params
         *   original: the string to be encoded
         * @return
         *  The encoded string  
         */
        protected String encodeString(String original)
        {
            char[] srcBuffer = original.toCharArray();
            StringBuffer sbuf = new StringBuffer();
            StringBuffer sbufret = new StringBuffer();
            int length = original.length();
            Charset set = Charset.forName("UTF-16BE");
            int element;
               
            if(original.length() == 0)
                return "";
               
            try
            {
                for(int i = 0 ; i < length ; i ++)
                {
                    element = srcBuffer[i];
                    sbuf.delete(0,sbuf.length());
                   
                    if(element == ‘\\’)
                        sbuf.append("\\\\");
                    else
                    {
                        if(element > 0xFF)
                        {
                            sbuf.append("\\u");
                            /*
                            if(element < 0×1000)
                                tempString += "0";
                            tempString += Integer.toHexString(element).toUpperCase();
                            */
                            ByteBuffer bb = set.encode(original.substring(i,i+1));
                            int tempInt = bb.get(0) & 0xFF;
                            if(tempInt < 0×10)
                                sbuf.append("0");
                            sbuf.append(Integer.toHexString(tempInt));
                            tempInt = bb.get(1) & 0xFF;
                            if(tempInt < 0×10)
                                sbuf.append("0");
                            sbuf.append(Integer.toHexString(tempInt));
                        } else
                            sbuf.append(original.charAt(i));
                    }
                    sbufret.append(sbuf);
                }
            }
            catch(RuntimeException e)
            {
                e.printStackTrace();
                return null;
            }
                  
            return new String(sbufret);
        }
       
        /**
         *   The function to decode a string
         * @params
         *   encoded: the string to be decoded
         * @return
         *  The original string  
         */
        protected String decodeString(String encoded)
        {
            StringBuffer sbuf = new StringBuffer();
            StringBuffer sbufret = new StringBuffer();
            int length = encoded.length();
            int tempInt;
            Charset set = Charset.forName("UTF-16BE");
               
            boolean noConvert = false;
            try
            {
                int stepPoint = 0;
               
                for(int i = 0 ; i < length;)
                {
                 sbuf.delete(0,sbuf.length());
                    stepPoint = encoded.indexOf(‘\\’, stepPoint);
                   
                    if(stepPoint == -1)
                        stepPoint = length;
                       
                    if(i < stepPoint)
                    {
                        sbufret.append(encoded.substring(i,stepPoint));
                        i = stepPoint;
                    }
                   
                    if((i <= length – 2) && (encoded.charAt(i) == ‘\\’))
                    {
                     if(encoded.charAt(i+1) == ‘\\’)
                     {
                         sbufret.append("\\");
                         i += 2;
                     }
                     else if(i <= length – 6)
                     {
                         sbuf.append(encoded.substring(i,i + 6));
                         if(isUnicode(sbuf)) {
                             ByteBuffer bb = ByteBuffer.allocateDirect(2);
                             try{
                              tempInt = Integer.parseInt(sbuf.substring(2,4),16);
                              bb.put(0,(byte)tempInt);
                              tempInt = Integer.parseInt(sbuf.substring(4,6),16);
                          }
                          catch(NumberFormatException ne)
                          {
                           ne.printStackTrace();
                     continue;
                          }
                             bb.put(1,(byte)tempInt);
                             CharBuffer cb = set.decode(bb);
                             sbufret.append(cb.toString());
                             i += 6;
                         }
                     }
                     
                    }
                   
                    stepPoint ++;
                }
            }
            catch(RuntimeException e)
            {
                e.printStackTrace();
                return null;
            }
            return new String(sbufret);
        }   
    }

    2005年08月10日

    公司的一套产品客户端与服务器端使用XML-RPC进行通讯。因为XML-RPC不支持Unicode,所以需要解决这个问题。从网上看到别人的解决思路是使用BASE64编码,我不想这么做。转换起来太罗嗦了。我的思路是这样的:

    首先将所有的’\'字符均变为’\\’。其次是将Unicode字符均变为’\uXXXX’的形式,其中,X代表一个16进制数字。例如"你"的UNICODE为4F60,则将其变成"\u4F60"然后传送。在接收端检测到相应的字符后进行相应的解码即可。同时,若原字符串中含有’\',则将其转换为"\\"传送。以下是代码:

    import java.util.*;
    import java.lang.*;
    import java.io.*;

    /**
     * @author Franky Bie
    **/

    public class DataObject {
      /*
       Function:
        Check to see whether a String represents a Unicode char
       Parameter:
        chopString: The string to be checked
       Return Value:
        True if it is. False otherwise.   
        */

      protected static boolean isUnicode(String chopString) {
        boolean beUnicode = true;

        chopString = chopString.toUpperCase();
        beUnicode = beUnicode && chopString.charAt(0) == ‘\\’;
        beUnicode = beUnicode && chopString.charAt(1) == ‘U’;

        for(int i = 2; i < 6 ; i ++) {
          beUnicode = beUnicode && (chopString.charAt(i) >= ‘0′ && chopString.charAt(i) <= ‘9′) ||
                      (chopString.charAt(i) >= ‘A’ && chopString.charAt(i) <= ‘F’);

          if(!beUnicode)
            return false;
        }

        return true;
      }
      /*
       Function:
        The function to encode the original string
       Parameter:
        original: the string to be encoded
       Return value:
        The encoded string   
      */

      protected static String EncodeString(String original) {
        String encodedString = new String();
        char[] srcBuffer = original.toCharArray();
        String tempString = new String();
        int length = original.length();
        boolean beUnicode;
        short element;

        if(original.length() == 0)
          return "";

        try {
          for(int i = 0 ; i < length ; i ++) {
            element = (short)srcBuffer[i];
            tempString = "";

            if(element == ‘\\’)
              tempString = "\\\\";
            else {
              if(element > 0xFF) {
                tempString = "\\u";
                if(element < 0×1000)
                  tempString += "0";
                tempString += Integer.toHexString(element).toUpperCase();
              } else
                tempString += original.charAt(i);
            }
            encodedString += tempString;
          }
        } catch(RuntimeException e) {
          e.printStackTrace();
          return null;
        }

        return encodedString;
      }

      /*
       Function:
        The function to decode a string
       Parameter:
        encoded: the string to be decoded
       Return value:
        The original string   
      */

      protected static String DecodeString(String encoded) {
        String tempString = "";
        String decodedString = "";
        int length = encoded.length();
        int tempInt;

        try {
          for(int i = 0 ; i < length; i ++) {
            tempString = "";
            if(i <= length – 6) {
              tempString = encoded.substring(i,i + 6);

              if(isUnicode(tempString)) {
                tempString = tempString.substring(2,6);
                tempInt = Integer.valueOf(tempString,16);
                tempString = String.valueOf(tempInt);
                int[] spawnArray = new int[1];
                spawnArray[0] = tempInt;
                String spawnString = new String(spawnArray,0,1);
                decodedString += spawnString;
                i += 5;
                continue;
              }
            }
            if((i <= length – 2) && (encoded.charAt(i) == ‘\\’) && (encoded.charAt(i+1) == ‘\\’)) {
              decodedString += "\\";
              i++;
            } else {
              decodedString += encoded.substring(i,i+1);
            }
          }
        } catch(RuntimeException e) {
          e.printStackTrace();
          return null;
        }
        return decodedString;
      }

      /*
      Function:
       Test the fucntions of encoding and decoding a specified string
      */

      private static void testFunc(String origString) {
        String encode = EncodeString(origString);
        String dest = DecodeString(encode);
        System.out.println("Initial String: \t"+origString);
        System.out.println("Encoded String: \t"+encode);
        System.out.println("Decoded String: \t"+dest);
        System.out.println("");
      }

      public static void main(String[] args) {
        System.out.println("Test a Unicode String(Chinese Character):");
        String orig = "你好!(Hello!)";
        testFunc(orig);

        System.out.println("Test ANSI String");
        orig = "Hello world!";
        testFunc(orig);

        System.out.println("Test ‘\\u’ String");
        orig = "\\u2023\\uFeDc\\U55AA
        testFunc(orig);

        System.out.println("Test ‘\\’ terminated String");
        orig = "Hello world!\\";
        testFunc(orig);
      }
    }

    整个代码中觉得DecodeString()函数写得有些罗嗦,如果哪位能够简化一下int->unicode char的操作烦请告知以下在下。谢谢!QQ: 2124618

    2005年08月06日

    摘自Matrix: http://www.matrix.org.cn/thread.shtml?topicId=14875&forumId=19

    对于这个系列里的问题,每个学Java的人都应该搞懂。当然,如果只是学Java玩玩就无所谓了。如果你认为自己已经超越初学者了,却不很懂这些问题,请将你自己重归初学者行列。内容均来自于CSDN的经典老贴。

    问题一:我声明了什么!

    String s = "Hello world!";

    许多人都做过这样的事情,但是,我们到底声明了什么?回答通常是:一个String,内容是“Hello world!”。这样模糊的回答通常是概念不清的根源。如果要准确的回答,一半的人大概会回答错误。
    这个语句声明的是一个指向对象的引用,名为“s”,可以指向类型为String的任何对象,目前指向"Hello world!"这个String类型的对象。这就是真正发生的事情。我们并没有声明一个String对象,我们只是声明了一个只能指向String对象的引用变量。所以,如果在刚才那句语句后面,如果再运行一句:

    String string = s;

    我们是声明了另外一个只能指向String对象的引用,名为string,并没有第二个对象产生,string还是指向原来那个对象,也就是,和s指向同一个对象。

    问题二:"=="和equals方法究竟有什么区别?

    ==操作符专门用来比较变量的值是否相等。比较好理解的一点是:
    int a=10;
    int b=10;
    则a==b将是true。
    但不好理解的地方是:
    String a=new String("foo");
    String b=new String("foo");
    则a==b将返回false。

    根据前一帖说过,对象变量其实是一个引用,它们的值是指向对象所在的内存地址,而不是对象本身。a和b都使用了new操作符,意味着将在内存中产生两个内容为"foo"的字符串,既然是“两个”,它们自然位于不同的内存地址。a和b的值其实是两个不同的内存地址的值,所以使用"=="操作符,结果会是false。诚然,a和b所指的对象,它们的内容都是"foo",应该是“相等”,但是==操作符并不涉及到对象内容的比较。
    对象内容的比较,正是equals方法做的事。

    看一下Object对象的equals方法是如何实现的:
    boolean equals(Object o){

    return this==o;

    }
    Object对象默认使用了==操作符。所以如果你自创的类没有覆盖equals方法,那你的类使用equals和使用==会得到同样的结果。同样也可以看出,Object的equals方法没有达到equals方法应该达到的目标:比较两个对象内容是否相等。因为答案应该由类的创建者决定,所以Object把这个任务留给了类的创建者。

    看一下一个极端的类:
    Class Monster{
    private String content;

    boolean equals(Object another){ return true;}

    }
    我覆盖了equals方法。这个实现会导致无论Monster实例内容如何,它们之间的比较永远返回true。

    所以当你是用equals方法判断对象的内容是否相等,请不要想当然。因为可能你认为相等,而这个类的作者不这样认为,而类的equals方法的实现是由他掌握的。如果你需要使用equals方法,或者使用任何基于散列码的集合(HashSet,HashMap,HashTable),请察看一下java doc以确认这个类的equals逻辑是如何实现的。

    问题三:String到底变了没有?

    没有。因为String被设计成不可变(immutable)类,所以它的所有对象都是不可变对象。请看下列代码:

    String s = "Hello";
    s = s + " world!";

    s所指向的对象是否改变了呢?从本系列第一篇的结论很容易导出这个结论。我们来看看发生了什么事情。在这段代码中,s原先指向一个String对象,内容是"Hello",然后我们对s进行了+操作,那么s所指向的那个对象是否发生了改变呢?答案是没有。这时,s不指向原来那个对象了,而指向了另一个String对象,内容为"Hello world!",原来那个对象还存在于内存之中,只是s这个引用变量不再指向它了。
    通过上面的说明,我们很容易导出另一个结论,如果经常对字符串进行各种各样的修改,或者说,不可预见的修改,那么使用String来代表字符串的话会引起很大的内存开销。因为String对象建立之后不能再改变,所以对于每一个不同的字符串,都需要一个String对象来表示。这时,应该考虑使用StringBuffer类,它允许修改,而不是每个不同的字符串都要生成一个新的对象。并且,这两种类的对象转换十分容易。
    同时,我们还可以知道,如果要使用内容相同的字符串,不必每次都new一个String。例如我们要在构造器中对一个名叫s的String引用变量进行初始化,把它设置为初始值,应当这样做:
    public class Demo {
    private String s;

    public Demo {
    s = "Initial Value";
    }

    }
    而非
    s = new String("Initial Value");
    后者每次都会调用构造器,生成新对象,性能低下且内存开销大,并且没有意义,因为String对象不可改变,所以对于内容相同的字符串,只要一个String对象来表示就可以了。也就说,多次调用上面的构造器创建多个对象,他们的String类型属性s都指向同一个对象。
    上面的结论还基于这样一个事实:对于字符串常量,如果内容相同,Java认为它们代表同一个String对象。而用关键字new调用构造器,总是会创建一个新的对象,无论内容是否相同。
    至于为什么要把String类设计成不可变类,是它的用途决定的。其实不只String,很多Java标准类库中的类都是不可变的。在开发一个系统的时候,我们有时候也需要设计不可变类,来传递一组相关的值,这也是面向对象思想的体现。不可变类有一些优点,比如因为它的对象是只读的,所以多线程并发访问也不会有任何问题。当然也有一些缺点,比如每个不同的状态都要一个对象来代表,可能会造成性能上的问题。所以Java标准类库还提供了一个可变版本,即StringBuffer。

    问题四:final关键字到底修饰了什么?

    final使得被修饰的变量"不变",但是由于对象型变量的本质是“引用”,使得“不变”也有了两种含义:引用本身的不变,和引用指向的对象不变。

    引用本身的不变:
    final StringBuffer a=new StringBuffer("immutable");
    final StringBuffer b=new StringBuffer("not immutable");
    a=b;//编译期错误

    引用指向的对象不变:
    final StringBuffer a=new StringBuffer("immutable");
    a.append(" broken!"); //编译通过

    可见,final只对引用的“值”(也即它所指向的那个对象的内存地址)有效,它迫使引用只能指向初始指向的那个对象,改变它的指向会导致编译期错误。至于它所指向的对象的变化,final是不负责的。这很类似==操作符:==操作符只负责引用的“值”相等,至于这个地址所指向的对象内容是否相等,==操作符是不管的。

    理解final问题有很重要的含义。许多程序漏洞都基于此—-final只能保证引用永远指向固定对象,不能保证那个对象的状态不变。在多线程的操作中,一个对象会被多个线程共享或修改,一个线程对对象无意识的修改可能会导致另一个使用此对象的线程崩溃。一个错误的解决方法就是在此对象新建的时候把它声明为final,意图使得它“永远不变”。其实那是徒劳的。

    问题五:到底要怎么样初始化!

    本问题讨论变量的初始化,所以先来看一下Java中有哪些种类的变量。
    1. 类的属性,或者叫值域
    2. 方法里的局部变量
    3. 方法的参数

    对于第一种变量,Java虚拟机会自动进行初始化。如果给出了初始值,则初始化为该初始值。如果没有给出,则把它初始化为该类型变量的默认初始值。

    int类型变量默认初始值为0
    float类型变量默认初始值为0.0f
    double类型变量默认初始值为0.0
    boolean类型变量默认初始值为false
    char类型变量默认初始值为0(ASCII码)
    long类型变量默认初始值为0
    所有对象引用类型变量默认初始值为null,即不指向任何对象。注意数组本身也是对象,所以没有初始化的数组引用在自动初始化后其值也是null。

    对于两种不同的类属性,static属性与instance属性,初始化的时机是不同的。instance属性在创建实例的时候初始化,static属性在类加载,也就是第一次用到这个类的时候初始化,对于后来的实例的创建,不再次进行初始化。这个问题会在以后的系列中进行详细讨论。

    对于第二种变量,必须明确地进行初始化。如果再没有初始化之前就试图使用它,编译器会抗议。如果初始化的语句在try块中或if块中,也必须要让它在第一次使用前一定能够得到赋值。也就是说,把初始化语句放在只有if块的条件判断语句中编译器也会抗议,因为执行的时候可能不符合if后面的判断条件,如此一来初始化语句就不会被执行了,这就违反了局部变量使用前必须初始化的规定。但如果在else块中也有初始化语句,就可以通过编译,因为无论如何,总有至少一条初始化语句会被执行,不会发生使用前未被初始化的事情。对于try-catch也是一样,如果只有在try块里才有初始化语句,编译部通过。如果在catch或finally里也有,则可以通过编译。总之,要保证局部变量在使用之前一定被初始化了。所以,一个好的做法是在声明他们的时候就初始化他们,如果不知道要出事化成什么值好,就用上面的默认值吧!

    其实第三种变量和第二种本质上是一样的,都是方法中的局部变量。只不过作为参数,肯定是被初始化过的,传入的值就是初始值,所以不需要初始化。

    问题六:instanceof是什么东东?

    instanceof是Java的一个二元操作符,和==,>,<是同一类东东。由于它是由字母组成的,所以也是Java的保留关键字。它的作用是测试它左边的对象是否是它右边的类的实例,返回boolean类型的数据。举个例子:

    String s = "I AM an Object!";
    boolean isObject = s instanceof Object;

    我们声明了一个String对象引用,指向一个String对象,然后用instancof来测试它所指向的对象是否是Object类的一个实例,显然,这是真的,所以返回true,也就是isObject的值为True。
    instanceof有一些用处。比如我们写了一个处理账单的系统,其中有这样三个类:

    public class Bill {//省略细节}
    public class PhoneBill extends Bill {//省略细节}
    public class GasBill extends Bill {//省略细节}

    在处理程序里有一个方法,接受一个Bill类型的对象,计算金额。假设两种账单计算方法不同,而传入的Bill对象可能是两种中的任何一种,所以要用instanceof来判断:

    public double calculate(Bill bill) {
    if (bill instanceof PhoneBill) {
    //计算电话账单
    }
    if (bill instanceof GasBill) {
    //计算燃气账单
    }

    }
    这样就可以用一个方法处理两种子类。

    然而,这种做法通常被认为是没有好好利用面向对象中的多态性。其实上面的功能要求用方法重载完全可以实现,这是面向对象变成应有的做法,避免回到结构化编程模式。只要提供两个名字和返回值都相同,接受参数类型不同的方法就可以了:

    public double calculate(PhoneBill bill) {
    //计算电话账单
    }

    public double calculate(GasBill bill) {
    //计算燃气账单
    }

    所以,使用instanceof在绝大多数情况下并不是推荐的做法,应当好好利用多态。