How to migrate a Java EE REST API from JBoss EAP 6 to EAP 7

(P) Codever is an open source bookmarks and snippets manager for developers & co. See our How To guides to help you get started. Public bookmarks repos on Github ⭐🙏
In this post I will briefly describe the steps and issues encountered when migrating a Java EE REST API from JBoss EAP 6 to JBoss EAP 7 - this implies migrating from a Java EE 6/JAX RS 1.0 implementation to a Java EE 7/JAX RS.2.0 implementation. The trigger was the announcement from Red Hat regarding the general availability of their JBoss Enterprise Application Platform 7 (JBoss EAP) 1. JBoss EAP 7 is based on Wildfly 102, so the code snippets showed along the post should work on Wildfly 10 too.
- Update dependencies versions
- Java EE Deployment Descriptors
- Meet JAX-RS 2.0
- Jackson
- Other good REST(easy) related resources
- References
Update dependencies versions
The first thing I’ve done was to migrate the library dependencies used in the project. You can find usual component versions, like RESTEasy, Hibernate, Hibernate-validator etc. at JBoss Enterprise Application Platform Component Details. A couple of example are:
Library | JBoss EAP 6 | JBoss EAP 7 |
---|---|---|
Hibernate Core | 4.2.9.Final | 5.0.9.Final |
RESTEasy | 2.3.6.Final | 3.0.16.Final |
Be aware to adjust the versions whenever you update to a newer minor/major release
Update the JBoss JavaEE Specs API 3
From
<dependency>
<groupId>org.jboss.spec</groupId>
<artifactId>jboss-javaee-6.0</artifactId>
<version>3.0.3.Final</version>
<type>pom</type>
<scope>provided</scope>
</dependency>
to
<!-- https://mvnrepository.com/artifact/org.jboss.spec/jboss-javaee-7.0 -->
<dependency>
<groupId>org.jboss.spec</groupId>
<artifactId>jboss-javaee-7.0</artifactId>
<version>1.0.3.Final</version>
<type>pom</type>
</dependency>
Java EE Deployment Descriptors
The next thing to modify, were the Java EE deployment descriptors. Check out the Java EE: XML Schemas for Java EE Deployment Descriptors document from Oracle to see the new descriptor versions and namespaces. All Java EE 7 and newer Deployment Descriptor Schemas share now the namespace http://xmlns.jcp.org/xml/ns/javaee/
Down below are listed the ones I used in my project:
web.xml
From
<web-app
xmlns="https://java.sun.com/xml/ns/javaee"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://java.sun.com/xml/ns/javaee https://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
.....
</web-app>
to
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://xmlns.jcp.org/xml/ns/javaee https://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
beans.xml
From
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="https://java.sun.com/xml/ns/javaee" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://java.sun.com/xml/ns/javaee https://java.sun.com/xml/ns/javaee/beans_1_0.xsd">
</beans>
To
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="https://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://xmlns.jcp.org/xml/ns/javaee https://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd"
version="1.1" bean-discovery-mode="all">
</beans>
persistence.xml
From
<?xml version="1.0" encoding="UTF-8"?>
<persistence
xmlns="https://java.sun.com/xml/ns/persistence"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://java.sun.com/xml/ns/persistence https://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
version="2.0">
...........
</persistence>
To
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="https://xmlns.jcp.org/xml/ns/persistence"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://xmlns.jcp.org/xml/ns/persistence https://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"
version="2.1">
...........
</persistence>
Meet JAX-RS 2.0
The RESTEasy documentation version referenced throughout this post is 3.0.16.Final, as this is the version used for JBoss EAP 7.0.0, for which the migration took place at the time of the writing. For other/newer versions check the RESTEasy Documentation, where you can find examples, HTML, PDF, Javadocs for all RESTEasy versions.
Server API
On the server side the things have remained much or less the same. One has to admit they were very good to begin with.
The way to go with with RESTEasy is to define an javax.ws.rs.core.Application
class that is annotated with the @ApplicationPath
annotation.If you return an empty set of classes and singletons (or nothing), your WAR will be scanned for JAX-RS annotation resource and provider classes4:
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
@ApplicationPath("/root-path")
public class MyApplication extends Application
{
}
Client API
JAX-RS 1.0 was more or less a server side API. To write client calls one would most likely go to Apache’s HTTP Client5. Now JAX-RS 2.0 introduces a new API to make requests to REST web services. I needed such a REST client to make api calls to a Keycloak6 Admin REST API7. Although the current RESTEasy implementation comes with JAX-RS 2.0 support8, I preferred to use the RestEasyClientBuilder
implementation in combination with the Resteasy Proxy Framework7, because I’ve used it like that in JBoss EAP 6 and I still find it cool to use JAX-RS annotations on the client side too. The way it works is that you write a Java interface and use JAX-RS annotations on methods of the interface. Check out the code snippets posted below and the documentation9 to see what I mean
REST Client interface
From
import org.jboss.resteasy.client.ClientResponse;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import java.util.List;
/**
* Client to access the Keycloak admin REST API
*/
public interface KeycloakApiClient {
@POST
@Path("users")
@Consumes(MediaType.APPLICATION_JSON)
ClientResponse<String> createUser(UserRepresentation userRepresentation,
@HeaderParam("Authorization") String authorization);
@GET
@Path("users")
@Produces(MediaType.APPLICATION_JSON)
ClientResponse<List<UserRepresentation>> getUserByEmail(@QueryParam("email") String email,
@HeaderParam("Authorization") String authorization);
...........
}
to
import org.jboss.resteasy.client.ClientResponse;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;
/**
* Client to access the Keycloak admin REST API
*/
public interface KeycloakApiClient {
@POST
@Path("users")
@Consumes(MediaType.APPLICATION_JSON)
Response createUser(UserRepresentation userRepresentation,
@HeaderParam("Authorization") String authorization);
@GET
@Path("users")
@Produces(MediaType.APPLICATION_JSON)
List<UserRepresentation> getUserByEmail(@QueryParam("email") String email,
@HeaderParam("Authorization") String authorization);
...............................
}
REST client producer
From
import org.jboss.resteasy.client.ProxyFactory;
import javax.enterprise.inject.Produces;
public class KeycloakApiClientProducer {
private static final String KEYCLOAK_AUTH_ADMIN_REALMS_TEST_BASE_URL = "http://localhost:8180/auth/admin/realms/podcastpedia";
@Produces
public KeycloakApiClient getKeycloakApiClient(){
return ProxyFactory.create(KeycloakApiClient.class, KEYCLOAK_AUTH_ADMIN_REALMS_TEST_BASE_URL);
}
}
to
import org.jboss.resteasy.client.jaxrs.ResteasyClient;
import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
import org.jboss.resteasy.client.jaxrs.ResteasyWebTarget;
import javax.enterprise.inject.Produces;
public class KeycloakApiClientProducer {
private static final String KEYCLOAK_AUTH_ADMIN_REALMS_TEST_BASE_URL = "http://localhost:8180/auth/admin/realms/podcastpedia";
@Produces
public KeycloakApiClient getKeycloakApiClient(){
ResteasyClientBuilder resteasyClientBuilder = new ResteasyClientBuilder().connectionPoolSize(20);
ResteasyClient client = resteasyClientBuilder.build();
ResteasyWebTarget target = client.target(KEYCLOAK_AUTH_ADMIN_REALMS_TEST_BASE_URL);
return target.proxy(KeycloakApiClient .class);
}
}
A producer method acts as a source of objects to be injected10 in CDI
REST Client Usage
From
@Inject
KeycloakClient keycloakClient;
String keycloakUserId;
ClientResponse<String> userDetailsUpdateResponse = null;
try{
userDetailsUpdateResponse = keycloakApiClient.createUser(userRepresentation, bearerToken);
if(userDetailsUpdateResponse.getResponseStatus() != Response.Status.CREATED) {
ErrorMessage errorMessage = new ErrorMessage.Builder()
.httpStatus(userDetailsUpdateResponse.getResponseStatus().getStatusCode())
.message("Error when trying to create user in Keycloak")
.build();
throw new AppException("Error when trying to create user in Keycloak", errorMessage);
} else {
//get keycloakUserId of the user
String location = userDetailsUpdateResponse.getHeaders().get("Location").get(0);
String[] split = location.split("/");
keycloakUserId = split[split.length - 1];
}
} finally {
if(userDetailsUpdateResponse != null) {
userDetailsUpdateResponse.releaseConnection();
}
}
ClientResponse<UserRepresentation> userDetailsRepresentation = null;
String ldapId = null;
try{
userDetailsRepresentation = keycloakApiClient.getUserDetails(keycloakUserId, bearerToken);
UserRepresentation userDetails = userDetailsRepresentation.getEntity();
if(userDetailsRepresentation.getResponseStatus() != Response.Status.OK) {
ErrorMessage errorMessage = new ErrorMessage.Builder()
.httpStatus(userDetailsRepresentation.getResponseStatus().getStatusCode())
.message("Error when trying to get Keycloak user details")
.build();
throw new AppException("Error when trying to get Keycloak user details", errorMessage);
}
ldapId = userDetails.getAttributesAsListValues().get("LDAP_ID").get(0);
} finally {
if(userDetailsRepresentation !=null) {
userDetailsRepresentation.releaseConnection();
}
}
To
@Inject
KeycloakClient keycloakClient;
.......
String keycloakUserId;
Response userDetailsUpdateResponse = null;
try{
userDetailsUpdateResponse = keycloakApiClient.createUser(userRepresentation, realmAdminBearerToken);
if(userDetailsUpdateResponse.getStatus() != Response.Status.CREATED.getStatusCode()) {
ErrorMessage errorMessage = new ErrorMessage.Builder()
.httpStatus(userDetailsUpdateResponse.getStatus())
.message("Error when trying to create user in Keycloak")
.build();
throw new AppException("Error when trying to create user in Keycloak", errorMessage);
} else {
//get keycloakUserId of the user
String location = (String) userDetailsUpdateResponse.getHeaders().get("Location").get(0);
String[] split = location.split("/");
keycloakUserId = split[split.length - 1];
}
} finally {
userDetailsUpdateResponse.close();
}
UserRepresentation userDetails = keycloakApiClient.getUserDetails(keycloakUserId, realmAdminBearerToken);
Note in the examples above that Resteasy will release the connection under covers. On the other hand, if the result of an invocation is an instance of Response, then Response.close() method must be used to released the connection - the method
Response createUser(UserRepresentation userRepresentation, @HeaderParam("Authorization") String authorization)
and its usage in the To snippet above is a good example
Filters and Interceptors
Logging interceptor
The logging interceptor is used to trace incoming request paths and responses at INFO level and now is implemented with standard JAX RS 2.0 filters - javax.ws.rs.container.ContainerResponseFilter
From
import org.jboss.resteasy.annotations.interception.ServerInterceptor;
import org.jboss.resteasy.core.ResourceMethod;
import org.jboss.resteasy.core.ServerResponse;
import org.jboss.resteasy.spi.Failure;
import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.resteasy.spi.interception.PreProcessInterceptor;
import org.slf4j.Logger;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.ext.Provider;
/**
* Basic logging interceptor
*/
@Provider
@ServerInterceptor
public class LoggingInterceptor implements PreProcessInterceptor {
@Inject
private Logger logger;
@Context
HttpServletRequest servletRequest;
public ServerResponse preProcess(HttpRequest request,
ResourceMethod resourceMethod) throws Failure,
WebApplicationException {
String methodName = resourceMethod.getMethod().getName();
String httpMethod = resourceMethod.getHttpMethods().iterator().next();
logger.info("Receiving " + httpMethod + " request from: " + servletRequest.getRemoteAddr());
logger.info("Attempt to invoke method \"" + methodName + "\"");
return null;
}
}
To
import org.codehaus.jackson.map.ObjectMapper;
import org.slf4j.Logger;
import javax.inject.Inject;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.ext.Provider;
import java.io.IOException;
/**
* Basic logging interceptor for all API calls. Logs in INFO log level the HTTP Method, path and the response entity
*
*/
@Provider
public class LoggingInterceptor implements javax.ws.rs.container.ContainerResponseFilter {
@Inject
private Logger logger;
@Context
private UriInfo uriInfo;
@Override
public void filter(ContainerRequestContext containerRequestContext, ContainerResponseContext containerResponseContext) throws IOException {
String httpMethod = containerRequestContext.getRequest().getMethod();
int httpResponseStatus = containerResponseContext.getStatus();
logger.info(httpMethod.toUpperCase() + " " + uriInfo.getPath() + " " + httpResponseStatus);
ObjectMapper mapper = new ObjectMapper();
logger.info("Response entity: \n"
+ mapper.writerWithDefaultPrettyPrinter().writeValueAsString(containerResponseContext.getEntity())
+ "\n");
}
}
All interceptors and filters registered with the
@Provider
annotation are globally enabled for all resources.
CORS interceptor
Resteasy has a built-in ContainerRequestFilter
that can be used to handle CORS preflight and actual requests - the org.jboss.resteasy.plugins.interceptors.CorsFilter
11. In order to use it, you must allocate this and register it as a singleton provider from your Application
class. See below an example:
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
import java.util.HashSet;
import java.util.Set;
@ApplicationPath("")
public class TestApplication extends Application {
private Set<Object> singletons = new HashSet<Object>();
private HashSet<Class<?>> classes = new HashSet<Class<?>>();
public TestApplication()
{
CorsFilter corsFilter = new CorsFilter();
corsFilter.getAllowedOrigins().add("*");
corsFilter.setAllowedMethods("OPTIONS, GET, POST, DELETE, PUT, PATCH");
singletons.add(corsFilter);
classes.add(ApiResource.class);
classes.add(UsersResource.class);
}
@Override
public Set<Object> getSingletons() {
return singletons;
}
@Override
public HashSet<Class<?>> getClasses(){
return classes;
}
}
If you want to implement a CORS filter yourself and not make you dependent of the RESTEasy framework you can just inspire yourself from the RESTEasy implementation, which, as said, handles CORS12 requests both preflight and simple CORS requests.
Jackson
Besides the Jettison JAXB adapter for JSON, Resteasy also support integration with the Jackson project. Many users (I am one of them) find the output from Jackson much much nicer than the Badger format or Mapped format provided by Jettison. Jackson allows you to easily marshal Java objects to and from JSON. It has a Java Bean based model as well as JAXB like APIs. Resteasy integrates with the JavaBean model.
While Jackson does come with its own JAX-RS integration. Resteasy expanded it a little.To include it within your project, just add this maven dependency to your build. Resteasy supports both Jackson 1.9.x and Jackson 2.2.x. Read further on how to use each.13.
Because of the Keycloak version I am using, 1.7.0.Final still use Jackson Version 1.9.x (apparently the newest one, 2.1.0.Final, also uses the same version), I had to convince the JBoss Server EAP 7 that this is the version I want. To do this I had to import the RestEasy Jackson Provider maven dependency and mark it as provided:
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jackson-provider</artifactId>
<version>3.0.16.Final</version>
<scope>provided</scope>
</dependency>
and also create a jboss-deployment-structure.xml
file within the WEB-INF directory and tell JBoss to exclude the resteasy-jackson2-provider
and import the resteasy-jackson-provider
:
<jboss-deployment-structure>
<deployment>
<exclusions>
<module name="org.jboss.resteasy.resteasy-jackson2-provider"/>
</exclusions>
<dependencies>
<module name="org.jboss.resteasy.resteasy-jackson-provider" services="import"/>
</dependencies>
</deployment>
</jboss-deployment-structure>
Some of the Jackson classes can now be used throughout the application, like for example the ObjectMapper
, to pretty print in JSON a AppException
:
ObjectMapper mapper = new ObjectMapper();
try {
logger.error("Application Exception: \n "
+ mapper.writerWithDefaultPrettyPrinter().writeValueAsString(appException.getErrorMessage())
+ "\n");
} catch (IOException e1) {
logger.error("Error when pretty printing the error message", e1);
}
Keycloak
As mentioned, the Keycloak core uses also Jackson, and to avoid a potential conflicting or undebuggable errors, I force Keycloak to use the Jackson packages that come with the RESTEasy provider via maven exclusions:
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
<version>${keycloak-version}</version>
<scope>provided</scope>
<exclusions>
<exclusion>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-core-asl</artifactId>
</exclusion>
<exclusion>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-mapper-asl</artifactId>
</exclusion>
</exclusions>
</dependency>
Other good REST(easy) related resources
- RESTEasy Documentation examples, HTML, PDF, Javadocs - for all versions…
- Java EE 7 and JAX-RS 2.0 by Adam Bien
- What’s new in JAX-RS 2.0 at InfoQ
- Java EE 7 Deployment Descriptors
References
-
https://www.redhat.com/en/about/press-releases/red-hat-delivers-jboss-eap-7-foundation-hybrid-cloud-applications ↩
-
https://access.redhat.com/documentation/en/red-hat-jboss-enterprise-application-platform/7.0/paged/introduction-to-jboss-eap/chapter-2-overview-of-jboss-eap ↩
-
https://docs.jboss.org/resteasy/docs/3.0.19.Final/userguide/html_single/index.html#d4e42 ↩
-
https://docs.jboss.org/resteasy/docs/3.0.16.Final/userguide/html_single/index.html#RESTEasy_Client_Framework ↩
-
https://docs.jboss.org/resteasy/docs/3.0.16.Final/userguide/html_single/index.html#d4e2149 ↩
-
https://stackoverflow.com/questions/16534728/please-explain-the-produces-annotation-in-cdi ↩
-
https://docs.jboss.org/resteasy/docs/3.0.16.Final/userguide/html_single/index.html#d4e1327 ↩
-
https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS ↩
-
https://docs.jboss.org/resteasy/docs/3.0.16.Final/userguide/html_single/index.html#json ↩