Building an injectable IoC centric, small and lightweight HTTP server and making it runnable as a native executable
CDI (Contexts and Dependency Injection) is an IoC (Inversion of Control) container, that is implemented in Apache OWB (OpenWebBeans). Even though there is an existing micro HTTP server that supports CDI capabilities in the Apache world (Meecrowave), it is not IoC centric and it has a lot more features than the basics. Furthermore, it is not runnable as a native executable – there’s always the big JVM necessary. The proposal was to build a much slimmer HTTP API and implement it in a project with CDI (OpenWebBeans) as the starting point and webserver components (Netty) on top. This can then be injected as a managed bean in the web application. It would be an even more minimalistic microserver and IoC centric. Making this then natively runnable as a native executable for Windows is delivering important work for future JakartaEE projects within Apache, that need to be turned into a native image (via GraalVM). Before this GSOC project, there was little to none such experience in OWB.
For the project three separate repositories were created:
- A repository for small example projects, that were important to know for the main work on GSOC: https://github.com/a-rekkusu/servlet-examples
- The Http Microserver with API, implementation and showcase with native config, which was merged (see the Pull Request here) to a dedicated ASF repo: https://github.com/apache/openwebbeans-peeco
- GraalVM native-image example with CDI usage and BuildTimeProxyGeneration: https://github.com/a-rekkusu/graalvm-native-image-example
In the initial work period I got to know several technologies around this topic and used them in small sample projects: Maven, Netty, CDI/OWB, Servlets/Tomcat and CDI extensions. The repository with the separate sample projects can be found here.
As the requested HTTP server should be even slimmer and more basic than i. e. Meecrowave, we settled on the simple name Peeco (as pico < micro) for now, but that might change later.
The checked boxes were the successfully implemented features. The unchecked boxes could not be finished within the project time.
- basic HTTP API definition for writing and handling requests/responses
- marry this API, CDI/OWB and Netty
- implement basic HTTP functionalities: GET and POST requests work
- exact and wildcard url matching
- reactive request/response behavior with CompletionStage
- make the server configurable with a builder pattern
- deposit the code in a dedicated ASF repository
- enable file upload
- enable chunking
- server configuration with properties file
Here's an example for how to configure and build this server:
public HttpServer init()
{
return new HttpServer.Builder()
.port(0) //applies a random valid port
.ssl(false)
.host("localhost")
.build();
}
And here's how you can use the API with the HttpHandler
annotation:
@Inject private HttpServer httpServer;
@HttpHandler(method = {HttpMethod.GET}, url = "/hello/*", matching = Matching.WILDCARD)
public CompletionStage<Response> helloWorld(Request request)
{
String responseContent = "Hello World from " + httpServer.getHost() + ":" + httpServer.getPort() + " !";
ByteArrayInputStream output = new ByteArrayInputStream(responseContent.getBytes(StandardCharsets.UTF_8));
return CompletableFuture.supplyAsync(() ->
{
Response response = new Response();
response.addHeader("statusCode", "200");
response.addHeader("content-type", "text/html");
response.setOutput(output);
return response;
});
}
This is a reactive example with CompletionStage; you can also straight up use Response
as the method return type.
The formal requirements of a valid handler method and annotation are:
- return type must be
CompletionStage<Response>
orResponse
- first method parameter must be
Request
Matching.WILDCARD
needs a star at the end of the given url
For more examples please refer to the respective showcase. The implementation is found here.
PeecoExtension.java
collects all methods that are annotated with our @HttpHandler
and starts up the Netty Server.
The flow of the request/response handling is then happening in PeecoChannelHandler.java
.
After the server was usable, next up was making it runnable as a native-image.
The work progress on the tasks is as followed after the GSOC deadline:
- setup native-image building and build simple HelloWorld app
- build simple native-image with CDI and proxy usage
- build runnable native-image from server-showcase module
Unfortunately, the showcase was not running natively at the end of the project. As GraalVM native-image offers lots of configuration and mistake opportunities, the project time just wasn't quite enough to finish it. The main native-image problems in the end were Logging Frameworks, Netty and build-time vs. runtime instantiation, as well as reflectable availability in the native-image.
The first obstacle was setting up the correct developing environment on my machine with Windows 10, so the GraalVM native-image
command would compile correctly.
I only managed to make it work by downloading and installing the
Windows 7.1 SDK
(specifically the GRMSDKX_EN_DVD.iso
). After the installation, I had the Windows 7.1 SDK Command Prompt available, through which the native-image
command from GraalVM worked successfully and gave me a simple HelloWorld.exe
out of a .jar
file. Also, a basic installation of Visual Studio 2017 (C++ build tools workload) or later with its x64 Native Tools Command Prompt might be necessary. For instructions on a Linux machine, refer to the information provided on the GraalVM website.
Next up was enabling OWB to generate its proxy classes at build-time, as the regular runtime-generation cannot be done at native-image runtime. Hence, the proxy classes must already be created and available in the classpath at runtime. This can be achieved by using OWB's ClassLoaderProxyService.Spy to generate at build-time (like so), and ClassLoaderProxyService.LoadFirst at runtime like so.
In order to build a native-image, some special resources are needed in the .jar
, namely for configuring the use cases of reflection, dynamic proxies, jni and other resources.
These files can be generated by using a command on the .jar
file:
java -agentlib:native-image-agent=config-output-dir=OUTPUT_PATH (-classpath optional.jar) -jar myJar.jar
Merging updated config files is also possible, by stating config-merge-dir
instead of config-output-dir
:
VVVVV
java -agentlib:native-image-agent=config-merge-dir=OUTPUT_PATH (-classpath optional.jar) -jar myJar.jar
The resulting config files might have to be manually corrected, as some execution paths may be missing.
It is recommended to place the config files under resources/META-INF/native-image
, as the native-image
command searches here by default.
Then package this again and build it from the Windows 7.1 SDK Command Prompt with:
native-image --no-fallback -jar myJar.jar
The --no-fallback
argument prevents the image to create a fallback image that's basically using the JVM again in case the config files are not correct. This way we get a true native-image or none at all, if it fails.
Before the showcase, we started with a basic graal-vm native-image example project, that used OWB. To test the proxying, this was the workflow:
- generate proxy class and deposit it in a given path (we toggled that manually with a separate
static void main(String[] args)
) - add the generated proxy class manually into the
.jar
withjar uf myJar.jar package/proxyClass.class
- run the
.jar
in the console with the-agentlib
command described before, for the auto-generation of GraalVM config files, with themainClass
accessing the proxy class - re-package the module with the new config files, re-add the proxy classes, re-test its runnability in console
- build
native-image
- run the native image
.exe
in console
Some problems and difficulties arose in that workflow - the app wouldn't run in the console (we had to remove the MANIFEST.MF
file from the .jar
and run the mainClass
itself to generate our native-image config files) and some classes were missing in the auto-generated config files, for example.
Also, a resolution error came from the CDI container, as multiple beans were found for one class (the class itself and its proxy class). Therefore, all proxy classes should have the @Vetoed
annotation, so they are not being scanned at runtime, as they are already available. This Pull Request applied this change in the openwebbeans-impl.
With the @Vetoed
proxy class, finally, the example project worked as it should in native.
The server showcase was a bit more complex, as it had more dependencies, mainly because of Netty. This meant that the quite timely workflow from before had to be run more often, and it took even longer as the bigger components affected the native-image buildtime, of course. Applying possible fixes to exceptions and errors at various stages in the workflow often means running through that whole workflow. It seemed most promising to run the native image command with the following arguments like so:
native-image --no-fallback --initialize-at-run-time=<classes or packages> --initialize-at-build-time=<classes or packages> --report-unsupported-elements-at-runtime -H:+TraceClassInitialization --allow-incomplete-classpath --class-path (all dependencies) -jar peeco-showcase.jar
Also, researching took away a lot of the time here, as the native-image command has a massive documentation from GraalVM and lots of possibly helpful blog entries on the web. Unfortunately, the time was just not enough to master it all in the end. Maybe some code substitution regarding Netty will solve the current problem, as proposed in this blog entry.
Even though I was not able to finish every last thing in time, not much was missing in the end. I learned lots of new technologies and how they work. The project threw me into state-of-the-art java development with web driven technologies such as servlets, CDI, microservers and native images, as well as teached me a more basic understanding of the java world in exposing me to bytecode enhancement, proxy class generations, JakartaEE and maven project structures. I'm positive that this broad sense of understanding will be quite valuable in my professional future.
My mentors were very helpful throughout the whole program timeline. They were very responsive and didn't give away too much of the solutions, most of the time, and instead just gave hints and directions in which to research and try further. When necessary, we would meet up in person and go through problems together. I couldn't really think of anything to improve here.
I intend to stay active in the open source community and finish the remaining open points of the project eventually.