You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

无需GlobalControllerAdvice,如何配置存储用户时区以自动转换日期?

需求背景与现有实现

我已经通过前端代码将用户时区存入Cookie:

document.addEventListener("DOMContentLoaded", () => {
    const currentTZ = Intl.DateTimeFormat().resolvedOptions().timeZone;
    const cookies = document.cookie.split("; ").reduce((acc, cookie) => {
        const [key, value] = cookie.split("=");
        acc[key] = value;
        return acc;
    }, {});

    if (!cookies["COOKIE.TZ"]) {
        document.cookie = `COOKIE.TZ=${currentTZ}; path=/`;

        fetch(`/?zone=${encodeURIComponent(currentTZ)}`, {
            method: "GET",
        });
    } else if (cookies["COOKIE.TZ"] !== currentTZ) {
        document.cookie = `COOKIE.TZ=${currentTZ}; path=/`;

        fetch(`/?zone=${encodeURIComponent(currentTZ)}`, {
            method: "GET",
        });
    }
});

目前通过GlobalControllerAdvice注入时区到Model:

@ModelAttribute("userZoneId")
public ZoneId getUserZoneId(HttpServletRequest request) {
    String timeZoneId = getCookieValue(request, "COOKIE.TZ");
    return (timeZoneId != null) ? ZoneId.of(timeZoneId) : ZoneId.of("Europe/Moscow");
}

private String getCookieValue(HttpServletRequest request, String name) {
    if (request.getCookies() != null) {
        for (Cookie cookie : request.getCookies()) {
            if (cookie.getName().equals(name)) {
                return cookie.getValue();
            }
        }
    }
    return null;
}

并在Thymeleaf中显式传入时区转换日期:

<td th:text="${#temporals.format(user.dateCreated, 'dd-MM-yyyy HH:mm', userZoneId)}"></td>

现在希望无需显式使用userZoneId,直接通过以下代码自动完成时区转换:

<td th:text="${#temporals.format(user.dateCreated, 'dd-MM-yyyy HH:mm')}"></td>

请问应在何处指定并存储时区以实现这一需求?


实现方案

要让Thymeleaf的#temporals.format自动使用用户时区,核心是让Thymeleaf的默认日期格式化上下文自动加载Cookie中的时区。推荐以下两种标准实现方式:

1. 自定义LocaleContextResolver(优先推荐)

Spring MVC的LocaleContextResolver负责管理请求的本地化信息(含时区),自定义该Resolver可让整个应用的日期格式化逻辑自动复用Cookie中的时区。

步骤1:实现自定义Resolver

import org.springframework.web.servlet.i18n.CookieLocaleResolver;
import javax.servlet.http.HttpServletRequest;
import java.util.TimeZone;

public class CookieTimeZoneLocaleResolver extends CookieLocaleResolver {
    private static final String TIMEZONE_COOKIE_NAME = "COOKIE.TZ";
    private static final String DEFAULT_TIMEZONE = "Europe/Moscow";

    @Override
    protected LocaleContext buildLocaleContext(HttpServletRequest request) {
        TimeZone timeZone = determineTimeZone(request);
        return new SimpleTimeZoneAwareLocaleContext(super.determineLocale(request), timeZone);
    }

    private TimeZone determineTimeZone(HttpServletRequest request) {
        String timeZoneId = getCookieValue(request, TIMEZONE_COOKIE_NAME);
        if (timeZoneId != null) {
            try {
                return TimeZone.getTimeZone(timeZoneId);
            } catch (IllegalArgumentException e) {
                // 无效时区 fallback 到默认值
                return TimeZone.getTimeZone(DEFAULT_TIMEZONE);
            }
        }
        return TimeZone.getTimeZone(DEFAULT_TIMEZONE);
    }

    private String getCookieValue(HttpServletRequest request, String name) {
        if (request.getCookies() != null) {
            for (javax.servlet.http.Cookie cookie : request.getCookies()) {
                if (name.equals(cookie.getName())) {
                    return cookie.getValue();
                }
            }
        }
        return null;
    }
}

步骤2:注册Resolver到Spring容器

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver;

@Configuration
public class WebConfig {
    @Bean
    public LocaleResolver localeResolver() {
        return new CookieTimeZoneLocaleResolver();
    }
}

配置完成后,Thymeleaf的#temporals.format会自动使用请求上下文里的时区,无需显式传参。

2. 自定义Thymeleaf日期方言

如果不想修改Spring全局上下文,可直接扩展Thymeleaf的Temporals表达式对象,让其默认读取Cookie时区。

步骤1:实现自定义方言

import org.thymeleaf.context.IExpressionContext;
import org.thymeleaf.dialect.AbstractDialect;
import org.thymeleaf.dialect.IExpressionObjectDialect;
import org.thymeleaf.expression.IExpressionObjectFactory;
import org.thymeleaf.extras.java8time.expression.Temporals;
import javax.servlet.http.HttpServletRequest;
import java.time.ZoneId;
import java.util.Collections;
import java.util.Set;

public class CustomTemporalDialect extends AbstractDialect implements IExpressionObjectDialect {
    private static final String TIMEZONE_COOKIE_NAME = "COOKIE.TZ";
    private static final String DEFAULT_TIMEZONE = "Europe/Moscow";

    public CustomTemporalDialect() {
        super("CustomTemporalDialect");
    }

    @Override
    public IExpressionObjectFactory getExpressionObjectFactory() {
        return new IExpressionObjectFactory() {
            @Override
            public Set<String> getAllExpressionObjectNames() {
                return Collections.singleton("temporals");
            }

            @Override
            public Object buildObject(IExpressionContext context, String expressionObjectName) {
                if ("temporals".equals(expressionObjectName)) {
                    HttpServletRequest request = (HttpServletRequest) context.getRequest();
                    ZoneId userZone = getUserZone(request);
                    return new CustomTemporals(userZone);
                }
                return null;
            }

            @Override
            public boolean isCacheable(String expressionObjectName) {
                return true;
            }
        };
    }

    private ZoneId getUserZone(HttpServletRequest request) {
        String timeZoneId = getCookieValue(request, TIMEZONE_COOKIE_NAME);
        if (timeZoneId != null) {
            try {
                return ZoneId.of(timeZoneId);
            } catch (IllegalArgumentException e) {
                return ZoneId.of(DEFAULT_TIMEZONE);
            }
        }
        return ZoneId.of(DEFAULT_TIMEZONE);
    }

    private String getCookieValue(HttpServletRequest request, String name) {
        if (request.getCookies() != null) {
            for (javax.servlet.http.Cookie cookie : request.getCookies()) {
                if (name.equals(cookie.getName())) {
                    return cookie.getValue();
                }
            }
        }
        return null;
    }

    private static class CustomTemporals extends Temporals {
        private final ZoneId defaultZone;

        public CustomTemporals(ZoneId defaultZone) {
            this.defaultZone = defaultZone;
        }

        @Override
        public String format(Object temporal, String pattern) {
            return super.format(temporal, pattern, defaultZone);
        }
    }
}

步骤2:注册方言到Spring容器

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.thymeleaf.dialect.IDialect;

@Configuration
public class ThymeleafConfig {
    @Bean
    public IDialect customTemporalDialect() {
        return new CustomTemporalDialect();
    }
}

内容的提问来源于stack exchange,提问作者Лейла

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 12:29:51