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:
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.