The Microservice Example
The microservice example is an OSGi enRoute application created from the enRoute project bare archetype. The microservice example contains a REST endpoint which talks to a data access service for persistence. The application project then packages up these services and their dependencies into a runnable JAR file.
There are two choices for the persistence service implementation used in this example, one using JDBC and one using JPA. This page describes the front end of the application. The back-end persistence services and application projects are described in
The microservice example project has the following layout:
$ pwd
~/microservice
$ ls
dao-api dao-impl rest-app rest-service
dao-impl-jpa rest-app-jpa rest-service-test pom.xml
The Reactor POM
The reactor POM is responsible for setting common configuration for the build plugins used by modules in the build, and for setting the scopes and versions of common dependencies.
As the enRoute example projects all live in a single workspace each of their reactor poms inherit configuration from this root reactor. In scenarios where application projects have their own dedicated workspaces, then the following items would be included directly in each of their reactor poms.
The root reactor pom defines configuration for the bnd plugins used by enRoute, and the following common dependencies.
APIs
The OSGi and Java EE APIs commonly used in OSGi enRoute applications are included at provided scope. This is because they should not be used at runtime, instead being provided by implementations, such as the OSGi framework.
Implementations
The OSGi reference implementations used in OSGi enRoute are included at runtime scope so that they are eligible to be selected for use by the application at runtime, but not available to compile against.
Debug and Test
The remaining dependencies are made available at test scope so that they may be used when unit testing, integration testing, or debugging OSGi enRoute applications.
The DAO API Module
The DAO API module contains the API for the data access service. The packages contain the service interfaces and the Data Transfer Objects used to pass data between the service client and implementation.
The API packages
You may have noticed that both of the api packages contain package-info.java
files. These files look like this:
@org.osgi.annotation.bundle.Export
@org.osgi.annotation.versioning.Version("1.0.0")
package org.osgi.enroute.examples.microservice.dao;
@org.osgi.annotation.bundle.Export
@org.osgi.annotation.versioning.Version("1.0.0")
package org.osgi.enroute.examples.microservice.dao.dto;
The annotations of these packages indicate that they should:
- be exported from the bundle
- be versioned at version
1.0.0
Defining API roles
When an API package defines a service interface it is important to consider the role of the interface. Is the interface designed to be implemented by a provider, or by a consumer?
If you’re confused it can be worth thinking about the Java Servlet API, where we have the Servlet
interface and the ServletRequest
interface. The Servlet
interface is designed to be implemented by consumers - most web applications will implement this interface lots of times. The ServletRequest
interface, however, is designed to only be implemented by providers, such as Eclipse Jetty or Apache TomCat. If a method is added to ServletRequest
then only the providers need to be updated (this happens with each new release of the Servlet specification) but if a new method were added to Servlet
(which has never happened) then it would break all the people using Servlets.
In this case the API interfaces are all designed to be implemented by the provider of the service, so both are annotated with @ProviderType
package org.osgi.enroute.examples.microservice.dao;
import java.util.List;
import org.osgi.annotation.versioning.ProviderType;
import org.osgi.enroute.examples.microservice.dao.dto.PersonDTO;
@ProviderType
public interface PersonDao {
public List<PersonDTO> select();
public PersonDTO findByPK(Long pk) ;
public Long save(PersonDTO data);
public void update(PersonDTO data);
public void delete(Long pk) ;
}
package org.osgi.enroute.examples.microservice.dao;
import java.util.List;
import org.osgi.annotation.versioning.ProviderType;
import org.osgi.enroute.examples.microservice.dao.dto.AddressDTO;
@ProviderType
public interface AddressDao {
public List<AddressDTO> select(Long personId);
public AddressDTO findByPK(String emailAddress);
public void save(Long personId,AddressDTO data);
public void update(Long personId,AddressDTO data);
public void delete(Long personId) ;
}
The POM
The pom.xml is simple, it includes the OSGi API, which provides the annotations described above, it activates the bnd-maven-plugin
which will generate the OSGi manifest, and the bnd-baseline-maven-plugin
which ensures that future versions of the API use correct semantic versions.
The REST Service Module
The REST module contains the REST layer of the application. It contains two declarative services components in src/main/java
, and a unit test in src/test/java
. The src/main/resources
folder contains files contributing a Web User Interface for the component.
The POM
The pom.xml is simple, it includes the OSGi API, enterprise API and testing dependencies required by the module, and it activates the bnd-maven-plugin
which will generate the OSGi manifest, and a Declarative Service descriptor for the component.
The DS REST Component
The DS REST component contains a number of important annotations.
package org.osgi.enroute.examples.microservice.rest;
import java.util.List;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import org.osgi.enroute.examples.microservice.dao.PersonDao;
import org.osgi.enroute.examples.microservice.dao.dto.PersonDTO;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.http.whiteboard.propertytypes.HttpWhiteboardResource;
import org.osgi.service.jaxrs.whiteboard.propertytypes.JSONRequired;
import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource;
@Component(service=RestComponentImpl.class)
@JaxrsResource
@Path("person")
@Produces(MediaType.APPLICATION_JSON)
@JSONRequired
@HttpWhiteboardResource(pattern="/microservice/*", prefix="static")
public class RestComponentImpl {
@Reference
private PersonDao personDao;
@GET
@Path("{person}")
public PersonDTO getPerson(@PathParam("person") Long personId) {
return personDao.findByPK(personId);
}
@GET
public List<PersonDTO> getPerson() {
return personDao.select();
}
@DELETE
@Path("{person}")
public boolean deletePerson(@PathParam("person") long personId) {
personDao.delete(personId);
return true;
}
@POST
public PersonDTO postPerson(PersonDTO person) {
if (person.personId > 0) {
personDao.update(person);
return person;
}
else {
long id = personDao.save(person);
person.personId = id;
return person;
}
}
}
-
@Component - This annotation indicates that
RestComponentImpl
is a Declarative Services component. Theservice
attribute means that even thoughRestComponentImpl
does not directly implement any interfaces it will still be registered as a service. The@Component
annotation also acts as a runtime Requirement; prompting the host OSGi framework to automatically load a Declarative Services implementation. -
@JaxrsResource - This annotation marks the
RestComponentImpl
service as a JAX-RS resource type that should be processed by the JAX-RS whiteboard. It also acts as a runtime Requirement; prompting the host OSGi framework to automatically load a JAX-RS Whiteboard implementation. -
@JSONRequired - This annotation marks the component as requiring a serializer capable of supporting JSON. The service declares that it produces JSON in the @Produces annotation, but this can only work if a suitable implementation is available at runtime. This annotation also acts as a runtime Requirement; prompting the host OSGi framework to automatically load a JAX-RS Whiteboard extension that can support JSON serialization.
-
@HttpWhiteboardResource - This annotation indicates to the Http Whiteboard that the
Upper
bundle contains one or more static files which should be served over HTTP. The pattern attribute indicates the URI request patterns that should be mapped to resources from the bundle, while the prefix attribute indicates the folder within the bundle where the resources can be found.
The @Path, @Produces, @GET, @POST, @DELETE and @PathParam annotations are defined by JAX-RS, and used to map incoming requests to the resource methods.
The DS JSON Serializer
The DS JSON Serializer component contains a number of important annotations.
package org.osgi.enroute.examples.microservice.rest;
import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE;
import static org.osgi.service.component.annotations.ServiceScope.PROTOTYPE;
import static org.osgi.util.converter.ConverterFunction.CANNOT_HANDLE;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import javax.json.Json;
import javax.json.JsonArray;
import javax.json.JsonArrayBuilder;
import javax.json.JsonNumber;
import javax.json.JsonObject;
import javax.json.JsonObjectBuilder;
import javax.json.JsonReader;
import javax.json.JsonString;
import javax.json.JsonStructure;
import javax.json.JsonValue;
import javax.json.JsonValue.ValueType;
import javax.json.JsonWriter;
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 org.osgi.service.component.annotations.Component;
import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsExtension;
import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsMediaType;
import org.osgi.util.converter.Converter;
import org.osgi.util.converter.Converters;
import org.osgi.util.converter.TypeReference;
@Component(scope = PROTOTYPE)
@JaxrsExtension
@JaxrsMediaType(APPLICATION_JSON)
public class JsonpConvertingPlugin<T> implements MessageBodyReader<T>, MessageBodyWriter<T> {
private final Converter converter = Converters.newConverterBuilder()
.rule(JsonValue.class, this::toJsonValue)
.rule(this::toScalar)
.build();
private JsonValue toJsonValue(Object value, Type targetType) {
if (value == null) {
return JsonValue.NULL;
} else if (value instanceof String) {
return Json.createValue(value.toString());
} else if (value instanceof Boolean) {
return ((Boolean) value) ? JsonValue.TRUE : JsonValue.FALSE;
} else if (value instanceof Number) {
Number n = (Number) value;
if (value instanceof Float || value instanceof Double) {
return Json.createValue(n.doubleValue());
} else if (value instanceof BigDecimal) {
return Json.createValue((BigDecimal) value);
} else if (value instanceof BigInteger) {
return Json.createValue((BigInteger) value);
} else {
return Json.createValue(n.longValue());
}
} else if (value instanceof Collection || value.getClass().isArray()) {
return toJsonArray(value);
} else {
return toJsonObject(value);
}
}
private JsonArray toJsonArray(Object o) {
List<?> l = converter.convert(o).to(List.class);
JsonArrayBuilder builder = Json.createArrayBuilder();
l.forEach(v -> builder.add(toJsonValue(v, JsonValue.class)));
return builder.build();
}
private JsonObject toJsonObject(Object o) {
Map<String, Object> m = converter.convert(o).to(new TypeReference<Map<String, Object>>(){});
JsonObjectBuilder jsonBuilder = Json.createObjectBuilder();
m.entrySet().stream().forEach(e -> jsonBuilder.add(e.getKey(), toJsonValue(e.getValue(), JsonValue.class)));
return jsonBuilder.build();
}
private Object toScalar(Object o, Type t) {
if (o instanceof JsonNumber) {
JsonNumber jn = (JsonNumber) o;
return converter.convert(jn.bigDecimalValue()).to(t);
} else if (o instanceof JsonString) {
JsonString js = (JsonString) o;
return converter.convert(js.getString()).to(t);
} else if (o instanceof JsonValue) {
JsonValue jv = (JsonValue) o;
if (jv.getValueType() == ValueType.NULL) {
return null;
} else if (jv.getValueType() == ValueType.TRUE) {
return converter.convert(Boolean.TRUE).to(t);
} else if (jv.getValueType() == ValueType.FALSE) {
return converter.convert(Boolean.FALSE).to(t);
}
}
return CANNOT_HANDLE;
}
@Override
public boolean isWriteable(Class<?> c, Type t, Annotation[] a, MediaType mediaType) {
return APPLICATION_JSON_TYPE.isCompatible(mediaType) || mediaType.getSubtype().endsWith("+json");
}
@Override
public boolean isReadable(Class<?> c, Type t, Annotation[] a, MediaType mediaType) {
return APPLICATION_JSON_TYPE.isCompatible(mediaType) || mediaType.getSubtype().endsWith("+json");
}
@Override
public void writeTo(T o, Class<?> arg1, Type arg2, Annotation[] arg3, MediaType arg4,
MultivaluedMap<String, java.lang.Object> arg5, OutputStream out)
throws IOException, WebApplicationException {
JsonValue jv = converter.convert(o).to(JsonValue.class);
try (JsonWriter jw = Json.createWriter(out)) {
jw.write(jv);
}
}
@SuppressWarnings("unchecked")
@Override
public T readFrom(Class<T> arg0, Type arg1, Annotation[] arg2, MediaType arg3, MultivaluedMap<String, String> arg4,
InputStream in) throws IOException, WebApplicationException {
try (JsonReader jr = Json.createReader(in)) {
JsonStructure read = jr.read();
return (T) converter.convert(read).to(arg1);
}
}
}
-
@Component - This annotation indicates that
JsonpConvertingPlugin
is a Declarative Services component. Note that unlike theRestComponentImpl
this component does implement interfaces and will be automatically registered as a service using theMessageBodyReader
andMessageBodyWriter
interfaces. Also the@Component
annotation declares this component to bePROTOTYPE
scope - this means that the person using the service can ask for multiple separate instances, and is recommended for all JAX-RS extensions. -
@JaxrsExtension - This annotation marks the
JsonpConvertingPlugin
service as a JAX-RS extension type that should be processed by the JAX-RS whiteboard. -
@JaxrsMediaType - This annotation marks the component as providing a serializer capable of supporting the named media type, in this case the standard media type for JSON.
The DS JSON Serializer Implementation
The implementation of the JsonpConvertingPlugin
makes use of two specifications - one is JSON-P, a JSON parser and emitter, the other is the OSGi Converter.
The OSGi Converter is a useful utility that can convert objects from one type to another. It contains many standard conversions, however in this case we use a pair of custom rules.
We use the first rule to teach the converter how to turn the DTOs from our API into JSON-P JsonValue
instances. depending on the type of the incoming object we pick the appropriate JSON type to create - if the incoming object is a complex value we recursively convert the types that make it up. The resulting JsonValue
can then be easily serialized to a JSON string.
It turns out that the reverse mapping is even easier - the second rule is used to convert a JsonStructure
into a DTO. A JSONStructure
is either a JsonArray
, a JsonObject
or a JsonValue
. A JsonArray
is a List
of JsonStructure
objects and a JsonObject
is a Map
of String
to JsonStructure
objects, therefore the converter can natively handle these types as it would any list or map type. All that remains is teaching the converter how to handle JsonValue
which is easily achieved by transforming to the implicit java type and calling the converter again!
The Unit Test
The unit test makes use of JUnit 4 to perform a basic test on the JsonpConvertingPlugin
. This test is why the Johnzon implementation is needed as a test
scope dependency.
The REST Service Test
The rest-service-test
component is generated from enRoute’s bundle-test archetype. Rather than creating a bundle for use in the application this project uses the bnd-testing-maven-plugin to test the rest service bundle.
The Test Case
The REST service test cases are written using JUnit 4
RestServiceIntegrationTest.java
package org.osgi.enroute.examples.microservice.rest.service.test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.Mockito.mock;
import static org.osgi.namespace.service.ServiceNamespace.SERVICE_NAMESPACE;
import static org.osgi.service.jaxrs.runtime.JaxrsServiceRuntimeConstants.JAX_RS_SERVICE_ENDPOINT;
import java.util.Collections;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.WebTarget;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.osgi.annotation.bundle.Capability;
import org.osgi.enroute.examples.microservice.dao.PersonDao;
import org.osgi.enroute.examples.microservice.dao.dto.PersonDTO;
import org.osgi.framework.Bundle;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.jaxrs.runtime.JaxrsServiceRuntime;
import org.osgi.util.converter.Converters;
import org.osgi.util.tracker.ServiceTracker;
@Capability(namespace=SERVICE_NAMESPACE,
attribute="objectClass=org.osgi.enroute.examples.microservice.dao.PersonDao")
public class RestServiceIntegrationTest {
private final Bundle bundle = FrameworkUtil.getBundle(this.getClass());
private PersonDao mockDAO;
private ServiceRegistration<PersonDao> registration;
private ServiceTracker<JaxrsServiceRuntime, JaxrsServiceRuntime> runtimeTracker;
private ServiceTracker<ClientBuilder, ClientBuilder> clientTracker;
private JaxrsServiceRuntime jaxrsServiceRuntime;
private Client client;
@Before
public void setUp() throws Exception {
assertNotNull("OSGi Bundle tests must be run inside an OSGi framework", bundle);
mockDAO = mock(PersonDao.class);
runtimeTracker = new ServiceTracker<>(bundle.getBundleContext(), JaxrsServiceRuntime.class, null);
runtimeTracker.open();
clientTracker = new ServiceTracker<>(bundle.getBundleContext(), ClientBuilder.class, null);
clientTracker.open();
jaxrsServiceRuntime = runtimeTracker.waitForService(2000);
assertNotNull(jaxrsServiceRuntime);
ClientBuilder cb = clientTracker.getService();
assertNotNull(cb);
client = cb.build();
}
@After
public void tearDown() throws Exception {
runtimeTracker.close();
clientTracker.close();
if(registration != null) {
registration.unregister();
}
}
private void registerDao() {
registration = bundle.getBundleContext().registerService(PersonDao.class, mockDAO, null);
}
@Test
public void testRestServiceRegistered() throws Exception {
assertEquals(0, jaxrsServiceRuntime.getRuntimeDTO().defaultApplication.resourceDTOs.length);
registerDao();
assertEquals(1, jaxrsServiceRuntime.getRuntimeDTO().defaultApplication.resourceDTOs.length);
}
@Test
public void testGetPerson() throws Exception {
registerDao();
// Set up a Base URI
String base = Converters.standardConverter().convert(
runtimeTracker.getServiceReference().getProperty(JAX_RS_SERVICE_ENDPOINT)).to(String.class);
WebTarget target = client.target(base);
// There should be no results in the answer
assertEquals("[]", target.path("person")
.request()
.get(String.class));
// Add a person to the DAO
PersonDTO dto = new PersonDTO();
dto.firstName = "Fizz";
dto.lastName = "Buzz";
dto.personId = 42;
dto.addresses = Collections.emptyList();
Mockito.when(mockDAO.select()).thenReturn(Collections.singletonList(dto));
// We should get back the person in the answer
assertEquals("[{\"firstName\":\"Fizz\",\"lastName\":\"Buzz\",\"addresses\":[],\"personId\":42}]",
target.path("person")
.request()
.get(String.class));
}
}
Note that:
-
The test class includes a
@Capability
annotation advertising aPersonDao
service. This is because the test case provides a mock service for use in testing, and so we don’t need a separate implementation to be resolved -
The test set up method uses Mockito to create a mock
PersonDao
, and gets hold of a JAX-RS client using the service registry -
The
testServiceRegistered
method validates that there are no JAX-RS whiteboard services present unless aPersonDao
service is present -
The
testGetPerson
method uses the JAX-RS client to call the REST API, firstly expecting no results, then registering a person with the mock DAO and expecting that result to be returned.
The integration-test.bndrun
The integration-test.bndrun
file defines the set of test cases and bundles that should be used when testing.
-standalone: target/index.xml
-resolve.effective: active
# Run all integration tests which are named xyzTest
Test-Cases: ${classes;CONCRETE;PUBLIC;NAMED;*Test}
# A temporary inclusion until an R7 framework is available
Import-Package: org.osgi.framework.*;version="[1.8,2)",*
-runproperties: \
logback.configurationFile=file:${.}/logback.xml
# Used by Objenesis/Mockito and not actually optional
-runsystempackages: sun.reflect
-runfw: org.eclipse.osgi
-runee: JavaSE-1.8
-runrequires: \
bnd.identity;id='org.osgi.enroute.examples.microservice.rest-service',\
bnd.identity;id='org.osgi.enroute.examples.microservice.rest-service-test',\
bnd.identity;id='org.apache.johnzon.core'
-runbundles: \
ch.qos.logback.classic;version='[1.2.3,1.2.4)',\
ch.qos.logback.core;version='[1.2.3,1.2.4)',\
net.bytebuddy.byte-buddy;version='[1.7.9,1.7.10)',\
net.bytebuddy.byte-buddy-agent;version='[1.7.9,1.7.10)',\
org.apache.aries.javax.jax.rs-api;version='[1.0.4,1.0.5)',\
org.apache.felix.http.servlet-api;version='[1.1.2,1.1.3)',\
org.apache.servicemix.bundles.junit;version='[4.12.0,4.12.1)',\
org.apache.servicemix.specs.json-api-1.1;version='[2.9.0,2.9.1)',\
org.mockito.mockito-core;version='[2.13.0,2.13.1)',\
org.objenesis;version='[2.6.0,2.6.1)',\
org.osgi.service.jaxrs;version='[1.0.0,1.0.1)',\
org.apache.felix.converter;version='[1.0.10,1.0.11)',\
org.osgi.util.function;version='[1.1.0,1.1.1)',\
org.osgi.util.promise;version='[1.1.0,1.1.1)',\
slf4j.api;version='[1.7.25,1.7.26)',\
org.apache.johnzon.core;version='[1.1.0,1.1.1)',\
org.apache.geronimo.specs.geronimo-annotation_1.3_spec;version='[1.1.0,1.1.1)',\
org.apache.aries.jax.rs.whiteboard;version='[1.0.6,1.0.7)',\
org.apache.felix.configadmin;version='[1.9.16,1.9.17)',\
org.apache.felix.http.jetty;version='[4.0.14,4.0.15)',\
org.apache.felix.scr;version='[2.1.16,2.1.17)',\
org.osgi.enroute.examples.microservice.dao-api;version='[0.0.2,0.0.3)',\
org.osgi.enroute.examples.microservice.rest-service;version='[0.0.2,0.0.3)',\
org.osgi.enroute.examples.microservice.rest-service-test;version='[0.0.2,0.0.3)'
Note that:
-
The
Test-Cases
header uses a bnd macro to select all public, concrete classes with names ending inTest
-
The
-runrequires
header includes requirements for the bundle we want to test (rest-service
) the tester bundle, and Apache Johnzon (as a JSON-P implementation) -
The
-runbundles
list is generated by the resolver based on the requirements from the-runrequires
. This therefore includes things like the JAX-RS whiteboard, but not a dao implementation as the tester bundle advertises a dao service capability. -
The
-runee
targets Java 8.
The default target Java version for the OSGi enRoute examples is Java 8. Due to the incompatible changes made in Java 9 you may need to change the -runee
instruction to point at your JRE version and re-resolve if you want to use a later version of Java to run the tests.
The two possible DAO implementations, and the resulting application packaging, are described in subsequent pages.
Prev Next