和AI工具DeepSeek R1聊聊Java中枚举的应用知识

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 :当需要简单的类型转换(如 StringLocalDateStringEnum)时。
  • 使用 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

通过 WebMvcConfigurerConversionService 注册自定义转换器。

方式 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  # 自动注册默认的日期转换

常见问题

  1. 转换器未生效 :检查是否已正确注册到 ConversionService
  2. 泛型类型擦除 :使用 Converter<S, T> 时确保泛型类型明确。
  3. 优先级问题 :多个转换器匹配同一类型时,使用 @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;
}

六、总结与最佳实践

通过本文的探讨,我们可以得出以下结论:

  1. 设计原则

    • 优先使用枚举替代传统常量
    • 为业务枚举实现统一接口
    • 保持枚举类的不可变性
    • 通过合理使用 EnumMapEnumSet,可以显著提升代码的性能和可读性!
  2. 序列化规范

    • Web层:使用@JsonValue + @JsonCreator 或 Spring MVC Converter
    • ORM层:实现自定义TypeHandler
    • NoSQL:配置字段类型或转换器
  3. 性能考量

    • 避免在枚举中定义重型操作
    • 缓存频繁使用的枚举查找结果
    • 使用EnumMap进行高效映射

DeepSeek R1结语:枚举是Java送给开发者的礼物,合理运用可以显著提升代码质量。当你在实践中遇到枚举相关问题时,不妨思考:这个场景是否可以通过枚举的特性更优雅地解决?

扩展思考:如何实现跨微服务的枚举值同步?建议采用共享常量库或Protobuf枚举定义的方式保持一致性。

本文由博客一文多发平台 OpenWrite 发布!