·

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的WebMvcConfigurationSupportList<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去请求后续接口。

1637904514115

​ 获取token后,将token放入请求头里。

1637904724769

​ 可以看到。仅仅通过传入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)方法。

1637916587378

​ 这里的getMethodArgumentValues()显然就是获取参数的方法,进入里面看一下实现逻辑。可以看到逻辑很简单,获取该方法的所有参数,然后循环去给参数赋值,赋值的操作是this.resolvers.resolveArgument()

1637917208485

​ 可以看到这里的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解析器。

1637918063845

​ 该方法会遍历所有的resolvers,找出第一个能够处理该参数的resolver。自定义的resolver这里的supportsParameter()会返回true,跟进会看到这里会进入到自定义的resolver里面。

1637918649015

​ 这里判断参数是否有@InjectUser注解,这里返回true

1637918759767

​ 这里返回的就是自定义的UserLoginArgumentResolver

1637918248699

​ 进入自定义resolveArgument()逻辑,返回了获取的user对象。

1637919066373

​ 至此,解析过程已经完成。原理就这么简单。

1637918924903

​ 回到最开始的入口,这个参数会传入doInvoke(args),反射去调用doSomething(user)方法,获取结果返回。

1637919146533

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;
    }
}
posted @ 2021-11-26 18:11  Codegitz  阅读(242)  评论(0编辑  收藏  举报