Spring Boot - Microservice- JaxRS Jersey - HATEOAS - JerseyTest - Integration

In this article, we discuss how to write a Spring Boot microservice based on Spring Jax-Rs Jersey, HATEOAS API, and JerseyTest framework integration. We are going to take the material of our previous article Spring Boot - Microservice - Spring Data REST and HATEOAS Integration and rewrite it for new Spring Jax-Rs Jersey usage.

You may also like: REST API — What Is HATEOAS?

Both articles are based on the sample project written by Greg Turnquist one of the authors of the Spring HATEOAS - Reference Documentation. If you are already familiar with this project and its problem domain feel free to skip its description. Otherwise, we do encourage you to keep reading.

Maven Project Dependencies

The project has the following Maven dependencies.

maven dependencies

The following is the relevant snippet from the project pom file.

XML
 




xxxxxxxxxx
1
44


 
1
   <dependencies>
2
        <dependency>
3
            <groupId>org.springframework.boot</groupId>
4
            <artifactId>spring-boot-starter-data-jpa</artifactId>
5
        </dependency>
6
        <dependency>
7
            <groupId>org.springframework.boot</groupId>
8
            <artifactId>spring-boot-starter-hateoas</artifactId>
9
        </dependency>
10
        <dependency>
11
            <groupId>org.springframework.boot</groupId>
12
            <artifactId>spring-boot-starter-jersey</artifactId>
13
        </dependency>
14
 
          
15
        <dependency>
16
            <groupId>com.h2database</groupId>
17
            <artifactId>h2</artifactId>
18
            <scope>runtime</scope>
19
        </dependency>
20
        <dependency>
21
            <groupId>org.springframework.boot</groupId>
22
            <artifactId>spring-boot-starter-test</artifactId>
23
            <scope>test</scope>
24
            <exclusions>
25
                <exclusion>
26
                    <groupId>org.junit.vintage</groupId>
27
                    <artifactId>junit-vintage-engine</artifactId>
28
                </exclusion>
29
            </exclusions>
30
        </dependency>
31
 
          
32
        <dependency>
33
            <groupId>org.glassfish.jersey.test-framework</groupId>
34
            <artifactId>jersey-test-framework-core</artifactId>
35
            <scope>test</scope>
36
        </dependency>
37
 
          
38
        <dependency>
39
            <groupId>org.glassfish.jersey.test-framework.providers</groupId>
40
            <artifactId>jersey-test-framework-provider-grizzly2</artifactId>
41
            <scope>test</scope>
42
        </dependency>
43
 
          
44
    </dependencies>



Project Code Structure

This project has the following structure.

project structure

KievSpringJaxrsJerseyAndHateoasApplication.java — Spring Boot Application class generated by the Spring Boot. You have seen similar codes many times by now. The addition of the highlighted static property mapperString is necessary for the integration with the JerseyTest framework and would be discussed in detail later.

@SpringBoot

JerseyConfig.java — Plain vanilla Jersey 2.x configuration file. There is nothing special about it. As we can see, here the single endpoint is registered.

JerseyConfig.java

CustomOrderHateoasController.java, OrdersModel.java, and HalConfig.java – Custom controller, aka registered endpoint, and its support and configuration classes accordingly. It is here where the integration between Spring Jax-Rs Jersey and HATEOAS API happens.

KievSpringJaxrsJerseyAndHateoasApplcationTests.java, MockMvc.java, and MockMvcConfig.java – Classes responsible for integration JerseyTest framework with Spring Boot application.And that what makes them rather interesting.

We are ready to start to dissect the project. But before we do that, let's refresh our memory what are the goals for the project. The following are relevant excerpts taken from the README.adoc document.

Defining the Problem

PROBLEM: You wish to implement the concept of orders. These orders have certain status codes that dictate what transitions the system can take, e.g. an order can’t be fulfilled until it’s paid for, and a fulfilled order can’t be canceled.

SOLUTION: You must encode a set of OrderStatus codes, and enforce them using a custom Spring Web MVC controller. This controller should have routes that appear alongside the ones provided by Spring Data REST.

Let's describe the basics of an ordering system.

That starts with a domain object:

Java
 




xxxxxxxxxx
1
24


 
1
@Entity
2
@Table(name = "ORDERS") // (1)
3
class Order {
4
 
          
5
    @Id @GeneratedValue
6
    private Long id; // (2)
7
 
          
8
    private OrderStatus orderStatus; // (3)
9
 
          
10
    private String description; // (4)
11
 
          
12
    private Order() {
13
        this.id = null;
14
        this.orderStatus = OrderStatus.BEING_CREATED;
15
        this.description = "";
16
    }
17
 
          
18
    public Order(String description) {
19
        this();
20
        this.description = description;
21
    }
22
 
          
23
    ...
24
}



The next step in defining your domain is to define OrderStatus. Assuming we want a general flow of Create an orderPay for an orderFulfill an order, with the option to cancel only if you have not yet paid for it, this will do nicely:

Java
 




xxxxxxxxxx
1
24


 
1
public enum OrderStatus {
2
 
          
3
  BEING_CREATED, PAID_FOR, FULFILLED, CANCELLED;
4
 
          
5
  /**
6
   * Verify the transition between {@link OrderStatus} is valid.
7
   *
8
   * NOTE: This is where any/all rules for state transitions should be kept and enforced.
9
   */
10
  static boolean valid(OrderStatus currentStatus, OrderStatus newStatus) {
11
 
          
12
    if (currentStatus == BEING_CREATED) {
13
      return newStatus == PAID_FOR || newStatus == CANCELLED;
14
    } else if (currentStatus == PAID_FOR) {
15
      return newStatus == FULFILLED;
16
    } else if (currentStatus == FULFILLED) {
17
      return false;
18
    } else if (currentStatus == CANCELLED) {
19
      return false;
20
    } else {
21
      throw new RuntimeException("Unrecognized situation.");
22
    }
23
  }
24
}



At the top are the actual states. At the bottom is a static validation method. This is where the rules of state transitions are defined, and where the rest of the system should look to discern whether or not a transition is valid.

The last step to get off the ground is a Spring Data repository definition:

Java
 




xxxxxxxxxx
1


 
1
public interface OrderRepository extends CrudRepository<Order, Long> {
2
 
          
3
}



This repository extends Spring Data Commons' CrudRepository, filling in the domain and key types (Order and Long).

The following class preloads some testing data. We do believe that @Configuration annotation should be used instead of the @Component, but we are trying to reuse the code of our foundation as much as possible and that is the reason why we have @Component annotation in place.

Java
 




xxxxxxxxxx
1
12


 
1
@Component
2
public class DatabaseLoader {
3
 
          
4
  @Bean
5
  CommandLineRunner init(OrderRepository repository) { // (1)
6
 
          
7
    return args -> { // (2)
8
      repository.save(new Order("grande mocha")); // (3)
9
      repository.save(new Order("venti hazelnut machiatto"));
10
    };
11
  }
12
}


It should be stated that right here, you can launch your application. Spring Boot will launch the web container, preload the data, and then bring Spring Data REST online. Spring Data REST with all of its prebuilt, hypermedia-powered routes, will respond to calls to create, replace, update and delete Order objects.

Spring Data REST will know nothing of valid and invalid state transitions. Its pre-built links will help you navigate from /api to the aggregate root for all orders, to individual entries, and back. But there will no concept of paying for, fulfilling, or canceling orders. At least, not embedded in the hypermedia. The only hint end users may have are the payloads of the existing orders.

That’s not effective.

No, it’s better to create some extra operations and then serve up their links when appropriate.

That concludes the description of our preliminary steps. At this point, you should know the goals of the project. We do know what has to be done, so let's see how it can be done. Let's talk about Spring Jax-Rs Jersey and HATEOAS integration.

Spring Jax-Rs Jersey and HATEOAS Integration

In a moment we would talk about the endpoint CustomOrderHateoasController and its support and configuration files. First, let's make it crystal clear what this integration with HATEOAS is all about. So what is the deal?

As we know a core principle of HATEOAS is that resources should be discoverable through the publication of the links that point to available resources. For example, if we issue an HTTP GET to the root URL of our project we should see the following output.

HTTP GETThe first link should direct us to the resource orders, while the latest link should direct us to the ALPS document. We do not have an ALPS document for this demo, so let's issue an HTTP GET to the first link, aka orders. We should see the following output, and now we can see that there are two orders and we can pay or cancel them accordingly.

localhost

Let's stop here for a second and contemplate what is the difference between the first root resource and the second orders resource. Well, one thing that comes to mind is the observation that the orders resource is dealing with the data from the repository while the root resource does not.

It means, that the second resource would use EntityModel from Spring Data JPA, and that is where we have to start doing some integration with HATEOAS API provided by Spring. Let's look at the relevant methods from CustomOrderHateoasController.java.

CustomOrderHateoasController

That is where support class OrderModel.java comes into the picture.

OrderModel.java

This static class is responsible for the root resource HAL description. Due to the fact that it does not use EntityModel, we can have the property named _links, and Jersey would generate JSON according to our expectations. After all, we are in control, we are masters of our domain! We would like to point out that this representation is written manually, but it should be acceptable.

After all, we are talking about microservices, and that implies that the root API that is not excessively large. Otherwise, it would not be a microservice but something else. Anyway, let's take a look at what is going on when we have to generate HAL representation for the resource orders.

EntityModel


Here we can see the property named _embedded so the Jersey would generate it accordingly. But then we can see that we are using instantiation of the EntityModel interface, and this instantiation is out of our reach. It does not have property _links, but links instead.Variables

So what does it mean and why do we care? We care, because the Jersey would generate the next representation.

code screenshot

The highlighted boxes show where the problem is. Here we can clearly see that Jersey generated links and not the _links that we would like to have. It makes sense after all links are what we have in the EntityModel. It means we are ready to start integration with Spring HATEOAS that would lead us to the HAL format. Let's consider and welcome the HalConfig.java, the configuration class that makes the things done.HalConfig.java

Now you can start the spring boot application and follow steps outlined in the README.adoc document.

Java
 




xxxxxxxxxx
1
43


 
1
$ curl localhost:8080/api/orders/1 { "orderStatus" : "BEING_CREATED", "description" : "grande mocha", "_links" : {
2
"self" : {
3
  "href" : "http://localhost:8080/api/orders/1"
4
},
5
"order" : {
6
  "href" : "http://localhost:8080/api/orders/1"
7
},
8
"payment" : {
9
  "href" : "http://localhost:8080/api/orders/1/pay"
10
},
11
"cancel" : {
12
  "href" : "http://localhost:8080/api/orders/1/cancel"
13
}
14
}
15
====
16
Apply the payment link:
17
====
18
$ curl -X POST localhost:8080/api/orders/1/pay { "id" : 1, "orderStatus" : "PAID_FOR", "description" : "grande mocha" }
19
$ curl localhost:8080/api/orders/1 { "orderStatus" : "PAID_FOR", "description" : "grande mocha", "_links" : {
20
"self" : {
21
  "href" : "http://localhost:8080/api/orders/1"
22
},
23
"order" : {
24
  "href" : "http://localhost:8080/api/orders/1"
25
},
26
"fulfill" : {
27
  "href" : "http://localhost:8080/api/orders/1/fulfill"
28
}
29
}
30
====
31
The `pay` and `cancel` links have disappeared, replaced with a `fulfill` link.
32
Fulfill the order and see the final state:
33
====
34
$ curl -X POST localhost:8080/api/orders/1/fulfill { "id" : 1, "orderStatus" : "FULFILLED", "description" : "grande mocha" }
35
$ curl localhost:8080/api/orders/1 { "orderStatus" : "FULFILLED", "description" : "grande mocha", "_links" : {
36
"self" : {
37
  "href" : "http://localhost:8080/api/orders/1"
38
},
39
"order" : {
40
  "href" : "http://localhost:8080/api/orders/1"
41
}
42
}
43
 
          



Everything works as expected. It means our integration Jax-Rs Jersey with the HATEOAS API was a sweet-sweet success. But wait! What is this highlighted line above all about? What is this mapperString? Yes, indeed! What is it?

Spring Boot Integration With JerseyTest Framework

We are using it for the integration of the Spring Boot application and the JerseyTest framework. After all, we are living in a material world, and they do demand unit tests over here for the loaf of bread. So we can't stop, we don't have any choice but to keep going instead. As you probably know, Jax-Rs Jersey is its own world, and in this world, they do like the JerseyTest framework. 

We do have to accept it as a fact and live with it. They do also like the Grizzly test container, and it is ok with us too. So what do we have at the end? We do have the JerseyTest framework and Grizzly test container to use. Remember our pom file above, the last two dependencies? If the answer is No, then scroll the text back and take a look.

So now you know what we have to do. We have to run test cases written by Greg Turnquist OrderIntegrationTest.java. It should not be a big problem, right? Well, the answer is yes and no. The truth is we can't use it the way it is. The reason is quite simple. It was written by Spring folks for Spring crowd. In other words, it is based on the Spring MockMvc test framework. And our JerseyTest application is not MVC. Does it mean, that we should rewrite the test cases, reuse the logic but not the already written code? 

We can do that. But what about if you have a ton of already written JUnit test cases for the Spring MVC, and now they are telling you that you are migrating to the Jersey world. Yeah, all the way! Are you going to do the rewrite? Take a breath and don't panic. At least, don't panic yet. 

We are excited to let you know that you don't have to. While you can't reuse all the Spring MockMVC written code, you can reuse a chunk of it, and most importantly you would be able to execute all already written JUnit test cases without any modification but with a little bit of help, of course. Now (drum beats) Ladies and Gentlemen let us introduce you to the Spring Boot – JerseyTest integration!

Where do we start? We would start with the existing code. Let's see what we have.MVC

The first things to notice are the _links, highlighted by a red box. Also there are such instance methods as perform(), andDo(), andExpect(). These methods are chained. In other words, their return value is the instance itself (this.mvc). We can also deduct that these instance methods arguments are static methods calls. Let's list some of them to make things clear: get(), post(), jsonPath(), and ext.

Let's start talking about these things one by one. Let's start talking about the _links. At this point of our discussion, we should be familiar with its origin. It is the HAL links representation. As our JerseyTest application is not integrated with HATEOAS yet, and we can assume that it would fail here. And it does. Consider the KievSpringJaxrsJerseyAndHateoasApplcationTests.java.

KievSpring

The JerseyTest application has its own application context. By default, it would try to use XML spring schema-based configuration. That would be our first failure. After all the spring boot application that we generated is based on the annotation-based configuration. So the first highlighted line is taking care of that. 

We can see that we are trying to process our Spring Boot application, and our expectation is that everything would be loaded. It does, but it is not the same. One thing you would notice that the repository lost test loaded data. That is easy to fix, and we can reload the test data. Not big deal, we can say to ourselves. After all, we are running tests, and it makes sense to load some data for the tests. So we are good at that.

However, to our surprise, we would discover that the loaded HATEOAS integration, the object mapper, does not deliver. The mapper would be in the JerseyTest context, but the generated representation would be links, not the _links that the written test cases expect. It is obvious that some trickery has to be done in order to remedy the situation. 

Remember, the mapperString the static property of the Spring Boot application class? You sure do. So what do we know? We know that the object mapper from the Spring Boot application context delivers. So we can try to grab this instance and push it to the JerseyTest context. That is what the property mapperSpring is for. And the approach works!

MVC

So what is this object MVC that we are getting from the JerseyTest application context (line 55)? One thing is for sure, it can't be original MockMvc from the Spring Boot testing framework. If you guessed as so, you were right! It is not! It is our adapter, that puts all pieces of the puzzle in place. Here is how it works. But before we go to its discussion, let's take a look at his configuration. So we do know how it gets into the JerseyTest application context.

So it is an instance of the MockMvc.java the adapter that we wrote to be able to reuse existing Junit test cases. With the help of the class, we are able to execute the test written for the Spring MockMvc test framework in the JerseyTest suite. It is simple, it is rather crude but it makes the job done! Feel free to explore it, of course. Before we complete the article and thank you for the reading, we would like to highlight some of its features like static and instance methods invocations. That we believe is informative.

Let's dissect the statement this.mvc.perform(get("/api"))

Java
 




xxxxxxxxxx
1
24


 
1
   public static Pair<String, String> get(String path) {
2
        return Pair.of(HttpMethod.GET, path);
3
    }
4
 
          
5
    public static Pair<String, String> post(String path) {
6
        return Pair.of(HttpMethod.POST, path);
7
    }
8
...
9
    public MockMvc perform(Pair<String, String> methodPath) {
10
        String httpMethod = methodPath.getFirst();
11
        String path = methodPath.getSecond();
12
        if (HttpMethod.GET.equals(httpMethod)) {
13
            Response response = client
14
                    .target(String.format("%s://%s%s", baseUri.getScheme(), baseUri.getAuthority(), path)).request()
15
                    .get();
16
            setResponse(response);
17
        } else if (HttpMethod.POST.equals(httpMethod)) {
18
            Response response = client
19
                    .target(String.format("%s://%s%s", baseUri.getScheme(), baseUri.getAuthority(), path)).request()
20
                    .post(Entity.json(null));
21
            setResponse(response);
22
        }
23
        return this;
24
    }  



As you can see, we are invoking our endpoint deployed to the Grizzly test container by means of the JerseyTest framework.

The other interesting statement to consider is .andExpect(jsonPath("$._embedded.orders[0].orderStatus", is("BEING_CREATED")))

Java
 




xxxxxxxxxx
1
77


 
1
   public static COMMAND jsonPath(String expression, String expected) {
2
        COMMAND.JSON_PATH.setExpression(expression);
3
        COMMAND.JSON_PATH.setExpected(expected);
4
        return COMMAND.JSON_PATH;
5
    }
6
 
          
7
    public static String is(String expected) {
8
        return expected;
9
    }
10
...
11
    public MockMvc andExpect(MockMvc.COMMAND command) throws Exception {
12
        switch (command) {
13
        case PRINT:
14
            break;
15
        case STATUS_OK: {
16
            if (!Response.Status.OK.equals(Response.Status.fromStatusCode(response.getStatus())))
17
                throw new Exception();
18
            break;
19
        }
20
        case CONTENT_HAL_JSON: {
21
            String contentType = response.getHeaderString("Content-Type");
22
            if (!(MediaTypes.HAL_JSON).equals(MediaType.valueOf(contentType)))
23
                throw new Exception();
24
            break;
25
        }
26
        case JSON_PATH: {
27
            String expression = COMMAND.JSON_PATH.getExpression();
28
            String expected = COMMAND.JSON_PATH.expected;
29
            String json = getJson();
30
            Object[] args = {};
31
            JsonPathExpectationsHelper helper = new JsonPathExpectationsHelper(expression, args);
32
 
          
33
            Object rawActual = helper.evaluateJsonPath(json);
34
            boolean isNumeric = expected.chars().allMatch(Character::isDigit);
35
            if (isNumeric) {
36
                Integer actualValue = (Integer) rawActual;
37
                rawActual = String.format("%d", actualValue);
38
            }
39
            String actualUri = (String) rawActual;
40
            String expectedUri = expected;
41
            if (PORT_HANDLING.SET_EXPECTED_PORT_FROM_ACTUAL.equals(portHandling) && expectedUri.startsWith("http://")) {
42
                // very naive approach, but it is good enough for demo
43
                URL originalURL = new URL(expected);
44
                // add port
45
                URL portURL = new URL(originalURL.getProtocol(), originalURL.getHost(), baseUri.getPort(),
46
                        originalURL.getFile());
47
                expectedUri = portURL.toString();
48
            }
49
            if (!expectedUri.equals(actualUri))
50
                throw new Exception();
51
 
          
52
            break;
53
        }
54
        case IS4XXCLIENTERROR: {
55
            if (!Response.Status.Family.CLIENT_ERROR.equals(Response.Status.Family.familyOf(response.getStatus())))
56
                throw new Exception();
57
            break;
58
        }
59
        case CONTENT_APPLICATION_JSON: {
60
            String contentType = response.getHeaderString("Content-Type");
61
            if (!(MediaType.APPLICATION_JSON).equals(MediaType.valueOf(contentType)))
62
                throw new Exception();
63
            break;
64
        }
65
        case CONTENT_STRING: {
66
            String expected = COMMAND.CONTENT_STRING.getExpected();
67
            String json = getJson();
68
            if (!json.equals(expected))
69
                throw new Exception();
70
            break;
71
        }
72
        default:
73
            throw new Exception();
74
        }
75
 
          
76
        return this;
77
    }  



It is worth mentioning the class JsonPathExpectationsHelper.java (line 121). It is part of the Spring MockMvc testing framework, and it is the real core component that process expectations and actual expressions — things like "$._embedded.orders[0].orderStatus" and ext.

The last part to mention is the JerseyTest client that the MockMvc is using in order to invoke the endpoint deployed to the Grizzly container (perform()). The setup happens before every test. Consider the following.

By default, the JerseyTest resets the test container before every test. As a result, we have to reset the client in our MVC adapter.

Conclusions

That's it. Feel free to take the code from the GitHub and give it a spin. Feel free to expand it and use it, if it does make a sense. After all, they do say that it is better to see something once than to hear about it a thousand times. The saying is an Asian proverb. One of the authors is living and working there. So if you find yourself in Seoul, Republic of Korea, stop by and say Hi to Bogdan. As for myself, I am a bit tired. In fact, I am tired a lot lately. Time to sleep. Rest in peace Konstantin. :)


Further Reading

HATEOAS REST Services With Spring

RESTful Web Services With Spring Boot, Gradle, HATEOAS, and Swagger

Spring Boot - HATEOAS for RESTful Services

 

 

 

 

Top