SpringMVC自動封裝List物件——自定義引數解析器
前臺傳遞的引數為集合物件時,後臺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) 具體的引數解析實現。
import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import org.springframework.beans.BeanUtils; import org.springframework.core.MethodParameter; import org.springframework.stereotype.Component; 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/> * 目前僅支援List集合中元素為基本資料型別或由基本資料型別組成的簡單物件 * @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 { List<Object> actualParameter = new ArrayList<Object>(); String[] parameterValues = null; Object element = null; Object attributeValue = null; Method setter = null; Class<?> elementClass = getElementTypeFromAnnotation(parameter); WebDataBinder binder = binderFactory.createBinder(webRequest, actualParameter, parameter.getParameterName()); if (BeanUtils.isSimpleValueType(elementClass)) { // 如果是基本物件集合,直接轉換型別後新增到集合中 parameterValues = webRequest.getParameterValues(parameter.getParameterName()); for (int i = 0; i < parameterValues.length; i++) { attributeValue = binder.convertIfNecessary(parameterValues[i], elementClass); actualParameter.add(attributeValue); } } else { // 否則,使用反射獲取屬性的setter方法注入值 Map<String, Method> setterMap = getSetterMap(elementClass); Set<String> attributeNameSet = setterMap.keySet(); for (String attributeName : attributeNameSet) { parameterValues = webRequest.getParameterValues(attributeName); if (null == parameterValues) { continue; } setter = setterMap.get(attributeName); for (int i = 0; i < parameterValues.length; i++) { if (actualParameter.size() <= i) { element = BeanUtils.instantiateClass(elementClass); actualParameter.add(element); } else { element = actualParameter.get(i); } attributeValue = binder.convertIfNecessary(parameterValues[i], setter.getParameterTypes()[0]); setter.invoke(element, attributeValue); } } } return actualParameter; } private Class<?> getElementTypeFromAnnotation(MethodParameter parameter) { ListParam parameterAnnotation = parameter.getParameterAnnotation(ListParam.class); return parameterAnnotation.value(); } private Map<String, Method> getSetterMap(Class<?> clz) { Map<String, Method> setterMap = new HashMap<String, Method>(); Field[] fields = clz.getDeclaredFields(); Method[] declaredMethods = clz.getDeclaredMethods(); for (Field field : fields) { for (Method method : declaredMethods) { if (isSetter(field, method)) { setterMap.put(field.getName(), method); break; } } } return setterMap; } private boolean isSetter(Field field, Method method) { Class<?>[] parameterTypes = method.getParameterTypes(); if (parameterTypes.length != 1 || !parameterTypes[0].equals(field.getType())) { return false; } return method.getName().equalsIgnoreCase("set" + field.getName()); } }
定義好解析器後,需要注入到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註解,申明一下型別就好。最後還是給個使用的小例子吧!
@RestController public class UserContoller { @Autowired private UserService userService; @RequestMapping("/addUsers") public Object addUsers(@ListParam(User.class) List<User> users){ return userService.addUsers(users); } }