Статьи

Автоматическое создание WADL в приложении Spring MVC REST

В прошлый раз мы изучили основы WADL . Сам язык не так интересен, чтобы написать отдельную статью о нем, но название этой статьи показывает, почему нам нужны эти знания.

Многие реализации JSR 311: JAX-RS: API Java для веб-сервисов RESTful обеспечивает готовую генерацию WADL во время выполнения: Apache CXF , Jersey и Restlet . RESTeasy все еще ждет. По сути, эти платформы исследуют код Java с аннотациями JSR-311 и генерируют документ WADL, доступный по некоторым URL-адресам. К сожалению, Spring MVC не только не реализует стандарт JSR-311 (см .: Поддерживает ли Spring MVC аннотации JSR 311? ), Но также не генерирует для нас WADL (см. SPR-8705 ), даже если он идеально подходит для предоставление услуг REST.

По разным причинам я начал разрабатывать REST-сервисы на стороне сервера с помощью Spring MVC, и через некоторое время (скажем, через треть ресурсов) я немного заблудился. Мне действительно нужен был способ каталогизировать и документировать все доступные ресурсы и операции. WADL показался мне отличным выбором.

К счастью, среда Spring открыта для расширения, и в нее легко добавить новые функции, основанные на существующей инфраструктуре, если вы хотите некоторое время копаться в коде. Для генерации WADL мне понадобился список URI, которые обрабатывает приложение, какие методы HTTP реализованы и — в идеале — какой метод Java обрабатывает каждый из них. Очевидно, что Spring выполняет эту работу уже где-то во время загрузки загрузчика MVC DispatcherServlet — сканирование на наличие @Controller , @RequestMapping , @PathVariable и т. Д. — поэтому кажется разумным повторно использовать эту информацию, а не выполнять ее снова.

Угадайте, что, похоже, вся необходимая информация хранится в странно названном классе RequestMappingHandlerMapping . Вот снимок экрана отладчика, чтобы дать вам представление о том, насколько доступна информация:

Но это становится еще лучше: RequestMappingHandlerMapping на самом деле является бином Spring, который вы можете легко внедрить и использовать:

1
2
3
4
5
6
7
@Controller
class WadlController @Autowired()(mapping: RequestMappingHandlerMapping) {
  
    @RequestMapping(method = Array(GET))
    @ResponseBody def generate(request: HttpServletRequest) = new WadlApplication()
  
}

Правильно, мы будем использовать еще один Spring MVC-контроллер для генерации WADL-документа. В прошлый раз нам удалось сгенерировать классы JAXB, представляющие документ WADL (ведь WADL — это файл XML), поэтому, возвращая пустой экземпляр WadlApplication, мы фактически возвращаем пустой, но действительный WADL:

1
2
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<application xmlns="http://wadl.dev.java.net/2009/02"/>

Я не буду объяснять детали реализации ( полный исходный код доступен, включая пример приложения). Это был вопрос переписывания моделей Spring в классы WADL. Если вам интересно, взгляните на WadlGenerator.scala, который является центральной точкой решения и контрольных примеров . Вот один из них:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
test("should add parameter info for template parameter in URL") {
    given("")
    val mapping = Map(
        mappingInfo("/books", GET) -> handlerMethod("listBooks"),
        mappingInfo("/books/{bookId}", GET) -> handlerMethod("readBook")
    )
  
    when("")
    val wadl = generate(mapping)
  
    then("")
    assertXMLEqual(wadlHeader + """
        <resource path="books">
            <method name="GET">
                <doc title="com.blogspot.nurkiewicz.springwadl.TestController.listBooks"/>
            </method>
            <resource path="{bookId}">
                <param name="bookId" required="true" />
                <method name="GET">
                    <doc title="com.blogspot.nurkiewicz.springwadl.TestController.readBook"/>
                </method>
            </resource>
        </resource>
    """ + wadlFooter, wadl)
}

К сожалению, я был слишком ленив, чтобы правильно назвать данные / когда / затем блоки. Но тесты должны быть довольно читабельными.

Единственная техническая трудность, о которой я хотел бы упомянуть, — это преобразование шаблонов плоских URI, предоставляемых инфраструктурой Spring, в иерархические объекты WADL (в основном, в дерево). Вот упрощенная версия этой проблемы: список шаблонов URI выглядит следующим образом:

1
2
3
4
5
6
7
8
9
/books
/books/{bookId}
/books/{bookId}/reviews
/books/best-sellers
/readers
/readers/{readerId}
/readers/{readerId}/account/new-password
/readers/active
/readers/passive

Создайте следующую древовидную структуру данных:

Конечно, структура данных так же проста, как объект Node, содержащий метку и список дочерних узлов. Не очень сложный, но, вероятно, интересный CodeKata .

Так что же это такое с этим WADL? Действительно ли XML более читабелен и помогает в управлении REST-приложениями? Я бы даже не потрудился поиграть с ним, если бы не отличная поддержка soapUI для WADL . WADL, сгенерированный для примера приложения, которое я также добавил, может быть легко импортирован в soapUI:

Стоит упомянуть две особенности. Прежде всего soapUI отображает дерево ресурсов REST (в отличие от простого списка операций при импорте WSDL). Рядом с каждым методом HTTP есть соответствующий метод Java, который его обрабатывает (это можно отключить) для устранения неполадок и отладки. Во-вторых, мы можем выбрать любой метод / ресурс HTTP и вызвать его. На основе описания WADL soapUI создаст удобный для пользователя мастер, в который можно вводить параметры. Значения по умолчанию заполняются автоматически. Когда мы закончим, приложение сгенерирует HTTP-запрос с правильными URL-адресом и содержимым, отображая ответ при его получении. Действительно полезно!

Кстати, вы заметили параметры запроса max и page? Наша небольшая библиотека использует отражение, чтобы найти аннотации @RequestParam, например, следующий контроллер:

01
02
03
04
05
06
07
08
09
10
11
12
13
@Controller
@RequestMapping(value = Array("/book/{bookId}/review"))
class ReviewController @Autowired()(reviewService: ReviewService) {
  
    @RequestMapping(method = Array(GET))
    @ResponseBody def listReviews(
            @RequestParam(value = "page", required = false, defaultValue = "1") page: Int,
            @RequestParam(value = "max", required = false, defaultValue = "20") max: Int) =
        new ResultPage(reviewService.listReviews(new PageRequest(page - 1, max)))
  
    //...
  
}

будет переведено в WADL-совместимое описание:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<application xmlns="http://wadl.dev.java.net/2009/02">
  <doc title="Spring MVC REST appllication"/>
  <resources base="http://localhost:8080/api">
    <resource path="book">
      <!-- -->
      <resource path="{bookId}">
        <param required="true" style="template" name="bookId"/>
        <!-- -->
        <resource path="review">
          <method name="GET">
            <doc title="com.blogspot.nurkiewicz.web.ReviewController.listReviews"/>
            <request>
              <param required="false" default="1" style="query" name="page"/>
              <param required="false" default="20" style="query" name="max"/>
            </request>
        </resource>
      </resource>
    </resource>
  </resource
</application>

Надеюсь, вам было весело с этой маленькой библиотекой, которую я написал. Не стесняйтесь включать его в свой проект и не стесняйтесь сообщать об ошибках. Полный исходный код под лицензией Apache доступен на GitHub: https://github.com/nurkiewicz/spring-rest-wadl.

Справка: Автоматическая генерация WADL в Spring MVC REST приложении от нашего партнера JCG