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

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

一、ORM核心原理初探

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

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

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

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

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

二、miniorm框架设计与实现

2.1 项目结构规划

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

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

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

2.2.1 @Entity:实体类与表映射

java 复制代码
/**
 * 标记实体类与数据库表的映射关系,可指定表名(默认用类名)
 */
@Target(ElementType.TYPE)  // 仅作用于类
@Retention(RetentionPolicy.RUNTIME)  // 运行时保留
public @interface Entity {
    String value() default "";  // 表名,默认空(使用类名)
}

2.2.2 @Id:标记主键字段

java 复制代码
/**
 * 标记实体类的主键字段
 */
@Target(ElementType.FIELD)  // 作用于字段
@Retention(RetentionPolicy.RUNTIME)
public @interface Id {
    String generator() default "manual";  // 主键生成策略,默认手动输入
}

2.2.3 @Column:普通字段与列映射

java 复制代码
/**
 * 标记实体类字段与数据库列的映射关系,可指定列名(默认用字段名)
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Column {
    String value() default "";  // 列名,默认空(使用字段名)
}

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

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

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

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)

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生成逻辑。

java 复制代码
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接口的方法名(如insertUser、selectByIdUser),通过JDK动态代理拦截这些方法调用,自动执行对应的数据库操作。核心是实现InvocationHandler接口,重写invoke方法。

java 复制代码
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)

为了让框架使用更友好,我们封装Session和SessionFactory类,负责创建代理对象和管理会话(采用单例模式确保SessionFactory唯一)。

Session.java(会话类,获取DAO代理)

java 复制代码
/**
 * 会话类:提供获取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)       // 代理处理器
        );
    }
}

SessionFactory.java(会话工厂,单例)

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 复制代码
<?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 复制代码
<?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:

java 复制代码
@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:

java 复制代码
public interface UserDao {

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

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

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

    /**
     * 根据主键删除用户
     */
    int deleteByIdUser(Long id);
}
数据库表创建脚本
sql 复制代码
-- 创建数据库
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

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
单元测试
java 复制代码
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);
    }

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

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

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

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

测试结果

三、总结

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

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

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

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

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

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

相关推荐
ss2732 小时前
Java线程池全解:工作原理、参数调优
java·linux·python
麦麦鸡腿堡2 小时前
Java_MySQL介绍
java·开发语言·mysql
shoubepatien2 小时前
JavaWeb_Web基础
java·开发语言·前端·数据库·intellij-idea
北里闻箫2 小时前
Java spinrg 4.x 及 jsp 简单心得(PHP转JAVA视角)
java·php·jsp
Charlie_Byte2 小时前
Netty + Sa-Token 实现 WebSocket 握手认证
java·后端
计算机毕设VX:Fegn08952 小时前
计算机毕业设计|基于springboot + vue旅游信息推荐系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·课程设计·旅游
CC.GG2 小时前
【C++】红黑树
java·开发语言·c++
学IT的周星星2 小时前
java常见面试题
java·开发语言
shoubepatien2 小时前
JAVA -- 12
java·后端·intellij-idea