美文网首页程序员
Spring boot Mvc实现自定义参数类型解析和转换

Spring boot Mvc实现自定义参数类型解析和转换

作者: 不废的废柴 | 来源:发表于2019-02-21 16:10 被阅读15次

    首先讲一下本文对应的需求,毕竟脱离现实讲的都是P话。一般做项目的时候,由于需求多变都会遇到一个问题,一个接口最初设计的参数数据模型已经无法满足新的需求了。这个时候一般就2种做法(可能更多):1. 新做一个接口, 2. 在原接口上加入新参数或者在数据模型上加。但这样做最大的问题就是可能新加的参数就一两个,但是重复的代码抄好几份。一个项目重复代码多了维护的成本也就高了。
    其实在一般来说,处理这些需求的时候自己已经定义了一个继承的参数模型。下面就讲一下如何使用spring实现参数的自动转换为对应的子类。通过实现HandlerMethodArgumentResolver接口来完成这个功能。

    自定义参数解析实现动态转换类型

    spring里面参数解析基本都是通过HandlerMethodArgumentResolver接口的实现类来完成(如果不明白这里就不解释了,还是自行搜索吧),所以要实现上面的需求必然会用到这个接口。

    自定义 HandlerMethodArgumentResolver

    创建一个自己的HandlerMethodArgumentResolver,这个接口有2个方法。

    public interface HandlerMethodArgumentResolver {
        // 看名字就可以理解,是否使用这个类来执行参数解析
        // 返回值为tue的时候才会执行resolveArgument方法
        boolean supportsParameter(MethodParameter parameter);
    
        // 实际解析参数的方法
        @Nullable
        Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
                NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
    
    }
    

    首先并不是所有请求参数都要进行转换,所以先定义一个参数过滤条件来处理只对特定参数的转换。

    1. 创建一个注解,来标记哪个参数需要转换
    @Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Sldp {
    }
    
    1. 实现supportsParameter方法进行筛选
    public class MyHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
        // 当参数上有这个注解的时候才返回true
        @Override
        public boolean supportsParameter(MethodParameter parameter) {
            Sldp sldp = parameter.getParameterAnnotation(Sldp.class);
            return sldp != null;
        }
    
    

    下面就可以执行实际的解析转换了。

    首先实际类型既然是不确定的,这时候就需要用到反射来实现了。所以实现resolveArgument方法的时候需要能够获取到实际的类型信息。姑且就先放在请求参数里面吧。

    public class MyHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
    
        @Override
        public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
            // 从请求参数中取出实际类型,这里还可以做写额外操作,先简单的实现功能
            String realClassName = webRequest.getParameter("TypeName");
    
            Class<?> realClass = ClassUtils.forName(realClassName, getClass().getClassLoader());
            if (!ClassUtils.isAssignable(parameter.getParameterType(), realClass)) {
                throw new IllegalStateException("sldp real class [ " + realClassName + " ] not cast [ " + parameter.getParameterType().getName() + " ]");
            }
            // 创建实例
            Object obj = realClass.newInstance();
            // 是用WebDataBinder绑定参数到类型
            // spring本身就有一个很方便的参数绑定机制,直接使用就可以很方便的实现参数注入
            // createBinder中的第三个参数还不知道作用是什么,知道读者可以给我解释一下
            WebDataBinder binder = binderFactory.createBinder(webRequest, obj, parameter.getParameterName());
            ServletRequest servletRequest = webRequest.getNativeRequest(ServletRequest.class);
            Assert.state(servletRequest != null, "No ServletRequest");
    
            ServletRequestDataBinder servletBinder = (ServletRequestDataBinder) binder;
            servletBinder.bind(servletRequest);
            return obj;
        }
    
    

    完成上面这些操作就已经实现的参数的动态解析和绑定了,然后就是把它放到spring容器中去了。
    由于这个解析需求的优先级并不高,所以可以直接通过WebMvcConfigurer 加入进去就可以了。

        @Bean
        public WebMvcConfigurer sldpWebMvcConfigurer() {
            return new WebMvcConfigurer() {
                @Override
                public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
                    resolvers.add(new MyHandlerMethodArgumentResolver());
                }
            };
        }
    

    添加使用接口

    先创建数据模型Animal和Cat

    public class Animal {
        private String name;
        private int age;
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public int getAge() {
            return age;
        }
    
        public void setAge(int age) {
            this.age = age;
        }
    }
    public class Cat extends Animal {
        private String speed;
    
        public String getSpeed() {
            return speed;
        }
    
        public void setSpeed(String speed) {
            this.speed = speed;
        }
    }
    
    

    添加访问接口,在参数上加上注解

    @RestController
    public class TestController {
        private final static Logger log = getLogger(TestController.class);
    
        @RequestMapping("/1")
        public void test1(@Sldp Animal a) {
            log.info("test1: {}", a);
        }
    }
    
    

    当以下面这种方式访问时实际的Animal类型就是Cat啦

    http://xxxxx/1?name=test&age=12&speed=120&TypeName=top.shenluw.sldp.Cat
    

    实现参数验证功能

    现在已经实现了基本的参数转换,但是Spring的数据验证就没法直接用了,下面就通过一些修改实现数据验证。
    其实WebDataBinder已经附带了参数的验证功能,所以只需要简单调用一下就可以实现了。

    直接修改resolveArgument方法,在原内容下加入验证逻辑。

      public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
            ... 省略上面部分
            servletBinder.bind(servletRequest);
            // 在bind后直接加入验证逻辑即可
            validate(servletBinder, parameter, mavContainer, webRequest);
            return obj;
        }
        
        protected void validate(WebDataBinder binder, MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
            validateIfApplicable(binder, parameter);
            if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
                throw new BindException(binder.getBindingResult());
            }
        }
    
        protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
            for (Annotation ann : parameter.getParameterAnnotations()) {
                Object[] validationHints = determineValidationHints(ann);
                if (validationHints != null) {
                    binder.validate(validationHints);
                    break;
                }
            }
        }
    
        protected boolean isBindExceptionRequired(WebDataBinder binder, MethodParameter parameter) {
            return isBindExceptionRequired(parameter);
        }
    
        protected boolean isBindExceptionRequired(MethodParameter parameter) {
            int i = parameter.getParameterIndex();
            Class<?>[] paramTypes = parameter.getExecutable().getParameterTypes();
            boolean hasBindingResult = (paramTypes.length > (i + 1) && Errors.class.isAssignableFrom(paramTypes[i + 1]));
            return !hasBindingResult;
        }
    
        @Nullable
        private Object[] determineValidationHints(Annotation ann) {
            Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
            if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
                Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
                if (hints == null) {
                    return new Object[0];
                }
                return (hints instanceof Object[] ? (Object[]) hints : new Object[]{hints});
            }
            return null;
        }
    

    这样就实现了参数的验证,使用方法和平时的一样,在需要的地方加入Validated或者Valid注解即可。

    上面介绍的内容只实现了一个简单的使用,原理应该讲的比较清楚了,主要是通过HandlerMethodArgumentResolver接口实现。一些扩展的使用可以参考我下面的项目 项目源码地址

    相关文章

      网友评论

        本文标题:Spring boot Mvc实现自定义参数类型解析和转换

        本文链接:https://www.haomeiwen.com/subject/mcfdyqtx.html