SpringMVC请求处理流程源码解析(第3篇):视图渲染与异常处理

SpringMVC请求处理流程源码解析(第3篇):视图渲染与异常处理

本文深入剖析视图解析与渲染、异常处理机制、国际化支持、FlashMap重定向传递和文件上传处理的完整源码流程。

目录

  1. 视图解析与渲染
  2. 异常处理与结果分发
  3. Locale与主题处理
  4. FlashMap与重定向传递
  5. 文件上传与Multipart处理

1. 视图解析与渲染

1.1 ViewResolver体系

<<interface>>
ViewResolver
+resolveViewName(viewName, locale)
<<abstract>>
AbstractCachingViewResolver
-Map<String~ View> viewCache
<<abstract>>
UrlBasedViewResolver
-String prefix
-String suffix
InternalResourceViewResolver
ThymeleafViewResolver
FreeMarkerViewResolver
ContentNegotiatingViewResolver

1.2 ViewResolver接口定义

java 复制代码
// org.springframework.web.servlet.ViewResolver
public interface ViewResolver {
    
    /**
     * 解析视图名
     * @param viewName 视图逻辑名
     * @param locale Locale信息
     * @return View实例,或null(交给下一个Resolver处理)
     */
    @Nullable
    View resolveViewName(String viewName, Locale locale) throws Exception;
}

1.3 AbstractCachingViewResolver缓存机制

java 复制代码
// org.springframework.web.servlet.view.AbstractCachingViewResolver
public abstract class AbstractCachingViewResolver extends WebApplicationObjectSupport
        implements ViewResolver {
    
    // 视图缓存Map
    private final Map<String, View> viewCache = new ConcurrentHashMap<>(64);
    
    private static final int DEFAULT_CACHE_LIMIT = 1024;
    private int cacheLimit = DEFAULT_CACHE_LIMIT;
    private boolean cacheUnresolved = true;
    
    @Override
    @Nullable
    public View resolveViewName(String viewName, Locale locale) throws Exception {
        // 1. 检查缓存
        String viewCacheKey = createCacheKey(viewName, locale);
        View view = this.viewCache.get(viewCacheKey);
        
        if (view == null) {
            synchronized (this.viewCache) {
                view = this.viewCache.get(viewCacheKey);
                if (view == null) {
                    // 2. 子类实现:创建View
                    view = createView(viewName, locale);
                    
                    // 3. 缓存null结果,避免重复查找
                    if (view == null && this.cacheUnresolved) {
                        this.viewCache.put(viewCacheKey, null);
                    } else if (view != null) {
                        view = getCacheKey(viewName, locale);
                    }
                }
            }
        }
        
        return (view != null ? view : null);
    }
    
    protected String createCacheKey(String viewName, Locale locale) {
        return viewName + '_' + locale.toString();
    }
}

1.4 UrlBasedViewResolver视图创建

java 复制代码
// org.springframework.web.servlet.view.UrlBasedViewResolver
public class UrlBasedViewResolver extends AbstractCachingViewResolver 
        implements Ordered {
    
    private String prefix = "";
    private String suffix = "";
    private String contentType = "text/html;charset=ISO-8859-1";
    
    @Override
    protected View createView(String viewName, Locale locale) throws Exception {
        // 1. 检查视图是否可解析
        if (!canHandle(viewName, locale)) {
            return null;
        }
        
        // 2. 处理重定向视图
        if (viewName.startsWith("redirect:")) {
            String redirectUrl = viewName.substring("redirect:".length());
            return new RedirectView(redirectUrl, isRedirectContextRelative(),
                isRedirectHttp10Compatible());
        }
        
        // 3. 处理转发视图
        if (viewName.startsWith("forward:")) {
            String forwardUrl = viewName.substring("forward:".length());
            return new InternalResourceView(forwardUrl);
        }
        
        // 4. 创建普通视图
        return buildView(viewName);
    }
    
    protected AbstractUrlBasedView buildView(String viewName) throws Exception {
        // 1. 拼接前缀和后缀
        String url = this.prefix + viewName + this.suffix;
        
        // 2. 创建视图实例
        AbstractUrlBasedView view = instantiateView();
        if (view == null) {
            view = (getViewClass() != null ? 
                getViewClass() : InternalResourceView.class)
                .getDeclaredConstructor().newInstance();
        }
        
        view.setUrl(url);
        view.setContentType(this.contentType);
        view.setRequestContextAttribute(this.requestContextAttribute);
        
        return view;
    }
}

1.5 配置示例

java 复制代码
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        // JSP视图解析器
        registry.jsp("/WEB-INF/views/", ".jsp");
        
        // Thymeleaf配置
        registry.beanName();
    }
}

// 等价于XML配置
// <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
//     <property name="prefix" value="/WEB-INF/views/"/>
//     <property name="suffix" value=".jsp"/>
// </bean>

1.6 视图解析流程

View ViewResolver DispatcherServlet View ViewResolver DispatcherServlet alt [InternalResourceView- ] [ThymeleafView] [FreeMarkerView] resolveViewName(viewName, locale) View render(model, request, response) RequestDispatcher.forward() templateEngine.process() freemarker.render()

1.7 视图渲染决策







ModelAndView
viewName存在?
mv.getView直接返回
遍历viewResolvers
vr.resolveViewName方法
返回View?
选择该View
遍历结束
抛出异常
View存在?
渲染视图

1.8 View接口与渲染

java 复制代码
// org.springframework.web.servlet.View
public interface View {
    
    /**
     * 获取内容类型
     */
    String getContentType();
    
    /**
     * 渲染视图
     */
    void render(@Nullable Map<String, ?> model, 
                HttpServletRequest request,
                HttpServletResponse response) throws Exception;
}

1.9 InternalResourceView渲染源码

java 复制代码
// org.springframework.web.servlet.view.InternalResourceView
public class InternalResourceView extends AbstractUrlBasedView {
    
    @Override
    protected void renderMergedOutputModel(Map<String, Object> model,
                                          HttpServletRequest request,
                                          HttpServletResponse response) {
        // 1. 暴露模型到请求属性
        exposeModelAsRequestAttributes(model, request);
        
        // 2. 获取转发路径
        String dispatcherPath = prepareForRendering(request, response);
        
        // 3. 获取RequestDispatcher
        RequestDispatcher rd = getRequestDispatcher(request, dispatcherPath);
        if (rd == null) {
            throw new ServletException(
                "Could not get RequestDispatcher for [" + getUrl() + "]");
        }
        
        // 4. 处理include请求
        if (request.getAttribute(WebUtils.INCLUDE_REQUEST_URI_ATTRIBUTE) != null) {
            rd.include(request, response);
        } else {
            // 5. 执行转发
            rd.forward(request, response);
        }
    }
}

2. 异常处理与结果分发

2.1 processDispatchResult源码

java 复制代码
// DispatcherServlet.processDispatchResult()
private void processDispatchResult(HttpServletRequest request,
                                   HttpServletResponse response,
                                   @Nullable HandlerExecutionChain mappedHandler,
                                   @Nullable ModelAndView mv,
                                   @Nullable Exception exception) {
    
    boolean errorView = false;
    
    // 1. 如果有异常,进行异常处理
    if (exception != null) {
        if (exception instanceof ModelAndViewDefiningException mvdException) {
            mv = mvdException.getModelAndView();
        } else {
            Object handler = (mappedHandler != null ? 
                mappedHandler.getHandler() : null);
            
            ModelAndView exMv = processHandlerException(request, response, 
                handler, exception);
            
            if (exMv != null) {
                mv = exMv;
                errorView = true;
            }
        }
    }
    
    // 2. 如果有ModelAndView,进行视图渲染
    if (mv != null && !mv.wasCleared()) {
        render(mv, request, response);
    }
}

2.2 processHandlerException源码

java 复制代码
@Nullable
protected ModelAndView processHandlerException(HttpServletRequest request,
                                               HttpServletResponse response,
                                               @Nullable Object handler,
                                               Exception ex) throws Exception {
    
    request.setAttribute(ERROR_EXCEPTION_ATTRIBUTE, ex);
    
    ModelAndView exMv = null;
    if (this.handlerExceptionResolvers != null) {
        for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
            exMv = resolver.resolveException(request, response, handler, ex);
            if (exMv != null) {
                break;
            }
        }
    }
    
    if (exMv == null) {
        throw ex;
    }
    
    if (!exMv.hasView()) {
        exMv.setViewName(getDefaultViewName(request));
    }
    
    if (mappedHandler != null) {
        mappedHandler.triggerAfterCompletion(request, response, null);
    }
    
    return exMv;
}

2.3 HandlerExceptionResolver体系

<<interface>>
HandlerExceptionResolver
+resolveException()
<<abstract>>
AbstractHandlerExceptionResolver
+resolveException()
HandlerExceptionResolverComposite
ExceptionHandlerExceptionResolver
DefaultHandlerExceptionResolver
SimpleMappingExceptionResolver

2.4 ExceptionHandlerExceptionResolver

处理@ExceptionHandler注解的方法:

java 复制代码
// org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver
public class ExceptionHandlerExceptionResolver extends 
        AbstractHandlerExceptionResolver {
    
    @Override
    protected ModelAndView doResolveException(HttpServletRequest request,
                                             HttpServletResponse response,
                                             @Nullable Object handler,
                                             Exception ex) {
        // 1. 查找ExceptionHandler方法
        ServletInvocableHandlerMethod exceptionHandlerMethod = 
            getExceptionHandlerMethod(handler, ex);
        
        if (exceptionHandlerMethod == null) {
            return null;
        }
        
        // 2. 创建参数解析器
        exceptionHandlerMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
        exceptionHandlerMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
        
        // 3. 创建ModelAndViewContainer
        ModelAndViewContainer mavContainer = new ModelAndViewContainer();
        mavContainer.addAllAttributes(
            RequestContextUtils.getMergedOutputAttributes(request));
        
        // 4. 执行异常处理方法
        try {
            exceptionHandlerMethod.invokeAndHandle(mavContainer, request, response);
        } catch (Exception e) {
            return null;
        }
        
        // 5. 获取ModelAndView
        if (mavContainer.isRequestHandled()) {
            return new ModelAndView();
        } else {
            ModelAndView mav = new ModelAndView();
            mav.setViewName(mavContainer.getViewName());
            mav.addAllObjects(mavContainer.getModel());
            return mav;
        }
    }
}

2.5 DefaultHandlerExceptionResolver

处理Spring MVC标准异常:

java 复制代码
// org.springframework.web.servlet.mvc.method.annotation.ResponseStatusExceptionResolver
public class DefaultHandlerExceptionResolver {
    
    private static final Map<Class<? extends Throwable>, HttpStatus> EXCEPTION_STATUS;
    
    static {
        EXCEPTION_STATUS = Map.of(
            NoSuchRequestHandlingMethodException.class, HttpStatus.NOT_FOUND,
            HttpRequestMethodNotSupportedException.class, HttpStatus.METHOD_NOT_ALLOWED,
            HttpMediaTypeNotSupportedException.class, HttpStatus.UNSUPPORTED_MEDIA_TYPE,
            HttpMediaTypeNotAcceptableException.class, HttpStatus.NOT_ACCEPTABLE,
            MissingPathVariableException.class, HttpStatus.BAD_REQUEST,
            MissingServletRequestParameterException.class, HttpStatus.BAD_REQUEST,
            ServletRequestBindingException.class, HttpStatus.BAD_REQUEST,
            TypeMismatchException.class, HttpStatus.BAD_REQUEST,
            HttpMessageNotReadableException.class, HttpStatus.BAD_REQUEST,
            HttpMessageNotWritableException.class, HttpStatus.INTERNAL_SERVER_ERROR,
            MethodArgumentNotValidException.class, HttpStatus.BAD_REQUEST,
            NoHandlerFoundException.class, HttpStatus.NOT_FOUND
        );
    }
    
    @Override
    public ModelAndView resolveException(HttpServletRequest request,
                                         HttpServletResponse response,
                                         Object handler,
                                         Exception ex) {
        HttpStatus status = EXCEPTION_STATUS.get(ex.getClass());
        
        if (status != null) {
            try {
                response.sendError(status.value(), ex.getMessage());
            } catch (IOException e) {
                // ...
            }
        }
        
        return new ModelAndView();
    }
}

2.6 异常处理决策

ModelAndViewDefiningException
其他异常


发生异常
异常类型
直接获取mv
遍历HandlerExceptionResolver
找到匹配的Resolver?
返回error视图
抛出ServletException
视图渲染

2.7 全局异常处理示例

java 复制代码
@RestControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(ValidationException.class)
    public Result<Void> handleValidation(ValidationException ex) {
        return Result.error(400, ex.getMessage());
    }
    
    @ExceptionHandler(ResourceNotFoundException.class)
    public Result<Void> handleNotFound(ResourceNotFoundException ex) {
        return Result.error(404, ex.getMessage());
    }
    
    @ExceptionHandler(Exception.class)
    public Result<Void> handleGeneral(Exception ex) {
        log.error("Unexpected error", ex);
        return Result.error(500, "Internal server error");
    }
}

3. Locale与主题处理

3.1 LocaleResolver体系

<<interface>>
LocaleResolver
+resolveLocale(request)
+setLocale(request, response, locale)
<<abstract>>
AbstractLocaleContextResolver
+resolveLocaleContext(request)
+setLocaleContext()
AcceptHeaderLocaleResolver
FixedLocaleResolver
SessionLocaleResolver
CookieLocaleResolver

3.2 AcceptHeaderLocaleResolver源码

从HTTP Accept-Language头解析Locale:

java 复制代码
// org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver
public class AcceptHeaderLocaleResolver implements LocaleResolver {
    
    @Override
    public Locale resolveLocale(HttpServletRequest request) {
        return request.getLocale();
    }
    
    @Override
    public void setLocale(HttpServletRequest request,
                         HttpServletResponse response,
                         Locale locale) {
        throw new UnsupportedOperationException(
            "Cannot change HTTP accept header");
    }
}

3.3 SessionLocaleResolver源码

从Session中获取/保存Locale:

java 复制代码
// org.springframework.web.servlet.i18n.SessionLocaleResolver
public class SessionLocaleResolver extends AbstractLocaleContextResolver {
    
    public static final String LOCALE_SESSION_ATTRIBUTE_NAME = 
        SessionLocaleResolver.class.getName() + ".LOCALE";
    
    @Override
    public Locale resolveLocale(HttpServletRequest request) {
        Locale locale = (Locale) request.getAttribute(LOCALE_SESSION_ATTRIBUTE_NAME);
        
        if (locale == null) {
            HttpSession session = request.getSession(false);
            if (session != null) {
                locale = (Locale) session.getAttribute(LOCALE_SESSION_ATTRIBUTE_NAME);
            }
        }
        
        return (locale != null ? locale : determineDefaultLocale(request));
    }
    
    @Override
    public void setLocale(HttpServletRequest request,
                         HttpServletResponse response,
                         Locale locale) {
        HttpSession session = request.getSession(false);
        if (session != null) {
            session.setAttribute(LOCALE_SESSION_ATTRIBUTE_NAME, locale);
        }
        request.setAttribute(LOCALE_SESSION_ATTRIBUTE_NAME, locale);
    }
}

3.4 CookieLocaleResolver源码

将Locale存储在Cookie中:

java 复制代码
// org.springframework.web.servlet.i18n.CookieLocaleResolver
public class CookieLocaleResolver extends AbstractLocaleContextResolver {
    
    public static final String DEFAULT_COOKIE_NAME = 
        CookieLocaleResolver.class.getName() + ".LOCALE";
    
    private String cookieName = DEFAULT_COOKIE_NAME;
    private String cookiePath = "/";
    private int cookieMaxAge = -1;
    
    @Override
    public Locale resolveLocale(HttpServletRequest request) {
        // 1. 从请求属性获取(优先级最高)
        Locale locale = (Locale) request.getAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME);
        if (locale != null) {
            return locale;
        }
        
        // 2. 从Cookie获取
        Cookie cookie = WebUtils.getCookie(request, cookieName);
        if (cookie != null) {
            String value = cookie.getValue();
            locale = StringUtils.parseLocaleString(value.replace('-', '_'));
            if (locale != null) {
                return locale;
            }
        }
        
        // 3. 使用默认Locale
        return determineDefaultLocale(request);
    }
    
    @Override
    public void setLocale(HttpServletRequest request,
                         HttpServletResponse response,
                         Locale locale) {
        String cookieValue = locale != null ? locale.toString().replace('_', '-') : "";
        
        Cookie cookie = new Cookie(cookieName, cookieValue);
        cookie.setPath(cookiePath);
        
        if (locale == null) {
            cookie.setMaxAge(0); // 删除Cookie
        } else {
            cookie.setMaxAge(cookieMaxAge);
        }
        
        response.addCookie(cookie);
        request.setAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME, locale);
    }
}

3.5 LocaleContextHolder与LocaleContext

LocaleContextHolder使用ThreadLocal保存Locale上下文:

java 复制代码
// org.springframework.context.i18n.LocaleContextHolder
public class LocaleContextHolder {
    
    private static final ThreadLocal<LocaleContext> localeContextHolder =
        new ThreadLocal<>();
    
    /**
     * 获取当前Locale
     */
    public static Locale getLocale() {
        LocaleContext localeContext = localeContextHolder.get();
        return (localeContext != null ? 
            localeContext.getLocale() : Locale.getDefault());
    }
    
    /**
     * 设置当前Locale
     */
    public static void setLocale(Locale locale) {
        setLocaleContext(new SimpleLocaleContext(locale));
    }
    
    /**
     * 设置LocaleContext
     */
    public static void setLocaleContext(@Nullable LocaleContext localeContext) {
        if (localeContext == null) {
            localeContextHolder.remove();
        } else {
            localeContextHolder.set(localeContext);
        }
    }
    
    @Nullable
    public static LocaleContext getLocaleContext() {
        return localeContextHolder.get();
    }
    
    public static void resetLocaleContext() {
        localeContextHolder.remove();
    }
}

// LocaleContext接口
public interface LocaleContext {
    @Nullable
    Locale getLocale();
}

// 简单实现
public class SimpleLocaleContext implements LocaleContext {
    private final Locale locale;
    
    public SimpleLocaleContext(@Nullable Locale locale) {
        this.locale = locale;
    }
    
    @Override
    @Nullable
    public Locale getLocale() {
        return locale;
    }
}

3.6 ThemeResolver主题解析

主题Resolver用于国际化主题资源(如CSS、JS等):

java 复制代码
// org.springframework.web.servlet.ThemeResolver
public interface ThemeResolver {
    
    /**
     * 解析当前请求的主题
     */
    String resolveThemeName(HttpServletRequest request);
    
    /**
     * 设置主题名称
     */
    void applyThemeName(HttpServletRequest request, 
                       HttpServletResponse response,
                       @Nullable String themeName);
}

// org.springframework.web.servlet.theme.FixedThemeResolver
public class FixedThemeResolver implements ThemeResolver {
    
    private String defaultThemeName = "theme-default";
    
    @Override
    public String resolveThemeName(HttpServletRequest request) {
        return this.defaultThemeName;
    }
    
    @Override
    public void applyThemeName(HttpServletRequest request,
                              HttpServletResponse response,
                              String themeName) {
        throw new UnsupportedOperationException(
            "Cannot change theme for FixedThemeResolver");
    }
}

// org.springframework.web.servlet.theme.SessionThemeResolver
public class SessionThemeResolver extends AbstractThemeResolver {
    
    public static final String THEME_SESSION_ATTRIBUTE = 
        SessionThemeResolver.class.getName() + ".THEME";
    
    @Override
    public String resolveThemeName(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if (session == null) {
            return getDefaultThemeName();
        }
        
        String theme = (String) session.getAttribute(THEME_SESSION_ATTRIBUTE);
        return (theme != null ? theme : getDefaultThemeName());
    }
    
    @Override
    public void applyThemeName(HttpServletRequest request,
                              HttpServletResponse response,
                              String themeName) {
        HttpSession session = request.getSession(false);
        if (session != null) {
            session.setAttribute(THEME_SESSION_ATTRIBUTE, themeName);
        }
    }
}

3.7 ThemeSource与Theme主题加载

java 复制代码
// org.springframework.ui.context.ThemeSource
public interface ThemeSource {
    
    /**
     * 获取主题
     */
    @Nullable
    Theme getTheme(String themeName);
}

// org.springframework.web.servlet.context.ThemeSource
public class ResourceBundleThemeSource implements ThemeSource {
    
    private String basenamePrefix = "theme.";
    
    @Override
    @Nullable
    public Theme getTheme(String themeName) {
        // 从资源包加载主题
        String basename = this.basenamePrefix + themeName;
        ResourceBundle bundle = ResourceBundle.getBundle(basename, 
            LocaleContextHolder.getLocale());
        
        return new SimpleTheme(themeName, bundle);
    }
}

// org.springframework.ui.Theme
public interface Theme {
    
    /**
     * 获取主题名称
     */
    String getName();
    
    /**
     * 获取主题资源
     */
    ResourceBundle getBundle();
}

// org.springframework.web.servlet.ThemeResolver在DispatcherServlet中的使用
// DispatcherServlet.doService()中:
request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());

3.8 Locale与主题完整处理流程

Controller ThemeSource ThemeResolver LocaleContextHolder LocaleResolver DispatcherServlet HTTP Client Controller ThemeSource ThemeResolver LocaleContextHolder LocaleResolver DispatcherServlet HTTP Client Locale解析 Theme解析 HTTP Request with Accept-Language resolveLocale(request) Locale(zh_CN) setLocaleContext(locale) request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, locale) resolveThemeName(request) theme-default getTheme(theme-default) Theme request.setAttribute(THEME_SOURCE_ATTRIBUTE, theme) 使用Locale/Theme

3.9 切换Locale示例

使用LocaleResolver在运行时切换语言:

java 复制代码
@Controller
@RequestMapping("/i18n")
public class I18nController {
    
    private final LocaleResolver localeResolver;
    
    public I18nController(LocaleResolver localeResolver) {
        this.localeResolver = localeResolver;
    }
    
    // 方式1:使用RedirectAttributes
    @GetMapping("/change")
    public String changeLocale(
            @RequestParam("lang") String lang,
            RedirectAttributes redirectAttributes) {
        
        Locale locale = Locale.forLanguageTag(lang);
        redirectAttributes.addFlashAttribute("lang", locale);
        return "redirect:/";
    }
    
    // 方式2:直接在请求中设置Locale
    @PostMapping("/set-locale")
    public void setLocale(HttpServletRequest request,
                         HttpServletResponse response,
                         @RequestParam("locale") String locale) {
        Locale newLocale = Locale.forLanguageTag(locale);
        localeResolver.setLocale(request, response, newLocale);
    }
}

// 前端页面使用JSTL fmt标签
// <fmt:setLocale value="${pageContext.response.locale}"/>
// <fmt:setBundle basename="messages"/>

4. FlashMap与重定向传递

4.1 FlashMapManager接口

java 复制代码
// org.springframework.web.servlet.FlashMapManager
public interface FlashMapManager {
    
    /**
     * 恢复输入FlashMap
     */
    @Nullable
    FlashMap retrieveAndUpdate(HttpServletRequest request);
    
    /**
     * 保存输出FlashMap
     */
    void saveOutputFlashMap(String location,
                           HttpServletRequest request,
                           HttpServletResponse response);
}

4.2 SessionFlashMapManager实现

java 复制代码
// org.springframework.web.servlet.support.SessionFlashMapManager
public class SessionFlashMapManager extends AbstractFlashMapManager {
    
    private static final String FLASH_MAPS_SESSION_ATTRIBUTE = 
        SessionFlashMapManager.class.getName() + ".FLASH_MAPS";
    
    @Override
    @Nullable
    public FlashMap retrieveAndUpdate(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if (session == null) {
            return null;
        }
        
        List<FlashMap> flashMaps = 
            (List<FlashMap>) session.getAttribute(FLASH_MAPS_SESSION_ATTRIBUTE);
        
        if (CollectionUtils.isEmpty(flashMaps)) {
            return null;
        }
        
        List<FlashMap> matchingMaps = new ArrayList<>();
        Iterator<FlashMap> iterator = flashMaps.iterator();
        
        while (iterator.hasNext()) {
            FlashMap flashMap = iterator.next();
            
            if (flashMap.isExpired()) {
                iterator.remove();
                continue;
            }
            
            if (flashMap.getTargetRequestPath() != null) {
                String path = getUrlPathHelper().getPathWithinApplication(request);
                if (!flashMap.getTargetRequestPath().equals(path)) {
                    continue;
                }
            }
            
            matchingMaps.add(flashMap);
            iterator.remove();
        }
        
        if (!matchingMaps.isEmpty()) {
            request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, matchingMaps.get(0));
            request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this);
        }
        
        return (!matchingMaps.isEmpty() ? matchingMaps.get(0) : null);
    }
    
    @Override
    protected void saveFlashMap(FlashMap flashMap,
                              HttpServletRequest request,
                              HttpServletResponse response) {
        HttpSession session = request.getSession(false);
        
        @SuppressWarnings("unchecked")
        List<FlashMap> flashMaps = (List<FlashMap>) 
            session.getAttribute(FLASH_MAPS_SESSION_ATTRIBUTE);
        
        if (flashMaps == null) {
            flashMaps = new ArrayList<>();
            session.setAttribute(FLASH_MAPS_SESSION_ATTRIBUTE, flashMaps);
        }
        
        flashMaps.add(flashMap);
    }
}

4.3 FlashMap工作流程

目标Controller HttpSession FlashMapManager Controller 目标Controller HttpSession FlashMapManager Controller 1. 重定向 2. 目标页面 model可访问Flash属性 return redirect:/users saveOutputFlashMap() 保存FlashMap retrieveAndUpdate(request) 获取并删除FlashMap FlashMap

4.4 使用示例

java 复制代码
@Controller
@RequestMapping("/users")
public class UserController {
    
    private final UserService userService;
    
    public UserController(UserService userService) {
        this.userService = userService;
    }
    
    @PostMapping("/create")
    public String createUser(User user, RedirectAttributes attributes) {
        User savedUser = userService.save(user);
        
        // Flash属性:重定向后可在Model中访问
        attributes.addFlashAttribute("message", "创建成功");
        attributes.addFlashAttribute("userId", savedUser.getId());
        
        return "redirect:/users/" + savedUser.getId();
    }
    
    @GetMapping("/{id}")
    public String showUser(@PathVariable Long id,
                         @ModelAttribute("message") String message,
                         @ModelAttribute("userId") Long userId,
                         Model model) {
        if (message != null) {
            model.addAttribute("alert", message);
        }
        model.addAttribute("user", userService.findById(id));
        return "user/detail";
    }
}

5. 文件上传与Multipart处理

5.1 MultipartResolver接口

java 复制代码
// org.springframework.web.multipart.MultipartResolver
public interface MultipartResolver {
    
    boolean isMultipart(HttpServletRequest request);
    
    MultipartHttpServletRequest resolveMultipart(HttpServletRequest request)
            throws MultipartException;
    
    void cleanupMultipart(MultipartHttpServletRequest request);
}

5.2 CommonsMultipartResolver实现

java 复制代码
// org.springframework.web.multipart.commons.CommonsMultipartResolver
public class CommonsMultipartResolver extends CommonsFileUploadSupport
        implements MultipartResolver, ServletContextAware {
    
    private boolean resolveLazily = false;
    
    @Override
    public boolean isMultipart(HttpServletRequest request) {
        return StringUtils.startsWithIgnoreCase(
            request.getContentType(), "multipart/");
    }
    
    @Override
    public MultipartHttpServletRequest resolveMultipart(
            HttpServletRequest request) throws MultipartException {
        
        if (this.resolveLazily) {
            return new LazyMultipartHttpServletRequest(request);
        }
        
        return parseRequest(request);
    }
    
    protected MultipartHttpServletRequest parseRequest(HttpServletRequest request) {
        String encoding = determineEncoding(request);
        FileUpload fileUpload = prepareFileUpload(encoding);
        
        List<FileItem> fileItems = ((ServletFileUpload) fileUpload)
            .parseRequest(request);
        
        return new CommonsMultipartRequest(request, fileItems);
    }
}

5.3 StandardServletMultipartResolver实现

java 复制代码
// org.springframework.web.multipart.support.StandardServletMultipartResolver
public class StandardServletMultipartResolver implements MultipartResolver {
    
    @Override
    public boolean isMultipart(HttpServletRequest request) {
        return StringUtils.startsWithIgnoreCase(
            request.getContentType(), "multipart/");
    }
    
    @Override
    public MultipartHttpServletRequest resolveMultipart(
            HttpServletRequest request) throws MultipartException {
        try {
            Collection<Part> parts = request.getParts();
            
            MultiValueMap<String, MultipartFile> files = 
                new LinkedMultiValueMap<>();
            
            for (Part part : parts) {
                String filename = extractFilename(part.getHeader("Content-Disposition"));
                if (filename != null) {
                    files.add(part.getName(), 
                        new StandardMultipartFile(part, filename));
                }
            }
            
            return new StandardMultipartHttpServletRequest(request, files);
        } catch (Exception ex) {
            throw new MultipartException("Failed to parse multipart", ex);
        }
    }
}

5.4 Multipart处理流程



HTTP Request
isMultipart?
继续处理
checkMultipart
resolveMultipart
MultipartHttpServletRequest
参数解析
MultipartFile参数
Controller方法
finally清理
cleanupMultipart

5.5 文件上传配置

java 复制代码
@Configuration
public class MultipartConfig {
    @Bean
    public MultipartResolver multipartResolver() {
        CommonsMultipartResolver resolver = new CommonsMultipartResolver();
        resolver.setMaxUploadSize(5242880); // 5MB
        resolver.setMaxInMemorySize(1048576); // 1MB
        resolver.setDefaultEncoding("UTF-8");
        return resolver;
    }
}

// Controller使用
@PostMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file) {
    if (file.isEmpty()) {
        return "redirect:/upload";
    }
    
    String filename = file.getOriginalFilename();
    Path path = Paths.get("/uploads/", filename);
    Files.write(path, file.getBytes());
    
    return "redirect:/uploads/" + filename;
}

总结

视图渲染关键点

组件 说明
ViewResolver 解析视图名,返回View对象
View 渲染视图内容到HTTP响应
AbstractCachingViewResolver 提供视图缓存机制
UrlBasedViewResolver 支持redirect:/、forward:前缀

异常处理关键点

组件 说明
@ExceptionHandler 方法级异常处理
@ControllerAdvice 全局异常处理
HandlerExceptionResolver 异常解析链
DefaultHandlerExceptionResolver 处理标准Spring异常

FlashMap使用场景

  • 重定向时传递一次性数据
  • POST-REDIRECT-GET模式
  • 防止表单重复提交

文件上传关键点

配置项 说明
maxUploadSize 最大上传文件大小
maxInMemorySize 内存缓冲区大小
defaultEncoding 默认编码

完整请求处理流程回顾

HTTP Client
DispatcherServlet
HandlerMapping查找
HandlerExecutionChain
applyPreHandle
HandlerAdapter.handle
Controller调用
applyPostHandle
HandlerExceptionResolver
ViewResolver解析
View.render
triggerAfterCompletion
HTTP Response

系列总结

本文档分三篇深入解析了SpringMVC请求处理流程:

  1. 第1篇:请求入口与处理器映射

    • DispatcherServlet核心机制
    • HandlerMapping处理器映射
    • 请求处理完整流程
  2. 第2篇:处理器执行与参数绑定

    • HandlerAdapter适配器模式
    • 参数解析器ArgumentResolver
    • 返回值处理器ReturnValueHandler
    • 拦截器链执行机制
  3. 第3篇:视图渲染与异常处理

    • ViewResolver视图解析
    • HandlerExceptionResolver异常处理
    • LocaleResolver国际化
    • FlashMap重定向传递
    • Multipart文件上传
相关推荐
逻辑驱动的ken1 小时前
Java高频面试场景题19
java·开发语言·面试·职场和发展·求职招聘
leoufung2 小时前
LeetCode 42:接雨水 —— 从“矩形法”到双指针的完整思考过程
java·算法·leetcode
小碗羊肉2 小时前
【MySQL | 第十一篇】InnoDB引擎
java·数据库·mysql
Dylan的码园2 小时前
Maven基础架构与整体认识
java·junit·maven
弹不出的5h3ll2 小时前
Ghost Bits:高位截断如何让 Java WAF 形同虚设
java·开发语言
memories1982 小时前
Go 语言 Channel(管道/通道)
开发语言·后端·golang
庞轩px3 小时前
第七篇:注解与APT深度解析——从@Override到Lombok的底层原理
java·注解·编译·lombok
千寻girling3 小时前
五一劳动节快乐 [特殊字符][特殊字符][特殊字符]
java·c++·git·python·学习·github·php
计算机安禾3 小时前
【Linux从入门到精通】第47篇:SystemTap与eBPF——Linux内核观测的显微镜
java·linux·前端