Prev Next

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. The service attribute means that even though RestComponentImpl 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 the RestComponentImpl this component does implement interfaces and will be automatically registered as a service using the MessageBodyReader and MessageBodyWriter interfaces. Also the @Component annotation declares this component to be PROTOTYPE 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

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 a PersonDao 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 a PersonDao 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 in Test

  • 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.