Jersey Client APIの使い方、その1

Jersey Client APIの相手としては、特にJAX-RSサーバに限らず、一般的なウェブサービスとのやりとりに使用できる。ここでは、その方法を見ていく。

※なお、ここではJava9モジュールを使うが、使わない人でもモジュール対応部分を無視すればよい。

依存

以下の依存を入れる。excludeする理由はError occurred during initialization of boot layerに記述した。

// モジュール対応のため
configurations {
   implementation.exclude module:'jakarta.inject' // defines javax.inject
   implementation.exclude module:'jsr305' // defines javax.annotation
   implementation.exclude module:'aopalliance-repackaged' // defines org.aopalliance.aop
}

dependencies {
  implementation group: 'javax.inject', name: 'javax.inject', version: '1'

  implementation group: 'javax.ws.rs', name: 'javax.ws.rs-api', version: '2.1.1'
  implementation group: 'org.glassfish.jersey.media', name: 'jersey-media-json-jackson', version: '2.29'
  implementation group: 'org.glassfish.jersey.inject', name: 'jersey-hk2', version: '2.29'  
  implementation group: 'org.glassfish.jersey.core', name: 'jersey-client', version: '2.29'  
  implementation group: 'javax.xml.bind', name: 'jaxb-api', version: '2.3.1'  
  implementation group: 'org.glassfish.jersey.media', name: 'jersey-media-json-jackson', version: '2.29'
}

module-info

モジュール定義は以下。openにしておかないと、Jersey側からアプリ側のクラスにアクセスできない(個別にやっても良いが面倒なので全開にしている)。

open module some_test {
  requires java.ws.rs;
  requires jersey.media.json.jackson;
  requires com.fasterxml.jackson.databind;
  requires jersey.client;
}

JSONの結果をJavaオブジェクトとして受け取る

ここでは、既にウェブサービスが動作しているものとする。URLを叩くとJSONを返すだけだ。

import javax.ws.rs.client.*;
import javax.ws.rs.core.*;

public class Test1 {

  // Clientはスレッドセーフとのこと
  static Client client = ClientBuilder.newClient();

  public static void main(String[]args) {
    WebTarget target = 
        client.target("http://localhost:8080").path("/api/employees/1");
    Response r = target.request().get();
    Employee e = r.readEntity(Employee.class);
    System.out.println(e);
  }

  public static class Employee {
    public int id;
    public String name;    
    @Override
    public String toString() {
      return "id:" + id + ", name:" + name;
    }
  }
}

出力結果は以下だ。

id:1, name:佐藤

Javaクラスに存在しないフィールドを無視する1

長期的な使用を見越すと、サーバ側から返されたJSON中のフィールドがアプリ側に存在しないことも考えられる。これを単純に無視したいのだが、このままではエラーが発生してしまう。先のEmployeeを以下にすると、

  public static class Employee {
    public int id;   
    @Override
    public String toString() {
      return "id:" + id;
    }
  }

以下のエラーになる。

Exception in thread "main" javax.ws.rs.ProcessingException: Error reading entity from input stream.
    at jersey.common@2.29/org.glassfish.jersey.message.internal.InboundMessageContext.readEntity(InboundMessageContext.java:865)
(略)
Caused by: com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "name" (class some_test.Test1$Employee), not marked as ignorable (one known property: "id"])
 at [Source: (org.glassfish.jersey.message.internal.ReaderInterceptorExecutor$UnCloseableInputStream); line: 1, column: 17] (through reference chain: nato_test.Test1$Employee["name"])
    at com.fasterxml.jackson.databind@2.9.9/com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException.from(UnrecognizedPropertyException.java:61)

受け取ったJSONの不明なフィールドを無視するにはこうする。


import javax.ws.rs.client.*; import javax.ws.rs.core.*; import org.glassfish.jersey.client.*; import org.glassfish.jersey.jackson.internal.jackson.jaxrs.json.*; import com.fasterxml.jackson.databind.*; public class Test2 { static final Client client = ClientBuilder.newClient( new ClientConfig( new JacksonJaxbJsonProvider() .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) ) ); public static void main(String[]args) { WebTarget target = client.target("http://localhost:8080").path("/api/employees/1"); Response r = target.request().get(); Employee e = r.readEntity(Employee.class); System.out.println(e); } public static class Employee { public int id; @Override public String toString() { return "id:" + id; } } }

出力結果は以下になる。

id:1

※これはもちろん「すべてのクラスについてフィールドがなければ無視する」というやり方で、クラスごとに「無視する」設定としてはクラスにアノテーションをつける方法がある。

Javaクラスに存在しないフィールドを無視する2

MessageBodyReaderというものを定義して、完全に自前でJSON・Javaオブジェクト変換を行うこともできる。

import java.io.*;
import java.lang.annotation.*;
import java.lang.reflect.*;

import javax.ws.rs.*;
import javax.ws.rs.client.*;
import javax.ws.rs.core.*;
import javax.ws.rs.ext.*;

import com.fasterxml.jackson.databind.*;

public class Test3 {

  static final Client client = ClientBuilder.newClient().register(MyReader.class);

  public static void main(String[]args) {
    WebTarget target = 
        client.target("http://localhost:8080").path("/api/employees/1");
    Response r = target.request().get();
    Employee e = r.readEntity(Employee.class);
    System.out.println(e);
  }

  @Consumes("application/json")
  public static class MyReader implements MessageBodyReader<Object> {

    ObjectMapper mapper = new ObjectMapper()  
        .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
        ;

    @Override
    public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
      return type.equals(Employee.class);
    }

    @Override
    public Object readFrom(Class<Object> type, Type genericType, Annotation[] annotations, MediaType mediaType,
        MultivaluedMap<String, String> httpHeaders, InputStream entityStream)
        throws IOException, WebApplicationException {
      return mapper.readValue(entityStream,  type);
    }
  }

  public static class Employee {
    public int id;
    @Override
    public String toString() {
      return "id:" + id;
    }
  }
}

出力結果は前と同じだ。

非同期で実行する

以上の例は同期、つまりサーバ側から結果が返ってくるまでブロックされるのだが、非同期で実行することもできる。

get()を実行する前にasync()を挟むと、結果はResponseではなく、Futureとなる。これに対してget()を実行すると、ブロックしてしまうため、別スレッドで行う。

  public static void main(String[]args) {
    WebTarget target = 
        client.target("http://localhost:8080").path("/api/employees/1");    
    // get()の前にasync()を挟む
    Future<Response>future = target.request().async().get();     
    ExecutorService service =  Executors.newSingleThreadExecutor();
    System.out.println("start");
    service.submit(()-> {
      Response r;
      try {
        r = future.get();
      } catch (Exception ex) {
        ex.printStackTrace();
        return;
      }
      Employee e = r.readEntity(Employee.class);
      System.out.println(e);      
    });
    service.shutdown();    
    System.out.println("finished");
  }

出力結果は以下だ。

start
finished
id:1name:佐藤

POSTしてみる

新たな従業員を作成してPOSTしてみる。返り値はその従業員を返す。

  public static void main(String[]args) {
    WebTarget target = 
        client.target("http://localhost:8080").path("/api/employees/1");    
    Response r = target.request().post(Entity.json(new Employee(2, "田中")));
    System.out.println(r.readEntity(Employee.class));    
  }

  public static class Employee {
    public int id;
    public String name;
    public Employee() {}
    public Employee(int id, String name) {
      this.id = id;
      this.name = name;
    }
    @Override
    public String toString() {
      return "id:" + id + ",name:" + name;
    }
  }

出力結果は以下。

id:2,name:田中