tokuhirom's blog

gradle で maven central にあげる

ちょっと前に gradle で maven central にあげようとしたら極めて困難な印象で挫折していたのだが、最近試したらすんなりといったのでご報告申し上げます。

単に gradle が難しいというよりは、maven central 等の toolchain 的な部分に関する知識がだいたいわかったので、すんなりわかったという点も多いんですが。

まず、gradle に maven central にアップロードする方法は、実は公式サイトにまとめられています。 http://central.sonatype.org/pages/gradle.html

基本的にはこの通りに設定すればいいのだが、このドキュメント、gnupg とかについてはすでに設定済みだ、というような前提に基づいているので注意が必要。

アカウントの取得等についてはモリスさんのエントリを読めばよろしい。 http://tagomoris.hatenablog.com/entry/20141028/1414485679

で、GPG 鍵の設定についてはこのへん参考にすればよい。 http://int128.hatenablog.com/entry/20130409/1365434513

で、gradle の設定については、基本的には gradle init と打てばなんとなくマイグレーションが行われる。gradle init しても maven central 関連のデータが出来上がるわけではまったくないことに注意すべき。

必要な設定については http://central.sonatype.org/pages/gradle.html の公式ドキュメントに載っている通りなのだが、いくつか変更してる。

具体的には

  • findbugs プラグインを有効に
  • checkstyle プラグインを有効に
  • travis 環境下では ossrhPassword などの変数がないが死なないように

といったところを変更している。

apply plugin: 'java'
apply plugin: 'maven'
apply plugin: 'signing'
apply plugin: 'findbugs'
apply plugin: 'checkstyle'

group = 'me.geso'
archivesBaseName = "regexp-trie"
version = '0.1.11-SNAPSHOT'

description = """regexp-trie"""

[compileJava, compileTestJava]*.options*.encoding = 'UTF-8'
sourceCompatibility = 1.7
targetCompatibility = 1.7

repositories {
     maven { url "http://repo.maven.apache.org/maven2" }
}

dependencies {
    testCompile group: 'junit', name: 'junit', version:'4.11'
}

findbugsMain {
  ignoreFailures = false
  effort = "max"
}

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

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

artifacts {
    archives javadocJar, sourcesJar
}

signing {
    sign configurations.archives
}

try {
  uploadArchives {
    repositories {
      mavenDeployer {
        beforeDeployment {
          MavenDeployment deployment -> signing.signPom(deployment)
        }

        repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") {
          authentication(userName: ossrhUsername, password: ossrhPassword)
        }

        snapshotRepository(url: "https://oss.sonatype.org/content/repositories/snapshots/") {
          authentication(userName: ossrhUsername, password: ossrhPassword)
        }

        pom.project {
          name 'regexp-trie'
          packaging 'jar'
          // optionally artifactId can be defined here 
          description 'Trie based regexp generator'
          url 'http://github.com/tokuhirom/regexp-trie/'

          scm {
            url 'https://github.com/tokuhirom/regexp-trie/'
            connection 'scm:git:git://github.com/tokuhirom/regexp-trie.git'
            developerConnection 'scm:git:git@github.com:tokuhirom/regexp-trie.git'
          }

          licenses {
            license {
              name 'MIT License'
              url 'http://www.opensource.org/licenses/mit-license.php'
            }
          }

          developers {
            developer {
              id 'tokuhirom'
              name 'Tokuhiro Matsuno'
              email 'tokuhirom@gmail.com'
            }
          }
        }
      }
    }
  }
} catch (MissingPropertyException mpe) {
  if (System.env['CI']) {
    println("Run on CI");
  } else {
    throw mpe;
  }
}

ossrhPassword などの設定は、~/.gradle/gradle.properties に書く。

org.gradle.daemon=true

signing.keyId=MY_KEY_ID
signing.password=GPG_KEY_PASSPHRASE
signing.secretKeyRingFile=/path/to/.gnupg/secring.gpg

ossrhUsername=MY_JIRA_ID
ossrhPassword=MY_JIRA_PASSWORD

checkstyle の設定は config/checkstyle/checkstyle.xml に置く。 このルールは、普段使っているルールからパッケージ名の宣言等を抜いたもの。

<property name="severity" value="error"/>

とすることにより、すべてのルールをエラーレベルにしている。 開発時にコーディングルールを順守しないコードをマージしてもろくなことがないので、エラーレベルにしておくのが良い。

そして、travis ci の設定もカエル。簡単。travis の設定としてはほぼ一緒だが gradlew を実行しているところが違う。 また、teppeis さんのエントリを参考に、sudo を false にしている。 このフラグ変更により、コンテナで実行されるようになり、圧倒的に早く処理が開始されるようになる。 また、~/.gradle/ をキャッシュする設定をいれることにより、gradlew が毎回ダウンロードしなくなるので実行が速くなる。

language: java
sudo: false
jdk:
  - oraclejdk8
install: ./gradlew check
cache:
  directories:
    - $HOME/.gradle/

ちょっと前に調べた時は、maven central にあげるための workaround みたいなものを無限にしろ! って書いてある記事が多くて萎えてやめたのだが、今や普通に使えるようだ。

Created: 2015-04-19 10:30:09
Updated: 2015-04-19 10:30:09

PMD をちょっと試した

Java コードの品質を図るやつであると思うところの PMD をちょっと試してみたが、 検出項目が極めて微妙なんで諦めた。

PMD はネストした if 文をまとめろとか言ってくるが、そこは意味があってやってるんで、 そんなこと言われても困る。。 ッて感じだったり。

テストコードに入ってる "127.0.0.1" っていう IP ハードコードはやめろ、とか。。 いや、いいじゃん。

なんか瑣末なことをネチネチいってきてどうでもいい感じなので、オフ。

Created: 2015-04-17 09:34:16
Updated: 2015-04-17 09:34:16

GS Collections Kata をひと通りやった

GS Collections というゴールドマン・サックスさんが出してる Java 用のコレクションライブラリがある。 Java Day Tokyo/JJUG CCC 2015 Spring で知ったのだが、これは以下のような利点を持っているようだ。

  • 使いやすいインターフェース
  • メモリ使用量が標準のコレクションよりもよい場合がある
  • Multimap や sortedbag などの標準にはないコレクション

GS Collections には GS Collections Kata という、チュートリアルがある。これは junit test case の形式をしていて IDE で読みこめばすぐに試せる。 順番に指示通りにやっていけば試せるんで、なかなか良い。 例も Company, Order, Supplier, LineItem など具体的で、わかりやすい。結構手間かかってるな、という印象。

で、ひと通り試した感想。

GS Collections API は、GS 社内で大量のデータを Java でオンメモリで扱う案件があるっていうバックグラウンドがあった上で実用を目指して作られているのかな、という印象を受けました。社内のライブラリなどがひと通り GS Collections 前提で書いてあると、それ自体がアドバンテージになる感じがします。

既存のコードと混ぜて一般的なウェブアプリケーションで利用するというイメージはわかない。コレクションライブラリをピンポイントで変えるのも結構微妙かな、と。また、メソッドの語彙選択が Smalltalk 由来らしく、Java 8 の Stream API とは違うので、慣れるまでちょっと戸惑いそう。

全般的には、GS Collections 使いやすくて、グループバイ的なことするのとかが Java 8 の Stream API だとまあごっちゃごちゃになるんで、そのへんは GS Collections 使った方が綺麗に書けそう。

Java 8 Stream API でロジックがゴッチャゴチャになったときには、GS Collections の利用を考慮しても良さそう。

大量のデータを取り扱うケース等では、これを利用すればメモリ使用量が節約できてよさそう。いざという時に、「あ、これ GS Collections でやったらメモリ使用量節約できるのでは??」と思い出せる状態にしておくと、よい気がします。

Web Application でも管理画面系等で、集計等を行う必要がある場合には便利かもしれない。ほら、こう、がんばって pivot table もどきを実装したりすることになることってあるじゃないですか。。

そんな感じで、Java エンジニアだったら、ひと通り GS Collections Kata やっておいて損はないんじゃないかな、といった感じでした。

Created: 2015-04-16 10:09:12
Updated: 2015-04-16 10:09:12

assertj 3.0.0 が良い

Java 8 前提になっていて、非常に良い感じになっている。 Java 8 の Date & Time に標準対応した上に、様々なエンハンスメントが入っている。

中でも注目すべきは例外関連の機能追加である。 例外をこのコードが投げているの?みたいなのを簡単に書けるようになっている。 この機能は、今まで自前で実装していたが、自前でやらなくて良くなったので便利だといえる。

@Test
public void testException() {
    assertThatThrownBy(() -> { throw new Exception("boom!") }).isInstanceOf(Exception.class)
                                                            .hasMessageContaining("boom");
}
Created: 2015-04-13 12:46:34
Updated: 2015-04-13 12:46:34

jetty で vhost でいい感じにアプリケーション動かしていく

jetty で vhost 使う方法みたいな話が、意外と情報ない。

例えば、web 画面と admin 画面があるようなサイトで、実際には vhost を利用して運用したいという場合、一般的な ide や servlet container の想定では PATH_INFO でアプリケーションを切り替えて頑張る、みたいなのが多いのだが、こういったアプローチはあまり現代的ではないと思う。

named vhost でやりたいよ!!

最近、以下のような構造のでアプリケーションを構築している。

- pom.xml
+ common ← jar
  - pom.xml
+ admin ← war
  - pom.xml
  - src/main/webapp/WEB-INF/web.xml
+ web   ← war
  - pom.xml
  - src/main/webapp/WEB-INF/web.xml

このような状況で、local で開発したいというシナリオだと思いねえ。

ってわけで、jetty でやるには、vhost を設定したWebAppContext を HandlerCollection に詰め込んでハンドラとして設定すればよろしい。

private Server startServer(int port) throws Exception {
    Server server = new Server(port);
    HandlerCollection handler = new HandlerCollection();
    for (final String module : Arrays.asList("web", "admin")) {
        handler.addHandler(createContext(module));
    }
    server.setHandler(handler);
    server.start();
    return server;
}

それで、WebAppContext は以下のような形で作れば、IntelliJ から起動する分には意外といい感じに動く。mvn exec:java からだとなんかうまくイカなくて困ってるけど。。本当は war を作って explode したほうが正しいが、そのへん工夫してる暇ないのでこういう感じになっている。

private WebAppContext createContext(String module) throws Exception {
    String descriptorPath = appBase + "/" + module + "/src/main/webapp/WEB-INF/web.xml";
    if (!new File(descriptorPath).exists()) {
        log.error("There is no {}", descriptorPath);
        throw new RuntimeException("Invalid descriptor path: " + descriptorPath);
    }

    String resourcePath = appBase + "/" + module + "/src/main/webapp/";
    if (!new File(resourcePath).exists()) {
        log.error("There is no {}", resourcePath);
        throw new RuntimeException("Invalid resourcePath " + resourcePath);
    }

    String hostname = module + "-proj.example.com";

    log.info("Starting {} server: {}, {}, {}", module, descriptorPath, resourcePath, hostname);

    WebAppContext context = new WebAppContext();
    context.setDescriptor(descriptorPath);
    context.setResourceBase(resourcePath);
    context.setContextPath("/");
    context.setParentLoaderPriority(true);
    context.setVirtualHosts(new String[] {hostname});
    return context;
}
Created: 2015-04-10 11:04:18
Updated: 2015-04-10 11:04:18

mvn の exec:java が終わるまでの時間が15秒ぐらい待ってて遅いってとき

daemon thread の終了を待ってるせいで遅いっていう話があるんで雑に殺せば良いっていう気分のときは以下のオプションを与えればよい。

-Dexec.cleanupDaemonThreads=false

ref. http://stackoverflow.com/questions/13471519/running-daemon-with-exec-maven-plugin

Created: 2015-04-09 17:37:45
Updated: 2015-04-09 17:37:45

maven で特定の module だけ処理させたくない。

たとえば maven で multi module でプロジェクトを構築していて、開発用ツールが入っている module があるとする(localdev や devtools などという名前だったりする)。

localdev や devtools などといったものは、nexus enterprise に deploy はしたくないので、処理対象から除外させてやりたい。

maven 3.2.1 以後であれば mvn -pl '!localdev' deploy というように、特定のモジュールを除外してビルドさせることができる。 うちの場合、Jenkins で deploy 処理を行っているので、jenkins の設定に -pl '!localdev' のように記述すればよい。

ref. http://stackoverflow.com/q/8304110/756865

Created: 2015-04-06 16:49:21
Updated: 2015-04-06 16:49:21

jetty がログを出さないよって人

jetty は slf4j が classpath にあると slf4j を使うようになる。 で、slf4j 使ってて logback で設定している場合、logback.xml の中で明示的に org.eclipse.jetty.* を明示的に出力するように設定してないと出ない感じになりがち、と。

Created: 2015-04-06 14:59:16
Updated: 2015-04-06 14:59:16

[java] モダンな APT の開発手順まとめ

APT とは Annotation Processing Tool のことで、Java でコードの自動生成を行う際に利用される。 APT を利用すると、Java クラスやリソースの自動生成が可能となる。

インターネットに情報は結構あるのだが、昔のものが多くて、Eclipse に JAR を追加して云々とかそういう感じのものが多くて辛いので調べたことをまとめておく。

アノテーションを作る

適当なアノテーションを作る。

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.SOURCE)
@Target({
        ElementType.TYPE
})
public @interface Hello {
}

アノテーションプロセッサを実装する

AbstractProcessor を implement する。@SupportedAnnotationTypes で、処理対象のアノテーションを指定。@SupportedSourceVersion で、ソースのバージョンを指定。

ソースの生成には、テキスト処理用の template engine を利用してもよいが、いろいろやるなら javapoet などの Java コード生成用 DSL を利用したほうが、インデントや空行なども綺麗に揃うし、良い。

process() の引数にある RoundEnvironment を扱うための便利メソッドが google auto-common に入っているから、参考にするとよい。

package me.geso.sample.hello;

import java.io.IOException;
import java.io.Writer;
import java.util.Set;

import javax.annotation.Generated;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic.Kind;
import javax.tools.JavaFileObject;

import com.google.auto.common.MoreElements;
import com.squareup.javapoet.AnnotationSpec;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;

@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("me.geso.sample.hello.*")
public class MyProcessor extends AbstractProcessor {
    @Override
    public boolean process(final Set<? extends TypeElement> annotations, final RoundEnvironment roundEnv) {
        if (annotations.isEmpty()) {
            System.out.println("no annotations");
            return true;
        }
        log("HUAAAAAAAAAAAAAAAAAAAAA");

        final Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Hello.class);
        log(elements.toString());
        for (final Element element : elements) {
            final PackageElement aPackage = MoreElements.getPackage(element);
            final TypeSpec blah = TypeSpec.classBuilder("Blah")
                .addAnnotation(AnnotationSpec.builder(Generated.class)
                    .addMember("value", "{$S}", getClass().getCanonicalName())
                    .build())
                .addModifiers(Modifier.PUBLIC)
                .addMethod(MethodSpec.methodBuilder("hello")
                    .addModifiers(Modifier.PUBLIC)
                    .addCode(
                        CodeBlock.builder()
                            .add("return \"hello\";\n")
                            .build()
                    )
                    .returns(TypeName.get(String.class))
                    .build()
                ).build();
            JavaFile javaFile = JavaFile.builder(aPackage.getQualifiedName().toString(), blah)
                .build();

            try {
                JavaFileObject sourceFile = processingEnv.getFiler().createSourceFile(aPackage.toString() + ".Blah");
                Writer writer = sourceFile.openWriter();
                javaFile.writeTo(writer);
                writer.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

        return true;
    }

    private void log(String msg) {
        if (processingEnv.getOptions().containsKey("debug")) {
            processingEnv.getMessager().printMessage(Kind.NOTE, msg);
        }
    }

}

src/main/resources/META-INF/services/javax.annotation.processing.Processor

src/main/resources/META-INF/services/javax.annotation.processing.Processor というファイルに、アノテーションプロセッサのクラス名を記述する。

me.geso.sample.hello.MyProcessor

maven pom.xml の設定

Annotation Processor 自体のコンパイル時に APT 有効にしていると、Annotation Processor 自体のコンパイルに Annotation Processor 使おうとしてわけわかめになるということがあるので、無効にしたほうがいいかもしれない(IntelliJ だけかも)。

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <!-- disable annotation processing -->
                    <compilerArgument>-proc:none</compilerArgument>
                </configuration>
            </plugin>
        </plugins>
    </build>

テストコード書く

検索すると AptinaUnit 使えって書いてあるものが多いのだが、今どきは google compile-testing を使うとよさそう。 APT に限らず、コードコンパイルして云々するもの全般で使える。 開発もアクティブだし。

https://github.com/google/compile-testing

書き方は、javadoc か test code か google auto のコードを参考にする。

コンパイラオプションの指定をしたいところだが、現行バージョンでは指定できない。 実装はされててリリース待なので今後に期待。 https://github.com/google/compile-testing/pull/64

package me.geso.sample.hello;

import static com.google.common.truth.Truth.assert_;
import static com.google.testing.compile.JavaSourceSubjectFactory.javaSource;

import org.junit.Test;

import com.google.common.io.Resources;
import com.google.testing.compile.JavaFileObjects;

public class ProcessorTest {

    @Test
    public void testProcess() throws Exception {
        // Compiler option coming soon.
        // https://github.com/google/compile-testing/pull/64

        assert_().about(javaSource())
            .that(JavaFileObjects.forResource(Resources.getResource("HelloWorld.java")))
            .processedWith(new MyProcessor())
            .compilesWithoutError()
            .and()
            .generatesSources(JavaFileObjects.forSourceString("foo.bar.baz.Blah", "package foo.bar.baz;\n"
                    + "\n"
                    + "import java.lang.String;\n"
                    + "import javax.annotation.Generated;\n"
                    + "\n"
                    + "@Generated({\"me.geso.sample.hello.MyProcessor\"})\n"
                    + "public class Blah {\n"
                    + "  public String hello() {\n"
                    + "    return \"hello\";\n"
                    + "  }\n"
                    + "}"));
    }
}

参考になるコード

google auto

https://github.com/google/auto/

google auto は APT 関連のサンプルとしてよい。

auto commmon は、APT 用のユーティリティクラスとして便利。

APT の実際の使い道

コンパイル時に計算するとか、ボイラープレートコードの生成とかには役立つが、実際、既存のコードを変更できるわけではないので、できることは意外と少ない。

例えば、Dagger2 のようなものにはよい。DI 対象がコンパイル時に確定するので、実行時の速度を抑えることができる。Android 等の始動速度が必要なことが有用なテクニックとなる。

生成されたコードは target/generated-sources/ を見れば確認できるので、実際の動作が想像しやすい。デバッグもしやすくなる。JVM bytecode の知識がなくても修正可能だし、デバッグ等もやりやすい。

lombok のような AST を処理するものよりはよほどやりやすい。

参考資料

http://www.slideshare.net/vvakame/apt-7568357

Created: 2015-03-26 06:59:22
Updated: 2015-03-26 06:59:22

[perl] Error in tempfile() using template /tmp/XXXXXXXXXX: Have exceeded the maximum number of attempts (1000) to open temp file/dir at hoge.pl line 14. のようなエラーが発生した場合の対処方法

File::Temp を使っていると、表題のようなエラーが発生することがある。 これは一件すると、ファイルを開く処理が失敗した用に見えて、ファイル開き過ぎとかかなあと思うんだけど、実際はそういうことではないようだ。

File::Temp の実装はこのへんになっていて、要するに EEXIST が発生した場合に 1000 回ファイル名を変えてリトライして、その結果としてダメだったというときにいい感じにあがる例外である。 https://github.com/Perl-Toolchain-Gang/File-Temp/blob/master/lib/File/Temp.pm#L602

ここを見れば分かる通り、EEXIST の時だけしか次のループにいかないので、OS 的な問題ではない。 https://github.com/Perl-Toolchain-Gang/File-Temp/blob/master/lib/File/Temp.pm#L529

ファイル名のテンプレートは /tmp/XXXXXXXXXX となっており、デフォルトでは X が 10 文字である。10 文字に対して、X は [0-9a-zA-Z_] のうち一文字が使用されるので、63パターンもある。

my @CHARS = (qw/ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
                a b c d e f g h i j k l m n o p q r s t u v w x y z
                0 1 2 3 4 5 6 7 8 9 _
            /);

ということは、984930291881790849 通りのパターンがあるので、通常ならばコンフリクトすることはない。

しかし、このファイル名の生成は Perl5 の rand() 関数を用いている。Perl5 の rand() 関数は fork() した場合に seed の再生成が行われることはないので、 conflict する可能性が考えられる。

以下のような再現コードにより、まさにその現象が起きていることが確認できる。

use strict;
use 5.018000;

use File::Temp qw/tempfile/;

rand(); # set seed

my $pid = fork() // die $!;
if ($pid) {
    # parent
    sleep 1;
    eval {
        tempfile();
    };
    say $@;
    kill $pid;
    waitpid($pid, -1);
} else {
    my @tempfiles;
    for (1..2000) {
        push @tempfiles, tempfile();
    }
    warn "created";
    sleep 2;
    exit 1;
}

以上まで調査した結果、この事象はマニュアルに書いてあることが判明した。

If you are forking many processes in parallel that are all creating
temporary files, you may need to reset the random number seed using
srand(EXPR) in each child else all the children will attempt to walk
through the same set of random file names and may well cause themselves to
give up if they exceed the number of retry attempts.

以上です。よろしくお願いします。

Created: 2015-03-23 11:15:01
Updated: 2015-03-23 11:15:01