
該圖檔由Анастасия Белоусова在Pixabay上釋出
你好,我是看山。
這次說一下,怎樣檢查給出的字元串,是否是合法日期字元串。本文将從 Java 原生和第三方元件兩種方式來說明。
WHY
後端接口在接收資料的時候,都需要進行檢查。檢查全部通過後,才能夠執行業務邏輯。對于時間格式,我們一般需要檢查這麼幾方面:
字元串格式是否正确,比如格式是不是yyyy-MM-dd
時間在合法範圍内,比如我們需要限定在一個月内的時間
字元串可以解析為正常的時間,比如 2 月 30 号就不是正常時間
對于時間格式的判斷,我們可以通過正規表達式來檢查。不過考慮到正規表達式的性能、輸入資料的複雜性,一般能用别的方式,就不選正規表達式。我們還是選擇一種更加通用、更加高效的檢查方式。
首先,定義時間校驗器的接口:
public interface DateValidator {
boolean isValid(String dateStr);
}
接口方法接收一個字元串,傳回布爾類型,表示字元串是否是合法的時間格式。
HOW
接下來就是通過不同方式實作DateValidator。
使用 DateFormat 檢查
Java 提供了格式化和解析時間的工具:DateFormat抽象類和SimpleDataFormat實作類。我們借此實作時間校驗器:
public class DateValidatorUsingDateFormat implements DateValidator {
private final String dateFormat;
public DateValidatorUsingDateFormat(String dateFormat) {
this.dateFormat = dateFormat;
}
@Override
public boolean isValid(String dateStr) {
final DateFormat sdf = new SimpleDateFormat(this.dateFormat);
sdf.setLenient(false);
try {
sdf.parse(dateStr);
} catch (ParseException e) {
return false;
}
return true;
}
}
這裡需要注意一下,DateFormat和SimpleDataFormat是非線程安全的,是以每次方法調用時,都需要建立執行個體。
我們通過單元測試驗證下:
class DateValidatorUsingDateFormatTest {
@Test
void isValid() {
final DateValidator validator = new DateValidatorUsingDateFormat("yyyy-MM-dd");
Assertions.assertTrue(validator.isValid("2021-02-28"));
Assertions.assertFalse(validator.isValid("2021-02-30"));
}
}
在 Java8 之前,一般都是用這種方式來驗證。Java8 之後,我們有了更多的選擇。
使用 LocalDate 檢查
Java8 引入了更加好用日期和時間 API(想要了解更多内容,請移步參看 Java8 中的時間類及常用 API)。其中包括LocalDate類,是一個不可變且線程安全的時間類。
LocalDate提供了兩個靜态方法,用來解析時間。這兩個方法内部都是使用java.time.format.DateTimeFormatter來處理資料:
// 使用 DateTimeFormatter.ISO_LOCAL_DATE 處理資料
public static LocalDate parse(CharSequence text) {
return parse(text, DateTimeFormatter.ISO_LOCAL_DATE);
}
// 使用提供的 DateTimeFormatter 處理資料
public static LocalDate parse(CharSequence text, DateTimeFormatter formatter) {
Objects.requireNonNull(formatter, "formatter");
return formatter.parse(text, LocalDate::from);
}
通過LocalDate的parse方法實作我們的校驗器:
public class DateValidatorUsingLocalDate implements DateValidator {
private final DateTimeFormatter dateFormatter;
public DateValidatorUsingLocalDate(DateTimeFormatter dateFormatter) {
this.dateFormatter = dateFormatter;
}
@Override
public boolean isValid(String dateStr) {
try {
LocalDate.parse(dateStr, this.dateFormatter);
} catch (DateTimeParseException e) {
return false;
}
return true;
}
}
java.time.format.DateTimeFormatter類是不可變的,也就是天然的線程安全,我們可以在不同線程使用同一個校驗器執行個體。
class DateValidatorUsingLocalDateTest {
@Test
void isValid() {
final DateTimeFormatter dateFormatter = DateTimeFormatter.ISO_LOCAL_DATE;
final DateValidator validator = new DateValidatorUsingLocalDate(dateFormatter);
Assertions.assertTrue(validator.isValid("2021-02-28"));
Assertions.assertFalse(validator.isValid("2021-02-30"));
}
}
既然LocalDate#parse是通過DateTimeFormatter實作的,那我們也可以直接使用DateTimeFormatter。
使用 DateTimeFormatter 檢查
DateTimeFormatter解析文本總共分兩步。第一步,根據配置将文本解析為日期和時間字段;第二步,用解析後的字段建立日期和時間對象。
實作驗證器:
public class DateValidatorUsingDateTimeFormatter implements DateValidator {
private final DateTimeFormatter dateFormatter;
public DateValidatorUsingDateTimeFormatter(DateTimeFormatter dateFormatter) {
this.dateFormatter = dateFormatter;
}
@Override
public boolean isValid(String dateStr) {
try {
this.dateFormatter.parse(dateStr);
} catch (DateTimeParseException e) {
return false;
}
return true;
}
}
通過單元測試驗證:
class DateValidatorUsingDateTimeFormatterTest {
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("uuuu-MM-dd", Locale.CHINA);
@Test
void isValid() {
final DateTimeFormatter dateFormatter = DATE_FORMATTER.withResolverStyle(ResolverStyle.STRICT);
final DateValidator validator = new DateValidatorUsingDateTimeFormatter(dateFormatter);
Assertions.assertTrue(validator.isValid("2021-02-28"));
Assertions.assertFalse(validator.isValid("2021-02-30"));
}
}
可以看到,我們指定了轉換模式是ResolverStyle.STRICT,這個類型是說明解析模式。共有三種:
STRICT:嚴格模式,日期、時間必須完全正确。
SMART:智能模式,針對日可以自動調整。月的範圍在 1 到 12,日的範圍在 1 到 31。比如輸入是 2 月 30 号,當年 2 月隻有 28 天,傳回的日期就是 2 月 28 日。
LENIENT:寬松模式,主要針對月和日,會自動後延。結果類似于LocalData#plusDays或者LocalDate#plusMonths。
我們通過例子看下差別:
class DateValidatorUsingDateTimeFormatterTest {
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("uuuu-MM-dd", Locale.CHINA);
@Test
void testResolverStyle() {
Assertions.assertEquals(LocalDate.of(2021, 2,28), parseDate("2021-02-28", ResolverStyle.STRICT));
Assertions.assertNull(parseDate("2021-02-29", ResolverStyle.STRICT));
Assertions.assertEquals(LocalDate.of(2021, 2,28), parseDate("2021-02-28", ResolverStyle.STRICT));
Assertions.assertNull(parseDate("2021-13-28", ResolverStyle.STRICT));
Assertions.assertEquals(LocalDate.of(2021, 2,28), parseDate("2021-02-28", ResolverStyle.SMART));
Assertions.assertEquals(LocalDate.of(2021, 2,28), parseDate("2021-02-29", ResolverStyle.SMART));
Assertions.assertNull(parseDate("2021-13-28", ResolverStyle.SMART));
Assertions.assertNull(parseDate("2021-13-29", ResolverStyle.SMART));
Assertions.assertEquals(LocalDate.of(2021, 2,28), parseDate("2021-02-28", ResolverStyle.LENIENT));
Assertions.assertEquals(LocalDate.of(2021, 3,1), parseDate("2021-02-29", ResolverStyle.LENIENT));
Assertions.assertEquals(LocalDate.of(2022, 1,28), parseDate("2021-13-28", ResolverStyle.LENIENT));
Assertions.assertEquals(LocalDate.of(2022, 2,2), parseDate("2021-13-33", ResolverStyle.LENIENT));
}
private static LocalDate parseDate(String dateString, ResolverStyle resolverStyle) {
try {
return LocalDate.parse(dateString, DATE_FORMATTER.withResolverStyle(resolverStyle));
} catch (DateTimeParseException e) {
return null;
}
}
}
從例子可以看出,ResolverStyle.STRICT是嚴格控制,用來做時間校驗比較合适;ResolverStyle.LENIENT可以最大程度将字元串轉化為時間對象,在合理範圍内可以随便玩;ResolverStyle.SMART名為智能,但智力有限,兩不沾邊,優勢不夠明顯。JDK 提供的DateTimeFormatter實作,都是ResolverStyle.STRICT模式。
說了 JDK 自帶的實作,接下來說說第三方元件的實作方式。
使用 Apache 出品的 commons-validator 檢查
Apache Commons 項目提供了一個校驗器架構,包含多種校驗規則,包括日期、時間、數字、貨币、IP 位址、郵箱、URL 位址等。本文主要說檢查時間,是以重點看看GenericValidator類提供的isDate方法:
public class GenericValidator implements Serializable {
// 其他方法
public static boolean isDate(String value, Locale locale) {
return DateValidator.getInstance().isValid(value, locale);
}
public static boolean isDate(String value, String datePattern, boolean strict) {
return org.apache.commons.validator.DateValidator.getInstance().isValid(value, datePattern, strict);
}
}
先引入依賴:
<dependency>
<groupId>commons-validator</groupId>
<artifactId>commons-validator</artifactId>
<version>1.7</version>
</dependency>
public class DateValidatorUsingCommonsValidator implements DateValidator {
private final String dateFormat;
public DateValidatorUsingCommonsValidator(String dateFormat) {
this.dateFormat = dateFormat;
}
@Override
public boolean isValid(String dateStr) {
return GenericValidator.isDate(dateStr, dateFormat, true);
}
}
class DateValidatorUsingCommonsValidatorTest {
@Test
void isValid() {
final DateValidator dateValidator = new DateValidatorUsingCommonsValidator("yyyy-MM-dd");
Assertions.assertTrue(dateValidator.isValid("2021-02-28"));
Assertions.assertFalse(dateValidator.isValid("2021-02-30"));
}
}
看org.apache.commons.validator.DateValidator#isValid源碼可以發現,内部是通過DateFormat和SimpleDateFormat實作的。
文末總結
在本文中,我們通過四種方式實作了時間字元串校驗邏輯。為了節省篇幅,文中代碼隻提供了核心内容。想要了解具體實作,可以關注公号「看山的小屋」,回複“date”擷取源碼。
推薦閱讀
Java8 中的時間類及常用 API
Date 與 LocalDate 或 LocalDateTime 互相轉換
使用 Java8 中的時間類
檢查日期字元串是否合法