Java+MySQL时区难题-Date自动转换String差8小时

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_zoneUTC,则表示 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=UTCJDBC 驱动与 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 驱动的时区转换,偏差的产生流程如下:

  1. Java 客户端直接将字符串格式的时间值,原封不动地通过 JDBC 连接发送给 MySQL 服务器(驱动仅做数据透传,不进行任何时间解析和时区转换);
  2. MySQL 服务器接收到字符串后,会按照「自身的全局 / 会话时区」(东八区)来解析这个字符串,将其转换为 MySQL 内部存储的时间格式(TIMESTAMP/DATETIME);
  3. 简单说:字符串参数是「裸传」,驱动不处理,直接落地到 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 驱动会主动进行时区转换,保证时间一致性,流程如下:

  1. Java 客户端将 Date 类型的时间对象传递给 JDBC 驱动(日期类型本身是「时间戳(long 型,从 1970-01-01 00:00:00 UTC 开始的毫秒数)」,与时区无关,是绝对时间);
  2. JDBC 驱动接收到这个时间戳后,会读取 URL 中的 serverTimezone=UTC 配置,将时间戳从 JVM 默认时区(东八区)转换为 UTC 时区
  3. 驱动将转换后的 UTC 时间,封装为 MySQL 服务器可识别的时间格式,发送给 MySQL 服务器;
  4. 简单说:日期类型参数会被驱动拦截,驱动基于 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类型参数,确保驱动层时区转换生效。

相关推荐
壹米饭2 小时前
MYSQL进阶:删除视图时视图被lock解决方案
后端·mysql
萧曵 丶2 小时前
Redis 是单线程的吗?
数据库·redis
老邓计算机毕设2 小时前
SSM校园招聘管理系统968b0(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面
数据库·ssm 框架·校园招聘管理系统·简历投递
Zoey的笔记本3 小时前
敏捷与稳定并行:Scrum看板+BPM工具选型指南
大数据·前端·数据库·python·低代码
晴天¥3 小时前
Oracle DB 的相关管理工具
数据库·oracle
oMcLin3 小时前
如何在Ubuntu 22.04 LTS上配置并优化MySQL 8.0分区表,提高大规模数据集查询的效率与性能?
android·mysql·ubuntu
Codeking__3 小时前
Redis的value类型介绍——set
数据库·redis·缓存
youyicc4 小时前
Qt连接Pg数据库
开发语言·数据库·qt
DO_Community4 小时前
DigitalOcean容器注册表推出多注册表支持功能
服务器·数据库·docker·kubernetes