Spring MVC REST Handing Bad Url (404) by returning

2019-03-09 02:56发布

问题:

I am developing a REST service using SpringMVC, where I have @RequestMapping at class and method level.

This application is currently configured to return error-page jsp configured in web.xml.

<error-page>
    <error-code>404</error-code>
    <location>/resourceNotFound</location>
</error-page>

I however want to return custom JSON instead of this error page.

I am able to handle exception and return json for other exceptions, by writing this in controller, but not sure how and where to write the logic to return JSON when the url does not exist at all.

    @ExceptionHandler(TypeMismatchException.class)
        @ResponseStatus(value=HttpStatus.NOT_FOUND)
        @ResponseBody
        public ResponseEntity<String> handleTypeMismatchException(HttpServletRequest req, TypeMismatchException ex) {

            HttpHeaders headers = new HttpHeaders();
            headers.add("Content-Type", "application/json; charset=utf-8");
            Locale locale = LocaleContextHolder.getLocale();
            String errorMessage = messageSource.getMessage("error.patient.bad.request", null, locale);

            errorMessage += ex.getValue();
            String errorURL = req.getRequestURL().toString();

            ErrorInfo errorInfo = new ErrorInfo(errorURL, errorMessage);
            return new ResponseEntity<String>(errorInfo.toJson(), headers, HttpStatus.BAD_REQUEST);

        }

I tried @ControllerAdvice, it works for other exception scenarios, but not when mapping is not avaialble,

@ControllerAdvice
public class RestExceptionProcessor {

    @Autowired
    private MessageSource messageSource;

    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    @ResponseStatus(value=HttpStatus.NOT_FOUND)
    @ResponseBody
    public ResponseEntity<String> requestMethodNotSupported(HttpServletRequest req, HttpRequestMethodNotSupportedException ex) {
        Locale locale = LocaleContextHolder.getLocale();
        String errorMessage = messageSource.getMessage("error.patient.bad.id", null, locale);

        String errorURL = req.getRequestURL().toString();

        ErrorInfo errorInfo = new ErrorInfo(errorURL, errorMessage);
        return new ResponseEntity<String>(errorInfo.toJson(), HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler(NoSuchRequestHandlingMethodException.class)
    @ResponseStatus(value=HttpStatus.NOT_FOUND)
    @ResponseBody
    public ResponseEntity<String> requestHandlingMethodNotSupported(HttpServletRequest req, NoSuchRequestHandlingMethodException ex) {
        Locale locale = LocaleContextHolder.getLocale();
        String errorMessage = messageSource.getMessage("error.patient.bad.id", null, locale);

        String errorURL = req.getRequestURL().toString();

        ErrorInfo errorInfo = new ErrorInfo(errorURL, errorMessage);
        return new ResponseEntity<String>(errorInfo.toJson(), HttpStatus.BAD_REQUEST);
    }


}

回答1:

After digging around DispatcherServlet and HttpServletBean.init() in SpringFramework I see that its possible in Spring 4.

org.springframework.web.servlet.DispatcherServlet

/** Throw a NoHandlerFoundException if no Handler was found to process this request? **/
private boolean throwExceptionIfNoHandlerFound = false;

protected void noHandlerFound(HttpServletRequest request, HttpServletResponse response) throws Exception {
    if (pageNotFoundLogger.isWarnEnabled()) {
        String requestUri = urlPathHelper.getRequestUri(request);
        pageNotFoundLogger.warn("No mapping found for HTTP request with URI [" + requestUri +
                "] in DispatcherServlet with name '" + getServletName() + "'");
    }
    if(throwExceptionIfNoHandlerFound) {
        ServletServerHttpRequest req = new ServletServerHttpRequest(request);
        throw new NoHandlerFoundException(req.getMethod().name(),
                req.getServletRequest().getRequestURI(),req.getHeaders());
    } else {
        response.sendError(HttpServletResponse.SC_NOT_FOUND);
    }
}

throwExceptionIfNoHandlerFound is false by default and we should enable that in web.xml

<servlet>
    <servlet-name>appServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>throwExceptionIfNoHandlerFound</param-name>
            <param-value>true</param-value>
        </init-param>
    <load-on-startup>1</load-on-startup>
    <async-supported>true</async-supported>
</servlet>

And then you can catch it in a class annotated with @ControllerAdvice using this method.

@ExceptionHandler(NoHandlerFoundException.class)
@ResponseStatus(value=HttpStatus.NOT_FOUND)
@ResponseBody
public ResponseEntity<String> requestHandlingNoHandlerFound(HttpServletRequest req, NoHandlerFoundException ex) {
    Locale locale = LocaleContextHolder.getLocale();
    String errorMessage = messageSource.getMessage("error.bad.url", null, locale);

    String errorURL = req.getRequestURL().toString();

    ErrorInfo errorInfo = new ErrorInfo(errorURL, errorMessage);
    return new ResponseEntity<String>(errorInfo.toJson(), HttpStatus.BAD_REQUEST);
}

Which allows me to return JSON response for bad URLs for which no mapping exist, instead of redirecting to a JSP page :)

{"message":"URL does not exist","url":"http://localhost:8080/service/patientssd"}


回答2:

If you are using Spring Boot, set BOTH of these two properties:

spring.resources.add-mappings=false
spring.mvc.throw-exception-if-no-handler-found=true

Now your @ControllerAdvice annotated class can handle the "NoHandlerFoundException", as below.

@ControllerAdvice
@RequestMapping(produces = "application/json")
@ResponseBody
public class RestControllerAdvice {

    @ExceptionHandler(NoHandlerFoundException.class)
    public ResponseEntity<Map<String, Object>> unhandledPath(final NoHandlerFoundException e) {
        Map<String, Object> errorInfo = new LinkedHashMap<>();
        errorInfo.put("timestamp", new Date());
        errorInfo.put("httpCode", HttpStatus.NOT_FOUND.value());
        errorInfo.put("httpStatus", HttpStatus.NOT_FOUND.getReasonPhrase());
        errorInfo.put("errorMessage", e.getMessage());
        return new ResponseEntity<Map<String, Object>>(errorInfo, HttpStatus.NOT_FOUND);
    }

}

NOTE it is not sufficient to ONLY specify this property:

spring.mvc.throw-exception-if-no-handler-found=true

, as by default Spring maps unknown urls to /**, so there really never is "no handler found".

To disable the unknown url mapping to /**, you need

spring.resources.add-mappings=false ,

which is why the two properties TOGETHER produce the desired behavior.



回答3:

If you're using spring 3.2 or later you can use a controller advice (@ControllerAdvice) to deal with, amongst other things, mapping errors (404's). You can find documentation here. Take a look at section 17.11. You can use this, for example, to provide more detailed logging on why your request bindings aren't being matched for specific urls, or to simply return a more specific response than a generic 404.



回答4:

you can return json in the location below,that /handle/404.

<error-page>
    <error-code>404</error-code>
    <location>/handle/404</location>
</error-page>

after you config this in web.xml,a 404 error will redirect to /handle/404,and you can create a controller with this mapping and return a json result. for example.

@RestController
@RequestMapping(value = "handle")
public class HttpErrorController {
   @RequestMapping(value = "404")
   public String handle404() {
      return "404 error";
   }
}