Gobble up pudding

プログラミングの記事がメインのブログです。

MENU

Spring Bootの外部設定値(application.yml)のプロファイルの優先順について

f:id:fa11enprince:20140905165611j:plain Spring Bootのapplication.ymlの仕組み素敵ですよね。
かなり柔軟性がある。
あるときにアプリケーションをMariaDBとMySQLでどちらも対応できるように外部設定値(Externalized Configuration)
であるapplication.ymlにて制御しようとしたときにハマったので記録に残します。

※この環境はSpring Boot 1のころのHikariCPでない設定の場合ですので注意

背景

もともとMariaDBを使用していたが、宗教的な理由からMySQLを使う必要が出てきた。 MariaDBをデフォルトとし、MySQLは適宜切り替えるようにしたいと思った。

基本的に優先順位はここに書いてある。
https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-external-config

環境変数かシステムプロパティでactiveなprofileを切り替えてやればうまくいくはず!
ということで設定ファイルを新たに作った。

最初に思いついた方法

application.ymlがもともとあり、 この中にDB設定も入っているとする(実際にはもっと複雑で分割されていたが…)

application.yml

app:
  myProperty:
    ipAddress: 127.0.0.1
    port: 8080 
spring:
  jpa:
    properties:
      hibernate:
        show_sql: false
        use_sql_comments: false
        format_sql: false
  datasource:
    url: jdbc:mariadb://localhost:3306/mysql
    username: root
    password: root
    driverClassName: org.mariadb.jdbc.Driver
    tomcat:
      max-active: 15
      max-age: 60000
      max-idle: 2
      max-wait: 10000
      min-idle: 2
      initial-size: 2
      test-on-borrow: true
      test-on-return: false
      test-while-idle: true
      validation-query: "SELECT 1"
      validation-query-timeout: 1000

こんなのがあったとして、 環境変数が空の場合はMariaDBのドライバを使って接続

上書きしたい文だけMySQLの差分を用意してmysqlを作ればいいんだ! そんなふうに思っていました。

※これは古いバージョンのコネクションプールの周りの設定なので、
Spring Boot2以降でHikariCPの場合はこちらを参照のこと。
GitHub - brettwooldridge/HikariCP: 光 HikariCP・A solid, high-performance, JDBC connection pool at last.

application-mysql.yml

spring:
  datasource:
    url: jdbc:mysql://localhost:3307/mysql
    username: root
    password: root
    driverClassName: com.mysql.jdbc.Driver
    tomcat:
      max-active: 15
      max-age: 60000
      max-idle: 2
      max-wait: 10000
      min-idle: 2
      initial-size: 2
      test-on-borrow: true
      test-on-return: false
      test-while-idle: true
      validation-query: "SELECT 1"
      validation-query-timeout: 1000

これでガサっとMySQLのとき(環境変数SPRING_PROFILES_ACTIVEがmysqlのものがあるとき)は MySQLに書き換えられる! ヤッター!!SUCCESS!!だと思っていたのですが… これ、気まぐれな挙動を見せます。

どういうことだ…

デフォルトが勝つ場合とmysqlが勝つ場合がある… じゃあ環境変数やめてシステムプロパティから -Dspring.profiles.active="default,dev" のように指定すればイケるか?と思ったら、 この指定は全く優先順位に関係がない。

調べると、やっぱりダメみたい。

https://stackoverflow.com/questions/23617831/what-is-the-order-of-precedence-when-there-are-multiple-springs-environment-pro

ベストアンサーになっていた人の主張するベストプラクティスはこんな感じ

1. プロファイルに固有ではない、「デフォルト」のBean定義する
2. 環境固有のプロファイルでのBean定義のオーバーライド
3. テスト固有のプロファイル内のBean定義をオーバーライドする

つまり、固有のものは必ず分けろってことです。

検証コードを書きました

以下抜粋です application.yml

app:
  myProperty:
    ipAddress: 127.0.0.1
    port: 8080 
spring:
  jpa:
    properties:
      hibernate:
        show_sql: false
        use_sql_comments: false
        format_sql: false

application-mysql.yml

spring:
  datasource:
    url: jdbc:mysql://localhost:3307/mysql
    username: root
    password: root
    driverClassName: com.mysql.jdbc.Driver
    tomcat:
      max-active: 15
      max-age: 60000
      max-idle: 2
      max-wait: 10000
      min-idle: 2
      initial-size: 2
      test-on-borrow: true
      test-on-return: false
      test-while-idle: true
      validation-query: "SELECT 1"
      validation-query-timeout: 1000

aplication-mariadb.yml

spring:
  datasource:
    url: jdbc:mariadb://localhost:3306/mysql
    username: root
    password: root
    driverClassName: org.mariadb.jdbc.Driver
    tomcat:
      max-active: 15
      max-age: 60000
      max-idle: 2
      max-wait: 10000
      min-idle: 2
      initial-size: 2
      test-on-borrow: true
      test-on-return: false
      test-while-idle: true
      validation-query: "SELECT 1"
      validation-query-timeout: 1000

※本当はinclude等を使えばtomcatの部分はすっきりすると思います。

エントリポイントのコード

package com.example.externalconfig;

import java.util.Arrays;
import java.util.stream.Collectors;

import org.apache.commons.lang3.ArrayUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.Environment;

import com.example.externalconfig.config.AppConfig;

@SpringBootApplication
public class ExternalConfigApplication {
    
    @Autowired
    Environment env;
    
    @Autowired
    AppConfig appConfig;

    public static void main(String[] args) {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(ExternalConfigApplication.class, args)) {
            ExternalConfigApplication app = ctx.getBean(ExternalConfigApplication.class);
            app.printProperties();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void printProperties() {
        System.out.println("-------------------------------------------");
        // 今回の場合は環境変数で指定したActiveなProfileを取得
        String[] profiles = env.getActiveProfiles();
        // 以下、Environment経由でPropertyを取得
        if (!ArrayUtils.isEmpty(profiles)) {
            System.out.println("active profiles : " + Arrays.stream(profiles).collect(Collectors.joining(",")));
            System.out.println("spring.datasource.url : " + env.getProperty("spring.datasource.url"));
            System.out.println("spring.datasource.driverClassName : "
                    + env.getProperty("spring.datasource.driverClassName"));
            System.out.println("spring.datasource.tomcat.max-age : "
                    + env.getProperty("spring.datasource.tomcat.max-age"));
        }
        System.out.println("spring.jpa.properties.hibernate.show_sql : "
                + env.getProperty("spring.jpa.properties.hibernate.show_sql"));
        System.out.println("-------------------------------------------");
        // @ConfigurationProperties経由で読込み
        System.out.println("app.myProperty.ipAddress : " + appConfig.getMyProperty().getIpAddress());
        System.out.println("app.myProperty.port : " + appConfig.getMyProperty().getPort());
        System.out.println("-------------------------------------------");
    }

}

結果

SPRING_PROFILES_ACTIVEがmariadbのとき

-------------------------------------------
active profiles : mariadb
spring.datasource.url : jdbc:mariadb://localhost:3306/mysql
spring.datasource.driverClassName : org.mariadb.jdbc.Driver
spring.datasource.tomcat.max-age : 60000
spring.jpa.properties.hibernate.show_sql : false
-------------------------------------------
app.myProperty.ipAddress : 127.0.0.1
app.myProperty.port : 8080
-------------------------------------------

SPRING_PROFILES_ACTIVEがmysqlのとき

-------------------------------------------
active profiles : mysql
spring.datasource.url : jdbc:mysql://localhost:3307/mysql
spring.datasource.driverClassName : com.mysql.jdbc.Driver
spring.datasource.tomcat.max-age : 60000
spring.jpa.properties.hibernate.show_sql : false
-------------------------------------------
app.myProperty.ipAddress : 127.0.0.1
app.myProperty.port : 8080
-------------------------------------------

その他参考リンク

[Qiita]Spring-Bootの設定プロパティと環境変数 https://qiita.com/NewGyu/items/d51f527c7199b746c6b6

[Qiita]Spring Boot の application.properties (yml) でプロパティが重複したときの挙動 https://qiita.com/yo1000/items/c511e7f9ff59ab8c3ce3 →ただしこれはあくまでincludeした時の挙動

[Qiita]Spring Bootの外部設定値の扱い方を理解する https://qiita.com/kazuki43zoo/items/0ce92fce6d6f3b7bf8eb

スーパークラスで呼び出されているメソッドのサブクラスでのオーバーライドについて

f:id:fa11enprince:20180929032631j:plain

TL;DR

スーパークラスのメソッドはサブクラスでオーバーライドすれば
サブクラスのインスタンス経由で呼び出すと、
たとえスーパークラスでそのメソッドが呼び出されていても当然上書きされる。

経緯

とあるライブラリをカスタマイズしたくて、
そういや、スーパークラスのメソッドってオーバーライドすれば
たとえ、スーパークラス内で呼び出されていたものだったとしても、
サブクラスでオーバーライドしていればオーバーライドしているほうで呼び出されるよね…
って、日本語でうまく説明できているかどうか怪しいですが、
しばらく継承なんか使ってないこともあり、不安になりました。 自分で書くアプリだとよほどフレームワークよりとか、凝ったゲームとか使わない限り
継承はアンチパターンなので使わないですよね。 というわけで検証しました。

クラス図

f:id:fa11enprince:20181124051349p:plain

クラス図はObject Aid UMLというecliseのプラグインで書きました http://www.objectaid.com/home

検証コード

検証コードはJavaです Javaに慣れているせいかvirtualと書いてOverridableにするよりもfinalで封じるほうが好きになってきた。 まぁでもC++の場合は思想が違うからな…でもC#はvirtualなんだよな…。 そのモデリングおかしいだろというのは重々承知ですが、サンプルということなので。

// ここのメソッドいろいろおかしいですが気にせず
abstract class Animal {
    abstract public void greet();
    protected void say() {  // 継承可能
        System.out.println("");
    }
}

class Human extends Animal {
    public Human() { }
    @Override
    public final void greet() {  // 挨拶  継承不可
        //super.say(); // これだと当然いろいろ破綻する
        say();   // ライブラリとかでこうなってる場合、こいつを書き換えたいなーと思うことがあるわけ
    }
    @Override
    protected void say() {  // 継承可能
        System.out.println("\"Hello.\"");  // 英語なのはなんか変だが…
    }
}

class Japanese extends Human {
    public Japanese() { }
    @Override
    protected void say() {
        System.out.println("「こんにちは」");
    }
}

class Otaku extends Japanese {
    public Otaku() { }
    @Override
    protected void say() {
        System.out.println("「ふひひ、こんにちわ」");
    }
    public void majimeNiAisatsu() {
        super.say();
    }
}

public class Sample {
    public static void main(String[] args) {
        Human human = new Human();
        human.greet();   // => "hello."
        
        Japanese japanese = new Japanese();
        japanese.greet();  // => 「こんにちは」
        
        Otaku otaku = new Otaku();
        otaku.greet();   // => 「ふひひ、こんにちわ」
        otaku.majimeNiAisatsu();  // => 「こんにちは」
        
        // おまけ
        Human otaku2 = new Otaku();
        otaku2.greet();   // =>  「ふひひ、こんにちわ」 当然、参照によって決まるのでこうなる
    }
}

うーん相変わらず低レベルな記事だ。まあいいか。

2018年のReact最小構成の構築(非SPA対応)

f:id:fa11enprince:20180815055023j:plain

今回作成する構成

  • React v16
  • Babel Core v6
  • Webpack v4

Reactの勉強を再開しようと思い、だいぶ前回から期間が開いてしまい、
最近のReact環境どうなんだってのがさっぱりわからず、
また、割とブラックボックスなcreate-react-appを使ってReact勉強しても初心者にあまりよくないのかも…
と思い、見よう見まねでReactの最小構成を作ってみました。
もちろんReduxは使ってません。
どうしてこういう構成にしたかというと、おそらくですが、まだフルでSPAを作るケースってそれほどないと思うのですよね。 ということでたぶん、既存のサーバサードでレンダリングされた各ページに動きをちょい足しするような使い方が多いと思いますので、 bundleも分割できるような作りにしました。
ひょっとするとこの辺りの用途はもうVue.js一択なのかもしれませんが、
そこそこ歴史があり、Reactには結構便利なコンポーネントが転がってるのでそこはVue.jsより優位性があるのではないのかと。

フロントエンド回りはだいぶ落ち着いてきた感があります。
Reactの勉強をするつもりが今回はほぼWebpack4の勉強になってしまいました…まぁいいか。
自分は最近Angularばかり触っていてReactにはかなり疎いので、
間違いやこうしたほうが良い等ありましたらご意見いただけると幸いです。
Angularはこのあたりほぼ何も考えなくてよいので楽ですね。

事前準備

開発環境を整えます。VS Codeがないとフロントエンドは始まりません。

VS Codeを入れる

これがないと始まりません
DLしてインストールしましょう
https://code.visualstudio.com/
Atomでもいいですが、AtomはVS Codeに完敗してしまった感があります

入れたほうが良いプラグイン

  • vscode-styled-jsx
  • Beautify
  • ESLint

Node.jsを入れる

DLしてインストール https://nodejs.org/ja/

webpackコマンドを叩けるようにする

$ npm install -g webpack

Reactプロジェクトを作成する

$ mkdir react-boilerplate

packageを追加する

$ cd $_
$ npm init -y
$ npm install --save react react-dom
$ npm install --save-dev webpack babel-loader babel-core babel-preset-react babel-preset-env babel-preset-stage-2 webpack-cli webpack-dev-serve

ソースコードを書く

ここを参考に階層を作成してください
若干フォルダ構成がSpring-Bootなのはご容赦ください。 下記にあるように src/main/client配下がReactのjsxを作成する領域
src/main/resources配下のpublicとstaticがwebサーバで公開する領域
として作成します。 今回は
index.htmlと
subpage/index.htmlのReactを用いたページを作成します。

src配下

src
└── main
    ├── client
    │   ├── components
    │   │   └── hello.jsx
    │   ├── index.jsx
    │   └── subpage
    │       ├── components
    │       │   └── world.jsx
    │       └── index.jsx
    └── resources
        ├── public
        │   ├── index.html
        │   └── subpage
        │       └── index.html
        └── static
            └── react
                この配下にトランスパイルされたjsファイルが生成される

 
src/main/resources/public/index.html

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>React Test</title>
    </head>
    <body>
        <div id="app" />
        <!-- NOTE: webpack-dev-server root path is src/main/resources -->
        <script src="/static/react/index.js"></script>
    </body>
</html>

 
src/main/resources/public/subpage/index.html

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>React Subpage Test</title>
    </head>
    <body>
        <div id="app" />
        <!-- NOTE: webpack-dev-server root path is src/main/resources -->
        <script src="/static/react/subpage/index.js"></script>
    </body>
</html>

 
src/main/client/components/hello.jsx

import React, { Component } from 'react';

export default class Hello extends Component {
    render() {
        return (
            <h1>Hello React World!</h1>
        );
    }
}

 
src/main/client/index.jsx

import React from 'react';
import { render } from 'react-dom';

import Hello from './components/hello';

render(
    <div>
        <Hello />
    </div>,
    document.getElementById('app')
);

 
src/main/client/subpage/components/world.jsx

import React, { Component } from 'react';

export default class World extends Component {
    render() {
        return (
            <h1>Subpage Hello React World!</h1>
        );
    }
}

 
src/main/client/subpage/index.jsx

import React from 'react';
import { render } from 'react-dom';

import World from './components/world';

render(
    <div>
        <World />
    </div>,
    document.getElementById('app')
);

 
注意
import { Hoge } from './components/hoge';

import Hoge from './components/hoge';
は意味が違うので注意
Angularのときここは意識していなかった・・・
defaultがついたクラスだと後者でimportしないと
Warning: React.createElement: type is invalid -- expected a string
という警告がでて、エラーになる。

package.jsonに追記する

$ vi package.json

...
  "description": "",
  "scripts": {
    "start": "webpack-dev-server --mode development --progress --colors --hot --open",
    "watch": "webpack --mode development --watch --progress --colors --hot",
    "build-dev": "webpack --mode development --progress --colors --hot",
    "build": "webpack --mode production --progress --colors"
  },
...
  "license": "MIT",
  "babel": {
    "presets": [
      "env",
      "react",
      "stage-2"
    ]
  },
  "dependencies": {
...

webpack.config.jsを書く

$ touch webpack.config.js
$ vi webpack.config.js
const path = require('path');

module.exports = {
    entry: {
        'index': path.resolve(__dirname, 'src/main/client/index.jsx'),
        'subpage/index': path.resolve(__dirname, 'src/main/client/subpage/index.jsx'),
    },
    output: {
        path: path.resolve(__dirname, 'src/main/resources/static/react'),
        filename: '[name].js'
    },
    module: {
        rules: [
            {
                test: /\.(js|jsx)$/,
                exclude: /node_modules/,
                use: 'babel-loader',
            }
        ]
    },
    resolve: {
        extensions: ['*', '.js', '.jsx'],
    },
    devServer: {
        contentBase: path.resolve(__dirname, 'src/main/resources'),
        port: 3000,
    },
}

※Macだと、バックスラッシュと円マークが違うので注意!
macだとbackslashと円マークは別物なので注意
Optionキー+円マーク
ちなみに間違えると・・・
"You may need an appropriate loader to handle this file type”
のエラーが出続けてハマった・・・
Macではバックスラッシュ()と円マーク(¥)を区別するのを忘れていて、
正規表現の部分を円マーク(¥)で書いていた・・・・・・

resolveのextentionsを書くことによりjsxでimport時に拡張子を省略できる

path.resolve, path.join, __dirnameを知らない人はググることをおすすめ(私もよく知らなかったのでググりました)。
ざっくりいうと__dirnameは現在動かしているカレントのディレクトリ的なものです。
resolve, joinは動きが違いますが、パスをconcatする的なものです。

Reactの動作確認をする

まずはビルドする

$ npm run watch

別ウィンドウで

$ npm run start

これでwebpack-dev-serverが立ち上がります。 http://localhost:3000/public/
http://localhost:3000/public/subpage/
をたたくと、作ったものが表示されます。

プロダクション用のビルドはnpm run buildで!
以上です!!

完成したものはここです。

参考リスト

minimai-react-webpack-babel-setup
React simple boilerplate

その他関連情報

webpack tutorial
react-webpack-babel tutorial
webpack-dev-serverの使い方
npm option
webpackでbundle分割

旧バージョンの情報

※情報が古いのでwebpackの記載方法が非推奨になっているものもあります。
Webpack + React + ES6の最小構成を考えてみる。
Reactを「webpack + babel-loader」でビルドする方法

Angular moment.js脱却メモ

f:id:fa11enprince:20180815053913j:plain

Angularを使っていてWebpackでのbundle.jsが肥大化したときにmoment.jsをやめたいときのメモ

可能な限りDateとimport { DatePipe } from '@angular/common';を使う

date -> string

moment

moment(date).format('YYYY-MM-DD HH:mm:ss.SSS');

TypeScript + DatePipe

this.datePipe.transform(date, 'yyyy-MM-dd HH:mm:ss.SSS');

※書式設定の仕方が違うので注意!

String -> toLocalString

moment

moment(dateString).toDate().toLocaleString();

TypeScript + DatePipe

this.datePipe.transform(dateString);

String -> Date

Dateを使う

Date.parse('2018-1-10 01:00:00.111');
Date.parse('2018-01-10T01:00:00.111');
new Date('2018-1-10 01:00:00.111');
new Date('2018-01-10T01:00:00.111');

時間差

moment

const diffFrame = moment(toDate).diff(moment(fromDate)) / 100;

Date

const diffFrame = (Date.parse(toDate) - Date.parse(fromDate)) / 100;

日付加算(ミリ秒)

moment

addedDate = moment(date).add(1000, 'milliseconds').toDate().toLocaleString();

Date

addedDate = this.datePipe.transform(Date.parse(date) + 1000);

ISOString

moment

moment(date).toISOString();

Date

new Date(date).toISOString()

経過時間

msから経過時間のhh:mm:ssを作るようなとき moment.durationだけはどうにもならない →自前実装するしかない

追記

date_fnsを使うのもありかも