Building microservices using Undertow, CDI and JAX-RS

Choosing the right tools to create microservices is not always that simple. First, there are a lot of microservices application types, so, you need to choose the right tool for the right job. Second, there are a lot of tools to help you build these applications.

I’m going to show you another tool set to help you build your microservices applications: Undertow + CDI + JAX-RS, persisting in a MongoDB. This will probably meet all your needs when we talk about microservices.

Actually, it is pretty simple: We just need to add, delete, get and update a specific resource and there are no transactions, so, why bother to create a JavaEE or a Spring application?

Also, we are going to use Gradle to manage our maven dependencies and to build our application.

Okay, let’s start then.

apply plugin: 'java'
apply plugin: 'application'

sourceCompatibility = 1.8
targetCompatibility = 1.8

repositories {
    jcenter()
}

mainClassName = 'com.thedevpiece.mss.Application'

distTar {
    archiveName = 'mss.tar'
}

sourceSets {
    main {
        output.resourcesDir = "build/classes/main"
    }
}

dependencies {
    compile 'io.undertow:undertow-core:1.2.9.Final'
    compile 'io.undertow:undertow-servlet:1.2.9.Final'

    compile 'org.glassfish.jersey.core:jersey-server:2.21'
    compile 'org.glassfish.jersey.ext.cdi:jersey-cdi1x:2.21'
    compile 'org.glassfish.jersey.media:jersey-media-json-jackson:2.21'
    compile 'org.glassfish.jersey.containers:jersey-container-servlet-core:2.21'
    compile 'org.glassfish.jersey.ext:jersey-bean-validation:2.21'

    compile 'org.jboss.weld:weld-core:2.2.14.Final'
    compile 'org.jboss.weld.servlet:weld-servlet-core:2.2.14.Final'

    compile 'com.fasterxml.jackson.core:jackson-core:2.6.1'
    compile 'com.fasterxml.jackson.core:jackson-annotations:2.6.1'
    compile 'com.fasterxml.jackson.core:jackson-databind:2.6.1'

    compile 'org.mongodb.morphia:morphia:1.0.1'

    testCompile "junit:junit:4.11"
}

These are all dependencies we are going to use in this article. Nothing more, nothing less.

As we are going to use JAX-RS, I chose to use Jersey. It is easy to configure and it is the JAX-RS reference implementation.

package com.thedevpiece.mss;

import org.glassfish.jersey.server.ResourceConfig;

import javax.ws.rs.ApplicationPath;

/**
 * @author Gabriel Francisco - gabfssilva@gmail.com
 */
@ApplicationPath("/api/*")
public class JerseyApp extends ResourceConfig {
    public JerseyApp() {
        packages(true, "com.thedevpiece.mss.api");
    }
}

We also need a starter class. The class below is our main class. This class runs the Undertow container, registering the Jersey Servlet and configuring the Weld listener.

package com.thedevpiece.mss;

import io.undertow.Handlers;
import io.undertow.Undertow;
import io.undertow.server.handlers.PathHandler;
import io.undertow.servlet.Servlets;
import io.undertow.servlet.api.DeploymentInfo;
import io.undertow.servlet.api.DeploymentManager;
import org.glassfish.jersey.servlet.ServletContainer;
import org.jboss.weld.environment.servlet.Listener;

import javax.servlet.ServletException;

import static io.undertow.servlet.Servlets.listener;
import static io.undertow.servlet.Servlets.servlet;

/**
 * @author Gabriel Francisco - gabfssilva@gmail.com
 */
public class Application {
    private static Undertow server;

    public static void main(String[] args) throws ServletException {
        startContainer(9090);
    }

    public static void stopContainer(){
        server.stop();
    }

    public static void startContainer(int port) throws ServletException {
        DeploymentInfo servletBuilder = Servlets.deployment();

        servletBuilder
                .setClassLoader(Application.class.getClassLoader())
                .setContextPath("/mss")
                .setDeploymentName("mss.war")
                .addListeners(listener(Listener.class))
                .addServlets(servlet("jerseyServlet", ServletContainer.class)
                        .setLoadOnStartup(1)
                        .addInitParam("javax.ws.rs.Application", JerseyApp.class.getName())
                        .addMapping("/api/*"));

        DeploymentManager manager = Servlets.defaultContainer().addDeployment(servletBuilder);
        manager.deploy();
        PathHandler path = Handlers.path(Handlers.redirect("/mss"))
                .addPrefixPath("/mss", manager.start());

        server =
                Undertow
                        .builder()
                        .addHttpListener(port, "localhost")
                        .setHandler(path)
                        .build();

        server.start();
    }
}

After doing this, we will create an Object Mapper factory, to create the Object Mapper for a future use.

package com.thedevpiece.mss.util;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;

/**
 * @author Gabriel Francisco - gabfssilva@gmail.com
 */
public class ObjectMapperFactory {
    private static final ObjectMapper OBJECT_MAPPER;

    static {
        OBJECT_MAPPER = new ObjectMapper();

        OBJECT_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        OBJECT_MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);
    }

    private ObjectMapperFactory() {
    }

    public static ObjectMapper get() {
        return OBJECT_MAPPER;
    }
}

Now we are going to define a JAX-RS ContextResolver, returning our ObjectMapper.

package com.thedevpiece.mss.api.providers;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.thedevpiece.mss.util.ObjectMapperFactory;

import javax.ws.rs.ext.ContextResolver;
import javax.ws.rs.ext.Provider;

@Provider
public class ObjectMapperResolver implements ContextResolver<ObjectMapper> {
    @Override
    public ObjectMapper getContext(Class<?> type) {
        return ObjectMapperFactory.get();
    }
}

Okay, let's see our domain classes then. We only have two classes in the domain package: our entity and its repository.

package com.thedevpiece.mss.domain.entities;

import org.mongodb.morphia.annotations.Entity;
import org.mongodb.morphia.annotations.Id;

/**
 * @author Gabriel Francisco - gabfssilva@gmail.com
 */
@Entity
public class Product {
    @Id
    private String id;
    private String name;
    private String description;
    private Double value;

    public Product() {
    }

    private Product(Builder builder) {
        setId(builder.id);
        setName(builder.name);
        setDescription(builder.description);
        setValue(builder.value);
    }

    public static Builder newProduct() {
        return new Builder();
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public Double getValue() {
        return value;
    }

    public void setValue(Double value) {
        this.value = value;
    }

    public static final class Builder {
        private String id;
        private String name;
        private String description;
        private Double value;

        private Builder() {
        }

        public Builder id(String id) {
            this.id = id;
            return this;
        }

        public Builder name(String name) {
            this.name = name;
            return this;
        }

        public Builder description(String description) {
            this.description = description;
            return this;
        }

        public Builder value(Double value) {
            this.value = value;
            return this;
        }

        public Product build() {
            return new Product(this);
        }
    }
}

I really enjoy using the builder pattern even to create small objects. (don't judge me)

You must also have noticed the @Entity and @Id annotations. No, it is not JPA. We are using Morphia, the official MongoDB ODM (object document mapper). You can read more about Morphia in this link.

As I said above, our repository is right below.

package com.thedevpiece.mss.domain;

import com.thedevpiece.mss.domain.entities.Product;
import org.bson.types.ObjectId;
import org.mongodb.morphia.Datastore;
import org.mongodb.morphia.Key;
import org.mongodb.morphia.query.Query;
import org.mongodb.morphia.query.UpdateOperations;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import java.util.List;

/**
 * @author Gabriel Francisco - gabfssilva@gmail.com
 */
@ApplicationScoped
public class ProductRepository {
    @Inject
    private Datastore datastore;

    public Product save(Product product){
        final Key<Product> productKey = datastore.save(product);
        product.setId((String) productKey.getId());
        return product;
    }

    public Product find(String id){
        return datastore.createQuery(Product.class).filter("_id ==", new ObjectId(id)).get();
    }

    public Product delete(String id){
        return datastore.findAndDelete(datastore.createQuery(Product.class).filter("_id ==", new ObjectId(id)));
    }

    public List<Product> findAll(){
        return datastore.find(Product.class).asList();
    }

    public Product update(String id, Product product){
        final UpdateOperations<Product> updateOperations = datastore.createUpdateOperations(Product.class).set("name", product.getName()).set("description", product.getDescription()).set("value", product.getValue());
        final Query<Product> query = datastore.createQuery(Product.class).filter("_id ==", new ObjectId(id));
        datastore.update(query, updateOperations);
        return query.get();
    }
}

Okay, so, now our domain is done. As I do not like to mix the domain with the resource object, we need to create another class called ProductResource. This class will be used in our endpoint and will represent the json that we are going to receive and return.

package com.thedevpiece.mss.api.v1.resources;

import javax.validation.constraints.NotNull;

/**
 * @author Gabriel Francisco - gabfssilva@gmail.com
 */
public class ProductResource {
    private String id;
    @NotNull(message = "name cannot be null")
    private String name;
    @NotNull(message = "description cannot be null")
    private String description;
    @NotNull(message = "value cannot be null")
    private Double value;

    public ProductResource() {
    }

    private ProductResource(Builder builder) {
        setId(builder.id);
        setName(builder.name);
        setDescription(builder.description);
        setValue(builder.value);
    }

    public static Builder newProductResource() {
        return new Builder();
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public Double getValue() {
        return value;
    }

    public void setValue(Double value) {
        this.value = value;
    }

    public static final class Builder {
        private String id;
        private String name;
        private String description;
        private Double value;

        private Builder() {
        }

        public Builder id(String id) {
            this.id = id;
            return this;
        }

        public Builder name(String name) {
            this.name = name;
            return this;
        }

        public Builder description(String description) {
            this.description = description;
            return this;
        }

        public Builder value(Double value) {
            this.value = value;
            return this;
        }

        public ProductResource build() {
            return new ProductResource(this);
        }
    }
}

See the "v1" in the package name. Yes, we are going to use a versioned api. It is safer if you feel the need to change the api in the future. (be sure it happens a lot)

I didn't say before, but, if you were attentive to the build.gradle file you certainly noticed that we are using Bean Validation. Pretty useful to save us some time for not creating our own validator classes.

Well, Jersey will throw an exception if the json sent from the client is not valid, so, we need somehow tell him what he is doing wrong. We could return a header telling him what he is messing up, but, I really don't like this design, I prefer to create an envelop to wrap the api returns. In this envelop, we could add an error field and the item/items field for the successful returns.

package com.thedevpiece.mss.api;

import java.util.List;

/**
 * @author Gabriel Francisco - gabfssilva@gmail.com
 */
public class Envelop<T> {
    private String uri;
    private T item;
    private List<T> items;
    private ErrorMessage error;

    public Envelop() {
    }

    private Envelop(Builder builder) {
        setUri(builder.uri);
        setItem((T) builder.item);
        setItems(builder.items);
        setError(builder.error);
    }

    public static Builder newEnvelop() {
        return new Builder();
    }

    public String getUri() {
        return uri;
    }

    public void setUri(String uri) {
        this.uri = uri;
    }

    public T getItem() {
        return item;
    }

    public void setItem(T item) {
        this.item = item;
    }

    public ErrorMessage getError() {
        return error;
    }

    public void setError(ErrorMessage error) {
        this.error = error;
    }

    public List<T> getItems() {
        return items;
    }

    public void setItems(List<T> items) {
        this.items = items;
    }

    @Override
    public String toString() {
        return "Envelop{" +
                "uri='" + uri + '\'' +
                ", item=" + item +
                ", items=" + items +
                ", error=" + error +
                '}';
    }

    public static class ErrorMessage {
        private String message;

        public ErrorMessage(String message) {
            this.message = message;
        }

        public String getMessage() {
            return message;
        }

        public void setMessage(String message) {
            this.message = message;
        }

        @Override
        public String toString() {
            return "ErrorMessage{" +
                    "message='" + message + '\'' +
                    '}';
        }
    }

    public static final class Builder<T> {
        private String uri;
        private T item;
        private List<T> items;
        private ErrorMessage error;

        private Builder() {
        }

        public Builder uri(String uri) {
            this.uri = uri;
            return this;
        }

        public Builder item(T item) {
            this.item = item;
            return this;
        }

        public Builder items(List<T> items) {
            this.items = items;
            return this;
        }

        public Builder error(ErrorMessage error) {
            this.error = error;
            return this;
        }

        public Envelop build() {
            return new Envelop(this);
        }
    }
}

JAX-RS gives us the ExceptionMapper. Always when a certain exception is thrown, JAX-RS catches and choose the right Mapper for this exception. We are using bean validation, so, we need a specific mapper for the ConstraintViolationException.

package com.thedevpiece.mss.api.mappers;

import com.thedevpiece.mss.api.Envelop;

import javax.validation.ConstraintViolationException;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;

/**
 * @author Gabriel Francisco - gabfssilva@gmail.com
 */
@Provider
public class ConstraintViolationExceptionMapper implements ExceptionMapper<ConstraintViolationException> {
    @Override
    public Response toResponse(ConstraintViolationException exception) {
        StringBuilder stringBuilder = new StringBuilder("Validation error: ");
        exception.getConstraintViolations().stream().forEach(e -> stringBuilder.append(e.getMessage()).append(", "));
        stringBuilder.replace(stringBuilder.lastIndexOf(", "), stringBuilder.length(), "");
        return Response.status(400).entity(Envelop.newEnvelop().error(new Envelop.ErrorMessage(stringBuilder.toString())).build()).build();
    }
}

Okaay, so, always when the ConstraintViolationException is thrown, we are going to return a bad request and tell the client what he is messing up. Cool!

As we have two different objects representing the same entity, we need converters.

package com.thedevpiece.mss.api.v1.converters;

import com.thedevpiece.mss.api.v1.resources.ProductResource;
import com.thedevpiece.mss.domain.entities.Product;

import javax.enterprise.context.ApplicationScoped;

import static com.thedevpiece.mss.domain.entities.Product.newProduct;

/**
 * @author Gabriel Francisco - gabfssilva@gmail.com
 */
@ApplicationScoped
public class ProductConverter {

    public Product convert(ProductResource productResource){
        return newProduct()
                    .description(productResource.getDescription())
                    .value(productResource.getValue())
                    .name(productResource.getName())
                .build();
    }
}
package com.thedevpiece.mss.api.v1.converters;

import com.thedevpiece.mss.api.v1.resources.ProductResource;
import com.thedevpiece.mss.domain.entities.Product;

import javax.enterprise.context.ApplicationScoped;

import java.util.List;
import java.util.stream.Collectors;

import static com.thedevpiece.mss.api.v1.resources.ProductResource.newProductResource;
import static java.util.stream.Collectors.toList;

/**
 * @author Gabriel Francisco - gabfssilva@gmail.com
 */
@ApplicationScoped
public class ProductResourceConverter {
    public ProductResource convert(Product product){
        return newProductResource()
                    .id(product.getId())
                    .description(product.getDescription())
                    .value(product.getValue())
                    .name(product.getName())
                .build();
    }

    public List<ProductResource> convert(List<Product> products){
        return products.stream().map(this::convert).collect(toList());
    }
}

Our converters are also versioned. Remember, one resource for one version, so, we need to create the converters for each version too.

After all these classes, finally, we will create the endpoint!

package com.thedevpiece.mss.api.v1.endpoint;

import com.thedevpiece.mss.api.v1.converters.ProductConverter;
import com.thedevpiece.mss.api.v1.converters.ProductResourceConverter;
import com.thedevpiece.mss.api.v1.resources.ProductResource;
import com.thedevpiece.mss.domain.ProductRepository;
import com.thedevpiece.mss.domain.entities.Product;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import javax.ws.rs.*;
import javax.ws.rs.core.Response;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;

import static com.thedevpiece.mss.api.Envelop.newEnvelop;
import static javax.ws.rs.core.Response.created;
import static javax.ws.rs.core.Response.ok;
import static javax.ws.rs.core.Response.status;

/**
 * @author Gabriel Francisco - gabfssilva@gmail.com
 */
@Produces("application/json")
@Path("/v1/products")
@ApplicationScoped
public class ProductEndpoint {
    @Inject
    private ProductRepository repository;

    @Inject
    private ProductResourceConverter productResourceConverter;

    @Inject
    private ProductConverter productConverter;

    @GET
    public Response findAll(){
        final List<Product> products = repository.findAll();
        final List<ProductResource> resources = productResourceConverter.convert(products);
        return ok(newEnvelop().items(resources).build()).build();
    }

    @GET
    @Path("/{id}")
    public Response findById(@PathParam("id") @NotNull  String id){
        final Product product = repository.find(id);

        if(product == null){
            return status(404).build();
        }

        return ok(newEnvelop().item(productResourceConverter.convert(product)).build()).build();
    }

    @POST
    public Response create(@NotNull @Valid ProductResource productResource) throws URISyntaxException {
        Product product = repository.save(productConverter.convert(productResource));
        return created(new URI("/api/v1/products/" + product.getId())).entity(newEnvelop().item(productResourceConverter.convert(product)).build()).build();
    }

    @PUT
    @Path("/{id}")
    public Response update(@NotNull @Valid ProductResource productResource, @PathParam("id") @NotNull  String id) throws URISyntaxException {
        Product product = repository.update(id, productConverter.convert(productResource));
        return ok(newEnvelop().item(productResourceConverter.convert(product)).build()).build();
    }

    @DELETE
    @Path("/{id}")
    public Response delete(@PathParam("id") @NotNull  String id) throws URISyntaxException {
        Product product = repository.delete(id);

        if(product == null){
            return status(404).build();
        }

        return ok(newEnvelop().item(productResourceConverter.convert(product)).build()).build();
    }
}

The endpoint exposes the product entity CRUD operations. Obviously it is versioned, as we could see in the classes above.

Well, no complications, just two layers: the domain and the rest api layer.

Actually, we are not finished. We need to finalize some configurations.
First of all, two annotations to help us inject properties from a property file in Strings.

package com.thedevpiece.mss.producers.qualifiers;

import javax.inject.Qualifier;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * @author Gabriel Francisco - gabfssilva@gmail.com
 */
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface InjectableProperties {
}
package com.thedevpiece.mss.producers.qualifiers;

import javax.enterprise.util.Nonbinding;
import javax.inject.Qualifier;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * @author Gabriel Francisco - gabfssilva@gmail.com
 */
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface Property {
    @Nonbinding String value();
}

And our only CDI producer.

package com.thedevpiece.mss.producers;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.mongodb.MongoClient;
import com.mongodb.MongoClientURI;
import com.thedevpiece.mss.producers.qualifiers.InjectableProperties;
import com.thedevpiece.mss.producers.qualifiers.Property;
import com.thedevpiece.mss.util.ObjectMapperFactory;
import org.mongodb.morphia.Datastore;
import org.mongodb.morphia.Morphia;

import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Produces;
import javax.enterprise.inject.spi.InjectionPoint;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.ResourceBundle;

/**
 * @author Gabriel Francisco - gabfssilva@gmail.com
 */
public class ApplicationProducer {
    @Produces
    public ObjectMapper objectMapper() {
        return ObjectMapperFactory.get();
    }

    @Produces
    @ApplicationScoped
    public Datastore datastore(@Property("mongodb.uri") String mongoDbUri) throws Exception {
        MongoClientURI uri = new MongoClientURI(mongoDbUri);
        final MongoClient mongoClient = new MongoClient(uri);

        final Morphia morphia = new Morphia();
        morphia.mapPackage("com.thedevpiece.mss.domain.entities");

        final Datastore datastore = morphia.createDatastore(mongoClient, "mss");
        datastore.ensureIndexes();
        return datastore;
    }

    @Produces
    @Property(value = "")
    public String property(@InjectableProperties Map<String, String> properties, InjectionPoint injectionPoint){
        final Property property = injectionPoint.getAnnotated().getAnnotation(Property.class);
        return properties.get(property.value());
    }

    @ApplicationScoped
    @Produces
    @InjectableProperties
    public Map<String, String> properties() throws IOException {
        Map<String, String> map = new HashMap<>();
        final ResourceBundle bundle = ResourceBundle.getBundle("application");
        bundle.keySet().forEach(key -> map.put(key, bundle.getString(key)));
        return map;
    }
}

We also need to create a beans.xml file in the /META-INF/ folder.

<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
        http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd"
       bean-discovery-mode="all">
</beans>

The last thing we need to do is to create the application.properties file in the resources folder.

mongodb.uri = mongodb://<username>:<password>@<hostname>:<port>/mss 

This file just have one property: the MongoDB uri. We need this to connect to the right MongoDB instance.

Finalizing...

You can check this project out in this github repository
You could see how easy it is and how this is a lightweight solution. Certainly a nice way to go when we are talking about microservices. ;)

Other posts about microservices

Building microservices with Akka HTTP and MongoDB
Building microservices with Finatra and Slick
Building microservices with Kotlin and Spring Boot

I hope this article was helpful to you! Any questions, please let me know leaving a comment.

[]'s

Gabriel Francisco

Software Engineer at GFG, 25 years, under graduated in Computer Science and graduated in Service-oriented Software Engineering. Like playing guitar once in a while. Oh, and I'm kind of boring.

São Paulo

comments powered by Disqus