场景
典型场景为DTO的参数有类型,例如lessonType,这种是典型的枚举类型。
过去的做法
过去的做法是,DTO这个参数为String类型,在controller的开始对这个参数进行校验。
这种实现最大的问题是,逻辑不聚焦,DTO参数的校验,最好在DTO本身完成,不应该逃逸到其他领域。
更佳的工程实践
Spring也意识到这个问题,因此,给出了Converter解决方案,这里,就是综合应用Spring的ConverterFactory和自定义的JSON反序列化方法来实现
这个工程实践有如下优点:
- 【代码单一职责】无需在controller层进行参数校验,校验代码封闭在DTO内
- 【更广域的入参值】不再仅仅限制为Enum的Value,而可以是Enum的其他属性等
- 【HTTP的所有方法】不再仅局限在GET方法的RequestParam中,而是可以支持POST/PUT等所使用的RequestBody中
- 【Convert通用工厂】不再需要每个枚举类型写一套Converter等代码,ConverterFactory支持通用类型Enum,代码更精简
具体工程实现
在DTO中应用Enum类型
@Data public class LessonDTO { private String id; private ESparringWorkflowType lessonType; private String lessonName; private String lessonIntroduction; }
定义一个Enum类型
其中,fromString方法是负责将前端传入的String类型,转换为DTO的Enum类型。
代码实现上,即支持Enum值的匹配也支持code值的匹配
注意:如果都匹配不上,应该抛出异常,而不是网上的返回null。因为抛出异常,才能被捕获,实现自动返回错误码给前端。如果是返回null,将会流入controller代码,导致null错误。
入参有2种情况:
- 一种是POST/PUT的RequestBody的JSON Payload。
- 一种是GET的RequestParam参数;
对于POST/PUT的参数,使用Jackson的如下注解:
@JsonCreator(mode = JsonCreator.Mode.DELEGATING)
WebMVC在反序列化这个枚举类型时,就会使用这个静态方法来转换。
@Getter public enum ESparringWorkflowType implements RequestParamEnum { SYNC("sync", "从谛听平台同步过来的陪练流程类型"), DIALOGUE("dialogue", "对话陪练"), OPERATION("operation", "操作陪练"), MIX("mix", "混合陪练"); String code; String desc; ESparringWorkflowType(String code, String desc) { this.code = code; this.desc = desc; } @JsonCreator(mode = JsonCreator.Mode.DELEGATING) public static ESparringWorkflowType fromString(String code){ try{ return ESparringWorkflowType.valueOf(code.toUpperCase()); }catch(ConversionFailedException error){ for(ESparringWorkflowType eSparringWorkflowType: ESparringWorkflowType.values()){ if(eSparringWorkflowType.getCode().equals(code)){ return eSparringWorkflowType; } } throw new IllegalArgumentException("lessonType error"); } } @Override public ESparringWorkflowType paramTransfer(String param){ return ESparringWorkflowType.fromString(param); } }
如果入参不对,Spring框架自动返回错误码
为了将参数校验代码封闭在DTO中,必然不能在业务代码中实现返回异常Code等处理。
这里采用Spring的机制实现如下:
当controller层抛出对应的异常,这里负责捕获并处理。
注意:虽然Enum类里抛出的是IllegalArgumentException,但是这会导致WebMVC读消息失败,导致抛出另一个异常 HttpMessageNotReadableException,所以应该捕获是这个异常。
(下面的ResultVO和ResultUtil是自定义的类,可以忽略)
@ControllerAdvice public class GlobalControllerExceptionHandler { @ExceptionHandler(HttpMessageNotReadableException.class) public ResponseEntity<ResultVO> handleConflict(RuntimeException ex) { return new ResponseEntity<>(ResultUtil.error(ResultEnum.LESSON_MANAGEMENT_LESSON_TYPE_NOT_EXISTS), HttpStatus.BAD_REQUEST); } }
如果只关系POST和PUT的方法,则本文结束。
如果也需要处理GET方法的参数,请继续阅读。
GET方法的RequestParam需要使用Spring的Convert技术实现。
定义一个接口
public interface RequestParamEnum { <E extends RequestParamEnum> E paramTransfer(String param); }
实现接口
ESparringWorkflowType implements RequestParamEnum
@Override public ESparringWorkflowType paramTransfer(String param){ return ESparringWorkflowType.fromString(param); }
实现自定义的枚举转换工厂
这个枚举转换工厂和Spring默认的ConvertFactory 通过是否继承 RequestParamEnum来区分。
Spring默认的枚举转换只能通过Enum.valueOf来实现,限制多。
public class EnumConverterFactory implements ConverterFactory<String, RequestParamEnum> { @Override public <T extends RequestParamEnum> Converter<String, T> getConverter(Class<T> targetType) { return source -> targetType.getEnumConstants()[0].paramTransfer(source); }; }
让这个枚举转换工厂应用在WebMVC中
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addFormatters(FormatterRegistry registry){ registry.addConverterFactory(new EnumConverterFactory()); } }
Controller里的参数默认值
@RequestParam(defaultValue = "") ESparringWorkflowType lessonType
一般而言,lessonType不传时,应代表查询所有类型,那defaultValue应该填哪个enum的name?似乎都不合适
这里为空字符串,并不是enum内的name,实际前端不传lessonType,这里的lessonType为null。
后续必须假设enum可能为null的情况