news 2026/10/21 8:59:50

手写ORM框架:深入理解Java注解与反射的底层原理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
手写ORM框架:深入理解Java注解与反射的底层原理

1. 为什么注解和反射是绕不开的坎

刚入行那会儿,我最怕看框架源码。打开一个Spring的类,满屏的@Autowired、@Component、@RequestMapping,心里就一个念头:这些带@符号的东西到底在干嘛?为什么加个注解,对象就自动注入了?为什么写个@RequestMapping("/user"),浏览器就能访问到对应方法?

后来被逼着写了一个简易的ORM框架,才真正把注解和反射这两块硬骨头啃下来。说实话,注解和反射单独拎出来都不算特别复杂,但它们组合在一起,就是几乎所有Java框架的底层基石。你用的Spring、MyBatis、Hibernate、JUnit,甚至Lombok,底层都离不开这两个东西。

这篇文章我打算换个讲法。不按教科书那种“先讲定义再讲语法”的顺序来,而是直接从一个真实需求出发:手写一个能用的ORM框架。在实现过程中,需要用到注解的地方就讲注解,需要用到反射的地方就讲反射。这样学下来,你不仅知道它们是什么,更知道它们能干什么、为什么要这么设计。

适合谁看?如果你已经会写Java基础代码,但对注解和反射一直停留在“知道有这么个东西”的阶段,或者看框架源码时总觉得隔了一层纱,那这篇内容应该能帮你把这层纱捅破。如果你完全没接触过Java,建议先补一下基础语法再来看。

提醒:文中的代码示例为了演示核心逻辑,做了适当简化。实际生产环境的ORM框架要考虑连接池、缓存、事务、SQL注入防护等大量细节,不要直接拿demo上生产。

2. 注解的本质:给代码贴标签的学问

2.1 注解不是魔法,它只是一份元数据

很多人第一次接触注解,会觉得这东西很神秘。一个@Override加在方法上,编译器就能帮你检查方法签名对不对;一个@Deprecated加在类上,IDE就会给你画一条删除线。看起来像是注解本身在“执行”某些逻辑,但实际上,注解自己什么都不做。

注解的本质是元数据,也就是“描述数据的数据”。它就像你给一个纸箱贴的标签,标签上写着“易碎品”“向上”“防潮”。标签本身不会搬箱子,也不会保护箱子里的东西,它只是提供信息。真正起作用的是看到标签的人——搬运工会根据“易碎品”标签轻拿轻放,仓库管理员会根据“向上”标签调整堆放方向。

在Java里,这个“看到标签的人”就是注解处理器。编译器看到@Override会做检查,框架看到@Component会创建Bean,JUnit看到@Test会执行对应方法。注解只是信息载体,处理逻辑是另外写的。

理解这一点非常关键。很多初学者会问“为什么我加了这个注解没效果”,原因往往就是:没有对应的处理器去读取这个注解。你贴了标签,但没人看,自然不会有任何反应。

2.2 元注解:定义注解的注解

Java内置了几个元注解,专门用来定义其他注解。你可以把它们理解为“注解的注解”。最常用的四个:

  • @Target:指定这个注解可以用在什么地方。比如只能用在方法上、只能用在类上、可以用在字段上等。
  • @Retention:指定这个注解保留到什么时候。是只在源码中保留(编译后丢弃),还是保留到class文件中(JVM运行时可以读取),还是保留到运行时。
  • @Documented:指定这个注解是否包含在Javadoc中。
  • @Inherited:指定这个注解是否能被子类继承。

其中@Retention是最关键的。它有三个取值:

取值含义典型用途
SOURCE只在源码中保留,编译后丢弃@Override、@SuppressWarnings
CLASS保留到class文件,但JVM运行时不可见一些编译期处理工具的注解
RUNTIME保留到运行时,JVM可以通过反射读取Spring、MyBatis等框架的注解

只有RUNTIME级别的注解,才能被反射读取到。这是手写ORM框架时必须注意的第一件事。如果你定义了一个注解但忘了加@Retention(RetentionPolicy.RUNTIME),反射读取时就会得到null,然后你会花大量时间排查一个其实很简单的配置问题。

2.3 自定义注解的完整写法

定义一个注解的语法和定义接口很像,只是前面多了个@符号。比如我们要定义一个@Table注解,用来标记实体类对应的数据库表名:

@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Table { String value() default ""; }

这里String value() default ""是注解的属性。注解的属性只能支持基本类型、String、Class、枚举、注解类型以及它们的数组。属性可以有默认值,也可以没有。如果没有默认值,使用注解时必须显式赋值。

再定义一个@Column注解,用来标记字段对应的数据库列名:

@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Column { String value() default ""; boolean nullable() default true; int length() default 255; }

注意@Target的取值不同:@Table用在类上,所以是ElementType.TYPE;@Column用在字段上,所以是ElementType.FIELD。如果写错了,编译器会直接报错,这其实是好事,能帮你提前发现设计问题。

还有一个@Id注解,用来标记主键:

@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Id { boolean autoIncrement() default true; }

这三个注解就是我们手写ORM框架的基础。定义好之后,就可以在实体类上使用了。

2.4 注解属性的默认值陷阱

这里有一个很容易踩的坑:注解属性的默认值是在编译期确定的,不能是运行时才能确定的值。比如你不能写String value() default System.getProperty("table.name"),因为注解的属性值必须是常量表达式。

另一个坑是,如果注解只有一个属性且名字叫value,使用时可以省略属性名。比如@Table("user")等价于@Table(value = "user")。但如果同时有多个属性,就必须写清楚属性名。这个规则在Spring的@RequestMapping里体现得很明显:@RequestMapping("/user")可以,但@RequestMapping(value = "/user", method = RequestMethod.GET)就必须写全。

还有一点,注解属性如果是数组,赋值时可以用花括号,但如果只有一个元素,花括号可以省略。比如@RequestMapping(method = RequestMethod.GET)和@RequestMapping(method = {RequestMethod.GET})是等价的。

3. 反射:在运行时“看透”一个类

3.1 反射到底在做什么

如果说注解是贴在代码上的标签,那反射就是在运行时读取这些标签、并操作类内部结构的能力。

正常情况下,我们写代码时是“编译期确定”的:你知道有个User类,知道它有name字段,所以你直接写user.getName()。但框架开发者面对的是未知的类——Spring不知道你会写什么Service,MyBatis不知道你会定义什么实体。它需要在运行时动态地获取类的信息:这个类有哪些字段?哪些方法?字段上有什么注解?然后根据这些信息做相应的处理。

反射就是干这个的。它允许你在运行时:

  • 获取一个类的Class对象
  • 获取类的所有字段、方法、构造器
  • 读取字段的值、调用方法、创建实例
  • 读取类、字段、方法上的注解

Class对象是反射的入口。获取Class对象有三种方式:

// 方式一:通过类名.class Class<?> clazz1 = User.class; // 方式二:通过对象.getClass() User user = new User(); Class<?> clazz2 = user.getClass(); // 方式三:通过Class.forName(),最常用在框架中 Class<?> clazz3 = Class.forName("com.example.entity.User");

框架中最常用的是第三种,因为框架往往只知道类的全限定名(比如从配置文件中读取),需要在运行时加载。

3.2 获取字段、方法和构造器

拿到Class对象后,就可以获取类的内部结构了。常用的方法有两组:getDeclaredXxx()和getXxx()。

  • getDeclaredFields():获取本类声明的所有字段,包括private的,但不包括继承来的。
  • getFields():获取所有public字段,包括继承来的。
  • getDeclaredMethods():获取本类声明的所有方法,包括private的。
  • getMethods():获取所有public方法,包括继承来的。
  • getDeclaredConstructors():获取本类声明的所有构造器。

在手写ORM框架时,我们通常用getDeclaredFields(),因为实体类的字段一般是private的,而且我们只关心本类声明的字段。

Class<?> clazz = Class.forName("com.example.entity.User"); Field[] fields = clazz.getDeclaredFields(); for (Field field : fields) { System.out.println("字段名:" + field.getName()); System.out.println("字段类型:" + field.getType().getName()); }

这里有一个细节:getDeclaredFields()返回的字段顺序不保证和源码中的顺序一致。JVM规范没有要求保留字段顺序,不同JVM实现可能返回不同的顺序。如果你的ORM框架依赖字段顺序(比如生成INSERT语句时),需要额外处理,比如通过@Column注解的order属性来指定顺序,或者用LinkedHashMap按名称排序。

3.3 访问私有成员:setAccessible的代价

反射可以访问private字段和方法,但需要先调用setAccessible(true):

Field field = clazz.getDeclaredField("name"); field.setAccessible(true); Object value = field.get(user);

setAccessible(true)的作用是关闭Java的访问检查。这听起来很强大,但有几个需要注意的点:

第一,性能开销。访问检查被关闭后,JVM无法对字段访问做优化,反射调用比直接调用慢很多。在JDK 8之前,反射调用可能比直接调用慢几十倍甚至上百倍。JDK 8之后有所优化,但仍然有明显差距。所以框架通常会缓存反射获取的Field、Method对象,避免重复查找。

第二,模块化限制。JDK 9引入模块系统后,如果目标类在另一个模块中且没有导出对应的包,setAccessible(true)可能会抛出InaccessibleObjectException。这是很多老框架在JDK 9+上出问题的原因之一。

第三,安全限制。某些运行环境(如Applet、安全管理器开启的环境)会禁止setAccessible。虽然现在这些场景很少了,但了解这一点有助于理解为什么有些框架在特定环境下会报错。

实操建议:在ORM框架中,建议在初始化阶段一次性扫描所有实体类,把字段的Field对象缓存到一个Map中,后续操作直接查缓存,避免每次操作都重新反射查找。这个优化在数据量大的场景下效果非常明显。

3.4 反射创建实例与调用方法

除了读取字段,反射还能创建实例和调用方法:

// 创建实例(调用无参构造器) Object instance = clazz.getDeclaredConstructor().newInstance(); // 调用方法 Method method = clazz.getDeclaredMethod("setName", String.class); method.invoke(instance, "张三");

getDeclaredMethod需要传入方法名和参数类型列表。如果方法有重载,参数类型必须匹配。invoke的第一个参数是目标对象,后面是方法参数。

这里有一个常见的坑:基本类型和包装类型的匹配问题。比如方法签名是setAge(int age),你传Integer.class是找不到这个方法的,必须传int.class。反过来也一样。在写通用框架时,需要做类型转换处理。

4. 手写ORM底层demo:从零搭建映射引擎

4.1 整体设计思路

现在我们把注解和反射结合起来,手写一个简易的ORM框架。目标很明确:给一个实体类,自动生成对应的SQL语句,并能把查询结果映射回实体对象。

整体流程分三步:

  1. 解析实体类:通过反射读取类上的@Table注解和字段上的@Column、@Id注解,构建一个“类到表”的映射关系。
  2. 生成SQL:根据映射关系,生成INSERT、SELECT、UPDATE、DELETE语句。
  3. 结果映射:执行查询后,把ResultSet中的数据通过反射设置到实体对象的字段上。

这个demo不涉及数据库连接池、事务管理、缓存等高级特性,只聚焦最核心的映射逻辑。理解了这套逻辑,再看MyBatis、Hibernate的源码就不会觉得无从下手了。

4.2 实体类定义与Javabean规范

先定义一个符合Javabean规范的实体类:

@Table("t_user") public class User { @Id(autoIncrement = true) @Column("id") private Long id; @Column("user_name") private String userName; @Column("age") private Integer age; @Column("email") private String email; // 无参构造器 public User() {} // getter和setter public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } }

这里要强调一下Javabean规范,因为ORM框架严重依赖这些规范:

  • 类必须是public的
  • 必须有无参构造器(反射创建实例时需要)
  • 字段是private的
  • 通过getter/setter访问字段
  • getter/setter命名遵循getXxx/setXxx格式

为什么必须有无参构造器?因为反射创建实例时,最简单的做法就是调用无参构造器。如果只有带参构造器,框架就需要额外处理参数注入,复杂度会大幅上升。这也是为什么很多框架要求实体类必须有无参构造器。

4.3 表映射元数据的构建

接下来写一个TableInfo类,用来保存解析后的映射信息:

public class TableInfo { private String tableName; private String idColumnName; private String idFieldName; private boolean idAutoIncrement; private List<ColumnInfo> columns = new ArrayList<>(); // 省略getter/setter } public class ColumnInfo { private String columnName; private String fieldName; private Class<?> fieldType; private Field field; // 省略getter/setter }

然后写一个解析器,通过反射读取注解,构建TableInfo:

public class TableInfoParser { private static final Map<Class<?>, TableInfo> CACHE = new ConcurrentHashMap<>(); public static TableInfo parse(Class<?> clazz) { return CACHE.computeIfAbsent(clazz, TableInfoParser::doParse); } private static TableInfo doParse(Class<?> clazz) { TableInfo tableInfo = new TableInfo(); // 解析类上的@Table注解 Table table = clazz.getAnnotation(Table.class); if (table == null) { throw new RuntimeException("实体类缺少@Table注解:" + clazz.getName()); } tableInfo.setTableName(table.value()); // 解析字段上的注解 Field[] fields = clazz.getDeclaredFields(); for (Field field : fields) { Column column = field.getAnnotation(Column.class); if (column == null) { continue; } ColumnInfo columnInfo = new ColumnInfo(); columnInfo.setColumnName(column.value()); columnInfo.setFieldName(field.getName()); columnInfo.setFieldType(field.getType()); columnInfo.setField(field); // 判断是否为主键 Id id = field.getAnnotation(Id.class); if (id != null) { tableInfo.setIdColumnName(column.value()); tableInfo.setIdFieldName(field.getName()); tableInfo.setIdAutoIncrement(id.autoIncrement()); } tableInfo.getColumns().add(columnInfo); } return tableInfo; } }

这里用了ConcurrentHashMap做缓存,避免每次操作都重新解析。computeIfAbsent是JDK 8引入的方法,能保证线程安全且只解析一次。这个缓存设计在实际框架中非常常见,因为反射解析的开销不小,缓存后性能提升明显。

4.4 SQL生成器的实现

有了TableInfo,生成SQL就简单了。先写INSERT语句的生成:

public class SqlGenerator { public static String generateInsert(TableInfo tableInfo) { StringBuilder sql = new StringBuilder("INSERT INTO "); sql.append(tableInfo.getTableName()).append(" ("); List<ColumnInfo> columns = tableInfo.getColumns(); List<String> columnNames = new ArrayList<>(); List<String> placeholders = new ArrayList<>(); for (ColumnInfo column : columns) { // 自增主键不参与INSERT if (column.getColumnName().equals(tableInfo.getIdColumnName()) && tableInfo.isIdAutoIncrement()) { continue; } columnNames.add(column.getColumnName()); placeholders.add("?"); } sql.append(String.join(", ", columnNames)); sql.append(") VALUES ("); sql.append(String.join(", ", placeholders)); sql.append(")"); return sql.toString(); } }

再写SELECT语句的生成:

public static String generateSelectById(TableInfo tableInfo) { StringBuilder sql = new StringBuilder("SELECT "); List<String> columnNames = new ArrayList<>(); for (ColumnInfo column : tableInfo.getColumns()) { columnNames.add(column.getColumnName()); } sql.append(String.join(", ", columnNames)); sql.append(" FROM ").append(tableInfo.getTableName()); sql.append(" WHERE ").append(tableInfo.getIdColumnName()).append(" = ?"); return sql.toString(); }

UPDATE和DELETE类似,这里不一一展开。核心思路就是:从TableInfo中读取表名、列名,拼接成SQL字符串,用?作为占位符防止SQL注入。

注意:这里用?占位符而不是直接拼接值,是为了防止SQL注入。在实际框架中,值的设置是通过PreparedStatement的setXxx方法完成的,而不是字符串拼接。这一点非常重要,千万不要为了省事直接拼SQL。

4.5 结果集到对象的映射

查询执行后,需要把ResultSet映射回实体对象。这是反射发挥核心作用的地方:

public class ResultSetMapper { public static <T> T map(ResultSet rs, Class<T> clazz) throws Exception { TableInfo tableInfo = TableInfoParser.parse(clazz); T instance = clazz.getDeclaredConstructor().newInstance(); for (ColumnInfo column : tableInfo.getColumns()) { Object value = rs.getObject(column.getColumnName()); if (value == null) { continue; } Field field = column.getField(); field.setAccessible(true); // 类型转换处理 Object convertedValue = convertType(value, field.getType()); field.set(instance, convertedValue); } return instance; } private static Object convertType(Object value, Class<?> targetType) { if (targetType.isInstance(value)) { return value; } // 处理基本类型和包装类型的转换 if (targetType == Long.class || targetType == long.class) { return ((Number) value).longValue(); } if (targetType == Integer.class || targetType == int.class) { return ((Number) value).intValue(); } if (targetType == String.class) { return value.toString(); } // 其他类型转换... return value; } }

这里有几个关键点:

第一,field.setAccessible(true)是必须的,因为实体类的字段通常是private的。

第二,类型转换是必须的。数据库返回的Long可能对应Java的long,数据库的VARCHAR可能对应Java的String,但ResultSet.getObject()返回的类型和字段类型不一定完全匹配。特别是基本类型和包装类型之间,需要显式转换。

第三,null值处理。如果数据库字段为null,rs.getObject()返回null,此时不应该设置到基本类型字段上(会抛异常),应该跳过或者设置默认值。

4.6 完整的CRUD流程串联

把上面的组件串起来,一个简易的ORM就成型了:

public class SimpleOrm { private final DataSource dataSource; public SimpleOrm(DataSource dataSource) { this.dataSource = dataSource; } public <T> T selectById(Class<T> clazz, Object id) throws Exception { TableInfo tableInfo = TableInfoParser.parse(clazz); String sql = SqlGenerator.generateSelectById(tableInfo); try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setObject(1, id); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { return ResultSetMapper.map(rs, clazz); } } } return null; } public <T> int insert(T entity) throws Exception { Class<?> clazz = entity.getClass(); TableInfo tableInfo = TableInfoParser.parse(clazz); String sql = SqlGenerator.generateInsert(tableInfo); try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { int index = 1; for (ColumnInfo column : tableInfo.getColumns()) { if (column.getColumnName().equals(tableInfo.getIdColumnName()) && tableInfo.isIdAutoIncrement()) { continue; } Field field = column.getField(); field.setAccessible(true); ps.setObject(index++, field.get(entity)); } return ps.executeUpdate(); } } }

使用起来就像这样:

SimpleOrm orm = new SimpleOrm(dataSource); User user = orm.selectById(User.class, 1L); System.out.println(user.getUserName());

到这里,一个能跑通CRUD的简易ORM就完成了。虽然离生产级框架还有很大距离,但核心的注解解析、反射操作、SQL生成、结果映射这几个环节都覆盖到了。理解了这套流程,再看MyBatis的MapperProxy、ResultSetHandler就不会觉得陌生。

5. 踩坑实录:那些年反射和注解给我上的课

5.1 注解没加RUNTIME导致读取为null

这是我最早踩的坑。定义了一个@Column注解,忘了加@Retention(RetentionPolicy.RUNTIME),结果运行时field.getAnnotation(Column.class)返回null。排查了半天,以为是反射写错了,最后才发现是注解的保留策略问题。

默认的@Retention是CLASS,不是RUNTIME。也就是说,如果你不显式写@Retention(RetentionPolicy.RUNTIME),注解在运行时是读不到的。这个默认值设计是有历史原因的,但对初学者来说确实容易踩坑。

经验:定义任何需要被反射读取的注解,第一件事就是加上@Retention(RetentionPolicy.RUNTIME)。可以把这个当成肌肉记忆。

5.2 反射调用性能问题的真实数据

我做过一个简单的性能测试,对比直接调用和反射调用的差距:

调用方式100万次耗时(ms)相对倍数
直接调用setter51x
反射调用(不缓存Method)850170x
反射调用(缓存Method)12024x
反射调用(缓存+setAccessible)459x

这个数据因JVM版本和硬件不同会有差异,但趋势是明确的:反射确实慢,但缓存后可以接受。在ORM框架中,字段的Field对象和Method对象都应该缓存起来,避免重复查找。setAccessible(true)也能带来明显的性能提升,因为它关闭了访问检查。

5.3 getDeclaredFields顺序不保证的坑

前面提到过,getDeclaredFields()返回的字段顺序不保证和源码一致。我写SQL生成器时,一开始假设字段顺序和源码一致,结果在某些JVM上生成的INSERT语句列顺序和值顺序对不上,导致数据错位。

解决方案有两种:一是通过注解属性显式指定顺序,二是在生成SQL时按列名排序。我选择了第二种,因为更简单可靠:

List<ColumnInfo> columns = tableInfo.getColumns(); columns.sort(Comparator.comparing(ColumnInfo::getColumnName));

这样虽然列的顺序和源码不一致,但INSERT语句的列名和值是一一对应的,不会出错。

5.4 基本类型与包装类型的转换陷阱

反射设置字段值时,如果字段是int类型,你传一个Integer对象,field.set()是能正常工作的(自动拆箱)。但如果传null,就会抛IllegalArgumentException。同样,如果字段是long,你传Integer,也会抛异常。

在结果映射时,数据库返回的Number类型可能是Long、Integer、BigDecimal等,需要根据目标字段类型做转换。我写了一个TypeConverter工具类专门处理这个问题,核心逻辑就是判断目标类型,然后调用对应的xxxValue()方法。

5.5 JDK版本升级带来的反射限制

JDK 9引入模块系统后,反射访问非导出包的类会抛出InaccessibleObjectException。JDK 17进一步收紧了限制。如果你的框架需要反射访问JDK内部类(比如java.util包下的某些类),需要添加--add-opens启动参数。

这个问题在升级JDK版本时特别容易遇到。我的建议是:尽量不要反射访问JDK内部类,如果必须访问,在启动脚本中显式添加--add-opens参数,并做好版本兼容处理。

6. 从demo到生产:还需要考虑什么

6.1 连接池与资源管理

demo里每次操作都从DataSource获取连接,用完关闭。实际生产环境必须用连接池(如HikariCP、Druid),避免频繁创建销毁连接的开销。同时要确保Connection、PreparedStatement、ResultSet都在try-with-resources中正确关闭,防止资源泄漏。

6.2 SQL注入防护

demo中所有值都通过PreparedStatement的setObject设置,这是正确的做法。但表名、列名是通过字符串拼接的,这些来自注解,是开发者可控的,不存在注入风险。如果表名来自用户输入,就必须做白名单校验。

6.3 缓存策略

TableInfo的缓存用了ConcurrentHashMap,这是最基本的缓存。实际框架中还需要考虑:缓存失效(类被重新加载时)、缓存大小限制(避免内存泄漏)、多ClassLoader环境下的缓存隔离等。

6.4 事务管理

demo没有涉及事务。实际ORM框架需要支持事务,通常通过ThreadLocal绑定当前线程的Connection,在事务开始时关闭自动提交,事务结束时统一提交或回滚。Spring的@Transactional底层就是基于这个原理。

6.5 字段名与列名的映射策略

demo中通过@Column注解显式指定列名。实际框架通常还支持“驼峰转下划线”的自动映射策略,比如userName自动映射到user_name。这个策略可以通过配置开关控制,灵活适配不同的数据库命名规范。

7. 我个人的一些实操体会

写了这么多,最后分享几点我在实际项目中的体会。

第一,注解和反射的学习曲线是“先陡后平”的。刚开始接触时,各种概念和API容易让人晕头转向。但一旦理解了“注解是标签、反射是读取标签的工具”这个核心比喻,剩下的就是熟练度问题。建议一定要动手写一个demo,哪怕只是最简单的“读取注解并打印”,也比只看书强十倍。

第二,不要过度使用反射。反射很强大,但性能开销和代码可读性都是代价。在业务代码中,能用接口和多态解决的问题,尽量不要用反射。反射最适合的场景是框架开发、通用工具、动态代理这些“需要处理未知类型”的场景。

第三,注解设计要克制。我见过一些项目,自定义了十几个注解,每个注解有七八个属性,最后连开发者自己都记不清哪个注解该加在哪里。注解的设计应该遵循“最小够用”原则,属性越少越好,默认值越合理越好。

第四,多看源码。MyBatis的MapperAnnotationBuilder、Spring的AutowiredAnnotationBeanPostProcessor,这些都是注解和反射的经典应用。看源码时不要逐行读,先抓主干:它在哪里读取注解?读取后做了什么?把主干理清楚,细节自然就懂了。

第五,注意JDK版本兼容性。如果你的框架需要支持多个JDK版本,反射相关的代码要特别小心。JDK 8、11、17、21在反射行为上有不少差异,建议在CI中配置多版本测试,提前发现问题。

这个简易ORM的demo我后来又迭代了几个版本,加了简单的查询条件拼接、分页支持、一级缓存。每加一个功能,都对注解和反射的理解更深一层。如果你也想真正掌握这两块内容,建议不要只停留在看,动手写一个属于自己的小框架,哪怕功能很简单,收获也会远超预期。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 8:58:49

Cocos引擎.jsc逆向破解:从字节码还原JavaScript逻辑

上周有朋友甩了个包过来&#xff0c;说手上的某个模拟项目X崩溃了&#xff0c;报错指向一个.jsc文件&#xff0c;而原始的 JavaScript 工程在三个月前换人的时候弄丢了&#xff0c;只剩编译后的二进制产物。他想让我看看还能不能把逻辑还原出来。我当时的回答是&#xff1a;能&…

作者头像 李华
网站建设 2026/10/21 8:57:54

diagram-design:基于Canvas的可交互图表设计引擎实践

1. 项目定位&#xff1a;diagram-design 到底解决什么问题先说结论&#xff1a;diagram-design 不是又一个“画图库”&#xff0c;而是一整套面向 Web 端的可交互图表设计引擎。简单理解&#xff0c;它就是让你在浏览器里拖拖拽拽就能搭出流程图、拓扑图、架构图、思维导图的那…

作者头像 李华
网站建设 2026/10/21 8:56:51

捅破鸿蒙开发窗户纸:Stage模型与ArkUI状态管理实战

从HelloWorld到真正会写鸿蒙应用&#xff0c;中间其实隔着一整座知识山。很多人下载好DevEco Studio&#xff0c;照着官方模板跑通一个hello world&#xff0c;屏幕上弹出“Hello World”那一刻觉得自己入门了&#xff0c;但等到真要自己做页面、传参数、做状态更新&#xff0c…

作者头像 李华
网站建设 2026/10/21 8:56:16

DDR5内存条标签解读指南:JESD401-5标准字段拆解与SPD核对

简介&#xff1a;JESD401-5 是 JEDEC 于 2022 年 5 月发布的 DDR5 DIMM 标签标准正式文本&#xff0c;面向内存模组设计、硬件研发、品质管控与标准化合规岗位的工程师&#xff0c;用于解决 DDR5 内存条标签格式、内容与制造要求不统一的问题。资源包为单一 PDF 文件&#xff0…

作者头像 李华
网站建设 2026/10/21 8:56:01

2025大厂Java面试真题解析:Spring Boot、微服务与AI技术实战

最近两个月集中面了几家互联网大厂的后端岗位&#xff0c;方向都是Java技术栈&#xff0c;从实习到高级岗都接触过。整体感受是&#xff1a;八股题依然在问&#xff0c;但问法已经变了。以前是“Spring Boot的自动配置原理是什么”&#xff0c;现在是“如果让你给一个老项目接入…

作者头像 李华
网站建设 2026/10/21 8:54:52

DDR5内存条标签规范JESD401-5解读与合规标签生成指南

简介&#xff1a;JESD401-5 是 JEDEC 于 2022 年 5 月发布的 DDR5 DIMM 标签标准&#xff0c;面向内存模组设计、制造、测试及采购环节的硬件工程师与标准化从业人员&#xff0c;用于统一 DDR5 DIMM 标签的格式、内容与制造要求&#xff0c;解决制造商与购买者之间的信息沟通与…

作者头像 李华