SIerだけど技術やりたいブログ

5年目のSIerのブログです

Javaアプリケーションのログ出力

Javaアプリケーションで考慮すべきログはだいたい3パターンに分類できると思う。

標準ロガー系

アプリケーションやライブラリのログで使われる。
ログの実装ライブラリ(java.util.logging, Log4Jなど)と、
それを抽象化するライブラリ(SLF4J, Commons Loggingなど)がある。

いくらでも詳しいサイトがあったので、そちらを参照。
www.bunkei-programmer.net

実現方法

例えばSLF4J + Logback を利用すると、以下のように書ける。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
...
  // SLF4J + Logback
  Logger logger = LoggerFactory.getLogger(Main.class.getName());
  logger.info("Logback logging");

SLF4Jのクラスしか利用していないのに、Lobackの実装が利用される。この実現方法は詳しい説明があったので、そちらを参照。
qiita.com


また、依存ライブラリのログも Logbackで出力できる。外部ライブラリで java.util.logging や Log4J を利用したコードが書かれているにも関わらず、Logbackでログ出力される。なぜか?
f:id:kimulla:20171010125956j:plain

Javaではコンパイル時に依存するクラスファイルをクラスパスに通すが、一度クラスファイルが生成されたら実行時まで依存するクラスファイルは必要ない。
そのため、コンパイル時に利用したクラスファイルとパッケージやメソッドは同一だけど、SLF4Jを利用した実装をしている別のクラスファイルを使うことで、ログ実装を強制的に切り替えている。log4j-over-slf4j や jul-to-slf4j というアダプタ関連のクラスがそれ。
f:id:kimulla:20171010110315j:plain

実際にライブラリの中を見てみるとlog4jのクラスをラップしていることがわかる。
f:id:kimulla:20171010130359p:plain

github.com


※ ただし java.util.logging だけは特別。
java.util.logging は 標準APIのため、クラスファイルがrt.jarに入ってしまっている。
ということは上記のようにjarで実装を差し替えられない。
qiita.com


独自ロガー系

Apache Tomcat の内部ロガーの JULI が有名だと思う。

Apache Tomcat 8 (8.5.23) - Logging in Tomcat

The internal logging for Apache Tomcat uses JULI, a packaged renamed fork of Apache Commons Logging that is hard-coded to use the java.util.logging framework. This ensures that Tomcat's internal logging and any web application logging will remain independent, even if a web application uses Apache Commons Logging.

アクセスログなど、大抵はミドルウェア(APサーバ)ごとにログ出力の設定が必要になる。

標準出力/標準エラー出力

起動・停止のためのbashスクリプトでログを出力するために、標準出力/標準エラー出力をファイルにリダイレクトすることがある。
Apache Tomcatの起動スクリプトの catalina.sh は catalina.out にログを出す。

26-Sep-2017 22:09:52.430 情報 [main] org.apache.catalina.core.StandardService.startInternal サービス [Catalina] を起 動します
26-Sep-2017 22:09:52.430 情報 [main] org.apache.catalina.core.StandardEngine.startInternal Starting Servlet Engine: Apache Tomcat/8.5.23
26-Sep-2017 22:09:52.450 情報 [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployDirectory Webアプ リケーションディレクトリ [/home/kimura/apache-tomcat-8.5.23/webapps/ROOT] を配備します
26-Sep-2017 22:09:52.887 情報 [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployDirectory Deployment of web application directory [/home/kimura/apache-tomcat-8.5.23/webapps/ROOT] has finished in [436] ms
26-Sep-2017 22:09:52.887 情報 [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployDirectory Webアプ リケーションディレクトリ [/home/kimura/apache-tomcat-8.5.23/webapps/docs] を配備します
26-Sep-2017 22:09:52.909 情報 [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployDirectory Deployment of web application directory [/home/kimura/apache-tomcat-8.5.23/webapps/docs] has finished in [22] ms
26-Sep-2017 22:09:52.910 情報 [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployDirectory Webアプ リケーションディレクトリ [/home/kimura/apache-tomcat-8.5.23/webapps/examples] を配備します
26-Sep-2017 22:09:53.208 情報 [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployDirectory Deployment of web application directory [/home/kimura/apache-tomcat-8.5.23/webapps/examples] has finished in [299] ms
26-Sep-2017 22:09:53.208 情報 [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployDirectory Webアプ リケーションディレクトリ [/home/kimura/apache-tomcat-8.5.23/webapps/host-manager] を配備します
26-Sep-2017 22:09:53.258 情報 [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployDirectory Deployment of web application directory [/home/kimura/apache-tomcat-8.5.23/webapps/host-manager] has finished in [50] ms
26-Sep-2017 22:09:53.259 情報 [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployDirectory Webアプ リケーションディレクトリ [/home/kimura/apache-tomcat-8.5.23/webapps/manager] を配備します
26-Sep-2017 22:09:53.287 情報 [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployDirectory Deployment of web application directory [/home/kimura/apache-tomcat-8.5.23/webapps/manager] has finished in [28] ms
26-Sep-2017 22:09:53.303 情報 [main] org.apache.coyote.AbstractProtocol.start Starting ProtocolHandler ["http-nio-8080"]
26-Sep-2017 22:09:53.392 情報 [main] org.apache.coyote.AbstractProtocol.start Starting ProtocolHandler ["ajp-nio-8009"]
26-Sep-2017 22:09:53.394 情報 [main] org.apache.catalina.startup.Catalina.start Server startup in 995 ms


大抵のログ実装で、設定すればアプリケーションログも標準出力に出せる。

またコード中でハンドリングされない例外(OutOfMemoryErrorなど)は標準エラー出力に出る。

public static void main(String[] args) {
    throw new OutOfMemoryError("oome");
}


f:id:kimulla:20171010132955p:plain

最近の流行り

ログをファイルに書き出すのをやめて、stdoutに出す。そしてログ収集基盤などの基盤機能に任せてアプリで管理しない。
12factor.net

Javaのビルドの基礎知識

Javaのビルド関連の知識については、Java学習初期にこんなコード書いて終わり、あとは便利なビルドツール(mavenやらgradle)に任せようって感じで知識が薄かった。

$ cat Sample.java
public class Sample {
    public static void main(String[] args) {
        System.out.println("hello world");
    }
}
$ javac Sample.java
$ java Sample
hello world

なので再勉強する。


題材

複数モジュールを作成し、mavenがやってるようなことをjdk付属ツールでやる。
module-a.jarのクラス が module-b.jarのクラス を参照する構成にする。

f:id:kimulla:20170830132404j:plain

module-b.jarを作る

以下のようなプロジェクト構成でmodule-b.jarを作成していく。

$ tree 
|-- src
|  |-- module
|  |  |-- b
|  |     |- B.java
|-- target
   |-- classes

ソースコードは適当に用意する。

$ cat src/module/b/B.java
package module.b;

public class B {
    public void print() {
        System.out.println("B class is called");
    }
}

まずはコンパイルしてclassファイルを作る。

$ javac -d target/classes/ src/module/b/B.java
$ tree 
|-- src
|  |-- module
|  |  |-- b
|  |     |- B.java
|-- target
   |-- classes
      |-- module
         |-- b
            |-- B.class

jarを作る。jarの中身はclassファイルをまとめたzip的なやつ。

$ jar cvf target/module-b.jar -C target/classes/ .
マニフェストが追加されました
module/を追加中です(=0)(=0)(0%格納されました)
module/b/を追加中です(=0)(=0)(0%格納されました)
module/b/B.classを追加中です(=398)(=283)(28%収縮されました)
$ ls target/
classes  module-b.jar

module-a.jarを作成する

続いて module-a.jar を作成する。

$ tree 
|-- src
|  |-- module
|  |  |-- a
|  |     |- A.java
|-- target
   |-- classes

moduel-a のクラスでは module-b のクラスを利用する。

$ cat src/module/a/A.java
package module.a;
import module.b.B;

public class A {
    public static void main(String[] args) {
        B b = new B();
        b.print();
    }
}

先ほどと同様にコンパイルするとエラーになる。

$ javac -d target/classes/ src/module/a/A.java
src/module/a/A.java:3: エラー: パッケージmodule.bは存在しません
import module.b.B;
               ^
src/module/a/A.java:7: エラー: シンボルを見つけられません
            B b = new B();
            ^
  シンボル:   クラス B
  場所: クラス A
src/module/a/A.java:7: エラー: シンボルを見つけられません
            B b = new B();
                      ^
  シンボル:   クラス B
  場所: クラス A
エラー3

利用する(importする)クラスをクラスパスに通さないといけない。(B.class を参照しないと型チェックもできないので当然)

$ javac -cp ../module-b/target/module-b.jar -d target/classes/ src/module/a/A.java

jarを作る。

$ jar cvf target/module-a.jar -C target/classes/ .
マニフェストが追加されました
module/を追加中です(=0)(=0)(0%格納されました)
module/a/を追加中です(=0)(=0)(0%格納されました)
module/a/A.classを追加中です(=315)(=237)(24%収縮されました)


実行時にも module-b にクラスパスが通っていないとダメ。(module-a.jar には module-b.jar のクラスファイルが含まれていないから)

$ java -cp target/module-a.jar module.a.A
Exception in thread "main" java.lang.NoClassDefFoundError: module/b/B
        at module.a.A.main(A.java:7)
Caused by: java.lang.ClassNotFoundException: module.b.B
        at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
        at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
        ... 1 more

そのため、実行するときは、以下のようにクラスパスを指定する。

$ java -cp target/module-a.jar:../module-b/target/module-b.jar module.a.A
B class is called

module-b.jar のライブラリがバージョンアップした場合

module-b.jar がバージョンアップしたとしても、一度 module-a.jar をコンパイルしたあとは同じ module-a.jar を使うことができる。

メソッドの中の処理が変わった場合

メソッドのシグネチャは変えずに、中の処理だけ変える。

$ cat src/module/b/B.java
package module.b;

public class B {
    public void print() {
        // メソッドの中を変える
        System.out.println("B class version up");
    }
}

module-b.jarを作り直す。

$ javac -d target/classes/ src/module/b/B.java
$ jar cvf target/module-b.jar -C target/classes/ .
マニフェストが追加されました
module/を追加中です(=0)(=0)(0%格納されました)
module/b/を追加中です(=0)(=0)(0%格納されました)
module/b/B.classを追加中です(=399)(=285)(28%収縮されました)

module-a.jarを再コンパイルしなくても、実行できる。

$ java -cp ../module-a/target/module-a.jar:target/module-b.jar module.a.A
B class version up <- 変わってる

メソッドシグネチャが変わった場合

今度はメソッドシグネチャを変えて試してみる。

$ cat src/module/b/B.java
package module.b;

public class B {
    // メソッドの名前を変える
    public void printVerUp() {
        System.out.println("B class version up");
    }
}

module-b.jarを作り直す。

$ javac -d target/classes/ src/module/b/B.java
$ jar cvf target/module-b.jar -C target/classes/ .
マニフェストが追加されました
module/を追加中です(=0)(=0)(0%格納されました)
module/b/を追加中です(=0)(=0)(0%格納されました)
module/b/B.classを追加中です(=404)(=288)(28%収縮されました)

module-a.jar を再コンパイルしなくても実行できるが、 module-b.jar にはprint()メソッドがないので実行時エラーになる。

$ java -cp ../module-a/target/module-a.jar:target/module-b.jar module.a.A
Exception in thread "main" java.lang.NoSuchMethodError: module.b.B.print()V
        at module.a.A.main(A.java:8)

これが「ライブラリのバージョン上げたら動かなくなった」というやつ。

実行可能jarを作る

前述の実行方法だと、java起動時に、実行するクラスを引数で指定する必要がある。アプリケーションとして色々な人に配布するときに、これだと不便。

マニフェストファイルを作ると、起動時のクラスをあらかじめ指定できるようになる。
https://docs.oracle.com/javase/jp/1.5.0/guide/jar/jar.html

$ cat Manifest.MF
Main-Class: module.a.A
Class-Path: ./../../module-b/target/module-b.jar

マニフェストファイルをjarに含める。

$ jar cvfm  target/module-a.jar Manifest.MF -C target/classes/ .
マニフェストが追加されました
module/を追加中です(=0)(=0)(0%格納されました)
module/a/を追加中です(=0)(=0)(0%格納されました)
module/a/A.classを追加中です(=315)(=237)(24%収縮されました)

java -jar xxx を指定するだけで実行可能になった。

$ java -jar target/module-a.jar
B class is called

Fat jar を作る

先述の例ではマニフェストファイルの Class-Path に module-b.jar のURLを指定するため、所定のディレクトリにjarが存在しないといけない。

所定のディレクトリにjarが存在しないと、以下のようなエラーになる。

java -jar module-a.jar
Exception in thread "main" java.lang.NoClassDefFoundError: module/b/B
        at module.a.A.main(A.java:7)
Caused by: java.lang.ClassNotFoundException: module.b.B
        at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
        at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
        ... 1 more


単一のjarを配布するだけで実行したければ、全classファイルを一つのjarに入れてしまえばいい。これは、Fat jar と呼ばれる。

f:id:kimulla:20170830132429j:plain

$ pwd
/home/kimura/maven/module-a/target
// jarを解凍してclassファイルを取り出す
$ jar -xf ../../module-b/target/module-b.jar
$ ls
META-INF  classes  module
$ rm -rf META-INF
// classファイルをまとめる
$ mv module/b classes/module/
$ ls classes/module
a/ b/
$ cd ..
// まとめたclassファイルをjarにする
$ cat Manifest.MF
Main-Class: module.a.A
$ jar cvfm  target/module-a.jar Manifest.MF -C target/classes/ .
マニフェストが追加されました
module/を追加中です(=0)(=0)(0%格納されました)
module/a/を追加中です(=0)(=0)(0%格納されました)
module/a/A.classを追加中です(=315)(=237)(24%収縮されました)
module/b/を追加中です(=0)(=0)(0%格納されました)
module/b/B.classを追加中です(=398)(=283)(28%収縮されました)
// まとめたclassファイルをjarにする
$ java -jar target/module-a.jar
B class is called

他の Fat jar 形式

先述の例はjarを展開して取り出したclassファイルを、Fat jarに格納した。

Spring Bootでは、依存するjarファイルを、そのままjar形式で Fat jarに格納できる。

example.jar
|
+-META-INF
| +-MANIFEST.MF
+-org
| +-springframework
| +-boot
| +-loader
| +-
+-BOOT-INF
+-classes
| +-mycompany
| +-project
| +-YourClasses.class
+-lib
+-dependency1.jar
+-dependency2.jar

Appendix E. The executable jar format


ただしこの機能(JarにJarを含める)は標準的な実現方法がないため、Spring Bootは独自のクラスローダを提供している。

Java does not provide any standard way to load nested jar files (i.e. jar files that are themselves contained within a jar). This can be problematic if you are looking to distribute a self-contained application that you can just run from the command line without unpacking.

To solve this problem, many developers use “shaded” jars. A shaded jar simply packages all classes, from all jars, into a single 'uber jar'. The problem with shaded jars is that it becomes hard to see which libraries you are actually using in your application. It can also be problematic if the same filename is used (but with different content) in multiple jars. Spring Boot takes a different approach and allows you to actually nest jars directly.

MANIFEST.MFをのぞくと以下のようになっている。

cat META-INF/MANIFEST.MF
Manifest-Version: 1.0
Implementation-Title: demo
Implementation-Version: 0.0.1-SNAPSHOT
Archiver-Version: Plexus Archiver
Built-By: kimura
Implementation-Vendor-Id: com.example
Spring-Boot-Version: 1.5.6.RELEASE
Implementation-Vendor: Pivotal Software, Inc.
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.example.demo.DemoApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Created-By: Apache Maven 3.0.5
Build-Jdk: 1.8.0_141
Implementation-URL: http://projects.spring.io/spring-boot/demo/

起動クラスの org.springframework.boot.loader.JarLauncher で、各種classファイルをロードしている様子。

github.com

まとめ

ビルドツールに任せよう

Vuej.sでSPAを実現するときは注意してsetIntervalを使おう

setIntervalとは

一定の遅延間隔を置いて関数を繰り返し実行したいときに利用する。例えばポーリング。
developer.mozilla.org

1秒ごとにコンソール出力する例。

<!doctype html>
<html lang="ja">
<script>
  window.setInterval(function(){
    console.log("polling");
  }, 1000);
</script>
  <body>
  </body>
</html>

setIntervalの生存期間

リファレンスには特に記述がなかった。
6.3 Timers — HTML5
WindowOrWorkerGlobalScope.setInterval() - Web API インターフェイス | MDN

動作確認してみたら以下のようになった。

  • ページ内リンクやJSでの動的なDOM書き換え、はsetIntervalがクリアされない
  • ページ外リンク、はsetIntervalがクリアされる

以下、検証内容

Google Chromeバージョン: 60.0.3112.90で動作確認。

<!doctype html>
<html lang="ja">
<script>
  window.onload = function () {
    window.setInterval(function() { console.log("hi"); }, 1000);
    document.getElementById('btn').addEventListener('click', function() {
        var parent = document.getElementsByTagName('body')[0];
        while(parent.firstChild) parent.removeChild(parent.firstChild);
        var newElm = document.createElement('div');
        newElm.textContent = 'hello world';
        parent.appendChild(newElm);
    });
  }
</script>
  <body>
    <a href="#xxx">ページ内リンク</a>
    <a href="other.html">ページ外リンク</a>
    <button id="btn" type="button">JSで動的に書き替える</button>
    <p>dummy</p>
    ...
    <p>dummy</p>
    <p id="xxx">xxx</p>
    <p>dummy</p>
    ...
    <p>dummy</p>
  </body>
</html>
<!doctype html>
<html lang="ja">
<body>
<a href="polling.html">戻る</a>
</body>
</html>

f:id:kimulla:20170811110448g:plain

ということで、Chromeだと、DOMをunloadして次のDOMをloadするまで(=Documentオブジェクトと同じ生存期間)っぽい。
WindowオブジェクトとDocumentオブジェクトの関係はここが詳しかった。
Window オブジェクトや Document オブジェクト、DOMなど | Web Design Leaves


Vue.jsでsetIntervalを使うときの注意点

SPAの場合、データのみリクエストしてDOMを差分更新することになる。そのため、Documentオブジェクトの入れ替えが起こらず、一度setIntervalしたタイマは、全画面で有効になり続ける。

以下、検証内容

ポーリングしたいページ。

<template>
  <div>
    <h1>polling</h1>
   <router-link to="/goodbye">Goodbye</router-link>
  </div>
</template>

<script>
export default {
  name: 'hello',
  mounted () {
    setInterval(function () {
      console.log('hi')
    }, 1000)
  }
}
</script>


ポーリングしたくないページ。

<template>
  <div>
    <h1>ポーリングしたくないページ</h1>
  </div>
</template>

<script>
export default {
  name: 'boodbye'
}
</script>

f:id:kimulla:20170811111832g:plain

問題点と解決方法

この場合無駄なネットワークコストがかかるため、SPAでsetIntervalする際は明示的にclearIntervalを利用してタイマを削除したほうがよい。

window.clearInterval - Web API インターフェイス | MDN

ページを遷移する際にclearIntervalを呼ぶためには、ライフサイクルフックを利用できる。

jp.vuejs.org

ということで、以下のコードで、ページ遷移する際にポーリングを止められる。

<template>
  <div>
    <h1>polling</h1>
   <router-link to="/goodbye">Goodbye</router-link>
  </div>
</template>

<script>
export default {
  name: 'hello',
  data: function () {
    return {
      intervalId: undefined
    }
  },
  mounted () {
    this.intervalId = setInterval(function () {
      console.log('hi')
    }, 1000)
  },
  beforeDestroy () {
    console.log('clearInterval')
    clearInterval(this.intervalId)
  }
}
</script>

f:id:kimulla:20170811112524g:plain

結論

Vue.js(というかSPA全般)でsetIntervalするときは、ちゃんとclearIntervalしないとダメ。


p.s.
setIntervalの生存期間調べるためにWebKitのソース読もうとしたんだけど、C++難しすぎて1日頑張って諦めた。C++力がほしい。

Spring 抽象クラスに定義したアノテーションは引き継がれるのか

以下のコードの実行結果からわかるように、引き継がれる。はい終わり!

@EnableAsync //非同期処理を有効化する
@SpringBootApplication
public class InheritApplication {

    public static void main(String[] args) {
        new SpringApplicationBuilder(InheritApplication.class).web(false).run(args);
    }

    @Bean
    public CommandLineRunner job(AsyncService service) {
        return i -> {
            service.exec();
        };
    }
}
@Async //検証のため、抽象クラスにアノテーションをつける
public abstract class AsyncService {
    public abstract void exec();
}
@Slf4j
@Service
public class AsyncServiceImpl extends AsyncService {
    @Override
    public void exec() {
        // 別スレッドで非同期に実行される場合、実行スレッドがmainスレッドじゃなくなる
        log.info("-----------------------");
        log.info("exec");
        log.info("-----------------------");
    }
}
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.5.6.RELEASE)
...
2017-08-06 16:02:52.514  INFO 8224 --- [           main] com.example.demo.InheritApplication      : Started InheritApplication in 1.498 seconds (JVM running for 1.811)
// AsyncServiceクラスのexecメソッドの実行スレッドが変わっているため、@Asyncが有効になっている
2017-08-06 16:02:52.519  INFO 8224 --- [cTaskExecutor-1] com.example.demo.AsyncServiceImpl        : -----------------------
2017-08-06 16:02:52.519  INFO 8224 --- [cTaskExecutor-1] com.example.demo.AsyncServiceImpl        : exec
2017-08-06 16:02:52.519  INFO 8224 --- [cTaskExecutor-1] com.example.demo.AsyncServiceImpl        : -----------------------
...

これだとあまり勉強にならないので、もう少し調べる。

アノテーションを処理するには

そもそもアノテーションを処理するには主に、MethodInterceptorという仕組みが使われる。例えば@Transactionalを処理するTransactionInterceptorクラスや@Asyncを処理するAsyncExecutionInterceptorクラスといった感じ。

このMethodInterceptorはAOPのAdviceになっていて、これを実行するタイミングはPointcutで制御される。ざっくりいうと、Adviceが処理したい内容(トランザクション管理するとか、別スレッドで実行するとか、そういうやつ)で、Pointcutは実行されるタイミング(パッケージ配下は実行する、とか、アノテーションついてたら実行する、とかそういうやつ)。

11. Aspect Oriented Programming with Spring

MethodInterceptorを設定する流れ

具体例として、@Asyncを処理するAsyncExecutionInterceptorが設定されるまでの流れを調べる。
バージョンは 1.5.6.RELEASE

まずはConfigurationを読み込む

@EnableAsyncの定義は以下のようになっている。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AsyncConfigurationSelector.class)
public @interface EnableAsync {
...

@Importで指定されたクラスはImportSelectorを継承したクラスで、ImportSelectorは有効にするConfigurationを条件によって切り替えるためのクラスらしい。
ImportSelector (Spring Framework 4.3.10.RELEASE API)

AOPの処理をProxyで実現するかASPECTJで実現するかで、設定を切り替えているっぽい。Proxyの場合はProxyAsyncConfigurationが読み込まれる。

public class AsyncConfigurationSelector extends AdviceModeImportSelector<EnableAsync> {
    ...
    @Override
    public String[] selectImports(AdviceMode adviceMode) {
        switch (adviceMode) {
            case PROXY:
                return new String[] { ProxyAsyncConfiguration.class.getName() };
            case ASPECTJ:
                return new String[] { ASYNC_EXECUTION_ASPECT_CONFIGURATION_CLASS_NAME };
            default:
                return null;
        }
    }

@EnableAsyncから読み込まれるConfiguration内で@Asyncを有効にする設定を書くことで、@EnableAsyncがついているときのみ@Asyncを有効にできる。

BPPを定義する

Springの初期化フェーズは大まかに、Bean定義の読み込み -> BFPPの実行 -> Bean生成、依存性の解決 -> BPPの実行 の流れになっている。
howtodoinjava.com

ここらへんを日本語で詳しく知りたい人は「Spring徹底入門」がわかりやすいですよ!(お金もらってないのにステマ
www.shoeisha.co.jp

今回のBPPはAsyncAnnotationBeanPostProcessorで、このBPPが実行される際にAsyncExecutionInterceptorをAdviceとして埋め込む。

先ほどのImportSelectorによって読み込まれた@Configurationクラス内で、AsyncAnnotationBeanPostProcessorがBean登録される。

@Configuration
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class ProxyAsyncConfiguration extends AbstractAsyncConfiguration {

    @Bean(name = TaskManagementConfigUtils.ASYNC_ANNOTATION_PROCESSOR_BEAN_NAME)
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    public AsyncAnnotationBeanPostProcessor asyncAdvisor() {
        Assert.notNull(this.enableAsync, "@EnableAsync annotation metadata was not injected");
        AsyncAnnotationBeanPostProcessor bpp = new AsyncAnnotationBeanPostProcessor();
        Class<? extends Annotation> customAsyncAnnotation = this.enableAsync.getClass("annotation");
        if (customAsyncAnnotation != AnnotationUtils.getDefaultValue(EnableAsync.class, "annotation")) {
            bpp.setAsyncAnnotationType(customAsyncAnnotation);
        }
        ...
        return bpp;
     }

BPPを実行する

Bean登録されたAsyncAnnotationBeanPostProcessorは、postProcessAfterInitializationメソッド内でAsyncAnnotationAdvisorを呼び出す。

このAsyncAnnotationAdvisorは、AdviceとしてAnnotationAsyncExecutionInterceptorクラスを、PointcutとしてAnnotationMatchingPointcutクラスを用いる。

特にPointcutについて抜粋する。

    protected Pointcut buildPointcut(Set<Class<? extends Annotation>> asyncAnnotationTypes) {
        ComposablePointcut result = null;
        for (Class<? extends Annotation> asyncAnnotationType : asyncAnnotationTypes) {
            Pointcut cpc = new AnnotationMatchingPointcut(asyncAnnotationType, true);
            Pointcut mpc = AnnotationMatchingPointcut.forMethodAnnotation(asyncAnnotationType);
            if (result == null) {
                result = new ComposablePointcut(cpc);
            }
            else {
                result.union(cpc);
            }
            result = result.union(mpc);
        }
        return result;
    }

PointcutとしてAnnotationMatchingPointcutが利用されている。このクラスのJavadocには以下のように書かれている。

Simple Pointcut that looks for a specific Java 5 annotation being present on a class or method.

AnnotationMatchingPointcut (Spring Framework 4.3.10.RELEASE API)


ということで、AnnotationMatchingPointcutが実行される仕組みがわかれば、抽象クラスに定義したアノテーションが引き継がれた仕組みもわかりそう。

先ほどのコードの、クラスに対するPointcutをnewしている部分について

    Pointcut cpc = new AnnotationMatchingPointcut(asyncAnnotationType, true);

javadocには以下のように書かれている。
AnnotationMatchingPointcut (Spring Framework 4.3.10.RELEASE API)

public AnnotationMatchingPointcut(Class classAnnotationType,
boolean checkInherited)
Create a new AnnotationMatchingPointcut for the given annotation type.
Parameters:
classAnnotationType - the annotation type to look for at the class level
checkInherited - whether to explicitly check the superclasses and interfaces for the annotation type as well (even if the annotation type is not marked as inherited itself)

継承先のクラスまでアノテーションがついているかを探索してくれるっぽい。

なるほど。じゃあ抽象クラスに定義したアノテーションも引き継がれるな!安心した!

AnnotationMatchingPointcut

AnnotationMatchingPointcutは条件に合致するクラスやメソッドをフィルタするが、フィルタ処理自体はAnnotationClassFilterやAnnotationMethodMatcherに処理を委譲している。

AnnotationClassFilterを見ると、アノテーションの検索処理はAnnotationUtilsに委譲していた。

public class AnnotationClassFilter implements ClassFilter {
    @Override
    public boolean matches(Class<?> clazz) {
        return (this.checkInherited ?
                (AnnotationUtils.findAnnotation(clazz, this.annotationType) != null) :
                clazz.isAnnotationPresent(this.annotationType));
    }  

AnnotationUtilsクラスでのアノテーションの検索は、以下のようにClassクラスのgetDeclaredAnnotationsメソッドを利用しながら、親クラスや継承元のアノテーションまで再帰的に探すことで実現していた。

  private static <A extends Annotation> A findAnnotation(Class<?> clazz, Class<A> annotationType, Set<Annotation> visited) {
      try {
          Annotation[] anns = clazz.getDeclaredAnnotations();
          for (Annotation ann : anns) {
              if (ann.annotationType() == annotationType) {
                  return (A) ann;
              }
          }
          for (Annotation ann : anns) {
              if (!isInJavaLangAnnotationPackage(ann) && visited.add(ann)) {
                  A annotation = findAnnotation(ann.annotationType(), annotationType, visited);
                  if (annotation != null) {
                      return annotation;
                  }
              }
          }
            // 再帰的に親クラスや継承元のアノテーションまで探索する

ClassクラスのgetDeclaredAnnotationsを試してみると

public class Main {
    public static void main(String[] args) {
        Stream.of(Children.class.getDeclaredAnnotations())
                .forEach(System.out::println);
        System.out.println("--------------");
        Stream.of(Parent.class.getDeclaredAnnotations())
                .forEach(System.out::println);
    }
}

@Async
@Service
class Parent{}

@Controller
class Children extends Parent{}

確かに指定したクラスのアノテーションしか取得できない。

@org.springframework.stereotype.Controller(value=)
--------------
@org.springframework.scheduling.annotation.Async(value=)
@org.springframework.stereotype.Service(value=)

親クラスのアノテーションや、アノテーションの継承元のアノテーション再帰的に探索すると循環参照による無限ループの危険があるはずだが、いい感じにAnnotationUtilsは処理してくれる。さすがSpringさん、アタマいい。

@EnableAsyncの雑学

コードリーディングしてる最中に知らなかった機能に気づいたのでメモる。

@EnableAsyncのannotation属性に指定すれば、@Async以外のアノテーションも非同期のためのアノテーションに指定できる。

@EnableAsync(annotation = MyAsync.class)
@SpringBootApplication
public class InheritApplication {

    public static void main(String[] args) {
        new SpringApplicationBuilder(InheritApplication.class).web(false).run(args);
    }

    @Bean
    public CommandLineRunner job(AsyncService service) {
        return i -> {
            service.exec();
        };
    }
}
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAsync {
}
@MyAsync //独自アノテーションをつける
public abstract class AsyncService {
    public abstract void exec();
}

結論

抽象クラスに定義したアノテーションは引き継がれる。(少なくともPointcutとしてAnnotationMatchingPointcutが使われていれば)

Spring 非同期タスクの同時実行数を制限する方法

Springには非同期処理を実行するための@Asyncアノテーションがある。
34. Task Execution and Scheduling
kimulla.hatenablog.com



この非同期タスクの同時実行数を絞るための方法をメモる。

デフォルトだと非同期処理の実行にSimpleAsyncTaskExecutorが使われるため、@Asyncの呼び出しごとにスレッドが生成されて即実行されてしまう。


TaskExecutorにThreadPoolTaskExecutorを指定すると、スレッドがプールされるようになる。ThreadPoolTaskExecutorはjava標準のThreadPoolExecutorをラップしたようなクラスのため、パラメータで以下の設定ができる。

corePoolSize - アイドルであってもプール内に維持されるスレッドの数
maximumPoolSize - プール内で可能なスレッドの最大数
keepAliveTime - スレッドの数がコアよりも多い場合、これは超過したアイドル状態のスレッドが新しいタスクを待機してから終了するまでの最大時間
unit - keepAliveTime 引数の時間単位
workQueue - タスクが超過するまで保持するために使用するキュー。このキューは、execute メソッドで送信された Runnable タスクだけを保持する

corePoolSize (getCorePoolSize() を参照) と maximumPoolSize (getMaximumPoolSize() を参照) で設定された境界に従って、ThreadPoolExecutor は自動的にプールサイズを調整します (getPoolSize() を参照)。新しいタスクが execute(java.lang.Runnable) メソッドで送信され、corePoolSize よりも少ない数のスレッドが実行中である場合は、その他のワークスレッドがアイドル状態であっても、要求を処理するために新しいスレッドが作成されます。corePoolSize よりも多く、maximumPoolSize よりも少ない数のスレッドが実行中である場合、新しいスレッドが作成されるのはキューがいっぱいである場合だけです。corePoolSize と maximumPoolSize を同じ値に設定すると、固定サイズのスレッドプールが作成されます。maximumPoolSize を Integer.MAX_VALUE などの実質的にアンバウンド形式である値に設定すると、プールに任意の数の並行タスクを格納することができます。コアプールサイズと最大プールサイズは構築時にのみ設定されるのがもっとも一般的ですが、setCorePoolSize(int) および setMaximumPoolSize(int) を使用して動的に変更することもできます。

ということで、corePoolSize=maximumPoolSize=制限したい同時実行数 にしてThreadTaskExecutorを生成すれば同時実行数が制御できる。

ついでに、QueueCapacityでいくつまでタスクをキューにため込むかも制御できる。

以下、SpringBoot 1.5.6.RELEASE で動作確認する。

まずは非同期処理を有効にするために@EnableAsyncをつける。またtaskExecutorを生成し、@QualifierでBeanに名前をつける。(TaskExecutorがひとつだけなら@Qualifierは不要)

@EnableAsync
@SpringBootApplication
public class AsyncApplication {

    public static void main(String[] args) {
        SpringApplication.run(AsyncApplication.class, args);
    }

    @Bean
    @Qualifier("heavyTaskTaskExecutor")
    public TaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(1);
        executor.setMaxPoolSize(1);
        executor.setQueueCapacity(5);
        return executor;
    }
}


非同期にしたい処理に@Asyncアノテーションをつける。また、名前をつけたTaskExecutorを利用するために@Asyncアノテーションの引数に指定する。

@Slf4j
@Service
public class TaskServiceImpl implements TaskService {

    @Async("heavyTaskTaskExecutor")
    @Override
    public void heavyTask() {
        try {
            log.info("start heavy task");
            TimeUnit.SECONDS.sleep(5);
            log.info("end heavy task");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

非同期処理の呼び出し側は呼び出すだけ。スレッドのキューサイズよりも多く呼び出した場合、TaskRejectedExceptionが発生する。そのため、@ExceptionHandlerでキャッチしてハンドリングする。

@RestController
@AllArgsConstructor
@RequestMapping("api/tasks")
public class TaskRestController {
    private final TaskService taskService;

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Boolean createNewTask() {
        taskService.heavyTask();
        return true;
    }

    @ExceptionHandler(TaskRejectedException.class)
    @ResponseStatus(HttpStatus.SERVICE_UNAVAILABLE)
    public String handle() {
        return "too busy";
    }

}

実行すると同時実行数が制御されていることがわかる。
f:id:kimulla:20170805215224p:plain

お手軽に非同期タスクの同時実行数を制限できた。

「mvn clean test」みたいに実行するコマンドと「mvn checkstyle:checkstyle」みたいに実行するコマンドとの差を理解したい

「:」なしで実行できるコマンドと、

mvn clean test 

「:」でつないで実行するコマンドとの差を理解するのが目的です。

mvn checkstyle:checkstyle

動作確認環境

mvn -v
Apache Maven 3.3.3 (7994120775791599e205a5524ec3e0dfe41d4a06; 2015-04-22T20:57:37+09:00)
Maven home: c:\Program Files\apache-maven-3.3.3
Java version: 1.8.0_25, vendor: Oracle Corporation
Java home: c:\Program Files\Java\jdk1.8.0_25\jre
Default locale: ja_JP, platform encoding: MS932
OS name: "windows 8.1", version: "6.3", arch: "amd64", family: "dos"

ライフサイクルとフェーズ

そもそもmavenには、ライフサイクルとフェーズという概念があるらしい。

7. Maven Tips | TECHSCORE(テックスコア)

Maven – Introduction to the Build Lifecycle


ライフサイクルは、フェーズをグループとしてまとめたもので、デフォルトは3つある。

  • default Lifecycle
  • clean Lifecycle
  • site Lifecycle

フェーズは、mavenが用意したライフサイクル中の各ステップ。
https://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html#Lifecycle_Reference

validate validate the project is correct and all necessary information is available.
initialize initialize build state, e.g. set properties or create directories.
generate-sources generate any source code for inclusion in compilation.
process-sources process the source code, for example to filter any values.
generate-resources generate resources for inclusion in the package.
process-resources copy and process the resources into the destination directory, ready for packaging.
compile compile the source code of the project.
process-classes post-process the generated files from compilation, for example to do bytecode enhancement on Java classes.
generate-test-sources generate any test source code for inclusion in compilation.
process-test-sources process the test source code, for example to filter any values.
generate-test-resources create resources for testing.
process-test-resources copy and process the resources into the test destination directory.
test-compile compile the test source code into the test destination directory
process-test-classes post-process the generated files from test compilation, for example to do bytecode enhancement on Java classes. For Maven 2.0.5 and above.
test run tests using a suitable unit testing framework. These tests should not require the code be packaged or deployed.
... ...


ポイントは、フェーズ自体は何らかの具体的な処理をするわけではなくて、そういうイベントってみんな共通してあるよね、くらいのものだということ。

それぞれのフェーズにpluginが登録でき、各フェーズで登録されたpluginのゴールを実行することで具体的な処理が実行できるようになる。

とはいえそれだとすぐ使い始められないので、組み込みでいくつかのpluginのゴールがフェーズに割り当てられている。(Built-in Lifecycle Bindings)

process-resources resources:resources
compile compiler:compile
process-test-resources resources:testResources
test-compile compiler:testCompile
test surefire:test


つまり、「mvn test」を実行したら裏で「mvn surefire:test」が実行されていたらしい。
(「mvn test」は指定されたフェーズまでの各フェーズ(compileとかもろもろ)も実行されるので、「mvn test」=「mvn surefire:test」ではない)

雑なたとえ話

フェーズとライフサイクルをカレーに例えると

  • カレーを作るライフサイクル
    • 具材を切るフェーズ
    • 具材を炒めるフェーズ
    • 具材を煮込むフェーズ
    • ルーを入れるフェーズ
    • 皿によそうフェーズ
  • 片づけるライフサイクル
    • 皿を集めるフェーズ
    • 洗うフェーズ

カレーの具材は人によって異なるため「具材を切るフェーズ」には自分で「たまねぎ切るplugin」や「ニンジン切るplugin」を割り当てる。

「ルーを入れるフェーズ」は当然99%の人がジャワカレーを選択するので、mavenが「ジャワカレーplugin」をデフォルトで割り当ててくれている。

具材を炒めるフェーズを実行すると、具材を切るフェーズから実行される。

うーん、あんまりわかりやすくならなかった。

サンプル

プロジェクトを作る。

mvn archetype:generate -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false -DgroupId=example.com -DartifactId=sample

echo-maven-pluginというechoするだけのpluginをtestフェーズに割り当てる。

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>example.com</groupId>
  <artifactId>sample</artifactId>
  <packaging>jar</packaging>
  <version>1.0-SNAPSHOT</version>
  <name>sample</name>
  <url>http://maven.apache.org</url>
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>com.soebes.maven.plugins</groupId>
        <artifactId>echo-maven-plugin</artifactId>
        <version>0.3.0</version>
        <executions>
          <execution>
            <phase>test</phase>
            <goals>
              <goal>echo</goal>
            </goals>
          </execution>
        </executions>
        <configuration>
          <echos>
            <echo>This is the Test Text </echo>
          </echos>
        </configuration>
      </plugin>
    </plugins>
  </build>

</project>

testフェーズを実行するとecho-maven-pluginも実行されることがわかる。
ついでに、testフェーズまでの各フェーズも実行されている。

$ mvn test
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building sample 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ sample ---
[WARNING] Using platform encoding (MS932 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory c:\Users\pbreh_000\Desktop\study\sample\src\main\resources
[INFO]
[INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ sample ---
[INFO] Nothing to compile - all classes are up to date
[INFO]
[INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ sample ---
[WARNING] Using platform encoding (MS932 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory c:\Users\pbreh_000\Desktop\study\sample\src\test\resources
[INFO]
[INFO] --- maven-compiler-plugin:3.1:testCompile (default-testCompile) @ sample ---
[INFO] Nothing to compile - all classes are up to date
[INFO]
[INFO] --- maven-surefire-plugin:2.12.4:test (default-test) @ sample ---
[INFO] Surefire report directory: c:\Users\pbreh_000\Desktop\study\sample\target\surefire-reports

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running com.example.AppTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.002 sec

Results :

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

[INFO]
[INFO] --- echo-maven-plugin:0.3.0:echo (default) @ sample ---
[INFO] This is the Test Text
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1.049 s
[INFO] Finished at: 2017-07-31T20:24:31+09:00
[INFO] Final Memory: 11M/304M
[INFO] ------------------------------------------------------------------------

実効pomを表示すれば、各フェーズで実行されるpluginが調べられる。

mvn help:effective-pom
...

      <plugin>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>2.12.4</version>
        <executions>
          <execution>
            <id>default-test</id>
            <phase>test</phase>
            <goals>
              <goal>test</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
...

フェーズをスキップする

フェーズに割り当てられたpluginの実行をスキップする方法は各pluginごとに用意されている。(例えば、surefire:testだと-Dmaven.test.skip=trueや-DskipTests=trueだったり、install:installだと-Dmaven.install.skip=true)

しかし、フェーズ自体は飛ばせないっぽい。
試しにコマンド打ってみても、echo-maven-pluginは動いてしまう。

mvn clean package -Dmaven.test.skip=true
...
[INFO] --- maven-compiler-plugin:3.1:testCompile (default-testCompile) @ sample ---
[INFO] Not compiling test sources
[INFO]
[INFO] --- maven-surefire-plugin:2.12.4:test (default-test) @ sample ---
[INFO] Tests are skipped.
[INFO]
[INFO] --- echo-maven-plugin:0.3.0:echo (default) @ sample ---
[INFO] This is the Test Text
[INFO]
[INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ sample ---
[INFO] Building jar: c:\Users\pbreh_000\Desktop\study\sample\target\sample-1.0-SNAPSHOT.jar
...

フェーズに割り当てたすべてのpluginをスキップさせるなら、Profileを使えば良さそう。

https://stackoverflow.com/questions/3147714/how-to-skip-install-phase-in-maven-build-if-i-already-have-this-version-installe

ゴール

mavenのpluginは各フェーズに割り当てなくても使える。そのときは以下の形式で実行する。

mvn prefix:goal

echo-maven-pluginだと、

$ mvn echo:echo
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building sample 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- echo-maven-plugin:0.3.0:echo (default-cli) @ sample ---
[INFO] This is the Test Text
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 0.147 s
[INFO] Finished at: 2017-07-31T20:48:17+09:00
[INFO] Final Memory: 8M/304M
[INFO] ------------------------------------------------------------------------

ゴールはゴールしか実行されない。pluginによってはcompileフェーズで生成されたclassesファイルを使うもの(findbugsとか?)もあるので、注意する。


で、このprefix:goalのprefixってなに指定すりゃいいねんと思いながらいつもググってたけど、ちゃんとしたルールがあるらしい。

https://maven.apache.org/guides/introduction/introduction-to-plugin-prefix-mapping.html

またprefix:goalのgoalついても一覧を表示できる。

ググったら詳しいサイトがあったので詳細はこちらを参照。
qiita.com


とりあえずhelpゴールを利用すればいいっぽい。

$ mvn echo:help
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building sample 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- echo-maven-plugin:0.3.0:help (default-cli) @ sample ---
[INFO] Maven Echo Plugin 0.3.0
  The Echo Maven Plugin is intended to print out messages during the build.

This plugin has 3 goals:

echo:echo

echo:format

echo:help
  Display help information on echo-maven-plugin.
  Call mvn echo:help -Ddetail=true -Dgoal=<goal-name> to display parameter
  details.
 ...

結論

「mvn clean test」みたいに実行するコマンドと「mvn checkstyle:checkstyle」みたいに実行するコマンドとの差は、「フェーズ」か「ゴール」か。

Vuejsでdataプロパティに値を動的に追加する方法(といいつつJSの基本を理解してなかったという話)

Vuejsのバージョン

この記事はVuejs v2.3.3 で動作確認をしました。

  ...
  "dependencies": {
    "vue": "^2.3.3",
    "vue-router": "^2.6.0"
  },
  ...

Vuejsのdataプロパティ

Vuejsはdataオブジェクトのプロパティをリアクティブな値として扱う。つまり、dataオブジェクトのプロパティの値が書き換わったタイミングで、HTMLも書き換わる。

例を挙げると

<template>
  <div>
    <h1>{{ msg }}</h1>
    <button type="button" @click="change">change</button>
  </div>
</template>

<script>
export default {
  name: 'sample',
  data () {
    return {
      msg: 'hello world'
    }
  },
  methods: {
    change: function () {
      this.msg = 'good bye'
    }
  }
}
</script>

<style scoped>
</style>

この単一コンポーネントのファイルをブラウザで表示すると、以下のように {{ msg }} 部分がdataオブジェクトのmsgプロパティの値である「hello world」に置き換えられたものが表示される。
f:id:kimulla:20170724215420p:plain

ボタンをクリックすると、メッセージが「good bye」に変わる。
f:id:kimulla:20170724215507p:plain

これは、

  1. HTMLのボタンをクリックするとclickイベントが発生し
  2. @click(イベントハンドラ的なもの)で指定したchangeメソッドが呼ばれ
  3. Vueインスタンスのdataオブジェクトのmsgプロパティが書き換わり
  4. dataオブジェクトのプロパティの変更をVueが検知して
  5. VuejsがHTMLに値を反映するため


このプロパティの変更の検知は、JSの機能的な制限(Object.observe)によってプロパティ自体の追加には対応していない。

つまり以下のような処理では、動的に追加したプロパティは無視されてしまう。

<template>
  <div>
    <ul>
      <li v-for="(value,key) in favorites">
        {{value}} likes {{key}}
      </li>
    </ul>
    <button type="button" @click="add">add charie</button>
  </div>
</template>

<script>
export default {
  name: 'sample',
  data () {
    return {
      favorites: {
        'alice': 'apple',
        'bob': 'banana'
      }
    }
  },
  methods: {
    add: function () {
      // 動的にプロパティとして値を追加する
      this.favorites.charie = 'cherry'
    }
  }
}
</script>

<style scoped>
</style>

実際にブラウザでボタンをクリックし、動的にdataオブジェクトにプロパティを追加してみる。
f:id:kimulla:20170724220021p:plain

Chrome Developer Toolで確認すると、favoritesに値が追加されているにもかかわらず、その値が画面に反映されていない。
f:id:kimulla:20170724220508p:plain


で、これをどうするかというのも公式に書いてある。
https://jp.vuejs.org/v2/guide/reactivity.html#変更検出の注意事項

抜粋すると

Vue はすでに作成されたインスタンスに対して動的に新しいルートレベルのリアクティブなプロパティを追加することはできません。しかしながら Vue.set(object, key, value) メソッドを使うことで、ネストしたオブジェクトにリアクティブなプロパティを追加することができます:
グローバル Vue.set の単なるエイリアスとなっている vm.$set インスタンスメソッドを使用することもできます:
Vue.set(vm.someObject, 'b', 2)


ここからが自分にとっては本題で、これを単一コンポーネントでどう使えばいいの?と悩んでしまった。

リファレンスみたいにVueコンポーネントなんてimportしてないし、vmインスタンスなんて変数に格納してないし、どうすればいいの?みたいな


でひとまずメソッド内ではthisがVueを指す(dataオブジェクトへの値の変更もthis.msgとしているように)とリファレンスに載っていたので、とりあえずchangeメソッド内でthisをChrome Developer Toolsで表示してみた。

すると

f:id:kimulla:20170724223116p:plain

$setプロパティがない・・・?



いちおう this.$set も試すと・・・

f:id:kimulla:20170724220443p:plain

$setプロパティがある・・・?ある!



JSでは、prototypeで定義されたメソッドはそのオブジェクトのプロパティとしては見えないらしい。
vividcode.hatenablog.com


コンソールで試してみると、確かにその通り。
f:id:kimulla:20170724220750p:plain

代わりに for..in.. を使えば全プロパティ表示される。なるほど。
f:id:kimulla:20170724220801p:plain


Vueでも確かめてみると確かに$setプロパティある!
f:id:kimulla:20170724220840p:plain

プロパティを片っ端から調べたければ for..in..で調べれば良かったのか。


ということで以下の通りにすれば動作します。

...
  methods: {
    add: function () {
      this.$set(this.favorites, 'charie', 'cherry')
    }
  }
...

めでたしめでたし。
f:id:kimulla:20170724223747p:plain


ちなみにディレクティブの値に直接書くならthisは不要。JSのthisはコンテキストによって指す場所変わるから・・・

    ...
    <button type="button" @click="$set(favorites, 'charie', 'cherry')">add charie</button>
  </div>

結論

Vueうんぬんの前にJSのスコープ周りをいちから勉強し直そう。

ついでに

テンプレート内の key,valueの順番を間違えて、「Apple likes Alice」みたいな食人植物が誕生してるけど、スクショ張り直すのめんどくさいので放置。

...
      <li v-for="(value,key) in favorites">
    // 誤
        {{value}} likes {{key}}
    // 正
        {{key}} likes {{value}}
      </li>
...