Во время работы JSF-приложения может возникать множество ошибок. Не все они видны пользователю — так, при AJAX-запросах пользователь не увидит никакой информации об ошибке. Кроме того, так как ошибок может возникать много у разных пользователей, есть необходимость в возможности найти в логах именно то исключение, которое привело к ошибке у пользователя — например, отображать пользователю некий код (временную метку), который будет отображаться и в логах.
В библиотеке Primefaces, которая используется в нашем проекте, есть что-то похожее. Становится возможной переадресация человека на страницу с информацией об ошибке — стектрейсом. Но стектрейс для конечного пользователя малоинформативен, так что необходимо отображать что-то лаконичное — точную временную метку, по которой будет происходить поиск в логах.
В JSF возможно написать и зарегестировать глобальный обработчик исключений. Для этого необходимо:
- Создать класс-обработчик, наследующийся от javax.faces.context.ExceptionHandler
- Реализовать фабрику для создания обработчика, наследующуюся от javax.faces.context.ExceptionHandlerFactory
- Зарегестрировать фабрику в 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;
}
}
Засим откланиваюсь, прощайте.