Oleg Atamanenko

thoughts about programming

JAX-RS Client API

JAX-RS - набор Java API для работы с REST сервисами. Существует несколько реализаций, о которых я уже писал раньше.

Предположим, что проект А выставляет наружу REST API, который мы хотим использовать в проекте Б. Очевидно, что сразу возникает вопрос - можно ли переиспользовать классы модели и интерфейс в другом проекте. Ответ - да, можно. Client API, появившийся в JAX RS 2.0, упрощает реализацию клиента, но это всё равно не самый оптимальный вариант.

Есть более интересный способ - в замечательной библиотеке Apache CXF есть класс org.apache.cxf.jaxrs.client.JAXRSClientFactory, который позволяет автоматически получать прокси-клиент для работы с сервером.

К сожалению, у меня не получилось завести его с JSON без JAXB - Проблема с Generic Collections - org.apache.cxf.jaxrs.provider.json.JSONProvider подразумевает, что в проекте используется JAXB, с помощью которого у нас проаннотирована модель. Поэтому пришлось написать собственную реализацию javax.ws.rs.ext.MessageBodyReader<T> и javax.ws.rs.ext.MessageBodyWriter.

Основная проблема при работе с JSON - это то, что, в общем случае, у нас нет информации об используемых типах данных, а вкупе с тем, что в Java Generics реализованы с Type Erasure - ситуация усугубляется, если не предпринять каких-то действий.

Для реализации чтения будем использовать Jackson Java JSON-processor и его возможности com.fasterxml.jackson.databind.type.TypeFactory

public class JsonMessageHandler implements MessageBodyReader<Object>, MessageBodyWriter<Object> {

    protected final ObjectMapper mapper;

    public JsonMessageHandler(ObjectMapper mapper) {
        this.mapper = mapper;
    }

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

    @Override
    public Object readFrom(Class<Object> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, String> httpHeaders, InputStream entityStream)
            throws IOException, WebApplicationException {
        if (genericType instanceof ParameterizedType) {
            if (Collection.class.isAssignableFrom(type)) {
                CollectionType collectionType = resolveCollectionType(type, (ParameterizedType) genericType);
                return mapper.readValue(entityStream, collectionType);
            } else if (Map.class.isAssignableFrom(type)) {
                MapType mapType = resolveMapType(type, (ParameterizedType) genericType);
                return mapper.readValue(entityStream, mapType);
            } else {
                Type[] actualTypeArguments = ((ParameterizedType) genericType).getActualTypeArguments();
                Class[] typeArgs = new Class[actualTypeArguments.length];
                for (int i = 0; i < actualTypeArguments.length; i++) {
                    Type actualTypeArgument = actualTypeArguments[i];
                    typeArgs[i] = (Class) actualTypeArgument;
                }
                JavaType javaType =
                        TypeFactory.defaultInstance().constructParametricType(type, typeArgs);
                return mapper.readValue(entityStream, javaType);
            }

        }

        return mapper.readValue(entityStream, type);

    }

    private MapType resolveMapType(Class<Object> type, ParameterizedType genericType) {
        Type[] args = genericType.getActualTypeArguments();
        Class<? extends Map> mapClass = type.asSubclass(Map.class);
        return TypeFactory.defaultInstance().constructMapType(mapClass, (Class) args[0], (Class) args[1]);
    }

    private CollectionType resolveCollectionType(Class<Object> type, ParameterizedType genericType) {
        Type type1 = genericType.getActualTypeArguments()[0];
        Class<? extends Collection> collectionClass = type.asSubclass(Collection.class);
        return TypeFactory.defaultInstance().constructCollectionType(collectionClass, (Class) type1);
    }

    @Override
    public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
        return true;
    }

    @Override
    public long getSize(Object o, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
        return -1L;
    }

    @Override
    public void writeTo(Object o, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream)
            throws IOException, WebApplicationException {
        mapper.writeValue(entityStream, o);
    }
}

Основной код класса сосредоточен в методе readFrom, который определяет типизацию класса и создаёт необходимый подкласс com.fasterxml.jackson.databind.JavaType, который будет использован во время десериализации.

Теперь мы можем создать клиента следующим образом:

List<?> providers = Arrays.asList(new JsonMessageHandler(objectMapper));
UserRest userRest = JAXRSClientFactory.create("http://localhost:8080", UserRest.class, providers);

// usage:
User user = userRest.findByLogin("uthark");

Когда вызывается метод findByLogin под капотом создаётся HTTP Request, дёргается сервер и полученный ответ десериализуется. От конечного пользотеля детали реализации API скрыты.

В следующем посте описано, как автоматически добавить полученные прокси в фабрику бинов Spring для дальнейшего использования.