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
  2. Implementing a custom Spring Boot starter for CXF and Swagger (you are here)
  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

Consider this scenario, the development team has increased over the last few years, software releases and deployment takes more time, that quick fix or feature couldn’t be delivered this Sprint because the deliverable now includes many changes that haven’t been signed off by QA and it might take another month to do so.

This seems like a good opportunity to break up the big Java monolithic application into multiple services or for that legacy set of applications in which business logic is duplicated to be refactored or rewritten, each by a small team benefiting from more frequent releases, easier to understand a small piece of functionality, having a low impact on other services, promoting ownership and accountability, etc..

Assuming for instance, it has been decided to implement the APIs using Spring Boot, Apache CXF and Swagger along with logging, metrics, security, etc. and maybe packaging the service in a Docker image, how do you setup this up for a dozen or more services? Do you create a baseline project to be copied and pasted for each service? Do you create a custom Maven archetype? What are other options?

This post covers how to create a custom Spring Boot starter for Apache CXF 3.1.x and Swagger 2 as the starting point to create services with a common set of dependencies and functionality also providing Spring beans auto-configuration to reduce explicit beans definition.

We are going to create two projects, asimio-api-springboot-starter, the custom Spring Boot starter which will auto-configure CXF and Swagger-related beans and asimio-api-starter-demo, a demo using asimio-api-springboot-starter.

2. REQUIREMENTS

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

3. CREATE asimio-api-springboot-starter, A SPRING BOOT STARTER FOR CXF AND SWAGGER

asimio-api-springboot-starter will be setup as a multi-module Maven project where the modules artifactIds follow Spring’s suggested naming convention:

3.1. CREATE asimio-api-springboot-starter-parent PARENT POM MAVEN MODULE

A regular parent pom.xml defining its modules and dependencies management to ease artifacts upgrade.

...
<modules>
  <module>../asimio-cxf-swagger-springboot-autoconfigure</module>
  <module>../asimio-cxf-swagger-springboot-starter</module>
</modules>

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-dependencies</artifactId>
      <version>${spring-boot.version}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
    <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>
  </dependencies>
</dependencyManagement>
...

3.2. CREATE asimio-cxf-swagger-springboot-autoconfigure MAVEN MODULE

This Maven child module is the main artifact of the asimio-api-springboot-starter Spring Boot starter, where all the beans are defined and auto-configured. Here is a snippet of its pom.xml:

...
<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-autoconfigure</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
  </dependency>
...
  <!-- SpringBoot + CXF + Swagger -->
  <dependency>
    <groupId>org.apache.cxf</groupId>
    <artifactId>cxf-spring-boot-starter-jaxrs</artifactId>
    <optional>true</optional>
  </dependency>
  <dependency>
    <groupId>org.apache.cxf</groupId>
    <artifactId>cxf-rt-rs-service-description</artifactId>
    <optional>true</optional>
  </dependency>
  <dependency>
    <groupId>org.apache.cxf</groupId>
    <artifactId>cxf-rt-rs-service-description-swagger</artifactId>
    <optional>true</optional>
  </dependency>
  <dependency>
    <groupId>org.webjars</groupId>
    <artifactId>swagger-ui</artifactId>
    <optional>true</optional>
  </dependency>		
  <dependency>
    <groupId>com.fasterxml.jackson.jaxrs</groupId>
    <artifactId>jackson-jaxrs-json-provider</artifactId>
    <optional>true</optional>
  </dependency>
...

Most of the dependencies are optional because they will only get included when asimio-cxf-swagger-springboot-starter is added to a project, for example, asimio-api-starter-demo in this blog post.

Next comes AsimioApiAutoConfiguration.java, the meat and potatoes of this module, where the beans are defined and auto-configured depending on some auto-configuration criteria, more on this later.

AsimioApiAutoConfiguration.java:

package com.asimio.api.springboot.autoconfigure;
...
@Configuration
@ConditionalOnClass({ Swagger2Feature.class, JacksonJsonProvider.class })
@EnableConfigurationProperties(AsimioApiProperties.class)
public class AsimioApiAutoConfiguration {
...
  @Autowired
  private AsimioApiProperties properties;

  @Bean
  @ConditionalOnMissingBean
  public Swagger2Feature swagger2Feature() {
    SwaggerDocs docs = this.properties.getDocs();
    if (StringUtils.isAnyBlank(docs.getTitle(), docs.getDescription(), docs.getBasePath(), docs.getVersion(), docs.getContact())) {

      LOGGER.error(API_PROPERTIES_EXCEPTION_MSG);
      throw new AsimioApiException(API_PROPERTIES_EXCEPTION_MSG);
    }
    Swagger2Feature result = new Swagger2Feature();
    result.setTitle(docs.getTitle());
    result.setDescription(docs.getDescription());
    result.setBasePath(docs.getBasePath());
    result.setVersion(docs.getVersion());
    result.setContact(docs.getContact());
    result.setSchemes(new String[] { "http", "https" });
    result.setPrettyPrint(Boolean.valueOf(docs.getPrettyPrint()));
    return result;
  }

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

As explained in Microservices using Spring Boot, Jersey, Swagger and Docker, @Configuration annotation marks this class will provide beans managed by the Spring container, swagger2Feature and jsonProvider in this case.

@EnableConfigurationProperties enables support for @ConfigurationProperties-annotated beans, AsimioApiProperties.java in this case, allowing an instance of AsimioApiProperties to be injected in other beans.

@ConditionalOnClass, @ConditionalOnMissingBean and other @Conditional annotations are used to constrain when the auto-configuration should be applied. In the case of AsimioApiAutoConfiguration.java, swagger2Feature and jsonProvider beans will be auto-configured if Swagger2Feature.class and JacksonJsonProvider.class are found in the classpath (remember the artifacts where these classes reside were included as optional) and the consumer of this starter didn’t explicitly define such beans.

Frequently used conditional annotations:

Condition Matches when the annotation args
ConditionalOnClass Classes are in the classpath.
ConditionalOnMissingClass Classes are not in the classpath.
ConditionalOnBean Bean classes and/or names are included contained in the bean factory.
ConditionalOnMissingBean Bean classes and/or names are not included contained in the bean factory.
ConditionalOnProperty Properties have a specific value or the property is not in the environment.
ConditionalOnExpression SpEL evaluates to true.


Then AsimioApiProperties.java, which allows setting custom properties in files like application.yml, application.properties, etc.. A usage example is included in asimio-api-starter-demo:

AsimioApiProperties.java:

package com.asimio.api.springboot.autoconfigure;
...
@ConfigurationProperties(prefix = "asimio-api")
public class AsimioApiProperties {
...
  @NestedConfigurationProperty
  private SwaggerDocs docs = new SwaggerDocs();

  @PostConstruct
  public void init() {
    // Safe defaults
    if (this.docs.prettyPrint == null) {
      this.docs.prettyPrint = "true";
    }
  }

  // Getters, Setters

  public static class SwaggerDocs {

    @Value("${cxf.path:/}")
    private String basePath;
    private String title;
    private String description;
    private String version;
    private String contact;
    private String prettyPrint;

    // Getters, Setters
  }
}

The last part of this Maven module is to enable auto-configuration of the Spring context, this is done in resources/META-INF/spring.factories:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.asimio.api.springboot.autoconfigure.AsimioApiAutoConfiguration

3.3. CREATE asimio-cxf-swagger-springboot-starter MAVEN MODULE

This is an extract of the third Maven child module’s pom.xml:

...
<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
    <exclusions>
      <exclusion>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-logging</artifactId>
      </exclusion>
    </exclusions>
  </dependency>
  <dependency>
    <groupId>com.asimio.api.starter</groupId>
    <artifactId>asimio-cxf-swagger-springboot-autoconfigure</artifactId>
    <version>${project.version}</version>
  </dependency>

  <!-- asimio api dependencies to auto-configure -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
    <exclusions>
      <exclusion>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-logging</artifactId>
      </exclusion>
    </exclusions>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-log4j2</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
    <groupId>org.apache.cxf</groupId>
    <artifactId>cxf-spring-boot-starter-jaxrs</artifactId>
  </dependency>
  <dependency>
    <groupId>org.apache.cxf</groupId>
    <artifactId>cxf-rt-rs-service-description</artifactId>
  </dependency>
  <dependency>
    <groupId>org.apache.cxf</groupId>
    <artifactId>cxf-rt-rs-service-description-swagger</artifactId>
  </dependency>
  <dependency>
    <groupId>org.webjars</groupId>
    <artifactId>swagger-ui</artifactId>
  </dependency>		
  <dependency>
    <groupId>com.fasterxml.jackson.jaxrs</groupId>
    <artifactId>jackson-jaxrs-json-provider</artifactId>
  </dependency>		
</dependencies>
...

It’s including the dependencies this starter provides, artifacts that were included in asimio-cxf-swagger-springboot-autoconfigure but now they are not optional, meaning whoever uses this starter will have these artifacts in the classpath and will meet one of the conditions for the beans to be auto-configured. We can also optionally define which dependencies this starter provides via resources/META-INF/spring.provides:

provides: cxf-spring-boot-starter-jaxrs, cxf-rt-rs-service-description, cxf-rt-rs-service-description-swagger, swagger-ui, jackson-jaxrs-json-provider

4. PACKAGE asimio-api-springboot-starter modules

For asimio-api-starter-demo to be able to use this custom Spring Boot starter we first need to make it available to the local .m2 folder or have a Continuous Integration server to build it and deploy it to a Maven repository manager or S3 bucket:

cd <path to asimio-api-springboot-starter>/asimio-api-springboot-starter-parent
mvn clean install

5. CREATE asimio-api-starter-demo DEMO SERVICE

A demo application using asimio-cxf-swagger-springboot-starter custom Spring Boot starter gets simpler than the one described in Implementing APIs using Spring Boot, Apache CXF and Swagger:

pom.xml:

...
<dependencies>
  <dependency>
    <groupId>com.asimio.api.starter</groupId>
    <artifactId>asimio-cxf-swagger-springboot-starter</artifactId>
    <version>0-SNAPSHOT</version>
  </dependency>
...
</dependencies>

The only important dependency that needs to be included, it would pull all the dependencies provided by asimio-cxf-swagger-springboot-starter.

Demo starter main class AsimioApiStarterDemoApp.java:

...
@SpringBootApplication(scanBasePackages = { "com.asimio.api.demo.rest" })
public class AsimioApiStarterDemoApp {

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

The resource interface with JAX-RS and Swagger annotations HelloResource.java:

package com.asimio.api.demo.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);
...
}

The resource implementation HelloResourceImpl.java:

package com.asimio.api.demo.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");
  }
...
}

Notice HelloResource.java and HelloResourceImpl.java classes are being scanned at application startup via @SpringBootApplication annotation.

A demo starter properties file, application.yml:

# 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

asimioApi:
  docs:
    basePath: ${cxf.path}
    title: 'Demo Docs Title'
    description: 'Demo Docs Description'
    version: v1
    contact: 'Orlando L Otero'
...

What’s interesting here is the asimioApi-related properties, those are the properties that get converted into a AsimioApiProperties bean and injected in a AsimioApiAutoConfiguration bean to auto-configure CXF and Swagger.

6. RUN asimio-api-starter-demo DEMO SERVICE

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

cd <path to asimio-api-starter-demo>
mvn clean package
java -jar target/asimio-api-starter-demo.jar

or

mvn spring-boot:run

Sending a request to the /api/v1/hello endpoint:

curl http://localhost:8080/api/v1/hello/Orlando
{"msg":"Hello Orlando. Version 1 - passed in URL"}

Available 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
Swagger UI /api/api-docs?url=/api/swagger.json


And that’s it for this Spring Boot custom starter for CXF and Swagger, a similar starter could be enhanced with logging, metrics, profiling, etc., anything that services might have in common.

Earlier this month this blog covered Implementing APIs using Spring Boot, Apache CXF and Swagger and this post details the benefits of using a custom Spring Boot starter to provide a common set of dependencies and functionality, beans auto-configuration and more. Definitely an improvement, but we would still need to create a new Spring Boot project and manually include the starter dependency and add configuration properties.

Wouldn’t it be better to also provide a Maven archetype that would take care of the service generation? Or better yet, wouldn’t it be really productive to not only generate the new service including the custom Spring Boot starter but also add it to a Git repo, create a CI pipeline, etc. through a shell command or clicking a button?

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.

7. SOURCE CODE

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

8. REFERENCES