tokuhirom's blog

orelang を Java で実装してみた

http://qiita.com/shuetsu@github/items/ac21e597265d6bb906dc

わりとよくある JSON ベースの lisp っぽいインタープリタの実装ですが、コードを見ていてもよくわからなかったので自分で実装しなおしてみました。

package com.example;

import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.stream.Collectors;

import com.fasterxml.jackson.databind.ObjectMapper;

public class OreLang {
    HashMap<String, Object> vars = new HashMap<>();

    private Object eval(Object o) {
        if (o instanceof List) {
            System.out.println("Running: " + o);
            Object result = doRun((List<?>) o);
            System.out.println("Result: " + o + " : " + result + " : " + vars);
            return result;
        } else {
            return o;
        }
    }

    private Object doRun(List<?> list) {
        String op = (String) list.get(0);
        List<?> args = list.subList(1, list.size());
        switch (op) {
            case "set":
                vars.put((String) args.get(0), eval(args.get(1)));
                return null;
            case "get":
                return vars.get(args.get(0));
            case "+":
                return (Integer) eval(args.get(0)) + (Integer) eval(args.get(1));
            case "=":
                return eval(args.get(0)) == eval(args.get(1));
            case "until":
                while (!(Boolean) eval(args.get(0))) {
                    eval(args.get(1));
                }
                return null;
            case "step":
                List<Object> collect = args.stream()
                                           .map(this::eval)
                                           .collect(Collectors.toList());
                return collect.get(collect.size() - 1);
        }
        throw new RuntimeException("Unknown operation: " + op);
    }

    public static void main(String[] args) throws IOException {
        String source = "[\"step\",\n"
                        + "  [\"set\", \"i\", 10],\n"
                        + "  [\"set\", \"sum\", 0],\n"
                        + "  [\"until\", [\"=\", [\"get\", \"i\"], 0], [\n"
                        + "    \"step\",\n"
                        + "    [\"set\", \"sum\", [\"+\", [\"get\", \"sum\"], [\"get\", \"i\"]]],\n"
                        + "    [\"set\", \"i\", [\"+\", [\"get\", \"i\"], -1]]\n"
                        + "  ]],\n"
                        + "  [\"get\", \"sum\"]\n"
                        + "]\n";
        ObjectMapper objectMapper = new ObjectMapper();
        Object script = objectMapper.readValue(source, Object.class);
        Object result = new OreLang().eval(script);
        System.out.println("RESULT: " + result);
    }
}
Created: 2016-09-30 01:43:58 +0000
Updated: 2016-09-30 01:43:58 +0000

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

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

結論

  • つけなくても実害はない
  • XML シリアライザが依存に入っている場合、Accept: application/xml のようにヘッダが入っている場合、XML でレスポンスが返却される。
  • ほとんどの場合は produces 指定するかどうか指定するよりも HttpMessageConverter を明示するようにしたほうが良い。
  • 公開される API の場合には HttpMessageConverter を明示的に JSON 以外出力されないように設定しておくほうが良いかもしれない
    • Accept:application/xml を利用していてそれに依存する利用者がいると困る、という程度だが……

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 から除外する、などの対応を取ったほうがいいと思う。

Created: 2016-09-27 21:37:04 +0000
Updated: 2016-09-27 21:37:04 +0000

[java] SnakeYaml の機能を利用して Java でも YAML の custom tag を利用する

YAML には tag という型を規定する機能がある。 ! が文書固有の型で !! がグローバルな型。 see http://yaml.org/type/

h2o の 2.1 では以下のような設定ファイルが書けるようになるそうです。 !file がカスタムタグになっていて、設定されているハンドラによってファイルが読み込まれる。

hosts:
  "example.com":
    listen:
      port: 443
      ssl: !file default_ssl.conf
    paths:
      ...
  "example.org":
    listen:
      port: 443
      ssl:
        <<: !file default_ssl.conf
        certificate-file: /path/to/example.org.crt
        key-file:         /path/to/example.org.crt
    paths:
      ...

同じことを jackson-dataformat-yaml でやろうと思ったが、これは無理。issue は上がっている

jackson-dataformat-yaml の元になっている SnakeYaml を直接使えばいけるので、直接使う。

package me.geso.tinyconfig;

import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.AbstractConstruct;
import org.yaml.snakeyaml.constructor.SafeConstructor;
import org.yaml.snakeyaml.nodes.Node;
import org.yaml.snakeyaml.nodes.Tag;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;

public class ImportableConstructor extends SafeConstructor {
    private Yaml yaml;

    public ImportableConstructor() {
        this.yamlConstructors.put(new Tag("!file"), new FileConstruct());
        this.yamlConstructors.put(new Tag("!resource"), new ResourceConstruct());
    }

    public void setYaml(Yaml yaml) {
        this.yaml = yaml;
    }

    public Yaml getYaml() {
        if (this.yaml == null) {
            throw new IllegalStateException("You must set Yaml object to ImportableConstructor.");
        }
        return this.yaml;
    }

    private class FileConstruct extends AbstractConstruct {
        @Override
        public Object construct(Node nnode) {
            org.yaml.snakeyaml.nodes.ScalarNode snode = (org.yaml.snakeyaml.nodes.ScalarNode) nnode;
            String fileName = snode.getValue();
            try (BufferedReader bufferedReader = Files.newBufferedReader(Paths.get(fileName))) {
                return getYaml().load(bufferedReader);
            } catch (IOException e) {
                throw new YamlImportFailedException(fileName, snode.getTag(), e);
            }
        }
    }

    private class ResourceConstruct extends AbstractConstruct {
        @Override
        public Object construct(Node nnode) {
            org.yaml.snakeyaml.nodes.ScalarNode snode = (org.yaml.snakeyaml.nodes.ScalarNode) nnode;
            String resourceName = snode.getValue();
            try (InputStream resourceAsStream = getClass().getClassLoader().getResourceAsStream(resourceName)) {
                return getYaml().load(resourceAsStream);
            } catch (IOException e) {
                throw new YamlImportFailedException(resourceName, snode.getTag(), e);
            }
        }
    }

    public static class YamlImportFailedException extends RuntimeException {
        public YamlImportFailedException(String fileName, Tag tag, IOException cause) {
            super("Cannot load " + tag.getValue() + " from " + fileName + " : " + cause.getClass().getCanonicalName() + " : " + cause.getMessage(), cause);
        }
    }
}

利用側はこんな感じ。

        this.yaml = new Yaml(importableConstructor);
        importableConstructor.setYaml(yaml);

情報が少ないので難儀するが、頑張れば実装できました。

Java なので、!file!resource をじっそうして、!resource の方は classpath からロードできるようにしているところがオシャレポイントです。

Created: 2016-09-26 00:25:39 +0000
Updated: 2016-09-26 00:25:39 +0000

WebMvcAutoConfiguration で設定されるべき項目が設定されねーぞ!! ってときは @EnableWebMvc ついてないか確認しよう

WebMvcAutoConfiguration/webjars/** に対するパス設定などをしてくれているはずなのに、なぜか有効にならない。。という場合、@EnableWebMvc がついてないか確認しよう。

If you want to keep Spring Boot MVC features, and you just want to add additional MVC configuration (interceptors, formatters, view controllers etc.) you can add your own @Configuration class of type WebMvcConfigurerAdapter, but without @EnableWebMvc. If you wish to provide custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdapter or ExceptionHandlerExceptionResolver you can declare a WebMvcRegistrationsAdapter instance providing such components.

http://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-developing-web-applications.html#boot-features-spring-mvc-auto-configuration

without @EnableWebMvc...!!!

@EnableWebMvc ついた WebMvcConfigurerAdapter があるとデフォルトの設定が諸々オフになって不便になる。/webjars/** もマッピングされなくなる。ウケる。

Created: 2016-09-22 11:46:58 +0000
Updated: 2016-09-22 11:46:58 +0000

kotlin で slf4j 使いたい

    companion object {
        val log = org.slf4j.LoggerFactory.getLogger(this::class.java.enclosingClass)!!
    }

結局このへんが鉄板なんじゃないでしょうか。

KLogger というのもあるようですが、実際手で書いたのと大差ないので。 上記コードをスニペットとして登録して満足しています

【2016.09.22 追記】 enclosingClass 指定しないと companion object 自体を指定してしまうので修正。 ref. https://github.com/MicroUtils/kotlin-logging/blob/master/src/main/kotlin/mu/internal/KLoggerNameResolver.kt

Created: 2016-09-20 23:02:20 +0000
Updated: 2016-09-20 23:02:20 +0000

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 より。どなたでもお気軽にご参加いただけます。

Created: 2016-09-10 04:14:43 +0000
Updated: 2016-09-10 04:14:43 +0000

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() とかで満たしてくれる補完タイプとかありゃいいのだが見つからない)

Created: 2016-09-06 20:44:13 +0000
Updated: 2016-09-06 20:44:13 +0000

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();
    }
}

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

Created: 2016-09-05 01:25:41 +0000
Updated: 2016-09-05 01:25:41 +0000

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 提供できる、とかだと嬉しい。

まとめ

今後に期待。

Created: 2016-08-30 23:38:46 +0000
Updated: 2016-08-30 23:38:46 +0000

コンテナ型の内部にラッパー型が入っている時に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 では問題点が修正されることかと思います(いつでるんや〜)

Created: 2016-08-24 21:53:05 +0000
Updated: 2016-08-24 21:53:05 +0000

spring boot で logging の設定のやり方まとめ

spring boot にはデフォルトでロギング機構が付いている。spring-boot-starter-web の依存に spring-boot-starter-logging 入ってるので、web 有効にしてたら自動でロギング機構も依存に入っている。デフォルトのバックエンドは logback である。

パッケージごとの loglevel の設定は application.yml で出来る。

logging.level.org.springframework.web.servlet.PageNotFound: ERROR

しかし、例えば logback の appender を追加したいなどの場合、もはや logback.xml を設定するしかない。XML で記述するのは苦行だが、耐え忍ぶしかない。(XML ではなく groovy でも設定できるが、groovy の方が情報量が少なく、辛い)。なお、設定ファイルは logback.xml ではなく logback-spring.xml という名前で置くのが spring 流。

logback-spring.xml を置かない場合、デフォルトだと以下の logback-spring.xml を設置されてるのと同じことである。

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <include resource="org/springframework/boot/logging/logback/defaults.xml" />
    <property name="LOG_FILE" value="${LOG_FILE:-${LOG_PATH:-${LOG_TEMP:-${java.io.tmpdir:-/tmp}}}/spring.log}"/>
    <include resource="org/springframework/boot/logging/logback/console-appender.xml" />
    <include resource="org/springframework/boot/logging/logback/file-appender.xml" />
    <root level="INFO">
        <appender-ref ref="CONSOLE" />
        <appender-ref ref="FILE" />
    </root>
</included>

最近の実装では、実際には org.springframework.boot.logging.logback.DefaultLogbackConfiguration で設定されている。 https://github.com/spring-projects/spring-boot/blob/master/spring-boot/src/main/java/org/springframework/boot/logging/logback/DefaultLogbackConfiguration.java

Java コードで記述することで 100msec ほど起動速度を削減できているとのこと。 https://github.com/spring-projects/spring-boot/issues/1796

springProfile

さて、spring boot では logback-spring.xml の中で springProfile というタグを記述することで、「特定の profile 下でのみ動作する」みたいな条件つけられるので、これを利用すると良い。

<springProfile name="staging">
    <!-- 'staging' のときに有効 -->
</springProfile>

<springProfile name="dev, staging">
    <!--'dev' または 'staging' のときに有効 -->
</springProfile>

<springProfile name="!production">
    <!--'production' じゃない時に有効 -->
</springProfile>

ファイルにログを出力する設定

src/main/resources/logback/appender/logback-file.xml とかに以下のようなファイルを設置する。

ファイルを出力するディレクトリは、logback の設定ファイルには記述しない。記述したとしてもデフォルト値を設定し、system property などで外から変更できるようにしておくのが望ましい。jar の外から変更できるのが本筋だと思う。logging.path というプロパティから、LOG_PATH という変数が spring.boot により設定されているので、これをベースのディレクトリとして利用している。

java 起動時に -Dlogging.path=/path/to/logs のように指定するのが良いと思う。

<included>
  <!-- ファイルに出力するロガー -->

  <!--
   LOG_PATH は spring boot が設定している。logging.path から取得される値。
   project 名は -Dproject= で java 起動時に指定。
   -->
  <property name="APP_LOG_FILENAME" value="${LOG_PATH}/application/${project}.log"/>

  <appender name="APPLICATION_LOG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>${APP_LOG_FILENAME}</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <fileNamePattern>${APP_LOG_FILENAME}.%d{yyyyMMdd}.gz</fileNamePattern>
      <maxHistory>30</maxHistory>
    </rollingPolicy>
    <encoder>
      <charset>UTF-8</charset>
      <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%t]%X{request_thread} %logger{45}:%L - %msg %n</pattern>
    </encoder>
  </appender>

  <appender name="ASYNC_APPLICATION_LOG_FILE" class="ch.qos.logback.classic.AsyncAppender">
    <appender-ref ref="APPLICATION_LOG_FILE"/>
  </appender>
</included>

jetty のアクセスログを取る

jetty のアクセスログをファイルに書いておく設定がこれです。

src/main/resources/logback/appender/logback-jettyaccesslog.xml とかに置くと良いでしょう。

<included>
  <!-- jetty の access log を出力する -->

  <!--
   LOG_PATH は spring boot が設定している。logging.path から取得される値。
   project は -Dproject= で設定。supervisord.ini で設定している。
   -->
  <property name="JETTY_LOG_FILE" value="${LOG_PATH}/jetty/${project}-request.log"/>

  <appender name="REQUEST_LOG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>${JETTY_LOG_FILE}</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <fileNamePattern>${JETTY_LOG_FILE}.%d{yyyyMMdd}.gz</fileNamePattern>
      <maxHistory>30</maxHistory>
    </rollingPolicy>
    <encoder>
      <pattern>%msg %X%n</pattern>
    </encoder>
  </appender>

</included>

最終的な logback-spring.xml の設定

最終的に logback-spring.xml を以下のように設定すると良い。

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <!-- http://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-logging.html -->

  <!-- spring boot の設定を読む -->
  <include resource="org/springframework/boot/logging/logback/defaults.xml" />
  <include resource="org/springframework/boot/logging/logback/console-appender.xml" />

  <!--ローカル環境及びテスト環境では、INFO レベル以上を console に出力する -->
  <springProfile name="test,local">
    <root level="INFO">
      <appender-ref ref="CONSOLE"/>
    </root>
  </springProfile>

  <springProfile name="release, staging">
    <!-- release,staging 環境では、アプリケーションログはログファイルに書く。ログレベルは INFO -->
    <include resource="logback/appender/logback-file.xml"/>
    <root level="INFO">
      <appender-ref ref="ASYNC_APPLICATION_LOG_FILE"/>
    </root>

    <!-- release, staging 環境では、jetty のアクセスログをファイルに書いておく。fluentd とかでログ飛ばす用 -->
    <include resource="logback/appender/logback-jetty_access_log.xml"/>
    <logger name="org.eclipse.jetty.server.RequestLog" level="INFO" additivity="false">
      <appender-ref ref="REQUEST_LOG_FILE"/>
    </logger>
  </springProfile>

</configuration>

環境ごとの特定パッケージごとのログレベルの変更は、application-release.yml などの YAML ファイルに以下のように記述するようにし、logback-spring.xml には必要以上の設定は書かないほうが良いかと思う。

logging.level.org.springframework.web.servlet.PageNotFound: ERROR
Created: 2016-08-19 03:18:19 +0000
Updated: 2016-08-19 03:18:19 +0000

俺がレビューコメントでよくつけるやつ

基本的にメンバーは良いコードを書くという前提で暮らしています。

  • 良いと思います。テストがあるともっと良いと思います。
    • 変更が軽微で、まあテストなくてもいいけど、あったほうがより良いよねえ、という時に使う
  • 動いてるんならいいと思います
    • 処理が複雑なので、実際に動かしてみないとうまく動くかどうかコードをパッと見ただけだったらよくわからないけど、動いてるんならちゃんと動いてるんだろうな、という気がするときに使います。
  • lgtm
    • シフトキーをオスのもめんどくさいなって時には小文字で書きます。
  • コメントが無いとあとでなんだかわからなくなっちゃうかも?
    • ちょっと複雑な処理なのにコメントないときとか
  • wiki へのリンクを貼ってくだされ~
    • 仕様がちょっと複雑だったり、後から経緯おえないと死ぬな、という時に言います
  • 日本語でOK
    • 英語でコメントついてるけど、コメントの内容が端折り過ぎで説明不足なときにいいます
Created: 2016-08-04 02:48:15 +0000
Updated: 2016-08-04 02:48:15 +0000

spring boot + embedded tomcat で async request を graceful shutdown する

graceful shutdown 厨みたいになってますが、まあいいとして。。 embedded tomcat はちょっと見た感じ async request を綺麗に graceful shutdown する方法が見当たらなかった。

結局、各コントローラなりサーブレットなりで個々に graceful shutdown するのが良さそう。 例えば以下のように。

package com.example;

import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.connector.Connector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer;
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;
import org.springframework.boot.context.embedded.tomcat.TomcatConnectorCustomizer;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;

import javax.annotation.PreDestroy;
import java.util.concurrent.*;

@SpringBootApplication
@Slf4j
public class SpringBootTomcatApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringBootTomcatApplication.class, args);
    }

    @RestController
    public static class MyController {
        private final ExecutorService pool = Executors.newFixedThreadPool(10);

        @GetMapping("/")
        public String ok() {
            return "OK";
        }

        @GetMapping("/sleep")
        public DeferredResult<String> sleep() {
            DeferredResult<String> objectDeferredResult = new DeferredResult<>();
            pool.submit(() -> {
                try {
                    log.info("Sleeping");
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                log.info("Send result");
                objectDeferredResult.setResult("OK");
            });
            return objectDeferredResult;
        }

        /**
         * Graceful shutdown.
         */
        @PreDestroy
        public void stop() {
            log.info("STOP");
            pool.shutdown();
            try {
                if (!pool.awaitTermination(7, TimeUnit.SECONDS)) {
                    log.info("shutdown failed");
                    pool.shutdownNow();
                } else {
                    log.info("shutdown succeeded");
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
}

jetty や undertow なら全体で処理できるからそれでもいいけど、まあ基本的には各コントローラでやるほうが綺麗かも。 あるいは処理が okhttp 等の場合だと、各 Service なりなんなりで @PreDestroy でやるのがいいかも。

【追記】

ThreadPoolTaskExecutor 使えばいいと @making さんと @kazuki43zoo さんから聞いたので、そうしてみたらすっきりしました。 (やってることは内部的には同じ)

package com.example;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;

import javax.annotation.PreDestroy;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

@SpringBootApplication
@Slf4j
public class SpringBootTomcatApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringBootTomcatApplication.class, args);
    }

    @Bean(name = "async1")
    public AsyncTaskExecutor mvcAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setMaxPoolSize(10);
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.setAwaitTerminationSeconds(7);
        return executor;
    }

    @RestController
    public static class MyController {
        @Autowired
        @Qualifier("async1")
        AsyncTaskExecutor asyncTaskExecutor;

        @GetMapping("/")
        public String ok() {
            return "OK";
        }

        @GetMapping("/sleep")
        public DeferredResult<String> sleep() {
            DeferredResult<String> objectDeferredResult = new DeferredResult<>();
            asyncTaskExecutor.submit(() -> {
                try {
                    log.info("Sleeping");
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                log.info("Send result");
                objectDeferredResult.setResult("OK");
            });
            return objectDeferredResult;
        }
    }
}
Created: 2016-07-30 01:18:03 +0000
Updated: 2016-07-30 01:18:03 +0000

undertow と graceful shutdown と

undertow を利用する予定は特にないのだが、undertow だと graceful shutdown はどのように実現可能なのだろうか、ということが気になったので調べてみました。 ここでいう graceful shutdown は listen socket を close したうえで、処理をすべて正常に終了し、終了後にプロセスを exit するようなものを指しています。

いくつかハマりどころがあるので注意。

undertow はドキュメントが貧弱

どうも、貧弱ですね。。ソース読めないと厳しい。ソースか javadoc 眺めて探すとかしないと見つからない。利用方法ものってないからテストから探すとかしないといけない。

DeploymentManager.stop() を呼ばないと Servlet#destroy が呼ばれない

これは、Java EE になれた人だと常識なんだろうけど、DeploymentManager.stop() を明示的に呼ばないと Servlet#destroy が呼ばれないんでクリーンアップ処理が正常に処理されません。shutdown まわりの正しい呼び出し手順が undertow のマニュアルには載ってないので割と困る。

frsyuki++ に教えてもらいました。

GracefulShutdownHandler を利用することで graceful shutdown が可能。

可能なのだが、ドキュメントがないのでテストコードとソースコードから把握する必要がある。

まず、GracefulShtudownHandler#shutdown を呼ぶと shutdown 状態になります。shutdown 状態になるとすべてのリクエストに 503 を返すようになります。 GracefulShutdownHandler#awaitShutdown を呼ぶと、すべてのリクエストが終了するまで待ってくれます。すべてのリクエストが止まったあとにサーバーを止めればOK、ということっぽいです。

しかし、HTTP レベルでのレスポンスを返してしまうので load balancer になんとなくバランスさせるとかは難しい。明示的に load balancer から外すようにしないとダメでしょう(そして、明示的に外すのは nginx だとちょっと面倒なことしないとできない)。health check モジュール入れて、health check の結果に false を返したあとで GracefulShutdownHandler#shutdown 呼んで、、とかすればできる。

ちょっと頑張れば graceful shutdown できる

  • AcceptingChannel.close()
    • listen channel を閉じる
  • GracefulShutdownHandler.shutdown()
    • 新規のリクエストには 503 を返す
  • GracefulShutdownHandler.awaitShutdown()
    • 実行中のすべてのリクエストが処理完了するまで待つ
  • XnioWorker.shutdown()
    • 処理ワーカーを閉じる

という順番で閉じていけばOK。

Undertow.stop() は AcceptingChannel を close して worker を shutdown するところまで一気にやってしまうので使えない。 もともと Undertow クラスは、便利クラスってだけなので無理して利用しなくても良い。

サンプルコードは以下。

package com.example;

import io.undertow.Handlers;
import io.undertow.Undertow;
import io.undertow.UndertowOptions;
import io.undertow.connector.ByteBufferPool;
import io.undertow.server.DefaultByteBufferPool;
import io.undertow.server.handlers.GracefulShutdownHandler;
import io.undertow.server.handlers.PathHandler;
import io.undertow.server.protocol.http.HttpOpenListener;
import io.undertow.servlet.Servlets;
import io.undertow.servlet.api.DeploymentInfo;
import io.undertow.servlet.api.DeploymentManager;
import io.undertow.servlet.api.InstanceHandle;
import lombok.extern.slf4j.Slf4j;
import org.xnio.*;
import org.xnio.channels.AcceptingChannel;

import javax.servlet.AsyncContext;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.Inet4Address;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@Slf4j
public class UndertowApp {
    public static void main(String[] args) throws ServletException, InterruptedException, IOException {
        new UndertowApp().run();
    }

    private void run() throws ServletException, InterruptedException, IOException {
        CountDownLatch latch = new CountDownLatch(1);

        DeploymentInfo servletBuilder = Servlets.deployment()
                .setClassLoader(UndertowApp.class.getClassLoader())
                .setContextPath("/")
                .setDeploymentName("async.war")
                .addServlets(
                        Servlets.servlet("MessageServlet", AsyncResponseServlet.class, () -> new InstanceHandle<Servlet>() {
                            @Override
                            public Servlet getInstance() {
                                log.info("Getting instance");
                                return new AsyncResponseServlet(latch);
                            }

                            @Override
                            public void release() {
                                /* nop */
                            }
                        })
                                .setAsyncSupported(true)
                                .addInitParam("message", "Hello World")
                                .addMapping("/*")
                );

        DeploymentManager manager = Servlets.defaultContainer()
                .addDeployment(servletBuilder);
        manager.deploy();
        PathHandler path = Handlers
                .path()
                .addExactPath("/", manager.start());

        int ioThreads = Math.max(Runtime.getRuntime().availableProcessors(), 2);
        int workerThreads = ioThreads * 8;

        Xnio xnio = Xnio.getInstance(Undertow.class.getClassLoader());

        XnioWorker worker = xnio.createWorker(OptionMap.builder()
                .set(Options.WORKER_IO_THREADS, ioThreads)
                .set(Options.CONNECTION_HIGH_WATER, 1000000)
                .set(Options.CONNECTION_LOW_WATER, 1000000)
                .set(Options.WORKER_TASK_CORE_THREADS, workerThreads)
                .set(Options.WORKER_TASK_MAX_THREADS, workerThreads)
                .set(Options.TCP_NODELAY, true)
                .set(Options.CORK, true)
                .getMap());

        OptionMap socketOptions = OptionMap.builder()
                .set(Options.WORKER_IO_THREADS, ioThreads)
                .set(Options.TCP_NODELAY, true)
                .set(Options.REUSE_ADDRESSES, true)
                .set(Options.BALANCING_TOKENS, 1)
                .set(Options.BALANCING_CONNECTIONS, 2)
                .set(Options.BACKLOG, 1000)
                .getMap();

        OptionMap serverOptions = OptionMap.builder()
                .set(UndertowOptions.NO_REQUEST_TIMEOUT, 60000000)
                .getMap();

        ByteBufferPool buffers = new DefaultByteBufferPool(true, 1024 * 16, -1, 4);

        GracefulShutdownHandler gracefulShutdownHandler = Handlers.gracefulShutdown(path);

        OptionMap undertowOptions = OptionMap.builder().set(UndertowOptions.BUFFER_PIPELINED_DATA, true).addAll(serverOptions).getMap();
        HttpOpenListener openListener = new HttpOpenListener(buffers, undertowOptions);
        openListener.setRootHandler(gracefulShutdownHandler);
        ChannelListener<AcceptingChannel<StreamConnection>> acceptListener = ChannelListeners.openListenerAdapter(openListener);
        AcceptingChannel<? extends StreamConnection> server = worker.createStreamConnectionServer(new InetSocketAddress(Inet4Address.getByName("localhost"), 18080), acceptListener, socketOptions);
        server.resumeAccepts();

        log.info("Send request");
        Thread clientThread = new Thread(this::startClient);
        clientThread.setName("http client");
        clientThread.start();

        log.info("Waiting request");
        latch.await();

        log.info("Stopping listening channel");
        IoUtils.safeClose(server);

        log.info("Entering shutdown state");
        gracefulShutdownHandler.shutdown();

        log.info("Await all requests(7sec)");
        gracefulShutdownHandler.awaitShutdown(7 * 1000);

        log.info("Shutdown workers");
        worker.shutdown();

        log.info("Stopped");

        manager.stop();
        manager.undeploy();

        log.info("Undeployed");

        log.info("joining client thread");
        clientThread.join();
    }

    private void startClient() {
        try (Socket clientSocket = new Socket("localhost", 18080);
             DataOutputStream outToServer = new DataOutputStream(clientSocket.getOutputStream())
        ) {
            outToServer.write("GET / HTTP/1.0\015\012Content-Length: 0\015\012\015\012".getBytes(StandardCharsets.UTF_8));
            log.info("Sent request");
            clientSocket.shutdownOutput();

            StringBuilder builder = new StringBuilder();
            while (true) {
                byte[] buf = new byte[1024];
                int read = clientSocket.getInputStream().read(buf);
                if (read == -1) {
                    log.info("Got response: {}, {},{},{}, {}",
                            builder.toString(),
                            clientSocket.isConnected(),
                            clientSocket.isBound(),
                            clientSocket.isInputShutdown(),
                            clientSocket.isClosed());
                    break;
                }
                builder.append(new String(buf, 0, read)).append("\\n");
            }
        } catch (IOException e) {
            log.error("IOException", e);
            throw new UncheckedIOException(e);
        }
    }

    @Slf4j
    public static class AsyncResponseServlet extends HttpServlet {
        private final ExecutorService pool = Executors.newFixedThreadPool(10);
        private final CountDownLatch latch;

        public AsyncResponseServlet(CountDownLatch latch) {
            this.latch = latch;
        }


        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            AsyncResponseServlet.log.info("Got request: {}", req.getPathInfo());
            this.latch.countDown();

            AsyncContext asyncContext = req.startAsync();

            pool.submit(() -> {
                AsyncResponseServlet.log.info("Sleeping...");
                try {
                    Thread.sleep(3L * 1000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }

                AsyncResponseServlet.log.info("Sending response");
                resp.setStatus(200);
                try {
                    resp.getWriter().print("OK\n");
                } catch (IOException e) {
                    AsyncResponseServlet.log.error("Can't send response", e);
                } finally {
                    AsyncResponseServlet.log.info("complete async thread");
                    asyncContext.complete();
                }
            });
        }

        @Override
        public void destroy() {
            AsyncResponseServlet.log.info("Shutdown servlet");
            this.pool.shutdown();
        }
    }
}
Created: 2016-07-27 00:52:26 +0000
Updated: 2016-07-27 00:52:26 +0000

hs_err_pidの生成タイミング

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

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

VMError::resetsignalhandlers で crashhandler が以下の signals に対して設定される。see VMError::resetsignal_handlers

static const int SIGNALS[] = { SIGSEGV, SIGBUS, SIGILL, SIGFPE, SIGTRAP }; // add more if needed
static const int NUM_SIGNALS = sizeof(SIGNALS) / sizeof(int);
Created: 2016-07-22 23:18:34 +0000
Updated: 2016-07-22 23:18:34 +0000

mybatis の mapper を groovy で書くぐらいなら kotlin でも良いのではないか

mybatis の xml は painful なので、groovy で書く、というライフハックがあるようです。 これはとても良いハックなのですが、最近の情勢を考えると、groovy よりも kotlin で書いておいたほうが良いのかなという気がしなくもない。 kotlin の方が勢いがあり、IDEA のサポートも今後 kotlin のほうが受けやすそうですし。

というわけで、kotlin で書いてみるデモコードを書いてみました。

package com.example.dao

import com.example.entity.Blog
import org.apache.ibatis.annotations.Mapper
import org.apache.ibatis.annotations.Param
import org.apache.ibatis.annotations.Select

@Mapper
interface BlogDao {
    @Select("""
        SELECT * FROM blog
    """)
    fun findAll(): List<Blog>

    @Select("""
        SELECT * FROM blog WHERE id=#{id}
    """)
    fun findById(@Param("id") id: Long): Blog
}

メソッドの定義にやや癖があるものの、今後 kotlin を利用するケースが増えることを考えれば、許容範囲かなといったところ。

フルのサンプルコードはこちら: https://github.com/tokuhirom/java-samples/tree/master/spring-boot-mybatis-kotlin

Created: 2016-07-20 00:30:13 +0000
Updated: 2016-07-20 00:30:13 +0000

jailing を golang に移植した

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

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

  • capabilities で制御したかった
    • jailing は perl で記述されているために、setuid/setcap などで制御することが難しい
    • 今時 suid-perl 使うのもなあ、という。
  • C で書くと getopt の処理とかだるい
  • C++ で書くと、ビルドとかで悩むのがめんどい
  • golang は segv しにくくて楽

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

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


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

Created: 2016-07-17 10:45:52 +0000
Updated: 2016-07-17 10:45:52 +0000

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: foobar@example.com
 > 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 foobar@example.com 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 みたいな構成になってていじりやすい。コードは綺麗。インストールも楽。 今後に期待がモテる。

Created: 2016-06-25 01:06:38 +0000
Updated: 2016-06-25 01:06:38 +0000

circleci_ikachan を書いた

https://github.com/tokuhirom/circleci_ikachan

circle ci の webhook を受け取って、ikachan に forward するやつです。 ちょっと必要だったので書きました。

今回は、go で書いてみました。

Created: 2016-06-24 23:33:06 +0000
Updated: 2016-06-24 23:33:06 +0000

Promgen talk - Prometheus casual talks

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

I'll be release it as OSS application.

Created: 2016-06-17 03:13:52 +0000
Updated: 2016-06-17 03:13:52 +0000

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>
Created: 2016-06-14 06:25:16 +0000
Updated: 2016-06-14 06:25:16 +0000
Next page