1. 程式人生 > >SpringMVC自動封裝List物件——自定義引數解析器

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);
    }
}