@ImplementedBy VS TypeLiteral

Google guice のインジェクションバインド定義で、
com.google.inject.TypeLiteral は、良くこのように使用される。

Injector injector = Guice.createInjector(new AbstractModule(){
      @Override
      protected void configure(){
         binder().bind(new TypeLiteral<List<String>>(){})
         .toInstance(new ArrayList<String>());
      }
   }
);

(例題)

import java.util.function.Function;
import com.google.inject.ImplementedBy;

public interface SampleBuilder<R extends BaseDto>{
    public R create(String str, Function<String, R> function);
}
import java.util.function.Function;

public class SampleBuilderImpl<R extends BaseDto> implements SampleBuilder<R> {
    @Override
    public R create(String str, Function<String, R > function){
        return function.apply(str);
    }
}

インジェクション対象は、このような状況とする

@Inject private SampleBuilder<SampleDto> builder;

こんな時、TypeLiteral を使わずに、

Injector injector = Guice.createInjector(new AbstractModule(){
    @Override
    protected void configure(){
        binder().bind(SampleBuilder.class).to(SampleBuilderImpl.class);
    }
});

これはダメだ。
com.google.inject.ConfigurationException: Guice configuration errors
1) [Guice/MissingImplementation]: No implementation for SampleBuilder was bound.

と、怒られてしまう。

TypeLiteral を使うならこうする。

import com.google.inject.TypeLiteral;

Injector injector = Guice.createInjector(new AbstractModule(){
    @Override
    protected void configure(){
        binder().bind(new TypeLiteral<SampleBuilder<SampleDto>>(){}).to(new TypeLiteral<SampleBuilderImpl<SampleDto>>(){});
    }
});

これで解決なのだが、TypeLiteral の bind定義を書かなくても
インターフェースの方で、@ImplementedBy で実装クラスを指定する方法でも解決する。

import com.google.inject.ImplementedBy;

import java.util.function.Function;

@ImplementedBy(SampleBuilderImpl.class)
public interface SampleBuilder<R extends BaseDto>{
    public R create(String str, Function<String, R> function);
}

@ImplementedBy はとても便利

Javaで、JSONキーによるシリアライズ時のソート

このようなクラスオブジェクトが存在したとします。

import java.io.Serializable;
import lombok.Data;
@Data
public class Person implements Serializable{
    private static final long serialVersionUID = 1L;

    private String name;
    private String alias;
    private int id;
    private int age;
}

これを、Jackson ObjectMapperシリアライズすると以下のようになります
インデントを指定してシリアライズ実行

ObjectMapper mapper = new ObjectMapper();
DefaultPrettyPrinter.Indenter indenter = new DefaultIndenter("    ", DefaultIndenter.SYS_LF);
DefaultPrettyPrinter printer = new DefaultPrettyPrinter();
printer.indentObjectsWith(indenter);
printer.indentArraysWith(indenter);
// person = Person オブジェクト生成済
String json = mapper.writer(printer).writeValueAsString(person);
System.out.println(json);
{
    "name" : "佐藤",
    "alias" : "sato",
    "id" : 104,
    "age" : 50
}

でも、これを
 id → name → alias → age
の順、=任意の順にしたい時は、JsonProperty で、index を指定します。

ここに JavaDoc があります。
JsonProperty (Jackson-annotations 2.13.0 API)

import com.fasterxml.jackson.annotation.JsonProperty;
import java.io.Serializable;
import lombok.Data;
@Data
public class Person implements Serializable{
    private static final long serialVersionUID = 1L;

    @JsonProperty(index=1)
    private String name;

    @JsonProperty(index=2)
    private String alias;

    @JsonProperty(index=0)
    private int id;

    @JsonProperty(index=3)
    private int age;
}

すると、以下のとおり期待どおりになります

{
    "id" : 104,
    "name" : "佐藤",
    "alias" : "sato",
    "age" : 50
}

もう1つ、クラスに @JsonPropertyOrder で、プロパティ名(JSONキー)の順列を書く方法もあります。

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import lombok.Data;
@Data
@JsonPropertyOrder({"id", "name", "alias", "age"})
public class Person implements Serializable{
    private static final long serialVersionUID = 1L;

    private String name;

    private String alias;

    private int id;

    private int age;

}

こちらの方が楽な気もします。
@JsonPropertyOrder(alphabetic=true) ならアルファベット順になります、

MockWebServer POST に対応する

HttpClient 実行を含むJUnit で、Mockwebserver を使う - Oboe吹きプログラマの黙示録

okhttp の Mockwebserver で、複数リクエストに対応する - Oboe吹きプログラマの黙示録

ここまでくると、次はPOST送信された内容に対するレスポンスの設定である。
okhttp3.mockwebserver.RecordedRequest から取得する BODY の InputStream を取り出せば良い。

getBody().inputStream() で取り出せる!

import okhttp3.mockwebserver.Dispatcher;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
MockWebServer server = new MockWebServer();
Dispatcher dispatcher = new Dispatcher(){
    @Override
    public MockResponse dispatch(RecordedRequest request) throws InterruptedException{
        try(InputStream inst = request.getBody().inputStream()){
            ObjectMapper mapper = new ObjectMapper();
            Map<String, Object> map = mapper.readValue(inst, new TypeReference<Map<String,Object>>(){});
            // TODO 適切な MockResponse を返却させる
            
        }catch(StreamReadException e){
            e.printStackTrace();
        }catch(DatabindException e){
            e.printStackTrace();
        }catch(IOException e){
            e.printStackTrace();
        }
        return new MockResponse().setResponseCode(404);
    }
};
server.setDispatcher(dispatcher);
try{ server.start(); }catch(IOException e){}

なんか、こうなると以下のようにラムダ関数でまとめたくなりますね。
Function<RecordedRequest, MockResponse>

public static Dispatcher buildDispatcher(Function<RecordedRequest, MockResponse> function){
    return new Dispatcher(){
        @Override
        public MockResponse dispatch(RecordedRequest request) throws InterruptedException{
            return function.apply(request);
        }
    };
}

すると、、、

MockWebServer server = new MockWebServer();
server.setDispatcher(buildDispatcher(request->{
    try(InputStream inst = request.getBody().inputStream()){
        ObjectMapper mapper = new ObjectMapper();
        Map<String, Object> map = mapper.readValue(inst, new TypeReference<Map<String,Object>>(){});
        // TODO 適切な MockResponse を返却させる
        
    }catch(StreamReadException e){
        e.printStackTrace();
    }catch(DatabindException e){
        e.printStackTrace();
    }catch(IOException e){
        e.printStackTrace();
    }
    return new MockResponse().setResponseCode(404);
}));
try{ server.start(); }catch(IOException e){}

でも、ObjectMapper での解析にException はつきものです。
なので、

public static Dispatcher buildDispatcher(Function<RecordedRequest, MockResponse> function
, BiFunction<RecordedRequest, Exception, MockResponse> onCatch){
    return new Dispatcher(){
        @Override
        public MockResponse dispatch(RecordedRequest request) throws InterruptedException{
            try{
                return function.apply(request);
            }catch (Exception e){
                return onCatch.apply(request, e);
            }
        }
    };
}

ここまですると、、

MockWebServer server = new MockWebServer();
server.setDispatcher(buildDispatcher(request->{
        ObjectMapper mapper = new ObjectMapper();
        try(InputStream inst = request.getBody().inputStream()){
            Map<String, Object> map = mapper.readValue(inst, new TypeReference<Map<String,Object>>(){});
            // TODO
            return new MockResponse().addHeader("Content-Type", "application/json; " + "charset=utf-8")
                .setBody("{ \"auth status\":\"OK\" }").setResponseCode(200);
        }catch(Exception ex){
            throw new RuntimeException(ex);
        }
    }, (request, x)->{
        return new MockResponse().setResponseCode(404);
}));
try{ server.start(); }catch(IOException e){}

okhttp の Mockwebserver で、複数リクエストに対応する

okhhtp3 Mockwebserver の使い方の問題ですが、
Mockwebserver 1回の enqueue(MockResponse response) メソッドの設定だけでは、
2回リクエストを実行すると2回目は応答しなくなる。
解決方法は2通り。

enqueue レスポンス設定をリクエストの回数分実行しておく

これはテストする HttpClient の実行回数に合わせてenqueue 実行を並べる。

MockWebServer server = new MockWebServer();
// 3回リクエストに対応
IntStream.rangeClosed(1, 3).boxed().forEach(i->{
    server.enqueue(new MockResponse().addHeader("Content-Type", "application/json; " + "charset=utf-8")
    .setBody("{ \"status\":\"OK\", \"count\":" + i + " }").setResponseCode(200));
});
try{  server.start(); }catch(IOException e){ }
// TODO HttpClient 実行テスト3回
// 終わったら server.shutdown(); を忘れずに。
Dispatcher を使う

okhttp3.mockwebserver.Dispatcher をインポートしてこの抽象クラスを
実装化して MockWebServer にディスパッチャーとして登録する。
これは、リクエストによって返却する MockResponse を変更できるのでリクエストのテストケースに
対応することが可能である。

MockWebServer server = new MockWebServer();
Dispatcher dispatcher = new Dispatcher() {
    @Override
    public MockResponse dispatch(RecordedRequest request) throws InterruptedException {
        if (request.getPath().equals("/v1/login/auth/")){
            return new MockResponse().addHeader("Content-Type", "application/json; " + "charset=utf-8")
                .setBody("{ \"auth status\":\"OK\" }").setResponseCode(200);
        }else if(request.getPath().equals("/v1/check/version/")){
            return new MockResponse().addHeader("Content-Type", "application/json; " + "charset=utf-8")
                .setBody("{ \"check status\":\"OK\" }").setResponseCode(200);
        }else if(request.getPath().equals("/v1/profile/info/")){
            return new MockResponse().addHeader("Content-Type", "application/json; " + "charset=utf-8")
                .setBody("{ \"info\":\"ABCD\" }").setResponseCode(200);
        }
        return new MockResponse().setResponseCode(404);
    }
};
server.setDispatcher(dispatcher);
try{  server.start(); }catch(IOException e){ }
// TODO HttpClient 実行テスト
// 終わったら server.shutdown(); を忘れずに。

リクエストが期待どおりでなければ、return new MockResponse().setResponseCode(404);
を実行している。
HttpClient にセットする MockwebServer に向けるために、
HttpClient のURLは以下で生成する String から作成するようにする。

String path = server.url("/v1/login/auth/").toString();
// これを、HttpRequest.newBuilder().uri メソッドに渡す URI 作成、URI.create(path) とする  

HttpClient 実行を含むJUnit で、Mockwebserver を使う

Square社が開発した Apache License2.0 の OkHttp を使う

<dependency>
   <groupId>com.squareup.okhttp3</groupId>
   <artifactId>mockwebserver</artifactId>
   <version>4.0.1</version>
   <scope>test</scope>
</dependency>

Java11 HttpClient を使ったテスト対象プログラム(省略して書いてます)

@Inject @Named("BASEURL")
private String baseURL;

URL path の "~/" までがインジェクションされるものとする

HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1)
.followRedirects(HttpClient.Redirect.NORMAL)
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseURL + "sample"))
.POST(BodyPublishers.ofString(postJson))
.build();

JUnit ソースは、だいたいこんな風に書く

private MockWebServer server;

@Before
public void before() {
    server = new MockWebServer();
}

@Test
public void case1()  {
    server = new MockWebServer();
    server.enqueue(new MockResponse().addHeader("Content-Type", "application/json; " + "charset=utf-8")
    .setBody("{ \"status\":\"OK\" }").setResponseCode(200));

    // HttpClient 実行する対象インスタンスを生成
    // MockWebServer で作るURLをテスト対象に登録する
    Injector injector = Guice.createInjector(new AbstractModule(){
        @Override
        protected void configure(){
            binder().bind(String.class).annotatedWith(Names.named("BASEURL"))
            .toInstance(server.url("/").toString());
        }
    });
    // 実行と検証
    Sample sample = injector.getInstance(Sample.class);
    // TODO 実行と検証
}

public void after() {
   try{
       server.shutdown();
   }catch(IOException e){
       e.printStackTrace();
   }
}

このJUnit 実行時、MockWebServer が、どんなURLをテスト用の Base URL を生成しているか?
というと、
http://127.0.0.1:61993
ポート番号は、Dynamic Port Number 49152 ~ 65535 が使われている。
これは実行の都度、違う番号

HttpClient.Redirect.SAME_PROTOCOL → NORMAL

もうだいぶ年月がたってしまった、Java9 で HttpClient インキュベーター
Java11 で HttpClient 正式リリース
昔書いた、Java9 の HttpClient を試す - Oboe吹きプログラマの黙示録
この中で HttpClient.Redirect.SAME_PROTOCOL「同じプロトコルのみにリダイレクトします。」
をリクエスト作成時に指定したりしていたが、つまり指定したURLで同じプロトコルとして違うURLにリダイレクトされることがある
この指定なんですが、
Java11 の案件で HttpClient を使うという機会に巡り合わないと
SAME_PROTOCOL という指定方法がなくなったことを気がつかない。。
bugs.java.com
この中で、、、

5. The `HttpClient.Redirect` policy has been simplified, by replacing
`SAME_PROTOCOL` and `SECURE` policies, with `NORMAL`. It has been
observed that the previously named `SECURE` was not really appropriately
named and should be renamed to `NORMAL`, since it will likely be
suitable for most normal cases. Given the newly named, aforementioned,
`NORMAL`, `SAME_PROTOCOL` appears oddly named, possibly confusing, and
not likely to be used.

要するに、SECURE と混乱するから簡略すべきで、NORMAL に置き換える。
ことになったんだと。

こう書くことになる。

HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1)
.followRedirects(HttpClient.Redirect.NORMAL)
.build();

HttpClient のモックを書くとしたら、、

Java11 の HttpClient のモックを書くとしたら、
だいたいこんな感じかな。。。

@Test
public void case1()  {
    try{
        HttpResponse<Object> resp = Mockito.mock(HttpResponse.class);
        HttpClient client = Mockito.mock(HttpClient.class);

        Mockito.when(resp.body()).thenReturn("hello");
        Mockito.when(client.send(Mockito.any(), Mockito.any(HttpResponse.BodyHandlers.ofString().getClass())))
            .thenReturn(resp );

        // TODO 実行と検証
        
    }catch(Exception e){
        e.printStackTrace();
        fail();
    }
}