做Java Web开发,第一步往往不是学会Spring,而是先让请求能打通数据库。我这两年带新人的时候发现,很多人卡在最开始的登录注册模块上,问题不在前端页面,也不在Servlet转发,而是卡在JDBC这一层:驱动加载报ClassNotFound,连接串写错报Communications link failure,好不容易连上了又面对一堆中文乱码。其实JDBC加MySQL这套组合,就是给Java程序和数据库之间架一座桥,桥通了,后面学习MyBatis、Hibernate都只是在给这座桥装护栏、加红绿灯。
这篇是我java_web之路系列第六篇的实操总结,内容围绕Web数据库开发展开,重点解决三个问题:JDBC怎么连上MySQL、怎么写出能维护的数据库代码、真出问题时怎么排查。适合刚做完Servlet和JSP准备进入数据持久化阶段的读者,也适合想回来把JDBC基础夯实的同学。文章里的代码都是照着实际项目写法整理出来的,可以直接抄到工程里改改用。
1. 先搞清楚JDBC的价值和开发思路
1.1 JDBC到底解决什么问题
JDBC的全称是Java Database Connectivity,也就是Java数据库连接规范。它不是某个具体的数据库驱动,而是一套由Java定义的标准接口,接口放在java.sql和javax.sql这两个包里。
为什么要定义一套标准接口?因为数据库厂商太多了,MySQL、Oracle、PostgreSQL各有各的通信协议。如果Java程序员每换一种数据库就重新学一套API,这玩意根本没法在工业界铺开。JDBC的做法是:Sun公司定义接口,数据库厂商提供实现,也就是我们常说的驱动包。你在Maven里引入mysql-connector-j,本质上是引入MySQL数据库对JDBC规范的具体实现,里面封装了和MySQL服务端对话的协议细节。
从Java Web项目角度理解,JDBC是整个数据访问的最底层。Servlet收到请求后,最终要通过JDBC把SQL发给MySQL,再从ResultSet里把数据捞出来组装成对象。框架层无论包装得再花哨,底层仍然是JDBC这套东西。所以别觉得现在用MyBatis多,JDBC就可以跳过,恰恰相反,JDBC学得扎实的人,看框架源码时往往是秒懂。
1.2 JDBC的开发主线,说穿了就四步
JDBC的操作流程,概括起来就四个动作:加载驱动、获取连接、执行SQL、关闭资源。很多人一开始把这四步拆得稀碎,写出来的代码东一块西一块,连接关闭到处漏,最后数据库连接数被打满。其实这个流程有固定的章法,理解了每一步的存在意义,代码就不会乱。
加载驱动老写法是Class.forName("com.mysql.cj.jdbc.Driver"),它的作用是把驱动类注册到DriverManager里。新版的MySQL驱动其实可以省略这一步,但写上没坏处,能让代码更明确。获取连接是通过DriverManager.getConnection(url, username, password)得到一个Connection对象,这个对象代表一条到MySQL服务端的物理连接,是整个操作的基础。执行SQL是通过Connection创建Statement或者PreparedStatement,再把结果拿到ResultSet里。关闭资源更不用多说,数据库连接是稀缺资源,不关就会耗尽,最稳妥的做法是用try-with-resources,让代码块执行完自动关。
很多老项目里能看到这样的代码:
java复制Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
try {
// 获取连接并操作
} finally {
if (rs != null) rs.close();
if (stmt != null) stmt.close();
if (conn != null) conn.close();
}
这是典型的手动释放方式,能保证finally里执行关闭。但Java 7以后有了try-with-resources,写法简洁得多,实现AutoCloseable的资源会自动关闭。推荐一开始就养成这个习惯,后面接连接池也会顺很多。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备:把MySQL装到能用为止
2.1 版本选择,5.7还是8.0
热搜里一堆mysql安装教程,5.7和8.0都有。作为Java Web开发者,我的建议是别纠结,能用8.0就用8.0。MySQL 5.7确实经典,很多生产环境还在跑,但5.7的生命周期已经结束,对窗口函数、CTE这些特性的支持也不如8.0。更麻烦的是认证插件,5.7默认是mysql_native_password,8.0默认是caching_sha2_password,这直接关系到JDBC连接时的参数配置。
MySQL 8.0的默认字符集是utf8mb4,对emoji和生僻字的支持更完整。乱码问题在5.7时代非常普遍,改字符集、改连接参数,折腾半天的场景我见得太多了。8.0在这条路上走得更顺,虽然JDBC驱动版本和时区参数仍然有需要注意的地方,但整体体验比5.7好很多。
顺便说一句,等级包的事情。官方一般会继续维护最接近的版本号,5.7.44之后不会出5.7.45这种常规版本号了,原因是5.7系列已经停止维护,除非有严重安全修复才破例。所以网上看到5.7.44是最后一个正式小版本是很正常的事,生产环境如果还在用5.7,得注意安全补丁的来源。
2.2 安装方式:Windows、Linux和容器
Windows下安装MySQL有几种路子。最省事的是下载msi安装包,一路Next,中间设置root密码和安装路径。但我大概有两三年没这么装了,因为msi卸载起来麻烦,解锁选项也少。
我现在更喜欢用免安装zip包,适合想把环境掌握在自己手里的朋友。步骤很简单:去官网下载mysql-8.0.x-win64.zip,解压到一个目录,然后在目录下创建my.ini配置文件,主要写端口、数据目录、字符集这几项:
ini复制[mysqld]
port=3306
basedir=C:/mysql-8.0.40-win64
datadir=C:/mysql-8.0.40-win64/data
character-set-server=utf8mb4
[mysql]
default-character-set=utf8mb4
配好后以管理员身份打开命令行,执行mysqld --initialize-insecure,这一步会初始化数据目录,root密码初始为空。再执行mysqld --install,把MySQL注册成Windows服务,最后net start mysql启动服务。启动后执行mysql -u root -p进入客户端,用alter user 'root'@'localhost' identified by '你的密码';设置密码。
Linux上如果用rpm安装,步骤是下载rpm包,执行rpm -ivh mysql80-community-release-*.rpm,再yum install mysql-server,然后systemctl start mysqld。初次启动后日志里会有临时密码,登录进去后也是要先改密码。
Docker安装的方式后面专门有一节展开,这里先提一下,容器方式在开发机上确实很香,一条命令就有MySQL,还能随时换版本,但要注意数据卷的问题,不然容器一删数据全没。
2.3 初始化库表和账号权限
数据库装好后,别急着直接拿root连JDBC。我强烈建议建一个专用账号,权限只控制在你需要的库上。开发环境图省事可以直接用root,但一旦接进项目,root账户误操作的风险太高。
登录MySQL后执行下面这组SQL:
sql复制create database webdev default character set utf8mb4 collate utf8mb4_unicode_ci;
create user 'webuser'@'%' identified by '123456';
grant all privileges on webdev.* to 'webuser'@'%';
flush privileges;
这里有几个细节必须讲清楚。第一,数据库字符集用utf8mb4,不要用utf8,前者是真正的四字节UTF-8,能存emoji和生僻字,后者在MySQL里是utf8mb3,遇到四个字节的字符就报错。第二,创建用户时指定'webuser'@'%',意思是允许从任何主机连接,在开发环境方便,生产环境最好指定具体IP范围。第三,flush privileges是让权限修改立即生效。
建表语句我也给一个示例,后续JDBC案例会用到:
sql复制use webdev;
create table t_user (
id bigint primary key auto_increment,
username varchar(50) not null unique,
password varchar(100) not null,
nickname varchar(50) default null,
create_time datetime default current_timestamp
) engine=InnoDB default charset=utf8mb4;
InnoDB表引擎是MySQL 8.0的默认选择,事务、行级锁、外键都靠它支撑。如果你在建表语句里还写engine=MyISAM,请立刻停下来,除非你有只做只读全文搜索之类的特殊需求。
3. JDBC操作MySQL的核心代码
3.1 引入驱动包和连接参数配置
Java Web工程通常用Maven管理依赖,MySQL驱动包的坐标从8.0.31版本以后改过。老写法是:
xml复制<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
新写法是groupId换成com.mysql,artifactId换成mysql-connector-j:
xml复制<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.4.0</version>
</dependency>
两个坐标指向的驱动类是一致的,新坐标更规范,但老项目里依然大量使用老坐标。如果是从零开始,直接用新坐标。
JDBC连接URL是重点,也是报错重灾区。下面是我常用的配置:
code复制jdbc:mysql://localhost:3306/webdev?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&characterEncoding=utf8
逐个拆解一下。第一个useSSL=false,是因为MySQL 8.0默认开SSL,本地开发环境配置证书太麻烦,直接关掉省心;生产环境再用证书。第二个serverTimezone=Asia/Shanghai,这个问题在MySQL 8.0里尤其明显,服务端默认时区可能不是中国时区,JDBC驱动一看两边对不上就报错,因此要显式声明。第三个allowPublicKeyRetrieval=true,配合caching_sha2_password认证插件使用,当用SSL不可用时,驱动需要从服务端拿公钥,不设置会直接报Public Key Retrieval is not allowed。第四个characterEncoding=utf8,确保客户端发SQL和取结果时用UTF-8编码。
3.2 PreparedStatement完成增删改查
直接看一个查询用户的代码。这里我用try-with-resources,连接、PreparedStatement、ResultSet全部自动关闭,代码干净也不会漏:
java复制String sql = "select id, username, nickname, create_time from t_user where username = ?";
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, username);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
Long id = rs.getLong("id");
String name = rs.getString("username");
String nickname = rs.getString("nickname");
Timestamp createTime = rs.getTimestamp("create_time");
// 组装成User对象
}
}
} catch (SQLException e) {
throw new RuntimeException("查询用户失败", e);
}
新增用户的代码稍微复杂一点,因为通常要拿到数据库生成的自增主键。MySQL的auto_increment列插入后自动生成id,怎么拿?要在创建PreparedStatement时显式声明返回生成键:
java复制String sql = "insert into t_user(username, password, nickname) values (?, ?, ?)";
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
ps.setString(1, "zhangsan");
ps.setString(2, "123456");
ps.setString(3, "张三");
int rows = ps.executeUpdate();
try (ResultSet keys = ps.getGeneratedKeys()) {
if (keys.next()) {
Long newId = keys.getLong(1);
// newId就是刚刚插入的主键
}
}
}
更新和删除就是setString、setInt配上executeUpdate,这里不再展开。有一点要提醒,executeUpdate返回的是受影响行数,判断操作是否成功要看这个返回值,不是看有没有抛出异常。如果UPDATE语句确实执行了,但条件没有匹配到任何行,返回0不代表出错。
3.3 Statement和PreparedStatement,选哪个
这个问题的答案很明确:能用PreparedStatement就不用Statement。抛开性能不谈,单是安全问题就足够说明问题。
假设登录场景,用Statement拼接SQL:
java复制String sql = "select * from t_user where username = '" + name + "' and password = '" + pwd + "'";
如果name传入的是abc' or '1'='1,拼出来的SQL就变成:
sql复制select * from t_user where username = 'abc' or '1'='1' and password = 'xxx'
由于and优先级高于or,整体条件可能直接绕过密码校验,这也就是著名的SQL注入。PreparedStatement用问号占位符把参数和SQL结构分离:参数会被当作纯值传给数据库,不参与语法拼接。从原理上说,SQL注入对PreparedStatement是无效的。
性能上,PreparedStatement会把SQL先发给MySQL做预编译,同一SQL多次执行时不用反复解析。如果你在一个循环里执行一万次插入,用Statement和用PreparedStatement的性能差异会非常明显。当然,MySQL连接池在某些参数下还需要开启cachePrepStmts才能让预编译缓存真正生效,这点后文会提到。
3.4 事务提交与回滚,ACID是底线
Java Web里最常见的业务场景是转账:一个账户扣钱,另一个账户加钱。这个过程必须在一个事务里完成,保证原子性。JDBC默认情况下Connection是自动提交的,每执行一条SQL就立即提交。要开启事务,得先把自动提交关掉:
java复制Connection conn = null;
try {
conn = dataSource.getConnection();
conn.setAutoCommit(false);
String sqlA = "update t_account set balance = balance - 100 where id = 1";
String sqlB = "update t_account set balance = balance + 100 where id = 2";
try (PreparedStatement ps1 = conn.prepareStatement(sqlA)) {
ps1.executeUpdate();
}
// 假如这里抛异常,下面的代码不会执行
try (PreparedStatement ps2 = conn.prepareStatement(sqlB)) {
ps2.executeUpdate();
}
conn.commit();
} catch (SQLException e) {
if (conn != null) {
try {
conn.rollback();
} catch (SQLException ex) {
// 记录日志
}
}
throw new RuntimeException("转账失败", e);
} finally {
if (conn != null) {
conn.setAutoCommit(true);
conn.close();
}
}
事务的几个要素ACID:原子性保证多个操作要么全成功要么全失败,一致性保证操作前后数据符合业务规则,隔离性保证事务之间不会互相干扰,持久性保证提交后数据不丢。JDBC层面主要能控制的是原子性和隔离级别,另外两个更多靠数据库设计。
隔离级别的概念也值得一说。MySQL InnoDB默认隔离级别是REPEATABLE READ,意思是同一事务中多次查询结果一致。如果在高并发场景下追求更高性能,可以退到READ COMMITTED,很多互联网公司基于这个级别避免间隙锁带来的锁冲突。但这不是说级别越低越好,具体要看业务。
4. 把JDBC用进Java Web工程
4.1 别再把连接代码散落在Servlet里
我见过不少初学者的项目,每个Servlet里都写一遍Class.forName和DriverManager.getConnection。写完第一个Servlet能跑,写到第五个Servlet的时候数据库就开始报Too many connections。
为什么?因为每次请求都创建一个物理连接,用完关掉连接,在高并发下连接的创建和销毁代价极高。数据库连接是很重的资源,不应该每次都从零创建,更不应该让请求等连接创建。解决方案就是连接池:启动时提前创建一批连接放在池里,要用的时候从池里借,用完归还。
Java Web主流连接池有两个:HikariCP和Druid。Spring Boot 2.x以后默认用HikariCP,它的核心卖点就是快,字节码级别优化,启动耗时和连接获取速度都很优秀。Druid是阿里开源的,除了连接池功能还带监控、SQL防火墙、慢SQL日志,国内不少中型项目在用它。
HikariCP引入Maven依赖后,可以通过代码初始化数据源:
java复制import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
public class ConnectionPool {
private static final HikariDataSource DATA_SOURCE;
static {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/webdev?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true");
config.setUsername("webuser");
config.setPassword("123456");
config.setMaximumPoolSize(10);
config.setMinimumIdle(2);
config.setConnectionTimeout(30000);
config.setIdleTimeout(600000);
DATA_SOURCE = new HikariDataSource(config);
}
public static Connection getConnection() throws SQLException {
return DATA_SOURCE.getConnection();
}
}
这里有几个参数值得注意。maximumPoolSize不是越大越好,数据库本身有连接上限,MySQL默认是151,你一个应用开200个连接就是把数据库压垮的节奏。流量不大的Java Web项目,10到20个线程并发足够撑住中小规模访问。minimumIdle是池子里最少保持的空闲连接数,这个没必要和最大连接一致,避免池子空转。
4.2 用DAO模式隔离SQL操作
有了连接池,下一步是代码结构。不把SQL散落在Servlet里的另一个原因是职责分离。Servlet负责接收HTTP请求、处理参数、控制页面跳转,数据库访问应该由专门的DAO类负责。
一个标准的UserDao长这样:
java复制public class UserDao {
private static final String SELECT_BY_USERNAME =
"select id, username, nickname, create_time from t_user where username = ?";
private static final String INSERT_USER =
"insert into t_user(username, password, nickname) values (?, ?, ?)";
public User findUserByUsername(String username) {
try (Connection conn = ConnectionPool.getConnection();
PreparedStatement ps = conn.prepareStatement(SELECT_BY_USERNAME)) {
ps.setString(1, username);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
return mapToUser(rs);
}
}
return null;
} catch (SQLException e) {
throw new RuntimeException("根据用户名查询用户失败", e);
}
}
public Long insertUser(User user) {
try (Connection conn = ConnectionPool.getConnection();
PreparedStatement ps = conn.prepareStatement(INSERT_USER, Statement.RETURN_GENERATED_KEYS)) {
ps.setString(1, user.getUsername());
ps.setString(2, user.getPassword());
ps.setString(3, user.getNickname());
ps.executeUpdate();
try (ResultSet keys = ps.getGeneratedKeys()) {
if (keys.next()) {
return keys.getLong(1);
}
}
return null;
} catch (SQLException e) {
throw new RuntimeException("新增用户失败", e);
}
}
private User mapToUser(ResultSet rs) throws SQLException {
User user = new User();
user.setId(rs.getLong("id"));
user.setUsername(rs.getString("username"));
user.setNickname(rs.getString("nickname"));
user.setCreateTime(rs.getTimestamp("create_time"));
return user;
}
}
这种写法有几个好处。SQL常量集中在一起,改动一眼能看到;mapToUser方法把结果集映射逻辑收拢,后面加字段只改一处;异常抛成RuntimeException,由上层统一处理,不会逼着Servlet去处理底层SQLException。
这里我还想提一个很多人忽略的点:ResultSet映射时,getXxx方法既可以用列索引,也可以用列别名。如果SQL里用了别名,比如select username as name,那么rs.getString("name")对应的就是别名。这个细节在写复杂报表SQL时特别有用。
4.3 Servlet加DAO,一个可跑的登录案例
把上面这些组装起来,就是一个完整的Java Web登录功能。Servlet层只处理HTTP相关的事,不再碰JDBC:
java复制@WebServlet("/login")
public class LoginServlet extends HttpServlet {
private final UserDao userDao = new UserDao();
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String username = req.getParameter("username");
String password = req.getParameter("password");
User user = userDao.findUserByUsername(username);
if (user != null && user.getPassword().equals(
DigestUtils.md5DigestAsHex(password.getBytes()))) {
req.getSession().setAttribute("loginUser", user);
resp.sendRedirect("welcome.jsp");
} else {
req.setAttribute("error", "用户名或密码错误");
req.getRequestDispatcher("login.jsp").forward(req, resp);
}
}
}
这段代码的密码校验用的是明文和目标比对,我这里只是示范流程,真实项目要加盐哈希或使用BCrypt。为什么要用Session存登录状态?因为HTTP请求是无状态的,这次请求处理完,服务器就忘了你是谁。Session把用户信息放在服务端,浏览器靠Cookie里的JESSIONID找到对应会话,这是Java Web最经典的会话跟踪方式。
等到后面学了Filter,还可以在这里加登录拦截:没有Session就跳回登录页,不用每个Servlet都写一遍判断。
5. 高频报错、排查思路和避坑宝典
5.1 连接失败类异常的排查清单
JDBC连接MySQL的报错五花八门,但只要理解每一步的依赖关系,排查起来就有章法。我把最常见的几类列成一张表:
| 报错信息 | 原因 | 解决方法 |
|---|---|---|
| ClassNotFoundException: com.mysql.jdbc.Driver | 缺少驱动包,或驱动类名写错 | 确认Maven依赖存在,驱动类名是com.mysql.cj.jdbc.Driver |
| Communications link failure | 网络不通、端口错误、MySQL未启动 | 检查MySQL服务状态,ping端口,确认连接地址 |
| Access denied for user | 用户名密码错误或账号没有远端权限 | 检查用户名密码,确认这个账号能从这个主机连接 |
| Unknown database 'webdev' | 连接串里数据库名不存在 | 先到MySQL里创建库 |
| Public Key Retrieval is not allowed | MySQL 8.0认证插件导致 | 在连接串加allowPublicKeyRetrieval=true |
| The server time zone value is unrecognized | 时区配置不一致 | 在连接串加serverTimezone=Asia/Shanghai |
有个技巧很好用:遇到连接问题,先排除MySQL服务本身。登录MySQL命令行执行一条select 1,如果登录和查询都正常,说明服务没问题,问题出在Java端配置。接着检查连接串,一项项对照上面的表格。
5.2 时区、字符集和SSL的经典大坑
时区问题在MySQL 8.0时代尤其普遍。MySQL 8.0默认使用系统时区,如果服务端是UTC,Java应用的默认时区是东八区,两边一比较,驱动就报time zone value is unrecognized。解决办法很简单,连接串加serverTimezone=Asia/Shanghai。
字符集的问题也不容忽视。数据库要建库时用utf8mb4,表也要继承,连接串再加characterEncoding=utf8,这一步缺一不可。我遇到过数据库建表用utf8,连接串没配字符集,插入中文后变成问号的情况。排查顺序是从连接串看起,再到数据库和表的字符集设置。
SSL报错是另一个高频问题。MySQL 8.0默认启用SSL,客户端驱动默认也会尝试用SSL连接。本地开发环境没有配证书,经常看到SSLHandshakeException之类的错误。最简单的处置是在连接串末尾加useSSL=false。有安全要求的项目,生产环境建议还是走内网配置,或者把SSL证书做完整。
5.3 锁表和死锁,MySQL事务里的时间黑洞
热搜里很多关于mysql锁分类、锁表的问题。InnoDB支持行级锁,但只在查询能命中索引的时候才有效。如果UPDATE或DELETE语句的WHERE条件没有走索引,MySQL要对全表扫描才能定位记录,这时候行锁就可能升级成表锁。表锁会让其他事务干等着,表现就是数据库突然变慢,请求堆积。
实际排查可以这样来。第一步看当前事务和锁等待:
sql复制select * from information_schema.innodb_trx;
select * from information_schema.innodb_lock_waits;
第二步除了历史以外,用explain看执行计划:
sql复制explain select * from t_user where username = 'admin';
如果type列是ALL,说明全表扫描没走索引,就该考虑给username加索引:
sql复制create index idx_username on t_user(username);
死锁的排查思路也类似。两个事务各自持有一把锁,又同时在等对方手里的锁,就会形成循环等待。InnoDB检测到死锁会回滚其中一方,这时业务层会收到Deadlock found的异常。避免死锁的办法是让事务访问资源尽量保持固定顺序,比如A事务先更user再更order,B事务也先更user再更order,而不是反着来。
5.4 Docker装载MySQL时的注意事项
Docker开发MySQL确实方便,一个问题就是拉镜像失败,或者拉下来启动后连不上。很多人在本机用Docker Desktop,镜像源默认在国外,可以配置国内镜像加速。这个不同环境的配置路径不一样,大家根据当前版本在设置里找到Registry mirrors加一条源地址就行。
启动命令我建议这么写:
bash复制docker run -d \
--name mysql8 \
-p 3306:3306 \
-e MYSQL_ROOT_PASSWORD=root \
-e TZ=Asia/Shanghai \
-v /data/mysql8:/var/lib/mysql \
mysql:8.0
这里有几个细节。第一个是-v数据卷,把容器里的/var/lib/mysql挂到宿主机目录,不然容器一删,数据库文件全没。第二个是TZ环境变量,设置成Asia/Shanghai,否则容器内时区是UTC,JDBC连接时又得处理时间偏移。第三个是-p端口映射,宿主机3306映射到容器3306,映射前先确认宿主机3306没被占用。
Docker方式还有个优点是可以随便换版本,想试MySQL 8.0就拉8.0的镜像,想试5.7就拉5.7,数据卷不冲突就行。不过要注意,不同大版本的数据库文件格式不兼容,不能把8.0的数据目录直接挂给5.7用。
写到这里,我不打算来一段标准总结。这个系列还在继续,下一篇大概率会往Servlet生命周期和JSP调优方向走。我实际带项目的体会是,JDBC这层写顺了,后面学MyBatis的时候你会一眼看懂那些Mapper XML的来龙去脉;踩过连接池和事务的坑,才会明白框架里那些Transactional注解到底在忙着做什么。如果看完这篇你能从零搭一个MySQL库、写完一个登录注册案例,这个技能点就真正焊死在身上了。
