Gobble up pudding

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

MENU

Spring Boot 例外処理/エラーハンドリングメモ

f:id:fa11enprince:20190306032659j:plain

Spring Bootでは例外処理はthrowしてしまえばわりとよしなにやってくれるが… それでも考えるべきことはある。 場合によっては例外処理をあまり使わず、オブジェクトの戻り値による処理をすることもある。 と、去年の1月からSpring Bootを使い始めてSpringの良さがわかり、Springの深いところに潜り始めた。 気になったところをメモします。 気が向いたら更新しようかと思います。

全てここを参考にさせていただいています。 いろいろ学びが多く、とても良い資料です。 speakerdeck.com

例外処理のパターン@Controller編

基本的には

  1. @Controllerに個別で@ExceptionHandler
  2. @ControllerAdvice@ExceptionHandler
  3. Spring BootのError Controller

3番目は知らなかった 1番目と2番目はよく使うと思う

@Slf4j
@ControllerAdvice("com.example.controllers.foo")
public class GlobalControllerAdvice {
    @ExceptionHandler(Exception.class)
    public String handleException(Exception e, HttpServletResponse response, Model model) {
        response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        log.error("Error occurred.", e);
        model.addAttribute("errorMessage", e.getMessage());
        
        return "foo/error";
    }
}

プレゼンテーション層、404エラーなど全ての例外を補足できない&status codeがセットされないという問題がある

ErrorControllerを使う

Spring MVCでは次の順に呼ばれるとのこと

  1. ExceptionHandlerExceptionResolver(@ExceptionHandler)
  2. ResponseStatusExceptionResolver(@ResponseStatus)
  3. DefaultHandlerExceptionResolver(Spring MVCで起きた例外を処理)

ErrorControllerはこれらが適用された後に呼ばれる

例外処理のパターン@RestController編

  1. @RestControllerに個別で@ExceptionHandler
  2. @RestControllerAdvice@ExceptionHandler
  3. @ResponseStatusを付与した例外をThrow

qiita.com

ExceptionHandlerを使う場合

@Data
public class ErrorResponse {
    private String KeyName;
    private String KeyValue;
    private String Message;

    public ErrorResponse(String keyName,String keyValue,String message){
        this.KeyName = keyName;
        this.KeyValue = keyValue;
        this.Message = message;
    }
    public ResponseEntity<ErrorResponse> createResponse(HttpStatus status){
        return new ResponseEntity<ErrorResponse>(this, status);
    }
    public static ResponseEntity<ErrorResponse> createResponse(BadRequestException e){
        return new ResponseEntity<ErrorResponse>(
                new ErrorResponse(e.getKeyName(),e.getKeyValue(),e.getMessage())
                    ,HttpStatus.BAD_REQUEST);
    }
}

自前例外

@Data
public class BadRequestException extends Exception {

    private String keyName;
    private String keyValue;
    private String error;

    public BadRequestException(String error, String keyName, String keyValue){
        this.keyName = keyName;
        this.keyValue = keyValue;
        this.error = error;
    }

}

例外ハンドラ

@RestControllerAdvice
public class BadRequestExceptionHandler {

    @ExceptionHandler(BadRequestException.class)
    public ResponseEntity<ErrorResponse> getException(HttpServletRequest req, BadRequestException e){
        return ErrorResponse.createResponse(e);
    }
    
}

その他リンク

https://www.baeldung.com/exception-handling-for-rest-with-spring https://www.slideshare.net/shintanimoto/spring-boot10 https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html

Windowsサービスで共有フォルダにアクセスするときのTips

f:id:fa11enprince:20161015172759j:plain サービスからネットワーク越しの共有フォルダを覗きたいことがあるが
¥¥example.machine.local¥folder
のようなUNC(Universal Naming Convention)パスはプログラムからは使えない。
なのでネットワークドライブの割り当てをしたいと思うはず。

GUI(Explorer)からネットワークドライブの割り当てをしていてもWindowsサービス側では認識されない。

サービスで起動された実行ファイルから、ネットワークドライブが使えないのは何故? | ユニリタブログ

ユーザがデスクトップにログインした状態のときにエクスプローラなどで作成したネットワークドライブを使用している場合、その接続認証はユーザと紐付いているので、そのネットワークドライブは、ユーザが対話形式のログオンをしている状態でないと使用することができません。そのため、実行ファイルを呼び出しているサービスのアカウントにAdministrators権限があったとしても、対話形式のログオンをしている状態でない限り、デスクトップにログインした状態で作成したネットワークドライブを使うことはできません。

サービスで起動された実行ファイルから、ネットワークドライブにあるファイルを利用するには? | ユニリタブログ

ユーザが対話形式のログオンをしていないときに、サービスで起動された実行ファイルからネットワークドライブ上のファイルを扱うには、エクスプローラでネットワークドライブを設定する代わりに、net use コマンドを使用してネットワークドライブを設定します。

要は、ネットワークの接続が切れたときに再びnet useをやり直さないとつながらないが、Explorer上から設定してもダメということです。

windows - Map a network drive to be used by a service - Stack Overflow

解決策1.

nssmを使ってるならばサービスにするプログラムをbatでラップしてnet useする (ただし、これは接続断したらサービス再起動しないとダメ) 通常の用途ではこれで十分と思われる。

解決策2.

サービスの中のプログラムの中でnet useを実行する

解決策3.

タスクスケジューラ等でnet useを定期的にする

if not exist X: (
    X: \\example.machine.local\share
)

解決策4.

シンボリックリンクを使う

ただ、注意点がある

ファイル共有とシンボリックリンクの利用について – Ask the Network & AD Support Team

fsutilにて設定を変更する必要がある

measurement_pcは任意の名前にしてもらってよいです

管理者権限で実行 ネットワークドライブを消して、

net use X: /delete

シンボリックリンクを作成します。

mklink /D C:¥share \\example.machine.local\share

Spring BootのScheduledTaskの検証

f:id:fa11enprince:20160613112501j:plain @EnableSchedulingを用いて
Spring BootのScheduling Tasksについて調査しました。

Spring BootにはSpring Batchという仕組みがあるのですが、やや大仰で使いにくいです。
Web系システムでの利用だと単純なスケジューリングのTaskで十分なことが多いと思います。
そんなわけで@EnableSchedulingのcronの機能について調査しました。

  1. 5秒以内で終わるTask
  2. 5秒以上かかるTask(10秒かかる)
  3. 5秒以上かかるTask(10秒かかる)でFuture利用

ソースコード

全部入りはここに置いてます。 本当はコメント化で対処したくなかったのですが、mainメソッドやCommandLineRunnerApplicationRunnerがあると同時に動いてわけがわからなくなるので、コメント化で対処してしまっています。
引数でやればいいじゃんという話もありますが、面倒でした。 github.com

ひとまずmainのクラスです

@SpringBootApplication
public class ScheduledTaskApplication implements ApplicationRunner {

    private static final Logger logger = LoggerFactory.getLogger(ScheduledTaskApplication.class);
    
    public static void main(String[] args) {
        SpringApplication.run(ScheduledTaskApplication.class, args);
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {
        logger.info("{} invoked.", ReflectionUtil.getClassAndMethodName());
    }

}

今回のケースでは別にApplicationRunnerは不要でmainだけでよいのですが、ログを書きたかったので、 わざわざimplementsしているのはそのためです。 Javaラーにありがちですが、importは省略しています。

5秒以内で終わるTaskのケース

ScheduledTask.java

@EnableScheduling
@Component
public class ScheduledTask {
    /**
    * 5秒ごとに実行されるTask
    * Springから自動的に呼び出される
    * ちなみにメソッドの名前は何でもよい
    */
    @Scheduled(cron = "0/5 * * * * *")
    public void run() {
        logger.info("{} invoked.", ReflectionUtil.getClassAndMethodName());
        lightTask();
    }

    /**
    * 軽いTask
    */
    public void lightTask() {
        logger.info("{} start.", ReflectionUtil.getClassAndMethodName());
        logger.info("light task processing ...");
        // TODO something...
        logger.info("{} end.", ReflectionUtil.getClassAndMethodName());
    }
}

これで動かしてみます
実行すると当たり前ですが…

2019-02-10 04:47:08.745[main][INFO ][c.e.s.ScheduledTaskApplication          ] :ScheduledTaskApplication#run invoked.
2019-02-10 04:47:10.001[scheduling-1][INFO ][c.e.scheduledtask.tasks.ScheduledTask   ] :ScheduledTask#run invoked.
2019-02-10 04:47:10.001[scheduling-1][INFO ][c.e.scheduledtask.tasks.ScheduledTask   ] :ScheduledTask#lightTask start.
2019-02-10 04:47:10.001[scheduling-1][INFO ][c.e.scheduledtask.tasks.ScheduledTask   ] :light task processing ...
2019-02-10 04:47:10.002[scheduling-1][INFO ][c.e.scheduledtask.tasks.ScheduledTask   ] :ScheduledTask#lightTask end.
2019-02-10 04:47:15.001[scheduling-1][INFO ][c.e.scheduledtask.tasks.ScheduledTask   ] :ScheduledTask#run invoked.
2019-02-10 04:47:15.001[scheduling-1][INFO ][c.e.scheduledtask.tasks.ScheduledTask   ] :ScheduledTask#lightTask start.
2019-02-10 04:47:15.001[scheduling-1][INFO ][c.e.scheduledtask.tasks.ScheduledTask   ] :light task processing ...
2019-02-10 04:47:15.001[scheduling-1][INFO ][c.e.scheduledtask.tasks.ScheduledTask   ] :ScheduledTask#lightTask end.

こんな感じになります。

5秒以上かかるTask(10秒かかる場合)のケース

ScheduledTask.java

@EnableScheduling
@Component
public class ScheduledTask {
    /**
    * 5秒ごとに実行されるTask
    * Springから自動的に呼び出される
    * ちなみにメソッドの名前は何でもよい
    */
    @Scheduled(cron = "0/5 * * * * *")
    public void run() {
        logger.info("{} invoked.", ReflectionUtil.getClassAndMethodName());
        heavyTask();
    }

    /**
    * 重いTask
    */
    public void heavyTask() {
        logger.info("{} start.", ReflectionUtil.getClassAndMethodName());
        try {
            logger.info("heavy task processing ...");
            TimeUnit.SECONDS.sleep(10);
            // TODO something...
        } catch (InterruptedException e) {
            logger.error(e.getMessage(), e);
        }
        logger.info("{} end.", ReflectionUtil.getClassAndMethodName());
    }
}
2019-02-10 04:50:26.033[main][INFO ][c.e.s.ScheduledTaskApplication          ] :ScheduledTaskApplication#run invoked.
2019-02-10 04:50:30.001[scheduling-1][INFO ][c.e.scheduledtask.tasks.ScheduledTask   ] :ScheduledTask#run invoked.
2019-02-10 04:50:30.001[scheduling-1][INFO ][c.e.scheduledtask.tasks.ScheduledTask   ] :ScheduledTask#heavyTask start.
2019-02-10 04:50:30.001[scheduling-1][INFO ][c.e.scheduledtask.tasks.ScheduledTask   ] :heavy task processing ...
2019-02-10 04:50:40.002[scheduling-1][INFO ][c.e.scheduledtask.tasks.ScheduledTask   ] :ScheduledTask#heavyTask end.
2019-02-10 04:50:45.002[scheduling-1][INFO ][c.e.scheduledtask.tasks.ScheduledTask   ] :ScheduledTask#run invoked.
2019-02-10 04:50:45.002[scheduling-1][INFO ][c.e.scheduledtask.tasks.ScheduledTask   ] :ScheduledTask#heavyTask start.
2019-02-10 04:50:45.002[scheduling-1][INFO ][c.e.scheduledtask.tasks.ScheduledTask   ] :heavy task processing ...
2019-02-10 04:50:55.002[scheduling-1][INFO ][c.e.scheduledtask.tasks.ScheduledTask   ] :ScheduledTask#heavyTask end.
2019-02-10 04:51:00.006[scheduling-1][INFO ][c.e.scheduledtask.tasks.ScheduledTask   ] :ScheduledTask#run invoked.
2019-02-10 04:51:00.006[scheduling-1][INFO ][c.e.scheduledtask.tasks.ScheduledTask   ] :ScheduledTask#heavyTask start.
2019-02-10 04:51:00.006[scheduling-1][INFO ][c.e.scheduledtask.tasks.ScheduledTask   ] :heavy task processing ...
2019-02-10 04:51:10.007[scheduling-1][INFO ][c.e.scheduledtask.tasks.ScheduledTask   ] :ScheduledTask#heavyTask end.

となり、ログをよく見ると、実行を待ち合わせてくれます。 本処理が10秒かかる場合、次回の起動は終わるまで待ってくれます。

5秒以上かかるTask(10秒かかる場合)でFuture利用のケース

ということで、別スレッドで本処理を起動してみました。
まぁ、別スレッドということなんで、処理は当然、待ち合わせなくなるので、
Springが決まった時間に起動をかける当たり前の挙動になります。
とはいえ、スレッドの多重起動が起きないように終わるまで次回の処理を起動しないように制御はしています。
こちらのメリットはプログラム自体が異常停止しているかそうでないか、
ログを見ると一目瞭然ということでしょうか
ちなみにFutureTaskは別クラスにする必要は特にないんですが、GitHubにあげるソースコードで見にくくなるので、
とりあえず分けてます。 ScheduledTask.java

@EnableScheduling
@Component
public class ScheduledTask {

    private static final Logger logger = LoggerFactory.getLogger(ScheduledTask.class);
    private ExecutorService executorService = Executors.newSingleThreadExecutor();
    private Future<?> future = null;
    @Autowired
    FutureTask futureTask;

    /**
    * 重いTask (Future利用)
    */
    public void heavyTaskWithFuture() {
        logger.info("{} start.", ReflectionUtil.getClassAndMethodName());
        if (future == null || future.isDone()) {
            future = executorService.submit(futureTask);  // 別スレッドで実行
        }
        logger.info("{} end.", ReflectionUtil.getClassAndMethodName());
    }
}

別スレッドで呼び出されるTask FutureTask.java

@Component
public class FutureTask implements Callable<Boolean> {
    private static final Logger logger = LoggerFactory.getLogger(ScheduledTask.class);

    @Override
    public Boolean call() throws Exception {
        logger.info("{} start.", ReflectionUtil.getClassAndMethodName());
        try {
            logger.info("future heavy task processing ...");
            TimeUnit.SECONDS.sleep(10);
            // TODO something...
        } catch (InterruptedException e) {
            logger.error(e.getMessage(), e);
            return false;
        }
        logger.info("{} end.", ReflectionUtil.getClassAndMethodName());
        return true;
    }
}

これを実行すると…、

2019-02-10 05:00:24.189[main][INFO ][c.e.s.ScheduledTaskApplication          ] :ScheduledTaskApplication#run invoked.
2019-02-10 05:00:25.002[scheduling-1][INFO ][c.e.scheduledtask.tasks.ScheduledTask   ] :ScheduledTask#run invoked.
2019-02-10 05:00:25.002[scheduling-1][INFO ][c.e.scheduledtask.tasks.ScheduledTask   ] :ScheduledTask#heavyTaskWithFuture start.
2019-02-10 05:00:25.003[scheduling-1][INFO ][c.e.scheduledtask.tasks.ScheduledTask   ] :ScheduledTask#heavyTaskWithFuture end.
2019-02-10 05:00:25.003[pool-1-thread-1][INFO ][c.e.scheduledtask.tasks.ScheduledTask   ] :FutureTask#call start.
2019-02-10 05:00:25.003[pool-1-thread-1][INFO ][c.e.scheduledtask.tasks.ScheduledTask   ] :future heavy task processing ...
2019-02-10 05:00:30.002[scheduling-1][INFO ][c.e.scheduledtask.tasks.ScheduledTask   ] :ScheduledTask#run invoked.
2019-02-10 05:00:30.002[scheduling-1][INFO ][c.e.scheduledtask.tasks.ScheduledTask   ] :ScheduledTask#heavyTaskWithFuture start.
2019-02-10 05:00:30.002[scheduling-1][INFO ][c.e.scheduledtask.tasks.ScheduledTask   ] :ScheduledTask#heavyTaskWithFuture end.
2019-02-10 05:00:35.001[scheduling-1][INFO ][c.e.scheduledtask.tasks.ScheduledTask   ] :ScheduledTask#run invoked.
2019-02-10 05:00:35.001[scheduling-1][INFO ][c.e.scheduledtask.tasks.ScheduledTask   ] :ScheduledTask#heavyTaskWithFuture start.
2019-02-10 05:00:35.001[scheduling-1][INFO ][c.e.scheduledtask.tasks.ScheduledTask   ] :ScheduledTask#heavyTaskWithFuture end.
2019-02-10 05:00:35.003[pool-1-thread-1][INFO ][c.e.scheduledtask.tasks.ScheduledTask   ] :FutureTask#call end.
2019-02-10 05:00:40.001[scheduling-1][INFO ][c.e.scheduledtask.tasks.ScheduledTask   ] :ScheduledTask#run invoked.
2019-02-10 05:00:40.001[scheduling-1][INFO ][c.e.scheduledtask.tasks.ScheduledTask   ] :ScheduledTask#heavyTaskWithFuture start.
2019-02-10 05:00:40.001[scheduling-1][INFO ][c.e.scheduledtask.tasks.ScheduledTask   ] :ScheduledTask#heavyTaskWithFuture end.
2019-02-10 05:00:40.002[pool-1-thread-1][INFO ][c.e.scheduledtask.tasks.ScheduledTask   ] :FutureTask#call start.
2019-02-10 05:00:40.002[pool-1-thread-1][INFO ][c.e.scheduledtask.tasks.ScheduledTask   ] :future heavy task processing ...
2019-02-10 05:00:45.002[scheduling-1][INFO ][c.e.scheduledtask.tasks.ScheduledTask   ] :ScheduledTask#run invoked.
2019-02-10 05:00:45.002[scheduling-1][INFO ][c.e.scheduledtask.tasks.ScheduledTask   ] :ScheduledTask#heavyTaskWithFuture start.
2019-02-10 05:00:45.002[scheduling-1][INFO ][c.e.scheduledtask.tasks.ScheduledTask   ] :ScheduledTask#heavyTaskWithFuture end.

ちょっとわかりにくいですが、必ず5分に1回呼び出されています。

とりあえず、自分でお手軽バッチを作るなら3のパターンかなと思った次第です。
にしても、Javaのマルチスレッドプログラミングはマジで便利…。 C++だとめんどくさすぎる。
ちなみにRx系は複雑度が高すぎな気がしていまいち好きになれない…。

補足

Spring Bootですが1プロジェクトに別にいくつもエントリーポイントがあってもよい。 コマンドラインからの起動の時は同時に動きます。

参考リンク

Spring & Spring Boot

Getting Started · Scheduling Tasks
Integration

ApplicationRunnerとCommandLineRunnerについて

Spring Boot: ApplicationRunner and CommandLineRunner - DZone Java
Spring Bootでコマンドラインアプリを作る時の注意点 - Qiita

Future関連

言語によってちょっと違うFuture/Promiseをまとめてみた(1) - Qiita

Spring BootのThymeleafとTypeScriptを組み合わせてみたかったのでやってみた

f:id:fa11enprince:20180501154902j:plain

Web開発では最近はReact, Vue, Angularを使ってSPAが主流ですが、
とはいっても、レガシーなjQuery UIとBootstrapと組み合わせてサーバ側でレンダリングしたいこともままありますよね。 そんなわけで、僕の場合、特にjQuery UIを使いたい。
というのが一番にあって、かつTypeScriptを使いたい。
しかしながらフロントエンドフレームワーク使うまでもない。
といったところでWebpackと組み合わせて使ってみよう!ということで作りました。 サーバサイドはSpring Boot2です。別にこれじゃなくてもよいのだけれど、
最近一番慣れているフレームワークがコレなのです。
最近PHP未経験なので使いたい病にかかってるのでそっちでやればよかったかもしれない。

おおまかなプロジェクト構成はこんな感じです。 Gulpを使ったほうが実はシンプルになるのかとも考えましたが、 最近Gulp使わないっすよね。Angularでは使われてるっぽいですが。 直近までAngularを使っていたせいかAngularチックな構成になってしまい。
Angularでいいんじゃ…状態

下記をすべて置いたものをGitHubにあげています。 github.com

大まかなプロジェクト構成

src/
  main/
     java/
     client/
        ts/
           commons/
           controllers/
           models/
           services/
           pages/index.js (エントリーポイント2つ目)
           index.js (エントリーポイント1つ目)
     resources/
        public/
        static/
        resoruces/
          template/
             pages/index.html (エントリーポイント2つ目を読み込む)
             index.html(エントリーポイント1つ目を読み込む)

webpack.config.jsですが、開発用と本番用を分けたかったので、分けてます。
webpackに詳しくない人はここをやっておくとよいと思います。
Getting Started | webpack
ただ、後で少し解説しています。

webpack.common.js

const path = require('path');
const CleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = {
    entry: {
        'index': path.resolve(__dirname, 'ts/index.ts'),
        'pages/page2': path.resolve(__dirname, 'ts/pages/page2.ts'),
        // ページが増えたらここに追記
    },
    output: {
        path: path.resolve(__dirname, '../resources/static'),
        filename: '[name].js',
        publicPath: '/' // webpack-dev-server等が使うディレクトリ この場合、../resources/staticが/となる
    },
    module: {
        rules: [
            // ts -> ES2015 -> babel -> ES5
            {
                test: /\.ts$/,
                exclude: /node_modules/,
                loader: ['babel-loader'],
            },
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: ["source-map-loader"],
                enforce: "pre",
            },
            {
                test: /\.css$/,
                use: [
                    'style-loader',
                    'css-loader',
                ],
            },
            {
                test: /\.(png|svg|jpg|gif)$/,
                use: [{
                    loader: 'file-loader',
                    options: {
                        name: '[name].[ext]',
                        outputPath: 'assets/images/',
                    },
                }, ],
            },
            {
                test: /\.(woff|woff2|eot|ttf|otf)$/,
                use: [
                    'file-loader'
                ]
            }
        ]
    },
    resolve: {
        extensions: [
            '.ts', '.js'
        ],
    },
    plugins: [
        new CleanWebpackPlugin('static', {
            root: path.resolve(__dirname, '../resources/static'),
            verbose: true,
            dry: false,
        }),
    ],
}

わかる人にはここだけでふーんだと思いますが、これだけだと意味不明な人も多いはずなので説明します。

説明

代表的な依存モジュール

  • date-fns
  • jQuery
  • jQuery UI
  • popper.js
  • jQuery cookie
  • Bootstrap

開発支援系

  • Babel
  • TypeScript
  • clean-webpack-plugin
  • css-loader
  • style-loader
  • file-loader
  • source-map-loader
  • url-loader
  • webpack
  • webpack-bundle-analyzer
  • webpack-merge

本来は必要がないもの

  • html-loader
  • html-webpack-plugin
  • webpack-dev-server

なおBabelを入れているのはIE対応等でpolyfillするため
polyfillしないとIEでPromiseやES6のArray.prototype系のメソッドが使えない
momentでなくてdate-fnsにしているのはバンドルサイズの肥大化を避けるためである

webpack-dev-serverはSpring BootのようなWeb側がなくても
独立して開発できるようにしようとしたものだが、
SPAを前提としていないこともあり、画像/CSSのパス等の調整に難があり、
それを妥協してもいいなら一応使えるようにはなっている。

開発方法

従来型のMPAで作成する。 エントリーポイントはSpring Bootのディレクトリ構成と対にし、index.tsを作成する
ここで必要なjQuery, Bootstrap等を読み込み、controllerを読み込む。
その際、webpack.common.jsにentryを追加する

なお、importするモジュールは次のように分けられたディレクトリのいずれかに配置する

- commons
  共通系のclass, functionを置く
- controllers
  Spring BootのController相当のclassを置く
- models
  Spring BootのEntity相当のclassを置く
- services
  APIアクセスを記述する  
  $.Deferredを返すようにする(ほぼPromiseベース)

なお、index.js自体にはonclick, $(document).ready()相当の処理しか書かず、
あとはcontrollerに任せる

その他

npm run build-dev, npm run watchしたときにeclipse側で変更が読み取れないので、
面倒だが、都度static配下をrefreshする → ※eclipseの Preferences > General > Workspace > Reflesh using native hooks or polling をすれば即時反映されそうです。

TypeScriptについて

tsconfig.jsonについて
targetはトランスパイルした結果のバージョンのこと。
ECMAScriptのバージョンでES2015(ES6)にしている。
ES5にまで一気に落とせるのだが、これはなぜかというと、あとでBabelでpolyfillするためである。 moduleはコード生成モジュールのことでimportの挙動に影響を与える。
ひとまずES2015とする。

その他についてはここを見るとよいかもしれません。

Webpack関連

基礎知識編

Webpackはビルドツールであるが、gulpと違い、コードでタスクを書くようなことはできない。 基本はentryでエントリーポイントを決め、
outputで出力先を決める MPAの構成にしたい場合は、gulpでないとできなそうだが、webpackで可能である。 entryを複数にしてoutputfilenameにて[name].jsとすると
entrykeyの値(=PATH)がそのまま出力先になる(ディレクトリ階層のあるPATHであればそれもそのまま作成される)
なお、outputのPublicPathはwebpack-dev-serverなどを使ったときに、 サーバの公開先のrootのパスになる。

__dirnameについて
これはnodeの変数で、現在コマンドを動かしているディレクトリになる。
絶対パスで指定しないとダメな場合に活用できる。

babel-loaderについて

JavaScriptをトランスパイルするもの。 babel-loaderの7からTypeScriptもトランスパイル可能。

source-map-loaderについて

source-mapを作成するもの。 これはデバッグ実行したときにトランスパイル済みコードではデバッグが厳しいので、トランスパイル前のコードを表示してくれるもの

CSS関連

style-loaderはスタイルシートをJSからlinkタグに展開してhtml内に注入してくれるもの css-loaderはCSSをバンドルするための機能

画像関連

file-loaderはビルド・バンドルしたものが参照している画像等を適切な場所に配置してくれるもの。 フロントだけの開発の場合はPATHがなかなか合わず、dev-serverと組み合わせないとうまくいかない。 url-loaderというのもあり、こちらは画像/CSSをbase64にして埋め込んでくれて、
これによりURLやPATHを気にしなくてよくなるが、ファイルサイズが肥大化するというデメリットがある。

resolveについて

resolveで指定した拡張子はimport時に拡張子が不要となる。
通常はjstsだけで良いと思われる。
Reactな人はjsxとかもあったほうが良い。
もちろんcssとかも指定可能。

あとがき

以上こんな感じで作ってみました。 てゆうか既にそんなもの、OSSで転がってるよ。っていうのがあるかもしれません。
ただ、MPA前提のはあまりみかけない。
そしてあえてレガシーなjQuery UIを使う…というのも新規で作る場合に選択肢には上がりにくいし。 ただ、便利な部品が多いんですよね。
SPAにするといろいろ問題が起こるんですよね。特に要員的な問題で…。 書いてみて思ったのですがやはりまだまだ知識不足…。

参考リンク

最新版で学ぶwebpack 4入門 - Bootstrapをバンドルする方法 - ICS MEDIA
webpack~Bootstrap4移行ガイド
webpack-dev-serverを使ってみる - のぐそんブログ
webpack-dev-serverの基本的な使い方とポイント - dackdive's blog
webpackでhtmlファイルも出力する - emahiro/b.log
npmとwebpack4でビルド - jQueryからの次のステップ - Qiita
http://js.studio-kingdom.com/typescript/tutorials/react_and_webpack
TypeScript With Babel: A Beautiful Marriage
TypeScriptでasync/await (webpackビルド) - Qiita
フロントエンド知らない私のwebpack入門 その1 - Qiita
webpackでCSSやSASSを使う - 30歳からのプログラミング
【意訳】Webpackの混乱ポイント - Qiita
今時のフロントエンド開発2017 (3. webpack編) - Qiita

MS公式のTypeScript + Babelスターター

GitHub - Microsoft/TypeScript-Babel-Starter: A sample setup using Babel CLI to build TypeScript code, and using TypeScript for type-checking.

Bootstrapのサンプル

glacial-webwork.com

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

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

背景

もともと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を作ればいいんだ! そんなふうに思っていました。

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.yaml

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("-------------------------------------------");
    }

}

github.com

結果

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