Spring MVC 数据转换和格式化

Spring MVC 数据转换和格式化

Spring MVC通过注解便可以让控制器得到丰富的参数类型,那么它是如何做到的呢,其实它是Spring MVC的消息转换机制实现的。

核心转换流程

  1. 请求到达:HTTP 请求首先抵达 DispatcherServlet,通过 HandlerMapping 找到对应的 HandlerAdapter
  2. 消息预处理HandlerAdapter 调用 HttpMessageConverter 对 HTTP 消息进行初步转换(仅支持 String 类型、文件类型等简单转换)。
  3. 参数转换增强:为支持 POJO 等复杂类型转换,Spring 4 提供了转换器(Converter)格式化器(Formatter),通过注解信息与参数类型,完成 HTTP 消息到控制器所需参数的最终转换。
  4. 数据校验:参数转换完成后,Spring MVC 会对参数进行数据校验。
  5. 调用控制器:将转换后的参数传入控制器,执行业务逻辑并返回结果。
  6. 响应转换(可选):若存在匹配返回结果类型的 HttpMessageConverter 实现类,会将返回结果转换为 HTTP 响应消息。
  7. 视图解析:后续进入视图解析器流程,渲染最终视图。

注:当需要处理特殊格式数据(如第三方非标准 JSON)时,可自定义转换规则,简化开发。

转换核心组件

当配置 <mvc:annotation-driven>(XML 方式)或 @EnableWebMvc(Java 配置方式)时,Spring IoC 容器会自动创建 FormattingConversionServiceFactoryBean 实例,该工厂可生成 DefaultFormattingConversionService 对象(实现了 ConverterRegistryFormatterRegistry 接口),用于注册转换器和格式化器。Spring MVC 已默认注册常用转换器和处理器。

一对一转换器(Converter)

接口定义

Converter 是 Spring MVC 提供的一对一类型转换器接口,源码如下:

import org.springframework.lang.Nullable;

/**
 * 转换接口
 * @param <S> 源类型
 * @param <T> 目标类型
 */
@FunctionalInterface
public interface Converter<S, T> {
    @Nullable
    T convert(S source);
}

自定义 Converter 示例

场景说明

JSP 页面通过复选框提交权限列表(String[] 类型),需转换为 Set<Privilege> 类型并封装到 Role POJO 中。

页面代码(JSP)

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>添加角色</title>
</head>
<body>
    <form action="${pageContext.request.contextPath }/control/role/save" method="post">
        <label>角色名称:</label><input type="text" name="name" required="required"/><br/>
        <input type="checkbox" name="privileges" value="departmentsave"/>部门添加
        <input type="checkbox" name="privileges" value="departmentremove"/>部门删除
        <input type="checkbox" name="privileges" value="departmentedit"/>部门编辑
        <input type="checkbox" name="privileges" value="departmentlist"/>部门查看
        <br/>
        <input type="checkbox" name="privileges" value="rolesave"/>角色添加
        <input type="checkbox" name="privileges" value="roleremove"/>角色删除
        <input type="checkbox" name="privileges" value="roleedit"/>角色编辑
        <input type="checkbox" name="privileges" value="rolelist"/>角色查看
        <br/>
        <input type="submit"/>
    </form>
</body>
</html>

POJO 类

// Role 类
public class Role {
    private Integer id;
    private String name;
    // 角色所拥有的权限(目标类型)
    private Set<Privilege> privileges = new HashSet<Privilege>();
    // getter 和 setter 方法
}
// Privilege 类
public class Privilege {
    private Integer id;
    private String name;
    
    public Privilege() {}
    
    public Privilege(String name) {
        this.name = name;
    }
    // getter 和 setter 方法
}

控制器(Controller)

@Controller
@RequestMapping("control/role")
public class RoleManageController {
    @RequestMapping("/save")
    public String saveRole(Role role) {
        // 打印转换后的权限列表
        role.getPrivileges().forEach(pri -> System.out.println(pri.getName()));
        return "role_list";
    }
    
    @RequestMapping("/saveUI")
    public String saveUI() {
        return "role_add";
    }
}

自定义转换器实现

public class StringArray2SetRoleConverter implements Converter<String[], Set<Privilege>> {
    @Override
    public Set<Privilege> convert(String[] source) {
        if (source == null) return null;
        Set<Privilege> ret = new HashSet<Privilege>();
        for (String privilegeName : source) {
            ret.add(new Privilege(privilegeName));
        }
        return ret;
    }
}

注册转换器

方式 1:XML 配置
<!-- 启用注解驱动,并指定转换服务 -->
<mvc:annotation-driven conversion-service="conversionService"/>

<!-- 配置转换服务工厂,注册自定义转换器 -->
<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
    <property name="converters">
        <set>
            <bean class="com.wise.tiger.web.converter.StringArray2SetRoleConverter"/>
        </set>
    </property>
</bean>
方式 2:Java 配置
@Configuration
@ComponentScan(basePackages = "com.controller")
@EnableWebMvc
public class ServletConfig implements WebMvcConfigurer {

    /**
     * 配置一对一转换器
     * @param registry 格式化器注册机
     */
    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new StringArray2SetRoleConverter());
    }
}

前后端分离(JSON 数据)场景扩展

场景说明

前端传递 JSON 数据中的 job 字段(字符串类型),需转换为自定义枚举 Position 类型。

枚举类(Enum)

package com.enums;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor // 全参构造函数
public enum Position {
    CLERK(2000, "业务员"),
    SALESMAN(2000, "销售员"),
    ANALYST(5000, "分析员"),
    SEARCHER(3000, "搜索员"),
    MANAGER(6000, "经理");

    private float lowSalary;
    private String value;

    // 根据字符串匹配枚举
    public static Position position(String job) {
        for (Position value : values()) {
            if (value.name().equals(job)) {
                return value;
            }
        }
        return null;
    }
}

JSON 反序列化转换器

package com.converter;

import com.enums.Position;
import com.fasterxml.jackson.core.JacksonException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;

import java.io.IOException;

public class PositionJsonDeserializer extends JsonDeserializer<Position> {
    @Override
    public Position deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JacksonException {
        String job = jsonParser.getText();
        return Position.position(job);
    }
}

POJO 类(关联转换器)

package com.pojo;

import com.converter.PositionJsonDeserializer;
import com.enums.Position;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import lombok.Data;

import java.time.LocalDate;

@Data
public class Employee {
    private Integer id;
    private String username;
    private String password;
    private String email;
    private String phone;
    private LocalDate birthday;
    private LocalDate hireDate;
    private int salary;

    // 指定 JSON 反序列化转换器
    @JsonDeserialize(using = PositionJsonDeserializer.class)
    private Position job = Position.CLERK;

    private String intro;
}

普通 String 转枚举转换器(非 JSON 场景)

package com.converter;

import com.enums.Position;
import org.springframework.core.convert.converter.Converter;

public class String2Position implements Converter<String, Position> {
    @Override
    public Position convert(String job) {
        if (job == null) return null;
        return Position.position(job);
    }
}

注册转换器(Java 配置)

@Override
public void addFormatters(FormatterRegistry registry) {
    registry.addConverter(new String2Position()); // 注册 String 转枚举转换器
}

数组和集合转换器(GenericConverter)

Converter 仅支持一对一转换(单一源类型 → 单一目标类型),无法满足一对多转换场景(如单个字符串 → 集合/数组)。Spring Core 提供 GenericConverter 接口,专门解决复杂类型转换问题(支持多源类型、多目标类型)。

注:GenericConverter 接口更灵活,需根据实际场景实现类型匹配逻辑,此处重点关注基础用法,具体实现可参考 Spring 官方文档。

格式化器(Formatter)

核心作用

用于对日期、金额等数据进行格式化处理(如 2024-05-20LocalDate1000010,000.00),本质上委托 Converter 机制实现。

接口关系

image

常用格式化注解

Spring 提供注解简化格式化配置,无需自定义 Formatter

注解 作用 常用属性
@DateTimeFormat 日期/时间格式化 iso(如 ISO.DATE)、pattern(如 "yyyy-MM-dd")
@NumberFormat 数字格式化 pattern(如 "#,###.##")

使用示例

直接作用于方法参数

@Controller
@RequestMapping("/convert")
public class ConvertController {
    @RequestMapping("/format")
    public String format(
            @DateTimeFormat(iso = ISO.DATE) LocalDate date, // 日期格式化(ISO 标准:yyyy-MM-dd)
            @NumberFormat(pattern = "#,###.##") Double amount // 数字格式化(如 12345.67 → 12,345.67)
    ) {
        // 业务逻辑处理
        return "format_result";
    }
}

作用于 POJO 字段

import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.format.annotation.NumberFormat;

import java.math.BigDecimal;
import java.time.LocalDate;

@Data
public class FormatPojo {
    @DateTimeFormat(iso = ISO.DATE) // 日期格式:yyyy-MM-dd
    private LocalDate date1;

    @NumberFormat(pattern = "##,###.00") // 数字格式:保留两位小数,千分位分隔
    private BigDecimal amount;
}

控制器接收 POJO 参数

@RequestMapping("/format")
public String format(FormatPojo bean) {
    // 直接使用格式化后的 bean 属性
    System.out.println("日期:" + bean.getDate1());
    System.out.println("金额:" + bean.getAmount());
    return "format_result";
}
posted @ 2025-12-29 10:32  Jing61  阅读(1)  评论(0)    收藏  举报