tokuhirom's blog

RxJava で無理やり MDC を使うライフハック

import java.util.Map;

import org.slf4j.MDC;

import rx.functions.Action0;
import rx.functions.Func1;

/**
 * Propagate MDC contexts between Observables
 */
public class MdcPropagator implements Func1<Action0, Action0> {
    @Override
    public Action0 call(Action0 original) {
        Map<String, String> context = MDC.getCopyOfContextMap();
        return () -> {
            final Map<String, String> originalMdc = MDC.getCopyOfContextMap();

            if (context != null) {
                MDC.setContextMap(context);
            }

            try {
                original.call();
            } finally {
                if (originalMdc != null) {
                    MDC.setContextMap(originalMdc);
                }
            }
        };
    }
}
Created: 2016-11-25 07:37:58 +0000
Updated: 2016-11-25 07:37:58 +0000

Jet Brains Night 行ってきた

Yahoo! Japan さんのオフィスで Jet Brains Night というミートアップイベントがあったので行ってきた。

大きめのセミナールームを2個ぶち抜いた感じで、スクリーンが前面4面とサイド2面あってとにかく広くてきれいな開場でした。

IntelliJ の tips とか一個ぐらい学べればいいなーと思って行ったけど結構いろいろあって良かったです。


JetBrains 製品全般の紹介という感じで良かった。

JS については以下の tips 知らなかった! なるほどなー。

↑これが目からうろこというか、なるほどそういう考え方あるのか、という話でした。IDEA のプラグインとかに設定を寄せると、IDEA 以外を使ってる人との間で設定の共有が難しくなるので、grunt/gulp/eslint 等に設定を寄せておいて File Watchers というプラグインで処理するという話。

http://448.jp/blog/article/1459/

↑これが便利。IDEA のショートカットが windows 用と mac 用のショートカットがいい感じに表示されて、良い。IDEA のデモをやるときには便利。

これは、たしかにいいなぁ。今まで SQL については language injection 使ってたんですが、JSON については気にしてなかった。JSON も今後は language injection 使う人いそうなんで。

↓structual に replace めっちゃ便利っぽい。が、実際使う段になったらやっぱり regexp で置換するんだろうな、という気もする。

↓これもたまに見ると楽しい。

というのもあって、github enterprise との連携できるならわりと有りな選択肢なんじゃないかなーと思ってみたり。実際にたてて触ってみたい。が、触る時間あるかなー。 という感じ。触ってみないとわからないけど一回手元で動かしてみないとさわれないのがネックか。

Java だと Java 言語を理解してよしなに理解してくれるのと IDE との連携が良さそうなので、試してみたいぞ!

Created: 2016-11-24 04:41:36 +0000
Updated: 2016-11-24 04:41:36 +0000

Vert.x で access log を出したい

router.route().handler(LoggerHandler.create(LoggerFormat.DEFAULT));

でいいのだが、ドキュメントを見ていてもわかりづらいね。

Created: 2016-11-15 05:54:11 +0000
Updated: 2016-11-15 05:54:11 +0000

netty で http server

RxNetty 触ってると、素の Netty だとどうなんだっけ? というところも把握しておかないと厳しい面もありますので、一応ひさびさに netty で素の http server を書いてみる。

keep-alive まわりの処理とか chunked の処理とか手で全部書かないといけないからやはり生で使うものではないな、という感じ。

package com.example;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.*;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;

import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;

import static io.netty.handler.codec.http.HttpHeaderValues.KEEP_ALIVE;

/**
 * もっとも基本的な HTTP server の実装。
 */
@Slf4j
public class HttpServer {
    public static void main(String[] args) throws InterruptedException {
        int port = 3000;

        NioEventLoopGroup group = new NioEventLoopGroup();
        ServerBootstrap bootstrap = new ServerBootstrap();
        bootstrap.group(group)
                .channel(NioServerSocketChannel.class)
                .localAddress(new InetSocketAddress(port))
                .handler(new LoggingHandler(LogLevel.DEBUG))
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ch.pipeline().addLast(
                                new HttpServerCodec(),
                                new HttpObjectAggregator(512 * 1024),
                                new HttpServerHandler());
                    }
                });
        try {
            ChannelFuture f = bootstrap.bind().syncUninterruptibly();
            log.info("Listening: {}", f.channel().localAddress());
            f.channel().closeFuture().syncUninterruptibly();
        } finally {
            group.shutdownGracefully().syncUninterruptibly();
        }
    }

    @Slf4j
    public static class HttpServerHandler extends ChannelInboundHandlerAdapter {
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            FullHttpRequest request = (FullHttpRequest) msg;
            log.info("[{}] {} {} {}",
                    ctx.channel().remoteAddress(),
                    request.protocolVersion(),
                    request.method(),
                    request.uri());
            byte[] content = "Hello".getBytes(StandardCharsets.UTF_8);
            DefaultHttpResponse defaultHttpResponse = new DefaultFullHttpResponse(
                    HttpVersion.HTTP_1_1,
                    HttpResponseStatus.OK,
                    Unpooled.buffer()
                            .writeBytes(content),
                    new DefaultHttpHeaders()
                            .add(HttpHeaderNames.CONTENT_TYPE, "text/plain")
                            .add(HttpHeaderNames.CONTENT_LENGTH, content.length),
                    EmptyHttpHeaders.INSTANCE);

            if (!HttpUtil.isKeepAlive(request)) {
                ctx.writeAndFlush(defaultHttpResponse)
                        .addListener(ChannelFutureListener.CLOSE);
            } else {
                defaultHttpResponse.headers()
                        .add(HttpHeaderNames.CONNECTION, KEEP_ALIVE);
                ctx.writeAndFlush(defaultHttpResponse);
            }
        }

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx,
                                    Throwable cause) {
            log.warn("Caught unhandled exception", cause);
            ctx.close();
        }
    }
}

一通り書いてから気づいたけど https://github.com/netty/netty/tree/4.1/example/src/main/java/io/netty/example/http/helloworld に実装例が載っているわ。

Created: 2016-11-13 15:08:55 +0000
Updated: 2016-11-13 15:08:55 +0000

RxNetty 使ってるときに access log を取得する方法

RxNetty を利用しているときに、access log を取得しておくと便利です。

RxNetty 自体には access log を取得するフックがないので以下のようなフィルタを用意してみました。 DateTimeFormatter の速度を考慮して、1秒以内の場合にはキャッシュしているところがおしゃれです。

利用方法としては、以下のように acccess log 取得用の ロガーに流すだけです。簡単ですね。

        Logger accessLogLogger = LoggerFactory.getLogger("access_log");
        RequestHandler<ByteBuf, ByteBuf> handler = new RxNettyAccessLogFilter((request, response) -> {
            response.writeString("Hello");
            return response.close();
        }, request -> "-", accessLogLogger::info);
       RxNetty.createHttpServer(8080, handler).startAndWait();
Created: 2016-11-13 03:36:42 +0000
Updated: 2016-11-13 03:36:42 +0000

RxNetty のメトリクス取得について

RxNetty でサーバー運用するにあたっては、メトリクスを取得したくなるとおもいますが、デフォルトでは spectator がサポートされています。 spectator は netflix 製のライブラリですが netflix 以外ではほとんど利用されていない様子。情報が極めて少ないです。

コード例などは見当たらないのですが、以下のように Global の metrics に Dropwizard の MetricRepository を紐付けてやれば良いです。

RxNetty.useMetricListenersFactory(new SpectatorEventsListenerFactory());

MetricRegistry metricRegistry = new MetricRegistry();
MetricsRegistry registry = new MetricsRegistry(Clock.SYSTEM, metricRegistry);
Spectator.globalRegistry().add(registry);

JmxReporter.forRegistry(metricRegistry).build().start();

依存は以下のような感じ

        <dependency>
            <groupId>io.reactivex</groupId>
            <artifactId>rxnetty-spectator</artifactId>
            <version>0.4.19</version>
        </dependency>
        <dependency>
            <groupId>com.netflix.spectator</groupId>
            <artifactId>spectator-reg-metrics3</artifactId>
            <version>0.43.0</version>
        </dependency>
        <dependency>
            <groupId>com.netflix.spectator</groupId>
            <artifactId>spectator-api</artifactId>
            <version>0.43.0</version>
        </dependency>

取得されるデータは以下のような感じです。一応データとして取得はできていますが、ちょっとコレジャナイ感じもありますね。

https://gyazo.com/afee897ec9e9f919f9155d41f439a55b

Created: 2016-11-12 09:54:53 +0000
Updated: 2016-11-12 09:54:53 +0000

RxJava2 と reactor どっちがいいのか問題

なるほど!!!!!!!!

Created: 2016-11-10 07:48:53 +0000
Updated: 2016-11-10 07:48:53 +0000

PROXY protocl なサーバーに対して接続テストしたい

PROXY TCP4 255.255.255.255 255.255.255.255 65535 65535
GET /is/alive HTTP/1.0

とか telnet で話かければつながるかわかるって kazuho さんが言ってた。

Created: 2016-10-31 11:00:35 +0000
Updated: 2016-10-31 11:00:35 +0000

hotspot の NullPointerException 時の表示をわかりやすくできないかなあと思って調べたけど挫折した

解決したいこと

foo.bar(baz.boz()); みたいなケースで、NPE が発生した場合に、foo が null で例外発生したのか baz が null で例外発生したのかわからん

やってみる

bash configure --with-native-debug-symbols=internal --enable-headless-only --with-debug-level=slowdebug  --disable-warnings-as-errors
make

動かしてみると以下のような出力を得られる。

Exception in thread "main" java.lang.NullPointerException
        at NPE.main(NPE.java:4)

これは以下のソースから出力されている。Thread 単位で uncaught exception が発生させられている。

jdk/src/java.base/share/classes/java/lang/ThreadGroup.java の uncaughtException だ。

uncaughtException は jdk/src/java.base/share/classes/java/lang/Thread.java の dispatchUncaughtException から呼ばれているようだ。

これは hotspot/src/share/vm/runtime/thread.cpp の JavaThread::exit から呼ばれている。

ここには hotspot/src/share/vm/prims/jni.cpp の jni_DetachCurrentThread から来ていることがわかる。

更にたどるとjdk/src/java.base/share/native/libjli/java.c のJavaMain がエントリポイントになっていて

/* Invoke main method. */
(*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs);


/*
 * The launcher's exit code (in the absence of calls to
 * System.exit) will be non-zero if main threw an exception.
 */
ret = (*env)->ExceptionOccurred(env) == NULL ? 0 : 1;

のあたりで処理が行われる。つまり、psvm 実行された後で、例外が設定されているかどうかを見て、例外がVMに設定されていれば、例外を処理するという形。

問題は、NullPointerException のインスタンスが作成される箇所がどこかという点なので、そこまで追う。

ここで、env は JNIEnv で、これの定義は hotspot/src/share/vm/prims/jni.h にある。

void CallStaticVoidMethodV(jclass cls, jmethodID methodID,
                           va_list args) {
    functions->CallStaticVoidMethodV(this,cls,methodID,args);
}

であって、functions の定義は 

const struct JNINativeInterface_ *functions;

なので、hotspot/src/share/vm/prims/jni.cpp に実装がある。で、それは同じファイルの jniinvokestatic にいく。

hotspot/src/share/vm/runtime/javaCalls.cpp の JavaCalls::call に行くが、ここでまさかの os::osexceptionwrapper に。。

os::osexceptionwrapper は、Win32 対応で必要なだけの存在らしく、実際にはこれだけのことです。linux だと素でただのラッパです。

void
os::os_exception_wrapper(java_call_t f, JavaValue* value, const methodHandle& method,
                         JavaCallArguments* args, Thread* thread) {
  f(value, method, args, thread);
}

つまり、JavaCalls::call は実際には以下と同じ。

call_helper(result, method, args, THREAD);

call_helper の中身は、色々やっているが実際にコールしているのは以下の部分。

  // do call
  { JavaCallWrapper link(method, receiver, result, CHECK);
    { HandleMark hm(thread);  // HandleMark used by HandleMarkCleaner


      StubRoutines::call_stub()(
        (address)&link,
        // (intptr_t*)&(result->_value), // see NOTE above (compiler problem)
        result_val_address,          // see NOTE above (compiler problem)
        result_type,
        method(),
        entry_point,
        args->parameters(),
        args->size_of_parameters(),
        CHECK
      );


      result = link.result();  // circumvent MS C++ 5.0 compiler bug (result is clobbered across call)
      // Preserve oop return value across possible gc points
      if (oop_result_flag) {
        thread->set_vm_result((oop) result->get_jobject());
      }
    }
  } // Exit JavaCallWrapper (can block - potential return oop must be preserved)

で、StubRoutines::call_stub が実際にメソッドを呼んでいる部分なわけですが、 その実装は hotspot/src/share/vm/runtime/stubRoutines.hpp にあります。

こいつは、マクロを使ったハックで pointer から function pointer に warnings 無しでキャストするということをやっていたりして、ちょっと読みにくいですが、実際には StubRoutines::callstub_entry をコールしているだけということになります。

で、 StubRoutines::callstub_entry はマシン語生成していて大変読みにくいということがわかった。

で、色々調べていくと TemplateTable::resolvecacheand_index でメソッド呼び出し分コードは JIT されていることがわかった。

で、これは

InterpreterRuntime::resolvefromcache を呼んでいる。

IRT_ENTRY(void, InterpreterRuntime::resolve_from_cache(JavaThread* thread, Bytecodes::Code bytecode)) {
  switch (bytecode) {
  case Bytecodes::_getstatic:
  case Bytecodes::_putstatic:
  case Bytecodes::_getfield:
  case Bytecodes::_putfield:
    resolve_get_put(thread, bytecode);
    break;
  case Bytecodes::_invokevirtual:
  case Bytecodes::_invokespecial:
  case Bytecodes::_invokestatic:
  case Bytecodes::_invokeinterface:
    resolve_invoke(thread, bytecode);
    break;
  case Bytecodes::_invokehandle:
    resolve_invokehandle(thread);
    break;
  case Bytecodes::_invokedynamic:
    resolve_invokedynamic(thread);
    break;
  default:
    fatal("unexpected bytecode: %s", Bytecodes::name(bytecode));
    break;
  }
}
IRT_END

InterpreterRuntime::resolve_invoke では、LinkResolver::resolve_invoke を呼んでいる

void InterpreterRuntime::resolve_invoke(JavaThread* thread, Bytecodes::Code bytecode) {
  Thread* THREAD = thread;
  // extract receiver from the outgoing argument list if necessary
  Handle receiver(thread, NULL);
  if (bytecode == Bytecodes::_invokevirtual || bytecode == Bytecodes::_invokeinterface) {
    ResourceMark rm(thread);
    methodHandle m (thread, method(thread));
    Bytecode_invoke call(m, bci(thread));
    Symbol* signature = call.signature();
    receiver = Handle(thread,
                  thread->last_frame().interpreter_callee_receiver(signature));
    assert(Universe::heap()->is_in_reserved_or_null(receiver()),
           "sanity check");
    assert(receiver.is_null() ||
           !Universe::heap()->is_in_reserved(receiver->klass()),
           "sanity check");
  }

  // resolve method
  CallInfo info;
  constantPoolHandle pool(thread, method(thread)->constants());

  {
    JvmtiHideSingleStepping jhss(thread);
    LinkResolver::resolve_invoke(info, receiver, pool,
                                 get_index_u2_cpcache(thread, bytecode), bytecode,
                                 CHECK);

LinkResolver::resolve_invoke からは resolve_invokevirtual に遷移している。

void LinkResolver::resolve_invoke(CallInfo& result, Handle recv, const constantPoolHandle& pool, int index, Bytecodes::Code byte, TRAPS) {
  switch (byte) {
    case Bytecodes::_invokestatic   : resolve_invokestatic   (result,       pool, index, CHECK); break;
    case Bytecodes::_invokespecial  : resolve_invokespecial  (result,       pool, index, CHECK); break;
    case Bytecodes::_invokevirtual  : resolve_invokevirtual  (result, recv, pool, index, CHECK); break;
    case Bytecodes::_invokehandle   : resolve_invokehandle   (result,       pool, index, CHECK); break;
    case Bytecodes::_invokedynamic  : resolve_invokedynamic  (result,       pool, index, CHECK); break;
    case Bytecodes::_invokeinterface: resolve_invokeinterface(result, recv, pool, index, CHECK); break;
  }
  return;
}

LinkResolver::resolve_invokevirtual から resolvevirtualcall へ。

void LinkResolver::resolve_invokevirtual(CallInfo& result, Handle recv,
                                          const constantPoolHandle& pool, int index,
                                          TRAPS) {

  LinkInfo link_info(pool, index, CHECK);
  KlassHandle recvrKlass (THREAD, recv.is_null() ? (Klass*)NULL : recv->klass());
  resolve_virtual_call(result, recv, recvrKlass, link_info, /*check_null_or_abstract*/true, CHECK);
}

LinkResolver::resolvevirtualcall は runtimeresolvevirtual_method を呼ぶ

void LinkResolver::resolve_virtual_call(CallInfo& result, Handle recv, KlassHandle receiver_klass,
                                        const LinkInfo& link_info,
                                        bool check_null_and_abstract, TRAPS) {
  methodHandle resolved_method = linktime_resolve_virtual_method(link_info, CHECK);
  runtime_resolve_virtual_method(result, resolved_method,
                                 link_info.resolved_klass(),
                                 recv, receiver_klass,
                                 check_null_and_abstract, CHECK);
}

LinkResolver::runtimeresolvevirtual_method に至って、ついに NullPointerException を投げている!!

void LinkResolver::runtime_resolve_virtual_method(CallInfo& result,
                                                  const methodHandle& resolved_method,
                                                  KlassHandle resolved_klass,
                                                  Handle recv,
                                                  KlassHandle recv_klass,
                                                  bool check_null_and_abstract,
                                                  TRAPS) {

  // setup default return values
  int vtable_index = Method::invalid_vtable_index;
  methodHandle selected_method;

  assert(recv.is_null() || recv->is_oop(), "receiver is not an oop");

  // runtime method resolution
  if (check_null_and_abstract && recv.is_null()) { // check if receiver exists
    THROW(vmSymbols::java_lang_NullPointerException()); ←←←← ここ!!
  }

で、この THROW の実体は以下の通り

#define THROW(name)                                 \
  { Exceptions::_throw_msg(THREAD_AND_LOCATION, name, NULL); return;  }

THREADANDLOCATION は 以下

#define THREAD_AND_LOCATION                      THREAD, __FILE__, __LINE__

THREAD は以下。 要するに、各メソッドの引数に TRAPS をつけておいて、THREAD 変数で参照するってだけ。

// The THREAD & TRAPS macros facilitate the declaration of functions that throw exceptions.
// Convention: Use the TRAPS macro as the last argument of such a function; e.g.:
//
// int this_function_may_trap(int x, float y, TRAPS)

#define THREAD __the_thread__
#define TRAPS  Thread* THREAD

であって、Exceptions::throwmsg は、以下の通り

void Exceptions::_throw_msg(Thread* thread, const char* file, int line, Symbol* name, const char* message,
                            Handle h_loader, Handle h_protection_domain) {
  // Check for special boot-strapping/vm-thread handling
  if (special_exception(thread, file, line, name, message)) return;
  // Create and throw exception
  Handle h_cause(thread, NULL);
  Handle h_exception = new_exception(thread, name, message, h_cause, h_loader, h_protection_domain);
  _throw(thread, file, line, h_exception, message);
}

ここまで調べた結果

なかなか目的を達成するのは難しそうなのでまた今度。

Created: 2016-10-09 13:10:22 +0000
Updated: 2016-10-09 13:10:22 +0000

openjdk をコンパイルして gdb で実行すると sigsegv しちゃうんだけど、って時の対処方法

Java で開発していると openjdk を自分でコンパイルして gdb でステップ実行して内部動作をトレースしたいなーという時があると思います。

そんな時には以下のように、openjdk の configure 時に debug option つけて symbol つけましょう、みたいな情報は調べればすぐ出てくるんです。実際以下のようにオプションつければ OK

bash ./configure --with-debug-level=slowdebug --with-target-bits=64 --disable-zip-debug-info
make all

そして、いざ実行してみると以下のように SIGSEGV だという宣告が!!

(gdb) r
Starting program: /home/tokuhirom/jdk9/./build/linux-x86_64-normal-server-slowdebug/jdk/bin/java NPE
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
[New Thread 0x7ffff7fd3700 (LWP 19431)]

Program received signal SIGSEGV, Segmentation fault.
[Switching to Thread 0x7ffff7fd3700 (LWP 19431)]
0x00007fffe1000513 in ?? ()

どうしたらいいんだ、とおもって色々情報を探していたんですがまったく情報が見つからず、printf デバッグしていたんですが、ふとした拍子に情報を見つけました。

http://stackoverflow.com/questions/35611119/how-to-debug-openjdk-using-gdb

An information worth mentioning that finally when you start the debugging session with gdb and you run the program you may see a SEGFAULT ignore it and continue till gdb stops at your breakpoint.

ええええええ。SIGSEGV って出てもその後そのまま c してけばいつかブレークポイントにたどり着くので気にしなくていいんだってさ!

めっちゃ時間を無駄にしたわー。

Created: 2016-10-08 12:26:20 +0000
Updated: 2016-10-08 12:26:20 +0000

activemq を動かしてみる

brew install apache-activemq
activemq start

で起動完了。

以下のようにして実行開始。

for i in {1..1000}; curl -u admin:admin -d 'body="Hello World"' "http://localhost:8161/api/message/test$i?type=queue"

以下のようにして消費させる。

for i in {1..1000}; curl -u admin:admin "http://localhost:8161/api/message/test$i?type=queue"

しかし、1000 個ぶっこもうとすると途中で止まるので謎い。 (rabbitmq 使うので別にいいんだけど)

Created: 2016-10-05 22:19:38 +0000
Updated: 2016-10-05 22:19:38 +0000

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
Next page