SpringMvc请求参数的二次处理

440 阅读4分钟

这是我参与2022首次更文挑战的第16天,活动详情查看:2022首次更文挑战

需求

在一个项目中原来服务端是采用WebService的方式的进行客户端和服务进行交互,现根据需要调整为直接使用HTTP方式访问。仅修改访问的方式,其数据的访问结构等不变,在尽可能不修改代码的情况下,希望达到转换请求方式的目的。这种情况下就需要对原来的结构进行良好的抽取,并利用框架中原有的一些参数拦截机制进行处理。

尝试

1.HandlerMethodArgumentResolver

  1. 原数据结构:在旧项目的设计中,data数据是采用GBKbase64编码,在本次改造中,希望同时将解码的操作也统一处理。
public class RequestInfo {

  private String code;

  private String message;

  //请求数据(basic64)
  private String data;
}
  1. 第一时间想到的就是通过SpringMvc的参数处理器HandlerMethodArgumentResolver。拦截请求进入到这个参数处理器中,实现对应的拦截方法,通过在拦截方法中实现统一处理的业务逻辑。 ①是否支持本自定义参数解析器
// 自定义参数解析器
@Slf4j
public class ArgumentResolver implements HandlerMethodArgumentResolver {
    
    // ①
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return true;
    }

    @Override
    public Object resolveArgument() throws Exception {
        log.info("hello");
        return null;
    }
}

将自定义参数解析器添加到spring容器中,同时将自定义参数解析器添加到参数解析器列表中,这里的参数解析器列表是个伏笔,可以设想下,如果出现support方法对多种情况会返回true,那么框架是选择那个参数解析器处理?是多个都处理?还是只执行某一个?

public class WebConfig implements WebMvcConfigurer {

    @Bean
    public RequestDateParamMethodArgumentResolver argumentResolver(){
        return new RequestDateParamMethodArgumentResolver();
    }

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(argumentResolver());
    }
}

3.本以为可以完美解决,断点打在resolveArgument,正想启动后根据断点查询对应的参数信息,在进一步完善代码。测试发现,根本就没有进入断点。

4.虽说对参数解析器的执行过程不是非常的了解,但是大概知道参数解析器的处理是在适配器Adapter查找对应的HandlerMethod之后执行,所以根据调用栈,查找关键代码。

所以在内置的很多参数处理器中,只会选择一种进行处理,由于上述的写法中没有自定义注解,尽管supportsParameter方法的返回值为true,用户自定义的参数处理器也是排在内置处理器之后。根据以下代码可以发现,获取到一种匹配的参数处理器之后就跳出了。

private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
    for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
            if (resolver.supportsParameter(parameter)) {
                result = resolver;
                this.argumentResolverCache.put(parameter, result);
                break;
            }
        }
    return result;
}

所以:若需要使用参数处理器进行处理,需要自定义一个注解后,标记在方法的属性处,而改造的本意还是希望业务代码层无需关心解码这一行为,若需要在每个方法处都标记一个注解,与初衷相悖。

2. @InitBinder

这种方式是比较细粒度的控制方式,仅控制单个Controller,在这个Controller中,的参数会被拦截进行数据的二次处理。
①这里打印的passwordupdatePassword,是被修改之后的值, 所以解码的动作可以放在@InitBinder中标记的方法体中。

public class ArgumentResolverController {

    @InitBinder
    public void initBinder(WebDataBinder webDataBinder){
        if(webDataBinder.getTarget() instanceof User){
            User user = (User)webDataBinder.getTarget();
            user.setPassword("updatePassword");
        }
    }

    @RequestMapping("setUserInfo")
    public String setUserInfo(@RequestBody User user){
      	// ①
        log.info("user: {},{}",user.getUsername(), user.getPassword());
        return null;
    }
}

3. RequestBodyAdvice

经过尝试RequestBodyAdvice可以有效的解决这个问题,通过afterBodyRead方法可以在参数解析器处理之后获取到对应的解析实体,再根据该实体的类型,进行basic64解码。

@Component
@ControllerAdvice("cn.com.xx.controller")
public class ArgumentAdvice implements RequestBodyAdvice {

    @Override
    public boolean supports() {
        return true;
    }

    @Override
    public HttpInputMessage beforeBodyRead() throws IOException {
        return inputMessage;
    }

    @Override
    public Object afterBodyRead() {
        if(body instanceof User){
            ((User) body).setPassword("12312");
        }
        return body;
    }
}

小结

​ 虽然最后没有没有通过参数处理器来解决这个问题,但是在测试过程中也发现了自己对参数解析器的理解不足,原以为只要supportsParameter方法为true,就可以进行参数处理,而我在完善代码时只需要抽象一个顶层属性接口做是否是某种类型的判断即可。

@InitBinderRequestBodyAdvice都是可取的方案,取决于需求是需全局(某个包下的所有Controller)的参数都需要进行某种统一处理,或者是仅仅只需针对某一个Controller进行参数处理。