SELECT COUNT(*) as visit_count FROM weblog where start_time >='2026-01-01 00:00:00' and start_time < '2026-01-02 00:00:00' GROUP BY url 这个sql代码中传参数start_time如果是字符串就会差8小时,如果start_time传值date类型,则不会差8小时。以前都是传值Date,今天突然发现这个问题,登录到mysql数据库查看数据,发现表中数据也差8小时。为什么呢?
一、分别查看操作系统、MySQL、JVM 时区
1. 查看操作系统时区
不同操作系统的查询命令不同,核心是获取系统的默认时区:
Windows 系统,命令行:打开 CMD/PowerShell,执行以下命令,显示使用东八区时间(如中国标准时间 CST)
编辑
2. 查看 MySQL 5.7 时区
MySQL 的时区分为「全局时区」和「会话时区」,两种都需要验证,可通过 MySQL 客户端(navicat、sqlyog、mysql 命令行)执行 SQL 查询:
sql
-- 方法1:同时查看全局时区(global_time_zone)和会话时区(session_time_zone)
show variables like '%time_zone%';
编辑
- MySQL 5.7 默认时区通常为
SYSTEM(表示跟随操作系统时区);系统默认时区是UTC+8 北京 - 若查询结果中
time_zone为UTC,则表示 MySQL 直接使用 UTC 时区,不跟随系统。
3. 查看 JVM 时区
JVM 时区是 Java 程序运行时的默认时区,有 3 种常用查询方式,适配 Jboot 框架场景:
java
import java.util.TimeZone;
public class TimeZoneTest {
public static void main(String[] args) {
// 方法1:获取JVM默认时区(显示时区ID,如 Asia/Shanghai)
TimeZone defaultTimeZone = TimeZone.getDefault();
System.out.println("JVM 默认时区 ID:" + defaultTimeZone.getID());
System.out.println("JVM 默认时区名称:" + defaultTimeZone.getDisplayName());
// 方法2:Java 8+ 新增方式,更简洁
java.time.ZoneId zoneId = java.time.ZoneId.systemDefault();
System.out.println("JVM 默认时区(Java 8+):" + zoneId);
}
}
java打印结果模式使用的东八区时间(如中国标准时间 CST)UTC+8 北京
JVM 默认时区 ID:Asia/Shanghai
JVM 默认时区名称:中国标准时间
JVM 默认时区(Java 8+):Asia/Shanghai
二、关键注意点:你的 MySQL 连接 URL 配置分析
系统配置的 URL:jdbc:mysql://127.0.0.1:3306/dbname?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC
其中 serverTimezone=UTC 是JDBC 驱动与 MySQL 服务器通信时使用的时区,核心注意点:该配置仅作用于「Java 程序(Jboot)←→MySQL 服务器」的连接层,不会改变 MySQL 的全局 / 会话时区,也不会改变 JVM 和操作系统的时区;
时区转换的执行载体是「MySQL JDBC 驱动 jar 包」(不是 Java 客户端业务代码,也不是 MySQL 服务器端),serverTimezone=UTC 这个配置的作用域仅在JDBC 驱动(mysql-connector-java) 内部,所有与时区相关的时间转换、适配,都是在驱动 jar 中完成的,与 Java 业务代码、MySQL 服务器本身无直接关联。
三、代码验证时区问题
场景 1:统计某天访问量,如果开始时间和结束时间用字符串转值,开始时间"2026-01-01",结束时间"2026-01-02"
ini
public Integer count(String start_time, String end_time) {
StringBuilder sql = new StringBuilder();
sql.append("SELECT COUNT(*) as visit_count FROM weblog WHERE start_time >= ? AND start_time <?");
List<Object> paras = new ArrayList<Object>();
paras.add(0, end_time);
paras.add(0, start_time);
Weblog weblog =(Weblog)weblogDao.getDao().findFirst(sql.toString(), paras.toArray());
return weblog.getInt("visit_count");
}
这种场景下,没有经过 JDBC 驱动的时区转换,偏差的产生流程如下:
- Java 客户端直接将字符串格式的时间值,原封不动地通过 JDBC 连接发送给 MySQL 服务器(驱动仅做数据透传,不进行任何时间解析和时区转换);
- MySQL 服务器接收到字符串后,会按照「自身的全局 / 会话时区」(东八区)来解析这个字符串,将其转换为 MySQL 内部存储的时间格式(TIMESTAMP/DATETIME);
- 简单说:字符串参数是「裸传」,驱动不处理,直接落地到 MySQL 服务器按其自身时区解析,与
serverTimezone=UTC配置无关,进而出现偏差。
场景 2:统计某天访问量,如果开始时间和结束时间用Date转值,开始时间Date(2026-01-01),结束时间Date(2026-01-02)
ini
public Integer count(Date start_time, Date end_time) {
StringBuilder sql = new StringBuilder();
sql.append("SELECT COUNT(*) as visit_count FROM weblog WHERE start_time >= ? AND start_time <?");
List<Object> paras = new ArrayList<Object>();
paras.add(0, end_time);
paras.add(0, start_time);
Weblog weblog =(Weblog)weblogDao.getDao().findFirst(sql.toString(), paras.toArray());
return weblog.getInt("visit_count");
}
这种场景下,JDBC 驱动会主动进行时区转换,保证时间一致性,流程如下:
- Java 客户端将
Date类型的时间对象传递给 JDBC 驱动(日期类型本身是「时间戳(long 型,从 1970-01-01 00:00:00 UTC 开始的毫秒数)」,与时区无关,是绝对时间); - JDBC 驱动接收到这个时间戳后,会读取 URL 中的
serverTimezone=UTC配置,将时间戳从 JVM 默认时区(东八区)转换为 UTC 时区; - 驱动将转换后的 UTC 时间,封装为 MySQL 服务器可识别的时间格式,发送给 MySQL 服务器;
- 简单说:日期类型参数会被驱动拦截,驱动基于
serverTimezone=UTC完成「JVM 时区 ↔ 通信时区(UTC)」的转换,再与 MySQL 服务器交互。增加操作和查询操作都试用日期格式,驱动都会转换时间,所以用户没有在页面发现时间差问题。
四、时区转换代码分析
com.jfinal.plugin.activerecord.Model
typescript
/**
* Find first model. I recommend add "limit 1" in your sql.
* @param sql an SQL statement that may contain one or more '?' IN parameter placeholders
* @param paras the parameters of sql
* @return Model
*/
public M findFirst(String sql, Object... paras) {
List<M> result = find(sql, paras);
return result.size() > 0 ? result.get(0) : null;
}
/**
* Find model.
* @param sql an SQL statement that may contain one or more '?' IN parameter placeholders
* @param paras the parameters of sql
* @return the list of Model
*/
public List<M> find(String sql, Object... paras) {
return find(_getConfig(), sql, paras);
}
protected List<M> find(Config config, String sql, Object... paras) {
Connection conn = null;
try {
conn = config.getConnection();
return find(config, conn, sql, paras);
} catch (Exception e) {
throw new ActiveRecordException(e);
} finally {
config.close(conn);
}
}
/**
* Find model.
*
* 警告:传入的 Connection 参数需要由传入者在 try finally 块中自行
* 关闭掉,否则将出现 Connection 资源不能及时回收的问题
*/
protected List<M> find(Config config, Connection conn, String sql, Object... paras) throws Exception {
try (PreparedStatement pst = conn.prepareStatement(sql)) {
config.dialect.fillStatement(pst, paras);
ResultSet rs = pst.executeQuery();
List<M> result = config.dialect.buildModelList(rs, _getUsefulClass()); // ModelBuilder.build(rs, getUsefulClass());
DbKit.close(rs);
return result;
}
}
com.jfinal.plugin.activerecord.dialect.fillStatement
css
public void fillStatement(PreparedStatement pst, Object... paras) throws SQLException {
for (int i=0; i<paras.length; i++) {
pst.setObject(i + 1, paras[i]);
}
}
com.alibaba.druid.pool.DruidPooledPreparedStatement
java
@Override
public void setObject(int parameterIndex, Object x) throws SQLException {
checkOpen();
try {
stmt.setObject(parameterIndex, x);
} catch (Throwable t) {
throw checkException(t);
}
}
com.alibaba.druid.proxy.jdbc.PreparedStatementProxyImpl
java
@Override
public void setObject(int parameterIndex, Object x) throws SQLException {
setObjectParameter(parameterIndex, x);
createChain().preparedStatement_setObject(this, parameterIndex, x);
}
FilterChainImpl
java
@Override
public void preparedStatement_setObject(PreparedStatementProxy statement, int parameterIndex, Object x)
throws SQLException {
if (this.pos < filterSize) {
nextFilter().preparedStatement_setObject(this, statement, parameterIndex, x);
return;
}
statement.getRawObject().setObject(parameterIndex, x);
}
com.mysql.cj.jdbc.ClientPreparedStatement
java
@Override
public void setObject(int parameterIndex, Object parameterObj) throws SQLException {
synchronized (checkClosed().getConnectionMutex()) {
((PreparedQuery<?>) this.query).getQueryBindings().setObject(getCoreParameterIndex(parameterIndex), parameterObj);
}
}
com.mysql.cj.AbstractQueryBindings
scss
@Override
public void setObject(int parameterIndex, Object parameterObj) {
if (parameterObj == null) {
setNull(parameterIndex);
} else {
if (parameterObj instanceof Byte) {
setInt(parameterIndex, ((Byte) parameterObj).intValue());
} else if (parameterObj instanceof String) {
setString(parameterIndex, (String) parameterObj);
} else if (parameterObj instanceof BigDecimal) {
setBigDecimal(parameterIndex, (BigDecimal) parameterObj);
} else if (parameterObj instanceof Short) {
setShort(parameterIndex, ((Short) parameterObj).shortValue());
} else if (parameterObj instanceof Integer) {
setInt(parameterIndex, ((Integer) parameterObj).intValue());
} else if (parameterObj instanceof Long) {
setLong(parameterIndex, ((Long) parameterObj).longValue());
} else if (parameterObj instanceof Float) {
setFloat(parameterIndex, ((Float) parameterObj).floatValue());
} else if (parameterObj instanceof Double) {
setDouble(parameterIndex, ((Double) parameterObj).doubleValue());
} else if (parameterObj instanceof byte[]) {
setBytes(parameterIndex, (byte[]) parameterObj);
} else if (parameterObj instanceof java.sql.Date) {
setDate(parameterIndex, (java.sql.Date) parameterObj);
} else if (parameterObj instanceof Time) {
setTime(parameterIndex, (Time) parameterObj);
} else if (parameterObj instanceof Timestamp) {
setTimestamp(parameterIndex, (Timestamp) parameterObj);
} else if (parameterObj instanceof Boolean) {
setBoolean(parameterIndex, ((Boolean) parameterObj).booleanValue());
} else if (parameterObj instanceof InputStream) {
setBinaryStream(parameterIndex, (InputStream) parameterObj, -1);
} else if (parameterObj instanceof java.sql.Blob) {
setBlob(parameterIndex, (java.sql.Blob) parameterObj);
} else if (parameterObj instanceof java.sql.Clob) {
setClob(parameterIndex, (java.sql.Clob) parameterObj);
} else if (this.treatUtilDateAsTimestamp.getValue() && parameterObj instanceof java.util.Date) {
setTimestamp(parameterIndex, new Timestamp(((java.util.Date) parameterObj).getTime()));
} else if (parameterObj instanceof BigInteger) {
setString(parameterIndex, parameterObj.toString());
} else if (parameterObj instanceof LocalDate) {
setDate(parameterIndex, Date.valueOf((LocalDate) parameterObj));
} else if (parameterObj instanceof LocalDateTime) {
setTimestamp(parameterIndex, Timestamp.valueOf((LocalDateTime) parameterObj));
} else if (parameterObj instanceof LocalTime) {
setTime(parameterIndex, Time.valueOf((LocalTime) parameterObj));
} else {
setSerializableObject(parameterIndex, parameterObj);
}
}
}
com.mysql.cj.ClientPreparedQueryBindings
kotlin
@Override
public void setTimestamp(int parameterIndex, Timestamp x) {
int fractLen = -1;
if (!this.sendFractionalSeconds.getValue() || !this.session.getServerSession().getCapabilities().serverSupportsFracSecs()) {
fractLen = 0;
} else if (this.columnDefinition != null && parameterIndex <= this.columnDefinition.getFields().length && parameterIndex >= 0) {
fractLen = this.columnDefinition.getFields()[parameterIndex].getDecimals();
}
setTimestamp(parameterIndex, x, null, fractLen);
}
com.mysql.cj.ClientPreparedQueryBindings
ini
@Override
public void setTimestamp(int parameterIndex, Timestamp x, Calendar targetCalendar, int fractionalLength) {
if (x == null) {
setNull(parameterIndex);
} else {
x = (Timestamp) x.clone();
if (!this.session.getServerSession().getCapabilities().serverSupportsFracSecs()
|| !this.sendFractionalSeconds.getValue() && fractionalLength == 0) {
x = TimeUtil.truncateFractionalSeconds(x);
}
if (fractionalLength < 0) {
// default to 6 fractional positions
fractionalLength = 6;
}
x = TimeUtil.adjustTimestampNanosPrecision(x, fractionalLength, !this.session.getServerSession().isServerTruncatesFracSecs());
this.tsdf = TimeUtil.getSimpleDateFormat(this.tsdf, "''yyyy-MM-dd HH:mm:ss", targetCalendar,
targetCalendar != null ? null : this.session.getServerSession().getDefaultTimeZone());
StringBuffer buf = new StringBuffer();
buf.append(this.tsdf.format(x));
if (this.session.getServerSession().getCapabilities().serverSupportsFracSecs()) {
buf.append('.');
buf.append(TimeUtil.formatNanos(x.getNanos(), 6));
}
buf.append(''');
setValue(parameterIndex, buf.toString(), MysqlType.TIMESTAMP);
}
}
this.session.getServerSession().getDefaultTimeZone()------ 这个时区就是你 JDBC URL 中serverTimezone=UTC配置的时区!
驱动会基于serverTimezone配置的时区,对Timestamp进行格式化,完成了「JVM 默认时区 → serverTimezone 配置时区」的转换 ,这就是为什么传入Date/Timestamp类型参数不会有 8 小时偏差的底层原因。
关键区别:这里的字符串是驱动经过时区转换后生成的,和你手动传入的原始字符串(如 "2025-12-29 16:00:00")有本质不同 ------ 前者带时区适配,后者是裸传无处理。
五、总结
本文围绕操作系统、MySQL 5.7、JVM 时区查看方法及Java程序时区偏差问题展开,核心结论如下。三者时区查看各有路径:操作系统需按Windows等系统类型执行对应命令,核心获取默认时区(如东八区CST);MySQL需区分全局与会话时区,通过SQL查询验证,默认多跟随系统时区(东八区),也可能直接使用UTC;JVM可通过代码获取默认时区(如Asia/Shanghai),适配Java 8+新方式。
JDBC连接URL中serverTimezone=UTC仅作用于驱动层,负责程序与MySQL通信时的时区转换,不改变各端本身时区,转换逻辑由MySQL JDBC驱动包实现,与业务代码、MySQL服务器无直接关联。
代码场景中,时间参数类型决定是否出现时区偏差:字符串类型为裸传,驱动不处理,MySQL按自身时区解析,与驱动配置脱节,易产生8小时偏移;Date/Timestamp类型为时间戳(绝对时间),驱动基于serverTimezone=UTC完成JVM时区到UTC的转换,MySQL再反向解析,抵消时差无偏差。
底层原理上,驱动通过AbstractQueryBindings类判断参数类型,对Date/Timestamp调用setTimestamp方法,基于serverTimezone配置格式化时间,实现时区适配,这是两种参数类型处理差异的核心原因。综上,避免时区偏差需优先使用Date/Timestamp类型参数,确保驱动层时区转换生效。