Произошла ужасная ошибка! Обработка исключений в Java-приложении с сервлетами

Недавно мне было лень делать основную работу, и я подумал, что было бы неплохо сделать что-нибудь другое из списка задач. Я вспомнил, что в приложении ошибки до сих пор показываются в самом стандартном виде – это пугает пользователей и выглядит не очень красиво, ведь всё остальное приложение уже оформлено в соответствии с брэндбуком (корпоративные цвета, один шрифт и всё такое), а страница с исключением очень уж отличается от выбранного дизайна.

Примерно так выглядят исключения. Здесь есть все самые важные данные: и статус, и название исключения, и стэк-трейс. Но я решил, что пришло время эту информацию ещё и самостоятельно оформить и избавиться от печального белого экрана. И в этой статье расскажу, как это сделать в приложении с сервлетами и jsp-страницами.

Сервлет для обработки исключений и ошибок

Для начала нужно сделать новый сервлет. Выглядеть он будет абсолютно так же, как и все остальные: наследуется от HttpServlet, есть методы doGet и doPost. Я предлагаю из них обоих просто вызывать ещё один метод для обработки исключений и ошибок process.

В итоге обработчик будет находиться по адресу /exceptionHandler, называется класс ExceptionHandler и выглядит следующим образом:

@WebServlet("/exceptionHandler")
public class ExceptionHandler extends HttpServlet {
    private static final long serialVersionUID = 1L;

    protected void doGet(HttpServletRequest request,
                         HttpServletResponse response) throws ServletException, IOException {
        processError(request, response);
    }

    protected void doPost(HttpServletRequest request,
                          HttpServletResponse response) throws ServletException, IOException {
        processError(request, response);
    }

    private void processError(HttpServletRequest request,
                              HttpServletResponse response) throws IOException, ServletException {
        // Здесь будет код с обработкой исключения или ошибки
    }
}

Ну и не надо забывать о дескрипторе – нужно добавить информацию о новом сервлете в web.xml. Сделать это можно двумя способами. Например, можно отталкиваться от типа исключения, тогда нужен тэг <exception-type>.

error-page>
    <exception-type>javax.servlet.ServletException</exception-type>
    <location>/exceptionHandler</location>
</error-page>

А можно отталкиваться от кода ошибки, тогда указывается <error-code>.

  </error-page>
 	<error-code>404</error-code>
  	<location>/exceptionHandler</location>
  </error-page>

Соответственно, для разных типов исключений и разных ошибок можно создавать разные сервлеты и по-разному их обрабатывать. Моя обработка будет не очень объёмной, я просто вывожу информацию об ошибке на экран, поэтому всё будет вести к одному сервлету:

    <error-page>
        <error-code>404</error-code>
        <location>/exceptionHandler</location>
    </error-page>


    <error-page>
        <exception-type>java.lang.Throwable</exception-type>
        <location>/exceptionHandler</location>
    </error-page>

Я также не стал вдаваться в подробности и указал лишь java.lang.Throwable, как самый общий класс для всех исключений.

Обработка исключений и ошибок

Теперь, когда сервлет на месте, можно приступить к обработке информации в методе processError:

Способ 1. Вывод информации с помощью PrintWriter.

Самый примитивный способ – просто напихать всю нужную информацию в Writer. Например, так:

private void processError(HttpServletRequest request,
			HttpServletResponse response) throws IOException {
	response.setContentType("text/html");
	 
	PrintWriter out = response.getWriter();
	out.write("<html><head><title>Ошибочка вышла!</title></head><body>");
	out.write("<h3>Ошибка</h3>");
	out.write("<p>Что-то пошло не так, и произошла ошибка. Программист обязательно всё когда-нибудь исправит.</p>");
	  
	out.write("</body></html>");
}

Можно добавить немного больше информации: например, адрес сервлета, код и т. д.

private void processError(HttpServletRequest request,
			HttpServletResponse response) throws IOException {
		
	Throwable throwable = (Throwable) request
				.getAttribute("javax.servlet.error.exception");
	Integer statusCode = (Integer) request
				.getAttribute("javax.servlet.error.status_code");
	String servletName = (String) request
				.getAttribute("javax.servlet.error.servlet_name");
	String requestUri = (String) request
				.getAttribute("javax.servlet.error.request_uri");
		
	response.setContentType("text/html");
	 
	PrintWriter out = response.getWriter();
	out.write("<html><head><title>Ошибочка вышла!</title></head><body>");
	out.write("<h3>Ошибка</h3>");
	out.write("<ul><li>Сервлет:"+servletName+"</li>");
	out.write("<li>Исключение:"+throwable.getClass().getName()+"</li>");
	out.write("<li>Запрошенный URI:"+requestUri+"</li>");
	out.write("<li>Сообщение:"+throwable.getMessage()+"</li>");
	out.write("</ul>");
	out.write("</body></html>");
}

Но никакой красоты и элегантности от этого способа ожидать не стоит. По сути, мы создаём такую же уродливую страничку, которая никак не сочетается с оформлением приложения, как и стандартная страница об ошибке. Только тут мы ещё строчка за строчкой прописываем весь HTML код, только из сервлета. Ужасно.

Способ 2. Jsp-страница

Так как мне нужно было сохранить оформление, как на остальных страницах, то построчный вывод кода из прошлого способа вообще не подходил.Вместо этого я собираю нужную информацию (всё ту же информацию об ошибке) и отправляю её в jsp-страницу, не заморачиваясь с тем, как страница будет выглядеть. Мне кажется, так логичнее, ведь в обязанности сервлета не должен входить внешний вид страниц.

private void processError(HttpServletRequest request,
                              HttpServletResponse response) throws IOException, ServletException {
        Throwable throwable = (Throwable) request
                .getAttribute("javax.servlet.error.exception");

        Integer statusCode = (Integer) request
                .getAttribute("javax.servlet.error.status_code");

        String servletName = (String) request
                .getAttribute("javax.servlet.error.servlet_name");

        String requestUri = (String) request
                .getAttribute("javax.servlet.error.request_uri");

        SimpleDateFormat format = new SimpleDateFormat("dd.MM.yyyy HH:mm");
        request.setAttribute("date", format.format(new Date()));

        request.setAttribute("statusCode", statusCode);
        request.setAttribute("servletName", servletName);
        request.setAttribute("requestUri", requestUri);

        if (statusCode == 500) {
            request.setAttribute("stackTrace", throwable.getStackTrace());
            request.setAttribute("exception", throwable.getClass().getName());
        } else {
            request.setAttribute("stackTrace", null);
            request.setAttribute("exception", "Error");
        }

        request.getRequestDispatcher( "errorPage.jsp").forward(request, response);
        response.flushBuffer();

    }

Так как я вообще все нештатные ситуации решаю с помощью этого сервлета, то нужно было разграничить ситуации с кодом 500 и остальные. В первом случае у нас есть исключение, из которого можно достать информацию вроде названия класса, или сообщения с помощью getMessage (у меня в примере этого метода нет, но ничего не мешает вам его добавить к себе). Во втором случае throwable будет null, поэтому любая попытка достать оттуда любую информацию поломает всю систему и мы снова получим некрасивую стандартную страничку.

Ниже страница errorPage.jsp. Стили описаны в файле error.css, а меню приложения и футер добавляются подключением файлов nav.jsp и footer.jsp, соответственно, как и на всех других страницах приложения.

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<html>
<head>
    <%@include file="head.jsp"%>
    <%@include file="nav.jsp"%>
    <style><%@include file="/WEB-INF/css/error.css"%></style>

</head>
<body>

<main>
    <div class="container">
        <div class="main-wrapper">
            <div class="main-error-block">
                <div class="error-area">
                    <h4>Ошибка</h4>
                    <p>Покажите эту страницу с информацией об ошибке администратору системы, чтобы он мог найти причину</p>

                </div>

                <div class="error-info-area">
                    <table>
                        <tr>
                            <td class="error-label">Статус:</td>
                            <td class="error-info">${statusCode}</td>
                        </tr>
                        <tr>
                            <td class="error-label">Сервлет:</td>
                            <td class="error-info">${servletName}</td>
                        </tr>
                        <tr>
                            <td class="error-label">Адрес: </td>
                            <td class="error-info">${requestUri}</td>
                        </tr>
                        <tr>
                            <td class="error-label">Exception: </td>
                            <td class="error-info">${exception}</td>
                        </tr>
                        <tr>
                            <td class="error-label"><b>Дата: </b></td>
                            <td>${date}</td>
                        </tr>
                    </table>
                </div>


                <div class="stack-trace-area">
                    <p><b>Stack trace:</b></p>
                    <c:forEach items="${stackTrace}" var="list">
                        <p> ${list} </p>
                    </c:forEach>
                </div>

            </div>
        </div>
    </div>
</main>

<%@include file="footer.jsp"%>
</body>
</html>

И в итоге всё стало выглядеть намного лучше, чем изначально. Конечно, можно улучшать /обработку ещё и ещё, потому что условный оператор внутри processError не совсем то, что рекомендуют делать в чистом коде, но я пока остановился на этом варианте.

3 Comments

    1. как раз с программированием на английском языке очень много материалов, а на русском маловато, поэтому на русском выгоднее писать) а вот с музыкой наоборот — в мире больше фанатов, до переводов всё никак не доберусь))

      Нравится 1 человек

      1. Действительно, я на русском почти не находила материалов, когда нужно было. Но я уже и не искала на русском — сам поиск уже делала на английском.

        Нравится

Ответить на Egor Zimowski Отменить ответ