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 includesminho-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 useminho-jpa
which provide a JPA engine ready to be used (by default, Apache OpenJPA).
We will "just" create:
BlogEntity
META-INF/peristente.xml
- 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
andminho-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 theonRegister(ServiceRegistry serviceRegistry)
andpriority()
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:
- the logic in
onRegister()
method is called when Minho loads the service - optionaly, we can add our logic in
LifeCycleService#onStart()
that will be called after Minho loaded all services, during startup phase - optionaly, we can add our logic in
LifeCycleService#onShutdown()
that will be called when Minho stops
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 theBlogJpaService
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>
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 usedminho-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
andminho-rest
dependencies as we will create a REST API - the
minho-blog-jpa
dependencies is there because we will use ourBlogJpaService
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
:
- the
setup()
(executed before each test), we start Minho, and load the required services: MinhoConfigService
, MinhoLifeCycleService
, MinhoOpenJPAService
, ourBlogJpaService
, MinhoJettyWebContainerService
, MinhoJerseyRestService
. We set some properties for our REST service via the MinhoConfigService
:rest.packages
contains the package containing our JAX-RS annotated classes,net.nanthrax.blog.minho.rest
in our caserest.path
is the location of the exposed REST API,/blog/*
in our case
ConfigService
(programmatically as we do in the tests, viaminho.json
file, via system properties or environment variables, via KubernetesConfigMap
). - in the test, we directly use HTTP client to interact with our blog REST API.
- 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 thesrc/main/resources/banner.txt
file with our fancy logo at startup :)minho-config-json
is more interesting. It's a Minho service that "populate" theConfigService
with a json file. In our demo, we create thesrc/main/resource/minho.json
file to populate theConfigService
. Minho provides different services to populate theConfigService
:minho-config-properties
,minho-config-json
. NB: by default, theConfigService
is looking for system properties and environment variables, it's what we will use in the Kubernetes part viaConfigMap
.
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 addingjib-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 thecontextPath
(/hello
in our case).
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.
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)
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.
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
Post a Comment