SpringBoot自动配置和自定义starter

本文详细探讨了SpringBoot的自动配置原理,包括依赖管理、XML配置的替代方案以及自动配置的实现。SpringBoot通过预先配置的jar包,简化了项目的搭建和配置,依赖于starter依赖管理。此外,文章深入源码分析了SpringApplication的初始化和运行过程,以及@SpringBootApplication注解的作用。文章还介绍了如何实现自定义starter,包括定义Properties类、配置类以及创建Spring.factories文件。通过这些步骤,读者可以理解并创建自己的自动配置模块。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. SpringBoot做了什么?

可以看到我们利用SpringBoot搭建好工程,开始专注业务开发,省去了所有的配置、依赖管理,非常简单!

那么这些省去的配置、依赖SpringBoot是什么时候帮我们完成的呢?
在这里插入图片描述

1.1 依赖管理

首先来看SpringBoot如何帮我们管理依赖及其版本信息。

打开项目的pom.xml文件,可以看到我们的项目依赖比较简单:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>cn.itcast</groupId>
    <artifactId>bank</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>bank</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
        <skipTests>true</skipTests>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

这里包含了4部分内容,分别是:

  • 父工程配置
  • Properties配置
  • 依赖项
  • 插件
1)父工程配置

这个项目有一个父工程配置:

在这里插入图片描述

可以看到使用的SpringBoot版本是最新的2.1.6RELEASE 版本。跟入这个pom查看,发现又继承了另一个父工程:

在这里插入图片描述

继续跟入,发现里面已经管理了各种依赖及其版本了,列举一部分大家看:

在这里插入图片描述

因此,我们的项目需要引入依赖时,只需要指定坐标,版本都有SpringBoot管理,避免了依赖间的冲突。

2)Properties:

properties中主要定义了项目的JDK版本:

在这里插入图片描述

3)依赖项:

这里我们总共引入了3个依赖项:

在这里插入图片描述

但是,看看项目中实际引入了哪些jar包:

在这里插入图片描述

当我们移除两个spring-boot-starter命名的依赖项时,可以发现所有的jar都消失了:

在这里插入图片描述

也就是说,在下面的这个依赖项(starter)中:

<!--web工程起步依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

定义好了一个web工程所需的所有依赖,当我们引入依赖时,无需像以前那样一个一个寻找需要的依赖坐标并且验证版本是否冲突,只需要引入这个starter即可!

4)插件:

最后是一个插件:

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

这个是SpringBoot工程在打包时用到的插件。

5)总结:

因为SpringBoot对于依赖的管理,我们搭建项目时只需要引入starter,无需再依赖引入上话费更多时间,大大提高了开发效率。

1.2 xml配置去哪儿了?

在以前,我们搭建一个web工程,至少需要下列配置文件:

  • tomcat配置:web.xml(包括spring监听器、SpringMVC的DispatcherServlet、各种过滤器)
  • SpringMVC配置:springmvc.xml(包括注解驱动、视图解析器等)
  • Spring配置:applicationContext.xml(包括数据源、Bean扫描、事务等)
  • mybatis配置:mybatis-config.xml全局配置,mybatis与spring整合配置、mapper映射文件配置

完成所有配置需要花费大量时间,每天都淹没在xml的海洋中,整个人生都陷入了被xml支配的恐惧、黑暗当中。

直到有一天,SpringBoot如同一页方舟,将你从xml海洋中拯救出来。

在SpringBoot中,就用Java代码配置代替了以前繁杂的xml配置,而且这些配置都已经放到了SpringBoot提供的jar包中,因此我们引入starter依赖的那一刻,这些配置都已经生效了!这就是springBoot的自动配置功能。

SpringBoot的自动配置原理

1.3 SpringBoot自动配置初探

回到开始的问题,SpringBoot取消了xml,并且完成了框架的自动配置,那么是如何实现的?

其实在SpringBoot的中,已经提前利用Java配置的方式,为Spring平台及第三方库做好了默认配置,并打包到了依赖中。当我们引入SpringBoot提供的starter依赖时,就获得了这些默认配置,因此我们就无需配置,开箱即用了!

默认配置的位置如图:
在这里插入图片描述
其中就包括了SpringMVC的配置:
在这里插入图片描述
并且在这些类中,定义了大量的Bean,包括以前我们自己需要配置的如:ViewResolver、HandlerMapping、HandlerAdapter等。

在这里插入图片描述

因此,我们就无需配置了。

至于,这些配置是如何被加载的?怎样生效的,我们进行源码分析。

2.源码分析

接下来我们来看看SpringBoot的源码,分析下SpringBoot的自动配置原理。

2.1.启动类

整个项目 的入口是带有main函数的启动类:BankApplication

package cn.itcast.bank;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class BankApplication {

    public static void main(String[] args) {
        SpringApplication.run(BankApplication.class, args);
    }
}

这里跟SpringBoot有关联的部分有两个,一个是SpringApplication.run(BankApplication.class, args);,另一个就是启动类上的注解:@SpringBootApplication

我们分别来跟踪这两部分源码。

2.2.SpringApplication类的初始化和run

看下整个类的介绍:

在这里插入图片描述

可以看出,核心作用就是从主函数加载一个Spring的应用

其中启动应用的是几个run方法:

/**
	 * Static helper that can be used to run a {@link SpringApplication} from the
	 * specified source using default settings.
	 * @param primarySource the primary source to load
	 * @param args the application arguments (usually passed from a Java main method)
	 * @return the running {@link ApplicationContext}
	 */
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
    return run(new Class<?>[] { primarySource }, args);
}

/**
	 * Static helper that can be used to run a {@link SpringApplication} from the
	 * specified sources using default settings and user supplied arguments.
	 * @param primarySources the primary sources to load
	 * @param args the application arguments (usually passed from a Java main method)
	 * @return the running {@link ApplicationContext}
	 */
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
    return new SpringApplication(primarySources).run(args);
}

最终走的是第二个run方法。而这个方法做两件事情:

  • new SpringApplication(primarySources):创建本类实例
  • run(args):运行Spring应用

2.2.1.构造函数

我把跟构造函数有关的几个变量和方法提取出来,方便查看:

// SpringApplication.java

/**
 * 资源加载器
 */
private ResourceLoader resourceLoader;
/**
 * SpringBoot核心配置类的集合,这里只有一个元素,是我们传入的主函数
 */
private Set<Class<?>> primarySources;
/**
 * 应用类型
 */
private WebApplicationType webApplicationType;

/**
 * ApplicationContextInitializer 数组
 */
private List<ApplicationContextInitializer<?>> initializers;
/**
 * ApplicationListener 数组
 */
private List<ApplicationListener<?>> listeners;

public SpringApplication(Class<?>... primarySources) {
    this(null, primarySources);
}

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
    this.resourceLoader = resourceLoader;
    Assert.notNull(primarySources, "PrimarySources must not be null");
    // 将传入的启动类装入集合
    this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
    // 判断当前项目的类型,可以是SERVLET、REACTIVE、NONE
    this.webApplicationType = WebApplicationType.deduceFromClasspath();
    // 初始化 initializers 数组
    setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
    // 初始化 listeners 数组
    setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
    this.mainApplicationClass = deduceMainApplicationClass();
}

注解:

  • ResourceLoader resourceLoader:Spring中用来加载资源(配置文件)的加载器
  • Class<?>... primarySources:这里是Config配置类,本例中就是BankApplication
  • WebApplicationType.deduceFromClasspath():判断当前项目的类型,可以是SERVLET、REACTIVE、NONE,根据当前classpath中包含的class来判断,会影响后续创建的ApplicationContext的类型
  • getSpringFactoriesInstances(ApplicationContextInitializer.class)
    获取ApplicationContextInitializer类型的实现类对象数组
  • getSpringFactoriesInstances(ApplicationListener.class):获取ApplicationListener类型的实现类对象数组
  • deduceMainApplicationClass():没有实际用途,打印日志,输出当前启动类名称
2.2.1.1.deduceFromClasspath()

判断项目类型:

static WebApplicationType deduceFromClasspath() {
    if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
        && !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
        return WebApplicationType.REACTIVE;
    }
    for (String className : SERVLET_INDICATOR_CLASSES) {
        if (!ClassUtils.isPresent(className, null)) {
            return WebApplicationType.NONE;
        }
    }
    return WebApplicationType.SERVLET;
}

可以看到判断结果包含3种:

  • REACTIVE:要求classpath中包含org.springframework.web.reactive.DispatcherHandler这个是WebFlux中的核心处理器,我们并没有。
  • SERVLET:要求classpath中包含org.springframework.web.servlet.DispatcherServlet,这是SpringMVC的核心控制器,在classpath中肯定可以找到(web项目)
  • NONE:以上都不满足,就是NONE
2.2.1.2.getSpringFactoriesInstances

getSpringFactoriesInstances(Class<T> type) 方法,获得指定类型的实现类对象的实例。

private <T> Collection<T> getSpringFactoriesInstances(Class<T> type) {
    return getSpringFactoriesInstances(type, new Class<?>[] {});
}

private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
    ClassLoader classLoader = getClassLoader();
    // Use names and ensure unique to protect against duplicates
    // 1.先加载指定类型的实现类的名称集合
    Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
    // 2.根据类的名称,创建对应实例
    List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
    // 3.排序
    AnnotationAwareOrderComparator.sort(instances);
    return instances;
}

这里关键是第1步中,调用SpringFactoriesLoader.loadFactoryNames(type, classLoader)方法,跟入进去

2.2.1.3.loadFactoryNames方法

SpringFactoriesLoader.loadFactoryNames(type, classLoader)方法:

// SpringFactoriesLoader
/**
  * 使用指定的类加载器,加载{@value #FACTORIES_RESOURCE_LOCATION}中记录的,指定factoryClass
  * 类型的实现类的全路径名。
  * @param factoryClass 需要加载的接口或抽象类
  * @param classLoader 用来加载资源的类加载器
  */
public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
    // 获取接口名称
    String factoryClassName = factoryClass.getName();
    // 从loadSpringFactories(classLoader)方法返回的Map中根据factoryClass名称获取对应的实现类名称数组
    return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
}

private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
    // 尝试从缓存中获取结果
    MultiValueMap<String, String> result = cache.get(classLoader);
    if (result != null) {
        return result;
    }

    try {
        // 从默认路径加载资源文件,地址是:"META-INF/spring.factories"
        Enumeration<URL> urls = (classLoader != null ?
                                 classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
                                 ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
        // 创建空map
        result = new LinkedMultiValueMap<>();
        // 遍历资源路径
        while (urls.hasMoreElements()) {
            // 获取某个路径
            URL url = urls.nextElement();
            UrlResource resource = new UrlResource(url);
            // 加载文件内容,文件中是properties格式,key是接口名,value是实现类的名称以,隔开
            Properties properties = PropertiesLoaderUtils.loadProperties(resource);
            for (Map.Entry<?, ?> entry : properties.entrySet()) {
                // 获取key的 名称
                String factoryClassName = ((String) entry.getKey()).trim();
                // 将实现类字符串变成数组并遍历,然后添加到结果result中
                for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
                    result.add(factoryClassName, factoryName.trim());
                }
            }
        }
        // 缓存中放一份,下次再加载可以从缓存中读取
        cache.put(classLoader, result);
        return result;
    }
    catch (IOException ex) {
        throw new IllegalArgumentException("Unable to load factories from location [" +
                                           FACTORIES_RESOURCE_LOCATION + "]", ex);
    }
}

此处,加载的资源是/META-INF/spring.factories文件中的properties:

在这里插入图片描述

内容类似这样:

# PropertySource Loaders
org.springframework.boot.env.PropertySourceLoader=\
org.springframework.boot.env.PropertiesPropertySourceLoader,\
org.springframework.boot.env.YamlPropertySourceLoader

# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\
org.springframework.boot.context.event.EventPublishingRunListener

# Error Reporters
org.springframework.boot.SpringBootExceptionReporter=\
org.springframework.boot.diagnostics.FailureAnalyzers

# Application Context Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\
org.springframework.boot.context.ContextIdApplicationContextInitializer,\
org.springframework.boot.context.config.DelegatingApplicationContextInitializer,\
org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.ClearCachesApplicationListener,\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\
org.springframework.boot.context.FileEncodingApplicationListener,\
org.springframework.boot.context.config.AnsiOutputApplicationListener,\
org.springframework.boot.context.config.ConfigFileApplicationListener,\
org.springframework.boot.context.config.DelegatingApplicationListener,\
org.springframework.boot.context.logging.ClasspathLoggingApplicationListener,\
org.springframework.boot.context.logging.LoggingApplicationListener,\
org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener

根据传入的接口名称,就可以寻找到对应的实现类,例如org.springframework.boot.SpringApplicationRunListener.

结束后,把得到的名字集合传递给createSpringFactoriesInstance方法

2.2.1.4.createSpringFactoriesInstances

然后看看#createSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, ClassLoader classLoader, Object[] args, Set<String> names) 方法,创建对象的代码:

/**
 * 根据类的全名称路径数组,创建对应的对象的数组
 *
 * @param type 父类类型
 * @param parameterTypes 构造方法的参数类型
 * @param classLoader 类加载器
 * @param args 构造方法参数
 * @param names 类全名称的数组
 */
private <T> List<T> createSpringFactoriesInstances(Class<T> type,
		Class<?>[] parameterTypes, ClassLoader classLoader, Object[] args,
		Set<String> names) {
    // 定义空实例集合
	List<T> instances = new ArrayList<>(names.size()); 
	// 遍历 names 数组
	for (String name : names) {
		try {
			// 获得类名称 name
			Class<?> instanceClass = ClassUtils.forName(name, classLoader);
			// 判断类是否实现自 type 类
			Assert.isAssignable(type, instanceClass);
			// 获得构造方法
			Constructor<?> constructor = instanceClass.getDeclaredConstructor(parameterTypes);
			// 创建对象
			T instance = (T) BeanUtils.instantiateClass(constructor, args);
			instances.add(instance);
		} catch (Throwable ex) {
			throw new IllegalArgumentException("Cannot instantiate " + type + " : " + name, ex);
		}
	}
	return instances;
}

2.2.2.run方法

在完成SpringApplication对象初始化后,会调用其中的run方法,一起看下:

  1. 启动计时器,计算启动时间。
  2. 加载配置属性和监听器
  3. 开始创建ApplicationContext(根据前面的WebApplicationType进行创建容器)
  4. 准备、初始化容器prepareContext(扫描bean,完成bean的加载),同时会调用刚才说到的Initializers的初始化方法,通过反射创建类。
  5. 加载bean和resource(资源)。
public ConfigurableApplicationContext run(String... args) {
    // 1.计时器,记录springBoot启动耗时
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    ConfigurableApplicationContext context = null;
    Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
    // 2.配置headLess属性,这个跟AWT有关,忽略即可
    configureHeadlessProperty();
    // 3.获取SpringApplicationRunListener实例数组,默认获取的是EventPublishRunListener
    SpringApplicationRunListeners listeners = getRunListeners(args);
    // 启动监听
    listeners.starting();
    try {
        // 4.创建ApplicationArguments对象
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
        //5.加载属性配置。所有的environment的属性都会加载进来,包括 application.properties 和外部的属性配置
        ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
        configureIgnoreBeanInfo(environment);
        // 6.打印Banner
        Banner printedBanner = printBanner(environment);
        // 7.根据WebApplicationType,创建不同的ApplicationContext
        context = createApplicationContext();
        // 8.获取异常报告器
        exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
                                                         new Class[] { ConfigurableApplicationContext.class }, context);
        // 9.调用各种初始化器的initialize方法,初始化容器
        prepareContext(context, environment, listeners, applicationArguments, printedBanner);
        refreshContext(context);
        // 10.执行初始化的后置逻辑,默认为空
        afterRefresh(context, applicationArguments);
        // 停止计时器
        stopWatch.stop();
        // 11.打印 Spring Boot 启动的时长日志
        if (this.logStartupInfo) {
            new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
        }
        // 12.通知监听器,SpringBoot启动完成
        listeners.started(context);
        // 13.调用 ApplicationRunner的运行方法
        callRunners(context, applicationArguments);
    }
    catch (Throwable ex) {
        handleRunFailure(context, ex, exceptionReporters, listeners);
        throw new IllegalStateException(ex);
    }

    try {
        // 通知监听器,SpringBoot正在运行
        listeners.running(context);
    }
    catch (Throwable ex) {
        handleRunFailure(context, ex, exceptionReporters, null);
        throw new IllegalStateException(ex);
    }
    return context;
}

2.3.@SpringBootApplication注解

点击进入,查看源码:

/**
 * Indicates a {@link Configuration configuration} class that declares one or more
 * {@link Bean @Bean} methods and also triggers {@link EnableAutoConfiguration
 * auto-configuration} and {@link ComponentScan component scanning}. This is a convenience
 * annotation that is equivalent to declaring {@code @Configuration},
 * {@code @EnableAutoConfiguration} and {@code @ComponentScan}.
 *
 * @author Phillip Webb
 * @author Stephane Nicoll
 * @since 1.2.0
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {

	/**
	 * Exclude specific auto-configuration classes such that they will never be applied.
	 * @return the classes to exclude
	 */
	@AliasFor(annotation = EnableAutoConfiguration.class)
	Class<?>[] exclude() default {};

	/**
	 * Exclude specific auto-configuration class names such that they will never be
	 * applied.
	 * @return the class names to exclude
	 * @since 1.3.0
	 */
	@AliasFor(annotation = EnableAutoConfiguration.class)
	String[] excludeName() default {};

	/**
	 * Base packages to scan for annotated components. Use {@link #scanBasePackageClasses}
	 * for a type-safe alternative to String-based package names.
	 * @return base packages to scan
	 * @since 1.3.0
	 */
	@AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
	String[] scanBasePackages() default {};

	/**
	 * Type-safe alternative to {@link #scanBasePackages} for specifying the packages to
	 * scan for annotated components. The package of each class specified will be scanned.
	 * <p>
	 * Consider creating a special no-op marker class or interface in each package that
	 * serves no purpose other than being referenced by this attribute.
	 * @return base packages to scan
	 * @since 1.3.0
	 */
	@AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
	Class<?>[] scanBasePackageClasses() default {};

}

这里重点的注解有3个:

  • @SpringBootConfiguration
  • @EnableAutoConfiguration
  • @ComponentScan

逐个来看。

2.3.1.@SpringBootConfiguration

org.springframework.boot.@SpringBootConfiguration 注解,标记这是一个 Spring Boot 配置类。代码如下:

/**
 * Indicates that a class provides Spring Boot application
 * {@link Configuration @Configuration}. Can be used as an alternative to the Spring's
 * standard {@code @Configuration} annotation so that configuration can be found
 * automatically (for example in tests).
 * <p>
 * Application should only ever include <em>one</em> {@code @SpringBootConfiguration} and
 * most idiomatic Spring Boot applications will inherit it from
 * {@code @SpringBootApplication}.
 *
 * @author Phillip Webb
 * @since 1.4.0
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {

}
  • 可以看到,它上面继承自 @Configuration 注解,所以两者功能也一致,可以将当前类内声明的一个或多个以 @Bean 注解标记的方法的实例纳入到 Srping 容器中,并且实例名就是方法名。

2.3.2.@ComponentScan

注解的类所在的包开始,扫描包及子包

我们跟进源码:

在这里插入图片描述

并没有看到什么特殊的地方。我们查看注释:
在这里插入图片描述
大概的意思:

配置组件扫描的指令。提供了类似与<context:component-scan>标签的作用

通过basePackageClasses或者basePackages属性来指定要扫描的包。如果没有指定这些属性,那么将从声明这个注解的类所在的包开始,扫描包及子包

而我们的@SpringBootApplication注解声明的类就是main函数所在的启动类,因此扫描的包是该类所在包及其子包。因此,一般启动类会放在一个比较前的包目录中

在这里插入图片描述

2.4.@EnableAutoConfiguration(非常重要)

2.4.1.官方介绍

关于这个注解,官网上有一段说明:

The second class-level annotation is @EnableAutoConfiguration. This annotation
tells Spring Boot to “guess” how you want to configure Spring, based on the jar
dependencies that you have added. Since spring-boot-starter-web added Tomcat
and Spring MVC, the auto-configuration assumes that you are developing a web
application and sets up Spring accordingly.

简单翻译以下:

第二级的注解@EnableAutoConfiguration,告诉SpringBoot基于你所添加的依赖,去“猜测”你想要如何配置Spring。比如我们引入了spring-boot-starter-web,而这个启动器中帮我们添加了tomcatSpringMVC的依赖。此时自动配置就知道你是要开发一个web应用,所以就帮你完成了web及SpringMVC的默认配置了!

总结,SpringBoot内部对大量的第三方库或Spring内部库进行了默认配置,这些配置是否生效,取决于我们是否引入了对应库所需的依赖,如果有那么默认配置就会生效。

那么,这里SpringBoot是如何进行判断和猜测的呢?

我们来看源码:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {

	String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

	/**
	 * Exclude specific auto-configuration classes such that they will never be applied.
	 * @return the classes to exclude
	 */
	Class<?>[] exclude() default {};

	/**
	 * Exclude specific auto-configuration class names such that they will never be
	 * applied.
	 * @return the class names to exclude
	 * @since 1.3.0
	 */
	String[] excludeName() default {};

}

其中,核心的就是顶部注解:

@Import(AutoConfigurationImportSelector.class)

继续来看这两个类

2.4.2.@Import

看源码:

/**
 * Indicates one or more {@link Configuration @Configuration} classes to import.
 *
 * <p>Provides functionality equivalent to the {@code <import/>} element in Spring XML.
 * Allows for importing {@code @Configuration} classes, {@link ImportSelector} and
 * {@link ImportBeanDefinitionRegistrar} implementations, as well as regular component
 * classes (as of 4.2; analogous to {@link AnnotationConfigApplicationContext#register}).
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Import {

	/**
	 * {@link Configuration}, {@link ImportSelector}, {@link ImportBeanDefinitionRegistrar}
	 * or regular component classes to import.
	 */
	Class<?>[] value();

}

@Import注解的作用就是把一个或多个类导入到Spring容器中。不过,导入的方式多种多样:

  • 可以直接通过类名导入:@Import(User.class)就是把User这个类导入
  • 可以通过ImportSelector来导入。而ImportSelector是一个接口,用来获取需要导入的类的名称数组,

2.4.3.ImportSelect

public interface ImportSelector {

	/**
	 * Select and return the names of which class(es) should be imported based on
	 * the {@link AnnotationMetadata} of the importing @{@link Configuration} class.
	 */
	String[] selectImports(AnnotationMetadata importingClassMetadata);

}

基于AnnotationMetadata来导入多个@Configuration类型的类的名称数组。

而在@EnableAutoConfiguration中通过@Import(AutoConfigurationImportSelector)其中的参数:AutoConfigurationImportSelector正是这个接口的一个子类。

如图:

在这里插入图片描述

其中DeferredImportSerlector有自己的特殊功能。

2.4.4.DeferredImportSerlector

部分源码如下:

/**
 * A variation of {@link ImportSelector} that runs after all {@code @Configuration} beans
 * have been processed. This type of selector can be particularly useful when the selected
 * imports are {@code @Conditional}.
 *
 * <p>Implementations can also extend the {@link org.springframework.core.Ordered}
 * interface or use the {@link org.springframework.core.annotation.Order} annotation to
 * indicate a precedence against other {@link DeferredImportSelector DeferredImportSelectors}.
 *
 * <p>Implementations may also provide an {@link #getImportGroup() import group} which
 * can provide additional sorting and filtering logic across different selectors.
 *
 * @author Phillip Webb
 * @author Stephane Nicoll
 * @since 4.0
 */
public interface DeferredImportSelector extends ImportSelector {

	/**
	 * Return a specific import group.
	 * <p>The default implementations return {@code null} for no grouping required.
	 * @return the import group class, or {@code null} if none
	 * @since 5.0
	 */
	@Nullable
	default Class<? extends Group> getImportGroup() {
		return null;
	}
    
    // 其它代码略
}

其作用也是导入一组配置类,但是当你需要对导入的配置类进行过滤时使用。

其中的Class<? extends Group> getImportGroup()方法,就是用来获取需要导入的配置组,而其返回值Group,而后续就会利用Group内部方法去获取需要加载的配置类。

2.4.5.DeferredImportSelector.Group

Group是DeferredImportSelector中的一个内部类。用于选择和处理要导入的配置类。

源码如下:

// DeferredImportSelector
/**
	 * Interface used to group results from different import selectors.
	 */
interface Group {

    /**
		 * Process the {@link AnnotationMetadata} of the importing @{@link Configuration}
		 * class using the specified {@link DeferredImportSelector}.
		 */
    void process(AnnotationMetadata metadata, DeferredImportSelector selector);

    /**
		 * Return the {@link Entry entries} of which class(es) should be imported
		 * for this group.
		 */
    Iterable<Entry> selectImports();

    // 略。。。
}

回头看看@EnableAutoConfiguration中的注解:

在这里插入图片描述

其中的@Import导入的AutoConfigurationImportSelector一定是ImportSelector的实现类。

2.5.AutoConfigurationImportSelector

AutoConfigurationImportSelectorDeferredImportSelector的一个实现类,因此实现了Class<? extends Group> getImportGroup()这个方法:

@Override
public Class<? extends Group> getImportGroup() {
    return AutoConfigurationGroup.class;
}

返回的是AutoConfigurationImportSelector的内部类AutoConfigurationGroup

2.5.1.AutoConfigurationGroup

其中包含一些成员变量:

private static class AutoConfigurationGroup
			implements DeferredImportSelector.Group, BeanClassLoaderAware, BeanFactoryAware, ResourceLoaderAware {

    // key:自动配置类的名称,AnnotationMetadata:导入配置类的元信息
    private final Map<String, AnnotationMetadata> entries = new LinkedHashMap<>();
    // 自动配置类的数组
    private final List<AutoConfigurationEntry> autoConfigurationEntries = new ArrayList<>();

    private ClassLoader beanClassLoader;
    private BeanFactory beanFactory;
    private ResourceLoader resourceLoader;

    private AutoConfigurationMetadata autoConfigurationMetadata;
    
    // ...略
}
  • Map<String, AnnotationMetadata> entries属性: key:自动配置类的名称,AnnotationMetadata:导入配置类的元信息

  • private final List<AutoConfigurationEntry> autoConfigurationEntries: AutoConfigurationEntry的数组

    • AutoConfigurationEntry源码:

    • protected static class AutoConfigurationEntry {
      		// 自动配置类名称数组
      		private final List<String> configurations;
      		// 需要排除的配置
      		private final Set<String> exclusions;
      }
      

包含2个重要的方法:

//AutoConfigurationGroup
// 加载要导入的配置类,成员变量赋值
@Override
public void process(AnnotationMetadata annotationMetadata, DeferredImportSelector deferredImportSelector) {
    Assert.state(deferredImportSelector instanceof AutoConfigurationImportSelector,
                 () -> String.format("Only %s implementations are supported, got %s",
                                     AutoConfigurationImportSelector.class.getSimpleName(),
                                     deferredImportSelector.getClass().getName()));
    // 加载配置类
    AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector) deferredImportSelector)
        .getAutoConfigurationEntry(getAutoConfigurationMetadata(), annotationMetadata);
    // 放入list
    this.autoConfigurationEntries.add(autoConfigurationEntry);
    // 放入entries
    for (String importClassName : autoConfigurationEntry.getConfigurations()) {
        this.entries.putIfAbsent(importClassName, annotationMetadata);
    }
}
// 对这些配置类过滤,筛选并返回
@Override
public Iterable<Entry> selectImports() {
    if (this.autoConfigurationEntries.isEmpty()) {
        return Collections.emptyList();
    }
    // 获取需要排除的配置类名称
    Set<String> allExclusions = this.autoConfigurationEntries.stream()
        .map(AutoConfigurationEntry::getExclusions).flatMap(Collection::stream).collect(Collectors.toSet());
    // 获取处理过的配置类名称
    Set<String> processedConfigurations = this.autoConfigurationEntries.stream()
        .map(AutoConfigurationEntry::getConfigurations).flatMap(Collection::stream)
        .collect(Collectors.toCollection(LinkedHashSet::new));
    // 移除需要排除的配置类
    processedConfigurations.removeAll(allExclusions);
	// 对配置类排序,并重新封装为Entry返回
    return sortAutoConfigurations(processedConfigurations, getAutoConfigurationMetadata()).stream()
        .map((importClassName) -> new Entry(this.entries.get(importClassName), importClassName))
        .collect(Collectors.toList());
}
  • AutoConfigurationGroup#process(...) 方法:用于加载自动配置,并给上述两个变量赋值
  • #selectImports()方法:从entries中筛选要引入的配置类。

其中的加载配置的代码是这行:

AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector) deferredImportSelector)
        .getAutoConfigurationEntry(getAutoConfigurationMetadata(), annotationMetadata);

跟入:

// AutoConfigurationImportSelector.java

protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata, AnnotationMetadata annotationMetadata) {
    // 1 判断是否开启。如未开启,返回空数组。
    if (!isEnabled(annotationMetadata)) {
        return EMPTY_ENTRY;
    }
    // 2 获得注解的属性(主要获取exclude排除的自动配置项)
    AnnotationAttributes attributes = getAttributes(annotationMetadata);
    // 3 从 /META-INF/spring.factories读取@EnableAutoConfiguration相关的配置类的名称数组
    List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
    // 3.1 移除重复的配置类
    configurations = removeDuplicates(configurations);
    // 4 获得需要排除的配置类
    Set<String> exclusions = getExclusions(annotationMetadata, attributes);
    // 4.1 校验排除的配置类是否合法,没有则跳过检查
    checkExcludedClasses(configurations, exclusions);
    // 4.2 从 configurations 中,移除需要排除的配置类
    configurations.removeAll(exclusions);
    // 5 根据条件(@ConditionalOn注解),过滤掉不符合条件的配置类
    configurations = filter(configurations, autoConfigurationMetadata);
    // 6 触发自动配置类引入完成的事件
    fireAutoConfigurationImportEvents(configurations, exclusions);
    // 7 创建 AutoConfigurationEntry 对象
    return new AutoConfigurationEntry(configurations, exclusions);
}

其中,最关键的是第三步:

List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);

2.5.2.getCandidateConfigurations

继续跟入,源码如下:

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
    // 利用SpringFactoriesLoader加载指定类型对应的类的全路径
    List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
                                                                         getBeanClassLoader());
    Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
                    + "are using a custom packaging, make sure that file is correct.");
    return configurations;
}

这里通过getSpringFactoriesLoaderFactoryClass(),获取到的是@EnableAutoConfiguration:

在这里插入图片描述

SpringFactoriesLoader.loadFactoryNames()方法我们已经见过一次了,它会去classpath下的/META-INF/spring.factories中寻找以EnableAutoConfiguration为key的配置类的名称:

在这里插入图片描述

到此,所有的自动配置类加载完毕,大致流程如图:
在这里插入图片描述

2.6.自动配置类中有什么

2.6.1.autoconfigure包

刚才加载的所有自动配置类,都可以再spring-boot-autoconfigure包中找到这些自动配置类:

非常多,几乎涵盖了现在主流的开源框架,例如:

  • redis
  • jms
  • amqp
  • jdbc
  • jackson
  • mongodb
  • jpa
  • solr
  • elasticsearch

… 等等

4.6.2.默认配置

我们来看一个我们熟悉的,例如SpringMVC,查看mvc 的自动配置类:

打开WebMvcAutoConfiguration:

我们看到这个类上的4个注解:

  • @Configuration:声明这个类是一个配置类

  • @ConditionalOnWebApplication(type = Type.SERVLET)

    ConditionalOn,翻译就是在某个条件下,此处就是满足项目的类是是Type.SERVLET类型,也就是一个普通web工程,显然我们就是

  • @ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })

    这里的条件是OnClass,也就是满足以下类存在:Servlet、DispatcherServlet、WebMvcConfigurer,其中Servlet只要引入了tomcat依赖自然会有,后两个需要引入SpringMVC才会有。这里就是判断你是否引入了相关依赖,引入依赖后该条件成立,当前类的配置才会生效!

  • @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)

    这个条件与上面不同,OnMissingBean,是说环境中没有指定的Bean这个才生效。其实这就是自定义配置的入口,也就是说,如果我们自己配置了一个WebMVCConfigurationSupport的类,那么这个默认配置就会失效!

接着,我们查看该类中定义了什么:

视图解析器:

处理器适配器(HandlerAdapter):

还有很多,这里就不一一截图了。

2.6.3.默认配置的属性覆盖

另外,这些默认配置的属性来自哪里呢?

我们看到,这里通过@EnableAutoConfiguration注解引入了两个属性:WebMvcProperties和ResourceProperties。这不正是SpringBoot的属性注入玩法嘛。

我们查看这两个属性类:

找到了内部资源视图解析器的prefix和suffix属性。

ResourceProperties中主要定义了静态资源(.js,.html,.css等)的路径:

如果我们要覆盖这些默认属性,只需要在application.properties中定义与其前缀prefix和字段名一致的属性即可。

2.7.总结(非常重要)

SpringBoot为我们提供了默认配置,而默认配置生效的步骤:

  • @EnableAutoConfiguration注解会去寻找META-INF/spring.factories文件,读取其中以EnableAutoConfiguration为key的所有类的名称,这些类就是提前写好的自动配置类
  • 这些类都声明了@Configuration注解,并且通过@Bean注解提前配置了我们所需要的一切实例。完成自动配置
  • 但是,这些配置不一定生效,因为有@ConditionalOn注解,满足一定条件才会生效。比如条件之一:是一些相关的类要存在
  • 类要存在,我们只需要引入了相关依赖(启动器),依赖有了条件成立,自动配置生效。
  • 如果我们自己配置了相关Bean,那么会覆盖默认的自动配置的Bean
  • 我们还可以通过配置application.yml文件,来覆盖自动配置中的属性

因此,使用SpringBoot自动配置的关键有两点:

1)启动器starter

要想自动配置生效,只需要引入依赖即可,而依赖版本我们也不用操心,因为只要引入了SpringBoot提供的stater(启动器),就会自动管理依赖及版本了。

因此,玩SpringBoot的第一件事情,就是找启动器,SpringBoot提供了大量的默认启动器

2)全局配置

另外,SpringBoot的默认配置,都会读取默认属性,而这些属性可以通过自定义application.properties文件来进行覆盖。这样虽然使用的还是默认配置,但是配置中的值改成了我们自定义的。

因此,玩SpringBoot的第二件事情,就是通过application.properties来覆盖默认属性值,形成自定义配置。

3.实现自定义starter

明白了SpringBoot自动配置原理,我们甚至可以自己定义starter,完成自己的自动配置。

3.1.自定义starter的步骤

再来回顾一下SpringBoot自动配置的扫描过程:

  • 通过@EnableAutoConfiguration上的注解,扫描到/META-INF/spring.factories文件
  • 文件中记录了需要导入的自动配置类
  • 在自动配置类中通过@Configuration、@Bean等等方式定义了需要完成的配置
  • 配置类中通过@ConfigurationProperties读取application.yml文件中的属性,允许用户覆盖默认属性

因此,我们要做的是:

  • 定义Properties类,通过@ConfigurationProperties读取application.yml文件中的属性
  • 定义@Configuration配置类,使用Properties中读取到的属性完成默认配置
  • 定义/META-INF/spring.factories文件,加载定义好的配置类,使其可以被SpringBoot加载

3.2.模拟DataSource的starter

我们来完成一个DataSource数据源的starter。

3.2.1.创建项目

首先创建一个新的项目,注意创建空maven项目:

在这里插入图片描述

填写项目名称:

在这里插入图片描述

3.2.2.引入依赖

需要引入一些基本的SpringBoot依赖:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>cn.itcast.starter</groupId>
    <artifactId>itcast-jdbc-spring-boot-starter</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
            <version>2.1.6.RELEASE</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <version>2.1.6.RELEASE</version>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
            <version>2.1.6.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.10</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

3.2.3.编写属性读取类

我们假设用户会在配置文件中配置jdbc连接参数,配置格式如下:

itcast:
  jdbc:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/bank?characterEncoding=utf-8&serverTimezone=UTC
    username: root
    password: 123

为了与SpringBoot的自带启动器区分,我们故意设置不一样的配置。

然后编写一个类来加载:

package cn.itcast.jdbc.config;

import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * @author 虎哥
 */
@ConfigurationProperties("itcast.jdbc")
public class ItcastJdbcProperties {
    private String driverClassName;
    private String url;
    private String username;
    private String password;

    public String getDriverClassName() {
        return driverClassName;
    }

    public void setDriverClassName(String driverClassName) {
        this.driverClassName = driverClassName;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

3.2.4.编写配置类

编写配置类,初始化DataSource对象:

package cn.itcast.jdbc.config;

import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import javax.sql.DataSource;

/**
 * @author 虎哥
 */
@Configuration
@ConditionalOnClass(DruidDataSource.class)
@EnableConfigurationProperties(ItcastJdbcProperties.class)
public class ItcastDataSourceAutoConfiguration {

    @Bean
    @Primary
    public DataSource dataSource(ItcastJdbcProperties prop){
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName(prop.getDriverClassName());
        dataSource.setUrl(prop.getUrl());
        dataSource.setUsername(prop.getUsername());
        dataSource.setPassword(prop.getPassword());
        return dataSource;
    }
}

3.2.5.编写Spring.factories

最后,编写Spring.factories,注册这个配置类:

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  cn.itcast.jdbc.config.ItcastDataSourceAutoConfiguration

运行mvn install 命令,把项目代码安装到仓库中:

在这里插入图片描述

然后在本地仓库目录可以看到:

在这里插入图片描述

3.3.测试

在我们的 bank项目中,引入自己编写的starter:

<dependency>
    <groupId>cn.itcast.starter</groupId>
    <artifactId>itcast-jdbc-spring-boot-starter</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

移除原来的jdbc连接池配置,添加自定义配置:

itcast:
  jdbc:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/bank?characterEncoding=utf-8&serverTimezone=UTC
    username: root
    password: 123

在AccountController中注入DataSource,断点查看注入的情况:

在这里插入图片描述
可以看到连接池变了:
在这里插入图片描述
说明我们的自定义starter生效了!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值