目录
-
- 一、为什么要懂这段"古董"历史?
- 二、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 也不香了?
- 职责混乱:JSP 里能写 Java,导致有人把业务逻辑也塞进去
- 前后端耦合:前端必须会 Java 才能改页面
- 性能问题:JSP 第一次编译慢,且不适合高并发
- 前后端分离浪潮: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;
}
}
配置方式的演进:
- 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) │
└─────────────────┘
⑥ 渲染
七步走:
- 浏览器发起请求 →
DispatcherServlet DispatcherServlet问HandlerMapping:这个 URL 谁来处理?HandlerMapping返回Handler(也就是 Controller 方法)DispatcherServlet调用HandlerAdapter执行 Controller,得到ModelAndViewDispatcherServlet把ModelAndView交给ViewResolverViewResolver解析出具体的View(比如/WEB-INF/views/user.jsp)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 思路------这些才是真正值得花时间理解的东西。学会了这些,无论以后出什么新框架,你都能快速上手。