Automating Service Provisioning Series

Subscribe to my newsletter to receive updates when content like this is published.

  1. Implementing APIs using Spring Boot, CXF and Swagger (you are here)
  2. Implementing a custom Spring Boot starter for CXF and Swagger
  3. Implementing a custom Maven Archetype to generate Spring Boot-based services
  4. Automating service provisioning and CI/CD using AWS Pipeline, Bitbucket and Terraform (work in progress)

1. OVERVIEW

A while back I published a blog post about Microservices using Spring Boot, Jersey, Swagger and Docker that takes advantage of the Spring ecosystem and a JAX-RS implementation in Jersey 2. In another post, Services Registration and Discovery using Eureka, Ribbon and Feign I also mentioned that Jersey 2-based endpoints attempting to register with Spring Cloud Netflix Eureka registry failed due to the fact that Eureka and Ribbon clients bring in Jersey 1 dependencies and thus conflicting with Jersey 2’s.

Instead some endpoints were implemented using Jersey 1 successfully registering with Eureka but the downside was it required configuring Jersey 1 in a multi-module Maven setup because there is no Spring Boot starter for Jersey 1 and a limitation to scan nested jars.

In this follow-up post I plan to demonstrate how to integrate Apache CXF 3.1.x JAX-RS-based endpoints implementation with Spring Boot and documenting them using Swagger. The services following this setup should be able to register with Spring Cloud Netflix Eureka since no Jersey dependency would be transitively included.

2. REQUIREMENTS

  • Java 7+.
  • Maven 3.2+.
  • Familiarity with Spring Framework.

3. CREATE THE DEMO SERVICE

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

This command will create a Maven project in a folder named springboot-cxf-swagger with the actuator and web-related Spring Boot dependencies. Read on if you are interested in adding Spring Boot support using the BOM approach.

This is a snippet of the application’s start class as defined in pom.xml:

package com.asimio.cxfdemo.main;

@SpringBootApplication(scanBasePackages = { "com.asimio.cxfdemo.rest" })
public class DemoCxfApplication {
...
}

@SpringBootApplication, also explained here, scans for @Component and derived annotations in classes found in the specified packages to build and wire dependencies.

4. IMPLEMENT AND DOCUMENT API ENDPOINTS USING CXF AND SWAGGER

Let’s first add Apache CXF and Swagger dependencies:

...
<properties>
  <cxf.version>3.1.11</cxf.version>
  <swagger-ui.version>2.2.10-1</swagger-ui.version>
</properties>
...
<dependency>
  <groupId>org.apache.cxf</groupId>
  <artifactId>cxf-spring-boot-starter-jaxrs</artifactId>
  <version>${cxf.version}</version>
</dependency>
<dependency>
  <groupId>org.apache.cxf</groupId>
  <artifactId>cxf-rt-rs-service-description</artifactId>
  <version>${cxf.version}</version>
</dependency>
<dependency>
  <groupId>org.apache.cxf</groupId>
  <artifactId>cxf-rt-rs-service-description-swagger</artifactId>
  <version>${cxf.version}</version>
</dependency>
<dependency>
  <groupId>org.webjars</groupId>
  <artifactId>swagger-ui</artifactId>
  <version>${swagger-ui.version}</version>
</dependency>
<dependency>
  <groupId>com.fasterxml.jackson.jaxrs</groupId>
  <artifactId>jackson-jaxrs-json-provider</artifactId>
</dependency>
...

cxf-spring-boot-starter-jaxrs is a CXF Spring Boot starter that autoconfigures the CXF servlet based on configuration properties found in a properties file such as application.yml.

cxf-rt-rs-service-description allows the WADL to be auto-generated at runtime.

cxf-rt-rs-service-description-swagger allows the generation Swagger 2.0 documents from JAX-RS endpoints.

swagger-ui includes the Swagger html and js files via a Maven dependency instead of downloading a zip file, extracting it to a certain folder and manually update settings in a file like I previously did.

Let’s include a snippet of the HelloResource.java interface, this is where the JAX-RS and Swagger annotations are configured:

package com.asimio.cxfdemo.rest.v1;
...
@Path("/")
@Api(value = "Hello resource Version 1", consumes = MediaType.APPLICATION_JSON, produces = MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public interface HelloResource {

  @GET
  @Path("v1/hello/{name}")
  @ApiOperation(value = "Gets a hello resource. Version 1 - (version in URL)")
  @ApiResponses(value = {
    @ApiResponse(code = 200, message = "Hello resource found", response = Hello.class),
    @ApiResponse(code = 404, message = "Hello resource not found")
  })
  Response getHelloVersionInUrl(@PathParam("name") @ApiParam(value = "The name") String name);

  @GET
  @Path("hello/{name}")
  @Consumes("application/vnd.asimio-v1+json")
  @Produces("application/vnd.asimio-v1+json")
  @ApiOperation(value = "Gets a hello resource. Version 1 - (version in Accept Header)")
  @ApiResponses(value = {
    @ApiResponse(code = 200, message = "Hello resource found", response = Hello.class),
    @ApiResponse(code = 404, message = "Hello resource not found")
  })
  Response getHelloVersionInAcceptHeader(@PathParam("name") String name);
}

@ApiXXXXXX annotations are Swagger-specific and the remaining are JAX-RS’s.

Also notice the @Path, @Consumes and @Produces annotations, their purpose is to pass the API version either in the URL or in the Accept header.

A snippet of the HelloResourceImpl.java interface implementation follows:

package com.asimio.cxfdemo.rest.v1.impl;
...
// No JAX-RS annotation in class, method or method arguments
@Component("helloResourceV1")
public class HelloResourceImpl implements HelloResource {
...
  @Override
  public Response getHelloVersionInUrl(String name) {
    LOGGER.info("getHelloVersionInUrl() v1");
    return this.getHello(name, "Version 1 - passed in URL");
  }

  @Override
  public Response getHelloVersionInAcceptHeader(String name) {
    LOGGER.info("getHelloVersionInAcceptHeader() v1");
    return this.getHello(name, "Version 1 - passed in Accept Header");
  }

  private Response getHello(String name, String partialMsg) {
    if ("404".equals(name)) {
      return Response.status(Status.NOT_FOUND).build();
    }
    Hello result = new Hello();
    result.setMsg(String.format("Hello %s. %s", name, partialMsg));
    return Response.status(Status.OK).entity(result).build();
  }
}

A simple implementation worth mentioning the class should not include any JAX-RS annotation.

CXF includes the concept of Feature and according to the documentation, a Feature is used to customize a Server, Client, or Bus, normally adding capabilities.

CXF’s Swagger2Feature is used to wrap Swagger’s BeanConfig in a CXF Feature via a Spring bean to dynamically generate the Swagger definition file used to feed the Swagger UI. Here is a snippet of such configuration:

package com.asimio.cxfdemo.rest.config;
...
@Configuration
public class FeaturesConfig {

  @Value("${cxf.path}")
  private String basePath;

  @Bean("swagger2Feature")
  public Feature swagger2Feature() {
    Swagger2Feature result = new Swagger2Feature();
    result.setTitle("Spring Boot + CXF + Swagger Example");
    result.setDescription("Spring Boot + CXF + Swagger Example description");
    result.setBasePath(this.basePath);
    result.setVersion("v1");
    result.setContact("Orlando L Otero");
    result.setSchemes(new String[] { "http", "https" });
    result.setPrettyPrint(true);
    return result;
  }
}

It was mentioned earlier a JacksonJsonProvider bean is needed to fix Java to JSON conversion issues:

package com.asimio.cxfdemo.rest.config;
...
@Configuration
public class ProvidersConfig {

  @Bean
  public JacksonJsonProvider jsonProvider() {
    return new JacksonJsonProvider();
  }
}

And lastly the CXF-related configuration settings used by the CXF Spring Boot starter to autoconfigure the CXF servlet:

application.yml:

1
2
3
4
5
6
7
8
9
10
...
# Spring MVC dispatcher servlet path. Needs to be different than CXF's to enable/disable Actuator endpoints access (/info, /health, ...)
server.servlet-path: /

# http://cxf.apache.org/docs/springboot.html#SpringBoot-SpringBootCXFJAX-RSStarter
cxf:
  path: /api # CXFServlet URL pattern
  jaxrs:
    component-scan: true
...

More settings could be found at http://cxf.apache.org/docs/springboot.html#SpringBoot-SpringBootCXFJAX-RSStarter

5. PACKAGING AND RUNNING THE SERVICE

This application can be run from your preferred IDE as a regular Java application or from command line:

cd <path to demo application>
mvn clean package
java -jar target/springboot-cxf-swagger.jar

or

mvn spring-boot:run

Let’s send some requests to these endpoints:

Description Endpoint
info actuator /info
WADL /api/?_wadl
Hello API version in URL (GET method) /api/v1/hello/<name>
Hello API version in Access header (GET method) /api/hello/<name>
Swagger JSON doc /api/swagger.json


info actuator endpoint

curl -v "http://localhost:8080/info"
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /info HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.51.0
> Accept: */*
>
< HTTP/1.1 200
< X-Application-Context: application
< Content-Type: application/vnd.spring-boot.actuator.v1+json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Mon, 12 Jun 2017 04:37:15 GMT
<
* Curl_http_done: called premature == 0
* Connection #0 to host localhost left intact
{"app":{"name":"springboot-cxf-swagger"},"build":{"version":"0-SNAPSHOT"}}

Resources WADL

curl -v "http://localhost:8080/api/?_wadl"
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /api/?_wadl HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.51.0
> Accept: */*
>
< HTTP/1.1 200
< X-Application-Context: application
< Date: Mon, 12 Jun 2017 04:39:52 GMT
< Content-Type: application/xml
< Content-Length: 1163
<
* Curl_http_done: called premature == 0
* Connection #0 to host localhost left intact
<?xml version="1.0" encoding="UTF-8"?>
<application xmlns="http://wadl.dev.java.net/2009/02" xmlns:xs="http://www.w3.org/2001/XMLSchema">
   <grammars />
   <resources base="http://localhost:8080/api/">
      <resource path="/">
         <resource path="hello/{name}">
            <param name="name" style="template" type="xs:string" />
            <method name="GET">
               <request />
               <response>
                  <representation mediaType="application/vnd.asimio-v1+json" />
               </response>
            </method>
         </resource>
         <resource path="v1/hello/{name}">
            <param name="name" style="template" type="xs:string" />
            <method name="GET">
               <request />
               <response>
                  <representation mediaType="application/json" />
               </response>
            </method>
         </resource>
      </resource>
      <resource path="/swagger.{type:json|yaml}">
         <param name="type" style="template" type="xs:string" />
         <method name="GET">
            <request />
            <response>
               <representation mediaType="application/json" />
               <representation mediaType="application/yaml" />
            </response>
         </method>
      </resource>
      <resource path="/api-docs">
         <resource path="/{resource:.*}">
            <param name="resource" style="template" type="xs:string" />
            <method name="GET">
               <request />
               <response>
                  <representation mediaType="*/*" />
               </response>
            </method>
         </resource>
      </resource>
   </resources>
</application>

Get Hello resource - Version in URL

curl -v "http://localhost:8080/api/v1/hello/world"
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /api/v1/hello/world HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.51.0
> Accept: */*
>
< HTTP/1.1 200
< X-Application-Context: application
< Date: Mon, 12 Jun 2017 04:43:10 GMT
< Content-Type: application/json
< Transfer-Encoding: chunked
<
* Curl_http_done: called premature == 0
* Connection #0 to host localhost left intact
{"msg":"Hello world. Version 1 - passed in URL"}

Get Hello resource - Version in Access header

curl -v -H "Accept: application/vnd.asimio-v1+json" "http://localhost:8080/api/hello/world"
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /api/hello/world HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.51.0
> Accept: application/vnd.asimio-v1+json
>
< HTTP/1.1 200
< X-Application-Context: application
< Date: Mon, 12 Jun 2017 04:46:10 GMT
< Content-Type: application/vnd.asimio-v1+json
< Transfer-Encoding: chunked
<
* Curl_http_done: called premature == 0
* Connection #0 to host localhost left intact
{"msg":"Hello world. Version 1 - passed in Accept Header"}

Swagger JSON document

curl "http://localhost:8080/api/swagger.json"
{
  "swagger" : "2.0",
  "info" : {
    "description" : "Spring Boot + CXF + Swagger Example description",
    "version" : "v1",
    "title" : "Spring Boot + CXF + Swagger Example",
    "contact" : {
      "name" : "Orlando L Otero"
    },
    "license" : {
      "name" : "Apache 2.0 License",
      "url" : "http://www.apache.org/licenses/LICENSE-2.0.html"
    }
  },
  "basePath" : "/api",
  "tags" : [ {
    "name" : "Hello resource Version 1"
  } ],
  "schemes" : [ "http", "https" ],
  "paths" : {
    "/v1/hello/{name}" : {
      "get" : {
        "tags" : [ "Hello resource Version 1" ],
        "summary" : "Gets a hello resource. Version 1 - (version in URL)",
        "description" : "",
        "operationId" : "getHelloVersionInUrl",
        "consumes" : [ "application/json" ],
        "produces" : [ "application/json" ],
        "parameters" : [ {
          "name" : "name",
          "in" : "path",
          "description" : "The name",
          "required" : true,
          "type" : "string"
        } ],
        "responses" : {
          "200" : {
            "description" : "Hello resource found",
            "schema" : {
              "$ref" : "#/definitions/Hello"
            }
          },
          "404" : {
            "description" : "Hello resource not found"
          }
        }
      }
    },
    "/hello/{name}" : {
      "get" : {
        "tags" : [ "Hello resource Version 1" ],
        "summary" : "Gets a hello resource. Version 1 - (version in Accept Header)",
        "description" : "",
        "operationId" : "getHelloVersionInAcceptHeader",
        "consumes" : [ "application/vnd.asimio-v1+json" ],
        "produces" : [ "application/vnd.asimio-v1+json" ],
        "parameters" : [ {
          "name" : "name",
          "in" : "path",
          "required" : true,
          "type" : "string"
        } ],
        "responses" : {
          "200" : {
            "description" : "Hello resource found",
            "schema" : {
              "$ref" : "#/definitions/Hello"
            }
          },
          "404" : {
            "description" : "Hello resource not found"
          }
        }
      }
    }
  },
  "definitions" : {
    "Hello" : {
      "type" : "object",
      "properties" : {
        "msg" : {
          "type" : "string"
        }
      }
    }
  }
}

Some screenshots of the Swagger UI, available at /api/api-docs?url=/api/swagger.json:

Hello Resource endpoints Swagger, Spring Boot, CXF - Hello Resource endpoints Swagger, Spring Boot, CXF - Hello Resource endpoints

GET Hello resource - Version in Accept header Swagger, Spring Boot, CXF - GET Hello resource - Version in Accept header Swagger, Spring Boot, CXF - GET Hello resource - Version in Accept header

If you are curious about how to add support for multi-versioned API generating multiple Swagger definition files, read on Documenting multiple REST API versions using Spring Boot Jersey and Swagger. I have also included a multi-versioned documented application using CXF and Swagger in the source code section.
Also interesting is my blog post Implementing a custom Spring Boot starter for CXF and Swagger.

And that’s all for now.

Thanks for reading and as always, feedback is very much appreciated. If you found this post helpful and would like to receive updates when content like this gets published, sign up to the newsletter.

6. SOURCE CODE

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

7. REFERENCES