spring boot 七:SpringBoot2.5.4自定义配置Jackson的ObjectMapper
1 前言
SpringBoot底层默认使用的自动依赖注入,即spring-boot-autoconfigure包的META-INF下,存在spring.factories文件,里面有自动注入的jackson自动配置类。在EnableAutoConfiguration的配置下,名为JacksonAutoConfiguration。根据对该自动配置实施自定义Bean配置,可实现对@ResponseBody或@RestController注解下的响应结果的全局序列化jackson配置。
比如SpringBoot默认的json类型返回中,new Date() 类型数据返回的时间戳,LocalDateTime类型的格式化不美观等等,可自定义jackson objectMapper全局配置解决。



SpringBoot参考文档(版本为SpringBoot2.5.14文档,实际使用的SpringBoot依赖版本2.5.4,参考实际源码即可):
https://docs.spring.io/spring-boot/docs/2.5.14/reference/html/features.html#features.json
https://docs.spring.io/spring-boot/docs/2.5.14/reference/html/howto.html#howto.spring-mvc.customize-jackson-objectmapper
 
依赖配置:
<parent>
    <artifactId>spring-boot-starter-parent</artifactId>
    <groupId>org.springframework.boot</groupId>
    <version>2.5.4</version>
</parent>
<dependencies>
	<dependency>
	    <groupId>org.springframework.boot</groupId>
	    <artifactId>spring-boot-starter-web</artifactId>
	</dependency>
	
	<dependency>
	    <groupId>org.projectlombok</groupId>
	    <artifactId>lombok</artifactId>
	</dependency>
</dependencies>
 
2 使用
2.1 自定义Jackson ObjectMapper配置类:
使用前,先观察下部分源码:
启动类的@SpringBootApplication注解中,包含了@EnableAutoConfiguration注解,而@EnableAutoConfiguration注解上,又使用@Import导入了资源类AutoConfigurationImportSelector.class:



针对SpringApplication.run(MainApplication.class, args)进行debug:
在SpringApplication下的refreshContext打上断点:

一直debug到AutoConfigurationImportSelector的getAutoConfigurationEntry方法,执行筛选后,仅剩24个自动配置类,其中就包含jackson的自动配置类JacksonAutoConfiguration:

由此根据源码JacksonAutoConfiguration的配置,可做如下自定义的配置:
jackson的ObjectMapper全局配置类如下,其中ObjectMapper配置非单例,每次Spring的上下文获取该objectMapper bean时,均是重新生成的实例对象:
package com.xiaoxu.test.springBoot.configure;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Scope;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import java.text.SimpleDateFormat;
/**
 * @author xiaoxu
 * @date 2022-12-30
 * java_demo:com.xiaoxu.test.springBoot.configure.JacksonGlobalConfig
 */
@Configuration
public class JacksonGlobalConfig {
    @Bean(name = "jackson2ObjectMapperBuilder")
    /* 观察SpringBoot-2.5.4
    * jackson的自动配置类:JacksonAutoConfiguration 源码
    *  */
    @SuppressWarnings(value = "   deprecation ")
    public Jackson2ObjectMapperBuilder xiaoxuJackson2ObjectMapperBuilder(){
        System.out.println("1.jackson builder注入:");
        Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
        builder.serializationInclusion(JsonInclude.Include.NON_NULL);
        builder.propertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
        builder.featuresToEnable(SerializationFeature.WRAP_ROOT_VALUE);
        /* 针对new Date  SimpleDateFormat线程不安全, 建议使用自定义的日期工具类,
        * 此处只做简单演示配置*/
        /* 注意: 使用 yyyy-MM-dd HH:mm:ss.fff 将报错 */
        /* 使用 dateFormat, new Date 和 LocalDateTime 均会产生对应效果*/
        /* new Date 不设置的情况下,默认返回时间戳 */
        builder.dateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
        return builder;
    }
    @Bean(name = "jacksonObjectMapper")
    @Primary
    @ConditionalOnBean(name = {"jackson2ObjectMapperBuilder"})
    /* Jackson的ObjectMapper 是线程安全的, 不过SpringBoot2.5.4源码上使用的是非单例模式,这里和源码保持一致 */
    @Scope("prototype")
//    @Scope("singleton")
    public ObjectMapper xiaoxuJacksonObjectMapper(@Qualifier("jackson2ObjectMapperBuilder") Jackson2ObjectMapperBuilder builder){
        System.out.println("2.jackson objectMapper注入:");
        return builder.createXmlMapper(false).build();
    }
}
 
因为jackson默认序列化json数据是会展示为null的数据,所以上述修改后的全局配置,去掉了为null数据的展示,以及将实体类的小驼峰Field样式改为了SNAKE_CASE下划线样式,并且增加了实体类的根key(这里为class name)的展示,还有日期的格式化展示,其余配置可参考SpringBoot源码配置。
如下是配置的bean的先后顺序:

测试使用的实体类JackSonDemo定义如下(暂时不使用自定义jackson注解):
package com.xiaoxu.test.jackson;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import lombok.Data;
import java.lang.annotation.*;
import java.time.LocalDateTime;
import java.util.Date;
/**
 * @author xiaoxu
 * @date 2022-12-30
 * java_demo:com.xiaoxu.test.jackson.JackSonDemo
 */
//@Wrap
@Data
public class JackSonDemo {
    long uid;
    String name;
    Date now;
    LocalDateTime localDay;
}
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@JacksonAnnotationsInside
@JsonSerialize(using = JackSonDemoSerializer.class)
@interface Wrap{}
 
JackSonDemoSub 实体类如下:
@Data
public class JackSonDemoSub {
    String name;
    JackSonDemo jackSonDemo;
}
 
controller:
package com.xiaoxu.test.springBoot.controller;
import com.xiaoxu.test.jackson.JackSonDemo;
import com.xiaoxu.test.jackson.JackSonDemoSub;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.util.Date;
/**
 * @author xiaoxu
 * @date 2022-12-30
 * java_demo:com.xiaoxu.test.springBoot.controller.TestJackSonController
 */
@RequestMapping(value = "/jackson")
@RestController
public class TestJackSonController {
    @RequestMapping(value = "/demo1")
    public JackSonDemo jackSonOne(@RequestParam(required = false) String name, @RequestParam(required = false) Integer uid){
        JackSonDemo jackSonDemo = new JackSonDemo();
        if(uid != null){
            jackSonDemo.setUid(uid);
        }else{
            jackSonDemo.setUid(100);
        }
        if(name != null){
            jackSonDemo.setName(name);
        }
        jackSonDemo.setNow(new Date());
        jackSonDemo.setLocalDay(LocalDateTime.now());
        return jackSonDemo;
    }
    @RequestMapping(value = "/demo2")
    public JackSonDemoSub jackSonDemoSub(@RequestParam(required = false) String name){
        JackSonDemo jackSonDemo = new JackSonDemo();
        jackSonDemo.setUid(100);
        if(name != null){
            jackSonDemo.setName(name);
        }
        jackSonDemo.setNow(new Date());
        jackSonDemo.setLocalDay(LocalDateTime.now());
        JackSonDemoSub jackSonDemoSub = new JackSonDemoSub();
        jackSonDemoSub.setJackSonDemo(jackSonDemo);
        jackSonDemoSub.setName("nihao");
        return jackSonDemoSub;
    }
}
 
postman请求测试:

 可见name为null时,并未展示name字段,可知使用的为自定义配置的jackson objectMapper.
增加name字段请求:

 请求demo2接口,效果如下:

2.2 自定义Jackson ObjectMapper配置类,可注册自定义jackson的序列化、反序列化器:
自定义序列化器:
package com.xiaoxu.test.jackson;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.time.format.DateTimeFormatter;
import java.util.Optional;
/**
 * @author xiaoxu
 * @date 2022-12-30
 * java_demo:com.xiaoxu.test.jackson.JackSonDemoSerializer
 */
public class JackSonDemoSerializer extends JsonSerializer<JackSonDemo> implements ContextualSerializer {
        @Override
        public void serialize(JackSonDemo jackSonDemo, JsonGenerator jsonGenerator,
                              SerializerProvider serializerProvider) throws IOException {
            jsonGenerator.writeStartObject();
            jsonGenerator.writeStringField("RealName",jackSonDemo.getName());
            jsonGenerator.writeNumberField("userId",jackSonDemo.uid + 1);
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            Optional.ofNullable(jackSonDemo.getNow()).ifPresent(date -> {
                try {
                    String day = sdf.format(jackSonDemo.getNow());
                    jsonGenerator.writeStringField("nowDay",day);
                } catch (IOException e) {
                    throw new RuntimeException("now转换异常:"+e.getMessage(),e.getCause());
                }
            });
            Optional.ofNullable(jackSonDemo.getLocalDay()).ifPresent(localDate -> {
                try {
                    jsonGenerator.writeStringField("localDay",
                            jackSonDemo.getLocalDay().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")));
                } catch (IOException e) {
                    throw new RuntimeException("todayNew转换异常:"+e.getMessage(),e.getCause());
                }
            });
            jsonGenerator.writeStringField("extra", "over");
            jsonGenerator.writeEndObject();
        }
        @Override
        public JsonSerializer<JackSonDemo> createContextual(SerializerProvider serializerProvider,
                                                            BeanProperty beanProperty) throws JsonMappingException {
            return this;
        }
}
 
全局配置中,注册自定义序列化器:
@Configuration
public class JacksonGlobalConfig {
    @Bean(name = "jackson2ObjectMapperBuilder")
    /* 观察SpringBoot-2.5.4
    * jackson的自动配置类:JacksonAutoConfiguration 源码
    *  */
    @SuppressWarnings(value = "   deprecation ")
    public Jackson2ObjectMapperBuilder xiaoxuJackson2ObjectMapperBuilder(){
        System.out.println("1.jackson builder注入:");
        Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
        builder.serializationInclusion(JsonInclude.Include.NON_NULL);
        builder.propertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
        builder.featuresToEnable(SerializationFeature.WRAP_ROOT_VALUE);
        /* 针对new Date  SimpleDateFormat线程不安全, 建议使用自定义的日期工具类,
        * 此处只做简单演示配置*/
        /* 注意: 使用 yyyy-MM-dd HH:mm:ss.fff 将报错 */
        /* 使用 dateFormat, new Date 和 LocalDateTime 均会产生对应效果*/
        /* new Date 不设置的情况下,默认返回时间戳 */
        builder.dateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
        /* 全局注册自定义序列化器 */
        builder.serializerByType(JackSonDemo.class, new JackSonDemoSerializer());
        return builder;
    }
    @Bean(name = "jacksonObjectMapper")
    @Primary
    @ConditionalOnBean(name = {"jackson2ObjectMapperBuilder"})
    /* Jackson的ObjectMapper 是线程安全的, 不过SpringBoot2.5.4源码上使用的是非单例模式,这里和源码保持一致 */
    @Scope("prototype")
//    @Scope("singleton")
    public ObjectMapper xiaoxuJacksonObjectMapper(@Qualifier("jackson2ObjectMapperBuilder") Jackson2ObjectMapperBuilder builder){
        System.out.println("2.jackson objectMapper注入:");
        return builder.createXmlMapper(false).build();
    }
}
 
postman再次执行demo1请求,可见现在序列化时,使用的是自定义的JackSonDemoSerializer序列化器:

demo1接口增加name和uid再次请求:

同理,请求demo2效果如下:


或者直接在实体类上,使用自定义的jackson注解,此时可不需配置Jackson ObjectMapper全局序列化器:

实体类上使用自定义的jackson注解@Wrap,再次执行,效果和全局配置一致:

![[数据结构基础]栈和队列的结构及接口函数](https://img-blog.csdnimg.cn/1891b6b26f8d4cbc850b93c7d6c6a697.png)


















