こんにちは。 キャスレーコンサルティング SI(システム・インテグレーション)部の満石です。

今回は、バイトコードを操作することでEnumの挙動を変える方法を簡単な例で紹介します。

予めお断りしておきますが、バイトコードを操作してEnumを書き換えることは基本的におすすめできません。
なぜなら、ソースコードから読み解けるものとは違う挙動をさせることになり、特に保守の観点でリスクが大きいからです。

しかし、大量のコード定義用のEnumが存在しているけれど、それをリリース無しで書き換える必要があるような
特殊な条件の場合に有効な手段になることもあります。

書き換える内容

以前、再帰的ジェネリクスを活用したJava8時代のコード定義Enum用インタフェースを作成してみたで作成した、F1Constructorを書き換え対象のEnumとします。

書き換え内容は2017年第4戦終了時のコンストラクターズポイントランキング順を持つ状態から
2017年第11戦終了時のコンストラクターズポイントランキング順への変更です。

とは言っても、ザウバーが9位、マクラーレンが10位から逆転して、
9位がマクラーレン、10位がザウバーに入れ替わるだけです。

実行環境

今回はバイトコード操作ライブラリとして、Javassistを使います。
バージョンは現時点での最新の安定バージョンである、3.21.0-GAです。
Javaのバージョンも最新の1.8.0_144で実行します。

バイトコードを書き換えるプログラムの作成

バイトコードを書き換えるプログラムは、書き換え対象となるプログラムとは別物(別のjarファイル)として
作成する必要があります。

なぜなら、バイトコードを書き換えるのはクラスがロードされる前に限られているからです。
クラスがロードされた後に書き換えようとすると、java.lang.LinkageErrorが発生します。

そのため、mainメソッドよりも前に実行される、premainというメソッドを実装したクラスを作成する必要があります。

premainメソッドの宣言は

public static void premain(String agentArgs, Instrumentation inst)

と決められています。

Javaにpremainなんてメソッドがあるのか!と驚く人いると思います。
実はJava SE 5から使えるようになっているので、もう使えるようになってから10年以上経っています。

このpremainメソッドの第2引数にある、Instrumentationはjava.lang.instrumentパッケージで定義されている
インタフェースです。

このInstrumentationに宣言されているメソッドの中にaddTransformerというメソッドがあり、
引数にはClassFileTransformerという、今回の目的にそのものズバリな名前のインタフェースを渡すようになっています。

ClassFileTransformerインタフェースにはただ1つtransformというメソッドが宣言されていて、
今回はこのメソッドの中身をJavassistを使用しながら実装することになります。

では、実装したコード(EnumRewritingクラス)を見てみましょう。

package com.example;

import javassist.*;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;

public class EnumRewriting {
    public static void premain(String agentArgs, Instrumentation instrumentation) {
        instrumentation.addTransformer(
                (loader, className, classBeingRedefined, protectionDomain, classfileBuffer) -> {
            // クラスがcom.example.F1Constructorの場合だけバイトコード書き換えを実行
            if (className.equals("com/example/F1Constructor")) {
                // デフォルトのクラスパスを持つClassPoolオブジェクトを取得
                ClassPool classPool = ClassPool.getDefault();
                // クラスファイルがバイト配列として読み込まれたものをByteArrayInputStreamにする
                ByteArrayInputStream stream = new ByteArrayInputStream(classfileBuffer);
                try {
                    // クラスを作成、つまりcom.example.F1Constructorを作成
                    CtClass ctClass = classPool.makeClass(stream);
                    // staticイニシャライザのブロックを取得
                    CtConstructor ctConstructor = ctClass.getClassInitializer();
                    // マクラーレンの順序を9に設定
                    ctConstructor.insertAfter("MCLAREN = new com.example.F1Constructor(\"MCLAREN\", 3, \"04\", \"マクラーレン\", 9);");
                    // ザウバーの順序を10に設定
                    ctConstructor.insertAfter("SAUBER = new com.example.F1Constructor(\"SAUBER\", 7, \"08\", \"ザウバー\", 10);");
                    // $VALUESを上書き
                    ctConstructor.insertAfter("$VALUES = new com.example.F1Constructor[] { FERRARI, FORCE_INDIA, HAAS, MCLAREN, MERCEDES, RED_BULL, RENAULT, SAUBER, TORO_ROSSO, WILLIAMS };");
                    // 書き換えたcom.example.F1Constructorをバイトコードに変換して、Metaspaceに読み込ませる
                    return ctClass.toBytecode();
                } catch (IOException | CannotCompileException e) {
                    // 上記例外発生時にスタックトレースを出力する。これを書いておかないと例外発生時にコンソールに何も出力されない
                    e.printStackTrace();
                    IllegalClassFormatException illegalClassFormatException = new IllegalClassFormatException();
                    illegalClassFormatException.initCause(e);
                    // 例外はIllegalClassFormatExceptionをスローするよう決められているが、挙動はnullをreturnした場合と同じで、コンソールには何も出力されない
                    throw illegalClassFormatException;
                }
            }
            // バイトコードを書き換えない場合はnullをreturnする決まりになっている
            return null;
        });
    }
}

各行がそれぞれどのような処理をしているかは上記のコード内のコメントを見ていただくとして、
どうして、このような実装になるのかをcom.example.F1Constructorを逆コンパイルしたコードで解説します。

バイトコード書き換え前後の姿

まず、書き換え対象となるcom.example.F1Constructorのソースコードがこれです。

package com.example;

/**
 * F1コンストラクター
 */
public enum F1Constructor implements CodeEnum<F1Constructor> {
    FERRARI("01", "フェラーリ", 2),
    FORCE_INDIA("02", "フォース・インディア", 4),
    HAAS("03", "ハース", 7),
    MCLAREN("04", "マクラーレン", 10),
    MERCEDES("05", "メルセデス", 1),
    RED_BULL("06", "レッドブル", 3),
    RENAULT("07", "ルノー", 8),
    SAUBER("08", "ザウバー", 9),
    TORO_ROSSO("09", "トロ・ロッソ", 6),
    WILLIAMS("10", "ウィリアムズ", 5);

    private String code;

    private String name;

    private int order;

    F1Constructor(String code, String name, int order) {
        this.code = code;
        this.name = name;
        this.order = order;
    }

    @Override
    public String getCode() {
        return code;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public int getOrder() {
        return order;
    }
}

最初に説明したとおり、ザウバーのorderが9、マクラーレンのorderには10が設定されています。

上記ソースコードをコンパイルしたら作成されるクラスファイルを逆コンパイルしたのが以下のコードになります。
逆コンパイルにはjadコマンドを利用しました。

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name:   F1Constructor.java

package com.example;

// Referenced classes of package com.example:
//            CodeEnum

public final class F1Constructor extends Enum
    implements CodeEnum
{

    public static F1Constructor[] values()
    {
        return (F1Constructor[])$VALUES.clone();
    }

    public static F1Constructor valueOf(String name)
    {
        return (F1Constructor)Enum.valueOf(com/example/F1Constructor, name);
    }

    private F1Constructor(String s, int i, String code, String name, int order)
    {
        super(s, i);
        this.code = code;
        this.name = name;
        this.order = order;
    }

    public String getCode()
    {
        return code;
    }

    public String getName()
    {
        return name;
    }

    public int getOrder()
    {
        return order;
    }

    public static final F1Constructor FERRARI;
    public static final F1Constructor FORCE_INDIA;
    public static final F1Constructor HAAS;
    public static final F1Constructor MCLAREN;
    public static final F1Constructor MERCEDES;
    public static final F1Constructor RED_BULL;
    public static final F1Constructor RENAULT;
    public static final F1Constructor SAUBER;
    public static final F1Constructor TORO_ROSSO;
    public static final F1Constructor WILLIAMS;
    private String code;
    private String name;
    private int order;
    private static final F1Constructor $VALUES[];

    static
    {
        FERRARI = new F1Constructor("FERRARI", 0, "01", "\u30D5\u30A7\u30E9\u30FC\u30EA", 2);
        FORCE_INDIA = new F1Constructor("FORCE_INDIA", 1, "02", "\u30D5\u30A9\u30FC\u30B9\u30FB\u30A4\u30F3\u30C7\u30A3\u30A2", 4);
        HAAS = new F1Constructor("HAAS", 2, "03", "\u30CF\u30FC\u30B9", 7);
        MCLAREN = new F1Constructor("MCLAREN", 3, "04", "\u30DE\u30AF\u30E9\u30FC\u30EC\u30F3", 10);
        MERCEDES = new F1Constructor("MERCEDES", 4, "05", "\u30E1\u30EB\u30BB\u30C7\u30B9", 1);
        RED_BULL = new F1Constructor("RED_BULL", 5, "06", "\u30EC\u30C3\u30C9\u30D6\u30EB", 3);
        RENAULT = new F1Constructor("RENAULT", 6, "07", "\u30EB\u30CE\u30FC", 8);
        SAUBER = new F1Constructor("SAUBER", 7, "08", "\u30B6\u30A6\u30D0\u30FC", 9);
        TORO_ROSSO = new F1Constructor("TORO_ROSSO", 8, "09", "\u30C8\u30ED\u30FB\u30ED\u30C3\u30BD", 6);
        WILLIAMS = new F1Constructor("WILLIAMS", 9, "10", "\u30A6\u30A3\u30EA\u30A2\u30E0\u30BA", 5);
        $VALUES = (new F1Constructor[] {
            FERRARI, FORCE_INDIA, HAAS, MCLAREN, MERCEDES, RED_BULL, RENAULT, SAUBER, TORO_ROSSO, WILLIAMS
        });
    }
}

今回の書き換え処理でやっていることをこの逆コンパイルしたコードで表すと以下のようになります。

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name:   F1Constructor.java

package com.example;

// Referenced classes of package com.example:
//            CodeEnum

public final class F1Constructor extends Enum
    implements CodeEnum
{

    public static F1Constructor[] values()
    {
        return (F1Constructor[])$VALUES.clone();
    }

    public static F1Constructor valueOf(String name)
    {
        return (F1Constructor)Enum.valueOf(com/example/F1Constructor, name);
    }

    private F1Constructor(String s, int i, String code, String name, int order)
    {
        super(s, i);
        this.code = code;
        this.name = name;
        this.order = order;
    }

    public String getCode()
    {
        return code;
    }

    public String getName()
    {
        return name;
    }

    public int getOrder()
    {
        return order;
    }

    public static final F1Constructor FERRARI;
    public static final F1Constructor FORCE_INDIA;
    public static final F1Constructor HAAS;
    public static final F1Constructor MCLAREN;
    public static final F1Constructor MERCEDES;
    public static final F1Constructor RED_BULL;
    public static final F1Constructor RENAULT;
    public static final F1Constructor SAUBER;
    public static final F1Constructor TORO_ROSSO;
    public static final F1Constructor WILLIAMS;
    private String code;
    private String name;
    private int order;
    private static final F1Constructor $VALUES[];

    static
    {
        FERRARI = new F1Constructor("FERRARI", 0, "01", "\u30D5\u30A7\u30E9\u30FC\u30EA", 2);
        FORCE_INDIA = new F1Constructor("FORCE_INDIA", 1, "02", "\u30D5\u30A9\u30FC\u30B9\u30FB\u30A4\u30F3\u30C7\u30A3\u30A2", 4);
        HAAS = new F1Constructor("HAAS", 2, "03", "\u30CF\u30FC\u30B9", 7);
        MCLAREN = new F1Constructor("MCLAREN", 3, "04", "\u30DE\u30AF\u30E9\u30FC\u30EC\u30F3", 10);
        MERCEDES = new F1Constructor("MERCEDES", 4, "05", "\u30E1\u30EB\u30BB\u30C7\u30B9", 1);
        RED_BULL = new F1Constructor("RED_BULL", 5, "06", "\u30EC\u30C3\u30C9\u30D6\u30EB", 3);
        RENAULT = new F1Constructor("RENAULT", 6, "07", "\u30EB\u30CE\u30FC", 8);
        SAUBER = new F1Constructor("SAUBER", 7, "08", "\u30B6\u30A6\u30D0\u30FC", 9);
        TORO_ROSSO = new F1Constructor("TORO_ROSSO", 8, "09", "\u30C8\u30ED\u30FB\u30ED\u30C3\u30BD", 6);
        WILLIAMS = new F1Constructor("WILLIAMS", 9, "10", "\u30A6\u30A3\u30EA\u30A2\u30E0\u30BA", 5);
        $VALUES = (new F1Constructor[] {
            FERRARI, FORCE_INDIA, HAAS, MCLAREN, MERCEDES, RED_BULL, RENAULT, SAUBER, TORO_ROSSO, WILLIAMS
        });
        MCLAREN = new F1Constructor("MCLAREN", 3, "04", "\u30DE\u30AF\u30E9\u30FC\u30EC\u30F3", 9);
        SAUBER = new F1Constructor("SAUBER", 7, "08", "\u30B6\u30A6\u30D0\u30FC", 10);
        $VALUES = new F1Constructor[] { FERRARI, FORCE_INDIA, HAAS, MCLAREN, MERCEDES, RED_BULL, RENAULT, SAUBER, TORO_ROSSO, WILLIAMS };
    }
}

staticイニシャライザブロックの最後にctConstructor.insertAfterメソッドで追加した3行が入ることで、
定数を上書きされた結果挙動が変わることがご理解いただけると思います。

バイトコードを書き換えるとは言っても、Javassitを使うと人間に簡単に理解できることコードで
あっさり実現できてしまうことに拍子抜けした方もいるのではないでしょうか?

ビルドと実行

バイトコードを書き換えるプログラム(EnumRewritingクラス)はjarファイルとしてビルドする必要があります。
今回はGradleでビルドすることにしたため、build.gradleファイルを作成しました。

group 'javassist-sample'
version '1.0-SNAPSHOT'

apply plugin: 'java'

sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

dependencies {
    compile group: 'javassist', name: 'javassist', version: '3.12.1.GA'
}

jar {
    manifest {
        attributes 'Premain-Class': 'com.example.EnumRewriting', 'Boot-Class-Path': 'javassist-3.12.1.GA.jar', 'Can-Redefine-Classes': 'true'
    }
}

dependenciesには必要なライブラリとしてjavassistを指定しています。

バイトコード書き換えを行う場合、jarファイルの中に以下の3項目を書いたマニフェストファイルが必要で、
上記jar.manifestブロック内はそのための記述です。

  • Premain-Class:この項目のみ設定必須。バイトコード操作を行うクラスを指定するので、
    今回はcom.example.EnumRewritingになる。
  • Boot-Class-Path:バイトコード操作に必要なライブラリやクラスを記述する。
    今回はjavassistのjarファイル名になる。
  • Can-Redefine-Classes:バイトコードを操作してクラスの再定義を許すかどうかを指定するため、
    今回はtrueになる。

ビルドして出来上がるjarファイルは、javassist-sample-1.0-SNAPSHOT.jarになります。

mainメソッドを実装したクラスとして以下のMainクラスを作成しました。
F1Constructorの列挙子を表示順にソートした結果を出力するだけのシンプルな実装です。

package com.example;

import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<F1Constructor> orderedList = CodeEnum.getOrderedList(F1Constructor.class);
        orderedList.forEach(e -> System.out.println(e.getName()));
    }
}

上記Mainクラスをまずは単純に以下のコマンドで実行してみます。

java com.example.Main

結果は

メルセデス
フェラーリ
レッドブル
フォース・インディア
ウィリアムズ
トロ・ロッソ
ハース
ルノー
ザウバー
マクラーレン

になり、実装どおりマクラーレンよりザウバーが上に来ています。

今度はEnumを書き換えるため、javaコマンドを実行するディレクトリに先ほどビルドした
javassist-sample-1.0-SNAPSHOT.jarと、javassist-3.12.1.GA.jarも置いて、
以下のコマンドを実行します。

バイトコード書き換えはmainメソッドが動く前に動作させる必要があるため、作成したjarファイルは
-javaagentオプションで指定する必要があります。

java -javaagent:javassist-sample-1.0-SNAPSHOT.jar com.example.Main

結果は、

メルセデス
フェラーリ
レッドブル
フォース・インディア
ウィリアムズ
トロ・ロッソ
ハース
ルノー
マクラーレン
ザウバー

となり、無事にマクラーレンとザウバーの順序が入れ替わりました!

まとめ

果たしてこの記事が誰かの役に立つことはあるのか!?と書いている本人が思ってしまうくらい
マニアックな内容になりましたが、Javaってこんなこともできるんだ・・・!と楽しんでいただけたなら幸いです。

再度、念押ししておきますが、このようなバイトコード書き換えはやらずに済むならその方がいいです。
通常のプログラムよりかなりデバッグしづらく、思ったように動かない場合は相当難航します。
バイトコード書き換えは慎重に。


みなさん、こんにちは!
キャスレーコンサルティング、システムデザイン部の石井です。

はじめに

Javaで開発を行なうに当たり、様々なフレームワークを使用する機会があると思います。
私自身、今まで主にseasar2での開発経験のみのため、
他のフレームワークには、どのような特徴があるのかと疑問に思い調べ初めました。

そんな中で、特に興味を持った「Apache Wicket」(以降Wicketと表現します。)について、
実際に開発環境を作成しましたので、ご紹介します。 (続きを読む…)


こんにちは。 キャスレーコンサルティング SI(システム・インテグレーション)部の満石です。

今年の7月にJava SE 9のリリースが迫っていますが、ちょっとここでJava SE 8の新機能を思い出してみましょう。

Lambda式、Stream API、Date and Time API。

これらは、有名なので実際に使ってみたことがある人は多いと思います。

でも、インタフェースにデフォルト実装とstaticメソッドが書けるようになった、というのは忘れている人や
知っていても使い所が分からないという人は多いのではないでしょうか?

今回は、そんなインタフェースのデフォルト実装とstaticメソッドについて、コード定義Enum用インタフェースの作成を
通して実用的な例を紹介したいと思います。 (続きを読む…)


こんにちは。
キャスレーコンサルティング SI(システム・インテグレーション)部の北見です。

Java9については、来年リリースが予定されています。
Java8の新機能の中で目玉機能の一つであった、「java.util.stream」へ新しくメソッドが追加予定となっております。
今回は、新しく追加予定のメソッドについて、試してみたいと思います。 (続きを読む…)


MyBatisを使ってみよう その2

Posted on 10月 19, 2016

こんにちは。
キャスレーコンサルティングSI(システム・インテグレーション)部の西川です。

前回に引き続きMyBatisを使ってDBアクセスするプログラムをご紹介していきたいと思います。
前回の記事で構築した環境がある前提で進めてまいりますので構築手順などは前回の記事を参照くださいませ。
(前回の記事はコチラ

今回は以下のテーマでMyBatisを動かしてみたいと思います。
・Mapping機能を使用してDB検索結果をエンティティクラス(Java)の構造に沿って格納されるようにする
・自動生成されたMapperのInsert/Update/Deleteを使ってみる
・動的SQLを使って複数行のInsertができるようにする

それでは早速始めていきましょう。 (続きを読む…)


MyBatisを使ってみよう

Posted on 06月 15, 2016

こんにちは。
キャスレーコンサルティングSI(システム・インテグレーション)部の西川です。

すでに沢山の解説サイトがあるなかでいまさら・・・とか言われそうですが、
今回はJavaのO/RMapperフレームワークであるMyBatisを使ってDBにアクセスしてみたいと思います。

DB関連の学習ってテーブル用意して・・・データ用意して・・・といった若干面倒な準備が必要なので
既存のDBなどを使って簡単に動かす方法ないかなと思ったことはありませんか?

既にMyBatisの使い方を解説されているサイトはたくさんありますが、
今回は以下のテーマでMyBatisを動かしてみたいと思います。

  • MyBatisが動くEclipseプロジェクトを時間をかけずに作る
  • サンプルや既存のDB環境をそのまま流用する

MyBatisの真価はマッピング機能や動的SQL、SpringFrameworkなどといったDIコンテナとの連携で発揮される
とは思いますが、まずは入門編ということでご了承ください。 (続きを読む…)


jOOXでjQueryライクにXML操作

Posted on 04月 20, 2016

こんにちは、キャスレーコンサルティングのSD(システム・デザイン)部ヤマナです。

今回はjOOXというjavaのライブラリを紹介させて頂きます。
jQueryライクなAPIでXMLの読取り/書込みが可能です。

今回は以下の操作についてjOOXのサンプルソースをご紹介します。

  • ファイル読取
  • Webからの読取
  • ゼロから作成
  • 既存XMLの変更

ファイル読取

まずは読取りの例から。

(続きを読む…)


[JavaFX] FXMLの使い方

Posted on 11月 25, 2015

こんにちは。SI部の吉原です。

以前、Swingに取って代わりJAVA標準GUIとなったJavaFX 2.0について記事を書きました。
前回はSwingとJavaFXの違いを見るために、JavaFXプログラミングによる画面描画の記事でしたので
今回はJavaFXの一番の目玉とも言えるFXMLについて書こうと思います。

FXMLとは

まず始めにFXMLについて簡単に説明します。
FXMLはJavaFXに用意されているXMLベースのGUI記述言語です。
「言語」と言っても、XMLベースのマークアップ言語ですので、Javaコードより記述は非常に簡単で分かり易くなっています。

利用するコンテナやコントロールの内容をXMLベースで記述するだけでGUIがデザインでき、
FXMLで定義した情報をJava側にて読み込むコードを記述するだけで画面描画が可能となります。
要するにプログラム内でシーン(Scene)グラフを構築するのではなく、FXMLファイル内で構築できるのです。

FXMLを使用すれば、あらゆるユーザー・インタフェースを作成できますが、FXMLは、大規模で複雑なシーン・グラフ、フォーム、データ・エントリまたは複雑なアニメーションが備わったユーザー・インタフェースを作成する際に特に役立ちます。
また、FXMLにスクリプトを含めることで動的なレイアウト作成も可能となっているのです。 (続きを読む…)


前回、Java8のラムダ式について記事(【初心者向け】今更聞けない?Java8のラムダ式について知ろう!
を書きました金巻です。
今回も引き続きJava8の新機能についてご紹介したいと思います。

Java 8では新たに導入される機能で特に注目を浴びている「Stream API」というものがあります。
こちらについて今回はご紹介いたします。

Stream APIについて

Stream APIはJava8で導入された機能の中で特に注目されている機能で、配列やCollectionなどの集合体を扱う為のもので、値の集計やデータを使った処理などが出来る便利なAPIです。

Streamの種類としては幾つかのインタフェースが用意されています。
よく使われるものとしては以下のものがあります。

インタフェース 概要
Stream<t> 要素がTのインスタンスのStream
IntStream 要素がintの値のStream
LongStream 要素がlongの値のStream
DoubleStream 要素がdoubleの値のStream

プリミティブ型にはそれに応じたStreamインタフェースが用意されています。
JavaはStringをプリミティブ型ではないとしているのでStringStreamはありません。

昔はAとBの処理を同時に実行するには2台のPCにそれぞれの処理を振り分けて実行することで
時間の短縮をしていました。これは分散処理と言われています。
これは1つのPCに1つのCPUしかなかったからでした。 (続きを読む…)


こんにちは、キャスレーコンサルティングSI部の清水です。

プロジェクト内に一人ぐらいは物凄いコーディングが早い方がいるものですが、そのような方は決まってショートカットキーの達人だったりします。
今回はすでにご存じのものが多いかもしれませんが、私が独断でEclipseで使えると思うショートカットを10個挙げてみました。
ショートカットキーはおぼえるというより、身に浸み込ますようなものです。
日ごろから積極的に使用し、ショートカットキーマスターを目指しましょう。

補完機能を利用する [Ctrl + Space]

変数名やメソッド名、クラス名等を途中まで入力し「Ctrl + space」を押すと、それ以降のキーワードを検索しを補完してくれます。

(続きを読む…)



  • Profile
    キャスレーコンサルティングの技術ブログです。
    当社エンジニアが技術面でのTips、技術系イベント等についてご紹介いたします。
  • CSV社長ブログ
  • チーム・キャスレーブログ