Skip to content

扫描机制

启动时序

整体流程在 Spring Boot 启动尾部一次性完成:

扫描根的解析

DdlInitializer#fetchScanPackages 并没有去解析 @SpringBootApplication.scanBasePackages,而是直接读 Spring Boot 内部的 AutoConfigurationPackages 前置知识

java
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?
  • @SpringBootApplication 内部已经组合了 @AutoConfigurationPackage,会自动调用 AutoConfigurationPackages.register(...) 把启动类所在包记录到容器
  • 业务方如果改用更细粒度的 @AutoConfigurationPackage(basePackages=...) 或自定义 @EnableAutoConfiguration 组合,AutoConfigurationPackages.get(...) 一样能正确返回,通用性更强
  • 还能避免反射查找 main 类的脏活
  • 完整说明

包扫描

ClazzScanner#scan 内部使用 ClassPathScanningCandidateComponentProvider 前置知识 做候选发现:

java
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;
}

四道过滤:

  1. Spring 默认非法元素:通过覆写 isCandidateComponent(...) 限定为「独立、非抽象、非接口」
  2. 注解 include filterAnnotationTypeFilter(Model.class) 只保留标了 @Model 的类
  3. 类名 exclude filter:含 $ 字符的全部排除(典型内部类命名)
  4. 反射兜底isMemberClass / isAnonymousClass / isLocalClass / isAbstract / isInterface 再过一遍,防止类名过滤遗漏

为什么有第 4 道兜底?

类名带 $ 通常代表内部类,但不绝对。比如 Lombok / Kotlin 生成的合成类,命名规律可能不统一。多一道反射判断换来零误判,成本极小。

类型推导

DdlInitializer#buildFieldDefinitionFieldTypeHelper 一起完成「子注解 → Java 类型」的双层推导:

子注解 → FieldType 映射表

FieldTypeHelper.ANNOTATION_TO_TYPE

注解FieldType
@Field.StringSTRING
@Field.TextTEXT
@Field.LongTextLONG_TEXT
@Field.IntegerINTEGER
@Field.LongLONG
@Field.BooleanBOOLEAN
@Field.BigDecimalBIG_DECIMAL
@Field.DateDATE

Java 类型 → FieldType 兜底表

FieldTypeHelper.JAVA_TYPE_TO_TYPE(节选):

Java 类型FieldType
StringSTRING
int / Integer / short / ShortINTEGER
long / LongLONG
boolean / BooleanBOOLEAN
BigDecimalBIG_DECIMAL
LocalDateTime / LocalDate / LocalTime / DateDATE

不支持的类型直接抛

如果字段既无类型子注解,Java 类型又不在兜底表中(比如 byte[]、自定义枚举),inferTypeFromJavaType 会抛 IllegalStateException,提示开发者明确意图。框架不做静默忽略

字段解析

parseFields 调用 clazz.getDeclaredFields() 前置知识 拿所有声明字段,跳过 static,逐个委托给 buildFieldDefinition

java
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 内部的关键决策:

java
Class<?> javaType = reflectField.getType();
boolean nullable = !javaType.isPrimitive();
boolean store = advanced == null || advanced.store();
String columnName = NamingUtils.resolveColumnName(advanced, reflectField.getName());

可空性的判断很质朴:基本类型不可空,包装/引用类型可空

命名转换

NamingUtils.camelToUnderscore 是一段无依赖的纯函数:

java
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();
}
输入输出
userNameuser_name
XMLParserx_m_l_parser
idid

⚠️ 注意 XMLParser 这种连续大写会被逐字母切开,不是 xml_parser。如果业务里有这种情况,请显式写 @Field.Advanced(column = "xml_parser") 兜底。

computeTableNameresolveColumnName 都是「优先使用显式声明,否则走 camelToUnderscore」的同一套策略,保证模型名/字段名与表名/列名之间有可预期的默认规则。