Java SE 6 新特性系列

来源:互联网 发布:韩国网络短剧迷你剧 编辑:程序博客网 时间:2024/05/29 07:33

源自:http://blog.csdn.net/fuliangliang/archive/2007/10/09/1816848.aspx)
 Java SE 6 新特性系列之一 Instrumentation 新功能      

 

级别: 中级

胡 睿 (ruihu@cn.ibm.com), 软件工程师, IBM
吕 晶 (purefire@126.com), 软件工程师, IBM

2007 年 5 月 16 日

2006 年底,Sun 公司发布了 Java Standard Edition 6(Java SE 6)的最终正式版,代号 Mustang(野马)。跟 Tiger(Java SE 5)相比,Mustang 在性能方面有了不错的提升。与 Tiger 在 API 库方面的大幅度加强相比,虽然 Mustang 在 API 库方面的新特性显得不太多,但是也提供了许多实用和方便的功能:在脚本,WebService,XML,编译器 API,数据库,JMX,网络和 Instrumentation 方面都有不错的新特性和功能加强。 本系列 文章主要介绍 Java SE 6 在 API 库方面的部分新特性,通过一些例子和讲解,帮助开发者在编程实践当中更好的运用 Java SE 6,提高开发效率。

本文是 本系列 的第一篇,介绍了 Java SE 6 在 Instrumentation 方面的新特性。

Instrumentation 简介

利用 Java 代码,即 java.lang.instrument 做动态 Instrumentation 是 Java SE 5 的新特性,它把 Java 的 instrument 功能从本地代码中解放出来,使之可以用 Java 代码的方式解决问题。使用 Instrumentation,开发者可以构建一个独立于应用程序的代理程序(Agent),用来监测和协助运行在 JVM 上的程序,甚至能够替换和修改某些类的定义。有了这样的功能,开发者就可以实现更为灵活的运行时虚拟机监控和 Java 类操作了,这样的特性实际上提供了一种虚拟机级别支持的 AOP 实现方式,使得开发者无需对 JDK 做任何升级和改动,就可以实现某些 AOP 的功能了。

在 Java SE 6 里面,instrumentation 包被赋予了更强大的功能:启动后的 instrument、本地代码(native code)instrument,以及动态改变 classpath 等等。这些改变,意味着 Java 具有了更强的动态控制、解释能力,它使得 Java 语言变得更加灵活多变。

在 Java SE6 里面,最大的改变使运行时的 Instrumentation 成为可能。在 Java SE 5 中,Instrument 要求在运行前利用命令行参数或者系统参数来设置代理类,在实际的运行之中,虚拟机在初始化之时(在绝大多数的 Java 类库被载入之前),instrumentation 的设置已经启动,并在虚拟机中设置了回调函数,检测特定类的加载情况,并完成实际工作。但是在实际的很多的情况下,我们没有办法在虚拟机启动之时就为其设定代理,这样实际上限制了 instrument 的应用。而 Java SE 6 的新特性改变了这种情况,通过 Java Tool API 中的 attach 方式,我们可以很方便地在运行过程中动态地设置加载代理类,以达到 instrumentation 的目的。

另外,对 native 的 Instrumentation 也是 Java SE 6 的一个崭新的功能,这使以前无法完成的功能 —— 对 native 接口的 instrumentation 可以在 Java SE 6 中,通过一个或者一系列的 prefix 添加而得以完成。

最后,Java SE 6 里的 Instrumentation 也增加了动态添加 class path 的功能。所有这些新的功能,都使得 instrument 包的功能更加丰富,从而使 Java 语言本身更加强大。



回页首

Instrumentation 的基本功能和用法

“java.lang.instrument”包的具体实现,依赖于 JVMTI。JVMTI(Java Virtual Machine Tool Interface)是一套由 Java 虚拟机提供的,为 JVM 相关的工具提供的本地编程接口集合。JVMTI 是从 Java SE 5 开始引入,整合和取代了以前使用的 Java Virtual Machine Profiler Interface (JVMPI) 和 the Java Virtual Machine Debug Interface (JVMDI),而在 Java SE 6 中,JVMPI 和 JVMDI 已经消失了。JVMTI 提供了一套”代理”程序机制,可以支持第三方工具程序以代理的方式连接和访问 JVM,并利用 JVMTI 提供的丰富的编程接口,完成很多跟 JVM 相关的功能。事实上,java.lang.instrument 包的实现,也就是基于这种机制的:在 Instrumentation 的实现当中,存在一个 JVMTI 的代理程序,通过调用 JVMTI 当中 Java 类相关的函数来完成 Java 类的动态操作。除开 Instrumentation 功能外,JVMTI 还在虚拟机内存管理,线程控制,方法和变量操作等等方面提供了大量有价值的函数。关于 JVMTI 的详细信息,请参考 Java SE 6 文档(请参见 参考资源)当中的介绍。

Instrumentation 的最大作用,就是类定义动态改变和操作。在 Java SE 5 及其后续版本当中,开发者可以在一个普通 Java 程序(带有 main 函数的 Java 类)运行时,通过 –javaagent 参数指定一个特定的 jar 文件(包含 Instrumentation 代理)来启动 Instrumentation 的代理程序。

在 Java SE 5 当中,开发者可以让 Instrumentation 代理在 main 函数运行前执行。简要说来就是如下几个步骤:

  1. 编写 premain 函数

    编写一个 Java 类,包含如下两个方法当中的任何一个

    public static void premain(String agentArgs, Instrumentation inst);        [1]
    public static void premain(String agentArgs);            [2]

    其中,[1] 的优先级比 [2] 高,将会被优先执行([1] 和 [2] 同时存在时,[2] 被忽略)。

    在这个 premain 函数中,开发者可以进行对类的各种操作。

    agentArgs 是 premain 函数得到的程序参数,随同 “–javaagent”一起传入。与 main 函数不同的是,这个参数是一个字符串而不是一个字符串数组,如果程序参数有多个,程序将自行解析这个字符串。

    Inst 是一个 java.lang.instrument.Instrumentation 的实例,由 JVM 自动传入。java.lang.instrument.Instrumentation 是 instrument 包中定义的一个接口,也是这个包的核心部分,集中了其中几乎所有的功能方法,例如类定义的转换和操作等等。

  2. jar 文件打包

    将这个 Java 类打包成一个 jar 文件,并在其中的 manifest 属性当中加入” Premain-Class”来指定步骤 1 当中编写的那个带有 premain 的 Java类。(可能还需要指定其他属性以开启更多功能)

  3. 运行

    用如下方式运行带有 Instrumentation 的 Java 程序:

    java -javaagent:jar文件的位置[=传入premain的参数]

对 Java 类文件的操作,可以理解为对一个 byte 数组的操作(将类文件的二进制字节流读入一个 byte 数组)。开发者可以在“ClassFileTransformer”的 transform 方法当中得到,操作并最终返回一个类的定义(一个 byte 数组)。这方面,Apache 的 BCEL 开源项目提供了强有力的支持,读者可以在参考文章“Java SE 5 特性 Instrumentation 实践”中看到一个 BCEL 和 Instrumentation 结合的例子。具体的字节码操作并非本文的重点,所以,本文中所举的例子,只是采用简单的类文件替换的方式来演示 Instrumentation 的使用。

下面,我们通过简单的举例,来说明 Instrumentation 的基本使用方法。

首先,我们有一个简单的类,TransClass, 可以通过一个静态方法返回一个整数 1。

public class TransClass {
    
public int getNumber() {
    
return 1;
    }
}

我们运行如下类,可以得到输出 ”1“。

public class TestMainInJar {
    
public static void main(String[] args) {
        System.out.println(
new TransClass().getNumber());
    }
}    

然后,我们将 TransClass 的 getNumber 方法改成如下:

public int getNumber() {
        
return 2;
}

再将这个返回 2 的 Java 文件编译成类文件,为了区别开原有的返回 1 的类,我们将返回 2 的这个类文件命名为 TransClass2.class.2。

接下来,我们建立一个 Transformer 类:

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

class Transformer implements ClassFileTransformer {

    
public static final String classNumberReturns2 = "TransClass.class.2";

    
public static byte[] getBytesFromFile(String fileName) {
        
try {
            
// precondition
            File file = new File(fileName);
            InputStream is 
= new FileInputStream(file);
            
long length = file.length();
            
byte[] bytes = new byte[(int) length];

            
// Read in the bytes
            int offset = 0;
            
int numRead = 0;
            
while (offset <bytes.length
                    
&& (numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) {
                offset 
+= numRead;
            }

            
if (offset < bytes.length) {
                
throw new IOException("Could not completely read file "
                        
+ file.getName());
            }
            is.close();
            
return bytes;
        } 
catch (Exception e) {
            System.out.println(
"error occurs in _ClassTransformer!"
                    
+ e.getClass().getName());
            
return null;
        }
    }

    
public byte[] transform(ClassLoader l, String className, Class<?> c,
            ProtectionDomain pd, 
byte[] b) throws IllegalClassFormatException {
        
if (!className.equals("TransClass")) {
            
return null;
        }
        
return getBytesFromFile(classNumberReturns2);

    }
}

这个类实现了 ClassFileTransformer 接口。其中,getBytesFromFile 方法根据文件名读入二进制字符流,而 ClassFileTransformer 当中规定的 transform 方法则完成了类定义的替换转换。

最后,我们建立一个 Premain 类,写入 Instrumentation 的代理方法 premain:

public class Premain {
    
public static void premain(String agentArgs, Instrumentation inst)
            
throws ClassNotFoundException, UnmodifiableClassException {
        inst.addTransformer(
new Transformer());
    }
}

可以看出,addTransformer 方法并没有指明要转换哪个类。转换发生在 premain 函数执行之后,main 函数执行之前,这时每装载一个类,transform 方法就会执行一次,看看是否需要转换,所以,在 transform(Transformer 类中)方法中,程序用 className.equals("TransClass") 来判断当前的类是否需要转换。

代码完成后,我们将他们打包为 TestInstrument1.jar。返回 1 的那个 TransClass 的类文件保留在 jar 包中,而返回 2 的那个 TransClass.class.2 则放到 jar 的外面。在 manifest 里面加入如下属性来指定 premain 所在的类:

Manifest-Version: 1.0Premain-Class: Premain

在运行这个程序的时候,如果我们用普通方式运行这个 jar 中的 main 函数,可以得到输出“1”。如果用下列方式运行:

java –javaagent:TestInstrument1.jar –cp TestInstrument1.jar TestMainInJar

则会得到输出“2”。

当然,程序运行的 main 函数不一定要放在 premain 所在的这个 jar 文件里面,这里只是为了例子程序打包的方便而放在一起的。

除开用 addTransformer 的方式,Instrumentation 当中还有另外一个方法“redefineClasses”来实现 premain 当中指定的转换。用法类似,如下:

public class Premain {
    
public static void premain(String agentArgs, Instrumentation inst)
            
throws ClassNotFoundException, UnmodifiableClassException {
        ClassDefinition def 
= new ClassDefinition(TransClass.class, Transformer
                .getBytesFromFile(Transformer.classNumberReturns2));
        inst.redefineClasses(
new ClassDefinition[] { def });
        System.out.println(
"success");
    }
}

redefineClasses 的功能比较强大,可以批量转换很多类。



回页首

Java SE 6 的新特性:虚拟机启动后的动态 instrument

在 Java SE 5 当中,开发者只能在 premain 当中施展想象力,所作的 Instrumentation 也仅限与 main 函数执行前,这样的方式存在一定的局限性。

在 Java SE 5 的基础上,Java SE 6 针对这种状况做出了改进,开发者可以在 main 函数开始执行以后,再启动自己的 Instrumentation 程序。

在 Java SE 6 的 Instrumentation 当中,有一个跟 premain“并驾齐驱”的“agentmain”方法,可以在 main 函数开始运行之后再运行。

跟 premain 函数一样, 开发者可以编写一个含有“agentmain”函数的 Java 类:

public static void agentmain (String agentArgs, Instrumentation inst);        [1]
public static void agentmain (String agentArgs);            [2]

同样,[1] 的优先级比 [2] 高,将会被优先执行。

跟 premain 函数一样,开发者可以在 agentmain 中进行对类的各种操作。其中的 agentArgs 和 Inst 的用法跟 premain 相同。

与“Premain-Class”类似,开发者必须在 manifest 文件里面设置“Agent-Class”来指定包含 agentmain 函数的类。

可是,跟 premain 不同的是,agentmain 需要在 main 函数开始运行后才启动,这样的时机应该如何确定呢,这样的功能又如何实现呢?

在 Java SE 6 文档当中,开发者也许无法在 java.lang.instrument 包相关的文档部分看到明确的介绍,更加无法看到具体的应用 agnetmain 的例子。不过,在 Java SE 6 的新特性里面,有一个不太起眼的地方,揭示了 agentmain 的用法。这就是 Java SE 6 当中提供的 Attach API。

Attach API 不是 Java 的标准 API,而是 Sun 公司提供的一套扩展 API,用来向目标 JVM ”附着”(Attach)代理工具程序的。有了它,开发者可以方便的监控一个 JVM,运行一个外加的代理程序。

Attach API 很简单,只有 2 个主要的类,都在 com.sun.tools.attach 包里面: VirtualMachine 代表一个 Java 虚拟机,也就是程序需要监控的目标虚拟机,提供了 JVM 枚举,Attach 动作和 Detach 动作(Attach 动作的相反行为,从 JVM 上面解除一个代理)等等; VirtualMachineDescriptor 则是一个描述虚拟机的容器类,配合 VirtualMachine 类完成各种功能。

为了简单起见,我们举例简化如下:依然用类文件替换的方式,将一个返回 1 的函数替换成返回 2 的函数,Attach API 写在一个线程里面,用睡眠等待的方式,每隔半秒时间检查一次所有的 Java 虚拟机,当发现有新的虚拟机出现的时候,就调用 attach 函数,随后再按照 Attach API 文档里面所说的方式装载 Jar 文件。等到 5 秒钟的时候,attach 程序自动结束。而在 main 函数里面,程序每隔半秒钟输出一次返回值(显示出返回值从 1 变成 2)。

TransClass 类和 Transformer 类的代码不变,参看上一节介绍。 含有 main 函数的 TestMainInJar 代码为:

public class TestMainInJar {
    
public static void main(String[] args) throws InterruptedException {
        System.out.println(
new TransClass().getNumber());
        
int count = 0;
        
while (true) {
            Thread.sleep(
500);
            count
++;
            
int number = new TransClass().getNumber();
            System.out.println(number);
            
if (3 == number || count >= 10) {
                
break;
            }
        }
    }
}

含有 agentmain 的 AgentMain 类的代码为:

 
import java.lang.instrument.ClassDefinition;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;

public class AgentMain {
    
public static void agentmain(String agentArgs, Instrumentation inst)
            
throws ClassNotFoundException, UnmodifiableClassException,
            InterruptedException {
        inst.addTransformer(
new Transformer (), true);
        inst.retransformClasses(TransClass.
class);
        System.out.println(
"Agent Main Done");
    }
}

其中,retransformClasses 是 Java SE 6 里面的新方法,它跟 redefineClasses 一样,可以批量转换类定义,多用于 agentmain 场合。

Jar 文件跟 Premain 那个例子里面的 Jar 文件差不多,也是把 main 和 agentmain 的类,TransClass,Transformer 等类放在一起,打包为“TestInstrument1.jar”,而 Jar 文件当中的 Manifest 文件为:

Manifest-Version: 1.0Agent-Class: AgentMain

另外,为了运行 Attach API,我们可以再写一个控制程序来模拟监控过程:(代码片段)

import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
……
// 一个运行 Attach API 的线程子类
static class AttachThread extends Thread {
        
private final List<VirtualMachineDescriptor> listBefore;

        
private final String jar;

        AttachThread(String attachJar, List
<VirtualMachineDescriptor> vms) {
            listBefore 
= vms;  // 记录程序启动时的 VM 集合
            jar = attachJar;
        }

        
public void run() {
            VirtualMachine vm 
= null;
            List
<VirtualMachineDescriptor> listAfter = null;
            
try {
                
int count = 0;
                
while (true) {
                    listAfter 
= VirtualMachine.list();
                    
for (VirtualMachineDescriptor vmd : listAfter) {
                        
if (!listBefore.contains(vmd)) { 
// 如果 VM 有增加,我们就认为是被监控的 VM 启动了
// 这时,我们开始监控这个 VM
                            vm = VirtualMachine.attach(vmd);
                            
break;
                        }
                    }
                    Thread.sleep(
500);
                    count
++;
                    
if (null != vm || count >= 10) {
                        
break;
                    }
                }
                vm.loadAgent(jar);
                vm.detach();
            } 
catch (Exception e) {
                 ignore
            }
        }
    }
……
public static void main(String[] args) throws InterruptedException {     
     
new AttachThread("TestInstrument1.jar", VirtualMachine.list()).start();

}

运行时,可以首先运行上面这个启动新线程的 main 函数,然后,在 5 秒钟内(仅仅简单模拟 JVM 的监控过程)运行如下命令启动测试 Jar 文件:

java –javaagent:TestInstrument2.jar –cp TestInstrument2.jar TestMainInJar

如果时间掌握得不太差的话,程序首先会在屏幕上打出 1,这是改动前的类的输出,然后会打出一些 2,这个表示 agentmain 已经被 Attach API 成功附着到 JVM 上,代理程序生效了,当然,还可以看到“Agent Main Done”字样的输出。

以上例子仅仅只是简单示例,简单说明这个特性而已。真实的例子往往比较复杂,而且可能运行在分布式环境的多个 JVM 之中。



回页首

Java SE 6 新特性:本地方法的 Instrumentation

在 1.5 版本的 instumentation 里,并没有对 Java 本地方法(Native Method)的处理方式,而且在 Java 标准的 JVMTI 之下,并没有办法改变 method signature, 这就使替换本地方法非常地困难。一个比较直接而简单的想法是,在启动时替换本地代码所在的动态链接库 —— 但是这样,本质上是一种静态的替换,而不是动态的 Instrumentation。而且,这样可能需要编译较大数量的动态链接库 —— 比如,我们有三个本地函数,假设每一个都需要一个替换,而在不同的应用之下,可能需要不同的组合,那么如果我们把三个函数都编译在同一个动态链接库之中,最多我们需要 8 个不同的动态链接库来满足需要。当然,我们也可以独立地编译之,那样也需要 6 个动态链接库——无论如何,这种繁琐的方式是不可接受的。

在 Java SE 6 中,新的 Native Instrumentation 提出了一个新的 native code 的解析方式,作为原有的 native method 的解析方式的一个补充,来很好地解决了一些问题。这就是在新版本的 java.lang.instrument 包里,我们拥有了对 native 代码的 instrument 方式 —— 设置 prefix。

假设我们有了一个 native 函数,名字叫 nativeMethod,在运行中过程中,我们需要将它指向另外一个函数(需要注意的是,在当前标准的 JVMTI 之下,除了 native 函数名,其他的 signature 需要一致)。比如我们的 Java 代码是:

package nativeTester;
class nativePrefixTester{
    …
    
native int nativeMethod(int input);
    …
}

那么我们已经实现的本地代码是:

jint Java_nativeTester_nativeMethod(jclass thiz, jobject thisObj, jint input);

现在我们需要在调用这个函数时,使之指向另外一个函数。那么按照 J2SE 的做法,我们可以按他的命名方式,加上一个 prefix 作为新的函数名。比如,我们以 "another_" 作为 prefix,那么我们新的函数是:

jint Java_nativeTester_another_nativePrefixTester(jclass thiz, jobject thisObj, jint input);

然后将之编入动态链接库之中。

现在我们已经有了新的本地函数,接下来就是做 instrument 的设置。正如以上所说的,我们可以使用 premain 方式,在虚拟机启动之时就载入 premain 完成 instrument 代理设置。也可以使用 agentmain 方式,去 attach 虚拟机来启动代理。而设置 native 函数的也是相当简单的:

premain(){  // 或者也可以在 agentmain 里

if (!isNativeMethodPrefixSupported()){
        
return// 如果无法设置,则返回
}
setNativeMethodPrefix(transformer,
"another_"); // 设置 native 函数的 prefix,注意这个下划线必须由用户自己规定

}

在这里要注意两个问题。一是不是在任何的情况下都是可以设置 native 函数的 prefix 的。首先,我们要注意到 agent 包之中的 Manifest 所设定的特性:

Can-Set-Native-Method-Prefix

要注意,这一个参数都可以影响是否可以设置 native prefix,而且,在默认的设置之中,这个参数是 false 的,我们需要将之设置成 true(顺便说一句,对 Manifest 之中的属性来说都是大小写无关的,当然,如果给一个不是“true”的值,就会被当作 false 值处理)。

当然,我们还需要确认虚拟机本身是否支持 setNativePrefix。在 Java API 里,Instrumentation 类提供了一个函数 isNativePrefix,通过这个函数我们可以知道该功能是否可以实行。

二是我们可以为每一个 ClassTransformer 加上它自己的 nativeprefix;同时,每一个 ClassTransformer 都可以为同一个 class 做 transform,因此对于一个 Class 来说,一个 native 函数可能有不同的 prefix,因此对这个函数来说,它可能也有好几种解析方式。

在 Java SE 6 当中,Native prefix 的解释方式如下:对于某一个 package 内的一个 class 当中的一个 native method 来说,首先,假设我们对这个函数的 transformer 设置了 native 的 prefix“another”,它将这个函数接口解释成:

由 Java 的函数接口

native void method()

和上述 prefix"another",去寻找本地代码中的函数

void Java_package_class_another_method(jclass theClass, jobject thiz);  // 请注意 prefix 在函数名中出现的位置!

一旦可以找到,那么调用这个函数,整个解析过程就结束了;如果没有找到,那么虚拟机将会做进一步的解析工作。我们将利用 Java native 接口最基本的解析方式,去找本地代码中的函数:

void Java_package_class_method(jclass theClass, jobject thiz);

如果找到,则执行之。否则,因为没有任何一个合适的解析方式,于是宣告这个过程失败。

那么如果有多个 transformer,同时每一个都有自己的 prefix,又该如何解析呢?事实上,虚拟机是按 transformer 被加入到的 Instrumentation 之中的次序去解析的(还记得我们最基本的 addTransformer 方法吗?)。

假设我们有三个 transformer 要被加入进来,他们的次序和相对应的 prefix 分别为:transformer1 和“prefix1_”,transformer2 和 “prefix2_”,transformer3 和 “prefix3_”。那么,虚拟机会首先做的就是将接口解析为:

native void prefix1_prefix2_prefix3_native_method()

然后去找它相对应的 native 代码。

但是如果第二个 transformer(transformer2)没有设定 prefix,那么很简单,我们得到的解析是:

native void prefix1_prefix3_native_method()

这个方式简单而自然。

当然,对于多个 prefix 的情况,我们还要注意一些复杂的情况。比如,假设我们有一个 native 函数接口是:

native void native_method()

然后我们为它设置了两个 prefix,比如 "wrapped_" 和 "wrapped2_",那么,我们得到的是什么呢?是

void Java_package_class_wrapped_wrapped2_method(jclass theClass, jobject thiz); // 这个函数名正确吗?

吗?答案是否定的,因为事实上,对 Java 中 native 函数的接口到 native 中的映射,有一系列的规定,因此可能有一些特殊的字符要被代入。而实际中,这个函数的正确的函数名是:

void Java_package_class_wrapped_1wrapped2_1method(jclass theClass, jobject thiz); // 只有这个函数名会被找到

很有趣不是吗?因此如果我们要做类似的工作,一个很好的建议是首先在 Java 中写一个带 prefix 的 native 接口,用 javah 工具生成一个 c 的 header-file,看看它实际解析得到的函数名是什么,这样我们就可以避免一些不必要的麻烦。

另外一个事实是,与我们的想像不同,对于两个或者两个以上的 prefix,虚拟机并不做更多的解析;它不会试图去掉某一个 prefix,再来组装函数接口。它做且仅作两次解析。

总之,新的 native 的 prefix-instrumentation 的方式,改变了以前 Java 中 native 代码无法动态改变的缺点。在当前,利用 JNI 来写 native 代码也是 Java 应用中非常重要的一个环节,因此它的动态化意味着整个 Java 都可以动态改变了 —— 现在我们的代码可以利用加上 prefix 来动态改变 native 函数的指向,正如上面所说的,如果找不到,虚拟机还会去尝试做标准的解析,这让我们拥有了动态地替换 native 代码的方式,我们可以将许多带不同 prefix 的函数编译在一个动态链接库之中,而通过 instrument 包的功能,让 native 函数和 Java 函数一样动态改变、动态替换。

当然,现在的 native 的 instrumentation 还有一些限制条件,比如,不同的 transformer 会有自己的 native prefix,就是说,每一个 transformer 会负责他所替换的所有类而不是特定类的 prefix —— 因此这个粒度可能不够精确。



回页首

Java SE 6 新特性:BootClassPath / SystemClassPath 的动态增补

我们知道,通过设置系统参数或者通过虚拟机启动参数,我们可以设置一个虚拟机运行时的 boot class 加载路径(-Xbootclasspath)和 system class(-cp)加载路径。当然,我们在运行之后无法替换它。然而,我们也许有时候要需要把某些 jar 加载到 bootclasspath 之中,而我们无法应用上述两个方法;或者我们需要在虚拟机启动之后来加载某些 jar 进入 bootclasspath。在 Java SE 6 之中,我们可以做到这一点了。

实现这几点很简单,首先,我们依然需要确认虚拟机已经支持这个功能,然后在 premain/agantmain 之中加上需要的 classpath。我们可以在我们的 Transformer 里使用 appendToBootstrapClassLoaderSearch/appendToSystemClassLoaderSearch 来完成这个任务。

同时我们可以注意到,在 agent 的 manifest 里加入 Boot-Class-Path 其实一样可以在动态地载入 agent 的同时加入自己的 boot class 路径,当然,在 Java code 中它可以更加动态方便和智能地完成 —— 我们可以很方便地加入判断和选择成分。

在这里我们也需要注意几点。首先,我们加入到 classpath 的 jar 文件中不应当带有任何和系统的 instrumentation 有关的系统同名类,不然,一切都陷入不可预料之中 —— 这不是一个工程师想要得到的结果,不是吗?

其次,我们要注意到虚拟机的 ClassLoader 的工作方式,它会记载解析结果。比如,我们曾经要求读入某个类 someclass,但是失败了,ClassLoader 会记得这一点。即使我们在后面动态地加入了某一个 jar,含有这个类,ClassLoader 依然会认为我们无法解析这个类,与上次出错的相同的错误会被报告。

再次我们知道在 Java 语言中有一个系统参数“java.class.path”,这个 property 里面记录了我们当前的 classpath,但是,我们使用这两个函数,虽然真正地改变了实际的 classpath,却不会对这个 property 本身产生任何影响。

在公开的 JavaDoc 中我们可以发现一个很有意思的事情,Sun 的设计师们告诉我们,这个功能事实上依赖于 ClassLoader 的 appendtoClassPathForInstrumentation 方法 —— 这是一个非公开的函数,因此我们不建议直接(使用反射等方式)使用它,事实上,instrument 包里的这两个函数已经可以很好的解决我们的问题了。



回页首

结语

从以上的介绍我们可以得出结论,在 Java SE 6 里面,instrumentation 包新增的功能 —— 虚拟机启动后的动态 instrument、本地代码(native code)instrumentation,以及动态添加 classpath 等等,使得 Java 具有了更强的动态控制、解释能力,从而让 Java 语言变得更加灵活多变。

这些能力,从某种意义上开始改变 Java 语言本身。在过去很长的一段时间内,动态 脚本语言的大量涌现和快速发展,对整个软件业和网络业提高生产率起到了非常重要的作用。在这种背景之下,Java 也正在慢慢地作出改变。而 Instrument 的新功能和 Script 平台(本系列的后面一篇中将介绍到这一点)的出现,则大大强化了语言的动态化和与动态语言融合,它是 Java 的发展的值得考量的新趋势。

参考资料

  • 阅读 Java SE 6 新特性系列 文章的完整列表,了解 Java SE 6 其它重要的增强。
  • Java SE 6 文档:Java SE 6 的规范文档,可以找到绝大部分新特性的官方说明。
  • Apache BCEL: Apache BCEL 项目,可以帮助开发者操作 class 文件,开发出功能强大的 instrumentation 代理程序
  • 阅读文章“Java 5 特性 Instrumentation 实践”:我的同事写的文章,介绍了在 Java SE 5 环境下,利用 BCEL 完成一个计时程序
           

Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=1816848





 Java SE 6 新特性系列之二 HTTP 增强     

概述

Java 语言从诞生的那天起,就非常注重网络编程方面的应用。随着互联网应用的飞速发展,Java 的基础类库也不断地对网络相关的 API 进行加强和扩展。在 Java SE 6 当中,围绕着 HTTP 协议出现了很多实用的新特性:NTLM 认证提供了一种 Window 平台下较为安全的认证机制;JDK 当中提供了一个轻量级的 HTTP 服务器;提供了较为完善的 HTTP Cookie 管理功能;更为实用的 NetworkInterface;DNS 域名的国际化支持等等。

NTLM 认证

不可避免,网络中有很多资源是被安全域保护起来的。访问这些资源需要对用户的身份进行认证。下面是一个简单的例子:

import java.net.*;
import java.io.*;

public class Test {
    
public static void main(String[] args) throws Exception {
        URL url 
= new URL("http://PROTECTED.com");
        URLConnection connection 
= url.openConnection();
        InputStream in 
= connection.getInputStream();
        
byte[] data = new byte[1024];
        
while(in.read(data)>0)
        {
            
//do something for data
        }
        in.close();
    }
}

当 Java 程序试图从一个要求认证的网站读取信息的时候,也就是说,从联系于 http://Protected.com 这个 URLConnection 的 InputStream 中 read 数据时,会引发 FileNotFoundException。尽管笔者认为,这个 Exception 的类型与实际错误发生的原因实在是相去甚远;但这个错误确实是由网络认证失败所导致的。

要解决这个问题,有两种方法:

其一,是给 URLConnection 设定一个“Authentication”属性:

String credit = USERNAME + ":" + PASSWORD;
String encoding 
= new sun.misc.BASE64Encoder().encode (credit.getBytes());
connection.setRequestProperty (
"Authorization""Basic " + encoding);

这里假设 http://PROTECTED.COM 使用了基本(Basic)认证类型。

从上面的例子,我们可以看出,设定 Authentication 属性还是比较复杂的:用户必须了解认证方式的细节,才能将用户名/密码以一定的规范给出,然后用特定的编码方式加以编码。Java 类库有没有提供一个封装了认证细节,只需要给出用户名/密码的工具呢?

这就是我们要介绍的另一种方法,使用 java.net.Authentication 类。

每当遇到网站需要认证的时候,HttpURLConnection 都会向 Authentication 类询问用户名和密码。

Authentication 类不会知道究竟用户应该使用哪个 username/password 那么用户如何向 Authentication 类提供自己的用户名和密码呢?

提供一个继承于 Authentication 的类,实现 getPasswordAuthentication 方法,在 PasswordAuthentication 中给出用户名和密码:

class DefaultAuthenticator extends Authenticator {
    
public PasswordAuthentication getPasswordAuthentication () {
        
return new PasswordAuthentication ("USER""PASSWORD".toCharArray());
    }
}

然后,将它设为默认的(全局)Authentication:

Authenticator.setDefault (new DefaultAuthenticator());

那么,不同的网站需要不同的用户名/密码又怎么办呢?

Authentication 提供了关于认证发起者的足够多的信息,让继承类根据这些信息进行判断,在 getPasswordAuthentication 方法中给出了不同的认证信息:

  • getRequestingHost()
  • getRequestingPort()
  • getRequestingPrompt()
  • getRequestingProtocol()
  • getRequestingScheme()
  • getRequestingURL()
  • getRequestingSite()
  • getRequestorType()

另一件关于 Authentication 的重要问题是认证类型。不同的认证类型需要 Authentication 执行不同的协议。至 Java SE 6.0 为止,Authentication 支持的认证方式有:

  • HTTP Basic authentication
  • HTTP Digest authentication
  • NTLM
  • Http SPNEGO Negotiate
    • Kerberos
    • NTLM

这里我们着重介绍 NTLM。

NTLM 是 NT LAN Manager 的缩写。早期的 SMB 协议在网络上明文传输口令,这是很不安全的。微软随后提出了 WindowsNT 挑战/响应验证机制,即 NTLM。

NTLM 协议是这样的:

  1. 客户端首先将用户的密码加密成为密码散列;
  2. 客户端向服务器发送自己的用户名,这个用户名是用明文直接传输的;
  3. 服务器产生一个 16 位的随机数字发送给客户端,作为一个 challenge(挑战) ;
  4. 客户端用步骤1得到的密码散列来加密这个 challenge ,然后把这个返回给服务器;
  5. 服务器把用户名、给客户端的 challenge 、客户端返回的 response 这三个东西,发送域控制器 ;
  6. 域控制器用这个用户名在 SAM 密码管理库中找到这个用户的密码散列,然后使用这个密码散列来加密 challenge;
  7. 域控制器比较两次加密的 challenge ,如果一样,那么认证成功;

Java 6 以前的版本,是不支持 NTLM 认证的。用户若想使用 HttpConnection 连接到一个使用有 Windows 域保护的网站时,是无法通过 NTLM 认证的。另一种方法,是用户自己用 Socket 这样的底层单元实现整个协议过程,这无疑是十分复杂的。

终于,Java 6 的 Authentication 类提供了对 NTLM 的支持。使用十分方便,就像其他的认证协议一样:

class DefaultAuthenticator extends Authenticator {
    
private static String username = "username ";
    
private static String domain =  "domain ";
    
private static String password =  "password ";
   
    
public PasswordAuthentication getPasswordAuthentication() {
        String usernamewithdomain 
= domain + ""+username;
        
return (new PasswordAuthentication(usernamewithdomain, password.toCharArray()));
    }
}

这里,根据 Windows 域账户的命名规范,账户名为域名+”/”+域用户名。如果不想每生成 PasswordAuthentication 时,每次添加域名,可以设定一个系统变量名“http.auth.ntlm.domain“。

Java 6 中 Authentication 的另一个特性是认证协商。目前的服务器一般同时提供几种认证协议,根据客户端的不同能力,协商出一种认证方式。比如,IIS 服务器会同时提供 NTLM with kerberos 和 NTLM 两种认证方式,当客户端不支持 NTLM with kerberos 时,执行 NTLM 认证。

目前,Authentication 的默认协商次序是:

GSS/SPNEGO -> Digest -> NTLM -> Basic

那么 kerberos 的位置究竟在哪里呢?

事实上,GSS/SPNEGO 以 JAAS 为基石,而后者实际上就是使用 kerberos 的。

轻量级 HTTP 服务器

Java 6 还提供了一个轻量级的纯 Java Http 服务器的实现。下面是一个简单的例子:

public static void main(String[] args) throws Exception{
    HttpServerProvider httpServerProvider 
= HttpServerProvider.provider();
    InetSocketAddress addr 
= new InetSocketAddress(7778);
    HttpServer httpServer 
= httpServerProvider.createHttpServer(addr, 1);
    httpServer.createContext(
"/myapp/"new MyHttpHandler());
    httpServer.setExecutor(
null);
    httpServer.start();
    System.out.println(
"started");
}

static class MyHttpHandler implements HttpHandler{
    
public void handle(HttpExchange httpExchange) throws IOException {          
        String response 
= "Hello world!";
        httpExchange.sendResponseHeaders(
200, response.length());
        OutputStream out 
= httpExchange.getResponseBody();
        out.write(response.getBytes());
        out.close();
    }  
}

然后,在浏览器中访问 http://localhost:7778/myapp/,我们得到:
图一 浏览器显示

首先,HttpServer 是从 HttpProvider 处得到的,这里我们使用了 JDK 6 提供的实现。用户也可以自行实现一个 HttpProvider 和相应的 HttpServer 实现。

其次,HttpServer 是有上下文(context)的概念的。比如,http://localhost:7778/myapp/ 中“/myapp/”就是相对于 HttpServer Root 的上下文。对于每个上下文,都有一个 HttpHandler 来接收 http 请求并给出回答。

最后,在 HttpHandler 给出具体回答之前,一般先要返回一个 Http head。这里使用 HttpExchange.sendResponseHeaders(int code, int length)。其中 code 是 Http 响应的返回值,比如那个著名的 404。length 指的是 response 的长度,以字节为单位。

Cookie 管理特性

Cookie 是 Web 应用当中非常常用的一种技术, 用于储存某些特定的用户信息。虽然,我们不能把一些特别敏感的信息存放在 Cookie 里面,但是,Cookie 依然可以帮助我们储存一些琐碎的信息,帮助 Web 用户在访问网页时获得更好的体验,例如个人的搜索参数,颜色偏好以及上次的访问时间等等。网络程序开发者可以利用 Cookie 来创建有状态的网络会话(Stateful Session)。 Cookie 的应用越来越普遍。在 Windows 里面,我们可以在“Documents And Settings”文件夹里面找到IE使用的 Cookie,假设用户名为 admin,那么在 admin 文件夹的 Cookies 文件夹里面,我们可以看到名为“admin@(domain)”的一些文件,其中的 domain 就是表示创建这些 Cookie 文件的网络域, 文件里面就储存着用户的一些信息。

JavaScript 等脚本语言对 Cookie 有着很不错的支持。 .NET 里面也有相关的类来支持开发者对 Cookie 的管理。 不过,在 Java SE 6 之前, Java一直都没有提供 Cookie 管理的功能。在 Java SE 5 里面, java.net 包里面有一个 CookieHandler 抽象类,不过并没有提供其他具体的实现。到了 Java SE 6, Cookie 相关的管理类在 Java 类库里面才得到了实现。有了这些 Cookie 相关支持的类,Java 开发者可以在服务器端编程中很好的操作 Cookie, 更好的支持 HTTP 相关应用,创建有状态的 HTTP 会话。

  • 用 HttpCookie 代表 Cookie

    java.net.HttpCookie 类是 Java SE 6 新增的一个表示 HTTP Cookie 的新类, 其对象可以表示 Cookie 的内容, 可以支持所有三种 Cookie 规范:

    • Netscape 草案
    • RFC 2109 - http://www.ietf.org/rfc/rfc2109.txt
    • RFC 2965 - http://www.ietf.org/rfc/rfc2965.txt

    这个类储存了 Cookie 的名称,路径,值,协议版本号,是否过期,网络域,最大生命期等等信息。

  • 用 CookiePolicy 规定 Cookie 接受策略

    java.net.CookiePolicy 接口可以规定 Cookie 的接受策略。 其中唯一的方法用来判断某一特定的 Cookie 是否能被某一特定的地址所接受。 这个类内置了 3 个实现的子类。一个类接受所有的 Cookie,另一个则拒绝所有,还有一个类则接受所有来自原地址的 Cookie。

  • 用CookieStore 储存 Cookie

    java.net.CookieStore 接口负责储存和取出 Cookie。 当有 HTTP 请求的时候,它便储存那些被接受的 Cookie; 当有 HTTP 回应的时候,它便取出相应的 Cookie。 另外,当一个 Cookie 过期的时候,它还负责自动删去这个 Cookie。

  • 用 CookieManger/CookieHandler 管理 Cookie

    java.net.CookieManager 是整个 Cookie 管理机制的核心,它是 CookieHandler 的默认实现子类。下图显示了整个 HTTP Cookie 管理机制的结构:

    图 2. Cookie 管理类的关系

    一个 CookieManager 里面有一个 CookieStore 和一个 CookiePolicy,分别负责储存 Cookie 和规定策略。用户可以指定两者,也可以使用系统默认的 CookieManger。

  • 例子

    下面这个简单的例子说明了 Cookie 相关的管理功能:

    // 创建一个默认的 CookieManager
    CookieManager manager = new CookieManager();

    // 将规则改掉,接受所有的 Cookie
    manager.setCookiePolicy(CookiePolicy.ACCEPT_ALL);

    // 保存这个定制的 CookieManager
    CookieHandler.setDefault(manager);
            
    // 接受 HTTP 请求的时候,得到和保存新的 Cookie
    HttpCookie cookie = new HttpCookie("...(name)...","...(value)...");
    manager.getCookieStore().add(uri, cookie);
            
    // 使用 Cookie 的时候:
    // 取出 CookieStore        
    CookieStore store = manager.getCookieStore();

    // 得到所有的 URI        
    List<URI> uris = store.getURIs();
    for (URI uri : uris) {
        
    // 筛选需要的 URI
        
    // 得到属于这个 URI 的所有 Cookie
        List<HttpCookie> cookies = store.get(uri);
        
    for (HttpCookie cookie : cookies) {
            
    // 取出了 Cookie
        }
    }
            
    // 或者,取出这个 CookieStore 里面的全部 Cookie
    // 过期的 Cookie 将会被自动删除
    List<HttpCookie> cookies = store.getCookies();
    for (HttpCookie cookie : cookies) {
        
    // 取出了 Cookie
    }

其他新特性

NetworkInterface 的增强

从 Java SE 1.4 开始,JDK 当中出现了一个网络工具类 java.net.NetworkInterface,提供了一些网络的实用功能。 在 Java SE 6 当中,这个工具类得到了很大的加强,新增了很多实用的方法。例如:

  • public boolean isUp()

    用来判断网络接口是否启动并运行

  • public boolean isLoopback()

    用来判断网络接口是否是环回接口(loopback)

  • public boolean isPointToPoint()

    用来判断网络接口是否是点对点(P2P)网络

  • public boolean supportsMulticast()

    用来判断网络接口是否支持多播

  • public byte[] getHardwareAddress()

    用来得到硬件地址(MAC)

  • public int getMTU()

    用来得到最大传输单位(MTU,Maximum Transmission Unit)

  • public boolean isVirtual()

    用来判断网络接口是否是虚拟接口

关于此工具类的具体信息,请参考 Java SE 6 相应文档(见 参考资源)。

域名的国际化

在最近的一些 RFC 文档当中,规定 DNS 服务器可以解析除开 ASCII 以外的编码字符。有一个算法可以在这种情况下做 Unicode 与 ASCII 码之间的转换,实现域名的国际化。java.net.IDN 就是实现这个国际化域名转换的新类,IDN 是“国际化域名”的缩写(internationalized domain names)。这个类很简单,主要包括 4 个静态函数,做字符的转换。

结语

Java SE 6 有着很多 HTTP 相关的新特性,使得 Java SE 平台本身对网络编程,尤其是基于 HTTP 协议的因特网编程,有了更加强大的支持。 

Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=1816852




 Java SE 6 新特性系列之三 JMX 与系统管理     

前言

在 Java 程序的运行过程中,对 JVM 和系统的监测一直是 Java 开发人员在开发过程所需要的。一直以来,Java 开发人员必须通过一些底层的 JVM API,比如 JVMPI 和 JVMTI 等,才能监测 Java 程序运行过程中的 JVM 和系统的一系列情况,这种方式一直以来被人所诟病,因为这需要大量的 C 程序和 JNI 调用,开发效率十分低下。于是出现了各种不同的专门做资源管理的程序包。为了解决这个问题,Sun 公司也在其 Java SE 5 版本中,正式提出了 Java 管理扩展(Java Management Extensions,JMX)用来管理检测 Java 程序(同时 JMX 也在 J2EE 1.4 中被发布)。

JMX 的提出,让 JDK 中开发自检测程序成为可能,也提供了大量轻量级的检测 JVM 和运行中对象/线程的方式,从而提高了 Java 语言自己的管理监测能力。



回页首

JMX 和系统管理

管理系统(Management System)

要了解 JMX,我们就必须对当前的 IT 管理系统有一个初步的了解。随着企业 IT 规模的不断增长,IT 资源(IT resource)数量不断增加,IT 资源的分布也越来越分散。可以想象,甚至对于一家只有几百台 PC 公司的 IT 管理人员来说,分发一个安全补丁并且保证其在每台 PC 上的安装,如果只依赖人工来完成那简直就是一场噩梦。这样,IT 管理系统就应运而生。

然而,CPU、网卡、存储阵列是 IT 资源;OS、MS Office、Oracle database、IBM Websphere 也是 IT 资源。IT 管理系统若要对这些 IT 资源进行管理,就必须对这些管理对象有所了解:形形色色的 IT 资源就像是说着不同语言的人:Oralce 数据库表达内存紧张的方式和 Window XP 是绝然不同的, 而 IT 管理系统就像建造通天塔的经理,必须精通所有的语言, 这几乎是一个不可能完成的任务。难道 IT 管理系统是另外一个通天塔吗?当然不是!其实我们只要给每个 IT 资源配个翻译就可以了。

管理系统的构架
图 1. 管理系统构架

上图分析了管理系统的基本构架模式。其中 Agent / SubAgent 起到的就是翻译的作用:把 IT 资源报告的消息以管理系统能理解的方式传送出去。

也许读者有会问,为什么需要 Agent 和 SubAgent 两层体系呢?这里有两个现实的原因:

  1. 管理系统一般是一个中央控制的控制软件,而 SubAgent 直接监控一些资源,往往和这些资源分布在同一物理位置。当这些 SubAgent 把状态信息传输到管理系统或者传达管理系统的控制指令的时候,需要提供一些网络传输的功能。
  2. 管理系统的消息是有一定规范的,消息的翻译本身是件复杂而枯燥的事情。

一般来说,管理系统会将同一物理分布或者功能类似的 SubAgent 分组成一组,由一个共用的 Agent 加以管理。在这个 Agent 里封装了 1 和 2 的功能。

JMX 和管理系统

JMX 既是 Java 管理系统的一个标准,一个规范,也是一个接口,一个框架。图 2 展示了 JMX 的基本架构。
图 2. JMX 构架

和其它的资源系统一样,JMX 是管理系统和资源之间的一个接口,它定义了管理系统和资源之间交互的标准。javax.management.MBeanServer 实现了 Agent 的功能,以标准的方式给出了管理系统访问 JMX 框架的接口。而 javax.management.MBeans 实现了 SubAgent 的功能,以标准的方式给出了 JMX 框架访问资源的接口。而从类库的层次上看,JMX 包括了核心类库 java.lang.managementjavax.management 包。java.lang.management 包提供了基本的 VM 监控功能,而 javax.management 包则向用户提供了扩展功能。



回页首

JMX 的基本框架

JMX 使用了 Java Bean 模式来传递信息。一般说来,JMX 使用有名的 MBean,其内部包含了数据信息,这些信息可能是:应用程序配置信息、模块信息、系统信息、统计信息等。另外,MBean 也可以设立可读写的属性、直接操作某些函数甚至启动 MBean 可发送的 notification 等。MBean 包括 Standard,MXBean,Dynamic,Model,Open 等几种分类,其中最简单是标准 MBean 和 MXBean,而我们使用得最多的也是这两种。MXBean 主要是 java.lang.management 使用较多,将在下一节中介绍。我们先了解其他一些重要的 MBean 的种类。

标准 MBean

标准 MBean 是最简单的一类 MBean,与动态 Bean 不同,它并不实现 javax.management 包中的特殊的接口。说它是标准 MBean, 是因为其向外部公开其接口的方法和普通的 Java Bean 相同,是通过 lexical,或者说 coding convention 进行的。下面我们就用一个例子来展现,如何实现一个标准 MBean 来监控某个服务器 ServerImpl 状态的。ServerImpl 代表了用来演示的某个 Server 的实现:

package standardbeans;
public class ServerImpl {
    
public final long startTime;
    
public ServerImpl() {
        startTime 
= System.currentTimeMillis();
    }
}

然后,我们打算使用一个标准 MBean,ServerMonitor 来监控 ServerImpl:

package standardbeans;
public class ServerMonitor implements ServerMonitorMBean {
    
private final ServerImpl target;
    
public ServerMonitor(ServerImpl target){
        
this.target = target;
    }
    
public long getUpTime(){
        
return System.currentTimeMillis() - target.startTime;
    }
}

这里的 ServerMonitorBean 又是怎么回事呢?MXBean 规定了标准 MBean 也要实现一个接口,所有向外界公开的方法都要在这个接口中声明。否则,管理系统就不能从中获得相应的信息。此外,该接口的名字也有一定的规范:即在标准 MBean 类名之后加上“MBean”后缀。若 MBean 的类名叫做 MBeansName 的话,对应的接口就要叫做 MBeansNameMBean。

对于管理系统来说,这些在 MBean 中公开的方法,最终会被 JMX 转化成属性(Attribute)、监听(Listener)和调用(Invoke)的概念。如果读者对 Java Bean 有一些了解的话,不难看出,public long getUpTime() 对应了 Bean 中的一个称为“upTime”的只读属性。

下面我们就看一个模拟管理系统的例子:

package standardbeans;
import javax.management.MBeanServer;
import javax.management.MBeanServerFactory;
import javax.management.ObjectName;
public class Main {
    
private static ObjectName objectName ;
    
private static MBeanServer mBeanServer;
    
public static void main(String[] args) throws Exception{
        init();
        manage();               
    }
    
private static void init() throws Exception{
        ServerImpl serverImpl 
= new ServerImpl();
        ServerMonitor serverMonitor 
= new ServerMonitor(serverImpl);
        mBeanServer 
= MBeanServerFactory.createMBeanServer();
        objectName 
= new ObjectName("objectName:id=ServerMonitor1");
        mBeanServer.registerMBean(serverMonitor,objectName);  
    }
    
private static void manage() throws Exception{
        Long upTime 
= (Long) mBeanServer.getAttribute(objectName,
        
"upTime");
        System.out.println(upTime);
    } 
}

JMX 的核心是 MBServer。Java SE 已经提供了一个默认实现,可以通过 MBServerFactory.createMBeanServer() 获得。每个资源监控者(MBean)一般都会有名称(ObjectName), 登记在 MBServer 内部的一个 Repository 中。注意,这个 ObjectName 对于每一个 MBServer 必须是唯一的,只能对应于一个 MBean。(读者有兴趣的话,可以试着再给 mBeanServer 注册一个同名的 objectName,看看会怎么样。)上述例子是在 init() 方法中完成向 MBeanServer 注册工作的。

在管理过程中,管理系统并不与资源或者 SubAgent 直接打交道,也就是说,这里不会直接引用到 MBean。而是通过 MBeanServer 的 getAttribute 方法取得对应 MBean 的属性的。

动态 MBean

但是对于很多已有的 SubAgent 实现,其 Coding Convention 并不符合标准 MBean 的要求。重构所有这些 SubAgent 以符合标准 MBean 标准既费力也不实际。JMX 中给出了动态(Dynamic) MBean 的概念,MBServer 不再依据 Coding Convention 而是直接查询动态 MBean 给出的元数据(meta data)以获得 MBean 的对外接口。

package dynamicbeans;

import javax.management.*;
import java.lang.reflect.*;
public class ServerMonitor implements DynamicMBean {
 
    
private final ServerImpl target;    
    
private MBeanInfo mBeanInfo;    
        
    
public ServerMonitor(ServerImpl target){
        
this.target = target;
    }
    
    
//实现获取被管理的 ServerImpl 的 upTime
    public long upTime(){
        
return System.currentTimeMillis() - target.startTime;
    }

    
//javax.management.MBeanServer 会通过查询 getAttribute("Uptime") 获得 "Uptime" 属性值
    public Object getAttribute(String attribute) throws AttributeNotFoundException, 
        MBeanException, ReflectionException {
        
if(attribute.equals("UpTime")){
            
return upTime();
        }
        
return null;
    }
    
    
//给出 ServerMonitor 的元信息。  
    public MBeanInfo getMBeanInfo() {
        
if (mBeanInfo == null) {
            
try {
                Class cls 
= this.getClass();
                
//用反射获得 "upTime" 属性的读方法
                Method readMethod = cls.getMethod("upTime"new Class[0]); 
                
//用反射获得构造方法
                Constructor constructor = cls.getConstructor(new Class[]
                    {ServerImpl.
class});
                
//关于 "upTime" 属性的元信息:名称为 UpTime,只读属性(没有写方法)。
                MBeanAttributeInfo upTimeMBeanAttributeInfo = new MBeanAttributeInfo(
                        
"UpTime""The time span since server start",
                        readMethod, 
null);
                
//关于构造函数的元信息
                MBeanConstructorInfo mBeanConstructorInfo = new MBeanConstructorInfo(
                        
"Constructor for ServerMonitor", constructor);
                
//ServerMonitor 的元信息,为了简单起见,在这个例子里,
                
//没有提供 invocation 以及 listener 方面的元信息 
                mBeanInfo = new MBeanInfo(cls.getName(),
                        
"Monitor that controls the server",
                        
new MBeanAttributeInfo[] { upTimeMBeanAttributeInfo },
                        
new MBeanConstructorInfo[] { mBeanConstructorInfo },
                        
nullnull);                
            } 
catch (Exception e) {
                
throw new Error(e);
            }

        }
        
return mBeanInfo;
    }

    
public AttributeList getAttributes(String[] arg0) {        
        
return null;
    }
        
    
public Object invoke(String arg0, Object[] arg1, String[] arg2) 
        
throws MBeanException, 
        ReflectionException {        
        
return null;
    }

    
public void setAttribute(Attribute arg0) throws AttributeNotFoundException, 
        InvalidAttributeValueException, MBeanException, ReflectionException {
        
return;        
    }

    
public AttributeList setAttributes(AttributeList arg0) {        
        
return null;
    }   
}

其它动态 MBean

另外还有两类 MBean:Open MBean 和 Model MBean。实际上它们也都是动态 MBean。

Open MBean 与其它动态 MBean 的唯一区别在于,前者对其公开接口的参数和返回值有所限制 —— 只能是基本类型或者 javax.management.openmbean 包内的 ArrayType、CompositeType、TarbularType 等类型。这主要是考虑到管理系统的分布,很可能远端管理系统甚至 MBServer 层都不具有 MBean 接口中特殊的类。

Model Bean

然而,普通的动态 Bean 通常缺乏一些管理系统所需要的支持:比如持久化 MBean 的状态、日志记录、缓存等等。如果让用户去一一实现这些功能确实是件枯燥无聊的工作。为了减轻用户的负担,JMX 提供商都会提供不同的 ModelBean 实现。其中有一个接口是 Java 规范中规定所有厂商必须实现的:javax.management.modelmbean.RequiredModelBean。通过配置 Descriptor 信息,我们可以定制这个 Model Bean, 指定哪些 MBean 状态需要记入日志、如何记录以及是否缓存某些属性、缓存多久等等。这里,我们以 RequiredModelBean 为例讨论 ModelBean。比如,我们先来看一个例子,首先是 server 端:

package modelmbean;

public class Server {

    
private long startTime;
    
    
public Server() {    }
    
    
public int start(){
        startTime 
= System.currentTimeMillis();
        
return 0;
    }
    
    
public long getUpTime(){
        
return System.currentTimeMillis() - startTime;
    }
}

然后我们对它的监测如下:

package modelmbean;

import javax.management.*;
import javax.management.modelmbean.*;
public class Main {
    
    
public static void main(String[] args) throws Exception{
        MBeanServer mBeanServer 
= MBeanServerFactory.createMBeanServer();
        RequiredModelMBean serverMBean 
=
            (RequiredModelMBean) mBeanServer.instantiate(
              
"javax.management.modelmbean.RequiredModelMBean");        
        
        ObjectName serverMBeanName 
=
            
new ObjectName("server: id=Server");
        serverMBean.setModelMBeanInfo(getModelMBeanInfoForServer(serverMBeanName));        
        Server server 
= new Server();
        serverMBean.setManagedResource(server, 
"ObjectReference");
        
        ObjectInstance registeredServerMBean 
=
            mBeanServer.registerMBean((Object) serverMBean, serverMBeanName);
        
        serverMBean.invoke(
"start",nullnull);
        
        Thread.sleep(
1000);
        
        System.out.println(serverMBean.getAttribute(
"upTime"));
        Thread.sleep(
5000);
        System.out.println(serverMBean.getAttribute(
"upTime"));
    }
    
    
private static ModelMBeanInfo getModelMBeanInfoForServer(ObjectName objectName) 
        
throws Exception{
        ModelMBeanAttributeInfo[] serverAttributes 
=
              
new ModelMBeanAttributeInfo[1];
        Descriptor upTime 
=
              
new DescriptorSupport(
                
new String[] {
                  
"name=upTime",
                  
"descriptorType=attribute",
                  
"displayName=Server upTime",
                  
"getMethod=getUpTime",                  
                   });
        serverAttributes[
0=
              
new ModelMBeanAttributeInfo(
                
"upTime",
                
"long",
                
"Server upTime",
                
true,
                
false,
                
false,
                upTime);
        
        ModelMBeanOperationInfo[] serverOperations 
=
              
new ModelMBeanOperationInfo[2];
        
        Descriptor getUpTimeDesc 
=
              
new DescriptorSupport(
                
new String[] {
                  
"name=getUpTime",
                  
"descriptorType=operation",
                  
"class=modelmbean.Server",
                  
"role=operation"                  
                  });
        
        MBeanParameterInfo[] getUpTimeParms 
= new MBeanParameterInfo[0];
        serverOperations[
0= new ModelMBeanOperationInfo("getUpTime",
                  
"get the up time of the server",
                  getUpTimeParms,
                  
"java.lang.Long",
                  MBeanOperationInfo.ACTION,
                  getUpTimeDesc);
            
        Descriptor startDesc 
=
              
new DescriptorSupport(
                
new String[] {
                  
"name=start",
                  
"descriptorType=operation",
                  
"class=modelmbean.Server",
                  
"role=operation"
                  });
        MBeanParameterInfo[] startParms 
= new MBeanParameterInfo[0];
        serverOperations[
1= new ModelMBeanOperationInfo("start",
                  
"start(): start server",
                  startParms,
                  
"java.lang.Integer",
                  MBeanOperationInfo.ACTION,
                  startDesc);
        
        ModelMBeanInfo serverMMBeanInfo 
=
              
new ModelMBeanInfoSupport(
                
"modelmbean.Server",
                
"ModelMBean for managing an Server",
                serverAttributes,
                
null,
                serverOperations,
                
null);
        
        
//Default strategy for the MBean.
        Descriptor serverDescription =
              
new DescriptorSupport(
                
new String[] {
                  (
"name=" + objectName),
                  
"descriptorType=mbean",
                  (
"displayName=Server"),
                  
"type=modelmbean.Server",
                  
"log=T",
                  
"logFile=serverMX.log",
                  
"currencyTimeLimit=10" });
        serverMMBeanInfo.setMBeanDescriptor(serverDescription);
       
return serverMMBeanInfo;
      }

很明显,和其它 MBean 类似,使用 Model MBean 的过程也是下面几步:

  1. 创建一个 MBServer:mBeanServe
  2. 获得管理资源用的 MBean:serverBean
  3. 给这个 MBean 一个 ObjectName:serverMBeanName
  4. 将 serverBean 以 serverMBeanName 注册到 mBeanServer 上去

唯一不同的是,ModelMBean 需要额外两步:

1.serverMBean.setModelMBeanInfo(getModelMBeanInfoForServer(serverMBeanName));
2.serverMBean.setManagedResource(server, "ObjectReference");

第一步用于提供 serverMBean 的元数据,主要包括以下两类

  1. 类似于普通的动态 MBean,需要 MBean 的 Attribute、Invocation、Notification 的类型/反射信息,诸如返回类型、参数类型和相关的 get/set 方法等。这里将不再赘述。
  2. 关于缓存、持久化以及日志等的策略。后面我们将介绍一些这方面的信息。

第二步指出了 ServerMBean 管理的对象,也就是说,从元数据中得到的 Method 将施加在哪个 Object 上。需要指出的是 setManagedResource(Object o, String type); 中第二个参数是 Object 类型,可以是 "ObjectReference"、"Handle"、"IOR"、"EJBHandle" 或 "RMIReference"。目前 SE 中的实现只支持 "ObjectReference"。笔者认为后面几种类型是为了将来 JMX 管理对象扩展而设定的,可能将来 Model Bean 不仅可以管理 Plain Java Object(POJO),还可能管理 Native Resource, 并给诸如 EJB 和 RMI 对象的管理提供更多的特性。

Model Bean 与普通动态 Bean 区别在于它的元数据类型 ModelMBeanInfo 扩展了前者的 MBeanInfo,使得 ModelMBeanOperationInfo、ModelMBeanConstructor_Info、ModelMBeanAttributeInfo 和 ModelMBeanNotificationInfo 都有一个额外的元数据:javax.management.Descriptor,它是用来设定 Model Bean 策略的。数据的存储是典型的 "key-value" 键值对。不同的 Model Bean 实现,以及不同的 MBeanFeatureInfo 支持不同的策略特性。下面我们就以 Attribute 为例,看一下 RequiredModelBean 支持的策略。

首先,它最重要的 Descriptor 主要是 name、displayName 和 descriptorType,其中 name 是属性名称。"name" 要与对应 ModelMBeanAttributeInfo 的 name 相同。descriptorType 必须是 "attribute"。

另外,value、default、legalValues "value" 是用来设定初始值的,"default" 指当不能从 resource 中获得该属性时的默认返回值,"legalValues" 是一组合法的属性数据。它并不用来保证 setAttribute 的数据一致性,而是在 UI 系统,如 JConsole 中提示用户可能的数据输入。

在属性访问的 getMethod, setMethod 方法上,事实上所有对属性的访问都会被 delegate 给同一 MBeanInfo 中特定的 Operation。 getMethod/setMethod 给出了对应的 ModelMBeanOperationInfo 名称。

还有一些额外的属性,比如:persistPolicy, persistPeriod 是代表了持久化策略;currencyTimeLimit, lastUpdatedTimeStamp 缓存策略;iterable 属性是否必须使用 iterate 来访问。默认为否;protocolMap 定义了与第三方系统有关的数据转换的 data model;visibility 定义了与第三方 UI 系统有关的 MBean 如何显示的策略;presentationString 也是定义了与第三方 UI 系统有关的 MBean 如何显示策略,比如 "presentation=server.gif"。

事实上,策略特性有两个层次的作用域:整个 Model Bean 和特定的 MBeanFeature。

Model Bean 的策略描述会被施加到该 Model Bean 的所有 MBeanFeature 上去,除非该 MBeanFeature 重写了这个策略特性。

在上面的例子里,这一个语句:

serverMMBeanInfo.setMBeanDescriptor(serverDescription);

给整个 serverMBeanInfo 设了一个策略描述 serverDescription,其中用 "currencyTimeLimit=10" 指出属性的缓存时间是 10 秒。所以,在 Main 方法中,两次 serverMBean.getAttribute("upTime");之间的间隔小于 10 秒就会得到同样的缓存值。

如果我们不想让 "upTime" 这个属性被缓存,我们可以在它的策略描述中加入 "currencyTimeLimit=-1":

Descriptor upTime =    new DescriptorSupport(
                
new String[] {
                  
"name=upTime",
                  
"descriptorType=attribute",
                  
"displayName=Server upTime",
                  
"getMethod=getUpTime",
                  
"currencyTimeLimit=-1" //不需要缓存
                   });

Descriptor getUpTimeDesc 
=
              
new DescriptorSupport(
                
new String[] {
                  
"name=getUpTime",
                  
"descriptorType=operation",
                  
"class=modelmbean.Server",
                  
"role=operation"
                  ,
"currencyTimeLimit=-1" //不需要缓存
              });

getUpTimeDesc 也要改动的原因是 RequiredModelBean 会把获取 upTime 属性的工作 delegate 给 getUpTime invocation。只要其中一处使用 MBean 级的缓存策略,就没法获得实时 upTime 数据了。



回页首

虚拟机检测

JMX 与虚拟机检测

JMX 的提出,为 Java 虚拟机提供了 Java 层上的检测机制。J2SE 中,新提出的 java.lang.management 包即是 JMX 在 JDK 的一个应用,它提供了大量的有用的接口,通过 MBean 方式,提供了对 Java 虚拟机和运行时远端的监控和检测方式,来帮助用户来检测本地或者远端的虚拟机的运行情况。有了 JMX 之后,我们可以设计一个客户端,来检测远端一个正在运行的虚拟机中的线程数、线程当前的 Stack、内存管理、GC 所占用的时间、虚拟机中的对象和当前虚拟机参数等重要的参数和运行时信息。JMX 另外的一个重要功能是对配置信息的检测和再配置。比如,我们可以在远端查看和修改当前 JVM 的 verbose 参数,以达到动态管理的目的。甚至,我们可以在远端指挥 JVM 做一次 GC,这在下文中有详细介绍。

JMX 提供的虚拟机检测 API

检测虚拟机当前的状态总是 Java 开放人员所关心的,也正是因为如此,出现了大量的 profiler 工具来检测当前的虚拟机状态。从 Java SE 5 之后,在 JDK 中,我们有了一些 Java 的虚拟机检测 API,即 java.lang.management 包。Management 包里面包括了许多 MXBean 的接口类和 LockInfo、MemoryUsage、MonitorInfo 和 ThreadInfo 等类。从名字可以看出,该包提供了虚拟机内存分配、垃圾收集(GC)情况、操作系统层、线程调度和共享锁,甚至编译情况的检测机制。这样一来,Java 的开发人员就可以很简单地为自己做一些轻量级的系统检测,来确定当前程序的各种状态,以便随时调整。

要获得这些信息,我们首先通过 java.lang.management.ManagementFactory 这个工厂类来获得一系列的 MXBean。包括:

  • ClassLoadingMXBean

    ClassLoadMXBean 包括一些类的装载信息,比如有多少类已经装载/卸载(unloaded),虚拟机类装载的 verbose 选项(即命令行中的 Java –verbose:class 选项)是否打开,还可以帮助用户打开/关闭该选项。

  • CompilationMXBean

    CompilationMXBean 帮助用户了解当前的编译器和编译情况,该 mxbean 提供的信息不多。

  • GarbageCollectorMXBean

    相对于开放人员对 GC 的关注程度来说,该 mxbean 提供的信息十分有限,仅仅提供了 GC 的次数和 GC 花费总时间的近似值。但是这个包中还提供了三个的内存管理检测类:MemoryManagerMXBean,MemoryMXBean 和 MemoryPoolMXBean。

    • MemoryManagerMXBean

      这个类相对简单,提供了内存管理类和内存池(memory pool)的名字信息。

    • MemoryMXBean

      这个类提供了整个虚拟机中内存的使用情况,包括 Java 堆(heap)和非 Java 堆所占用的内存,提供当前等待 finalize 的对象数量,它甚至可以做 gc(实际上是调用 System.gc)。

    • MemoryPoolMXBean

      该信息提供了大量的信息。在 JVM 中,可能有几个内存池,因此有对应的内存池信息,因此,在工厂类中,getMemoryPoolMXBean() 得到是一个 MemoryPoolMXBean 的 list。每一个 MemoryPoolMXBean 都包含了该内存池的详细信息,如是否可用、当前已使用内存/最大使用内存值、以及设置最大内存值等等。

  • OperatingSystemMXBean

    该类提供的是操作系统的简单信息,如构架名称、当前 CPU 数、最近系统负载等。

  • RuntimeMXBean

    运行时信息包括当前虚拟机的名称、提供商、版本号,以及 classpath、bootclasspath 和系统参数等等。

  • ThreadMXBean

    在 Java 这个多线程的系统中,对线程的监控是相当重要的。ThreadMXBean 就是起到这个作用。ThreadMXBean 可以提供的信息包括各个线程的各种状态,CPU 占用情况,以及整个系统中的线程状况。从 ThreadMXBean 可以得到某一个线程的 ThreadInfo 对象。这个对象中则包含了这个线程的所有信息。

java.lang.management 和虚拟机的关系

我们知道,management 和底层虚拟机的关系是非常紧密的。其实,有一些的是直接依靠虚拟机提供的公开 API 实现的,比如 JVMTI;而另外一些则不然,很大一块都是由虚拟机底层提供某些不公开的 API / Native Code 提供的。这样的设计方式,保证了 management 包可以提供足够的信息,并且使这些信息的提供又有足够的效率;也使 management 包和底层的联系非常紧密。



回页首

Java 6 中的 API 改进

Management 在 Java SE 5 被提出之后,受到了欢迎。在 Java 6 当中,这个包提供更多的 API 来更好地提供信息。

OperatingSystemMXBean. getSystemLoadAverage()

Java 程序通常关注是虚拟机内部的负载、内存等状况,而不考虑整个系统的状况。但是很多情况下,Java 程序在运行过程中,整个计算机系统的系统负荷情况也会对虚拟机造成一定的影响。随着 Java 的发展,Java 程序已经覆盖了各个行业,这一点也必须得到关注。在以前,利用 Native 代码来检测系统负载往往是唯一的选择,但是在 Java 6 当中,JDK 自己提供了一个轻量级的系统负载检测 API,即 OperatingSystemMXBean.getSystemLoadAverage()

当然这个 API 事实上仅仅返回一个对前一分钟系统负载的简单的估测。它设计的主要目标是简单快速地估测当前系统负荷,因此它首先保证了这个 API 的效率是非常高的;也因为如此,这个 API 事实上并不适用于所有的系统。

锁检测

我们知道,同步是 Java 语言很重要的一个特性。在 Java SE 中,最主要的同步机制是依靠 synchronize 关键字对某一个对象加锁实现的;在 Java SE 5 之后的版本中,concurrent 包的加入,大大强化了 Java 语言的同步能力,concurrent 提供了很多不同类型的锁机制可供扩展。因此,要更好地观测当前的虚拟机状况和不同线程的运行态,去观察虚拟机中的各种锁,以及线程与锁的关系是非常必要的。很可惜的是,在过去的 JDK 中,我们并没有非常方便的 API 以供使用。一个比较直接的检测方式是查看线程的 stack trace,更为强大全面(但是也更复杂并且效率低下)的方案是得到一个 VM 所有对象的快照并查找之,这些策略的代价都比较大,而且往往需要编写复杂的 Native 代码。

JDK 6 里提供了一些相当简单的 API 来提供这个服务。首先了解两个新类,LockInfo 和 MonitorInfo 这两个类承载了锁的信息。LockInfo 可以是任何的 Java 锁,包括简单 Java 锁和 java.util.concurrent 包中所使用的锁(包括 AbstractOwnableSynchronizer 和 Condition 的实现类/子类),而 MonitorInfo 是简单的 Java 对象所代表的锁。要检测一个线程所拥有的锁和等待的锁,首先,要得到一个线程的 ThreadInfo,然后可以简单地调用:

  • getLockedMonitors()

    返回一个所有当前线程已经掌握的锁对象的列表。

  • getLockedSynchronizers()

    对于使用 concurrent 包的线程,返回一个该线程所掌握的“ownable synchronizer”(即 AbstractOwnableSynchronizer 及其子类)所组成的列表。

  • getLockInfo()

    当前线程正在等待的那个锁对象的信息就可以知道线程所有的锁信息。通过这些锁信息,我们很方便的可以知道当前虚拟机的所有线程的锁信息。由此,我们还可以推导出更多的信息。

死锁检测

死锁检测一直以来是软件工程师所重视的,显然一个死锁的系统永远是工程师最大的梦魇。Java 程序的死锁检测也一直以来是 Java 程序员所头痛的。为了解决线程间死锁问题,一般都有预防(代码实现阶段)和死锁后恢复(运行时)两种方式。以前 Java 程序员都重视前者,因为在运行态再来检测和恢复系统是相当麻烦的,缺少许多必要的信息;但是,对于一些比较复杂的系统,采取后者或者运行时调试死锁信息也是非常重要的。由上面所说,现在我们已经可以知道每一个线程所拥有和等待的锁,因此要计算出当前系统中是否有死锁的线程也是可行的了。当然,Java 6 里面也提供了一个 API 来完成这个功能,即:

  • ThreadMXBean.findDeadlockedThreads()

    这个函数的功能就是检测出当前系统中已经死锁的线程。当然,这个功能复杂,因此比较费时。基本上仅仅将之用于调试,以便对复杂系统线程调用的改进。





回页首

未来的发展

JMX 在 Java SE 5/6 中的功能已经相当强大,但是距离 Java 程序开发人员的要求还是有一段距离,因此 Sun 公司已经向 JCP 提出了 JSR 255 (JMX API 2.0 版本)来扩充和进一步发展 JMX,并希望这个 JSR 将在 Java SE 7 中实现。在这个文档中,新的 JMX 2.0 将着重于:

  • 对 management 模型的优化,并提供更好的支持;加入了比如 annotation 等等的新特性;
  • 对 JMX 定义的优化,在进一步强化 MBean 扩充性好的优点的同时,尽量改变(用户普遍认为的)MBean 很难实现的缺点;
  • 对非 Java 平台客户端的支持。这将是一个令人振奋的新特性;

具体的扩展可能包括:

  • 层次性的命名域(Hierarchical namespace);
  • 新的事件服务功能;
  • 对 locales 的新支持;
  • 为 MBean 启用 annotation 服务;
  • 也可以使用用户类型的 mapping 了;

可以看到,JMX 的进一步发展主要关注的是可扩展性、动态性和易用性等 Java 用户非常关注的方面。



回页首

总结

在 Java SE 5 出现的 JMX 在 Java SE 6 中有了更多的功能和扩展能力,这很好地适应了 Java 语言的发展和用户的要求,让 Java 的监测、管理的的功能更加强大。 

Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=1816856




 Java SE 6 新特性系列之四编译器 API     

新 API 功能简介

JDK 6 提供了在运行时调用编译器的 API,后面我们将假设把此 API 应用在 JSP 技术中。在传统的 JSP 技术中,服务器处理 JSP 通常需要进行下面 6 个步骤:

  1. 分析 JSP 代码;
  2. 生成 Java 代码;
  3. 将 Java 代码写入存储器;
  4. 启动另外一个进程并运行编译器编译 Java 代码;
  5. 将类文件写入存储器;
  6. 服务器读入类文件并运行;

但如果采用运行时编译,可以同时简化步骤 4 和 5,节约新进程的开销和写入存储器的输出开销,提高系统效率。实际上,在 JDK 5 中,Sun 也提供了调用编译器的编程接口。然而不同的是,老版本的编程接口并不是标准 API 的一部分,而是作为 Sun 的专有实现提供的,而新版则带来了标准化的优点。

新 API 的第二个新特性是可以编译抽象文件,理论上是任何形式的对象 —— 只要该对象实现了特定的接口。有了这个特性,上述例子中的步骤 3 也可以省略。整个 JSP 的编译运行在一个进程中完成,同时消除额外的输入输出操作。

第三个新特性是可以收集编译时的诊断信息。作为对前两个新特性的补充,它可以使开发人员轻松的输出必要的编译错误或者是警告信息,从而省去了很多重定向的麻烦。



回页首

运行时编译 Java 文件

在 JDK 6 中,类库通过 javax.tools 包提供了程序运行时调用编译器的 API。从这个包的名字 tools 可以看出,这个开发包提供的功能并不仅仅限于编译器。工具还包括 javah、jar、pack200 等,它们都是 JDK 提供的命令行工具。这个开发包希望通过实现一个统一的接口,可以在运行时调用这些工具。在 JDK 6 中,编译器被给予了特别的重视。针对编译器,JDK 设计了两个接口,分别是 JavaCompilerJavaCompiler.CompilationTask

下面给出一个例子,展示如何在运行时调用编译器。

  • 指定编译文件名称(该文件必须在 CLASSPATH 中可以找到):String fullQuanlifiedFileName = "compile" + java.io.File.separator +"Target.java";
  • 获得编译器对象: JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();

通过调用 ToolProvidergetSystemJavaCompiler 方法,JDK 提供了将当前平台的编译器映射到内存中的一个对象。这样使用者可以在运行时操纵编译器。JavaCompiler 是一个接口,它继承了 javax.tools.Tool 接口。因此,第三方实现的编译器,只要符合规范就能通过统一的接口调用。同时,tools 开发包希望对所有的工具提供统一的运行时调用接口。相信将来,ToolProvider 类将会为更多地工具提供 getSystemXXXTool 方法。tools 开发包实际为多种不同工具、不同实现的共存提供了框架。

  • 编译文件:int result = compiler.run(null, null, null, fileToCompile);

获得编译器对象之后,可以调用 Tool.run 方法对源文件进行编译。Run 方法的前三个参数,分别可以用来重定向标准输入、标准输出和标准错误输出,null 值表示使用默认值。清单 1 给出了一个完整的例子:
清单 1. 程序运行时编译文件

01 package compile;
02 import java.util.Date;
03 public class Target {
04   public void doSomething(){
05     Date date = new Date(1033); 
         
// 这个构造函数被标记为deprecated, 编译时会
         
// 向错误输出输出信息。
06     System.out.println("Doing...");
07   }
08 }

09 package compile;
10 import javax.tools.*;
11 import java.io.FileOutputStream;
12 public class Compiler {
13   public static void main(String[] args) throws Exception{
14     String fullQuanlifiedFileName = "compile" + java.io.File.separator +
             
"Target.java";     
15     JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();

16     FileOutputStream err = new FileOutputStream("err.txt");

17     int compilationResult = compiler.run(nullnull, err, fullQuanlifiedFileName);

18     if(compilationResult == 0){
19       System.out.println("Done");
20     } else {
21       System.out.println("Fail");
22     }
23   }
24 }
   

首先运行 <JDK60_INSTALLATION_DIR>/bin/javac Compiler.java,然后运行 <JDK60_INSTALLATION_DIR>/jdk1.6.0/bin/java compile.Compiler。屏幕上将输出 Done ,并会在当前目录生成一个 err.txt 文件,文件内容如下:

Note: compile/Target.java uses or overrides a deprecated API.Note: Recompile with -Xlint:deprecation for details.

仔细观察 run 方法,可以发现最后一个参数是 String...arguments,是一个变长的字符串数组。它的实际作用是接受传递给 javac 的参数。假设要编译 Target.java 文件,并显示编译过程中的详细信息。命令行为:javac Target.java -verbose。相应的可以将 17 句改为:

int compilationResult = compiler.run(null, null, err, “-verbose”,fullQuanlifiedFileName);





回页首

编译非文本形式的文件

JDK 6 的编译器 API 的另外一个强大之处在于,它可以编译的源文件的形式并不局限于文本文件。JavaCompiler 类依靠文件管理服务可以编译多种形式的源文件。比如直接由内存中的字符串构造的文件,或者是从数据库中取出的文件。这种服务是由 JavaFileManager 类提供的。通常的编译过程分为以下几个步骤:

  1. 解析 javac 的参数;
  2. 在 source path 和/或 CLASSPATH 中查找源文件或者 jar 包;
  3. 处理输入,输出文件;

在这个过程中,JavaFileManager 类可以起到创建输出文件,读入并缓存输出文件的作用。由于它可以读入并缓存输入文件,这就使得读入各种形式的输入文件成为可能。JDK 提供的命令行工具,处理机制也大致相似,在未来的版本中,其它的工具处理各种形式的源文件也成为可能。为此,新的 JDK 定义了 javax.tools.FileObjectjavax.tools.JavaFileObject 接口。任何类,只要实现了这个接口,就可以被 JavaFileManager 识别。

如果要使用 JavaFileManager,就必须构造 CompilationTask。JDK 6 提供了 JavaCompiler.CompilationTask 类来封装一个编译操作。这个类可以通过:

JavaCompiler.getTask (
    Writer out, 
    JavaFileManager fileManager,
    DiagnosticListener
<? super JavaFileObject> diagnosticListener,
    Iterable
<String> options,
    Iterable
<String> classes,
    Iterable
<? extends JavaFileObject> compilationUnits
)

方法得到。关于每个参数的含义,请参见 JDK 文档。传递不同的参数,会得到不同的 CompilationTask。通过构造这个类,一个编译过程可以被分成多步。进一步,CompilationTask 提供了 setProcessors(Iterable<? extends Processor>processors) 方法,用户可以制定处理 annotation 的处理器。图 1 展示了通过 CompilationTask 进行编译的过程:
图 1. 使用 CompilationTask 进行编译

下面的例子通过构造 CompilationTask 分多步编译一组 Java 源文件。
清单 2. 构造 CompilationTask 进行编译

01 package math;

02 public class Calculator {
03     public int multiply(int multiplicand, int multiplier) {
04         return multiplicand * multiplier;
05     }
06 }

07 package compile;
08 import javax.tools.*;
09 import java.io.FileOutputStream;
10 import java.util.Arrays;
11 public class Compiler {
12   public static void main(String[] args) throws Exception{
13     String fullQuanlifiedFileName = "math" + java.io.File.separator +"Calculator.java";
14     JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
15     StandardJavaFileManager fileManager  =
           compiler.getStandardFileManager(
nullnullnull);

16     Iterable<? extends JavaFileObject> files =
             fileManager.getJavaFileObjectsFromStrings(
             Arrays.asList(fullQuanlifiedFileName));
17     JavaCompiler.CompilationTask task = compiler.getTask(
             
null, fileManager, nullnullnull, files);

18     Boolean result = task.call();
19     if( result == true ) {
20       System.out.println("Succeeded");
21     }
22   }
23 }

以上是第一步,通过构造一个 CompilationTask 编译了一个 Java 文件。14-17 行实现了主要逻辑。第 14 行,首先取得一个编译器对象。由于仅仅需要编译普通文件,因此第 15 行中通过编译器对象取得了一个标准文件管理器。16 行,将需要编译的文件构造成了一个 Iterable 对象。最后将文件管理器和 Iterable 对象传递给 JavaCompilergetTask 方法,取得了 JavaCompiler.CompilationTask 对象。

接下来第二步,开发者希望生成 Calculator 的一个测试类,而不是手工编写。使用 compiler API,可以将内存中的一段字符串,编译成一个 CLASS 文件。
清单 3. 定制 JavaFileObject 对象

01 package math;
02 import java.net.URI;
03 public class StringObject extends SimpleJavaFileObject{
04     private String contents = null;
05     public StringObject(String className, String contents) throws Exception{
06         super(new URI(className), Kind.SOURCE);
07         this.contents = contents;
08     }

09     public CharSequence getCharContent(boolean ignoreEncodingErrors) 
             
throws IOException {
10         return contents;
11     }
12 }

SimpleJavaFileObjectJavaFileObject 的子类,它提供了默认的实现。继承 SimpleJavaObject 之后,只需要实现 getCharContent 方法。如 清单 3 中的 9-11 行所示。接下来,在内存中构造 Calculator 的测试类 CalculatorTest,并将代表该类的字符串放置到 StringObject 中,传递给 JavaCompilergetTask 方法。清单 4 展现了这些步骤。
清单 4. 编译非文本形式的源文件

01 package math;
02 import javax.tools.*;
03 import java.io.FileOutputStream;
04 import java.util.Arrays;
05 public class AdvancedCompiler {
06   public static void main(String[] args) throws Exception{

07     // Steps used to compile Calculator
08     // Steps used to compile StringObject

09     // construct CalculatorTest in memory
10     JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
11     StandardJavaFileManager fileManager  =
           compiler.getStandardFileManager(
nullnullnull);
12         JavaFileObject file = constructTestor();
13         Iterable<? extends JavaFileObject> files = Arrays.asList(file);
14         JavaCompiler.CompilationTask task = compiler.getTask (
                 
null, fileManager, nullnullnull, files);

15         Boolean result = task.call();
16         if( result == true ) {
17           System.out.println("Succeeded");
18         }
19   }

20   private static SimpleJavaFileObject constructTestor() {
21     StringBuilder contents = new StringBuilder(
           
"package math;" +
           
"class CalculatorTest { " +
             
"  public void testMultiply() { " +
           
"    Calculator c = new Calculator(); " +
           
"    System.out.println(c.multiply(2, 4)); " +
           
"  } " +
           
"  public static void main(String[] args) { " +
           
"    CalculatorTest ct = new CalculatorTest(); " +
           
"    ct.testMultiply(); " +
           
"  } " +
           
"} ");
22      StringObject so = null;
23      try {
24        so = new StringObject("math.CalculatorTest", contents.toString());
25      } catch(Exception exception) {
26        exception.printStackTrace();
27      }
28      return so;
29    }
30 }

实现逻辑和 清单 2 相似。不同的是在 20-30 行,程序在内存中构造了 CalculatorTest 类,并且通过 StringObject 的构造函数,将内存中的字符串,转换成了 JavaFileObject 对象。



回页首

采集编译器的诊断信息

第三个新增加的功能,是收集编译过程中的诊断信息。诊断信息,通常指错误、警告或是编译过程中的详尽输出。JDK 6 通过 Listener 机制,获取这些信息。如果要注册一个 DiagnosticListener,必须使用 CompilationTask 来进行编译,因为 Tool 的 run 方法没有办法注册 Listener。步骤很简单,先构造一个 Listener,然后传递给 JavaFileManager 的构造函数。清单 5清单 2 进行了改动,展示了如何注册一个 DiagnosticListener
清单 5. 注册一个 DiagnosticListener 收集编译信息

01 package math;

02 public class Calculator {
03   public int multiply(int multiplicand, int multiplier) {
04     return multiplicand * multiplier 
         
// deliberately omit semicolon, ADiagnosticListener 
         
// will take effect
05   }
06 }

07 package compile;
08 import javax.tools.*;
09 import java.io.FileOutputStream;
10 import java.util.Arrays;
11 public class CompilerWithListener {
12   public static void main(String[] args) throws Exception{
13     String fullQuanlifiedFileName = "math" + 
           java.io.File.separator 
+"Calculator.java";
14     JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
15     StandardJavaFileManager fileManager  =
           compiler.getStandardFileManager(
nullnullnull);

16     Iterable<? extends JavaFileObject> files =
           fileManager.getJavaFileObjectsFromStrings(
           Arrays.asList(fullQuanlifiedFileName));
17       DiagnosticCollector<JavaFileObject> collector =
           
new DiagnosticCollector<JavaFileObject>(); 
18       JavaCompiler.CompilationTask task = 
           compiler.getTask(
null, fileManager, collector, nullnull, files);

19     Boolean result = task.call();
20     List<Diagnostic<? extends JavaFileObject>> diagnostics = 
           collector.getDiagnostics();
21     for(Diagnostic<? extends JavaFileObject> d : diagnostics){
22         System.out.println("Line Number->" + d.getLineNumber());
23           System.out.println("Message->"+ 
                   d.getMessage(Locale.ENGLISH));
24           System.out.println("Source" + d.getCode());
25           System.out.println(" ");
26     }

27     if( result == true ) {
28       System.out.println("Succeeded");
29     }
30   }
31 }

在 17 行,构造了一个 DiagnosticCollector 对象,这个对象由 JDK 提供,它实现了 DiagnosticListener 接口。18 行将它注册到 CompilationTask 中去。一个编译过程可能有多个诊断信息。每一个诊断信息,被抽象为一个 Diagnostic。20-26 行,将所有的诊断信息逐个输出。编译并运行 Compiler,得到以下输出:
清单 6. DiagnosticCollector 收集的编译信息

Line Number->5Message->math/Calculator.java:5: ';' expectedSource->compiler.err.expected

实际上,也可以由用户自己定制。清单 7 给出了一个定制的 Listener
清单 7. 自定义的 DiagnosticListener

01 class ADiagnosticListener implements DiagnosticListener<JavaFileObject>{
02      public void report(Diagnostic<? extends JavaFileObject> diagnostic) {
03       System.out.println("Line Number->" + diagnostic.getLineNumber());
04       System.out.println("Message->"+ diagnostic.getMessage(Locale.ENGLISH));
05       System.out.println("Source" + diagnostic.getCode());
06       System.out.println(" ");
07     }
08 }





回页首

总结

JDK 6 的编译器新特性,使得开发者可以更自如的控制编译的过程,这给了工具开发者更加灵活的自由度。通过 API 的调用完成编译操作的特性,使得开发者可以更方便、高效地将编译变为软件系统运行时的服务。而编译更广泛形式的源代码,则为整合更多的数据源及功能提供了强大的支持。相信随着 JDK 的不断完善,更多的工具将具有 API 支持,我们拭目以待。 

Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=1816863



 Java SE 6 新特性系列之五 Java DB 和 JDBC 4.0     

长久以来,由于大量(甚至几乎所有)的 Java 应用都依赖于数据库,如何使用 Java 语言高效、可靠、简洁地访问数据库一直是程序员们津津乐道的话题。新发布的 Java SE 6 也在这方面更上层楼,为编程人员提供了许多好用的新特性。其中最显著的,莫过于 Java SE 6 拥有了一个内嵌的 100% 用 Java 语言编写的数据库系统。并且,Java 6 开始支持 JDBC 4.0 的一系列新功能和属性。这样,Java SE 在对持久数据的访问上就显得更为易用和强大了。

Java DB:Java 6 里的数据库

新安装了 JDK 6 的程序员们也许会发现,除了传统的 bin、jre 等目录,JDK 6 新增了一个名为 db 的目录。这便是 Java 6 的新成员:Java DB。这是一个纯 Java 实现、开源的数据库管理系统(DBMS),源于 Apache 软件基金会(ASF)名下的项目 Derby。它只有 2MB 大小,对比动辄上 G 的数据库来说可谓袖珍。但这并不妨碍 Derby 功能齐备,支持几乎大部分的数据库应用所需要的特性。更难能可贵的是,依托于 ASF 强大的社区力量,Derby 得到了包括 IBM 和 Sun 等大公司以及全世界优秀程序员们的支持。这也难怪 Sun 公司会选择其 10.2.2 版本纳入到 JDK 6 中,作为内嵌的数据库。这就好像为 JDK 注入了一股全新的活力:Java 程序员不再需要耗费大量精力安装和配置数据库,就能进行安全、易用、标准、并且免费的数据库编程。在这一章中,我们将初窥 Java DB 的世界,来探究如何使用它编写出功能丰富的程序。

Hello, Java DB:内嵌模式的 Derby

既然有了内嵌(embedded)的数据库,就让我们从一个简单的范例(代码在 清单 1 中列出)开始,试着使用它吧。这个程序做了大多数数据库应用都可能会做的操作:在 DBMS 中创建了一个名为 helloDB 的数据库;创建了一张数据表,取名为 hellotable;向表内插入了两条数据;然后,查询数据并将结果打印在控制台上;最后,删除表和数据库,释放资源。
清单 1. HelloJavaDB 的代码

                
public class HelloJavaDB {
    
public static void main(String[] args) {
        
try { // load the driver
            Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance();
            System.out.println(
"Load the embedded driver");
            Connection conn 
= null;
            Properties props 
= new Properties();
            props.put(
"user""user1");  props.put("password""user1");
           
//create and connect the database named helloDB 
            conn=DriverManager.getConnection("jdbc:derby:helloDB;create=true", props);
            System.out.println(
"create and connect to helloDB");
            conn.setAutoCommit(
false);

            
// create a table and insert two records
            Statement s = conn.createStatement();
            s.execute(
"create table hellotable(name varchar(40), score int)");
            System.out.println(
"Created table hellotable");
            s.execute(
"insert into hellotable values('Ruth Cao', 86)");
            s.execute(
"insert into hellotable values ('Flora Shi', 92)");
            
// list the two records
            ResultSet rs = s.executeQuery(
                
"SELECT name, score FROM hellotable ORDER BY score");
            System.out.println(
"name score");
            
while(rs.next()) {
                StringBuilder builder 
= new StringBuilder(rs.getString(1));
                builder.append(
" ");
                builder.append(rs.getInt(
2));
                System.out.println(builder.toString());
            }
            
// delete the table
            s.execute("drop table hellotable");
            System.out.println(
"Dropped table hellotable");
            
            rs.close();
            s.close();
            System.out.println(
"Closed result set and statement");
            conn.commit();
            conn.close();
            System.out.println(
"Committed transaction and closed connection");
            
            
try { // perform a clean shutdown 
                DriverManager.getConnection("jdbc:derby:;shutdown=true");
            } 
catch (SQLException se) {
                System.out.println(
"Database shut down normally");
            }
        } 
catch (Throwable e) {
            
// handle the exception
        }
        System.out.println(
"SimpleApp finished");
    }
}

随后,我们在命令行(本例为 Windows 平台,当然,其它系统下稍作改动即可)下键入以下命令:
清单 2. 运行 HelloJavaDB 命令

                java –cp .;%JAVA_HOME%/db/lib/derby.jar HelloJavaDB

程序将会按照我们预想的那样执行,图 1 是执行结果的一部分截屏:
图 1. HelloJavaDB 程序的执行结果

上述的程序和以往没什么区别。不同的是我们不需要再为 DBMS 的配置而劳神,因为 Derby 已经自动地在当前目录下新建了一个名为 helloDB 的目录,来物理地存储数据和日志。需要做的只是注意命名问题:在内嵌模式下驱动的名字应为 org.apache.derby.jdbc.EmbeddedDriver;创建一个新数据库时需要在协议后加入 create=true。另外,关闭所有数据库以及 Derby 的引擎可以使用以下代码:
清单 3. 关闭所有数据库及 Derby 引擎

                DriverManager.getConnection("jdbc:derby:;shutdown=true");

如果只想关闭一个数据库,那么则可以调用:
清单 4. 关闭一个数据库

                DriverManager.getConnection("jdbc:derby:helloDB;shutdown=true ");

这样,使用嵌入模式的 Derby 维护和管理数据库的成本接近于 0。这对于希望专心写代码的人来说不失为一个好消息。然而有人不禁要问:既然有了内嵌模式,为什么大多数的 DBMS 都没有采取这样的模式呢?不妨做一个小实验。当我们同时在两个命令行窗口下运行 HelloJavaDB 程序。结果一个的结果与刚才一致,而另一个却出现了错误,如 图 2 所示。
图 2. 内嵌模式的局限

错误的原因其实很简单:在使用内嵌模式时,Derby 本身并不会在一个独立的进程中,而是和应用程序一起在同一个 Java 虚拟机(JVM)里运行。因此,Derby 如同应用所使用的其它 jar 文件一样变成了应用的一部分。这就不难理解为什么在 classpath 中加入 derby 的 jar 文件,我们的示例程序就能够顺利运行了。这也说明了只有一个 JVM 能够启动数据库:而两个跑在不同 JVM 实例里的应用自然就不能够访问同一个数据库了。

鉴于上述的局限性,和来自不同 JVM 的多个连接想访问一个数据库的需求,下一节将介绍 Derby 的另一种模式:网络服务器(Network Server)。

网络服务器模式

如上所述,网络服务器模式是一种更为传统的客户端/服务器模式。我们需要启动一个 Derby 的网络服务器用于处理客户端的请求,不论这些请求是来自同一个 JVM 实例,还是来自于网络上的另一台机器。同时,客户端使用 DRDA(Distributed Relational Database Architecture)协议连接到服务器端。这是一个由 The Open Group 倡导的数据库交互标准。图 3 说明了该模式的大体结构。

由于 Derby 的开发者们努力使得网络服务器模式与内嵌模式之间的差异变小,使得我们只需简单地修改 清单 1 中的程序就可以实现。如 清单 5所示,我们在 HelloJavaDB 中增添了一个新的函数和一些字符串变量。不难看出,新的代码只是将一些在上一节中特别指出的字符串进行了更改:驱动类为 org.apache.derby.jdbc.ClientDriver,而连接数据库的协议则变成了 jdbc:derby://localhost:1527/。这是一个类似 URL 的字符串,而事实上,Derby 网络的客户端的连接格式为:jdbc:derby://server[:port]/databaseName[;attributeKey=value]。在这个例子中,我们使用了最简单的本地机器作为服务器,而端口则是 Derby 默认的 1527 端口。
图 3. Derby 网络服务器模式架构


清单 5. 网络服务器模式下的 HelloJavaDB

                
public class HelloJavaDB {
    
public static String driver = "org.apache.derby.jdbc.EmbeddedDriver";
    
public static String protocol = "jdbc:derby:";

    
public static void main(String[] args) {
        
// same as before
    }
    
private static void parseArguments(String[] args) {
        
if (args.length == 0 || args.length > 1) {
            
return;
        }
        
if (args[0].equalsIgnoreCase("derbyclient")) {
            framework 
= "derbyclient";
            driver 
= "org.apache.derby.jdbc.ClientDriver";
            protocol 
= "jdbc:derby://localhost:1527/";
        }
    }
}

当然,仅仅有客户端是不够的,我们还需要启动网络服务器。Derby 中控制网络服务器的类是 org.apache.derby.drda.NetworkServerControl,因此键入以下命令即可。如果想了解 NetworkServerControl 更多的选项,只要把 start 参数去掉就可以看到帮助信息了。关于网络服务器端的实现,都被 Derby 包含在 derbynet.jar 里。
清单 6. 启动网络服务器

                java -cp .;"C:/Program Files/Java/jdk1.6.0/db/lib/derby.jar";"C:/Program Files/Java/jdk1.6.0/db/lib/derbynet.jar" org.apache.derby.drda.NetworkServerControl start

相对应的,网络客户端的实现被包含在 derbyclient.jar 中。所以,只需要在 classpath 中加入该 jar 文件,修改后的客户端就可以顺利地读取数据了。再一次尝试着使用两个命令行窗口去连接数据库,就能够得到正确的结果了。如果不再需要服务器,那么使用 NetworkServerControl 的 shutdown 参数就能够关闭服务器。

更多

至此,文章介绍了 Java SE 6 中的新成员:Java DB(Derby),也介绍了如何在内嵌模式以及网络服务器模式下使用 Java DB。当然这只是浅尝辄止,更多高级的选项还需要在 Sun 和 Derby 的文档中寻找。在这一章的最后,我们将简单介绍几个 Java DB 的小工具来加快开发速度。它们都位于 org.apache.derby.tools 包内,在开发过程中需要获取信息或者测试可以用到。

  • ij:一个用来运行 SQL 脚本的工具;
  • dblook:为 Derby 数据库作模式提取(Schema extraction),生成 DDL 的工具;
  • sysinfo:显示系统以及 Derby 信息的工具类;




回页首

JDBC 4.0:新功能,新 API

如果说上一章介绍了 Java 6 中的一个新成员,它本来就存在,但是没有被加入进 JDK。那么这一章,我们将关注在 JDBC 4.0 中又增加了哪些新功能以及与之相对应的新 API。

自动加载驱动

在 JDBC 4.0 之前,编写 JDBC 程序都需要加上以下这句有点丑陋的代码:
清单 7. 注册 JDBC 驱动

                Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance();

Java.sql.DriverManager 的内部实现机制决定了这样代码的出现。只有先通过 Class.forName 找到特定驱动的 class 文件,DriverManager.getConnection 方法才能顺利地获得 Java 应用和数据库的连接。这样的代码为编写程序增加了不必要的负担,JDK 的开发者也意识到了这一点。从 Java 6 开始,应用程序不再需要显式地加载驱动程序了,DriverManager 开始能够自动地承担这项任务。作为试验,我们可以将 清单 1 中的相关代码删除,重新编译后在 JRE 6.0 下运行,结果和原先的程序一样。

好奇的读者也许会问,DriverManager 为什么能够做到自动加载呢?这就要归功于一种被称为 Service Provider 的新机制。熟悉 Java 安全编程的程序员可能对其已经是司空见惯,而它现在又出现在 JDBC 模块中。JDBC 4.0 的规范规定,所有 JDBC 4.0 的驱动 jar 文件必须包含一个 java.sql.Driver,它位于 jar 文件的 META-INF/services 目录下。这个文件里每一行便描述了一个对应的驱动类。其实,编写这个文件的方式和编写一个只有关键字(key)而没有值(value)的 properties 文件类似。同样地,‘#’之后的文字被认为是注释。有了这样的描述,DriverManager 就可以从当前在 CLASSPATH 中的驱动文件中找到,它应该去加载哪些类。而如果我们在 CLASSPATH 里没有任何 JDBC 4.0 的驱动文件的情况下,调用 清单 8 中的代码会输出一个 sun.jdbc.odbc.JdbcOdbcDriver 类型的对象。而仔细浏览 JDK 6 的目录,这个类型正是在 %JAVA_HOME%/jre/lib/resources.jar 的 META-INF/services 目录下的 java.sql.Driver 文件中描述的。也就是说,这是 JDK 中默认的驱动。而如果开发人员想使得自己的驱动也能够被 DriverManager 找到,只需要将对应的 jar 文件加入到 CLASSPATH 中就可以了。当然,对于那些 JDBC 4.0 之前的驱动文件,我们还是只能显式地去加载了。
清单 8. 罗列本地机器上的 JDBC 驱动

                
Enumeration<Driver> drivers = DriverManager.getDrivers();

while(drivers.hasMoreElements()) {
    System.out.println(drivers.nextElement());
}

RowId

熟悉 DB2、Oracle 等大型 DBMS 的人一定不会对 ROWID 这个概念陌生:它是数据表中一个“隐藏”的列,是每一行独一无二的标识,表明这一行的物理或者逻辑位置。由于 ROWID 类型的广泛使用,Java SE 6 中新增了 java.sql.RowId 的数据类型,允许 JDBC 程序能够访问 SQL 中的 ROWID 类型。诚然,不是所有的 DBMS 都支持 ROWID 类型。即使支持,不同的 ROWID 也会有不同的生命周期。因此使用 DatabaseMetaData.getRowIdLifetime 来判断类型的生命周期不失为一项良好的实践经验。我们在 清单 1 的程序获得连接之后增加以下代码,便可以了解 ROWID 类型的支持情况。
清单 9. 了解 ROWID 类型的支持情况

                
DatabaseMetaData meta = conn.getMetaData();
System.out.println(meta.getRowIdLifetime());

Java SE 6 的 API 规范中,java.sql.RowIdLifetime 规定了 5 种不同的生命周期:ROWID_UNSUPPORTEDROWID_VALID_FOREVERROWID_VALID_OTHERROWID_VALID_SESSIONROWID_VALID_TRANSACTION。从字面上不难理解它们表示了不支持 ROWID、ROWID 永远有效等等。具体的信息,还可以参看相关的 JavaDoc。读者可以尝试着连接 Derby 进行试验,会发现运行结果是 ROWID_UNSUPPORTED ,即 Derby 并不支持 ROWID。

既然提供了新的数据类型,那么一些相应的获取、更新数据表内容的新 API 也在 Java 6 中被添加进来。和其它已有的类型一样,在得到 ResultSet 或者 CallableStatement 之后,调用 get/set/update 方法得到/设置/更新 RowId 对象,示例的代码如 清单 10 所示。
清单 10. 获得/设置 RowId 对象

                
// Initialize a PreparedStatement
PreparedStatement pstmt = connection.prepareStatement(
    
"SELECT rowid, name, score FROM hellotable WHERE rowid = ?");
// Bind rowid into prepared statement. 
pstmt.setRowId(1, rowid);
// Execute the statement
ResultSet rset = pstmt.executeQuery(); 
// List the records
while(rs.next()) {
     RowId id 
= rs.getRowId(1); // get the immutable rowid object
     String name = rs.getString(2);
      
int score = rs.getInt(3);
}

鉴于不同 DBMS 的不同实现,RowID 对象通常在不同的数据源(datasource)之间并不是可移植的。因此 JDBC 4.0 的 API 规范并不建议从连接 A 取出一个 RowID 对象,将它用在连接 B 中,以避免不同系统的差异而带来的难以解释的错误。而至于像 Derby 这样不支持 RowId 的 DBMS,程序将直接在 setRowId 方法处抛出 SQLFeatureNotSupportedException

SQLXML

SQL:2003 标准引入了 SQL/XML,作为 SQL 标准的扩展。SQL/XML 定义了 SQL 语言怎样和 XML 交互:如何创建 XML 数据;如何在 SQL 语句中嵌入 XQuery 表达式等等。作为 JDBC 4.0 的一部分,Java 6 增加了 java.sql.SQLXML 的类型。JDBC 应用程序可以利用该类型初始化、读取、存储 XML 数据。java.sql.Connection.createSQLXML 方法就可以创建一个空白的 SQLXML 对象。当获得这个对象之后,便可以利用 setStringsetBinaryStreamsetCharacterStream 或者 setResult 等方法来初始化所表示的 XML 数据。以 setCharacterStream 为例,清单 11 表示了一个 SQLXML 对象如何获取 java.io.Writer 对象,从外部的 XML 文件中逐行读取内容,从而完成初始化。
清单 11. 利用 setCharacterStream 方法来初始化 SQLXML 对象

                
SQLXML xml = con.createSQLXML();
Writer writer 
= xml.setCharacterStream();
BufferedReader reader 
= new BufferedReader(new FileReader("test.xml"));
String line
= null;
while((line = reader.readLine() != null) {
      writer.write(line);

由于 SQLXML 对象有可能与各种外部的资源有联系,并且在一个事务中一直持有这些资源。为了防止应用程序耗尽资源,Java 6 提供了 free 方法来释放其资源。类似的设计在 java.sql.ArrayClob 中都有出现。

至于如何使用 SQLXML 与数据库进行交互,其方法与其它的类型都十分相似。可以参照 RowId 一节 中的例子在 Java SE 6 的 API 规范中找到 SQLXML 中对应的 get/set/update 方法构建类似的程序,此处不再赘述。

SQLExcpetion 的增强

在 Java SE 6 之前,有关 JDBC 的异常类型不超过 10 个。这似乎已经不足以描述日渐复杂的数据库异常情况。因此,Java SE 6 的设计人员对以 java.sql.SQLException 为根的异常体系作了大幅度的改进。首先,SQLException 新实现了 Iterable<Throwable> 接口。清单 12 实现了 清单 1 程序的异常处理机制。这样简洁地遍历了每一个 SQLException 和它潜在的原因(cause)。
清单 12. SQLException 的 for-each loop

                
// Java 6 code
catch (Throwable e) {
   
if (e instanceof SQLException) {
       
for(Throwable ex : (SQLException)e ){
            System.err.println(ex.toString());
        }
    }

此外,图 4 表示了全部的 SQLException 异常体系。除去原有的 SQLException 的子类,Java 6 中新增的异常类被分为 3 种:SQLReoverableExceptionSQLNonTransientExceptionSQLTransientException。在 SQLNonTransientExceptionSQLTransientException 之下还有若干子类,详细地区分了 JDBC 程序中可能出现的各种错误情况。大多数子类都会有对应的标准 SQLState 值,很好地将 SQL 标准和 Java 6 类库结合在一起。
图 4. SQLException 异常体系

在众多的异常类中,比较常见的有 SQLFeatureNotSupportedException,用来表示 JDBC 驱动不支持某项 JDBC 的特性。例如在 Derby 下运行 清单 10 中的程序,就可以发现 Derby 的驱动并不支持 RowId 的特性。另外值得一提的是,SQLClientInfoException 直接继承自 SQLException,表示当一些客户端的属性不能被设置在一个数据库连接时所发生的异常。



回页首

小结:更多新特性与展望

在本文中,我们已经向读者介绍了 Java SE 6 中 JDBC 最重要的一些新特性:它们包括嵌在 JDK 中的 Java DB (Derby)和 JDBC 4.0 的一部分。当然,还有很多本文还没有覆盖到的新特性。比如增加了对 SQL 语言中 NCHARNVARCHARLONGNVARCHARNCLOB 类型的支持;在数据库连接池的环境下为管理 Statement 对象提供更多灵活、便利的方法等。

此外,在 Java SE 6 的 beta 版中,曾经将 Annotation Query 的特性包含进来。这项特性定义了一系列 Query 和 DataSet 接口,程序员可以通过撰写一些 Annotation 来自定义查询并获得定制的数据集结果。但是,由于这一特性的参考实现最终不能满足 JDK 的质量需求,Sun 公司忍痛割爱,取消了在 Java SE 6 中发布其的计划。我们有理由相信,在以后的 JDK 版本中,这一特性以及更多新的功能将被包含进来,利用 Java 语言构建数据库的应用也会变得更为自然、顺畅。 

Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=1816870