Nier
Nier

Reputation: 658

How to create transaction in Spring Boot aop @Around function?

I want to implement an authorization method by using Spring Boot AOP. The original idea is, if the return object return from the REST calls didn't pass the authorization check, it will throw a unauthorized exception.

I'm doing it like this:

@Aspect
@Component
public class AuthAspect {
  @Around("AllRestExecPoint()")
  public Object auth(ProceedingJoinPoint point) throws Throwable {
    Object returnObject = point.proceed();
    if (!checkAuthorization(returnObject)) {
      throw new UnauthException();
    }
    return returnObject;
  }
}

However, the problem is, if this REST service will execute some INSERT or UPDATE to my DB, it will commit before my authorization checks. Therefore, the UnauthException will be thrown but the transaction is still committed.

The first try I want to manually create transaction before the proceed() call and commit it before return, but it failed.

@Aspect
@Component
public class AuthAspect {
  private final EntityManager em;

  @Autowired
  public AuthAspect(EntityManager em) {
    this.em = em;
  }

  @Around("AllRestExecPoint()")
  public Object auth(ProceedingJoinPoint point) throws Throwable {
    em.getTransaction().begin();

    Object returnObject = point.proceed();
    if (!checkAuthorization(returnObject)) {
      throw new UnauthException();
    }

    em.getTransaction().commit();

    return returnObject;
  }
}

It will cause the java.lang.IllegalStateException: Not allowed to create transaction on shared EntityManager - use Spring transactions or EJB CMT instead.

I searched on the internet and some answers need to modify web.xml file, but I don't want to use xml to do the configuration.

Upvotes: 3

Views: 2662

Answers (2)

M. Deinum
M. Deinum

Reputation: 125212

Judging from your tags you are using Spring Boot. Spring Boot provides a pre-configured TransactionTemplate which you can use if you manually want to control transactions.

Instead of the EntityManger inject this into your aspect and wrap your code in it.

@Aspect
@Component
public class AuthAspect {
  private final TransactionTemplate tx;

  public AuthAspect(TransactionTemplate tx) {
    this.tx = tx;
  }

  @Around("AllRestExecPoint()")
  public Object auth(ProceedingJoinPoint pjp) throws Throwable {

    return tx.execute(ts -> this.executeAuth(pjp));   
  }

  private Object executeAuth(ProceedingJoinPoint pjp) {
    Object returnObject;
    try {
      returnObject  = pjp.proceed();
    } catch (Throwable t) {
      throw new AopInvocationException(t.getMessage(), t);
    }
    if (!checkAuthorization(returnObject)) {
      throw new UnauthException();
    }
    return returnObject;
  }
}

This will execute the logic inside a transaction. I moved the actual logic to a method so that the lambda can be a single method instead of code block. (Personal preference/best practice).

Upvotes: 4

Jonathan JOhx
Jonathan JOhx

Reputation: 5978

Looking your code, you need to update a couple of things:

  1. If you want to call EntityManager, you need to call by following way, using in persistence context.

    @PersistenceContext
    private EntityManager em;
    
  2. In order to do queries or something in transaction context, you just need to add Transactional tag on method header and don't call getTransaction from EntityManager.

    @Transactional
    @Around("AllRestExecPoint()")
    public Object auth(ProceedingJoinPoint point) throws Throwable {
         em.createNativeQuery(query) //If it is necessary. 
         ...
    } 
    

Upvotes: 0

Related Questions