引言

Spring MVC作为Spring框架中用于Web开发的重要模块,提供了一套强大而灵活的页面输出技术。在Web应用开发中,如何将控制器处理后的数据有效地渲染并展示给用户,是每个开发者必须掌握的核心技能。本文将全面解析Spring MVC中的页面输出技术,从ModelAndView到ViewResolver,详细剖析数据渲染与页面展示的完整流程,并结合实际案例分享实战技巧,帮助开发者深入理解并灵活运用这些技术。

Spring MVC页面输出技术概述

Spring MVC的页面输出技术主要涉及以下几个核心组件:

  1. Controller:处理用户请求,准备需要展示的数据
  2. Model:存储需要传递给视图的数据
  3. View:负责渲染数据并生成HTML响应
  4. ModelAndView:一个同时包含Model和View的对象
  5. ViewResolver:根据视图名称解析具体的View对象

这些组件协同工作,形成了一个完整的数据渲染与页面展示流程。当控制器处理完请求后,会将数据放入Model中,并指定一个视图名称,然后ViewResolver会根据这个视图名称找到具体的视图实现,最后视图将Model中的数据渲染成最终的HTML响应返回给客户端。

ModelAndView详解

ModelAndView的基本概念

ModelAndView是Spring MVC中一个非常重要的类,它同时封装了Model和View两个对象。Model用于存储数据,View用于指定视图。通过ModelAndView,我们可以在控制器方法中方便地将数据和视图信息返回给DispatcherServlet。

ModelAndView的使用方式

1. 构造ModelAndView对象

ModelAndView提供了多种构造方式:

// 1. 只指定视图名 ModelAndView mav = new ModelAndView("viewName"); // 2. 同时指定视图名和Model Map<String, Object> model = new HashMap<>(); model.put("key1", "value1"); model.put("key2", "value2"); ModelAndView mav = new ModelAndView("viewName", model); // 3. 指定视图名并添加单个模型数据 ModelAndView mav = new ModelAndView("viewName", "modelName", modelObject); // 4. 创建空的ModelAndView,后续再设置 ModelAndView mav = new ModelAndView(); mav.setViewName("viewName"); mav.addObject("key1", "value1"); mav.addObject("key2", "value2"); 

2. 在Controller中使用ModelAndView

下面是一个完整的Controller示例,展示如何使用ModelAndView:

@Controller @RequestMapping("/user") public class UserController { @Autowired private UserService userService; @RequestMapping("/detail/{id}") public ModelAndView getUserDetail(@PathVariable("id") Long id) { // 创建ModelAndView对象 ModelAndView mav = new ModelAndView(); // 获取用户数据 User user = userService.getUserById(id); // 添加模型数据 mav.addObject("user", user); // 获取用户的订单列表 List<Order> orders = orderService.getOrdersByUserId(id); mav.addObject("orders", orders); // 设置视图名称 mav.setViewName("user/detail"); return mav; } @RequestMapping("/list") public ModelAndView getUserList( @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size) { // 分页获取用户列表 Page<User> userPage = userService.getUserList(page, size); // 使用另一种方式创建ModelAndView return new ModelAndView("user/list", "page", userPage); } } 

3. ModelAndView的常用方法

ModelAndView提供了一些常用方法来操作模型数据和视图:

// 添加模型数据 mav.addObject("key", value); // 添加单个数据 mav.addAllObjects(map); // 批量添加数据 // 获取模型数据 Map<String, Object> model = mav.getModel(); // 获取整个模型 Object value = mav.getModel().get("key"); // 获取特定数据 // 设置和获取视图 mav.setViewName("viewName"); // 设置视图名称 String viewName = mav.getViewName(); // 获取视图名称 View view = mav.getView(); // 获取View对象 // 设置视图状态 mav.setStatus(HttpStatus.OK); // 设置HTTP状态码 mav.clear(); // 清除ModelAndView中的所有数据 

ModelAndView的工作原理

当控制器方法返回一个ModelAndView对象后,DispatcherServlet会进行以下处理:

  1. 获取ModelAndView中的Model数据,并将其合并到临时的ModelMap中
  2. 获取ModelAndView中的View信息(可以是视图名称或View对象)
  3. 如果是视图名称,则使用ViewResolver解析为具体的View对象
  4. 将Model数据传递给View对象,进行渲染
  5. 返回渲染后的响应结果

下面是一个简化版的处理流程示意图:

// DispatcherServlet处理ModelAndView的简化流程 protected void render(ModelAndView mav, HttpServletRequest request, HttpServletResponse response) throws Exception { // 1. 获取视图 View view = resolveViewName(mav.getViewName(), mav.getModelInternal(), request, response); // 2. 渲染视图 if (view != null) { view.render(mav.getModelInternal(), request, response); } } 

ViewResolver详解

ViewResolver的基本概念

ViewResolver是Spring MVC中负责将视图名称解析为具体View对象的组件。当控制器返回一个视图名称时,ViewResolver会根据这个名称找到对应的View实现,如JSP、Thymeleaf、FreeMarker等。

ViewResolver的接口定义

ViewResolver接口非常简单,只定义了一个方法:

public interface ViewResolver { View resolveViewName(String viewName, Locale locale) throws Exception; } 

其中,viewName是控制器返回的视图名称,locale是本地化信息,返回值是解析后的View对象。

Spring MVC提供的ViewResolver实现

Spring MVC提供了多种ViewResolver的实现,以适应不同的视图技术:

1. InternalResourceViewResolver

InternalResourceViewResolver是最常用的ViewResolver之一,主要用于解析JSP和HTML等内部资源视图:

@Configuration @EnableWebMvc @ComponentScan("com.example.controller") public class WebConfig implements WebMvcConfigurer { @Bean public InternalResourceViewResolver viewResolver() { InternalResourceViewResolver resolver = new InternalResourceViewResolver(); resolver.setPrefix("/WEB-INF/views/"); // 设置视图前缀 resolver.setSuffix(".jsp"); // 设置视图后缀 resolver.setViewClass(JstlView.class); // 设置视图类,支持JSTL return resolver; } } 

配置完成后,当控制器返回”user/detail”时,InternalResourceViewResolver会将其解析为”/WEB-INF/views/user/detail.jsp”。

2. UrlBasedViewResolver

UrlBasedViewResolver是InternalResourceViewResolver的父类,提供了一种更简单的方式来解析视图:

@Bean public UrlBasedViewResolver urlBasedViewResolver() { UrlBasedViewResolver resolver = new UrlBasedViewResolver(); resolver.setPrefix("/WEB-INF/views/"); resolver.setSuffix(".jsp"); resolver.setViewClass(JstlView.class); return resolver; } 

3. ResourceBundleViewResolver

ResourceBundleViewResolver允许我们在属性文件中定义视图名称和对应的View类:

@Bean public ResourceBundleViewResolver resourceBundleViewResolver() { ResourceBundleViewResolver resolver = new ResourceBundleViewResolver(); resolver.setBasename("views"); // 指定属性文件的基础名称 return resolver; } 

然后在classpath根目录下的views.properties文件中定义视图映射:

user.detail.(class)=org.springframework.web.servlet.view.JstlView user.detail.url=/WEB-INF/views/user/detail.jsp user.list.(class)=org.springframework.web.servlet.view.JstlView user.list.url=/WEB-INF/views/user/list.jsp 

4. XmlViewResolver

XmlViewResolver允许我们在XML配置文件中定义视图:

@Bean public XmlViewResolver xmlViewResolver() { XmlViewResolver resolver = new XmlViewResolver(); resolver.setLocation(new ClassPathResource("views.xml")); // 指定XML配置文件 return resolver; } 

然后在views.xml文件中定义视图:

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="userDetail" class="org.springframework.web.servlet.view.JstlView"> <property name="url" value="/WEB-INF/views/user/detail.jsp"/> </bean> <bean id="userList" class="org.springframework.web.servlet.view.JstlView"> <property name="url" value="/WEB-INF/views/user/list.jsp"/> </bean> </beans> 

5. FreeMarkerViewResolver

FreeMarkerViewResolver用于解析FreeMarker模板:

@Bean public FreeMarkerViewResolver freeMarkerViewResolver() { FreeMarkerViewResolver resolver = new FreeMarkerViewResolver(); resolver.setPrefix(""); // FreeMarker通常不需要前缀 resolver.setSuffix(".ftl"); // FreeMarker模板文件后缀 resolver.setContentType("text/html;charset=UTF-8"); return resolver; } @Bean public FreeMarkerConfigurer freeMarkerConfigurer() { FreeMarkerConfigurer configurer = new FreeMarkerConfigurer(); configurer.setTemplateLoaderPath("/WEB-INF/views/"); // 模板文件路径 return configurer; } 

6. ThymeleafViewResolver

ThymeleafViewResolver用于解析Thymeleaf模板:

@Bean public ThymeleafViewResolver thymeleafViewResolver(SpringTemplateEngine templateEngine) { ThymeleafViewResolver resolver = new ThymeleafViewResolver(); resolver.setTemplateEngine(templateEngine); resolver.setCharacterEncoding("UTF-8"); return resolver; } @Bean public SpringTemplateEngine templateEngine(ITemplateResolver templateResolver) { SpringTemplateEngine engine = new SpringTemplateEngine(); engine.setTemplateResolver(templateResolver); return engine; } @Bean public ITemplateResolver templateResolver() { SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver(); resolver.setPrefix("/WEB-INF/views/"); resolver.setSuffix(".html"); resolver.setTemplateMode(TemplateMode.HTML); resolver.setCharacterEncoding("UTF-8"); return resolver; } 

ViewResolver的链式处理

Spring MVC支持配置多个ViewResolver,并可以指定它们的优先级。当需要解析视图时,Spring MVC会按照优先级顺序依次调用每个ViewResolver,直到有一个能够解析出View对象为止。

@Configuration @EnableWebMvc @ComponentScan("com.example.controller") public class WebConfig implements WebMvcConfigurer { @Bean public ThymeleafViewResolver thymeleafViewResolver(SpringTemplateEngine templateEngine) { ThymeleafViewResolver resolver = new ThymeleafViewResolver(); resolver.setTemplateEngine(templateEngine); resolver.setCharacterEncoding("UTF-8"); resolver.setOrder(1); // 设置优先级,数字越小优先级越高 return resolver; } @Bean public InternalResourceViewResolver internalResourceViewResolver() { InternalResourceViewResolver resolver = new InternalResourceViewResolver(); resolver.setPrefix("/WEB-INF/views/"); resolver.setSuffix(".jsp"); resolver.setOrder(2); // 优先级低于ThymeleafViewResolver return resolver; } // 其他Bean定义... } 

在上述配置中,当控制器返回一个视图名称时,Spring MVC会首先尝试使用ThymeleafViewResolver进行解析,如果失败,则尝试使用InternalResourceViewResolver。

自定义ViewResolver

如果Spring MVC提供的ViewResolver无法满足需求,我们还可以自定义ViewResolver:

public class CustomViewResolver implements ViewResolver { private String prefix; private String suffix; public void setPrefix(String prefix) { this.prefix = prefix; } public void setSuffix(String suffix) { this.suffix = suffix; } @Override public View resolveViewName(String viewName, Locale locale) throws Exception { // 根据视图名称和本地化信息构建实际视图路径 String viewPath = prefix + viewName + suffix; // 这里可以根据实际需求创建不同的View对象 // 例如,根据文件扩展名决定使用哪种视图技术 if (viewPath.endsWith(".jsp")) { JstlView view = new JstlView(); view.setUrl(viewPath); return view; } else if (viewPath.endsWith(".html")) { InternalResourceView view = new InternalResourceView(); view.setUrl(viewPath); return view; } // 如果无法解析,返回null,让下一个ViewResolver尝试 return null; } } 

然后在配置类中注册自定义的ViewResolver:

@Bean public CustomViewResolver customViewResolver() { CustomViewResolver resolver = new CustomViewResolver(); resolver.setPrefix("/WEB-INF/views/"); resolver.setSuffix(".html"); resolver.setOrder(0); // 设置最高优先级 return resolver; } 

数据渲染与页面展示流程

完整的请求处理流程

Spring MVC处理请求并渲染页面的完整流程如下:

  1. 用户发送请求到DispatcherServlet
  2. DispatcherServlet根据请求URL找到对应的HandlerMapping
  3. HandlerMapping返回对应的HandlerExecutionChain
  4. DispatcherServlet通过HandlerAdapter调用对应的Controller方法
  5. Controller方法处理业务逻辑,并返回ModelAndView
  6. DispatcherServlet接收ModelAndView,并调用ViewResolver解析视图
  7. ViewResolver根据视图名称返回对应的View对象
  8. DispatcherServlet将Model数据传递给View对象
  9. View对象使用Model数据进行渲染,生成最终的HTML响应
  10. DispatcherServlet将HTML响应返回给用户

下面是一个详细流程的示意图:

// DispatcherServlet的简化处理流程 protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { // 1. 根据请求获取Handler HandlerExecutionChain mappedHandler = getHandler(request); // 2. 获取HandlerAdapter HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); // 3. 调用前置拦截器 if (!mappedHandler.applyPreHandle(request, response)) { return; } // 4. 调用Controller方法 ModelAndView mav = ha.handle(request, response, mappedHandler.getHandler()); // 5. 调用后置拦截器 mappedHandler.applyPostHandle(request, response, mav); // 6. 处理分发结果 processDispatchResult(request, response, mappedHandler, mav); } // 处理分发结果 private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, HandlerExecutionChain mappedHandler, ModelAndView mav) throws Exception { // 渲染视图 render(mav, request, response); // 触发完成回调 mappedHandler.triggerAfterCompletion(request, response, null); } // 渲染视图 protected void render(ModelAndView mav, HttpServletRequest request, HttpServletResponse response) throws Exception { // 1. 解析视图 View view = resolveViewName(mav.getViewName(), mav.getModelInternal(), request, response); // 2. 渲染视图 if (view != null) { view.render(mav.getModelInternal(), request, response); } } 

数据传递与渲染机制

1. Model数据的创建与传递

在Spring MVC中,Model数据可以通过多种方式创建和传递:

方式一:使用ModelAndView

@RequestMapping("/user/{id}") public ModelAndView getUser(@PathVariable Long id) { User user = userService.getUserById(id); ModelAndView mav = new ModelAndView(); mav.addObject("user", user); // 添加数据到Model mav.setViewName("user/detail"); return mav; } 

方式二:使用Model参数

@RequestMapping("/user/{id}") public String getUser(@PathVariable Long id, Model model) { User user = userService.getUserById(id); model.addAttribute("user", user); // 添加数据到Model return "user/detail"; } 

方式三:使用ModelMap参数

@RequestMapping("/user/{id}") public String getUser(@PathVariable Long id, ModelMap model) { User user = userService.getUserById(id); model.addAttribute("user", user); // 添加数据到Model return "user/detail"; } 

方式四:使用Map参数

@RequestMapping("/user/{id}") public String getUser(@PathVariable Long id, Map<String, Object> model) { User user = userService.getUserById(id); model.put("user", user); // 添加数据到Model return "user/detail"; } 

方式五:使用@ModelAttribute注解

@ModelAttribute("user") public User getUser(@PathVariable Long id) { return userService.getUserById(id); } @RequestMapping("/user/{id}") public String getUser() { return "user/detail"; } 

2. 视图渲染机制

不同的视图技术有不同的渲染机制,下面以JSP和Thymeleaf为例进行说明:

JSP渲染机制

JSP(JavaServer Pages)是一种基于Java的模板技术,它允许在HTML中嵌入Java代码。在Spring MVC中,JSP的渲染过程如下:

  1. ViewResolver将视图名称解析为JSP文件路径
  2. DispatcherServlet将请求转发到JSP文件
  3. JSP引擎解析JSP文件,将其转换为Servlet
  4. Servlet执行,将Model数据渲染到HTML中
  5. 最终的HTML响应返回给客户端

下面是一个使用JSTL的JSP示例:

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <!DOCTYPE html> <html> <head> <title>User Detail</title> </head> <body> <h1>User Detail</h1> <table border="1"> <tr> <th>ID</th> <th>Name</th> <th>Email</th> <th>Registration Date</th> </tr> <tr> <td>${user.id}</td> <td>${user.name}</td> <td>${user.email}</td> <td><fmt:formatDate value="${user.registrationDate}" pattern="yyyy-MM-dd"/></td> </tr> </table> <h2>Orders</h2> <table border="1"> <tr> <th>ID</th> <th>Product</th> <th>Amount</th> <th>Date</th> </tr> <c:forEach items="${orders}" var="order"> <tr> <td>${order.id}</td> <td>${order.product}</td> <td><fmt:formatNumber value="${order.amount}" type="currency"/></td> <td><fmt:formatDate value="${order.date}" pattern="yyyy-MM-dd"/></td> </tr> </c:forEach> </table> </body> </html> 

Thymeleaf渲染机制

Thymeleaf是一种现代的服务器端Java模板引擎,它强调自然模板的概念,允许模板作为静态原型工作。在Spring MVC中,Thymeleaf的渲染过程如下:

  1. ViewResolver将视图名称解析为Thymeleaf模板文件
  2. Thymeleaf引擎解析模板文件,构建DOM树
  3. Thymeleaf引擎将Model数据与DOM树结合,进行数据绑定和逻辑处理
  4. 最终的HTML响应返回给客户端

下面是一个使用Thymeleaf的HTML模板示例:

<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>User Detail</title> <meta charset="UTF-8"/> </head> <body> <h1>User Detail</h1> <table border="1"> <tr> <th>ID</th> <th>Name</th> <th>Email</th> <th>Registration Date</th> </tr> <tr> <td th:text="${user.id}">1</td> <td th:text="${user.name}">John Doe</td> <td th:text="${user.email}">john@example.com</td> <td th:text="${#dates.format(user.registrationDate, 'yyyy-MM-dd')}">2023-01-01</td> </tr> </table> <h2>Orders</h2> <table border="1"> <tr> <th>ID</th> <th>Product</th> <th>Amount</th> <th>Date</th> </tr> <tr th:each="order : ${orders}"> <td th:text="${order.id}">1</td> <td th:text="${order.product}">Product A</td> <td th:text="${#numbers.formatCurrency(order.amount)}">$100.00</td> <td th:text="${#dates.format(order.date, 'yyyy-MM-dd')}">2023-01-01</td> </tr> </table> </body> </html> 

视图解析与渲染的细节

1. 视图名称解析

视图名称解析是ViewResolver的核心功能,下面是一个简化的视图名称解析过程:

// InternalResourceViewResolver的简化解析过程 public View resolveViewName(String viewName, Locale locale) throws Exception { // 构建实际视图路径 String path = getPrefix() + viewName + getSuffix(); // 创建View对象 AbstractUrlBasedView view = buildView(path); // 应用其他配置 view.setApplicationContext(getApplicationContext()); return view; } protected AbstractUrlBasedView buildView(String viewUrl) throws Exception { // 实例化View类 AbstractUrlBasedView view = (AbstractUrlBasedView) BeanUtils.instantiateClass(getViewClass()); // 设置URL view.setUrl(viewUrl); // 设置其他属性 if (getContentType() != null) { view.setContentType(getContentType()); } return view; } 

2. 视图渲染过程

视图渲染是View的核心功能,下面是一个简化的视图渲染过程:

// InternalResourceView的简化渲染过程 public void render(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception { // 暴露Model数据到请求属性中 exposeModelAsRequestAttributes(model, request); // 准备请求 prepareRequest(request); // 获取请求分发器 RequestDispatcher rd = getRequestDispatcher(request, getUrl()); // 如果是包含请求,则包含资源 if (useInclude(request, response)) { response.setContentType(getContentType()); rd.include(request, response); } // 否则转发请求 else { rd.forward(request, response); } } protected void exposeModelAsRequestAttributes(Map<String, Object> model, HttpServletRequest request) throws Exception { // 将Model中的数据暴露为请求属性 for (Map.Entry<String, Object> entry : model.entrySet()) { String modelName = entry.getKey(); Object modelValue = entry.getValue(); if (modelValue != null) { request.setAttribute(modelName, modelValue); } } } 

3. 重定向与转发

在Spring MVC中,我们可以通过返回特定的视图名称来实现重定向和转发:

重定向

@RequestMapping("/save") public String saveUser(User user) { userService.saveUser(user); // 使用"redirect:"前缀实现重定向 return "redirect:/user/list"; } 

转发

@RequestMapping("/process") public String processForm() { // 使用"forward:"前缀实现转发 return "forward:/user/list"; } 

重定向和转发的区别:

  1. 重定向是客户端行为,浏览器地址栏会改变;转发是服务器端行为,浏览器地址栏不会改变
  2. 重定向会丢失请求属性;转发会保留请求属性
  3. 重定向可以重定向到任意URL;转发只能在同一应用内进行

实战技巧

1. 使用多个ViewResolver处理不同类型的视图

在实际项目中,我们可能需要同时使用多种视图技术,例如使用Thymeleaf处理HTML页面,使用JSP处理报表,使用JSON处理API响应等。这时,我们可以配置多个ViewResolver,并设置它们的优先级:

@Configuration @EnableWebMvc public class WebConfig implements WebMvcConfigurer { @Bean public ContentNegotiatingViewResolver contentViewResolver() { ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver(); // 设置内容协商策略 ContentNegotiationManager manager = new ContentNegotiationManager( new HeaderContentNegotiationStrategy(), new FixedContentNegotiationStrategy(MediaType.TEXT_HTML) ); resolver.setContentNegotiationManager(manager); // 设置默认视图 resolver.setDefaultViews(Arrays.asList(new MappingJackson2JsonView())); // 设置ViewResolver列表 List<ViewResolver> resolvers = new ArrayList<>(); resolvers.add(thymeleafViewResolver()); resolvers.add(jspViewResolver()); resolver.setViewResolvers(resolvers); return resolver; } @Bean public ThymeleafViewResolver thymeleafViewResolver(SpringTemplateEngine templateEngine) { ThymeleafViewResolver resolver = new ThymeleafViewResolver(); resolver.setTemplateEngine(templateEngine); resolver.setCharacterEncoding("UTF-8"); resolver.setOrder(1); return resolver; } @Bean public InternalResourceViewResolver jspViewResolver() { InternalResourceViewResolver resolver = new InternalResourceViewResolver(); resolver.setPrefix("/WEB-INF/jsp/"); resolver.setSuffix(".jsp"); resolver.setViewClass(JstlView.class); resolver.setOrder(2); return resolver; } // 其他Bean定义... } 

2. 使用ContentNegotiatingViewResolver实现内容协商

ContentNegotiatingViewResolver是Spring MVC提供的一个特殊ViewResolver,它可以根据请求的Accept头、文件扩展名或请求参数来决定使用哪种视图技术:

@Configuration @EnableWebMvc public class WebConfig implements WebMvcConfigurer { @Bean public ContentNegotiatingViewResolver contentViewResolver() { ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver(); // 设置内容协商策略 ContentNegotiationManager manager = new ContentNegotiationManager( new HeaderContentNegotiationStrategy(), new ParameterContentNegotiationStrategy(), new PathExtensionContentNegotiationStrategy() ); resolver.setContentNegotiationManager(manager); // 设置默认内容类型 resolver.setDefaultContentType(MediaType.APPLICATION_JSON); // 设置媒体类型映射 Map<String, MediaType> mediaTypes = new HashMap<>(); mediaTypes.put("html", MediaType.TEXT_HTML); mediaTypes.put("json", MediaType.APPLICATION_JSON); mediaTypes.put("xml", MediaType.APPLICATION_XML); manager.addMediaTypeMappings(mediaTypes); // 设置ViewResolver列表 List<ViewResolver> resolvers = new ArrayList<>(); resolvers.add(thymeleafViewResolver()); resolvers.add(jsonViewResolver()); resolvers.add(xmlViewResolver()); resolver.setViewResolvers(resolvers); return resolver; } @Bean public ThymeleafViewResolver thymeleafViewResolver(SpringTemplateEngine templateEngine) { ThymeleafViewResolver resolver = new ThymeleafViewResolver(); resolver.setTemplateEngine(templateEngine); resolver.setCharacterEncoding("UTF-8"); resolver.setContentType(MediaType.TEXT_HTML_VALUE); return resolver; } @Bean public ViewResolver jsonViewResolver() { return new ViewResolver() { @Override public View resolveViewName(String viewName, Locale locale) throws Exception { MappingJackson2JsonView view = new MappingJackson2JsonView(); view.setPrettyPrint(true); return view; } }; } @Bean public ViewResolver xmlViewResolver() { return new ViewResolver() { @Override public View resolveViewName(String viewName, Locale locale) throws Exception { MarshallingView view = new MarshallingView(jaxb2Marshaller()); return view; } }; } @Bean public Jaxb2Marshaller jaxb2Marshaller() { Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); marshaller.setPackagesToScan("com.example.model"); return marshaller; } } 

3. 使用异常处理器自定义错误页面

在Spring MVC中,我们可以通过@ExceptionHandler和@ControllerAdvice来处理异常并返回自定义的错误页面:

@ControllerAdvice public class GlobalExceptionHandler { // 处理特定异常 @ExceptionHandler(UserNotFoundException.class) public ModelAndView handleUserNotFoundException(UserNotFoundException ex) { ModelAndView mav = new ModelAndView(); mav.addObject("errorMessage", ex.getMessage()); mav.addObject("errorCode", "404"); mav.setViewName("error/404"); return mav; } // 处理所有异常 @ExceptionHandler(Exception.class) public ModelAndView handleAllExceptions(Exception ex) { ModelAndView mav = new ModelAndView(); mav.addObject("errorMessage", ex.getMessage()); mav.addObject("errorCode", "500"); mav.setViewName("error/500"); return mav; } } 

4. 使用拦截器预处理和后处理Model数据

Spring MVC的拦截器不仅可以用于权限验证、日志记录等,还可以用于预处理和后处理Model数据:

public class ModelDataInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 在控制器方法执行前执行 return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView mav) throws Exception { // 在控制器方法执行后,视图渲染前执行 if (mav != null) { // 添加全局数据 mav.addObject("currentTime", new Date()); mav.addObject("applicationName", "Spring MVC Demo"); // 处理Model数据 Map<String, Object> model = mav.getModel(); for (Map.Entry<String, Object> entry : model.entrySet()) { // 对特定数据进行处理 if (entry.getValue() instanceof String) { String value = (String) entry.getValue(); if (value != null && value.length() > 100) { // 截断过长的字符串 model.put(entry.getKey(), value.substring(0, 100) + "..."); } } } } } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { // 在视图渲染后执行 } } 

然后在配置类中注册拦截器:

@Configuration @EnableWebMvc public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new ModelDataInterceptor()) .addPathPatterns("/**") .excludePathPatterns("/static/**"); } // 其他配置... } 

5. 使用Flash属性传递重定向数据

在Spring MVC中,重定向会导致请求属性丢失,但我们可以使用Flash属性在重定向之间传递数据:

@Controller @RequestMapping("/user") public class UserController { @PostMapping("/save") public String saveUser(User user, RedirectAttributes redirectAttributes) { // 保存用户 userService.saveUser(user); // 添加Flash属性 redirectAttributes.addFlashAttribute("message", "User saved successfully!"); redirectAttributes.addFlashAttribute("user", user); // 重定向到列表页面 return "redirect:/user/list"; } @GetMapping("/list") public String listUsers(Model model) { // Flash属性会自动添加到Model中 // 不需要额外处理 // 获取用户列表 List<User> users = userService.getAllUsers(); model.addAttribute("users", users); return "user/list"; } } 

6. 使用自定义视图处理特殊需求

有时,标准的视图技术无法满足特殊需求,这时我们可以自定义视图:

public class PdfView extends AbstractView { public PdfView() { setContentType("application/pdf"); } @Override protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception { // 设置响应头 response.setContentType(getContentType()); response.setHeader("Content-Disposition", "attachment; filename="report.pdf""); // 获取数据 List<User> users = (List<User>) model.get("users"); // 创建PDF文档 Document document = new Document(); PdfWriter writer = PdfWriter.getInstance(document, response.getOutputStream()); document.open(); // 添加标题 Font titleFont = new Font(Font.FontFamily.HELVETICA, 18, Font.BOLD); Paragraph title = new Paragraph("User Report", titleFont); title.setAlignment(Element.ALIGN_CENTER); document.add(title); // 添加表格 PdfPTable table = new PdfPTable(3); table.setWidthPercentage(100); // 添加表头 table.addCell(createCell("ID", true)); table.addCell(createCell("Name", true)); table.addCell(createCell("Email", true)); // 添加数据行 for (User user : users) { table.addCell(createCell(String.valueOf(user.getId()), false)); table.addCell(createCell(user.getName(), false)); table.addCell(createCell(user.getEmail(), false)); } document.add(table); document.close(); writer.close(); } private PdfPCell createCell(String content, boolean isHeader) { Font font = isHeader ? new Font(Font.FontFamily.HELVETICA, 12, Font.BOLD) : new Font(Font.FontFamily.HELVETICA, 10, Font.NORMAL); PdfPCell cell = new PdfPCell(new Phrase(content, font)); cell.setHorizontalAlignment(Element.ALIGN_CENTER); cell.setVerticalAlignment(Element.ALIGN_MIDDLE); cell.setPadding(5); if (isHeader) { cell.setBackgroundColor(BaseColor.LIGHT_GRAY); } return cell; } } 

然后在控制器中使用自定义视图:

@Controller @RequestMapping("/report") public class ReportController { @Autowired private UserService userService; @GetMapping("/users.pdf") public View generateUserReport() { // 创建自定义视图 PdfView view = new PdfView(); // 准备数据 List<User> users = userService.getAllUsers(); // 添加静态属性 Map<String, Object> model = new HashMap<>(); model.put("users", users); view.setAttributesMap(model); return view; } } 

7. 使用Tiles框架构建复合视图

Apache Tiles是一个模板框架,允许构建可重用的页面组件。在Spring MVC中集成Tiles:

首先,添加Tiles依赖:

<dependency> <groupId>org.apache.tiles</groupId> <artifactId>tiles-jsp</artifactId> <version>3.0.8</version> </dependency> 

然后,配置Tiles:

@Configuration @EnableWebMvc @ComponentScan("com.example.controller") public class WebConfig implements WebMvcConfigurer { @Bean public TilesConfigurer tilesConfigurer() { TilesConfigurer configurer = new TilesConfigurer(); configurer.setDefinitions("/WEB-INF/tiles/tiles-defs.xml"); configurer.setCheckRefresh(true); return configurer; } @Bean public TilesViewResolver tilesViewResolver() { TilesViewResolver resolver = new TilesViewResolver(); resolver.setViewClass(TilesView.class); return resolver; } // 其他配置... } 

定义Tiles配置文件(tiles-defs.xml):

<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE tiles-definitions PUBLIC "-//Apache Software Foundation//DTD Tiles Configuration 3.0//EN" "http://tiles.apache.org/dtds/tiles-config_3_0.dtd"> <tiles-definitions> <!-- 基础模板 --> <definition name="base" template="/WEB-INF/tiles/layout.jsp"> <put-attribute name="header" value="/WEB-INF/tiles/header.jsp" /> <put-attribute name="menu" value="/WEB-INF/tiles/menu.jsp" /> <put-attribute name="body" value="" /> <put-attribute name="footer" value="/WEB-INF/tiles/footer.jsp" /> </definition> <!-- 首页 --> <definition name="home" extends="base"> <put-attribute name="body" value="/WEB-INF/views/home.jsp" /> <put-attribute name="title" value="Home Page" /> </definition> <!-- 用户列表 --> <definition name="user.list" extends="base"> <put-attribute name="body" value="/WEB-INF/views/user/list.jsp" /> <put-attribute name="title" value="User List" /> </definition> <!-- 用户详情 --> <definition name="user.detail" extends="base"> <put-attribute name="body" value="/WEB-INF/views/user/detail.jsp" /> <put-attribute name="title" value="User Detail" /> </definition> </tiles-definitions> 

创建基础模板(layout.jsp):

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="tiles" uri="http://tiles.apache.org/tags-tiles" %> <!DOCTYPE html> <html> <head> <title><tiles:getAsString name="title"/></title> <meta charset="UTF-8"/> </head> <body> <div id="header"> <tiles:insertAttribute name="header"/> </div> <div id="menu"> <tiles:insertAttribute name="menu"/> </div> <div id="body"> <tiles:insertAttribute name="body"/> </div> <div id="footer"> <tiles:insertAttribute name="footer"/> </div> </body> </html> 

最后,在控制器中返回Tiles视图名称:

@Controller @RequestMapping("/user") public class UserController { @Autowired private UserService userService; @RequestMapping("/list") public String listUsers(Model model) { List<User> users = userService.getAllUsers(); model.addAttribute("users", users); return "user.list"; // 返回Tiles视图名称 } @RequestMapping("/detail/{id}") public String getUserDetail(@PathVariable Long id, Model model) { User user = userService.getUserById(id); model.addAttribute("user", user); return "user.detail"; // 返回Tiles视图名称 } } 

总结

本文全面解析了Spring MVC中的页面输出技术,从ModelAndView到ViewResolver,详细介绍了数据渲染与页面展示的完整流程及实战技巧。

我们首先了解了ModelAndView的基本概念和使用方式,掌握了如何在控制器中准备数据和指定视图。然后,我们深入探讨了ViewResolver的工作机制,学习了Spring MVC提供的多种ViewResolver实现,以及如何配置和使用它们。

接着,我们详细分析了Spring MVC中的数据渲染与页面展示流程,包括完整的请求处理流程、数据传递与渲染机制,以及视图解析与渲染的细节。最后,我们分享了一些实用的实战技巧,如使用多个ViewResolver处理不同类型的视图、使用ContentNegotiatingViewResolver实现内容协商、使用异常处理器自定义错误页面、使用拦截器预处理和后处理Model数据、使用Flash属性传递重定向数据、使用自定义视图处理特殊需求,以及使用Tiles框架构建复合视图。

通过本文的学习,读者应该能够深入理解Spring MVC的页面输出技术,并能够灵活运用这些技术解决实际开发中的问题。希望本文能够帮助读者在Spring MVC开发中更加得心应手,构建出更加优秀的Web应用。