Premiers pas avec cxf

Client Web de base avec fournisseur

Pour commencer, nous avons besoin d’une usine qui produit des 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
        }
    }
}
  • Nous créons d’abord une liste de fournisseurs (nous y reviendrons plus tard)
  • Ensuite, nous créons un nouveau client Web en utilisant la fabrique statique “create()”. Ici, nous pouvons ajouter quelques autres éléments tels que les crédits d’authentification de base et la sécurité des threads. Pour l’instant, utilisez celui-ci.
  • Ensuite, nous sortons le HTTPConduit et définissons les délais d’attente. CXF est livré avec des clients de classe de base Java, mais vous pouvez utiliser Glassfish ou d’autres.
  • Enfin, nous mettons en cache le WebClient. Ceci est important car le code est jusqu’à présent coûteux à créer. La ligne suivante montre comment le rendre thread-safe. Notez que c’est aussi ainsi que nous extrayons le code du cache. Essentiellement, nous créons un modèle de l’appel REST, puis le clonons chaque fois que nous en avons besoin.
  • Notez ce qui n’est PAS ici : nous n’avons ajouté aucun paramètre ou URL. Ceux-ci peuvent être ajoutés ici, mais cela créerait un point final spécifique et nous en voulons un générique. De plus, aucun en-tête n’est ajouté à la demande. Ceux-ci ne dépassent pas le “clone”, ils doivent donc être ajoutés plus tard.

Nous avons maintenant un WebClient qui est prêt à fonctionner. Configurons le reste de l’appel.

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;
}
  • À l’intérieur de “l’essai”, nous récupérons un WebClient de l’usine. C’est un nouveau “glacial”.
  • Ensuite, nous définissons quelques en-têtes. Ici, nous ajoutons une sorte d’en-tête Auth, puis un en-tête d’acceptation. Notez que nous avons un en-tête d’acceptation personnalisé.
  • L’ajout du chemin et des chaînes de requête vient ensuite. Gardez à l’esprit qu’il n’y a pas d’ordre dans ces étapes.
  • Enfin on fait le “get”. Il y a plusieurs façons de le faire bien sûr. Ici, nous avons CXF qui fait le mappage JSON pour nous. Lorsque cela est fait de cette façon, nous devons traiter les WebApplicationExceptions. Donc, si nous obtenons un 404, CXF lèvera une exception. Remarquez ici que je mange ces exceptions parce que je viens de consigner la réponse dans le fichier finally. Je veux cependant obtenir toute autre exception car elles pourraient être importantes.
  • Si vous n’aimez pas cette commutation de gestion des exceptions, vous pouvez récupérer l’objet Response à partir du “get”. Cet objet contient l’entité et le code d’état HTTP. Il ne lancera JAMAIS une WebApplicationException. Le seul inconvénient est qu’il ne fait pas le mappage JSON pour vous.
  • Enfin, dans la clause “finally” nous avons un “wc.close()”. Si vous obtenez l’objet de la clause get, vous n’avez pas vraiment à le faire. Quelque chose peut mal tourner, c’est donc une bonne sécurité.

Alors, qu’en est-il de cet en-tête “accept”: application/person-v1+json Comment CXF saura-t-il comment l’analyser? CXF est livré avec des analyseurs JSON intégrés, mais je voulais utiliser Gson. De nombreuses autres implémentations d’analyseurs Json ont besoin d’une sorte d’annotation, mais pas de Gson. C’est stupide facile à utiliser.

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);
        }
    }
}

Le code est simple. Vous implémentez deux interfaces : MessageBodyReader et MessageBodyWriter (ou une seule) puis vous l’ajoutez aux “fournisseurs” lors de la création du WebClient. CXF le comprend à partir de là. Une option consiste à renvoyer “true” dans les méthodes “isReadable()” “isWriteable()”. Cela garantira que CXF utilise cette classe au lieu de toutes celles intégrées. Les fournisseurs ajoutés seront vérifiés en premier.

Configuration de CXF pour JAX-RS

Les jars pour CXF JAX-RS se trouvent dans 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>

Ces bocaux sont tout ce dont vous avez besoin pour le faire fonctionner :

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

Filtres clients

Une bonne raison d’utiliser les filtres est la journalisation. En utilisant cette technique, un appel REST peut être enregistré et chronométré facilement.

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();
    }
}

Ci-dessus, vous pouvez voir que la demande est interceptée avant que la réponse ne soit envoyée et qu’un ThreadLocal Long soit défini. Lorsque la réponse est renvoyée, nous pouvons enregistrer la demande et la réponse et toutes sortes de données pertinentes. Bien sûr, cela ne fonctionne que pour les réponses Gson et autres, mais peut être modifié facilement. Ceci est configuré de cette façon:

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);

Le journal fourni devrait ressembler à ceci :

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"}