本文已参与「新人创作礼」活动,一起开启掘金创作之路。
在工作中我们往往会用到了Spring框架做后端开发,从SpringBoot框架诞生后,开发的敏捷性得到了大幅度提升,但作为一个技术工程师,想寻求技术的突破,光学会用是远远不够的,例如Spring的核心容器IOC是如何实现的,Aop原理,Web应用启动原理,如何与其他框架整合,SpringBoot的零配置启动原理。
学习的道路曲折且漫长,加油ヾ(◍°∇°◍)ノ゙!
首先我们先看一个Spring框架的UML类图, 主要是展现DispatcherServlet、FramworkServlet、HttpServletBean和ApplicationContextAware的关系,DispatcherServlet最终实现了Servlet接口。
接着看另外一个核心接口WebApplicationContext的UML类图
由UML类图可以发现WebApplicationContext有一个父接口ApplicationContext和一个子接口ConfigurableWebApplicationContext子接口, 其中ConfigurableWebApplicationContext有4个实现GenericWebApplicationContext、GroovyWebApplicationContext、StaticWebApplicationContext和XmlWebApplicationContext。
此处实现用到了策略模式,Spring容器默认使用的XmlWebApplicationContext,可以在FrameworkServlet里找到一个静态属性DEFAULT_CONTEXT_CLASS, 在没有找到web容器的情况下,那么就使用XmlWebApplicationContext容器。
public static final Class<?> DEFAULT_CONTEXT_CLASS = XmlWebApplicationContext.class;
上述的类和接口基本都是Spring框架的核心类,为了掌握启动原理,不光要熟悉这些类,我们可以从Servlet开始追踪溯源,其中ContextLoaderListener是Spring框架提供的一个web应用的实现类, 实现了ServletContextListener接口。
一、web应用启动原理分析
1. Web应用的启动监听器ContextLoaderListener
早期的应用开发者在利用Spring框架开发web应用时,我们需要在web.xml文件里添加一个web监听器的配置 org.springframework.web.context.ContextLoaderListener, 为什么要添加ContextLoaderListener这个配置呢?
我们可以从Spring源码中找到这个ContextLoaderListener类,查看源码:
由上图可以发现ContextLoaderListener类继承了ContextLoader类并实现了ServletContextListener接口,如果对servlet比较熟悉的话,该接口也是sevlet的监听器,主要的作用在于监听容器的启动,初始化容器,tomcat就是一个很好的例子,可以从javax.servlet-api-3.1.0.jar里找到源代码,ServletContextListener接口包含2个方法 ContextInitialized(ServletContextEvent sce)和contextDestroyed(ServletContextEvent sce),主要的功能是初始化ServletContext 容器和销毁ServletContext容器。
public interface ServletContextListener extends EventListener {
/**
* Receives notification that the web application initialization
* process is starting.
*
* <p>All ServletContextListeners are notified of context
* initialization before any filters or servlets in the web
* application are initialized.
*
* @param sce the ServletContextEvent containing the ServletContext
* that is being initialized
*/
public void contextInitialized(ServletContextEvent sce);
/**
* Receives notification that the ServletContext is about to be
* shut down.
*
* <p>All servlets and filters will have been destroyed before any
* ServletContextListeners are notified of context
* destruction.
*
* @param sce the ServletContextEvent containing the ServletContext
* that is being destroyed
*/
public void contextDestroyed(ServletContextEvent sce);
}
contextInitialized放发在Servlet容器启动时,就会执行我们可以在ContextLoaderListener类的源代码里找到实现,找到了initWebApplicationContext(event.getServletContext())方法:
@Override
public void contextInitialized(ServletContextEvent event) {
initWebApplicationContext(event.getServletContext());
}
包含了Spring容器的初始化逻辑:
2. Web应用初始化入口init()
servlet接口里的init()方法是servlet容器初始化的入口,也就是说我们可以通过实现Servlet接口在init()方法里定义初始化逻辑。 接着看Spring框架里的org.springframework.web.servlet.HttpServletBean, HttpServletBean是一个抽象类,实现了servlet接口里的Init()方法, 源码如下:
public abstract class HttpServletBean extends HttpServlet implements EnvironmentCapable, EnvironmentAware {
@Override
public final void init() throws ServletException {
if (logger.isDebugEnabled()) {
logger.debug("Initializing servlet '" + getServletName() + "'");
}
// Set bean properties from init parameters.
PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
if (!pvs.isEmpty()) {
try {
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
initBeanWrapper(bw);
bw.setPropertyValues(pvs, true);
}
catch (BeansException ex) {
if (logger.isErrorEnabled()) {
logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
}
throw ex;
}
}
// Let subclasses do whatever initialization they like.
// web 应用的启动入口 initServletBean()
initServletBean();
if (logger.isDebugEnabled()) {
logger.debug("Servlet '" + getServletName() + "' configured successfully");
}
}
...
}
该方法的作用是容器初始化的入口,从代码里可以找到一个方法initServletBean(),该方法在FrameWorkServlet类里实现,代码如下:
@SuppressWarnings("serial")
public abstract class FrameworkServlet extends HttpServletBean implements ApplicationContextAware {
/**
* Overridden method of {@link HttpServletBean}, invoked after any bean properties
* have been set. Creates this servlet's WebApplicationContext.
* web 容器启动入口
*/
@Override
protected final void initServletBean() throws ServletException {
getServletContext().log("Initializing Spring FrameworkServlet '" + getServletName() + "'");
if (this.logger.isInfoEnabled()) {
this.logger.info("FrameworkServlet '" + getServletName() + "': initialization started");
}
long startTime = System.currentTimeMillis();
try {
// 初始化web容器
this.webApplicationContext = initWebApplicationContext();
initFrameworkServlet();
}
catch (ServletException ex) {
this.logger.error("Context initialization failed", ex);
throw ex;
}
catch (RuntimeException ex) {
this.logger.error("Context initialization failed", ex);
throw ex;
}
if (this.logger.isInfoEnabled()) {
long elapsedTime = System.currentTimeMillis() - startTime;
this.logger.info("FrameworkServlet '" + getServletName() + "': initialization completed in " +
elapsedTime + " ms");
}
}
// 其他代码用...表示
...
}
我们发现FrameworkServlet类实现了一个ApplicationContextAware接口,该接口里只包含一个方法setApplicationContext(ApplicationContext applicationContext),在Spring中,我们知道如果有类实现了ApplicationContextAware接口,那么我们可以通过setApplicationContext(AppicationContext applicationContext)方法拿到applicationContext容器, WebApplicationContext接口是ApplicationContext的子接口,他们俩是一个父子容器的关系。
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
if (this.webApplicationContext == null && applicationContext instanceof WebApplicationContext) {
this.webApplicationContext = (WebApplicationContext) applicationContext;
this.webApplicationContextInjected = true;
}
}
从上述代码,我们可以发现webApplicationContext容器先通过setApplicationContext方法初始化赋值。
为什么在FramworkServlet的initServletBean方法里又做了一次初始webApplicationContext
的动作呢?
接着看initWebApplicationContext()方法,里面判断了一下wac能否转为ConfigurableWebApplciationContext, 如果能的话,那就继续判断是否需要设置父容器,如果需要的父容器那么设置父容器WebApplicationContext。
if (this.webApplicationContext != null) {
// A context instance was injected at construction time -> use it
wac = this.webApplicationContext;
if (wac instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
if (!cwac.isActive()) {
// The context has not yet been refreshed -> provide services such as
// setting the parent context, setting the application context id, etc
if (cwac.getParent() == null) {
// The context instance was injected without an explicit parent -> set
// the root application context (if any; may be null) as the parent
cwac.setParent(rootContext);
}
configureAndRefreshWebApplicationContext(cwac);
}
}
}
从上述代码中我们可以找到问题的答案,Web应用需要通过parent属性将各容器之间关联起来,如果出现找不到容器的情况下,那么会去创建一个默认的web容器, 该默认的容器是XmlWebApplicationContext。
if (wac == null) {
// No context instance is defined for this servlet -> create a local one
wac = createWebApplicationContext(rootContext);
}
一切初始化好后,那么就开始初始化mvc所有相关的策略,前提是没有收到refresh事件
if (!this.refreshEventReceived) {
// Either the context is not a ConfigurableApplicationContext with refresh
// support or the context injected at construction time had already been
// refreshed -> trigger initial onRefresh manually here.
onRefresh(wac);
}
接着看DispatcherServlet!
3. Spring-webmvc核心实现DispatcherServlet
MVC是现在非常流行一种web应用架构,Spring框架在启动IOC容器时在DispatcherSevlet类里的Onrefresh()方法实现了MVC的九大组件的初始化策略:
public class DispatcherServlet extends FrameworkServlet {
@Override
protected void onRefresh(ApplicationContext context) {
initStrategies(context);
}
....
}
初始化策略如下: 多文件上传、国际化、主题、处理器映射、处理器适配器、处理器异常解决方案、请求转换为视图转换器、视图解析器、Flashmap管理器。
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context);
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}
上述流程执行完毕后,Web应用的启动流程就基本结束了,最后把WebApplicationContext放入到ServletContext上下文里使用。
if (this.publishContext) {
// Publish the context as a servlet context attribute.
String attrName = getServletContextAttributeName();
getServletContext().setAttribute(attrName, wac);
if (this.logger.isDebugEnabled()) {
this.logger.debug("Published WebApplicationContext of servlet '" + getServletName() +
"' as ServletContext attribute with name [" + attrName + "]");
}
}
也就是在初始化的时候,先去找从ServletContext里去拿WebApplicationContext,如果没有拿到,那么再去createWebApplicationContext。
web容器初始化完毕后,WebApplicationContext也具有了Servlet相关的特性。
二、 Spring IOC容器的启动原理分析
从上述代码中可以发现,在启动web应用时,会伴随着IOC容器的启动,我们都知道Spring框架的核心是IOC,IOC简单的讲就是将对象的管理和依赖交给Spring去管理,其实Spring的IOC容器真正启动的时候是在调用AbstractApplicationContext.refresh()方法结束后,refresh()方法包含了Spring IOC容器启动的所有流程。
refresh()方法在AbstractApplicationContext类里有具体的实现:
refresh()方法源代码:
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing.
prepareRefresh();
// Tell the subclass to refresh the internal bean factory.
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);
try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);
// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);
// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);
// Initialize message source for this context.
initMessageSource();
// Initialize event multicaster for this context.
initApplicationEventMulticaster();
// Initialize other special beans in specific context subclasses.
onRefresh();
// Check for listener beans and register them.
registerListeners();
// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);
// Last step: publish corresponding event.
finishRefresh();
}
catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}
// Destroy already created singletons to avoid dangling resources.
destroyBeans();
// Reset 'active' flag.
cancelRefresh(ex);
// Propagate exception to caller.
throw ex;
}
finally {
// Reset common introspection caches in Spring's core, since we
// might not ever need metadata for singleton beans anymore...
resetCommonCaches();
}
}
}
IOC容器的启动是也就意味着Spring应用的启动成功,我们才能从IOC容器中拿到Bean。
三、Spring Boot 应用启动原理分析
Spring本身是一个低侵入、可扩展的轻量级框架,现在最流行的Springboot 框架也是基于Spring实现的。
可以从main()方法找到boot的启动路线以及原理, 进入到SpringApplication.run(String... args)方法:
我们可以发现最终调用到了AbstractApplicationContext.refresh()方法, 如下:
由此可见,Springboot一方面帮我们的开发简化了很多配置上的工作,另一方面具备Spring 框架所有的功能。
在SpringBoot中,DispatcherServlet是有新的请求过来的时候会初始化, 是在Tomcat容器初始化完成后:
访问localhost:8080, 可以发现控制台多了三条打印,其实FrameWorkServlet在初始化DispatcherServlet。
再次证明了initServletBean() 是初始化Servlet相关web容器的入口。
四、SpringBoot零配置启动原理
我们在使用SpringBoot框架时,不需要配置额外的spring、springmvc等相关的配置,由SpringBoot帮我们去实现自动化配置,我们不需要配置任何xml信息就可以启动一个最简单的SpringBoot应用。
因为在spring-boot-starter-web包里依赖了一个org.springframework.boot:spring-boot-autoconfigure包,autoconfigure包里实现了自动装配原理。
Spring.factories
spring.factories是Spring框架对外提供的一个注册式的SPI,可以扫描到除了@ComponenetScan以为的类,也就是说我们可以在resource目录下新建META-INF/spring.factories文件,然后只需要将接口的实现写入到该文件里,Spring 框架会扫描到所有的接口实现,从而管理这些类。
Spring-core包里有扫描Spring.factoires文件的实现SpringFactoriesLoader,所在路径为org.springframework.core.io.support.SpringFactoriesLoader.java, 可以发现此类里会扫描Spring应用的META-INF/spring.factories文件里的所有配置到一个ConcurrentHashMap里,
static final Map<ClassLoader, Map<String, List<String>>> cache = new ConcurrentReferenceHashMap();
接着看org.springframework.boot:spring-boot-autoconfigure2.5.12 包里的spring.factoires,找到org.springframework.boot.autoconfigure.EnableAutoConfiguration 这一栏,可以发现配置了一个List列表数据。
这里面配置了Spring应用所有相关的配置类:
举个栗子,org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration, DataSourceAutoConfiguration类实现了mysql数据源的属性配置注入:
我们只需要在application.properties文件里配置数据源即可:
spring.datasource.url=jdbc:mysql://${server.host}:${mysql.port}/db?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.jdbc.Driver