前台傳遞的參數為集合對象時,后台Controller希望用一個List集合接收數據。
原生SpringMVC是不支持,Controller參數定義為List類型時,接收參數會報如下錯誤:
org.springframework.beans.BeanInstantiationException: Failed to instantiate [java.util.List]: Specified class is an interface
at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:99) ~[spring-beans-4.3.16.RELEASE.jar:4.3.16.RELEASE]
at org.springframework.web.method.annotation.ModelAttributeMethodProcessor.createAttribute(ModelAttributeMethodProcessor.java:139) ~[spring-web-4.3.16.RELEASE.jar:4.3.16.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.ServletModelAttributeMethodProcessor.createAttribute(ServletModelAttributeMethodProcessor.java:82) ~[spring-webmvc-4.3.16.RELEASE.jar:4.3.16.RELEASE]
at org.springframework.web.method.annotation.ModelAttributeMethodProcessor.resolveArgument(ModelAttributeMethodProcessor.java:106) ~[spring-web-4.3.16.RELEASE.jar:4.3.16.RELEASE]
at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:121) ~[spring-web-4.3.16.RELEASE.jar:4.3.16.RELEASE]
at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:158) ~[spring-web-4.3.16.RELEASE.jar:4.3.16.RELEASE]
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:128) ~[spring-web-4.3.16.RELEASE.jar:4.3.16.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:97) ~[spring-webmvc-4.3.16.RELEASE.jar:4.3.16.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:827) ~[spring-webmvc-4.3.16.RELEASE.jar:4.3.16.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:738) ~[spring-webmvc-4.3.16.RELEASE.jar:4.3.16.RELEASE]
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:85) ~[spring-webmvc-4.3.16.RELEASE.jar:4.3.16.RELEASE]
查看了一下源碼,發現問題在於ModelAttributeMethodProcessor解析參數時,會先使用BeanUtils.instantiateClass方法創建一個對象實例來接收參數。然而List是一個接口,不能被實例化。於是我想到,既然自帶的參數解析器不能解析,那就自定義一個參數解析器來實現這個功能。
List存在類型擦除,在運行期不能夠通過反射來獲取泛型,所以得有個辦法獲取泛型,我便定義了一個注解ListParam
/** * 強制申明List的泛型<br/> * 用於反射獲取參數類型 * * @author zengyuanjun * */ @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface ListParam { public Class<?> value(); }
接下來寫自定義的解析器ListArgumentResolver 。解析器需要實現HandlerMethodArgumentResolver接口,該接口有兩個方法:
1. supportsParameter(MethodParameter parameter) 返回當前解析器是否支持該參數。
2. resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) 具體的參數解析實現。
先使用webRequest.getParameterValues(String paramName)獲取request中的參數數組,遍歷添加到List<MutablePropertyValues>中,以分配給各個對象。再循環使用ServletRequestDataBinder綁定PropertyValues進行類型轉換,得到需要的對象集合。
import java.lang.reflect.Field; import java.util.ArrayList; import java.util.List; import org.springframework.beans.BeanUtils; import org.springframework.beans.MutablePropertyValues; import org.springframework.core.MethodParameter; import org.springframework.stereotype.Component; import org.springframework.util.ClassUtils; import org.springframework.web.bind.ServletRequestDataBinder; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; import com.zyj.springboot.study.annotation.ListParam; /** * List集合參數解析器 <br/> * @author zengyuanjun * */ @Component public class ListArgumentResolver implements HandlerMethodArgumentResolver { @Override public boolean supportsParameter(MethodParameter parameter) { if (null != parameter.getParameterAnnotation(ListParam.class) && List.class.equals(parameter.getParameterType())) { return true; } return false; } @Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { String[] parameterValues = null; MutablePropertyValues mutablePropertyValues = null; Class<?> elementClass = getElementTypeFromAnnotation(parameter); List<MutablePropertyValues> mpvList = new ArrayList<MutablePropertyValues>(); Field[] fields = elementClass.getDeclaredFields(); for (Field field : fields) { parameterValues = webRequest.getParameterValues(field.getName()); if (null == parameterValues) { continue; } for (int i = 0; i < parameterValues.length; i++) { if (mpvList.size() <= i) { mutablePropertyValues = new MutablePropertyValues(); mutablePropertyValues.add(field.getName(), parameterValues[i]); mpvList.add(mutablePropertyValues); } else { mutablePropertyValues = mpvList.get(i); mutablePropertyValues.add(field.getName(), parameterValues[i]); } } } String name = ClassUtils.getShortNameAsProperty(elementClass); Object attribute = null; WebDataBinder binder = null; ServletRequestDataBinder servletBinder = null; Object element = null; List<Object> actualParameter = new ArrayList<Object>(mpvList.size()); for (int i = 0; i < mpvList.size(); i++) { attribute = BeanUtils.instantiateClass(elementClass); binder = binderFactory.createBinder(webRequest, attribute, name); servletBinder = (ServletRequestDataBinder) binder; servletBinder.bind(mpvList.get(i)); element = binder.getTarget(); actualParameter.add(binder.convertIfNecessary(element, elementClass, parameter)); } return actualParameter; } private Class<?> getElementTypeFromAnnotation(MethodParameter parameter) { ListParam parameterAnnotation = parameter.getParameterAnnotation(ListParam.class); return parameterAnnotation.value(); } }
定義好解析器后,需要注入到RequestMappingHandlerAdapter中,這里要注意,自帶的ServletModelAttributeMethodProcessor解析器是對List類型生效的!!!所以必須把自定義的解析器放到ServletModelAttributeMethodProcessor前面,我這里直接把自定義的解析器ListArgumentResolver放到了第一個。
import java.util.LinkedList; import java.util.List; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.stereotype.Component; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; import com.zyj.springboot.study.resolver.ListArgumentResolver; /** * 初始化將自定義的ListArgumentResolver注入到RequestMappingHandlerAdapter中 * @author zengyuanjun * */ @Component public class InitialListArgumentResolver implements ApplicationContextAware { @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { RequestMappingHandlerAdapter handlerAdapter = applicationContext.getBean(RequestMappingHandlerAdapter.class); List<HandlerMethodArgumentResolver> argumentResolvers = handlerAdapter.getArgumentResolvers(); List<HandlerMethodArgumentResolver> resolvers = new LinkedList<HandlerMethodArgumentResolver>(); resolvers.add(new ListArgumentResolver()); resolvers.addAll(argumentResolvers); handlerAdapter.setArgumentResolvers(resolvers); } }
然后?就沒有然后了,使用時在List參數前加上ListParam注解,申明一下類型就好。最后還是給個使用的小例子吧!
頁面定義了多個user對象的信息,它們的name是重復的。
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>(-.-) ZYJ (*-*)</title> </head> <body> <form action="http://localhost:8080/addUser"> <input name="id" value="1"></input> <input name="userName" value="user1"></input> <input name="telephone" value="13411111111"></input> <input name="birthDay" value="2018-11-01"></input> <input name="id" value="2"></input> <input name="userName" value="user2"></input> <input name="telephone" value="15822222222"></input> <input name="birthDay" value="2018-11-02"></input> <input name="id" value="3"></input> <input name="userName" value="user3"></input> <input name="telephone" value="18033333333"></input> <input name="birthDay" value="2018-11-03"></input> </form> <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script> <script> $(function(){ $.ajax({ url : $('form').attr('action'), type : 'post', data : $('form').serialize(), success : function(data){ alert(data); } }) }) </script> </body> </html>
后台controller使用List<User>接收,注意要加上@ListParam(User.class)申明類型。
@RestController public class UserContoller { @Autowired private UserService userService; @RequestMapping("/addUsers") public Object addUsers(@ListParam(User.class) List<User> users){ return userService.addUsers(users); } }