天天看點

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>

繼續閱讀