5

I want to use AOP to intercept all runtime exceptions thrown in service layer and rethrow as domain exceptions.

@Aspect
@Component
public class ExceptionWrapperInterceptor {

    @Pointcut("within(*.service.*)")
    public void onlyServiceClasses() {}

    @AfterThrowing(pointcut = "onlyServiceClasses()", throwing = "ex")
    public void intercept(DataAccessException ex) throws Exception {
        //throw DatabaseException
    }

    @AfterThrowing(pointcut = "onlyServiceClasses()", throwing = "ex")
    public void intercept(RuntimeException ex) throws Exception {
        //throw ServiceException
    }

}

The problem here is that, with a subclass of DataAccessException, the runtime execute the wrong method. There is an elegant solution to this?

Spring Version: 4.2.4.RELEASE

P.S. A single generic method (read from other questions) with a lot of instanceof is not elegant for me ;-)

Thanks Francesco

Francesco
  • 313
  • 3
  • 11

2 Answers2

4

How about using an @Around advice? You can simply use type-safe try-catch therein, no need to use any instanceof or reflection.

Here is some sample code which I compiled using AspectJ instead of Spring AOP because I am not a Spring user. The pointcut should be the same anyway.

Helper classes:

package de.scrum_master.service;

public class DatabaseException extends RuntimeException {
    public DatabaseException(Throwable arg0) {
        super(arg0);
    }
}
package de.scrum_master.service;

public class ServiceException extends RuntimeException {
    public ServiceException(Throwable arg0) {
        super(arg0);
    }
}

Driver application (plain Java, no need to use Spring):

package de.scrum_master.service;

import java.util.Random;
import org.springframework.jdbc.datasource.init.ScriptParseException;

public class Application {
    private static final Random RANDOM = new Random();

    public static void main(String[] args) {
        Application application = new Application();
        for (int i = 0; i < 10; i++) {
            try {
                application.doSomething();
            }
            catch (Exception e) {
                System.out.println(e);
            }
        }
    }

    public void doSomething() {
        switch (RANDOM.nextInt(3)) {
            case 1: throw new ScriptParseException("uh-oh", null);
            case 2: throw new IllegalArgumentException("WTF");
            default: System.out.println("doing something");
        }
    }
}

Aspect:

package de.scrum_master.aspect;

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.springframework.dao.DataAccessException;
import org.springframework.stereotype.Component;
import de.scrum_master.service.DatabaseException;
import de.scrum_master.service.ServiceException;

@Aspect
@Component
public class ExceptionWrapperInterceptor {
    @Pointcut("within(*..service..*) && execution(* *(..))")
    public void onlyServiceClasses() {}

    @Around("onlyServiceClasses()")
    public Object intercept(ProceedingJoinPoint thisJoinPoint) {
        try {
            return thisJoinPoint.proceed();
        }
        catch (DataAccessException dae) {
            throw new DatabaseException(dae);
        }
        catch (RuntimeException re) {
            throw new ServiceException(re);
        }
    }
}

Console log:

doing something
de.scrum_master.service.DatabaseException: org.springframework.jdbc.datasource.init.ScriptParseException: Failed to parse SQL script from resource [<unknown>]: uh-oh
doing something
de.scrum_master.service.DatabaseException: org.springframework.jdbc.datasource.init.ScriptParseException: Failed to parse SQL script from resource [<unknown>]: uh-oh
doing something
de.scrum_master.service.ServiceException: java.lang.IllegalArgumentException: WTF
de.scrum_master.service.ServiceException: java.lang.IllegalArgumentException: WTF
de.scrum_master.service.ServiceException: java.lang.IllegalArgumentException: WTF
de.scrum_master.service.ServiceException: java.lang.IllegalArgumentException: WTF
doing something
kriegaex
  • 63,017
  • 15
  • 111
  • 202
3

I believe, that your expectation is wrong (that only one intercept method will match similarly as for method overloading).

But while RuntimeException is parent of DataAccessException both methods are executed...

spring.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.2.xsd">

    <context:component-scan base-package="test" />

    <aop:aspectj-autoproxy />

</beans>

AopTest

package test;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class AopTest {

    public static void main(String[] args) {
        ApplicationContext ac = new ClassPathXmlApplicationContext("classpath:spring.xml");
        MyService ms = ac.getBean(MyService.class);
        try {
            ms.throw1();
        } catch (Exception e) {
//          e.printStackTrace();
        }
        try {
            ms.throw2();
        } catch (Exception e) {
//          e.printStackTrace();
        }
    }
}

MyAspect

package test;

import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.dao.DataAccessException;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class MyAspect {

    @AfterThrowing(pointcut = "execution(public * *(..))", throwing = "ex")
    public void intercept(DataAccessException ex) throws Exception {
        //throw DatabaseException
        System.out.println("DAE");
    }

    @AfterThrowing(pointcut = "execution(public * *(..))", throwing = "ex")
    public void intercept(RuntimeException ex) throws Exception {
        //throw ServiceException
        System.out.println("RE - " + ex.getClass());
    }

}

MyService

package test;

import org.springframework.dao.DataAccessException;
import org.springframework.stereotype.Service;

@Service
public class MyService {

    public void throw1() throws DataAccessException {
        throw new MyDataAccessException("test");
    }

    public void throw2()  {
        throw new NullPointerException();
    }

    static class MyDataAccessException extends DataAccessException {

        public MyDataAccessException(String msg) {
            super(msg);
        }

    }
}

and in log there is:

DAE
RE - class test.MyService$MyDataAccessException
RE - class java.lang.NullPointerException

Maven dependencies:

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>4.2.4.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aspects</artifactId>
        <version>4.2.4.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-tx</artifactId>
        <version>4.2.4.RELEASE</version>
    </dependency>

From Spring documentation:

When two pieces of advice defined in the same aspect both need to run at the same join point, the ordering is undefined (since there is no way to retrieve the declaration order via reflection for javac-compiled classes). Consider collapsing such advice methods into one advice method per join point in each aspect class, or refactor the pieces of advice into separate aspect classes - which can be ordered at the aspect level.

When I tried following modification of MyAspect:

package test;

import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.dao.DataAccessException;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class MyAspect {

    @AfterThrowing(pointcut = "execution(public * *(..))", throwing = "ex")
    public void intercept(DataAccessException ex) throws Exception {
        //throw DatabaseException
        System.out.println("DAE");
        throw new IllegalArgumentException("DAE"); // added
    }

    @AfterThrowing(pointcut = "execution(public * *(..))", throwing = "ex")
    public void intercept(RuntimeException ex) throws Exception {
        //throw ServiceException
        System.out.println("RE - " + ex.getClass());
        throw new IllegalArgumentException("RE"); // added
    }

}

log changed to:

DAE
RE - class java.lang.IllegalArgumentException
RE - class java.lang.NullPointerException

and when modified to Exception I got:

package test;

import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.dao.DataAccessException;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class MyAspect {

    @AfterThrowing(pointcut = "execution(public * *(..))", throwing = "ex")
    public void intercept(DataAccessException ex) throws Exception {
        //throw DatabaseException
        System.out.println("DAE");
        throw new Exception("DAE2"); // changed
    }

    @AfterThrowing(pointcut = "execution(public * *(..))", throwing = "ex")
    public void intercept(RuntimeException ex) throws Exception {
        //throw ServiceException
        System.out.println("RE - " + ex.getClass());
        throw new Exception("RE2"); // changed
    }

}

the log was

DAE
RE - class java.lang.NullPointerException

I believe, that solution to your "problem" is to have two Aspects instead of one and define the ordering:

package test;

import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.Ordered;
import org.springframework.dao.DataAccessException;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class DaeAspect implements Ordered {

    public int getOrder() {
        return 200;
    }

    @AfterThrowing(pointcut = "execution(public * *(..))", throwing = "ex")
    public void intercept(DataAccessException ex) throws Exception {
        //throw DatabaseException
        System.out.println("DAE");
        throw new IllegalAccessException("DAE2"); // based on my testing, this stops second aspect to apply
    }

}

and

package test;

import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.Ordered;
import org.springframework.dao.DataAccessException;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class ReAspect implements Ordered {

    public int getOrder() {
        return 100;
    }

    @AfterThrowing(pointcut = "execution(public * *(..))", throwing = "ex")
    public void intercept(RuntimeException ex) throws Exception {
        //throw ServiceException
        System.out.println("RE - " + ex.getClass());
        throw new IllegalAccessException("RE2");
    }

}
Betlista
  • 10,327
  • 13
  • 69
  • 110
  • I think that the flow here is different from mine. You simply log and the interceptor execute all. I, in the first method throw another exception that not match with the second method. – Francesco Jan 05 '16 at 15:40
  • As I wrote, you are not sharing important details. I tried to throw `IllegalArgumentException` but it was very similar, I'll mention this in answer... – Betlista Jan 05 '16 at 15:42