@ModelAttribute修饰的方法会在执行每个目标方法之前被springMVC调用!
先看例子:
模拟修改操作,原始数据为用户id2,姓名zs,密码pass123,年龄12,默认密码不能被修改,表单回显,模拟操作直接在表单填写对应的属性值
<form action="springmvc/testmodelattribute" method="post">
<input type="hidden" name="id" value="2"/>
Name:<input type="text" name="name" value="zs"/><br>
age :<input type="text" name="age" value="12"/><br>
<input type="submit" value="submit"/>
</form>
目标方法:
@RequestMapping("/testmodelattribute")
public String textModelAttribute(User user) {
System.out.println("修改" + user);
return "success";
}
在目标方法的输出结果是
修改User [id=2, name=zs, pass=null, Age=15]
很明显我们没有给password赋值,那么这个值是为null的
接下来看一下加上@ModelAttribute注解修饰的方法,来模拟从数据库中查询了一个对象,然后在页面上修改age的值
@ModelAttribute
public String gectUserFromDB(@RequestParam(value = "id", required = false) Integer id, Map<String, User> map) {
if (id != null) {
User user = new User("zs", "pass123", 12);
System.out.println("从数据库中获得一个对象" + user);
map.put("user", user);
}
return "success";
}
再看一下输出结果
从数据库中获得一个对象User [id=2, name=zs, pass=pass123, Age=12]
修改User [id=2, name=zs, pass=pass123, Age=15]
看到了把表单的值赋给了模拟从数据库中查询的记录。
话说回来, 那么到底是什么原理, 翻查源码看一下;我们现在 @ModelAttribute修饰的方法内打一个断点。
看到停在了invokeHandlerMethod这个方法,进入这个方法看一下
public final Object invokeHandlerMethod(Method handlerMethod, Object handler,
NativeWebRequest webRequest, ExtendedModelMap implicitModel) throws Exception {
Method handlerMethodToInvoke = BridgeMethodResolver.findBridgedMethod(handlerMethod);
try {
boolean debug = logger.isDebugEnabled();
for (String attrName : this.methodResolver.getActualSessionAttributeNames()) {
Object attrValue = this.sessionAttributeStore.retrieveAttribute(webRequest, attrName);
if (attrValue != null) {
implicitModel.addAttribute(attrName, attrValue);
}
}
for (Method attributeMethod : this.methodResolver.getModelAttributeMethods()) {
Method attributeMethodToInvoke = BridgeMethodResolver.findBridgedMethod(attributeMethod);
Object[] args = resolveHandlerArguments(attributeMethodToInvoke, handler, webRequest, implicitModel);
if (debug) {
logger.debug("Invoking model attribute method: " + attributeMethodToInvoke);
}
String attrName = AnnotationUtils.findAnnotation(attributeMethod, ModelAttribute.class).value();
if (!"".equals(attrName) && implicitModel.containsAttribute(attrName)) {
continue;
}
ReflectionUtils.makeAccessible(attributeMethodToInvoke);
Object attrValue = attributeMethodToInvoke.invoke(handler, args);
if ("".equals(attrName)) {
Class<?> resolvedType = GenericTypeResolver.resolveReturnType(attributeMethodToInvoke, handler.getClass());
attrName = Conventions.getVariableNameForReturnType(attributeMethodToInvoke, resolvedType, attrValue);
}
if (!implicitModel.containsAttribute(attrName)) {
implicitModel.addAttribute(attrName, attrValue);
}
}
Object[] args = resolveHandlerArguments(handlerMethodToInvoke, handler, webRequest, implicitModel);
if (debug) {
logger.debug("Invoking request handler method: " + handlerMethodToInvoke);
}
ReflectionUtils.makeAccessible(handlerMethodToInvoke);
return handlerMethodToInvoke.invoke(handler, args);
}
catch (IllegalStateException ex) {
// Internal assertion failed (e.g. invalid signature):
// throw exception with full handler method context...
throw new HandlerMethodInvocationException(handlerMethodToInvoke, ex);
}
catch (InvocationTargetException ex) {
// User-defined @ModelAttribute/@InitBinder/@RequestMapping method threw an exception...
ReflectionUtils.rethrowException(ex.getTargetException());
return null;
}
}
24行,在执行@ModelAttribute修饰的方法之前,传入的两个参数一个是传入的id,一个是BindingAwareModelMap
其中一个为我们用@RequestParam接受的参数,为页面输入的ID属性,还有一个就是BindingAwareModelMap的map集合
15行,使用2行参数中的implicitModel进行关联,实际上在执行这个invokeHandlerMethod方法的时候就传入了一个map对象,这个map为ExtendedModelMap类型的implicitModel,这个map就是刚才的那个BindingAwareModelMap
public final Object invokeHandlerMethod(Method handlerMethod, Object handler,
NativeWebRequest webRequest, ExtendedModelMap implicitModel) throws Exception {
22行,调用@ModelAttribute修饰的方法,把仿从数据库查放到map总的user,放到了implicitModel中(args和implicitModel关联)。
执行完用@ModelAttribute修饰的方法后,那么紧接着就该去执行目标方法了。
我们在页面上修改一个age的值,紧接着在setAge方法打一个断点
看到在setAge的时候调用了刚才方法中的resolveHandlerArguments方法。然后把查出来的记录传入方法(代码太多用省略号表示)。
33行,去执行目标方法,这时候传入的map还是从仿数据库查询出来的记录
private Object[] resolveHandlerArguments(Method handlerMethod, Object handler,
NativeWebRequest webRequest, ExtendedModelMap implicitModel) throws Exception {
Class<?>[] paramTypes = handlerMethod.getParameterTypes();
Object[] args = new Object[paramTypes.length];
.....
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);
}
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) {
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;
}
30行,断点过后先来看一下执行结果
看到执行完这个代码后, 就已经是从修改age后提交表单过来的值,此时现在这个密码也是从数据库中获取的。那这个值是怎么赋的呢?
从这行看得到,是从binder中获取了一个target属性。先来看看这个binder是个什么东西。
25行,通过resolveModelAttribute方法获取的WebDataBinder对象。
private WebDataBinder resolveModelAttribute(String attrName, MethodParameter methodParam,
ExtendedModelMap implicitModel, NativeWebRequest webRequest, Object handler) throws Exception {
// Bind request parameter onto object...
String name = attrName;
if ("".equals(name)) {
name = Conventions.getVariableNameForParameter(methodParam);
}
Class<?> paramType = methodParam.getParameterType();
Object bindObject;
if (implicitModel.containsKey(name)) {
bindObject = implicitModel.get(name);
}
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 {
bindObject = BeanUtils.instantiateClass(paramType);
}
WebDataBinder binder = createBinder(webRequest, bindObject, name);
initBinder(handler, name, binder, webRequest);
return binder;
}
最后看到23行,创建了这个对象。
protected WebDataBinder createBinder(NativeWebRequest webRequest, Object target, String objectName)
throws Exception {
return new WebRequestDataBinder(target, objectName);
}
看到这个binder对象中其中的两个属性就是一个target和objectName。到底是怎么确定这个两个属性的,
回过头来看创建,target 对应bindObject ,objectName对应name。
4行,从方法参数中获取这个名字,发现是一个空的字符串,进入下面的方法,获取了我们的那个类名首字母小写。
这个name是上一个方法传入的。我们回到刚才的方法,在57行,看到可以有ModelAttribute的value属性得到。前提就是24行, 参数是注解的并且注解里面有一个ModelAttribute
此处可略过...其实就是在目标方法的形参上也添加一个ModelAttribute注解。可以看下
@RequestMapping("/testmodelattribute")
public String textModelAttribute(@ModelAttribute(value="user") User user) {
System.out.println("修改" + user);
return "success";
}
言归正传
11行,看implicitModel是否有这个属性值。由于实现在@ModelAttribute方法的map中put了user这个key对应的属性。
在执行完12行,后我们来看一下这个bindObject属性
那么implicitModel不存在,14行,会验证当前Handler是否使用了@SessionAttributs进行修饰。若使用了则尝试从session中获取获取attribute属性中获取,如果获取不到这个值为空,还会抛出一个异常。
21行,若Handler没有使用@ModelAttribute修饰,在@SessionAttributs中也没有找到对应的attr属性,那么就会通过反射来创建这个对象。
23行, 把获取到的这个name,和bindObject传入到方法中来创建这个webDataBinder对象。这个name其实就是刚才看到的那个objecctName属性,而这个bindObject就是那个target属性。执行完在回到刚才的resolveHandlerArguments方法来看一下现在的这个创建好的WebDataBinder对象。
看到objectName和target都已经已经有了值。
46行,doBind方法给WebDataBinder赋表单值。赋值后的WebDataBinder为
看到age已经修改为15。
48行,给args属性赋值,就是从WebDataBinder的target属性。这个args其实就是被请求赋过值的那个从数据库查出来的那个user对象
53行,把objectName,和target赋给了implicitModel。
@Override
public Map<String, Object> getModel() {
Map<String, Object> model = new LinkedHashMap<String, Object>(2);
// Mapping from name to target object.
model.put(getObjectName(), getTarget());
// Errors instance, even if no errors.
model.put(MODEL_KEY_PREFIX + getObjectName(), this);
return model;
}
总结:
- 调用@ModelAttribute注解修饰的方法,实际上把@ModelAttribute方法中map的数据放到了implicitModel中。
- 解析请求处理器的目标参数,实际上该目标参数来自于WebDataBinder对象的target属性
- 创建WebDataBinder对象
- 确定objectName属性:若传入attrName属性为空字符串,那么objectName就是类名的第一个字母小写(若目标方法的POJO属性使用了@ModelAttribute来修饰,啧attrName则为@ModelAttribute的value属性值)
- 确定target属性:先会在implicitModel中查找attrName对应的属性值。如果不存在,则验证当前Handler是否使用了@SessionAttributs进行修饰,如果使用了则会从session中回去attrName对应的属性值。若session中没有对应的属性值,则会抛出一个异常。若两者都不存在的话, 那么就会通过反射来创建一个POJO的对象。
- springMVC把表单的请求参数赋给了WebDataBinder的target对应的属性。
- 接着把WebDataBinder的attrName和target给到implicitModel;
- 到最后WebDataBinder的target作为参数传递给目标方法的如参。
到此整篇文章也算是到了头, 如果有什么不对的地方, 希望各位学习springMVC的各位帮忙指正。