Spring Boot @ControllerAdvice
最后修改于 2023 年 7 月 18 日
SpringBoot @ControllerAdvice 教程展示了如何使用 @ControllerAdvice 注解在 Spring Boot 应用程序中管理异常。
Spring 是一个流行的 Java 应用程序框架。Spring Boot 旨在以最小的努力创建独立的、生产级的基于 Spring 的应用程序。
@ControllerAdvice
@ControllerAdvice
是 @Component
注解的特化,它允许在一个全局处理组件中处理整个应用程序的异常。它可以被看作是由 @RequestMapping
和类似方法抛出的异常的拦截器。
它声明 @ExceptionHandler
、@InitBinder
或 @ModelAttribute
方法,以便在多个 @Controller
类之间共享。
ResponseEntityExceptionHandler
是 @ControllerAdvice
类的便捷基类,它希望通过 @ExceptionHandler
方法在所有 @RequestMapping
方法中提供集中式异常处理。它提供了用于处理内部 Spring MVC 异常的方法。它返回一个 ResponseEntity
,与返回 ModelAndView
的 DefaultHandlerExceptionResolver
形成对比。
Spring Boot @ControllerAdvice 示例
在下面的 Spring Boot 应用程序中,我们使用 @ControllerAdvice
来处理三个异常:未找到城市时、没有数据时以及要保存的新城市的数据无效时。
build.gradle ... src ├───main │ ├───java │ │ └───com │ │ └───zetcode │ │ │ Application.java │ │ │ MyRunner.java │ │ ├───controller │ │ │ MyController.java │ │ ├───exception │ │ │ CityNotFoundException.java │ │ │ ControllerAdvisor.java │ │ │ NoDataFoundException.java │ │ ├───model │ │ │ City.java │ │ ├───repository │ │ │ CityRepository.java │ │ └───service │ │ CityService.java │ │ ICityService.java │ └───resources │ application.properties └───test ├── java └── resources
这是项目结构。
plugins { id 'org.springframework.boot' version '3.1.1' id 'io.spring.dependency-management' version '1.1.0' id 'java' } group = 'com.zetcode' version = '0.0.1-SNAPSHOT' sourceCompatibility = '17' repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'com.h2database:h2' }
这是 Gradle 构建文件。从 Spring Boot 2.3 开始,必须显式指定 spring-boot-starter-validation
依赖项。
spring.main.banner-mode=off
application.properties
是主要的 Spring Boot 配置文件。通过 spring.main.banner-mode
属性,我们关闭 Spring 横幅。
package com.zetcode.model; import org.hibernate.validator.constraints.Range; import java.util.Objects; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; import jakarta.validation.constraints.NotEmpty; @Entity @Table(name = "cities") public class City { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @NotEmpty private String name; @Range(min=10, max=100_000_000) private int population; public City() { } public City(String name, int population) { this.name = name; this.population = population; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getPopulation() { return population; } public void setPopulation(int population) { this.population = population; } @Override public int hashCode() { int hash = 7; hash = 79 * hash + Objects.hashCode(this.id); hash = 79 * hash + Objects.hashCode(this.name); hash = 79 * hash + this.population; return hash; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final City other = (City) obj; if (this.population != other.population) { return false; } if (!Objects.equals(this.name, other.name)) { return false; } return Objects.equals(this.id, other.id); } @Override public String toString() { var builder = new StringBuilder(); builder.append("City{id=").append(id).append(", name=") .append(name).append(", population=") .append(population).append("}"); return builder.toString(); } }
这是 City
实体。 它包含以下属性:id
、name
和 population
。
@NotEmpty private String name; @Range(min=10, max=100_000_000) private int population;
我们有用于城市数据的验证注解。当名称为空且人口不符合指定范围时,会抛出异常。
package com.zetcode.repository; import com.zetcode.model.City; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Repository; @Repository public interface CityRepository extends CrudRepository<City, Long> { }
CityRepository
继承自 CrudRepository
。它提供了实体及其主键的类型。存储库是城市对象的存储库。
package com.zetcode.service; import com.zetcode.model.City; import java.util.List; public interface ICityService { City findById(Long id); City save(City city); List<City> findAll(); }
ICityService
提供了 contract 方法来保存城市、获取所有城市以及通过其 ID 从数据源获取城市。
package com.zetcode.service; import com.zetcode.exception.CityNotFoundException; import com.zetcode.exception.NoDataFoundException; import com.zetcode.model.City; import com.zetcode.repository.CityRepository; import org.springframework.stereotype.Service; import java.util.List; @Service public class CityService implements ICityService { private final CityRepository cityRepository; public CityService(CityRepository cityRepository) { this.cityRepository = cityRepository; } @Override public City findById(Long id) { return cityRepository.findById(id) .orElseThrow(() -> new CityNotFoundException(id)); } @Override public City save(City city) { return cityRepository.save(city); } @Override public List<City> findAll() { var cities = (List<City>) cityRepository.findAll(); if (cities.isEmpty()) { throw new NoDataFoundException(); } return cities; } }
CityService
包含 findAll
、save
和 findById
方法的实现。我们使用存储库从数据库检索数据。
return cityRepository.findById(id) .orElseThrow(() -> new CityNotFoundException(id));
如果找不到城市,则抛出 CityNotFoundException
。
if (cities.isEmpty()) { throw new NoDataFoundException(); }
如果数据库中没有数据,则抛出 NoDataFoundException
。
package com.zetcode.exception; public class CityNotFoundException extends RuntimeException { public CityNotFoundException(Long id) { super(String.format("City with Id %d not found", id)); } }
这是 CityNotFoundException
。
package com.zetcode.exception; public class NoDataFoundException extends RuntimeException { public NoDataFoundException() { super("No data found"); } }
这是 NoDataFoundException
。
package com.zetcode.exception; import org.springframework.context.support.DefaultMessageSourceResolvable; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseEntity; 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 java.time.LocalDate; import java.time.LocalDateTime; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @ControllerAdvice public class ControllerAdvisor extends ResponseEntityExceptionHandler { @ExceptionHandler(CityNotFoundException.class) public ResponseEntity<Object> handleCityNotFoundException( CityNotFoundException ex, WebRequest request) { Map<String, Object> body = new LinkedHashMap<>(); body.put("timestamp", LocalDateTime.now()); body.put("message", "City not found"); return new ResponseEntity<>(body, HttpStatus.NOT_FOUND); } @ExceptionHandler(NoDataFoundException.class) public ResponseEntity<Object> handleNodataFoundException( NoDataFoundException ex, WebRequest request) { Map<String, Object> body = new LinkedHashMap<>(); body.put("timestamp", LocalDateTime.now()); body.put("message", "No cities found"); return new ResponseEntity<>(body, HttpStatus.NOT_FOUND); } @Override public ResponseEntity<Object> handleMethodArgumentNotValid( MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) { Map<String, Object> body = new LinkedHashMap<>(); body.put("timestamp", LocalDate.now()); body.put("status", status.value()); List<String> errors = ex.getBindingResult() .getFieldErrors() .stream() .map(DefaultMessageSourceResolvable::getDefaultMessage) .collect(Collectors.toList()); body.put("errors", errors); return new ResponseEntity<>(body, HttpStatus.BAD_REQUEST); } }
ControllerAdvisor
是一个组件,它在一个地方处理所有三个异常。
@ControllerAdvice public class ControllerAdvisor extends ResponseEntityExceptionHandler {
ResponseEntityExceptionHandler
继承自 ResponseEntityExceptionHandler
,它是一个用于控制器顾问组件的便捷基类。
@ExceptionHandler(CityNotFoundException.class) public ResponseEntity<Object> handleCityNotFoundException( CityNotFoundException ex, WebRequest request) { Map<String, Object> body = new LinkedHashMap<>(); body.put("timestamp", LocalDateTime.now()); body.put("message", "City not found"); return new ResponseEntity<>(body, HttpStatus.NOT_FOUND); }
这是 CityNotFoundException
的处理程序方法。我们向客户端发送一个带有时间戳、错误消息和状态码的 ResponseEntity
。
@Override protected ResponseEntity<Object> handleMethodArgumentNotValid( MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) {
handleMethodArgumentNotValid
处理 MethodArgumentNotValidException
,当使用 @Valid
注解的参数的验证失败时,将抛出该异常。
List<String> errors = ex.getBindingResult() .getFieldErrors() .stream() .map(DefaultMessageSourceResolvable::getDefaultMessage) .collect(Collectors.toList()); body.put("errors", errors);
我们获取错误字段。
return new ResponseEntity<>(body, HttpStatus.BAD_REQUEST);
返回一个 ResponseEntity
,其中包含错误消息和状态码。
package com.zetcode.controller; import com.zetcode.model.City; import com.zetcode.service.ICityService; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import jakarta.validation.Valid; import java.util.List; @RestController public class MyController { private final ICityService cityService; public MyController(ICityService cityService) { this.cityService = cityService; } @GetMapping(value = "/cities/{id}") public City getCity(@PathVariable Long id) { return cityService.findById(id); } @PostMapping(value = "/cities", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) public City createCity(@RequestBody @Valid City city) { return cityService.save(city); } @GetMapping(value = "/cities") public List<City> findAll() { return cityService.findAll(); } }
MyController
是一个 Restful 控制器。它包含用于检索城市、保存城市和检索所有城市的映射。
@PostMapping(value = "/cities", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) public City createCity(@RequestBody @Valid City city) {
使用 @Valid
注解,我们确保数据符合验证规则。
package com.zetcode; import com.zetcode.model.City; import com.zetcode.repository.CityRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; @Component public class MyRunner implements CommandLineRunner { private static final Logger logger = LoggerFactory.getLogger(MyRunner.class); private final CityRepository cityRepository; @Autowired public MyRunner(CityRepository cityRepository) { this.cityRepository = cityRepository; } @Override public void run(String... args) throws Exception { logger.info("Saving cities"); cityRepository.save(new City("Bratislava", 432000)); cityRepository.save(new City("Budapest", 1759000)); cityRepository.save(new City("Prague", 1280000)); cityRepository.save(new City("Warsaw", 1748000)); cityRepository.save(new City("Los Angeles", 3971000)); cityRepository.save(new City("New York", 8550000)); cityRepository.save(new City("Edinburgh", 464000)); cityRepository.save(new City("Suzhou", 4327066)); cityRepository.save(new City("Zhengzhou", 4122087)); cityRepository.save(new City("Berlin", 3671000)); } }
在 MyRunner
中,我们将几个城市对象保存到数据库中。
private final CityRepository cityRepository; @Autowired public MyRunner(CityRepository cityRepository) { this.cityRepository = cityRepository; }
我们将 CityRepository
注入到 cityRepository
字段中。
cityRepository.save(new City("Bratislava", 432000));
使用 save
插入一个新城市。
package com.zetcode; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
`Application` 设置了 Spring Boot 应用程序。
$ curl localhost:8080/cities/23 {"timestamp":"2023-07-18T19:06:10.1986204","message":"City not found"}
没有 ID 为 23 的城市。
$ curl localhost:8080/cities -H "Content-Type: application/json" -X POST -d '{"name":"Sydney", "population":"2"}' {"timestamp":"2023-07-18","status":400,"errors":["must be between 10 and 100000000"]}
当我们提供无效的人口值时,我们会收到一条错误消息。(在 Windows 上使用 -d "{\"name\":\"Sydney\", \"population\":\"2\"}"
。)
在本文中,我们使用了 @ControllerAdvice
注解。