Works by

Ren's blog

@rennnosuke_rk 技術ブログです

【Golang】Goで画像処理: 画素勾配によるエッジ検出

画素勾配でエッジ検出

画像上に現れる物体の境界(エッジ)を検出する方法に、隣接する画素間の輝度の変化量を利用する方法があります。
この輝度の変化量が大きい部分をエッジとみなし、画像の分類や画像マッチングなどに使用できる特徴量として扱うことができます。

今回はこの方法を用いて、画像中のエッジを可視化してみました。

勾配

シンプルに輝度の勾配の大きさを求めるため、輝度勾配ベクトルのノルムを指標として使用します。

wピクセル、高さhピクセルの画像における、画素(i,j)の輝度をI_{ij}としたとき( 0 \leq i < w,  0 \leq j < h )、画素の輝度の勾配ノルム |\Delta|は以下のように求めます。

 d_x = I_{i + 1 , j} - I_{i, j}
 d_y = I_{i , j + 1} - I_{i, j}
 |\Delta| = \sqrt{d_x^2+d_y^2}

 d_x, d_yはそれぞれ水平・垂直方向の画素輝度差分を表しています。これらの二乗和平方根を輝度勾配ベクトルのノルム、すなわち勾配輝度の大きさとします。

実装

上記のエッジ検出をGoで実装しました。

画素(i,j)の右隣、下隣の画素との差分を求めているので、出力する輝度勾配ノルムの画像サイズは幅w-1、高さh-1となります。今回は簡単のため、末尾ピクセルi=wまたはj=hピクセル)はノルム0としました。
また、あくまで勾配の大きさを可視化するにとどめ、しきい値を決定した上でのエッジ判定は行いません。

package main

import (
    "image"
    "image/color"
    "image/jpeg"
    "math"
    "os"
)

// 勾配
type Gradient struct {
    dr int64
    dg int64
    db int64
    da int64
}

func main() {

    // 入力画像パス
    img, _ := jpeg.Decode(os.Stdin)

    // 出力画像
    bounds := img.Bounds()
    dest := image.NewRGBA(bounds)

    for y := bounds.Min.Y; y < bounds.Max.Y-1; y++ {
        for x := bounds.Min.X; x < bounds.Max.X-1; x++ {

            lt := img.At(x, y)
            rt := img.At(x+1, y)
            lb := img.At(x, y+1)

            dx := createGradient(&lt, &rt)
            dy := createGradient(&lb, &rt)

            d := createGradientNormRGBA(dx, dy)

            dest.Set(x, y, d)
        }
    }

    err := jpeg.Encode(os.Stdout, dest, nil)
    if err != nil {
        panic("Failed to encode JPEG gradient image.")
    }
}

// 画素RGBAの差分を求める関数
func createGradient(c1, c2 *color.Color) *Gradient {
    r1, g1, b1, a1 := (*c1).RGBA()
    r2, g2, b2, a2 := (*c2).RGBA()
    return &Gradient{
        int64(r2) - int64(r1),
        int64(g2) - int64(g1),
        int64(b2) - int64(b1),
        int64(a2) - int64(a1),
    }
}

// 画素のRGBA輝度勾配のノルムを求める関数
func createGradientNormRGBA(dx, dy *Gradient) *color.RGBA {
    dr := createGradientNorm(dx.dr, dy.dr)
    dg := createGradientNorm(dx.dg, dy.dg)
    db := createGradientNorm(dx.db, dy.db)
    da := createGradientNorm(dx.da, dy.da)
    return &color.RGBA{R: dr, G: dg, B: db, A: da}
}

// 輝度勾配のノルムを求める関数
func createGradientNorm(dx, dy int64) uint8 {
    d := math.Sqrt(float64(dx*dx + dy*dy))
    return uint8(float64(d) / math.Pow(2, 17) * 255)
}

出力画像はRGBAそれぞれの規模勾配ノルムを求めた上で、256階調のRGBA画像として出力したものとしています。 最終的な輝度はuint8に収まるように変換しているのですが、正規化のための最大値は2 ^17と決めでやっちゃってます。。。 ちゃんと正規化するなら、輝度勾配の最大値で正規化します。

結果

$ go run grad.go < gophar.jpeg > edge.jpeg

gophar.jpeg

f:id:rennnosukesann:20190815213704j:plain

edge.jpeg

f:id:rennnosukesann:20190816002917j:plain

入力した画像のエッジが取れているのが見て取れます。
Gopharくんの境界はやや太めなので、境界中に輝度勾配の変化のない領域があるのがわかりますね。

参考文献

ja.wikipedia.org

www.motorwarp.com

【Golang】Goで画像拡張子を変換する

goで画像処理を行う場合、image パッケージを使用することができます。
image/jpeg image/png パッケージの Encode 関数で拡張子を変換します。

package main

import(
    "os"
    "fmt"
    "bufio"
    "image"
    _ "image/jpeg"
    "image/png"
)

func main() {
    // 入力画像パス.
    scanner := bufio.NewScanner(os.Stdin)
    fmt.Print("Input source image file path  >>")
    if !scanner.Scan() {
        fmt.Println("Please input source image file path.")
        return;
    }
    srcPath := scanner.Text()

   // ファイルオープン
    file, err := os.Open(srcPath)
    assert(err, "Invalid image file path " + srcPath)
    defer file.Close()

    // ファイルオブジェクトを画像オブジェクトに変換
    img, _, err :=  image.Decode(file)
    assert(err, "Failed to convert file to image.")

    // 出力画像パス.
    fmt.Print("Input output image file path  >>")
    if !scanner.Scan() {
        fmt.Println("Please input output image file path.")
        return;
    }
    dstPath := scanner.Text()

    // 出力ファイルを生成
    out, err := os.Create(dstPath)
    assert(err, "Failed to create destination path.")
    defer out.Close()

    // 画像ファイル出力
//    jpeg.Encode(out, img, nil)
    png.Encode(out, img)

}

// errorオブジェクトをチェックし、nilの場合例外を送出
func assert(err error, msg string) {
    if err != nil {
        panic(err.Error() + ":" + msg)
    }
}

参考文献

golang.org

golang.org

【Spring】Spring Cache でキャッシュ機能を利用する

f:id:rennnosukesann:20181220183020p:plain

Spring Cacheとは

Spring Cacheは、Springでキャッシュ機能を使用するためのライブラリです。

Spring Cacheでは抽象的なキャッシュ機能の枠組みを提供します。そのため、内部の具体的な実装を気にせずに簡単にキャッシュ機能を使うことができます。
実装はデフォルト設定のほか、既存のキャッシュライブラリを使用することができます。例えば、CaffeineやJCacheなどをSpring Cache実装として選択することができます。

Spring Cacheのアーキテクチャ

具体的なキャッシュライブラリのことは考えず、まずはSpring Cacheのアーキテクチャを眺めてみます。
このアーキテクチャの概要は、Macchinetta Framework documentationで掲載されている図がとてもわかり易いので、構成だけ引用させていただきました。


f:id:rennnosukesann:20190810090751p:plain

青いアイコンが、Spring Cacheで提供される部分になります。

データ読み込み時の流れ

  1. データを参照するモジュールは、@Cacheable アノテーションがついたデータ供給用メソッドを呼び出す。
  2. Cache AOPが提供する @Cacheable にキー値が渡される。キー値はデフォルトで引数値の組み合わせとなる。Cache AOPはキー値をもとにCacheManagerにキャッシュ値の有無を問い合わせる。
  3. CacheManagerはキー値を元にハッシュテーブルに問い合わせる。キャッシュが存在すればキャッシュ値をCache AOPに返す。
  4. Cache AOPはキャッシュ値を受け取り、呼び出し元に返す。キャッシュがヒットしなかった場合、通常通りデータ供給用メソッドを実行し、その結果を返す。このとき、供給用メソッドの引数をキー値として(デフォルト設定であれば)、供給用メソッドの結果がハッシュテーブルに保存される。

Spring Cacheを構成するコンポーネント

Cache AOP

@Cacheable などCacheを有効化するためのアノテーションです。Cache機能の入り口となるインタフェースになります。この層が、キャッシュデータの使用元であるモジュールから、データがキャッシュされたものなのか、最新のデータなのかといった詳細を隠蔽します。 キャッシュ(コンピューティング) - Wikipedia)でも書かれていますが、

キャッシュは通常、隣接する層からは見えないように設計されている抽象化層です。

のように、通常キャッシュは隣接するデータ参照モジュールからはキャッシュの有無は隠蔽されます。 AOPという名がついているのはSpring AOPを使用した実装であるためで、Spring AOPはアプリケーション横断的に処理を挿入することを容易にします。この処理挿入は通常アノテーションで行われ、Spring Cacheもこのアノテーションによる処理挿入を利用しています。

CacheManager

Spring Cacheのキャッシュ処理をコントロールします。上図のキャッシュデータ引き当て処理の場合、CacheManagerはハッシュテーブルからキャッシュキーに対応するキャッシュ値を読み出し返します。CacheManagerはあくまでインタフェースであり、実装はキャッシュ実装によって変わります。

ハッシュテーブル

キャッシュ値をキーに紐付けて保存します。保存されたキャッシュがどこに格納されているのか、どのように管理されるのかはキャッシュ実装によって変わりますが、主にHashMap実装になります。

実装

では、実際にSpring Cacheを使用したキャッシュ処理を実装してみましょう。

Sprint Initializr上でSpring Web StartarとSpring Cacheを導入したプロジェクトを作成しました。言語選定はKotlinとしています。

https://start.spring.io/

プロジェクト内の構造は以下の通り。

 └── springcachedemo
     ├── ServletInitializer.kt
     ├── SpringCacheDemoApplication.kt
     ├── application
     │   └── controller
     │       └── ProductController.kt
     ├── config
     │   └── cache
     │       ├── CacheConfig.kt
     │       └── MyKeyGenerator.kt
     └── domain
         ├── entity
         │   └── Product.kt
         ├── repository
         │   └── ProductRepository.kt
         └── service
             └── ProductService.kt

依存関係は下記の通り。

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-cache")
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    runtime ("mysql:mysql-connector-java")
    providedRuntime("org.springframework.boot:spring-boot-starter-tomcat")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
}

キャッシュの検証用アプリとして、商品情報を取得するAPIを実装します。 また検証のためRDB(MySQL)上にテーブル PRODUCTS を用意し、その中に100000件のレコードを用意しました。

CREATE TABLE `PRODUCTS` (
  `ID` int(11) NOT NULL AUTO_INCREMENT,
  `NAME` varchar(45) COLLATE utf8mb4_general_ci NOT NULL,
  `PRICE` varchar(45) COLLATE utf8mb4_general_ci NOT NULL,
  PRIMARY KEY (`ID`)
) ENGINE=InnoDB AUTO_INCREMENT=100001 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
mysql> SELECT count(ID) FROM `cache-example`.PRODUCTS;
+-----------+
| count(ID) |
+-----------+
|    100000 |
+-----------+
1 row in set (0.01 sec)

次にコード実装に移ります。

まず、レコードをマッピングするオブジェクトのEntityクラスとして Product を定義します。

IDはAutoIncrementとしたので @GeneratedValue で明示的に値の自動生成を記述します。

@Entity
@Table(name = "PRODUCTS")
data class Product(

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "ID")
    val id: Int?,

    @Column(name = "NAME")
    val name: String,

    @Column(name = "PRICE")
    val price: Int

)

Product テーブルを抽象化するリポジトリ ProductRepository を用意。

@Repository
interface ProductRepository : JpaRepository<Product, Int>

ProductRepository を参照して商品情報を取得するサービスクラス ProductService では、商品情報取得メソッド getProducts()@Cacheable アノテーションを付加し、取得結果がキャッシュされるようにしています。 @Cacheable の引数としてキャッシュを識別するラベルを付与します。

@Service
class ProductService(
    @Autowired private val productRepository: ProductRepository
) {

    @Cacheable("getProducts")
    fun getProducts(): List<Product> = productRepository.findAll()

}

商品情報取得APIを定義した ProductController クラスを用意します。
呼び出し先の ProductService::getProducts() はキャッシュ設定が効いているので、二回目以降の呼び出しでは処理速度が変化するはずです。

@RestController
@RequestMapping("/products")
class ProductController(
    @Autowired private val productService: ProductService
) {

    @GetMapping("/")
    fun getProducts() = productService.getProducts()

}

最後に、 @SpringBootApplication を付加したクラスに @EnableCaching アノテーションを付加して、アプリケーションに対するSpring Cache機能を有効化します。

@SpringBootApplication
@EnableCaching
class SpringCacheDemoApplication

fun main(args: Array<String>) {
    runApplication<SpringCacheDemoApplication>(*args)
}

この状態でアプリケーションを起動し、以下のAPIをSpring Cache設定有り・無しの状態で3回ほど実行してそのレスポンスタイム(リクエストを送信してから、応答が返るまで、データ整形を除く)を見ます。なお、キャッシュなしの場合でも別の部分でキャッシュが効いている可能性があるため、複数回試行します。

$ curl http://localhost:8080/api/v1/products/ | jq '.[]'
{
  "id": 1,
  "name": "prod0",
  "price": 0
}
{
  "id": 2,
  "name": "prod1",
  "price": 100
}

jq

キャッシュ無効

1st 1307 ms
2nd 618 ms
3nd 451 ms

キャッシュ有効

1st 1277 ms
2nd 61 ms
3nd 53 ms

キャッシュ前と比較し、キャッシュ後のレスポンスタイムは約10分の1まで短くなりました。
他の実装でも線形にこの速度短縮が適用されるわけではありませんが、十分キャッシュの効果は現れているといえるでしょう。

Spring Cacheの主な機能

@Cacheable

Cacheable (Spring Framework 5.1.9.RELEASE API)

メソッド(またはクラス内のすべてのメソッド)を呼び出した結果をキャッシュできることを示す注釈。推奨メソッドが呼び出されるたびに、キャッシュ動作が適用され、指定された引数に対してメソッドが既に呼び出されているかどうかがチェックされます。賢明なデフォルトでは、単にメソッドのパラメーターを使用してキーを計算しますが、SpEL式はkey()属性を介して提供するか、カスタム KeyGenerator実装でデフォルトの実装を置き換えることができます(keyGenerator()を参照)。

(Cacheable (Spring Framework 5.1.9.RELEASE API) より)

@Cacheableをメソッドに付加することで、その返戻値の結果をキャッシュすることができます。キャッシュストレージや格納データ構造は後述するキャッシュ実装に任せます。

    @Cacheable("getProducts")
    fun getProducts(): List<Product> = productRepository.findAll()

キャッシュキー

Spring Cacheでは、キャッシュの結果に対し一意のキーを設定します。
このキーの値はデフォルトではキャッシュの引数を元に生成されます。
ただし、引数 keySpEL式と呼ばれる形式で独自のキーを設定することもできます。

@Cacheable(value = ["getProduct"], key = "'products/' + #id")
fun getProduct(id: Int): Product = productRepository.findById(id).orElseThrow()

上記の例では、引数 key に 'products/' という文字列と、引数である id を結合した文字列を設定した例となっています。
keyに渡す文字列内では、静的文字列はシングルクォートで囲む必要があります(これをしないと、変数として扱われてしまいます)。

KeyGenerator

@Cacheableデフォルトキーの生成規則をカスタマイズできます。
KeyGenerator インタフェースを実装したクラスを定義し、メソッド generate 内でどのようなキーを生成するかを定義します。

class MyKeyGenerator : KeyGenerator {
    override fun generate(target: Any, method: Method, vararg params: Any?): Any {
        return target.javaClass.simpleName + "/" + method.name + "/" + params.joinToString()
    }
}

生成したKeyGeneratorはBeanとして登録し、 @Cacheable の引数 keyGenerator に設定したBean名を渡せば自前のKeyGeneratorが適用されます。

@Configurationclass CacheConfig : CachingConfigurerSupport() {
    @Bean("MyKeyGenerator")    override fun keyGenerator(): KeyGenerator? = MyKeyGenerator()
}
@Service
class ProductService(@Autowired private val productRepository: ProductRepository) {
    @Cacheable("getProducts", keyGenerator = "MyKeyGenerator")
    fun getProductsWithMyKeyCache(): List<Product> = productRepository.findAll()
}

CacheManager

Springのキャッシュ機能を管理するクラスです。キャッシュの引当処理と、キャッシュに紐づくキーの取得機能を提供します。CacheManager実装では、キャッシュの有効期限などより細かい機能を提供します。
CacheManagerを用意せずとも @Cacheable アノテーションでキャッシュ機能を使うことができますが、@Cacheable の引数にCacheManagerを渡すことで、引数に渡したCacheManagerの管理の元、キャッシュ機能を使用することができます。 以下の例では、 SimpleCacheManager クラスをインスタンス化し、それを明示的に @Cacheable に引き渡しています。

@Configurationclass CacheConfig : CachingConfigurerSupport() {
    @Bean("MyCacheManager")
    override fun cacheManager(): CacheManager? = SimpleCacheManager()
}

先程の KeyGenerator 同様、定義した CacheManager Beanの名称を @Cacheable アノテーション引数に渡します。

@Service
class ProductService(@Autowired private val productRepository: ProductRepository) {
    @Cacheable("getProducts", cacheManager = "MyCacheManager")
    fun getProducts(): List<Product> = productRepository.findAll()
}

上記の SimpleCacheManager はデフォルトで使用されるCacheManager実装ですが、このキャッシュマネージャを独自にカスタマイズしたり、キャッシュライブラリの実装に基づいたCacheManagerも使用することができます。

@CachePut

キャッシュの値を更新します。 @Cacheable で指定したラベル値を指定し、それを更新するメソッドに付与します。

CachePut(Spring Framework 5.1.9.RELEASE API)

メソッド(またはクラスのすべてのメソッド)がcache put操作をトリガーすることを示す注釈 。 @Cacheable注釈とは対照的に、この注釈は推奨されるメソッドをスキップさせません。むしろ、常にメソッドが呼び出され、その結果が関連付けられたキャッシュに保存されます。Java8のOptional戻り値の型は自動的に処理され、そのコンテンツはキャッシュに保存されます(存在する場合)。

キャッシュは @CachePut を付与したメソッドの戻り値によって更新され、引き続き更新後の値がキャッシュされます。

    @CachePut("getProduct", key = "'products/' + #product.id")
    fun putProduct(product: Product) = productRepository.save(product)

@CacheEvict

付加したメソッドを呼び出すと、指定したキーに紐づくキャッシュを削除します。

主に不要なキャッシュを能動的に削除したい場合に使用します。 例えば、外部からキャッシュを削除するAPIを定義し、 @CacheEvict を付加したServiceメソッドを呼び出していつでも外からキャッシュ値を削除する・・・といったことができます。

    @CacheEvict("getProduct", key = "'products/' + #id")
    fun deleteProducts(id: Int) = productRepository.deleteAll()

参考文献

【Spring】JSONリクエストのBoolean型以外のパラメータを暗黙的にBooleanに変換させない

f:id:rennnosukesann:20181220183020p:plain:w400

Spring2ではJSONリクエスト/レスポンスボディのマッピングの際、Boolean型メンバに非Booleanの値をマッピングしようとしたときに正常に処理を終えてしまいます。
例えば、"1" や "true" という文字列を渡してもtrueに変換され、 "0" や "false" 文字列を渡すとfalseに変換されるといった塩梅です。

これは fasterxml/Jacksonのデフォルト仕様で 、このマッピングを防ぐにはSpringで使用されるObjectMapperの設定を修正する必要があります。

環境

  • Kotlin: 1.3.21
  • Spring: 2.1.4 RELEASE
  • JDK : OpenJDK11.0.2

ソースはKotlinです。

Deserializerを定義

JSONリクエストパラメータをKotlin内オブジェクトにマッピングする際の規則を明示的に記述する Deserializer クラスを定義します。

BooleanDeserializer
class BooleanDeserializer : JsonDeserializer<Boolean>() {
    // JSONパラメータをBooleanにマッピングしようとすると呼ばれる
    override fun deserialize(jsonParser: JsonParser, ctxt: DeserializationContext): Boolean {
        return jsonParser.booleanValue // booleanValue : booleanでない値のときに参照されるとJsonParseError
    }
}

deserialize メソッドはJSONパラメータをBooleanにマッピングしようとすると呼ばれます。このとき、 booleanでない値をマッピングしようとするとJsonParseErrorが呼ばれるので、制御したい場合は適宜ハンドリングしてください。

DeserializerをObjectMapperに適用する

次に作成したDeserializerをSpringが使用するObjectMapperに設定し、前述の変換規則が適用されるようにします。

JsonConfig
@Configuration
class JsonConfig {

    @Bean
    fun objectMapperBuilder(): Jackson2ObjectMapperBuilder {
        val builder = Jackson2ObjectMapperBuilder()
        return builder
    }

    @Bean
    fun objectMapper(): ObjectMapper {
        val objectMapper = objectMapperBuilder().build<ObjectMapper>()
        objectMapper.registerModule(createModule())
        return objectMapper
    }

    private fun createModule(): Module {
        val module = SimpleModule()
        // boolean型以外を暗黙的に許可しない
        module.addDeserializer(Boolean::class.java, BooleanDeserializer())
        return module
    }

}

Springで使用される ObjectMapper 及び ObjectMapperBuilderを、@Bean 宣言したメソッドで上書きしています。 さらに createModule() 内で BooleanDecelializer を登録した Module オブジェクトを生成し、それを ObjectMapper に登録しました。

【Cloud Foundry】Cloud Foundryアプリケーションのインスタンスを明示的に指定してHTTPリクエストを送る

f:id:rennnosukesann:20190205125549p:plain:w300

Cloud Foundryインスタンスを複数立ち上げたときに、特定のインスタンスにアクセスするやり方のメモです。

X-CF-APP-INSTANCE ヘッダーでインスタンスを指定

Cloud Foundry上にデプロイしたアプリケーションにアクセスするとき、 X-CF-APP-INSTANCE ヘッダーをつけることでアクセスするインスタンスを指定することができます。

$ curl --request GET --url http://localhost:8080/api/v1/hoges/1 
  --header 'X-CF-APP-INSTANCE: 6aa4cf0f-1dd4-29b6-af62-cc21bc23df10:1'

X-CF-APP-INSTANCE の値は {アプリのGUID}:{インスタンスインデックス} のフォーマットで指定します。上記例では、インデックスの採番順序で2番目(インデックスは0開始)のアプリケーションインスタンスへアクセスしています。

ちなみに、アプリのGUIDは cf app {アプリ名} --guid で取得できます。

$ cf app hoge-app --guid
6aa4cf0f-1dd4-29b6-af62-cc21bc23df10

また、インスタンスのインデックスはインスタンス環境変数 $CF_INSTANCE_INDEX で取得可能です。

参考文献

docs.cloudfoundry.org

docs.cloudfoundry.org

【Spring】Spring Sessionでセッション情報をDb2上に保存する

f:id:rennnosukesann:20181220183020p:plain

Spring でのセッション

SpringではJava ServletのHttpSessionの仕組みを利用することができます。ServletのHttpSessionはSpringフレームワークの上に構築されたアプリケーション上でそのまま利用できるほか、Springの各種ライブラリによってラッピングされた形でも利用することができます。

アプリケーションサーバを複数台構成とした場合のセッション

特に設定を行わなかった場合、Servletのセッションはメモリ上で管理されます。もし複数台のサーバや複数のクラウドインスタンスにアプリケーションがデプロイされていると、各インスタンス間で個別のメモリ領域を持つことになり、互いにセッションの情報が共有されません。そのため、例えばあるインスタンス上アプリケーションでログインに成功したユーザが、別のインスタンス上アプリにアクセスすると未ログイン状態として扱われてしまう・・・といったことが起こります。

このような問題に対する解決策はいくつかあるのですが、今回はDB上にセッション情報を保存する方法を紹介します。

適用方法

今回はDBMSとしてDb2を使用します。 またSpringアプリケーションのビルドツールとしてGradleを利用し、 すでにDBMSへの接続は完了しているものとします。

Spring Sessionの導入

セッションの管理をDB上で行えるようにします。 build.gradle に下記パッケージを追加します。

dependencies {
    ....
    implementation('org.springframework.boot:spring-boot-starter-web')
    implementation('org.springframework.boot:spring-boot-starter-data-jpa')
    implementation("org.springframework.session:spring-session-core")
    implementation("org.springframework.session:spring-session-jdbc")
    ....
}

Configuration Beanクラスの定義

@EnableJdbcHttpSession アノテーションを付加した Config クラスを定義します。これにより、JDBC経由でDB上でセッションを管理できるようになります。

import javax.sql.DataSource;

import org.springframework.context.annotation.Bean;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.session.jdbc.config.annotation.web.http.EnableJdbcHttpSession;
import org.springframework.transaction.PlatformTransactionManager;

/**
 * 
 * <p>
 * セッション情報をJDBC経由でDB上で管理するためのConfiguration
 * </p>
 * 
 */
@EnableJdbcHttpSession
public class JdbcHttpSessionConfig {
  @Bean
  public PlatformTransactionManager transactionManager(DataSource dataSource) {
    return new DataSourceTransactionManager(dataSource);
  }
}

Spring設定ファイルの変更

application.properties に下記項目を追加します。

# Session Setting in DB
server.session.jdbc.initialize-schema=always
server.session.jdbc.table-name=SPRING_SESSION

application.yml の場合は以下。

server:
    sesion:
        jdbc:
            initialize-schema: always
            table-name: SPRING_SESSION

2019/06/02追記

application.yml application.properties なしでも動作します。 テーブル名をデフォルトの SPRING_SESSION から変更したい場合 @EnableJdbcHttpSession のプロパティ tableName に値を渡します。

@EnableJdbcHttpSession(tableName = "APP_SESSION")

テーブルの追加

設定は上記で完了なのですが、このままではセッションを保存できません。 既存のDBにセッション情報を追加するテーブルを定義する必要があります。

Spring SessionのGitHubリポジトリDBMS別のテーブルCREATE用SQLがおいてあるので、こちらを活用しましょう。

ex. Db2の場合(少し編集してあります):

CREATE TABLE SPRING_SESSION (
    PRIMARY_ID CHAR(36) NOT NULL,
    SESSION_ID CHAR(36) NOT NULL,
    CREATION_TIME BIGINT NOT NULL,
    LAST_ACCESS_TIME BIGINT NOT NULL,
    MAX_INACTIVE_INTERVAL INT NOT NULL,
    EXPIRY_TIME BIGINT NOT NULL,
    PRINCIPAL_NAME VARCHAR(100),
    CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (PRIMARY_ID)
)
ORGANIZE BY ROW;

CREATE UNIQUE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (SESSION_ID);
CREATE INDEX SPRING_SESSION_IX2 ON SPRING_SESSION (EXPIRY_TIME);
CREATE INDEX SPRING_SESSION_IX3 ON SPRING_SESSION (PRINCIPAL_NAME);

CREATE TABLE SPRING_SESSION_ATTRIBUTES (
    SESSION_PRIMARY_ID CHAR(36) NOT NULL,
    ATTRIBUTE_NAME VARCHAR(200) NOT NULL,
    ATTRIBUTE_BYTES BLOB NOT NULL,
    CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_PRIMARY_ID, ATTRIBUTE_NAME),
    CONSTRAINT SPRING_SESSION_ATTRIBUTES_FK FOREIGN KEY (SESSION_PRIMARY_ID) REFERENCES SPRING_SESSION(PRIMARY_ID) ON DELETE CASCADE
)
ORGANIZE BY ROW;

Db2は列指向*1を定義する機能を持ち、一部データ分析用DBではこの機能がデフォルトになっていたりします(IBM Cloud Db2 Warehouseなど)。この方針のままだと、BLOB 型定義をサポートしていない仕様のため*2、上記SQLでは元のSQLに明示的に ORGANIZE BY ROW を指定し、メジャーなRDBMS同様の行指向操作がされるようにしています。


2019/06/02追記

@EnableJdbcHttpSession にてテーブル名を変更した場合、上記SQLでもテーブル名を変更します。


これで、DB上にセッションが保持されるはずです!

f:id:rennnosukesann:20190204174125p:plain

参考文献

qiita.com

qiita.com

*3 ja.wikipedia.org

*1:列指向の概念をよくわかっていない...
曰く、通常のRDBMSで行単位での操作をするように、列単位での操作を主とするらしい

*2:https://www-01.ibm.com/support/docview.wss?uid=swg22003176

*3:完全にDBMS=RDBMSの文脈前提で語っている...

【Grafana】Grafanaでアラート時通知を設定する

f:id:rennnosukesann:20190129142110p:plain

Grafanaで可視化対象になっている監視サーバが障害を検知したら、Grafana側でメールやWebhookなどの通知を行うための設定メモです。

Grafanaでメール通知を設定する

ダッシュボード左上のGrafanaアイコンをクリックし、プルダウンから「Alerting」->「Notification channels」をクリック。 f:id:rennnosukesann:20190129110135p:plain

「+New Channel」をクリック。

f:id:rennnosukesann:20190129110402p:plain

「Name」に設定のタイトルを任意に入力。
「Description」には設定内容の概要を任意に入力。
「Type」はEmailを選択。
「Email ID」に送信したいメールアドレスを入力してください。

f:id:rennnosukesann:20190129111342p:plain

「Send Test」を押すと試しに「Email ID」に指定したメールアドレス宛にメールを送ることができます。

メールが受け取れることが確認できたら「Save」をクリック。簡単。

監視対象メトリクスと通知設定を紐付ける

メールの設定が完了したら、今度はメール送信のトリガーとなるアラートを設定します。

アラートを設定したいメトリクスのグラフを選択肢、タイトルをクリックします。クリックして表示されるEditボタンをクリックします。

f:id:rennnosukesann:20190129164016p:plain

出現するGraph設定用ボードの「Alert」タブを開き、「Create Alert」をクリック。

f:id:rennnosukesann:20190129164057p:plain

「Alert Config」でアラートのタイトル・しきい値を設定します。
「Conditions」ではアラート条件を指定します。「WHEN」にはグラフ中のどのタイミングでの値を参照するか決定します。デフォルトは last() で、グラフの最右端の値を参照します。「or」句で他の条件を指定しています。

「IS BELOW」の部分ではメトリクス上で参照する値のしきい値を設定しています。「IS BELOW」であれば参照値がしきい値を下回ったときに、「IS ABOVE」であれば参照値がしきい値を上回った場合にアラートを行います。

f:id:rennnosukesann:20190129164132p:plain

下のプルダウンでは、値が未定・nullのとき/実行時エラー・タイムアウトのときにアラートを行うかどうかを設定できます。

これで設定完了です。

参考文献

Grafana Conditionの設定

docs.grafana.org