Aspect Oriented Programming: Spring, Kotlin, Gradle

System architecture and compile-time checks using AOP.

  1. Setup
  2. Aspects
    1. Pointcuts
  3. Compile-time checks

I’ve seen a lot of resources regarding AOP for Java and / or Maven, but couldn’t find a lot about the Kotlin+Gradle combo. As for the Spring side, the Spring Initializr doesn’t even list the aop starter, which is apparently by design. So I decided to show a working solution for the mentioned stack. The code referenced can be found here.

Aspect oriented programming is a great way of defining cross-cutting concerns that can simplify your project and help establish your architecture.

Setup

First thing is to add the required dependencies to the build.

  • The spring aop starter is needed for defining aspects.
  • The aspectj plugin is needed for compile-time checks.

See the reference build file.

There are a bunch of gradle plugins, but most are outdated and lack proper documentation. The freefair plugin was the only one I could get to work, documentation here. Just add the post-compile-weaving plugin and set your aspectj version in gradle.properties.

Aspects

Now we can start writing a custom aspect. The example shows an aspect that monitors all activities by logging start and end events including the time elapsed and whether it succeeded or failed. Using @Around advice we can wrap all methods that match the defined pointcut in a try-catch-finally.

@Aspect
@Component
class ActivityAspect {

    private val logger = LoggerFactory.getLogger(this::class.java)

    @Around("Architecture.activity() || Architecture.repository() || Architecture.service()")
    fun monitorActivity(proceedingJoinPoint: ProceedingJoinPoint): Any? {
        val name = proceedingJoinPoint.staticPart.signature.toShortString()
        var state = "SUCCESS"
        logger.info("Activity start: $name")
        val startTime = System.currentTimeMillis()
        try {
            return proceedingJoinPoint.proceed()
        }
        catch (ex: Exception) {
            state = "ERROR"
            throw ex
        }
        finally {
            val timeElapsed = System.currentTimeMillis() - startTime
            logger.info("Activity end: $name ($state $timeElapsed ms)")
        }
    }
}

Pointcuts

In order to define what methods the advice is applied to, pointcuts can be used to describe the architecture of the application. In the example we want to monitor all public methods in any service or repository, as well as any method annotated with a custom @Activity annotation.

class Architecture {

    @Pointcut("execution(* (@org.springframework.stereotype.Repository *).*(..))")
    fun service() = Unit

    @Pointcut("execution (* (@org.springframework.stereotype.Service *).*(..))")
    fun repository() = Unit

    @Pointcut("(@annotation(Activity) && execution(* *(..)))")
    fun activity() = Unit
}

Here is a demo service that would match these pointcuts:

@Service
class DemoService {

    @Activity // optional, can be used on methods outside of services / repositories
    fun run(someBool: Boolean): String {
        ...
    }

    fun noAnnotation() {
        ...
    }
}

Compile-time checks

AspectJ can be used to define compile-time checks that produce errors or warnings when matched. For example, we can introduce a check that ensures that no code from the java.sql or javax.sql package is called outside of a repository.

@Aspect
class CheckAspect {

    @DeclareError("!Architecture.repository() && (call (* java.sql..*.*(..)) || call (* javax.sql..*.*(..)))")
    val jdbcOutsideRepository = "Only call jdbc code from within repositories."
}

When compiling the following function, the compiler will throw an error.

fun methodUsingJavaxSql() {
    javax.sql.rowset.RowSetMetaDataImpl().getCatalogName(2)
}

I am really happy with the results and the fact that the Spring team is pushing and actively promoting Kotlin as the language of choice. One issue that still remains is handling suspending functions with AOP, since proceed will return when suspending. Hopefully the fix makes it into the next release. Tracking issue here.


© 2023. All rights reserved.