DOCUMENTING MULTIPLE REST API VERSIONS USING SPRING BOOT, JERSEY AND SWAGGER

A few days ago I was completing the accompanying source code for Microservices using Spring Boot, Jersey, Swagger and Docker blog entry and found some problems while adding documentation support to multiple implementation versions of the “Hello” JAX-RS resource endpoints where the version information was passed in the URL or Accept Header.

Recapping the details, I stated my belief that Swagger’s BeanConfig singleton instantiation approach wasn’t going to work. The reason I believe so is because a BeanConfig instance dynamically creates only one Swagger definition file and it would require one definition file for each version. Yes, there are some “hacks” to get away with it, like removing the endpoints implementation where the version is passed in the Accept Header and then only one Swagger definition file will include the documentation for every endpoint implementation version. I don’t think this is the right answer though.

This post and its accompanying source code will tackle on this issue and demonstrate how to generate multiple Swagger definition files to feed Swagger UI to display JAX-RS APIs documentation for multiple implementation versions.

REQUIREMENTS

  • Java 8 or Java 7. For Java 7, java.version property inside pom.xml needs to be updated accordingly.
  • Maven 3.3.x
  • Familiarity with Spring Framework.

I’ll follow same steps as described in Microservices using Spring Boot, Jersey, Swagger and Docker to create the initial project but would only proceed with the explanation of the new additions to this post.

CREATE THE HELLO WORLD SERVICE

curl "https://start.spring.io/starter.tgz" -d dependencies=actuator,jersey,web -d language=java -d type=maven-project -d baseDir=multiversion-api-jersey-swagger -d groupId=com.asimio.api -d artifactId=multiversion-api-jersey-swagger -d version=0-SNAPSHOT | tar -xzvf -

The accompanying source code has been refactored to match the packages as described in this post.

Add the entry point to the application via start-class property and Swagger dependency to pom.xml.

...
<properties>
  <start-class>com.asimio.swaggerexample.main.Application</start-class>
...
  <swagger.version>1.5.9</swagger.version>
...
</properties>
...
<dependency>
  <groupId>io.swagger</groupId>
  <artifactId>swagger-jersey2-jaxrs</artifactId>
  <version>${swagger.version}</version>
</dependency>
...

This is a snippet of the app’s entry point, that allows this application to be built and run as either a fat jar or as a war in a Servlet Container.

package com.asimio.swaggerexample.main;
...
@SpringBootApplication(
  scanBasePackages = {
    "ccom.asimio.swaggerexample.config", "com.asimio.swaggerexample.rest"
  }
)
public class Application extends SpringBootServletInitializer {

  @Override
  protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
    return builder.sources(Application.class);
  }

  public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
  }
}

IMPLEMENT MULTIPLE VERSIONS OF THE JAX-RS ENDPOINTS USING JERSEY

At this point spring-boot-starter-jersey and spring-boot-starter-web are already dependencies of the application, this is important because they need to be mapped to different paths, Jersey’s will provide access to the Resources endpoints while Spring MVC will provide access to Actuator endpoints, for more detailed information, please visit Microservices using Spring Boot, Jersey, Swagger and Docker

JAX-RS providers, endpoints, etc.. are registered in a subclass of Jersey’s ResourceConfig class.

package com.asimio.swaggerexample.config;
...
@Component
public class JerseyConfig extends ResourceConfig {

  public JerseyConfig() {
    // Register endpoints, providers, ...
    this.registerEndpoints();
  }

  private void registerEndpoints() {
    this.register(com.asimio.swaggerexample.rest.v1.HelloResourceV1.class);
    this.register(com.asimio.swaggerexample.rest.v2.HelloResourceV2.class);
    // Access through /<Jersey's servlet path>/application.wadl
    this.register(WadlResource.class);
  }
}

It could be noticed there is not reference to Swagger’s BeanConfig as it was in previous blog post.

Next comes the HelloWorldV1 (Version 1) resource implementation which include both JAX-RS and Swagger annotations:

 1 package com.asimio.swaggerexample.rest.v1;
 2 ...
 3 @Component
 4 @Path("/")
 5 @Consumes(MediaType.APPLICATION_JSON)
 6 @Produces(MediaType.APPLICATION_JSON)
 7 @Api(value = "Hello resource", produces = "application/json")
 8 public class HelloResourceV1 {
 9 
10   private static final Logger LOGGER = LoggerFactory.getLogger(HelloResourceV1.class);
11 
12   @GET
13   @Path("v1/hello/{name}")
14   @ApiOperation(value = "Gets a hello resource. Version 1 - (version in URL)", response = Hello.class)
15   @ApiResponses(value = {
16     @ApiResponse(code = 200, message = "Hello resource found"),
17     @ApiResponse(code = 404, message = "Hello resource not found")
18   })
19   public Response getHelloVersionInUrl(@ApiParam @PathParam("name") String name) {
20     LOGGER.info("getHelloVersionInUrl() v1");
21     return this.getHello(name, "Version 1 - passed in URL");
22   }
23 
24   @GET
25   @Path("hello/{name}")
26   @Consumes("application/vnd.asimio-v1+json")
27   @Produces("application/vnd.asimio-v1+json")
28   @ApiOperation(value = "Gets a hello resource. World Version 1 (version in Accept Header)", response = Hello.class)
29   @ApiResponses(value = {
30     @ApiResponse(code = 200, message = "Hello resource found"),
31     @ApiResponse(code = 404, message = "Hello resource not found")
32   })
33   public Response getHelloVersionInAcceptHeader(@PathParam("name") String name) {
34     LOGGER.info("getHelloVersionInAcceptHeader() v1");
35     return this.getHello(name, "Version 1 - passed in Accept Header");
36   }
37 
38   private Response getHello(String name, String partialMsg) {
39     if ("404".equals(name)) {
40       return Response.status(Status.NOT_FOUND).build();
41     }
42     Hello result = new Hello();
43     result.setMsg(String.format("Hello %s. %s", name, partialMsg));
44     return Response.status(Status.OK).entity(result).build();
45   }
46 ...
47 }

HelloWorldV2 (Version 2) resource implementation is similar and the source code could be found in accompanying repository at https://bitbucket.org/asimio/multiversion-api-jersey-swagger-example

Notice /hello/{name} endpoint is versioned through URL and Accept Header, what’s needed now is to generate Swagger definition files for both versions to documents the endpoints.

DOCUMENT THE REST APIs WITH SWAGGER

Swagger definition files are going to be generated at build time using a Maven plugin which would need to be added to pom.xml:

 1 ...
 2 <properties>
 3   <swagger-maven-plugin.version>3.1.3</swagger-maven-plugin.version>
 4 </properties>
 5 ...
 6 <plugin>
 7   <groupId>com.github.kongchen</groupId>
 8   <artifactId>swagger-maven-plugin</artifactId>
 9   <version>${swagger-maven-plugin.version}</version>
10   <configuration>
11     <apiSources>
12       <!-- Version 1 -->
13       <apiSource>
14         <springmvc>false</springmvc>
15         <locations>com.asimio.swaggerexample.rest.v1</locations>
16         <schemes>http,https</schemes>
17         <basePath>/api</basePath>
18         <info>
19           <title>Multiversion Spring Boot + Jersey + Swagger Demo (Version 1)</title>
20           <version>v1</version>
21           <description>A multi-version demo (version 1) of a RESTful service using Spring Boot, Jersey and Swagger.</description>
22           <termsOfService>http://www.github.com/kongchen/swagger-maven-plugin</termsOfService>
23           <contact>
24             <email>ootero@asimio.net</email>
25             <name>Orlando L Otero</name>
26             <url>http://tech.asimio.net</url>
27           </contact>
28           <license>
29             <url>http://www.apache.org/licenses/LICENSE-2.0.html</url>
30             <name>Apache 2.0</name>
31           </license>
32         </info>
33         <outputFormats>json</outputFormats>
34         <swaggerDirectory>${basedir}/target/classes/static/v1</swaggerDirectory>
35         <swaggerApiReader>com.github.kongchen.swagger.docgen.reader.JaxrsReader</swaggerApiReader>
36       </apiSource>
37       <!-- Version 2 -->
38 ...
39       </apiSources>
40     </configuration>
41     <executions>
42       <execution>
43         <phase>compile</phase>
44         <goals>
45           <goal>generate</goal>
46         </goals>
47       </execution>
48     </executions>
49     <dependencies>
50       <!-- Adding dependency to swagger-hibernate-validations to enable the BeanValidator as a custom model converter -->
51       <dependency>
52         <groupId>io.swagger</groupId>
53         <artifactId>swagger-hibernate-validations</artifactId>
54         <version>${swagger.version}</version>
55       </dependency>
56     </dependencies>
57   </plugin>
58 ...

I’ll add a brief explanation of the elements configured with this plugin just for Version 1 since Version 2 is similar.

Starting from line 12:

springmvc the type of API being documented, false for JAX-RS, true for Spring MVC
locations comma-separated list of packages containing classes with @Api Swagger annotation
schemes comma-separated list of protocols on which API are available
basePath the base path on which the API are available. Jersey’s servlet path in this case
info information about the API, nested elements will be used by Swagger UI
outputFormats format in which Swagger definition file will be generated, either json or yaml
swaggerDirectory directory where this plugin will generate the Swagger definition file


For more information about these configuration elements, please visit the plugin home page at https://github.com/kongchen/swagger-maven-plugin

Next download Swagger UI zip file from https://github.com/swagger-api/swagger-ui/releases, 2.1.4 was used for the example app bundled with this post.
Extract and move resulting content to src/main/resources/static.
Create directories v1 and v2 in src/main/resources/static and place a copy of previously extracted index.html in them, then remove the one found in static folder.

Update index.html located in both, static/v1 and static/v2 directories for Swagger UI to find Swagger definition files.
Replace:

url = "http://petstore.swagger.io/v2/swagger.json";

with:

url = "/v1/swagger.json";

in …/static/v1/index.html and:

url = "/v2/swagger.json";

in …/static/v2/index.html.

Since there aren’t any Swagger defintion file yet, the application would run but there wouldn’t be any documentation available, let’s generate them:

mvn clean package

swagger-maven-plugin is bounded to Maven compile phase, which will generate json definition files in target/classes/static/v1 and target/classes/static/v2 as configured in pom.xml and will be included in the resulting jar application along with their corresponding index.html.

RUNNING THE APPLICATION

java -jar target/multiversion-api-jersey-swagger.jar

Accessing the endpoints:

Create resource - Version 1 in Accept Header

$ curl -v -X POST -H 'Content-Type: application/vnd.asimio-v1+json' -d '{ "msg": "world"}' http://staging.asimio.net:8702/api/hello
*   Trying 73.24.84.41...
* Connected to staging.asimio.net (73.24.84.41) port 8702 (#0)
> POST /api/hello HTTP/1.1
> Host: staging.asimio.net:8702
> User-Agent: curl/7.43.0
> Accept: */*
> Content-Type: application/vnd.asimio-v1+json
> Content-Length: 17
>
* upload completely sent off: 17 out of 17 bytes
< HTTP/1.1 201 Created
< Server: Apache-Coyote/1.1
< X-Application-Context: application:8702
< Location: http://staging.asimio.net:8702/api/hello/world
< Content-Length: 0
< Date: Sat, 07 May 2016 04:40:11 GMT
<
* Connection #0 to host staging.asimio.net left intact

Create resource - Version 2 in Accept Header

$ curl -v -X POST -H 'Content-Type: application/vnd.asimio-v2+json' -d '{ "msg1": "world 1", "msg2": "world 2" }' http://staging.asimio.net:8702/api/hello
*   Trying 73.24.84.41...
* Connected to staging.asimio.net (73.24.84.41) port 8702 (#0)
> POST /api/hello HTTP/1.1
> Host: staging.asimio.net:8702
> User-Agent: curl/7.43.0
> Accept: */*
> Content-Type: application/vnd.asimio-v2+json
> Content-Length: 40
>
* upload completely sent off: 40 out of 40 bytes
< HTTP/1.1 201 Created
< Server: Apache-Coyote/1.1
< X-Application-Context: application:8702
< Location: http://staging.asimio.net:8702/api/hello/world%201--world%202
< Content-Length: 0
< Date: Sat, 07 May 2016 04:42:59 GMT
<
* Connection #0 to host staging.asimio.net left intact

Some screenshots taken from running the application:

Versions available Version available

Hello Resource - Version 1 Create resource - Version in Accept Header

Hello Resource - Version 2 Create resource - Version in Accept Header

And that’s all for now.

SOURCE CODE

Accompanying source code for this blog post can be found at:

REFERENCES