Throwing custom exceptions in multi-layered spring project - spring

I am trying to handle the use case where there's a violation for an unique field (for example, the username/mail), would it be correct to handle it like so? (I am using jdbcInsert on the dao layer)
#Transactional
#Override
public User register(String name, String surname, String username, String email, String password) {
User user = null;
try {
user = userDao.register(name, surname, username,
email, passwordEncoder.encode(password));
} catch (DuplicateKeyException duplicateKeyException) {
throw new DuplicateUserException(duplicateKeyException.getMessage());
} catch (DataAccessException dataAccessException) {
throw new SystemUnavailableException(dataAccessException.getMessage());
}
return user;
}
And catching my custom exceptions in the controller:
#ControllerAdvice
public class ErrorControllerAdvice {
#ExceptionHandler(DuplicateUserException.class)
public ModelAndView keyViolation(DuplicateUserException ex) {
ModelAndView mav = new ModelAndView("admin/user/new");
mav.addObject("duplicateMessage", ex.getErrorMessage());
return mav;
}
#ExceptionHandler(SystemUnavailableException.class)
#ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR)
public ModelAndView unexpectedDatabaseError(SystemUnavailableException ex) {
LOGGER.error(ex.getErrorMessage());
return new ModelAndView("500");
}
}

Looks fine to me. Your custom exceptions live at a different level of abstraction, which gives them good reason for existing.
You might consider handling the exceptions in your controller, instead of using an Error translator class (ErrorControllerAdvice) though. This makes things more explicit and limit surprises about how exceptions are handled.

Related

How to handle exceptions thrown in the service layer?

I'm working on a spring-boot application. I tried handling exceptions .But i guess there is something wrong about how I'm doing it because it always throws internal server error 500.
I tried setting up custom exception classes and also used response status codes with #ResponseStatus. But regardless of what the exception is it throws an internal server error only.
I'm using intellij and the message i've given in the exception is printed there but the response body is empty.This i guess must be because it is throwing an internal server error.
Controller class
#RequestMapping(value = "/attendance",method = RequestMethod.POST)
public ResponseEntity<?> enterAttendance(#RequestBody ViewDTO viewDTO) throws CustomException{
return new ResponseEntity<>(tempResultServices.handleAttendance(viewDTO),HttpStatus.OK);
}
}
Service layer
#Override
public TempResult handleAttendance(ViewDTO viewDTO) throws CustomException {
TempIdentity tempIdentity=new TempIdentity();
tempIdentity.setRegistrationNo(viewDTO.getRegistrationNo());
tempIdentity.setCourseId(viewDTO.getCourseId());
tempIdentity.setYear(viewDTO.getYear());
tempIdentity.setSemester(viewDTO.getSemester());
User user=userService.findByUserId(viewDTO.getUserId());
tempIdentity.setUser(user);
if(!viewDTO.isAttendance()){
TempResult tempResultUser =new TempResult(tempIdentity,viewDTO.isAttendance(),0);
ResultIdentity resultIdentity=new ResultIdentity(tempIdentity.getRegistrationNo(),tempIdentity.getCourseId(),tempIdentity.getYear(),tempIdentity.getSemester());
Result result=new Result(resultIdentity,0,"E*");
AttendanceDraft attendanceDraft=atteDraftService.findDraft(viewDTO.getRegistrationNo(),viewDTO.getCourseId(),viewDTO.getYear(),viewDTO.getSemester(),viewDTO.getUserId());
if(attendanceDraft!=null){
attendanceDraft.setStatus(true);
atteDraftService.save(attendanceDraft);
//atteDraftService.delete(attendanceDraft);
tempResultRepository.save(tempResultUser);
resultRepository.save(result);
return tempResultUser;
}
else{
throw new CustomException("No draft available");
}
}
else{
TempResult tempResultUser =new TempResult(tempIdentity,viewDTO.isAttendance());
AttendanceDraft attendanceDraft=atteDraftService.findDraft(viewDTO.getRegistrationNo(),viewDTO.getCourseId(),viewDTO.getYear(),viewDTO.getSemester(),viewDTO.getUserId());
if(attendanceDraft!=null){
attendanceDraft.setStatus(true);
atteDraftService.save(attendanceDraft);
//atteDraftService.delete(attendanceDraft);
tempResultRepository.save(tempResultUser);
return tempResultUser;
}
else{
throw new CustomException("No draft available");
}
}
}
The exception class
#ResponseStatus(code= HttpStatus.NOT_FOUND)
public class CustomException extends RuntimeException {
public CustomException(String message){
super(message);
}
}
The terminal in the intellij prints "No draft available ". But i want it not as an internal server error.
Can some one tell me how i should be handling these errors please?
I tried using the #RestControllerAdvice
#RestControllerAdvice
public class WebRestControllerAdvice {
#ExceptionHandler(CustomException.class)
public ResponseMsg handleNotFoundException(CustomException ex) {
ResponseMsg responseMsg = new ResponseMsg(ex.getMessage());
return responseMsg;
}
}
And this is my response message class
public class ResponseMsg {
private String message;
//getters and setters
}
This is another simple request in the application
#RequestMapping(value = "/user/view",method = RequestMethod.POST)
public ResponseEntity<?> getUser(#RequestBody UserDTO userDTO) throws CustomException{
User user=userService.findByUsername(userDTO.getUsername());
if(user!=null){
return ResponseEntity.ok(user);
}
//
throw new CustomException("User not found");
}
But still the custom exception is not thrown. The response body is empty. but intellij says "user not found" and postman returns the status code 500.
Spring boot has a very convenient way to handle exceptions in any layer of your application which is defining a #ControllerAdvice bean. Then you can throw any type of exception in your code and it will be "captured" on this class.
After this you can handle and return whatever your app needs to return.
By the way, you can return your custom object and it will be parsed to json automatically.
Documentation: https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/
Sample code:
#ControllerAdvice
public class ErrorHandler {
#ExceptionHandler(BadRequestException.class)
#ResponseStatus(HttpStatus.BAD_REQUEST)
#ResponseBody
public Object processValidationError(BadRequestException ex) {
//return whatever you need to return in your API
}
}

Handling Form Validation Result in ErrorHandler

I use spring-boot as a backend server. It has tens of Action Methods. As usual Some of them contains validation. Actually I use BindingResult and returns validation error for returning Http 400 Status.
#CrossOrigin
#RestController
public class ValidationTestController {
#RequestMapping(value = {"/validation-test", "/validation-test/"}, method = RequestMethod.POST)
#ResponseBody
public ResponseEntity<String> login(#RequestBody #Valid final TestData data, final BindingResult result) {
if (result.hasErrors()) {
return new ResponseEntity<>("Sorry incoming data is not valid!", HttpStatus.BAD_REQUEST);
}
return new ResponseEntity<>("OK!", HttpStatus.OK);
}
private static final class TestData {
#NotNull
private String value;
}
}
My aim is removing follpwing lines:
if (result.hasErrors()) {
return new ResponseEntity<>("Sorry incoming data is not valid!", HttpStatus.BAD_REQUEST);
}
IMHO it's a cross cutting concern like Authentication and Auditing. I want to handle it in a one global ErrorHandler Method. It's possible to throw a CustomValidationException Before executing the method. So I can handle the exception in ErrorController.
Yes, you can centralize the exception handling logic at one place, using #ExceptionHandler which is a ControllerAdvice from Spring.
You can look at here

Spring Validator and BindingResult - How to set different HttpStatus Codes?

Dear Spring Community,
I am building my project using Spring. In my API layer, I am leveraging the Validator interface in order to do some custom validation and set an error.
#Override
public void validate(Object obj, Errors e) {
SignUpRequest signUpRequest = (SignUpRequest) obj;
User user = userService.getUserByEmail(signUpRequest.getEmail());
if (user != null) {
e.rejectValue("user", ErrorCodes.USER_EXIST, "user already exist");
}
}
Now, in my API signature, since I am using the BindingResult object, in the #ControllerAdvice that I have, if the user provides an empty value for an attribute of my DTO object, I wont be able to get into the #ExceptionHandler(MethodArgumentNotValidException.class).
What this means is that, I wont be able to throw an HttpStatus.BAD_REQUEST for any empty value provided.
In the above case of my validator, It wont be a HttpStatus.BAD_REQUEST but rather it will be a HttpStatus.OK. So my problem is that, how do I provide different HttpStatus types based on the errors I am getting from my validator? Also is there a way to have the empty value still get picked up by the #ExceptionHandler(MethodArgumentNotValidException.class) in my #ControllerAdvice and have my other custom validations picked up by the bindingResult?
I hope I am clear on the question. Appreciate any help!
OK, I believe I came up with the solution!
In order to have different HttpStatus being thrown base on the type of error you have, you need to have custom exceptions. Then have your custom exceptions thrown inside your Validator. Your ControllerAdvice should register to pick up the custom exceptions and act upon them accordingly.
For example the Validator should have something like this:
if (!matcher.matches()) {
e.rejectValue("email", ErrorCodes.EMAIL_INVALID, "email address is invalid");
throw new BadRequestException("email address is invalid", e);
}
User user = userService.getUserByEmail(signUpRequest.getEmail());
if (user != null) {
e.rejectValue("email", ErrorCodes.USER_EXIST, "user already exist");
throw new ValidationException("User Exist", e);
}
And the Controller Advice should have:
#ExceptionHandler(ValidationException.class)
#ResponseBody
public ResponseEntity<Map<String, Object>> handleValidationException(
ValidationException validationException) {
Map<String, Object> result = createErrorResponse(validationException.getErrors());
return new ResponseEntity<Map<String, Object>>(result, HttpStatus.OK);
}
#ExceptionHandler(BadRequestException.class)
#ResponseBody
public ResponseEntity<Map<String, Object>> handleBadRequestException(
BadRequestException badRequestException) {
Map<String, Object> result = createErrorResponse(badRequestException.getErrors());
return new ResponseEntity<Map<String, Object>>(result, HttpStatus.BAD_REQUEST);
}
This way, you can have different HttpStatus returned base on the type of error you have.

Spring-MVC using a Converter to load object from path variable, but need to return 404 for unfound

TL;DR - Is there a way to throw an error from a registered type converter during the MVC databinding phase such that it will return a response with a specific HTTP status code? I.e. if my converter can't find an object from the conversion source, can I return a 404?
I have a POJO:
public class Goofball {
private String id = "new";
// others
public String getName () { ... }
public void setName (String name) { ... }
}
and am using a StringToGoofballConverter to create an empty object when "new".equals(id) or try to load a Goofball from the database if it exists:
public Goofball convert(String idOrNew) {
Goofball result = null;
log.debug("Trying to convert " + idOrNew + " to Goofball");
if ("new".equalsIgnoreCase(idOrNew))
{
result = new Goofball ();
result.setId("new");
}
else
{
try
{
result = this.repository.findOne(idOrNew);
}
catch (Throwable ex)
{
log.error (ex);
}
if (result == null)
{
throw new GoofballNotFoundException(idOrNew);
}
}
return result;
}
That converter is used by spring when the request matches this endpoint:
#RequestMapping(value = "/admin/goofballs/{goofball}", method=RequestMethod.POST)
public String createOrEditGoofball (#ModelAttribute("goofball") #Valid Goofball object, BindingResult result, Model model) {
// ... handle the post and save the goofball if there were no binding errors, then return the template string name
}
This all works quite well insofar as GET requests to /admin/goofballs/new and /admin/goofballs/1234 work smoothly in the controller for both creating new objects and editing existing ones. The hitch is that if I issue a request with a bogus id, one that isn't new and also doesn't exist in the database I want to return a 404. Currently the Converter is throwing a custom exception:
#ResponseStatus(value= HttpStatus.NOT_FOUND, reason="Goofball Not Found") //404
public class GoofballNotFoundException extends RuntimeException {
private static final long serialVersionUID = 422445187706673678L;
public GoofballNotFoundException(String id){
super("GoofballNotFoundException with id=" + id);
}
}
but I started with a simple IllegalArgumentException as recommended in the Spring docs. In either case, the result is that Spring is returning a response with an HTTP status of 400.
This makes me think I'm misusing the Converter interface but that approach appears to be recommended by the #ModelAttribute docs.
So, again the question: is there a way to throw an error from a registered type converter during the databinding phase such that it will return a response with a specific HTTP status code?
Answering my own question:
Change StringToGoofballConverter to simply return null for the unfound entity instead of throwing IllegalArgumentException or a custom exception. The #Controller method will then be given a Goofball object that has a null id (e.g. the id is not "new" nor the path element value). At that point I can throw a GoofballNotFoundException or any other #ResponseStatus exception from there, within the controller method to affect the response status code.

Spring MVC handle Exceptions and how to show in same view

If I handle exceptions with #ControllerAdvice and #ExceptionHandler How can I show error message to user in the same View. For ex. suppose user in "customer/new" view. and invoke "save" action, then it will go to controller and I call methods service layer. but if internal exception occurred in service layer, I want to show error message on same "customer/new" View.
I have written a separate class to handle exceptions as follow.
#ControllerAdvice
public class DefaultControllerHandler {
#ExceptionHandler({MyProjectException.class, DataAccessException.class})
public ResponseEntity<String> handleInternalErrorException(Exception e) {
logger.error(e.getMessage(), e);
return new ResponseEntity<String>(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
Appreciate your ideas.
Thanks!
You can use flash redirect attributes.
#RequestMapping(value = "/administrator/users", method = RequestMethod.POST)
public String adminAddUser(#ModelAttribute("user") #Valid User user, BindingResult bindingResult, Model model, RedirectAttributes redirectAttrs) {
String redirectUrl = "/administrator/users";
try {
userService.save(user);
} catch (YourServiceException e) {
redirectAttrs.addFlashAttribute("errorMessage", "error occured: " + e.getMessage());
redirectAttrs.addFlashAttribute("userObject", user);
redirectUrl = "/administrator/users?form"; // If error - return to same view
}
return "redirect:" + redirectUrl;
}
#RequestMapping(value = "/administrator/users", params = "form", method = RequestMethod.GET, produces = "text/html")
public String adminUsersList(#ModelAttribute("errorMessage") final String errorMessage, #ModelAttribute("userObject") final User user Model model) {
if(user == null) {
user = new User();
}
model.addAttribute("user", user);
if(errorMessage != null) {
model.addAttribure("errorMessage", errorMessage);
}
return "administrator/users/create";
}
In that case you must have section on your users.jsp page to show errorMessaage. Something like this:
<c:if test="${not empty errorMessage}">${errorMessage}</c:if>
If you can fetch url path and redirectAttributes from controller method - you can do this through #ControllerAdvice
The only solution I can think about is make your call to the service layer AJAX and then redirect only if there are no errors, if not display the error message.
It might look something like this in your Javascript file
$("#someButton").click(function(){
//make your ajax call
if (noError){
window.location.href = contextPath+"/somePath";
}
else{
//display your error message
}
});

Resources