Java 在控制器spring MVC中控制异常流
springmvc中controllin异常流的良好实践是什么 假设我有一个DAO类,它将对象保存到数据库中,但如果违反了某些规则,例如名称太长,年龄太低,就会抛出异常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
@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));
}
//...
}
异常链接和泄漏的抽象
这里的原则是:
- 开发人员必须非常了解他们正在使用的抽象,并且知道这种抽象或类可能引发的任何异常
- 不应允许库中的异常从您自己的抽象中逃逸
- 确保使用异常链接,以避免在将低级异常包装到高级异常时丢失重要的上下文详细信息
@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 {
// ...
}