Ich möchte die generische Methode verwenden, um 5xx-Fehlercodes zu verwalten. Nehmen wir an, dass die Datenbank in meiner gesamten Frühlingsanwendung nicht verfügbar ist. Ich möchte einen hübschen Fehler json anstelle eines Stack-Trace.
Für die Controller habe ich eine @ControllerAdvice
-Klasse für die verschiedenen Ausnahmen, und dies ist auch der Fall, dass die db mitten in der Anforderung stoppt. Aber das ist nicht alles. Ich habe auch zufällig eine benutzerdefinierte CorsFilter
, die OncePerRequestFilter
erweitert, und wenn ich doFilter
anrufe, bekomme ich die CannotGetJdbcConnectionException
und wird nicht vom @ControllerAdvice
verwaltet. Ich habe im Internet einige Dinge gelesen, die mich nur verwirrter gemacht haben.
Ich habe also viele Fragen:
ExceptionTranslationFilter
gefunden, aber dies behandelt nur AuthenticationException
oder AccessDeniedException
. HandlerExceptionResolver
zu implementieren, was mich jedoch bezweifelte, dass ich keine benutzerdefinierten Ausnahmen habe, die zu verwalten sind. Es muss einen naheliegenderen Weg geben. Ich habe auch versucht, ein try/catch hinzuzufügen und eine Implementierung der HandlerExceptionResolver
aufzurufen (sollte gut genug sein, meine Ausnahme ist nichts Besonderes), aber dies gibt nichts in der Antwort zurück, ich bekomme einen Status 200 und einen leeren Körper. Gibt es eine gute Möglichkeit, damit umzugehen? Vielen Dank
Also das habe ich getan:
Ich habe die Grundlagen über Filter hier gelesen, und ich habe herausgefunden, dass ich einen benutzerdefinierten Filter erstellen muss, der zuerst in der Filterkette ist und versuchen wird, alle Laufzeitausnahmen abzufangen, die dort auftreten könnten. Dann muss ich den Json manuell erstellen und in die Antwort einfügen.
Hier ist mein benutzerdefinierter Filter:
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);
}
}
Und dann habe ich es in der web.xml vor der CorsFilter
eingefügt. Und es funktioniert!
<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>
Ich bin selbst auf dieses Problem gestoßen und habe die folgenden Schritte ausgeführt, um meine ExceptionController
, die mit @ControllerAdvise
für Exceptions
versehen ist, für einen registrierten Filter wiederzuverwenden.
Es gibt natürlich viele Möglichkeiten, mit Ausnahmen umzugehen, aber in meinem Fall wollte ich, dass die Ausnahme von meiner ExceptionController
behandelt wird, weil ich stur bin und auch nicht, weil ich denselben Code kopieren/einfügen möchte (dh ich habe etwas Verarbeitung/Protokollierungscode in ExceptionController
). Ich möchte die schöne JSON
-Antwort genauso zurückgeben wie die übrigen Ausnahmen, die nicht von einem Filter geworfen wurden.
{
"status": 400,
"message": "some exception thrown when executing the request"
}
Jedenfalls habe ich es geschafft, meine ExceptionHandler
zu nutzen, und ich musste etwas mehr tun, wie in den folgenden Schritten gezeigt:
Schritte
@ControllerAdvise
, d. H. MyExceptionController, behandeltBeispielcode
//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);
}
}
Und nun der Beispiel-Spring Controller, der Exception
in normalen Fällen behandelt (d. H. Ausnahmen, die normalerweise nicht in der Filterebene geworfen werden, die wir für Ausnahmen verwenden möchten, die in einem Filter geworfen werden).
//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
}
Teilen der Lösung mit denen, die ExceptionController
für Exceptions
verwenden möchten, die in einem Filter geworfen werden.
Wenn Sie einen generischen Weg wünschen, können Sie eine Fehlerseite in web.xml definieren:
<error-page>
<exception-type>Java.lang.Throwable</exception-type>
<location>/500</location>
</error-page>
Und Mapping in Spring MVC hinzufügen:
@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.";
}
}
Dies ist meine Lösung, indem der standardmäßige Spring Boot/Error-Handler überschrieben wird
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) ;
}
}
Ich habe also eine Kombination der obigen Antworten erstellt ... Wir hatten bereits eine GlobalExceptionHandler
, die mit @ControllerAdvice
kommentiert wurde, und ich wollte auch einen Weg finden, diesen Code wiederzuverwenden, um Ausnahmen zu behandeln, die von Filtern kommen.
Die einfachste Lösung, die ich finden konnte, bestand darin, den Ausnahmebehandler in Ruhe zu lassen und einen Fehlercontroller wie folgt zu implementieren:
@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");
}
}
}
Daher gehen Fehler, die durch Ausnahmen verursacht werden, zuerst durch die Variable ErrorController
und werden an den Ausnahmebehandler weitergeleitet, indem sie aus einem @Controller
-Kontext zurückgesetzt werden, wohingegen andere Fehler (die nicht direkt durch eine Ausnahme verursacht werden) die Änderung ErrorController
ohne Änderung durchlaufen.
Gründe, warum das eigentlich eine schlechte Idee ist?
Wenn Sie einen Anwendungsstatus testen möchten und im Falle eines Problems einen HTTP-Fehler zurückgeben möchten, würde ich einen Filter vorschlagen. Der Filter unten behandelt alle HTTP-Anforderungen. Die kürzeste Lösung in Spring Boot mit einem Javax-Filter.
Bei der Implementierung können verschiedene Bedingungen vorliegen. In meinem Fall testet der applicationManager, ob die Anwendung bereit ist.
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() {}
}
Ich wollte eine Lösung basierend auf der Antwort von @kopelitsa anbieten. Die Hauptunterschiede sind:
HandlerExceptionResolver
.Zuerst müssen Sie sicherstellen, dass Sie eine Klasse haben, die Ausnahmen behandelt, die in einem regulären RestController/Controller auftreten (eine Klasse, die mit @RestControllerAdvice
oder @ControllerAdvice
kommentiert ist, und eine oder mehrere Methoden, die mit @ExceptionHandler
kommentiert sind). Dies behandelt Ihre Ausnahmen, die in einem Controller auftreten. Hier ist ein Beispiel mit dem 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) {
(...)
}
}
Um dieses Verhalten in der Spring Security-Filterkette wiederzuverwenden, müssen Sie einen Filter definieren und in Ihre Sicherheitskonfiguration einbinden. Der Filter muss die Ausnahme an die oben definierte Ausnahmebehandlung umleiten. Hier ist ein Beispiel:
@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 (RuntimeException e) {
log.error("Spring Security Filter Chain RuntimeException:", e);
resolver.resolveException(request, response, null, e);
}
}
}
Der erstellte Filter muss dann zur SecurityConfiguration hinzugefügt werden. Sie müssen es sehr früh in die Kette einhängen, da alle Ausnahmen des vorhergehenden Filters nicht erfasst werden. In meinem Fall war es sinnvoll, es vor der LogoutFilter
einzufügen. Siehe die Standardfilterkette und ihre Reihenfolge in den offiziellen Dokumenten . Hier ist ein Beispiel:
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private FilterChainExceptionHandler filterChainExceptionHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterBefore(filterChainExceptionHandler, LogoutFilter.class)
(...)
}
}
Es ist seltsam, weil @ControllerAdvice funktionieren sollte. Fangen Sie die richtige Ausnahme ein?
@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
}
}
Versuchen Sie auch, diese Ausnahme in CorsFilter einzufangen, und senden Sie einen Fehler in etwa 500
@ExceptionHandler(DataAccessException.class)
@ResponseBody
public String handleDataException(DataAccessException ex, HttpServletResponse response) {
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
//Json return
}
Nachdem ich verschiedene Methoden durchgelesen hatte, die in den obigen Antworten vorgeschlagen wurden, entschied ich mich, die Authentifizierungsausnahmen mithilfe eines benutzerdefinierten Filters zu behandeln. Ich konnte den Antwortstatus und die Codes mithilfe einer Fehlerantwortklasse mit der folgenden Methode verarbeiten.
Ich habe einen benutzerdefinierten Filter erstellt und meine Sicherheitskonfiguration mithilfe der addFilterAfter-Methode geändert und nach der CorsFilter-Klasse hinzugefügt.
@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-Klasse
@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-Klasse
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;
}}
Um die anderen feinen Antworten zu ergänzen, wollte ich in einer einfachen SpringBoot-App, die Filter enthält, die möglicherweise Ausnahmen auslösen können, vor kurzem eine single error/exception-Komponente, mit anderen Ausnahmen, die möglicherweise von Controller-Methoden ausgelöst wurden.
Glücklicherweise scheint es nichts zu verhindern, dass Sie Ihre Steuerungsempfehlungen mit einer Überschreibung des standardmäßigen Fehlerhandlers von Spring kombinieren, um konsistente Antwortnutzlasten bereitzustellen, die Logik gemeinsam zu nutzen, Ausnahmen von Filtern zu überprüfen, bestimmte von Service ausgelöste Ausnahmen abzufangen usw.
Z.B.
@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";
}
}