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

実行時でのクラスファイル修正を自動化する

データ・クラスすべてに対してtoString()メソッドを構築したり維持管理したりするのに飽きていませんか? 今回のクラスワーキング・ツールキットでは、コンサルタントのDennis Sosnoskiが、J2SE 5.0の注釈(annotation)とASMバイトコード操作フレームワークを使って、このプロセスを自動化する方法を解説します。J2SE 5.0での新しいインスツルメンテーションAPI(instrumentation API)を利用して、JVMにクラスをロードする時にASMを呼び出し、実行時にオンザフライ(on-the-fly)でクラス修正を行います。

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年 6月 07日

SunはJ2SE 5.0で、Java™プラットフォームに幾つかの新機能を追加しました。最も重要な新機能の1つが、注釈(annotation)のサポートです。注釈は、多くのタイプのメタデータをJavaコードに関連させる上で有用であると言われており、既にカスタムのコンフィギュレーション・ファイルの置き換えとして、新規あるいはアップデートされたJSRでのJavaプラットフォーム拡張として、広く使われています。このコラムでは、ASMバイトコード操作フレームワークを、J2SE 5.0のもう一つの新機能であるインスツルメンテーション・パッケージ(instrumentation package)と組み合わせ、クラスをJVMの中にロードする際に、注釈で指示された通りにクラスを変換する方法を解説します。

注釈の基礎

J2SE 5.0の注釈に関しては、既に多くの記事が書かれています(その一部を参考文献に挙げました)。ここでは、簡単な要約だけを書くことにします。注釈は、Javaコード用の、一種のメタデータです。機能的には、複雑なフレームワーク構成を扱うものとして人気が高まっているXDocletスタイルのメタデータと似ていますが、実装に関しては、XDoclet よりもむしろC#属性と共通性があります。

Javaでは、注釈機能の実装に対して、Java言語構文に対する特別な拡張を持ったインターフェース風の構造を使っています。ただし大部分の目的に対しては、このインターフェース風の構造を無視し、むしろ注釈を、名前と値の対のハッシュマップと考えた方が明確になる、と私は思っています。注釈タイプはそれぞれ、その注釈に関連付けられた、固定した名前セットを定義します。それぞれの名前にはデフォルトの値が与えらますが、与えられていない場合には、注釈を使うたびに定義する必要があります。注釈は、特定なJavaコンポーネント・タイプ(例えばクラスやフィールド、メソッド、など)にのみ適用するように規定したり、さらには他の注釈に適用するように規定したりすることができます。(実際、注釈を使う対象としてのコンポーネントを制限するには、制限すべき注釈の定義に対して特別に事前定義された注釈を使います。)

通常のインターフェースとは異なり、注釈の定義には、キーワード、@interfaceを使う必要があります。また、これも通常のインターフェースとは異なり、注釈は「メソッド」定義として、パラメーターを持たず単純な値(プリミティブ型やString、Class、enumなどの型、注釈およびこれらの配列)を返すだけのメソッドしか定義することができません。こうした「メソッド」は、注釈に関連付けられた値に対する名前です。

注釈はpublicやfinalなど、J2SE 5.0以前のJava言語で定義されたキーワード修飾子と同じように、宣言に対する修飾子として使われます。注釈が使われていることは、@シンボルに続いて注釈名があることで示されます。注釈に対して値がある場合には、注釈名に続く括弧の中で、名前と値の対として値が与えられます。

リスト1は注釈宣言のサンプルですが、その宣言の後に続いて、あるメソッドにその注釈を使ったクラスの定義が続いています。このLogMe注釈は、アプリケーションのログに含まれるべきメソッドに対してフラグを上げるためのものです。私はこの注釈に対して、2つの値を与えています。1つは、このコールが含まれるべきログのレベルを表し、もう1つは、メソッド・コールに使われる名前を表します。(何も名前が与えられない場合には、この注釈を扱うコードで実際のメソッド名を代用するという前提で、デフォルトは空のテキストとしています。)次にこの注釈を、StringArrayクラスにある1対のメソッド(つまり、単にデフォルト値を使うmerge()メソッドと、明示的な値を提供するindexOf()メソッド)に対して使っています。

リスト1. ログに関する注釈と、使い方の例
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;

/**
 * Annotation for method to be included in logging.
 */
@Target({ElementType.METHOD})
public @interface LogMe {
    int level() default 0;
    String name() default "";
}

public class StringArray
{
    private final String[] m_list;
    
    public StringArray(String[] list) {
        ...
    }
    
    public StringArray(StringArray base, String[] adds) {
        ...
    }
    
    @LogMe private String[] merge(String[] list1, String[]list2) {
        ...
    }
    
    public String get(int index) {
        return m_list[index];
    }
    
    @LogMe(level=1, name="lookup") public int indexOf(String value) {
        ...
    }
    
    public int size() {
        return m_list.length;
    }
}

次のセクションでは、別の(もっと面白い)アプリケーションを紹介します。


toString()を作る

Javaプラットフォームは、toString()メソッドの形でオブジェクトのテキスト記述を生成するために、便利なフック(hook)を用意しています。このメソッドのデフォルト実装は、根本的な基底クラスであるjava.lang.Objectですが、実際にはこのデフォルト実装をオーバーライドして、もっと便利な記述を提供するように推奨されています。開発者の多くは、最低限として、主にデータ表現であるようなクラスに対しては、独自の実装を提供するようにしています。実は正直に言うと、私はそうしていません。確かにtoString()は便利だと思うのですが、私はこれをオーバーライドする手間はかけていません。toString()実装が実用的であるためには、クラスにフィールドが追加、削除されるのに合わせてtoString()実装を最新に保つ必要がありますが、一般的にこのステップには手間がかかりすぎ、あまり意味がないと思うのです。

注釈を、クラスファイルの変更と組み合わせると、このジレンマから抜け出すことができます。toString()メソッドを維持する上での問題点として、クラスの中でフィールド宣言とコードが分離しているため、フィールドを追加、削除する度に、もう1つ別のものを変更することを覚えておかなければならないのです。フィールド宣言に注釈を使うことによって、どのフィールドをtoString()メソッドに含めておきたいかを容易に示すことができ、一方toString()メソッドの実際の実装は、クラスワーキング・ツールに任せられます。こうすることによって、全てを1ヶ所(フィールド宣言)に集めることができ、コードの維持管理をすることなく、toString()から有用な記述を取得できるのです。

ソースのサンプル

toString()メソッド構築に対する注釈の実装を始める前に、達成しようとしている目標のサンプルを示すことにしましょう。リスト2は、ソースコードの中にtoString()メソッドを持った単純なデータ・ホルダー・クラスのサンプルを示しています。

リスト2. toString()メソッドを持つデータ・クラス
public class Address
{
    private String m_street;
    private String m_city;
    private String m_state;
    private String m_zip;
    
    public Address() {}

    public Address(String street, String city, String state, String zip) {
        m_street = street;
        m_city = city;
        m_state = state;
        m_zip = zip;
    }
    public String getCity() {
        return m_city;
    }
    public void setCity(String city) {
        m_city = city;
    }
    ...
    public String toString() {
        StringBuffer buff = new StringBuffer();
        buff.append("Address: street=");
        buff.append(m_street);
        buff.append(", city=");
        buff.append(m_city);
        buff.append(", state=");
        buff.append(m_state);
        buff.append(", zip=");
        buff.append(m_zip);
        return buff.toString();
    }
}

リスト2のサンプルでは、toString()出力中の全フィールドを、クラス中で宣言される通りに含めるように、そして各フィールド値の前には、出力での識別のためにテキスト「name=」を付けるようにしました。この場合ではテキストは、(メンバー・フィールドを識別するために使った)前に付いている「m_」接頭辞を単に取り除くことによって、フィールド名から直接生成されています。他の場合では、あるフィールドのみを出力に含めるようにしたり、順序を変更したり、値に対する識別テキストを変更したり、あるいは識別テキストを完全に省いてしまうかも知れません。そうしたこと全てができるくらい、注釈のフォーマットは柔軟なのです。

注釈を定義する

toString()生成に対する注釈は、他にも様々な方法で定義することができます。私としては、とにかく使いやすくするために、必要な注釈の数は最小限にしたいと思います。例えば、フィールドに対するデフォルト処理をオーバーライドするために、個々のフィールド注釈と組み合わせてメソッドを生成したいクラスにフラグを上げるために、クラス注釈を使うこともできます。これは大して難しくはありませんが、実装コードはかなり複雑になります。この記事では単純なままとし、インスタンス記述の中に含むべき個々のフィールドに対してのみ注釈を使うようにしています。

私が制御したいと思っている対象の要素としては、どのフィールドを含めるかどうか、フィールド値の前にはテキストがあるかどうか、そのテキストがフィールド名に基づくものかどうか、また出力におけるフィールドの順序などです。リスト3は、このための基本的な注釈を示しています。

リスト3. toString()生成に関する注釈
package com.sosnoski.asm;

import java.lang.annotation.ElementType;
import java.lang.annotation.Target;

@Target({ElementType.FIELD})
public @interface ToString {
    int order() default 0;
    String text() default "";
}

リスト3の注釈は、名前付きの値の対を単に定義して順序をつけ、フィールドの前に付けるテキストを定義しています。この注釈は、@Targetを持つ行でのフィールド宣言に対してのみ使っています。また、それぞれの値にはデフォルトを定義しています。こうしたデフォルトは、生成される注釈情報のうち、バイナリー・クラス表現の中に入るものには適用されません(実行時に、疑似インターフェースとして注釈をアクセスした場合にのみ適用されますが、ここではそうしたアクセスはしません)。ですから実際には、ここでどんな値が使われているかは無関係です。デフォルトを定義することによって、注釈を使う度に値を定義しなくてすむように、値をオプションとしているだけなのです。

注釈を扱う際に覚えておくべきことは、名前付きの値は必ずコンパイル時に定数である必要があり、nullではありえない、ということです。この規則は、(デフォルト値が与えられている場合には)デフォルト値にも適用され、またユーザーが設定する値にも適用されます。この結論は、初期のJava言語定義との一貫性を保つためだと思いますが、Java言語にこれほど大きな変更を加えるような仕様が、この1つの領域にのみ一貫性を保とうとするのは変だと私は思います。


生成を実装する

下準備はできたので、注釈付きクラスがロードされる時にtoString()メソッドを追加する、クラスワーキング変換の実装を見ることにしましょう。この実装には、3つの別々のコードがあります。クラスのローディングをインターセプトする部分、注釈情報にアクセスする部分、そして実際の変換、という3つの部分です。

インスツルメンテーションでインターセプトする

J2SE 5.0では、Javaプラットフォームに様々な機能を追加しています。私個人としては、こうした追加がすべて本当に改善なのか、疑問を持っています。とは言っても、あまり注目されてはいませんが、クラスワーキングにとって非常に便利な新しい機能も2つ追加されています。これらはjava.lang.instrumentパッケージとJVMインターフェースですが、(何よりも)これらを使うことによって、プログラムを実行する際に使用するクラス変換エージェントを規定できるのです。

変換エージェントを使うためには、JVMを起動する時にエージェント・クラスを規定する必要があります。javaコマンドを使ってJVMを起動する時に、-javaagent:jarpath[=options]という形式のコマンドライン・パラメーターを使って、エージェントを規定します。ここで、「jarpath」は、エージェント・クラスを含むJARファイルへのパスであり、「options」は、エージェントに対するパラメーター・ストリングです。エージェントJARファイルは、特別なマニフェスト属性を使って実際のエージェント・クラスを規定しますが、このクラスはメソッド、public static void premain(String options, Instrumentation inst)を定義する必要があります。このエージェントpremain()メソッドは、アプリケーションのmain()メソッドの前に呼ばれ、渡されたjava.lang.instrument.Instrumentationクラス・インスタンスを使って実際の変換プログラム(トランスフォーマー)を登録することができます。

トランスフォーマー・クラスは、1つのtransform()メソッドを定義するjava.lang.instrument.ClassFileTransformerインターフェースを実装する必要があります。トランスフォーマー・インスタンスがInstrumentationクラス・インスタンスで登録されると、そのトランスフォーマー・インスタンスは、JVMの中で作られる各クラスに対して呼ばれます。トランスフォーマーはバイナリー・クラス表現にアクセスすることができ、JVMにロードされる前に、そのクラス表現を変更することができます。

リスト4は、注釈を処理するためのエージェントとトランスフォーマー・クラス(この場合は両者が同じクラスですが、必ずしも同じである必要はありません)の実装を示しています。transform()実装はASMを使って、提供されたバイナリー・クラス表現をスキャンして適当な注釈を探し、(そのクラスの)注釈が付いたフィールドに関する情報を収集します。注釈の付いたフィールドが見つかると、そのクラスは生成されたtoString()メソッドを含むように修正され、修正されたバイナリー表現が返されます。見つからない場合には、transform()メソッドは単にnullを返し、何も修正が必要ないことを知らせます。

リスト4. エージェントとトランスフォーマー・クラス
package com.sosnoski.asm;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;

public class ToStringAgent implements ClassFileTransformer
{
    // transformer interface implementation
    public byte[] transform(ClassLoader loader, String cname, Class class,
        ProtectionDomain domain, byte[] bytes)
        throws IllegalClassFormatException {
        System.out.println("Processing class " + cname);
        try {
            
            // scan class binary format to find fields for toString() method
            ClassReader creader = new ClassReader(bytes);
            FieldCollector visitor = new FieldCollector();
            creader.accept(visitor, true);
            FieldInfo[] fields = visitor.getFields();
            if (fields.length > 0) {
                
                // annotated fields present, generate the toString() method
                System.out.println("Modifying " + cname);
                ClassWriter writer = new ClassWriter(false);
                ToStringGenerator gen = new ToStringGenerator(writer,
                        cname.replace('.', '/'), fields);
                creader.accept(gen, false);
                return writer.toByteArray();
                
            }
        } catch (IllegalStateException e) {
            throw new IllegalClassFormatException("Error: " + e.getMessage() +
                " on class " + cname);
        }
        return null;
    }
    
    // Required method for instrumentation agent.
    public static void premain(String arglist, Instrumentation inst) {
        inst.addTransformer(new ToStringAgent());
    }
}

J2SE 5.0でのインスツルメンテーション機能には、JVMにロードされている全クラスにアクセスできる機能や、(もしJVMがサポートしていれば)既存のクラスを再定義することまでできるなど、ここで示した以上の機能を持っています。このコラムでは、こうした他の機能は無視し、注釈の処理やクラスの変更に使われるASMコードのみに限定して先に進むことにします。

メタデータを累積する

ASM 2.0では、注釈の処理が容易にできます。前回学んだ通り、ASMでは、クラス・データの全コンポーネントをレポートするためにビジター(visitor)の手法を使っています。J2SE 5.0の注釈では、org.objectweb.asm.AnnotationVisitorインターフェースを使っていると言われています。このインターフェースは幾つかのメソッドを定義していますが、ここではその中の2つのみを使います。visitAnnotation()は注釈を処理する時に呼ばれるメソッドであり、visit()は、注釈に対する特定な名前と値の対を処理する時に呼ばれるメソッドです。また、実際のフィールド情報も必要ですが、これは基本のorg.objectweb.asm.ClassVisitorインターフェースのvisitField()メソッドを使ってレポートされます。

対象とする2つのインターフェースの全メソッドを実装するのは退屈な作業ですが、幸いASMには、独自のビジターを書くためのベースとして、org.objectweb.asm.commons.EmptyVisitorという便利なクラスが提供されています。EmptyVisitorは単に、様々な、あらゆるタイプのビジターの空実装を提供します。これを利用すると、関係のあるビジター・メソッドのみをサブクラス化し、オーバーライドすることができるのです。リスト5は、ToString注釈を処理するために私が実装したFieldCollectorクラスですが、これはEmptyVisitorクラスを拡張したものです。またここには、収集したフィールド情報を保持するために使用するFieldInfoクラスも含まれています。

リスト5. 注釈を処理するクラス
package com.sosnoski.asm;

import java.util.ArrayList;
import java.util.Arrays;

import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.EmptyVisitor;

/**
 * Visitor implementation to collect field annotation information from class.
 */
public class FieldCollector extends EmptyVisitor
{
    private boolean m_isIncluded;
    private int m_fieldAccess;
    private String m_fieldName;
    private Type m_fieldType;
    private int m_fieldOrder;
    private String m_fieldText;
    private ArrayList m_fields = new ArrayList();
    
    // finish field handling, once we're past it
    private void finishField() {
        if (m_isIncluded) {
            m_fields.add(new FieldInfo(m_fieldName, m_fieldType,
                m_fieldOrder, m_fieldText));
        }
        m_isIncluded = false;
    }
    
    // return array of included field information
    public FieldInfo[] getFields() {
        finishField();
        FieldInfo[] infos =
            (FieldInfo[])m_fields.toArray(new FieldInfo[m_fields.size()]);
        Arrays.sort(infos);
        return infos;
    }
    
    // process field found in class
    public FieldVisitor visitField(int access, String name, String desc,
        String sig, Object init) {
        
        // finish processing of last field
        finishField();
        
        // save information for this field
        m_fieldAccess = access;
        m_fieldName = name;
        m_fieldType = Type.getReturnType(desc);
        m_fieldOrder = Integer.MAX_VALUE;
        
        // default text is empty if non-String object, otherwise from field name
        if (m_fieldType.getSort() == Type.OBJECT &&
            !m_fieldType.getClassName().equals("java.lang.String")) {
            m_fieldText = "";
        } else {
            String text = name;
            if (text.startsWith("m_") && text.length() > 2) {
                text = Character.toLowerCase(text.charAt(2)) +
                    text.substring(3);
            }
            m_fieldText = text;
        }
        return super.visitField(access, name, desc, sig, init);
    }
    
    // process annotation found in class
    public AnnotationVisitor visitAnnotation(String sig, boolean visible) {
        
        // flag field to be included in representation
        if (sig.equals("Lcom/sosnoski/asm/ToString;")) {
            if ((m_fieldAccess & Opcodes.ACC_STATIC) == 0) {
                m_isIncluded = true;
            } else {
                throw new IllegalStateException("ToString " +
                    "annotation is not supported for static field +" +
                    " m_fieldName");
            }
        }
        return super.visitAnnotation(sig, visible);
    }
    
    // process annotation name-value pair found in class
    public void visit(String name, Object value) {
        
        // ignore anything except the pair defined for toString() use
        if ("order".equals(name)) {
            m_fieldOrder = ((Integer)value).intValue();
        } else if ("text".equals(name)) {
            m_fieldText = value.toString();
        }
    }
}

package com.sosnoski.asm;

import org.objectweb.asm.Type;

/**
 * Information for field value to be included in string representation.
 */
public class FieldInfo implements Comparable
{
    private final String m_field;
    private final Type m_type;
    private final int m_order;
    private final String m_text;
    
    public FieldInfo(String field, Type type, int order,
        String text) {
        m_field = field;
        m_type = type;
        m_order = order;
        m_text = text;
    }
    public String getField() {
        return m_field;
    }
    public Type getType() {
        return m_type;
    }
    public int getOrder() {
        return m_order;
    }
    public String getText() {
        return m_text;
    }
    
    /* (non-Javadoc)
     * @see java.lang.Comparable#compareTo(java.lang.Object)
     */
    public int compareTo(Object comp) {
        if (comp instanceof FieldInfo) {
            return m_order - ((FieldInfo)comp).m_order;
        } else {
            throw new IllegalArgumentException("Wrong type for comparison");
        }
    }
}

リスト5のコードは、フィールドが訪問を受けた時にフィールド情報を保存します。これは、そのフィールドに注釈が存在する場合には、後でその情報が必要になるためです。注釈が訪問を受けると、このコードはそれがToString注釈かどうかをチェックし、ToString注釈であれば、ToStringメソッド生成に使われるリストにカレント・フィールドを含めるべきである、というフラグを立てます。注釈の、名前と値の対が訪問を受けると、コードはToString注釈で定義される2つの名前をチェックし、それぞれの名前に対する値が見つかると、その値を保存します。こうした名前に対する真のデフォルト値は(注釈定義の中で使われるデフォルトとは異なり)、フィールド・ビジター・メソッドの中で設定されます。ですから、こうしたデフォルトは、ユーザーが定義する任意の値によって上書きされます。

ASMはまずフィールドを訪れ、次に注釈と注釈の値を訪れます。フィールドに対する注釈を処理し終わった後に呼ばれるような、特別なメソッドはありません。ですから、新しいフィールドを処理する時、および完成したフィールド・リストが要求された時に呼ばれるfinishField()メソッドがあるだけです。getFields()メソッドは、この完成したフィールド・リストを、注釈の値によって決められる順序で呼び出し側に提供します。

クラスを変換する

リスト6は、実装コードの最後の部分であり、ここで実際にtoString()メソッドをクラスに追加します。このコードは、前回のコラムの、ASMを使ってクラスを作るコードと似ていますが、今回は既存のクラスを修正するために、異なった構造が必要です。今回はASMが使うビジター手法によって、少し複雑になっています。つまり既存のクラスを修正するために、カレント・クラスの内容全てを訪ね、その内容を最終的にクラス・ライターにまで渡し、削除したい部分をフィルターで除去した上で、新しい内容をライターに直接追加する必要があるのです。このためのベース・クラスとして、org.objectweb.asm.ClassAdapterが便利です。このベース・クラスは、提供されたクラス・ライター・インスタンスに対するパス・スルー処理を実装しているため、特別な処理を必要とするメソッドのみをオーバーライドすることができます。

リスト6. toString()メソッドを追加する
package com.sosnoski.asm;

import org.objectweb.asm.ClassAdapter;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;

/**
 * Visitor to add <code>toString</code> method to a class.
 */
public class ToStringGenerator extends ClassAdapter
{
    private final ClassWriter m_writer;
    private final String m_internalName;
    private final FieldInfo[] m_fields;
    
    public ToStringGenerator(ClassWriter cw, String iname, FieldInfo[] props) {
        super(cw);
        m_writer = cw;
        m_internalName = iname;
        m_fields = props;
    }
    
    // called at end of class
    public void visitEnd() {
        
        // set up to build the toString() method
        MethodVisitor mv = m_writer.visitMethod(Opcodes.ACC_PUBLIC,
            "toString", "()Ljava/lang/String;", null, null);
        mv.visitCode();
        
        // create and initialize StringBuffer instance
        mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuffer");
        mv.visitInsn(Opcodes.DUP);
        mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuffer",
            "<init>", "()V");
        
        // start text with class name
        String name = m_internalName;
        int split = name.lastIndexOf('/');
        if (split >= 0) {
            name = name.substring(split+1);
        }
        mv.visitLdcInsn(name + ":");
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuffer",
            "append", "(Ljava/lang/String;)Ljava/lang/StringBuffer;");
        

        // loop through all field values to be included
        boolean newline = false;
        for (int i = 0; i < m_fields.length; i++) {
            
            // check type of field (objects other than Strings need conversion)
            FieldInfo prop = m_fields[i];
            Type type = prop.getType();
            boolean isobj = type.getSort() == Type.OBJECT &&
                !type.getClassName().equals("java.lang.String");
            
            // format lead text, with newline for object or after object
            String lead = (isobj || newline) ? "\n " : " ";
            if (prop.getText().length() > 0) {
                lead += prop.getText() + "=";
            }
            mv.visitLdcInsn(lead);
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
                "java/lang/StringBuffer", "append",
                "(Ljava/lang/String;)Ljava/lang/StringBuffer;");
            
            // load the actual field value and append
            mv.visitVarInsn(Opcodes.ALOAD, 0);
            mv.visitFieldInsn(Opcodes.GETFIELD, m_internalName,
                prop.getField(), type.getDescriptor());
            if (isobj) {
                
                // convert objects by calling toString() method
                mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
                    type.getInternalName(), "toString",
                    "()Ljava/lang/String;");
                mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
                    "java/lang/StringBuffer", "append",
                    "(Ljava/lang/String;)Ljava/lang/StringBuffer;");
                
            } else {
                
                // append other types directly to StringBuffer
                mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
                    "java/lang/StringBuffer", "append", "(" +
                    type.getDescriptor() + ")Ljava/lang/StringBuffer;");
                
            }
            newline = isobj;
        }
        
        // finish the method by returning accumulated text
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuffer",
            "toString", "()Ljava/lang/String;");
        mv.visitInsn(Opcodes.ARETURN);
        mv.visitMaxs(3, 1);
        mv.visitEnd();
        super.visitEnd();
    }
}

リスト6では、オーバーライドする必要があるのはvisitEnd()メソッドのみです。このメソッドは、既存クラスの全情報が訪問を受け終わった時に呼ばれるため、新しい内容の追加を実装するには便利な場所です。ここではvisitEnd()メソッドを使って、処理対象のクラスにtoString()メソッドを追加しています。コード生成では、toString()出力をうまくフォーマットするために、幾つかの機能を追加していますが、基本的な原則は単純です。つまり提供されたフィールドの配列をループし、最初は先頭のテキストを付加するためのコードを、次にはStringBufferインスタンスに実際のフィールド値を付加するためのコードを生成しています。

現在のコードは(クラスのロードをインターセプトするためにインスツルメンテーション・メソッドを使っているため)、J2SE 5.0でしか動作しません。StringBufferに等価で、より効率的なものとして、新しいStringBuilderクラスを使うこともできたのですが、ここでは古い方法のままにしました。その理由は、このコードを使った追加作業を次回のコラムで取り上げる予定があるためですが、皆さんがJ2SE 5.0専用のコードを書く際には、StringBuilderを頭に置いておく方が良いでしょう。

ToStringの実際の動作

リスト7は、ToString注釈に対する、ちょっとしたテスト・クラスを示しています。実際の注釈に対して、ある場合には名前と値の対を規定し、別の場合では注釈を単独で使うなど、ここでは様々なスタイルを混ぜて使っています。Runクラスは、単に何らかのサンプルデータを持つCustomerクラスのインスタンスを作り、toString()メソッド・コールの結果を出力するだけです。

リスト7. ToStringに対するテスト・クラス
package com.sosnoski.dwct;

import com.sosnoski.asm.ToString;

public class Customer
{
    @ToString(order=1, text="#") private long m_number;
    @ToString() private String m_homePhone;
    @ToString() private String m_dayPhone;
    @ToString(order=2) private Name m_name;
    @ToString(order=3) private Address m_address;
    
    public Customer() {}
    public Customer(long number, Name name, Address address, String homeph,
        String dayph) {
        m_number = number;
        m_name = name;
        m_address = address;
        m_homePhone = homeph;
        m_dayPhone = dayph;
    }
    ...
}
...
public class Address
{
    @ToString private String m_street;
    @ToString private String m_city;
    @ToString private String m_state;
    @ToString private String m_zip;
    
    public Address() {}
    public Address(String street, String city, String state, String zip) {
        m_street = street;
        m_city = city;
        m_state = state;
        m_zip = zip;
    }
    public String getCity() {
        return m_city;
    }
    public void setCity(String city) {
        m_city = city;
    }
    ...
}
...
public class Name
{
    @ToString(order=1, text="") private String m_first;
    @ToString(order=2, text="") private String m_middle;
    @ToString(order=3, text="") private String m_last;
    
    public Name() {}
    public Name(String first, String middle, String last) {
        m_first = first;
        m_middle = middle;
        m_last = last;
    }
    public String getFirst() {
        return m_first;
    }
    public void setFirst(String first) {
        m_first = first;
    }
    ...
}
...
public class Run
{
    public static void main(String[] args) {
        Name name = new Name("Dennis", "Michael", "Sosnoski");
        Address address = new Address("1234 5th St.", "Redmond", "WA", "98052");
        Customer customer = new Customer(12345, name, address,
            "425 555-1212", "425 555-1213");
        System.out.println(customer);
    }
}

最後にリスト8は、テスト実行によるコンソール出力を示しています(最初の行は、枠に収まるように折り返しています)。

リスト8. テスト実行によるコンソール出力(最初の行は折り返しています)
[dennis@notebook code]$ java -cp lib/asm-2.0.RC1.jar:lib/asm-commons-2.0.RC1.jar
  :lib/tostring-agent.jar:classes -javaagent:lib/tostring-agent.jar
  com.sosnoski.dwct.Run
Processing class sun/misc/URLClassPath$FileLoader$1
Processing class com/sosnoski/dwct/Run
Processing class com/sosnoski/dwct/Name
Modifying com/sosnoski/dwct/Name
Processing class com/sosnoski/dwct/Address
Modifying com/sosnoski/dwct/Address
Processing class com/sosnoski/dwct/Customer
Modifying com/sosnoski/dwct/Customer
Customer: #=12345
 Name: Dennis Michael Sosnoski
 Address: street=1234 5th St. city=Redmond state=WA zip=98052
 homePhone=425 555-1212 dayPhone=425 555-1213

まとめ

ここでは、ASMをJ2SE 5.0の注釈と組み合わせ、実行時にクラスファイルの自動修正を実行する方法を説明しました。ここで例として使用したToString注釈は、なかなか面白く、しかも(少なくとも私にとっては)便利なものです。ToString注釈だけを使う場合には、コードはそれほど読みにくくなりません。しかし、もし様々な目的に対して注釈を使うようになると(注釈を利用するために、いかに多くのJava拡張が書かれ、あるいは書き直されているかを考えれば、今後は実際に様々な目的で使われるのは確実です)、コードの中で目障りな存在になるでしょう。

この点に関しては、今後のコラムで、注釈と外部コンフィギュレーション・ファイルとのトレードオフを調べる際に、再度取り上げることにします。私の個人的な意見としては、どちらにも使い道があると思います。注釈は主に、コンフィギュレーション・ファイルを手軽に置き換えるものとして開発されたものですが、ある場合においては、相変わらず別のコンフィギュレーション・ファイルの方が適切なのです。念のために言っておくと、ToString注釈は、適切な使い方の一例だと思います。

J2SE 5.0での拡張を扱う上での制限の一つとして、JDK 1.5コンパイラーの出力が、JDK 1.5 JVMでしか使えないという問題があります。クラスワーキング・ツールキットの次回の記事では、この制限を回避するツールを調べ、ToString実装を古いJVMで動作するように修正する方法を説明する予定です。


ダウンロード

内容ファイル名サイズ
Sample codej-cwt06075code.zip175KB

参考文献

  • この記事の先頭あるいは最後にあるCodeアイコンをクリックして(あるいはダウンロード・セクションを見てください)、この記事で議論したソースコードをダウンロードしてください。
  • 高速で柔軟な、Javaバイトコード操作フレームワークであるASMの詳細を学んでください。
  • J2SE 5.0での注釈を扱う際の簡単な要点を知りたい人は、Brett McLaughlinによる2回シリーズの記事を調べてみてください(developerWorks, 2004年9月)。第1回ではコードにメタデータを追加する方法、第2回ではカスタム注釈を作る方法を解説しています。
  • ASMを使ってJ2SE 5.0注釈を読み書きする方法を学ぶには、Eugene Kuleshovによる「Create and Read J2SE 5.0 Annotations with the ASM Bytecode Toolkit」を読んでください。
  • 古いバージョンのJavaプラットフォームとJ2SE 5.0がどのように異なるかに興味のある人は、John ZukowskiによるdeveloperWorksのシリーズ、タイガーを使いこなすシリーズを読んで、違いの詳細を学んでください。
  • J2SEでの注釈の全てを知るために、JSR-175 -A Metadata Facility for the Java Programming Languageを見てください。
  • Dennis Sosnoskiによる、クラスワーキング・ツールキット・シリーズの他の記事も読んでください。
  • Peter Haggarによる「Java bytecode: Understandingbytecode makes you a better programmer」(developerWorks, 2001年7月)を読んで、Javaバイトコード設計について学んでください。
  • JVMのアーキテクチャーや命令セットに関する素晴らしい参照として、Bill VennersによるInside the Java Virtual Machine(Artima Software, Inc.、2004年刊)を見てください。実際の本を購入する前に、オンラインで一部の章を見ることができます。
  • JVM操作のすべてに関して決定的な資料である、公式のJava Virtual Machine Specification を、オンラインで閲覧、あるいは購入することができます。
  • この記事の著者のDennis Sosnoskiによる、Javaプログラミング・ダイナミックス・シリーズの全記事を読んで、Javaのクラス構造、リフレクション、クラスワーキングなどを知るツアーに出かけてください(日本語に翻訳されていない記事もあります)。
  • オープンソースのJikesプロジェクトでは、非常に高速で厳密に規格準拠したJavaプログラミング言語用コンパイラーを提供しています。昔ながらの方法で、(Javaソースコードから)バイトコードを生成するために使ってみてください。
  • developerWorksのJava technologyゾーンには、Java技術に関する資料が他にも豊富に用意されています。技術的なドキュメンテーションや、ハウツー記事、教育資料、ダウンロード、製品情報など、様々な情報を得ることができます。
  • New to Java technologyには、Javaプログラミングを始めるために役立つ最新情報が用意されています。
  • developerWorks blogsに参加して、developerWorksコミュニティーに加わってください。
  • Java関連の書籍と他の技術的な話題をご覧ください。

コメント

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, Open source
ArticleID=219475
ArticleTitle=クラスワーキング・ツールキット: 注釈とASM
publish-date=06072005