0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

看看人家在接口中使用枚举类型的方式

jf_ro2CN3Fa ? 来源:geekhalo ? 2023-02-24 13:59 ? 次阅读
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

1. 概览

枚举作为 Java 5 的重要特征,相信大家并不陌生,但在实际开发过程中,当 name 和 ordrial 发生变化时,如果处理不当非常容易引起系统bug。这种兼容性bug非常难以定位,需要从框架层次进行避免,而非仅靠开发人员的主观意识。

1.1. 背景

枚举很好用,特别是提供的 name 和 ordrial 特性,但这点对重构造成了一定影响,比如:

某个枚举值业务语义发生变化,需要将其进行 rename 操作,以更好的表达新业务语义

新增、删除或者为了展示调整了枚举定义顺序

这些在业务开发中非常常见,使用 IDE 的 refactor 功能可以快速且准确的完成重构工作。但,如果系统将这些暴露出去或者存储到数据库等存储引擎就变得非常麻烦,不管是 name 还是 ordrial 的变更都会产生兼容性问题。

对此,最常见的解决方案便是放弃使用 name 和 ordrial,转而使用控制能力更强的 code。

1.2. 目标

提供一组工具,以方便的基于 code 使用枚举,快速完成对现有框架的集成:

完成与 Spring MVC 的集成,基于 code 使用枚举;加强返回值,以对象的方式进行返回,信息包括 code、name、description

提供统一的枚举字典,自动扫描系统中的枚举并将其以 restful 的方式暴露给前端

使用 code 进行数据存储操作,避免重构的影响

2. 快速入门

2.1. 添加 starter

在 Spring boot 项目的 pom 中增加如下依赖:

com.geekhalo.lego
lego-starter
0.1.19-enum-SNAPSHOT

2.2. 统一枚举结构

如何统一枚举行为呢?公共父类肯定是不行的,但可以为其提供一个接口,在接口中完成行为的定义。

2.2.1. 定义枚举接口

除了在枚举中自定义 code 外,通常还会为其提供描述信息,构建接口如下:

publicinterfaceCodeBasedEnum{
intgetCode();
}
publicinterfaceSelfDescribedEnum{
defaultStringgetName(){
returnname();
}

Stringname();

StringgetDescription();
}
publicinterfaceCommonEnumextendsCodeBasedEnum,SelfDescribedEnum{

}

整体结构如下:

7cabccf2-b37f-11ed-bfe3-dac502259ad0.png

在定义枚举时便可以直接使用CommonEnum这个接口。

2.2.2. 实现枚举接口

有了统一的枚举接口,在定义枚举时便可以直接实现接口,从而完成对枚举的约束。

publicenumNewsStatusimplementsCommonEnum{
DELETE(1,"删除"),
ONLINE(10,"上线"),
OFFLINE(20,"下线");
privatefinalintcode;
privatefinalStringdesc;

NewsStatus(intcode,Stringdesc){
this.code=code;
this.desc=desc;
}

@Override
publicintgetCode(){
returnthis.code;
}

@Override
publicStringgetDescription(){
returnthis.desc;
}
}

2.3. 自动注册 CommonEnum

有了统一的 CommonEnum 最大的好处便是可以进行统一管理,对于统一管理,第一件事便是找到并注册所有的 CommonEnum。

7cbbd3d6-b37f-11ed-bfe3-dac502259ad0.png

以上是核心处理流程:

首先通过 Spring 的 ResourcePatternResolver 根据配置的 basePackage 对classpath进行扫描

扫描结果以Resource来表示,通过 MetadataReader 读取 Resource 信息,并将其解析为 ClassMetadata

获得 ClassMetadata 之后,找出实现 CommonEnum 的类

将 CommonEnum 实现类注册到两个 Map 中进行缓存

备注:此处万万不可直接使用反射技术,反射会触发类的自动加载,将对众多不需要的类进行加载,从而增加 metaspace 的压力。

在需要 CommonEnum 时,只需注入 CommonEnumRegistry Bean 便可以方便的获得 CommonEnum 的具体实现。

2.4. Spring MVC 接入层

Web 层是最常见的接入点,对于 CommonEnum 我们倾向于:

参数使用 code 来表示,避免 name、ordrial 变化导致业务异常

丰富返回值,包括枚举的 code、name、description 等

7ce61fba-b37f-11ed-bfe3-dac502259ad0.png

2.4.1. 入参转化

Spring MVC 存在两种参数转化扩展:

对于普通参数,比如 RequestParam 或 PathVariable 直接从 ConditionalGenericConverter 进行扩展

基于 CommonEnumRegistry 提供的 CommonEnum 信息,对 matches 和 getConvertibleTypes方法进行重写

根据目标类型获取所有的 枚举值,并根据 code 和 name 进行转化

对于 Json 参数,需要对 Json 框架进行扩展(以 Jackson 为例)

遍历 CommonEnumRegistry 提供的所有 CommonEnum,依次进行注册

从 Json 中读取信息,根据 code 和 name 转化为确定的枚举值

两种扩展核心实现见:

@Order(1)
@Component
publicclassCommonEnumConverterimplementsConditionalGenericConverter{
@Autowired
privateCommonEnumRegistryenumRegistry;

@Override
publicbooleanmatches(TypeDescriptorsourceType,TypeDescriptortargetType){
Classtype=targetType.getType();
returnenumRegistry.getClassDict().containsKey(type);
}

@Override
publicSetgetConvertibleTypes(){
returnenumRegistry.getClassDict().keySet().stream()
.map(cls->newConvertiblePair(String.class,cls))
.collect(Collectors.toSet());
}

@Override
publicObjectconvert(Objectsource,TypeDescriptorsourceType,TypeDescriptortargetType){
Stringvalue=(String)source;
ListcommonEnums=this.enumRegistry.getClassDict().get(targetType.getType());
returncommonEnums.stream()
.filter(commonEnum->commonEnum.match(value))
.findFirst()
.orElse(null);
}
}

staticclassCommonEnumJsonDeserializerextendsJsonDeserializer{
privatefinalListcommonEnums;

CommonEnumJsonDeserializer(ListcommonEnums){
this.commonEnums=commonEnums;
}

@Override
publicObjectdeserialize(JsonParserjsonParser,DeserializationContextdeserializationContext)throwsIOException,JacksonException{
Stringvalue=jsonParser.readValueAs(String.class);
returncommonEnums.stream()
.filter(commonEnum->commonEnum.match(value))
.findFirst()
.orElse(null);
}
}

2.4.2. 增强返回值

默认情况下,对于枚举类型在转换为 Json 时,只会输出 name,其他信息会出现丢失,对于展示非常不友好,对此,需要对 Json 序列化进行能力增强。

首先,需要定义 CommonEnum 对应的返回对象,具体如下:

@Value
@AllArgsConstructor(access=AccessLevel.PRIVATE)
@ApiModel(description="通用枚举")
publicclassCommonEnumVO{
@ApiModelProperty(notes="Code")
privatefinalintcode;

@ApiModelProperty(notes="Name")
privatefinalStringname;

@ApiModelProperty(notes="描述")
privatefinalStringdesc;

publicstaticCommonEnumVOfrom(CommonEnumcommonEnum){
if(commonEnum==null){
returnnull;
}
returnnewCommonEnumVO(commonEnum.getCode(),commonEnum.getName(),commonEnum.getDescription());
}

publicstaticListfrom(ListcommonEnums){
if(CollectionUtils.isEmpty(commonEnums)){
returnCollections.emptyList();
}
returncommonEnums.stream()
.filter(Objects::nonNull)
.map(CommonEnumVO::from)
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
}

CommonEnumVO 是一个标准的 POJO,只是增加了 Swagger 相关注解。

CommonEnumJsonSerializer 是自定义序列化的核心,会将 CommonEnum 封装为 CommonEnumVO 并进行写回,具体如下:

staticclassCommonEnumJsonSerializerextendsJsonSerializer{

@Override
publicvoidserialize(Objecto,JsonGeneratorjsonGenerator,SerializerProviderserializerProvider)throwsIOException{
CommonEnumcommonEnum=(CommonEnum)o;
CommonEnumVOcommonEnumVO=CommonEnumVO.from(commonEnum);
jsonGenerator.writeObject(commonEnumVO);
}
}

2.4.3. 效果展示

首先,新建一个测试枚举 NewsStatus,具体如下:

publicenumNewsStatusimplementsCommonEnum{
DELETE(1,"删除"),
ONLINE(10,"上线"),
OFFLINE(20,"下线");
privatefinalintcode;
privatefinalStringdesc;

NewsStatus(intcode,Stringdesc){
this.code=code;
this.desc=desc;
}

@Override
publicintgetCode(){
returnthis.code;
}

@Override
publicStringgetDescription(){
returnthis.desc;
}
}

然后新建 EnumController,具体如下:

@RestController
@RequestMapping("enum")
publicclassEnumController{
@GetMapping("paramToEnum")
publicRestResultparamToEnum(@RequestParam("newsStatus")NewsStatusnewsStatus){
returnRestResult.success(CommonEnumVO.from(newsStatus));
}

@GetMapping("pathToEnum/{newsStatus}")
publicRestResultpathToEnum(@PathVariable("newsStatus")NewsStatusnewsStatus){
returnRestResult.success(CommonEnumVO.from(newsStatus));
}

@PostMapping("jsonToEnum")
publicRestResultjsonToEnum(@RequestBodyNewsStatusRequestBodynewsStatusRequestBody){
returnRestResult.success(CommonEnumVO.from(newsStatusRequestBody.getNewsStatus()));
}

@GetMapping("bodyToJson")
publicRestResultbodyToJson(){
NewsStatusResponseBodynewsStatusResponseBody=newNewsStatusResponseBody();
newsStatusResponseBody.setNewsStatus(Arrays.asList(NewsStatus.values()));
returnRestResult.success(newsStatusResponseBody);
}

@Data
publicstaticclassNewsStatusRequestBody{
privateNewsStatusnewsStatus;
}

@Data
publicstaticclassNewsStatusResponseBody{
privateListnewsStatus;
}
}

执行结果如下:

7d89d560-b37f-11ed-bfe3-dac502259ad0.png

整体符合预期:

使用 code 作为请求参数可以自动转化为对应的 CommonEnum

使用 CommonEnum 作为返回值,返回标准的 CommonEnumVO 对象结构

2.5. 通用枚举字典接口

有时可以将 枚举 理解为系统的一类字段,比较典型的就是管理页面的各种下拉框,下拉框中的数据来自于后台服务。

有了 CommonEnum 之后,可以提供统一的一组枚举字典,避免重复开发,同时在新增枚举时也无需进行扩展,系统自动识别并添加到字典中。

2.5.1. 构建字典Controller

在 CommonEnumRegistry 基础之上实现通用字典接口非常简单,只需按规范构建 Controller 即可,具体如下:

@Api(tags="通用字典接口")
@RestController
@RequestMapping("/enumDict")
@Slf4j
publicclassEnumDictController{
@Autowired
privateCommonEnumRegistrycommonEnumRegistry;

@GetMapping("all")
publicRestResult>>allEnums(){
Map>dict=this.commonEnumRegistry.getNameDict();
Map>dictVo=Maps.newHashMapWithExpectedSize(dict.size());
for(Map.Entry>entry:dict.entrySet()){
dictVo.put(entry.getKey(),CommonEnumVO.from(entry.getValue()));
}
returnRestResult.success(dictVo);
}

@GetMapping("types")
publicRestResult>enumTypes(){
Map>dict=this.commonEnumRegistry.getNameDict();
returnRestResult.success(Lists.newArrayList(dict.keySet()));
}

@GetMapping("/{type}")
publicRestResult>dictByType(@PathVariable("type")Stringtype){
Map>dict=this.commonEnumRegistry.getNameDict();
ListcommonEnums=dict.get(type);

returnRestResult.success(CommonEnumVO.from(commonEnums));
}
}

该 Controller 提供如下能力:

获取全部字典,一次性获取系统中所有的 CommonEnum

获取所有字典类型,仅获取字典类型,通常用于测试

获取指定字典类型的全部信息,比如上述所说的填充下拉框

2.5.2. 效果展示

获取全部字典:

7e020882-b37f-11ed-bfe3-dac502259ad0.png

获取所有字典类型:

7e15eece-b37f-11ed-bfe3-dac502259ad0.png

获取指定字段类型的全部信息:

7e2409d2-b37f-11ed-bfe3-dac502259ad0.png

2.6. 输出适配器

输出适配器主要以 ORM 框架为主,同时各类 ORM 框架均提供了类型映射的扩展点,通过该扩展点可以对 CommonEnum 使用 code 进行存储。

2.6.1. MyBatis 支持

MyBatis 作为最流行的 ORM 框架,提供了 TypeHandler 用于处理自定义的类型扩展。

@MappedTypes(NewsStatus.class)
publicclassMyBatisNewsStatusHandlerextendsCommonEnumTypeHandler{
publicMyBatisNewsStatusHandler(){
super(NewsStatus.values());
}
}

MyBatisNewsStatusHandler 通过 @MappedTypes(NewsStatus.class) 对其进行标记,以告知框架该 Handler 是用于 NewsStatus 类型的转换。

CommonEnumTypeHandler 是为 CommonEnum 提供的通用转化能力,具体如下:

publicabstractclassCommonEnumTypeHandler&CommonEnum>
extendsBaseTypeHandler{
privatefinalListcommonEnums;

protectedCommonEnumTypeHandler(T[]commonEnums){
this(Arrays.asList(commonEnums));
}

protectedCommonEnumTypeHandler(ListcommonEnums){
this.commonEnums=commonEnums;
}

@Override
publicvoidsetNonNullParameter(PreparedStatementpreparedStatement,inti,Tt,JdbcTypejdbcType)throwsSQLException{
preparedStatement.setInt(i,t.getCode());
}

@Override
publicTgetNullableResult(ResultSetresultSet,StringcolumnName)throwsSQLException{

intcode=resultSet.getInt(columnName);
returncommonEnums.stream()
.filter(commonEnum->commonEnum.match(String.valueOf(code)))
.findFirst()
.orElse(null);
}

@Override
publicTgetNullableResult(ResultSetresultSet,inti)throwsSQLException{
intcode=resultSet.getInt(i);
returncommonEnums.stream()
.filter(commonEnum->commonEnum.match(String.valueOf(code)))
.findFirst()
.orElse(null);
}

@Override
publicTgetNullableResult(CallableStatementcallableStatement,inti)throwsSQLException{
intcode=callableStatement.getInt(i);
returncommonEnums.stream()
.filter(commonEnum->commonEnum.match(String.valueOf(code)))
.findFirst()
.orElse(null);
}
}

由于逻辑比较简单,在此不做过多解释。

有了类型之后,需要在 spring boot 的配置文件中指定 type-handler 的加载逻辑,具体如下:

mybatis:
type-handlers-package:com.geekhalo.lego.enums.mybatis

完成配置后,使用 Mapper 对数据进行持久化,数据表中存储的便是 code 信息,具体如下:

7e326b76-b37f-11ed-bfe3-dac502259ad0.png

2.6.2. JPA 支持

随着 Spring data 越来越流行,JPA 又焕发出新的活力,JPA 提供 AttributeConverter 以对属性转换进行自定义。

首先,构建 JpaNewsStatusConverter,具体如下:

publicclassJpaNewsStatusConverterextendsCommonEnumAttributeConverter{
publicJpaNewsStatusConverter(){
super(NewsStatus.values());
}
}

CommonEnumAttributeConverter 为 CommonEnum 提供的通用转化能力,具体如下:

publicabstractclassCommonEnumAttributeConverter&CommonEnum>
implementsAttributeConverter{
privatefinalListcommonEnums;

publicCommonEnumAttributeConverter(E[]commonEnums){
this(Arrays.asList(commonEnums));
}

publicCommonEnumAttributeConverter(ListcommonEnums){
this.commonEnums=commonEnums;
}

@Override
publicIntegerconvertToDatabaseColumn(Ee){
returne.getCode();
}

@Override
publicEconvertToEntityAttribute(Integercode){
return(E)commonEnums.stream()
.filter(commonEnum->commonEnum.match(String.valueOf(code)))
.findFirst()
.orElse(null);
}
}

在有了 JpaNewsStatusConverter 之后,我们需要在 Entity 的属性上增加配置信息,具体如下:

@Entity
@Data
@Table(name="t_jpa_news")
publicclassJpaNewsEntity{
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
privateLongid;

@Convert(converter=JpaNewsStatusConverter.class)
privateNewsStatusstatus;
}

@Convert(converter = JpaNewsStatusConverter.class) 是对 status 的配置,使用 JpaNewsStatusConverter 进行属性的转换。

运行持久化指令后,数据库如下:

7e45c996-b37f-11ed-bfe3-dac502259ad0.png






审核编辑:刘清

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 适配器
    +关注

    关注

    9

    文章

    2073

    浏览量

    70003
  • JAVA
    +关注

    关注

    20

    文章

    2990

    浏览量

    111301
  • MVC
    MVC
    +关注

    关注

    0

    文章

    73

    浏览量

    14180
  • JSON
    +关注

    关注

    0

    文章

    122

    浏览量

    7466

原文标题:看看人家在接口中使用枚举类型的方式,那叫一个优雅!

文章出处:【微信号:芋道源码,微信公众号:芋道源码】欢迎添加关注!文章转载请注明出处。

收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    使用枚举类型表示状态机进入死循环

    定义状态机中的状态时,除了可以使用宏(define)或者参数(parameter)声明定义外,还可以使用枚举类型
    的头像 发表于 11-07 17:46 ?1444次阅读
    使用<b class='flag-5'>枚举</b><b class='flag-5'>类型</b>表示状态机进入死循环

    详细讲解c语言enum枚举类型

    详细讲解C语言enum枚举类型实际应用中,有的变量只有几种可能取值。如人的性别只有两种可能取值,星期只有七种可能取值。 C 语言中对这样取值比较特殊的变量可以定义为
    发表于 12-21 23:22

    枚举enum的使用

    }SysError_TypeDef;//创建一个枚举变量就可以这样SysError_TypeDefSysError;个人比较喜欢第三种方式。另外就是使用过程中,遇到这样一个情况。要列举出系统错误
    发表于 02-06 15:09

    深入理解java枚举类型enum用法

    enum 的全称为 enumeration, 是 JDK 1.5 中引入的新特性,存放在 java.lang 包中。 下面是我使用 enum 过程中的一些经验和总结。 原始的接口定义常量
    发表于 09-27 11:49 ?0次下载

    枚举是C语言中的一种基本数据类型

    C语言中,枚举类型是被当做 int 或者 unsigned int 类型来处理的,所以按照 C 语言规范是没有办法遍历枚举
    的头像 发表于 09-25 15:45 ?8981次阅读
    <b class='flag-5'>枚举</b>是C语言中的一种基本数据<b class='flag-5'>类型</b>

    go语言枚举类型怎么用

    go 语言枚举类型是这么用的?什么场景下会用到枚举?本文对 go 语言枚举做了详细讲解。 枚举
    的头像 发表于 09-02 09:43 ?5522次阅读

    数字硬件建模SystemVerilog-枚举数据类型

    上一节介绍了已经被淘汰的$unit声明空间,今天我们来看看一种重要的数据类型-枚举数据类型
    的头像 发表于 07-01 17:44 ?2226次阅读

    SystemVerilog中枚举类型的使用建议

    SystemVerilog中枚举类型虽然属于一种“强类型”,但是枚举类型还是提供了一些“不正经”的用法可以实现一些很常见的功能,本文将示例一
    的头像 发表于 09-01 14:20 ?2164次阅读

    浅析SystemVerilog中的枚举类型

    枚举类型定义了一组具有名称的值,没有指定值时默认是int型数值。
    的头像 发表于 10-13 09:44 ?2175次阅读

    枚举类型的简单扩展学习

    定义枚举类型的值只能是整型常量,正数、负数和零都可以,但不可以是浮点数
    的头像 发表于 05-25 15:45 ?884次阅读

    接口中的有效数据类型和布局

    接口中包含有块所用局部变量和局部常量的声明。这些变量可分为以下两组。
    的头像 发表于 06-10 11:33 ?1376次阅读
    块<b class='flag-5'>接口中</b>的有效数据<b class='flag-5'>类型</b>和布局

    Java枚举的特点及用法

    Java 枚举出现之前,通常会使用常量类来表示一组固定的常量值,直到Java 1.5之后推出了枚举,那么枚举类型有哪些特点,它比常量类又
    的头像 发表于 09-30 10:02 ?2082次阅读

    java switch case值能为枚举值吗

    Java中的switch语句可以接受枚举类型的值作为参数。Java中,枚举是一种特殊的数据类型,它定义了一个固定数量的命名常量。因此,可以
    的头像 发表于 11-30 14:41 ?6439次阅读

    枚举有多大?c语言枚举end的作用是什么?

    枚举有多大?c语言枚举end的作用是什么? 枚举C语言中是一种常见的数据类型,用于定义一组相互关联的常量或者变量。它通常用于表示一系列可能
    的头像 发表于 01-19 14:19 ?1005次阅读

    看看人家都用树莓派5做了什么产品?

    看看人家都用树莓派5做了什么产品?
    的头像 发表于 03-25 09:46 ?378次阅读
    <b class='flag-5'>看看人家</b>都用树莓派5做了什么产品?