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() 背后发生了什么

  1. TCP三次握手建立连接

    客户端(你的程序)              MySQL服务器
          SYN       ------------>
          <------------       SYN-ACK
          ACK       ------------>
    • 客户端发送SYN包,请求建立连接

    • 服务器回应SYN-ACK包,确认请求

    • 客户端发送ACK包,连接建立

  2. MySQL协议握手

    • 身份验证:验证用户名和密码

    • 协商字符集和加密方式

    • 建立会话上下文

  3. 创建Connection对象

    • JDBC驱动将TCP连接包装成Connection对象

    • 这个对象维护着与数据库的会话状态

关键理解:每个Connection对象背后都是一个TCP连接。建立TCP连接需要网络往返时间(RTT),通常需要100-200毫秒,这是一个相当昂贵的操作。

这时你发现了问题:你的Web应用部署后,随着用户增多,系统性能急剧下降。经过分析,你发现:

  1. 并发访问阻塞:所有用户线程共享同一个Connection实例。当用户A在使用连接时,用户B必须等待。Connection对象的方法大多不是线程安全的,多线程并发使用会导致数据混乱。

  2. 连接生命周期问题:单例连接一旦建立就长期存在。如果网络中断或MySQL服务器重启,连接会失效,但单例实例不会自动重建。

  3. 资源利用不均:在用户访问低谷期,连接闲置;在高峰期,连接成为瓶颈。

你意识到:每个请求需要独立的数据库连接,但创建连接的开销又太大了。这个矛盾怎么解决呢?

第二章:连接池的革命性解决方案

为了解决单例连接的问题,你研究了数据库连接池技术。连接池的核心思想是:预先创建一批连接,使用时借用,用完后归还,而不是关闭

你决定自己实现一个简易的连接池:

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应用启动时:

  1. 自动配置:Spring Boot的DataSourceAutoConfiguration检测到数据库依赖,自动配置数据源。

  2. 创建连接池:Spring Boot默认使用HikariCP(当前性能最好的连接池实现)创建连接池实例。

  3. 初始化连接:根据配置,创建初始数量的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的优势

  1. 代码简洁:减少了大量样板代码

  2. 维护方便:添加新字段时,不需要手动修改getter/setter

  3. 减少错误:自动生成的equals/hashCode更可靠

  4. 编译时生成:生成的代码在编译阶段,不影响运行时性能

JDBC操作的核心步骤总结

  1. 获取连接:从连接池中获取一个Connection对象

  2. 创建Statement:使用PreparedStatement防止SQL注入

  3. 执行SQL:执行查询,获取ResultSet

  4. 处理结果:遍历ResultSet,手动映射到Java对象

  5. 释放资源:按顺序关闭ResultSet、Statement,归还Connection

你很快发现了问题

  1. 样板代码太多:每个查询方法都要重复try-catch-finally结构

  2. 手动映射繁琐:ResultSet到对象的映射需要大量getXxx()调用

  3. 容易出错:忘记关闭资源会导致连接泄漏

  4. 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的优势

  1. SQL与代码分离:SQL在XML中,便于维护和优化

  2. 自动映射:简化ResultSet到对象的转换

  3. 连接管理:自动从连接池获取/归还连接

  4. 保留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内部发生了什么?

  1. 代理拦截:Spring为Repository接口创建代理,拦截方法调用

  2. 方法解析:解析方法名,生成对应的SQL(SELECT * FROM students WHERE id = ?)

  3. 获取连接:从连接池获取数据库连接(透明,开发者无需关心)

  4. 执行查询:通过JDBC执行生成的SQL

  5. 结果映射:将ResultSet自动映射为实体对象

  6. 管理会话:Hibernate的Session管理对象状态

  7. 返回结果:返回实体对象

JPA的魔法特性

  1. 自动SQL生成:根据方法名自动生成SQL

  2. 对象状态管理:自动跟踪对象变化,自动生成UPDATE语句

  3. 延迟加载:关联对象按需加载

  4. 事务管理:声明式事务,简化编程模型

总结:从连接建立到数据操作的完整演进

  1. 连接建立阶段

    • 从单例连接 → 连接池(单例+享元模式)→ Spring Boot自动配置

  2. 数据操作阶段

    • 从JDBC手动操作 → MyBatis半自动化 → JPA全自动化

    • 配合Lombok简化实体类和DTO

  3. Lombok的贯穿作用

    • 在各层减少样板代码

    • 提高开发效率

    • 保持代码整洁

  4. 核心原则

    • 底层不变:无论上层如何封装,最终都通过JDBC建立TCP连接

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

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

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

posted @ 2025-12-17 15:12  雨花阁  阅读(4)  评论(0)    收藏  举报