SpringBoot——原理

目录

1、自动配置

1、bean加载方式

2、bean的加载控制

3、自动配置原理(工作流程)

4、变更自动配置

方式二:通过注解参数排除自动配置类

2、自定义starter开发

3、SpringBoot程序启动流程解析


1、自动配置

1、bean加载方式

  • XML方式声明bean

SpringBoot——原理_第1张图片

  • XML+注解方式声明bean

SpringBoot——原理_第2张图片

 使用@Component及其衍生注解@Controller 、@Service、@Repository定义bean

@Service
public class BookServiceImpl implements BookService {

}

使用@Bean定义第三方bean,并将所在类定义为配置类或Bean

@Component
public class DbConfig {
    @Bean
    public DruidDataSource getDataSource(){
        DruidDataSource ds = new DruidDataSource();
        return ds;
    }
}
  • 注解方式声明配置类

定义一个类并使用@ComponentScan替代原始xml配置中的包扫描这个动作,其实功能基本相同。

@ComponentScan({"com.itheima.bean","com.itheima.config"})
public class SpringConfig3 {
    @Bean
    public DogFactoryBean dog(){
        return new DogFactoryBean();
    }
}

使用FactroyBean接口

spring提供了一个接口FactoryBean,也可以用于声明bean,只不过实现了FactoryBean接口的类造出来的对象不是当前类的对象,而是FactoryBean接口泛型指定类型的对象。

作用:可以在对象初始化前做一些事情,下例中的注释位置就是让你自己去扩展要做的其他事情的。

你可以理解为Dog是一个抽象后剥离的特别干净的模型,但是实际使用的时候必须进行一系列的初始化动作。只不过根据情况不同,初始化动作不同而已。

如果写入Dog,或许初始化动作A当前并不能满足你的需要,这个时候你就要做一个DogB的方案了。你就要做两个Dog类。当使用FactoryBean接口就可以完美解决这个问题。

public class DogFactoryBean implements FactoryBean {
    @Override
    public Dog getObject() throws Exception {
        Dog d = new Dog();
        //.........
        return d;
    }
    @Override
    public Class getObjectType() {
        return Dog.class;
    }
    @Override
    public boolean isSingleton() {
        return true;
    }
}
@ComponentScan({"com.itheima.bean","com.itheima.config"})
public class SpringConfig3 {
    @Bean
    public DogFactoryBean dog(){
        return new DogFactoryBean();
    }
}
  • 注解格式导入XML格式配置的bean  
//注解格式导入XML格式配置的bean
@Configuration
@ImportResource("applicationContext1.xml")
public class SpringConfig32 {

}

@Configuration可以保障配置类中使用方法创建的bean的唯一性。为@Configuration注解设置proxyBeanMethods属性值为true即可,由于此属性默认值为true。  

@Configuration(proxyBeanMethods = true)
public class SpringConfig33 {
    @Bean
    public Cat cat(){
        return new Cat();
    }
}
  • 使用@Import注解注入bean

我们需要一种精准制导的加载方式,使用@Import注解就可以解决你的问题。 但是@Import注解拥有其重要的应用场景。它可以解决要加载的bean没有使用@Component修饰呢。

@Import({Dog.class,DbConfig.class})
public class SpringConfig4 {

}
//使用@Import注解注入配置类
@Import(DogFactoryBean.class)
public class SpringConfig4 {

}
  • 编程形式注册bean

前面介绍的加载bean的方式都是在容器启动阶段完成bean的加载,下面这种方式就比较特殊了,可以在容器初始化完成后手动加载bean。

public class App5 {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);

        //上下文容器对象已经初始化完毕后,手工加载bean
        ctx.register(Mouse.class);
    }
}

注:容器中已经有了某种类型的bean,再加载会覆盖。

  • 导入实现了ImportSelector接口的类

在容器初始化过程中进行控制

public class MyImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata metadata) {
        //各种条件的判定,判定完毕后,决定是否装载指定的bean
        boolean flag = metadata.hasAnnotation("org.springframework.context.annotation.Configuration");
        if(flag){
            return new String[]{"com.itheima.bean.Dog"};
        }
        return new String[]{"com.itheima.bean.Cat"};
    }
}
  • 导入实现了ImportBeanDefinitionRegistrar接口的类

spring中定义了一个叫做BeanDefinition的东西,它才是控制bean初始化加载的核心。 ImportBeanDefinitionRegistrar接口的方式定义bean,并且还可以让你对bean的初始化进行更加细粒度的控制。

public class MyRegistrar implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
        BeanDefinition beanDefinition = 	
            BeanDefinitionBuilder.rootBeanDefinition(BookServiceImpl2.class).getBeanDefinition();
        registry.registerBeanDefinition("bookService",beanDefinition);
    }
}
  • 导入实现了BeanDefinitionRegistryPostProcessor接口的类

BeanDefinitionRegistryPostProcessor 全称bean定义后处理器,在所有bean注册都折腾完后,它把最后一道关,说白了,它说了算,它是最后一个运行的。

public class MyPostProcessor implements BeanDefinitionRegistryPostProcessor {
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        BeanDefinition beanDefinition = 
            BeanDefinitionBuilder.rootBeanDefinition(BookServiceImpl4.class).getBeanDefinition();
        registry.registerBeanDefinition("bookService",beanDefinition);
    }
}

总结:

  1. bean的定义由前期xml配置逐步演化成注解配置,本质是一样的,都是通过反射机制加载类名后创建对象,对象就是spring管控的bean

  2. @Import注解可以指定加载某一个类作为spring管控的bean,如果被加载的类中还具有@Bean相关的定义,会被一同加载

  3. spring开放出了若干种可编程控制的bean的初始化方式,通过分支语句由固定的加载bean转成了可以选择bean是否加载或者选择加载哪一种bean

2、bean的加载控制

企业级开发中不可能在spring容器中进行bean的饱和式加载的。什么是饱和式加载,就是不管用不用,全部加载。

在spring容器中,通过判定是否加载了某个类来控制某些bean的加载是一种常见操作。 先判断一个类的全路径名是否能够成功加载,加载成功说明有这个类,那就干某项具体的工作,否则就干别的工作。

public class MyImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        try {
            Class clazz = Class.forName("com.itheima.bean.Mouse");
            if(clazz != null) {
                return new String[]{"com.itheima.bean.Cat"};
            }
        } catch (ClassNotFoundException e) {
//            e.printStackTrace();
            return new String[0];
        }
        return null;
    }
}

下例使用@ConditionalOnClass注解实现了当虚拟机中加载了com.itheima.bean.Wolf类时加载对应的bean。

@Bean
@ConditionalOnClass(name = "com.itheima.bean.Wolf")
public Cat tom(){
    return new Cat();
}

@ConditionalOnMissingClass注解控制虚拟机中没有加载指定的类才加载对应的bean。  

@Bean
@ConditionalOnMissingClass("com.itheima.bean.Dog")
public Cat tom(){
    return new Cat();
}

这种条件还可以做并且的逻辑关系,写2个就是2个条件都成立,写多个就是多个条件都成立。

@Bean
@ConditionalOnClass(name = "com.itheima.bean.Wolf")
@ConditionalOnMissingClass("com.itheima.bean.Mouse")
public Cat tom(){
    return new Cat();
}

除了判定是否加载类,还可以对当前容器类型做判定,下例是判定当前容器环境是否是web环境。

@Bean
@ConditionalOnWebApplication
public Cat tom(){
    return new Cat();
}

下面是判定容器环境是否是非web环境。

@Bean
@ConditionalOnNotWebApplication
public Cat tom(){
    return new Cat();
}

判定是否加载了指定名称的bean,没有的话就提供给你,有的话就用你自己的。

@Bean
@ConditionalOnBean(name="jerry")
public Cat tom(){
    return new Cat();
}

以下就是判定当前是否加载了mysql的驱动类,如果加载了,我就给你搞一个Druid的数据源对象出来。

public class SpringConfig {
    @Bean
    @ConditionalOnClass(name="com.mysql.jdbc.Driver")
    public DruidDataSource dataSource(){
        return new DruidDataSource();
    }
}

3、自动配置原理(工作流程)

        阶段一:准备阶段

  1. springboot的开发人员先大量收集Spring开发者的编程习惯,整理开发过程每一个程序经常使用的技术列表,形成一个技术集A

  2. 收集常用技术(技术集A)的使用参数,不管你用什么常用设置,我用什么常用设置,统统收集起来整理一下,得到开发过程中每一个技术的常用设置,形成每一个技术对应的设置集B

    阶段二:加载阶段

  3. springboot初始化Spring容器基础环境,读取用户的配置信息,加载用户自定义的bean和导入的其他坐标,形成初始化环境

  4. springboot将技术集A包含的所有技术在SpringBoot启动时默认全部加载,这时肯定加载的东西有一些是无效的,没有用的

  5. springboot会对技术集A中每一个技术约定出启动这个技术对应的条件,并设置成按条件加载,由于开发者导入了一些bean和其他坐标,也就是与初始化环境,这个时候就可以根据这个初始化环境与springboot的技术集A进行比对了,哪个匹配上加载哪个

  6. 因为有些技术不做配置就无法工作,所以springboot开始对设置集B下手了。它统计出各个国家各个行业的开发者使用某个技术时最常用的设置是什么,然后把这些设置作为默认值直接设置好,并告诉开发者当前设置我已经给你搞了一套,你要用可以直接用,这样可以减少开发者配置参数的工作量

  7. 但是默认配置不一定能解决问题,于是springboot开放修改设置集B的接口,可以由开发者根据需要决定是否覆盖默认配置

  • 首先指定一个技术X,我们打算让技术X具备自动配置的功能,这个技术X可以是任意功能,这个技术隶属于上面描述的技术集A
public class CartoonCatAndMouse{

}

然后找出技术X使用过程中的常用配置Y,这个配置隶属于上面表述的设置集B  

cartoon:
  cat:
    name: "图多盖洛"
    age: 5
  mouse:
    name: "泰菲"
    age: 1

将常用配置Y设计出对应的yml配置书写格式,然后定义一个属性类封装对应的配置属性,这个过程其实就是上一节咱们做的bean的依赖属性管理,一模一样

@ConfigurationProperties(prefix = "cartoon")
@Data
public class CartoonProperties {
    private Cat cat;
    private Mouse mouse;
}

最后做一个配置类,当这个类加载的时候就可以初始化对应的功能bean,并且可以加载到对应的配置

@EnableConfigurationProperties(CartoonProperties.class)
public class CartoonCatAndMouse implements ApplicationContextAware {
    private CartoonProperties cartoonProperties;
}

 当然,你也可以为当前自动配置类设置上激活条件,例如使用@CondtionOn* * * * 为其设置加载条件

@ConditionalOnClass(name="org.springframework.data.redis.core.RedisOperations")
@EnableConfigurationProperties(CartoonProperties.class)
public class CartoonCatAndMouse implements ApplicationContextAware {
    private CartoonProperties cartoonProperties;
}

springboot启动的时候去加载这个类 :springboot为我们开放了一个配置入口,在配置目录中创建META-INF目录,并创建spring.factories文件,在其中添加设置,说明哪些类要启动自动配置就可以了。

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.itheima.bean.CartoonCatAndMouse

自动配置其实是一个小的生态,可以按照如下思想理解:

  1. 自动配置从根本上来说就是一个bean的加载

  2. 通过bean加载条件的控制给开发者一种感觉,自动配置是自适应的,可以根据情况自己判定,但实际上就是最普通的分支语句的应用,这是蒙蔽我们双眼的第一层面纱

  3. 使用bean的时候,如果不设置属性,就有默认值,如果不想用默认值,就可以自己设置,也就是可以修改部分或者全部参数,感觉这个过程好屌,也是一种自适应的形式,其实还是需要使用分支语句来做判断的,这是蒙蔽我们双眼的第二层面纱

  4. springboot技术提前将大量开发者有可能使用的技术提前做好了,条件也写好了,用的时候你导入了一个坐标,对应技术就可以使用了,其实就是提前帮我们把spring.factories文件写好了,这是蒙蔽我们双眼的第三层面纱。

总结

  1. springboot启动时先加载spring.factories文件中的org.springframework.boot.autoconfigure.EnableAutoConfiguration配置项,将其中配置的所有的类都加载成bean

  2. 在加载bean的时候,bean对应的类定义上都设置有加载条件,因此有可能加载成功,也可能条件检测失败不加载bean

  3. 对于可以正常加载成bean的类,通常会通过@EnableConfigurationProperties注解初始化对应的配置属性类并加载对应的配置

  4. 配置属性类上通常会通过@ConfigurationProperties加载指定前缀的配置,当然这些配置通常都有默认值。如果没有默认值,就强制你必须配置后使用了

4、变更自动配置

系统默认会加载100多种自动配置的技术,可以手工干预此工程,禁用自动配置

方式一:通过yaml配置设置排除指定的自动配置类

spring:
  autoconfigure:
    exclude:
      - org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration

方式二:通过注解参数排除自动配置类

@EnableAutoConfiguration(excludeName = "",exclude = {})

方式三:排除坐标(应用面较窄)

例如web程序启动时会自动启动tomcat服务器,可以通过排除坐标的方式,让加载tomcat服务器的条件失效。


    
        org.springframework.boot
        spring-boot-starter-web
        
        
            
                org.springframework.boot
                spring-boot-starter-tomcat
            
        
    
    
    
        org.springframework.boot
        spring-boot-starter-jetty
    

2、自定义starter开发

我们需要做到的效果是导入当前模块即开启此功能,因此使用自动配置实现功能的自动装载,需要开发自动配置类在启动项目时加载当前功能。

public class IpAutoConfiguration {
    @Bean
    public IpCountService ipCountService(){
        return new IpCountService();
    }
}

自动配置类需要在spring.factories文件中做配置方可自动运行。

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.itcast.autoconfig.IpAutoConfiguration

 编译并安装到仓库中。为防止问题出现,建议每次安装之前先clean然后install,保障资源进行了更新。

原始调用项目中导入当前开发的starter

开启yml提示功能:

springboot提供有专用的工具实现此功能,仅需要导入下列坐标。


    org.springframework.boot
    spring-boot-configuration-processor
    true

程序编译后,在META-INF目录中会生成对应的提示文件,然后拷贝生成出的文件到自己开发的META-INF目录中,并对其进行编辑。

打开生成的文件,可以看到如下信息。

  • 其中groups属性定义了当前配置的提示信息总体描述,当前配置属于哪一个属性封装类,
  • properties属性描述了当前配置中每一个属性的具体设置,包含名称、类型、描述、默认值等信息。
  • hints属性默认是空白的,没有进行设置。

hints属性可以参考springboot源码中的制作,设置当前属性封装类专用的提示信息,下例中为日志输出模式属性model设置了两种可选提示信息。  

{
  "groups": [
    {
      "name": "tools.ip",
      "type": "cn.itcast.properties.IpProperties",
      "sourceType": "cn.itcast.properties.IpProperties"
    }
  ],
  "properties": [
    {
      "name": "tools.ip.cycle",
      "type": "java.lang.Long",
      "description": "日志显示周期",
      "sourceType": "cn.itcast.properties.IpProperties",
      "defaultValue": 5
    },
    {
      "name": "tools.ip.cycle-reset",
      "type": "java.lang.Boolean",
      "description": "是否周期内重置数据",
      "sourceType": "cn.itcast.properties.IpProperties",
      "defaultValue": false
    },
    {
      "name": "tools.ip.model",
      "type": "java.lang.String",
      "description": "日志输出模式  detail:详细模式  simple:极简模式",
      "sourceType": "cn.itcast.properties.IpProperties"
    }
  ],
  "hints": [
    {
      "name": "tools.ip.model",
      "values": [
        {
          "value": "detail",
          "description": "详细模式."
        },
        {
          "value": "simple",
          "description": "极简模式."
        }
      ]
    }
  ]
}

3、SpringBoot程序启动流程解析

其实不管是springboot程序还是spring程序,启动过程本质上都是在做容器的初始化,并将对应的bean初始化出来放入容器。

在spring环境中,每个bean的初始化都要开发者自己添加设置,但是切换成springboot程序后,自动配置功能的添加帮助开发者提前预设了很多bean的初始化过程,加上各种各样的参数设置,使得整体初始化过程显得略微复杂,但是核心本质还是在做一件事,初始化容器。

springboot初始化的参数根据参数的提供方,划分成如下3个大类,每个大类的参数又被封装了各种各样的对象,具体如下:

  • 环境属性(Environment)

  • 系统配置(spring.factories)

  • 参数(Arguments、application.properties)

Springboot30StartupApplication【10】->SpringApplication.run(Springboot30StartupApplication.class, args);
    SpringApplication【1332】->return run(new Class[] { primarySource }, args);
        SpringApplication【1343】->return new SpringApplication(primarySources).run(args);
            SpringApplication【1343】->SpringApplication(primarySources)
            # 加载各种配置信息,初始化各种配置对象
                SpringApplication【266】->this(null, primarySources);
                    SpringApplication【280】->public SpringApplication(ResourceLoader resourceLoader, Class... primarySources)
                        SpringApplication【281】->this.resourceLoader = resourceLoader;
                        # 初始化资源加载器
                        SpringApplication【283】->this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
                        # 初始化配置类的类名信息(格式转换)
                        SpringApplication【284】->this.webApplicationType = WebApplicationType.deduceFromClasspath();
                        # 确认当前容器加载的类型
                        SpringApplication【285】->this.bootstrapRegistryInitializers = getBootstrapRegistryInitializersFromSpringFactories();
                        # 获取系统配置引导信息
                        SpringApplication【286】->setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
                        # 获取ApplicationContextInitializer.class对应的实例
                        SpringApplication【287】->setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
                        # 初始化监听器,对初始化过程及运行过程进行干预
                        SpringApplication【288】->this.mainApplicationClass = deduceMainApplicationClass();
                        # 初始化了引导类类名信息,备用
            SpringApplication【1343】->new SpringApplication(primarySources).run(args)
            # 初始化容器,得到ApplicationContext对象
                SpringApplication【323】->StopWatch stopWatch = new StopWatch();
                # 设置计时器
                SpringApplication【324】->stopWatch.start();
                # 计时开始
                SpringApplication【325】->DefaultBootstrapContext bootstrapContext = createBootstrapContext();
                # 系统引导信息对应的上下文对象
                SpringApplication【327】->configureHeadlessProperty();
                # 模拟输入输出信号,避免出现因缺少外设导致的信号传输失败,进而引发错误(模拟显示器,键盘,鼠标...)
                    java.awt.headless=true
                SpringApplication【328】->SpringApplicationRunListeners listeners = getRunListeners(args);
                # 获取当前注册的所有监听器
                SpringApplication【329】->listeners.starting(bootstrapContext, this.mainApplicationClass);
                # 监听器执行了对应的操作步骤
                SpringApplication【331】->ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
                # 获取参数
                SpringApplication【333】->ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
                # 将前期读取的数据加载成了一个环境对象,用来描述信息
                SpringApplication【333】->configureIgnoreBeanInfo(environment);
                # 做了一个配置,备用
                SpringApplication【334】->Banner printedBanner = printBanner(environment);
                # 初始化logo
                SpringApplication【335】->context = createApplicationContext();
                # 创建容器对象,根据前期配置的容器类型进行判定并创建
                SpringApplication【363】->context.setApplicationStartup(this.applicationStartup);
                # 设置启动模式
                SpringApplication【337】->prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
                # 对容器进行设置,参数来源于前期的设定
                SpringApplication【338】->refreshContext(context);
                # 刷新容器环境
                SpringApplication【339】->afterRefresh(context, applicationArguments);
                # 刷新完毕后做后处理
                SpringApplication【340】->stopWatch.stop();
                # 计时结束
                SpringApplication【341】->if (this.logStartupInfo) {
                # 判定是否记录启动时间的日志
                SpringApplication【342】->    new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
                # 创建日志对应的对象,输出日志信息,包含启动时间
                SpringApplication【344】->listeners.started(context);
                # 监听器执行了对应的操作步骤
                SpringApplication【345】->callRunners(context, applicationArguments);
                # 调用运行器
                SpringApplication【353】->listeners.running(context);
                # 监听器执行了对应的操作步骤

1. 初始化各种属性,加载成对象

  • 读取环境属性(Environment)
  • 系统配置(spring.factories)
  • 参数(Arguments、application.properties)

2. 创建Spring容器对象ApplicationContext,加载各种配置
3. 在容器创建前,通过监听器机制,应对不同阶段加载数据、更新数据的需求
4. 容器初始化过程中追加各种功能,例如统计时间、输出日志等 

如果想干预springboot的启动过程,比如自定义一个数据库环境检测的程序,。

遇到这样的问题,大部分技术是这样设计的,设计若干个标准接口,对应程序中的所有标准过程。当你想干预某个过程时,实现接口就行了。例如spring技术中bean的生命周期管理就是采用标准接口进行的。  

public class Abc implements InitializingBean, DisposableBean {
    public void destroy() throws Exception {
        //销毁操作
    }
    public void afterPropertiesSet() throws Exception {
        //初始化操作
    }
}

springboot采用了一种最原始的设计模式来解决这个问题(标准接口,整体过程管理分散,各自为政,管理难度大,过程过于松散。 ),这就是监听器模式,使用监听器来解决这个问题。

springboot将自身的启动过程比喻成一个大的事件,该事件是由若干个小的事件组成的。例如:

  • org.springframework.boot.context.event.ApplicationStartingEvent

    • 应用启动事件,在应用运行但未进行任何处理时,将发送 ApplicationStartingEvent

  • org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent

    • 环境准备事件,当Environment被使用,且上下文创建之前,将发送 ApplicationEnvironmentPreparedEvent

  • org.springframework.boot.context.event.ApplicationContextInitializedEvent

    • 上下文初始化事件

  • org.springframework.boot.context.event.ApplicationPreparedEvent

    • 应用准备事件,在开始刷新之前,bean定义被加载之后发送 ApplicationPreparedEvent

  • org.springframework.context.event.ContextRefreshedEvent

    • 上下文刷新事件

  • org.springframework.boot.context.event.ApplicationStartedEvent

    • 应用启动完成事件,在上下文刷新之后且所有的应用和命令行运行器被调用之前发送 ApplicationStartedEvent

  • org.springframework.boot.context.event.ApplicationReadyEvent

    • 应用准备就绪事件,在应用程序和命令行运行器被调用之后,将发出 ApplicationReadyEvent,用于通知应用已经准备处理请求

  • org.springframework.context.event.ContextClosedEvent(上下文关闭事件,对应容器关闭)

除了系统内置的事件处理,用户还可以根据需要自定义开发当前事件触发时要做的其他动作。

//设定监听器,在应用启动开始事件时进行功能追加
public class MyListener implements ApplicationListener {
    public void onApplicationEvent(ApplicationStartingEvent event) {
		//自定义事件处理逻辑
    }
}

总结

  1. springboot启动流程是先初始化容器需要的各种配置,并加载成各种对象,初始化容器时读取这些对象,创建容器

  2. 整体流程采用事件监听的机制进行过程控制,开发者可以根据需要自行扩展,添加对应的监听器绑定具体事件,就可以在事件触发位置执行开发者的业务代码

你可能感兴趣的:(微服务技术,spring,boot,java,spring)