在SpringBoot的开发中,为了进步程序运行的鲁棒性,咱们经常需要对各种程序反常进行处理,但是假如在每个出反常的当地进行独自处理的话,这会引入很多事务不相关的反常处理代码,增加了程序的耦合,同时未来想改动反常的处理逻辑,也变得比较困难。这篇文章带大家了解一下怎么高雅的进行大局反常处理。

为了完结大局阻拦,这儿运用到了Spring中供给的两个注解,@RestControllerAdvice@ExceptionHandler,结合运用能够阻拦程序中产生的反常,而且根据不同的反常类型别离处理。下面我会先介绍怎么利用这两个注解,高雅的完结大局反常的处理,接着解释这背面的原理。

1. 怎么完结大局阻拦?

1.1 自界说反常处理类

在下面的比如中,咱们承继了ResponseEntityExceptionHandler并运用@RestControllerAdvice注解了这个类,接着结合@ExceptionHandler针对不同的反常类型,来界说不同的反常处理办法。这儿能够看到我处理的反常是自界说反常,后续我会展开介绍。

ResponseEntityExceptionHandler中包装了各种SpringMVC在处理恳求时可能抛出的反常的处理,处理结果都是封装成一个ResponseEntity目标。ResponseEntityExceptionHandler是一个抽象类,通常咱们需要界说一个用来处理反常的运用@RestControllerAdvice注解标示的反常处理类来承继自ResponseEntityExceptionHandler。ResponseEntityExceptionHandler中为每个反常的处理都独自界说了一个办法,假如默认的处理不能满足你的需求,则能够重写对某个反常的处理。

@Log4j2
@RestControllerAdvice  
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {  
    /**  
     * 界说要捕获的反常 能够多个 @ExceptionHandler({})     *  
     * @param request  request  
     * @param e        exception  
     * @param response response  
     * @return 响应结果  
     */  
    @ExceptionHandler(AuroraRuntimeException.class)  
    public GenericResponse customExceptionHandler(HttpServletRequest request, final Exception e, HttpServletResponse response) {  
        AuroraRuntimeException exception = (AuroraRuntimeException) e;  
       if (exception.getCode() == ResponseCode.USER_INPUT_ERROR) {  
           response.setStatus(HttpStatus.BAD_REQUEST.value());  
       } else if (exception.getCode() == ResponseCode.FORBIDDEN) {  
           response.setStatus(HttpStatus.FORBIDDEN.value());  
       } else {  
           response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());  
       }  
        return new GenericResponse(exception.getCode(), null, exception.getMessage());  
    }  
    @ExceptionHandler(NotLoginException.class)  
    public GenericResponse tokenExceptionHandler(HttpServletRequest request, final Exception e, HttpServletResponse response) {  
        log.error("token exception", e);  
        response.setStatus(HttpStatus.FORBIDDEN.value());  
        return new GenericResponse(ResponseCode.AUTHENTICATION_NEEDED);  
    }  
}

1.2 界说反常码

这儿界说了常见的几种反常码,首要用在抛出自界说反常时,对不同的景象进行区分。

@Getter
public enum ResponseCode {  
    SUCCESS(0, "Success"),  
    INTERNAL_ERROR(1, "服务器内部过错"),  
    USER_INPUT_ERROR(2, "用户输入过错"),  
    AUTHENTICATION_NEEDED(3, "Token过期或无效"),  
    FORBIDDEN(4, "禁止拜访"),  
    TOO_FREQUENT_VISIT(5, "拜访太频繁,请休息一会儿");  
    private final int code;  
    private final String message;  
    private final Response.Status status;  
    ResponseCode(int code, String message, Response.Status status) {  
        this.code = code;  
        this.message = message;  
        this.status = status;  
    }  
    ResponseCode(int code, String message) {  
        this(code, message, Response.Status.INTERNAL_SERVER_ERROR);  
    }  
}

1.3 自界说反常类

这儿我界说了一个AuroraRuntimeException的反常,便是在上面的反常处理函数中,用到的反常。每个反常实例会有一个对应的反常码,也便是前面刚界说好的。

@Getter
public class AuroraRuntimeException extends RuntimeException {  
    private final ResponseCode code;  
    public AuroraRuntimeException() {  
        super(String.format("%s", ResponseCode.INTERNAL_ERROR.getMessage()));  
        this.code = ResponseCode.INTERNAL_ERROR;  
    }  
    public AuroraRuntimeException(Throwable e) {  
        super(e);  
        this.code = ResponseCode.INTERNAL_ERROR;  
    }  
    public AuroraRuntimeException(String msg) {  
        this(ResponseCode.INTERNAL_ERROR, msg);  
    }  
    public AuroraRuntimeException(ResponseCode code) {  
        super(String.format("%s", code.getMessage()));  
        this.code = code;  
    }  
    public AuroraRuntimeException(ResponseCode code, String msg) {  
        super(msg);  
        this.code = code;  
    }  
}

1.4 自界说回来类型

为了确保各个接口的回来一致,这儿专门界说了一个回来类型。

@Getter
@Setter  
public class GenericResponse<T> {  
    private int code;  
    private T data;  
    private String message;  
    public GenericResponse() {};  
    public GenericResponse(int code, T data) {  
        this.code = code;  
        this.data = data;  
    }  
    public GenericResponse(int code, T data, String message) {  
        this(code, data);  
        this.message = message;  
    }  
    public GenericResponse(ResponseCode responseCode) {  
        this.code = responseCode.getCode();  
        this.data = null;  
        this.message = responseCode.getMessage();  
    }  
    public GenericResponse(ResponseCode responseCode, T data) {  
        this(responseCode);  
        this.data = data;  
    }  
    public GenericResponse(ResponseCode responseCode, T data, String message) {  
        this(responseCode, data);  
        this.message = message;  
    }  
}

实际测验反常

下面的比如中,咱们想获取到用户的信息,假如用户的信息不存在,能够直接抛出一个反常,这个反常会被咱们上面界说的大局反常处理办法所捕获,然后根据不同的反常编码,完结不同的处理和回来。

public User getUserInfo(Long userId) {
	// some logic
    User user = daoFactory.getExtendedUserMapper().selectByPrimaryKey(userId);  
    if (user == null) {  
        throw new AuroraRuntimeException(ResponseCode.USER_INPUT_ERROR, "用户id不存在");  
    }
    // some logic
	....
}

以上就完结了整个大局反常的处理进程,接下来要点说说为什么@RestControllerAdvice@ExceptionHandler结合运用能够阻拦程序中产生的反常?

大局阻拦的背面原理?

下面会说到@ControllerAdvice注解,简略地说,@RestControllerAdvice与@ControllerAdvice的差异就和@RestController与@Controller的差异类似,@RestControllerAdvice注解包含了@ControllerAdvice注解和@ResponseBody注解。

接下来咱们深入Spring源码,看看是怎么完结的,首要DispatcherServlet目标在创立时会初始化一系列的目标,这儿要点重视函数initHandlerExceptionResolvers(context);.

public class DispatcherServlet extends FrameworkServlet {
    // ......
	protected void initStrategies(ApplicationContext context) {
		initMultipartResolver(context);
		initLocaleResolver(context);
		initThemeResolver(context);
		initHandlerMappings(context);
		initHandlerAdapters(context);
		// 要点重视
		initHandlerExceptionResolvers(context);
		initRequestToViewNameTranslator(context);
		initViewResolvers(context);
		initFlashMapManager(context);
	}
    // ......
}

在initHandlerExceptionResolvers(context)办法中,会取得一切完结了HandlerExceptionResolver接口的bean并保存起来,其间就有一个类型为ExceptionHandlerExceptionResolver的bean,这个bean在应用发动进程中会获取一切被@ControllerAdvice注解标示的bean目标做进一步处理,关键代码在这儿:

public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExceptionResolver
		implements ApplicationContextAware, InitializingBean {
    // ......
	private void initExceptionHandlerAdviceCache() {
		// ......
		List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
		AnnotationAwareOrderComparator.sort(adviceBeans);
		for (ControllerAdviceBean adviceBean : adviceBeans) {
			ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(adviceBean.getBeanType());
			if (resolver.hasExceptionMappings()) {
			    // 找到一切ExceptionHandler标示的办法并保存成一个ExceptionHandlerMethodResolver类型的目标缓存起来
				this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
				if (logger.isInfoEnabled()) {
					logger.info("Detected @ExceptionHandler methods in " + adviceBean);
				}
			}
			// ......
		}
	}
    // ......
}

当Controller抛出反常时,DispatcherServlet经过ExceptionHandlerExceptionResolver来解析反常,而ExceptionHandlerExceptionResolver又经过ExceptionHandlerMethodResolver 来解析反常, ExceptionHandlerMethodResolver 最终解析反常找到适用的@ExceptionHandler标示的办法是这儿:

public class ExceptionHandlerMethodResolver {
	// ......
	private Method getMappedMethod(Class<? extends Throwable> exceptionType) {
		List<Class<? extends Throwable>> matches = new ArrayList<Class<? extends Throwable>>();
		// 找到一切适用于Controller抛出反常的处理办法,例如Controller抛出的反常
		// 是AuroraRuntimeException(承继自RuntimeException),那么@ExceptionHandler(AuroraRuntimeException.class)和
		// @ExceptionHandler(Exception.class)标示的办法都适用此反常
		for (Class<? extends Throwable> mappedException : this.mappedMethods.keySet()) {
			if (mappedException.isAssignableFrom(exceptionType)) {
				matches.add(mappedException);
			}
		}
		if (!matches.isEmpty()) {
		/* 这儿经过排序找到最适用的办法,排序的规则根据抛出反常相对于声明反常的深度,例如
	Controller抛出的反常是是AuroraRuntimeException(承继自RuntimeException),那么AuroraRuntimeException
	相对于@ExceptionHandler(AuroraRuntimeException.class)声明的AuroraRuntimeException.class其深度是0,
	相对于@ExceptionHandler(Exception.class)声明的Exception.class其深度是2,所以
	@ExceptionHandler(BizException.class)标示的办法会排在前面 */
			Collections.sort(matches, new ExceptionDepthComparator(exceptionType));
			return this.mappedMethods.get(matches.get(0));
		}
		else {
			return null;
		}
	}
    // ......
}

整个@RestControllerAdvice处理的流程便是这样,结合@ExceptionHandler就完结了对不同反常的灵活处理。


重视大众号【码老思】,第一时间获取最通俗易懂的原创技术干货。