Day616.SpringException常见错误 -Spring常见编程错误
Posted 阿昌喜欢吃黄桃
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Day616.SpringException常见错误 -Spring常见编程错误相关的知识,希望对你有一定的参考价值。
SpringException常见错误
Spring针对异常处理也会存在系列的问题,以下举两个SpringException的常见错误。
一、小心过滤器异常
还是沿用之前在事务处理中用到的学生注册的案例,来讨论异常处理的问题:
@Controller
@Slf4j
public class StudentController
public StudentController()
System.out.println("construct");
@PostMapping("/regStudent/name")
@ResponseBody
public String saveUser(String name) throws Exception
System.out.println("......用户注册成功");
return "success";
为了保证安全,这里需要给请求加一个保护,通过验证 Token 的方式来验证请求的合法性。
这个 Token 需要在每次发送请求的时候带在请求的 header 中,header 的 key 是 Token。
为了校验这个 Token,我们引入了一个 Filter 来处理这个校验工作,这里我使用了一个最简单的 Token:111111。当 Token 校验失败时,就会抛出一个自定义的 NotAllowException
,交由 Spring 处理:
@WebFilter
@Component
public class PermissionFilter implements Filter
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader("token");
if (!"111111".equals(token))
System.out.println("throw NotAllowException");
throw new NotAllowException();
chain.doFilter(request, response);
@Override
public void init(FilterConfig filterConfig) throws ServletException
@Override
public void destroy()
NotAllowException 就是一个简单的 RuntimeException
的子类:
public class NotAllowException extends RuntimeException
public NotAllowException()
super();
同时,新增了一个 RestControllerAdvice
来处理这个异常,处理方式也很简单,就是返回一个 403 的 resultCode:
@RestControllerAdvice
public class NotAllowExceptionHandler
@ExceptionHandler(NotAllowException.class)
@ResponseBody
public String handle()
System.out.println("403");
return "\\"resultCode\\": 403";
为了验证一下失败的情况,我们模拟了一个请求,在 HTTP 请求头里加上一个 Token,值为 111,这样就会引发错误了,我们可以看看会不会被 NotAllowExceptionHandler 处理掉。
然而,在控制台上,我们只看到了下面这样的输出,这其实就说明了 NotAllowExceptionHandler
并没有生效。
throw NotAllowException
那问题出在哪里呢???
针对SringFilter的执行流程图如下:
从这张图中可以看出,当所有的过滤器被执行完毕以后,Spring 才会进入 Servlet 相关的处理,而 DispatcherServlet
才是整个 Servlet 处理的核心,它是前端控制器设计模式的实现,提供 Spring Web MVC 的集中访问点并负责职责的分派。
正是在这里,Spring 处理了请求和处理器之间的对应关系,以及这个案例我们所关注的问题——统一异常处理。其实说到这里,我们已经了解到过滤器内异常无法被统一处理的大致原因,就是因为异常处理发生在上图的红色区域,即 DispatcherServlet 中的 doDispatch(),而此时,过滤器已经全部执行完毕了。
针对 ControllerAdvice 是如何被 Spring 加载并对外暴露的。
在 Spring Web 的核心配置类 WebMvcConfigurationSupport 中,被 @Bean 修饰的 handlerExceptionResolver(),会调用 addDefaultHandlerExceptionResolvers() 来添加默认的
异常解析器。
@Bean
public HandlerExceptionResolver handlerExceptionResolver(
@Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager)
List<HandlerExceptionResolver> exceptionResolvers = new ArrayList<>();
configureHandlerExceptionResolvers(exceptionResolvers);
//当exceptionResolvers为空的时候
if (exceptionResolvers.isEmpty())
//添加默认的异常解析器
addDefaultHandlerExceptionResolvers(exceptionResolvers, contentNegotiationManager);
extendHandlerExceptionResolvers(exceptionResolvers);
HandlerExceptionResolverComposite composite = new HandlerExceptionResolverComposite();
composite.setOrder(0);
composite.setExceptionResolvers(exceptionResolvers);
return composite;
最终按照下图的调用栈,Spring 实例化了 ExceptionHandlerExceptionResolver 类。
从源码中我们可以看出,ExceptionHandlerExceptionResolver 类实现了 InitializingBean 接口,并覆写了 afterPropertiesSet()。
public void afterPropertiesSet()
// Do this first, it may add ResponseBodyAdvice beans
initExceptionHandlerAdviceCache();
//省略非关键代码
并在 initExceptionHandlerAdviceCache() 中完成了所有 ControllerAdvice 中的 ExceptionHandler 的初始化。
其具体操作,就是查找所有 @ControllerAdvice 注解的 Bean,把它们放到成员变量 exceptionHandlerAdviceCache 中。在我们这个案例里,就是指 NotAllowExceptionHandler 这个异常处理器。
private void initExceptionHandlerAdviceCache()
//省略非关键代码
List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
for (ControllerAdviceBean adviceBean : adviceBeans)
Class<?> beanType = adviceBean.getBeanType();
if (beanType == null)
throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);
if (resolver.hasExceptionMappings())
this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
//省略非关键代码
到这,我们可以总结一下,WebMvcConfigurationSupport 中的 handlerExceptionResolver() 实例化并注册了一个 ExceptionHandlerExceptionResolver 的实例,而所有被 @ControllerAdvice 注解修饰的异常处理器,都会在 ExceptionHandlerExceptionResolver 实例化的时候自动扫描并装载在其类成员变量 exceptionHandlerAdviceCache 中。
当第一次请求发生时,DispatcherServlet 中的 initHandlerExceptionResolvers() 将获取所有注册到 Spring 的 HandlerExceptionResolver 类型的实例,而 ExceptionHandlerExceptionResolver 恰好实现了 HandlerExceptionResolver 接口,这些 HandlerExceptionResolver 类型的实例则会被写入到类成员变量 handlerExceptionResolvers 中。
private void initHandlerExceptionResolvers(ApplicationContext context)
this.handlerExceptionResolvers = null;
if (this.detectAllHandlerExceptionResolvers)
// Find all HandlerExceptionResolvers in the ApplicationContext, including ancestor contexts.
Map<String, HandlerExceptionResolver> matchingBeans = BeanFactoryUtils
.beansOfTypeIncludingAncestors(context, HandlerExceptionResolver.class, true, false);
if (!matchingBeans.isEmpty())
this.handlerExceptionResolvers = new ArrayList<>(matchingBeans.values());
// We keep HandlerExceptionResolvers in sorted order.
AnnotationAwareOrderComparator.sort(this.handlerExceptionResolvers);
//省略非关键代码
ControllerAdvice 是如何被 Spring 消费并处理异常的。
下文贴出的是核心类 DispatcherServlet 中的核心方法 doDispatch() 的部分代码:
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception
//省略非关键代码
try
ModelAndView mv = null;
Exception dispatchException = null;
try
//省略非关键代码
//查找当前请求对应的 handler,并执行
//省略非关键代码
catch (Exception ex)
dispatchException = ex;
catch (Throwable err)
dispatchException = new NestedServletException("Handler dispatch failed", err);
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
//省略非关键代码
Spring 在执行用户请求时,当在“查找”和“执行”请求对应的 handler 过程中发生异常,就会把异常赋值给 dispatchException,再交给 processDispatchResult() 进行处理。
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception
boolean errorView = false;
if (exception != null)
if (exception instanceof ModelAndViewDefiningException)
mv = ((ModelAndViewDefiningException) exception).getModelAndView();
else
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
mv = processHandlerException(request, response, handler, exception);
errorView = (mv != null);
//省略非关键代码
进一步处理后,即当 Exception 不为 null 时,继续交给 processHandlerException 处理。
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
@Nullable Object handler, Exception ex) throws Exception
//省略非关键代码
ModelAndView exMv = null;
if (this.handlerExceptionResolvers != null)
for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers)
exMv = resolver.resolveException(request, response, handler, ex);
if (exMv != null)
break;
//省略非关键代码
然后,processHandlerException 会从类成员变量 handlerExceptionResolvers 中获取有效的异常解析器,对异常进行解析。
显然,这里的 handlerExceptionResolvers 一定包含我们声明的 NotAllowExceptionHandler#NotAllowException 的异常处理器的 ExceptionHandlerExceptionResolver 包装类。
解决方案:
为了利用 Spring MVC 的异常处理机制,我们需要对 Filter 做一些改造。手动捕获异常,并将异常 HandlerExceptionResolver 进行解析处理。我们可以这样修改 PermissionFilter,注入 HandlerExceptionResolver:
@Autowired
@Qualifier("handlerExceptionResolver")
private HandlerExceptionResolver resolver;
然后,在 doFilter 里捕获异常并交给 HandlerExceptionResolver 处理:
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
String token = httpServletRequest.getHeader("token");
if (!"111111".equals(token))
System.out.println("throw NotAllowException");
resolver.resolveException(httpServletRequest, httpServletResponse, null, new NotAllowException());
return;
chain.doFilter(request, response);
当我们尝试用错误的 Token 请求,控制台得到了以下信息:
throw NotAllowException
403
返回的 JSON 是:
“resultCode”: 403
二、自定义抛出404 异常失效
继续沿用案例,为了防止一些异常的访问,我们需要记录所有 404 状态
的访问记录,并返回一个我们的自定义结果。
一般使用 RESTful 接口时我们会统一返回 JSON 数据,返回值格式如下:
“resultCode”: 404
但是 Spring 对 404 异常是进行了默认资源映射的,并不会返回我们想要的结果,也不会对这种错误做记录。
于是我们添加了一个 ExceptionHandlerController,它被声明成 @RestControllerAdvice 来全局捕获 Spring MVC 中抛出的异常。ExceptionHandler 的作用正是用来捕获指定的异常:
@RestControllerAdvice
public class MyExceptionHandler
@ResponseStatus(HttpStatus.NOT_FOUND)
@ExceptionHandler(Exception.class)
@ResponseBody
public String handle404()
System.out.println("404");
return "\\"resultCode\\": 404";
当效果不如人意,如下:
“timestamp”:“2021-05-19T22:24:01.559+0000”,“status”:404,“error”:“Not Found”,“message”:“No message available”,“path”:“/regStudent1”
一看就知道Spring自己捕获到了一场,并处理抛出
那针对为什么会这样子,我们肯定会想,Spring的那个异常处理器处理了我们的抛出的404异常?
从DispatcherServlet 中的 doDispatch() 核心代码寻找,如下:
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception
//省略非关键代码
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null)
noHandlerFound(processedRequest, response);
return;
//省略非关键代码
首先调用 getHandler() 获取当前请求的处理器,如果获取不到,则调用 noHandlerFound():
protected void noHandlerFound(HttpServletRequest request, HttpServletResponse response) throws Exception
//this.throwExceptionIfNoHandlerFound为true,就会抛出NoHandlerFoundException异常
if (this.throwExceptionIfNoHandlerFound)
throw new NoHandlerFoundException(request.getMethod(), getRequestUri(request),
new ServletServerHttpRequest(request).getHeaders());
else
response.sendError(HttpServletResponse.SC_NOT_FOUND);
noHandlerFound() 的逻辑非常简单,如果 throwExceptionIfNoHandlerFound 属性为 true
,则直接抛出 NoHandlerFoundException 异常,反之则会进一步获取到对应的请求处理器执行,并将执行结果返回给客户端。
实际上这里还存在另一个坑,在 Spring Web 的 WebMvcAutoConfiguration 类中,其默认添加的两个 ResourceHandler,一个是用来处理请求路径 /webjars/**
,而另一个是 /**
。
即便当前请求没有定义任何对应的请求处理器,getHandler() 也一定会获取到一个 Handler 来处理当前请求,因为第二个匹配 /** 路径的 ResourceHandler 决定了任何请求路径都会被其处理。
mappedHandler == null 判断条件永远不会成立,显然就不可能走到 noHandlerFound(),那么就不会抛出 NoHandlerFoundException 异常,也无法被后续的异常处理器进一步处理。
WebMvcAutoConfiguration 类中的 addResourceHandlers(),通过这个方法,我们可以知道当前有哪些 ResourceHandler 的集合被注册到了 Spring 容器中:
public void addResourceHandlers(ResourceHandlerRegistry registry)
//可以看到,当this.resourceProperties.isAddMappings()为false的时候,就会直接reture,直接跳过,不添加以下两个默认异常处理器
if (!this.resourceProperties.isAddMappings())
logger.debug("Default resource handling disabled");
return;
Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
if (!registry.hasMappingForPattern("/webjars/**"))
customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/")
.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
String staticPathPattern = this.mvcProperties.getStaticPathPattern();
if (!registry.hasMappingForPattern(staticPathPattern))
customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
.addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))
.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
从而验证我们一开始得出的结论,此处添加了两个 ResourceHandler,一个是用来处理请求路径 /webjars/**
, 而另一个是 /**
。
if (!this.resourceProperties.isAddMappings())
logger.debug("Default resource handling disabled");
return;
可以看到,当this.resourceProperties.isAddMappings()为false的时候,就会直接reture,直接跳过
,不添加以下两个默认异常处理器
那对应的解决方案:
增加两个配置文件
如下:
spring.resources.add-mappings=false
spring.mvc.throwExceptionIfNoHandlerFound=true
修改 MyExceptionHandler 的 @ExceptionHandler 为 NoHandlerFoundException
即可:
@ExceptionHandler(NoHandlerFoundException.class)
三、总结
- DispatcherServlet 类中的 doDispatch() 是整个 Servlet 处理的核心,它不仅实现了请求的分发,也提供了异常统一处理等等一系列功能;
- WebMvcConfigurationSupport 是 Spring Web 中非常核心的一个配置类,无论是异常处理器的包装注册(HandlerExceptionResolver),还是资源处理器的包装注册(SimpleUrlHandlerMapping),都是依靠这个类来完成的。
以上是关于Day616.SpringException常见错误 -Spring常见编程错误的主要内容,如果未能解决你的问题,请参考以下文章
day.java:5: 错: 编码 GBK 的不可映射字符 (0x88) System.out.println((i+1)+"链?"+"链?"+day[i]+&