详解SpringBootTest运行原理
ztj100 2024-12-14 16:12 56 浏览 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
相关推荐
- Jquery 详细用法
-
1、jQuery介绍(1)jQuery是什么?是一个js框架,其主要思想是利用jQuery提供的选择器查找要操作的节点,然后将找到的节点封装成一个jQuery对象。封装成jQuery对象的目的有...
- 前端开发79条知识点汇总
-
1.css禁用鼠标事件2.get/post的理解和他们之间的区别http超文本传输协议(HTTP)的设计目的是保证客户机与服务器之间的通信。HTTP的工作方式是客户机与服务器之间的请求-应答协议。...
- js基础面试题92-130道题目
-
92.说说你对作用域链的理解参考答案:作用域链的作用是保证执行环境里有权访问的变量和函数是有序的,作用域链的变量只能向上访问,变量访问到window对象即被终止,作用域链向下访问变量是不被允许的。...
- Web前端必备基础知识点,百万网友:牛逼
-
1、Web中的常见攻击方式1.SQL注入------常见的安全性问题。解决方案:前端页面需要校验用户的输入数据(限制用户输入的类型、范围、格式、长度),不能只靠后端去校验用户数据。一来可以提高后端处理...
- 事件——《JS高级程序设计》
-
一、事件流1.事件流描述的是从页面中接收事件的顺序2.事件冒泡(eventbubble):事件从开始时由最具体的元素(就是嵌套最深的那个节点)开始,逐级向上传播到较为不具体的节点(就是Docu...
- 前端开发中79条不可忽视的知识点汇总
-
过往一些不足的地方,通过博客,好好总结一下。1.css禁用鼠标事件...
- Chrome 开发工具之Network
-
经常会听到比如"为什么我的js代码没执行啊?","我明明发送了请求,为什么反应?","我这个网站怎么加载的这么慢?"这类的问题,那么问题既然存在,就需要去解决它,需要解决它,首先我们得找对导致问题的原...
- 轻量级 React.js 虚拟美化滚动条组件RScroll
-
前几天有给大家分享一个Vue自定义滚动条组件VScroll。今天再分享一个最新开发的ReactPC端模拟滚动条组件RScroll。...
- 一文解读JavaScript事件对象和表单对象
-
前言相信做网站对JavaScript再熟悉不过了,它是一门脚本语言,不同于Python的是,它是一门浏览器脚本语言,而Python则是服务器脚本语言,我们不光要会Python,还要会JavaScrip...
- Python函数参数黑科技:*args与**kwargs深度解析
-
90%的Python程序员不知道,可变参数设计竟能决定函数的灵活性和扩展性!掌握这些技巧,让你的函数适应任何场景!一、函数参数设计的三大进阶技巧...
- 深入理解Python3密码学:详解PyCrypto库加密、解密与数字签名
-
在现代计算领域,信息安全逐渐成为焦点话题。密码学,作为信息保护的关键技术之一,允许我们加密(保密)和解密(解密)数据。...
- 阿里Nacos惊爆安全漏洞,火速升级!(附修复建议)
-
前言好,我是threedr3am,我发现nacos最新版本1.4.1对于User-Agent绕过安全漏洞的serverIdentitykey-value修复机制,依然存在绕过问题,在nacos开启了...
- Python模块:zoneinfo时区支持详解
-
一、知识导图二、知识讲解(一)zoneinfo模块概述...
- Golang开发的一些注意事项(一)
-
1.channel关闭后读的问题当channel关闭之后再去读取它,虽然不会引发panic,但会直接得到零值,而且ok的值为false。packagemainimport"...
- Python鼠标与键盘自动化指南:从入门到进阶——键盘篇
-
`pynput`是一个用于控制和监控鼠标和键盘的Python库...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- 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)