Java, Doma, JAX-RS, GlassFish, Thymeleaf |
やっぱりこの人やってるよねー / JAX-RSとThymeleafの組み合わせをためしてみた - 裏紙 (id:backpaper0 / @backpaper0) URL
@bufferings 結局それ以降はやってないけどな!
2012-12-30 17:29:42 via YoruFukurou to @bufferings
@backpaper0 明日あたり Doma も組み合わせて頼むわ!
2012-12-30 17:30:20 via web to @backpaper0
@bufferings おk!
2012-12-30 17:30:30 via YoruFukurou to @bufferings
というわけでやってみました。
とは言え、Thymeleaf + JAX-RSとJAX-RS + Domaをやれば良いのでそれぞれ見ていきます。
以前やったときはJAX-RSのリソースメソッドでテンプレートのパスとバインドするパラメータをまとめたオブジェクトを返してました。
例えば、こんな感じです。
@GET
public ThymeleafTemplate sayHello(@QueryParam("yourName") String yourName) {
Map<String, Object> variables = new HashMap<>();
variables.put("message", "Hello, " + yourName + "!");
return new ThymeleafTemplate("hello.html", variables);
}
ThymeleafTemplateというぁゃιぃクラスをnewしてたり、テンプレートにバインドする変数名がマップのキーなので文字列で設定してたり、あんまり嬉しくないなー、とか思ったり。
POJOを返すようにしておけばJacksonかましてJSONを返すように簡単に出来たりテストやりやすかったりすると思うので、今回は@Templateというアノテーションを作ってそれでテンプレートのパスを設定するようにしてみました。
リソースメソッドはこんな感じになりました。
@GET
@Template("hello.html")
public HelloResponse sayHello(@QueryParam("yourName") String yourName) {
HelloResponse response = new HelloResponse();
response.setMessage("Hello, " + yourName + "!");
return response;
}
MessageBodyWriter#writeToではリフレクション使ってプロパティ名をキーにしてマップに突っ込んでからテンプレートに渡しています。リソースメソッドの戻り値がStringや基本型のラッパー、コレクションの場合は固定でvalueというキーでマップに突っ込むなどすると上記サンプルコードのように余計なクラスを作らなくてすむので良いかもしれませんね。というかどっちかっつーとMessageBodyWriterの実装の方が大事なのにコードを載せていないという。詳しくはThymeleafMessageBodyWriterを見てくださいませ。
シンプルに普通にDaoImplをnewしても良いかもですね。
@Path("hello") public class Hello { private MessageDao messageDao = new MessageDaoImpl(); ... }
Configはこんな感じ。DataSourceはJNDIルックアップ。
public class DomaConfig extends DomaAbstractConfig { private static Dialect dialect = new StandardDialect(); @Override public DataSource getDataSource() { try { return InitialContext.doLookup("jdbc/__default"); } catch (NamingException ex) { throw new RuntimeException(ex); } } @Override public Dialect getDialect() { return dialect; } }
ただこの方法だとDaoをモックにすげ替えて雲丹っとテスト回したりするのがちょい面倒です。Java EE 6ならCDIで接着する方が嬉しそうですね。
DataSource、Dialectのプロデューサーを作成します。DataSourceのプロデューサーはこんな感じ。
package sample; import javax.annotation.Resource; import javax.enterprise.inject.Produces; import javax.sql.DataSource; public class DataSourceProducer { @Resource(name = "jdbc/__default") @Produces DataSource dataSource; }
適当にクラス作ってフィールド定義して@Producesで注釈するだけです。JAX-RSのアノテーションにも@Produces(javax.ws.rs.Produces)があるけどそっちじゃないのでパッケージ名に注意です。
次に、Config実装クラスはJNDIルックアップしていましたが、フィールドを定義して@Injectでインジェクトします。
package sample; import javax.inject.Inject; import javax.sql.DataSource; import org.seasar.doma.jdbc.DomaAbstractConfig; import org.seasar.doma.jdbc.dialect.Dialect; public class DomaConfig extends DomaAbstractConfig { @Inject private DataSource dataSource; @Inject private Dialect dialect; @Override public DataSource getDataSource() { return dataSource; } @Override public Dialect getDialect() { return dialect; } }
DAOクラスは@DaoでConfig実装クラスを指定するのではなくコンストラクタインジェクションされるようにします。コンストラクタインジェクションするにはコンストラクタを@Injectで注釈する必要があります。DomaではDao実装クラスへの注釈を@AnnotateWithで設定することができます。Doma素晴らしいですね。
package sample; import javax.inject.Inject; import org.seasar.doma.AnnotateWith; import org.seasar.doma.Annotation; import org.seasar.doma.AnnotationTarget; import org.seasar.doma.Dao; import org.seasar.doma.Select; @Dao @AnnotateWith(annotations = { @Annotation(type = Inject.class, target = AnnotationTarget.CONSTRUCTOR) }) public interface MessageDao { @Select Message select(Long id); }
あとはリソースクラスでDaoを@Injectでインジェクトすればおk。
package sample; import javax.ejb.LocalBean; import javax.ejb.Stateless; import javax.inject.Inject; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.QueryParam; @Path("hello") @LocalBean @Stateless public class Hello { @Inject private MessageDao messageDao; ... }
@Statelessで注釈してSession Beanにするとリソースメソッドがトランザクション境界になってSAStrutsにお世話になってたひと(つまり私)は幸せです。あとno-interfaceなので@LocalBeanで注釈しています。
大晦日に何やってんねん。
GlassFishにはデフォでもJDBCレルムとかLDAPレルムとかありますけど、オレオレ認証したいんじゃー、ってときにはカスタムレルムですよ!
FQDNはcom.sun.appserv.security.AppservPasswordLoginModuleです。extendsしてauthenticateUser()メソッドを実装しましょう。例えばこんな感じ。
package example; import com.sun.appserv.security.AppservPasswordLoginModule; import javax.security.auth.login.LoginException; public class ExampleLoginModule extends AppservPasswordLoginModule { @Override protected void authenticateUser() throws LoginException { ExampleRealm realm = (ExampleRealm) getCurrentRealm(); String[] groups = realm.authenticate(_username, _passwd); if (groups == null) { throw new LoginException("ログイン失敗><"); } commitUserAuthentication(groups); } }
ユーザ名やパスワードはAppservPasswordLoginModuleに定義されているprotectedフィールドから取得できます。realmに処理を投げて、戻りがnullなら認証失敗とみなします。認証に成功したらcommitUserAuthentication(groups)を忘れず呼びましょー。
FQDNはcom.sun.appserv.security.AppservRealmです。extendsしてgetAuthType()メソッドとgetGroupNames(String)メソッドを実装します。こんな感じ。
package example; import com.sun.appserv.security.AppservRealm; import com.sun.enterprise.security.auth.realm.BadRealmException; import com.sun.enterprise.security.auth.realm.InvalidOperationException; import com.sun.enterprise.security.auth.realm.NoSuchRealmException; import com.sun.enterprise.security.auth.realm.NoSuchUserException; import java.util.Arrays; import java.util.Collections; import java.util.Enumeration; import java.util.Properties; public class ExampleRealm extends AppservRealm { @Override protected void init(Properties props) throws BadRealmException, NoSuchRealmException { String jaasContext = props.getProperty(JAAS_CONTEXT_PARAM); setProperty(JAAS_CONTEXT_PARAM, jaasContext); } @Override public String getAuthType() { return "example"; } @Override public Enumeration getGroupNames(String username) throws InvalidOperationException, NoSuchUserException { return Collections.enumeration(Arrays.asList(findGroups(username))); } String[] authenticate(String username, char[] password) { if (validate(username, password) == false) { return null; } return findGroups(username); } private boolean validate(String username, char[] password) { return "hoge".equals(username) && Arrays.equals("fuga".toCharArray(), password); } private String[] findGroups(String username) { return new String[]{"foo", "bar", "baz"}; } }
init(Properties)はスーパークラスで実装されていますが、上記例のようにJAASコンテキストをカスタムRealmクラス自身にsetPropertyする必要があります。わざわざ引数のPropertiesから取得しなくても固定値を突っ込んでも良いでしょうし、getJAASContext()をオーバーライドしても良いかも知れませんが(スーパークラスのgetJAASContextでgetProperty(JAAS_CONTEXT)して得た値を返してる)。まあでもinitでゴニョるのが楽っぽいです。
ふたつのクラスを実装したらJARにしてGlassFishのドメインのlibディレクトリに突っ込みます。
GlassFishのドメインのconfig/login.confを編集します。他のレルムの設定をパクってこんな感じのを追加します。
exampleRealm {
example.ExampleLoginModule required;
};
こんな感じ。
あとはこの認証レルムを使用するアプリケーションをデプロイするだけです。
<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.hogedriven.backpaper0</groupId> <artifactId>glassfish-custom-realm-example</artifactId> <version>1.0</version> <packaging>jar</packaging> <dependencies> <dependency> <groupId>org.glassfish.main.security</groupId> <artifactId>security</artifactId> <version>3.1.2.2</version> <scope>provided</scope> </dependency> </dependencies> </project>
本日開催予定だった関西GlassFish勉強会ですが、台風の影響が大きいと判断して中止致しました。
楽しみにして下さっていた皆様やスピーカーの吉田さんと寺田さん、会場を押さえてくれていた奥さん、申し訳ありませんでした。
いつになるかは分かりませんが、いずれリベンジしたいと考えています。その際はまた皆様にお付き合い頂けると嬉しいです。
どうも。#kancon2011でGlassFish勉強会やろうず!と言っていたうらがみです。
というわけでやろうず!
まず初回はGlassFish入門的な内容にしたいと考えています。それとJavaEE6入門っぽいセッションもあれば良いなぁと思っています。
私ひとりでは力不足なので次のような内容で話してくださるスピーカーさんを緩募します。(もちろん、言い出しっぺの法則、という事で私も話します(・∀・)b)
その他、GlassFish入門、JavaEE6入門的なお話であれば上記テーマにこだわりませんのでご協力頂けるとありがたいです。御連絡は backpaper0@gmail.com またはTwitterで @backpaper0 まで宜しくお願いします。
なお、開催日は9月3日(土)、場所は株式会社クロノス様の研修ルームを予定しております(@kiy0takaさん、いつもありがとう)。
ATND立てました。
最初に“初回は”と書いた通り、二回三回と続けていけるような有意義なものにしたいと思っています。至らない点も多いと思いますが、私と一緒にこのイベントを作って下さる方をお待ちしております。
どうも! ぼけーっとウェブ徘徊してたらGlassFishRuntimeとかいうクラスをハヶ━m9( ゚д゚)っ━ン!!しました。どうやら組込みGlassFishのためのAPIっぽいのですが、v3.0.1のときは無かったように思います。
私が今まで使ってた組込み用のAPIはこんな感じでした。
GlassFishRuntimeを使うと下記のような感じっぽいです。
package samples.javaee; import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; import java.io.File; import java.io.IOException; import javax.annotation.Resource; import javax.ejb.EJB; import javax.ejb.Stateless; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.sql.DataSource; import javax.transaction.UserTransaction; import javax.ws.rs.core.MediaType; import org.glassfish.embeddable.GlassFish; import org.glassfish.embeddable.GlassFishProperties; import org.glassfish.embeddable.GlassFishRuntime; import org.junit.Test; import com.sun.jersey.api.client.Client; /** * pom.xmlへの記述例。 * * <pre> * {@code * <repository> * <id>glassfish</id> * <url>http://download.java.net/maven/glassfish</url> * </repository> * (略) * <dependency> * <groupId>org.glassfish.extras</groupId> * <artifactId>glassfish-embedded-all</artifactId> * <version>3.1</version> * </dependency> * } * </pre> * */ public class GlassFishEmbeddedSample { @Test public void test() throws Exception { GlassFishRuntime gfr = GlassFishRuntime.bootstrap(); GlassFishProperties props = new GlassFishProperties(); props.setPort("http-listener", 8080); // GlassFishのインスタンス作ってサーバ起動? GlassFish gf = gfr.newGlassFish(props); gf.start(); // JDBCコネクションプールとJDBCリソース作成 gf.getCommandRunner().run( "create-jdbc-connection-pool", "--datasourceclassname", "org.apache.derby.jdbc.EmbeddedDataSource40", "--property", "user=sa:password=pwd:databaseName=gftest", "gftest"); gf.getCommandRunner().run( "create-jdbc-resource", "--connectionpoolid", "gftest", "jdbc/gftest"); // デ プ ロ イ (`・ω・´)シャキーン gf.getDeployer().deploy( new File("src/main/webapp"), "--contextroot", "sample", "--name", "sampleapp"); // 動作の確認はJerseyのクライアントでやってみた Client client = Client.create(); String servletResponse = client .resource("http://localhost:8080/sample/hello") .queryParam("name", "world") .get(String.class); assertThat(servletResponse, is("Hello, world!")); gf.stop(); gfr.shutdown(); } /** * サーブレットはアノテーションで定義しました。web.xml要らないです。 * */ @WebServlet(urlPatterns = "/hello") public static class HelloServlet extends HttpServlet { @EJB HelloService service; @Resource UserTransaction userTransaction; @Resource(name = "jdbc/gftest") DataSource dataSource; @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // UserTransactionもDataSourceも取って来れてる様子 System.out.println(userTransaction); System.out.println(dataSource); resp.setContentType(MediaType.TEXT_PLAIN); String name = req.getParameter("name"); if (name == null) { name = "Guest"; } String said = service.say(name); resp.getWriter().print(said); } } /** * EJBもフツーに使えるみたいです。お手軽。 * */ @Stateless public static class HelloService { public String say(String name) { return "Hello, " + name + "!"; } } }
個人的にはGlassFishRuntimeの方がさくっと書けて好きです。JDBCリソース作るところとかかなりasadminコマンドに近いし。
楽しいのでもっと遊ぼうと思います。GlassFish v3.1いいよ! JavaEE6良いよ!