开心一刻
今天我突然顿悟了,然后跟我妈聊天
我:妈,我发现一个饿不死的办法
妈:什么办法
我:我先养个狗,再养个鸡
妈:然后了
我:我拉的狗吃,狗拉的鸡吃,鸡下的蛋我吃,如此反复,我们三都饿不死
妈:你整那么多中间商干啥,你就自己拉的自己吃得了,还省事
我又顿悟了,回到:也是啊
说句很重要的心里话:祝大家在2024年,身体健康,万事如意!
场景重温
为了让大家更好的明白问题,先做下相关准备工作
环境准备
数据库:MySQL 8.0.30,表:tbl_order
DROP TABLE IF EXISTS `tbl_order`; CREATE TABLE `tbl_order` ( `id` bigint(0) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增主键', `order_no` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '业务名', `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最终修改时间', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '订单' ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of tbl_order -- ---------------------------- INSERT INTO `tbl_order` VALUES (1, '123456', '2023-04-20 07:37:34.000', '2023-04-20 07:37:34.720'); INSERT INTO `tbl_order` VALUES (2, '654321', '2023-04-20 07:37:34.020', '2023-04-20 07:37:34.727');
View Code
基于JDK1.8、druid 1.1.12、mysql-connector-java 8.0.21、Spring 5.2.3.RELEASE
完整代码:druid-timeout
毫秒位数捉摸不透
直接运行com.qsl.DruidTimeoutTest#main,会看到如下结果
数据库表中的值:2023-04-20 07:37:34.000运行出来后是2023-04-20 07:37:34.0,2023-04-20 07:37:34.720对应2023-04-20 07:37:34.72
2023-04-20 07:37:34.020对应2023-04-20 07:37:34.02,2023-04-20 07:37:34.727对应2023-04-20 07:37:34.727
毫秒位数时而1位,时而2位,时而3位,搞的我好乱呐
原因分析
大家注意看这个代码
获取列值,sqlRowSet.getObject(i)返回的类型是Object,我们调整下输出:System.out.println(obj.getClass().getName() + ” ” + obj);
此时输出结果如下
可以看到,java程序中,此时的时间类型是java.sql.Timestamp
有了这个依托点,原因就很好分析了
Timestamp的toString
我们知道,java中直接输出对象,会调用对象的toString方法,如果自身没有重写toString则会沿用Object的toString方法
我们先来看一下Object的toString方法
粗略看一下,返回值明显不是2023-04-20 07:37:34.0这种时间字符串格式
那说明什么?
说明Timestamp肯定重写了toString方法嘛
java.sql.Timestamp#toString内容如下
/** * Formats a timestamp in JDBC timestamp escape format. *yyyy-mm-dd hh:mm:ss.fffffffff
, * whereffffffffff
indicates nanoseconds. **
@return aString
object in *yyyy-mm-dd hh:mm:ss.fffffffff
format */ @SuppressWarnings("deprecation") public String toString () { int year = super.getYear() + 1900; int month = super.getMonth() + 1; int day = super.getDate(); int hour = super.getHours(); int minute = super.getMinutes(); int second = super.getSeconds(); String yearString; String monthString; String dayString; String hourString; String minuteString; String secondString; String nanosString; String zeros = "000000000"; String yearZeros = "0000"; StringBuffer timestampBuf; if (year ) { // Add leading zeros yearString = "" + year; yearString = yearZeros.substring(0, (4-yearString.length())) + yearString; } else { yearString = "" + year; } if (month ) { monthString = "0" + month; } else { monthString = Integer.toString(month); } if (day ) { dayString = "0" + day; } else { dayString = Integer.toString(day); } if (hour ) { hourStr服务器托管网ing = "0" + hour; } else { hourString = Integer.toString(hour); } if (minute ) { minuteString = "0" + minute; } else { minuteString = Integer.toString(minute); } if (second ) { secondString = "0" + second; } else { secondString = Integer.toString(second); } if (nanos == 0) { nanosString = "0"; } else { nanosString = Integer.toString(nanos); // Add leading zeros nanosString = zeros.substring(0, (9-nanosString.length())) + nanosString; // Truncate trailing zeros char[] nanosChar = new char[nanosString.length()]; nanosString.getChars(0, nanosString.length(), nanosChar, 0); int truncIndex = 8; while (nanosChar[truncIndex] == '0') { truncIndex--; } nanosString = new String(nanosChar, 0, truncIndex + 1); } // do a string buffer here instead. timestampBuf = new StringBuffer(20+nanosString.length()); timestampBuf.append(yearString); timestampBuf.append("-"); timestampBuf.append(monthString); timestampBuf.append("-"); timestampBuf.append(dayString); timestampBuf.append(" "); timestampBuf.append(hourString); timestampBuf.append(":"); timestampBuf.append(minuteString); timestampBuf.append(":"); timestampBuf.append(secondString); timestampBuf.append("."); timestampBuf.append(nanosString); return (timestampBuf.toString()); }
View Code
注意看注释:yyyy-mm-dd hh:mm:ss.fffffffff,说明精度是到纳秒级别,不只是到毫秒哦!
该方法很长,我们只需要关注fffffffff的处理,也就是如下代码
nanos类型是int:private int nanos;,用来存储秒后面的那部分值
数据库表中的值:2023-04-20 07:37:34.000对应的nanos的值是 0,2023-04-20 07:37:34.720对应的nanos的值是多少了?
不是、不是、不是720,因为它的格式是fffffffff,所以应该是720000000
那2023-04-20 07:37:34.020对应的nanos的值又是多少?
不是、不是、不是200000000,而是20000000,因为nanos是int类型,不能以0开头
再回到上述代码,当nanos等于 0 时,nanosString即为字符串0,所以2023-04-20 07:37:34.000对应2023-04-20 07:37:34.0
当nanos不等于 0 时
1、先将nanos转换成字符串nanosString,nanosString的位数与nanos一致
2、nanosString前补0,nanos的位数与 9 差多少就前补多少个0
例如2023-04-20 07:37:34.020对应的nanos是20000000,只有8位,前补1个0,则nanosString的值是020000000
3、去掉末尾的0
020000000去掉末尾的0,得到02
原因是不是找到了?
总结下就是:java.sql.Timestamp#toString会格式化掉nanosString末尾的0!(注意:nanos的值是没有变的)
是不是很精辟
但是问题又来了:为什么要格式化末尾的0?
说实话,我没有找到一个确切的、准确的说明
只是自己给自己编造了一个勉强的理由:简洁化,提高可读性
去掉nanosString末尾的 0,并没有影响时间值的准确性,但是可以简化整个字符串,末尾跟着一串0,可读性会降低
如果非要保留末尾的0,可以自定义格式化方法,想保留几个0就保留几个0
类型对应
MySQL类型和JAVA类型是如何对应的,是不是很想知道这个问题?
那就安排起来,如何寻找了?
别慌,我有葵花宝典:杂谈篇之我是怎么读源码的,授人以渔
为了节约时间,我就不带你们一步一步debug了,直接带你们来到关键点com.mysql.cj.protocol.a.ColumnDefinitionReader#read
里面有如下关键代码
为了方便你们跟源码,我服务器托管网把此刻的堆栈信息贴一下
我们继续跟进unpackField,会发现里面有这样一行代码
恭喜你,只差临门一脚了
按住ctrl键,鼠标左击MysqlType,欢迎来到类型对应世界:com.mysql.cj.MysqlType
其构造方法
我们暂时只需要关注:mysqlTypeName、jdbcType和javaClass
接下来我们找到MySQL的DATETIME
此处的Timestamp.class就是java.sql.Timestamp
其他的对应关系,大家也可以看看,比如
额外拓展
TIMESTAMP范围
回答这个问题的时候,一定要说明前提条件
MySQL8,范围是‘1970-01-01 00:00:01’ UTC to ‘2038-01-19 03:14:07’ UTC
JDK8,Timestamp构造方法
入参是long类型,其最大值是9223372036854775807,1 年是365*24*60*60*1000=31536000000毫秒
也就是long最大可以记录6269161692年,所以范围是1970 ~ (1970 + 6269161692),不会有2038年问题
MySQL的TIMESTAMP和JAVA的Timestamp是对应关系,并不是对等关系,大家别搞混了
关于不允许使用java.sql.Timestamp
阿里巴巴的开发手册中明确指出不能用:java.sql.Timestamp
为什么mysql-connector-java还要用它?
可以从以下几点来分析
1、java.sql.Timestamp存在有存在的道理,它有它的优势
1.1 精度到了纳秒级别
1.2被设计为与SQL TIMESTAMP类型兼容,这意味着在数据库交互中,使用Timestamp可以减少数据类型转换的问题,提高数据的一致性和准确性
1.3 时间方面的计算非常方便
2、在某些特定情况下才会触发Timestamp的bug,我们不能以此就完全否定Timestamp吧
况且JDK9也修复了
3、MySQL的TIMESTAMP如果不对应java.sql.Timestamp,那该对应JAVA的哪个类型?
MySQL的DATETIME为什么也对应java.sql.Timestamp
MySQL的TIMESTAMP对应java.sql.Timestamp,对此我相信大家都没有疑问
为何MySQL的DATETIME也对应java.sql.Timestamp?
我反问一句,不对应java.sql.Timestamp对应哪个?
LocalDateTime?试问JDK8之前有LocalDateTime吗?
不过mysql-connector-java还是做了调整,我们来看下
我把mysql-connector-java的源码clone下来了,更方便我们查看提交记录
找到com.mysql.cj.MysqlType#DATETIME,在其前面空白处右击
鼠标左击Annotate with Git Blame,会看到每一行的最新修改提交记录
我们继续左击DATETIME的最新修改提交记录
可以看到详细的提交信息
双击MysqlType.java,可以看到修改内容
可以看到MySQL的DATETIME对应的JAVA类型从java.sql.Timestamp调整成了java.time.LocalDateTime
那mysql-connector-java哪个版本开始生效的了?
它是开源的,那就直接在github上找mysql-connector-java的issue:Bug#102321
但是你会发现搜不到
这是因为mysql-connector-java调整成了mysql-connector-j,相关issue没有整合
那么我们就换个方式搜,就像这样
回车,结果如下
也没有搜到!!!
但你去点一下左侧的Commits,你会发现有结果!!!
Commits不是 0 吗,怎么有结果,谁来都懵呀
这绝对是github的Bug呀(这个我回头找下官方确认下,不深究!)
我们点击Commits的这个搜索结果,会来到如下界面
答案已经揭晓
从8.0.24开始,MySQL的DATETIME对应的JAVA类型从java.sql.Timestamp调整成java.time.LocalDateTime
总结
java.sql.Timestamp
1、设计初衷就是为了对应SQL TIMESTAMP,所以不管是MySQL还是其他数据库,其TIMESTAMP对应的JAVA类型都是java.sql.Timestamp
2、MySQL的TIMESTAMP有2038年问题,是因为它的底层存储是 4 个字节,并且最高位是符号位,至于其他类型的数据库是否有该问题,得看具体实现
3、在清楚使用情况的前提下(不触发JDK8 BUG)是可以使用的,有些场景使用java.sql.Timestamp确实更方便
DATETIME对应类型
SQL DATETIME对应的JAVA类型,没有统一标准,需要看具体数据库的jdbc版本
比如mysql-connector-java,8.0.24之前,DATETIME对应的JAVA类型是java.sql.Timestamp,而8.0.24及之后,对应的是java.time.LocalDateTime
至于其他数据库的jdbc是如何对应的,就交给你们了,可以从最新版本着手去分析
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net
机房租用,北京机房租用,IDC机房托管, http://www.fwqtg.net
一、SPI的概念 1.1、什么是API? API在我们日常开发工作中是比较直观可以看到的,比如在 Spring 项目中,我们通常习惯在写 service 层代码前,添加一个接口层,对于 service 的调用一般也都是基于接口操作,通过依赖注入,可以使用接口实…