Jackson是用来序列化和反序列化json的Java的开源框架。Spring的默认json解析器便是Jackson。与其他Java的json的框架Gson等相比,Jackson解析大的json文件速度比较快;Jackson运行时占用内存比较低,性能比较好;Jackson有灵活的API,可以很容易进行扩展和定制。
踩到的坑
前段时间在做公司的手机APP消息推送,在测试iOS的消息推送时,踩到了Jackson序列化/反序列化的大小写转换的坑。
在定义的DTO里有如下一些字段
1 2 3 4 5 6 7 8 9 10
| @Data public class IOSPushDto { private String iOSSubtitle; private String iOSRemindBody; private String iOSMusic; private Boolean iOSSilentNotification; private Boolean iOSMutableContent; }
|
然后自测的时候,写了个demo接口,就直接按照DTO里定义的字段写了个json进行发送测试,然后在日志里发现各个字段的值都为null,json里的值没能塞进去。
1 2 3 4 5 6 7
| { "iOSSubtitle": "subtitle", "iOSRemindBody": "body", "iOSMusic": "default", "iOSSilentNotification": false, "iOSMutableContent":false }
|
当时很是疑惑,接口没错,字段没错,为啥就塞不进去值。
问题发现
然后用fastjson做对比,手动的进行序列化以及反序列化,做了个测试,发现fastjson能正常序列化。
打印出Jackson的序列化结果后发现了问题,Jackson序列化结果的字段和原DTO的字段大小写并不一样。
1 2 3 4 5 6 7
| { "iossilentNotification": false, "iosmutableContent": true, "iossubtitle": "subtitle", "iosremindBody": "body", "iosmusic": "default" }
|
我们项目使用的是lombok生成的setter与getter方法,lombok在生成方法的方法名是将各个DTO里各个字段的首字母转大写,然后前面拼生set/get方法。
这样生成的方法是这个样子的。
1 2 3
| public String getIOSSubtitle() { return iOSSubtitle; }
|
Jackson在进行序列化/反序列化的时候,会通过setter/getter方法来进行序列化/反序列化。
已进行序列化为例,Jackson在将方法名截取掉getter方法后,从第一个字符开始,将大写的字符转小写,直到遇到第一个小写字符。
根据getIOSSubtitle()方法,Jackson会生成iossubtitle的json字段。反序列化同理。
如何解决
如果DTO只是全程在后端各个spring服务之间进行调用,且全程都是spring默认的Jackson进行序列化/反序列化的话,可以无视这个,按照同一套规则就不会有问题。
如果序列化/反序列化的时候使用了不同的框架,比如一个Jackson一个fastjson的话,可以在属性上加个@JsonProperty(value = "")注解。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @Data public class IOSPushDto { @JsonProperty("iOSSubtitle") private String iOSSubtitle; @JsonProperty("iOSRemindBody") private String iOSRemindBody; @JsonProperty("iOSMusic") private String iOSMusic; @JsonProperty("iOSSilentNotification") private Boolean iOSSilentNotification; @JsonProperty("iOSMutableContent") private Boolean iOSMutableContent; }
|
这样Jackson在进行序列化的时候,会按照注解里的value进行序列化。
或者在类上添加这个注解@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY,getterVisibility = JsonAutoDetect.Visibility.NONE, setterVisibility = JsonAutoDetect.Visibility.NONE)
1 2 3 4 5 6 7 8 9 10 11
| @Data @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY,getterVisibility = JsonAutoDetect.Visibility.NONE, setterVisibility = JsonAutoDetect.Visibility.NONE) public class IOSPushDto { private String iOSSubtitle; private String iOSRemindBody; private String iOSMusic; private Boolean iOSSilentNotification; private Boolean iOSMutableContent; }
|
这个注解是设置序列化/反序列化时属性的可见性,设置field的可见性为所有,设置getter/setter方法可见性为None,这样的话就可以通过field来进行序列化/反序列化,json的字段就可以喝DTO的字段保持一致了。
一些测试
已知默认情况下iOSSubtitle会被序列化为iossubtitle,而如果字段本身就是iossubtitle的话,lombok会生成getIossubtitle,最终也会被序列化成iossubtitle。那么这两个字段出现在一起的时候会怎么样呢?
我们做个测试看一看会出现什么情况。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @Data public class IOSPushDto { private String iOSSubtitle; private String iossubtitle; private String iOSRemindBody; private String iosremindBody; private String iOSMusic; private String iosmusic; private Boolean iOSSilentNotification; private Boolean iossilentNotification; private Boolean iOSMutableContent; private Boolean iosmutableContent; }
|
写个main方法,看一下序列化的结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public static void main(String[] args) throws JsonProcessingException { var iosPushDto = new IOSPushDto();
iosPushDto.setIOSMusic("default"); iosPushDto.setIosmusic("default2");
iosPushDto.setIOSSubtitle("subtitle"); iosPushDto.setIossubtitle("subtitle2");
iosPushDto.setIOSRemindBody("body"); iosPushDto.setIosremindBody("body2"); iosPushDto.setIOSSilentNotification(false); iosPushDto.setIossilentNotification(true); iosPushDto.setIOSMutableContent(true); iosPushDto.setIosmutableContent(false); ObjectMapper mapper = new ObjectMapper(); String string = mapper.writeValueAsString(iosPushDto); System.out.println(string); }
|
emm,直接编译报错
我们注释掉main方法,再次编译,然后到target目录下看一下编译的class文件,然后通过idea反编译查看类里的属性和字段
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222
| public class IOSPushDto { private String iOSSubtitle; private String iossubtitle; private String iOSRemindBody; private String iosremindBody; private String iOSMusic; private String iosmusic; private Boolean iOSSilentNotification; private Boolean iossilentNotification; private Boolean iOSMutableContent; private Boolean iosmutableContent;
public IOSPushDto() { }
public String getIOSSubtitle() { return this.iOSSubtitle; }
public String getIOSRemindBody() { return this.iOSRemindBody; }
public String getIOSMusic() { return this.iOSMusic; }
public Boolean getIOSSilentNotification() { return this.iOSSilentNotification; }
public Boolean getIOSMutableContent() { return this.iOSMutableContent; }
public void setIOSSubtitle(final String iOSSubtitle) { this.iOSSubtitle = iOSSubtitle; }
public void setIOSRemindBody(final String iOSRemindBody) { this.iOSRemindBody = iOSRemindBody; }
public void setIOSMusic(final String iOSMusic) { this.iOSMusic = iOSMusic; }
public void setIOSSilentNotification(final Boolean iOSSilentNotification) { this.iOSSilentNotification = iOSSilentNotification; }
public void setIOSMutableContent(final Boolean iOSMutableContent) { this.iOSMutableContent = iOSMutableContent; }
public boolean equals(final Object o) { if (o == this) { return true; } else if (!(o instanceof IOSPushDto)) { return false; } else { IOSPushDto other = (IOSPushDto)o; if (!other.canEqual(this)) { return false; } else { Object this$iOSSilentNotification = this.getIOSSilentNotification(); Object other$iOSSilentNotification = other.getIOSSilentNotification(); if (this$iOSSilentNotification == null) { if (other$iOSSilentNotification != null) { return false; } } else if (!this$iOSSilentNotification.equals(other$iOSSilentNotification)) { return false; }
Object this$iossilentNotification = this.getIOSSilentNotification(); Object other$iossilentNotification = other.getIOSSilentNotification(); if (this$iossilentNotification == null) { if (other$iossilentNotification != null) { return false; } } else if (!this$iossilentNotification.equals(other$iossilentNotification)) { return false; }
Object this$iOSMutableContent = this.getIOSMutableContent(); Object other$iOSMutableContent = other.getIOSMutableContent(); if (this$iOSMutableContent == null) { if (other$iOSMutableContent != null) { return false; } } else if (!this$iOSMutableContent.equals(other$iOSMutableContent)) { return false; }
label110: { Object this$iosmutableContent = this.getIOSMutableContent(); Object other$iosmutableContent = other.getIOSMutableContent(); if (this$iosmutableContent == null) { if (other$iosmutableContent == null) { break label110; } } else if (this$iosmutableContent.equals(other$iosmutableContent)) { break label110; }
return false; }
label103: { Object this$iOSSubtitle = this.getIOSSubtitle(); Object other$iOSSubtitle = other.getIOSSubtitle(); if (this$iOSSubtitle == null) { if (other$iOSSubtitle == null) { break label103; } } else if (this$iOSSubtitle.equals(other$iOSSubtitle)) { break label103; }
return false; }
Object this$iossubtitle = this.getIOSSubtitle(); Object other$iossubtitle = other.getIOSSubtitle(); if (this$iossubtitle == null) { if (other$iossubtitle != null) { return false; } } else if (!this$iossubtitle.equals(other$iossubtitle)) { return false; }
label89: { Object this$iOSRemindBody = this.getIOSRemindBody(); Object other$iOSRemindBody = other.getIOSRemindBody(); if (this$iOSRemindBody == null) { if (other$iOSRemindBody == null) { break label89; } } else if (this$iOSRemindBody.equals(other$iOSRemindBody)) { break label89; }
return false; }
label82: { Object this$iosremindBody = this.getIOSRemindBody(); Object other$iosremindBody = other.getIOSRemindBody(); if (this$iosremindBody == null) { if (other$iosremindBody == null) { break label82; } } else if (this$iosremindBody.equals(other$iosremindBody)) { break label82; }
return false; }
Object this$iOSMusic = this.getIOSMusic(); Object other$iOSMusic = other.getIOSMusic(); if (this$iOSMusic == null) { if (other$iOSMusic != null) { return false; } } else if (!this$iOSMusic.equals(other$iOSMusic)) { return false; }
Object this$iosmusic = this.getIOSMusic(); Object other$iosmusic = other.getIOSMusic(); if (this$iosmusic == null) { if (other$iosmusic != null) { return false; } } else if (!this$iosmusic.equals(other$iosmusic)) { return false; }
return true; } } }
protected boolean canEqual(final Object other) { return other instanceof IOSPushDto; }
public int hashCode() { int PRIME = true; int result = 1; Object $iOSSilentNotification = this.getIOSSilentNotification(); result = result * 59 + ($iOSSilentNotification == null ? 43 : $iOSSilentNotification.hashCode()); Object $iossilentNotification = this.getIOSSilentNotification(); result = result * 59 + ($iossilentNotification == null ? 43 : $iossilentNotification.hashCode()); Object $iOSMutableContent = this.getIOSMutableContent(); result = result * 59 + ($iOSMutableContent == null ? 43 : $iOSMutableContent.hashCode()); Object $iosmutableContent = this.getIOSMutableContent(); result = result * 59 + ($iosmutableContent == null ? 43 : $iosmutableContent.hashCode()); Object $iOSSubtitle = this.getIOSSubtitle(); result = result * 59 + ($iOSSubtitle == null ? 43 : $iOSSubtitle.hashCode()); Object $iossubtitle = this.getIOSSubtitle(); result = result * 59 + ($iossubtitle == null ? 43 : $iossubtitle.hashCode()); Object $iOSRemindBody = this.getIOSRemindBody(); result = result * 59 + ($iOSRemindBody == null ? 43 : $iOSRemindBody.hashCode()); Object $iosremindBody = this.getIOSRemindBody(); result = result * 59 + ($iosremindBody == null ? 43 : $iosremindBody.hashCode()); Object $iOSMusic = this.getIOSMusic(); result = result * 59 + ($iOSMusic == null ? 43 : $iOSMusic.hashCode()); Object $iosmusic = this.getIOSMusic(); result = result * 59 + ($iosmusic == null ? 43 : $iosmusic.hashCode()); return result; }
public String toString() { String var10000 = this.getIOSSubtitle(); return "IOSPushDto(iOSSubtitle=" + var10000 + ", iossubtitle=" + this.getIOSSubtitle() + ", iOSRemindBody=" + this.getIOSRemindBody() + ", iosremindBody=" + this.getIOSRemindBody() + ", iOSMusic=" + this.getIOSMusic() + ", iosmusic=" + this.getIOSMusic() + ", iOSSilentNotification=" + this.getIOSSilentNotification() + ", iossilentNotification=" + this.getIOSSilentNotification() + ", iOSMutableContent=" + this.getIOSMutableContent() + ", iosmutableContent=" + this.getIOSMutableContent() + ")"; } }
|
通过反编译结果我们可以看到并没有生成iossubtitle、iosremindBody、iosmusic、iossilentNotification、iosmutableContent这几个小写属性的setter/getter方法。
这看起来是lombok在生成方法的时候过滤掉了这几个字段。我们把iOSSubtitle、iOSRemindBody这两大写的字段注释掉,再编译看看。
原类定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @Data public class IOSPushDto { private String iossubtitle;
private String iosremindBody;
private String iOSMusic; private String iosmusic;
private Boolean iOSSilentNotification; private Boolean iossilentNotification;
private Boolean iOSMutableContent; private Boolean iosmutableContent; }
|
编译生成的结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195
| public class IOSPushDto { private String iossubtitle; private String iosremindBody; private String iOSMusic; private String iosmusic; private Boolean iOSSilentNotification; private Boolean iossilentNotification; private Boolean iOSMutableContent; private Boolean iosmutableContent;
public IOSPushDto() { }
public String getIossubtitle() { return this.iossubtitle; }
public String getIosremindBody() { return this.iosremindBody; }
public String getIOSMusic() { return this.iOSMusic; }
public Boolean getIOSSilentNotification() { return this.iOSSilentNotification; }
public Boolean getIOSMutableContent() { return this.iOSMutableContent; }
public void setIossubtitle(final String iossubtitle) { this.iossubtitle = iossubtitle; }
public void setIosremindBody(final String iosremindBody) { this.iosremindBody = iosremindBody; }
public void setIOSMusic(final String iOSMusic) { this.iOSMusic = iOSMusic; }
public void setIOSSilentNotification(final Boolean iOSSilentNotification) { this.iOSSilentNotification = iOSSilentNotification; }
public void setIOSMutableContent(final Boolean iOSMutableContent) { this.iOSMutableContent = iOSMutableContent; }
public boolean equals(final Object o) { if (o == this) { return true; } else if (!(o instanceof IOSPushDto)) { return false; } else { IOSPushDto other = (IOSPushDto)o; if (!other.canEqual(this)) { return false; } else { label107: { Object this$iOSSilentNotification = this.getIOSSilentNotification(); Object other$iOSSilentNotification = other.getIOSSilentNotification(); if (this$iOSSilentNotification == null) { if (other$iOSSilentNotification == null) { break label107; } } else if (this$iOSSilentNotification.equals(other$iOSSilentNotification)) { break label107; }
return false; }
Object this$iossilentNotification = this.getIOSSilentNotification(); Object other$iossilentNotification = other.getIOSSilentNotification(); if (this$iossilentNotification == null) { if (other$iossilentNotification != null) { return false; } } else if (!this$iossilentNotification.equals(other$iossilentNotification)) { return false; }
Object this$iOSMutableContent = this.getIOSMutableContent(); Object other$iOSMutableContent = other.getIOSMutableContent(); if (this$iOSMutableContent == null) { if (other$iOSMutableContent != null) { return false; } } else if (!this$iOSMutableContent.equals(other$iOSMutableContent)) { return false; }
label86: { Object this$iosmutableContent = this.getIOSMutableContent(); Object other$iosmutableContent = other.getIOSMutableContent(); if (this$iosmutableContent == null) { if (other$iosmutableContent == null) { break label86; } } else if (this$iosmutableContent.equals(other$iosmutableContent)) { break label86; }
return false; }
label79: { Object this$iossubtitle = this.getIossubtitle(); Object other$iossubtitle = other.getIossubtitle(); if (this$iossubtitle == null) { if (other$iossubtitle == null) { break label79; } } else if (this$iossubtitle.equals(other$iossubtitle)) { break label79; }
return false; }
label72: { Object this$iosremindBody = this.getIosremindBody(); Object other$iosremindBody = other.getIosremindBody(); if (this$iosremindBody == null) { if (other$iosremindBody == null) { break label72; } } else if (this$iosremindBody.equals(other$iosremindBody)) { break label72; }
return false; }
Object this$iOSMusic = this.getIOSMusic(); Object other$iOSMusic = other.getIOSMusic(); if (this$iOSMusic == null) { if (other$iOSMusic != null) { return false; } } else if (!this$iOSMusic.equals(other$iOSMusic)) { return false; }
Object this$iosmusic = this.getIOSMusic(); Object other$iosmusic = other.getIOSMusic(); if (this$iosmusic == null) { if (other$iosmusic != null) { return false; } } else if (!this$iosmusic.equals(other$iosmusic)) { return false; }
return true; } } }
protected boolean canEqual(final Object other) { return other instanceof IOSPushDto; }
public int hashCode() { int PRIME = true; int result = 1; Object $iOSSilentNotification = this.getIOSSilentNotification(); result = result * 59 + ($iOSSilentNotification == null ? 43 : $iOSSilentNotification.hashCode()); Object $iossilentNotification = this.getIOSSilentNotification(); result = result * 59 + ($iossilentNotification == null ? 43 : $iossilentNotification.hashCode()); Object $iOSMutableContent = this.getIOSMutableContent(); result = result * 59 + ($iOSMutableContent == null ? 43 : $iOSMutableContent.hashCode()); Object $iosmutableContent = this.getIOSMutableContent(); result = result * 59 + ($iosmutableContent == null ? 43 : $iosmutableContent.hashCode()); Object $iossubtitle = this.getIossubtitle(); result = result * 59 + ($iossubtitle == null ? 43 : $iossubtitle.hashCode()); Object $iosremindBody = this.getIosremindBody(); result = result * 59 + ($iosremindBody == null ? 43 : $iosremindBody.hashCode()); Object $iOSMusic = this.getIOSMusic(); result = result * 59 + ($iOSMusic == null ? 43 : $iOSMusic.hashCode()); Object $iosmusic = this.getIOSMusic(); result = result * 59 + ($iosmusic == null ? 43 : $iosmusic.hashCode()); return result; }
public String toString() { String var10000 = this.getIossubtitle(); return "IOSPushDto(iossubtitle=" + var10000 + ", iosremindBody=" + this.getIosremindBody() + ", iOSMusic=" + this.getIOSMusic() + ", iosmusic=" + this.getIOSMusic() + ", iOSSilentNotification=" + this.getIOSSilentNotification() + ", iossilentNotification=" + this.getIOSSilentNotification() + ", iOSMutableContent=" + this.getIOSMutableContent() + ", iosmutableContent=" + this.getIOSMutableContent() + ")"; } }
|
这下能正常生成了iossubtitle、iosremindBody的setter/getter方法。
这样看lombok可能为了避免一些json的序列化/反序列化框架出问题,在这种场景下做了特殊处理。那既然这样的话,我们就手动的写一下setter/getter方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| @Data public class IOSPushDto { private String iOSSubtitle; private String iossubtitle;
private String iOSRemindBody; private String iosremindBody;
private String iOSMusic; private Boolean iOSSilentNotification; private Boolean iOSMutableContent;
public String getIossubtitle() { return this.iossubtitle; }
public String getIosremindBody() { return this.iosremindBody; }
public void setIossubtitle(final String iossubtitle) { this.iossubtitle = iossubtitle; }
public void setIosremindBody(final String iosremindBody) { this.iosremindBody = iosremindBody; }
public String getIOSSubtitle() { return iOSSubtitle; }
public IOSPushDto setIOSSubtitle(String iOSSubtitle) { this.iOSSubtitle = iOSSubtitle; return this; }
public String getIOSRemindBody() { return iOSRemindBody; }
public IOSPushDto setIOSRemindBody(String iOSRemindBody) { this.iOSRemindBody = iOSRemindBody; return this; } }
|
然后是main方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public static void main(String[] args) throws JsonProcessingException { var iosPushDto = new IOSPushDto();
iosPushDto.setIOSSubtitle("subtitle"); iosPushDto.setIossubtitle("subtitle2");
iosPushDto.setIOSRemindBody("body"); iosPushDto.setIosremindBody("body2"); iosPushDto.setIOSMusic("default"); iosPushDto.setIOSSilentNotification(false); iosPushDto.setIOSMutableContent(true); ObjectMapper mapper = new ObjectMapper(); String string = mapper.writeValueAsString(iosPushDto); System.out.println(string); }
|
编译测试。然后Jackson就抛异常了InvalidDefinitionException
Jackson针对这种情况做了做了校验。想来也是,我都能想到的可能会出问题的场景,开发这个框架的大佬怎么会想不到、、、
反序列化就不测试了,结果估计也是差不多