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高級之原理分析



分享到:


相關文章: