ZetCode

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,与返回 ModelAndViewDefaultHandlerExceptionResolver 形成对比。

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

这是项目结构。

build.gradle
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 依赖项。

src/resources/application.properties
spring.main.banner-mode=off

application.properties 是主要的 Spring Boot 配置文件。通过 spring.main.banner-mode 属性,我们关闭 Spring 横幅。

com/zetcode/model/City.java
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 实体。 它包含以下属性:idnamepopulation

@NotEmpty
private String name;

@Range(min=10, max=100_000_000)
private int population;

我们有用于城市数据的验证注解。当名称为空且人口不符合指定范围时,会抛出异常。

com/zetcode/repository/CityRepository.java
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。它提供了实体及其主键的类型。存储库是城市对象的存储库。

com/zetcode/service/ICityService.java
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 从数据源获取城市。

com/zetcode/service/CityService.java
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 包含 findAllsavefindById 方法的实现。我们使用存储库从数据库检索数据。

return cityRepository.findById(id)
    .orElseThrow(() -> new CityNotFoundException(id));

如果找不到城市,则抛出 CityNotFoundException

if (cities.isEmpty()) {

    throw new NoDataFoundException();
}

如果数据库中没有数据,则抛出 NoDataFoundException

com/zetcode/exception/CityNotFoundException.java
package com.zetcode.exception;

public class CityNotFoundException extends RuntimeException {

    public CityNotFoundException(Long id) {

        super(String.format("City with Id %d not found", id));
    }
}

这是 CityNotFoundException

com/zetcode/exception/NoDataFoundException.java
package com.zetcode.exception;

public class NoDataFoundException extends RuntimeException {

    public NoDataFoundException() {

        super("No data found");
    }
}

这是 NoDataFoundException

com/zetcode/exception/ControllerAdvisor.java
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,其中包含错误消息和状态码。

com/zetcode/controller/MyController.java
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 注解,我们确保数据符合验证规则。

com/zetcode/MyRunner.java
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 插入一个新城市。

com/zetcode/Application.java
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 注解。

作者

我的名字是 Jan Bodnar,我是一位充满激情的程序员,拥有丰富的编程经验。自 2007 年以来,我一直在撰写编程文章。到目前为止,我撰写了 1,400 多篇文章和 8 本电子书。我拥有超过十年的编程教学经验。

列出 所有 Spring Boot 教程