クラスワーキング・ツールキット: ASMクラスワーキング

小さくて高速、というASMクラスワーキング・ライブラリーの主張は正しいのか? ASM 2.0を使ってテストする

今回のクラスワーキング・ツールキットでは、コンサルタントのDennis Sosnoskiが、ASMバイトコード操作フレームワークを、彼が以前Javaプログラミングのダイナミックス・シリーズの中で議論したBCEL(ByteCode Engineering Library)やJavassistフレームワークと比較します。ASMは小さくて高速だと言われていますが、他のフレームワークと比較して、どの程度なのでしょう。Dennisは以前のシリーズで使った例を使いながら、使いやすさとパフォーマンスの面から評価を行います。

Dennis Sosnoski, President, Sosnoski Software Solutions, Inc.

Photo of Dennis SosnoskiDennis Sosnoski はシアトル地域にある Java 技術のコンサルティング会社、Sosnoski Software Solutions, Inc. の創立者で、主席コンサルタントでもあり、また XML や Web サービスに関するトレーニングやコンサルティングの専門家でもあります。彼のプロとしてのソフトウェア開発経験は 30年以上に渡り、ここ数年はサーバー側の XML 技術や Java 技術に注力しています。Dennis は、全米各地で行われる会議で頻繁に講演を行っています。また、Java クラスワーキング技術を基に構築された、オープンソースの JiBX XML Data Binding フレームワークの中心開発者でもあります。



2005年 5月 12日

バイトコードやクラスファイルを扱うためのJava™ライブラリーが幾つか開発されています。その中には、私が以前、Javaプログラミングのダイナミックス・シリーズ(参考文献)の中で取り上げたJavassistやBCELなどのライブラリーも含まれています。ASMもこれらと同様のライブラリーとして、より新しいものです。ASMは他のライブラリーとは異なり、できるだけ小さく高速になるように設計、実装されています。今月のコラムでは、私が以前のシリーズで使用した2つのライブラリーと比較しながら、ASMのうたい文句がどの程度本物かを調べてみます。

以前の記事では、リフレクションの置き換えとして、実行時バイトコード生成の使い方を解説しました。その際には1.4.1JVMを使ってテストを行いましたが、置き換える前のリフレクション・コードよりも、生成されたコードの方がずっと速く実行することが分かりました。ASMでも同じ手法を試しますが、同時に1.5.0JVMを使って以前の結果を更新し、1.5.0で行われたパフォーマンス改善によって結果が変わるかどうかも確認します。

リフレクションを置き換える

このアプリケーション例の目的は、実行時に生成されるコードでリフレクションを置き換えることです。これについては、私のJavaプログラミングのダイナミックス・シリーズで詳しく説明しました。このコラムでは、まず以前の資料の背景を簡単に要約します。次にJavassistやBCELフレームワークの代わりにASMを使い、ASMのパフォーマンスや使いやすさを、他のものと比較します。

ステージの設定

リフレクションは、実行時にオブジェクトとメタデータの両方にアクセスする上で、非常に強力な機構です(Javaプログラミングのダイナミックス 第2回で議論した通りです)。リフレクションを使うことによって、アプリケーションは柔軟に構成でき、実行時に様々なものをつなぎ合わせるために使用される外部情報を、実働のコンフィギュレーションにすることができます。ところが実際のオブジェクト・アクセスにリフレクションを使用すると、同じ操作を直接実行した場合に比べて、動作がずっと遅いのです。リフレクションによって実現される柔軟性を他の方法で実現するのは困難なため、リフレクション・ベースの手法を基にアプリケーションを構築した後でパフォーマンス改善が必要だと判明した場合には、大きな問題となります。

こうした場合に、クラスワーキングの手法が役立ちます。リフレクションを使ってオブジェクトのプロパティーにアクセスするのではなく、例えば、同じことをずっと高速に行うクラスを、実行時に作るのです。「Javaプログラミングのダイナミックス 第8回」では、このタイプによるリフレクション置き換えとして、JavassistとBCELという2つのクラスワーキング・フレームワークを使っています。この記事での基本的な考え方は単純です。まず必要な機能を定義するインターフェースを作り、次にそのインターフェースを実行時に実装するクラスを作り、そのファンクションを、対象とするオブジェクトにフックするのです。

リスト1は、この手法を示しています。ここでHolderBeanクラスは1対のプロパティーを含んでおり、これらは、getメソッドとsetメソッドを呼ぶリフレクションを使うことによって、実行時にアクセスされます。IAccessインターフェースは、int値を持つプロパティーに、getメソッドとsetメソッドを使ってアクセスする概念を抽象化したものです。AccessValue1クラスは、このインターフェースを、特にHolderBeanクラスの「value1」プロパティーに対して実装したものです。

リスト1. リフレクション置き換えのインターフェースと実装
public class HolderBean
{
    private int m_value1;
    private int m_value2;
    
    public int getValue1() {
        return m_value1;
    }
    public void setValue1(int value) {
        m_value1 = value;
    }
    
    public int getValue2() {
        return m_value2;
    }
    public void setValue2(int value) {
        m_value2 = value;
    }
}

public interface IAccess
{
    public void setTarget(Object target);
    public int getValue();
    public void setValue(int value);
}

public class AccessValue1 implements IAccess
{
    private HolderBean m_target;
    
    public void setTarget(Object target) {
        m_target = (HolderBean)target;
    }
    public int getValue() {
        return m_target.getValue1();
    }
    public void setValue(int value) {
        m_target.setValue1(value);
    }
}

リスト1のAccessValue1クラスのような実装クラスを、それぞれ手動でコーディングする必要があるような場合には、この手法はあまり便利ではありません。しかしAccessValue1のコードは非常に単純であり、実行時でのクラス生成のターゲットとして理想的です。AccessValue1バイトコードは、特定なターゲット・オブジェクト・タイプやget/setメソッド対に合わせ込んだクラス生成用のテンプレートとして使うことができます(AccessValue1で使われているものの代わりに、単にこうしたターゲットで置き換えるのです)。私が以前の記事で使ったのはこの手法であり、このコラムでも、同じ手法をASMに対して使います。


ASMを扱う

私が以前の記事で説明した2つのクラスワーキング・フレームワークは、非常に異なった手法でバイトコードを扱います。Javassistは単純化されたバージョンのJavaソースコードを使い、それをバイトコードへとコンパイルします。そのためJavassistは非常に扱いが容易なのですが、同時にバイトコードは、Javassistソースコードの制限の中で表現できるものに限定されてしまいます。一方BCELは、バイトコードを直接扱います。BCELはバイトコード命令を操作するための構造や手法を用意しており、純然たるバイナリー値のレベルよりも一段階上なのですが、Javassistよりも扱いはずっと困難です。

ASMは、操作のレベルという点ではBCELよりもJavassistに近いのですが、BCELよりもずっとクリーンなインターフェースを使っていると私は思います。この理由の一つは、ASMの基本設計によるものです。ASMでは、バイトコード命令を直接操作する代わりに、ビジター・パターン(visitorpattern)を使って、イベントのストリームとしてクラス・データ(命令シーケンスを含みます)を処理します。既存のクラスをデコードする時には、ASMはユーザーのためにイベントのストリームを生成し、イベント処理のためにメソッドを呼ぶのです。新しいクラスを生成する時には、この手法が逆になります。つまりユーザーがASMクラスを呼ぶと、コールによって表現されるイベントのストリームから、新しいクラスが作られるのです。あるいは、この両方を使うこともできます。つまり既存のクラスから生成されたイベントをインターセプト(intercept)して何らかの変更を加え、変更されたストリームを、新しいクラスの生成にと送り返すのです。

クラスをASM化する

BCELにもASMにも、クラスを書くためのJavaソースコードを生成するツールが備わっています。こうしたツールは、実行時のクラス生成には既存クラスをテンプレートして使う、という考え方を基本にしています。生成されたソースコードは、バイナリー形式でのテンプレート・クラスを再現するために必要な全てのコールを含んでいます。ですから理論的には、このコードをアプリケーション・コードの中にマージし、必要に合うように修正することができます(例えば、実行時に修正可能なことが必要な値に対してパラメーターを置き換える、など)。

ただし現実的には、クラスを書くためにBCELに備わっているプログラム(org.apache.bcel.util.BCELifier)は、限定的な使い方しかできないようです。命令リストを操作するためのBCELコードは複雑であり、それに、BCELifierが生成するソースコードは、実用とするには醜悪すぎます。クラスを書くためにASMに用意されているプログラムも、少しばかり醜悪なコードを生成しますが、ちょっと手を加えれば、実用には充分なようです。リスト2は、このプログラム(org.objectweb.asm.util.ASMifierClassVisitor)を、リスト1のgen.AccessValue1クラスに対して実行した結果を示しています。

リスト2. gen.AccessValue1から生成されたASMコード
package asm.gen;
import org.objectweb.asm.*;
public class AccessValue1Dump implements Opcodes {

public static byte[] dump () throws Exception {

ClassWriter cw = new ClassWriter(false);
FieldVisitor fv;
MethodVisitor mv;
AnnotationVisitor av0;

cw.visit(V1_2, ACC_PUBLIC + ACC_SUPER, "gen/AccessValue1", null, "java/lang/Object", new String[] 
  { "gen/IAccess" });

cw.visitSource("AccessValue1.java", null);

{
fv = cw.visitField(0, "m_bean", "Lgen/HolderBean;", null, null);
fv.visitEnd();
}
{
mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V");
mv.visitInsn(RETURN);
mv.visitMaxs(1, 1);
mv.visitEnd();
}
{
mv = cw.visitMethod(ACC_PUBLIC, "setTarget", "(Ljava/lang/Object;)V", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitVarInsn(ALOAD, 1);
mv.visitTypeInsn(CHECKCAST, "gen/HolderBean");
mv.visitFieldInsn(PUTFIELD, "gen/AccessValue1", "m_bean", "Lgen/HolderBean;");
mv.visitInsn(RETURN);
mv.visitMaxs(2, 2);
mv.visitEnd();
}
{
mv = cw.visitMethod(ACC_PUBLIC, "getValue", "()I", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETFIELD, "gen/AccessValue1", "m_bean", "Lgen/HolderBean;");
mv.visitMethodInsn(INVOKEVIRTUAL, "gen/HolderBean", "getValue1", "()I");
mv.visitInsn(IRETURN);
mv.visitMaxs(1, 1);
mv.visitEnd();
}
{
mv = cw.visitMethod(ACC_PUBLIC, "setValue", "(I)V", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETFIELD, "gen/AccessValue1", "m_bean", "Lgen/HolderBean;");
mv.visitVarInsn(ILOAD, 1);
mv.visitMethodInsn(INVOKEVIRTUAL, "gen/HolderBean", "setValue1", "(I)V");
mv.visitInsn(RETURN);
mv.visitMaxs(2, 2);
mv.visitEnd();
}
cw.visitEnd();

return cw.toByteArray();
}
}

リスト2のコードを少し再フォーマットすると、リフレクションの置き換えとなるコードの基礎になります。これを次に見ることにします。


ASMでリフレクションを置き換える

Javaプログラミングのダイナミックス 第8回」では、リフレクションを置き換えるためのコード生成に関して様々な実装をテストするために、あるベース・クラスを使っています。そこでは、それぞれのクラスワーキング・ライブラリーは、別々のサブクラスを使ってベース・クラスを拡張しています。ASMを試すのにも、同じ手法を使います。

リスト3は、ASMの実装サブクラスを示しています。リフレクションの置き換えクラスの構成は、(リスト2でASMが生成したコードに基づく)createAccess()メソッドを使って行われています。リスト2のコードとの主な差として、私はリスト3を少し再フォーマットして再構成しています。また、ターゲット・クラスのパラメーター、プロパティーgetメソッドとsetメソッドを作り、クラス名を生成して、ASM版のcreateAccess()メソッドが、以前の記事でJavassistやBCELで使用したものと互換性があるようにしています。

リスト3. ASMテスト・クラス
public class ASMCalls extends TimeCalls
{
    protected byte[] createAccess(Class tclas, Method gmeth, Method smeth,
        String cname) throws Exception {
        
        // initialize writer for new class
        String ciname = cname.replace('.', '/');
        ClassWriter cw = new ClassWriter(false);
        cw.visit(Opcodes.V1_2, Opcodes.ACC_PUBLIC + Opcodes.ACC_SUPER,
            cname, null, "java/lang/Object", new String[] { "gen/IAccess" });
        
        // add field definition for reference to target class instance
        String tiname = Type.getInternalName(tclas);
        String ttype = "L" + tiname + ";";
        cw.visitField(0, "m_bean", ttype, null, null).visitEnd();
        
        // generate the default constructor
        MethodVisitor mv =
            cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
        mv.visitCode();
        mv.visitVarInsn(Opcodes.ALOAD, 0);
        mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object",
            "<init>", "()V");
        mv.visitInsn(Opcodes.RETURN);
        mv.visitMaxs(1, 1);
        mv.visitEnd();
        
        // generate the setTarget method
        mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "setTarget",
            "(Ljava/lang/Object;)V", null, null);
        mv.visitCode();
        mv.visitVarInsn(Opcodes.ALOAD, 0);
        mv.visitVarInsn(Opcodes.ALOAD, 1);
        mv.visitTypeInsn(Opcodes.CHECKCAST, tiname);
        mv.visitFieldInsn(Opcodes.PUTFIELD, ciname, "m_bean", ttype);
        mv.visitInsn(Opcodes.RETURN);
        mv.visitMaxs(2, 2);
        mv.visitEnd();
        
        // generate the getValue method
        mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "getValue", "()I", null, null);
        mv.visitCode();
        mv.visitVarInsn(Opcodes.ALOAD, 0);
        mv.visitFieldInsn(Opcodes.GETFIELD, ciname, "m_bean", ttype);
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, tiname,
            gmeth.getName(), "()I");
        mv.visitInsn(Opcodes.IRETURN);
        mv.visitMaxs(1, 1);
        mv.visitEnd();
        
        // generate the setValue method
        mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "setValue", "(I)V", null, null);
        mv.visitCode();
        mv.visitVarInsn(Opcodes.ALOAD, 0);
        mv.visitFieldInsn(Opcodes.GETFIELD, ciname, "m_bean", ttype);
        mv.visitVarInsn(Opcodes.ILOAD, 1);
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, tiname,
            smeth.getName(), "(I)V");
        mv.visitInsn(Opcodes.RETURN);
        mv.visitMaxs(2, 2);
        mv.visitEnd();
        
        // complete the class generation
        cw.visitEnd();
        return cw.toByteArray();
    }
    
    public static void main(String[] args) throws Exception {
        if (args.length == 1) {
            ASMCalls inst = new ASMCalls();
            inst.test(args[0]);
        } else {
            System.out.println("Usage: ASMCalls loop-count");
        }
    }
}

リスト3のcreateAccess()コードは、ASMを扱う上での基本的な原則を示しています。まず、org.objectweb.asm.ClassWriterを作ることから始めています。これは、クラス・イベントのストリームを(メソッド・コールの形で)受け付け、出力のバイナリー・クラス表現を生成します。このライターのvisitField()メソッドを呼ぶことによって、構成されるクラスにフィールドを追加します(visitField()メソッドは、そのフィールドに対するビジターを返します)。返されるフィールド・ビジターは、フィールドに注記(annotation)や特別な属性情報を追加するために使われます。しかしこの場合には、何ら特別なものは必要ないので、単にフィールド・ビジターのvisitEnd()メソッドを即座に呼んでいます。

フィールドを追加した後、構成されるクラスに対して、必要なメソッドを4つ追加しています。最初のメソッドは、先ほどのリスト1の、テンプレート・クラス用のソースコードには現れてもいませんが、このクラスに対するデフォルトのコンストラクターです。このコンストラクターは何も引数を取らず、スーパークラス・コンストラクターを呼ぶだけですが、クラスに対するコンストラクターを規定しない場合には、Javaコンパイラーによって自動生成されます。私は自分でクラスを構成しているので、明示的にデフォルト・コンストラクターを作る必要があります。残りの3つのメソッドは、リスト1のソースコードの中に示したものと同じです。

フィールドを追加する場合と同じように、クラス・ライターのvisitMethod()メソッドを呼ぶと、追加されるメソッドに対するビジターが返ります。このメソッド・ビジター(org.objectweb.asm.MethodVisitorインターフェースのインスタンス)は、このメソッドに注記や特別な属性を追加するために使いますが、メソッド本体を構成する実際のバイトコード命令シーケンスを生成するためのインターフェースも提供しています。リスト1のコードは、メソッド・ビジターへのコールによって、どのように命令が付加されるかを示しています。全ての命令の追加が終わると、最後の一対のコールを使ってメソッド生成が完了します。その対の最初の方、visitMaxs()は、最大スタック・サイズと、このメソッドに対するローカル変数カウントを設定します(こうした値は、コールの中にあるtrue引数をClassWriterコンストラクターに渡すように設定しておけば、ASMによって自動的に計算することもできます)。最終コール対のうちの2番目、visitEnd()は、単にメソッド構築プロセスを完了します。

フィールドとメソッドが追加されてしまえば、完成したクラスのバイナリー版を得るのは簡単です。クラス・ライターに対するvisitEnd()コールは、クラスを書くことが終了したことを示し、またtoByteArray()コールは、実際にバイナリーのクラス・イメージを返します。


結果をチェックする

Javaプログラミング・ダイナミックスス 第8回」では、JavassistとBCELを使ってリフレクション置き換えクラスを実行時生成した場合の時間を比較しました。また、リフレクションを使った場合と置き換えクラスを使った場合とで、アクセス回数を変えて実行した場合の時間差を示しました。このコラムでは、同じような形式でテスト結果を示しますが、少し変更を加えています。第1に、生成時間の比較にASMを含めています。また、テストにはJDK1.5を使い、より正確な時間結果を得るためにjava.lang.System.nanoTime()メソッドが使えるようにしています。

図1は、リフレクション・メソッド・コールを使った場合と、2Kから512Kの範囲のループ・カウントを持つループで生成されたクラスの時間の比較を示しています。(テストは、1GHzのPIIImを使ったノートブック・システム上で、Sunの1.5.0JVMを使ったMandrake Linux 10.0の上で実行しています。)これらの時間は、全てのフレームワークで共通です。生成されたコードを使うパフォーマンス上の利点は、以前のテストで1.4.2JVMを使った場合ほど良くはないようですが、やはり大幅に優れており、生成されたコードは、リフレクションよりも10倍から14倍速く実行しています。

図1. リフレクションを使った場合と、生成されたコードを使った場合のスピード比較(時間の単位はミリ秒)
リフレクションを使った場合と、生成されたコードを使った場合のスピード比較

図1の結果は興味深いものですが、このコラムでの要点ではありません。最も関心を寄せるべきなのは、表1です。これは、各フレームワークを使って生成クラスを構築するために要した時間を示しています。ここでは、それぞれのフレームワークに対して2つの別々の時間を使っています。『第1回』の値は、リフレクション置き換え用の最初のクラスを構築するために要した時間です。これには、フレームワーク・コードの中でクラスをロードし、初期化するための時間が含まれています。『その後』の値は、(他のプロパティーに対する)リフレクション置き換え用のクラスを、さらに3つビルドする時間の平均です。

表1. クラスを構築するために要した時間
フレームワーク第1回その後
Javassist2575.2
BCEL4735.5
ASM62.41.1

表1の結果は、ASMが他のフレームワークよりも高速という評判が事実であること、またその強みが、起動時にも、繰り返し使った場合にも現れていることを示しています。


まとめ

ASMを、他のクラスワーキング・フレームワークと比較すると、ASMの方が他よりも数倍高速なことが分かります(少なくとも、この記事で取り上げた、ごく典型的な場合に関する限り)。また、ASMはずっとコンパクトであり、ランタイムJARはたった33Kしかありません(一方Javassistでは310K、BCELでは何と504Kです)。使いやすいかどうかは判断しにくいところですが、そのインターフェースはBCELよりもはるかにクリーンなようであり、BCELと同程度に柔軟性があります(ただし、BCEL独特の特徴、例えばコード構築を、直線的ではなくセグメントで行えるなどの機能には欠けています)。ASMは、そのJava風のソースコード・インターフェースから、Javassistほど容易には使えませんが、バイトコード・レベルで扱おうとする人にとっては、ASMを使うこともお勧めと言えるでしょう。

ASMでのクラスワーキングについては、今後の記事でも、元々BCELで設計されたクラスワーキング・アプリケーションをASM用に変換する話題を議論する際に、あらためて取り上げるつもりです。次回は、ASMを別の領域に適用し、J2SE5.0でJavaプラットフォームに追加された、注記(annotation)サポートに関して調べます。ASMをJ2SE5.0注記と組み合わせると、注記サポートを非常に効果的に利用できるのです。その時に再度、この強力なクラスワーキング・フレームワークについて調べることにしましょう。


ダウンロード

内容ファイル名サイズ
ソースコード j-cwt05125code.zip912KB

参考文献

  • この記事の先頭あるいは最後にあるCodeアイコンをクリックして(あるいはダウンロード・セクションを見てください)、この記事で議論したソースコードをダウンロードしてください。
  • Dennis Sosnoskiによる、クラスワーキング・ツールキット・シリーズの他の記事も読んでください(日本語に翻訳されていない記事もあります)。
  • 高速で柔軟な、ASMバイトコード操作フレームワークの詳細を学んでください。
  • JVMのアーキテクチャーや命令セットに関する素晴らしい参照として、Bill VennersによるInsidethe Java Virtual Machine(Artima Software, Inc.、2004年刊)を見てください。実際の本を購入する前に、オンラインで一部の章を見ることができます。
  • JVM操作のすべてに関して決定的な資料である、公式のJava Virtual Machine Specification を、オンラインで閲覧、あるいは購入することができます。
  • この記事の著者のDennis Sosnoskiによる、Javaプログラミング・ダイナミックス・シリーズの全記事を読んで、Javaのクラス構造、リフレクション、クラスワーキングなどを知るツアーに出かけてください(日本語に翻訳されていない記事もあります)。
  • Apache project pageで、人気のあるオープンソースBCELのすべてに関して学んでください。
  • オープンソースのJikes Projectでは、非常に高速で厳密に規格準拠したJavaプログラミング言語用コンパイラーを提供しています。昔ながらの方法で、(Javaソースコードから)バイトコードを生成するために使ってみてください。
  • developerWorksのJava technologyゾーンには、Java技術に関する資料が他にも豊富に用意されています。技術的なドキュメンテーションや、ハウツー記事、教育資料、ダウンロード、製品情報など、様々な情報を得ることができます。
  • New to Java technologyには、Javaプログラミングを始めるために役立つ最新情報が用意されています。
  • developerWorks blogsに参加して、developerWorksコミュニティーに加わってください。

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology
ArticleID=219473
ArticleTitle=クラスワーキング・ツールキット: ASMクラスワーキング
publish-date=05122005