天天看点

SpringMVC——(4)模型数据与视图一、ModelAndView二、@ModelAttribute三、视图四、处理静态资源

目录

一、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进入:

SpringMVC——(4)模型数据与视图一、ModelAndView二、@ModelAttribute三、视图四、处理静态资源

2、查看代码运行流程,找到DispatcherServlet.doDispatch( )方法

SpringMVC——(4)模型数据与视图一、ModelAndView二、@ModelAttribute三、视图四、处理静态资源

定位到实例化mv对象。

SpringMVC——(4)模型数据与视图一、ModelAndView二、@ModelAttribute三、视图四、处理静态资源

可以看到这里有个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);方法进行视图对象解析。

SpringMVC——(4)模型数据与视图一、ModelAndView二、@ModelAttribute三、视图四、处理静态资源

查看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。

SpringMVC——(4)模型数据与视图一、ModelAndView二、@ModelAttribute三、视图四、处理静态资源

查看得到的视图对象:得到的url就是配置的/WEB-INF/views/success.jsp

SpringMVC——(4)模型数据与视图一、ModelAndView二、@ModelAttribute三、视图四、处理静态资源

得到View对象后,重新返回到render(mv, request, response)方法。向下执行到view.render方法,此方法才是真正的进行视图view的解析处理。

SpringMVC——(4)模型数据与视图一、ModelAndView二、@ModelAttribute三、视图四、处理静态资源

5、执行真正的view.render方法进行视图解析:

SpringMVC——(4)模型数据与视图一、ModelAndView二、@ModelAttribute三、视图四、处理静态资源

方法中最后一步调用renderMergedOutputModel方法。进入查看:

直接进入InternalResourceView的renderMergedOutputModel方法,先获取RequestDispatcher对象,然后调用rd.forward方法进行转发。

SpringMVC——(4)模型数据与视图一、ModelAndView二、@ModelAttribute三、视图四、处理静态资源
SpringMVC——(4)模型数据与视图一、ModelAndView二、@ModelAttribute三、视图四、处理静态资源

总结:

1、视图的作用是渲染模型数据,将模型里的数据以某种形式呈现给用户。Spring在org.springframework.web.servlet包中定义了一个高度抽象的View接口。视图对象由视图解析器负责实例化。由于视图是无状态的,所以他们不会有线程安全的问题。

2、视图解析器:SpringMVC为逻辑视图名的解析提供了不同的策略,可以在Spring WEB上下文中配置一种或多种解析策略,并指定他们之间的先后顺序。每一种映射策略对应一个具体的视图解析器实现类。视图解析器的作用比较单一:将逻辑视图解析为一个具体的视图对象。所有的视图解析器都必须实现ViewResolver接口。

SpringMVC——(4)模型数据与视图一、ModelAndView二、@ModelAttribute三、视图四、处理静态资源

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>

继续阅读