Spring AOP Fundamentals: Concepts and Practical Implementation

    1. Transaction-Enabled Transfer Example
  • 1.1 Implementation

  • Data Access Layer

  • Database Connection Utility

  • Transaction Management Utility

  • bean.xml Configuration

  • 1.2 Service Layer Transaction Control Issues

    1. Transfer Control Using Proxy Pattern
  • 2.1 Proxy Pattern Review

  • Interface-Based Dynamic Proxy

  • Class-Based Dynamic Proxy

  • 2.2 Refactoring Transfer Case with Dynamic Proxy

  • Proxy Factory Class

  • Transfer Service Class (Target)

  • Remaining Issues (AOP Motivation)

    1. AOP in Spring Framework
  • 3.1 AOP Core Concepts

  • 3.2 XML-Based AOP Configuration

  • 3.3 AOP Advice Types

  • 3.4 Annotation-Based AOP Configuration

  • Related Annotations

  • 3.5 Transfer Case with AOP Annotations

1-1 Implementation

Data Access Layer

package com.example.demo.dao.impl;

import com.example.demo.dao.IAccountDao;
import com.example.demo.domain.Account;
import com.example.demo.utils.ConnectionHolder;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;

import java.sql.SQLException;
import java.util.List;

public class AccountDao implements IAccountDao {
    private QueryRunner queryRunner;
    private ConnectionHolder connectionHolder;

    public void setConnectionHolder(ConnectionHolder connectionHolder) {
        this.connectionHolder = connectionHolder;
    }

    public void setQueryRunner(QueryRunner queryRunner) {
        this.queryRunner = queryRunner;
    }

    public List<Account> findAllAccounts() {
        try {
            return queryRunner.query(
                connectionHolder.getConnection(),
                "SELECT * FROM account",
                new BeanListHandler<>(Account.class)
            );
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return null;
    }

    public Account findAccountById(Integer id) {
        try {
            return queryRunner.query(
                connectionHolder.getConnection(),
                "SELECT * FROM account WHERE id = ?",
                new BeanHandler<>(Account.class),
                id
            );
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return null;
    }

    public void insertAccount(Account account) {
        try {
            queryRunner.update(
                connectionHolder.getConnection(),
                "INSERT INTO account(name, balance) VALUES(?, ?)",
                account.getName(),
                account.getBalance()
            );
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public void modifyAccount(Account account) {
        try {
            queryRunner.update(
                connectionHolder.getConnection(),
                "UPDATE account SET name = ?, balance = ? WHERE id = ?",
                account.getName(),
                account.getBalance(),
                account.getId()
            );
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public void removeAccount(Integer id) {
        try {
            queryRunner.update(
                connectionHolder.getConnection(),
                "DELETE FROM account WHERE id = ?",
                id
            );
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    @Override
    public Account findAccountByName(String name) {
        try {
            List<Account> accounts = queryRunner.query(
                connectionHolder.getConnection(),
                "SELECT * FROM account WHERE name = ?",
                new BeanListHandler<>(Account.class),
                name
            );
            if (accounts == null || accounts.isEmpty()) {
                return null;
            }
            if (accounts.size() > 1) {
                throw new RuntimeException("Multiple records found, data integrity issue");
            }
            return accounts.get(0);
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return null;
    }
}

Database Connection Utility

  • Connection Object: Thread isolation using ThreadLocal
package com.example.demo.utils;

import javax.sql.DataSource;
import java.sql.Connection;

/**
 * Manages database connections with thread-local storage for isolation.
 */
public class ConnectionHolder {
    private ThreadLocal<Connection> threadConnection = new ThreadLocal<>();
    private DataSource dataSource;

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public Connection getConnection() {
        Connection conn = threadConnection.get();
        try {
            if (conn == null) {
                conn = dataSource.getConnection();
                threadConnection.set(conn);
            }
            return conn;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public void cleanup() {
        threadConnection.remove();
    }
}

Transaction Management Utility

package com.example.demo.utils;

import java.sql.Connection;
import java.sql.SQLException;

/**
 * Transaction management utility providing:
 * 1) Begin transaction
 * 2) Commit transaction
 * 3) Rollback transaction
 * 4) Release connection
 */
public class TransactionManager {
    private ConnectionHolder connectionHolder;

    public void setConnectionHolder(ConnectionHolder holder) {
        this.connectionHolder = holder;
    }

    public void startTransaction() {
        try {
            connectionHolder.getConnection().setAutoCommit(false);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public void commitTransaction() {
        try {
            connectionHolder.getConnection().commit();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public void rollbackTransaction() {
        try {
            connectionHolder.getConnection().rollback();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public void closeConnection() {
        try {
            connectionHolder.getConnection().close();
            connectionHolder.cleanup();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

bean.xml Configuration

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">
    
    <context:component-scan base-package="com.example.demo"/>
    
    <bean id="accountService" class="com.example.demo.service.impl.AccountService">
        <property name="accountDao" ref="accountDao"/>
        <property name="transactionManager" ref="transactionManager"/>
    </bean>

    <bean id="accountDao" class="com.example.demo.dao.impl.AccountDao">
        <property name="queryRunner" ref="queryRunner"/>
        <property name="connectionHolder" ref="connectionHolder"/>
    </bean>

    <!-- QueryRunner as prototype scope for thread safety -->
    <bean id="queryRunner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype"/>
    
    <bean id="connectionHolder" class="com.example.demo.utils.ConnectionHolder">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="com.mysql.jdbc.Driver"/>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/demo"/>
        <property name="user" value="root"/>
        <property name="password" value="password"/>
    </bean>
    
    <bean id="transactionManager" class="com.example.demo.utils.TransactionManager">
        <property name="connectionHolder" ref="connectionHolder"/>
    </bean>
</beans>

1-2 Service Layer Transaction Control and Problems

package com.example.demo.service.impl;

import com.example.demo.dao.IAccountDao;
import com.example.demo.domain.Account;
import com.example.demo.service.IAccountService;
import com.example.demo.utils.TransactionManager;

import java.util.List;

public class AccountService implements IAccountService {
    private IAccountDao accountDao;
    private TransactionManager transactionManager;

    public void setAccountDao(IAccountDao accountDao) {
        this.accountDao = accountDao;
    }

    public void setTransactionManager(TransactionManager transactionManager) {
        this.transactionManager = transactionManager;
    }

    public List<Account> findAllAccounts() {
        try {
            transactionManager.startTransaction();
            List<Account> accounts = accountDao.findAllAccounts();
            transactionManager.commitTransaction();
            return accounts;
        } catch (Exception e) {
            transactionManager.rollbackTransaction();
        } finally {
            transactionManager.closeConnection();
        }
        return null;
    }

    public Account findAccountById(Integer id) {
        try {
            transactionManager.startTransaction();
            Account account = accountDao.findAccountById(id);
            transactionManager.commitTransaction();
            return account;
        } catch (Exception e) {
            transactionManager.rollbackTransaction();
        } finally {
            transactionManager.closeConnection();
        }
        return null;
    }

    public void insertAccount(Account account) {
        try {
            transactionManager.startTransaction();
            accountDao.insertAccount(account);
            transactionManager.commitTransaction();
        } catch (Exception e) {
            transactionManager.rollbackTransaction();
        } finally {
            transactionManager.closeConnection();
        }
    }

    public void modifyAccount(Account account) {
        try {
            transactionManager.startTransaction();
            accountDao.modifyAccount(account);
            transactionManager.commitTransaction();
        } catch (Exception e) {
            transactionManager.rollbackTransaction();
        } finally {
            transactionManager.closeConnection();
        }
    }

    public void removeAccount(Integer id) {
        try {
            transactionManager.startTransaction();
            accountDao.removeAccount(id);
            transactionManager.commitTransaction();
        } catch (Exception e) {
            transactionManager.rollbackTransaction();
        } finally {
            transactionManager.closeConnection();
        }
    }

    @Override
    public void transfer(String sourceName, String targetName, float amount) {
        try {
            transactionManager.startTransaction();
            
            Account source = accountDao.findAccountByName(sourceName);
            Account target = accountDao.findAccountByName(targetName);
            
            source.setBalance(source.getBalance() - amount);
            target.setBalance(target.getBalance() + amount);
            
            accountDao.modifyAccount(source);
            accountDao.modifyAccount(target);
            
            transactionManager.commitTransaction();
        } catch (Exception e) {
            e.printStackTrace();
            transactionManager.rollbackTransaction();
        } finally {
            transactionManager.closeConnection();
        }
    }
}

Identified Problems

Problem 1: Service methods are tightly coupled to the TransactionManager API.
          Changing TransactionManager method names requires updating all service methods.
Problem 2: Significant code duplication in service layer for transaction handling.

Dynamic Proxy Benefits

1) No proliferation of proxy classes when adding target classes
2) Interface modifications don't affect proxy classes

  • Problem 1 can be solved using Dynamic Proxy benefit #2
  • Problem 2 is addressed by the Proxy Pattern, which externalizes transaction control
  1. Transfer Control Using Proxy Pattern =======================================

2-1 Proxy Pattern Review

Dynamic Proxy Characteristics: Proxy class bytecode is created on-demand, loaded dynamically without modifying source code.

Types:

1) Interface-based dynamic proxy
2) Class-based (CGLIB) dynamic proxy

2-1-1 Interface-Based Dynamic Proxy

Required Class: JDK's Proxy class

Creation Method: Proxy.newProxyInstance()

Requirement: Target class must implement at least one interface

Method Signature:

public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)

Parameters:

ClassLoader: Loads proxy class bytecode, use target's class loader
Class[]: Bytecode array for methods, proxy and target share same interface
InvocationHandler: Enhancement logic, defines how proxy methods behave

InvocationHandler Method:

Object invoke(Object proxy, Method method, Object[] args)

invoke Parameters:

proxy: Proxy instance reference
method: Currently executing method
args: Method arguments
Return value: Same as target method's return value

Example:

package com.example.demo.proxy;

public interface IProducer {
    void sellProduct(float price);
    void afterSalesService(float price);
}

package com.example.demo.proxy;

public class Producer implements IProducer {
    @Override
    public void sellProduct(float price) {
        System.out.println("Product sold, revenue: " + price);
    }

    @Override
    public void afterSalesService(float price) {
        System.out.println("After-sales service, revenue: " + price);
    }
}

package com.example.demo.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class Client {
    public static void main(String[] args) {
        final Producer producer = new Producer();
        
        IProducer proxyInstance = (IProducer) Proxy.newProxyInstance(
            producer.getClass().getClassLoader(),
            producer.getClass().getInterfaces(),
            new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    Object result = null;
                    Float price = (Float) args[0];
                    
                    if ("sellProduct".equals(method.getName())) {
                        result = method.invoke(producer, price * 0.8f);
                    }
                    return result;
                }
            }
        );
        
        proxyInstance.sellProduct(12000f);
    }
}

Output:

Product sold, revenue: 9600.0

2-1-2 Class-Based Dynamic Proxy

Required Class: CGLIB's Enhancer

Creation Method: Enhancer.create()

Requirement: Target class must be non-final (extendable)

Method Parameters:

class: Target class bytecode
Callback: Enhancement logic, implements MethodInterceptor

intercept Parameters:

intercept(Object obj, Method method, Object[] args, MethodProxy proxy)
obj: Proxy instance reference
method: Currently executing method
args: Method arguments
proxy: Method proxy for invocation

Example:

package com.example.demo.cglib;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

public class Client {
    public static void main(String[] args) {
        final Producer producer = new Producer();
        
        Producer proxyInstance = (Producer) Enhancer.create(
            producer.getClass(),
            new MethodInterceptor() {
                @Override
                public Object intercept(Object obj, Method method, Object[] args, 
                        MethodProxy proxy) throws Throwable {
                    Object result = null;
                    Float price = (Float) args[0];
                    
                    if ("sellProduct".equals(method.getName())) {
                        result = method.invoke(producer, price * 0.8f);
                    }
                    return result;
                }
            }
        );
        
        proxyInstance.sellProduct(12000f);
    }
}

2-2 Refactoring Transfer Case with Dynamic Proxy

Proxy Factory Class

package com.example.demo.factory;

import com.example.demo.domain.Account;
import com.example.demo.service.IAccountService;
import com.example.demo.utils.TransactionManager;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.List;

/**
 * Factory producing proxied AccountService instances with transaction handling.
 */
public class ServiceProxyFactory {
    private IAccountService targetService;
    private TransactionManager transactionManager;

    public void setTargetService(IAccountService service) {
        this.targetService = service;
    }

    public void setTransactionManager(TransactionManager manager) {
        this.transactionManager = manager;
    }

    public IAccountService createProxy() {
        return (IAccountService) Proxy.newProxyInstance(
            targetService.getClass().getClassLoader(),
            targetService.getClass().getInterfaces(),
            new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    Object result = null;
                    try {
                        transactionManager.startTransaction();
                        result = method.invoke(targetService, args);
                        transactionManager.commitTransaction();
                        return result;
                    } catch (Exception e) {
                        transactionManager.rollbackTransaction();
                    } finally {
                        transactionManager.closeConnection();
                    }
                    return null;
                }
            }
        );
    }
}

Transfer Service Class (Target)

package com.example.demo.service.impl;

import com.example.demo.dao.IAccountDao;
import com.example.demo.domain.Account;
import com.example.demo.service.IAccountService;

import java.util.List;

public class AccountService implements IAccountService {
    private IAccountDao accountDao;

    public void setAccountDao(IAccountDao accountDao) {
        this.accountDao = accountDao;
    }

    public List<Account> findAllAccounts() {
        return accountDao.findAllAccounts();
    }

    public Account findAccountById(Integer id) {
        return accountDao.findAccountById(id);
    }

    public void insertAccount(Account account) {
        accountDao.insertAccount(account);
    }

    public void modifyAccount(Account account) {
        accountDao.modifyAccount(account);
    }

    public void removeAccount(Integer id) {
        accountDao.removeAccount(id);
    }

    @Override
    public void transfer(String sourceName, String targetName, float amount) {
        Account source = accountDao.findAccountByName(sourceName);
        Account target = accountDao.findAccountByName(targetName);
        
        source.setBalance(source.getBalance() - amount);
        target.setBalance(target.getBalance() + amount);
        
        accountDao.modifyAccount(source);
        accountDao.modifyAccount(target);
    }
}

bean.xml Updated Configuration

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">
    
    <context:component-scan base-package="com.example.demo"/>
    
    <bean id="accountService" class="com.example.demo.service.impl.AccountService">
        <property name="accountDao" ref="accountDao"/>
    </bean>

    <bean id="accountDao" class="com.example.demo.dao.impl.AccountDao">
        <property name="queryRunner" ref="queryRunner"/>
        <property name="connectionHolder" ref="connectionHolder"/>
    </bean>

    <bean id="serviceProxyFactory" class="com.example.demo.factory.ServiceProxyFactory">
        <property name="transactionManager" ref="transactionManager"/>
        <property name="targetService" ref="accountService"/>
    </bean>

    <bean id="proxiedAccountService" factory-bean="serviceProxyFactory" factory-method="createProxy"/>

    <bean id="queryRunner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype"/>
    <bean id="connectionHolder" class="com.example.demo.utils.ConnectionHolder">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="com.mysql.jdbc.Driver"/>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/demo"/>
        <property name="user" value="root"/>
        <property name="password" value="password"/>
    </bean>
    
    <bean id="transactionManager" class="com.example.demo.utils.TransactionManager">
        <property name="connectionHolder" ref="connectionHolder"/>
    </bean>
</beans>

Test Class

package com.example.demo.test;

import com.example.demo.domain.Account;
import com.example.demo.service.IAccountService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import java.util.List;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:bean.xml")
public class AccountServiceTest {
    
    @Autowired
    @Qualifier("proxiedAccountService")
    private IAccountService accountService;

    @Test
    public void testFindAll() {
        List<Account> accounts = accountService.findAllAccounts();
        for (Account account : accounts) {
            System.out.println(account);
        }
    }

    @Test
    public void testTransfer() {
        accountService.transfer("alice", "bob", 100);
    }
}

Remaining Issue (AOP Motivation)

While dynamic proxy solves the problems, complex XML configuration is still required
for proxy classes. Spring AOP simplifies this configuration significantly.

  1. AOP in Spring Framework ==========================

3-1 AOP Core Concepts

Definition: Aspect-Oriented Programming (AOP) separates cross-cutting concerns by extracting repeated code and applying it via dynamic proxy without modifying source code.

Mechenism: Pre-compilation or runtime dynamic proxy to enhance methods without changing implementation

Benefits:

- Eliminates code duplication
- Improves development efficiency
- Easier maintenance (focus only on enhancement logic)

AOP Terminology:

Joinpoint: Methods in a class that can be enhanced

Pointcut: Definition of wich Joinpoints to intercept

Advice: Actions to perform when intercepting Joinpoints. Types include:

- Before advice
- After returning advice
- After throwing advice
- After (finally) advice
- Around advice

Aspect: Combination of Pointcuts and Advice

Introduction: Special advice to dynamically add methods or fields to classes

Target: The object being proxied

Weaving: Process of applying advice to target

Proxy: Resulting class after weaving

3-2 XML-Based AOP Configuration

Requirement: Add logging before service method execution

Steps:

1) Register advice bean with Spring
2) Use aop:config to begin AOP configuration
3) Use aop:aspect to define aspect
4) Configure advice types with pointcut expressions

Pointcut Expression Syntax:

execution([modifiers] returnType [classPath].methodName(parameters))

Examples:

execution(public void com.example.demo.service.impl.AccountService.saveAccount())
execution(* com.example.demo.service.impl.*.*(..))  // Wildcards for any modifiers, return, methods

Logger Advice Class:

package com.example.demo.logger;

public class Logger {
    public void logBefore() {
        System.out.println("Before advice: Logging started");
    }
}

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">
    
    <bean id="accountService" class="com.example.demo.service.impl.AccountService"/>
    <bean id="logger" class="com.example.demo.logger.Logger"/>
    
    <aop:config>
        <aop:aspect id="logAspect" ref="logger">
            <aop:before method="logBefore" pointcut="execution(* com.example.demo.service.impl.*.*(..))"/>
        </aop:aspect>
    </aop:config>
</beans>

package com.example.demo.aop;

import com.example.demo.service.IAccountService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class AopDemo {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
        IAccountService service = (IAccountService) context.getBean("accountService");
        service.saveAccount();
    }
}

Output:

Before advice: Logging started
Account saved

3-3 AOP Advice Types

try {
    try {
        // @Before
        method.invoke();
    } finally {
        // @After
    }
    // @AfterReturning
} catch() {
    // @AfterThrowing
}

Advice Type Characteristics
Before Executes before target method
After Returning Executes after successful completion
After Throwing Executes when exception occurs
After (Finally) Always executes
Around Full control over execution timing

Around Advice: Requires ProceedingJoinPoint.proceed() to execute target method.

package com.example.demo.logger;

import org.aspectj.lang.ProceedingJoinPoint;

public class Logger {
    public Object aroundAdvice(ProceedingJoinPoint pjp) {
        Object result = null;
        try {
            System.out.println("Before");
            result = pjp.proceed();
            System.out.println("After Returning");
            return result;
        } catch (Throwable t) {
            System.out.println("After Throwing");
            throw new RuntimeException(t);
        } finally {
            System.out.println("After (Finally)");
        }
    }
}

<aop:config>
    <aop:pointcut id="pt1" expression="execution(* com.example.demo.service.impl.*.*(..))"/>
    <aop:aspect id="logAspect" ref="logger">
        <aop:around method="aroundAdvice" pointcut-ref="pt1"/>
    </aop:aspect>
</aop:config>

3-4 Annotation-Based AOP Configuration

Related Annotations

Annotation Purpose
@Aspect Declares class as aspect
@Pointcut Defines pointcut expression
@Before Before advice
@AfterReturning After successful return
@AfterThrowing After exception
@After Finally advice
@Around Around advice
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">
    
    <context:component-scan base-package="com.example.demo"/>
    <aop:aspectj-autoproxy/>
</beans>

package com.example.demo.logger;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class Logger {
    @Pointcut("execution(* com.example.demo.service.impl.*.*(..))")
    private void pointcut() {}

    @Around("pointcut()")
    public Object aroundAdvice(ProceedingJoinPoint pjp) {
        Object result = null;
        try {
            System.out.println("Before");
            result = pjp.proceed();
            System.out.println("After Returning");
            return result;
        } catch (Throwable t) {
            System.out.println("After Throwing");
            throw new RuntimeException(t);
        } finally {
            System.out.println("After (Finally)");
        }
    }
}

Note: With annotation-based advice, @After and @AfterReturning execution order may vary. Around advice provides precise control over execution sequence.

3-5 Transfer Case with AOP Annotations

package com.example.demo.factory;

import com.example.demo.utils.TransactionManager;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class TransactionAspect {
    @Autowired
    private TransactionManager transactionManager;

    @Pointcut("execution(* com.example.demo.service.impl.*.*(..))")
    private void serviceMethods() {}

    @Around("serviceMethods()")
    public Object manageTransaction(ProceedingJoinPoint pjp) {
        Object result;
        try {
            System.out.println("Starting transaction");
            transactionManager.startTransaction();
            result = pjp.proceed();
            transactionManager.commitTransaction();
            System.out.println("Committing transaction");
            return result;
        } catch (Throwable t) {
            transactionManager.rollbackTransaction();
            throw new RuntimeException(t);
        } finally {
            transactionManager.closeConnection();
            System.out.println("Connection released");
        }
    }
}

Summary: AOP annotations enable declarative transaction management by enhancing service layer operations without modifying business logic code.

Tags: Spring aop java proxy-pattern dynamic-proxy

Posted on Thu, 15 Oct 2026 16:47:53 +0000 by system_critical