JBoss.orgCommunity Documentation

第 52 章 MicroProfile Rest Client

52.1. Client proxies
52.2. Concepts imported from JAX-RS
52.3. Beyond JAX-RS and RESTEasy

As the microservices style of system architecture (see, for example, Microservices by Martin Fowler) gains increasing traction, new API standards are coming along to support it. One set of such standards comes from the Microprofile Project supported by the Eclipse Foundation, and among those is one, MicroProfile Rest Client, of particular interest to RESTEasy and JAX-RS. In fact, it is intended to be based on, and consistent with, JAX-RS, and it includes ideas already implemented in RESTEasy. For a more detailed description of MicroProfile Rest Client, see https://github.com/eclipse/microprofile-rest-client. In particular, the API code is in https://github.com/eclipse/microprofile-rest-client/tree/master/api. and the specification is in https://github.com/eclipse/microprofile-rest-client/tree/master/spec.

One of the central ideas in MicroProfile Rest Client is a version of distributed object communication, a concept implemented in, among other places, CORBA, Java RMI, the JBoss Remoting project, and RESTEasy. Consider the resource

@Path("resource")
public class TestResource {

   @Path("test")
   @GET
   String test() {
      return "test";
   }
}

The JAX-RS native way of accessing TestResource looks like

Client client = ClientBuilder.newClient();
String response = client.target("http://localhost:8081/test").request().get(String.class);

The call to TestResource.test() is not particularly onerous, but calling test() directly allows a more natural syntax. That is exactly what Microprofile Rest Client supports:

@Path("resource")
public interface TestResourceIntf {

   @Path("test")
   @GET
   public String test();
}
   
TestResourceIntf service = RestClientBuilder.newBuilder()
                              .baseUrl("http://localhost:8081/")
                              .build(TestResourceIntf.class);
String s = service.test();

The first four lines of executable code are spent creating a proxy, service, that implements TestResourceIntf, but once that is done, calls on TestResource can be made very naturally in terms of TestResourceIntf, as illustrated by the call service.test().

Beyond the natural syntax, another advantage of proxies is the way the proxy construction process quietly gathers useful information from the implemented interface and makes it available for remote invocations. Consider a more elaborate version of TestResourceIntf:

@Path("resource")
public interface TestResourceIntf2 {

   @Path("test/{path}")
   @Consumes("text/plain")
   @Produces("text/html")
   @POST
   public String test(@PathParam("path") String path, @QueryParam("query") String query, String entity);
}

Calling service.test("p", "q", "e") results in an HTTP message that looks like

POST /resource/test/p/?query=q HTTP/1.1
Accept: text/html
Content-Type: text/plain
Content-Length: 1

e

The HTTP verb is derived from the @POST annotation, the request URI is derived from the two instances of the @Path annotation (one on the class, one on the method) plus the first and second parameters of test(), the Accept header is derived from the @Produces annotation, and the Content-Type header is derived from the @Consumes annotation,

Using the JAX-RS API, service.test("p", "q", "e") would look like the more verbose

Client client = ClientBuilder.newClient();
String response = client.target("http://localhost:8081/resource/test/p")
                     .queryParam("query", "q")
                     .request()
                     .accept("text/html")
                     .post(Entity.entity("e", "text/plain"), String.class);

One other basic facility offered by MicroProfile Rest Client is the ability to configure the client environment by registering providers:

TestResourceIntf service = RestClientBuilder.newBuilder()
                              .baseUrl("http://localhost:8081/")
                              .register(MyClientResponseFilter.class)
                              .register(MyMessageBodyReader.class)
                              .build(TestResourceIntf.class);

Naturally, the registered providers should be relevant to the client environment, rather than, say, a ContainerResponseFilter.

注意

So far, the MicroProfile Rest Client should look familiar to anyone who has used the RESTEasy client proxy facility (Section ""RESTEasy Proxy Framework"). The construction in the previous listing would look like

ResteasyClient client = (ResteasyClient) ResteasyClientBuilder.newClient();
TestResourceIntf service = client.target("http://localhost:8081/")
                              .register(MyClientResponseFilter.class)
                              .register(MyMessageBodyReader.class)
                              .proxy(TestResourceIntf.class);

in RESTEasy.

Beyond the central concept of the client proxy, some basic concepts in MicroProfile Client originate in JAX-RS. Some of these have already been introduced in the previous section, since the interface implemented by a client proxy represents the facilities provided by a JAX-RS server. For example, the HTTP verb annotations and the @Consumes and @Produces annotations originate on the JAX-RS server side. Injectable parameters annotated with @PathParameter, @QueryParameter, etc., also come from JAX-RS.

Nearly all of the provider concepts supported by MicroProfile Client also originate in JAX-RS. These are:

  • javax.ws.rs.client.ClientRequestFilter
  • javax.ws.rs.client.ClientResponseFilter
  • javax.ws.rs.ext.MessageBodyReader
  • javax.ws.rs.ext.MessageBodyWriter
  • javax.ws.rs.ext.ParamConverter
  • javax.ws.rs.ext.ReaderInterceptor
  • javax.ws.rs.ext.WriterInterceptor

Like JAX-RS, MicroProfile Client also has the concept of mandated providers. These are

  • JSON-P MessageBodyReader and MessageBodyWriter must be provided.
  • JSON-B MessageBodyReader and MessageBodyWriter must be provided if the implementation supports JSON-B.
  • MessageBodyReaders and MessageBodyWriters must be provided for the following types:
    • byte[]
    • String
    • InputStream
    • Reader
    • File

Some concepts in MicroProfile Rest Client do not appear in either JAX-RS or RESTEasy.

An instance of org.eclipse.microprofile.rest.client.ext.ClientHeadersFactory,

public interface ClientHeadersFactory {

/**
 * Updates the HTTP headers to send to the remote service. Note that providers
 * on the outbound processing chain could further update the headers.
 *
 * @param incomingHeaders - the map of headers from the inbound JAX-RS request. This will
 * be an empty map if the associated client interface is not part of a JAX-RS request.
 * @param clientOutgoingHeaders - the read-only map of header parameters specified on the
 * client interface.
 * @return a map of HTTP headers to merge with the clientOutgoingHeaders to be sent to
 * the remote service.
 */
MultivaluedMap<String, String> update(MultivaluedMap<String, String> incomingHeaders,
                                      MultivaluedMap<String, String> clientOutgoingHeaders);
}

if activated, can do a bulk transfer of incoming headers to an outgoing request. The default instance org.eclipse.microprofile.rest.client.ext.DefaultClientHeadersFactoryImpl will return a map consisting of those incoming headers listed in the comma separated configuration property

org.eclipse.microprofile.rest.client.propagateHeaders

In order for an instance of ClientHeadersFactory to be activated, the interface must be annotated with org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders. Optionally, the annotation may include a value field set to an implementation class; without an explicit value, the default instance will be used.

Although a ClientHeadersFactory is not officially designated as a provider, it is now (as of MicroProfile REST Client specification 1.4) subject to injection. In particular, when an instance of ClientHeadersFactory is managed by CDI, then CDI injection is mandatory. When a REST Client is executing in the context of a JAX-RS implementation, then @Context injection into a ClientHeadersFactory is currently optional. RESTEasy supports CDI injection and does not currently support @Context injection.

MicroProfile Rest Client mandates that implementations must support CDI injection of proxies. At first, the concept might seem odd in that CDI is more commonly available on the server side. However, the idea is very consistent with the microservices philosophy. If an application is composed of a number of small services, then it is to be expected that services will often act as clients to other services.

CDI (Contexts and Dependency Injection) is a fairly rich subject and beyond the scope of this Guide. For more information, see JSR 365: Contexts and Dependency Injection for JavaTM 2.0 (the specification), Java EE 8 Tutorial, or WELD - CDI Reference Implementation.

The fundamental thing to know about CDI injection is that annotating a variable with javax.inject.Inject will lead the CDI runtime (if it is present and enabled) to create an object of the appropriate type and assign it to the variable. For example, in

   public interface Book {
      public String getTitle();
      public void setTitle(String title);
   }

   public class BookImpl implements Book {
      
      private String title;

      @Override
      public String getTitle() {
         return title;
      }
      
      @Override
      public void setTitle(String title) {
         this.title = title;
      }
   }
   
   public class Author {
      
      @Inject private Book book; 
      
      public Book getBook() {
         return book;
      }
   }

The CDI runtime will create an instance of BookImpl and assign it to the private field book when an instance of Author is created;

In this example, the injection is done because BookImpl is assignable to book, but greater discrimination can be imposed by annotating the interface and the field with qualifier annotations. For the injection to be legal, every qualifier on the field must be present on the injected interface. For example:

   @Qualifier
   @Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD})
   @Retention(RetentionPolicy.RUNTIME)
   public @interface Text {}
   
   @Qualifier
   @Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD})
   @Retention(RetentionPolicy.RUNTIME)
   public @interface Graphic {}
   
   @Text
   public class TextBookImpl extends BookImpl { }
   
   @Graphic
   public class GraphicNovelImpl extends BookImpl { }
   
   public class Genius {
      
      @Inject @Graphic Book book;
   }

Here, the class TextBookImpl is annotated with the @Text qualifier and GraphicNovelImpl is annotated with @Graphic. It follows that an instance of GraphicNovelImpl is eligible for assignment to the field book in the Genius class, but an instance of TextBookImpl is not.

Now, in MicroProfile Rest Client, any interface that is to be managed as a CDI bean must be annotated with @RegisterRestClient:

   @Path("resource")
   @RegisterProvider(MyClientResponseFilter.class)
   public static class TestResourceImpl {

      @Inject TestDataBase db;
      
      @Path("test/{path}")
      @Consumes("text/plain")
      @Produces("text/html")
      @POST
      public String test(@PathParam("path") String path, @QueryParam("query") String query, String entity) {
         return db.getByName(query);
      }
   }

   @Path("database")
   @RegisterRestClient
   public interface TestDataBase {
      
      @Path("")
      @POST
      public String getByName(String name);
   }

Here, the MicroProfile Rest Client implementation creates a proxy for a TestDataBase service, allowing easy access by TestResourceImpl. Notice, though, that there's no indication of where the TestDataBase implementation lives. That information can be supplied by the optional @RegisterProvider parameter baseUri:

   @Path("database")
   @RegisterRestClient(baseUri="https://localhost:8080/webapp")
   public interface TestDataBase {
      
      @Path("")
      @POST
      public String getByName(String name);
   }

which indicates that an implementation of TestDatabase can be accessed at https://localhost:8080/webapp. The same information can be supplied externally with the system variable

<fqn of TestDataBase>/mp-rest/uri=<URL>

or

<fqn of TestDataBase>/mp-rest/url=<URL>

which will override the value hardcoded in @RegisterRestClient. For example,

com.bluemonkeydiamond.TestDatabase/mp-rest/url=https://localhost:8080/webapp

A number of other properties will be examined in the course of creating the proxy, including, for example

com.bluemonkeydiamond.TestDatabase/mp-rest/providers

a comma separated list of provider classes to be registered with the proxy. See the MicroProfile Client documentation for more such properties.

These properties can be simplified through the use of the configKey field in @RegisterRestClient. For example, setting the configKey as in

@Path("database")
@RegisterRestClient(configKey="bmd")
public interface TestDataBase { ... }

allows the use of properties like

bmd/mp-rest/url=https://localhost:8080/webapp

Note that, since the configKey is not tied to a particular interface name, multiple proxies can be configured with the same properties.

An interface method can be designated as asynchronous by having it return a java.util.concurrent.CompletionStage. For example, in

public interface TestResourceIntf extends Closeable {

   @Path("test")
   @GET
   public String test();
   
   @Path("testasync")
   @GET
   public CompletionStage<String> testAsync();
}

the test() method can be turned into the asynchronous method testAsync() by having it return a CompletionStage<String> instead of a String.

Asynchronous methods are made to be asynchronous by scheduling their execution on a thread distinct from the calling thread. The MicroProfile Client implementation will have a default means of doing that, but RestClientBuilder.executorService(ExecutorService) provides a way of substituting an application specific ExecutorService.

The classes AsyncInvocationInterceptorFactory and AsyncInvocationInterceptor in package org.eclipse.microprofile.rest.client.ext provides a means of communication between the calling thread and the asynchronous thread:

public interface AsyncInvocationInterceptorFactory {

    /**
     * Implementations of this method should return an implementation of the
     * AsyncInvocationInterceptor interface.  The MP Rest Client
     * implementation runtime will invoke this method, and then invoke the
     * prepareContext and applyContext methods of the
     * returned interceptor when performing an asynchronous method invocation.
     * Null return values will be ignored.
     *
     * @return Non-null instance of AsyncInvocationInterceptor
     */
    AsyncInvocationInterceptor newInterceptor();
}

public interface AsyncInvocationInterceptor {

    /**
     * This method will be invoked by the MP Rest Client runtime on the "main"
     * thread (i.e. the thread calling the async Rest Client interface method)
     * prior to returning control to the calling method.
     */
    void prepareContext();

    /**
     * This method will be invoked by the MP Rest Client runtime on the "async"
     * thread (i.e. the thread used to actually invoke the remote service and
     * wait for the response) prior to sending the request.
     */
    void applyContext();

    /**
     * This method will be invoked by the MP Rest Client runtime on the "async"
     * thread (i.e. the thread used to actually invoke the remote service and
     * wait for the response) after all providers on the inbound response flow
     * have been invoked.
     *
     * @since 1.2
     */
     void removeContext();
}

The following sequence of events occurs:

  1. AsyncInvocationInterceptorFactory.newInterceptor() is called on the calling thread to get an instance of the AsyncInvocationInterceptor.

  2. AsyncInvocationInterceptor.prepareContext() is executed on the calling thread to store information to be used by the request execution.

  3. AsyncInvocationInterceptor.applyContext() is executed on the asynchronous thread.

  4. All relevant outbound providers such as interceptors and filters are executed on the asynchronous thread, followed by the request invocation.

  5. All relevant inbound providers are executed on the asynchronous thread, followed by executing AsyncInvocationInterceptor.removeContext()

  6. The asynchronous thread returns.

An AsyncInvocationInterceptorFactory class is enabled by registering it on the client interface with @RegisterProvider.