前言
本篇文章包含Springboot配置文件解释、热部署、自动装配原理源码级剖析、内嵌tomcat源码级剖析、缓存深入、多环境部署等等,如果能耐心看完,想必会有不少收获。
一、Spring Boot基础应用
Spring Boot特征
概念:
约定优于配置,简单来说就是你所期待的配置与约定的配置一致,那么就可以不做任何配置,约定不符合期待时才需要对约定进行替换配置。
特征:
1. SpringBoot Starter:他将常用的依赖分组进行了整合,将其合并到一个依赖中,这样就可以一次性添加到项目的Maven或Gradle构建中。
2,使编码变得简单,SpringBoot采用 JavaConfig的方式对Spring进行配置,并且提供了大量的注解,极大的提高了工作效率,比如@Configuration和@bean注解结合,基于@Configuration完成类扫描,基于@bean注解把返回值注入IOC容器。
3.自动配置:SpringBoot的自动配置特性利用了Spring对条件化配置的支持,合理地推测应用所需的bean并自动化配置他们。
4.使部署变得简单,SpringBoot内置了三种Servlet容器,Tomcat,Jetty,undertow.我们只需要一个Java的运行环境就可以跑SpringBoot的项目了,SpringBoot的项目可以打成一个jar包。
Spring Boot创建
Spring Boot项目结构图:
Spring Boot热部署
通过引入spring-bootdevtools插件,可以实现不重启服务器情况下,对项目进行即时编译。引入热部署插件的步骤如下:
1. 在pom.xml添加热部署依赖
2. IDEA热部署工具设置
3. 在项目任意页面中使用组合快捷键“Ctrl+Shift+Alt+/”打开Maintenance选项框,选中并打开Registry页面,列表中找到“compiler.automake.allow.when.app.running”,将该选项后的Value值勾选,用于指定IDEA工具在程序运行过程中自动编译,最后单击【Close】按钮完成设置。
热部署原理:
基本原理就是我们在编辑器上启动项目,然后改动相关的代码,然后编辑器自动触发编译,替换掉历史的.class文件后,项目检测到有文件变更后会重启srpring-boot项目。内部主要是通过引入的插件对我们的classpath资源变化进行监听,当classpath有变化,才会触发重启。
从官方文档可以得知,其实这里对类加载采用了两种类加载器,对于第三方jar包采用baseclassloader来加载,对于开发人员自己开发的代码则使用restartClassLoader来进行加载,这使得比停掉服务重启要快的多,因为使用插件只是重启开发人员编写的代码部分。
排除资源:
默认情况下,改变资源 /META-INF/maven , /META-INF/resources , /resources , /static , /public ,或 /templates 不触发重新启动,但确会触发现场重装。如果要自定义这些排除项,则可以使用该spring.devtools.restart.exclude 属性。例如,仅排除 /static , /public 在application.properties设置以下属性。
spring.devtools.restart.exclude=static/**,public/**,config/**
全局配置文件优先级
优先级:以下图顺序号代表配置文件的优先级,并且相同配置文件按顺序加载可以实现互补,但是不会被覆盖。
注意:Spring Boot 有application.properties 和 application.yaml 两种配置文件的方式,yaml是一种JSON超文本格式文件,如果是2.4.0之前版本,优先级properties>yaml;但是如果是2.4.0的版本,优先级yaml>properties。
自定义application.properties 配置文件注入IOC容器
填加相应依赖配置可以实现在自定义配置properties配置提示
@ConfigurationProperties(prefix = "person")注解的作用是将配置文件中以person开头的属性值通过setXX()方法注入到实体类对应属性中。
@Component注解的作用是将当前注入属性值的Person类对象作为Bean组件放到Spring容器中,只有这样才能被@ConfigurationProperties注解进行赋值。
application.yaml配置文件
YAML文件格式是Spring Boot支持的一种JSON超集文件格式,以数据为中心,比properties、xml等更
适合做配置文件.
1.yml和xml相比,少了一些结构化的代码,使数据更直接,一目了然
2.相比properties文件更简洁
3.yaml文件的扩展名可以使用.yml或者.yaml。
4.application.yml文件使用 “key:(空格)value”格式配置属性,使用缩进控制层级关系。
属性注入
如果配置属性是Spring Boot已有属性,例如服务端口server.port,那么Spring Boot内部会自动扫描并读取这些配置文件中的属性值并覆盖默认属性。
@Configuration:声明一个类作为配置类。
@Bean:声明在方法上,将方法的返回值加入Bean容器。
@Value:属性注入
@ConfigurationProperties(prefix = "jdbc"):批量属性注入。
@PropertySource("classpath:/jdbc.properties")指定外部属性文件,在类上添加。
第三方配置:
@ConfigurationProperties 用于注释类之外,您还可以在公共 @Bean 方法上使用它。将属性绑定到控件之外的第三方组件
松散绑定:
Spring Boot使用一些宽松的规则将环境属性绑定到@ConfigurationProperties bean,因此环境属性名和bean属性名之间不需要完全匹配,比如在application.properties文件里定义一个first-name=tom,在对应bean类中使用firstName也能获取到对应的值,这就是松散绑定。
Spring Boot日志框架
SLF4J 的使用:
注意:由于每一个日志的实现框架都有自己的配置文件,所以在使用 SLF4j 之后,配置文件还是要使用实现日志框架的配置文件。
统一日志框架使用:
实现步骤
1. 排除系统中的其他日志框架。
2. 使用中间包替换要替换的日志框架。
3. 导入我们选择的 SLF4J 实现。
从图中我们得到一种统一日志框架使用的方式,可以使用一种要替换的日志框架类完全一样的 jar 进行替换,这样不至于原来的第三方 jar 报错,而这个替换的 jar 其实使用了 SLF4J API. 这样项目中的日志就都可以通过 SLF4J API 结合自己选择的框架进行日志输出。
Spring Boot 的日志关系:
Spring Boot 默认已经使用了 SLF4J + LogBack . 所以我们在不进行任何额外操作的情况下就可以使用 SLF4J + Logback 进行日志输出。SLF4J 日志级别从小到大trace,debug,info,warn,error,默认是info级别。
自定义日志输出:
可以在配置文件编写日志相关配置实现自定义日志输出。
替换日志框架:
二、Spring Boot源码分析
spring-boot-starter-parent
Spring Boot项目的统一版本父项目依赖管理。
在底层源文件定义了工程的Java版本;工程代码的编译源文件编码格式;工程编译后的文件编码格式;Maven打包编译的版本。
接着在build节点做了资源过滤
接着从spring-boot-starter-parent找到他父依赖 spring-boot-dependencies,从里面就可以发现里面定义了各种版本声明,通过这里声明可以让部分依赖不需要写版本号,一些没有引入的第三方jar包仍然需要自己声明版本号。
spring-boot-starter-web
Spring Boot项目的所依赖jar包进行打包起步依赖管理
在spring-boot-starter-web的父依赖spring-boot-starters包中,可以发现在他的dependencies标签有着各种依赖包引入,点进去就是具体包的导入配置管理。
注意:Spring Boot官方并不是针对所有场景开发的技术框架都提供了场景启动器,例如阿里巴巴的Druid数据源等,Spring Boot官方就没有提供对应的依赖启动器。为了充分利用Spring Boot框架的优势,在Spring Boot官方没有整合这些技术框架的情况下,Druid等技术框架所在的开发团队主动与Spring Boot框架进行了整合,实现了各自的依赖启动器,例如druid-spring-boot-starter等。我们在pom.xml文件中引入这些第三方的依赖启动器时,切记要配置对应的版本号。
自动配置@SpringBootApplication
他是一个组合注解,核心代码:
自动配置@SpringBootConfiguration
通过上面可以发现我们的核心启动类注解源码中含此注解,这个注解标注在某个类上,表示这是一个 Spring Boot的配置类。他的核心代码中,内部有一个核心注解@Configuration来表明当前类是配置类,并且可以被组件扫描器扫到,所以@SpringBootConfiguration与@Configuration具有相同作用,只是前者又做了一次封装。
自动配置@ EnableAutoConfiguration
Spring 中有很多以 Enable 开头的注解,其作用就是借助 @Import 来收集并注册特定场景相关的Bean ,并加载到 IOC 容器。而这个注解就是借助@Import来收集所有符合自动配置条件的bean定义,并加载到IoC容器,他的核心源码如下:
通过@AutoConfigurationPackage注解进入类别,发现他通过import引入了一个AutoConfigurationPackages.Registrar.class,在Registrar.class中就重写了一个registerBeanDefinitions方法,在方法内部调用了一个register方法来实现将注解标注的元信息传入,获取到相应的包名。通俗点就是注册bean,然后根据 @AutoConfigurationPackage找到需要注册bean的类路径,这个路径就被自动保存了下来,后面需要使用bean,就直接获取使用,比如Spring Boot整合JPA可以完成一些注解扫描。
自动配置@Import(AutoConfigurationImportSelector.class)
该注解是Spring boot的底层注解,AutoConfigurationImportSelector类可以帮助 Springboot 应用将所有符合条件的 @Configuration配置都加载到当前Spring Boot创建并使用的IOC容器( ApplicationContext )中。
该注解实现了实现了 DeferredImportSelector 接口和各种Aware 接口,在源码中截图中,通过四个接口回调,把值返回给了
定义的四个成员变量。
1.自动配置逻辑相关的入口方法在 DeferredImportSelectorGrouping 类的 getImports 方法。
2.自动配置的相关的绝大部分逻辑全在第一处也就是this.group.proces方法里,主要做的事就是在方法中,传入的 AutoConfigurationImportSelector对象来选择一些符合条件的自动配置类,过滤掉一些不符合条件的自动配置类,而第二处的this.group.selectImports的方法主要是针对前面的process方法处理后的自动配置类再进一步有选择的选择导入。
3.进入getAutoConfigurationEntry方法,这个方法主要是用来获取自动配置类有关,承担了自动配置的主要逻辑。AutoConfigurationEntry 方法主要做的事情就是获取符合条件的自动配置类,避免加载不必要的自动配置类从而造成内存浪费
4.进入getCandidateConfigurations方法,里面有一个重要方法 loadFactoryNames ,这个方法是让 SpringFactoryLoader 去加载一些组件的名字。
5.进入 loadFactoryNames方法,获取到出入的键factoryClassName。
6.进入loadSpringFactories方法,加载配置文件,而这个配置文件就是spring.factories文件
由此我们可以知道,在这个方法中会遍历整个ClassLoader中所有jar包下的spring.factories文件。spring.factories里面保存着springboot的默认提供的自动配置类。
AutoConfigurationEntry 方法主要做的事情:
【1】从 spring.factories 配置文件中加载 EnableAutoConfiguration 自动配置类),获取的自动配置类如图所示。
【2】若 @EnableAutoConfiguration 等注解标有要 exclude 的自动配置类,那么再将这个自动配置类排除掉;
【3】排除掉要 exclude 的自动配置类后,然后再调用 filter 方法进行进一步的过滤,再次排除一些不符合条件的自动配置类;
【4】经过重重过滤后,此时再触发 AutoConfigurationImportEvent 事件,告诉ConditionEvaluationReport 条件评估报告器对象来记录符合条件的自动配置类;
【5】 最后再将符合条件的自动配置类返回。
AutoConfigurationImportSelector 的 filter 方法
主要做的事情就是调用AutoConfigurationImportFilter 接口的 match 方法来判断每一个自动配置类上的条件注解(若有的话) @ConditionalOnClass , @ConditionalOnBean 或 @ConditionalOnWebApplication 是否满足条件,若满足,则返回true,说明匹配,若不满足,则返回false说明不匹配。其实就是排除自动配置类,因为全部加载出来的类太多,不需要全部都反射成对象,而这个方法就是通过注解进行该自动配置类是否有相应匹配的类的判断,存在即加入,不存在即过滤。
@Conditional是Spring4新提供的注解,它的作用是按照一定的条件进行判断,满足条件给容器注册bean。
@ConditionalOnBean:仅仅在当前上下文中存在某个对象时,才会实例化一个Bean。
@ConditionalOnClass:某个class位于类路径上,才会实例化一个Bean。
@ConditionalOnExpression:当表达式为true的时候,才会实例化一个Bean。基于SpEL表达式的条件判断。
@ConditionalOnMissingBean:仅仅在当前上下文中不存在某个对象时,才会实例化一个Bean。
@ConditionalOnMissingClass:某个class类路径上不存在的时候,才会实例化一个Bean。
@ConditionalOnNotWebApplication:不是web应用,才会实例化一个Bean。
@ConditionalOnWebApplication:当项目是一个Web项目时进行实例化。
@ConditionalOnNotWebApplication:当项目不是一个Web项目时进行实例化。
@ConditionalOnProperty:当指定的属性有指定的值时进行实例化。
@ConditionalOnJava:当JVM版本为指定的版本范围时触发实例化。
@ConditionalOnResource:当类路径下有指定的资源时触发实例化。
@ConditionalOnJndi:在JNDI存在的条件下触发实例化。
@ConditionalOnSingleCandidate:当指定的Bean在容器中只有一个,或者有多个但是指定了首选的Bean时触发实例化。
有选择的导入自动配置类
在第一步最后的一个方法this.group.selectImports主要是针对经过排除掉 exclude 的和被AutoConfigurationImportFilter 接口过滤后的满足条件的自动配置类再进一步排除 exclude 的自动配置类,然后再排序,至此实现了自动配置。
综合以上总结:
自动配置HttpEncodingAutoConfiguration实例
1. SpringBoot 启动会加载大量的自动配置类
2.我们看我们需要实现的功能有没有 SpringBoot 默认写好的自动配置类
3.我们再来看这个自动配置类中到底配置了哪些组件;(只要我们有我们要用的组件,我们就不需要再来配置了)
4.给容器中自动配置类添加组件的时候,会从 properties 类中获取某些属性,我们就可以在配置文件中指定这些属性的值。
xxxAutoConfiguration :自动配置类,用于给容器中添加组件从而代替之前我们手动完成大量繁琐的配置。
xxxProperties : 封装了对应自动配置类的默认属性值,如果我们需要自定义属性值,只需要根据xxxProperties 寻找相关属性在配置文件设值即可。
@ComponentScan注解
主要作用是从定义的扫描路径中,找出标识了需要装配的类自动装配到spring 的bean容器中。默认扫描路径是为@ComponentScan注解的类所在的包为基本的扫描路径(也就是标注了@SpringBootApplication注解的项目启动类所在的路径),所以这里就解释了之前spring boot为什么只能扫描自己所在类的包及其子包。
常用属性:
basePackages、value:指定扫描路径,如果为空则以@ComponentScan注解的类所在的包为基本的扫描路径。
basePackageClasses:指定具体扫描的类。
includeFilters:指定满足Filter条件的类。
excludeFilters:指定排除Filter条件的类。
SpringApplication() 构造方法
第二步getSpringFactoriesInstances方法解析
主要就是 loadFactoryNames()方法,这个方法是spring-core中提供的从META-INF/spring.factories中获取指定的类(key)的同一入口方法,获取的是key为 org.springframework.context.ApplicationContextInitializer 的类,是Spring框架的类, 这个类的主要目的就是在ConfigurableApplicationContext 调用refresh()方法之前,回调这个类的initialize方法。通过 ConfigurableApplicationContext 的实例获取容器的环境Environment,从而实现对配置文件的修改完善等工作。
源码剖析Run方法整体流程
重要六步:
第一步:获取并启动监听器
第二步:构造应用上下文环境
第三步:初始化应用上下文
第四步:刷新应用上下文前的准备阶段
第五步:刷新应用上下文
第六步:刷新应用上下文后的扩展接口
Run方法第一步:获取并启动监听器
事件机制在Spring是很重要的一部分内容,通过事件机制我们可以监听Spring容器中正在发生的一些事件,同样也可以自定义监听事件。Spring的事件为Bean和Bean之间的消息传递提供支持。当一个对象处理完某种任务后,通知另外的对象进行某些处理,常用的场景有进行某些操作后发送通知,消息、邮件等情况。
通过getRunListeners方法来获取监听器,在getRunListeners方法内部调用了一个getSpringFactoriesInstances方法,返回值是一个SpringApplicationRunListeners有参构造的监听器类,这个方法加载SpringApplicationRunListener类,把这个类当做key,这个类的作用就是负责在SpringBoot启动的不同阶段,广播出不同的消息,传递给ApplicationListener监听器实现类。
getSpringFactoriesInstances方法被重复使用。
总结:如何获取到监听器并进行启动开启监听。
Run方法第二步:构造应用上下文环境
应用上下文环境包括什么呢?包括计算机的环境,Java环境,Spring的运行环境,Spring项目的配置(在SpringBoot中就是那个熟悉的application.properties/yml)等等。
通过prepareEnvironment方法创建并按照相应的应用类型配置相应的环境,然后根据用户的配置,配置系统环境,然后启动监听器,并加载系统配置文件。
主要步骤方法:
getOrCreateEnvironment方法
configureEnvironment方法
listeners.environmentPrepared方法
总结:最终目的就是把环境信息封装到environment对象中,方便后面使用。
Run方法第三步:初始化应用上下文
通过createApplicationContext方法构建应用上下文对象context,而context中有一个属性beanFactory他是一个DefaultListableBeanFactory类,这就是我们所说的IoC容器。应用上下文对象初始化的同时IOC容器也被创建了。
在SpringBoot工程中,应用类型分为三种
通过反射拿到配置类的字节码对象并通过BeanUtils.instantiateClass方法进行实例化并返回。
总结:就是创建应用上下文对象同时创建了IOC容器。
Run方法第四步:刷新应用上下文前的准备阶段
主要的目的就是为前面的上下文对象context进行一些属性值的设置,在执行过程中还要完成一些Bean对象的创建,其中就包含核心启动类的创建。
属性设置
Bean对象创建
Spring容器在启动的时候,会将类解析成Spring内部的beanDefintion结构,并将beanDefintion存储到DefaultListableBeanFactory的Map中。BeanDefinitionLoader方法就是完成赋值。
总结:就是应用上下文属性的设置并把核心启动类生成实例化对象存储到容器中。
Run方法第五步:刷新应用上下文
Spring Boot的自动配置原理:通常来说主要就是依赖核心启动类上面的@SpringBootApplication注解,这个注解是一个组合注解,他组合了@EnableAutoConfiguration这个注解,在run方法启动会执行getImport方法,最终找到process方法,进行注解的扫描,通过注解组合关系,在底层借助@Import注解向容器导入AutoConfigurationImportSelector.class组件类,这个类在执行过程中他会去加载WEB-INF下名称为spring.factories的文件,从这个文件中根据EnableAutoConfiguration这个key来加载pom.xml引入的所有对应自动配置工厂类的全部路径配置,在经过过滤,选出真正生效的自动配置工厂类去生成实例存到容器中,从而完成自动装配。如果从Main方法的Run方法出发,了解实际实现的原理,就能知道他是怎么通过Main方法找到主类,然后再扫描主类注解,完成一系列操作。而在刷新应用上下文这步就是根据找到的主类来执行解析注解,完成自动装配的一系列过程。
通过refreshContext()方法一路跟下去,最终来到AbstractApplicationContext类的refresh()方法,其中最重要的方法就是invokeBeanFactoryPostProcessors方法,他就是在上下文中完成Bean的注册。
运行步骤:
1.prepareRefresh()刷新上下文
2.obtainFreshBeanFactory()在第三步初始化应用上下文中我们创建了应用的上下文,并触发了GenericApplicationContext类的构造方法如下所示,创建了beanFactory,也就是创建了DefaultListableBeanFactory类,这里就是拿到之前创建的beanFactory。
3.prepareBeanFactory()对上面获取的beanFactory,准备bean工厂,以便在此上下文中使用。
4.postProcessBeanFactory()向上下文中添加了一系列的Bean的后置处理器。
接着就进入到我们最重要的invokeBeanFactoryPostProcessors()方法,完成了IoC容器初始化过程的三个步骤:
1) 第一步:Resource定位
2) 第二步:BeanDefinition的载入
3) 第三个过程:注册BeanDefinition
总结:spring启动过程中,就是通过各种扫描,获取到对应的类,然后将类解析成spring内部的BeanDefition结构,存到容器中(注入到ConCurrentHashMap中),也就是最后的beanDefinitionMap中。
第一步分析:
主要方法,从invokeBeanFactoryPostProcessors方法一直往下跟,直到ConfigurationClassPostProcessor类的parse方法,会发现他把核心启动类传入了这个方法中。
在这个方法内部,他判断这个类上知否存在注解,如果存在继续进入下一个方法,直到真正做事的doProcessConfigurationClass方法,在这个方法类,他就开始处理@ComponentScan注解,获取到componentScans对象,然后调用this.componentScanParser.parse方法对他进行解析。
在方法内部根据basePackages获取对应类全限定集合,如果集合为空,就把当前的核心启动类全限定名的包名即com.lg加入,设置为basePackages(扫描的包范围),这里就完成了第一步,获取扫描路径。
第二步分析
接着再跳到doScan方法,开始把他转成BeanDefition并注入IOC容器。
在doScan方法中第一个关键点findCandidateComponents方法,根据传入的初始路径地址扫描该包及其子包所有的class,并封装成BeanDefinition并存入一个Set集合中,完成第二步。
第三步分析
有了BeanDefinition集合之后,对他进行遍历,在遍历的最后调用了一个registerBeanDefinition方法进行注册BeanDefinition。
在方法内部,执行到他的实现类DefaultListableBeanFactory中的registerBeanDefinition方法,就直接通过put方式把BeanDefinition注册进了beanDefinitionMap中。
@Import注解指定类解析
解析完主类扫描包之后,接着又开始解析@import注解指定类。
首先参数里面有一个getImports方法,他作用就是根据@import注解来获取到要导入到容器中的组件类。他从核心启动类中找到对应的@Import注解。在内部最终要的collectImports方法中,进行递归调用一直找到有@import注解的全类名,最后返回所有有@Import注解的组件类。
获取到注解组件类之后,就需要去执行组件类了,回到ConfigurationClassParser类的parse方法,执行this.deferredImportSelectorHandler.process方法。
接着往下走最后到processGroupImports方法内,里面有非常重要的一步grouping.getImports()。
先通过grouping.getImports()方法里面调用了process方法,加载spring.factories文件配置所有类,一步步过滤,最后封装成AutoConfigurationEntry对象返回,把这些对象放入Map<String, AnnotationMetadata> entries集合中。
最后通过this.group.selectImports()方法再进行过滤排序,返回要生效的自动装配对象全路径集合,最后通过this.reader.loadBeanDefinitions(configClasses)方法使这些自动装配类全部生效。
Run方法第六步:刷新应用上下文后的扩展接口
afterRefresh方法,他其实就是一个扩展接口,设计模式中的模板方法,默认为空实现。如果有自定义需求,可以重写该方法。比如打印一些启动结束log,或者一些其它后置处理。
自定义Starter
Spring Boot中的starter是一种非常重要的机制,能够抛弃以前繁杂的配置,将其统一集成进starter,应用者只需要在maven中引入starter依赖,Spring Boot就能自动扫描到要加载的信息并启动相应的默认配置。starter让我们摆脱了各种依赖库的处理,需要配置各种信息的困扰。Spring Boot会自动通过classpath路径下的类发现需要的Bean,并注册进IOC容器。Spring Boot提
供了针对日常企业应用研发各种场景的spring-boot-starter依赖模块。所有这些依赖模块都遵循着约定成俗的默认配置,并允许我们调整这些配置,即遵循“约定大于配置”的理念。简而言之,starter就是一个外部的项目,我们需要使用它的时候就可以在当前Spring Boot项目中引入它,Spring Boot会自动完成装配。
使用场景
比如动态数据源、登陆模块、AOP日志切面等等就可以将这些功能封装成一个starter,复用的时候只需要在pom.xml引入即可,比如阿里的Druid数据源,就是阿里自己实现了一个第三方starter,因此Spring Boot就可以直接引入并使用这个数据库。命令规则SpringBoot提供的starter以 spring-boot-starter-xxx 的方式命名的。官方建议自定义的starter使用 xxx-spring-boot-starter 命名规则。以区分SpringBoot生态提供的starter,比如阿里的druid-spring-boot-starter。
自定义starter代码实现:
1. 新建maven jar工程,工程名为zdy-spring-boot-starter,导入依赖
2. 编写JavaBean
3. 编写配置类MyAutoConfiguration
4. resources下创建/META-INF/spring.factories
使用自定义starter
1. 对应项目导入自定义starter的依赖
2. 在全局配置文件中配置属性值
自定义Starter热插拔技术
@Enablexxx注解就是一种热拔插技术,加了这个注解就可以启动对应的starter,当不需要对应的starter的时候只需要把这个注解注释掉就行。
1. 新增标记类ConfigMarker
2. 新增EnableRegisterServer注解,将@Import引入的组件类生成实例,添加进容器
3. 改造 MyAutoConfiguration 新增条件注解 @ConditionalOnBean(ConfigMarker.class) ,@ConditionalOnBean 这个是条件注解,前面的意思代表只有当期上下文中含有 ConfigMarker对象,被标注的类才会被实例化,才能让自动配置类生效。
4. 在启动类上新增上面自定义@EnableRegisterServer注解,根据之前的源码分析可以知道他在执行过程中会解析这个注解,再去解析里面的@Import注解,从而拿到组件类生成实例化对象存入容器,满足自动装配条件。
关于条件注解的讲解:
@ConditionalOnBean:仅仅在当前上下文中存在某个对象时,才会实例化一个Bean。
@ConditionalOnClass:某个class位于类路径上,才会实例化一个Bean。
@ConditionalOnExpression:当表达式为true的时候,才会实例化一个Bean。基于SpEL表达式的条件判断。
@ConditionalOnMissingBean:仅仅在当前上下文中不存在某个对象时,才会实例化一个Bean。
@ConditionalOnMissingClass:某个class类路径上不存在的时候,才会实例化一个Bean。
@ConditionalOnNotWebApplication:不是web应用,才会实例化一个Bean。
@ConditionalOnWebApplication:当项目是一个Web项目时进行实例化。
@ConditionalOnNotWebApplication:当项目不是一个Web项目时进行实例化。
@ConditionalOnProperty:当指定的属性有指定的值时进行实例化。
@ConditionalOnJava:当JVM版本为指定的版本范围时触发实例化。
@ConditionalOnResource:当类路径下有指定的资源时触发实例化。
@ConditionalOnJndi:在JNDI存在的条件下触发实例化。
@ConditionalOnSingleCandidate:当指定的Bean在容器中只有一个,或者有多个但是指定了首选的Bean时触发实例化。
内嵌tomcat原理
Spring Boot默认支持Tomcat,Jetty,和Undertow作为底层容器。而Spring Boot默认使用Tomcat,一旦引入spring-boot-starter-web模块,就默认使用Tomcat容器。
通过前面的源码分析我们可以知道核心启动类在启动的时候,进入AutoConfigurationImportSelector类中的getAutoConfigurationEntry方法去各个模块WEB-INF下的spring.factories配置文件中加载相关配置类,获取到ServletWebServerFactoryAutoConfiguration自动配置类,也就是tomcat自动配置。
通过spring.factories配置文件找到ServletWebServerFactoryAutoConfiguration注解类分析上面的注解。
通过@Import发现他将EmbeddedTomcat、EmbeddedJetty、EmbeddedUndertow等嵌入式容器类加载进来了,进入EmbeddedTomcat.class类,通过TomcatServletWebServerFactory类的getWebServer方法,实现了tomcat的实例化,最后调用 getTomcatWebServer方法进入下一步操作。
根据上面方法继续往下走,到了initialize,就开始启动了tomcat。
总结:@Import引入的EmbeddedTomcat类里面TomcatServletWebServerFactory工厂类的getWebServer方法一旦被启动他就会创建并启动内嵌tomcat。
getWebServer方法被调用点分析
调用的地方其实就是之前分析过的refresh方法中的onRefresh方法
在方法里面进入到ServletWebServerApplicationContext类的createWebServer方法,他会获取嵌入式的Servlet容器工厂,并通过工厂来获取Servlet容器,当获取到了容器工厂之后就通过工厂来调用getWebServer方法,也就是上面那个,来完成内嵌tomcat的创建和启动。
自动配置Spring MVC源码分析
SpringBoot项目里面是可以直接使用诸如 @RequestMapping 这类的SpringMVC的注解,在以前的项目中除了引入包之外还需要在web.xml配置一个前端控制器org.springframework.web.servlet.DispatcherServlet。spring Boot通过自动装配就实现了相同的效果, IOC容器的注入后,最重要的一点就是需要把DispatcherServlet再注册进servletContext中,也就是servlet容器中。
自动配置DispatcherServlet加载
和前面tomcat分析一样,我们还是要先去找到DispatcherServlet的自动配置类
接着到META-INF/spring.factories下,找到我们对应的DispatcherServletAutoConfiguration全限定名路径,进入对应注解类查看源码。
最下面那个类点进去发现他就是tomcat的那个注解类,也就是先需要tomcat完成自动装配再来解析DispatcherServletAutoConfiguration类。
在DispatcherServletAutoConfiguration类里面有两个内部类DispatcherServletConfiguration类和DispatcherServletRegistrationCondition类。
在第一个内部类中创建了DispatcherServlet实例化对象设值存到IOC容器,但是还未添加到servletContext。
在第二个内部类中就判断存在DispatcherServlet类对象再执行,而这个类对象已经在第一个内部类已经创建存在于上下文中。接着他构建了DispatcherServletRegistrationBean对象,并进行了返回。由此我们得到了两个对象,第一个前端控制器DispatcherServlet,第二个DispatcherServletRegistrationBean是DispatcherServlet的注册类,他的作用就是把他注册进servletContext中。
自动配置DispatcherServlet注册
通过类图分析最上面是一个ServletContextInitializer接口。我们可以知道,实现该接口意味着是用来初始化ServletContext的。
进入onStartup方法内部,根据类图一直往下直到ServletRegistrationBean类中的addRegistration方法,发现他把DispatcherServlet给add进了servletContext完成了注册。
而这个addRegistration方法的触发跟上面tomcat的触发是相同地方,通过getWebServer方法里面的getSelfInitializer方法在方法内部继续调用selfInitialize方法,通过getServletContextInitializerBeans拿到所有的ServletContextInitializer 集合,而集合中就包含了我们需要的DispatcherServlet控制器,接着遍历这个集合,使用遍历结果集调用onStartup方法,就完成了DispatcherServlet的注册。
三、Spring Boot高级进阶
Spring Boot数据源自动配置
application.properties文件中数据源指定的命名规则是因为底层源码设定了需要这样才能获取,注入DataSourceProperties类对象中。
数据源自动配置连接池默认指定Hikari,通过指定TYPE就可以实现更换连接池。
在引入spring-boot-starter-jdbc包后就把HikariCP也一并引入进来了,满足条件,而其他连接池未引入,不满足因此默认是HikariCP连接池。
如果多个连接池都满足的情况下,按照配置的数组顺序取值,第一个仍然是HikariCP,所以在不指定的情况下,默认永远是他。
Mybatis自动配置源码分析
与前面的tomcat自动配置,DispatcherServlet自动配置是一样的,都是通过run方法解析EnableAutoConfiguration注解,进入AutoConfigurationImportSelector类,然后去WEB-INF下的spring.factories配置文件中加载相关配置类,完成自动配置类组装。
1.通过application.properties文件设置mybatis属性可以注解注入到对应的MybatisProperties类使用。
2..这个类里面第一个方法sqlSessionFactory,通过他实现了创建sqlSessionFactory类、 Configuration类并添加容器,内部实现其实就是先解析配置,封装Configuration对象,完成准备工作,然后调用MyBatis的初始化流程。
3. 这个类第二个方法sqlSessionTemplate,作用是与mapperProoxy代理类有关。SqlSessionTemplate是线程安全的,可以被多个Dao持有。
4. 得到了SqlSessionFactory了,接下来就是如何扫描到相关的Mapper接口了,通过注解@MapperScan(basePackages = “com.mybatis.mapper”)实现,在内部@Import引入MapperScannerRegistrar类调用registerBeanDefinitions方法进行注册。
通俗点就是:@MapperScan(basePackages = “com.mybatis.mapper”)这个定义,扫描指定包下的mapper接口,通过动态代理生成了实现类,然后设置每个mapper接口的beanClass属性为MapperFactoryBean类型并加入到spring的bean容器中。使用者就可以通过@Autowired或者getBean等方式,从spring容器中获取。
SpringBoot + Mybatis实现动态数据源切换
业务背景
电商订单项目分正向和逆向两个部分:其中正向数据库记录了订单的基本信息,包括订单基本信息、订单商品信息、优惠卷信息、发票信息、账期信息、结算信息、订单备注信息、收货人信息等;逆向数据库主要包含了商品的退货信息和维修信息。数据量超过500万行就要考虑分库分表和读写分离,那么我们在正向操作和逆向操作的时候,就需要动态的切换到相应的数据库,进行相关的操作。
解决思路
现在项目的结构设计基本上是基于MVC的,那么数据库的操作集中在dao层完成,主要业务逻辑在service层处理,controller层处理请求。假设在执行dao层代码之前能够将数据源(DataSource)换成我们想要执行操作的数据源,那么这个问题就解决了。
实现原理
Spring内置了一个AbstractRoutingDataSource抽象类,它可以把多个数据源配置成一个Map,然后,根据不同的key返回不同的数据源。因为AbstractRoutingDataSource也是一个DataSource接口,因此,应用程序可以先设置好key, 访问数据库的代码就可以从AbstractRoutingDataSource拿到对应的一个真实的数据源,从而访问指定的数据库。
通过内部的getConnection方法获取数据源,在连接数据库之前会执行determineCurrentLookupKey()方法,这个方法返回的数据作为key去targetDataSources中查找相应的值,如果查到相对应的DataSource,那么就使用此DataSource获取数据库连接。
环境准备工作
实体类:
Mapper类:
Service类
Controller类:
具体实现步骤
多数据源配置:
读取初始数据源类:
创造一个RoutingDataSourceContext类指定数据源
创建一个继承AbstractRoutingDataSource类的RoutingDataSource类,重写determineCurrentLookupKey方法
创建一个primaryDataSource方法,引入两个数据源bean对象,获取数据源,设置key存入继承类RoutingDataSource中。
改造Controller类,在具体业务逻辑执行前,进行数据源绑定确认。
测试结果,分别调用两个方法,返回了不同数据库的结果
动态数据源优化
需要读数据库的地方,在对应controller类方法里面就需要加上一大段RoutingDataSourceContext routingDataSourceContext = new RoutingDataSourceContext(key);代码,这里可以通过自定义注解@RoutingWith("slaveDataSource"),来实现这个效果,取消大量重复代码编写。
新增RoutingWith注解类:
借助AOP的动态代理对方法拦截实现横切逻辑增强,添加Maven依赖
切面类RoutingAspect
自定义注解定义完毕,把controller类改造成注解方式就完成了优化改造。
四、Spring Boot缓存深入
JSR107
关于如何使用缓存的规范,是java提供的一个接口规范,类似于JDBC规范。
五个核心接口:
CachingProvider(缓存提供者):创建、配置、获取、管理和控制多个CacheManager。
CacheManager(缓存管理器):创建、配置、获取、管理和控制多个唯一命名的Cache,Cache存在于CacheManager的上下文中。一个CacheManager仅对应一个CachingProvider。
Cache(缓存):是由CacheManager管理的,CacheManager管理Cache的生命周期,Cache存在于CacheManager的上下文中,是一个类似map的数据结构,并临时存储以key为索引的值。一个Cache仅被一个CacheManager所拥有
Entry(缓存键值对):是一个存储在Cache中的key-value对。
Expiry(缓存时效):每一个存储在Cache中的条目都有一个定义的有效期。一旦超过这个时间,条目就自动过期,过期后,条目将不可以访问、更新和删除操作。缓存有效期可以通过ExpiryPolicy设置。
一个应用里面可以有多个缓存提供者(CachingProvider),一个缓存提供者可以获取到多个缓存管理器(CacheManager),一个缓存管理器管理着不同的缓存(Cache),缓存中是一个个的缓存键值对(Entry),每个entry都有一个有效期(Expiry)。缓存管理器和缓存之间的关系有点类似于数据库中连接池和连接的关系。
缓存概念及缓存注解
Spring从3.1开始定义了org.springframework.cache.Cache和org.springframework.cache.CacheManager接口来统一不同的缓存技术;并支持使用JavaCaching(JSR-107)注解简化我们进行缓存开发。
Spring Cache 只负责维护抽象层,具体的实现由自己的技术选型来决定。将缓存处理和缓存技术解除耦合。
每次调用需要缓存功能的方法时,Spring会检查指定参数的指定的目标方法是否已经被调用过,如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回给用户。下次调用直接从缓存中获取。
两个接口:
Cache:缓存抽象的规范接口,缓存实现有:RedisCache、EhCache、ConcurrentMapCache等。
CacheManager:缓存管理器,管理Cache的生命周期。
1.@Cacheable标注在方法上,表示该方法的结果需要被缓存起来,缓存的键由keyGenerator的策略决定,缓存的值的形式则由serialize序列化策略决定(序列化还是json格式);标注上该注解之后,在缓存时效内再次调用该方法时将不会调用方法本身而是直接从缓存获取结果。
2.@CachePut也标注在方法上,和@Cacheable相似也会将方法的返回值缓存起来,不同的是标注@CachePut的方法每次都会被调用,而且每次都会将结果缓存起来,适用于对象的更新。
缓存注解@Cacheable实现
1. 在核心启动类开启基于注解的缓存功能:主启动类标注@EnableCaching
2. 标注缓存相关注解:@Cacheable、CacheEvict、CachePut
@Cacheable 开启缓存查询 会将查询出来的值存到缓存中
* value/cacheNames 指定缓存的名称,cacheManager是管理多个cache,以名称进行区分
* key:缓存数据时指定key值,(key,value),value就是查询的结果,key默认方法的参数值,也可以去使用spEl去计key值
* keyGenerator:key的生成策略,和key二选一,通过他可以自定义keyGenerator
* cacheManager:指定缓存管理器,比如redis和ehcache
* cacheResolver:功能和cacheManager相同 二选一
* condition:条件属性,必须要满足这个条件才会进行缓存
*unless: 否定条件,满足这个条件不进行缓存
*sync : 是否使用异步模式进行缓存
注意:既满足condition又满足unless条件的也不进行缓存;使用异步模式进行缓存时(sync=true):unless条件将不被支持
可用的SpEL表达式:
自动配置缓存实现源码分析
Spring Boot中所有的自动配置都是基于AutoConfigurationImportSelector类,和前面的自动配置一样,都是在getAutoConfigurationEntry方法中在WEB-INF下的spring.factories配置文件中加载相关配置类,找到org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,这就是对应的缓存配置类。
进入CacheAutoConfiguration缓存配置类。
进入引入的组件类CacheConfigurationImportSelector,发现他实现了ImportSelector接口重写了selectImports方法,返回了所有的缓存组件类数组。这些加载顺序是自上而下的。
打开RedisCacheConfiguration组件类,通过上面的注解条件分析,在没有引入redis的情况下,这些组件类是不会生效的。
而SimpleCacheConfiguration类里面的注解条件满足,因此默认的缓存组件类就是SimpleCacheConfiguration。
分析源码得到通过cacheManager方法,会创建ConcurrentMapCacheManager对象并返回,在这个对象类里面实现了CacheManager接口,在里面有一个getCache方法,通过双重校验锁机制,先从缓存中取缓存对象,如果有直接返回,如果没得,就创建,并且添加到cacheMap,也就是缓存池中。而我们的这个cacheMap他的底层结构就是通过createConcurrentMapCache方法创建并返回的Cache,里面定义了Cache的属性以及操作Cache的方法,比如lookup、put方法,在执行查询的时候,他会先调用lookup方法查询是否有缓存结果,如果没有查询数据库,就把结果拿到再调用put方法存入缓存。
@CachePut&@CacheEvict&@CacheConfig
@CachePut
既调用方法,又更新缓存数据,一般用于更新操作,在更新缓存时一定要和想更新的缓存有相同的缓存名称和相同的key(可类比同一张表的同一条数据)。
1.先调用目标方法
2.将目标方法的结果缓存起来
@CacheEvict
缓存清除,清除缓存时要指明缓存的名字和key,相当于告诉数据库要删除哪个表中的哪条数据,key默认为参数的值
value/cacheNames:缓存的名字
key:缓存的键
allEntries:是否清除指定缓存中的所有键值对,默认为false,设置为true时会清除缓存中的所有键值对,与key属性二选一使用,就是是否清除所有。
beforeInvocation:在@CacheEvict注解的方法调用之前清除指定缓存,默认为false,即在方法调用之后清除缓存,设置为true时则会在方法调用之前清除缓存(在方法调用之前还是之后清除缓存的区别在于方法调用时是否会出现异常,若不出现异常,这两种设置没有区别,若出现异常,设置为在方法调用之后清除缓存将不起作用,因为方法调用失败了)。
@CacheConfig
标注在类上,抽取缓存相关注解的公共配置,可抽取的公共配置有缓存名字、主键生成器等(如注解中的属性所示)
就是把下面方法上的注解抽取上来统一配置,避免繁琐的重复配置代码。
基于Redis的缓存实现
SpringBoot默认开启的缓存管理器是ConcurrentMapCacheManager,创建缓存组件是ConcurrentMapCache,将缓存数据保存在一个个的ConcurrentHashMap<Object, Object>中。开发时我们可以使用缓存中间件:redis、memcache、ehcache等,这些缓存中间件的启用很简单——只要向容器中加入相关的bean就会启用,可以启用多个缓存中间件。
1. 引入Redis的starter
引入相关Bean之后,Spring Boot的自动装配就会在初始化时,找到Redis自定义的RedisAutoConfiguration进行装配,里面有两个返回类型,RedisTemplate和StringRedisTemplate(用来操作字符串:key和value都是字符串),template中封装了操作各种数据类型的操作(stringRredisTemplate.opsForValue()、stringRredisTemplate.opsForList()等)。
2. 配置redis:只需要配置redis的主机地址spring.redis.host=127.0.0.1,
注意:如何要使用Redis缓存数据,对应操作的实体类一定要实现Serializable接口。
在前面自定义配置缓存上说了缓存的九大组件,并且自上而下加载顺序,当Redis被引入之后,他的条件先被满足完成自动装配并创建了CacheManager类对象,那么下面的SimpleCacheConfiguration类,因为CacheManager.class已经存在,就不会生效。
自定义RedisCacheManager
SpringBoot默认采用的是JDK的对象序列化方式,我们可以切换为使用JSON格式进行对象的序列化操作,这时需要我们自定义序列化规则。
RedisConfig配置类中使用@Bean注解注入了一个默认名称为方法名的cacheManager组件。在定义的Bean组件中,通过RedisCacheConfiguration对缓存数据的key和value分别进行了序列化方式的定制,其中缓存数据的key定制为StringRedisSerializer(即String格式),而value定制为了Jackson2JsonRedisSerializer(即JSON格式),同时还使用entryTtl(Duration.ofDays(1))方法将缓存数据有效期设置为1天完成基于注解的Redis缓存管理器RedisCacheManager定制后,可以对该缓存管理器的效果进行测试(使用自定义序列化机制的RedisCacheManager测试时,实体类可以不用实现序列化接口)
五、Spring Boot部署与监控
JAR包部署
1. 项目打包
2. 项目启动
WAR包部署
1. 修改pom.xml配置
2. 添加依赖
3. 排除Spring Boot内置Tomcat
4. 改造启动类
5. 打包交给外置Tomcat运行
JAR包和WAR包部署差异:
1.jar更加简单方便,使用 java -jar xx.jar 就可以启动。所以打成 jar 包的最多。而 war包可以部署到tomcat的 webapps 中,随Tomcat的启动而启动。具体使用哪种方式,应视应用场景而定。
2、打jar包时不会把src/main/webapp 下的内容打到jar包里 (你认为的打到jar包里面,路径是不行的会报404)打war包时会把src/main/webapp 下的内容打到war包里。
3.打成什么文件包进行部署与项目业务有关,就像提供 rest 服务的项目需要打包成 jar文件,用命令运行很方便。。。而有大量css、js、html,且需要经常改动的项目,打成 war 包去运行比较方便,因为改动静态资源可以直接覆盖,很快看到改动后的效果,这是 jar 包不能比的。
多环境部署
线上环境prod(product)、开发环境dev(development)、测试环境test、提测环境qa、单元测试unitest等等多种环境进行不同配置。
1. 多环境配置文件
2. 指定要加载的配置文件
监控插件-Acturator
Spring boot作为微服务框架,除了它强大的快速开发功能外,还有就是它提供了actuator模块,引入该模块能够自动为Spring boot应用提供一系列用于监控的端点。Spring Boot Actuator提供了对单个Spring Boot的监控,信息包含:应用状态、内存、线程、堆栈等等,比较全面的监控了Spring Boot应用的整个生命周期。
Actuator 的 REST 接口
Actuator 监控分成两类:原生端点和用户自定义端点;自定义端点主要是指扩展性,用户可以根据自己的实际应用,定义一些比较关心的指标,在运行期进行监控。
原生端点是在应用程序里提供众多 Web 接口,通过它们了解应用程序运行时的内部状况。
原生端点又可以分成三类:
应用配置类:可以查看应用在运行期的静态信息:例如自动配置信息、加载的 springbean信息、yml 文件配置信息、环境信息、请求映射信息;
度量指标类:主要是运行期的动态信息,例如堆栈、请求链、一些健康指标、metrics 信息等;
操作控制类:主要是指 shutdown,用户可以发送一个请求将应用的监控功能关闭。
原生端点十三个接口:
引入依赖
注意:保证actuator 暴露的监控接口的安全性,需要添加安全控制的依赖 spring-boot-startsecurity 依赖,访问应用监控端点时,都需要输入验证信息。
属性详解
在 Spring Boot 2.x 中为了安全期间,Actuator 只开放了两个端点 /actuator/health 和/actuator/info。
开启所有的监控点,也可部分:
Health:
health 主要用来检查应用的运行状态,这是我们使用最高频的一个监控点。通常使用此接口提醒我们应用实例的运行状态,以及应用不”健康“的原因,比如数据库连接、磁盘空间不够等,UP表示程序健康运行。
Info:
info 就是我们自己配置在配置文件中以 info 开头的配置信息。
Beans:
展示了 bean 的别名、类型、是否单例、类的地址、依赖等信息。
Conditions:
Spring Boot 的自动配置功能非常便利,但有时候也意味着出问题比较难找出具体的原因。使用conditions 可以在应用运行时查看代码了某个配置在什么条件下生效,或者某个自动配置为什么没有生效。
Heapdump:
返回一个 GZip 压缩的 JVM 堆 dump,我们可以使用 JDK 自带的 Jvm 监控工具 VisualVM 打开此文件查看内存快照。
Mappings:
描述全部的 URI 路径,以及它们和控制器的映射关系。
Threaddump:
生成当前线程活动的快照。这个功能非常好,方便我们在日常定位问题的时候查看线程的情况。 主要展示了线程名、线程ID、线程的状态、是否等待锁资源等信息。
Shutdown:
开启接口优雅关闭 Spring Boot 应用,要使用这个功能首先需要在配置文件中开启。
management.endpoint.shutdown.enabled=true
监控插件-Spring Boot Admin
Spring Boot Admin 是一个针对spring-boot的actuator接口进行UI美化封装的监控工具。他可以返回在列表中浏览所有被监控spring-boot项目的基本信息比如:Spring容器管理的所有的bean、详细的Health信息、内存信息、JVM信息、垃圾回收信息、各种配置信息(比如数据源、缓存列表和命中率)等,Threads 线程管理,Environment 管理等。
1. 搭建Server端
2. 搭建client端
3. 通过地址http://localhost:8080/applications测试结果