第39项:注解优于命名模式

Java注解:替代命名模式的测试利器
本文指出命名模式用于程序元素特殊处理存在拼写易出错、无法确保适用范围、难关联参数值等缺点。介绍了Java注解如何解决这些问题,以自定义测试框架为例,展示了注解类型声明、应用及处理,还提及可重复注解,强调注解优于命名模式,建议开发者使用预定义注解。

  在之前的做法中(Historically),一般使用命名模式(naming pattern) 表明有些程序元素需要通过某种工具或者框架进行特殊处理。例如,在第4版之前,JUnit测试框架要求其用户通过使用字符test作为测试方法名称的开头[Beck04]。这种方法可行,但它有几个很大的缺点。首先,文字拼写错误会导致失败,并且没有任何提示。例如,假设你不小心将测试方法命名为tsetSafetyOverride而不是testSafetyOverride。Junit 3不会出错,但是它也不会执行测试,造成错误的安全感【也就是说,那个命名错误的测试方法不会执行测试,并且不会报错,让我们误以为测试成功了的假象】。

  命名模式的第二个缺点是,无法确保它们只用于相应的程序元素上。例如,假设你将某个类命名为TestSafetyMechanisms ,是希望JUnit 3会自动测试它所有的方法,而不管它们叫什么名称。Junit 3还是不会出错,但也同样不会执行测试。

  命名模式的第三个缺点是它们没有提供将参数值与程序元素相关联的好方法。例如,假设您希望支持仅在抛出特定异常时才成功的测试类别。异常类型本质上是测试的参数。您可以使用一些复杂的命名模式将异常类型名称编码到测试方法的名称中,但是这样的代码会很不雅观,也很脆弱(第62项)。编译器不知道要去检验准备命名异常的字符串是否真正命名成功。如果命名类不存在,或者不是一个异常,你也要试着运行测试时才会发现。

  注解[JLS,9.7]很好地解决了所有这些问题,JUnit从第4版开始采用它们。在该项中,我们将编写自己的测试框架来显示注释是如何工作的。假设想要定义一个注解类型来指定简单的测试,它们自动运行,并在抛出异常时失败。以下就是这样一个注解类型,命名为Test:

// Marker annotation type declaration
import java.lang.annotation.*;
/**
 * Indicates that the annotated method is a test method.
 * Use only on parameterless static methods.
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}

  Test注解类型的声明就是它自身通过Retention和Target注解进行了注解。注解类型声明中的这种注解被称作元注解(meta-annotation)。@Retention(RetentionPolicy.RUNTIME)元注解表明,Test注解应该在运行时保留。如果没有保留,测试工具就无法知道Test注解。@Target(ElementType.METHOD)元注解表明,Test注解只在方法声明中才是合法的;它不能运用到类声明、域声明或者其他程序元素上。

  注意Test注解声明上方的注释:“Use only on parameterless static methods.(只用于无参的静态方法)。”如果编译器能够强制这一限制最好,但是它做不到。除非你写一个注释处理器这样做。有关此专题的更多信息,请参阅javax.annotation.processing的文档。在缺少这样的注释处理器的情况下,如果将Test注解放在实例方法的声明中,或者放在带有一个或者多个参数的方法中,测试程序仍然会编译,让测试工具在运行时来处理这个问题。

  下面就是在实践中应用Test注解,称作标记注解(marker annotation),因为它没有参数,只是“标注”被注解的元素。如果程序猿平错了Test,或者将Test注解应用到程序元素而非方法声明,程序就无法编译:

// Program containing marker annotations
public class Sample {
    @Test
    public static void m1() { } // Test should pass
    public static void m2() { }
    @Test
    public static void m3() { // Test should fail
        throw new RuntimeException("Boom");
    }
    public static void m4() { }
    @Test
    public void m5() { } // INVALID USE: nonstatic method
    public static void m6() { }
    @Test
    public static void m7() { // Test should fail
        throw new RuntimeException("Crash");
    }
    public static void m8() { }
}

  这个Sample类有7个静态方法,其中有4个被注解为测试。在这4个里面,有2个抛出了异常:m3和m7,另外两个则没有:m1和m5。但是其中一个没有抛出异常的被注解方法:m5,是一个实例方法,因此不属于注解的有效使用。总之,Sample包含4项测试:一项会通过,两项会失败,另外一项则无效。没有用Test注解进行标注的4个方法会被测试工具忽略。

  Test注释对Sample类的语义没有直接影响。它们只负责提供信息供相关的程序使用。更一般地讲,注解永远不会改变被注解代码的语义,但是使它可以通过工具进行特殊的处理,例如像这种简单的测试运行类:

// Program to process marker annotations
import java.lang.reflect.*;
public class RunTests {
    public static void main(String[] args) throws Exception {
        int tests = 0;
        int passed = 0;
        Class<?> testClass = Class.forName(args[0]);
            for (Method m : testClass.getDeclaredMethods()) {
                if (m.isAnnotationPresent(Test.class)) {
                    tests++;
                try {
                    m.invoke(null);
                    passed++;
                } catch (InvocationTargetException wrappedExc) {
                    Throwable exc = wrappedExc.getCause();
                    System.out.println(m + " failed: " + exc);
                } catch (Exception exc) {
                    System.out.println("Invalid @Test: " + m);
                }
            }
        }
        System.out.printf("Passed: %d, Failed: %d%n", passed, tests - passed);
    }
}

  测试运行工具在命令行上使用完全匹配的类名,并通过调用Method.invoke反射式地运行类中所有标注了Test的方法。isAnnotationPresent方法告知该工具要运行哪些方法。如果测试方法抛出异常,并打印失败报告,包含测试方法抛出的原始异常,该异常时使用getCause方法从InvocationTargetException中提取出来的。

  如果尝试通过反射调用测试方法时抛出InvocationTargetException之外的任何异常,表明编译时没有捕捉到Test注解的无效用法。这种用法包括实例方法的注解,或者带有一个或者多个参数的方法的注解,或者不可访问的方法的注解。测试运行类中的第二个cache块捕捉到了这些Test用法错误,并打印出相应的错误消息。下面就是RunTests在Sample上运行时打印的输出:

public static void Sample.m3() failed: RuntimeException: Boom
Invalid @Test: public void Sample.m5()
public static void Sample.m7() failed: RuntimeException: Crash
Passed: 1, Failed: 3

  现在我们要针对只在抛出特殊异常时才成功的测试添加支持。为此我们需要一个新的注解类型:

// Annotation type with a parameter
import java.lang.annotation.*;
/**
 * Indicates that the annotated method is a test method that
 * must throw the designated exception to succeed.
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
    Class<? extends Throwable> value();
}

  这个注解的参数类型是Class<? extends Throwable>。这个通配符类型无疑很绕口。它在英语中的意思是:某个扩展Exception的类的Class对象,它允许注解的用户指定任何异常(或错误)类型。这种用法是有限制的类型令牌(第33项)。下面就是实际应用中的这个注解。注意类名称被用作了注解的参数值:

// Program containing annotations with a parameter
public class Sample2 {
    @ExceptionTest(ArithmeticException.class)
    public static void m1() { // Test should pass
        int i = 0;
        i = i / i;
    }
    @ExceptionTest(ArithmeticException.class)
    public static void m2() { // Should fail (wrong exception)
        int[] a = new int[0];
        int i = a[1];
    }
    @ExceptionTest(ArithmeticException.class)
    public static void m3() { } // Should fail (no exception)
}

  现在我们要修改一下测试运行工具来处理新的注解。这其中包括将以下代码添加到main方法中:

if (m.isAnnotationPresent(ExceptionTest.class)) {
    tests++;
    try {
        m.invoke(null);
        System.out.printf("Test %s failed: no exception%n", m);
    } catch (InvocationTargetException wrappedEx) {
        Throwable exc = wrappedEx.getCause();
        Class<? extends Throwable> excType = m.getAnnotation(ExceptionTest.class).value();
        if (excType.isInstance(exc)) {
            passed++;
        } else {
            System.out.printf("Test %s failed: expected %s, got %s%n", m, excType.getName(), exc);
        }
    } catch (Exception exc) {
        System.out.println("Invalid @Test: " + m);
    }
}

  这段代码类似于用来处理Test注解的代码,但有一处不同:这段代码提取了注解参数的值,并用它检验该测试抛出的异常是否为正确的类型。没有显示的转换,因此没有出现ClassCastException的危险。编译过的测试程序确保它的注解参数表示的是有效的异常类型,需要提醒一点:有可能注解参数在编译时是有效的,但是表示特定异常类型的类文件在运行时却不再存在。在这种希望很少出现的情况下,测试运行类会抛出TypeNotPresentException异常。

  将上面的异常测试示例再深入一点,想象测试可以在抛出任何一种指定异常时都得到通过。注解机制有一种工具,使得支持这种用法变得十分容易,假设我们将ExceptionTest注解的参数类型改成Class对象的一个数组:

// Annotation type with an array parameter
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
    Class<? extends Exception>[] value();
}

  注解中的数组参数的语法十分灵活。它是进行过优化的单元素数组。使用了ExceptionTest新版的数组参数之后,之前的所有ExceptionTest注解仍然有效,并产生单元素的数组。为了指定多元素素组,要用花括号({})将元素包围起来,并用逗号(,)将它们隔开:

// Code containing an annotation with an array parameter
@ExceptionTest({ IndexOutOfBoundsException.class, NullPointerException.class })
public static void doublyBad() {
    List<String> list = new ArrayList<>();
    // The spec permits this method to throw either
    // IndexOutOfBoundsException or NullPointerException
    list.addAll(5, null);
}

  修改测试运行工具来处理新版本的ExceptionTest是相当简单的。此代码替换原始版本:

if (m.isAnnotationPresent(ExceptionTest.class)) {
    tests++;
    try {
        m.invoke(null);
        System.out.printf("Test %s failed: no exception%n", m);
    } catch (Throwable wrappedExc) {
        Throwable exc = wrappedExc.getCause();
        int oldPassed = passed;
        Class<? extends Exception>[] excTypes = m.getAnnotation(ExceptionTest.class).value();
        for (Class<? extends Exception> excType : excTypes) {
            if (excType.isInstance(exc)) {
                passed++;
                break;
            }
        }
        if (passed == oldPassed)
            System.out.printf("Test %s failed: %s %n", m, exc);
    }
}

  从Java 8开始,还有另一种方法可以进行多值注释。 您可以使用@Repeatable元注释来注释注释的声明,而不是使用数组参数声明注释类型,以表示注释可以重复应用于单个元素。此元注释采用单个参数,该参数是包含注释类型的类对象,其唯一参数是注释类型的数组[JLS,9.6.3]。以下我们在声明ExceptionTest注释上采用此方法时的样子。请注意,包含注释类型必须使用适当的保留策略和目标进行注释,否则声明将无法编译:

// Repeatable annotation type
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(ExceptionTestContainer.class)
public @interface ExceptionTest {
    Class<? extends Exception> value();
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTestContainer {
    ExceptionTest[] value();
}

  以下是我们的doublelyBad测试使用重复注释代替数组值注释的方式:

// Code containing a repeated annotation
@ExceptionTest(IndexOutOfBoundsException.class)
@ExceptionTest(NullPointerException.class)
public static void doublyBad() { ... }

  处理可重复的注释需要小心一点。重复注释生成包含注释类型的合成注释。 getAnnotationsByType方法掩盖了这一事实,并且可用于访问可重复注释类型的重复和非重复注释。但isAnnotationPresent明确指出重复注释不是注释类型,而是包含注释类型。如果一个元素具有某种类型的重复注释,并且你使用isAnnotationPresent方法来检查该元素是否具有该类型的注释,你将发现它没有。因此,使用此方法检查是否存在注释类型将导致程序默认忽略重复注释。同样,使用此方法检查包含注释类型将导致程序默认忽略非重复注释。要使用isAnnotationPresent检测重复和非重复注释,你需要检查注释类型及其包含注释类型。以下是我们的RunTests程序的相关部分在修改后使用ExceptionTest注释的可重复版本时的样子:

// Processing repeatable annotations
if (m.isAnnotationPresent(ExceptionTest.class) || m.isAnnotationPresent(ExceptionTestContainer.class)) {
    tests++;
    try {
        m.invoke(null);
        System.out.printf("Test %s failed: no exception%n", m);
    } catch (Throwable wrappedExc) {
        Throwable exc = wrappedExc.getCause();
        int oldPassed = passed;
        ExceptionTest[] excTests = m.getAnnotationsByType(ExceptionTest.class);
        for (ExceptionTest excTest : excTests) {
            if (excTest.value().isInstance(exc)) {
                passed++;
                break;
            }
        }
        if (passed == oldPassed)
            System.out.printf("Test %s failed: %s %n", m, exc);
    }
}

  添加了可重复的注释以提高源代码的可读性,逻辑上将相同注释类型的多个实例应用在给定的程序元素。如果你认为它们增强了源代码的可读性,请使用它们,但请记住,在声明和处理可重复的注释时有更多的样板,并且处理可重复的注释容易出错。

  这个项目中的测试框架只是一个试验,但它清晰地证明了注释优于命名模式,它这只是揭开了注解功能的冰山一角。如果你是在编写一个需要程序猿给源文件添加信息的工具,请定义适当的注释类型。当你可以使用注释时,根本没有理由使用命名模式。

  也就是说,除了“工具铁匠(toolsmiths————特定的程序猿)”之外,大多数程序猿都不比定义注解类型。**但是所有的程序猿都应该使用Java平台所提供的预定义的注解类型(第40、27项)。还要考虑使用IDE或者静态分析工具所提供的任何注解。这种注解可以提升由这些工具所提供的诊断信息的质量。但是要注意这些注解还没有标准化,因此如果变换工具或者形成标准,就有很多工作要做了。

第40项:坚持使用Overide注解

02_springboot_自动配置原理_基础Import注解说明_笔记(1) 以下为AI生成的图文笔记的内容 一、自动配置原理 00:34 1. 配置类使用示例 02:06 1)组件准备 ● ● 组件注解:使用@Component注解标记User类,使其成为Spring管理的组件 ● 属性注入:通过@Value注解实现属性值注入,支持表达式如@Value("${fanren.name:名}")设置默认值 ● 基础结构:组件类需要包含toString()方法以便调试输出 2)配置类编写 ● ● 核心注解: ○ @Configuration:标识当前类为配置类,替代XML配置文件 ○ @ComponentScan:指定扫描包路径(如"com.atguigu.java.ioc_01") ○ @PropertySource:加载外部配置文件(如"classpath:user.properties") ● 功能划分:配置类主要实现三大功能:声明配置、资源加载、组件扫描 3)容器初始化 ● ● 创建方式: ○ 方案1:直接创建AnnotationConfigApplicationContext并传入配置类 ○ 方案2:分步创建容器→注册配置类→调用refresh()方法 ● 注意事: ○ 不同方式创建的容器实例相互独立(user != user2) ○ 必须调用refresh()方法才能使配置生效 4)组件注册三步骤 ● ● 完整流程: ○ 准备组件类并添加相应注解(如@Component) ○ 编写配置类进行组件扫描和资源配置 ○ 创建应用上下文加载配置类 ● 关键点: ○ 组件扫描范围需要包含组件所在包 ○ 外部资源配置需要在属性注入前加载 ○ 容器刷新是必要的初始化步骤 2. 配置类批量加载和读取 03:36 1)基础配置方式 ● ● 方案1:直接创建配置类 ● 方案2:分步创建容器 2)多配置类加载方案 ● 方案1:构造函数可变参数 ○ ○ 实现方式:通过构造函数一次性传入多个配置类 ○ 特点: ■ 可以完成多个配置类加载 ■ 只能加载固定已知的配置类 ■ 不适合动态加载场景 ● 方案2:@Import注解 ○ ○ 实现步骤: ■ 创建主配置类 ■ 使用@Import导入其他配置类 ○ 特点: ■ 需要定义主配置类 ■ 仍然需要显式指定所有配置类 ■ 相比方案1代码组织更清晰 3)配置类编写规范 ● ● 基本结构: ● 注意事: ○ 必须添加@Configuration注解 ○ @Bean方法名默认作为bean名称 ○ 可以添加初始化逻辑和打印语句验证加载过程 4)实际应用建议 ● 选择依据: ○ 少量固定配置类:直接使用方案1 ○ 需要更好组织性:使用方案2 ○ 需要动态加载:考虑其他方案(如组件扫描) ● 最佳实践: ○ 保持配置类单一职责 ○ 合理分组相关配置 ○ 主配置类只做导入不做具体配置 3. 配置类动态导入 14:08 1)方案一:利用构造函数可变参数进行传入多个配置类 ● ● 实现方式:通过AnnotationConfigApplicationContext构造函数的可变参数传入多个配置类 ● 特点: ○ 只能加载固定的已知配置类 ○ 代码示例: ● 局限性:配置类需要预先确定,无法动态变更 2)方案二:@Import导入其他的组件和配置类 14:40 ● ● 实现方式: ○ 定义一个主配置类 ○ 使用@Import注解指定其他要加载的配置类 ● 特点: ○ 示例代码: ○ 仍然需要预先确定配置类 ● 局限性:与方案一类似,配置类是"写死"的,无法动态加载 3)方案三:@Import注解动态导入配置类 14:53 ● 实现原理 ○ ○ 核心机制:@Import注解可以接受两种参数: ■ 配置类(Configuration) ■ ImportSelector接口的实现类 ○ 关键接口:ImportSelector ■ 定义方法:String[] selectImports(AnnotationMetadata importingClassMetadata) ■ 返回需要导入的配置类的全限定名数组 ■ 允许在方法中编写动态逻辑 ● 实现步骤 ○ ○ 创建主配置类并添加@Configuration注解 ○ 实现ImportSelector接口 ○ 在selectImports方法中返回配置类全限定名数组 ○ 在主配置类的@Import中指定ImportSelector实现类 4)例题1:实现ImportSelector接口动态导入 16:49 ● ● 实现代码: public class MyImportSelector implements ImportSelector { @Override public String[] selectImports(AnnotationMetadata importingClassMetadata) { return new String[]{ "com.atguigu.java.config_03.TeacherConfiguration", "com.atguigu.java.config_03.StudentConfiguration", "com.atguigu.java.config_03.UserConfiguration"}; } } ● 使用方式: @Import(MyImportSelector.class) @Configuration public class DynMainConfiguration {} 5)例题2:测试动态导入配置类 18:56 ● ● 测试代码: ApplicationContext context2 = new AnnotationConfigApplicationContext(DynMainConfiguration.class); String[] beanDefinitionNames2 = context2.getBeanDefinitionNames(); System.out.println(Arrays.toString(beanDefinitionNames2)); ● 优势: ○ 可以在selectImports方法中编写动态逻辑 ○ 相比前两种方案更加灵活 ○ 实现了配置类的"按需加载"能力 ● 注意事: ○ 配置类的全限定名必须准确 ○ 返回的数组不能包含不存在的类名 4. 三种配置类方式总结 20:05 1)方案1:利用构造函数可变参数传入多个配置类 ● 实现方式:通过AnnotationConfigApplicationContext构造函数直接传入多个配置类 ● 特点: ○ 可以加载多个配置类 ○ 只能加载固定的已知配置类 ○ 属于硬编码方式,不够灵活 ● 示例代码: 2)方案2:@Import导入其他组件和配置类 20:37 ● ● 实现方式: ○ 定义一个主配置类 ○ 在主配置类上使用@Import注解指定其他要加载的配置类 ● 特点: ○ 本质上与方案1相同,仍然是写死的配置 ○ 无法实现动态加载 ○ 相比方案1只是将配置集中到了主配置类中 ● 示例代码: 3)方案3:@Import注解动态导入配置类 21:00 ● 实现方式: ○ 主配置类使用@Import注解 ○ 导入ImportSelector接口的实现类 ○ 在selectImports方法中返回需要加载的配置类全限定名数组 ● 特点: ○ 真正实现了动态加载 ○ 可以在运行时决定加载哪些配置类 ○ 通过方法体中的逻辑实现灵活配置 4)例题1:MyImportSelector实现 21:21 ● ● 实现要点: ○ 实现ImportSelector接口 ○ 重写selectImports方法 ○ 返回配置类的全限定名数组 ● 示例代码: 5)动态配置类加载的实现方式 22:24 ● ● 核心思想: ○ 将配置类信息存储在外部配置文件中 ○ 在selectImports方法中读取配置文件 ○ 动态决定加载哪些配置类 ● 实现步骤: ○ 创建配置文件(如configs.properties) ○ 在配置文件中列出需要加载的配置类全限定名 ○ 在selectImports方法中读取配置文件内容 ○ 将读取到的配置类信息转换为数组返回 ● 优势: ○ 配置灵活可调整 ○ 无需修改代码即可改变加载的配置类 ○ 适合生产环境使用 6)@Import注解的作用 23:33 ● ● 主要功能: ○ 导入其他配置类或组件 ○ 支持静态和动态两种导入方式 ● 使用方式: ○ 直接指定配置类(静态) ○ 通过ImportSelector实现动态导入 ● 动态导入原理: ○ Spring会调用ImportSelector的selectImports方法 ○ 方法返回值决定实际加载的配置类 ○ 可以在方法体中实现复杂的加载逻辑 ● 应用场景: ○ 需要根据条件动态加载不同配置 ○ 需要从外部配置文件决定加载内容 ○ 实现模块化、可插拔的配置方案 二、知识小结 知识点 核心内容 考试重点/易混淆点 难度系数 Spring自动配置原理 通过@Import注解实现配置类动态加载,支持直接指定配置类或通过ImportSelector接口动态返回配置类全限定名 @Import的两种用法:静态导入(直接写配置类) vs 动态导入(实现ImportSelector接口) ⭐⭐⭐⭐ 多配置类加载方案 三种实现方式: 1. 核心容器构造函数直接传入多个配置类 2. 主配置类通过@Import静态导入子配置类 3. 主配置类通过@Import+ImportSelector动态加载子配置类 动态加载的核心优势:通过外部配置文件灵活控制加载的配置类清单 ⭐⭐⭐ @Import注解作用 用于导入其他配置类或组件,支持: - 直接指定配置类Class对象 - 通过ImportSelector接口实现类动态返回配置类全限定名字符串 易混淆点:@Import与@ComponentScan的区别(前者精确导入,后者包扫描) ⭐⭐⭐ ImportSelector接口 关键方法selectImports()需返回配置类的全限定名字符串数组,可通过读取外部配置文件实现动态加载 实现类需重写selectImports()方法,方法体内可嵌入动态逻辑(如读取properties文件) ⭐⭐⭐⭐ 配置类生效流程 三步生效机制: 1. 组件类加注解(如@Component) 2. 配置类扫描组件(@Configuration+@ComponentScan) 3. 核心容器加载配置类 常见错误:漏掉容器加载步骤或扫描路径配置错误 ⭐⭐⭐ 03_springboot_自动配置原理_基础Conditional注解说明_笔记(1) 以下为AI生成的图文笔记的内容 一、条件注解 00:00 1. @Conditional注解 01:22 ● ● 作用:添加到@Bean方法上,用于配置条件,只有满足条件时Bean才会生效 ● 应用场景: ○ 主要用于第三方框架开发 ○ 普通开发中较少使用(因为我们通常确定Bean需要生效) ○ 实现按需加载的核心原理 1)使用方式 01:54 ● ● 语法:@Conditional(条件类.class) ● 要求: ○ 必须指定value值 ○ value值必须是实现了Condition接口的类 ● 特点: ○ 不支持继承(父类的条件不会被考虑) ○ 可以用于类级别(影响所有Bean方法)或方法级别 2)实现接口 03:11 ● ● 实现步骤: ○ 创建类实现org.springframework.context.annotation.Condition接口 ○ 实现matches()方法 ● matches方法: ○ 返回boolean类型 ○ 参数: ■ ConditionContext:条件上下文 ■ AnnotatedTypeMetadata:注解元数据 ○ 返回true表示条件匹配,Bean生效 ○ 返回false表示条件不匹配,Bean不生效 3)写判定逻辑 04:03 ● ● 逻辑编写: ○ 在matches()方法中编写条件判断逻辑 ○ 可以根据上下文环境动态决定是否生效 ● 示例: 4)使用示例 04:16 ● ● 配置类示例: ● 效果: ○ 即使配置了@Bean方法,如果条件不满足,该Bean也不会被注册到容器中 ○ 示例中虽然定义了Teacher Bean,但由于条件返回false,实际容器中不会有Teacher实例 二、自动配置原理总结 05:06 1. 简化配置 05:17 ● ● 核心思想: ○ 无需手动编写配置类和配置文件 ○ SpringBoot提供预置配置类 ○ 按需加载配置类并将组件加入IoC容器 ● 实现机制: ○ 使用@Import和@Conditional注解组合 ○ @Import用于导入多个配置类 ○ @Conditional用于条件判断,决定哪些配置类/Bean应该生效 ● 优势: ○ 开发者只需专注业务逻辑 ○ 第三方集成更加灵活 ○ 实现"约定优于配置"的理念 三、知识小结 04_springboot_自动配置原理_详细说明_笔记(1) 知识点 核心内容 考试重点/易混淆点 难度系数 @Conditional注解的作用 用于控制@Bean是否生效,需满足指定条件才会加载 第三方框架开发时常用,与普通开发中直接生效的@Bean区别 ⭐⭐⭐ 条件判断逻辑实现 需实现Condition接口,在matches()方法中返回布尔值决定生效性 false时即使声明@Bean也不生效,动态条件需通过代码逻辑实现 ⭐⭐⭐⭐ 自动配置原理关联 Spring Boot自动配置通过@Conditional实现按需加载 多个配置类的选择性加载依赖条件注解 ⭐⭐⭐⭐ 注解使用语法 @Conditional(value=MyCondition.class),需指定实现类 泛型要求:参数类必须继承Condition接口 ⭐⭐ IDE操作提示 内部类标识(三角符号)、接口(I符号)等IDE图标含义 易混淆Condition接口与其他同名类(如德鲁伊数据源的类) ⭐⭐ 04_springboot_自动配置原理_详细说明_笔记(1) 以下为AI生成的图文笔记的内容 一、自动配置原理 00:06 1. 启动类和其他类不同 01:29 1)启动类和其他的类不同点 02:58 ● ● 核心特征:启动类与其他类的本质区别在于包含@SpringBootApplication注解 ● 程序入口:具有main方法作为程序执行的唯一入口,可以启动整个应用程序 ● 复合注解:@SpringBootApplication是一个复合注解,实际集成了三个核心注解: ○ @SpringBootConfiguration ○ @EnableAutoConfiguration ○ @ComponentScan ● 主配置类:启动类本质上是Spring Boot应用的主配置类,负责初始化核心容器 ● ● 注解层次:@SpringBootApplication通过元注解方式组合了多个功能注解 ● 包装特性:@SpringBootConfiguration本身也是一个包装注解,最终指向@Configuration ● 自动配置:@EnableAutoConfiguration负责第三方组件的自动配置加载 ● 组件扫描:@ComponentScan默认扫描启动类所在包及其子包下的组件 2)@SpringBootConfiguration注解分析 03:14 ● ● 配置类本质:@SpringBootConfiguration注解最终指向标准的@Configuration注解 ● 主配置类:启动类作为主配置类,是Spring容器初始化的核心配置来源 ● 容器创建:核心容器创建时机是在main方法中调用SpringApplication.run()时 ● 运行机制:SpringApplication.run()方法内部会: ○ 接收主配置类作为参数 ○ 创建AnnotationConfigApplicationContext核心容器 ○ 返回初始化完成的应用上下文对象 ○ ● 执行链路:完整启动流程为:启动类 → main方法 → SpringApplication.run() → 创建IoC容器 ● 参数传递:run()方法接收的Main.class参数实际上是被作为主配置类使用 ● 容器类型:底层创建的是AnnotationConfigApplicationContext而非传统的XML容器 ● 自动扫描:由于@ComponentScan的存在,启动类所在包下的组件会被自动扫描注册 2. 类扫描 06:24 1)如何排除类和第三方的配置类 06:31 ● ● 启动类本质:启动类本身就是一个配置类,是主配置类 ● 核心容器创建时机:通过SpringApplication.run(Main.class,args)方法创建,底层实际上是new AnnotationConfigApplicationContext(Main.class) ● 自动扫描范围:默认扫描启动类所在包及其子包下的组件 2)@Import注解的使用 07:18 ● ● 默认扫描规则:当@ComponentScan不指定包时,默认扫描当前配置类所在包 ● 手动添加组件:可以通过在配置类上添加@ComponentScan注解来显式指定扫描范围 ● @Import作用:用于导入其他配置类,如@Import({StudentConfiguration.class,TeacherConfiguration.class}) 3)@SpringBootApplication注解详解 07:53 ● ● 复合注解:包含三个核心注解: ○ @SpringBootConfiguration:标识为配置类 ○ @EnableAutoConfiguration:启用自动配置 ○ @ComponentScan:组件扫描 ● 默认扫描行为:未指定包时,自动扫描启动类所在包 ● 自定义扫描:可通过scanBasePackages属性指定扫描包,如@SpringBootApplication(scanBasePackages="com.example") ● 开发建议:遵循约定优于配置原则,通常不需要显式指定扫描包 ● ● 最佳实践: ○ 业务类应放在启动类同包或子包下 ○ 避免随意修改默认扫描范围 ○ 第三方配置类可通过exclude属性排除 ● 注意事: ○ 显式指定scanBasePackages会覆盖默认行为 ○ 修改扫描范围可能导致某些类不被扫描 ○ 自动配置类排除需谨慎 3. 自动配置原理 10:20 1)第三方配置类 11:46 ● 启动类特性: ○ 包含三个核心注解:@SpringBootConfiguration(本质是@Configuration)、@EnableAutoConfiguration、@ComponentScan ○ 是主配置类,通过SpringApplication.run()创建核心容器(AnnotationConfigApplicationContext) ○ 默认扫描启动类所在包及其子包,可通过@SpringBootApplication(scanBasePackages)修改但不推荐 ○ ● 配置类来源: ○ SpringBoot官方场景启动器(spring-boot-starter-xxx)的配置类由SpringBoot团队编写 ○ 第三方整合(如mybatis-spring-boot-starter)的配置类由第三方自行编写 ○ 配置类统一存放在META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件中 2)清单文件 18:53 ● ● 作用机制: ○ 将所有配置类的全限定名存储在固定位置的清单文件中 ○ SpringBoot3使用META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ○ 每个starter模块都有自己的清单文件,如mybatis存放在mybatis-spring-boot-autoconfigure中 ● 版本差异: ○ SpringBoot2和3的主要区别在于清单文件格式不同 ○ 未升级支持Boot3的场景启动器无法正常使用,因为找不到对应的清单文件 ○ 解决方案:可手动创建清单文件添加配置类路径实现兼容 3)配置类加载 22:26 ● 加载原理: ○ 通过@EnableAutoConfiguration注解触发自动配置流程 ○ 核心是读取各starter模块清单文件中的配置类信息 ○ 实际加载时会进行条件过滤(@Conditional),并非所有配置类都会生效 ● 典型配置类: ○ 命名规范:XxxAutoConfiguration ○ 示例:AopAutoConfiguration包含多个内部配置类,根据条件动态启用 ○ SpringBoot官方提供了142个自动配置类(SpringBoot3.0.5版本) 二、自动配置原理 22:43 1. 自动配置原理概述 22:58 ● ● 配置类存储位置: ○ SpringBoot官方配置类:存储在spring-boot-starter-autoconfigure中 ○ 第三方配置类:如MyBatis配置类存储在mybatis-spring-boot-starter-autoconfigure中 ● 清单文件作用: ○ 存储所有配置类的全限定符信息,路径为META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ○ SpringBoot2和SpringBoot3清单文件格式不同,不兼容会导致启动器无法使用 2. 配置类批量自动加载 23:14 ● ● 加载流程: ○ 启动类→主配置类→@SpringBootApplication→@EnableAutoConfiguration→@Import→ImportSelector ○ 底层通过ImportSelector接口加载固定清单文件 ● 核心组件: ○ AutoConfigurationImportSelector类实现批量加载 ○ 通过getAutoConfigurationEntry()方法加载142个配置类 3. 配置类批量自动加载详解 27:03 1)配置类生效问题 27:07 ● ● 生效条件: ○ 配置类考虑所有场景,但目可能不使用部分功能 ○ 通过@Conditional系列注解实现条件化注入 ● 条件类型: ○ 导入某个包拥有某个类生效(如@ConditionalOnClass) ○ 没有某个组件生效(如@ConditionalOnMissingBean) ○ 必须包含某个参数key生效 2)应用案例 27:53 ● 例题:配置类GsonAutoConfiguration ○ ○ 条件注解: ■ @ConditionalOnClass(Gson.class):存在Gson类时生效 ■ @ConditionalOnMissingBean:不存在指定Bean时生效 ○ 实现原理: ■ 导入了Gson相关依赖才会加载该配置类 ■ 确保不会重复创建已存在的Bean实例 3)应用案例 34:25 ● 例题:配置类排除问题 ○ ○ 问题现象: ■ 导入MyBatis和MySQL依赖后报错 ■ 错误信息:缺少DataSource必要参数(url等) ○ 原因分析: ■ 导入依赖导致DataSourceAutoConfiguration生效 ■ 需要四个必要参数但未配置 4. 配置类排除问题详解 36:12 ● ● 解决方案: ○ 方案1:填写必要参数(如spring.datasource.url) ○ 方案2:排除自动配置类(@SpringBootApplication(exclude=DataSourceAutoConfiguration.class)) ● 注意事: ○ 排除功能仅适用于第三方框架的自动配置类 ○ 最佳实践是不要随意导入不需要的依赖 5. 应用案例 38:43 1)例题:配置类排除问题 ● ● 自定义配置类: ○ 放在启动类同包或子包下会被自动扫描 ○ 通过@Configuration注解声明 ● 验证方法: ○ 启动应用后查看Bean定义列表 ○ 确认myConfiguration是否被成功加载 三、问题引入 39:43 ● ● 配置类依赖问题:当配置类需要特定参数才能运行时,若缺少必要参数会导致目无法启动 ● 多服务场景冲突:在多个服务共用公共配置时,可能出现部分服务不需要某些配置的情况 ● 解决方案选择:可以通过排除配置或条件化加载来解决,但需要考虑实际开发中的维护成本 四、自动配置原理总结 40:50 ● ● 配置类来源: ○ SpringBoot官方:通过spring-boot-starter-xxx提供,存储在spring-boot-starter-autoconfigure中 ○ 第三方框架:如mybatis-spring-boot-starter,存储在对应的autoconfigure包中 ● 清单文件机制: ○ 存储位置:META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ○ 版本差异:SpringBoot2和3的清单文件格式不同,不兼容会导致启动器失效 ● 加载流程: ○ 启动类→主配置类→@SpringBootApplication→@EnableAutoConfiguration ○ 通过@Import和ImportSelector读取清单文件加载配置类 ○ ● 条件过滤机制: ○ @ConditionalOnClass:当类路径存在指定类时生效 ○ 参数依赖:需要特定配置参数才会生效 ○ 组件检查:根据容器中是否存在指定组件决定是否加载 ● 常见问题: ○ 导入依赖后自动配置类生效但缺少必要参数(如DataSource需要url等4个参数) ○ 建议:不要随意添加不需要的依赖 ● 排除方法: ○ 方案1:提供必要参数(如配置spring.datasource.url) ○ 方案2:使用@SpringBootApplication(exclude=DataSourceAutoConfiguration.class) ○ 方案3:通过@ComponentScan的excludeFilters排除指定配置类 ● 开发建议: ○ 公共配置需要谨慎设计,避免引起服务间冲突 ○ 排除第三方配置的频率可能高于自定义配置的排除 五、知识小结 知识点 核心内容 考试重点/易混淆点 难度系数 Spring Boot启动类特性 启动类本质是主配置类,包含@SpringBootApplication复合注解(集成@Configuration、@ComponentScan、@EnableAutoConfiguration) 与其他类的区别在于注解组合及作为程序入口 ⭐⭐ 核心容器创建时机 在main()方法调用SpringApplication.run()时触发,传入启动类(主配置类)创建AnnotationConfigApplicationContext 需明确run()方法内部通过主配置类初始化容器 ⭐⭐⭐ 组件扫描规则 @ComponentScan未指定包时默认扫描启动类同级及子包,显式指定scanBasePackages会覆盖默认规则 约定大于配置原则,不推荐手动指定包路径 ⭐⭐ 自动配置原理 1. 第三方/官方预写配置类存储在META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 2. 通过@EnableAutoConfiguration的@Import(AutoConfigurationImportSelector.class)动态加载清单文件 Spring Boot 2与3差异仅在于清单文件路径(spring.factories→AutoConfiguration.imports) ⭐⭐⭐⭐ 条件化注入机制 配置类通过@Conditional系列注解(如@ConditionalOnClass)控制生效条件,依赖包存在/参数配置/组件缺失等 误导入依赖可能导致非预期配置类生效(如MyBatis需数据库参数) ⭐⭐⭐ 配置类排除方案 1. 第三方配置:@SpringBootApplication(exclude=DataSourceAutoConfiguration.class) 2. 自定义配置:在@ComponentScan中添加excludeFilters按类型排除 排除自定义类需通过扫描过滤器,仅第三方支持exclude语法 ⭐⭐⭐ 05_springboot_自动配置原理_自定义场景启动器_笔记(1) 以下为AI生成的图文笔记的内容 一、自定义场景启动器 00:00 1. 非正式课程 00:02 ● ● 核心作用:确保核心组件自动加入IoC容器 ● 实现原理:通过自动配置类+清单文件实现组件自动加载 ● 典型场景:公司内部工具类目需要被多个模块复用 2. 创建工程 01:56 ● ● 目结构: ○ 必须继承spring-boot-starter-parent ○ 命名规范:xxx-spring-boot-starter ● 关键配置: 3. 引入依赖 02:21 ● ● 基础依赖:必须引入spring-boot-starter ● 模块依赖:根据功能需要添加特定依赖 ● 版本管理:通过父工程统一管理依赖版本 4. 创建工具类 02:53 ● ● 示例工具类: ● 设计原则:工具方法应保持无状态 5. 自动配置类定义 05:27 ● ● 配置类规范: ○ 使用@Configuration注解 ○ 类名格式:XxxAutoConfiguration ○ 通过@Bean方法注册组件 ● 示例配置: 6. 编写配置文件 06:46 ● ● 文件位置:META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ● 内容格式:写入配置类的全限定名 ● Boot2/3区别:Boot3使用新格式清单文件 7. 配置类批量自动加载 07:31 ● ● 加载流程: ○ 启动类→主配置类→@EnableAutoConfiguration ○ 通过ImportSelector读取清单文件 ○ 加载所有配置类 ● 条件过滤:通过@Conditional系列注解过滤不需要的配置 8. 应用案例 08:37 1)例题:配置类使用示例 ● ● 使用方式: ● 验证方法:通过打印所有Bean定义确认组件已加载 9. 自定义场景启动器作用 09:10 ● 核心价值:简化组件使用流程,实现"开箱即用" ● 与传统工具包区别:自动完成组件注册,无需手动配置 ● 适用场景:公司内部通用组件、跨目共享工具 10. 自定义场景启动器步骤 09:36 ● ● 标准流程: ○ 创建工具类 ○ 编写自动配置类 ○ 配置清单文件 ○ 打包发布 ○ 其他目引入使用 ● 注意事:包名应避免与SpringBoot官方冲突 二、知识小结 根据上述资料,按照下列结构,详细系统的总结springboot的自动配置核心流程,一个流程步骤一段话 ● 自动配置核心流程 ○ 启动触发 ○ 加载配置类 ○ 过滤配置类 ○ 组件扫描与属性注入
最新发布
11-03
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值