Java 在控制器spring MVC中控制异常流

Java 在控制器spring MVC中控制异常流,java,spring,spring-mvc,jpa,exception,Java,Spring,Spring Mvc,Jpa,Exception,springmvc中controllin异常流的良好实践是什么 假设我有一个DAO类,它将对象保存到数据库中,但如果违反了某些规则,例如名称太长,年龄太低,就会抛出异常 @Entity class A{ @Id @GeneratedValue private long id; @Column(nullable=false,length=10) private String name; } class A_DAO{ public void save(A a) throw

springmvc中controllin异常流的良好实践是什么

假设我有一个DAO类,它将对象保存到数据库中,但如果违反了某些规则,例如名称太长,年龄太低,就会抛出异常

@Entity
class A{
  @Id
  @GeneratedValue
  private long id;

  @Column(nullable=false,length=10)
  private String name;
}

class A_DAO{
  public void save(A a) throws ConstraintViolationException{ persistance.save(a)}
}
现在,如果我想保存一个名称长度超过10的文件,它应该抛出异常

但是有一个数据操纵器对象

class A_DataManipulator{
  public Something save(A a ){
    try{
       a_dao.save(a);
    }
    catch(ConstraintViolationException e){
       return new ObjectThatHasExceptionDescription();
    }
    return new SomethingThatSaysItsok()
  }

}
和控制器

@RequestMapping(value = "/addA", method = RequestMethod.POST)
@ResponseBody
public Something addA(@RequestBody A a){
   return a_data_manipulator.save(a)
}
我希望在不抛出异常的情况下保留控制器(我听说这是一个很好的实践)

但我的问题是,在这种情况下,
A_Data_操纵器
会是什么样子? 如果出现异常,我想返回一些状态(404/500等)和一些自定义消息。如果成功,我只想返回200

我想我可以创造这样的东西:

class Message{
 public String msg;
 Message(String s) { this.msg = s}
}

  class A_Data_Manipulator{
              public Message save(A a ){
              try{
                 a_dao.save(a);
              }catch(ConstraintViolationException e){
               return new Message("some violation");
              }
             return null; 
          }
        }

// controller annotations
public ResponseEntity add(A a){
  Msg m = a_data_manipulator.save(a);
  if( m == null )
    return new ResponseEntity(HttpStatus.OK);
  return new ResponseEntity(HttpStatus.BAD_GATE,msg);

}
 /**
  * Thrown when the bank account does not have sufficient funds to satisfy
  * an operation, e.g. a withdrawal.
  */
 public class InsufficientFundsException extends SavingsAccountException {

    private final double balance;
    private final double withdrawal;

    //stores contextual details
    public InsufficientFundsException(AccountNumber accountNumber, double balance, double withdrawal) {
        super(accountNumber);
        this.balance = balance;
        this.withdrawal = withdrawal;
    }

    public double getBalance() {
        return balance;
    }

    public double getWithdrawal() {
        return withdrawal;
    }

    //the importance of overriding getMessage to provide a personalized message
    @Override
    public String getMessage() {
        return String.format("Insufficient funds in bank account %s: (balance $%.2f, withdrawal: $%.2f)." +
                                     " The account is short $%.2f",
                this.getAccountNumber(), this.balance, this.withdrawal, this.withdrawal - this.balance);
    }
 }
这在我看来太“强迫”了,有没有办法创造这样的行为


谢谢你的帮助

我的开发团队通常遵循一些原则。几个月前,我真的花时间讨论了这个话题

以下是与您的问题相关的一些方面

异常序列化 控制器层应该如何处理将异常序列化回客户端的需要

处理这个问题有多种方法,但最简单的解决方案可能是定义一个注释为的类。在这个带注释的类中,我们将为我们要处理的内部应用程序层中的任何特定异常放置异常处理程序,并将它们转换为有效的响应对象,以返回到我们的客户机:

 @ControllerAdvice
 public class ExceptionHandlers {

    @ExceptionHandler
    public ResponseEntity<ErrorModel> handle(ValidationException ex) {
        return ResponseEntity.badRequest()
                             .body(new ErrorModel(ex.getMessages()));
    }

    //...
 }
最后,请注意前面的
ExceptionHandlers
中的错误处理程序代码如何将任何
ValidationException
处理为HTTP状态400:Bad请求。这将允许客户端检查响应的状态代码,并发现我们的服务拒绝了它们的负载,因为它有问题。同样容易的是,我们可以为异常设置处理程序,这些异常应该与5xx错误相链接

设计上下文异常 这里的原则是:

  • 好的异常包含其上下文的所有相关细节,因此任何捕获块都可以获得处理它们所需的任何细节
  • 努力设计特定于您的业务运营的例外情况。已经传达业务语义的异常。这比仅仅抛出
    RuntimeException
    或任何其他通用异常要好
  • 设计您的异常以完美地记录所有这些有意义的信息
因此,这里的第一点是,设计好的异常意味着异常应该封装来自异常抛出位置的任何上下文细节。此信息对于捕获块处理异常(例如,我们以前的处理程序)非常重要,或者在故障排除过程中非常有用,可以确定问题发生时系统的确切状态,使开发人员更容易重现完全相同的事件

此外,异常本身最好能传递一些业务语义。换句话说,与其仅仅抛出
RuntimeException
,不如创建一个已经传达了它发生的特定条件语义的异常

考虑以下示例:

public class SavingsAccount implements BankAccount {

     //...

     @Override
     public double withdrawMoney(double amount) {
         if(amount <= 0)
             throw new IllegalArgumentException("The amount must be >= 0: " + amount);

         if(balance < amount) {
             throw new InsufficientFundsException(accountNumber, balance, amount);
         }
         balance -= amount;

         return balance;
     }

     //...

  }
此策略使API用户能够在任何时候捕获此异常并以任何方式进行处理,即使原始参数(传递给异常发生的方法)无效,该API用户也可以访问此异常发生原因的特定详细信息在处理异常的上下文中不再可用

我们希望在某种
ExceptionHandlers
类中处理此异常的地方之一。在下面的代码中,请注意异常是如何在与抛出异常的位置完全脱离上下文的位置进行处理的。尽管如此,由于异常包含所有上下文细节,我们能够构建一个非常有意义的上下文消息以发送回API客户机

我使用Spring
@ControllerAdvice
为特定异常定义异常处理程序

 @ControllerAdvice
 public class ExceptionHandlers {

    //...

    @ExceptionHandler
    public ResponseEntity<ErrorModel> handle(InsufficientFundsException ex) {

        //look how powerful are the contextual exceptions!!!
        String message = String.format("The bank account %s has a balance of $%.2f. Therefore you cannot withdraw $%.2f since you're short $%.2f",
                ex.getAccountNumber(), ex.getBalance(), ex.getWithdrawal(), ex.getWithdrawal() - ex.getBalance());

        logger.warn(message, ex);
        return ResponseEntity.badRequest()
                             .body(new ErrorModel(message));
    }

    //...
 }
异常链接和泄漏的抽象 这里的原则是:

  • 开发人员必须非常了解他们正在使用的抽象,并且知道这种抽象或类可能引发的任何异常
  • 不应允许库中的异常从您自己的抽象中逃逸
  • 确保使用异常链接,以避免在将低级异常包装到高级异常时丢失重要的上下文详细信息
高效的Java很好地解释了这一点:

当一个方法抛出一个没有 与它执行的任务的明显连接。这种情况经常发生 当方法传播由较低级别引发的异常时 抽象。这不仅令人不安,而且还污染了API 更高一层包含实现细节。如果执行 较高的层在以后的版本中更改,它抛出的异常 也将发生变化,可能会破坏现有的客户端程序

为了避免这个问题,较高的层应该捕获较低的层 异常,并在其位置抛出可以解释的异常 就更高层次的抽象而言。这个成语被称为 例外翻译:

每次我们使用第三方API、库或框架时,我们的代码都会因类引发异常而失败。我们决不能允许这些例外从我们的抽象中逃脱。我们使用的库引发的异常应该从我们自己的API异常层次结构转换为适当的异常

例如,对于数据访问层,您应该
 @ControllerAdvice
 public class ExceptionHandlers {

    //...

    @ExceptionHandler
    public ResponseEntity<ErrorModel> handle(InsufficientFundsException ex) {

        //look how powerful are the contextual exceptions!!!
        String message = String.format("The bank account %s has a balance of $%.2f. Therefore you cannot withdraw $%.2f since you're short $%.2f",
                ex.getAccountNumber(), ex.getBalance(), ex.getWithdrawal(), ex.getWithdrawal() - ex.getBalance());

        logger.warn(message, ex);
        return ResponseEntity.badRequest()
                             .body(new ErrorModel(message));
    }

    //...
 }
 com.training.validation.demo.api.InsufficientFundsException: Insufficient funds in bank account 1-234-567-890: (balance $0.00, withdrawal: $1.00). The account is short $1.00
    at com.training.validation.demo.domain.SavingsAccount.withdrawMoney(SavingsAccount.java:40) ~[classes/:na]
    at com.training.validation.demo.impl.SavingsAccountService.lambda$null$0(SavingsAccountService.java:45) ~[classes/:na]
    at java.util.Optional.map(Optional.java:215) ~[na:1.8.0_141]
    at com.training.validation.demo.impl.SavingsAccountService.lambda$withdrawMoney$2(SavingsAccountService.java:45) ~[classes/:na]
    at org.springframework.retry.support.RetryTemplate.doExecute(RetryTemplate.java:287) ~[spring-retry-1.2.1.RELEASE.jar:na]
    at org.springframework.retry.support.RetryTemplate.execute(RetryTemplate.java:164) ~[spring-retry-1.2.1.RELEASE.jar:na]
    at com.training.validation.demo.impl.SavingsAccountService.withdrawMoney(SavingsAccountService.java:40) ~[classes/:na]
    at com.training.validation.demo.controllers.SavingsAccountController.onMoneyWithdrawal(SavingsAccountController.java:35) ~[classes/:na]
   // Exception Translation
   try {
      //Use lower-level abstraction to do our bidding
      //...
   } catch (LowerLevelException cause) {
      throw new HigherLevelException(cause, context, ...);
   }
 @Override
 public double saveMoney(SaveMoney savings) {

    Objects.requireNonNull(savings, "The savings request must not be null");

    try {
        return accountRepository.findAccountByNumber(savings.getAccountNumber())
                                .map(account -> account.saveMoney(savings.getAmount()))
                                .orElseThrow(() -> new BankAccountNotFoundException(savings.getAccountNumber()));
    }
    catch (DataAccessException cause) {
        //avoid leaky abstractions and wrap lower level abstraction exceptions into your own exception
        //make sure you keep the exception chain intact such that you don't lose sight of the root cause
        throw new SavingsAccountException(savings.getAccountNumber(), cause);
    }
 }
 com.training.validation.demo.api.SavingsAccountException: Failure to execute operation on account '1-234-567-890'
    at com.training.validation.demo.impl.SavingsAccountService.lambda$withdrawMoney$2(SavingsAccountService.java:51) ~[classes/:na]
    at org.springframework.retry.support.RetryTemplate.doExecute(RetryTemplate.java:287) ~[spring-retry-1.2.1.RELEASE.jar:na]
    at org.springframework.retry.support.RetryTemplate.execute(RetryTemplate.java:164) ~[spring-retry-1.2.1.RELEASE.jar:na]
    at com.training.validation.demo.impl.SavingsAccountService.withdrawMoney(SavingsAccountService.java:40) ~[classes/:na]
    at com.training.validation.demo.controllers.SavingsAccountController.onMoneyWithdrawal(SavingsAccountController.java:35) ~[classes/:na]
    at java.lang.Thread.run(Thread.java:748) [na:1.8.0_141]
    ... 38 common frames omitted
 Caused by: org.springframework.dao.QueryTimeoutException: Database query timed out!
    at com.training.validation.demo.impl.SavingsAccountRepository.findAccountByNumber(SavingsAccountRepository.java:31) ~[classes/:na]
    at com.training.validation.demo.impl.SavingsAccountRepository$$FastClassBySpringCGLIB$$d53e9d8f.invoke(<generated>) ~[classes/:na]
    ... 58 common frames omitted
/**
  * Thrown when any unexpected error occurs during a bank account transaction.
  */
 public class SavingsAccountException extends RuntimeException {

    //all SavingsAccountException are characterized by the account number.
    private final AccountNumber accountNumber;

    public SavingsAccountException(AccountNumber accountNumber) {
        this.accountNumber = accountNumber;
    }

    public SavingsAccountException(AccountNumber accountNumber, Throwable cause) {
        super(cause);
        this.accountNumber = accountNumber;
    }

    public SavingsAccountException(String message, AccountNumber accountNumber, Throwable cause) {
        super(message, cause);
        this.accountNumber = accountNumber;
    }

    public AccountNumber getAccountNumber() {
        return accountNumber;
    }

    //the importance of overriding getMessage
    @Override
    public String getMessage() {
        return String.format("Failure to execute operation on account '%s'", accountNumber);
    }
 }
  @Override
  public double withdrawMoney(WithdrawMoney withdrawal) throws InsufficientFundsException {
     Objects.requireNonNull(withdrawal, "The withdrawal request must not be null");

     //we may also configure this as a bean
     RetryTemplate retryTemplate = new RetryTemplate();
     SimpleRetryPolicy policy = new SimpleRetryPolicy(3, singletonMap(TransientDataAccessException.class, true), true);
     retryTemplate.setRetryPolicy(policy);

     //dealing with transient exceptions locally by retrying up to 3 times
     return retryTemplate.execute(context -> {
         try {
             return accountRepository.findAccountByNumber(withdrawal.getAccountNumber())
                                     .map(account -> account.withdrawMoney(withdrawal.getAmount()))
                                     .orElseThrow(() -> new BankAccountNotFoundException(withdrawal.getAccountNumber()));
         }
         catch (DataAccessException cause) {
            //we get here only for persistent exceptions
            //or if we exhausted the 3 retry attempts of any transient exception.
            throw new SavingsAccountException(withdrawal.getAccountNumber(), cause);
         }
     });
  }
  @ControllerAdvice
  public class ExceptionHandlers {

    private final BinaryExceptionClassifier transientClassifier = new BinaryExceptionClassifier(singletonMap(TransientDataAccessException.class, true), false);
    {
        transientClassifier.setTraverseCauses(true);
    }

    //..

    @ExceptionHandler
    public ResponseEntity<ErrorModel> handle(SavingsAccountException ex) {
        if(isTransient(ex)) {
            //when transient, status code 503: Service Unavailable is sent
            //and a backoff retry period of 5 seconds is suggested to the client
            return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
                                 .header("Retry-After", "5000")
                                 .body(new ErrorModel(ex.getMessage()));
        } else {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                                 .body(new ErrorModel(ex.getMessage()));
        }
    }

    private boolean isTransient(Throwable cause) {
        return transientClassifier.classify(cause);
    }

 }
 @ResponseStatus(value=HttpStatus.NOT_FOUND, reason="No such Order")  // 404
 public class OrderNotFoundException extends RuntimeException {
     // ...
 }