Java EE 7でもアクションベースMVC! -MVC 1.0への移行を睨んだJersey MVCの活用- #javaee
この記事について
このブログは、Java EE Advent Calendar 2015 - Qiitaの25日目(最終日)です。
昨日はhondaYoshitakaさんの「Java - JAX-RSによるExcel/CSV/PDFファイルダウンロード - Qiita」でした。
MVC 1.0はJava EE 8から
アクションベースMVCと言えば、Java EE 8で導入される「MVC 1.0」ですね。
MVC 1.0の詳細については、今年のGlassFish勉強会の資料をご覧ください。
Java EE 8先取り!MVC 1.0入門 [EDR2対応版] 2015-10-10更新
EE 8は2017年上半期リリース予定ですので、現在のEE 7ではMVC 1.0は使えません。
MVC 1.0の参照実装「Ozark」は既にGitHub・Maven上で公開されていますが、まだ策定途中のため仕様が変更される可能性が多々あり、学習目的以外では使わないほうがよいでしょう。
Jersey MVCならJava EE 7でも使える!
JAX-RSの参照実装「Jersey」には、独自機能「Jersey MVC」があります。
Jersey MVC自体はすでに完成しているフレームワークであり、Java EE 7時代の現在でも使うことができます。
MVC 1.0は、このJersey MVCを参考にして作られていると言われており、実際にも似た部分が多くあります(もちろん、異なる部分もあります)。
Java EE 7の今はJersey MVCで作り、EE 8リリース後にMVC 1.0に移行するという手もアリなのではないでしょうか。
そこで今回は、EE 8でMVC 1.0に移行することを見据えた上で、EE 7とJersey MVCでどのように作るか、ということを考えていきたいと思います。
Jersey MVCは日本語ブログも結構多いのですが、必要な設定などが結構変わっているため、最新情報をまとめるという意味で、この記事を書きました。
注意点
Jersey MVCは、あくまでJerseyの独自機能であり、Java EE 7標準のものではありません。
よって、Java EEのメリットの1つである「サーバーベンダーからのサポート」は対象外になる可能性があります。
サーバーベンダーのサポートポリシーや、ご自分のプロジェクトの事情などを考慮した上で、ご利用ください。
今回の方針
- MVC 1.0の再発明はしない。(MVC 1.0はまだ仕様が確定していないため)
- Jersey MVC自体への修正も加えない。(どこか修正すると修正点が芋づる式に増えてキリがないため)
- Jersey MVCおよびJava EE 7の機能の範囲内で、MVC 1.0への移行コストがなるべく小さくなる実装を模索する。
環境
- Payara Web ML 4.1.1.154
- Jersey 2.22.1
- Maven 3.3.3
- IntelliJ IDEA 15 (NetBeansでもEclipseでもいいです)
- JDK 8u66
比較対象のMVC 1.0は、2015年10月に公開されたEDR2版とします。
完成版のコード
これ以降のコードは、重要な部分のみ抜き出して、一部省略しています。
完全なコードはGitHubにアップしていますので、こちらをご参照ください。
GitHub - MasatoshiTada/JavaEEAdventCalendar2015-JerseyMVC
Mavenプロジェクトの作成
それでは、手順を説明していきます。
Mavenでプロジェクトを作成し、pom.xmlに依存性を追加します。
<properties> <javaee.version>7.0</javaee.version> <jersey.version>2.22.1</jersey.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.glassfish.jersey</groupId> <artifactId>jersey-bom</artifactId> <version>${jersey.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <!-- Java EE 7 Web Profile --> <dependency> <groupId>javax</groupId> <artifactId>javaee-web-api</artifactId> <version>${javaee.version}</version> <scope>provided</scope> </dependency> <!-- Jersey MVC + Jersey MVC JSP --> <dependency> <groupId>org.glassfish.jersey.ext</groupId> <artifactId>jersey-mvc-jsp</artifactId> <scope>provided</scope> </dependency> <!-- JerseyでBean Validationを使う --> <dependency> <groupId>org.glassfish.jersey.ext</groupId> <artifactId>jersey-bean-validation</artifactId> <scope>provided</scope> </dependency> <!-- Jersey MVCでBean Validationを使う --> <dependency> <groupId>org.glassfish.jersey.ext</groupId> <artifactId>jersey-mvc-bean-validation</artifactId> <!-- Payara(GlassFish)に含まれていないので「compile」でWARに含める --> <scope>compile</scope> </dependency> <!-- JSTL --> <dependency> <groupId>org.glassfish.web</groupId> <artifactId>javax.servlet.jsp.jstl</artifactId> <version>1.2.4</version> <scope>provided</scope> </dependency> </dependencies>
ほとんどの依存性はPayaraに含まれているのでprovided
ですが、jersey-mvc-bean-validationのみPayaraに含まれていないのでcompile
にしています。
設定クラスの作成
JAX-RSを有効化するためには、通常javax.ws.rs.Application
クラスのサブクラスを作成しますが、今回はJersey独自のApplication
サブクラスであるResourceConfig
クラスを継承します。
package com.example.rest; import org.glassfish.jersey.server.ResourceConfig; import org.glassfish.jersey.server.mvc.beanvalidation.MvcBeanValidationFeature; import org.glassfish.jersey.server.mvc.jsp.JspMvcFeature; import javax.ws.rs.ApplicationPath; @ApplicationPath("api") public class MyApplication extends ResourceConfig { public MyApplication() { // Jersey MVCの登録、ビューとしてJSPを使う register(JspMvcFeature.class); // Jersey MVCにおけるBean Validationを有効化する register(MvcBeanValidationFeature.class); // JSPファイルを保存するフォルダを指定する property(JspMvcFeature.TEMPLATE_BASE_PATH, "/WEB-INF/views/"); // com.example以下の全パッケージを登録対象にする packages(true, this.getClass().getPackage().getName()); } }
Jersey MVCを利用するには、JspMvcFeature
クラスの登録が必要になります。
Application
クラスを継承しても出来なくはないのですが、全リソースクラスおよび@Provider
が付加されたクラスもすべて登録しなければならず、手間がかかります。
JAX-RSには、もともとAuto Discoveryというリソースクラスなどを自動的に登録する機能があるのですが、1つでもクラスを自前で登録してしまうと、Auto Discoveryが無効になってしまうのです。
ResourceConfig
にはpackage()
という、指定されたパッケージ名内のクラスをすべて登録するメソッドが定義されており、便利です。第1引数をtrue
にすることで、サブパッケージ内のクラスも再帰的に登録します。
コントローラークラスの作成
Jersey MVCでは、リソースメソッドの戻り値をorg.glassfish.jersey.server.mvc.Viewable
することで、リソースメソッドをコントローラーメソッドにすることができます。
package com.example.rest.resource; import com.example.rest.dto.HelloDto; import com.example.rest.exception.MyException; import java.io.IOException; import com.example.rest.exception.MyRuntimeException; import org.glassfish.jersey.server.mvc.ErrorTemplate; import org.glassfish.jersey.server.mvc.Viewable; import javax.enterprise.context.RequestScoped; import javax.inject.Inject; import javax.validation.constraints.Pattern; import javax.validation.constraints.Size; import javax.ws.rs.DefaultValue; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.QueryParam; @Path("hello") @RequestScoped public class HelloResource { @Inject private HelloDto helloDto; @GET @Path("index") public Viewable index() { return new Viewable("/hello/index.jsp"); } @GET @Path("result") @ErrorTemplate(name = "index") public Viewable result(@QueryParam("name") @DefaultValue("") @Size(message = "{name.size}", min = 1, max = 10) @Pattern(message = "{name.pattern}", regexp = "[a-zA-Z]*") String name) throws Exception { // 例外を起こすサンプル switch (name) { case "null": throw new NullPointerException("NULLPO!"); case "myrun": throw new MyRuntimeException("MyRuntime!"); case "run": throw new RuntimeException("Runtime!"); case "io": throw new IOException("IOE!"); case "myex": throw new MyException("MY EXCEPTION!"); case "ex": throw new Exception("EXCEPTION!"); } // 本来の処理 helloDto.setMessage("Hello, " + name); return new Viewable("/hello/result.jsp"); } }
return new Viewable("/hello/index.jsp");
とすることで、index.jspにフォワードするという意味になります。(拡張子.jsp
は付けなくても動きます)。
相対パスと絶対パス
Viewable
コンストラクタの引数に指定するJSPファイルへのパスは、「/」で始める絶対パスと、「/」で始めない相対パスの2種類があります。
まず、絶対パスと相対パスで共通するのは、前述のJspMvcFeature.TEMPLATE_BASE_PATH
で指定したフォルダ(今回の場合は「/WEB-INF/views/」)を読むということです。
絶対パスの場合、「/WEB-INF/views/」からの絶対パスになります。例えば、戻り値をreturn new Viewable("/hello/index.jsp");
とした場合、フォワード先のJSPは/WEB-INF/views/index.jsp
です。
相対パスの場合、「/WEB-INF/views/リソースクラスのパッケージのパス/リソースクラス名/コントローラーの戻り値」となります。例えば、リソースクラスのパッケージがcom.example.rest.resource
、リソースクラス名がHelloResource
、戻り値がreturn new Viewable("index.jsp");
の場合、フォワード先のJSPは/WEB-INF/views/com/example/rest/resource/HelloResource/index.jsp
です。
MVC 1.0との比較
今のところEDR2の仕様(一部Ozarkの挙動)では、以下のようになっています。
- コントローラーの戻り値は
String
またはjavax.mvc.Viewable
(void
やResponse
も可能) - 拡張子の指定が必須
- 「/」で始める絶対パスの場合、フォワード先のビューは
コンテキストルート/コントローラーの戻り値
(JSRには「/」で始める場合の記述がないため、今のところOzark独自の挙動っぽい) - 「/」で始めない相対パスの場合、デフォルトでは
/WEB-INF/views/コントローラーの戻り値
絶対パス・相対パス共に、Jersey MVCとは微妙に異なります。
こうなると、Jersey MVCでは相対パス・絶対パスのどちらで書いたほうが移行コスト(=プログラム等の修正箇所)が少ないかは、一概には言えない感じがしますね・・・(^^;
そもそも、現在のMVC 1.0のサブフォルダ名まで指定しなければいけないこと自体がカッコよくない気がするなあ。。。
似たような議論がMVC 1.0のメーリングリストでもあったのですが、採用されないまま終わっています。
https://java.net/projects/mvc-spec/lists/users/archive/2015-12/message/6
Jersey MVCでは絶対パス・相対パスのどちらがいいのか、まだ悩み中です・・・。
ビューの作成
前述のフォルダに、JSPファイルを作成します。
index.jsp(入力画面)
<%@ taglib prefix="mytag" uri="http://example.com/myTag" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>名前入力画面(絶対パス)</title> <link rel="stylesheet" href="../../css/style.css"> </head> <body> <h1>名前を入力してください</h1> <mytag:errors errorClass="error"/> <form method="get" action="./result"> 名前:<input type="text" name="name"> <input type="submit" value="送信"> </form> <a href="./redirect">リダイレクト</a> </body> </html>
result.jsp(出力画面)
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>メッセージ表示画面(絶対パス)</title> </head> <body> <c:out value="${hello.message}"/> </body> </html>
JSP以外のビューを使う方法
Jersey MVCでサポートしているビューは、JSP・FreeMarker・Mustacheです。
https://jersey.java.net/documentation/latest/user-guide.html#d0e15182
また、org.glassfish.jersey.server.mvc.spi.TemplateProcessor
インタフェースを実装することで、他のビューを使うことも可能です。
下記の@glory_ofさんのブログの場合、Thymeleafを使っていらっしゃいます。
JerseyMVCとThymeleafを組み合わせる - シュンツのつまづき日記
コントローラーからビューへの値の受け渡し
Jersey MVCで定義されているのは、Viewable
コンストラクタにオブジェクトを渡し、JSPではELでmodel
という名前で参照する方法です。
@GET @Path("result") public Viewable result(@QueryParam("name") @DefaultValue("") String name) throws Exception { helloDto.setMessage("Hello, " + name); return new Viewable("/hello/result.jsp", helloDto); }
<c:out value="${model.message}"/>
しかし、この方法はMVC 1.0には現時点では無く、かつオブジェクトが1つしか渡せないというデメリットがあります。
MVC 1.0にはModels
というマップがあるのですが、これを再発明することは今回の「MVC 1.0の再発明はしない」という方針に反します。
そこで、Jersey MVCとMVC 1.0の両方で使える、CDIビーンを使う方法を紹介します。
まず、DTOクラスを作成し、@Named
と@RequestScoped
を付加します。
package com.example.rest.dto; import javax.enterprise.context.RequestScoped; import javax.inject.Named; @Named("hello") @RequestScoped public class HelloDto { private String message; public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } }
コントローラーでは、このDTOをフィールドインジェクションし、DTOのsetterで値をセットします。
@Inject private HelloDto helloDto; @GET @Path("result") public Viewable result(@QueryParam("name") @DefaultValue("") String name) throws Exception { helloDto.setMessage("Hello, " + name); return new Viewable("/hello/result.jsp"); }
JSPのELでは、@Named
で指定した名前で呼び出します。
<c:out value="${hello.message}"/>
この方法なら、Jersey MVC (というかJava EE 7)でもMVC 1.0でも使えます。
受け渡す値が2つ以上ならば、その数だけDTOクラスを作成することになります。
2016-01-14追記
改めて考えてみたら、CDIビーンを使う方法は微妙な気がしてきました・・・。
データが複数件の場合、Listをフィールドに持つDTOクラスを別途作るのか?例えば、社員DTOと社員リストDTOを両方作るのは微妙です・・・。
データが無かった場合(DBの検索結果が無かったとか)、それを表すフラグフィールドを作るのか?これも何かなあ・・・。
以上のことを考えると、CDIビーンではなく、Viewableコンストラクタに渡す方法がいいかもしれません。
複数の値をビューに渡す場合は、複数の値をHashMapにputして、このマップをViewableコンストラクタに渡せばOKです。
2016-01-27追記
@glory_ofさんから、「複数の値をビューに渡す場合は、それらをラップする大きなDTOクラスを作って、それをViewableコンストラクタ渡す」という方法を教えていただきました。
// @Namedや@RequestScopedなどはつけなくてOK public class LargeDto { private Employee employee; private Department department; ... } // コントローラーメソッド内 LargeDto largeDto = ...; return new Viewable("/hello/result", largeDto);
この方法のメリットは、クラスを作るので型安全やIDEの補完機能が効くことです。
デメリットは、作るクラスが増えて手間が少しかかることですね。でも、そんなに大きな工数はかからないと思います。
上記のMapを使う方法と、メリット・デメリットを比較して、どちらの方法を使うか考えていただければと思います。
バリデーションと例外処理
MVC 1.0では、BindingResult
でバリデーションエラーの有無およびエラーメッセージの表示を行い、例外処理はJAX-RS標準のExceptionMapper
を利用します。
Jersey MVCにはBindingResult
と同様の動きをするものが存在しません。
色々と考えたのですが、ここは素直にJersey MVCで提供されている機能を使いましょう。
@ErrorTemplate
の利用
コントローラーメソッドに@ErrorTemplate
を付加し、バリデーションエラーおよび例外発生時に遷移するビューを指定します。
このビュー名も、コントローラーと同様のルールで相対パスまたは絶対パスで指定します。
@GET @Path("result") @ErrorTemplate(name = "index.jsp") // 相対パス // @ErrorTemplate(name = "/hello/index.jsp") // 絶対パス public Viewable result(@QueryParam("name") @DefaultValue("") @Size(message = "{name.size}", min = 1, max = 10) @Pattern(message = "{name.pattern}", regexp = "[a-zA-Z]*") String name) throws Exception { // 例外を起こすサンプル switch (name) { case "null": throw new NullPointerException("NULLPO!"); case "myrun": throw new MyRuntimeException("MyRuntime!"); case "run": throw new RuntimeException("Runtime!"); case "io": throw new IOException("IOE!"); case "myex": throw new MyException("MY EXCEPTION!"); case "ex": throw new Exception("EXCEPTION!"); } // 本来の処理 helloDto.setMessage("Hello, " + name); // return new Viewable("/hello/result.jsp"); // 絶対パス return new Viewable("result.jsp"); // 相対パス }
今回は、「1文字以上10文字以下でなければならない」「入力文字列は半角英字でなければならない」というルールにしました。
@Size
や@Pattern
は、Java EEのBean Validationで定義されたアノテーションです。
バリデーションエラー時は、javax.validation.ConstraintViolationException
が発生します。
この例外に対応したExceptionMapper
実装クラスが、jersey-mvc-bean-validationに含まれています(org.glassfish.jersey.server.mvc.beanvalidation.ValidationErrorTemplateExceptionMapper
クラス)。
このValidationErrorTemplateExceptionMapper
には、バリデーションエラー発生時に@ErrorTemplate
で指定されたビューにフォワードする処理が記述されています。
ConstraintViolationException
以外の例外がコントローラーメソッド内で発生した場合、jersey-mvcに含まれているorg.glassfish.jersey.server.mvc.internal.ErrorTemplateExceptionMapper
クラスが動き、@ErrorTemplate
で指定されたビューにフォワードします。
エラーメッセージの表示
バリデーションエラー時もその他の例外発生時も、JSPのELではmodel
という名前で参照します。
バリデーションエラー時はList<ValidationError>
、例外発生時はその例外オブジェクトそのものが、model
に格納されます。
これもどうするか非常に悩んだのですが、JSPカスタムタグを作りました。
package com.example.servlet.tag; import org.glassfish.jersey.server.validation.ValidationError; import javax.servlet.jsp.JspException; import javax.servlet.jsp.JspWriter; import javax.servlet.jsp.tagext.SimpleTagSupport; import java.io.IOException; import java.util.List; public class ErrorsHandler extends SimpleTagSupport { private String errorClass; public void setErrorClass(String errorClass) { this.errorClass = errorClass; } @Override public void doTag() throws JspException, IOException { JspWriter out = getJspContext().getOut(); Object model = getJspContext().findAttribute("model"); if (model instanceof Exception) { out.println("<ul class=\"" + errorClass + "\">"); Exception exception = (Exception) model; out.println("<li>"); out.println(exception.getMessage()); out.println("</li>"); out.println("</ul>"); } else if (isValidationErrorList(model)) { out.println("<ul class=\"" + errorClass + "\">"); List<ValidationError> validationErrors = (List<ValidationError>) model; for (ValidationError error : validationErrors) { out.println("<li>"); out.println(error.getMessage()); out.println("</li>"); } out.println("</ul>"); } } private boolean isValidationErrorList(Object model) { if (model instanceof List) { List list = (List) model; if ( ! list.isEmpty()) { Object firstElement = list.get(0); if (firstElement instanceof ValidationError) { return true; } } } return false; } }
<%@ taglib prefix="mytag" uri="http://example.com/myTag" %> <mytag:errors errorClass="error"/>
あんまりカッコよくない実装なので、もっと良い案がありましたら是非コメントください!
例外発生時は別のエラーページに遷移する
ExceptionMapper
実装クラスを作り、Viewable
でエラーページ指定しました。
package com.example.rest.exception.mapper; import org.glassfish.jersey.server.mvc.Viewable; import javax.ws.rs.core.Response; import javax.ws.rs.ext.Provider; @Provider public class ExceptionMapper implements javax.ws.rs.ext.ExceptionMapper<Exception> { @Override public Response toResponse(Exception e) { Viewable viewable = new Viewable("/error/exception", e.getMessage()); return Response.status(Response.Status.BAD_REQUEST) .entity(viewable).build(); } }
// importは省略 @Provider public class MyExceptionMapper implements ExceptionMapper<MyException> { @Override public Response toResponse(MyException e) { Viewable viewable = new Viewable("/error/exception", e.getMessage()); return Response.status(Response.Status.BAD_REQUEST) .entity(viewable).build(); } }
// importは省略 @Provider public class MyRuntimeExceptionMapper implements ExceptionMapper<MyRuntimeException> { @Override public Response toResponse(MyRuntimeException e) { Viewable viewable = new Viewable("/error/exception", e.getMessage()); return Response.status(Response.Status.BAD_REQUEST) .entity(viewable).build(); } }
// importは省略 @Provider public class RuntimeExceptionMapper implements ExceptionMapper<RuntimeException> { @Override public Response toResponse(RuntimeException e) { Viewable viewable = new Viewable("/error/exception", e.getMessage()); return Response.status(Response.Status.BAD_REQUEST) .entity(viewable).build(); } }
で、実行していただくと分かるのですが、IOException
とException
の場合のみ、exception.jspではなく、index.jspに遷移します。
これは、org.glassfish.jersey.server.mvc.internal.ErrorTemplateExceptionMapper
クラスが優先されているようです。
IOException
は自作のExceptionMapper
を作っていないので、当然ErrorTemplateExceptionMapper
クラスが動く形になります。
Exception
は自作のExceptionMapper
を作っていますが、ErrorTemplateExceptionMapper
クラスが優先されるようです。
JAX-RSの仕様やJerseyのドキュメントを読んで確認しましたが、@Priority
で優先度をつけることが出来ないようで、回避のしようが無いっぽいです。
リダイレクト
MVC 1.0だと、コントローラメソッドの戻り値の文字列にredirect:
という接頭辞をつけるだけでリダイレクトになりますが、Jersey MVCにはその機能はありません。
なので、JAX-RS標準の機能を使いましょう。ステータスコードを300番台にして、HTTPレスポンスのlocationヘッダーにリダイレクト先を指定します。
// リダイレクト元 @GET @Path("redirect") public Response redirect(@Context UriInfo uriInfo) throws URISyntaxException { URI location = uriInfo.getBaseUriBuilder() .path(HelloResource.class) .path("redirect2") .build(); return Response.status(Response.Status.FOUND) .location(location).build(); } // リダイレクト先 @GET @Path("redirect2") public Viewable redirect2() { return new Viewable("redirect2.jsp"); }
JAX-RS標準の機能なので、MVC 1.0に移行しても特に修正の必要はないはずです。
Payara以外のAPサーバーでのJersey MVCの利用
ここまでの内容は、Payara(およびGlassFish)、つまりJerseyおよびJersey MVCが内包されたAPサーバー前提で書きました。
Tomcatの場合
pom.xmlでjersey関連の依存性をすべてcompile
にすればできるはずです。
検証ができたら後ほど追記します。
Oracle WebLogic Serverの場合
内包されているJAX-RS実装はJerseyですが、Jersey MVCは内包されていないようです。
ですので、pom.xmlを記述する際は、jersey-mvc-jspのスコープをcompile
にすればOKだと思います。
未検証なので、どなたかWebLogicを使っている方は試してみてください!
WildFly/JBoss/IBM WebSphereの場合
内包されているJAX-RS実装がJerseyではありません(WildFly/JBossはRESTEasy、WebSphereはApache CXF)。
pom.xmlでjersey関連の依存性をすべてcompile
するだけだと、サーバーに内包されている方のJAX-RS実装が動くような気がするので、web.xmlにorg.glassfish.jersey.servlet.ServletContainer
を追加する必要があるかも・・・。
WildFlyで検証しますので、検証ができたら後ほど追記します。
ちなみに、RESTEasyには「HTML Provider」というJersey MVCに似た独自機能が存在します。
機能自体はかなりシンプルですが、WildFly/JBossの場合はこちらを使うのもアリかもしれません。
ただし、こちらはMVC 1.0とは相違点がかなり多いので、その点はご注意ください。
RESTEasyのHTML Providerで遊んでみる - CLOVER
まとめ
- EE 7でアクションベースMVCを使いたい場合は、Jersey MVCを使いましょう!
- Jersey MVCとMVC 1.0の相対パス・絶対パスを理解しよう!
ビューへ値を渡す時は、CDIビーンを使いましょう!Jersey MVCでは素直にViewableコンストラクタに渡しましょう。複数の値を渡す場合は、Mapか大きなDTOを使いましょう。- バリデーションと例外処理は、
@ErrorTemplate
とExceptionMapper
を併用しましょう!
最後に
繰り返しになりますが、Jersey MVCはあくまで独自機能であり、Java EE 7標準の「範囲外」です。
Java EE 7標準の範囲内では、コンポーネントベースのJSFが、唯一のHTMLを返すフレームワークです。
Java EE 7標準にどこまでこだわるか、プロジェクトの事情を考慮して、利用を検討してください。
@Transactional(Spring Framework)のpropagation属性
表題の件を調べていて、ほぼJava EEのJTAと同じかと思いきや、NESTEDというJTAには無いものがあったのでまとめました。
@Transactionalとは?
メソッドに付加すると、メソッドの開始がトランザクションの開始、メソッドの終了がトランザクションの終了になります。
メソッド内で非チェック例外(RuntimeException及びそのサブクラス)が発生した場合はロールバックされます。
チェック例外の場合はロールバックされません。
具体的なコードで説明します。
実際にDBにアクセスするTxTestRepositoryクラス(以下「リポジトリ」)があり、それを呼び出しているTxTestService1(以下「サービス1」)とTxTestService2(以下「サービス2」)があります。
そして、サービス1はサービス2も呼び出しているとします。
@Repository public class TxTestRepository { @Autowired private NamedParameterJdbcTemplate jdbcTemplate; public void insert(int id) { Map<String, Object> params = new HashMap<>(); params.put("id", id); params.put("created_at", new Date()); jdbcTemplate.update("INSERT INTO tx_test VALUES(:id, :created_at)", params); } }
@Service public class TxTestService2 { @Autowired private TxTestRepository repository; @Transactional(propagation = Propagation.REQUIRED) public void insert(int id) { repository.insert(id); } }
@Service public class TxTestService1 { @Autowired private TxTestRepository repository; @Autowired private TxTestService2 service2; @Transactional(propagation = Propagation.REQUIRED) public void insert() { repository.insert(1); service2.insert(2); repository.insert(3); }
サービス1のinsertメソッドを呼び出すと、「1」と「3」はリポジトリ経由で、「2」はサービス2経由でDBに追加されます。
問題は、サービス1からサービス2を呼び出した時に、この2つが同一トランザクションなのか、別のトランザクションなのかです。
それを決めているのが、@Transactionalのpropagation属性です。
REQUIRED
上記のコードでは、サービス1のpropagationはREQUIRED、サービス2もREQUIREDです。
REQUIREDの場合、そのメソッドが呼び出された時にトランザクションが開始されていなければ新規に開始し、すでに開始されていればそのトランザクションをそのまま利用します。
上記のコードの場合、まずサービス1のinsertメソッドが呼ばれたら、トランザクションが開始されます。
その中でサービス2のinsertメソッドを読んだ時に、サービス1のトランザクションがそのまま利用されます。つまり、サービス1とサービス2は同一トランザクションです。
ここで、サービス2の中で、わざと例外をスローしてみます。
propagationはREQUIREDのままです。
@Service public class TxTestService2 { ... @Transactional(propagation = Propagation.REQUIRED) public void insert(int id) { repository.insert(id); throw new RuntimeException("ERROR 2"); } }
サービス1では、service2.insert(2)
をtry-catchで囲みます。
こちらも、propagationはREQUIREDのままです。
@Service public class TxTestService1 { ... @Transactional(propagation = Propagation.REQUIRED) public void insert() { repository.insert(1); // ロールバックされる try { service2.insert(2); // ロールバックされる } catch (Exception e) { e.printStackTrace(); } repository.insert(3); // ロールバックされる } }
こうすると、サービス2が非チェック例外発生→ロールバックしたら、同一トランザクションであるサービス1もロールバックされます。
すなわち、追加される件数は0件です。
例外をサービス1で発生させても同じです。
サービス2では例外が発生していないので、サービス2側はコミットされるかと思いきや、サービス1側で例外が発生してロールバックされるので、同一トランザクションであるサービス2で追加したレコードもロールバックされます。
@Service public class TxTestService2 { ... @Transactional(propagation = Propagation.REQUIRED) public void insert(int id) { repository.insert(id); } }
@Service public class TxTestService1 { ... @Transactional(propagation = Propagation.REQUIRED) public void insert() { repository.insert(1); // ロールバックされる try { service2.insert(2); // ロールバックされる } catch (Exception e) { e.printStackTrace(); } repository.insert(3); // ロールバックされる throw new RuntimeException("ERROR 1"); } }
REQUIRES_NEW
REQUIRES_NEWの場合、そのメソッドが呼び出された時にトランザクションが開始されていようがなかろうが、常に新規のトランザクションを開始します。
サービス2のみをREQUIRES_NEWに変更し、サービス2内で例外をスローしてみます。
@Service public class TxTestService2 { ... @Transactional(propagation = Propagation.REQUIRES_NEW) public void insert(int id) { repository.insert(id); throw new RuntimeException("ERROR 2"); } }
@Service public class TxTestService1 { ... @Transactional(propagation = Propagation.REQUIRED) public void insert() { repository.insert(1); // コミットされる try { service2.insert(2); // これはロールバックされる } catch (Exception e) { e.printStackTrace(); } repository.insert(3); // コミットされる } }
こうすると、サービス1とサービス2は別のトランザクションなので、サービス2で例外発生→ロールバックされても、サービス1には影響がありません。
よって、サービス2で追加した「2」だけロールバックされ、サービス1で追加した「1」「3」はコミットされます。つまり、追加される件数は2件です。
サービス1で例外が発生した場合、サービス1で追加した「1」「3」はロールバックされ、サービス2で追加した「2」はコミットされます。つまり、追加される件数は1件です。
@Service public class TxTestService2 { ... @Transactional(propagation = Propagation.REQUIRES_NEW) public void insert(int id) { repository.insert(id); } }
@Service public class TxTestService1 { ... @Transactional(propagation = Propagation.REQUIRED) public void insert() { repository.insert(1); // ロールバックされる try { service2.insert(2); // これはコミットされる } catch (Exception e) { e.printStackTrace(); } repository.insert(3); // ロールバックされる throw new RuntimeException("ERROR 1"); } }
NESTED
NESTEDの場合、REQUIREDと同様に、そのメソッドが呼び出された時にトランザクションが開始されていなければ新規に開始し、すでに開始されていればそのトランザクションをそのまま利用します。
しかし、「部分的ロールバック」が可能になっており、サービス2で例外発生→ロールバックされても、サービス1はロールバックされません。
部分的ロールバックについてはこちら→http://qiita.com/yuba/items/9b5b86bc3e128a84db5e
内部的には、JDBC 3.0以降の機能であるjavax.sql.Savepoint
を利用しているようです。
Savepointについてはこちら→http://java-reference.sakuraweb.com/java_db_savepoint.html
サービス2のみをNESTEDに変更し、サービス2内で例外をスローしてみます。
@Service public class TxTestService2 { ... @Transactional(propagation = Propagation.NESTED) public void insert(int id) { repository.insert(id); throw new RuntimeException("ERROR 2"); } }
@Service public class TxTestService1 { ... @Transactional(propagation = Propagation.REQUIRED) public void insert() { repository.insert(1); // コミットされる try { service2.insert(2); // これはロールバックされる } catch (Exception e) { e.printStackTrace(); } repository.insert(3); // コミットされる } }
サービス1とサービス2は同一トランザクションですが、サービス2には部分的ロールバックが適用され、サービス1には影響がありません。
よって、サービス2で追加した「2」だけロールバックされ、サービス1で追加した「1」「3」はコミットされます。つまり、追加される件数は2件です。
サービス1で例外が発生した場合、サービス1で追加した「1」「3」はロールバックされるのは当然ですが、あくまで同一トランザクションなので、サービス2で追加した「2」もロールバックされます。つまり、追加される件数は0件です。
ここが、完全に別トランザクションであるREQUIRES_NEWと違うところですね。
@Service public class TxTestService2 { ... @Transactional(propagation = Propagation.NESTED) public void insert(int id) { repository.insert(id); } }
@Service public class TxTestService1 { ... @Transactional(propagation = Propagation.REQUIRED) public void insert() { repository.insert(1); // ロールバックされる try { service2.insert(2); // これもロールバックされる } catch (Exception e) { e.printStackTrace(); } repository.insert(3); // ロールバックされる throw new RuntimeException("ERROR 1"); } }
ちなみに、サービス1とサービス2をNESTEDにした場合でも、追加件数は0件です。
部分的ロールバックというのは、あくまで内部トランザクション(今回はサービス2)がロールバックされた時に適用され、外部トランザクション(今回はサービス1)がロールバックされた場合は、内部トランザクションも一緒にロールバックされます。
NESTEDの注意点
Javadocを読むと、「特定のトランザクションマネージャでのみ動作する」と書いてあります。
DataSourceTransactionManager
の場合は「nestedTransactionAllowed
はデフォルトでtrue」と書かれていますが、
JpaTransactionManager
の場合は「nestedTransactionAllowed
はデフォルトでfalse」と書かれています。
他のトランザクションマネージャの種類によって違うっぽいので注意ですね。
参考資料
Spring Framework Reference
Java EE 7 Tutorial (JTA)
https://docs.oracle.com/javaee/7/tutorial/transactions003.htm
Java EEのチュートリアルなのでNESTEDの説明はありませんが、Table 51-1にその他の属性とトランザクションが新規か否かの関係がまとめられており、分かりやすいです。
Java EEハンズオン事前準備手順(JJUG CCC 2015 Fall) #jjug_ccc #ccc_m1
JJUG CCC 2015 Fallが近づいてきました。
今回、僕は2時間のJava EEハンズオンを担当することになりました。
■脱Struts&独自フレームワーク!今からでも遅くないJava EE Web開発入門ハンズオン!
http://www.java-users.jp/?page_id=2064#M-1
このハンズオンは、StrutsなどのWebフレームワークは経験があるけど、Java EEは初めてという方を対象にしていますので、「Java EEってそもそも何?」という方も歓迎です!
基礎からの講義・解説をしながらハンズオンを進めますし、演習をサポートするチューターも何人か参加しますので、Java EE初心者でも安心のセッションです!
内容は、Java EEのWebフレームワークである「JSF」「JAX-RS」「Jersey MVC」を、実際にご自分のPCでプログラムを作りながら学ぶものになっています。
ハンズオン課題は、DBと連携した基本的なCRUDアプリケーションです。
前半50分がJSF、10分休憩を挟んで、後半50分がJAX-RS + Jersey MVCです。
参加にあたって当日必要なもの
- 事前準備済かつ十分に充電されたご自分のノートPC
事前準備について
ハンズオンに参加していただくにあたって、事前準備をお願いしています。
事前準備にはインターネット接続が必要ですが、会場にはWiFiはありません。また、ハンズオン当日は大人数なので、個人でWiFiルーターを持ち込まれる際も、混雑してつながりにくくなることが多いためです。
インターネット回線の速度やPCスペックにもよりますが、所用時間は30分前後です。
お手数ではありますが、下記の手順に沿ってご準備をお願いします。
- 手順1. 開発環境をインストールする
- 手順2. GitHubから演習用プロジェクトのクローンしてビルドする
事前準備についてご不明な点がありましたら、お気軽にご連絡ください。
ご連絡は、このブログ記事にコメントを書いていただくか、もしくは僕のTwitterアカウント@suke_masaまでよろしくお願いします。
手順1. 開発環境をインストールする
開発環境の概要
- JDK 8u66
- NetBeans 8.1 (8.0.2でも大丈夫です)
- Payara Web ML 4.1.1.154
- curl (MacやLinuxの場合は、OSに組み込まれているのでインストールの必要なし)
データベースは、Payara内包のJavaDBを利用するので、インストールする必要はありません。
データベースのテーブル作成は、JPAのスキーマ生成機能を利用するので、これも事前準備は必要ありません。
ビルドにはMaven、GitHubからのクローンにはGitを使いますが、これらはNetBeansに内包されているので、インストールする必要はありません。
JDK
下記のWebサイトから、お使いのOSに合わせた「JDK 8u66」のインストーラーをダウンロードして、実行してください。
http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html
環境変数JAVA_HOMEおよびPATHを設定してください。
NetBeans
下記のWebサイトから、お使いのOSに合わせたインストーラーをダウンロードして、実行してください。
ダウンロードバンドルは、「Java EE」または「すべて」を選択してください。
https://netbeans.org/downloads/
インストーラーを実行した後は、すべて「次へ」などでOKです。
Payara
Payaraは、英国C2B2社がGlassFish(Java EE参照実装サーバー)をベースに、バグフィックスや機能追加をして開発しているAPサーバーです。
使い方などは、GlassFishと全く変わりません。
下記のWebサイトから、「Payara-Web-ML 4.1.1.154 (Multi-Language Web Profile)」をダウンロードしてください。
http://www.payara.co.uk/payara_multi-language
ダウンロードしたZIPファイルは、適当なフォルダに展開してください。
次に、NetBeansにPayaraを認識させます。
NetBeansを起動したら、画面左の[サービス]タブを開き、[サーバー]を右クリック→[サーバーの追加]を選択します。
[サーバー]で[GlassFish Server]を選択し、[名前]の欄には適当な名前を入力後、[次>]をクリックします。
[インストール場所]の[参照]ボタンをクリックして、先ほどPayaraを展開したフォルダを選択します。僕の場合(下記の画像)ではフォルダ名を変えていますが、展開してそのままでは「payara41」というフォルダ名になっていますので、そのpayara41フォルダを選択してください。選択したら[ローカルドメイン]・[ライセンス契約を読んで同意しました...]を選択して、[次>]をクリックします。
デフォルトで下記の画像のような状態になっていますので、このまま[終了]をクリックします。
サーバーにPayaraが追加されました。
curl
curlは、HTTPリクエストを送るための簡易ツールです。JAX-RSのハンズオンで利用します。
MacやLinuxの場合は、OSに最初から組み込まれていますので、この手順はスキップしてください。
以下、Windows 7を前提に説明します。
下記のWebサイトから、ZIPファイルをダウンロードしてください。
http://curl.haxx.se/latest.cgi?curl=win64-nossl
ダウンロード後、ZIPファイルを適当なフォルダに展開してください。
その展開したフォルダ(curl.exeが入っているフォルダです)のパスを、環境変数PATHに追加してください。
コマンドプロンプトを開いて「curl」コマンドを実行し、下記のように表示されればOKです。
C:\User\User01 > curl curl: try 'curl --help' or 'curl --manual' for more information C:\User\User01 >
2. GitHubから演習用プロジェクトのクローンしてビルドする
クローン
今回、プロジェクトは4つ用意しています。
これらのプロジェクトをクローンして、ビルドするところまで実施してください。
これらはMavenプロジェクトで、初回ビルド時に依存ライブラリのダウンロードが実行されます。
GitHubアカウントの作成
今回のハンズオン用プロジェクトは、すべてGitHubにアップしています。
GitHubからプロジェクトをクローンするためには、ご自身のGitHubアカウントが必要になります。
お持ちでない方は、下記のURLからSign Upでアカウントを作成してください。
NetBeansでGitHubからjjug-logicプロジェクトをクローン
NetBeansを開いたら、画面上部のメニューから[チーム]-[Git]-[クローン]と選択します。
[リポジトリURL]に「https://github.com/MasatoshiTada/jjug-logic.git」と入力します。
[ユーザー]と[パスワード]は、ご自身のGitHubアカウント情報を入力します。
[クローン先]は、クローンしたプロジェクトを保存するローカルのフォルダ名を指定します。
入力したら、[次>]をクリックします。
[master]が選択されていることを確認して、[次>]をクリックします。
デフォルトのまま[終了]をクリックします。
[プロジェクトを開く]をクリックします。
画面上部のメニューから[表示]-[バージョン・ラベルを表示]を選択します。
プロジェクトを右クリックして[依存性でビルド]を選択します。
残り3つのプロジェクトをクローン
プロジェクトが何も選択されていない状態で、画面上部のメニューから[チーム]-[Git]-[クローン]と選択します。(一度、[プロジェクト]ウィンドウ内で右クリックすると、プロジェクトの選択が外れます)
[リポジトリURL]に「https://github.com/MasatoshiTada/jjug-jsf.git」と入力します。
その他の情報は、先ほど入力したものが残っていると思いますので、そのまま[次>]をクリックします。
exerciseとmasterという2つのリモート分岐がありますので、2つとも選択した状態にして[次>]をクリックします。
デフォルトのまま[終了]をクリックします。
[プロジェクトを開く]をクリックします。
プロジェクトを右クリックして、[Git]-[分岐/タグ]-[分岐に切り替え]を選択します。
[origin/exercise]を選択し、[新しい分岐としてチェックアウト]にチェックを入れて[切替え]をクリックします。
プロジェクト名の横に[exercise]という分岐名が表示されていることを確認したら、プロジェクトを右クリックして[依存性でビルド]を選択します。
プロジェクトを右クリックして[プロパティ]を選択すると、下記のようなプロパティのウィンドウが開きます。[実行]を選択し、[サーバー]で今回インストールしたPayara Web ML 4.1.1.154を選択後、[OK]をクリックします。
残り2つのプロジェクトについても、以上の手順を行ってください。
リポジトリURLは下記の通りです。
https://github.com/MasatoshiTada/jjug-jax-rs.git
https://github.com/MasatoshiTada/jjug-jersey-mvc.git
NetBeansでTODOコメントを一覧表示
ハンズオン用のソースコードは、所々を穴埋め形式にして、その箇所はTODOコメントを書いてあります。
NetBeansのTODOコメント一覧表示機能を使うと、ダブルクリックでその場所にジャンプできるので、ハンズオン中はこの機能を使います。
NetBeans画面上部のメニューから[ウィンドウ]-[アクション項目]と選択します。
黄色い四角のマークをクリックすると、現在選択中のプロジェクトのTODOのみが表示されます。
漏斗(フィルター)のマークをクリックして、[編集]を選択します。[タイプ]で[TODO]のみにチェックを入れます。
[説明]を1回クリックすると、TODOコメントが並び替えられて見やすくなります。
スライドのダウンロード
当日は下記のスライドでセッションを進めます。あらかじめダウンロードしてあると、手元でも資料を確認することができます。
www.slideshare.net
何か分からないことがあったら
手順は以上になりますが、何か分からないことがあったら僕までご連絡ください。出来る限りご対応します。
この記事へのコメントかツイッター(@suke_masa)でご連絡ください。
(多分、ツイッターの方が僕が気付くのが早いと思います)
これ以上の予習などは必要ありませんが、もし事前に動作確認などを行いたい場合は、各プロジェクトの分岐を[master]に切替え、[消去してビルド]後に[実行]でデプロイできます。
それでは、皆様のご参加をお待ちしております!
【全部俺】JavaのIDE、どれを使う?【3大IDE頂上決戦】
2015年現在では、おそらく以下の3つのいずれかだと思います。
(ちなみに、僕自身が使った経験がほとんどないので、VimやEmacsなどのエディタは今回のスコープから除外しますm( )m)
僕自身は、社会人になってからJavaを学び始め、新人研修はサクラエディタで受講し、それ以降はしばらく数年はEclipseのみを使っていました。
NetBeans歴は2年くらい、IntelliJ IDEA歴は1年くらいで、どのIDEも普段使いにはほぼ問題ない、というくらいのレベルです。
逆に言えば、どれもまだまだ使いこなせてないんじゃないかとも自分では思っています(^^;
その程度のレベルの僕ですが、現時点でどのような見解を持っているのか、それぞれのIDEの特徴を比較しつつ説明したいと思います。
Eclipse
良い点
- プラグインが豊富
- 様々な開発環境に対応可能
- 日本語化が容易
- 日本語情報も豊富、使っている人が多いのでノウハウが蓄積されている
なにがしかのフレームワークだったりAPサーバーだったりをサポートするプラグインは、三大IDEの中では真っ先に作られる印象です。
うーん?
- 他のIDEと比較すると、補完能力が弱い
- やや動きが重い
あくまで僕の感覚ですが、メモリ使用量はほぼ同じでも、IntelliJの方が軽快に動作する印象です。あまりスペックが高くないPCだと、その傾向が顕著です。
NetBeans
良い点
僕はJava EEをやりだした時はEclipseを使っていて、研修のコンテンツもすべてEclipseで作っていました。しかし、今一つやり辛さを感じていたので、全てNetBeansに置き換えました。もちろん最初は慣れませんでしたが、ものの数日で感覚は掴めました。
うーん?
- プラグインが少ない
- 多様な環境には対応できていない
例えば、Spring Bootをサポートするプラグインは、僕の知る限りありません。また、8.1が出るまでは、WildFly連携も不安定な感じでした。GlassFishなどのOracle製品との相性は良いです。
IntelliJ IDEA
良い点
- 補完が至る所で効く
- 何か書いた後のカーソル位置が「ここ!」というところに来てくれる
- JavaScriptサポート機能も強い
一言で言うと「コーディングがとても快適!」です。この点は、EclipseやNetBeansとは大きな差があります。
うーん?
- ぜんぶ英語(今のところ日本語化は不可能)
- コーディング以外の手順はやや複雑
- 基本的に有償
英語であることを差し引いても、使い方の手順を身に付けるには時間がかかりました。4カ月くらいでようやっと慣れたかな?という感じでした。
あと、三大IDEの中で唯一有償です。無償バージョンもあるのですが、これはAPサーバー連携やらHTML/JavaScriptやらのWeb開発機能が付いていません。
で、結局どれを使う?
いま僕は、これらのIDEを使い分けています。
Eclipseは研修用です。既存の研修コンテンツがEclipseプロジェクトが多いのと、受講者の方が慣れているという理由です。あと、前述の通りNetBeansはWildFly対応が今一つな感じがするので、Java EEでもWildFlyを使う時はEclipseです。
NetBeansは、Maven/GlassFish/JavaDB/JSF/JPAを使いたい時用です。
上記以外ではIntelliJ IDEAです。Gradleを使う時や、Java EEでもJPAを使わない(Domaなど)場合はIntelliJです。あとはSpringの場合もIntelliJです。
まとめ
どのIDEもそれぞれ良いところがあり、でも特定の環境だとやり辛さを感じたりで、まだどれか1つに統一できていないのが、僕の現状です。
ただ、もともとはEclipse一辺倒だった自分自身の反省なのですが、「食わず嫌い」ではもったいないと思います。
上述の通りEclipseにはEclipseの良さがあり、否定するつもりはありません。ただ、NetBeansやIntelliJ IDEAも良いIDEであり、少しずつつまみ食いしながら知っていって、その上で自分の感覚や開発環境などに応じて、ひとりひとりのベストなIDEを選べばいいんじゃないかというのが、僕の意見です!
この記事を読んだ方の「ベストなIDE」選びの一助になれば嬉しいです。
GlassFish/Payaraで@Transactional境界内で発生した例外が失われる現象が治ってた!
普段から僕は、Java EEやるときはGlassFish/Payaraを使っているのですが、1つだけ懸念事項がありました。それが表題の現象です。それがPayara 4.1.1.154(おそらくGlassFish 4.1.1)で治っていた、というお話です。
どんな現象?
Java EEでWeb作るときは、永続化層(JPA)・ビジネスロジック層(CDI/EJB)・プレゼンテーション層(JSF/JAX-RS)というような階層構造にすることが多いと思います。
永続化層
@Dependent public class EmployeeDao { @PersistenceContext(unitName = "empPU") private EntityManager em; @Transactional(TxType.REQUIRED) public Employee findById(Integer id) { // Employee emp = em.find(Employee.class, id); // return emp; throw new RuntimeException("ERROR IN DAO!!!"); } }
ビジネスロジック層
@Dependent public class EmployeeService { @Inject private EmployeeDao dao; @Transactional(TxType.REQUIRED) public Employee findById(Integer id) { Employee emp = dao.findById(id); return emp; } }
プレゼンテーション層
@Path("employees") public class EmployeeResource { @Inject private EmployeeService servicee; @GET @Path("{id}") public Response findById(@PathParam("id") Integer id) { Employee emp = service.findById(id); return Response.ok().entity(emp).build(); } }
永続化層で、わざとRuntimeExceptionをスローしています。
これを実行すると
これで実行してGlassFish/Payaraのログを見ると、javax.transaction.RollbackException
がスローされていると書かれています。RuntimeExceptionのスタックトレースは、表示されません。元の例外が失われてしまうのです。
なぜ、こうなるのか
上妻さんのブログで詳しく書かれています。昨年のGlassFishアドベントカレンダーの記事です。
GlassFish4.1をなおしてみた - 見習いプログラミング日記
簡単に言うと、javax.transaction.RollbackException
クラスにThrowable causeを引数に取るコンストラクタがそもそも定義されていないことや、トランザクションがロールバック確定であるにも関わらずコミットしようとするコードが存在していたことが問題でした。
この問題はとても悩みの種でした。研修中にビジネスロジック以下の層で例外が発生した時にデバッグするのがとても難しかったり、開発現場やシステムの実運用環境でコレが起こったら大変だよなあ・・・と思ったり。
Java EEの大きなメリットの1つは、トランザクション管理をAPサーバーに任せることができるという点だと僕は思っているので、これはどうしたもんかなあと、長い間思っていました。
Payara 4.1.1.154で治ってた!
先日、Payara 4.1.1.154がリリースされました。で、ぼんやりとリリースノートを読んでいたら、「Upstream Fixes」に"GLASSFISH-21172 - javax.transaction.RollbackException from @Transactional bean has no cause set"の文言があるじゃありませんか!
http://www.payara.co.uk/release_notes
そして、リンクを辿るとGlassFishのJIRAへ。
コメント一覧からさらにリンクを辿ると、ソースコードのDIFFが読めます。
https://java.net/projects/glassfish/sources/svn/revision/64135
おお、治ってる!しかも、上妻さんがブログに書かれていた修正案とほぼ、いや、全く同じ!
僕はPayara 4.1.1.154で動作確認しましたが、修正日付を見ると、おそらくGlassFish 4.1.1で修正されていたものと思われます。
で、修正後の挙動は?
これも上妻さんが書かれていた通りなのですが、@Transactional
境界内で発生した例外がそのままスローされます。上記のコードならRuntimeExceptionですね。
まとめ
- GlassFish/Payaraで例外が失われる現象が治っていました!
- 上妻さん凄すぎる!
10分で出来る!初めてのTwitter4J & Twitterアプリ作り方メモ in 2015-10-18
最近はJava EE系でもJAX-RS研修の担当が多くなっている関係上、OAuthやRESTクライアントにも興味が出てきました。ということで、初めてTwitter4Jでプログラムを作ってみました。参考にしたのは、@kikutaro_さんのこちらのブログです。
Twitter4Jを使ったら10分でつぶやきJavaプログラムが作れました! ~NetBeans編~ - Challenge Java EE !
分かりやすくまとまっていて、基本的な手順は今も変わっていないのですが、管理画面のUIが変わっていたので、本日時点での画面キャプチャ付きで解説します。
目標
Twitter4Jを使って、Javaのmain()メソッドからつぶやきを送信する!
Twitter Developer登録
プログラムを作る前に、Twitter Developer登録を行って、アプリ開発に必要なアクセストークンなどの情報を取得します。この手順は、JavaやTwitter4Jによらないはずです。
まず、下記のTwitterのDeveloperサイトにアクセスします。
すると、トップ画面が表示されます。画面右上のリンクから、ご自分のTwitterアカウントでサインインしてください。このアカウントは、普段のつぶやきなどで使っているアカウントでOKです。
トップ画面で下の方にスクロールして、[TOOLS]-[Managing Your App]をクリックします。
すると、アプリケーションの管理画面に移ります。まだ何もアプリケーションを作っていないので、[Create New App]ボタンだけが表示されます。
では、[Create New App]ボタンをクリックします。すると、アプリケーションの作成画面に移ります。適当なアプリケーション名、アプリの説明、自分のブログなどのURLを入力します。[Callback URL]は必須項目ではないので、今回は空欄のままにします。
上記の画面で下の方にスクロールすると、[Developer Agreement]が記載されているので、内容を確認した上で[Yes, I agree]にチェックを入れて、[Create your Twitter app]ボタンをクリックします。
そうすると、下記のような画面が表示されます。[Consumer Key]の部分は、後で使います。実際には英数字&記号の羅列が表示されていますが、秘密の情報のため画像を加工して伏せています。
次に、この画面の[Keys and Access Tokens]タブをクリックします。表示されている[Consumer Key (API Key)]と[Consumer Secret (API Secret)]という2つの文字列は、後で使います。
この画面で下の方にスクロールして、[Create my access token]ボタンをクリックします。
画面が遷移するので、また下の方にスクロールすると、[Access Token]と[Access Token Secret]が表示されています。この2つの文字列も、後で使います。
以上で、準備は完了です。
Twitter4JでJavaプログラムを作る
さて、後は簡単です。IDEはEclipseでもNetBeansでもIntelliJ IDEAでも何でも構いません。ビルドツールは、MavenまたはGradleを使います。
まず、Twitter4Jを依存性に含めます。
Gradleならこうです。
compile 'org.twitter4j:twitter4j-core:4.0.4'
<dependency> <groupId>org.twitter4j</groupId> <artifactId>twitter4j-core</artifactId> <version>4.0.4</version> </dependency>
次に、src/main/resourcesに「twitter4j.properties」というプロパティファイルを作ります。そして、下記のように記述します。
debug=true #右辺にConsumer Key (API Key)をコピーして貼り付ける oauth.consumerKey=XXXXXXXXXXXXXXXX #右辺にConsumer Secret (API Secret))をコピーして貼り付ける oauth.consumerSecret=XXXXXXXXXXXXXXXXXX #右辺にAccess Tokenをコピーして貼り付ける oauth.accessToken=XXXXXXXXXXXXXXXX #右辺にAccess Token Secretをコピーして貼り付ける oauth.accessTokenSecret=XXXXXXXXXXXXXXXX
src/main/javaにクラスを作ります。菊田さんのブログのプログラムそのままです(^^;
import twitter4j.*; public class UserInfoMain { public static void main(String[] args) throws TwitterException { Twitter twitter = TwitterFactory.getSingleton(); User user = twitter.verifyCredentials(); System.out.println(user.getName()); System.out.println(user.getScreenName()); System.out.println(user.getFriendsCount()); System.out.println(user.getFollowersCount()); Status status = twitter.updateStatus("Twitter4Jから初めてのツイート! #twitter4j"); } }
これだけです。いざ、実行します。すると・・・
多田真敏 suke_masa 63 199 . . . (他にもデバッグログがいっぱい表示されます)
おおお、自分のユーザー情報が表示されて、つぶやきが投稿されてる!
まとめ
- Developer登録をして、アクセストークンなどの情報を取得する
- アクセストークンなどは、コピーしてtwitter4j.propertiesに貼り付ける
- Twitter4J使えば、REST APIやらJSONやら認証やらのコードは書かなくてOK!
感想
あまりに簡単すぎて感動すら覚えました・・・。
Twitter4Jのソースも、これから読んでみたいと思います!
Payara(GlassFish)でMariaDBのJDBC接続プールが作成できない
表題の通りです。
環境
- MacBook Air (OS X El Capitan)
- MariaDB 10.1.x
- Payara Web 4.1.153
- JDK 1.8.0_60
試していませんが、おそらくWindowsやGlassFishでも同じ現象が起こると思います。
現象
- MariaDBのJDBCドライバのJARを「<PAYARA_HOME>/glassfish/lib」や「<PAYARA_HOME>/glassfish/domains/domain1/lib」に置く
- Payaraを起動してlocalhost:4848にアクセスし、管理コンソールからURL/USER/PASSWORDを設定する
という一般的な手順で作成しても、MariaDBへのpingが通りません。
ping時のPayara管理コンソール(ブラウザ上)でのエラーメッセージは、こんなのが出ます。
Access denied for user ‘’@’localhost’ (using password: NO)
どうやら、ユーザー名やパスワード設定が間違っている時に出てくるMariaDBのエラーメッセージのようなのですが、全く同じURL/USER/PASSWORDでメインメソッドから普通にJDBC接続したら出来ました。 (URL/USER/PASSWORDはPayara管理コンソールからコピペしたので間違いないはずです)
Payaraのプロパティ名も間違えていません。
しかも、ごくたまにpingが通ります。しかし、またすぐにpingが通らなくなります。
対策
今回は、MariaDBをPayaraで使うこと自体が目的ではないので、MySQL 5.6に変更しました。そうすると無事にJDBC接続プールが作成できました。
原因は分かりませんが、Payara管理コンソールで選択するときのDBの種類にはMariaDBは入っていないし、Payara(というか恐らくGlassFish)はMariaDBに対応していないのかもしれません。 (完全に僕の憶測です)
注意
この問題は、あくまでPayara+MariaDBの組み合わせによるものです。