Spring MVC 视图机制的核心接口与类
Spring MVC 的视图机制解决的是"Controller 返回一个逻辑视图名,如何变成最终的 HTML/JSON/PDF 响应"。围绕这个目标,Spring MVC 设计了一组接口和实现类,核心是 ViewResolver(视图解析器) 和 View(视图)。
一、两个核心接口
1. ViewResolver
视图解析器接口,负责把 Controller 返回的逻辑视图名 解析成具体的 View 对象。
java
public interface ViewResolver {
View resolveViewName(String viewName, Locale locale) throws Exception;
}
DispatcherServlet 在 processDispatchResult() 中遍历所有 ViewResolver,调用 resolveViewName(),拿到第一个非 null 的 View 后停止。
2. View
视图接口,负责把模型数据渲染成 HTTP 响应。
java
public interface View {
String getContentType();
void render(Map<String, ?> model,
HttpServletRequest request,
HttpServletResponse response) throws Exception;
}
render() 是真正执行渲染的地方:把 model 中的数据写入 response。
二、ViewResolver 的常见实现
| 实现类 | 作用 | 典型场景 |
|---|---|---|
InternalResourceViewResolver |
解析为 JSP 或内部资源 | JSP 项目 |
ThymeleafViewResolver |
解析为 Thymeleaf 模板 | Spring Boot 推荐 |
FreeMarkerViewResolver |
解析为 FreeMarker 模板 | FreeMarker 项目 |
ContentNegotiatingViewResolver |
根据请求的媒体类型选择视图 | 同一接口返回 JSON/XML/HTML |
BeanNameViewResolver |
把视图名当作 Bean 名称查找 | 自定义 View Bean |
XmlViewResolver |
从 XML 文件加载视图 Bean | 老项目 |
ResourceBundleViewResolver |
从 properties 文件加载视图 | 国际化视图 |
ViewResolverComposite |
组合多个 ViewResolver | 多视图技术共存 |
InternalResourceViewResolver
最经典的实现,通常配置 prefix 和 suffix:
xml
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/"/>
<property name="suffix" value=".jsp"/>
</bean>
返回 "user/list" 时,解析为 /WEB-INF/views/user/list.jsp。
它创建的是 InternalResourceView 或 JstlView,最终通过 RequestDispatcher.forward() 转发到 JSP。
ThymeleafViewResolver
Spring Boot 中 Thymeleaf 的视图解析器:
java
@Bean
public ThymeleafViewResolver thymeleafViewResolver(SpringTemplateEngine templateEngine) {
ThymeleafViewResolver resolver = new ThymeleafViewResolver();
resolver.setTemplateEngine(templateEngine);
resolver.setCharacterEncoding("UTF-8");
return resolver;
}
它依赖 SpringTemplateEngine 和 TemplateResolver 来定位和渲染模板。
ContentNegotiatingViewResolver
根据请求的 Accept 头或扩展名,委托给不同的 ViewResolver:
java
@Bean
public ContentNegotiatingViewResolver contentNegotiatingViewResolver() {
ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver();
resolver.setViewResolvers(List.of(
new ThymeleafViewResolver(),
new MappingJackson2JsonViewResolver()
));
return resolver;
}
同一个 /users/1,浏览器访问返回 HTML,Apipost 设置 Accept: application/json 返回 JSON。
BeanNameViewResolver
把逻辑视图名直接当作 Spring Bean 名称:
java
@Bean
public View pdfView() {
return new PdfView();
}
Controller 返回 "pdfView",BeanNameViewResolver 会从容器中找到名为 pdfView 的 Bean 作为视图。
三、View 的常见实现
| 实现类 | 作用 |
|---|---|
InternalResourceView |
内部资源转发,如 JSP |
JstlView |
支持 JSTL 的 JSP 视图 |
RedirectView |
重定向 |
MappingJackson2JsonView |
把模型数据序列化为 JSON |
MappingJackson2XmlView |
序列化为 XML |
AbstractPdfView |
PDF 视图基类 |
AbstractExcelView |
Excel 视图基类 |
FreeMarkerView |
FreeMarker 模板视图 |
ThymeleafView |
Thymeleaf 模板视图 |
InternalResourceView / JstlView
InternalResourceView 通过 RequestDispatcher.forward() 转发到目标资源。JstlView 是它的子类,增加了对 JSTL 的支持。
java
public class InternalResourceView extends AbstractUrlBasedView {
@Override
protected void renderMergedOutputModel(Map<String, Object> model,
HttpServletRequest request, HttpServletResponse response) throws Exception {
exposeModelAsRequestAttributes(model, request);
RequestDispatcher rd = getRequestDispatcher(request, getUrl());
rd.forward(request, response);
}
}
exposeModelAsRequestAttributes() 把 Model 中的数据逐个 setAttribute 到 request 域,这就是为什么 JSP 里能用 ${key} 取到 Controller 放入 Model 的数据。
RedirectView
处理 redirect: 前缀,向浏览器发送 302/301 响应:
java
@GetMapping("/save")
public String save() {
return "redirect:/list";
}
UrlBasedViewResolver 检测到 redirect: 前缀时,会创建 RedirectView,而不是 InternalResourceView。
MappingJackson2JsonView
把模型数据序列化为 JSON 写入响应。@ResponseBody 和 @RestController 的底层不是这个 View,而是 HttpMessageConverter,但 MappingJackson2JsonView 在需要"通过视图机制返回 JSON"时使用。
四、视图解析的辅助类
ViewResolverComposite
组合多个 ViewResolver,按顺序查找:
java
ViewResolverComposite composite = new ViewResolverComposite();
composite.setViewResolvers(List.of(
new ThymeleafViewResolver(),
new BeanNameViewResolver()
));
DispatcherServlet 初始化时,会把容器中所有的 ViewResolver 收集起来,如果配置了 ViewResolverComposite,就用它统一管理。
AbstractCachingViewResolver
ViewResolver 的抽象基类,提供视图缓存能力。InternalResourceViewResolver、ThymeleafViewResolver 等都继承它。解析过的 View 会被缓存,避免重复创建。
java
public abstract class AbstractCachingViewResolver extends WebApplicationObjectSupport
implements ViewResolver {
private final Map<Object, View> viewCache = new ConcurrentHashMap<>(16);
@Override
public View resolveViewName(String viewName, Locale locale) throws Exception {
// 先查缓存,未命中再调用 createView()
View view = this.viewCache.get(cacheKey);
if (view == null) {
view = createView(viewName, locale);
this.viewCache.put(cacheKey, view);
}
return view;
}
protected abstract View createView(String viewName, Locale locale) throws Exception;
}
UrlBasedViewResolver
基于 URL 的视图解析器抽象类,支持 prefix、suffix、redirect:、forward: 等前缀。
java
public abstract class UrlBasedViewResolver extends AbstractCachingViewResolver {
private String prefix = "";
private String suffix = "";
@Override
protected View createView(String viewName, Locale locale) throws Exception {
if (viewName.startsWith(REDIRECT_URL_PREFIX)) {
return createView(viewName, REDIRECT_URL_PREFIX);
}
if (viewName.startsWith(FORWARD_URL_PREFIX)) {
return createView(viewName, FORWARD_URL_PREFIX);
}
return buildView(prefix + viewName + suffix);
}
}
InternalResourceViewResolver 继承它,只负责返回 InternalResourceView 或 JstlView。
五、视图解析在 DispatcherServlet 中的位置
一次请求的完整流程:
1. DispatcherServlet.doDispatch()
2. 找到 Handler,通过 HandlerAdapter 调用 Controller
3. Controller 返回 ModelAndView
4. DispatcherServlet.processDispatchResult()
├── 如果 ModelAndView 中有 View 对象,直接使用
└── 如果只有视图名,调用 resolveViewName()
├── 遍历所有 ViewResolver
├── 调用 resolveViewName(viewName, locale)
└── 拿到第一个非 null 的 View
5. 调用 View.render(model, request, response)
6. 渲染完成,响应返回客户端
核心方法在 DispatcherServlet.resolveViewName():
java
protected View resolveViewName(String viewName, Map<String, Object> model,
Locale locale, HttpServletRequest request) throws Exception {
if (this.viewResolvers != null) {
for (ViewResolver viewResolver : this.viewResolvers) {
View view = viewResolver.resolveViewName(viewName, locale);
if (view != null) {
return view;
}
}
}
return null;
}
多个 ViewResolver 按顺序尝试,谁先返回非 null 的 View,就用谁。
六、各视图技术的组件对应关系
| 视图技术 | ViewResolver | View | 模板引擎 |
|---|---|---|---|
| JSP | InternalResourceViewResolver |
InternalResourceView / JstlView |
无(容器处理) |
| Thymeleaf | ThymeleafViewResolver |
ThymeleafView |
SpringTemplateEngine |
| FreeMarker | FreeMarkerViewResolver |
FreeMarkerView |
FreeMarkerConfigurer |
| JSON | MappingJackson2JsonView |
MappingJackson2JsonView |
Jackson ObjectMapper |
| 重定向 | UrlBasedViewResolver |
RedirectView |
无 |
七、总结
| 维度 | 核心内容 |
|---|---|
| 核心接口 | ViewResolver、View |
| 核心流程 | DispatcherServlet 遍历 ViewResolver → 拿到 View → 调用 render |
| ViewResolver 常见实现 | InternalResourceViewResolver、ThymeleafViewResolver、ContentNegotiatingViewResolver、BeanNameViewResolver |
| View 常见实现 | InternalResourceView、JstlView、RedirectView、MappingJackson2JsonView |
| 辅助基类 | AbstractCachingViewResolver、UrlBasedViewResolver、ViewResolverComposite |
| 关键机制 | 视图缓存、前缀后缀拼接、redirect: / forward: 前缀、内容协商 |
Spring MVC 视图机制的核心可以概括为:ViewResolver 负责"把视图名变成 View",View 负责"把模型数据渲染成响应" 。DispatcherServlet 在两者之间做协调。理解 InternalResourceViewResolver 和 ThymeleafViewResolver 这两个最常用的实现,以及 View.render() 中模型数据如何暴露到 request 域,就能解释为什么 Controller 返回的字符串最终变成了页面。