Merge pull request #6144 from rozagerardo/geroza/BAEL-11599_Update-exception-handling-rest-article

[BAEL-11599] Update and move code for the "Error Handling for REST with Spring" article
This commit is contained in:
Loredana Crusoveanu 2019-01-20 10:11:12 +02:00 committed by GitHub
commit 08a951a4ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 270 additions and 110 deletions

View File

@ -1,3 +1,4 @@
Module for the articles that are part of the Spring REST E-book: Module for the articles that are part of the Spring REST E-book:
1. [Bootstrap a Web Application with Spring 5](https://www.baeldung.com/bootstraping-a-web-application-with-spring-and-java-based-configuration) 1. [Bootstrap a Web Application with Spring 5](https://www.baeldung.com/bootstraping-a-web-application-with-spring-and-java-based-configuration)
2. [Error Handling for REST with Spring](http://www.baeldung.com/exception-handling-for-rest-with-spring)

View File

@ -20,12 +20,30 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId> <artifactId>spring-boot-starter-web</artifactId>
</dependency> </dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId> <artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>net.sourceforge.htmlunit</groupId>
<artifactId>htmlunit</artifactId>
<version>${htmlunit.version}</version>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
<build> <build>
@ -39,5 +57,6 @@
<properties> <properties>
<start-class>com.baeldung.SpringBootRestApplication</start-class> <start-class>com.baeldung.SpringBootRestApplication</start-class>
<htmlunit.version>2.32</htmlunit.version>
</properties> </properties>
</project> </project>

View File

@ -1,4 +1,4 @@
package com.baeldung; package com.baeldung.web;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;

View File

@ -0,0 +1,23 @@
package com.baeldung.web.config;
import java.util.Map;
import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.WebRequest;
@Component
public class MyCustomErrorAttributes extends DefaultErrorAttributes {
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, includeStackTrace);
errorAttributes.put("locale", webRequest.getLocale()
.toString());
errorAttributes.remove("error");
errorAttributes.put("cause", errorAttributes.get("message"));
errorAttributes.remove("message");
errorAttributes.put("status", String.valueOf(errorAttributes.get("status")));
return errorAttributes;
}
}

View File

@ -0,0 +1,31 @@
package com.baeldung.web.config;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.springframework.boot.autoconfigure.web.ErrorProperties;
import org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController;
import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;
@Component
public class MyErrorController extends BasicErrorController {
public MyErrorController(ErrorAttributes errorAttributes) {
super(errorAttributes, new ErrorProperties());
}
@RequestMapping(produces = MediaType.APPLICATION_XML_VALUE)
public ResponseEntity<Map<String, Object>> xmlError(HttpServletRequest request) {
Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.APPLICATION_XML));
body.put("xmlkey", "the XML response is different!");
HttpStatus status = getStatus(request);
return new ResponseEntity<>(body, status);
}
}

View File

@ -0,0 +1,15 @@
package com.baeldung.web.controller;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class FaultyRestController {
@GetMapping("/exception")
public ResponseEntity<Void> requestWithException() {
throw new RuntimeException("Error in the faulty controller!");
}
}

View File

@ -0,0 +1,85 @@
package com.baeldung.web.error;
import javax.persistence.EntityNotFoundException;
import org.hibernate.exception.ConstraintViolationException;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import com.baeldung.web.exception.MyResourceNotFoundException;
@ControllerAdvice
public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
public RestResponseEntityExceptionHandler() {
super();
}
// API
// 400
@ExceptionHandler({ ConstraintViolationException.class })
public ResponseEntity<Object> handleBadRequest(final ConstraintViolationException ex, final WebRequest request) {
final String bodyOfResponse = "This should be application specific";
return handleExceptionInternal(ex, bodyOfResponse, new HttpHeaders(), HttpStatus.BAD_REQUEST, request);
}
@ExceptionHandler({ DataIntegrityViolationException.class })
public ResponseEntity<Object> handleBadRequest(final DataIntegrityViolationException ex, final WebRequest request) {
final String bodyOfResponse = "This should be application specific";
return handleExceptionInternal(ex, bodyOfResponse, new HttpHeaders(), HttpStatus.BAD_REQUEST, request);
}
@Override
protected ResponseEntity<Object> handleHttpMessageNotReadable(final HttpMessageNotReadableException ex, final HttpHeaders headers, final HttpStatus status, final WebRequest request) {
final String bodyOfResponse = "This should be application specific";
// ex.getCause() instanceof JsonMappingException, JsonParseException // for additional information later on
return handleExceptionInternal(ex, bodyOfResponse, headers, HttpStatus.BAD_REQUEST, request);
}
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(final MethodArgumentNotValidException ex, final HttpHeaders headers, final HttpStatus status, final WebRequest request) {
final String bodyOfResponse = "This should be application specific";
return handleExceptionInternal(ex, bodyOfResponse, headers, HttpStatus.BAD_REQUEST, request);
}
// 404
@ExceptionHandler(value = { EntityNotFoundException.class, MyResourceNotFoundException.class })
protected ResponseEntity<Object> handleNotFound(final RuntimeException ex, final WebRequest request) {
final String bodyOfResponse = "This should be application specific";
return handleExceptionInternal(ex, bodyOfResponse, new HttpHeaders(), HttpStatus.NOT_FOUND, request);
}
// 409
@ExceptionHandler({ InvalidDataAccessApiUsageException.class, DataAccessException.class })
protected ResponseEntity<Object> handleConflict(final RuntimeException ex, final WebRequest request) {
final String bodyOfResponse = "This should be application specific";
return handleExceptionInternal(ex, bodyOfResponse, new HttpHeaders(), HttpStatus.CONFLICT, request);
}
// 412
// 500
@ExceptionHandler({ NullPointerException.class, IllegalArgumentException.class, IllegalStateException.class })
/*500*/public ResponseEntity<Object> handleInternal(final RuntimeException ex, final WebRequest request) {
logger.error("500 Status Code", ex);
final String bodyOfResponse = "This should be application specific";
return handleExceptionInternal(ex, bodyOfResponse, new HttpHeaders(), HttpStatus.INTERNAL_SERVER_ERROR, request);
}
}

View File

@ -0,0 +1,21 @@
package com.baeldung.web.exception;
public final class MyResourceNotFoundException extends RuntimeException {
public MyResourceNotFoundException() {
super();
}
public MyResourceNotFoundException(final String message, final Throwable cause) {
super(message, cause);
}
public MyResourceNotFoundException(final String message) {
super(message);
}
public MyResourceNotFoundException(final Throwable cause) {
super(cause);
}
}

View File

@ -0,0 +1,3 @@
### Spring Boot default error handling configurations
#server.error.whitelabel.enabled=false
#server.error.include-stacktrace=always

View File

@ -1,4 +1,4 @@
package com.baeldung.spring.boot.rest; package com.baeldung.web;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;

View File

@ -0,0 +1,65 @@
package com.baeldung.web.error;
import static io.restassured.RestAssured.given;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.hasKey;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.isA;
import static org.hamcrest.Matchers.not;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import com.gargoylesoftware.htmlunit.WebClient;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
public class ErrorHandlingLiveTest {
private static final String BASE_URL = "http://localhost:8080";
private static final String EXCEPTION_ENDPOINT = "/exception";
private static final String ERROR_RESPONSE_KEY_PATH = "error";
private static final String XML_RESPONSE_KEY_PATH = "xmlkey";
private static final String LOCALE_RESPONSE_KEY_PATH = "locale";
private static final String CAUSE_RESPONSE_KEY_PATH = "cause";
private static final String RESPONSE_XML_ROOT = "Map";
private static final String XML_RESPONSE_KEY_XML_PATH = RESPONSE_XML_ROOT + "." + XML_RESPONSE_KEY_PATH;
private static final String LOCALE_RESPONSE_KEY_XML_PATH = RESPONSE_XML_ROOT + "." + LOCALE_RESPONSE_KEY_PATH;
private static final String CAUSE_RESPONSE_KEY_XML_PATH = RESPONSE_XML_ROOT + "." + CAUSE_RESPONSE_KEY_PATH;
private static final String CAUSE_RESPONSE_VALUE = "Error in the faulty controller!";
private static final String XML_RESPONSE_VALUE = "the XML response is different!";
@Test
public void whenRequestingFaultyEndpointAsJson_thenReceiveDefaultResponseWithConfiguredAttrs() {
given().header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.get(EXCEPTION_ENDPOINT)
.then()
.body("$", hasKey(LOCALE_RESPONSE_KEY_PATH))
.body(CAUSE_RESPONSE_KEY_PATH, is(CAUSE_RESPONSE_VALUE))
.body("$", not(hasKey(ERROR_RESPONSE_KEY_PATH)))
.body("$", not(hasKey(XML_RESPONSE_KEY_PATH)));
}
@Test
public void whenRequestingFaultyEndpointAsXml_thenReceiveXmlResponseWithConfiguredAttrs() {
given().header(HttpHeaders.ACCEPT, MediaType.APPLICATION_XML_VALUE)
.get(EXCEPTION_ENDPOINT)
.then()
.body(LOCALE_RESPONSE_KEY_XML_PATH, isA(String.class))
.body(CAUSE_RESPONSE_KEY_XML_PATH, is(CAUSE_RESPONSE_VALUE))
.body(RESPONSE_XML_ROOT, not(hasKey(ERROR_RESPONSE_KEY_PATH)))
.body(XML_RESPONSE_KEY_XML_PATH, is(XML_RESPONSE_VALUE));
}
@Test
public void whenRequestingFaultyEndpointAsHtml_thenReceiveWhitelabelPageResponse() throws Exception {
try (WebClient webClient = new WebClient()) {
webClient.getOptions()
.setThrowExceptionOnFailingStatusCode(false);
HtmlPage page = webClient.getPage(BASE_URL + EXCEPTION_ENDPOINT);
assertThat(page.getBody()
.asText()).contains("Whitelabel Error Page");
}
}
}

View File

View File

@ -18,7 +18,6 @@ The "Learn Spring Security" Classes: http://github.learnspringsecurity.com
- [Metrics for your Spring REST API](http://www.baeldung.com/spring-rest-api-metrics) - [Metrics for your Spring REST API](http://www.baeldung.com/spring-rest-api-metrics)
- [Bootstrap a Web Application with Spring 4](http://www.baeldung.com/bootstraping-a-web-application-with-spring-and-java-based-configuration) - [Bootstrap a Web Application with Spring 4](http://www.baeldung.com/bootstraping-a-web-application-with-spring-and-java-based-configuration)
- [Build a REST API with Spring and Java Config](http://www.baeldung.com/building-a-restful-web-service-with-spring-and-java-based-configuration) - [Build a REST API with Spring and Java Config](http://www.baeldung.com/building-a-restful-web-service-with-spring-and-java-based-configuration)
- [Error Handling for REST with Spring](http://www.baeldung.com/exception-handling-for-rest-with-spring)
- [Spring Security Expressions - hasRole Example](https://www.baeldung.com/spring-security-expressions-basic) - [Spring Security Expressions - hasRole Example](https://www.baeldung.com/spring-security-expressions-basic)

View File

@ -212,23 +212,6 @@
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId> <artifactId>maven-war-plugin</artifactId>
</plugin> </plugin>
<!-- Because we are using custom surefire configs in live profile hence need to disable all other in default profile -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<forkCount>3</forkCount>
<reuseForks>true</reuseForks>
<excludes>
<exclude>**/*IntegrationTest.java</exclude>
<exclude>**/*IntTest.java</exclude>
<exclude>**/*LongRunningUnitTest.java</exclude>
<exclude>**/*ManualTest.java</exclude>
<exclude>**/*LiveTest.java</exclude>
<exclude>**/*TestSuite.java</exclude>
</excludes>
</configuration>
</plugin>
<plugin> <plugin>
<groupId>org.codehaus.cargo</groupId> <groupId>org.codehaus.cargo</groupId>
<artifactId>cargo-maven2-plugin</artifactId> <artifactId>cargo-maven2-plugin</artifactId>
@ -274,32 +257,6 @@
<id>live</id> <id>live</id>
<build> <build>
<plugins> <plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<executions>
<execution>
<phase>integration-test</phase>
<goals>
<goal>test</goal>
</goals>
<configuration>
<excludes>
<exclude>**/*IntegrationTest.java</exclude>
<exclude>**/*IntTest.java</exclude>
</excludes>
<includes>
<include>**/*LiveTest.java</include>
</includes>
</configuration>
</execution>
</executions>
<configuration>
<systemPropertyVariables>
<test.mime>json</test.mime>
</systemPropertyVariables>
</configuration>
</plugin>
<plugin> <plugin>
<groupId>org.codehaus.cargo</groupId> <groupId>org.codehaus.cargo</groupId>
<artifactId>cargo-maven2-plugin</artifactId> <artifactId>cargo-maven2-plugin</artifactId>

View File

@ -1,17 +1,9 @@
package org.baeldung.web.error; package org.baeldung.web.error;
import javax.persistence.EntityNotFoundException;
import org.baeldung.web.exception.MyResourceNotFoundException; import org.baeldung.web.exception.MyResourceNotFoundException;
import org.hibernate.exception.ConstraintViolationException;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest; import org.springframework.web.context.request.WebRequest;
@ -24,61 +16,10 @@ public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionH
super(); super();
} }
// API @ExceptionHandler(value = { MyResourceNotFoundException.class })
// 400
@ExceptionHandler({ ConstraintViolationException.class })
public ResponseEntity<Object> handleBadRequest(final ConstraintViolationException ex, final WebRequest request) {
final String bodyOfResponse = "This should be application specific";
return handleExceptionInternal(ex, bodyOfResponse, new HttpHeaders(), HttpStatus.BAD_REQUEST, request);
}
@ExceptionHandler({ DataIntegrityViolationException.class })
public ResponseEntity<Object> handleBadRequest(final DataIntegrityViolationException ex, final WebRequest request) {
final String bodyOfResponse = "This should be application specific";
return handleExceptionInternal(ex, bodyOfResponse, new HttpHeaders(), HttpStatus.BAD_REQUEST, request);
}
@Override
protected ResponseEntity<Object> handleHttpMessageNotReadable(final HttpMessageNotReadableException ex, final HttpHeaders headers, final HttpStatus status, final WebRequest request) {
final String bodyOfResponse = "This should be application specific";
// ex.getCause() instanceof JsonMappingException, JsonParseException // for additional information later on
return handleExceptionInternal(ex, bodyOfResponse, headers, HttpStatus.BAD_REQUEST, request);
}
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(final MethodArgumentNotValidException ex, final HttpHeaders headers, final HttpStatus status, final WebRequest request) {
final String bodyOfResponse = "This should be application specific";
return handleExceptionInternal(ex, bodyOfResponse, headers, HttpStatus.BAD_REQUEST, request);
}
// 404
@ExceptionHandler(value = { EntityNotFoundException.class, MyResourceNotFoundException.class })
protected ResponseEntity<Object> handleNotFound(final RuntimeException ex, final WebRequest request) { protected ResponseEntity<Object> handleNotFound(final RuntimeException ex, final WebRequest request) {
final String bodyOfResponse = "This should be application specific"; final String bodyOfResponse = "This should be application specific";
return handleExceptionInternal(ex, bodyOfResponse, new HttpHeaders(), HttpStatus.NOT_FOUND, request); return handleExceptionInternal(ex, bodyOfResponse, new HttpHeaders(), HttpStatus.NOT_FOUND, request);
} }
// 409
@ExceptionHandler({ InvalidDataAccessApiUsageException.class, DataAccessException.class })
protected ResponseEntity<Object> handleConflict(final RuntimeException ex, final WebRequest request) {
final String bodyOfResponse = "This should be application specific";
return handleExceptionInternal(ex, bodyOfResponse, new HttpHeaders(), HttpStatus.CONFLICT, request);
}
// 412
// 500
@ExceptionHandler({ NullPointerException.class, IllegalArgumentException.class, IllegalStateException.class })
/*500*/public ResponseEntity<Object> handleInternal(final RuntimeException ex, final WebRequest request) {
logger.error("500 Status Code", ex);
final String bodyOfResponse = "This should be application specific";
return handleExceptionInternal(ex, bodyOfResponse, new HttpHeaders(), HttpStatus.INTERNAL_SERVER_ERROR, request);
}
} }

View File

@ -1,7 +1,7 @@
package org.baeldung; package org.baeldung;
import org.baeldung.persistence.PersistenceTestSuite; import org.baeldung.persistence.PersistenceTestSuite;
import org.baeldung.web.LiveTestSuite; import org.baeldung.web.LiveTestSuiteLiveTest;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.junit.runners.Suite; import org.junit.runners.Suite;
@ -9,8 +9,8 @@ import org.junit.runners.Suite;
@Suite.SuiteClasses({ @Suite.SuiteClasses({
// @formatter:off // @formatter:off
PersistenceTestSuite.class PersistenceTestSuite.class
,LiveTestSuite.class ,LiveTestSuiteLiveTest.class
}) // }) //
public class TestSuite { public class TestSuiteLiveTest {
} }

View File

@ -10,6 +10,6 @@ import org.junit.runners.Suite;
,FooLiveTest.class ,FooLiveTest.class
,FooPageableLiveTest.class ,FooPageableLiveTest.class
}) // }) //
public class LiveTestSuite { public class LiveTestSuiteLiveTest {
} }