Json在SpringBoot/SpringCloud微服务中的应用

目录导读

  • 聊聊Json这点事儿
    • 1. Json使用场景
    • 2. Json类型
      • 2.1 Json对比列表
      • 2.2 Json 选型
    • 3. Json在项目中的实践
      • 3.1 Json字符串和模型转换
        • 3.1.1 Json字符串和模型转换
        • 3.1.2 Json字符串转换成复杂的Java对象
        • 3.1.3 Java特殊类型转换成Json字符串
        • 3.1.4 Java Json扩展使用
          • 3.1.4.1 @JsonAlias使用
          • 3.1.4.2 @JsonProperty使用
          • 3.1.4.3 @JsonIgnore使用
          • 3.1.4.4 动态忽略Json字段
          • 3.1.4.5 自定义Json打码
          • 3.1.4.6 自定义Json脱敏
    • 3.2 Json在spring-boot-starter-web的应用
      • 3.2.1 Json驼峰/下划线字符串自动转换成Java模型
      • 3.2.2 Json接口和运行日志脱敏在web中的综合应用
    • 3.3 Json在spring-boot-starter-webflux的应用
      • 3.3.1 WebFlux Json驼峰/下划线字符串与Java模型互转应用
      • 3.3.2 WebFlux日志脱敏说明
    • 3.4 Json在redis中的应用
    • 4. 参考资料

聊聊Json这点事儿

  • Json(JavaScript Object Notation,JavaScript 对象简谱),是一种轻量级的数据交换格式。易于人阅读和编写,可以在多种语言之间进行数据交换;
  • 在当下流行的分布式服务开发中,前后端交互、后端之间的接口交互,一般都使用json格式的数据。因此,你只要做后端研发,就很难绕过json数据的转换;
  • 在项目中,经常会存在多种类型的json。部分是你主动选型的,部分是被你引用的其它三方件给引用的,不得不用;
  • json结构的数据具备可读写强,占用存储空间较小的综合优势。随着当下RESTful风格的流行,HTTP服务逐渐取代了WSDL规范的WebService。基于HTTP的json数据相比基于WebService的xml数据,就具备更好的可读写、更小存储空间的特点;当然json并不是最省存储空间的数据格式,在约定好字段顺序的前提下,完全可以只传输json的value值(类似csv格式),这样占用的存储空间就可以进一步大大减小,但又违背了可读写原则,无法做到自解释。所以,Json数据格式是在可读写和存储空间平衡下的较优选择。

    所谓存储空间,是相对Json数据而言的。Json数据在网络传输时,存储空间其实就是占用的带宽大小;Json数据在在内存中处理时,存储空间对应的就是占用的内存大小;Json数据在持久化至本地磁盘时,存储空间就是占用的磁盘大小;

  • Json有非常多的实现方式,包括jackson/fastjson/gson/json-lib/org.json等,做好Json选型也不是件容易的事;

1. Json使用场景

Json通常用在接口交互,随着Json的广泛使用,在不同的业务场景下,会演化出不同的使用方式,随之出现的问题也越来越来。仅从本人的经验来讲,就碰到了Json使用上的如下诉求:

  • 在有的项目中,接口的Json参数是驼峰的;在另外一些项目中,接口的Json参数是下划线的;甚至存在同一个接口,在不同的项目中,有要求驼峰的,也有要求下划线的;

    有人可能觉得这样很奇怪,但这在企业应用中是真实存在的。存在即合理,为了保障客户使用的延续性,需要架构师/coder做好兼容性设计和编码。

  • Json返回参数不能多字段,多了会导致部分客户端报错;

    这里有客户设计不合理的地方,也可以通过服务端的Json处理去规避这个问题。

  • 部分模型字段涉及敏感数据,在接口返回Json参数或日志打印时,必须脱敏;

    这个场景比较复杂:存在日志必须脱敏,但是接口响应Json结果不脱敏的情况;也存在日志脱敏,接口响应Json也要脱敏的情况。接口设计时,必须考虑这些场景的兼容支持。

  • Json数据的字段非常大,日志打印时,需要忽略或者缩减,否则日志报文非常大;

    直接忽略Json属性的好处是处理简单,缺点是不方便定位问题,缩减则是比较优雅的处理方式。

  • Json数据需要转换成复杂的带多层泛型的业务模型,如:List>

    在Json反序列化成模型时,存在泛型类型内存擦除的问题,必须要做特殊处理。

  • 接口入参需要接收多个不同的Json属性名;

    可以提升接口的兼容性;

  • 接口调用第三方时,模型字段需要同时接收多个不同名字的Json属性名;

    可以提升接口的兼容性;

  • 接口中存在特殊类型,业务中需要自定义扩展才能满足要求;

    如:复用的oauth2框架时,返回JwtToken Json包含了Instant类型,现在需要把默认的秒转换成毫秒;

  • 有接口返回Json的某字段是字符串,但在另外场景下,该接口返回Json的该字段为字符串数组;

    一般出现在接口的兼容设计中,如:刚开始的字段只考虑了支持单个类型,但是随着业务的发展,接口必须要支持批量。虽然设计了2个不同的接口搞定了这个问题,但是也有更优的复用处理方式。

2. Json类型

Json实现方式非常多,各有各的使用场景,常见的方式是和同源的其他组件绑定。常用的Json主要有jackson/fastjson/gson 3种,其它还包括:json-lib/org.json等。

2.1 Json对比列表

结合网上信息及本人的使用体验,Json对比汇总如下:

Json类型 序列化性能 反序列化 安全性 可扩展性 生态
Jackson 最好 好,SpringBoot默认集成
FastJson 最好 较差 NA 较好,AlibabaSpringCloud默认集成
Gson 较好 NA 较好,Guava等默认集成
JsonLib NA NA NA
org.json NA NA NA NA NA

补充说明:

  1. 在多年的项目经验中,FastJson是漏洞最多的,因为漏洞导致的发版次数较多;客观来讲,FastJson还是非常优秀的,尤其是在当下,国人更当自强;
  2. Jackson极少有安全漏洞,且能够支持各种形式的扩展,比如属性忽略、别名注解,以及多种形式的序列化、反序列化注解,扩展非常优雅;
  3. 参考数据1:Java几种常用JSON库性能比较
  4. 参考数据2:性能大比拼!这三个主流的JSON解析库,一个快,一个稳,还有一个你想不到!

2.2 Json 选型

综合观察上述对比指标,Jackson和FastJson表现较为突出,结合业务场景,选型如下:

  • 以SpringBoot/SpringCloud套件为主的后端服务中,Jackson为默认内置的Json框架,且具有高性能、可扩展性强、生态完善的综合优势,建议采用;

  • 以AlibabaSpringCloud为基础的服务中,则建议使用FastJson,因其具备高性能、迭代快的优点。但这几年漏洞较多,建议尽量按照简单优雅的方式去使用,避免深度定制后,漏洞修复困难;

    在项目中,尽量只使用一种Json框架的工具类及Web应用封装,方便后续能够快速迁移到其它Json框架,一旦大家开始使用不同的Json框架编写业务逻辑,项目会非常混乱且难以维护;

3. Json在项目中的实践

本人先后使用过FastJson和Jackson,基于上面的实战原因,最终还是回归到Jackson。后面所有的内容,均以Jackson框架为基础,结合个人的经验进行阐述。如有疏误,敬请斧正。

3.1 Json字符串和模型转换

  • Json字符串和模型转换,在Java语言中,就是Json序列化(Java模型->Json字符串)和Json反序列化(Json字符串->Java模型),Json反序列化其实也是深拷贝种的一种,就是创建出了一个Java模型对象出来。一旦理解了Json 反序列化深拷贝的关系,就会很容易想到Json反序列化其实和Object.clone()/org.springframework.beans.BeanUtils.copyProperties/java.io.ObjectInputStream.readObject有异曲同工之妙。
  • json基础能力的封装,在已开源的Spring基础框架 中,核心包名为: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包可以同步了;

  • Json处理的工具类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);
    }
    

    源码 基本上包含了上述业务场景的大部分解决方案,在后续对应章节再一一阐述。

3.1.1 Json字符串和模型转换

  • 验证代码 :
    @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封装相对较为简洁;

3.1.2 Json字符串转换成复杂的Java对象

  • 验证代码 :
    @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来解决这个问题,但是此方法的性能相对较差,且因为内存擦除的问题,无法抽象使用;

3.1.3 Java特殊类型转换成Json字符串

  • 验证代码 :

    @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"])
    

3.1.4 Java Json扩展使用

3.1.4.1 @JsonAlias使用
  • 当需要和多个关联方渠道对接时,由于关联方的接口定义存在一定差异,如:A/B/C 3个渠道接口的Json描述字段名分别为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"}
    
3.1.4.2 @JsonProperty使用
  • 当我们需要修改已定义好接口的字段名时,由于对应的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的差异:

    1. @JsonAlias只参与Json反序列化,支持多对一转换,即:把Json字符串中的多个不同的字段名,统一转换成Java模型中的属性名。
    2. @JsonProperty既参与Json序列化,也参与Json反序列化,只支持唯一转换,即:在Json和模型相互转换时,只使用@JsonProperty注解中的唯一属性名。
3.1.4.3 @JsonIgnore使用
  • @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={}
    
3.1.4.4 动态忽略Json字段
  • 动态忽略Json字段,同@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类型的属性都做过滤。

3.1.4.5 自定义Json打码
  • 自定义Json打码,主要是通过模型的@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摘要等)。

3.1.4.6 自定义Json脱敏
  • 自定义Json打码,主要是通过模型的@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"}
    

3.2 Json在spring-boot-starter-web的应用

  • Jackson是SpringBoot的内置Json框架,可以做到客户端传入Json字符串时,后端通过@RestController注解暴露的Web服务就可以做到反序列化成Java模型对象。当然,Web服务的Java模型入参还需要通过@RequestBody注解进行标记。
  • 在后端Web服务做完业务逻辑处理后,又会通过Json框架把要返回的出参Java模型序列化成Json字符串,返回给客户端。注意:返回数据时,还要指定ContentType为:application/json;charset=UTF-8,避免响应数据乱码。
  • SpringBoot也提供了Json框架的扩展点,方便项目对默认的Json框架进行替换,同时也可以指定全局的ContentType。扩展方法会在下一小节说明。

3.2.1 Json驼峰/下划线字符串自动转换成Java模型

  • 前面提到了有些大公司因为系统复杂,开发团队众多,导致有些服务接口入参和出参的字段名是下划线的,有些是驼峰的。可以通过SpringBoot Web框架的扩展点进行扩展。代码摘录 如下:
    @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.2.2 Json接口和运行日志脱敏在web中的综合应用

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);
        }
    }
    

    JsonMaskMgrJson打码管理器的代码为:

    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;
    }
    
  • 脱敏实现和打码实现类似,且要简单很多,就没必要详细介绍了。

3.3 Json在spring-boot-starter-webflux的应用

  • 基于netty的webflux和基于servlet的web差异较大,核心就是要通过类似的spring框架扩展点实现json的定制。

3.3.1 WebFlux Json驼峰/下划线字符串与Java模型互转应用

  • 通过Spring扩展点支持Json驼峰FluxConfigurer 如下:
    @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开源接口微服务代码框架 相关说明。

3.3.2 WebFlux日志脱敏说明

  • 在Java开源接口微服务代码框架 中,代码架构明确说明,WebFlux主要应用为鉴权网关,理论上是不应该关注业务模型的,也不应该在网关中配置模型的打码等屏蔽规则。
  • 此章节仍然列出的目的是希望大家了解下,所有的架构、代码设计均是以业务为目标,而不要盲目地去做技术实现,也不要轻易打破架构设计和代码设计的责任边界。

3.4 Json在redis中的应用

  • 同上面Web和WebFlux对SpringBoot框架的扩展类似,也可以自动注入Spring Redis的RedisConfigurer扩展代码 :
    @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密码托管场景重新设置密码
        }
    }
    
    1. 注意:这里因为已经在后台内部转换了,所以就没有必要考虑下划线的适配逻辑了。后台模型及redis数据全部是驼峰式的会更优雅。
    2. 验证因为涉及到服务初始化、存储初始化,暂未提供。有兴趣的朋友可以去redis中看看存储Java对象的效果。

4. 参考资料

  • [1] Java几种常用JSON库性能比较
  • [2] 性能大比拼!这三个主流的JSON解析库,一个快,一个稳,还有一个你想不到!
  • [3] JSON百度百科

你可能感兴趣的:(springcloud,biuqu项目,json,java,springboot,脱敏,打码)