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

放几个阿里云的优惠链接 代金券 / 高性能服务器2折起 / 高性能服务器5折

首先讲一下本文对应的需求,毕竟脱离现实讲的都是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)