Reactのプロジェクト作成
vue-cliによるプロジェクト作成
reactを始める時は最初のプロジェクト作成が障壁になりそうでreact-cliという公式が出しているツールを使えば開発環境を素早く構築することができますが、内部で利用しているwebpackの設定がどうなっているかなど良く分からなさそうで、使ってみると不便そうな気がしたので自分は試したことがありません。
そこで今回はvueでの開発に使用しているvue-cliを使用してプロジェクトを作成後、reactを利用できるようにしたいと思います。vue-cliの方が開発環境や、ビルドの設定の確認、編集が行いやすくなっておりますので、個人的にはこちらの方が扱いやすいのでこっちで試してみたいと思います。
vue-cliのインストール
npm install -g vue-cli
プロジェクト作成
vue init webpack my_project
cd my-project
npm install
一旦起動してみる
プロジェクト作成直後はvueのプロジェクトになっておりますが、とりあえず以下のコマンドでwebpackを起動して動作を確認してみたいと思います。
npm run dev
上記コマンド実行後にブラウザが開いてvue-jsのアイコンが表示された画面が表示されたかと思います。
vue-cliで作成したプロジェクトをreact化する
それではreactで開発できるようにしていきたいと思います。まずは必要なモジュールをインストールします
npm install –save react react-dom redux react-redux npm install –save-dev babel-loader babel-core babel-preset-react babel-preset-es2015
npm install –save-dev eslint@3.x babel-eslint@6
npm install –save-dev file-loader
npm install –save-dev glob-loader
npm install –save-dev eslint-plugin-react
それから以下の".babelrc"をプロジェクトのルートディレクトリに作成します。
{ "presets": [ "es2015", "react"] }
次にeslintでreactのプラグインを使うように以下の内容にします。
// http://eslint.org/docs/user-guide/configuring module.exports = { root: true, parser: 'babel-eslint', parserOptions: { sourceType: 'module' }, env: { browser: true }, // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style extends: [ "eslint:recommended", "plugin:react/recommended" ], // required to lint *.vue files plugins: [ 'html', 'react' ], // add your custom rules here 'rules': { // allow paren-less arrow functions 'arrow-parens': 0, // allow async-await 'generator-star-spacing': 0, 'react/jsx-uses-vars': 1, // allow debugger during development 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0 } }
あとは"src/main.js"を以下のないように変更したらReactでのコンポーネント初期化が確認できると思います。コンポーネントの初期化についてはes2015の機能であるconstで値の再代入をできないようにしております。es2015を使う場合は変数を定義する場合できるだけlet, constでスコープを制限したり再代入を禁止することで想定外の副作用を防げるようにしておいた方がよさそうです。
import React from 'React' import { render } from 'react-dom' const element = ( <div> <h1>Hello, React!</h1> </div> ) render( element , document.getElementById('app') );
それから"npm run dev"で起動すると思うのですがたくさん警告が表示されると思います。これについてインポートするフモジュール名が実際はreactなのにReactでしているのが原因で、モジュール名はケバブケースを用いるのが標準のようですhttps://github.com/AngularClass/angular2-webpack-starter/issues/926。 それでは以下のようにimport先のモジュール名を変更して再度"npm run dev"を実行してみると警告もなく動くのが確認できると思います。
import React from 'React' ↓ import React from 'react'
ビルドをする場合は"npm run build"を実行すればよく、設定などを変更したい場合はbuildディレクトリの中身を確認して変更していけばどうにかなりそうです。
ScalaでfoldLeftを使ってみる
foldLeftを使ってみる
foldLeft使用の覚書
list内の単語の集計を行って降順にする
これだけでリスト内の単語の集計とソートが行える
val countWordsList = List("a", "b", "c", "a", "d", "b", "e", "b") val wordAggregate = countWordsList.foldLeft(Map[String, Int]().withDefaultValue(0)){ (map, key) => map + (key -> (map(key) + 1)) }.toSeq.sortBy(_._2).reverse println(wordAggregate)
文字列の置換を行う
val replaceMap = Map("{$1}" -> "health", "{$2}" -> "wealth") val beforeText = "{$1} is better than {$2}" val afterText = replaceMap.foldLeft(beforeText){ (tex, map) => tex.replace(map._1, map._2) } println(afterText)
PostgreSQLの文字列関数を使ってみる
PostgreSQLの文字列関数を使ってみる
translate関数で文字を置換する
1文字ずつ置換先の文字を指定することができる
例えば以下のように指定することで全角数値を半角数値に変換できる
translate('13ndfhauo392805735678936', '0123456789','0123456789');
また置換先の文字を指定しない場合は文字を消すことができる(PostgreSQL以外の場合では動きが違うかもしれないです)。以下のように指定すると数値を除去して表示できる。
select translate( '13ndfhauo392805735678936', '0123456789', '');
これらを組み合わせると数値のみを表示するといったことができます。
select translate(translate('13ndfhauo392805735678936', '0123456789','0123456789'), translate( '13ndfhauo392805735678936', '0123456789', ''), '') ;
lpad,rpadで指定した文字数で表示する
lpadでは指定した文字数に足りない場合は右側に文字を埋めて表示する。以下のSQLでは5文字の長さで表示するように、足りない場合は'02'で文字埋めして表示する。
select lpad('12', 5, '02');
文字数を超過する場合は最初の文字を切り出して表示します。
select lpad('236789977', 5, '02')
左側に文字を超過する場合はrpadを使います。
select rpad('12', 5, '02');
PlayFramework事始め
PlayFramework 事始め
- PlayFramework 事始め
PlayFramework 事始め
playのインストール
公式から最新のactivatorをダウンロードしてbinディレクトリにパスを通しておく。
プロジェクトの作成
activatorからプロジェクト作成
新規プロジェクトの作成は"activator new"から行う
activator new
giter8からplayのプロジェクトを作成
giter8を使うことでsbt newでplayのプロジェクトを作成できる
sbt new playframework/play-scala-seed.g8
上記コマンドで作成したプロジェクトをInteliJに取り込んでみるとbuild.sbtでPlayのプラグインがcannot resolveになることがあります。 その場合は、以下の手順でInteliJのプロジェクトを作成します。
1.既存のtarget, .idea, .idea_modulesを削除 2.project/plugins.sbtに以下を追記 addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.6.0") 3."sbt gen-idea"を実行してプロジェクトファイルを作成。それからはInteliJで取り込んでみるとbuild.sbtで発生していたcannot resolveが消えていると思います。 それでもcannot resolveになる場合はもう一度.ideaを削除して開きなおしてみると直るかもしれないです。
コンソールの起動
cd プロジェクト名
activator
サーバを起動
[プロジェクト名] $ run
コンパイル
[プロジェクト名] $ compile
テスト
[プロジェクト名] $ test
デバッグ
ポート指定でデバッグ起動できる
activator -jvm-debug 9999
sbtの利用
Play コンソールは普通の sbt コンソールでもあるため、sbtの機能も利用することができます。 ソース更新のたびにコンパイルする
[プロジェクト名] $ ~ compile
サーバ稼働中ソース更新のたびにビルドする
[プロジェクト名] $ ~ run
ソースが更新されるたびにテストする
[プロジェクト名] $ ~ test
IDE
eclipseで開発する
project/plugins.sbtに以下のプラグインを追加する設定を行う
addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "4.0.0")
それからeclipseのプロジェクトファイルを生成する
activator eclipse
InteliJ IDEA
ファイルを開くでプロジェクトとして認識してくれる
Playアプリケーションの構造
app → アプリケーションソース └ assets → コンパイルされたアセットソース └ stylesheets → 通常は LESS CSS ソース └ javascripts → 通常は CoffeeScript ソース └ controllers → アプリケーションコントローラ └ models → アプリケーションビジネス層 └ views → テンプレート build.sbt → アプリケーションビルドスクリプト conf → 設定ファイル、および (クラスパス上の) その他のコンパイルされないリソース └ application.conf → メイン設定ファイル └ routes → ルート定義 dist → プロジェクト成果物に含める任意のファイル public → 公開アセット └ stylesheets → CSS ファイル └ javascripts → Javascript ファイル └ images → 画像ファイル project → sbt 設定ファイル群 └ build.properties → sbt プロジェクトの目印 └ plugins.sbt → Play 自身の定義を含む sbt プラグイン lib → 管理されていない依存ライブラリ logs → ログフォルダ └ application.log → デフォルトログファイル target → 生成物 └ resolution-cache → 依存性に関する情報 └ scala-2.10 └ api → 生成された API ドキュメント └ classes → コンパイルされたクラスファイル └ routes → routes から生成されたソース └ twirl → テンプレートから生成されたソース └ universal → アプリケーションパッケーs時 └ web → コンパイルされた web アセット test → 単体、および機能テスト用のソースフォルダ
コントローラーを追加してみる
まず簡単なコントローラを追加してみる
package controllers import javax.inject.{Inject, Singleton} import play.api.mvc.{Action, Controller} @Singleton class TestController @Inject() extends Controller{ def index = Action { request => Ok("Got request [" + request + "]") } }
play 2.4からDIが使われるようになっている、今回はDI対象のコンポーネントがないが、DI対象のコンポーネントがある場合は@Injectアノテーションの後に引数としてDI対象コンポーネントを渡すことができる。またクラス自体に@Singletonアノテーションが付与されておりシングルトンとして扱う事を明示しているが、PlayフレームワークがDIに対応する前は普通にobject型として定義するなどしていた。
また受け取るリクエストはrequestで指定することができるが、これはplayフレームワークのplay.api.mvc.Actionクラスがリクエストを受け取る際にapplyメソッドでリクエストをパースしているようである。
それからリクエストを受け取れるようにするためのルート定義をconf/routesに追加する
GET /test controllers.TestController.index
これで"http://localhost:9000/test"にアクセスしたら自作のコントローラが呼び出されるようになる。
リクエストを暗黙のパラメータとして扱う
受け取るリクエストを暗黙のパラメータにすることもできる。受け取ったリクエストのjsonをパースするときとかは暗黙のパラメータとして受け取って
package controllers import javax.inject.{Inject, Singleton} import play.api.mvc.{Action, Controller, RequestHeader} @Singleton class TestController @Inject() extends Controller{ def index = Action { implicit request => Ok(greeting("Hello")) } private def greeting(say: String)(implicit req: RequestHeader) = say + "," + req.remoteAddress }
jsonをリクエストとして受け取れるようにする
まずはplay標準のパーサでjsonのリクエストを受け取ってみるため以下のメソッドを追加する。
def jsonReq = Action(parse.json) { request => (request.body \ "name").asOpt[String].map { name => Ok("Hello " + name) }.getOrElse { BadRequest("Missing parameter [name]") } }
それからroutesを追加
POST /json controllers.TestController.jsonReq
次に以下のコマンドでリクエストを飛ばしjsonを受け取れている事を確認する
curl ‘http://localhost:9000/json’ -H ‘Content-Type: text/json’ –data-binary ‘{“name”: “jon doh”}’
jsonをcase classにセットしてみる
次にjsonのリクエストをcase classにセットする方法を試してみたいと思う。次のようにcaseクラスとmapping、Actionメソッドを定義しておく。
case class AddTodo(categoryId:Long, title:String, text:String) implicit val addTodoForm:Form[AddTodo] = Form ( mapping( "categoryId" -> longNumber, "title"-> text, "text"-> text )(AddTodo.apply)(AddTodo.unapply) ) def jsonReq = Action { request => try{ addTodoForm.bindFromRequest.fold( errors => {BadRequest("bad request")}, validForm => { Ok("categoryId: " + validForm.categoryId + " title:" + validForm.title + " text:" + validForm.text) } ) } }
それから以下のリクエストを投げることでjsonをcase classにセットできることが確認できる
curl ‘http://localhost:9000/json’ -H ‘Content-Type: text/json’ –data-binary ‘{“categoryId”: 1, “title”: “play test”, “text”: “text”}’
Formにセットする際のバリデーション処理は以下の公式を確認する
https://www.playframework.com/documentation/ja/2.4.x/ScalaForms
jsonを返してみる
jsonをレスポンスで返すのも簡単に行える。先ほどのjsonで受け取ったリクエストをそのまま返すようにして動作確認してみたいと思う。
まず先ほど利用したコントローラに以下のメソッドを追加しておく
implicit val todoWrite = new Writes[AddTodo] { def writes(todo: AddTodo) = Json.obj( "categoryId" -> todo.categoryId, "title" -> todo.title, "text" -> todo.text ) }
それから先ほどのコントローラのメソッドのレスポンスを以下のように変更する
def jsonReq = Action { request => try{ addTodoForm.bindFromRequest.fold( errors => {BadRequest("bad request")}, validForm => { Ok(Json.toJson(validForm)) } ) } }
これで受け取ったjsonをFormにセットしてそのまま返す動作が確認できたはずである。jsonのルート要素を変更したい場合は以下のようにすれば良い
Ok(Json.obj("todo" -> validForm))
セッションを使ってみる
以下のメソッドによりセッションごとでのカウントアップを行うことができる。
def count = Action { request => val nextCount = request.session.get("count") match { case Some(x) => x.toInt + 1 case None => 1 } Ok("counta: " + nextCount).withSession( request.session + ("count" -> nextCount.toString) ) }
ただしplayフレームワークはステートレスのフレームワークとなっておりplay側ではセッションを保持せずクライアント側でクッキーとして保持させる形になっている。クライアント側で自由に帰られては困るような情報はredisなど別で持たせるような仕組みにしておく必要がある。
htmlテンプレートを使ってみる
htmlテンプレートのtwirlを使って画面を描画してみる まず以下のコントローラを作成する
package controllers import javax.inject.{Inject, Singleton} import play.api.mvc.{Action, Controller} case class User(firstName: String, lastName: String) @Singleton class SampleController @Inject() extends Controller{ def index = Action { val user = User("john", "does") Ok(views.html.sample(user)) } }
それからapp/viewsにsample.scala.htmlのファイルを作成し、以下を書き込む
@(user: User) <span>this is twirl page</span> @defining(user.firstName + " " + user.lastName) { fullName => <div>Hello @fullName</div> }
これでtwirlのページが表示されるはずである。置換周りについて詳しくか以下を参照する。
https://www.playframework.com/documentation/ja/2.4.x/ScalaTemplates
DBにアクセスする
anromを使う
play2.5では既にplayの標準ではなくなっているけどanormを使ってDBにアクセスしてみる。
依存ライブラリの追加
まずbuild.sbtのdependenciesに以下を追加する
今回は動作確認なのでDBにはh2を使う
libraryDependencies ++= Seq( jdbc, "com.typesafe.play" %% "anorm" % "2.4.0", "com.h2database" % "h2" % "1.4.193" )
DB接続先設定
それからapplication.confにDBの接続先設定として以下を記入する。defaultはplayフレームワークで扱うDB名になる
db { # You can declare as many datasources as you want. # By convention, the default datasource is named `default` # https://www.playframework.com/documentation/latest/Developing-with-the-H2-Database default.driver = org.h2.Driver default.url = "jdbc:h2:mem:play" default.username = sa default.password = "" # You can turn on SQL logging for any datasource # https://www.playframework.com/documentation/latest/Highlights25#Logging-SQL-statements default.logSql=true }
初期データの投入
h2のコンソールにアクセスする場合はactivatorのコンソールで"h2-browser"を入力する
h2-browser
h2のコンソールで接続先のDBにはapplication.confで設定した"jdbc:h2:mem:play"を指定する。
それから今回使用するDB作成のSQLを実行する
create table request_text ( id bigint identity primary key, input_text char(256) ); insert into request_text(id, input_text) values ( null, 'one text'); insert into request_text(id, input_text) values ( null, 'two text'); insert into request_text(id, input_text) values ( null, 'three text'); insert into request_text(id, input_text) values ( null, 'four text');
今回はDBのデータを永続化させていなため次回play起動時にはデータがリセットされるので注意が必要
DBオブジェクト
defaultのDBを使えるようにするために以下のクラスを作成する。使用するときはコントローラ側でDIするようにする。
package db import javax.inject.Inject import play.api.db.DBApi @javax.inject.Singleton class DefaultDB @Inject()(dbapi: DBApi){ val db = dbapi.database("default") }
DAOの作成
次にDBアクセス用のDAOを作成する
package services.repository import javax.inject.Inject import dto.RequestText import play.api.db.Database import anorm.SqlParser.get import anorm.{SQL, ~} case class RequestText(id: Long, input_text: String) @javax.inject.Singleton class TextService @Inject()() { val textParser = { get[Long]("id") ~ get[String]("input_text") map { case id ~ input_text => RequestText(id, input_text) } } def addText(requestText: String)(implicit db: Database): Option[Long] = { insertText(requestText) } def insertText(requestText: String)(implicit db: Database) = { db.withConnection { implicit connection => SQL( """ insert into request_text (id, input_text) values ( (select nextval('text_id_seq')), {request_text} ) """ ).on( 'request_text -> requestText ).executeInsert() } } def getText()(implicit db: Database): Seq[RequestText] = { db.withConnection { implicit connection => SQL("select * from request_text").as(textParser.*) } } }
コントローラ側からDAOを利用する
それから、DBとDAOをDIして利用するコントローラも作成する
package controllers import javax.inject.{Inject, Singleton} import db.DefaultDB import dto.RequestText import play.api.data.Form import play.api.data.Forms._ import play.api.data.Forms.mapping import play.api.i18n.MessagesApi import play.api.mvc.{Action, Controller} import services.repository.TextService @Singleton class SampleController @Inject()(val messagesApi: MessagesApi,defaultDB: DefaultDB,textService: TextService) extends Controller{ val db = defaultDB.db def anormSample = Action { implicit request => db.withTransaction { tr => val texts:Seq[RequestText] = textService.getText()(db) Ok(views.html.sample(texts)) } } case class AddText(input_text:String) implicit val addTextForm:Form[AddText] = Form ( mapping( "input_text"-> text )(AddText.apply)(AddText.unapply) ) def anormAdd = Action { implicit request => addTextForm.bindFromRequest.fold( errors => {BadRequest("bad request")}, validForm => { db.withTransaction{ tr => textService.insertText(validForm.input_text)(db) } } ) Redirect("/anorm") } }
DBオブジェクトとDAOはDIさせて利用している
最後に動作確認ように以下のhtmlを準備したらanormの確認が行えるはず
@import dto.RequestText @(requestTexts: Seq[RequestText]) <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>anorm sample page</title> </head> <body> <span>this is anorm sample</span> <form method="post" action="anorm"> <input type="text" name="input_text" ></input><input type="submit" name="butto" value="送信" /> </form> <ul> @for(requestText <- requestTexts) { <li>@requestText.id: @requestText.input_text</li> } </ul> </body> </html>
今回はコントローラ側から暗黙のパラメータとしてDBを渡すようにしており、利用するDBを切り替える場合にも簡単に対応できる。トランザクション処理は"db.withTransaction"で行うことができる
ScalikeJDBCを使う
次にScalikeJDBCを試してみたいと思います。 まずbuild.sbtのlibDependenciesが以下になるようにします。
libraryDependencies ++= Seq( filters, "com.h2database" % "h2" % "1.4.193", "org.scalikejdbc" %% "scalikejdbc" % "2.5.1", "org.scalikejdbc" %% "scalikejdbc-config" % "2.5.1", "org.scalikejdbc" %% "scalikejdbc-play-initializer" % "2.5.1", "org.scalatestplus.play" %% "scalatestplus-play" % "2.0.0" % Test )
それからconf/application.confに以下を追加します。
scalikejdbc.global.loggingSQLAndTime.enabled=true scalikejdbc.global.loggingSQLAndTime.singleLineMode=false scalikejdbc.global.loggingSQLAndTime.logLevel=debug scalikejdbc.global.loggingSQLAndTime.warningEnabled=true scalikejdbc.global.loggingSQLAndTime.warningThresholdMillis=5 scalikejdbc.global.loggingSQLAndTime.warningLogLevel=warn play.modules.enabled += "scalikejdbc.PlayModule" # scalikejdbc.PlayModule doesn't depend on Play's DBModule play.modules.disabled += "play.api.db.DBModule"
Query DSLでSQLを実行してみる
それではまずは一覧表示できるようにしてみたいと思います。 以下のmodelを追加します。
import scalikejdbc._ case class RequestTexts(id: Long, input_text: String) { def save()(implicit session: DBSession = RequestTexts.autoSession): RequestTexts = RequestTexts.save(this)(session) def destroy()(implicit session: DBSession = RequestTexts.autoSession): Int = RequestTexts.destroy(this)(session) } object RequestTexts extends SQLSyntaxSupport[RequestTexts] { override val autoSession = AutoSession override val schemaName = Some("PUBLIC") override val tableName = "request_text" override val columns = Seq("id", "input_text") val r = RequestTexts.syntax("r") def apply(r: SyntaxProvider[RequestTexts])(rs: WrappedResultSet): RequestTexts = apply(r.resultName)(rs) def apply(r: ResultName[RequestTexts])(rs: WrappedResultSet): RequestTexts = new RequestTexts( id = rs.get(r.id), input_text = rs.get(r.input_text) ) def save(entity: RequestTexts)(implicit session: DBSession = autoSession): RequestTexts = { withSQL { update(RequestTexts).set( column.id -> entity.id, column.input_text -> entity.input_text ).where.eq(column.id, entity.id) }.update.apply() entity } def destroy(entity: RequestTexts)(implicit session: DBSession = autoSession): Int = { withSQL { delete.from(RequestTexts).where.eq(column.id, entity.id) }.update.apply() } def findAll()(implicit session: DBSession = autoSession): List[RequestTexts] = { withSQL(select.from(RequestTexts as r)).map(RequestTexts(r.resultName)).list.apply() } }
それからmodelを利用するコントローラを追加します。
package controllers import javax.inject._ import models.RequestTexts import play.api.data.Form import play.api.data.Forms.mapping import play.api.data.Forms._ import play.api.libs.json.{JsError, Json, Writes} import play.api.mvc._ import scalikejdbc.DB @Singleton class ScalikeJdbcController @Inject() extends Controller { // jsonの書き込み implicit val requestWrite = new Writes[RequestTexts] { def writes(request: RequestTexts) = Json.obj( "id" -> request.id, "input_text" -> request.input_text ) } def list = Action { implicit request => DB.readOnly { implicit session => Ok(Json.obj("root" -> RequestTexts.findAll())) } } }
あとはapiを利用できるようにするためconf/routesに以下を追加します。
GET /scalike controllers.ScalikeJdbcController.list
これで以下のコマンドを実行するとjsonのレスポンスが確認できると思います。 curl ‘http://localhost:9000/scalike’
id指定でselectする場合は以下のようにwhereのパラメータを渡すことができる
def findById(id: Long)(implicit session: DBSession = autoSession): Option[RequestTexts] = { withSQL {select.from(RequestTexts as r).where.eq(r.id, id) }.map(RequestTexts(r.resultName)).single.apply() }
SQLInterpolationでSQLを実行してみる
実行するSQLを自分で書きたい場合は以下のようになります。
def findByIdWithQuery(id: Long)(implicit session: DBSession = autoSession): Option[RequestTexts] = { sql"select id, input_text from request_text where id = ${id}" .map{rs => RequestTexts(rs.long("id"), rs.string("input_text"))}.single.apply }
insertを実行する
insert文は以下のようになります。
def create(entity: RequestTexts)(implicit session: DBSession = autoSession): RequestTexts = { val generatedKey = withSQL { insert.into(RequestTexts).namedValues( column.input_text -> entity.input_text ) }.updateAndReturnGeneratedKey.apply() RequestTexts( id = generatedKey, input_text = entity.input_text) }
slick3を使ってみる
slick3も使ってみたいと思います。
build.sbtが以下になるようにします。
libraryDependencies ++= Seq( filters, "com.h2database" % "h2" % "1.4.193", "com.typesafe.play" %% "play-slick" % "2.0.2", "com.typesafe.slick" %% "slick-codegen" % "3.1.1", "org.scalatestplus.play" %% "scalatestplus-play" % "2.0.0" % Test )
それからapplication.confでDBの接続先を設定します。Anorm、scalikeとは設定が違うので注意です。
slick.dbs.default.driver="slick.driver.H2Driver$" slick.dbs.default.db.driver=org.h2.Driver slick.dbs.default.db.url="jdbc:h2:mem:play" slick.dbs.default.db.user=sa slick.dbs.default.db.password=""
SourceCodeGeneratorでモデルを自動生成する
SourceCodeGeneratorを使ってモデルのクラスを自動で生成したいと思います。次のobjectを作成しておいてください。
package tool.codegen import slick.codegen.SourceCodeGenerator object SlickCodgeGen { def main(args: Array[String]): Unit = { codeGen } def codeGen: Unit ={ val slickDriver = "slick.driver.H2Driver" val jdbcDriver = "org.h2.Driver" val url ="jdbc:h2:mem:play" val user = "sa" val password = "" val outputFolder = "app" val pkg = "models" SourceCodeGenerator.main( Array( slickDriver, jdbcDriver, url, outputFolder, pkg, user, password ) ) } }
あとはcodeGenを呼び出せば良いのですが、プロジェクト作成時に作られるHomeControllerの最初の部分で呼び出すようにしてみます。
package controllers import javax.inject._ import play.api._ import play.api.mvc._ @Singleton class HomeController @Inject() extends Controller { import tool.codegen.SlickCodgeGen SlickCodgeGen.codeGen def index = Action { implicit request => Ok(views.html.index()) } }
これでhttp://localhost:9000/にアクセスするとモデルクラスが自動で生成されると思います。
テーブルのカラムのデフォルト値をnextval(‘自作seq’)などとしているとモデルクラスを作成するときにエラーになりました。その場合はカラムにauto incrementを付与するなどしてDB自体の機能で自動採番させれば大丈夫なようです。PostgreSQLならserial, bigserial型を使えば良さそうだと思います。
一覧表示してみる
それからmodelを利用するコントローラを追加します。
package controllers import play.api.mvc._ import play.api.libs.concurrent.Execution.Implicits.defaultContext import play.api.db.slick._ import slick.driver.JdbcProfile import models.Tables._ import javax.inject.Inject import javax.inject.Singleton import slick.driver.H2Driver.api._ import play.api.libs.json._ @Singleton class SlickContorller @Inject()(val dbConfigProvider: DatabaseConfigProvider) extends Controller with HasDatabaseConfigProvider[JdbcProfile] { // jsonの書き込み implicit val requestWrite = new Writes[RequestTextRow] { def writes(request: RequestTextRow) = Json.obj( "id" -> request.id, "input_text" -> request.inputText ) } def list = Action.async { implicit rs => db.run(RequestText.sortBy(t => t.id).result).map{ texts => Ok(Json.obj("root" -> texts)) } } }
あとはapiを利用できるようにするためconf/routesに以下を追加します。
GET /slick controllers.SlickContorller.list
これで以下のコマンドを実行するとjsonのレスポンスが確認できると思います。 curl ‘http://localhost:9000/slick’
DatabaseForconfigで使用するDBを選択する
Slick3では使用するDBを変更することも簡単に行えます。まずapplication.confを以下のようにして新しいDB接続設定を追記してください。
mydb = { driver=org.h2.Driver url="jdbc:h2:mem:play" user=sa password="" }
それから一覧表示するコントローラを以下のように編集してください。
package controllers import play.api.mvc._ import play.api.libs.concurrent.Execution.Implicits.defaultContext import play.api.db.slick._ import slick.driver.JdbcProfile import models.Tables._ import javax.inject.Inject import javax.inject.Singleton import slick.driver.H2Driver.api._ import play.api.libs.json._ @Singleton class SlickContorller @Inject() extends Controller { val mydb = Database.forConfig("mydb") // jsonの書き込み implicit val requestWrite = new Writes[RequestTextRow] { def writes(request: RequestTextRow) = Json.obj( "id" -> request.id, "input_text" -> request.inputText ) } def list = Action.async { implicit rs => mydb.run(RequestText.sortBy(t => t.id).result).map{ texts => Ok(Json.obj("root" -> texts)) } } }
これで'http://localhost:9000/slick'にアクセスすると新しく用意した接続先が使えていることが確認できます。
ID指定で取得する
先ほどは一覧表示したので今回はidを指定してみたいと思います。コントローラに以下のidでフィルターをかけるメソッドを追加します。
def findById(id: Long) = Action.async {implicit rs => mydb.run(RequestText.filter(t => t.id === id).result).map{ text => Ok(Json.obj("root" -> text)) } }
それからroutesに以下を追加してメソッドを呼び出せるようにします。
GET /find/:id controllers.SlickContorller.findById(id :Long)
これで'curl ‘http://localhost:9000/find/2'のようにid指定でSQLを実行できるのを確認できます。
createを実行する
次にinsertを行ってみます。先ほどのコントローラーに以下を追加します。
case class Reuqest(input_text:String) // jsonの読み込み implicit val addRequestForm:Form[Reuqest] = Form ( mapping( "input_text"-> text )(Reuqest.apply)(Reuqest.unapply) ) def create = Action.async { implicit rs => addRequestForm.bindFromRequest.fold( error => { Future { BadRequest(Json.obj("result" ->"failure")) } }, form => { val requestTextRow = RequestTextRow(0, Option(form.input_text)) mydb.run(RequestText += requestTextRow).map { _ => Ok(Json.obj("result" -> "success")) } } ) }
それからroutesに以下を追加します。
POST /create controllers.SlickContorller.create
これで以下のコマンドを実行するとinsertが行えているのが確認できるはずです。
curl -H “Content-type: application/json” -XPOST -d ‘{“input_text”:“new text”}’ http://localhost:9000/create
updateを実行する
updateを実行する場合は以下のようになります。
def update (id: Long)= Action.async { implicit rs => addRequestForm.bindFromRequest.fold( error => { Future { BadRequest(Json.obj("result" ->"failure")) } }, form => { val requestTextRow = RequestTextRow(id, Option(form.input_text)) mydb.run(RequestText.filter(t => t.id === requestTextRow.id.bind).update(requestTextRow)).map { _ => Ok(Json.obj("result" -> "success")) } } ) }
それからroutesに以下を追加します。
POST /update/:id controllers.SlickContorller.update(id :Long)
これで以下のコマンドを実行するとupdateが行えているのが確認できるはずです。
curl -H “Content-type: application/json” -XPOST -d ‘{“input_text”:“update text”}’ http://localhost:9000/update/2
deleteを実行する
updateを実行する場合は以下のようになります。
def delete(id: Long) = Action.async {implicit rs => mydb.run(RequestText.filter(t => t.id === id).delete).map { _=> Ok(Json.obj("result" -> "success")) } }
それからroutesに以下を追加します。
POST /delete/:id controllers.SlickContorller.delete(id :Long)
これで以下のコマンドを実行するとdeleteが行えているのが確認できるはずです。
curl -H “Content-type: application/json” -XPOST -d ‘{}’ http://localhost:9000/delete/3
Javaでの文字化け検出について
文字化け検出
ファイル読み込み時などで文字化けが発生した時の検出方法について
例えばUTF-8でエンコードされたテキストファイルをShift-JISで読み込もうとした際に、該当する文字が存在しない場合は'0xFFFD'の文字に変換されます。
Javaは内部的にはUnicode(正確にはUTF-16らしい)実際Stringクラスのコメントを確認してみると"represents a string in the UTF-16 format in which supplementary characters"のコメントが確認できるので内部的にはUTF-16が使われているような感じです。(jdk1.8.0_73で確認)
それで文字化けが発生した時に使われる文字ですがひし形ないに?が表記されている�が使われると思いますが、UTF-16でこの文字は0xFFFDの文字コードで表せられるので、文字列内に"(char) 0xfffd"の文字が含まれているかのチェックするのが文字化けしているかの確認方法で一番簡単そうです
SpringBootのアプリをansibleでデプロイ
Spring Bootのアプリをansibleでデプロイ
CentoOS7の環境で確認
初期設定
1. ansibleをインストール
2. 疎通設定
管理側端末で鍵生成
Generating public/private rsa key pair. Enter file in which to save the key (/root/.ssh/id_rsa): Enter passphrase (empty for no passphrase): Enter same passphrase again: Your identification has been saved in /root/.ssh/id_rsa. Your public key has been saved in /root/.ssh/id_rsa.pub. The key fingerprint is: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx root@localhost.localdomain The key's randomart image is:
chmod 600 ~/.ssh/id_rsa
chmod 644 ~/.ssh/id_rsa.pub
openssl rsa -in id_rsa -out -out id_rsa_nopass
mv id_rsa_nopass id_rsa
管理される側の端末で操作
scp -p 192.168.xx.xx:/root/.ssh/id_rsa.pub ./id_rsa_temp.pub
cat id_rsa_temp.pub >> ~/.ssh/authorized_keys
chmod 644 ~/.ssh/authorized_keys
管理する側の端末のansibleのhostsにip追加
echo IPアドレス >> /etc/ansible/hosts
ansibleで接続してコマンド実行ができるか確認する
Spring BootのアプリをリストアするPlayBookを書いてみる
とりあえず疎通が確認できたところでjavaとapacheさえ入っていれば動く簡単なjavaのアプリをデプロイできるようにしたいと思います。
まず任意のディレクトリを作成しそこに移動します。
mkdiry myAnsible
cd myAnsible
それからhostsファイルを作成します。hostsファイル内で使用するsshキーを指定します。
vi hosts
[test] IPアドレス [test:vars] ansible_ssh_user = root ansible_ssh_private_key_file = ~/.ssh/ansible
次に以下のようにインストール対象のroleのフォルダを作成しておきます。
└── roles ├── apache │ ├── files │ ├── tasks │ └── templates ├── java │ ├── files │ ├── tasks │ └── templates └── myApp ├── files ├── tasks └── templates
それから実行するPlayBookを作成します。アプリのデプロイ元のパスとデプロイ先のパスはここで直接指定していますが、デプロイ元のファイルパス等環境毎に設定を行いたいものがあれば別で設定をするようにお願いします。
vi playbook.yml
- hosts: test sudo: true vars: - myapp_path: SpringBootのビルドディレクトリ - app_dest_path: アプリデプロイ先のパス roles: - role: apache - role: java - role: myApp
まずapacheのインストールを行うplaybookを書きたいと思います。
vi roles/apache/tasks/main.yml
--- - name: install apache yum: name=httpd state=installed - copy: src: "{{ role_path }}/files/my_app.conf" dest: "/etc/httpd/conf.d/my_app.conf" owner: root group: root mode: 0644 - name: add http service to firewall command: firewall-cmd --permanent --zone=public --add-service=http - name: add https service to firewall command: firewall-cmd --permanent --zone=public --add-service=https - name: reload firewall command: firewall-cmd --reload - name: restart apache command: apachectl restart
上から順にapacheのインストール、apacheとspringboot連携用の設定ファイル、ファイアウォールを通すようにして反映、apache再起動というようになっています。apacheの設定ファイルをコピーしている箇所ではrole_pathの変数を使用していまして、ここではroles/apacheがそれに該当します。ロール毎のディレクトリにあるfilesディレクトリにはデプロイ先に配置するファイルを置いていまして、今回はapache springboot連携用の設定ファイルを配置しています。
次にjavaのインストールですが、とりあえずyumでjavaをインストールするものだけ記述しておきました。
vi roles/java/main.yml
--- - name: install java yum: name=java-1.8.0-openjdk state=installed
最後にjavaのアプリをデプロイするplaybookに以下のようなものを作成します。
vi roles/myApp/tasks/main.yml
--- - copy: src: "{{ myapp_path }}" dest: "{{ app_dest_path }}" owner: root group: root mode: 0644 - copy: src: "{{ role_path }}/files/myAppRestart.sh" dest: "{{ app_dest_path }}/myAppRestart.sh" owner: root group: root mode: 0644 - name: "stop myApp" shell: "sh /var/myapp/myAppRestart.sh {{ app_dest_path }}" ignore_errors: True - name: "start myApp" shell: nohup java -jar "{{ app_dest_path }}"/build/libs/JenkinsBuildTest-0.0.1-SNAPSHOT.jar &
copyではデプロイ元の環境にあるjavaのビルド結果をsrcとしてデプロイ先の環境にコピーしています。それからjavaのアプリ再起動用のスクリプトをコピーして実行しています。"ignore_errors: True"は実行したスクリプトが失敗した場合であっても次に進めるという動きをし、今回はjavaのアプリを停止しようとした際に既に停止済みで失敗の結果が返ってきたとしても次のjavaアプリスタートのタスクを実行するようにしています。javaのアプリ実行についてはansible実行に使うssh接続が切れた後でもプロセスが残る必要があったのでnohupコマンドを使用しています。これでおそらく簡単なjavaアプリについてはデプロイできたと思います。
ちなみに今回用意したjavaアプリ停止用のスクリプトではpkillを実行するようにしています。
#!/bin/bash find $1/build/libs/ -name "*.jar" | gawk -F/ '{print $NF}' | xargs pkill -IO -f
SpringBoot事始め
- struts2からSpringBootへの移行検討
- ビルドツール
- SpringBootプロジェクト起動用Mainクラスの追加
- 最初のコントローラを追加してみる
- Thymeleafで画面を描画する
- Thymeleafに任意の値を渡してみる
- プロパティファイルを読み込んでみる
- HttpSessionを使ってみる
- 画面からのリクエストを受け取ってみる
- リクエストパラメータをFormにセットする
- Mybatisを使ってみる
- トランザクション処理を行う
- 静的コンテンツにアクセスする
- フィルターを追加する
- エラーをハンドリングする
- コントローラーごとで発生するエラーをハンドリングできるようにする。
- logbackでログを出力する
- アノテーションからAPIドキュメントを自動生成できるようにする
struts2からSpringBootへの移行検討
struts2からspring bootへの移行手順について調べてみた時の備忘録メモ
試した環境は以下になります。
- java8
- Eclipse Neon
ビルドツール
ビルドツールにGradleを使いたいので、Gradleプロジェクトとしてプロジェクトを作成する。
MavenではなくGradleを使う理由として、よう々な環境用にデプロイを行う場合などの管理がしやすくなるという点が挙げられます。Mavenが設定ファイルなのに対してGradleがスクリプトなので柔軟な対応が行えるっぽいです。
事前準備
STS(Spring Tool Suite)をEclipseにインストール
STSがSpring開発に必要になるツール群をeclipseに提供しますGradleプロジェクト作成用プラグインのBuildShipが入っているか確認する
STSからはGradle IDEというプラグインが出ていますが、eclipseの公式が出しているgraldeプラグインの方が今後開発が進んでいくらしいのでこっちを使う
プロジェクト作成
以下の手順でプロジェクトを作成します。
New → Other → GradleProjectと進みます。プロジェクト名は任意のものを選び次のGradleDistributionではGradleWrapperを選択します。GradleWrapperは開発環境にGradleがインストールされていなくてもGradleのコマンドを実行することができるようになったり複数の環境で開発を行う場合はこっちの方がオススメです。それからFinishクリックでプロジェクトを作成します。
*SpringBootのGradleプロジェクト作成であれば、プロジェクト作成→SpringStarterProjectのSPringBootプロジェクト作成画面でTypeにMavenではなくGradle(STS)やGradle(BuildShip)を行うことでも可能でこっちの方が簡単かもしれないです。
依存モジュールの追加
まずSpringBoot用の依存モジュールを追加してみたいと思います。依存モジュールの追加はbuild.gradleから行いまして、プロジェクト作成直後は以下のようになっていると思います。
apply plugin: 'java' repositories { jcenter() } dependencies { compile 'org.slf4j:slf4j-api:1.7.21' testCompile 'junit:junit:4.12' }
Eclipseのbuildshipでプロジェクトを新規作成するとリポジトリがMavenCentralではなくJCenterになっています。リポジトリにはMevenCentralや自前で公開しているリポジトリを指定することもできますが今回はデフォルトのJCenterをリポジトリに指定します。
SpringBoot(Web)のモジュールを追加する場合はBuild.gradleを以下のように修正します。
apply plugin: 'java' apply plugin: 'eclipse' apply plugin: 'org.springframework.boot' version = '0.0.1-SNAPSHOT' sourceCompatibility = 1.8 repositories { jcenter() } buildscript { ext { springBootVersion = '1.5.2.RELEASE' } repositories { mavenCentral() } dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") } } dependencies { compile('org.springframework.boot:spring-boot-starter-web') compile('org.springframework.boot:spring-boot-starter-web-services') testCompile('org.springframework.boot:spring-boot-starter-test') }
でBuild.gradleを更新しただけではElipse側が参照モジュールの反映処理を行ってくれないと思います。そういったときはプロジェクトを右クリックしてGradle → Reflesh Gradle Projectをクリックしてください。これで参照モジュールが追加できたと思います。
SpringBootプロジェクト起動用Mainクラスの追加
SpringBootを起動させるためのクラスを追加してみます。以下のようなクラスを追加すれば大丈夫です。
package jp.co.sample.boot; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class BootMain { public static void main(String[] args) { SpringApplication.run(BootMain.class, args); } }
それからプロジェクトを右クリックして"Run → Run As Spring Boot Project"を実行してみてください。コンソールに"Tomcat started on port(s): 8080 (http)“といった出力が行われSpringBoot内に組み込まれたtomcatで起動していることが確認できたと思います。
最初のコントローラを追加してみる
ブラウザよりリクエストを受け取れるようにするため以下のクラスを追加してみたいと思います。
package jp.co.sample.boot.controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/") public class SampleController { @RequestMapping(value = "OK", method = RequestMethod.GET) public SampleResponse index() { return SampleResponse.OK(); } } class SampleResponse{ String message = ""; private SampleResponse(String message) { this.message = message; }; static SampleResponse OK(){ return new SampleResponse("OK"); } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } }
それからSpringBootを再起動して"http://localhost:8080/OK"にアクセスするとレスポンスが帰ってくるのが確認できます。ここではクラスにRestControllerを指定していまして、こうすることでRestFulなレスポンスを返せるようになりまして、indexメソッドではレスポンスに指定しているSmapleResponseクラスをjson形式に変換して返してくれます。
Thymeleafで画面を描画する
spring bootではjspを使用することもできますが、ここではThymeleafを使用してみたいと思います。Thymeleafはhtml風に記述をすることができるためエンジニア以外の人であっても扱いやすいはずです。Thymeleafを使用する場合はbuild.gradleのdependenciesにthymeleafを追加する必要があります。
dependencies { compile('org.springframework.boot:spring-boot-starter-web') compile('org.springframework.boot:spring-boot-starter-web-services') compile('org.springframework.boot:spring-boot-starter-thymeleaf') testCompile('org.springframework.boot:spring-boot-starter-test') }
build.gradleを修正したらReflesh Gradle Projectを実行します。
次にsrc/main/resorucesに以下のapplication.propertiesを追加します。ちなみに設定ファイルはymlでも大丈夫です。
spring.thymeleaf.prefix=classpath:/templates/ spring.thymeleaf.suffix=.html spring.thymeleaf.mode=HTML5 spring.thymeleaf.encoding=UTF-8 spring.thymeleaf.content-type=text/html spring.thymeleaf.cache=false
それから画面表示用コントローラとして以下のクラスを追加します。
ppackage jp.co.sample.boot.controller; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @Controller @RequestMapping("/") public class SampleThymeleaf { @RequestMapping(value = "/hello", method = RequestMethod.GET) public String index(Model model) { return "hello"; } }
最後にsrc/main/resoruces/templatesに以下のhello.htmlを追加後spring bootを再起動して"http://localhost:8080/hello"にアクセスすると作成した画面が表示されたかと思います。
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <style> article, aside, dialog, figure, footer, header, hgroup, menu, nav, section { display: block; } </style> </head> <body> <p>Hello Thymeleaf</p> </body> </html>
Thymeleafに任意の値を渡してみる
java側で画面に表示する内容を指定する場合は、Modelクラスに値をセットすることで可能です。詳しくは公式を見るのが早いですが簡単なサンプルを試してみたいと思います。
ます先ほど作成したコントローラを以下のように修正します。
package jp.co.sample.boot.controller; import java.util.ArrayList; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @Controller @RequestMapping("/") public class SampleThymeleaf { @RequestMapping(value = "/hello", method = RequestMethod.GET) public String index(Model model) { model.addAttribute("valForJs", "abc"); model.addAttribute("valForFlg", true); model.addAttribute("valForText", "Sample Thymeleaf"); model.addAttribute("valForList", new ArrayList<String>(){{add("AA"); add("BB"); add("CC");}}); return "hello"; } }
それからhtmlを以下のように編集することでjava側で設定した値が反映されていることが確認できます。
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <style> article, aside, dialog, figure, footer, header, hgroup, menu, nav, section { display: block; } </style> <script th:inline="javascript"> /*<![CDATA[*/ var JsVal = /*[[${valForJs}]]*/ 'default val'; console.log(JsVal); /*]]>*/ </script> </head> <body> <div th:if="${valForFlg} == true"> <p>flag is on</p> </div> <div th:if="${valForFlg} == false"> <p>flag is off</p> </div> <span th:text="${valForText}" /> <table> <tr th:each="val : ${valForList}"> <td th:text="${val}"></td> </tr> </table> </body> </html>
プロパティファイルを読み込んでみる
アプリケーションの設定ファイルを読み込んでみたいと思います。試しにsrc/main/resourcesに以下のsystem.propertiesを追加してみてください。
app.version=0.01
それから設定ファイルを読み込む以下のクラスを追加してください。
package jp.co.sample.boot.config; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.PropertySource; import org.springframework.stereotype.Component; @Component @ConfigurationProperties(prefix = "app") @PropertySource(value = "system.properties") public class SampleConfig { private String version; public String getVersion() { return version; } public void setVersion(String version) { this.version = version; } }
ここではプレフィックスにappを指定していますが必須ではありません。ここではプレフィックスにappを指定していますが必須ではありません。上記のサンプルではプレフィックス指定で変数名をプロパティと一致させることで値をセットしていますが、以下のようにセットする値を直接指定することも可能です。
@Value("${app.version}") private String version;
また@ComponentとしておりDIの対象としております。これを利用する場合は以下のように@Autowiredすることで可能です。@Component意外にも@Servieや@RepositoryもDIの対象とすることができまして、アノテーションによって動きが違うのかと思ったけど特にそんなことはなさそうな感じでした。
package jp.co.sample.boot.controller; import java.util.ArrayList; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import jp.co.sample.boot.config.SampleConfig; @Controller @RequestMapping("/") public class SampleThymeleaf { @Autowired private SampleConfig sampleConfig; @RequestMapping(value = "/hello", method = RequestMethod.GET) public String index(Model model) { model.addAttribute("valForJs", "abc"); model.addAttribute("valForFlg", true); model.addAttribute("valForText", "Sample Thymeleaf"); model.addAttribute("valForList", new ArrayList<String>(){{add("AA"); add("BB"); add("CC");}}); model.addAttribute("configVal", sampleConfig.getVersion()); return "hello"; } }
これでhtml側に表示用の処理を追加することで値が取得できていることが確認できると思います。
HttpSessionを使ってみる
springではredisなどにsessionを永続化することも可能ですが、strutsからの移行案として検討するためまずHttpSessionを試してみたいと思います。HttpSessionもDIの対象となっておりまして使う場合は以下のようにすることで可能です。
package jp.co.sample.boot.controller; import java.util.ArrayList; import javax.servlet.http.HttpSession; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import jp.co.sample.boot.config.SampleConfig; @Controller @RequestMapping("/") public class SampleThymeleaf { @Autowired private HttpSession session; @Autowired private SampleConfig sampleConfig; @RequestMapping(value = "/hello", method = RequestMethod.GET) public String index(Model model) { model.addAttribute("valForJs", "abc"); model.addAttribute("valForFlg", true); model.addAttribute("valForText", "Sample Thymeleaf"); model.addAttribute("valForList", new ArrayList<String>() { { add("AA"); add("BB"); add("CC"); } }); model.addAttribute("configVal", sampleConfig.getVersion()); model.addAttribute("count", count()); return "hello"; } private int count() { int count = session.getAttribute("count") == null ? 1 : (int) session.getAttribute("count") + 1; session.setAttribute("count", count); return count; } }
画面からのリクエストを受け取ってみる
HttpServletRequestもHttpSessionと同ようにDIの対象になっています。動作確認ように以下のクラスを作成してみます。
package jp.co.sample.boot.controller; import java.util.ArrayList; import java.util.List; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @Controller @RequestMapping("test") public class TestController { @Autowired private HttpSession session; @Autowired private HttpServletRequest request; @RequestMapping(value = "", method = RequestMethod.GET) public String get(Model model) { setModelFromSession(model); return "test"; } @RequestMapping(value = "", method = RequestMethod.POST) public String post(Model model) { setRquest(request.getParameter("input_text")); setModelFromSession(model); return "test"; } private void setRquest(String inputText){ List<String> textList = session.getAttribute("textList") == null ? new ArrayList<String>() : (List<String>)session.getAttribute("textList"); textList.add(inputText); session.setAttribute("textList", textList); } private void setModelFromSession(Model model){ List<String> textList = session.getAttribute("textList") == null ? new ArrayList<String>() : (List<String>)session.getAttribute("textList"); model.addAttribute("textList", textList); } }
それから、画面表示用のhtmlを追加します。
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <style> article, aside, dialog, figure, footer, header, hgroup, menu, nav, section { display: block; } </style> <script th:inline="javascript"> </script> </head> <body> <form method="post" action="test"> <input type="text" name="input_text" ></input><input type="submit" name="butto" value="送信" /> </form> <ul> <li th:each="val : ${textList}"> <span th:text="${val}" /> </li> </ul> </body> </html>
これでリクエストに受け取ったパラメータをセッションに保存させて表示できていることが確認できると思います。
リクエストパラメータをFormにセットする
もちろんリクエストパラメータをFormにセットすることも可能でその場合は以下のようにまず値をセットするクラスを作成します。
package jp.co.sample.boot.form; public class SampleForm { private String input_text; public String getInput_text() { return input_text; } public void setInput_text(String input_text) { this.input_text = input_text; } }
それから先ほどのpostメソッドを以下のように@ModelAttributeのアノテーションを指定した引数を追加することでFormに値をセットして使用することができます。
@RequestMapping(value = "", method = RequestMethod.POST) public String post(@ModelAttribute SampleForm sampleForm, Model model) { setRquest(sampleForm.getInput_text()); setModelFromSession(model); return "test"; }
Mybatisを使ってみる
JPAなどのO/Rマッピングツールもありますが、既存のSQL資源を活かすためにMyBatisを試してみたいと思います。まずbuild.gradleに以下のdependeicyを追加します。
compile('org.mybatis.spring.boot:mybatis-spring-boot-starter:1.3.0') runtime('com.h2database:h2')
今回は動作確認するだけなのでDBにはH2を使用します。
それからDBの接続先を設定ファイルに記述します。今回はymlに設定したいと思いますのでsrc/mail/resourcesに以下のapplication.ymlを追加します。
spring: datasource: driverClassName: org.h2.Driver url: jdbc:h2:mem:AZ;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=TRUE username: sa password: h2: console: enabled: true
SpringBoot起動後に'http://localhost:8080/h2-console'にアクセスするとh2のコンソールにアクセスできるので、必要なクラスが作成できた後に以下のSQLを実行してテーブルを作成します。
create sequence text_id_seq; create table request_text ( id bigint default text_id_seq.nextval primary key, input_text char(256) );
とりあえずinsertとselectを行う以下のDaoを作成します。@Mapperとかibatisのモジュールのようで流用できるようです。SQLはxmlとアノテーションのどちらかに記述することができ、insertとかならアノテーションで大丈夫そうですがselectになってくるとxmlファイルじゃないと見づらくなりそうだったのでファイルを分けています。
package jp.co.sample.boot.dao; import java.util.List; import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Options; import org.apache.ibatis.annotations.Param; import jp.co.sample.boot.someContext.vo.RequestText; @Mapper public interface RequestTextDao { @Insert("INSERT INTO request_text (input_text) VALUES (#{input_text})") @Options(useGeneratedKeys = true) void insert(RequestText todo); List<RequestText> find(@Param("SEARCH_TEXT") String SEARCH_TEXT); }
select用のxmlファイルは以下になります。
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="jp.co.sample.boot.dao.RequestTextDao"> <select id="find" resultType="jp.co.sample.boot.someContext.vo.RequestText"> SELECT id, input_text FROM request_text <if test="SEARCH_TEXT != null"> WHERE input_text like #{SEARCH_TEXT} </if> ORDER BY ID </select> </mapper>
それからSQLの実行結果をセットするクラスを作成します。
package jp.co.sample.boot.someContext.vo; public class RequestText { private Long id; private String input_text; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getInput_text() { return input_text; } public void setInput_text(String input_text) { this.input_text = input_text; } }
それからDaoを利用するcontrollerを作成します。
package jp.co.sample.boot.controller; import java.util.ArrayList; import java.util.List; import javax.servlet.http.HttpSession; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import jp.co.sample.boot.dao.RequestTextDao; import jp.co.sample.boot.form.SampleForm; import jp.co.sample.boot.someContext.vo.RequestText; @Controller @RequestMapping("test") public class TestController { @Autowired private HttpSession session; @Autowired private RequestTextDao requestTextDao; @RequestMapping(value = "", method = RequestMethod.GET) public String get(Model model) { setModelFromDb(model); return "test"; } @RequestMapping(value = "", method = RequestMethod.POST) public String post(@ModelAttribute SampleForm sampleForm, Model model) { setRquestToDb(sampleForm.getInput_text()); setModelFromDb(model); return "test"; } private void setRquestToDb(String inputText){ RequestText insertData = new RequestText(); insertData.setInput_text(inputText); requestTextDao.insert(insertData); } private void setModelFromDb(Model model){ List<RequestText> textList = requestTextDao.find(null); model.addAttribute("textList", textList); } }
そして結果を表示する以下のhtmlを準備したら動作確認が行えます。
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <style> article, aside, dialog, figure, footer, header, hgroup, menu, nav, section { display: block; } </style> <script th:inline="javascript"> </script> </head> <body> <form method="post" action="test"> <input type="text" name="input_text" ></input><input type="submit" name="butto" value="送信" /> </form> <ul> <li th:each="val : ${textList}"> <span th:text="${val.input_text}" /> </li> </ul> </body> </html>
トランザクション処理を行う
対象のコンポーネントに"@Transactional"のアノテーションを付与するだけでトランザクション対象にすることができる。"@Transactional"はクラスまたはメソッド単位で設定することができる。
静的コンテンツにアクセスする
spring bootで静的コンテンツにアクセスする場合は"src/main/resources/static"の配下にファイルを追加します。"src/main/resources/static/asset.html"を追加した場合はブラウザから"http://localhost:8080/asset.html"と指定することでアクセスすることができます。
フィルターを追加する
例えば特定のURLにアクセスされた場合にリダイレクトするフィルターのクラスは以下のように作成します。
package jp.co.sample.boot.filter; import java.io.IOException; import java.util.regex.Pattern; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class SampleFilter implements Filter { Pattern urlPatter = Pattern.compile("dummy*"); @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { System.out.println("Before"); if(this.urlCheck(request, response)){ ((HttpServletResponse)response).sendRedirect("/asset.html"); } chain.doFilter(request, response); System.out.println("After"); } @Override public void destroy() { } private boolean urlCheck(ServletRequest request, ServletResponse response) throws IOException{ String reuqestURI = ((HttpServletRequest)request).getRequestURI(); return urlPatter.matcher(reuqestURI).find(); } }
それからSpring Bootを起動するmainクラス内に以下のようにbeanを登録することでfilterとして動作するようになります。
package jp.co.sample.boot; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import jp.co.sample.boot.filter.SampleFilter; @SpringBootApplication public class BootMain { public static void main(String[] args) { SpringApplication.run(BootMain.class, args); } @Bean SampleFilter sampleFilter() { return new SampleFilter(); } }
それからspring bootを起動して"http://localhost:8080/dummy"にアクセスするとフィルターでリダイレクトされるようになりますが事前に対象のURLをコントローラに登録しておかなければフィルターに通る前にページが存在しないということでエラーになります。
エラーをハンドリングする
まずエラー発生時に返すjsonを表すクラスを作成します。
public class ErrorResponse { String error_code; String message; public String getError_code() { return error_code; } public void setError_code(String error_code) { this.error_code = error_code; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public static ErrorResponse defaultError() { ErrorResponse errorResponse = new ErrorResponse(); errorResponse.setError_code("001"); errorResponse.setMessage("error occurred!"); return errorResponse; } }
それからエラーをハンドリングするクラスを作成します。@RestControllerAdviceを付与するだけで全てのコントローラで発生するエラーをハンドリングできるようになるので簡単です。
package jp.co.sample.boot.errorHandling; import java.io.PrintWriter; import java.io.StringWriter; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RestControllerAdvice; @RestControllerAdvice public class SampleErrorHandling { public void initBinder(WebDataBinder binder) { System.out.println("controller advice: init binder"); } @ExceptionHandler(Exception.class) public ErrorResponse exception(Exception e) { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); e.printStackTrace(pw); pw.flush(); System.out.println(sw.toString()); return ErrorResponse.defaultError(); } @ModelAttribute public void modelAttribute() { System.out.println("controller advice:model Attribute"); } }
コントローラーごとで発生するエラーをハンドリングできるようにする。
コントローラごとでエラーハンドリングする場合は@ExceptionHandlerアノテーションを付与したメソッドを定義するだけです
package jp.co.sample.boot.controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import jp.co.sample.boot.errorHandling.ErrorResponse; @RestController @RequestMapping("/error") public class ErrorSampleController { @ExceptionHandler public ErrorResponse notFound(NullPointerException ex) { return ErrorResponse.defaultError2(); } @RequestMapping(value = "/test", method = RequestMethod.GET) public String index(Model model) { String a = null; a.trim(); return "error_test"; } }
logbackでログを出力する
logbackでログ出力を行いたいと思います。まず以下のlogback.xmlをsrc/main/resourcesに作成しエラーログを出力できるようにします。ここでは日別のエラーログを2世代分残すように設定しています。
<?xml version="1.0" encoding="UTF-8"?> <configuration> <include resource="org/springframework/boot/logging/logback/defaults.xml"/> <include resource="org/springframework/boot/logging/logback/console-appender.xml"/> <timestamp key="LOG_DATE" datePattern="yyyyMMdd"/> <property name="LOG_FILE" value="logs/error.log" /> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <encoder> <pattern>${FILE_LOG_PATTERN}</pattern> </encoder> <file>${LOG_FILE}</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>${LOG_FILE}.%d{yyyyMMdd}</fileNamePattern> <maxHistory>2</maxHistory> </rollingPolicy> </appender> <root level="ERROR"> <appender-ref ref="CONSOLE"/> <appender-ref ref="FILE"/> </root> </configuration>
それからjava側でエラーログを残すように設定しておきます。
package jp.co.sample.boot.errorHandling; import java.io.PrintWriter; import java.io.StringWriter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RestControllerAdvice; @RestControllerAdvice public class SampleErrorHandling { private final static Logger logger = LoggerFactory.getLogger(SampleErrorHandling.class); public void initBinder(WebDataBinder binder) { System.out.println("controller advice: init binder"); } @ExceptionHandler(Exception.class) public ErrorResponse exception(Exception e) { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); e.printStackTrace(pw); pw.flush(); logger.error(sw.toString()); return ErrorResponse.defaultError(); } @ModelAttribute public void modelAttribute() { System.out.println("controller advice:model Attribute"); } }
アノテーションからAPIドキュメントを自動生成できるようにする
SpringFoxがSwaggerで使用するjsonの自動生成及びswagger-uiの機能を提供します。
まずbuild.gradleに以下のdependencyを追加します。
compile('io.springfox:springfox-swagger2:2.6.1') compile('io.springfox:springfox-swagger-ui:2.6.1')
それから、以下のSwagger設定用のクラスを作成します。公式を確認してみるとAPI KEYやセキュリティなど他にも設定を行える箇所があるので、導入を検討する場合はここを確認した方が良さそです。
package jp.co.sample.boot.tool; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import springfox.documentation.builders.ApiInfoBuilder; import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.service.ApiInfo; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; @Configuration @EnableSwagger2 public class SwaggerConfig { @Bean public Docket documentation() { return new Docket(DocumentationType.SWAGGER_2).select().apis( RequestHandlerSelectors.any()).build().pathMapping("/").apiInfo(metadata()); } private ApiInfo metadata() { return new ApiInfoBuilder().title("Sample Swagger-ui title").version("1.0").build(); } }
次にコントローラーに対してAPIドキュメント生成用のアノテーションを追加します。試しに以下のように修正します。
package jp.co.sample.boot.controller; import javax.websocket.server.PathParam; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; @RestController @RequestMapping("dummy") public class Dummy { @RequestMapping(value = "", method = RequestMethod.GET) public String get(Model model) { return "dummy"; } @RequestMapping(value = "/{id}", method = RequestMethod.POST) @ApiOperation(value = "Returns dummy response", notes = "response description", response = DummyResponse.class, produces = "application/json") public DummyResponse post( @PathVariable("id") @ApiParam(name = "dummyPathParam", value = "dummy description", required = true) String id, @PathParam("dummyParam") @ApiParam(name = "dummyRequestParam", value = "dummy description", required = false) String dummyParam ) { return new DummyResponse(); } }
ここでは、@ApiOperationでレスポンスの情報を記述しています。それからapiに渡すパラメータは@ApiParamで指定しています。コントローラーに対してアノテーションの付与が完了したらspring boot起動後"http://localhost:8080/swagger-ui.html"にアクセスしてみたらAPIドキュメントが確認できるかと思います。
ただし、実際に使う場合は開発環境のみ利用できるなどセキュリティ面は気をつけた方が良さそうです。例えばリリース環境では@EnableSwagger2アノテーションを付与したクラスを含めないようにするやSpring-Securityや公式の説明を参考に一般の利用者からはアクセスできないようにする工夫には気をつけた方が良さそうです。