使用枚举接收参数
一、目的:
前端传入一个字符串数据,后端存入数据库时使用数字进行存储。(或者类似的需求【数据库字段与前端字段不一致】)
比如前端传入一个参数的值为spring
而后端存储在数据库中时存储数字1
(经常使用这些的类似性别等)
有很多参数如果能用上枚举将会变的很舒服,后续有业务添加有新的类型出现时只需在枚举类中添加即可
[!important]
最后要达成的目的:
- 直接使用枚举值接收前端传入的值
- 返回时枚举类型自动映射为指定枚举字段的值(不是直接返回枚举类型)
- 能够对传入的值进行枚举校验(校验该值是否在枚举类型中)
二、映射代码实现
1.创建枚举类
在创建枚举类时使用mybatis-plus
的注解**@EnumValue
和jackson
的注解@JsonValue
**
[!caution]
**
@EnumValue
**用于标记存入数据库中的字段**
@JsonValue
**用于标记Json序列化时需要序列化的值(传递给前端的值)
例如:
package com.platform.enumtest.modules.Enums.Enum;
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 季节枚举对象
*
* @author Liu
* @date 2025/05/02
*/
@AllArgsConstructor
@Getter
public enum SeasonEnum {
SPRING(1, "spring", "春天"),
SUMMER(2, "summer", "夏天"),
AUTUMN(3, "autumn", "秋天"),
WINTER(4, "winter", "冬天");
//保存到数据库的值
@EnumValue
private final int code;
//返回给前端的值
@JsonValue
private final String name;
//前端传入的值
private final String desc;
}
2.修改数据库映射对象
将数据库映射的对象对应的值修改为枚举类型
例如之前我是使用:
String season;
来接收这个值的,现在就改为:
SeasonEnum season;
全部示例代码:
package com.platform.enumtest.modules.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.platform.enumtest.modules.Enums.Enum.SeasonEnum;
import lombok.Data;
/**
* 枚举测试表映射对象
*
* @author Liu
* @date 2025/05/02
*/
@Data
@TableName("enum_test")
public class EnumTest {
/*
* 主键ID
* */
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
/*
* 季节
* */
@TableField("season")
private SeasonEnum season;
}
3.接口测试
在接收前端传入的参数时也直接只用枚举类来进行接收:
示例:
控制器代码
package com.platform.enumtest.controller;
import com.platform.enumtest.modules.entity.EnumTest;
import com.platform.enumtest.modules.query.TestAddQuery;
import com.platform.enumtest.service.TestService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 测试控制器
*
* @author Liu
* @date 2025/05/02
*/
@RestController
@RequestMapping("/test")
@RequiredArgsConstructor
public class TestController {
private final TestService testService;
@PostMapping("/add")
public String add(TestAddQuery query) {
EnumTest enumTest = new EnumTest();
enumTest.setSeason(query.getSeasonName());
//插入数据
testService.save(enumTest);
return "成功";
}
}
add接口参数对象TestAddQuery
package com.platform.enumtest.modules.query;
import com.platform.enumtest.modules.Enums.Enum.SeasonEnum;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* 添加接口query参数
*
* @author Liu
* @date 2025/05/02
*/
@Data
public class TestAddQuery {
@NotBlank(message = "季节名称不能为空")
private SeasonEnum seasonName;
}
4.运行测试
此时进行运行测试会发现类型转换错误报错:
2025-05-02T17:27:53.532+08:00 WARN 68348 --- [EnumTest] [nio-8080-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public java.lang.String com.platform.enumtest.controller.TestController.add(com.platform.enumtest.modules.query.TestAddQuery): [Field error in object 'testAddQuery' on field 'seasonName': rejected value [spring]; codes [typeMismatch.testAddQuery.seasonName,typeMismatch.seasonName,typeMismatch.com.platform.enumtest.modules.Enums.Enum.SeasonEnum,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [testAddQuery.seasonName,seasonName]; arguments []; default message [seasonName]]; default message [Failed to convert property value of type 'java.lang.String' to required type 'com.platform.enumtest.modules.Enums.Enum.SeasonEnum' for property 'seasonName'; Failed to convert from type [java.lang.String] to type [@jakarta.validation.constraints.NotBlank com.platform.enumtest.modules.Enums.Enum.SeasonEnum] for value [spring]]] ]
[!tip]
说明:请求参数 seasonName 是一个字符串(如 “spring”),但程序期望的是枚举类型 SeasonEnum。
这里发现单单是依靠mybatis-plus还不能直接使用枚举类型接收参数,但是依靠**@EnumValue
**是可以实现存入数据库时枚举数据向指定字段映射的,感兴趣可以自己尝试(前端传字符串后端接收后存入数据库时存入对应的枚举类型即可)
所以这里我们还需要进行额外的设置
三、query数据映射工厂
这里看见标题是不是有点好奇为什么是query映射工厂?
因为如果前端传入的是body且类型是json的话spring是可以进行自动映射的
例如此时将上面接口改为接收请求体:
package com.platform.enumtest.controller;
import com.platform.enumtest.modules.entity.EnumTest;
import com.platform.enumtest.modules.query.TestAddQuery;
import com.platform.enumtest.service.TestService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 测试控制器
*
* @author Liu
* @date 2025/05/02
*/
@RestController
@RequestMapping("/test")
@RequiredArgsConstructor
public class TestController {
private final TestService testService;
@PostMapping("/add")
public String add(@RequestBody TestAddQuery query) {
EnumTest enumTest = new EnumTest();
enumTest.setSeason(query.getSeasonName());
//插入数据
testService.save(enumTest);
return "成功";
}
}
此时进行传参测试:
发现保存成功了
再查看此时数据库保存的值:
存入的值也正确映射为了指定的值
1.映射工厂创建
方法一:使用spring的类型转换服务
ApplicationConversionService类带有一组已配置的转换器和格式化程序。
在这些开箱即用的转换器中,我们找到了StringToEnumIgnoringCaseConverterFactory。顾名思义,它以不区分大小写的方式将字符串转换为枚举。
[!note]
优点:代码简单
缺点:不能自定义
步骤:
- 写一个配置类
- 实现
WebMvcConfigurer
接口 - 重写方法
addFormatters
- 配置服务
示例:
我创建了一个配置类:WebConfig
代码如下:
package com.platform.enumtest.config;
import org.springframework.boot.convert.ApplicationConversionService;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* web配置
*
* @author Liu
* @date 2025/05/02
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
/**
* 添加格式化程序
*
* @param registry 登记处
*/
@Override
public void addFormatters(FormatterRegistry registry) {
ApplicationConversionService.configure(registry);
}
}
此时重启服务器进行测试:
毫无意外的测试成功
再查看数据库中存储的内容:
也是正常的一一对应的
方法二:自定义处理工厂
1)创建基础枚举接口
这里创建一个接口**BaseEnum
**里面编写一个获取值的方法,让所有枚举类都实现该接口
示例:
BaseEnum
package com.platform.enumtest.modules.Enums;
/**
* 基枚举
*
* @author Liu
* @date 2025/04/29
*/
public interface BaseEnum {
/**
* 枚举值
*
* @return {@link String }
*/
String getValue();
}
修改后的SeasonEnum
枚举类:
package com.platform.enumtest.modules.Enums.Enum;
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonValue;
import com.platform.enumtest.modules.Enums.BaseEnum;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 季节枚举对象
*
* @author Liu
* @date 2025/05/02
*/
@AllArgsConstructor
@Getter
public enum SeasonEnum implements BaseEnum {
SPRING(1, "spring", "春天"),
SUMMER(2, "summer", "夏天"),
AUTUMN(3, "autumn", "秋天"),
WINTER(4, "winter", "冬天");
//保存到数据库的值
@EnumValue
private final int code;
//返回给前端的值
@JsonValue
private final String name;
//前端传入的值
private final String desc;
/**
* 枚举值
*
* @return {@link String }
*/
@Override
public String getValue() {
return name; //前端传入的值
}
}
[!caution]
这里实现接口的类不一定要String类型,可以自定义
返回的值为前端传入的值
如果你希望枚举接收前端的值为number类型这里将其改为Integer或者其他复合的都可以
[!caution]
如果这里修改了类型下面在写工厂的时候也要改为相应的类型
2)创建工厂
这里我创建了一个工厂UniversalEnumConverterFactory
它实现了ConverterFactory
接口
示例:
UniversalEnumConverterFactory
package com.platform.enumtest.factory;
import com.platform.enumtest.modules.Enums.BaseEnum;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;
import java.util.HashMap;
import java.util.Map;
import java.util.WeakHashMap;
/**
* 通用枚举转换工厂类。
* 用于将传入的字符串参数自动转换为对应的枚举类型(BaseEnum 的子类)。
* 主要用于 Spring MVC 参数绑定场景,如从 URL 查询参数或表单中接收到的字符串值转换为对应枚举对象。
*/
public class UniversalEnumConverterFactory implements ConverterFactory<String, BaseEnum> {
/**
* 缓存每个枚举类型的对应的转换器实例。
* 使用 WeakHashMap 是为了当枚举类型不再被引用时,可以自动被垃圾回收,避免内存泄漏。
*/
private static final Map<Class, Converter> converterMap = new WeakHashMap<>();
/**
* 获取指定目标类型的转换器。
*
* @param targetType 枚举类型(BaseEnum 的子类)
* @return 转换器实例
*/
@Override
public <T extends BaseEnum> Converter<String, T> getConverter(Class<T> targetType) {
// 尝试从缓存中获取已有的转换器
Converter result = converterMap.get(targetType);
if (result == null) {
// 如果不存在,则创建一个新的转换器并放入缓存
result = new IntegerStrToEnum<>(targetType);
converterMap.put(targetType, result);
}
return result;
}
/**
* 字符串到枚举的转换器内部类。
* 支持将字符串形式的输入转换为具体的枚举对象。
*
* @param <T> 枚举类型,必须是 BaseEnum 的子类
*/
class IntegerStrToEnum<T extends BaseEnum> implements Converter<String, T> {
private final Class<T> enumType; // 当前处理的枚举类类型
private Map<String, T> enumMap = new HashMap<>(); // 缓存枚举值与枚举对象的映射关系
/**
* 构造函数,初始化枚举类和对应的值-枚举映射表。
*
* @param enumType 枚举类类型
*/
public IntegerStrToEnum(Class<T> enumType) {
this.enumType = enumType;
// 获取该枚举类的所有枚举常量
T[] enums = enumType.getEnumConstants();
// 遍历所有枚举常量,并以 value 字段作为 key 存入 map
for (T e : enums) {
enumMap.put(e.getValue() + "", e); // 将 value 转成字符串用于匹配字符串输入
}
}
/**
* 转换方法,将字符串转换为对应的枚举对象。
*
* @param source 输入字符串
* @return 对应的枚举对象
* @throws IllegalArgumentException 如果没有匹配的枚举值,抛出异常
*/
@Override
public T convert(String source) {
// 根据字符串查找对应的枚举对象
T result = enumMap.get(source);
if (result == null) {
// 如果找不到匹配项,抛出非法参数异常
throw new IllegalArgumentException("无法匹配到枚举值:" + source);
}
return result;
}
}
}
3)注册工厂
这里创建的工厂还没有在spring的bean中,我们使用方法一创建一个配置类进行配置即可
步骤:
- 写一个配置类
- 实现
WebMvcConfigurer
接口 - 重写方法
addFormatters
- 配置服务
示例:
package com.platform.enumtest.config;
import com.platform.enumtest.factory.UniversalEnumConverterFactory;
import org.springframework.boot.convert.ApplicationConversionService;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* web配置
*
* @author Liu
* @date 2025/05/02
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
/**
* 添加格式化程序
*
* @param registry 登记处
*/
@Override
public void addFormatters(FormatterRegistry registry) {
// ApplicationConversionService.configure(registry);
registry.addConverterFactory(new UniversalEnumConverterFactory());
}
}
[!caution]
这里的这个工厂写法我参考其他人写的
此时测试发现结果也是正常的
残留问题:当传入参数不在枚举类型中时
这里我传入了一个不存在的值发现返回的值不是自定义的
这里诞生出几个问题:
- 如何实现统一的错误返回
- 如何自定义错误返回信息
问题解决方法:
思路:当传入的参数在枚举类中没有匹配时会抛出报错,设计一个全局异常处理类来抓取异常,然后实现统一返回
[!tip]
步骤:
- 创建一个全局异常处理器
- 抓取对应报错异常
- 设置统一返回
- 设置统一返回对象:
示例:
package com.platform.enumtest.utils;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 统一响应结果类,用于封装API的响应信息。
*
* @author Liu
* @date 2025/05/02
*/
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Result<T> {
/**
* 业务状态码,0表示成功,1表示失败。
*/
private Integer code;
/**
* 提示信息,用于向客户端传递具体的响应消息。
*/
private String message;
/**
* 响应数据,用于传递具体的业务数据。
*/
private T data;
/**
* 返回操作成功的响应结果(带响应数据)。
*
* @param message 提示信息
* @param data 响应数据
* @param <E> 数据类型
* @return 操作成功的响应结果
*/
public static <E> Result<E> success(String message, E data) {
return new Result<>(0, message, data);
}
/**
* 返回操作成功的响应结果。
*
* @param message 提示信息
* @return 操作成功的响应结果
*/
public static Result success(String message) {
return new Result(0, message, null);
}
/**
* 快速返回操作失败的响应结果。
*
* @param message 提示信息
* @return 操作失败的响应结果
*/
public static Result error(String message) {
return new Result(1, message, null);
}
}
这里写的是简单的方法,更多业务支持的使用其他也可以
- 初始化全局异常处理器
示例:
package com.platform.enumtest.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
/**
* 全局异常配置
*
* @author Liu
* @date 2025/05/02
*/
@ControllerAdvice
@Slf4j
public class GlobalExceptionConfig {
}
query参数未匹配与body参数未匹配抛出的异常不一样所以要分开抓取
1.query参数报错统一
当query的参数没有匹配到枚举时抛出的异常为:
MethodArgumentNotValidException
所以在处理异常时抓取该异常进程处理然后返回统一对象即可:
这里使用的方法比较暴力,但是目前我也只能做到这一个方法
我通过一步步拆解MethodArgumentNotValidException得到的各种参数的获取,然后直接拦截使用
示例:
因为这个异常在做参数校验的时候也会抛出,所以进行分开判断然后返回信息
package com.platform.enumtest.config;
import com.platform.enumtest.utils.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
/**
* 全局异常配置
*
* @author Liu
* @date 2025/05/02
*/
@ControllerAdvice
@Slf4j
public class GlobalExceptionConfig {
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseBody
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<?> methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e){
log.error("捕获参数校验异常:{}", e.getMessage());
// 默认错误信息
String errorMessage = "参数校验失败";
// 若 BindingResult 不为空且存在字段错误,则使用字段的默认错误消息
if (e.getBindingResult() != null && e.getBindingResult().getFieldError() != null) {
errorMessage = e.getBindingResult().getFieldError().getDefaultMessage();
}
// 特殊情况处理:针对枚举类型参数或类型不匹配导致的 typeMismatch 错误
if (e.getBindingResult().getFieldError().getCodes().length > 2 &&
!e.getBindingResult().getFieldError().getCode().isEmpty()) {
// 判断是否为类型不匹配错误
if ("typeMismatch".equals(e.getBindingResult().getFieldError().getCode())) {
errorMessage = e.getBindingResult().getFieldError().getDefaultMessage();
}
}
return Result.error(errorMessage);
}
}
测试:
至于自定义错误信息在后面讲
2.body参数报错统一
当body的参数没有匹配到枚举时抛出的异常为:
HttpMessageNotReadableException
[!important]
HttpMessageNotReadableException
是 Spring Framework 中抛出的一个异常,通常发生在 HTTP 请求的消息体无法被读取或解析的时候。这种异常常见于使用Spring MVC 或 Spring WebFlux 开发RESTful服务时,特别是在处理JSON、XML等格式的数据转换过程中。以下是一些可能导致
HttpMessageNotReadableException
异常的原因及解决方法:
- 请求体为空:客户端发送的请求中没有提供必要的请求体数据。
- 解决方案:确保客户端发送的请求包含了正确的请求体。
- 内容类型不匹配:客户端发送的内容类型(Content-Type)与服务器端期望的内容类型不一致,比如服务器端期望接收JSON格式的数据,但客户端发送的是XML格式的数据。
- 解决方案:检查并确保客户端设置正确的
Content-Type
头信息,并与服务器端期望的格式相匹配。- JSON格式错误:如果客户端发送的数据是JSON格式,但是其结构不符合预期(例如缺少必需的字段、字段类型不正确等),则会导致解析失败。
- 解决方案:验证发送给服务器的数据是否符合预期的JSON结构和字段要求。
- Jackson反序列化问题:当使用Jackson作为JSON处理器时,如果对象映射过程中出现问题(如未知属性、日期格式不兼容等),也可能导致此异常。
- 解决方案:调整对象映射配置,或者在实体类中使用适当的注解来指导Jackson如何正确地进行反序列化。
- 字符编码问题:有时,字符编码的问题也会导致消息无法被正确读取。
- 解决方案:确保客户端和服务器之间使用相同的字符编码,一般推荐使用UTF-8。
示例:
这里将请求体参数异常一起进行书写(不只写枚举参数映射异常)
@ExceptionHandler(HttpMessageNotReadableException.class)
@ResponseBody
@ResponseStatus(HttpStatus.NOT_FOUND)
public Result<?> httpMessageNotReadableException(HttpMessageNotReadableException e){
log.error("捕获Http消息不可读异常:{}", e.getMessage());
Throwable rootCause = e.getRootCause();
// 无效格式异常处理。比如:目标格式为数值,输入为非数字的字符串("80.5%"、"8.5.1"、"张三")。
if (rootCause instanceof InvalidFormatException) {
String userMessage = getHttpMessageNotReadableExceptionMessage((InvalidFormatException) rootCause);
return Result.error(userMessage);
}
String userMessage = "Http消息不可读异常!请稍后重试,或联系业务人员处理。";
return Result.error(userMessage);
}
/**
* 用户提示词获取器
*
* @param rootCause 根本原因
* @return {@link String }
*/
private String getHttpMessageNotReadableExceptionMessage(InvalidFormatException rootCause) {
Class<?> targetType = rootCause.getTargetType();
Object value = rootCause.getValue();
if (targetType.isEnum()) {
// 如果是枚举类型,列出所有允许的枚举值
Enum<?>[] enumConstants = (Enum<?>[]) targetType.getEnumConstants();
StringBuilder validValues = new StringBuilder("[");
for (int i = 0; i < enumConstants.length; i++) {
validValues.append(enumConstants[i].name());
if (i < enumConstants.length - 1) validValues.append(", ");
}
validValues.append("]");
return String.format("无效的枚举值 [%s],字段类型应为:%s,支持的取值为:%s", value, targetType.getSimpleName(), validValues);
}
String targetTypeNotification = "";
if (targetType == BigInteger.class || targetType == Integer.class || targetType == Long.class
|| targetType == Short.class || targetType == Byte.class) {
targetTypeNotification = "参数类型应为:整数;";
} else if (targetType == BigDecimal.class || targetType == Double.class || targetType == Float.class) {
targetTypeNotification = "参数类型应为:数值;";
}
return String.format("参数格式错误!%s当前输入参数:[%s]", targetTypeNotification, value);
}
测试:
新问题:message参数返回不友好
query参数异常返回:
body参数返回:
遵循《阿里巴巴Java开发手册》的规范,避免使用枚举类型作为接口返回值。
这里虽然直接使用枚举类进行返回最后也会被映射为非枚举类的字段,但是这里当错误产生时提示支持的字段是枚举类型的字段
更好的信息返回
这里要达成两点:
- 能够自定义返回的错误信息
- 能够将当前枚举类中可以传入的参数罗列出来返回
[!caution]
这里默认使用的是方法二创建的映射工厂进行
如果是使用的方法一,请参考方法二将
BaseEnum
接口建立,同时让枚举类实现该接口(只用建立接口实现接口,不用创建注册工厂)
[!tip]
思路:
- 建立一个map,让枚举类初始化时将自己注册进入
- 当需要获取这个枚举类时可以通过map来获取
- 键为参数名
- 值为这个枚举类
1.工具类
首先需要建立一个工具类
这个类可以参照这个来写
package com.platform.enumtest.utils;
import com.platform.enumtest.modules.Enums.BaseEnum;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Web枚举注册表工具类(静态化)
*
* @author Liu
* @date 2025/05/02
*/
public class WebEnumsRegistryUtil {
// 使用泛型 Map<String, Enum<?>> 可以存储任意枚举类型
private static final Map<String, Class<? extends BaseEnum>> ENUM_MAP = new ConcurrentHashMap<>();
// 私有构造器,防止外部实例化
private WebEnumsRegistryUtil() {
}
/**
* 获取已注册的枚举
*
* @param key 键
* @return 对应的枚举对象,不存在则返回 null
*/
public static Class<? extends BaseEnum> getEnum(String key) {
return ENUM_MAP.get(key);
}
/**
* 注册枚举
*
* @param key 唯一标识符(如 "MessageType")
* @param value 枚举值
*/
public static void registerEnum(String key, Class<? extends BaseEnum> value) {
ENUM_MAP.put(key, value);
}
/**
* 检查是否包含某个键
*
* @param key 键
* @return boolean
*/
public static boolean containsKey(String key) {
return ENUM_MAP.containsKey(key);
}
/**
* 获取所有注册的枚举键集合
*
* @return {@link Map }<{@link String }, {@link Enum }<{@link ? }>>
*/
public static Map<String, Class<? extends BaseEnum>> getAllEnums() {
return Map.copyOf(ENUM_MAP); // 返回只读副本
}
}
2.修改枚举类
在枚举类中添加一个static块,让枚举类能在初始化的时候将正确的值注册进map
示例:
[!tip]
如果相同的这个类需要接收许多不同名称的参数这里注册多次即可
即:如果有两个参数"test1"、"test2"都需要这个枚举类接受,static块中就这样写:
static { //注册枚举类 WebEnumsRegistryUtil.registerEnum("test1", SeasonEnum.class); WebEnumsRegistryUtil.registerEnum("test2", SeasonEnum.class); }
package com.platform.enumtest.modules.Enums.Enum;
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonValue;
import com.platform.enumtest.modules.Enums.BaseEnum;
import com.platform.enumtest.utils.WebEnumsRegistryUtil;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 季节枚举对象
*
* @author Liu
* @date 2025/05/02
*/
@AllArgsConstructor
@Getter
public enum SeasonEnum implements BaseEnum {
SPRING(1, "spring", "春天"),
SUMMER(2, "summer", "夏天"),
AUTUMN(3, "autumn", "秋天"),
WINTER(4, "winter", "冬天");
static {
//注册枚举类
WebEnumsRegistryUtil.registerEnum("seasonName", SeasonEnum.class);
}
//保存到数据库的值
@EnumValue
private final int code;
//返回给前端的值
@JsonValue
private final String name;
//前端传入的值
private final String desc;
/**
* 枚举值
*
* @return {@link String }
*/
@Override
public String getValue() {
return name; //前端传入的值
}
}
3.修改query的异常捕获
-
捕获错误的参数名
- 因为之前通过键值对的形式将参数名和枚举对象绑定放入了map中存储,所以这里可以通过它直接获取到枚举类
-
通过转换获取到接口BaseEnum的对象集合
-
BaseEnum[] enumConstants = enumClass.getEnumConstants();
-
-
通过循环调用接口中的方法将所有枚举值添加到字符串中(方便返回正确的提示信息)
示例:
/**
* 参数校验异常处理方法。
* <p>该方法用于全局捕获控制器(Controller)中因请求参数校验失败而抛出的
* {@link MethodArgumentNotValidException}
* 异常,并返回统一格式的错误响应。</p>
* <p>主要处理两种类型的参数校验错误:</p>
* <ul>
* <li>1. 普通字段校验失败(如:@NotBlank、@Size 等注解触发的错误)</li>
* <li>2. 枚举类型或格式不匹配导致的 typeMismatch 错误</li>
* </ul>
*
* @param e 捕获到的参数校验异常对象
* @return {@link Result }<{@link ? }>
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseBody
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<?> methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e){
log.error("捕获参数校验异常:{}", e.getMessage());
// 默认错误信息
String errorMessage = "参数校验失败";
// 若 BindingResult 不为空且存在字段错误,则使用字段的默认错误消息
if (e.getBindingResult() != null && e.getBindingResult().getFieldError() != null) {
errorMessage = e.getBindingResult().getFieldError().getDefaultMessage();
}
// 特殊情况处理:针对枚举类型参数或类型不匹配导致的 typeMismatch 错误
if (e.getBindingResult().getFieldError().getCodes().length > 2 &&
!e.getBindingResult().getFieldError().getCode().isEmpty()) {
// 判断是否为类型不匹配错误
if ("typeMismatch".equals(e.getBindingResult().getFieldError().getCode())) {
//获取错误参数名
String fieldName = e.getBindingResult().getFieldError().getField();
//获取错误参数值
Object rejectedValue = e.getBindingResult().getFieldError().getRejectedValue();
//获取对应的枚举
StringBuilder validValues = new StringBuilder("[");
Class<? extends BaseEnum> enumClass = WebEnumsRegistryUtil.getEnum(fieldName);
BaseEnum[] enumConstants = enumClass.getEnumConstants();
for (int i = 0; i < enumConstants.length; i++){
validValues.append(enumConstants[i].getValue());
if (i < enumConstants.length - 1) validValues.append(",");
}
validValues.append("]");
errorMessage = String.format("无效的值 [%s],字段类型应为:%s,支持的取值为:%s", rejectedValue, enumClass.getSimpleName(), validValues);
}
}
// 返回统一格式的错误响应,使用 BAD_REQUEST 状态码和具体错误信息
return Result.error(errorMessage);
}
测试:
4.修改body的异常捕获
- 首先还是想办法从异常中获取到异常的参数名
- 然后通过参数获取到枚举类
示例:
/**
* Http消息不可读异常。
* <p>
* 报错原因包括(不完全的列举):
* <p>
* (1)缺少请求体(RequestBody)异常;
* <p>
* (2)无效格式异常。比如:参数为数字,但是前端传递的是字符串且无法解析成数字。
* <p>
* (3)Json解析异常(非法Json格式)。传递的数据不是合法的Json格式。比如:key-value对中的value(值)为String类型,却没有用双引号括起来。
* <p>
* 举例:
* (1)缺少请求体(RequestBody)异常。报错:
* DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotReadableException:
* Required request body is missing:
* public void com.example.web.user.controller.UserController.addUser(com.example.web.model.param.UserAddParam)]
*/
@ExceptionHandler(HttpMessageNotReadableException.class)
@ResponseBody
@ResponseStatus(HttpStatus.NOT_FOUND)
public Result<?> httpMessageNotReadableException(HttpMessageNotReadableException e){
log.error("捕获Http消息不可读异常:{}", e.getMessage());
Throwable rootCause = e.getRootCause();
// 无效格式异常处理。比如:目标格式为数值,输入为非数字的字符串("80.5%"、"8.5.1"、"张三")。
if (rootCause instanceof InvalidFormatException) {
String userMessage = getHttpMessageNotReadableExceptionMessage((InvalidFormatException) rootCause);
return Result.error(userMessage);
}
String userMessage = "Http消息不可读异常!请稍后重试,或联系业务人员处理。";
return Result.error(userMessage);
}
/**
* 用户提示词获取器
*
* @param rootCause 根本原因
* @return {@link String }
*/
private String getHttpMessageNotReadableExceptionMessage(InvalidFormatException rootCause) {
//获取错误的参数字段名
String fieldName = rootCause.getPath().get(0).getFieldName();
//通过字段名查询字典获取枚举类
Class<? extends BaseEnum> enumClass = WebEnumsRegistryUtil.getEnum(fieldName);
//获取字段类型
Class<?> targetType = rootCause.getTargetType();
//获取传入的错误的值
Object value = rootCause.getValue();
if (targetType.isEnum()) {
// 如果是枚举类型,列出所有允许的枚举值
//搭建枚举值
BaseEnum[] enumConstants = enumClass.getEnumConstants();
StringBuilder validValues = new StringBuilder("[");
for (int i = 0; i < enumConstants.length; i++) {
validValues.append(enumConstants[i].getValue());
if (i < enumConstants.length - 1) validValues.append(", ");
}
validValues.append("]");
return String.format("无效的值 [%s],字段类型应为:%s,支持的取值为:%s", value, targetType.getSimpleName(), validValues);
}
String targetTypeNotification = "";
if (targetType == BigInteger.class || targetType == Integer.class || targetType == Long.class
|| targetType == Short.class || targetType == Byte.class) {
targetTypeNotification = "参数类型应为:整数;";
} else if (targetType == BigDecimal.class || targetType == Double.class || targetType == Float.class) {
targetTypeNotification = "参数类型应为:数值;";
}
return String.format("参数格式错误!%s当前输入参数:[%s]", targetTypeNotification, value);
}
测试:
传值为空
1.值为空
当值为空时,此时不会有任何报错,会直接通过
1.1.空值校验
想要实现对空值进行校验使用其他校验注解即可
示例:
在请求对象对应的值上加入@NotBlank(message = "季节名称不能为空")
package com.platform.enumtest.modules.dto;
import com.platform.enumtest.modules.Enums.Enum.SeasonEnum;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* 添加2接口请求参数body/json
*
* @author Liu
* @date 2025/05/02
*/
@Data
public class TestAdd2Dto {
@NotBlank(message = "季节名称不能为空")
private SeasonEnum seasonName;
}
汇总:
[!note]
所以要实现完整的使用枚举类型接收参数要以下几个步骤:
- 创建枚举类,在数据库和json序列化对应的参数上加上注解
- 修改该参数的接收方式,改为使用枚举类进行接收
- 创建一个工具类用来存储枚举类和参数的字典
- 在枚举类中加入static程序块,绑定参数和枚举类
- 全局异常处理
工具类参考:
package com.platform.enumtest.utils;
import com.platform.enumtest.modules.Enums.BaseEnum;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Web枚举注册表工具类(静态化)
*
* @author Liu
* @date 2025/05/02
*/
public class WebEnumsRegistryUtil {
// 使用泛型 Map<String, Enum<?>> 可以存储任意枚举类型
private static final Map<String, Class<? extends BaseEnum>> ENUM_MAP = new ConcurrentHashMap<>();
// 私有构造器,防止外部实例化
private WebEnumsRegistryUtil() {
}
/**
* 获取已注册的枚举
*
* @param key 键
* @return 对应的枚举对象,不存在则返回 null
*/
public static Class<? extends BaseEnum> getEnum(String key) {
return ENUM_MAP.get(key);
}
/**
* 注册枚举
*
* @param key 唯一标识符(如 "MessageType")
* @param value 枚举值
*/
public static void registerEnum(String key, Class<? extends BaseEnum> value) {
ENUM_MAP.put(key, value);
}
/**
* 检查是否包含某个键
*
* @param key 键
* @return boolean
*/
public static boolean containsKey(String key) {
return ENUM_MAP.containsKey(key);
}
/**
* 获取所有注册的枚举键集合
*
* @return {@link Map }<{@link String }, {@link Enum }<{@link ? }>>
*/
public static Map<String, Class<? extends BaseEnum>> getAllEnums() {
return Map.copyOf(ENUM_MAP); // 返回只读副本
}
}
枚举类搭建参考:
package com.platform.enumtest.modules.Enums.Enum;
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonValue;
import com.platform.enumtest.modules.Enums.BaseEnum;
import com.platform.enumtest.utils.WebEnumsRegistryUtil;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 季节枚举对象
*
* @author Liu
* @date 2025/05/02
*/
@AllArgsConstructor
@Getter
public enum SeasonEnum implements BaseEnum {
SPRING(1, "spring", "春天"),
SUMMER(2, "summer", "夏天"),
AUTUMN(3, "autumn", "秋天"),
WINTER(4, "winter", "冬天");
static {
//注册枚举类
WebEnumsRegistryUtil.registerEnum("seasonName", SeasonEnum.class);
}
//保存到数据库的值
@EnumValue
private final int code;
//返回给前端的值
@JsonValue
private final String name;
//前端传入的值
private final String desc;
/**
* 枚举值
*
* @return {@link String }
*/
@Override
public String getValue() {
return name; //前端传入的值
}
}
枚举类实现的接口参考:
package com.platform.enumtest.modules.Enums;
/**
* 基枚举
*
* @author Liu
* @date 2025/04/29
*/
public interface BaseEnum {
/**
* 枚举值
*
* @return {@link String }
*/
String getValue();
}
全局异常处理参考:
package com.platform.enumtest.config;
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
import com.platform.enumtest.modules.Enums.BaseEnum;
import com.platform.enumtest.utils.Result;
import com.platform.enumtest.utils.WebEnumsRegistryUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import java.math.BigDecimal;
import java.math.BigInteger;
/**
* 全局异常配置
*
* @author Liu
* @date 2025/05/02
*/
@ControllerAdvice
@Slf4j
public class GlobalExceptionConfig {
/**
* 参数校验异常处理方法。
* <p>该方法用于全局捕获控制器(Controller)中因请求参数校验失败而抛出的
* {@link MethodArgumentNotValidException}
* 异常,并返回统一格式的错误响应。</p>
* <p>主要处理两种类型的参数校验错误:</p>
* <ul>
* <li>1. 普通字段校验失败(如:@NotBlank、@Size 等注解触发的错误)</li>
* <li>2. 枚举类型或格式不匹配导致的 typeMismatch 错误</li>
* </ul>
*
* @param e 捕获到的参数校验异常对象
* @return {@link Result }<{@link ? }>
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseBody
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<?> methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e){
log.error("捕获参数校验异常:{}", e.getMessage());
// 默认错误信息
String errorMessage = "参数校验失败";
// 若 BindingResult 不为空且存在字段错误,则使用字段的默认错误消息
if (e.getBindingResult() != null && e.getBindingResult().getFieldError() != null) {
errorMessage = e.getBindingResult().getFieldError().getDefaultMessage();
}
// 特殊情况处理:针对枚举类型参数或类型不匹配导致的 typeMismatch 错误
if (e.getBindingResult().getFieldError().getCodes().length > 2 &&
!e.getBindingResult().getFieldError().getCode().isEmpty()) {
// 判断是否为类型不匹配错误
if ("typeMismatch".equals(e.getBindingResult().getFieldError().getCode())) {
//获取错误参数名
String fieldName = e.getBindingResult().getFieldError().getField();
//获取错误参数值
Object rejectedValue = e.getBindingResult().getFieldError().getRejectedValue();
//获取对应的枚举
StringBuilder validValues = new StringBuilder("[");
Class<? extends BaseEnum> enumClass = WebEnumsRegistryUtil.getEnum(fieldName);
BaseEnum[] enumConstants = enumClass.getEnumConstants();
for (int i = 0; i < enumConstants.length; i++){
validValues.append(enumConstants[i].getValue());
if (i < enumConstants.length - 1) validValues.append(",");
}
validValues.append("]");
errorMessage = String.format("无效的值 [%s],字段类型应为:%s,支持的取值为:%s", rejectedValue, enumClass.getSimpleName(), validValues);
}
}
// 返回统一格式的错误响应,使用 BAD_REQUEST 状态码和具体错误信息
return Result.error(errorMessage);
}
/**
* Http消息不可读异常。
* <p>
* 报错原因包括(不完全的列举):
* <p>
* (1)缺少请求体(RequestBody)异常;
* <p>
* (2)无效格式异常。比如:参数为数字,但是前端传递的是字符串且无法解析成数字。
* <p>
* (3)Json解析异常(非法Json格式)。传递的数据不是合法的Json格式。比如:key-value对中的value(值)为String类型,却没有用双引号括起来。
* <p>
* 举例:
* (1)缺少请求体(RequestBody)异常。报错:
* DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotReadableException:
* Required request body is missing:
* public void com.example.web.user.controller.UserController.addUser(com.example.web.model.param.UserAddParam)]
*/
@ExceptionHandler(HttpMessageNotReadableException.class)
@ResponseBody
@ResponseStatus(HttpStatus.NOT_FOUND)
public Result<?> httpMessageNotReadableException(HttpMessageNotReadableException e){
log.error("捕获Http消息不可读异常:{}", e.getMessage());
Throwable rootCause = e.getRootCause();
// 无效格式异常处理。比如:目标格式为数值,输入为非数字的字符串("80.5%"、"8.5.1"、"张三")。
if (rootCause instanceof InvalidFormatException) {
String userMessage = getHttpMessageNotReadableExceptionMessage((InvalidFormatException) rootCause);
return Result.error(userMessage);
}
String userMessage = "Http消息不可读异常!请稍后重试,或联系业务人员处理。";
return Result.error(userMessage);
}
/**
* 用户提示词获取器
*
* @param rootCause 根本原因
* @return {@link String }
*/
private String getHttpMessageNotReadableExceptionMessage(InvalidFormatException rootCause) {
//获取错误的参数字段名
String fieldName = rootCause.getPath().get(0).getFieldName();
//通过字段名查询字典获取枚举类
Class<? extends BaseEnum> enumClass = WebEnumsRegistryUtil.getEnum(fieldName);
//获取字段类型
Class<?> targetType = rootCause.getTargetType();
//获取传入的错误的值
Object value = rootCause.getValue();
if (targetType.isEnum()) {
// 如果是枚举类型,列出所有允许的枚举值
//搭建枚举值
BaseEnum[] enumConstants = enumClass.getEnumConstants();
StringBuilder validValues = new StringBuilder("[");
for (int i = 0; i < enumConstants.length; i++) {
validValues.append(enumConstants[i].getValue());
if (i < enumConstants.length - 1) validValues.append(", ");
}
validValues.append("]");
return String.format("无效的值 [%s],字段类型应为:%s,支持的取值为:%s", value, targetType.getSimpleName(), validValues);
}
String targetTypeNotification = "";
if (targetType == BigInteger.class || targetType == Integer.class || targetType == Long.class
|| targetType == Short.class || targetType == Byte.class) {
targetTypeNotification = "参数类型应为:整数;";
} else if (targetType == BigDecimal.class || targetType == Double.class || targetType == Float.class) {
targetTypeNotification = "参数类型应为:数值;";
}
return String.format("参数格式错误!%s当前输入参数:[%s]", targetTypeNotification, value);
}
}
格式化工厂参考:
package com.platform.enumtest.factory;
import com.platform.enumtest.modules.Enums.BaseEnum;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;
import java.util.HashMap;
import java.util.Map;
import java.util.WeakHashMap;
/**
* 通用枚举转换工厂类。
* 用于将传入的字符串参数自动转换为对应的枚举类型(BaseEnum 的子类)。
* 主要用于 Spring MVC 参数绑定场景,如从 URL 查询参数或表单中接收到的字符串值转换为对应枚举对象。
*/
public class UniversalEnumConverterFactory implements ConverterFactory<String, BaseEnum> {
/**
* 缓存每个枚举类型的对应的转换器实例。
* 使用 WeakHashMap 是为了当枚举类型不再被引用时,可以自动被垃圾回收,避免内存泄漏。
*/
private static final Map<Class, Converter> converterMap = new WeakHashMap<>();
/**
* 获取指定目标类型的转换器。
*
* @param targetType 枚举类型(BaseEnum 的子类)
* @return 转换器实例
*/
@Override
public <T extends BaseEnum> Converter<String, T> getConverter(Class<T> targetType) {
// 尝试从缓存中获取已有的转换器
Converter result = converterMap.get(targetType);
if (result == null) {
// 如果不存在,则创建一个新的转换器并放入缓存
result = new IntegerStrToEnum<>(targetType);
converterMap.put(targetType, result);
}
return result;
}
/**
* 字符串到枚举的转换器内部类。
* 支持将字符串形式的输入转换为具体的枚举对象。
*
* @param <T> 枚举类型,必须是 BaseEnum 的子类
*/
class IntegerStrToEnum<T extends BaseEnum> implements Converter<String, T> {
private final Class<T> enumType; // 当前处理的枚举类类型
private Map<String, T> enumMap = new HashMap<>(); // 缓存枚举值与枚举对象的映射关系
/**
* 构造函数,初始化枚举类和对应的值-枚举映射表。
*
* @param enumType 枚举类类型
*/
public IntegerStrToEnum(Class<T> enumType) {
this.enumType = enumType;
// 获取该枚举类的所有枚举常量
T[] enums = enumType.getEnumConstants();
// 遍历所有枚举常量,并以 value 字段作为 key 存入 map
for (T e : enums) {
enumMap.put(e.getValue() + "", e); // 将 value 转成字符串用于匹配字符串输入
}
}
/**
* 转换方法,将字符串转换为对应的枚举对象。
*
* @param source 输入字符串
* @return 对应的枚举对象
* @throws IllegalArgumentException 如果没有匹配的枚举值,抛出异常
*/
@Override
public T convert(String source) {
// 根据字符串查找对应的枚举对象
T result = enumMap.get(source);
if (result == null) {
// 如果找不到匹配项,抛出非法参数异常
throw new IllegalArgumentException("无法匹配到枚举值:" + source);
}
return result;
}
}
}
格式化程序参考:
package com.platform.enumtest.config;
import com.platform.enumtest.factory.UniversalEnumConverterFactory;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* web配置
*
* @author Liu
* @date 2025/05/02
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
/**
* 添加格式化程序
*
* @param registry 登记处
*/
@Override
public void addFormatters(FormatterRegistry registry) {
// ApplicationConversionService.configure(registry);
registry.addConverterFactory(new UniversalEnumConverterFactory());
}
}