Gradle を使って JAR ファイルへ AspectJ を適用

Gradle を使って既存の JAR ファイルへ AspectJ を適用してみました。

Gradle 用の AspectJ プラグインとして gradle-aspectj というものがあるようですが、今回は AspectJaspectjtools) に含まれている Ant 用の AjcTask (iajc) を Gradle から使う事にします。

AjcTask の利用方法

Gradle で AjcTask を使用するには Gradle ビルドスクリプト (build.gradle) で下記のように定義します。

ant.taskdef(resource: 'org/aspectj/tools/ant/taskdefs/aspectjTaskdefs.properties', classpath: <aspectjtools へのパス>)

ant.iajc() で AjcTask を利用できるようになります。

Struts の JAR ファイルへ AspectJ を適用

今回は下記のアスペクト定義を Struts 1.3.10 の JAR ファイル (struts-core-1.3.10.jar) へ適用してみる事にします。

org.apache.struts.mock パッケージを除いた org.apache.struts 以降のパッケージに属しているクラスの BeanUtils.populate() 呼び出し箇所でプロパティ名がパターンにマッチした際に IllegalArgumentException を発生させるような処理を織り込みます。

アスペクト定義 src/main/java/fits/sample/StrutsAspect.java
package fits.sample;

import java.util.Map;
import java.util.regex.Pattern;

import org.aspectj.lang.annotation.*;
import org.aspectj.lang.*;

@Aspect
public class StrutsAspect {
    private final static Pattern PATTERN = Pattern.compile("(^|\\W)[cC]lass\\W");

    @Around(
        "call(void org.apache..BeanUtils.populate(Object, Map)) &&" + 
        "within(org.apache.struts..*) && " +
        "!within(org.apache.struts.mock.*) && " +
        "args(bean, properties)"
    )
    public void aroundPopulate(ProceedingJoinPoint pjp, Object bean, Map properties) throws Throwable {
        if (properties != null) {
            checkProperties(properties);

            pjp.proceed();
        }
    }

    private void checkProperties(Map properties) {
        for (Object key : properties.keySet()) {
            String property = (String)key;

            if (PATTERN.matcher(property).find()) {
                throw new IllegalArgumentException(key + " is invalid");
            }
        }
    }
}

ビルドスクリプトの内容は下記のようになります。

aspectjtools のパスを取得するために configurations へ ajc を定義し、AspectJ を適用するタスクを ajc という名称で定義しました。

struts-core-1.3.10.jar へ AspectJ を適用するための依存ライブラリは、configurations.compile から取得して、ant.iajc の classpath へ設定するようにしています。

なお、commons-fileupload や servlet-api 等の依存ライブラリはアスペクト定義の内容によって代わると思います。

ビルドスクリプト build.gradle
apply plugin: 'java'

repositories {
    mavenCentral()
}

configurations {
    ajc
}

dependencies {
    ajc "org.aspectj:aspectjtools:1.8.0"
    compile "org.aspectj:aspectjrt:1.8.0"
    compile "org.apache.struts:struts-core:1.3.10"
    compile 'commons-fileupload:commons-fileupload:1.3.1'
    compile 'javax.servlet:servlet-api:2.5'
}

task ajc << {
    ant.taskdef(resource: 'org/aspectj/tools/ant/taskdefs/aspectjTaskdefs.properties', classpath: configurations.ajc.asPath)

    ant.iajc(outJar: "struts-core_custom.jar", source: '1.7', showWeaveInfo: 'true') {
        sourceroots {
            sourceSets.main.java.srcDirs.each {
                pathelement(location: it.absolutePath)
            }
        }
        classpath {
            // 依存ライブラリの設定
            pathelement(location: configurations.compile.asPath)
        }
        inpath {
            // struts-core へのパスを設定
            pathelement(location: configurations.compile.files.find { it.path.contains 'struts-core' }.absolutePath)
        }
    }
}

実行結果は下記のようになり、 ActionServlet クラスと RequestUtils クラスへ StrutsAspect が適用された JAR ファイル (struts-core_custom.jar) がカレントディレクトリへ作成されます。

実行結果
> gradle ajc --info
・・・
:ajc (Thread[main,5,main]) started.
:ajc
Executing task ':ajc' (up-to-date check took 0.0 secs) due to:
  Task has not declared any outputs.
[ant:iajc] weaveinfo Join point 'method-call(void org.apache.commons.beanutils.BeanUtils.populate(java.lang.Object, java.util.Map))' in Type 'org.apache.struts.action.ActionServlet' (ActionServlet.java:845) advised by around advice from 'fits.sample.StrutsAspect' (StrutsAspect.java:19)
[ant:iajc] weaveinfo Join point 'method-call(void org.apache.commons.beanutils.BeanUtils.populate(java.lang.Object, java.util.Map))' in Type 'org.apache.struts.util.RequestUtils' (RequestUtils.java:473) advised by around advice from 'fits.sample.StrutsAspect' (StrutsAspect.java:19)
:ajc (Thread[main,5,main]) completed. Took 5.211 secs.

BUILD SUCCESSFUL

今回使用したサンプルのソースは http://github.com/fits/try_samples/tree/master/blog/20140518/

Gradle で任意の Maven 設定ファイルのローカルリポジトリ設定を適用する

Gradle のビルドスクリプトにて mavenLocal() を使用した際、${user.home}/.m2/settings.xml ファイルに localRepository 設定があれば、これを反映してくれますが、現時点では任意の Maven 設定ファイル (settings.xml) の localRepository を反映する方法は特に用意されていないようです。

  • Gradle 1.12

任意のローカルリポジトリを使うには mavenLocal() を使わず maven { url <リポジトリのパス> } とすればよいだけですので、下記の方法で任意の Maven 設定ファイルの localRepository を反映できます。

  • Maven 設定ファイルをコマンドライン引数 (-P) で指定できるようにする (下記サンプルでは maven.settings としています)
  • Maven 設定ファイルから取り出した localRepository の値を maven { url <ローカルリポジトリのパス> } で設定する

とりあえず実装してみると下記のようになります。 (Maven 設定ファイルに localRepository 設定の無いケースは考慮していない点に注意)

ビルドスクリプト build.gradle
apply plugin: 'java'

repositories {
    // プロジェクトプロパティ maven.settings の有無で処理を振り分け
    if (project.hasProperty('maven.settings')) {
        // Maven 設定ファイルをパース
        def xml = new XmlSlurper().parse(new File(project['maven.settings']))

        maven {
            url xml.localRepository.text()
        }
    }
    else {
        mavenLocal()
    }
}

dependencies {
    ・・・
}

-P<プロジェクトプロパティ名>=<値> オプションで Maven 設定ファイルを指定して実行します。

実行例
> gradle build -Pmaven.settings=settings.xml

Groovy のトレイトと @Immutable

Groovy 2.3 からトレイト機能が追加されています。

ここで、トレイトと @Immutable アノテーションを共に使用した場合、現バージョン (2.3) では以下のような注意点がありました。

  • トレイトで定義したプロパティの値は変更可能 (immutable とはならない)
  • マップベースコンストラクタで値を設定するには "<トレイト名>__<プロパティ名>" と指定する必要あり

検証に使用したサンプルスクリプトは下記です。

ソースは http://github.com/fits/try_samples/tree/master/blog/20140504/

sample.groovy
import groovy.transform.*

interface Pricing {
    BigDecimal total()
}
// トレイトの定義1
trait BasicPricing implements Pricing {
    BigDecimal price = 0
    BigDecimal total() { price }
}
// トレイトの定義2
trait QuantityPricing extends BasicPricing {
    int qty
    BigDecimal total() { price * qty }
}
// 実装クラス1
@ToString
class Sample1 implements QuantityPricing {
    String name
}
// 実装クラス2
@Immutable
class Sample2 implements QuantityPricing {
    String name
}

println '----- 1 -----'
// (a) トレイトのプロパティ名を指定して値を設定できる
def s1 = new Sample1(name: 'S1', price: 100, qty: 5)
println "${s1}, total: ${s1.total()}, price: ${s1.price}"
println s1.dump()

println ''
println '----- 2a -----'
// (b) @Immutable の場合はトレイトのプロパティ名を指定しても値を設定できない (初期値のまま)
def s2a = new Sample2(name: 'S2a', price: 200, qty: 6)
println "${s2a}, total: ${s2a.total()}, price: ${s2a.price}"
println s2a.dump()
// (c) トレイトで定義したプロパティには @Immutable なクラスでも値を書き込める
s2a.price = 300
s2a.qty = 3
println "${s2a}, total: ${s2a.total()}, price: ${s2a.price}"
println s2a.dump()

println ''
println '----- 2b -----'
// (d) @Immutable なクラスの場合は "<トレイト名>__<プロパティ名>" で値を設定する必要あり
def s2b = new Sample2(name: 'S2b', BasicPricing__price: 200, QuantityPricing__qty: 6)
println "${s2b}, total: ${s2b.total()}, price: ${s2b.price}"
println s2b.dump()

上記 (c) のように、トレイトで定義したプロパティ (price や qty) は実装クラスへ @Immutable アノテーションを付与していても値を変更する事が可能です。

マップベースコンストラクタを使用する場合、通常は (a) のようにトレイトのプロパティ名 (price や qty) を指定して値を設定できますが、@Immutable なクラスの場合は (d) のように <トレイト名>__<プロパティ名> とする必要があるようです。

(b) のようにコンストラクタで通常のプロパティ名を指定しても値を設定できませんでした。

実行結果は以下の通りです。

実行結果
> groovy sample.groovy

----- 1 -----
Sample1(S1), total: 500, price: 100
<Sample1@3d285d7e name=S1 QuantityPricing__qty=5 BasicPricing__price=100>

----- 2a -----
Sample2(S2a), total: 0, price: 0
<Sample2@14d63 QuantityPricing__qty=0 BasicPricing__price=0 name=S2a $hash$code=85347>
Sample2(S2a), total: 900, price: 300
<Sample2@14d63 QuantityPricing__qty=3 BasicPricing__price=300 name=S2a $hash$code=85347>

----- 2b -----
Sample2(S2b), total: 1200, price: 200
<Sample2@14d64 QuantityPricing__qty=6 BasicPricing__price=200 name=S2b $hash$code=85348>

dump() の結果より、トレイトで定義したプロパティは実装クラス内で <トレイト名>__<プロパティ名> フィールドとして定義されている事が分かります。

Java で ISO-8601 日付文字列をパース

Java2014-04-27T13:10:02+09:00 のような ISO-8601 形式の日付文字列をパースし java.util.Date を取得する方法として、主に下記が考えられます。

  • (1) 日付フォーマットのタイムゾーンに 'X' を使用 (Java SE 7 以降)
  • (2) Apache Commons Lang の DateFormatUtils を使用

ソースは http://github.com/fits/try_samples/tree/master/blog/20140427/

(1) 日付フォーマットのタイムゾーンに 'X' を使用 (Java SE 7 以降)

ISO-8601 のタイムゾーン部分のパターンに X を使えば SimpleDateFormat でパースできます。

ただし、X を使えるのは Java SE 7 以降です。

Sample1.java
import java.text.SimpleDateFormat;
import java.util.Date;

public class Sample1 {
    public static void main(String... args) throws Exception {
        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX");
        Date date = df.parse(args[0]);
        System.out.println(date);
    }
}
実行結果
> java Sample1 2014-04-27T13:10:02+09:00

Sun Apr 27 13:10:02 JST 2014

なお、タイムゾーン+09:00 の他にも +0900+09 でもパース可能です。

(2) Apache Commons Lang の DateFormatUtils を使用

Commons Lang の DateFormatUtils でパースできます。 Commons Lang 3 なら DateFormatUtils.ISO_DATETIME_TIME_ZONE_FORMAT を使用すれば簡単です。

Sample2.java
import java.util.Date;
import org.apache.commons.lang3.time.DateFormatUtils;

public class Sample2 {
    public static void main(String... args) throws Exception {
        Date date = DateFormatUtils.ISO_DATETIME_TIME_ZONE_FORMAT.parse(args[0]);
        System.out.println(date);
    }
}
実行結果
> java -cp .;commons-lang3-3.3.2.jar Sample2 2014-04-27T13:10:02+09:00

Sun Apr 27 13:10:02 JST 2014

+0900タイムゾーンもパース可能ですが、+09 では ParseException が発生しました。

まとめ

3種類のタイムゾーン表記 (+09:00, +0900, +09) のパース可否をまとめると下記のようになりました。

方法 +09:00 のパース +0900 のパース +09 のパース
(1) 'X' の使用
(2) Commons Lang 3.3.2 ×

Java 8 ラムダ式の実装メソッド名を取得 - SerializedLambda

Java 8 ラムダ式の実装メソッド名を実行時に取得する方法を探ってみました。

ソースは http://github.com/fits/try_samples/tree/master/blog/20140413/

はじめに

前回ラムダ式デコンパイルしてみましたが、ラムダ式の処理は lambda$main$0 のような synthetic メソッドに実装されます。

そして、ラムダ式の部分 (下記 f 変数の値) は実行時に java.lang.invoke.LambdaMetafactory.metafactory() で動的にクラス・オブジェクトが生成されます。

Sample0.java
import java.util.function.Predicate;

class Sample0 {
    public static void main(String... args) {
        int a = 3;
        Predicate<Integer> f = (x) -> x % a == 0;
        System.out.println(f.test(6));
    }
}
Sample0 の javap 結果 (lambda$main$0 がラムダ式の実装)
> javap -p Sample0
Compiled from "Sample0.java"
class Sample0 {
  Sample0();
  public static void main(java.lang.String...);
  private static boolean lambda$main$0(int, java.lang.Integer);
}

そこで、実行時に生成されたラムダ式のオブジェクトから実装メソッド名などの情報を取得する事ができるのか調査してみました。

結論としては、通常のラムダ式では無理そうでしたが、シリアライズ可にする事で取得できると分かりました。

  • ラムダ式を Serializable にすると writeReplace メソッドからラムダ式の情報が入った java.lang.invoke.SerializedLambda を取得可能

ただし、writeReplace は private final なメソッドです。(シリアライズのための処理なので)

ちなみに、ラムダ式部分の動的なクラス生成などは java.lang.invoke.InnerClassLambdaMetafactory 内で ASM を使って実施されているようです。

シリアライズ可能なラムダ式から SerializedLambda を取得

それでは、シリアライズ可能なラムダ用のインターフェース (下記の SPredicate) を定義し、writeReplace メソッドを実行して SerializedLambda を取得してみます。

Sample1.java
import java.lang.invoke.SerializedLambda;
import java.lang.reflect.Method;
import java.util.function.Predicate;
import java.io.Serializable;

class Sample1 {
    public static void main(String... args) throws Exception {
        int a = 3;

        SPredicate<Integer> f = (x) -> x % a == 0;

        Method m = f.getClass().getDeclaredMethod("writeReplace");
        m.setAccessible(true);

        // リフレクションで writeReplace を実行し SerializedLambda を取得
        SerializedLambda sl = (SerializedLambda)m.invoke(f);

        System.out.println(sl);

        System.out.println("-----");

        // 実装先クラス名の出力
        System.out.println(sl.getImplClass());
        // 実装先メソッド名の出力
        System.out.println(sl.getImplMethodName());

        // 設定されている引数の出力
        for (int i = 0; i < sl.getCapturedArgCount(); i++) {
            System.out.println("arg: " + sl.getCapturedArg(i));
        }
    }

    // シリアライズ可能にした Predicate の定義
    public interface SPredicate<T> extends Predicate<T>, Serializable {
    }
}

実行してみると、実装メソッド名や設定されている引数(上記 a 変数に該当する 3 の値)を取得できました。

実行結果
> java Sample1
SerializedLambda[capturingClass=class Sample1, 
functionalInterfaceMethod=Sample1$SPredicate.test:(Ljava/lang/Object;)Z, 
implementation=invokeStatic Sample1.lambda$main$50fc8a8$1:(ILjava/lang/Integer;)Z, 
instantiatedMethodType=(Ljava/lang/Integer;)Z, numCaptured=1]
-----
Sample1
lambda$main$50fc8a8$1
arg: 3

CFR でラムダ式の実装メソッドをデコンパイル

SerializedLambda と前回の CFR を使えば、実行時にラムダ式の実装メソッドをデコンパイルしたりする事も可能です。

Sample2.java
import java.lang.invoke.SerializedLambda;
import java.util.function.Predicate;
import java.io.Serializable;

import org.benf.cfr.reader.util.getopt.GetOptParser;
import org.benf.cfr.reader.util.getopt.Options;
import org.benf.cfr.reader.util.getopt.OptionsImpl;
import org.benf.cfr.reader.entities.ClassFile;
import org.benf.cfr.reader.entities.Method;
import org.benf.cfr.reader.state.DCCommonState;
import org.benf.cfr.reader.util.output.ToStringDumper;

class Sample2 {
    public static void main(String... args) throws Exception {
        int a = 3;

        SPredicate<Integer> f = (x) -> x % a == 0;

        java.lang.reflect.Method m = f.getClass().getDeclaredMethod("writeReplace");
        m.setAccessible(true);

        SerializedLambda sl = (SerializedLambda)m.invoke(f);

        String src = decompileLambda(sl);

        System.out.println(src);
    }

    // ラムダ式の実装メソッドをデコンパイル
    private static String decompileLambda(SerializedLambda sl) throws Exception {
        ToStringDumper d = new ToStringDumper();

        Options options = new GetOptParser().parse(new String[] {sl.getImplClass()}, OptionsImpl.getFactory());
        DCCommonState dcCommonState = new DCCommonState(options);

        ClassFile c = dcCommonState.getClassFileMaybePath(options.getFileName());
        c = dcCommonState.getClassFile(c.getClassType());

        for (Method m : c.getMethodByName(sl.getImplMethodName())) {
            m.dump(d, true);
        }

        return d.toString();
    }

    public interface SPredicate<T> extends Predicate<T>, Serializable {
    }
}

実行結果は下記の通り、ラムダ式の実装メソッド(lambda$main$50fc8a8$1)のソースが出力されます。

実行結果
> java -cp .;cfr_0_78.jar Sample2
private static /* synthetic */ boolean lambda$main$50fc8a8$1(int n, java.lang.Integer n2) {
    return n2 % n == 0;
}

上記を工夫すれば groovy.sql.DataSet のような O-R マッピング処理(クロージャSQL の where 部分を定義)をラムダ式で実現できると思います。

CFR で Java 8 のラムダ式をデコンパイルする

Java 8 のラムダ式にも対応した CFR という Java のデコンパイラをご紹介します。

使い方

使い方は簡単で、http://www.benf.org/other/cfr/ から JAR ファイルをダウンロードして下記のように実行するだけです。

java -jar cfr_0_78.jar <Java クラスファイル> [メソッド名] [オプション]
java -jar cfr_0_78.jar <JAR ファイル> [オプション]

ラムダ式デコンパイル

それでは、下記ソースをコンパイルして出来た LambdaSample.class を CFR でデコンパイルしてみます。

LambdaSample.java
import java.util.function.Predicate;

class LambdaSample {
    public static void main(String... args) {
        Predicate<Integer> f = (x) -> x > 10;

        System.out.println("5 : " + f.test(5));
        System.out.println("15 : " + f.test(15));
    }
}

まずは、オプションを全く指定せずにデコンパイルしてみます。 ラムダ式の部分も見事にデコンパイルされたソースコードが標準出力に出力されます。

デコンパイル結果(オプション指定なし)
> java -jar cfr_0_78.jar LambdaSample.class

/*
 * Decompiled with CFR 0_78.
 */
import java.io.PrintStream;
import java.util.function.Predicate;

class LambdaSample {
    LambdaSample() {
    }

    public static /* varargs */ void main(String ... arrstring) {
        Predicate<Integer> predicate = n -> n > 10;
        System.out.println("5 : " + predicate.test(5));
        System.out.println("15 : " + predicate.test(15));
    }
}

次に、--decodelambdas false オプションを指定してラムダ式の部分をデコンパイルしないようにしてみます。

こうする事で、ラムダ式へ展開されず synthetic のメソッド定義 (ラムダ式の実体)と LambdaMetafactory.metafactory() 呼び出し処理のソースが出力されます。

デコンパイル結果(--decodelambdas false)
> java -jar cfr_0_78.jar LambdaSample.class --decodelambdas false

/*
 * Decompiled with CFR 0_78.
 */
import java.io.PrintStream;
import java.util.function.Predicate;

class LambdaSample {
    LambdaSample() {
    }

    public static /* varargs */ void main(String ... arrstring) {
        Predicate<Integer> predicate = (Predicate<Integer>)LambdaMetafactory.metafactory(null, null, null, (Ljava/lang/Object;)Z, lambda$main$0(java.lang.Integer ), (Ljava/lang/Integer;)Z)();
        System.out.println("5 : " + predicate.test(5));
        System.out.println("15 : " + predicate.test(15));
    }

    private static /* synthetic */ boolean lambda$main$0(Integer n) {
        return n > 10;
    }
}

Javaの列挙型(Enum)へ新しい要素を追加2 - Javassist

前回Java の列挙型(Enum)へ新しい要素(識別子)を追加するためリフレクションを駆使しましたが、今回は Javassist を使ってもっと容易に実現する方法をご紹介します。

ソースは http://github.com/fits/try_samples/tree/master/blog/20140316/

はじめに

前回と同様の列挙型へ Second 要素を追加してみる事にします。

EType.java
enum EType {
    First
}

Javassist を使った列挙型への要素追加

Javassist を使う場合、下記を実施して $VALUES フィールドへ列挙型の要素を好きなように設定するだけです。

  • CtConstructorinsertAfter メソッドを使って、静的初期化子へ $VALUES を書き換える処理を挿入

なお、静的初期化子のための CtConstructor オブジェクトは CtClass オブジェクトの getClassInitializerメソッドで取得します。

EnumAddValueJavassist.java
import javassist.*;

public class EnumAddValueJavassist {
    public static void main(String... args) throws Exception {
        ClassPool pool = ClassPool.getDefault();

        CtClass cc = pool.get("EType");

        // 静的初期化子(static イニシャライザ)へ $VALUES を変更する処理を追加
        cc.getClassInitializer().insertAfter("$VALUES = new EType[] { First, new EType(\"Second\", 1) };");

        cc.toClass();

        System.out.println(EType.valueOf("Second"));

        System.out.println("-----");

        for (EType type : EType.values()) {
            System.out.println(type);
        }
    }
}

実行すると下記のように Second の追加を確認できます。

ビルドと実行
> javac -cp .;javassist-3.18.1-GA.jar *.java

> java -cp .;javassist-3.18.1-GA.jar EnumAddValueJavassist
Second
-----
First
Second

実際は、下記のように Second クラスフィールドを追加しておいた方が望ましいかもしれません。

EnumAddValueJavassist2.java
import javassist.*;

public class EnumAddValueJavassist2 {
    public static void main(String... args) throws Exception {
        ClassPool pool = ClassPool.getDefault();

        CtClass cc = pool.get("EType");

        // Second フィールドの追加
        CtField second = CtField.make("public static final EType Second = new EType(\"Second\", 1);", cc);
        cc.addField(second);

        cc.getClassInitializer().insertAfter("$VALUES = new EType[] { First, Second };");

        cc.toClass();
        ・・・
    }
}