Can not set advice for the methods of the same aspect class

Viewed 310

I am trying to learn SpringAOP with AspectJ, by building a small bank transaction simulation. But I am unable to add advice (@Before, @After, @AfterThrowing) to the methods of the aspect class itself.

This is the model

Bank.java

@Component
public class Bank {
    private int balance;
    private int pinCode;
    private int tempPin;

    public int getBalance() {
        return balance;
    }

    @Value("10000")
    public void setBalance(int balance) {
        this.balance = balance;
    }

    public int getPinCode() {
        return pinCode;
    }

    @Value("6789")
    public void setPinCode(int pinCode) {
        this.pinCode = pinCode;
    }

    public int getTempPin() {
        return tempPin;
    }

    public void setTempPin(int tempPin) {
        this.tempPin = tempPin;
    }

    public void withDraw(int amount) {
        if (amount <= balance) {
            balance -= amount;
            System.out.println("Successful Withdraw");
        } else {
            System.out.println("Insufficient Balance");
        }
    }
}

This is the aspect class

BankAspect.java

@Component
@Aspect
public class BankAspect {
    private Bank bank;

    public Bank getBank() {
        return bank;
    }

    @Autowired
    public void setBank(Bank bank) {
        this.bank = bank;
    }

    @Before("execution(public void dev.ritam.model.Bank.withDraw(..))")
    public void validatePin() {
        if (bank.getPinCode() != bank.getTempPin()) {
            throw new RuntimeException();
        }
    }

    @AfterThrowing("execution(public void dev.ritam.aspect.BankAspect.validatePin(..))")
    public void logException() {
        System.out.println("Wrong Pin");
    }
}

This is the configuration class

AppConfig.java

@Configuration
@EnableAspectJAutoProxy
@ComponentScan("dev.ritam")
public class AppConfig {
    @Bean
    Bank bank() {
        return new Bank();
    }

    @Bean
    BankAspect bankAspect() {
        return new BankAspect();
    }
}

This is the main method

App.java

public class App {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        Bank bank = context.getBean(Bank.class);

        try {
            bank.setTempPin(1234);
            bank.withDraw(1000000);
        } catch (Exception ignore) {
        }
    }
}

Only validatePin() @Before advice is getting executed. I am suppose to get 'Wrong Pin' as output, but the @AfterThrowing advice is not being recognized.

2 Answers

The method public void validatePin() (in the class BankAspect) acts as an advice; therefore, if you were using AspectJ alone you could not intercepted it in the way you where doing (e.g., you would need to use the pointcut adviceexecution). In the case of Spring AOP things are even more strict; Spring AOP does not allow aspects to be the targets of advices.

To solve your problem you can intercept the same method that is being intercepted by before pointcut, namely:

@AfterThrowing("execution(public void dev.ritam.model.Bank.withDraw(..))")
public void logException() {
    System.out.println("Wrong Pin");
}

since the exception will be thrown within the scope of the method Bank.withDraw your @AfterThrowing pointcut will be able to catch that exception.

For AspectJ only

A straightforward solution, would be to wrap this code:

  if (bank.getPinCode() != bank.getTempPin()) {
        throw new RuntimeException();
    }

into a method, for instance:

public void check_pin_code() {
    if (bank.getPinCode() != bank.getTempPin()) {
        throw new RuntimeException();
    }
}

now intercept that method instead:

@AfterThrowing("execution(public void dev.ritam.aspect.BankAspect.check_pin_code(..))")
public void logException() {
    System.out.println("Wrong Pin");
}

Another alternative approach, but that one has to be careful using, would be to use adviceexecution()

@AfterThrowing("within(BankAspect) && adviceexecution()")
public void logException() {
    System.out.println("Wrong Pin");
}

This will intercept all the advice executions that have happened within the BankAspect and that resulted in throwing an Exception.

From the reference documentation :

Advising aspects with other aspects? In Spring AOP, aspects themselves cannot be the targets of advice from other aspects. The @Aspect annotation on a class marks it as an aspect and, hence, excludes it from auto-proxying.

You cannot advice an aspect in Spring AOP and that is the reason for @AfterThrowing not executing.

To throw the exception and to log the message , you can advice the same method Bank.withDraw() with @AfterThrowing. That way, the validatePin() advice will get executed before Bank.withDraw() and logException() advice will get executed if an exception was thrown from validation. This is the second option mentioned in the answer from @dreamcash.

Example code

@Before("execution(public void dev.ritam.model.Bank.withDraw(..))")
public void validatePin() {
    if (bank.getPinCode() != bank.getTempPin()) {
        throw new RuntimeException();
    }
}

@AfterThrowing("execution(public void dev.ritam.model.Bank.withDraw(..))")
public void logException() {
    System.out.println("Wrong Pin");
}

This would result in the following sequence of console output

Wrong Pin
java.lang.RuntimeException

Couple of points to consider.

Bank and BankAspect are annotated with @Component and @ComponentScan would register them as beans. Which means beans registered with @Bean in AppConfig is not required. Either of the two is required.

The last option mentioned in @dreamcrash's answer is not supported through Spring AOP : Refer section starting with Other pointcut types

Update:

This was tricky one , the answer I suggested was based on Spring Boot Version : 2.2.6.RELEASE which uses spring libraries from version : 5.2.5.RELEASE

The code shared by OP is based on Spring Version : 5.3.1 and the advices are not executing as expected.

The Spring AOP advices in the code shared to work as expected, the spring version should be <= 5.2.6.RELEASE

Please update the the pom.xml entry from

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-context</artifactId>
  <version>5.3.1</version>
</dependency>

to

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-context</artifactId>
  <version>5.2.6.RELEASE</version>
</dependency>

For quick reference, following is OP's code that is not working with version 5.3.1

@After("execution(public void dev.ritam.model.Bank.setTempPin(..))")
public void validatePin() {
    if (bank.getPinCode() != bank.getTempPin()) {
        throw new RuntimeException("Wrong Pin");
    } else {
        System.out.println("Correct Pin");
    }
}

@AfterThrowing(value = "execution(public void dev.ritam.model.Bank.setTempPin(..))", throwing = "e")
public void logException(Exception e) {
    System.out.println(e.getMessage());
}

Not sure if this change in behaviour is documented or not or this is a bug. Any further information on this will be updated if I find any.

Info:

This appears to be a framework issue . More details here:

https://github.com/spring-projects/spring-framework/issues/26202

Relevant bits from the reference documentation ,

As of Spring Framework 5.2.7, advice methods defined in the same @Aspect class that need to run at the same join point are assigned precedence based on their advice type in the following order, from highest to lowest precedence: @Around, @Before, @After, @AfterReturning, @AfterThrowing. Note, however, that due to the implementation style in Spring’s AspectJAfterAdvice, an @After advice method will effectively be invoked after any @AfterReturning or @AfterThrowing advice methods in the same aspect.

Related