文章目录

  • 引言(Introduction)
  • 1. 实例(Example)
  • 2. 源码(Source code)
  • 总结(Conclusion)
  • 参考资料(Reference)

引言(Introduction)

写在文章开头的一句话,怕什么真理无穷,进一步有一步的惊喜。

在第三篇的字符串解析中,介绍了cJSON的源码是如何实现解析字符串对象为一个json结构的。本文将介绍cJSON是如何实现将json结构转化为字符串的,因为该部分源码比较长,所以可能有些地方有些错误,还望纠正。

1. 实例(Example)

在分析源码之前,一样先给一个例子:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "cJSON.h"// {//     "name": "Zhang San",    // string
//     "sex": 1,               // boolen
//     "height": 1.8
//     "family": [
//         {//             "name": "Zhang Si",
//             "relationship": "Father"
//         },
//         {//             "name": "Li Si",
//             "relationship": "Mother"
//         }
//     ],
//     "birthday": {//         "year": 2000,
//         "month": 1,
//         "day":1
//     },
// }int CJSON_CDECL main(void){cJSON *root = NULL;cJSON *family = NULL;cJSON *father = NULL;cJSON *mother = NULL;cJSON *birthday = NULL;char *output = NULL;char outputBuffer[1024];root = cJSON_CreateObject();cJSON_AddItemToObject(root, "name", cJSON_CreateString("Zhang San"));cJSON_AddTrueToObject(root, "sex");cJSON_AddNumberToObject(root, "height", 1.8);family = cJSON_AddArrayToObject(root, "family");cJSON_AddItemToArray(family, father = cJSON_CreateObject());cJSON_AddItemToObject(father, "name", cJSON_CreateString("Zhang Si"));cJSON_AddItemToObject(father, "relationship", cJSON_CreateString("Father"));cJSON_AddItemToArray(family, mother = cJSON_CreateObject());cJSON_AddItemToObject(mother, "name", cJSON_CreateString("Li Si"));cJSON_AddItemToObject(mother, "relationship", cJSON_CreateString("Mother"));cJSON_AddItemToObject(root, "birthday", birthday = cJSON_CreateObject());cJSON_AddNumberToObject(birthday, "year", 2000);cJSON_AddNumberToObject(birthday, "month", 1);cJSON_AddNumberToObject(birthday, "day", 1);output = cJSON_Print(root);printf("cJSON_Print(): \n%s\n", output);output = cJSON_PrintUnformatted(root);printf("cJSON_PrintUnformatted(): \n%s\n", output);output = cJSON_PrintBuffered(root, (int)sizeof(root) + 5, 1);printf("cJSON_PrintBuffered(): \n%s\n", output);if(cJSON_PrintPreallocated(root, outputBuffer, 1000, 1))printf("cJSON_PrintPreallocated(): \n%s\n", outputBuffer);free(output);cJSON_Delete(root);return 0;
}

cJSON中提供了四个接口:

  • cJSON_Print:将json结构转换为字符数组,字符数组中含有制表符与换行符,并返回该字符数组的指针;
  • cJSON_PrintUnformated:将json结构转换为字符数组,字符数组中不含有制表符与换行符,并返回该字符数组的指针;
  • cJSON_PrintBuffered:指定输出字符串长度的版本,可以选择是否按格式输出;
  • cJSON_PrintPreallocated:指定输出字符串指针与输出字符串长度,可以选择是否按格式输出。

这些函数在底层实现上是相近的,因为最终都会调用到"print_value",这将会在下面的源码分析上进行介绍。

2. 源码(Source code)

下面直接给出"cJSON_Print"以及"cJSON_PrintUnformated"的源码以及其注释。

  • cJSON_Print:

    // cJSON.h
    // 将cJSON对象转化为字符串,这是有格式的版本
    CJSON_PUBLIC(char *) cJSON_Print(const cJSON *item);// cJSON.c
    CJSON_PUBLIC(char *) cJSON_Print(const cJSON *item)
    {return (char*)print(item, true, &global_hooks);
    }
    
  • cJSON_PrintUnformated:

    // cJSON.h
    // 将cJSON对象转化为字符串,这是上面无格式的版本
    CJSON_PUBLIC(char *) cJSON_PrintUnformatted(const cJSON *item);// cJSON.c
    CJSON_PUBLIC(char *) cJSON_PrintUnformatted(const cJSON *item)
    {return (char*)print(item, false, &global_hooks);
    }
    

上面两个接口都调用了"print"函数,该函数是在cJSON.c文件中定义的静态函数,只能用于该文件。

// cJSON.c
static unsigned char *print(const cJSON * const item, cJSON_bool format, const internal_hooks * const hooks)
{static const size_t default_buffer_size = 256;// printbuffer为存储json转换过程中信息的结构体,与parsebuffer相似printbuffer buffer[1];// printed为最终输出的字符数组unsigned char *printed = NULL;memset(buffer, 0, sizeof(buffer));buffer->buffer = (unsigned char*) hooks->allocate(default_buffer_size);buffer->length = default_buffer_size;buffer->format = format;buffer->hooks = *hooks;if (buffer->buffer == NULL){goto fail;}// 把当前cJSON内部的信息存入到buffer中if (!print_value(item, buffer)){goto fail;}update_offset(buffer);if (hooks->reallocate != NULL){printed = (unsigned char*) hooks->reallocate(buffer->buffer, buffer->offset + 1);if (printed == NULL) {goto fail;}buffer->buffer = NULL;}else{printed = (unsigned char*) hooks->allocate(buffer->offset + 1);if (printed == NULL){goto fail;}// 将buffer中的信息复制到printed中// #define cjson_min(a, b) (((a) < (b)) ? (a) : (b))memcpy(printed, buffer->buffer, cjson_min(buffer->length, buffer->offset + 1));printed[buffer->offset] = '\0';hooks->deallocate(buffer->buffer);}return printed;fail:if (buffer->buffer != NULL){hooks->deallocate(buffer->buffer);}if (printed != NULL){hooks->deallocate(printed);}return NULL;
}

上示源码中,使用到了"printbuffer"结构体存储字符串信息,其内容为:

// cJSON.c
// 用于将cJSON转化为字符串的结构体
typedef struct
{unsigned char *buffer;  // 用于存放字符串size_t length;          // buffer的长度size_t offset;          // 输入指针在buffer中距离开端的偏移量size_t depth;            // 表示json结构体中嵌套的深度cJSON_bool noalloc;        // 表示是否需要分配重新分配内存cJSON_bool format;         // 表示是否按照格式输出字符串internal_hooks hooks;   // 内存分配函数
} printbuffer;

另外两个接口分别为:

  • cJSON_PrintBuffered:

    // cJSON.h
    // 这是使用指定buffer大小策略的Print版本,可以根据fmt选择是否具有格式。
    CJSON_PUBLIC(char *) cJSON_PrintBuffered(const cJSON *item, int prebuffer, cJSON_bool fmt);// cJSON.c
    CJSON_PUBLIC(char *) cJSON_PrintBuffered(const cJSON *item, int prebuffer, cJSON_bool fmt)
    {printbuffer p = { 0, 0, 0, 0, 0, 0, { 0, 0, 0 } };if (prebuffer < 0){return NULL;}// 预先按照指定大小分配内存p.buffer = (unsigned char*)global_hooks.allocate((size_t)prebuffer);if (!p.buffer){return NULL;}p.length = (size_t)prebuffer;p.offset = 0;p.noalloc = false;          // 指明会对buffer进行扩容p.format = fmt;p.hooks = global_hooks;if (!print_value(item, &p)){global_hooks.deallocate(p.buffer);return NULL;}return (char*)p.buffer;
    }
    
  • cJSON_PrintPreallocated:

    // cJSON.h
    // 使用指定buffer的版本,解析的字符串会写入到指定的buffer中
    // NOTE: cJSON在预用内存的估计上并不是百分百准确的,所以可以多分配5个字节的内存
    CJSON_PUBLIC(cJSON_bool) cJSON_PrintPreallocated(cJSON *item, char *buffer, const int length, const cJSON_bool format);// cJSON.c
    CJSON_PUBLIC(cJSON_bool) cJSON_PrintPreallocated(cJSON *item, char *buffer, const int length, const cJSON_bool format)
    {printbuffer p = { 0, 0, 0, 0, 0, 0, { 0, 0, 0 } };if ((length < 0) || (buffer == NULL)){return false;}// 将输出的数组指向指定的bufferp.buffer = (unsigned char*)buffer;p.length = (size_t)length;p.offset = 0;p.noalloc = true;     // 不支持对buffer扩容p.format = format;p.hooks = global_hooks;return print_value(item, &p);
    }
    

可以发现,上面四个接口最终都调用了"print_value"函数,该函数的源码如下:

// 将cJSON对象转化为字符串的实际操作函数
static cJSON_bool print_value(const cJSON * const item, printbuffer * const output_buffer)
{unsigned char *output = NULL;if ((item == NULL) || (output_buffer == NULL)){return false;}switch ((item->type) & 0xFF){case cJSON_NULL:// ensure函数为output_buffer分配多余的内存// 为"null"字符串在最后添加5字节的空间,预留一个空字节的空间output = ensure(output_buffer, 5);if (output == NULL){return false;}strcpy((char*)output, "null");return true;case cJSON_False:output = ensure(output_buffer, 6);if (output == NULL){return false;}strcpy((char*)output, "false");return true;case cJSON_True:output = ensure(output_buffer, 5);if (output == NULL){return false;}strcpy((char*)output, "true");return true;case cJSON_Number:// 将数值型对象转化为字符串型,因为存在double型的值,所以需要将其小数点位置也找出来return print_number(item, output_buffer);case cJSON_Raw:{size_t raw_length = 0;if (item->valuestring == NULL){return false;}raw_length = strlen(item->valuestring) + sizeof("");output = ensure(output_buffer, raw_length);if (output == NULL){return false;}memcpy(output, item->valuestring, raw_length);return true;}case cJSON_String:return print_string(item, output_buffer);case cJSON_Array:return print_array(item, output_buffer);case cJSON_Object:return print_object(item, output_buffer);default:return false;}
}

这里面涉及到了扩容函数"ensure",打印数字"print_number",打印字符串"print_string",打印数组"print_array"以及打印对象"print_object"的函数。

扩容函数"ensure"主要是为了确保buffer中还存在足够的空余空间,其函数实现如下:

// 检查p中的buffer是否还具有needed大小的空间,如果空余空间不足,则需要分配多余的内存(noalloc为false)
static unsigned char* ensure(printbuffer * const p, size_t needed)
{// newbuffer为新的buffer空间unsigned char *newbuffer = NULL;size_t newsize = 0;if ((p == NULL) || (p->buffer == NULL)){return NULL;}if ((p->length > 0) && (p->offset >= p->length)){return NULL;}if (needed > INT_MAX){return NULL;}// 在当前偏移量的基础上判断是否需要分配更多的内存needed += p->offset + 1;if (needed <= p->length){// 不需要分配更多的内存return p->buffer + p->offset;}if (p->noalloc) {// 判断是否能够分配内存给bufferreturn NULL;}// 后续代码实现分配两倍needed或者INT_MAX大小的内存if (needed > (INT_MAX / 2)){// INT_MAX/2 < needed <= INT_MAX/2时分配INT_MAX大小内存if (needed <= INT_MAX){newsize = INT_MAX;}else{return NULL;}}else{// needed <= INT_MAX/2时分配needed大小内存newsize = needed * 2;}if (p->hooks.reallocate != NULL){// 如果存在内存重分配函数newbuffer = (unsigned char*)p->hooks.reallocate(p->buffer, newsize);if (newbuffer == NULL){p->hooks.deallocate(p->buffer);p->length = 0;p->buffer = NULL;return NULL;}}else{// 如果不存在内存重分配函数newbuffer = (unsigned char*)p->hooks.allocate(newsize);if (!newbuffer){p->hooks.deallocate(p->buffer);p->length = 0;p->buffer = NULL;return NULL;}memcpy(newbuffer, p->buffer, p->offset + 1);p->hooks.deallocate(p->buffer);}p->length = newsize;p->buffer = newbuffer;return newbuffer + p->offset;
}

打印各种类型的函数的源码依次为:

  • print_number

    // 将数字转化为字符串
    static cJSON_bool print_number(const cJSON * const item, printbuffer * const output_buffer)
    {unsigned char *output_pointer = NULL;            // 指向output_buffer中的转化结果double d = item->valuedouble;                      // 需要转换的数字int length = 0;size_t i = 0;                                   // 迭代下标unsigned char number_buffer[26] = {0};                // 临时存储数字字符的数组unsigned char decimal_point = get_decimal_point();  // 获取小数点的字符表达double test = 0.0;                               // number_buffer中的数字if (output_buffer == NULL){return false;}// 检查需要打印的数字是否是nan或者infif (isnan(d) || isinf(d)){length = sprintf((char*)number_buffer, "null");}else{// 检查d的小数点后15位,避免不必要的空间length = sprintf((char*)number_buffer, "%1.15g", d);// 检查number_buffer中的数字是否能够表示dif ((sscanf((char*)number_buffer, "%lg", &test) != 1) || !compare_double((double)test, d)){// 如果不能,则使用小数点后17位表示dlength = sprintf((char*)number_buffer, "%1.17g", d);}}// 转换错误或者发生溢出if ((length < 0) || (length > (int)(sizeof(number_buffer) - 1))){return false;}// 为output_buffer分配内存output_pointer = ensure(output_buffer, (size_t)length + sizeof(""));if (output_pointer == NULL){return false;}for (i = 0; i < ((size_t)length); i++){// 利用output_pointer将number_buffer中的字符一一复制到output_buffer中if (number_buffer[i] == decimal_point){output_pointer[i] = '.';continue;}output_pointer[i] = number_buffer[i];}// 添加结束字符output_pointer[i] = '\0';output_buffer->offset += (size_t)length;return true;
    }
    
  • print_string
    print_string中调用了"print_string_ptr"函数,因为在"print_string_ptr"中能够实现键名的打印与键值字符串的打印功能。

    // 调用print_string_ptr打印键名
    static cJSON_bool print_string(const cJSON * const item, printbuffer * const p)
    {return print_string_ptr((unsigned char*)item->valuestring, p);
    }
    

    print_string_ptr的源码如下:

    // 打印字符串,需要处理转义字符
    static cJSON_bool print_string_ptr(const unsigned char * const input, printbuffer * const output_buffer)
    {const unsigned char *input_pointer = NULL;         // 打印字符的遍历指针unsigned char *output = NULL;                     // 指向output_buffer中buffer的末尾地址unsigned char *output_pointer = NULL;             // 输出字符的遍历指针size_t output_length = 0;                     // 需要申请的多余空间size_t escape_characters = 0;                  // 需要跳过的字节数,在处理取消转义字符时用到if (output_buffer == NULL){return false;}if (input == NULL){// 如果字符串为NULL,那么输出为"\"\""output = ensure(output_buffer, sizeof("\"\""));if (output == NULL){return false;}strcpy((char*)output, "\"\"");return true;}for (input_pointer = input; *input_pointer; input_pointer++){// 利用input_pointer遍历一遍input字符串switch (*input_pointer){case '\"':case '\\':case '\b':case '\f':case '\n':case '\r':case '\t':// 一个字符的转义序列,扩充一个字节escape_characters++;break;default:if (*input_pointer < 32){// 特殊字符的转义需要需要扩充五个字节escape_characters += 5;}break;}}// 计算需要申请的多余空间output_length = (size_t)(input_pointer - input) + escape_characters;// 扩容output = ensure(output_buffer, output_length + sizeof("\"\""));if (output == NULL){return false;}if (escape_characters == 0){// 没有转义字符存在的情况output[0] = '\"';memcpy(output + 1, input, output_length);output[output_length + 1] = '\"';output[output_length + 2] = '\0';return true;}output[0] = '\"';output_pointer = output + 1;// 复制字符串到outputfor (input_pointer = input; *input_pointer != '\0'; (void)input_pointer++, output_pointer++){if ((*input_pointer > 31) && (*input_pointer != '\"') && (*input_pointer != '\\')){// 普通字符*output_pointer = *input_pointer;}else{// 需要转义的字符,先在前面加上'\\'*output_pointer++ = '\\';switch (*input_pointer){case '\\':*output_pointer = '\\';break;case '\"':*output_pointer = '\"';break;case '\b':*output_pointer = 'b';break;case '\f':*output_pointer = 'f';break;case '\n':*output_pointer = 'n';break;case '\r':*output_pointer = 'r';break;case '\t':*output_pointer = 't';break;default:// 特殊字符需要编码为unicode的格式sprintf((char*)output_pointer, "u%04x", *input_pointer);output_pointer += 4;break;}}}output[output_length + 1] = '\"';output[output_length + 2] = '\0';// 字符串的输出都是 \" + string + \" + \0 的格式return true;
    }
    
  • print_array

    // 将json数组转化为字符串
    static cJSON_bool print_array(const cJSON * const item, printbuffer * const output_buffer)
    {unsigned char *output_pointer = NULL;size_t length = 0;cJSON *current_element = item->child; // 通过链表遍历json数组if (output_buffer == NULL){return false;}// 为"["字符确保一个字节的空间output_pointer = ensure(output_buffer, 1);if (output_pointer == NULL){return false;}*output_pointer = '[';output_buffer->offset++;output_buffer->depth++;while (current_element != NULL){// 遍历array中的数据if (!print_value(current_element, output_buffer)){return false;}update_offset(output_buffer);if (current_element->next){// 判断数组的相邻元素之间是否需要添加空格length = (size_t) (output_buffer->format ? 2 : 1);output_pointer = ensure(output_buffer, length + 1);if (output_pointer == NULL){return false;}*output_pointer++ = ',';if(output_buffer->format){*output_pointer++ = ' ';}*output_pointer = '\0';output_buffer->offset += length;}current_element = current_element->next;}output_pointer = ensure(output_buffer, 2);if (output_pointer == NULL){return false;}*output_pointer++ = ']';*output_pointer = '\0';output_buffer->depth--;// 最终的结果是 [ + array + ] + \0return true;
    }
    
  • print_object

    // 将一个object对象转换为字符串
    static cJSON_bool print_object(const cJSON * const item, printbuffer * const output_buffer)
    {unsigned char *output_pointer = NULL;size_t length = 0;cJSON *current_item = item->child;if (output_buffer == NULL){return false;}// 为"{"与"\n"分配空间length = (size_t) (output_buffer->format ? 2 : 1); /* fmt: {\n */output_pointer = ensure(output_buffer, length + 1);if (output_pointer == NULL){return false;}*output_pointer++ = '{';output_buffer->depth++;if (output_buffer->format){*output_pointer++ = '\n';}output_buffer->offset += length;while (current_item){if (output_buffer->format){size_t i;output_pointer = ensure(output_buffer, output_buffer->depth);if (output_pointer == NULL){return false;}for (i = 0; i < output_buffer->depth; i++){// 每增加一层深度,添加一个制表符'\t'*output_pointer++ = '\t';}output_buffer->offset += output_buffer->depth;}// 转化键名if (!print_string_ptr((unsigned char*)current_item->string, output_buffer)){// 打印键值字符串return false;}update_offset(output_buffer);// 为 ":" 申请缓冲区大小length = (size_t) (output_buffer->format ? 2 : 1);output_pointer = ensure(output_buffer, length);if (output_pointer == NULL){return false;}*output_pointer++ = ':';if (output_buffer->format){*output_pointer++ = '\t';}output_buffer->offset += length;// 打印键值if (!print_value(current_item, output_buffer)){return false;}update_offset(output_buffer);// 确保一个","或者"\n",与下一个item的大小的空间length = ((size_t)(output_buffer->format ? 1 : 0) + (size_t)(current_item->next ? 1 : 0));output_pointer = ensure(output_buffer, length + 1);if (output_pointer == NULL){return false;}if (current_item->next){*output_pointer++ = ',';}if (output_buffer->format){*output_pointer++ = '\n';}*output_pointer = '\0';output_buffer->offset += length;current_item = current_item->next;}output_pointer = ensure(output_buffer, output_buffer->format ? (output_buffer->depth + 1) : 2);if (output_pointer == NULL){return false;}if (output_buffer->format){size_t i;for (i = 0; i < (output_buffer->depth - 1); i++){*output_pointer++ = '\t';}}*output_pointer++ = '}';*output_pointer = '\0';output_buffer->depth--;// 输出对象的格式为 { + cJSON object + } + \0return true;
    }
    

总结(Conclusion)

cJSON转化json结构为字符串时,需要分别针对不同类型的键值进行分支处理,每种情况需要的字节数不同。同时处理的逻辑也不尽相同,如处理数字时需要判断该数字大小,处理object对象时需要判断深度添加制表符等。从源码中学习,不断成长。

参考资料(Reference)

cjson-sourceforge

cjson-github

cJSON Note(4):转换字符串相关推荐

  1. python整数转换字符串_Python | 将字符串转换为整数列表

    python整数转换字符串 Given a string with digits and we have to convert the string to its equivalent list of ...

  2. c语言uppercase恢复小写,C语言转换字符串为大写和小写

    下面是编程之家 jb51.cc 通过网络收集整理的代码片段. 编程之家小编现在分享给大家,也给大家做个参考. #include /* * Convert a string to lowercase * ...

  3. python整数转换字符串_使用Python中的str()函数将整数值转换为字符串

    python整数转换字符串 Given an integer value and we have to convert the value to the string using str() func ...

  4. 2027. 转换字符串的最少操作次数

    2027. 转换字符串的最少操作次数 给你一个字符串 s ,由 n 个字符组成,每个字符不是 'X' 就是 'O' . 一次 操作 定义为从 s 中选出 三个连续字符 并将选中的每个字符都转换为 'O ...

  5. 在Linux下使用iconv转换字符串编码

    在Linux下写C程序,尤其是网络通信程序时经常遇到编码转换的问题,这里要用到iconv函数库. iconv函数库有以下三个函数 1 2 3 4 5 6 #include <iconv.h> ...

  6. mysql中转换成字符串_如何在R中转换字符串的大小写?

    mysql中转换成字符串 Hello, folks. In this tutorial we are going to convert the case of the string in R. The ...

  7. sql to_char 日期转换字符串

    sql to_char 日期转换字符串 1.转换函数 与date操作关系最大的就是两个转换函数:to_date(),to_char() to_date() 作用将字符类型按一定格式转化为日期类型: 具 ...

  8. 摩尔斯电码转换python编码_python转换字符串为摩尔斯电码的方法

    python转换字符串为摩尔斯电码的方法 本文实例讲述了python转换字符串为摩尔斯电码的方法.分享给大家供大家参考.具体实现方法如下: chars = ",.0123456789?abc ...

  9. 对象转换字符串格式的JSON

    开发工具与关键技术:Eclipse 10.java 作者:梁添荣 撰写时间:2020-04-28 有时我们传到页面的json数据,如果有日期格式,则不会以我们想要的格式去输出,这是我们可以自定义工具, ...

最新文章

  1. java 证件识别_证件识别接口JAVA调用示例
  2. F#探险之旅(三):命令式编程(上)
  3. ce修改器传奇刷元宝_真原始传奇刷元宝方法 不封号刷元宝技巧
  4. Echarts 解决饼图文字过长重叠的问题
  5. 4、通过uiautomatorviewer实现appium元素定位
  6. mysql不同服务器数据库查询_不同服务器不同数据库两张表连接查询使用经验
  7. apache dubbo 自定义全局统一的异常处理器
  8. 局域网ARP协议和欺骗技术及其对策
  9. Oracle Awr
  10. [从零开始学习FPGA编程-16]:快速入门篇 - 操作步骤2-4- Verilog HDL语言描述语言基本语法(软件程序员和硬件工程师都能看懂)
  11. ps基本操作以及常用快捷键
  12. 利用接口和继承实现  求三角形 圆形面积 和以圆形为底的圆锥形的体积
  13. 澳洲PHP工作,怀爱伦澳洲行_在新西兰的工作
  14. 卫星影像0.3米到2米精度样例参照图
  15. 关于提升短信ROI,我的6点思考
  16. java八皇后答案_java八皇后问题详解
  17. WindowsPE无法安装系统
  18. VBA实现xls批量转换为xlsx(非新增副本文件)
  19. 你知道管理工作中要远离三只猫吗?
  20. 如何批量修改文件后缀名?(批量修改文件的扩展名)

热门文章

  1. 职称计算机考试输入破折号,2015职称计算机考试Dreamweaver考前测试题及答案
  2. Splitter Control for Dialog
  3. Office Visio简介
  4. 人工智能(网络爬虫)
  5. react脚手架创建项目报错,ReactDOM.render is no longer supported in React 18.
  6. 村庄规划gis基础操作详细步骤
  7. Ubuntu 16.04通过无线网卡使用桥接模式上网
  8. 抑制剧毒弧菌的新型噬菌体被发现
  9. AS400 - DB2 for i的加密、解密
  10. 纸上得来终觉浅,绝知此事要躬行——Spring boot任务调度