【Spring Boot】夺名连环问(持续更新ing)

Spring的了解与特性

  • 简单介绍:快速开发Spring项目的脚手架。简化Spring应用的初始搭建以及开发过程。

  • 特性

    • 提供了很多内置的Starter结合自动配置,对主流框架的无配置集成、开箱即用。即不需要自己去引入很多依赖

    • 并且管理了常用的第三方依赖的版本,减少了版本冲突的问题

    • 简化开发,采用JavaConfig的方式可以使用0xml方式进行开发。

    • 内置Web容器无需依赖外部Web服务器,省略了Web.xml,直接运行就使Web应用。

    • 提供了监控功能,可以监控应用程序的运行状态、内存、线程池、HTTP请求统计等。

Spring与SpringBoot的关系

  • 两者都是Spring生态的产品

  • 前者是容器框架,后者是一个快速开发Spring的脚手架(真正意义上不算新的框架)

核心注解

  • @SpringBootApplication

    • @SpringBootConfiguration(标记为配置类的注解)

      • @Configuration

    • @EnableAutoConfiguration:向Spring容器中导入了一个Selector,用来加载ClassPath下SpringFactories中所定义的配置类,将这些自动加载为配置Bean。

    • @Conditional(待补充)

自动配置原理

  • 通过@SpringBootApplication中的@EnableAutoConfiguration

  • @EnableAutoConfiguration引入了@Import

  • 在Spring容器启动时会加载IoC容器解析@Import注解

  • @Import导入了@DeferredImportSeletor注解,会使SpringBoot的自动配置类的顺序在最后,方便扩展的覆盖

  • 读取META-INF/spring.factories文件

  • 过滤出所有的AutoConfigurationClass类型的类

  • 最后通过@Condition排除无效的自动配置类

为什么SpringBoot的jar可以直接运行

  • 部署SpringBoot的插件才能将package后的jar包使用:java -jar packageName运行

    • 也就是在pom.xml中要有中的的spring-boot-maven-plugin。如果没有这个插件在运行命令的时候会报没有主清单属性。

  • 使用插件打包后生成的其实是一个Fat jar(也就是jar包中还有jar包),包含了应用依赖的jar包和Spring Boot Loader相关的类。 

  • java - jar会去找jar中的MANIFEST.MF文件,在那里面找到真正的启动类(Main-Class)。 【Spring Boot】夺名连环问(持续更新ing)_第1张图片

  • Fat jar的启动Main函数是JarLauncher,它负责创建一个LaunchedURLClassLoader加载boot-lib下面的jar,并以一个新的线程启动应用Main函数(其实就是找到MANIFEST中的Start-Class)。

  • 当然,为了去验证此说法,就需要引入依赖从而去看JarLauncher中的源码,引入此依赖后再package就可以查看源码了。

    
        org.springframework.boot
        spring-boot-loader
    

启动原理

  • 运行主启动类的main方法,会初始化SpringApplication,从spring.factories读取listener ApplicationContextInitializer。此primarySources就是当前SpringBoot项目的class文件。

  • 在初始化SpringApplication中对一些属性的初始化,比如开启日志记录、关闭懒加载等等。不同版本的Boot在初始化的时候不一样,主要是最下面的七行。以下讲解最下面五行的作用。就不根据源码展示了。 【Spring Boot】夺名连环问(持续更新ing)_第2张图片

    • 将启动类primarySources放入到名为primartSrouces的LinkedHashSet集合中

    • 推算当前Web应用类型webApplicationType

    • 读取ApplicationContextInitializer初始化器

    • 读取ApplicationListener监听器

    • 将Main方法所在的类放入mainApplicationClass中

  • 接下来就是运行run()方法了。代码太多就不展示了。主要是做了以下的操作:

    • 从上面读取的ApplicationListener监听器去读取SpringApplicationRunListener监听器运行器

    • 发布ApplicationStartingEvent事件

    • 封装命令行参数ApplicationArguments

    • 读取环境配置信息

      • 根据webApplicationType创建环境变量对象

      • 配置环境变量

      • 将现有环境信息设置为@ConfigurationProperties的数据源 并且放在第一位

      • 发布ApplicationEnvironmentPreparedEvent事件

      • 将spring.main的配置绑定到SpringApplication属性上

      • 将现有的环境信息设置为@ConfigurationProperties的数据源 更新放在第一位

    • 设置忽略bean.spring.beaninfo.ignore

    • 打印Banner

    • 实例化Spring上下文 AnnotationConfigServletWebServerApplicationContext

    • 初始化失败分析器

    • 准备上下文:将启动类作为配置类进行读取 -> 将配置注册为BeanDefinition

      • 将当前环境信息设置到context

      • 调用ApplicationContextInitializer

      • 发布ApplicationContextInitializerdEvent事件

      • 打印启动信息和profile.active(测试/生产/开发)信息

      • 将applicationArguments、printedBanner注册为Bean

      • 设置不允许同名Bean

      • 设置是否懒加载Bean

      • 读取启动类为BeanDefinition

      • 将SpringBoot的监听器添加到context发布 ApplicationPreparedEvent事件

    • 加载IoC容器(refresh()方法)其中的十二个步骤。具体干了什么可以去看我另外几篇源码解读的博文。主要两个方法

      • invokeBeanFactoryPostProcessor() -> 解析@Import 加载所有的自动配置类

      • onRefresh() 创建内置的Servlet容器

    • 待扩展(刷新后的处理)

    • 发布ApplicationStartedEvent事件
    • 发布ApplicationReadyEvent事件
    • 如果启动异常会发送ApplicationFailedEvent事件

public ConfigurableApplicationContext run(String... args) {
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    ConfigurableApplicationContext context = null;
    Collection exceptionReporters = new ArrayList();
    this.configureHeadlessProperty();
    // 从上面读取的ApplicationListener监听器去读取SpringApplicationRunListener监听器运行器
    SpringApplicationRunListeners listeners = this.getRunListeners(args);
    // 发布ApplicationStartingEvent事件
    listeners.starting();
​
    Collection exceptionReporters;
    try {
        // 封装命令行参数ApplicationArguments
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
        // 读取环境配置信息
        ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments);
        // 设置忽略bean.spring.beaninfo.ignore       
        this.configureIgnoreBeanInfo(environment);
        // 打印Banner
        Banner printedBanner = this.printBanner(environment);
        // 实例化Spring上下文 AnnotationConfigServletWebServerApplicationContext
        context = this.createApplicationContext();
        // 初始化失败分析器
        exceptionReporters = this.getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[]{ConfigurableApplicationContext.class}, context);
        // 准备上下文:将启动类作为配置类进行读取 -> 将配置注册为BeanDefinition
        this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);
        // 加载IoC容器
        this.refreshContext(context);
        // 待扩展(刷新后的处理)
        this.afterRefresh(context, applicationArguments);
        stopWatch.stop();
        if (this.logStartupInfo) {
            (new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), stopWatch);
        }
​
        // 发布ApplicationStartedEvent事件
        listeners.started(context);
        this.callRunners(context, applicationArguments);
    } catch (Throwable var10) {
        this.handleRunFailure(context, var10, exceptionReporters, listeners);
        throw new IllegalStateException(var10);
    }
​
    try {
        // 发布ApplicationReadyEvent事件
        listeners.running(context);
        return context;
    } catch (Throwable var9) {
        this.handleRunFailure(context, var9, exceptionReporters, (SpringApplicationRunListeners)null);
        throw new IllegalStateException(var9);
    }
}

内置Tomcat启动原理 

  • 当依赖spring-boot-starter-web依赖时会在SpringBoot中添加:ServletWebServerFactoryAutoConfiguration(servlet容器自动配置类)

  • 该自动配置类通过@Import导入了可用的(通过@ConditionalOnClass判断决定使用哪一个)的Web容器工厂(默认Tomcat)

  • 在内嵌的Tomcat类中配置了一个TomcatServletWebServerFactory的Bean(Web容器工厂)

  • 它会在SpringBoot启动时加载IoC容器、在onRefrush()方法中创建内嵌的Tomcat并启动

外部Tomcat启动原理

  • SpringBoot启动后会加载SpringServletContainerInitializer类,此类会去加载实现了WebApplicationInitializer接口的类,而其中SpringBootServletInitializer这个类就实现了这个接口,而需要继承这个类去传入当前应用的启动类。

WebApplicationContext rootAppContext = this.createRootApplicationContext(servletContext);
  • 想要使用外部的Tomcat,就需要排除内部的Tomcat。


    org.springframework.boot
    spring-boot-starter-web
    
    	
        	org.springframework.boot
            spring-boot-starter-tomcat
        
    
  • 创建一个配置类去继承SpringBootServletInitializer去重写configure()方法,将当前模块的启动类传入进去 。

public class TomcatConfig extends SpringBootServletInitializer {
    
    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
        return builder.sources(StudySpringApplication.class);
    }
}

如何自定义Starter

  • 首先知道什么是Starter:Starter又名启动器。众所周知SpringBoot是简化了企业级Spring框架的开发,采用了约定优于配置的理念,而这个理念的实现就是用Starter组件。其会把对应功能的jar包导入进来使得开发者无需在意版本冲突问题,而只需在乎业务逻辑即可,这是作为引入依赖方面的简化配置。而此处要介绍的Starter就是代替Spring的内部组件,使用自定义的组件去完成启动。步骤如下

    • 新建resources/META-INF/spring.factories。这是因为Spring Boot在启动的时候会去读取ClassPath下的这个文件信息。

    • 在spring.factories中去用伪SPI去扫描我们自定义的Starter启动器组件

【Spring Boot】夺名连环问(持续更新ing)_第3张图片

读取配置文件的原理以及加载顺序

  • 通过事件监听的方式读取配置文件,而读取配置文件的类则是ConfigFileApplicationListener

【Spring Boot】夺名连环问(持续更新ing)_第4张图片

【Spring Boot】夺名连环问(持续更新ing)_第5张图片

读取配置文件的方式

  • 属性上使用注解@Value("${xxx.yyy}"),需要注意三点:

    • 当前类是交给IoC容器管理的Bean,否则@Value注解不生效。

    • 当前属性不能是static或者final修饰。

    • 如果配置文件中没有对应的xxx: yyy的话会报错,但是可以在@Value中给默认值比如@Value("${xxx.yyy:}"),这样默认为""

    • 缺点是一个属性对应一个,如果需要读取多个属性的话,效率低下。

  • 类上使用注解@ConfigurationProperties(prefix = "xxx")去指定配置文件中的前缀去读取多个,然后属性采用同名方式去进行批量匹配。相比于上面那种而言,如果遇到大量的配置信息,则效率会更高。

@ConfigurationProperties(prefix = "xxx")
public class MyYamlConfig {
    private String yyy;
}
  • 通过Spring的应用上下文ApplicationContext去获取Environment对象。但相较于直接获取Environment对象,这样显得多此一举,除非除了要使用ApplicationContext获取运行时环境还有其它的作用。毕竟ApplicationContext是Spring应用程序中的中央接口,由于继承了多个组件所以有多个核心的功能。

  • Spring Bean类中去注入IoC管理的Environment对象。可以使用@Resource、@Autowired来注入。然后在代码块中通过此对象的getProperty("xxx.yyy")返回String对象进行使用即可。但是如果不用自动装配的方式(也就是非Spring管理的Bean),就需要此配置类去实现EnvironmentAware接口重写setEnvironment()方法去获取Environment对象。

  • 配置类上获取外部properties后缀配置文件的方式,比如需要读取/resources/gok.properties文件。在配置类上使用注解@PropertySources。然后就可以在属性上使用@Value注解去读取。

@Configuraion
@PropertySources({@propertySource(value = "classpath:gok.properties", encoding = "UTF-8")})
public class MyYamlConfig {
    @Value("${xxx.yyy}")
    private String name;
}
  • 获取/resources/gok.yml文件。在被@Configuration注解的配置类去配置

@Configuration
public class MyYamlConfig {
    @Bean
    public static PropertySourcesPlaceholderConfigurer yamlConfigurer() {
        PropertySourcesPlaceholderConfigurer configurer = new PropertySourcesPlaceholderConfigurer();
        YamlPropertiesFactoryBean yaml = new yamlPropertiesFactoryBean();
        yaml.setResources(new ClassPathResource("gok.yml"));
        configurer.setProperties(Object.requireNonNull(yaml.getObject()));
        return configurer;
    }
}
  • 采用Java原生态的方式(InputStream流)去读取文件

public class MyYamlConfig {
    public void yamlConfigurer() {
        Properties props = new Properties();
        try {
            InputStreamReader reader = new InputStreamReader(
                    this.getClass().getClassLoader().getResourceAsStream("gok.properties"),
                    StandardCharsets.UTF_8);
            props.load(reader);
        } catch (IOException e) {
        }
        String name = props.getProperty("xxx.yyy");
    }
}

解决跨域的方式

  • 首先了解什么是跨域:两个URL的协议/域名/IP/端口不同就会产生跨域。跨域是在前端才会发生的问题,因为浏览器有一个同源策略,这是防止不安全的不同域访问,所以会抛出异常(也就是跨域问题)。

  • Jsoup

    • 前端使用Ajax的jsoup方式,同时后端每个接口的参数都接收一个String callback,并且将其放到JSONObject的第一个参数返回。仅支持GET请求,而且存在安全问题。

  • CORS

    • 前端无需多做处理,后端有三种方式可以进行实现

      • 在接口上使用@CrossOrign注解,并且可以带上请求的协议以及域名/IP以及端口,比如"http://localhost:8080"

  • 让配置类(@Configuration)去实现WebMvcConfigurer接口重写addCorsMappings()方法。这个的弊端主要是浏览器会自动添加一些附加的头信息,甚至还会多发送一次附加的请求,第一次就是"options",第二次才是接口真正调用的请求方法。

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**") // 添加映射路径
                .allowedOriginPatterns("*") // 设置放行哪些原始域 SpringBoot2.4.4低版本使用.allowedOrigins
                .allowedMethods("GET","HEAD","POST","PUT","DELETE","OPTIONS") // 放行哪些请求方式
                .allowCredentials(true) // 是否发送Cookie
                .maxAge(3600)
                .allowedHeaders("*"); // 暴露哪些原始请求头部信息
    }
  • Nginx

    • 使用反向代理不需要在前后端多做处理,不需要添加代码量。只需要在Nginx的nginx.conf文件的server中进行配置,这里就略微详细说明一下:listen 端口; location /前端路径/ { proxy_pass http://localhost:8080/后端路径 }。  

可以同时处理多少请求?

  • 在yml中的Tomcat中配置(因为这是Spring Boot默认的Web容器)。配置与请求线程相关的配置进行测试。

    server:
      tomcat:
      	# 最大连接数
        max-connections: 30
        # 最大等待数
        accept-count: 10
        threads:
        	# 最少线程数
        	min-spare: 10
        	# 最多线程数
        	max: 50
  • 可以使用ApiPost/JMeter进行并发测试。如下所示就是测试结果。我们不难发现:首先执行了30个线程,而后先执行完的10个线程又再一次进来执行了。这说明了1s可以处理40次请求,并且这40次请求中可以一次性处理30次,而最多可以等待10次。即配置中的accept-count(最大等待数) + max-connections(最大连接数)。【Spring Boot】夺名连环问(持续更新ing)_第6张图片

  • 在spring-configuration-metadata.json中可以发现最大连接数默认为10000,最大等待数为100。注意:我打开的是SpringBoot2.2.1版本,每个版本可能不一样

【Spring Boot】夺名连环问(持续更新ing)_第7张图片

默认的日志框架以及如何切换

  • 默认情况下SpringBoot会采用slf4j(日志接口) + logback(日志框架)完成日志的实现。那么我们从哪里可以得出来呢?看过源码的基本都知道使用LoggerFactory中获取Logger对象从而记录日志的,所以我也创建了此对象来记录日志。【Spring Boot】夺名连环问(持续更新ing)_第8张图片

  •  而这个LoggerFactory与Logger都是slf4j的日志接口,其中slf4j内有三种日志框架:logback(默认)、log4j、jul-to-slf4j 【Spring Boot】夺名连环问(持续更新ing)_第9张图片

  •  那么如何做到切换默认的日志实现框架呢?这里有两种情况:

    • 将logback切换为lof4j2:这样就需要将logback的场景启动器排除,因为slf4j这个日志接口只能运行1个桥接器。并且添加log4j2的场景启动器

      
      
          org.springframework.boot
          spring-boot-starter-web
          
              
              
                  org.springframework.boot
                  spring-boot-starter-logging
              
          
      
      
      
      
          org.springframework.boot
          spring-boot-starter-log4j2
      
    • 将logback切换为log4j。就需要将logback的桥接器排除。

      
      
          org.springframework.boot
          spring-boot-starter-web
          
              
              
                  logback-classic
                  ch.qos.logback
              
          
      
      
      
      
          org.slf4j
          slf4j-log4j12
      

如何优化启动速度

  • 延迟初始化Bean

    • 一般在SpringBoot中拥有很多的耗时任务,比如数据库连接、初始化线程池创建等等。我们可以延迟这些操作的初始化,来达到优化启动速度的目的。可以在yml中配置:spring:main:lazy-initialization: true。来将所有的Bean延迟初始化(懒加载机制)。

  • 创建扫描索引(Spring 5.0+)

    • 通过编译时创建一个静态候选列表来提高大型应用程序的启动性能。在项目中使用@Indexed之后,编译打包后的时候会在项目中自动生成META-INF/spring.components文件。当Spring应用上下文执行ComponentScan扫描时,就会用CandidateComponentsIndexLoader读取这个文件并加载,转换为CandidateComponentsIndex对象。

    • 步骤如下:引入依赖、启动类加上注解@Indexed。首次编译运行后会在/target/classes/META-INF中生成文件

      
          org.springframework
          spring-context-indexer
          true
      

      【Spring Boot】夺名连环问(持续更新ing)_第10张图片

  • 减少@ComponentScan即@SpringBootApplication扫描类的范围

  • 关闭SpringBoot的JMX监控,yml中设置:spring:jmx:enabled: false

  • 设置JVM参数 -noverify,不对类进行验证

  • 排除项目中多余的依赖jar

你可能感兴趣的:(源码解读,spring,boot,后端,java)