Comenzando con cxf

Cliente web básico con proveedor

Para empezar, necesitamos una fábrica que produzca WebClients.

public class ClientFactory {
    private Map<String, WebClient> cache = new HashMap<>();

    public enum RESTClient {
        PORTAL;
    }

    public WebClient fetchRestClient(RESTClient restClient) {

        if (this.cache.containsKey(restClient)) {
            return WebClient.fromClient(this.cache.get(rc));
        }

        if (RESTClient.enum.equals(rc)) {

            List<Object> providers = new ArrayList<Object>();
            providers.add(new GsonMessageBodyProvider());

            WebClient webClient = WebClient.create("https://blah.com", providers);

            HTTPConduit conduit = WebClient.getConfig(webClient).getHttpConduit();
            conduit.getClient().setReceiveTimeout(recieveTimeout);
            conduit.getClient().setConnectionTimeout(connectionTimout);

            this.cache.put(RESTClient.CAT_DEVELOPER_PORTAL.name(), webClient);
            return WebClient.fromClient(webClient);// thread safe
        }
    }
}
  • Primero creamos una lista de proveedores (llegaremos a ellos más adelante)
  • A continuación creamos un nuevo cliente web usando la fábrica estática “create()”. Aquí podemos agregar algunas otras cosas como credenciales de autenticación básica y seguridad de subprocesos. Por ahora solo usa este.
  • A continuación, extraemos el HTTPConduit y establecemos los tiempos de espera. CXF viene con clientes de clase base de Java, pero puede usar Glassfish u otros.
  • Finalmente cacheamos el WebClient. Esto es importante ya que el código hasta ahora es costoso de crear. La siguiente línea muestra cómo hacerlo seguro para subprocesos. Tenga en cuenta que esta es también la forma en que extraemos el código del caché. Esencialmente estamos haciendo un modelo de la llamada REST y luego lo clonamos cada vez que lo necesitamos.
  • Observe lo que NO está aquí: no hemos agregado ningún parámetro o URL. Estos se pueden agregar aquí, pero eso haría un punto final específico y queremos uno genérico. Además, no hay encabezados agregados a la solicitud. Estos no superan el “clon”, por lo que deben agregarse más tarde.

Ahora tenemos un WebClient que está listo para funcionar. Configuremos el resto de la llamada.

public Person fetchPerson(Long id) {
    long timer t = System.currentTimeMillis();
    Person person = null;
    try {
        wc = this.factory.findWebClient(RESTClient.PORTAL);
        wc.header(AUTH_HEADER, SUBSCRIPTION_KEY);
        wc.header(HttpHeaders.ACCEPT, "application/person-v1+json");

        wc.path("person").path("base");
        wc.query("id", String.valueOf(id));

        person = wc.get(Person.class);
    }
    catch (WebApplicationException wae) {
        // we wanna skip these. They will show up in the "finally" logs.
    }
    catch (Exception e) {
        log.error(MessageFormat.format("Error fetching Person: id:{0} ", id), e);
    }
    finally {
        log.info("GET HTTP:{} - Time:[{}ms] - URL:{} - Content-Type:{}", wc.getResponse().getStatus(), (System.currentTimeMillis() - timer), wc.getCurrentURI(), wc.getResponse().getMetadata().get("content-type"));
        wc.close();
    }
    return p;
}
  • Dentro del “probar” tomamos un WebClient de fábrica. Este es uno nuevo “escarchado”.
  • A continuación, establecemos algunos encabezados. Aquí agregamos algún tipo de encabezado de autenticación y luego un encabezado de aceptación. Tenga en cuenta que tenemos un encabezado de aceptación personalizado.
  • A continuación se agrega la ruta y las cadenas de consulta. Tenga en cuenta que no hay ningún orden en estos pasos.
  • Finalmente hacemos el “get”. Hay varias maneras de hacer esto, por supuesto. Aquí tenemos a CXF haciendo el mapeo JSON por nosotros. Cuando se hace de esta manera, tenemos que lidiar con WebApplicationExceptions. Entonces, si obtenemos un 404, CXF lanzará una excepción. Observe que aquí como esas excepciones porque simplemente registro la respuesta en el archivo finalmente. Sin embargo, quiero obtener OTRA excepción, ya que podrían ser importantes.
  • Si no le gusta este cambio de manejo de excepciones, puede recuperar el objeto de respuesta desde “obtener”. Este objeto contiene la entidad y el código de estado HTTP. NUNCA lanzará una WebApplicationException. El único inconveniente es que no hace el mapeo JSON por usted.
  • Finalmente, en la cláusula “finally” tenemos un “wc.close()”. Si obtiene el objeto de la Cláusula get, realmente no tiene que hacer esto. Sin embargo, algo podría salir mal, por lo que es un buen mecanismo de seguridad.

Entonces, ¿qué pasa con el encabezado “aceptar”: application/person-v1+json? ¿Cómo sabrá CXF cómo analizarlo? CXF viene con algunos analizadores JSON incorporados, pero quería usar Gson. Muchas de las otras implementaciones de los analizadores Json necesitan algún tipo de anotación, pero Gson no. Es estúpido fácil de usar.

public class GsonMessageBodyProvider<T> implements MessageBodyReader<T>, MessageBodyWriter<T> {

    private Gson gson = new GsonBuilder().create();
    
    @Override
    public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
        return StringUtils.endsWithIgnoreCase(mediaType.getSubtype(), "json");
    }

    @Override
    public T readFrom(Class<T> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, String> httpHeaders, InputStream entityStream) throws IOException, WebApplicationException {
        try {
            return gson.fromJson(new BufferedReader(new InputStreamReader(entityStream, "UTF-8")), type);
        }
        catch (Exception e) {
            throw new IOException("Trouble reading into:" + type.getName(), e);
        }
    }

    @Override
    public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
        return StringUtils.containsIgnoreCase(mediaType.getSubtype(), "json");
    }

    @Override
    public long getSize(T t, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
        return 0;
    }

    @Override
    public void writeTo(T t, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException {
        try {
            JsonWriter writer = new JsonWriter(new OutputStreamWriter(entityStream, "UTF-8"));
            writer.setIndent("  ");
            gson.toJson(t, type, writer);
            writer.close();
        }
        catch (Exception e) {
            throw new IOException("Trouble marshalling:" + type.getName(), e);
        }
    }
}

El código es sencillo. Implementa dos interfaces: MessageBodyReader y MessageBodyWriter (o solo una) y luego lo agrega a los “proveedores” al crear el WebClient. CXF se da cuenta a partir de ahí. Una opción es devolver “verdadero” en los métodos “isReadable()” “isWriteable()”. Esto asegurará que CXF use esta clase en lugar de todas las integradas. Los proveedores agregados se verificarán primero.

Configuración de CXF para JAX-RS

Los frascos para CXF JAX-RS se encuentran en Maven:

<!-- https://mvnrepository.com/artifact/org.apache.cxf/cxf-rt-rs-client -->
<dependency>
    <groupId>org.apache.cxf</groupId>
    <artifactId>cxf-rt-rs-client</artifactId>
    <version>3.1.10</version>
</dependency>

Estos frascos son todo lo que necesita para que funcione:

cxf-rt-rs-client-3.1.10.jar
cxf-rt-transports-http-3.1.10.jar
cxf-core-3.1.10.jar
woodstox-core-asl-4.4.1.jar
stax2-api-3.1.4.jar
xmlschema-core-2.2.1.jar
cxf-rt-frontend-jaxrs-3.1.10.jar
javax.ws.rs-api-2.0.1.jar
javax.annotation-api-1.2.jar

Filtros de clientes

Una buena razón para usar filtros es para iniciar sesión. Usando esta técnica, una llamada REST se puede registrar y programar fácilmente.

public class RestLogger implements ClientRequestFilter, ClientResponseFilter {
    private static final Logger log = LoggerFactory.getLogger(RestLogger.class);

    // Used for timing this call.
    private static final ThreadLocal<Long> startTime = new ThreadLocal<Long>();
    private boolean logRequestEntity;
    private boolean logResponseEntity;

    private static Gson GSON = new GsonBuilder().create();

    public RestLogger(boolean logRequestEntity, boolean logResponseEntity) {
        this.logRequestEntity = logRequestEntity;
        this.logResponseEntity = logResponseEntity;
    }


    @Override
    public void filter(ClientRequestContext requestContext) throws IOException {
        startTime.set(System.currentTimeMillis());
    }

    @Override
    public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) throws IOException {
        StringBuilder sb = new StringBuilder();
        sb.append("HTTP:").append(responseContext.getStatus());
        sb.append(" - Time:[").append(System.currentTimeMillis() - startTime.get().longValue()).append("ms]");
        sb.append(" - Path:").append(requestContext.getUri());
        sb.append(" - Content-type:").append(requestContext.getStringHeaders().getFirst(HttpHeaders.CONTENT_TYPE.toString()));
        sb.append(" - Accept:").append(requestContext.getStringHeaders().getFirst(HttpHeaders.ACCEPT.toString()));
        if (logRequestEntity) {
            sb.append(" - RequestBody:").append(requestContext.getEntity() != null ? GSON.toJson(requestContext.getEntity()) : "none");
        }
        if (logResponseEntity) {
            sb.append(" - ResponseBody:").append(this.logResponse(responseContext));
        }
        log.info(sb.toString());
    }

    private String logResponse(ClientResponseContext response) {
        StringBuilder b = new StringBuilder();
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        InputStream in = response.getEntityStream();
        try {
            ReaderWriter.writeTo(in, out);
            byte[] requestEntity = out.toByteArray();
            b.append(new String(requestEntity));
            response.setEntityStream(new ByteArrayInputStream(requestEntity));
        }
        catch (IOException ex) {
            throw new ClientHandlerException(ex);
        }
        return b.toString();
    }
}

Arriba puede ver que la solicitud se intercepta antes de que se envíe la respuesta y se establece un ThreadLocal Long. Cuando se devuelve la respuesta, podemos registrar la solicitud y la respuesta y todo tipo de datos pertinentes. Por supuesto, esto solo funciona para las respuestas de Gson y demás, pero se puede modificar fácilmente. Esto se configura de esta manera:

List<Object> providers = new ArrayList<Object>();
providers.add(new GsonMessageBodyProvider());
providers.add(new RestLogger(true, true)); <------right here!

WebClient webClient = WebClient.create(PORTAL_URL, providers);

El registro provisto debería verse así:

7278 [main] INFO  blah.RestLogger - HTTP:200 - Time:[1391ms] - User:unknown - Path:https://blah.com/tmet/moduleDescriptions/desc?languageCode=en&moduleId=142 - Content-type:null - Accept:application/json - RequestBody:none - ResponseBody:{"languageCode":"EN","moduleId":142,"moduleDescription":"ECAP"}