在国内企业开发项目中大多数都已经偏向Spring
家族式的开发风格,在前几年国内项目都是以Structs2
作为Web
开发的主导,不过由于近几年发生的事情确实让开发者对它失去了以往的信心。与此同时Spring
家族发布了SpringMVC
,而且完美的整合Spring
来开发企业级大型Web
项目。它有着比Structs2
更强大的技术支持以及更灵活的自定义配置,接下来我们就看看本章的内容,我们自定义实现SpringMVC
参数绑定规则,根据业务定制参数装载实现方式。
本章目标
根据项目定制SpringMVC
参数状态并了解SpringMVC
的装载过程以及实现方式。
构建项目
我们先来创建一个SpringBoot
项目,添加本章所需的依赖,pom.xml配置文件如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| ...//省略部分配置 <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-jasper</artifactId> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.38</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> ...//省略部分配置
|
本章需要JSP
相关的依赖支持,所以需要添加对应的依赖,修改application.properties
配置文件让JSP
生效,配置内容如下所示:
1 2
| spring.mvc.view.prefix=/WEB-INF/jsp/ spring.mvc.view.suffix=.jsp
|
相关JSP
配置可以访问第二章:SpringBoot与JSP间不可描述的秘密查看讲解。
SpringMVC的参数装载
在讲解我们自定义参数装载之前,我们先来看看SpringMVC
内部为我们提供的参数装载方式。
添加测试JSP
我们首先来添加一个测试的jsp页面,页面上添加一些输入元素,代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <%-- Created by IntelliJ IDEA. User: hengyu Date: 2017/9/17 Time: 10:33 To change this template use File | Settings | File Templates. --%> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Title</title> </head> <body> <form method="post" action="/submit"> 教师姓名:<input type="text" name="name"/><br/><br/> 学生姓名:<input type="text" name="name"/><br/><br/> 学生年龄:<input type="text" name="age"/><br/><br/> <input type="submit"/> </form> </body> </html>
|
在index.jsp
内添加了三个name
的文本输入框,如果我们现在提交到后台SpringMVC
为默认为我们解析成一个数组,如果根据描述而言的来处理则是不合理的,当然也可以使用各种手段完成字段参数的装载,比如:为教师的name
添加一个数组或者List集合进行接受,这种方式也是可以实现但不优雅
。
如果你们项目组有严格的开发规范要求,这种方式是不允许出现在Controller
方法内的。
那这个问题就让人头疼了,在之前我们使用Struct2
的时候是可以根据指定的前缀,如:xxx.xxx
来进行映射的,而SpringMVC
并没有提供这个支持,不过它提供了自定义参数装载的实现方法,那就没有问题了,我们可以手写。
自定义的参数装载
既然上面的代码实现满足不了我们的需求,那么我接下来就来重写参数装载。
创建ParameterModel注解
对于一直使用SpringMVC
的朋友来说,应该对@RequestParam
很熟悉,而本章我们自定义的注解跟@RequestParam
类似,主要目的也是标识指定参数完成数据的绑定。下面我们先来看看该注解的源码,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| package com.yuqiyu.chapter36.annotation;
import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target;
@Target(value = ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) public @interface ParameterModel { }
|
该注解目前没有添加任何一个属性,这个也是可以根据项目的需求已经业务逻辑进行相应添加的,比如@RequestParam
内常用的属性required
、defaultValue
等属性,由于我们本章内容不需要自定义注解内的属性所以这里就不添加了。
该注解的作用域是在参数上@Target(value = ElementType.PARAMETER)
,我们仅可以在方法参数上使用。
创建参数接受实体
我们可以回到上面看看index.jsp
的内容,我们需要教师的基本信息以及学生的基本信息,那我们就为教师、以及学生创建实体(注意:这个实体可以是对应数据库内的实体)
教师实体
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| package com.yuqiyu.chapter36.bean;
import lombok.Data;
@Data public class TeacherEntity { private String name; }
|
教师实体内目前为了测试就添加一个跟页面参数有关的字段。
学生实体
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| package com.yuqiyu.chapter36.bean;
import lombok.Data;
@Data public class StudentEntity { private String name; private String age; }
|
学生实体添加与页面参数对应的字段,名称、年龄。
编写CustomerArgumentResolver参数装载
在写参数装载之前,我们需要先了解下它的接口HandlerMethodArgumentResolver
,该接口内定义了两个方法:
supportsParameter
1
| boolean supportsParameter(MethodParameter var1);
|
supportsParameter
方法顾名思义,是允许装载的参数,也就是说方法返回true
时才会指定装载方法完成参数装载。
resolveArgument
1
| Object resolveArgument(MethodParameter var1, ModelAndViewContainer var2, NativeWebRequest var3, WebDataBinderFactory var4) throws Exception;
|
resolveArgument
方法是参数状态的实现逻辑方法,该方法返回的值会直接装载到指定的参数上,有木有很神奇啊?下面我们就创建实现类来揭开这位神奇的姑娘的面纱吧!
创建CustomerArgumentResolver
实现接口HandlerMethodArgumentResolver
内的两个方法,具体实现代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346
| package com.yuqiyu.chapter36.resovler;
import com.yuqiyu.chapter36.annotation.ParameterModel; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.BeanUtils; import org.springframework.core.MethodParameter; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; import org.springframework.util.StringUtils; import org.springframework.validation.DataBinder; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; import org.springframework.web.servlet.HandlerMapping;
import java.lang.reflect.Field; import java.util.*;
public class CustomerArgumentResolver implements HandlerMethodArgumentResolver {
private Logger logger = LoggerFactory.getLogger(CustomerArgumentResolver.class);
@Override public boolean supportsParameter(MethodParameter methodParameter) { return methodParameter.hasParameterAnnotation(ParameterModel.class); }
@Override public Object resolveArgument ( MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory ) throws Exception { String parameterName = methodParameter.getParameterName(); logger.info("参数名称:{}",parameterName);
Object target = modelAndViewContainer.containsAttribute(parameterName) ? modelAndViewContainer.getModel().get(parameterName) : createAttribute(parameterName, methodParameter, webDataBinderFactory, nativeWebRequest);;
return target; }
protected Object createAttribute(String attributeName, MethodParameter parameter, WebDataBinderFactory binderFactory, NativeWebRequest request) throws Exception {
String value = getRequestValueForAttribute(attributeName, request);
if (value != null) {
Object attribute = convertAttributeToParameterValue(value, attributeName, parameter, binderFactory, request);
if (attribute != null) { return attribute; } }
else { Object attribute = putParameters(parameter,request); if(attribute!=null) { return attribute; } }
return BeanUtils.instantiateClass(parameter.getParameterType()); }
protected Object convertAttributeToParameterValue(String sourceValue, String attributeName, MethodParameter parameter, WebDataBinderFactory binderFactory, NativeWebRequest request) throws Exception {
DataBinder binder = binderFactory.createBinder(request, null, attributeName); ConversionService conversionService = binder.getConversionService(); if (conversionService != null) {
TypeDescriptor source = TypeDescriptor.valueOf(String.class);
TypeDescriptor target = new TypeDescriptor(parameter);
if (conversionService.canConvert(source, target)) {
return binder.convertIfNecessary(sourceValue, parameter.getParameterType(), parameter); } } return null; }
protected String getRequestValueForAttribute(String attributeName, NativeWebRequest request) {
Map<String, String> variables = getUriTemplateVariables(request);
if (StringUtils.hasText(variables.get(attributeName))) { return variables.get(attributeName); }
else if (StringUtils.hasText(request.getParameter(attributeName))) { return request.getParameter(attributeName); } else { return null; } }
protected Map<String, String[]> getPrefixParameterMap(String namePrefix, NativeWebRequest request, boolean subPrefix) { Map<String, String[]> result = new HashMap();
Map<String, String> variables = getUriTemplateVariables(request);
int namePrefixLength = namePrefix.length(); for (String name : variables.keySet()) { if (name.startsWith(namePrefix)) {
if (subPrefix) { char ch = name.charAt(namePrefix.length()); if (illegalChar(ch)) { continue; } result.put(name.substring(namePrefixLength + 1), new String[]{variables.get(name)}); } else { result.put(name, new String[]{variables.get(name)}); } } }
Iterator<String> parameterNames = request.getParameterNames(); while (parameterNames.hasNext()) { String name = parameterNames.next(); if (name.startsWith(namePrefix)) { if (subPrefix) { char ch = name.charAt(namePrefix.length()); if (illegalChar(ch)) { continue; } result.put(name.substring(namePrefixLength + 1), request.getParameterValues(name)); } else { result.put(name, request.getParameterValues(name)); } } }
return result; }
private boolean illegalChar(char ch) { return ch != '.' && ch != '_' && !(ch >= '0' && ch <= '9'); }
protected final Map<String, String> getUriTemplateVariables(NativeWebRequest request) { Map<String, String> variables = (Map<String, String>) request.getAttribute( HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST); return (variables != null) ? variables : Collections.emptyMap(); }
protected Object putParameters(MethodParameter parameter,NativeWebRequest request) {
Object object = BeanUtils.instantiateClass(parameter.getParameterType());
Map<String, String[]> parameters = getPrefixParameterMap(parameter.getParameterName(),request,true); Iterator<String> iterator = parameters.keySet().iterator(); while(iterator.hasNext()) { String fieldName = iterator.next(); String[] parameterValue = parameters.get(fieldName); try { Field field = object.getClass().getDeclaredField(fieldName); field.setAccessible(true);
Class<?> fieldTargetType = field.getType();
if(List.class.isAssignableFrom(fieldTargetType)) { field.set(object, Arrays.asList(parameterValue)); }
else if(Object[].class.isAssignableFrom(fieldTargetType)) { field.set(object, parameterValue); }
else { field.set(object, parameterValue[0]); } } catch (Exception e) { logger.error("Set Field:{} Value Error,In {}",fieldName,object.getClass().getName()); continue; } } return object; } }
|
上面我直接贴出了参数装载的全部实现方法,下面我们就开始按照装载的流程进行讲解。
supportsParameter方法实现
1 2 3 4 5 6 7 8 9 10
|
@Override public boolean supportsParameter(MethodParameter methodParameter) { return methodParameter.hasParameterAnnotation(ParameterModel.class); }
|
我们只对配置了ParameterModel
注解的参数进行装载。
resolveArgument方法实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
|
@Override public Object resolveArgument ( MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory ) throws Exception { String parameterName = methodParameter.getParameterName(); logger.info("参数名称:{}",parameterName);
Object target = modelAndViewContainer.containsAttribute(parameterName) ? modelAndViewContainer.getModel().get(parameterName) : createAttribute(parameterName, methodParameter, webDataBinderFactory, nativeWebRequest);;
return target; }
|
该方法作为装载参数逻辑的入口,我们从MethodParameter
对象内获取了参数的名称,根据该名称检查Model内是否存在该名称的值,如果存在则直接使用并返回,反则需要从ParameterMap
内获取对应该参数名称的值返回。
我们下面主要看看从parameterMap
获取的方法实现
createAttribute方法实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
|
protected Object createAttribute(String attributeName, MethodParameter parameter, WebDataBinderFactory binderFactory, NativeWebRequest request) throws Exception {
String value = getRequestValueForAttribute(attributeName, request);
if (value != null) {
Object attribute = convertAttributeToParameterValue(value, attributeName, parameter, binderFactory, request);
if (attribute != null) { return attribute; } }
else { Object attribute = putParameters(parameter,request); if(attribute!=null) { return attribute; } }
return BeanUtils.instantiateClass(parameter.getParameterType()); }
|
该方法的逻辑存在两个分支,首先通过调用getRequestValueForAttribute
方法从parameterMap
内获取指定属性名的请求值,如果存在值则需要验证是否可以完成类型转换,验证通过后则直接返回值。
上面的部分其实是SpringMVC
原有的参数装载的流程,下面我们就来根据需求个性化定制装载逻辑。
putParameters方法实现
该方法实现了自定义规则xxx.xxx
方式进行参数装载的逻辑,我们在前台传递参数的时候只需要将Controller
内方法参数名称作为传递的前缀即可,如:teacher.name
、student.name
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
|
protected Object putParameters(MethodParameter parameter,NativeWebRequest request) {
Object object = BeanUtils.instantiateClass(parameter.getParameterType());
Map<String, String[]> parameters = getPrefixParameterMap(parameter.getParameterName(),request,true); Iterator<String> iterator = parameters.keySet().iterator(); while(iterator.hasNext()) { String fieldName = iterator.next(); String[] parameterValue = parameters.get(fieldName); try { Field field = object.getClass().getDeclaredField(fieldName); field.setAccessible(true);
Class<?> fieldTargetType = field.getType();
if(List.class.isAssignableFrom(fieldTargetType)) { field.set(object, Arrays.asList(parameterValue)); }
else if(Object[].class.isAssignableFrom(fieldTargetType)) { field.set(object, parameterValue); }
else { field.set(object, parameterValue[0]); } } catch (Exception e) { logger.error("Set Field:{} Value Error,In {}",fieldName,object.getClass().getName()); continue; } } return object; }
|
该方法首先实例化了一个MethodParameter
类型的空对象
,然后通过getPrefixParameterMap
获取PathVariables
、ParameterMap
内前缀为MethodParameter
名称的请求参数列表,遍历列表对应设置
object
内的字段,用于完成参数的装载,在装载过程中,我这里分别根据Collection
、List
、Array
、Single
类型进行了处理(注意:这里需要根据项目需求进行调整装载类型)。
配置Spring托管CustomerArgumentResolver
我们将CustomerArgumentResolver
托管交付给Spring
框架,我们来创建一个名叫WebMvcConfiguration
的配置类,该类继承抽象类WebMvcConfigurerAdapter
,代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
|
@Configuration public class WebMvcConfiguration extends WebMvcConfigurerAdapter {
@Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new CustomerArgumentResolver()); }
@Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/index").setViewName("index"); } }
|
我们重写了WebMvcConfigurerAdapter
抽象类内的两个方法addArgumentResolvers
、addViewControllers
,其中addArgumentResolvers
方法完成了参数装载的托管。
addViewControllers
配置了视图控制器映射,这样我们访问/index
地址就可以请求到index.jsp
页面。
创建测试控制器
创建名为IndexController
的控制器并添加数据提交的方法,具体代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
@RestController public class IndexController {
@RequestMapping(value = "/submit") public String resolver(@ParameterModel TeacherEntity teacher, @ParameterModel StudentEntity student) { return "教师名称:"+ JSON.toJSON(teacher.getName()) +",学生名称:"+student.getName()+",学生年龄:"+student.getAge(); } }
|
可以看到我们为TeacherEntity
、StudentEntity
分别添加了注解@ParameterModel
,也就证明了这两个实体需要使用我们的CustomerArgumentResolver
完成参数装载。
运行测试
在运行测试之前,我们需要修改下index.jsp
内的参数映射前缀,修改后代码如下所示:
1 2 3 4 5 6
| <form method="post" action="/submit"> 教师姓名:<input type="text" name="teacher.name"/><br/><br/> 学生姓名:<input type="text" name="student.name"/><br/><br/> 学生年龄:<input type="text" name="student.age"/><br/><br/> <input type="submit"/> </form>
|
测试单值装载
我们为教师名称、学生名称、学生年龄都分别添加了前缀,下面我们来启动项目,访问项目根下路径/index
,如下图1所示:
在上图1中输入了部分请求参数,点击“提交”按钮查看界面输出的效果,图下所示:
1
| 教师名称:王老师,学生名称:张小跑,学生年龄:23
|
可以看到参数已经被正确的装载到了不同的实体类内。
上面的例子只是针对实体内的单个值的装载,下面我们来测试下List
类型的值是否可以装载?
测试List装载
我们先来修改下教师实体内的名称为List,字段名称不需要变动,如下所示:
1 2
| private List<String> name;
|
再来修改下index.jsp
输入框,如下所示:
1 2 3 4 5 6 7
| <form method="post" action="/submit"> 语文老师姓名:<input type="text" name="teacher.name"/><br/><br/> 数学教师姓名:<input type="text" name="teacher.name"/><br/><br/> 学生姓名:<input type="text" name="student.name"/><br/><br/> 学生年龄:<input type="text" name="student.age"/><br/><br/> <input type="submit"/> </form>
|
在上代码中我们添加了两位老师的名称,接下来重启项目,再次提交测试,查看是不是我们想要的效果?
修改后的界面如下图2所示:
界面输出内容如下所示:
1
| 教师名称:["王老师","李老师"],学生名称:张小跑,学生年龄:24
|
可以看到我们已经拿到了两位老师的名称,这也证明了我们的CustomerArgumentResolver
是可以完成List
的映射装载的。
总结
以上内容就是本章的全部讲解内容,本章简单实现了参数的状态,其中还有很多细节性质的逻辑,如:@Valid
注解的生效、文件的上传等。在下一章我们会降到如果通过参数装载实现接口服务的安全认证。