null

Переадресация на страницу ошибки при возникновении исключения в JSF

Во время работы JSF-приложения может возникать множество ошибок. Не все они видны пользователю — так, при AJAX-запросах пользователь не увидит никакой информации об ошибке. Кроме того, так как ошибок может возникать много у разных пользователей, есть необходимость в возможности найти в логах именно то исключение, которое привело к ошибке у пользователя — например, отображать пользователю некий код (временную метку), который будет отображаться и в логах.

В библиотеке Primefaces, которая используется в нашем проекте, есть что-то похожее. Становится возможной переадресация человека на страницу с информацией об ошибке — стектрейсом. Но стектрейс для конечного пользователя малоинформативен, так что необходимо отображать что-то лаконичное — точную временную метку, по которой будет происходить поиск в логах.

В JSF возможно написать и зарегестировать глобальный обработчик исключений. Для этого необходимо:

  1. Создать класс-обработчик, наследующийся от javax.faces.context.ExceptionHandler
  2. Реализовать фабрику для создания обработчика, наследующуюся от javax.faces.context.ExceptionHandlerFactory
  3. Зарегестрировать фабрику в faces-config.xml

Создадим основу для обработчика.

Класс-обработчик:

@Slf4j
public class JsfExceptionHandler extends ExceptionHandlerWrapper {
    private final ExceptionHandler wrapped;

    public JsfExceptionHandler(ExceptionHandler wrapped) {
        this.wrapped = wrapped;
    }

    @Override
    public ExceptionHandler getWrapped() {
        return wrapped;
    }
}

Фабрика:

public class JsfExceptionHandlerFactory extends ExceptionHandlerFactory {
    private final ExceptionHandlerFactory parent;

    public JsfExceptionHandlerFactory(ExceptionHandlerFactory parent) {
        this.parent = parent;
    }

    @Override
    public ExceptionHandler getExceptionHandler() {
        return new JsfExceptionHandler(parent.getExceptionHandler());
    }
}

faces-config.xml

<?xml version="1.0" encoding="UTF-8"?>
<faces-config xmlns="http://xmlns.jcp.org/xml/ns/javaee"
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee/web-facesconfig_2_2.xsd"
              version="2.2">

    <application>
        <el-resolver>
            org.springframework.web.jsf.el.SpringBeanFacesELResolver
        </el-resolver>
        <resource-bundle>
            <base-name>ru.edu.portfolio.i18n.Messages</base-name>
            <var>i18n</var>
        </resource-bundle>
    </application>

    <factory>
        <exception-handler-factory>
            com.example.jsf.JsfExceptionHandlerFactory
        </exception-handler-factory>
    </factory>

</faces-config>

Далее, необходимо обработать исключения. Для этого нужно переопределить метод handle обработчика, для доступа к необработаным исключениям нужно вызвать метод getUnhandledExceptionQueuedEvents() базового класса.

Напишем пример для переадресации запроса на страницу error.xhtml, в логи будем записывать стектрейс и временную метку, эту же метку добавим в параметры запроса при переадресации.

@Override
public void handle() throws FacesException {
    FacesContext facesContext = FacesContext.getCurrentInstance();
    ExternalContext externalContext = facesContext.getExternalContext();
    for (ExceptionQueuedEvent event : getUnhandledExceptionQueuedEvents()) {
        Throwable exception = event.getContext().getException();
        long time = System.currentTimeMillis();
        log.error("An exception occurred during response render. Error code [{}], reason ", time, exception);
        String redirectUrl = "/error.xhtml?errorTimestamp=" + time;
       
        try {
            externalContext.redirect(redirectUrl);
        } catch (IOException e) {
            log.error("Unexpected error during faces redirect", e);
        }
        facesContext.responseComplete();
    }
}

Страница error.xhtml

<html>
    <f:view xmlns="http://www.w3c.org/1999/xhtml" xmlns:f="http://java.sun.com/jsf/core"
            xmlns:h="http://java.sun.com/jsf/html" xmlns:ui="http://xmlns.jcp.org/jsf/facelets">
        <h:head>
            <title>Title here</title>
        </h:head>
        <h:body style="height: 100vh">
            <h:outputText value="An error occured #{param['errorTimestamp']}"/>
        </h:body>
    </f:view>
</html>

Данная реализация работает с не-AJAX запросами. Но в ответе на AJAX-запрос может присутствовать тег redirect с url для переадресации — попробуем сделать так. Произведем проверку на то, является ли запрос AJAX, если да — сбросим ответ (ибо к моменту возникновения ошибки он уже начал формироваться) и перезапишем в него переадресацию. Получается что-то такое:

@Override
public void handle() throws FacesException {
    FacesContext facesContext = FacesContext.getCurrentInstance();
    ExternalContext externalContext = facesContext.getExternalContext();
    for (ExceptionQueuedEvent event : getUnhandledExceptionQueuedEvents()) {
        Throwable exception = event.getContext().getException();
        long time = System.currentTimeMillis();
        log.error("An exception occurred during response render. Error code [{}], reason ", time, exception);
        String redirectUrl = "/error.xhtml?errorTimestamp=" + time;
        boolean isAjax = facesContext.getPartialViewContext().isAjaxRequest();
        if (isAjax) {
            PartialResponseWriter writer = facesContext.getPartialViewContext().getPartialResponseWriter();
            try {
                externalContext.responseReset();

                writer.startDocument();
                writer.redirect(redirectUrl);
                writer.endDocument();

            } catch (IOException e) {
                log.error("Unexpected error during ajax request redirect", e);
            }
        } else {
            try {
                externalContext.redirect(redirectUrl);
            } catch (IOException e) {
                log.error("Unexpected error during faces redirect", e);
            }
        }
        facesContext.responseComplete();
    }
}

И... это не работает. Посмотрим на ответ, чтобы разобраться в причине:

<?xml version='1.0' encoding='UTF-8'?>
<partial-response id="j_id1"></changes><redirect url="/error.xhtml?errorTimestamp=1529506232178"></redirect></partial-response>

ВНЕЗАПНО в ответе пристутствует закрывающий тег </changes>. Поиск причин привел к странностям в PartialResponseWriter#redirect — в данном методе сперва происходит проверка на то, открыт ли тег changes, и если да, то происходит его закрытие. Должно быть, сброс ответа через ExternalContext не повлиял на внутреннее состояние объекта PartialResponseWriter. Одно из возможных решений — подпереть все костылем, и добавить запись redirect'а до сброса ответа, что приведет к обновлению состояния PartialResponseWriter. И это сработало! Итоговый код обработчика получился такой:

package com.example.jsf;

import lombok.extern.slf4j.Slf4j;

import javax.faces.FacesException;
import javax.faces.context.ExceptionHandler;
import javax.faces.context.ExceptionHandlerWrapper;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.faces.context.PartialResponseWriter;
import javax.faces.event.ExceptionQueuedEvent;
import java.io.IOException;

/**
 * Handle exceptions in JSF pages - redirect all to /error.xhtml.
 */
@Slf4j
public class JsfExceptionHandler extends ExceptionHandlerWrapper {
    private final ExceptionHandler wrapped;

    public JsfExceptionHandler(ExceptionHandler wrapped) {
        this.wrapped = wrapped;
    }

@Override
public void handle() throws FacesException {
    FacesContext facesContext = FacesContext.getCurrentInstance();
    ExternalContext externalContext = facesContext.getExternalContext();
    for (ExceptionQueuedEvent event : getUnhandledExceptionQueuedEvents()) {
        Throwable exception = event.getContext().getException();
        long time = System.currentTimeMillis();
        log.error("An exception occurred during response render. Error code [{}], reason ", time, exception);
        String redirectUrl = "/error.xhtml?errorTimestamp=" + time;
        boolean isAjax = facesContext.getPartialViewContext().isAjaxRequest();
        if (isAjax) {
            PartialResponseWriter writer = facesContext.getPartialViewContext().getPartialResponseWriter();
            try {
                //Workaround for JSF - it cause closing changes and other tags.
                writer.redirect(redirectUrl);
                externalContext.responseReset();

                writer.startDocument();
                writer.redirect(redirectUrl);
                writer.endDocument();

            } catch (IOException e) {
                log.error("Unexpected error during ajax request redirect", e);
            }
        } else {
            try {
                externalContext.redirect(redirectUrl);
            } catch (IOException e) {
                log.error("Unexpected error during faces redirect", e);
            }
        }
        facesContext.responseComplete();
    }
}

    @Override
    public ExceptionHandler getWrapped() {
        return wrapped;
    }
}

Засим откланиваюсь, прощайте.