Skip to content

Latest commit

 

History

History
356 lines (297 loc) · 18 KB

guide.adoc

File metadata and controls

356 lines (297 loc) · 18 KB

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.

What you’ll build

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.

What you’ll need

Endpoint

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;
    }
}
  1. FORMAT constant that can be used by clients of the class to create greeting messages in the expected format

  2. content field which will be used to populate our JSON payload This Greeting 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)
    }
}
  1. Standard JAX-RS annotation imports

  2. Specify that this class is a JAX-RS root resource and that the endpoint will answer requests on /greeting

  3. Mark the endpoint as a Spring component to be managed by Spring Boot. In conjunction with the cxf.jaxrs.component-scan property set to true in application.properties, this allows CXF to create a JAX-RS endpoint from the auto-discovered JAX-RS root resources.

  4. Mark the greeting method as answering HTTP GET requests

  5. Specify that the method returns JSON content (application/json content type)

  6. 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 is World if none is provided (thanks to the @DefaultValue("World") annotation)

  7. We format the message using the Greeting.FORMAT constant…

  8. 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();
    }
}
  1. 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.

  2. 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)
  1. 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.

  2. As mentioned above when we looked at the GreetingEndpoint class, we need to set that property to true to activate automatic creation of endpoint based on resource detection.

Frontend

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)
  1. Text input to enter the name to pass to the greeting service

  2. Button to trigger the call to the greeting service

  3. 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();
    });
  });
  1. Add a click event handler to the button with the invoke id

  2. Retrieve the value of the name input to pass to the greeting server

  3. Invoke the RESTful endpoint and retrieve the JSON response

  4. Replace the content of the element with the greeting-result id with the result of the invocation

Building and testing the application locally

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>
...
  1. Specify the BOM version we want to use. More details on the BOM content and its versioning scheme are available.

  2. Associated Spring Boot version

  3. The BOM version is imported in the dependencyManagement section of the POM file

  4. Since the BOM defines supported versions, we can then import supported dependencies without having to worry about their respective versions

  5. Specify that we want to use Spring Boot with an embedded Tomcat server

  6. Needed to be able to use Apache CXF integration with Spring Boot

  7. 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>
...
  1. Add the Spring Boot Maven plugin to the build using the previously defined spring-boot.version property.

  2. Specify that the repackage goal of the Spring Boot plugin should be executed during the package 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.

  3. 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

  4. Also perform Maven filtering on src/test/resources test resource files

Deploying the application on OpenShift

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.