Статьи

Декларативная гиперссылка в RESTEasy

API REST должны управляться гипертекстом . Это требование имеет немало последствий для определения интерфейса RESTful, что, в свою очередь, имеет немало последствий для видов инструментов, необходимых для разработки API RESTful. Как разработчик Java, меня интересует, как хорошо принятая спецификация JAX-RS может справиться с этими последствиями. Однако, прежде чем я исследую JAX-RS, давайте взглянем на некоторые из гипертекстовых последствий.

Что такое ресурс?

Ресурс это вещь

Начну с вопроса: «Что такое ресурс?» Ну, если вы знаете что-нибудь о REST, вы знаете, что ресурсы — это «то, что представлено URL». Ресурс — это уникальная вещь, которая имеет свой собственный URL.

Я уверен, что в этот момент вы думаете: «Ресурс — это вещь с URL. Большое спасибо, Соломон. Это все объясняет». с сарказмом. Ресурс это вещь. Это действительно отличное объяснение.

Второй удар — домен

В порядке Хорошо. Я еще раз попробую определить ресурс. «Ресурс может быть представлением объекта домена». Например, «GET http://mymusic.com/artist/1249938» может выдавать данные, относящиеся к MC Hammer (помните его?). Этот URL инкапсулирует MC Hammer-ness в «My Music Company, Inc.»

Это лучше, но определенно не полная картина, так сказать. Ресурсом также может быть изображение MC Hammer, электронная таблица активов MC Hammer, HTML-страница с подробным описанием его истории, файл Java Script, представляющий некоторую логику и код представления, функциональность поиска и множество других полезных «вещей».

Вернуться к «вещи»?

О, хорошо, я думаю, что я к сожалению вернулся к определению ресурса как вещи. Тем не менее, обратите внимание, что я пробрался в «функциональность поиска» в качестве ресурса. Функциональность поиска является одним из тех ресурсов, которые мне, как бэк-эндеру, небезразличны.

Ресурсом действительно может быть любая «вещь», но с моей точки зрения, как внутреннего разработчика RESTful API, я действительно беспокоюсь о «вещах» с данными в домене, а также о других функциях API.

Теперь, когда я определил, какие типы «вещей» меня интересуют (данные домена и другие функции API), что уникально для REST в том, как реализованы эти «вещи»? Гипермедиа, конечно. Все взаимодействие между клиентом и сервером должно определяться информацией гиперссылки, указанной сервером. Все навигационные отношения между набором открытых доменных объектов и функциональностью API должны быть представлены сервером в виде URL-адреса (или некоторого другого общепризнанного идентификатора). Исключением из этого правила является точка входа в систему, то есть закладка

Забей это

Давайте вернемся к примеру MC Hammer. Пользователь выполняет следующие шаги, чтобы «ПОЛУЧИТЬ» к ресурсу MC Hammer:

  1. ПОЛУЧИТЬ http://mymusic.com/
  2. GET <самый популярный список исполнителей url>
  3. GET <адрес первого исполнителя>

Этот трехэтапный процесс «привел» меня к месту назначения MC Hammer. На шаге 1 у меня была закладка, которая давала мне доступ по URI / API / рабочему процессу к наиболее популярным функциям, которые я вызывал на шаге 2. Шаг № 2 дал результат, который я искал, поэтому я перешел по этому URL-адресу до моего конечного пункта назначения.

Перспектива на стороне сервера: что такое ресурс?

Теперь мы знаем, как найти мистера Хаммера с точки зрения клиента. Как реализован этот трехступенчатый процесс? Ну, если мы используем JAX-RS, у нас есть три «ресурса»:

  1. Контроллер домашней страницы . Это может быть простой контроллер или даже файл XML. Это точка входа в систему. По ссылкам на домашней странице данные ссылаются на самый популярный URL.

    public class HomepageController{
    @GET
    @Produces({"application/xml", "application/json"})
    @Path("/")
    public Homepage getHomepage(){
    // homepage retrieval magic goes here
    }
    }
  2. Контроллер Artists , который имеет несколько методов , которые возвращают различные списки художников. Каким-то образом он должен конвертировать исполнителя в имя, биографию и URL.

    public class ArtistsController{

    private ArtistService service;

    @GET
    @Produces({"application/xml", "application/json"})
    @Path("/most-popular-artist")
    public Artists getMostPopular(){
    // do something with ArtistService
    // add on URL to each Artist object in the list
    // we'll fill this in later
    }
    }
  3. Исполнитель контроллер , который знает , как GET, POST, PUT, DELETE с marshallable объекта Исполнитель домена. Здесь нет ничего нового, вы, наверное, видели это довольно много раз в других блогах / статьях / книгах JAX-RS.

    public class ArtistController{

    private ArtistService service;

    @GET
    @Produces({"application/xml", "application/json"})
    @Path("/artist/{id: \\d+}")
    public Artist getArtist(@PathParam("id") Integer id){
    return service.getById(id);
    }

    @PUT
    @Consumes({"application/xml", "application/json"})
    @Path("/artist/{id: \\d+}")
    public void updateArtist(@PathParam("id") Integer id, Artist artist){
    service.save(artist);
    }

    ...
    }

The most interesting aspect of this article is how the URL conversion in the getMostPopular() method in step #2 works. Let’s take a look at how RESTEasy will do it in the next release.

Domain Data Declarative Hyperlinking

There are lots of articles on what REST should be, and lots of articles about how JAX-RS works, but I’ve found the Domain Object to URL conversion to be a «missing link» (pun intended). The JAX-RS committee recognizes this, and have proposed «Declarative Hyperlinking» as part of JAX-RS 2.0. I’ve been working on this concept for quite a while in RESTEasy, and I’d like to show you what I’ve done.

The end result

In order to fully understand the «hows» that I’m proposing, I need to first show you the end result first. The getMostPopular() method has to return XML (or JSon, or something else). It has to convert a list of the most popular Artist objects into a list of abridged information about which artists are popular, a bio, and a link. Here’s an example output:

<artists>
<artist url="/artists/23523">
<name>MC Hammer</name>
<bio></bio>
</artist>
<artist url="/artists/23992">
<name>Vanilla Ice</name>
<bio></bio>
</artist>
<artist url="/artists/21421">
<name>Michael Jackson</name>
<bio></bio>
</artist>
<artist url="/artists/41025">
<name>Britany Spears</name>
<bio></bio>
</artist>
....
</artists>

(you can pretend there’s a bio there… Or you can comment about what you think the bio should be)

Decorating the Artist

That seems straight forward enough of an output, so let’s take a closer look at a potential implementation using the new RESTEasy I’ve put into SVN. The most interesting thing here is going to be how we get that url to show up. Let’s say the Artist object had some information on it that described how it should be translated from an object to a URL:

...
@URITemplate("artist/{id}")
// this maps to "/artist/{id}" - JavaBeans conventions are used to convert
// '{id}' with the results of getId()
@XmlRootElement
public class Artist {
private Long id;
private String name;
private List<Albums> albus;

//getters and setters
}

Is it a Good Thing ™ to add URI elements to your objects? I’m not not sure since it probably breaks Separation of Concerns. However, it’s something that makes your live as a REST developer a bit easier.

Let’s see how we can use Artist in other settings.

Mini Artist DTO — Artist url

Since we only want a little bit of the Artist (for example, we want name, and we don’t want to the albums). We need a way to get only the data we want, including the URL. For that, I’m going to create a new DTO. Take a look at the Object to URL conversion magic:

@XmlElement(name="artist")
public class MiniArtist{
public String name;

@XmlJavaTypeAdapter(value = UriAdapter.class)
@Attribute
public Artist url;

public MiniArtist(Artist artist){
this.name = artist.first;
this.url = artist;
}
}

JAXB goodness

Here’s a good discussion on XmlAdapters, just in case you want to learn more. UriAdapter is half baked at the moment, so it’s not in the resteasy SVN. It’s just an example of how the new RESTEasy utilties can be used (I’ll be writing more on these utilities in future blog posts). Here’s what UriAdapter looks like:

public class UriAdapter extends XmlAdapter<String, Object> {
@Override
public String marshal(Object domainObject) throws Exception {
return ObjectToURI.getInstance().resolveURI(domainObject);
}

@Override
public Object unmarshal(String uri) throws Exception {
return ResteasyProviderFactory.getContextData(InternalDispatcher.class)
.getEntity(uri);
}
}

Conversion through Declarative Magic

ArtistController ends up being elegantly simple because all of the work is really done elsewhere:

 @GET
@Produces({"application/xml", "application/json"})
@Path("/most-popular-artist")
public Artists getMostPopular(){
Artists artists = new Artists();
for(Artist artist : service.getMostPopular()){
artists.add(new MiniArtist(artist))
}
return artists;
}

So what’s the big deal?

ArtistsController.getMostPopular() needs to somehow convert an Artist to a URL. Sure, we can use String manipulation to create the URL. IMHO, However, while the server it’s probably a bad practice to scatter that kind of logic across your code. While URLs usually don’t change once they’re live, you can still get into quite a bit of complicated logic in creating those URLs:

  • You allow multiple server hosts to expose your data, and you want the outgoing full URLs to match the incoming host. This can get a bit tricky when you have a layered system. CDNs, for example, will use a different host name than your server. They do give you enough information about the initial host in custom headers that you may want to reuse.
  • You want to add cross-cutting parameters to your URLs, for example a query parameter tha allows the user to specify format (for example «?format=xml|json» or «.xml|.json»). You’ll want to make sure that all subsequent URLs use that same content-negotiation URL strategy.
  • You decide to change the URL structure before production

For those reasons, you really need a two step conversion:

  1. object to url conversion
  2. cross-cutting concerns to url enhancements

For the former concern, it’s a good idea to declare just enough information about how to perform the object instance to URL translation in a location relative to the domain object, and let a centralized URLConverter take care of the «rest». Annotations are the du jour strategy for this kind of information, but RESTEasy does provide more mechanisms beyond annotations

Where do we go from here?

I hope that you have good questions and constructive criticism. I’d be more than glad to hear it. The topic of URL <-> object is a core REST concern that needs to be «addressed.» Tools need to be built and best practices need to be invented. There’s plenty of work left.

I showed you how the Object -> URL conversion works, but not URL -> Object. I’ll show you that in another blog post. I also need to give you more details about how to use the underlying RESTEasy utilities that I mentioned earlier. There are also plenty more RESTful goodies that I’ve worked that I’d like to talk about in the upcoming weeks.