Cooking with meecrowave

What: Building microservices in Java(9) painlessly
Why: Small and maintainable services
How: Using meecrowave

Introduction

Creating microservices in Java can be quite complicated. You either do it by yourself using the Java internal HTTP-Server, using one of the many application servers or using one of the integrated frameworks like wildfly-swarm. While the first option doesn’t work well if you want to use something like dependency injection and you have to include all useful libraries like jax-rs by yourself, the second option already contains the most parts of it. You can use for example Glassfish, Wildfly, Websphere or Tomcat (and TomEE). Nevertheless, you rely on heavy application servers and you have to start an instance of these and deploy for each testing (although there exists some integration solutions into IDEs).

The integrated frameworks are sometimes huge, need extended configuration or doesn’t play well with Java9. Partly testing is not as easy as it should be (dependency injection is one of the issues).

Meecrowave on the other hand is a small framework which works well with CDI, JAX-RS and Jackson out of the box which is super easy to set up and performing integration tests is as easy as starting a JUnit test. The following tutorial shows an easy example.

The source code for this tutorial is available here (folder micro).

Note: Although the example runs with Java9 it is not modularized. Some of the dependencies are not yet available as Java9 modules and thus creating this example as a module is outof scope for this tutorial).

Setup

In the following, maven and jdk9 (both for compiling and running) is used.

Add the following dependencies to your pom.xml to include the needed libraries for this example.

1
2
3
	org.apache.meecrowave
	meecrowave-core
	1.2.0

Server

Starting meecrowave is simple: Just start the meecrowave server. All the rest like scanning classes for endpoints, … is done automatically. Create a class with a main method and add the following code:

1
2
3
4
5
6
public static void main(String[] args) {
	try (final Meecrowave meecrowave = new Meecrowave();final Scanner scanner=new Scanner(System.in);) {
		meecrowave.bake();
	    scanner.nextLine();
	}
}

If you start the class, you should see some printout and meecrowave is up and running. To start it from Java9 you have to add –add-modules java.xml.bind as argument to the virtual machine.

Note: The main class is not needed at all for running it outside of ides(at least not on Linux machines) since meecrowave can create a whole distribution package (see below).

You should see output like:

[09:56:57.591][INFO ][           main][.webbeans.config.BeansDeployer] All injection points were validated successfully.
[09:56:57.904][INFO ][           main][apache.cxf.endpoint.ServerImpl] Setting the server's publish address to be /
[09:56:57.959][INFO ][           main][ifecycle.WebContainerLifecycle] OpenWebBeans Container has started, it took [694] ms.
[09:56:58.119][WARN ][           main][na.util.SessionIdGeneratorBase] Creation of SecureRandom instance for session ID generation using [SHA1PRNG] took [120] milliseconds.
[09:56:58.164][INFO ][           main][meecrowave.cxf.CxfCdiAutoSetup] REST Application: / -> org.apache.cxf.cdi.DefaultApplication
[09:56:58.164][INFO ][           main][meecrowave.cxf.CxfCdiAutoSetup]      Service URI: /test  -> de.moduliertersingvogel.micro.SimpleEndpoint
[09:56:58.169][INFO ][           main][meecrowave.cxf.CxfCdiAutoSetup]               GET /test/ ->      Response test()

JAX-RS endpoints

You can create arbitrary endpoints based on the jax-rs annotations. Each endpoint needs to be annotated with a Path and scope annotation. The following example defines a simple endpoint returning the string „Hello World“:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package de.moduliertersingvogel.micro;
 
import javax.enterprise.context.RequestScoped;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.Response;
 
@RequestScoped
@Path("test")
public class SimpleEndpoint {
	@GET
	public Response test() {
		return Response.ok().entity("Hello World").build();
	}
}

You can point your browser to http://localhost:8080/test and see the result.

Dependency injection

Dependency injection works like expected. You need a class, which is annotated with the scope and inject it somewhere. Lets test it with a simple object:

1
2
3
4
5
6
7
8
9
10
package de.moduliertersingvogel.micro;
 
import javax.enterprise.context.ApplicationScoped;
 
@ApplicationScoped
public class SimpleObject {
	public boolean callMe() {
		return true;
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
 
@RequestScoped
@Path("test")
public class SimpleEndpoint {
	@Inject
	SimpleObject obj;
 
	@GET
	public Response test() {
		if(obj.callMe()) {
			return Response.ok().entity("Hello World").build();
		}
		return Response.status(Status.BAD_REQUEST).entity("Something went wrong").build();
	}
}

Run your main class and check in yout browser (see above) to see that everything works fine.

Testing

Testing in meecrowave is as simple as writing (annotated) unit tests. In order to get it working, you have to add the following dependencies to your pom.xml (okhttp is used for getting the result from the running microservice):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
	org.apache.meecrowave
	meecrowave-junit
	1.2.0
	test
 
 
	org.junit.jupiter
	junit-jupiter-api
	5.0.2
	test
 
 
	org.junit.jupiter
	junit-jupiter-engine
	5.0.2
	test
 
 
	com.squareup.okhttp3
	okhttp
	3.9.1

Additionally, you need some tweaking to get the tests working with Java9. Add the following line to the properties section in the pom.xml:

1
--add-modules java.xml.bind

Add the following test class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package de.moduliertersingvogel.micro;
 
import static org.junit.jupiter.api.Assertions.assertEquals;
 
import org.apache.meecrowave.Meecrowave;
import org.apache.meecrowave.junit5.MeecrowaveConfig;
import org.apache.meecrowave.testing.ConfigurationInject;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
 
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;
 
@MeecrowaveConfig /*(some config)*/
public class SimpleEndpointTest {
    @ConfigurationInject
    private Meecrowave.Builder config;
	private static OkHttpClient client;
 
	@BeforeAll
	public static void setup() {
		client = new OkHttpClient();
	}
 
	@Test
	public void test() throws Exception {
		final String base = "http://localhost:" + config.getHttpPort();
 
		Request request = new Request.Builder()
	      .url(base+"/test")
	      .build();
		Response response = client.newCall(request).execute();
		assertEquals("Hello World",  response.body().string());
	}
}

And run it either from your IDE or from maven:

1
mvn clean test

During test execution the server should be started and the tests should be executed successfully against your running meecrowave application should be performed:

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.8 sec - in de.moduliertersingvogel.micro.SimpleEndpointTest

Distribution

Creating a distribution package (for Linux) is as simple as adding a new goal to the maven call:

1
mvn clean package meecrowave:bundle

Note: The meecrowave:bundle goal creates the distribution and includes what is already in compiled as jar in target directory. A call to meecrowave:bundle without package would result in an empty meecrowave server without your application.

After that, your target directory should contain a file called micro-meecrowave-distribution.zip. The zip archive contains a bin folder in which the executable (….sh) is located. For running this in Java9, the java.xml.bind module needs too be added (remember: We are not using modules here, therefore no module-info and no automatic way for Java to figure this out). Search the line strting with JAVA_OPTS in the start script and add:

--add-modules java.xml.bind

Now you can start the service by:

1
2
cd bin
./meecrowave.sh start

The default port used is 8080 and thus you can test your application easily with curl or the browser. Enjoy!

PS: Cors

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
 * See: http://stackoverflow.com/a/28067653
 *
 */
@ApplicationScoped
@Provider
public class CorsFilter implements ContainerResponseFilter {
    @Override
    public void filter(ContainerRequestContext request, ContainerResponseContext response) throws IOException {
        response.getHeaders().add("Access-Control-Allow-Origin", "*");
        response.getHeaders().add("Access-Control-Allow-Headers", "origin, content-type, accept, authorization");
        response.getHeaders().add("Access-Control-Allow-Credentials", "true");
        response.getHeaders().add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, HEAD");
    }
}

PPS: GSON

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.lang.reflect.Type;
 
import javax.enterprise.context.ApplicationScoped;
import javax.ws.rs.Consumes;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.MessageBodyReader;
import javax.ws.rs.ext.MessageBodyWriter;
import javax.ws.rs.ext.Provider;
 
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
 
@ApplicationScoped
@Provider
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class GsonMessageBodyHandler implements MessageBodyWriter<object width="300" height="150">, MessageBodyReader<object> {	private static final String UTF_8 = "UTF-8";	private Gson gson = new GsonBuilder().create();	@Override	public boolean isReadable(Class<!--?--> type, Type genericType, java.lang.annotation.Annotation[] annotations,			MediaType mediaType) {		return true;	}	@Override	public Object readFrom(Class<object> clazz, Type type, java.lang.annotation.Annotation[] annotations,			MediaType mediatype, MultivaluedMap&lt;string, string=""&gt; headers, InputStream instream)			throws IOException, WebApplicationException {		try (InputStreamReader streamReader = new InputStreamReader(instream, UTF_8)) {			return gson.fromJson(streamReader, type);		}	}	@Override	public boolean isWriteable(Class<!--?--> arg0, Type arg1, java.lang.annotation.Annotation[] arg2, MediaType arg3) {		return true;	}	@Override	public void writeTo(Object obj, Class<!--?--> clazz, Type type, java.lang.annotation.Annotation[] annotations,			MediaType mediatype, MultivaluedMap&lt;string, object=""&gt; headers, OutputStream outstream)			throws IOException, WebApplicationException {		try (OutputStreamWriter writer = new OutputStreamWriter(outstream, UTF_8)) {			final String content = gson.toJson(obj);			writer.write(content);		}	}}

Update 20200331:Logging

There seems to be a problem in the generated meecrowave.bat file for starting in Windows. It does not include the log4j2 configuration file. The meecrowave.sh for Linux is working. As a workaround the Windows bat file can be edited manually.