在JavaWeb程序的開發過程中,接口是前后端對接的主要窗口,而接口參數的接收有時候是一個令人頭疼的事情,這其中最困擾程序猿的,應該是時間參數的接收。
比如:設置一個用戶的過期時間,前端到底以什么格式傳遞參數呢?時間戳?還是2019-12-01 22:13:00這種格式?還是其他格式?
今天我就來總結一下SpringBoot Web應用接口接收時間類型參數的問題解決方案。
注:目前我對Spring源碼的掌握還不是很好,所以這一篇僅僅總結一下解決方法,后面感悟多了會重寫一下!😎
示例代碼請前往:https://github.com/laolunsi/spring-boot-examples
經過簡單的測試,我們知道:
-
不使用@RequestBody注解的情況下,所有時間類型參數都會引起報錯;
-
使用@RequestBody,前端傳遞時間戳或
2019-11-22
形式正常,傳遞2019-11-22 11:22:22
報錯,其他格式同樣報錯。
之前有接觸過類似的解決辦法,在類的屬性上加上@DateFormat注解,解決單個時間參數問題。
但是局限較多。
理想的解決方案是:一次配置,全局通用,多種格式,自動轉換(朗朗上口嗷)
一、源碼簡要分析
首先我們來簡單分析一下源碼:
深入的就不解釋了(我現在也不懂🤦♂️)
簡單來說,接口接收的參數,首先被HandlerMethodArgumentResolver的實現類處理了一遍,將其轉換為我們需要的格式。
這里主要分為兩種情況:
- 使用了@RequestBody的參數,一般是對象接收,前端傳遞的通常是JSON形式
- 其他接收參數的方式,比如@RequestAttribute,@RequestParam,或者默認形式,前端傳遞的通常是表單參數、請求URL后綴參數等
二、解決方法
- 默認形式,或使用@RequestAttribute,或使用@RequestParam,這樣的參數,通過配置converter來解決問題
- 使用@RequestBody解析的參數,通過在ObjectMapper中配置序列化和反序列化規則來處理
2.1 自定義converter
針對第一種情況,我們需要配置converter,這里介紹兩種方法:
- @ControllerAdvice + @InitBinder
- 直接使用@Bean定義converter類
首先我們這里需要一個DateConverter類,這個類實現了Converter接口,重寫了其中的convert方法,將String轉成Date類型:
我們這里定義了三種處理格式:
/**
* 日期轉換類
* 將標准日期、標准日期時間、時間戳轉換成Date類型
*/
/*@Deprecated*/
public class DateConverter implements Converter<String, Date> {
private Logger logger = LoggerFactory.getLogger(DateConverter.class);
private static final String dateFormat = "yyyy-MM-dd HH:mm:ss";
private static final String shortDateFormat = "yyyy-MM-dd";
private static final String timeStampFormat = "^\\d+$";
@Override
public Date convert(String value) {
logger.info("轉換日期:" + value);
if(value == null || value.trim().equals("") || value.equalsIgnoreCase("null")) {
return null;
}
value = value.trim();
try {
if (value.contains("-")) {
SimpleDateFormat formatter;
if (value.contains(":")) {
formatter = new SimpleDateFormat(dateFormat);
} else {
formatter = new SimpleDateFormat(shortDateFormat);
}
return formatter.parse(value);
} else if (value.matches(timeStampFormat)) {
Long lDate = new Long(value);
return new Date(lDate);
}
} catch (Exception e) {
throw new RuntimeException(String.format("parser %s to Date fail", value));
}
throw new RuntimeException(String.format("parser %s to Date fail", value));
}
}
注:這個DateConverter類在下面都會用到。
import com.aegis.yqmanagecenter.config.date.DateConverter;
import com.aegis.yqmanagecenter.model.bean.common.JsonResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.propertyeditors.CustomDateEditor;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.*;
import java.beans.PropertyEditorSupport;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
@ControllerAdvice
public class ControllerHandler {
private Logger logger = LoggerFactory.getLogger(ControllerHandler.class);
@InitBinder
public void initBinder(WebDataBinder binder) {
// 方法1,注冊converter
GenericConversionService genericConversionService = (GenericConversionService) binder.getConversionService();
if (genericConversionService != null) {
genericConversionService.addConverter(new DateConverter());
}
// 方法2,定義單格式的日期轉換,可以通過替換格式,定義多個dateEditor,代碼不夠簡潔
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
CustomDateEditor dateEditor = new CustomDateEditor(df, true);
binder.registerCustomEditor(Date.class, dateEditor);
// 方法3,同樣注冊converter
binder.registerCustomEditor(Date.class, new PropertyEditorSupport() {
@Override
public void setAsText(String text) throws IllegalArgumentException {
setValue(new DateConverter().convert(text));
}
});
}
}
注:上面的三個方法都是利用@ControllerAdvice+@InitBinder來設置時間參數處理的,其中1和3都可以設置DateConverter,而方法2只能一個一個手動設置格式。
這里需要注意,上述配置方法都無法解決Json格式數據中的時間參數接收問題。下面我們直接看完整的解決方案——將DateConverter注冊為組件,並使用ObjectMapper來配置時間參數的序列化(接口返回值)和反序列化形式(接口接收參數)。
2.2 配置ObjectMapper以及完整解決方案
完整的解決方案:
/**
* 日期轉換配置
* 解決@RequestAttribute、@RequestParam和@RequestBody三種類型的時間類型參數接收與轉換問題
*/
@Configuration
public class DateConfig {
/**
* 默認日期時間格式
*/
public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
/**
* Date轉換器,用於轉換RequestParam和PathVariable參數
*/
@Bean
public Converter<String, Date> dateConverter() {
return new DateConverter();
}
/**
* Json序列化和反序列化轉換器,用於轉換Post請求體中的json以及將我們的對象序列化為返回響應的json
* 使用@RequestBody注解的對象中的Date類型將從這里被轉換
*/
@Bean
public ObjectMapper objectMapper(){
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
objectMapper.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE);
JavaTimeModule javaTimeModule = new JavaTimeModule();
//Date序列化和反序列化
javaTimeModule.addSerializer(Date.class, new JsonSerializer<Date>() {
@Override
public void serialize(Date date, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
SimpleDateFormat formatter = new SimpleDateFormat(DEFAULT_DATE_TIME_FORMAT);
String formattedDate = formatter.format(date);
jsonGenerator.writeString(formattedDate);
}
});
javaTimeModule.addDeserializer(Date.class, new JsonDeserializer<Date>() {
@Override
public Date deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
return new DateConverter().convert(jsonParser.getText());
}
});
objectMapper.registerModule(javaTimeModule);
return objectMapper;
}
}