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")
}
}
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)
}
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
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
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
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" }
?: ""
}
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")
}
}
}
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:
- It allows separation of cross-cutting concerns, like logging, from the main business logic;
- It allows reusing across different parts of your application, reducing code duplication;
- Single Responsibility Principle: By separating logging and other cross-cutting concerns into aspects, each class has only one reason to change;
- 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.
Top comments (0)