Step 5 – AOP (Aspect Oriented Programming)

Important

Aspect-

It’s a class, which contains advices, join points, it can be configured using configuration file,  or spring AspectJ integration.

Join point-

It’s a point, where we can plug in AOP aspect, spring only supports execution joint type.

Code example-

public class UserServiceImpl implements UserService {
    public void addUser(User user) {
        // implementation code
    }
}

In this example, the execution of the addUser method is a join point where an aspect can be applied.

Advice –

It represents the methods to be executed at a particular join point.

public class LoggingAspect {
    public void logBefore(JoinPoint joinPoint) {
        System.out.println("Before " + joinPoint.getSignature().getName() + " method is called");
    }
}

In this example, the logBefore method is a “before” advice that logs a message before the execution of a method.

public class LoggingAspect {
    @Pointcut("execution(* com.example.service.*.*(..))")
    public void serviceMethods() {}

    @Before("serviceMethods()")
    public void logBefore(JoinPoint joinPoint) {
        System.out.println("Before " + joinPoint.getSignature().getName() + " method is called");
    }
}

In this example, the serviceMethods() method is a pointcut that matches all methods in the com.example.service package

public interface Auditable {
    void audit();
}

public class UserServiceImpl implements UserService, Auditable {
    public void addUser(User user) {
        // implementation code
    }

    public void audit() {
        System.out.println("Audit trail for user service");
    }
}

In this example, the Auditable interface is introduced to the UserServiceImpl class, which allows it to implement the audit() method.

It’s an object on which advice are applied, spring uses proxied object.

public class UserServiceImpl implements UserService {
    public void addUser(User user) {
        // implementation code
    }
}

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
    @Bean
    public LoggingAspect loggingAspect() {
        return new LoggingAspect();
    }

    @Bean
    public UserService userService() {
        return new UserServiceImpl();
    }
}

In this example, the UserServiceImpl object is the target object that the LoggingAspect advice is applied to.

Weaving –

It’s a process of linking aspects with other application type/object to create the advised proxy objects.

public class UserServiceImpl implements UserService {
    public void addUser(User user) {
        // implementation code
    }
}

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
    @Bean
    public LoggingAspect loggingAspect() {
        return new LoggingAspect();
    }

    @Bean
    public UserService userService() {
        return new UserServiceImpl();
    }
}

In this example, the LoggingAspect is woven into the UserService object at runtime to create a new advised proxy object. This is accomplished by using the @EnableAspectJAutoProxy annotation, which enables AspectJ-based proxying for Spring beans. When a bean is created by the Spring container, the container checks whether any aspects are applicable to the bean and creates a proxy object that intercepts method invocations on the original bean. The proxy object then applies the advice associated with the applicable aspect before or after the method execution, depending on the type of advice. This process is known as weaving, and it allows aspects to be applied to objects without modifying their original source code.

  1. Before advice example:
public class LoggingAspect {
    @Before("execution(* com.example.UserService.addUser(..))")
    public void beforeAddUserAdvice() {
        System.out.println("Before adding a user...");
    }
}

In this example, the beforeAddUserAdvice() method is executed before the addUser() method of the UserService class is called. The @Before annotation specifies the pointcut expression that defines when the advice should be executed.

  1. After returning advice example:
public class LoggingAspect {
    @AfterReturning("execution(* com.example.UserService.getUser(..))")
    public void afterGetUserAdvice() {
        System.out.println("After getting a user...");
    }
}

In this example, the afterGetUserAdvice() method is executed after the getUser() method of the UserService class has successfully completed. The @AfterReturning annotation specifies the pointcut expression that defines when the advice should be executed.

  1. After throwing advice example:
public class LoggingAspect {
    @AfterThrowing(value="execution(* com.example.UserService.deleteUser(..))", throwing="ex")
    public void afterDeleteUserAdvice(Throwable ex) {
        System.out.println("After deleting a user...");
        System.out.println("Exception message: " + ex.getMessage());
    }
}

In this example, the afterDeleteUserAdvice() method is executed when the deleteUser() method of the UserService class throws an exception. The @AfterThrowing annotation specifies the pointcut expression that defines when the advice should be executed, and the throwing attribute specifies the name of the variable that will hold the thrown exception.

  1. After (finally) advice example:
public class LoggingAspect {
    @After("execution(* com.example.UserService.*(..))")
    public void afterUserServiceAdvice() {
        System.out.println("After calling a UserService method...");
    }
}

In this example, the afterUserServiceAdvice() method is executed after any method of the UserService class is called, regardless of whether it completes normally or throws an exception. The @After annotation specifies the pointcut expression that defines when the advice should be executed.

  1. Around advice example:
public class LoggingAspect {
    @Around("execution(* com.example.UserService.getAllUsers(..))")
    public Object aroundGetAllUsersAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("Before getting all users...");
        Object result = joinPoint.proceed();
        System.out.println("After getting all users...");
        return result;
    }
}

In this example, the aroundGetAllUsersAdvice() method is executed around the getAllUsers() method of the UserService class. The @Around annotation specifies the pointcut expression that defines when the advice should be executed. The method takes a ProceedingJoinPoint object as a parameter, which allows it to control the method invocation. The proceed() method is called on the ProceedingJoinPoint object to proceed with the method invocation, and the advice can perform additional logic before and after the method call. The method returns the result of the method invocation.

Let’s write try to integrate above examples (with and without IOC) with Logging, we’ll see how AOP makes it easier in spring.

-Integrate Logging in without IOC program-

public class SimpleSpellChecker implements SpellChecker {
    public void checkSpelling() {
        System.out.println("Inside checkSpelling method of SimpleSpellChecker");
        // logging the start of method execution
        Logger.getLogger(SimpleSpellChecker.class.getName()).info("Starting checkSpelling method execution");
        System.out.println("Spell check complete.");
    }
}

public class TextEditor {
    private SpellChecker spellChecker;

    public TextEditor() {
        spellChecker = new SimpleSpellChecker();
    }

    public void spellCheck() {
        spellChecker.checkSpelling();
        // logging the end of method execution
        Logger.getLogger(TextEditor.class.getName()).info("Finished spellCheck method execution");
    }
}

And here’s how we can integrate logging into the IOC example using AOP:

public interface SpellChecker {
    void checkSpelling();
}

public class SimpleSpellChecker implements SpellChecker {
    @Override
    public void checkSpelling() {
        System.out.println("Inside checkSpelling method of SimpleSpellChecker");
    }
}

@Component
public class TextEditor {
    private SpellChecker spellChecker;

    @Autowired
    public TextEditor(SpellChecker spellChecker) {
        this.spellChecker = spellChecker;
    }

    public void spellCheck() {
        spellChecker.checkSpelling();
    }
}

@Aspect
@Component
public class LoggingAspect {
    @Before("execution(* com.example.*.*(..))")
    public void logBefore(JoinPoint joinPoint) {
        System.out.println("Starting method execution: " + joinPoint.getSignature().getName());
    }

    @After("execution(* com.example.*.*(..))")
    public void logAfter(JoinPoint joinPoint) {
        System.out.println("Finishing method execution: " + joinPoint.getSignature().getName());
    }
}

Conclusion-

In the example without IOC, we’re using the standard Java Logger class to log method execution at the beginning and end of the checkSpelling and spellCheck methods, respectively.

In the IOC example, we’re using Spring’s AOP features to apply the LoggingAspect to all methods in the com.example package, which logs method execution at the beginning and end of each method. This is done using the @Aspect annotation on the LoggingAspect class, and the @Before and @After annotations on the logBefore and logAfter methods, respectively. Additionally, we’re using the @Component and @Autowired annotations to inject the SpellChecker dependency into the TextEditor class.

AOP other features than logging-

  1. Caching: AOP can be used to cache frequently accessed data to improve performance and reduce the load on the database.
  2. Transactions: AOP can be used to apply transaction management to our application without modifying the core business logic.
  3. Exception handling: AOP can be used to handle exceptions in a consistent and centralized manner, instead of duplicating exception-handling code throughout the application.
  4. Performance monitoring: AOP can be used to monitor the performance of our application, such as by measuring the execution time of methods and identifying performance bottlenecks.
  5. Security: AOP can be used to apply security checks, such as authentication and authorization, to our application.
  6. Validation: AOP can be used to validate input data, such as by checking for null values or enforcing data format rules.

By using AOP to apply cross-cutting concerns to our application, we can keep our code modular, flexible, and easier to maintain, while also improving performance, security, and overall quality.

Advertisement

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.