沧澜的博客

芝兰生于幽谷,不以无人而不芳


  • 首页

  • 归档

  • 分类

  • 标签

  • 搜索
软件思想 SpringBoot 领域驱动设计 算法 中间件 计算机网络 MySQL 数据库 javascript 极客时间 分布式架构 Jenkins JVM 多线程 Java基础 CentOS安装 编译OpenJDK 持续集成 杂谈

SpringBoot核心思想及源码解析(下)——启动原理

发表于 2021-09-23 | 分类于 Spring源码专题 | 0 | 阅读次数 188

引言

得益于SpringBoot这个脚手架封装了很多繁琐的操作,我们只需要通过java -jar一行命令便启动了一个Web服务器;再也不需要搭建tomcat等相关服务,这里我们就来深入探究一下SpringBoot容器启动的原理。

这篇文章是接着上文SpringBoot核心思想及源码解析——自动装配的下篇,主要分析SpringBoot的启动原理,主要包括三部分:启动Jar包、创建Bean容器和内嵌Tomcat的原理,这里我们逐一分析。


1. SpringBoot中jar包启动原理

当我们执行java -jar做了什么?

在oracle官网找到了该命令的描述:

Executes a program encapsulated in a JAR file. The filename argument is the name of a JAR file with a manifest that contains a line in the form Main-Class:classname that defines the class with the public static void main(String[] args) method that serves as your application's starting point.

大概意思就是说:执行封装在 JAR 文件中的程序。 filename 参数是带有MANIFEST的 JAR 文件的名称,该MANIFEST包含 Main-Class:classname 形式的一行,该行定义使用 public static void main(String[] args) 方法作为应用程序启动类。

说人话就是:使用java -jar时会去找jar中的MANIFEST文件中找到Main-Class标明的类,作为启动类启动

我们可以打开我们Springboot打好包的jar文件,可以看到在META-INF文件夹下有一个MANIFEST.MF文件,文件内容如下

Manifest-Version: 1.0
...// 省略无关紧要的内容
Start-Class: com.thoughtworks.mini.springboot.config.OriginStarter
...// 省略无关紧要的内容
Main-Class: org.springframework.boot.loader.JarLauncher

其中Main-Class是JarLauncher,而Start-Class OriginStarter才是我们指定的启动类,这里打开看了才发现实际上java -jar命令只会启动JarLauncher却导致了OriginStarter被执行了,这里面发生了什么?为什么要这样做呢?

原因在于这里是一个fat jar(俗称肥胖包,把所有依赖也打进了jar包),而:

Java没有提供任何标准的方式来加载嵌套的jar文件(简单说:它们本身包含在jar中的jar文件,想要直接运行需要涉及自定义开发)

SpringBoot打包插件将依赖打包

SpringBoot在pom依赖中默认使用以下插件打包:

    <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>

执行打包命令mvn clean package之后会生成两个文件:

springboot-0.0.1-SNAPSHOT.jar
springboot-0.0.1-SNAPSHOT.jar.original

spring-boot-maven-plugin项目存在于spring-boot-tools目录中,打包时默认执行的是repackag命令,对应代码层面在于RepackageMojo的execute方法,这里不再详细解析,读者感兴趣可以自行跟读。主要的逻辑是:spring-boot-maven-plugin的repackage能够将mvn package生成的软件包,再次打包为可执行的软件包,并将mvn package生成的软件包重命名为*.original

生成新的Jar包目录结构为:

springboot-0.0.1-SNAPSHOT.jar
+---BOOT-INF
|   |   classpath.idx
|   |   layers.idx
|   +---classes
|   |   \---用户程序启动类
|   \---lib
|        第三方依赖包...
+---META-INF
|   |   MANIFEST.MF
|   \---maven
|       \---pom文件相关
\---org
    \---springframework
        \---boot
            \---loader
                SpringBoot程序启动类及其依赖等

简单说就是spring-boot-maven-plugin插件帮我们重新将jar包组织,将依赖也打进了新的jar包内,而且更改了MANIFEST文件的内容,将Main-Class修改为SpringBoot程序启动类

SpingBoot自定义启动类JarLauncher

  • 引入相关依赖进行调试
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-loader</artifactId>
    </dependency>

从MANIFEST.MF可以看到Main函数是JarLauncher,下面来分析它的工作流程。JarLauncher存在于spring-boot-loader包,调试的话可以自行引入(调试jar的操作可以直接google),它的继承结构如下:

image.png

JarLauncher的逻辑比较简单,具体如下:

public class JarLauncher extends ExecutableArchiveLauncher {
	public JarLauncher() {
	}

	public static void main(String[] args) throws Exception {
		new JarLauncher().launch(args);
	}
}

public abstract class ExecutableArchiveLauncher extends Launcher {
	public ExecutableArchiveLauncher() {
		try {
			// 找到自己所在的jar,并创建Archive
			this.archive = createArchive();
			this.classPathIndex = getClassPathIndex(this.archive);
		}
		catch (Exception ex) {
			throw new IllegalStateException(ex);
		}
	}
}

主入口新建了 JarLauncher 并调用父类 Launcher 中的launch 方法启动程序。在创建JarLauncher时,父类ExecutableArchiveLauncher找到自己所在的jar,并根据资源路径创建Archive(Archive即归档文件,SpringBoot抽象了Archive的概念,JarFileArchive是jar资源的抽象,ExplodedArchive是对文件目录的抽象,统一抽象为资源的逻辑层,方便在对应目录中寻找资源)。

launch(args)这里主要做了两件事:

  1. 根据JarLauncher的Jar(fat jar)构造JarFileArchive对象,根据jar中的所有资源信息构建了自定义类加载器LaunchedURLClassLoader,该类加载器继承于URLClassLoader
  2. 使用类加载器加载Jar中的MANIFEST.MF文件,读取Start-Class指向的应用程序启动类,通过反射调用静态主方法,启动用户的应用程序(当遇到需要加载的类时会按照所需使用自定义的类加载器LaunchedURLClassLoader加载jar中jar包)
    (具体的逻辑这里不再深究,读者可自行查阅SpringBoot官方文档)

SpringBoot的Jar应用启动流程总结

  1. SpringBoot应用使用maven插件执行repackage打包后,重新生成了一个Fat jar,它是一个嵌套jar,包含了应用所依赖的jar和Spring-Boot-Leader启动相关的类
  2. 在使用java -jar命令时,会调用Fat jar中定义的启动类JarLauncher,这个类主要创建了一个自定义的类加载器LaunchedURLClassLoader来加载lib包目录下的所有jar包,并通过反射调用应用程序的启动类

2. SpringBoot启动Bean容器原理

SpringApplication构造逻辑

从jar启动后,我们就进到了我们的应用启动类,对于SpringBoot的应用启动类很简单,就一句代码:

    public static void main(String[] args) {
        SpringApplication.run(OriginStarter.class, args);
    }

对于这一句代码,主要调用的逻辑有:

	public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
		// 调用构造方法 创建一个SpringApplication对象
		return new SpringApplication(primarySources).run(args);
	}

	public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
		this.resourceLoader = resourceLoader;
		Assert.notNull(primarySources, "PrimarySources must not be null");
		// 将启动类放入primarySources
		this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
		// 根据classpath 下的类,推算当前web应用类型(webFlux, servlet)
		this.webApplicationType = WebApplicationType.deduceFromClasspath();
		// 找到所有spring.factories 文件中的key:org.springframework.context.ApplicationContextInitializer
		setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
		// 找到所有spring.factories 文件中的key:org.springframework.context.ApplicationListener
		setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
		// 根据main方法推算出mainApplicationClass
		this.mainApplicationClass = deduceMainApplicationClass();
	}

构造方法主要的逻辑有:

  1. 将启动类保存(它会做一个核心配置类)
  2. 设置web应用类型
  3. 读取了对外扩展的ApplicationContextInitializer和ApplicationListener
  4. 根据main方法推算所在的mainClass

run方法逻辑

	public ConfigurableApplicationContext run(String... args) {
		// 用来记录启动耗时
		StopWatch stopWatch = new StopWatch();
		stopWatch.start();
		// 接收一个Spring上下文对象
		ConfigurableApplicationContext context = null;
		Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
		// 开启了Headless模式 简单说就是 不需要硬件资源 显示器等 的一种服务器模式 提高计算效率和适配性
		configureHeadlessProperty();
		// 读取spring.factroies文件中 SpringApplicationRunListener 的组件, 就是用来发布事件或者运行监听器
		SpringApplicationRunListeners listeners = getRunListeners(args);
		// 发布ApplicationStartingEvent事件,在运行开始时就发送
		listeners.starting();
		try {
			// 根据命令行参数 初始化一个ApplicationArguments
			ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
			// 预初始化环境:读取环境变量,配置文件信息 (基于监听器实现的)
			ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
			// 设置beanInfo 信息 暂时不知道是啥意思
			configureIgnoreBeanInfo(environment);
			// 打印Banner 横幅
			Banner printedBanner = printBanner(environment);
			// 根据web的类型创建ApplicationContext上下文
			context = createApplicationContext();
			exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
					new Class[] { ConfigurableApplicationContext.class }, context);
			// 预初始化Spring上下文 会将传入的配置类读取成BeanDefinition
			prepareContext(context, environment, listeners, applicationArguments, printedBanner);
			// 加载spring ioc 容器 refresh相当重要 由于是使用AnnotationConfigServletWebServerApplicationContext
			// 启动的spring容器所以springboot对它做了扩展 嵌入Tomcat容器
			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);
		}

		try {
			listeners.running(context);
		}
		catch (Throwable ex) {
			handleRunFailure(context, ex, exceptionReporters, null);
			throw new IllegalStateException(ex);
		}
		return context;
	}

这个代码的层级就很清晰了,一个方法里包含了启动的逻辑,主要有:

  1. 读取 环境变量、配置信息
  2. 根据web容器类型,创建SpringApplication上下文对象:ServletWebServerApplicationContext
  3. 预初始化上下文对象,读取启动配置类
  4. 调用refresh方法:
    1. 加载所有的自动配置类
    2. 创建servlet容器(自动装配 tomcat 容器)

这里发现SpringBoot是和Spring息息相关的,并且扩展了refresh方法创建我们需要的web容器,它的主要流程我们画个图再细细理一下:

image.png


3. SpringBoot内嵌Tomcat原理

在学习这个内嵌原理之前,我建议先了解一下内嵌tomcat是怎么流程?首先引入相关依赖包:

        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-core</artifactId>
        </dependency>

以下是内嵌tomcat最简单的一个示例代码:

    public static void run() throws Exception {
        Tomcat tomcat = new Tomcat();
        tomcat.setBaseDir("target"); // 工作目录

        Connector connector = new Connector();
        connector.setPort(8080); // 端口号
        tomcat.getService().addConnector(connector);


        Context context = tomcat.addContext("/", null);

        // ServletContextInitializer
        context.addServletContainerInitializer((c, servletContext) -> {
            ServletRegistration.Dynamic helloServlet = servletContext.addServlet("ServletName", new HelloServlet());
            helloServlet.addMapping("/hello");
        }, null);


        tomcat.start();  // 启动tomcat
        tomcat.getServer().await(); // 挂起tomcat
    }

相关代码:github链接地址

上面是一个内嵌tomcat的简单示例,主要有以下几步:

  1. 创建一个Tomcat对象,并设置工作目录(上传文件和编译jsp等临时目录)
  2. 创建一个Connector对象(主要用于收发Http请求),并设置监听端口号
  3. 设置上下文根路径,在这个上下文中添加回调函数来注册Servlet(tomcat在容器启动之后会回调此方法),并添加映射路径
  4. 启动tomcat,监听服务请求并挂起
    (Servlet是J2EE 规范中的一种,Servlet接口定义的是一套处理网络请求的规范)

搞懂了内嵌Tomcat的基本操作,我们再看SpringBoot的逻辑,主要关注点:1. 创建的tomcat;2. 注册的DispatcherServlet;这里先回到ServletWebServerApplicationContext中的onRefresh方法:

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

1. 创建Tomcat容器

ServletWebServerApplicationContext.onRefresh()这个方法覆写了AbstractApplicationContext中的空方法(ApplicationContext就什么都没有做),在调用父类的onRefresh方法后,使用createWebServer创建了Web容器

	private void createWebServer() {
		WebServer webServer = this.webServer;
		// 内置的tomcat servletContext 为null 如果为外置的tomcat 则此处非null
		ServletContext servletContext = getServletContext();
		if (webServer == null && servletContext == null) {
			// 从容器中获得一个ServletWebServerFactory 此处来自于SpringBoot中的自动装配
			ServletWebServerFactory factory = getWebServerFactory();
			// 调用factory的getWebServer方法构造一个web容器,这里的参数引用了一个方法的返回值 返回一个ServletContextInitializer引用
			this.webServer = factory.getWebServer(getSelfInitializer());
		}
		// 外置tomcat 走这里
		else if (servletContext != null) {
			try {
				// 手动调用onStartup注册servlet容器
				getSelfInitializer().onStartup(servletContext);
			}
			catch (ServletException ex) {
				throw new ApplicationContextException("Cannot initialize servlet context", ex);
			}
		}
		initPropertySources();
	}

这里我们可以来看一下getWebServerFactory()方法的实现,因为onRefresh()在调用AbstractApplicationContext.invokeBeanFactoryPostProcessors方法之后,所有的配置类已经解析完成,我们可以从容器中拿到自动装配的TomcatServletWebServerFactory

	protected ServletWebServerFactory getWebServerFactory() {
		// Use bean names so that we don't consider the hierarchy
		// 从容器中获取ServletWebServerFactory 这里来自于ServletWebServerFactoryConfiguration自动装配
		String[] beanNames = getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class);
		if (beanNames.length == 0) {
			throw new ApplicationContextException("Unable to start ServletWebServerApplicationContext due to missing "
					+ "ServletWebServerFactory bean.");
		}
		if (beanNames.length > 1) {
			throw new ApplicationContextException("Unable to start ServletWebServerApplicationContext due to multiple "
					+ "ServletWebServerFactory beans : " + StringUtils.arrayToCommaDelimitedString(beanNames));
		}
		// 调用getBean生产Bean
		return getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class);
	}

其中ServletWebServerFactoryConfiguration中自动装配的代码如下:

	@Configuration(proxyBeanMethods = false)
	@ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class })
	@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
	static class EmbeddedTomcat {
		@Bean
		TomcatServletWebServerFactory tomcatServletWebServerFactory(
				ObjectProvider<TomcatConnectorCustomizer> connectorCustomizers,
				ObjectProvider<TomcatContextCustomizer> contextCustomizers,
				ObjectProvider<TomcatProtocolHandlerCustomizer<?>> protocolHandlerCustomizers) {
			TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
			factory.getTomcatConnectorCustomizers()
					.addAll(connectorCustomizers.orderedStream().collect(Collectors.toList()));
			factory.getTomcatContextCustomizers()
					.addAll(contextCustomizers.orderedStream().collect(Collectors.toList()));
			factory.getTomcatProtocolHandlerCustomizers()
					.addAll(protocolHandlerCustomizers.orderedStream().collect(Collectors.toList()));
			return factory;
		}
	}

这里我们可以看到,从自动装配中获益的Tomcat自动装配:

	@Override
	public WebServer getWebServer(ServletContextInitializer... initializers) {
		// 构造一个Tomcat容器
		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);
		}
		// 配置servlet回调函数
		prepareContext(tomcat.getHost(), initializers);
		return getTomcatWebServer(tomcat);
	}

2. 注册DispatcherServlet

到这里已经将tomcat容器创建出来了,我们进行第二步,查看DispatcherServlet在哪里注入的,回到createWebServer()方法中的getSelfInitializer()方法:

	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);
		// 从获取所有的ServletContextInitializer 调用它们的onStartup方法
		for (ServletContextInitializer beans : getServletContextInitializerBeans()) {
			beans.onStartup(servletContext);
		}
	}

这里我们可以回忆起SpringBoot注册中Servlet的方式:ServletRegistrationBean:

@Bean
public ServletRegistrationBean registerServlet() {
    ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(
            new RegisterServlet(), "/registerServlet");
    servletRegistrationBean.addInitParameter("name", "javastack");
    servletRegistrationBean.addInitParameter("sex", "man");
    return servletRegistrationBean;
}

它和ServletContextInitializer的关系如下:

image.png

就是说,ServletRegistrationBean继承自ServletContextInitializer,我可以直接用idea搜索DispatcherServletAuto*即可发现DispatcherServlet已经被DispatcherServletAutoConfiguration这个配置类自动装配了,它的逻辑如下:

	@Configuration(proxyBeanMethods = false)
	@Conditional(DefaultDispatcherServletCondition.class)
	@ConditionalOnClass(ServletRegistration.class)
	@EnableConfigurationProperties(WebMvcProperties.class)
	protected static class DispatcherServletConfiguration { //自动配置DispatcherServlet
		@Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
		public DispatcherServlet dispatcherServlet(WebMvcProperties webMvcProperties) {
			DispatcherServlet dispatcherServlet = new DispatcherServlet();
			dispatcherServlet.setDispatchOptionsRequest(webMvcProperties.isDispatchOptionsRequest());
			dispatcherServlet.setDispatchTraceRequest(webMvcProperties.isDispatchTraceRequest());
			dispatcherServlet.setThrowExceptionIfNoHandlerFound(webMvcProperties.isThrowExceptionIfNoHandlerFound());
			dispatcherServlet.setPublishEvents(webMvcProperties.isPublishRequestHandledEvents());
			dispatcherServlet.setEnableLoggingRequestDetails(webMvcProperties.isLogRequestDetails());
			return dispatcherServlet;
		}
	}

	@Configuration(proxyBeanMethods = false)
	@Conditional(DispatcherServletRegistrationCondition.class)
	@ConditionalOnClass(ServletRegistration.class)
	@EnableConfigurationProperties(WebMvcProperties.class)
	@Import(DispatcherServletConfiguration.class)
	protected static class DispatcherServletRegistrationConfiguration { //自动配置ServletRegistrationBean注册Servlet

		@Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)
		@ConditionalOnBean(value = DispatcherServlet.class, name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
		public DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet,
				WebMvcProperties webMvcProperties, ObjectProvider<MultipartConfigElement> multipartConfig) {
			DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet,
					webMvcProperties.getServlet().getPath());
			registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME);
			registration.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup());
			multipartConfig.ifAvailable(registration::setMultipartConfig);
			return registration;
		}
	}

而ServletContextInitializer的方法入口为:onStartup,这个类的实现是一个模板方法,其中注册Servlet的逻辑为:

public abstract class RegistrationBean implements ServletContextInitializer, Ordered {
	@Override // ServletContextInitializer接口的实现
	public final void onStartup(ServletContext servletContext) throws ServletException {
		// 获取Servlet的名字
		String description = getDescription();
		if (!isEnabled()) {
			logger.info(StringUtils.capitalize(description) + " was not registered (disabled)");
			return;
		}
		// 调用register方法,这里可以注册Filter、Servlet 分别对应不同的实现
		register(description, servletContext);
	}
}

public abstract class DynamicRegistrationBean<D extends Registration.Dynamic> extends RegistrationBean {
	@Override // 覆写RegistrationBean中的register方法
	protected final void register(String description, ServletContext servletContext) {
		// 调用addRegistration方法注册 Filter、Servlet 分别对应不同的实现
		D registration = addRegistration(description, servletContext);
		if (registration == null) {
			logger.info(StringUtils.capitalize(description) + " was not registered (possibly already registered?)");
			return;
		}
		// 调用configure 配置 Filter、Servlet 分别对应不同的实现
		configure(registration);
	}
}

// 这里仅关注Servlet的部分
public class ServletRegistrationBean<T extends Servlet> extends DynamicRegistrationBean<ServletRegistration.Dynamic> {
	@Override // 覆写DynamicRegistrationBean中的实现
	protected ServletRegistration.Dynamic addRegistration(String description, ServletContext servletContext) {
		String name = getServletName();
		// 注册Servlet
		return servletContext.addServlet(name, this.servlet);
	}
}

至此,SpringBoot已经将DispatcherServlet注册到Tomcat容器里了,我们也可以正常访问我们的后台程序,这里在画一张图进行一个小节:

image.png

上面只包含了部分主逻辑,可以按照这个线路来继续深入学习,主要包含两大块:1. 创建web容器 2. 注册Servlet。

小节

本文主要是从SpringBoot打包Jar原理,装配Bean容器,再到内嵌tomcat原理中逐一解析:

  1. SpringBoot 将依赖打进了fat jar中之后,虽然java提供任何标准的方式来加载嵌套的jar文件,但是SpringBoot重写了fat jar中的主函数入口,从而封装了自定义的类加载器来解决这一问题,再通过反射调用用户的程序类,完成SpringBoot的fat jar启动应用程序。
  2. 在应用程序入口,逐渐解析传入的参数,也根据当前引入依赖的类型而创建了Spring的Servlet Bean容器
  3. 这个Servlet Bean容器唯一的区别就是在ApplicationContext中覆写了onRefresh方法(SpringBoot根据引入的starter自动装配Web容器所需要的Bean),主要逻辑是:根据Servlet容器类型创建对应的Web容器,此处默认为tomcat,完成容器的内嵌tomcat启动
  • 本文作者: 沧澜
  • 本文链接: https://www.meetxiyu.cn/archives/SpringBoot核心思想及源码解析(下)——启动原理
  • 版权声明: 本博客所有文章除特别声明外,均采用CC BY-NC-SA 3.0 许可协议。转载请注明出处!
# 软件思想 # SpringBoot # 领域驱动设计 # 算法 # 中间件 # 计算机网络 # MySQL # 数据库 # javascript # 极客时间 # 分布式架构 # Jenkins # JVM # 多线程 # Java基础 # CentOS安装 # 编译OpenJDK # 持续集成 # 杂谈
个人技术的思考和自我反思
领域驱动设计(DDD)复杂的术语——软件设计(二)
  • 文章目录
  • 站点概览
沧澜

沧澜

芝兰生于幽谷,不以无人而不芳
君子修身养德,不以穷困而改志

74 日志
19 分类
19 标签
RSS
Creative Commons
0%
© 2019 — 2026 蜀ICP备19039166号
由 Halo 强力驱动
|
主题 - NexT.Mist v5.1.4