I'm using Spring along with Spring boot to make a REST API.
I'm almost done implementing my first endpoints but I have a problem with validation and error handling.
I first found a way to do the validation by creating a #ControllerAdvice class as follow:
#ControllerAdvice
public class RestErrorHandler {
private MessageSource messageSource;
#Autowired
public RestErrorHandler(MessageSource messageSource) {
this.messageSource = messageSource;
}
#ExceptionHandler(MethodArgumentNotValidException.class)
#ResponseStatus(HttpStatus.BAD_REQUEST)
#ResponseBody
public ValidationErrorDTO processValidationError(MethodArgumentNotValidException ex) {
BindingResult result = ex.getBindingResult();
List<FieldError> fieldErrors = result.getFieldErrors();
return processFieldErrors(fieldErrors);
}
private ValidationErrorDTO processFieldErrors(List<FieldError> fieldErrors) {
ValidationErrorDTO dto = new ValidationErrorDTO();
for (FieldError fieldError: fieldErrors) {
String localizedErrorMessage = resolveLocalizedErrorMessage(fieldError);
dto.addFieldError(fieldError.getField(), localizedErrorMessage);
}
return dto;
}
private String resolveLocalizedErrorMessage(FieldError fieldError) {
Locale currentLocale = LocaleContextHolder.getLocale();
String localizedErrorMessage = messageSource.getMessage(fieldError, currentLocale);
//If the message was not found, return the most accurate field error code instead.
//You can remove this check if you prefer to get the default error message.
if (localizedErrorMessage.equals(fieldError.getDefaultMessage())) {
String[] fieldErrorCodes = fieldError.getCodes();
localizedErrorMessage = fieldErrorCodes[0];
}
return localizedErrorMessage;
}
}
Then create a messages.properties:
Size.userWithPasswordDTO.password=The password should be between 8 and 250 characters
And finally adding the #Size and #NotNull annotations in the DTO file and the #Valid annotations in the endpoint.
The validation of the DTO works well like this, but I'm trying to find a way to handle errors in the db. For example a duplicate user.
In the official documentation, they make a class like this:
#ResponseStatus(HttpStatus.NOT_FOUND)
class UserNotFoundException extends RuntimeException {
public UserNotFoundException(String userId) {
super("could not find user '" + userId + "'.");
}
}
That handles the exceptions thrown in the endpoints.
Now my question is, how do you make the two methods return the same DTOs?
I was also wondering if the annotations #NotNull and #Size should also be in the Entities as well as in the DTOs?
EDIT
I tried updating my #ControllerAdvice to handle DataIntegrityViolationException but it doesn't seem to work. Also, I don't know what I should put inside the FieldError class.
#ControllerAdvice
public class RestErrorHandler {
private MessageSource messageSource;
#Autowired
public RestErrorHandler(MessageSource messageSource) {
this.messageSource = messageSource;
}
#ExceptionHandler(MethodArgumentNotValidException.class)
#ResponseStatus(HttpStatus.BAD_REQUEST)
#ResponseBody
public ValidationErrorDTO processValidationError(MethodArgumentNotValidException ex) {
BindingResult result = ex.getBindingResult();
List<FieldError> fieldErrors = result.getFieldErrors();
return processFieldErrors(fieldErrors);
}
#ExceptionHandler(DataIntegrityViolationException.class)
#ResponseStatus(HttpStatus.CONFLICT)
#ResponseBody
public ValidationErrorDTO processDuplicateError(DataIntegrityViolationException ex) {
//BindingResult result = ex.getMessage();
List<FieldError> fieldErrors = new ArrayList<>();
fieldErrors.add(new FieldError("duplicate", "email", "error"));
return processFieldErrors(fieldErrors);
}
private ValidationErrorDTO processFieldErrors(List<FieldError> fieldErrors) {
ValidationErrorDTO dto = new ValidationErrorDTO();
for (FieldError fieldError: fieldErrors) {
String localizedErrorMessage = resolveLocalizedErrorMessage(fieldError);
dto.addFieldError(fieldError.getField(), localizedErrorMessage);
}
return dto;
}
private String resolveLocalizedErrorMessage(FieldError fieldError) {
Locale currentLocale = LocaleContextHolder.getLocale();
String localizedErrorMessage = messageSource.getMessage(fieldError, currentLocale);
//If the message was not found, return the most accurate field error code instead.
//You can remove this check if you prefer to get the default error message.
if (localizedErrorMessage.equals(fieldError.getDefaultMessage())) {
String[] fieldErrorCodes = fieldError.getCodes();
localizedErrorMessage = fieldErrorCodes[0];
}
return localizedErrorMessage;
}
}
Related
The controller:
#RestController
#RequestMapping(value = "/test", produces = MediaType.APPLICATION_JSON_VALUE)
#Validated
public class ApiController {
#PostMapping(value = "/in",
consumes = MediaType.APPLICATION_JSON_VALUE)
ResponseEntity<InitResponse> inPost(
#ApiParam(required = true) #Valid #RequestBody InRequest inRequest) {
LOG.info("inPost request was received = {}", inRequest);
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
}
The exception handler:
#ControllerAdvice(assignableTypes = ApiController .class, annotations = RestController.class)
public class InExceptionHandler {
#ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<INErrors> handleConstraintViolation(ConstraintViolationException ex) {
LOG.info("handleConstraintViolation was trigerred");
INError INError = new INError(HttpStatus.BAD_REQUEST.toString(), ex.getLocalizedMessage());
return new ResponseEntity<>(new INErrors(), HttpStatus.BAD_REQUEST);
}
#ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<INErrors> handleMethodArgumentConstraintViolation(MethodArgumentNotValidException ex) {
BindingResult result = ex.getBindingResult();
List<FieldError> fieldErrors = result.getFieldErrors();
return new ResponseEntity<>(processFieldErrors(fieldErrors), HttpStatus.BAD_REQUEST);
}
}
If the InRequest has all fields within the javax validation constraint then I get the right code, but when a field doesn't match validation I just get 400 response code.
There are other exception handlers defined but I've put breakpoints everywhere and nothing is triggered.
I also added the log4j property:
log4j.logger.org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod=DEBUG,stdout
but this didn't produce any additional output when debugging.
I'm expecting to also to have the INErrors object sent back, but it doesn't even enter either one of the 2 handling methods.
This is happening because Spring's default exception handler handles all WebMvc's standard exceptions by itself and then delegates unhandled exceptions to user-defined #ExceptionHandler methods.
In your case #Valid constraint violation throws Spring's MethodArgumentNotValidException which is handled by ResponseEntityExceptionHandler#handleMethodArgumentNotValid. So, to change the default behaviour for this exception, you need to override this method in your #ControllerAdivce.
#ControllerAdvice(assignableTypes = ApiController .class, annotations = RestController.class)
public class InExceptionHandler extends ResponseEntityExceptionHandler {
#Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
BindingResult result = ex.getBindingResult();
List<FieldError> fieldErrors = result.getFieldErrors();
return ResponseEntity.badRequest().body(processFieldErrors(fieldErrors));
}
}
EDIT: I saw that you're using both assignableTypes and annotations for #ControllerAdvice exception handler. This makes Spring register one exception handler for all #RestControllers. Try using either assignableTypes or annotations.
As an option, you can create your custom annotation for different exception handlers.
Following code prints "one" when invalid data supplied to /one and "two" when data was sent to "/two".
#RestController
#Target(ElementType.TYPE)
#Retention(RetentionPolicy.RUNTIME)
#interface One {}
#RestController
#Target(ElementType.TYPE)
#Retention(RetentionPolicy.RUNTIME)
#interface Two {}
#One
class ControllerOne {
#PostMapping("one")
String a(#RequestBody #Valid Data data) {
return data.value;
}
}
#Two
class ControllerTwo {
#PostMapping("two")
String a(#RequestBody #Valid Data data) {
return data.value;
}
}
#ControllerAdvice(annotations = One.class)
class HandlerOne extends ResponseEntityExceptionHandler {
#Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
return ResponseEntity.badRequest().body("one");
}
}
#ControllerAdvice(annotations = Two.class)
class HandlerTwo extends ResponseEntityExceptionHandler {
#Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
return ResponseEntity.badRequest().body("two");
}
}
The javax annotation style validation for the model worked for me only when I added the method inside the controller :
public class ApiController {
...
#ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<INErrors> handleConstraintViolation(MethodArgumentNotValidException exception) {
BindingResult result = exception.getBindingResult();
List<FieldError> fieldErrors = result.getFieldErrors();
return new ResponseEntity<>(processFieldErrors(fieldErrors), HttpStatus.BAD_REQUEST);
}
}
everyone!
I making a defense against password brute force.
I successfully handle AuthenticationFailureBadCredentialsEvent when the user writes the right login and wrong password. But the problem is that I want to return JSON with two fields
{
message : '...' <- custom message
code : 'login_failed'
}
The problem is that it returns standart forbidden exception, but I need custom json.
#Log4j2
#Component
#RequiredArgsConstructor
public class AuthenticationAttemptsHandler {
protected final MessageSource messageSource;
private final AuthenticationAttemptsStore attemptsStore;
private final UserDetailsService userDetailsService;
private final UserDetailsLockService userDetailsLockService;
#EventListener
public void handleFailure(AuthenticationFailureBadCredentialsEvent event) {
val authentication = event.getAuthentication();
val userDetails = findUserDetails(authentication.getName());
userDetails.ifPresent(this::failAttempt);
}
private Optional<UserDetails> findUserDetails(String username) {
...
}
private void failAttempt(UserDetails details) {
val username = details.getUsername();
val attempt = attempt(loginAttemptsProperties.getResetFailuresInterval());
int failures = attemptsStore.incrementFailures(username, attempt);
if (failures >= 2) {
Instant lockedUntil = Instant.now().plus(loginAttemptsProperties.getLockDuration());
userDetailsLockService.lockUser(username, lockedUntil);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm");
String date = formatter.format(lockedUntil);
String message = String.format("Account will locked till %s", date);
throw new SecurityException(message);
//FailAttemptsExceptionResponse response = new FailAttemptsExceptionResponse(message, //
//"login_ failed"); <---- tryed return entity from this method. Does not work.
// return new ResponseEntity<>(response,HttpStatus.FORBIDDEN);
} else {
String message = String.format("You have %s attempts.", (3 - failures));
// FailAttemptsExceptionResponse response = new FailAttemptsExceptionResponse(message,
"login_ failed");
throw new SecurityException(message);
// return new ResponseEntity<>(response,HttpStatus.FORBIDDEN);
}
}
}
RuntimeException returns 500 status? but I need forbidden
public class SecurityException extends RuntimeException {
private static final long serialVersionUID = 1L;
public SecurityException(String msg) {
super(msg);
}
}
Responce model
public class FailAttemptsExceptionResponse {
String message;
String code;
public FailAttemptsExceptionResponse(String message, String code) {
super();
this.message = message;
this.code = code;
}
public String getMessage() {
return message;
}
public String getCode() {
return code;
}
}
Tried to handle SecurityException and then returns model? but it does not work
#ControllerAdvice
public class SeurityAdvice extends ResponseEntityExceptionHandler {
#ExceptionHandler(SecurityException.class)
public ResponseEntity<FailAttemptsExceptionResponse> handleNotFoundException(SecurityException ex) {
FailAttemptsExceptionResponse exceptionResponse = new FailAttemptsExceptionResponse(ex.getMessage(),
"login_ failed");
return new ResponseEntity<FailAttemptsExceptionResponse>(exceptionResponse,
HttpStatus.NOT_ACCEPTABLE);
}
}
I successfully handle AuthenticationFailureBadCredentialsEvent, but how can I return JSON response model from the handler with a custom message?
#ControllerAdvice
public class SeurityAdvice extends ResponseEntityExceptionHandler {
#ExceptionHandler(SecurityException.class)
public ResponseEntity<FailAttemptsExceptionResponse> handleNotFoundException(SecurityException ex, HttpServletResponse response) {
FailAttemptsExceptionResponse exceptionResponse = new FailAttemptsExceptionResponse(ex.getMessage(),
"login_ failed");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
return new ResponseEntity<FailAttemptsExceptionResponse>(exceptionResponse,
HttpStatus.NOT_ACCEPTABLE);
}
}
maybe you need to add HttpServletResponse and set the http status.
Register the entry point
As mentioned, I do it with Java Config. I just show the relevant configuration here, there should be other configuration such as session stateless, etc.
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling().authenticationEntryPoint(new CustomEntryPoint());
}
}
U can create AuthenticationEntryPoint.
Короч тут почитай xD
Handle spring security authentication exceptions with #ExceptionHandler
given the following dto and controller
public class PasswordCredentials implements AuthenticationProvider {
#NotNull
#NotEmpty
#JsonProperty( access = JsonProperty.Access.WRITE_ONLY )
private String user;
#NotNull
#NotEmpty
#JsonProperty( access = JsonProperty.Access.WRITE_ONLY )
private CharSequence pass;
public void setPass( final CharSequence pass ) {
this.pass = pass;
}
public void setUser( final String user ) {
this.user = user;
}
#Override
public Authentication toAuthentication() {
return new UsernamePasswordAuthenticationToken( user, pass );
}
}
#RestController
#RequestMapping( path = "authentication" )
class AuthenticationController {
private final AuthenticationManager am;
AuthenticationController( final AuthenticationManager am ) {
this.am = am;
}
#RequestMapping( path = "password", method = RequestMethod.POST, consumes = {
"!" + MediaType.APPLICATION_FORM_URLENCODED_VALUE
} )
ResponseEntity<?> login( #Valid #RequestBody final PasswordCredentials credentials ) {
Authentication authenticate = am.authenticate( credentials.toAuthentication() );
if ( authenticate.isAuthenticated() ) {
return ResponseEntity.status( HttpStatus.NO_CONTENT ).build();
}
return ResponseEntity.status( HttpStatus.FORBIDDEN ).build();
}
}
if for example pass is null there will be a validation error, and a 400 will happen without ever calling my controller, which is fine. That 400 however has no content, is there any way to have the controllers BindResults output as content so that the consumer of the API knows what caused the problem? Ideally I would not do this in the controller method, so that it would happen on all controllers?
I was able to get this behavior with spring data rest as follows, but I'd like it for all API controllers.
class RestConfig extends RepositoryRestConfigurerAdapter {
#Bean
Validator validator() {
return new LocalValidatorFactoryBean();
}
#Override
public void configureValidatingRepositoryEventListener(
final ValidatingRepositoryEventListener validatingListener ) {
Validator validator = validator();
//bean validation always before save and create
validatingListener.addValidator( "beforeCreate", validator );
validatingListener.addValidator( "beforeSave", validator );
}
#Override
public void configureRepositoryRestConfiguration( final RepositoryRestConfiguration config ) {
config.setBasePath( "/v0" );
config.setReturnBodyOnCreate( false );
config.setReturnBodyOnUpdate( false );
}
Spring have #ControllerAdvice and #ExceptionHandler annotation to handle errors in controllers.
#ControllerAdvice
public class ExceptionTranslator {
#ExceptionHandler(MethodArgumentNotValidException.class)
#ResponseStatus(HttpStatus.BAD_REQUEST)
#ResponseBody
public Error processValidationError(MethodArgumentNotValidException ex) {
BindingResult result = ex.getBindingResult();
.....
return new Error();
}
// Other exceptions
}
i want to improve the answer of Anton Novopashin: just return the error in response entity.
#ControllerAdvice
public class ExceptionTranslator {
#ExceptionHandler(MethodArgumentNotValidException.class)
#ResponseBody
public ResponseEntity<String> processValidationError(MethodArgumentNotValidException ex) {
return new ResponseEntity(ex.getMessage, HttpStatus.BAD_REQUEST);
}
// Other exceptions
}
I'm not sure who or why downvoted the existing answers but they are both right - the best way to handle validation errors would be to declare a #ControllerAdvice and then handle the exceptions there. Here's a snippet of my global error handler, taken from an existing project:
#ControllerAdvice
#ResponseBody
public class RestfulErrorHandler {
#ResponseStatus(HttpStatus.BAD_REQUEST)
#ExceptionHandler(MethodArgumentNotValidException.class)
public ErrorResponse methodValidationError(MethodArgumentNotValidException e) {
final ErrorResponse response = new ErrorResponse();
for (ObjectError error : e.getBindingResult().getAllErrors()) {
if (error instanceof FieldError) {
response.addFieldError((FieldError) error);
} else {
response.addGeneralError(error.getDefaultMessage());
}
}
return response;
}
#ResponseStatus(HttpStatus.BAD_REQUEST)
#ExceptionHandler(ConstraintViolationException.class)
public ErrorResponse constraintViolationError(ConstraintViolationException e) {
final ErrorResponse response = new ErrorResponse();
for (ConstraintViolation<?> v : e.getConstraintViolations()) {
response.addFieldError(v.getPropertyPath(), v.getMessage());
}
return response;
}
}
You should also process ConstraintViolationExceptions since they too may be thrown. Here's a link to my ErrorResponse class, I'm including it as a Gist so as not to obscure the main point.
You should also probably process the RepositoryConstraintViolationException, I'm not sure if spring-data-rest handles them already.
I'm working with java and Spring MVC, In the first version of the app I was response with a ResponseEntity<String> and where I haved and error I returned something like return new ResponseEntity<String>(httpErrors.toString(), responseHeaders, HttpStatus.BAD_REQUEST); and when all were right something like return new ResponseEntity<String>(loginResponse.toString(), responseHeaders, HttpStatus.OK);. But now I believe theres is a better way to do it, without using the toString() method, returning the specific object according to the case like this:
#RestController
#RequestMapping("/user")
public class LoginController {
/** The login service to validate the user. */
#Autowired
LoginService loginService;
#RequestMapping(value = "/login", method = RequestMethod.POST)
public #ResponseBody ResponseEntity<?> validate(#RequestBody final UserLog login) {
WebUser webUser = loginService.getUserDetails(login.getLogin(), login.getPassword());
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.setContentType(MediaType.APPLICATION_JSON);
if (webUser == null) {
HttpErrors httpErrors = new HttpErrors(ApiCommonResources.ERROR_402, "error" + "." + ApiCommonResources.ERROR_402, ApiCommonResources.ERROR_402_TEXT);
return new ResponseEntity<HttpErrors>(httpErrors, responseHeaders, HttpStatus.BAD_REQUEST);
}
List<Account> userAccounts = loginService.getMerchantAccounts(webUser.getMerchantId());
// Json Web Token builder
token = "b7d22951486d713f92221bb987347777";
LoginResponse loginResponse = new LoginResponse(ApiCommonResources.SUCCESS_REQUEST_CODE, token);
return new ResponseEntity<LoginResponse>(loginResponse, responseHeaders, HttpStatus.OK);
}
}
The question is how can I create a class that can wraps the LoginResponse as well as HttpErrorsobject types and send it in ? as the returning object in ResponseEntity:
LoginResponse class:
public class LoginResponse{
public LoginResponse(Integer statusCode, String token){
this.setStatusCode(statusCode);
this.setToken(token);
}
private String token;
private Integer statusCode;
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public Integer getStatusCode() {
return statusCode;
}
public void setStatusCode(Integer statusCode) {
this.statusCode = statusCode;
}
#Override
public String toString() {
StringBuilder jsonResponse = new StringBuilder();
jsonResponse.append("{");
jsonResponse.append("\"statusCode\":");
jsonResponse.append("\"" + statusCode + "\",");
jsonResponse.append("\"token\":");
jsonResponse.append("\"" + token + "\"");
jsonResponse.append("}");
return jsonResponse.toString();
}
}
And HttpErrors class:
public class HttpErrors {
public HttpErrors(){
}
public HttpErrors(String errorCode, String errorKey, String errorMessage) {
super();
this.errorCode = errorCode;
this.errorKey = errorKey;
this.errorMessage = errorMessage;
}
private String errorCode;
private String errorKey;
private String errorMessage;
public String getErrorCode() {
return errorCode;
}
public void setErrorCode(String errorCode) {
this.errorCode = errorCode;
}
public String getErrorKey() {
return errorKey;
}
public void setErrorKey(String errorKey) {
this.errorKey = errorKey;
}
public String getErrorMessage() {
return errorMessage;
}
public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}
#Override
public String toString() {
StringBuilder jsonError = new StringBuilder();
jsonError.append("{");
jsonError.append("\"errorCode\":");
jsonError.append("\"" + errorCode + "\",");
jsonError.append("\"errorKey\":");
jsonError.append("\"" + errorKey + "\",");
jsonError.append("\"errorMessage\":");
jsonError.append("\"" + errorMessage + "\"");
jsonError.append("}");
return jsonError.toString();
}
}
public class Response<T> {
private int httpStatus;
private T data;
//getter and setter consructor
eg constructors
public RestResponse(T data){
this(HTTP_OK,data)
}
public RestResponse(int httpStatus,T data){
this.httpStatus = httpStaus;
this.data = data;
}
Now just use this template for all your response objects (repsone objects can be POJOs too)
return new RestEntity<LoginResponse>(loginResponse,statusCode) //loginResponse object
where LoginResponse is
public class LoginResponse {
private String token;
//getter and setter and constructors.
}
You should take some time to establish a REST contracts (Read about it using google :)) and then just follow through using this basic logic. Java and spring are magic together.
Have fun.
maybe try something like this (in my opinion it will be more elegant)
create a method in controller which returns LoginResponse, but firstly perform validation of the input UserLog and once there are any issues, throw a custom exception, which in the end will be caught by the exceptionHandler
take a look at my example controller
#RestController
public class ProductController {
private ProductRequestValidator productRequestValidator;
#InitBinder
public void initBinder(WebDataBinder binder){
binder.addValidators(productRequestValidator);
}
#Autowired
public ProductController(ProductRequestValidator productRequestValidator, ProductService productService) {
this.productRequestValidator = productRequestValidator;
}
#RequestMapping(value = "/products", method = RequestMethod.GET)
public List<ProductResponse> retrieveProducts(#Valid #RequestBody ProductRequest requestProduct, BindingResult bindingResult)
throws ValidationException {
// validate input and throw exception if any error occured
if (bindingResult.hasErrors()){
throw new ValidationException(bindingResult);
}
// business logic
return new ProductRequest();
}
if you want you can check my bitbucket project which has it all implemented:
controller
exceptionHandler
customException
customValidator
I have defined a global exception handling in my Spring Boot based Rest service:
#ControllerAdvice
public class GlobalExceptionController {
private final Logger LOG = LoggerFactory.getLogger(getClass());
#ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR, reason = "Internal application error")
#ExceptionHandler({ServiceException.class})
#ResponseBody
public ServiceException serviceError(ServiceException e) {
LOG.error("{}: {}", e.getErrorCode(), e.getMessage());
return e;
}
}
and a custom ServiceException:
public class ServiceException extends RuntimeException {
private static final long serialVersionUID = -6502596312985405760L;
private String errorCode;
public ServiceException(String message, String errorCode, Throwable cause) {
super(message, cause);
this.errorCode = errorCode;
}
// other constructors, getter and setters omitted
}
so far so good, when an exception is fired the controller works as it should and respond with:
{
"timestamp": 1413883870237,
"status": 500,
"error": "Internal Server Error",
"exception": "org.example.ServiceException",
"message": "somthing goes wrong",
"path": "/index"
}
but the field errorCode isn't shown in the JSON response.
So how can I define a custom exception response in my application.
Spring Boot uses an implementation of ErrorAttributes to populate the Map that's rendered as JSON. By default, an instance of DefaultErrorAttributes is used. To include your custom errorCode you'll need to provide a custom ErrorAttributes implementation that knows about ServiceException and its error code. This custom implementation should be an a #Bean in your configuration.
One approach would be to sub-class DefaultErrorAttributes:
#Bean
public ErrorAttributes errorAttributes() {
return new DefaultErrorAttributes() {
#Override
public Map<String, Object> getErrorAttributes(
RequestAttributes requestAttributes,
boolean includeStackTrace) {
Map<String, Object> errorAttributes = super.getErrorAttributes(requestAttributes, includeStackTrace);
Throwable error = getError(requestAttributes);
if (error instanceof ServiceException) {
errorAttributes.put("errorCode", ((ServiceException)error).getErrorCode());
}
return errorAttributes;
}
};
}
#Alex You can use annotation #ExceptionHandler(YourExceptionClass.class) to handle the specific exception in specific RestController. I think it's a better way to handle complicated scenarios in business applications. Moreover i will suggest you to use custom exception translator to deal with different type of exceptions. You can consider spring oauth2 exception translator as reference code for exception translator.
Note: Following code is only to understand concept of this solution. It's not production ready code. Feel free to discuss more about it.
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
/**
* #author Harpreet
* #since 16-Aug-17.
*/
#RestController
public class RestTestController {
#RequestMapping(value = "name", method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public ResponseObject name(#RequestParam(value="name") String value){
//your custom logic
if (value == null || value.length() < 2) {
//throwing exception to invoke customExceptionHandler
throw new NullPointerException("value of request_param:name is invalid");
}
ResponseObject responseObject = new ResponseObject();
responseObject.setMessage(value)
.setErrorCode(1);
return responseObject;
}
// to handle null pointer exception
#ExceptionHandler(NullPointerException.class)
public ResponseObject customExceptionHandler
(NullPointerException e) {
ResponseObject responseObject = new ResponseObject();
responseObject.setMessage(e.getMessage())
.setErrorCode(-1);
return responseObject;
}
// response data transfer class
static class ResponseObject{
String message;
Integer errorCode;
public String getMessage() {
return message;
}
public ResponseObject setMessage(String message) {
this.message = message;
return this;
}
public Integer getErrorCode() {
return errorCode;
}
public ResponseObject setErrorCode(Integer errorCode) {
this.errorCode = errorCode;
return this;
}
}
}