声明

源码基于Spring Boot 2.3.12.RELEASE

背景

此文的目的主要想弄明白为什么在Spring Boot中注册ServletFilterListener组件时需要加上@ServletComponentScan注解才能生效。

启动分析

Spring Boot应用程序的启动类一般如下所示

  • jar包启动
@SpringBootApplication
public class BootApplication { public static void main(String[] args) {
SpringApplication.run(BootApplication.class, args);
}
}
  • war包启动
@SpringBootApplication
public class BootApplication extends SpringBootServletInitializer { @Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(BootApplication.class);
}
}

下面主要分析jar包启动流程,war包其实最终走的也是jar包启动逻辑。只不过入口不是在main方法中调用而已,当war包部署在外部容器时,servlet容器会通过SPI寻找ServletContainerInitializer接口实现,然后调用它的onStartup方法。在spring-webjar包中,META-INF/service目录下便指定了一个实现类SpringServletContainerInitializer,入口便是该类了,该类会调用SpringApplication中的run方法。

现在来看SpringApplication

public static ConfigurableApplicationContext run(Class<?> primarySource,
String... args) {
return run(new Class<?>[] { primarySource }, args);
} public static ConfigurableApplicationContext run(Class<?>[] primarySources,
String[] args) {
return new SpringApplication(primarySources).run(args);
}

构造方法

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
// 配置类,一般为启动类
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
// 推断应用程序类型,主要根据类路径存不存在指定的类来推断,如SERVLET、REACTIVE、NONE
// 不同的类型会对应不同的ApplicationContext实现
this.webApplicationType = WebApplicationType.deduceFromClasspath();
// 从spring.factories文件中获取ApplicationContextInitializer实现
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
// 从spring.factories文件中获取ApplicationListener实现
// 注意这里的监听器不会注册到Spring容器中,而是在Spring Boot启动中依次触发,独立于Srping容器上下文
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
// 推断启动类
this.mainApplicationClass = deduceMainApplicationClass();
}

核心实例run方法

public ConfigurableApplicationContext run(String... args) {
// 用于记录启动时间
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// Spring容器
ConfigurableApplicationContext context = null;
configureHeadlessProperty();
// 用于管理SpringBoot启动过程中的声明周期,目前只有一个实现类
SpringApplicationRunListeners listeners = getRunListeners(args);
// starting生命周期
listeners.starting();
try {
// 封装参数
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
// 构建环境
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);
// 创建容器
context = createApplicationContext();
// 配置容器
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
// 刷新容器,实际调用applicationContext.refresh方法
refreshContext(context);
// 空实现
afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
// started生命周期
listeners.started(context);
// 调用ApplicationRunner、CommandLineRunner
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, listeners);
throw new IllegalStateException(ex);
} try {
// running生命周期
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, null);
throw new IllegalStateException(ex);
}
return context;
}

getRunListeners方法

/**
* 从spring.factories获取SpringApplicationRunListener实现,管理启动过程中的生命周期
* 目前只有EventPublishingRunListener一个实现,用于广播事件,与前文构造方法中提到的监听器一起工作
* 比如加载application.yml文件到环境中,就是通过它实现的
*/
private SpringApplicationRunListeners getRunListeners(String[] args) {
Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
return new SpringApplicationRunListeners(logger,
getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args));
}

prepareEnvironment方法

private ConfigurableEnvironment prepareEnvironment(
SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments) {
// Create and configure the environment
ConfigurableEnvironment environment = getOrCreateEnvironment();
configureEnvironment(environment, applicationArguments.getSourceArgs());
ConfigurationPropertySources.attach(environment);
/*
* 触发environmentPrepared生命周期,会广播ApplicationEnvironmentPreparedEvent事件
* 从而触发ConfigFileApplicationListener监听器,加载application.yml文件的属性到
* 环境中
*/
listeners.environmentPrepared(environment);
bindToSpringApplication(environment);
if (!this.isCustomEnvironment) {
environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
deduceEnvironmentClass());
}
ConfigurationPropertySources.attach(environment);
return environment;
}

createApplicationContext方法

/**
* 创建ApplicationContext对象
* servlet环境,实现类AnnotationConfigServletWebServerApplicationContext
* 响应式环境, 实现类AnnotationConfigReactiveWebServerApplicationContext
* 其它, 实现类AnnotationConfigApplicationContext
*/
protected ConfigurableApplicationContext createApplicationContext() {
Class<?> contextClass = this.applicationContextClass;
if (contextClass == null) {
try {
switch (this.webApplicationType) {
case SERVLET:
contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);
break;
case REACTIVE:
contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
break;
default:
contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
}
}
catch (ClassNotFoundException ex) {
throw new IllegalStateException(
"Unable create a default ApplicationContext, please specify an ApplicationContextClass", ex);
}
}
return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
}

prepareContext方法

/**
* 主要对ApplicationContext做一些初始化
*/
private void prepareContext(
ConfigurableApplicationContext context,
ConfigurableEnvironment environment,
SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments,
Banner printedBanner) {
// 设置环境
context.setEnvironment(environment);
postProcessApplicationContext(context);
// 调用前文构造方法中提到的ApplicationContextInitializer实例,对context做一些配置
applyInitializers(context);
listeners.contextPrepared(context);
if (this.logStartupInfo) {
logStartupInfo(context.getParent() == null);
logStartupProfileInfo(context);
}
// Add boot specific singleton beans
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
if (printedBanner != null) {
beanFactory.registerSingleton("springBootBanner", printedBanner);
}
if (beanFactory instanceof DefaultListableBeanFactory) {
((DefaultListableBeanFactory) beanFactory)
.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
}
if (this.lazyInitialization) {
context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
}
Set<Object> sources = getAllSources();
Assert.notEmpty(sources, "Sources must not be empty");
// 注册配置类
load(context, sources.toArray(new Object[0]));
listeners.contextLoaded(context);
}

refreshContext方法

private void refreshContext(ConfigurableApplicationContext context) {
// 注册钩子,jvm退出时会回调
if (this.registerShutdownHook) {
try {
context.registerShutdownHook();
}
catch (AccessControlException ex) {
// Not allowed in some environments.
}
}
/*
* 刷新容器,这里会创建tomcat容器,实例化IOC容器中所有的单例bean
*/
refresh((ApplicationContext) context);
} protected void refresh(ConfigurableApplicationContext applicationContext) {
applicationContext.refresh();
}

tomcat内嵌容器创建

createApplicationContext方法可知,servlet环境中,Spring容器实现类为AnnotationConfigServletWebServerApplicationContext,便来看看它的refresh方法。

该方法继承自父类AbstractApplicationContext,也就是Spring IOC容器的核心方法。

@Override
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(); /**
* 子类实现
*/
onRefresh(); // Check for listener beans and register them.
registerListeners(); // 初始化单例bean
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();
}
}
}

再看AnnotationConfigServletWebServerApplicationContextonRefresh方法。继承自ServletWebServerApplicationContext

@Override
protected void onRefresh() {
super.onRefresh();
try {
// 创建WebServer
createWebServer();
}
catch (Throwable ex) {
throw new ApplicationContextException("Unable to start web server", ex);
}
}
private void createWebServer() {
WebServer webServer = this.webServer;
ServletContext servletContext = getServletContext();
// 内嵌容器这两个都是null,外部容器servletContext不为null
if (webServer == null && servletContext == null) {
// 从IOC容器中获取工厂
ServletWebServerFactory factory = getWebServerFactory();
// 创建WebServer,其中getSelfInitializer是注册servlet,filter组件的核心入口
this.webServer = factory.getWebServer(getSelfInitializer());
getBeanFactory().registerSingleton("webServerGracefulShutdown",
new WebServerGracefulShutdownLifecycle(this.webServer));
getBeanFactory().registerSingleton("webServerStartStop",
new WebServerStartStopLifecycle(this, this.webServer));
}
else if (servletContext != null) {
try {
// 外部容器手动调用
getSelfInitializer().onStartup(servletContext);
}
catch (ServletException ex) {
throw new ApplicationContextException("Cannot initialize servlet context", ex);
}
}
initPropertySources();
}
/**
* 从IOC容器获取WebServer工厂
* WebServer工厂的自动配置类ServletWebServerFactoryAutoConfiguration
* 主要注册了tomcat、jetty、Undertow这3个servlet容器工厂,默认使用tomcat
*/
protected ServletWebServerFactory getWebServerFactory() {
// Use bean names so that we don't consider the hierarchy
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));
}
return getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class);
}

getWebServer方法,以tomcat为例,TomcatServletWebServerFactory.java

@Override
public WebServer getWebServer(ServletContextInitializer... initializers) {
if (this.disableMBeanRegistry) {
Registry.disableRegistry();
}
Tomcat tomcat = new Tomcat();
File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat");
tomcat.setBaseDir(baseDir.getAbsolutePath());
Connector connector = new Connector(this.protocol);
connector.setThrowOnFailure(true);
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();
if (documentRoot != null) {
context.setResources(new LoaderHidingResourceRoot(context));
}
context.setName(getContextPath());
context.setDisplayName(getDisplayName());
context.setPath(getContextPath());
File docBase = (documentRoot != null) ? documentRoot : createTempDir("tomcat-docbase");
// 熟悉的名字,tomcat部署应用程序的根目录
context.setDocBase(docBase.getAbsolutePath());
context.addLifecycleListener(new FixContextListener());
context.setParentClassLoader((this.resourceLoader != null) ? this.resourceLoader.getClassLoader()
: ClassUtils.getDefaultClassLoader());
resetDefaultLocaleMapping(context);
addLocaleMappings(context);
try {
context.setCreateUploadTargets(true);
}
catch (NoSuchMethodError ex) {
// Tomcat is < 8.5.39. Continue.
}
configureTldPatterns(context);
WebappLoader loader = new WebappLoader();
loader.setLoaderClass(TomcatEmbeddedWebappClassLoader.class.getName());
loader.setDelegate(true);
context.setLoader(loader);
if (isRegisterDefaultServlet()) {
addDefaultServlet(context);
}
if (shouldRegisterJspServlet()) {
addJspServlet(context);
addJasperInitializer(context);
}
context.addLifecycleListener(new StaticResourceConfigurer(context));
ServletContextInitializer[] initializersToUse = mergeInitializers(initializers);
host.addChild(context);
configureContext(context, initializersToUse);
postProcessContext(context);
}
protected void configureContext(Context context,
ServletContextInitializer[] initializers) {
// ServletContainerInitializer实现,并设置ServletContextInitializer列表
TomcatStarter starter = new TomcatStarter(initializers);
if (context instanceof TomcatEmbeddedContext) {
TomcatEmbeddedContext embeddedContext = (TomcatEmbeddedContext) context;
embeddedContext.setStarter(starter);
embeddedContext.setFailCtxIfServletStartFails(true);
}
/*
* 添加ServletContainerInitializer实现
* tomcat启动时会异步调用ServletContainerInitializer实例的onStartup方法
* 而TomcatStarter内部维护了ServletContextInitializer列表,依次调用
* ServletContextInitializer实例的onStartup方法,从而注册Servlet、Filter组件
*/
context.addServletContainerInitializer(starter, NO_CLASSES);
for (LifecycleListener lifecycleListener : this.contextLifecycleListeners) {
context.addLifecycleListener(lifecycleListener);
}
for (Valve valve : this.contextValves) {
context.getPipeline().addValve(valve);
}
for (ErrorPage errorPage : getErrorPages()) {
org.apache.tomcat.util.descriptor.web.ErrorPage tomcatErrorPage = new org.apache.tomcat.util.descriptor.web.ErrorPage();
tomcatErrorPage.setLocation(errorPage.getPath());
tomcatErrorPage.setErrorCode(errorPage.getStatusCode());
tomcatErrorPage.setExceptionType(errorPage.getExceptionName());
context.addErrorPage(tomcatErrorPage);
}
for (MimeMappings.Mapping mapping : getMimeMappings()) {
context.addMimeMapping(mapping.getExtension(), mapping.getMimeType());
}
configureSession(context);
new DisableReferenceClearingContextCustomizer().customize(context);
for (TomcatContextCustomizer customizer : this.tomcatContextCustomizers) {
customizer.customize(context);
}
}
protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
return new TomcatWebServer(tomcat, getPort() >= 0, getShutdown());
}
public TomcatWebServer(Tomcat tomcat, boolean autoStart, Shutdown shutdown) {
Assert.notNull(tomcat, "Tomcat Server must not be null");
this.tomcat = tomcat;
this.autoStart = autoStart;
this.gracefulShutdown = (shutdown == Shutdown.GRACEFUL) ? new GracefulShutdown(tomcat) : null;
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();
}
}); // 该方法会导致ServletContainerInitializer实例的onStartup执行
// 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();
destroySilently();
throw new WebServerException("Unable to start embedded Tomcat", ex);
}
}
}

总结

下面主要总结下比较在意的点

  • SpringApplication在创建时会从spring.factories文件中获取ApplicationContextInitializer以及ApplicationListener实现,其中ApplicationContextInitializer可以对ApplicationContext做一些定制。
  • run方法中接着从spring.factories文件中获取SpringApplicationRunListener实现,目前只有一个实现类,即EventPublishingRunListener,与前面ApplicationListener搭配使用,用于广播事件以及监听。这里的事件监听与ApplicationContext中的事件监听是相互独立的,毕竟有些事件触发时,应用上下文ApplicationContext还没有初始化好。其中application.yml属性文件加载到Environment中便是借助它实现的,SpringApplicationprepareEnvironment方法执行了environmentPrepared方法,从而广播了ApplicationEnvironmentPreparedEvent事件,而ConfigFileApplicationListener监听了该事件,会去执行application.yml属性文件的加载逻辑。
  • Environment初始化了之后,会创建ApplicationContext,对于Servlet环境,无论时内嵌容器还是运行在外部容器,实现类都是AnnotationConfigServletWebServerApplicationContext
  • 接着会调用refresh方法对ApplicationContext进行初始化,而refresh方法内部会调用onRefresh方法,然后再实例化单例bean。AnnotationConfigServletWebServerApplicationContext类在onRefresh方法中会实例化tomcat容器(默认是tomcat),其中添加了一个TomcatStarter类,这是一个ServletContainerInitializer实现,tomcat启动时会异步调用ServletContainerInitializer实例的onStartup方法,而TomcatStarteronStartup方法则会执行Spring Boot中ServletContextInitializer实例的onStartup方法,Spring Boot中注册ServletFilterListener便是通过ServletContextInitializer实现。这么做的目的是为了统一jar包运行和war包运行的差异,tomcat注册时只会在应用的部署目录搜索带有@WebServlet@WebFilter注解的类,war包运行时这没有任何问题,但是jar包运行时,我们的类文件并没有在tomcat指定的部署目录中,因此不会被注册。

黑魔法

为了验证上面所说的想法,来做以下一个实验,在上述tomcat创建过程时,会指定tomcat的docBase目录,即应用程序的部署目录,默认情况下是一个临时目录,为了方便测试,便指定一个目录。

/**
* 实现WebServerFactoryCustomizer接口,可自定义WebServerFactory
* 处理逻辑在ServletWebServerFactoryAutoConfiguration自动配置类中,
* 往容器中注册了WebServerFactoryCustomizerBeanPostProcessor
*/
@Component
public class WebServerFactoryConfig implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
@Override
public void customize(TomcatServletWebServerFactory factory) {
factory.setDocumentRoot(new File("D:\\Deploy"));
}
}
package com.wangtao.tomcat;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; @WebServlet({"/test"})
public class TestServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("=================TestServlet============");
System.out.println(req.getRequestURI());
}
}

然后往D:\Deploy目录中增加以下内容WEB-INF\classes\com\wangtao\tomcat\TestServlet.class,记得项目中本身不要有这个类,要的效果就是tomcat能不能加载到这个类。

测试类

@RestController
public class HelloController { @RequestMapping("/hello")
public String hello() {
// tomcat类加载器
System.out.println(Thread.currentThread().getContextClassLoader());
TomcatEmbeddedWebappClassLoader classLoader = (TomcatEmbeddedWebappClassLoader) Thread.currentThread().getContextClassLoader();
// file:/D:/Deploy/WEB-INF/classes/
Arrays.stream(classLoader.getURLs()).forEach(System.out::println);
// true
System.out.println(ClassUtils.isPresent("com.wangtao.tomcat.FirstFilter", classLoader));
// 系统类加载器
System.out.println(HelloController.class.getClassLoader());
// false
System.out.println(ClassUtils.isPresent("com.wangtao.tomcat.FirstFilter", HelloController.class.getClassLoader()));
return "Hello, Spring Boot!";
}
}

从结果中可以看到,内嵌tomcat确实可以加载到部署目录下的类文件,当开开心心的去访问这个Servlet时却发现访问不了,然而tomcat类加载器确实可以加载到它呀,经过多番查找资料,原来tomcat扫描web.xml以及@WebServlet等这些注解是通过ContextConfig组件实现的,但是Spring Boot创建tomcat时并没有加入该组件,于是虽然可以加载到类,但是相当于是一个普通的类罢了,并没有注册成Servlet。于是改写配置,如下所示

@Component
public class WebServerFactoryConfig implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
@Override
public void customize(TomcatServletWebServerFactory factory) {
// 指定docBase目录
factory.setDocumentRoot(new File("D:\\Deploy"));
/*
* 添加生命周期组件,用于解析扫描web.xml、@WebFilter、@WebServlet等注解
* 从类路径中使用SPI扫描ServletContainerInitializer实现添加到tomcat中
*/
factory.addContextLifecycleListeners(new ContextConfig());
}
}

再次访问,便能看到控制台的打印了。

最新文章

  1. IIS下配置PHP
  2. 大叔也说并行和串行`性能提升N倍(N由操作系统位数和cpu核数决定)
  3. 16、ASP.NET MVC入门到精通——MVC过滤器
  4. 使用curl命令获取文件下载速度
  5. php特性包括哪些?
  6. (3)I2C总线的字节格式,时钟同步和仲裁
  7. JSP九大隐式对象
  8. 避免ssh断开导致运行命令的终止:screen
  9. 第三章SignalR在线聊天例子
  10. 3.cocos2dx它Menu,由menu为了实现场景切换
  11. 深入分析Java Web开发
  12. iOS 跑马灯带图片可点击
  13. 3-1.Hadoop单机模式安装
  14. 为什么说Redis是单线程的以及Redis为什么这么快!
  15. xpath路径表达式
  16. linux常用命令:more 命令
  17. 聊聊如何设计千万级吞吐量的.Net Core网络通信!
  18. HTML标签 闭合还是不闭合?
  19. maven多模块启动required a bean of type com.xxx.xxx.service that could not be found.
  20. 如何使用 nslookup 查域名的 ttl

热门文章

  1. Xmake v2.7.7 发布,支持 Haiku 平台,改进 API 检测和 C++ Modules 支持
  2. 抗TNF治疗改变JIA患者PBMC基因表达谱,可预测疗效
  3. [EULAR文摘] 脊柱放射学持续进展是否显著影响关节功能
  4. GeoServer在Linux上源码安装、启动、发布地图服务
  5. ubuntu18.04开机后NVIDIA显卡驱动加载失败
  6. Docker-Compose 管理镜像和容器(2)
  7. Linux系统下追加记录到文件中的实例代码解读
  8. CentOS 7.6 部署zabbix 6.0 支持Kubernetes
  9. 解决React 安装 antd 后出现的Module not found: Can&#39;t resolve &#39;./locale&#39; in &#39;...rc-picker/node-modules.....&#39;一系列问题问题
  10. 看图王 10.9.1.9791 With X64 去广告绿色版