面试题:springboot的自启动原理

wushaopei wushaopei     2023-04-23     533

关键词:

引言

不论在工作中,亦或是求职面试,Spring Boot 已经成为我们必知必会的技能项。除了比较老旧的政府项目或金融项目依旧使用如 SSM SSH 做单体框架开发项目外,如今的各行各业基于项目的快速开发与发布、迭代更新,都在逐渐替换使用 Spring Boot 框架,而逐步摒弃配置沉重和效率低下的 Spring 启动框架。

使用一门新的技术,立足于对它足够了解的基础上,能够让你更加得心应手的去进行应用、开发。SpringBoot 的精髓 “自动配置原理” 不仅仅是在面试过程中才用的上;在工作中如果能深入理解Spring Boot 的自动配置原理,我们就可以根据自己的需求自定义一个 started 满足开发的需求,让开发更加便捷与高效。

Spring Boot的出现,得益于“习惯优于配置”的理念,没有繁琐的配置、难以集成的内容(大多数流行第三方技术都被集成),这是基于Spring 4.x提供的按条件配置Bean的能力。

本篇博文主要针对三个方面进行分析、讲解;这三个方面分别是:

  1. Spring Boot Spring 的差别与优点?以及与Spring MVC 的关系?
  2. Spring Boot 主启动类的启动机制
  3. Spring Boot 自动配置原理是怎么实现的?
  4. 根据自动配置原理自定义一个 个性的 started ?

1、Spring Boot Spring 的差别与优点?以及与Spring MVC的关系?

1.1 Spring BootSpring MVC 的关系?

Spring MVC 是基于Spring 的MVC框架;

而Spring Boot 是基于Spring 配置的开发工具框架,使用注解更加简洁和适应快速开发。

1.2 Spring Boot 的优势

简单来说:Spring boot 简化了使用 Spring 的难度,简省了繁重的配置,提供了各种启动器,开发者能快速上手。

(1)编码更简单

  1. Spring 框架由于超重量级的 XML,annotation 配置,使得系统变得很笨重,难以维护
  2. SpringBoot 采用约点大于配置的方法,直接引入依赖,即可实现代码的开发

(2)配置更简单

          技术图片技术图片?

Xml文件使用javaConfig代替,XML中bean的创建,使用@bean代替后可以直接注入。

配置文件变少很多,只保留 application.yml

(3)部署更简单

            技术图片技术图片?

(4)监控更简单

  • Spring-boot-start-actuator:
  • 可以查看属性配置
  • 线程工作状态
  • 环境变量
  • JVM性能监控

2、Spring Boot 主启动类的启动机制

关于程序的入口问题:

在使用SSMSSH以及原生的Servlet 作为架构的项目中,项目的入口都不是 main 方法,main 方法更多的是作为 HelloWorld 练习或 测试时使用。其程序的入口基本都是handler 中对外暴露的每个接口:

  @RequestMapping(value = "/listarea",method = RequestMethod.GET)
    private Map<String,Object> listArea()
        HashMap<String, Object> modelMap = new HashMap<>();
        List<Area> list = areaService.getAreaList();
        modelMap.put("areaList",list);
        ArrayList arrayList = new ArrayList();
        return  modelMap;
    
技术图片

外部通过对接口地址的调用,从而实现交互获取数据、进行页面渲染和展示。

但是,在使用了 Spring Boot 后, Main 方法又成为了程序的入口;

使用过springBoot进行项目打包和部署启动的都知道, SpringBoot 打包成 jar 包后,可以不需要部署到Tomcat 上而直接用命令启动,如下:

    java -jar xxx.jar
技术图片

这是因为Spring Boot 的jar 在启动时,程序经过 主启动类中的main 方法进行启动的;主启动类代码如下:

package com.mmall;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication 

	public static void main(String[] args) 
		SpringApplication.run(DemoApplication.class, args);
	

技术图片

在这里为项目创建一个接口,并分别在接口和主启动类main方法上打断点,

技术图片技术图片?

技术图片技术图片?

在Spring Boot 项目中每次调用接口前都会先启动 主启动类,这里对主启动类的 main 方法入口进行分析;

在上图中,主启动类启动后会进入main 方法中,并在方法中 由 SpringApplication 调用 run ()方法,方法中形参传入当前类class 对象以及main 方法的 args  参数。

下一步进入 run() 方法中,

public static ConfigurableApplicationContext run(Object source, String... args) 
        return run(new Object[]source, args);
    
技术图片

技术图片技术图片?

由断点可以知道,在run() 方法中还调用了另一个 run( )方法,再点击进入下一步,

public static ConfigurableApplicationContext run(Object[] sources, String[] args) 
        return (new SpringApplication(sources)).run(args);
    
技术图片

技术图片技术图片?

由截图可知,传入的class 对象被用来创建 SpringApplication实例对象,并由生成的实例调用 run (args) 方法做启动操作,方法形参为 args 参数。

①创建 SpringApplication 实例,进行初始化对象:

new SpringApplication ( Object... sources)

  public SpringApplication(Object... sources) 
        this.bannerMode = Mode.CONSOLE;
        this.logStartupInfo = true;
        this.addCommandLineProperties = true;
        this.headless = true;
        this.registerShutdownHook = true;
        this.additionalProfiles = new HashSet();
        this.initialize(sources);   // 初始化启动类
    
技术图片

在 该构造函数的 方法体中,对当前类进行了初始化, 

   private void initialize(Object[] sources) 
        if (sources != null && sources.length > 0) 
            this.sources.addAll(Arrays.asList(sources));   // 添加启动类 DemoApplication.class 到 set 集合中,以备后用
        

        this.webEnvironment = this.deduceWebEnvironment();  // 重点:检测当前是否为 web 环境
        this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class));
        this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
        this.mainApplicationClass = this.deduceMainApplicationClass();
    
技术图片

技术图片技术图片?

上图中的 webEnvironment 用于检测当前环境是否为 web 环境,如果是,则返回true.

具体如何判断当前环境是否为 web 环境,后面再做分析.

获取初始化器:

技术图片技术图片?

在确定是否为web环境后,会根据 ApplicationContextInitializer.class 获取工厂实例参数从而设置初始化器的参数

随后 用setListeners 设置监听器

技术图片技术图片?

判断并获取主启动类的类型,通过获取jvm运行时的栈(StackTraceElement[])中的多个加载的类,判断类中方法是否符合  main 方法 的类即为当前启动类,

技术图片技术图片?

   到这里,SpringApplication 的初始化过程就执行完毕了;此时的SpringApplication 实例的启动需要调用run() 方法来实现,下一节进行分析;

以下是初始化过程的重要环节的关系图:    

       技术图片技术图片?

判断当前环境是否为 web 环境:

        this.webEnvironment = this.deduceWebEnvironment();  // 重点:检测当前是否为 web 环境
技术图片

我们进入 deduceWebEnvironment()方法去看一看,

    private boolean deduceWebEnvironment() 
        String[] var1 = WEB_ENVIRONMENT_CLASSES;
        int var2 = var1.length;

        for(int var3 = 0; var3 < var2; ++var3) 
            String className = var1[var3];
            if (!ClassUtils.isPresent(className, (ClassLoader)null)) 
                return false;
            
        

        return true;
    
技术图片

由源码可知, deduceWebEnvironment 中对 私有变量 WEB_ENVIRONMENT_CLASSES进行了循环,继续查看它所代表的是什么?

    private static final String[] WEB_ENVIRONMENT_CLASSES = new String[]"javax.servlet.Servlet", "org.springframework.web.context.ConfigurableWebApplicationContext";
技术图片

该私有变量数组有两个参数构成,第一个参数是 “javax.servlet.Servlet”,第二个参数是 “ConfigurableWebApplicationContext”,这里对“ConfigurableWebApplicationContext”进行分析:

技术图片技术图片?

进入ConfigurableWebApplicationContext类中,我们看到该类继承自 WebApplicationContext 和 ConfigurableApplicationContext两个类,再往下看:

技术图片技术图片?

我们看到 WebApplicationContext 继承自 ApplicationContext 类,而我们知道 ApplicationContext 是可以用来创建IOC 容器的对象的,如:

    ApplicatioonContext context = new ClasspathXmlApplicationContext();
技术图片

该ApplicationContext类是所有IOC 容器对象的顶级接口

阶段小结:ConfigurableWebApplicationContext web 环境下可以配置的IOC 容器。

那么拿到该类的全类名有什么用呢?

            if (!ClassUtils.isPresent(className, (ClassLoader)null)) 
                return false;
            
技术图片
    public static boolean isPresent(String className, ClassLoader classLoader) 
        try 
            forName(className, classLoader);
            return true;
         catch (Throwable var3) 
            return false;
        
    
技术图片

deduceWebEnvironment 方法中调用了 isPresent 方而在isPresent方法中有调用 forName ,将全类名 className 作为形参传入 forName中,此处的 classLoader null值。

这里进入 ClassUtils的forName()方法中,代码如下:

public static Class<?> forName(String name, ClassLoader classLoader) throws ClassNotFoundException, LinkageError 
        Assert.notNull(name, "Name must not be null");
        Class<?> clazz = resolvePrimitiveClassName(name);
        if (clazz == null) 
            clazz = (Class)commonClassCache.get(name);
        

        if (clazz != null) 
            return clazz;
         else 
            Class elementClass;
            String elementName;
            if (name.endsWith("[]")) 
                elementName = name.substring(0, name.length() - "[]".length());
                elementClass = forName(elementName, classLoader);
                return Array.newInstance(elementClass, 0).getClass();
             else if (name.startsWith("[L") && name.endsWith(";")) 
                elementName = name.substring("[L".length(), name.length() - 1);
                elementClass = forName(elementName, classLoader);
                return Array.newInstance(elementClass, 0).getClass();
             else if (name.startsWith("[")) 
                elementName = name.substring("[".length());
                elementClass = forName(elementName, classLoader);
                return Array.newInstance(elementClass, 0).getClass();
             else 
                ClassLoader clToUse = classLoader;
                if (classLoader == null) 
                    clToUse = getDefaultClassLoader();
                

                try 
                    return clToUse != null ? clToUse.loadClass(name) : Class.forName(name);
                 catch (ClassNotFoundException var9) 
                    int lastDotIndex = name.lastIndexOf(46);
                    if (lastDotIndex != -1) 
                        String innerClassName = name.substring(0, lastDotIndex) + ‘$‘ + name.substring(lastDotIndex + 1);

                        try 
                            return clToUse != null ? clToUse.loadClass(innerClassName) : Class.forName(innerClassName);
                         catch (ClassNotFoundException var8) 
                            ;
                        
                    

                    throw var9;
                
            
        
    
技术图片

其中的   Class<?> clazz = resolvePrimitiveClassName(name); 根据全类名获取class 字节码文件对象;在获取到的class 对象不为null 的情况下,clazz = (Class)commonClassCache.get(name); 获取常用类缓存中是否也存在该class对象;

如果两次获取的 clazz 都不为null是存在的,那么就直接返回:

        if (clazz != null) 
            return clazz;
技术图片

由此我们可以知道,isPresent()方法的作用,是为了查看当前环境中有没有指定的工程项目的类。

从这里,我们可以做出总结,deduceWebEnvironment () 方法中通过判断 “WEB_ENVIRONMENT_CLASSES”数组中声明的两个类能否在当前环境中找到,如果,说明当前环境为web 环境。

    private static final String[] WEB_ENVIRONMENT_CLASSES = new String[]"javax.servlet.Servlet", "org.springframework.web.context.ConfigurableWebApplicationContext";
技术图片

SpringApplication实例的启动过程:

在获取到SpringApplication的实例并初始化完成后,其启动使用run() 方法实现,

技术图片技术图片?

进入 run(args)方法中,

public ConfigurableApplicationContext run(String... args) 
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        ConfigurableApplicationContext context = null;
        FailureAnalyzers analyzers = null;   // 失败分析器
        this.configureHeadlessProperty();    // 读取property 文件
        SpringApplicationRunListeners listeners = this.getRunListeners(args); // 加载监听器
        listeners.starting();

        try 
            ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);       // 应用参数
            ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments);                   // 配置文件参数(有默认值,可读取自定义配置文件),用于配置当前环境
            Banner printedBanner = this.printBanner(environment);   // banner的自定义机制
            context = this.createApplicationContext();              // 创建可配置的 IOC 容器
            new FailureAnalyzers(context);                          // 传入配置好的 IOC 容器 创建失败分析器对象
            this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);                              // 传入失败分析器,环境参数、监听器、banner 配置等进行刷新
            this.refreshContext(context);                          // 根据ioc 刷新上下文容器,并打印日志 ,此时TOMCAT / JETTY 容器会被启动
            this.afterRefresh(context, applicationArguments);      // 映射 dispatcherServlet 等
            listeners.finished(context, (Throwable)null);          // 监听IOC容器上下文
            stopWatch.stop();                                       // 计时结束
            if (this.logStartupInfo) 
                (new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), stopWatch);
             // 停止打印日志

            return context;        // 返回启动成功的结果,以及返回启动消耗的时间
         catch (Throwable var9) 
            this.handleRunFailure(context, listeners, (FailureAnalyzers)analyzers, var9);
            throw new IllegalStateException(var9);
        
    
技术图片

这里先从  stopWatch 开始说,在run() 方法中,一开始创建了 StopWatch的对象实例,并调用 start() 进行启动,

         StopWatch stopWatch = new StopWatch();
         stopWatch.start();
    
         。。。。
         stopWatch.stop();
技术图片

这里的 stopWatch 是一个观察用的表,springboot项目启动后返回的启动花费了多少秒、分的时间就是由 stopWatch() 来提供的。

技术图片技术图片?

总结: 在public ConfigurableApplicationContext run(String... args)方法中,最终返回的是自定义配置的IOC容器。

到这里,SpringBoot 的主启动类 Main 方法的初始化和启动过程就结束了。

包扫描;

我们知道 Spring 中IOC通过 依赖注入和控制反转将被调用者Bean的创建交给Spring 代理生成,并注入到IOC容器中;而在SpringBoot中, 接口的调用必须基于SpringBoot 主启动类的同级包或子包范围内;

规范化的开发中需要指定好 Handler dao 层的包位置,分别指定的包扫描方式为:

控制器与业务层 :

自动扫描:  主要针对 @Controller、@Service 标注的类,该类所在包必须为主启动类所在包的子包;

手动指定扫描:在主启动类上使用@CompomentScan(“com.mmall.handler”) 注解

持久层:

方式一:使用@MapperScan

          @MapperScan("com.mmall.demo.dao")标记在主启动类上
技术图片

方式二: 使用@Mapper

     @Mapper
     public interface EmpMapper

          ......................

      
技术图片

面试题:springboot的自动配置原理及定制starter(代码片段)

3、SpringBoot的自动配置原理packagecom.mmall;importorg.springframework.boot.SpringApplication;importorg.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplicationpublicclassDemoApplication publicstaticvoidmain(String[]args) SpringApplication.run(DemoApplication.class,arg... 查看详情

2022最新超详细spring全家桶面试题(待更新ing)

...pring全家桶面试题完整版0.参考视频1.谈谈对Spring的理解77.SpringBoot的自动配置原理81.Springboot启动原理(启动过程)86.Springboot默认的日志实现框架,如何切换其他日志框架?101.@Component和@ComponentScan的联系102.... 查看详情

面试高频题:springboot自动装配的原理你能说出来吗?(代码片段)

...,面试中被问到了这样一个问题“看你项目中用到了springboot,你说下springboot的自动配置是怎么实现的?”这应该是一个springboot里面最最常见的一个面试题了。下面我们就来带着这个问题一起解剖下springBoot的自动配... 查看详情

springboot高频面试题:springboot执行原理(代码片段)

之前一篇文章SpringBoot快速入门文章中,我们已经体会到SpringBoot的神器,不再像之前Spring那样需要繁琐的XML,甚至几秒钟就能搭建出Spring的项目骨架。接下来我们简单分析SpringBoot的基本原理,让我们揭开它神秘的面纱吧。1@SpringB... 查看详情

2022最新java面试宝典——springboot面试题(44道含答案)(代码片段)

目录1.什么是SpringBoot?2.为什么要用SpringBoot3.SpringBoot与SpringCloud区别4.SpringBoot有哪些优点?5.SpringBoot的核心注解是哪个?它主要由哪几个注解组成的?6.SpringBoot支持哪些日志框架?推荐和默认的日志框架是哪个... 查看详情

springboot进阶:原理实战与面试题分析读后感

❤️作者主页:小虚竹❤️作者简介:大家好,我是小虚竹。Java领域优质创作者🏆,CSDN博客专家🏆,华为云享专家🏆,掘金年度人气作者🏆,阿里云专家博主🏆❤️技术活,该赏... 查看详情

springboot进阶:原理实战与面试题分析读后感

❤️作者主页:小虚竹❤️作者简介:大家好,我是小虚竹。Java领域优质创作者🏆,CSDN博客专家🏆,华为云享专家🏆,掘金年度人气作者🏆,阿里云专家博主🏆❤️技术活,该赏... 查看详情

springboot进阶:原理实战与面试题分析|文末送书5本

在当下的互联网应用中,业务体系日益复杂,业务功能也在不断地变化。以典型的电商类应用为例,其背后的业务功能复杂度以及快速迭代要求的开发速度,与5年前的同类业务系统相比,面临着诸多新的挑战... 查看详情

第十次java实习生面试题打卡(代码片段)

springboot自动配置原理是什么?1、在springboot的启动过程中,有一个步骤是创建上下文,如果不记得可以看下面的代码:publicConfigurableApplicationContextrun(String...args) StopWatchstopWatch=newStopWatch(); stopWatch. 查看详情

微服务之springboot面试题(代码片段)

1,什么是SpringBoot?SpringBoot是Spring开源组织下的子项目,是Spring组件一站式解决方案,主要是简化了使用Spring的难度,简省了繁重的配置,提供了各种启动器,开发者能快速上手。2,为什么要用SpringBoot?SpringBoot优点非常多,如... 查看详情

微服务之springboot面试题(代码片段)

1,什么是SpringBoot?SpringBoot是Spring开源组织下的子项目,是Spring组件一站式解决方案,主要是简化了使用Spring的难度,简省了繁重的配置,提供了各种启动器,开发者能快速上手。2,为什么要用SpringBoot?SpringBoot优点非常多,如... 查看详情

springboot面试题

1、什么是SpringBoot?多年来,随着新功能的增加,spring变得越来越复杂。我们就会看到可以在我们的应用程序中使用的所有Spring项目的不同功能。如果必须启动一个新的Spring项目,我们必须添加构建路径或添加Maven... 查看详情

springboot面试题(代码片段)

SpringBoot的自动配置是如何实现的?这个是因为@SpringBootApplication 注解的原因,我们知道 @SpringBootApplication 看作是 @Configuration、@EnableAutoConfiguration、@ComponentScan 注解的集合。@EnableAutoConfiguration:启用SpringBoot的自动... 查看详情

springboot面试题

什么是SpringBoot?  查看详情

20道springboot面试题(代码片段)

...技术栈(id:javastack)面试了一些人,简历上都说自己熟悉SpringBoot,或者说正在学习SpringBoot,一问他们时,都只停留在简单的使用阶段,很多东西都不清楚,也让我对面试者大失所望。下面,我给大家总结下有哪些SpringBoot的面试... 查看详情

2022年度最常见的springboot面试题附解析

SpringBoot面试题是在java面试中必考的一个题目,今天小编为大家整理了2022年最新的SpringBoot的面试题目,这些都是必考的面试题,大家要好好看哦!1、问:SpringBoot是什么?答:spring随着新功能的增加... 查看详情

java面试题-框架篇(代码片段)

...依赖的原理4.Spring事务失效5.SpringMVC执行流程6.Spring注解7.SpringBoot自动配置原理8.Spring中的设计模式1.Springrefresh流程要求掌握refresh的12个步骤Springrefresh概述refresh是Abs 查看详情

大厂面试题阿里字节最新springboot面试题15问(代码片段)

做Java开发,没有人敢小觑SpringBoot的重要性,现在出去面试,无论多小的公司or项目,都要跟你扯一扯SpringBoot,扯一扯微服务,不会?没用过?Sorry,我们不合适!今天小编就给大家整理了15... 查看详情