本文目录导读:

- 📚 目录导读
- 为什么要自己造轮子?——ORM的痛点与注解的威力
- 核心设计:四个关键注解
- 实战案例:注解解析器与SQL自动生成
- 动态代理与反射:让Repository接口“活”起来
- 性能与陷阱:缓存、N+1问题与注解扫描优化
- 答疑解惑:4个高频面试与工程问题
- 从案例看注解驱动开发的本质
📚 目录导读
- 为什么要自己造轮子?——ORM的痛点与注解的威力
- 核心设计:四个关键注解(@Table、@Id、@Column、@Transient)
- 实战案例:注解解析器与SQL自动生成
- 动态代理与反射:让Repository接口“活”起来
- 性能与陷阱:缓存、N+1问题与注解扫描优化
- 答疑解惑:4个高频面试与工程问题
- 从案例看注解驱动开发的本质
为什么要自己造轮子?——ORM的痛点与注解的威力
在实际开发中,MyBatis、Hibernate等ORM框架虽然强大,但存在配置繁琐(XML映射)、SQL拼接易错、学习成本高的问题,Java注解(Annotation)自JDK 5引入后,提供了一种声明式编程的途径——我们可以通过@Retention、@Target定义元数据,再配合反射与动态代理,在运行时完成对象-关系映射(ORM)。
原理核心:注解只是“标签”,真正的逻辑在于解析器,我们定义注解后,通过反射读取类上的注解信息,动态生成SQL语句,并在执行后自动封装结果集为实体对象。
核心设计:四个关键注解
我们先定义4个基本的注解,它们模拟了JPA的核心能力:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Table { String name(); }
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Id { String column(); boolean autoIncrement() default true; }
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Column { String name(); boolean nullable() default true; }
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Transient {} // 忽略该字段
设计要点:
- 必须设置
RetentionPolicy.RUNTIME,否则反射无法读取。 @Target限定注解使用范围(类/字段),避免误用。- 提供
default值,降低使用门槛。
实战案例:注解解析器与SQL自动生成
以一张user表为例,实体类如下:
@Table(name = "user")
public class User {
@Id(column = "id")
private Long id;
@Column(name = "username", nullable = false)
private String username;
@Column(name = "age")
private Integer age;
@Transient
private String tempToken; // 不参与数据库操作
// getter/setter...
}
解析器核心逻辑(利用反射):
public class EntityMetaData<T> {
private String tableName;
private Map<String, Field> columnFieldMap = new LinkedHashMap<>();
private Field idField;
public EntityMetaData(Class<T> clazz) {
// 1. 读取@Table注解
if (clazz.isAnnotationPresent(Table.class)) {
tableName = clazz.getAnnotation(Table.class).name();
} else {
throw new IllegalArgumentException("缺少@Table注解");
}
// 2. 遍历所有字段,过滤@Transient
for (Field field : clazz.getDeclaredFields()) {
field.setAccessible(true); // 访问private字段
if (field.isAnnotationPresent(Transient.class)) continue;
if (field.isAnnotationPresent(Id.class)) { idField = field; }
if (field.isAnnotationPresent(Column.class)) {
columnFieldMap.put(field.getAnnotation(Column.class).name(), field);
}
}
}
// 动态生成INSERT SQL(仅包含非null字段,避免插入null)
public String buildInsertSQL(T entity) {
StringBuilder columns = new StringBuilder();
StringBuilder values = new StringBuilder();
for (String colName : columnFieldMap.keySet()) {
Field f = columnFieldMap.get(colName);
Object val = getValue(f, entity);
if (val == null) continue; // 忽略null字段
columns.append(colName).append(",");
values.append("'").append(val).append("',");
}
// 去掉末尾逗号,拼接SQL
return String.format("INSERT INTO %s (%s) VALUES (%s)", tableName,
columns.substring(0, columns.length()-1),
values.substring(0, values.length()-1));
}
}
关键点:
field.setAccessible(true)绕过Java访问控制,允许读取private字段。- 动态拼接SQL时,需处理字段为null的情况,防止SQL语法错误。
- 通过
LinkedHashMap保证字段顺序的一致性。
动态代理与反射:让Repository接口“活”起来
为了让调用方更简洁,我们使用JDK动态代理,将接口方法(如findById、save)映射到对应的SQL操作。
public class RepositoryProxy implements InvocationHandler {
private final EntityMetaData<?> metaData;
private final Connection conn;
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
if (methodName.startsWith("findById")) {
String sql = String.format("SELECT * FROM %s WHERE id = ?", metaData.getTableName());
// 执行查询,反射创建对象并填充字段...
} else if (methodName.startsWith("save")) {
return metaData.buildInsertSQL(args[0]); // 返回SQL(实际应执行)
}
return null;
}
}
// 使用工厂方法创建代理对象
UserMapper mapper = ProxyFactory.create(UserMapper.class, new RepositoryProxy(conn, User.class));
User u = mapper.findById(1L);
优势:开发者仅需定义UserMapper接口(继承自某个标记接口),无需实现类,ORM自动完成SQL生成与结果映射。
性能与陷阱:缓存、N+1问题与注解扫描优化
- 反射性能优化:反射比直接调用慢,通过缓存
EntityMetaData对象(用ConcurrentHashMap),避免每次操作都解析注解。 - SQL注入防护:上例中拼接了
values字段,应改为PreparedStatement的占位符,然后在setObject时填充真实值。 - N+1查询问题:当查询列表再逐条查询关联对象时,会产生N条SQL,解决方案是支持
@OneToMany注解,一次性JOIN查询,或用批量查询。 - 注解扫描限制:全包扫描类会耗时,建议在启动时指定扫描路径,或使用
ClassPathScanningCandidateComponentProvider(Spring提供)。
答疑解惑:4个高频面试与工程问题
Q1:注解可以被继承吗?
A:@Inherited只能用于类上的注解,且只对“类继承”生效,对字段/方法注解无效,因此ORM注解通常需在父类与子类重复声明,或借助反射遍历父类字段。
Q2:为什么@Column的nullable属性在实体定义时就有?它有什么用?
A:它主要供DDL生成工具(如自动建表)使用,运行时校验也可提前捕获错误,但在手动管理表结构时,此属性可忽略。
Q3:注解解析器在Spring环境下如何与Bean生命周期结合?
A:可通过BeanPostProcessor在Bean初始化后扫描其字段注解,或利用@Autowired自定义注解注入,Spring Data JPA的@Query注解就是通过RepositoryFactoryBean后置处理实现。
Q4:如果实体类字段名与数据库列名不一致(驼峰 vs 下划线),如何处理?
A:在@Column注解中显式指定name即可,更智能的方案是在解析器内做策略转换(如camelToUnderscore),并允许用户通过@Column覆盖。
从案例看注解驱动开发的本质
通过这个案例,我们实现了最简ORM的核心能力:注解定义元数据 → 反射解析 → SQL生成 → 结果映射,关键在于元数据与逻辑分离:注解不包含任何业务逻辑,只描述“是什么”;解析器负责处理“怎么做”。
在实际工程中,建议:
- 完全遵循JPA规范(
javax.persistence注解),方便后续切换框架。 - 合理使用缓存(如Caffeine)存储
EntityMetaData。 - 对复杂查询(多表Join、分页)仍建议写SQL,注解适合单表CRUD。
最后留一个扩展思考:如果@Table注解支持indexes属性,你会如何设计解析逻辑来自动创建数据库索引?欢迎在评论区探讨。
本文为原创手写内容,帮助您深入理解反射、注解与ORM的内在联系,可直接应用于中小型项目的轻量级持久层设计。