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类型参数,确保驱动层时区转换生效。

相关推荐
IvorySQL14 小时前
PostgreSQL 分区表的 ALTER TABLE 语句执行机制解析
数据库·postgresql·开源
·云扬·14 小时前
MySQL 8.0 Redo Log 归档与禁用实战指南
android·数据库·mysql
IT邦德14 小时前
Oracle 26ai DataGuard 搭建(RAC到单机)
数据库·oracle
惊讶的猫14 小时前
redis分片集群
数据库·redis·缓存·分片集群·海量数据存储·高并发写
不爱缺氧i14 小时前
完全卸载MariaDB
数据库·mariadb
纤纡.15 小时前
Linux中SQL 从基础到进阶:五大分类详解与表结构操作(ALTER/DROP)全攻略
linux·数据库·sql
jiunian_cn15 小时前
【Redis】渐进式遍历
数据库·redis·缓存
橙露15 小时前
Spring Boot 核心原理:自动配置机制与自定义 Starter 开发
java·数据库·spring boot
冰暮流星15 小时前
sql语言之分组语句group by
java·数据库·sql
符哥200815 小时前
Ubuntu 常用指令集大全(附实操实例)
数据库·ubuntu·postgresql