所谓
存储空间
,是相对Json数据而言的。Json数据在网络传输时,存储空间
其实就是占用的带宽大小;Json数据在在内存中处理时,存储空间
对应的就是占用的内存大小;Json数据在持久化至本地磁盘时,存储空间
就是占用的磁盘大小;
Json通常用在接口交互,随着Json的广泛使用,在不同的业务场景下,会演化出不同的使用方式,随之出现的问题也越来越来。仅从本人的经验来讲,就碰到了Json使用上的如下诉求:
有人可能觉得这样很奇怪,但这在企业应用中是真实存在的。存在即合理,为了保障客户使用的延续性,需要架构师/coder做好兼容性设计和编码。
这里有客户设计不合理的地方,也可以通过服务端的Json处理去规避这个问题。
这个场景比较复杂:存在日志必须脱敏,但是接口响应Json结果不脱敏的情况;也存在日志脱敏,接口响应Json也要脱敏的情况。接口设计时,必须考虑这些场景的兼容支持。
直接忽略Json属性的好处是处理简单,缺点是不方便定位问题,缩减则是比较优雅的处理方式。
List>
;
在Json反序列化成模型时,存在泛型类型内存擦除的问题,必须要做特殊处理。
可以提升接口的兼容性;
可以提升接口的兼容性;
如:复用的oauth2框架时,返回JwtToken Json包含了Instant类型,现在需要把默认的秒转换成毫秒;
一般出现在接口的兼容设计中,如:刚开始的字段只考虑了支持单个类型,但是随着业务的发展,接口必须要支持批量。虽然设计了2个不同的接口搞定了这个问题,但是也有更优的复用处理方式。
Json实现方式非常多,各有各的使用场景,常见的方式是和同源的其他组件绑定。常用的Json主要有jackson/fastjson/gson 3种,其它还包括:json-lib/org.json等。
结合网上信息及本人的使用体验,Json对比汇总如下:
Json类型 | 序列化性能 | 反序列化 | 安全性 | 可扩展性 | 生态 |
---|---|---|---|---|---|
Jackson | 最好 | 好 | 好 | 好 | 好,SpringBoot默认集成 |
FastJson | 好 | 最好 | 较差 | NA | 较好,AlibabaSpringCloud默认集成 |
Gson | 较好 | 好 | 好 | NA | 较好,Guava等默认集成 |
JsonLib | 差 | 差 | NA | NA | NA |
org.json | NA | NA | NA | NA | NA |
补充说明:
- 在多年的项目经验中,FastJson是漏洞最多的,因为漏洞导致的发版次数较多;客观来讲,FastJson还是非常优秀的,尤其是在当下,国人更当自强;
- Jackson极少有安全漏洞,且能够支持各种形式的扩展,比如属性忽略、别名注解,以及多种形式的序列化、反序列化注解,扩展非常优雅;
- 参考数据1:Java几种常用JSON库性能比较
- 参考数据2:性能大比拼!这三个主流的JSON解析库,一个快,一个稳,还有一个你想不到!
综合观察上述对比指标,Jackson和FastJson表现较为突出,结合业务场景,选型如下:
以SpringBoot/SpringCloud套件为主的后端服务中,Jackson为默认内置的Json框架,且具有高性能、可扩展性强、生态完善的综合优势,建议采用;
以AlibabaSpringCloud为基础的服务中,则建议使用FastJson,因其具备高性能、迭代快的优点。但这几年漏洞较多,建议尽量按照简单优雅的方式去使用,避免深度定制后,漏洞修复困难;
在项目中,尽量只使用一种Json框架的工具类及Web应用封装,方便后续能够快速迁移到其它Json框架,一旦大家开始使用不同的Json框架编写业务逻辑,项目会非常混乱且难以维护;
本人先后使用过FastJson和Jackson,基于上面的实战原因,最终还是回归到Jackson。后面所有的内容,均以Jackson框架为基础,结合个人的经验进行阐述。如有疏误,敬请斧正。
Json序列化
(Java模型->Json字符串)和Json反序列化
(Json字符串->Java模型),Json反序列化
其实也是深拷贝
种的一种,就是创建出了一个Java模型对象出来。一旦理解了Json 反序列化
和深拷贝
的关系,就会很容易想到Json反序列化
其实和Object.clone()
/org.springframework.beans.BeanUtils.copyProperties
/java.io.ObjectInputStream.readObject
有异曲同工之妙。com.biuqu.json
,Json工具类为com.biuqu.utils.JsonUtil
:<dependency>
<groupId>com.biuqugroupId>
<artifactId>bq-baseartifactId>
<version>1.0.4version>
dependency>
如果在代理的maven中央仓库无法下载到上述依赖包,则需要临时更换alibaba maven代理为maven官方代理 :
https://repo1.maven.org/maven2/
,因为alibaba在2022年底之后就不保证新的jar包可以同步了;
com.biuqu.utils.JsonUtil
源码 如下:public final class JsonUtil
{
/**
* 把json字符串转换成指定类型的对象
*
* @param json json字符串
* @param clazz 模型的class类型
* @param 模型类型
* @return 业务模型实例
*/
public static <T> T toObject(String json, Class<T> clazz)
{
return toObject(json, clazz, false);
}
/**
* 把json字符串转换成指定类型的对象
*
* @param json json字符串
* @param clazz 模型的class类型
* @param snake 是否下划线转驼峰
* @param 模型类型
* @return 业务模型实例
*/
public static <T> T toObject(String json, Class<T> clazz, boolean snake)
{
ObjectMapper mapper = JsonMappers.getMapper(snake);
try
{
return mapper.readValue(json, clazz);
}
catch (JsonProcessingException e)
{
LOGGER.error("parse object error.", e);
}
return null;
}
/**
* 把json字符串转换成指定类型的List集合
*
* @param json json字符串
* @param clazz 模型的class类型
* @param 模型类型
* @return 业务模型实例集合
*/
public static <T> List<T> toList(String json, Class<T> clazz)
{
return toList(json, clazz, false);
}
/**
* 把json字符串转换成指定类型的List集合
*
* @param json json字符串
* @param clazz 模型的class类型
* @param snake 是否下划线转驼峰
* @param 模型类型
* @return 业务模型实例集合
*/
public static <T> List<T> toList(String json, Class<T> clazz, boolean snake)
{
ObjectMapper mapper = JsonMappers.getMapper(snake);
JavaType type = mapper.getTypeFactory().constructParametricType(ArrayList.class, clazz);
try
{
return mapper.readValue(json, type);
}
catch (JsonProcessingException e)
{
LOGGER.error("parse list error.", e);
}
return null;
}
/**
* 获取Map集合
*
* @param json json字符串
* @param kClazz 模型的key class类型
* @param vClazz 模型的value class类型
* @param 模型的key类型
* @param 模型的value类型
* @return 业务模型实例集合
*/
public static <K, V> Map<K, V> toMap(String json, Class<K> kClazz, Class<V> vClazz)
{
return toMap(json, kClazz, vClazz, false);
}
/**
* 获取Map集合
*
* @param json json字符串
* @param kClazz 模型的key class类型
* @param vClazz 模型的value class类型
* @param snake 是否下划线转驼峰
* @param 模型的key类型
* @param 模型的value类型
* @return 业务模型实例集合
*/
public static <K, V> Map<K, V> toMap(String json, Class<K> kClazz, Class<V> vClazz, boolean snake)
{
ObjectMapper mapper = JsonMappers.getMapper(snake);
JavaType type = mapper.getTypeFactory().constructParametricType(Map.class, kClazz, vClazz);
try
{
return mapper.readValue(json, type);
}
catch (JsonProcessingException e)
{
LOGGER.error("parse map error.", e);
}
return null;
}
/**
* 把json字符串转换成指定复杂类型的对象(对象有多层嵌套)
*
* @param json json字符串
* @param typeRef 模型的依赖类型
* @param 模型类型
* @return 业务模型实例集合
*/
public static <T> T toComplex(String json, TypeReference<T> typeRef)
{
return toComplex(json, typeRef, false);
}
/**
* 把json字符串转换成指定复杂类型的对象(对象有多层嵌套)
*
* @param json json字符串
* @param typeRef 模型的依赖类型
* @param 模型类型
* @param snake 是否下划线转驼峰
* @return 业务模型实例集合
*/
public static <T> T toComplex(String json, TypeReference<T> typeRef, boolean snake)
{
ObjectMapper mapper = JsonMappers.getMapper(snake);
try
{
return mapper.readValue(json, typeRef);
}
catch (JsonProcessingException e)
{
LOGGER.error("parse snake complex error.", e);
}
return null;
}
/**
* 获取json字符串
*
* @param t 业务模型
* @param 模型类型
* @return json字符串
*/
public static <T> String toJson(T t)
{
return toJson(t, false);
}
/**
* 获取json字符串
*
* @param t 业务模型
* @param snake 是否支持驼峰转换
* @param 模型类型
* @return json字符串
*/
public static <T> String toJson(T t, boolean snake)
{
ObjectWriter writer = JsonMappers.getMapper(snake).writer();
try
{
return writer.writeValueAsString(t);
}
catch (JsonProcessingException e)
{
LOGGER.error("parse json error.", e);
}
return null;
}
/**
* 获取带忽略属性列表的json字符串
*
* @param t 业务模型
* @param ignoreFields 模型中待忽略的属性列表
* @param 模型类型
* @return json字符串
*/
public static <T> String toJson(T t, Set<String> ignoreFields)
{
return toJson(t, ignoreFields, false);
}
/**
* 获取带忽略属性列表的json字符串
*
* @param t 业务模型
* @param ignoreFields 模型中待忽略的属性列表
* @param snake 是否支持驼峰转换
* @param 模型类型
* @return json字符串
*/
public static <T> String toJson(T t, Set<String> ignoreFields, boolean snake)
{
ObjectWriter writer = JsonMappers.getIgnoreWriter(ignoreFields, snake, false);
try
{
return writer.writeValueAsString(t);
}
catch (JsonProcessingException e)
{
LOGGER.error("parse json error.", e);
}
return null;
}
/**
* 获取json字符串
*
* @param t 业务模型
* @param 模型类型
* @return json字符串
*/
public static <T> String toMask(T t)
{
return toMask(t, false);
}
/**
* 获取json字符串
*
* @param t 业务模型
* @param snake 是否支持驼峰转换
* @param 模型类型
* @return json字符串
*/
public static <T> String toMask(T t, boolean snake)
{
return toMask(t, null, snake);
}
/**
* 获取带忽略属性列表的json字符串
*
* @param t 业务模型
* @param ignoreFields 模型中待忽略的属性列表
* @param 模型类型
* @return json字符串
*/
public static <T> String toMask(T t, Set<String> ignoreFields)
{
return toMask(t, ignoreFields, false);
}
/**
* 获取带忽略属性列表的json字符串
*
* @param t 业务模型
* @param ignoreFields 模型中待忽略的属性列表
* @param snake 是否支持驼峰转换
* @param 模型类型
* @return json字符串
*/
public static <T> String toMask(T t, Set<String> ignoreFields, boolean snake)
{
ObjectWriter writer = JsonMappers.getIgnoreWriter(ignoreFields, snake, true);
try
{
return writer.writeValueAsString(t);
}
catch (JsonProcessingException e)
{
LOGGER.error("parse json error.", e);
}
return null;
}
private JsonUtil()
{
}
/**
* 日志句柄
*/
private static final Logger LOGGER = LoggerFactory.getLogger(JsonUtil.class);
}
源码 基本上包含了上述业务场景的大部分解决方案,在后续对应章节再一一阐述。
@Test
public void toObject()
{
String json = JsonUtil.toJson(person);
Person object1 = JsonUtil.toObject(json, Person.class);
System.out.println("object1==" + JsonUtil.toJson(object1));
Assert.assertTrue(object1.getCardId() != null);
String json2 = JsonUtil.toJson(person, true);
System.out.println("json2==" + json2);
Assert.assertTrue(json2.contains("card_id"));
Person object2 = JsonUtil.toObject(json2, Person.class, true);
System.out.println("object2==" + JsonUtil.toJson(object2));
Assert.assertTrue(object2.getCardId() != null);
}
验证了Json字符串和模型的相互转换,包括下划线Json和驼峰模型的转换,API封装相对较为简洁;
@Test
public void toComplex()
{
List<Person> persons = Lists.newArrayList(person);
ResultCode<List<Person>> result = ResultCode.ok(persons);
String json = JsonUtil.toJson(result);
System.out.println("complex json==" + json);
Assert.assertTrue(json.contains("["));
ResultCode<List<Person>> newResult = JsonUtil.toComplex(json, new TypeReference<ResultCode<List<Person>>>()
{
});
System.out.println("complex new json==" + JsonUtil.toJson(newResult));
Assert.assertTrue(newResult.getData().size() == 1);
}
ResultCode
对象对应的Json字符串,因为Java泛型编译后内存擦除问题,是不好直接用>
JsonUtil.toObject
来反序列化的,
Jackson提供了TypeReference
来解决这个问题,但是此方法的性能相对较差,且因为内存擦除的问题,无法抽象使用;
验证代码 :
@Test
public void testToJson()
{
Person person = new Person();
person.setName("haha");
person.setTime(Instant.now());
String json = JsonUtil.toJson(person);
System.out.println("json=" + json);
Assert.assertTrue(json.contains("time"));
JSONObject jsonObject = new JSONObject(json);
Object time = jsonObject.get("time");
Assert.assertTrue((time instanceof Long) && time.toString().length() == 13);
}
打印的效果为:json={"name":"haha","time":1686097955210}
。这是因为我在JsonUtil 内引用的JsonMappers 中扩展了对Instant类型的支持,代码如下:
JavaTimeModule timeModule = new JavaTimeModule();
JsonInstantSerializer instantSerializer = new JsonInstantSerializer();
//替换其中的Instant时间转换(从秒转到毫秒)
timeModule.addSerializer(Instant.class, instantSerializer);
SNAKE_MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);
SNAKE_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
SNAKE_MAPPER.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
SNAKE_MAPPER.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
SNAKE_MAPPER.setAnnotationIntrospector(new JsonDisableAnnIntrospector());
//注册时间处理模块,注册全部模块方法:findAndRegisterModules
SNAKE_MAPPER.registerModule(timeModule);
如果不扩展的话,则会抛出如下异常:
Java 8 date/time type `java.time.Instant` not supported by default: add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling (through reference chain: com.biuqu.json.JsonUtilTest$Person["time"])
message
/tips
/msg
,则可以采用@JsonAlias
屏蔽这种差异。测试代码 如下:@Test
public void testAlias()
{
Map<String, String> map = Maps.newHashMap();
map.put("message", "msg1");
String json = JsonUtil.toJson(map);
Result result1 = JsonUtil.toObject(json, Result.class);
System.out.println("alias json1=" + JsonUtil.toJson(result1));
Map<String, String> map2 = Maps.newHashMap();
map2.put("tips", "tip1");
String json2 = JsonUtil.toJson(map2);
Result result2 = JsonUtil.toObject(json2, Result.class);
System.out.println("alias json2=" + JsonUtil.toJson(result2));
Map<String, String> map3 = Maps.newHashMap();
map3.put("msg", "msg2");
String json3 = JsonUtil.toJson(map3);
Result result3 = JsonUtil.toObject(json3, Result.class);
System.out.println("alias json3=" + JsonUtil.toJson(result3));
}
@NoArgsConstructor
@Data
private static class Result
{
@JsonAlias({"message", "tips"})
private String msg;
@JsonProperty("name")
private String user;
@JsonIgnore
private String spanId;
}
运行效果如下:alias json1={"msg":"msg1"}
alias json2={"msg":"tip1"}
alias json3={"msg":"msg2"}
当我们需要修改已定义好接口的字段名时,由于对应的Java模型代码在多处被引用,直接改Java属性工作量较大,且容易出错,则可以采用@JsonProperty
来实现Json字段的重命名。测试代码 如下:
@Test
public void testProperty()
{
Map<String, String> map = Maps.newHashMap();
map.put("user", "user1");
String json = JsonUtil.toJson(map);
Result result1 = JsonUtil.toObject(json, Result.class);
System.out.println("property json1=" + JsonUtil.toJson(result1));
Map<String, String> map2 = Maps.newHashMap();
map2.put("name", "user2");
String json2 = JsonUtil.toJson(map2);
Result result2 = JsonUtil.toObject(json2, Result.class);
System.out.println("property json2=" + JsonUtil.toJson(result2));
Result param3 = new Result();
param3.setUser("user3");
String json3 = JsonUtil.toJson(param3);
Result result3 = JsonUtil.toObject(json3, Result.class);
System.out.println("property json3=" + JsonUtil.toJson(result3));
}
运行效果如下:
property json1={}
property json2={"name":"user2"}
property json3={"name":"user3"}
总结下
@JsonAlias
和@JsonProperty
的差异:
@JsonAlias
只参与Json反序列化
,支持多对一转换,即:把Json字符串中的多个不同的字段名,统一转换成Java模型中的属性名。@JsonProperty
既参与Json序列化
,也参与Json反序列化
,只支持唯一转换,即:在Json和模型相互转换时,只使用@JsonProperty
注解中的唯一属性名。
@JsonIgnore
就是Json序列化
和Json反序列化
时,都忽略属性,使用上非常容易理解。该注解的主要使用场景是Java模型有,Json中没有,且在Java模型中,只能通过Java的方法去赋值。测试代码 如下:@Test
public void testIgnore()
{
Map<String, String> map = Maps.newHashMap();
map.put("spanId", "span1");
String json = JsonUtil.toJson(map);
Result result1 = JsonUtil.toObject(json, Result.class);
System.out.println("ignore json1=" + JsonUtil.toJson(result1));
Result param2 = new Result();
param2.setSpanId("span2");
System.out.println("ignore json2=" + JsonUtil.toJson(param2));
}
运行效果如下:ignore json1={}
ignore json2={}
@JsonIgnore
效果相同。差异点在于:前者可以动态设置(比如说:可在配置文件中配置),且还可以批量设置;后者只能单个属性设置,且是在源码中写死。@Test
public void testIgnore2()
{
Result result1 = new Result();
result1.setId("req001");
String json1 = JsonUtil.toJson(result1);
System.out.println("json1=" + json1);
Assert.assertTrue(json1.contains("id"));
Set<String> ignoreFields = Sets.newHashSet("id");
String json2 = JsonUtil.toJson(result1, ignoreFields);
System.out.println("json2=" + json2);
Assert.assertTrue(!json2.contains("id"));
}
运行效果如下:json1={"id":"req001"}
json2={}
动态忽略Json字段核心代码逻辑:/**
* 获取带忽略属性的Mapper对象
*
* @param mapper jackson转换器
* @param ignoreFields 忽略的属性列表
* @return Mapper对象的新Writer对象
*/
public static ObjectWriter getIgnoreWriter(ObjectMapper mapper, Set<String> ignoreFields)
{
if (!CollectionUtils.isEmpty(ignoreFields))
{
SimpleFilterProvider provider = new SimpleFilterProvider();
SimpleBeanPropertyFilter fieldFilter = SimpleBeanPropertyFilter.serializeAllExcept(ignoreFields);
provider.addFilter(IGNORE_ID, fieldFilter);
//对所有的Object的子类都生效的属性过滤
mapper.addMixIn(Object.class, JsonIgnoreField.class);
return mapper.writer(provider);
}
return mapper.writer();
}
上述代码最核心逻辑
mapper.addMixIn(Object.class, JsonIgnoreField.class);
其实就是设置了对指定的class类型的属性都做过滤。
@JsonMaskAnn
和自定义的JsonRule
规则来实现的。测试代码 如下:@Test
public void toJson()
{
//case1:不忽略+不打码
String json1 = JsonUtil.toJson(this.person);
System.out.println("json1=" + json1);
Assert.assertTrue(json1.contains("20000101"));
Assert.assertTrue(json1.contains("pwd"));
//case2:不忽略+打码
String json2 = JsonUtil.toMask(this.person);
System.out.println("json2=" + json2);
Assert.assertTrue(!json2.contains("20000101"));
Assert.assertTrue(json2.contains("pwd"));
//case3:忽略+不打码
Set<String> ignoreFields = Sets.newHashSet("pwd");
String json3 = JsonUtil.toJson(this.person, ignoreFields);
System.out.println("json3=" + json3);
Assert.assertTrue(json3.contains("20000101"));
Assert.assertTrue(!json3.contains("pwd"));
//case4:忽略+打码
String json4 = JsonUtil.toMask(this.person, ignoreFields);
System.out.println("json4=" + json4);
Assert.assertTrue(!json4.contains("pwd"));
Assert.assertTrue(!json4.contains("20000101"));
}
运行效果如下:json1={"name":"狄仁杰","base64":"/9j/4AAQSkZJRgABAQEAYABgAAD/4QAwRXhpZgAATU0AKgAAAAgAAQExAAIAAAAOAAAAGgAAVpdHUuY29tAP/bAEMA","pwd":"123456","phone":"13234567890","cardId":"444444200001016666","bankNo":"666444444200001016666333"}
json2={"name":"狄$$","base64":"/9j/4AAQSkZJRgABAQEAYABgAAD/4QAwRXhpZgAATU0AKgAAAA......","pwd":"123456","phone":"132****7890","cardId":"444444********6666","bankNo":"666444**************6333"}
json3={"name":"狄仁杰","base64":"/9j/4AAQSkZJRgABAQEAYABgAAD/4QAwRXhpZgAATU0AKgAAAAgAAQExAAIAAAAOAAAAGgAAVpdHUuY29tAP/bAEMA","phone":"13234567890","cardId":"444444200001016666","bankNo":"666444444200001016666333"}
json4={"name":"狄$$","base64":"/9j/4AAQSkZJRgABAQEAYABgAAD/4QAwRXhpZgAATU0AKgAAAA......","phone":"132****7890","cardId":"444444********6666","bankNo":"666444**************6333"}
@JsonMaskAnn
代码如下:/**
* Jackson针对对象的属性打码注解
*
* @author BiuQu
* @date 2023/1/4 15:01
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@JacksonAnnotationsInside
@JsonSerialize(using = JsonMaskSerializer.class)
public @interface JsonMaskAnn
{
}
@JsonMaskAnn
中的核心逻辑在其注解的JsonMaskSerializer
类中:public class JsonMaskSerializer extends BaseJsonSerializer<String>
{
@Override
protected Object getNewValue(Object object, String key, String value)
{
if (ApplicationContextHolder.containsBean(Const.JSON_MASK_SVC))
{
JsonMaskMgr maskMgr = ApplicationContextHolder.getBean(Const.JSON_MASK_SVC);
return maskMgr.applyRule(object.getClass().getName(), key, value);
}
else
{
return JsonRuleMgr.applyRule(key, value);
}
}
}
public abstract class BaseJsonSerializer<T> extends JsonSerializer<T>
{
@Override
public void serialize(T value, JsonGenerator jsonGen, SerializerProvider provider) throws IOException
{
String name = jsonGen.getOutputContext().getCurrentName();
Object object = provider.getGenerator().currentValue();
Object newValue = getNewValue(object, name, value);
if (value instanceof String)
{
if (newValue instanceof String)
{
jsonGen.writeString(newValue.toString());
}
}
else if (value instanceof Instant)
{
jsonGen.writeNumber(Long.parseLong(newValue + StringUtils.EMPTY));
}
}
/**
* 获取序列后的新值
*
* @param object 原始对象
* @param key 键值对的key
* @param value 键值对的value
* @return 新值
*/
protected abstract Object getNewValue(Object object, String key, T value);
}
打码的核心逻辑在JsonRule
类中,其实就是字符串判断和替换,逻辑不复杂,就不展示了。
注意:Json
打码
和脱敏
都是针对敏感数据的模糊处理方式,二者含义其实是有区别的。打码
是隐藏部分字段,但是还有部分字段是真实的敏感数据,脱敏
则是通过换算,完全不暴露原始敏感信息,也无法还原(如:Hash摘要等)。
@JsonFuzzyAnn
来实现的。测试代码@Test
public void testFuzzy()
{
Result result1 = new Result();
result1.setSign("sign001");
String json1 = JsonUtil.toMask(result1);
System.out.println("json1=" + json1);
Assert.assertTrue(json1.contains("sign"));
Assert.assertTrue(!json1.contains(result1.getSign()));
}
运行结果:json1={"sign":"e934a381a2ef37e88d0a5f4e449209ab01f44825da22a9ea5adca7ffa70dbfd98f98d16f32f1a180e8d402cd8602d6cd19f19e52e0e0c5b23b14783cea29df39"}
@RestController
注解暴露的Web服务就可以做到反序列化成Java模型对象。当然,Web服务的Java模型入参还需要通过@RequestBody
注解进行标记。ContentType
为:application/json;charset=UTF-8
,避免响应数据乱码。ContentType
。扩展方法会在下一小节说明。@Slf4j
@Configuration
public class WebMvcConfigurer extends BaseWebConfigurer
{
/**
* actuator健康检查的自定义类型
*/
private static final String ACTUATOR_TYPE = "vnd.spring-boot.actuator.v3+json";
/**
* 是否驼峰式json(默认支持)
*/
@Value("${bq.json.snake-case:true}")
private boolean snakeCase;
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters)
{
for (int i = 0; i < converters.size(); i++)
{
HttpMessageConverter<?> converter = converters.get(i);
if (converter instanceof MappingJackson2HttpMessageConverter)
{
ObjectMapper mapper = JsonMappers.getMapper(this.snakeCase);
MappingJackson2HttpMessageConverter conv = new MappingJackson2HttpMessageConverter();
conv.setObjectMapper(mapper);
//默认返回的Jackson对应的Rest服务的ContentType为:'application/json;charset=UTF-8'
List<MediaType> types = Lists.newArrayList();
MediaType utf8Type = new MediaType(MediaType.APPLICATION_JSON, StandardCharsets.UTF_8);
types.add(utf8Type);
//兼容支持actuator健康检查
MediaType actuatorType = new MediaType(MediaType.APPLICATION_JSON.getType(), ACTUATOR_TYPE);
types.add(actuatorType);
conv.setSupportedMediaTypes(types);
//把旧的转换器替换成新的转换器
converters.set(i, conv);
break;
}
}
}
}
项目对应的yaml配置(以bq-service-biz
服务为例),代码示例 如下:bq:
json:
snake-case: true
通过上述代码设计,把驼峰和下划线转换变成了yaml中的一个配置项。同时约定了所有接口的
ContentType
为:application/json;charset=UTF-8
,使用也非常简单。
在3.1.4
章节中就介绍了每种扩展的单独使用及验证效果。在实际项目中基本上都是一起使用的。为了一次性说明这个问题,定义了一个入参Java模型和一个出参Java模型,并通过一个RestController来进行串联。设计的样例中,同时包含了脱敏、打码、动态忽略等Json应用。
入参模型:
@Data
public class UserInner
{
/**
* 用户名(打码到日志)
*/
@JsonMaskAnn
private String name;
/**
* 密码(不能打印到日志)
*/
private String pwd;
/**
* 秘钥key(不能打印到日志)
*/
private String key;
/**
* 真实姓名(打码到日志)
*/
@JsonMaskAnn
private String realName;
/**
* 身份证号(打码到日志)
*/
@JsonMaskAnn
private String cardId;
/**
* 银行卡号(打码到日志)
*/
@JsonMaskAnn
private String bankNo;
/**
* 电话号码(打码到日志)
*/
@JsonMaskAnn
private String phone;
/**
* 头像Base64(打码到日志)
*/
@JsonMaskAnn
private String photo;
/**
* 头像Base64签名值(打印摘要值到日志)
*/
@JsonFuzzyAnn
private String photoHash;
}
出参模型:
@Data
public class UserOuter
{
/**
* 用户名(打码返回)
*/
@JsonMaskAnn
private String name;
/**
* 密码(不能接口返回)
*/
private String pwd;
/**
* 秘钥key(不能接口返回)
*/
private String key;
/**
* 真实姓名(接口返回)
*/
@JsonMaskAnn
private String realName;
/**
* 身份证号(打码返回)
*/
@JsonMaskAnn
private String cardId;
/**
* 银行卡号(打码返回)
*/
@JsonMaskAnn
private String bankNo;
/**
* 电话号码(打码返回)
*/
@JsonMaskAnn
private String phone;
/**
* 头像Base64(接口返回)
*/
private String photo;
/**
* 头像Base64签名值(不能接口返回)
*/
private String photoHash;
}
定义RestController:
@Slf4j
@RestController
public class DemoUserController
{
@PostMapping("/demo/user/query")
public ResultCode<UserOuter> execute(@RequestBody UserInner user)
{
log.info("current inner 1:{}", JsonUtil.toJson(user));
log.info("current inner snake 2:{}", jsonFacade.toJson(user, true));
log.info("current inner mask 3:{}", jsonFacade.toMask(user, true));
log.info("current inner ignore 4:{}", jsonFacade.toIgnore(user, true));
log.info("current inner all 5:{}", jsonFacade.toJson(user, true, true, true));
UserOuter outer = new UserOuter();
BeanUtils.copyProperties(user, outer);
log.info("current outer 1:{}", JsonUtil.toJson(outer));
log.info("current outer snake 2:{}", jsonFacade.toJson(outer, true));
log.info("current outer mask 3:{}", jsonFacade.toMask(outer, true));
log.info("current outer ignore 4:{}", jsonFacade.toIgnore(outer, true));
log.info("current outer all 5:{}", jsonFacade.toJson(outer, true, true, true));
ResultCode<UserOuter> resultCode = ResultCode.ok(outer);
return resultCode;
}
/**
* 注入json处理服务
*/
@Autowired
private JsonFacade jsonFacade;
}
执行curl命令:
curl --location 'http://localhost:9993/demo/user/query' \
--header 'Content-Type: application/json' \
--data '{
"name": "name123",
"pwd": "pwd123",
"key": "key123",
"real_name": "狄仁杰",
"card_id": "123456789876543212",
"bank_no": "1234567898765432123456789",
"phone": "12345678901",
"photo": "1234567898765432123456789123456789876543212345678912345678987654321234567891234567898765432123456789",
"photo_hash":"fff1234567898765432123456789"
}'
打码、脱敏、动态忽略后的接口返回结果:
{
"code": "100001",
"msg": "通过",
"data": {
"name": "na$$$23",
"real_name": "狄**",
"card_id": "123456********3212",
"bank_no": "123456***************6789",
"phone": "123****8901",
"photo": "1234567898765432123456789123456789876543212345678912345678987654321234567891234567898765432123456789"
},
"cost": 0
}
打码、脱敏、动态忽略后的后台日志结果:
current inner 1:{"name":"name123","pwd":"pwd123","key":"key123","realName":"狄仁杰","cardId":"123456789876543212","bankNo":"1234567898765432123456789","phone":"12345678901","photo":"1234567898765432123456789123456789876543212345678912345678987654321234567891234567898765432123456789","photoHash":"fff1234567898765432123456789"}
current inner snake 2:{"name":"name123","pwd":"pwd123","key":"key123","real_name":"狄仁杰","card_id":"123456789876543212","bank_no":"1234567898765432123456789","phone":"12345678901","photo":"1234567898765432123456789123456789876543212345678912345678987654321234567891234567898765432123456789","photo_hash":"fff1234567898765432123456789"}
current inner mask 3:{"name":"na$$$23","pwd":"pwd123","key":"key123","real_name":"狄**","card_id":"123456********3212","bank_no":"123456***************6789","phone":"123****8901","photo":"12345678987654321234567891234567898765432123456789......","photo_hash":"40d3b1825d4a47d77df97a383399300a12180961a60492d10d7ac46f85cfd32e81325bbdbc78a7c69b07c0d66122217855048f9234538dceb1cdfd579d64b7bc"}
current inner ignore 4:{"name":"name123","real_name":"狄仁杰","card_id":"123456789876543212","bank_no":"1234567898765432123456789","phone":"12345678901","photo":"1234567898765432123456789123456789876543212345678912345678987654321234567891234567898765432123456789","photo_hash":"fff1234567898765432123456789"}
current inner all 5:{"name":"na$$$23","real_name":"狄**","card_id":"123456********3212","bank_no":"123456***************6789","phone":"123****8901","photo":"12345678987654321234567891234567898765432123456789......","photo_hash":"40d3b1825d4a47d77df97a383399300a12180961a60492d10d7ac46f85cfd32e81325bbdbc78a7c69b07c0d66122217855048f9234538dceb1cdfd579d64b7bc"}
current outer 1:{"name":"name123","pwd":"pwd123","key":"key123","realName":"狄仁杰","cardId":"123456789876543212","bankNo":"1234567898765432123456789","phone":"12345678901","photo":"1234567898765432123456789123456789876543212345678912345678987654321234567891234567898765432123456789","photoHash":"fff1234567898765432123456789"}
current outer snake 2:{"name":"name123","pwd":"pwd123","key":"key123","real_name":"狄仁杰","card_id":"123456789876543212","bank_no":"1234567898765432123456789","phone":"12345678901","photo":"1234567898765432123456789123456789876543212345678912345678987654321234567891234567898765432123456789","photo_hash":"fff1234567898765432123456789"}
current outer mask 3:{"name":"na$$$23","pwd":"pwd123","key":"key123","real_name":"狄**","card_id":"123456********3212","bank_no":"123456***************6789","phone":"123****8901","photo":"1234567898765432123456789123456789876543212345678912345678987654321234567891234567898765432123456789","photo_hash":"fff1234567898765432123456789"}
current outer ignore 4:{"name":"name123","real_name":"狄仁杰","card_id":"123456789876543212","bank_no":"1234567898765432123456789","phone":"12345678901","photo":"1234567898765432123456789123456789876543212345678912345678987654321234567891234567898765432123456789"}
current outer all 5:{"name":"na$$$23","real_name":"狄**","card_id":"123456********3212","bank_no":"123456***************6789","phone":"123****8901","photo":"1234567898765432123456789123456789876543212345678912345678987654321234567891234567898765432123456789"}
可以看到打码、脱敏、动态忽略等既可以单独配置,也可以综合在一起使用生效。
配置规则 为:
bq:
json:
rules:
- clazz: com.biuqu.boot.common.demo.model.UserInner
rules:
#用户名除了前2个字符和后2个字符外的字符都打'$'码
- name: name
index: 2
mask: $
len: -2
#超过50个字符的后面都不显示打码
- name: photo
index: -1
mask: .
len: 50
#手机号前3后4之外的都打码
- name: phone
index: 3
len: -4
#身份证号6后4之外的都打码
- name: card_id
index: 6
len: -4
#银行卡号前6后4之外的都打码
- name: bank_no
index: 6
len: -4
#姓名字段的第一个字符之后的所有字符都打码
- name: real_name
index: 1
len: 0
ignores:
- clazz: com.biuqu.boot.common.demo.model.UserInner
fields: pwd,key
- clazz: com.biuqu.boot.common.demo.model.UserOuter
fields: pwd,key,photo_hash
在SpringBoot中注入Json管理的JsonMaskMgr
过程说明如下:
JsonMaskMgr
的注入方式,需要引用如下依赖(这样设计的原因,需要关注下Java开源接口微服务代码框架 相关说明):
<dependency>
<groupId>com.biuqugroupId>
<artifactId>bq-boot-rootartifactId>
<version>1.0.4version>
dependency>
,对应的注入代码为:
@Configuration
public class JsonConfigurer
{
@Bean(CommonBootConst.JSON_RULES)
@ConfigurationProperties(prefix = "bq.json.rules")
public List<JsonMask> jsonMasks()
{
return Lists.newArrayList();
}
@Bean(Const.JSON_MASK_SVC)
public JsonMaskMgr maskMgr(@Qualifier(CommonBootConst.JSON_RULES) List<JsonMask> masks)
{
return new JsonMaskMgr(masks);
}
}
JsonMaskMgr
Json打码管理器的代码为:
public class JsonMaskMgr
{
/**
* 构造方法,初始化所有规则
*
* @param masks 打码规则
*/
public JsonMaskMgr(List<JsonMask> masks)
{
for (JsonMask mask : masks)
{
String className = mask.getClazz();
List<JsonRule> rules = mask.getRules();
Map<String, JsonRule> ruleCache = Maps.newConcurrentMap();
for (JsonRule rule : rules)
{
ruleCache.put(rule.getName(), rule);
}
MASK_CACHE.put(className, ruleCache);
}
}
/**
* 应用打码规则,并返回打码后的字符
*
* @param className 打码的全路径类名
* @param key 打码的key
* @param value 待打码的value
* @return 打码后的value
*/
public String applyRule(String className, String key, String value)
{
if (MASK_CACHE.containsKey(className))
{
Map<String, JsonRule> ruleCache = MASK_CACHE.get(className);
if (ruleCache.containsKey(key))
{
JsonRule rule = ruleCache.get(key);
return rule.apply(value);
}
}
return StringUtils.EMPTY;
}
/**
* 打码的缓存规则
*/
private static final Map<String, Map<String, JsonRule>> MASK_CACHE = Maps.newConcurrentMap();
}
此逻辑仅能做到后台日志文件中的脱敏和打码效果,做不到接口的动态忽略效果。
为了达成接口动态忽略效果,还必须在3.2.1
章节的代码
基础上进一步继承覆写其中的MappingJackson2HttpMessageConverter
类才能达成最终效果:
public class Json2HttpConverter extends MappingJackson2HttpMessageConverter
{
/**
* 定制Json转换器ObjectMapper的写对象ObjectWriter
*
* @param objectMapper Json转换器
* @param serializationView Jackson特殊视图对象
* @param object 原始数据对象
* @return Json转换器的定制写对象(支持配置忽略属性列表)
*/
private ObjectWriter getObjectWriter(ObjectMapper objectMapper, Class<?> serializationView, Object object)
{
ObjectWriter objectWriter;
if (null != serializationView)
{
objectWriter = objectMapper.writerWithView(serializationView);
}
else
{
Set<String> ignoreFields = ignoreMgr.getIgnores(object);
objectWriter = JsonMappers.getIgnoreWriter(objectMapper, ignoreFields);
}
return objectWriter;
}
public Json2HttpConverter(JsonIgnoreMgr ignoreMgr)
{
super();
this.ignoreMgr = ignoreMgr;
}
/**
* json属性忽略管理器
*/
private final JsonIgnoreMgr ignoreMgr;
}
脱敏
实现和打码
实现类似,且要简单很多,就没必要详细介绍了。
@Slf4j
@Configuration
public class FluxConfigurer implements WebFluxConfigurer
{
@Bean
public ServerCodecConfigurer.ServerDefaultCodecs defaultCodecs(ServerCodecConfigurer configurer)
{
ServerCodecConfigurer.ServerDefaultCodecs codecs = configurer.defaultCodecs();
codecs.maxInMemorySize(maxSize);
return codecs;
}
@Override
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer)
{
configurer.defaultCodecs().maxInMemorySize(maxSize);
configurer.defaultCodecs().jackson2JsonEncoder(new JsonEncoder(snakeCase));
}
/**
* WebFlux json转换
*/
private static class JsonEncoder implements Encoder<Object>
{
public JsonEncoder(boolean snake)
{
this.snake = snake;
}
@Override
public boolean canEncode(ResolvableType elementType, MimeType mimeType)
{
return true;
}
@Override
public Flux<DataBuffer> encode(Publisher<?> inputStream, DataBufferFactory bufferFactory,
ResolvableType elementType, MimeType mimeType, Map<String, Object> hints)
{
if (inputStream instanceof Mono)
{
return Mono.from(inputStream).map(value -> encodeValue(value, bufferFactory)).flux();
}
if (inputStream instanceof Flux)
{
return Flux.from(inputStream).map(value -> encodeValue(value, bufferFactory));
}
return null;
}
@Override
public List<MimeType> getEncodableMimeTypes()
{
MimeType mt = MimeTypeUtils.APPLICATION_JSON;
MimeType mimeType = new MimeType(mt.getType(), mt.getSubtype(), StandardCharsets.UTF_8);
return Collections.singletonList(mimeType);
}
/**
* 使用系统标准的json处理数据
*
* @param value 业务对象
* @param bufferFactory netty对应的数据处理工厂
* @return 经过转换后的netty数据类型
*/
private DataBuffer encodeValue(Object value, DataBufferFactory bufferFactory)
{
DataBuffer buffer = bufferFactory.allocateBuffer();
byte[] bytes = new byte[0];
try
{
bytes = JsonMappers.getMapper(snake).writeValueAsBytes(value);
}
catch (JsonProcessingException e)
{
log.error("failed to write back json.", e);
}
buffer.write(bytes);
return buffer;
}
/**
* 是否驼峰转换
*/
private final boolean snake;
}
/**
* 报文的大小限制
*/
@Value("${spring.codec.max-in-memory-size}")
private int maxSize;
/**
* 是否驼峰式json(默认支持)
*/
@Value("${bq.json.snake-case:true}")
private boolean snakeCase;
}
这样设计的原因,需要关注下Java开源接口微服务代码框架 相关说明。
@Configuration
public class RedisConfigurer
{
@Primary
@Bean
public RedisTemplate<String, Object> instance(RedisConnectionFactory factory)
{
RedisTemplate<String, Object> redis = new RedisTemplate<>();
redis.setConnectionFactory(factory);
StringRedisSerializer keySerializer = new StringRedisSerializer();
redis.setKeySerializer(keySerializer);
redis.setHashKeySerializer(keySerializer);
Jackson2JsonRedisSerializer<?> valueSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
valueSerializer.setObjectMapper(mapper);
redis.setValueSerializer(valueSerializer);
redis.setHashValueSerializer(valueSerializer);
redis.afterPropertiesSet();
return redis;
}
@Autowired
public void redisProperties(RedisProperties redisProperties)
{
//TODO 支持redis密码托管场景重新设置密码
}
}
- 注意:这里因为已经在后台内部转换了,所以就没有必要考虑下划线的适配逻辑了。后台模型及redis数据全部是驼峰式的会更优雅。
- 验证因为涉及到服务初始化、存储初始化,暂未提供。有兴趣的朋友可以去redis中看看存储Java对象的效果。