从 Servlet 到 SSM:Java Web 进化史

目录

    • 一、为什么要懂这段"古董"历史?
    • 二、Servlet:一切的起点
      • [2.1 Servlet 是什么?](#2.1 Servlet 是什么?)
      • [2.2 第一个 Servlet 程序](#2.2 第一个 Servlet 程序)
      • [2.3 Servlet 生命周期(重点)](#2.3 Servlet 生命周期(重点))
      • [2.4 Servlet 的痛点](#2.4 Servlet 的痛点)
    • [三、JSP:让页面写起来像写 HTML](#三、JSP:让页面写起来像写 HTML)
      • [3.1 JSP 是什么?](#3.1 JSP 是什么?)
      • [3.2 JSP 基本语法](#3.2 JSP 基本语法)
      • [3.3 JSP 的九大内置对象(必背)](#3.3 JSP 的九大内置对象(必背))
      • [3.4 JSP + Servlet 的经典组合](#3.4 JSP + Servlet 的经典组合)
      • [3.5 为什么后来 JSP 也不香了?](#3.5 为什么后来 JSP 也不香了?)
    • 四、原生三层架构:手写一遍才知道痛
      • [4.1 手写 JDBC 工具类](#4.1 手写 JDBC 工具类)
      • [4.2 手写 DAO 层](#4.2 手写 DAO 层)
      • [4.3 Service 层 + 事务](#4.3 Service 层 + 事务)
      • [4.4 总结原生开发的问题](#4.4 总结原生开发的问题)
    • 五、Spring:解耦的哲学
      • [5.1 Spring 解决的核心问题](#5.1 Spring 解决的核心问题)
      • [5.2 Spring 的两大核心](#5.2 Spring 的两大核心)
      • [5.3 Spring 核心注解速查](#5.3 Spring 核心注解速查)
      • [5.4 Spring 配置示例(Java Config 方式)](#5.4 Spring 配置示例(Java Config 方式))
    • [六、Spring MVC:让 Web 开发优雅起来](#六、Spring MVC:让 Web 开发优雅起来)
      • [6.1 Spring MVC 的核心流程图(重点)](#6.1 Spring MVC 的核心流程图(重点))
      • [6.2 第一个 Spring MVC 程序](#6.2 第一个 Spring MVC 程序)
      • [6.3 常用注解详解](#6.3 常用注解详解)
      • [6.4 关于 DispatcherServlet 的真相](#6.4 关于 DispatcherServlet 的真相)
    • [七、MyBatis:告别手写 JDBC](#七、MyBatis:告别手写 JDBC)
      • [7.1 为什么用 MyBatis?](#7.1 为什么用 MyBatis?)
      • [7.2 MyBatis 快速上手](#7.2 MyBatis 快速上手)
      • [7.3 MyBatis 三大核心对象](#7.3 MyBatis 三大核心对象)
      • [7.4 MyBatis 高级特性](#7.4 MyBatis 高级特性)
    • [八、SSM 整合:三剑合璧](#八、SSM 整合:三剑合璧)
      • [8.1 整合思路(核心!)](#8.1 整合思路(核心!))
      • [8.2 完整整合配置](#8.2 完整整合配置)
      • [8.3 完整业务代码示例](#8.3 完整业务代码示例)
      • [8.4 SSM 请求全流程(串起来看)](#8.4 SSM 请求全流程(串起来看))
    • [九、从 SSM 到 Spring Boot:为什么还要进化?](#九、从 SSM 到 Spring Boot:为什么还要进化?)
    • 十、避坑指南
    • 十一、总结

一、为什么要懂这段"古董"历史?

先讲个真实的段子:

面试官:说说 Spring MVC 的请求流程。

候选人:用户请求 → DispatcherServlet → HandlerMapping → Controller → 返回视图。

面试官:那 DispatcherServlet 是什么?

候选人:是......一个 Servlet?

面试官:它为什么能拦截所有请求?web.xml 里怎么配的?Filter 和 Servlet 的区别?

候选人:......(沉默)

结论: 不懂 Servlet,你对 Spring MVC 的理解永远停留在"背八股"层面。知其然,更要知其所以然。

这篇文章会带你走完这条路:

复制代码
Servlet → JSP → MVC 分层 → 原生三层架构 → Spring → Spring MVC → MyBatis → SSM 整合

二、Servlet:一切的起点

2.1 Servlet 是什么?

通俗理解: Servlet 就是"跑在服务器上的 Java 小程序"。浏览器发请求,服务器(Tomcat)把请求交给 Servlet 处理,Servlet 处理完再把结果写回浏览器。

它是 Java 官方(Sun 公司)定义的 Java Web 规范,不是某个框架。Tomcat、Jetty 这些"Web 容器"负责实现这个规范。

2.2 第一个 Servlet 程序

步骤一:创建 Servlet 类

java 复制代码
package com.example.servlet;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

public class HelloServlet extends HttpServlet {

    // 处理 GET 请求
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {

        // 1. 设置响应类型和编码(否则中文乱码)
        resp.setContentType("text/html;charset=UTF-8");

        // 2. 获取请求参数
        String name = req.getParameter("name");
        if (name == null || name.isEmpty()) {
            name = "World";
        }

        // 3. 向浏览器输出内容
        PrintWriter out = resp.getWriter();
        out.println("<html><body>");
        out.println("<h1>Hello, " + name + "!</h1>");
        out.println("</body></html>");
        out.flush();
    }

    // 处理 POST 请求
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        // POST 请求转给 doGet 处理,保证逻辑复用
        doGet(req, resp);
    }
}

步骤二:配置 web.xml(传统方式)

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
                             http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">

    <!-- 注册 Servlet -->
    <servlet>
        <servlet-name>HelloServlet</servlet-name>
        <servlet-class>com.example.servlet.HelloServlet</servlet-class>
    </servlet>

    <!-- 映射 URL -->
    <servlet-mapping>
        <servlet-name>HelloServlet</servlet-name>
        <url-pattern>/hello</url-pattern>
    </servlet-mapping>

</web-app>

步骤三:注解方式(Servlet 3.0+)

java 复制代码
@WebServlet("/hello")  // 一行搞定,不用 web.xml
public class HelloServlet extends HttpServlet {
    // ... 同上
}

访问 http://localhost:8080/你的项目名/hello?name=张三,浏览器就会显示 "Hello, 张三!"。

2.3 Servlet 生命周期(重点)

这是面试高频题,也是理解 Spring MVC 的关键。

复制代码
        容器启动
           │
           ▼
    ┌──────────────┐
    │  1. 实例化   │  new HelloServlet()      ← 只执行1次
    └──────┬───────┘
           ▼
    ┌──────────────┐
    │  2. 初始化   │  init(ServletConfig)     ← 只执行1次
    └──────┬───────┘
           ▼
    ┌──────────────┐
    │  3. 服务     │  service() → doGet/doPost ← 每次请求执行
    └──────┬───────┘
           ▼
    ┌──────────────┐
    │  4. 销毁     │  destroy()               ← 只执行1次
    └──────────────┘
           │
           ▼
        容器关闭

关键结论:

  • Servlet 是单例的(一个 URL 对应一个 Servlet 实例)
  • init() 只执行一次 → 适合做初始化操作
  • service() 每次请求都执行 → 线程安全问题要注意! 不要用成员变量存储请求数据
java 复制代码
// ❌ 错误示范:多个用户会串数据
public class BadServlet extends HttpServlet {
    private String username;  // 危险!所有线程共享

    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        username = req.getParameter("name");  // 线程A写的,线程B可能读到
        // ...
    }
}

// ✅ 正确:用局部变量
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
    String username = req.getParameter("name");  // 局部变量,线程安全
}

2.4 Servlet 的痛点

写多了你就会发现,Servlet 有几个致命问题:

痛点 具体表现
代码重复 每个 Servlet 都要写 resp.setContentType()、读写流
输出 HTML 麻烦 用 out.println() 拼 HTML,稍微复杂点的页面就疯掉
参数解析繁琐 request.getParameter() 一个个取,还要手动转型
业务逻辑混杂 数据库、业务、页面渲染全塞在 doGet 里
配置繁琐 每个 Servlet 都要在 web.xml 注册

于是,JSP 出现了。


三、JSP:让页面写起来像写 HTML

3.1 JSP 是什么?

通俗理解: JSP(Java Server Pages)本质就是 Servlet 的马甲 。你写的 .jsp 文件,第一次被访问时会被 Tomcat 编译成一个 Servlet 的 .java 文件,再编译成 .class。

复制代码
hello.jsp  →  Tomcat 翻译  →  hello_jsp.java(一个Servlet)  →  编译  →  hello_jsp.class

3.2 JSP 基本语法

jsp 复制代码
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.util.List" %>
<%@ page import="com.example.entity.User" %>
<html>
<head><title>用户列表</title></head>
<body>

<!-- 1. 脚本片段:写 Java 代码 -->
<%
    List<User> users = (List<User>) request.getAttribute("users");
%>

<!-- 2. 表达式:输出变量 -->
<h1>共 <%= users.size() %> 位用户</h1>

<!-- 3. 声明:定义方法/变量 -->
<%!
    private String formatName(String name) {
        return name == null ? "匿名" : name;
    }
%>

<!-- 4. 循环 -->
<table border="1">
    <tr><th>ID</th><th>姓名</th><th>年龄</th></tr>
    <% for (User u : users) { %>
        <tr>
            <td><%= u.getId() %></td>
            <td><%= formatName(u.getName()) %></td>
            <td><%= u.getAge() %></td>
        </tr>
    <% } %>
</table>

</body>
</html>

3.3 JSP 的九大内置对象(必背)

对象 类型 作用域 说明
request HttpServletRequest 一次请求 请求数据
response HttpServletResponse 一次响应 响应数据
session HttpSession 一次会话 用户会话
application ServletContext 整个应用 全局数据
out JspWriter - 输出流
page Object 当前页面 相当于 this
pageContext PageContext 当前页面 页面上下文
config ServletConfig - Servlet 配置
exception Throwable - 异常信息(错误页)

3.4 JSP + Servlet 的经典组合

早期的开发模式:

java 复制代码
// UserListServlet.java --- 负责查数据
@WebServlet("/user/list")
public class UserListServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        // 1. 调用 Service 查数据
        List<User> users = userService.findAll();

        // 2. 把数据存到 request 域
        req.setAttribute("users", users);

        // 3. 转发到 JSP 页面渲染
        req.getRequestDispatcher("/WEB-INF/views/user-list.jsp")
           .forward(req, resp);
    }
}
jsp 复制代码
<%-- user-list.jsp --- 只负责展示 --%>
<c:forEach items="${users}" var="u">
    <tr>
        <td>${u.id}</td>
        <td>${u.name}</td>
    </tr>
</c:forEach>

这就是 MVC 的雏形:

  • M(Model) :User、UserService、UserDao
  • V(View) :.jsp 页面
  • C(Controller) :UserListServlet

3.5 为什么后来 JSP 也不香了?

  1. 职责混乱:JSP 里能写 Java,导致有人把业务逻辑也塞进去
  2. 前后端耦合:前端必须会 Java 才能改页面
  3. 性能问题:JSP 第一次编译慢,且不适合高并发
  4. 前后端分离浪潮:Vue/React 兴起后,后端只返回 JSON

所以现在 JSP 基本只在老项目里见了,但理解它的 MVC 思想非常关键。


四、原生三层架构:手写一遍才知道痛

在框架出现之前,我们是这样写的:

复制代码
src/
├── dao/            ← 数据访问层(Data Access Object)
│   ├── UserDao.java
│   └── impl/UserDaoImpl.java
├── service/        ← 业务逻辑层
│   ├── UserService.java
│   └── impl/UserServiceImpl.java
├── servlet/        ← 控制层
│   └── UserServlet.java
├── entity/         ← 实体类
│   └── User.java
└── util/           ← 工具类
    └── DBUtil.java

4.1 手写 JDBC 工具类

java 复制代码
public class DBUtil {
    private static final String URL = "jdbc:mysql://localhost:3306/demo";
    private static final String USER = "root";
    private static final String PASSWORD = "123456";

    static {
        try {
            Class.forName("com.mysql.cj.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            throw new RuntimeException("MySQL驱动加载失败", e);
        }
    }

    public static Connection getConnection() throws SQLException {
        return DriverManager.getConnection(URL, USER, PASSWORD);
    }

    public static void close(Connection conn, Statement stmt, ResultSet rs) {
        // 关闭顺序:后开先关
        if (rs != null) try { rs.close(); } catch (SQLException ignored) {}
        if (stmt != null) try { stmt.close(); } catch (SQLException ignored) {}
        if (conn != null) try { conn.close(); } catch (SQLException ignored) {}
    }
}

4.2 手写 DAO 层

java 复制代码
public interface UserDao {
    List<User> findAll();
    User findById(Long id);
    int insert(User user);
    int update(User user);
    int deleteById(Long id);
}

public class UserDaoImpl implements UserDao {

    @Override
    public List<User> findAll() {
        Connection conn = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        List<User> list = new ArrayList<>();
        try {
            conn = DBUtil.getConnection();
            String sql = "SELECT id, name, age FROM user";
            ps = conn.prepareStatement(sql);
            rs = ps.executeQuery();
            while (rs.next()) {
                User u = new User();
                u.setId(rs.getLong("id"));
                u.setName(rs.getString("name"));
                u.setAge(rs.getInt("age"));
                list.add(u);
            }
        } catch (SQLException e) {
            throw new RuntimeException("查询失败", e);
        } finally {
            DBUtil.close(conn, ps, rs);
        }
        return list;
    }

    // 其他方法类似......(写多了你就知道有多重复)
}

看到没? 每个 DAO 方法都是"取连接 → 写 SQL → 设参 → 执行 → 映射结果 → 关资源"这套模板,90% 的代码是重复的。这就是 MyBatis 要解决的问题。

4.3 Service 层 + 事务

java 复制代码
public class UserServiceImpl implements UserService {

    private UserDao userDao = new UserDaoImpl();

    @Override
    public void transfer(Long fromId, Long toId, BigDecimal amount) {
        Connection conn = null;
        try {
            conn = DBUtil.getConnection();
            conn.setAutoCommit(false);  // 开启事务

            // 业务逻辑
            userDao.deduct(conn, fromId, amount);
            userDao.add(conn, toId, amount);

            conn.commit();  // 提交
        } catch (Exception e) {
            try { if (conn != null) conn.rollback(); } catch (SQLException ex) { }
            throw new RuntimeException("转账失败", e);
        } finally {
            DBUtil.close(conn, null, null);
        }
    }
}

痛点: 事务的 commit / rollback 代码和业务逻辑混在一起,每个方法都要重复写。这就是 Spring AOP 声明式事务要解决的问题。

4.4 总结原生开发的问题

问题 具体表现 解决方案
DAO 重复代码多 每个方法都是 JDBC 模板 MyBatis / JPA
事务管理繁琐 手动 commit/rollback Spring AOP 事务
对象创建耦合 new UserDaoImpl() 硬编码 Spring IoC
Servlet 参数解析烦 每个方法都 getParameter Spring MVC
配置分散 web.xml、jdbc 属性散落 Spring 统一管理

于是,Spring 家族登场了。


五、Spring:解耦的哲学

5.1 Spring 解决的核心问题

Spring 最核心的两个概念:IoC (控制反转)和 AOP(面向切面)。

通俗举例:

没有 IoC 的世界:

你想喝咖啡,你得自己买咖啡豆、磨粉、烧水、冲泡。每个想喝咖啡的人都得重复这套流程。

有 IoC 的世界:

你告诉"咖啡机"(Spring 容器)"我要一杯美式",它自动帮你做好送到手上。你只管喝,不管怎么做。

代码对比:

java 复制代码
// ❌ 传统方式:自己 new,耦合严重
public class UserServiceImpl {
    private UserDao userDao = new UserDaoImpl();  // 强耦合
}

// ✅ Spring IoC:容器注入
@Service
public class UserServiceImpl {
    @Autowired
    private UserDao userDao;  // 由 Spring 容器注入
}

5.2 Spring 的两大核心

① IoC(Inversion of Control)控制反转

  • 对象的创建权 和依赖关系管理交给 Spring 容器
  • 我们只需要"声明需要什么",Spring 负责"给什么"

② AOP(Aspect Oriented Programming)面向切面

  • 把横切关注点(日志、事务、权限)从业务代码中抽离
  • 通过动态代理在方法前后"织入"增强逻辑
java 复制代码
// 一个注解搞定事务,不用写 commit/rollback
@Transactional
public void transfer(Long fromId, Long toId, BigDecimal amount) {
    userDao.deduct(fromId, amount);
    userDao.add(toId, amount);
}

5.3 Spring 核心注解速查

java 复制代码
// ===== 组件注册 =====
@Component      // 通用组件
@Service        // 业务层
@Repository     // 数据层(DAO)
@Controller     // 控制层
@RestController // = @Controller + @ResponseBody

// ===== 依赖注入 =====
@Autowired      // 按类型自动注入
@Qualifier      // 按名称指定
@Resource       // JDK 注解,按名称注入
@Value          // 注入配置文件的值

// ===== 配置类 =====
@Configuration  // 声明配置类
@Bean           // 声明一个 Bean
@ComponentScan  // 组件扫描
@Import         // 导入其他配置
@PropertySource // 加载 properties 文件

5.4 Spring 配置示例(Java Config 方式)

java 复制代码
@Configuration
@ComponentScan("com.example")
@PropertySource("classpath:db.properties")
public class AppConfig {

    @Value("${jdbc.url}")
    private String url;

    @Value("${jdbc.username}")
    private String username;

    @Value("${jdbc.password}")
    private String password;

    @Bean
    public DataSource dataSource() {
        DruidDataSource ds = new DruidDataSource();
        ds.setUrl(url);
        ds.setUsername(username);
        ds.setPassword(password);
        return ds;
    }
}

配置方式的演进:

  1. XML 配置(老) → 2. 注解 + XML 混合 → 3. 全注解 Java Config → 4. Spring Boot 自动配置

六、Spring MVC:让 Web 开发优雅起来

6.1 Spring MVC 的核心流程图(重点)

这张图请务必背下来,面试必考:

复制代码
                                    ┌─────────────────────┐
                                    │    HandlerMapping   │
                                    │  (URL → 处理方法)    │
                                    └──────────┬──────────┘
                                               │ ② 查找Handler
                                               ▼
┌────────┐  ① 请求   ┌──────────────────┐   ┌─────────────┐
│ 浏览器  │ ────────→│ DispatcherServlet │──→│  Handler    │
└────────┘          │  (前端控制器)      │   │ (Controller)│
     ▲              └────────┬─────────┘   └──────┬──────┘
     │                       │ ④ 调用            │ ③ 返回
     │                       ▼                   ▼
     │              ┌─────────────────┐   ┌──────────────┐
     │              │  ViewResolver   │←──│ ModelAndView │
     │              │ (视图解析器)     │   └──────────────┘
     │              └────────┬────────┘
     │                       │ ⑤ 解析视图
     │                       ▼
     │              ┌─────────────────┐
     │  ⑦ 响应      │      View       │
     └──────────────│    (JSP/HTML)   │
                    └─────────────────┘
                          ⑥ 渲染

七步走:

  1. 浏览器发起请求 → DispatcherServlet
  2. DispatcherServlet 问 HandlerMapping:这个 URL 谁来处理?
  3. HandlerMapping 返回 Handler(也就是 Controller 方法)
  4. DispatcherServlet 调用 HandlerAdapter 执行 Controller,得到 ModelAndView
  5. DispatcherServlet 把 ModelAndView 交给 ViewResolver
  6. ViewResolver 解析出具体的 View(比如 /WEB-INF/views/user.jsp)
  7. View 渲染后返回给浏览器

6.2 第一个 Spring MVC 程序

java 复制代码
@Controller
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    // 访问:/user/list
    @GetMapping("/list")
    public String list(Model model) {
        List<User> users = userService.findAll();
        model.addAttribute("users", users);  // 存数据到 model
        return "user-list";  // 返回视图名,ViewResolver 会解析为 /WEB-INF/views/user-list.jsp
    }

    // 访问:/user/detail/1
    @GetMapping("/detail/{id}")
    public String detail(@PathVariable Long id, Model model) {
        User user = userService.findById(id);
        model.addAttribute("user", user);
        return "user-detail";
    }

    // 访问:/user/add(表单提交)
    @PostMapping("/add")
    public String add(User user) {  // 参数自动绑定
        userService.insert(user);
        return "redirect:/user/list";  // 重定向
    }

    // 返回 JSON(前后端分离)
    @GetMapping("/api/{id}")
    @ResponseBody
    public User getById(@PathVariable Long id) {
        return userService.findById(id);  // 自动转 JSON
    }
}

6.3 常用注解详解

java 复制代码
@Controller       // 声明为控制器
@RequestMapping   // 映射路径(类级别 + 方法级别)
@GetMapping       // = @RequestMapping(method = GET)
@PostMapping      // = @RequestMapping(method = POST)
@PutMapping       // = @RequestMapping(method = PUT)
@DeleteMapping    // = @RequestMapping(method = DELETE)

@RequestParam     // 获取请求参数 ?name=xxx
@PathVariable     // 获取路径变量 /user/{id}
@RequestBody      // 获取请求体(JSON → 对象)
@ResponseBody     // 返回值转 JSON
@ModelAttribute   // 参数绑定到 Model
@SessionAttributes// 存入 Session

@RestController   // = @Controller + @ResponseBody(推荐前后端分离用)

参数获取的几种方式:

java 复制代码
@GetMapping("/demo")
public String demo(
        @RequestParam("name") String name,           // ?name=张三
        @RequestParam(value = "age", defaultValue = "18") Integer age,
        @RequestHeader("User-Agent") String ua,      // 获取请求头
        @CookieValue("JSESSIONID") String sessionId, // 获取 Cookie
        HttpServletRequest request,                   // 原生 request
        HttpServletResponse response                  // 原生 response
) {
    // ...
    return "demo";
}

6.4 关于 DispatcherServlet 的真相

还记得第一章讲的 Servlet 生命周期吗?现在回头看:

xml 复制代码
<!-- 老版 web.xml 里 Spring MVC 的配置 -->
<servlet>
    <servlet-name>dispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:spring-mvc.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>  <!-- 启动时初始化,不是第一次请求才初始化 -->
</servlet>

<servlet-mapping>
    <servlet-name>dispatcher</servlet-name>
    <url-pattern>/</url-pattern>  <!-- 拦截所有请求(除了 *.jsp) -->
</servlet-mapping>

恍然大悟: DispatcherServlet 就是一个 Servlet!只不过它很"聪明",会根据 URL 分发给不同的 Controller 方法。Spring MVC 的本质就是一个超级 Servlet + 一堆组件协同工作。


七、MyBatis:告别手写 JDBC

7.1 为什么用 MyBatis?

回到第四章那个 UserDaoImpl,你会发现所有方法都是:

复制代码
拿连接 → 写 SQL → 设参数 → 执行 → 映射结果 → 关资源

MyBatis 帮你做了 除了写 SQL 之外的所有事。

7.2 MyBatis 快速上手

① 实体类

java 复制代码
public class User {
    private Long id;
    private String name;
    private Integer age;
    private String email;
    // getter/setter/toString 省略
}

② Mapper 接口

java 复制代码
public interface UserMapper {
    List<User> findAll();
    User findById(Long id);
    int insert(User user);
    int update(User user);
    int deleteById(Long id);
}

③ Mapper XML 映射文件

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.UserMapper">

    <select id="findAll" resultType="com.example.entity.User">
        SELECT id, name, age, email FROM user
    </select>

    <select id="findById" parameterType="long" resultType="com.example.entity.User">
        SELECT id, name, age, email FROM user WHERE id = #{id}
    </select>

    <insert id="insert" parameterType="com.example.entity.User"
            useGeneratedKeys="true" keyProperty="id">
        INSERT INTO user(name, age, email) VALUES(#{name}, #{age}, #{email})
    </insert>

    <update id="update" parameterType="com.example.entity.User">
        UPDATE user SET name=#{name}, age=#{age}, email=#{email} WHERE id=#{id}
    </update>

    <delete id="deleteById" parameterType="long">
        DELETE FROM user WHERE id = #{id}
    </delete>

</mapper>

④ 全局配置文件 mybatis-config.xml

xml 复制代码
<configuration>
    <!-- 开启驼峰命名自动映射:数据库 user_name → Java userName -->
    <settings>
        <setting name="mapUnderscoreToCamelCase" value="true"/>
        <setting name="logImpl" value="STDOUT_LOGGING"/>
    </settings>

    <typeAliases>
        <package name="com.example.entity"/>
    </typeAliases>

    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/demo"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <mapper resource="mapper/UserMapper.xml"/>
    </mappers>
</configuration>

7.3 MyBatis 三大核心对象

复制代码
SqlSessionFactoryBuilder  →  读取配置,构建工厂
        ↓
SqlSessionFactory         →  工厂,全局唯一
        ↓
SqlSession                →  会话,非线程安全,每次操作新建
        ↓
     Mapper 接口           →  通过 SqlSession.getMapper() 获取

使用示例:

java 复制代码
public class MyBatisTest {
    public static void main(String[] args) throws IOException {
        // 1. 读取配置
        InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
        // 2. 构建工厂
        SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
        // 3. 获取会话
        try (SqlSession session = factory.openSession()) {
            // 4. 获取 Mapper
            UserMapper mapper = session.getMapper(UserMapper.class);
            // 5. 执行
            User user = new User();
            user.setName("张三");
            user.setAge(20);
            mapper.insert(user);
            // 6. 提交
            session.commit();
            System.out.println("插入成功,id = " + user.getId());
        }
    }
}

7.4 MyBatis 高级特性

① 动态 SQL

xml 复制代码
<select id="findByCondition" resultType="User">
    SELECT * FROM user
    <where>
        <if test="name != null and name != ''">
            AND name LIKE CONCAT('%', #{name}, '%')
        </if>
        <if test="age != null">
            AND age = #{age}
        </if>
    </where>
</select>

<update id="updateSelective">
    UPDATE user
    <set>
        <if test="name != null">name = #{name},</if>
        <if test="age != null">age = #{age},</if>
    </set>
    WHERE id = #{id}
</update>

② 结果映射(一对一、一对多)

xml 复制代码
<resultMap id="userWithOrders" type="User">
    <id column="id" property="id"/>
    <result column="name" property="name"/>
    <!-- 一对多:一个用户多个订单 -->
    <collection property="orders" ofType="Order">
        <id column="o_id" property="id"/>
        <result column="o_amount" property="amount"/>
    </collection>
</resultMap>

③ 缓存

  • 一级缓存:SqlSession 级别,默认开启
  • 二级缓存:Mapper 级别,需 <cache/> 开启

④ 分页插件 PageHelper

java 复制代码
PageHelper.startPage(1, 10);  // 第1页,每页10条
List<User> users = userMapper.findAll();
PageInfo<User> pageInfo = new PageInfo<>(users);
System.out.println("总记录数:" + pageInfo.getTotal());
System.out.println("总页数:" + pageInfo.getPages());

八、SSM 整合:三剑合璧

SSM = Spring + Spring MVC + MyBatis

8.1 整合思路(核心!)

复制代码
┌─────────────────────────────────────────────────┐
│                   Spring 容器                    │
│  ┌───────────────────────────────────────────┐  │
│  │  Spring MVC 子容器                         │  │
│  │  - Controller                             │  │
│  │  - ViewResolver                           │  │
│  │  - HandlerMapping                         │  │
│  └───────────────────────────────────────────┘  │
│  ┌───────────────────────────────────────────┐  │
│  │  父容器(Spring Root)                     │  │
│  │  - Service                                │  │
│  │  - Mapper(MyBatis)                      │  │
│  │  - DataSource                             │  │
│  │  - TransactionManager                     │  │
│  └───────────────────────────────────────────┘  │
└─────────────────────────────────────────────────┘

关键点:

  • Spring MVC 容器是 Spring 容器的子容器
  • Controller 在子容器,Service/Mapper 在父容器
  • 子容器能访问父容器的 Bean,反之不行
  • 所以:Controller 能注入 Service,Service 不能注入 Controller

8.2 完整整合配置

① Maven 依赖 pom.xml

xml 复制代码
<properties>
    <spring.version>5.3.30</spring.version>
    <mybatis.version>3.5.13</mybatis.version>
</properties>

<dependencies>
    <!-- Spring 核心 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-tx</artifactId>
        <version>${spring.version}</version>
    </dependency>

    <!-- MyBatis -->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>${mybatis.version}</version>
    </dependency>
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis-spring</artifactId>
        <version>2.1.2</version>
    </dependency>

    <!-- 数据库 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.33</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.2.20</version>
    </dependency>

    <!-- 其他 -->
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>4.0.1</version>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.15.2</version>
    </dependency>
</dependencies>

② Spring 核心配置(applicationContext.xml)

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="...">

    <!-- 1. 组件扫描(排除 Controller,让 Spring MVC 扫) -->
    <context:component-scan base-package="com.example">
        <context:exclude-filter type="annotation"
            expression="org.springframework.stereotype.Controller"/>
    </context:component-scan>

    <!-- 2. 加载数据库配置 -->
    <context:property-placeholder location="classpath:db.properties"/>

    <!-- 3. 数据源 -->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${jdbc.driver}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>

    <!-- 4. SqlSessionFactory -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="typeAliasesPackage" value="com.example.entity"/>
        <property name="mapperLocations" value="classpath:mapper/*.xml"/>
        <property name="configuration">
            <bean class="org.apache.ibatis.session.Configuration">
                <property name="mapUnderscoreToCamelCase" value="true"/>
            </bean>
        </property>
    </bean>

    <!-- 5. Mapper 扫描(自动生成 Mapper 代理) -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.example.mapper"/>
    </bean>

    <!-- 6. 事务管理器 -->
    <bean id="transactionManager"
          class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- 7. 开启注解事务 -->
    <tx:annotation-driven transaction-manager="transactionManager"/>

</beans>

③ Spring MVC 配置(spring-mvc.xml)

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="...">

    <!-- 1. 只扫描 Controller -->
    <context:component-scan base-package="com.example.controller"/>

    <!-- 2. 开启 MVC 注解驱动 -->
    <mvc:annotation-driven/>

    <!-- 3. 静态资源放行 -->
    <mvc:default-servlet-handler/>

    <!-- 4. 视图解析器 -->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/views/"/>
        <property name="suffix" value=".jsp"/>
    </bean>

</beans>

④ web.xml

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         version="4.0">

    <!-- 1. Spring 父容器 -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:applicationContext.xml</param-value>
    </context-param>
    <listener>
        <listener-class>
            org.springframework.web.context.ContextLoaderListener
        </listener-class>
    </listener>

    <!-- 2. 字符编码过滤器(解决中文乱码) -->
    <filter>
        <filter-name>encodingFilter</filter-name>
        <filter-class>
            org.springframework.web.filter.CharacterEncodingFilter
        </filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
        <init-param>
            <param-name>forceEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>encodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <!-- 3. Spring MVC 子容器(DispatcherServlet) -->
    <servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>
            org.springframework.web.servlet.DispatcherServlet
        </servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:spring-mvc.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

</web-app>

8.3 完整业务代码示例

Entity:

java 复制代码
public class User {
    private Long id;
    private String name;
    private Integer age;
    private String email;
    // getter/setter 省略
}

Mapper:

java 复制代码
public interface UserMapper {
    List<User> findAll();
    User findById(Long id);
    int insert(User user);
    int update(User user);
    int deleteById(Long id);
}

Service:

java 复制代码
@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public List<User> findAll() {
        return userMapper.findAll();
    }

    @Override
    public User findById(Long id) {
        return userMapper.findById(id);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void insert(User user) {
        userMapper.insert(user);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void transfer(Long fromId, Long toId, BigDecimal amount) {
        // 事务由 Spring AOP 管理,不用手动 commit/rollback
        // ...
    }
}

Controller:

java 复制代码
@Controller
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/list")
    public String list(Model model) {
        model.addAttribute("users", userService.findAll());
        return "user-list";
    }

    @GetMapping("/detail/{id}")
    public String detail(@PathVariable Long id, Model model) {
        model.addAttribute("user", userService.findById(id));
        return "user-detail";
    }

    @PostMapping("/add")
    public String add(User user) {
        userService.insert(user);
        return "redirect:/user/list";
    }

    @GetMapping("/delete/{id}")
    public String delete(@PathVariable Long id) {
        userService.deleteById(id);
        return "redirect:/user/list";
    }
}

8.4 SSM 请求全流程(串起来看)

复制代码
浏览器请求 /user/list
    ↓
web.xml 中 DispatcherServlet 拦截
    ↓
HandlerMapping 找到 UserController.list()
    ↓
DispatcherServlet 调用 Controller
    ↓
Controller 调用 UserService.findAll()
    ↓
Service 调用 UserMapper.findAll()
    ↓
MyBatis 执行 SQL,返回 List<User>
    ↓
Controller 把数据放入 Model
    ↓
返回视图名 "user-list"
    ↓
ViewResolver 解析为 /WEB-INF/views/user-list.jsp
    ↓
JSP 渲染,返回 HTML 给浏览器

九、从 SSM 到 Spring Boot:为什么还要进化?

SSM 虽然好用,但配置太繁琐。一个项目光 XML 就 N 个:

  • web.xml
  • applicationContext.xml
  • spring-mvc.xml
  • mybatis-config.xml
  • xxxMapper.xml
  • db.properties
  • log4j.properties
  • ......

Spring Boot 的核心思想:约定优于配置。

java 复制代码
// Spring Boot 启动类,一行搞定
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

@SpringBootApplication = @Configuration + @EnableAutoConfiguration + @ComponentScan

  • 内置 Tomcat,不用部署 war 包
  • 自动配置,不用写一堆 XML
  • starter 依赖,版本自动管理

但! Spring Boot 不是银弹,底层还是 Spring + Spring MVC + MyBatis。你不懂 SSM,用 Spring Boot 遇到问题只会抓瞎。


十、避坑指南

坑 原因 解决
中文乱码 编码不一致 过滤器 + setCharacterEncoding
404 URL 不匹配 检查 @RequestMapping 和 web.xml
500 配置错误 看堆栈,通常是 Bean 没注入
事务不生效 方法非 public / 自调用 用 public,注入自身代理
Mapper 找不到 扫描路径不对 检查 MapperScannerConfigurer
循环依赖 A 依赖 B,B 依赖 A 用 @Lazy 或重构
JSP 无法访问 放在 WEB-INF 下 通过 Controller 转发

十一、总结

从 Servlet 到 SSM,本质上是一部 "消除重复代码、降低耦合、提高开发效率" 的进化史:

复制代码
Servlet       →  处理 HTTP 请求(基础)
JSP           →  页面渲染(View)
MVC 分层      →  职责分离(思想)
原生三层      →  代码组织(实践)
Spring        →  解耦 & 事务(IoC/AOP)
Spring MVC    →  Web 层优雅化(DispatcherServlet)
MyBatis       →  持久层简化(SQL 映射)
SSM           →  三者整合(工程化)
Spring Boot   →  约定优于配置(自动化)

最后送大家一句话:

框架会过时,但思想永存。Servlet 的请求处理模型、MVC 的分层思想、Spring 的 IoC/AOP、MyBatis 的 ORM 思路------这些才是真正值得花时间理解的东西。学会了这些,无论以后出什么新框架,你都能快速上手。

相关推荐
需要8266 天前
JVM 内存区域与对象创建:一次 GC 从哪来
java·jvm·spring boot·spring·servlet·tomcat
ashiho15 天前
【Spring】RequestParam/PathVariable/RequestBody详解
java·后端·spring·servlet·springmvc
有点。16 天前
*C++哈夫曼树与哈夫曼编码
java·c++·servlet
Gl�ria19 天前
Linux 查看日志常用命令
java·linux·servlet
weixin_4407305021 天前
socket解决多次输入问题-大数据接收问题-模块动态导入
java·开发语言·servlet
evans在进步22 天前
Tomcat 整体架构与启动流程:从 Server 到 Servlet 加载
servlet·架构·tomcat
大梦想家a22 天前
基于Servlet+JSP的小区物业管理系统设计与实现(Bootstrap+MySQL)
java·servlet·bootstrap
X1A0RAN24 天前
Jenkins 流水线跨阶段的数据共享:让变量“动态更新“成为可能
java·servlet·jenkins
智能运维指南25 天前
2026研发管理平台选型指南:从“工具堆砌“到“研运一体化“的跃迁
servlet