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 |
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();
}
} |
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();
}
} |
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;
}
} |
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();
}
} |
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 |
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-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());
}
} |
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:
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 |
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 |
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");
}
} |
/**
* 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<string, string=""> 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<string, object=""> headers, OutputStream outstream) throws IOException, WebApplicationException { try (OutputStreamWriter writer = new OutputStreamWriter(outstream, UTF_8)) { final String content = gson.toJson(obj); writer.write(content); } }} |
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<string, string=""> 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<string, object=""> 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.