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

5年目のSIerのブログです

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

Jenkinsジョブ(ビルド~テスト)をDockerコンテナ上で実行する ~Docker Pipeline Pluginを使ってみる~

ビルドやテストなどの一連の作業をJenkinsで自動実行することを考える。
通常これらのジョブはJenkinsのmasterやslaveで実行されることになり、ビルドに必要な環境構築(例えばjavaのインストールやmavenのインストール)はJenkinsのmasterやslaveに対して、事前に行うことになる。そして一度環境構築したサーバは長いこと使われる。

そうなると、同一サーバで複数バージョンのjavaを入れる苦労をしたりディストリビューションの差に基づくLinuxコマンドのあるなしに翻弄されたり、ローカルにしかないファイルを参照してそのうちジョブが動かなくなったりする。

これを解決するための手段としてビルド〜テストをDockerコンテナ上で実行しようという例は多くみられ、以下の記事が導入検討の参考になる。

www.buildinsider.net

www.slideshare.net


以下、JenkinsジョブをDockerコンテナ上で実行するための方法を調べたときのメモ。
Jenkinsのpipelineが前提。使い方については昔にまとめました。

kimulla.hatenablog.com

JenkinsのDocker関連のplugin

Jenkins上でDockerコンテナを利用する方法は複数あるらしく

f:id:kimulla:20170612003857j:plain

Docker Pluginは、Dockerイメージを指定してスレーブに登録する作業が必要になるし、バージョン管理しているDockerfileからイメージを毎回生成する用途には向いてなさそう。(偏見かも)


またpluginを使わなくともJenkins上でshが実行できるので、自分でDockerコンテナを管理する方法もある。以下の記事がとても参考になった。

nulab-inc.com

現在自分がやってること


pluginは特に使わず、自身でDockerコンテナを管理している。
具体的には以下の4ステップをMakefile内で行っている。

  • Dockerイメージの生成
  • Dockerコンテナの生成とタスクの実行
  • 成果物の取り出し
  • Dockerコンテナの破棄

また、mavenやnpmのライブラリがコンテナ上で毎回全ダウンロードされないように、ホストにマウントしてキャッシュしている。

現在自分がやってることの詳細

以下にサンプルを作ったので抜粋する。
github.com

基本的にDockerfileを自分でビルドしてコンテナを起動している。
主なファイルは以下の4つ。

ファイル 説明
Dockerfile.build ビルドサーバを構築するためのDockerfile
Dockerfile.integration 結合試験を実施するためのDockerfile
Makefile 開発時にローカル環境で利用するコマンドをまとめたMakefile
Makefile.docker dockerコンテナを操作するためのコマンドをまとめたMakefile
Makefile

ビルドに必要なタスクはMakefileにまとめて記述する。

setup:
        npm --prefix client install

build: setup
        npm --prefix client run build
        cp -R client/dist/* server/src/main/resources/static
        mvn -f server/pom.xml package -DskipTests=true

使い方

# server/target配下にjarができる
make build
Dockerfile.build

ビルドサーバをDockerイメージとして構築する。
タスクはMakefileにまとまっているので、ENTRYPOINTにmakeを指定する。

FROM openjdk:8u131-jdk-alpine

RUN apk update &amp;&amp; apk upgrade &amp;&amp; \
    apk add curl nodejs-npm=6.10.3-r0 maven=3.3.9-r1 make

RUN curl -Ls "https://github.com/dustinblackman/phantomized/releases/download/2.1.1/Dockerized-phantomjs.tar.gz" | tar xz -C /
WORKDIR /build
COPY . /build

ENTRYPOINT ["make"]
CMD ["help"]
Makefile.docker

Dockerの操作が煩雑(イメージの作成~コンテナの生成~ビルドの実行~成果物の取り出し~破棄)なのでMakefileにまとめている。

# ホストで複数のDockerを起動できるようにイメージ/コンテナ名にidを付与する
ID = 'default'
BUILD_SERVER_IMAGE = sample-base-build-server-$(ID)
BUILD_CONTAINER    = sample-build-$(ID)
CACHE_PATH='/tmp/docker/cache'

base:
        docker build -t $(BUILD_SERVER_IMAGE) -f Dockerfile.build .

build: base
        docker run --name $(BUILD_CONTAINER) -v $(CACHE_PATH)/.m2:/root/.m2 -v $(CACHE_PATH)/.node_modules:/build/client/node_modules $(BUILD_SERVER_IMAGE) build || Docker rm $(BUILD_CONTAINER)
        docker cp $(BUILD_CONTAINER):/build/server/target server
        docker rm $(BUILD_CONTAINER

使い方

# コンテナ内でビルドしてホスト環境のserver/target配下にjarをコピーしてくる
make ID=1 -f Makefile.docker build
Jenkinsfile

Makefile.dockerを実行するだけ。

node {
  stage('build') {
      sh 'make ID=${BUILD_ID} -f Makefile.docker build'  
      archiveArtifacts 'server/target/*jar'
  }
  ...
}

自分でDockerを管理する問題点

成果物の取り出しやコンテナの破棄を作り込む(考慮する)のがめんどくさい。
対応できる範囲だけど、なんだかんだめんどくさい。

ジョブ実行時のファイルパスについても考慮が必要で、コンテナ内でビルドしたときのパスとJenkinsのworkspaceのパスが違うとlintツールの出力結果で問題になることがある。
例えばcheckstyleのテスト結果(checkstyle-result.xml)は各javaファイルへのリンクがフルパスで記述されるので、lint実行時のDockerコンテナ内のパスとJenkinsのworkspaceのパスが違うと出力レポートのリンクが開けないなんてこともある。

f:id:kimulla:20170611235823p:plain

f:id:kimulla:20170612002455p:plain

ただこれはコンテナのworkspaceをDocker起動時のディレクトリと同じにすれば解決する問題。

でも

つもりつもると・・・めんどくさい!

Docker Pipeline Plugin

そこでDocker Pipeline Pluginですよ!!!

CloudBees Jenkins Enterprise User Guide

動作確認環境

  • Jenkins version 2.46.3
  • Docker version 17.03.1-ce, build c6d412e

使い方(scripted pipeline)

新しめのバージョンだと、Global Variable Referenceに載ってる。

Global Variable Referenceはここ。
f:id:kimulla:20170611235425p:plain

f:id:kimulla:20170611235434p:plain

既存イメージを利用する場合

Pipelineプロジェクトを作成する。

f:id:kimulla:20170611235500p:plain

とりあえずGlobal Variable Referenceに記述されてる動きを確認するために、scripted pipelineで記述する。


f:id:kimulla:20170612004723p:plain

node {
    sh 'id'
    docker.image('openjdk:8u131-jdk').inside() {
        sh 'java -version'
    }
}

ビルド実行をクリックする。

f:id:kimulla:20170611235530p:plain

コンソール出力を読む。

f:id:kimulla:20170612004738p:plain


裏でDockerコマンドを実行してるのがわかる。

ユーザーanonymousが実行
[Pipeline] node
Running on master in /home/kimura/.jenkins/workspace/sample
[Pipeline] {
[Pipeline] sh
[sample] Running shell script
+ id
uid=1000(kimura) gid=1000(kimura) groups=1000(kimura),10(wheel),987(docker) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
[Pipeline] sh
[sample] Running shell script
+ docker inspect -f . openjdk:8u131-jdk
.
[Pipeline] withDockerContainer
Jenkins does not seem to be running inside a container
$ docker run -t -d -u 1000:1000 -w /home/kimura/.jenkins/workspace/sample -v /home/kimura/.jenkins/workspace/sample:/home/kimura/.jenkins/workspace/sample:rw -v /home/kimura/.jenkins/workspace/sample@tmp:/home/kimura/.jenkins/workspace/sample@tmp:rw -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** --entrypoint cat openjdk:8u131-jdk
[Pipeline] {
[Pipeline] sh
[sample] Running shell script
+ java -version
openjdk version "1.8.0_131"
OpenJDK Runtime Environment (build 1.8.0_131-8u131-b11-1~bpo8+1-b11)
OpenJDK 64-Bit Server VM (build 25.131-b11, mixed mode)
[Pipeline] }
$ docker stop --time=1 c98ccf713004056fc3a59cebe1b4cd1f924653c63e0ba2301ed0fa9ed86f976d
$ docker rm -f c98ccf713004056fc3a59cebe1b4cd1f924653c63e0ba2301ed0fa9ed86f976d
[Pipeline] // withDockerContainer
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
Finished: SUCCESS

コンテナ起動部分について抜粋し、さらに見ていく。


ローカル環境で実行したときと同じ環境になるように、

  • -wコマンドでコンテナのworkspaceをJenkinsのworkspaceと合わせている
  • -vコマンドでJenkinsのworkspaceをコンテナのworkspaceディレクトリにマウントしている
  • -uコマンドでJenkinsの起動ユーザでコンテナを実行している
...
+ id
uid=1000(kimura) gid=1000(kimura) groups=1000(kimura),10(wheel),987(docker) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
...
$ docker run -t -d -u 1000:1000 -w /home/kimura/.jenkins/workspace/sample -v /home/kimura/.jenkins/workspace/sample:/home/kimura/.jenkins/workspace/sample:rw -v /home/kimura/.jenkins/workspace/sample@tmp:/home/kimura/.jenkins/workspace/sample@tmp:rw -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** --entrypoint cat openjdk:8u131-jdk
...

あーなるほどー、そうすればよかったのか。


inside(){...}内で記述した...はコンテナ内で実行されている。

[Pipeline] sh
[sample] Running shell script
+ java -version
openjdk version "1.8.0_131"
OpenJDK Runtime Environment (build 1.8.0_131-8u131-b11-1~bpo8+1-b11)
OpenJDK 64-Bit Server VM (build 25.131-b11, mixed mode)
[Pipeline] }

コンテナが残らないようにstopしてrmしている。

...
[Pipeline] }
$ docker stop --time=1 a900ace94102ba4096ddeec5d172ae010d16379a8d0c486dbf8058c01dc7271b
$ docker rm -f a900ace94102ba4096ddeec5d172ae010d16379a8d0c486dbf8058c01dc7271b
...

既存のイメージを使う場合、docker.image(...).inside(...)を使えばいい感じにDockerを裏で実行してくれる。

Dockerfileからイメージをビルドする場合

Dockerfileからイメージを自分でビルドしたい場合に使うコマンド。

通常はcheckout scmでチェックアウトしたDockerfileを利用するが、とりあえずworkspaceにDockerfileを用意する。

$ pwd
/home/kimura/.jenkins/workspace/sample
$ ls -l
合計 4
-rw-rw-r--. 1 kimura kimura 528  61 14:35 Dockerfile.build
$ cat Dockerfile.build
FROM openjdk:8u131-jdk-alpine

RUN apk update && apk upgrade && \
    apk add curl nodejs-npm=6.10.3-r0 maven=3.3.9-r1 make
RUN curl -Ls "https://github.com/dustinblackman/phantomized/releases/download/2.1.1/dockerized-phantomjs.tar.gz" | tar xz -C /
node {
    docker.build("${BUILD_ID}", "-f Dockerfile.build .").inside() {
        sh 'node -v'  
    }
}

第一引数はイメージ名、第二引数はコマンド引数になる。
Dockerfileからビルドでき、またコマンドが実行されていることがわかる。

...
[sample] Running shell script
+ docker build -t 38 -f Dockerfile.build .
Sending build context to Docker daemon  2.56 kB

Step 1/3 : FROM openjdk:8u131-jdk-alpine
 ---> c7105179e75b
Step 2/3 : RUN apk update && apk upgrade &&     apk add curl nodejs-npm=6.10.3-r0 maven=3.3.9-r1 make
 ---> Using cache
 ---> 0a22744743d5
Step 3/3 : RUN curl -Ls "https://github.com/dustinblackman/phantomized/releases/download/2.1.1/dockerized-phantomjs.tar.gz" | tar xz -C /
 ---> Using cache
 ---> 9b99b208c53e
Successfully built 9b99b208c53e
[Pipeline] dockerFingerprintFrom
[Pipeline] sh
[sample] Running shell script
+ docker inspect -f . 38
.
[Pipeline] withDockerContainer
Jenkins does not seem to be running inside a container
$ docker run -t -d -u 1000:1000 -w /home/kimura/.jenkins/workspace/sample -v /home/kimura/.jenkins/workspace/sample:/home/kimura/.jenkins/workspace/sample:rw -v /home/kimura/.jenkins/workspace/sample@tmp:/home/kimura/.jenkins/workspace/sample@tmp:rw -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** --entrypoint cat 38
[Pipeline] {
[Pipeline] sh
[sample] Running shell script
+ node -v
v6.10.3
[Pipeline] }
$ docker stop --time=1 00cb2f603ca6eb3ea558bb82e906c6075f8593a5f2cb52115ae86ff4c3d82726
$ docker rm -f 00cb2f603ca6eb3ea558bb82e906c6075f8593a5f2cb52115ae86ff4c3d82726
...
Docker Pipeline Pluginの注意点

docker.image(...).inside(...)で実行するコマンドはDockerfileに記述したユーザではなくて、jenkinsの起動ユーザになる。

sampleのpipelineプロジェクトのworkspaceにDockerfileを置いてみる。

$ pwd
/home/kimura/.jenkins/workspace/sample
$ cat Dockerfile.build
FROM openjdk:8u131-jdk-alpine

USER root

Jenkinsのスクリプトに以下を書いて実行する。

node {
    sh 'id'
    docker.build("${BUILD_ID}","-f Dockerfile.build .").inside() {
        sh 'id'
    }
}

コンソール出力を見ると、userがrootではなくて1000になってる。
パーミッションで困ることがあるかも。困る場合は -u で実行ユーザを明示的に指定しちゃえばよさそう。

...
[sample] Running shell script
+ id
uid=1000(kimura) gid=1000(kimura) groups=1000(kimura),10(wheel),987(docker) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
[Pipeline] sh
[sample] Running shell script
+ docker build -t 5 -f Dockerfile.build .
Sending build context to Docker daemon 2.048 kB

Step 1/2 : FROM openjdk:8u131-jdk-alpine
 ---> c7105179e75b
Step 2/2 : USER root
 ---> Using cache
 ---> 4dc2ef524f48
Successfully built 4dc2ef524f48
[Pipeline] dockerFingerprintFrom
[Pipeline] sh
[sample] Running shell script
+ docker inspect -f . 5
.
[Pipeline] withDockerContainer
Jenkins does not seem to be running inside a container
$ docker run -t -d -u 1000:1000 -w /home/kimura/.jenkins/workspace/sample -v /home/kimura/.jenkins/workspace/sample:/home/kimura/.jenkins/workspace/sample:rw -v /home/kimura/.jenkins/workspace/sample@tmp:/home/kimura/.jenkins/workspace/sample@tmp:rw -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** --entrypoint cat 5
[Pipeline] {
[Pipeline] sh
[sample] Running shell script
+ id
uid=1000 gid=1000
...
ホスト環境にライブラリをキャッシュするときの注意点

Jenkinsのworkspace以外のホストディレクトリにキャッシュさせる場合、存在しないディレクトリにマウントするとアクセス権がrootになってしまうので注意。
Add ability to mount volume as user other than root · Issue #2259 · moby/moby · GitHub

以下、動きを確かめる。

dir1は作っておき、dir2をDockerコンテナ起動時の-vに指定する。

$ ls -al
合計 4
drwxrwxr-x.  3 kimura kimura   33  61 19:24 .
drwx------. 30 kimura kimura 4096  61 19:20 ..
drwxrwxr-x.  2 kimura kimura    6  61 19:21 dir1
-rw-rw-r--.  1 kimura kimura    0  61 19:20 hello.txt

起動時に-vに指定して生成されたdir2はrootになっていることがわかる。

]$ docker run -it --rm -u 1000:1000 -v $(pwd)/dir1:$(pwd)/dir1 -v $(pwd)/dir2:$(pwd)/dir2 -w
$(pwd) alpine /bin/sh
/home/kimura/sample $ ls -al
total 0
drwxr-xr-x    4 root     root            28 Jun  1 10:24 .
drwxr-xr-x    3 root     root            19 Jun  1 10:24 ..
drwxrwxr-x    2 1000     1000             6 Jun  1 10:21 dir1
drwxr-xr-x    2 root     root             6 Jun  1 10:24 dir2

なので、ライブラリをキャッシュするディレクトリは事前に作っておくと安全だと思う。
以下のようにするとうまくいった。(本当はユーザごとのディレクトリを指定したほうが安全)

  sh 'mkdir -p /tmp/docker/cache/.node_modules || true'
  sh 'mkdir -p /tmp/docker/cache/.m2 || true'
  docker.build("${BUILD_ID}", "-f Dockerfile.build .").inside("-v /tmp/docker/cache/.m2:/var/maven/.m2 -v /tmp/docker/cache/.node_modules:${WORKSPACE}/client/node_modules") {
  ...
npm使うときの注意点

jenkinsの実行ユーザがDockerコンテナ内でコマンドを実行するため、パーミッションに注意が必要。とくにnpmを使う場合は /.npm にキャッシュディレクトリを作ろうとしてJenkins実行ユーザにパーミッションがなくて失敗することがある。回避策は npm_config_cache変数を環境変数に設定してキャッシュディレクトリの作られる場所を変えること。

npm install fails in jenkins pipeline in docker - Stack Overflow

docker.build(...).inside(...) {
    withEnv(['npm_config_cache=npm-cache']) {
        stage('build') {
            ....
        }
サンプルコード

上記をふまえてJenkinsfile内でDockerコンテナを管理してみた。

github.com

実行した結果はこちら。
ビルドの48からキャッシュさせたところ、ダウンロード時間が大幅に減ってるのがわかる。

f:id:kimulla:20170613002527p:plain

Image.withRun[(args[, command])] {…}

Dockerコンテナを起動したあとに、ホスト側で{...}のコマンドを実行する。

node {
    docker.image('postgres:9.6.3').withRun("-p 5432:5432") {
        sh 'ls -al'
    }
}

argsで指定した引数が起動引数に追加される。
Dockerコンテナの停止は{...}を抜けたあとに勝手に行われる。

...
+ docker run -d -p 5432:5432 postgres:9.6.3
[Pipeline] dockerFingerprintRun
[Pipeline] sh
[sample] Running shell script
+ ls -al
合計 8
drwxrwxr-x.  2 kimura kimura   29  61 14:36 .
drwxrwxr-x. 18 kimura kimura 4096  61 13:20 ..
-rw-rw-r--.  1 kimura kimura  528  61 14:35 Dockerfile.build
[Pipeline] sh
[sample] Running shell script
+ docker stop 4be5a879c0ed353e7e0d5031a78a450d88c7392febcee75e90671b8171b95268
4be5a879c0ed353e7e0d5031a78a450d88c7392febcee75e90671b8171b95268
+ docker rm -f 4be5a879c0ed353e7e0d5031a78a450d88c7392febcee75e90671b8171b95268
4be5a879c0ed353e7e0d5031a78a450d88c7392febcee75e90671b8171b95268
...

テストのために一時的にDBを上げる用途に使うんでしょう、たぶん。

使い方(declarative pipeline)

最近は scripted pipeline よりも declarative pipeline のほうがおすすめらしい。
使い方はリファレンスに載っている。

jenkins.io

既存イメージを利用する場合

scripted pipeline と同じように、とりあえずPipelineプロジェクトで進める。

pipeline {
    agent {
        docker 'openjdk:8u131-jdk'
    }
    stages() {
        stage('build') {
            steps() {
                sh 'java -version'
            }
        }
    }
}

コンソール出力をみると、 scripted pipeline と同じようなことをやってる。
Dockerコンテナ上でshが実行され、ジョブが終わったときにコンテナをstopしてrmしている。

ユーザーanonymousが実行
[Pipeline] node
Running on master in /home/kimura/.jenkins/workspace/yyy
[Pipeline] {
[Pipeline] stage
[Pipeline] { (Declarative: Agent Setup)
[Pipeline] sh
[yyy] Running shell script
+ docker pull openjdk:8u131-jdk
8u131-jdk: Pulling from library/openjdk
Digest: sha256:90c3ac824aa5ce63d8540bb73b6d548b40dc0d536702a48e6a7f21efdc10a861
Status: Image is up to date for openjdk:8u131-jdk
[Pipeline] }
[Pipeline] // stage
[Pipeline] sh
[yyy] Running shell script
+ docker inspect -f . openjdk:8u131-jdk
.
[Pipeline] withDockerContainer
Jenkins does not seem to be running inside a container
$ docker run -t -d -u 1000:1000 -w /home/kimura/.jenkins/workspace/yyy -v /home/kimura/.jenkins/workspace/yyy:/home/kimura/.jenkins/workspace/yyy:rw -v /home/kimura/.jenkins/workspace/yyy@tmp:/home/kimura/.jenkins/workspace/yyy@tmp:rw -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** --entrypoint cat openjdk:8u131-jdk
[Pipeline] {
[Pipeline] stage
[Pipeline] { (build)
[Pipeline] sh
[yyy] Running shell script
+ java -version
openjdk version "1.8.0_131"
OpenJDK Runtime Environment (build 1.8.0_131-8u131-b11-1~bpo8+1-b11)
OpenJDK 64-Bit Server VM (build 25.131-b11, mixed mode)
[Pipeline] }
[Pipeline] // stage
[Pipeline] }
$ docker stop --time=1 3143403b1d45c91f61b64b1d983cbf0562e9fe682aee566f2cdf1f16216b9437
$ docker rm -f 3143403b1d45c91f61b64b1d983cbf0562e9fe682aee566f2cdf1f16216b9437
[Pipeline] // withDockerContainer
...
Dockerfileからイメージをビルドする場合

まずはJenkinsのworkspaceにDockerfileを用意する。

$ pwd
/home/kimura/.jenkins/workspace/yyy
$ cat Dockerfile.build
FROM openjdk:8u131-jdk-alpine

RUN apk update && apk upgrade && \
    apk add curl nodejs-npm=6.10.3-r0 maven=3.3.9-r1 make

以下のようにファイル名を指定する。

pipeline {
    agent {
        dockerfile {
            filename 'Dockerfile.build'
            args '-v /tmp/docker/cache/.m2:/var/maven/.m2'
        }
    }
    stages() {
        stage('build') {
            steps() {
                sh 'java -version'
            }
        }
    }
}

ただしDockerfileという名前でworkspace直下にある場合は指定不要。
その場合は以下のようにする。

    agent {
        dockerfile true
    }
...


コンソール出力をみると、Dockerfile.buildからイメージをビルドしているのがわかる。
また、argsに指定した引数がdocker run時に指定されているのがわかる。

ユーザーanonymousが実行
[Pipeline] node
Running on master in /home/kimura/.jenkins/workspace/yyy
[Pipeline] {
[Pipeline] stage
[Pipeline] { (Declarative: Agent Setup)
[Pipeline] readFile
[Pipeline] sh
[yyy] Running shell script
+ docker build -t 05acb8528ede11dbba8c1b1dc5d2f3ef110d3a0a -f Dockerfile.build .
Sending build context to Docker daemon 2.048 kB

Step 1/2 : FROM openjdk:8u131-jdk-alpine
 ---> c7105179e75b
Step 2/2 : RUN apk update && apk upgrade &&     apk add curl nodejs-npm=6.10.3-r0 maven=3.3.9-r1 make
 ---> Using cache
 ---> 0a22744743d5
Successfully built 0a22744743d5
[Pipeline] dockerFingerprintFrom
[Pipeline] }
[Pipeline] // stage
[Pipeline] sh
[yyy] Running shell script
+ docker inspect -f . 05acb8528ede11dbba8c1b1dc5d2f3ef110d3a0a
.
[Pipeline] withDockerContainer
Jenkins does not seem to be running inside a container
$ docker run -t -d -u 1000:1000 -v /tmp/docker/cache/.m2:/var/maven/.m2 -w /home/kimura/.jenkins/workspace/yyy -v /home/kimura/.jenkins/workspace/yyy:/home/kimura/.jenkins/workspace/yyy:rw -v /home/kimura/.jenkins/workspace/yyy@tmp:/home/kimura/.jenkins/workspace/yyy@tmp:rw -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** --entrypoint cat 05acb8528ede11dbba8c1b1dc5d2f3ef110d3a0a
...
サンプルコード

とりあえずscripted pipeline pluginのコードに変えてみた。

github.com

注意点

agentをDockerにしたときにpostでdeleteDir()すると、エラーになる。

#!/usr/bin/env groovy
pipeline {

    agent {
        dockerfile {
            filename 'Dockerfile.build'
            args '-v /tmp/docker/cache/.m2:/var/maven/.m2 -v /tmp/docker/cache/.node_modules:${WORKSPACE}/client/node_modules'
        }
    }
...
     post {
         always {
             deleteDir()
         }
     }


コンソール出力のエラー内容から予想するに、agentが1つしか指定されていない場合はすべてのステップをコンテナ内で実行するため、コンテナ起動時に割り当てられたworkspaceのディレクトリをdeleteDir()で消そうとしてエラーになってるっぽい。(自信ない)

[Pipeline] }
[Pipeline] // stage
[Pipeline] stage
[Pipeline] { (Declarative: Post Actions)
[Pipeline] deleteDir
[Pipeline] }
[Pipeline] // stage
[Pipeline] }
[Pipeline] // withEnv
[Pipeline] }
$ docker stop --time=1 dddaf3a162e5d72f112fa040cd2af2012dbd0df5d25b794d17e72412787ed750
$ docker rm -f dddaf3a162e5d72f112fa040cd2af2012dbd0df5d25b794d17e72412787ed750
[Pipeline] // withDockerContainer
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
java.nio.file.FileSystemException: /home/kimura/.jenkins/workspace/zzz_plugin-declarative-CRDZ7E2TTQRHZTXPZFVEXD5PM6IOKJAECQWTPPBF52DR5B4W7TKQ/client/node_modules: デバイスもしくはリソースがビジー状態です
	at sun.nio.fs.UnixException.translateToIOException(UnixException.java:91)
	at sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:102)
	at sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:107)
	at sun.nio.fs.UnixFileSystemProvider.implDelete(UnixFileSystemProvider.java:244)

試しにtargetディレクトリをdocker起動時にマウントして消そうとすると同じエラーが出る。

$ pwd
/home/kimura/docker/sample
$ mkdir target
$ ls -al
合計 0
drwxr-xr-x. 3 kimura kimura 19  62 07:27 .
drwxrwxr-x. 5 kimura kimura 57  61 19:36 ..
drwxrwxr-x. 2 kimura kimura  6  62 07:27 target
$ docker run -it -v $(pwd)/target:$(pwd)/target -w $(pwd) centos:7 /bin/bash
# ls
target
# ll
total 0
drwxrwxr-x. 2 1000 1000 6 Jun  1 22:27 target
# rm -rf target
rm: cannot remove 'target': Device or resource busy
#

とはいえ、scripted pipelineと似たような要領で進められそう。
declarative pipelineのほうが事後処理がやりやすいので(always, failureなどなど)、新規に作成する人は declarative pipelineで作成したほうがよさそう。

Docker Pipelin Plugin の良いところ

  • ビルドサーバのメンテナンス性向上
  • ビルドタスクの柔軟性の向上
  • バージョン管理できる
  • ほぼローカルで実行するのと変わらないくらいコンテナ起動停止が早い
  • コンテナに関するあれこれの考慮をあんまり意識せずに使える

Docker Pipelin Plugin の悪いところ

  • Jenkinsfileにビルドやテストに関するあれこれを書きすぎるとローカル環境でDockerを気軽に使えなくなりそう


ということでローカルでもDockerを利用したビルドができるように、Docker Pipeline Pluginを参考にMakefileを作ってみた。

github.com


f:id:kimulla:20170612235217j:plain

これが言いたかった

参考