Skip to content
Advertisement

Not able to rollback DB changes in Aspect in Spring boot application

I have written one aspect around a service class. In the aspect, I am doing some operation in the before section, which I would like to be rolled back if some exception occurs in the enclosed service method.

The service class is as follows:

@Service
@Transactional
class ServiceA {
   ...
   public void doSomething() {
      ...
   }
   ...
}

The aspect is as follows:

  @Aspect
  @Order(2)
  public class TcStateManagementAspect {
  
  ...

  @Around(value = "applicationServicePointcut()", argNames = "joinPoint")
  public Object process(ProceedingJoinPoint joinPoint)
      throws Throwable {
    ...
    */Before section */

    do some processing and persist in DB
    ...
    Object object = joinPoint.proceed();
    ...
    do some post-processing
  }
}

I am seeing an exception in the service method is not rolling back the DB operation in the Begin Section. I tried putting @Transactional on @Around, but it did not help.

In this context, I have gone through the following posts:

  1. Spring @Transactional in an Aspect (AOP)
  2. Custom Spring AOP Around + @Transactional

But I am not able to get any concrete idea regarding how to achieve this. Could anyone please help here? Thanks.

Advertisement

Answer

Like I said in my comment, what your around advice does must be declared transactional too. You cannot do that directly, because @Transactional internally uses Spring AOP via dynamic proxies. However, Spring AOP aspects cannot be the target of other aspects. But you can simply create a new helper @Component which you delegate your advice’s action to.

Let us assume that the goal is to log the arguments of the @Transactional method targeted by your aspect. Then simply do this:

package com.example.managingtransactions;

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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class TxLogAspect {
  private final static Logger logger = LoggerFactory.getLogger(TxLogAspect.class);

  @Autowired
  TxLogService txLogService;

  @Pointcut(
    "@annotation(org.springframework.transaction.annotation.Transactional) && " +
    "!within(com.example.managingtransactions.TxLogService)"
  )
  public void applicationServicePointcut() {}

  @Around("applicationServicePointcut()")
  public Object process(ProceedingJoinPoint joinPoint) throws Throwable {
    logger.info(joinPoint.toString());
    // Delegate to helper component in order to be able to use @Transactional
    return txLogService.logToDB(joinPoint);
  }
}
package com.example.managingtransactions;

import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.util.Arrays;
import java.util.List;

/**
 * Helper component to delegate aspect advice execution to in order to make the
 * advice transactional.
 * <p>
 * Aspect methods themselves cannot be @Transactional, because Spring AOP aspects
 * cannot be targeted by other aspects. Delegation is a simple and elegant
 * workaround.
 */
@Component
public class TxLogService {
  @Autowired
  private JdbcTemplate jdbcTemplate;

  @Transactional
  public Object logToDB(ProceedingJoinPoint joinPoint) throws Throwable {
    jdbcTemplate.update(
      "insert into TX_LOG(MESSAGE) values (?)",
      Arrays.deepToString(joinPoint.getArgs())
    );
    return joinPoint.proceed();
  }

  public List<String> findAllTxLogs() {
    return jdbcTemplate.query(
      "select MESSAGE from TX_LOG",
      (rs, rowNum) -> rs.getString("MESSAGE")
    );
  }
}

See? We are passing through the joinpoint instance to the helper component’s own @Transactional method, which means that the transaction is started when entering that method and committed or rolled back depending on the result of joinPoint.proceed(). I.e. what the aspect helper writes to the DB itself will also be rolled back if something goes wrong in the aspect’s target method.

BTW, because I never used Spring transactions before, I simply took the example from https://spring.io/guides/gs/managing-transactions/ and added the two classes above. Before, I also added this to schema.sql:

create table TX_LOG(ID serial, MESSAGE varchar(255) NOT NULL);

Next, I added made sure that TxLogService is injected into AppRunner:

  private final BookingService bookingService;
  private final TxLogService txLogService;

  public AppRunner(BookingService bookingService, TxLogService txLogger) {
    this.bookingService = bookingService;
    this.txLogService = txLogger;
  }

If then at the end of AppRunner.run(String...) you add these two statements

    logger.info("BOOKINGS: " + bookingService.findAllBookings().toString());
    logger.info("TX_LOGS: " + txLogService.findAllTxLogs().toString());

you should see something like this at the end of the console log:

c.e.managingtransactions.AppRunner       : BOOKINGS: [Alice, Bob, Carol]
c.e.managingtransactions.AppRunner       : TX_LOGS: [[[Alice, Bob, Carol]]]

I.e. you see that only for the successful booking transaction a log message something was written to the DB, not for the two failed ones.

User contributions licensed under: CC BY-SA
9 People found this is helpful
Advertisement