Writing SAP CAP services in Kotlin

Estimated read time 15 min read

Introduction

SAP CAP has become a very popular way to write BTP services & is available in Node.js and Java flavours. I became curious recently if anyone had tried to write a CAP service using Kotlin, a JetBrains developed language which also runs on the JVM. Kotlin supports interoperability with Java, which in theory should mean that we can write our CAP service code in Kotlin, right? 

 

Setting up our test service

Let’s start by running the CAP Java Maven archectype provided in the getting started documentation, using the following command. When this command is run, we will be prompted for a ‘groupId’ and an ‘artefactId’. I used ‘com.sap.test’ and ‘ktcap’, but feel free to use whatever values you want.

mvn archetype:generate -DarchetypeArtifactId=”cds-services-archetype” -DarchetypeGroupId=”com.sap.cds” -DarchetypeVersion=”RELEASE” -DinteractiveMode=true

Once the process has completed you should see a new CAP Java project contained within a subfolder, but if you open it in an IDE you’ll notice that we don’t have any code or schema definitions. Luckily the getting started docs also provide us with a CDS Maven plugin to give us a simple bookstore example.

mvn com.sap.cds:cds-maven-plugin:add -Dfeature=TINY_SAMPLE

Now we should have some entities defined in the db/data-model.cds file, as well as a handler Java file at srv/src/java/…/handlers/CatalogServiceHandler.java. Let’s try to build & run the code to make sure we have a working service.

mvn spring-boot:run

 Your code should build successfully & once you see the following log, your server is started.

INFO 81621 — [ restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path ‘/’

Navigate to http://localhost:8080 in your browser and click on the “Books” OData endpoint. You should see some data, similar to the below screenshot. 

A Quick note for IntelliJ users

You may notice when you try to look at the handler code in IntelliJ that you are seeing a lot of errors, similar to the screenshot below.

When the CAP build happens (which it does as part of our previous maven command) it generates a lot of new java files located in the src/gen folder. Because IntelliJ doesn’t know about this, it doesn’t understand all of the references in our source code. You can easily resolve this issue by going to the src/gen/java folder in the “Project” panel of IntelliJ, right clicking the folder and selecting “Mark Directory as” > “Generated sources root”. 

 

Inspecting the handler code

@Component
@ServiceName(CatalogService_.CDS_NAME)
public class CatalogServiceHandler implements EventHandler {

@After(event = CqnService.EVENT_READ)
public void discountBooks(Stream<Books> books) {
books.filter(b -> b.getTitle() != null && b.getStock() != null)
.filter(b -> b.getStock() > 200)
.forEach(b -> b.setTitle(b.getTitle() + ” (discounted)”));
}

}

The sample handler code CAP gave us is pretty straightforward. Basically what we need to know is:

The class is annotated as a “Component“, which will enable the class to be automatically detected and registered as a spring bean in the Spring Application Context.The class implements an interface EventHandler which enables CAP to distinguish the spring bean as one that may contain event handler methods.It is also annotated with @ServiceName. This tells CAP for which service the handler functions should be registered. In our case we have only defined one service; CatalogService, in the srv folder.Finally we have a function which appends “(discounted)” to any book with more than 200 copies in stock. This function is marked with the @After annotation, which registers the function as a handler to be executed after the On handlers.

Let’s try to do something similar in Kotlin.

 

Basic Kotlin Handler

// src/main/java/com/sap/test/ktcap/handlers/TestKotlinHandler.kt

package com.sap.test.ktcap.handlers

import cds.gen.catalogservice.Books
import cds.gen.catalogservice.CatalogService_
import com.sap.cds.services.cds.CqnService
import com.sap.cds.services.handler.EventHandler
import com.sap.cds.services.handler.annotations.After
import com.sap.cds.services.handler.annotations.ServiceName
import org.springframework.stereotype.Component
import java.util.stream.Stream

@Component
@ServiceName(CatalogService_.CDS_NAME)
class TestKotlinHandler: EventHandler {

@After(event = [CqnService.EVENT_READ])
fun appendKotlinStr(books: Stream<Books>){
books.forEach { b -> b.title += ” was processed by Kotlin handler” }
}
}

Here is a basic Kotlin handler implementation that should simply append some text to the title field of all books returned to the client. When you create this Kotlin file, IntelliJ will detect that we do not have Kotlin configured in our project. Click “configure” on the warning to handle this automatically or alternatively you can manually add kotlin as a dependency in the pom.xml. With kotlin configured for our project, we can build & run the CAP service as before and navigate to the Books endpoint.

Looks like our handler ran as expected! Inspecting the logs we can also see that the handler was registered to the Cds Runtime as expected.

INFO 89519 — [ restartedMain] c.s.c.s.impl.runtime.CdsRuntimeImpl : Registered handler class com.sap.test.ktcap.handlers.TestKotlinHandler

With this first handler we have proven that our Kotlin class could be registered as a Spring Bean, a handler in the Cds Runtime and that it can exist alongside other Java code in our service. 

 

Replacing the Java Handler with Kotlin

If you look closer at our previous handler, you may notice there is one issue with our implementation; The handler is still accepting Stream<Books> , a Java steam. So although we have implemented our handler in Kotlin, we are not yet using the Kotlin standard library structures to process the data. Let’s see if we can achieve the original Java handler functionality by re-writing our Kotlin handler.

As per the CAP documentation, we don’t actually need to accept a stream in our function signature – we can accept any Collection or a singular cds type (if we know we are only dealing with one item). So let’s try to use a Kotlin List and see if the handler behaves as it did before.

@After(event = [CqnService.EVENT_READ])
fun appendKotlinStr(books: MutableList<Books>)

Recompiling & running the code gives us the same behaviour as before, so we should be able to continue using collections from the Kotlin standard library in our final implementation.

package com.sap.test.ktcap.handlers

import cds.gen.catalogservice.Books
import cds.gen.catalogservice.CatalogService_
import com.sap.cds.services.cds.CqnService
import com.sap.cds.services.handler.EventHandler
import com.sap.cds.services.handler.annotations.After
import com.sap.cds.services.handler.annotations.ServiceName
import org.springframework.stereotype.Component

@Component
@ServiceName(CatalogService_.CDS_NAME)
class TestKotlinHandler: EventHandler {

@After(event = [CqnService.EVENT_READ])
fun appendKotlinStr(books: MutableList<Books>){
books
.filter { b -> b.title != null && b.stock != null }
.filter { b -> b.stock > 200 }
.forEach { b -> b.title += ” was processed by Kotlin handler” }
}
}

  And a final compile & run to check that we get the result we expect..

Nice! Our handler is now affecting only the records with more than 200 copies in stock, just like the original Java handler. The only difference is that we are not using a stream, which we can easily introduce by changing line 17 to books.asSequence() and then chaining our filters as normal, if desired.

 

Conclusion

What we have proven with the above PoC is that Kotlin/Java interoperability works pretty nicely within an SAP CAP application, at least in this basic scenario. We were able to:

Implement a working Kotlin handlerRegister the class as a spring bean within a Spring Java ApplicationRegister the handler within the CdsRuntimeProcess the generated Java “Books” class within a Kotlin data structure

Now we know that we can do it, the question is – should we? Well, Kotlin gives us some pretty nice language features that are not available in Java, for example:

Extensions, the ability to extend classes from 3rd party packages without inheriting from them Compile time nullability checksCoroutines, an asynchronous programming technique similar to Go’s goroutinesA more concise, intuitive syntax

However, given that we are writing our Kotlin code within two frameworks (CAP & Spring) which were written specifically for Java, there is a risk that differences between how the languages implement null checking, reflection or asynchronocity could result in adverse behaviour at runtime. Even in these cases, it’s nice to know that we can fall back to a pure Java class within the same project if needed.

 

​ IntroductionSAP CAP has become a very popular way to write BTP services & is available in Node.js and Java flavours. I became curious recently if anyone had tried to write a CAP service using Kotlin, a JetBrains developed language which also runs on the JVM. Kotlin supports interoperability with Java, which in theory should mean that we can write our CAP service code in Kotlin, right?  Setting up our test serviceLet’s start by running the CAP Java Maven archectype provided in the getting started documentation, using the following command. When this command is run, we will be prompted for a ‘groupId’ and an ‘artefactId’. I used ‘com.sap.test’ and ‘ktcap’, but feel free to use whatever values you want.mvn archetype:generate -DarchetypeArtifactId=”cds-services-archetype” -DarchetypeGroupId=”com.sap.cds” -DarchetypeVersion=”RELEASE” -DinteractiveMode=trueOnce the process has completed you should see a new CAP Java project contained within a subfolder, but if you open it in an IDE you’ll notice that we don’t have any code or schema definitions. Luckily the getting started docs also provide us with a CDS Maven plugin to give us a simple bookstore example.mvn com.sap.cds:cds-maven-plugin:add -Dfeature=TINY_SAMPLENow we should have some entities defined in the db/data-model.cds file, as well as a handler Java file at srv/src/java/…/handlers/CatalogServiceHandler.java. Let’s try to build & run the code to make sure we have a working service.mvn spring-boot:run Your code should build successfully & once you see the following log, your server is started.INFO 81621 — [ restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path ‘/’Navigate to http://localhost:8080 in your browser and click on the “Books” OData endpoint. You should see some data, similar to the below screenshot. A Quick note for IntelliJ usersYou may notice when you try to look at the handler code in IntelliJ that you are seeing a lot of errors, similar to the screenshot below.When the CAP build happens (which it does as part of our previous maven command) it generates a lot of new java files located in the src/gen folder. Because IntelliJ doesn’t know about this, it doesn’t understand all of the references in our source code. You can easily resolve this issue by going to the src/gen/java folder in the “Project” panel of IntelliJ, right clicking the folder and selecting “Mark Directory as” > “Generated sources root”.  Inspecting the handler code@Component
@ServiceName(CatalogService_.CDS_NAME)
public class CatalogServiceHandler implements EventHandler {

@After(event = CqnService.EVENT_READ)
public void discountBooks(Stream<Books> books) {
books.filter(b -> b.getTitle() != null && b.getStock() != null)
.filter(b -> b.getStock() > 200)
.forEach(b -> b.setTitle(b.getTitle() + ” (discounted)”));
}

}The sample handler code CAP gave us is pretty straightforward. Basically what we need to know is:The class is annotated as a “Component”, which will enable the class to be automatically detected and registered as a spring bean in the Spring Application Context.The class implements an interface EventHandler which enables CAP to distinguish the spring bean as one that may contain event handler methods.It is also annotated with @ServiceName. This tells CAP for which service the handler functions should be registered. In our case we have only defined one service; CatalogService, in the srv folder.Finally we have a function which appends “(discounted)” to any book with more than 200 copies in stock. This function is marked with the @After annotation, which registers the function as a handler to be executed after the On handlers.Let’s try to do something similar in Kotlin. Basic Kotlin Handler// src/main/java/com/sap/test/ktcap/handlers/TestKotlinHandler.kt

package com.sap.test.ktcap.handlers

import cds.gen.catalogservice.Books
import cds.gen.catalogservice.CatalogService_
import com.sap.cds.services.cds.CqnService
import com.sap.cds.services.handler.EventHandler
import com.sap.cds.services.handler.annotations.After
import com.sap.cds.services.handler.annotations.ServiceName
import org.springframework.stereotype.Component
import java.util.stream.Stream

@Component
@ServiceName(CatalogService_.CDS_NAME)
class TestKotlinHandler: EventHandler {

@After(event = [CqnService.EVENT_READ])
fun appendKotlinStr(books: Stream<Books>){
books.forEach { b -> b.title += ” was processed by Kotlin handler” }
}
}Here is a basic Kotlin handler implementation that should simply append some text to the title field of all books returned to the client. When you create this Kotlin file, IntelliJ will detect that we do not have Kotlin configured in our project. Click “configure” on the warning to handle this automatically or alternatively you can manually add kotlin as a dependency in the pom.xml. With kotlin configured for our project, we can build & run the CAP service as before and navigate to the Books endpoint.Looks like our handler ran as expected! Inspecting the logs we can also see that the handler was registered to the Cds Runtime as expected.INFO 89519 — [ restartedMain] c.s.c.s.impl.runtime.CdsRuntimeImpl : Registered handler class com.sap.test.ktcap.handlers.TestKotlinHandlerWith this first handler we have proven that our Kotlin class could be registered as a Spring Bean, a handler in the Cds Runtime and that it can exist alongside other Java code in our service.  Replacing the Java Handler with KotlinIf you look closer at our previous handler, you may notice there is one issue with our implementation; The handler is still accepting Stream<Books> , a Java steam. So although we have implemented our handler in Kotlin, we are not yet using the Kotlin standard library structures to process the data. Let’s see if we can achieve the original Java handler functionality by re-writing our Kotlin handler.As per the CAP documentation, we don’t actually need to accept a stream in our function signature – we can accept any Collection or a singular cds type (if we know we are only dealing with one item). So let’s try to use a Kotlin List and see if the handler behaves as it did before.@After(event = [CqnService.EVENT_READ])
fun appendKotlinStr(books: MutableList<Books>)Recompiling & running the code gives us the same behaviour as before, so we should be able to continue using collections from the Kotlin standard library in our final implementation.package com.sap.test.ktcap.handlers

import cds.gen.catalogservice.Books
import cds.gen.catalogservice.CatalogService_
import com.sap.cds.services.cds.CqnService
import com.sap.cds.services.handler.EventHandler
import com.sap.cds.services.handler.annotations.After
import com.sap.cds.services.handler.annotations.ServiceName
import org.springframework.stereotype.Component

@Component
@ServiceName(CatalogService_.CDS_NAME)
class TestKotlinHandler: EventHandler {

@After(event = [CqnService.EVENT_READ])
fun appendKotlinStr(books: MutableList<Books>){
books
.filter { b -> b.title != null && b.stock != null }
.filter { b -> b.stock > 200 }
.forEach { b -> b.title += ” was processed by Kotlin handler” }
}
}  And a final compile & run to check that we get the result we expect..Nice! Our handler is now affecting only the records with more than 200 copies in stock, just like the original Java handler. The only difference is that we are not using a stream, which we can easily introduce by changing line 17 to books.asSequence() and then chaining our filters as normal, if desired. ConclusionWhat we have proven with the above PoC is that Kotlin/Java interoperability works pretty nicely within an SAP CAP application, at least in this basic scenario. We were able to:Implement a working Kotlin handlerRegister the class as a spring bean within a Spring Java ApplicationRegister the handler within the CdsRuntimeProcess the generated Java “Books” class within a Kotlin data structureNow we know that we can do it, the question is – should we? Well, Kotlin gives us some pretty nice language features that are not available in Java, for example:Extensions, the ability to extend classes from 3rd party packages without inheriting from them Compile time nullability checksCoroutines, an asynchronous programming technique similar to Go’s goroutinesA more concise, intuitive syntaxHowever, given that we are writing our Kotlin code within two frameworks (CAP & Spring) which were written specifically for Java, there is a risk that differences between how the languages implement null checking, reflection or asynchronocity could result in adverse behaviour at runtime. Even in these cases, it’s nice to know that we can fall back to a pure Java class within the same project if needed.   Read More Technology Blog Posts by SAP articles 

#SAP

#SAPTechnologyblog

You May Also Like

More From Author