在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>
创建类User和UserDao
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步:
-
注解约定:通过@Entity、@Id、@Column定义实体与表的映射关系。
-
反射解析:SQLUtil通过反射读取注解信息,生成对应的SQL语句。
-
动态代理:DaoProxy拦截DAO接口方法调用,执行SQL并封装结果(无需手动实现DAO)。
反射、注解、动态代理是Java非常重要的特性,它们共同作为很多框架的底层实现。