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
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
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
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.
thank you very much