読者です 読者をやめる 読者になる 読者になる

mike-neckのブログ

JavaかJavaFXかJavaEE(なんかJava8が多め)

ArtifactoryのDockerイメージとgradleでのartifactのデプロイのメモ

少々こまったことがあったので、Artifactoryを調べたので、そのメモ。なお、数年前も同じことを調べたという記憶があるのだが、メモを残していなかったのであらたに調べることになった…


書かれていること

  • ArtifactoryのイメージをビルドするDockerfile
  • プロジェクトの成果物をpublishする場合のビルドファイル
  • Artifactoryにpublishしたjarファイルをプロジェクトで利用する場合のビルドファイル

ArtifactoryのイメージをビルドするDockerfile

作りたいArtifactoryのDockerイメージは次のようなもの

  • なるべく小さいイメージにしたい
  • データベースにデフォルトで有効になっているderbyではなく、mysqlを利用する
  • ポート番号をデフォルトの8081は使いたくないので5000にする
  • データベースのスキーマ/ユーザー/パスワードは環境変数によって設定可能にする

Artifactoryではデータベースの接続設定はファイルから読み込む形になっているが、これを起動時の環境変数により修正するためDockerfileだけではなく(もちろんDockerfileで完結させることはできる)、別途シェル(entry-point.sh)を用意した。

Dockerfile

FROM openjdk:8u111-jre-alpine

ENV ARTIFACTORY_VERSION 5.1.0
ENV ARTIFACTORY_HOME    /artifactory-oss-${ARTIFACTORY_VERSION}

ENV ARTIFACTORY_PORT    5000

ADD entry-point.sh /entry-point.sh

RUN apk --no-cache add curl bash && \
  curl -SL "https://bintray.com/jfrog/artifactory/download_file?file_path=jfrog-artifactory-oss-${ARTIFACTORY_VERSION}.zip" > artifactory.zip && \
  unzip -q artifactory.zip && \
  rm artifactory.zip && \
  curl -SL "https://dev.mysql.com/get/Downloads/Connector-J/mysql-connector-java-5.1.40.zip" > mysql-connector.zip && \
  unzip -q mysql-connector.zip && \
  rm mysql-connector.zip && \
  apk --no-cache del curl && \
  mv mysql-connector-java-5.1.40/mysql-connector-java-5.1.40-bin.jar ${ARTIFACTORY_HOME}/tomcat/lib/ && \
  rm -rf mysql-connector-java-5.1.40/ && \
  mkdir ${ARTIFACTORY_HOME}/data && \
  mv ${ARTIFACTORY_HOME}/tomcat/conf/server.xml ${ARTIFACTORY_HOME}/tomcat/conf/tmp.xml && \
  cat ${ARTIFACTORY_HOME}/tomcat/conf/tmp.xml | \
    sed -e "s/8081/${ARTIFACTORY_PORT}/g" > \
    ${ARTIFACTORY_HOME}/tomcat/conf/server.xml && \
  rm ${ARTIFACTORY_HOME}/tomcat/conf/tmp.xml && \
  chmod +x /entry-point.sh

VOLUME ["${ARTIFACTORY_HOME}/data"]
EXPOSE ${ARTIFACTORY_PORT}

CMD     ["/entry-point.sh"]

entry-point.sh

#!/bin/bash

set -eu

if [ ! -v ARTIFACTORY_HOME ]; then
    echo ARTIFACTORY_HOME is not set
    exit 1
fi

if [ ! -v DB_ADDRESS ]; then
  DB_ADDRESS=db
fi

if [ ! -v DB_PORT ]; then
    DB_PORT=3306
fi

provider_types=("filesystem" "fullDb" "cachedFS")

if [ ! -v PROVIDER_TYPE ]; then
  PROVIDER_TYPE=filesystem
elif [[ ! ${provider_types} =~ (^|[[:space:]])${PROVIDER_TYPE}([[:space:]]|$) ]]; then
  PROVIDER_TYPE=filesystem
fi

if [ ! -v PROVIDER_SIZE ]; then
    PROVIDER_SIZE=1GB
fi

cat << EOF > ${ARTIFACTORY_HOME}/etc/db.properties
type=mysql
driver=com.mysql.jdbc.Driver
url=jdbc:mysql://${DB_ADDRESS}:${DB_PORT}/${DB_NAME}?characterEncoding=UTF-8&elideSetAutoCommits=true
username=${DB_USER}
password=${DB_PASSWORD}
binary.provider.type=${PROVIDER_TYPE}
binary.provider.cache.maxSize=${PROVIDER_SIZE}
EOF

${ARTIFACTORY_HOME}/bin/artifactory.sh

あとは次のような docker-compose.yml を作って起動すればよい

version: '2'
services:
  db:
    image: mysql:5.7
    environment:
      - MYSQL_DATABASE=artifactory
      - MYSQL_USER=user
      - MYSQL_PASSWORD=password
      - MYSQL_RANDOM_ROOT_PASSWORD=yes
    ports:
      - 3306:3306
  artifactory:
    build: ./artifactory
    environment:
      - DB_NAME=artifactory
      - DB_USER=user
      - DB_PASSWORD=password
    volumes:
      - ./artifactory/data:/artifactory-oss-5.1.0/data
    depends_on:
      - db
    ports:
      - 5000:5000

そうすると、ローカルで実行した場合は http://localhost:5000/artifactory にアクセスすると Artifactory を開くことができる。なお、デフォルトのユーザーとパスワードがadmin/password。とりあえず、デフォルトのユーザーは使えなくして、別のユーザーを作成する。


プロジェクトの成果物をpublishする

次のようにレポジトリーにあげたい

  • Artifactoryのkey gradle-dev-local
  • あげるファイル
    • pom
    • 成果物のjar
    • ソースのjar
    • javadocのjar

build.gradle は次のとおり

buildscript {
  repositories {
    jcenter()
  }
  dependencies {
    classpath "org.jfrog.buildinfo:build-info-extractor-gradle:4+"
  }
}

apply plugin: 'java'
apply plugin: 'maven-publish'
apply plugin: "com.jfrog.artifactory"

group = 'com.example'
version = '0.1'

repositories {
  mavenCentral()
}

dependencies {
  compileOnly     'org.jetbrains:annotations:15.0'
  compileOnly     'org.projectlombok:lombok:1.16.14'
  compile         'javax.inject:javax.inject:1'
}

task sourceJar(type: Jar) {
  from sourceSets.main.allSource
  classifier = 'sources'
}

task javadocJar(type: Jar) {
  from tasks.javadoc
  classifier = 'javadoc'
}

artifactory {
  contextUrl = 'http://localhost:5000/artifactory'
  publish {
    repository {
      repoKey = 'gradle-dev-local'
      username = "user"
      password = "password"
      maven = true
    }
    defaults {
      publications('javaProject')
      publishArtifacts = true
      publishPom = true
    }
  }
  resolve {
    repository {
      repoKey = 'gradle-dev'
      username = "user"
      password = "password"
      maven = true
    }
  }
}

publishing {
  publications {
    javaProject(MavenPublication) {
      from components.java
      artifacts = [sourceJar, javadocJar, jar]
    }
  }
}

これによって、artifactoryPublish タスクによって Artifactoryにpublishできるようになる。なお、ポイントとしては次のとおり

  • ArtifactoryにてRepository BrowserのSet Me Upからビルドファイルの雛形を吐き出してくれるが、 maven-publish の記述がないので、そのままでは何もできない。またusername/password用にGStringで何かやってくれているプロパティファイルを吐き出してくれるが、残念ながらプロパティファイルはそのまま文字列になるので何にもならない。
  • maven-publish プラグインにてpublishの準備が整えられたartifactを artifactoryのpublicationsに指定する(上記の javaProject と書いてあるやつ)

なお、こちらのページを参考にした、というかほとんどコピーした…

kinjouj.github.io


Artifactoryにpublishしたjarファイルをプロジェクトで利用する

次のようなビルドファイルを用いる

repositories {
  maven {
    url "http://localhost:5000/artifactory/gradle-dev-local"
  }
}

dependencies {
  compile 'com.example:sample-java-project:0.1'
}

以上

eclipseがメインのIDEに指定されているプロジェクトでIntelliJを使って開発する

職場のプロジェクトがメインで指定するIDEeclipseですが、IntelliJ IDEAが大好きなのでIntelliJ IDEAで仕事しています。

するとどうしても問題が出てきます。

  • importの順番が異なるために無駄なdiffが出てくる
  • コードスタイルが異なるのでレビューしづらい
  • インスペクションのレベルが違うのでレビューしづらい/チェックしづらい

importの順番

importの順番は気合で直します(嘘)

eclipseからコードスタイルフォーマットを出力してIntelliJで取り込んでもImportの順番は出力できないようです。

したがって下のQiitaの記事を参考に Code Style > Java > Impors > Import Layout にてImportの順番をeclipseのそれに合わせます。

qiita.com

しかし、時折ですが、eclipseで書いたコードのimport順がおかしな場合もあります。そのようなイレギュラーケースは気合で直していたりします。

コードスタイル

上記の通りeclipseからコードフォーマット設定をxmlで出力して、取り込むことで対応します。しかしeclipseのデフォルト設定で開発している場合などはxmlの情報が少なくなり、かなり変なスタイルが取り込まれることもあったり、そもそもコードスタイルをxmlで出力していないということもあります(.settings/org.eclipse.jdt.core.prefs で管理している場合など)。

この場合はeclipseのフォーマッタをターミナルから起動して修正します。

$ ~/path/to/Eclipse.app/Contents/MacOS/eclipse -application org.eclipse.jdt.core.JavaCodeFormatter -verbose -config ~/path/to/setting/org.eclipse.jdt.core.prefs ~/path/to/project/src/main/java/com/example/Foo.java

なお、以前はこれをgit statusと組み合わせてgit commitのフックで実行していましたが、IntelliJのchangelistが壊れるのでやめて、次のスクリプトを利用してpushの前に実行するようにしています。

#!/usr/bin/env bash

project=~/path/to/project
eclipse=~/path/to/Eclipse.app/Contents/MacOS/eclipse

cd ${project}

head=`git log --oneline --decorate | grep HEAD | awk '{printf $1}'`
last=`git log --oneline -decorate` | grep origin | awk '{printf $1}'`
java_files=`git diff --name-only ${head} ${last} | awk -v dir=${project} '{print dir"/"$1}' awk -F\| 'system("test -f " $1)==0 { print $1 }'`

${eclipse} -application org.eclipse.jdt.core.JavaCodeFormatter -verbose -config ~/path/to/setting/org.eclipse.jdt.core.prefs ${java_files}

git add ${java_files}
git commit -m 'apply eclipse formatter'

コードインスペクション

これが一番困っています。eclipseIntelliJで警告になるレベルが異なる(IntelliJでは設定可能)ようです。下手に警告がちらつくと重要な警告を発見できなかったり、レビューに集中できなかったりと弊害が大きいです。

いろいろと方法を考えたのですが、とりあえずecjでコンパイルするのが一番よさそうです。

apply plugin: 'java'
configurations {
  eclipse
}
dependencies {
  eclipse 'org.eclipse.jdt.core.compiler:ecj:4.4'
  // 他省略
}

task ecj(type: JavaExec) {
    main = 'org.eclipse.jdt.internal.compiler.batch.Main'
    classpath configurations.eclipse.asPath
    args '-encoding', 'utf8'
    args '-source', '8'
    args '-d', "$buildDir/ecj"
    args '-target', '8'
    args '-cp', configurations.compileClasspath.asPath
    args sourceSets.main.allJava
}

このようなタスクを記述した ecj.gradle ファイルを作って、 ./gradlew -b ecj.gradle ecj を起動することで警告を探してeclipseに合わせたコードを記述します。

出力例

15. WARNING in /path/to/project/src/main/java/org/mikeneck/util/Last.java (at line 143)
        .<Last<T>>map(Candidate::new)
                      ^^^^^^^^^^^^^^
Type safety: The constructor Candidate(Object) belongs to the raw type Candidate. References to generic type Candidate<T> should be parameterized
----------
16. WARNING in /path/to/project/src/main/java/org/mikeneck/util/Last.java (at line 147)
        @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Unsupported @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
----------

例えば、上の出力例ではIntelliJでは Optional をフィールドまたはパラメーターに利用すると警告が出てくるので @SuppressWarnings("OptionalUsedAsFieldOrParameterType") をつけますが、eclipseでは OptionalUsedAsFieldOrParameterType に対応していないので警告になっています(Optional をフィールドまたはパラメーターに使っても警告が出ないのだろうか…詳しい人教えて…)。またeclipseでは推論可能な型パラメーターを省略すると警告が出てしまうようです(Foo<T> のコンストラクタを Foo::new と書くと警告が出るが、 Foo<T>::new と書くと警告が出ない。一方IntelliJでは推論可能な型パラメーターの省略を推奨している)。おそらく言語仕様で曖昧に解釈できる箇所をecjでは安全側に倒しているというところでしょうか…。

以上

sdkmanでJavaのインストール

結構前のことですが、 sdkmanJavaをインストールできるようになったとのことで、Javaの更新をsdkmanでやるようにしてみた。

sdk list java

まず、どのバージョンのJavaが利用できるのか確認してみます。

$ sdk list java

================================================================================
Available Java Versions
================================================================================
     8u111                                                                         
     7u79                                                                          
     6u65                                                                          

================================================================================
+ - local version
* - installed
> - currently in use
================================================================================

sdk install java 7u79

Java7をインストールしてみます。

$ sdk install 7u79

Oracle requires that you agree with the Oracle Binary Code License Agreement
prior to installation. The license agreement can be found at:

  http://www.oracle.com/technetwork/java/javase/terms/license/index.html

Do you agree to the terms of this agreement? (Y/n): 

まず、ライセンスアグリーメントが求められます。ここは y でよいです。

Downloading: java 7u79

In progress...

######################################################################## 100.0%
Binary validation passed...
We will be needing super powers...
Password:

Repackaging java 7u79...
Attaching to the DMG...
Mounting DMG as Volume...
Volume(s) mounted successfully
Installing PKG inside DMG...
installer: Package name is JDK 7 Update 79
installer: Installing at base path /
installer: The install was successful.
Copy JDK Home to temp folder...
Preparing archive...
Unmounting volume...
"/Volumes/JDK 7 Update 79" unmounted successfully.
Done repackaging...
Cleaning up cookie...

Installing: java 7u79
Done installing!

Do you want java 7u79 to be set as default? (Y/n): n

Java7はあまり使うものではないのでデフォルトは n にしておきます。


注意 - Javaのインストール先と JAVA_HOME の場所

sdkJavaをインストールする場合は以下の点に注意が必要です(Macでのみ確認)。

  • Javaのインストール先がいつもどおりの /Library/Java/JavaVirtualMachines に加えて /Users/my-home/.sdkman/candidates/java にもコピーがインストールされます。
  • sdkスクリプトによって JAVA_HOME/Library/Java/JavaVirtualMachines ではなく、 /Users/my-home/.sdkman/candidates/java の方に向きます。
    • したがって、 JAVA_HOME/usr/libexec/java_home を用いて設定/切り替えしていた場合などは JAVA_HOMEPATH が指しているパスが異なるという気持ち悪い状態になるかもしれません。

lombokで生成されたgetterにアノテーションを付与する方法と問題

lombokの既存バグにハマったので、その一時的な回避策のメモ。

なお、この情報は以下の環境でおこなった。

  • Java - 1.8.0_102
  • lombok - 1.16.12

なお、issueはこちら。

github.com


lombokで生成されるgetter(あるいはsetter)にアノテーションを付与する方法

GetteronMethod に特殊な記述法でついてほしいアノテーションを渡す。

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Bool {
  @Getter(onMethod = @__({ @XmlAttribute }))
  private boolean value;
}

問題 - onMethod で渡すアノテーションにプロパティを設定するとコンパイルで落ちる

lombokのonXのドキュメントを読むかぎり、 onMethod に渡すアノテーションにプロパティが設定されていてもうまくコンパイルされるようである。

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Int {
  @Getter(onMethod = @__({ @XmlAttribute(name = "int") }))
  private int value;
}

ところが、これをコンパイルすると次のようなエラーが発生してコンパイルが失敗する。

/path/to/project/src/main/java/foo/bar/Int.java:13: エラー: シンボルを見つけられません
    @Getter(onMethod = @__({ @XmlAttribute(name = "int") }))
                        ^
  シンボル:   クラス __
  場所: クラス Int
/path/to/project/src/main/java/foo/bar/Int.java:13: エラー: 注釈@<any?>に重複y?>'があります。
    @Getter(onMethod = @__({@XmlAttribute(name = "int")}))
                                          ^

回避策 - getterを手で書く

回避策がlombokを入れた意味があまりなくなってしまう方法で非常に残念である。

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Int {
  private int value;
  @XmlAttribute(name = "int")
  public int getValue() {
    return value;
  }
}