Auto Logging in Class and Method Level Using Custom Annotations in Spring Boot App
Learn how to automate logging in class and method level using custom annotations in the SpringBoot application, benefitting aspect-oriented programming.
Join the DZone community and get the full member experience.
Join For FreeIt doesn't matter which type or size of software application we are developing, we all come to a point where we want to do a process over and over again but also don't want to re-implement or make a recall every time.
This situation is not new nor it is valid for one case. There are many different variations for this situation and as a result many solutions. I would like to stage one of these solutions in a process common for all applications: Logging. I will be creating a spring boot application and will benefit from AOP.
Let's first list what we want to achieve:
- Able to define class so every method in it will be logged when called
- Able to exclude a method in the class from being logged
- Able to define methods easily as loggable when we don't want the whole class to be loggable
Now lets first create a spring boot application named "logger" and add the following dependencies:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
Next, let's create our custom annotations to manage the loggability of our classes and methods. Let's have 3 different annotations.
- An annotation to define the classes as loggable
- An annotation to define the methods as loggable
- An annotation to define the methods as not loggable
1. Annotation Loggable Class
We define our loggable class annotation as follows:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface LoggableClass {}
@Target indicates the contexts in which an annotation type is applicable. ElementType.TYPE is used to indicate that our annotation is for classes.
@Retention indicates how long annotations with the annotated type are to be retained. Since we want them to be retained in run time, we pass RetentionPolicy.RUNTIME to it.
2. Annotation Loggable Method
We define our loggable method annotation just like our loggable class annotation but with just one difference. Instead of passing ElementType.TYPE to @Target, we pass ElementType.METHOD.
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface LoggableMethod {}
3. Annotation Not Loggable Method
This is the same as the loggable method.
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NotLoggableMethod {}
Now we have our annotations. It is time to make these annotations meaningful. We must define what will be done when a class or method has these annotations.
Let's create a class named Logger:
@Aspect
@Component
public class Logger {
}
Here we add @Aspect
to make the class to be detected automatically by spring and make the necessary configuration for AOP. We also add @Component
for spring to detect our bean.
Now let's define which executions we are interested in.
As we mentioned before, our first interest is all method executions in a class with annotated @LoggableClass
. We define this as the following:
@Pointcut("@within(com.kutluk.logger.annotations.LoggableClass)")
public void loggableClass() {}
The @Pointcut
annotation has two parts. The first part is the first line which is the expression part. The expression covers everything within the class with LoggableClass annotation. The second part is the second line which is the signature. It is a sort of a shortcut for the expression.
Our second and third interests are similar. Let's now define them.
@Pointcut("@annotation(com.kutluk.logger.annotations.LoggableMethod)")
public void loggableMethod() {}
@Pointcut("@annotation(com.kutluk.logger.annotations.NotLoggableMethod)")
public void notLoggableMethod() {}
For these two pointcuts, we defined our join points using "@annotation" instead of "@within" in the expression part because we only want to cover methods that have our annotation. We don't need more.
Since we have our pointcuts ready, now let's define the "advice" which is what to do when the execution comes to our join points.
There are many types of "advice" which we can use but we will use the following three:
@Before
- The advice that is executed before the join point@After
- The advice that is executed after the join point@AfterThrowing
- The advice that is executed after an exception is thrown at the join point
Let's create our "advice" which just prints out what they are called.
@Before("(loggableClass() || loggableMethod()) && !notLoggableMethod()")
public void adviceBefore(JoinPoint jp) {
System.out.println("@Before called");
}
@After("(loggableClass() || loggableMethod()) && !notLoggableMethod()")
public void adviceAfter(JoinPoint jp) {
System.out.println("@After called");
}
@AfterThrowing("(loggableClass() || loggableMethod()) && !notLoggableMethod()")
public void adviceAfterThrowing(JoinPoint jp){
System.out.println("@AfterThrowing called");
}
All three "advices" take a logical expression using the pointcut signatures. In our case; for all of them, we gave the same expression: if the joining point is coming from a loggable class or from a loggable method, but at the same time not from a not loggable method.
All three "advices" also have the same parameter: JoinPoint jp. This is optional. It contains information about the joining point which caused advice to be triggered which is very useful but we won't benefit from it in our case.
Now our structure is ready. What we need is now is some dummy code with the annotations we just created and a code enabling us to call the dummy code. For this let's create:
- A dummy class that has
@LoggableClass
annotation and multiple methods where some have @NotLoggableMethod annotation - A dummy class with multiple methods with
@LoggableMethod
annotation - A dummy rest service that calls all the methods inside the two dummy classes mentioned above
1. Dummy Class With @LoggableClass Annotation
@LoggableClass
@Service
public class ClassWhichIsLoggable {
public void methodOne(){
System.out.println("ClassWhichIsLoggable -> method with no annotation called");
}
@LoggableMethod
public void methodTwo(){
System.out.println("ClassWhichIsLoggable -> method with @LoggableMethod annotation called");
}
@NotLoggableMethod
public void methodThree(){
System.out.println("ClassWhichIsLoggable -> method with @NotLoggableMethod annotation called");
}
public void methodFour() throws Exception{
System.out.println("ClassWhichIsLoggable -> method throwing exception called");
throw new Exception("custom exception");
}
}
2. Dummy Class With @LoggableMethod Annotation
@Service
public class ClassWithLoggableMethods {
@LoggableMethod
public void methodOne(){
System.out.println("ClassWithLoggableMethods -> method with @LoggableMethod annoation called");
}
@LoggableMethod
public void methodTwo() throws Exception{
System.out.println("ClassWithLoggableMethods -> method with @LoggableMethod annoation throwing exception called");
throw new Exception("custom exception");
}
public void methodThree(){
System.out.println("ClassWithLoggableMethods -> method without @LoggableMethod annoation called");
}
}
3. Dummy Rest Service That Calls All Methods
@RestController
@RequestMapping("/logRest")
public class LogRestController {
@Autowired
ClassWithLoggableMethods classWithLoggableMethods;
@Autowired
ClassWhichIsLoggable classWhichIsLoggable;
@RequestMapping(value = "/serviceWithLog", method = RequestMethod.POST)
@ResponseBody
public String serviceWithLog() {
System.out.println("\n\n\n------------------------------------------------------------------------------");
System.out.println(" ClassWhichIsLoggable");
System.out.println("------------------------------------------------------------------------------");
printSeparator();
classWhichIsLoggable.methodOne();
printSeparator();
classWhichIsLoggable.methodTwo();
printSeparator();
classWhichIsLoggable.methodThree();
printSeparator();
try {
classWhichIsLoggable.methodFour();
}catch(Exception e){
System.out.println("Custom exception caught");
}
System.out.println("\n\n\n------------------------------------------------------------------------------");
System.out.println(" ClassWithLoggableMethods");
System.out.println("------------------------------------------------------------------------------");
printSeparator();
classWithLoggableMethods.methodOne();
printSeparator();
try {
classWithLoggableMethods.methodTwo();
}catch(Exception e){
System.out.println("Custom exception caught");
}
printSeparator();
classWithLoggableMethods.methodThree();
printSeparator();
return "done";
}
private void printSeparator(){
System.out.println("\n---------------------------------------\n");
}
}
Let's run our application and call the dummy rest service and see the result:
You can find the source code for the application at the following repository: https://github.com/artunkutluk/AopLogging.git
Opinions expressed by DZone contributors are their own.
Comments