本文探討SpringBoot應用中的序列化和反序列化過程,主要是SpringBoot默認的Jackson庫的注解使用和代碼演示。
基礎知識
序列化
序列化是指將內存中的對象狀態信息轉換為可以存儲或傳輸的形式的過程,
與之對應,反序列化就是存儲或傳輸形式轉為對象狀態信息的過程
序列化有多種方式,常用的有Java原生提供的字節格式和json格式。現在REST應用主要使用json格式傳輸數據。
jackson序列化規則
public修飾的字段序列化時會直接讀取字段值,不需要有get方法。如果有get方法的話,以get方法返回的字段值優先。
默認情況下其他訪問權限的屬性沒有get方法的話不會參與序列化。
序列化的字段名命名規則是類中的 get方法
去掉get后首字母小寫,與之類似,反序列化對應的是set方法
。比如getUser
序列化之后就是user
,所以一些不標准的命名方式會出現意料之外的問題,比如``getCtime`。
雙向注解
即序列化與反序列化過程中都有作用的注解,jackson官方沒有對這種注解起名字,是我自己造的詞
@JsonIgnoreProperties
類級別注解,兩個主要作用
-
在序列化過程中標識忽略一個或多個屬性;
-
反序列化過程中,表明忽略json中多余的屬性,否則會報錯。
第一個作用的代碼示例:
@NoArgsConstructor
@JsonIgnoreProperties(value = {"gender", "age"})
public class User {
@Getter @Setter private String name = "亞當";
@Getter @Setter private String gender = "male";
@Getter @Setter private int age = 0;
}
@GetMapping("serializeJsonIgnoreProperties")
public User serializeJsonIgnoreProperties() {
return new User();
}
這里因為使用了注解忽略了屬性,所以只會返回name一個屬性,去掉注解的話會返回全部三個屬性:
//加上@JsonIgnoreProperties(value = {"gender", "age"})
{
"name": "亞當"
}
//去掉注解以后:
{
"name": "亞當",
"gender": "male",
"age": 0
}
接下來我們看第二個作用,即忽略json中多余的屬性,先不使用注解:
@NoArgsConstructor
public class Person {
@Getter @Setter private String name = "夏娃";
@Getter @Setter private String gender = "female";
@Getter @Setter private int age = 0;
}
@PostMapping("deserializeJsonIgnoreProperties")
public Person deserializeJsonIgnoreProperties(@RequestBody Person person) {
return person;
}
//傳入
{
"name": "demoData",
"gender": "demoData",
"age": 1,
"action":"eatApple"//多余屬性
}
結果我們發現即使有多余屬性,也沒有報錯,不符合預期,難道jackson的注解失效了?我們再試一下使用ObjectMapper
手動序列化
@GetMapping("deserializeJsonIgnorePropertiesWithOM")
public Person deserializeJsonIgnorePropertiesWithOM() throws JsonProcessingException {
String json =
"{\"name\": \"demoData\",\n"
+ "\"gender\": \"demoData\",\n"
+ "\"age\": 1,\n"
+ "\"action\":\"eatApple\"\n }";
return new ObjectMapper().readValue(json, Person.class);
}
//報錯
com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "action" (class com.hans.jackson.vo.jsonignoreproperties.Person), not marked as ignorable (3 known properties: "gender", "name", "age"])
//加上注解,不再報錯
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class Person {
@Getter @Setter private String name = "夏娃";
@Getter @Setter private String gender = "female";
@Getter @Setter private int age = 0;
}
至此我們得出結論,@JsonIgnoreProperties(ignoreUnknown = true)
是有效的,在SpringBoot中無效是因為SpringBoot對ObjectMapper
進行了定制化配置。具體參見SpringBoot官方文檔
@JsonProperty,@JsonGetter,@JsonSetter
用在不標准的get或set方法上,對序列化和反序列化的字段名進行重命名。
@NoArgsConstructor
public class Student {
@Getter @Setter private String name;
@Getter @Setter private int age;
@Getter @Setter private double score;
private String grade = "三年級";
public String getMessGrade() {
return grade;
}
}
@GetMapping("serializeJsonProperty")
public Student serializeJsonProperty() {
return new Student();
}
//接口返回
{
"name": null,
"age": 0,
"score": 0.0,
"messGrade": "三年級"
}
修改實體,加上@JsonProperty注解
@NoArgsConstructor
public class Student {
@Getter @Setter private String name;
@Getter @Setter private int age;
@Getter @Setter private double score;
private String grade = "三年級";
@JsonProperty("grade")
public String getMessGrade() {
return grade;
}
}
//接口返回
{
"name": null,
"age": 0,
"score": 0.0,
"grade": "三年級"
}
可以看到我們重新定義了返回json的字段名,同理在set方法上使用@JsonProperty
可以自定義入參json的字段名。
與之類似的還有用在get方法上的@JsonGetter
和用在set方法上的@JsonSetter
,這兩個注解是@JsonProperty
的特例,一般使用不到,官網是這樣描述他們的關系的:
The @JsonGetter annotation is an alternative to the @JsonProperty annotation
值得注意的是,@JsonProperty
也可以用在屬性上,會將字段名重命名的同時強制使該字段可見,在字段名符合首字母小寫,第二個字母大寫這種情況時,序列化會多返回一個字段,所以盡量不要在字段上直接使用@JsonProperty
。
這里有一段網上找到的原理說明:
jackson2對pojo類型序列化的處理。
Jackson2在初始化序列器時,對pojo類型對象會收集其屬性信息,屬性包括成員變量及方法,然后屬性名稱和處理過后的方法名稱做為key保存到一個LinkedHashMap中。收集過程中會調用com.fasterxml.jackson.databind.util.BeanUtil中的legacyManglePropertyName方法用來處理方法名稱,它會將get/set方法前綴,即get或set去掉,並將其后面的連續大寫字符轉換成小寫字符返回。例如: getNEWString會轉變成newstring返回。你的屬性名稱如果有這樣的"nSmallSellCount",lombok自動生成的get方法就會是這樣的"getNSmallSellCount",處理過后就是這樣的"nsmallSellCount",這與屬性nSmallSellCount並不沖突,可以同時存在於HashMap中。收集完屬性信息后,下面的步驟中會刪除掉非可見的屬性,一般指的是私有成員變量,這時,名稱為"nSmallSellCount"的成員變量屬性會被刪除掉,這樣的序列化結果是不會有問題的,但,如果加了@JsonProperty注釋,Jackson2會認為這個屬性是可見的,不必會刪除,這時這兩個表示同一個值得屬性就會被一同序列化。
@JsonUnwrapped
使用在嵌套類上,用來將嵌套類的屬性“扁平化”,官網稱其為unwrapped/flattened。同時用在序列化和反序列化過程中。
需要注意的是用了注解之后反序列時,之前的結構失效。
@NoArgsConstructor
@Getter
@Setter
public class Car {
private String name = "奔馳";
@JsonUnwrapped private Wheel wheel = new Wheel();
@NoArgsConstructor
@Getter
@Setter
public static class Wheel {
private String brand = "米其林";
private double price = 10.1;
}
}
/**序列化**/
@GetMapping("serializeJsonUnwrapped")
public Car serializeJsonUnwrapped() {
return new Car();
}
//接口返回
{
"name": "奔馳",
"brand": "米其林",
"price": 10.1
}
/**反序列化**/
@PostMapping("deserializeJsonUnwrapped")
public Car deserializeJsonUnwrapped(@RequestBody Car car) {
return car;
}
//入參
{
"name": "法拉利",
"brand": "黑馬",
"price": 10.222
}
//返回值
{
"name": "法拉利",
"brand": "黑馬",
"price": 10.222
}
//入參
{
"name":"法拉利",
"wheel":{
"brand":"黑馬",
"price":10.222
}
}
//brand和price屬性反序列化失敗,返回值:
{
"name": "法拉利",
"brand": "米其林",
"price": 10.1
}
@JsonAnyGetter,@JsonAnySetter
用在實體中的Map類型屬性的get/set方法上,來使序列化/反序列化過程中的json扁平化
,與@JsonUnwrapped
作用類似。
值得注意的是:這兩個注解只能用在返回值為Map類型的方法上。
@AllArgsConstructor
public class AnytterBean {
@Getter @Setter private String name;
private Map<String, String> properties;
@JsonAnyGetter
public Map<String, String> getProperties() {
return properties;
}
}
@GetMapping("serializeAnytterBean")
public AnytterBean serializeAnytterBean() {
HashMap<String, String> propertiesMap = new HashMap<>();
propertiesMap.put("game", "合金裝備");
propertiesMap.put("gender", "女");
return new AnytterBean("靜靜", propertiesMap);
}
//返回值
{
"name": "靜靜",
"game": "合金裝備",
"gender": "女"
}
//不使用注解返回值
{
"name": "靜靜",
"properties": {
"game": "合金裝備",
"gender": "女"
}
}
@JsonSerialize,@JsonDeserialize
通過實現StdSerializer
或StdDeSerializer
抽象類方法,以自定義序列化和反序列化規則。然后在字段上表明注解使用這個自定義規則。
public class CustomDateSerializer extends StdSerializer<Date> {
private static SimpleDateFormat formatter
= new SimpleDateFormat("dd-MM-yyyy hh:mm:ss");
@Override
public void serialize(
Date value, JsonGenerator gen, SerializerProvider arg2)
throws IOException, JsonProcessingException {
gen.writeString(formatter.format(value));
}
}
public class EventWithSerializer {
public String name;
@JsonSerialize(using = CustomDateSerializer.class)
public Date eventDate;
}
@JsonView
一個很有用的注解,用在序列化和反序列化過程中,可以根據不同情況定制化不同的視圖,視圖中選擇參與序列化和反序列化的具體字段,並且支持繼承。
public class ViewBean {
//聲明兩個視圖,子類視圖會繼承父類的字段
public interface BaseView {}
public interface DetailView extends BaseView {}
@JsonView(BaseView.class)
public String name;
@JsonView(BaseView.class)
public String color;
@JsonView(DetailView.class)
public String detail;
}
@GetMapping("serializeViewBean")
@JsonView(ViewBean.BaseView.class)//指定父類視圖
public ViewBean serializeViewBean() {
ViewBean viewBean = new ViewBean();
viewBean.name = "名字";
viewBean.color = "blue";
viewBean.detail = "view";
return viewBean;
}
//返回值,沒有包含detail字段
{
"name": "名字",
"color": "blue"
}
@GetMapping("serializeViewBean")
@JsonView(ViewBean.DetailView.class)//指定父類視圖
public ViewBean serializeViewBean() {
ViewBean viewBean = new ViewBean();
viewBean.name = "名字";
viewBean.color = "blue";
viewBean.detail = "view";
return viewBean;
}
//返回值,包含了全部字段
{
"name": "名字",
"color": "blue",
"detail": "view"
}
值得注意的是:
- 如果方法上指定了具體視圖,則實體中未被@JsonView修飾的屬性在任何視圖中都不會生效。
- 如果方法上沒有指定任何視圖,則實體中所有屬性都會返回,就像實體上沒有使用
@JsonView
注解一樣。
public class ViewBean {
public interface BaseView {}
public interface DetailView extends BaseView {}
//沒有@JsonView注解
public String name;
@JsonView(BaseView.class)
public String color;
@JsonView(DetailView.class)
public String detail;
}
@GetMapping("serializeViewBean")
@JsonView(ViewBean.DetailView.class)//指定視圖
public ViewBean serializeViewBean() {
ViewBean viewBean = new ViewBean();
viewBean.name = "名字";
viewBean.color = "blue";
viewBean.detail = "view";
return viewBean;
}
//返回值,沒有注解的字段不返回
{
"color": "blue",
"detail": "view"
}
/**方法上沒有指定視圖**/
@GetMapping("serializeViewBean")
@JsonView(ViewBean.DetailView.class)//指定視圖
public ViewBean serializeViewBean() {
ViewBean viewBean = new ViewBean();
viewBean.name = "名字";
viewBean.color = "blue";
viewBean.detail = "view";
return viewBean;
}
//返回值,相當於實體上的注解無效。
{
"name": "名字",
"color": "blue",
"detail": "view"
}
與之對應,反序列化過程中@JsonView
也是類似作用
public class ViewBean {
public interface BaseView {}
public interface DetailView extends BaseView {}
@JsonView(BaseView.class)
public String name;
@JsonView(BaseView.class)
public String color;
@JsonView(DetailView.class)
public String detail;
}
@PostMapping("deserializeViewBean")
public ViewBean deserializeViewBean(
@RequestBody @JsonView(ViewBean.BaseView.class) ViewBean viewBean) {
return viewBean;
}
//入參json
{
"name": "名字",
"color": "blue",
"detail": "view"
}
//返回值,沒有序列化子類字段
{
"name": "名字",
"color": "blue",
"detail": null
}
序列化注解
@JsonFormat
序列化Date/Time類型的字段時,格式化為指定格式。並且字段不需要有get方法。但不能用於反序列化。
@NoArgsConstructor
public class Calendar {
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy hh:mm:ss")
private LocalDateTime localDateTime = LocalDateTime.now();
}
//接口返回
{
"localDateTime": "25-03-2020 06:26:24"
}
@JsonIgnore
用在屬性上,表明序列化時,忽略被注解的屬性
@JsonIgnoreType
用在類上,表明序列化時,包含這個類作為屬性的對象會忽略該屬性。也就是說@JsonIgnore
是標注在忽略的屬性上,@JsonIgnoreType
是標注在忽略的屬性的類聲明上。
@JsonInclude
用在屬性上,在序列化過程中,指定哪些字段參與序列化。
JsonJsonInclude.Include.ALWAYS
默認策略,任何情況下都序列化該字段
JsonJsonInclude.Include.NON_NULL
這個最常用,如果字段值為null,那么就不序列化這個字段了。
JsonJsonInclude.Include.NON_EMPTY
表明字段為空時,也不參與序列化。
JsonJsonInclude.Include.NON_DEFAULT
表明子段是默認值的話,就不參與序列化。
@JsonAutoDetect
指定自動識別的級別,不指定的話默認只能識別public字段和get/set方法。
@JsonPropertyOrder
用在類上,在序列化過程中指定字段順序.
@JsonPropertyOrder({ "name", "id" })
public class MyBean {
public int id;
public String name;
}
@JsonRawValue
用在String
類型的屬性上,在序列化過程中,強制
將String轉換為對象(即使字符串不能轉換成合法對象,也會強制轉換引發錯誤,不推薦使用)
/**不使用@JsonRawValue修飾字段的實體類**/
public class RawBean {
public String name;
public String json;
}
@GetMapping("serializeJsonRawValue")
public RawBean serializeJsonRawValue() {
RawBean rawBean = new RawBean();
rawBean.name = "raw";
rawBean.json = "{\n" + " \"name\": \"法拉利\",\n" + " \"brand\": \"米其林\",\n" + " \"price\": 10.1\n" + "}";
return rawBean;
}
//返回值
{
"name": "raw",
"json": "{\n \"name\": \"法拉利\",\n \"brand\": \"米其林\",\n \"price\": 10.1\n}"
}
/**使用@JsonRawValue修飾**/
public class RawBean {
public String name;
@JsonRawValue public String json;
}
//返回值
{
"name": "raw",
"json": {
"name": "法拉利",
"brand": "米其林",
"price": 10.1
}
}
/**如果字符串不能從json轉化成對象**/
@GetMapping("serializeJsonRawValue")
public RawBean serializeJsonRawValue() {
RawBean rawBean = new RawBean();
rawBean.name = "raw";
rawBean.json = "messString";
return rawBean;
}
//返回值是不合法的json
{"name":"raw","json":messString}
@JsonValue
用在屬性或get方法上,一個類只能使用一次,作用是序列化的結果只返回這一個字段值。
值得注意的是,這個注解用在屬性上時對反序列化過程也有作用,但是作用有些奇怪:不會為被注解的字段進行反序列化。所以這個注解最好是用在get方法上。
@NoArgsConstructor
public class ValueBean {
@JsonValue public String name = "dove";
public String color = "yellow";
}
@GetMapping("serializeJsonValue")
public ValueBean serializeJsonValue() {
return new ValueBean();
}
//返回值
"dove"
/**反序列化**/
@PostMapping("deserializeJsonValue")
public ValueBean deserializeJsonValue(@RequestBody ValueBean valueBean) {
return valueBean;
}
//入參json
{
"name": "Robert",
"color": "oh"
}
//返回值
"dove"
@JsonRootValue
用在最外部的類上,在序列化和反序列化過程中生效,為json添加一個根節點值,用在嵌套類上沒有作用。並且要設置ObjectMapper的相關屬性才能激活這個注解,而SpringBoot默認沒有設置屬性,所以這個注解在SpringBoot應用中默認是無效的。
@NoArgsConstructor
@Getter
@Setter
@JsonRootName("root")
public class RootBean {
private String name = "根";
private Wheel wheel = new Wheel();
@NoArgsConstructor
@Getter
@Setter
public static class Wheel {
private String brand = "團藏";
private double price = 10.1;
}
}
/**序列化**/
@GetMapping("serializeJsonRootNameWithOM")
public String serializeJsonRootNameWithOM() throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper().enable(SerializationFeature.WRAP_ROOT_VALUE);
return objectMapper.writeValueAsString(new RootBean());
}
//返回值
{
"RootBean": {
"name": "根",
"wheel": {
"brand": "團藏",
"price": 10.1
}
}
}
/**反序列化**/
@PostMapping("deserializeJsonRootName")
public RootBean deserializeJsonRootName() throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper().enable(DeserializationFeature.UNWRAP_ROOT_VALUE);
String json =
"{\n"
+ " \"RootBean\": {\n"
+ " \"name\": \"根\",\n"
+ " \"wheel\": {\n"
+ " \"brand\": \"團藏\",\n"
+ " \"price\": 10.1\n"
+ " }\n"
+ " }\n"
+ "}";
return objectMapper.readValue(json, RootBean.class);
}
反序列化注解
@JsonAlias
用在屬性上,在反序列化過程中,為屬性定義一個別名,在序列化中不起作用。一個字段上可以定義多個別名,在客戶端傳參不統一的情況下很有用。
@NoArgsConstructor
@Getter
@Setter
public class AliasBean {
@JsonAlias({"name", "desc"})
private String theName;
private double price;
}
@PostMapping("deserializeAliasBean")
public AliasBean deserializeAliasBean(@RequestBody AliasBean aliasBean) {
return aliasBean;
}
//入參json
{
"name": "name",
"price": 1.0
}
//返回值,返回字段名不變
{
"theName": "desc",
"price": 1.0
}
//入參json,嘗試同時傳兩個別名
{
"name": "name",
"desc": "desc",
"price": 1.0
}
//返回值,第二個別名的值會覆蓋第一個
{
"theName": "desc",
"price": 1.0
}
以上就是Jackson常用注解在SpringBoot中的使用實踐。
參考資料:
https://www.baeldung.com/jackson-annotations
http://fasterxml.github.io/jackson-annotations/javadoc/2.10/