SpringMVC请求处理流程源码解析(第3篇):视图渲染与异常处理
本文深入剖析视图解析与渲染、异常处理机制、国际化支持、FlashMap重定向传递和文件上传处理的完整源码流程。
目录
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篇:请求入口与处理器映射
- DispatcherServlet核心机制
- HandlerMapping处理器映射
- 请求处理完整流程
-
第2篇:处理器执行与参数绑定
- HandlerAdapter适配器模式
- 参数解析器ArgumentResolver
- 返回值处理器ReturnValueHandler
- 拦截器链执行机制
-
第3篇:视图渲染与异常处理
- ViewResolver视图解析
- HandlerExceptionResolver异常处理
- LocaleResolver国际化
- FlashMap重定向传递
- Multipart文件上传