Saturday, February 29. 2020
Testing Spring Boot (Part II)
In the previous entry a simple Spring Boot application was presented. The main goal in the PoC was using Spring Boot but trying to limit the development to JavaEE technologies (servlet and jax-rs) plus some common security and web service libraries (keycloak to secure the endpoints and swagger to document them). The first part left some steps still missing, I wanted to check how easy was moving from one implementation to another (from the default tomcat servlet container to undertow, and from jersey to resteasy jax-rs implementation) and, finally, run the same application inside a wildfly server (check war and jar deployment). This second part of the series deals with those two missing points.
Moving from tomcat to undertow
Spring Boot by default uses tomcat as the embedded web servlet container. So, in the first part, tomcat was used because nothing was modified and the default dependencies were in place. In order to include undertow the tomcat dependency should be excluded from the jersey starter and the undertow one should be added. So now the application only used the following two spring starters.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jersey</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
And it works, the startup logs show that undertow is used at port 8080. So moving from tomcat to undertow is perfect, just switching the starters, undertow is added and tomcat is excluded. Really nice!
Moving from jersey to resteasy
Now let's change the jax-rs implementation from the oracle/eclipse implementation (jersey) to the JBoss one (resteasy). As both are standard jax-rs implementations the change should have been transparent but, as it was commented in the previous entry, the jersey starter is using the private ResourceConfig configuration class. So that will not work for sure with the new implementation. First the jersey started should be substituted with the reasteasy one (jersey was commented out). Only the resteasy and the undertow starter remains and, inside the former, tomcat should also be excluded to maintain undertow working.
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-spring-boot-starter</artifactId>
<version>4.4.0.Final</version>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
Direct compilation doesn't work, because the ResourceConfig is not available in the resteasy implementation. Now the standard Application class should be used. In this case the jersey starter is doing the weird thing, the standard way should also be allowed, but it is not. We need a little change in the code, only in the application definition.
package es.rickyepoderi.springboot.rest;
import io.swagger.v3.jaxrs2.integration.JaxrsOpenApiContextBuilder;
import io.swagger.v3.jaxrs2.integration.resources.OpenApiResource;
import io.swagger.v3.oas.integration.OpenApiConfigurationException;
import io.swagger.v3.oas.integration.SwaggerConfiguration;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import javax.servlet.ServletConfig;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
import javax.ws.rs.core.Context;
//import org.glassfish.jersey.server.ResourceConfig;
import org.springframework.stereotype.Component;
@ApplicationPath("api")
@Component
public class RestApplication extends Application { // extends Application { //extends ResourceConfig {
@Context
private ServletConfig servletConfig;
// it doesn't work with standard Application, why?
//https://stackoverflow.com/questions/47237180/how-to-register-resource-with-javax-ws-rs-core-applicaiton-in-spring-boot-starte
public RestApplication() {
//register(HelloEndpoint.class);
//register(OpenApiResource.class);
//register(RolesAuthorizationFilter.class);
try {
new JaxrsOpenApiContextBuilder()
.servletConfig(servletConfig)
.application(this)
.openApiConfiguration(new SwaggerConfiguration()
.openAPI(new OpenAPI()
.info(new Info()
.title("Hello App")
.description("Sample Spring Boot Hello App")
.version("0.0.1-SNAPSHOT")))
.prettyPrint(true)
.resourcePackages(Collections.singleton(HelloEndpoint.class.getPackage().getName())))
.buildContext(true);
} catch (OpenApiConfigurationException e) {
throw new RuntimeException(e);
}
}
@Override
public Set<Class<?>> getClasses() {
Set<Class<?>> classes = new HashSet<>();
classes.add(HelloEndpoint.class);
classes.add(OpenApiResource.class);
classes.add(RolesAuthorizationFilter.class);
return classes;
}
}
The Application class is used and the resources to integrate are returned in the getClasses method (instead of the register that jersey uses). The jersey configuration was maintained as comments to change between both implementations in my tests. After this change the application can be compiled and everything else works, our little app is running with undertow and resteasy.
Moving to a wildfly server
The last step was moving from the jar bundle that has been used until now to a war deployment inside wildfly. The first change is switching the packaging type in the pom.xml.
<packaging>war</packaging>
If the application is just generated like this the war is very big, with tons of libraries in the WEB-INF/lib directory that are not needed (they are already available inside the wildfly server). A lot of time was spent checking the maven dependencies (mvn dependency:tree -Dverbose and so on and so forth), playing with exclusions and scopes (provided scope is very useful here). I finished with the following pom.xml. Some points to comment:
The resteasy-spring-boot-starter and spring-boot-starter-undertow are defined as provided in order to have them for compilation and avoid the inclusion of their libraries. But, in spite of that, a lot of artifacts should be explicitly excluded in both starters.
As the previous two starters are defined as provided, the default spring-boot-starter-web is added (this way default spring libraries are incorporated to the war). Again a lot of exclusions are listed in order to remove duplicated or not needed artifacts.
The dependency reactive-streams is also needed in the war bundle. It seems that at deploy time wildfly checks for CDI annotations (that some spring classes seem to have) and processing them several NoClassDefFoundError exceptions are thrown (org/reactivestreams/Subscriber or org/reactivestreams/Processor).
<dependency> <groupId>org.reactivestreams</groupId> <artifactId>reactive-streams</artifactId> <version>1.0.3</version> </dependency>
The keycloak filter depends in bouncy castle and http-client, those libraries are inside wildfly but are private modules (they are not exposed by default to applications). I decided to add a dependency for both in the MANIFEST file and exclude those two artifacts.
<plugin> <artifactId>maven-war-plugin</artifactId> <version>3.2.3</version> <configuration> <archive> <manifestEntries> <Dependencies>org.apache.httpcomponents.core,org.bouncycastle</Dependencies> </manifestEntries> </archive> </configuration> </plugin>
Finally, with the final pom.xml, the resulting war file is about 14MB in size (the initial file was more than 30MB!). I suppose that you can strip it even more. But the summary is that a detailed review of the dependencies is needed to change from the standalone jar to the war bundle. This part is painful and necessary to create a correct war for wildfly with a decent size.
Video
The same steps done in the previous video are shown but in a different order. This time the war is deployed inside a wildfly 18. First the bundle is generated and deployed in the server. Then the context is accessed and keycloak intercepts it and requests the user to login. Same as before, user ricky can execute the endpoint but admin cannot. After logout the simple servlet in the application continues working, the endpoint returns 401 if not logged in and swagger keeps giving a correct definition for it. So everything is working again, but this time inside the wildfly JavaEE container.
In my humble opinion Spring Boot gives a good support for interchanging implementations (tomcat or undertow, jersey or resteasy, standalone or container deployment). There are several snags, jersey using a private initialization class (this issue is something very specific to this starter) or the mess with dependencies when deploying inside the container. But I think it is almost impossible to do it seamlessly, some minor changes are going to be needed for sure. I like the Spring Boot idea. That is all for today. The final project deployed inside wildfly can be downloaded from here.
Regards!
Comments