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