开心一刻

今天我突然顿悟了,然后跟我妈聊天

我:妈,我发现一个饿不死的办法

妈:什么办法

我:我先养个狗,再养个鸡

妈:然后了

我:我拉的狗吃,狗拉的鸡吃,鸡下的蛋我吃,如此反复,我们三都饿不死

妈:你整那么多中间商干啥,你就自己拉的自己吃得了,还省事

我又顿悟了,回到:也是啊

说句很重要的心里话:祝大家在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.
* <code>yyyy-mm-dd hh:mm:ss.fffffffff</code>,
* where <code>ffffffffff</code> indicates nanoseconds.
* <P>
*
@returna <code>String</code> object in
* <code>yyyy-mm-dd hh:mm:ss.fffffffff</code> format
*/@SuppressWarnings("deprecation")publicString 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 < 1000) {//Add leading zeros yearString = "" +year;
yearString
= yearZeros.substring(0, (4-yearString.length())) +yearString;
}
else{
yearString
= "" +year;
}
if (month < 10) {
monthString
= "0" +month;
}
else{
monthString
=Integer.toString(month);
}
if (day < 10) {
dayString
= "0" +day;
}
else{
dayString
=Integer.toString(day);
}
if (hour < 10) {
hourString
= "0" +hour;
}
else{
hourString
=Integer.toString(hour);
}
if (minute < 10) {
minuteString
= "0" +minute;
}
else{
minuteString
=Integer.toString(minute);
}
if (second < 10) {
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
是如何对应的,就交给你们了,可以从最新版本着手去分析

标签: none

添加新评论