HandlerMethodArgumentResolver 自定义使用
HandlerMethodArgumentResolver 自定义使用
1.HandlerMethodArgumentResolver 的应用场景
HandlerMethodArgumentResolver
是Spring提供的一个请求参数解析接口,用于对一个request进行解析并且对方法的入参进行赋值,对于这个接口Spring提供了非常多的内置实现。摘抄HandlerMethodArgumentResolver 类上的注释如下:
Strategy interface for resolving method parameters into argument values in the context of a given request.
翻译一下就是:用于在给定请求的上下文中将方法参数解析为参数值的策略接口。这么说可能有点绕口,举个Spring内置实现的类例子RequestResponseBodyMethodProcessor
,该类用于处理加了@RequestBody
注解的参数。@RequestBody
注解的使用应该非常的广泛,项目里经常可以看到这种形式的代码:
@RequestMapping("/update")
public ResponseResult<User> update(@RequestBody User user){
System.out.println("当前操作的用户为: " + user.toString());
// update...
return ResponseResult.success(user,"更新用户成功!");
}
@RequestBody
用于处理接收一个对象类型的参数,这个注解会把属性注入到对象里,并且传进我们的方法。如果不加这个注解,user
参数为null
,对于刚接触的人来说这是非常头疼的。可见Spring一个简单的注解为我们省去了非常多的烦恼,一个注解就能实现这个功能,是不是十分神奇。这里面Spring替我们做了很多操作,对于我们是透明的,下面再展开叙述原理。果然,好用的东西总是朴实无华的。
这里先模仿一下Spring的实现,自己定义一个类实现HandlerMethodArgumentResolver
。
2.HandlerMethodArgumentResolver 的简单应用
假设有一个业务场景,在每个方法执行前,需要获取当前用户的信息,在每个方法自定义去解析似乎是个不错的办法,但是如果方法很多,那么就会出现非常多的冗余代码,这时候我们可以通过参数直接注入,即可实现获取用户的信息,这就是HandlerMethodArgumentResolver
的经典应用场景了。HandlerMethodArgumentResolver
的使用非常简单。先定义一个类UserLoginArgumentResolver
实现HandlerMethodArgumentResolver
,该接口只有两个待实现的方法,boolean supportsParameter()
方法表示该resolver是否支持该类型的参数解析和Object resolveArgument() throws Exception
返回解析后的参数值。
自定义实现如下:其中InjectUser
是自定义注解,标识该参数由UserLoginArgumentResolver
解析。
/**
* @author Codegitz
* @date 2021/11/24 19:46
**/
public class UserLoginArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
// 标识如果参数上标有InjectUser注解,则可以处理
return parameter.hasMethodAnnotation(InjectUser.class) || parameter.hasParameterAnnotation(InjectUser.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
// 自定义的处理逻辑,这里逻辑为简单获取request中的Authorization,解析出用户信息,返回一个User对象
System.out.println("UserLoginArgumentResolver work....");
String token = webRequest.getHeader(ReqRespConstants.AUTHORIZATION);
// 该方法由JwtTokenUtils类提供
return checkToken(token);
}
}
@InjectUser
的定义如下,该注解可以标识在参数上。
/**
* @author Codegitz
* @date 2021/11/24 19:48
**/
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface InjectUser {
}
自定义的解析器UserLoginArgumentResolver
已经准备好,接下来的工作就是把它注入到原有的逻辑里,让它生效,简而言之,就是注入到Spring的WebMvcConfigurationSupport
的List<HandlerMethodArgumentResolver> argumentResolvers
里。WebMvcConfigurationSupport
类提供了一个addArgumentResolvers()
抽象方法,摘取方法以及注解,可以看到这里就是为了自定义注入而设定的。看到这里不由得感慨,这种设计真的是太友好了,在当前写的时候已经考虑到以后的扩展,这是非常值得我们学习的点。所以我们只需要新建一个配置类继承WebMvcConfigurationSupport
,把自定义的UserLoginArgumentResolver
加入就行。
/**
* Add custom {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers}
* to use in addition to the ones registered by default.
* <p>Custom argument resolvers are invoked before built-in resolvers except for
* those that rely on the presence of annotations (e.g. {@code @RequestParameter},
* {@code @PathVariable}, etc). The latter can be customized by configuring the
* {@link RequestMappingHandlerAdapter} directly.
* @param argumentResolvers the list of custom converters (initially an empty list)
*/
protected void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
}
自定义的配置类如下:
/**
* @author Codegitz
* @date 2021/11/24 21:43
**/
@Configuration
public class MyWebMvcConfiguration extends WebMvcConfigurationSupport {
@Bean
public UserLoginArgumentResolver userLoginArgumentResolver(){
return new UserLoginArgumentResolver();
}
@Override
protected void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
UserLoginArgumentResolver userLoginArgumentResolver = userLoginArgumentResolver();
argumentResolvers.add(userLoginArgumentResolver);
super.addArgumentResolvers(argumentResolvers);
}
}
到这里已经把基础设施搭建完成,接下来就可以写个测试代码进行测试。新建一个Controller,写下测试方法如下:/login
方法用于获取用户的token,这里用个简单的缓存实现,获取token后,后续的请求会带上token,/doSomething
方法展示了通过@InjectUser
注解注入一个User
参数。
@RequestMapping("/login")
public ResponseResult<String> login(@RequestBody User request){
try {
String user = resolverService.login(request);
return ResponseResult.success(user);
} catch (ExecutionException e) {
return ResponseResult.fail("获取token失败!" + e.getMessage());
}
}
@RequestMapping("/doSomething")
public ResponseResult<User> doSomething(@InjectUser User user){
System.out.println("当前操作的用户为: " + user.toString());
return ResponseResult.success(user,"通过UserLoginArgumentResolver解析参数成功!");
}
接下来启动项目,先获取token,然后request请求头里带上token去请求后续接口。
获取token后,将token放入请求头里。
可以看到。仅仅通过传入token,我们获取到了一个User对象,并且返回给了响应,那么这一切到底是如何发生的呢?我们是在哪一步将token解析成User,并且把它赋值给我们的方法入参呢?下面就来剖析一下它的原理。
3.HandlerMethodArgumentResolver 的底层实现
本着言简意赅的原则,这里不会给出一个请求到底是怎么进入到spring的详细过程,但是会贴出一个调用链。解析的过程我会先给出spring处理请求参数的地方,然后给出spring是怎么选择适合的resolver
的,然后是自定义解析器的执行过程。
首先来看一下调用链:
DispatcherServlet#doDispatch() ->
AbstractHandlerMethodAdapter#handle() ->
RequestMappingHandlerAdapter#handleInternal() ->
RequestMappingHandlerAdapter#invokeHandlerMethod() ->
ServletInvocableHandlerMethod#invokeAndHandle() ->
InvocableHandlerMethod#invokeForRequest()
从这个InvocableHandlerMethod#invokeForRequest()
方法开始我们的解析过程,这里调用了Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs)
方法,获取了方法参数,随后通过doInvoke(args)
调用ResolverController#doSomething(User)
方法。
这里的getMethodArgumentValues()
显然就是获取参数的方法,进入里面看一下实现逻辑。可以看到逻辑很简单,获取该方法的所有参数,然后循环去给参数赋值,赋值的操作是this.resolvers.resolveArgument()
。
可以看到这里的resolvers
的类型为HandlerMethodArgumentResolverComposite
,这里应用了组合模式HandlerMethodArgumentResolverComposite
对象里维护了两个属性,这里面保存了spring容器里所有的HandlerMethodArgumentResolver
实现类。
private final List<HandlerMethodArgumentResolver> argumentResolvers = new LinkedList<>();
private final Map<MethodParameter, HandlerMethodArgumentResolver> argumentResolverCache = new ConcurrentHashMap<>(256);
进入到HandlerMethodArgumentResolverComposite#resolveArgument()
里面。
首先会调用getArgumentResolver(parameter)
获取适合的resolver
,对于这个方法,这里会获取我们自定义的UserLoginArgumentResolver
解析器。
该方法会遍历所有的resolvers
,找出第一个能够处理该参数的resolver
。自定义的resolver
这里的supportsParameter()
会返回true
,跟进会看到这里会进入到自定义的resolver
里面。
这里判断参数是否有@InjectUser
注解,这里返回true
。
这里返回的就是自定义的UserLoginArgumentResolver
。
进入自定义resolveArgument()
逻辑,返回了获取的user对象。
至此,解析过程已经完成。原理就这么简单。
回到最开始的入口,这个参数会传入doInvoke(args)
,反射去调用doSomething(user)
方法,获取结果返回。
4.总结
这一个过程还是比较简单明了的,应用起来也非常简单。看到这里,最让我深思的问题是,spring为什么能把一个比较复杂的功能写得这么简单明了,且随时可以扩展,这里面的代码功力绝非一朝一夕能习得。首先HandlerMethodArgumentResolver
应用了策略模式,不同的实现提供不同的处理逻辑,通过supportsParameter()
方法区分。其次,在选择合适的resolver
时候,运用了组合模式,里面维护了所有的HandlerMethodArgumentResolver
实现,还维护了一个缓存,减少了寻找resolvers
时遍历的消耗。 细微之处的消耗节省,扣得让人发指。
冰冻三尺非一日之寒,还需要好好学习。
最后附上一个工具代码。完整代码见github。
JwtTokenUtils
代码。
/**
* @author Codegitz
* @date 2021/11/24 19:58
**/
public class JwtTokenUtils {
//用于签名的私钥
private static final String PRIVATE_KEY = "EDCYHNMYTRESXCVBNMKL";
//签发者
private static final String ISS = "Codegitz";
//过期时间1小时
private static final long EXPIRATION_ONE_HOUR = 3600L;
//过期时间1天
private static final long EXPIRATION_ONE_DAY = 604800L;
/**
* 生成Token
* @param user
* @return
*/
public static String createToken(User user, ExpireTimeType type){
//过期时间
long expireTime = 0;
if (type == ExpireTimeType.HOUR){
expireTime = EXPIRATION_ONE_HOUR;
}else {
expireTime = EXPIRATION_ONE_DAY;
}
//Jwt头
Map<String,Object> header = new HashMap<>();
header.put("typ","JWT");
header.put("alg","HS256");
Map<String,Object> claims = new HashMap<>();
//自定义有效载荷部分
claims.put("id",user.getId());
claims.put("userName",user.getUserName());
claims.put("password",user.getPassword());
claims.put("address",user.getAddress());
claims.put("token",user.getToken());
return Jwts.builder()
//发证人
.setIssuer(ISS)
//Jwt头
.setHeader(header)
//有效载荷
.setClaims(claims)
//设定签发时间
.setIssuedAt(new Date())
//设定过期时间
.setExpiration(new Date(System.currentTimeMillis() + expireTime * 1000))
//使用HS256算法签名,PRIVATE_KEY为签名**
.signWith(SignatureAlgorithm.HS256,PRIVATE_KEY)
.compact();
}
/**
* 验证Token,组装对象
* @param token
* @return
*/
public static User checkToken(String token){
//解析token后,从有效载荷取出值
Claims claimsFromToken = getClaimsFromToken(token);
String id = (String) claimsFromToken.get("id");
String userName = (String) claimsFromToken.get("userName");
String address = (String) claimsFromToken.get("address");
//封装为User对象
User user = new User();
user.setId(id);
user.setUserName(userName);
user.setAddress(address);
user.setToken(token);
return user;
}
/**
* 获取有效载荷
* @param token
* @return
*/
public static Claims getClaimsFromToken(String token){
Claims claims = null;
try {
claims = Jwts.parser()
//设定解密私钥
.setSigningKey(PRIVATE_KEY)
//传入Token
.parseClaimsJws(token)
//获取载荷类
.getBody();
}catch (Exception e){
return null;
}
return claims;
}
}
缓存实现`TokenCache`类。这里默认给个`admin`用户。
/**
* @author Codegitz
* @date 2021/11/24 22:11
**/
@Component
public class TokenCache {
private static final String CACHEKEY = "cacheKey";
LoadingCache<String,HashMap<String, User>> cache;
private void initCache(){
cache = CacheBuilder.newBuilder()
.expireAfterAccess(12, TimeUnit.HOURS)
.maximumSize(100)
.build(new CacheLoader<String, HashMap<String, User>>() {
@Override
public HashMap<String, User> load(String token) throws Exception {
HashMap<String, User> map = new HashMap<>();
User admin = new User();
admin.setId("1");
admin.setUserName("admin");
admin.setAddress("GZ");
admin.setPassword("123456");
map.put("admin",admin);
return map;
}
});
}
public User getUser(String userName) throws ExecutionException {
if (cache == null){
initCache();
}
HashMap<String, User> map = cache.get(CACHEKEY);
return map.get(userName);
}
public void setUser(User user){
if (cache == null){
initCache();
}
HashMap<String, User> map = new HashMap<>();
map.put(user.getUserName(),user);
cache.put(CACHEKEY,map);
}
}
简单的`ResolverService`类。
/**
* @author Codegitz
* @date 2021/11/24 21:52
**/
@Component
public class ResolverService {
@Autowired
private TokenCache tokenCache;
public String login(User user) throws ExecutionException {
User exist = tokenCache.getUser(user.getUserName());
if (exist != null){
String token = exist.getToken();
token = token == null ? createToken(exist,ExpireTimeType.HOUR) : token;
exist.setToken(token);
return token;
}
String token = createToken(user, ExpireTimeType.HOUR);
user.setToken(token);
tokenCache.setUser(user);
return token;
}
}
HandlerMethodArgumentResolver 自定义使用的更多相关文章
- SpringMVC HandlerMethodArgumentResolver自定义参数转换器 针对HashMap失效的问题
自定义Spring MVC3的参数映射和返回值映射 + fastjson 自定义Spring MVC3的参数映射和返回值映射 + fastjson首先说一下场景:在一些富客户端Web应用程序中我们会有 ...
- SpringMVC HandlerMethodArgumentResolver自定义参数转换器
来源: https://www.cnblogs.com/daxin/p/3296493.html 自定义Spring MVC3的参数映射和返回值映射 + fastjson首先说一下场景:在一些富客户端 ...
- springMVC使用HandlerMethodArgumentResolver 自定义解析器实现请求参数绑定方法参数
http://blog.csdn.net/truong/article/details/30971317 http://blog.csdn.net/fytain/article/details/439 ...
- SpringBoot:自定义注解实现后台接收Json参数
0.需求 在实际的开发过程中,服务间调用一般使用Json传参的模式,SpringBoot项目无法使用@RequestParam接收Json传参 只有@RequestBody支持Json,但是每次为了一 ...
- Halo(二)
@Conditional 满足条件给容器注册Bean(在配置类 @Configuration 的类和方法上配置) 需要实现Condition接口, 实现matches方法 public class L ...
- SpringBoot集成自定义HandlerMethodArgumentResolver
传统SpringMVC集成自定义HandlerMethodArgumentResolver的方式为:http://www.cnblogs.com/yangzhilong/p/6282218.html ...
- 自定义HandlerMethodArgumentResolver参数解析器和源码分析
在初学springmvc框架时,我就一直有一个疑问,为什么controller方法上竟然可以放这么多的参数,而且都能得到想要的对象,比如HttpServletRequest或HttpServletRe ...
- SpringBoot让你的Bean动起来(自定义参数解析HandlerMethodArgumentResolver)
SpringBoot让你的Bean动起来(自定义参数解析HandlerMethodArgumentResolver) 简介 我们 Controller 用到的一些 Bean 需要通过一定的方式去获取的 ...
- Springboot使用自定义注解实现简单参数加密解密(注解+HandlerMethodArgumentResolver)
前言 我黄汉三又回来了,快半年没更新博客了,这半年来的经历实属不易,疫情当头,本人实习的公司没有跟员工共患难, 直接辞掉了很多人.作为一个实习生,本人也被无情开除了.所以本人又得重新准备找工作了. 算 ...
随机推荐
- jQuery--筛选【串联函数】
串联函数简介 A.add(B) 将A和B组合成一个对象 A.children().andSelf() 将之前的对象添加到操作集合中 A.children().children().end() 回到最近 ...
- 说说finally和final的区别
final用于声明属性,方法和类,分别表示属性不可变,方法不可覆盖,类不可继承.内部类要访问局部变量,局部变量必须定义成final类型. finally是异常处理语句结构的一部分,表示总是 ...
- 在虚拟机里面安装mysql
https://dev.mysql.com/downloads/repo/yum/ 首先到网站里面下载 mysql80-community-release-el7-3.noarch.rpm 通过xft ...
- .NET 6学习笔记(3)——在Windows Service中托管ASP.NET Core并指定端口
在上一篇<.NET 6学习笔记(2)--通过Worker Service创建Windows Service>中,我们讨论了.NET Core 3.1或更新版本如何创建Windows Ser ...
- 切图崽的自我修养-[ES6] 迭代器Iterator浅析
Iterator 这真是毅种循环 Iterator不是array,也不是set,不是map, 它不是一个实体,而是一种访问机制,是一个用来访问某个对象的接口规范,为各种不同的数据结构提供统一的访问机制 ...
- 菜鸟的谷歌浏览器devtools日志分析经验
1 别管什么性能,尽可能输出详细的必要日志.(除非你明显感觉到性能变低,而且性能变低的原因是由于日志输出太多而引起的) 2 不要总是使用console.log,试试console.info, cons ...
- python-爬楼梯
[题目描述] 假设一段楼梯共n(n>1)个台阶,小朋友一步最多能上3个台阶,那么小朋友上这段楼梯一共有多少种方法. [练习要求]请给出源代码程序和运行测试结果,源代码程序要求添加必要的注释. [ ...
- 一个抽取百度定位的教程(下载百度地图Demo+配置+抽取)
效果展示 已经下载Demo的可以直接到第五步,已经配置好的并可以运行的可以直接到第七步. 1.在浏览器搜索 " 百度定位API ",点击下面这个链接 2.翻到最下面找到并点击 &q ...
- DOM节点详解
@ 目录 学习总结 1. 什么是 DOM 2. HTMLDOM 3. 元素获取 元素获取方式: 元素节点的属性操作 4. Node 对象的属性和方法 常用属性 常用方法 5. 事件处理 事件驱动编程 ...
- vue中对element-ui框架中el-table的列的每一项数据进行操作
vue中使用element table,表格参数格式化formatter 后台返回对应的数字, 那肯定不能直接显示数字,这时候就要对 表格进行数据操作 如图: 代码: methods: { //状态改 ...