详解SpringBootTest运行原理
ztj100 2024-12-14 16:12 60 浏览 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
相关推荐
- sharding-jdbc实现`分库分表`与`读写分离`
-
一、前言本文将基于以下环境整合...
- 三分钟了解mysql中主键、外键、非空、唯一、默认约束是什么
-
在数据库中,数据表是数据库中最重要、最基本的操作对象,是数据存储的基本单位。数据表被定义为列的集合,数据在表中是按照行和列的格式来存储的。每一行代表一条唯一的记录,每一列代表记录中的一个域。...
- MySQL8行级锁_mysql如何加行级锁
-
MySQL8行级锁版本:8.0.34基本概念...
- mysql使用小技巧_mysql使用入门
-
1、MySQL中有许多很实用的函数,好好利用它们可以省去很多时间:group_concat()将取到的值用逗号连接,可以这么用:selectgroup_concat(distinctid)fr...
- MySQL/MariaDB中如何支持全部的Unicode?
-
永远不要在MySQL中使用utf8,并且始终使用utf8mb4。utf8mb4介绍MySQL/MariaDB中,utf8字符集并不是对Unicode的真正实现,即不是真正的UTF-8编码,因...
- 聊聊 MySQL Server 可执行注释,你懂了吗?
-
前言MySQLServer当前支持如下3种注释风格:...
- MySQL系列-源码编译安装(v5.7.34)
-
一、系统环境要求...
- MySQL的锁就锁住我啦!与腾讯大佬的技术交谈,是我小看它了
-
对酒当歌,人生几何!朝朝暮暮,唯有己脱。苦苦寻觅找工作之间,殊不知今日之事乃我心之痛,难道是我不配拥有工作嘛。自面试后他所谓的等待都过去一段时日,可惜在下京东上的小金库都要见低啦。每每想到不由心中一...
- MySQL字符问题_mysql中字符串的位置
-
中文写入乱码问题:我输入的中文编码是urf8的,建的库是urf8的,但是插入mysql总是乱码,一堆"???????????????????????"我用的是ibatis,终于找到原因了,我是这么解决...
- 深圳尚学堂:mysql基本sql语句大全(三)
-
数据开发-经典1.按姓氏笔画排序:Select*FromTableNameOrderByCustomerNameCollateChinese_PRC_Stroke_ci_as//从少...
- MySQL进行行级锁的?一会next-key锁,一会间隙锁,一会记录锁?
-
大家好,是不是很多人都对MySQL加行级锁的规则搞的迷迷糊糊,一会是next-key锁,一会是间隙锁,一会又是记录锁。坦白说,确实还挺复杂的,但是好在我找点了点规律,也知道如何如何用命令分析加...
- 一文讲清怎么利用Python Django实现Excel数据表的导入导出功能
-
摘要:Python作为一门简单易学且功能强大的编程语言,广受程序员、数据分析师和AI工程师的青睐。本文系统讲解了如何使用Python的Django框架结合openpyxl库实现Excel...
- 用DataX实现两个MySQL实例间的数据同步
-
DataXDataX使用Java实现。如果可以实现数据库实例之间准实时的...
- MySQL数据库知识_mysql数据库基础知识
-
MySQL是一种关系型数据库管理系统;那废话不多说,直接上自己以前学习整理文档:查看数据库命令:(1).查看存储过程状态:showprocedurestatus;(2).显示系统变量:show...
- 如何为MySQL中的JSON字段设置索引
-
背景MySQL在2015年中发布的5.7.8版本中首次引入了JSON数据类型。自此,它成了一种逃离严格列定义的方式,可以存储各种形状和大小的JSON文档,例如审计日志、配置信息、第三方数据包、用户自定...
你 发表评论:
欢迎- 一周热门
-
-
MySQL中这14个小玩意,让人眼前一亮!
-
旗舰机新标杆 OPPO Find X2系列正式发布 售价5499元起
-
【VueTorrent】一款吊炸天的qBittorrent主题,人人都可用
-
面试官:使用int类型做加减操作,是线程安全吗
-
C++编程知识:ToString()字符串转换你用正确了吗?
-
【Spring Boot】WebSocket 的 6 种集成方式
-
PyTorch 深度学习实战(26):多目标强化学习Multi-Objective RL
-
pytorch中的 scatter_()函数使用和详解
-
与 Java 17 相比,Java 21 究竟有多快?
-
基于TensorRT_LLM的大模型推理加速与OpenAI兼容服务优化
-
- 最近发表
- 标签列表
-
- 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)