前言
前面通过三篇文章介绍了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<String, Object> 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<HandlerMethodArgumentResolver> 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<String,Object> 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<HandlerMethodArgumentResolver> 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<HandlerMethodArgumentResolver> argumentResolvers) { CurrUserArgumentResolver resolver = new CurrUserArgumentResolver(); // 利用工厂给容器外的对象注入所需组件 applicationContext.getAutowireCapableBeanFactory().autowireBean(resolver); argumentResolvers.add(resolver); } }
本姿势的技巧是利用了AutowireCapableBeanFactory巧妙完成了给外部对象赋能,从而即使自己并不是容器内的Bean,也能自由注入、使用容器内Bean的能力(同样可以随意使用@Autowired注解了~)。
这种方式是侵入性最弱的,是我推荐的方式。当然这需要你对Spring容器有一定的了解才能运用自如,做到心中有数才行,否则不建议你使用~
可以和内置的一些注解/类型一起使用吗?(参数类型是Map类型?)