回味SpringMVC——在前后端分离的时代,你还答出来V的详情么?

114 阅读2分钟

一、前后端分离 vs 传统MVC模式对比

1.1 数据流向差异

graph TD
    subgraph 前后端分离
    A[浏览器] --> B[前端服务器] -->|Ajax| C[Spring API]
    C -->|JSON| B -->|组装页面| A
    end
    
    subgraph 传统MVC
    D[浏览器] --> E[Spring MVC]
    E -->|处理请求| F[模板引擎]
    F -->|HTML| D
    end

1.2 核心组件差异

组件前后端分离传统MVC
返回类型@RestController@Controller
视图解析器ViewResolver
数据载体ResponseEntityModel
响应内容JSON/XMLHTML

二、传统MVC请求全流程(重点视图阶段)

2.1 完整处理流程

sequenceDiagram
    participant Browser
    participant DispatcherServlet
    participant HandlerMapping
    participant HandlerAdapter
    participant Controller
    participant ViewResolver
    participant View
    
    Browser->>DispatcherServlet: 1. 发起请求
    DispatcherServlet->>HandlerMapping: 2. 查找Handler
    HandlerMapping-->>DispatcherServlet: 返回HandlerChain
    DispatcherServlet->>HandlerAdapter: 3. 适配执行
    HandlerAdapter->>Controller: 4. 调用控制器方法
    Controller->>HandlerAdapter: 5. 返回ModelAndView
    HandlerAdapter-->>DispatcherServlet: 返回ModelAndView
    DispatcherServlet->>ViewResolver: 6. 解析视图名称
    ViewResolver-->>DispatcherServlet: 返回View对象
    DispatcherServlet->>View: 7. 渲染视图
    View-->>DispatcherServlet: 生成HTML
    DispatcherServlet-->>Browser: 8. 返回响应

2.2 关键阶段详解(视图相关)

阶段6:视图解析(ViewResolver)

// 伪代码:DispatcherServlet中处理视图解析
protected void render(ModelAndView mv, HttpServletRequest request, 
                     HttpServletResponse response) throws Exception {
    // 解析视图名称
    View view;
    if (mv.isReference()) {
        view = resolveViewName(mv.getViewName(), mv.getModelInternal(), locale);
    }
    
    // 渲染视图
    view.render(mv.getModelInternal(), request, response);
}

阶段7:视图渲染(View)

// 以JSP视图为例
public class InternalResourceView extends AbstractUrlBasedView {
    protected void renderMergedOutputModel(
        Map<String, Object> model, 
        HttpServletRequest request,
        HttpServletResponse response) throws Exception {
        
        // 将模型数据暴露为请求属性
        exposeModelAsRequestAttributes(model, request);
        
        // 转发到JSP页面
        RequestDispatcher rd = request.getRequestDispatcher(getUrl());
        rd.forward(request, response);
    }
}

三、代码实战:传统MVC开发模式

3.1 控制器写法对比

// 前后端分离写法
@RestController
public class ProductApiController {
    @GetMapping("/api/products")
    public List<Product> listProducts() {
        return productService.findAll();
    }
}

// 传统MVC写法
@Controller
public class ProductViewController {
    @GetMapping("/products")
    public String listProducts(Model model) {
        model.addAttribute("products", productService.findAll());
        return "product/list"; // 视图名称
    }
}

3.2 视图层三要素配置

3.2.1 视图解析器配置(JSP示例)

@Configuration
@EnableWebMvc
public class MvcConfig implements WebMvcConfigurer {
    
    @Bean
    public ViewResolver viewResolver() {
        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
        resolver.setPrefix("/WEB-INF/views/");
        resolver.setSuffix(".jsp");
        return resolver;
    }
}

3.2.2 JSP页面示例

<%@ page contentType="text/html;charset=UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
    <title>产品列表</title>
</head>
<body>
    <h1>所有产品</h1>
    <table>
        <tr>
            <th>ID</th>
            <th>名称</th>
            <th>价格</th>
        </tr>
        <c:forEach items="${products}" var="product">
            <tr>
                <td>${product.id}</td>
                <td>${product.name}</td>
                <td>¥<fmt:formatNumber value="${product.price}" pattern="#,##0.00"/></td>
            </tr>
        </c:forEach>
    </table>
</body>
</html>

3.2.3 文件结构

src/main/webapp
└── WEB-INF
    └── views
        └── product
            └── list.jsp

四、核心机制深度解析

4.1 模型数据传递原理

classDiagram
    class Model {
        <<interface>>
        +addAttribute(String name, Object value) Model
        +addAttribute(Object value) Model
        +mergeAttributes(Map<String, ?> attributes) Model
        +asMap() Map<String,Object>
    }
    
    class BindingAwareModelMap {
        -target: Map<String, Object>
        +addAttribute(String name, Object value)
        +getAttribute(String name) Object
    }
    
    Model <|.. BindingAwareModelMap

4.2 视图解析器链

// DispatcherServlet中的视图解析逻辑
protected View resolveViewName(String viewName, Map<String, Object> model,
                              Locale locale, HttpServletRequest request) {
    
    for (ViewResolver viewResolver : this.viewResolvers) {
        View view = viewResolver.resolveViewName(viewName, locale);
        if (view != null) {
            return view;
        }
    }
    return null;
}

4.3 视图渲染过程

  1. 模型数据转换:将Model中的属性设置到Request作用域
  2. 视图定位:根据视图解析器的前缀/后缀找到物理文件
  3. 模板处理:执行JSP编译或Thymeleaf渲染
  4. 输出响应:将生成的HTML写入HttpServletResponse

五、高级视图技术集成

5.1 Thymeleaf整合示例

@Configuration
public class ThymeleafConfig {
    
    @Bean
    public SpringResourceTemplateResolver templateResolver() {
        SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
        resolver.setPrefix("classpath:/templates/");
        resolver.setSuffix(".html");
        resolver.setTemplateMode(TemplateMode.HTML);
        return resolver;
    }
    
    @Bean 
    public SpringTemplateEngine templateEngine() {
        SpringTemplateEngine engine = new SpringTemplateEngine();
        engine.setTemplateResolver(templateResolver());
        engine.addDialect(new Java8TimeDialect());
        return engine;
    }
    
    @Bean
    public ThymeleafViewResolver viewResolver() {
        ThymeleafViewResolver resolver = new ThymeleafViewResolver();
        resolver.setTemplateEngine(templateEngine());
        resolver.setCharacterEncoding("UTF-8");
        return resolver;
    }
}

5.2 复杂数据展示

<!-- Thymeleaf模板示例 -->
<table>
    <tr th:each="prod : ${products}" th:class="${prodStat.odd}? 'odd'">
        <td th:text="${prod.name}">默认产品名</td>
        <td th:text="${#numbers.formatDecimal(prod.price, 1, 2)}">0.00</td>
        <td>
            <span th:switch="${prod.status}">
                <span th:case="0" class="text-muted">已下架</span>
                <span th:case="1" class="text-success">在售</span>
            </span>
        </td>
    </tr>
</table>

六、常见问题解决方案

6.1 视图找不到(404错误)

排查步骤

  1. 检查视图解析器的前缀/后缀配置
  2. 确认返回的视图名称是否匹配模板文件名
  3. 查看控制台是否有模板编译错误

6.2 模型数据未显示

解决方案

// 确保使用Model.addAttribute()
@GetMapping("/detail")
public String productDetail(Model model) {
    Product product = productService.findById(1L);
    model.addAttribute("product", product); // 正确
    // model.addAttribute(product); // 自动根据类型命名(首字母小写)
    return "product/detail";
}

6.3 静态资源加载失败

@Configuration
public class WebConfig implements WebMvcConfigurer {
    
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/static/**")
                .addResourceLocations("classpath:/static/");
    }
}

七、从理论到实践:完整案例

7.1 表单提交处理

@Controller
public class ProductController {
    
    @GetMapping("/products/new")
    public String showForm(Model model) {
        model.addAttribute("product", new Product());
        return "product/form";
    }
    
    @PostMapping("/products")
    public String createProduct(@ModelAttribute Product product, 
                               BindingResult result) {
        if (result.hasErrors()) {
            return "product/form";
        }
        productService.save(product);
        return "redirect:/products";
    }
}

7.2 表单页面(Thymeleaf)

<form th:action="@{/products}" method="post" th:object="${product}">
    <div>
        <label>产品名称:</label>
        <input type="text" th:field="*{name}">
        <span th:if="${#fields.hasErrors('name')}" 
              th:errors="*{name}">名称错误提示</span>
    </div>
    <div>
        <label>价格:</label>
        <input type="number" step="0.01" th:field="*{price}">
    </div>
    <button type="submit">提交</button>
</form>

总结:视图层的核心价值

通过传统MVC模式开发,可以实现:

  1. 快速原型开发:前后端协同在同一个工程中
  2. SEO友好:服务端渲染HTML对搜索引擎更友好
  3. 渐进增强:可逐步引入前端框架
  4. 完整功能支持:表单处理、验证、国际化等开箱即用

理解视图层的工作原理,是掌握传统Web开发模式的关键。建议从简单JSP开始实践,再逐步过渡到Thymeleaf等现代模板引擎,最终形成完整的MVC开发能力。