详解SpringBootTest运行原理
ztj100 2024-12-14 16:12 52 浏览 0 评论
SpringBootTest运行原理解析
SpringBootTest注解又引用了两个元注解,@ExtendWith和@BootstrapWith。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@BootstrapWith(SpringBootTestContextBootstrapper.class)
@ExtendWith(SpringExtension.class)
public @interface SpringBootTest {
}
@ExtendWith是Junit5提供的一个使用其扩展来执行回调的一种方式。 其引用的SpringExtention会在Junit5执行过程中执行回调方法。 然后由SpringExtention驱动SpringBoot初始化。理解整个过程之前,先理解几个关键组件。
@ContextConfiguration和ContextConfigurationAttributes
@ContextConfiguration定义了类级别的元数据,它决定了如何去加载和配置Spring容器。
String[] locations() default {};
Class<?>[] classes() default {};
Class<? extends ApplicationContextInitializer<?>>[] initializers() default {};
ContextConfigurationAttributes 是用来封装@ContextConfiguration解析后的信息。
public class ContextConfigurationAttributes {
/**
* 空的位置数组常量,用于表示没有配置位置。
*/
private static final String[] EMPTY_LOCATIONS = new String[0];
?
/**
* 空的类数组常量,用于表示没有配置类。
*/
private static final Class<?>[] EMPTY_CLASSES = new Class<?>[0];
?
/**
* 日志记录器,用于记录该类的日志信息。
*/
private static final Log logger = LogFactory.getLog(ContextConfigurationAttributes.class);
?
/**
* 声明这些配置属性的类。
* 通常是被@ContextConfiguration注解的测试类。
*/
private final Class<?> declaringClass;
?
/**
* 配置类数组,用于指定Spring配置类。
*/
private Class<?>[] classes = new Class<?>[0];
?
/**
* 配置文件位置数组,用于指定XML或其他类型的Spring配置文件的位置。
*/
private String[] locations = new String[0];
?
/**
* 指示是否应该继承父类的locations配置。
*/
private final boolean inheritLocations;
?
/**
* ApplicationContext初始化器类数组,用于在context创建后但在加载之前进行自定义初始化。
*/
private final Class<? extends ApplicationContextInitializer<?>>[] initializers;
?
/**
* 指示是否应该继承父类的initializers配置。
*/
private final boolean inheritInitializers;
?
/**
* 可选的名称,用于标识特定的配置。
* 可以为null,表示没有指定名称。
*/
@Nullable
private final String name;
?
/**
* 用于加载ApplicationContext的ContextLoader类。
* 指定如何创建和配置ApplicationContext。
*/
private final Class<? extends ContextLoader> contextLoaderClass;
?
// ... 构造函数和其他方法 ...
}
MergedContextConfiguration
MergedContextConfiguration封装了一个类上定义的@ContextConfiguration、@ActiveProfiles、@TestPropertySource的信息。
public class MergedContextConfiguration {
?
/**
* 空字符串数组常量,用于表示没有配置的字符串值。
*/
private static final String[] EMPTY_STRING_ARRAY = new String[0];
?
/**
* 空类数组常量,用于表示没有配置的类。
*/
private static final Class<?>[] EMPTY_CLASS_ARRAY = new Class<?>[0];
?
/**
* 空的ApplicationContextInitializer类集合常量,用于表示没有配置的初始化器。
*/
private static final Set<Class<? extends ApplicationContextInitializer<?>>> EMPTY_INITIALIZER_CLASSES =
Collections.emptySet();
?
/**
* 空的ContextCustomizer集合常量,用于表示没有配置的上下文定制器。
*/
private static final Set<ContextCustomizer> EMPTY_CONTEXT_CUSTOMIZERS = Collections.emptySet();
?
/**
* 与此配置相关联的测试类。
*/
private final Class<?> testClass;
?
/**
* Spring配置文件的位置数组。
*/
private final String[] locations;
?
/**
* 用于配置ApplicationContext的配置类数组。
*/
private final Class<?>[] classes;
?
/**
* ApplicationContextInitializer类的集合,用于初始化ApplicationContext。
*/
private final Set<Class<? extends ApplicationContextInitializer<?>>> contextInitializerClasses;
?
/**
* 激活的Spring profiles数组。
*/
private final String[] activeProfiles;
?
/**
* 属性源描述符列表,用于配置测试的属性源。
*/
private final List<PropertySourceDescriptor> propertySourceDescriptors;
?
/**
* 属性源文件的位置数组。
*/
private final String[] propertySourceLocations;
?
/**
* 内联属性源属性数组,格式为"key=value"。
*/
private final String[] propertySourceProperties;
?
/**
* ContextCustomizer的集合,用于自定义ApplicationContext。
*/
private final Set<ContextCustomizer> contextCustomizers;
?
/**
* 用于加载ApplicationContext的ContextLoader。
*/
private final ContextLoader contextLoader;
?
/**
* 缓存感知的上下文加载器委托,用于管理ApplicationContext的缓存。
* 可以为null,表示不使用缓存。
*/
@Nullable
private final CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate;
?
/**
* 父MergedContextConfiguration,用于支持层次结构的配置。
* 可以为null,表示没有父配置。
*/
@Nullable
private final MergedContextConfiguration parent;
?
// ... 构造函数和其他方法 ...
}
MergedContextConfiguration的构建过程
MergedContextConfiguration对象的构建在AbstractTestContextBootstrapper#buildMergedContextConfiguration方法中。
image.png
1、方法开始于对 AbstractTestContextBootstrapper 的 buildMergedContextConfiguration 方法的调用,传入多个参数。
2、首先,调用 resolveContextLoader 方法来确定要使用的 ContextLoader。
3、然后,进入一个循环,遍历 configAttributesList 中的每个 ContextConfigurationAttributes:
如果 contextLoader 是 SmartContextLoader 的实例: 调用 processContextConfiguration 方法 添加位置和类到相应的列表 否则: 调用 processLocations 方法 只添加位置到列表(因为旧版 ContextLoader 不知道如何处理类) 添加初始化器到列表 如果不继承位置,则跳出循环 4、调用 getContextCustomizers 方法获取上下文定制器。
5、使用 TestPropertySourceUtils 构建合并的测试属性源。
6、使用 ApplicationContextInitializerUtils 解析初始化器类。
7、使用 ActiveProfilesUtils 解析激活的配置文件。
8、创建一个新的 MergedContextConfiguration 实例,使用所有收集到的信息。
9、最后,调用 processMergedContextConfiguration 方法处理创建的 MergedContextConfiguration。
10、返回处理后的 MergedContextConfiguration 对象。
TestContextManager和TestContext创建过程
SpringExtention实现了Junit5提供的回调接口
public class SpringExtension implements BeforeAllCallback, AfterAllCallback, TestInstancePostProcessor,
BeforeEachCallback, AfterEachCallback, BeforeTestExecutionCallback, AfterTestExecutionCallback,
ParameterResolver {
public void beforeAll(ExtensionContext context) throws Exception {
// 创建TestContextManager对象,其构造函数内部会创建TestContext实例
TestContextManager testContextManager = getTestContextManager(context);
registerMethodInvoker(testContextManager, context);
// 回调所有 TestExecutionListener#beforeTestClass方法
testContextManager.beforeTestClass();
}
public void postProcessTestInstance(Object testInstance, ExtensionContext context) throws Exception {
validateAutowiredConfig(context);
validateRecordApplicationEventsConfig(context);
TestContextManager testContextManager = getTestContextManager(context);
registerMethodInvoker(testContextManager, context);
// 会回调ServletTestExecutionListener#prepareTestInstance 创建Spring容器
testContextManager.prepareTestInstance(testInstance);
}
}
public TestContextManager(TestContextBootstrapper testContextBootstrapper) {
this.testContext = testContextBootstrapper.buildTestContext();
this.testContextHolder = ThreadLocal.withInitial(() -> copyTestContext(this.testContext));
registerTestExecutionListeners(testContextBootstrapper.getTestExecutionListeners());
}
// 基于MergedContextConfiguration 创建TestContext实例
public TestContext buildTestContext() {
return new DefaultTestContext(getBootstrapContext().getTestClass(), buildMergedContextConfiguration(),
getCacheAwareContextLoaderDelegate());
}
Spring容器的创建过程
ServletTestExecutionListener#prepareTestInstance方法解析
上面说了ServletTestExecutionListener#prepareTestInstance方法会创建Spring容器。源码如下
private void setUpRequestContextIfNecessary(TestContext testContext) {
if (!isActivated(testContext) || alreadyPopulatedRequestContextHolder(testContext)) {
return;
}
// 调用TestContext的getApplication方法获取Spring容器实例
ApplicationContext context = testContext.getApplicationContext();
?
if (context instanceof WebApplicationContext wac) {
ServletContext servletContext = wac.getServletContext();
?
MockHttpServletRequest request = new MockHttpServletRequest(mockServletContext);
request.setAttribute(CREATED_BY_THE_TESTCONTEXT_FRAMEWORK, Boolean.TRUE);
MockHttpServletResponse response = new MockHttpServletResponse();
ServletWebRequest servletWebRequest = new ServletWebRequest(request, response);
?
RequestContextHolder.setRequestAttributes(servletWebRequest);
testContext.setAttribute(POPULATED_REQUEST_CONTEXT_HOLDER_ATTRIBUTE, Boolean.TRUE);
testContext.setAttribute(RESET_REQUEST_CONTEXT_HOLDER_ATTRIBUTE, Boolean.TRUE);
?
if (wac instanceof ConfigurableApplicationContext configurableApplicationContext) {
ConfigurableListableBeanFactory bf = configurableApplicationContext.getBeanFactory();
// 代码中可以获取此处注入的Mock对象依赖
bf.registerResolvableDependency(MockHttpServletResponse.class, response);
bf.registerResolvableDependency(ServletWebRequest.class, servletWebRequest);
}
}
}
TestContext#getApplicationContext方法解析
public ApplicationContext getApplicationContext() {
// 这里会去调用DefaultCacheAwareContextLoaderDelegate#loadContext方法
ApplicationContext context = this.cacheAwareContextLoaderDelegate.loadContext(this.mergedConfig);
return context;
}
DefaultCacheAwareContextLoaderDelegate#loadContext方法解析
public ApplicationContext loadContext(MergedContextConfiguration mergedConfig) {
mergedConfig = replaceIfNecessary(mergedConfig);
synchronized (this.contextCache) {
ApplicationContext context = this.contextCache.get(mergedConfig);
try {
if (context == null) {
if (mergedConfig instanceof AotMergedContextConfiguration aotMergedConfig) {
context = loadContextInAotMode(aotMergedConfig);
}
else {
// 此处会去调用loadContextInternal方法,传入的参数即前面创建好的MergedContextConfiguration对象
context = loadContextInternal(mergedConfig);
}
this.contextCache.put(mergedConfig, context);
}
}
finally {
this.contextCache.logStatistics();
}
return context;
}
}
?
?
protected ApplicationContext loadContextInternal(MergedContextConfiguration mergedConfig)
throws Exception {
// 这里解析出来的是SpringBootContextLoader对象,其实现了SmartContextLoader接口
ContextLoader contextLoader = getContextLoader(mergedConfig);
if (contextLoader instanceof SmartContextLoader smartContextLoader) {
会去调用SpringBootContextLoader#loadContext方法
return smartContextLoader.loadContext(mergedConfig);
}
else {
String[] locations = mergedConfig.getLocations();
return contextLoader.loadContext(locations);
}
}
SpringBootContextLoader#loadContext方法解析
private ApplicationContext loadContext(MergedContextConfiguration mergedConfig, Mode mode,
ApplicationContextInitializer<ConfigurableApplicationContext> initializer) throws Exception {
assertHasClassesOrLocations(mergedConfig);
SpringBootTestAnnotation annotation = SpringBootTestAnnotation.get(mergedConfig);
String[] args = annotation.getArgs();
// 判断是否需要去执行Main方法初始化Spring容器
UseMainMethod useMainMethod = annotation.getUseMainMethod();
Method mainMethod = getMainMethod(mergedConfig, useMainMethod);
if (mainMethod != null) {
ContextLoaderHook hook = new ContextLoaderHook(mode, initializer,
(application) -> configure(mergedConfig, application));
return hook.runMain(() -> ReflectionUtils.invokeMethod(mainMethod, null, new Object[] { args }));
}
// 这里会去调用 new SpringApplication(),创建SpringApplication对象
SpringApplication application = getSpringApplication();
// 这里会去配置Spring 容器
configure(mergedConfig, application);
ContextLoaderHook hook = new ContextLoaderHook(mode, initializer, ALREADY_CONFIGURED);
return hook.run(() -> application.run(args));
}
SpringBootContextLoader#configure方法解析
private void configure(MergedContextConfiguration mergedConfig, SpringApplication application) {
// 设置入口类
application.setMainApplicationClass(mergedConfig.getTestClass());
application.addPrimarySources(Arrays.asList(mergedConfig.getClasses()));
application.getSources().addAll(Arrays.asList(mergedConfig.getLocations()));
// 获取ApplicationContextInitializer
List<ApplicationContextInitializer<?>> initializers = getInitializers(mergedConfig, application);
if (mergedConfig instanceof WebMergedContextConfiguration) {
application.setWebApplicationType(WebApplicationType.SERVLET);
if (!isEmbeddedWebEnvironment(mergedConfig)) {
new WebConfigurer().configure(mergedConfig, initializers);
}
}
else if (mergedConfig instanceof ReactiveWebMergedContextConfiguration) {
application.setWebApplicationType(WebApplicationType.REACTIVE);
}
else {
application.setWebApplicationType(WebApplicationType.NONE);
}
application.setApplicationContextFactory(getApplicationContextFactory(mergedConfig));
if (mergedConfig.getParent() != null) {
application.setBannerMode(Banner.Mode.OFF);
}
// 设置初始化器,SpringBoot启动过程会回调初始化器
application.setInitializers(initializers);
ConfigurableEnvironment environment = getEnvironment();
if (environment != null) {
prepareEnvironment(mergedConfig, application, environment, false);
application.setEnvironment(environment);
}
else {
// 添加事件监听器,在Spring容器启动过程中会初始化Environment
application.addListeners(new PrepareEnvironmentListener(mergedConfig));
}
}
TestExecutionListener
在测试用例运行过程中,Junit5会回调其提供的回调接口,而SpringExtention实现了多个回调接口,如下
public class SpringExtension implements BeforeAllCallback, AfterAllCallback, TestInstancePostProcessor,
BeforeEachCallback, AfterEachCallback, BeforeTestExecutionCallback, AfterTestExecutionCallback,
ParameterResolver {
public void beforeAll(ExtensionContext context) throws Exception {
TestContextManager testContextManager = getTestContextManager(context);
registerMethodInvoker(testContextManager, context);
testContextManager.beforeTestClass();
}
public void afterAll(ExtensionContext context) throws Exception {
try {
TestContextManager testContextManager = getTestContextManager(context);
registerMethodInvoker(testContextManager, context);
testContextManager.afterTestClass();
}
finally {
getStore(context).remove(context.getRequiredTestClass());
}
}
public void postProcessTestInstance(Object testInstance, ExtensionContext context) throws Exception {
validateAutowiredConfig(context);
validateRecordApplicationEventsConfig(context);
TestContextManager testContextManager = getTestContextManager(context);
registerMethodInvoker(testContextManager, context);
testContextManager.prepareTestInstance(testInstance);
}
public void beforeEach(ExtensionContext context) throws Exception {
Object testInstance = context.getRequiredTestInstance();
Method testMethod = context.getRequiredTestMethod();
TestContextManager testContextManager = getTestContextManager(context);
registerMethodInvoker(testContextManager, context);
testContextManager.beforeTestMethod(testInstance, testMethod);
}
public void beforeTestExecution(ExtensionContext context) throws Exception {
Object testInstance = context.getRequiredTestInstance();
Method testMethod = context.getRequiredTestMethod();
TestContextManager testContextManager = getTestContextManager(context);
registerMethodInvoker(testContextManager, context);
testContextManager.beforeTestExecution(testInstance, testMethod);
}
public void afterTestExecution(ExtensionContext context) throws Exception {
Object testInstance = context.getRequiredTestInstance();
Method testMethod = context.getRequiredTestMethod();
Throwable testException = context.getExecutionException().orElse(null);
TestContextManager testContextManager = getTestContextManager(context);
registerMethodInvoker(testContextManager, context);
testContextManager.afterTestExecution(testInstance, testMethod, testException);
}
public void afterEach(ExtensionContext context) throws Exception {
Object testInstance = context.getRequiredTestInstance();
Method testMethod = context.getRequiredTestMethod();
Throwable testException = context.getExecutionException().orElse(null);
TestContextManager testContextManager = getTestContextManager(context);
registerMethodInvoker(testContextManager, context);
testContextManager.afterTestMethod(testInstance, testMethod, testException);
}
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
Parameter parameter = parameterContext.getParameter();
Executable executable = parameter.getDeclaringExecutable();
Class<?> testClass = extensionContext.getRequiredTestClass();
PropertyProvider junitPropertyProvider = propertyName ->
extensionContext.getConfigurationParameter(propertyName).orElse(null);
return (TestConstructorUtils.isAutowirableConstructor(executable, testClass, junitPropertyProvider) ||
ApplicationContext.class.isAssignableFrom(parameter.getType()) ||
supportsApplicationEvents(parameterContext) ||
ParameterResolutionDelegate.isAutowirable(parameter, parameterContext.getIndex()));
}
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
Parameter parameter = parameterContext.getParameter();
int index = parameterContext.getIndex();
Class<?> testClass = extensionContext.getRequiredTestClass();
ApplicationContext applicationContext = getApplicationContext(extensionContext);
return ParameterResolutionDelegate.resolveDependency(parameter, index, testClass,
applicationContext.getAutowireCapableBeanFactory());
}
}
其在整个测试用例执行过程中的执行顺序如下
1、beforeAll(ExtensionContext context)
在所有测试方法执行之前调用,用于整个测试类的设置。
2、postProcessTestInstance(Object testInstance, ExtensionContext context)
在创建测试实例后,但在执行任何测试方法之前调用。 用于处理测试实例,例如注入依赖。
3、beforeEach(ExtensionContext context)
在每个测试方法执行之前调用。 用于设置每个测试方法的环境。
4、beforeTestExecution(ExtensionContext context)
在测试方法执行之前,但在 beforeEach 之后调用。 用于在测试方法执行前进行最后的准备工作。
5、supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) 和resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
这两个方法用于参数解析,在测试方法执行时被调用。 supportsParameter 检查是否支持解析特定参数。 resolveParameter 实际解析并提供参数值。 测试方法执行
6、afterTestExecution(ExtensionContext context)
在测试方法执行之后立即调用。 用于在测试方法执行后立即进行一些清理或验证工作。
7、afterEach(ExtensionContext context)
在每个测试方法执行之后调用。 用于清理每个测试方法的环境。 重复步骤 3-8 对于每个测试方法
8、afterAll(ExtensionContext context)
在所有测试方法执行完毕后调用。 用于整个测试类的清理工作。
常见的TestExecutionListener如下:
org.springframework.test.context.web.ServletTestExecutionListener
org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener
org.springframework.test.context.event.ApplicationEventsTestExecutionListener
org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener
org.springframework.test.context.support.DependencyInjectionTestExecutionListener
org.springframework.test.context.support.DirtiesContextTestExecutionListener
org.springframework.test.context.event.EventPublishingTestExecutionListener
org.springframework.boot.test.mock.mockito.ResetMocksTestExecutionListener
org.springframework.boot.test.autoconfigure.restdocs.RestDocsTestExecutionListener
org.springframework.boot.test.autoconfigure.web.client.MockRestServiceServerResetTestExecutionListener
org.springframework.boot.test.autoconfigure.web.servlet.MockMvcPrintOnlyOnFailureTestExecutionListener
org.springframework.boot.test.autoconfigure.web.servlet.WebDriverTestExecutionListener
org.springframework.boot.test.autoconfigure.webservices.client.MockWebServiceServerTestExecutionListener
原文:https://juejin.cn/post/7405733837132005430
作者:Truism2
相关推荐
- 使用Python编写Ping监测程序(python 测验)
-
Ping是一种常用的网络诊断工具,它可以测试两台计算机之间的连通性;如果您需要监测某个IP地址的连通情况,可以使用Python编写一个Ping监测程序;本文将介绍如何使用Python编写Ping监测程...
- 批量ping!有了这个小工具,python再也香不了一点
-
号主:老杨丨11年资深网络工程师,更多网工提升干货,请关注公众号:网络工程师俱乐部下午好,我的网工朋友。在咱们网工的日常工作中,经常需要检测多个IP地址的连通性。不知道你是否也有这样的经历:对着电脑屏...
- python之ping主机(python获取ping结果)
-
#coding=utf-8frompythonpingimportpingforiinrange(100,255):ip='192.168.1.'+...
- 网站安全提速秘籍!Nginx配置HTTPS+反向代理实战指南
-
太好了,你直接问到重点场景了:Nginx+HTTPS+反向代理,这个组合是现代Web架构中最常见的一种部署方式。咱们就从理论原理→实操配置→常见问题排查→高级玩法一层层剖开说,...
- Vue开发中使用iframe(vue 使用iframe)
-
内容:iframe全屏显示...
- Vue3项目实践-第五篇(改造登录页-Axios模拟请求数据)
-
本文将介绍以下内容:项目中的public目录和访问静态资源文件的方法使用json文件代替http模拟请求使用Axios直接访问json文件改造登录页,配合Axios进行登录请求,并...
- Vue基础四——Vue-router配置子路由
-
我们上节课初步了解Vue-router的初步知识,也学会了基本的跳转,那我们这节课学习一下子菜单的路由方式,也叫子路由。子路由的情况一般用在一个页面有他的基础模版,然后它下面的页面都隶属于这个模版,只...
- Vue3.0权限管理实现流程【实践】(vue权限管理系统教程)
-
作者:lxcan转发链接:https://segmentfault.com/a/1190000022431839一、整体思路...
- swiper在vue中正确的使用方法(vue中如何使用swiper)
-
swiper是网页中非常强大的一款轮播插件,说是轮播插件都不恰当,因为它能做的事情太多了,swiper在vue下也是能用的,需要依赖专门的vue-swiper插件,因为vue是没有操作dom的逻辑的,...
- Vue怎么实现权限管理?控制到按钮级别的权限怎么做?
-
在Vue项目中实现权限管理,尤其是控制到按钮级别的权限控制,通常包括以下几个方面:一、权限管理的层级划分...
- 【Vue3】保姆级毫无废话的进阶到实战教程 - 01
-
作为一个React、Vue双修选手,在Vue3逐渐稳定下来之后,是时候摸摸Vue3了。Vue3的变化不可谓不大,所以,本系列主要通过对Vue3中的一些BigChanges做...
- Vue3开发极简入门(13):编程式导航路由
-
前面几节文章,写的都是配置路由。但是在实际项目中,下面这种路由导航的写法才是最常用的:比如登录页面,服务端校验成功后,跳转至系统功能页面;通过浏览器输入URL直接进入系统功能页面后,读取本地存储的To...
- vue路由同页面重定向(vue路由重定向到外部url)
-
在Vue中,可以使用路由的重定向功能来实现同页面的重定向。首先,在路由配置文件(通常是`router/index.js`)中,定义一个新的路由,用于重定向到同一个页面。例如,我们可以定义一个名为`Re...
- 那个 Vue 的路由,路由是干什么用的?
-
在Vue里,路由就像“页面导航的指挥官”,专门负责管理页面(组件)的切换和显示逻辑。简单来说,它能让单页应用(SPA)像多页应用一样实现“不同URL对应不同页面”的效果,但整个过程不会刷新网页。一、路...
- Vue3项目投屏功能开发!(vue投票功能)
-
最近接了个大屏项目,产品想在不同的显示器上展示大屏项目不同的页面,做出来的效果图大概长这样...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- idea eval reset (50)
- vue dispatch (70)
- update canceled (42)
- order by asc (53)
- spring gateway (67)
- 简单代码编程 贪吃蛇 (40)
- transforms.resize (33)
- redisson trylock (35)
- 卸载node (35)
- np.reshape (33)
- torch.arange (34)
- npm 源 (35)
- vue3 deep (35)
- win10 ssh (35)
- vue foreach (34)
- idea设置编码为utf8 (35)
- vue 数组添加元素 (34)
- std find (34)
- tablefield注解用途 (35)
- python str转json (34)
- java websocket客户端 (34)
- tensor.view (34)
- java jackson (34)
- vmware17pro最新密钥 (34)
- mysql单表最大数据量 (35)