第一部分:SpringBoot应用回顾
1.1 概述
约定优于配置(Convention over Configuration),又称按约定编程,是一种软件设计规范。
好处:大大减少了配置项。
这个思想贯穿springboot始终。
springboot:通过Spring Boot,可以轻松地创建独立的,基于生产级别的基于Spring的应用程序,基于spring4.0设计
1.2 主要特性
- SpringBoot Starter:他将常用的依赖分组进行了整合,将其合并到一个依赖中,这样就可以一次
性添加到项目的Maven或Gradle构建中; - 使编码变得简单,SpringBoot采用 JavaConfig的方式对Spring进行配置,并且提供了大量的注解,
极大的提高了工作效率。 - 自动配置:SpringBoot的自动配置特性利用了Spring对条件化配置的支持,合理地推测应用所需的
bean并自动化配置他们; - 使部署变得简单,SpringBoot内置了三种Servlet容器,Tomcat,Jetty,undertow.我们只需要一个
Java的运行环境就可以跑SpringBoot的项目了,SpringBoot的项目可以打成一个jar包。
1.3 抛出疑问
- starter是什么?我们如何去使用这些starter?
- 为什么包扫描只会扫描核心启动类所在的包及其子包
- 在springBoot启动的过程中,是如何完成自动装配的?
- 内嵌Tomcat是如何被创建及启动的?
- 使用了web场景对应的starter,springmvc是如何自动装配?
下文源码分析部分进行逐个解答。
1.4 热部署
修改代码以后不需要手动重新启动应用即可服务功能更新
1、引入依赖
<!-- 引入热部署依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
2、配置IEDA
选择IDEA工具界面的【File】->【Settings】选项,打开Compiler面板设置页面
选择Build下的Compiler选项,在右侧勾选“Build project automatically”选项将项目设置为自动编
译,单击【Apply】→【OK】按钮保存设置
在项目任意页面中使用组合快捷键“Ctrl+Shift+Alt+/”打开Maintenance选项框,选中并打开
Registry页面,具体如图
列表中找到“compiler.automake.allow.when.app.running”,将该选项后的Value值勾选,用于指
定IDEA工具在程序运行过程中自动编译,最后单击【Close】按钮完成设置
热部署原理
就是我们在编辑器上启动项目,然后改动相关的代码,然后编辑器自动触发编译
替换掉历史的.class文件后,项目检测到有文件变更后会重启srpring-boot项目。
引入热部署插件以后,会对类加载采用了两种类加载器,对于第三方jar包采用baseclassloader来加载,对于开发人员自己开发的代码则使用restartClassLoader来进行加载,这使得比停
掉服务重启要快的多,因为使用插件只是重启开发人员编写的代码部分。
某些资源在更改后不一定需要触发重新启动,使用如下配置自定义更改后不需要重新启动的资源目录:
spring.devtools.restart.exclude=static/**,public/**
1.5 全局配置文件
springboot默认会从
–file:./config/
–file:./
–classpath:/confi
–classpath:/
这些目录寻找全局配置文件(按照优先级从高到低的顺序):
- 先去项目根目录找config文件夹下找配置文件件
- 再去根目录下找配置文件
- 去resources下找cofnig文件夹下找配置文件
- 去resources下找配置文件
备注:
1、如果高优先级中配置文件属性与低优先级配置文件不冲突的属性,则会共同存在— 互补配置
2、如果同一个配置属性,在多个配置文件都配置了,默认使用第1个读取到的,后面读取的不覆盖前面读取
到的。3、如果同一个目录下,有application.yml也有application.properties,如果是2.4.0之前版本,优先级properties>yaml
但是如果是2.4.0的版本,优先级yaml>properties,如果想继续使用 Spring Boot 2.3 的配置逻辑,也可以通过在 application.properties 或者application.yml 配置文件中添加以下参数:spring.config.use-legacy-processing = true
如果我们的配置文件名字不叫application.properties或者application.yml,可以通过以下参数来指定
配置文件的名字,myproject是配置文件名
$ java -jar myproject.jar --spring.config.name=myproject
我们同时也可以指定其他位置的配置文件来生效
指定配置文件和默认加载的这些配置文件共同起作用形成互补配置
ava -jar run-0.0.1-SNAPSHOT.jar --
spring.config.location=D:/application.properties
在使用@ConfigurationProperties
的时候,可以添加如下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
然后重新启动或者构建项目,即可在application.yml文件出现相关的配置提示
1.6 属性注入
1.6.1 属性注入常用注解
@Configuration:声明一个类作为配置类
@Bean:声明在方法上,将方法的返回值加入Bean容器
@Value:属性注入
@ConfigurationProperties(prefix = “jdbc”):批量属性注入,搭配@Component或者@EnableConfigurationProperties一起使用可以放在类上,也可以搭配@Bean放在方法上
@PropertySource(“classpath:/jdbc.properties”)指定外部属性文件。在类上添加
1.6.2 如何覆盖第三方配置
除了 @ConfigurationProperties 用于注释类之外,您还可以在公共 @Bean 方法上使用它。当要将属
性绑定到控件之外的第三方组件时,这样做特别有用。如下所示
@Configuration
public class MyService {
@ConfigurationProperties("another")
@Bean
public AnotherComponent anotherComponent(){
return new AnotherComponent();
}
}
another.enabled=true
another.remoteAddress=192.168.10.11
1.7 日志框架
Spring 框架选择使用了 JCL 作为默认日志输出。而 Spring Boot 默认选择了 SLF4J + LogBack
对应的依赖如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
<version>2.4.0.RELEASE</version>
</dependency>
替换日志框架
修改pom文件如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-logging</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
第二部分:SpringBoot源码剖析
2.1 【重点】自动配置
@SpringBootApplication
@Target({ElementType.TYPE}) //注解的适用范围,Type表示注解可以描述在类、接口、注解或枚举中
@Retention(RetentionPolicy.RUNTIME) //表示注解的生命周期,Runtime运行时
@Documented //表示注解可以记录在javadoc中
@Inherited //表示可以被子类继承该注解
@SpringBootConfiguration // 标明该类为配置类
@EnableAutoConfiguration // 启动自动配置功能
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes =
TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes =
AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication
@SpringBootConfiguration
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration // 配置类的作用等同于配置文件,配置类也是容器中的一个对象
public @interface SpringBootConfiguration {
}
是一个配置类
@EnableAutoConfiguration
// 自动配置包
@AutoConfigurationPackage
// Spring的底层注解@Import,给容器中导入一个组件;
// 导入的组件是AutoConfigurationPackages.Registrar.class
@Import(AutoConfigurationImportSelector.class)
// 告诉SpringBoot开启自动配置功能,这样自动配置才能生效。
public @interface EnableAutoConfiguration
@EnableAutoConfiguration就是借助@Import来收集所有符合自动配置条件的bean定义,并加载到
IoC容器
@AutoConfigurationPackage
@Import(AutoConfigurationPackages.Registrar.class) // 导入Registrar中注册的组件
public @interface AutoConfigurationPackage
@Import(AutoConfigurationPackages.Registrar.class) ,它就是将 Registrar 这个组件类导入
到容器中,可查看 Registrar 类中 registerBeanDefinitions 方法:
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
// 将注解标注的元信息传入,获取到相应的包名
register(registry, new PackageImport(metadata).getPackageName());
}
注册一个 Bean ,这个 Bean 就是
org.springframework.boot.autoconfigure.AutoConfigurationPackages.BasePackages,它保存了spring要扫描的基础包路径。
@Import(AutoConfigurationImportSelector.class)
AutoConfigurationImportSelector 重点是实现了 DeferredImportSelector 接口和各种
Aware 接口,然后 DeferredImportSelector 接口又继承了 ImportSelector 接口。
根据org.springframework.context.annotation.ConfigurationClassParser#processImports
...
if (candidate.isAssignable(ImportSelector.class)) {
// Candidate class is an ImportSelector -> delegate to it to determine imports
Class<?> candidateClass = candidate.loadClass();
ImportSelector selector = ParserStrategyUtils.instantiateClass(candidateClass, ImportSelector.class,
this.environment, this.resourceLoader, this.registry);
if (selector instanceof DeferredImportSelector) {
//对于@Import(AutoConfigurationImportSelector.class)的处理会走到这里
this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
}
else {
String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames);
processImports(configClass, currentSourceClass, importSourceClasses, false);
}
}
...
继续跟org.springframework.context.annotation.ConfigurationClassParser#handle
...
DeferredImportSelectorHolder holder = new DeferredImportSelectorHolder(
configClass, importSelector);
if (this.deferredImportSelectors == null) {
DeferredImportSelectorGroupingHandler handler = new DeferredImportSelectorGroupingHandler();
handler.register(holder);
//会走到这里,处理import
handler.processGroupImports();
}
...
继续跟org.springframework.context.annotation.ConfigurationClassParser#processGroupImports
//重点关注grouping.getImports()
grouping.getImports().forEach(entry -> {
...
});
继续跟org.springframework.context.annotation.ConfigurationClassParser#getImports
...
//处理DeferredImportSelector导入的配置类
this.group.process(deferredImport.getConfigurationClass().getMetadata(),
deferredImport.getImportSelector());
...
继续跟org.springframework.boot.autoconfigure.AutoConfigurationImportSelector.AutoConfigurationGroup#process
...
AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector) deferredImportSelector)
.getAutoConfigurationEntry(getAutoConfigurationMetadata(), annotationMetadata);
...
继续跟org.springframework.boot.autoconfigure.AutoConfigurationImportSelector#getAutoConfigurationEntry
...
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
...
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
//重点!!!使用SpringFactoriesLoader加载classpath下的META-INF/spring.factories文件
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;
}
总结下 AutoConfigurationEntry 方法主要做的事情:
-
从spring.factories配置文件中加载自动配置类;
-
加载的自动配置类中排除掉 @EnableAutoConfiguration 注解的 exclude 属性指定的自动配置
类; -
然后再用 AutoConfigurationImportFilter 接口去过滤自动配置类是否符合其标注注解(若有
标注的话) @ConditionalOnClass , @ConditionalOnBean 和
@ConditionalOnWebApplication 的条件,若都符合的话则返回匹配结果; -
然后触发 AutoConfigurationImportEvent 事件,告诉 ConditionEvaluationReport 条件评
估报告器对象来分别记录符合条件和 exclude 的自动配置类。 -
最后spring再将最后筛选后的自动配置类导入IOC容器中
条件注解
@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时触发实例化。
@ComponentScan注解
主要是从定义的扫描路径中,找出标识了需要装配的类自动装配到spring 的bean容器中。
@ComponentScan注解没有标注basePackages及value,所以扫描路径默认为@ComponentScan
注解的类所在的包为基本的扫描路径(也就是标注了@SpringBootApplication注解的项目启动类所在
的路径)
2.2 Run方法执行流程
2.2.1 SpringApplication() 构造方法
public SpringApplication(ResourceLoader resourceLoader, Class<?>...
primarySources) {
//设置资源加载器为null
this.resourceLoader = resourceLoader;
//断言加载资源类不能为null
Assert.notNull(primarySources, "PrimarySources must not be null");
//将primarySources数组转换为List,最后放到LinkedHashSet集合中
this.primarySources = new LinkedHashSet<>
(Arrays.asList(primarySources));
//【1.1 推断应用类型,后面会根据类型初始化对应的环境。常用的一般都是servlet环境 】
this.webApplicationType = WebApplicationType.deduceFromClasspath();
//【1.2 初始化classpath下 META-INF/spring.factories中已配置的
ApplicationContextInitializer 】
setInitializers((Collection)
getSpringFactoriesInstances(ApplicationContextInitializer.class));
//【1.3 初始化classpath下所有已配置的 ApplicationListener 】
setListeners((Collection)
getSpringFactoriesInstances(ApplicationListener.class));
//【1.4 根据调用栈,推断出 main 方法的类名 】
this.mainApplicationClass = deduceMainApplicationClass();
}
2.2.2 run(args)
/**
* Run the Spring application, creating and refreshing a new
* {@link ApplicationContext}.
* *
@param args the application arguments (usually passed from a Java main
method)
* @return a running {@link ApplicationContext}
* *
运行spring应用,并刷新一个新的 ApplicationContext(Spring的上下文)
* ConfigurableApplicationContext 是 ApplicationContext 接口的子接口。在
ApplicationContext
* 基础上增加了配置上下文的工具。 ConfigurableApplicationContext是容器的高级接口
*/
public ConfigurableApplicationContext run(String... args) {
//记录程序运行时间
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// ConfigurableApplicationContext Spring 的上下文
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>
();
configureHeadlessProperty();
//从META-INF/spring.factories中获取监听器
//1、获取并启动监听器
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
ApplicationArguments applicationArguments = new
DefaultApplicationArguments(
args);
//2、构造应用上下文环境
ConfigurableEnvironment environment = prepareEnvironment(listeners,
applicationArguments);
//处理需要忽略的Bean
configureIgnoreBeanInfo(environment);
//打印banner
Banner printedBanner = printBanner(environment);
///3、初始化应用上下文
context = createApplicationContext();
//实例化SpringBootExceptionReporter.class,用来支持报告关于启动的错误
exceptionReporters = getSpringFactoriesInstances(
SpringBootExceptionReporter.class,
new Class[]{ConfigurableApplicationContext.class}, context);
//4、刷新应用上下文前的准备阶段
prepareContext(context, environment, listeners, applicationArguments,
printedBanner);
//5、刷新应用上下文
refreshContext(context);
//刷新应用上下文后的扩展接口
afterRefresh(context, applicationArguments);
//时间记录停止
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass)
.logStarted(getApplicationLog(), stopWatch);
} /
/发布容器启动完成事件
listeners.started(context);
callRunners(context, applicationArguments);
} catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
} t
ry {
listeners.running(context);
} catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
} r
eturn context;
}
- 第一步:获取并启动监听器,ApplicationListener
- 第二步:构造应用上下文环境,创建StandardServletEnvironment对象
- 第三步:初始化应用上下文,初始化AnnotationConfigServletWebServerApplicationContext对象,并设置其beanFactory属性为DefaultListableBeanFactory
- 第四步:刷新应用上下文前的准备阶段,将上面构造的上下文环境设置到AnnotationConfigServletWebServerApplicationContext中,执行ApplicationContextInitializer,将启动类注册到DefaultListableBeanFactory里的beanDefinitionMap
- 第五步:刷新应用上下文,spring容器启动操作,在onRefresh()里启动内嵌tomcat
- 第六步:刷新应用上下文后的扩展接口
ConfigFileApplicationListener.java
:将用户自定义的配置文件的属性添加到environment对象中的PropertySource中
2.3 内嵌Tomcat
Spring Boot默认支持Tomcat,Jetty,和Undertow作为底层容器。
而Spring Boot默认使用Tomcat,一旦引入spring-boot-starter-web模块,就默认使用Tomcat容
器。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
该依赖里面包含了tomcat和SpringMvc相关的依赖
如何替换默认的tomcat容器?
- 将tomcat依赖移除掉
- 引入其他Servlet容器依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<!--移除spring-boot-starter-web中的tomcat-->
<artifactId>spring-boot-starter-tomcat</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<!--引入jetty-->
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
2.3.1 内嵌Tomcat自动配置原理
在spring.factories中找到tomcat自动配置类ServletWebServerFactoryAutoConfiguration
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aSYryvTP-1623652323174)(/Users/jarry/Library/Application Support/typora-user-images/image-20210613130741534.png)]
继续进入EmbeddedTomcat类中,见下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R1lLiYXo-1623652323175)(/Users/jarry/Library/Application Support/typora-user-images/image-20210613131318337.png)]
进入TomcatServletWebServerFactory类,里面的getWebServer()是关键方法,如图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-50mtYwZB-1623652323176)(/Users/jarry/Library/Application Support/typora-user-images/image-20210613131437891.png)]
继续进入getTomcatWebServer()等方法,一直往下跟到tomcat初始化方法,调用tomcat.start()
方法,tomcat就正式开启运行,见图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qdC6ewic-1623652323177)(/Users/jarry/Library/Application Support/typora-user-images/image-20210613131535947.png)]
getWebServer()的调用分析在那里调用呢?
答:AbstractApplicationContext#onRefresh()
onRefresh()会调用到ServletWebServerApplicationContext中的createWebServer()
private void createWebServer() {
WebServer webServer = this.webServer;
ServletContext servletContext = getServletContext();
if (webServer == null && servletContext == null) {
//重点,factory = TomcatServletWebServerFactory
ServletWebServerFactory factory = getWebServerFactory();
//重点
this.webServer = factory.getWebServer(getSelfInitializer());
} else if (servletContext != null) {
try {
getSelfInitializer().onStartup(servletContext);
} catch (ServletException ex) {
throw new ApplicationContextException("Cannot initialize servlet
context", ex);
}
}
initPropertySources();
}
TomcatServletWebServerFactory#getWebServer
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dKCLHZwG-1623652323178)(/Users/jarry/Library/Application Support/typora-user-images/image-20210613132402400.png)]
总结:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qNQZ2d6w-1623652323180)(/Users/jarry/Library/Application Support/typora-user-images/image-20210613133024432.png)]
2.3.3 自动配置SpringMVC
在一个普通的WEB项目中如何去使用SpringMVC,我
们首先就是要在web.xml中配置如下配置
<servlet>
<description>spring mvc servlet</description>
<servlet-name>springMvc</servlet-name>
<servletclass>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springMvc</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
但是在SpringBoot中,我们没有了web.xml文件,我们如何去配置一个 Dispatcherservlet 呢?
其实Servlet3.0规范中规定,要添加一个Servlet,除了采用xml配置的方式,还有一种通过代码的
方式,伪代码如下
servletContext.addServlet(name, this.servlet);
自动配置DispatcherServlet和DispatcherServletRegistry
在spring.factories中找到springmvc自动配置类DispatcherServletAutoConfiguration
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass(DispatcherServlet.class)
@AutoConfigureAfter(ServletWebServerFactoryAutoConfiguration.class)
public class DispatcherServletAutoConfiguration {
//...
}
DispatcherServletAutoConfiguration类主要包含了两个内部类,分别是
1、DispatcherServletConfiguration,配置DispatcherServlet
2、DispatcherServletRegistrationConfiguration,配置DispatcherServlet的注册类,负责将
DispatcherServlet给注册到ServletContext中
注册DispatcherServlet到ServletContext
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d4cer39Y-1623652323182)(/Users/jarry/Library/Application Support/typora-user-images/image-20210613134236921.png)]
注册DispatcherServlet流程:
ServletContextInitializer
我们看到,最上面是一个ServletContextInitializer接口。我们可以知道,实现该接口意味着是用
来初始化ServletContext的。我们看看该接口
public interface ServletContextInitializer {
void onStartup(ServletContext servletContext) throws ServletException;
}
RegistrationBean
看看RegistrationBean是怎么实现onStartup方法的
@Override
public final void onStartup(ServletContext servletContext) throws
ServletException {
String description = getDescription();
if (!isEnabled()) {
logger.info(StringUtils.capitalize(description) + " was not
registered (disabled)");
return;
} r
egister(description, servletContext);
}
调用了内部register方法,跟进它
protected abstract void register(String description, ServletContext
servletContext);
这是一个抽象方法
DynamicRegistrationBean
再看DynamicRegistrationBean是怎么实现register方法的
@Override
protected final void register(String description, ServletContext
servletContext) {
D registration = addRegistration(description, servletContext);
if (registration == null) {
logger.info(StringUtils.capitalize(description) + " was not
registered (possibly already registered?)");
return;
}
configure(registration);
}
跟进addRegistration方法
protected abstract D addRegistration(String description, ServletContext
servletContext);
一样是一个抽象方法
ServletRegistrationBean
再看ServletRegistrationBean是怎么实现addRegistration方法的
@Override
protected ServletRegistration.Dynamic addRegistration(String description,
ServletContext servletContext) {
String name = getServletName();
return servletContext.addServlet(name, this.servlet);
}
我们看到,这里直接将DispatcherServlet给add到了servletContext当中。
SpringBoot启动流程中具体体现
getSelfInitializer().onStartup(servletContext);
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j0gg5jPq-1623652323183)(/Users/jarry/Library/Application Support/typora-user-images/image-20210613135915400.png)]
getSelfInitializer() 最终会
去调用到 ServletWebServerApplicationContext 的 selfInitialize 方法,该方法代码如下
private void selfInitialize(ServletContext servletContext) throws
ServletException {
prepareWebApplicationContext(servletContext);
ConfigurableListableBeanFactory beanFactory = getBeanFactory();
ExistingWebApplicationScopes existingScopes = new
ExistingWebApplicationScopes(
beanFactory);
WebApplicationContextUtils.registerWebApplicationScopes(beanFactory,
getServletContext());
existingScopes.restore();
WebApplicationContextUtils.registerEnvironmentBeans(beanFactory,
getServletContext());
for (ServletContextInitializer beans :
getServletContextInitializerBeans()/**重点**/) {
beans.onStartup(servletContext);
}
}
我们通过调试,知道 getServletContextInitializerBeans() 返回的是一个
ServletContextInitializer 集合,然后依次去调用对象的 onStartup 方法
第三部分:SpringBoot数据访问
3.1 数据源自动配置源码剖析
SpringBoot提供了三种数据库连接池:
- HikariCP
- Commons DBCP2
- Tomcat JDBC Connection Pool
其中spring boot2.x版本默认使用HikariCP,maven中配置如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
如果不使用HikariCP,而改用Commons DBCP2,则配置如下
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<exclusions>
<exclusion>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</exclusion>
</exclusions>
</dependency>
数据源自动配置
spring.factories中找到数据源的配置类:DataSourceAutoConfiguration
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@EnableConfigurationProperties(DataSourceProperties.class)
@Import({ DataSourcePoolMetadataProvidersConfiguration.class,
DataSourceInitializationConfiguration.class })
public class DataSourceAutoConfiguration {
@Configuration(proxyBeanMethods = false)
@Conditional(EmbeddedDatabaseCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
@Import(EmbeddedDataSourceConfiguration.class)
protected static class EmbeddedDatabaseConfiguration {
}
@Configuration(proxyBeanMethods = false)
@Conditional(PooledDataSourceCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
@Import({ DataSourceConfiguration.Hikari.class,
DataSourceConfiguration.Tomcat.class,
DataSourceConfiguration.Dbcp2.class,
DataSourceConfiguration.Generic.class,
DataSourceJmxConfiguration.class })
protected static class PooledDataSourceConfiguration {
}
...
@Import({ DataSourceConfiguration.Hikari.class,
DataSourceConfiguration.Tomcat.class,
DataSourceConfiguration.Dbcp2.class,
DataSourceConfiguration.Generic.class,
DataSourceJmxConfiguration.class })
@ConditionalOnMissingBean({DataSource.class})
@ConditionalOnProperty(
name = {"spring.datasource.type"},
havingValue = "com.zaxxer.hikari.HikariDataSource",
matchIfMissing = true
)
static class Hikari {
Hikari() {
}
@Bean
@ConfigurationProperties(
prefix = "spring.datasource.hikari"
)
public HikariDataSource dataSource(DataSourceProperties properties)
{
HikariDataSource dataSource =
(HikariDataSource)DataSourceConfiguration.createDataSource(properties,
HikariDataSource.class);
if (StringUtils.hasText(properties.getName())) {
dataSource.setPoolName(properties.getName());
}
return dataSource;
}
}
如果不配置spring.datasource.type,则默认值为com.zaxxer.hikari.HikariDataSource
数据库配置spring.datasource(DataSourceProperties.java)前缀可以,配置spring.datasource.hikari(HikariConfig.java)前缀也可以
spring.datasource.hikari会覆盖spring.datasource的配置
3.2 Druid连接池的配置
1、添加依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</dependency>
2、在application.yml中引入druid的相关配置
spring:
datasource:
username: root
password: root
url: jdbc:mysql:///springboot_h?useUnicode=true&characterEncoding=utf-
8&useSSL=true&serverTimezone=UTC
driver-class-name: com.mysql.cj.jdbc.Driver
initialization-mode: always
# 使用druid数据源
type: com.alibaba.druid.pool.DruidDataSource
# 数据源其他配置
initialSize: 5
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
filters: stat,wall,log4j
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
connectionProperties:
druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
3、编写配置类
@Configuration
public class DruidConfig {
@ConfigurationProperties(prefix = "spring.datasource")
@Bean
public DataSource druid(){
return new DruidDataSource();
}
}
3.3 SpringBoot整合Mybatis
1、添加依赖
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
2、配置application.yml
spring:
datasource:
username: root
password: root
url: jdbc:mysql:///springboot_h?useUnicode=true&characterEncoding=utf-
8&useSSL=true&serverTimezone=UTC
driver-class-name: com.mysql.jdbc.Driver
# 使用druid数据源
type: com.alibaba.druid.pool.DruidDataSource
3.4 SpringBoot + Mybatis实现动态数据源切换
原理:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hRIPk3cj-1623652323184)(/Users/jarry/Library/Application Support/typora-user-images/image-20210613191254854.png)]
Spring内置了一个AbstractRoutingDataSource,它可以把多个数据源配置成一个Map,然后,根
据不同的key返回不同的数据源。因为AbstractRoutingDataSource也是一个DataSource接口,因
此,应用程序可以先设置好key, 访问数据库的代码就可以从AbstractRoutingDataSource拿到对
应的一个真实的数据源,从而访问指定的数据库
public abstract class AbstractRoutingDataSource extends AbstractDataSource
implements InitializingBean {
.......
/**
* Specify the map of target DataSources, with the lookup key as key.
* The mapped value can either be a corresponding {@link
javax.sql.DataSource}
* instance or a data source name String (to be resolved via a
* {@link #setDataSourceLookup DataSourceLookup}).
* <p>The key can be of arbitrary type; this class implements the
* generic lookup process only. The concrete key representation will
* be handled by {@link #resolveSpecifiedLookupKey(Object)} and
* {@link #determineCurrentLookupKey()}.
*/
//翻译如下
/**
*指定目标数据源的映射,查找键为键。
*映射的值可以是相应的{@link javax.sql.DataSource}
*实例或数据源名称字符串(要通过
* {@link #setDataSourceLookup DataSourceLookup})。
*键可以是任意类型的; 这个类实现了
*通用查找过程只。 具体的关键表示将
*由{@link #resolveSpecifiedLookupKey(Object)}和
* {@link #determineCurrentLookupKey()}。
*/
public void setTargetDataSources(Map<Object, Object> targetDataSources)
{
this.targetDataSources = targetDataSources;
} .
.....
/**
* Determine the current lookup key. This will typically be
* implemented to check a thread-bound transaction context.
* <p>Allows for arbitrary keys. The returned key needs
* to match the stored lookup key type, as resolved by the
* {@link #resolveSpecifiedLookupKey} method.
*/
//翻译如下
/**
* 确定当前的查找键。这通常会
* 实现以检查线程绑定的事务上下文。
* <p> 允许任意键。返回的密钥需要
* 与存储的查找密钥类型匹配, 如
* {@link #resolveSpecifiedLookupKey} 方法。
*/
protected abstract Object determineCurrentLookupKey();
}
推荐的方式是创建一个类来继承它并且实现它的determineCurrentLookupKey() 方法
第一步:配置多数据源
spring.druid.datasource.master.password=root
spring.druid.datasource.master.username=root
spring.druid.datasource.master.jdbcurl=jdbc:mysql://localhost:3306/product_master?
useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
spring.druid.datasource.master.driver-class-name=com.mysql.cj.jdbc.Driver
spring.druid.datasource.slave.password=root
spring.druid.datasource.slave.username=root
spring.druid.datasource.slave.jdbcurl=jdbc:mysql://localhost:3306/product_slave?
useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
spring.druid.datasource.slave.driver-class-name=com.mysql.cj.jdbc.Driver
在SpringBoot的配置代码中,我们初始化两个数据源
@Configuration
public class MyDataSourceConfiguratioin {
Logger logger =
LoggerFactory.getLogger(MyDataSourceConfiguratioin.class);
/**
* Master data source.
*/
@Bean("masterDataSource")
@ConfigurationProperties(prefix = "spring.druid.datasource.master")
DataSource masterDataSource() {
logger.info("create master datasource...");
return DataSourceBuilder.create().build();
} /
**
* Slave data source.
*/
@Bean("slaveDataSource")
@ConfigurationProperties(prefix = "spring.druid.datasource.slave")
DataSource slaveDataSource() {
logger.info("create slave datasource...");
return DataSourceBuilder.create().build();
}
}
@Bean
@Primary
DataSource primaryDataSource(
@Autowired @Qualifier("masterDataSource") DataSource
masterDataSource,
@Autowired @Qualifier("slaveDataSource") DataSource
slaveDataSource
) {
logger.info("create routing datasource...");
Map<Object, Object> map = new HashMap<>();
map.put("masterDataSource", masterDataSource);
map.put("slaveDataSource", slaveDataSource);
RoutingDataSource routing = new RoutingDataSource();
routing.setTargetDataSources(map);
routing.setDefaultTargetDataSource(masterDataSource);
return routing;
}
第二步:编写RoutingDataSource
public class RoutingDataSourceContext {
// holds data source key in thread local:
static final ThreadLocal<String> threadLocalDataSourceKey = new
ThreadLocal<>();
public static String getDataSourceRoutingKey() {
String key = threadLocalDataSourceKey.get();
return key == null ? "masterDataSource" : key;
}
public RoutingDataSourceContext(String key) {
threadLocalDataSourceKey.set(key);
}
public void close() {
threadLocalDataSourceKey.remove();
}
}
使用ThreadLocal来存放动态选择的数据源名称“master”还是“slave”
public class RoutingDataSource extends AbstractRoutingDataSource {
protected Object determineCurrentLookupKey() {
return RoutingDataSourceContext.getDataSourceRoutingKey();
}
}
编写 @RoutingWith(“slaveDataSource”) 注解,放到某个Controller的方
法上,使用AOP自动选择对应的数据源。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RoutingWith {
String value() default "master";
}
添加aop依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
切面类
@Aspect
@Component
public class RoutingAspect {
@Around("@annotation(routingWith)")
public Object routingWithDataSource(ProceedingJoinPoint joinPoint,
RoutingWith routingWith) throws Throwable {
String key = routingWith.value();
RoutingDataSourceContext ctx = new
RoutingDataSourceContext(key);
return joinPoint.proceed();
}
}
使用
@RoutingWith("masterDataSource")
@GetMapping("/findAllProductM")
public String findAllProductM() {
/* String key = "masterDataSource";
RoutingDataSourceContext routingDataSourceContext = new
RoutingDataSourceContext(key);*/
productService.findAllProductM();
return "lagou";
}
@RoutingWith("slaveDataSource")
@GetMapping("/findAllProductS")
public String findAllProductS() {
/*String key = "slaveDataSource";
RoutingDataSourceContext routingDataSourceContext = new
RoutingDataSourceContext(key);*/
productService.findAllProductS();
return "lagou";
}
第四部分:SpringBoot缓存深入
4.1 JSR107
Java Caching(JSR-107)定义了5个核心接口,分别是CachingProvider、CacheManager、Cache、Entry和Expiry。
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设置
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sakL1zek-1623652323186)(/Users/jarry/Library/Application Support/typora-user-images/image-20210613193514354.png)]
4.2 Spring的缓存抽象
Spring从3.1开始定义了org.springframework.cache.Cache
和org.springframework.cache.CacheManager接口来统一不同的缓存技术;并支持使用Java
Caching(JSR-107)注解简化我们进行缓存开发。
Spring Cache 只负责维护抽象层,具体的实现由自己的技术选型来决定。将缓存处理和缓存技术
解除耦合。
4.3 Spring缓存使用
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qbvac8ct-1623652323187)(/Users/jarry/Library/Application Support/typora-user-images/image-20210613201500633.png)]
说明:
① @Cacheable标注在方法上,表示该方法的结果需要被缓存起来,缓存的键由keyGenerator的
策略决定,缓存的值的形式则由serialize序列化策略决定(序列化还是json格式);标注上该注解之
后,在缓存时效内再次调用该方法时将不会调用方法本身而是直接从缓存获取结果
② @CachePut也标注在方法上,和@Cacheable相似也会将方法的返回值缓存起来,不同的是标
注@CachePut的方法每次都会被调用,而且每次都会将结果缓存起来,适用于对象的更新
@Cacheable注解的属性
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N3U8lvOE-1623652323188)(/Users/jarry/Library/Application Support/typora-user-images/image-20210613201754103.png)]
注:
①既满足condition又满足unless条件的也不进行缓存
②使用异步模式进行缓存时(sync=true):unless条件将不被支持
可用的SpEL表达式见下表:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-M8fbJhvk-1623652323188)(/Users/jarry/Library/Application Support/typora-user-images/image-20210613201858407.png)]
4.4 缓存自动配置原理源码剖析
Spring.factories中找到CacheAutoConfiguration
在这个类中有一个静态内部类 CacheConfigurationImportSelector 他有一个 selectImport 方
法是用来给容器中添加一些缓存要用的组件;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-um86kceJ-1623652323190)(/Users/jarry/Library/Application Support/typora-user-images/image-20210613202120751.png)]
我们在这里打上断点,debug调试一下看看 imports 中有哪些缓存组件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aXNEWQgx-1623652323191)(/Users/jarry/Library/Application Support/typora-user-images/image-20210613202142191.png)]
默认情况下使用 SimpleCacheConfiguration ;
然后我们进入到 SimpleCacheConfiguration 中:
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(CacheManager.class)
@Conditional(CacheCondition.class)
class SimpleCacheConfiguration {
@Bean
ConcurrentMapCacheManager cacheManager(CacheProperties cacheProperties,
CacheManagerCustomizers cacheManagerCustomizers) {
ConcurrentMapCacheManager cacheManager = new
ConcurrentMapCacheManager();
List<String> cacheNames = cacheProperties.getCacheNames();
if (!cacheNames.isEmpty()) {
cacheManager.setCacheNames(cacheNames);
}
return cacheManagerCustomizers.customize(cacheManager);
}
}
我们会发现他给springBoot容器添加了一个bean,是一个 CacheManager ;
ConcurrentMapCacheManager 实现了 CacheManager 接口
再来看 ConcurrentMapCacheManager 的getCache方法
@Override
@Nullable
public Cache getCache(String name) {
Cache cache = this.cacheMap.get(name);
if (cache == null && this.dynamic) {
synchronized (this.cacheMap) {
cache = this.cacheMap.get(name);
if (cache == null) {
cache = createConcurrentMapCache(name);
this.cacheMap.put(name, cache);
}
}
}
return cache;
}
getCache 方法使用了双重锁校验(这种验证机制一般是用在单例模式中)
我们可以看到如果没有 Cache 会调用
cache = this.createConcurrentMapCache(name);
protected Cache createConcurrentMapCache(String name) {
SerializationDelegate actualSerialization = (isStoreByValue() ?
this.serialization : null);
return new ConcurrentMapCache(name, new ConcurrentHashMap<>(256),
isAllowNullValues(), actualSerialization);
}
这个方法会创建一个 ConcurrentMapCache 这个就是我们说的 Cache ;
public class ConcurrentMapCache extends AbstractValueAdaptingCache {
private final String name;
//存放缓存键值对
private final ConcurrentMap<Object, Object> store;
@Nullable
private final SerializationDelegate serialization;
4.5 @Cacheable源码分析
@Cacheable运行流程:
①方法运行之前,先去查询Cache(缓存组件),按照cacheNames指定的名字获取(CacheManager
先获取相应的缓存,第一次获取缓存如果没有Cache组件会自动创建)
②去Cache中查找缓存的内容,使用的key默认就是方法的参数:
key默认是使用keyGenerator生成的,默认使用的是SimpleKeyGenerator
SimpleKeyGenerator生成key的默认策略:
如果没有参数:key = new SimpleKey();
如果有一个参数:key = 参数的值
如果有多个参数:key = new SimpleKey(params);
③没有查到缓存就调用目标方法
④将目标方法返回的结果放进缓存中
总
结 :@Cacheable标注的方法在执行之前会先检查缓存中有没有这个数据,默认按照参数的值
为key查询缓存,如果没有就运行方法并将结果放入缓存,以后再来调用时直接使用缓存中的数
据
核心:
1、使用CacheManager(ConcurrentMapCacheManager)按照名字得到
Cache(ConcurrentMapCache)组件
2、key使用keyGenerator生成,默认使用SimpleKeyGenerator
4.6 基于Redis的缓存实现
引入Redis的starter
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
即可直接使用基于redis的cache,cache的配置类使用RedisCacheConfiguration
4.7 自定义RedisCacheManager
打开Spring Boot整合Redis组件提供的缓存自动配置类
RedisCacheConfiguration(org.springframework.boot.autoconfigure.cache包下的),查看该
类的源码信息,其核心代码如下
@Configuration
class RedisCacheConfiguration {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory
redisConnectionFactory,ResourceLoader
resourceLoader) {
RedisCacheManagerBuilder builder =
RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(this.determineConfiguration(resourceLoader.getClassLoader()))
;
List<String> cacheNames = this.cacheProperties.getCacheNames();
if(!cacheNames.isEmpty()) {
builder.initialCacheNames(new LinkedHashSet(cacheNames));
} r
eturn
(RedisCacheManager)this.customizerInvoker.customize(builder.build());
} p
rivate org.springframework.data.redis.cache.RedisCacheConfiguration
determineConfiguration(ClassLoader classLoader){
if(this.redisCacheConfiguration != null) {
return this.redisCacheConfiguration;
} else {
Redis redisProperties = this.cacheProperties.getRedis();
org.springframework.data.redis.cache.RedisCacheConfiguration
config =
org.springframework.data.redis.cache.RedisCacheConfiguration.defaultCacheCo
nfig();
config =
config.serializeValuesWith(SerializationPair.fromSerializer(
new
JdkSerializationRedisSerializer(classLoader)));
...
return config;
}
}
}
从上述核心源码中可以看出,RedisCacheConfiguration内部同样通过Redis连接工厂
RedisConnectionFactory定义了一个缓存管理器RedisCacheManager;同时定制
RedisCacheManager时,也默认使用了JdkSerializationRedisSerializer序列化方式。
如果想要使用自定义序列化方式的RedisCacheManager进行数据缓存操作,可以参考上述核心代
码创建一个名为cacheManager的Bean组件,并在该组件中设置对应的序列化方式即可
自定义RedisCacheManager
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory
redisConnectionFactory) {
// 分别创建String和JSON格式序列化对象,对缓存数据key和value进行转换
RedisSerializer<String> strSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jacksonSeial =
new Jackson2JsonRedisSerializer(Object.class);
// 解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jacksonSeial.setObjectMapper(om);
// 定制缓存数据序列化方式及时效
RedisCacheConfiguration config =
RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofDays(1))
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(strSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(jacksonSeial))
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager
.builder(redisConnectionFactory).cacheDefaults(config).build();
return cacheManager;
}
上述代码中,在RedisConfig配置类中使用@Bean注解注入了一个默认名称为方法名的
cacheManager组件。在定义的Bean组件中,通过RedisCacheConfiguration对缓存数据的key和
value分别进行了序列化方式的定制,其中缓存数据的key定制为StringRedisSerializer(即String
格式),而value定制为了Jackson2JsonRedisSerializer(即JSON格式),同时还使用
entryTtl(Duration.ofDays(1))方法将缓存数据有效期设置为1天