A little new to the Java Spring Boot flavor of webservices -- so please be gentle. Why do most Spring Boot Controller examples not show any exceptions being captured? I see some of my fellow developers do this a lot. And the answer is: it's a common convention. But why. Am I missing a key concept of web services created using Spring Boot?
For example:
#PostMapping(path = "/sampleEndpoint/v1/myEndpoint", produces = "application/json")
public ResponseEntity<String> myEndpoint(#RequestBody MyEndpointRequest myEndpointRequest) {
MyEndpointResponse response = someService.myEndpoint(myEndpointRequest);
return new ResponseEntity<>(response, HttpStatus.OK);
}
I would think, with respects to the architecture you would add AT LEAST a try/catch block with say some logging, and throw a new exception with the exceptions message:
#PostMapping(path = "/sampleEndpoint/v1/myEndpoint", produces = "application/json")
public ResponseEntity<String> myEndpoint(#RequestBody MyEndpointRequest myEndpointRequest) {
try{
MyEndpointResponse response = someService.myEndpoint(myEndpointRequest);
return new ResponseEntity<>(response, HttpStatus.OK);
}catch(Exception ex){
//Your favorite logger:
log.error("STACK_TRACE: {}", StaticClass.stackTraceToString(ex));
//throw exception for calling or consuming system/application:
throw new MiscException(ex.getMessage());
}
}
A couple of things to give context to this question (observation):
Use multiple data sources: a couple of databases, and some other web services (gives our client a one stop place to get their data.
Using this webservice with potentially 4 different client side /presentation layer type of applications.
My team would like to capture unexpected exceptions emanating from the data sources we tap into...and log them.
Well it's up to developer to implement catch of exceptions mechanism. But it's a good practise to define exceptions types and error codes/messages for that. Let's say you have an endpoint which fetch product with id, but there is no product with that id, in that case client will receive http 500 code with internal server error message. This will confuse users and also developers, what was the real cause of that error.
So prevent those, you can get help from #ControllerAdvice annotation, which will allow to apply exception handlers to more than one or all controllers.
First you will define your custom exceptions like :
public class ProductNotFoundException extends RuntimeException {
public ProductNotFoundException(Long id) {
super(String.format("Product with id %d not found", id));
}
}
and then you can define your ControllerAdvice class:
#ControllerAdvice
public class ExceptionHandler extends ResponseEntityExceptionHandler {
#ExceptionHandler(ProductNotFound.class)
public ResponseEntity<Object> handleProductNotFoundException(
ProductNotFoundException ex, WebRequest request) {
Map<String, Object> body = new LinkedHashMap<>();
body.put("timestamp", LocalDateTime.now());
body.put("message", "Product not found");
return new ResponseEntity<>(body, HttpStatus.NOT_FOUND);
}
}
Related
In my Spring Boot app, I just use Optional for the first time and after examining several projects and topics, now I am trying to build an approach as shown below:
Repository:
Optional<Employee> findByEmail(String email);
Service:
public Response findByEmail(String email) {
return employeeRepository.findByEmail(email)
// if record is found, I think no need to return status or message
.map(e -> Response.builder().data(e).build())
.orElseGet(() -> Response.builder().status(404)
.data(null).message("Not found!").build());
}
Response:
#Data
#Builder
public class Response {
private int status;
private Object data;
private String message;
}
Controller:
#GetMapping("/employees/{email}")
public ResponseEntity<Response> findByEmail(#PathVariable String email) {
final Response response = employeeService.findByEmail(email);
return ResponseEntity
.status(response.getStatus())
.body(response.getMessage(), response.getData());
// throws "Expected 1 arguments but found 2" error
}
Here is the points that I need to be clarified:
1. Is this a proper approach to use a common response for all the Optional types in a Spring Boot app? If not, how should I change it (I want to return a common response from the Service)?
2. How to fix the throws "Expected 1 arguments but found 2" error in the Controller?
From my comment above - You are mixing concerns. Service is supposed to only care about business logic (e.g. not HTTP Status codes). That's controller's job. Use of Optional is correct, but the Response return type from service layer is not. Also errors like Not Found are automatically handled by a Rest Controller in Spring boot if a resource is not found. If you want to add custom logic and prepare generic responses, include a proper exception handling e.g. #ControllerAdvice (which allows you reuse exceptions for controllers).
As an example, one of the solutions would be to throw NoSuchElementException.
This is illustrative and would apply if you want to handle other such situations (e.g. null pointers, internal server error, authentication errors in a more custom manner) in a generic manner.
public Employee findByEmail(String email) {
return employeeRepository.findByEmail(email) //assuming findByEmail is returning an Optional<Employee>, otherwise - simply use a null check.
.orElseThrow(NoSuchElementException::new)
}
Inside #ControllerAdvice class
#ExceptionHandler(NoSuchElementException.class)
#ResponseBody
#Order(Ordered.HIGHEST_PRECEDENCE)
public final ResponseEntity<APIResponseErrorContainer> handleNotFound(
NoSuchElementException ex) {
// log exception here if you wish to
return new ResponseEntity<>(createCustomResponseBody(), HttpStatus.NOT_FOUND);
}
I have a simple spring boot application in which I have alot of tables. I have build their models, repositories, service and controller files. I have also tested all the apis through postman.
Now I need to implement custom exception in my models. Since I am at the beginning stage and learning things, I am a little confused as to how can I apply exceptions?
From what I have explored, I need to create three files
ErrorDetails.java
GlobalExceptionHandler.java
ResourceNotFoundException.java
Is this correct? If yes suppose I have added these files in my project . How do I implement these exceptions in my apis? Can anybody help me? Would mean alot. Thanks!
Whenever there is a case where resources will be not available then throw ResourceNotFoundException i.e. throw new ResourceNotFoundException("Error message of your choice");
For example in class CustomerTypeRepository within method getCustomerTypebyID instead of below code:
if (a == null) {
return ResponseEntity.notFound().build();
}
you can write
if (a == null) {
throw new ResourceNotFoundException("Customer type doesn't exist with the given id: "+Id);
}
and after that #ControllerAdvice GlobalExceptionHandler has already implamented for ResourceNotFoundException handler. So no need to worry about.
I believe in declaring checked exception as a contract, so I would do something like this
#Service
public class CustomerCategorizationService {
#Autowired
private CustomerTypeRepository customerTypeRepository;
// -------------- CustomerType API ----------------------- ///
public CustomerType saveCustomerType(CustomerType obj) throws ServiceException {
Where ServiceException is custom checked exception defined in application.
public CustomerType saveCustomerType(CustomerType obj) throws ServiceException {
//Other code
Long id;
try {
id = customerTypeRepository.save(obj);
}catch(DataAccessException cause) {
throw new ServiceException(cause.getMessage());
}
return id;
}
And in #ControllerAdvice
#ExceptionHandler(ServiceException.class)
public ResponseEntity<?> resourceNotFoundException(ServiceException ex, WebRequest request) {
ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(),
request.getDescription(false));
return new ResponseEntity<>(errorDetails, HttpStatus.NOT_FOUND);
}
We can go one step further and throw custom exception from Controller class (say ResourceException) which will wrap ServiceException. In that case my #ControllerAdvice needs to only deal with ResourceException
I have a post endpoint in my controller in spring that looks like so
#Autowired
private BookRepository bookRepository;
#PostMapping(path="/book")
public #ResponseBody BookEntity addBook(#RequestBody BookEntity bookEntity)
{
return this.bookRepository.save(bookEntity);
}
Now, if invalid json data is submitted and the save function is called my console will output an error such as h.engine.jdbc.spi.SqlExceptionHelper : Duplicate entry '1091209' for key 'isbn'
And the user will see a very non-user friendly api response.
Another issue this brings it seems is that an auto_incremented id is skipped e.g. I go 1,2,4, skipping 3 due the 3rd api call being the failing api call.
What is best practice for handling the above scenarios?
You can define your own error handler which extends ResponseEntityExceptionHandler and catch db specific exception. Here is a code...
#RestControllerAdvice
#RequestMapping(produces = "application/json")
public class DefaultExceptionHandler extends ResponseEntityExceptionHandler {
// This method handles constraint violation exception raised by DB.
// Similarly other type exceptions like custom exception and HTTP status related
//exception can be handled here.
#ExceptionHandler(ConstraintViolationException.class)
#ResponseStatus(value = HttpStatus.CONFLICT)
public Map<String, String> handleConstraintViolationException(ConstraintViolationException ex) {
// write your own logic to return user friendly response
}
// Below method is to handle _SqlExceptionHelper_ exception
#ExceptionHandler(SqlExceptionHelper.class)
#ResponseStatus(value = HttpStatus.CONFLICT)
public Map<String, String> handleConstraintViolationException(SqlExceptionHelper ex) {
// write your own logic to return user friendly response
}
}
Let's say I have a repository like:
public interface MyRepository extends PagingAndSortingRepository<MyEntity, String> {
#Query("....")
Page<MyEntity> findByCustomField(#Param("customField") String customField, Pageable pageable);
}
This works great. However, if the client sends a formed request (say, searching on a field that does not exist), then Spring returns the exception as JSON. Revealing the #Query, etc.
// This is OK
http://example.com/data-rest/search/findByCustomField?customField=ABC
// This is also OK because "secondField" is a valid column and is mapped via the Query
http://example.com/data-rest/search/findByCustomField?customField=ABC&sort=secondField
// This throws an exception and sends the exception to the client
http://example.com/data-rest/search/findByCustomField?customField=ABC&sort=blahblah
An example of the exception thrown and sent to client:
{
message:null,
cause: {
message: 'org.hibernate.QueryException: could not resolve property: blahblah...'
}
}
How can I handle those exceptions? Normally, I use the #ExceptionHandler for my MVC controllers but I'm not using a layer between the Data Rest API and the client. Should I?
Thanks.
You could use a global #ExceptionHandler with the #ControllerAdvice annotation. Basically, you define which Exception to handle with #ExceptionHandler within the class with #ControllerAdvice annotation, and then you implement what you want to do when that exception is thrown.
Like this:
#ControllerAdvice(basePackageClasses = RepositoryRestExceptionHandler.class)
public class GlobalExceptionHandler {
#ExceptionHandler({QueryException.class})
public ResponseEntity<Map<String, String>> yourExceptionHandler(QueryException e) {
Map<String, String> response = new HashMap<String, String>();
response.put("message", "Bad Request");
return new ResponseEntity<Map<String, String>>(response, HttpStatus.BAD_REQUEST); //Bad Request example
}
}
See also: https://web.archive.org/web/20170715202138/http://www.ekiras.com/2016/02/how-to-do-exception-handling-in-springboot-rest-application.html
You could use #ControllerAdvice and render the content your way. Here is tutorial if you need know how to work on ControllerAdvice, just remember to return HttpEntity
I have a REST service for accounts. The controller calls the Service layer to find Accounts. AccountService can throw domain exceptions. It is usually a case related to client input error.In such a situation, I want to wrap the domain exception with ClientException. Is there a way that client can be presented with status code 400 and just the exception message? Or is there a better to handle the situation where the service layer detects an illegal argument?
#Controller
class AccountController
#ResponseBody
#RequestMapping("/accounts/${accountId}")
public Account account(#PathVariable int accountId, HttpServletResponse response){
try{
return accountService.find("Account not found with id: " + accountId);
}catch(Exception e){
response.setStatus(400);
throw new ClientException(e);
}
}
}
class AccountService{
public Account find(int accountId){
if(acountId > 100){
throw new AccountNotFoundException(accountId);
}else{
return new Account(accountId, "someone");
}
}
}
If this is a REST service, then simply setting the response status and a JSON body (with error message) should be enough. There's no real need to throw that ClientException.
You can create an exception handler that takes the exception and marshals it into a JSON response object.
I did this some time ago, and it worked fine, however I know longer have the source-code to share. .
Look at Spring MVC's exception handlers as a starting point.