Apache Karaf Microservices

Apache Karaf is a small OSGi based runtime which provides a lightweight container onto which various components and applications can be deployed.

Apache Karaf can be run as a standalone container and provides some enterprise-ready features like shell console, remote access, hot deployment, and dynamic configuration. It can be the perfect solution for microservices. The idea of microservices on Apache Karaf has already been introduced a few years ago. In March of 2010, Peter Kriens, software architect at OSGi Alliance, said, “What I am promoting is the idea of µServices, the concepts of an OSGi service as a design primitive.”

Karaf on Docker

First, we need to run Docker container with Apache Karaf. Surprisingly, there is no official repository with such an image. I found an image on Docker Hub with Karaf here. Unfortunately, there is no port 8181 exposed – default Karaf web port. We will use this image to create our own with 8181 port available outside. Here’s our Dockerfile.

FROM java:8-jdk
MAINTAINER Piotr Minkowski <piotr.minkowski@gmail.com>
ENV JAVA_HOME /usr/lib/jvm/java-8-openjdk-amd64

ENV KARAF_VERSION=4.0.8

RUN wget http://www-us.apache.org/dist/karaf/${KARAF_VERSION}/apache-karaf-${KARAF_VERSION}.tar.gz; \
    mkdir /opt/karaf; \
    tar --strip-components=1 -C /opt/karaf -xzf apache-karaf-${KARAF_VERSION}.tar.gz; \
    rm apache-karaf-${KARAF_VERSION}.tar.gz; \
    mkdir /deploy; \
    sed -i 's/^\(felix\.fileinstall\.dir\s*=\s*\).*$/\1\/deploy/' /opt/karaf/etc/org.apache.felix.fileinstall-deploy.cfg

VOLUME ["/deploy"]
EXPOSE 1099 8101 8181 44444
ENTRYPOINT ["/opt/karaf/bin/karaf"]

Then, by running Docker commands below we are building our image from Dockerfile and starting new Karaf container.

docker build -t karaf-api .
docker run -d --name karaf -p 1099:1099 -p 8101:8101 -p 8181:8181 -p 44444:44444 karaf-api

Now, we can login to a new Docker container. Karaf is installed in /opt/karaf directory. We should run the client by calling ./client in /opt/karaf/bin directory. Then we should install Apache Felix web console which is by default available under port 8181. You can check it out by calling on web browser http://192.168.99.100:8181/system/console. The default username and password is karaf. In the web console, you can check the full list of features installed on our OSGi container. You can also display that list in the Karaf console using the feature:list command. After web console installation, you decide if you prefer using Karaf command line or Apache Felix console for further actions. For our sample application, we need to add some OSGi repositories and features. First, we are adding Apache CXF framework repository and its features for HTTP and RESTful web services. Then we are adding a repository for the jackson framework and some jackson and Jetty server features.

docker exec -i -t karaf /bin/bash (1)
cd /opt/karaf/bin
./client (2)
karaf@root()> feature:install webconsole (3)
karaf@root()> feature:list (4)
karaf@root()> feature:repo-add cxf 3.1.10 (5)
karaf@root()> feature:install http cxf-jaxrs cxf (6)
karaf@root()> feature:repo-add mvn:org.code-house.jackson/features/2.7.6/xml/features (7)
karaf@root()> feature:install jackson-jaxrs-json-provider jetty (8)

Microservices

Our environment has been configured. Now, we can take a brief look at the sample application. It’s really simple. It has only three modules: account-cxf, customer-cxf, sample-api. In the sample-api module, we have base service interfaces and model objects. In account-cxf and customer-cxf, there are service implementations and OSGi services declarations in the Blueprint file. The sample application source code is available on GitHub. Here’s the account service controller class and its interface below.

public class AccountServiceImpl implements AccountService {

    private List<Account> accounts;

    public AccountServiceImpl() {
        accounts = new ArrayList<>();
        accounts.add(new Account(1, "1234567890", 12345, 1));
        accounts.add(new Account(2, "1234567891", 6543, 2));
        accounts.add(new Account(3, "1234567892", 45646, 3));
    }

    public Account findById(Integer id) {
        return accounts.stream().filter(a -> a.getId().equals(id)).findFirst().get();
    }

    public List<Account> findAll() {
        return accounts;
    }

    public Account add(Account account) {
        accounts.add(account);
        account.setId(accounts.size());
        return account;
    }

    @Override
    public List<Account> findAllByCustomerId(Integer customerId) {
        return accounts.stream().filter(a -> a.getCustomerId().equals(customerId)).collect(Collectors.toList());
    }

}

TheAccountService interface is in the sample-api module. We use JAX-RS annotations for declaring REST endpoints.

public interface AccountService {

    @GET
    @Path("/{id}")
    @Produces("application/json")
    public Account findById(@PathParam("id") Integer id);

    @GET
    @Path("/")
    @Produces("application/json")
    public List<Account> findAll();

    @GET
    @Path("/customer/{customerId}")
    @Produces("application/json")
    public List<Account> findAllByCustomerId(@PathParam("customerId") Integer customerId);

    @POST
    @Path("/")
    @Consumes("application/json")
    @Produces("application/json")
    public Account add(Account account);

}

Here you can see the OSGi services declaration in the blueprint.xml file. We have declared theAccountServiceIpl bean and set that bean as a service for JAX-RS endpoint. This endpoint uses JacksonJsonProvider as a data format provider. There is also an important OSGi service declaration with AccountService referencing  AccountServiceImpl. This service will be available for other microservices deployed on the Karaf container, for example, customer-cxf.

<cxf:bus id="accountRestBus">
</cxf:bus>

<bean id="accountServiceImpl" class="pl.piomin.services.cxf.account.service.AccountServiceImpl"/>
<service ref="accountServiceImpl" interface="pl.piomin.services.cxf.api.AccountService" />

<jaxrs:server address="/account" id="accountService">
    <jaxrs:serviceBeans>
        <ref component-id="accountServiceImpl" />
    </jaxrs:serviceBeans>
    <jaxrs:features>
        <cxf:logging />
    </jaxrs:features>
    <jaxrs:providers>
        <bean class="com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider"/>
    </jaxrs:providers>
</jaxrs:server>

Now, let’s take a look at the customer-cxf microservice. Here’s the OSGi blueprint of that service. The JAX-RS server declaration is pretty similar to account-cxf. There is only one addition to the earlier OSGi blueprint – the reference to AccountService. This reference is injected into CustomerServiceImpl.

<reference id="accountService"       interface="pl.piomin.services.cxf.api.AccountService" />

<bean id="customerServiceImpl"       class="pl.piomin.services.cxf.customer.service.CustomerServiceImpl">
    <property name="accountService" ref="accountService" />
</bean>

<jaxrs:server address="/customer" id="customerService">
    <jaxrs:serviceBeans>
        <ref component-id="customerServiceImpl" />
    </jaxrs:serviceBeans>
    <jaxrs:features>
        <cxf:logging />
    </jaxrs:features>
    <jaxrs:providers>
        <bean class="com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider" />
    </jaxrs:providers>
</jaxrs:server>

CustomerService uses an OSGi reference to AccountService in findById method to collect all accounts belonged to the customer with the specified id path parameter, and also exposes some other operations.

public class CustomerServiceImpl implements CustomerService {

    private AccountService accountService;

    private List<Customer> customers;

    public CustomerServiceImpl() {
        customers = new ArrayList<>();
        customers.add(new Customer(1, "XXX", "1234567890"));
        customers.add(new Customer(2, "YYY", "1234567891"));
        customers.add(new Customer(3, "ZZZ", "1234567892"));
    }

    @Override
    public Customer findById(Integer id) {
        Customer c = customers.stream().filter(a -> a.getId().equals(id)).findFirst().get();
        c.setAccounts(accountService.findAllByCustomerId(id));
        return c;
    }

    @Override
    public List<Customer> findAll() {
        return customers;
    }

    @Override
    public Customer add(Customer customer) {
        customers.add(customer);
        customer.setId(customers.size());
        return customer;
    }

    public AccountService getAccountService() {
        return accountService;
    }

    public void setAccountService(AccountService accountService) {
        this.accountService = accountService;
    }

}

Each service has a packaging type bundle inside pom.xml and uses maven-bundle-plugin during the build process. After running mvn clean install on the root project, all bundles will be generated in the target catalog.You can install them using Apache Felix web console or Karaf command line client in that order: sample-api, account-cxf, customer-cxf.

Testing

Finally, you can see a list of available CXF endpoints on Karaf by calling http://192.168.99.100:8181/cxf in your web browser. Call http://192.168.99.100:8181/cxf/customer/1 to test findById in CustomerService. You should see JSON with customer data and all accounts collected from the account microservice.

Treat this post as a short introduction to microservices conception on the Apache Karaf OSGi container. I presented how to use CXF endpoints on the Karaf container as a kind of service gateway and OSGi services for inter-communication process between deployed microservices. Instead of the OSGi reference, we could use JAX-RS proxy client for connecting with account service from customer service. You can find some basic examples of that concept on the web. There are also available more advanced solutions for service registration and discovery on Karaf: for example, remote service call with Apache ZooKeeper. I think we will take a closer look at them in subsequent posts.

 

 

 

 

Top