The Logging, The AOP and The Clean Code

thiagoematos

Thiago Matos

Posted on August 9, 2024

The Logging, The AOP and The Clean Code

Logging is an essential practice that helps developers understand the flow of their applications and diagnose issues.

Understand the flow: It shows you how your application is behaving over time, making it easier to understand and improve.
Diagnose issues: It keeps a record of what your application is doing.

It is like having a notebook where you write down every step to build a robot. After you build, if the robot makes a mistake, you can look back at your notes to see what went wrong and fix it.

One clean-code way to implement logging in a Spring Boot application is through Aspect-Oriented Programming (AOP).

What is Aspect-Oriented Programming (AOP)?

AOP is a programming paradigm that aims to increase modularity by allowing the separation of aspects of a program that affect multiple components (cross-cutting concerns). AOP allows you to encapsulate these concerns into reusable modules called "aspects".

Imagine you are responsible to log what the program is doing. Without AOP, you would have to write the same log pattern in many different places. This makes your code hard to manage. With AOP, you would create a special module called aspect to handle the logs and then you would tell your program to use the aspect.This makes your code clean and organized.

In this article, I will show a practical example of AOP, implementing a custom annotation to log method inputs and outputs at a class level. After that, you can use this annotation in many places of your code.

Learning AOP with an example

First, let's see a class without AOP.

import org.springframework.stereotype.Service

@Service
class ExampleWithoutAop {

    fun methodA(
        parameterA: String,
        parameterB: Int
    ): String {
        println("ExampleWithoutAop#methodA START - parameterA=$parameterA, parameterB=$parameterB")
        val result = "$parameterA $parameterB"
        println("ExampleWithoutAop#methodA END - result=$result")
        return result
    }

    fun methodB(
        throwsException: Boolean
    ) {
        println("ExampleWithoutAop#methodB START - throwsException=$throwsException")
        if (throwsException) {
            val message = "error message"
            println("ExampleWithoutAop#methodB ERROR - message=$message")
            throw RuntimeException(message)
        }
        println("ExampleWithoutAop#methodB END")
    }
}
Enter fullscreen mode Exit fullscreen mode

Add this runner in the main class of Spring Boot:

import org.springframework.boot.ApplicationRunner
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.annotation.Bean

@SpringBootApplication
class DemoApplication {

    @Bean
    fun runner(exampleWithoutAop: ExampleWithoutAop): ApplicationRunner {
        return ApplicationRunner {
            exampleWithoutAop.methodA("Hello", 42)
            exampleWithoutAop.methodB(false)
            exampleWithoutAop.methodB(true)
        }
    }
}

fun main(args: Array<String>) {
    runApplication<DemoApplication>(*args)
}
Enter fullscreen mode Exit fullscreen mode

Here is the output:

ExampleWithoutAop#methodA START - parameterA=Hello, parameterB=42
ExampleWithoutAop#methodA END - result=Hello 42
ExampleWithoutAop#methodB START - throwsException=false
ExampleWithoutAop#methodB END
ExampleWithoutAop#methodB START - throwsException=true
ExampleWithoutAop#methodB ERROR - message=error message
Enter fullscreen mode Exit fullscreen mode

Let's clean the code using AOP and Custom Annotation

Step 1: Define the custom annotation

@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class LogExecution
Enter fullscreen mode Exit fullscreen mode

Step 2: Add the following dependency in your build.gradle.kts:

implementation("org.springframework.boot:spring-boot-starter-aop")

Step 3: Enable the AOP for Spring Boot by adding this annotation in your DemoApplication class:

@EnableAspectJAutoProxy

Here is how your main class will look like:

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.context.annotation.EnableAspectJAutoProxy

@EnableAspectJAutoProxy
@SpringBootApplication
class DemoApplication
Enter fullscreen mode Exit fullscreen mode

Step 4: Create the Aspect

import org.aspectj.lang.ProceedingJoinPoint
import org.aspectj.lang.annotation.Around
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.Pointcut
import org.aspectj.lang.reflect.MethodSignature
import org.springframework.stereotype.Component

// Define this class as an Aspect
@Aspect
// Makes this class a Spring-managed bean,
// allowing Spring to discover and manage it
@Component
class LoggingAspect {

    // An expression that selects the join points
    // where additional code (advice) will be applied.
    // It selects all methods
    // within classes annotated with @LogExecution.
    @Pointcut("within(@com.example.demo.LogExecution *)")
    fun logExecutionMethods() {}

    // Define an "Around" Advice (additional code),
    // which surrounds (around) the execution of
    // the method selected by the Pointcut
    @Around("logExecutionMethods()")
    fun logMethodExecution(joinPoint: ProceedingJoinPoint): Any? {

        // Get the method signature from the join point
        val methodSignature = joinPoint.signature as MethodSignature

        // Get the name of the class where the method is located
        val className = methodSignature.declaringType.simpleName

        // Get the name of the method
        val methodName = methodSignature.name

        // Get the names of the method's parameters
        val inputNames = methodSignature.parameterNames

        // Get the values of the arguments passed to the method
        val inputValues = joinPoint.args

        // Combine the parameter names and values into a formatted string
        val inputs = inputNames.zip(inputValues)
            .joinToString(", ") {
                "${it.first}=${it.second}"
            }

        // Create a tag that combines the class name and method name
        val tag = "$className#$methodName"

        // Print a log message indicating
        // the starting of the method execution,
        // including the input parameters
        println("$tag START${log(inputs)}")

        // Try to execute the original method
        // and store the output value
        val output = try {
            joinPoint.proceed()
        } catch (ex: Throwable) {
            // If an exception occurs,
            // print an error message to the log
            // and rethrow the exception
            println("$tag ERROR - message=${ex.message}")
            throw ex
        }

        // Print a log message indicating
        // the ending of the method execution,
        // including the return value
        println("$tag END${log(output)}")

        // Return the output value of the original method
        return output
    }

    // Helper function to format
    // the input parameters for logging
    private fun log(inputs: String) =
      if (inputs.isEmpty()) ""
      else " - $inputs"

    // Helper function to format
    // the output value for logging
    private fun log(output: Any?) =
      output
        ?.let { " - result=$it" }
        ?: ""
}
Enter fullscreen mode Exit fullscreen mode

Step 5: Replace ExampleWithoutAop by its new version using AOP with custom annotation:

import org.springframework.stereotype.Service

@LogExecution
@Service
class ExampleWithAop {

    fun methodA(
        parameterA: String,
        parameterB: Int
    ): String = "$parameterA $parameterB"

    fun methodB(
        throwsException: Boolean
    ) {
        if (throwsException) {
            throw RuntimeException("error message")
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Execute the Spring Boot application and see the same output. But now, notice that the code of ExampleService is much more clean allowing you to focus on your business rules.

Conclusion

By creating a custom annotation and an aspect, we can easily log into any class of our application. This approach not only helps in debugging but also keeps our code clean and modular.

Benefits of Using AOP:

  1. It allows separation of cross-cutting concerns, like logging, from the main business logic;
  2. It allows reusing across different parts of your application, reducing code duplication;
  3. Single Responsibility Principle: By separating logging and other cross-cutting concerns into aspects, each class has only one reason to change;
  4. Open/Closed Principle: It allows you to add new behaviors (like logging) without modifying existing code, making your classes open for extension but closed for modification.
💖 💪 🙅 🚩
thiagoematos
Thiago Matos

Posted on August 9, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related