【Java Web開發學習】Spring MVC異常統一處理


【Java Web開發學習】Spring MVC異常統一處理

文采有限,若有錯誤,歡迎留言指正。

轉載:https://www.cnblogs.com/yangchongxing/p/9271900.html

目錄

1、使用@ControllerAdvice和@ExceptionHandler注解統一處理異常

2、在控制器中使用@ExceptionHandler統一處理異常

3、使用SimpleMappingExceptionResolver統一處理異常 

4、將異常映射為HTTP狀態碼

正文

異常處理是每一個系統必須面對的,對於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";
}

4、將異常映射為HTTP狀態碼

這個比較簡單,就是拋出對應異常時,會轉換為對應的狀態碼。看代碼

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();

 

續寫中...


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM