0%

尚硅谷尚庭公寓-01

枚举类型转换(WebDataBinder、TypeHandler、HTTPMessageConverter)

为什么要用枚举类

  • 实体类中的公共字段(例如idcreate_timeupdate_timeis_deleted)抽取到一个基类,进行统一管理,然后让各实体类继承该基类。

  • 实体类中的状态字段(例如status)或类型字段(例如type),全部使用枚举类型。

    状态(类型)字段,在数据库中通常用一个数字表示一个状态(类型)。例如:订单状态(1:待支付,2:待发货,3:待收货,4:已收货,5:已完结)。若实体类中对应的字段也用数字类型,例如int,那么程序中就会有大量的如下代码:

    1
    2
    3
    4
    order.setStatus(1);
    if (order.getStatus() == 1) {
    order.setStatus(2);
    }

    这些代码后期维护起来会十分麻烦,所以本项目中所有的此类字段均使用枚举类型。例如上述订单状态可定义为以下枚举:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public enum Status {

    CANCEL(0, "已取消"),
    WAIT_PAY(1, "待支付"),
    WAIT_TRANSFER(2, "待发货"),
    WAIT_RECEIPT(3, "待收货"),
    RECEIVE(4, "已收货"),
    COMPLETE(5, "已完结");

    private final Integer value;
    private final String desc;

    public Integer value() {
    return value;
    }
    public String desc() {
    return desc;
    }
    }

    订单实体类中的状态字段定义为Status类型:

    1
    2
    3
    4
    5
    6
    7
    @Data
    public class Order{
    private Integer id;
    private Integer userId;
    private Status status;
    ...
    }

    这样上述代码便可调整为如下效果,后期维护起来会容易许多。

    1
    order.setStatus(Status.WAIT_PAY);

转换场景

在[根据类型]查询标签列表API中,前端携带的参数为枚举类的code变量值,在controller层需要将code转换为完整的枚举类,需要使用WebDataBinder进行转换,在于数据库的交互中,需要将枚举类的type值拿出来与数据库中的字段匹配,需要使用Mybatis-PlusTypeHandler进行转换,在返回数据至前端的过程中,又需要使用HTTPMessageConverterController方法的返回值(Java对象)转换为HTTP响应体中的JSON字符串。

1
2
3
4
5
6
7
8
9
@Operation(summary = "(根据类型)查询标签列表")
@GetMapping("list")
public Result<List<LabelInfo>> labelList(@RequestParam(required = false) ItemType type) {

LambdaQueryWrapper<LabelInfo> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(type != null, LabelInfo::getType, type);
List<LabelInfo> list = service.list(queryWrapper);
return Result.ok(list);
}

上述接口的功能是根据type(公寓/房间),查询标签列表。由于这个type字段在数据库、实体类、前后端交互的过程中有多种不同的形式,因此在请求和响应的过程中,type字段会涉及到多次类型转换。

首先明确一下type字段的各种形式:

  • 数据库中

    数据库中的type字段为tinyint类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    +-------------+--------------+
    | Field | Type |
    +-------------+--------------+
    | id | bigint |
    | type | tinyint |
    | name | varchar(255) |
    | create_time | timestamp |
    | update_time | timestamp |
    | is_deleted | tinyint |
    +-------------+--------------+
  • 实体类

    实体类中的type字段为ItemType枚举类型

    LabelInfo实体类如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @Schema(description = "标签信息表")
    @TableName(value = "label_info")
    @Data
    public class LabelInfo extends BaseEntity {

    private static final long serialVersionUID = 1L;

    @Schema(description = "类型")
    @TableField(value = "type")
    private ItemType type;

    @Schema(description = "标签名称")
    @TableField(value = "name")
    private String name;
    }

    ItemType枚举类如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public enum ItemType {

    APARTMENT(1, "公寓"),
    ROOM(2, "房间");

    private Integer code;
    private String name;

    ItemType(Integer code, String name) {
    this.code = code;
    this.name = name;
    }
    }
  • 前后端交互中

    前后端交互所传递的数据中type字段为数字(1/2)。

具体转换过程如下图所示:

  • 请求流程

    说明

    • SpringMVC中的WebDataBinder组件负责将HTTP的请求参数绑定到Controller方法的参数,并实现参数类型的转换。
    • Mybatis中的TypeHandler用于处理Java中的实体对象与数据库之间的数据类型转换。
  • 响应流程

    说明

    • SpringMVC中的HTTPMessageConverter组件负责将Controller方法的返回值(Java对象)转换为HTTP响应体中的JSON字符串,或者将请求体中的JSON字符串转换为Controller方法中的参数(Java对象),例如下一个接口保存或更新标签信息

WebDataBinder枚举类型和属性的转换

WebDataBinder依赖于Converter实现类型转换,若Controller方法声明的@RequestParam参数的类型不是StringWebDataBinder就会自动进行数据类型转换。SpringMVC提供了常用类型的转换器,例如StringIntegerStringDateStringBoolean等等,其中也包括String到枚举类型,但是String到枚举类型的默认转换规则是根据实例名称(”APARTMENT”)转换为枚举对象实例(ItemType.APARTMENT)。若想实现code属性到枚举对象实例的转换,需要自定义Converter,代码如下,具体内容可参考官方文档

  • web-admin模块自定义com.atguigu.lease.web.admin.custom.converter.StringToItemTypeConverter

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Component
    public class StringToItemTypeConverter implements Converter<String, ItemType> {
    @Override
    public ItemType convert(String code) {

    for (ItemType value : ItemType.values()) {
    if (value.getCode().equals(Integer.valueOf(code))) {
    return value;
    }
    }
    throw new IllegalArgumentException("code非法");
    }
    }
  • 注册上述的StringToItemTypeConverter,在web-admin模块创建com.atguigu.lease.web.admin.custom.config.WebMvcConfiguration,内容如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Configuration
    public class WebMvcConfiguration implements WebMvcConfigurer {

    @Autowired
    private StringToItemTypeConverter stringToItemTypeConverter;

    @Override
    public void addFormatters(FormatterRegistry registry) {
    registry.addConverter(this.stringToItemTypeConverter);
    }
    }

但是我们有很多的枚举类型都需要考虑类型转换这个问题,按照上述思路,我们需要为每个枚举类型都定义一个Converter,并且每个Converter的转换逻辑都完全相同,针对这种情况,我们使用ConverterFactory接口更为合适,这个接口可以将同一个转换逻辑应用到一个接口的所有实现类,因此我们可以定义一个BaseEnum接口,然后另所有的枚举类都实现该接口,然后就可以自定义ConverterFactory,集中编写各枚举类的转换逻辑了。具体实现如下:

  • model模块定义com.atguigu.lease.model.enums.BaseEnum接口

    1
    2
    3
    4
    public interface BaseEnum {
    Integer getCode();
    String getName();
    }
  • 令所有com.atguigu.lease.model.enums包下的枚举类都实现BaseEnun接口

  • web-admin模块自定义com.atguigu.lease.web.admin.custom.converter.StringToBaseEnumConverterFactory

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @Component
    public class StringToBaseEnumConverterFactory implements ConverterFactory<String, BaseEnum> {
    @Override
    public <T extends BaseEnum> Converter<String, T> getConverter(Class<T> targetType) {
    return new Converter<String, T>() {
    @Override
    public T convert(String source) {

    for (T enumConstant : targetType.getEnumConstants()) {
    if (enumConstant.getCode().equals(Integer.valueOf(source))) {
    return enumConstant;
    }
    }
    throw new IllegalArgumentException("非法的枚举值:" + source);
    }
    };
    }
    }
  • 注册上述的ConverterFactory,在web-admin模块创建com.atguigu.lease.web.admin.custom.config.WebMvcConfiguration,内容如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Configuration
    public class WebMvcConfiguration implements WebMvcConfigurer {

    @Autowired
    private StringToBaseEnumConverterFactory stringToBaseEnumConverterFactory;

    @Override
    public void addFormatters(FormatterRegistry registry) {
    registry.addConverterFactory(this.stringToBaseEnumConverterFactory);
    }
    }

    注意:

    最终采用的是ConverterFactory方案,因此StringToItemTypeConverter相关代码可以直接删除。

TypeHandler枚举类型和属性的转换

Mybatis预置的TypeHandler可以处理常用的数据类型转换,例如StringIntegerDate等等,其中也包含枚举类型,但是枚举类型的默认转换规则是枚举对象实例(ItemType.APARTMENT)和实例名称(”APARTMENT”)相互映射。若想实现code属性到枚举对象实例的相互映射,需要自定义TypeHandler

不过MybatisPlus提供了一个通用的处理枚举类型的TypeHandler。其使用十分简单,只需在ItemType枚举类的code属性上增加一个注解@EnumValue,Mybatis-Plus便可完成从ItemType对象到code属性之间的相互映射,具体配置如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public enum ItemType {

APARTMENT(1, "公寓"),
ROOM(2, "房间");

@EnumValue
private Integer code;
private String name;

ItemType(Integer code, String name) {
this.code = code;
this.name = name;
}
}

HTTPMessageConverter实现枚举类型和属性的转换

HttpMessageConverter依赖于Json序列化框架(默认使用Jackson)。其对枚举类型的默认处理规则也是枚举对象实例(ItemType.APARTMENT)和实例名称(”APARTMENT”)相互映射。不过其提供了一个注解@JsonValue,同样只需在ItemType枚举类的code属性上增加一个注解@JsonValue,Jackson便可完成从ItemType对象到code属性之间的互相映射。具体配置如下,详细信息可参考Jackson官方文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Getter
public enum ItemType {

APARTMENT(1, "公寓"),
ROOM(2, "房间");

@EnumValue
@JsonValue
private Integer code;
private String name;

ItemType(Integer code, String name) {
this.code = code;
this.name = name;
}
}