マイクロフレームワークEnkan(とKotowari)ではじめるREPL駆動開発
-
Upload
yoshitka-kawashima -
Category
Technology
-
view
1.477 -
download
1
Transcript of マイクロフレームワークEnkan(とKotowari)ではじめるREPL駆動開発
A new server-side frameworkWhy now?
The twelve factor app
Microservices
主流のJavaフレームワークで大丈夫か?
Focus on mean time to recovery
Enkan's backbone
Connectjavascript
Ringclojure
ductclojure
liberatorclojure
Rackruby
Componentclojure
Enkan Kotowari
Middleware / Component Web application framework based on Enkan(Routing, HTML Template, ...)
Trade-off
Implicit utility 暗黙的な便利機能
High level dependency injection
Easy configuration by many annotations
Understability 理解のしやすさ
Explicit declaration
Middleware
Applicationの共通なコードはすべてMiddlewareに集約されている
Filter? Interceptor? Containerの機能? など迷うことが無くなる
app.use(new DefaultCharsetMiddleware());
app.use(new MetricsMiddleware<>());
app.use(NONE, new ServiceUnavailableMiddleware<>(
new ResourceEndpoint("/public/html/503.html")));
app.use(envIn("development"), new StacktraceMiddleware());
Middleware
Params
NestedParams
MultipartParams
Session
Conversation
Flash
Normalization
Authenticate
ContentNegotiation
SerDes
Routing
ValidateForm
ControllerInvoker
Trace
その他、多くのMiddlewareがありますhttp://enkan.github.io/reference/middlewares.html
Predicate
ここが他のミドルウェアフレームワークと違う
以下のPredicateが標準で利用可能None 全てのリクエストに適用しない
Any 全てのリクエストに適用する
Path パス、methodのマッチしたものだけ
Authenticated 認証されたリクエストのみ
Permission 権限を持っているリクエストのみ
Env 指定した環境のみ
適用するミドルウェアの条件をカスタマイズできる
Predicateの合成
Predicateはjava.utilfunction.Predicateの実装なので、
negate(), and(), or()で合成が可能。
app.use(and(path("^/guestbook/"), authenticated().negate()), (Endpoint<HttpRequest, HttpResponse>) req -> HttpResponseUtils.redirect("/guestbook/login?url=" + req.getUri(), HttpResponseUtils.RedirectStatusCode.TEMPORARY_REDIRECT));
↓ /guestbook 配下のURIで、認証されていないリクエスト(をログインページにリダイレクトする)
No configuration files&
Avoid annotaion hell
???「評判の悪かったXMLは、Annotationに形を変
え生き残っていると言えよう」
Java8 Syntaxを活かせば、DSL的に書けるようになる
Routes routes = Routes.define(r -> { r.get("/").to(HomeController.class, "index"); r.get("/login").to(LoginController.class, "index"); r.post("/login").to(LoginController.class, "login"); r.scope("/admin", admin -> { admin.resource(UserController.class); });}).compile();
例えばルーティングの定義
割と可読性高いのでは!?
EnkanとKotowariで使うAnnotation
@InjectコントローラのフィールドにComponentをインジェクションするためのマーカー
@Named同型のコンポーネントを区別する場合にのみ使用
@Transactionalコントローラメソッドをトランザクション境界にする
Default or Using BeanBuilder
builder(new UndertowComponent()) .set(UndertowComponent::setPort, 77777) .build();
例えばポートをデフォルトから変更する場合
build() コール時に、BeanValidationされるので、MisconfigurationExceptionが発生する
デフォルトで十分動作するように設定されていて、
変更する場合もBeanBuilderを使っていれば
設定ミスにすぐ気づくことができる
http://qiita.com/kawasima/items/0bee34fea7749aa2e776
Hot Reloading
Dispose ClassLoader
Seasar2型のHot deployと同じ方式だが、毎リクエスト破棄するのでは
なく、REPLから/resetを実行したタイミングで再ロードされる。
/autoresetするとクラスの変更を監視し、自動でResetしてくれる
将来的には、JVM containerを使うことで、より高速かつ
安全にリロード可能になる
(詳しくはJDT2016でお話します)
Start application quickly
Enkan application starts 1~3 sec
Causes starting application is slow
Scanning classes
Loading and parsing many configurations
REPL
Connect a remote Enkan application
Start, stop, restart a server
Show the middlewares
Change the predicate of a middleware
Show the routes
Show the metrics
Restart components automatically
Compile sources
Why not JShell
現段階では以下の問題があり未対応
動いているプロセスにアタッチできない
コマンドを追加できない
将来的にはJShell REPL対応する予定です。
(多分 Java 10)
Show the routes
ルーティングの定義を一覧化します
enkan> /routes appGET / {controller=class hoge.controller.IndexController, action=index}GET /product/ {controller=class hoge.controller.ProductController, action=index}GET /product/:id {controller=class hoge.controller.ProductController, action=show}GET /product/new {controller=class hoge.controller.ProductController, action=newForm}POST /product/ {controller=class hoge.controller.ProductController, action=create}GET /product/:id/edit {controller=class hoge.controller.ProductController, action=edit}PUT /product/:id {controller=class hoge.controller.ProductController, action=update}DELETE /product/:id {controller=class hoge.controller.ProductController, action=delete}
Show the metricsenkan> /metrics-- Active Requests ---------------------------------- count = 16-- Errors ------------------------------------ count = 0 mean rate = 0.00 events/s 1-minute rate = 0.00 events/s 5-minute rate = 0.00 events/s 15-minute rate = 0.00 events/s-- Request Timer ----------------------------- count = 23815 mean rate = 176.33 calls/sec 1-minute rate = 176.33 calls/sec 5-minute rate = 73.44 calls/sec 15-minute rate = 24.78 calls/sec min = 0.00 sec max = 0.29 sec mean = 0.01 sec stddev = 0.02 sec median = 0.00 sec 75% <= 0.00 sec 95% <= 0.04 sec 98% <= 0.07 sec 99% <= 0.09 sec 99.9% <= 0.29 sec
Show the middlewares
enkan> /middleware app listANY defaultCharset (enkan.middleware.DefaultCharsetMiddleware@64596fc4)NONE serviceUnavailable (enkan.middleware.ServiceUnavailableMiddleware@252e9fc0)env = {development} stacktrace (enkan.middleware.devel.StacktraceMiddleware@20ef35b2)env = {development} traceWeb (enkan.middleware.devel.TraceWebMiddleware@1b7068c2)ANY trace (enkan.middleware.TraceMiddleware@34f80327)ANY contentType (enkan.middleware.ContentTypeMiddleware@65f95ae7)env = {development} httpStatusCat (enkan.middleware.devel.HttpStatusCatMiddleware@3ce2b14f)ANY params (enkan.middleware.ParamsMiddleware@244cf869)ANY multipartParams (enkan.middleware.MultipartParamsMiddleware@50d936fc)ANY methodOverride (enkan.middleware.MethodOverrideMiddleware@7f586062)ANY normalization (enkan.middleware.NormalizationMiddleware@5619ce0f)ANY nestedParams (enkan.middleware.NestedParamsMiddleware@1555fb15)ANY cookies (enkan.middleware.CookiesMiddleware@c225195)ANY session (enkan.middleware.SessionMiddleware@30c56b03)
Change the predicate of a middleware
enkan> /middleware app listANY defaultCharset (enkan.middleware.DefaultCharsetMiddleware@4929dbc3)NONE serviceUnavailable (enkan.middleware.ServiceUnavailableMiddleware@2ee4fa3b)ANY stacktrace (enkan.middleware.StacktraceMiddleware@545872dd)
enkan> /middleware app predicate serviceUnavailable ANYenkan> /middleware app listANY defaultCharset (enkan.middleware.DefaultCharsetMiddleware@4929dbc3)ANY serviceUnavailable (enkan.middleware.ServiceUnavailableMiddleware@2ee4fa3b)ANY stacktrace (enkan.middleware.StacktraceMiddleware@545872dd)
現在はすべてのリクエストについて非適用
すべてのリクエストについて有効にする
Dependency policy
依存ライブラリは極力少なく
enkan-core
slf4j-api
javaee-api
enkan-webenkan-system
cache-apijline
msgpack
Component
Singleton scope
Field injection
Start/stop lifecycle
Componentとはアプリケーションの状態を管理するもの
Componentとはその状態を安全に開始、終了できるもの
Available components
Undertow / Jetty Web server
HikariCP DataSource
Doma2 O/R Mapper
Flyway Database migration
Freemarker / Thymeleaf HTML Templating
Jackson Bean converter
Metrics Metrics
Enkan System
Minimal DI containerEnkanSystem.of( "doma", new DomaProvider(), "jackson", new JacksonBeansConverter(), "flyway", new FlywayMigration(), "template", new FreemarkerTemplateEngine(), "metrics", new MetricsComponent(), "datasource", new HikariCPComponent( OptionMap.of("uri", "jdbc:h2:mem:test")), "app", new ApplicationComponent( "kotowari.example.MyApplicationFactory"), "http", builder(new UndertowComponent()) .set(UndertowComponent::setPort, Env.getInt("PORT", 3000)) .build()).relationships( component("http").using("app"), component("app").using("datasource", "template", "doma", "jackson", "metrics"), component("doma").using("datasource", "flyway"), component("flyway").using("datasource"));
What's injected to Controller
Field injection
Component
Parameter injection
Parameters (Query string & Post body)
Session
Flash
User principal
Conversation / Conversation state
Request body object
Controller code
public class CustomerController { @Inject private TemplateEngine templateEngine;
@Inject private DomaProvider daoProvider;
@Inject private BeansConverter beans;
public HttpResponse index() { CustomerDao customerDao = daoProvider.getDao(CustomerDao.class); List<Customer> customers = customerDao.selectAll(); return templateEngine.render("customer/list", "customers", customers); }
public List<Customer> list() { CustomerDao customerDao = daoProvider.getDao(CustomerDao.class); return customerDao.selectAll(); }
Session
Selectable session store
In-memory
JCache
Separate between the request session and response session
リクエストセッションとレスポンスセッションが分離しているのが
HttpSessionとの大きな違い
Sessionを使ったコード
public HttpResponse login(Parameters params, Conversation conversation) { if (!conversation.isTransient()) conversation.end(); CustomerDao dao = daoProvider.getDao(CustomerDao.class); String email = params.get("email"); Customer customer = dao.loginByPassword(email, params.get("password")); if (customer == null) { return templateEngine.render("guestbook/login"); } else { Session session = new Session(); session.put("principal", new LoginPrincipal(email)); return builder(redirect(GuestbookController.class, "list", SEE_OTHER)) .set(HttpResponse::setSession, session) .build(); }}
Conversation
入力-確認-完了のような遷移で、Conversation IDを
発行し、それに状態をもつことができる
Servlet APIのHttpSessionのように、複数タブの操
作で状態が混ざりあうことがない
二重サブミット対策にセッションを使う必要がない
Routing
Rails-like
scopeによるグルーピングやresourceによるRESTfulな
ルーティングの定義が可能
Routes routes = Routes.define(r -> { r.get("/").to(HomeController.class, "index"); r.scope("/admin", admin -> { admin.resource(UserController.class); });}).compile();
Generate routes
Routes routes = Routes.define(r -> {
r.get("/a/b/").to(TestController.class, "index");
r.get("/a/b/:id").to(TestController.class, "show");
}).compile();
// Generates "/a/b/"
routes.generate(OptionMap.of("controller", TestController.class,
"action", "index"));
// Generates "/a/b/1"
routes.generate(OptionMap.of("controller", TestController.class,
"action", "show", "id", 1));
Content Negotiation
RESTful APIをEasyに作る
JAX-RSのBodyWriter, BodyReaderがそのまま利用できる。
public List<Customer> list() {
CustomerDao customerDao = daoProvider
.getDao(CustomerDao.class);
return customerDao.selectAll();
}
Maven archetype
最小限のアプリケーションを出力します
デフォルトコンポーネントを組み込んだEnkanSystem
REPLサーバを起動するMain
% mvn archetype:generate \ -DarchetypeGroupId=net.unit8.enkan \ -DarchetypeArtifactId=kotowari-archetype \ -DarchetypeVersion=0.1.0-beta2
Scaffolding
テーブル(Flyway migration)の生成
CRUDアプリケーションの生成
enkan> /generate table PRODUCT (id identity primary key, name varchar(255))
enkan> /generate crud PRODUCT
Amagicman
Yeoman的なものをJVMで。
Yoeman含むよくあるScaffoldingツールとは違い、
テンプレートはピュアJavaファイル
つまり、Amagicmanのタスクは、テンプレートを
いじること
https://github.com/kawasima/amagicman
Javaでも結構REPL-Drivenできそう
仕事でJavaで書かなきゃいけないClojure好きな人は
是非使ってみてください
Java EE、Spring派の方も、ポートフォリオとして
Enkanを加えていただけるとウレシイです