IBM®
本文へジャンプ
    Japan [変更]    ご利用条件
 
 
検索範囲検索:    
    ホーム    製品    サービス & ソリューション    サポート & ダウンロード    マイアカウント    
skip to main content

developerWorks Japan  >  Java technology  >

Javaプログラミングのダイナミックス 第6回: Javassistを使用したアスペクト指向の変更

バイトコードの検索・置換変換にJavassistを使用する

developerWorks
ページオプション

JavaScript を要するドキュメントオプションは表示されません

原文はこちら

原文はこちら


レベル: 初級

Dennis Sosnoski (dms@sosnoski.com), President, Sosnoski Software Solutions, Inc.

2004年 3月 02日

JavaコンサルタントであるDennis Sosnoskiは、Javassistフレームワークの3部のカバレッジで最後に最高の技術を提示します。今回、彼はどのようにJavassistの探索・置換サポートが、テキスト・エディターの全置換コマンドと同じぐらい簡単にJavaのバイトコード編集を行うかを示してくれます。メソッド呼び出しで渡されたパラメーター変更で、特定のフィールドあるいはパッチへの全ての書き込みをレポートしたいですか?Javassistはそれを容易にし、Dennisがその方法を教えてくれます。

このシリーズの第4回第5回では、バイナリークラスへのローカライズ変更のために、Javassistをどのように使用するかという方法を提示しました。今回は、バイトコード内の特定なメソッド、あるいはフィールドの全ての使用を発見するJavassistのサポートを利用して、より強力なフレームワークの使用方法を学習します。この機能は、Javassistのパワーにとってバイトコードを指定するソースコードのような方法のサポートと同じくらい重要です。さらに、選択的な置換操作のサポートにより、Javassistはアスペクト指向プログラミングの機能を標準なJavaコードへ追加する素晴らしいツールになります。

第5回では、Javassistを使ってどのようにクラスローディング・プロセスをインターセプトするのかを見てきました。また、ロード時にバイナリークラス表現への変更を行うことも見てきました。この記事で説明するシステマチックなバイトコード変換は、staticクラスファイル変換か実行時インターセプションのいずれかに使用することができますが、実行時に使用すると、特に有用となります。

バイトコード変更の処理

Javassistは、システマチックなバイトコード変更を処理する2つの方法を提供しています。javassist.CodeConverterクラスを使用する1つ目のテクニックは少し簡単ですが、達成できることに関しては、多くの制限があります。2つ目のテクニックは、javassist.ExprEditorクラスのカスタムサブクラスを使用します。多少手間がかかりますが、追加された柔軟性には努力しただけの価値はあります。この記事で2つの方法の例題を見ていきます。




上に戻る


コード変換

このシリーズのこれまでの記事を見逃さないでください。

第1回 「クラスとクラスのロード処理」(2003年4月)

第2回 「リフレクション入門」(2003年6月)

第3回 「実用的なリフレクション」(2003年7月)

第4回 「Javassistでのクラス変換」(2003年9月)

第5回 「オンザフライでクラスを変換する」(2004年2月)

第7回 「Bytecode engineering with BCEL」(2004年4月)

システマチックなバイトコード変更する1番目のJavassist技術は、javassist.CodeConverterクラスを使用します。この技術を利用するには、CodeConverterクラスのインスタンスを作成し、1つ以上の変換アクションでそれを構成します。それぞれの変換は、その変換タイプを識別するメソッドの呼び出しを使用して構成されます。これらは、メソッド呼び出し変換、フィールドアクセス変換、新規オブジェクト変換という3つのカテゴリーに分類されます。

リスト1は、メソッド呼び出し変換を使用した例題です。この場合、この変換はメソッドが呼ばれているという通知を追加します。このコードでは、最初に、変換プログラム(以前の第5回のような)を使用するためにそれを構成して、至る所で使用するためにjavassist.ClassPoolインスタンスをゲットします。そして、ClassPoolによりメソッド定義のペアにアクセスします。この最初のメソッド定義は、モニターされるset- styleメソッド(クラスやメソッド名形式のコマンドライン引数で)のためであり、2番目のメソッド定義は、最初のメソッドへの呼び出しをリポートするTranslateConvertクラスにあるreportSet()メソッドのためです。

一度メソッド情報を持つと、setメソッドへの各呼び出し前に、reportingメソッドへの呼び出しを追加する変換を構成するCodeConverter insertBeforeMethod()を使用することができます。そして、しなければならないことは、1つ以上のクラスにこのコンバーターを適用させることです。リスト1のコードでは、クラスオブジェクトのinstrument()メソッドへの呼び出しを持つConverterTranslator内部クラスのonWrite()メソッド内でこれを行います。これにより、ClassPoolインスタンスからロードされるあらゆるクラスに変換が自動的に適用されるでしょう。


リスト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);
        }
    }
}

構成するのにかなり複雑な操作ですが、一度セット・アップしてしまうと、簡単に動作します。リスト2は、テストケースとして使うコード・サンプルを示しています。ここでは、値にアクセスするBeanTestプログラムが使用するbeanのようなgetやsetメソッドを持つテストオブジェクトをBeanが提供しています。


リスト2. bean テスター
                
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();
    }
}

ここに、リスト2のBeanTestプログラムを直接実行した場合の結果出力があります。

[dennis]$ java -cp . BeanTest
Bean values are originalA and originalB
Bean values are newA and newB

リスト1TranslateConvertプログラムを使用してBeanTestを実行し、モニタへsetメソッドのうちの1つを指定すれば、結果出力はこのようになるでしょう。

[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

すべて前と同様に動作しますが、今回はプログラムが実行されている間、選択されたメソッドが呼ばれているという通知があります。

この場合、例えば、第4回での技術を使用したsetメソッド本文へコードを追加すれば、これと同じ結果が簡単に達成できたかもしれません。ここでの違いは、使用ポイントでコードを追加することにより、柔軟性が得られるということです。例えば、ロードされているクラス名をチェックするようにTranslateConvert.ConverterTranslator onWrite()メソッドを修正し、リストに含まれているクラスの変換だけをするでしょう。setメソッドの本文に直接コードを追加すると、そのような選択的なモニタリングができません。

システマチックなバイトコード変換により提供される柔軟性は、それらを標準Javaコードのアスペクト指向の拡張を実装するための強力なツールにするものです。これに関して、この記事の残りでもっと見ていきます。

変換の制限

CodeConverterによる変換は役に立ちますが、制限があります。例えば、ターゲットメソッドが呼ばれる前後で、モニタリングメソッドを呼び出したい場合、そのモニタリングメソッドはstatic void として定義される必要があり、ターゲットメソッドと同じ数、同じタイプのパラメーターに従って、(ターゲットメソッドのクラスの)シングルパラメータを持たなければなりません。モニタリングメソッドが呼ばれると、ターゲットメソッドの全引数に従って、一番目(最初の)引数として実際のターゲットオブジェクトを渡します。

この厳密な構造は、モニタリングメソッドがターゲットクラスやメソッドに正確に一致する必要があることを意味します。では例として、異なるターゲットクラスで使用できるように、一般的なjava.lang.Objectパラメータを持つよう、リスト1reportSet()メソッドの定義を変更しましょう。

    public static void reportSet(Object target, String value) {
        System.out.println("Call to set value " + value);
    }

コンパイルは上手くいきますが、実行するとブレークしてしまいます。

[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 ...

しかし、この制限をうまく回避する方法があります。その解決策は、ターゲットメソッドと一致するカスタム・モニタリングメソッドを実行時に生成することです。しかしながら、これを行うには多くの手間がかかりますので、この記事では行いません。幸運にも、Javassistが、システマチックなバイトコード変換処理の方法を提供します。この方法は、javassist.ExprEditorを使用するので、CodeConverterと比べてより柔軟でより強力なのです。




上に戻る


簡単なクラスのミューティレーション

javassist.ExprEditorを使用したバイトコード変換は、CodeConverterを使用する場合と同じ原則を持っています。しかし、ExprEditorの方法は、理解するのがちょっと難しいので、基本原則の実証から始め、その後で実際の変換について説明していきます。

リスト3は、アスペクト指向の変換において、ターゲットの基本アイテムをレポートするためのExprEditorの使用方法を示しています。ここでは、3つの基本クラスメソッドをオーバーライドし、VerboseEditorExprEditorをサブクラス化します。すべてにedit()と名前を付けますが、異なったパラメータータイプを持っています。リスト1のコードのように、ClassPoolインスタンスからロードされる全てのクラスに対してクラスオブジェクトのinstrument()メソッドの呼び出しでインスタンスを渡し、DissectionTranslatorクラスのonWrite()メソッド内からこのサブクラスを実際に使用します。


リスト3.クラスのディセクター
                
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は、リスト2BeanTestプログラム上でリスト4のDissectプログラムの実行により生成された結果を示しています。これは、全メソッドの呼び出し、フィールド・アクセス、新しいオブジェクト生成をリスト表示しており、それぞれロードされたクラスのメソッド内で行われた内容の詳細です。


リスト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 に適切なメソッドを実装することで、ブロックのcatchすることができました。しかし、これらのコンポーネントアイテムに関する情報を単にリスト表示するだけでは退屈ですので、実際にアイテムの修正について見ていきましょう。

進行の分析

リスト4にあるクラスの分析は、基本コンポーネントの操作をリスト表示しています。アスペクト指向の機能を実装する時、これらがどれだけ役立つかということを確かめるのは簡単です。例えば、選択されたフィールドへの全ての書き込みアクセスをレポートするロガーは、多くのアプリケーションで適用する有用なアスペクト(様相)でしょう。結局、それは私が皆さんに見ようとしている方法なのです。

この記事のテーマにとって幸運にも、ExprEditorはどのような操作がコード中に存在しているのかを教えてくれるだけでなく、レポートされている操作の修正が可能です。様々なExprEditor.edit()メソッドで渡されるパラメータタイプは、それぞれのreplace()メソッドの定義を呼び出します。もし、通常のJavassistのソースコード形式にあるステートメント(第4回でカバーされた)をこのメソッドに渡すと、そのステートメントはバイトコードにコンパイルされ、オリジナル操作の置き換えに使われます。これは、バイトコードを容易にスライシングしたり、ダイシングしたりします。

リスト5は、コード置換の適用を示しています。ロギング操作ではなく、ここでは選択されたフィールドへ格納されているString値を実際に修正することにします。FieldSetEditorでは、フィールド・アクセスと一致するメソッド・シグネチャをインプリメントします。このメソッド内では、フィールド名は検索しているものか、操作は格納されているかという2つのことをチェックします。一致を見つけると、オリジナルの格納を実際のTranslateEditorクラス内にあるreverse()メソッドへの呼び出し結果を使用するものに置き換えます。reverse()メソッドはオリジナルのstringにある文字順序を単に逆にし、使用されたことを示すメッセージを出力します。


リスト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());
            }
        }
    }
}

ここに、リスト2BeanTestプログラム上でTranslateEditorを実行した結果があります。

[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

うまく、Bean.m_a フィールド (コンストラクタおよびsetメソッドにあるもの)への各ストアにおいて追加コードへの呼び出しに移植することができました。フィールドからのロード上で同様の修正の実装により、この結果を打ち消すことができましたが、個人的には、最初に行ったことより、逆になった値にかなり興味がありますので、これらを続けていきます。




上に戻る


Javassistの要約

この記事では、Javassistを使用して、より簡単にシステマチックなバイトコード変換を行う方法を学習しました。これに最近の2つの記事を組み合わせると、別々の組み込みステップあるいは実行時に、Javaアプリケーションのアスペクト指向な変換を実装する堅実な基礎を持つでしょう。

この方法のパワーのより良いアイデアを得るために、Javassistを中心として構築されたJBoss アスペクト指向プログラミングプロジェクト(JBossAOP)を見てみたいと思うかもしれません。JBossAOPは、アプリケーション・クラスが行う様々な異なる操作を定義するためにXML構成ファイルを使用します。これらには、フィールド・アクセスあるいはメソッド呼び出しでのインターセプターの使用や既存クラスへのミックスインのインタフェース実装の追加を含んでいます。JBossAOPは、現在開発中のJBossアプリケーションサーバのバージョンに組み込まれていますが、JBoss以外のアプリケーションでもスタンドアローン・ツールとして利用可能です。

このシリーズの次の題材は、Apache Software FoundationのJakarta ProjectのであるByte Code Engineering Library (BCEL)です。BCELはJava のclassworkingを目的として、最も広く使用されているフレームワークのうちの1つです。BCFLは、Javassistの強さであるソースレベルの処理ではなく、個々のバイトコード命令に焦点を置きながら、最近の3つの記事で見てきたJavassistの方法とはかなり異なるバイトコードの使用方法を提供してくれます。次回、バイトコードのアセンブラレベルでの動作について十分な詳細をチェックしてください。



参考文献

  • Dennis Sosnoskiによる「Javaプログラミングのダイナミックス」シリーズの残りをチェックしてください。

  • この記事のサンプルコードをダウンロードして下さい。

  • Javassist は、東京工業大学 情報理工学研究科の千葉 滋によって発案され、近年、新しいアスペクト指向プログラミング機能の追加基盤となるオープンソースのJBoss アプリケーションサーバー・プロジェクトに加わりました。SourceforgeのJBoss project FilesのページからJavassistの現在のリリースをダウンロード可能です。

  • Peter Haggarの「Java bytecode: Understanding bytecode makes you a better programmer」(developerWorks 、2001年7月) で、Java のバイトコード設計についてさらに深い知識を得ることができます。

  • アスペクト指向プログラミングをもっと知りたいですか?AspectJ 言語を使用する概要のために、Nicholas Lesieckiの「アスペクト指向プログラミングで、モジュール性を改善する」(developerWorks 、2002年1月)をご覧下さい。さらに、Andrew Gloverによる最近の記事の「AOPが密結合の憂うつさを取り除く」(developerWorks 、2004年2月)では、AOPの機能設計概念であるstatic crosscutting(静的なcrosscutting)がどのようにしてからみ合う密結合コードを強力で拡張可能なエンタープライズアプリケーションに変えるのかが分かるでしょう。

  • オープンソースのJikes Projectは、Javaプログラミング言語のための非常に高速で極めて従順なコンパイラを提供します。旧式の方法でJavaソースコードからバイトコードを生成するために使用してください。

  • Javaに関連する何百ものタイトルを含む技術本の包括的なリストのあるDeveloper Bookstoreを訪れてみて下さい。

  • developerWorksJava technologyゾーンには、多数のJavaテクノロジーに関する参考文献があります。


著者について

Photo of Dennis Sosnoski

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




記事の評価


サイト改善のため、ご意見をお寄せください。こちらのフォームからお願いいたします。



はいいいえわからない
 


 


12345
不充分・不完全である大変素晴らしい
 


この記事を共有する

はてなブックマーク はてなブックマーク livedoorクリップ livedoorクリップ del.icio.us del.icio.us Buzzurl(バザール) Buzzurl(バザール) Choix! Choix!
Saafブックマーク Saafブックマーク FC2ブックマーク FC2ブックマーク MM/memo MM/memo ニフティクリップ ニフティクリップ Yahoo!ブックマーク Yahoo!ブックマーク
CZブックマーク CZブックマーク newsing newsing




上に戻る


    日本IBMについて プライバシー お問い合わせ