CDI1.2によるbean-discovery-modeの見直し

2014年4月にCDI仕様のマイナーアップデート CDI1.1 -> CDI1.2 *1が行われ、@java.inject.Singletonが付与されたクラスはデフォルトではインジェクションできるBean対象から外れました。

後方互換性のない変更であるため、以下にまとめます。

default bean-discovery-modeの見直し

CDI1.1 (Java EE7) から導入されたbean-discovery-modeについての詳細は過去の記事にまとめていますが、簡単にいうと、以下の場合にどのクラスをインジェクション対象としてスキャンするかの仕様が見直されました。

  • warやearにbeans.xmlを含めなかった場合
  • またはbeans.xmlbean-discovery-mode="annotated"を明示していた場合

@java.inject.Singletonがスキャン対象から除外された

冒頭に書いたとおり、@javax.inject.Singletonはデフォルトのスキャン対象から除外されたため、以下のようなコードはCDI1.2よりデプロイ時にエラーになります。

@Path("/echo")
@RequestScoped
public class EchoResource {
  @Inject
  EchoService service;
    
  @GET
  public String echo(@QueryParam("name") String name) {
    return service.echo(name);
  }
}

@java.inject.Singleton
public class EchoService {
  public String echo(String name) {
    return "Hello " + name;
  } 
}

CDI1.2に準拠しているWildFly8.2.0で試すと、デプロイ時に以下のような例外メッセージが出力されます。@Singletonがスキャン対象から外されたため、依存性の解決例外になっています。

2015-01-11 16:12:54,944 ERROR [org.jboss.msc.service.fail] (MSC service thread 1-1) MSC000001: Failed to start service jboss.deployment.unit."CDITest.war".WeldStartService: org.j
boss.msc.service.StartException in service jboss.deployment.unit."CDITest.war".WeldStartService: Failed to start service
        ...
Caused by: org.jboss.weld.exceptions.DeploymentException: WELD-001408: Unsatisfied dependencies for type EchoService with qualifiers @Default
シングルトンをインジェクションしたい場合の対処

思い当たる範囲で、4つの対処があります。

対処案1. インジェクション先を@Singletonにする

インジェクション先のクラスのスコープを@Singletonにすることで、依存先のServiceクラスに『インジェクション先にスコープを合わせる』ことを示す@Dependentを付与すると、Serviceはシングルトンになります。

この案が使えるのは、インジェクション先(例ではJAX-RSエンドポイント)が状態を持たない場合のみです。以下のように、フィールドに状態を持っていた場合、@Singletonでは最初のリクエスト時にしかパラメータが設定されません。

@Path("/echo")
@javax.inject.Singleton
public class EchoResource {
  @Inject
  EchoService service;

  // ↓ 最初の1回目のリクエストしかパラメータが反映されない
  @QueryParam("name")
  String name;
    
  @GET
  public String echo() {
    return service.echo(name);
  }
}

@Dependent
public class EchoService {
  public String echo(String name) {
    return "Hello " + name;
  } 
}

JAX-RSCDIを組み合わせる場合はこの案でもなんとかなりますが、フィールドにフォーム入力値などの状態を持つ事が多いJSFと組み合わせる場合は対処案1は使えません。

追記: 書いてはみましたが、Serviceをシングルトンにしたいだけなのに、JAX-RSエンドポイントを変更するこの案はあんまりお勧めできません。

対処案2. @Stereotypeを使う
CDI1.2より@Steraotypeによって自作したアノテーションが付与されていた場合、デフォルトのスキャン対象となっているため、この仕組みを利用します。

ステレオタイプとは、スコープや@Transactionalなど、Beanの機能を示すアノテーションをまとめる仕組みです。JSF2ユーザは、一度は@Modelを使ったことがあると思いますが、@ModelのJavadocを見ると、@Stereotypeによって、@RequestScopedと@Namedを組み合わせたアノテーションであるとわかります。

@Singleton単体ではスキャン対象となりませんが、@Stereotypeが付与されたアノテーションが付与されたクラスはスキャン対象となるため、以下のように@Singletonをラップしたアノテーションを作ります。

@Singleton
@Stereotype
@Target(TYPE)
@Retention(RUNTIME)
public @interface Service {
}

作成したアノテーションを@Singletonの代わりに付与します。Spring Framework風に@Serviceにしてみました。

@Service
public class EchoService {
  public String echo(String name) {
    return "Hello " + name;
  } 
}

対処案1と比べて一手間掛かりますが、デメリットは特にありません。

対処案3. @ApplicationScopedを使う

リクエスト毎にインスタンス生成させない方法として、アプリケーションスコープを使う案もあります。今まではアプリケーションで共有のコンフィグ情報やキャッシュなどを持たせる印象が強いアプリケーションスコープですが、1つのインスタンスを共有する観点ではシングルトンと同じように使えます。

@ApplicationScoped
public class EchoService {
  public String echo(String name) {
    return "Hello " + name;
  } 
}

対処案4. ServiceはEJBで作る

スコープに困ったら、Serviceクラスのようなトランザクション境界となるクラスはJavaEE6時代と同様にEJBで作るのもありだと思います。

@Stateless
public class EchoService {
  public String echo(String name) {
    return "Hello " + name;
  } 
}

個人的には@ApplicationScopedはアプリケーション全体の状態を持たせる場合に使いたいため、案2の@Stereotypeを使う案が好きです。シンプルに使う場合は案3の@ApplicationScopedがおすすめです。

何故この変更が盛り込まれたのか

2013年のGlassFish Advant Calanderで書いた記事に詳細をまとめていますが、GuavaなどのCDI1.1が存在する前からあったライブラリが、CDI1.1に準拠したAPサーバ上で動かなくなったことが@Singletonをスキャン対象から外した要因です。

スキャン対象の拡大

@Singletonがスキャン対象から外れた修正以外として、CDI1.2では新たなスキャン対象の追加も行われています。

ステレオタイプについては前述の通りです。特に@Interceptorが地味に有効で、インターセプタ実装クラスにスコープアノテーションを付ける必要がなくなりました。

@InterceptorBinding
@Target({TYPE,METHOD})
@Retention(RUNTIME)
public @interface Logging {   
}

// @Dependent <<== CDI1.2からはスコープがなくてもスキャンされる
@Interceptor
@Logging
@Priority(APPLICATION + 10)
public class LoggingInterceptor {
    @AroundInvoke
    public Object log(InvocationContext ic) throws Exception {
        System.out.println("logging ...");
        return ic.proceed();
    }
}

@Service
public class EchoService {
  @Logging
  public String echo(String name) {
    return "Hello " + name;
  } 
}

インターセプタのスコープは?と言われても違和感があったので、嬉しい修正です。

CDI1.2 APサーバの準拠状況

OSS製品では、JavaEE7準拠の代表的なサーバは既にCDI1.2に対応しています。

  • GlassFish4.1 (2014/9/9 リリース)
  • WildFly8.2.0 Final (2014/11/20 リリース)

WebLogicやJBossEAP等の商用サーバは、まだJavaEE7準拠版がリリースされていないため何とも言えませんが、恐らくリリース時にはCDI1.2に対応させてくると思います。

まとめ

  • CDI1.2 マイナーバージョンアップが2014/4に行われた
    • beans.xmlがない場合のスキャン対象Beanが変更
      • @javax.inject.Singletonはスキャン対象から除外された
      • @Interceptor/@Decorator/@Stereotypeがスキャン対象に追加された
  • この修正はJavaEE7準拠サーバにも反映されている
    • GlassFish4.1、WildFly8.2.0はCDI1.2に対応済み

-XX:+DisableExplicitGCに関するJDK7とJDK8の違い

この記事は JVM Advent Calendar 2014の12/5分の記事*1です。昨日は
@jyukutyoさんのJITWatchでJITコンパイルを見よう!でした。


HotSpotには-XX:+DisableExplicitGCというオプションがありますが、この挙動がJDK8のリリースから変わっていたのでまとめます。

-XX:+DisableExplicitGC とは

System.gc()GCリクエストされても無視するオプションです。アプリケーション中でSystem.gc()を実行されるとメジャーGCが発生します。HotSpotに任せた場合はマイナーGCで十分回収できたかもしれないので、アプリケーションでSystem.gc()を呼び出すのはあまり好ましいことではありません。

無視するといっても内部的にそんな難しいことは行われておらず、OpenJDK8のソースを見るとif文でオプションが有効だったらGCしないだけです。

jdk8u/hotspot/src/share/vm/prims/jvm.cpp

451 JVM_ENTRY_NO_ENV(void, JVM_GC(void))
452   JVMWrapper("JVM_GC");
453   if (!DisableExplicitGC) {
454     Universe::heap()->collect(GCCause::_java_lang_system_gc);
455   }
456 JVM_END

JDK8よりjcmd <pid> GC.runに対しても有効になる

JDK7までは -XX:+DisableExplicitGC が有効化されていても、jcmd GC.runでGCリクエストが受け付けられていたのですが、JDK8より以下のメッセージ『Explicit GC is disabled, no GC has been performed.』が返ってきてGCは動きません。

$ jcmd 18952 GC.run
18952:
Explicit GC is disabled, no GC has been performed.

ソースを見ると、JDK8よりDisableExplicitGCフラグのチェックがjcmd <pid> GC.runの実装に追加されています。

jdk8u/hotspot/src/share/vm/services/diagnosticCommand.cpp

258 void SystemGCDCmd::execute(DCmdSource source, TRAPS) {
259   if (!DisableExplicitGC) {
260     Universe::heap()->collect(GCCause::_java_lang_system_gc);
261   } else {
262     output()->print_cr("Explicit GC is disabled, no GC has been performed.");
263   }
264 }
この修正による影響を受けるシステム

例えば、以下のようなシステムで影響を受けると思います。

  • 意図的にcronなどで定期的にjcmd <pid> GC.runを実行しているシステム
  • 連続運転テストをしてもメジャーGCが発生しないので、テスト終了時にjcmd <pid> GC.runを実行してJavaヒープにリーク傾向がないか考察しているシステム
回避策

JDK8において-XX:+DisableExplicitGC有効時にコマンドラインGCリクエストしたい場合は、jcmd <pid> GC.class_histogramによるヒストグラム取得の副作用を利用してGCを発生させることができます。

この場合、GCログのGC Causeに以下のように表示されます。

[Full GC (Heap Inspection Initiated GC)  39999K->37578K(250368K), 0.5085624 secs]

まとめ

  • JDK8より起動オプション-XX:+DisableExplicitGC有効時にはjcmd <pid> GC.runは無視されます
  • jcmd <pid> GC.class_histogramによってメジャーGCのリクエストをすることは可能です

*1:ええ、申し込んだのは2015/1/7ですがせっかく空いてたので

書籍「詳解Tomcat」を読んで

本書のレビューアの方から頂いたので読んでみました。以下、感想です。

詳解 Tomcat

詳解 Tomcat

最近、Twitterのタイムラインで『はじめてのXXXや、XXX 入門じゃなくて、もっと至高のXXXとか終焉のXXXみたいな書籍が欲しい』のような意見を目にした気がしますが、本書はまさに詳解Tomcatでした。設定方法を羅列するのではなく、Tomcatはどういくアーキテクチャで成り立っていて、各機能の詳細な挙動はどのような仕様なのかがまとめられています。

1章 Tomcatとは 〜 2章 Tomcatの基本 までは、背景やインストール方法などの色々な書籍で見かける導入部分の内容です。3章 アーキテクチャからが本番です。

Tomcatアーキテクチャの文書化が嬉しい

普段Tomcatを使っている人であれば、catalina/coyoteといったキーワードはスタックトレースやログで一度は目にした方も多いと思います。本書では、Servletコンテナ実装であるCatalinaアーキテクチャ(3章)およびHTTPコネクタ実装であるCoyote(5章)の全体像が文書化されています。

server.xmlを書いていると、<Service>、<Connector>、<Engine>などのCatalinaを構成するコンポーネント名が出てきますが、お恥ずかしながら今まで私はコンフィグ例を参照しながらなんとなく設定していたので、各コンポーネントの役割が理解できていませんでした。

トラブル対応時、いざTomcatのソースを読む時にアーキテクチャを理解していることはすごく重要だと思います。自身を振り返ると『Tomcat7以前ではなぜリクエスト処理後もスレッドがプールに返らないのか?』、『maxThreadsを超えるリクエストが来た時の仕様はどうなってるの?』などの問い合わせにもっと早く対応できたなと。

Tomcat固有機能の解説が嬉しい

5章 応用機能 以降ではTomcat固有の様々な便利機能の使い方、各機能がどういう仕様なのかが詳解されています。以下のような機能の詳細な解説は他の書籍ではあまり見かけないと思います。

マイグレーションガイドが嬉しい

バージョン間の差分が少ないTomcatではありますが、どんなAPサーバでもバージョンアップ対応は思わぬところでハマることがあるので悩ましい課題です。

9章のマイグレーションガイドには、 Tomcat6.0から8.0までのマイグレーションについて注意したい点、デフォルトパラメータの変更/ 修正が必要なコンフィグなどがまとめられています。

まとめ

詳解Tomcat、以下のような方におすすめです。


余談
中身の仕組みを解説している本は貴重なので、詳解シリーズがもっと増えて『詳解HotSpot』 や『詳解GlassFish』とかが出ると嬉しいなぁと。

Apache ShiroでBCryptを使う

先日のJava EE Advant Calander 2014にApache Shiroを使ってみましたを書いたところ、Shiroのデフォルトのハッシュポリシーソルト付きSHA-256, 500000 イテレーションについて、ハッシュイテレーションアルゴリズムをShiroのように独自実装せずに、総当たり攻撃耐性を持たせるために低速化させた既存のアルゴリズム(PBKDF2, BCrypt)もあるよ*1*2*3*4 と教えてもらったため、Shiroに組み込めないか試してみる。

ハッシュアルゴリズムをどうするか

@watarinさんのJavaでパスワードをハッシュ化するのに良い方法を調べてみたを入り口に色々と調べてみたが、以下の理由でBCrypt (Java実装はjBCrypt) を選んでみた。

  • Java7で動かしたい (PBKDF2WithHMAC-SHA-256がJava8から)
    • JBossEAP6.3が今のところJava8をサポートしてないため
  • ここで言及されている意見
    • しかし、実検証データがないのはちょっと不安
    • 処理において配列アクセスの多いBCryptがGPU対策に本当によいのかは不明
  • Java実装jBCryptSpringSecurityPicketlinkに若干の修正の上、引用されている
    • 今回はサンプルとしてjBCryptを使う
      • SpringSecurityをpom.xmlに追加すると、BCryptクラスを使いたいだけなのに、warサイズが増えるため
    • SpringSecurityのライセンスがApache License, Version 2.0だったので、jBCryptが怪しい場合はライセンスファイルと一緒にソースコード引用すれば良いと思う

実装する

公式コミュニティにおいても、SHIRO-290 Create a BCrypt Hash implementationとしてBCryptサポートが要望されているが、添付されている.patch案がストレッチ強度が調整できないようになってたので、作ってみる。

BCrypt向けPasswordServiceを作る

Shiro1.2より、パスワードのハッシュ化と照合を担うPasswordServiceインタフェースが用意されているため、デフォルト実装であるDefaultPasswordServiceクラスのコードを参考にして作ってみる。

public class BCryptPasswordService implements PasswordService {

    public static final int DEFAULT_BCRYPT_ROUND = 10;
    private int logRounds;

    public BCryptPasswordService() {
        this.logRounds = DEFAULT_BCRYPT_ROUND;
    }

    public BCryptPasswordService(int logRounds) {
        this.logRounds = logRounds;
    }

    @Override
    public String encryptPassword(Object plaintextPassword) {
        if (plaintextPassword instanceof String) {
            String password = (String) plaintextPassword;
            return BCrypt.hashpw(password, BCrypt.gensalt(logRounds));
        }
        throw new IllegalArgumentException(
            "BCryptPasswordService encryptPassword only support java.lang.String credential.");
    }

    @Override
    public boolean passwordsMatch(Object submittedPlaintext, String encrypted) {
        if (submittedPlaintext instanceof char[]) {
            String password = String.valueOf((char[]) submittedPlaintext);
            return BCrypt.checkpw(password, encrypted);
        }
        throw new IllegalArgumentException(
            "BCryptPasswordService passwordsMatch only support char[] credential.");
    }

    public void setLogRounds(int logRounds) {
        this.logRounds = logRounds;
    }

    public int getLogRounds() {
        return logRounds;
    }
}
ハッシュパスワード生成コードの修正

アカウント生成時にDefaultPasswordServiceを使っている部分を今作ったBCryptPasswordServiceに置き換える。

//  PasswordService ps = new DefaultPasswordService();

// ラウンド数をデフォルトから変えたい場合はコンストラクタに設定
// デフォルトは10
//  PasswordService ps = new BCryptPasswordService(12);
PasswordService ps = new BCryptPasswordService();
String encryptedPassword = ps.encryptPassword(form.getPassword());

コメントアウトしているように、ストレッチ強度を返る時にはコンストラクタに設定する。BCryptの特徴として、引数のラウンド数は2のXX乗のような指数で示すため、デフォルトの10を20に変更したら、計算量は2倍ではなく1024倍になるので注意。

ストレッチ強度はマシンスペックおよび許容できる遅延に応じて調整する。

shiro.iniの修正

パスワード照合時に先ほど作ったBCryptPasswordServiceが使われるように、shiro.iniのpasswordMatcherを修正する。

[main]
# Default SHA-256, 500000 hash iteration, use salt
#passwordService = org.apache.shiro.authc.credential.DefaultPasswordService

# BCrypt PasswordService
passwordService = net.agetsuma.sample.shiro.security.BCryptPasswordService
passwordMatcher = org.apache.shiro.authc.credential.PasswordMatcher
passwordMatcher.passwordService = $passwordService

ハッシュパスワード(例: $2a$10$e4l5...)にイテレーションのラウンド数(例では10)が組み込まれているため、shiro.iniで明示的にラウンド数を設定しなくてもパスワード照合が可能。

試しに作ってみたものの一式はGithubに置いてあります。

まとめ

  • jBCryptをApache Shiroに組み込んでみました
  • Apache ShiroではPasswordServiceインタフェースを拡張することで、様々なハッシュアルゴリズムに対応できます

GlassFish4.1をなおしてみた

この記事は GlassFish Advent Calendar 2014の12/25分の記事です。昨日は
@backpaper0さんのGlassFishでセッションIDを生成してるところでした。


2013/11にGlassFishの商用サポートの終了がアナウンスされて私も不安に思っていましたが、冷静に考えるとGlassFishはそもそもオープンソース(CDDL/GPLv2)です。ソースが公開されているので、もし不具合や納得いかない動作があれば自分で修正することができます。

先日、Java EE Advant Calanderの12/17分の記事を書いていたときに既知の不具合を踏んだので試しになおしてみました。以下の修正案は、私がGlassFishのソースに適当に試行錯誤して修正したとても怪しいものなので、間違ってたらすいません。

@Transactionalの例外時に原因がセットされていない

私が踏んだ不具合はGLASSFISH-21172として既にJIRAに登録されている、Java EE7から導入された@Transactionalでトランザクション境界を設定した場合、トランザクション境界内からランタイム例外が投げられても原因がよくわからないものです。

2014/12/25現在においても、今のところ修正はされていません。

具体的には以下のようなコードを実行すると問題に当たります。

@Dependent
public class OrderService {
  @Inject OrderRepository repository;

  @Transactional
  public Order takeOrder(Order order) {
    return repository.persist(order);
  }
}

@Dependent
public class OrderRepository {
  @PersistenceContext private EntityManager em;

  public Order persist(Order order) {
      Order o = em.merge(order);
      throw new NullPointerException("ぬるぽ");
  }
}

RepositoryからNPEを投げてます。上記のコードを動かし、catchせずに無視すると、GlassFish4.1では以下のようにログ出力されます。

[2014-12-16T22:04:57.576+0900] [glassfish 4.1] [SEVERE] [] [org.glassfish.jersey.server.ServerRuntime$Responder] [tid: _ThreadID=30 _ThreadName=http-listener-1(5)] [timeMillis: 1418735097576] [levelValue
: 1000] [[
  An exception has been thrown from an exception mapper class org.glassfish.jersey.gf.cdi.internal.TransactionalExceptionMapper$Proxy$_$$_WeldClientProxy.
javax.transaction.TransactionalException: Managed bean with Transactional annotation and TxType of REQUIRED encountered exception during commit javax.transaction.RollbackException: Transaction marked for
 rollback.
        at org.glassfish.cdi.transaction.TransactionalInterceptorRequired.transactional(TransactionalInterceptorRequired.java:105)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:483)
        at org.jboss.weld.interceptor.reader.SimpleInterceptorInvocation$SimpleMethodInvocation.invoke(SimpleInterceptorInvocation.java:74)
        at org.jboss.weld.interceptor.chain.AbstractInterceptionChain.invokeNext(AbstractInterceptionChain.java:116)
        at org.jboss.weld.interceptor.chain.AbstractInterceptionChain.invokeNextInterceptor(AbstractInterceptionChain.java:94)
        at org.jboss.weld.interceptor.proxy.InterceptorMethodHandler.executeInterception(InterceptorMethodHandler.java:43)
        at org.jboss.weld.interceptor.proxy.InterceptorMethodHandler.invoke(InterceptorMethodHandler.java:36)
        at org.jboss.weld.bean.proxy.CombinedInterceptorAndDecoratorStackMethodHandler.invoke(CombinedInterceptorAndDecoratorStackMethodHandler.java:51)
        at net.agetsuma.transactional.OrderService$Proxy$_$$_WeldSubclass.takeOrder(Unknown Source)
        .. 途中省略
Caused by: javax.transaction.RollbackException: Transaction marked for rollback.
        at com.sun.enterprise.transaction.JavaEETransactionImpl.commit(JavaEETransactionImpl.java:445)
        at com.sun.enterprise.transaction.JavaEETransactionManagerSimplified.commit(JavaEETransactionManagerSimplified.java:854)
        at com.sun.enterprise.transaction.TransactionManagerHelper.commit(TransactionManagerHelper.java:81)
        at org.glassfish.cdi.transaction.TransactionalInterceptorRequired.transactional(TransactionalInterceptorRequired.java:97)
        ... 66 more

なんとなくロールバックしたことはわかりますが、NPEが投げられたことが原因とはわかりません。システムの保守をやっていると、『環境的な要因なのか』『入力が間違ってたのか』などの例外の原因がわからないと結構困ります。

なおしてみる

オープンソースなので、困ったらなおしてみます。バグ報告やパッチ投稿方法、ロギングの指針などコミュニティに関するルールはGlassFish Project - Community Rulesにまとまっています。

ソースのダウンロード

GlassFishソースコードSubversionで管理されています。とりあえずソースを確認したいだけなので、4.1タグのソースをsvn exportで持ってきます。

svn export https://svn.java.net/svn/glassfish~svn/tags/4.1
ソースコードを読む

例外のスタックトレースを改めて見ると、JTA実装であるJavaEETransactionImpl.javaの445行目において、commitを試みたが既にトランザクションロールバック確定とマーキングされていたので、RollbackExceptionを投げたとあります。

Caused by: javax.transaction.RollbackException: Transaction marked for rollback.
        at com.sun.enterprise.transaction.JavaEETransactionImpl.commit(JavaEETransactionImpl.java:445)

JavaEETransactionImpl.commitメソッドを見てみます。

com.sun.enterprise.transaction.JavaEETransactionImpl.commit()メソッド (445行目付近)

439    if ( isRollbackOnly() ) {
440      // rollback nonXA resource
441      if ( nonXAResource != null )
442        nonXAResource.getXAResource().rollback(xid);
443
444        localTxStatus = Status.STATUS_ROLLEDBACK;
445        throw new RollbackException(sm.getString("enterprise_distributedtx.mark_rollback"));
446    }

JavaEETransactionalImpl.commit()の445行目で、commit()実行時に既にjavax.transaction.Transaction.setTransactionRollback()メソッドの実行によって、ロールバックすべきとマーキングされていた場合、ロールバックの上、さらにRollbackExceptionがスローされています。

このjavax.transaction.RollbackExceptionのjavadocを見ると、RollbackExceptionには例外causeを引数に取るコンストラクタは定義されていないことに気づきます。例外スタックトレースにcauseが入ってこないのは、ここのコードが原因のようです。しかし、例外の原因を設定するコンストラクタがなければ、この部分を修正して対処することは難しいと思いました。

次に、そもそもランタイム例外であるNPEが投げられてロールバックが確定しているのに、何故コミットを実行しようとしているのかが気になりました。あえてJavaEETransactionalImpl.commit()を実行によってロールバックさせずに、最初からロールバックを実行すれば良いのではと。

そこで、コミットを呼び出しているコードを探してみます。

org.glassfish.cdi.transaction.TransactionalInterceptorRequired.transactionalメソッド

 69    @AroundInvoke
 70    public Object transactional(InvocationContext ctx) throws Exception {
         ... 途中省略
 91    Object proceed = null;
 92    try {
 93        proceed = proceed(ctx);
 94    } finally {
 95        if (isTransactionStarted)
 96            try {
 97                getTransactionManager().commit();
 98            } catch (Exception exception) {
 99                 String messageString =
100                "Managed bean with Transactional annotation and TxType of REQUIRED " +
101                        "encountered exception during commit " +
102                         exception;
103                 _logger.log(java.util.logging.Level.INFO,
104                    CDI_JTA_MBREQUIREDCT, exception);
105                 throw new TransactionalException(messageString, exception);
106           }
107   }

proceed(ctx)の中から例外が投げられても、問答無用でcommit()しています。

proceedメソッドの挙動が気になります。このメソッドは今確認したTransactionalInterceptorRequiredクラスの親クラスTransactionalInterceptorBaseクラスで定義されています。

org.glassfish.cdi.transaction.TransactionalInterceptorBase クラス

208     public Object proceed(InvocationContext ctx) throws Exception {
      ... 途中省略
224         Object object;
225         try {
226             object = ctx.proceed();
227         } catch (RuntimeException runtimeException) {
228             Class dontRollbackOnClass =
229                     getClassInArrayClosestToClassOrNull(dontRollbackOn, runtimeException.getClass());
230             if (dontRollbackOnClass == null) { //proceed as default...
231                 markRollbackIfActiveTransaction();
232                 throw runtimeException;
233             }

トランザクション境界内の処理からRuntimeExceptionが投げられた場合、@Transactional(dontRollbackOn=XXX.class)に設定されているロールバック対象外の例外でなければ、231行目でトランザクションロールバック対象とマークし、次の行でメソッドの中で発生した例外を再スローしています。

すなわち、現在の実装は例外が発生した時点ではロールバック対象としてマーキングして、例外が再スローされていますが、すぐその後に子クラスのTransactionalInterceptorRequiredクラスでcommit()が実行されているため、既にロールバック対象としてマーキング済によるコミット時例外によって、元の例外が上書きされていることによって、例外の原因がわからない事象が発生しているようです。

TransactionalInterceptorRequiredクラスの修正アイディア

@AroundInvoke
public Object transactional(InvocationContext ctx) throws Exception {
   ... 途中省略
Object proceed = null;
try {
    proceed = proceed(ctx);
} finally {
    if (isTransactionStarted) { 
        if (ロールバックマーク済み?) {
            getTransactionmanager().rollback();
        } else {
            try {
                getTransactionManager().commit();

上記のように、ロールバック対象としてマーキングされている場合は、コミットせずに最初からロールバックして、投げられた例外がコミット例外によって上書きされないように修正したいです。

スペック(JTA1.2)の確認

GlassFish4.1はJava EE7仕様を実装した参照実装です。コードを修正するには仕様に違反しないか注意が必要です。

今回の場合、今まではロールバック時にはRollbackExceptionが返されていましたが、修正後にはアプリから投げられたランタイム例外をそのまま投げることになります。トランザクション周りの仕様はJava Transaction API (JTA)で決まれている為、JCPのサイトから仕様のPDFを入手して読んでみます。

JTA1.2仕様をRollbackExceptionで検索しても、『ロールバック時には必ずRollbackExceptionを投げること』のような記述は見当たらないため、アプリのランタイム例外をそのまま@Transactionalを付与したメソッドの呼び出し元に返しても大丈夫そうです。

不安なのでWildFly8.2でも試してみますが、@Transactionalの中からランタイム例外を投げると、特にラップされずに返ってきます。

22:37:46,952 ERROR [io.undertow.request] (default task-2) UT005023: Exception handling request to /TransactionalTest-1.0-SNAPSHOT/rest/orders: org.jboss.resteasy.spi.UnhandledException: java.lang.NullPointerException: ぬるぽ
	at org.jboss.resteasy.core.ExceptionHandler.handleApplicationException(ExceptionHandler.java:76) [resteasy-jaxrs-3.0.10.Final.jar:]
        ... 途中省略
Caused by: java.lang.NullPointerException: ぬるぽ
	at net.agetsuma.transactional.OrderRepository.persist(OrderRepository.java:24) [classes:]
	at net.agetsuma.transactional.OrderService.takeOrder(OrderService.java:26) [classes:]
ソースコード修正

最終的になおしてみたコードの.patchはGistにおいています。
patchコマンドで適用できます。

patch -p1 -d 4.1 < GLASSFISH-21172.patch

修正したソースコードGlassFish本流にパッチとして盛り込みをお願いする場合は、Oracle Contributor License (OCA)へのサインとオラクル社への送付が必要です。

修正したモジュールのビルド

GlassFish全体ビルドには時間が掛かるので、修正したモジュールのみビルドします。今回の対象モジュールはweld-integrationです。

cd 4.1/appserver/web/weld-integration
mvn package

GlassFish4.1はJDK7u9以上ででビルドします。JDK8でビルドすると、以下のようなメッセージが出力されて失敗します。

[WARNING] Rule 0: org.apache.maven.plugins.enforcer.RequireJavaVersion failed with message:
You need JDK greater or equal than 1.7.0-09 (JDK8 not supported yet)
モジュールの差し替え

新たに生成した weld-integration.jar を、元のファイルから差し替えます。

cp target/weld-integration.jar /home/test/work/glassfish4/glassfish/modules/weld-integration.jar 
統合テストの実行

GlassFishのソースにはQuicklook Testsと呼ばれる統合テストが含まれています。修正によって新たにバグを盛り込んでいないか確認したいので動かしてみます。
GlassFish Project - Community Rulesでは、maven runtestで動かせると書いてあります(mvnのこと?)。陳腐化しているようです。

試しに、普通にtestフェーズを指定して動かしてみます。テスト対象のGlassFishホームも合わせて設定してみます。

cd glassfish4/4.1/appserver/tests/quicklook
mvn -Dglassfish.home=/home/test/work/glassfish4/glassfish test
...
testng-summary:
     [echo]    [testng]
     [echo]    [testng] ===============================================
     [echo]    [testng] QuickLookTests
     [echo]    [testng] Total tests run: 120, Failures: 0, Skips: 0
     [echo]    [testng] ===============================================
     [echo]    [testng]

これで良いのか自信がないですが、テストをパスしたようです。

なおした結果

ロールバックの原因となった例外がそのままスローされるので、ロールバック原因がすぐわかるようになりました。

[2014-12-23T14:35:25.032+0900] [glassfish 4.1] [WARNING] [] [javax.enterprise.web] [tid: _ThreadID=28 _ThreadName=http-listener-1(2)] [timeMillis: 1419312925032] [levelValue: 900] [[
  StandardWrapperValve[org.netbeans.rest.application.config.ApplicationConfig]: Servlet.service() for servlet org.netbeans.rest.application.config.ApplicationConfig threw exception
java.lang.NullPointerException: ぬるぽ
    at net.agetsuma.transactional.OrderRepository.persist(OrderRepository.java:24)
    at net.agetsuma.transactional.OrderService.takeOrder(OrderService.java:26)
    ... 省略

まとめ

  • 試しにGLASSFISH-21172をなおしてみました。
  • なおしたコードはGistにおいてます
  • GlassFishオープンソースなので、誰でもソースが見れます。問題が起きても商用サーバならサポートに問い合わせるしかないですが、GlassFishなら解析する権利が誰にでも平等に与えられています。
  • Java EE がどう実装されているかコードを読むのはとても楽しいですね。

Apache Shiro を使ってみました

この記事は Java EE Advent Calendar 2014の12/17分の記事です。昨日は
@glory_ofさんのJAX-RSのレスポンスでした。明日は@nagaseyasuhitoさんです。


Java EE8では、『使い方が複雑・各APサーバ固有のレルム設定がよくわからん』とあまり評判のよくないセキュリティ周りの機能の再整理*1が行われようとしています。

しかし、Java EE8の仕様がリリースされるのはだいぶ先の2016年。

そんなに待てないので、Apache Shiroを試してみました。

Apache Shiroの既存の他の記事は部分的なコードの抜粋が多く、動かせるコードがあまり見当たらなかったので、GitHubにサンプルコードとしてまとめてみました。

Apache Shiro とは

Easy To Useを一番の目的にしたJavaのセキュリティフレームワークです。

歴史は古く2003年頃にJSecurityプロジェクトとして始まり、2008年よりApache Software Foundationに移管されました。Shiroは以下のような機能を持ちます。

  • 認証 (Authentication)
    • いわゆる『ログイン』機能です。
  • 認可 (Authorization)
    • アクセス制御。例えば、偉い人だけが人事計画情報が見れるなど。
  • 暗号 (Cryptography)
    • Java標準の暗号APIをラップして、簡単にSHA-256ハッシュが取得できるなど。
  • セッション管理

Apache Shiroを使うと、APサーバごとにレルム設定に悩まなくても同じ設定で認証・認可周りが実装できます。

アーキテクチャと設定

コード例とshiroのコンフィグであるshiro.iniを踏まえて紹介します。
例では、DBMSに認証・認可情報を保存していますが、LDAP連携もできます。

f:id:n_agetsuma:20141214121915p:plain

Subject

ユーザは、このSubjectと呼ばれるインタフェースを通して、認証・認可の判断をShiroに委ねます。Subjectには、以下のようにSecurityUtilsからアクセス可能です。

// Shiroによる認証コード
Subject currentUser = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(email, password);
try {
  currentUser.login(token);
} catch (AuthenticationException ae) {
  // authentication failed.
}
SecurityManager

認証・認可に具体的にどのような仕組みを使うのかを管理しているクラスです。Shiroの設定ファイルであるshiro.iniをクラスパスからロードして、このSecurityManagerを組み立てます。実際のshoro.iniの例を示します。

[main]
# Default SHA-256, 500000 hash iteration, use salt
passwordService = org.apache.shiro.authc.credential.DefaultPasswordService
passwordMatcher = org.apache.shiro.authc.credential.PasswordMatcher
passwordMatcher.passwordService = $passwordService

ds = org.apache.shiro.jndi.JndiObjectFactory
ds.requiredType   = javax.sql.DataSource
ds.resourceName = java:comp/DefaultDataSource

jdbcRealm = org.apache.shiro.realm.jdbc.JdbcRealm
jdbcRealm.authenticationQuery = SELECT password FROM useraccount WHERE email = ?
jdbcRealm.userRolesQuery = SELECT userrole FROM useraccount WHERE email = ?
jdbcRealm.credentialsMatcher = $passwordMatcher
jdbcRealm.dataSource = $ds

securityManager.realms = $jdbcRealm

PasswordMatcherの設定

  • 最初の3行(passwordMatcher)は、パスワードの照合の仕組みを設定
    • Shiroに組み込まれているDefaultPasswordServiceを指定
    • ソルト付き/SHA-256/イテレーション50万回

データソース設定

  • 次の3行(ds)は、jdbcRealmが使うデータベースのアクセス先設定
    • コネクションプールを使いたいのでデータソースjava:comp/DefaultDataSourceを設定

JDBCレルム設定

  • 次の5行(jdbcRealm)は、認証・認可時にDBMSよりユーザ情報を取得するJdbcRealmを設定
    • デフォルトの設定はソースコードより確認できます
    • authenticationQueryは、認証時にパスワードを取得するSQL
      • デフォルトselect password from users where username = ?
    • userRolesQueryは、認可時にロール情報を取得するSQL
      • デフォルトselect role_name from user_roles where username = ?
    • jdbcRealm.credentialsMatcherに先ほど設定したpasswordMatcherを指定
      • 認証時に平文パスワードをShiroに渡すと、PasswordMatcherの設定に基づきハッシュ化してDBMS上に保存しているハッシュパスワードと照合される
    • dataSourceに先ほど設定ししたdsを指定
      • SQL実行時にデータソースが使われる

SecurityManager設定

  • 最後の securityManager.realms = $jdbcRealmで、セキュリティマネージャにレルムとして今まで設定してきたjdbcRealmを使うと設定
Realm

どこから認証・認可情報を取得するのかを示します(DBMS/LDAP/静的ファイル)。
公式リファレンスではDAO(Data Access Object)のようなものだとのこと。

前述の設定例にて、JdbcRealmの例を示しました。

使い方

サンプルコードに実例が色々と含まれていますが、簡単にまとめます。

pom.xml

shiro-coreが本体で、shiro-webがカスタムJSPタグや、Servletフィルタの実装が入っています。WebアプリケーションでShiroを使う場合は両方とも必要です。

<dependency>
  <groupId>org.apache.shiro</groupId>
  <artifactId>shiro-core</artifactId>
  <version>1.2.3</version>
</dependency>
<dependency>
   <groupId>org.apache.shiro</groupId>
   <artifactId>shiro-web</artifactId>
   <version>1.2.3</version>
</dependency>

設定ファイル (shiro.ini)

内容については前述の通りです。
Webアプリケーション(.war)の場合、WEB-INFの直下、またはクラスパスのルートmain/src/resources/shiro.iniに置きます。

API

パスワードのハッシュ化

Shiroは『ユーザ作成機能』を持っていません。
その代わり、ユーザ作成に便利なパスワードハッシュ機能を持っています。

Shiroが用意するDefaultPasswordServiceクラスではを使うと、ソルト付き/SHA-256/イテレーション50万回のパスワードハッシュが2行のコードで取得できます。

PasswordService ps = new DefaultPasswordService();
String encryptedPassword = ps.encryptPassword(yourPassword);

ハッシュ化されたパスワードは以下のような形式です。

将来的にSHA-256でも弱くなってきたとき、パスワードフォーマットにハッシュアルゴリズム名が入っているので、SHA-256が含まれるパスワードを持つユーザ全てにパスワード再設定依頼のメールを送るなどの使い方ができます。

$shiro1$SHA-256$500000$DaYxi7JkOR5asUxrMJVZiQ==$ELBJExhsFc+RiiV8uqGF5z/8DvoPZzzo8mXZHkWGVh0=

フォーマット
$shiro1$ハッシュアルゴリズム名$イテレーション回数$base64エンコードされたソルト$base64エンコードされたパスワード

このパスワード化されたハッシュをJPAなどを使ってDBMSに保存します。

DefaultPasswordServiceを使ってハッシュ化されたパスワードを生成した場合は、設定例で紹介したようにshiro.iniには認証時に使うハッシュとしてDefaultPasswordServiceを設定しなければいけません。ハッシュパスワード生成時と、認証処理の照合時で同じアルゴリズムでないと同一性が判断できないためです。

パスワードの中にソルト入ってて大丈夫?
安全性とプログラミングの煩雑さのトレードオフだと、徳丸先生からのアドバイスがあったので、扱う情報に応じてDefaultPasswordServiceではなくて、独自に実装したものを使うと良いと思います。

認証・認可

認証・認可はとても簡単にできます。

認証
UserNamePasswordTokenクラスにユーザIDと平文パスワードの組を設定し、Subjectに渡して判定します。
このloginメソッドの内部で、shiro.iniで設定したレルム設定に基づき、引数パスワードのハッシュ化と、DBMSからハッシュパスワードの取得および照合を行います。

// Shiroによる認証コード (再掲)
Subject currentUser = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(email, password);
try {
  currentUser.login(token);
} catch (AuthenticationException ae) {
  // authentication failed.
}

認証に成功すると、これ以降にsubject.isAuthenticated()を実行するとtrueが返るようになります。未認証の場合はfalseです。

// 認証済みかの判定
Subject currentUser = SecurityUtils.getSubject();
if (currentUser.isAuthenticated()) {
    // already login
} else {
   // not logged in
}

認証に失敗した場合は、AuthenticationExceptionの子クラス例外がエラー要因に応じて投げれられます。

あまり認証エラー理由を細かくユーザにフィードバックするのも良くないので、AuthenticationExceptionをキャッチしても良いと思います。

認可
認可もかんたんです。hasRoleメソッドで、現在アクセスしているユーザが引数のロールを持っているか判定します。

Subject currentUser = SecurityUtils.getSubject();
if (currentUser.hasRole("yourRoleName")) {
    // hasRole
} else {
    // not hasRole
}

このhasRoleメソッドの内部でも同様に、shiro.iniで設定したレルム設定に基づいてロール情報をDBMSから取得して照合しています。

ログアウト
ログアウトもシンプルで、logoutメソッドを実行するだけです。

Subject currentUser = SecurityUtils.getSubject();
currentUser.logout();
セッション操作

ServletのセッションAPIに似ていますが、セッションの終了だけはsession.invalidate()ではなく、session.stop()です。

Subject current = SecurityUtils.getSubject();
// セッションの取得。既存セッションが無ければ新規生成。
Session session = current.getSession();
// 既存セッションがなければnullを返す。
Session session1 = current.getSession(false);
// 属性の取得・設定
int cnt = (Integer)session.getAttribute("counter");
session.setAttribute("counter", 1);
// セッションの終了
session.stop();

Shiroはスタンドアロン環境でも使えるようにShiro固有のセッション管理機構を持っていますが、Servletコンテナで動作させた場合はデフォルトでServletコンテナのセッションが使われます。

このため、セッションを取得するといつも通り、クッキー名JSESSIONIDがSet-Cookieヘッダに載ってきます。

f:id:n_agetsuma:20141215225920p:plain

Webアプリで使う

フィルタの設定

ApacheShiroはスタンドアロンのアプリでも使えるらしいですが(試してない)、Webアプリで使う場合は、web.xmlに以下の設定が必要です。

<filter>
  <filter-name>ShiroFilter</filter-name>
  <filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class>
</filter>
<filter-mapping>
  <filter-name>ShiroFilter</filter-name>
  <url-pattern>/*</url-pattern>
  <dispatcher>REQUEST</dispatcher>
  <dispatcher>FORWARD</dispatcher>
  <dispatcher>INCLUDE</dispatcher>
  <dispatcher>ERROR</dispatcher>
</filter-mapping>
<listener>
   <listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class>
</listener>

ShiroFilterの設定が漏れていると、SecurityUtils.getSubject()を実行したときに現在のコンテキストを取得できないと以下のような例外が返ってきます。dispatcherの部分はforwardなどのリクエストを伴わない遷移時にもフィルタが有効になることを意図しています。

org.apache.shiro.UnavailableSecurityManagerException: No SecurityManager accessible to the calling code, either bound to the org.apache.shiro.util.ThreadContext or as a vm static singleton.  This is an invalid application configuration.
	at org.apache.shiro.SecurityUtils.getSecurityManager(SecurityUtils.java:123)
	at org.apache.shiro.subject.Subject$Builder.<init>(Subject.java:627)
	at org.apache.shiro.SecurityUtils.getSubject(SecurityUtils.java:56)

EnvironmentLoaderListenerは、アプリ起動時にshiro.iniをロードしています。

カスタムJSPタグ

認証OK/NGでログインフォームへのリンクを表示するか、Webcome ユーザ名を表示するか分けたい、ロールに応じて出力情報を変えたいなど、認証・認可とテンプレートは密接な関係があります。

ShiroではJSPを対象にカスタムタグを用意しています。GitHub上では、Thymeleaf向けの拡張を作っている人もいるみたいです。

認証タグ

<shiro:authenticated>は、認証済みの場合のみ表示されます。
<shiro:principal />で認証済のユーザ名を取得することもできます。
<shiro:notAuthenticated>は未認証の場合のみ表示です。

<%@taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
<shiro:authenticated>
    <h3>Hello, <shiro:principal/></h3>
</shiro:authenticated>
<shiro:notAuthenticated>
    <h3>Hello, Guest</h3>
</shiro:notAuthenticated>

認可タグ
<hasRole>タグでは、ロール名を指定して表示可否を設定できます。

<shiro:hasRole name="MANAGER">
    <!-- 大事な情報 -->
</shiro:hasRole>
Remember me

ログインフォームには、以下のようにブラウザを閉じた後でも状態を保持するようにkeep me signed inのようなチェックボックスがあると思います。

f:id:n_agetsuma:20141215231013p:plain

ShiroではRemember me機能として、この機能の仕組みを提供しています。

このRemember meを有効にするためには、ログイン時のUsernamePasswordTokenのコンストラクタの引数にtrue/falseを追加するだけです。trueを設定するとRemember meが有効になります。

Subject currentUser = SecurityUtils.getSubject();
// remember me の有効化
UsernamePasswordToken token = new UsernamePasswordToken(email, password, true);
try {
  currentUser.login(token);
  ...

remember meを有効にした状態でログインすると、ユーザにはクッキー名rememberMeが返されます。ブラウザを閉じても、再度アクセスしたときにこのrememberMeクッキーが送られることで、状態を保持しています。

f:id:n_agetsuma:20141215231931p:plain

ログアウトすると、サーバよりMax-Age=0が設定されたrememberMeが返され、クッキーは削除されます。

f:id:n_agetsuma:20141215232111p:plain

注意が必要なのは、認証済み状態と、Remember Me状態は異なることです。例えば、JSPタグ<shiro:authenticated>で囲った部分はRemember Me状態では表示されません。

Remember Me状態は、Amazonでいうと買い物かごの操作はできるが、購入処理ができない状態を示しており、購入処理をするためには再度認証処理が必要です。

Remember Me状態は、APIではSubject.isRemembered()で判定できます。

Subject currentUser = SecurityUtils.getSubject();
if (currentUser.isRemembered()) {
   ...

JSPカスタムでは、<shiro:user>タグで囲ってある範囲が、Remember me状態で表示されます。

<shiro:user>
  <h3>Welcome back <shiro:principal/> !</h3>
  <p>Not <shiro:principal/> ?</p>
  <p>Please signin <a href="#">here</a></p>
</shiro:user>
その他

サンプルコードでは使っていないため詳細は紹介しませんが、form認証のように設定ファイルに認証対象のパスを書いて宣言的に設定する機能や、ロールよりももっと詳細に認可管理できるPermission機能など、必要そうな機能は一通り揃っています。

サンプルコード

以下のような画面を持つサンプルを作って公開してみました。上記で紹介した色々な機能がこのコードに盛り込まれています。ぜひShiroを使ってみようと思っている方は参考にしてみてください。

f:id:n_agetsuma:20141214010825p:plain

  • 左上のSign Up
    • ShiroのPasswordService機能を使って、入力されたパスワードをハッシュ化して、DBに保存しています
  • 右上のSign In
    • ShiroのAPIを使ってログインしています。ブラウザを閉じた後もセッションを保持するRemember meも実装しています。
  • 下のAutholizationのセクション
    • managerロールを持つアカウントでログインすると、登録されているユーザ一覧が表示されます。

まとめ

  • Apache Shiroは比較的シンプルに使えるセキュリティ管理ライブラリです
    • とはいえ、shiro.iniはそれなりに設定が必要です
    • APサーバの固有設定に依存せずにレルム設定できるのは嬉しいところ
  • ここまで書いておいて、そもそも認証・認可を各アプリで作り込むべきか?
    • まだ追いつけてませんが、OpenID Connectのような仕組みを使って、認証をアプリ外に委譲するのが、社内システムでも一般的になっていくのでしょうか
  • え? Java EEじゃない? サンプルコードの環境にGlassFish4を使ってるのでお許しを

MavenのJAX-WSプラグイン

まだまだエンタープライズな分野ではJAX-WSがシステム間連携に使われることが多い。wsdlからスタブを生成するMavenプラグインの使い方に関するメモ。

JAX-WS Mavenプラグイン

https://jax-ws-commons.java.net/jaxws-maven-plugin/JAX-WSの参照実装Metroの一部として提供されている。最新バージョンは2.3。

pom.xml の書き方

wsimport (wsdlからスタブコード生成) の例。設定可能なパラメータの一覧より、よく使う部分だけ以下に記載する。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>net.agetsuma</groupId>
    <artifactId>SOAPClient</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.jvnet.jax-ws-commons</groupId>
                <artifactId>jaxws-maven-plugin</artifactId>
                <version>2.3</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>wsimport</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <!--  .wsdlの格納ディレクトリ。デフォルト ${basedir}/src/wsdl -->
                    <!--
                    <wsdlDirectory>src/main/wsdl</wsdlDirectory>
                    -->
                    <!-- 
                       スタブ生成対象のwsdlファイル名。wsdlDirectory配下のファイル名を指定。
                       定義がなかった場合はwsdlDirectory配下すべての.wsdlが対象
                     -->
                    <!--
                    <wsdlFiles>
                        <wsdlFile>HelloWorld.wsdl</wsdlFile>
                    </wsdlFiles>
                    -->
                    <!-- 
                        中間生成ファイルを保持するか。具体的にはスタブのソースコードのこと。
                        デフォルトはfalseで.classファイルのみ生成する。
                    -->
                    <keep>true</keep>
                    <vmArgs>
                        <vmArg>-Djavax.xml.accessExternalSchema=all</vmArg>
                    </vmArgs>
                </configuration>  
            </plugin>
        </plugins>
    </build>
</project>

このプラグインはデフォルトでフェーズgenerate-source (コンパイルする前) *1 に動くため、中間生成ファイルとして出力されるソースコードは破棄しても問題ないが、デバッグ目的で生成しておくと便利。

JDK8ではvmArgsの設定がないと例外が出て動かない。詳細についてはNetBeansのBug 241570や、このMavenプラグインのJIRA JAX_WS_COMMONS-129で紹介されている。

ビルド

いつも通りmvn packageでビルドできる。

mvm clean package

*1:Mavenフェーズ一覧についてはこちらを参照