Как управлять исключениями, выбрасываемыми в фильтрах в Spring?
Я хочу использовать общий способ для управления кодами ошибок 5xx, скажем конкретно, в случае, когда db падает по всему моему приложению spring. Я хочу довольно ошибочную ошибку json вместо трассировки стека.
Для контроллеров у меня есть класс @ControllerAdvice
для разных исключений, и это также ловут случай, когда db останавливается в середине запроса. Но это еще не все. У меня также есть пользовательский CorsFilter
расширяющий OncePerRequestFilter
, и там, когда я вызываю doFilter
, я получаю CannotGetJdbcConnectionException
, и он не будет управляться @ControllerAdvice
. Я читал несколько вещей в Интернете, которые только меня смутили.
Итак, у меня много вопросов:
- Нужно ли мне реализовать собственный фильтр? Я нашел
ExceptionTranslationFilter
, но это только обрабатывает AuthenticationException
или AccessDeniedException
.
- Я подумал о том, чтобы реализовать свой собственный
HandlerExceptionResolver
, но это заставило меня сомневаться, у меня нет никакого настраиваемого исключения для управления, должен быть более очевидный путь, чем это. Я также попытался добавить try/catch и вызвать реализацию HandlerExceptionResolver
(должно быть достаточно хорошо, мое исключение ничего особенного), но это ничего не возвращает в ответ, я получаю статус 200 и пустые тела.
Есть ли хороший способ справиться с этим? Благодаря
Ответы
Ответ 1
Так вот что я сделал:
Я читал основы об фильтрах здесь, и я понял, что мне нужно создать настраиваемый фильтр, который будет первым в цепочке фильтров и будет иметь попытку поймать чтобы уловить все исключения во время выполнения, которые могут произойти там. Затем мне нужно вручную создать json и поместить его в ответ.
Итак, вот мой пользовательский фильтр:
public class ExceptionHandlerFilter extends OncePerRequestFilter {
@Override
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
filterChain.doFilter(request, response);
} catch (RuntimeException e) {
// custom error response class used across my project
ErrorResponse errorResponse = new ErrorResponse(e);
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
response.getWriter().write(convertObjectToJson(errorResponse));
}
}
public String convertObjectToJson(Object object) throws JsonProcessingException {
if (object == null) {
return null;
}
ObjectMapper mapper = new ObjectMapper();
return mapper.writeValueAsString(object);
}
}
И затем я добавил его в web.xml перед CorsFilter
. И это работает!
<filter>
<filter-name>exceptionHandlerFilter</filter-name>
<filter-class>xx.xxxxxx.xxxxx.api.controllers.filters.ExceptionHandlerFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>exceptionHandlerFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter>
<filter-name>CorsFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>CorsFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
Ответ 2
Я сам столкнулся с этой проблемой, и я выполнил следующие шаги, чтобы повторно использовать мой ExceptionController
, который аннотируется с @ControllerAdvise
для Exceptions
, брошенным в зарегистрированный фильтр.
Существует много способов обработки исключений, но в моем случае я хотел, чтобы исключение обрабатывалось моим ExceptionController
, потому что я упрямый, а также потому, что я не хочу копировать/вставлять один и тот же код (т.е. У меня есть код обработки/регистрации в ExceptionController
). Я хотел бы вернуть красивый ответ JSON
так же, как и остальные исключения, выброшенные не из фильтра.
{
"status": 400,
"message": "some exception thrown when executing the request"
}
В любом случае мне удалось использовать мой ExceptionHandler
, и мне пришлось сделать немного лишнего, как показано ниже:
Шаги
- У вас есть настраиваемый фильтр, который может или не может генерировать исключение.
- У вас есть контроллер Spring, который обрабатывает исключения, используя
@ControllerAdvise
i.e MyExceptionController
Пример кода
//sample Filter, to be added in web.xml
public MyFilterThatThrowException implements Filter {
//Spring Controller annotated with @ControllerAdvise which has handlers
//for exceptions
private MyExceptionController myExceptionController;
@Override
public void destroy() {
// TODO Auto-generated method stub
}
@Override
public void init(FilterConfig arg0) throws ServletException {
//Manually get an instance of MyExceptionController
ApplicationContext ctx = WebApplicationContextUtils
.getRequiredWebApplicationContext(arg0.getServletContext());
//MyExceptionHanlder is now accessible because I loaded it manually
this.myExceptionController = ctx.getBean(MyExceptionController.class);
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
try {
//code that throws exception
} catch(Exception ex) {
//MyObject is whatever the output of the below method
MyObject errorDTO = myExceptionController.handleMyException(req, ex);
//set the response object
res.setStatus(errorDTO .getStatus());
res.setContentType("application/json");
//pass down the actual obj that exception handler normally send
ObjectMapper mapper = new ObjectMapper();
PrintWriter out = res.getWriter();
out.print(mapper.writeValueAsString(errorDTO ));
out.flush();
return;
}
//proceed normally otherwise
chain.doFilter(request, response);
}
}
И теперь образец Spring Controller, который обрабатывает Exception
в обычных случаях (т.е. исключения, которые обычно не выбрасываются на уровне фильтра, тот, который мы хотим использовать для исключений, созданных в Фильтре)
//sample SpringController
@ControllerAdvice
public class ExceptionController extends ResponseEntityExceptionHandler {
//sample handler
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
@ExceptionHandler(SQLException.class)
public @ResponseBody MyObject handleSQLException(HttpServletRequest request,
Exception ex){
ErrorDTO response = new ErrorDTO (400, "some exception thrown when "
+ "executing the request.");
return response;
}
//other handlers
}
Совместное использование решения с теми, кто хочет использовать ExceptionController
для Exceptions
, брошенного в Фильтр.
Ответ 3
Если вам нужен общий способ, вы можете определить страницу с ошибкой в web.xml:
<error-page>
<exception-type>java.lang.Throwable</exception-type>
<location>/500</location>
</error-page>
И добавьте отображение в Spring MVC:
@Controller
public class ErrorController {
@RequestMapping(value="/500")
public @ResponseBody String handleException(HttpServletRequest req) {
// you can get the exception thrown
Throwable t = (Throwable)req.getAttribute("javax.servlet.error.exception");
// customize response to what you want
return "Internal server error.";
}
}
Ответ 4
Итак, вот что я сделал на основе объединения приведенных выше ответов... У нас уже был GlobalExceptionHandler
аннотированный @ControllerAdvice
и я также хотел найти способ повторно использовать этот код для обработки исключений, которые поступают из фильтров.
Самым простым решением, которое я смог найти, было оставить обработчик исключений в одиночку и реализовать контроллер ошибок следующим образом:
@Controller
public class ErrorControllerImpl implements ErrorController {
@RequestMapping("/error")
public void handleError(HttpServletRequest request) throws Throwable {
if (request.getAttribute("javax.servlet.error.exception") != null) {
throw (Throwable) request.getAttribute("javax.servlet.error.exception");
}
}
}
Таким образом, любые ошибки, вызванные исключениями, сначала проходят через ErrorController
и перенаправляются в обработчик исключений, перебрасывая их из контекста @Controller
, тогда как любые другие ошибки (не вызванные напрямую исключением) проходят через ErrorController
без изменений,
Какие-либо причины, почему это на самом деле плохая идея?
Ответ 5
Это мое решение, переопределяя стандартный Spring обработчик загрузки/ошибки
package com.mypackage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.web.ErrorAttributes;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
/**
* This controller is vital in order to handle exceptions thrown in Filters.
*/
@RestController
@RequestMapping("/error")
public class ErrorController implements org.springframework.boot.autoconfigure.web.ErrorController {
private final static Logger LOGGER = LoggerFactory.getLogger(ErrorController.class);
private final ErrorAttributes errorAttributes;
@Autowired
public ErrorController(ErrorAttributes errorAttributes) {
Assert.notNull(errorAttributes, "ErrorAttributes must not be null");
this.errorAttributes = errorAttributes;
}
@Override
public String getErrorPath() {
return "/error";
}
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest aRequest, HttpServletResponse response) {
RequestAttributes requestAttributes = new ServletRequestAttributes(aRequest);
Map<String, Object> result = this.errorAttributes.getErrorAttributes(requestAttributes, false);
Throwable error = this.errorAttributes.getError(requestAttributes);
ResponseStatus annotation = AnnotationUtils.getAnnotation(error.getClass(), ResponseStatus.class);
HttpStatus statusCode = annotation != null ? annotation.value() : HttpStatus.INTERNAL_SERVER_ERROR;
result.put("status", statusCode.value());
result.put("error", statusCode.getReasonPhrase());
LOGGER.error(result.toString());
return new ResponseEntity<>(result, statusCode) ;
}
}
Ответ 6
Если вы хотите протестировать состояние приложения, и в случае возникновения проблемы возвратите ошибку HTTP, я бы предложил фильтр. Фильтр ниже обрабатывает все HTTP-запросы. Самое короткое решение в Spring Загрузка с фильтром javax.
В реализации могут быть различные условия. В моем случае приложение applicationManager проверяет, готово ли приложение.
import ...ApplicationManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class SystemIsReadyFilter implements Filter {
@Autowired
private ApplicationManager applicationManager;
@Override
public void init(FilterConfig filterConfig) throws ServletException {}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if (!applicationManager.isApplicationReady()) {
((HttpServletResponse) response).sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "The service is booting.");
} else {
chain.doFilter(request, response);
}
}
@Override
public void destroy() {}
}
Ответ 7
Я хотел предложить решение, основанное на ответе @kopelitsa. Основными отличиями являются:
- Повторное использование обработки исключений контроллера с помощью
HandlerExceptionResolver
.
- Использование конфигурации Java поверх конфигурации XML
Во-первых, вам нужно убедиться, что у вас есть класс, который обрабатывает исключения, возникающие в обычном RestController/Controller (класс, аннотированный с помощью @RestControllerAdvice
или @ControllerAdvice
, и метод (ы), аннотированный с помощью @ExceptionHandler
). Это обрабатывает ваши исключения, возникающие в контроллере. Вот пример использования RestControllerAdvice:
@RestControllerAdvice
public class ExceptionTranslator {
@ExceptionHandler(RuntimeException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorDTO processRuntimeException(RuntimeException e) {
return createErrorDTO(HttpStatus.INTERNAL_SERVER_ERROR, "An internal server error occurred.", e);
}
private ErrorDTO createErrorDTO(HttpStatus status, String message, Exception e) {
(...)
}
}
Чтобы повторно использовать это поведение в цепочке фильтров Spring Security, необходимо определить фильтр и подключить его к конфигурации безопасности. Фильтр должен перенаправить исключение на определенную выше обработку исключений. Вот пример:
@Component
public class FilterChainExceptionHandler extends OncePerRequestFilter {
private final Logger log = LoggerFactory.getLogger(getClass());
@Autowired
@Qualifier("handlerExceptionResolver")
private HandlerExceptionResolver resolver;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
filterChain.doFilter(request, response);
} catch (Exception e) {
log.error("Spring Security Filter Chain Exception:", e);
resolver.resolveException(request, response, null, e);
}
}
}
Затем необходимо добавить созданный фильтр в конфигурацию безопасности. Вам нужно подключить его к цепочке очень рано, потому что все предыдущие исключения фильтра не будут перехвачены. В моем случае было разумно добавить его до LogoutFilter
. См. цепочку фильтров по умолчанию и ее порядок в официальных документах. Вот пример:
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private FilterChainExceptionHandler filterChainExceptionHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterBefore(filterChainExceptionHandler, LogoutFilter.class)
(...)
}
}
Ответ 8
Просто чтобы дополнить другие прекрасные ответы, так как я недавно хотел иметь один компонент обработки ошибок/исключений в простом приложении SpringBoot, содержащем фильтры, которые могут генерировать исключения, с другими исключениями, потенциально генерируемыми из методов контроллера.
К счастью, кажется, что ничто не мешает вам совмещать рекомендации вашего контроллера с переопределением обработчика ошибок Spring по умолчанию для обеспечения согласованной полезной нагрузки ответов, позволяя вам делиться логикой, проверять исключения из фильтров, перехватывать определенные исключения, генерируемые сервисом, и т.д.
Например
@ControllerAdvice
@RestController
public class GlobalErrorHandler implements ErrorController {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(ValidationException.class)
public Error handleValidationException(
final ValidationException validationException) {
return new Error("400", "Incorrect params"); // whatever
}
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(Exception.class)
public Error handleUnknownException(final Exception exception) {
return new Error("500", "Unexpected error processing request");
}
@RequestMapping("/error")
public ResponseEntity handleError(final HttpServletRequest request,
final HttpServletResponse response) {
Object exception = request.getAttribute("javax.servlet.error.exception");
// TODO: Logic to inspect exception thrown from Filters...
return ResponseEntity.badRequest().body(new Error(/* whatever */));
}
@Override
public String getErrorPath() {
return "/error";
}
}
Ответ 9
После прочтения различных методов, предложенных в ответах выше, я решил обработать исключения аутентификации с помощью специального фильтра. Мне удалось обработать статус ответа и коды, используя класс ответа об ошибке, используя следующий метод.
Я создал собственный фильтр и изменил свою конфигурацию безопасности с помощью метода addFilterAfter и добавил после класса CorsFilter.
@Component
public class AuthFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
//Cast the servlet request and response to HttpServletRequest and HttpServletResponse
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
// Grab the exception from the request attribute
Exception exception = (Exception) request.getAttribute("javax.servlet.error.exception");
//Set response content type to application/json
httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
//check if exception is not null and determine the instance of the exception to further manipulate the status codes and messages of your exception
if(exception!=null && exception instanceof AuthorizationParameterNotFoundException){
ErrorResponse errorResponse = new ErrorResponse(exception.getMessage(),"Authetication Failed!");
httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
PrintWriter writer = httpServletResponse.getWriter();
writer.write(convertObjectToJson(errorResponse));
writer.flush();
return;
}
// If exception instance cannot be determined, then throw a nice exception and desired response code.
else if(exception!=null){
ErrorResponse errorResponse = new ErrorResponse(exception.getMessage(),"Authetication Failed!");
PrintWriter writer = httpServletResponse.getWriter();
writer.write(convertObjectToJson(errorResponse));
writer.flush();
return;
}
else {
// proceed with the initial request if no exception is thrown.
chain.doFilter(httpServletRequest,httpServletResponse);
}
}
public String convertObjectToJson(Object object) throws JsonProcessingException {
if (object == null) {
return null;
}
ObjectMapper mapper = new ObjectMapper();
return mapper.writeValueAsString(object);
}
}
SecurityConfig class
@Configuration
public class JwtSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
AuthFilter authenticationFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterAfter(authenticationFilter, CorsFilter.class).csrf().disable()
.cors(); //........
return http;
}
}
ErrorResponse class
public class ErrorResponse {
private final String message;
private final String description;
public ErrorResponse(String description, String message) {
this.message = message;
this.description = description;
}
public String getMessage() {
return message;
}
public String getDescription() {
return description;
}}
Ответ 10
Это странно, потому что @ControllerAdvice должен работать, вы поймаете правильное исключение?
@ControllerAdvice
public class GlobalDefaultExceptionHandler {
@ResponseBody
@ExceptionHandler(value = DataAccessException.class)
public String defaultErrorHandler(HttpServletResponse response, DataAccessException e) throws Exception {
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
//Json return
}
}
Также попробуйте поймать это исключение в CorsFilter и отправить 500 ошибок, что-то вроде этого
@ExceptionHandler(DataAccessException.class)
@ResponseBody
public String handleDataException(DataAccessException ex, HttpServletResponse response) {
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
//Json return
}