Статьи

Swagger: заставьте разработчиков любить работать с вашим REST API

По мере развития API JAX-RS с версией 2.0, выпущенной ранее в этом году под эгидой JSR-339 , становится все проще создавать службы REST с использованием превосходной платформы Java .

Но с большой простотой возникает большая ответственность: документирование всех этих API, чтобы другие разработчики могли быстро понять, как их использовать. К сожалению, в этой области разработчики сами по себе: JSR-339 не сильно помогает. Конечно, было бы просто здорово генерировать подробную и простую в использовании документацию из исходного кода, не требуя, чтобы кто-то писал ее в процессе разработки. Звучит нереально, правда? В определенной степени, это действительно так, но помощь приходит в виде чванства .

По сути, Swagger делает простую, но очень мощную вещь: с помощью нескольких дополнительных аннотаций он генерирует описания API REST (методы HTTP , параметры пути / запроса / формы, ответы, коды ошибок HTTP и т. Д.) И даже предоставляет простой веб-интерфейс для поиграйте с вызовами REST ваших API (не говоря уже о том, что все эти метаданные также доступны через REST ).

Прежде чем углубляться в детали реализации, давайте кратко рассмотрим, что представляет собой Swagger с точки зрения потребителя API. Предположим, вы разработали отличный сервис REST для управления людьми. Как хороший гражданин, эта служба REST полностью функциональна и предоставляет следующие функциональные возможности:

  • списки всех людей ( GET )
  • ищет человека по электронной почте ( GET )
  • добавляет нового человека ( POST )
  • обновляет существующего человека ( PUT )
  • и, наконец, удаляет человека ( УДАЛИТЬ )

Вот тот же API с точки зрения Swagger :

чванство-Frontpage

Это выглядит довольно красиво. Давайте сделаем больше и назовем наш REST- сервис из Swagger UI , здесь эта замечательная среда действительно сияет. Самым сложным вариантом использования является добавление нового человека ( POST ), поэтому этот пример будет рассмотрен внимательно.

чванство-пост

Как вы можете видеть на снимке выше, каждая часть вызова службы REST находится там:

  • описание услуги
  • относительный контекстный путь
  • параметры (форма / путь / запрос), обязательные или необязательные
  • Коды состояния HTTP : 201 CREATED и 409 CONFLICT
  • готов к работе Попробуйте! немедленно вызвать службу REST (с проверкой параметров из коробки)

чванство-ответ

Для завершения демонстрационной части позвольте мне показать еще один пример, где задействован ресурс REST (в нашем случае это простой класс Person ). Swagger может предоставить свои свойства и содержательное описание вместе с ожидаемыми типами содержимого ответа.

чванство-прибудете

Выглядит хорошо! Переходя к следующей части, это все о деталях реализации. Swagger поддерживает плавную интеграцию со службами JAX-RS , для чего требуется всего несколько дополнительных аннотаций поверх существующих. Во-первых, каждый сервис JAX-RS, который должен быть задокументирован, должен быть аннотирован @Api , в нашем случае:

1
2
3
4
5
@Path( "/people" )
@Api( value = "/people", description = "Manage people" )
public class PeopleRestService {
    // ...
}

Далее, тот же подход применим к операциям службы REST : каждый метод, который должен быть задокументирован, должен быть аннотирован @ApiOperation аннотацией, и, необязательно, @ ApiResponses / @ ApiResponse . Если он принимает параметры, они должны быть помечены аннотацией @ApiParam . Пара примеров здесь:

01
02
03
04
05
06
07
08
09
10
11
12
13
@Produces( { MediaType.APPLICATION_JSON } )
@GET
@ApiOperation(
    value = "List all people",
    notes = "List all people using paging",
    response = Person.class,
    responseContainer = "List"
)
public Collection< Person > getPeople( 
        @ApiParam( value = "Page to fetch", required = true )
        @QueryParam( "page") @DefaultValue( "1" ) final int page ) {
    // ...
}

И еще один:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
@Produces( { MediaType.APPLICATION_JSON } )
@Path( "/{email}" )
@GET
@ApiOperation(
    value = "Find person by e-mail",
    notes = "Find person by e-mail",
    response = Person.class
)
@ApiResponses( {
    @ApiResponse( code = 404, message = "Person with such e-mail doesn't exists" )   
} )
public Person getPeople(
        @ApiParam( value = "E-Mail address to lookup for", required = true )
        @PathParam( "email" ) final String email ) {
    // ...
}

Классы ресурсов REST (или классы моделей) требуют специальных аннотаций: @ApiModel и @ApiModelProperty . Вот как выглядит наш класс Person :

01
02
03
04
05
06
07
08
09
10
11
@ApiModel( value = "Person", description = "Person resource representation" )
public class Person {
    @ApiModelProperty( value = "Person's first name", required = true )
    private String email;
    @ApiModelProperty( value = "Person's e-mail address", required = true )
    private String firstName;
    @ApiModelProperty( value = "Person's last name", required = true )
    private String lastName;
 
    // ...
}

Последние шаги – подключить Swagger к приложению JAX-RS . В примере, который я разработал, используются Spring Framework , Apache CXF , Swagger UI и встроенный Jetty (полный проект доступен на Github ). Интеграция Swagger заключается в добавлении bean-компонента конфигурации ( swaggerConfig ), одной дополнительной службы JAX-RS ( apiListingResourceJson ) и двух поставщиков JAX-RS ( resourceListingProvider и apiDeclarationProvider ).

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
package com.example.config;
 
import java.util.Arrays;
 
import javax.ws.rs.ext.RuntimeDelegate;
 
import org.apache.cxf.bus.spring.SpringBus;
import org.apache.cxf.endpoint.Server;
import org.apache.cxf.jaxrs.JAXRSServerFactoryBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.core.env.Environment;
 
import com.example.resource.Person;
import com.example.rs.JaxRsApiApplication;
import com.example.rs.PeopleRestService;
import com.example.services.PeopleService;
import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider;
import com.wordnik.swagger.jaxrs.config.BeanConfig;
import com.wordnik.swagger.jaxrs.listing.ApiDeclarationProvider;
import com.wordnik.swagger.jaxrs.listing.ApiListingResourceJSON;
import com.wordnik.swagger.jaxrs.listing.ResourceListingProvider;
 
@Configuration
public class AppConfig {
    public static final String SERVER_PORT = "server.port";
    public static final String SERVER_HOST = "server.host";
    public static final String CONTEXT_PATH = "context.path"
 
    @Bean( destroyMethod = "shutdown" )
    public SpringBus cxf() {
        return new SpringBus();
    }
 
    @Bean @DependsOn( "cxf" )
    public Server jaxRsServer() {
        JAXRSServerFactoryBean factory = RuntimeDelegate.getInstance().createEndpoint( jaxRsApiApplication(), JAXRSServerFactoryBean.class );
        factory.setServiceBeans( Arrays.< Object >asList( peopleRestService(), apiListingResourceJson() ) );
        factory.setAddress( factory.getAddress() );
        factory.setProviders( Arrays.< Object >asList( jsonProvider(), resourceListingProvider(), apiDeclarationProvider() ) );
        return factory.create();
    }
 
    @Bean @Autowired
    public BeanConfig swaggerConfig( Environment environment ) {
        final BeanConfig config = new BeanConfig();
 
        config.setVersion( "1.0.0" );
        config.setScan( true );
        config.setResourcePackage( Person.class.getPackage().getName() );
        config.setBasePath(
            String.format( "http://%s:%s/%s%s",
                environment.getProperty( SERVER_HOST ),
                environment.getProperty( SERVER_PORT ),
                environment.getProperty( CONTEXT_PATH ),
                jaxRsServer().getEndpoint().getEndpointInfo().getAddress()
            )
        );
 
        return config;
    }
 
    @Bean
    public ApiDeclarationProvider apiDeclarationProvider() {
        return new ApiDeclarationProvider();
    }
 
    @Bean
    public ApiListingResourceJSON apiListingResourceJson() {
        return new ApiListingResourceJSON();
    }
 
    @Bean
    public ResourceListingProvider resourceListingProvider() {
        return new ResourceListingProvider();
    }
 
    @Bean
    public JaxRsApiApplication jaxRsApiApplication() {
        return new JaxRsApiApplication();
    }
 
    @Bean
    public PeopleRestService peopleRestService() {
        return new PeopleRestService();
    }
 
    // ...
}

Чтобы избавиться от любой возможной жестко заданной конфигурации, все параметры передаются через именованные свойства ( SERVER_PORT , SERVER_HOST и CONTEXT_PATH ). Swagger предоставляет дополнительную конечную точку REST для предоставления документации по API, в нашем случае она доступна по адресу: http: // localhost: 8080 / rest / api / api-docs . Он используется Swagger UI, который сам встроен в окончательный JAR- архив и обслуживается Jetty в качестве статического веб-ресурса.

Последняя часть головоломки заключается в запуске встроенного контейнера Jetty, который склеивает все эти части вместе и инкапсулируется в класс Starter :

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package com.example;
 
import org.apache.cxf.transport.servlet.CXFServlet;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.HandlerList;
import org.eclipse.jetty.servlet.DefaultServlet;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.resource.Resource;
import org.springframework.core.io.ClassPathResource;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
 
import com.example.config.AppConfig;
 
public class Starter {
    private static final int SERVER_PORT = 8080;
    private static final String CONTEXT_PATH = "rest";
 
    public static void main( final String[] args ) throws Exception {
        Resource.setDefaultUseCaches( false );
 
        final Server server = new Server( SERVER_PORT ); 
        System.setProperty( AppConfig.SERVER_PORT, Integer.toString( SERVER_PORT ) );
        System.setProperty( AppConfig.SERVER_HOST, "localhost" );
        System.setProperty( AppConfig.CONTEXT_PATH, CONTEXT_PATH );   
 
        // Configuring Apache CXF servlet and Spring listener 
        final ServletHolder servletHolder = new ServletHolder( new CXFServlet() );     
        final ServletContextHandler context = new ServletContextHandler();  
        context.setContextPath( "/" );
        context.addServlet( servletHolder, "/" + CONTEXT_PATH + "/*" );    
        context.addEventListener( new ContextLoaderListener() );
 
        context.setInitParameter( "contextClass", AnnotationConfigWebApplicationContext.class.getName() );
        context.setInitParameter( "contextConfigLocation", AppConfig.class.getName() );
 
        // Configuring Swagger as static web resource
        final ServletHolder swaggerHolder = new ServletHolder( new DefaultServlet() );
        final ServletContextHandler swagger = new ServletContextHandler();
        swagger.setContextPath( "/swagger" );
        swagger.addServlet( swaggerHolder, "/*" );
        swagger.setResourceBase( new ClassPathResource( "/webapp" ).getURI().toString() );
 
        final HandlerList handlers = new HandlerList();
        handlers.addHandler( context );
        handlers.addHandler( swagger );
 
        server.setHandler( handlers );
        server.start();
        server.join();
    }
}

Несколько комментариев проясняют ситуацию : наши сервисы JAX-RS будут доступны в контекстном пути / rest / *, а интерфейс Swagger доступен в контекстном пути / swagger . Важное замечание, касающееся Resource.setDefaultUseCaches (false) : поскольку мы обслуживаем статический веб-контент из файла JAR , мы должны установить для этого свойства значение false в качестве обходного пути для этой ошибки .

Теперь давайте создадим и запустим наше приложение JAX-RS , набрав:

1
2
mvn clean package
java -jar target/jax-rs-2.0-swagger-0.0.1-SNAPSHOT.jar

Через секунду пользовательский интерфейс Swagger должен быть доступен в вашем браузере по адресу: http: // localhost: 8080 / swagger /

В завершение, о Swagger можно сказать гораздо больше, но я надеюсь, что этот простой пример показывает, как сделать наши REST- сервисы самодокументированными и легко потребляемыми с минимальными усилиями. Большое спасибо команде Wordnik за это.

  • Исходный код доступен на Github .