読者です 読者をやめる 読者になる 読者になる

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 事始め

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を受け取れている事を確認する

curlhttp://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にセットできることが確認できる

curlhttp://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 DSLSQLを実行してみる

それではまずは一覧表示できるようにしてみたいと思います。 以下の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のレスポンスが確認できると思います。 curlhttp://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のレスポンスが確認できると思います。 curlhttp://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)

これで'curlhttp://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をインストール

yum install epel-release
yum install ansible

 2. 疎通設定
管理側端末で鍵生成

ssh-keygen -t rsa

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で接続してコマンド実行ができるか確認する

ansible -i /etc/ansible/hosts IPアドレス -m ping

Spring BootのアプリをリストアするPlayBookを書いてみる

とりあえず疎通が確認できたところでjavaapacheさえ入っていれば動く簡単な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のインストールですが、とりあえずyumjavaをインストールするものだけ記述しておきました。

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への移行検討

struts2からspring bootへの移行手順について調べてみた時の備忘録メモ
試した環境は以下になります。
- java8
- Eclipse Neon

ビルドツール

ビルドツールにGradleを使いたいので、Gradleプロジェクトとしてプロジェクトを作成する。
MavenではなくGradleを使う理由として、よう々な環境用にデプロイを行う場合などの管理がしやすくなるという点が挙げられます。Mavenが設定ファイルなのに対してGradleがスクリプトなので柔軟な対応が行えるっぽいです。

事前準備

プロジェクト作成

以下の手順でプロジェクトを作成します。
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のモジュールのようで流用できるようです。SQLxmlアノテーションのどちらかに記述することができ、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や公式の説明を参考に一般の利用者からはアクセスできないようにする工夫には気をつけた方が良さそうです。

jenkinsのインストールからGradleの手動ビルドまで

jenkins

Javaで書かれたオープンソース継続的インテグレーションツール
いろいろ自動化しなきゃと思ったのでちょっと触ったときのメモ

インストール

事前にjavaがインストールされているか確認、入ってなかったら7以上のjavaを入れておく

# yum install java

jenkinsのインストール

公式の手順に従いリポジトリを追加してyumからインストール

sudo wget -O /etc/yum.repos.d/jenkins.repo http://pkg.jenkins-ci.org/redhat-stable/jenkins.repo
sudo rpm --import https://jenkins-ci.org/redhat/jenkins-ci.org.key
sudo yum install jenkins

jenkinsの起動

CentOS7も起動スクリプトから起動

service jenkins start

jenkinsの自動起動設定

chkconfig jenkins on

外部からの通信許可

firewall-cmd --zone=public --add-port=8080/tcp --permanent
firewall-cmd --zone=public --add-service=http --permanent
firewall-cmd --reload

初期設定

http://IPアドレス:8080/"でJenkinsにアクセスログイン画面では”/var/lib/jenkins/secrets/initialAdminPassword"に初期パスワードの設定があるとの表示があるのでそれに従いパスワードを入力してログイン 標準的なプラグインをインストールするかどうか選べるのでインストールするで次に進む
プラグインインストール後はユーザを作成してダッシュボード画面に遷移

秘密鍵を追加

“/var/lib/jenkins/.ssh"に秘密鍵を追加する

mkdir /var/lib/jenkins/.ssh
chown jenkns:jenkins /var/lib/jenkins/.ssh
[root@localhost .ssh]# ssh-keygen -t rsa
Generating public/private rsa key pair.
Enter file in which to save the key (/root/.ssh/id_rsa): /var/lib/jenkins/.ssh/id_rsa
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /var/lib/jenkins/.ssh/id_rsa.
Your public key has been saved in /var/lib/jenkins/.ssh/id_rsa.pub.
The key fingerprint is:
xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx xxxx@xxxxxxxxxx

[root@localhost jenkins]# chown -R jenkins:jenkins .ssh/var/lib/jenkins/.ssh/
[root@localhost jenkins]# chmod 600 .ssh/id_rsa
[root@localhost jenkins]# chmod 644 .ssh/id_rsa.pub
[root@localhost jenkins]# chmod 644 .ssh/known_hosts

# sudo -u jenkins ssh -T git@bitbucket.org

Gradleをビルドしてみる

新規ジョブ作成から"フリースタイル・プロジェクトのビルド"を選択
ソースコード管理ではgitを選択してリポジトリURLにはgitプロトコルのURLを入力する。認証情報の追加から事前に準備していた鍵を指定する。
ビルドトリガも選びたいけど今回は手動で動かせば良いので未設定、ビルド環境も今回は簡単な動作確認が目的のため未設定
ビルド手順の追加では"Invoke Gradle Script"でも良いのかもしれないけど今回はコミット前に"gradle wrapper"を準備しておいてGradleをインストールしていなくても ビルドを実行できる状態にしておいたので"シェルの実行"を選択して"./gradlew build"を指定しておく    

それから手動で実行をしたら

Could not find tools.jar. Please check that /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.121-0.b13.el7_3.x86_64/jre contains a valid JDK installation.

と怒られたのでjdkをインンストール

yum install java-1.8.0-openjdk-devel

再度手動で実行してビルド成功を確認!

便利そうなのでgitのイベントをフックして自動でビルド、デプロイまで出来るようにしたいと思います。