前言
Spring Boot提供了與三個JSON映射庫的集成:
- Gson
- Jackson
- JSON-B
Jackson是首選的默認庫。
官網介紹:
通常,我們將Java對象轉成Json時稱之為序列化,反之將Json轉成Java對象時稱之為反序列化,本文簡單介紹一下Jackson,以及在SpringBoot項目開發中常用的Jackson方法
如何引入
SpringBoot提供了JSON依賴,我們可以按下面方式引入
1、直接引入JSON依賴
<!-- springboot-json --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-json</artifactId> </dependency>
2、一般情況下我們引入MVC,MVC里面幫我們引入了JSON依賴
<!-- springboot web(MVC)--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
最終引入的依賴是
Jackson注解
Jackson的注解詳細介紹
英文官方介紹:https://github.com/FasterXML/jackson-annotations
常用注解
@JsonProperty 序列化、反序列化時,屬性的名稱
@JsonIgnoreProperties
序列化、反序列化忽略屬性,多個時用“,”隔開
@JsonIgnore 序列化、反序列化
忽略屬性
@JsonAlias 為反序列化期間要接受的屬性定義一個或多個替代名稱,可以與@JsonProperty一起使用
@JsonInclude
當屬性的值為空(null或者"")時,不進行序列化,可以減少數據傳輸
@JsonFormat
序列化、反序列化時,格式化時間
測試
寫一個controller測試一下
先寫一個頁面跳轉
/** * 跳轉頁面,頁面引入了jquery,主要用於下面的ajax調用測試 */ @GetMapping("/") public ModelAndView index(){ return new ModelAndView("index"); }
反序列化方式
完整測試Vo:
@Data //序列化、反序列化忽略的屬性,多個時用“,”隔開 @JsonIgnoreProperties({"captcha"}) //當屬性的值為空(null或者"")時,不進行序列化,可以減少數據傳輸 @JsonInclude(JsonInclude.Include.NON_EMPTY) public class UserVoByJson { // 序列化、反序列化時,屬性的名稱 @JsonProperty("userName") private String username; // 為反序列化期間要接受的屬性定義一個或多個替代名稱,可以與@JsonProperty一起使用 @JsonAlias({"pass_word", "passWord"}) @JsonProperty("pwd") private String password; //序列化、反序列化時,格式化時間 @JsonFormat(locale = "zh", timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") private Date createDate; //序列化、反序列化忽略屬性 @JsonIgnore private String captcha; }
使用@RestController標注類,相對於所有的方法都用@ResponseBody標注,MVC會幫我們調用序列化,將Java對象轉成Json再響應給調用方,同時形參要加@RequestBody標注,MVC會幫我們調用反序列化將Json轉成Java對象,這就要求我們調用的時候需要傳一個Json字符串過來
/* $.ajax({ type:"POST", url:"http://localhost:10099/testByJson", data:JSON.stringify({ userName:"sa", pass_word:"123fff", captcha:"abcd", createDate:"2019-08-05 11:34:31" }), dataType:"JSON", contentType:"application/json;charset=UTF-8", success:function(data){ console.log(data); }, error:function(data){ console.log("報錯啦"); } }) */ /** * 反序列化方式注入,只能post請求 */ @PostMapping("testByJson") public UserVoByJson testByJson(@RequestBody UserVoByJson userVo) { System.out.println(userVo); return userVo; }
調用測試
1、先注釋所有注解,僅打開這個兩個類上面的注解@JsonIgnoreProperties、@JsonInclude
@Data //序列化、反序列化忽略的屬性,多個時用“,”隔開 @JsonIgnoreProperties({"captcha"}) //當屬性的值為空(null或者"")時,不進行序列化,可以減少數據傳輸 @JsonInclude(JsonInclude.Include.NON_EMPTY) public class UserVoByJson { // 序列化、反序列化時,屬性的名稱 // @JsonProperty("userName") private String username; // 為反序列化期間要接受的屬性定義一個或多個替代名稱,可以與@JsonProperty一起使用 // @JsonAlias({"pass_word", "passWord"}) // @JsonProperty("pwd") private String password; //序列化、反序列化時,格式化時間 // @JsonFormat(locale = "zh", timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") private Date createDate; //序列化、反序列化忽略屬性 // @JsonIgnore private String captcha; }
前端調用時全部按屬性名稱
data:JSON.stringify({ username:"sa", password:"123fff", captcha:"abcd" })
反序列化(后端控制台打印)
UserVoByJson(username=sa, password=123fff, createDate=null, captcha=null)
序列化(ajax的回調)
{username: "sa", password: "123fff"}
captcha屬性前端已經傳值,但設置了@JsonIgnoreProperties注解反序列化時該屬性被忽略,因此為空,而序列化的時候@JsonInclude配置的是JsonInclude.Include.NON_EMPTY,當屬性的值為空(null或者"")時,不進行序列化,所以序列化的最終結果如上所示
2、先注釋所有注解,放開@JsonProperty、@JsonAlias、@JsonIgnore
@Data //序列化、反序列化忽略的屬性,多個時用“,”隔開 //@JsonIgnoreProperties({"captcha"}) //當屬性的值為空(null或者"")時,不進行序列化,可以減少數據傳輸 //@JsonInclude(JsonInclude.Include.NON_EMPTY) public class UserVoByJson { // 序列化、反序列化時,屬性的名稱 @JsonProperty("userName") private String username; // 為反序列化期間要接受的屬性定義一個或多個替代名稱,可以與@JsonProperty一起使用 @JsonAlias({"pass_word", "passWord"}) @JsonProperty("pwd") private String password; //序列化、反序列化時,格式化時間 // @JsonFormat(locale = "zh", timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") private Date createDate; //序列化、反序列化忽略屬性 @JsonIgnore private String captcha; }
前端調用還是按屬性名稱
data:JSON.stringify({ username:"sa", password:"123fff", captcha:"abcd" })
反序列化(后端控制台打印)
UserVoByJson(username=null, password=null, createDate=null, captcha=null)
序列化(ajax的回調)
{createDate: null, userName: null, pwd: null}
captcha被@JsonIgnore標注,序列化、反序列忽略它,username、password被@JsonProperty標注,傳參的時候只能用別名,password同時被@JsonAlias標注,可以用代替名稱
因此我們可以這樣調用
data:JSON.stringify({ userName:"sa", pass_word:"123fff", //以下兩種也一樣 //passWord:"123fff", //pwd:"123fff", captcha:"abcd" })
反序列化(后端控制台打印)
UserVoByJson(username=sa, password=123fff, createDate=null, captcha=null)
序列化(ajax的回調)
{userName: "sa", pwd: "123fff"}
3、先注釋所有注解,放開@JsonFormat
@Data //序列化、反序列化忽略的屬性,多個時用“,”隔開 //@JsonIgnoreProperties({"captcha"}) //當屬性的值為空(null或者"")時,不進行序列化,可以減少數據傳輸 //@JsonInclude(JsonInclude.Include.NON_EMPTY) public class UserVoByJson { // 序列化、反序列化時,屬性的名稱 // @JsonProperty("userName") private String username; // 為反序列化期間要接受的屬性定義一個或多個替代名稱,可以與@JsonProperty一起使用 // @JsonAlias({"pass_word", "passWord"}) // @JsonProperty("pwd") private String password; //序列化、反序列化時,格式化時間 @JsonFormat(locale = "zh", timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") private Date createDate; //序列化、反序列化忽略屬性 // @JsonIgnore private String captcha; }
前端調用
data:JSON.stringify({ createDate:"2019-08-05 11:34:31" })
反序列化(后端控制台打印)
UserVoByJson(username=null, password=null, createDate=Mon Aug 05 11:34:31 GMT+08:00 2019, captcha=null)
序列化(ajax的回調)
{username: null, password: null, createDate: "2019-08-05 11:34:31", captcha: null}
PS:沒有配置之前這樣調用會報錯400
Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize value of type `java.util.Date` from String "2019-08-05 11:34:31": not a valid representation (error: Failed to parse Date value '2019-08-05 11:34:31': Cannot parse date "2019-08-05 11:34:31": while it seems to fit format 'yyyy-MM-dd'T'HH:mm:ss.SSSZ', parsing fails (leniency? null)); nested exception is com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `java.util.Date` from String "2019-08-05 11:34:31": not a valid representation (error: Failed to parse Date value '2019-08-05 11:34:31': Cannot parse date "2019-08-05 11:34:31": while it seems to fit format 'yyyy-MM-dd'T'HH:mm:ss.SSSZ', parsing fails (leniency? null))
MVC方式注入
Vo類
@Data public class UserVoByMvc { private String username; private String password; private Date createDate; private String captcha; }
如果不是以反序列化的方式注入,而是MVC的方式注入又是怎么樣呢?去掉@RequestBody就變成MVC注入
/* $.ajax({ type:"POST", url:"http://localhost:10099/testByMvc", data:{ username:"sa", password:"123fff", captcha:"abcd" }, dataType:"JSON", //contentType:"application/json;charset=UTF-8",//使用這個,get請求能接到參數,post接不到 contentType:"application/x-www-form-urlencoded",//使用這個,get、post都能接收到參數 success:function(data){ console.log(data); }, error:function(data){ console.log("報錯啦"); } }) */ /** * MVC方式注入 */ @RequestMapping("testByMvc") public UserVoByMvc testByMvc(UserVoByMvc userVo) { System.out.println(userVo); return userVo; }
MVC注入的時候,接參過程Jackson的注解就不再生效了,這時候我們傳參就得按照MVC的規則來,Date類型首先就不能傳字符串
前端調用
data:{ username:"sa", password:"123fff", captcha:"abcd" }
后台打印
UserVoByMvc(username=sa, password=123fff, createDate=null, captcha=abcd)
ajax回調
{username: "sa", password: "123fff", createDate: null, captcha: "abcd"}
那MVC方式注入,Date日期類型該怎么支持傳字符串呢?在配置文件新增MVC日期格式化就可以愉快的傳輸固定格式的日期字符串了
#MVC接參時,日期處理 spring.mvc.date-format=yyyy-MM-dd HH:mm:ss
(偷個懶,效果與預期一樣,就貼圖了。。。)
同時,不管是采用哪種注入方法,我們可以配置全局的日期處理,這樣一來就可以愉快開發了
#全局日期格式化處理 #MVC接參時,日期處理 spring.mvc.date-format=yyyy-MM-dd HH:mm:ss #Jackson序列化、反序列化時,日期處理 spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
我們順便來看一下在配置文件都有哪些Jackson配置,每個配置的具體功能見名思意,就不闡述了
接收集合對象
1、反序列化方式
/* let datas = [];//對象集合 for(let i = 0; i < 5; i++){ let data = {"userName":i + ""};//對象 datas.push(data); } $.ajax({ type:"POST", url:"http://localhost:10099/testListByJson", data:JSON.stringify(datas), dataType:"JSON", contentType:"application/json;charset=UTF-8", success:function(data){ console.log(data); }, error:function(data){ console.log("報錯啦"); } }) */ /** * 反序列化方式,接收集合對象,只能post請求 */ @PostMapping("testListByJson") public String testListByJson(@RequestBody List<UserVoByJson> userVos){ userVos.forEach(System.out::println); return "{\"code\":200}"; }
后台打印
UserVoByJson(username=0, password=null, createDate=null, captcha=null) UserVoByJson(username=1, password=null, createDate=null, captcha=null) UserVoByJson(username=2, password=null, createDate=null, captcha=null) UserVoByJson(username=3, password=null, createDate=null, captcha=null) UserVoByJson(username=4, password=null, createDate=null, captcha=null)
ObjectMapper
以上都是配置注解,具體操作都是MVC幫我們做了,那我們如何使用Jackson進行Json操作呢?我們在官方文檔可以看到Jackson為我們提供了com.fasterxml.jackson.databind.ObjectMapper類操作Json
常用方法
/** * 測試 ObjectMapper對象 */ public static void main(String[] args) { try { ObjectMapper mapper = new ObjectMapper(); //當屬性的值為空(null或者"")時,不進行序列化,可以減少數據傳輸 mapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY); //設置日期格式 mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); //1、Java對象轉Json字符串 UserVoByJson userVo = new UserVoByJson(); userVo.setUsername("張三"); userVo.setPassword("666"); String jsonString = mapper.writeValueAsString(userVo); System.out.println(jsonString); //2、Json字符串轉Java對象 jsonString = "{\"userName\":\"張三\"}"; UserVoByJson userVo1 = mapper.readValue(jsonString, UserVoByJson.class); System.out.println(userVo1); //3、Java對象類型轉換 HashMap<Object, Object> map = new HashMap<>(); map.put("userName", "張三"); UserVoByJson userVo2 = mapper.convertValue(map, UserVoByJson.class); System.out.println(userVo2); //4、將json字符串轉換成List String listJsonString = "[{\"userName\":\"張三\"},{\"userName\":\"李四\"}]"; List<UserVoByJson> userVoList = mapper.readValue(listJsonString, mapper.getTypeFactory().constructParametricType(List.class, UserVoByJson.class)); System.out.println(userVoList); } catch (IOException e) { e.printStackTrace(); } }
打印
{"userName":"張三","pwd":"666"} UserVoByJson(username=張三, password=null, createDate=null, captcha=null) UserVoByJson(username=張三, password=null, createDate=null, captcha=null) [UserVoByJson(username=張三, password=null, createDate=null, captcha=null), UserVoByJson(username=李四, password=null, createDate=null, captcha=null)]
還有一些不怎么常用的方法,比如下面這幾個(除了轉成Json字符串)
后記
通常,實體類用於ORM映射框架與數據打交道,比如:User,要求對象的屬性要與數據庫字段一一對應,少了不行,多了也不行,沒有對應映射的得用注解標注(比如JPA),所以我們一般用Vo對象進行傳輸、接參等,會多很多亂七八糟的屬性(分頁信息,僅用於接參的臨時屬性等),比如:UserVo,User、UserVo兩個對象使用工具類相互轉換,有時候Vo對象有些亂七八糟的屬性不想進行序列化傳輸,就需要設置序列化過濾
在SpringBoot中使用Jackson操作Json序列化、反序列化的簡單操作就暫時記錄到這,以后再繼續補充
補充
2019-10-22補充:不同時區,時間序列化處理
需求:要求系統根據當前登錄賬號存儲的時區字段,web端顯示對應時區的時間
通常情況下,系統會分為svc端服務、web端服務,svc服務負責與數據庫打交道,web服務負責與瀏覽器打交道;因此,我們可以在svc服務數據存庫的時候統一存儲GMT+0000,web服務序列化響應的時候根據當前登錄賬戶時區進行顯示,簡單來說就是:web端服務根據當前登錄人的時區來顯示日期時間,但svc端服務日期入庫統一采用GMT+0000時區。
實現:
svc端服務,在系統啟動時設置全局默認GMT+0000時區
@SpringBootApplication public class XXXApplication { public static void main(String[] args) { //設置全局默認時區 TimeZone.setDefault(TimeZone.getTimeZone("GMT+0000")); SpringApplication.run(XXXApplication .class, args); } }
web端服務,設置自定義JsonSerializer<Date>日期序列化實現類,在實現類中獲取登錄賬戶時區,設置序列化日期格式
@JsonComponent public class WebDateFormat { //SimpleDateFormat對象 private static final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z"); @Autowired private LoginService loginService; //格式化日期 public static class DateFormatSerializer extends JsonSerializer<Date> { @Override public void serialize(Date value, JsonGenerator gen, SerializerProvider serializers) { try { //獲取登錄賬號時區字段,並設置序列化日期格式 String timeZone = loginService.getLoginUser().getTimeZone(); format.setTimeZone(TimeZone.getTimeZone(timeZone)); gen.writeString(format.format(value)); } catch (IOException e) { throw new RuntimeException(e); } } } //解析日期字符串 public static class DateParseDeserializer extends JsonDeserializer<Date> { @Override public Date deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { try { //獲取登錄賬號時區字段,並設置序列化日期格式 String timeZone = loginService.getLoginUser().getTimeZone(); format.setTimeZone(TimeZone.getTimeZone(timeZone)); return format.parse(p.getValueAsString()); } catch (ParseException e) { throw new RuntimeException(e); } } } }
2020-07-31更新
lombok跟mvc注入的bug記錄
/** mvc參數自動注入 反射生成set方法,正常來說是set后面緊跟的第一個字母大寫,比如這樣:objId,setObjId, 但當它碰到第二個字母大寫的時候,第一個是小寫,比如:uPosition,setuPosition, 實體類中我用的是lombok,它幫我們生成的是setUPosition,導致找不到set方法值注入不進去 解決方法:手動寫uPosition uUnits的set,get方法,覆蓋lombok幫我們生成的set,get方法 */ public String getuPosition() { return uPosition; } public void setuPosition(String uPosition) { this.uPosition = uPosition; } public String getuUnits() { return uUnits; } public void setuUnits(String uUnits) { this.uUnits = uUnits; }
所以當某個字段接不到參、或者序列化丟失數據、ORM框架無法映射數據等問題時,可以往這個方向去排查問題
代碼開源
代碼已經開源、托管到我的GitHub、碼雲: