基于Java注解、反射与动态代理:打造简易ORM框架

209 阅读12分钟

基于Java注解、反射与动态代理:实现简易ORM框架

在Java开发中,ORM(对象关系映射)框架早已成为标配,像MyBatis、Hibernate这类成熟框架,极大地简化了数据库操作,让我们无需手动编写繁琐的SQL语句。但你是否好奇这些框架的底层实现逻辑?本文将带你从零开始,基于Java注解、反射和动态代理三大核心技术,实现一个具备基础CRUD功能的简易ORM框架(miniorm)。

一、ORM核心原理初探

ORM的核心思想是建立Java实体类与数据库表之间的映射关系,通过操作实体类来间接操作数据库。其底层依赖三大关键技术:

  • 注解:用于标记实体类与表、字段与列、主键等映射关系,是ORM框架的“配置契约”。

  • 反射:通过反射解析实体类上的注解信息,获取类名、字段名、字段值等,为SQL生成提供数据支撑。

  • 动态代理:拦截DAO层接口的方法调用,自动生成并执行对应的SQL语句,实现“无实现类却能执行数据库操作”的魔法。

miniorm框架将围绕这三大技术,实现“实体类注解映射-自动生成SQL-动态执行数据库操作”的完整链路,支持基础的插入、查询(主键)、更新、删除功能。

二、miniorm框架设计与实现

我们要实现的 miniorm 框架具备以下核心功能:

  1. 注解定义:通过注解标记实体类与数据库表的映射关系(表名、字段名、主键)。
  2. 反射解析:利用反射读取实体类的注解信息,生成 SQL 语句。
  3. 动态代理:通过动态代理实现 DAO 层的方法增强,自动执行 SQL 操作(如增、删、改、查)。
  4. 基础 CRUD:支持简单的插入、查询(主键)、更新、删除操作。

2.1 项目结构规划

先梳理清晰的项目结构,确保代码分层合理、职责明确:

2.2 第一步:定义核心注解(映射契约)

我们需要3个核心注解,分别用于标记实体类与表、主键字段、普通字段与列的映射关系。注解的保留策略必须为RUNTIME,确保运行时能通过反射获取。

2.2.1 @Entity:实体类与表映射
/**
 * 标记实体类与数据库表的映射关系,可指定表名(默认用类名)
 */
@Target(ElementType.TYPE)  // 仅作用于类
@Retention(RetentionPolicy.RUNTIME)  // 运行时保留
public @interface Entity {
    String value() default "";  // 表名,默认空(使用类名)
}
2.2.2 @Id:标记主键字段
/**
 * 标记实体类的主键字段
 */
@Target(ElementType.FIELD)  // 作用于字段
@Retention(RetentionPolicy.RUNTIME)
public @interface Id {
    String generator() default "manual";  // 主键生成策略,默认手动输入
}
2.2.3 @Column:普通字段与列映射
/**
 * 标记实体类字段与数据库列的映射关系,可指定列名(默认用字段名)
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Column {
    String value() default "";  // 列名,默认空(使用字段名)
}

2.3 第二步:数据库连接工具(JDBC+连接池)

数据库操作离不开JDBC,为了提升性能,我们使用HikariCP连接池管理连接(比原生JDBC更高效、更易维护)。创建JDBCUtil工具类,负责加载配置、获取连接、关闭资源。

2.3.1 数据库配置文件(db.properties)

# 数据库连接配置
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/miniorm?useSSL=false&serverTimezone=UTC&characterEncoding=utf8
jdbc.username=root  # 你的数据库用户名
jdbc.password=123456  # 你的数据库密码
2.3.2 JDBC工具类(DbHelper.java)
public class DbHelper {

    private static final HikariDataSource DATA_SOURCE;

    static {
        try {
            // 加载配置文件
            Properties props = new Properties();
            props.load(DbHelper.class.getClassLoader().getResourceAsStream("db.properties"));

            // 初始化HikariCP
            HikariConfig config = new HikariConfig();
            config.setJdbcUrl(props.getProperty("jdbc.url"));
            config.setUsername(props.getProperty("jdbc.username"));
            config.setPassword(props.getProperty("jdbc.password"));
            config.setDriverClassName(props.getProperty("jdbc.driver"));

            // 连接池配置(可选)
            config.setMaximumPoolSize(10);
            config.setMinimumIdle(5);

            DATA_SOURCE = new HikariDataSource(config);
        } catch (Exception e) {
            throw new RuntimeException("初始化数据库连接池失败", e);
        }
    }

    /**
     * 获取数据库连接
     */
    public static Connection getConnection() throws SQLException {
        return DATA_SOURCE.getConnection();
    }

    /**
     * 关闭连接(连接池会回收连接,此处仅关闭Statement/ResultSet)
     */
    public static void close(Connection conn, java.sql.PreparedStatement ps, java.sql.ResultSet rs) {
        try {
            if (rs != null) rs.close();
            if (ps != null) ps.close();
            if (conn != null) conn.close(); // 连接池的close是归还连接
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    /**
     * 关闭连接
     */
    public static void close(Connection conn) {
        close(conn, null, null);
    }
}

2.4 第三步:SQL生成工具(反射解析注解)

这是ORM框架的核心之一:通过反射解析实体类的注解信息,自动生成插入、查询、更新、删除的SQL语句。创建SqlUtil工具类,封装SQL生成逻辑。

SqlUtil核心功能

  1. SQL语句自动生成
  • generateInsertSql()- 生成INSERT语句
  • generateSelectByIdSql()- 根据主键生成SELECT语句
  • generateUpdateSql()- 生成UPDATE语句
  • generateDeleteByIdSql()- 根据主键生成DELETE语句
  1. 注解驱动映射

基于自定义注解进行映射配置:

  • @Entity- 标识实体类,可指定表名
  • @Id- 标识主键字段
  • @Column- 标识字段与列的映射关系

反射机制

利用Java反射动态解析类结构:

  • 获取字段信息
  • 读取注解配置
  • 动态访问私有字段值

约定优于配置

  • 表名默认使用类名
  • 列名默认使用字段名
  • 必须标注主键字段(@Id)
public class SqlUtil {

    /**
     * 生成插入SQL
     */
    public static <T> String generateInsertSql(Class<T> clazz) {
        // 1. 获取表名
        Entity entity = clazz.getAnnotation(Entity.class);
        String tableName = entity.value().isEmpty() ? clazz.getSimpleName() : entity.value();

        // 2. 解析字段和列名
        StringBuilder columns = new StringBuilder();
        StringBuilder placeholders = new StringBuilder();
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            // 跳过静态字段
            if (java.lang.reflect.Modifier.isStatic(field.getModifiers())) {
                continue;
            }

            // 获取列名
            String columnName = getColumnName(field);
            columns.append(columnName).append(",");
            placeholders.append("?,");
        }

        // 移除最后一个逗号
        columns.deleteCharAt(columns.length() - 1);
        placeholders.deleteCharAt(placeholders.length() - 1);

        // 3. 生成SQL
        return String.format("INSERT INTO %s (%s) VALUES (%s)", tableName, columns, placeholders);
    }

    /**
     * 生成根据主键查询的SQL
     */
    public static <T> String generateSelectByIdSql(Class<T> clazz) {
        // 1. 获取表名
        Entity entity = clazz.getAnnotation(Entity.class);
        String tableName = entity.value().isEmpty() ? clazz.getSimpleName() : entity.value();

        // 2. 获取主键列名
        Field idField = getIdField(clazz);
        String idColumnName = getColumnName(idField);

        // 3. 生成SQL
        return String.format("SELECT * FROM %s WHERE %s = ?", tableName, idColumnName);
    }

    /**
     * 生成更新SQL(根据主键)
     */
    public static <T> String generateUpdateSql(Class<T> clazz) {
        // 1. 获取表名
        Entity entity = clazz.getAnnotation(Entity.class);
        String tableName = entity.value().isEmpty() ? clazz.getSimpleName() : entity.value();

        // 2. 解析字段和列名(排除主键)
        StringBuilder setClause = new StringBuilder();
        Field idField = getIdField(clazz);
        String idColumnName = getColumnName(idField);

        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            if (java.lang.reflect.Modifier.isStatic(field.getModifiers()) || field.equals(idField)) {
                continue;
            }
            String columnName = getColumnName(field);
            setClause.append(columnName).append(" = ?,");
        }

        // 移除最后一个逗号
        setClause.deleteCharAt(setClause.length() - 1);

        // 3. 生成SQL
        return String.format("UPDATE %s SET %s WHERE %s = ?", tableName, setClause, idColumnName);
    }

    /**
     * 生成根据主键删除的SQL
     */
    public static <T> String generateDeleteByIdSql(Class<T> clazz) {
        // 1. 获取表名
        Entity entity = clazz.getAnnotation(Entity.class);
        String tableName = entity.value().isEmpty() ? clazz.getSimpleName() : entity.value();

        // 2. 获取主键列名
        Field idField = getIdField(clazz);
        String idColumnName = getColumnName(idField);

        // 3. 生成SQL
        return String.format("DELETE FROM %s WHERE %s = ?", tableName, idColumnName);
    }

    /**
     * 获取字段对应的列名
     */
    private static String getColumnName(Field field) {
        Column column = field.getAnnotation(Column.class);
        return column != null && !column.value().isEmpty() ? column.value() : field.getName();
    }

    /**
     * 获取主键字段
     */
    public static <T> Field getIdField(Class<T> clazz) {
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            if (field.isAnnotationPresent(Id.class)) {
                return field;
            }
        }
        throw new RuntimeException("实体类未标记@Id主键字段:" + clazz.getName());
    }

    /**
     * 获取实体类的字段值(包括私有字段)
     */
    public static Object getFieldValue(Object obj, Field field) throws IllegalAccessException {
        field.setAccessible(true);
        return field.get(obj);
    }

    /**
     * 设置实体类的字段值(包括私有字段)
     */
    public static void setFieldValue(Object obj, Field field, Object value) throws IllegalAccessException {
        field.setAccessible(true);
        field.set(obj, value);
    }

    /**
     * 获取实体类的所有字段与列名的映射
     */
    public static <T> Map<String, Field> getColumnFieldMap(Class<T> clazz) {
        Map<String, Field> map = new HashMap<>();
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            if (java.lang.reflect.Modifier.isStatic(field.getModifiers())) {
                continue;
            }
            String columnName = getColumnName(field);
            map.put(columnName, field);
        }
        return map;
    }
}

2.5 第四步:动态代理实现DAO增强

为了简化实现,通过对Dao接口的方法名进行解析,然后在动态代理中判断执行的方法名称,实现**"方法名到SQL操作的自动映射器"** - 通过解析方法名来自动执行对应的数据库CRUD操作。DaoProxy的关键就是在代理中拦截到执行的方法,然后根据实际调用的方法名判断属于什么类型的操作,最后依赖SqlUtil生成sql,并使用JDBC操作数据库。

DaoProxy类的关键功能:

  1. 动态代理模式
  • 实现InvocationHandler接口
  • 运行时为DAO接口生成代理实例
  • 方法调用被拦截并转换为数据库操作
  1. 约定优于配置

根据方法名前缀自动匹配操作类型:

  • insertXxx()→ INSERT操作
  • selectByIdXxx()→ 按ID查询
  • updateXxx()→ 更新操作
  • deleteByIdXxx()→ 按ID删除
  1. 与SqlUtil协同工作
  • 使用SqlUtil生成SQL语句
  • 利用反射进行对象-关系映射
  • 自动参数设置和结果集封装

public class DaoProxy implements InvocationHandler {

    // 实体类的Class对象
    private final Class<?> entityClass;

    public DaoProxy(Class<?> entityClass) {
        this.entityClass = entityClass;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String methodName = method.getName();
        Connection conn = null;
        PreparedStatement ps = null;
        ResultSet rs = null;

        try {
            // 获取连接
            conn = DbHelper.getConnection();

            // 处理插入操作
            if (methodName.startsWith("insert")) {
                return handleInsert(conn, ps, args[0]);
            }

            // 处理根据主键查询操作
            else if (methodName.startsWith("selectById")) {
                return handleSelectById(conn, ps, rs, args[0]);
            }

            // 处理更新操作
            else if (methodName.startsWith("update")) {
                return handleUpdate(conn, ps, args[0]);
            }

            // 处理根据主键删除操作
            else if (methodName.startsWith("deleteById")) {
                return handleDeleteById(conn, ps, args[0]);
            }

            // 未匹配的方法,执行原方法(如果有实现)
            else {
                return method.invoke(proxy, args);
            }
        } finally {
            // 每次处理结束执行连接的关闭
            DbHelper.close(conn, ps, rs);
        }
    }

    /**
     * 处理插入操作
     */
    private int handleInsert(Connection conn, PreparedStatement ps, Object entity) throws Throwable {
        String sql = SqlUtil.generateInsertSql(entityClass);
        ps = conn.prepareStatement(sql);

        // 设置参数
        Field[] fields = entityClass.getDeclaredFields();
        int paramIndex = 1;
        for (Field field : fields) {
            if (java.lang.reflect.Modifier.isStatic(field.getModifiers())) {
                continue;
            }
            Object value = SqlUtil.getFieldValue(entity, field);
            ps.setObject(paramIndex++, value);
        }

        return ps.executeUpdate();
    }

    /**
     * 处理根据主键查询操作
     */
    private Object handleSelectById(Connection conn, PreparedStatement ps, ResultSet rs, Object id) throws Throwable {
        String sql = SqlUtil.generateSelectByIdSql(entityClass);
        ps = conn.prepareStatement(sql);
        ps.setObject(1, id);
        rs = ps.executeQuery();

        if (rs.next()) {
            // 创建实体对象
            Object entity = entityClass.getDeclaredConstructor().newInstance();
            Map<String, Field> columnFieldMap = SqlUtil.getColumnFieldMap(entityClass);
            ResultSetMetaData metaData = rs.getMetaData();
            int columnCount = metaData.getColumnCount();

            // 封装结果集到实体对象
            for (int i = 1; i <= columnCount; i++) {
                String columnName = metaData.getColumnName(i);
                Field field = columnFieldMap.get(columnName);
                if (field != null) {
                    Object value = rs.getObject(columnName);
                    SqlUtil.setFieldValue(entity, field, value);
                }
            }

            return entity;
        }

        return null;
    }

    /**
     * 处理更新操作
     */
    private int handleUpdate(Connection conn, PreparedStatement ps, Object entity) throws Throwable {
        String sql = SqlUtil.generateUpdateSql(entityClass);
        ps = conn.prepareStatement(sql);

        // 设置参数(先设置普通字段,最后设置主键)
        Field idField = SqlUtil.getIdField(entityClass);
        Field[] fields = entityClass.getDeclaredFields();
        int paramIndex = 1;

        for (Field field : fields) {
            if (java.lang.reflect.Modifier.isStatic(field.getModifiers()) || field.equals(idField)) {
                continue;
            }
            Object value = SqlUtil.getFieldValue(entity, field);
            ps.setObject(paramIndex++, value);
        }

        // 设置主键参数
        Object idValue = SqlUtil.getFieldValue(entity, idField);
        ps.setObject(paramIndex, idValue);

        return ps.executeUpdate();
    }

    /**
     * 处理根据主键删除操作
     */
    private int handleDeleteById(Connection conn, PreparedStatement ps, Object id) throws Throwable {
        String sql = SqlUtil.generateDeleteByIdSql(entityClass);
        ps = conn.prepareStatement(sql);
        ps.setObject(1, id);

        return ps.executeUpdate();
    }
}

2.6 第五步:会话管理(Session与SessionFactory)

在完成了核心的SqlUtil(SQL生成)和DaoProxy(动态代理)之后,我们的框架已经具备了基本的CRUD能力。用户理论上可以直接使用DaoProxy来操作数据库,就像这样

DaoProxy userDaoProxy = new DaoProxy(User.class);
UserDao userDao = (UserDao) Proxy.newProxyInstance(...);
int result = userDao.insertUser(new User(...));

这种方式虽然能工作,但存在几个明显的痛点

  1. 直接创建DaoProxy对象对用户来说太“底层”了。用户需要了解Proxy.newProxyInstance等JDK动态代理的细节,这增加了使用门槛,且代码不直观。

  2. DaoProxy中,每个数据库操作都是独立的:getConnection()-> 执行SQL-> closeConnection()

    1. 性能低下:频繁地创建和关闭真实的数据库连接(即使有关连接池,获取和归还也有开销)。
    2. 无法实现事务:一个业务逻辑可能包含多个DAO操作(例如转账:A扣钱,B加钱)。如果每个操作独立连接,就无法保证它们在一个数据库事务中,一旦失败就会导致数据不一致。

解决方案: 引入Session作为上下文(Context)容器

Session代表一次数据库会话的概念。在一次会话内,可以共享资源(如一个数据库连接)。

// 伪代码:未来支持事务的雏形
session.beginTransaction();
try {
    UserDao userDao = session.getDao(UserDao.class, User.class);
    OrderDao orderDao = session.getDao(OrderDao.class, Order.class);
    // 多个操作共享同一个连接和事务
    userDao.update(user);
    orderDao.insert(order);
    session.commitTransaction(); // 提交事务
} catch (Exception e) {
    session.rollbackTransaction(); // 回滚事务
}
Session.java(会话类,获取DAO代理)
/**
 * 会话类:提供获取DAO代理对象的入口
 */
public class Session {
    /**
     * 获取DAO接口的动态代理对象
     * @param daoInterface DAO接口Class
     * @param entityClass 对应的实体类Class
     * @param <T> DAO接口类型
     * @return DAO代理对象
     */
    @SuppressWarnings("unchecked")
    public <T> T getDao(Class<T> daoInterface, Class<?> entityClass) {
        return (T) Proxy.newProxyInstance(
                daoInterface.getClassLoader(),  // 类加载器
                new Class[]{daoInterface},      // 要代理的接口
                new DaoProxy(entityClass)       // 代理处理器
        );
    }
}

session不需要频繁创建,引入SessionFactory作为工厂(Factory)模式的应用。

SessionFactory.java(会话工厂,单例)
/**
 * 会话工厂类:单例模式,负责创建Session对象
 */
public class SessionFactory {
    // 单例实例(饿汉式)
    private static final SessionFactory INSTANCE = new SessionFactory();
    private final Session session;

    // 私有构造器,防止外部实例化
    private SessionFactory() {
        this.session = new Session();
    }

    /**
     * 获取单例SessionFactory
     */
    public static SessionFactory getInstance() {
        return INSTANCE;
    }

    /**
     * 打开一个会话(获取Session)
     */
    public Session openSession() {
        return session;
    }
}

2.7 Maven依赖(pom.xml)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>cn.com.notnull.orm</groupId>
    <artifactId>mini-orm</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>25</maven.compiler.source>
        <maven.compiler.target>25</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.zaxxer</groupId>
            <artifactId>HikariCP</artifactId>
            <version>7.0.2</version>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <version>9.5.0</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>6.0.1</version>
            <scope>test</scope>
        </dependency>
        <!-- 简单日志框架,避免HikariCP无日志实现警告 -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>2.0.17</version>
        </dependency>
    </dependencies>
</project>

3. 测试验证

3.1 新建测试项目

使用maven(mvn install)将miniorm安装到本地仓库。

创建一个新项目:miniorm-test,引入刚刚安装到本地的miniorm。因为mini-rom已经导入mysql驱动,所以测试项目不再引入。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>cn.com.notnull.ormtest</groupId>
    <artifactId>miniorm-test</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>25</maven.compiler.source>
        <maven.compiler.target>25</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!-- 导入本地的mini-orm -->
        <dependency>
            <groupId>cn.com.notnull.orm</groupId>
            <artifactId>mini-orm</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>6.0.1</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

</project>
创建类UserUserDao

User:

@Entity("user")
public class User {

    @Id
    private Long id;

    /**
     * 数据库使用username字段,映射到Java的name字段
     */
    @Column("username")
    private String name;

    @Column
    private String password;

    @Column
    private Integer age;

UserDao:

public interface UserDao {

    /**
     * 插入用户
     */
    int insertUser(User user);

    /**
     * 根据主键查询用户
     */
    User selectByIdUser(Long id);

    /**
     * 更新用户
     */
    int updateUser(User user);

    /**
     * 根据主键删除用户
     */
    int deleteByIdUser(Long id);
}
数据库表创建脚本

-- 创建数据库
CREATE DATABASE IF NOT EXISTS miniorm;
USE miniorm;

-- 创建user表(与User实体类对应)
CREATE TABLE IF NOT EXISTS user (
    id BIGINT PRIMARY KEY,
    username VARCHAR(50) NOT NULL,
    age INT
);
创建db.properties配置文件

因为现在mini-orm默认读取类路径下的db.properties配置数据库。所在在测试项目resources目录配置创建db.properties

jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/miniorm?useSSL=false&serverTimezone=UTC&characterEncoding=utf8&allowPublicKeyRetrieval=true
jdbc.username=root
jdbc.password=root
单元测试
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class UserDaoTest {

    private static final Logger logger = LoggerFactory.getLogger(UserDaoTest.class);
    private static User user = new User(null, "Jack", "fdsjgea", 18);
    private static UserDao userDao;

    @BeforeAll
    public static void setUp() {
        Session session = SessionFactory.getInstance().openSession();
        userDao = session.getDao(UserDao.class, User.class);
    }

    @Order(1)
    @DisplayName("插入测试")
    @Test
    void insertUserTest() {
        userDao.insertUser(user);
    }

    @Order(2)
    @DisplayName("根据id查询测试")
    @Test
    void selectByIdTest() {
        User userByDb = userDao.selectByIdUser(9L);
        Assertions.assertNotNull(userByDb);
        logger.info(userByDb.toString());
    }

    @Order(3)
    @DisplayName("更新测试")
    @Test
    void updateUserTest() {
        User userByDb = userDao.selectByIdUser(9L);
        userByDb.setName("Tom");
        userDao.updateUser(userByDb);
        userByDb = userDao.selectByIdUser(9L);
        Assertions.assertEquals("Tom", userByDb.getName());
    }

    @Order(4)
    @DisplayName("根据id删除测试")
    @Test
    void deleteUserTest() {
        userDao.deleteByIdUser(9L);
        User result = userDao.selectByIdUser(9L);
        Assertions.assertNull(result);
    }
}

测试结果

三、总结

可以看到,在测试项目中只使用了一个实体类User和一个Dao接口UserDao,我们就完成了数据库的增删改查,没有编写一行Sql语句,这就是全自动ORM。

miniorm框架的完整工作流程可总结为3步:

  1. 注解约定:通过@Entity、@Id、@Column定义实体与表的映射关系。

  2. 反射解析:SQLUtil通过反射读取注解信息,生成对应的SQL语句。

  3. 动态代理:DaoProxy拦截DAO接口方法调用,执行SQL并封装结果(无需手动实现DAO)。

反射、注解、动态代理是Java非常重要的特性,它们共同作为很多框架的底层实现。