Announcement Announcement Module
Collapse
No announcement yet.
Custom HandlerMethodArgumentResolver that uses messageConverters Page Title Module
Move Remove Collapse
X
Conversation Detail Module
Collapse
  • Filter
  • Time
  • Show
Clear All
new posts

  • Custom HandlerMethodArgumentResolver that uses messageConverters

    Hello
    I want to share my experience creating custom HandlerMethodArgumentResolver that binds and validates json object received in request parameter in a same fashion as @RequestBody and @RequestPart work. I will omit explanation why I need it, but let's say I had no choice.

    Please correct any false statements you would find.

    Ok, here is the problem:
    1)messageConverters availability: Although default resolvers receive messageConverters in the constructor, your custom resolver can't. This is because it's instance is created before RequestMappingHandlerAdapter that provides access to messageConverters and RequestMappingHandlerAdapter needs to know your resolver in it's creation time. If you try to add your resolver after RequestMappingHandlerAdapter is created, the only access is provided by HandlerMethodArgumentResolverComposite, that allows adding resolvers only at the end of the list. This is a real issue because the method parameter will get processed by "catch-all" resolvers, namely RequestParamMethodArgumentResolver, so you need to place your custom resolver higher in the list. The only remaining solution is to register your resolver without messageConverters and get converters on first invocation. In this scenario you can't extend any abstract resolver that receives messageConverters in constructor(for example AbstractMessageConverterMethodArgumentResolver)

    2)HttpMessageConverter implemetation doesn't provide any way for reading serialized object from request parameter. They basicaly invoke HttpInputMessage.getBody() which works only for @RequestBody and @RequestPart.

    Here is working implementation I produced after half day struggle, learning Spring internals on the way. For the most part it's customized copy/paste from existing resolvers. It's using beanFactory to get messageConverters on first invocation and creates fake HttpInputMessage with parameter content in the body to satisfy mesage converter. Cotent type is determine from custom annotation. I can't use content-type header itself, because I'm limited to "application/x-www-form-urlencoded". More convenient way would be to use a custom header to receive parameter content type from client application, but I don't need such implementation for now.

    HandlerMethodArgumentResolver:
    Code:
           public class RequestFormEntityMethodArgumentResolver  implements HandlerMethodArgumentResolver{
    	
    	private final Logger log = LoggerFactory.getLogger(getClass());
    	
    	protected List<HttpMessageConverter<?>> messageConverters;
    	
    	private final BeanFactory beanFactory;
    	
    	public RequestFormEntityMethodArgumentResolver(BeanFactory beanFactory){
    		this.beanFactory = beanFactory;
    	}
    
    
    	@SuppressWarnings("unchecked")
    	protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter, Class<T> paramType) throws IOException,
    			HttpMediaTypeNotSupportedException {
    		
    				RequestFormEntity annot = parameter.getParameterAnnotation(RequestFormEntity.class);
    				MediaType contentType   = MediaType.parseMediaType(annot.contentType());
    			
    				for (HttpMessageConverter<?> messageConverter : getMessageConverters()) {
    					if (messageConverter.canRead(paramType, contentType)) {
    						if (log.isDebugEnabled()) {
    							log.debug("Reading [" + paramType.getName() + "] as \"" + contentType + "\" using [" +
    									messageConverter + "]");
    						}
    						return ((HttpMessageConverter<T>) messageConverter).read(paramType, inputMessage);
    					}
    				}
    			
    				throw new IbException(ReturnCode.UNEXPECTED_ERROR);
    			}	
    
    	/**
    	 * Supports the following:
    	 * <ul>
    	 * 	<li>@RequestFormEntity-annotated method arguments. 
    	 * 	<li>Arguments of type {@link BaseInput} 
    	 * 		unless annotated with @{@link RequestPart} or @{@link RequestBody}.
    	 * </ul>
    	 */
    	@Override
    	public boolean supportsParameter(MethodParameter parameter) {
    		Class<?> paramType = parameter.getParameterType();
    		if (parameter.hasParameterAnnotation(RequestFormEntity.class)) {
    			return true;
    		}
    		else {
    			if (BaseInput.class.isAssignableFrom(paramType)) {				
    				if (parameter.hasParameterAnnotation(RequestPart.class)) {
    					return false;
    				}
    				if (parameter.hasParameterAnnotation(RequestBody.class)) {
    					return false;
    				}
    				return true;
    			}
    			else {
    				return false;
    			}
    		}
    	}
    
    	@Override
    	public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, 
    			NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
    		
    		String paramName = getParamName(parameter);  
    		Object arg;
    		
    		try {
    			arg = readWithMessageConverters(createInputMessage(webRequest, paramName), parameter, parameter.getParameterType());
    			if (arg != null) {
    				Annotation[] annotations = parameter.getParameterAnnotations();
    				for (Annotation annot : annotations) {
    					if (annot.annotationType().getSimpleName().startsWith("Valid")) {
    						WebDataBinder binder = binderFactory.createBinder(webRequest, arg, paramName);
    						Object hints = AnnotationUtils.getValue(annot);
    						binder.validate(hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
    						BindingResult bindingResult = binder.getBindingResult();
    						if (bindingResult.hasErrors()) {
    							throw new MethodArgumentNotValidException(parameter, bindingResult);
    						}
    					}
    				}
    			}
    		}
    		catch (MissingServletRequestParameterException ex) {
    			// handled below
    			arg = null;
    		}				
    		
    		RequestPart annot = parameter.getParameterAnnotation(RequestPart.class);
    		boolean isRequired = (annot == null || annot.required());
    
    		if (arg == null && isRequired) {
    			throw new MissingServletRequestParameterException(paramName, parameter.getParameterType().getSimpleName());
    		}
    		
    		return arg;
    	}
    	
    	
    	private String getParamName(MethodParameter parameter) {
    		
    		RequestFormEntity annot = parameter.getParameterAnnotation(RequestFormEntity.class);
    		String paramName = (annot != null) ? annot.value() : "";
    		if (paramName.length() == 0) {
    			paramName = parameter.getParameterName();
    			Assert.notNull(paramName, "Request parameter name for argument type [" + parameter.getParameterType().getName()
    					+ "] not available, and parameter name information not found in class file either.");
    		}
    		return paramName;
    	}
    	
    	
    	/**
    	 * Creates a new {@link HttpInputMessage} from the given {@link NativeWebRequest}.
    	 *
    	 * @param webRequest the web request to create an input message from MockHttpInputMessage
    	 * @param paramName the name of parameter
    	 * @return fake input message
    	 */
    	protected HttpInputMessage createInputMessage(final NativeWebRequest webRequest, final String paramName) {
    		return new HttpInputMessage(){
    
    			@Override
    			public HttpHeaders getHeaders() {
    				return null;
    			}
    
    			@Override
    			public InputStream getBody() throws IOException {
    				return new ByteArrayInputStream(webRequest.getParameter(paramName).getBytes());
    			}};
    	}	
    
    	
    	
    	private List<HttpMessageConverter<?>> getMessageConverters(){
    		if(messageConverters == null){
    			messageConverters = beanFactory.getBean(RequestMappingHandlerAdapter.class).getMessageConverters();
    		}
    		return messageConverters;
    	}
    }
    Custom annotation:
    Code:
    @Target(ElementType.PARAMETER)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface RequestFormEntity {
    
    	/**
    	 * The name of the request parameter..
    	 */	
    	String value() default "args";
    	
    	String contentType() default MediaType.APPLICATION_JSON_VALUE;
    
    	boolean required() default true;	
    }
    Config:
    Code:
    @EnableWebMvc   
    @Configuration
    public class ServletContext extends WebMvcConfigurerAdapter {
    	
    	@Autowired BeanFactory beanFactory;
    	
    	@Override
    	public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
    		argumentResolvers.add(new RequestFormEntityMethodArgumentResolver(beanFactory));
    	}
    }
    Controller:
    Code:
    	@RequestMapping(value="/somemapping", method = RequestMethod.POST)
    	public @ResponseBody OutputObject somemapping(@RequestFormEntity @Valid InputObject args){
    		return myservice.dosomething(args);
    	}
    Feel free to recommend better solution.

    Pavel

  • #2
    I would extend AbstractMessageConverterMethodArgumentResolver and override the method createInputMessage. Then return a ServletServerHttpRequest sub-class that can read the body from a request parameter. For example see how ServletServerHttpRequest already does something similar in its method getBodyFromServletRequestParameters.

    Comment

    Working...
    X