Серия галантереи Spring Boot: (13) глобальная обработка исключений Spring Boot

Spring Boot Spring JSON HTML
Серия галантереи Spring Boot: (13) глобальная обработка исключений Spring Boot

Оригинальный адрес:Серия галантереи Spring Boot: (13) глобальная обработка исключений Spring Boot
адрес блога:tengj.top/

предисловие

Сегодня давайте узнаем об обработке исключений в Spring Boot, Когда в повседневной веб-разработке возникает исключение, часто необходимо использовать унифицированную обработку исключений, чтобы гарантировать, что клиент может получать дружественные подсказки.

текст

Основные пункты этой статьи следующие

  • Представьте механизм обработки исключений по умолчанию в Spring Boot.
  • Как настроить страницы ошибок
  • Обработка исключений с помощью аннотации @ControllerAdvice

Представьте механизм обработки исключений по умолчанию в Spring Boot.

По умолчанию Spring Boot предоставляет разные ответы для двух ситуаций.

Во-первых, когда клиент браузера запрашивает несуществующую страницу или при обработке на стороне сервера возникает исключение, в общем случае заголовок запроса, отправляемый браузером, по умолчанию содержит Accept: text/html, поэтому Spring Boot ответит на запрос html по умолчанию, который называется Make «Страница ошибок Whitelabel».

image.png

Другой — использовать инструмент отладки, такой как Postman, для отправки запроса на несуществующий URL-адрес или, когда при обработке на стороне сервера возникает исключение, Spring Boot вернет следующую информацию о строке формата Json.

{
    "timestamp": "2018-05-12T06:11:45.209+0000",
    "status": 404,
    "error": "Not Found",
    "message": "No message available",
    "path": "/index.html"
} 

Принцип также очень прост, Spring Boot по умолчанию предоставляет путь сопоставления результатов / ошибку ошибки программы. Этот запрос /error будет обработан в BasicErrorController, который внутренне различает, исходит ли запрос от клиентского браузера, определяя, является ли содержимое Accept в заголовке запроса text/html (браузер обычно автоматически отправляет содержимое заголовка запроса Accept:text/ по умолчанию).html) или вызов клиентского интерфейса, чтобы решить, следует ли возвращать просмотр страницы или содержимое сообщения JSON. Код в соответствующем BasicErrorController выглядит следующим образом:

image.png

Как настроить страницы ошибок

Что ж, разобравшись с механизмом ошибок Spring Boot по умолчанию, давайте сделаем кое-что интересное: если на стороне браузера есть доступ, любая ошибка, которую возвращает Spring Boot,Whitelabel Error PageСтраница ошибки, это очень недружелюбно, поэтому мы можем настроить страницу ошибки.

1. Начните с самого простого, прямо в/resources/templatesСоздайте файл error.html ниже, чтобы переопределить значение по умолчанию.Whitelabel Error PageСтраница ошибки моего проекта использует шаблон тимелеафа, и соответствующий код error.html выглядит следующим образом:

image.png

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
动态error错误页面
<p th:text="${error}"></p>
<p th:text="${status}"></p>
<p th:text="${message}"></p>
</body>
</html>

При таком запуске, когда запрашивается несуществующая страница или возникает исключение при обработке на стороне сервера, отображается следующий настраиваемый интерфейс ошибки:

image.png

2. Кроме того, если вы хотите быть более точным, верните разные страницы просмотра в соответствии с разными кодами состояния, то есть соответствующие страницы 404, 500 и другие, есть два типа, страница ошибки может быть статическим HTML (то есть , добавленный в любой статический HTML) Папка Resources), вы также можете использовать сборку шаблона, имя файла должно быть точным кодом состояния.

  • Если это просто статическая HTML-страница без информации об ошибке, создайте каталог ошибок в resources/public/ и создайте соответствующий код состояния html в каталоге ошибок.Например, чтобы сопоставить 404 со статическим файлом HTML, ваша папка Структура выглядит так:
    image.png

Статическая простая страница 404.html выглядит следующим образом:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    静态404错误页面
</body>
</html>

При доступе к неправильному пути, подобному этому, он будет отображать静态404错误页面страница ошибки

image.png

Примечание. В настоящее время, если имеется страница error.html, описанная выше для первого типа, страница ошибки с кодом состояния будет охватывать error.html, а страница с кодом ошибки с конкретным кодом состояния имеет более высокий приоритет.

  • Если это страница динамического шаблона, вы можете вывести сообщение об ошибке вresources/templates/Создайте каталог ошибок ниже и назовите его в каталоге ошибок:
    image.png

Здесь мы моделируем ошибку 500, код уровня управления и моделируем ошибку деления на 0:

@Controller 
public class BaseErrorController extends  AbstractController{ 
private Logger logger = LoggerFactory.getLogger(this.getClass()); 

    @RequestMapping(value="/ex") 
    @ResponseBody 
    public String error(){ 
        int i=5/0; 
        return "ex"; 
    } 
} 

500.html код:

<!DOCTYPE html> 
<html xmlns:th="http://www.thymeleaf.org"> 
<head> 
<meta charset="UTF-8"> 
<title>Title</title> 
</head> 
<body> 
    动态500错误页面 
    <p th:text="${error}"></p> 
    <p th:text="${status}"></p> 
    <p th:text="${message}"></p> 
</body> 
</html> 

В это время посетите http://localhost:8080/spring/ex, и вы увидите следующую ошибку, указывающую, что она действительно сопоставлена ​​с 500.html.

image.png

Примечание. Если есть как статическая страница 500.html, так и динамический шаблон 500.html, последний имеет приоритет над первым. которыйtemplates/error/Приоритет этогоresources/public/errorвысокий.

Общий итог описанных выше ситуаций выглядит следующим образом:

  • error.html переопределит сообщение об ошибке страницы ошибок whitelabel по умолчанию.
  • Статические страницы с ошибками имеют более высокий приоритет, чем error.html.
  • Страницы ошибок динамического шаблона имеют более высокий приоритет, чем страницы ошибок статического шаблона.

3. Вышеуказанное — это самый простой способ переопределить страницу ошибок для настройки.Если вы хотите специально обработать некоторые ошибки, вы можете сделать это

@Configuration 
public class ContainerConfig { 
    @Bean 
    public EmbeddedServletContainerCustomizer containerCustomizer(){ 
        return new EmbeddedServletContainerCustomizer(){ 
           @Override 
           public void customize(ConfigurableEmbeddedServletContainer container) { 
               container.addErrorPages(new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/error/500")); 
           } 
        }; 
   } 
} 

В приведенном выше кодеHttpStatus.INTERNAL_SERVER_ERRORОн соответствует коду ошибки 500, что означает, что если программа столкнется с ошибкой 500, она перенаправит запрос на/error/500Это отображение приходит, тогда нам нужно только реализовать метод, соответствующий этому/error/500Отображение может поймать это исключение и обработать его.

@RequestMapping("/error/500") 
@ResponseBody 
public String showServerError() { 
    return "server error"; 
} 

Таким образом, когда мы запрашиваем вышеупомянутый аномальный запрос http://localhost:8080/spring/ex, он будет перехвачен нашим методом.

image.png

Здесь мы делаем специальную обработку только для 500 и возвращаем строку.Если вы хотите вернуть представление, удалите аннотацию @ResponseBody и верните соответствующую страницу представления. Если вы хотите настроить сопоставление для других кодов состояния, вы можете добавить его в методе настройки.

Несмотря на то, что мы переписали сопоставление /500 в приведенном выше методе, существует проблема, заключающаяся в том, что мы не можем получить информацию об ошибке.Если мы хотим получить информацию об ошибке, мы можем наследовать BasicErrorController или просто реализовать интерфейс ErrorController, в дополнение к ответу к запросу страницы ошибок /error , который может предоставлять больше типов форматов ошибок и т. д. (BasicErrorController упоминается во введении механизма исключений SpringBoot по умолчанию выше)

Здесь блогер решает напрямую наследовать BasicErrorController, а затем поместить указанный выше/error/500Метод отображения может быть добавлен

@Controller
public class MyBasicErrorController extends BasicErrorController {

    public MyBasicErrorController() {
        super(new DefaultErrorAttributes(), new ErrorProperties());
    }

    /**
    * 定义500的ModelAndView
    * @param request
    * @param response
    * @return
    */

    @RequestMapping(produces = "text/html",value = "/500")
    public ModelAndView errorHtml500(HttpServletRequest request,HttpServletResponse response) {
        response.setStatus(getStatus(request).value());
        Map<String, Object> model = getErrorAttributes(request,isIncludeStackTrace(request, MediaType.TEXT_HTML));
        model.put("msg","自定义错误信息");
        return new ModelAndView("error/500", model);
    }

    /**
    * 定义500的错误JSON信息
    * @param request
    * @return
    */

    @RequestMapping(value = "/500")
    @ResponseBody

    public ResponseEntity<Map<String, Object>> error500(HttpServletRequest request) {
        Map<String, Object> body = getErrorAttributes(request,isIncludeStackTrace(request, MediaType.TEXT_HTML));
        HttpStatus status = getStatus(request);
        return new ResponseEntity<Map<String, Object>>(body, status);
    }
}

Код также очень прост, он просто реализует пользовательский анализ сопоставления ошибок 500 и отвечает на запросы браузера и запросы json соответственно.

@RequestMapping по умолчанию, соответствующий BasicErrorController, равен/error, что соответствует нашему методу@RequestMapping(produces = "text/html",value = "/500")На самом деле полный запрос карты/error/500, который соответствует пути сопоставления, настроенному описанным выше методом настройки.

В методе errorHtml500 я возвращаю страницу шаблона, соответствующую /templates/error/500.html, здесь кстати я настроил информацию msg, а также вывожу эту информацию в 500.html<p th:text="${msg}"></p>, если в выводе есть эта информация, значит наша конфигурация правильная.

Доступ к запросу снова http://localhost:8080/spring/ex, результат следующий

image.png

## Обработка исключений с помощью аннотации @ControllerAdvice

ErrorController, предоставляемый Spring Boot, представляет собой глобальный механизм отказоустойчивости. Кроме того, вы можете реализовать специальную обработку определенных исключений с помощью аннотаций @ControllerAdvice и @ExceptionHandler.

Вот две ситуации:

  • Локальная обработка исключений @Controller + @ExceptionHandler
  • Глобальная обработка исключений @ControllerAdvice + @ExceptionHandler

Локальная обработка исключений @Controller + @ExceptionHandler

Локальное исключение в основном использует аннотацию @ExceptionHandler.Эта аннотация аннотируется к методу класса.При возникновении исключения, определенного в этой аннотации, этот метод будет выполнен. Если класс, в котором находится @ExceptionHandler, — это @Controller, этот метод работает только с этим классом. Если класс, в котором находится @ExceptionHandler, аннотирован @ControllerAdvice, этот метод будет работать глобально.

Эта аннотация используется для аннотации методов обработки для обработки этих конкретных исключений. Методы, аннотированные этой аннотацией, могут иметь типы параметров в любом порядке:

  • Throwable, Exception и другие объекты исключений;

  • ServletRequest, HttpServletRequest, ServletResponse, HttpServletResponse;

  • Объекты сеанса, такие как HttpSession;

  • org.springframework.web.context.request.WebRequest;

  • java.util.Locale;

  • java.io.InputStream, java.io.Reader;

  • java.io.OutputStream, java.io.Writer;

  • org.springframework.ui.Модель;

И методы, аннотированные этой аннотацией, могут иметь следующие типы возвращаемых значений:

  • модель и представление;

  • org.springframework.ui.Модель;

  • Java.UTIL.карта;

  • org.springframework.web.servlet.View;

  • Любой объект, аннотированный аннотацией @ResponseBody;

  • HttpEntity или ResponseEntity;

  • пустота;

Приведенный выше список не является полным, для получения более подробной информации см.: [Spring ExceptionHandler](https://docs.spring.IO/spring/docs/current/Java doc-API/org/spring framework/Web/Pentax/annotation/обработчик исключений.HTML).

В качестве простого примера здесь мы используем @ExceptionHandler для обнаружения исключений деления на 0.

@Controller
public class BaseErrorController extends  AbstractController{ 
    private Logger logger = LoggerFactory.getLogger(this.getClass()); 

    @RequestMapping(value="/ex") 
    @ResponseBody 
    public String error(){ 
        int i=5/0; 
        return "ex"; 
  } 

    //局部异常处理 
    @ExceptionHandler(Exception.class) 
    @ResponseBody 
    public String exHandler(Exception e){ 
      // 判断发生异常的类型是除0异常则做出响应 
      if(e instanceof ArithmeticException){ 
          return "发生了除0异常"; 
      } 
      // 未知的异常做出响应 
      return "发生了未知异常"; 
    }
} 

image.png

Глобальная обработка исключений @ControllerAdvice + @ExceptionHandler

Весной 3.2 была добавлена ​​аннотация @ControllerAdvice, которую можно использовать для определения @ExceptionHandler, @InitBinder, @ModelAttribute и применять ко всем @RequestMappings.

Проще говоря, ошибки, поступающие на уровень контроллера, будут обрабатываться @ControllerAdvice, ошибки, выдаваемые перехватчиком, и случай доступа к неправильному адресу не могут быть обработаны @ControllerAdvice и обрабатываются механизмом обработки исключений SpringBoot по умолчанию.

В нашей фактической разработке, если мы хотим реализовать RESTful API, то сообщение об ошибке JSON по умолчанию — это не то, что нам нужно.На данный момент нам нужно унифицировать формат JSON, поэтому нам нужно его инкапсулировать.

/**
* 返回数据
*/
public class AjaxObject extends HashMap<String, Object> {
    private static final long serialVersionUID = 1L;
 
    public AjaxObject() {
        put("code", 0);
    }
    
    public static AjaxObject error() {
        return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, "未知异常,请联系管理员");
    }
    
    public static AjaxObject error(String msg) {
        return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, msg);
    }
    
    public static AjaxObject error(int code, String msg) {
        AjaxObject r = new AjaxObject();
        r.put("code", code);
        r.put("msg", msg);
        return r;
    }

    public static AjaxObject ok(String msg) {
        AjaxObject r = new AjaxObject();
        r.put("msg", msg);
        return r;
    }
    
    public static AjaxObject ok(Map<String, Object> map) {
        AjaxObject r = new AjaxObject();
        r.putAll(map);
        return r;
    }
    
    public static AjaxObject ok() {
        return new AjaxObject();
    }

    public AjaxObject put(String key, Object value) {
        super.put(key, value);
        return this;
    }
    
    public AjaxObject data(Object value) {
        super.put("data", value);
        return this;
    }

    public static AjaxObject apiError(String msg) {
        return error(1, msg);
    }
}

Вышеупомянутый AjaxObject — это то, что я обычно использую.Если это правильно, он вернет:

{
    code:0,
    msg:“获取列表成功”,
    data:{ 
        queryList :[]
    }
}

Правильный код по умолчанию возвращает 0, а данные могут быть коллекцией или объектом.Если это исключение, возвращаемый json:

{
    code:500,
    msg:“未知异常,请联系管理员”
}

Затем создайте собственный класс исключений:

public class BusinessException extends RuntimeException implements Serializable {

    private static final long serialVersionUID = 1L;
    private String msg;
    private int code = 500;
    
    public BusinessException(String msg) {
        super(msg);
        this.msg = msg;
    }
    
    public BusinessException(String msg, Throwable e) {
        super(msg, e);
        this.msg = msg;
    }
    
    public BusinessException(int code,String msg) {
        super(msg);
        this.msg = msg;
        this.code = code;
    }
    
    public BusinessException(String msg, int code, Throwable e) {
        super(msg, e);
        this.msg = msg;
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }
}

Примечание. Spring откатывает транзакцию только для исключений RuntimeException.

Добавьте карту json в контроллер для обработки этого исключения.

@Controller
public class BaseErrorController{
    @RequestMapping("/json")
    public void json(ModelMap modelMap) {
        System.out.println(modelMap.get("author"));
        int i=5/0;
    }
}

Наконец, создайте этот глобальный класс обработчика исключений:

/**
 * 异常处理器
 */
@RestControllerAdvice
public class BusinessExceptionHandler {
	private Logger logger = LoggerFactory.getLogger(getClass());



	/**
	 * 应用到所有@RequestMapping注解方法,在其执行之前初始化数据绑定器
	 * @param binder
	 */
	@InitBinder
	public void initBinder(WebDataBinder binder) {
		System.out.println("请求有参数才进来");
	}

	/**
	 * 把值绑定到Model中,使全局@RequestMapping可以获取到该值
	 * @param model
	 */
	@ModelAttribute
	public void addAttributes(Model model) {
		model.addAttribute("author", "嘟嘟MD");
	}

	@ExceptionHandler(Exception.class)
	public Object handleException(Exception e,HttpServletRequest req){
		AjaxObject r = new AjaxObject();
		//业务异常
		if(e instanceof BusinessException){
			r.put("code", ((BusinessException) e).getCode());
			r.put("msg", ((BusinessException) e).getMsg());
		}else{//系统异常
			r.put("code","500");
			r.put("msg","未知异常,请联系管理员");
		}

		//使用HttpServletRequest中的header检测请求是否为ajax, 如果是ajax则返回json, 如果为非ajax则返回view(即ModelAndView)
		String contentTypeHeader = req.getHeader("Content-Type");
		String acceptHeader = req.getHeader("Accept");
		String xRequestedWith = req.getHeader("X-Requested-With");
		if ((contentTypeHeader != null && contentTypeHeader.contains("application/json"))
				|| (acceptHeader != null && acceptHeader.contains("application/json"))
				|| "XMLHttpRequest".equalsIgnoreCase(xRequestedWith)) {
			return r;
		} else {
			ModelAndView modelAndView = new ModelAndView();
			modelAndView.addObject("msg", e.getMessage());
			modelAndView.addObject("url", req.getRequestURL());
			modelAndView.addObject("stackTrace", e.getStackTrace());
			modelAndView.setViewName("error");
			return modelAndView;
		}
	}
}

@ExceptionHandler перехватывает исключения, и мы можем реализовать собственную обработку исключений с помощью этой аннотации. Среди них значение, настроенное @ExceptionHandler, указывает тип исключения, которое необходимо перехватить.Я настроил исключение перехвата выше. Затем верните разные ответы в соответствии с разными типами исключений и, наконец, добавьте суждение.Если это запрос Ajax, верните json, если это не ajax, вернитесь к просмотру, вот возврат на страницу error.html.

Чтобы быть более дружелюбным при отображении ошибок, я инкапсулировал error.html, который не только отображает ошибки, но и добавляет кнопки для перехода к Baidu Google и StackOverFlow следующим образом:

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org" layout:decorator="layout">
<head>
    <title>Spring Boot管理后台</title>
    <script type="text/javascript">
    </script>
</head>
<body>
<div layout:fragment="content" th:remove="tag">
    <div  id="navbar">
        <h1>系统异常统一处理</h1>
        <h3 th:text="'错误信息:'+${msg}"></h3>
        <h3 th:text="'请求地址:'+${url}"></h3>

        <h2>Debug</h2>
        <a th:href="@{'https://www.google.com/webhp?hl=zh-CN#safe=strict&hl=zh-CN&q='+${msg}}"
           class="btn btn-primary btn-lg" target="_blank" id="Google">Google</a>
        <a th:href="@{'https://www.baidu.com/s?wd='+${msg}}" class="btn btn-info btn-lg"  target="_blank" id="Baidu">Baidu</a>
        <a th:href="@{'http://stackoverflow.com/search?q='+${msg}}"
           class="btn btn-default btn-lg"  target="_blank" id="StackOverFlow">StackOverFlow</a>
        <h2>异常堆栈跟踪日志StackTrace</h2>
        <div th:each="line:${stackTrace}">
            <div th:text="${line}"></div>
        </div>
    </div>
</div>
<div layout:fragment="js" th:remove="tag">
</div>
</body>
</html>

При посещении http://localhost:8080/json, поскольку он инициируется браузером, возвращается интерфейс ошибки:

image.png

Если это запрос ajax, возвращается ошибка:

{ "msg":"未知异常,请联系管理员", "code":500 }

Здесь я устанавливаю значение автора через модель для метода, аннотированного с помощью @ModelAttribute, и получаю измененное значение через ModelMwap в методе сопоставления json.

Серьезно, вы можете обнаружить, что я использую @RestControllerAdvice вместо @ControllerAdvice для глобального класса исключений, потому что основной возврат здесь в формате json, поэтому вы можете написать на один @ResponseBody меньше.

Суммировать

На этом использование исключений в SpringBoot почти завершено, порядок обработки исключений в этом проекте будет таким, когда будет отправлен запрос:

  • Перехватчик сначала определяет, нужно ли авторизоваться, если нет, то возвращается на страницу авторизации.
  • Перед входом в Контроллер, например при запросе несуществующего адреса, возвращается интерфейс ошибки 404.
  • При выполнении @RequestMapping различные обнаруженные ошибки (такие как ошибка базы данных, ошибка формата параметра запроса/отсутствующее/недопустимое значение и т. д.) единообразно обрабатываются @ControllerAdvice, и в зависимости от того, возвращается ли Ajax, возвращается json или представление.

Чтобы увидеть больше руководств по галантерейным товарам Spring Boot, перейдите по ссылке: [Общий обзор серии галантерейных товаров Spring Boot](http://tengj.top/2017/04/24/springboot0/)

# Загрузка исходного кода

( ̄︶ ̄)↗[[Связанный пример полного кода](https://github.com/tengj/SpringBootDemo/tree/master)]

- Chapter13=="Серия галантереи Spring Boot: (13) Завершение глобальной обработки исключений Spring Boot

Я всегда чувствую, что пишу не о технике, а о чувствах, и каждая статья — это след моего пути. Успех опоры на профессиональные навыки является наиболее воспроизводимым.Я надеюсь, что мой путь поможет вам избежать окольных путей.Я надеюсь, что смогу помочь вам стереть пыль знаний.Я надеюсь, что смогу помочь вам прояснить контекст знаний.Я надеюсь, что в будущем Мы с тобой на вершине технологий.