Getting started with Apache Karaf Minho

Apache Karaf Minho

Apache Karaf Minho is a new runtime, completely created from scratch.
The objectives are:
  • super easy to use and run
  • ligtning fast, including GraalVM support
  • Kubernetes compliant to be easily used on the cloud
  • helping to optimize cloud costs by supporting applications colocation
  • providing turnkey services allowing you to create Minho applications in a minute

Minho is composed by:
  • minho-boot is the core of Minho. It contains the runtime launcher and the core services (service registry, lifecycle service, configuration service, ...).
  • minho-* services provided additional functionnalities in the runtime. You can use these services to create your own services, it's also where you will find the services managing sepcific kind of applications, allowing colocation.
  • minho-tooling optionally provide convenient tools to create your own runtime. It includes minho-maven-plugin for instance.

In this blog post, we will take a quick tour on Apache Karaf Minho, showing two main Minho usages:
  • create your own services based on Minho, and create a runtime with these services
  • colocate Spring Boot applications in Minho

To illustrate Minho services, we will create a simple Blog application backend, storing blog posts in a database, and creating a REST API to manipulate blog posts.
You can find the code source here: https://github.com/jbonofre/minho-blog.

Simple JPA service

Let's start with a very simple JPA backend service. In this service, we will use minho-jpa which provide a JPA engine ready to be used (by default, Apache OpenJPA).
We will "just" create:
  1. BlogEntity
  2. META-INF/peristente.xml
  3. the Blog backend service named BlogJpaService

Let's start the Maven pom.xml:

<xml version="1.0" encoding="UTF-8">
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>net.nanthrax.blog</groupId>
        <artifactId>minho-blog</artifactId>
        <version>1.0-SNAPSHOT</version>
        <relativePath>../pom.xml</relativePath>
    </parent>

    <artifactId>minho-blog-jpa</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.apache.karaf.minho</groupId>
            <artifactId>minho-boot</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.karaf.minho</groupId>
            <artifactId>minho-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.derby</groupId>
            <artifactId>derby</artifactId>
            <version>10.15.2.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.derby</groupId>
            <artifactId>derbytools</artifactId>
            <version>10.15.2.0</version>
        </dependency>

        <!-- test -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-params</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

</project>

This pom.xml is pretty simple:
  • we can see the minho-boot and minho-jpa dependencies, because we will use the Minho JPA service
  • we can see the derby dependencies because we will use Derby as database
  • we can see the junit dependencies used for the tests

Let's now create a very simple entity representing the blog post:

package net.nanthrax.blog.minho.jpa;

import javax.persistence.Entity;
import javax.persistence.Id;

@Entity
public class BlogEntity {

    @Id
    private String title;
    private String content;

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}

Now, we implement our BlogJpaService manipulating the entity:

package net.nanthrax.blog.minho.jpa;

import org.apache.karaf.minho.boot.service.LifeCycleService;
import org.apache.karaf.minho.boot.service.ServiceRegistry;
import org.apache.karaf.minho.boot.spi.Service;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import javax.persistence.Query;
import java.util.List;

public class BlogJpaService implements Service {

    private EntityManagerFactory factory;
    private EntityManager entityManager;

    @Override
    public int priority() {
        return Service.DEFAULT_PRIORITY + 1;
    }

    @Override
    public void onRegister(ServiceRegistry serviceRegistry) {
        LifeCycleService lifeCycleService = serviceRegistry.get(LifeCycleService.class);
        lifeCycleService.onStart(() -> {
            factory = Persistence.createEntityManagerFactory("BlogEntity", System.getProperties());
            entityManager = factory.createEntityManager();
        });
        lifeCycleService.onShutdown(() -> {
            entityManager.close();
            factory.close();
        });
    }

    public void createBlogPost(BlogEntity entity) {
        entityManager.getTransaction().begin();
        entityManager.persist(entity);
        entityManager.getTransaction().commit();
    }

    public List listBlogPosts() {
        Query query = entityManager.createQuery("SELECT blog FROM BlogEntity blog");
        return query.getResultList();
    }

    public void deleteBlogPost(BlogEntity entity) {
        entityManager.getTransaction().begin();
        entityManager.remove(entity);
        entityManager.getTransaction().commit();
    }

}

The only Minho specific parts are:
  • our class implements the Service interface: it represents a service in the Minho runtime.
  • you can implement default methods from Service, especially the onRegister(ServiceRegistry serviceRegistry) and priority() methods.

The priority() method allows you to define order in thee service registration. Here we define 1001 (Service.DEFAULT_PRIORITY + 1) as priority.
Let's have a deeper look on the onRegister(ServiceRegistry serviceRegistry) method. This method is called by the Minho runtime when the service is registered (loaded) in the runtime. This method gives us access to the Mingo ServiceRegistry where we can interact with other runtime services.
We will use a core service provided by Minho: the LifeCycleService. We can add "hook" code in the lifecycle service to execute our logic when the runtime start or stop. It means we have the following lifecycle in our services:
  1. the logic in onRegister() method is called when Minho loads the service
  2. optionaly, we can add our logic in LifeCycleService#onStart() that will be called after Minho loaded all services, during startup phase
  3. optionaly, we can add our logic in LifeCycleService#onShutdown() that will be called when Minho stops
In our BlogJpaService, we create the EntityManagerFactory and EntityManager in the LifeCycleService#start() phase, and we close them in LifeCycleService#stop() phase.
In our "service" methods, we use the entityManager to deal with the entity.
If the code is done, we need two resource files:
  • the Minho META-INF/services/org.apache.karaf.minho.boot.spi.Service contains the BlogJpaService full class name (net.nanthrax.blog.minho.jpa.BlogJpaService). It's used by Minho to automatically load services.
  • the META-INF/persistence.xml is the configuration of the JPA entity manager. Nothing Minho specific here, it's a pure JPA file:
    
    <?xml version="1.0" encoding="UTF-8"?>
    <persistence xmlns="http://java.sun.com/xml/ns/persistence"
                 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                 version="1.0">
    
        <persistence-unit name="BlogEntity" transaction-type="RESOURCE_LOCAL">
            <class>net.nanthrax.blog.minho.jpa.BlogEntity</class>
            <properties>
                <property name="javax.persistence.schema-generation.database.action" value="drop-and-create" />
                <property name="openjpa.RuntimeUnenhancedClasses" value="supported"/>
                <property name="openjpa.jdbc.SynchronizeMappings"
                          value="buildSchema"/>
                <property name="openjpa.ConnectionURL"
                          value="jdbc:derby:test;databaseName=blog;create=true"/>
                <property name="openjpa.ConnectionDriverName"
                          value="org.apache.derby.jdbc.EmbeddedDriver"/>
            </properties>
        </persistence-unit>
    
    </persistence>
    
    
It's done :) We have simple JPA backend service.
Now, we can add tests on our JPA service.
Basically, in the test, we can start a Minho runtime, adding our BlogJpaServvice and test the expected behaviors:

ackage net.nanthrax.blog.minho.jpa;

import org.apache.karaf.minho.boot.Minho;
import org.apache.karaf.minho.boot.service.ConfigService;
import org.apache.karaf.minho.boot.service.LifeCycleService;
import org.apache.karaf.minho.jpa.openjpa.OpenJPAService;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import java.util.List;
import java.util.stream.Stream;

public class BlogJpaServiceTest {

    @Test
    public void simpleUsage() throws Exception {
        System.setProperty("derby.system.home", "target/derby");
        System.setProperty("derby.stream.error.file", "target/derby.log");

        Minho minho = Minho.builder().loader(() -> Stream.of(new ConfigService(), new LifeCycleService(), new OpenJPAService(), new BlogJpaService())).build().start();

        BlogJpaService blogJpaService = minho.getServiceRegistry().get(BlogJpaService.class);

        BlogEntity blog = new BlogEntity();
        blog.setTitle("My First Blog");
        blog.setContent("Hello world!");
        blogJpaService.createBlogPost(blog);

        List list = blogJpaService.listBlogPosts();
        Assertions.assertEquals(1, list.size());

        blogJpaService.deleteBlogPost(blog);
        list = blogJpaService.listBlogPosts();
        Assertions.assertEquals(0, list.size());
    }

}

Easy right ?
Now, we can create another service (REST API) using this backend one.

Simple REST API

As we used minho-jpa in our JPA backend, Minho provides minho-rest service we can use to create the REST API. minho-rest provides the JAX-RS resources and engine. We just have to directly implement a class with JAX-RS annotation, nothing else.
Let's start with the pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>net.nanthrax.blog</groupId>
        <artifactId>minho-blog</artifactId>
        <version>1.0-SNAPSHOT</version>
        <relativePath>../pom.xml</relativePath>
    </parent>

    <artifactId>minho-blog-rest</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.apache.karaf.minho</groupId>
            <artifactId>minho-boot</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.karaf.minho</groupId>
            <artifactId>minho-http</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.karaf.minho</groupId>
            <artifactId>minho-rest</artifactId>
        </dependency>
        <dependency>
            <groupId>net.nanthrax.blog</groupId>
            <artifactId>minho-blog-jpa</artifactId>
            <version>${project.version}</version>
        </dependency>

        <!-- test -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-params</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

</project>

No rocket science in this pom.xml:
  • we can see minho-boot and minho-rest dependencies as we will create a REST API
  • the minho-blog-jpa dependencies is there because we will use our BlogJpaService in thee REST API
  • the junit dependencies are there for the tests

We can now immplement BlogApi JAX-RS class:

package net.nanthrax.blog.minho.rest;

import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import net.nanthrax.blog.minho.jpa.BlogEntity;
import net.nanthrax.blog.minho.jpa.BlogJpaService;
import org.apache.karaf.minho.boot.Minho;

import java.util.List;
import java.util.Objects;

@Path("/")
public class BlogApi {

    @POST
    @Path("/")
    @Consumes(MediaType.APPLICATION_JSON)
    public Response createBlog(BlogEntity blog) {
        getBlogJpaService().createBlogPost(blog);
        return Response.ok().build();
    }

    @GET
    @Path("/")
    @Produces(MediaType.APPLICATION_JSON)
    public List listBlogs() {
        return getBlogJpaService().listBlogPosts();
    }

    private BlogJpaService getBlogJpaService() {
        Minho minho = Minho.getInstance();
        BlogJpaService blogJpaService = minho.getServiceRegistry().get(BlogJpaService.class);
        if (Objects.isNull(blogJpaService)) {
            throw new IllegalStateException("Blog JPA service is not available");
        }
        return blogJpaService;
    }

}

We can see the JAX-RS annotations (@GET, @POST, ...) on the createBlog() and listBlogs() method. The Minho "specific" part is the getBlogJpaService() method. In this method, we retrieve the current Minho instance to gete the service registry. Then, we have access to our BlogJpaService registered in the Minho service registry.
It's done ! Nothing else to do.
As we did in minho-blog-jpa, we can add tests:

package net.nanthrax.blog.minho.rest;

import net.nanthrax.blog.minho.jpa.BlogJpaService;
import org.apache.karaf.minho.boot.Minho;
import org.apache.karaf.minho.boot.service.ConfigService;
import org.apache.karaf.minho.boot.service.LifeCycleService;
import org.apache.karaf.minho.jpa.openjpa.OpenJPAService;
import org.apache.karaf.minho.rest.jersey.JerseyRestService;
import org.apache.karaf.minho.web.jetty.JettyWebContainerService;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Stream;

public class BlogApiTest {

    @Test
    public void simpleTest() throws Exception {
        String blog = "{ \"title\": \"Foo\", \"content\": \"Bar\" }";

        URL url = new URL("http://localhost:8080/blog");
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        connection.setRequestMethod("POST");
        connection.setRequestProperty("Content-Type", "application/json");
        connection.setDoInput(true);
        connection.setDoOutput(true);

        try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(connection.getOutputStream()))) {
            writer.write(blog);
            writer.flush();
        }

        Assertions.assertEquals("OK", connection.getResponseMessage());
        Assertions.assertEquals(200, connection.getResponseCode());

        connection = (HttpURLConnection) url.openConnection();
        connection.setRequestMethod("GET");
        connection.setDoOutput(true);

        String line;
        StringBuilder builder = new StringBuilder();
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
            while ((line = reader.readLine()) != null) {
                builder.append(line);
            }
        }

        Assertions.assertEquals("[{\"title\":\"Foo\",\"content\":\"Bar\"}]", builder.toString());
    }

    @BeforeEach
    protected void setup() throws Exception {
        ConfigService configService = new ConfigService();
        Map properties = new HashMap<>();
        properties.put("rest.packages", "net.nanthrax.blog.minho.rest");
        properties.put("rest.path", "/blog/*");
        configService.setProperties(properties);

        Minho minho = Minho.builder().loader(() -> Stream.of(
                configService,
                new LifeCycleService(),
                new OpenJPAService(),
                new BlogJpaService(),
                new JettyWebContainerService(),
                new JerseyRestService()
        )).build().start();
    }

    @AfterEach
    protected void teardown() throws Exception {
        Minho.getInstance().close();
    }

}

The logic is the same as for BlogJpaServiceTest:
  1. the setup() (executed before each test), we start Minho, and load the required services: Minho ConfigService, Minho LifeCycleService, Minho OpenJPAService, our BlogJpaService, Minho JettyWebContainerService, Minho JerseyRestService. We set some properties for our REST service via the Minho ConfigService:
    • rest.packages contains the package containing our JAX-RS annotated classes, net.nanthrax.blog.minho.rest in our case
    • rest.path is the location of the exposed REST API, /blog/* in our case
    We will see later how to use the Minho ConfigService (programmatically as we do in the tests, via minho.json file, via system properties or environment variables, via Kubernetes ConfigMap).
  2. in the test, we directly use HTTP client to interact with our blog REST API.
  3. after the test execution, we cleanly stop the Minho instance in the teardown() method.

Build and run

So now, we have two modules ready: minho-blog-jpa and minho-blog-rest. It's time to create a runtime ready to be launched.
In the tests, we programmatically started Minho runtime (Minho.builder().build().start()). For the actual runtime, there's even a simpler approach: put all resources (jar files and other files) in one folder, and just do java -jar minho-boot-0.1.jar.
Let's do the preparation step with Maven, creating a folder with all resource files and jar files. We create the following pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>net.nanthrax.blog</groupId>
        <artifactId>minho-blog</artifactId>
        <version>1.0-SNAPSHOT</version>
        <relativePath>../pom.xml</relativePath>
    </parent>

    <artifactId>minho-blog-runtime</artifactId>

    <dependencies>
        <dependency>
            <groupId>net.nanthrax.blog</groupId>
            <artifactId>minho-blog-jpa</artifactId>
            <version>${project.version}</version>
        </dependency>
        <dependency>
            <groupId>net.nanthrax.blog</groupId>
            <artifactId>minho-blog-rest</artifactId>
            <version>${project.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.karaf.minho</groupId>
            <artifactId>minho-banner</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.karaf.minho</groupId>
            <artifactId>minho-config-json</artifactId>
        </dependency>
    </dependencies>

    <build>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <filtering>true</filtering>
            </resource>
        </resources>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <version>3.3.0</version>
                <executions>
                    <execution>
                        <id>copy-dependencies</id>
                        <phase>package</phase>
                        <goals>
                            <goal>copy-dependencies</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>${project.build.directory}/runtime</outputDirectory>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-resources-plugin</artifactId>
                <version>3.3.0</version>
                <executions>
                    <execution>
                        <id>copy-resources</id>
                        <phase>package</phase>
                        <goals>
                            <goal>copy-resources</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>${project.build.directory}/runtime</outputDirectory>
                            <resources>
                                <resource>
                                    <directory>src/main/resources</directory>
                                    <filtering>true</filtering>
                                </resource>
                            </resources>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

The runtime "assembly" is pretty simple: we copy our minho-blog-jpa and minho-blog-rest modules (and their transitive dependencies, including the minho-boot, minho-jpa, etc) jar files in runtime/target/runtime folder.
You can see we added two additional Minho modules: minho-banner and minho-config-json:
  • minho-banner is a service just displaying a fancy message at startup. It looks for various places for the banner. In our demo, we create the src/main/resources/banner.txt file with our fancy logo at startup :)
  • minho-config-json is more interesting. It's a Minho service that "populate" the ConfigService with a json file. In our demo, we create the src/main/resource/minho.json file to populate the ConfigService. Minho provides different services to populate the ConfigService: minho-config-properties, minho-config-json. NB: by default, the ConfigService is looking for system properties and environment variables, it's what we will use in the Kubernetes part via ConfigMap.
Now we can build the runtime simple with mvn clean install, creating runtime folder in runtime/target/runtime folder.
To launch the runtime, we simple do:

$ cd runtime/target/runtime
$ java -jar minho-boot-0.1.jar
...
Jan 10, 2023 3:31:45 PM org.apache.karaf.minho.banner.WelcomeBannerService onRegister
INFO: 
 ____  _
|  _ \| |
| |_) | | ___   __ _
|  _ <| |/ _ \ / _` |
| |_) | | (_) | (_| |
|____/|_|\___/ \__, |
                __/ |
               |___/

  Welcome to Apahe Karaf Minho Blog (1.0-SNAPSHOT)

Jan 10, 2023 3:31:45 PM org.apache.karaf.minho.boot.service.ServiceRegistry lambda$start$2
INFO: Starting services
Jan 10, 2023 3:31:45 PM org.apache.karaf.minho.boot.service.LifeCycleService start
INFO: Starting lifecycle service
16  BlogEntity  INFO   [main] openjpa.Runtime - Starting OpenJPA 3.2.2
39  BlogEntity  INFO   [main] openjpa.jdbc.JDBC - Using dictionary class "org.apache.openjpa.jdbc.sql.DerbyDictionary".
569  BlogEntity  INFO   [main] openjpa.jdbc.JDBC - Connected to Apache Derby version 10.15 using JDBC driver Apache Derby Embedded JDBC Driver version 10.15.2.0 - (1873585).
799  BlogEntity  WARN   [main] openjpa.Enhance - Creating subclass for "[class net.nanthrax.blog.minho.jpa.BlogEntity]". This means that your application will be less efficient and will consume more memory than it would if you ran the OpenJPA enhancer. Additionally, lazy loading will not be available for one-to-one and many-to-one persistent attributes in types using field access; they will be loaded eagerly instead.
Jan 10, 2023 3:31:46 PM org.eclipse.jetty.server.Server doStart
INFO: jetty-11.0.12; built: 2022-09-14T02:38:00.723Z; git: d5b8c29485f5f56a14be5f20c2ccce81b93c5555; jvm 17.0.2+8-86
Jan 10, 2023 3:31:46 PM org.eclipse.jetty.server.session.DefaultSessionIdManager doStart
INFO: Session workerName=node0
Jan 10, 2023 3:31:46 PM org.glassfish.jersey.server.wadl.WadlFeature configure
WARNING: JAXBContext implementation could not be found. WADL feature is disabled.
Jan 10, 2023 3:31:46 PM org.glassfish.jersey.internal.Errors logErrors
WARNING: The following warnings have been detected: WARNING: The (sub)resource method createBlog in net.nanthrax.blog.minho.rest.BlogApi contains empty path annotation.
WARNING: The (sub)resource method listBlogs in net.nanthrax.blog.minho.rest.BlogApi contains empty path annotation.

Jan 10, 2023 3:31:46 PM org.eclipse.jetty.server.handler.ContextHandler doStart
INFO: Started o.e.j.s.ServletContextHandler@3bb9ca38{/,null,AVAILABLE}
Jan 10, 2023 3:31:46 PM org.eclipse.jetty.server.AbstractConnector doStart
INFO: Started ServerConnector@16267862{HTTP/1.1, (http/1.1)}{0.0.0.0:8080}
Jan 10, 2023 3:31:46 PM org.eclipse.jetty.server.Server doStart
INFO: Started Server@5b84f14{STARTING}[11.0.12,sto=0] @1592ms

We can see our Blog logo at startup :), the runtime is ready.
We can test the REST API with curl:

$ curl http://localhost:8080/blog
[]

All set !

Docker

Now, let's package our runtime as a Docker image. It's actually very easy by just adding jib-maven-plugin to our runtime pom.xml:

            <plugin>
                <groupId>com.google.cloud.tools</groupId>
                <artifactId>jib-maven-plugin</artifactId>
                <version>3.3.1</version>
                <configuration>
                    <containerizingMode>packaged</containerizingMode>
                    <to>
                        <image>minho-blog</image>
                        <tags>
                            <tag>${project.version}</tag>
                        </tags>
                    </to>
                    <container>
                        <mainClass>org.apache.karaf.minho.boot.Main</mainClass>
                        <appRoot>/blog</appRoot>
                        <workingDirectory>/blog</workingDirectory>
                        <creationTime>USE_CURRENT_TIMESTAMP</creationTime>
                    </container>
                </configuration>
                <executions>
                    <execution>
                        <id>docker-build</id>
                        <phase>install</phase>
                        <goals>
                            <goal>dockerBuild</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

That's it !
Let's make a new build of our runtime:

$ mvn clean install -pl runtime

We can now find our docker image in local docker registry:

$ docker images
REPOSITORY                      TAG              IMAGE ID       CREATED          SIZE
minho-blog                      1.0-SNAPSHOT     91f9cbeed27e   53 seconds ago   286MB
minho-blog                      latest           91f9cbeed27e   53 seconds ago   286MB

We can create a docker container with our image:

$ docker run -d --name minho-blog -p 8080:8080 minho-blog

We can see that our container is up, and we can see the container log message:

$ docker ps -a
CONTAINER ID   IMAGE          COMMAND                  CREATED          STATUS                     PORTS                                       NAMES
6bd4e56f3361   minho-blog     "java -cp @/blog/jib…"   40 seconds ago   Up 39 seconds              0.0.0.0:8080->8080/tcp, :::8080->8080/tcp   minho-blog
$ docker logs minho-blog
...
Jan 10, 2023 5:30:02 PM org.apache.karaf.minho.banner.WelcomeBannerService onRegister
INFO: 
 ____  _
|  _ \| |
| |_) | | ___   __ _
|  _ <| |/ _ \ / _` |
| |_) | | (_) | (_| |
|____/|_|\___/ \__, |
                __/ |
               |___/

  Welcome to Apahe Karaf Minho Blog (1.0-SNAPSHOT)

Jan 10, 2023 5:30:02 PM org.apache.karaf.minho.boot.service.ServiceRegistry lambda$start$2
INFO: Starting services
Jan 10, 2023 5:30:02 PM org.apache.karaf.minho.boot.service.LifeCycleService start
INFO: Starting lifecycle service
8  BlogEntity  WARN   [main] openjpa.Runtime - The persistence unit "BlogEntity" was found multiple times in the following resources "[jar:file:/blog/libs/minho-blog-jpa-1.0-SNAPSHOT.jar!/META-INF/persistence.xml]", but persistence unit names should be unique. The first persistence unit matching the provided name in "jar:file:/blog/libs/minho-blog-jpa-1.0-SNAPSHOT.jar!/META-INF/persistence.xml" is being used.
15  BlogEntity  INFO   [main] openjpa.Runtime - Starting OpenJPA 3.2.2
37  BlogEntity  INFO   [main] openjpa.jdbc.JDBC - Using dictionary class "org.apache.openjpa.jdbc.sql.DerbyDictionary".
583  BlogEntity  INFO   [main] openjpa.jdbc.JDBC - Connected to Apache Derby version 10.15 using JDBC driver Apache Derby Embedded JDBC Driver version 10.15.2.0 - (1873585).
814  BlogEntity  WARN   [main] openjpa.Enhance - Creating subclass for "[class net.nanthrax.blog.minho.jpa.BlogEntity]". This means that your application will be less efficient and will consume more memory than it would if you ran the OpenJPA enhancer. Additionally, lazy loading will not be available for one-to-one and many-to-one persistent attributes in types using field access; they will be loaded eagerly instead.
Jan 10, 2023 5:30:03 PM org.eclipse.jetty.server.Server doStart
INFO: jetty-11.0.12; built: 2022-09-14T02:38:00.723Z; git: d5b8c29485f5f56a14be5f20c2ccce81b93c5555; jvm 17.0.5+8
Jan 10, 2023 5:30:03 PM org.eclipse.jetty.server.session.DefaultSessionIdManager doStart
INFO: Session workerName=node0
Jan 10, 2023 5:30:03 PM org.glassfish.jersey.server.wadl.WadlFeature configure
WARNING: JAXBContext implementation could not be found. WADL feature is disabled.
Jan 10, 2023 5:30:04 PM org.glassfish.jersey.internal.Errors logErrors
WARNING: The following warnings have been detected: WARNING: The (sub)resource method createBlog in net.nanthrax.blog.minho.rest.BlogApi contains empty path annotation.
WARNING: The (sub)resource method listBlogs in net.nanthrax.blog.minho.rest.BlogApi contains empty path annotation.

Jan 10, 2023 5:30:04 PM org.eclipse.jetty.server.handler.ContextHandler doStart
INFO: Started o.e.j.s.ServletContextHandler@2be818da{/,null,AVAILABLE}
Jan 10, 2023 5:30:04 PM org.eclipse.jetty.server.AbstractConnector doStart
INFO: Started ServerConnector@3ec300f1{HTTP/1.1, (http/1.1)}{0.0.0.0:8080}
Jan 10, 2023 5:30:04 PM org.eclipse.jetty.server.Server doStart
INFO: Started Server@2650f79{STARTING}[11.0.12,sto=0] @1653ms

As we expose port 8080, we can access our Blog REST API from the container:

$ curl http://localhost:8080/blog
[]

All set !
We can stop the container now:

$ docker stop minho-blog
$ docker rm minho-blog

Kubernetes

As we have a Docker image, we can easily orchestrate with Kubernetes.
The source code contains the Kubernetes manifest files ready to deploy with kubectl.
In this section, we will deploy on AWS EKS and we will use ConfigMap to show how to work with Minho ConfigService.
First, we create a ECR repository to upload our Docker image. This ECR repository will be used by our EKS cluster later.
We create minho-blog ECR repository:
You have to use AWS ECR login password with docker login:

$ aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin public.ecr.aws/xxxxxx

Now, we can tag our minho-blog image with ECR location as prefix:

$ docker tag minho-blog:latest public.ecr.aws/xxxxxx/minho-blog:1.0

And we can push minho-blog image to our ECR repository:

$ docker push public.ecr.aws/xxxxxx/minho-blog:1.0

We can see now the image on our ECR repository:
Let's create a EKS cluster now (with nodes group, etc):
Now we can "connect" kubectl on our EKS cluster:

$ aws eks update-kubeconfig --region us-east-2 --name minho-eks
$ kubectl get nodes
...

You can find all K8S manifest files in runtimes/kubernetes folder.
For clarity, I made each resource in a dedicated file (it would have been possible to use a single file).
Let's start by creating a minho namespace:

$ kubectl apply -f namespace.yml
namespace/minho created

Now we can create minho-blog-config ConfigMap. This ConfigMap can pass configuration to the runtime (populating the Minho ConfigService):

$ kubectl apply -f configmap.yml
configmap/minho-blog-config created

Let's create the deployment now:

$ kubectl apply -f deployment.yml
deployment.apps/minho-blog created

Now, we can check the status of the deployment:

$ kubectl get deployments -n minho
NAME          READY   UP-TO-DATE   AVAILABLE   AGE
minho-blog    1/1     1            1           20s

and the assocciated pod running:

$ kubectl get pods -n minho
NAME                          READY   STATUS    RESTARTS   AGE
minho-blog-8699fd7547-vj44p   1/1     Running   0          42s

We can see the logs of the running pod:

$ kubectl logs minho-blog-8699fd7547-vj44p -n minho
...
Jan 19, 2023 7:49:48 PM org.apache.karaf.minho.banner.WelcomeBannerService onRegister
INFO: 
 ____  _
|  _ \| |
| |_) | | ___   __ _
|  _ <| |/ _ \ / _` |
| |_) | | (_) | (_| |
|____/|_|\___/ \__, |
                __/ |
               |___/

  Welcome to Apache Karaf Minho Blog (1.0-SNAPSHOT)

Jan 19, 2023 7:49:48 PM org.apache.karaf.minho.boot.service.ServiceRegistry lambda$start$2
INFO: Starting services
Jan 19, 2023 7:49:48 PM org.apache.karaf.minho.boot.service.LifeCycleService start
INFO: Starting lifecycle service
Jan 19, 2023 7:49:48 PM org.eclipse.jetty.server.Server doStart
INFO: jetty-11.0.12; built: 2022-09-14T02:38:00.723Z; git: d5b8c29485f5f56a14be5f20c2ccce81b93c5555; jvm 17.0.5+8
Jan 19, 2023 7:49:48 PM org.eclipse.jetty.server.session.DefaultSessionIdManager doStart
INFO: Session workerName=node0
Jan 19, 2023 7:49:49 PM org.glassfish.jersey.server.wadl.WadlFeature configure
WARNING: JAXBContext implementation could not be found. WADL feature is disabled.
Jan 19, 2023 7:49:49 PM org.eclipse.jetty.server.handler.ContextHandler doStart
INFO: Started o.e.j.s.ServletContextHandler@18e7143f{/,null,AVAILABLE}
Jan 19, 2023 7:49:49 PM org.eclipse.jetty.server.AbstractConnector doStart
INFO: Started ServerConnector@48eff760{HTTP/1.1, (http/1.1)}{0.0.0.0:8080}
Jan 19, 2023 7:49:49 PM org.eclipse.jetty.server.Server doStart
INFO: Started Server@694e1548{STARTING}[11.0.12,sto=0] @1985ms
20  BlogEntity  WARN   [main] openjpa.Runtime - The persistence unit "BlogEntity" was found multiple times in the following resources "[jar:file:/blog/libs/minho-blog-jpa-1.0-SNAPSHOT.jar!/META-INF/persistence.xml]", but persistence unit names should be unique. The first persistence unit matching the provided name in "jar:file:/blog/libs/minho-blog-jpa-1.0-SNAPSHOT.jar!/META-INF/persistence.xml" is being used.
38  BlogEntity  INFO   [main] openjpa.Runtime - Starting OpenJPA 3.2.2
96  BlogEntity  INFO   [main] openjpa.jdbc.JDBC - Using dictionary class "org.apache.openjpa.jdbc.sql.HSQLDictionary".
460  BlogEntity  INFO   [main] openjpa.jdbc.JDBC - Connected to HSQL Database Engine version 2.6 using JDBC driver HSQL Database Engine Driver version 2.6.1.
681  BlogEntity  WARN   [main] openjpa.Enhance - Creating subclass for "[class net.nanthrax.blog.minho.jpa.BlogEntity]". This means that your application will be less efficient and will consume more memory than it would if you ran the OpenJPA enhancer. Additionally, lazy loading will not be available for one-to-one and many-to-one persistent attributes in types using field access; they will be loaded eagerly instead.
...

Now, we can deploy the service:

$ kubectl apply -f service.yaml 
service/minho-blog created

Now, we can access the public address of our EKS cluster to get the service endpoint.

Creating native executable with GraalVM

Minho supports GraalVM and native image. The big advtange is a very fast loading and memory footprint reduction.
As prequisite, you have to get GraalVM installed on your machine, and install native-image:

$ gu install native-image

Now, in the runtime/pom.xml file, we can add a profile to build native, using even Apache Geronimo arthur-maven-plugin or Graalvm native-maven-plugin.
If Minho supports GraalVM, it's not necessary the case for all dependencies (Jetty, OpenJPA, Derby, ...). Most of the time, we have to add Graalvm metadata to deal with reflections or resources.
To simplify this blog post, and the corresponding source code, let's create a new module and simple Minho runtime.
We create a new service that registers a servlet. The pom.xml of this module is pretty simple:


<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>net.nanthrax.blog</groupId>
        <artifactId>minho-blog</artifactId>
        <version>1.0-SNAPSHOT</version>
        <relativePath>../pom.xml</relativePath>
    </parent>

    <artifactId>minho-blog-servlet</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.apache.karaf.minho</groupId>
            <artifactId>minho-http</artifactId>
        </dependency>
    </dependencies>

</project>

Very simple, isn't it ? :) Basically, we just define minho-http dependnecy, providing everything we need for our servlet.
Now, let's create a servlet:

package net.nanthrax.blog.minho.servlet;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.karaf.minho.boot.spi.Service;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.Properties;

public class HelloServlet extends HttpServlet implements Service {

    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(response.getOutputStream()))) {
            writer.write("Hello world!");
        }
    }

    @Override
    public Properties properties() {
        Properties properties = new Properties();
        properties.put("contextPath", "/hello");
        return properties;
    }

}

A classic servlet after all. The only Minho specific are:
  • our servlet implements Service, allowing Minho to automatically register the servlet
  • as Minho HTTP service is able to automatically deploy our servlet, we have to give hints how to deploy (especially the context path). For that, we provide the properties() method, used by the HTTP service when it deploys our servlet. It's where we define the contextPath (/hello in our case).
As our servlet is a Minho service, as usual, we have to provide META-INF/services/org.apache.karaf.minho.boot.spi.Service file, containing net.nanthrax.blog.minho.servlet.HelloServlet.
Done, our servlet is ready.
Now, we can create a Minho runtime, and use GraalVM Maven plugin to create a native executable.
Let's create a native module with this pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>net.nanthrax.blog</groupId>
        <artifactId>minho-blog</artifactId>
        <version>1.0-SNAPSHOT</version>
        <relativePath>../pom.xml</relativePath>
    </parent>

    <artifactId>minho-blog-native</artifactId>

    <dependencies>
        <dependency>
            <groupId>net.nanthrax.blog</groupId>
            <artifactId>minho-blog-servlet</artifactId>
            <version>${project.version}</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.graalvm.buildtools</groupId>
                <artifactId>native-maven-plugin</artifactId>
                <version>0.9.19</version>
                <extensions>true</extensions>
                <executions>
                    <execution>
                        <id>build-native</id>
                        <phase>package</phase>
                        <goals>
                            <goal>compile-no-fork</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <fallback>false</fallback>
                    <buildArgs>
                        <arg>-H:Class=org.apache.karaf.minho.boot.Main -H:Name=minho-blog</arg>
                    </buildArgs>
                    <agent>
                        <enabled>true</enabled>
                        <options>
                            <option>experimental-class-loader-support</option>
                        </options>
                    </agent>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

We use the native-maven-plugin to build the native executable.
The minho-http service uses Jetty as web container.
Native image uses a different way of compiling Java compared to the traditional JVM. It distinguishes between build time and run time. At the image build time, the native-image builder performs static analysis to find all the methods that are reachable from the entry point of the application. The builder then compiles only these methods into an executable binary. Because of this different compilation model, the Java application can behave differently when compiled into a native image. Minho guarantees to be GraalVM builder compliant.
Native image provides an optimization to reduce the memory footprint and startup time. This approach relies on a "closed-world assumption" in which all code is known at build time, meaning no new code is loaded at run time.
Due to that, some Java functionnalities require metadata. To be suitable for closed-world assumption, the metadata are used to pass to native-image at build time. This metadata ensures that a ,native image uses the minimulm amount of space necessary. It's basically a descriptor especially for:
  • Java Reflections (the java.lang.reflect.* API) enables Java code to examine its own classes, methods, fields, and their properties at run time.
  • JNI allows native code to access classes, methods, fields and their properties at run time.
  • Resources and Resource Bundles allow arbitrary files present in the application to be loaded.
  • Dynamic Proxies create classes on demand that implement a given list of interfaces.
  • Serialization enables writing and reading Java objects to and from streams.
  • Predefined Classes provide support for dynamicaklly generated classes.
You can provide the metadata using JSON files. These files have to be in the META-INF/native-code/[group.id]/[artifact.id].
Back on our native runtime, let's build our native executable with mvn clean install -pl native:

...
[7/7] Creating image...                                                                                  (3,1s @ 1,82GB)
   8,53MB (42,94%) for code area:    13 826 compilation units
  11,11MB (55,98%) for image heap:  148 194 objects and 8 resources
 219,05KB ( 1,08%) for other data
  19,85MB in total
------------------------------------------------------------------------------------------------------------------------
Top 10 packages in code area:                               Top 10 object types in image heap:
 810,66KB java.util                                            1,82MB byte[] for code metadata
 529,00KB java.lang.invoke                                     1,36MB java.lang.String
 469,28KB org.eclipse.jetty.server                             1,12MB byte[] for general heap data
 407,79KB com.sun.crypto.provider                              1,08MB java.lang.Class
 394,12KB java.lang                                          890,53KB byte[] for java.lang.String
 364,59KB org.eclipse.jetty.http                             484,50KB java.util.HashMap$Node
 330,61KB java.text                                          382,73KB com.oracle.svm.core.hub.DynamicHubCompanion
 288,62KB java.util.concurrent                               274,31KB java.util.concurrent.ConcurrentHashMap$Node
 230,91KB java.util.stream                                   261,14KB java.util.HashMap$Node[]
 216,87KB org.eclipse.jetty.util                             258,88KB java.lang.String[]
   4,48MB for 170 more packages                                2,34MB for 1199 more object types
------------------------------------------------------------------------------------------------------------------------
                        0,8s (1,5% of total time) in 20 GCs | Peak RSS: 4,92GB | CPU load: 7,90
------------------------------------------------------------------------------------------------------------------------
Produced artifacts:
 /Users/jbonofre/Workspace/minho-blog/native/target/minho-blog (executable)
 /Users/jbonofre/Workspace/minho-blog/native/target/minho-blog.build_artifacts.txt (txt)
========================================================================================================================
Finished generating 'minho-blog' in 50,5s.
...

Now, we have a executable binary: native/target/minho-blog. However, if we launch minho-blog, we have the following error:

...
janv. 17, 2023 5:13:59 PM org.eclipse.jetty.server.Server doStart
INFO: jetty-11.0.12; built: unknown; git: unknown; jvm 17.0.5
Exception in thread "main" java.lang.Error: java.lang.NoSuchMethodException: java.lang.Byte.valueOf(java.lang.String)
        at org.eclipse.jetty.util.TypeUtil.(TypeUtil.java:180)
        at org.eclipse.jetty.http.PreEncodedHttpField.(PreEncodedHttpField.java:42)
        at org.eclipse.jetty.http.MimeTypes$Type.(MimeTypes.java:91)
        at org.eclipse.jetty.http.MimeTypes$Type.(MimeTypes.java:49)
        at org.eclipse.jetty.http.MimeTypes.lambda$static$0(MimeTypes.java:169)
        at org.eclipse.jetty.util.Index$Builder.withAll(Index.java:346)
        at org.eclipse.jetty.http.MimeTypes.(MimeTypes.java:166)
        at org.eclipse.jetty.server.handler.ContextHandler.doStart(ContextHandler.java:883)
        at org.eclipse.jetty.servlet.ServletContextHandler.doStart(ServletContextHandler.java:306)
        at org.eclipse.jetty.util.component.AbstractLifeCycle.start(AbstractLifeCycle.java:93)
        at org.eclipse.jetty.util.component.ContainerLifeCycle.start(ContainerLifeCycle.java:171)
        at org.eclipse.jetty.util.component.ContainerLifeCycle.doStart(ContainerLifeCycle.java:114)
        at org.eclipse.jetty.server.handler.AbstractHandler.doStart(AbstractHandler.java:89)
        at org.eclipse.jetty.server.handler.StatisticsHandler.doStart(StatisticsHandler.java:253)
        at org.eclipse.jetty.util.component.AbstractLifeCycle.start(AbstractLifeCycle.java:93)
        at org.eclipse.jetty.util.component.ContainerLifeCycle.start(ContainerLifeCycle.java:171)
        at org.eclipse.jetty.server.Server.start(Server.java:470)
        at org.eclipse.jetty.util.component.ContainerLifeCycle.doStart(ContainerLifeCycle.java:114)
        at org.eclipse.jetty.server.handler.AbstractHandler.doStart(AbstractHandler.java:89)
        at org.eclipse.jetty.server.Server.doStart(Server.java:415)
        at org.eclipse.jetty.util.component.AbstractLifeCycle.start(AbstractLifeCycle.java:93)
        at org.apache.karaf.minho.web.jetty.JettyWebContainerService.lambda$onRegister$0(JettyWebContainerService.java:107)
        at org.apache.karaf.minho.boot.service.LifeCycleService.lambda$start$0(LifeCycleService.java:69)
        at java.base@17.0.5/java.util.ArrayList.forEach(ArrayList.java:1511)
        at org.apache.karaf.minho.boot.service.LifeCycleService.start(LifeCycleService.java:67)
        at org.apache.karaf.minho.boot.service.ServiceRegistry.lambda$start$2(ServiceRegistry.java:128)
        at java.base@17.0.5/java.util.Optional.ifPresent(Optional.java:178)
        at org.apache.karaf.minho.boot.service.ServiceRegistry.start(ServiceRegistry.java:126)
        at org.apache.karaf.minho.boot.Minho.start(Minho.java:59)
        at org.apache.karaf.minho.boot.Main.main(Main.java:63)
Caused by: java.lang.NoSuchMethodException: java.lang.Byte.valueOf(java.lang.String)
        at java.base@17.0.5/java.lang.Class.getMethod(DynamicHub.java:2227)
        at org.eclipse.jetty.util.TypeUtil.(TypeUtil.java:151)
        ... 29 more

The reason is because Jetty is using reflection to load its configuration. So, it means we have to add metadata for this method, loading the corresponding reflection.
We can generate the metadata by hand (following Reachability Metadata documentation), meaning a json file with something like:

{
  {
    "condition": {
      "typeReachable": "org.eclipse.jetty.util.TypeUtil"
    },
    "name": "java.lang.Byte",
    "queriedMethods": [
      {
        "name": "valueOf",
        "parameterTypes": [
          "java.lang.String"
        ]
      }
    ]
  }
}

You can use the tracing agent to collect metadata and create a complete JSON file.
Fortunately, metadata is ready for some libraries (GraalVM Reachability Metadata Repository). We gonna use the Jetty one from https://github.com/oracle/graalvm-reachability-metadata/tree/master/metadata/org.eclipse.jetty/jetty-server, just copying the files in src/main/resources/META-INF/native-image.
Now, if we build again and run directly ./target/minho-blog:

...
INFO: Started ServerConnector@5a1e81ef{HTTP/1.1, (http/1.1)}{0.0.0.0:8080}
janv. 17, 2023 5:28:10 PM org.eclipse.jetty.server.Server doStart
INFO: Started Server@380a153f{STARTING}[11.0.12,sto=0] @24ms

it works (and it's super fast), and we can access our servlet on http://localhost:8080/hello.
If Jetty is provided on the GraalVM Reachability Metadata Repository, it's not the case for all dependencies. For instance, OpenJPA metadata are not there. You have few options:
  • you create the metadata by hand (and eventually contribute to the graalvm-reachability-metadata repository)
  • you use metadata from someone else (human or tool)
For the later, we can use Apache Geronimo Arthur, especially the arthur-maven-plugin. Arthur analyses your code to generate the metadata. It also provides Knights: a knight is an extension that deal with metadata from some dependencies, mixing "static" metadata with generated metadata. For instance, you have OpenJPA Knight. Arthur is also able to download GraalVM for you (no need to install it by yourself as we did at the beginning of this section).
It means that we can replace the native-maven-plugin with the arthur-maven-plugin. It means, our pom.xml can look like:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>net.nanthrax.blog</groupId>
        <artifactId>minho-blog</artifactId>
        <version>1.0-SNAPSHOT</version>
        <relativePath>../pom.xml</relativePath>
    </parent>

    <artifactId>minho-blog-native</artifactId>

    <dependencies>
        <dependency>
            <groupId>net.nanthrax.blog</groupId>
            <artifactId>minho-blog-servlet</artifactId>
            <version>${project.version}</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.geronimo.arthur</groupId>
                <artifactId>arthur-maven-plugin</artifactId>
                <version>1.0.5</version>
                <extensions>true</extensions>
                <executions>
                    <execution>
                        <id>build-native</id>
                        <phase>package</phase>
                        <goals>
                            <goal>native-image</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <main>org.apache.karaf.minho.boot.Main</main>
                    <nativeImage>native-image</nativeImage>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

And we obtain the same executable.

Applications colocation

One interesting Minho feature is the applications colocation. Minho can manage and colocate several applications in an unique runtime. The goal is to optimize resources consumtion, especially in cloud context.
To illustrate this, we will create a simple Spring Boot application (that we will run two times with different configuration in a single runtime) and use a different approach to create Minho distribution (just for demo, we can still use the runtime we created before).
Let's start by creating my-spring-boot-application Spring Boot application: a very simple Spring Boot application just displaying Hello World on a HTTP endpoint. Let's start with pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>net.nanthrax.blog</groupId>
        <artifactId>minho-blog</artifactId>
        <version>1.0-SNAPSHOT</version>
        <relativePath>../pom.xml</relativePath>
    </parent>

    <artifactId>my-spring-boot-app</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>3.0.1</version>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-tomcat</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jetty</artifactId>
            <version>3.0.1</version>
        </dependency>
        <dependency>
            <groupId>jakarta.servlet</groupId>
            <artifactId>jakarta.servlet-api</artifactId>
            <version>5.0.0</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>3.0.1</version>
                <executions>
                    <execution>
                        <id>repackage</id>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

Now, we create a simple Spring RestController:

package net.nanthrax.blog.minho.myapp;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@SpringBootApplication
public class FirstApp {

    @RequestMapping("/")
    String home() {
        return "Hello World!";
    }

    public static void main(String[] args) {
        SpringApplication.run(FirstApp.class, args);
    }

}

As for any Spring Boot application, we can run with mvn spring-boot:run:

$ mvn install spring-boot:run
...
[INFO] --- spring-boot-maven-plugin:3.0.1:run (default-cli) @ my-spring-boot-app ---
[INFO] Attaching agents: []

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.0.1)

2023-01-18T20:31:23.835+01:00  INFO 58480 --- [           main] net.nanthrax.blog.minho.myapp.FirstApp   : Starting FirstApp using Java 17.0.5 with PID 58480 (/Users/jbonofre/Workspace/minho-blog/my-spring-boot-app/target/classes started by jbonofre in /Users/jbonofre/Workspace/minho-blog/my-spring-boot-app)
2023-01-18T20:31:23.840+01:00  INFO 58480 --- [           main] net.nanthrax.blog.minho.myapp.FirstApp   : No active profile set, falling back to 1 default profile: "default"
2023-01-18T20:31:24.666+01:00  INFO 58480 --- [           main] o.s.b.w.e.j.JettyServletWebServerFactory : Server initialized with port: 8080
2023-01-18T20:31:24.668+01:00  INFO 58480 --- [           main] org.eclipse.jetty.server.Server          : jetty-11.0.13; built: 2022-12-07T20:47:15.149Z; git: a04bd1ccf844cf9bebc12129335d7493111cbff6; jvm 17.0.5+8-jvmci-22.3-b08
2023-01-18T20:31:24.715+01:00  INFO 58480 --- [           main] o.e.j.s.h.ContextHandler.application     : Initializing Spring embedded WebApplicationContext
2023-01-18T20:31:24.717+01:00  INFO 58480 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 830 ms
2023-01-18T20:31:24.783+01:00  INFO 58480 --- [           main] o.e.j.s.session.DefaultSessionIdManager  : Session workerName=node0
2023-01-18T20:31:24.790+01:00  INFO 58480 --- [           main] o.e.jetty.server.handler.ContextHandler  : Started o.s.b.w.e.j.JettyEmbeddedWebAppContext@164a62bf{application,/,[file:///private/var/folders/09/drqlm0993rx6_739ty4scqlh0000gn/T/jetty-docbase.8080.11217831767068239824/],AVAILABLE}
2023-01-18T20:31:24.795+01:00  INFO 58480 --- [           main] org.eclipse.jetty.server.Server          : Started Server@9a2ec9b{STARTING}[11.0.13,sto=0] @1665ms
2023-01-18T20:31:25.040+01:00  INFO 58480 --- [           main] o.e.j.s.h.ContextHandler.application     : Initializing Spring DispatcherServlet 'dispatcherServlet'
2023-01-18T20:31:25.040+01:00  INFO 58480 --- [           main] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2023-01-18T20:31:25.041+01:00  INFO 58480 --- [           main] o.s.web.servlet.DispatcherServlet        : Completed initialization in 1 ms
2023-01-18T20:31:25.075+01:00  INFO 58480 --- [           main] o.e.jetty.server.AbstractConnector       : Started ServerConnector@963176{HTTP/1.1, (http/1.1)}{0.0.0.0:8080}
2023-01-18T20:31:25.076+01:00  INFO 58480 --- [           main] o.s.b.web.embedded.jetty.JettyWebServer  : Jetty started on port(s) 8080 (http/1.1) with context path '/'
2023-01-18T20:31:25.085+01:00  INFO 58480 --- [           main] net.nanthrax.blog.minho.myapp.FirstApp   : Started FirstApp in 1.644 seconds (process running for 1.957)

We are good. Now, let's create a coloc Minho runtime that will run the Spring Boot application two time with different port number.
To illustrate another way to use Minho, we gonna use a different approach: creating a Uber jar with Minho artifacts ready to run. We use the maven-assembly-plugin :

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>net.nanthrax.blog</groupId>
        <artifactId>minho-blog</artifactId>
        <version>1.0-SNAPSHOT</version>
        <relativePath>../pom.xml</relativePath>
    </parent>

    <artifactId>coloc</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.apache.karaf.minho</groupId>
            <artifactId>minho-config-json</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.karaf.minho</groupId>
            <artifactId>minho-spring-boot</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>3.4.2</version>
                <executions>
                    <execution>
                        <id>repackage</id>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                        <configuration>
                            <descriptors>
                                <descriptor>src/repackage/minho.xml</descriptor>
                            </descriptors>
                            <appendAssemblyId>false</appendAssemblyId>
                            <archive>
                                <manifest>
                                    <mainClass>org.apache.karaf.minho.boot.Main</mainClass>
                                </manifest>
                            </archive>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

The assembly descriptor is pretty simple:

<?xml version="1.0" encoding="UTF-8"?>
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.1.0"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.1.0 http://maven.apache.org/xsd/assembly-2.1.0.xsd">

    <id>minho-coloc-jar</id>

    <formats>
        <format>jar</format>
    </formats>

    <includeBaseDirectory>false</includeBaseDirectory>

    <dependencySets>
        <dependencySet>
            <outputDirectory>/</outputDirectory>
            <useProjectArtifact>true</useProjectArtifact>
            <unpack>true</unpack>
            <scope>runtime</scope>
        </dependencySet>
    </dependencySets>

    <containerDescriptorHandlers>
        <containerDescriptorHandler>
            <handlerName>metaInf-services</handlerName>
        </containerDescriptorHandler>
    </containerDescriptorHandlers>

</assembly>

Now, we just create a minho.json runtime configuration in the resources. This minho.json file will be included in the runtime. In this file, we define the location of Spring Boot applications (URL, mvn URL is supported):

{
  "applications": [
    {
      "name": "one-app",
      "type": "spring-boot",
      "url": "file:./app/my-spring-boot-app-1.0-SNAPSHOT.jar",
      "properties": {
        "args": "--server.port=8181"
      }
    },
    {
      "name": "another-app",
      "type": "spring-boot",
      "url": "file:./app/my-spring-boot-app-1.0-SNAPSHOT.jar",
      "properties": {
        "args": "--server.port=8282"
      }
    }
  ]
}

We can see we will start our Spring Boot application two times, with a different configurattion (properties), uing different port numbers.
Now, we can build our coloc runtime with mvn clean install. We can copy our Spring Boot application jar in coloc/target/app folder:

$ mkdir coloc/target/app
$ cp my-spring-boot-app/target/my-spring-boot-app-1.0-SNAPSHOT.jar coloc/target/app

Now, we can start our runtime with java -jar coloc-1.0-SNAPSHOT.jar:

$ cd coloc/target
$ java -jar coloc-1.0-SNAPSHOT.jar
...
janv. 18, 2023 8:45:39 PM org.apache.karaf.minho.springboot.SpringBootApplicationManagerService start
INFO: Starting Spring Boot module file:./app/my-spring-boot-app-1.0-SNAPSHOT.jar

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.0.1)

2023-01-18T20:45:39.865+01:00  INFO 58930 --- [           main] net.nanthrax.blog.minho.myapp.FirstApp   : Starting FirstApp using Java 17.0.5 with PID 58930 (/Users/jbonofre/Workspace/minho-blog/coloc/target/./app/my-spring-boot-app-1.0-SNAPSHOT.jar started by jbonofre in /Users/jbonofre/Workspace/minho-blog/coloc/target)
2023-01-18T20:45:39.868+01:00  INFO 58930 --- [           main] net.nanthrax.blog.minho.myapp.FirstApp   : No active profile set, falling back to 1 default profile: "default"
2023-01-18T20:45:40.964+01:00  INFO 58930 --- [           main] o.s.b.w.e.j.JettyServletWebServerFactory : Server initialized with port: 8181
2023-01-18T20:45:40.966+01:00  INFO 58930 --- [           main] org.eclipse.jetty.server.Server          : jetty-11.0.13; built: 2022-12-07T20:47:15.149Z; git: a04bd1ccf844cf9bebc12129335d7493111cbff6; jvm 17.0.5+8-jvmci-22.3-b08
2023-01-18T20:45:41.023+01:00  INFO 58930 --- [           main] o.e.j.s.h.ContextHandler.application     : Initializing Spring embedded WebApplicationContext
2023-01-18T20:45:41.024+01:00  INFO 58930 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1087 ms
2023-01-18T20:45:41.119+01:00  INFO 58930 --- [           main] o.e.j.s.session.DefaultSessionIdManager  : Session workerName=node0
2023-01-18T20:45:41.128+01:00  INFO 58930 --- [           main] o.e.jetty.server.handler.ContextHandler  : Started o.s.b.w.e.j.JettyEmbeddedWebAppContext@3bcbb589{application,/,[file:///private/var/folders/09/drqlm0993rx6_739ty4scqlh0000gn/T/jetty-docbase.8181.4347411586576066397/],AVAILABLE}
2023-01-18T20:45:41.138+01:00  INFO 58930 --- [           main] org.eclipse.jetty.server.Server          : Started Server@42bc14c1{STARTING}[11.0.13,sto=0] @2439ms
2023-01-18T20:45:41.480+01:00  INFO 58930 --- [           main] o.e.j.s.h.ContextHandler.application     : Initializing Spring DispatcherServlet 'dispatcherServlet'
2023-01-18T20:45:41.481+01:00  INFO 58930 --- [           main] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2023-01-18T20:45:41.482+01:00  INFO 58930 --- [           main] o.s.web.servlet.DispatcherServlet        : Completed initialization in 0 ms
2023-01-18T20:45:41.511+01:00  INFO 58930 --- [           main] o.e.jetty.server.AbstractConnector       : Started ServerConnector@30e868be{HTTP/1.1, (http/1.1)}{0.0.0.0:8181}
2023-01-18T20:45:41.512+01:00  INFO 58930 --- [           main] o.s.b.web.embedded.jetty.JettyWebServer  : Jetty started on port(s) 8181 (http/1.1) with context path '/'
2023-01-18T20:45:41.522+01:00  INFO 58930 --- [           main] net.nanthrax.blog.minho.myapp.FirstApp   : Started FirstApp in 2.142 seconds (process running for 2.823)
2023-01-18T20:45:41.526+01:00  INFO 58930 --- [           main] .m.s.SpringBootApplicationManagerService : Starting Spring Boot module file:./app/my-spring-boot-app-1.0-SNAPSHOT.jar

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.0.1)

2023-01-18T20:45:42.105+01:00  INFO 58930 --- [           main] net.nanthrax.blog.minho.myapp.FirstApp   : Starting FirstApp using Java 17.0.5 with PID 58930 (/Users/jbonofre/Workspace/minho-blog/coloc/target/./app/my-spring-boot-app-1.0-SNAPSHOT.jar started by jbonofre in /Users/jbonofre/Workspace/minho-blog/coloc/target)
2023-01-18T20:45:42.107+01:00  INFO 58930 --- [           main] net.nanthrax.blog.minho.myapp.FirstApp   : No active profile set, falling back to 1 default profile: "default"
2023-01-18T20:45:42.971+01:00  INFO 58930 --- [           main] o.s.b.w.e.j.JettyServletWebServerFactory : Server initialized with port: 8282
2023-01-18T20:45:42.974+01:00  INFO 58930 --- [           main] org.eclipse.jetty.server.Server          : jetty-11.0.13; built: 2022-12-07T20:47:15.149Z; git: a04bd1ccf844cf9bebc12129335d7493111cbff6; jvm 17.0.5+8-jvmci-22.3-b08
2023-01-18T20:45:43.018+01:00  INFO 58930 --- [           main] o.e.j.s.h.ContextHandler.application     : Initializing Spring embedded WebApplicationContext
2023-01-18T20:45:43.018+01:00  INFO 58930 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 862 ms
2023-01-18T20:45:43.096+01:00  INFO 58930 --- [           main] o.e.j.s.session.DefaultSessionIdManager  : Session workerName=node0
2023-01-18T20:45:43.103+01:00  INFO 58930 --- [           main] o.e.jetty.server.handler.ContextHandler  : Started o.s.b.w.e.j.JettyEmbeddedWebAppContext@9d1a267{application,/,[file:///private/var/folders/09/drqlm0993rx6_739ty4scqlh0000gn/T/jetty-docbase.8282.4386261170406112556/],AVAILABLE}
2023-01-18T20:45:43.105+01:00  INFO 58930 --- [           main] org.eclipse.jetty.server.Server          : Started Server@3f92c349{STARTING}[11.0.13,sto=0] @4405ms
2023-01-18T20:45:43.387+01:00  INFO 58930 --- [           main] o.e.j.s.h.ContextHandler.application     : Initializing Spring DispatcherServlet 'dispatcherServlet'
2023-01-18T20:45:43.387+01:00  INFO 58930 --- [           main] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2023-01-18T20:45:43.388+01:00  INFO 58930 --- [           main] o.s.web.servlet.DispatcherServlet        : Completed initialization in 1 ms
2023-01-18T20:45:43.396+01:00  INFO 58930 --- [           main] o.e.jetty.server.AbstractConnector       : Started ServerConnector@3c0036b{HTTP/1.1, (http/1.1)}{0.0.0.0:8282}
2023-01-18T20:45:43.397+01:00  INFO 58930 --- [           main] o.s.b.web.embedded.jetty.JettyWebServer  : Jetty started on port(s) 8282 (http/1.1) with context path '/'
2023-01-18T20:45:43.404+01:00  INFO 58930 --- [           main] net.nanthrax.blog.minho.myapp.FirstApp   : Started FirstApp in 1.66 seconds (process running for 4.704)

We can see the two applications colocated, with different configuration. We can now access to http://localhost:8181/ and http://localhost:8282/.
Minho provides application managers for Spring Boot and OSGi. We plan to add new managers for Microprofile, CDI, ....

What's next ? Minho is dead, long life to Minho

We started Minho in the Apacche Karaf project. But quickly, the scope has grown, far from the Karaf core objectives.
The motivation behind Minho is to create a new runtime, focusing on colocalization support of multiple applications to optimize cloud resources more efficiently than containers can do by using JVM features.
We realized we need a neutral and stable backbone to write out software so also PoC-ed some frameworks we would reuse.
Since this framework is quite generic we would like to promote it as a Java cloud solution which enables it to go native efficiently and easily (targeting GraalVM).
All the idea behind is to limit the dependency stack but also the memory, CPU and disk IO usages.
So, we would like to create a new Apache TLP (TopLevelProject) providing a full Java stack answering these needs:
  • A light cloud friendly stack.
  • A colocalization backbone.
  • A set of cloud deployment helpers.
Goal is really to go fast and with a single reference project for the cloud.
We are working in the preparation steps right now. I will keep you posted soon about this new project, starting from Minho.
Stay tuned !

Comments

Popular posts from this blog

Quarkus and "meta" extension

Apache Karaf Minho and OpenTelemetry