记一次Get请求数据预处理处理操作

young 41 2024-11-13

继上一次处理了Json请求和地址参数的加密问题后,又出现了新的问题,后端在处理Get请求的时候,RequestParam的接收采用了两种模式,一种是直接在接口上平铺了所有的参数(平铺模式),一种是通过一个对象来接受参数(对象模式),由于是老系统,并且时间有限,直接改造所有接口的话时间够,于是考虑采用Spring内置的机制来进行处理。

由于上一次处理地址参数的处理经验,本次依旧考虑采用Spring的HandlerMethodArgumentResolver进行处理。

/**
 * Strategy interface for resolving method parameters into argument values in
 * the context of a given request.
 *
 * @author Arjen Poutsma
 * @since 3.1
 * @see HandlerMethodReturnValueHandler
 */
public interface HandlerMethodArgumentResolver {

	/**
	 * Whether the given {@linkplain MethodParameter method parameter} is
	 * supported by this resolver.
	 * @param parameter the method parameter to check
	 * @return {@code true} if this resolver supports the supplied parameter;
	 * {@code false} otherwise
	 */
	boolean supportsParameter(MethodParameter parameter);

	/**
	 * Resolves a method parameter into an argument value from a given request.
	 * A {@link ModelAndViewContainer} provides access to the model for the
	 * request. A {@link WebDataBinderFactory} provides a way to create
	 * a {@link WebDataBinder} instance when needed for data binding and
	 * type conversion purposes.
	 * @param parameter the method parameter to resolve. This parameter must
	 * have previously been passed to {@link #supportsParameter} which must
	 * have returned {@code true}.
	 * @param mavContainer the ModelAndViewContainer for the current request
	 * @param webRequest the current request
	 * @param binderFactory a factory for creating {@link WebDataBinder} instances
	 * @return the resolved argument value, or {@code null} if not resolvable
	 * @throws Exception in case of errors with the preparation of argument values
	 */
	@Nullable
	Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;

}

优先处理平铺模式,通过自定义的注解进行标记,然后从NativeWebRequest中根据MethodParameter获取到的参数名称获取对应的参数值接口,解密之后直接返回对应的数据即可。

webRequest.getParameter(parameter.getParameterName())

然而实际测试的过程中发现parameter.getParameterName()并不能获取到真正的参数名称,而是arg0arg1这种,这样我们从webRequest中就无法获取需要处理的数据。

因此考虑将自己的注解与@RequestParam注解进行结合,通过@RequestParam获取属性的值,然而依旧存在一个问题,如果@RequestParam不写属性值,也是可以映射上的。

因此再创建一个自定义注解,并且让这个注解的属性值为必填,以此来强制获取属性的名称,从而获取我们需要的值,平铺模式的参数就这么处理好了。

当平铺模式处理完之后,准备处理对象模式的参数,发现Spring提供了ServletModelAttributeMethodProcessor来处理对象参数的映射操作,查其源码,发现在bindRequestParameters中通过ServletRequestDataBinder进行了参数了和对象的绑定操作

/**
 * This implementation downcasts {@link WebDataBinder} to
 * {@link ServletRequestDataBinder} before binding.
 * @see ServletRequestDataBinderFactory
 */
@Override
protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) {
    ServletRequest servletRequest = request.getNativeRequest(ServletRequest.class);
    Assert.state(servletRequest != null, "No ServletRequest");
    ServletRequestDataBinder servletBinder = (ServletRequestDataBinder) binder;
    servletBinder.bind(servletRequest);
}

一路深追ServletRequestDataBinder的bind方法

public void bind(ServletRequest request) {
    MutablePropertyValues mpvs = new ServletRequestParameterPropertyValues(request);
    MultipartRequest multipartRequest = WebUtils.getNativeRequest(request, MultipartRequest.class);
    if (multipartRequest != null) {
       bindMultipart(multipartRequest.getMultiFileMap(), mpvs);
    }
    addBindValues(mpvs, request);
    doBind(mpvs);
}
@Override
protected void doBind(MutablePropertyValues mpvs) {
    checkFieldDefaults(mpvs);
    checkFieldMarkers(mpvs);
    super.doBind(mpvs);
}
protected void doBind(MutablePropertyValues mpvs) {
    checkAllowedFields(mpvs);
    checkRequiredFields(mpvs);
    applyPropertyValues(mpvs);
}

最终发现是在applyPropertyValues方法中进行了参数的映射操作

protected void applyPropertyValues(MutablePropertyValues mpvs) {
    try {
       // Bind request parameters onto target object.
       getPropertyAccessor().setPropertyValues(mpvs, isIgnoreUnknownFields(), isIgnoreInvalidFields());
    }
    catch (PropertyBatchUpdateException ex) {
       // Use bind error processor to create FieldErrors.
       for (PropertyAccessException pae : ex.getPropertyAccessExceptions()) {
          getBindingErrorProcessor().processPropertyAccessException(pae, getInternalBindingResult());
       }
    }
}

applyPropertyValues是protectd的,只要可以继承并重写此方法,就可以满足我们的解密要求。

于是去查看如何WebDataBinder是从何而来,发现是在ModelAttributeMethodProcessor类中由WebDataBinderFactory创建的

f (bindingResult == null) {
    // Bean property binding and validation;
    // skipped in case of binding failure on construction.
    WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
    if (binder.getTarget() != null) {
       if (!mavContainer.isBindingDisabled(name)) {
          bindRequestParameters(binder, webRequest);
       }
       validateIfApplicable(binder, parameter);
       if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
          throw new BindException(binder.getBindingResult());
       }
    }
    // Value type adaptation, also covering java.util.Optional
    if (!parameter.getParameterType().isInstance(attribute)) {
       attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
    }
    bindingResult = binder.getBindingResult();
}

查看源码发现WebDataBinderFactory是由RequestMappingHandlerAdapter创建的,如果其进行修改,一旦有没有考虑到的地方影响会很大,因此考虑将Spring的ServletModelAttributeMethodProcessor复制出来,将里面的WebDataBinderFactory修改为自定义的,由自定义的WebDataBinderFactory创建自定义的ServletRequestDataBinder去进行数据处理。

首先创建自定义的数据绑定器CustomServletRequestDataBinder

public class CustomServletRequestDataBinder extends ServletRequestDataBinder {
    public CustomServletRequestDataBinder(Object target) {
        super(target);
    }

    public CustomServletRequestDataBinder(Object target, String objectName) {
        super(target, objectName);
    }


    @Override
    protected void applyPropertyValues(MutablePropertyValues mpvs) {
        Object target = getTarget();
        PropertyValue[] propertyValues = mpvs.getPropertyValues();
        Map<String, PropertyValue> map = Arrays.stream(propertyValues).collect(Collectors.toMap(e -> e.getName(), e -> e));

        Field[] declaredFields = target.getClass().getDeclaredFields();
        for (Field declaredField : declaredFields) {
            if (declaredField.isAnnotationPresent(IdCrypto.class)) {
                String name = declaredField.getName();
                PropertyValue propertyValue = map.get(name);
                mpvs.removePropertyValue(propertyValue);
                mpvs.addPropertyValue(new PropertyValue(propertyValue,"999"));
            }
        }
        super.applyPropertyValues(mpvs);
    }
}

接着创建自定义的数据绑定器工厂CustomWebDataBinderFactory,让它创建我们的自定义绑定器

public class CustomWebDataBinderFactory extends DefaultDataBinderFactory {
    /**
     * Create a new {@code DefaultDataBinderFactory} instance.
     *
     * @param initializer for global data binder initialization
     *                    (or {@code null} if none)
     */
    public CustomWebDataBinderFactory(WebBindingInitializer initializer) {
        super(initializer);
    }

    @Override
    protected WebDataBinder createBinderInstance(Object target, String objectName, NativeWebRequest webRequest) throws Exception {
        return new CustomServletRequestDataBinder(target, objectName);
    }
}

接着复制Spring的ServletModelAttributeMethodProcessor,由于ServletModelAttributeMethodProcessor继承了ModelAttributeMethodProcessor,并且创建绑定器的操作也是在ModelAttributeMethodProcessor中进行的,因此也需要ModelAttributeMethodProcessor复制一份CustomModelAttributeMethodProcessor,并给他传递自定义的绑定器的工厂CustomWebDataBinderFactory

public class CustomModelAttributeMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {

    private static final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();

    protected final Log logger = LogFactory.getLog(getClass());

    private final boolean annotationNotRequired;
    private final CustomWebDataBinderFactory customWebDataBinderFactory;
  
  ......
  ......
}    

将创建绑定器的地方修改为通过自定义的工厂创建

if (mavContainer.containsAttribute(name)) {
    attribute = mavContainer.getModel().get(name);
}
else {
    // Create attribute instance
    try {
      // 使用自定义工厂
        attribute = createAttribute(name, parameter, customWebDataBinderFactory, webRequest);
    }
    catch (BindException ex) {
        if (isBindExceptionRequired(parameter)) {
            // No BindingResult parameter -> fail with BindException
            throw ex;
        }
        // Otherwise, expose null/empty value and associated BindingResult
        if (parameter.getParameterType() == Optional.class) {
            attribute = Optional.empty();
        }
        bindingResult = ex.getBindingResult();
    }
}

if (bindingResult == null) {
    // Bean property binding and validation;
    // skipped in case of binding failure on construction.
    // 使用自定义工厂
    WebDataBinder binder = customWebDataBinderFactory.createBinder(webRequest, attribute, name);
    if (binder.getTarget() != null) {
        if (!mavContainer.isBindingDisabled(name)) {
            bindRequestParameters(binder, webRequest);
        }
        validateIfApplicable(binder, parameter);
        if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
            throw new BindException(binder.getBindingResult());
        }
    }
    // Value type adaptation, also covering java.util.Optional
    if (!parameter.getParameterType().isInstance(attribute)) {
        attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
    }
    bindingResult = binder.getBindingResult();
}

复制ServletModelAttributeMethodProcessor

public class CustomServletModelAttributeMethodProcessor extends CustomModelAttributeMethodProcessor{


    /**
     * Class constructor.
     *
     * @param customWebDataBinderFactory
     * @param annotationNotRequired      if "true", non-simple method arguments and
     *                                   return values are considered model attributes with or without a
     *                                   {@code @ModelAttribute} annotation
     */
    public CustomServletModelAttributeMethodProcessor(CustomWebDataBinderFactory customWebDataBinderFactory, boolean annotationNotRequired) {
        super(customWebDataBinderFactory, annotationNotRequired);
    }
  
  ......
  ......
    
}      

最后注册到RequestMappingHandlerAdapter中

@Autowired
private RequestMappingHandlerAdapter requestMappingHandlerAdapter;


@PostConstruct
public void init() {
    List<HandlerMethodArgumentResolver> argumentResolvers = requestMappingHandlerAdapter.getArgumentResolvers();
    List<HandlerMethodArgumentResolver> list = new ArrayList<>();

    list.add(0, new IdWithPathVariableValueConvert());
    list.add(1, new CustomServletModelAttributeMethodProcessor(new CustomWebDataBinderFactory(null), true));
   list.add(2, new CustomeRequestParamAttributeMethodProcessor());
    list.addAll(argumentResolvers);
    requestMappingHandlerAdapter.setArgumentResolvers(list);
}

采用此方式注册的原因已在处理地址参数的时候说明,不再赘述。

后续发现无论是form-data的请求,还是application/json的请求,都会进入这里,而且平铺模式也会进行处理,于是对supportsParameter进行扩展

@Override
public boolean supportsParameter(MethodParameter parameter) {
    Parameter[] parameters = parameter.getMethod().getParameters();
    if (parameter == null || parameters.length == 0) {
        return false;
    }
    long excludeParameterCount = Arrays.stream(parameters)
            .filter(e -> !e.isAnnotationPresent(PathVariable.class))
            .filter(e -> !ServletResponse.class.isAssignableFrom(e.getType()))
            .filter(e -> !ServletRequest.class.isAssignableFrom(e.getType()))
            .filter(e -> !MultipartFile.class.isAssignableFrom(e.getType())).count();
    return (parameter.hasParameterAnnotation(ModelAttribute.class) ||
            (excludeParameterCount == 1 && this.annotationNotRequired && !BeanUtils.isSimpleProperty(parameter.getParameterType()) && !parameter.hasParameterAnnotation(RequestBody.class)));
}

要求参数在排除了地址参数、ServletRequest、ServletResponse、MultipartFile的下只能有一个参数,并且不能含有RequestBody注解。

在后续的调试过程中发现,有的接口虽然只有一个参数,但是不是它并不是用的我们自己创建的类,比如使用的是 List,或者 String 这种,这种场景下也会进入到我们的自定义对象处理器中,
因此继续增加限制条件,要求这个单一的参数必须是由我们项目里创建的,根据标准的包命名规范,报名最开始采用公司域名反写,因此,检查参数的class全限定名称,检查其是否由公司域名反写开头。

public boolean supportsParameter(MethodParameter parameter) {
        Parameter[] parameters = parameter.getMethod().getParameters();
        if (parameter == null || parameters.length == 0) {
            return false;
        }
        long excludeParameterCount = Arrays.stream(parameters)
                .filter(e -> !e.isAnnotationPresent(PathVariable.class))
                .filter(e -> !ServletResponse.class.isAssignableFrom(e.getType()))
                .filter(e -> !ServletRequest.class.isAssignableFrom(e.getType()))
                .filter(e -> !MultipartFile.class.isAssignableFrom(e.getType())).count();
        boolean isCustomClass = parameter.getParameterType().getName().startsWith("xx.yyy.zzz");
        return (parameter.hasParameterAnnotation(ModelAttribute.class) ||
                (excludeParameterCount == 1 && isCustomClass && this.annotationNotRequired && !BeanUtils.isSimpleProperty(parameter.getParameterType()) && !parameter.hasParameterAnnotation(RequestBody.class)));
    }


后续如要限制死为HttpGet请求,则在supportsParameter中判断Method的Http注解即可。