Spring で Bean Validation のエラーメッセージに任意のフィールド名を埋め込む

Bean Validation のエラーメッセージに任意のフィールド名を埋め込む方法を調べたときのメモ。タイトルには Bean Validation と書いていますが、正確には Spring が提供する機能によってエラーメッセージに任意のフィールド名を埋め込みます。

サンプルコード。

github.com


バリデーション用のアノテーションは以下の通り。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Constraint(validatedBy = { SampleValidator.class })
public @interface SampleValidation {
    String value();
    String message() default "{com.example.demo.constraint.SampleValidation.message}";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

今回は Controller で @Validated を利用して Form バリデーションをするパターン。

@RestController
public class DemoController {
    @Autowired
    MessageSource messageSource;
    @PostMapping("index")
    public String index(@RequestBody @Validated FooForm form, Errors errors) {
        if (errors.hasErrors()) {
            errors.getAllErrors().stream()
                    .map(e -> messageSource.getMessage(e, Locale.getDefault()))
                    .forEach(System.out::println);
            return "NG";
        }
        return "OK";
    }
    static class FooForm {
        @SampleValidation("foo")
        private String name; // バリデーション対象のフィールド
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
    }
}

パターン1

プロパティ未定義の場合、Form クラスのフィールド名がメッセージ定義の {0} にそのまま出力される。

com.example.demo.constraint.SampleValidation.message={0} is invalid.

出力結果:

> name is invalid.

パターン2

フィールド名をキーにしてプロパティを定義した場合、定義した名前がメッセージ定義の {0} に埋め込まれて出力される。<Bean 名>.<フィールド名> 形式も可。

com.example.demo.constraint.SampleValidation.message={0} is invalid.
name=Name

出力結果:

> Name is invalid.

パターン3

<フィールド名> 形式と <Bean 名>.<フィールド名> 形式を一緒に定義した場合、<Bean 名>.<フィールド名> 形式のプロパティが優先される。

com.example.demo.constraint.SampleValidation.message={0} is invalid.
name=Name
fooForm.name=NAME

出力結果:

> NAME is invalid.

現場からは以上です。

Bootstrap と Vue.js で簡単なモックアップを作る

以前、Bootstrap ベースのモックアップについて書きました。

Bootstrap を利用して簡単なモックアップを作る - kntmr-blog

で、今回は Bootstrap + Vue.js 版のモックアップを作ってみました。モックアップとしての内容は Bootstrap 版と同じです。Vue.js と Webpack の初学習を兼ねているので、いろいろとあやしいところがあると思います。特に、Webpack の機能はまだよく理解できていない…。

github.com


以下、備忘録。

package.json を作る。

npm init

開発時に使うライブラリは --save-dev を付けてインストールする。実行時に必要なライブラリは --save を付けてインストールする。(たぶん)

npm install --save-dev webpack-dev-server webpack vue-template-compiler vue-style-loader vue-loader url-loader style-loader file-loader extract-text-webpack-plugin css-loader
npm install --save vue-router vue jquery bootstrap

package.json に scripts を定義して、npm run build で webpack コマンドを叩く。webpack.config.js の設定に従ってモジュールをビルドする。

{
  "scripts": {
    "dev": "webpack-dev-server",
    "build": "webpack"
  }
}

ローカルの動作環境には webpack-dev-server を使う。npm run dev でサーバーを立ち上げて、http://localhost:8080 にアクセスするとページが表示される。webpack-dev-server は、ファイルの変更を検知して自動でリビルドしてライブリロードする機能を持つ。

一応、vue ファイルでコンポーネント化しているが、試行錯誤したものの、いろいろと中途半端なところはありそう。

ご参考まで。

Oracle 認定資格デジタルバッジ

8月から Oracle 認定資格を保有していることを証明する電子証明書デジタルバッジ』の提供が開始されたようです。保有資格をオンラインで公開できるものらしい。

education.oracle.com

というわけで、Oracle Certified Java Programmer のデジタルバッジを入手。

Oracle Certified Java Programmer, Silver SE 8
Oracle Certified Java Programmer, Gold SE 8

その他

Silver SE 8 を受験したときのメモはこちら。

kntmr.hatenablog.com

Gold SE 8 を受験したときのメモはこちら。

kntmr.hatenablog.com

サービスイン or 切り戻し

先日、お客様の新規システムのリリースがあり、開発担当のリリース支援として現地で待機していたときのこと。

ちなみに、新旧システム間のデータ移行が想定通りに進まず、最終的には切り戻しとなりました。データ移行はお客様側の作業。「切り戻し」とは、新システムのリリースで何らかの問題が発生した際に、旧バージョンのシステムに戻すことを指します。

今回、移行対象のデータ量が多かった上に、たびたび移行データの不備 (不整合) が発生していたようです。データを直しつつリトライしていたようですが、さすがに終わる見通しが付けられなかった模様。

そもそも、対象がECサイトのシステムであり、メンテナンスにより深夜から日中にかけてサービス停止していたため、さすがにビジネス的なインパクトが大きかったと思います。

とは言っても、さすがに切り戻しを判断するのが遅すぎたように思えます。

所感

なんとかデータ移行とリリースをやり切ってサービスインするという意思は感じたけど、もう少し早いタイミングで切り戻しを決断できなかったものか。

もしかしたら、自分たちが見えている範囲ではやり切れるという自信があったのかもしれません。しかし落とし穴は見えないところにあるものです。

リリース計画やリリース手順を準備していても、計画通りにいかないことはあると思います。そして、計画にない作業を手探りで進めていると、次第に冷静さを欠いていくものです。特に、リリース作業のような非日常の状況下だと。

その結果、適切なタイミングで大事な判断ができず、ズルズルと長い時間に亘ってわるい状況が続いていったりします。

こういう状況になる前に勇気を持ってストップをかけられるか、切り戻しの判断ができるか、これが大事だと思いました。もちろん、切り戻しの作業をリリースの計画に含めておくことは重要です。切り戻しで事故ったらどうしようもないので。

ただ、当事者の立場だとなかなかこういう決断は難しいものです。なので、マイルストーンを置いてそれに従うことを徹底するのがポイントかなと思います。それか、当事者以外のひとがスケジュールを管理するとか、とにかく機械的に判断できる状態にするのが重要かと思いました。

また、システム自体も、切り戻し可能かつ切り戻ししやすいアーキテクチャにするのが大事だと思います。

その他

今回、自分はリリース支援の立場で、直接リリース作業に関わっていたわけではないのですが、いろいろと学びがありました。ちなみに、前日夕方から仕事しており、そのまま深夜リリース、翌日夕方過ぎまで立ち合いしていたため、28時間勤務となりました。

Haskell を使ってみる 9 (高階関数1)

前回の続き

Haskell を使ってみる 8 (再帰) - kntmr-blog

引数に取ったり返り値として返せる関数を高階関数と呼ぶ。

カリー化関数

Haskell のすべての関数は引数を1つだけ取る。複数の引数を受け取るような関数はカリー化されている。関数を本来より少ない引数で呼び出すことを部分適用と呼ぶ。

Prelude> :t max
max :: Ord a => a -> a -> a -- a 型の値を引数に取る関数で、「a 型の値を引数に取って a 型の値を返す関数」を返す

これは、max :: Ord a => a -> (a -> a) と同義。

中置関数はセクションを使って部分適用する。

divideByTen :: (Floating a) => a -> a
divideByTen = (/10) -- 片側に値を置いて括弧で囲む

*Main> divideByTen 200
20.0

高階プログラミング

引数に関数を取ったり返り値として関数を返す関数の例。

 applyTwice :: (a -> a) -> a -> a -- 「a 型の引数を受け取り a 型を返す関数」を引数に取る
 applyTwice f x = f (f x)

*Main> applyTwice (+2) 10
14
*Main> applyTwice ("Hello " ++) "Haskell"
"Hello Hello Haskell"

ラムダ式

1回だけ必要な関数を作るときに使う無名関数をラムダ式と呼ぶ。主に高階関数に渡す関数を作るときに使われる。ラムダ式を宣言する場合は \ を使う。

Prelude> map (+1) [1,2,3,4] -- 部分適用
[2,3,4,5]
Prelude> map (\x -> x + 1) [1,2,3,4] -- ラムダ式
[2,3,4,5]

今回はカリー化と部分適用、ラムダ式まで。

LINE Messaging API と Google Apps Script で LINE BOT を作ってみる

LINE Messaging APIGoogle Apps Script で LINE BOT を作ってみるメモ。今回は、LINEのグループに送信したメッセージをメールで転送するBOT

アカウント作成 / BOT設定

※事前にLINEアカウントを作成すること

「LINE Business Center > サービス」の Messaging API で、「Developer Trial を始める」から LINE Business Center アカウントを登録する。

アカウントを登録すると LINE@ MANAGER ページが表示されるので、「APIを利用する」をクリックする。

「アカウント設定 > Bot設定」で以下を設定する。

  • リクエスト設定 > Webhook送信 > 「利用する」にチェック
  • 詳細設定 > Botグループトーク参加 > 「利用する」にチェック

BOTを友達に追加したときや自動応答のメッセージが不要の場合は「自動応答メッセージ」「友だち追加時あいさつ」のデフォルトメッセージを削除する。

最後に自分のLINEアカウントでBOTを友達に追加する。

LINE Developers

「アカウント設定 > Bot設定」のステータス欄にある「LINE Developersで設定する」リンクから LINE Developers ページを表示して Channel Access Token を発行する。これは、Google Apps Script から Messaging API にアクセスする際に使う。

Google Apps Script

今回、サーバを立てる代わりに Google Apps Script を使う。Google アカウントがあれば使えるので便利。

LINE BOT のプロジェクトと、処理を記述するスクリプトファイルを作成する。作成したプロジェクトは Google Drive に保存される。

LINE BOT からは POST でメッセージが渡ってくるので、doPost 関数を定義する。今回は受け取ったメッセージと送信したユーザの名前をメールで転送する。

var CHANNEL_ACCESS_TOKEN = '<CHANNEL_ACCESS_TOKEN>';

function getUsername(userId) {
  var url = 'https://api.line.me/v2/bot/profile/' + userId;
  var response = UrlFetchApp.fetch(url, {
    'headers': {
      'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN
    }
  });
  return JSON.parse(response.getContentText()).displayName;
}

function doPost(e) {
  var messageText = JSON.parse(e.postData.contents).events[0].message.text;
  var userId = JSON.parse(e.postData.contents).events[0].source.userId;
  var username = getUsername(userId);
  
  MailApp.sendEmail('sample@mail.com', 'Forwarded LINE Messages', 'From: ' + username + String.fromCharCode(10) + messageText);
  return JSON.stringify({});
}

ドキュメントトップ を参考。

作成したスクリプトは「Publish > Deploy as web app...」で Web アプリとして公開する。Who has access to the app は Anyone, even anonymous を選択する。デプロイすると URL が表示されるので、コピーして LINE Developers の Webhook URL に設定する。

まとめ

思いの外、簡単に LINE BOT が作れる。あと、複数人トークグループトークは違うので要注意。

追記 (2017/07/10)

メッセージをリプライするだけのBOTを作る場合は以下。

var CHANNEL_ACCESS_TOKEN = '<CHANNEL_ACCESS_TOKEN >';

function doPost(e) {
  var replyToken = JSON.parse(e.postData.contents).events[0].replyToken;
  var messageText = JSON.parse(e.postData.contents).events[0].message.text;
  UrlFetchApp.fetch('https://api.line.me/v2/bot/message/reply', {
    'headers': {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN,
    },
    'method': 'POST',
    'payload': JSON.stringify({
      'replyToken': replyToken,
      'messages': [{
        'type': 'text',
        'text': messageText + '!!!!',
      }],
    })
  });
  return JSON.stringify({});
}

Haskell を使ってみる 8 (再帰)

前回の続き

Haskell を使ってみる 7 (ガード) - kntmr-blog

再帰

関数を再帰的に定義する場合は問題を同じ種類のより小さな問題に分解する。再帰を使わずに定義できる問題を基底部と呼ぶ。再帰を実装する場合は基底部から考える。

maximum' :: (Ord a) => [a] -> a
maximum' [] = error "empty list"
maximum' [x] = x -- 基底部 (単一要素のリストの最大値はその唯一の要素を返す)
maximum' (x:xs) = max x (maximum' xs)

replicate' :: Int -> a -> [a]
replicate' n x
    | n < 1 = [] -- 基底部 (繰り返しが0以下のときは空のリストを返す)
    | otherwise = x : replicate' (n-1) x

take' :: Int -> [a] -> [a]
take' n _
    | n < 1 = [] -- 基底部 (取り出す個数が0以下のときは空のリストを返す)
take' _ [] = [] -- 基底部 (空のリストからは要素を取り出せないので空のリストを返す)
take' n (x:xs) = x : take' (n-1) xs

reverse' :: [a] -> [a]
reverse' [] = [] -- 基底部 (空のリストの逆順は空のリスト)
reverse' (x:xs) = reverse' xs ++ [x] -- tail の逆順の後ろに head を付ける

-- 基底部のない再帰で無限リストを作成する
repeat' :: a -> [a]
repeat' x = x : repeat' x

zip' :: [a] -> [b] -> [(a,b)]
zip' _ [] = [] -- 基底部 (空のリストを zip したときは空のリストを返す)
zip' [] _ = [] -- 基底部 (同上)
zip' (x:xs) (y:ys) = (x,y) : zip' xs ys -- head のペアの後ろに tail を zip したものを繋げる

elem' :: (Eq a) => a -> [a] -> Bool
elem' a [] = False -- 基底部 (空のリストは値を含まないので False を返す)
elem' a (x:xs)
    | a == x = True -- 渡された値と head が一致するか調べる
    | otherwise = a `elem'` xs -- head が一致しなければ tail を調べる

クイックソート

再帰を使ってクイックソートを実装する例。

quicksort :: (Ord a) => [a] -> [a]
quicksort [] = [] -- 基底部 (空のリストをソートしたときは空のリストを返す)
quicksort (x:xs) =
    let smallerOrEqual = [a | a <- xs, a <= x]
        larger = [a | a <- xs, a > x]
    in quicksort smallerOrEqual ++ [x] ++ quicksort larger

リストの先頭要素をピボットとする。リスト内包表記でピボット以下の要素のリスト (a <= x) とピボットより大きい要素のリスト (a > x) を取り出す。取り出した要素のリストには let 束縛で名前を付ける。それぞれのリストに quicksort 関数を再帰的に適用する。


今回は再帰について。