Daily use: spring boot custom parameter resolver

Posted by seidel on Fri, 15 Nov 2019 07:11:18 +0100

SpringBoot custom parameter resolver

We all know that SpringMvc's Controller method can receive various parameters, such as HttpServletRequest or HttpServletResponse, various annotations @ RequestParam, @ RequestHeader, @ RequestBody, @ PathVariable, @ ModelAttribute. Where are these parameters obtained?

These parameters are parsed for us by different parameter parsers, which can parse classes or annotated classes

  • We can use the parser to parse the custom parameters (class, annotation), upload them into the controller method we need (we don't need to do a series of operations inside the method through the request, response and other parameters to get the class object every time)

Add parser

  • When we want to customize the parsing parameters, we need to add our own parsing classes by changing the configuration of SpringBoot

The user-defined configuration class implements the WebMvcConfigurer interface, rewrites the addArgumentResolvers method to add its own parsing class (through automatic injection method injection)

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private SecKillUserArgumentResolvers secKillUserArgumentResolvers;

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

Implement a custom parser

  • We need to implement the HandlerMethodArgumentResolver processor method resolver interface, and the supportsParameter and resolveArgument methods

The interface of HandlerMethodArgumentResolver is defined as follows:

(1) supportsaparameter is used to determine whether the resolution of a parameter is supported (true if supported)

(2) resolveArgument resolves the parameter value in the request to an object (the specific operation gets the resolution object)

The following custom resolver is used to get the User object (get the User saved in redis through token). It does not need to use the request and response to get it inside the controller method every time, but can get it directly as a parameter

/**
 * Custom parameter resolver
 * Parse the user to be obtained every time and automatically pass in, without obtaining every time
 */
@Component
public class SecKillUserArgumentResolvers implements HandlerMethodArgumentResolver {

    @Autowired
    private UserService userService;

    @Override
    public boolean supportsParameter(MethodParameter methodParameter) {
        Class<?> c = methodParameter.getParameterType();
        return c == User.class;
    }

    @Override
    public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
        HttpServletRequest request = nativeWebRequest.getNativeRequest(HttpServletRequest.class);
        HttpServletResponse response = nativeWebRequest.getNativeResponse(HttpServletResponse.class);
        assert request != null;
        String paramToken = request.getParameter(UserServiceImpl.COOKIE_NAME_TOKEN);//COOKIE_NAME_TOKEN="token"
        String cookieToken = getCookieValue(request);
        if(StringUtils.isEmpty(cookieToken)&&StringUtils.isEmpty(paramToken)){//Get it in two ways. If both fail, return null
            return null;
        }else {
            String token = StringUtils.isEmpty(paramToken)?cookieToken:paramToken;
            return UserService.getUserByToken(response,token);
        }
    }
//Traverse the cookie to get the value of the cookie with the same name
    private String getCookieValue(HttpServletRequest request) {
        Cookie[] cookies = request.getCookies();
        for(Cookie cookie:cookies){
            if(cookie.getName().equals(SecKillUserServiceImpl.COOKIE_NAME_TOKEN)){
                return cookie.getValue();
            }
        }
        return null;
    }
}

At this point, a custom parameter parser will be implemented. We can get it directly through the parser in the form of incoming parameters

Use of resolution objects

  • We pass in this parameter in the controller method, and we can get the User object directly for us
@RequestMapping("/to_list")
    public String toList(Model model,User user){
    }

Other parameter parsers in Springboot

Model

  • We know that we can pass in Model object parameters and use it directly. Let's take a look at its parameter parser

ModelAttributeMethodProcessor class

public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(ModelAttribute.class) || this.annotationNotRequired && !BeanUtils.isSimpleProperty(parameter.getParameterType());
    }

    @Nullable
    public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
        Assert.state(mavContainer != null, "ModelAttributeMethodProcessor requires ModelAndViewContainer");
        Assert.state(binderFactory != null, "ModelAttributeMethodProcessor requires WebDataBinderFactory");
        String name = ModelFactory.getNameForParameter(parameter);
        ModelAttribute ann = (ModelAttribute)parameter.getParameterAnnotation(ModelAttribute.class);
        if (ann != null) {
            mavContainer.setBinding(name, ann.binding());
        }
        Object attribute = null;
        BindingResult bindingResult = null;
        if (mavContainer.containsAttribute(name)) {
            attribute = mavContainer.getModel().get(name);
        } else {
            try {
                attribute = this.createAttribute(name, parameter, binderFactory, webRequest);
            } catch (BindException var10) {
                if (this.isBindExceptionRequired(parameter)) {
                    throw var10;
                }
                if (parameter.getParameterType() == Optional.class) {
                    attribute = Optional.empty();
                }
                bindingResult = var10.getBindingResult();
            }
        }
        if (bindingResult == null) {
            WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
            if (binder.getTarget() != null) {
                if (!mavContainer.isBindingDisabled(name)) {
                    this.bindRequestParameters(binder, webRequest);
                }
                this.validateIfApplicable(binder, parameter);
                if (binder.getBindingResult().hasErrors() && this.isBindExceptionRequired(binder, parameter)) {
                    throw new BindException(binder.getBindingResult());
                }
            }
            if (!parameter.getParameterType().isInstance(attribute)) {
                attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
            }
            bindingResult = binder.getBindingResult();
        }
        Map<String, Object> bindingResultModel = bindingResult.getModel();
        mavContainer.removeAttributes(bindingResultModel);
        mavContainer.addAllAttributes(bindingResultModel);
        return attribute;
    }

RequestParam

RequestParamMethodArgumentResolver class, annotation based

If the annotation exists on the parameter passed in, it can be parsed

public boolean supportsParameter(MethodParameter parameter) {
        if (parameter.hasParameterAnnotation(RequestParam.class)) {
            if (!Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) {
                return true;
            } else {
                RequestParam requestParam = (RequestParam)parameter.getParameterAnnotation(RequestParam.class);
                return requestParam != null && StringUtils.hasText(requestParam.name());
            }
        } else if (parameter.hasParameterAnnotation(RequestPart.class)) {
            return false;
        } else {
            parameter = parameter.nestedIfOptional();
            if (MultipartResolutionDelegate.isMultipartArgument(parameter)) {
                return true;
            } else {
                return this.useDefaultResolution ? BeanUtils.isSimpleProperty(parameter.getNestedParameterType()) : false;
            }
        }
    }

PathVariable

The PathVariableMethodArgumentResolver class is also annotation based, similar to RequestParam

public boolean supportsParameter(MethodParameter parameter) {
        if (!parameter.hasParameterAnnotation(PathVariable.class)) {
            return false;
        } else if (!Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) {
            return true;
        } else {
            PathVariable pathVariable = (PathVariable)parameter.getParameterAnnotation(PathVariable.class);
            return pathVariable != null && StringUtils.hasText(pathVariable.value());
        }
    }

Custom parameter resolver based on annotation

  • Write the annotation class. The class to be parsed needs this annotation
@Target(value = ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface ParamModel {
}
  • Implement parser
public class MyArgumentResolver implements HandlerMethodArgumentResolver {
    @Override
    public boolean supportsParameter(MethodParameter methodParameter) {
        return methodParameter.hasParameterAnnotation(ParamModel.class);//Parse with comments
    }
    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer container,
            NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
        //Implement parsing object code
    }
  • Configure to WebMvcConfigurer
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private MyArgumentResolvers myArgumentResolvers;

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

Topics: Java Attribute SpringBoot Redis