【Java Web开发学习】Spring MVC异常统一处理
文采有限,若有错误,欢迎留言指正。
转载:https://www.cnblogs.com/yangchongxing/p/9271900.html
目录
1、使用@ControllerAdvice和@ExceptionHandler注解统一处理异常
2、在控制器中使用@ExceptionHandler统一处理异常
3、使用SimpleMappingExceptionResolver统一处理异常
正文
异常处理是每一个系统必须面对的,对于Web系统异常必须统一处理,否者业务代码会被无穷无尽的异常处理包围。对于Spring MVC来说有以下几种异常处理方式。
1、使用@ControllerAdvice和@ExceptionHandler注解统一处理异常(推荐)
我们自定义一个全局异常处理类GlobalExceptionHandler打印异常信息到日志并且跳转到异常页面,看代码
package cn.ycx.web.exception; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.log4j.Logger; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.servlet.ModelAndView; /** * 全局异常处理 * @author 杨崇兴 2018-07-05 */ @ControllerAdvice //已经包含@Component注解,能被自动扫描 public class GlobalExceptionHandler { public Logger logger = Logger.getLogger(getClass()); /** * 所有异常处理,返回名为error的视图 * @param e * @return */ @ExceptionHandler(value={Exception.class}) public ModelAndView exceptionHandler(HttpServletRequest request, HttpServletResponse response, Exception ex) { printStackTrace(ex); ModelAndView mav = new ModelAndView(); mav.setViewName("error"); return mav; } /** * 打印异常堆栈信息 * @param ex */ private void printStackTrace(Exception ex) { StringBuilder errors = new StringBuilder(); errors.append("【异常信息】 "); errors.append(ex.getClass().getName()); if (ex.getMessage() != null) { errors.append(": "); errors.append(ex.getMessage()); } for (StackTraceElement stackTraceElement : ex.getStackTrace()) { errors.append(" at "); errors.append(stackTraceElement.toString()); } //打印异常堆栈信息 logger.fatal(errors.toString()); } }
若异常返回的不是视图而是JSON数据对象怎么办呢?添加@ResponseBody注解,将方法的返回值直接写入到response的body区域。
/** * 所有异常处理 * @param e * @return */ @ExceptionHandler(value={Exception.class})
@ResponseBody public Map<String, String> exceptionHandler(HttpServletRequest request, HttpServletResponse response, Exception ex) { printStackTrace(ex); Map<String, String> data = new HashMap<String, String>(); data.put("status", "failure"); return data; }
@ControllerAdvice注解已经包含@Component注解故能被自动扫描 ,看代码
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Component public @interface ControllerAdvice
@ExceptionHandler(value={Exception.class})注解指定要被处理的异常有哪些,value是一个类数组可以指定多个异常类型,这里处理了所有的异常。
业务代码使用很简单,直接抛出异常就行。
@RequestMapping(value={"/", "/login"}) public String index() { User user = null; if (user == null) throw new ObjectNotFoundException(); return "login"; }
假如你请求一个不存在的地址:/abc123,这时异常统一处理却没有工作。(前提是没有配置静态资源默认处理servelt,即java配置重写configureDefaultServletHandling方法设置configurer.enable() 或者 xml配置添加<mvc:default-servlet-handler/>,若配置了静态资源处理servlet,在url没有匹配时会被当做静态资源处理,从而导致异常统一处理没有工作。)
为什么呢?看DispatcherServlet源码的doDispatch方法,红色加粗部分
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { HttpServletRequest processedRequest = request; HandlerExecutionChain mappedHandler = null; boolean multipartRequestParsed = false; WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); try { ModelAndView mv = null; Exception dispatchException = null; try { processedRequest = checkMultipart(request); multipartRequestParsed = (processedRequest != request); // Determine handler for the current request. mappedHandler = getHandler(processedRequest); if (mappedHandler == null || mappedHandler.getHandler() == null) { noHandlerFound(processedRequest, response); return; } // Determine handler adapter for the current request. HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); // Process last-modified header, if supported by the handler. String method = request.getMethod(); boolean isGet = "GET".equals(method); if (isGet || "HEAD".equals(method)) { long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); if (logger.isDebugEnabled()) { logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified); } if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) { return; } } if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; } // Actually invoke the handler. mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); if (asyncManager.isConcurrentHandlingStarted()) { return; } applyDefaultViewName(processedRequest, mv); mappedHandler.applyPostHandle(processedRequest, response, mv); } catch (Exception ex) { dispatchException = ex; } catch (Throwable err) { // As of 4.3, we're processing Errors thrown from handler methods as well, // making them available for @ExceptionHandler methods and other scenarios. dispatchException = new NestedServletException("Handler dispatch failed", err); } processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); } catch (Exception ex) { triggerAfterCompletion(processedRequest, response, mappedHandler, ex); } catch (Throwable err) { triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", err)); } finally { if (asyncManager.isConcurrentHandlingStarted()) { // Instead of postHandle and afterCompletion if (mappedHandler != null) { mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response); } } else { // Clean up any resources used by a multipart request. if (multipartRequestParsed) { cleanupMultipart(processedRequest); } } } } protected void noHandlerFound(HttpServletRequest request, HttpServletResponse response) throws Exception { if (pageNotFoundLogger.isWarnEnabled()) { pageNotFoundLogger.warn("No mapping found for HTTP request with URI [" + getRequestUri(request) + "] in DispatcherServlet with name '" + getServletName() + "'"); } if (this.throwExceptionIfNoHandlerFound) { throw new NoHandlerFoundException(request.getMethod(), getRequestUri(request), new ServletServerHttpRequest(request).getHeaders()); } else { response.sendError(HttpServletResponse.SC_NOT_FOUND); } }
noHandlerFound方法的throwExceptionIfNoHandlerFound属性判断为false,所以没有抛出异常,而是直接返回客户端了。
注意!注意!注意。处理Spring MVC抛出的404,500等异常,以及无法匹配到请求地址的异常。
第一步、throwExceptionIfNoHandlerFound赋值为true
我们知道原因是if (this.throwExceptionIfNoHandlerFound)没有进,throwExceptionIfNoHandlerFound属性是false导致的,所以我们把他赋值为true就行。
方式一、重写AbstractDispatcherServletInitializer类的protected void customizeRegistration(ServletRegistration.Dynamic registration)方法,给throwExceptionIfNoHandlerFound赋值true(推荐)
package cn.ycx.web.config; import javax.servlet.ServletRegistration; import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer; public class ServletWebApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { // 将一个或多个路径映射到DispatcherServlet上 @Override protected String[] getServletMappings() { return new String[] {"/"}; } // 返回的带有@Configuration注解的类将会用来配置ContextLoaderListener创建的应用上下文中的bean @Override protected Class<?>[] getRootConfigClasses() { return new Class<?>[] {RootConfig.class}; } @Override protected Class<?>[] getServletConfigClasses() { return new Class<?>[] {ServletConfig.class}; }
@Override protected void customizeRegistration(ServletRegistration.Dynamic registration) { boolean done = registration.setInitParameter("throwExceptionIfNoHandlerFound", "true"); if(!done) throw new RuntimeException(); } }
方式二、重写AbstractDispatcherServletInitializer类的protected void registerDispatcherServlet(ServletContext servletContext)方法,给throwExceptionIfNoHandlerFound赋值true
protected void registerDispatcherServlet(ServletContext servletContext) { String servletName = getServletName(); Assert.hasLength(servletName, "getServletName() must not return empty or null"); WebApplicationContext servletAppContext = createServletApplicationContext(); Assert.notNull(servletAppContext, "createServletApplicationContext() did not return an application " + "context for servlet [" + servletName + "]"); FrameworkServlet dispatcherServlet = createDispatcherServlet(servletAppContext); dispatcherServlet.setContextInitializers(getServletApplicationContextInitializers()); dispatcherServlet.setThrowExceptionIfNoHandlerFound(true); ServletRegistration.Dynamic registration = servletContext.addServlet(servletName, dispatcherServlet); Assert.notNull(registration, "Failed to register servlet with name '" + servletName + "'." + "Check if there is another servlet registered under the same name."); registration.setLoadOnStartup(1); registration.addMapping(getServletMappings()); registration.setAsyncSupported(isAsyncSupported()); Filter[] filters = getServletFilters(); if (!ObjectUtils.isEmpty(filters)) { for (Filter filter : filters) { registerServletFilter(servletContext, filter); } } customizeRegistration(registration); }
方式三、web.xml追加init-param,给throwExceptionIfNoHandlerFound赋值true
<servlet> <servlet-name>dispatcher</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> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:ycxcode-servlet.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet>
第二步、去掉静态资源处理Servlet,若不去掉会被静态资源处理匹配没有的请求。
code-base配置方式,若重载了下面的方法则去掉,(该方法在WebMvcConfigurerAdapter的扩展类中)
/** * 配置静态文件处理 */ @Override public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { configurer.enable(); }
xml配置方式,若追加了下面的配置则去掉,(在springmvc配置文件中)
<!-- 静态资源默认servlet配置 --> <mvc:default-servlet-handler />
以上我们对异常统一处理就完成了。去掉静态资源默认处理后,静态资源处理如下:
去掉静态资源处理servlet后,静态资源的请求也会被当成错误的 请求地址异常 拦截,那怎么办呢?自定义Filter在DispatchServlet之前拦截所有的资源然后直接返回给浏览器。
假设js,css,image都在static目录下放着,定义一个StaticFilter静态资源过滤器,直接返回静态资源。
package cn.ycx.web.filter; import java.io.FileInputStream; import java.io.IOException; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.ServletOutputStream; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; /** * 资源访问 * @author 杨崇兴 2018-07-05 */public class StaticFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpServletRequest = (HttpServletRequest) request; String path = httpServletRequest.getServletPath(); String realPath = httpServletRequest.getServletContext().getRealPath(path); System.out.println(realPath); ServletOutputStream out = response.getOutputStream(); FileInputStream in = new FileInputStream(realPath); byte[] buf = new byte[2048]; int len = -1; while((len = in.read(buf)) != -1) { out.write(buf, 0, len); } in.close(); out.flush(); out.close(); } }
把定义好的StaticFilter添加到Spring MVC上下文中,如下红色代码部分。如何添加自定义Servelt、Filter、Listener请参考另一片博文:https://www.cnblogs.com/yangchongxing/p/9968483.html
package cn.ycx.initializer; import javax.servlet.FilterRegistration; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.ServletRegistration; import org.springframework.web.WebApplicationInitializer; import cn.ycx.filter.MyFilter; import cn.ycx.filter.StaticFilter; import cn.ycx.listener.MyServletRequestAttributeListener; import cn.ycx.listener.MyServletRequestListener; import cn.ycx.servlet.MyServlet; public class MyInitializer implements WebApplicationInitializer { @Override public void onStartup(ServletContext servletContext) throws ServletException { System.out.println(">>>>>>>>>>>> 自定义 onStartup ..."); // 自定义Servlet ServletRegistration.Dynamic myServlet = servletContext.addServlet("myservlet", MyServlet.class); myServlet.addMapping("/myservlet"); // 自定义Filter FilterRegistration.Dynamic staticFilter = servletContext.addFilter("staticfilter", StaticFilter.class); staticFilter.addMappingForUrlPatterns(null, false, "/static/*"); FilterRegistration.Dynamic myFilter = servletContext.addFilter("myfilter", MyFilter.class); myFilter.addMappingForUrlPatterns(null, false, "/*"); // 自定义Listener servletContext.addListener(MyServletRequestListener.class); servletContext.addListener(MyServletRequestAttributeListener.class.getName()); } }
2、在控制器中使用@ExceptionHandler统一处理异常
这种方式可以在每一个控制器中都定义处理方法,也可以写一个BaseController基类,其他控制器继承这个类;
未知请求地址我们也要处理一下,将其跳转到错误页面。这个要利用Spring MVC请求地址的精准匹配,@RequestMapping("*")会匹配剩下没有匹配成功的请求地址,相当于所有请求地址都是有的,只是我们把其他的处理到错误界面了。看代码
package cn.ycx.web.controller; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.log4j.Logger; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.servlet.ModelAndView; /** * 控制器基类 * @author 杨崇兴 2018-07-05 */ public class BaseController { public Logger logger = Logger.getLogger(getClass()); /** * 所有异常处理 * @param e * @return */ @ExceptionHandler(value={Exception.class}) public ModelAndView exceptionHandler(HttpServletRequest request, HttpServletResponse response, Exception ex) { ModelAndView mav = new ModelAndView(); mav.setViewName("error"); return mav; } /** * 未知请求处理 * @return */ @RequestMapping("*") public String notFount() { return "error"; } }
3、使用SimpleMappingExceptionResolver统一处理异常
/** * 异常处理 * @return */ @Bean public SimpleMappingExceptionResolver exceptionResolver() { Properties exceptionMappings = new Properties(); exceptionMappings.put("cn.ycx.web.exception.ObjectNotFoundException", "error"); Properties statusCodes = new Properties(); statusCodes.put("error", "404"); SimpleMappingExceptionResolver exceptionResolver = new SimpleMappingExceptionResolver(); exceptionResolver.setDefaultErrorView("error"); exceptionResolver.setExceptionMappings(exceptionMappings); exceptionResolver.setStatusCodes(statusCodes); return exceptionResolver; }
以上的方式是无法处理Spring MVC抛出的404,500等需要配合下面的处理,看代码
/** * 未知请求处理 * @return */ @RequestMapping("*") public String notFount() { return "error"; }
这个比较简单,就是抛出对应异常时,会转换为对应的状态码。看代码
package cn.ycx.web.exception; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; /** * 对象没有找到异常 * @author 杨崇兴 2018-07-05 */ @ResponseStatus(value = HttpStatus.NOT_FOUND, reason="对象没有找到") public class ObjectNotFoundException extends RuntimeException { private static final long serialVersionUID = 2874051947922252271L; }
业务代码直接抛出异常就行
throw new ObjectNotFoundException();
续写中...