quinta-feira, 28 de novembro de 2013

Logar e Relançar - Antipattern de exceção


Quando se trata uma exceção, o correto é:
  • Ou envolver a exceção em uma nova exceção, passando mais informações;
  • Ou tratá-la, logando a exceção e exibindo ao usuário, quando for o caso. 
Mas nunca os dois (relançar e logar). Considere o Servlet abaixo. Neste exemplo, este é o nível mais alto da operação, então o papel do tratamento de exceção é informar ao usuário (logar e enviar uma mensagem para o Response).

public class ClienteServlet ...
    ...
    public void gravarCliente(ServletRequest req, ServletResponse res) {
        try {
            DAOCliente.gravar(criaClienteVo(req));
        } catch (RuntimeException e) {
            log.error("Erro ao salvar cliente", e);
            res.setAttribute("error", "Erro. Contate suporte.");
        } catch (BusinessException e) {
            log.error("Erro de negócio ao salvar cliente", e);
            res.setAttribute("error", "Erro ao salvar cliente " 
                    + e.getMessage());
        }
    }
}    

Exemplo de Antipattern 1

No caso de um código que loga e relança, como o código abaixo, teremos a saída mostrada na sequência. Note que a exceção é mostrada 2 vezes, mas de maneiras diferentes. 

public class DAOCliente ...
    ...
    public void gravar(ClienteVo cliente) throws BusinessException {
        try {
            ...
            ... COMANDO INSERT
            ...
        } catch (SQLException e) {
            log.error("Erro ao salvar cliente", e);
            throw new BusinessException("Erro no banco", e);
        }
    }
}

13:40:02 [ERROR] Erro ao salvar cliente
java.sql.SQLException: ORA-00001: unique constraint ...
    at oracle.driver...SomeOracleDriverClass()
    at DAOCliente.gravar(DAOCliente.java:50)
    at ClienteServlet.gravarCliente(ClienteServlet.java:85)
13:40:02 [ERROR] Erro de negócio ao salvar cliente
BusinessException: Erro no banco
    at DAOCliente.gravar(DAOCliente.java:63)
    at ClienteServlet.gravarCliente(ClienteServlet.java:85)
Caused by: java.sql.SQLException: ORA-00001: unique constraint ...
    at oracle.driver...SomeOracleDriverClass()
    at DAOCliente.gravar(DAOCliente.java:50)
    ... 2 more

Exemplo de Antipattern 2

Ou ainda como o código abaixo, exatamente a mesma exceção é logada 2 vezes.

public class DAOCliente ...
    ...
    public void gravar(ClienteVo cliente) {
        try {
            ...
            ... COMANDO INSERT
            ...
        } catch (SQLException e) {
            log.error("Erro ao salvar cliente, e);
            throw new e;
        }
    }
}

13:40:02 [ERROR] Erro ao salvar cliente
java.sqlSQLException: ORA-00001: unique constraint ...
    at oracle.driver...SomeOracleDriverClass()
    at DAOCliente.gravar(DAOCliente.java:50)
    at ClienteServlet.gravarCliente(ClienteServlet.java:85)
13:40:02 [ERROR] Erro de negócio ao salvar cliente
java.sql.SQLException: ORA-00001: unique constraint ...
    at oracle.driver...SomeOracleDriverClass()
    at DAOCliente.gravar(DAOCliente.java:50)
    at ClienteServlet.gravarCliente(ClienteServlet.java:85)

Código esperado

O código deve OU relançar uma exceção (caso seja um nível intermediário ou inferior) OU logar e informar ao usuário (caso seja o nível superior). No exemplo abaixo, como não é a classe de nível superior, ela deve apenas relançar a exceção. Com isso, o log fica com apenas uma única ocorrência.

public class DAOCliente ...
    ...
    public void gravar(ClienteVo cliente) {
        try {
            ...
            ... COMANDO INSERT
            ...
        } catch (SQLException e) {
            throw new BusinessException("Erro no banco", e);
        }
    }
}

13:40:02 [ERROR] Erro de negócio ao salvar cliente
BusinessException: Erro no banco
    at DAOCliente.gravar(DAOCliente.java:63)
    at ClienteServlet.gravarCliente(ClienteServlet.java:85)
Caused by: java.sql.SQLException: ORA-00001: unique constraint ...
    at oracle.driver...SomeOracleDriverClass()
    at DAOCliente.gravar(DAOCliente.java:50)
    ... 2 more

terça-feira, 19 de novembro de 2013

Exceção Java, torne-se sua aliada

Quem é que gosta quando a sua aplicação retorna com alguma exceção? Acho que ninguém, não é mesmo? E um atendente que está em um atendimento telefônico tentando registrar uma solicitação do cliente no sistema e acontece algum erro? O atendente e o cliente também não vão gostar nem um pouco de receber a exceção.
Sentimento do usuário quando verifica uma mensagem genérica de erro

E o que deve ser feito? Deve mostrar o problema claro e preciso possível para o atendente ou para o operador, para se tenha total condição de resolver o problema o quanto antes. 
Sempre que acontece um erro, a JVM gera uma classe que contém as informações sobre o problema. Esta classe estende Throwable. Existem 3 tipos destas classes:

  • Exceções verificadas (checked exceptions) - exceções que devem ser tratadas na cláusula throws de um método ou catch de um bloco try. Estas exceções estendem a classe Exception. Problemas de comunicação com sistema externo, ou problemas de entrada do usuário, são alguns dos exemplos destas exceções. 
  • Exceções não-verificadas (unchecked exceptions) - exceções que não devem ser tratadas na cláusula throws de um método ou catch de um bloco try. Estas exceções estendem a classe RuntimeException. Em geral são problemas não esperados, podendo até mesmo indicar um bug no sistema. O mais comum é o NullPointerException
  • Erros - são problemas na JVM que, em geral, não são recuperáveis. Estendem a classe Error. Exemplo: OutOfMemoryError, LinckageError, e StackOverflowError
Mesmo as vezes sendo Error, e estendendo Throwable, o conjunto de todas estas classes, normalmente são chamadas de Exceções (ou Exception).
Diagrama de classes referente as exceções (Throwable)

E é importante entender que a exceção (ou Throwable) tem 4 componentes e TODOS eles são importantes:

  1. Classe - indica qual tipo de erro ocorreu
  2. Mensagem (opcional) - detalha o erro com uma mensagem, quando for o caso
  3. Stack Trace - indica em que ponto do código aconteceu o erro, e toda a pilha de métodos que foi chamada para chegar neste ponto
  4. Cause (opcional) - indica qual foi a exceção original, quando for o caso

segunda-feira, 11 de novembro de 2013

Boas práticas para o uso de logs em Java

O JDK vem com o pacote java.util.logging que tem a biblioteca para tratamento de log nativa e o Log4J do projeto Apache é a opção log mais utilizada. O Log4J tem mais features que o java.util.logging. O Common Logging também da Apache é apenas um wrapper (uma casca) para encobrir o uso de qualquer mecanismo de log, inclusive o Log4J e java.util.logging.

Uma mensagem em qualquer mecanismo de log pode ter níveis de severidade diferentes. Estes níveis se dividem basicamente em:

  • fatal - este nível deve ser utilizado em casos extremos, em geral mensagens que impacta negativamente todo o ambiente (e não apenas uma sessão de um usuário) e exige uma ação não de um operador, mas sim do administrador do ambiente e do arquiteto do sistema
  • error - este nível representa uma exceção Java que ocorreu durante a execução de uma operação de um usuário ou a execução de uma tarefa. Pode-se enviar mensagens neste nível para alguma inconsistência identificada pelo próprio sistema, mesmo que não gere exceção. 
  • warn - este nível é utilizado para alguma informação que seja interessante ser registrada, que pode representar um problema, mas o sistema se recuperou e não deixou de executar a tarefa.
  • info - este nível de mensagem é utilizado para indicar o que é que a aplicação está fazendo em alto nível. 
  • debug - este nível de mensagem, são de baixo nível, onde detalha resultados intermediários de uma tarefa, e são utilizados, normalmente para depuração de um trecho da aplicação
Ata da Reunião - cada anotação tem uma severidade diferente
Não se deve usar a saída padrão (System.out) ou a saída de erro (System.err). Deve sempre usar o log. O log é configurável, você habilita e desabilita seus níveis, permite escrever em diversos destinos diferentes (console, arquivo, banco de dados, rede, etc), seleciona o nível por pacote Java, permite rotacionar, informar data e hora para toda mensagem, entre outras vantagens. Escrever na saída padrão ou de erro não permite nada disso, o que pode tornar a informação volumosa e confusa. 

Quando na aplicação se escreve em níveis mais baixo, principalmente o nível debug, é importantíssimo utilizar isDebugEnabled() por questão de otimização. É normal que, quanto menor o nível, mais mensagens existam, mas é normal que você não queira ver as mensagens de log de todos os módulos da aplicação, mas apenas de alguns. Então deve-se habilitar o log apenas destes módulos.

log.debug("Retorno cliente " + cliente + ", Status=" + status);

No exemplo acima, sempre irá concatenar a string, para chamar o método log.debug, para verificar se está habilitado ou não o log para este pacote Java. Caso esteja, escreve no log normalmente, mas caso não esteja, o Java realizou a concatenação da string desnecessariamente. Agora imagine este tipo de código em um loop e em diversos pacotes diferentes, sendo que o modo debug não está ligado. É uma concatenação desnecessária que ocorre com todos os log.debug. Por questão de otimização, deve-se verificar o nível de log antes de concatenar a string do debug.


if (log.isDebugEnabled()) {
    log.debug("Retorno cliente " + cliente + ", Status=" + status);
}

Logando exceção

Quando o código encontra uma exceção, ele deve:

  • tratar a exceção - se seu código pode tratar a exceção (ex.: falha de rede), então deve fazê-lo; ou
  • relançar a mesma exceção - em caso de exceção unchecked; ou
  • reencapsular a exceção - no caso de exceções checked; ou
  • logar e exibir ao usuário - se em nenhum outro ponto foi possível tratar a exceção, em geral no nível mais alto da stack trace (na Action, ou onMessage, ou no método main), então a mensagem deve ser logada e informada ao usuário.
Sempre que reencapsular a exceção, a nova exceção precisa fazer bom uso dos dois parâmetros que existem nos construtores das exceções:
  • message - informar a atividade que estava tentando ser realizada quando a exceção ocorreu, preferencialmente informando algum parâmetro de entrada para ajudar a identificar o erro; e
  • cause - é a exceção original
Um exemplo de como relançar a exceção pode ser conferido a seguir:


void salvar(Cliente c) {
    try {
        DAOCliente.salvar(c);
    } catch (SQLException ex) {
        throw new AppException(
            "Erro de banco ao salvar cliente " // ação com erro
            + c != null ? c.getNome() : null, // um parâm. de entrada
            ex); // exceção original
    }
}

Da mesma maneira, estes dois parâmetros são de extrema importância durante o log no nível mais alto de erro:

void doGet(HttpServletRequest req, HttpServletResponse resp) {
    try {
        // . . .
        businessClient.salvar(c);
        // . . .
    } catch (AppException ex) {
        log.error(
            "Erro ao tentar cadastrar cliente " // ação com erro
            + req.getParameter("nome"), // um parâmetro de entrada
            ex); // exceção original
    }
}