目錄
一、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>