Write your Spring Boot (3.x) starter with Kotlin & Maven

Some companies use Camunda(TM) BPMN Engine for managing the business automatization process. Most of the functionality provided out of the…

Write your Spring Boot (3.x) starter with Kotlin & Maven
Kotlin and Spring Boot logo

Some companies use Camunda(TM) BPMN Engine for managing the business automatization process. Most of the functionality provided out of the box is enough for most cases. But recently I have realized that we have always written our service to send messages to the engine.

So, I decided to write my starter. The native language for Camunda is Java, but it supports many other languages. Since I want programming experience in Java, I have decided to write my starter with Kotlin.

For those who are already tired — the full code is available on the GitHub

There are no breaking backward capability changes in writing spring boot starters.

Changes:

  1. Now auto configurations load from another file placed at resources/META-INF/spring/packageName.className.imports. Don’t worry old way of spring.factories work too

I decided to use Maven as a build tool, so it is my pom.xml file

<?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> 
    <groupId>io.vrnsky</groupId> 
    <artifactId>camunda-messaging-starter</artifactId> 
    <version>0.0.1-SNAPSHOT</version> 
    <name>camunda-messaging-starter</name> 
    <description>Message Camunda without overhead</description> 
 
    <properties> 
        <java.version>17</java.version> 
        <kotlin.version>1.7.22</kotlin.version> 
    </properties> 
 
    <dependencies> 
        <dependency> 
            <groupId>org.springframework.boot</groupId> 
            <artifactId>spring-boot-starter</artifactId> 
            <version>3.0.2</version> 
            <optional>true</optional> 
        </dependency> 
        <dependency> 
            <groupId>org.springframework.boot</groupId> 
            <artifactId>spring-boot-starter-web</artifactId> 
            <version>3.0.2</version> 
        </dependency> 
 
        <dependency> 
            <groupId>com.fasterxml.jackson.module</groupId> 
            <artifactId>jackson-module-kotlin</artifactId> 
            <version>2.14.1</version> 
        </dependency> 
        <dependency> 
            <groupId>org.jetbrains.kotlin</groupId> 
            <artifactId>kotlin-reflect</artifactId> 
            <version>1.8.0</version> 
        </dependency> 
        <dependency> 
            <groupId>org.jetbrains.kotlin</groupId> 
            <artifactId>kotlin-stdlib-jdk8</artifactId> 
            <version>1.8.0</version> 
        </dependency> 
 
        <dependency> 
            <groupId>org.jetbrains.kotlin</groupId> 
            <artifactId>kotlin-maven-lombok</artifactId> 
            <version>${kotlin.version}</version> 
        </dependency> 
 
        <dependency> 
            <groupId>org.projectlombok</groupId> 
            <artifactId>lombok</artifactId> 
            <version>1.18.24</version> 
            <scope>provided</scope> 
        </dependency> 
 
        <dependency> 
            <groupId>org.apache.logging.log4j</groupId> 
            <artifactId>log4j-api</artifactId> 
            <version>2.19.0</version> 
        </dependency> 
        <dependency> 
            <groupId>org.apache.logging.log4j</groupId> 
            <artifactId>log4j-core</artifactId> 
            <version>2.19.0</version> 
        </dependency> 
        <dependency> 
            <groupId>org.apache.logging.log4j</groupId> 
            <artifactId>log4j-slf4j-impl</artifactId> 
            <version>2.19.0</version> 
        </dependency> 
 
    </dependencies> 
 
    <build> 
        <plugins> 
            <plugin> 
                <groupId>org.apache.maven.plugins</groupId> 
                <artifactId>maven-source-plugin</artifactId> 
                <version>3.2.1</version> 
            </plugin> 
            <plugin> 
                <groupId>org.jetbrains.kotlin</groupId> 
                <artifactId>kotlin-maven-plugin</artifactId> 
                <version>1.7.22</version> 
                <configuration> 
                    <sourceDirs> 
                        <sourceDir>${project.basedir}/src/main/kotlin</sourceDir> 
                    </sourceDirs> 
                    <args> 
                        <arg>-Xjsr305=strict</arg> 
                    </args> 
                </configuration> 
                <dependencies> 
                    <dependency> 
                        <groupId>org.jetbrains.kotlin</groupId> 
                        <artifactId>kotlin-maven-allopen</artifactId> 
                        <version>${kotlin.version}</version> 
                    </dependency> 
                </dependencies> 
            </plugin> 
            <plugin> 
                <groupId>org.apache.maven.plugins</groupId> 
                <artifactId>maven-compiler-plugin</artifactId> 
                <version>3.5.1</version> 
                <configuration> 
                    <source>17</source> 
                    <target>17</target> 
                    <annotationProcessorPaths> 
                        <annotationProcessorPath> 
                            <groupId>org.projectlombok</groupId> 
                            <artifactId>lombok</artifactId> 
                            <version>1.18.24</version> 
                        </annotationProcessorPath> 
                    </annotationProcessorPaths> 
                </configuration> 
            </plugin> 
        </plugins> 
    </build> 
 
</project>

We use some dependency as spring-boot-web-starter because we need RestTemplate class for HTTP calls. There are dependencies for Kotlin and Logging. For building projects, I use the following command

mvn clean kotlin:compile install

So, let’s start with defining model classes of objects which start will be working with. Before we start writing code — I strongly advise checking the actual documentation of Camunda messaging

enum class VariableType { 
    Json, String 
}

So we can have two types of variables — json and string types

data class ProcessVariable( 
    val value: String, 
    val type: VariableType 
) {

This class describes process variables — which widely used in Camunda

data class CamundaMessage( 
    val businessKey: String, 
    val messageName: String, 
    val correlationKeys: Map<String, VariableType>, 
    val processVariables: Map<String, VariableType> 
) { 
}

And finally the last data transfer object — the message itself

Now is the time to create a configuration class, where we can map values from application.yml or application.properties into the bean

@ConfigurationProperties(prefix = "camunda") 
data class CamundaMessageConfiguration( 
    val baseUrl: String = "" 
)

For this moment we don’t need anything except the base URL

Let’s create CamundaMessageTemplate, I have decided to name it this way since a lot of starters use this naming strategy like — KafkaTemplate, RabbitTemplate

class CamundaMessageTemplate( 
    properties: CamundaMessageConfiguration 
) { 
    val logger: Logger = LoggerFactory.getLogger(CamundaMessageTemplate::class.java) 
 
    var restTemplate: RestTemplate? = null 
 
    init { 
        logger.info("baseUrl obtained from configs = {}", properties.baseUrl) 
        restTemplate = RestTemplateBuilder() 
            .uriTemplateHandler(DefaultUriBuilderFactory(properties.baseUrl)) 
            .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) 
            .build() 
    } 
 
    fun message(message: CamundaMessage) { 
        val httpEntity: HttpEntity<CamundaMessage> = HttpEntity<CamundaMessage>(message) 
        restTemplate?.postForObject("/message", httpEntity, Unit.javaClass) 
    } 
}

We are close enough to finishing, so it is time to create a class for auto-configuration

@AutoConfiguration 
@ConditionalOnClass(CamundaMessageTemplate::class) 
@EnableConfigurationProperties(CamundaMessageConfiguration::class) 
class CamundaMessageAutoConfiguration { 
 
    @Bean 
    @ConditionalOnMissingBean 
    fun createTemplate(properties: CamundaMessageConfiguration): CamundaMessageTemplate { 
        return CamundaMessageTemplate(properties) 
    } 
}

Okay, we are mostly done, need to add a file for auto-configuration import according to the changes mentioned above. The name of the file is io.vrnsky.camunda.messaging.starter.CamundaMessageAutoConfiguration.imports

io.vrnsky.camunda.messaging.starter.CamundaMessageAutoConfiguration

If you are a good developer as I try to be — we need to add documentation. For documentation about properties, we can use spring-configuration-metadata.json

{ 
  "properties": [ 
    { 
      "name": "camunda.baseUrl", 
      "type": "java.lang.String", 
      "description": "The URL of Camunda engine", 
      "defaultValue": "any" 
    } 
  ] 
}

So, it is time we can start using starter

<?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>org.springframework.boot</groupId> 
  <artifactId>spring-boot-starter-parent</artifactId> 
  <version>3.0.2</version> 
  <relativePath/> <!-- lookup parent from repository --> 
 </parent> 
 <groupId>io.vrnsky</groupId> 
 <artifactId>camunda-message-starter-example</artifactId> 
 <version>0.0.1-SNAPSHOT</version> 
 <name>camunda-message-starter-example</name> 
 <description>Demo project for Spring Boot</description> 
 <properties> 
  <java.version>17</java.version> 
 </properties> 
 <dependencies> 
  <dependency> 
   <groupId>io.vrnsky</groupId> 
   <artifactId>camunda-messaging-starter</artifactId> 
   <version>0.0.1-SNAPSHOT</version> 
  </dependency> 
  <dependency> 
   <groupId>org.springframework.boot</groupId> 
   <artifactId>spring-boot-starter</artifactId> 
  </dependency> 
  <dependency> 
   <groupId>org.springframework.boot</groupId> 
   <artifactId>spring-boot-starter-web</artifactId> 
  </dependency> 
 
 
  <dependency> 
   <groupId>org.springframework.boot</groupId> 
   <artifactId>spring-boot-starter-test</artifactId> 
   <scope>test</scope> 
  </dependency> 
 </dependencies> 
 
 <build> 
  <plugins> 
   <plugin> 
    <groupId>org.springframework.boot</groupId> 
    <artifactId>spring-boot-maven-plugin</artifactId> 
   </plugin> 
  </plugins> 
 </build> 
 
</project>

I’d leave this class without changes

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

Below is an example of sending messages

@RestController 
public class CamundaController { 
 
    @Autowired 
    private CamundaMessageTemplate camundaMessageTemplate; 
 
 
    @GetMapping("/message") 
    public void message(@RequestBody CamundaMessage camundaMessage) { 
        camundaMessageTemplate.message(camundaMessage); 
    } 
}

Logs:

.   ____          _            __ _ _ 
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \ 
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ 
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) ) 
  '  |____| .__|_| |_|_| |_\__, | / / / / 
 =========|_|==============|___/=/_/_/_/ 
 :: Spring Boot ::                (v3.0.2) 
 
2023-01-29T14:06:14.085+08:00  INFO 25707 --- [           main] .CamundaMessageStarterExampleApplication : Starting CamundaMessageStarterExampleApplication using Java 18.0.1.1 with PID 25707 (/Users/vrnsky/Downloads/camunda-message-starter-example/target/classes started by vrnsky in /Users/vrnsky/Downloads/camunda-message-starter-example) 
2023-01-29T14:06:14.091+08:00  INFO 25707 --- [           main] .CamundaMessageStarterExampleApplication : No active profile set, falling back to 1 default profile: "default" 
2023-01-29T14:06:15.413+08:00  INFO 25707 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http) 
2023-01-29T14:06:15.421+08:00  INFO 25707 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat] 
2023-01-29T14:06:15.421+08:00  INFO 25707 --- [           main] o.apache.catalina.core.StandardEngine    : Starting Servlet engine: [Apache Tomcat/10.1.5] 
2023-01-29T14:06:15.500+08:00  INFO 25707 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext 
2023-01-29T14:06:15.500+08:00  INFO 25707 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1323 ms 
2023-01-29T14:06:15.595+08:00  INFO 25707 --- [           main] i.v.c.m.starter.CamundaMessageTemplate   : baseUrl obtained from configs = http://localhost:8080 
2023-01-29T14:06:15.921+08:00  INFO 25707 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path '' 
2023-01-29T14:06:15.935+08:00  INFO 25707 --- [           main] .CamundaMessageStarterExampleApplication : Started CamundaMessageStarterExampleApplication in 2.323 seconds (process running for 2.875)

Pay attention to this line. This line gives us a clear understanding that the bean of CamundaMessageTemplate has been created successfully

2023-01-29T14:06:15.595+08:00  INFO 25707 --- [           main] i.v.c.m.starter.CamundaMessageTemplate   : baseUrl obtained from configs = http://localhost:8080

Thank you for reading!
Follow me for more interesting articles

Subscribe to Egor Voronianskii | Java Development and whatsoever

Sign up now to get access to the library of members-only issues.
Jamie Larson
Subscribe