HandlerMethodArgumentResolver(四):自定参数解析器处理特定场景需求,介绍PropertyNamingStrategy的使用【享学Spring MVC】(中)

简介: HandlerMethodArgumentResolver(四):自定参数解析器处理特定场景需求,介绍PropertyNamingStrategy的使用【享学Spring MVC】(中)

作为一个"合格"的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还提供了更多实用注解,有兴趣的可以自行去了解


image.png


我个人意见:jackson可能是由于功能设计得太过于全面了,使用起来有反倒很多不便之处,学习成本颇高。因为个人觉得还是我天朝的Fastjson好用啊~


说明:这些策略在异构的语言交互时是很有用的,因为各种语言命名规范都不尽相同,有了它们就可以有很好的兼容性。

如:.net命名都是大写开头形如DemoName表示属性名

如:js/python喜欢用下划线形全小写如demo_name表示属性名

相关文章
|
13天前
|
设计模式 前端开发 Java
步步深入SpringMvc DispatcherServlet源码掌握springmvc全流程原理
通过对 `DispatcherServlet`源码的深入剖析,我们了解了SpringMVC请求处理的全流程。`DispatcherServlet`作为前端控制器,负责请求的接收和分发,处理器映射和适配负责将请求分派到具体的处理器方法,视图解析器负责生成和渲染视图。理解这些核心组件及其交互原理,有助于开发者更好地使用和扩展SpringMVC框架。
24 4
|
1月前
|
前端开发 Java 开发者
Spring MVC中的请求映射:@RequestMapping注解深度解析
在Spring MVC框架中,`@RequestMapping`注解是实现请求映射的关键,它将HTTP请求映射到相应的处理器方法上。本文将深入探讨`@RequestMapping`注解的工作原理、使用方法以及最佳实践,为开发者提供一份详尽的技术干货。
123 2
|
1月前
|
前端开发 Java Spring
探索Spring MVC:@Controller注解的全面解析
在Spring MVC框架中,`@Controller`注解是构建Web应用程序的基石之一。它不仅简化了控制器的定义,还提供了一种优雅的方式来处理HTTP请求。本文将全面解析`@Controller`注解,包括其定义、用法、以及在Spring MVC中的作用。
55 2
|
1月前
|
前端开发 Java UED
SpringMVC全局异常处理+拦截器使用+参数校验
通过使用 SpringMVC 的全局异常处理、拦截器和参数校验,可以有效提升 Web 应用程序的安全性、稳定性和用户体验。这些技术的合理应用,不仅可以保证代码的健壮性,还能提高代码的可维护性,为开发高质量的 Web 应用程序提供了坚实的基础。
57 6
|
2月前
|
JSON 前端开发 Java
Spring MVC——获取参数和响应
本文介绍了如何在Spring框架中通过不同的注解和方法获取URL参数、上传文件、处理cookie和session、以及响应不同类型的数据。具体内容包括使用`@PathVariable`获取URL中的参数,使用`MultipartFile`上传文件,通过`HttpServletRequest`和`@CookieValue`获取cookie,通过`HttpSession`和`@SessionAttribute`获取session,以及如何返回静态页面、HTML代码片段、JSON数据,并设置HTTP状态码和响应头。
73 1
Spring MVC——获取参数和响应
|
1月前
|
前端开发 Java 开发者
Spring MVC中的控制器:@Controller注解全解析
在Spring MVC框架中,`@Controller`注解是构建Web应用程序控制层的核心。它不仅简化了控制器的定义,还提供了灵活的请求映射和处理机制。本文将深入探讨`@Controller`注解的用法、特点以及在实际开发中的应用。
81 0
|
2月前
|
JSON 前端开发 Java
SSM:SpringMVC
本文介绍了SpringMVC的依赖配置、请求参数处理、注解开发、JSON处理、拦截器、文件上传下载以及相关注意事项。首先,需要在`pom.xml`中添加必要的依赖,包括Servlet、JSTL、Spring Web MVC等。接着,在`web.xml`中配置DispatcherServlet,并设置Spring MVC的相关配置,如组件扫描、默认Servlet处理器等。然后,通过`@RequestMapping`等注解处理请求参数,使用`@ResponseBody`返回JSON数据。此外,还介绍了如何创建和配置拦截器、文件上传下载的功能,并强调了JSP文件的放置位置,避免404错误。
|
2月前
|
前端开发 Java Spring
【Spring】“请求“ 之传递单个参数、传递多个参数和传递对象
【Spring】“请求“ 之传递单个参数、传递多个参数和传递对象
147 2
|
2月前
|
前端开发 Java 应用服务中间件
【Spring】Spring MVC的项目准备和连接建立
【Spring】Spring MVC的项目准备和连接建立
65 2
|
1月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
83 2