目录
1、pom.xml 文件
创建一个 SpringBoot 工程,查看 pom.xml
文件,当前工程是依赖 parent
模块。如下图:
1.1、parent 模块
parent
模块主要加载配置文件【yml】和插件。如下图:
parent
模块又依赖 dependencies
模块
1.1.1、资源文件
src/main/resources
目录下的yml/yaml/properties
文件进行占位符替换【第一个resource
:启用过滤】,其它文件不进行处理【第二个resource
:防止其它文件误被修改】,最终都会打包
1.1.1.1、resources
标签说明
Maven 处理 <resources>
时采用顺序合并策略,每个 <resource>
块定义不同的过滤规则:
- 第一个资源块(精准过滤)
- 处理范围:仅
application*.yml/yaml/properties
- 操作:开启过滤(
filtering=true
) - 目标:动态替换这些配置文件中的
@variable@
占位符
- 处理范围:仅
- 第二个资源块(全量保护)
- 处理范围:排除上述配置文件后的其他所有资源
- 操作:默认关闭过滤(
filtering=false
) - 目标:原样复制静态资源(如图片、HTML、XML 等)
处理流程如下图:
1.1.1.2、从 Maven 视角:资源处理全流程
如下图:
关键结论:
- 所有资源文件都会被打包,无论是否经过过滤
- 过滤(filtering)只是对文件内容的处理,与是否打包无关
- 两个资源块共同作用确保:
- 配置文件:动态内容替换
- 其他资源:保持原始状态
1.1.2、插件
1.2、dependencies 模块
dependencies
模块主要管理了各种依赖的版本。所以,在 SpringBoot 工程中导入依赖时,不需要指定版本,已有默认的版本。如下图:
2、启动器
启动器:SpringBoot 启动的场景。如:spring-boot-starter-web
,会帮我们自动导入 web 环境的所有的依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
SpringBoot 将所有的功能场景变成一个个启动器。当我们需要使用什么功能时,就只需要导入对应的启动器即可。
3、主程序
@SpringBootApplication
public class MybootApplication {
public static void main(String[] args) {
SpringApplication.run(MybootApplication.class, args);
}
}
@SpringBootApplication
:标注这个类是一个 SpringBoot 应用SpringApplication.run()
方法启动 SpringBoot 应用
3.1、@SpringBootApplication
注解
@SpringBootApplication
注解是一个复合注解
@SpringBootConfiguration
@EnableAutoConfiguration
public @interface SpringBootApplication {
//...
}
3.2、@SpringBootConfiguration
注解
// 保证了 bean 的唯一性。@Component 无法保证
@Configuration
public @interface SpringBootConfiguration {
// 默认使用 CGLIB 代理该类
@AliasFor(annotation = Configuration.class)
boolean proxyBeanMethods() default true;
}
@SpringBootConfiguration
注解相当于 @Configuration
注解
3.2.1、@Configuration
和 @Component
区别
@Component
在 Spring 中是代表 LITE 模式的配置注解,这种模式下的注解不会被 Spring 所代理,就是一个标准类,如果在这个类中有@Bean
标注的方法,那么方法间的相互调用,其实就是普通 Java 类的方法的调用。@Configuration
在 Spring 中是代表 FULL 模式的配置注解,这种模式下的类会被 Spring 所代理,那么在这个类中的@Bean
方法的相互调用,就相当于调用了代理方法,那么在代理方法中会判断,是否调用getBean
方法还是invokeSuper
方法,这里就是这两个注解的最根本的区别。@Configuration
中所有带@Bean
注解的方法都会被动态代理,且该方法返回的都是同一个实例。且@Configuration(proxyBeanMethods = false)
和@Component
作用一样
如下:
①:使用 @Configuration
注解:
@Configuration
//@Component
public class UserConfig {
@Bean
public User user() {
System.out.println("user..........");
return new User();
}
@Bean
public Teacher teacher() {
// 通过代理调用, 返回同一实例
return new Teacher(user());
}
}
②:测试
@GetMapping("/testAuto")
public void testAuto() {
ApplicationContext ctx = new AnnotationConfigApplicationContext(UserConfig.class);
User configB1 = ctx.getBean(User.class);
User configB2 = ctx.getBean(UserConfig.class).user();
// 输出 true
System.out.println(configB1 == configB2);
ApplicationContext ctx = new AnnotationConfigApplicationContext(UserComponent.class);
User component1 = ctx.getBean(User.class);
User component2 = ctx.getBean(UserConfig.class).user();
// 输出 false
System.out.println(configB1 == configB2);
}
在 @Component
类中,若需要依赖其他 @Bean
,应通过参数注入而非直接调用方法:
@Component
public class UserConfig {
@Bean
public User user() {
System.out.println("user..........");
return new User();
}
/*@Bean
public Teacher teacher() {
// 通过代理调用, 返回同一实例
return new Teacher(user());
}*/
@Bean
public Teacher teacher(User user) {
return new Teacher(user);
}
}
3.3、@EnableAutoConfiguration
注解
@AutoConfigurationPackage
// 导入参数类到 IOC 容器中
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
//...
}
3.3.1、@AutoConfigurationPackage
注解
// 保存扫描路径,提供给 spring-data-jpa 查询【@Entity】
@Import({AutoConfigurationPackages.Registrar.class})
public @interface AutoConfigurationPackage {
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
}
3.3.1.1、AutoConfigurationPackages.Registrar#registerBeanDefinitions()
方法:
public abstract class AutoConfigurationPackages {
public static void register(BeanDefinitionRegistry registry, String... packageNames) {
if (registry.containsBeanDefinition(BEAN)) {
BeanDefinition beanDefinition = registry.getBeanDefinition(BEAN);
ConstructorArgumentValues constructorArguments = beanDefinition.getConstructorArgumentValues();
constructorArguments.addIndexedArgumentValue(0, addBasePackages(constructorArguments, packageNames));
} else {
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClass(BasePackages.class);
beanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(0, packageNames);
beanDefinition.setRole(2);
registry.registerBeanDefinition(BEAN, beanDefinition);
}
}
private static String[] addBasePackages(ConstructorArgumentValues constructorArguments, String[] packageNames) {
String[] existing = (String[])((String[])constructorArguments.getIndexedArgumentValue(0, String[].class).getValue());
Set<String> merged = new LinkedHashSet();
merged.addAll(Arrays.asList(existing));
merged.addAll(Arrays.asList(packageNames));
return StringUtils.toStringArray(merged);
}
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
Registrar() {
}
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
AutoConfigurationPackages.register(registry, (new PackageImport(metadata)).getPackageName());
}
}
}
3.3.2、@Import
注解
@Import
注解:SpringBoot 自动装配的核心注解。它有三种用法:
- 参数如果是普通类:将该类实例化并交给 IOC 容器管理
- 参数如果是
ImportBeanDefinitionRegistrar
的实现类,则支持手工注册 Bean - 参数如果是
ImportSelector
的实现类,注册selectImports()
方法返回的数组【类全路径】到 IOC 容器 【批量注册】
3.3.3、AutoConfigurationImportSelector
selectImports()
方法:查询导入的组件
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!this.isEnabled(annotationMetadata)) {
return NO_IMPORTS;
} else {
AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
}
getAutoConfigurationEntry()
方法:
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
if (!this.isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
} else {
// 获取注解中的属性
AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
// 获取候选的配置
List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
configurations = this.removeDuplicates(configurations);
Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
this.checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = this.getConfigurationClassFilter().filter(configurations);
this.fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}
}
getCandidateConfigurations()
方法:获取候选的配置
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.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;
}
SpringFactoriesLoader#loadFactoryNames()
方法:
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
ClassLoader classLoaderToUse = classLoader;
if (classLoader == null) {
classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
}
String factoryTypeName = factoryType.getName();
return (List)loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}
loadSpringFactories()
方法:
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
Map<String, List<String>> result = (Map)cache.get(classLoader);
if (result != null) {
return result;
} else {
Map<String, List<String>> result = new HashMap();
try {
Enumeration<URL> urls = classLoader.getResources("META-INF/spring.factories");
while(urls.hasMoreElements()) {
URL url = (URL)urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
Iterator var6 = properties.entrySet().iterator();
while(var6.hasNext()) {
Map.Entry<?, ?> entry = (Map.Entry)var6.next();
String factoryTypeName = ((String)entry.getKey()).trim();
String[] factoryImplementationNames = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
String[] var10 = factoryImplementationNames;
int var11 = factoryImplementationNames.length;
for(int var12 = 0; var12 < var11; ++var12) {
String factoryImplementationName = var10[var12];
((List)result.computeIfAbsent(factoryTypeName, (key) -> {
return new ArrayList();
})).add(factoryImplementationName.trim());
}
}
}
result.replaceAll((factoryType, implementations) -> {
return (List)implementations.stream().distinct().collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
});
cache.put(classLoader, result);
return result;
} catch (IOException var14) {
throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var14);
}
}
}
加载 autoconfigure
包下的 META-INF/spring.factories
自动配置类到 IOC 容器中去:
3.4、自动装配原理
SpringBoot 启动的时候会通过 @EnableAutoConfiguration
注解找到 META-INF/spring.factories
配置文件中的所有自动配置类,并对其进行加载【但不一定生效,要判断条件是否成立】。只要导入了对应的 starter
,就有对应的启动器,自动配置类就会生效,然后就配置成功。
- 这些自动配置类都是以
AutoConfiguration
结尾来命名的,它实际上就是一个 Java 配置类形式的 Spring 容器配置类,它能通过以Properties
结尾命名的类中取得在全局配置文件中配置的属性如:server.port
。 XxxxProperties
类是通过@ConfigurationProperties
注解与全局配置文件中对应的属性进行绑定的。
AutoConfigurationImportSelector#selectImports()
方法通过SpringLoaderFactories#loadFactories()
方法扫描所有具有META-INFO/spring.factories
文件的 jar 包(spring-boot-autoconfigure.jar
就有)。这个spring.factories
文件也是一组一组的key=value
的形式,其中一个 key 是EnableAutoConfiguration
类的全类名,而它的 value 是一个xxxxAutoConfiguration
的类名的列表,这些类名以逗号分隔。在SpringApplication.run(...)
的内部就会执行selectImports()
方法,找到所有JavaConfig
自动配置类的全限定名对应的 class,然后将所有自动配置类加载到 Spring 容器中
4、自定义一个 starter
4.1、基础版
结构图如下:
4.1.1、创建一个 SpringBoot 工程
创建一个 SpringBoot 工程:read-spring-boot-starter
【官方推荐命名:××-spring-boot-starter
】,其 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 https://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.7.14</version>
<relativePath/>
</parent>
<groupId>com.zzc</groupId>
<artifactId>read-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>read-spring-boot-starter</name>
<description>read-spring-boot-starter</description>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.7.6</spring-boot.version>
</properties>
<dependencies>
<!--自动配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.9</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>
4.1.2、新建一个 Properteis 类
@Data
@ConfigurationProperties(prefix = "reading")
public class ReadingProperties {
// 类型
private String type = "txt";
}
4.1.3、新建一个自动配置类,关联 Properties 类
@Configuration
@EnableConfigurationProperties(ReadingProperties.class)
public class ReadingAutoConfiguration {
@Autowired
private ReadingProperties readingProperties;
// 若当前IOC容器中没有ReadingService时,提供一个默认的ReadingService实现
@Bean
@ConditionalOnMissingBean(ReadingService.class)
public ReadingService readingService() {
return new ReadingServiceImpl(readingProperties);
}
}
4.1.4、添加一个对外服务接口
@Slf4j
@NoArgsConstructor
@AllArgsConstructor
public class ReadingServiceImpl implements ReadingService {
private ReadingProperties readingProperties;
@Override
public void reading() {
log.info("start reading..., type is {}", readingProperties.getType());
}
}
4.1.5、注册自动配置类
添加文件 resources/META-INF/spring.factories
,其内容为:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.zzc.read.auto.ReadingAutoConfiguration
key 为
EnableAutoConfiguration
类的全限定名,value 为配置类的全限定名
4.1.6、mvn install
执行 mvn install
命令进行打包
4.1.7、引用 starter
4.1.7.1、pom.xml 中引入 starter
<dependency>
<groupId>com.zzc</groupId>
<artifactId>read-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
4.1.7.2、使用 starter
@Slf4j
@RestController
public class TestController {
@Autowired
private ReadingService readingService;
@GetMapping("/testAuto")
public void testAuto() {
readingService.reading();
}
}
结果如下:
4.2、引入条件装配
4.2.1、修改配置类 ReadingAutoConfiguration
修改配置类 ReadingAutoConfiguration
:
@Configuration
@EnableConfigurationProperties(ReadingProperties.class)
// 当存在reading.enable属性,且其值为true时,才初始化该Bean
@ConditionalOnProperty(name = "reading.enable", havingValue = "true")
public class ReadingAutoConfiguration {
@Autowired
private ReadingProperties readingProperties;
// 若当前IOC容器中没有ReadingService时,提供一个默认的ReadingService实现
@Bean
@ConditionalOnMissingBean(ReadingService.class)
public ReadingService readingService() {
return new ReadingServiceImpl(readingProperties);
}
}
再重新 mvn install
4.2.2、修改使用 starter
yml
文件添加配置:
reading:
enable: true
@RestController
public class TestController {
@Autowired(required = false)
private ReadingService readingService;
@GetMapping("/testAuto")
public void testAuto() {
readingService.reading();
}
}
4.3、重写 starter 的默认配置
之前创建 starter 时,reading.type
默认配置为 reading.type=txt
,想重写默认配置也是很简单的,只需要在当前项目 application.yml
中稍作添加:
reading:
enable: true
type: json
再次调用,结果如下:
4.4、重写 starter 默认实现
如果觉得 starter 默认的 ReadingService
实现不够好,那么也可以自定义 ReadingService
实现。因为 starter 构造 ReadingService
实现那里加上了 @ConditionalOnMissingBean(ReadingService.class)
。所以,只需要在当前工程自行实现 ReadingService
接口,并将其注册到 SpringIOC
中,则 starter 中默认 ReadingService
实现将不会被初始化。
添加类 MyReadingServiceImpl
:
@Slf4j
@Service
public class MyReadingServiceImpl implements ReadingService {
@Autowired
private ReadingProperties readingProperties;
@Override
public void reading() {
log.info("My reading start reading..., type is {}", readingProperties.getType());
}
}
重启项目后,结果如下:
4.5、实现一个自己的 @EnableXXX
在 application.yml
配置 reading.enable=true
这样的方式让自动装配生效,其实不够优雅。那么我们也可以像别的 starter 那样提供 @EnableXXX
(@EnableScheduling
、@EnableAsync
、@EnableCaching
…)注解,然后在 SpringApplication
启动类加上 @EnableXXX
,让我们的自动装配生效
4.5.1、在 starter 中添加一个 selector
创建一个 ReadingSelector
,用来替换之前的 ReadingAutoConfiguration
:
@Configuration
@EnableConfigurationProperties(ReadingProperties.class)
public class ReadingSelector {
@Autowired
private ReadingProperties readingProperties;
@Bean
@ConditionalOnMissingBean(ReadingService.class)
public ReadingService readingService() {
return new ReadingServiceImpl(readingProperties);
}
}
4.5.2、在 starter 中删除 resources/META-INF/spring.factories
文件
4.5.3、在 starter 中添加一个注解 @EnableReading
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
// @Import: 往Spring IOC中导入一个类
@Import(ReadingSelector.class)
public @interface EnableReading {
}
再重新 mvn install
4.5.4、在当前项目中做出修改
①:修改 yml
文件:
reading:
type: json
②:修改主启动类,添加注解 @EnableReading
@EnableReading
@SpringBootApplication
public class TestApplication {
public static void main(String[] args) {
SpringApplication.run(TestApplication.class, args);
}
}
③:删除类 MyReadingServiceImpl
④:测试接口
@RestController
public class TestController {
@Autowired
private ReadingService readingService;
@GetMapping("/testAuto")
public void testAuto() {
readingService.reading();
}
}
结果如下: