原创

SpringMVC 绝对路径和相对路径源码分析

温馨提示:
本文最后更新于 2022年10月27日,已超过 964 天没有更新。若文章内的图片失效(无法正常加载),请留言反馈或直接联系我

1. 前言

我们在使用java做web开发,进行页面跳转时,一般都直接使用页面的名称进行匹配。springboot-web已经帮我们自动配置好了mvc相关的配置。

    //测试接口
    @RequestMapping
    @Controller
    public class TestController {
        //此处返回index页面没有携带/ 
        @GetMapping("/hello/kugou")
        public String index(){
            return "index";
        }
    }
    #yml基础配置
    spring:
      mvc:
        view:
          suffix: .html     #配置后缀
    //pom只用了基础的spring-web
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

springboot版本2.x+,使用以上的基础配置,在浏览器进行访问127.0.0.1:8080/hello/kugou时,按照我们的想法应该是正常返回默认静态路径下的index页面。

classpath:/static
classpath:/public
classpath:/resources
classpath:/META-INF/resources

但是结果却是404!!!

Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.

Wed Jun 29 15:38:03 CST 2022
There was an unexpected error (type=Not Found, status=404).

2.分析

  1. 我们都知道MVC的请求流程第一步,前端发起的请求都会被DispatcherServlet拦截,所以将断点打在doDispatch方法中

    ...
                    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);
                }
    ...
    
  2. 调用处理映射器,获取到对应的处理handler mappedHandler.getHandler() ==》 com.wu.springlearn.controller.TestController#index()

  3. 处理适配器ha(HandlerAdapter)进行处理,得到模型视图mv(ModelAndView) ha.handle ==》ModelAndView [view="index"; model={}]

  4. 得到模型视图后进行解析 processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

    由于解析视图后出现问题,所以直接进入解析视图的方法进行查找。

        //1. org.springframework.web.servlet.DispatcherServlet#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) {
                    logger.debug("ModelAndViewDefiningException encountered", exception);
                    mv = ((ModelAndViewDefiningException) exception).getModelAndView();
                }
                else {
                    Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
                    mv = processHandlerException(request, response, handler, exception);
                    errorView = (mv != null);
                }
            }
    
            // Did the handler return a view to render?
            if (mv != null && !mv.wasCleared()) {
                //渲染模型视图
                render(mv, request, response);
                if (errorView) {
                    WebUtils.clearErrorRequestAttributes(request);
                }
            }
            else {
                if (logger.isTraceEnabled()) {
                    logger.trace("No view rendering, null ModelAndView returned.");
                }
            }
    
            if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
                // Concurrent handling started during a forward
                return;
            }
    
            if (mappedHandler != null) {
                // Exception (if any) is already handled..
                mappedHandler.triggerAfterCompletion(request, response, null);
            }
        }
    
        //2. 渲染render(mv, request, response);   org.springframework.web.servlet.DispatcherServlet#render
            protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
            // Determine locale for request and apply it to the response.
            Locale locale =
                    (this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale());
            response.setLocale(locale);
    
            View view;
            //获取当前视图名
            String viewName = mv.getViewName();
            if (viewName != null) {
                // We need to resolve the view name.
                //解析视图 ==》 org.springframework.web.servlet.view.InternalResourceView: name 'index'; URL [index.html]
                view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
                if (view == null) {
                    throw new ServletException("Could not resolve view with name '" + mv.getViewName() +
                            "' in servlet with name '" + getServletName() + "'");
                }
            }
            else {
                // No need to lookup: the ModelAndView object contains the actual View object.
                view = mv.getView();
                if (view == null) {
                    throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " +
                            "View object in servlet with name '" + getServletName() + "'");
                }
            }
    
            // Delegate to the View object for rendering.
            if (logger.isTraceEnabled()) {
                logger.trace("Rendering view [" + view + "] ");
            }
            try {
                if (mv.getStatus() != null) {
                    response.setStatus(mv.getStatus().value());
                }
                //视图渲染
                view.render(mv.getModelInternal(), request, response);
            }
            catch (Exception ex) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Error rendering view [" + view + "]", ex);
                }
                throw ex;
            }
        }
        //3. 视图渲染   org.springframework.web.servlet.view.AbstractView#render
        @Override 
        public void render(@Nullable Map<String, ?> model, HttpServletRequest request,
                HttpServletResponse response) throws Exception {
    
            if (logger.isDebugEnabled()) {
                logger.debug("View " + formatViewName() +
                        ", model " + (model != null ? model : Collections.emptyMap()) +
                        (this.staticAttributes.isEmpty() ? "" : ", static attributes " + this.staticAttributes));
            }
            //创建输出模型 
            Map<String, Object> mergedModel = createMergedOutputModel(model, request, response);
            //预处理返回结果
            prepareResponse(request, response);
            //渲染合并输出模型 =》由于前两步都没问题,所以直接进入该方法继续
            renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);
        }
        //4. 渲染合并输出模型renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);
        //org.springframework.web.servlet.view.InternalResourceView#renderMergedOutputModel
        protected void renderMergedOutputModel(
                Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
    
            // Expose the model object as request attributes.
            //将model里的值放到request中,这也是为什么我们在页面能直接使用model里面的参数的原因
            exposeModelAsRequestAttributes(model, request);
    
            // Expose helpers as request attributes, if any.
            exposeHelpers(request);
    
            // Determine the path for the request dispatcher.
            //确认请求调度程序的路径
            String dispatcherPath = prepareForRendering(request, response);
            // Obtain a RequestDispatcher for the target resource (typically a JSP).
            //获取当前请求的调度器
            //不带/  ==>requestURI=/hello/index.html   带/  ==>requestURI=/index.html  
            RequestDispatcher rd = getRequestDispatcher(request, dispatcherPath);
            ......
        }
        //5. 获取调度器
        @Override
        public RequestDispatcher getRequestDispatcher(String path) {
    
            Context context = getContext();
            if (context == null) {
                return null;
            }
    
            if (path == null) {
                return null;
            }
    
            int fragmentPos = path.indexOf('#');
            if (fragmentPos > -1) {
                log.warn(sm.getString("request.fragmentInDispatchPath", path));
                path = path.substring(0, fragmentPos);
            }
    
            // If the path is already context-relative, just pass it through
            //如果path是以/开头,则返回由当前path生成的调度器
            if (path.startsWith("/")) {
                return context.getServletContext().getRequestDispatcher(path);
            }
            /* 绝对路径和相对路径的解释
             * From the Servlet 4.0 Javadoc:
             * - The pathname specified may be relative, although it cannot extend
             *   outside the current servlet context.
             * - If it is relative, it must be relative against the current servlet
             *
             * From Section 9.1 of the spec:
             * - The servlet container uses information in the request object to
             *   transform the given relative path against the current servlet to a
             *   complete path.
             *
             * It is undefined whether the requestURI is used or whether servletPath
             * and pathInfo are used. Given that the RequestURI includes the
             * contextPath (and extracting that is messy) , using the servletPath and
             * pathInfo looks to be the more reasonable choice.
             */
            // Convert a request-relative path to a context-relative one
            //将请求相对路径转换为上下文相对路径
            String servletPath = (String) getAttribute(
                    RequestDispatcher.INCLUDE_SERVLET_PATH);
            if (servletPath == null) {
                servletPath = getServletPath();
            }
    
            // Add the path info, if there is any
            String pathInfo = getPathInfo();
            String requestPath = null;
    
            if (pathInfo == null) {
                requestPath = servletPath;
            } else {
                requestPath = servletPath + pathInfo;
            }
    
            int pos = requestPath.lastIndexOf('/');
            String relative = null;
            if (context.getDispatchersUseEncodedPaths()) {
                if (pos >= 0) {
                    relative = URLEncoder.DEFAULT.encode(
                            requestPath.substring(0, pos + 1), StandardCharsets.UTF_8) + path;
                } else {
                    relative = URLEncoder.DEFAULT.encode(requestPath, StandardCharsets.UTF_8) + path;
                }
            } else {
                if (pos >= 0) {
                    relative = requestPath.substring(0, pos + 1) + path;
                } else {
                    relative = requestPath + path;
                }
            }
            //返回相对路径生成的调度器
            return context.getServletContext().getRequestDispatcher(relative);
        }
    

3. 总结

当我们的视图(index)返回带/ 即 “/index”,调度器返回的是绝对路径。

当我们的视图(index)返回不带/ 即 “index”,调度器返回的是相对路径,会将我们的请求的URI进行拼接重组。

正文到此结束