Статьи

Изящно разбираться с ошибками в Galleria — часть 6

6 Пример Galleria Java EE приложений растет. Сегодня я собираюсь написать о том, как изящно бороться с ошибками. Много уже сделано для проверки пользовательского ввода, но все еще есть много ситуаций сбоя, которые не обрабатываются, но должны быть. Если вам интересно, что произошло в прошлом, ознакомьтесь с первыми частями серии: 
Основы ,
запуск на GlassFish ,
запуск на WebLogic ,
тестирование и
повышение безопасности .

Общий механизм исключений

Приложение использует проверенные исключения для передачи ошибок между уровнями. ApplicationException является корнем всех возможных бизнес-исключений.

Эти бизнес-исключения сообщают о нарушениях проверки и всех известных ошибках между доменом и уровнем представления. Классы <domain> Manager (например, AlbumManger) в проекте представления galleria-jsf ловят их и используют ExceptionPrecessor для заполнения сообщений об ошибках в представлении. Другой вид исключений, которые могут возникнуть между этими двумя уровнями, — RuntimeExceptions. Они помещаются в EJBException контейнером, а также перехватываются классами диспетчера <domain>. Они генерируют более общее сообщение об ошибке, которое показывается пользователю.

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

Чего не хватает? ViewExpired и многое другое.

Кажется, как будто все решается прямо сейчас. Но только на первое впечатление. Откройте экран входа в систему и немного подождите, и время ожидания http сессии истечет. Теперь вы встретили не очень хороший экран исключений ViewExpired.

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

<error-page>
    <exception-type>javax.faces.application.ViewExpiredException</exception-type>
    <location>/viewExpired.xhtml</location>
</error-page>

Теперь вы перенаправляете своих пользователей на выделенную страницу, которая может рассказать ему / ей что-то приятное о безопасности на рабочем месте и не оставлять приложение без присмотра в течение столь длительного времени. Это работает для большинства приложений там. Если вы хотите иметь некоторую дополнительную информацию на странице или просто хотите перехватить несколько исключений и обработать их по отдельности, не настраивая их статически, вам нужно нечто, называемое ExceptionHandler. Это новое в JSF 2, и все, что вам нужно сделать, это реализовать ExceptionHandler и его фабрику. Сама фабрика настроена в facex-config.xml, потому что для нее нет аннотаций.

Откройте файл face-config.xml и добавьте следующие строки внизу: 

<factory>
     <exception-handler-factory>info.galleria.handlers.GalleriaExceptionHandlerFactory</exception-handler-factory>
 </factory> 

Теперь мы собираемся реализовать GalleriaExceptionHandlerFactory в выделенном пакете. Интересный метод здесь:

@Override
   public ExceptionHandler getExceptionHandler() {
       ExceptionHandler result = parent.getExceptionHandler();
       result = new GalleriaExceptionHandler(result);
       return result;
   }

Он вызывается один раз за запрос, и каждый раз он должен возвращать новый экземпляр ExceptionHandler. Здесь вызывается реальный ExceptionHandlerFactory, и ему предлагается создать экземпляр, который затем оборачивается в пользовательский класс GalleriaExceptionHandler . Здесь происходят действительно интересные вещи.

@Override
public void handle() throws FacesException {
    for (Iterator<ExceptionQueuedEvent> i = getUnhandledExceptionQueuedEvents().iterator(); i.hasNext();) {
        ExceptionQueuedEvent event = i.next();
        ExceptionQueuedEventContext context = (ExceptionQueuedEventContext) event.getSource();
        Throwable t = context.getException();
        if (t instanceof ViewExpiredException) {
            ViewExpiredException vee = (ViewExpiredException) t;
            FacesContext fc = FacesContext.getCurrentInstance();
            Map<String, Object> requestMap = fc.getExternalContext().getRequestMap();
            NavigationHandler nav =
                    fc.getApplication().getNavigationHandler();
            try {
                // Push some stuff to the request scope for later use in the page
                requestMap.put("currentViewId", vee.getViewId());
                nav.handleNavigation(fc, null, "viewExpired");
                fc.renderResponse();
 
            } finally {
                i.remove();
            }
        }
    }
    // Let the parent handle all the remaining queued exception events.
    getWrapped().handle();
}

 

Выполните итерацию по необработанным исключениям, используя итератор, возвращенный из getUnhandledExceptionQueuedEvents (). Iterator (). ExeceptionQueuedEvent — это SystemEvent, из которого вы можете получить фактическое исключение ViewExpiredException. Наконец, вы извлекаете некоторую дополнительную информацию из исключения и помещаете ее в область запроса для доступа к ней через EL на следующей странице. Последнее, что нужно сделать для исключения ViewExpiredException, это использовать неявную систему навигации JSF («viewExpired» разрешается в «viewExpired.xhtml») и перейти на страницу «viewExpired» через NavigationHandler. Не забудьте удалить обработанное исключение в блоке finally. Вы не хотите, чтобы это снова обрабатывалось родительским обработчиком исключений. Теперь нам нужно создать страницу viewExpired.xhtml. Сделайте это в папке galleria-jsf \ src \ main \ webapp. 

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE composition PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<ui:composition xmlns:ui="http://java.sun.com/jsf/facelets"
                template="./templates/defaultLayout.xhtml"
                xmlns:f="http://java.sun.com/jsf/core"
                xmlns:h="http://java.sun.com/jsf/html"
                >
    <ui:define name="title">
        <h:outputText value="#{msg['Exception.page.title']}" />
    </ui:define>
 
    <ui:define name="content">
        <h:form>
            <h:outputText value="#{msg['Exception.page.message']}" />
            <p>You were on page #{currentViewId}.  Maybe that's useful.</p>
            <p>Please re-login via the <h:outputLink styleClass="homepagelink" value="#{request.contextPath}/Index.xhtml" ><h:outputText value="Homepage" /></h:outputLink>.</p>
        </h:form>
    </ui:define>
</ui:composition>

Обратите внимание, что я добавил новые свойства сообщений здесь, поэтому вам необходимо убедиться, что они помещены в galleria-jsf \ src \ main \ resources \ resources \ messages.properties и translations.
До сих пор это, очевидно, обрабатывает только один особый случай исключения. Вы могли бы расширить это, чтобы обращаться с другими также. Теперь, когда у нас есть базовый механизм, вы можете это сделать.

Refactoring the RuntimeException handling
As I said, I am not happy with the way the application is handling RuntimeExceptions. Now that we have a nice central exception handling in place we can move those stuff around a bit and refactor the *Manager classes. Delete all those    catch (EJBException ejbEx) { blocks from all of them. We are going to take care of them in the GalleriaExceptionHandler in a minute. Simply add an another check to the GalleriaExceptionHandler and redirect the user to another page if any other exception than a ViewExpiredException is thrown.

// check for known Exceptions
           if (t instanceof ViewExpiredException) {
               ViewExpiredException vee = (ViewExpiredException) t;
               // Push some stuff to the request scope for later use in the page
               requestMap.put("currentViewId", vee.getViewId());
 
           } else {
               forwardView = "generalError";
 
               Locale locale = fc.getViewRoot().getLocale();
               String key = "Excepetion.GeneralError";
               logger.error(Messages.getLoggerString(key), t);
               String message = Messages.getString(key, locale);
               FacesMessage facesMessage = new FacesMessage(FacesMessage.SEVERITY_ERROR, message, null);
               fc.addMessage(null, facesMessage);
           }

This approach has some advantages. It reduces the needed code in the *Manager classes and we finally have a central place to take care of those unrecoverable exceptions. This still is not very enterprise like. Imagine your first level support team needs to look after customers and they start complaining that the only message they get is a «GeneralError». That is not very helpful. You support team would need to escalate it and second or third level would need to check the logs and and and .. All this because of an error, that we could have know. First thing to do is to find out about the causing error. Parsing stack traces isn’t big fun. Especially not of RuntimeExceptions that are wrapped in EJBExceptions and further on in FacesExceptions. Thank god for the Apache Commons ExceptionUtils. Open your galleria-jsf pom.xml and add them as dependency: 

<dependency>
          <groupId>commons-lang</groupId>
          <artifactId>commons-lang</artifactId>
          <version>2.6</version>
      </dependency>

Now you can start to examine the root cause: 

} else {
               forwardView = "generalError";
               // no known instance try to specify
 
               Throwable causingEx = ExceptionUtils.getRootCause(t);
               if (causingEx == null) {
                   causingEx = t;
               }
//...
 logger.error(Messages.getLoggerString(key), t);
requestMap.put("errorCode", errorCode);

Don’t forget to also log the complete stack-trace (t and not only causingEx) here. In general it’s a bad thing to let users know about exceptions. Nobody really wants to see errors happen (because we do hate making mistakes) and after all exception stack-traces could disclose sensitive information which you wouldn’t like to see on a screen somewhere. So you need to find a way to display something meaningful to the user without disclosing too much. That is where the famous error-codes come into play. Use the root-cause exception as message key or make your own decisions on what effort you are going to put in here. It might be a system of categories of errors (db, interface systems, etc.) which give the first-level support a good hint about what was causing the error. I would stick to a simpler solution from the beginning. Simply generate a UUID for every caught exception and trace it to both the log and the UI. A very simple one could be the following.

String errorCode = String.valueOf(Math.abs(new Date().hashCode()));

This should also be added to the message properties and don’t forget that you need another one for the generalError template. If slf4j would use the same message format than jdk logging does you would only need one property .. anyway:

Exception.generalError.log=General Error logged: {}.

Exception.generalError.message=A general error with id {0} occured. Please call our hotline.

Add this to the generalError.xhtml and see how the error code is passed to the message template.

<h:outputFormat  value="#{msg['Exception.generalError.message']}" >
               <f:param value="#{errorCode}"/>
           </h:outputFormat>

There is still a lot to improve on here. You could use the javax.faces.application.ProjectStage to lookup the current mode the application is running in. If you are running in ProjectStage.Development you could also put the complete stack-trace to the UI and make debugging live a bit easier. The following snippet is trying to get the ProjectStage from JNDI.

public static boolean isProduction() {
        ProjectStage stage = ProjectStage.Development;
        String stageValue = null;
        try {
            InitialContext ctx = new InitialContext();
            stageValue = (String) ctx.lookup(ProjectStage.PROJECT_STAGE_JNDI_NAME);
            stage = ProjectStage.valueOf(stageValue);
        } catch (NamingException | IllegalArgumentException | NullPointerException e) {
            logger.error("Could not lookup JNDI object with name 'javax.faces.PROJECT_STAGE'. Using default 'production'");
        }
        return ProjectStage.Production == stage;
    }

What about the 3-digit Http Error Pages?
That is another thing to take care of. All those remaining 3-digit http error codes which return one of those not nice looking error pages. The only thing to do this is to map them in the web.xml like shown in the following:

<error-page>
        <error-code>404</error-code>
        <location>/404.xhtml</location>
    </error-page>

 

You should make sure to have those mappings in place and present a meaningful error to your users. It should become best practice to always offer a way to navigate further from there.