一、模型数据处理的方式
1、将控制器方法的返回值类型设置为ModelAndView:通过ModelAndView对象我们既可以设置视图,也可以设置模型数据,模型数据可以在视图(如jsp页面)中通过EL表达式或者jsp表达式获取:
@RequestMapping(value = "/testModelAndView")
public ModelAndView testModelAndView(ModelAndView mv) {
mv.addObject("modelKey", "modelValue");//模型数据
mv.setViewName("success");//设置视图名
return mv;
}
//ModelAndView对象可以通过SpringMVC注入也可以自己创建
@RequestMapping(value="/testModelAndView",method=RequestMethod.GET)
public ModelAndView testModelAndView(){
ModelAndView mv = new ModelAndView();
mv.addObject("modelKey", "modelValue");//模型数据
mv.setViewName("ok");//设置视图名
return mv;
}
在视图中获取模型数据:
${modelKey}
${requestScope.modelKey}
<%=request.getAttribute("modelKey") %>
Tip
:
1️⃣ModelAndView.addObject(k,v)方法的本质是request.setAttribute(k,v),所以在视图中可以在request域中获取到设置的k的值
2️⃣ModelAndView对象可以由SpringMVC注入,也可以自己创建
2、处理器方法入参中的Map、Model和ModelMap:处理器方法中的这三个入参是由SpringMVC注入的,也叫做隐含域,我们可以通过它们向模型中放数据。这三个其实是同一个对象,在底层都是BindingAwareModelMap类型的对象,在一个方法中用Map、Model和ModelMap保存的键值对会保存在同一块内存中,在存放数据的时候都是调用了request.setAttribute(k,v);也就是说隐含域操作的是Request对象
@RequestMapping(value = "/testMapModel")
public ModelAndView testMapModel(ModelAndView mv, Map<String, Object> m, Model model, ModelMap mm) {
m.put("k", "v1");
model.addAttribute("k", "v2");
String k = (String) m.get("k");
System.out.println(k);// 结果是v2
ModelMap modelMap = mv.getModelMap();
Object object = modelMap.get("k");
System.out.println(object);//null
System.out.println(modelMap == m);// false
System.out.println(model == m);// true
System.out.println(mm == m);// true
mv.setViewName("success");
return mv;
}
Tip
:通过打印的结果可以看出来Map、Model、ModelMap操作的是同一个对象,但是令人意外的是,SpringMVC为我们注入的这三个对象竟然不是ModelAndView对象的Model或ModelMap属性值,只不过它们都是操作的Request对象
3、@SessionAttributes:这个注解只能放在类上,不能放在方法上,它的value属性值表示将key值为value值指定的这些对象放在Session域中而不再是默认的Request域中(即ModelAndView以及SpringMVC为我们注入的Map、Model、ModelMap中如果放置指定key的对象时,这些对象将会被放在Session中),types属性的值是指定将某一类所有的对象都放在Session域中(key在放置的时候指定),然后根据key的值就可以在视图的Session域中获取到,一般采用value的方式
@Controller
@SessionAttributes(value={"user"})
public class Hello {
@RequestMapping(value="/testSession")
public String testSession(Map<String,Object> map){
map.put("user", "hello every body");
return "hello";
}
}
${sessionScope.user }
注意session域中的键值对在requestScope中也可以获取到,因为EL表达式中默认从小的范围开始查找,小的范围找不到就会去大的域中查找
示例:会将key值为user和password以及类型为Hello和String的键值对置于Session域中,前提是放置这些数据的map必须是SpringMVC注入的,若是我们自己创建的Map是不起作用的
@Controller
@SessionAttributes(value={"user","password"},types={Hello.class,String.class})
public class Hello {
@RequestMapping(value="/testSession")
public String testSession(Map<String,Object> map){
System.out.println("hello");
map.put("user", "hello every body");
map.put("hello",new Hello());
return "hello";
}
}
4、@ModelAttribute:模型属性,看下面的示例——模拟一个根据id修改User对象的操作
①使用@ModelAttribute之前的代码
表单:
<form action="${pageContext.request.contextPath }/testModelAttr" method="post">
<input type="hidden" name="_method" value="put"/>
<input type="hidden" name="id" value="11"/>
<input type="text" name="username" value="老王"/>
<input type="text" name="email" value="[email protected]"/>
<input type="submit" value="提交"/>
form>
控制器方法:
@RequestMapping(value="/testModelAttr",method=RequestMethod.PUT)
public String testModelAttr(User user){
System.out.println(user);
return "hello";
}
但仅仅这样的话是有问题的,会导致部分数据的丢失,比如说:如果User对象除了id、username、email这三个属性之外还有其他属性,比如age等,但我们在提交表单的时候只给User的id、username、email这三个属性赋值了,这是根据id将username、email的值修改之后是会将其他表单中没有出现的属性的值都置为null的。这就不是修改某个属性了,同时也将别的不相关的属性的值置空了,所以不能这么做。
②使用@ModelAttribute注解:所有控制器方法执行之前都会调用该注解标注的方法
表单:和之前没什么变化
<form action="${pageContext.request.contextPath }/testModelAttr" method="post">
<input type="hidden" name="_method" value="put"/>
<input type="hidden" name="id" value="11"/>
<input type="text" name="username" value="老王"/>
<input type="text" name="email" value="[email protected]"/>
<input type="submit" value="提交"/>
form>
控制器方法:和之前没什么变化
@RequestMapping(value="/testModelAttr",method=RequestMethod.PUT)
public String testModelAttr(User user){
System.out.println(user);
return "hello";
}
在Controller中多了一个使用@ModelAttribute注解的方法
@ModelAttribute
public void getUserById(@RequestParam(value="id") Integer id,Map<String,Object> map){
User user = new User(id,"小李","123456","[email protected]",23);
map.put("user", user);
System.out.println(user);
}
@ModelAttribute标注的方法的作用就是为了充实被修改的对象的其他属性,以免其他属性的值被置空。由@ModelAttribute注解的方法在每一个控制器方法执行之前都会被调用,因此若想某些控制器方法执行的时候不执行该方法的内部,可以在该方法中的逻辑代码执行之前加一个if条件
@ModelAttribute
public void getUserById(@RequestParam(value="id") Integer id,Map<String,Object> map){
if(id != null){
//模拟到数据库中查询:service.getUserById(id);
User user = new User(id,"小李","123456","[email protected]",23);
//这个key是调用正式方法的入参,这一步的作用就是为了充实user对象
map.put("user", user);
System.out.println(user);
}
}
加了if判断之后虽然其他没有id请求参数的控制器方法也会调用该@ModelAttribute注解的方法,但是由于不满足代码执行的条件,其内部的代码是不会执行的,相对就提升了效率。要注意的是Map中key的值必须是简单类名的首字母小写,比如:map对象是由SpringMVC注入的那个对象
@Controller
@SessionAttributes(types={Hello.class,String.class})
public class Hello {
@RequestMapping(value="/testModelAttr",method=RequestMethod.PUT)
public String testModelAttr(User user1){
System.out.println(user1);
return "hello";
}
@ModelAttribute
public void getUserById(@RequestParam(value="id") Integer id,Map<String,Object> map){
if(id != null){
//模拟到数据库中查询:service.getUserById(id);
User user12 = new User(id,"小李","123456","[email protected]",23);
map.put("user", user12);//注意key的值必须是简单类名的首字母小写
System.out.println(user12);
}
}
}
流程解析
:入参中的@RequestParam(value=“id”) Integer id:由@RequestParam标注的形参是从前台传送过来的
①先调用由@ModelAttribute注解的方法,从数据库中查询要修改的对象,这个对象中的所有属性的值和数据库是保持一致的
②前端传入的参数注入到由①查询出来的这个对象中,替换同名参数的值
③将前端参数注入之后的对象传给处理器的处理方法
@ModelAttribute入参的方式:使用入参的方式可以指定key为map中定义的key,就是说不必再强制map中的key一定要是简单类名的首字母小写了
@Controller
public class Hello {
@RequestMapping(value = "/testModelAttr", method = RequestMethod.PUT)
public String testModelAttr(@ModelAttribute(value = "user32") User user12) {
System.out.println(user12);
return "hello";
}
@ModelAttribute
public void getUserById(@RequestParam(value = "id") Integer id, Map<String, Object> map) {
if (id != null) {
// 模拟到数据库中查询:service.getUserById(id);
User user1 = new User(id, "小李", "123456", "[email protected]", 23);
map.put("user32", user1);
System.out.println(user1);
}
}
}
在开发中其实用到@ModelAttribute的时候并不多,有其他的方式可以杜绝修改不出现在请求参数中的属性值
二、模型数据处理源码分析
模型数据处理主要是调用了底层HandlerMethodInvoker.class类中的两个方法:解析控制器方法的形参的方法resolveHandlerArguments()和解析模型类的属性的方法resolveModelAttribute()
1、resolveHandlerArguments():解析控制器方法的形参
private Object[] resolveHandlerArguments(Method handlerMethod, Object handler,NativeWebRequest webRequest, ExtendedModelMap implicitModel) throws Exception {
Class<?>[] paramTypes = handlerMethod.getParameterTypes();//1、获取控制器类方法的参数类型的数组
Object[] args = new Object[paramTypes.length];//2、根据数组的长度创建一个Object数组
for (int i = 0; i < args.length; i++) {
//3、根据形参位置和控制器方法创建方法的形参
MethodParameter methodParam = new MethodParameter(handlerMethod, i);
//4、根据发现的形参名字初始化形参
methodParam.initParameterNameDiscovery(this.parameterNameDiscoverer);
GenericTypeResolver.resolveParameterType(methodParam, handler.getClass());
//4、以下定义是参数的类型
String paramName = null;
String headerName = null;
boolean requestBodyFound = false;
String cookieName = null;
String pathVarName = null;
String attrName = null;
boolean required = false;
String defaultValue = null;
boolean validate = false;
Object[] validationHints = null;
int annotationsFound = 0;
//5、获取形参的注解
Annotation[] paramAnns = methodParam.getParameterAnnotations();
//6、遍历注解参数
for (Annotation paramAnn : paramAnns) {
//7、根据注解类型解析
if (RequestParam.class.isInstance(paramAnn)) {
RequestParam requestParam = (RequestParam) paramAnn;
paramName = requestParam.value();
required = requestParam.required();
defaultValue = parseDefaultValueAttribute(requestParam.defaultValue());
annotationsFound++;
}
else if (RequestHeader.class.isInstance(paramAnn)) {
RequestHeader requestHeader = (RequestHeader) paramAnn;
headerName = requestHeader.value();
required = requestHeader.required();
defaultValue = parseDefaultValueAttribute(requestHeader.defaultValue());
annotationsFound++;
}
else if (RequestBody.class.isInstance(paramAnn)) {
requestBodyFound = true;
annotationsFound++;
}
else if (CookieValue.class.isInstance(paramAnn)) {
CookieValue cookieValue = (CookieValue) paramAnn;
cookieName = cookieValue.value();
required = cookieValue.required();
defaultValue = parseDefaultValueAttribute(cookieValue.defaultValue());
annotationsFound++;
}
else if (PathVariable.class.isInstance(paramAnn)) {
PathVariable pathVar = (PathVariable) paramAnn;
pathVarName = pathVar.value();
annotationsFound++;
}
//8、当注解类型是ModelAttribute时的操作
else if (ModelAttribute.class.isInstance(paramAnn)) {
ModelAttribute attr = (ModelAttribute) paramAnn;
//9、将该参数的名字赋值为注解中的value值
attrName = attr.value();
annotationsFound++;
}
else if (Value.class.isInstance(paramAnn)) {
defaultValue = ((Value) paramAnn).value();
}
else if (paramAnn.annotationType().getSimpleName().startsWith("Valid")) {
validate = true;
Object value = AnnotationUtils.getValue(paramAnn);
validationHints = (value instanceof Object[] ? (Object[]) value : new Object[] {value});
}
}
//10、如果有入参注解不唯一的操作,则抛出异常:同一个形参在多个注解中存在
if (annotationsFound > 1) {
throw new IllegalStateException("Handler parameter annotations are exclusive choices - " +
"do not specify more than one such annotation on the same parameter: " + handlerMethod);
}
//11、如果没有注解
if (annotationsFound == 0) {
Object argValue = resolveCommonArgument(methodParam, webRequest);
if (argValue != WebArgumentResolver.UNRESOLVED) {
args[i] = argValue;
}
else if (defaultValue != null) {
args[i] = resolveDefaultValue(defaultValue);
}
else {
Class<?> paramType = methodParam.getParameterType();
if (Model.class.isAssignableFrom(paramType) || Map.class.isAssignableFrom(paramType)) {
if (!paramType.isAssignableFrom(implicitModel.getClass())) {
throw new IllegalStateException("Argument [" + paramType.getSimpleName() + "] is of type " +
"Model or Map but is not assignable from the actual model. You may need to switch " +
"newer MVC infrastructure classes to use this argument.");
}
args[i] = implicitModel;
}
else if (SessionStatus.class.isAssignableFrom(paramType)) {
args[i] = this.sessionStatus;
}
else if (HttpEntity.class.isAssignableFrom(paramType)) {
args[i] = resolveHttpEntityRequest(methodParam, webRequest);
}
else if (Errors.class.isAssignableFrom(paramType)) {
throw new IllegalStateException("Errors/BindingResult argument declared " +
"without preceding model attribute. Check your handler method signature!");
}
else if (BeanUtils.isSimpleProperty(paramType)) {
paramName = "";
}
else {
//12、没有注解又不是上面的几种情况,就将属性名的值赋值为空串""
attrName = "";
}
}
}
//13、形参列表不为空时的操作
if (paramName != null) {
args[i] = resolveRequestParam(paramName, required, defaultValue, methodParam, webRequest, handler);
}
else if (headerName != null) {
args[i] = resolveRequestHeader(headerName, required, defaultValue, methodParam, webRequest, handler);
}
else if (requestBodyFound) {
args[i] = resolveRequestBody(methodParam, webRequest, handler);
}
else if (cookieName != null) {
args[i] = resolveCookieValue(cookieName, required, defaultValue, methodParam, webRequest, handler);
}
else if (pathVarName != null) {
args[i] = resolvePathVariable(pathVarName, methodParam, webRequest, handler);
}
//14、属性不为null时的操作
else if (attrName != null) {
WebDataBinder binder =
resolveModelAttribute(attrName, methodParam, implicitModel, webRequest, handler);
boolean assignBindingResult = (args.length > i + 1 && Errors.class.isAssignableFrom(paramTypes[i + 1]));
if (binder.getTarget() != null) {
//15、这行代码负责将web资源(请求参数)赋值给之前绑定的对象
doBind(binder, webRequest, validate, validationHints, !assignBindingResult);
}
args[i] = binder.getTarget();
if (assignBindingResult) {
args[i + 1] = binder.getBindingResult();
i++;
}
implicitModel.putAll(binder.getBindingResult().getModel());
}
}
return args;
}
2、resolveModelAttribute方法分析:解析模型类的属性,先在隐含域中找,再到session中找,若还是找不到的话则利用反射创建一个对象
private WebDataBinder resolveModelAttribute(String attrName,MethodParameter methodParam,ExtendedModelMap implicitModel, NativeWebRequest webRequest, Object handler) throws Exception {
// Bind request parameter onto object...将请求参数绑定到一个对象
String name = attrName;
//1、如果请求参数名为""时,请求参数名在resolveHandlerArguments方法中确定
if ("".equals(name)) {
//就赋值为参数的类型名首字母小写
name = Conventions.getVariableNameForParameter(methodParam);
}
Class<?> paramType = methodParam.getParameterType();
Object bindObject;
//2、如果请求参不空时,会先在隐含域(@ModelAttribute中放在map中的)中查找,找不到再到session中查找
if (implicitModel.containsKey(name)) {
bindObject = implicitModel.get(name);
}
//3、在session中查找,该name是注解@SessionAttribute中的value,
//如果找到了但是却并没有放入到session中与该value对应的对象的话就会抛出异常,有的话就绑定到对象
else if (this.methodResolver.isSessionAttribute(name, paramType)) {
bindObject = this.sessionAttributeStore.retrieveAttribute(webRequest, name);
if (bindObject == null) {
raiseSessionRequiredException("Session attribute '" + name + "' required - not found in session");
}
}
else {
//如果session中也没有的话就会创建一个Object对象兜底:代码的体现在下一层的方法中
bindObject = BeanUtils.instantiateClass(paramType);
}
WebDataBinder binder = createBinder(webRequest, bindObject, name);
initBinder(handler, name, binder, webRequest);
return binder;
}