Spring MVC: bind an exception handler to particular method
Asked Answered
K

9

19

Good day!

I have a @Controller. Some of its methods throw the same exception, but I want to handle this exceptions in different way.

Is there a way how to bind an @ExceptionHandler to a particular method?

Krenn answered 8/7, 2013 at 9:41 Comment(4)
Why don't you handle the exception in the methods then?Highkey
It is DataIntegrityViolationException and as I understand it throws by hibernate iterceptor, i.e. actually it throws not inside my method's body. I tried try{}catch(Exception ex){} and catch no exception. But exception hadler handles it nicely.Krenn
I see. There is no way to bind an exception handler to a method. You could only pass in the request as parameter, read the path and then decide what to do.Highkey
6 years later, no work on this feature, I think this feature is necessary for clean code.Archiepiscopal
A
5

You need to use AOP tools like CDI Interceptor or AspectJ to achieve this cross-cutting concerns. A Concern is a term that refers to a part of the system divided on the basis of the functionality.

Basically this type of feature is used to handle logging, security and also handling the errors... which are not part of your business logic...

Like if you want to change the logger for application from log4j to sl4j then you need to go through each and every classes where you have used log4j and change it. But if you have used AOP tools then you only need to go the interceptor class and change the implementation. Something like plug and play and very powerful tool.

Here is a code snippet using JavaEE CDI Interceptor

/*
    Creating the interceptor binding
*/
@InterceptorBinding
@Retention(RUNTIME)
@Target({TYPE, METHOD})
public @interface BindException {

}

After we have define interceptor binding we need to define interceptor binding implementation

/*
    Creating the interceptor implementation
*/
@Interceptor
@BindException
public class ExceptionCDIInterceptor {

    @AroundInvoke
    public Object methodInterceptor(InvocationContext ctx) throws Exception {
        System.out.println("Invoked method " + ctx.getMethod().getName());
        try {
            return ctx.proceed(); // this line will try to execute your method
                                 // and if the method throw the exception it will be caught  
        } catch (Exception ex) {
            // here you can check for your expected exception 
            // code for Exception handler
        }
    }

}

Now we only need to apply interceptor to our method

/*
    Some Service class where you want to implement the interceptor
*/
@ApplicationScoped
public class Service {

    // adding annotation to thisMethodIsBound method to intercept
    @BindException
    public String thisMethodIsBound(String uid) {
        // codes....

        // if this block throw some exception then it will be handled by try catch block
        // from ExceptionCDIInterceptor
    }
}

You can achieve same feature using AspectJ also.

/*
    Creating the Aspect implementation
*/
@Aspect
public class  ExceptionAspectInterceptor {

    @Around("execution(* com.package.name.SomeService.thisMethodIsBound.*(..))")
    public Object methodInterceptor(ProceedingJoinPoint ctx) throws Throwable {
        System.out.println("Invoked method " + ctx.getSignature().getName());
        try {
            return ctx.proceed(); // this line will try to execute your method
                                 // and if the method throw the exception it will be caught  
        } catch (Exception ex) {
            // here you can check for your expected exception 
            // codes for Exception handler
        }
    }
}

Now we only need to enable the AspectJ to our application config

/*
    Enable the AspectJ in your application
*/
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {

    @Bean
    public SomeService SomeService() {
        return new SomeService();
    }

}

/*
    Some Service class where you want to implement the Aspect
*/
package com.package.name;
public class SomeService {

    public String thisMethodIsBound(String uid) {
        // codes....

        // if this block throw some exception then it will be handled by try catch block
        // from ExceptionAspectInterceptor
    }
}

I have code example in my git repo https://github.com/prameshbhattarai/javaee-exceptionBinding by using CDI interceptor.

Antagonistic answered 19/1, 2019 at 11:20 Comment(0)
R
4

Just as an option (obviously, it's not ideal): you can wrap the exception into a custom exception in one of your methods and then catch it in an @ExceptionHandler

void boo() throws WrappingException {
    try {

    } catch (TargetException e) {
        throw new WrappingException(e);
    }
}

then

@ExceptionHandler(WrappingException.class)
public void handleWrappingException() {
    // handle
}

@ExceptionHandler(TargetException.class)
public void handleTargetException() {
    // handle
}
Redfaced answered 17/1, 2019 at 8:14 Comment(2)
Wrapping is cumbersomeArchiepiscopal
This is a good solution when the exception is thrown by the logic inside the method. But what if we need to handle exception that happen at the binding stage (thrown by the framework)? This is actually my case. I need a particular treatment for one method.Ishtar
B
2

Could you please explain why do you need this? I'm asking out of curiosity, because I've never felt like this is required and here is why:

Exception usually represents a very specific "mistake" - something that went wrong in a very specific way. Basically, exception represents a mistake, not a flow...

There are two "degrees of freedom" that spring can support out of the box:

  1. Exception parameters. Maybe stuff like error code, which can be declared as a data field of exception itself.

  2. Exception inheritance. Example:

If you have in your system a UserDoesNotExistException and you want to be more specific in a case of say, the system that manages the users that are retired in some flows, you can always create a more specific exception:

class UserRetiredException extends UserDoesNotExistException {...}

Obviously, spring can support both the cases: In ExceptionMapper you have the access to the exception anyway so you can do something like:

handleException(SomeExceptionWithErrorCode ex) {
   if(ex.getErrorCode() == "A") {
      // do this
   }
   else if(ex.getErrroCode() == "B") {
      // do that
   }
}

In the second case you just have different exception mappers for the different types of exceptions.

You can also consider @ControllerAdvice annotation to reuse code or something.

Barbarism answered 17/1, 2019 at 8:7 Comment(8)
Lets say I have 2 APIs in the same legacy controller(which I cannot change due to different design constraints), one is transactional and one is not. If I get A Hibernate/Hikari Exception due to underlying database issue, I need to send different responses to the client, (PENDING in one api and Failure in the other).Archiepiscopal
Hmmm, controllers usually don't work with a DB. So the exception should be generated on service/dao layer. Can you intercept and rethrow more specific exception on service?Barbarism
That would mean more of unnecessary try catches throughout the code , where ever I am having DB calls, which can be more than 10 per APiArchiepiscopal
I think the exception handling could be externalized into some utility class in this case, so that it would deal with multiple translations in one place (which you have to do somewhere) and won't pollute the code of DAO/Service.Barbarism
Another technique that comes to my my mind is AOP You could handle the exceptions in Advice. well it kind of how the spring exception mapping works under the hood, but maybe you could take advantage of some manual configuration (using @AfterThrowing annotation of AOP)Barbarism
So what you are saying is that there is no spring feature that supports this, after all these years. Of course I can do this on my own, Creating an advice named "@MethodAdvice", just like "@ControllerAdvice", and then implement ordering on both, but I find it too cumbersome, and maybe error prone, Can I guarantee ordering?Archiepiscopal
I don't think there is a change here... You could place method advice on Service/DAO which natually happens before the exception hits controller layer, after all, Contoller advice is to adjust a response in a WebMVC/Rest right before passing it back to clientBarbarism
In my case, users are bypassing a view by using direct URL they enter the userId into. This causes massive errors in our logs (apparently lots of users do this!) So I want to handle these differently, than if the exception were to occur any other way (Any other way would be a real issue that I cant see happening.)Sklar
E
1

I don't think you can specify a specific @ExceptionHandler for a method, but you can bind an @ExceptionHandler method to a specific Exception.

So if you want to handle all DataIntegrityViolationException one way and all other Exceptions in another you should be able to achieve that with something like this:

@ExceptionHandler(DataIntegrityViolationException.class)
public void handleIntegrityViolation() {
    // do stuff for integrity violation here
}

@ExceptionHandler(Exception.class)
public void handleEverythingElse() {
    // do stuff for everything else here
}
Evangelical answered 9/7, 2013 at 1:41 Comment(0)
C
1

You can derive sub-exceptions from the common exception thrown by other methods according to how you want to handle them.

Say you have declared the parent exception as ParentException. Derive sub classes like ChildAException extends ParentException, ChildBException extends ParentException etc.

Define a @ControllerAdvice class that catches the ParentException and define the specific behaviors in delegate methods.

@ControllerAdvice
public class ParentExceptionHandler {

    @ExceptionHandler(ParentException.class)
    public ResponseEntity<Object> handleParentException(ParentException pe) {
        if (pe instanceof ChildAException) {
            return handleChildAException((ChildAException) pe);
        } else if (...) {
            ...
        } else {
            // handle parent exception
        }
    }

    private ResponseEntity<Object> handleChildAException(ChildAException cae) {
        // handle child A exception
    }
}
Concertmaster answered 22/1, 2019 at 6:27 Comment(3)
Lets say I have 2 APIs in the same legacy controller(which I cannot change due to different design constraints), one is transactional and one is not. If I get A Hibernate/Hikari Exception due to underlying database issue, I need to send different responses to the client, (PENDING in one api and Failure in the other)Archiepiscopal
It's considered good practice to handle business logic in service layer. Do you have access to modify service layer logic? If so, you can throw 2 different child exceptions of the same parent exception, as I mentioned in the answer. When you throw an exception from service layer, the control will not get passed back to controller.Concertmaster
That would mean more of unnecessary try catches throughout the code , where ever I am having DB calls, which can be more than 10 per APiArchiepiscopal
H
1

I just got the same issue like you. So I checked the spring source code for this situation. It seems that spring will search in the @Controller class for any method that is annotated with @ExceptionHandler first, if nothing matched then it will continue to search for all class that is annotated with @ControllerAdvice. So you can just use the strategy below:

  • MyController with a @ExceptionHandler method:
@RestController
public class MyController {
  @RequestMapping("/foo")
  public String foo() {
    throw new IllegalArgumentException();
  }

  @ExceptionHandler(IllegalArgumentException.class)
  public ResponseEntity<String> handle(IllegalArgumentException ex) {
    return new ResponseEntity<>("Specific handler", HttpStatus.BAD_REQUEST);
  }
}
  • AnotherController without any method annotated with @ExceptionHandler:
@RestController
public class AnotherController {
  @RequestMapping("/bar")
  public String bar() {
    throw new IllegalArgumentException();
  }
}
  • A global @ControllerAdvice class:
@ControllerAdvice
public class GlobalExceptionHandler {
  @ExceptionHandler(IllegalArgumentException.class)
  public ResponseEntity<String> handle(IllegalArgumentException ex) {
    return new ResponseEntity<>("Global handler", HttpStatus.BAD_REQUEST);
  }
}

Then if you visiting http://ip:port/foo, you will get 400 status code with Specific handler, and 400 status code with Global handler when you visit http://ip:port/bar.

Homestead answered 9/10, 2021 at 8:46 Comment(0)
H
0

I agree that the inability to map a specific @ExceptionHandler to handle only one specific method in the @RestController should be a very desirable feature.

Herzel answered 23/8, 2019 at 22:15 Comment(0)
N
0

I tried try{}catch(Exception ex){} and catch no exception. But exception handler handles it nicely.

Since we are talking about hibernate exceptions, these exceptions are usually thrown at the commit phase of transaction. The problem here is that seems like you have transaction opened right in your controller which is considered as a bad practice.

What you should do is - open transaction in the application layer.

Controller just maps xml/json to incomming RequestDto object. And then you call the Service to handle the business logic. The Service(or its method) should be annotated by @Transactional.

@RestController
public class MyController {

    @Autowired // but better to use constructor injection
    private MyService myService;

    public ResponseDto doSomething(RequestDto request) {
        try {
            myService.doSomething(request);
        } catch (DataIntegrityViolationException ex) {
            // process exception
        }
    }
}

@Transactional
class MyService {

    public void doSomething() {
       // do your processing which uses jpa/hibernate under the hood
   }
}

Once you done that, the try catch will start behaving as expected on controller level. However, I would even go further as DatabaseExeption shouldn't really go that far to controller. The alternative would be to use manual transaction inside of a service and do a try catch there.

Then in the Service layer transform database exception in a more generic exception with all necessary information for controllers to process.

And then you should catch that more generic exception (MyDatabaseAccessException) in the controller and transform as you wish for the sake of a presentation layer.

===

The @ControllerAdvice suggested here is good for a global exception handling across controllers.

The @ExceptionHandler is not suitable for each method unless you wnat to have controller per method. And even after that it can clash with global @ControllerAdvice.

I am not sure why spring doesn't allow @ExceptionHandler at a method level, it would simplify a lot of cases like yours.

Ningpo answered 22/4, 2020 at 10:52 Comment(0)
P
0

My solution is to annotate a method with a marker:

@ExceptionHandler(SomeException.class)
public ResponseEntity<String> handleSomeException(SomeException e, HandlerMethod handlerMethod) {
    var marker = AnnotatedElementUtils.findMergedAnnotation(handlerMethod.getMethod(), MarkerAnnotation.class);
    if (marker != null) return something();
    else return somethingElse();
}
Percaline answered 18/8, 2022 at 9:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.