マイクロフレームワークEnkan(とKotowari)ではじめるREPL駆動開発

53
マイクロフレームワーク Enkan( Kotowari) ではじめる REPL 駆動開発 kawasima JJUG CCC 2016 Sping CD-7

Transcript of マイクロフレームワークEnkan(とKotowari)ではじめるREPL駆動開発

マイクロフレームワーク Enkan(とKotowari)ではじめる

REPL駆動開発

kawasima

JJUG CCC 2016 Sping CD-7

Enkanhttp://enkan.github.io/

Background

A new server-side frameworkWhy now?

The twelve factor app

Microservices

主流のJavaフレームワークで大丈夫か?

Focus on mean time to recovery

Requirements

Java8 以上

Maven3以上

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

Concepts

Key Concepts

Explicitness 明示的であること

Ease of deveopment 開発がしやすいこと

Ease of Operation 保守運用が簡単であること

Explicitness

Middleware pattern

No configuration files

Anti-blackbox

Avoid annotation hell

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コントローラメソッドをトランザクション境界にする

Anti Blackbox

Component, Middlewareの初期化は

すべて明示的にnewする

黒魔術を使わない

バイトコードを触るProxy

Default or Using BeanBuilder

builder(new UndertowComponent()) .set(UndertowComponent::setPort, 77777) .build();

例えばポートをデフォルトから変更する場合

build() コール時に、BeanValidationされるので、MisconfigurationExceptionが発生する

デフォルトで十分動作するように設定されていて、

変更する場合もBeanBuilderを使っていれば

設定ミスにすぐ気づくことができる

http://qiita.com/kawasima/items/0bee34fea7749aa2e776

Ease of development

Hot reloading

Easy to realize misconfiguration

Trace the request / response

Hot Reloading

 Dispose ClassLoader

Seasar2型のHot deployと同じ方式だが、毎リクエスト破棄するのでは

なく、REPLから/resetを実行したタイミングで再ロードされる。

/autoresetするとクラスの変更を監視し、自動でResetしてくれる

 将来的には、JVM containerを使うことで、より高速かつ

安全にリロード可能になる

(詳しくはJDT2016でお話します)

ExceptionMisconfigurationException

設定のミスと思われるものは問題と対策をもった専用の

Exceptionを出力する。

Trace the request/response

どのミドルウェアをとおり、

どこでどれだけ時間がかかっているのかを

1リクエスト毎に可視化して表示できる

Ease of Operation

Start application quickly

Metrics

REPL

Start application quickly

Enkan application starts 1~3 sec

Causes starting application is slow

Scanning classes

Loading and parsing many configurations

Metrics

Dropwizard metrics

アクティブなリクエスト数

エラーになったリクエスト数

1リクエストあたりの性能

スループット

View in the REPL

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)

現在はすべてのリクエストについて非適用

すべてのリクエストについて有効にする

Core parts

Architecture

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();

}

Getting Startedhttp://enkan.github.io/getting-started.html

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

Conclusion

Javaでも結構REPL-Drivenできそう

仕事でJavaで書かなきゃいけないClojure好きな人は

是非使ってみてください

Java EE、Spring派の方も、ポートフォリオとして

Enkanを加えていただけるとウレシイです

さぁ、みなさんも、

EnkanとKotowariに

導かれるのです…

円 環 理