作为一个"合格"的coder,理应发出如题这样的疑问。
譬如上例我这么写,你可以猜猜是什么结果:
@ResponseBody @GetMapping("/test/curruser") public Object testCurrUser(@CurrUser @RequestParam CurrUserVo currUser) { return currUser; }
表面上看起来木有毛病,但请求:/test/curruser?currUser=fsx。报错如下:
Resolved [org.springframework.web.method.annotation.MethodArgumentConversionNotSupportedException: Failed to convert value of type 'java.lang.String' to required type 'com.fsx.validator.CurrUserVo'; nested exception is java.lang.IllegalStateException: Cannot convert value of type 'java.lang.String' to required type 'com.fsx.validator.CurrUserVo': no matching editors or conversion strategy found]
调试源码可以发现它最终使用的参数解析器是:RequestParamMethodArgumentResolver,而并非我们自定义的CurrUserArgumentResolver。so可得出结论:我们自定义的参数解析器的优先级是低于Spring内置的。
那么到底是什么样的优先级规则呢?我这里不妨给指出如下,供以大家学习:
1、首先就得从RequestMappingHandlerAdapter说起,它对参数解析器的加载(初始化)顺序:
RequestMappingHandlerAdapter: @Override public void afterPropertiesSet() { // 显然,也是允许你自己通过setArgumentResolvers()方法手动添加的~~~ // 加入你调用过set方法,这里就不会执行啦~~~~~(一般不建议手动set) if (this.argumentResolvers == null) { List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers(); this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers); } ... } private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() { List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>(); // Annotation-based argument resolution // 加载处理所有内置注解的解析器们 resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false)); resolvers.add(new RequestParamMapMethodArgumentResolver()); resolvers.add(new PathVariableMethodArgumentResolver()); ... // Type-based argument resolution // 比如request、response等等这些的解析器们 resolvers.add(new ServletRequestMethodArgumentResolver()); ... // Custom arguments // 加载自定义的解析器们(我们自定义的在这里会被加载进来) if (getCustomArgumentResolvers() != null) { resolvers.addAll(getCustomArgumentResolvers()); } // Catch-all // 加载这两个用于兜底 resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true)); resolvers.add(new ServletModelAttributeMethodProcessor(true)); return resolvers; }
2、RequestMappingHandlerAdapter
这个Bean
配置处如下:
WebMvcConfigurationSupport: @Bean public RequestMappingHandlerAdapter requestMappingHandlerAdapter() { RequestMappingHandlerAdapter adapter = createRequestMappingHandlerAdapter(); // 内容协商管理器 adapter.setContentNegotiationManager(mvcContentNegotiationManager()); // 消息转换器们 adapter.setMessageConverters(getMessageConverters()); // ConfigurableWebBindingInitializer:配置数据绑定、校验的相关配置项 adapter.setWebBindingInitializer(getConfigurableWebBindingInitializer()); // 参数解析器、返回值解析器 adapter.setCustomArgumentResolvers(getArgumentResolvers()); adapter.setCustomReturnValueHandlers(getReturnValueHandlers()); ... }
WebMvcConfigurationSupport应该没有不熟悉它的了,它用于开启WebMVC的配置支持~
从这个源码(配置顺序)中可以很清晰的得出答案:为何本例加了@RequestParam注解就访问就报错了;同样也解释了为何入参不能是Map(但Object类型是可以的~)。
在介绍场景二之前,我先介绍一个类:PropertyNamingStrategy
PropertyNamingStrategy
它表示序列化/反序列化过程中:Java属性到序列化key的一种命名策略。
默认情况下从字符串反序列为一个Java对象,要求需要完全一样才能反序列赋值成功。但了解了这些策略之后,可以帮你带来更好的兼容性,下面以最为常用的两个JSON库为例分别讲解~
Gson库对应的类叫FieldNamingStrategy,功能类似。因为我个人使用较少,所以此处忽略它~
fastjson中
fastjson在1.2.15版本(2016年6月)中提供了这个功能,它以枚举的形式管理:
public enum PropertyNamingStrategy { CamelCase, // 骆驼: PascalCase, // 帕斯卡: SnakeCase, // 蛇形: KebabCase; // 烤肉串: // 提供唯一一个实例方法:转换translate public String translate(String propertyName) { switch (this) { case SnakeCase: { ... } case KebabCase: { ... } case PascalCase: { ... } case CamelCase: { ... } } } }
针对此4种策略,给出使用用例如下:
public static void main(String[] args) { String propertyName = "nameAndAge"; System.out.println(PropertyNamingStrategy.CamelCase.translate(propertyName)); // nameAndAge System.out.println(PropertyNamingStrategy.PascalCase.translate(propertyName)); // NameAndAge // 下面两种的使用很多的情况:下划线 System.out.println(PropertyNamingStrategy.SnakeCase.translate(propertyName)); // name_and_age System.out.println(PropertyNamingStrategy.KebabCase.translate(propertyName)); // name-and-age }
继续演示使用Fastjson序列化/反序列化的时候的示例:
public static void main(String[] args) { DemoVo vo = new DemoVo(); vo.setDemoName("fsx"); vo.setDemoAge(18); vo.setDemoNameAndAge("fsx18"); PropertyNamingStrategy strategy = PropertyNamingStrategy.SnakeCase; // 序列化配置对象 SerializeConfig config = new SerializeConfig(); config.propertyNamingStrategy = strategy; // 反序列化配置对象 ParserConfig parserConfig = new ParserConfig(); parserConfig.propertyNamingStrategy = strategy; // 序列化对象 String json = JSON.toJSONString(vo, config); System.out.println("序列化vo对象到json -> " + json); // 反序列化对象 vo = JSON.parseObject(json, DemoVo.class, parserConfig); System.out.println("反序列化json到vo -> " + vo); }
运行打印:
序列化vo对象到json -> {"demo_age":18,"demo_name":"fsx","demo_name_and_age":"fsx18"} 反序列化json到vo -> Main.DemoVo(demoName=fsx, demoAge=18, demoNameAndAge=fsx18)
若策略是SnakeCase,它是支持下划线_到驼峰格式的Java属性的相互转换的。若使用另外三种,我把结果摘录如下:
CamelCase: 序列化vo对象到json -> {"demoAge":18,"demoName":"fsx","demoNameAndAge":"fsx18"} 反序列化json到vo -> Main.DemoVo(demoName=fsx, demoAge=18, demoNameAndAge=fsx18) PascalCase: 序列化vo对象到json -> {"DemoAge":18,"DemoName":"fsx","DemoNameAndAge":"fsx18"} 反序列化json到vo -> Main.DemoVo(demoName=fsx, demoAge=18, demoNameAndAge=fsx18) KebabCase: 序列化vo对象到json -> {"demo-age":18,"demo-name":"fsx","demo-name-and-age":"fsx18"} 反序列化json到vo -> Main.DemoVo(demoName=fsx, demoAge=18, demoNameAndAge=fsx18)
FastJson
默认使用CamelCase
。
题外话:除了上面那样分别在序列化时临时制定序列化、反序列化策略外,还可以用如下方式指定:
SerializeConfig.getGlobalInstance().propertyNamingStrategy = PropertyNamingStrategy.PascalCase;
@JSONType指定
@JSONType(naming = PropertyNamingStrategy.SnakeCase) private static class DemoVo { @JSONField(name = "name") private String demoName; private Integer demoAge; private Object demoNameAndAge; }
若@JSONField
没有指定name属性,那就会使用PropertyNamingStrategy
策略~
jackson中
除了fastjson
,作为全球范围内更为流行的jackson
自然也是支持此些策略的。
// was abstract until 2.7 在2.7版本之前一直是抽象类 public class PropertyNamingStrategy implements java.io.Serializable { public static final PropertyNamingStrategy SNAKE_CASE = new SnakeCaseStrategy(); public static final PropertyNamingStrategy UPPER_CAMEL_CASE = new UpperCamelCaseStrategy(); public static final PropertyNamingStrategy LOWER_CAMEL_CASE = new PropertyNamingStrategy(); public static final PropertyNamingStrategy KEBAB_CASE = new KebabCaseStrategy(); // 上面几个策略都是@since 2.7,这个基于@since 2.4 public static final PropertyNamingStrategy LOWER_CASE = new LowerCaseStrategy(); // 提供的API方法如下: public String nameForField(MapperConfig<?> config, AnnotatedField field, String defaultName); public String nameForGetterMethod(MapperConfig<?> config, AnnotatedMethod method, String defaultName); public String nameForSetterMethod(MapperConfig<?> config, AnnotatedMethod method, String defaultName); public String nameForConstructorParameter(MapperConfig<?> config, AnnotatedParameter ctorParam, String defaultName); // 所有策略都使用静态内部类来实现(只需要实现translate方法即可) public static class SnakeCaseStrategy extends PropertyNamingStrategyBase public static class UpperCamelCaseStrategy extends PropertyNamingStrategyBase ... }
下面结合它的注解@JsonNaming来演示它的使用:
@Getter @Setter @ToString // 此注解只能标注在类上 @JsonNaming(value = PropertyNamingStrategy.SnakeCaseStrategy.class) private static class DemoVo { private String demoName; private Integer demoAge; @JsonProperty("diyProp") private Object demoNameAndAge; } public static void main(String[] args) throws IOException { DemoVo vo = new DemoVo(); vo.setDemoName("fsx"); vo.setDemoAge(18); vo.setDemoNameAndAge("fsx18"); // 序列化对象 ObjectMapper objectMapper = new ObjectMapper(); String json = objectMapper.writeValueAsString(vo); System.out.println("序列化vo对象到json -> " + json); // 反序列化对象 vo = objectMapper.readValue(json,DemoVo.class); System.out.println("反序列化json到vo -> " + vo); }
打印输出结果:
序列化vo对象到json -> {"demo_name":"fsx","demo_age":18,"diyProp":"fsx18"} 反序列化json到vo -> Main.DemoVo(demoName=fsx, demoAge=18, demoNameAndAge=fsx18)
显然基于字段的注解@JsonProperty
它的优先级是高于@JsonNaming
的
除此之外,jackson
还提供了更多实用注解,有兴趣的可以自行去了解
我个人意见:jackson可能是由于功能设计得太过于全面了,使用起来有反倒很多不便之处,学习成本颇高。因为个人觉得还是我天朝的Fastjson好用啊~
说明:这些策略在异构的语言交互时是很有用的,因为各种语言命名规范都不尽相同,有了它们就可以有很好的兼容性。
如:.net命名都是大写开头形如DemoName表示属性名
如:js/python喜欢用下划线形全小写如demo_name表示属性名