目录
一、Condition
1、Condition的具体实现
2、Condition小结
(1)自定义条件
(2)SpringBoot 提供的常用条件注解
二、@Enable注解
三、@EnableAutoConfiguration 注解和自动配置
1、@EnableAutoConfiguration的三个注解属性
2、自动配置过程
四、自定义启动器
开发过程中我们要配置很多依赖项和类,减少开发者的配置工作,在程序启动时自动配置功能通过检测类路径中的库和依赖项,并基于这些信息自动配置相应的 Spring Bean,从而减少了手动配置的需求。
一、Condition
Condition 是在Spring 4.0 增加的条件判断功能,通过这个可以功能可以实现选择性的创建 Bean操作。
那SpringBoot是如何知道要创建哪个Bean的?比如SpringBoot是如何知道要创建RedisTemplate 的?
1、Condition的具体实现
首先我们要先导入依赖坐标:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
首先是我们要创建一个条件类,也就是在如果有这个条件类的时候我再去创建对象。
public class ClassCondition implements Condition {
/**
*
* @param context 上下文对象。用于获取环境,IOC容器,ClassLoader对象
* @param metadata 注解元对象。 可以用于获取注解定义的属性值
* @return
*/
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
//2.需求: 导入通过注解属性值value指定坐标后创建Bean
//获取注解属性值 value
Map<String,Object> map = metadata.getAnnotationAttributes(ConditionOnClass.class.getName());
System.out.println(map);
// 得到{"com.alibaba.fastjson.JSON", "redis.clients.jedis.Jedis"}
String[] value = (String[]) map.get("value");
boolean flag = true;
try {
for (String className : value) {
Class<?> cls = Class.forName(className);
}
} catch (ClassNotFoundException e) {
flag = false;
}
return flag;
}
}
这里首先实现了Condition接口,Condition 接口用于定义条件,Spring 在决定是否加载某些配置或 Bean 时会使用它。这也就是上面的问题的解决,通过条件来判断是否创建bean。
matches 方法是 Condition 接口中的唯一方法。它的返回值决定了某些配置或 Bean 是否应该被加载。ConditionContext context:提供了上下文对象,可以用来获取 Spring 环境、IOC 容器、ClassLoader 等。AnnotatedTypeMetadata metadata:提供了注解的元数据,可以用来获取注解的属性值。
首先通过getAnnotationAttributes方法获取ConditionOnClass注解的属性值,这里的ConditionOnClass是我们自定义的注解,等会我们来讲。
通过map集合通过get方法获取到了里面的"com.alibaba.fastjson.JSON", "redis.clients.jedis.Jedis"两个依赖。
通过遍历里面的类名,并尝试去加载这些类,创建这些类的对象。如果能加载成功,则保持返回true,如果不能成功加载,则抛出异常并将flag设置为false返回给ConditionOnClass自定义注解。
然后我们要去创建一个自定义注解,因为你不能每次去判断某个类是否依赖都去在条件类里卖弄重新写入,所以通过一个自定义注解来接收测试类中传进来的依赖类来完成判断,下面是自定义注解ConditionOnClass的代码:
// 指定了这个注解可以应用的目标。ElementType.TYPE 表示注解可以用在类、接口或枚举上,ElementType.METHOD 表示注解可以用在方法上。
@Target({ElementType.TYPE,ElementType.METHOD}) // 可以修饰在类与方法上
// 指定了注解的保留策略。RUNTIME 表示注解会在运行时保留,可以通过反射机制读取。
@Retention(RetentionPolicy.RUNTIME) // 注解生效节点runtime
@Documented //生成工具文档化
// 表示这个注解的作用是条件性的,只有当 ClassCondition 条件满足时,注解修饰的类或方法才会生效。ClassCondition 是一个实现了 Condition 接口的类,用来定义条件逻辑。
@Conditional(value = ClassCondition.class)
public @interface ConditionOnClass {
String[] value(); // 设置此注解的属性redis.clients.jedis.Jedis
}
这段代码里面@Target定义了这个自定义注解可以用在谁身上,比如ElementType.TYPE是可以用在类、接口或枚举上,ElementType.METHOD是可以用在方法上。
然后是@Retention是指定了保留策略,RUNTIME是代表注解会在运行时保留,并且可以通过反射机制读取,就比如测试类中传来的参数我们就可以进行保留,传递给ClassCondition进行相关操作判断。
@Documented是指定该注解在生成 Javadoc 时会包含在文档中,便于开发者查阅。
@Conditional()代表的是指定了自己的条件逻辑由 ClassCondition 类来实现,由它的返回值来判断自己的注解是否生效。
@interface是用来定义自定义注解的关键字。
value 属性是一个字符串数组,用于指定需要检查的类名(通常是全限定类名)。
然后是我们的配置类,它通过刚才的ClassCondition条件类和ConditionOnClass自定义配置来帮助它判断是否要注册加载当前的bean。下面是配置类UserConfig类的代码:
@Configuration
public class UserConfig {
// 情况1
@Bean
// 该注解的作用是通过条件类 ClassCondition 来决定 user() 方法是否应该被注册为一个 Bean。
@ConditionOnClass(value={"com.alibaba.fastjson.JSON","redis.clients.jedis.Jedis"})
public User user(){
return new User();
}
// 情况2
@Bean
// 当容器中有一个key=k1且value=v1的时候user2才会注入
// 在application.properties文件中条件k1=v1
@ConditionalOnProperty(name = "k1",havingValue = "v1")
public User user2(){
return new User();
}
}
里面调用了@ConditionOnClass我们已经配置好的自定义注解,传入两个类路径,只有在类路径中存在 com.alibaba.fastjson.JSON 和 redis.clients.jedis.Jedis 这两个类时,ConditionOnClass调用ClassCondition判断返回true的情况 user() 方法才会被执行,返回的 User 对象才会被注册为 Bean。
第二种user2的方式是通过@ConditionalOnProperty的注解根据配置属性的值来决定是否创建 Bean。这里我们已经准备好了配置文件(存在于application.properties中):
k1=v1
其中的两个参数为name:指定要检查的属性名称,这里是 k1。havingValue:指定属性的期望值,这里是 v1。当 Spring 环境中的属性 k1 的值等于 v1 时,user2() 方法会被执行,返回的 User 对象会被注册为 Bean;否则,user2() 方法不会被执行,也不会注册该 Bean,也就是说在这里我们user2()方法是可以被执行的。
最后我们在测试类主入口中去通过bean的id创建一个bean对象,看是否被注册加载:
@SpringBootApplication
public class SpringbootCondition02Application {
public static void main(String[] args) {
//启动SpringBoot的应用,返回Spring的IOC容器
ConfigurableApplicationContext context = SpringApplication.run(SpringbootCondition02Application.class, args);
/********************获取容器中user********************/
// Object user1 = context.getBean("user");
// System.out.println(user1);
Object user2 = context.getBean("user");
System.out.println(user2);
}
}
2、Condition小结
(1)自定义条件
① 定义条件类:自定义类实现Condition接口,重写 matches 方法,在 matches 方法中进行逻辑判 断,返回boolean值。matches 方法两个参数:
- context:上下文对象,可以获取属性值,获取类加载器,获取BeanFactory等。
- metadata:元数据对象,用于获取注解属性。
② 判断条件: 在初始化Bean时,使用 @Conditional(条件类.class)注解。
(2)SpringBoot 提供的常用条件注解
以下注解在springBoot-autoconfigure的condition包下:
- ConditionalOnProperty:判断配置文件中是否有对应属性和值才初始化Bean
- ConditionalOnClass:判断环境中是否有对应字节码文件才初始化Bean
- ConditionalOnMissingBean:判断环境中没有对应Bean才初始化Bean
- ConditionalOnBean:判断环境中有对应Bean才初始化Bean
二、@Enable注解
SpringBoot中提供了很多Enable开头的注解,这些注解都是用于动态启用某些功能的。而其底层原理是使用@Import注解导入一些配置类,实现Bean的动态加载。
思考 SpringBoot 工程是否可以直接获取jar包中定义的Bean?
这里我们就要@Import注解来解决这个问题了:
@Enable底层依赖于@Import注解导入一些类,使用@Import导入的类会被Spring加载到IOC容器中。而@Import提供4中用法:
- ① 导入Bean。
- ② 导入配置类。
- ③ 导入 ImportSelector 实现类。一般用于加载配置文件中的类。
- ④ 导入 ImportBeanDefinitionRegistrar 实现类。
演示示例(1):
就比如说我们要在一个项目子模块中去配置一些依赖坐标,并去对其进行自定义注解的创建,再通过主项目模块来调用,呢该怎么实现呢?
首先是子项目模块:
子项目模块配置类config中的EnableUser(自定义配置类):
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(UserConfig.class)
public @interface EnableUser {
}
里面@Target已经讲过是ElementType.TYPE用来用在类、接口或枚举上,Retention是用来运行时保留,并且可以通过反射机制读取,@Documented是生成工具文档化。最重要的是@Import,他是用来将指定的配置类导入到当前应用上下文中,就是把UserConfig 类导入到 Spring 容器中,使 UserConfig 中定义的 Bean 被注册到 Spring 容器里。
模块配置类config中的UserConfig配置类:
@Configuration
public class UserConfig {
@Bean
public User user() {
return new User();
}
}
这里定义了user()方法用来封装名为user的bean。
这里子模块项目实现了相关代码,呢么主模块就进行导入子模块的坐标,来获取他的方法和配置类等:
<dependency>
<groupId>com.apesource</groupId>
<artifactId>springboot-enable_other-04</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
再通过测试主入口进行调用:
@SpringBootApplication
//@ComponentScan("com.apesource.config")
//@Import(User.class)//导入javaBean
//@Import(UserConfig.class)
@EnableUser
//@EnableScheduling
//@EnableCaching
public class SpringbootEnable03Application {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnable03Application.class, args);
/**
* @SpringBootApplication中有@ComponentScan注解, 扫描范围:当前引导类所在包及其子包
* 当前引导类所在包com.apesource.springbootenable03
* 注入user类所在包com.apesource.springbootenable_other04.config
* 因此扫描不到,所以容器中没有user
* 解决方案:
* 1.使用@ComponentScan扫描com.apesource.springbootenable_other04.config包
* 2.可以使用@Import注解,加载类。这些类都会被Spring创建,并放入IOC容器
* 3.可以对Import注解进行封装。
*
*/
//获取Bean
User user = context.getBean(User.class);
System.out.println(user);
}
}
如果注解的是@ComponentScan("com.apesource.config"),那么就是通过扫描指定包名com.apesource.config来注册bean。
如果是@Import(User.class),那么直接将 User 类导入到 Spring 容器中,使得 User 成为一个 Bean。
如果是@Import(UserConfig.class),那么就是导入导入 UserConfig 配置类,也会自动注册bean。
如果是@EnableUser,那么就是用咱们刚才自定义的注解来完成注册bean。
@EnableScheduling,启用 Spring 的任务调度功能,如果需要使用定时任务,可以启用此注解。
@EnableCaching,启用 Spring 的缓存功能,允许使用注解来缓存方法的返回结果。
这几种方式都可以去注册bean。
演示示例(2):
刚才只是演示了最基础的调用Config配置类,再提供两种可以调用的方法:
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
//AnnotationMetadata注解
//BeanDefinitionRegistry向spring容器中注入
//1.获取user的definition对象
AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(User.class).getBeanDefinition();
//2.通过beanDefinition属性信息,向spring容器中注册id为user的对象
registry.registerBeanDefinition("user", beanDefinition);
}
}
这里的ImportBeanDefinitionRegistrar是Spring 提供的一个接口,允许你以编程方式在配置类被处理时向 Spring 容器中注册额外的 Bean 定义。
registerBeanDefinitions是用于向 Spring 容器注册新的 Bean 定义。参数里的importingClassMetadata提供关于使用 @Import 导入当前类的注解元数据(注解信息),BeanDefinitionRegistry是一个 Bean 定义注册中心,允许通过这个接口向 Spring 容器注册新的 Bean 定义。
然后是通过BeanDefinitionBuilder调用rootBeanDefinition(User.class).getBeanDefinition()获取到 User 类的 BeanDefinition 对象,描述了如何创建这个 User Bean。BeanDefinition 是 Spring 用来描述一个 Bean 的配置元数据对象,包括 Bean 的类型、作用范围、依赖关系等。
最后通过通过beanDefinition属性信息将 User Bean定义注册到Spring容器中。
还有一种可以解决写死配置类和组件的情况,用于动态选择要导入的配置类或组件:
public class MyImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
// 目前字符串数组的内容是写死的,未来可以设置在配置文件中动态加载
return new String[]{"com.apesource.domain.User","com.apesource.domain.Student"};
}
}
这里面实现了ImportSelector接口,用于动态选择要导入的配置类或组件,重写selectImports方法来实现返回一个字符串数组,返回值就为已经需要导入的类的名称,代表将"字符串数组"中的的类,全部导入spring容器。
然后同样的道理在主项目里导子模块项目的包,进行调用测试:
@SpringBootApplication
//@Import(User.class)
//@Import(UserConfig.class)
//@Import(MyImportSelector.class)
@Import({MyImportBeanDefinitionRegistrar.class})
//@EnableCaching
//@EnableAsync
public class SpringbootEnableMain05Application {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnableMain05Application.class, args);
/**
* Import4中用法:
* 1. 导入Bean
* 2. 导入配置类
* 3. 导入ImportSelector的实现类
* 查看ImportSelector接口源码
* String[] selectImports(AnnotationMetadata importingClassMetadata);
* 代表将“字符串数组”中的的类,全部导入spring容器
* 4. 导入ImportBeanDefinitionRegistrar实现类
*
*/
// User user = context.getBean(User.class);
// System.out.println(user);
//
// Student student = context.getBean(Student.class);
// System.out.println(student);
User user = (User) context.getBean("user");
System.out.println(user);
}
}
这里多个一个注释,@EnableAsync是启用 Spring 的异步方法执行功能。
三、@EnableAutoConfiguration 注解和自动配置
这个注释主要是用来帮助我们配置,首先它的自动配置是从主启动类中启动时通过注解@SpringBootApplication里的@EnableAutoConfiguration来实现:
//@SpringBootApplication 来标注一个主程序类
//说明这是一个Spring Boot应用
@SpringBootApplication
public class SpringbootApplication {
public static void main(String[] args) {
//以为是启动了一个方法,没想到启动了一个服务
SpringApplication.run(SpringbootApplication.class, args);
}
}
当我们进去到 @SpringBootApplication 注解的源码当中,可以发现它是一个复合注解,它是由 @SpringBootConfiguration + @EnableAutoConfiguration + @ComponentScan 这三个注解组成。
1、@EnableAutoConfiguration的三个注解属性
@SpringBootConfiguration:在@SpringBootConfiguration 源码中可以发现有 @Configuration,代表是一个配置类,说明主程序类也是一个配置类。
//@SpringBootConfiguration注解内部 //这里的 @Configuration,说明这是一个配置类 ,配置类就是对应Spring的xml 配置文件; @Configuration public @interface SpringBootConfiguration {} //里面的 @Component 这就说明,启动类本身也是Spring中的一个组件而已,负责启动应用 @Component public @interface Configuration {}
@ComponentScan:这个注解在Spring中很重要 ,它对应XML配置中的元素,刚才也讲到过,是自动扫描并加载符合条件的组件或者bean , 将这个bean定义加载到IOC容器中。
@EnableAutoConfiguration:以前我们需要自己配置的东西,而现在SpringBoot可以自动帮我们配置 ;@EnableAutoConfiguration 告诉SpringBoot开启自动配置功能,这样自动配置才能生效;
@AutoConfigurationPackage @Import({AutoConfigurationImportSelector.class}) public @interface EnableAutoConfiguration { }
2、自动配置过程
@AutoConfigurationPackage:自动配置包,将指定的一个包下的所有组件导入到容器当中,在@AutoConfigurationPackage 注解中存在一个 @Import({Registrar.class}) 注解,自动配置包就是通过这个 Registrar 类的方法来完成的。
//AutoConfigurationPackage的子注解
//Registrar.class 作用:将主启动类的所在包及包下面所有子包里面的所有组件扫描到Spring容器
@Import({Registrar.class})
public @interface AutoConfigurationPackage {
}
由于@AutoConfigurationPackage 是在 @EnableAutoConfiguration 中,所以@AutoConfigurationPackage 是标识在主程序类上,所以 metadata为主程序类。
因为在AutoConfigurationPackage注解中用import导入了Registrar类:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({Registrar.class})
public @interface AutoConfigurationPackage {
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
}
所以在可以调用Registrar的方法,在Registrar中的 registerBeanDefinitions 方法,首先先获取到注解所标识的类,然后将这个类所在的包以及子包的名称放入到一个String数组当中,再将该String数组中的包的所有组件导入到容器当中。
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
AutoConfigurationPackages.register(registry, (String[])(new AutoConfigurationPackages.PackageImports(metadata)).getPackageNames().toArray(new String[0]));
}
其中PackageImports(metadata)).getPackageNames()中的PackageImports的大致意思是,将包下所有的组件导入,获取包名,然后将包名存放在一个String数组中,Register的作用就是将包下的所有组件导入,这也是为什么Spring容器启动后会自动导入 主程序所在的包及其子包中的所有组件。
@Import({AutoConfigurationImportSelector.class}):这个注解是@EnableAutoConfiguration下的注解,该注解的大概流程就是:将 spring-boot-autoconfigure-x.x.x.jar包中 META-INF/spring.factories文件中的。
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!this.isEnabled(annotationMetadata)) {
return NO_IMPORTS;
} else {
AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
}
getAutoConfigurationEntry(annotationMetadata); //给容器批量导入一些组件
selectImports()是AutoConfigurationImportSelector的核心函数,其核心功能就是获取spring.factories中EnableAutoConfiguration所对应的Configuration类列表,由@EnableAutoConfiguration注解中的exclude/excludeName参数筛选一遍,再由AutoConfigurationImportFilter类所有实例筛选一遍,得到最终的用于Import的configuration和exclusion。
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!this.isEnabled(annotationMetadata)) {
return NO_IMPORTS;
} else {
AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
}
其中this.getAutoConfigurationEntry(annotationMetadata),该方法就是批量导入一些组件,让我们进方法看一看:
List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes)就是获取到所有需要导入到容器当中的组件,利用工厂加载。
Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) 是得到所有组件从META-INF/spring.factories 位置来加载一个文件,默认扫描我们当前系统里面所有 META-INF/spring.factories位置的文件spring-boot-autoconfigure-2.6.7.jar包里边也有 META-INF/spring.factories。
configurations中一共有133个组件:
可以发现,spring-boot-autoconfigure-2.6.7.jar包里边也有 META-INF/spring.factories, 而 spring.factories 的 EnableAutoConfiguration,正好有133条,而configurations中正好也有 133 条,所以configurations中读取到的内容,正好是 spring.factories 的 EnableAutoConfiguration 中的内容。
当 Spring Boot 应用启动时,它会扫描所有的 spring.factories 文件,查找 org.springframework.boot.autoconfigure.EnableAutoConfiguration 属性对应的类,并将这些类作为自动配置类进行加载。这样,配置类中的配置逻辑(如 Jedis Bean 的创建)会被自动执行,而无需显式在应用代码中引用或调用。
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=
以上就是自动配置的全部过程。
四、自定义启动器
通过刚才的自动配置我们可以发现我们可以自己配置一个spring.factories来完成自定义的启动器。
首先我们要创建一个子模块项目用来定义自动配置的信息:
RedisAutoconfiguration配置类:
@Configuration
// RedisProperties 是一个包含 Redis 配置参数的类
// 通常使用 @ConfigurationProperties 注解标注,定义了 Redis 的主机、端口等属性。
@EnableConfigurationProperties(RedisProperties.class)
public class RedisAutoconfiguration {
// 注入jedis
@Bean
// 由于上面使用了 @EnableConfigurationProperties 注解,Spring 会自动将 RedisProperties 实例注入到这个方法中。
public Jedis jedis(RedisProperties redisProperties){
// 主机/端口
// 这个 Jedis 实例就是 Redis 的客户端,用于连接和操作 Redis 数据库。
return new Jedis(redisProperties.getHost(),redisProperties.getPort());
}
}
加了@EnableConfigurationProperties(RedisProperties.class)注解用于对对 RedisProperties 配置属性类的支持。RedisProperties 是一个包含了 Redis 配置参数的类,通常包括主机名、端口等。通过这个注解,Spring Boot 可以将外部配置文件(如 application.properties 或 application.yml)中的相关配置映射到 RedisProperties 类的属性中,并自动实例化一个 RedisProperties 对象。
其中RedisProperties的代码为:
@ConfigurationProperties(prefix = "spring.redis") public class RedisProperties { private String host="localhost"; private int port=6379; public String getHost() { return host; } public void setHost(String host) { this.host = host; } public int getPort() { return port; } public void setPort(int port) { this.port = port; } }
这里面@ConfigurationProperties(prefix = "spring.redis")用于将外部配置文件,就比如 application.properties 或 application.yml中的配置参数绑定到这个类的字段上。
剩下的host是Redis服务器的主机名,port是于存储 Redis 服务器的端口号。
然后是创建了一个Jedis实例的方法, 由于使用了 @EnableConfigurationProperties(RedisProperties.class),Spring 会自动将 RedisProperties 实例作为参数注入到这个方法中。后面的getHost()和getPort()是刚才在RedisProperties中的主机名和端口号,获取到Jedis实例中并创建,从而连接到 Redis 服务器。
其中根据springboot自动配置的过程,我们也手动的配置了一个spring.factories用来设置自动加载类是谁:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.apesource.RedisAutoconfiguration
这里我们设置了RedisAutoconfiguration为自动加载类,当Spring Boot 会在启动时自动加载并应用这个配置类。
然后我们在redis-spring-boot-starter子模块项目中导入刚才的子模块项目的坐标:
<!-- 引入自定义的redis-spring-boot-autoconfigure-->
<dependency>
<groupId>com.apesource</groupId>
<artifactId>redis-spring-boot-autoconfigure</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
最后我们在主项目中先导入redis-spring-boot-starter子模块项目的坐标:
<!--导入坐标-->
<dependency>
<groupId>com.apesource</groupId>
<artifactId>redis-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
然后我们配置一下application.yml中的Redis 的连接参数的信息,用于刚才RedisProperties中的@ConfigurationProperties的获取:
spring:
redis:
port: 6060
host: 127.0.0.1
最后的最后我们去测试类主入口进行测试:
@SpringBootApplication
public class SpringbootStarter04Application {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringbootStarter04Application.class, args);
Jedis bean1 = context.getBean(Jedis.class);
System.out.println(bean1);
}
}
这就是自定义启动器的全部过程了。