Spring Boot 2 – REST 异常





0/5 (0投票)
Spring Boot 2 – REST 异常
本教程可能让你想要更多。我不会给出明确的“如果这样,就那样做”的建议,而是向你展示在使用 Spring Boot 2 开发 Rest 端点时处理异常的三种不同技术。有经验的读者可能会问,为什么还要费心呢?因为 Spring Boot 默认会处理异常并呈现一个漂亮的 Rest 响应。然而,在某些情况下,你可能需要自定义异常处理,本教程将演示三种技术。与本网站上的其他教程一样,这里的“买者自负”原则依然适用……如果你使用不同版本的 Spring Boot,或者更糟的是,Spring 而非 Spring Boot 来遵循本教程,那么请做好进行进一步研究的准备,因为 Spring Boot 2 的主要目的是简化 Spring 开发。通过简化,许多实现细节变得隐藏起来了。
使用 Spring Boot Rest,我们可以通过三种方式处理异常:默认处理、控制器中的异常处理或全局异常处理。在本教程中,我们将探讨处理异常的所有这三种方法。
项目设置
开始之前,请创建你的 Spring Boot 应用程序。如果你是 Spring Boot 新手,那么在尝试本教程之前,你应该参考此处或网上的教程之一。本教程假设你能创建、编译和运行一个 Spring Boot Rest 应用程序。它还假设你知道如何调用 Rest 端点。
- 在 Eclipse 中创建一个新的 Spring Boot Maven 项目。我使用 Spring Initializer 来创建项目。(Spring Initializr 视频,书面教程)
- 将组的值指定为
com.tutorial.exceptions.spring.rest
,将工件的值指定为exceptions-tutorial
。 - 为简单起见,请将 POM 替换为以下内容:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation= "http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.3.RELEASE</version> <relativePath /> <!-- lookup parent from repository --> </parent> <groupId>com.tutorial.exceptions.spring.rest</groupId> <artifactId>exceptions-tutorial</artifactId> <version>0.0.1-SNAPSHOT</version> <name>exceptions-tutorial</name> <description>Tutorial project demonstrating exceptions in Spring Rest. </description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
- 创建一个名为
ExceptionTutorialApplication
的新类,该类扩展SpringBootApplication
并在 main 方法中启动 Spring 应用程序。package com.tutorial.exceptions.spring.rest.exceptionstutorial; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class ExceptionsTutorialApplication { public static void main(String[] args) { SpringApplication.run(ExceptionsTutorialApplication.class, args); } }
- 创建一个名为
HelloGoodbye
的新类。创建三个属性:greeting
、goodbye
和type
。package com.tutorial.exceptions.spring.rest.exceptionstutorial; public class HelloGoodbye { private String greeting; private String goodbye; private String type; public String getType() { return type; } public void setType(String type) { this.type = type; } public String getGoodbye() { return goodbye; } public void setGoodbye(String goodbye) { this.goodbye = goodbye; } public String getGreeting() { return greeting; } public void setGreeting(String greeting) { this.greeting = greeting; } }
- 创建一个名为
GreetingService
的新 Spring 服务。 - 暂时放下疑虑,实现一个名为
createGreeting
的方法,如下所示:package com.tutorial.exceptions.spring.rest.exceptionstutorial; import org.springframework.stereotype.Service; @Service public class GreetingService { public HelloGoodbye createGreeting(String type) { HelloGoodbye helloGoodbye = new HelloGoodbye(); if(type.equals("hello")) { helloGoodbye.setGreeting("Hello there."); } else if(type.equals("goodbye")) { helloGoodbye.setGoodbye("Goodbye for now."); } helloGoodbye.setType(type); return helloGoodbye; } }
- 创建一个新的 Spring Rest 控制器并自动注入
GreetingService
。 - 创建一个名为
getGreeting
的新方法,该方法接受一个名为type
的请求参数,并调用GreetingService
的createGreeting
方法。package com.tutorial.exceptions.spring.rest.exceptionstutorial; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping(value = "/greeting") public class GreetingController { @Autowired protected GreetingService service; @GetMapping("/greet") public HelloGoodbye getGreeting(@RequestParam("type") String type) { HelloGoodbye goodBye = service.createGreeting(type); return goodBye; } }
- 编译并运行应用程序。
- 使用 Curl、
WebBrowser
或 Postman 等其他工具调用 Rest 端点,并将type
的值指定为hello
。https://:8080/greeting/greet?type=hello
- 注意 JSON 响应。
{ "greeting": "Hello there.", "goodbye": null, "type": "hello" }
- 将
type
改为goodbye
,然后再次调用 Rest 端点。https://:8080/greeting/greet?type=goodbye
{ "greeting": null, "goodbye": "Goodbye for now.", "type": "goodbye" }
- 将
type
改为wrong
,然后注意响应。https://:8080/greeting/greet?type=wrong
{ "greeting": null, "goodbye": null, "type": "wrong" }
当为 Rest 端点传递错误的
type
值时,响应并不十分有用。此外,由于greeting
和goodbye
都是null
,响应很可能会导致客户端应用程序抛出NullPointerException
。相反,当向端点传递错误值时,我们应该抛出异常。顺带一提,是的,
HelloGoodbye
的设计很糟糕。返回null
是糟糕的编程实践。更好的选择是这样做。但是,创建设计良好的 POJO 不是本教程的意图。因此,请继续使用上面设计糟糕的HelloGoodbye
实现。public class HelloGoodbye { private String message; private String type; public String getType() { return type; } public void setType(String type) { this.type = type; } public String getMessage() { return message; } public void setMessage(String msg) { this.message = msg; } }
默认异常处理
Spring Boot 默认提供异常处理。这使得服务终端和客户端能够更容易地在没有复杂编码的情况下通信失败。
- 修改
createGreeting
方法,如果type
的值不是hello
或goodbye
,则抛出Exception
。package com.tutorial.exceptions.spring.rest.exceptionstutorial; import org.springframework.stereotype.Service; @Service public class GreetingService { public HelloGoodbye createGreeting(String type) throws Exception { HelloGoodbye helloGoodbye = new HelloGoodbye(); if (type.equals("hello")) { helloGoodbye.setGreeting("Hello there."); } else if (type.equals("goodbye")) { helloGoodbye.setGoodbye("Goodbye for now."); } else { throw new Exception("Valid types are hello or goodbye."); } helloGoodbye.setType(type); return helloGoodbye; } }
- 修改
GreetingController
的getGreeting
方法以抛出Exception
。package com.tutorial.exceptions.spring.rest.exceptionstutorial; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping(value = "/greeting") public class GreetingController { @Autowired protected GreetingService service; @GetMapping("/greet") public HelloGoodbye getGreeting(@RequestParam("type") String type) throws Exception { HelloGoodbye goodBye = service.createGreeting(type); return goodBye; } }
- 编译、运行应用程序,然后访问 Rest 端点。注意响应以 JSON 格式返回错误。
{ "timestamp": "2019-04-06T18:07:34.344+0000", "status": 500, "error": "Internal Server Error", "message": "Valid types are hello or goodbye.", "path": "/greeting/greet" }
当修改
createGreeting
方法时,我们需要捕获异常或抛出异常。这是因为Exception
是一个受检异常(有关受检异常的更多信息)。但将该异常返回给客户端应用程序作为 JSON 没有特殊要求。这是因为 Spring Boot 为错误提供了一个默认的 JSON 错误消息。相关的类是 DefaultErrorAttributes,它实现了ErrorAttributes
接口。当发生异常时,此类提供以下属性:timestamp
、status
、error
、exception
、message
、errors
、trace
和path
。你可以轻松地用自己的错误属性类覆盖默认值;但是,此处不演示此技术。有关编写ErrorAttributes
接口自定义实现的信息,请参阅本教程(使用 ErrorAttributes 自定义错误 JSON 响应)。通常,业务逻辑异常需要业务逻辑异常而不是通用异常。让我们修改代码以抛出自定义异常。
- 创建一个名为
GreetingTypeException
的类,该类扩展Exception
。 - 通过
@ResponseStatus
注解为其分配一个“Bad Request”(错误请求)状态。package com.tutorial.exceptions.spring.rest.exceptionstutorial; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.http.HttpStatus; @ResponseStatus(value = HttpStatus.BAD_REQUEST) public class GreetingTypeException extends Exception { private static final long serialVersionUID = -189365452227508599L; public GreetingTypeException(String message) { super(message); } public GreetingTypeException(Throwable cause) { super(cause); } public GreetingTypeException(String message, Throwable cause) { super(message, cause); } }
- 修改
createGreeting
以抛出GreetingTypeException
而不是Exception
。public HelloGoodbye createGreeting(String type) throws GreetingTypeException { HelloGoodbye helloGoodbye = new HelloGoodbye(); if (type.equals("hello")) { helloGoodbye.setGreeting("Hello there."); } else if (type.equals("goodbye")) { helloGoodbye.setGoodbye("Goodbye for now."); } else { throw new GreetingTypeException("Valid types are hello or goodbye."); } helloGoodbye.setType(type); return helloGoodbye; }
- 编译、运行应用程序,然后访问 Rest 端点。为
type
参数分配一个不正确的值。https://:8080/greeting/greet?type=cc
{ "timestamp": "2019-03-29T01:54:40.114+0000", "status": 400, "error": "Bad Request", "message": "Valid types are hello or goodbye.", "path": "/greeting/greet" }
- 创建一个名为
NameNotFoundException
的异常。让该异常扩展RuntimeException
而不是Exception
。 - 为其分配“Not Found”(未找到)的响应状态。
package com.tutorial.exceptions.spring.rest.exceptionstutorial; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.http.HttpStatus; @ResponseStatus(value = HttpStatus.NOT_FOUND) public class NameNotFoundException extends RuntimeException { public NameNotFoundException(String message) { super("The id: " + message + " could not be found."); } }
- 修改
GreetingService
的createGreeting
方法以接受整数类型的id
。 - 创建一个名为
getPersonName
的新方法。暂时放下疑虑,并按如下方式实现它。显然,在真实项目中,你会从数据库、LDAP 服务器或其他数据存储中获取用户信息。 - 修改
createGreeting
以使用getPersonName
方法来个性化问候语。package com.tutorial.exceptions.spring.rest.exceptionstutorial; import org.springframework.stereotype.Service; @Service public class GreetingService { public HelloGoodbye createGreeting(String type, int id) throws GreetingTypeException { HelloGoodbye helloGoodbye = new HelloGoodbye(); if (type.equals("hello")) { helloGoodbye.setGreeting("Hello there " + this.getPersonName(id)); } else if (type.equals("goodbye")) { helloGoodbye.setGoodbye("Goodbye for now " + this.getPersonName(id)); } else { throw new GreetingTypeException("Valid types are hello or goodbye."); } helloGoodbye.setType(type); return helloGoodbye; } public String getPersonName(int id) { if(id==1) { return "Tom"; } else if(id==2) { return "Sue"; } else { throw new NameNotFoundException(Integer.toString(id)); } } }
- 修改
GreetingController
以接受id
作为请求参数,并修改其对GreetingService
的createGreeting
方法的调用,以将id
也传递给服务。package com.tutorial.exceptions.spring.rest.exceptionstutorial; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping(value = "/greeting") public class GreetingController { @Autowired protected GreetingService service; @GetMapping("/greet") public HelloGoodbye getGreeting(@RequestParam("type") String type, @RequestParam("id") int id) { HelloGoodbye goodBye = service.createGreeting(type, id); return goodBye; } }
- 编译、运行应用程序,然后访问端点。
https://:8080/greeting/greet?type=hello&id=2
{ "greeting": "Hello there Sue", "goodbye": null, "type": "hello" }
- 将
id
查询参数的值更改为六,然后注意异常。https://:8080/greeting/greet?type=hello&id=6
{ "timestamp": "2019-03-31T20:30:18.727+0000", "status": 404, "error": "Not Found", "message": "The id: 6 could not be found.", "path": "/greeting/greet" }
顺带一提,请注意,我们让 NameNotFoundException
扩展了 RuntimeException
而不是 Exception
。通过这样做,我们使 NameNotFoundException
成为一个非受检异常(有关非受检异常的更多信息),并且不需要处理该异常。
控制器错误处理器
尽管 Spring Boot 的默认异常处理非常健壮,但有时应用程序可能需要更自定义的错误处理。一种技术是在 Rest 控制器中声明一个异常处理方法。这是使用 Spring 的 @ExceptionHandler
注解来实现的(javadoc)。
- 创建一个名为
GreetingError
的新简单类。请注意,它是一个 POJO,不扩展Exception
。package com.tutorial.exceptions.spring.rest.exceptionstutorial; import java.util.Date; public class GreetingError { private Date timestamp; private String message; public Date getTimestamp() { return timestamp; } public void setTimestamp(Date timestamp) { this.timestamp = timestamp; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } }
- 修改
GreetingController
以拥有一个名为nameNotFoundException
的方法,该方法被@ExceptionHandler
注解。 - 实现
nameNotFoundException
以返回一个ResponseEntity<>
。package com.tutorial.exceptions.spring.rest.exceptionstutorial; import java.util.Date; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.request.WebRequest; @RestController @RequestMapping(value = "/greeting") public class GreetingController { @Autowired protected GreetingService service; @GetMapping("/greet") public HelloGoodbye getGreeting(@RequestParam("type") String type, @RequestParam("id") int id) throws Exception { HelloGoodbye goodBye = service.createGreeting(type, id); return goodBye; } @ExceptionHandler(NameNotFoundException.class) public ResponseEntity<?> nameNotFoundException (NameNotFoundException ex, WebRequest request) { GreetingError errorDetails = new GreetingError(); errorDetails.setTimestamp(new Date()); errorDetails.setMessage ("This is an overriding of the standard exception: " + ex.getMessage()); return new ResponseEntity<>(errorDetails, HttpStatus.NOT_FOUND); } }
- 编译、运行应用程序,然后访问端点。
https://:8080/greeting/greet?type=hello&id=33
{ "timestamp": "2019-04-01T02:14:51.744+0000", "message": "This is an overriding of the standard exception: The id: 33 could not be found." }
NameNotFoundException
的默认错误处理在控制器中被覆盖。但是,你并不局限于在控制器中实现一个错误处理器,你可以定义多个错误处理器,如下面的代码所示。 - 修改
GreetingController
的getGreeting
方法以抛出算术异常。 - 为
ArithmeticException
创建一个新的异常处理器。package com.tutorial.exceptions.spring.rest.exceptionstutorial; import java.util.Date; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.request.WebRequest; @RestController @RequestMapping(value = "/greeting") public class GreetingController { @Autowired protected GreetingService service; @GetMapping("/greet") public HelloGoodbye getGreeting(@RequestParam("type") String type, @RequestParam("id") int id) throws Exception { int i = 0; int k = 22/i; HelloGoodbye goodBye = service.createGreeting(type, id); return goodBye; } @ExceptionHandler(NameNotFoundException.class) public ResponseEntity<?> nameNotFoundException (NameNotFoundException ex, WebRequest request) { GreetingError errorDetails = new GreetingError(); errorDetails.setTimestamp(new Date()); errorDetails.setMessage("This is an overriding of the standard exception: " + ex.getMessage()); return new ResponseEntity<>(errorDetails, HttpStatus.NOT_FOUND); } @ExceptionHandler(ArithmeticException.class) public ResponseEntity<?> arithmeticException (ArithmeticException ex, WebRequest request) { GreetingError errorDetails = new GreetingError(); errorDetails.setTimestamp(new Date()); errorDetails.setMessage("This is an overriding of the standard exception: " + ex.getMessage()); return new ResponseEntity<>(errorDetails, HttpStatus.INTERNAL_SERVER_ERROR); } }
- 编译、运行应用程序,然后访问 Rest 端点。
{ "timestamp": "2019-04-01T02:40:53.527+0000", "message": "This is an overriding of the standard exception: / by zero" }
- 在继续之前,不要忘记删除除零的代码。
异常处理器是一个有用的注解,它允许在类中处理异常。我们在控制器中使用了它来处理异常。用于处理异常的方法返回了一个
ResponseEntity<T>
类(javadoc)。此类是HttpEntity
(javadoc)的子类。HttpEntity
包装实际的请求或响应——这里是响应——而ResponseEntity
添加了HttpStatus
代码。这允许你从 Rest 端点返回自定义响应。
全局错误处理器
@ControllerAdvice
是一个在 Spring Controllers 中处理异常的方法。它允许使用带有 @ExceptionHandler
注解的方法来处理应用程序中的所有异常。
package com.tutorial.exceptions.spring.rest.exceptionstutorial;
import java.util.Date;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
@ControllerAdvice
public class GreetingExceptionHandler {
@ExceptionHandler(NameNotFoundException.class)
public ResponseEntity<?> nameNotFoundException
(NameNotFoundException ex, WebRequest request) {
GreetingError errorDetails = new GreetingError();
errorDetails.setTimestamp(new Date());
errorDetails.setMessage("This a global exception handler: " + ex.getMessage());
return new ResponseEntity<>(errorDetails, HttpStatus.NOT_FOUND);
}
}
- 创建一个名为
GreetingExceptionHandler
的新类。 - 用
@ControllerAdvice
注解对其进行注释。 - 将
nameNotFoundException
方法从GreetingController
类复制粘贴过来。更改消息文本,以确保它确实被调用了。 - 从
GreetingController
类中移除NameNotFoundException
异常处理器。package com.tutorial.exceptions.spring.rest.exceptionstutorial; import java.util.Date; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.request.WebRequest; @RestController @RequestMapping(value = "/greeting") public class GreetingController { @Autowired protected GreetingService service; @GetMapping("/greet") public HelloGoodbye getGreeting(@RequestParam("type") String type, @RequestParam("id") int id) throws Exception { HelloGoodbye goodBye = service.createGreeting(type, id); return goodBye; } @ExceptionHandler(ArithmeticException.class) public ResponseEntity<?> arithmeticException (ArithmeticException ex, WebRequest request) { GreetingError errorDetails = new GreetingError(); errorDetails.setTimestamp(new Date()); errorDetails.setMessage("This is an overriding of the standard exception: " + ex.getMessage()); return new ResponseEntity<>(errorDetails, HttpStatus.INTERNAL_SERVER_ERROR); } }
- 编译、运行应用程序,然后访问 Rest 端点。你收到了全局处理器中创建的错误。
https://:8080/greeting/greet?type=hello&id=33
{ “timestamp”: “2019-04-06T21:21:17.258+0000”, “message”: “This a global exception handler: The id: 33 could not be found.” }
@ControllerAdvice
注解(Javadoc)允许跨控制器共享异常处理器。如果你希望在多个控制器之间创建统一的异常处理,它会很有用。你可以将@ControllerAdvice
异常处理限制为仅适用于某些控制器,有关更多信息,请参阅 Javadoc。
结论
Spring 异常处理既简单又困难。简单是因为有具体的方法来实现异常处理。而且,即使你没有提供任何异常处理,Spring 也会默认为你提供。困难是因为有许多不同的方法可以实现异常处理。Spring 提供了如此多的自定义选项,如此多的不同技术,有时很容易迷失在细节中。
在本教程中,我们探讨了在使用 Spring Boot 2.1 Rest 时使用的三种不同技术。你应该参考其他教程,然后再决定哪一种技术是你应该使用的。为了完全披露,我个人认为 @ControllerAdvice
技术是最健壮的,因为它允许创建统一的异常处理框架。
GitHub 项目
https://github.com/jamesabrannan/spring-rest-exception-tutorial