Logging, Performance Monitoring, Security and Transaction Management with Spring AOP



This article is the continuation of the previous article and aims to implement logging, events publishing, user access security, transaction management, and performance monitoring using Spring AOP.

Design

Spring AOP aspects, advice, pointcut and join points
Spring AOP

Logging

When you want to log some method calls without modifying the actual code, then you can do so using Spring AOP. For example, if we have to log all the method calls of Spring Data JPA Repositories, then we can configure the logging functionality as shown below.

LogTraceConfig.java

package com.javachinna.config;

import org.aspectj.lang.annotation.Aspect;
import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.interceptor.CustomizableTraceInterceptor;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@Aspect
public class LogTraceConfig {

	@Bean
	public CustomizableTraceInterceptor interceptor() {
		CustomizableTraceInterceptor interceptor = new CustomizableTraceInterceptor();
		interceptor.setEnterMessage("Entering $[methodName]($[arguments]).");
		interceptor.setExitMessage("Leaving $[methodName](..) with return value $[returnValue], took $[invocationTime]ms.");
		return interceptor;
	}

	@Bean
	public Advisor traceAdvisor() {
		AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
		pointcut.setExpression("execution(public * org.springframework.data.repository.Repository+.*(..))");
		return new DefaultPointcutAdvisor(pointcut, interceptor());
	}
}

Pointcut expression pattern

execution( [scope] [ReturnType] [FullClassName].[MethodName] ([Arguments]) throws [ExceptionType])

Understanding the Logging Pointcut Expression

logging pointcut expression

Performance Monitoring

When you are facing some performance issues with your application and you wanna find out the execution time of service and repository methods, then you can get that information in the log by using the following aspect.

PerformanceMonitorConfig.java

package com.javachinna.config;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.interceptor.PerformanceMonitorInterceptor;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@Aspect
public class PerformanceMonitorConfig {

	@Bean
	public PerformanceMonitorInterceptor performanceMonitorInterceptor() {
		return new PerformanceMonitorInterceptor(true);
	}

	/**
	 * Pointcut for execution of methods on classes annotated with {@link Service}
	 * annotation
	 */
	@Pointcut("execution(public * (@org.springframework.stereotype.Service com.javachinna..*).*(..))")
	public void serviceAnnotation() {
	}

	/**
	 * Pointcut for execution of methods on classes annotated with
	 * {@link Repository} annotation
	 */
	@Pointcut("execution(public * (@org.springframework.stereotype.Repository com.javachinna..*).*(..))")
	public void repositoryAnnotation() {
	}

	@Pointcut("serviceAnnotation() || repositoryAnnotation()")
	public void performanceMonitor() {
	}

	@Bean
	public Advisor performanceMonitorAdvisor() {
		AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
		pointcut.setExpression("com.javachinna.config.PerformanceMonitorConfig.performanceMonitor()");
		return new DefaultPointcutAdvisor(pointcut, performanceMonitorInterceptor());
	}
}

Events Publishing

In a social networking application like Facebook, when someone comments on a post, then we may need to notify the owner of that post. In this use case, we will be creating a new comment entity and persist into the database. Hence, we can trigger an event with the help of the below aspect whenever a new entity is persisted.

EventPublishingAspect.java

This aspect triggers an EntityCreatedEvent whenever the repository.save(..) method is called to persist a new entity

@Aspect annotation declares an aspect

@AfterReturning annotation defines an aspect that will be executed after the repository.save(..) method (which returns the saved entity) is executed without any exception.

package com.javachinna.config;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class EventPublishingAspect {
	@Autowired
	private ApplicationEventPublisher eventPublisher;

	@AfterReturning(value = "execution(public * org.springframework.data.repository.Repository+.save*(..))", returning = "entity")
	public void publishEntityCreatedEvent(JoinPoint jp, Object entity) throws Throwable {
		String entityName = entity.getClass().getSimpleName();
		if (!entityName.endsWith("EntityNamesToBeExcluded")) {
			eventPublisher.publishEvent(new EntityCreatedEvent(entity));
		}
	}
}

Understanding Event Publishing Pointcut Expression

events publishing pointcut expression

EntityCreatedEvent.java

package com.javachinna.config;

import org.springframework.context.ApplicationEvent;

public class EntityCreatedEvent extends ApplicationEvent {

	public EntityCreatedEvent(Object source) {
		super(source);
	}
}

EntityCreationEventListener.java

package com.javachinna.config;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.context.ApplicationListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

@Component
public class EntityCreationEventListener implements ApplicationListener<EntityCreatedEvent> {
	private final Logger logger = LogManager.getLogger(getClass());

	@Async
	@Override
	public void onApplicationEvent(EntityCreatedEvent event) {
		logger.info("Created instance: " + event.getSource().toString());
		// Logic goes here to notify the interested parties
	}
}

User Access Restriction

In a social networking site like Facebook, one user cannot modify or delete other user’s comments. Only the user who posted it or the admin can modify or delete it. In this use case, the following aspect can be used to restrict the user from modifying or deleting an entity if he is not the owner of it.

UserAccessAspect.java

This aspect checks if the currently logged in user is the owner of the entity that he is trying to modify or delete. if not, it throws an exception.

@Pointcut annotation declares a pointcut that matches the repository.save() and delete() method executions

@Before annotation defines an advice that would intercept the save and delete method calls before they are executed

package com.javachinna.config;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.context.SecurityContextHolder;

import com.javachinna.model.AbstractEntity;
import com.javachinna.model.LocalUser;
import com.javachinna.model.Role;
import com.javachinna.model.UserEntity;

@Aspect
@Configuration
public class UserAccessAspect {

	private final Logger logger = LogManager.getLogger(getClass());

	@Pointcut("execution(public * org.springframework.data.repository.Repository+.save(..))")
	public void saveMethods() {
	}

	@Pointcut("execution(public * org.springframework.data.repository.Repository+.delete(..))")
	public void deleteMethods() {
	}

	// What kind of method calls I would intercept
	// execution(* PACKAGE.*.*(..))
	// Weaving & Weaver
	@Before("saveMethods() || deleteMethods()")
	public void before(JoinPoint joinPoint) {
		// Advice
		logger.info("Check for user access");
		Object object = joinPoint.getArgs()[0];
		if (object instanceof AbstractEntity) {
			// Get the currently logged in user from the Spring Security Context
			LocalUser user = (LocalUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
			if (isAdmin(user.getUser())) {
				return; // User has admin rights
			}
			// AbstractEntity is a mapped super class with common properties like id, createdBy,
			// modifiedBy etc which will be extended by multiple entities
			if (object instanceof AbstractEntity) {
				AbstractEntity entity = (AbstractEntity) object;
				// Check if the currently logged in user is the owner of this entity
				if (entity.getId() != null && !isSameUser(user.getUser(), entity.getCreatedBy())) {
					throw new RuntimeException("User does not have access to modify this entity id: " + entity.getId());
				}
			}
		}
	}

	private static boolean isAdmin(UserEntity user) {
		if (user != null) {
			for (Role role : user.getRoles()) {
				return role.getName().equals(Role.ROLE_ADMIN);
			}
		}
		return false;
	}

	private static boolean isSameUser(UserEntity currentUser, UserEntity entityOwner) {
		if (currentUser != null && entityOwner != null) {
			return currentUser.getId().equals(entityOwner.getId());
		}
		return false;
	}
}

Transaction Management

Spring provides support for both programmatic and declarative transactions.

Programmatic Transactions

With programmatic transactions, transaction management code needs to be explicitly written so as to commit when everything is successful and rolling back if anything goes wrong. The transaction management code is tightly bound to the business logic in this case.

Declarative Transactions

Declarative transactions separates transaction management code from business logic. Spring supports declarative transactions using transaction advice (using AOP) via @Transactional annotation.

To start using @Transactional annotation in our Spring Boot application, we need to add the @EnableTransactionManagement annotation in the @Configuration class.

Understanding @Transactional annotation

At a high level, when a class declares @Transactional on itself or its members, Spring creates a proxy that implements the same interface(s) as the class you’re annotating. In other words, Spring wraps the bean in the proxy and the bean itself has no knowledge of it. A proxy provides a way for Spring to inject behaviors before, after, or around method calls into the object being proxied.

Internally, it’s the same as using transaction advice (using AOP), where a proxy is created first and is invoked before/after the target bean’s method.

The generated proxy object is supplied with a TransactionInterceptor, which is created by Spring. So when the @Transactional method is called from client code, the TransactionInterceptor gets invoked first from the proxy object, which begins the transaction and eventually invokes the method on the target bean. When the invocation finishes, the TransactionInterceptor commits/rolls back the transaction accordingly.

Note that only calls from “outside” the target bean go through the proxy.

Run with Maven

You can run the application using mvn clean spring-boot:run 

Output

Logging

2020-04-01 19:23:25.680 TRACE 12900 --- [restartedMain] o.s.a.i.CustomizableTraceInterceptor     : Entering save(Role [name=ROLE_USER][id=1]).
2020-04-01 19:23:25.683  INFO 12900 --- [restartedMain] sAspect$$EnhancerBySpringCGLIB$$360075ef : Check for user access
2020-04-01 19:23:26.303  INFO 12900 --- [restartedMain] c.j.config.EntityCreationEventListener   : Created instance: Role [name=ROLE_USER][id=1]
2020-04-01 19:23:26.303 TRACE 12900 --- [restartedMain] o.s.a.i.CustomizableTraceInterceptor     : Leaving save(..) with return value Role

Performance Monitoring

2020-04-01 19:23:26.825 TRACE 12900 --- [restartedMain] c.j.service.impl.UserServiceImpl         : StopWatch 'com.javachinna.service.impl.UserServiceImpl.save': running time = 520763184 ns

Events Publishing

2020-04-01 19:23:26.824  INFO 12900 --- [restartedMain] c.j.config.EntityCreationEventListener   : Created instance: com.javachinna.model.UserEntity@2e0f3289

Seccurity

2020-04-01 19:23:27.408  INFO 12900 --- [restartedMain] sAspect$$EnhancerBySpringCGLIB$$360075ef : Check for user access
2020-04-01 19:23:27.416 TRACE 12900 --- [restartedMain] o.s.a.i.CustomizableTraceInterceptor     : Exception thrown in method 'save' of class [com.sun.proxy.$Proxy109]

java.lang.RuntimeException: User does not have access to modify this entity id: 1
	at com.javachinna.config.UserAccessAspect.before(UserAccessAspect.java:51) ~[classes/:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]

Source code

https://github.com/JavaChinna/spring-boot-aop-real-time-samples

References

https://docs.spring.io/spring/docs/5.2.x/spring-framework-reference/core.html#aop

http://learningsolo.com/spring-aop-weaving-mechanism/

https://howtodoinjava.com/spring-aop-tutorial/

https://www.javacodegeeks.com/2016/05/understanding-transactional-annotation-spring.html

Conclusion

That’s all folks! In this article, you’ve learned how to implement logging, events publishing, security, transaction management, and performance monitoring using Spring AOP.

I hope you enjoyed this article. Please share it with your friends if you liked it. Thank you for reading.

Leave a Reply