O Spring MVC fornece diversas abordagens complementares para manipulação de exceções, por isso muitos usuários podem ficar confusos ou não se sentirem confortáveis com todas essas opções.
Neste artigo, traduzido do blog oficial do spring.io, o autor mostra as várias opções disponíveis. O objetivo do autor é, sempre que possível, não lidar explicitamente com exceções em métodos do Controller, pois elas são preocupações que são melhor tratadas em um código dedicado.
Existem três tipos de opções: por exceção, por controller ou globalmente.
Uma aplicação de demonstração que mostra os tópicos discutidos nesse artigo podem ser encontrados em
http://github.com/paulc4/mvc-exceptions.
Veja a seção Aplicação exemplo e Spring Boot abaixo para mais detalhes
Usando códigos de status HTTP
Normalmente, qualquer exceção não tratada que é disparada durante o processamento de uma requisição, faz com que o servidor retorne um erro HTTP 500. Porém, qualquer exceção que você cria pode ser anotada com @ResponseStatus
(que suporta todos os código de status definidos pela especificação HTTP). Quando uma exceção anotada é disparada de um método do controller, e não é tratada em algum lugar, a resposta HTTP apropriada será retornada com o código de status especificado.
Por exemplo, abaixo segue uma exceção para um pedido inexistente.
@ResponseStatus(value=HttpStatus.NOT_FOUND, reason="No such Order") // 404
public class OrderNotFoundException extends RuntimeException {
// ...
}
E abaixo segue um método do controller que usa essa exceção:
@RequestMapping(value="/orders/{id}", method=GET)
public String showOrder(@PathVariable("id") long id, Model model) {
Order order = orderRepository.findOrderById(id);
if (order == null) throw new OrderNotFoundException(id);
model.addAttribute(order);
return "orderDetail";
}
Uma resposta HTTP 404 familiar será retornada se a URL mapeada por esse método incluir um id de um pedido inexistente.
Manipulação de exceções baseada no Controller
Usando @ExceptionHandler
Você pode adicionar métodos (@ExceptionHandler
) extras para qualquer controller para poder manipular exceções disparadas por métodos de mapeamento (@RequestMapping
) do mesmo controller. Tais métodos podem:
- Tratar exceções sem a anotação
@ResponseStatus
(tipicamente exceções pré-definidas que você não criou) - Redirecionar o usuário para uma página de erro própria
- Criar uma resposta ao erro totalmente personalizada
O controller a seguir demonstra essas três opções:
@Controller
public class ExceptionHandlingController {
// @RequestHandler methods
...
// Exception handling methods
// Convert a predefined exception to an HTTP Status code
@ResponseStatus(value=HttpStatus.CONFLICT, reason="Data integrity violation") // 409
@ExceptionHandler(DataIntegrityViolationException.class)
public void conflict() {
// Nothing to do
}
// Specify the name of a specific view that will be used to display the error:
@ExceptionHandler({SQLException.class,DataAccessException.class})
public String databaseError() {
// Nothing to do. Returns the logical view name of an error page, passed to
// the view-resolver(s) in usual way.
// Note that the exception is _not_ available to this view (it is not added to
// the model) but see "Extending ExceptionHandlerExceptionResolver" below.
return "databaseError";
}
// Total control - setup a model and return the view name yourself. Or consider
// subclassing ExceptionHandlerExceptionResolver (see below).
@ExceptionHandler(Exception.class)
public ModelAndView handleError(HttpServletRequest req, Exception exception) {
logger.error("Request: " + req.getRequestURL() + " raised " + exception);
ModelAndView mav = new ModelAndView();
mav.addObject("exception", exception);
mav.addObject("url", req.getRequestURL());
mav.setViewName("error");
return mav;
}
}
Em qualquer um desses métodos você pode optar por executar algum processamento adicional – o exemplo mais comum é o registro (log) de exceções.
Manipuladores de métodos possuem assinaturas flexíveis, de forma que você pode usar como parâmetro objetos óbvios relacionados como HttpServletRequest
, HttpServletResponse
, HttpSession
e/ou Principle
. Observação Importante: Model
não pode ser um parâmetro de um método @ExceptionHandler
. Ao invés disso, configure um modelo dentro do método usando ModelAndView
como mostrado no método handleError()
acima.
Exceções e Views
Tenha cuidado quando adiciona exceções ao modelo. Usuários não querem visualizar páginas web com detalhes da exceção Java e stack-traces. Porém, pode ser útil colocar os detalhes da exceção em uma página source como um comentário, para ajudar o pessoal do suporte. Se estiver usando JSP, pode fazer algo como isso para que a saida seja composta da exceção e do stack-trace correspondente (usar um <div> oculto é outra opção).
<h1>Error Page</h1>
<p>Application has encountered an error. Please contact support on ...</p>
<!--
Failed URL: ${url}
Exception: ${exception.message}
<c:forEach items="${exception.stackTrace}" var="ste"> ${ste}
</c:forEach>
-->
O resultado se parecerá com isso (veja também support.jsp
na aplicação de demonstração):
Manipulação global de exceções
Usando classes anotadas com @ControllerAdvice
Um aviso do controller permite que você use exatamente as mesmas técnicas de manipulação de esceções mas aplica-las por toda a aplicação, não apenas à controllers individuais. Você pode pensar nelas como um interceptador direcionado por anotação.
Qualquer classe anotada com @ControllerAdvice
torna-se um avido de controller e três tipos de métodos são suportados:
- Método de manipulação de anotações anotados com
@ExceptionHandler
. - Método de melhoramento do Modelos (para adicionar dados adicionais ao modelo), anotados com
@ModelAttribute
. Note que esses atributos não estão disponíveis aos views de manipulação de exceção. - Método de inicialização de capa (binder – usados para configuração da manipulação de formulários) anotados com
@InitBinder
.
Iremos da uma olhada na manipulação de exceções = dê uma olhada no manual para mais informações sobre os métodos anotados com
@ControllerAdvice
methods.
Qualquer um dos manipuladores de exceção que você viu acima podem ser definidor como uma classe de aviso do controller – mas agora eles serão aplicados a exceções disparadas de qualquer controller. Abaixo um exemplo simples:
@ControllerAdvice
class GlobalControllerExceptionHandler {
@ResponseStatus(HttpStatus.CONFLICT) // 409
@ExceptionHandler(DataIntegrityViolationException.class)
public void handleConflict() {
// Nothing to do
}
}
Se quiser definir um manipulador padrão para qualquer exceção, existe uma pequena dificuldade a ser transposta. VocÊ precisa garantir que as exceções anotadas sejam tratadas pelo framework. O código irá se parecer com isso:
@ControllerAdvice
class GlobalDefaultExceptionHandler {
public static final String DEFAULT_ERROR_VIEW = "error";
@ExceptionHandler(value = Exception.class)
public ModelAndView defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception {
// If the exception is annotated with @ResponseStatus rethrow it and let
// the framework handle it - like the OrderNotFoundException example
// at the start of this post.
// AnnotationUtils is a Spring Framework utility class.
if (AnnotationUtils.findAnnotation(e.getClass(), ResponseStatus.class) != null)
throw e;
// Otherwise setup and send the user to a default error-view.
ModelAndView mav = new ModelAndView();
mav.addObject("exception", e);
mav.addObject("url", req.getRequestURL());
mav.setViewName(DEFAULT_ERROR_VIEW);
return mav;
}
}
Indo mais fundo
HandlerExceptionResolver
Qualquer bean do Spring declarado no contexto da aplicação (DispatcherServlet
‘s) que implemente
HandlerExceptionResolver
será usado para interceptar e processar qualquer exceção disparada no sistema MVC e não tratada por um controller. A interface se parece com o seguinte:
public interface HandlerExceptionResolver {
ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex);
}
O handler
se refere ao controller que disparou a exceção (lembre que instâncias de
@Controller
são apenas um dos tipos de handler suportados pelo Spring MVC.
Por exemplo: HttpInvokerExporter
e WebFlow também são tipos de handler).
Por trás das cortinas, o MVC cria três resolvers por padrão. São eles que implementam os comportamentos discutivos acima:
ExceptionHandlerExceptionResolver
coincide exceções não pegas com o adequado método anotado com@ExceptionHandler
tanto no handler (controller) quanto em avidos de controller.ResponseStatusExceptionResolver
procura por exceções não tratadas anotadas com@ResponseStatus
(como descrito na primeira seção)DefaultHandlerExceptionResolver
converte exceções padrão do Spring em códigos de status do HTTP (não foi mencionado acima por ser um mecanismo interno do Spring MVC).
Eles são encadeados e processados na ordem listada (internamente, o SPring cria um bean dedicado – o HandlerExceptionResolverComposite para fazer isso).
Observe que a assinatura de método de resolveException
não inclui Model
. Isso se deve ao fato de que método anotados com @ExceptionHandler
não podem ser injetado com o modelo.
Você pode, se quiser, implementat seu próprio HandlerExceptionResolver
para configurar seu próprio sistema de manipulação de exceções. Os manipuladores tipicamente implementam a interface Ordered
do Spring de forma que você pode definir a ordem que eles são executados.
SimpleMappingExceptionResolver
O Spring fornece a bastante tempo uma implementação simplesmas conveniente de HandlerExceptionResolver
que pode ser que já esteja sendo usada em sua aplicação – o SimpleMappingExceptionResolver
.
Ele fornece as seguintes opções:
- Mapeia exceções de nomes de classes para nomes de views – apenas especifique o nome da classe, nenhum pacote necessário.
- Especifica um página de erro padrão para qualquer exceção não tratada em algum lugar
- Registra uma mensagem de log (não ativada por padrão).
- Configura o nome do atributo
exception
para adiciona-lo ao Modelo de forma que possa ser usada em um View (como em um JSP). Por padrão, esse atributo é chamadoexception
. Configure paranull
para desativar. Lembre que views retornados por método@ExceptionHandler
não tem acesso à exceções mas views definidor porSimpleMappingExceptionResolver
tem.
Abaixo uma configuração típica usando XML:
<bean id="simpleMappingExceptionResolver"
class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<property name="exceptionMappings">
<map>
<entry key="DatabaseException" value="databaseError"/>
<entry key="InvalidCreditCardException" value="creditCardError"/>
</map>
</property>
<!-- See note below on how this interacts with Spring Boot -->
<property name="defaultErrorView" value="error"/>
<property name="exceptionAttribute" value="ex"/>
<!-- Name of logger to use to log exceptions. Unset by default, so logging disabled -->
<property name="warnLogCategory" value="example.MvcLogger"/>
</bean>
Ou usando código Java:
@Configuration
@EnableWebMvc // Optionally setup Spring MVC defaults if you aren't doing so elsewhere
public class MvcConfiguration extends WebMvcConfigurerAdapter {
@Bean(name="simpleMappingExceptionResolver")
public SimpleMappingExceptionResolver createSimpleMappingExceptionResolver() {
SimpleMappingExceptionResolver r =
new SimpleMappingExceptionResolver();
Properties mappings = new Properties();
mappings.setProperty("DatabaseException", "databaseError");
mappings.setProperty("InvalidCreditCardException", "creditCardError");
r.setExceptionMappings(mappings); // None by default
r.setDefaultErrorView("error"); // No default
r.setExceptionAttribute("ex"); // Default is "exception"
r.setWarnLogCategory("example.MvcLogger"); // No default
return r;
}
...
}
A propriedade defaultErrorView é especialmente útil pois garante que qualquer exceção não tratada irá gerar uma página de erro adequada (O padrão para a maioria dos servidores de aplicação é mostrar um stack-trace Java – algo que os usuários nunca devem visualizar).
Estendendo SimpleMappingExceptionResolver
É bastante comum estender SimpleMappingExceptionResolver
por diversas razões:
- Usar o construtor para configurar propriedades diretamente – por exemplom, para ativar o registro (log) de exceções e definir o logger a ser usado;
- Sobrecarregar as mensagens de log padrão através do sobrecarregamente de
buildLogMessage
. A implementação padrão sempre retorna esse texto: Handler execution resulted in exception - Para fazer com que informações adicionais sejam disponibilizadas para o view pelo sobrecarregamento de
doResolveException
Por exemplo:
public class MyMappingExceptionResolver extends SimpleMappingExceptionResolver {
public MyMappingExceptionResolver() {
// Enable logging by providing the name of the logger to use
setWarnLogCategory(MyMappingExceptionResolver.class.getName());
}
@Override
public String buildLogMessage(Exception e, HttpServletRequest req) {
return "MVC exception: " + e.getLocalizedMessage();
}
@Override
protected ModelAndView doResolveException(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception exception) {
// Call super method to get the ModelAndView
ModelAndView mav = super.doResolveException(request, response, handler, exception);
// Make the full URL available to the view - note ModelAndView uses addObject()
// but Model uses addAttribute(). They work the same.
mav.addObject("url", request.getRequestURL());
return mav;
}
}
Esse código está na aplicação exemplo:
ExampleSimpleMappingExceptionResolver
Estendendo ExceptionHandlerExceptionResolver
É possível também estender ExceptionHandlerExceptionResolver
e sobrecarregar seus métodos doResolveHandlerMethodException
da mesma forma. Ele possui quase a mesma assinatura (apenas usa HandlerMethod
ao invés de Handler
).
Para garantir que seja usado, também ajuste a ordem herdada das propriedades (por exemplo, no construtor de sua nova classe) para um valor menor que MAX_INT
de forma que seja executada antes da instância padrão de ExceptionHandlerExceptionResolver (É mais fácil criar seu próprio manipulador do que tentar modificar/substituir um criado pelo Spring). Veja o ExampleExceptionHandlerExceptionResolver
na aplicação exemplo para mais detalhes.
Erros e REST
Requisições GET RESTful também podem gerar exceções e nós já vimos como podemos retornar códigos de respostas de erro HTTP padrão. Porém, e se você quiser retornar informações sobre o erro? Isso é bem fácil de fazer. Em primeiro lugar, defina uma classe de erro:
public class ErrorInfo {
public final String url;
public final String ex;
public ErrorInfo(String url, Exception ex) {
this.url = url;
this.ex = ex.getLocalizedMessage();
}
}
Agora, podemos retornar uma instância dela de um manipulador como @ResponseBody
dessa forma:
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MyBadDataException.class)
@ResponseBody ErrorInfo handleBadRequest(HttpServletRequest req, Exception ex) {
return new ErrorInfo(req.getRequestURL(), ex);
}
O que usar e Quando usar?
Como esperado, o Spring oferece a você muitas opções de escolha; quais você deve usar então? Abaixo segue algumas regras.
- Para exceçõesque você criou, considere adicionar
@ResponseStatus
à elas. - Para todas as demais exceções, implemente um método
@ExceptionHandler
em uma classe
@ControllerAdvice
ou use uma instância deSimpleMappingExceptionResolver
.
Você pode até já terSimpleMappingExceptionResolver
configurado para sua aplicação, o que torna mais fácil adicionar classes de exceção do que implementar um@ControllerAdvice
. - Para exceções específicas de um controller, adicione métodos
@ExceptionHandler
à ele. - Atenção: Seja cuidados ao misturar demais todas essas opções em uma mesma aplicação. Se a mesma exceção puder ser tratada de mais de uma forma, você pode não ter o comportamento esperado. Métodos
@ExceptionHandler
no controller são sempre selecionadas antes de qualquer instância de@ControllerAdvice
. A ordem de processamento de avisos de controller é indefinida.
Aplicação exemplo e Spring Boot
Uma aplicação exemplo pode ser encontrada no github. Ela usa Spring Boot e Thymeleaf para construir uma aplicação simples. Algumas explicações são necessárias:
Sobre a aplicação de demonstração
O exemplo roda em dois modos: controller ou global. Isso é ajustada por uma propriedade booleana na classe Main que ativa o correspondente perfil.
- Quando Main.global é configurado para false, o modo controller é ativado. Um ExceptionHandlingController
é criado que trata todas as requisições e exceções separadamente. - Quando Main.global é configurada para true, o modo global é ativado. Um ControllerWithoutExceptionHandlers é criado apenas para manipular requisições. Exceções são tratadas globalmente por uma instância de GlobalControllerExceptionHandler que é um aviso de controller.
Em acréscimo, um SimpleMappingExceptionResolver
pode ser definido opcionalmente. A classe Main
possui uma segunda propriedade chamda smerConfig
que pode ter um dos valores a seguir:
NONE
: Nenhum resolver definido. Exceções não tratadas são processadas pelo container. Como estamos usando O Tomcat, páginas de erro familiares dele como stack-yraces Java serão produzidas.XML
: UmSimpleMappingExceptionResolver
é configurado usando – veja mvc-configuration.xml. Um bean Springxml-config
faz isso acontecer. Se você prefere XML, essa é a solução para você.JAVA
: Também configura umSimpleMappingExceptionResolver
pela ativação de um bean diferente,java-config
, usando código Java. Veja ExceptionConfiguration.
Uma descrição dos arquivos mais importantes do projeto está no arquivo README.md. A única páginas web é index.html que:
- Contém diversos links, sendo que todos deliberadamente disparam exceções.
- Na parte de baixo da página estão links para páginas de informações sobre o Spring Boot para aqueles interessados.
Graças ao Spring Boot, você pode rodar essa aplicação como uma aplicação Java ou um WAR em seu container favorito. A URL da página inicial é http://localhost:8080 quando rodar como uma aplicação ou http://localhost:8080/mvc-exceptions quando rodar em um container.
Aviso
- Esse projeto foi compilado usando a versão 0.50 do Spring Boot.
- As APIs podem ter sido alteradas desde então e este projeto pode não ser compilado.
- Verifique http://spring.io/spring-boot por um snapshot, milestone
e outras versões. - Atualize o
pom.xml
e/ou o código Java se necessário.
Traduzido de spring.io/blog