SpringBoot教程,SpringBoot高级之原理分析

一、SpringBoot自动配置--注解说明

1.1、Condition条件判断

1.1.1、创建Condition模块

Condition(条件):Condition是在Spring4.0增加的条件判断功能,通过这个可以功能可以实现选择性的创建Bean操作。

思考:

SpringBoot是如何知道要创建哪个Bean的?比如SpringBoot是如何知道要创建RedisTemplate的?

创建一个模块,springboot-condition:

<code>@SpringBootApplication
public class SpringbootConditionApplication {
public static void main(String[] args) {
//启动springBoot的应用,返回spring的IOC容器
ConfigurableApplicationContext context =
SpringApplication.run(SpringbootConditionApplication.cla
ss, args);
//获取一个Bean,RedisTemplate
Object redis = context.getBean("redisTemplate");
System.out.println(redis);
}
}/<code>


SpringBoot教程,SpringBoot高级之原理分析

没有引入坐标,所以没有redisTemplate的Bean。

增加redis坐标

<code>
   org.springframework.boot
   spring-boot-starter-data-
redis
/<code>

redisTemplate的Bean已经引入

SpringBoot教程,SpringBoot高级之原理分析

1.1.2、Condition案例1

需求:

在 Spring 的 IOC 容器中有一个 User 的 Bean,现要求:

1. 导入Jedis坐标后,加载该Bean,没导入,则不加载。

2. 将类的判断定义为动态的。判断哪个字节码文件存在可以动态指定。

第一步:创建User实体类

<code>package com.itheima.condition.domain;
public class User {
}/<code>

第二步:创建User配置类

<code>package com.itheima.condition.config;
import com.itheima.condition.domain.User;
import org.springframework.context.annotation.Bean;
import
org.springframework.context.annotation.Configuration;
@Configuration
public class UserConfig {
   @Bean
   public User user(){
       return new User();
  }
}/<code>

第三步:修改启动类


SpringbootConditionApplication 中增加user的Bean获取

<code>Object user = context.getBean("user");
System.out.println(user);/<code>
SpringBoot教程,SpringBoot高级之原理分析

现在是任何情况下都能加载User这个类。

第四步:实现Condition

新建一个ClassCondition类,实现Condition接口里的matches方法来控制类的加载

新建一个类实现Condition接口

<code>public class ClassCondition implements Condition {
   @Override
   public boolean matches(ConditionContext
conditionContext, AnnotatedTypeMetadata
annotatedTypeMetadata) {
       return false;
  }
}/<code>

修改condition.config的UserConfig类:

<code>@Bean
@Conditional(ClassCondition.class)
public User user(){
   return new User();
}/<code>

将user对象加入Bean里的时候增加一个@Conditional注解

测试:当matches返回false时,不加载User类,返回true时,加载User类

第五步:导入Jedis坐标

<code>
   redis.clients
   jedis
/<code>

第六步:修改matches方法

<code>public boolean matches(ConditionContext
conditionContext, AnnotatedTypeMetadata
annotatedTypeMetadata) {
   boolean flag = true;
   try {
       Class> aClass =
Class.forName("redis.clients.jedis.Jedis");
  } catch (ClassNotFoundException e) {
       flag = false;
  }
   return flag;
}/<code>

当Jedis坐标导入后,可以加载到 redis.clients.jedis.Jedis 这个类,未导入时,加载不到redis.clients.jedis.Jedis这个类,所以通过异常捕获可以返回是否加载。

1.1.3、Condition案例2

现在的 Class.forName("redis.clients.jedis.Jedis") 这个是写死的,是否可以动态的加载呢?

第一步:新建注解类

新建ConditionOnClass注解类,增加@Conditional注解

<code>@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(ClassCondition.class)
public @interface ConditionOnClass {
   String[] value();
}/<code>

注解类中增加value变量。

第二步:修改UserConfig

<code>@Bean
//@Conditional(ClassCondition.class)
@ConditionOnClass("redis.clients.jedis.Jedis")
public User user(){
   return new User();
}/<code>

现在新建的注解@ConditionOnClass和原注解@Conditional作用一致

第三步:修改matches方法

<code>/**
* @param conditionContext 上下文对象,用于获取类加载,Bean工
厂等信息
* @param annotatedTypeMetadata 注解的元对象,可以用于获取注
解定义的属性值
* @return
*/
@Override
public boolean matches(ConditionContext
conditionContext, AnnotatedTypeMetadata
annotatedTypeMetadata) {
   Map map =
annotatedTypeMetadata.getAnnotationAttributes(ConditionO
nClass.class.getName());
   //System.out.println(map);
   String[] strings = (String[]) map.get("value");
   boolean flag = true;
   try {
       for (String className : strings) {
           Class> aClass = Class.forName(className);
      }
  } catch (ClassNotFoundException e) {
       flag = false;
  }
   return flag;
}/<code>

此时,通过UserConfig注解注入的类存在就加载User类,如果注入的类不存在就不加载User类测试,引入fastjson坐标,加载User类

第四步:查看源代码jar包

org.springframework.boot.autoconfigure.condition.ConditionalOnClass

org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration

第五步:判断配置文件

<code>@Bean
@ConditionalOnProperty(name = "itcast", havingValue =
"itheima")
public User user2(){
   return new User();
}/<code>

修改启动类

<code>Object user = context.getBean("user2");
System.out.println(user);/<code>
  • 在UserConfig类中新增一个方法,同样加载User类
  • 使用@ConditionalOnProperty注解,name是itcast,value是itheima
  • 修改配置文件application.properties,增加itcast=itheima
  • 测试加载User类

1.1.4、Condition小结

User实体类,UserConfig配置类将User放入Bean工厂,ClassCondition类重写matches方法,ConditionOnClass注解类增加注解。

自定义条件:

自定义条件类:自定义类实现Condition接口,重写 matches 方法,在 matches 方法中进行逻辑判断,返回boolean值 。matches 方法两个参数:

  • context:上下文对象,可以获取属性值,获取类加载器,获取FactoryBean等。
  • metadata:元数据对象,用于获取注解属性。

判断条件:在初始化Bean时,使用@Conditional(条件类.class)注解。

SpringBoot常用条件注解:

  • ConditionalOnProperty:判断配置文件中是否有对应的属性和值才初始化Bean
  • ConditionalOnClass:判断环境中是否有对应的字节码文件才初始化Bean
  • ConditionalOnMissingBean:判断环境中是否有对应的Bean才初始化Bean。

1.2、SpringBoot切换内置服务器

1.2.1、启用内置web服务器

引入starter-web坐标之后,服务器内置tomcat启动了

<code>
   org.springframework.boot
   spring-boot-starter-web
/<code>

1.2.2、查看一下源jar包

org.springframework.boot.autoconfigure.web.embedded.TomcatWebServerFactoryCustomizer

修改坐标:

<code>
   org.springframework.boot
   spring-boot-starter-web
   
       
         spring-boot-starter-
tomcat
           org.springframework.boot
       
   


   spring-boot-starter-jetty
   org.springframework.boot
/<code>
SpringBoot教程,SpringBoot高级之原理分析

Jetty服务器启动

1.3、@Enable*注解

SpringBoot中提供了很多Enable开头的注解,这些注解都是用于动态启用某

些功能的。而其底层原理是使用@Import注解导入一些配置类,实现Bean的动

态加载。

问题:

SpringBoot 工程是否可以直接获取jar包中定义的Bean?

1.3.1、创建两个模块

一个是springboot-enable,一个是springboot-enable-other

1.3.2、创建实体类与配置类

<code>package com.itheima.domain;
public class User {
}
@Configuration
public class UserConfig {
   @Bean
   public User user(){
       return new User();
  }
}/<code>

1.3.3、引入enable-other坐标

<code>
   com.itheima
   springboot-enable-other
   0.0.1-SNAPSHOT
/<code>

1.3.4、修改enable启动类

<code>public static void main(String[] args) {
   ConfigurableApplicationContext context =
SpringApplication.run(SpringbootEnableApplication.class,
args);
   Object user = context.getBean("user");
   System.out.println(user);
}/<code>

Bean里没有User这个类

SpringBoot教程,SpringBoot高级之原理分析

@ComponentScan扫描范围:当前引导类所在包以其子包

com.itheima.enable

com.itheima.config.UserConfig

两个包明显是平级的

第一种:增加扫描包的范围

<code>@SpringBootApplication
@ComponentScan("com.itheima.config")
public class SpringbootEnableApplication {}/<code>

第二种:使用@Import注解

使用@Import注解,都会被Spring创建,放入IOC容器

<code>@SpringBootApplication
@Import(UserConfig.class)
public class SpringbootEnableApplication {}/<code>

第三种:对@Import进行封装

创建EnableUser注解类,使用@Import注解

<code>@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(UserConfig.class)
public @interface EnableUser {}/<code>

此时,查看@SpringBootApplication注解时发现,里面有@EnableAutoConfiguration注解,而这个注解中使用了@Import({
AutoConfigurationImportSelector.class})注解,加入了一个类。

1.4、@Import注解

@Enable*底层依赖于@Import注解导入一些类,使用@Import导入的类会被Spring加载到IOC容器中。而@Import提供4中用法:

  • 导入Bean
  • 导入配置类
  • 导入ImportSelector实现类,一般用于加载配置文件中的类
  • 导入ImportBeanDefinitionRegistrar实现类

1.4.1、导入Bean

<code>@SpringBootApplication
@Import(User.class)
public class SpringbootEnableApplication {}/<code>
SpringBoot教程,SpringBoot高级之原理分析

这样导入不了的原因在于,我们是通过Bean的名称"user"来获取对象,而导入的这个User.class不一定叫"user"这个名字,所以需要修改启动类,通过类的类型来获取。

<code>public static void main(String[] args) {
   ConfigurableApplicationContext context =
SpringApplication.run(SpringbootEnableApplication.class,
args);
   /*Object user = context.getBean("user");
System.out.println(user);*/
   User user = context.getBean(User.class);
   System.out.println(user);
}/<code>

如果想要获取Bean的名称,那么可以使用context.getBeansOfType

<code>Map map =
context.getBeansOfType(User.class);
System.out.println(map);/<code>
SpringBoot教程,SpringBoot高级之原理分析

所以自动创建的Bean名称为com.itheima.domain.User

<code>Object user1 =
context.getBean("com.itheima.domain.User");
System.out.println(user1);/<code> 

那么通过这个名字就可以获取这个类

SpringBoot教程,SpringBoot高级之原理分析

1.4.2、导入配置类

配置类指的是前面创建好的UserConfig,那么现在再创建一个实体类

<code>package com.itheima.domain;
public class Role {
}/<code>

修改UserConfig配置类,将Role这个类也加入到Bean里

<code>@Bean
public Role role(){
   return new Role();
}/<code>

修改启动类,增加Role的类加载

<code>Role role = context.getBean(Role.class);
System.out.println(role);/<code>
SpringBoot教程,SpringBoot高级之原理分析

此时,两个类都可以被加载。

1.4.3、实现ImportSelector接口

创建一个MyImportSelector类,实现ImportSelector接口,重写里面的selectImports方法

<code>@SpringBootApplication
@Import(MyImportSelector.class)
public class SpringbootEnableApplication {}/<code>

修改启动类

<code>@SpringBootApplication
@Import(MyImportSelector.class)
public class SpringbootEnableApplication {}/<code>


SpringBoot教程,SpringBoot高级之原理分析

此时,两个类都可以被加载。

1.4.4、实现
ImportBeanDefifinitionRegistrar接口

创建一个
MyImportBeanDefifinitionRegistrar类,实现
ImportBeanDefifinitionRegistrar接口,重写registerBeanDefifinitions方法

<code>AbstractBeanDefinition beanDefinition =
BeanDefinitionBuilder.rootBeanDefinition(User.class).get
BeanDefinition();
registry.registerBeanDefinition("user", beanDefinition);/<code>

修改启动类

<code>@SpringBootApplication
@Import(MyImportBeanDefinitionRegistrar.class)
public class SpringbootEnableApplication {}/<code>
SpringBoot教程,SpringBoot高级之原理分析

此时,User类被加载,而Role没有被加载,想要加载Role类,再次修改registerBeanDefinitions方法

<code>@Override
public void registerBeanDefinitions(AnnotationMetadata
importingClassMetadata, BeanDefinitionRegistry registry)
{
   //注册User类
   AbstractBeanDefinition beanDefinition =
BeanDefinitionBuilder.rootBeanDefinition(User.class).get
BeanDefinition();
   registry.registerBeanDefinition("user",
beanDefinition);
   //注册Role类
   beanDefinition =
BeanDefinitionBuilder.rootBeanDefinition(Role.class).get
BeanDefinition();
   registry.registerBeanDefinition("role",
beanDefinition);
}/<code>

而通过名称也可以将User类加入到Bean中,只需要修改启动类用名称获取Bean即可。

1.5、@EnableAutoConfiguration

@SpringBootApplication中的@EnableAutoConfiguration注解,也是通过@Import({
AutoConfigurationImportSelector.class})来实现类的加载,那么说明Springboot中也是使用第三种方法导入类。

SpringBoot教程,SpringBoot高级之原理分析

SpringBoot教程,SpringBoot高级之原理分析

SpringBoot教程,SpringBoot高级之原理分析

SpringBoot教程,SpringBoot高级之原理分析

配置文件位置:META-INF/spring.factories,该配置文件中定义了大量的配置类,当 SpringBoot 应用启动时,会自动加载这些配置类,初始化Bean。

并不是所有的Bean都会被初始化,在配置类中使用Condition来加载满足条件的Bean。

二、SpringBoot自动配置--自定义Starter

2.1、分析MyBatis加载过程

引入坐标

<code> 

   org.mybatis.spring.boot
   mybatis-spring-boot-starter
   1.3.2
/<code>

加载过程

SpringBoot教程,SpringBoot高级之原理分析

2.2、自定义Starter需求

自定义redis-starter。要求当导入redis坐标时,SpringBoot自动创建Jedis的Bean。

步骤:

  • 创建 redis-spring-boot-autoconfigure 模块
  • 创建 redis-spring-boot-starter 模块,依赖 redis-spring-boot-autoconfigure的模块
  • 在 redis-spring-boot-autoconfigure 模块中初始化 Jedis 的 Bean。并定义META-INF/spring.factories 文件
  • 在测试模块中引入自定义的 redis-starter 依赖,测试获取 Jedis 的Bean,操作 redis。

2.3、创建两个模块

redis-spring-boot-autoconfigure

redis-spring-boot-starter

在starter的坐标中加入autoconfigure

<code>
   com.itheima
   redis-spring-boot-
autoconfigure
   0.0.1-SNAPSHOT
/<code>

在autoconfigure的坐标中加入Jedis

<code>
   redis.clients
   jedis
/<code>

2.4、创建RedisAutoConfigure配置类

在autoconfigure模块中创建RedisAutoConfigure配置类

<code>@Configuration
@EnableConfigurationProperties(RedisProperties.class)
public class RedisAutoConfigure {
   /**
    * 提供Jedis的Bean
    */
   @Bean
   public Jedis jedis(RedisProperties redisProperties){
       return new Jedis(redisProperties.getHost(),
redisProperties.getPort());
  }
}/<code>

在autoconfigure模块中创建RedisProperties配置类

<code>@ConfigurationProperties(prefix = "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;
  }
}/<code>

2.5、创建META-INF/spring.factories

在resources下新建META-INF/spring.factories

SpringBoot教程,SpringBoot高级之原理分析

<code>org.springframework.boot.autoconfigure.EnableAutoConfigu
ration=com.itheima.redis.RedisAutoConfigure/<code>

2.6、修改启动类

修改springboot-enable启动类

<code>@SpringBootApplication
public class SpringbootEnableApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context =
SpringApplication.run(SpringbootEnableApplication.class,
args);
       
Jedis jedis = context.getBean(Jedis.class);
System.out.println(jedis);
}
}/<code>
SpringBoot教程,SpringBoot高级之原理分析

此时,Jedis可以加载到。

2.7、使用Jedis

<code>jedis.set("name", "itcast");
String value = jedis.get("name");
System.out.println(value);/<code>

2.8、使用配置文件

修改enable里的application.properties

SpringBoot教程,SpringBoot高级之原理分析

启动时报错,连接不到本地的localhost:6666,证明redis配置文件已经起作用了。

2.9、优化RedisAutoConfigure

在RedisAutoConfigure类上增加注解@ConditionalOnClass(Jedis.class)加载的时候判断Jedis类存在的时候才加载Bean

<code>@Configuration
@EnableConfigurationProperties(RedisProperties.class)
@ConditionalOnClass(Jedis.class)
public class RedisAutoConfigure {
   /**
    * 提供Jedis的Bean
    */
   @Bean
   @ConditionalOnMissingBean(name = "jedis")
   public Jedis jedis(RedisProperties redisProperties){
       System.out.println("RedisAutoConfigure....");
       return new Jedis(redisProperties.getHost(),
redisProperties.getPort());
  }
}/<code>

在jedis方法上增加@ConditionalOnMissingBean注解,当jedis没有被创建时加载这个类,并写入Bean

测试:

在启动类中创建一个Jedis

<code>@Bean
public Jedis jedis(){
   return new Jedis();
}/<code>

启动的时候可以看到当Jedis存在时则不再加载。

2.10、查看redis源jar包

SpringBoot教程,SpringBoot高级之原理分析

三、SpringBoot监听机制

3.1、JAVA的监听机制

SpringBoot 的监听机制,其实是对Java提供的事件监听机制的封装。

Java中的事件监听机制定义了以下几个角色:

①事件:Event,继承 java.util.EventObject 类的对象

②事件源:Source ,任意对象Object

③监听器:Listener,实现 java.util.EventListener 接口的对象

3.2、Springboot监听器

SpringBoot 在项目启动时,会对几个监听器进行回调,我们可以实现这些监听器接口,在项目启动时完成一些操作。

一共有四种实现方法:

  • ApplicationContextInitializer
  • SpringApplicationRunListener
  • CommandLineRunner
  • ApplicationRunner

3.3、创建Listener模块

3.3.1、创建
MyApplicationContextInitializer

创建
MyApplicationContextInitializer,实现
ApplicationContextInitializer接口

<code>@Component
public class MyApplicationContextInitializer implements
ApplicationContextInitializer {
   @Override
   public void
initialize(ConfigurableApplicationContext
configurableApplicationContext) {
     
 System.out.println("ApplicationContextInitializer...ini
tialize");
  }
}/<code>

3.3.2、创建
MySpringApplicationRunListener

创建
MySpringApplicationRunListener,实现
SpringApplicationRunListener接口

<code>@Component
public class MySpringApplicationRunListener implements
SpringApplicationRunListener {
   @Override
   public void starting() {
     
 System.out.println("SpringApplicationRunListener...正在
启动");
  }
   @Override
   public void
environmentPrepared(ConfigurableEnvironment environment)
{
     
 System.out.println("SpringApplicationRunListener...环境
准备中");
  }
   @Override
   public void
contextPrepared(ConfigurableApplicationContext context)
{


     
 System.out.println("SpringApplicationRunListener...上下
文准备");
  }
   @Override
   public void
contextLoaded(ConfigurableApplicationContext context) {
     
 System.out.println("SpringApplicationRunListener...上下
文加载");
  }
   @Override
   public void started(ConfigurableApplicationContext
context) {
     
 System.out.println("SpringApplicationRunListener...已经
启动");
  }
   @Override
   public void running(ConfigurableApplicationContext
context) {
     
 System.out.println("SpringApplicationRunListener...正在
启动中");
  }
   @Override
   public void failed(ConfigurableApplicationContext
context, Throwable exception) {
     
 System.out.println("SpringApplicationRunListener...启动
失败");
  }
}/<code>

3.3.3、创建MyCommandLineRunner

创建MyCommandLineRunner,实现CommandLineRunner接口

<code>@Component
public class MyCommandLineRunner implements
CommandLineRunner {
@Override
public void run(String... args) throws Exception {
System.out.println("CommandLineRunner...run");
}
}/<code>

3.3.4、创建MyApplicationRunner

创建MyApplicationRunner,实现ApplicationRunner接口

<code>@Component
public class MyCommandLineRunner implements
CommandLineRunner {
   @Override
   public void run(String... args) throws Exception {
       System.out.println("CommandLineRunner...run");
  }
}/<code>

3.3.5、运行启动类

只有MyApplicationRunner和MyCommandLineRunner运行了监听。

<code>@Component
public class MyApplicationRunner implements
ApplicationRunner {
   @Override
   public void run(ApplicationArguments args) throws
Exception {
       System.out.println("ApplicationRunner...run");
  }
}/<code>

3.3.6、修改监听类

修改MyApplicationRunner和MyCommandLineRunner打印args

MyApplicationRunner:

<code>@Override
public void run(ApplicationArguments args) throws
Exception {
   System.out.println("ApplicationRunner...run");
 System.out.println(Arrays.asList(args.getSourceArgs()))
;
}/<code>

MyCommandLineRunner:

<code>@Override
public void run(String... args) throws Exception {
   System.out.println("CommandLineRunner...run");
   System.out.println(Arrays.asList(args));
}/<code>

在运行时可以将配置信息加载进来

SpringBoot教程,SpringBoot高级之原理分析

SpringBoot教程,SpringBoot高级之原理分析

所以,这两个监听ApplicationRunner和CommandLineRunner是一样的。

3.4、配置
MyApplicationContextInitializer

在模块中增加 META-INF/spring.factories 配置文件

<code>org.springframework.context.ApplicationContextInitializer=com.itheima.listener.listener.MyApplicationContextInitializer/<code>
SpringBoot教程,SpringBoot高级之原理分析

3.5、配置
MySpringApplicationRunListener

修改 META-INF/spring.factories 配置文件

<code>MyCommandLineRunner:@Override
public void run(String... args) throws Exception {
   System.out.println("CommandLineRunner...run");
   System.out.println(Arrays.asList(args));
}/<code>
SpringBoot教程,SpringBoot高级之原理分析

运行提示:没有
MySpringApplicationRunListener的init方法

所以需要查看
SpringApplicationRunListener这个接口的实现类

SpringBoot教程,SpringBoot高级之原理分析

需要SpringApplication和String[]两个参数进行构造,因此修改
MySpringApplicationRunListener实现类,增加构造方法

<code>public MySpringApplicationRunListener(SpringApplication
application, String[] args) {}/<code>

并且去掉@Component注解

SpringBoot教程,SpringBoot高级之原理分析

此时启动正常。

SpringBoot教程,SpringBoot高级之原理分析

ApplicationStartedEvent继承自SpringApplicationEvent

SpringApplicationEvent继承自ApplicationEvent

ApplicationEvent继承自EventObject

证明:Springboot里的监听事件是对java监听事件的一个封装

3.6、SpringBoot运行流程分析

SpringBoot教程,SpringBoot高级之原理分析

四、SpringBoot监控

SpringBoot自带监控功能Actuator,可以帮助实现对程序内部运行情况监控,比如监控状况、Bean加载情况、配置属性、日志信息等。

4.1、创建模块

需要勾选web和ops下的SpringBootActuator

坐标自动引入

<code>
   org.springframework.boot
   spring-boot-starter-
actuator


   org.springframework.boot
   spring-boot-starter-web
/<code>
SpringBoot教程,SpringBoot高级之原理分析

4.2、查看info

info获取的是配置文件以info开头的信息:

修改properties

<code>info.name=zhangsan
info.age=20/<code>
SpringBoot教程,SpringBoot高级之原理分析

4.3、查看所有信息

未开启所有明细状态:

SpringBoot教程,SpringBoot高级之原理分析

修改properties

<code>management.endpoint.health.show-details=always/<code>

开启所有明细状态:

SpringBoot教程,SpringBoot高级之原理分析

4.4、暴露所有的信息

修改properties:

<code>management.endpoints.web.exposure.include=*/<code>
SpringBoot教程,SpringBoot高级之原理分析



分享到:


相關文章: