Spring Boot with Kotlin Snippets
Spring Boot has turned the already mighty Spring framework into a real feature powerhouse. The official support for Kotlin leverages this even further and allows the development of enterprise grade applications with a high level of productivity. This post is a collection of Spring Boot Kotlin code snippets I use in my projects.
Running Code after Database Commit
Use Case
Its a common requirement to execute after a database transaction has been committed successfully, logging or writing to a message queue being good examples.
This is trivial to implement with programmatic transaction management but a bit more tricky with declarative transaction management.
The following code snippet registers a function to be executed after the transaction was committed.
Code
inline fun <T> T.afterCommit(crossinline block: T.() -> Unit): T {
if (!TransactionSynchronizationManager.isActualTransactionActive()) {
throw RuntimeException("No transaction active")
}
TransactionSynchronizationManager.registerSynchronization(
object : TransactionSynchronization {
override fun afterCommit() {
block()
}
})
return this
}
Example Usage
@Service
class CarService(
@Autowired val repository: CarRepository
) {
val logger by classLogger()
@Transactional
fun delete(carId: Long) =
repository.deleteById(carId).afterCommit {
logger.info("Deleted car id=[$carId] successfully")
}
}
Class Logger by Delegation
Use Case
There are different ways to instantiate a logger for a given class; explicitly, via @Autowired
’s
Dependency Injection or via Kotlin’s
lazy() Delegate. I consider the latter the
most elegant.
Code
fun <R : Any> R.classLogger(): Lazy<Logger> =
lazy { LoggerFactory.getLogger(this.javaClass) }
Example Usage
See the example above
Copy Data Transfer Object (DTO) fields
Use Case
Many Spring Boot @Service
’s in my codebase provide CRUD operations
for database entities. The requests are always Kotlin data classes
and responses Spring Data JPA projections.
The request flow is as followed:
read and delete operations are mostly handled by the @RestController
calling the @Repository
functions directly,
whereas create and update often have side-effects and are implemented in the @Service
.
update request properties are likely identical to the application’s @Entity
model to be modified. The following
code snippet copies properties by name from the request to the model.
Code
interface DataTransferObject {
fun <T : Any> copyPropertiesTo(obj: T): T {
copyProperties(this, obj)
return obj
}
}
private fun copyProperties(sourceObj: Any, targetObj: Any) =
sourceObj::class.memberProperties.forEach { sourceField ->
targetObj::class.memberProperties.filterIsInstance<KMutableProperty<*>>()
.find { it.name == sourceField.name }
?.let { targetField ->
targetField.setter.call(
targetObj,
sourceField.getter.call(sourceObj)
)
}
}
Example Usage
@Entity
data class Car(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
@ManyToOne
@JoinColumn(name = "manufacturer_id", nullable = false)
var manufacturer: Manufacturer,
@Column(nullable = false)
var productionDate: OffsetDateTime,
@Column(nullable = false)
var serialNo: String
)
data class CarUpdateRequest(
val manufacturerId: Long,
val productionDate: OffsetDateTime
)
@Service
class CarService(
@Autowired val repository: CarRepository,
@Autowired val manufacturerService: ManufacturerService
) {
@Transactional
fun update(carId: Long, request: CarUpdateRequest) =
repository.getOne(carId).let {
request.copyPropertiesTo(it)
it.manufacturer = manufacturerService.repository.getOne(
request.manufacturerId
)
repository.save(it)
}
}
Returning Projections from Services
Use Case
As shown in the previous use-case, returning Spring Data JPA projections from the service allows exposing only selected model attributes.
This is a helpful and frequent pattern in my codebase and used in almost almost every @Service
implementation.
The following code snippet adds projection extension functions for a given entity by using Kotlin’s delegation as mixin.
Code
interface Projectable<T : Any, P : Any> {
val projectionFactory: SpelAwareProxyProjectionFactory
val defaultProjectionClass: KClass<P>
fun <P : Any> T.projection(clazz: KClass<P>) =
projectionFactory.createProjection(clazz.java, this)
fun projection(source: T) =
source.defaultProjection()
fun <P : Any> projection(clazz: KClass<P>, obj: Any) =
projectionFactory.createProjection(clazz.java, obj)
fun T.defaultProjection() =
projectionFactory.createProjection(defaultProjectionClass.java, this)
}
class ProjectionMixin<T : Any, P : Any>(
override val defaultProjectionClass: KClass<P>
) : Projectable<T, P> {
override val projectionFactory = SpelAwareProxyProjectionFactory()
}
inline fun <T : Any, reified P : Any> projectionMixin() =
ProjectionMixin<T, P>(P::class)
Example Usage
interface CarProjection {
val id: Long
val manufacturer: Manufacturer
val productionDate: OffsetDateTime
}
@Service
class CarService(
@Autowired val repository: CarRepository,
@Autowired val manufacturerService: ManufacturerService
) : Projectable<Car, CarProjection> by projectionMixin() {
@Transactional
fun create(request: CarCreateRequest) =
Car(
manufacturer = manufacturerService.repository.getOne(
request.manufacturerId
),
productionDate = request.productionDate,
serialNo = request.serialNo
).let {
repository.save(it)
}.defaultProjection()
}
Functions and derived Projection Attributes
Use Case
Spring Data JPA projections are
compact and elegant. But sometimes mapping @Entity
model attributes is not enough and we’d like to add synthetic
projection attributes derived from the model or simply apply a conversion to an entity attribute.
This is possible by annotating the attribute getter via @get::JsonIgnore
and adding an default interface function
annotated with @JvmDefault
.
@JvmDefault
requires the Kotlin compiler argument -Xjvm-default=enable
. See the
JvmDefault documentation
for more information.
Example Usage
interface CarProjection {
val id: Long
val manufacturer: Manufacturer
@JvmDefault
val daysSinceProduction: Long
get() = ChronoUnit.DAYS.between(productionDate, OffsetDateTime.now())
@get:JsonIgnore
val productionDate: OffsetDateTime
}
JPA Entity lock / projection / getById
Use Case
JPA entities are manipulated using interfaces annotated @Repository
and extending the Spring interface Repository
.
The following interfaces and functions extend the Repository
implementation with features used as needed.
Lock
Sometimes the application must lock an entity to ensure exclusive access while manipulating its data; see PostgreSQL’s SELECT … FOR UPDATE for more information.
This can be achieved by annotating an repository interface function with @Lock(LockModeType.PESSIMISTIC_WRITE)
Dynamic projections and list of projections
For repositories to return
dynamic JPA projections,
the function must take a parameter of type Class<T>
and return the projection type T
. This works analogous for
lists of projections.
Avoiding getOne() references
JpaRepository::getOne(ID id)
might return a reference to an entity instead of an instance, which might lead to
issues if accessed after the transaction is completed. This scenario can happen if used with projections returned
by a REST controller. This might be a problem you never encountered and I really don’t wana encounter again….
The extension function CrudRepository<T, ID>::getById(id: Id): T
solves this specific problem.
Code
interface QueryLocked<T> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
fun getLockedById(id: Long): T
@Lock(LockModeType.PESSIMISTIC_WRITE)
fun getLockedIfExistsById(id: Long): T?
}
interface QueryAsProjection {
fun <T> findById(id: Long, clazz: Class<T>): T
fun <T> findAllBy(clazz: Class<T>): List<T>
}
inline fun <reified T : Any, ID> CrudRepository<T, ID>.getById(id: ID): T =
this.findById(id).orElseThrow {
EntityNotFoundException(
"Could not find entity ${T::class.simpleName} with id=$id"
)
}
Example Usage
@Repository
interface CarRepository :
JpaRepository<Car, Long>,
QueryAsProjection,
QueryLocked<Car>
interface CarProjection {
val id: Long
val manufacturer: Manufacturer
val productionDate: OffsetDateTime
val serialNo: String
}
interface CarSummaryProjection {
val id: Long
val manufacturer: Manufacturer
}
@Service
class CarService(
@Autowired val repository: CarRepository
) : Projectable<Car, CarProjection> by projectionMixin() {
@Transactional
fun productionCompleted(id: Long) =
repository.getLockedById(id).let { car ->
car.productionDate = OffsetDateTime.now()
}.let {
repository.save(it)
}.defaultProjection()
@Transactional
fun allCarsSummary(): List<CarSummaryProjection> =
repository.findAllBy(CarSummaryProjection::class)
@Transactional
fun byId(id: Long) =
repository.findById(id)
}
Conclusion
Hopefully some of the code snippets are useful to other developers. I’ll keep adding to this list and write dedicated posts for larger features. Drop me an email if you have any questions or feedback.