Spring AOP Application with Real Time Examples

In this article, we are gonna see the basic concepts of Aspect Oriented Programming and develop a Spring Boot application in order to demonstrate some real time examples. If you already have knowledge about AOP and Spring AOP applications and just looking for the real time usages of Spring AOP, then you can skip to the next article.

What is AOP ?

Aspect-oriented Programming (AOP) complements Object-oriented Programming (OOP) by providing another way of thinking about program structure. The key unit of modularity in OOP is the class, whereas in AOP the unit of modularity is the aspect.

Aspects enable the modularization of concerns (such as transaction management, logging, performance monitoring) that cut across multiple types and objects. (Such concerns are often termed “crosscutting” concerns in AOP literature.)

aop cross cutting concerns

In simple terms, it’s just an interceptor to intercept some processes, for example, when a method is executing, Spring AOP can hijack the executing method, and add extra functionality before or after the method execution.

What is Weaving ?

Weaving is the process of linking aspects with other application types or objects to create an advised object. This can be done at compile time (using the AspectJ compiler, for example), load time, or at runtime. Spring AOP, like other pure Java AOP frameworks, performs weaving at runtime.

Spring AOP does dynamic weaving of aspects by creating proxy of the target objects.

It uses either JDK dynamic proxies or CGLIB to create the proxy for a given target object. (JDK dynamic proxies are preferred whenever you have a choice).

If the target object to be proxied implements at least one interface then a JDK dynamic proxy will be used. All of the interfaces implemented by the target type will be proxied. If the target object does not implement any interfaces then a CGLIB proxy will be created. You can learn more about Spring AOP in the official documentation.

What is Aspect, Advice, Join point and Pointcut ?

  • Aspect: A modularization of a concern that cuts across multiple classes. Transaction management is a good example of a crosscutting concern in enterprise Java applications. In Spring AOP, aspects are implemented by using regular classes (the schema-based approach) or regular classes annotated with the @Aspect annotation (the @AspectJ style).
  • Join point: A point during the execution of a program, such as the execution of a method or the handling of an exception. In Spring AOP, a join point always represents a method execution.
  • Advice: Action taken by an aspect at a particular join point. Many AOP frameworks, including Spring, model an advice as an interceptor and maintain a chain of interceptors around the join point.
  • Pointcut: A predicate or expression that matches join points. Advice is associated with a pointcut expression and runs at any join point matched by the pointcut (for example, the execution of a method with a certain name). The concept of join points as matched by pointcut expressions is central to AOP, and Spring uses the AspectJ pointcut expression language by default.
  • Introduction: Declaring additional methods or fields on behalf of a type. Spring AOP lets you introduce new interfaces (and a corresponding implementation) to any advised object. For example, you could use an introduction to make a bean implement an IsModified interface, to simplify caching. (An introduction is known as an inter-type declaration in the AspectJ community.)
  • Target object: An object being advised by one or more aspects. Also referred to as the “advised object”. Since Spring AOP is implemented by using runtime proxies, this object is always a proxied object.
  • AOP proxy: An object created by the AOP framework in order to implement the aspect contracts (advise method executions and so on). In the Spring Framework, an AOP proxy is a JDK dynamic proxy or a CGLIB proxy.

Note: The Spring AOP framework supports only limited types of AspectJ pointcuts and allows aspects to apply to beans declared in the IoC container. If you want to use additional pointcut types or apply your aspects “to objects created outside the Spring IoC container“, you have to use the AspectJ framework in your Spring application and use it’s weaving feature.

Types of AOP advices

  1. Before advice: Advice that executes before a join point, but which does not have the ability to prevent execution flow proceeding to the join point (unless it throws an exception).
  2. After returning advice: Advice to be executed after a join point completes normally: for example, if a method returns without throwing an exception.
  3. After throwing advice: Advice to be executed if a method exits by throwing an exception.
  4. After (finally) advice: Advice to be executed regardless of the means by which a join point exits (normal or exceptional return).
  5. Around advice: Advice that surrounds a join point such as a method invocation. This is the most powerful kind of advice. Around advice can perform custom behavior before and after the method invocation. It is also responsible for choosing whether to proceed to the join point or to shortcut the advised method execution by returning its own return value or throwing an exception.

Bootstrap your application

You can create your spring boot application with the spring boot starter aop, security, jpa & devtools dependencies and download it from here

Project Dependencies

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.2.5.RELEASE</version>
		<relativePath /> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.javachinna</groupId>
	<artifactId>spring-boot-aop-demo</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>spring-boot-aop-demo</name>
	<description>Demo project for Spring Boot AOP</description>

	<properties>
		<java.version>11</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-aop</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency> 
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
			<exclusions>
				<exclusion>
					<groupId>org.junit.vintage</groupId>
					<artifactId>junit-vintage-engine</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<!-- <scope>runtime</scope> -->
		</dependency>

	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>

Create model classes and entities

UserEntity.java

package com.javachinna.model;

import java.util.Set;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;

@Entity
public class UserEntity {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name = "USER_ID")
	private Long id;

	private String username;
	private String password;
	// bi-directional many-to-many association to Role
	@ManyToMany
	@JoinTable(name = "user_role", joinColumns = { @JoinColumn(name = "USER_ID") }, inverseJoinColumns = { @JoinColumn(name = "ROLE_ID") })
	private Set<Role> roles;

	public UserEntity() {
		// TODO Auto-generated constructor stub
	}

	public UserEntity(String username, String password) {
		this.username = username;
		this.password = password;
	}

	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	public String getUsername() {
		return username;
	}

	public void setUsername(String username) {
		this.username = username;
	}

	public String getPassword() {
		return password;
	}

	public void setPassword(String password) {
		this.password = password;
	}

	public Set<Role> getRoles() {
		return roles;
	}

	public void setRoles(Set<Role> roles) {
		this.roles = roles;
	}
}

Role.java

package com.javachinna.model;

import java.io.Serializable;
import java.util.Set;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.ManyToMany;
import javax.persistence.NamedQuery;

/**
 * The persistent class for the role database table.
 */
@Entity
@NamedQuery(name = "Role.findAll", query = "SELECT r FROM Role r")
public class Role implements Serializable {
	private static final long serialVersionUID = 1L;
	public static final String USER = "USER";
	public static final String ADMIN = "ADMIN";
	public static final String DBA = "DBA";
	public static final String ROLE_USER = "ROLE_USER";
	public static final String ROLE_ADMIN = "ROLE_ADMIN";
	public static final String ROLE_DBA = "ROLE_DBA";

	@Id
	@Column(name = "ROLE_ID")
	private int roleId;

	private String name;

	// bi-directional many-to-many association to User
	@ManyToMany(mappedBy = "roles")
	private Set<UserEntity> users;

	public Role() {
	}

	public int getRoleId() {
		return this.roleId;
	}

	public void setRoleId(int roleId) {
		this.roleId = roleId;
	}

	public String getName() {
		return this.name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public Set<UserEntity> getUsers() {
		return this.users;
	}

	public void setUsers(Set<UserEntity> users) {
		this.users = users;
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((name == null) ? 0 : name.hashCode());
		return result;
	}

	@Override
	public boolean equals(final Object obj) {
		if (this == obj) {
			return true;
		}
		if (obj == null) {
			return false;
		}
		if (getClass() != obj.getClass()) {
			return false;
		}
		final Role role = (Role) obj;
		if (!role.equals(role.name)) {
			return false;
		}
		return true;
	}

	@Override
	public String toString() {
		final StringBuilder builder = new StringBuilder();
		builder.append("Role [name=").append(name).append("]").append("[id=").append(roleId).append("]");
		return builder.toString();
	}
}

AbstractEntity.java

@MappedSuperclass annotation is used to allow an entity to inherit properties from a base class.

This super class defines some common properties like id, createdBy, etc., which will be inherited by the child classes

package com.javachinna.model;

import java.io.Serializable;

import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.MappedSuperclass;

import org.springframework.data.annotation.CreatedBy;

@MappedSuperclass
public abstract class AbstractEntity implements Serializable {
	private static final long serialVersionUID = 1L;

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;

	@CreatedBy
	// bi-directional many-to-one association to User
	@ManyToOne(fetch = FetchType.EAGER)
	@JoinColumn(name = "created_by")
	protected UserEntity createdBy;

	public UserEntity getCreatedBy() {
		return createdBy;
	}

	public void setCreatedBy(UserEntity createdBy) {
		this.createdBy = createdBy;
	}

	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

}

ExampleEntity.java

package com.javachinna.model;

import javax.persistence.Entity;
import javax.persistence.Table;

@Entity
@Table(name = "example")
public class ExampleEntity extends AbstractEntity {
	private static final long serialVersionUID = 1L;

	private String text;

	public String getText() {
		return text;
	}

	public void setText(String text) {
		this.text = text;
	}
}

LocalUser.java

LocalUser extends org.springframework.security.core.userdetails.User which models core user information

package com.javachinna.model;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;

public class LocalUser extends User {

	private static final long serialVersionUID = -2845160792248762779L;
	private UserEntity user;

	public LocalUser(final String userID, final String password, final boolean enabled, final boolean accountNonExpired, final boolean credentialsNonExpired,
			final boolean accountNonLocked, final Collection<? extends GrantedAuthority> authorities, final UserEntity user) {
		super(userID, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
		this.user = user;
	}

	public static LocalUser create(UserEntity user) {
		LocalUser localUser = new LocalUser(user.getUsername(), user.getPassword(), true, true, true, true, buildSimpleGrantedAuthorities(user.getRoles()), user);
		return localUser;
	}

	public UserEntity getUser() {
		return user;
	}

	public static List<SimpleGrantedAuthority> buildSimpleGrantedAuthorities(final Set<Role> roles) {
		List<SimpleGrantedAuthority> authorities = new ArrayList<>();
		for (Role role : roles) {
			authorities.add(new SimpleGrantedAuthority(role.getName()));
		}
		return authorities;
	}
}

Create repositories

UserRepository.java

package com.javachinna.repo;

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import com.javachinna.model.UserEntity;

@Repository
public interface UserRepository extends JpaRepository<UserEntity, Long> {

	@Transactional
	@Modifying
	@Query("delete from UserEntity where id in :users")
	public void deleteUsers(@Param("users") List<Long> users);
}

RoleRepository.java

package com.javachinna.repo;

import org.springframework.data.jpa.repository.JpaRepository;

import com.javachinna.model.Role;

public interface RoleRepository extends JpaRepository<Role, Long> {

	Role findByName(String name);

	@Override
	void delete(Role role);

}

ExampleRepository.java

package com.javachinna.repo;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import com.javachinna.model.ExampleEntity;

@Repository
public interface ExampleRepository extends JpaRepository<ExampleEntity, Long> {

}

Create service layer

UserService.java

package com.javachinna.service;

import java.util.List;

import com.javachinna.model.UserEntity;

public interface UserService {

	public UserEntity save(UserEntity user);

	void deleteUsers(List<Long> userIds);
}

UserServiceImpl.java

package com.javachinna.service.impl;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.javachinna.model.UserEntity;
import com.javachinna.repo.UserRepository;
import com.javachinna.service.UserService;

@Service
public class UserServiceImpl implements UserService {

	@Autowired
	private UserRepository userRepository;

	@Override
	public UserEntity save(UserEntity user) {
		return userRepository.save(user);
	}

	@Override
	public void deleteUsers(List<Long> userIds) {
		userRepository.deleteUsers(userIds);
	}
}

Define Application Properties

application.properties

# Database configuration props
spring.datasource.url=jdbc:mysql://localhost:3306/aop?createDatabaseIfNotExist=true
spring.datasource.username=root
spring.datasource.password=secret
 
# hibernate props
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5InnoDBDialect
logging.level.com.javachinna=trace
logging.level.org.springframework.aop.interceptor=trace

Create Spring boot application class

SpringBootAopApplication.java

@SpringBootApplication Indicates a configuration class that declares one or more @Bean methods and also triggers auto-configuration and component scanning. This is a convenience annotation that is equivalent to declaring @Configuration@EnableAutoConfiguration and @ComponentScan

@EnableTransactionManagement annotation which enables Spring’s annotation-driven transaction management capability, similar to the support found in Spring’s <tx:*> XML namespace. To be used on @Configuration classes only.

@EnableAspectJAutoProxy annotation enables support for handling components marked with AspectJ’s @Aspect annotation, similar to functionality found in Spring’s <aop:aspectj-autoproxy> XML element. To be used any one of the @Configuration classes as follows:

CommandLineRunner is an interface. It is used to execute the code after the Spring Boot application started.

package com.javachinna;

import java.util.HashSet;
import java.util.List;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import com.javachinna.model.ExampleEntity;
import com.javachinna.model.LocalUser;
import com.javachinna.model.Role;
import com.javachinna.model.UserEntity;
import com.javachinna.repo.ExampleRepository;
import com.javachinna.repo.RoleRepository;
import com.javachinna.service.UserService;

@SpringBootApplication
@EnableTransactionManagement
@EnableAspectJAutoProxy
public class SpringBootAopApplication implements CommandLineRunner {

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

	@Autowired
	private UserService userService;

	@Autowired
	private RoleRepository roleRepository;

	@Autowired
	private ExampleRepository exampleRepository;

	public static void main(String[] args) {
		SpringApplication.run(SpringBootAopApplication.class, args);

	}

	@Override
	public void run(String... args) throws Exception {

		// Create new user role and persist it
		Role role = new Role();
		role.setRoleId(1);
		role.setName(Role.ROLE_USER);
		var userRole = roleRepository.save(role);

		final HashSet<Role> roles = new HashSet<Role>();
		roles.add(userRole);

		// Publishing events + logging + Performance Monitoring example

		// Create and persist user1
		UserEntity user1 = new UserEntity("JavaChinna", "secret");
		user1.setRoles(roles);
		// Time taken for executing by the service method will be logged
		// Repository method entry and exit will be logged
		// Entity created event will be triggered
		userService.save(user1);

		// Set user1 as the logged in user in the Spring's Security Context
		authenticateUser(LocalUser.create(user1));

		// Create and persist user2
		UserEntity user2 = new UserEntity("Chinna", "secret");
		user2.setRoles(roles);
		userService.save(user2);

		// User access restriction example
		ExampleEntity example = new ExampleEntity();
		example.setText("some text");
		// Set user2 as the owner of this entity. So that currently logged in user
		// (user1) cannot modify this entity
		example.setCreatedBy(user2);
		exampleRepository.save(example);

		// Try to modify this entity
		example.setText("some other text");

		try {
			exampleRepository.save(example); // Throws access denied exception
		} catch (Exception e) {
		}

		// Transaction Management
		userService.deleteUsers(List.of(1L));
	}

	private void authenticateUser(UserDetails userDetails) {
		logger.debug("Logging in principal: {}", userDetails);
		Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
		SecurityContextHolder.getContext().setAuthentication(authentication);
		logger.info("User: {} has been logged in.", userDetails);
	}
}

Conclusion

That’s all folks! We’ve developed a Spring Boot AOP application and in the next article we are gonna implement some real time examples like logging, performance monitoring, events publishing and security with Spring AOP.

Leave a Reply