zoukankan      html  css  js  c++  java
  • 【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("【异常信息】
    ");
            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";
    }

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

    续写中...

  • 相关阅读:
    Google Kubernetes设计文档之服务篇-转
    基于kubernetes构建Docker集群管理详解-转
    Pass云Docker介绍
    Spring <context:annotation-config/> 解说
    webapp开发需要注意的浏览器内核知识
    koala编译scss文件时不支持中文字体的解决方案
    CSS3硬件加速需要注意的事项
    ios客户端快速滚动和回弹效果的实现
    mui禁止滚动条和禁止滚动
    苹果端禁用左右滑动屏幕返回上级页面
  • 原文地址:https://www.cnblogs.com/yangchongxing/p/9271900.html
Copyright © 2011-2022 走看看