tokuhirom's Blog

@RequestMapping の produces を指定するとどうなるか

@RequestMappingproduces = MediaType.APPLICATION_JSON_UTF8_VALUE を指定したときの効果について。指定すべきかどうなのか

結論

produces を指定した場合としない場合で実際どう挙動が変わるのか

以下のようなコントローラを実装する。


    @ResponseBody
    @GetMapping(value = "/json", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    public Map<String, String> json() {
        return ImmutableMap.of("a", "b");
    }

    @ResponseBody
    @GetMapping(value = "/musitei")
    public Map<String, String> musitei() {
        return ImmutableMap.of("c", "d");
    }

produces 指定がある場合、許可されていない Acccept ヘッダでリクエストすると 406 Not Acceptable が返却される。

$ curl -v http://localhost:8080/json
*   Trying ::1...
* Connected to localhost (::1) port 8080 (#0)
> GET /json HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.43.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Date: Tue, 27 Sep 2016 21:47:24 GMT
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< 
* Connection #0 to host localhost left intact
{"a":"b"}

$ curl -v -H 'Accept: application/xml' http://localhost:8080/json
*   Trying ::1...
* Connected to localhost (::1) port 8080 (#0)
> GET /json HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.43.0
> Accept: application/xml
> 
< HTTP/1.1 406 Not Acceptable
< Date: Tue, 27 Sep 2016 21:47:02 GMT
< Content-Type: application/xml;charset=UTF-8
< Transfer-Encoding: chunked
< 
* Connection #0 to host localhost left intact
<Map><timestamp>1475012826311</timestamp><status>406</status><error>Not Acceptable</error><exception>org.springframework.web.HttpMediaTypeNotAcceptableException</exception><message>Not Acceptable</message><path>/json</path></Map>

無指定な場合、Accept: application/xml を指定すると、XML が返却される。

$ curl -v http://localhost:8080/musitei
*   Trying ::1...
* Connected to localhost (::1) port 8080 (#0)
> GET /musitei HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.43.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Date: Tue, 27 Sep 2016 21:48:51 GMT
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< 
* Connection #0 to host localhost left intact
{"c":"d"
$ curl -v -H 'Accept: application/xml' http://localhost:8080/musitei
*   Trying ::1...
* Connected to localhost (::1) port 8080 (#0)
> GET /musitei HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.43.0
> Accept: application/xml
> 
< HTTP/1.1 200 OK
< Date: Tue, 27 Sep 2016 21:48:30 GMT
< Content-Type: application/xml;charset=UTF-8
< Transfer-Encoding: chunked
< 
* Connection #0 to host localhost left intact
<Map><c>d</c></Map>

どのように判別されているのか

以下のあたりで、Accept ヘッダで許容可能と指定されている content-type とサーバーの設定で利用可能な HttpMessageConverter の突き合わせが行われ、利用可能ならシリアライザが使われる。

org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor#writeWithMessageConverters(T, org.springframework.core.MethodParameter, org.springframework.http.server.ServletServerHttpRequest, org.springframework.http.server.ServletServerHttpResponse) とかにデバッグポイントしかけて見てみると、そのへんの挙動がわかる。

		HttpServletRequest request = inputMessage.getServletRequest();
		List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(request);
		List<MediaType> producibleMediaTypes = getProducibleMediaTypes(request, valueType, declaredType);

		if (outputValue != null && producibleMediaTypes.isEmpty()) {
			throw new IllegalArgumentException("No converter found for return value of type: " + valueType);
		}

		Set<MediaType> compatibleMediaTypes = new LinkedHashSet<>();
		for (MediaType requestedType : requestedMediaTypes) {
			for (MediaType producibleType : producibleMediaTypes) {
				if (requestedType.isCompatibleWith(producibleType)) {
					compatibleMediaTypes.add(getMostSpecificMediaType(requestedType, producibleType));
				}
			}
		}
		if (compatibleMediaTypes.isEmpty()) {
			if (outputValue != null) {
				throw new HttpMediaTypeNotAcceptableException(producibleMediaTypes);
			}
			return;
		}

利用可能な HttpMessageConverters はどこで決定されているのか

org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#addDefaultHttpMessageConverters です。

	protected final void addDefaultHttpMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
		StringHttpMessageConverter stringConverter = new StringHttpMessageConverter();
		stringConverter.setWriteAcceptCharset(false);

		messageConverters.add(new ByteArrayHttpMessageConverter());
		messageConverters.add(stringConverter);
		messageConverters.add(new ResourceHttpMessageConverter());
		messageConverters.add(new SourceHttpMessageConverter<Source>());
		messageConverters.add(new AllEncompassingFormHttpMessageConverter());

		if (romePresent) {
			messageConverters.add(new AtomFeedHttpMessageConverter());
			messageConverters.add(new RssChannelHttpMessageConverter());
		}

		if (jackson2XmlPresent) {
			ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.xml().applicationContext(this.applicationContext).build();
			messageConverters.add(new MappingJackson2XmlHttpMessageConverter(objectMapper));
		}
		else if (jaxb2Present) {
			messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
		}

		if (jackson2Present) {
			ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json().applicationContext(this.applicationContext).build();
			messageConverters.add(new MappingJackson2HttpMessageConverter(objectMapper));
		}
		else if (gsonPresent) {
			messageConverters.add(new GsonHttpMessageConverter());
		}
	}

AtomFeedHttpMessageConverter と RssChannelHttpMessageConverter については無視して良い。以下のように、特定のクラスのインスタンスの場合のみ動作するからだ。

public class AtomFeedHttpMessageConverter extends AbstractWireFeedHttpMessageConverter<Feed> {

	public AtomFeedHttpMessageConverter() {
		super(new MediaType("application", "atom+xml"));
	}

	@Override
	protected boolean supports(Class<?> clazz) {
		return Feed.class.isAssignableFrom(clazz);
	}

}

そういうわけで、問題は XML シリアライザが依存に入ってきたケースに挙動が変わるという点のみが問題となる。

produces を指定すべきか

そもそも produces を指定すべきかという点についてだが、atom, xml, rss 等を明示的に返却したい場合については指定したほうが良いかもしれない(別にしなくてもいい気もする)。 String などの raw type を JSON で返したい場合、StringHttpMessageConverter が優先されてしまうため明示的に指定しないといけない。

しかし大枠でいうと、JSON で返す API 全てに produces 属性を指定するとコードがゴチャゴチャするので HttpMessageConverter から除外する、などの対応を取ったほうがいいと思う。

JDK9 で入るという新しい HttpClient を試してみた

jdk9 で HTTP/2 に対応した新しい HttpClient が提供される。これは待望の新機能である。

Java には標準で java.net.httpurlconnection という http client が付属しているが、低機能で、とてもじゃないが実用には耐えない。 そこで Apache HttpClient がデファクトスタンダードっぽくなっているのだが、これまたあまり使いやすくはない。。wirelog が見づらいし。。 OkHttp というクライアントライブラリもあり、これは使いやすいが、なんか apache httpclient 信者の人に導入を阻まれがち。

そんな中で、HTTP/2 に対応した http client が jdk9 ではついてくるということで、ドキュメントを読んだり、jshell で動かしたりして遊んでみた。

[12:43 PM] tokuhirom `HttpClient.create().build().request(URI.create("http://example.com/")).GET().response().body(HttpResponse.asString())` とかで GET できるのでいい感じでした!
[12:43 PM]  mattn 最後の body はなんなの
[12:44 PM]  tokuhirom response body を string として取得
[12:44 PM]   mattn 文字列で body をくれってこと?
[12:44 PM]  tokuhirom うむ。Collectors.toList() とかと同じノリだと思う
[12:44 PM]  mattn class でよかったんじゃ感
[12:45 PM]  tokuhirom うーん
[12:45 PM]                   オブジェクトじゃないと柔軟性が失われるでしょう
[12:45 PM]  mattn まぁこうしとくとあとから実装変えられるんかな
[12:46 PM]  tokuhirom body() の引数は http://download.java.net/java/jdk9/docs/api/java/net/http/HttpResponse.BodyProcessor.html なわけですが
[12:46 PM]  例えばここ、interface だから .body(parseJson(MyEntity.class)) とかできるじゃん
[12:47 PM]  mattn naru
[12:47 PM]            とにかく web client 標準になかったからよい進歩ですな。
[12:47 PM]  tokuhirom ですね
[12:47 PM]                   javadoc 読む時、いま見てるクラスの module のトップに遷移ってのができるようになったので見やすくなった
[12:48 PM]                   cookie manager とかも標準で入ってる
[12:50 PM]                   proxy ももちろん使えるし
[12:50 PM]                    http://download.java.net/java/jdk9/docs/api/javax/net/ssl/SSLParameters.html
[12:50 PM]                    SSL の設定も細かくできるようだ
[12:51 PM]                   timeout の設定が HttpClient に見つからないと思ったけどリクエスト単位かなあ
[12:53 PM]                   timeout の設定見当たらないが、timeout 時の例外はあるっぽいんだよなw
[12:53 PM]                   HttpTimeoutException はある
[12:53 PM]  mattn supplyAsync か
[12:54 PM]            CompletableFuture ってのでいけるっぽ
List<CompletableFuture<File>> futures = targets
          .stream()
          .map(target -> {
              return HttpRequest
                  .create(target)
                  .GET()
                  .responseAsync()
                  .thenCompose(response -> {
                      Path dest = Paths.get("base", target.getPath());
                      if (response.statusCode() == 200) {
                          return response.bodyAsync(asFile(dest));
                      } else {
                          return CompletableFuture.completedFuture(dest);
                      }
                  })
                  // convert Path -> File
                  .thenApply((Path dest) -> {
                      return dest.toFile();
                  });
              })
          .collect(Collectors.toList());
[12:54 PM]  もだん
[12:55 PM]  tokuhirom そうすねえ
[12:55 PM]                    async が標準で入ってますね。最近のは
[12:56 PM]                    `HttpRequest.create(uri)..timeout(TimeUnit.SECONDS, timeout)` とかでタイムアウト指定できるのか
[12:56 PM]                    使いやすいね
[12:56 PM]                    (結局ソースみたw
[12:56 PM]                    http://download.java.net/java/jdk9/docs/api/java/net/http/HttpRequest.Builder.html#timeout-java.util.concurrent.TimeUnit-long-
[12:56 PM]                    ここにあったわ
[12:57 PM]                    良いね
[12:57 PM]                    インターセプター的なのは無いっぽい
[1:01 PM]                      例えば HttpClient.Builder にデフォルトのヘッダ値を埋める、みたいな
[1:01 PM]  mattn a-
[1:01 PM]  tokuhirom 便利だけど本質的じゃない機能は入ってない。java らしくて正しいと思うけど。

個人的な予測としては Apache HttpClient はオワコンとなり OkHttp はどうかしらんけど、Retrofit 3 あたりで JDK9 ベースになる。みたいな未来が見えます。 そんな感じです。

なおログは https://soozy.arukascloud.io/ の #java より。どなたでもお気軽にご参加いただけます。

mockito の when で、ちまちまと実際に渡す値を設定すべきではない

メソッドがちゃんと呼ばれたかどうかのチェックには verify を利用すべきです。 when の引数は基本的には any を指定し、複数の引数パターンで挙動を変えたい時のみ、any 以外を指定すべきです。

例えば以下のようなシンプルなケースでは、以下のように書くべきではありません。

// 引数を明示的に指定しないほうが良い。
when(foo.bar("hoge")).thenReturn(3);

asserThat(foo.bar("hoge")).isEqualTo(3);

以下のように any を指定したほうが良いでしょう。

// anyString() を利用すると良い
when(foo.bar(anyString())).thenReturn(3);

asserThat(foo.bar("hoge")).isEqualTo(3);

このように習慣づけていないと、*「when() で設定した値が返ってこないぞ!なんでだ??」*と悩む時間が増えがちです。 これ本当に時間の無駄です。

同一のメソッドが複数回呼ばれる場合で、それぞれ挙動を変えたい場合のみ、引数を明示しましょう。

(IntelliJ のプラグインで、引数全部 any() とかで満たしてくれる補完タイプとかありゃいいのだが見つからない)

mockito でスタブし忘れて NullPointerException 発生するのが辛い時の解決策

mockito では mocking していない場合、org.mockito.internal.stubbing.defaultanswers.ReturnsEmptyValues が返ります。 JDK で提供されているコンテナ型の場合には空のコンテナ型が返りますのでいいのですが、通常のユーザーが作成したクラスの場合、null が返ります。 これにより NullPointerException が発生してつらいです。

SmartNullPointerException が発生した場合、以下のようなエラーメッセージが表示され、非常に問題が解決しやすくなります。

org.mockito.exceptions.verification.SmartNullPointerException: 
You have a NullPointerException here:
-> at com.example.mockito.MockitoReturnsSmartNullsTest.test(MockitoReturnsSmartNullsTest.java:20)
because this method call was *not* stubbed correctly:
-> at com.example.mockito.MockitoReturnsSmartNullsTest.test(MockitoReturnsSmartNullsTest.java:20)
foo.boz();

	at com.example.mockito.MockitoReturnsSmartNullsTest.test(MockitoReturnsSmartNullsTest.java:20)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:363)

Mockito 2.0 以後ではデフォルトが ReturnsEmptyValues から ReturnsSmartNulls に変更されており、NullPointerException が発生するようになった原因を教えてくれるようになります。 Mockito 2.0 でそうなるのであれば先取りしたいところですね。

when で指定されていないメソッドのデフォルトの挙動をグローバルに変更するには org.mockito.configuration.MockitoConfiguration クラスを実装すればよいです。 この名前は org.mockito.internal.configuration.ClassPathLoader にハードコードされてますので、変更は不可能です。 以下のクラスを設置するだけで、利用できるので簡単です。

package org.mockito.configuration;

import org.mockito.stubbing.Answer;

// https://solidsoft.wordpress.com/2012/07/02/beyond-the-mockito-refcard-part-1-a-better-error-message-on-npe-with-globally-configured-smartnull/
// See org.mockito.internal.configuration.ClassPathLoader
public class MockitoConfiguration extends DefaultMockitoConfiguration {
    public Answer<Object> getDefaultAnswer() {
        return new ReturnsSmartNulls();
    }
}

この設定はライフチェンジングなのでマジでお勧めです。

armeria で grpc するサンプルコードをかいた

https://github.com/tokuhirom/armeria-sample line が出している rpc フレームワークであるところの armeria が最新版で grpc をサポートしたとのことで、実際どんなもんか試してみた。

試すのは簡単。build.gradle に protobuf プラグインを追加。

buildscript {
    repositories {
        mavenCentral()
        maven { url "https://plugins.gradle.org/m2/" }
    }
    dependencies {
        classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.0'
    }
}

apply plugin: 'com.google.protobuf'

protobuf {
    protoc {
        // The version of protoc must match protobuf-java. If you don't depend on
        // protobuf-java directly, you will be transitively depending on the
        // protobuf-java version that grpc depends on.
        artifact = "com.google.protobuf:protoc:3.0.0"
    }
    plugins {
        grpc {
            artifact = 'io.grpc:protoc-gen-grpc-java:1.0.0'
        }
    }
    generateProtoTasks {
        all()*.plugins {
            grpc {}
        }
    }
}

idea {
    module {
        sourceDirs += file("${protobuf.generatedFilesBaseDir}/main/java");
        // If you have additional sourceSets and/or codegen plugins, add all of them
        sourceDirs += file("${protobuf.generatedFilesBaseDir}/main/grpc");
    }
}

dependencies {
    ["grpc-core", "grpc-stub", "grpc-protobuf"].each { module ->
        compile "io.grpc:$module:1.0.0"
    }
}

src/main/proto/hi.proto とかに以下のように protobuf の定義を置く。

syntax = "proto3";

package hi;

option java_multiple_files = true;
option java_package = "com.example.grpc";
option java_outer_classname = "HiProto";
option objc_class_prefix = "HI";

message MyStringValue {
    string value = 1;
}

service Hi {
    rpc Hello (MyStringValue) returns (MyStringValue) {
    }
}

で、./gradlew generateprotoで java コードが生成されるのでそれを継承したクラスを実装する。src/main/java/com/example/grpc/HiService.java 使い勝手は thrift と大差ない。

package com.example.grpc;

import io.grpc.stub.StreamObserver;

public class HiService extends HiGrpc.HiImplBase {
    @Override
    public void hello(MyStringValue request, StreamObserver<MyStringValue> responseObserver) {
        responseObserver.onNext(MyStringValue.newBuilder()
                .setValue("Hi, " + request.getValue() + "!")
                .build());
        responseObserver.onCompleted();
    }
}
```

最後に armeria サーバーの起動部分だけ実装すれば完成。簡単。

```
public class MyApp {
    public static void main(String[] args) {
        ServerBuilder sb = new ServerBuilder();
        sb.port(8080, SessionProtocol.HTTP);
        GrpcService grpcService = new GrpcServiceBuilder()
                .addService(new HiService())
                .build();
        sb.serviceUnder(
                "/",
                grpcService.decorate(LoggingService::new));
        Server server = sb.build();
        server.start();
    }
}
```

# gRPC 採用した場合のメリット

もともと armeria は thrift 用のサーバーなわけだけど、gRPC を採用した場合には以下のような恩恵を得られます。

 * Full support for request/response streaming
 * Protocol buffer generated code can be a nice change
 * Efficient clients on a variety of platforms (Android, iOS, Go, etc)

一方で、grpc 使うと DocService 使えないとか JSON で利用できないとかそういうデメリットも現状あるようだ

# armeria の上で gRPC 動かした場合のメリット

一方、gRPC 使ってるユーザーが armeria の上で gRPC を動かすメリットとしては以下のようなことがあるとのこと

 * Support for HTTP1 (not verified, will probably require some followup work). HTTP1 should open GRPC to the browser and work better with Cloud load balancers that generally translate HTTP2 -> HTTP1
 * Once implemented, DocService and Grpc on the same server
 * And any other servers they feel like having on the same server since armeria's flexible that way :)

とはいえ、現状だと grpc のクライアントって直接 HTTP で話せるわけでもないし HTTP1 サポートしたいというモチベーションも薄いかなあ。protobuf の方がネックになるし。あと DocService も grpc に対応してない様子。

DocService に対応して、さらに JSON エンドポイントが自動で生えるぐらいまでなったらぜひ使いたい。 https://github.com/grpc-ecosystem/grpc-gateway 的な感じで、` GrpcJsonServiceBuilder.service(myService).build()` とかで JSON  API 提供できる、とかだと嬉しい。

# まとめ
今後に期待。

コンテナ型の内部にラッパー型が入っている時にmockitoのverifyメソッドのエラー表示がわかりにくいのをなおした

たとえば以下のケース。Map の中に long が入っていることを検証しているのですが、実際にはいっているのは Integer。

@Test
public void foo() {
    Foo m = mock(Foo.class);
    m.foo(new HashMap<String, Object>(){{
        put("hoge", 4);
    }});
    verify(m).foo(new HashMap<String, Object>(){{
        put("hoge", 4L);
    }});
}

public static class Foo {
    void foo(Map<String, Object> map) {
    }
}

この場合、以下のような表示となり、「違わないやんけ!!」となり血管が破裂しそうになります(なりません)。

Argument(s) are different! Wanted:
foo.foo(() {hoge=4});
-> at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
Actual invocation has different arguments:
foo.foo(() {hoge=4});
-> at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

これが以下のように L suffix がついていれば問題点が歴然となり、血管に優しいのではないか、と考えました。

Argument(s) are different! Wanted:
foo.foo({"hoge"=4L});
-> at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
Actual invocation has different arguments:
foo.foo({"hoge"=4});
-> at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

https://github.com/mockito/mockito/pull/571#issuecomment-241416329 そういうわけで、p-r を送ったところ、取り込まれましたので mockito 2.0 では問題点が修正されることかと思います(いつでるんや〜)

hs_err_pidの生成タイミング

hs_err_pid は、jvm がクラッシュしたタイミングで生成されるが、実際どのようなタイミングで生成されるのかという話。

http://hg.openjdk.java.net/jdk9/jdk9/hotspot/file/tip/src/share/vm/utilities/vmError.cpp#l285 の report_and_die で生成されているとのこと。

VMError::reset_signal_handlers で crash_handler が以下の signals に対して設定される。see VMError::reset_signal_handlers

static const int SIGNALS[] = { SIGSEGV, SIGBUS, SIGILL, SIGFPE, SIGTRAP }; // add more if needed
static const int NUM_SIGNALS = sizeof(SIGNALS) / sizeof(int);

jailing を golang に移植した

なぜ golang に移植したかというとまあ以下のような理由です。

https://github.com/tokuhirom/jailingo/

golang で記述することにより、良い面もあるが一方で、普通に golang で記述すると、clone() したあとに処理を挟むことができないので、CLONE_NEWPID | CLONE_NEWNS とかしたあとに mount したいという要望が叶えられない。ので、いったん /proc/self/exec を実行しなおして、procfs をマウントしてから子プロセスを起動する、みたいな処理が必要になってしまう。ちょっと面倒。

と、一応書いてみたが、実際には proot を利用しているのだった。


↑↑ 結局、proot 遅すぎたので jailingo 使うようにした。

OSS版 Ansible Tower alternative である Ansible Semaphore を試したぞ!

Ansible Tower の OSS Alternative であるところの Ansible Semaphore を試した。 Ansible を利用してデプロイするのの web ui である。

installation guide 通りにやれば利用可能。

docker run -d --name=mysql -p 127.0.0.1:3306:3306 -e MYSQL_ROOT_PASSWORD=my-secret-pw mysql

などとして mysqld を立ち上げておく(もちろん、別に docker じゃなくてもいい)。

https://github.com/ansible-semaphore/semaphore/releases から最新版のバイナリを取得(golang なので single binary)。

semaphore -setup

として、起動。セットアップが始まる。セットアップは以下の様な感じ。

$ ~/Downloads/semaphore_darwin_amd64 -setup

 Hello! You will now be guided through a setup to:

 1. Set up configuration for a MySQL/MariaDB database
 2. Set up a path for your playbooks (auto-created)
 3. Run database Migrations
 4. Set up initial seamphore user & password

 > DB Hostname (default 127.0.0.1:3306):
 > DB User (default root):
 > DB Password: my-secret-pw
 > DB Name (default semaphore):
 > Playbook path:

 Generated configuration:
 {
        "mysql": {
                "host": "127.0.0.1:3306",
                "user": "root",
                "pass": "my-secret-pw",
                "name": "semaphore"
        },
        "port": "",
        "bugsnag_key": "",
        "tmp_path": "/tmp/semaphore",
        "cookie_hash": "3aIKJIwVCvXH6vLGb8xhQHMr90OeVZ1SimYMNJBWD+A=",
        "cookie_encryption": "TDFElfq2JZN5PfTfPedRjgMZ+MZUY9VgNTZCS/sGJTY="
 }

 > Is this correct? (yes/no): yes
 Running: mkdir -p /tmp/semaphore..
 Configuration written to /tmp/semaphore/semaphore_config.json..
 Pinging database..

 Running DB Migrations..
Creating migrations table
Executing migration v0.0.0 (at 2016-06-25 08:44:13.784368138 +0900 JST)...
 [11/11]
Executing migration v1.0.0 (at 2016-06-25 08:44:14.340635482 +0900 JST)...
 [7/7]
Executing migration v1.1.0 (at 2016-06-25 08:44:14.752282549 +0900 JST)...
 [1/1]
Executing migration v1.2.0 (at 2016-06-25 08:44:14.861796122 +0900 JST)...
 [1/1]
Executing migration v1.3.0 (at 2016-06-25 08:44:14.918184638 +0900 JST)...
 [3/3]
Executing migration v1.4.0 (at 2016-06-25 08:44:15.103055625 +0900 JST)...
 [2/2]
Executing migration v1.5.0 (at 2016-06-25 08:44:15.220915164 +0900 JST)...
 [1/1]
Executing migration v0.1.0 (at 2016-06-25 08:44:15.279912624 +0900 JST)...
 [6/6]

 > Username: foobar
 > Email: [email protected]
 > Your name: Foo Bar
 > Password: *********

 You are all setup Foo Bar!
 Re-launch this program pointing to the configuration file

./semaphore -config /tmp/semaphore/semaphore_config.json

 To run as daemon:

nohup ./semaphore -config /tmp/semaphore/semaphore_config.json &

 You can login with [email protected] or foobar.

設定ファイルが生成されるんで、メッセージどおりにそれを使って起動すれば OK.

ここまでは極めて簡単にできる。

現時点の最新版である v2.0.2 には致命的な問題がある(全くデプロイできない)ので、困ったなーって思って git log 見たら HEAD では治ってたので、リリースしてよ、って言ったら音速でリリースされた。

が、v2.0.2 でエンバグしていたので、パッチを送ってみたが、そうじゃないって言われたので様子を見ている。たぶんためそうと思ってる人はv2.0.3まで待つのが良い。 https://github.com/ansible-semaphore/semaphore/issues/142

開発

patch 送ったりするにはどーすりゃいいか。golang の開発環境はすでにあるとして、、

circle.yml の以下の部分を参考に、手でインストールする。。 https://github.com/ansible-semaphore/semaphore/blob/master/circle.yml#L7-L17

そしたらば、./make.sh watch とすると、ファイル変更すると自動で更新がかかる感じで、プロセスが起動する。

ってだけ。

利用手順

まず、Dashboard を見て、プロジェクトを追加します。 https://gyazo.com/b696178e7abea2ed4297ba65197f4698

次に、Key Store に ssh key を登録します。ここに登録した ssh key は、ログインと git の clone の両方に利用されます。 https://gyazo.com/8eaefe8f15641469d535517226c81d5b

playbook 登録してるレポジトリを登録しまうす。git clone されるだけなので、file:// でもなんでもOK. https://gyazo.com/e2e5d675faa41fbaffd2b0b5636075ca

インベントリを登録します。これは ansible コマンドでいうところの ansible-playbook -i www playbook.yml の www にあたる部分ですね。 https://gyazo.com/adb05fbf1c02e19057635dd3ed3110a4

Environment を登録します。ansible 内で variable として利用可能になる。のかな?よくわかってない。 https://gyazo.com/27cc47602aed92ce743cb438ab936649

最後にこれらの要素を利用して、タスクを登録。 https://gyazo.com/7d49c11134e177bbb8f98f0471dfb97c

run ボタン押すと、タスク実行画面が起動する。ここで、変数をいじったりとかできるので便利。 https://gyazo.com/6c3ab1ea2f72261161533e67258e208f

実行すると、普通に動く。 https://gyazo.com/7046532dee6aab89dab50290a9759b97

まとめ

v2 が出たばかりで v2 は荒削りだが、アーキテクチャは綺麗なので良さそう。 go+ansible みたいな構成になってていじりやすい。コードは綺麗。インストールも楽。 今後に期待がモテる。

Promgen talk - Prometheus casual talks

I promoted promgen, a prometheus configuration management console web app at Prometheus Casual Talks.

promgen - prometheus managemnet tool / simpleclient_java hacks @ Prometheus casual from Tokuhiro Matsuno

I'll be release it as OSS application.

How do I display custom element with IncrementalDOM?

Incremental DOM is great library to build dynamic DOM tree.

If you want to build DOM tree contains custom tags, you need to call IncrementalDOM.skip() to skip patching DOM elements inside custom element.

<!doctype html>
<html>
  <head>
    <script type="text/javascript" src="node_modules/incremental-dom/dist/incremental-dom.js"></script>
  </head>
<body>

<div id="container"></div>

<script>
document.registerElement('x-bar', (function () {
  var proto = Object.create(HTMLElement.prototype);
  proto.createdCallback = function () {
    IncrementalDOM.patch(this, function () {
      IncrementalDOM.elementOpen('div');
      IncrementalDOM.text('hello');
      IncrementalDOM.elementClose('div');
    });
  };
  return {prototype:proto};
})());

const container = document.getElementById('container');
for (let i=0; i<3; ++i) {
  IncrementalDOM.patch(container, function () {
    IncrementalDOM.elementOpen('x-bar');
    IncrementalDOM.skip();
    IncrementalDOM.elementClose('x-bar');
  });
}

</script>

</body>
</html>

Java mixed-mode flame graphsについてのメモ

http://www.brendangregg.com/blog/2015-11-06/java-mixed-mode-flame-graphs.html

Java mixed-mode flame graphs ってエントリがどんな問題を解決しているか

といった問題を解決しているのだが、解決している問題が多岐に渡っていて理解しにくい。

必要な環境

とりあえず試す

sudo yum install -y perf

で perf を install

sudo yum install -y java-1.8.0-openjdk-devel

openjdk なら -devel パッケージもインストールしないと perf-map-agent をビルドできない(oraclejdk でどうなのかは未検証)。

perf のメトリクス取るのの基本

sudo perf record -F 99 -ag -- sleep 30 で、99Hz で 30 sec のプロファイリング。

sudo perf report -f でレポートを curses で見れる

perf-map-agent を使う

https://github.com/jrudolph/perf-map-agent

[tokuhirom@develop perf-map-agent]$ git clone https://github.com/jrudolph/perf-map-agent.git                                                                                                        
Cloning into 'perf-map-agent'...
remote: Counting objects: 336, done.
remote: Total 336 (delta 0), reused 0 (delta 0), pack-reused 336
Receiving objects: 100% (336/336), 95.16 KiB | 0 bytes/s, done.
Resolving deltas: 100% (165/165), done.
[tokuhirom@develop perf-map-agent]$ cmake .
-- Found JNI: /usr/lib/jvm/jre/lib/amd64/libjawt.so  
-- JNI_INCLUDE_DIRS=/usr/lib/jvm/java/include;/usr/lib/jvm/java/include/linux;/usr/lib/jvm/java/include
-- JNI_LIBRARIES=/usr/lib/jvm/jre/lib/amd64/libjawt.so;/usr/lib/jvm/jre/lib/amd64/server/libjvm.so
-- JAVA_INCLUDE_PATH=/usr/lib/jvm/java/include
-- JAVA_INCLUDE_PATH2=/usr/lib/jvm/java/include/linux
-- Found Java: /bin/java (found version "1.8.0.91") 
-- Configuring done
-- Generating done
-- Build files have been written to: /home/tokuhirom/perf-map-agent

perf-map-agent というコマンドラインツールを用意する。これが特定の PID の java process に attach して、JVM TI (JVM Tool Inteface) で情報を集めて perf 用の形式で出力してくれる。このツールを利用するには -XX:+PreserveFramePointer をターゲットの jvm の起動時につける必要がある(このオプションは java 8u60+ で利用可能)。

create-java-perf-map.sh <options*>

./bin/perf-java-top

localdev-provisioning%20%E2%80%94%20tokuhirom@develop:~%20%E2%80%94%20ssh%20%E2%97%82%20vagrant%20ssh%20and%20blog%20admin

net.virtualvoid.perf.AttachOnce 実行して map を取得したあと、perf top している。↑↑のスクショを見ると、java method の情報も取得できていることがわかる。

まとめ

spring-boot のメトリクスデータを graphite に送る

graphite とはまあザックリ言うと growthforecast みたいなものだが、python で書かれていて、ダッシュボード的な機能が充実してるって感じのものです。

画面の様子は以下の様な感じ。

Graph Tree:

Dashboard:

以下の様な感じで、nc で直接書き込める。http based な gf とはちょっと違う。

PORT=2003
SERVER=graphite.your.org
echo "local.random.diceroll 4 `date +%s`" | nc -q0 ${SERVER} ${PORT}

で、こういうやつにメトリクスデータを送るにはどうしたらいいか。spring-boot 自体はメトリクスの出力先として statsd, redis, opentsdb しかサポートしていない。 statsd から graphite に書き込ませることもできるのだが、今回は直接 graphite に書かせてみた。statsd 経由でやる場合は、各ホストに statsd を動かすってことになるのかなあ。 あんまり statsd 使ってる話を聞かないのでよくわからない。そして、graphite に送るだけなら、直接 graphite に送ればいい気もする。


さて、spring-boot で graphite を送るには、例によって dropwizard-metrics を利用すれば良い。

依存に以下を足す。

compile('io.dropwizard.metrics:metrics-core:3.1.0')
compile('io.dropwizard.metrics:metrics-graphite:3.1.2')

で、実際に動くコードは以下。依存にいれておけば、org.springframework.boot.actuate.autoconfigure.MetricsDropwizardAutoConfiguration が動いて、 com.codahale.metrics.MetricRegistry を Autowired できるようになるんで、これを利用すれば良い。

package com.example.config;

import com.codahale.metrics.MetricFilter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.graphite.Graphite;
import com.codahale.metrics.graphite.GraphiteReporter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.autoconfigure.MetricsDropwizardAutoConfiguration;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;
import java.net.InetSocketAddress;
import java.util.concurrent.TimeUnit;

@Configuration
@Slf4j
public class GraphiteConfig {
    /**
     * {@link MetricsDropwizardAutoConfiguration} configures Dropwizard's MetricRegistry
     * if it's available in classpath.
     */
    @Autowired
    private MetricRegistry registry;

    @PostConstruct
    public void initialize() {
        final Graphite graphite = new Graphite(new InetSocketAddress("localhost", 2003));
        final GraphiteReporter reporter = GraphiteReporter.forRegistry(registry)
                .prefixedWith("web1.example.com")
                .convertRatesTo(TimeUnit.SECONDS)
                .convertDurationsTo(TimeUnit.MILLISECONDS)
                .filter(MetricFilter.ALL)
                .build(graphite);
        reporter.start(1, TimeUnit.MINUTES);

    }
}

簡単ですね。 full sample code is here: https://github.com/tokuhirom/java-samples/tree/master/spring-boot-graphite-demo

aspectj の post compile weaving を gradle で行う方法

spring boot の起動が極めて遅くて辛いなと感じていたところ、どうやら spring aop が極めて多くの時間を浪費しているということが判明した。 spring aop をオフにすると起動時間が16秒なのに対し、オンにすると 26 秒に増える。しかも、利用しているエンドポイントの数に比例しているようで、これは今後さらに遅くなりそうだ。。

調べてみると spring aop は load time weaving を採用していることがわかった。 ref. http://docs.spring.io/spring/docs/current/spring-framework-reference/html/aop.html

AOP には3種類のやり方がある。

ref. https://eclipse.org/aspectj/doc/next/devguide/ltw.html

このうち compile-time weaving は、lombok を利用できないために却下。

そういうわけで、Post compile weaving を試してみた。 しかし、gradle で post compile weaving を利用する方法は、なかなか見つからない。 見つからないのでゴリゴリと書いた。

gradle では、プロジェクト内にプラグインを置くことができるので、aspectj プラグインを書く。 ディレクトリ構成は以下のようにする。

buildSrc
├── build.gradle
└──c
    └── main
        ├── groovy
        │   └── aspectj
        │       └── AspectJPlugin.groovy
        └── resour
            └── METAF
                └── gradle-plugins
                    └── aspectj.properties

resources/META-INF/gradle-plugins/aspectj.properties に、implementation-class=aspectj.AspectJPlugin と書くことで、aspectj plugin の実装がどこにあるのかを指定する。

src/main/groovy/aspetctj/AspectJPlugin.groovy は、class AspectJPlugin implements org.gradle.api.Plugin<org.gradle.api.Project> { } のように、プラグインクラスを継承した、 クラスを配置する。このPluginクラスの void apply(Project project) メソッドが、apply plugin: 'aspectj' された時に呼ばれるので、ここにコードを記載していく。

spring security に入っていた aspectj plugin をベースに書いたが、もはや原型はとどめていないコードがこちらになります。

package aspectj

import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.plugins.JavaPlugin
import org.gradle.api.tasks.TaskAction

import java.nio.file.Path

// taken from spring security.
// https://github.com/spring-projects/spring-security/blob/master/buildSrc/src/main/groovy/aspectj/AspectJPlugin.groovy
/**
 *
 * @author Luke Taylor
 */
class AspectJPlugin implements Plugin<Project> {

    void apply(Project project) {
        project.plugins.apply(JavaPlugin)

        if (!project.hasProperty('aspectjVersion')) {
            throw new GradleException("You must set the property 'aspectjVersion' before applying the aspectj plugin")
        }

        if (project.configurations.findByName('ajtools') == null) {
            project.configurations.create('ajtools')
            project.dependencies {
                ajtools "org.aspectj:aspectjtools:${project.aspectjVersion}"
                compile "org.aspectj:aspectjrt:${project.aspectjVersion}"
            }
        }

        project.afterEvaluate {

            project.tasks.create(name: 'compileAspect', overwrite: true, description: 'Compiles AspectJ Source', type: Ajc) {
                dependsOn project.processResources, project.compileJava

                tmpDir = "${project.buildDir}/aspect/"
                args = [
                        "-inpath", project.sourceSets.main.output.classesDir.toPath(),
                        "-showWeaveInfo",
                        "-1.8",
                        "-d", tmpDir,
                        "-classpath", project.sourceSets.main.compileClasspath.asPath,
                ];
                dstDir = project.sourceSets.main.output.classesDir.toPath()
            }
            project.tasks.classes.dependsOn project.tasks.compileAspect

            project.tasks.create(name: 'compileTestAspect', overwrite: true, description: 'Compiles AspectJ Test Source', type: Ajc) {
                dependsOn project.processTestResources, project.compileTestJava

                tmpDir = "${project.buildDir}/test-aspect/"
                def classpath = project.sourceSets.test.compileClasspath.files.grep({ it.exists() }).join(":")
                args = [
                        "-inpath", project.sourceSets.test.output.classesDir.toPath(),
                        "-aspectpath", project.sourceSets.main.output.classesDir.toPath(),
                        "-aspectpath", project.sourceSets.test.output.classesDir.toPath(),
                        "-showWeaveInfo",
                        "-1.8",
                        "-d", tmpDir,
                        "-classpath", classpath
                ];
                dstDir = project.sourceSets.test.output.classesDir.toPath()
            }
            project.tasks.testClasses.dependsOn project.tasks.compileTestAspect
        }
    }
}

class Ajc extends DefaultTask {
    String[] args
    String tmpDir
    Path dstDir

    Ajc() {
        logging.captureStandardOutput(LogLevel.INFO)
    }

    //http://www.eclipse.org/aspectj/doc/released/devguide/ajc-ref.html

    // http://stackoverflow.com/questions/3660547/apt-and-aop-in-the-same-project-using-maven
    // https://github.com/uPhyca/gradle-android-aspectj-plugin/blob/8d580d8117932a23209421193da77f175d19d416/plugin/src/main/groovy/com/uphyca/gradle/android/AspectjCompile.groovy
    @TaskAction
    def compile() {
        logger.info("Running ajc ...")

        MessageHandler handler = new MessageHandler(false);
        logger.info("args: $args")
        new Main().run(args as String[], handler);
        for (IMessage message : handler.getMessages(null, true)) {
            switch (message.getKind()) {
                case IMessage.ABORT:
                case IMessage.ERROR:
                case IMessage.FAIL:
                    logger.error message.message, message.thrown
                    break;
                case IMessage.WARNING:
                    logger.warn message.message, message.thrown
                    break;
                case IMessage.INFO:
                    logger.info message.message, message.thrown
                    break;
                case IMessage.DEBUG:
                    logger.debug message.message, message.thrown
                    break;
            }
        }

        ant.move(file: tmpDir, tofile: dstDir)
    }
}

ネットで情報を gradle+aspectj の例を探していると、aspectj の ant task を利用する方法がいくつか出てくるが、aspectj の ant plugin の記述方法を元に gradle で ant task を呼ぶのは、 デバッグしにくいので難しい。aspectj の Main を呼ぶ方法が便利だと思う。 (もちろん、Main を呼ぶのはdocumentedなインターフェースではないので、壊れる可能性があるが、壊れたら別の Main を呼べばいいだけ)

この Main を直接叩く方法は、gradle の android で aspectj するプラグインを参考にした。

まあそういう感じでできるようになったので良かったね、と。

で、load time weaving についても、こんなに遅いわけもないと思うから、もうちょい頑張れば速くなるのかもしれないけど知見がまったくないです。

dropwizard metrics servlet で spring-boot app のメトリクスをいい感じに取得する

spring-boot の MetricsFilter はしょぼい。ステータスコードごとのリクエスト数のカウンタと、最後にアクセスされたリクエストの処理時間ぐらいしか取得できない。

これは不便。Dropwizard ならデフォルトで以下の項目を取得可能。

こういった値を簡単に dropwizard-metrics-servlet を利用して取得するための設定方法を紹介する。

設定方法

依存に以下を入れる。

  compile 'io.dropwizard.metrics:metrics-core:3.1.2'
  compile 'io.dropwizard.metrics:metrics-servlet:3.1.2'

以下のように、Dropwizard Metrics Servlet に入っているフィルタを利用する。これだけ。

package com.example.config;

import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.servlet.AbstractInstrumentedFilter;
import com.codahale.metrics.servlet.InstrumentedFilter;
import com.codahale.metrics.servlet.InstrumentedFilterContextListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.embedded.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

@Configuration
public class InstrumentedFilterConfig {
    @Bean
    public FilterRegistrationBean instrumentedFilter() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(new MyInstrumentedFilter());
        registration.addInitParameter("name-prefix", "instrumented");
        return registration;
    }

    public static class MyInstrumentedFilter extends AbstractInstrumentedFilter {
        private static final String NAME_PREFIX = "responseCodes.";
        private static final int OK = 200;
        private static final int CREATED = 201;
        private static final int NO_CONTENT = 204;
        private static final int BAD_REQUEST = 400;
        private static final int NOT_FOUND = 404;
        private static final int SERVER_ERROR = 500;

        /**
         * Creates a new instance of the filter.
         */
        public MyInstrumentedFilter() {
            super(InstrumentedFilter.REGISTRY_ATTRIBUTE,
                    createMeterNamesByStatusCode(),
                    NAME_PREFIX + "other");
        }

        private static Map<Integer, String> createMeterNamesByStatusCode() {
            final Map<Integer, String> meterNamesByStatusCode = new HashMap<Integer, String>(6);
            meterNamesByStatusCode.put(OK, NAME_PREFIX + "ok");
            meterNamesByStatusCode.put(CREATED, NAME_PREFIX + "created");
            meterNamesByStatusCode.put(NO_CONTENT, NAME_PREFIX + "noContent");
            meterNamesByStatusCode.put(BAD_REQUEST, NAME_PREFIX + "badRequest");
            meterNamesByStatusCode.put(NOT_FOUND, NAME_PREFIX + "notFound");
            meterNamesByStatusCode.put(SERVER_ERROR, NAME_PREFIX + "serverError");
            return meterNamesByStatusCode;
        }
    }

    // Pass MetricRegistry configured by Spring boot to InstrumentedFilter.
    @Bean
    public MyListener myListener() {
        return new MyListener();
    }

    public static class MyListener extends InstrumentedFilterContextListener {
        @Autowired
        private MetricRegistry metricRegistry;

        @Override
        protected MetricRegistry getMetricRegistry() {
            return metricRegistry;
        }
    }
}

これだけ設定すると、Actuator の /metrics に、以下の様な出力が出るようになる。


{
   "classes" : 5922,
   "classes.loaded" : 5922,
   "classes.unloaded" : 0,
   "counter.status.200.hello.id" : 1000,
   "counter.status.200.metrics" : 3,
   "counter.status.404.star-star" : 1000,
   "gauge.response.hello.id" : 3,
   "gauge.response.metrics" : 24,
   "gauge.response.star-star" : 105,
   "gc.ps_marksweep.count" : 1,
   "gc.ps_marksweep.time" : 85,
   "gc.ps_scavenge.count" : 9,
   "gc.ps_scavenge.time" : 151,
   "heap" : 1864192,
   "heap.committed" : 274944,
   "heap.init" : 131072,
   "heap.used" : 205912,
   "httpsessions.active" : 0,
   "httpsessions.max" : -1,
   "instance.uptime" : 52108,
   "instrumented.activeRequests" : 1,
   "instrumented.requests.count" : 2003,
   "instrumented.requests.fifteenMinuteRate" : 2.2068657173426,
   "instrumented.requests.fiveMinuteRate" : 6.51051261011083,
   "instrumented.requests.meanRate" : 37.8061917507824,
   "instrumented.requests.oneMinuteRate" : 29.5187887307098,
   "instrumented.requests.snapshot.75thPercentile" : 4,
   "instrumented.requests.snapshot.95thPercentile" : 18,
   "instrumented.requests.snapshot.98thPercentile" : 40,
   "instrumented.requests.snapshot.999thPercentile" : 135,
   "instrumented.requests.snapshot.99thPercentile" : 72,
   "instrumented.requests.snapshot.max" : 155,
   "instrumented.requests.snapshot.mean" : 5,
   "instrumented.requests.snapshot.median" : 2,
   "instrumented.requests.snapshot.min" : 0,
   "instrumented.requests.snapshot.stdDev" : 12,
   "instrumented.responseCodes.badRequest.count" : 0,
   "instrumented.responseCodes.badRequest.fifteenMinuteRate" : 0,
   "instrumented.responseCodes.badRequest.fiveMinuteRate" : 0,
   "instrumented.responseCodes.badRequest.meanRate" : 0,
   "instrumented.responseCodes.badRequest.oneMinuteRate" : 0,
   "instrumented.responseCodes.created.count" : 0,
   "instrumented.responseCodes.created.fifteenMinuteRate" : 0,
   "instrumented.responseCodes.created.fiveMinuteRate" : 0,
   "instrumented.responseCodes.created.meanRate" : 0,
   "instrumented.responseCodes.created.oneMinuteRate" : 0,
   "instrumented.responseCodes.noContent.count" : 0,
   "instrumented.responseCodes.noContent.fifteenMinuteRate" : 0,
   "instrumented.responseCodes.noContent.fiveMinuteRate" : 0,
   "instrumented.responseCodes.noContent.meanRate" : 0,
   "instrumented.responseCodes.noContent.oneMinuteRate" : 0,
   "instrumented.responseCodes.notFound.count" : 1000,
   "instrumented.responseCodes.notFound.fifteenMinuteRate" : 1.1078032687181,
   "instrumented.responseCodes.notFound.fiveMinuteRate" : 3.3036876086696,
   "instrumented.responseCodes.notFound.meanRate" : 18.8739613014361,
   "instrumented.responseCodes.notFound.oneMinuteRate" : 15.9438096464588,
   "instrumented.responseCodes.ok.count" : 1003,
   "instrumented.responseCodes.ok.fifteenMinuteRate" : 1.09906858728133,
   "instrumented.responseCodes.ok.fiveMinuteRate" : 3.20687964000899,
   "instrumented.responseCodes.ok.meanRate" : 18.929889559489,
   "instrumented.responseCodes.ok.oneMinuteRate" : 13.5762576633774,
   "instrumented.responseCodes.other.count" : 0,
   "instrumented.responseCodes.other.fifteenMinuteRate" : 0,
   "instrumented.responseCodes.other.fiveMinuteRate" : 0,
   "instrumented.responseCodes.other.meanRate" : 0,
   "instrumented.responseCodes.other.oneMinuteRate" : 0,
   "instrumented.responseCodes.serverError.count" : 0,
   "instrumented.responseCodes.serverError.fifteenMinuteRate" : 0,
   "instrumented.responseCodes.serverError.fiveMinuteRate" : 0,
   "instrumented.responseCodes.serverError.meanRate" : 0,
   "instrumented.responseCodes.serverError.oneMinuteRate" : 0,
   "mem" : 325332,
   "mem.free" : 69031,
   "nonheap" : 0,
   "nonheap.committed" : 51392,
   "nonheap.init" : 2496,
   "nonheap.used" : 50388,
   "processors" : 4,
   "systemload.average" : 3.869140625,
   "threads" : 24,
   "threads.daemon" : 22,
   "threads.peak" : 24,
   "threads.totalStarted" : 28,
   "uptime" : 60801
}

パフォーマンス

実際のところ、こんなに細かく数字とったらパフォーマンス劣化しないの?と気にする貧乏性の人もいると思う。 しかし実際、これは以下の程度のパフォーマンスがでるので、十分速いので気にしなくてよい。

Score:

benchHistogram:  1 wallclock secs ( 1.12 usr +  0.08 sys =  1.20 CPU) @ 3801478.48/s (n=4573152)
benchTimer:  1 wallclock secs ( 1.02 usr +  0.01 sys =  1.03 CPU) @ 3164606.01/s (n=3264693)
benchMetrics:  1 wallclock secs ( 1.15 usr +  0.04 sys =  1.19 CPU) @ 25211790.53/s (n=30020990)

Comparison chart:

                        Rate  benchHistogram  benchTimer  benchMetrics
  benchHistogram   3801478/s              --         20%          -85%
      benchTimer   3164606/s            -17%          --          -87%
    benchMetrics  25211791/s            563%        697%            --

まとめ

Dropwizard metrics を利用することで spring boot application のメトリクスを詳細に取得する方法について紹介した。 実際に動作するサンプルを以下のレポジトリに配置したのでご利用ください。

https://github.com/tokuhirom/spring-boot-dropwizard-metrics-filter-sample

SEE ALSO

参考: http://blog.64p.org/entry/2016/03/21/205935

jspm で angular2 を使ってみる。

jspm+systemjs を angular2 では推してるっぽいし使ってみるかとおもって使ってみるとなかなか一筋縄ではいかない。 ベータ! という感じがしてよい。

そういうわけで、jspm+systemjs でやってみた記録です。

node はすでにセットアップ済みという前提です。

npm install -g typescript jspm

として、必要なツールをインストールする。

次に jspm の初期設定をする。

$ jspm init
Package.json file does not exist, create it? [yes]:
Would you like jspm to prefix the jspm package.json properties under jspm? [yes]:
Enter server baseURL (public folder path) [./]:
Enter jspm packages folder [./jspm_packages]:
Enter config file path [./config.js]:
Configuration file config.js doesn't exist, create it? [yes]:
Enter client baseURL (public folder URL) [/]:
Do you wish to use a transpiler? [yes]:
Which ES6 transpiler would you like to use, Babel, TypeScript or Traceur? [babel]:typescript
(以下略)

なんかいろいろ聞かれてセットアップが完了する。

次に依存モジュールをインストールする。

jspm install angular2 reflect-metadata zone.js es6-shim typescript

config.js の中身を以下の様な感じにいじる。そんなに意味不明な設定はない。

System.config({
    baseURL: "/",
    defaultJSExtensions: true,
    transpiler: "typescript",
    typescriptOptions: {
        "module": "commonjs",
        "emitDecoratorMetadata": true
    },
    paths: {
        "github:*": "jspm_packages/github/*",
        "npm:*": "jspm_packages/npm/*"
    },

    packages: {
        "app": {
            "main": "main",
            "defaultExtension": "ts"
        }
    },

    map: {
        /* ここは自動生成される */
    }
});

で、app/main.ts に以下のように書く。基本的には、通常の angular2 アプリと一緒なのだが、依存を明示しないといけない点があってダサい。 この依存は config.js の中に 'meta' 項目として記載することも可能なのだが、まあ import を明示的にしておいたほうがわかりやすいだろうということで import した。

//import deps
import 'zone.js';
import 'zone.js/dist/long-stack-trace-zone';

import 'reflect-metadata';
import 'es6-shim';

// app code
import {Component} from 'angular2/core';
import {bootstrap} from "angular2/platform/browser";

@Component({
    selector: 'test-app',
    template: '<h4>Hello {{name}}</h4>'
})
class TestApp {
    name: string;

    constructor(){
        this.name = 'Angular2';
        setTimeout(() => {
        this.name = 'Angular2!!!'
        },1500);
    }
}

//start our app
bootstrap(TestApp);

最後に index.html に以下のように書く。

<html>
<head>
    <title>Demo App</title>
    <script src="jspm_packages/system.js"></script>
    <script src="config.js"></script>
</head>
<body>
    <test-app>
        Loading...
    </test-app>

    <script>
        System.import('app');
    </script>
</body>
</html>

bundle

jspm bundle-sfx --minify app

とするだけで bundle 版が作成可能。bundle-sfx で作成した場合、system.js も同梱されてるので、取り扱いが楽。

で、production 環境では以下のように、build.js だけを読めばOK.

<html>
<head>
    <title>Demo App</title>
</head>
<body>
    <test-app>
        Loading...
    </test-app>
    <script src="build.js"></script>
</body>
</html>

今後の展望

java app での良い感じの取り回しについて考えていきたい。

たとえば以下のような構成にするのが良いのではないかと考える。

src/main/java
src/main/resources/static/index.html ← build.js を読む
front/app/main.ts
front/index.html                     ← config.js 読んで、動的に処理

で、web server は file:front/,classpath:/static/ を static file として扱うようにして、gradle で jspm をキックして、build/resources/static/build.js を生成すればよい。

というところまで考えたのでよろしくお願いします。

まとめ

JS 系のツールは、なんかいろいろと設定が膨らみがちだが、jspm は最小限の設定ですむのでいい感じっぽい。 (最初 webpack でやろうと思っていたが、jspm の方が楽だった)

Java アプリとの連携については、ある程度、こんな感じでやればできるなあという感触をつかめた。

どうしてもブラウザでの確認がめんどくさいので、Ajax 通信部分のモッキング含めて、いい感じにテストできるようにしたい。そのへんもちゃんと読まないとな。 https://angular.io/docs/ts/latest/guide/testing.html

あと、本稿の内容は以下の gist の内容を参考に最新版でも動くようにしたものです。 https://gist.github.com/robwormald/429e01c6d802767441ec

oh-my-zsh やめて zplug にしようかと思ったけどやっぱり zgen にした

mput さんの記事→ https://moneyforward.com/engineers_blog/2016/03/15/dotfiles/ を見て、zplug というものの存在を知った。

zplug を設定しようと思ったが、そもそも zsh の設定をこまごまとやるのがあんま好きじゃないし、コマンドのインストールとかまで出来るのが zplug だということで、ややオーバースペックに感じた。 いろいろ見ていると zgen というのが好みにあってる感じっぽかったので zgen にした。

source "${HOME}/.zgen/zgen.zsh"

if ! zgen saved; then
    zgen oh-my-zsh

    zgen oh-my-zsh plugins/git
    zgen oh-my-zsh plugins/ssh-agent

    zgen load zsh-users/zsh-syntax-highlighting
    zgen load "zsh-users/zsh-completions"

    if [[ $HOST = 'www3301gi.sakura.ne.jp' ]]; then
        zgen oh-my-zsh themes/clean
    else
        zgen oh-my-zsh themes/sonicradish
    fi

    zgen save
fi

oh-my-zsh の機能もそんなに使ってなかったし、まあこれでいいかという感じ。

mput さんがおすすめしていた predict on も試してみたが、git stash ってウトウトしたらうっかり git stash pop 実行してしまったりして、イライラするばかりだったのでやめました。

Spring の Freemarker で参照可能な変数について

Spring で freemarker を利用している場合、いくつかの変数がデフォルトで利用可能になっている。

	protected SimpleHash buildTemplateModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) {
		AllHttpScopesHashModel fmModel = new AllHttpScopesHashModel(getObjectWrapper(), getServletContext(), request);
		fmModel.put(FreemarkerServlet.KEY_JSP_TAGLIBS, this.taglibFactory);
		fmModel.put(FreemarkerServlet.KEY_APPLICATION, this.servletContextHashModel);
		fmModel.put(FreemarkerServlet.KEY_SESSION, buildSessionModel(request, response));
		fmModel.put(FreemarkerServlet.KEY_REQUEST, new HttpRequestHashModel(request, response, getObjectWrapper()));
		fmModel.put(FreemarkerServlet.KEY_REQUEST_PARAMETERS, new HttpRequestParametersHashModel(request));
		fmModel.putAll(model);
		return fmModel;
	}

https://github.com/spring-projects/spring-framework/blob/master/spring-webmvc/src/main/java/org/springframework/web/servlet/view/freemarker/FreeMarkerView.java#L295-L304

これはつまり、Freemarker がデフォルトで提供している Servlet と同等の変数を埋めているということになる。 参考: http://freemarker.incubator.apache.org/docs/pgui_misc_servlet.html

といった値が利用可能なので、場合によったら利用してもよい。(controller できちんと処理したほうがよいケースも多いので、乱用しないように気をつけるべし)

たとえば、Spring Security の login form などは以下のように書けばよいってなわけ。(というか spring security のサンプルコードが thymeleaf だったので悩んでて見つけた)

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
>
<head>
    <title>Spring Security Example </title>
</head>
<body>
<#if RequestParameters.error??>
Invalid username and password.
</#if>
<#if RequestParameters.logout??>
You have been logged out.
</#if>
<form action="/login" method="post">
    <div><label> User Name : <input type="text" name="username"/> </label></div>
    <div><label> Password: <input type="password" name="password"/> </label></div>
    <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}">
    <div><input type="submit" value="Sign In"/></div>
</form>
</body>
</html>

gradle で maven central にリリースできた。

http://tagomoris.hatenablog.com/entries/2016/02/16

こちらを参考にしてやったら簡単にできた。 tagomoirs++

// releng: gradle clean && gradle uploadArchives -Prelease

group 'me.geso'
version '1.0.0'

apply plugin: 'java'
apply plugin: "maven"
apply plugin: "signing"

sourceCompatibility = 1.8

//set build variables based on build type (release, continuous integration, development)
def isDevBuild
def isCiBuild
def isReleaseBuild
def sonatypeRepositoryUrl
if (hasProperty("release")) {
    isReleaseBuild = true
    sonatypeRepositoryUrl = "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
} else if (hasProperty("ci")) {
    isCiBuild = true
    version += "-SNAPSHOT"
    sonatypeRepositoryUrl = "https://oss.sonatype.org/content/repositories/snapshots/"
} else {
    isDevBuild = true
    version += "-SNAPSHOT"
}

repositories {
    mavenCentral()
}

task javadocJar(type: Jar) {
    classifier = 'javadoc'
    from "${buildDir}/javadoc"
}

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

artifacts {
    archives javadocJar, sourcesJar
}

signing {
    required { isReleaseBuild }
    sign configurations.archives
}

uploadArchives {
    repositories {
        if (isDevBuild) {
            mavenLocal()
        }
        else {
            mavenDeployer {
                if(isReleaseBuild) {
                    beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
                }

                repository(url: sonatypeRepositoryUrl) {
                    authentication(userName: sonatypeUsername, password: sonatypePassword)
                }

                pom.project {
                    name 'paranoid_dns_resolver'
                    packaging 'jar'
                    description 'Paranoid DNS Resolver'
                    url 'https://github.com/tokuhirom/paranoid_dns_resolver'

                    scm {
                        url "scm:[email protected]:tokuhirom/paranoid_dns_resolver.git"
                        connection "scm:[email protected]:tokuhirom/paranoid_dns_resolver.git"
                        developerConnection "scm:[email protected]:tokuhirom/paranoid_dns_resolver.git"
                    }
                    licenses {
                        license {
                            name 'MIT'
                            url 'https://opensource.org/licenses/MIT'
                        }
                    }
                    developers {
                        developer {
                            id 'tokuhirom'
                            name 'Tokuhiro Matsuno'
                            email '[email protected]'
                        }
                    }
                }
            }
        }
    }
}

dependencies {
    compile 'org.apache.httpcomponents:httpclient:4.5.2'
    compile 'com.github.tomakehurst:wiremock:1.58'
    compile 'org.assertj:assertj-core:3.3.0'
    testCompile group: 'junit', name: 'junit', version: '4.11'
}

task wrapper(type: Wrapper) {
    gradleVersion = '2.11'
}

Spring Boot とファイルアップロード

File upload のサンプルがこれ。 https://spring.io/guides/gs/uploading-files/

http://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/autoconfigure/web/MultipartProperties.html このへんによると、

multipart.location specifies the directory where files will be stored. The default is "". A common value is to use the system's temporary directory, which can be obtained.
multipart.maxFileSize specifies the maximum size permitted for uploaded files. The default is 1Mb.
multipart.maxRequestSize specifies the maximum size allowed for multipart/form-data requests. The default is 10Mb
multipart.fileSizeThreshold specifies the size threshold after which files will be written to disk. Default is 0, which means that the file will be written to disk immediately.

とのこと。multipart.maxFileSize が 1Mb なのは、最近のでっかい画像を考えるとちょっと足りない。

なので、通常は、ファイルをアップロードさせるアプリケーションを開発するときには、明示する必要がある。

multipart.fileSizeThreshold は、なんか大きめの値を設定したくなりがちだが、通常はデフォルトの0で問題ないと思う。小さいファイルをたくさんアップさせるようなサービスの場合は別だが、そんなサービスそうそうない。