无需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,提问作者Лейла
相关产品推荐
相关产品推荐

