Spring MVC:将异常处理程序绑定到特定方法



您好!

我有一个@Controller。它的一些方法抛出了相同的异常,但我想用不同的方式处理这些异常。

有没有办法将@ExceptionHandler绑定到特定的方法?

您需要使用AOP工具,如CDI InterceptorAspectJ来实现这种交叉关注。问题是指根据功能划分的系统的一部分。

基本上,这种类型的功能用于处理日志记录、安全性以及处理错误。。。这不是你商业逻辑的一部分。。。

比如,如果你想将应用程序的记录器从log4j更改为sl4j,那么你需要遍历你使用过log4j的每个类并更改它。但如果你使用过AOP工具,那么你只需要转到拦截器类并更改实现。类似即插即用和功能强大的工具。

以下是使用JavaEECDI Interceptor的代码片段

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

在我们定义了拦截器绑定之后,我们需要定义拦截器绑定实现

/*
    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
        }
    }
}

现在我们只需要将拦截器应用于我们的方法

/*
    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
    }
}

您也可以使用AspectJ实现相同的功能。

/*
    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
        }
    }
}

现在我们只需要将AspectJ启用到我们的应用程序配置

/*
    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
    }
}

我的git repo中有代码示例https://github.com/prameshbhattarai/javaee-exceptionBinding通过使用CDI拦截器。

作为一个选项(显然,这并不理想):您可以在其中一个方法中将异常包装为自定义异常,然后在@ExceptionHandler 中捕获它

void boo() throws WrappingException {
    try {
    } catch (TargetException e) {
        throw new WrappingException(e);
    }
}

然后

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

你能解释一下为什么需要这个吗?我这么问是出于好奇,因为我从来没有觉得这是必要的,原因如下:

异常通常代表一个非常具体的"错误"——以一种非常具体的方式出错的东西。基本上,异常代表错误,而不是流。。。

弹簧可以开箱即用地支撑两个"自由度":

  1. 异常参数。也许是错误代码之类的东西,它可以声明为异常本身的数据字段。

  2. 异常继承。示例:

如果你的系统中有一个UserDoesNotExistException,并且你想在某种情况下更具体,比如说,管理某些流中退休用户的系统,你总是可以创建一个更具体的异常:

class UserRetiredException extends UserDoesNotExistException {...}

显然,spring可以支持这两种情况:在ExceptionMapper中,您无论如何都可以访问异常,因此您可以执行以下操作:

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

在第二种情况下,您只需要为不同类型的异常使用不同的异常映射器。

您还可以考虑使用@ControllerAdvice注释来重用代码或其他内容。

我不认为可以为方法指定特定的@ExceptionHandler,但可以将@ExceptionHandler方法绑定到特定的Exception

因此,如果你想用一种方式处理所有DataIntegrityViolationException,用另一种方式来处理所有其他异常,你应该能够用这样的东西来实现:

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

您可以根据处理方式从其他方法抛出的常见异常派生子异常。

假设您已将父异常声明为ParentException。派生子类,如ChildAException extends ParentExceptionChildBException extends ParentException

定义一个捕获ParentException@ControllerAdvice类,并定义委托方法中的特定行为。

@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
    }
}

我刚刚遇到了和你一样的问题。因此,我检查了spring的源代码以了解这种情况。看起来spring将首先在@Controller类中搜索任何用@ExceptionHandler注释的方法,如果没有匹配的方法,那么它将继续搜索所有用@ControllerAdvice注释的类。所以你可以使用下面的策略:

  • MyController@ExceptionHandler方法:
@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,没有任何用@ExceptionHandler注释的方法:
@RestController
public class AnotherController {
  @RequestMapping("/bar")
  public String bar() {
    throw new IllegalArgumentException();
  }
}
  • 全局@ControllerAdvice类:
@ControllerAdvice
public class GlobalExceptionHandler {
  @ExceptionHandler(IllegalArgumentException.class)
  public ResponseEntity<String> handle(IllegalArgumentException ex) {
    return new ResponseEntity<>("Global handler", HttpStatus.BAD_REQUEST);
  }
}

如果你来http://ip:port/foo,当您访问时,您将使用"特定"处理程序获得400状态代码,使用"全局"处理程序将获得400状态码http://ip:port/bar.

我同意,无法映射特定的@ExceptionHandler来只处理@RestController中的一个特定方法应该是一个非常理想的功能。

我尝试了{}catch(Exception ex){},但没有捕获异常。但是异常处理程序处理得很好。

由于我们讨论的是hibernate异常,这些异常通常在事务的提交阶段抛出。这里的问题是,您似乎在控制器中打开了事务,这被认为是一种糟糕的做法。

您应该做的是在应用程序层中打开事务。

控制器只是将xml/json映射到单独的RequestDto对象。然后您调用服务来处理业务逻辑。服务(或其方法)应通过@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
   }
}

完成此操作后,try-catch将开始在控制器级别上按预期运行。然而,我甚至会更进一步,因为DatabaseExeption不应该真正深入到控制器。另一种选择是在服务内部使用手动事务,并在那里进行尝试捕获。

然后在服务层中,将数据库异常转换为一个更通用的异常,其中包含控制器要处理的所有必要信息。

然后,您应该在控制器中捕获更通用的异常(MyDatabaseAccessException),并根据您的意愿进行转换,以实现表示层。

===

这里建议的@ControllerAdvice适用于跨控制器的全局异常处理。

@ExceptionHandler不适用于每种方法,除非您拥有每种方法的控制器。即使在那之后,它也可能与global@ControllerAdvice发生冲突。

我不知道spring为什么不允许在方法级别使用@ExceptionHandler,它会简化很多像您这样的情况。

我的解决方案是用标记注释一个方法:

@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();
}

最新更新