Just a quick note to say it is now possible, with a bit of fiddling. I’ve been able to get a Camunda (7.21) spring boot (3.4.2) application compiled using native-maven-plugin.
Note that I haven’t done much java for many many years, so my answers can absolutely be improved. But the headline is it works and is seemingly as quick, but much less memory usage. And single binary with no runtime java.
-
Groovy and javascript scripts in bpmn didn’t immediately work so i replaced them with expressions (we only had a few so it wasnt a big deal), but it might be possible to get working.
-
Resources for the web apps wouldn’t load from servlet-context, i had to prepend “META-INF/resources/” to in
getClasspathResourceAsStream
inAbstractAppPluginRootResource
(which i override by copypasting into my src dir -
Memory usage dropped from 600mb-800mb to 150mb-400mb (going up and down with gc under load) with a start time of 0.7s.
add the following class to do all the reflection hinting. you could also try the agent but i had trouble
package main;
import java.util.Set;
import java.util.regex.Pattern;
import org.camunda.bpm.engine.rest.impl.NamedProcessEngineRestServiceImpl;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.aot.hint.TypeReference;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.context.annotation.ImportRuntimeHints;
import org.springframework.core.type.filter.RegexPatternTypeFilter;
import org.springframework.stereotype.Component;
@Component
@ImportRuntimeHints(CamundaNativeRuntimeHints.class)
public class CamundaNativeRuntimeHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
registerCamundaClasses(hints);
registerResources(hints);
}
private void registerCamundaClasses(RuntimeHints hints) {
Class<?>[] filterClasses = {
com.fasterxml.jackson.databind.ObjectMapper.class,
com.fasterxml.jackson.jakarta.rs.base.JsonMappingExceptionMapper.class,
com.fasterxml.jackson.jakarta.rs.base.JsonParseExceptionMapper.class,
com.fasterxml.jackson.jakarta.rs.json.JacksonJsonProvider.class,
com.fasterxml.jackson.module.jakarta.xmlbind.JakartaXmlBindAnnotationIntrospector.class,
jakarta.servlet.Filter.class,
jakarta.servlet.http.HttpServlet.class,
jakarta.servlet.ServletContext.class,
jakarta.servlet.ServletContextListener.class,
java.lang.Boolean.class,
java.lang.Integer.class,
java.lang.String.class,
java.lang.Byte.class,
java.lang.Character.class,
java.lang.Double.class,
java.lang.Float.class,
java.lang.Short.class,
java.util.ArrayList.class,
java.util.List.class,
org.apache.catalina.core.ApplicationContext.class,
org.apache.catalina.core.ApplicationContextFacade.class,
org.apache.catalina.core.StandardContext.class,
org.apache.catalina.core.StandardWrapper.class,
org.apache.catalina.loader.WebappLoader.class,
org.apache.catalina.servlets.DefaultServlet.class,
org.apache.catalina.webresources.DirResourceSet.class,
org.apache.catalina.webresources.JarResourceSet.class,
org.apache.catalina.webresources.StandardRoot.class,
org.apache.ibatis.javassist.util.proxy.ProxyFactory.class,
org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl.class,
org.apache.ibatis.logging.jdk14.Jdk14LoggingImpl.class,
org.apache.ibatis.logging.log4j.Log4jImpl.class,
org.apache.ibatis.logging.log4j2.Log4j2Impl.class,
org.apache.ibatis.logging.nologging.NoLoggingImpl.class,
org.apache.ibatis.logging.slf4j.Slf4jImpl.class,
org.apache.ibatis.plugin.Interceptor.class,
org.apache.ibatis.scripting.defaults.RawLanguageDriver.class,
org.apache.ibatis.scripting.xmltags.XMLLanguageDriver.class,
org.apache.ibatis.session.Configuration.class,
org.camunda.bpm.spring.boot.starter.actuator.JobExecutorHealthIndicator.class,
org.camunda.bpm.spring.boot.starter.actuator.JobExecutorHealthIndicator.Details.class,
org.camunda.bpm.spring.boot.starter.rest.CamundaJerseyResourceConfig.class,
org.camunda.bpm.spring.boot.starter.util.SpringBootProcessEngineLogger.class,
org.camunda.bpm.spring.boot.starter.webapp.CamundaBpmWebappInitializer.class,
org.glassfish.jersey.internal.JaxrsProviders.class,
};
for (Class<?> clazz : filterClasses) {
hints.reflection().registerType(clazz,
MemberCategory.values());
}
// CamundaIntegrationDeterminator c;
registerClassesInPackage(hints, "jakarta.ws.rs", ".*");
registerClassesInPackage(hints, "org.camunda.bpm.dmn", ".*");
registerClassesInPackage(hints, "org.camunda.bpm.engine", ".*");
registerClassesInPackage(hints, "org.camunda.bpm.spring.boot.starter.webapp.filter", ".*");
registerClassesInPackage(hints, "org.camunda.bpm.admin", ".*");
registerClassesInPackage(hints, "org.camunda.bpm.webapp", ".*");
registerClassesInPackage(hints, "org.camunda.bpm.tasklist", ".*");
registerClassesInPackage(hints, "org.camunda.bpm.cockpit", ".*");
registerClassesInPackage(hints, "org.camunda.bpm.tasklist", ".*");
registerClassesInPackage(hints, "org.camunda.bpm.engine.rest", ".*");
registerClassesInPackage(hints, "org.camunda.commons", ".*");
registerClassesInPackage(hints, "org.camunda.connect", ".*");
registerClassesInPackage(hints, "org.camunda.spin", ".*");
registerClassesInPackage(hints, "org.glassfish.hk2", ".*");
registerClassesInPackage(hints, "org.glassfish.jersey", ".*");
registerClassesInPackage(hints, "org.glassfish.jersey.internal", ".*"); // internal dont get picked up
// otherwise?
registerClassesInPackage(hints, "org.jvnet", ".*");
registerClassesInPackage(hints, "org.jvnet.hk2.spring.bridge.internal", ".*");
registerClassesInPackage(hints, "org.joda.time", ".*");
registerClassesInPackage(hints, "java.lang", ".*");
registerClassesInPackage(hints, "java.util", ".*");
hints.serialization().registerType(TypeReference
.of(org.camunda.bpm.spring.boot.starter.actuator.JobExecutorHealthIndicator.Details.class));
hints.proxies().registerJdkProxy(
jakarta.ws.rs.ext.ParamConverterProvider.class,
jakarta.ws.rs.ext.Provider.class,
jakarta.ws.rs.ext.Providers.class,
jakarta.ws.rs.ext.ContextResolver.class,
org.glassfish.hk2.api.ProxyCtl.class,
org.glassfish.jersey.internal.inject.InjectionManager.class,
org.glassfish.jersey.internal.inject.InjectionResolver.class,
jakarta.servlet.ServletContext.class,
org.springframework.web.context.ServletContextAware.class
);
// org.camunda.bpm.engine.impl.identity.db.DbUserQueryImpl a;
// Add this to your existing hints
// AbstractQueryDto a;
// hints.reflection()
// .registerType(org.camunda.bpm.engine.rest.dto.task.TaskQueryDto.class,
// MemberCategory.values())
// .registerField(TypeReference.of("org.camunda.bpm.engine.rest.dto.task.TaskQueryDto").field("firstResult"))
// .registerField(TypeReference.of("org.camunda.bpm.engine.rest.dto.task.TaskQueryDto").field("maxResults"));
// hints.registerType(TypeReference.of(org.camunda.bpm.engine.rest.dto.task.TaskQueryDto.class),
// typeHint -> typeHint.withField(field.getName()));
// // You might also need the AbstractQueryDto
// hints.reflection()
// .registerType(org.camunda.bpm.engine.rest.dto.AbstractQueryDto.class,
// MemberCategory.DECLARED_FIELDS,
// MemberCategory.DECLARED_METHODS);
}
private void registerResources(RuntimeHints hints) {
hints.resources()
// .registerPattern(builder -> builder.excludes("*.class"))
// lots of random nonsense that might or might not be helping. some came from
// llm suggestions
.registerPattern("*.properties")
.registerPattern("*.xml")
.registerPattern("webapps/**")
.registerPattern("camunda/app/**")
.registerPattern("META-INF/maven/*")
.registerPattern("META-INF/resources/camunda/**")
.registerPattern("META-INF/services/*")
.registerPattern("org/apache/ibatis/builder/xml/mybatis-3-config.dtd")
.registerPattern("org/apache/ibatis/builder/xml/mybatis-3-mapper.dtd")
.registerPattern("org/camunda/bpm/**")
.registerPattern("org/camunda/bpm/engine/impl/scripting/**")
.registerPattern("org/camunda/bpm/engine/rest/**")
.registerPattern("org/camunda/bpm/spring/boot/starter/webapp/**")
.registerPattern("org/camunda/spin/**/*.xsl")
.registerPattern("public/**")
.registerPattern("scripts/**")
.registerPattern("static/**")
.registerPattern("WEB-INF/**")
// from looking at camunda-webapp-webjar-7.21.0.jar
.registerPattern("META-INF/resources/camunda-welcome/**")
.registerPattern("META-INF/resources/plugin/**")
.registerPattern("META-INF/resources/webjars/**")
// used by spin for script envs. camunda complains about script/env/groovy/spin.groovy
.registerPattern("script/env/**");
}
NamedProcessEngineRestServiceImpl x;
// add function to add all classes in a package, based on regex filter
private void registerClassesInPackage(RuntimeHints hints, String packageName, String regex) {
// Add all the entity classes
final ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(
false) {
@Override
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
return super.isCandidateComponent(beanDefinition)
|| beanDefinition.getMetadata().isAbstract();
}
};
provider.addIncludeFilter(new RegexPatternTypeFilter(Pattern.compile(regex)));
final Set<BeanDefinition> classes = provider.findCandidateComponents(packageName);
for (BeanDefinition bean : classes) {
// System.out.println("x: " + bean.getBeanClassName() + " - " + packageName);
try {
Class<?> clazz = Class.forName(bean.getBeanClassName());
hints.reflection().registerType(clazz,
MemberCategory.values());
// System.out.println(clazz.getName());
} catch (ClassNotFoundException e) {
// e.printStackTrace();
System.out.println("Error: Could not find class: " + bean.getBeanClassName());
// throw new RuntimeException(e);
} catch (java.lang.NoClassDefFoundError e) {
// is ok. some dependency not there
System.out.print(" XXX " + e.getMessage());
}
}
}
}
and also this one because for some reason things were being loaded out of order and ended up with null pointer exceptions
// ES: This is here because the built-in class the camunda boot starter has seems to provide everything too late. then NPE.
@Configuration
@EnableConfigurationProperties(CamundaBpmProperties.class)
public class CamundaConfig {
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
@Primary
public DefaultProcessEngineConfiguration defaultProcessEngineConfiguration(
CamundaBpmProperties camundaBpmProperties) {
DefaultProcessEngineConfiguration config = new DefaultProcessEngineConfiguration();
return config;
}
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
public DefaultDatasourceConfiguration defaultDatasourceConfiguration(
CamundaBpmProperties camundaBpmProperties) {
DefaultDatasourceConfiguration config = new DefaultDatasourceConfiguration();
return config;
}
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
public DefaultAuthorizationConfiguration defaultAuthorizationConfiguration(
CamundaBpmProperties camundaBpmProperties) {
DefaultAuthorizationConfiguration config = new DefaultAuthorizationConfiguration();
return config;
}
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
public DefaultDeploymentConfiguration defaultDeploymentConfiguration(
CamundaBpmProperties camundaBpmProperties) {
DefaultDeploymentConfiguration config = new DefaultDeploymentConfiguration();
return config;
}
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
public DefaultHistoryConfiguration defaultHistoryConfiguration(
CamundaBpmProperties camundaBpmProperties) {
DefaultHistoryConfiguration config = new DefaultHistoryConfiguration();
return config;
}
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
public DefaultJobConfiguration defaultJobConfiguration(
CamundaBpmProperties camundaBpmProperties) {
DefaultJobConfiguration config = new DefaultJobConfiguration();
return config;
}
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
public DefaultMetricsConfiguration defaultMetricsConfiguration(
CamundaBpmProperties camundaBpmProperties) {
DefaultMetricsConfiguration config = new DefaultMetricsConfiguration();
return config;
}
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
public SpringBootSpinProcessEnginePlugin spinProcessEnginePlugin() {
SpringBootSpinProcessEnginePlugin plugin = new SpringBootSpinProcessEnginePlugin();
return plugin;
}
}
If anyone is keen i could provide more info, but those were the big learnings.