最近の Java Web 開発
自己紹介
- トレーディングシステムとか作ってます
- Web アプリ開発は今回が初めて
自分が話せそうな Java ネタ
- JVM を利用した分散計算(GridGain, Hazelcast etc ...)
- 最近作ってる Web アプリ
今回は後者の紹介にします。
お品書き
- PJ 概要
- 主な使用技術
- サーバーサイド
- クライアントサイド
- まとめ
PJ 概要
- 金融機関向け Web アプリケーション
- クライアントは IE9 ~
- そこそこ複雑な業務ロジック
- 他システムとの SOA 連携も想定
- 顧客都合でサーバは WebLogic / Java 指定
アプリ概要
- Single Page Application
- 今後よりリッチなアプリを作る予定なので試してみた
- 社内利用のみを想定した業務アプリなので色々割り切れた
主な使用技術(サーバー)
- JAX-RS (Jersey)
- Jackson
- Hibernate
- guice
- guava
- Gradle
主な使用技術(クライアント)
- HTML 5
- SCSS
- CoffeeScript
- Handlebars
- Backbone.js
Require JS- bower
- grunt
最近の Java WAF を調べてみた
Java WAF について詳しくなかったので調べてみた。
速攻で採用を見送ったもの
- Struts:無いわー
- SAStruts:S2 は最近更新頻度が落ちてるので
- Juzu:枯れてない……
- Ninja:枯れてない……
Struts 系は無視。Juzu, Ninja は次世代感あるが……。
採用を見送ったもの
- Play Framework:プロダクションで使うの怖い
- Dropwizard:非常に良い感じだが……
いずれも J2EE サーバで動かすことは意図してない。
採用候補となったもの
- Spring MVC:現時点で Java WAF としては最有力?
- JAX-RS:シンプルかつ拡張性が高くて魅力的
いずれも REST API を実装するのに向いている。
- Spring MVC は Spring 周辺プロダクトとの組み合わせが可能
- JAX-RS はシンプルなので自分で好きなように組み立てられる
JAX-RS を採用
- Spring が好きではない(Guice 派)
- Spring MVC は REST サポートが後付け
- JAX-RS の方がシンプルかつ美しい
- 自分で使いやすいコンポーネントを組み合わせて構築したい
部分的には Dropwizard の実装も参考にしました。
JAX-RS を採用
- Web アプリなので入出力が明確
- JAX-RS の拡張性は高い
- フレームワークレイヤを作りこめばそこそこ綺麗に書ける
例えば
- セッション管理(JAX-RS のフィルタを使って実装)
- 権限チェック(JAX-RS のフィルタを使って実装)
- トランザクション管理(Guice の interceptor と JTA を実装)
- DTO - JSON 変換(Dropwizard を参考に Jackson で実装)
- Validation(↑と同じ箇所で実装)
というわけで主に使用しているライブラリを紹介します。
Jackson
- JSON・YAML 変換可能なライブラリ
- 使いやすいし速度も十分
用途
- JAX-RS に引っかけて DTO - JSON を自動変換
- ついでに各種設定ファイルを YAML で管理するように
- properties ファイルで設定管理するのは旧世代っぽい
- Enum などは永続的なコード値に変換するように設定
Hibernate
数年前に作った Hibernate のラッパーをライブラリ化して使用。
特徴
このへん に纏めました。
- ランタイムバインドを排し, 完全にタイプセーフ化(Query DSL より型安全)
- エンティティ・メタクラス・DDL 等の自動生成
- タイプセーフ DAO および継続クエリの実現(今回は後者は使いませんが)
更新処理
エンティティ・メタクラス等は自動生成。
ProjectMeta meta = ProjectMeta.getInstance();
Dao<Project> dao = daoFactory.createDao(meta);
dao.save(project);
dao.update(project);
dao.delete(project);
エンティティをクエリ
EntityQuery<Project> query = dao.createQuery();
List<Project> projects = query
.orderBy(meta.id, Order.ASC);
.list(meta.createCriteria()
.add(meta.sequenceNo.lt(5))
.add(meta.status.eq(ProjectStatus.ACTIVE)));
射影
ProjectionQuery<Project> query = dao.createProjectionQuery();
List<Projection<Project>> values = query.listValues(
meta.createCriteria()
.add(meta.users.admin.isTrue())
.add(meta.users.status.eq(UserStatus.ACTIVE)),
meta.id,
meta.name
);
// projection.get(meta.name) //=> String
集約
AggregationQuery<Project> query = dao.createAggregationQuery();
AggregationFunctionProperty<Project, Long> maxUserIdProperty = Aggregations.max(meta.users.id);
Long maxUserId = query.uniqueValue(
meta.createCriteria()
.add(meta.id.eq(1L))
.add(meta.status.eq(UserStatus.ACTIVE)),
maxUserIdProperty
);
guice
軽量・高速・型安全な DI コンテナ。
- リファクタリングが容易
- モジュール化しやすい
jersey-servlet 使えば JAX-RS 統合も簡単。
JerseyServletModule
public class WebAppModule extends JerseyServletModule {
@Override
protected void configureServlets() {
installModules();
Map<String, String> properties = // ...
serve(config.endpoint()).with(GuiceContainer.class, properties);
}
private void installModules() {
install(new SessionModule());
install(new PersistenceModule());
install(new TransactionModule());
install(new ServiceModule());
// ...
Gradle
柔軟なビルドツール。Gradle 使わなくて良いのは小学生まで。
- Maven より学習が容易で, Maven より柔軟
- スクリプトの使用・動的なタスク生成等が可能
- ビルド定義が DRY に
- 複雑なプロジェクト構成の管理が容易
- 頻繁にプロジェクト構成が変わる場合も対応が楽
- Gradle Wrapper で CI サーバとの連携が簡単
- ドキュメントが豊富
- Eclipse Plugin も十分に使えるレベル
- JS のビルドとの統合も容易
guava
昨今の Java 開発での必須ライブラリ。
- 取り敢えず base, collect だけは押さえておくべき
- Function(s), Predicate(s), Supplier(s), Optional
- Joiner, Splitter, Objects, Strings, CaseFormat
- ImmutableCollection, Collections2, FluentIterable, Ordering
guava あるある
Function, Predicate, Supplier を使って操作するようになる。
FluentIterable.from(users)
.filter(User.ADMIN) // Predicate
.transform(User.ID) // Function
.toList();
可読性と効率のために関数を定数化することが多くなるので
- Eclipse 等の IDE で Code Template 化する
- guavapt みたいな APT を使う
等の対応が効果的。
guava あるある
段々 guava の機能だけでは足りなくなってくる。
// Cache 化する Function を作ったり
Function<Integer, String> fn = Functions2.concurrentCacheOf(f, true);
// RichFunction 作ったり
RichFunction<Integer, RichFunction<Double, Double>> curried = PlusFunction.INSTANCE.curried();
assertThat(curried.apply(1).apply(-0.5d), is(0.5d));
assertThat(curried.apply(1).andThen(TripleFunction.INSTANCE).apply(2d), is(9d));
他言語の経験があると楽しい。
プロジェクト構成
プロジェクト構成
- 厳密に依存関係を整理する
- 業務レイヤが Web を意識することが無いようにする
- 将来的には application-rich-client とか増やしたい
(正直各クライアントが REST API 叩いても良いかなと思い始めてますが……)
開発環境
- WebLogic の起動・デプロイが遅いので Jetty を利用
- スタンドアローンの Java アプリ化
- grunt で assets compile + livereload
Jenkins + Gradle で継続デプロイも捗る。
関心事を分離する
public interface Session
extends TypeSafeProperties<Session>, Serializable {
Serializable getId();
// ...
void invalidate();
}
public interface SessionManager extends SessionProvider {
Option<Session> newSession(Principal principal);
Option<Session> removeSession();
}
- 業務レイヤと Web レイヤの切り離し
- HttpBoundSessionManager(webapp)
- ThreadBoundSessionManager(batch, rich-client)
REST API 定義
@Path("/branches")
@AuthenticationRequired // Session の有無
@AuthorityRequired(Authorities.BRANCH_MANAGEMENT) // 権限の有無
public interface BranchResource {
@GET
@Produces(MediaType.APPLICATION_JSON)
PageableCollectionDto<BranchDto> get(
@ResourceParam BranchSearchConditionDto condition);
@GET
@Path("{id}")
@Produces(MediaType.APPLICATION_JSON)
BranchDto get(@PathParam("id") Long id);
REST API 定義
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
BranchDto create(@Valid BranchDto branch);
// ...
REST API 定義
public class BranchDto
implements Serializable, Function<Branch, Branch> {
static final Function<Branch, BranchDto> FROM_ENTITY
static final Function<BranchDto, Branch> TO_ENTITY
private Long id;
@NotBlank
@Pattern(regexp = "B\\d{3}")
private String code;
@NotBlank
@Size(min = 1, max = 20)
private String name;
REST API 定義
public class BranchSearchConditionDto
extends PaginationConditionDto implements Serializable {
@QueryParam("status")
private Status status;
@QueryParam("includeDeleted")
private Boolean includeDeleted;
public Criteria<Branch> createCriteria() {
// Meta クラスに Criteria を定義することも可能
BranchMeta meta = BranchMeta.getInstance();
Criteria<Branch> criteria = meta.createCriteria();
// ...
return criteria;
}
// ...
API Implementation
単純なマスタなら DTO を介さずに Entity をそのまま JSON 変換しても良いですが。
@Transactional
public class BranchResourceImpl
implements BranchResource {
private static final Configuration CONFIGURATION =
// ... load branches-resource.yml
@Inject
private EntityQueryService queryService;
@Inject
private BranchService branchService;
// ...
API Implementation
@Override
public PageableCollectionDto<BranchDto> get(BranchSearchConditionDto condition) {
// executeQuery(criteria, paginationCondition)
return queryService
.executeQuery(condition.createCriteria(), condition)
.transform(BranchDto.FROM_ENTITY);
}
@Override
public BranchDto get(Long id) {
return BranchDto.FROM_ENTITY.apply(find(id));
}
API Implementation
@Override
public BranchDto create(BranchDto branch) {
try {
return BranchDto.FROM_ENTITY.apply(
branchService.register(
BranchDto.TO_ENTITY.apply(branch)));
} catch (DataUpdateException e) {
// 業務レイヤのチェック例外 -> Web レイヤのレスポンス
throw DataUpdateExceptionMapper.map(e);
}
}
// ...
課題
Bean Validation (JSR-303) が使いづらい
- 条件の組み合わせが難しい
REST API のデザインが難しい
- 業務アプリにありがちな複雑な更新処理
- 例えば約定の一括承認
一括更新
PUT /branches
{ branches: [ { id: 1, status: "AUTHORIZED "}, ... ] }
単純な一括更新でペイロードを最小化したければこんな感じ?
PUT /branches?id[]=1&id[]=2&id[]=3
{ status: "AUTHORIZED" }
- ステータスを直接更新する場合はきっとこんな感じ
- このへん参照
一括更新
- 業務アプリでありがちな複雑な更新処理の場合は?
- 単なるステータスの更新には留まらない
- 編集前履歴の保存
- ドメインロジックの実行
- 他の様々なプロパティの更新
一括更新
API 内部で Service にディスパッチするための情報を渡す?
PUT /branches?id[]=1&id[]=2&id[]=3
{ __op__: "Authorize" }
一括更新
操作毎にトランザクションリソースをつくる?
PUT /branches/authorize?id[]=1&id[]=2&id[]=3
または
PUT /branches/authorize
{ ids: [ 1, 2, 3 ] }
クライアントサイド
はじめての JS 開発……
方針
- 余りに複雑な技術は採用しない
- コードに規約を持ち込みたい
- アプリケーションのモジュール化
- 多言語対応
Coffee Script
- 素の JS は地獄に見えたので Alt JS, Alt CSS を採用
- Haxe は既存コンポーネントとの組み合わせが難しそう
- TS はまだ枯れてないので CS を採用
感想
- CS めっちゃ書きやすいワイワイ
- でも型は欲しいです
Backbone.js
取り敢えず JS MVC FW を調べてみた。
- pros: 軽量かつ柔軟
- pros: 既存ライブラリとの組み合わせが容易
- cons: 素のままでは機能が不足
ひとこと
- 特に学習の必要が無いので導入の敷居は非常に低い
- インスタンスのライフサイクル管理等を作りこむ必要あり
- イベントバスも自前で実装する必要あり
Angular JS
- pros: リッチなデータバインディング
- pros: CRUD 主体の業務アプリには向いている
- cons: 独特の概念があるため学習コストがやや高そう
ひとこと
- 最近もっとも人気がある(ように思える)
- チームを育てられるのであれば採用したい
Ember.js
- pros: 著名な開発者
- pros: リッチなデータバインディング
- pros: イケてる(ように見える)デザイン
- cons: デバッグ地獄
- cons: API 変更多すぎ
- cons: Ember Data ...
ひとこと
- 僕の期待を裏切ったな……!
Backbone.js を採用
- JS 経験が殆ど無いので勉強も兼ねてプレーンなものを使いたかった
- 素の JS に近いので最悪どうにか出来そう
- そのままでは機能不足だったので自前で FW を実装
特に以下の辺りは作り込んだけど……
- View のライフサイクル管理
- メッセージング周り
Backbone.Marionette
- 薄さを残したかったので FW は採用しなかったが……
- 結局自前で FW を作ったら Marionette みたいなものになった
- なので最近 Marionette ベースに書き直してます
- Marionette は比較的ベストプラクティスが共有されてる
ひとこと
- 車輪の再発明は勉強になる
- 車輪の再発明を使うのは避けましょう(保守的な意味で)
Backbone.Marionette
- module, reqres 等が使いやすい
- Router を自作して reqres で逆引き可能にすると捗る
- サブリソースを表現可能にすると更に捗る
- 全体的に API を Promise 化しておくと捗る
所謂 linkTo 等が自動的に出来上がって幸せ。
- execute :
App.execute "resource:method"
- navigate :
App.execute "navigate:resource:method"
- route :
App.request "route:resource:method"
Backbone.Marionette
Show = Application.module "Branches.Show"
Show.Controller =
show: (id) ->
$.when(Application.request "session:checkAuthorities", "BRANCH_MANAGEMENT")
.then ->
Application.request "branch", id
.done (branch) ->
view = new Show.View.FormView model: branch
Application.mainRegion.show view
Application.execute "set:layout", "application"
Backbone.js 感想
良かったところ
- Backbone は薄い(1500 行程度)のでハマらない
- 既存ライブラリとの組み合わせも非常に容易
悪かったところ
- 特にないが, 今なら Angular JS を採用するかも
bower
- JS ライブラリ依存関係管理ツール
- JS 版 Bundler みたいなもの
- 無いと困るレベル
ひとこと
- 特にないです
grunt
node.js ベースのビルドツール。
用途
- CoffeeScript / SCSS / Handlebars の監視・コンパイル
- JS / CSS / images 等の concat / minify, html の変換など
- development / production 等の環境情報の埋め込み
- livereload
などなど。
Yeoman
grunt + bower を利用したフロントエンド開発環境構築ツール。
- ベストプラクティス的な構成を自動生成してくれる
- これを元に改造するのがオススメ
その他に使ったライブラリ達
- modernizr
- lodash
- backbone-forms
- backgrid
- moment
- perfect-scrollbar
- i18next
などなど……。
まとめ
- JAX-RS + JS MVC は普通に開発出来る
- Gradle 最高
ありがとうございました。
思いがけず大量のブックマークありがとうございます。こちら に多少の追記・それからこちら に変換したプレゼンテーションがあります。プレゼンの都合上コード例は殆ど出していませんが、興味のある方おられましたら是非お声がけ下さい。一緒にお話しましょう!