【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("【異常信息】\r\n"); errors.append(ex.getClass().getName()); if (ex.getMessage() != null) { errors.append(": "); errors.append(ex.getMessage()); } for (StackTraceElement stackTraceElement : ex.getStackTrace()) { errors.append("\r\n\tat "); 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();
續寫中...