Dennis M. Sosnoski (dms@), 總裁, Sosnoski Software Solutions, Inc.
2004 年 3 月 15 日
Java 顧問 Dennis Sosnoski 在他的關(guān)于 Javassist 框架的三期文章中將精華部分留在了最后,。這次他展現(xiàn)了 Javassist 對(duì)搜索-替換的支持是如何使對(duì) Java 字節(jié)碼的編輯變得像文本編輯器的“替換所有(Replace All )”命令一樣容易的,。想報(bào)告所有寫入特定字段的內(nèi)容或者對(duì)方法調(diào)用中參數(shù)的更改中的補(bǔ)丁嗎,?Javassist 使這變得很容易,,Dennis 向您展示了其做法,。
本系列的 第 4 部分和 第 5 部分討論了如何用 Javassist 對(duì)二進(jìn)制類進(jìn)行局部更改,。這次您將學(xué)習(xí)以一種更強(qiáng)大的方式使用該框架,,從而充分利用 Javassist 對(duì)在字節(jié)碼中查找所有特定方法或者字段的支持。對(duì)于 Javassist 功能而言,,這個(gè)功能至少與它以類似源代碼的方式指定字節(jié)碼的能力同樣重要,。對(duì)選擇替換操作的支持也有助于使 Javasssist 成為一個(gè)在標(biāo)準(zhǔn) Java 代碼中增加面向方面的編程功能的絕好工具。
第 5 部分介紹了 Javassist 是如何讓您攔截類加載過程的 ―― 甚至在二進(jìn)制類表示正在被加載的時(shí)候?qū)λ鼈冞M(jìn)行更改,。這篇文章中討論的系統(tǒng)字節(jié)碼轉(zhuǎn)換可以用于靜態(tài)類文件轉(zhuǎn)換,,也可以用于運(yùn)行時(shí)攔截,但是在運(yùn)行時(shí)使用尤其有用,。
處理字節(jié)碼修改
Javassist 提供了兩種不同的系統(tǒng)字節(jié)碼修改的處理方法,。第一種技術(shù)是使用 javassist.CodeConverter 類,使用起來要稍微簡(jiǎn)單一些,,但是可以完成的任務(wù)有很多限制,。第二種技術(shù)使用 javassist.ExprEditor 類的自定義子類,它稍微復(fù)雜一些,但是所增加的靈活性足以抵銷所付出的努力,。在本文中我將分析這兩種方法的例子,。
代碼轉(zhuǎn)換
系統(tǒng)字節(jié)碼修改的第一種 Javassist 技術(shù)使用 javassist.CodeConverter 類。要利用這種技術(shù),,只需要?jiǎng)?chuàng)建 CodeConverter 類的一個(gè)實(shí)例并用一個(gè)或者多個(gè)轉(zhuǎn)換操作配置它,。每一個(gè)轉(zhuǎn)換都是用識(shí)別轉(zhuǎn)換類型的方法調(diào)用來配置的。轉(zhuǎn)換類型可分為三類:方法調(diào)用轉(zhuǎn)換,、字段訪問轉(zhuǎn)換和新對(duì)象轉(zhuǎn)換,。
清單 1 給出了使用方法調(diào)用轉(zhuǎn)換的一個(gè)例子。在這個(gè)例子中,,轉(zhuǎn)換只是增加了一個(gè)方法正在被調(diào)用的通知,。在代碼中,,首先得到將要使用的 javassist.ClassPool 實(shí)例,,將它配置為與一個(gè)翻譯器一同工作 (正如在前面 第 5 部分 所看到的)。然后,,通過 ClassPool 訪問兩個(gè)方法定義,。第一個(gè)方法定義針對(duì)的是要監(jiān)視的“set”類型的方法(類和方法名來自命令行參數(shù)),第二個(gè)方法定義針對(duì)的是 reportSet() 方法 ,,它位于 TranslateConvert 類中,,并會(huì)報(bào)告對(duì)第一個(gè)方法的調(diào)用。
有了方法信息后,,就可以用 CodeConverter insertBeforeMethod() 配置一個(gè)轉(zhuǎn)換,,以在每次調(diào)用這個(gè) set 方法之前增加一個(gè)對(duì)報(bào)告方法的調(diào)用。然后所要做的就是將這個(gè)轉(zhuǎn)換器應(yīng)用到一個(gè)或者多個(gè)類上,。在清單 1 的代碼中,,我是通過調(diào)用類對(duì)象的 instrument() 方法,在 ConverterTranslator 內(nèi)部類的 onWrite() 方法中完成這項(xiàng)工作的,。這將自動(dòng)對(duì)從 ClassPool 實(shí)例中加載的每一個(gè)類應(yīng)用這個(gè)轉(zhuǎn)換,。
清單 1. 使用 CodeConverter
public class TranslateConvert
{
public static void main(String[] args) {
if (args.length >= 3) {
try {
// set up class loader with translator
ConverterTranslator xlat =
new ConverterTranslator();
ClassPool pool = ClassPool.getDefault(xlat);
CodeConverter convert = new CodeConverter();
CtMethod smeth = pool.get(args[0]).
getDeclaredMethod(args[1]);
CtMethod pmeth = pool.get("TranslateConvert").
getDeclaredMethod("reportSet");
convert.insertBeforeMethod(smeth, pmeth);
xlat.setConverter(convert);
Loader loader = new Loader(pool);
// invoke "main" method of application class
String[] pargs = new String[args.length-3];
System.arraycopy(args, 3, pargs, 0, pargs.length);
loader.run(args[2], pargs);
} catch ...
}
} else {
System.out.println("Usage: TranslateConvert " +
"clas-name set-name main-class args...");
}
}
public static void reportSet(Bean target, String value) {
System.out.println("Call to set value " + value);
}
public static class ConverterTranslator implements Translator
{
private CodeConverter m_converter;
private void setConverter(CodeConverter convert) {
m_converter = convert;
}
public void start(ClassPool pool) {}
public void onWrite(ClassPool pool, String cname)
throws NotFoundException, CannotCompileException {
CtClass clas = pool.get(cname);
clas.instrument(m_converter);
}
}
}
|
配置轉(zhuǎn)換是一個(gè)相當(dāng)復(fù)雜的操作,但是設(shè)置好以后,,在它工作時(shí)就不用費(fèi)什么心了,。清單 2 給出了代碼示例,可以作為測(cè)試案例,。這里 Bean 提供了具有類似 bean 的 get 和 set 方法的測(cè)試對(duì)象,, BeanTest 程序用這些方法來訪問值。
清單 2. 一個(gè) bean 測(cè)試程序
public class Bean
{
private String m_a;
private String m_b;
public Bean() {}
public Bean(String a, String b) {
m_a = a;
m_b = b;
}
public String getA() {
return m_a;
}
public String getB() {
return m_b;
}
public void setA(String string) {
m_a = string;
}
public void setB(String string) {
m_b = string;
}
}
public class BeanTest
{
private Bean m_bean;
private BeanTest() {
m_bean = new Bean("originalA", "originalB");
}
private void print() {
System.out.println("Bean values are " +
m_bean.getA() + " and " + m_bean.getB());
}
private void changeValues(String lead) {
m_bean.setA(lead + "A");
m_bean.setB(lead + "B");
}
public static void main(String[] args) {
BeanTest inst = new BeanTest();
inst.print();
inst.changeValues("new");
inst.print();
}
}
|
如果直接運(yùn)行清單 2 中的 中的 BeanTest 程序,,則輸出如下:
[dennis]$ java -cp . BeanTest
Bean values are originalA and originalB
Bean values are newA and newB
|
如果用 清單 1 中的 TranslateConvert 程序運(yùn)行它并指定監(jiān)視其中的一個(gè) set 方法,,那么輸出將如下所示:
[dennis]$ java -cp .:javassist.jar TranslateConvert Bean setA BeanTest
Bean values are originalA and originalB
Call to set value newA
Bean values are newA and newB
|
每項(xiàng)工作都與以前一樣,但是現(xiàn)在在執(zhí)行這個(gè)程序時(shí),所選的方法被調(diào)用時(shí)會(huì)有一個(gè)通知,。
在這個(gè)例子中,,可以用其他的方法容易地實(shí)現(xiàn)同樣的效果,例如通過使用 第 4 部分 中的技術(shù)在實(shí)際的 set 方法體中增加代碼,。這里的區(qū)別是,,在使用位置增加代碼讓我有了靈活性。例如,,可以容易地修改 TranslateConvert.ConverterTranslator onWrite() 方法來檢查正在加載的類名,,并只轉(zhuǎn)換在我想要監(jiān)視的類的清單中列出的類。直接在 set 方法體中添加代碼無法進(jìn)行這種有選擇的監(jiān)視,。
系統(tǒng)字節(jié)碼轉(zhuǎn)換由于提供了靈活性而使其成為為標(biāo)準(zhǔn) Java 代碼實(shí)現(xiàn)面向方面的擴(kuò)展的強(qiáng)大工具,。在本文后面您會(huì)看到更多這方面的內(nèi)容。
轉(zhuǎn)換限制
由 CodeConverter 處理的轉(zhuǎn)換很有用,,但是有局限性,。例如,如果希望在調(diào)用目標(biāo)方法之前或者之后調(diào)用一個(gè)監(jiān)視方法,,那么這個(gè)監(jiān)視方法必須定義為 static void 并且必須先接受一個(gè)目標(biāo)方法的類的參數(shù),,然后是與目標(biāo)方法所要求的同樣數(shù)量和類型的參數(shù)。
這種嚴(yán)格的結(jié)構(gòu)意味著監(jiān)視方法需要與目標(biāo)類和方法完全匹配,。舉一個(gè)例子,,假設(shè)我改變了 清單 1 中 reportSet() 方法的定義,讓它接受一個(gè)一般性的 java.lang.Object 參數(shù),,想使它可以用于不同的目標(biāo)類:
public static void reportSet(Object target, String value) {
System.out.println("Call to set value " + value);
}
|
編譯沒有問題,,但是當(dāng)我運(yùn)行它時(shí)它就會(huì)中斷:
[dennis]$ java -cp .:javassist.jar TranslateConvert Bean setA BeanTest
Bean values are A and B
java.lang.NoSuchMethodError: TranslateConvert.reportSet(LBean;Ljava/lang/String;)V
at BeanTest.changeValues(BeanTest.java:17)
at BeanTest.main(BeanTest.java:23)
at ...
|
有辦法繞過這種限制。一種解決方案是在運(yùn)行時(shí)實(shí)際生成與目標(biāo)方法相匹配的自定義監(jiān)視方法,。不過這要做很多工作,,在本文中我不打算試驗(yàn)這種方法。幸運(yùn)的是,,Javassist 還提供了另一種處理系統(tǒng)字節(jié)碼轉(zhuǎn)換的方法,。這種方法使用 javassist.ExprEditor ,與 CodeConverter 相比,,它更靈活,、也更強(qiáng)大。
容易的類剖析
用 CodeConverter 進(jìn)行字節(jié)碼轉(zhuǎn)換與用 javassist.ExprEditor 的原理一樣,。不過,, ExprEditor 方式也許更難理解一些,所以我首先展示基本原理,,然后再加入實(shí)際的轉(zhuǎn)換,。
清單 3 顯示了如何用 ExprEditor 來報(bào)告面向方面的轉(zhuǎn)換的可能目標(biāo)的基本項(xiàng)目。這里我在自己的 VerboseEditor 中派生了 ExprEditor 子類,重寫了三個(gè)基本的類方法 ―― 它們的名字都是 edit() ,,但是有不同的參數(shù)類型,。如 清單 1 中的代碼,我實(shí)際上是在 DissectionTranslator 內(nèi)部類的 onWrite() 方法中使用這個(gè)子類,,對(duì)從 ClassPool 實(shí)例中加載的每一個(gè)類,,在對(duì)類對(duì)象的 instrument() 方法的調(diào)用中傳遞一個(gè)實(shí)例。
清單 3. 一個(gè)類剖析程序
public class Dissect
{
public static void main(String[] args) {
if (args.length >= 1) {
try {
// set up class loader with translator
Translator xlat = new DissectionTranslator();
ClassPool pool = ClassPool.getDefault(xlat);
Loader loader = new Loader(pool);
// invoke the "main" method of the application class
String[] pargs = new String[args.length-1];
System.arraycopy(args, 1, pargs, 0, pargs.length);
loader.run(args[0], pargs);
} catch (Throwable ex) {
ex.printStackTrace();
}
} else {
System.out.println
("Usage: Dissect main-class args...");
}
}
public static class DissectionTranslator implements Translator
{
public void start(ClassPool pool) {}
public void onWrite(ClassPool pool, String cname)
throws NotFoundException, CannotCompileException {
System.out.println("Dissecting class " + cname);
CtClass clas = pool.get(cname);
clas.instrument(new VerboseEditor());
}
}
public static class VerboseEditor extends ExprEditor
{
private String from(Expr expr) {
CtBehavior source = expr.where();
return " in " + source.getName() + "(" + expr.getFileName() + ":" +
expr.getLineNumber() + ")";
}
public void edit(FieldAccess arg) {
String dir = arg.isReader() ? "read" : "write";
System.out.println(" " + dir + " of " + arg.getClassName() +
"." + arg.getFieldName() + from(arg));
}
public void edit(MethodCall arg) {
System.out.println(" call to " + arg.getClassName() + "." +
arg.getMethodName() + from(arg));
}
public void edit(NewExpr arg) {
System.out.println(" new " + arg.getClassName() + from(arg));
}
}
}
|
清單 4 顯示了對(duì) 清單 2 中的 BeanTest 程序運(yùn)行清單 3 中的 Dissect 程序所產(chǎn)生的輸出,。它給出了加載的每一個(gè)類的每一個(gè)方法中所做的工作的詳細(xì)分析,列出了所有方法調(diào)用,、字段訪問和新對(duì)象創(chuàng)建。
清單 4. 已剖析的 BeanTest
[dennis]$ java -cp .:javassist.jar Dissect BeanTest
Dissecting class BeanTest
new Bean in BeanTest(BeanTest.java:7)
write of BeanTest.m_bean in BeanTest(BeanTest.java:7)
read of java.lang.System.out in print(BeanTest.java:11)
new java.lang.StringBuffer in print(BeanTest.java:11)
call to java.lang.StringBuffer.append in print(BeanTest.java:11)
read of BeanTest.m_bean in print(BeanTest.java:11)
call to Bean.getA in print(BeanTest.java:11)
call to java.lang.StringBuffer.append in print(BeanTest.java:11)
call to java.lang.StringBuffer.append in print(BeanTest.java:11)
read of BeanTest.m_bean in print(BeanTest.java:11)
call to Bean.getB in print(BeanTest.java:11)
call to java.lang.StringBuffer.append in print(BeanTest.java:11)
call to java.lang.StringBuffer.toString in print(BeanTest.java:11)
call to java.io.PrintStream.println in print(BeanTest.java:11)
read of BeanTest.m_bean in changeValues(BeanTest.java:16)
new java.lang.StringBuffer in changeValues(BeanTest.java:16)
call to java.lang.StringBuffer.append in changeValues(BeanTest.java:16)
call to java.lang.StringBuffer.append in changeValues(BeanTest.java:16)
call to java.lang.StringBuffer.toString in changeValues(BeanTest.java:16)
call to Bean.setA in changeValues(BeanTest.java:16)
read of BeanTest.m_bean in changeValues(BeanTest.java:17)
new java.lang.StringBuffer in changeValues(BeanTest.java:17)
call to java.lang.StringBuffer.append in changeValues(BeanTest.java:17)
call to java.lang.StringBuffer.append in changeValues(BeanTest.java:17)
call to java.lang.StringBuffer.toString in changeValues(BeanTest.java:17)
call to Bean.setB in changeValues(BeanTest.java:17)
new BeanTest in main(BeanTest.java:21)
call to BeanTest.print in main(BeanTest.java:22)
call to BeanTest.changeValues in main(BeanTest.java:23)
call to BeanTest.print in main(BeanTest.java:24)
Dissecting class Bean
write of Bean.m_a in Bean(Bean.java:10)
write of Bean.m_b in Bean(Bean.java:11)
read of Bean.m_a in getA(Bean.java:15)
read of Bean.m_b in getB(Bean.java:19)
write of Bean.m_a in setA(Bean.java:23)
write of Bean.m_b in setB(Bean.java:27)
Bean values are originalA and originalB
Bean values are newA and newB
|
通過在 VerboseEditor 中實(shí)現(xiàn)適當(dāng)?shù)姆椒?,可以容易地增加?duì)報(bào)告強(qiáng)制類型轉(zhuǎn)換,、 instanceof 檢查和 catch 塊的支持。但是只列出有關(guān)這些組件項(xiàng)的信息有些乏味,,所以讓我們來實(shí)際修改項(xiàng)目吧,。
進(jìn)行剖析
清單 4對(duì)類的剖析列出了基本組件操作。容易看出在實(shí)現(xiàn)面向方面的功能時(shí)使用這些操作會(huì)多么有用,。例如,,報(bào)告對(duì)所選字段的所有寫訪問的記錄器(logger)在許多應(yīng)用程序中都會(huì)發(fā)揮作用,。無論如何,,我已經(jīng)承諾要為您介紹如何完成 這類工作。
幸運(yùn)的是,,就本文討論的主題來說,, ExprEditor 不但讓我知道代碼中有什么操作,它還讓我可以修改所報(bào)告的操作,。在不同的 ExprEditor.edit() 方法調(diào)用中傳遞的參數(shù)類型分別定義一種 replace() 方法,。如果向這個(gè)方法傳遞一個(gè)普通 Javassist 源代碼格式的語(yǔ)句(在 第 4 部分中介紹),那么這個(gè)語(yǔ)句將編譯為字節(jié)碼,,并且用來替換原來的操作,。這使對(duì)字節(jié)碼的切片和切塊變得容易。
清單 5 顯示了一個(gè)代碼替換的應(yīng)用程序,。在這里我不是記錄操作,,而是選擇實(shí)際修改存儲(chǔ)在所選字段中的 String 值。在 FieldSetEditor 中,,我實(shí)現(xiàn)了匹配字段訪問的方法簽名,。在這個(gè)方法中,我只檢查兩樣?xùn)|西:字段名是否是我所查找的,,操作是否是一個(gè)存儲(chǔ)過程,。找到匹配后,就用使用實(shí)際的 TranslateEditor 應(yīng)用程序類中 reverse() 方法調(diào)用的結(jié)果來替換原來的存儲(chǔ)。 reverse() 方法就是將原來字符串中的字母順序顛倒并輸出一條消息表明它已經(jīng)使用過了,。
清單 5. 顛倒字符串集
public class TranslateEditor
{
public static void main(String[] args) {
if (args.length >= 3) {
try {
// set up class loader with translator
EditorTranslator xlat =
new EditorTranslator(args[0], new FieldSetEditor(args[1]));
ClassPool pool = ClassPool.getDefault(xlat);
Loader loader = new Loader(pool);
// invoke the "main" method of the application class
String[] pargs = new String[args.length-3];
System.arraycopy(args, 3, pargs, 0, pargs.length);
loader.run(args[2], pargs);
} catch (Throwable ex) {
ex.printStackTrace();
}
} else {
System.out.println("Usage: TranslateEditor clas-name " +
"field-name main-class args...");
}
}
public static String reverse(String value) {
int length = value.length();
StringBuffer buff = new StringBuffer(length);
for (int i = length-1; i >= 0; i--) {
buff.append(value.charAt(i));
}
System.out.println("TranslateEditor.reverse returning " + buff);
return buff.toString();
}
public static class EditorTranslator implements Translator
{
private String m_className;
private ExprEditor m_editor;
private EditorTranslator(String cname, ExprEditor editor) {
m_className = cname;
m_editor = editor;
}
public void start(ClassPool pool) {}
public void onWrite(ClassPool pool, String cname)
throws NotFoundException, CannotCompileException {
if (cname.equals(m_className)) {
CtClass clas = pool.get(cname);
clas.instrument(m_editor);
}
}
}
public static class FieldSetEditor extends ExprEditor
{
private String m_fieldName;
private FieldSetEditor(String fname) {
m_fieldName = fname;
}
public void edit(FieldAccess arg) throws CannotCompileException {
if (arg.getFieldName().equals(m_fieldName) && arg.isWriter()) {
StringBuffer code = new StringBuffer();
code.append("$0.");
code.append(arg.getFieldName());
code.append("=TranslateEditor.reverse($1);");
arg.replace(code.toString());
}
}
}
}
|
如果對(duì) 清單 2 中的 BeanTest 程序運(yùn)行清單 5 中的 TranslateEditor 程序,,結(jié)果如下:
[dennis]$ java -cp .:javassist.jar TranslateEditor Bean m_a BeanTest
TranslateEditor.reverse returning Alanigiro
Bean values are Alanigiro and originalB
TranslateEditor.reverse returning Awen
Bean values are Awen and newB
|
我成功地在每一次存儲(chǔ)到 Bean.m_a 字段時(shí),加入了一個(gè)對(duì)添加的代碼的調(diào)用(一次是在構(gòu)造函數(shù)中,,一次是在 set 方法中),。我可以通過對(duì)從字段的加載實(shí)現(xiàn)類似的修改而得到反向的效果,不過我個(gè)人認(rèn)為顛倒值比開始使用的值有意思得多,,所以我選擇使用它們,。
包裝 Javassist
本文介紹了用 Javassist 可以容易地完成系統(tǒng)字節(jié)碼轉(zhuǎn)換。將本文與上兩期文章結(jié)合在一起,,您應(yīng)該有了在 Java 應(yīng)用程序中實(shí)現(xiàn)自己面向方面的轉(zhuǎn)換的堅(jiān)實(shí)基礎(chǔ),,這個(gè)轉(zhuǎn)換過程可以作為單獨(dú)的編譯步驟,也可以在運(yùn)行時(shí)完成,。
要想對(duì)這種方法的強(qiáng)大之處有更好的了解,,還可以分析用 Javassis 建立的 JBoss Aspect Oriented Programming Project (JBossAOP)。JBossAOP 使用一個(gè) XML 配置文件來定義在應(yīng)用程序類中完成的所有不同的操作,。其中包括對(duì)字段訪問或者方法調(diào)用使用攔截器,,在現(xiàn)有類中添加 mix-in 接口實(shí)現(xiàn)等。JBossAOP 將被加入正在開發(fā)的 JBoss 應(yīng)用程序服務(wù)器版本中,,但是也可以在 JBoss 以外作為單獨(dú)的工具提供給應(yīng)用程序使用,。
本系列的下一步將介紹 Byte Code Engineering Library (BCEL),這是 Apache Software Foundation 的 Jakarta 項(xiàng)目的一部分,。BCEL 是 Java classworking 最廣泛使用的一種框架,。它使用與我們?cè)谧罱@三篇文章中看到的 Javassist 方法的不同方法處理字節(jié)碼,注重個(gè)別的字節(jié)碼指令而不是 Javassist 所強(qiáng)調(diào)的源代碼級(jí)別的工作,。下個(gè)月將分析在字節(jié)碼匯編器(assembler)級(jí)別工作的全部細(xì)節(jié),。
參考資料
- 您可以參閱本文在 developerWorks 全球站點(diǎn)上的 英文原文.
- 參閱 Dennis Sosnoski 的 Java 編程的動(dòng)態(tài)性 系列的其他部分。
- 下載本文的 示例代碼,。
- Javassist 是東京技術(shù)學(xué)院數(shù)學(xué)和計(jì)算機(jī)科學(xué)系的 Shigeru Chiba 開發(fā)的,。它最近加入了開放源代碼 JBoss 應(yīng)用服務(wù)器項(xiàng)目,并成為其中新增加的面向方面的編程功能的基礎(chǔ),。從 Sourceforge 上的 JBoss 項(xiàng)目文件頁(yè)面下載 Javassist 的最新版本,。
- 想看用 Javassist 構(gòu)建的面向方面的編程框架嗎?請(qǐng)看 JBossAOP 項(xiàng)目,。
- 從 Peter Haggar 的“ Java bytecode: Understanding bytecode makes you a better programmer”( developerWorks,,2001 年 7 月)中學(xué)習(xí)更多關(guān)于 Java 字節(jié)碼設(shè)計(jì)的內(nèi)容。
- 希望找到有關(guān)面向方面編程的更多內(nèi)容嗎,?請(qǐng)參閱 Nicholas Lesiechi 的“ Improve modularity with aspect-oriented programming”( developerWorks,,2002 年 1 月),,以了解有關(guān)使用 Aspectj 語(yǔ)言的簡(jiǎn)要介紹。Andrew Glover 新發(fā)表的文章“ AOP banishes the tight-coupling blues”( developerWorks,,2004 年 2 月)展示了 AOP 的一個(gè)功能設(shè)計(jì)概念 ―― 靜態(tài)橫切 ―― 是如何將纏繞在一起的,、緊密耦合的代碼轉(zhuǎn)變?yōu)閺?qiáng)大的、可擴(kuò)展的企業(yè)應(yīng)用程序,。
- 開放源代碼 Jikes 項(xiàng)目為 Java 編程語(yǔ)言提供了一個(gè)非??焖俸透叨燃嫒莸木幾g器。用它以老的方式(即從 Java 源代碼)生成字節(jié)碼,。
- 訪問 Developer Bookstore,,獲得技術(shù)圖書的完整列表,其中包括數(shù)百本 Java 相關(guān)的圖書,。
- 在 developerWorks Java 技術(shù)專區(qū) 上有數(shù)百篇關(guān)于 Java 技術(shù)各個(gè)方面的文章,。
關(guān)于作者
|
|
|
Dennis Sosnoski( dms@)是西雅圖地區(qū)的 Java 咨詢公司 Sosnoski Software Solutions, Inc.的創(chuàng)始人和首席顧問。他有 30 多年的專業(yè)軟件開發(fā)經(jīng)驗(yàn),,最近幾年致力于服務(wù)器端的 Java 技術(shù),,包括 servlet、Enterprise JavaBeans 和 XML,。他經(jīng)常在全國(guó)性的會(huì)議上就 Java 中的 XML 和 J2EE 技術(shù)發(fā)表言論,。
|
|