96SEO 2026-02-20 08:09 0
联系qq184480602加我进群大家一起学习一起进步一起对抗互联网寒冬

上一篇我们通过编写MyBatis的转换器最终完成枚举在DAO层和数据库之间的转换
现在让我们把目光往前移思考一下如何编写SpringMVC的转换器完成前端与Controller层的枚举转换。
pom.xml小册使用的版本都是2.3.4但今天遇到坑了后面会提到
xmlnshttp://maven.apache.org/POM/4.0.0
xmlns:xsihttp://www.w3.org/2001/XMLSchema-instancexsi:schemaLocationhttp://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsdmodelVersion4.0.0/modelVersionparentgroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-parent/artifactIdversion2.3.4.RELEASE/versionrelativePath/
--/parentgroupIdcom.example/groupIdartifactIdspringboot_enum/artifactIdversion0.0.1-SNAPSHOT/versionnamespringboot_enum/namedescriptionDemo
Boot/descriptionpropertiesjava.version1.8/java.version/propertiesdependenciesdependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependencydependencygroupIdorg.projectlombok/groupIdartifactIdlombok/artifactIdoptionaltrue/optional/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-test/artifactIdscopetest/scopeexclusionsexclusiongroupIdorg.junit.vintage/groupIdartifactIdjunit-vintage-engine/artifactId/exclusion/exclusions/dependency/dependenciesbuildpluginsplugingroupIdorg.springframework.boot/groupIdartifactIdspring-boot-maven-plugin/artifactId/plugin/plugins/build/projectPOJO
{log.info(userDTO.toString());}PostMapping(/postForm)public
{log.info(userDTO.toString());}PostMapping(/postJson)public
{log.info(userDTO.toString());}}
GET和POST有个很大区别是GET请求的参数放在请求行而POST请求的参数放在请求体(Body)。
form-datax-www-form-urlencodedjson
如果你足够细心平时使用Postman时就会注意到以上三种POST请求形式虽不同但参数都在Body
需要注意的是从后端接口参数的格式看POST请求中的表单提交方式和GET请求是很相似的
枚举名称(name)分别叫STUDENTTEACHER之前分析过所有的枚举类默认继承Enum而Enum重写了toString()
所以当我们打印STUDENT或TEACHER对象时最终会打印:
UserTypeEnum有两个字段type和desc抽象父类Enum也有两个字段ordinal序号从0开始和name枚举名称
测试请求时我们的关注点是前端传入userType:STUDENT后端是如何变成UserTypeEnum对象的。
很明显前端传STUDENT、TEACHER等枚举名称(name)时SpringMVC能自动帮我们转为对应的枚举对象而在实际打印时由于调用了toString()所以显示userTypeSTUDENT。
那么为什么枚举名称name为什么会自动转为枚举对象UserTypeEnum呢我们先不管SpringMVC怎么做到的通过断点很容易发现SpringMVC在解析STUDENT这个字符串时最终调用了Enum#valueOf()然后根据name获取枚举对象
无论是GET还是POST表单传入0或1都失败了ordinal从0开始也就是说SpringMVC默认不支持根据ordinal转换
[org.springframework.validation.BindException:
org.springframework.validation.BeanPropertyBindingResult:
[typeMismatch.userDTO.userType,typeMismatch.userType,typeMismatch.com.bravo.demo.enums.UserTypeEnum,typeMismatch];
[org.springframework.context.support.DefaultMessageSourceResolvable:
com.bravo.demo.enums.UserTypeEnum
org.springframework.core.convert.ConversionFailedException:
[com.bravo.demo.enums.UserTypeEnum]
java.lang.IllegalArgumentException:
com.bravo.demo.enums.UserTypeEnum.1]]
也就是说对于GET/POST表单请求SpringMVC都是根据valueOf()来匹配枚举对象的。
也即是说对于GET和POST表单请求而言如果想正确的反序列化String转为Enum对象前端只能传Enum.name。
对于前端来说他们可能更喜欢传递枚举内部的字段比如UserTypeEnum.type而不是Enum.name。
有没有办法更改SpringMVC的默认行为当前端传递userType1时把1转为UserTypeEnum的“学生”对象呢
了解GET/POST表单请求时SpringMVC默认的转换机制改写这个机制
由于我们已经知道整个请求链路的终点是调用Enum#valueOf()进行转换于是给valueOf()打上断点
省略中间的步骤根据调用链进行反推很快定位到AbstractPropertyAccessor#setPropertyValues()
这是个for循环它拿到了UserDTO的所有属性并逐个进行赋值。
比如截图的代码显示SpringMVC正在给UserDTO.userType字段赋值。
再往下走几步会看到GenericConversionService#convert()
找到converter后调用converter的convert()方法进行值转换
我们发现SpringMVC默认的枚举转换器是StringToEnumConverterFactory
它的convert()方法正好调用了Enum.valueOf()所以GET/POST表单请求时只能传Enum.name至此真相大白。
前端发起请求传递userTypeSTUDENT从Tomcat的Servlet到SpringMVC的Controller中间要经过很多类和方法SpringMVC会解析入参对象的每一个字段选取合适的ConverterFactory为其进行转换默认使用StringToEnumConverterFactory为枚举类型进行转换即调用Enum.valueOf(name)
有了上面的铺垫关于GET/POST表单请求时如何自定义枚举入参转换器已经很明确了。
自定义枚举转换器直接抄StringToEnumConverterFactory**
StringToEnum(targetType);}private
StringToEnumConverterFactory默认是调用Enum.valueOf()也就是根据Enum.name匹配*
(source.equals(String.valueOf(enumObject.ordinal())))
addFormatters(FormatterRegistry
把我们自定义的枚举转换器添加到Spring容器Spring容器会把它加入到SpringMVC的拦截链路中registry.addConverterFactory(new
特别特别注意把MyEnumConverterFactory加入调用链后jackson原本的StringToEnumConverterFactory就不起作用了此时前端传入STUDENT、TEACHER将无法成功解析。
上面这样还是无法满足我们的需求我们只是把原先默认支持Enum.name改为Enum.ordinal。
部分同学可能有疑问你刚才为什么不直接在上面的ConverterFactory中调用getType()或者getDesc()呢
getType()/getDesc不够通用项目中其他枚举可能叫getValue()/getDescription()最重要的是class
Enum使用Enum限定内部元素只能使用父类Enum的方法无法直接调用getType()等方法
抽取公共的IEnum接口强制指定按哪个字段反序列化使用注解反射
StringToEnum(targetType);}private
默认项目中所有Enum都实现了IEnum那么必然有getValue()if
(source.equals(String.valueOf(enumObject.getValue())))
Retention(RetentionPolicy.RUNTIME)
MyEnumConverterFactory主要负责第2、3步**
StringToEnum(targetType);}private
enumObject.getClass().getDeclaredFields();for
(declaredField.isAnnotationPresent(MyJsonCreator.class))
{declaredField.setAccessible(true);//
declaredField.get(enumObject);//
(source.equals(String.valueOf(fieldValue)))
我们惊奇的发现SpringMVC默认就支持了Enum.name和Enum.ordinal的转换但对于子类UserTypeEnum的特有字段type、desc是不识别的。
GET/POST表单默认使用StringToEnumConverterFactory只支持Enum.namePOST
JSON和GET/POST表单使用的不是同一个转换器并且从上面的异常信息可以捕捉到一丝丝信息
[org.springframework.http.converter.HttpMessageNotReadableException:
com.bravo.demo.enums.UserTypeEnum
com.fasterxml.jackson.databind.exc.InvalidFormatException:
com.bravo.demo.enums.UserTypeEnum
com.bravo.demo.pojo.UserDTO[userType])]
jacksonSpringBoot内置fastjson阿里gson谷歌
SpringBoot默认使用jackson作为JSON转换工具比如我们经常会用的ObjectMapper其实就是jackson的。
JSON请求RequestBody响应ResponseBody
GET或POST表单请求由于参数并不是JSON形式所以用不到jackson只需要实现ConverterFactory
JSON请求则需要实现HttpMessageConverterjackson已经提供
请注意ConverterFactory和HttpMessageConverter两个接口的包路径都不一样并没有什么关联。
由于JSON请求本质是字符串所以必须要有反序列化的过程。
SpringMVC对外提供了HttpMessageConverter接口用于处理JSON而SpringBoot内置的jackson提供了该接口的实现类MappingJackson2HttpMessageConverter
当一个JSON请求达到SpringMVC容器会根据为当前请求参数挑选合适的Converter
此时就轮到jackson的MappingJackson2HttpMessageConverter出场了。
如果你跟着debug就会发现实际上大部分工作都是AbstractJackson2HttpMessageConverter干的jackson的主要贡献是提供了ObjectMapper实例及各种Serializer、Deserializer用于序列化和反序列化
AbstractJackson2HttpMessageConverter内部的ObjectMapper被赋值后通过构造器如果有请求到达SpringMVC它会调用ObjectMapperSerializer、Deserializer对参数进行转换。
比如EnumDeserializer默认支持转换Enum.name、Enum.ordinal
具体的源码就不在这里带大家跟读了我们会在Spring章节分析RequestBody时解释目前大家可以像下面截图一样打上断点然后用Postman分别传递数字(Enum.ordinal)或字符串(Enum.name)体会一下
你会发现jackson的EnumDeserializer默认的解析策略是
如果你刚好是从上一篇文章过来的就会发现jackson的策略和MyBatis很像都支持了Enum.name和Enum.ordinal的转换。
那么如果前端传递的是UserTypeEnum.type或者UserTypeEnum.desc呢
好在jackson还提供了JsonCreator注解让我们自己指定反序列化的字段
用JsonValue指定序列化字段后面再介绍不用管*/JsonValueprivate
UserDTO.class);System.out.println(userDTO);//
objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(userDTO);System.out.println(returnJson);}
看起来很完美啊但是你用Postman去请求就会报错。
我调试了一晚上坑爹结果发现是当前SpringBoot版本问题2.3.3SpringBoot2.0.x是可以的估计2.3.3修改了jackson的默认设置
介绍完前端如何传入枚举参数入最后讲讲枚举如何响应给前端出。
其实答案已经呼之欲出JsonValue但方案不止一种。
在测试前请大家修改Controller让接口返回UserDto
{log.info(userDTO.toString());return
userDTO;}PostMapping(/postForm)public
{log.info(userDTO.toString());return
userDTO;}PostMapping(/postJson)public
{log.info(userDTO.toString());return
把之前请求相关的配置先注释掉并把SpringBoot版本改为2.0.5
OK我们自定义MyEnumConverterFacotry注释后对于GET/POST表单请求重新使用默认的StringToEnumConverterFactory仅支持Enum.name反序列化。
而POST
JSON请求默认支持Enum.name和Enum.ordinal。
现在你可以认为代码都回到了最初创建SpringBoot项目的状态。
由于这回是测试响应形式我们不关心入参所以统一传递大家都支持的Enum.name。
JSON请求它们只是请求方式不同而响应形式其实都是JSON因为我们使用了RestController
至于使用了ResponseBody后SpringMVC如何处理返回值由于篇幅已经太长留到Spring部分再聊。
但有一点可以肯定正如JSON请求那样JSON响应也会经过jackson的处理而且必然调用HttpMessageConverter的write()。
中间复杂的调用就跳过了直接看AbstractJackson2HttpMessageConverter#writeInternal()
即最终会调用objectWriter.writeValue(generator,
value)进行序列化写入response缓冲区。
我们注意到在调用writeValue()之前userType字段还是个UserTypeEnum对象
STUDENT这和SpringMVC本身没什么关系取决于JSON转换工具怎么设计的而jackson默认就是调用Enum.name()。
在需要序列化的字段上加JsonValue即可。
特别注意对于POST
JSON请求使用JsonValue必须配合使用JsonCreator否则会报错很难受
做了上面的设置相当于告诉jackson序列化响应时调用对象的toString()即可相应地我们要重写toString()
自定义JSON响应时枚举字段的序列化行为调用toString()**
Jackson2ObjectMapperBuilderCustomizer
builder.featuresToEnable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING);
没想到吧UserTypeEnum序列化后竟然是完全无关的文字~;}
SpringMVC对请求和响应的处理原本就复杂再加上枚举使得整篇文章难度加大不少。
很多同学可能有点晕这里总结一下并尝试给出我推荐的方案。
JSONJsonCreatorGET/POSTMyJsonCreator
JSON请求反序列字段请用jackson原生注解JsonCreator**
Retention(RetentionPolicy.RUNTIME)
MyEnumConvertFactoryMyJsonCreator指定GET/POST表单请求根据哪个字段反序列化*/MyJsonCreatorprivate
addFormatters(FormatterRegistry
把我们自定义的枚举转换器添加到Spring容器Spring容器会把它加入到SpringMVC的拦截链路中registry.addConverterFactory(new
MyEnumConverterFactory());}/***
自定义JSON响应时枚举字段的序列化行为调用toString()**
Jackson2ObjectMapperBuilderCustomizer
builder.featuresToEnable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING);}}
MyEnumConverterFactory主要负责第2、3步**
StringToEnum(targetType);}private
enumObject.getClass().getDeclaredFields();for
(declaredField.isAnnotationPresent(MyJsonCreator.class))
{declaredField.setAccessible(true);//
declaredField.get(enumObject);//
(source.equals(String.valueOf(fieldValue)))
我个人实际开发时无论是Controller层还是DAO层都习惯手动转换枚举。
作者简介大家好我是smart哥前中兴通讯、美团架构师现某互联网公司CTO
作为专业的SEO优化服务提供商,我们致力于通过科学、系统的搜索引擎优化策略,帮助企业在百度、Google等搜索引擎中获得更高的排名和流量。我们的服务涵盖网站结构优化、内容优化、技术SEO和链接建设等多个维度。
| 服务项目 | 基础套餐 | 标准套餐 | 高级定制 |
|---|---|---|---|
| 关键词优化数量 | 10-20个核心词 | 30-50个核心词+长尾词 | 80-150个全方位覆盖 |
| 内容优化 | 基础页面优化 | 全站内容优化+每月5篇原创 | 个性化内容策略+每月15篇原创 |
| 技术SEO | 基本技术检查 | 全面技术优化+移动适配 | 深度技术重构+性能优化 |
| 外链建设 | 每月5-10条 | 每月20-30条高质量外链 | 每月50+条多渠道外链 |
| 数据报告 | 月度基础报告 | 双周详细报告+分析 | 每周深度报告+策略调整 |
| 效果保障 | 3-6个月见效 | 2-4个月见效 | 1-3个月快速见效 |
我们的SEO优化服务遵循科学严谨的流程,确保每一步都基于数据分析和行业最佳实践:
全面检测网站技术问题、内容质量、竞争对手情况,制定个性化优化方案。
基于用户搜索意图和商业目标,制定全面的关键词矩阵和布局策略。
解决网站技术问题,优化网站结构,提升页面速度和移动端体验。
创作高质量原创内容,优化现有页面,建立内容更新机制。
获取高质量外部链接,建立品牌在线影响力,提升网站权威度。
持续监控排名、流量和转化数据,根据效果调整优化策略。
基于我们服务的客户数据统计,平均优化效果如下:
我们坚信,真正的SEO优化不仅仅是追求排名,而是通过提供优质内容、优化用户体验、建立网站权威,最终实现可持续的业务增长。我们的目标是与客户建立长期合作关系,共同成长。
Demand feedback