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 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:
- 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