HandlerMethodArgumentResolver(四):自定参数解析器处理特定应用场景,介绍PropertyNamingStrategy的使用【享学Spring MVC】 您所在的位置:网站首页 springmvc参数解析器 HandlerMethodArgumentResolver(四):自定参数解析器处理特定应用场景,介绍PropertyNamingStrategy的使用【享学Spring MVC】

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

2023-09-15 21:24| 来源: 网络整理| 查看: 265

前言

前面通过三篇文章介绍了HandlerMethodArgumentResolver这个参数解析器以及它的所有内置实现,相信看过的小伙伴对它的加载、初始化、处理原理等等已能够做到了心中有数了。 Spring MVC内置注册了灰常多的处理器给我们的使用,不客气说几乎100%的case我们都是足够用了的。但既然我们已经理解到了HandlerMethodArgumentResolver它深层的作用原理,那么本文就通过自定义参数处理器,来做到屏蔽(隔离)基础实现、更高效的编写业务编码(提效是本文的关注点)。

使用场景

关于它的应用场景可以非常多,本文我总结出最为常见、好理解的两个应用场景作为举例说明:

获取当前登陆人(当然用户)的基本信息调整(兼容)数据结构场景一:

在Controller层获取当前登陆人的基本信息(如id、名字…)是一个必须的、频繁的功能需求,这个时候如果团队内没有提供相关封装好的方法来调用,你便可看到大量的、重复的获取当前用户的代码,这就是各位经常吐槽的垃圾代码~

一般团队的做法是:提供BaseController,在基类里面提供获取当前用户的功能方法,这样业务控制器Controller只需要继承它就有这能力了,使用起来确实也还挺方便的。但是是否还思考过这种通过继承的方式它是有弊端的–>我只想获取当前登陆人我就得继承一个父类?这是不是设计太重了点?更坏的情况是如果此时我已经有父类了呢?

面对我提出的问题,本文针对性的提供一个新的、更加轻量的解决思路:自定义HandlerMethodArgumentResolver来实现获取当前登录用户的解决方案。实施步骤如下:

1、自定义一个参数注解(注解并不是100%必须的,可完全根据类型来决策)

/** * 用于获取当前登陆人信息的注解,配合自定义的参数处理器使用 * * @see CurrUserArgumentResolver */ @Target({ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface CurrUser { } // 待封装的Vo @Getter @Setter @ToString public class CurrUserVo { private Long id; private String name; }

2、自定义参数解析器CurrUserArgumentResolver并完成注册

public class CurrUserArgumentResolver implements HandlerMethodArgumentResolver { // 只有标注有CurrUser注解,并且数据类型是CurrUserVo/Map/Object的才给与处理 @Override public boolean supportsParameter(MethodParameter parameter) { CurrUser ann = parameter.getParameterAnnotation(CurrUser.class); Class parameterType = parameter.getParameterType(); return (ann != null && (CurrUserVo.class.isAssignableFrom(parameterType) || Map.class.isAssignableFrom(parameterType) || Object.class.isAssignableFrom(parameterType))); } @Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer container, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); // 从请求头中拿到token String token = request.getHeader("Authorization"); if (StringUtils.isEmpty(token)) { return null; // 此处不建议做异常处理,因为校验token的事不应该属于它来做,别好管闲事 } // 此处作为测试:new一个处理(写死的) CurrUserVo userVo = new CurrUserVo(); userVo.setId(1L); userVo.setName("fsx"); // 判断参数类型进行返回 Class parameterType = parameter.getParameterType(); if (Map.class.isAssignableFrom(parameterType)) { Map map = new HashMap(); BeanUtils.copyProperties(userVo, map); return map; } else { return userVo; } } } // 注册进Spring组件内 @Configuration @EnableWebMvc public class WebMvcConfig extends WebMvcConfigurerAdapter { @Override public void addArgumentResolvers(List argumentResolvers) { argumentResolvers.add(new CurrUserArgumentResolver()); } }

3、书写测试例子

@Controller @RequestMapping public class HelloController { @ResponseBody @GetMapping("/test/curruser") public Object testCurrUser(@CurrUser CurrUserVo currUser) { return currUser; } @ResponseBody @GetMapping("/test/curruser/map") public Object testCurrUserMap(@CurrUser Map currUser) { return currUser; } @ResponseBody @GetMapping("/test/curruser/object") public Object testCurrUserObject(@CurrUser Object currUser) { return currUser; } }

请求:/test/curruser或者/test/curruser/object 这两个请求得到的答案是一致的且符合预期,结果如下截图:

在这里插入图片描述在这里插入图片描述

但是,但是,但是若访问/test/curruser/map,它的结果如下:

在这里插入图片描述在这里插入图片描述

so参数类型是Map类型,自定义的参数解析器CurrUserArgumentResolver并没有生效,为什么呢??? 带着这个疑问,接下来我说说对此非常重要的使用细节:

如何使用Spring容器内的Bean?

在本例中,为了方便,我在CurrUserArgumentResolver里写死的自己new的一个CurrUserVo作为返回。实际应用场景中,此部分肯定是需要根据token去访问DB/Redis的,因此就需要使用到Spring容器内的Bean。

有的小伙伴就想当然了,在本例上直接使用@Autowired HelloService helloService;来使用,经测试发现这是注入不进来的,helloService值为null。那么本文就教你正确的使用姿势:

姿势一:把自定义的参数解析器也放进容器 这是一种十分快捷、见效的解决方案。@Configuration @EnableWebMvc public class WebMvcConfig extends WebMvcConfigurerAdapter { @Bean public CurrUserArgumentResolver currUserArgumentResolver(){ return new CurrUserArgumentResolver(); } @Override public void addArgumentResolvers(List argumentResolvers) { argumentResolvers.add(currUserArgumentResolver()); }

这样,你在CurrUserArgumentResolver就可以顺理成章的注入想要的组件了,形如这样:

public class CurrUserArgumentResolver implements HandlerMethodArgumentResolver { @Autowired HelloService helloService; @Autowired StringRedisTemplate stringRedisTemplate; ... }

这种方案的优点是:在Spring容器内它几乎能解决大部分类似问题,在组件不是很多的情况下,推荐新手使用,因为无需过多的理解Spring内部机制便可轻松使用。

姿势二:借助AutowireCapableBeanFactory给对象赋能 本着"减轻"Spring容器"负担"的目的,"手动"精细化控制Spring内的Bean组件。像本文的这种解析器其实是完全没必要放进容器内的,需要什么组件让容器帮你完成注入即可,自己本文就没必要放进去喽:@Configuration @EnableWebMvc public class WebMvcConfig extends WebMvcConfigurerAdapter { @Autowired private ApplicationContext applicationContext; @Override public void addArgumentResolvers(List argumentResolvers) { CurrUserArgumentResolver resolver = new CurrUserArgumentResolver(); // 利用工厂给容器外的对象注入所需组件 applicationContext.getAutowireCapableBeanFactory().autowireBean(resolver); argumentResolvers.add(resolver); } }

本姿势的技巧是利用了AutowireCapableBeanFactory巧妙完成了给外部对象赋能,从而即使自己并不是容器内的Bean,也能自由注入、使用容器内Bean的能力(同样可以随意使用@Autowired注解了~)。 这种方式是侵入性最弱的,是我推荐的方式。当然这需要你对Spring容器有一定的了解才能运用自如,做到心中有数才行,否则不建议你使用~

可以和内置的一些注解/类型一起使用吗?(参数类型是Map类型?)

作为一个"合格"的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 resolvers = getDefaultArgumentResolvers(); this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers); } ... } private List getDefaultArgumentResolvers() { List 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表示属性名

场景二:

在微服务场景中有个特别常见的现象:跟第三方服务做对接时(如python老系统),你不乏会遇到如下两个痛点:

对方系统是以下划线形式命名的(和Java命名规范相悖)对方系统的参数json串层次较深,而对你有用的仅仅是深处的一小部分

例如这个参数串:

{ "data": { "transport_data": { "demo_name": "fsx", "demo_age": 18 }, "secret_info": { "code": "fkldshjfkldshj" } }, "code": "200", "msg": "this is a message" }

对你真正有用的只有demo_name和demo_age两个值,怎么破??? 我相信绝大部分小伙伴都这么做:按照此结构先定义一个DTO全部接收回来(字段命名也用下划线方式命名),然后再一个个处理。若这么做虽然简单,我觉得还是有如下两个不妥的地方:

Java属性名也必须用下划线命名,看起来影响了命名体系(其实就是看着不爽,哈哈)按照参数这种复杂结构书写,使得我们关注点分散,不能聚焦到真真关心的那一块数据上

针对这些痛点,废话不多说,直接上我的处理方案:

1、定义一个模型(只写我自己关注的属性)

@Getter @Setter @ToString public class TranUserVo { private String demoName; private Long demoAge; }

定义的模型非常之简单,不仅只关心我要的数据,而且还是标准的java驼峰命名,没必要去迁就别的语言而丧失自己优雅性,否则容易把自己弄得四不像(万一又接python,又接.net呢?)~

2、自定义一个参数解析器并且注册上去

public class TranUserArgumentResolver implements HandlerMethodArgumentResolver { // 只处理这个类型,不需要注解 @Override public boolean supportsParameter(MethodParameter parameter) { Class parameterType = parameter.getParameterType(); return TranUserVo.class.isAssignableFrom(parameterType); } @Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer container, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); HttpMethod httpMethod = HttpMethod.valueOf(request.getMethod()); // 本例为了简单,演示get的情况(这里使用key为:demoKey) if (httpMethod == HttpMethod.GET) { String value = request.getParameter("demoKey"); JSONObject transportData = (JSONObject) ((JSONObject) JSON.parseObject(value).get("data")).get("transport_data"); // 采用命名策略,转换TranUserVo实例对象再返回 // 序列化配置对象 ParserConfig config = new ParserConfig(); config.propertyNamingStrategy = PropertyNamingStrategy.SnakeCase; TranUserVo tranUserVo = transportData.toJavaObject(TranUserVo.class, config, 0); return tranUserVo; } else { // 从body提里拿 // ... return null; } } } // 注册此自定义的参数解析器 @Override public void addArgumentResolvers(List argumentResolvers) { argumentResolvers.add(new TranUserArgumentResolver()); }

对此部分我说明一点:对于json到对象的解析,理应还加上@Valid校验的能力的,此部分我就省略了,毕竟也不是本文所关心的重点

测试用例:

@ResponseBody @GetMapping("/test/tranuser") public Object testCurrUser(TranUserVo tranUser) { return tranUser; }

请求:/test/tranuser?demoKey=上面那一大长串json串,得到的结果就是预期的结果喽:

在这里插入图片描述在这里插入图片描述

完美~

说明:这种长传现在需要使用post/put传递,本文只是为了简化演示,所以使用了GET请求,毕竟解析Body体不是本文所需讨论的~

总结

我认为,自定义参数解析器HandlerMethodArgumentResolver最重要不是它本身的实现,而是它的指导思想:分离关注,业务解耦。当然本文我摘出来的两个使用场景案例只是冰山一角,各位需要举一反三,才能融会贯通。

既然我们可以自定义参数处理器HandlerMethodArgumentResolver,自然也就可以自定义返回值处理器HandlerMethodReturnValueHandler喽,作为课后作业,有兴趣者不妨一试,还是非常有作用的。特别在处理"老项目"的兼容性上非常好使,或许能让你大放异彩~



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有