5

前言

温馨提示:本文使用的spring boot版本为2.1.8.RELEASE。

全局异常处理大家应该都接触过,也不是什么难事,网上一搜一大堆,但是写的对不对只能自己测试了,运气好的话找了一个能用的,运气不好的可能会烦到你怀疑人生。

我就是那个运气不好的人,也是因为碰到了一些问题,所以才会有这篇文章吧。

优势

全局异常处理主要的好处:

  1. 统一接口返回格式。比如说请求方法错误,本来是get但是用成了post,这种错误都不会经过自己写的代码,无法控制返回的数据格式。
  2. 方便记录日志。跟上面一样的道理,有的错误框架直接返回了,可能都没有日志记录。
  3. 减少冗余代码。统一处理总比每一个接口自己处理要好吧。

做法

一般的方案都是基于@ControllerAdvice@ExceptionHandler做的。

  • @ControllerAdvice相当于controller的切面,主要用于@ExceptionHandler, @InitBinder@ModelAttribute,使注解标注的方法对每一个controller都起作用。默认对所有controller都起作用,当然也可以通过@ControllerAdvice注解中的一些属性选定符合条件的controller。
  • @ExceptionHandler用于异常处理的注解,可以通过value指定处理哪种类型的异常还可以与@ResponseStatus搭配使用,处理特定的http错误。标记的方法入参与返回值都有很大的灵活性,具体可以看注释也可以后边的深度探究。

其实有上面两个已经够了,还可以继承ResponseEntityExceptionHandler

读一下这个类的注释就知道它是干啥的了

ResponseEntityExceptionHandler

大概意思就是这个类是为了方便统一异常处理的基类,但是要注意返回的是ResponseEntity,如果不需要往响应体中写内容或者返回一个视图,可以使用DefaultHandlerExceptionResolver

可以看一下这个类的实现

/**
    * Provides handling for standard Spring MVC exceptions.
    * @param ex the target exception
    * @param request the current request
    */
@ExceptionHandler({
        HttpRequestMethodNotSupportedException.class,
        HttpMediaTypeNotSupportedException.class,
        HttpMediaTypeNotAcceptableException.class,
        MissingPathVariableException.class,
        MissingServletRequestParameterException.class,
        ServletRequestBindingException.class,
        ConversionNotSupportedException.class,
        TypeMismatchException.class,
        HttpMessageNotReadableException.class,
        HttpMessageNotWritableException.class,
        MethodArgumentNotValidException.class,
        MissingServletRequestPartException.class,
        BindException.class,
        NoHandlerFoundException.class,
        AsyncRequestTimeoutException.class
    })
@Nullable
public final ResponseEntity<Object> handleException(Exception ex, WebRequest request) throws Exception {
    HttpHeaders headers = new HttpHeaders();

    if (ex instanceof HttpRequestMethodNotSupportedException) {
        HttpStatus status = HttpStatus.METHOD_NOT_ALLOWED;
        return handleHttpRequestMethodNotSupported((HttpRequestMethodNotSupportedException) ex, headers, status, request);
    }
    else if (ex instanceof HttpMediaTypeNotSupportedException) {
        HttpStatus status = HttpStatus.UNSUPPORTED_MEDIA_TYPE;
        return handleHttpMediaTypeNotSupported((HttpMediaTypeNotSupportedException) ex, headers, status, request);
    }
    else if (ex instanceof HttpMediaTypeNotAcceptableException) {
        HttpStatus status = HttpStatus.NOT_ACCEPTABLE;
        return handleHttpMediaTypeNotAcceptable((HttpMediaTypeNotAcceptableException) ex, headers, status, request);
    }
    else if (ex instanceof MissingPathVariableException) {
        HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
        return handleMissingPathVariable((MissingPathVariableException) ex, headers, status, request);
    }
    else if (ex instanceof MissingServletRequestParameterException) {
        HttpStatus status = HttpStatus.BAD_REQUEST;
        return handleMissingServletRequestParameter((MissingServletRequestParameterException) ex, headers, status, request);
    }
    else if (ex instanceof ServletRequestBindingException) {
        HttpStatus status = HttpStatus.BAD_REQUEST;
        return handleServletRequestBindingException((ServletRequestBindingException) ex, headers, status, request);
    }
    else if (ex instanceof ConversionNotSupportedException) {
        HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
        return handleConversionNotSupported((ConversionNotSupportedException) ex, headers, status, request);
    }
    else if (ex instanceof TypeMismatchException) {
        HttpStatus status = HttpStatus.BAD_REQUEST;
        return handleTypeMismatch((TypeMismatchException) ex, headers, status, request);
    }
    else if (ex instanceof HttpMessageNotReadableException) {
        HttpStatus status = HttpStatus.BAD_REQUEST;
        return handleHttpMessageNotReadable((HttpMessageNotReadableException) ex, headers, status, request);
    }
    else if (ex instanceof HttpMessageNotWritableException) {
        HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
        return handleHttpMessageNotWritable((HttpMessageNotWritableException) ex, headers, status, request);
    }
    else if (ex instanceof MethodArgumentNotValidException) {
        HttpStatus status = HttpStatus.BAD_REQUEST;
        return handleMethodArgumentNotValid((MethodArgumentNotValidException) ex, headers, status, request);
    }
    else if (ex instanceof MissingServletRequestPartException) {
        HttpStatus status = HttpStatus.BAD_REQUEST;
        return handleMissingServletRequestPart((MissingServletRequestPartException) ex, headers, status, request);
    }
    else if (ex instanceof BindException) {
        HttpStatus status = HttpStatus.BAD_REQUEST;
        return handleBindException((BindException) ex, headers, status, request);
    }
    else if (ex instanceof NoHandlerFoundException) {
        HttpStatus status = HttpStatus.NOT_FOUND;
        return handleNoHandlerFoundException((NoHandlerFoundException) ex, headers, status, request);
    }
    else if (ex instanceof AsyncRequestTimeoutException) {
        HttpStatus status = HttpStatus.SERVICE_UNAVAILABLE;
        return handleAsyncRequestTimeoutException((AsyncRequestTimeoutException) ex, headers, status, request);
    }
    else {
        // Unknown exception, typically a wrapper with a common MVC exception as cause
        // (since @ExceptionHandler type declarations also match first-level causes):
        // We only deal with top-level MVC exceptions here, so let's rethrow the given
        // exception for further processing through the HandlerExceptionResolver chain.
        throw ex;
    }
}

对于每一种异常,可以重写相应的方法。同时每个异常的具体处理方法最后又调用了同一个方法

    /**
     * A single place to customize the response body of all exception types.
     * <p>The default implementation sets the {@link WebUtils#ERROR_EXCEPTION_ATTRIBUTE}
     * request attribute and creates a {@link ResponseEntity} from the given
     * body, headers, and status.
     * @param ex the exception
     * @param body the body for the response
     * @param headers the headers for the response
     * @param status the response status
     * @param request the current request
     */
    protected ResponseEntity<Object> handleExceptionInternal(
            Exception ex, @Nullable Object body, HttpHeaders headers, HttpStatus status, WebRequest request) {

        if (HttpStatus.INTERNAL_SERVER_ERROR.equals(status)) {
            request.setAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE, ex, WebRequest.SCOPE_REQUEST);
        }
        return new ResponseEntity<>(body, headers, status);
    }

通用的处理可以在这个方法中实现,返回结果中可以自定义body、header、以及http status。比如对于一些异常,可能并不希望http status code为500,而是返回200,用body里的code再去判断是成功还是失败,用这种方法就非常容易实现。

使用这种方案,我们自己写的类就应该类似这种

package com.zworks.aircraft.web.advice;

import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

@ControllerAdvice
public class AirCraftExceptionHandler extends ResponseEntityExceptionHandler {

}

实战

可以去Spring Initializr去初始化工程

依赖只需要web就可以了,为了方便可以添加lombok

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.8.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.zworks</groupId>
    <artifactId>aircraft</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>aircraft</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

定义一个通用返回结果

package com.zworks.aircraft.model;

import lombok.Data;

@Data
public class RespMsg<T> {

    private int code = 200;

    private String msg;

    private T data;

    public RespMsg() {
    }

    public RespMsg(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public RespMsg(T data) {
        this.data = data;
    }

    public static <T> RespMsg<T> success() {
        return new RespMsg();
    }

    public static <T> RespMsg<T> success(T data) {
        return new RespMsg(data);
    }

    public static RespMsg failed(int code, String msg) {
        return new RespMsg(code, msg);
    }
}

定义用户实体类,在字段上加上校验,方便演示错误的请求

package com.zworks.aircraft.model;

import lombok.Data;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

@Data
public class User {

    @NotNull(message = "id不能为空")
    private Long id;

    @NotBlank(message = "名称不能为空")
    private String name;
}

定义一个Controller,什么都不用干,只需要验证参数的合法性,然后返回正确即可。

package com.zworks.aircraft.web.controller;

import com.zworks.aircraft.model.RespMsg;
import com.zworks.aircraft.model.User;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;

@RestController
public class UserController {

    @PostMapping(value = "user")
    public RespMsg user(@Valid @RequestBody User user) {
        return RespMsg.success();
    }
}

如果参数正确,返回如下

{
    "code": 200,
    "msg": null,
    "data": null
}

但如果参数错误,比如不传入name,则会返回

{
    "timestamp": "2019-09-07T14:04:54.440+0000",
    "status": 400,
    "error": "Bad Request",
    "errors": [
        {
            "codes": [
                "NotBlank.user.name",
                "NotBlank.name",
                "NotBlank.java.lang.String",
                "NotBlank"
            ],
            "arguments": [
                {
                    "codes": [
                        "user.name",
                        "name"
                    ],
                    "arguments": null,
                    "defaultMessage": "name",
                    "code": "name"
                }
            ],
            "defaultMessage": "名称不能为空",
            "objectName": "user",
            "field": "name",
            "rejectedValue": null,
            "bindingFailure": false,
            "code": "NotBlank"
        }
    ],
    "message": "Validation failed for object='user'. Error count: 1",
    "path": "/user"
}

这肯定不是我们想要的结果。

按照刚才的方案,实现一个全局异常处理。

package com.zworks.aircraft.web.advice;

import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

@ControllerAdvice
public class AirCraftExceptionHandler extends ResponseEntityExceptionHandler {

}

这个时候其实已经生效了,只是默认的什么数据都没有返回,可以重写handleExceptionInternal方法

package com.zworks.aircraft.web.advice;

import com.zworks.aircraft.model.RespMsg;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.lang.Nullable;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import org.springframework.web.util.WebUtils;

@ControllerAdvice
public class AirCraftExceptionHandler extends ResponseEntityExceptionHandler {
    protected ResponseEntity<Object> handleExceptionInternal(
            Exception ex, @Nullable Object body, HttpHeaders headers, HttpStatus status, WebRequest request) {
        if (HttpStatus.INTERNAL_SERVER_ERROR.equals(status)) {
            request.setAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE, ex, WebRequest.SCOPE_REQUEST);
        }
        logger.error(ex.getMessage(), ex);//打印异常信息
        RespMsg respMsg = RespMsg.failed(status.value(), ex.getMessage());//使用http状态码作为返回体的code,同时把异常信息返回
        return new ResponseEntity<>(respMsg, headers, status);
    }
}

这时返回的信息如下

{
    "code": 400,
    "msg": "Validation failed for argument [0] in public com.zworks.aircraft.model.RespMsg com.zworks.aircraft.web.controller.UserController.user(com.zworks.aircraft.model.User): [Field error in object 'user' on field 'name': rejected value [null]; codes [NotBlank.user.name,NotBlank.name,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.name,name]; arguments []; default message [name]]; default message [不能为空]] ",
    "data": null
}

虽然已经是我们想要的格式了,但是返回的内容不太已读。

刚才打印了日志,可以看到报错为MethodArgumentNotValidException,可以通过重写handleMethodArgumentNotValid方法改变异常信息

package com.zworks.aircraft.web.advice;

import com.zworks.aircraft.model.RespMsg;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.lang.Nullable;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import org.springframework.web.util.WebUtils;

@ControllerAdvice
public class AirCraftExceptionHandler extends ResponseEntityExceptionHandler {
    protected ResponseEntity<Object> handleExceptionInternal(
            Exception ex, @Nullable Object body, HttpHeaders headers, HttpStatus status, WebRequest request) {
        if (HttpStatus.INTERNAL_SERVER_ERROR.equals(status)) {
            request.setAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE, ex, WebRequest.SCOPE_REQUEST);
        }
        logger.error(ex.getMessage(), ex);//打印异常信息
        RespMsg respMsg = RespMsg.failed(status.value(), ex.getMessage());//使用http状态码作为返回体的code,同时把异常信息返回
        return new ResponseEntity<>(respMsg, headers, status);
    }

    protected ResponseEntity<Object> handleMethodArgumentNotValid(
            MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
        StringBuffer sb = new StringBuffer();
        ex.getBindingResult().getAllErrors().forEach(error -> {
            sb.append(error.getDefaultMessage()).append(";");
        });
        RespMsg respMsg = RespMsg.failed(status.value(), sb.toString());
        return new ResponseEntity<>(respMsg, headers, HttpStatus.OK);//这里可以根据具体情况改变状态码
    }
}

返回如下

{
    "code": 400,
    "msg": "名称不能为空;",
    "data": null
}

同时需要注意的是,由于我修改了返回码,这次请求返回的是http status code是200而不是之前的400。

拓展

默认异常处理

在最开始没有自定义全局异常处理的时候,也返回了错误信息,那这个是谁处理的呢。

默认spring boot 会提供一个/error映射处理所有的异常。而且会根据请求的不同返回一个页面或是json。

ErrorMvcAutoConfiguration类中可以看到

@Bean
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
public DefaultErrorAttributes errorAttributes() {
    return new DefaultErrorAttributes(this.serverProperties.getError().isIncludeException());
}

@Bean
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) {
    return new BasicErrorController(errorAttributes, this.serverProperties.getError(), this.errorViewResolvers);
}

都可以通过自己定义相应的类进行定制化。

之前由于被网上文章误导,使用前面那种方式没成功,为了记录异常,我还写了个切面。

package com.zworks.aircraft.config;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

@Aspect
@Component
@Slf4j
public class ErrorControllerAspect {

    @Before("execution(public * org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.*(..))")
    public void before(JoinPoint joinPoint) {
        Object arg = joinPoint.getArgs()[0];
        if (arg instanceof HttpServletRequest) {
            HttpServletRequest request = (HttpServletRequest) arg;
            log.error((String) request.getAttribute("javax.servlet.error.message"), (Throwable) request.getAttribute("javax.servlet.error.exception"));
        }
    }
}

返回类型

我被坑主要是因为看了一篇文章,Validation in Spring Boot,我不能说一定是文章的问题,至少我按他的没做对。

异常处理是这么写的

package com.zworks.aircraft.web.advice;

import org.springframework.http.HttpStatus;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;

import java.util.HashMap;
import java.util.Map;

@ControllerAdvice
public class AirCraftExceptionHandler {
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Map<String, String> handleValidationExceptions(MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getAllErrors().forEach((error) -> {
            String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            errors.put(fieldName, errorMessage);
        });
        return errors;
    }
}

注意这里返回的是Map类型

返回和没加是一样的

{
    "timestamp": "2019-09-07T14:30:21.688+0000",
    "status": 500,
    "error": "Internal Server Error",
    "errors": [
        {
            "codes": [
                "NotBlank.user.name",
                "NotBlank.name",
                "NotBlank.java.lang.String",
                "NotBlank"
            ],
            "arguments": [
                {
                    "codes": [
                        "user.name",
                        "name"
                    ],
                    "arguments": null,
                    "defaultMessage": "name",
                    "code": "name"
                }
            ],
            "defaultMessage": "名称不能为空",
            "objectName": "user",
            "field": "name",
            "rejectedValue": null,
            "bindingFailure": false,
            "code": "NotBlank"
        }
    ],
    "message": "Validation failed for object='user'. Error count: 1",
    "path": "/user"
}

然后我在那个方法上打了断点,发现代码居然执行了

代码停在了断点处

然后我就跟代码,也对比了两种实现的执行情况,发现在HandlerMethodReturnValueHandlerCompositeselectHandler方法中会根据返回的类型决定使用那个handler进行处理

@Nullable
private HandlerMethodReturnValueHandler selectHandler(@Nullable Object value, MethodParameter returnType) {
    boolean isAsyncValue = isAsyncReturnValue(value, returnType);
    for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {
        if (isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler)) {
            continue;
        }
        if (handler.supportsReturnType(returnType)) {
            return handler;
        }
    }
    return null;
}

现在的handler是MapMethodProcessor

而如果我们将返回值格式改一下

package com.zworks.aircraft.web.advice;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;

import java.util.HashMap;
import java.util.Map;

@ControllerAdvice
public class AirCraftExceptionHandler {

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity handleValidationExceptions(MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getAllErrors().forEach((error) -> {
            String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            errors.put(fieldName, errorMessage);
        });
        return new ResponseEntity<>(errors, new HttpHeaders(), HttpStatus.OK);//这里可以根据具体情况改变状态码
    }
}

会发现handler变成了HttpEntityMethodProcessor

返回值也没问题了

{
    "name": "名称不能为空"
}

请求参数

大部分人从网上查到了信息,都不会考虑请求参数该传那些,顺序有没有影响吧,受前面返回类型的影响,我也看了下请求参数相关的。

可以在自己的方法中打个断点,然后不断找上层调用。

InvocableHandlerMethodinvokeForRequest方法中可以看到

@Nullable
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
        Object... providedArgs) throws Exception {

    Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
    if (logger.isTraceEnabled()) {
        logger.trace("Arguments: " + Arrays.toString(args));
    }
    return doInvoke(args);
}

会获取参数,然后调用我们的方法

protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
            Object... providedArgs) throws Exception {

        MethodParameter[] parameters = getMethodParameters();
        if (ObjectUtils.isEmpty(parameters)) {
            return EMPTY_ARGS;
        }

        Object[] args = new Object[parameters.length];
        for (int i = 0; i < parameters.length; i++) {
            MethodParameter parameter = parameters[i];
            parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
            args[i] = findProvidedArgument(parameter, providedArgs);
            if (args[i] != null) {
                continue;
            }
            if (!this.resolvers.supportsParameter(parameter)) {
                throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
            }
            try {
                args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
            }
            catch (Exception ex) {
                // Leave stack trace for later, exception may actually be resolved and handled...
                if (logger.isDebugEnabled()) {
                    String exMsg = ex.getMessage();
                    if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
                        logger.debug(formatArgumentError(parameter, exMsg));
                    }
                }
                throw ex;
            }
        }
        return args;
    }

findProvidedArgument中会根据类型找参数

    @Nullable
    protected static Object findProvidedArgument(MethodParameter parameter, @Nullable Object... providedArgs) {
        if (!ObjectUtils.isEmpty(providedArgs)) {
            for (Object providedArg : providedArgs) {
                if (parameter.getParameterType().isInstance(providedArg)) {
                    return providedArg;
                }
            }
        }
        return null;
    }

providedArgs中有两个类型,一个是org.springframework.web.bind.MethodArgumentNotValidException类型,也就是异常,另一个是org.springframework.web.method.HandlerMethod。所以如果单写一个异常是没问题的。

如果通过providedArgs没取到,会从resolvers里去取。

只要这些resolvers能支持的参数,都可以取到。
resolvers
比如看到了上面有ServletRequestMethodArgumentResolver类,可以看到支持HttpSession

@Override
public boolean supportsParameter(MethodParameter parameter) {
    Class<?> paramType = parameter.getParameterType();
    return (WebRequest.class.isAssignableFrom(paramType) ||
            ServletRequest.class.isAssignableFrom(paramType) ||
            MultipartRequest.class.isAssignableFrom(paramType) ||
            HttpSession.class.isAssignableFrom(paramType) ||
            (pushBuilder != null && pushBuilder.isAssignableFrom(paramType)) ||
            Principal.class.isAssignableFrom(paramType) ||
            InputStream.class.isAssignableFrom(paramType) ||
            Reader.class.isAssignableFrom(paramType) ||
            HttpMethod.class == paramType ||
            Locale.class == paramType ||
            TimeZone.class == paramType ||
            ZoneId.class == paramType);
}

我们测试下,在参数里添加一个HttpSession

package com.zworks.aircraft.web.advice;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.context.request.WebRequest;

import javax.servlet.http.HttpSession;
import java.util.HashMap;
import java.util.Map;

@Slf4j
@ControllerAdvice
public class AirCraftExceptionHandler {

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity handleValidationExceptions(WebRequest request, MethodArgumentNotValidException ex, HttpSession httpSession) {
        log.info("sessionId:{}", httpSession.getId());
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getAllErrors().forEach((error) -> {
            String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            errors.put(fieldName, errorMessage);
        });
        return new ResponseEntity<>(errors, new HttpHeaders(), HttpStatus.OK);//这里可以根据具体情况改变状态码
    }
}


可以看到已经输出了sessionId

2019-09-07 23:15:43.257  INFO 25692 --- [nio-8080-exec-1] c.z.a.w.advice.AirCraftExceptionHandler  : sessionId:B9C6E5BA4156BBBC5931FFE9B259E5ED

请求参数的处理方式和返回类型的处理方式是否很相似呢

后记

全局异常处理我也处理过很多次了,大部分也都是网上找一篇,虽然每次不会花很多时间,但是做了这么多遍也只是有个大概印象,并没有很深入的去探究过。整理一下,知识才会变成自己的。

看到了这里一定是真爱了,关注微信公众号【憨憨的春天】第一时间获取更新
图片描述

参考


码农张思壮
31 声望6 粉丝