JEE and the myth of portability

Have you ever been in that conversation in which you had to choose between two or more frameworks/libraries and one of them was so-called JEE standard? Usually a person who advocates for it explains that thanks to standardized API it’s possible to switch the implementation to any vendor whenever needed without a worry that something in our application breaks. In theory, it sounds tempting, but does practice prove it? I’m going to focus on simple case using JAX-RS to verify the assertion.

Advertisement

REST endpoint demo

Note
This post contains only code samples relevant to the described problem, configuration is omitted. If you want to play with the demo on your own, you can find the whole app on the GitHub repository. To simplify environment setup, the demo was created with Spring Boot, but you don’t have to be familiar with the framework to start it. All you need to know is how to run JUnit tests using Maven or your preferable IDE.

Imagine that you had a REST resource with a significantly big payload which was rarely updated and you wanted to send the HTTP Not Modified 304 response if it hadn’t been changed since the last access. Using pure JAX-RS it would be implemented like this:

@Path("/api")
public class SampleResource {

    private static final DateFormat format = new SimpleDateFormat("y-M-d H:m:s.S");

    @GET
    @Path("/sample")
    @Produces(MediaType.APPLICATION_JSON)
    public Response getSample(@Context Request request) throws ParseException {
        Date lastUpdate = format.parse("2016-02-01 00:00:00.123");
        Response.ResponseBuilder responseBuilder = request.evaluatePreconditions(lastUpdate);
        if (responseBuilder == null) {
            responseBuilder = Response.ok("Significantly big payload ");
        }
        return responseBuilder.lastModified(lastUpdate).build();
    }

}

The following sample shows an integration test which verifies status code for two sequential requests. If you’re not familiar with Spring, it starts the application using Tomcat and executes code against the running endpoint presented above. The RestTemplate class is used to simplify HTTP calls. The @ActiveProfiles class annotation in this case allows switching between preconfigured JAX-RS implementations.

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = JaxRsDemoApplication.class)
@WebIntegrationTest
@ActiveProfiles({Profiles.JERSEY})
public class SampleResourceTest {

    RestTemplate restTemplate = new TestRestTemplate();

    @Test
    public void shouldReturn304OnSecondRequest() throws Exception {
        ResponseEntity<String> firstResponse = callGetSample(null);

        long lastModified = firstResponse.getHeaders().getLastModified();
        HttpHeaders headers = new HttpHeaders();
        headers.setIfModifiedSince(lastModified);

        ResponseEntity<String> secondResponse = callGetSample(new HttpEntity<>(headers));

        assertEquals(HttpStatus.OK, firstResponse.getStatusCode());
        assertEquals(HttpStatus.NOT_MODIFIED, secondResponse.getStatusCode());
    }

    private ResponseEntity<String> callGetSample(HttpEntity<String> entity) {
        return restTemplate.exchange("http://localhost:8888/api/sample", HttpMethod.GET, entity, String.class);
    }

}

First, one call to the endpoint is made and after extracting the last-modified header from the response, the value is used as the is-modified-since header in the second request to the same endpoint. Then the status codes are checked for both responses which are expected to be 200 and 304 accordingly. Here is the test result for Jersey as the JAX-RS implementation.

Jersey test result

It shouldn’t come as a surprise to anyone that test passed and everything works as expected. Let’s change the JAX-RS implementation to RESTeasy using the aforementioned @ActiveProfiles annotation and run the test once more. We’re expecting to see the same results. What will happen?

RESTEasy test result

Ups… the test failed. We were promised that standard implementation can be changed without affecting our application, but apparently something went wrong.

Test result explanation

Let’s examine the JavaDoc for the evaluatePreconditions() method. The execution result is described as:

null if the preconditions are met or a ResponseBuilder set with the appropriate status if the preconditions are not met.

The preconditions are met if the date provided as the method argument is after the date in the is-modified-since header, which in in our case for the second request based on the first result is set to Mon, 01 Feb 2016 00:00:00 GMT. Someone who has a keen eye for details will see there is a small difference between this value and the last update date in the REST resource from the first sample in this post which was 2016-02-01 00:00:00.123. The devil is in the details – the request header precision is lesser and doesn’t contain milliseconds. If you look into RESTeasy source code, you’ll see a comparison between these two dates is done as follows:

public Response.ResponseBuilder ifModifiedSince(String strDate, Date lastModified) {
    Date date = DateUtil.parseDate(strDate);
    if (date.getTime() >= lastModified.getTime()) {
      return Response.notModified();
    }
    return null;
}
Source: org.jboss.resteasy.specimpl.RequestImpl.ifModifiedSince() – version 3.0.14.Final

First, the header is parsed from String into the Date type and then compared with the date provided as the method parameter. In our case this condition always returns false because of difference in milliseconds and hence the test fails.

In Jersey it looks pretty much the same, but there is only one small difference. Before making the comparison with the parsed header, the method argument goes through a method that ignores milliseconds:

private static long roundDown(final long time) {
    return time - time % 1000;
}
Source: org.glassfish.jersey.server.ContainerRequest.roundDown() – version 2.22.1

Is JEE vendor agnostic?

The aim of this showcase wasn’t to compare Jersey and RESTeasy or suggest which JEE provider is better. It was about calling into question the possibility of vendor interchangeability. Both presented implementation are fully certified, but what does it actually mean? I guess nothing more than the fact they both passed Java EE Compatibility Test Suite, which as the demo shows, doesn’t grant promised portability. I’m aware it’s just one example, but I’m sure you can easily find much more, for instance, by going through the list of reported bugs in one or another solution. Unless CTS is more strict and detailed, vendor portability is just empty marketing words. Hopefully, this can be improved in the future, but for now this assurance only harms the image of Oracle as standard creator.

Facebooktwittergoogle_plusredditlinkedinmail
Advertisement