Reputation: 43
What is the good practice in controllin exception flow in spring MVC?
Lets say i have DAO class that saves object into database but throws exception if some rule is violated, e.g name is too long, age is too low,
@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)}
}
Now if i want to save A that has longer name than 10 it should throw exception.
Howver having a dataManipulator object
class A_DataManipulator{
public Something save(A a ){
try{
a_dao.save(a);
}
catch(ConstraintViolationException e){
return new ObjectThatHasExceptionDescription();
}
return new SomethingThatSaysItsok()
}
}
and controller
@RequestMapping(value = "/addA", method = RequestMethod.POST)
@ResponseBody
public Something addA(@RequestBody A a){
return a_data_manipulator.save(a)
}
I would like to keep controller without throwing exceptions ( as i heard was good practice).
But my question is, in this case, how would A_Data_Manipulator
look like?
In case of exception, i would like to return some status(404/500 etc) and some custom message. In case of success i would like to just return 200.
I suppose i could create something like this:
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);
}
This in my opinion is too "forced", is there any way how to create such behavior?
Thanks for help!
Upvotes: 1
Views: 2717
Reputation: 78579
There's a number of principles that we typically follow in my development team. A few months ago I actually took the time to document my thoughts on this topic.
The following are some of those relevant aspects related to your question.
How should the controller layer deal with the need to serialize exceptions back to the client?
There are multiple ways to deal with this, but perhaps the simplest solution is to define a class annotated as @ControllerAdvice. In this annotated class we will place our exception handlers for any specific exceptions from our inner application layers that we want to handle and turn them into a valid response object to travel back to our clients:
@ControllerAdvice
public class ExceptionHandlers {
@ExceptionHandler
public ResponseEntity<ErrorModel> handle(ValidationException ex) {
return ResponseEntity.badRequest()
.body(new ErrorModel(ex.getMessages()));
}
//...
}
Since we are not using Java RMI as the serialization protocol for our services, we simply cannot send a Java Exception
object back to the client. Instead, we must inspect the exception object generated by our inner application layers and construct a valid, serializable transport object that we can indeed send back to our clients. For that matter, we defined an ErrorModel
transport object and we simply populate it with details from the exception in a corresponding handler method.
The following is a simplified version of what could be done. Perhaps, for real production applications, we may want to put a few more details in this error model (e.g. status codes, reason codes, etc).
/**
* Data Transport Object to represent errors
*/
public class ErrorModel {
private final List<String> messages;
@JsonCreator
public ErrorModel(@JsonProperty("messages") List<String> messages) {
this.messages = messages;
}
public ErrorModel(String message) {
this.messages = Collections.singletonList(message);
}
public List<String> getMessages() {
return messages;
}
}
Finally, notice how the error handler code from the ExceptionHandlers
from before treats any ValidationException
as HTTP Status 400: Bad Request. That will allow the client to inspect the status code of the response and discover that our service rejected their payload because there is something wrong with it. Just as easily we could have handlers for exceptions that are supposed to be linked with 5xx errors.
The principles here are:
RuntimeException
or any other generic exception.So, the first point here is that designing good exceptions implies that the exceptions should encapsulate any contextual details from the place where the exception is being thrown. This information can be vital for a catching block to handle the exception (e.g. our handler from before) or it can be very useful during troubleshooting to determine the exact state of the system when the problem occurred, making it easier for the developers to reproduce the exact same event.
Additionally, it is ideal that exceptions themselves convey some business semantics. In other words, instead of just throwing RuntimeException
it is better if we create an exception that already conveys semantics of the specific condition under which it occurred.
Consider the following example:
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;
}
//...
}
Notice in the example above how we have defined a semantic exception InsufficientFundsException
to represent the exceptional condition of not having sufficient funds in an account when somebody tries to withdraw an invalid amount of money from it. This is a specific business exception.
Also notice how the exception carries all the contextual details of why this is considered an exceptional condition: it encapsulates the account number affected, its current balance and the amount of money we were trying to withdraw when the exception was thrown.
Any block catching this exception has sufficient details to determine what happened (since the exception itself is semantically meaningful) and why it happened (since the contextual details encapsulated within the exception object contain that information).
The definition of our exception class could be somewhat like this:
/**
* 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);
}
}
This strategy makes it possible that if, at any point, an API user wants to catch this exception to handle it in any way, that API user can gain access to the specific details of why this exception occurred, even if the original parameters (passed to the method where the exception occurred) are no longer available in the context where the exception is being handled.
One of such places where we'll want to handle this exception in some sort of ExceptionHandlers
class. In the code below notice how the exception is handled in a place where it is totally out of context from the place where it was thrown. Still, since the exception contains all contextual details, we are capable of building a very meaningful, contextual message to send back to our API client.
I use Spring @ControllerAdvice
to define exception handlers for specific exceptions.
@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));
}
//...
}
Also, it also worth noticing that the getMessage()
method of InsufficientFundsException
was overridden in this implementation. The contents of this message is what our log stack traces will display if we decide to log this particular exception. Therefore it is of paramount importance that we always override this method in our exceptions classes such that those valuable contextual details they contain are also rendered in our logs. It is in those logs where those details will most likely make a difference when we are trying to diagnose a problem with our system:
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]
The principles here are:
Effective Java explains it very well:
It is disconcerting when a method throws an exception that has no apparent connection to the task that it performs. This often happens when a method propagates an exception thrown by a lower-level abstraction. Not only is it disconcerting, but it pollutes the API of the higher layer with implementation details. If the implementation of the higher layer changes in a later release, the exceptions it throws will change too, potentially breaking existing client programs.
To avoid this problem, higher layers should catch lower-level exceptions and, in their place, throw exceptions that can be explained in terms of the higher-level abstraction. This idiom is known as exception translation:
// Exception Translation
try {
//Use lower-level abstraction to do our bidding
//...
} catch (LowerLevelException cause) {
throw new HigherLevelException(cause, context, ...);
}
Every time we use a third-party API, library or framework, our code is subject to fail for exceptions being thrown by their classes. We simply must not allow that those exceptions escape from our abstractions. Exceptions being thrown by the libraries we use should be translated to appropriate exceptions from our own API exception hierarchy.
For example, for your data access layer, you should avoid leaking exceptions like SQLException
or IOException
or JPAException
.
Instead, you may want to define a hierarchy of valid exceptions for you API. You may define a super class exception from which your specific business exceptions can inherit from and use that exception as part of your contract.
Consider the following example from our SavingsAccountService
:
@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);
}
}
In the example above we recognize that it is possible that our data access layer might fail in recovering the details of our savings account. There is no certainty of how this might fail, however we know that the Spring framework has a root exception for all data access exceptions: DataAccessException
. In this case we catch any possible data access failures and wrap them into a SavingsAccountException
to avoid that the underlying abstraction exceptions escape our own abstraction.
It is worth noticing how the SavingsAccountException
not only provides contextual details, but also wraps the underlying exception. This exception chaining is a fundamental piece of information that is included in the stack trace when the exception is logged. Without these details we could only know that our system failed, but not why:
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
The SavingsAccountException
is a somewhat generic exception for our savings account services. Its semantic power is a bit limited though. For example, it tells us there was a problem with a savings account, but it does not explicitly tell us what exactly. For that matter we may consider adding an additional message or weight the possibility of defining a more contextual exception (e.g. WithdrawMoneyException
).
Given its generic nature, it could be used as the root of our hierarchy of exceptions for our savings account services.
/**
* 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);
}
}
Some exceptions represent recoverable conditions (e.g. a QueryTimeoutException
) and some don't (e.g. DataViolationException
).
When an exception condition is temporal, and we believe that if we try again we could probably succeed, we say that such exception is transient. On the other hand, when the exceptional condition is permanent then we say such exception is persistent.
The major point here is that transient exceptions are good candidates for retry blocks, whereas persistent exceptions need to be handled differently, typically requiring some human intervention.
This knowledge of the 'transientness' of exceptions becomes even more relevant in distributed systems where an exception can be serialized somehow and sent beyond the boundaries of the system. For example, if the client API receives an error reporting that a given HTTP endpoint failed to execute, how can the client know if the operation should be retried or not? It would be pointless to retry if the condition for which it failed was permanent.
When we design an exception hierarchy based on a good understanding of the business domain and the classical system integration problems, then the information of wether an exceptions represents a recoverable condition or not can be crucial to design good behaving clients.
There are several strategies we could follow to indicate an exceptions is transient or not within our APIs:
@TransientException
annotation and add it to the exceptions.TransientServiceException
class.The Spring Framework follows the approach in the third option for its data access classes. All exceptions that inherit from TransientDataAccessException are considered transient and retryable in Spring.
This plays rather well with the Spring Retry Library. It becomes particularly simple to define a retry policy that retries any operation that caused a transient exception in the data access layer. Consider the following illustrative example:
@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);
}
});
}
In the code above, if the DAO fails to retrieve a record from the database due to e.g. a query timeout, Spring would wrap that failure into a QueryTimeoutException
which is also a TransientDataAccessException
and our RetryTemplate
would retry that operation up to 3 times before it surrenders.
How about transient error models?
When we send error models back to our clients we can also take advantage of knowing if a given exception is transient or not. This information let us tell the clients that they could retry the operation after certain back off period.
@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);
}
}
The code above uses a BinaryExceptionClassifier, which is part of the Spring Retry library, to determine if a given exception contains any transient exceptions in their causes and if so, categorizes that exception as transient. This predicate is used to determine what type of HTTP status code we send back to the client. If the exception is transient we send a 503 Service Unavailable
and provide a header Retry-After: 5000
with the details of the backoff policy.
Using this information, clients can decide whether it make sense to retry a given web service invocation and exactly how long they need to wait before retrying.
The Spring Framework also offers the possibility of annotating exceptions with specific HTTP status codes, e.g.
@ResponseStatus(value=HttpStatus.NOT_FOUND, reason="No such Order") // 404
public class OrderNotFoundException extends RuntimeException {
// ...
}
I personally tend to dislike this approach, not only because of its limitation to generate an appropriate contextual message, but also because it forces me to couple my business layer with my controller layer: if I do this, suddenly my bunsiness-layer exceptions need to know about HTTP 400 or 500 errors. This is a responsibility that I believe belongs exclusively in the controller layer and I prefer if knowledge of what specific communication protocol I use should not be a thing my business layer needs to worry about.
We could extend the topic a bit more with input validation exception techniques, but I believe answers have a limited amount of characters and I don't believe I could make it fit here.
I hope at least this information is useful for your investigation.
Upvotes: 5