首先讲一下本文对应的需求,毕竟脱离现实讲的都是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;
}
首先并不是所有请求参数都要进行转换,所以先定义一个参数过滤条件来处理只对特定参数的转换。
- 创建一个注解,来标记哪个参数需要转换
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface Sldp {
}
- 实现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接口实现。一些扩展的使用可以参考我下面的项目 项目源码地址。
网友评论