Tratamento de exceções em Java

Todo o desenvolvedor de software já passou pela situação de ter que caçar um bug no código do projeto. Na grande maioria das vezes o problema inicia-se com a queixa de um usuário sobre o insucesso ao tentar utilizar uma funcionalidade qualquer. Geralmente ele liga para o suporte e reporta uma mensagem de erro. Outras vezes você tem um pouco mais de artefatos como screenshots e códigos de erros. Nesse tipo de situação, qualquer evidência pode te poupar de perder tempo (dinheiro) e de dores de cabeça. É exatamente por isso que venho estudando boas práticas no que tange o tratamento de exceções e erros há algum tempo. Um pequeno conjunto de diretrizes neste sentido pode te poupar de horas e horas de transtornos.

Em uma das minhas pesquisas, um dos melhores “guias” – vamos chamar assim – que encontrei foi uma resposta à uma questão do StackOverflow resumindo em 10 tópicos as boas práticas do tratamento de exceções. É a partir dessa resposta que estou escrevendo este post. A partir dela vou descambar para outros materiais que venho utilizando.

Tipos de exceções em Java

A estrutura de exceções em Java está definida da seguinte forma (se você não está familiarizado com as exceções em Java, dê uma lida neste post da Caelum):

Throwable, embora esteja nomeada segundo a orientação de se nomear interfaces (sufixo “able”) é na verdade uma classe concreta e permite que, além dela, suas especializações sejam lançadas (cláusula throw*) e capturadas (bloco try-catch) no seu código. Não vamos falar de Errors aqui, contudo, cabe ressaltar que eles são irreversíveis e quase sempre vão interromper a execução da aplicação.

Exceções verificadas

A classe java.lang.Exception é a raiz das exceções em Java e, por natureza, é uma exceção verificada (checked exception). Exceções verificadas são uma das característica do Java que faz com que o desenvolvedor tome algumas medidas em relação à exceção lançada: tratá-la (bloco try-catch) ou propagá-la (assinar o métodos com a cláusula throws**) para alguma outra rotina tratar. Todas as classes que derivam imediatamente de Exception são exceções verificadas, exceto RuntimeException e suas especializações.

Exceções não-verificadas

A classe java.lang.RuntimeException é uma derivada especial de Exception que tem como característica principal não ser verificada (unchecked exception). Exceções não-verificadas podem ser lançadas a qualquer momento e não necessariamente precisam ser tratadas ou propagadas.

Estrutura das exceções

Uma exceção é uma classe Java comum; com construtores, métodos e atributos. Se você não tem o source code do Java instalado, dê uma olhada no GrepCode. A classe, despida do JavaDoc, não passa de vinte e poucas linhas de código sobrescrevendo o próprio construtor:


package java.lang;

public class Exception extends Throwable {

    static final long serialVersionUID = -3387516993124229948L;

    public Exception() {
        super();
    }

    public Exception(String message) {
        super(message);
    }

    public Exception(String message, Throwable cause) {
        super(message, cause);
    }

    public Exception(Throwable cause) {
        super(cause);
    }
}

E para estendê-la é necessário nada mais do que:


public class MyOwnException extends Exception {}

Sim! Uma linha de código. A maior parte do comportamento de Exception está encapsulado na superclasse dela – Throwable – e serve basicamente para auxiliar o rastreamento da origem da exceção na pilha de chamadas de métodos (dê uma olhada em StrackTraceElement).

A grande maioria das extensões de Exception com as quais me deparei na minha vida de programador não faziam nada a mais do que isso. Um pecado! O simples fato de você adicionar algum comportamento extra em uma especialização da classe Exception, seja ela qual for, já seria uma mão na roda na sua leitura de stacktrace em busca de erros. Mas não vamos falar disso aqui. Deixa isso para outra ocasião.

Tratando exceções

Com base na resposta do Vineet Reynolds no StackOverflow, que divide o tratamento de exceções em 10 tópicos primordiais,  pretendo aqui implementá-lo com todo o resto que encontrei por aí, seja em livros, fóruns, vídeos e podcasts. Eu tomei a liberdade de modificá-los um pouco, às vezes trocando a ordem, outras removendo ou adicionando algum tópico de forma que faça mais sentido pra mim. Você, no entanto, está livre para (e deve) dar uma lida na resposta original.

Lance imediatamente

Assim que o código encontra uma situação adversa, é interessante lançar a exceção. Isso ajuda a rastrear a linha onde o erro foi ocasionado.

Encapsule a exceção original

Em complemento ao tópico anterior, se você vai lançar uma exceção nova, ao invés de propagar a que foi gerada, encapsule a original. Isso faz com que seja possível rastrear a pilha inteira de erros até se chegar à causa:


try {
    // código que gera a exception
} catch(Exception originalException) {
    throw new MyOwnException(“Erro ao tentar fazer qualquer coisa”,
        originalException);
}

log ^ throw

Você loga ou (XOR) você lança. Simples assim. Nunca faça os dois! Logar uma exceção relançá-la vai gerar pilhas imensas de log, vai dificultar a leitura do arquivo de saída e consequentemente a busca por origens de bugs.

try {
    // código que gera a exception
} catch(Exception ex) {
    LOG.warn(“Problema ao tentar…”);
    // qualquer coisa que você for fazer para tratar a exception.
    // menos relançá-la
}

 Ou


try {
    // código que gera a exception
} catch(Exception ex) {
    throw new MyOwnException(
        “Erro ao tentar fazer qualquer coisa”, ex);
}

Só capture exceções que você puder tratar

Parece óbvio, não? Se o código tenta abrir um arquivo e o arquivo não existe a pergunta é: devo tratar o IOException lançado? Sim:


public File openConfigFile() {
    try {
        // código que abre o arquivo
    } catch(IOException ex) {
        LOG.warn(“Não foi possível abrir o arquivo: “
            + ex.getMessage());
        return createNewFile();
    }
}

Alternativamente, você pode decidir que o código que está chamando o método openConfigFile deve tomar alguma medida em relação à exceção:


public File openConfigFile() throws IOException {
    // código que abre o arquivo
}

Ou simplesmente você pode decidir que não há nada a fazer:


public File openConfigFile() {
    try {
        // código que abre o arquivo
    } catch(IOException ex) {
        throw new RuntimeException(“Erro ao abrir arquivo”, ex);
    }
}

Opte por exceções da API sempre que puder

A linguagem Java já especifica uma gama de exceções que são usadas dentro das próprias APIs. É encorajado reutilizá-las ao invés de criar uma série de outras exceções dentro da sua própria aplicação. Por exemplo, um método pode receber uma String representando uma data e, antes de gravá-la no banco de dados, ele pode tentar convertê-la em um objeto java.util.Date. Para isso, foi definido que a data tem que estar no padrão ISO 8601: yyyy-mm-dd. O código poderia, por exemplo, especificar (criar) uma InvalidDateFormatException ou qualquer outra coisa parecida. Mas porque não simplesmente lançar uma:

throw new IllegalArgumentException(“Formato de data inválido”);

do pacote java.lang? IllegalArgumentException é uma exceção não-verificada, já que certamente não há o que fazer neste caso. Nada mais justo do que propagá-la para para as camadas mais acima, nas quais ela possa ser logada e eventualmente ter a mensagem de erro ao usuário final.

Lance exceções não-verificadas para erros programação

No caso anterior, com a data em formato inválido, não há o que ser feito. Uma alternativa seria utilizar a data atual (new Date()) dentro do bloco catch, mas isso vai sempre depender do domínio do seu problema. Entre elas está a própria IllegalArgumentException, mencionada no tópico anterior, e qualquer outra que estenda RuntimeException, como IllegalStateException para objetos em estados inválidos, UnsupportedOperationException para operações não implementadas ou métodos não mais suportados (removidos de uma API, por exemplo). Vale frisar que – seja lá qual for a ocasião – nunca lance um NullPointerException e se você não consegue entender o porquê, dá uma lida nisso aqui. Hehehe. Brincadeira =). NullPointerException é uma condição adversa muito especifica: foi invocada uma operação em uma referência de objeto que estava nula. Você consegue imaginar em que situações isso aconteceria? Pois bem:


if(obj == null) {
    throw new NullPointerException(“Objeto obj null”);
} else {
    obj.doSomething();
}

Se você não conseguir entender o que está errado no código acima, sugiro que você então dê uma olhada neste link. E desta vez não é brincadeira.

Lance exceções verificadas quando a exceção deve ser tratada por quem invocou

Eu sei que você questionou o exemplo anterior da data, por isso vou reiterá-lo aqui. Se, por qualquer razão, você espera que o cliente do seu método (o método que o está invocando) trate a situação adversa da data não estar no formato esperado, aí sim: cria uma exception verificada do tipo InvalidDateFormatException, como foi mencionado naquela seção e a lance. Neste caso, o cliente estará ciente de que se a data não for informada no formato esperado, ele terá que tomar alguma providência.

Uma exceção por módulo é mais que suficiente

Não que você não possa especificar suas exceções, mas faça isso com carinho. Se você tem um módulo de controle de clientes, por exemplo, crie uma CustomerModuleException. Fica mais fácil trafegar-la entre as várias camadas ou serviços da sua aplicação. Nada te impedirá de especializá-la internamente, criando subclasses como CustomerNotFoundException, InativeCustomerException ou qualquer outra coisa que possa representar uma condição intrínseca ao seu módulo. Mas dele pra fora, a CustomerModuleException é mais do que suficiente. Dê uma olhada na estrutura de EJBException e de PersistenceException.

Converta exceções verificadas em não-verificadas somente quando necessário

Em um dos exemplos da leitura de arquivos, nas seções anteriores, foi encapsulada uma IOException em uma RuntimeException para que ela pudesse “vazar” do método. Tal coportamento não deve ser deliberado. Considere esse tipo de “conversão” (de checked para unchecked) em situações em que a thread em execução deva ser abortada.

Não use Throwable.printStackTrace()

Algumas IDE’s costumam inserir um ex.printStackTrace() quando você opta por englobar um código em um bloco try-catch. Embora seja útil quando se trata de pequenas codificações para testes, é completamente desencorajado em códigos que vão para ambientes de produção. O método printStackTrace é definido na classe Throwable e herdado por todas as subclasses de Exception. Ele faz uso da API System.err que às vezes compartilha os recursos com o System.out (se estiverem sendo redirecionadas para o mesmo dispositivo/arquivo). Só isso já é uma boa razão para evitá-la. Considere o seguinte: uma vez que elas não fornecem qualquer alternativa thread-safe de acesso, o log gerado em uma situação concorrente seria completamente ilegível.

Use enums para mensagens

Eu gosto de exibir mensagens de erros significativas para o usuário. Exibir as mensagens de exceções é melhor ainda, desde que elas sejam compreensíveis. Utilizar enums nesse contexto pode tornar sua vida mais fácil; já que você está definindo uma mensagem source-level como enum e na ponta (front) você pode usar qualquer mecanismo de internacionalização para traduzí-la para o usuário final.

Voltando ao exemplo do CustomerModuleException, poderíamos especificar algo do tipo:

throw new CustomerModuleException(
    CustomerModuleExceptionMessages.EMAIL_ALREADY_TAKEN);

Ainda neste cenário, considere configurar atributos dinâmicos à sua exceção. Lembre-se, exceptions são classes Java serializáveis comuns e você pode facilmente permitir o comportamento abaixo com o emprego de um Map<String, Serializable>:

throw new CustomerModuleException(
    CustomerModuleExceptionMessages.EMAIL_ALREADY_TAKEN)
        .put("email", customer.getEmail())
        .put("username", customer.getUsername());

 

Use o método de logging adequado

Evite usar log.error() ou seus variantes para tudo. Existem situações em que o comportamento excepcional é esperado ou não vai causar qualquer prejuízo para o usuário final. Nestes casos, você pode considerar usar log.warning() ou até mesmo log.info(). Use log.error() para situações irrecuperáveis.

Considerações

Bem, é isso. Gostaria de convidar você à compartilhar suas considerações sobre exception handling aqui nos comentários e me ajude a melhorar o post.

“No mais” [sic], muito obrigado e até o próximo.

 

3 comentários sobre “Tratamento de exceções em Java

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair /  Alterar )

Foto do Google

Você está comentando utilizando sua conta Google. Sair /  Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair /  Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair /  Alterar )

Conectando a %s