• SpringBoot-入门原理分析


    随便给最近想逐步整理的文章起了个皮皮虾名:SpringBoot起飞之路

    这是第二篇,关于前一篇入门涉及到的一些基本原理讲解,有兴趣的朋友可以去了解一下前一篇

    SpringBoot起飞之路-HelloWorld (一)

    说明:

    • 事情实在太多,并且内容同时全放到一起,后来感觉移动端篇幅太长,阅读体验太差了,就打算分成几篇来发
    • 才疏学浅,就会点浅薄的知识,大家权当一篇工具文来看啦,不喜勿愤哈 ~

    (一) 起步依赖原理分析

    (1) spring-boot-starter-parent

    在前一篇的入门程序编写时,我们自己通过 Maven 创建一个 SpringBoot 的项目的时候,我们需要在前面使用 <parent></parent> 标签对引入一个 spring-boot-starter-parent

    这就是 SpringBoot 的父级依赖,它就是一个特殊的 starter ,可以用来提供我们所需 Maven 的一些依赖,同时管理项目的资源过滤

    简单看一下,首先按下 ctrl 键点击 spring-boot-starter-parent,

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.7.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    

    进去之后,就是一些资源引入的内容,还有一些插件,接着又看到了一个父级标签,我们继续点进去看一看

    <parent>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-dependencies</artifactId>
      <version>2.2.7.RELEASE</version>
      <relativePath>../../spring-boot-dependencies</relativePath>
    </parent>
    

    到此为止,就看不到别的父级标签了,来观察一下这个文件,纵观一下,这里就是管理 SpringBoot 应用里面所有依赖版本的地方

    • properties 标签中配置了一些版本信息
    • dependencys 标签内就是各种依赖
    • plugins 标签内就是一些涉及的插件

    从前在 Spring 中的开发,我们总会考虑不同依赖版本之间的兼容冲突问题,而在这里,SpringBoot 就会帮我们根据我们选定的 SpringBoot 版本,选择出最适合的依赖们,这一点就舒服了~

     <properties>
         ......
        <activemq.version>5.15.12</activemq.version>
        <commons-pool.version>1.6</commons-pool.version>
        <jackson.version>2.10.4</jackson.version>
        <javax-mail.version>1.6.2</javax-mail.version>
        <log4j2.version>2.12.1</log4j2.version>
        <logback.version>1.2.3</logback.version>
        <mssql-jdbc.version>7.4.1.jre8</mssql-jdbc.version>
        <mysql.version>8.0.20</mysql.version>
         ......
    </properties>
    
    <dependencyManagement>
        <dependencies>
          <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot</artifactId>
            <version>2.2.7.RELEASE</version>
          </dependency>
          <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-test</artifactId>
            <version>2.2.7.RELEASE</version>
          </dependency>
          ......
          <dependency>
            <groupId>commons-pool</groupId>
            <artifactId>commons-pool</artifactId>
            <version>${commons-pool.version}</version>
          </dependency>
          .....      
        </dependencies>
    </dependencyManagement>
    
    <build>
    	<pluginManagement>
          <plugins>
            ......
            <plugin>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-maven-plugin</artifactId>
              <version>2.2.7.RELEASE</version>
            </plugin>
    		......
            <plugin>
              <artifactId>maven-install-plugin</artifactId>
              <version>${maven-install-plugin.version}</version>
            </plugin>
    		......
    	    <plugin>
              <artifactId>maven-jar-plugin</artifactId>
              <version>${maven-jar-plugin.version}</version>
            </plugin>
            ......
          </plugins>
    	</pluginManagement>
    </build>
    

    (2) spring-boot-starter-web

    接着我们再次回到 pom.xml 中,还有一个与我们过去开发不同的东西,那就是 spring-boot-starter-web,在前面第一篇的文章中初始化组件时,我们就选择了 Spring Web 组件,自然这个依赖就是用来实现 Web 功能的

    进入 spring-boot-starter-web

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    

    下面节选了一部分 xml 配置(依赖是全的),看到这里,也就能解释为什么 spring-boot-starter-web 这个依赖可以实现 Web 的开发,本质上就是对支持 Web 开发的 spring-webmvc、spring-webmvc等坐标进行了 “整合” 也就是将依赖传递了

    <?xml version="1.0" encoding="UTF-8"?>
    <project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
      <modelVersion>4.0.0</modelVersion>
      <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starters</artifactId>
        <version>2.2.7.RELEASE</version>
      </parent>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
      <version>2.2.7.RELEASE</version>
      <name>Spring Boot Web Starter</name>
    	......
    <dependencies>
        <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter</artifactId>
          <version>2.2.7.RELEASE</version>
          <scope>compile</scope>
        </dependency>
        <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-json</artifactId>
          <version>2.2.7.RELEASE</version>
          <scope>compile</scope>
        </dependency>
        <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-tomcat</artifactId>
          <version>2.2.7.RELEASE</version>
          <scope>compile</scope>
        </dependency>
        <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-validation</artifactId>
          <version>2.2.7.RELEASE</version>
          <scope>compile</scope>
          <exclusions>
            <exclusion>
              <artifactId>tomcat-embed-el</artifactId>
              <groupId>org.apache.tomcat.embed</groupId>
            </exclusion>
          </exclusions>
        </dependency>
        <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-web</artifactId>
          <version>5.2.6.RELEASE</version>
          <scope>compile</scope>
        </dependency>
        <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-webmvc</artifactId>
          <version>5.2.6.RELEASE</version>
          <scope>compile</scope>
        </dependency>
      </dependencies>
    </project>
    

    (3) 这就是启动器

    我们从一个入门程序讨论到了 spring-boot-starter-web,在 SpringBoot 中所有的功能场景都被抽取出来,定义成了很多很多启动器,通过这些启动器,开发人员就能按照自己的需求,选择不同的启动器进行配合,极为快速的就能搭建起一个想要的项目运行框架环境,启动器命名规则如下:

    • springboot-boot-starter-xxx

    在举几个例子,体验一下,以后的文章,在用的时候,会给出具体的说明

    spring-boot-starter-test:支持常规的测试依赖,包括JUnit、Hamcrest、Mockito以及spring-test模块

    spring-boot-starter-web:支持全栈式 Web 开发,包括 Tomcat 和 spring-webmvc

    spring-boot-starter-redis:支持 Redis 键值存储数据库

    (二) SpringBoot 启动类分析

    除了依赖,还有一个与我们从前 Spring 的开发不一样的点就是,SpringBoot 多了一个启动类,在入门程序的编写的时候,我只提到,这个启动类是必须存在的,看似简单的一个类,实则也很玄妙,下面我们来一起分析一下

    package cn.ideal;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    @SpringBootApplication
    public class SpringbootApplication {
        public static void main(String[] args) {
            SpringApplication.run(Springboot02QuickstartApplication.class, args);
        }    
    }
    

    (1) @SpringbootApplication 注解

    首先最显眼的也就是在类上的 @SpringbootApplication 注解了,我们 ctrl 点击进入

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Inherited
    @SpringBootConfiguration
    @EnableAutoConfiguration
    @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
          @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
    public @interface SpringBootApplication {
    	......
    }
    

    从上面的代码可以看到,这里又加注着很多注解,但是其中比较核心的有三个:@SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan

    为什么这么说呢,大家可以把这三个注解加到我们之前的启动类上,然后执行一下,简单的看呢,其实也是可以达到之前的效果的,下面,我们就主要讲解一下这三个注解

    package cn.ideal;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.SpringBootConfiguration;
    import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
    import org.springframework.context.annotation.ComponentScan;
    
    @SpringBootConfiguration
    @EnableAutoConfiguration
    @ComponentScan
    public class SpringbootApplication {
        public static void main(String[] args) {
            SpringApplication.run(Springboot02QuickstartApplication.class, args);
        }
    }
    

    A:@SpringBootConfiguration

    点击进去,我们可以看到 @SpringBootConfiguration 注解本质上,还是用了 Spring 的 @Configuration 注解

    @Configuration
    public @interface SpringBootConfiguration {...}
    

    在点 @Configuration 进去还可以看到下述内容

    @Component
    public @interface Configuration {...}
    

    这两个组件大家应该还是很熟悉的

    • @Configuration:指定当前类是 spring 的一个配置类,相当于 XML中的 applicationContext.xml 文件
    • @Component :说明这个类也是交给Spring管理,也能看出启动类本身也是Spring中的一个组件而已,用来启动应用

    B:@ComponentScan

    @ComponentScan的功能其实是自动扫描并加载符合条件的组件(比如 @Component 和 @Controller 等)或者bean定义,并把这些bean定义加载到IoC容器中,在 XML 中相当于:

    <!--开启扫描-->
    <context:component-scan base-package="cn.ideal"></context:component-scan>
    

    我们可以通过 basePackages 属性来指定 @ComponentScan 自动扫描的范围,且和这个注解中value属性的作用是一致

    如果不指定,则默认Spring框架实现会从声明 @ComponentScan 所在类的 package 进行扫描,由于默认是不指定的,所以这也是我们把启动类放在与 controller service 等包同级的位置下

    我们还可以看到指定了两个 Filter ,简单提一下他们两个

    @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
          @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
    
    • TypeExcludeFilter, 方便用户实现自定义的Filter,用于Spring Test中
    • AutoConfigurationExcludeFilter, 忽略@ Configuration和 EnableAutoConfiguration
      这样就可以使得,SpringBoot不会错误扫描到自动装配类

    C:@EnableAutoConfiguration

    这个注解是极其重要的,顾名思义可以看到,它的格式为 @EnableXxxxxx,还有一些类似的注解 @EnableMBeanExport、@EnableScheduling 等等,他们也是相同的开头,拿一个举例

    • @EnableScheduling:通过 @Import 将Spring 调度框架相关的bean定义都加载到 IOC容器

    @EnableAutoConfiguration 也是这样的,他借助 @Import ,就可以把所有符合自动配置要求的 bean 定义加载到 IOC 容器

    所以:这个注解的作用就是自动配置,代替了我们很多的配置操作,在 @SpringBootApplication 上加这个注解,也就代表 SpringBoot 自动配置的功能开启了

    我们点进去具体看一下

    首先同样可以看到很多注解,我们聚焦到这两个

    @AutoConfigurationPackage
    @Import(AutoConfigurationImportSelector.class)

    ① @AutoConfigurationPackage

    我们可以点进去 @AutoConfigurationPackag 这个注解看一下,其下还有一个注解很重要 @Import(AutoConfigurationPackages.Registrar.class)

    • @Import 就是用来给容器中导入某个组件类
    • @Import(AutoConfigurationPackages.Registrar.class) 就是将将Registrar这个组件类导入到 Spring 容器中

    进而看一下 Registrar ,其中有一个 registerBeanDefinitions 方法就是导入组件类的实现,作用就是将注解标注的元信息传进去,然后得到对应的包名

    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
       register(registry, new PackageImport(metadata).getPackageName());
    }
    

    总结一下:AutoConfigurationPackage注解的作用是将 添加该注解的类所在的package 作为 自动配置package 进行管理

    根据我们启动类的所在包位置可得,使用 @AutoConfigurationPackage 注解可以将主程序类所在包及所有子包下的组件到扫描到 Spring 容器中

    ② @Import(AutoConfigurationImportSelector.class)

    回到 EnableAutoConfiguration 下,除了 @AutoConfigurationPackage,最重要的来了,@Import(AutoConfigurationImportSelector.class),@Import 就不说了,那么 AutoConfigurationImportSelector 究竟做了什么呢?我们进入 AutoConfigurationImportSelector

    由于 AutoConfigurationImportSelector 字面意思叫做 自动配置导入选择器,所我们来找一下关于获得配置的相关代码

    首先在 getAutoConfigurationEntry 方法中看到这么一行代码:

    // 获取全部的配置
    List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
    

    这些配置是通过 getCandidateConfigurations 获取到的,所我们继续跳转过去

    找到 getCandidateConfigurations ,这个方法,其中又调用了 SpringFactoriesLoader 类的静态方法 loadFactoryNames(注意引入的第一个参数)

    // 获得候选的配置
    protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
       List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
             getBeanClassLoader());
       Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
             + "are using a custom packaging, make sure that file is correct.");
       return configurations;
    }
    
    // 在上面被当做参数调用了
    // 返回的就是最开始启动自动导入配置文件的注解类;EnableAutoConfiguration
    protected Class<?> getSpringFactoriesLoaderFactoryClass() {
    	return EnableAutoConfiguration.class;
    }
    

    继续深入这个方法,我节选了一部分重要的,关键部分我都加了注释

    public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
    
    public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
       // 前面传入的参数为EnableAutoConfiguration.class
       // 所以factoryClassName为org.springframework.boot.autoconfigure.EnableAutoConfiguration
       String factoryTypeName = factoryType.getName();
       // 把 factoryClassName传入,返回的是所有spring.factories文件中key为org.springframework.boot.autoconfigure.EnableAutoConfiguration的类路径
       return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
    }
    
    private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
       MultiValueMap<String, String> result = cache.get(classLoader);
       if (result != null) {
          return result;
       }
    
       try {
          // 找到所有的 "META-INF/spring.factories" 定义在上方
          Enumeration<URL> urls = (classLoader != null ?
                classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
                ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
          result = new LinkedMultiValueMap<>();
          while (urls.hasMoreElements()) {
             URL url = urls.nextElement();
             UrlResource resource = new UrlResource(url);
              // 读取文件内容,properties类似于HashMap,包含了属性的key和value
             Properties properties = PropertiesLoaderUtils.loadProperties(resource);
             for (Map.Entry<?, ?> entry : properties.entrySet()) {
                // 属性文件中用','分割多个value
                String factoryTypeName = ((String) entry.getKey()).trim();
                for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
                   result.add(factoryTypeName, factoryImplementationName.trim());
                }
             }
          }
          cache.put(classLoader, result);
          return result;
       }
       catch (IOException ex) {
          throw new IllegalArgumentException("Unable to load factories from location [" +
                FACTORIES_RESOURCE_LOCATION + "]", ex);
       }
    }
    

    到这里了,再怎么找也该知道去找 spring.factories 了,那么这个 META-INF/spring.factories 文件从哪里找起呢?一般都是默认当前文件下的,所以顺着上面的代码分析找到了源头 package org.springframework.boot.autoconfigure 下

    点进去一看,好多以 AutoConfiguration 结尾的配置内容,这就是自动配置的根本,挑选几个简单列出来

    ......
    org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration,
    org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration,
    org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration,
    org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration,
    org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration,
    org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,
    ......
    

    随便找几个点进去看一下,可以看到他们每个都是JavaConfig配置类,同时注入了一些Bean,所以可以看出这个 spring.factories 文件就是我们配置的根源内容了

    @EnableAutoConfiguration 注解总结:

    在 启动类中的 @SpringbootApplication 注解 下引入 @EnableAutoConfiguration 注解,它提供了一种配置查找的功能支持,也就是说,@EnableAutoConfiguration的完整类名org.springframework.boot.autoconfigure.EnableAutoConfiguration 作为查找的Key,获取对应的一组@Configuration类

    这些配置类具体是如何做的我们这里不深究,但是要补充一个点,我们首先进入下面这个 Configuration

    org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration,
    

    @Configuration(proxyBeanMethods = false) 自然代表这是一个配置类,这也与我们前面的分析一致

    要说的是这个注解:@EnableConfigurationProperties(ServerProperties.class)

    看名字来说,它是和 Properties 配置有关的,继续点入 ServerProperties

    @ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
    public class ServerProperties {
    
    	/**
    	 * Server HTTP port.
    	 */
    	private Integer port;
    
    	/**
    	 * Network address to which the server should bind.
    	 */
    	private InetAddress address
    	
    	......
    }
    

    又看到这样一条注解:@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)

    可以看到,这里指定了一个前缀 server,下面还定义了很多东西,例如这里定义了端口还有地址等等

    那么,为什么要说这个呢?有什么用呢?

    这就与我准备下一篇写的文章有关了,也就是配置相关的,我们可以在这里先体验一下

    在项目 resources 的 application.properties 下,我们可以添加这样一条配置:

    server.port=9090
    

    就这样一行配置,就把端口号修改好了,是不是很神奇

    这就是我们刚才想说的,实际上这里配置的 server.port 其中 server 就是那个 @ConfigurationProperties(prefix = "server", ignoreUnknownFields = true) 配置的前缀,port 就是下面定义的端口,也就是SpringBoot会将配置文件中以server开始的属性映射到该类的字段中

    (2) SpringApplication.run

    继续回到启动类,下面提一下 SpringApplication.run 这个内容

    @SpringBootApplication
    public class SpringbootApplication {
        public static void main(String[] args) {
            SpringApplication.run(SpringbootApplication.class, args);
        }
    }
    

    这部分内容设计的内容比较多,放到以后专门整理源码的时候分析,入门简单了解一下作用就行了

    • 先判断一下应用是普通项目还是Web项目

    • 加载初始化器和监听器, 并且设置到initializers属性中

    • 推断并设置main方法的定义类,找到运行的主类

    (三) 结尾

    如果文章中有什么不足,欢迎大家留言交流,感谢朋友们的支持!

    如果能帮到你的话,那就来关注我吧!如果您更喜欢微信文章的阅读方式,可以关注我的公众号

    在这里的我们素不相识,却都在为了自己的梦而努力 ❤

    一个坚持推送原创开发技术文章的公众号:理想二旬不止

  • 相关阅读:
    Linux工具之man手册彩色页设置
    使用bakefile编译C工程代码
    学会使用简单的 MySQL 常用操作
    Mysql数据库的通用安装方法
    从Mysql数据库中导入导出表结构
    CentOS下安装MySQL数据库
    lua函数调用
    innodb记录延迟删除对于其它DB操作的影响
    从apache派生cgi工作路径看软链接
    两台主机互为网关是否会像打乒乓球一样一直互发
  • 原文地址:https://www.cnblogs.com/ideal-20/p/12880186.html
Copyright © 2020-2023  润新知