搜索 K
Appearance
Appearance
整体流程在 Spring Boot 启动尾部一次性完成:
DdlInitializer#fetchScanPackages 并没有去解析 @SpringBootApplication.scanBasePackages,而是直接读 Spring Boot 内部的 AutoConfigurationPackages 前置知识:
private String[] fetchScanPackages() {
if (!AutoConfigurationPackages.has(beanFactory)) {
log.warn("No AutoConfigurationPackages registered, skip scanning");
return new String[0];
}
List<String> packages = AutoConfigurationPackages.get(beanFactory);
return packages.toArray(new String[0]);
}@SpringBootApplication 内部已经组合了 @AutoConfigurationPackage,会自动调用 AutoConfigurationPackages.register(...) 把启动类所在包记录到容器@AutoConfigurationPackage(basePackages=...) 或自定义 @EnableAutoConfiguration 组合,AutoConfigurationPackages.get(...) 一样能正确返回,通用性更强ClazzScanner#scan 内部使用 ClassPathScanningCandidateComponentProvider 前置知识 做候选发现:
public Set<Class<?>> scan(String... packages) {
Set<Class<?>> result = new LinkedHashSet<>();
if (packages == null || packages.length == 0) {
log.warn("Scan packages empty, skip scanning");
return result;
}
ClassPathScanningCandidateComponentProvider provider = buildProvider();
for (String pkg : packages) {
if (pkg == null || pkg.isBlank()) continue;
for (BeanDefinition bd : provider.findCandidateComponents(pkg)) {
String className = bd.getBeanClassName();
if (className == null) continue;
try {
Class<?> clazz = Class.forName(className, false,
Thread.currentThread().getContextClassLoader());
if (isUnsupportedClass(clazz)) continue;
result.add(clazz);
} catch (ClassNotFoundException e) {
log.warn("Failed to load scanned class: {}", className, e);
}
}
}
return result;
}四道过滤:
isCandidateComponent(...) 限定为「独立、非抽象、非接口」AnnotationTypeFilter(Model.class) 只保留标了 @Model 的类$ 字符的全部排除(典型内部类命名)isMemberClass / isAnonymousClass / isLocalClass / isAbstract / isInterface 再过一遍,防止类名过滤遗漏为什么有第 4 道兜底?
类名带 $ 通常代表内部类,但不绝对。比如 Lombok / Kotlin 生成的合成类,命名规律可能不统一。多一道反射判断换来零误判,成本极小。
DdlInitializer#buildFieldDefinition 与 FieldTypeHelper 一起完成「子注解 → Java 类型」的双层推导:
FieldTypeHelper.ANNOTATION_TO_TYPE:
| 注解 | FieldType |
|---|---|
@Field.String | STRING |
@Field.Text | TEXT |
@Field.LongText | LONG_TEXT |
@Field.Integer | INTEGER |
@Field.Long | LONG |
@Field.Boolean | BOOLEAN |
@Field.BigDecimal | BIG_DECIMAL |
@Field.Date | DATE |
FieldTypeHelper.JAVA_TYPE_TO_TYPE(节选):
| Java 类型 | FieldType |
|---|---|
String | STRING |
int / Integer / short / Short | INTEGER |
long / Long | LONG |
boolean / Boolean | BOOLEAN |
BigDecimal | BIG_DECIMAL |
LocalDateTime / LocalDate / LocalTime / Date | DATE |
不支持的类型直接抛
如果字段既无类型子注解,Java 类型又不在兜底表中(比如 byte[]、自定义枚举),inferTypeFromJavaType 会抛 IllegalStateException,提示开发者明确意图。框架不做静默忽略。
parseFields 调用 clazz.getDeclaredFields() 前置知识 拿所有声明字段,跳过 static,逐个委托给 buildFieldDefinition:
for (java.lang.reflect.Field reflectField : clazz.getDeclaredFields()) {
if (Modifier.isStatic(reflectField.getModifiers())) continue;
FieldDefinition definition = buildFieldDefinition(reflectField);
if (definition != null) list.add(definition);
}
getDeclaredFields()而非getFields():选前者拿到 private 字段且不递归父类,与阶段①「不处理继承」的取舍一致。两者对比详见 前置知识 · Java 反射 API。
buildFieldDefinition 内部的关键决策:
Class<?> javaType = reflectField.getType();
boolean nullable = !javaType.isPrimitive();
boolean store = advanced == null || advanced.store();
String columnName = NamingUtils.resolveColumnName(advanced, reflectField.getName());可空性的判断很质朴:基本类型不可空,包装/引用类型可空。
NamingUtils.camelToUnderscore 是一段无依赖的纯函数:
public static String camelToUnderscore(String name) {
if (name == null || name.isEmpty()) return name;
StringBuilder result = new StringBuilder(name.length() + 4);
for (int i = 0; i < name.length(); i++) {
char c = name.charAt(i);
if (Character.isUpperCase(c)) {
if (i > 0) result.append('_');
result.append(Character.toLowerCase(c));
} else {
result.append(c);
}
}
return result.toString();
}| 输入 | 输出 |
|---|---|
userName | user_name |
XMLParser | x_m_l_parser |
id | id |
⚠️ 注意
XMLParser这种连续大写会被逐字母切开,不是xml_parser。如果业务里有这种情况,请显式写@Field.Advanced(column = "xml_parser")兜底。
computeTableName 与 resolveColumnName 都是「优先使用显式声明,否则走 camelToUnderscore」的同一套策略,保证模型名/字段名与表名/列名之间有可预期的默认规则。