最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

SpringBoot項目中日期處理的最佳實踐

 更新時間:2026年02月28日 08:50:24   作者:程序員越  
這篇文章主要為大家詳細介紹了SpringBoot項目中日期處理的相關(guān)知識,文中的示例代碼講解詳細,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下

住在公司附近的壞處就是,夜里可能被領(lǐng)導(dǎo)一通電話叫去公司看問題。服務(wù)總是報錯,重啟也沒用。到公司打開電腦,日志好多這個錯誤:

Exception in thread "pool-1-thread-4" java.lang.NumberFormatException: For input string: ""

順著堆棧找過去,發(fā)現(xiàn)是SimpleDateFormat在多線程環(huán)境下出了幺蛾子。一個用了3年的工具類,在并發(fā)量上來之后,直接讓服務(wù)跪了。

今天就把這次踩坑換來的經(jīng)驗分享給你。全文3000字,看完至少讓你少踩3個生產(chǎn)級別的坑。

一、故事要從那個凌晨說起:老套路的致命問題

那天晚上修復(fù)完bug,我翻了翻項目代碼,發(fā)現(xiàn)一個扎心的事實:

  • 數(shù)據(jù)庫里存的是DateTime;
  • 業(yè)務(wù)代碼里混用Date,方法調(diào)用復(fù)雜,易出錯;
  • 時間比較全靠getTime()獲取毫秒值,可讀性極差;
  • 日期格式化用SimpleDateFormat,封裝好的工具類,埋下線程安全隱患。

其實早在Java 8發(fā)布時,官方就已經(jīng)給我們提供了一套完整、安全、高效的日期處理解決方案,只是很多人(包括之前的我)一直固守老套路,從未認真了解過這套“新玩具”。

先給大家梳理一下這套新方案的核心成員,記住它們的分工,就能避開80%的坑:

適用場景特點
Instant機器時間,記錄時間戳無時區(qū),精確到納秒,對應(yīng)絕對時間點
LocalDateTime本地日期時間(如生日、營業(yè)時間)不帶時區(qū),面向人類閱讀和使用
ZonedDateTime帶時區(qū)的日期時間(如跨時區(qū)會議)跨時區(qū)應(yīng)用必備,明確時區(qū)信息
DateTimeFormatter日期時間格式化/解析線程安全,性能強悍,可全局復(fù)用

這里先給大家拋一個核心原則,后面所有內(nèi)容都圍繞這個原則展開:數(shù)據(jù)庫存儲用bigint,java中用Instant和LocalDateTime,展示用DateTimeFormatter。

二、數(shù)據(jù)庫存儲:用BIGINT存時間戳,真香

聊完核心工具類,我們先解決第一個基礎(chǔ)問題:日期數(shù)據(jù)到底該怎么存?這也是我這次踩坑的間接原因之一。

我當(dāng)時的第一版數(shù)據(jù)庫設(shè)計,用的是MySQL的DATETIME類型,Java代碼中對應(yīng)java.util.Dat e。乍一看邏輯通順,日期類型存日期,直觀又方便,但實 際運行后,接連踩了3個坑:

  • 時區(qū)錯亂:不同環(huán)境(開發(fā)、測試、生產(chǎn))的MySQL時區(qū)配置不一致,導(dǎo)致存進去的時間和實際時間偏差幾小時,排查起來極其費力;
  • 性能瓶頸:當(dāng)數(shù)據(jù)量達到百萬級后,日期字段的排序、篩選操作速度急劇下降,索引優(yōu)化效果甚微;
  • 分庫分表麻煩:按時間分片時,DATETIME類型需要額外處理時區(qū)和格式,容易出現(xiàn)分片偏差,而整數(shù)類型的時間戳則能完美規(guī)避這個問題。

后來將數(shù)據(jù)庫字段全部改成BIGINT存時間戳(毫秒級),所有問題瞬間迎刃而解,感覺整個世界都清凈了。

推薦的實體類設(shè)計如下,兼顧數(shù)據(jù)庫性能和業(yè)務(wù)語義:

@Entity
@Table(name = "orders")
public class Order {
   
    // 數(shù)據(jù)庫存BIGINT,追求極致的查詢性能
    @Column(name = "create_time", nullable = false)
    private Long createTimeStamp;
   
    // 業(yè)務(wù)代碼里用Instant操作,兩全其美(語義準(zhǔn)確+操作便捷)
    public Instant getCreateTime() {
        return Instant.ofEpochMilli(createTimeStamp);
    }
   
    public void setCreateTime(Instant instant) {
        this.createTimeStamp = instant.toEpochMilli();
    }
}

這樣設(shè)計的優(yōu)勢非常明顯,主要有3點:

  • 性能炸裂:整數(shù)的比較、索引查詢速度,遠超DATETIME等日期類型,尤其適合大數(shù)據(jù)量場景;
  • 時區(qū)無關(guān):時間戳本身是絕對時間點,不依賴任何時區(qū)配置,不存在跨環(huán)境時區(qū)轉(zhuǎn)換錯誤;
  • 兼容性好:無論使用MySQL、PostgreSQL還是Oracle,BIGINT類型都是通用支持的,無需額外適配。

有同學(xué)可能會問:“用BIGINT存數(shù)字,我想在數(shù)據(jù)庫里直接查看具體時間,豈不是很麻煩?” 其實一點都不復(fù)雜,寫SQL時簡單轉(zhuǎn)換一下即可:

SELECT from_unixtime(create_time/1000) FROM orders;

這里除以1000,是因為我們存的是毫秒級時間戳,而MySQL的from_unixtime函數(shù)接收的是秒級時間戳,根據(jù)自己的存儲精度調(diào)整即可。

三、Java中的使用:優(yōu)先選Instant,按需轉(zhuǎn)LocalDateTime

解決了數(shù)據(jù)庫存儲(BIGINT時間戳)的問題,接下來重點聊核心疑問:數(shù)據(jù)庫存的是BIGINT時間戳,Java代碼里為什么不直接用long類型操作?反而要先映射成Instant?

總結(jié)來說,不直接用long時間戳、優(yōu)先將BIGINT映射到Instant,核心有3點原因,每一點都能幫我們避開生產(chǎn)坑:

  • 語義更清晰,避免歧義:long類型只是一個單純的數(shù)字,你無法直接判斷它是毫秒級、秒級時間戳,還是普通的計數(shù);而Instant明確表示“絕對時間點”,與數(shù)據(jù)庫的BIGINT時間戳語義完全對應(yīng),一看就知道是“某個具體的瞬間”,無需額外注釋。
  • 操作更便捷,減少計算錯誤:long類型計算時間差(如“7天前”),需要手動換算毫秒數(shù)(72460601000L),極易漏算、錯算;而Instant自帶豐富的API(如minus、plus),可以直接按天、小時、分鐘操作,無需手動換算,從根源上避免錯誤。
  • 可擴展性更強,適配復(fù)雜場景:long類型只能做簡單的大小比較和差值計算,無法直接轉(zhuǎn)換時區(qū)、格式化展示;而Instant可以輕松轉(zhuǎn)為LocalDateTime、ZonedDateTime,適配業(yè)務(wù)邏輯處理、前端展示等多種復(fù)雜場景,無需額外封裝工具類。

而Instant,正是為解決long時間戳的痛點而生,它與BIGINT時間戳是“天生一對”,也是我們將數(shù)據(jù)庫BIGINT映射到Java實體類的首選。

延伸疑問:為什么還要把Instant轉(zhuǎn)成LocalDateTime

有同學(xué)會問:“既然Instant這么好,為什么不全程用Instant?還要轉(zhuǎn)成LocalDateTime,多此一舉?”

答案很簡單:Instant適合“機器處理”,LocalDateTime適合“人類交互”。兩者的定位不同,各司其職——我們將數(shù)據(jù)庫BIGINT映射為Instant,是為了保證數(shù)據(jù)語義準(zhǔn)確、操作便捷;而將Instant轉(zhuǎn)為LocalDateTime,是為了適配“與人相關(guān)”的業(yè)務(wù)場景,讓代碼更易讀、更貼合實際需求。

哪些情況用Instant直接處理?哪些情況要轉(zhuǎn)LocalDateTime

結(jié)合實際項目經(jīng)驗,我整理了清晰的場景劃分,一看就懂:

可直接用Instant處理的場景(無需轉(zhuǎn)LocalDateTime)

Instant的核心優(yōu)勢是“絕對時間點”,無需考慮時區(qū),適合所有“機器層面”的時間操作,主要有3類場景:

  • 時間比較操作:比如判斷訂單創(chuàng)建時間是否在30分鐘內(nèi)、用戶注冊時間是否超過7天,直接用Instant的isBefore、isAfter方法,語義清晰、操作便捷,無需轉(zhuǎn)換。
  • 時間差值計算(無需展示):比如計算兩個操作之間的時間間隔(如接口調(diào)用耗時),用Instant的until方法,可直接獲取天、小時、分鐘等差值,無需轉(zhuǎn)為LocalDateTime。
  • 數(shù)據(jù)存儲與傳輸(中間過程):實體類中映射數(shù)據(jù)庫BIGINT、服務(wù)間傳輸時間信息,直接用Instant,無需轉(zhuǎn)換——它語義準(zhǔn)確、體積小,還能避免時區(qū)錯亂。

舉個直接用Instant處理的示例(時間比較):

// 訂單創(chuàng)建時間(Instant),判斷是否在30分鐘內(nèi)
Instant orderCreateTime = order.getCreateTime();
Instant now = Instant.now();
// 直接用Instant API判斷,無需轉(zhuǎn)LocalDateTime
if (orderCreateTime.isAfter(now.minus(30, ChronoUnit.MINUTES))) {
    System.out.println("訂單創(chuàng)建時間在30分鐘內(nèi)");
}

需要將Instant轉(zhuǎn)為LocalDateTime的場景

當(dāng)時間需要“被人閱讀”“與人交互”時,就需要將Instant轉(zhuǎn)為LocalDateTime,主要有4類場景,每一類都貼合實際業(yè)務(wù):

  • 前端展示時間:將后端的時間數(shù)據(jù)返回給前端,需要展示為“yyyy-MM-dd HH:mm:ss”格式(如訂單創(chuàng)建時間、用戶注冊時間),需先將Instant轉(zhuǎn)為LocalDateTime,再用DateTimeFormatter格式化。
  • 與人相關(guān)的業(yè)務(wù)計算:比如用戶生日、店鋪營業(yè)時間、本地定時任務(wù)(每天8點執(zhí)行),這些場景關(guān)注的是“本地時間”,與人的生活習(xí)慣相關(guān),適合用LocalDateTime。
  • 接收前端傳入的時間參數(shù):前端傳遞的日期字符串(如“2025-05-08 10:10:10”),通常對應(yīng)本地時間,先解析為LocalDateTime,再根據(jù)需求轉(zhuǎn)為Instant存儲。
  • 日志打印與調(diào)試:開發(fā)調(diào)試時,打印時間信息,用LocalDateTime能直觀看到具體的日期時間,方便排查問題;若打印Instant,還需要手動轉(zhuǎn)換才能看懂。

舉個Instant轉(zhuǎn)LocalDateTime的示例(前端展示):

// 實體類中的Instant(映射數(shù)據(jù)庫BIGINT)
Instant orderCreateTime = order.getCreateTime();
// 轉(zhuǎn)為LocalDateTime(指定時區(qū),避免錯亂)
LocalDateTime localDateTime = orderCreateTime.atZone(ZoneId.of("Asia/Shanghai")).toLocalDateTime();
// 格式化后返回給前端
String formatTime = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(localDateTime);

這里補充一個關(guān)鍵注意點:Instant轉(zhuǎn)LocalDateTime時,必須指定時區(qū)(如Asia/Shanghai),因為Instant本身無時區(qū),不指定時區(qū)會默認使用系統(tǒng)時區(qū),可能導(dǎo)致時間錯亂。

四、格式化:徹底告別SimpleDateFormat

回到文章開頭的報警事件,罪魁禍?zhǔn)拙褪?strong>SimpleDateFormat的線程不安全問題。這也是很多老項目的通病,我們先看看常見的錯誤用法,再講正確的姿勢。

先看兩個錯誤示范,尤其是第二個,幾乎是“踩坑重災(zāi)區(qū)”:

// 錯誤示范1:每個請求都new一個,浪費資源
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String dateStr = formatter.format(new Date());
// 錯誤示范2:定義成static共享,線程不安全?。ǜ卟l(fā)下必出問題)
private static final SimpleDateFormat FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

SimpleDateFormat之所以線程不安全,是因為它內(nèi)部有可修改的成員變量,多線程并發(fā)調(diào)用時,會出現(xiàn)資源競爭,導(dǎo)致格式化結(jié)果錯亂、拋出異常(就像我這次遇到的一樣)。

而Java 8提供的DateTimeFormatter,完美解決了這個問題——它是不可變的、線程安全的,可以放心地定義成靜態(tài)常量,全局復(fù)用。

推薦的工具類寫法如下,兼顧通用性和安全性:

public class DateUtils {
    // 定義為靜態(tài)常量,全局復(fù)用,線程安全
    private static final DateTimeFormatter FORMATTER =
        DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
   
    /**
     * 格式化Instant(需要指定時區(qū),因為Instant無時區(qū))
     */
    public static String format(Instant instant) {
        // 這里用系統(tǒng)默認時區(qū),也可根據(jù)業(yè)務(wù)指定(如ZoneId.of("Asia/Shanghai"))
        ZonedDateTime dateTime = instant.atZone(ZoneId.systemDefault());
        return dateTime.format(FORMATTER);
    }
   
    /**
     * 格式化LocalDateTime(自帶本地時區(qū)含義,可直接格式化)
     */
    public static String format(LocalDateTime dateTime) {
        return dateTime.format(FORMATTER);
    }
   
    /**
     * 將字符串解析為Instant(反向操作)
     */
    public static Instant parse(String dateStr) {
        // 先解析成LocalDateTime,再轉(zhuǎn)Instant(指定時區(qū))
        LocalDateTime dateTime = LocalDateTime.parse(dateStr, FORMATTER);
        return dateTime.atZone(ZoneId.systemDefault()).toInstant();
    }
}

這里有一個關(guān)鍵注意點,一定要記牢:格式化Instant時,必須指定時區(qū)——因為Instant本身不包含時區(qū)信息,直接格式化會報錯;而LocalDateTime自帶“本地”時區(qū)含義,無需額外指定時區(qū),可直接格式化。

五、Spring Boot中的實戰(zhàn)技巧:入?yún)ⅰ⒎祷?、?shù)據(jù)庫交互全適配

在實際的Spring Boot項目中,我們通常需要和前端交互(接收前端日期字符串、返回格式化后的日期),還要和數(shù)據(jù)庫交互(自動轉(zhuǎn)換時間戳和Instant)。這里分享3個實用技巧,幫你簡化開發(fā),避免重復(fù)編碼。

1. 接收前端數(shù)據(jù):@DateTimeFormat

前端傳遞的日期通常是字符串(如“2025-05-08 10:10:10”),我們無需手動解析,用@DateTimeFormat注解即可自動將字符串轉(zhuǎn)換為LocalDateTime/ZonedDateTime:

public class UserDTO {
   
    // 前端傳"2025-05-08 10:10:10",自動轉(zhuǎn)換為LocalDateTime
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime birthday;
   
    // getter/setter省略
}

注意:@DateTimeFormat主要用于接收前端的請求參數(shù)(如GET請求的參數(shù)、POST請求的表單參數(shù)),如果是JSON格式的請求體,需要用下面的@JsonFormat注解。

2. 返回給前端:@JsonFormat

我們需要將Java中的日期類型(LocalDateTime/Instant),格式化后以字符串形式返回給前端,用@JsonFormat注解即可實現(xiàn),還能指定時區(qū):

public class UserVO {
   
    // 返回給前端時,格式化為"yyyy/MM/dd HH:mm:ss",指定時區(qū)為GMT+8(北京時間)
    @JsonFormat(pattern = "yyyy/MM/dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime createTime;
   
    // getter/setter省略
}

這里建議明確指定timezone為“GMT+8”,避免因服務(wù)器時區(qū)配置不同,導(dǎo)致返回給前端的時間錯亂。

3. 數(shù)據(jù)庫交互(MyBatis):自定義TypeHandler自動轉(zhuǎn)換

之前我們說過,數(shù)據(jù)庫存BIGINT時間戳,實體類用Instant——如果每次查詢、插入都手動轉(zhuǎn)換,會非常繁瑣。這時可以自定義MyBatis的TypeHandler,讓框架自動幫我們完成轉(zhuǎn)換。

自定義InstantTypeHandler的代碼如下:

public class InstantTypeHandler extends BaseTypeHandler<Instant> {
   
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i,
                                     Instant parameter, JdbcType jdbcType) throws SQLException {
        // 插入/更新時,將Instant轉(zhuǎn)為BIGINT(毫秒級)
        ps.setLong(i, parameter.toEpochMilli());
    }
   
    @Override
    public Instant getNullableResult(ResultSet rs, String columnName) throws SQLException {
        // 查詢時,將BIGINT轉(zhuǎn)為Instant
        long timestamp = rs.getLong(columnName);
        return Instant.ofEpochMilli(timestamp);
    }
   
    // 其他兩個方法(getNullableResult的另外兩種重載)省略,實現(xiàn)邏輯類似
}

定義好TypeHandler后,在MyBatis的配置文件中注冊,或者在實體類的字段上直接指定,MyBatis就會自動幫你完成BIGINT和Instant的轉(zhuǎn)換,無需手動處理,極大提升開發(fā)效率。

六、總結(jié)所有最佳實踐

5條黃金法則,記牢這5條,就能避開絕大多數(shù)日期處理坑:

  • 存儲用時間戳:數(shù)據(jù)庫字段一律用BIGINT存毫秒值,兼顧性能和兼容性;
  • 映射用Instant:實體類里用Instant對應(yīng)BIGINT,語義準(zhǔn)確,避免時區(qū)歧義;
  • 業(yè)務(wù)用LocalDateTime:給人看的時間、日期計算,用LocalDateTime,代碼更易讀;
  • 比較用Instant API:時間比較用isBefore()、isAfter(),清晰表達業(yè)務(wù)意圖,避免計算錯誤;
  • 格式化用DateTimeFormatter:定義成static final常量,全局復(fù)用,線程安全。

到此這篇關(guān)于SpringBoot項目中日期處理的最佳實踐的文章就介紹到這了,更多相關(guān)SpringBoot日期處理內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • 詳解Maven安裝教程及是否安裝成功

    詳解Maven安裝教程及是否安裝成功

    這篇文章主要介紹了詳解Maven安裝教程及是否安裝成功,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2018-12-12
  • 基于IDEA中格式化代碼的快捷鍵分享

    基于IDEA中格式化代碼的快捷鍵分享

    這篇文章主要介紹了基于IDEA中格式化代碼的快捷鍵分享,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2021-02-02
  • spring框架集成flyway項目的詳細過程

    spring框架集成flyway項目的詳細過程

    今天通過本文給大家分享spring框架集成flyway項目的詳細過程,由于大多數(shù)都是springboot集成flyway,很少見到spring框架的項目,今天就抽空給大家介紹下spring框架集成flyway項目的方法,一起看看吧
    2021-07-07
  • java實現(xiàn)區(qū)域內(nèi)屏幕截圖示例

    java實現(xiàn)區(qū)域內(nèi)屏幕截圖示例

    這篇文章主要介紹了java截圖示例,需要的朋友可以參考下
    2014-04-04
  • Java使用@EnableEurekaServer實現(xiàn)自動裝配詳解

    Java使用@EnableEurekaServer實現(xiàn)自動裝配詳解

    這篇文章主要介紹了Java使用@EnableEurekaServer實現(xiàn)自動裝配過程,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧
    2022-10-10
  • Java并發(fā)Map面試線程安全數(shù)據(jù)結(jié)構(gòu)全面分析

    Java并發(fā)Map面試線程安全數(shù)據(jù)結(jié)構(gòu)全面分析

    本文將探討如何在Java中有效地應(yīng)對這些挑戰(zhàn),介紹一種強大的工具并發(fā)Map,它能夠幫助您管理多線程環(huán)境下的共享數(shù)據(jù),確保數(shù)據(jù)的一致性和高性能,深入了解Java中的并發(fā)Map實現(xiàn),包括ConcurrentHashMap和ConcurrentSkipListMap,及相關(guān)知識點
    2023-09-09
  • 關(guān)于Swagger注釋API的使用說明

    關(guān)于Swagger注釋API的使用說明

    這篇文章主要介紹了關(guān)于Swagger注釋API的使用說明,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-06-06
  • Spring Boot使用Value注解給靜態(tài)變量賦值的方法

    Spring Boot使用Value注解給靜態(tài)變量賦值的方法

    這篇文章主要介紹了Spring Boot使用Value注解給靜態(tài)變量賦值的方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2018-07-07
  • Java實戰(zhàn)項目之斗地主和斗牛游戲的實現(xiàn)

    Java實戰(zhàn)項目之斗地主和斗牛游戲的實現(xiàn)

    讀萬卷書不如行萬里路,只學(xué)書上的理論是遠遠不夠的,只有在實戰(zhàn)中才能獲得能力的提升,本篇文章手把手帶你用Java實現(xiàn)一個斗地主和一個斗牛游戲,大家可以在過程中查缺補漏,提升水平
    2021-11-11
  • idea中springboot項目連接數(shù)據(jù)庫報錯的原因解析

    idea中springboot項目連接數(shù)據(jù)庫報錯的原因解析

    這篇文章主要介紹了idea中springboot項目連接數(shù)據(jù)庫報錯的原因解析,本文給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2020-12-12

最新評論

富川| 玛沁县| 沙河市| 从化市| 上蔡县| 合山市| 冕宁县| 高唐县| 长治市| 汉川市| 石泉县| 安化县| 华安县| 西乌珠穆沁旗| 从化市| 大埔区| 周口市| 房山区| 曲水县| 肃宁县| 涡阳县| 永定县| 娄底市| 辉县市| 轮台县| 林芝县| 于田县| 泸水县| 怀集县| 晋江市| 北川| 奎屯市| 普洱| 民乐县| 察雅县| 土默特右旗| 永嘉县| 承德县| 玉山县| 嘉兴市| 汉阴县|