Glassfish4.0.1でJersey MVCのvalidationを試してみた
動機
このあたり便利そうなので試してみました
https://jersey.java.net/documentation/2.5.1/mvc.html#d0e12836
最終的にはバリデーションエラーの際に戻った画面上で入力フィールドに値を残しておきつつ、エラーのあったフィールドにエラーメッセージを出したい、という目標があります。
ただ、JAX-RSの目的からしてそういうのはあまり考えられてなさそうで、サーバ側ではRESTのインタフェースを用意しておいて、クライアントはAjaxでリクエストして結果が返ってきたらその情報をもとに適切な処理(成功の場合は画面遷移や部分更新、失敗したらエラーの表示とか)を行うというのが使いやすい方法なのかなと思ってます。
そんな中、Jersey MVCでは2.3から@ErrorTemplateアノテーションが追加されてバリデーションエラー時(実際は「any exception thrown after the resource matching phase」)に表示するテンプレートファイルを指定できるようになってました。
今回はこれを試してみました。
設定
Glassfish4.0.1 build4を使用します。
参考
pom.xmlに依存関係を追加します。
今回はテンプレートエンジンとしてjspではなくfreemarkerを使いました。
<dependency> <groupId>org.glassfish.jersey.ext</groupId> <artifactId>jersey-mvc</artifactId> <version>2.5.1</version> <scope>provided</scope> <exclusions> <exclusion> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.glassfish.jersey.ext</groupId> <artifactId>jersey-mvc-freemarker</artifactId> <version>2.5.1</version> </dependency> <dependency> <groupId>org.glassfish.jersey.ext</groupId> <artifactId>jersey-mvc-bean-validation</artifactId> <version>2.5.1</version> </dependency>
リソースの作成
@Path("mvc") public class MvcAction { private static final String TEMPLATE_NAME = "/demo/mvc.ftl"; @GET @Template(name = TEMPLATE_NAME) public String newPage() { return "new"; } @POST @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Template(name = TEMPLATE_NAME) @ErrorTemplate(name = TEMPLATE_NAME) @Valid public String create(@NotEmpty @FormParam("code") String code, @NotEmpty @FormParam("name") String name) { return "create"; } }
ApplicationPathを設定します
@ApplicationPath("demo") public class DemoConfig extends ResourceConfig { public DemoConfig() { register(FreemarkerMvcFeature.class); property(FreemarkerMvcFeature.TEMPLATES_BASE_PATH, "/freemarker"); register(MvcBeanValidationFeature.class); packages(MvcAction.class.getPackage().getName()); } }
テンプレートファイルを作成します(/src/main/webapp/freemarker/demo/mvc.ftl)
<!DOCTYPE html> <html> <body> <#if model?? && model?is_sequence> <#list model as error> <div>${error_index}:${error.message} ${error.invalidValue} ${error.path}</div> </#list> <#elseif model?? && model?is_string> <span>${model!''}</span> </#if> <form method="post" action="/dbdos/demo/mvc"> <input type="text" name="code"/> <input type="text" name="name"/> <input type="submit" value="create"> </form> </body> </html>
これでhttp://localhost:8080/コンテキストパス/demo/mvcに接続すると文字列「new」とフォームが表示され、値を入力してボタンを押すと「create」と表示されるはずです。また、値をからのままボタンを押すとエラーが表示されるはずです。
実行
試してみます。
フォームの表示と、値を入れたときの動きはOKでした。
値を入れずにボタンを押すとエラーになりました。
org.jboss.weld.exceptions.UnsatisfiedResolutionException: WELD-001308 Unable to resolve any beans for Types: [interface org.glassfish.jersey.server.ExtendedUriInfo]; Bindings: [QualifierInstance{annotationClass=interface javax.enterprise.inject.Default, values={}, hashCode=-1405755808}] at org.jboss.weld.manager.BeanManagerImpl.getBean(BeanManagerImpl.java:820) at org.jboss.weld.bean.builtin.InstanceImpl.get(InstanceImpl.java:78) at org.glassfish.jersey.server.mvc.spi.AbstractErrorTemplateMapper.getErrorTemplate(AbstractErrorTemplateMapper.java:87) at org.glassfish.jersey.server.mvc.spi.AbstractErrorTemplateMapper.isMappable(AbstractErrorTemplateMapper.java:78)
試しに同じ様なコードでTomcat7でも試してみましたがうまくいったようなのでGlassfish出の問題のようです。
このあたりのCDIの挙動についてはよく分からないのですが、ここで使っているExceptionMapper(ValidationErrorTemplateExceptionMapperのsuperクラス)のuriInfoProviderフィールドへのインジェクションがうまくいってないようです。
エラー時はorg.jboss.weeld.bean.builtin.InstanceImpl
正常時はorg.jvnet.hk2.internal.IterableProviderImpl
がインジェクションされているところまでは分かりました。
それ以上はちょっと分かりませんでした。
解決策
正しい方法か分かりませんが、下記のようにして動くようにはなりました。
まず、ExceptionMapperをValidationErrorTemplateExceptionMapperを参考に作ります。
変えているのは@Singletonアノテーションを削除して@Providerと@Priorityアノテーションを追加している点だけです
@Provider @Priority(Priorities.USER) public class ConstraintViolationExceptionMapper extends AbstractErrorTemplateMapper<ConstraintViolationException> { @Context HttpServletRequest request; @Override protected Response.Status getErrorStatus(final ConstraintViolationException cve) { return ValidationHelper.getResponseStatus(cve); } @Override protected Object getErrorModel(final ConstraintViolationException cve) { return ValidationHelper.constraintViolationToValidationErrors(cve); } }
DemoConfigを書き換えます
public DemoConfig() { register(FreemarkerMvcFeature.class); property(FreemarkerMvcFeature.TEMPLATES_BASE_PATH, "/freemarker"); register(ValidationFeature.class); register(ConstraintViolationExceptionMapper.class); packages(MvcAction.class.getPackage().getName()); }
これで、画面に次のように表示されるようになりました。
0:may not be empty MvcAction.create.arg1 1:may not be empty MvcAction.create.arg0
なお、この変更により
jersey-mvc-bean-validationは必要なくてjersey-bean-validationで十分です。
pom.xmlを書き換えます(変えなくても支障ないはず)。
ただ、これだとarg0とかarg1くらいしか識別する手立てがないのでこのままだと適切にエラー表示させるのは難しそう。
Glassfish4.0.1+jerseyでCDIエラー
最初に
まず、jersey-mvc-freemarkerを使うために次のようにpom.xmlを書きます。
jersey-mvcは既にGlassfishに含まれているためprovidedにします。
jersey-mvcはservlet-api2.4に依存しているんですが関係あるのはjspを使うときのようなので
https://jersey.java.net/documentation/latest/mvc.html#d0e13760
もともと含まれてる3系を使うように依存関係から外してます(ホントはダメかもしれないです)
jerseyの各ライブラリのバージョンはGlassfish4.0.1build4にあわせて2.5.1にしています。
<dependency> <groupId>org.glassfish.jersey.ext</groupId> <artifactId>jersey-mvc</artifactId> <version>2.5.1</version> <scope>provided</scope> <exclusions> <exclusion> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.glassfish.jersey.ext</groupId> <artifactId>jersey-mvc-freemarker</artifactId> <version>2.5.1</version> </dependency> <dependency> <groupId>javax</groupId> <artifactId>javaee-web-api</artifactId> <version>7.0</version> <scope>provided</scope> </dependency>
ここまで起動はうまくいきます。
CDIエラー
続いて、jsonを使用するためのライブラリとしてjacksonを追加します。
<dependency> <groupId>org.glassfish.jersey.media</groupId> <artifactId>jersey-media-json-jackson</artifactId> <version>2.5.1</version> </dependency>
すると、起動時にこのようなエラーが出るようになります。
org.glassfish.deployment.common.DeploymentException: CDI deployment failure:WELD-001408 Unsatisfied dependencies for type [Set] with qualifiers [@Default] at injection point [[BackedAnnotatedParameter] Parameter 1 of [BackedAnnotatedConstructor] @Inject com.google.common.util.concurrent.ServiceManager(Set )] at org.glassfish.weld.WeldDeployer.event(WeldDeployer.java:225) at org.glassfish.kernel.event.EventsImpl.send(EventsImpl.java:131) at org.glassfish.internal.data.ApplicationInfo.load(ApplicationInfo.java:328)
この事象は
guavaのバージョンを15にあげれば解決するようです。
詳細はこちら
ただ、guava15をpomに追加してもこんなエラーが出てしまいます。
重大: Exception during lifecycle processing org.glassfish.deployment.common.DeploymentException: CDI deployment failure:WELD-001408 Unsatisfied dependencies for type [IterableProvider>] with qualifiers [@Default] at injection point [[BackedAnnotatedParameter] Parameter 2 of [BackedAnnotatedConstructor] @Inject org.glassfish.jersey.internal.inject.JerseyClassAnalyzer(@Named ClassAnalyzer, IterableProvider >)] at org.glassfish.weld.WeldDeployer.event(WeldDeployer.java:225) at org.glassfish.kernel.event.EventsImpl.send(EventsImpl.java:131)
ここ、結構はまったんですが、結局はjersey-commonもprovidedにすればいいだけでした。
なぜはまったかというと、jersey-mvcをprovidedにした時点でそこからさらに依存しているライブラリもprovidedになってると思い込んでました。
これは基本的にはそのとおりなんですが、他にも依存関係が定義されているライブラリをpomに追加した場合、providedなライブラリを経由しないとscopeがcompileとなってしまうようです。
<dependency> <groupId>org.glassfish.jersey.core</groupId> <artifactId>jersey-common</artifactId> <version>2.5.1</version> <scope>provided</scope> </dependency>
同様にjersey-bean-validationを追加したとき、jersey-serverがcompileとなり下記のようなエラーとなるので、providedで設定する必要があります。
重大: Exception during lifecycle processing org.glassfish.deployment.common.DeploymentException: CDI deployment failure:WELD-001408 Unsatisfied dependencies for type [ContainerRequest] with qualifiers [@Default] at injection point [[BackedAnnotatedParameter] Parameter 1 of [BackedAnnotatedConstructor] @Inject org.glassfish.jersey.server.internal.routing.UriRoutingContext(ContainerRequest, ProcessingProviders)] at org.glassfish.weld.WeldDeployer.event(WeldDeployer.java:225) at org.glassfish.kernel.event.EventsImpl.send(EventsImpl.java:131)
guavaのエラーについて
ここ(再掲)にも書いてあるんですが、「バージョン14をWEB-INF/libに含めてデプロイすると」なので、そもそもprovidedであれば問題ないような気がします。
上のようにjersey-commonやjersey-serverをprovidedとすればとりあえずはguavaもprovidedとなって正常に起動しますし、明示的にguavaをprovidedとしてもいいと思います。
まとめ
結局、jersey使うときはjersey-commonやjersey-serverのようなGlassfishにバンドルされているライブラリはprovidedで設定しておいたほうがいいということだと思います。
Glassfish4のJerseyのFilterでセッション情報を取得できなかった件
これ、リソースクラス側でセッションに格納したログイン情報をfilterで見て制御するというのをやろうと試してあきらめてたんですが、単なるバグだったっぽいです。
https://java.net/jira/browse/JERSEY-1960
4.0.1 build4では直ってます。
こういう致命的なのがあるってGF4はまだまだリスキー。
JAX-RSの仕様書見てもやっぱりインジェクションできる、が正解っぽいです。
http://download.oracle.com/otndocs/jcp/jaxrs-2_0-fr-eval-spec/index.html
Chapter 10 Environmentのこのあたり
The following sections describe the additional container-managed resources available to a JAXRS root resource class or provider deployed in a variety of environments.
そのおかげというか、SecurityContextの仕組みも結構便利だなと感じることは出来ました。
簡単なAPIくらいならこれで十分かもしれませんがちょっと複雑なシステムになると厳しそうな気はしました。
あと、ロールとかユーザーの設定をxmlに書くのはやっぱりつらい。
Glassfish4でlambda式を使ってみた、の続き
前のメモが中途半端な感じで終わってしまったので、もうちょっと試してみました。
Glassfish4.0.1 buid4つかってラムダ式を書いて実行すると
起動時にこういうエラーが出たけど、なんか動いているっぽい。という話。
重大: Exception while visiting WEB-INF/classes/demo/DemoAction.class of size 2189 java.lang.ArrayIndexOutOfBoundsException: 52264 at org.objectweb.asm.ClassReader.readClass(ClassReader.java:2015) at org.objectweb.asm.ClassReader.accept(ClassReader.java:469) at org.objectweb.asm.ClassReader.accept(ClassReader.java:425) at org.glassfish.hk2.classmodel.reflect.Parser$5.on(Parser.java:359) at com.sun.enterprise.v3.server.ReadableArchiveScannerAdapter.handleEntry(ReadableArchiveScannerAdapter.java:165) at com.sun.enterprise.v3.server.ReadableArchiveScannerAdapter.onSelectedEntries(ReadableArchiveScannerAdapter.java:127) at org.glassfish.hk2.classmodel.reflect.Parser.doJob(Parser.java:344) at org.glassfish.hk2.classmodel.reflect.Parser.access$300(Parser.java:67) at org.glassfish.hk2.classmodel.reflect.Parser$3.call(Parser.java:303) at org.glassfish.hk2.classmodel.reflect.Parser$3.call(Parser.java:292) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) at java.lang.Thread.run(Thread.java:744)
で、ここからが本題。
このhk2というのはDIフレームワークです。
https://hk2.java.net/
なのでDIでこけているというのが推測できます。
でASMというのはJavaバイトコードの操作や分析を行うフレームワーク。
http://asm.ow2.org/
ちゃんと見てないので間違ってるかもしれませんが、DIするときにそのままコンパイルするのではなく前後処理等を追加した形でバイトコードを生成してる処理(@ASM)でラムダ式が対応できてない、って感じのエラーな気がします(うまく説明できない、、、)。
なので、このコードと一緒にCDIでインジェクションを使おうとすると実行時にもこけました。(DIされずにヌルポになる)
@PersistenceContext(unitName = "demo") EntityManager em; @javax.ws.rs.GET @javax.ws.rs.Produces("text/plain") public String demo() { List<Customer> customers = em.createQuery("select s from customer s", Customer.class).getResultList(); long count = customers.stream().filter(customer -> customer.getId() > 3).collect(java.util.stream.Collectors.counting()); return "this is demo! count=" + count; }
このあたりはGlassfishのチケットとしてもあがっているようで、修正されるのは時間の問題の気がします。
https://java.net/jira/browse/GLASSFISH-20972
どうしてもラムダ式で書きたいという時は、DIの対象外クラスに定義すればいいみたいです。
今回はユーティリティクラスっぽいクラスを作ってstaticメソッド内にラムダ式を書くようにしてそのクラスに@Vetoedアノテーションをつけることでエラーを解消できました。
まあ、実際はこれだけのためにこんな風にクラスを分けることはしないと思いますが。
http://yoshio3.com/2014/01/29/cdi-1-1-vetoed-annotation/
@javax.enterprise.inject.Vetoed public class CustomerUtils { public static long filterCount(java.util.List<Customer> customers, int base) { return customers.stream().filter(customer -> customer.getId() > base).collect(java.util.stream.Collectors.counting()); } }
public String demo() { List<Customer> customers = em.createQuery("select s from customer s", Customer.class).getResultList(); long count = CustomerUtils.filterCount(customers, 3); return "this is demo! count=" + count; }
正直、まだこの辺の動きがよく分からないんですが、
http://d.hatena.ne.jp/gloryof/20131205/1386170095
@Vetoedアノテーションを使わない場合、
beans.xmlのbean-discovery-modeが"all"でも"annotated"でもエラーが発生してて直感的には"annotated"ではエラーにならないでほしかった。
ここはscan.excludeで除外対象の設定をすべきようです。(1個上の寺田さんのブログへのリンク参照)
以上。
Glassfish4でlambda式を使ってみよう、としたが・・・
先日Java SE8がついにリリースされました。
早速ラムダ式試してみました。
慣れるまでは大変そうですが、記述が簡単になるのがいいですね。
さて、今回は昨年リリースされたGlassfish4でJava8のラムダ式が使えるかどうかを試してみました。
準備
まず、Java SE8とNetBeans8をダウンロード。今回は別々にダウンロードしました。
http://www.oracle.com/technetwork/java/javase/downloads/index.html
https://netbeans.org/downloads/
Glassfishは4.0.1 build4をダウンロード
ここから4.0.1_b4のzipをダウンロードして解凍
今回はweb-profileにしました
参考
で、それぞれインストール
プロジェクト作成
NetBeans上でプロジェクトを
Maven->Webアプリケーションからプロジェクト名等は適当に入力して
サーバは上でインストールしたGlassfish4、Java EEバージョンにJava EE 7 Webを指定して新規作成。
pom.xmlのmaven-compiler-pluginの設定で1.7->1.8に変更
<groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.1</version> <configuration> <source>1.8</source> <target>1.8</target> 以下略
プロジェクトのプロパティのビルド->コンパイルもJDK1.8になっていることを確認
JAX-RSの動作確認
今回はjersey(JAX-RS)を使って試したので
pom.xmlに依存性追加
<dependency> <groupId>org.glassfish.jersey.ext</groupId> <artifactId>jersey-mvc</artifactId> <version>2.5.1</version> <scope>provided</scope> </dependency>
リソースクラス
@javax.ws.rs.Path("demo") public class DemoAction { @javax.ws.rs.GET @javax.ws.rs.Produces("text/plain") public String demo() { return "this is demo!"; } }
コンフィグクラス
@javax.ws.rs.ApplicationPath("d") public class DemoConfig extends org.glassfish.jersey.server.ResourceConfig { public DemoConfig() { register(DemoAction.class); } }
これで
http://localhost:8080/[プロジェクト名]/d/demo
を開くと「this is demo!」と表示されるはず
ここまででJAX-RSの動作確認はひとまずOK
Java8 Stream APIの確認
ラムダ式の確認の前にJava8自体の動作確認
DemoActionを次のように書き換えます
public String demo() { java.util.List<Integer> list = java.util.Arrays.asList(1, 2, 3, 4, 5); long count = list.stream().collect(java.util.stream.Collectors.counting()); return "this is demo! count=" + count; }
http://localhost:8080/[プロジェクト名]/d/demo
を開くと「this is demo! count=5」と表示されるはず
ラムダ式の動作確認
本題です
DemoActionを次のように書き換えます
public String demo() { java.util.List<Integer> list = java.util.Arrays.asList(1, 2, 3, 4, 5); long count = list.stream().filter(i -> i < 2).collect(java.util.stream.Collectors.counting()); return "this is demo! count=" + count; }
起動時に次のようなエラーが出ました
重大: Exception while visiting WEB-INF/classes/demo/DemoAction.class of size 2189 java.lang.ArrayIndexOutOfBoundsException: 52264 at org.objectweb.asm.ClassReader.readClass(ClassReader.java:2015) at org.objectweb.asm.ClassReader.accept(ClassReader.java:469) at org.objectweb.asm.ClassReader.accept(ClassReader.java:425) at org.glassfish.hk2.classmodel.reflect.Parser$5.on(Parser.java:359) at com.sun.enterprise.v3.server.ReadableArchiveScannerAdapter.handleEntry(ReadableArchiveScannerAdapter.java:165) at com.sun.enterprise.v3.server.ReadableArchiveScannerAdapter.onSelectedEntries(ReadableArchiveScannerAdapter.java:127) at org.glassfish.hk2.classmodel.reflect.Parser.doJob(Parser.java:344) at org.glassfish.hk2.classmodel.reflect.Parser.access$300(Parser.java:67) at org.glassfish.hk2.classmodel.reflect.Parser$3.call(Parser.java:303) at org.glassfish.hk2.classmodel.reflect.Parser$3.call(Parser.java:292) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) at java.lang.Thread.run(Thread.java:744)
ただし、
http://localhost:8080/[プロジェクト名]/d/demo
を開くと「this is demo! count=1」と表示されるのでなんとか動いているようです
Javaの拡張for文のなかで簡単にindexを扱いたい
ものすごくシンプルな例としてList
普通にfor文で
for (int i = 0; i < strs.size(); i++) { System.out.println(i + ":" + strs.get(i)); }
ってやるのが普通なんですが、一般的に拡張for文の方が見やすいということで
int i = 0; for (String str : strs) { System.out.println(i + ":" + str); i++; }
みたいにやります。
ただ、どっちにしろ変数宣言とかインクリメントとか定型的な処理を書く必要があって、ちょっといけてない気がします。
なので、こんな風に書けないかと思って試してみました。(命名のセンスは置いておいて)
for (Elem<String> elem : ExtFor.loop(strs)) { System.out.println(elem.index() + ":" + elem.value()); }
これだと結構すっきりします。
他にはjQueryのeach()みたいな書き方も試してみました。
For.each(strs, new For.Function<String>() { public void execute(int index, String obj) { System.out.println(index + ":" + obj); } });
ちょっと長くなってしまいますね、今のJavaはこういうのはまだ苦手なんでしょう。
ExtForを使った3つ目の例は結構使えそうな気がしました。
最後に、
上の例で使ったクラス定義を以下に残します。
public class ExtFor { public static <T> Iterable<ExtFor.Elem<T>> loop(final Iterable<T> list) { return new Iterable<ExtFor.Elem<T>>() { @Override public Iterator<ExtFor.Elem<T>> iterator() { return ExtFor.iterator(list); } }; } private static <T> Iterator<ExtFor.Elem<T>> iterator(final Iterable<T> list) { return new Iterator<ExtFor.Elem<T>>() { private int counter = 0; private Iterator<T> iterator = list.iterator(); @Override public boolean hasNext() { return iterator.hasNext(); } @Override public Elem<T> next() { Elem<T> elem = new Elem<T>(counter++, iterator.next()); return elem; } @Override public void remove() { throw new RuntimeException(); } }; } public static class Elem<T> { private final int index; private final T value; private Elem(int index, T value) { this.index = index; this.value = value; } public int index() { return this.index; } public T value() { return this.value; } } }
public class For { public static <T> void each(List<T> list, Function<T> function) { int length = list.size(); for (int index = 0; index < length; index++) { function.execute(index, list.get(index)); } } public static interface Function<T> { public void execute(int index, T obj); } }
JavaでPostgreSQLにCSVデータを取込むときはCopyManagerが速くて便利
無知は怖いという話です。
今まではORMフレームワーク使って1件ずつinsertしたり、ちょっと早くしたい場合はINSERT文を構築して100件ずつとかまとめてinsertしたりしてました。
今回、ちょっと調べてみるとPostgreSQLではCOPYコマンドというのがあって、jdbcライブラリにこれに対応したAPIがあるというのも知りました。
それがCopyManagerです。
使い方はこんな感じ
CopyManager copyManager = new CopyManager(connection); Reader reader = new InputStreamReader(new FileInputStream(file), "MS932"); String sql = "COPY table_name (column1, column2,,,) FROM STDIN (FORMAT csv, HEADER)"; long result= copyManager.copyIn(sql, reader);
COPYコマンド自体のオプション等の説明はこちらを参照してください。
http://www.postgresql.jp/document/9.2/html/sql-copy.html
上の例ではCSV形式でヘッダ行ありのファイルを取込むようにしています。
Readerの生成は英数字だけとかの場合はFileReaderでも事足りるのかなと思います。(今回はSJISのファイルを扱ってます)
InputStreamでもOKです。
http://jdbc.postgresql.org/documentation/publicapi/org/postgresql/copy/CopyManager.html
CopyManagerのコンストラクタに渡しているconnectionはorg.postgresql.core.BaseConnectionを実装したインスタンスです。
特に深いことを考える必要がないなら
BaseConnection connection = new Jdbc4Connection(host, port, user, database, info, url);
とかでいいと思います。
フレームワーク等で管理しているコネクションプールを使いたい場合はどこから取得すればいいか探す必要があると思います。