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

5年目のSIerのブログです

Vuejs formタグ内のテキストフィールドでEnter押下時に、メソッドを実行する

SPAの場合はformタグに任せてデータをPOSTすることは稀で、通常は自身で定義したハンドラを呼び出してAjax通信を行う。

以下は、ボタン押下時にexecメソッドを実行する例。

<template>
  <form>
    <input type="text" placeholder="username" />
    <input type="text" placeholder="password" />
    <button type="submit" @click="exec">submit</button>
  </form>
</template>

<script>
export default {
  name: 'HelloWorld',
  methods: {
    exec: function () {
      // 本来はajax通信をする
      console.log('exec')
    }
  }
}
</script>

で、このとき、テキストフィールドでEnterを押してしまうと、一瞬コンソールログが出力された後に、自画面に遷移してしまう。

f:id:kimulla:20171103160527g:plain


これはブラウザの動作が関連していて、HTML5の仕様書から読み取れることは

  • formタグ内のテキストフィールドでEnterを押すと、自動で送信されるようにしたら?(Implicit submission)
  • Enterを押すと、submitボタンに click イベントが発生する
  • 入力フィールドのvalidationに引っかかったらformタグからinvalidイベントが発生する
  • もしvalidationに問題なかったら formタグからsubmitイベントが発生する
  • 誰もsubmitイベントをキャッチしなかったらformが送信される
  • formのaction属性が空だったら、ドキュメントが配置されているアドレスに送信する

4.10 Forms — HTML5

3 Semantics, structure, and APIs of HTML documents — HTML5


ということで、Enter押下時に自画面への遷移を防いでボタン押下時と同じふるまいにするためには、Vuejsの機能を利用してsubmitイベントを黙殺して、自身が作成したハンドラを実行するとよい。そのための仕組みがVuejsに存在する。

jp.vuejs.org

<template>
  <form v-on:submit.prevent="exec">
    <input type="text" placeholder="username" />
    <input type="text" placeholder="password" />
    <button type="submit">submit</button>
  </form>
</template>

<script>
export default {
  name: 'HelloWorld',
  methods: {
    exec: function () {
      // 本来はajax通信をする
      console.log('exec')
    }
  }
}
</script>

これでformタグ内のテキストフィールドでEnterを押下したときも、ボタン押下したときと同じふるまいにできる。
f:id:kimulla:20171103164159g:plain

Vuejsのリファレンスにpreventとかの修飾子が紹介されてるけど、ブラウザの仕組みに詳しくないと使いどころがわからないんだよなぁ。

なぜsubmitにするか?

あとは、submitにするとHTML上でのバリデーションがかけられます。

<template>
  <form>
    <input type="email" placeholder="email" />
    <input type="text" placeholder="password" />
    <button type="button" @click="exec">submit</button>
  </form>
</template>

email形式のチェックが実行されない。
f:id:kimulla:20171107063631g:plain

<template>
  <form>
    <input type="email" placeholder="email" />
    <input type="text" placeholder="password" />
    <button type="submit" @click="exec">submit</button>
  </form>
</template>

email形式のチェックが実行される。
f:id:kimulla:20171107063642g:plain

docker-composeを複数起動する

$ docker -v
Docker version 17.06.1-ce, build 874a737
$ docker-compose -v
docker-compose version 1.16.1, build 6d1ac21

docker-composeは複数のコンテナを手軽に扱うためのツール。

yml形式でコンテナに関する設定ができ、

sample]$  ls
docker-compose.yml
version: "3"
services:
    ap:
        image: "centos:7"
        command: ["tail","-f","/dev/null"]
        depends_on:
            - "db"
    db:
        image: 'centos:7'
        command: ["tail","-f","/dev/null"]

up コマンドでコンテナが起動できる。

sample]$ docker-compose up -d
sample_db_1 is up-to-date
Stopping and removing sample_ap_2 ... done
Starting sample_ap_1 ... done
sample]$ docker-compose ps
   Name            Command        State   Ports
-----------------------------------------------
sample_ap_1   tail -f /dev/null   Up
sample_db_1   tail -f /dev/null   Up

コンテナはsample_defaultというネットワークに接続され、

sample]$ docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
95c40fbb1b45        bridge              bridge              local
b26153e78e9d        host                host                local
9fd6390f2612        none                null                local
3ff92f46112a        sample_default      bridge              local
sample]$ docker inspect sample_ap_1
...
            "Networks": {
                "sample_default": {
                    "IPAMConfig": null,
                    "Links": null,
                    "Aliases": [
                        "ap",
                        "8c52534eb94b"
                    ],
...


コンテナ間は /etc/hosts に設定されたサービス名でアクセスできる。

sample]$ docker exec -it sample_ap_1 ping db
PING db (172.19.0.3) 56(84) bytes of data.
64 bytes from sample_db_1.sample_default (172.19.0.3): icmp_seq=1 ttl=64 time=0.039 ms
64 bytes from sample_db_1.sample_default (172.19.0.3): icmp_seq=2 ttl=64 time=0.097 ms

コンテナを複数起動する場合は --scale オプションを利用する。

sample]$ docker-compose up -d --scale ap=1 --scale db=2
Starting sample_db_1 ... done
Creating sample_db_2 ...
Creating sample_db_2 ... done
sample_ap_1 is up-to-date
sample]$ docker-compose ps
   Name            Command        State   Ports
-----------------------------------------------
sample_ap_1   tail -f /dev/null   Up
sample_db_1   tail -f /dev/null   Up
sample_db_2   tail -f /dev/null   Up

図にするとこんな感じ。
f:id:kimulla:20171023120731j:plain

ただし --scale オプションでコンテナを複数起動すると、コンテナ間リンクがどのように張られるかわからない。

以下の実行結果だと、sample_ap_1とsample_ap_2がどちらもsample_db_1に接続されてしまっている。

docker-compose up -d --scale ap=2 --scale db=2
sample_db_1 is up-to-date
sample_db_2 is up-to-date
Starting sample_ap_1 ... done
Creating sample_ap_2 ...
Creating sample_ap_2 ... done
[kimura@localhost sample]$ docker exec -it sample_ap_1 ping db
PING db (172.19.0.3) 56(84) bytes of data.
64 bytes from sample_db_1.sample_default (172.19.0.3): icmp_seq=1 ttl=64 time=0.044 ms
64 bytes from sample_db_1.sample_default (172.19.0.3): icmp_seq=2 ttl=64 time=0.100 ms
^C
--- db ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1001ms
rtt min/avg/max/mdev = 0.044/0.072/0.100/0.028 ms
[kimura@localhost sample]$ docker exec -it sample_ap_2 ping db
PING db (172.19.0.3) 56(84) bytes of data.
64 bytes from sample_db_1.sample_default (172.19.0.3): icmp_seq=1 ttl=64 time=0.063 ms
64 bytes from sample_db_1.sample_default (172.19.0.3): icmp_seq=2 ttl=64 time=0.096 ms
^C

ビルドやテストをdocker-compose上で行う場合、一つのDBが複数のAPから接続される可能性があるため、docker-compose up ごとに独立した環境を用意したい。図にすると以下のイメージ。
f:id:kimulla:20171023121220j:plain

上記を実現するためには、 -p オプションを利用する。

sample]$ docker-compose up -d
Stopping and removing sample_db_2 ... done
Starting sample_db_1 ...
Starting sample_db_1 ... done
Stopping and removing sample_ap_2 ... done
Starting sample_ap_1 ...
Starting sample_ap_1 ... done
sample]$ docker-compose -p other up -d
Creating other_db_1 ...
Creating other_db_1 ... done
Creating other_ap_1 ...
Creating other_ap_1 ... done
sample]$ docker ps
CONTAINER ID        IMAGE               COMMAND               CREATED             STATUS              PORTS               NAMES
d5badecce352        centos:7            "tail -f /dev/null"   19 seconds ago      Up 18 seconds                           other_ap_1
dee838c0cf24        centos:7            "tail -f /dev/null"   19 seconds ago      Up 18 seconds                           other_db_1
8c52534eb94b        centos:7            "tail -f /dev/null"   2 hours ago         Up 25 seconds                           sample_ap_1
c11653681145        centos:7            "tail -f /dev/null"   2 hours ago         Up 25 seconds                           sample_db_1
sample]$ docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
...
40c106123924        other_default       bridge              local
3ff92f46112a        sample_default      bridge              local

これで独立した環境を用意できる。

なぜ Spring Boot は Tomcat のログをアプリケーションから管理できるのか

Tomcatにwarをデプロイして運用することに慣れ親しんでいる自分は

Tomcatのログは conf/logging.properties で設定する
Apache Tomcat 8 (8.5.23) - Logging in Tomcat

と思っていましたが、Spring Bootから制御できています。

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.5.8.RELEASE)

2017-10-22 18:59:44.026  INFO 11864 --- [           main] c.example.embedded.EmbeddedApplication   : Starting EmbeddedApplication on kimura-pc with PID 11864 (C:\Users\pbreh_000\Desktop\study\log\embedded\target\classes started by pbreh_000 in C:\Users\pbreh_000\Desktop\study\log\embedded)
2017-10-22 18:59:44.029  INFO 11864 --- [           main] c.example.embedded.EmbeddedApplication   : No active profile set, falling back to default profiles: default
2017-10-22 18:59:44.114  INFO 11864 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@33cb5951: startup date [Sun Oct 22 18:59:44 JST 2017]; root of context hierarchy
2017-10-22 18:59:45.077  INFO 11864 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8080 (http)
2017-10-22 18:59:45.086  INFO 11864 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2017-10-22 18:59:45.087  INFO 11864 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/8.5.23

※ 最終行にTomcatのログが出力されている


どういう事なんでしょうか?

結論

  • warをデプロイするときは、アプリケーションからログを制御できない
  • 組み込みTomcatのときは、アプリケーションからログを制御できる

詳細

以下の話を読む際に前提となる内容です。
kimulla.hatenablog.com

Tomcatの内部ロガー

リファレンスによると
Apache Tomcat 8 (8.5.23) - Logging in Tomcat

  • Tomcatは JULI という独自ロガーの実装を持っている
  • JULI は Apache Commons Logging をforkしてパッケージ名を変えている
  • JULI は 実装クラスにjava.util.loggingを使うようにハードコードされている
  • java.util.logging はデフォルトだと VMごとにしか設定が切り替えられないため、独自のLogManager(ClassLoaderLogManager)によってクラスローダごとに設定を読み込めるようにしている

Tomcatにwarをデプロイするとき

catalina.shを利用したときの起動クラスは org.apache.catalina.startup.Bootstrap

Tomcatのクラスローダはwarごとにライブラリを管理できるように階層化されている。
Apache Tomcat 8 (8.5.23) - Class Loader HOW-TO

  Bootstrap
          |
       System
          |
       Common
       /     \
  Webapp1   Webapp2 ...

親のクラスローダ(複数の場合あり)が要求されたクラスやリソースを見つけられなかった場合だけ自分のリポジトリを見にいく。

warに jul-to-slf4j.jar を含めてログ実装を差し替えようとしても、Tomcatのクラスを読み込んでいるクラスローダ(Common)から、WEBアプリケーションのクラスは参照できないため、ログ実装をアプリケーションから差し替えられない。


組み込みTomcatとして使うとき

組み込みTomcatの起動クラスは org.apache.catalina.startup.Tomcat

org.apache.catalina.startup.Bootstrap で起動したときとは違い、Catalina系のクラスとWEBアプリケーションのクラスが同一クラスローダに読み込まれる。

そのため jul-to-slf4j.jar をクラスパスに通せば、ロガーの差し替えができる。

試してみる

pom.xmlを用意する。

<?xml version="1.0" encoding="UTF-8"?>
<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/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <packaging>jar</packaging>

    <groupId>example.com</groupId>
    <artifactId>embedded-tomcat-logging</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-core</artifactId>
            <version>8.5.23</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-core</artifactId>
            <version>1.2.3</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jul-to-slf4j</artifactId>
            <version>1.7.25</version>
        </dependency>
    </dependencies>

    <properties>
        <java.version>1.8</java.version>
        <maven.compiler.target>${java.version}</maven.compiler.target>
        <maven.compiler.source>${java.version}</maven.compiler.source>
    </properties>
</project

logback.xmlを用意する。

<configuration>

    <contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator">
        <resetJUL>true</resetJUL>
    </contextListener>

    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%c %d [%thread] %-5level - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="info">
        <appender-ref ref="STDOUT"/>
    </root>

</configuration>

組み込みTomcatを起動する。

public class Main {
    public static void main(String[] args) throws LifecycleException, InterruptedException {
        SLF4JBridgeHandler.removeHandlersForRootLogger();
        SLF4JBridgeHandler.install();
        Tomcat tomcat = new Tomcat();
        tomcat.setPort(8080);
        Context ctx = tomcat.addContext("/sample", new File(".").getAbsolutePath());
        Logger logger = LoggerFactory.getLogger(Main.class);
        logger.info("start...");

        Tomcat.addServlet(ctx, "hello", new HttpServlet() {
            protected void service(HttpServletRequest req, HttpServletResponse resp)
                    throws ServletException, IOException {
                Writer w = resp.getWriter();
                w.write("Hello, World!");
                w.flush();
            }
        });
        ctx.addServletMappingDecoded("/*", "hello");

        tomcat.start();
        tomcat.getServer().await();
    }
}

コンソール出力を見ると、Logbackに実装が切り替えられている。

Main 2017-10-22 20:04:33,884 [main] INFO  - start...
org.apache.coyote.http11.Http11NioProtocol 2017-10-22 20:04:34,066 [main] INFO  - Initializing ProtocolHandler ["http-nio-8080"]
org.apache.tomcat.util.net.NioSelectorPool 2017-10-22 20:04:34,124 [main] INFO  - Using a shared selector for servlet write/read
org.apache.catalina.core.StandardService 2017-10-22 20:04:34,126 [main] INFO  - Starting service [Tomcat]
org.apache.catalina.core.StandardEngine 2017-10-22 20:04:34,126 [main] INFO  - Starting Servlet Engine: Apache Tomcat/8.5.23
org.apache.coyote.http11.Http11NioProtocol 2017-10-22 20:04:34,246 [main] INFO  - Starting ProtocolHandler ["http-nio-8080"]

実際にクラスローダを確認してみると、Tomcat関連のクラスとWEBアプリケーションのクラスが同一のクラスローダに読み込まれている。

f:id:kimulla:20171022200154p:plain

Spring Boot

Spring Bootはもう少し複雑なことをしていて、Java標準のクラスローダで spring boot loader classes を読み込み、そこから依存する各jarをロードしているようす。

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

Appendix E. The executable jar format

まあどっちにしろTomcat関連のクラスからWEBアプリケーションのクラスが参照できているので、同じことかなと。

f:id:kimulla:20171022220026p:plain

f:id:kimulla:20171022221017p:plain

注意

「よし、これでTomcat関連のログはアプリケーションのログ設定で一元管理できるようになった」といきたいところだけど、アクセスログは上記のログでは出力されない。

Access logging is a related but different feature, which is implemented as a Valve. It uses self-contained logic to write its log files.


Spring Boot だとプロパティで設定できるようす。
Appendix A. Common application properties

server.tomcat.accesslog.directory=logs # Directory in which log files are created. Can be relative to the tomcat base dir or absolute.
server.tomcat.accesslog.enabled=false # Enable access log.
server.tomcat.accesslog.file-date-format=.yyyy-MM-dd # Date format to place in log file name.
server.tomcat.accesslog.pattern=common # Format pattern for access logs.
server.tomcat.accesslog.prefix=access_log # Log file name prefix.
...

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が使われていれば)