Spring Security 系列(1) DelegatingFilterProxy

Spring Security

DelegatingFilterProxy 初始化

帶入問題

  1. Spring Security 的過濾器鏈的入口在哪
  2. 非Spring Boot 項目 (簡稱 Spring)式與Spring Boot 項目(簡稱Spring Boot)DelegatingFilterProxy 的創建方式;

DelegatingFilterProxy

DelegatingFilterProxy 是整個Spring Security 過濾器鏈的入口,攔截所有的請求;最後交給 FilterChainProxy 處理

Spring DelegatingFilterProxy的初始化

過濾器創建需要提到 @HandlesTypes 註解和 ServletContainerInitializer 接口;Servlet 規範中指出容器啓動時通過jar中META-INF/services/javax.servlet.ServletContainerInitializer文件中的規定的並實現ServletContainerInitializer 接口的類,通過反射機制實例化這些類並調用onStart(Set<Class> cls,ServletContext sc)方法;通常這些類會被@HandlesTypes 修飾,在實例化這些類的同時查找@HandlesTypes 參數規定的類,將其作爲參數傳到onStart方法中。這裏只是瞭解下@HandlesTypesServletContainerInitializer 的用法,需要了解更多的可以參考 Servlet 4.0規範 官方文檔

創建一個ServletContainerInitializer 實現類如下;

@HandlesTypes(BootAppInitializer.class)
public class BootAppServletContainerInitializer implements ServletContainerInitializer {

    private Logger logger = LoggerFactory.getLogger(BootAppServletContainerInitializer.class);

    @Override
    public void onStartup(Set<Class<?>> cls, ServletContext ctx) throws ServletException {
            logger.info("BootAppInit-->onStart()");
            // 沒有找到BootAppInitializer相關的類,接口時該參數爲null
            if (cls==null ){
                return;
            }
            List<BootAppInitializer> initializers = new LinkedList<>();
            for (Class cs:cls){
                // 不是接口或抽象類
                if (!cs.isInterface()&&!Modifier.isAbstract(cs.getModifiers())){
                    try {
                        initializers.add((BootAppInitializer)cs.newInstance());
                    }catch (Throwable ex){
                        logger.error(ex.getMessage());
                    }

                }
            }

            if(initializers.isEmpty()){
                logger.warn("No BootAppInitializer");
            }
            // 調用onStart方法
            for (BootAppInitializer initializer:initializers){
                initializer.onStart(ctx);
            }

    }

}

在Spring中WebApplicationInitializerServletContainerInitializer的實現類,並在SpringServletContainerInitializer上使用了@HandlesTypes註解,指定收集WebApplicationInitializer的子類,其中AbstractSecurityWebApplicationInitializer 爲其子類;SpringServletContainerInitializer:onStart()方法核心代碼如下

@Override
public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
			throws ServletException {

		List<WebApplicationInitializer> initializers = new LinkedList<>();

		if (webAppInitializerClasses != null) {
			for (Class<?> waiClass : webAppInitializerClasses) {
				/* 判斷是否爲接口或抽象類 不是則收集,在集成Security 時要自己寫一個非抽象類實現
                 * AbstractSecurityWebApplicationInitializer抽象類,最後收集的就是自己寫的非抽象的實現類
				**/
                if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
						WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
					try {
						initializers.add((WebApplicationInitializer)
								ReflectionUtils.accessibleConstructor(waiClass).newInstance());
					}
					catch (Throwable ex) {
						throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
					}
				}
			}
		}

        .........
        // 循環調用收集到 {WebApplicationInitializer} 子類的onStart()方法
		for (WebApplicationInitializer initializer : initializers) {
			initializer.onStartup(servletContext);
		}
	}

啓動流程

具體過濾器鏈入口的配置與實例化的核心代碼如下,以下代碼刪除了部分代碼保留了創建springSecurityFilterChain的核心代碼,主要看中文註釋部分即可,入口在onStart方法

public abstract class AbstractSecurityWebApplicationInitializer
		implements WebApplicationInitializer {

	......

	public static final String DEFAULT_FILTER_NAME = "springSecurityFilterChain";

	
	public final void onStartup(ServletContext servletContext) throws ServletException {
		......
        beforeSpringSecurityFilterChain(servletContext);

        // 調用insertSpringSecurityFilterChain()創建
		insertSpringSecurityFilterChain(servletContext);
		
        afterSpringSecurityFilterChain(servletContext);
	}

	// 實例化Filter
	private void insertSpringSecurityFilterChain(ServletContext servletContext) {
		String filterName = DEFAULT_FILTER_NAME;
        // 實例化
		DelegatingFilterProxy springSecurityFilterChain = new DelegatingFilterProxy(
				filterName);
		String contextAttribute = getWebApplicationContextAttribute();
		if (contextAttribute != null) {
			springSecurityFilterChain.setContextAttribute(contextAttribute);
		}
		registerFilter(servletContext, true, filterName, springSecurityFilterChain);
	}

	
	// 配置FIlter的信息,
	private final void registerFilter(ServletContext servletContext,
			boolean insertBeforeOtherFilters, String filterName, Filter filter) {
        // 添加到 ServletContext,(讓Filter生效)
		Dynamic registration = servletContext.addFilter(filterName, filter);
		if (registration == null) {
			throw new IllegalStateException(
					"Duplicate Filter registration for '" + filterName
							+ "'. Check to ensure the Filter is only configured once.");
		}
		registration.setAsyncSupported(isAsyncSecuritySupported());
        /* dispatcherTypes getSecurityDispatcherTypes(){EnumSet.of(DispatcherType.REQUEST, DispatcherType.ERROR,
				DispatcherType.ASYNC)}
        */
		EnumSet<DispatcherType> dispatcherTypes = getSecurityDispatcherTypes();
        // 攔截的url (/*)
		registration.addMappingForUrlPatterns(dispatcherTypes, !insertBeforeOtherFilters,
				"/*");
	}
	
}

在onStart() 中 調用insertSpringSecurityFilterChain方法實例化 Filter ,之後調用registerFilter方法 配置Filter並添加到ServletContext中。ServletContext 初始化完成後,ContextLoaderListener 登場該類實現了ServletContextListener和繼承了ContextLoader並覆蓋了contextInitialized()方法,此方法會在ServletContext 初始完成後執行,具體方法如下,這個方法的具體方法initWebApplicationContext()實現在ContextLoader 中,這個方法完成了XmlWebApplicationContext 的初始化(bean收集等)。到這裏FilterChainProxy 已配置完成。

    @Override
	public void contextInitialized(ServletContextEvent event) {
		// 初始化XmlWebApplicationContext 並放入ServletContext 中
		initWebApplicationContext(event.getServletContext());
	}

這個方法執行完後成後便是各種Filter,HttpServlet,ServletRequestListener 等的初始化方法的調用,到這裏DelegatingFilterProxy 的init()方法也會執行,該方法會調用initFilterBean() 方法,最終DelegatingFilterProxy中的成員Filter (變量名:delegate) 被賦值,(從XmlWebApplicationContext 取出)

在這裏插入圖片描述

public class DelegatingFilterProxy extends GenericFilterBean {

	@Nullable
	private volatile Filter delegate;
	....
	@Override
	protected void initFilterBean() throws ServletException {
		synchronized (this.delegateMonitor) {
			if (this.delegate == null) {
				// If no target bean name specified, use filter name.
				if (this.targetBeanName == null) {
					this.targetBeanName = getFilterName();
				}
				// Fetch Spring root application context and initialize the delegate early,
				// if possible. If the root application context will be started after this
				// filter proxy, we'll have to resort to lazy initialization.
				WebApplicationContext wac = findWebApplicationContext();
				if (wac != null) {
					this.delegate = initDelegate(wac);
				}
			}
		}
	}

	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {

		// Lazily initialize the delegate if necessary.
		Filter delegateToUse = this.delegate;
		if (delegateToUse == null) {
			synchronized (this.delegateMonitor) {
				delegateToUse = this.delegate;
				if (delegateToUse == null) {
					WebApplicationContext wac = findWebApplicationContext();
					if (wac == null) {
						throw new IllegalStateException("No WebApplicationContext found: " +
								"no ContextLoaderListener or DispatcherServlet registered?");
					}
					delegateToUse = initDelegate(wac);
				}
				this.delegate = delegateToUse;
			}
		}

		// Let the delegate perform the actual doFilter operation.
		invokeDelegate(delegateToUse, request, response, filterChain);
	}



	@Nullable
	protected WebApplicationContext findWebApplicationContext() {
		if (this.webApplicationContext != null) {
			// The user has injected a context at construction time -> use it...
			if (this.webApplicationContext instanceof ConfigurableApplicationContext) {
				ConfigurableApplicationContext cac = (ConfigurableApplicationContext) this.webApplicationContext;
				if (!cac.isActive()) {
					// The context has not yet been refreshed -> do so before returning it...
					cac.refresh();
				}
			}
			return this.webApplicationContext;
		}
		String attrName = getContextAttribute();
		if (attrName != null) {
			return WebApplicationContextUtils.getWebApplicationContext(getServletContext(), attrName);
		}
		else {
			return WebApplicationContextUtils.findWebApplicationContext(getServletContext());
		}
	}

	
	protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
		String targetBeanName = getTargetBeanName();
		Assert.state(targetBeanName != null, "No target bean name set");
		Filter delegate = wac.getBean(targetBeanName, Filter.class);
		if (isTargetFilterLifecycle()) {
			delegate.init(getFilterConfig());
		}
		return delegate;
	}


	protected void invokeDelegate(
			Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {

		delegate.doFilter(request, response, filterChain);
	}
}

從上述的類圖和DelegatingFilterProxy 的部分代碼可以看出,執行最終邏輯的Filter爲FilterChainProxy,過濾器鏈已經初始化完成。接下來分析下Spring Boot 的DelegatingFilterProxy 初始化過程

Spring Boot DelegatingFilterProxy 初始化

在 Spring Boot 中 DelegatingFilterProxy 初始化在SecurityFilterAutoConfiguration 類中完成初始化,在這之前先看一個接口ServletContextInitializer關於接口的官方描述如下。

/**
  * Interface used to configure a Servlet 3.0+ context programmatically. 
  * Unlike WebApplicationInitializer, 
  * classes that implement this interface (and do not implement WebApplicationInitializer) will not be detected by SpringServletContainerInitializer and hence will not be automatically bootstrapped by the Servlet container.
*/
@FunctionalInterface
public interface ServletContextInitializer {

	/**
	 * Configure the given {@link ServletContext} with any servlets, filters, listeners
	 * context-params and attributes necessary for initialization.
	 * @param servletContext the {@code ServletContext} to initialize
	 * @throws ServletException if any call against the given {@code ServletContext}
	 * throws a {@code ServletException}
	 */
	void onStartup(ServletContext servletContext) throws ServletException;

}

官方註釋大概意思就是該接口用於編程的方式配置ServletContext,如果有類實現了該接口而沒有實現WebApplicationInitializer的話就不會被SpringServletContainerInitializer 檢測到,所以Servlet容器啓動時,不會自動調用該實現類的onStart()方法。

我們看一下DelegatingFilterProxyRegistrationBean類的繼承關係,該類間接實現了 ServletContextInitializer,該類主要用來初始化配置DelegatingFilterProxy。通過源碼分析一下DelegatingFilterProxyRegistrationBean 的onStart()方法什麼時候被調用,這裏只做簡單分析,並不會將每一行的代碼的意義都做分析。這裏先埋個坑,日後再做這個系列的文章。

在這裏插入圖片描述

Spring Boot 啓動流程的入口爲SpringApplication類的run方法,該方法如下(大部分已被省略…)。我們只重點關注流程,不做各個方法功能的具體分析,在這個方法中我們關注的重點是refreshContext(context)這個方法的調用,首先該方法在通過調用createApplicationContext() 以反射的方式創建並返回 AnnotationConfigServletWebServerApplicationContext 的實例,之後便是調用 refreshContext(context)context 作爲參數傳入,通過如下源碼可以看出在 SpringAbblication 這個類中的 refreshContext(context) 方法最後會調用 AbstractApplicationContextrefresh()方法。


public class SpringApplication {
	......
	public ConfigurableApplicationContext run(String... args) {
		.......
		ConfigurableApplicationContext context = null;
		
		try {
			.......
			// 通過反射的方式創建應用上下文,返回的是 AnnotationConfigServletWebServerApplicationContext 實例
			context = createApplicationContext();
		
			refreshContext(context);
			......
		}
		catch (Throwable ex) {
			.......
		}

		.......
	
		return context;
	}

	private void refreshContext(ConfigurableApplicationContext context) {
		refresh(context);
		if (this.registerShutdownHook) {
			try {
				context.registerShutdownHook();
			}
			catch (AccessControlException ex) {
				// Not allowed in some environments.
			}
		}
	}

	protected void refresh(ApplicationContext applicationContext) {
		Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext);
		((AbstractApplicationContext) applicationContext).refresh();
	}

	.......


}

AnnotationConfigServletWebServerApplicationContext 的繼承關係如下圖所示,在下圖中重點部分已經用紅框標出,接下來我們圍繞紅框標出的重點類的源碼進行分析。

在這裏插入圖片描述

通過上述的分析我們進入了 AbstractApplicationContextrefresh() 方法中,該方法如下(大部分被省略…),和之前的一樣我們只關注 onRefresh() 方法其他的跳過,該方法實際調用子類ServletWebServerApplicationContextonRefresh()

public abstract class AbstractApplicationContext extends DefaultResourceLoader
		implements ConfigurableApplicationContext {
	@Override
	public void refresh() throws BeansException, IllegalStateException {
		synchronized (this.startupShutdownMonitor) {
			......
			try {
				
				// Initialize other special beans in specific context subclasses.
				onRefresh();
				......
				
			}

			catch (BeansException ex) {
				.......
			}

			finally {
				.......
			}
		}
	}
}

ServletWebServerApplicationContext 的部分源碼如下,該方法中調用鏈 onRefresh()->createWebServer()->getWebServerFactory(),這裏使用的是Spring Boot默認的內嵌tomcat 所以 getWebServerFactory() 返回的的實例爲 TomcatServletWebServerFactory

public class ServletWebServerApplicationContext extends GenericWebApplicationContext
		implements ConfigurableWebServerApplicationContext {

	@Override
	protected void onRefresh() {
		super.onRefresh();
		try {
			createWebServer();
		}
		catch (Throwable ex) {
			throw new ApplicationContextException("Unable to start web server", ex);
		}
	}

	private void createWebServer() {
		WebServer webServer = this.webServer;
		ServletContext servletContext = getServletContext();
		if (webServer == null && servletContext == null) {
			// 默認Tomcat 返回的實例爲 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();
	}

	private org.springframework.boot.web.servlet.ServletContextInitializer getSelfInitializer() {
		return this::selfInitialize;
	}

	private void selfInitialize(ServletContext servletContext) throws ServletException {
		prepareWebApplicationContext(servletContext);
		registerApplicationScope(servletContext);
		WebApplicationContextUtils.registerEnvironmentBeans(getBeanFactory(),
				servletContext);
		for (ServletContextInitializer beans : getServletContextInitializerBeans()) {
			beans.onStartup(servletContext);
		}
	}

}

通過ServletWebServerFactorygetWebServer(ServletContextInitializer... initializers) 方法獲取一個 WebServer 實例,該方法傳入當前對象的 getSelfInitializer() 方法的返回值 ServletContextInitializer ;與其對應的方法體爲 selfInitialize(ServletContext servletContext),當某個方法中調用了當前實例ServletContextInitializeronStart(ServletContext servletContext) 相當於執行了selfInitialize(ServletContext servletContext)這個方法。(這裏需要去理解下@FunctionalInterface這個註解的作用及用法);我們繼續跟蹤進入TomcatServletWebServerFactorygetWebServer(ServletContextInitializer... initializers) 方法。

public class TomcatServletWebServerFactory extends AbstractServletWebServerFactory
		implements ConfigurableTomcatWebServerFactory, ResourceLoaderAware {

	@Override
	public WebServer getWebServer(ServletContextInitializer... initializers) {
		Tomcat tomcat = new Tomcat();
		File baseDir = (this.baseDirectory != null) ? this.baseDirectory
				: createTempDir("tomcat");
		tomcat.setBaseDir(baseDir.getAbsolutePath());
		Connector connector = new Connector(this.protocol);
		tomcat.getService().addConnector(connector);
		customizeConnector(connector);
		tomcat.setConnector(connector);
		tomcat.getHost().setAutoDeploy(false);
		configureEngine(tomcat.getEngine());
		for (Connector additionalConnector : this.additionalTomcatConnectors) {
			tomcat.getService().addConnector(additionalConnector);
		}
		prepareContext(tomcat.getHost(), initializers);
		return getTomcatWebServer(tomcat);
	}

	protected void prepareContext(Host host, ServletContextInitializer[] initializers) {
		File documentRoot = getValidDocumentRoot();
		TomcatEmbeddedContext context = new TomcatEmbeddedContext();
		......
		ServletContextInitializer[] initializersToUse = mergeInitializers(initializers);
		host.addChild(context);
		configureContext(context, initializersToUse);
		......
	}

	protected void configureContext(Context context,
			ServletContextInitializer[] initializers) {
		TomcatStarter starter = new TomcatStarter(initializers);
		......
		context.addServletContainerInitializer(starter, NO_CLASSES);
		......
	}

}

getWebServer方法中創建Tomcat實例並做了一些配置,該方法中的主要調用鏈 getWebServer()->prepareContext(tomcat.getHost(), initializers)->configureContext(context, initializersToUse),跟蹤進入configureContext(context, initializersToUse) 方法,其中以ServletContextInitializer[] initializers 作爲構造參數創建了TomcatStarter 實例,並將其添加到Tomcat的生命週期中,在Tomcat 啓動時會被調用它的 onStart(Set<Class<?>> classes,ServletContext context) 方法,具體的調用在StandardContext 的 5139 行左右,我們跟蹤進入 TomcatStarter

class TomcatStarter implements ServletContainerInitializer {
	......
	TomcatStarter(ServletContextInitializer[] initializers) {
		this.initializers = initializers;
	}
	@Override
	public void onStartup(Set<Class<?>> classes, ServletContext servletContext)
			throws ServletException {
		try {
			for (ServletContextInitializer initializer : this.initializers) {
				initializer.onStartup(servletContext);
			}
		}
		catch (Exception ex) {
			this.startUpException = ex;
			// Prevent Tomcat from logging and re-throwing when we know we can
			// deal with it in the main thread, but log for information here.
			if (logger.isErrorEnabled()) {
				logger.error("Error starting Tomcat context. Exception: "
						+ ex.getClass().getName() + ". Message: " + ex.getMessage());
			}
		}
	}

	public Exception getStartUpException() {
		return this.startUpException;
	}

}

TomcatStarter 類中的實現並不複雜,只是循環執行 ServletContextInitializeronStart方法,其中一個就是ServletWebServerApplicationContext 中的 selfInitialize 方法,在該方法中就是對ServletContextInitializer 的子類進行onStart方法的調用,我們關注的DelegatingFilterProxyRegistrationBean就是其中一個子類,到這裏我們回到之前的方法調用鏈getWebServer()->prepareContext(tomcat.getHost(), initializers)->configureContext(context, initializersToUse),分析到這步就是configureContext(context, initializersToUse) 執行完成,回到TomcatServletWebServerFactory 類的getWebServer() 方法中,之後便是調用getTomcatWebServer(tomcat) 方法獲取一個WebServer 實例,我們繼續跟蹤。(爲了方便再把上面的源碼在弄一份下來)

public class TomcatServletWebServerFactory extends AbstractServletWebServerFactory
		implements ConfigurableTomcatWebServerFactory, ResourceLoaderAware {

	@Override
	public WebServer getWebServer(ServletContextInitializer... initializers) {
		Tomcat tomcat = new Tomcat();
		......
		prepareContext(tomcat.getHost(), initializers);
		return getTomcatWebServer(tomcat);
	}

	protected void prepareContext(Host host, ServletContextInitializer[] initializers) {
		File documentRoot = getValidDocumentRoot();
		TomcatEmbeddedContext context = new TomcatEmbeddedContext();
		......
		ServletContextInitializer[] initializersToUse = mergeInitializers(initializers);
		host.addChild(context);
		configureContext(context, initializersToUse);
		......
	}

	protected void configureContext(Context context,
			ServletContextInitializer[] initializers) {
		TomcatStarter starter = new TomcatStarter(initializers);
		......
		context.addServletContainerInitializer(starter, NO_CLASSES);
		......
	}
	protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
		return new TomcatWebServer(tomcat, getPort() >= 0);
	}
}

從上述源碼中可以發現getTomcatWebServer(Tomcat tomcat) 只是一個簡單用 new 關鍵字實例化一個TomcatWebServer實例,跟蹤進入 TomcatWebServer

public class TomcatWebServer implements WebServer {
	......
	public TomcatWebServer(Tomcat tomcat, boolean autoStart) {
		Assert.notNull(tomcat, "Tomcat Server must not be null");
		this.tomcat = tomcat;
		this.autoStart = autoStart;
		initialize();
	}
	private void initialize() throws WebServerException {
		logger.info("Tomcat initialized with port(s): " + getPortsDescription(false));
		synchronized (this.monitor) {
			try {
				addInstanceIdToEngineName();

				Context context = findContext();
				context.addLifecycleListener((event) -> {
					if (context.equals(event.getSource())
							&& Lifecycle.START_EVENT.equals(event.getType())) {
						// Remove service connectors so that protocol binding doesn't
						// happen when the service is started.
						removeServiceConnectors();
					}
				});

				// Start the server to trigger initialization listeners
				this.tomcat.start();

				// We can re-throw failure exception directly in the main thread
				rethrowDeferredStartupExceptions();

				try {
					ContextBindings.bindClassLoader(context, context.getNamingToken(),
							getClass().getClassLoader());
				}
				catch (NamingException ex) {
					// Naming is not enabled. Continue
				}
				// Unlike Jetty, all Tomcat threads are daemon threads. We create a
				// blocking non-daemon to stop immediate shutdown
				startDaemonAwaitThread();
			}
			catch (Exception ex) {
				stopSilently();
				throw new WebServerException("Unable to start embedded Tomcat", ex);
			}
		}
	}
}

TomcatWebServer 的構造函數中執行了initialize() 啓動 Tomcat。分析到這裏Tomcat 啓動,TomcatStarteronStart 方法會被調用,最終DelegatingFilterProxyRegistrationBeanonStart 也會被調用。接下來我們分析當DelegatingFilterProxyRegistrationBean 的onStart調用之後 DelegatingFilterProxy 在哪裏被實例化。
在這裏插入圖片描述

根據上圖紅框圈出幾個重要的類,onStart 方法被定義在 RegistrationBean 中,onStart被調用的時候調用定義在DynamicRegistrationBean 中的的 register(String description, ServletContext servletContext) 方法,在register 中調用addRegistration()->getFilter() (部分方法參數省略)。getFilter定義在DelegatingFilterProxyRegistrationBean 類中

public class DelegatingFilterProxyRegistrationBean
		extends AbstractFilterRegistrationBean<DelegatingFilterProxy>
		implements ApplicationContextAware {
	......
	@Override
	public DelegatingFilterProxy getFilter() {
		return new DelegatingFilterProxy(this.targetBeanName,
				getWebApplicationContext()) {

			@Override
			protected void initFilterBean() throws ServletException {
				// Don't initialize filter bean on init()
			}

		};
	}
	......
}

getFilter() 方法直接new 創建並返回DelegatingFilterProxy 之後就是各種配置這裏就不在贅述,分析到這裏DelegatingFilterProxy 已經被實例並配置到 ServletContext 中。

總結

  1. 本篇講述了DelegatingFilterProxy作用。
  2. 從兩個角度(Spring 和Spring Boot)分析了 DelegatingFilterProxy 的初始化過程。
  3. 在Spring Boot 中還從源碼的角度細緻的進行跟蹤,簡單瞭解Spring Boot的啓動流程。

微信公衆號[UitBG]
在這裏插入圖片描述
GitHub 博客地址

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章