Skip to content

JSON 工具

JSONUtils 是基于 Jackson 封装的 JSON 序列化和反序列化工具类,提供了简化的 API 和预配置的时间格式化。

类信息

  • 包名: com.molandev.core.web.json
  • 类名: JSONUtils
  • 类型: 静态工具类
  • 依赖: Jackson (com.fasterxml.jackson.*)

核心特性

✅ 预配置的时间格式

  • 时区: GMT+8(中国标准时间)
  • LocalDateTime: yyyy-MM-dd HH:mm:ss
  • LocalDate: yyyy-MM-dd
  • LocalTime: HH:mm:ss
  • Date: yyyy-MM-dd HH:mm:ss

✅ 宽松的反序列化

  • 忽略未知属性(FAIL_ON_UNKNOWN_PROPERTIES = false
  • 自动处理多态类型(支持继承关系)

✅ 泛型支持

  • 支持 List<T>Map<K,V> 等复杂泛型
  • 类型安全的反序列化

✅ 异常封装

  • JsonProcessingException 封装为 RuntimeException
  • 无需手动处理检查型异常

核心方法

1. toJsonString

将对象序列化为 JSON 字符串。

java
public static String toJsonString(Object object)
参数类型说明
objectObject要序列化的对象
返回值StringJSON 字符串

示例

java
User user = new User();
user.setId(1L);
user.setName("张三");
user.setCreateTime(LocalDateTime.now());

String json = JSONUtils.toJsonString(user);
// {"id":1,"name":"张三","createTime":"2024-01-18 14:30:00"}

2. toObject(String, Class)

将 JSON 字符串反序列化为指定类型的对象。

java
public static <T> T toObject(String str, Class<T> clazz)
参数类型说明
strStringJSON 字符串
clazzClass<T>目标类型
返回值T反序列化后的对象

示例

java
String json = "{\"id\":1,\"name\":\"张三\",\"createTime\":\"2024-01-18 14:30:00\"}";
User user = JSONUtils.toObject(json, User.class);

3. toObject(String, Type)

将 JSON 字符串反序列化为指定泛型类型的对象。

java
public static <T> T toObject(String str, Type type)
参数类型说明
strStringJSON 字符串
typeType目标泛型类型
返回值T反序列化后的对象

示例

java
String json = "{\"code\":200,\"data\":{\"name\":\"张三\"}}";
Type type = new TypeToken<Response<User>>(){}.getType();
Response<User> response = JSONUtils.toObject(json, type);

4. toList

将 JSON 数组字符串反序列化为 List。

java
public static <T> List<T> toList(String str, Type type)
参数类型说明
strStringJSON 数组字符串
typeTypeList 中元素的类型
返回值List<T>反序列化后的 List

示例

java
String json = "[{\"id\":1,\"name\":\"张三\"},{\"id\":2,\"name\":\"李四\"}]";
List<User> users = JSONUtils.toList(json, User.class);

5. toMap

将 JSON 字符串转换为 Map。

java
public static Map<String, Object> toMap(String str)
参数类型说明
strStringJSON 字符串
返回值Map<String, Object>转换后的 Map

示例

java
String json = "{\"name\":\"张三\",\"age\":25,\"active\":true}";
Map<String, Object> map = JSONUtils.toMap(json);
// {name=张三, age=25, active=true}

完整示例

示例 1: REST API 响应处理

java
import com.molandev.core.web.json.JSONUtils;
import lombok.Data;

@RestController
@RequestMapping("/api")
public class UserController {

    @Autowired
    private UserService userService;

    // 返回 JSON 格式的用户信息
    @GetMapping("/user/{id}")
    public String getUser(@PathVariable Long id) {
        User user = userService.getById(id);
        return JSONUtils.toJsonString(user);
    }

    // 接收 JSON 格式的用户信息
    @PostMapping("/user")
    public User createUser(@RequestBody String json) {
        User user = JSONUtils.toObject(json, User.class);
        userService.save(user);
        return user;
    }

    // 批量处理
    @PostMapping("/users/batch")
    public List<User> batchCreate(@RequestBody String json) {
        List<User> users = JSONUtils.toList(json, User.class);
        userService.saveBatch(users);
        return users;
    }
}

@Data
class User {
    private Long id;
    private String name;
    private String email;
    private LocalDateTime createTime;
}

示例 2: Redis 缓存序列化

java
import com.molandev.core.web.json.JSONUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

@Service
public class CacheService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    // 缓存对象
    public void cacheUser(User user) {
        String key = "user:" + user.getId();
        String json = JSONUtils.toJsonString(user);
        redisTemplate.opsForValue().set(key, json, 30, TimeUnit.MINUTES);
    }

    // 读取缓存
    public User getUser(Long id) {
        String key = "user:" + id;
        String json = redisTemplate.opsForValue().get(key);
        if (json != null) {
            return JSONUtils.toObject(json, User.class);
        }
        return null;
    }

    // 缓存列表
    public void cacheUserList(List<User> users) {
        String json = JSONUtils.toJsonString(users);
        redisTemplate.opsForValue().set("users:all", json);
    }

    // 读取列表缓存
    public List<User> getUserList() {
        String json = redisTemplate.opsForValue().get("users:all");
        if (json != null) {
            return JSONUtils.toList(json, User.class);
        }
        return Collections.emptyList();
    }
}

示例 3: 配置文件解析

java
import com.molandev.core.web.json.JSONUtils;

import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Map;

@Component
public class ConfigLoader {

    // 加载 JSON 配置文件
    public AppConfig loadConfig(String configPath) throws IOException {
        String json = Files.readString(Paths.get(configPath));
        return JSONUtils.toObject(json, AppConfig.class);
    }

    // 加载动态配置(Map 形式)
    public Map<String, Object> loadDynamicConfig(String configPath) throws IOException {
        String json = Files.readString(Paths.get(configPath));
        return JSONUtils.toMap(json);
    }

    // 保存配置
    public void saveConfig(AppConfig config, String configPath) throws IOException {
        String json = JSONUtils.toJsonString(config);
        Files.writeString(Paths.get(configPath), json);
    }
}

@Data
class AppConfig {
    private String appName;
    private int port;
    private DatabaseConfig database;
    private List<String> allowedOrigins;
}

示例 4: MQ 消息序列化

java
import com.molandev.core.web.json.JSONUtils;

@Service
public class MessageProducer {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    // 发送对象消息
    public void sendOrderMessage(Order order) {
        String json = JSONUtils.toJsonString(order);
        rabbitTemplate.convertAndSend("order.exchange", "order.created", json);
    }
}

@Service
public class MessageConsumer {

    // 接收并解析消息
    @RabbitListener(queues = "order.queue")
    public void handleOrderMessage(String json) {
        Order order = JSONUtils.toObject(json, Order.class);
        System.out.println("收到订单: " + order.getOrderNo());
        // 处理订单逻辑
    }
}

高级用法

自定义 JsonMapper

如果需要自定义 Jackson 配置,可以设置自己的 JsonMapper:

java
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.molandev.core.web.json.JSONUtils;

@Configuration
public class JacksonConfig {

    @PostConstruct
    public void customizeJsonMapper() {
        JsonMapper customMapper = JsonMapper.builder()
                .defaultDateFormat(new SimpleDateFormat("yyyy/MM/dd"))
                .configure(SerializationFeature.INDENT_OUTPUT, true)  // 美化输出
                .build();

        JSONUtils.setJsonMapper(customMapper);
    }
}

获取底层 JsonMapper

java
JsonMapper mapper = JSONUtils.getJsonMapper();
// 使用 mapper 进行更复杂的操作

技术细节

默认配置

java
JsonMapper.builder()
    .addModule(new JsonJavaTimeModule())              // Java 8 时间支持
    .defaultTimeZone(TimeZone.getTimeZone("GMT+8"))   // 东八区
    .defaultLocale(Locale.CHINA)                      // 中国地区
    .defaultDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"))
    .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
    .build();

时间格式

类型格式示例
LocalDateTimeyyyy-MM-dd HH:mm:ss2024-01-18 14:30:00
LocalDateyyyy-MM-dd2024-01-18
LocalTimeHH:mm:ss14:30:00
Dateyyyy-MM-dd HH:mm:ss2024-01-18 14:30:00

注意事项

⚠️ 异常处理

JSONUtils 将 JsonProcessingException 封装为 RuntimeException,如果需要捕获异常:

java
try {
    User user = JSONUtils.toObject(json, User.class);
} catch (RuntimeException e) {
    // JSON 解析失败
    log.error("JSON 解析失败", e);
}

⚠️ 循环引用

避免对象之间存在循环引用,否则会导致序列化失败或栈溢出。

java
// 错误示例
class Parent {
    private Child child;
}
class Child {
    private Parent parent;  // 循环引用
}

⚠️ 性能考虑

  • JSONUtils 内部使用单例 JsonMapper,线程安全且高性能
  • 大对象序列化/反序列化会消耗较多内存和 CPU
  • 建议对频繁使用的 JSON 进行缓存

⚠️ null 值处理

默认情况下,null 值会被序列化:

java
User user = new User();
user.setName("张三");
// {"name":"张三","age":null,"email":null}

常见问题

Q1: 如何处理未知字段?

A: JSONUtils 默认忽略未知字段,不会抛出异常。

java
// JSON 中有 extra 字段,但 User 类没有
String json = "{\"name\":\"张三\",\"extra\":\"value\"}";
User user = JSONUtils.toObject(json, User.class);  // 正常解析,忽略 extra

Q2: 如何处理枚举?

A: Jackson 默认使用枚举的 name() 进行序列化:

java
enum Status { ACTIVE, INACTIVE }

User user = new User();
user.setStatus(Status.ACTIVE);
String json = JSONUtils.toJsonString(user);
// {"status":"ACTIVE"}

Q3: 如何序列化泛型?

A: 使用 toObject(String, Type) 方法:

java
Type type = new TypeToken<Response<List<User>>>(){}.getType();
Response<List<User>> response = JSONUtils.toObject(json, type);

Q4: 时间格式不匹配怎么办?

A: 可以自定义 JsonMapper 或在实体类字段上使用 @JsonFormat 注解:

java
@Data
class Event {
    @JsonFormat(pattern = "yyyy/MM/dd HH:mm:ss")
    private LocalDateTime eventTime;
}

相关工具

参考资料


JSON 自动配置(JsonAutoConfiguration)

JsonAutoConfiguration(包 com.molandev.core.web.json)在 Servlet Web 下始终启用,统一 Spring MVC / Feign 的 JSON 与日期格式,与上方 JSONUtils 共用同一套 JsonMapper。

功能说明

JSON 自动配置会自动配置 Spring MVC 的 JSON 转换器,统一使用 JSONUtils 的配置,确保整个应用的 JSON 序列化行为一致。

核心功能

✅ 统一 JSON 转换器

  • 替换 Spring 默认的 MappingJackson2HttpMessageConverter
  • 使用 JSONUtils 的 JsonMapper 配置
  • 确保 Controller 返回值和 @RequestBody 参数的 JSON 格式一致

✅ 时间格式统一

  • 请求参数时间: 支持 GET 请求参数中的时间类型自动转换
  • 响应时间格式: 统一使用 yyyy-MM-dd HH:mm:ss 格式
  • 时区: 统一使用 GMT+8(东八区)

✅ 字符编码

  • 自动配置 UTF-8 字符编码过滤器
  • 解决 Form 表单提交中文乱码问题
  • 最高优先级(HIGHEST_PRECEDENCE

配置说明

Servlet Web 应用下 始终启用,无需额外开关。自动统一 MVC / Feign 的 JSON 与日期格式。

使用示例

示例 1: 统一响应格式

所有 Controller 的返回值都会使用统一的 JSON 格式。

java
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;

@RestController
@RequestMapping("/api")
public class UserController {
    
    @GetMapping("/user/{id}")
    public User getUser(@PathVariable Long id) {
        User user = new User();
        user.setId(id);
        user.setName("张三");
        user.setCreateTime(LocalDateTime.now());
        
        // 返回的 JSON 格式:
        // {
        //   "id": 1,
        //   "name": "张三",
        //   "createTime": "2024-01-18 14:30:00"
        // }
        return user;
    }
}

示例 2: 请求参数时间转换

GET 请求参数中的时间字符串会自动转换为对应的时间类型。

java
@RestController
@RequestMapping("/api")
public class ReportController {
    
    // 支持 GET 请求参数中的时间类型
    @GetMapping("/report")
    public List<Order> getReport(
        @RequestParam LocalDate startDate,      // yyyy-MM-dd
        @RequestParam LocalDate endDate,        // yyyy-MM-dd
        @RequestParam LocalDateTime createTime  // yyyy-MM-dd HH:mm:ss
    ) {
        // 直接使用转换后的时间类型
        return orderService.queryByDateRange(startDate, endDate, createTime);
    }
    
    // 请求示例:
    // GET /api/report?startDate=2024-01-01&endDate=2024-01-31&createTime=2024-01-18 14:30:00
}

示例 3: 请求体 JSON 解析

POST 请求的 @RequestBody 也会使用统一的 JSON 配置。

java
@RestController
@RequestMapping("/api")
public class OrderController {
    
    @PostMapping("/order")
    public Result createOrder(@RequestBody Order order) {
        // order.getCreateTime() 会自动解析为 LocalDateTime
        // 支持格式: "2024-01-18 14:30:00"
        
        orderService.save(order);
        return Result.success();
    }
}

@Data
class Order {
    private Long id;
    private String orderNo;
    private LocalDateTime createTime;  // 自动解析时间字符串
    private LocalDate deliveryDate;    // 自动解析日期字符串
}

示例 4: 列表数据响应

java
@RestController
@RequestMapping("/api")
public class ProductController {
    
    @GetMapping("/products")
    public List<Product> getProducts() {
        // 返回的列表中所有时间字段都会格式化
        return productService.listAll();
    }
    
    // 响应 JSON 格式:
    // [
    //   {
    //     "id": 1,
    //     "name": "商品A",
    //     "createTime": "2024-01-18 14:30:00",
    //     "updateTime": "2024-01-18 15:00:00"
    //   },
    //   ...
    // ]
}

技术细节

HTTP 消息转换器配置

java
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    // 移除默认的 Jackson 转换器
    converters.removeIf(c -> c instanceof MappingJackson2HttpMessageConverter);
    
    // 添加使用 JSONUtils 配置的转换器
    MappingJackson2HttpMessageConverter converter = 
        new MappingJackson2HttpMessageConverter(JSONUtils.getJsonMapper());
    converter.setDefaultCharset(StandardCharsets.UTF_8);
    
    converters.add(converter);
}

时间参数格式化

java
@Override
public void addFormatters(FormatterRegistry registry) {
    DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
    
    // 配置时间格式
    registrar.setTimeFormatter(DateTimeFormatter.ofPattern("HH:mm:ss"));
    registrar.setDateFormatter(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
    registrar.setDateTimeFormatter(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
    
    registrar.registerFormatters(registry);
}

字符编码过滤器

java
@Bean
public OrderedCharacterEncodingFilter characterEncodingFilter() {
    OrderedCharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
    filter.setEncoding("UTF-8");
    filter.setForceEncoding(true);
    filter.setOrder(Ordered.HIGHEST_PRECEDENCE);
    return filter;
}

支持的时间格式

时间类型格式示例说明
LocalDateTimeyyyy-MM-dd HH:mm:ss2024-01-18 14:30:00日期时间
LocalDateyyyy-MM-dd2024-01-18日期
LocalTimeHH:mm:ss14:30:00时间
Dateyyyy-MM-dd HH:mm:ss2024-01-18 14:30:00旧版日期

注意事项

⚠️ 配置优先级

  • JSON 自动配置在 JacksonAutoConfiguration 之前执行(@AutoConfigureBefore
  • 会替换 Spring Boot 默认的 Jackson 配置
  • 如果你有自定义的 Jackson 配置,可能会被覆盖

⚠️ 始终启用

Servlet Web 下 JSON 自动配置始终生效,无需 enabled 开关。

⚠️ 字符编码

  • 自动配置的字符编码过滤器优先级最高
  • 确保请求和响应都使用 UTF-8 编码
  • 可以解决 Form 表单提交中文乱码问题

⚠️ 依赖

  • 需要 spring-boot-starter-web(Servlet Web)
  • 与项目内其他 Jackson 定制可能叠加,注意 @JsonFormat / 自定义 ObjectMapper

常见问题

Q1: 启用 JSON 配置后,时间格式还是不对?

A: 检查以下几点:

  1. 确认应用为 Servlet Web(非纯 WebFlux)且 JsonAutoConfiguration 已加载
  2. 查看是否有其他 Jackson 配置覆盖了设置
  3. 检查实体类字段上是否有 @JsonFormat 注解(会覆盖全局配置)

Q2: 如何在启用配置的同时使用自定义格式?

A: 可以在特定字段上使用 @JsonFormat 注解:

java
@Data
class Event {
    @JsonFormat(pattern = "yyyy/MM/dd HH:mm:ss")
    private LocalDateTime eventTime;  // 使用自定义格式
    
    private LocalDateTime createTime;  // 使用全局格式 yyyy-MM-dd HH:mm:ss
}

Q3: GET 请求的时间参数格式不对怎么办?

A: 确保请求参数使用正确的格式:

  • LocalDateTime: 2024-01-18 14:30:00(注意空格)
  • LocalDate: 2024-01-18
  • LocalTime: 14:30:00

如果需要支持其他格式,可以使用 @DateTimeFormat 注解:

java
@GetMapping("/report")
public Result getReport(
    @RequestParam 
    @DateTimeFormat(pattern = "yyyy/MM/dd")
    LocalDate date
) {
    // 支持 2024/01/18 格式
}

Q4: 是否会影响现有项目?

A: 会。Servlet Web 下 JSON 配置始终加载,与项目其它 Jackson 定制可能叠加,请注意 @JsonFormat 与自定义 ObjectMapper Bean。

Q5: 与 JSONUtils 的关系?

A:

  • JSONUtils 是静态工具类,可以独立使用
  • JSON 自动配置会将 JSONUtils 的配置应用到 Spring MVC
  • 两者使用相同的 JsonMapper 配置,确保行为一致

最佳实践

1. 统一时间格式

在整个项目中统一使用 yyyy-MM-dd HH:mm:ss 格式,避免前后端时间格式不一致的问题。

2. 禁用其他 Jackson 配置

建议避免再自定义全局 Jackson / ObjectMapper,以免与统一配置冲突。

3. 前端适配

前端在解析时间字符串时,需要按照 yyyy-MM-dd HH:mm:ss 格式处理:

javascript
// JavaScript 示例
const dateString = "2024-01-18 14:30:00";
const date = new Date(dateString.replace(' ', 'T'));  // 转换为 ISO 格式

4. 测试验证

建议编写测试用例验证 JSON 序列化行为:

java
@SpringBootTest
@AutoConfigureMockMvc
class JsonConfigTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @Test
    void testDateTimeFormat() throws Exception {
        mockMvc.perform(get("/api/user/1"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.createTime")
                .value(matchesPattern("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}")));
    }
}

参考资料