本文目录导读:

我将为您创建一个完整的Spring MVC视图解析案例,涵盖多种视图解析策略。
项目结构
spring-mvc-view-resolver/
├── pom.xml
├── src/main/java/
│ ├── config/
│ │ ├── WebConfig.java
│ │ └── WebInitializer.java
│ ├── controller/
│ │ └── ViewController.java
│ └── model/
│ └── User.java
└── src/main/resources/
│ └── application.properties
└── src/main/webapp/
├── WEB-INF/
│ └── views/
│ ├── jsp/
│ │ ├── home.jsp
│ │ └── user/
│ │ └── profile.jsp
│ ├── freemarker/
│ │ └── welcome.ftl
│ └── thymeleaf/
│ └── index.html
Maven依赖配置 (pom.xml)
<dependencies>
<!-- Spring MVC -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.29</version>
</dependency>
<!-- JSP 视图解析 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jsp-api</artifactId>
<version>2.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<!-- Thymeleaf 视图解析 -->
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId>
<version>3.0.15.RELEASE</version>
</dependency>
<!-- Freemarker 视图解析 -->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.32</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>5.3.29</version>
</dependency>
</dependencies>
Web配置类
WebInitializer.java
package config;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
public class WebInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[] { WebConfig.class };
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] { WebConfig.class };
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
@Override
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
// Customize if needed
registration.setInitParameter("throwExceptionIfNoHandlerFound", "true");
}
}
WebConfig.java
package config;
import org.springframework.context.annotation.*;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.*;
import org.springframework.web.servlet.view.*;
import org.springframework.web.servlet.view.freemarker.*;
import org.thymeleaf.spring5.SpringTemplateEngine;
import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver;
import org.thymeleaf.spring5.view.ThymeleafViewResolver;
import org.thymeleaf.templatemode.TemplateMode;
import org.springframework.web.servlet.view.JstlView;
import java.util.HashMap;
import java.util.Map;
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "controller")
public class WebConfig implements WebMvcConfigurer {
// ============ JSP 视图解析器 ============
@Bean
public ViewResolver jspViewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setViewClass(JstlView.class);
resolver.setPrefix("/WEB-INF/views/jsp/");
resolver.setSuffix(".jsp");
resolver.setOrder(3); // 最后解析
resolver.setViewNames("*.jsp"); // 仅处理以.jsp结尾的视图名
// 配置JSP属性
Map<String, Object> attributes = new HashMap<>();
attributes.put("active", true);
resolver.setAttributes(attributes);
// 设置内容类型
resolver.setContentType("text/html;charset=UTF-8");
resolver.setCharacterEncoding("UTF-8");
resolver.setCache(true);
return resolver;
}
// ============ Thymeleaf 视图解析器 ============
@Bean
public SpringResourceTemplateResolver templateResolver() {
SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
templateResolver.setPrefix("/WEB-INF/views/thymeleaf/");
templateResolver.setSuffix(".html");
templateResolver.setTemplateMode(TemplateMode.HTML);
templateResolver.setCharacterEncoding("UTF-8");
templateResolver.setCacheable(true);
templateResolver.setOrder(2);
return templateResolver;
}
@Bean
public SpringTemplateEngine templateEngine() {
SpringTemplateEngine engine = new SpringTemplateEngine();
engine.setTemplateResolver(templateResolver());
engine.setEnableSpringELCompiler(true);
return engine;
}
@Bean
public ViewResolver thymeleafViewResolver() {
ThymeleafViewResolver resolver = new ThymeleafViewResolver();
resolver.setTemplateEngine(templateEngine());
resolver.setCharacterEncoding("UTF-8");
resolver.setOrder(2); // 第二优先级
resolver.setViewNames(new String[] {"th_*", "thymeleaf_*"}); // 仅处理特定前缀的视图名
return resolver;
}
// ============ Freemarker 视图解析器 ============
@Bean
public FreeMarkerConfigurer freeMarkerConfigurer() {
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
configurer.setTemplateLoaderPath("/WEB-INF/views/freemarker/");
configurer.setDefaultEncoding("UTF-8");
Map<String, Object> variables = new HashMap<>();
variables.put("app", "Spring MVC Freemarker");
configurer.setFreemarkerVariables(variables);
// Freemarker 设置
Properties settings = new Properties();
settings.setProperty("number_format", "0.##");
settings.setProperty("datetime_format", "yyyy-MM-dd HH:mm:ss");
settings.setProperty("date_format", "yyyy-MM-dd");
settings.setProperty("time_format", "HH:mm:ss");
configurer.setFreemarkerSettings(settings);
return configurer;
}
@Bean
public ViewResolver freeMarkerViewResolver() {
FreeMarkerViewResolver resolver = new FreeMarkerViewResolver();
resolver.setConfiguration(freeMarkerConfigurer().getConfiguration());
resolver.setPrefix("");
resolver.setSuffix(".ftl");
resolver.setContentType("text/html;charset=UTF-8");
resolver.setOrder(1); // 第二优先级
resolver.setViewNames(new String[] {"ftl_*", "freemarker_*"}); // 仅处理特定前缀的视图名
return resolver;
}
// ============ XML/普通视图解析器(可选) ============
@Bean
public ViewResolver xmlViewResolver() {
XmlViewResolver resolver = new XmlViewResolver();
resolver.setLocation(new ClassPathResource("/WEB-INF/views/") );
resolver.setOrder(4);
resolver.setViewNames(new String[] {"xml_*"});
return resolver;
}
// ============ BeanName 视图解析器(可选) ============
@Bean
public ViewResolver beanNameViewResolver() {
BeanNameViewResolver resolver = new BeanNameViewResolver();
resolver.setOrder(0); // 最高优先级
return resolver;
}
// ============ 静态资源处理 ============
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**")
.addResourceLocations("/static/")
.setCachePeriod(3600);
}
// ============ 跨域配置 ============
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.maxAge(3600);
}
}
控制器类
ViewController.java
package controller;
import model.User;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
import java.util.Arrays;
import java.util.List;
@Controller
@RequestMapping("/view")
public class ViewController {
// ============ JSP 视图示例 ============
@GetMapping("/jsp/home")
public String jspHome(Model model) {
model.addAttribute("title", "Spring MVC JSP View");
model.addAttribute("message", "This view is rendered by JSP");
model.addAttribute("users", getSampleUsers());
return "home"; // 会被 JSP视图解析器处理
}
@GetMapping("/jsp/profile/{id}")
public String jspProfile(@PathVariable Long id, Model model) {
User user = getUserById(id);
model.addAttribute("user", user);
return "user/profile"; // JSP 视图名称
}
// ============ Thymeleaf 视图示例 ============
@GetMapping("/thymeleaf")
public String thymeleafView(Model model) {
model.addAttribute("title", "Spring MVC Thymeleaf View");
model.addAttribute("message", "This view is rendered by Thymeleaf");
model.addAttribute("users", getSampleUsers());
return "thymeleaf_index"; // 前缀匹配 "th_"
}
// ============ Freemarker 视图示例 ============
@GetMapping("/freemarker")
public ModelAndView freemarkerView() {
ModelAndView mv = new ModelAndView("freemarker_welcome");
mv.addObject("title", "Spring MVC Freemarker View");
mv.addObject("message", "This view is rendered by Freemarker");
mv.addObject("users", getSampleUsers());
return mv;
}
// ============ 使用ModelAndView返回多个数据 ============
@GetMapping("/modelandview")
public ModelAndView modelAndViewExample() {
ModelAndView mv = new ModelAndView("jsp/home");
mv.addObject("title", "ModelAndView Example");
mv.addObject("message", "Using ModelAndView to render view");
mv.addObject("users", getSampleUsers());
return mv;
}
// ============ JSON 返回示例 ============
@GetMapping("/json/users")
@ResponseBody
public List<User> getUsersJson() {
return getSampleUsers();
}
// ============ 重定向视图 ============
@GetMapping("/redirect")
public String redirectView() {
return "redirect:/view/freemarker";
}
// ============ 转发视图 ============
@GetMapping("/forward")
public String forwardView() {
return "forward:/view/jsp/home";
}
// ============ 辅助方法 ============
private List<User> getSampleUsers() {
return Arrays.asList(
new User(1L, "张三", "zhangsan@example.com"),
new User(2L, "李四", "lisi@example.com"),
new User(3L, "王五", "wangwu@example.com")
);
}
private User getUserById(Long id) {
return getSampleUsers().stream()
.filter(u -> u.getId().equals(id))
.findFirst()
.orElse(null);
}
}
视图模板
home.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>${title}</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
.container { max-width: 800px; margin: auto; }
.message { background: #e0f7fa; padding: 20px; border-radius: 5px; }
.user-list { margin-top: 20px; }
table { border-collapse: collapse; width: 100%; }
th, td { padding: 8px; text-align: left; border-bottom: 1px solid #ddd; }
th { background-color: #4CAF50; color: white; }
</style>
</head>
<body>
<div class="container">
<h1>${title}</h1>
<div class="message">${message}</div>
<div class="user-list">
<h2>用户列表</h2>
<c:if test="${not empty users}">
<table>
<thead>
<tr>
<th>ID</th>
<th>姓名</th>
<th>邮箱</th>
</tr>
</thead>
<tbody>
<c:forEach var="user" items="${users}">
<tr>
<td>${user.id}</td>
<td>${user.name}</td>
<td>${user.email}</td>
</tr>
</c:forEach>
</tbody>
</table>
</c:if>
</div>
</div>
</body>
</html>
welcome.ftl (Freemarker)
<!DOCTYPE html>
<html>
<head>${title}</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
.container { max-width: 800px; margin: auto; }
.highlight { color: #4CAF50; }
table { border-collapse: collapse; width: 100%; margin-top: 20px; }
th, td { padding: 8px; border: 1px solid #ddd; }
</style>
</head>
<body>
<div class="container">
<h1>${title}</h1>
<p class="highlight">${message}</p>
<h2>用户列表 (Freemarker)</h2>
<table>
<thead>
<tr>
<th>ID</th>
<th>姓名</th>
<th>邮箱</th>
</tr>
</thead>
<tbody>
<#list users as user>
<tr>
<td>${user.id}</td>
<td>${user.name}</td>
<td>${user.email}</td>
</tr>
</#list>
</tbody>
</table>
</div>
</body>
</html>
index.th.html (Thymeleaf)
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>th:text="${title}">Title</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
.container { max-width: 800px; margin: auto; }
table { border-collapse: collapse; width: 100%; margin-top: 20px; }
th, td { padding: 8px; border: 1px solid #ddd; }
</style>
</head>
<body>
<div class="container">
<h1 th:text="${title}">Title</h1>
<p th:text="${message}">Message</p>
<h2>用户列表 (Thymeleaf)</h2>
<table>
<thead>
<tr>
<th>ID</th>
<th>姓名</th>
<th>邮箱</th>
</tr>
</thead>
<tbody>
<tr th:each="user : ${users}">
<td th:text="${user.id}">ID</td>
<td th:text="${user.name}">Name</td>
<td th:text="${user.email}">Email</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>
user/profile.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html>
<head>用户资料</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
.profile { max-width: 400px; margin: auto; border: 1px solid #ddd;
padding: 20px; border-radius: 5px; }
.profile-item { margin-bottom: 10px; }
.label { font-weight: bold; color: #666; }
</style>
</head>
<body>
<div class="profile">
<h2>用户资料</h2>
<div class="profile-item">
<span class="label">ID:</span> ${user.id}
</div>
<div class="profile-item">
<span class="label">姓名:</span> ${user.name}
</div>
<div class="profile-item">
<span class="label">邮箱:</span> ${user.email}
</div>
<a href="/view/jsp/home">返回主页</a>
</div>
</body>
</html>
测试用例
ViewControllerTest.java
package controller;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
class ViewControllerTest {
private MockMvc mockMvc;
@BeforeEach
void setUp() {
mockMvc = MockMvcBuilders.standaloneSetup(new ViewController()).build();
}
@Test
void testJSPView() throws Exception {
mockMvc.perform(get("/view/jsp/home"))
.andExpect(status().isOk())
.andExpect(view().name("home"))
.andExpect(model().attributeExists("users"));
}
@Test
void testThymeleafView() throws Exception {
mockMvc.perform(get("/view/thymeleaf"))
.andExpect(status().isOk())
.andExpect(view().name("thymeleaf_index"));
}
@Test
void testFreemarkerView() throws Exception {
mockMvc.perform(get("/view/freemarker"))
.andExpect(status().isOk())
.andExpect(view().name("freemarker_welcome"));
}
@Test
void testJSONResponse() throws Exception {
mockMvc.perform(get("/view/json/users"))
.andExpect(status().isOk())
.andExpect(content().contentType("application/json"))
.andExpect(jsonPath("$").isArray())
.andExpect(jsonPath("$[0].name").value("张三"));
}
}
这个案例展示了Spring MVC的多种视图解析策略,包括JSP、Thymeleaf和Freemarker,以及它们的配置和使用方法,您可以根据需要选择合适的视图解析器。