Prompt: 提示词
java
请以"和AI工具DeepSeek R1聊聊Java中枚举的应用知识"为题,写一篇技术博文
1,介绍枚举的基本知识
2,介绍枚举实现接口的知识
3,介绍枚举搭配Jackson使用在Spring MVC @RequestBody, @ResponseBody中的知识
4, 介绍枚举在Spring MVC @RequestParam, @PathVariable中使用枚举值解析的知识
5,介绍MyBatis中使用枚举时写入枚举值而不是枚举名称;
6,介绍MongoDB中使用枚举时写入枚举值而不是枚举名称;
7, 总结
Answer: 和AI工具DeepSeek R1聊聊Java中枚举的应用知识
DeepSeek R1:在Java开发中,枚举(Enum)是一个看似简单却充满潜力的特性。今天我们将从基础到实战,探讨枚举在不同场景下的应用技巧,帮助开发者规避常见陷阱。
一、枚举基础:类型安全的优雅实践
Java枚举(enum
)本质上是语法糖,编译后继承java.lang.Enum
类。
java
// 标准枚举定义
public enum HttpStatus {
OK(200, "Success"),
NOT_FOUND(404, "Resource not found");
private final int code;
private final String message;
HttpStatus(int code, String message) {
this.code = code;
this.message = message;
}
// 通过code查找枚举实例
public static HttpStatus parse(int code) {
return Arrays.stream(values())
.filter(s -> s.code == code)
.findFirst()
.orElseThrow(IllegalArgumentException::new);
}
}
相较于传统常量类,枚举具备以下优势: 关键特性:
类型安全
: 编译时类型检查- 内置
values()
和valueOf()
方法 - 支持构造器、方法、字段
- 天然单例特性
- EnumMap, EnumSet泛型集合类
DeepSeek R1提示 :
优先使用枚举
替代整数常量,可提升代码可读性
并减少魔法值问题。Java枚举的缺陷:1.在Jacksong中,默认会使用枚举名称name()函数返回值来序列化和反序列化;2.在使用Mybatis持久化时,会默认使用枚举的ordinal()函数返回值,即枚举值在枚举的定义次序0,1,2..来表示。
由于上述Java枚举的缺陷,我们需要在Jackson, Spring MVC, Mybastis, MongoDB等spring-data项目中进行定制化,优先使用相对不会改变的业务枚举值来进行序列化和反序列化。
二、枚举实现接口:多态的高级玩法
枚举可实现接口实现差异化行为,这在状态机模式中尤为实用:
java
public interface Operation {
double apply(double a, double b);
}
public enum Calculator implements Operation {
ADD {
@Override
public double apply(double a, double b) {
return a + b;
}
},
MULTIPLY {
@Override
public double apply(double a, double b) {
return a * b;
}
};
}
// 使用示例
double result = Calculator.ADD.apply(3, 5); // 返回8.0
适用场景:
- 策略模式实现
- 有限状态机
- 命令模式封装
三、Spring MVC与Jackson的枚举协作
Jackson默认序列化枚举的name()
,可通过以下方式定制:
方案1:注解方式
java
public enum UserType {
ADMIN(1),
MEMBER(2),
;
@JsonValue
private final int code;
UserType(int code) { this.code = code; }
@JsonCreator
public static UserType fromCode(int code) {
return Arrays.stream(values())
.filter(t -> t.code == code)
.findFirst()
.orElseThrow(IllegalArgumentException::new);
}
}
方案2:全局配置
java
@Configuration
public class JacksonConfig {
@Bean
public Module enumModule() {
SimpleModule module = new SimpleModule();
module.addSerializer(Enum.class, new EnumSerializer());
module.addDeserializer(Enum.class, new EnumDeserializer());
return module;
}
}
DeepSeek R1提醒 :使用
@JsonValue
时确保反序列化逻辑的健壮性,建议配合@JsonCreator
使用。实践总结:我们可以定义一个枚举类的接口,来约束其它枚举类的定义
- 枚举类接口
java
public interface IntegerBaseEnum<E extends Enum<?> & IntegerBaseEnum<E>> {
Logger LOGGER = LoggerFactory.getLogger(IntegerBaseEnum.class);
String getLabel();
@JsonValue
Integer getValue();
Class<E> getEnumClass();
@JsonCreator
default E fromJsonNode(JsonNode node) {
Class<E> enumClass = getEnumClass();
return IntegerBaseEnum.fromJsonNode(enumClass, node);
}
static <E extends Enum<?> & IntegerBaseEnum<E>> E fromValue(Class<E> enumClass, Integer value) {
if (value == null) {
return null;
}
E[] enumConstants = enumClass.getEnumConstants();
for (E e : enumConstants) {
if (e.getValue().equals(value)) {
return e;
}
}
LOGGER.error("enum value {} not found in {}", value, enumClass.getSimpleName());
return null;
}
static <E extends Enum<?> & IntegerBaseEnum<E>> E fromJsonNode(Class<E> enumClass, JsonNode node) {
if (node == null) {
return null;
}
if (!StringUtils.hasText(node.asText())) {
return null;
}
return IntegerBaseEnum.fromValue(enumClass, Integer.valueOf(node.asText()));
}
static <E extends Enum<?> & IntegerBaseEnum<E>> E fromDescription(Class<E> enumClass, String label) {
if (StrUtil.isBlank(label)) {
return null;
}
E[] enumConstants = enumClass.getEnumConstants();
for (E e : enumConstants) {
if (e.getLabel().equals(label)) {
return e;
}
}
LOGGER.error("enum label {} not found in {}", label, enumClass.getSimpleName());
return null;
}
static <E extends Enum<?> & IntegerBaseEnum<E>> List<LabelValueDto> fromValue(Class<E> enumClass) {
E[] enumConstants = enumClass.getEnumConstants();
List<LabelValueDto> resultList = new ArrayList<>(enumConstants.length);
for (E e : enumConstants) {
LabelValueDto item = new LabelValueDto();
item.setLabel(e.getLabel());
item.setValue(e.getValue());
resultList.add(item);
}
return resultList;
}
}
- 枚举类定义
java
/**
* 通用状态枚举
* @author james
*/
@AllArgsConstructor
public enum CommonStatusEnum implements IntegerBaseEnum<CommonStatusEnum> {
/**
* 无效, 比如表示逻辑无效,也可以表示逻辑删除
*/
INVALID(0, "Invalid"),
/**
* 有效
*/
VALID(1, "Valid"),
;
private final Integer value;
private final String label;
@Override
public String getLabel() {
return this.label;
}
//@JsonValue
@Override
public Integer getValue() {
return this.value;
}
@Override
public Class<CommonStatusEnum> getEnumClass() {
return CommonStatusEnum.class;
}
// @JsonCreator
// public static CommonStatusEnum fromJsonNode(JsonNode node) {
// return IntegerBaseEnum.fromJsonNode(CommonStatusEnum.class, node);
// }
}
四、在Spring MVC @RequestParam, @PathVariable中使用枚举值解析
在 Spring MVC 中,Converter
是用于类型转换的核心接口,常用于将请求参数(如 URL 参数、表单字段等)转换为目标对象类型,或将对象类型转换为响应数据。以下是使用 Converter
的最佳实践:
1. 选择合适的场景
- 使用
Converter
:当需要简单的类型转换(如String
→LocalDate
、String
→Enum
)时。 - 使用
Formatter
:当转换需要本地化(如日期格式化)或同时处理输入和输出时。 - 使用
HttpMessageConverter
:当处理 HTTP 请求体(如 JSON/XML 到对象的转换)时。
2. 实现自定义 Converter
实现 Converter<S, T>
接口,定义从源类型 S
到目标类型 T
的转换逻辑。
示例:字符串转 LocalDate
java
public class StringToLocalDateConverter implements Converter<String, LocalDate> {
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
@Override
public LocalDate convert(String source) {
return LocalDate.parse(source, formatter);
}
}
示例:字符串转枚举(忽略大小写)
java
public class StringToEnumConverter<T extends Enum<T>> implements Converter<String, T> {
private final Class<T> enumType;
public StringToEnumConverter(Class<T> enumType) {
this.enumType = enumType;
}
@Override
public T convert(String source) {
return Enum.valueOf(enumType, source.toUpperCase());
}
}
3. 注册 Converter
通过 WebMvcConfigurer
或 ConversionService
注册自定义转换器。
方式 1:使用 WebMvcConfigurer
java
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new StringToLocalDateConverter());
// 逐个注册枚举类???
registry.addConverter(new StringToEnumConverter<>(UserRole.class));
}
}
方式 2:使用 ConversionService
java
@Configuration
public class ConversionConfig {
@Bean
public ConversionService conversionService() {
DefaultFormattingConversionService service = new DefaultFormattingConversionService();
service.addConverter(new StringToLocalDateConverter());
return service;
}
}
4. 确保线程安全
- 无状态设计 :
Converter
实现应是无状态的,避免使用实例变量
。 - 缓存常用对象 :例如,缓存
DateTimeFormatter
或正则表达式对象以提高性能。
5. 错误处理
在转换失败时抛出 IllegalArgumentException
,Spring 会自动将其转换为 400 Bad Request
。
示例:自定义错误消息
java
@Override
public LocalDate convert(String source) {
try {
return LocalDate.parse(source, formatter);
} catch (DateTimeParseException e) {
throw new IllegalArgumentException("Invalid date format. Expected format: yyyy-MM-dd");
}
}
6. 测试 Converter
编写单元测试覆盖所有可能的输入,包括边界值和异常情况。
示例:JUnit 测试
java
public class StringToLocalDateConverterTest {
private final StringToLocalDateConverter converter = new StringToLocalDateConverter();
@Test
public void testValidDate() {
LocalDate date = converter.convert("2023-10-01");
assertEquals(LocalDate.of(2023, 10, 1), date);
}
@Test(expected = IllegalArgumentException.class)
public void testInvalidDate() {
converter.convert("2023/10/01");
}
}
7. 结合泛型和工厂类
为减少重复代码
,可为通用转换逻辑(如枚举转换)创建工厂类
。
示例:通用枚举转换器工厂
java
/**
* 通用枚举转换工厂:将 String 转换为任意枚举类型(忽略大小写)
*/
public class EnumConverterFactory implements ConverterFactory<String, Enum<?>> {
@Override
public <T extends Enum<?>> Converter<String, T> getConverter(Class<T> targetType) {
// 返回一个针对具体枚举类型的转换器
return new StringToEnumConverter<>(targetType);
}
/**
* 内部转换器:处理单个枚举类型的转换逻辑
*/
private static class StringToEnumConverter<T extends Enum<T>> implements Converter<String, T> {
private final Class<T> enumType;
public StringToEnumConverter(Class<T> enumType) {
this.enumType = enumType;
}
@Override
public T convert(String source) {
if (source.isEmpty()) {
return null; // 允许空字符串转为 null
}
try {
// 忽略大小写匹配枚举值
return Enum.valueOf(enumType, source.trim().toUpperCase());
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(
"无效的枚举值: '" + source + "'. 支持的枚举值为: " + Arrays.toString(enumType.getEnumConstants())
);
}
}
}
}
注册工厂:
java
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
// 注册枚举转换工厂
registry.addConverterFactory(new EnumConverterFactory());
}
}
验证转换器的正确性和异常处理:
java
public class EnumConverterFactoryTest {
private final EnumConverterFactory factory = new EnumConverterFactory();
@Test
public void testConvertToAdmin() {
Converter<String, UserRole> converter = factory.getConverter(UserRole.class);
UserRole role = converter.convert("admin");
assertEquals(UserRole.ADMIN, role);
}
@Test(expected = IllegalArgumentException.class)
public void testInvalidEnumValue() {
Converter<String, UserRole> converter = factory.getConverter(UserRole.class);
converter.convert("unknown");
}
}
8. 与 @RequestParam
或 @PathVariable
结合使用
Spring 会自动使用注册的 Converter
处理参数绑定。
java
@GetMapping("/user")
public ResponseEntity<User> getUser(@RequestParam("birthDate") LocalDate birthDate) {
// 自动使用 StringToLocalDateConverter 转换 birthDate
}
9. 性能优化
- 避免重复初始化 :缓存
Converter
实例或内部使用的对象(如DateTimeFormatter
)。 - 使用静态内部类 :如果
Converter
是无状态的,可以声明为单例。
10. 结合 Spring Boot
在 Spring Boot 中,可以通过自动配置简化 Converter
的注册:
yaml
# application.yml
spring:
mvc:
format:
date: yyyy-MM-dd # 自动注册默认的日期转换
常见问题
- 转换器未生效 :检查是否已正确注册到
ConversionService
。 - 泛型类型擦除 :使用
Converter<S, T>
时确保泛型类型明确。 - 优先级问题 :多个转换器匹配同一类型时,使用
@Order
注解调整优先级。
通过遵循这些最佳实践,可以高效、安全地使用 Spring MVC 的 Converter
实现类型转换逻辑。
四、MyBatis枚举处理:存储值的正确姿势
默认情况下MyBatis存储枚举的name()
,通过TypeHandler实现自定义存储:
1. 实现自定义TypeHandler
java
public class IntegerBaseEnumTypeHandler<E extends Enum<?> & IntegerBaseEnum<E>> extends BaseTypeHandler<IntegerBaseEnum<E>> {
private final Class<E> clazz;
public IntegerBaseEnumTypeHandler(Class<E> enumType) {
if (enumType == null) {
throw new IllegalArgumentException("Type argument cannot be null");
}
this.clazz = enumType;
}
@Override
public void setNonNullParameter(PreparedStatement ps, int i, IntegerBaseEnum parameter, JdbcType jdbcType) throws SQLException {
ps.setInt(i, parameter.getValue());
}
@Override
public E getNullableResult(ResultSet rs, String columnName) throws SQLException {
Object object = rs.getObject(columnName);
Integer anInt = getValue(object);
return IntegerBaseEnum.fromValue(clazz, anInt);
}
@Override
public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
Object object = rs.getObject(columnIndex);
Integer anInt = getValue(object);
return IntegerBaseEnum.fromValue(clazz, anInt);
}
@Override
public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
Object object = cs.getObject(columnIndex);
Integer anInt = getValue(object);
return IntegerBaseEnum.fromValue(clazz, anInt);
}
private Integer getValue(Object object) {
if (object == null) {
return null;
}
return Integer.parseInt(object.toString());
}
}
2. 配置MyBatis
逐个定义枚举类型处理器
xml
<!-- mybatis-config.xml -->
<typeHandlers>
<typeHandler handler="com.example.CodeEnumTypeHandler"
javaType="com.example.HttpStatus"/>
</typeHandlers>
配置默认的枚举处理器
properties
# mybatis
mybatis.mapper-locations = classpath:mapper/*.xml
mybatis.type-aliases-package = ******.entities
mybatis.configuration.cache-enabled = true
mybatis.configuration.map-underscore-to-camel-case = true
mybatis.configuration.default-enum-type-handler=******..mybatis.IntegerBaseEnumTypeHandler
# sql 日志
mybatis.configuration.log-impl = org.apache.ibatis.logging.stdout.StdOutImpl
3. 枚举接口定义
java
public interface IntegerBaseEnum<E extends Enum<?> & IntegerBaseEnum<E>> {
Logger LOGGER = LoggerFactory.getLogger(IntegerBaseEnum.class);
String getLabel();
Integer getValue();
static <E extends Enum<?> & IntegerBaseEnum<E>> E fromValue(Class<E> enumClass, Integer value) {
if (value == null) {
return null;
}
E[] enumConstants = enumClass.getEnumConstants();
for (E e : enumConstants) {
if (e.getValue().equals(value)) {
return e;
}
}
LOGGER.error("enum value {} not found in {}", value, enumClass.getSimpleName());
return null;
}
static <E extends Enum<?> & IntegerBaseEnum<E>> E fromJsonNode(Class<E> enumClass, JsonNode node) {
if (node == null) {
return null;
}
if (!StringUtils.hasText(node.asText())) {
return null;
}
return IntegerBaseEnum.fromValue(enumClass, Integer.valueOf(node.asText()));
}
static <E extends Enum<?> & IntegerBaseEnum<E>> E fromDescription(Class<E> enumClass, String label) {
if (StrUtil.isBlank(label)) {
return null;
}
E[] enumConstants = enumClass.getEnumConstants();
for (E e : enumConstants) {
if (e.getLabel().equals(label)) {
return e;
}
}
LOGGER.error("enum label {} not found in {}", label, enumClass.getSimpleName());
return null;
}
static <E extends Enum<?> & IntegerBaseEnum<E>> List<LabelValueDto> fromValue(Class<E> enumClass) {
E[] enumConstants = enumClass.getEnumConstants();
List<LabelValueDto> resultList = new ArrayList<>(enumConstants.length);
for (E e : enumConstants) {
LabelValueDto item = new LabelValueDto();
item.setLabel(e.getLabel());
item.setValue(e.getValue());
resultList.add(item);
}
return resultList;
}
}
五、MongoDB枚举处理:存储优化的关键
MongoDB Java驱动默认存储枚举名称,可通过以下方式优化:
方案1:注解方式
java
@Document
public class User {
@Field(targetType = FieldType.INT32)
private UserType type;
}
方案2:自定义转换器
java
@WritingConverter
public class EnumToIntegerConverter implements Converter<UserType, Integer> {
public Integer convert(UserType source) {
return source.getCode();
}
}
// 注册转换器
@Configuration
public class MongoConfig extends AbstractMongoClientConfiguration {
@Override
public CustomConversions customConversions() {
return new CustomConversions(Arrays.asList(
new EnumToIntegerConverter(),
new IntegerToEnumConverter()
));
}
}
方案3:自定义转换器
java
public class EnumIntegerConverter<E extends Enum<?> & IntegerBaseEnum<E>> implements PropertyValueConverter<E, Integer, ValueConversionContext<?>> {
@Override
public E read(Integer value, ValueConversionContext<?> context) {
Class<?> rawType = context.getProperty().getRawType();
if (!IntegerBaseEnum.class.isAssignableFrom(rawType)) {
throw new IllegalStateException("Type mismatch");
}
@SuppressWarnings("unchecked")
Class<E> enumType = (Class<E>) rawType;
return IntegerBaseEnum.fromValue(enumType, value);
}
@Override
public Integer write(E value, ValueConversionContext<?> context) {
return value.getValue();
}
}
@Data
@Document("component_publish_event_detail")
public class ComponentPublishEventDetail extends BaseMongoEntity implements Serializable {
/**
* 业务类型
*/
@Field("business_type")
@ValueConverter(EnumIntegerConverter.class)
private PublishUpdateBusinessTypeEnum businessType;
/**
* businessId, 比如pageUrl/fragment相关的id
*/
@Field("business_id")
private String businessId;
/**
* 执行状态
*/
@ValueConverter(EnumIntegerConverter.class)
@Field("execute_status")
private EventDetailStatus executeStatus;
/**
* 异常信息
*/
@Field("exception_message")
private String exceptionMessage;
/**
* 修改组件影响的page
*/
@Field("update_source")
private List<PageUpdateSourceDto> updateSource;
}
六、总结与最佳实践
通过本文的探讨,我们可以得出以下结论:
-
设计原则
- 优先使用枚举替代传统常量
- 为业务枚举实现统一接口
- 保持枚举类的不可变性
- 通过合理使用
EnumMap
和EnumSet
,可以显著提升代码的性能和可读性!
-
序列化规范
- Web层:使用
@JsonValue
+@JsonCreator
或 Spring MVC Converter - ORM层:实现自定义TypeHandler
- NoSQL:配置字段类型或转换器
- Web层:使用
-
性能考量
- 避免在枚举中定义重型操作
- 缓存频繁使用的枚举查找结果
- 使用EnumMap进行高效映射
DeepSeek R1结语:枚举是Java送给开发者的礼物,合理运用可以显著提升代码质量。当你在实践中遇到枚举相关问题时,不妨思考:这个场景是否可以通过枚举的特性更优雅地解决?
扩展思考:如何实现跨微服务的枚举值同步?建议采用共享常量库或Protobuf枚举定义的方式保持一致性。
本文由博客一文多发平台 OpenWrite 发布!