目录
一、ModelAndView
1、使用ModelAndView
2、使用Map、Model、ModelMap
3、使用@SessionAttributes
二、@ModelAttribute
1、用法1:为POJO做增量更新
2、用法二:注释方法
三、视图
1、视图解析流程
2、自定义视图
3、mvc:view-controller标签
4、重定向
一、ModelAndView
1、使用ModelAndView
目标方法的返回值可以是ModelAndView类型。其中可以包含视图和模型信息。
@RequestMapping("/testModelAndView")
public ModelAndView testModelAndView() {
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName(SUCCESS);
modelAndView.addObject("time", new Date());
return modelAndView;
}
SpringMVC会把ModelAndView的model中数据放入到request域对象中,页面中可以从request域对象取出数据:
页面代码:time: ${requestScope.time }
2、使用Map、Model、ModelMap
SpringMVC在内部使用了一个org.springframework.ui.Model接口存储模型数据。
SpringMVC在调用方法前会创建一个隐含的模型对象作为模型数据的存储容器,如果方法的入参为Map或Model类型,SpringMVC会将隐含模型的引用传递给这些入参。在方法体内,开发者可以通过这个入参对象访问到模型中的所有数据,也可以向模型中添加新的属性数据。
@RequestMapping("/testMap")
public String testMap(Map<String, Object> map) {
map.put("map", Arrays.asList("Tom", "Jerry", "Mike"));
map.put("className", map.getClass().getName());
return SUCCESS;
}
@RequestMapping("/testModel")
public String testModel(Model model) {
model.addAttribute("className", model.getClass().getName());
model.addAttribute("model", Arrays.asList("Tom", "Jerry", "Mike"));
return SUCCESS;
}
@RequestMapping("/testModelMap")
public String testModelMap(ModelMap modelMap) {
modelMap.put("className", modelMap.getClass().getName());
modelMap.addAttribute("modelMap", Arrays.asList("Tom", "Jerry", "Mike"));
return SUCCESS;
}
存放在入参的Map、Model、ModelMap中的数据,都是放在request域对象中的,页面中同样可以使用requestScope取出数据。
3、使用@SessionAttributes
@SessionAttributes在多个请求之间共用某个模型属性数据,将在模型中对应的属性暂存到HttpSession中。
@SessionAttributes除了可以通过属性名指定需要放到会话中的属性外(实际上使用的是value属性值),还可以通过模型属性的对象类型指定哪些 模型属性需要放到会话中(实际上使用的是types属性值)。该注解只能放在类的上面。而不能修饰放方法。例如:
- @SessionAttributes(types=User.class)会将隐含模型中所有类型为User.class的属性添加到会话中。
- @SessionAttributes(value={“user1”,“user2”})
- @SessionAttributes(types={User.class,Dept.class})
- @SessionAttributes(value={“user1”, “user2”}, types={Dept.class})
@SessionAttributes(value = { "user" }, types = { String.class })
@Controller
@RequestMapping("/SpringMVC")
public class TestRequestMapping extends BaseController {
@RequestMapping("/testSessionAttributes")
public String testSessionAttributes(Map<String, Object> map) {
User user = new User();
user.setUsername("Tom");
user.setPassword("121345");
map.put("user", user);
map.put("school", "atguigu");
return SUCCESS;
}
}
二、@ModelAttribute
主要使用场景:@ModelAttribute用于SpringMVC在向目标方法传POJO参数时,使用已有的POJO做“增量”更新。
例如一个商品对象有三个属性:ID标识、成本价、销售价,而从模型表单传入的只有ID和销售价对象,此时如果使用该对象进行更新会把数据库中的成本价给覆盖掉,这当然不是我们想要的效果。那么SpringMVC提供的@ModelAttribute注解,让被标识的方法在所有方法执行之前被调用,因此在调用目标方法之前,可以根据ID获取到旧的成本价和销售价,并实例化一个POJO放到map中,在传入目标方法前,SpringMVC会将表单传入的ID和销售价在此POJO基础上更新,然后在更新到数据库,这样就能保证最终写入数据库中的数据是我们想要的。
1、用法1:为POJO做增量更新
页面:表单提交User 对象的数据,但是密码没有被提交,对应的后台接收的对象中的密码不能被修改。
<form action="springmvc/testModelAttribute" method="Post">
<input type="hidden" name="id" value="1"/>
username: <input type="text" name="username" value="Tom"/>
<br>
email: <input type="text" name="email" value="[email protected]"/>
<br>
age: <input type="text" name="age" value="12"/>
<br>
<input type="submit" value="Submit"/>
</form>
后台:请求“springmvc/testModelAttribute”进来的时候:
首先执行getUser方法,并且接收表单传回来的id参数的值。里面进行数据库查询user对象的信息,然后保存在Map中,key为user。
然后SpringMVC会从Map中取出User对象,并把表单的请求参数进行对应的赋值user对象,没有传回来的参数例如密码则保持原值。
最后执行请求的方法,然后将Map中的对象传给请求方法的入参对象。
@RequestMapping("/springmvc")
@Controller
public class SpringMVCTest {
@ModelAttribute
public void getUser(@RequestParam(value="id",required=false) Integer id,
Map<String, Object> map){
System.out.println("modelAttribute method");
if(id != null){
//模拟从数据库中获取对象
User user = new User(1, "Tom", "123456", "[email protected]", 12);
System.out.println("从数据库中获取一个对象: " + user);
map.put("user", user);
}
}
@RequestMapping("/testModelAttribute")
public String testModelAttribute(User user){
System.out.println("修改: " + user);
return SUCCESS;
}
}
分析:
1. 调用 @ModelAttribute 注解修饰的方法. 实际上把 @ModelAttribute 方法中 Map 中的数据放在了 implicitModel 中.
2. 解析请求处理器的目标参数, 实际上该目标参数来自于 WebDataBinder 对象的 target 属性
1). 创建 WebDataBinder 对象:
①. 确定 objectName 属性: 若传入的 attrName 属性值为 "", 则 objectName 为类名第一个字母小写. 注意: attrName. 若目标方法的 POJO 属性使用了 @ModelAttribute 来修饰, 则 attrName 值即为 @ModelAttribute 的 value 属性值
②. 确定 target 属性:
> 在 implicitModel 中查找 attrName 对应的属性值. 若存在, ok
> *若不存在: 则验证当前 Handler 是否使用了 @SessionAttributes 进行修饰, 若使用了, 则尝试从 Session 中获取 attrName 所对应的属性值. 若 session 中没有对应的属性值, 则抛出了异常.
> 若 Handler 没有使用 @SessionAttributes 进行修饰, 或 @SessionAttributes 中没有使用 value 值指定的 key 和 attrName 相匹配, 则通过反射创建了 POJO 对象。
2). SpringMVC 把表单的请求参数赋给了 WebDataBinder 的 target 对应的属性.
3). *SpringMVC 会把 WebDataBinder 的 attrName 和 target 给到 implicitModel. 近而传到 request 域对象中.
4). 把 WebDataBinder 的 target 作为参数传递给目标方法的入参.
SpringMVC 确定目标方法 POJO 类型入参的过程:
1. 确定一个 key:
1). 若目标方法的 POJO 类型的参数木有使用 @ModelAttribute 作为修饰, 则 key 为 POJO 类名第一个字母的小写
2). 若使用了 @ModelAttribute 来修饰, 则 key 为 @ModelAttribute 注解的 value 属性值.
2. 在 implicitModel 中查找 key 对应的对象, 若存在, 则作为入参传入
1). 若在 @ModelAttribute 标记的方法中在 Map 中保存过, 且 key 和 1 确定的 key 一致, 则会获取到.
3. 若 implicitModel 中不存在 key 对应的对象, 则检查当前的 Handler 是否使用 @SessionAttributes 注解修饰, 若使用了该注解, 且 @SessionAttributes 注解的 value 属性值中包含了 key, 则会从 HttpSession 中来获取 key 所对应的 value 值, 若存在则直接传入到目标方法的入参中. 若不存在则将抛出异常.
4. 若 Handler 没有标识 @SessionAttributes 注解或 @SessionAttributes 注解的 value 值中不包含 key, 则会通过反射来创建 POJO 类型的参数, 传入为目标方法的参数
5. SpringMVC 会把 key 和 POJO 类型的对象保存到 implicitModel 中, 进而会保存到 request 中.
2、用法二:注释方法
1、@ModelAttribute注释void返回值的方法
在获得请求/helloWorld后,populateModel方法在helloWorld方法之前先被调用,它把请求参数(/helloWorld?abc=text)加入到一个名为attributeName的model属性中,在它执行后helloWorld被调用,返回视图名helloWorld和model已由@ModelAttribute方法生产好了。
这个例子中model属性名称和model属性对象有model.addAttribute()实现,不过前提是要在方法中加入一个Model类型的参数。当URL或者post中不包含次参数时,会报错,其实不需要这个方法。
@Controller
public class HelloWorldController {
@ModelAttribute
public void populateModel(@RequestParam String abc, Model model) {
model.addAttribute("attributeName", abc);
}
@RequestMapping(value = "/helloWorld")
public String helloWorld() {
return "helloWorld";
}
}
2、注释返回具体类的方法
这种情况,model属性的名称没有指定,它由返回类型隐含表示,如这个方法返回Account类型,那么这个model属性的名称是account。
这个例子中model属性名称有返回对象类型隐含表示,model属性对象就是方法的返回值。它无须要特定的参数。
@ModelAttribute
public Account addAccount(@RequestParam String number) {
return accountManager.findAccount(number);
}
3、@ModelAttribute(value="")注释返回具体类的方法
这个例子中使用@ModelAttribute注释的value属性,来指定model属性的名称。model属性对象就是方法的返回值。它无须要特定的参数。
@Controller
public class HelloWorldController {
@ModelAttribute("attributeName")
public String addAccount(@RequestParam String abc) {
return abc;
}
@RequestMapping(value = "/helloWorld")
public String helloWorld() {
return "helloWorld";
}
}
4、@ModelAttribute和@RequestMapping同时注释一个方法
这时这个方法的返回值并不是表示一个视图名称,而是model属性的值,视图名称由RequestToViewNameTranslator根据请求"/helloWorld.do"转换为逻辑视图helloWorld。Model属性名称有@ModelAttribute(value=””)指定,相当于在request中封装了key=attributeName,value=hi。
@Controller
public class HelloWorldController {
@RequestMapping(value = "/helloWorld.do")
@ModelAttribute("attributeName")
public String helloWorld() {
return "hi";
}
}
三、视图
1、视图解析流程
1、在请求方法的return处加入断点,debugger进入:

2、查看代码运行流程,找到DispatcherServlet.doDispatch( )方法
定位到实例化mv对象。
可以看到这里有个mv对象,实际上就是ModelAndView,通过调试我们发现这里的mv中包括了model和view,view的指向就是success,而model此时因为没有进行初始化,所以为空。
3、执行processDispatchResult方法
可以看到2中实例化mv对象后,向后执行到processDispatchResult()。查看该方法的源代码:
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
HandlerExecutionChain mappedHandler, ModelAndView mv, Exception exception) throws Exception {
boolean errorView = false;
if (exception != null) {
if (exception instanceof ModelAndViewDefiningException) {
logger.debug("ModelAndViewDefiningException encountered", exception);
mv = ((ModelAndViewDefiningException) exception).getModelAndView();
}
else {
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
mv = processHandlerException(request, response, handler, exception);
errorView = (mv != null);
}
}
// Did the handler return a view to render?
if (mv != null && !mv.wasCleared()) {
render(mv, request, response);
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Null ModelAndView returned to DispatcherServlet with name '" + getServletName() +
"': assuming HandlerAdapter completed request handling");
}
}
if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
// Concurrent handling started during a forward
return;
}
if (mappedHandler != null) {
mappedHandler.triggerAfterCompletion(request, response, null);
}
}
可以看到在processDispatchResult方法中,首先进行exception异常的判断,如果有异常则进行异常的处理。没有则执行render(mv, request, response);方法。
4、执行render();
render(mv, request, response)方法中。
首先是调用view = resolveViewName(mv.getViewName(), mv.getModelInternal(), locale, request);方法进行视图对象解析。
查看resolveViewName方法:
protected View resolveViewName(String viewName, Map<String, Object> model, Locale locale,
HttpServletRequest request) throws Exception {
for (ViewResolver viewResolver : this.viewResolvers) {
View view = viewResolver.resolveViewName(viewName, locale);
if (view != null) {
return view;
}
}
return null;
}
debugger 进入,看到viewResolver中的视图解析器是在xml文件中配置的org.springframework.web.servlet.view.InternalResourceViewResolver。
查看得到的视图对象:得到的url就是配置的/WEB-INF/views/success.jsp
得到View对象后,重新返回到render(mv, request, response)方法。向下执行到view.render方法,此方法才是真正的进行视图view的解析处理。
5、执行真正的view.render方法进行视图解析:
方法中最后一步调用renderMergedOutputModel方法。进入查看:
直接进入InternalResourceView的renderMergedOutputModel方法,先获取RequestDispatcher对象,然后调用rd.forward方法进行转发。
总结:
1、视图的作用是渲染模型数据,将模型里的数据以某种形式呈现给用户。Spring在org.springframework.web.servlet包中定义了一个高度抽象的View接口。视图对象由视图解析器负责实例化。由于视图是无状态的,所以他们不会有线程安全的问题。
2、视图解析器:SpringMVC为逻辑视图名的解析提供了不同的策略,可以在Spring WEB上下文中配置一种或多种解析策略,并指定他们之间的先后顺序。每一种映射策略对应一个具体的视图解析器实现类。视图解析器的作用比较单一:将逻辑视图解析为一个具体的视图对象。所有的视图解析器都必须实现ViewResolver接口。
2、自定义视图
1、配置视图 BeanNameViewResolver 解析器
<!-- 配置视图 BeanNameViewResolver 解析器: 使用视图的名字来解析视图 -->
<!-- 通过 order 属性来定义视图解析器的优先级, order 值越小优先级越高,默认为Integer最大值 -->
<bean class="org.springframework.web.servlet.view.BeanNameViewResolver">
<property name="order" value="100"></property>
</bean>
查看下源代码:
public class BeanNameViewResolver extends WebApplicationObjectSupport implements ViewResolver, Ordered {
private int order = Integer.MAX_VALUE; // default: same as non-Ordered
public void setOrder(int order) {
this.order = order;
}
@Override
public int getOrder() {
return order;
}
@Override
public View resolveViewName(String viewName, Locale locale) throws BeansException {
ApplicationContext context = getApplicationContext();
if (!context.containsBean(viewName)) {
// Allow for ViewResolver chaining.
return null;
}
return context.getBean(viewName, View.class);
}
}
代码中默认的Order为最大值,此时配置为比常用的InternalResourceViewResolver优先级高。代码context.getBean(viewName, View.class);表示根据IOC容器的bean来解析视图。
2、创建自定义视图
实现View接口,重写getContentType()和render()方法。并且因为BeanNameViewResolver是根据BeanName获取视图,所以要加@Component注解
@Component
public class HelloView implements View{
@Override
public String getContentType() {
return "text/html";
}
@Override
public void render(Map<String, ?> model, HttpServletRequest request,
HttpServletResponse response) throws Exception {
response.getWriter().print("hello view, time: " + new Date());
}
}
3、mvc:view-controller标签
WEB-INF目录下面的JSP页面,是不能直接使用URL访问到。需要通过转发的方式,而一般都是在控制器中做转发映射,对应一些不需要其他操作的JSP页面,我们可以使用<mvc:view-controller path=""/>来配置,这样就可以不用再控制器中再去做转发映射。
配置mvc:view-controller:
<!-- 配置直接转发的页面 -->
<!-- 可以直接相应转发的页面, 而无需再经过 Handler 的方法. -->
<mvc:view-controller path="/success" view-name="success" />
<!-- 在实际开发中通常都需配置 mvc:annotation-driven 标签 -->
<mvc:annotation-driven></mvc:annotation-driven>
两种用法
1)、重定向
<mvc:view-controller path="/" view-name="redirect:/admin/index"/>
即如果当前路径是/ 则重定向到/admin/index
2)、view name
<mvc:view-controller path="/" view-name=admin/index"/>
如果当前路径是/ 则交给相应的视图解析器直接解析为视图
4、重定向
一般情况下,控制器方法返回字符串类型的值会被当成逻辑视图名处理。如果返回的字符串中带forward: 或 redirect: 前缀时,SpringMVC 会对他们进行特殊处理:将 forward: 和 redirect: 当成指示符,其后的字符串作为 URL 来处理 。
- – redirect:success.jsp:会完成一个到 success.jsp 的重定向的操作
- – forward:success.jsp:会完成一个到 success.jsp 的转发操作
源码解析:SpringMVC在进行创建视图View对象时候回调用UrlBasedViewResolver的createView方法,查看源码,发现viewName需要进行前缀判断:forward: 或 redirect:。redirect: 则返回RedirectView对象,forward:则是InternalResourceView对象。
@Override
protected View createView(String viewName, Locale locale) throws Exception {
// If this resolver is not supposed to handle the given view,
// return null to pass on to the next resolver in the chain.
if (!canHandle(viewName, locale)) {
return null;
}
// Check for special "redirect:" prefix.
if (viewName.startsWith(REDIRECT_URL_PREFIX)) {
String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length());
RedirectView view = new RedirectView(redirectUrl, isRedirectContextRelative(), isRedirectHttp10Compatible());
return applyLifecycleMethods(viewName, view);
}
// Check for special "forward:" prefix.
if (viewName.startsWith(FORWARD_URL_PREFIX)) {
String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length());
return new InternalResourceView(forwardUrl);
}
// Else fall back to superclass implementation: calling loadView.
return super.createView(viewName, locale);
}
四、处理静态资源
若将DispatcheServlet请求映射设置为/,则SpringMvc将捕获WEB容器的所有请求,包括静态资源的请求,SpringMvc会将它们当成一个普通的请求处理,那么将会出现因找不到对应的处理器将导致错误。可在SpringMvc的配置中配置<mvc:default-servlet-handler/>的方式解决静态资源的问题
<mvc:default-servlet-handler/>将在SpringMvc上下文中定义一个DefaultServletHttpRequestHandler,它会对进入DispatcheServlet的请求进行筛选,如果发现没有经过映射的请求,就将该请求交由WEB应用服务器默认的Servlet处理,如果不是静态资源的请求,才由DispatcheServlet处理。
一般WEB应用服务器默认的Servlet名称都是default,若使用的WEB应用服务器的默认Servlet不是default,则需要通过default-servlet-name属性显式指定。
配置<mvc:default-servlet-handler/>,同时需要配置<mvc:annotation-driven></mvc:annotation-driven>