Understanding how to deploy a simple RESTful service and front-end on OpenShift using Spring Boot and Apache CXF
This document will guide you through the steps to create a simple "Hello World" JAX-RS based, RESTful web service using Apache CXF and Spring Boot. While learning how to use JAX-RS with Spring Boot is interesting in itself, we will detail the steps and mechanisms involved in deploying the application on an OpenShift cluster in an effort to develop a truly cloud-native application with a workflow focused on making things natural to Java developers.
Conceptually, the application is similar to the one developed in Spring Boot’s guide on building RESTful services. However, where the Spring Boot guide uses Spring-specific annotations to define the endpoint and its method, we will use standard annotations, making it easier to reuse endpoints implementations across servers, should you want to target a different platform than Spring Boot at some point.
The application you will be building exposes a very simple greeting web service accepting GET
HTTP requests to
/api/greeting
, responding with a JSON message in the form {"content":"Hello, World!"}
. The
message can be customized by passing the name
query parameter to the request as in
http://localhost:8080/api/greeting?name=John
which would result in the following response: {"content":"Hello,
John!"}
.
Additionally, a very simple front-end is provided using HTML and jQuery to interact with the greeting endpoint
from a more user-friendly interface than query the service using cURL
.
Note
|
While we will go over the implementation, we will focus mostly on the specifics needed to get your application running on OpenShift as opposed to detailing all the Spring Boot-specific implementation. |
-
Java 11
-
Access to an OpenShift cluster whether locally via minishift or using the different flavors of OpenShift products
The application is composed of a RESTful service. Its code can be found in the src/main/java/dev/snowdrop/example/service
directory. It is split in two classes: GreetingEndpoint
, which implements the endpoint itself, and Greeting
which is a
simple class representing the payload sent back to users of the endpoint.
Let’s look at the Greeting
class first, which is pretty simple:
public class Greeting {
public static final String FORMAT = "Hello, %s!"; #(1)
private final String content; #(2)
public Greeting() {
this.content = null;
}
public Greeting(String content) {
this.content = content;
}
public String getContent() {
return content;
}
}
-
FORMAT
constant that can be used by clients of the class to create greeting messages in the expected format -
content
field which will be used to populate our JSON payload ThisGreeting
class will be automatically marshalled by Jackson using the accessor.
Let’s now look at the GreetingEndpoint
class, short and sweet but packing quite a punch, thanks to annotations:
import javax.ws.rs.DefaultValue; # (1)
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import org.springframework.stereotype.Component;
@Path("/greeting") # (2)
@Component # (3)
public class GreetingEndpoint {
@GET # (4)
@Produces("application/json") # (5)
public Greeting greeting(@QueryParam("name") @DefaultValue("World") String name) { #(6)
final String message = String.format(Greeting.FORMAT, name); #(7)
return new Greeting(message); #(8)
}
}
-
Standard JAX-RS annotation imports
-
Specify that this class is a JAX-RS root resource and that the endpoint will answer requests on
/greeting
-
Mark the endpoint as a Spring component to be managed by Spring Boot. In conjunction with the
cxf.jaxrs.component-scan
property set totrue
inapplication.properties
, this allows CXF to create a JAX-RS endpoint from the auto-discovered JAX-RS root resources. -
Mark the
greeting
method as answering HTTPGET
requests -
Specify that the method returns JSON content (
application/json
content type) -
The
name
method parameter is annotated with@QueryParam("name")
to specify that it is passed as a query parameter in the URL when the service is invoked and that its default value isWorld
if none is provided (thanks to the@DefaultValue("World")
annotation) -
We format the message using the
Greeting.FORMAT
constant… -
and return a
Greeting
instance with the proper message. This object will be automatically serialized to JSON using Jackson as we will see later.
As you can see, there isn’t much to it as far as code goes.
We still need to configure CXF and Spring Boot properly for everything to work well.
On the Spring Boot side, we need an entry point to our service in the form a class annotated with @SpringBootApplication
, also giving us the opportunity to further configure our stack:
import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication #(1)
public class ExampleApplication {
public static void main(String[] args) {
SpringApplication.run(BoosterApplication.class, args);
}
@Bean
public JacksonJsonProvider jsonProvider() { #(2)
return new JacksonJsonProvider();
}
}
-
Activates auto-configuration, component scan and marks the class as providing configuration using the
@SpringBootApplication
annotation. This also allows to package the application as a jar that can be run as a typical application. Spring Boot will then start the embedded Tomcat server. -
Specifies that the JSON provider to be used by CXF (which uses Spring as basis of its configuration) should be Jackson.
Note
|
You’ll notice that, contrary to Spring MVC where Jackson only needs to be present on the classpath for it to be used, Apache CXF requires Jackson to be explicitly configured. This could be done via XML but we might as well leverage the @SpringBootApplication configuration capability.
|
Let’s now look at the content of application.properties
which we need to further configure CXF:
cxf.path:/api #(1)
cxf.jaxrs.component-scan:true #(2)
-
Specify that CXF will answer to requests sent to the
/api
context. Our endpoint root resource is annotated with@Path("/greeting")
which means that the full context for our endpoint will be/api/greeting
. -
As mentioned above when we looked at the
GreetingEndpoint
class, we need to set that property totrue
to activate automatic creation of endpoint based on resource detection.
Let’s take a quick look at our frontend. It’s implemented as a static HTML src/resources/static/index.html
file served from the root of the embedded Tomcat server. The basic idea is similar to what is explained in the consuming a RESTful Web Service with jQuery Spring Boot guide so we will only focus on the salient parts for our purpose.
In our case, our service is running on the same server so we don’t need to worry about CORS. Moreover, for the same reason, we don’t need any extra code for Spring Boot to start Tomcat.
The simple UI consists in a form to specify which name to pass to the greeting service and then invoke it:
<form class="form-inline">
<div class="form-group">
<label for="name">Name</label>
<input type="text" class="form-control" id="name" placeholder="World"> #(1)
</div>
<button id="invoke" type="submit" class="btn btn-success">Invoke</button> #(2)
</form>
<p class="lead">Result:</p>
<pre><code id="greeting-result">Invoke the service to see the result.</code></pre> #(3)
-
Text input to enter the name to pass to the greeting service
-
Button to trigger the call to the greeting service
-
Placeholder text that will be replaced by the result of the service call
and the embedded jQuery script:
$(document).ready(function () {
$("#invoke").click(function (e) { #(1)
var n = $("#name").val() || "World"; #(2)
$.getJSON("/api/greeting?name=" + n, function (res) { #(3)
$("#greeting-result").text(JSON.stringify(res)); #(4)
});
e.preventDefault();
});
});
-
Add a
click
event handler to the button with theinvoke
id -
Retrieve the value of the
name
input to pass to the greeting server -
Invoke the RESTful endpoint and retrieve the JSON response
-
Replace the content of the element with the
greeting-result
id with the result of the invocation
You can run the application using ./mvnw spring-boot:run
, using the run
goal of the Maven Spring Boot plugin.
It’s also possible to build the JAR file with ./mvnw clean package
and run it like a traditional Java application:
java -jar target/rest-http-<version>.jar
where <version>
corresponds to the current version of the project.
Once the application is started, you can visit http://localhost:8080/index.html to see the frontend of the application and interact with the greeting service.
Let’s look at the important parts of the Maven project to properly build and run the application locally.
First, we need to tell Maven that we’re using Spring Boot and more specifically that we want to use the Snowdrop supported set of Spring Boot starters. This is accomplished by using 2 properties and importing the Snowdrop Bill Of Materials (BOM) and any dependencies we need for our application:
...
<properties>
<spring-boot-bom.version>1.5.14.Final</spring-boot-bom.version> #(1)
<spring-boot.version>1.5.14.RELEASE</spring-boot.version> #(2)
....
</properties>
...
<dependencyManagement>
<dependencies>
<dependency>
<groupId>me.snowdrop</groupId>
<artifactId>spring-boot-bom</artifactId>
<version>${spring-boot-bom.version}</version> #(3)
<type>pom</type>
<scope>import</scope>
</dependency>
...
</dependencies>
</dependencyManagement>
<dependencies> #(4)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId> #(5)
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-spring-boot-starter-jaxrs</artifactId> #(6)
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.jaxrs</groupId>
<artifactId>jackson-jaxrs-json-provider</artifactId> #(7)
</dependency>
...
</dependencies>
...
-
Specify the BOM version we want to use. More details on the BOM content and its versioning scheme are available.
-
Associated Spring Boot version
-
The BOM version is imported in the
dependencyManagement
section of the POM file -
Since the BOM defines supported versions, we can then import supported dependencies without having to worry about their respective versions
-
Specify that we want to use Spring Boot with an embedded Tomcat server
-
Needed to be able to use Apache CXF integration with Spring Boot
-
Needed so that Apache CXF can use Jackson as JSON marshaller as seen above when we defined a
jsonProvider
bean provider method in our application entry point
Let’s now look at the build configuration:
...
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering> #(3)
</resource>
</resources>
<testResources>
<testResource>
<directory>src/test/resources</directory>
<filtering>true</filtering> #(4)
</testResource>
</testResources>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version> #(1)
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration/>
<executions>
<execution>
<goals>
<goal>repackage</goal> #(2)
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
...
-
Add the Spring Boot Maven plugin to the build using the previously defined
spring-boot.version
property. -
Specify that the
repackage
goal of the Spring Boot plugin should be executed during thepackage
phase of the Maven build. This leads to the creation of new jar file repackaged to create a self-contained, executable application. The originally generated jar file is kept but renamed with the.original
suffix appended to its name. -
Activate Maven filtering on files put in
src/main/resources
where Spring Boot configuration files live so that properties in the${property.name}
can be interpolated and replaced during the build -
Also perform Maven filtering on
src/test/resources
test resource files
Now that we’ve seen the gist of the application and how to run it locally, let’s look at what’s needed to deploy it on OpenShift. This is accomplished using Dekorate. Dekorate brings your Java applications to OpenShift. Tightly integrated with Maven, it leverages the existing build configuration to focus on two tasks: building Docker images and creating OpenShift (or plain Kubernetes) resource descriptors. Since our application is built using Maven, it makes sense to continue to leverage that tool to generate whatever is necessary to deploy and run our application on OpenShift.
Note
|
The following steps assume that you are currently connected to a running OpenShift cluster via oc login . By doing so, FMP will be able to determine that you are targeting an OpenShift deployment automatically and take additional steps to generate OpenShift-specific descriptors (as opposed to generic Kubernetes ones).
|
First, we need to tell Maven that we want to use this dependency. This is accomplished in the parent POM of our example, which is declared as:
<parent>
<groupId>io.openshift</groupId>
<artifactId>booster-parent</artifactId>
<version>23</version>
</parent>
Note
|
We’re considering removing the need for a parent and including the FMP (Fabric8 Maven Plugin) configuration directly in our boosters. |
Let’s look at the parts that deal with configuring the Fabric8 Maven Plugin:
...
<dependencies>
<dependency>
<groupId>io.dekorate</groupId>
<artifactId>openshift-spring-starter</artifactId>
</dependency>
...
</dependencies>
...
And specify the Docker base image to use in the application.properties
:
dekorate.openshift.expose=true
dekorate.s2i.builder-image=registry.access.redhat.com/ubi8/openjdk-11:1.14
Specify which Docker base image to use when generating the images for our application. The base image will serve as the foundation on top of which Dekorate adds our application to create a container ready to be deployed on a Kubernetes cluster. In this case, the base image is the Red Hat supported OpenJDK 11 image since our application is, at its code, a Java application.
Note
|
You can see and explore the list of Red Hat supported images that can serve as base images for you applications at: https://access.redhat.com/containers/. |
We then need to generate the resources and deploy to the Openshift cluster we’re connected to. This is accomplished by running:
./mvnw clean verify -Popenshift -Ddekorate.deploy=true
The more interesting directory when it comes to files generated by Dekorate
is the target/classes/META-INF/dekorate
directory. This is where FMP puts the final version of the generated files once they have prepared. Looking at it, we notice it has the following structure:
- openshift.json
- openshift.yml
Next, you can access your application by running
oc get route rest-http -o jsonpath='{"http://"}{.spec.host}{"\n"}'
and pasting that URL in your favorite browser.