Java Web连接MySQL数据库代码的演进从单例连接 → 连接池(单例+享元模式)→ Spring Boot自动配置 从JDBC手动操作 → MyBatis半自动化 → JPA全自动化
前言
本文生动形象的描述了在 java web中连接mysql数据库以及进行数据操作(crud)的学习路线或者说技术演进和一些底层原理的理解。合适刚接触java web和软件工程系学生阅读,
本文旨在将各种技术串接起来,和一些设计模式在实际项目中的应用(包括单例模式,享元模式,建造者模式)。注意本文中的所有代码仅供参考。
第一章:单例连接的困境与TCP连接的真相
你是一名Java程序员,刚开始接触Java Web,老师布置了一个连接MySQL数据库的作业。要求很简单:编写一个能够连接到MySQL数据库的程序。
你查阅资料后了解到,Java连接数据库的标准接口是JDBC(Java Database Connectivity)。这是Java定义的一套访问数据库的通用API,让开发者可以用统一的方式连接各种数据库。
你的第一版实现很直接:
DatabaseConnector
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class DatabaseConnector {
public static void main(String[] args) {
try {
// 1. 加载JDBC驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 2. 建立数据库连接
String url = "jdbc:mysql://localhost:3306/mydatabase";
String username = "root";
String password = "123456";
Connection connection = DriverManager.getConnection(url, username, password);
System.out.println("数据库连接成功!");
// 3. 关闭连接
connection.close();
} catch (ClassNotFoundException | SQLException e) {
e.printStackTrace();
}
}
}
但老师提出了新要求:"这个连接要在整个应用中共享使用,不要每次都创建新的连接。"
你学习了设计模式后,想到了单例模式。单例模式确保一个类只有一个实例,并提供一个全局访问点。这正好符合"整个应用共享一个连接"的需求。
于是你改进了代码:
DatabaseConnection
public class DatabaseConnection {
// 单例实例
private static Connection instance;
// 私有构造方法,防止外部创建实例
private DatabaseConnection() {}
// 全局访问点
public static synchronized Connection getInstance() throws SQLException {
if (instance == null || instance.isClosed()) {
// 首次使用时创建连接
String url = "jdbc:mysql://localhost:3306/mydatabase";
String username = "root";
String password = "123456";
// 关键点:这里发生了什么?
instance = DriverManager.getConnection(url, username, password);
System.out.println("创建新的数据库连接");
}
return instance;
}
}
现在我们来深入理解 DriverManager.getConnection() 背后发生了什么:
-
TCP三次握手建立连接:
客户端(你的程序) MySQL服务器 SYN ------------> <------------ SYN-ACK ACK ------------>-
客户端发送SYN包,请求建立连接
-
服务器回应SYN-ACK包,确认请求
-
客户端发送ACK包,连接建立
-
-
MySQL协议握手:
-
身份验证:验证用户名和密码
-
协商字符集和加密方式
-
建立会话上下文
-
-
创建Connection对象:
-
JDBC驱动将TCP连接包装成Connection对象
-
这个对象维护着与数据库的会话状态
-
关键理解:每个Connection对象背后都是一个TCP连接。建立TCP连接需要网络往返时间(RTT),通常需要100-200毫秒,这是一个相当昂贵的操作。
这时你发现了问题:你的Web应用部署后,随着用户增多,系统性能急剧下降。经过分析,你发现:
-
并发访问阻塞:所有用户线程共享同一个Connection实例。当用户A在使用连接时,用户B必须等待。Connection对象的方法大多不是线程安全的,多线程并发使用会导致数据混乱。
-
连接生命周期问题:单例连接一旦建立就长期存在。如果网络中断或MySQL服务器重启,连接会失效,但单例实例不会自动重建。
-
资源利用不均:在用户访问低谷期,连接闲置;在高峰期,连接成为瓶颈。
你意识到:每个请求需要独立的数据库连接,但创建连接的开销又太大了。这个矛盾怎么解决呢?
第二章:连接池的革命性解决方案
为了解决单例连接的问题,你研究了数据库连接池技术。连接池的核心思想是:预先创建一批连接,使用时借用,用完后归还,而不是关闭。
你决定自己实现一个简易的连接池:
ConnectionPool
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
public class ConnectionPool {
// 连接池本身使用单例模式 - 整个应用只有一个连接池
private static ConnectionPool instance;
// 连接列表 - 体现享元模式的思想
private List<Connection> idleConnections; // 空闲连接
private List<Connection> activeConnections; // 使用中的连接
private static final int INITIAL_POOL_SIZE = 10;
private static final int MAX_POOL_SIZE = 20;
// 数据库连接配置
private final String url = "jdbc:mysql://localhost:3306/mydatabase";
private final String username = "root";
private final String password = "123456";
// 私有构造方法
private ConnectionPool() throws SQLException {
idleConnections = new ArrayList<>(INITIAL_POOL_SIZE);
activeConnections = new ArrayList<>();
// 初始化时创建一批连接
for (int i = 0; i < INITIAL_POOL_SIZE; i++) {
idleConnections.add(createNewConnection());
}
System.out.println("连接池初始化完成,创建了 " + INITIAL_POOL_SIZE + " 个连接");
}
// 创建新连接 - 内部方法
private Connection createNewConnection() throws SQLException {
// 底层仍然是JDBC建立TCP连接
return DriverManager.getConnection(url, username, password);
}
// 获取连接池单例
public static synchronized ConnectionPool getInstance() throws SQLException {
if (instance == null) {
instance = new ConnectionPool();
}
return instance;
}
// 获取连接
public synchronized Connection getConnection() throws SQLException {
Connection connection = null;
if (!idleConnections.isEmpty()) {
// 有空闲连接,直接取出
connection = idleConnections.remove(idleConnections.size() - 1);
} else if (activeConnections.size() < MAX_POOL_SIZE) {
// 没有空闲连接但还没达到最大连接数,创建新连接
System.out.println("连接池已满,创建新连接");
connection = createNewConnection();
} else {
// 连接池已满,等待空闲连接
System.out.println("连接池已满,等待空闲连接...");
try {
wait(1000); // 等待1秒
return getConnection(); // 重试
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new SQLException("获取连接时被中断", e);
}
}
activeConnections.add(connection);
return connection;
}
// 归还连接
public synchronized void releaseConnection(Connection connection) {
if (connection != null) {
activeConnections.remove(connection);
// 如果连接仍然有效,放回空闲池
try {
if (!connection.isClosed()) {
idleConnections.add(connection);
notifyAll(); // 通知等待连接的线程
} else {
// 连接已关闭,创建新连接替代
idleConnections.add(createNewConnection());
}
} catch (SQLException e) {
// 创建新连接替代失效连接
try {
idleConnections.add(createNewConnection());
} catch (SQLException ex) {
System.err.println("创建新连接失败: " + ex.getMessage());
}
}
}
}
}
连接池的双重设计模式
1. 外层:单例模式管理连接池
// 整个应用只有一个连接池实例
ConnectionPool pool = ConnectionPool.getInstance();
Connection conn = pool.getConnection();
// 使用连接...
pool.releaseConnection(conn);
为什么使用单例模式?
-
资源统一管理:确保所有数据库连接由一个中心点管理
-
避免资源浪费:防止创建多个连接池导致连接数失控
-
配置一致性:所有连接使用相同的配置(URL、用户名、密码等)
2. 内层:享元模式管理连接对象
享元模式的核心:共享可复用的对象,避免大量相似对象的创建开销。
在连接池中:
-
内部状态(Intrinsic State):TCP连接本身、数据库会话信息。这部分状态是昂贵的,被所有用户共享。
-
外部状态(Extrinsic State):当前执行的SQL语句、事务状态、结果集位置等。这部分状态是临时的,每次使用后会被清理。
关键理解:连接池并没有减少TCP连接的总数,而是改变了连接的生命周期。从"创建-使用-销毁"变为"创建-使用-归还-复用",大大减少了创建和销毁的频率。
第三章:现代化Java Web应用中的连接池配置
在实际的Java Web开发中,我们不需要自己实现连接池。Spring Boot框架提供了自动配置的数据库连接池。让我们看看现代应用是如何配置的。
1. 添加依赖
首先,在pom.xml中添加MySQL驱动和Spring Boot的数据库支持:
<!-- MySQL JDBC驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<!-- Spring Boot JDBC支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- Spring Boot Web支持(因为我们是Web应用) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
2. 配置连接池
在application.yml中配置数据库连接:
spring:
datasource:
# 数据库连接信息
url: jdbc:mysql://localhost:3306/mydatabase?useSSL=false&serverTimezone=UTC&characterEncoding=utf8
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
# HikariCP连接池配置(Spring Boot 2.x默认使用HikariCP)
hikari:
# 连接池名称
pool-name: MyHikariPool
# 连接池大小配置
minimum-idle: 5 # 最小空闲连接数
maximum-pool-size: 20 # 最大连接数(包括空闲和使用中的)
# 连接生命周期配置
idle-timeout: 600000 # 连接空闲超时时间(毫秒),超过此时间连接会被释放
max-lifetime: 1800000 # 连接最大生命周期(毫秒),超过此时间连接会被释放重建
connection-timeout: 30000 # 获取连接的超时时间(毫秒)
# 连接健康检查配置
connection-test-query: SELECT 1 # 连接测试查询语句
validation-timeout: 5000 # 验证连接的超时时间(毫秒)
# 其他优化配置
auto-commit: true # 自动提交事务
leak-detection-threshold: 60000 # 连接泄漏检测阈值(毫秒)
3. 应用启动时发生了什么?
当Spring Boot应用启动时:
-
自动配置:Spring Boot的
DataSourceAutoConfiguration检测到数据库依赖,自动配置数据源。 -
创建连接池:Spring Boot默认使用HikariCP(当前性能最好的连接池实现)创建连接池实例。
-
初始化连接:根据配置,创建初始数量的TCP连接。
4. 在代码中使用连接
在Spring Boot应用中,获取数据库连接非常简单:
DatabaseController
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
@RestController
public class DatabaseController {
// Spring Boot会自动注入DataSource(连接池)
@Autowired
private DataSource dataSource;
// 也可以直接使用JdbcTemplate,它内部使用DataSource
@Autowired
private JdbcTemplate jdbcTemplate;
@GetMapping("/test-connection")
public String testConnection() {
try (Connection connection = dataSource.getConnection()) {
if (connection != null && !connection.isClosed()) {
return "数据库连接成功! 连接池信息: " +
"Connection class: " + connection.getClass().getName() + ", " +
"Is valid: " + connection.isValid(2);
}
} catch (SQLException e) {
return "数据库连接失败: " + e.getMessage();
}
return "未知状态";
}
}
第四章:原始JDBC操作与Lombok简化
4.1 使用JDBC进行查询操作
有了数据库连接之后,老师布置了新的任务:查询学生信息。你决定从最基础的JDBC开始。
首先,你需要执行SQL并处理结果。基于之前的连接池,你写了这样的代码:
StudentDAO
import java.sql.*;
public class StudentDAO {
public Student findStudentById(int studentId) {
Connection connection = null;
PreparedStatement statement = null;
ResultSet resultSet = null;
Student student = null;
try {
// 1. 从连接池获取连接
connection = ConnectionPool.getInstance().getConnection();
// 2. 创建PreparedStatement(防止SQL注入)
String sql = "SELECT id, name, age, email FROM students WHERE id = ?";
statement = connection.prepareStatement(sql);
statement.setInt(1, studentId);
// 3. 执行查询
resultSet = statement.executeQuery();
// 4. 处理结果集(手动映射)
if (resultSet.next()) {
student = new Student();
student.setId(resultSet.getInt("id"));
student.setName(resultSet.getString("name"));
student.setAge(resultSet.getInt("age"));
student.setEmail(resultSet.getString("email"));
// 如果有更多字段,继续设置...
}
} catch (SQLException e) {
System.err.println("查询学生信息失败: " + e.getMessage());
} finally {
// 5. 释放资源(重要!)
try {
if (resultSet != null) resultSet.close();
if (statement != null) statement.close();
if (connection != null) ConnectionPool.getInstance().releaseConnection(connection);
} catch (SQLException e) {
System.err.println("释放资源失败: " + e.getMessage());
}
}
return student;
}
}
4.2 引入Lombok简化实体类
在写实体类时,你发现需要写大量的getter、setter、toString、equals和hashCode方法,这太繁琐了! 于是你发现了 Lombok - 一个Java库,通过注解自动生成代码。
首先,在pom.xml中添加Lombok依赖:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
然后,使用Lombok简化Student类:
Student
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
// @Data 注解:自动生成getter、setter、toString、equals和hashCode方法
@Data
// @Builder 注解:提供建造者模式,方便创建对象
@Builder
// @NoArgsConstructor 注解:生成无参构造函数(JPA要求)
@NoArgsConstructor
// @AllArgsConstructor 注解:生成全参构造函数
@AllArgsConstructor
public class Student {
private Integer id;
private String name;
private Integer age;
private String email;
}
重点:@Builder:提供建造者模式,让对象创建更灵活:
// 使用Builder创建对象
Student student = Student.builder()
.name("张三")
.age(20)
.email("zhangsan@example.com")
.build();
4.3 使用Lombok优化后的JDBC代码
现在你可以这样创建和操作Student对象:
StudentDAO
public class StudentDAO {
public Student findStudentById(int studentId) {
// ... JDBC代码 ...
if (resultSet.next()) {
// 使用构造方法创建对象
student = new Student(
resultSet.getInt("id"),
resultSet.getString("name"),
resultSet.getInt("age"),
resultSet.getString("email")
);
}
return student;
}
public void createStudent(Student student) {
// 使用Builder创建对象示例
Student newStudent = Student.builder()
.name("李四")
.age(22)
.email("lisi@example.com")
.build();
// 或者使用全参构造
Student anotherStudent = new Student(null, "王五", 23, "wangwu@example.com");
// 保存到数据库...
}
}
Lombok的优势:
-
代码简洁:减少了大量样板代码
-
维护方便:添加新字段时,不需要手动修改getter/setter
-
减少错误:自动生成的equals/hashCode更可靠
-
编译时生成:生成的代码在编译阶段,不影响运行时性能
JDBC操作的核心步骤总结:
-
获取连接:从连接池中获取一个Connection对象
-
创建Statement:使用PreparedStatement防止SQL注入
-
执行SQL:执行查询,获取ResultSet
-
处理结果:遍历ResultSet,手动映射到Java对象
-
释放资源:按顺序关闭ResultSet、Statement,归还Connection
你很快发现了问题:
-
样板代码太多:每个查询方法都要重复try-catch-finally结构
-
手动映射繁琐:ResultSet到对象的映射需要大量getXxx()调用
-
容易出错:忘记关闭资源会导致连接泄漏
-
SQL与Java代码混杂:SQL字符串拼接容易出错,且难以维护
你意识到:一定有更好的方法!
第五章:MyBatis - SQL与代码的优雅分离
为了解决JDBC的问题,你引入了MyBatis。MyBatis的核心思想是:将SQL与Java代码分离,同时提供自动的对象映射。
5.1 MyBatis的配置与使用
首先,添加MyBatis依赖到你的Spring Boot项目中:
<!-- MyBatis Spring Boot Starter -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.0</version>
</dependency>
更新你的application.yml配置,添加MyBatis相关配置:
mybatis:
# 指定Mapper XML文件的位置
mapper-locations: classpath:mappers/*.xml
# 配置类型别名包,这样在XML中可以直接使用类名而不需要全限定名
type-aliases-package: com.example.model
configuration:
# 开启下划线到驼峰的自动转换(数据库字段 user_name → Java属性 userName)
map-underscore-to-camel-case: true
# 开启二级缓存
cache-enabled: true
5.2 创建MyBatis Mapper
MyBatis有两种方式定义SQL:XML映射文件和注解。我们先看XML方式:
StudentMapper.xml(放在src/main/resources/mappers/目录下):
<?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">
<!-- namespace对应Mapper接口的全限定名 -->
<mapper namespace="com.example.mapper.StudentMapper">
<!-- resultMap定义结果集映射 -->
<resultMap id="StudentResultMap" type="Student">
<id property="id" column="id" />
<result property="name" column="name" />
<result property="age" column="age" />
<result property="email" column="email" />
<!-- 如果有复杂属性,可以继续定义 -->
</resultMap>
<!-- 查询语句 -->
<select id="findStudentById" parameterType="int" resultMap="StudentResultMap">
SELECT id, name, age, email
FROM students
WHERE id = #{id}
</select>
</mapper>
对应的Mapper接口:
package com.example.mapper;
import com.example.model.Student;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper // 告诉Spring这是MyBatis的Mapper
public interface StudentMapper {
// 根据ID查询学生
Student findStudentById(@Param("id") int studentId);
}
MyBatis的优势:
-
SQL与代码分离:SQL在XML中,便于维护和优化
-
自动映射:简化ResultSet到对象的转换
-
连接管理:自动从连接池获取/归还连接
-
保留SQL控制权:开发者仍然可以精确控制SQL
但你又发现新问题:这些基本的CRUD操作模式高度相似,为每个实体类都写一遍很繁琐。你思考:能否让框架自动处理这些通用操作?
第六章:JPA/Hibernate - 面向对象的数据库操作
为了进一步简化,你探索了JPA(Java Persistence API)。JPA是ORM(对象关系映射)的Java标准规范,Hibernate是其最流行的实现。
6.1 JPA的配置与实体定义
首先,添加JPA依赖:
<!-- Spring Data JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
更新application.yml,添加JPA配置:
spring:
jpa:
# 显示执行的SQL语句(调试用)
show-sql: true
# 自动更新数据库表结构(开发环境用,生产环境建议关闭)
hibernate:
ddl-auto: update
# 格式化SQL输出
properties:
hibernate:
format_sql: true
dialect: org.hibernate.dialect.MySQL8Dialect
6.2 创建Repository接口
Spring Data JPA最强大的特性:只需要定义接口,不需要实现!
package com.example.repository;
import com.example.model.Student;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface StudentRepository extends JpaRepository<Student, Integer> {
// JpaRepository已经提供了基本的CRUD方法:
// save(), findById(), findAll(), deleteById(), count()等
// 方法名查询:根据方法名自动生成SQL
List<Student> findByName(String name);
List<Student> findByAgeGreaterThan(int age);
}
6.3 在Service中使用JPA和Lombok
StudentService
package com.example.service;
import com.example.model.Student;
import com.example.repository.StudentRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Optional;
@Service
@Transactional
public class StudentService {
@Autowired
private StudentRepository studentRepository;
// 查询单个学生
public Student getStudentById(int id) {
// findById返回Optional,避免空指针异常
return studentRepository.findById(id)
.orElseThrow(() -> new RuntimeException("学生不存在,ID: " + id));
}
// 创建学生 - 使用Lombok的Builder
public Student createStudent(String name, Integer age, String email) {
// 使用Builder创建对象
Student student = Student.builder()
.name(name)
.age(age)
.email(email)
.build();
// JPA自动生成INSERT语句
return studentRepository.save(student);
}
// 创建学生 - 使用全参构造
public Student createStudent2(String name, Integer age, String email) {
// 使用全参构造创建对象(id为null,数据库自增)
Student student = new Student(null, name, age, email);
return studentRepository.save(student);
}
// 更新学生
public Student updateStudent(int id, String newName) {
Student student = getStudentById(id);
student.setName(newName); // 使用@Data生成的setter
// JPA自动检测到对象状态变化,自动生成UPDATE语句
return studentRepository.save(student);
}
// 使用Lombok生成的方法
public void printStudentInfo(int id) {
Student student = getStudentById(id);
// 使用@Data生成的toString方法
System.out.println("学生信息: " + student.toString());
// 使用@Data生成的getter方法
System.out.println("姓名: " + student.getName());
System.out.println("年龄: " + student.getAge());
}
}
6.4 JPA的执行原理
当调用studentRepository.findById(1)时,JPA内部发生了什么?
-
代理拦截:Spring为Repository接口创建代理,拦截方法调用
-
方法解析:解析方法名,生成对应的SQL(SELECT * FROM students WHERE id = ?)
-
获取连接:从连接池获取数据库连接(透明,开发者无需关心)
-
执行查询:通过JDBC执行生成的SQL
-
结果映射:将ResultSet自动映射为实体对象
-
管理会话:Hibernate的Session管理对象状态
-
返回结果:返回实体对象
JPA的魔法特性:
-
自动SQL生成:根据方法名自动生成SQL
-
对象状态管理:自动跟踪对象变化,自动生成UPDATE语句
-
延迟加载:关联对象按需加载
-
事务管理:声明式事务,简化编程模型
总结:从连接建立到数据操作的完整演进
-
连接建立阶段:
-
从单例连接 → 连接池(单例+享元模式)→ Spring Boot自动配置
-
-
数据操作阶段:
-
从JDBC手动操作 → MyBatis半自动化 → JPA全自动化
-
配合Lombok简化实体类和DTO
-
-
Lombok的贯穿作用:
-
在各层减少样板代码
-
提高开发效率
-
保持代码整洁
-
-
核心原则:
-
底层不变:无论上层如何封装,最终都通过JDBC建立TCP连接
-
抽象层次:每层抽象解决特定问题,让开发者更关注业务
-
工具选择:根据项目需求选择合适的技术组合
-
这个演进过程体现了软件工程的发展规律:通过抽象和封装,将复杂性隐藏在框架之下,让开发者能够以更符合直觉的方式解决问题。
浙公网安备 33010602011771号