Java Web连接MySQL数据库代码的演进从单例连接 → 连接池(单例+享元模式)→ Spring Boot自动配置 从JDBC手动操作 → MyBatis半自动化 → JPA全自动化

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 idleConnections; // 空闲连接

private List 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

mysql-connector-java

8.0.33

org.springframework.boot

spring-boot-starter-jdbc

org.springframework.boot

spring-boot-starter-web

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依赖:

org.projectlombok

lombok

1.18.30

provided

然后,使用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项目中:

org.mybatis.spring.boot

mybatis-spring-boot-starter

2.3.0

更新你的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/目录下):

"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

对应的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依赖:

org.springframework.boot

spring-boot-starter-data-jpa

更新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 {

// JpaRepository已经提供了基本的CRUD方法:

// save(), findById(), findAll(), deleteById(), count()等

// 方法名查询:根据方法名自动生成SQL

List findByName(String name);

List 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连接

抽象层次:每层抽象解决特定问题,让开发者更关注业务

工具选择:根据项目需求选择合适的技术组合

这个演进过程体现了软件工程的发展规律:通过抽象和封装,将复杂性隐藏在框架之下,让开发者能够以更符合直觉的方式解决问题。

相关推荐

失禁少女漫画,失禁少女最新章节
怎么无限注册365游戏账号

失禁少女漫画,失禁少女最新章节

🕒 01-28 👀 2888
剑网3怎么成为镖师_镖师任务怎么做_剑网3镖师任务玩法攻略?
怎么无限注册365游戏账号

剑网3怎么成为镖师_镖师任务怎么做_剑网3镖师任务玩法攻略?

🕒 08-11 👀 1238
为什么会呕吐
365bet体育在线主页

为什么会呕吐

🕒 10-09 👀 8241