[Re] SpringMVC-3
阿新 • • 發佈:2020-09-15
檢視解析
使用
@Controller public class HelloController { @RequestMapping("/hello") public String hello() { // → Go to WebContent/hello.jsp // 相對路徑的寫法 return "../../hello"; } /* * → Go to WebContent/hello.jsp * forward: 轉發到一個頁面(有字首的返回值獨立解析,不由檢視解析器拼串) * /hello.jsp 當前專案下的hello.jsp(加'/', 不然就是相對路徑,容易出錯) */ @RequestMapping("/handle01") public String handle01() { System.out.println("handle01"); return "forward:/hello.jsp"; } // 多次派發 @RequestMapping("/handle02") public String handle02() { System.out.println("handle02"); return "forward:/handle01"; // 2 次轉發 } /* * 重定向字首:redirect * 同重定向一樣,檢視解析器不會為其拼串 * /hello.jsp 代表的就是從當前專案下開始,SpringMVC 會為路徑自動拼接上專案名 */ @RequestMapping("/handle03") public String handle03() { System.out.println("handle03"); return "redirect:/hello.jsp"; } @RequestMapping("/handle04") public String handle04() { System.out.println("handle04"); return "redirect:/handle03"; // 2 次重定向 } }
原理
SpringMVC檢視解析:
- 方法執行後的返回值會作為頁面地址參考,轉發或者重定向到頁面
- 檢視解析器可能會進行頁面地址的拼串
- 任何方法的返回值,最終都會被包裝成 ModelAndView 物件。
- 來到頁面的方法:
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException)
- 檢視渲染流程:將域中的資料在頁面中展示,頁面就是用來渲染模型資料的。
[1012] render(mv, request, response); [1204] view = resolveViewName(mv.getViewName(), mv.getModelInternal(), locale, request);
- View 和 ViewResolver
- 根據檢視名得到檢視物件:
protected View resolveViewName(String viewName, Map<String, Object> model , Locale locale, HttpServletRequest request) throws Exception { // 遍歷所有的 ViewResolver(檢視解析器) for (ViewResolver viewResolver : this.viewResolvers) { // 檢視解析器根據目標方法的返回值得到一個 View 物件 View view = viewResolver.resolveViewName(viewName, locale); if (view != null) { return view; } } return null; }
@Override public View resolveViewName(String viewName, Locale locale) throws Exception { if (!isCache()) { return createView(viewName, locale); } else { Object cacheKey = getCacheKey(viewName, locale); View view = this.viewAccessCache.get(cacheKey); if (view == null) { synchronized (this.viewCreationCache) { view = this.viewCreationCache.get(cacheKey); if (view == null) { // Ask the subclass to create the View object. // 建立 View 物件! view = createView(viewName, locale); if (view == null && this.cacheUnresolved) { view = UNRESOLVED_VIEW; } if (view != null) { this.viewAccessCache.put(cacheKey, view); this.viewCreationCache.put(cacheKey, view); if (logger.isTraceEnabled()) { logger.trace("Cached view [" + cacheKey + "]"); } } } } } return (view != UNRESOLVED_VIEW ? view : null); } }
@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. // 如果沒有字首就使用父類預設建立一個 View return super.createView(viewName, locale); }
- 檢視解析器得到 View 物件的流程:所有配置的檢視解析器都來根據檢視名(返回值)得到 View 物件。如果能得到,就返回;得不到就換下一個檢視解析器。
- 呼叫 View 物件的 render 方法
@Override public void render(Map<String, ?> model, HttpServletRequest request , HttpServletResponse response) throws Exception { if (logger.isTraceEnabled()) { logger.trace("Rendering view with name '" + this.beanName + "' with model " + model + " and static attributes " + this.staticAttributes); } Map<String, Object> mergedModel = createMergedOutputModel(model, request, response); prepareResponse(request, response); // 渲染要給頁面輸出的所有資料 renderMergedOutputModel(mergedModel, request, response); }
- InternalResourceView 的 renderMergedOutputModel 方法
@Override protected void renderMergedOutputModel(Map<String, Object> model , HttpServletRequest request, HttpServletResponse response) throws Exception { // Determine which request handle to expose to the RequestDispatcher. HttpServletRequest requestToExpose = getRequestToExpose(request); // Expose the model object as request attributes !!! exposeModelAsRequestAttributes(model, requestToExpose); // Expose helpers as request attributes, if any. exposeHelpers(requestToExpose); // Determine the path for the request dispatcher. String dispatcherPath = prepareForRendering(requestToExpose, response); // Obtain a RequestDispatcher for the target resource (typically a JSP). RequestDispatcher rd = getRequestDispatcher(requestToExpose, dispatcherPath); if (rd == null) { throw new ServletException("Could not get RequestDispatcher for [" + getUrl() + "]: Check that the corresponding file exists within your web application archive!"); } // If already included or response already committed, perform include, else forward. if (useInclude(requestToExpose, response)) { response.setContentType(getContentType()); if (logger.isDebugEnabled()) { logger.debug("Including resource [" + getUrl() + "] in InternalResourceView '" + getBeanName() + "'"); } rd.include(requestToExpose, response); } else { // Note: The forwarded resource is supposed to determine the content type itself. if (logger.isDebugEnabled()) { logger.debug("Forwarding to resource [" + getUrl() + "] in InternalResourceView '" + getBeanName() + "'"); } // 請求轉發 rd.forward(requestToExpose, response); } }
- 為什麼隱含模型中的資料能在 request域 中取出?
protected void exposeModelAsRequestAttributes(Map<String, Object> model , HttpServletRequest request) throws Exception { for (Map.Entry<String, Object> entry : model.entrySet()) { String modelName = entry.getKey(); Object modelValue = entry.getValue(); if (modelValue != null) { request.setAttribute(modelName, modelValue); if (logger.isDebugEnabled()) { logger.debug("Added model object '" + modelName + "' of type [" + modelValue.getClass().getName() +"] to request in view with name '" + getBeanName() + "'"); } } else { request.removeAttribute(modelName); if (logger.isDebugEnabled()) { logger.debug("Removed model object '" + modelName + "' from request in view with name '" + getBeanName() + "'"); } } } }
檢視解析器只是為了得到檢視物件;檢視物件才能真正的渲染檢視(轉發 [將隱含模型中的資料放入請求域] 或者重定向到頁面)。
檢視和檢視解析器
- 請求處理方法執行完成後,最終返回一個 ModelAndView 物件。對於那些返回 String,View 或 ModeMap 等型別的處理方法,Spring MVC 也會在內部將它們裝配成一個 ModelAndView 物件,它包含了邏輯名和模型物件的檢視。
- Spring MVC 藉助檢視解析器(ViewResolver)得到最終的檢視物件(View),最終的檢視可以是 JSP ,也可能是 Excel、JFreeChart 等各種表現形式的檢視。
- 對於最終究竟採取何種檢視物件對模型資料進行渲染,處理器並不關心,處理器工作重點聚焦在生產模型資料的工作上,從而實現 MVC 的充分解耦。
檢視
- 檢視的作用是渲染模型資料,將模型裡的資料以某種形式呈現給客戶。
- 為了實現檢視模型和具體實現技術的解耦,Spring 在 org.springframework.web.servlet 包中定義了一個高度抽象的 View 介面:
- 檢視物件由檢視解析器負責例項化。由於檢視是無狀態的,所以他們不會有執行緒安全的問題。
- 常用的檢視實現類
檢視解析器
- SpringMVC 為邏輯檢視名的解析提供了不同的策略,可以在 Spring WEB 上下文中配置一種或多種解析策略,並指定他們之間的先後順序。每一種對映策略對應一個具體的檢視解析器實現類。
- 檢視解析器的作用比較單一:將邏輯檢視解析為一個具體的檢視物件。
- 所有的檢視解析器都必須實現 ViewResolver 介面
- 常用的檢視解析器實現類
- 程式設計師可以選擇一種檢視解析器或混用多種檢視解析器。
- 每個檢視解析器都實現了 Ordered 介面並開放出一個 order 屬性,可以通過 order 屬性指定解析器的優先順序,order 越小優先順序越高。
- SpringMVC 會按檢視解析器順序的優先順序對邏輯檢視名進行解析,直到解析成功並返回檢視物件,否則將丟擲 ServletException 異常。
JstlView
- 若專案中使用了 JSTL,則 SpringMVC 會自動把檢視由 InternalResourceView 轉為 JstlView
- 若使用 JSTL 的 fmt 標籤則需要在 SpringMVC 的配置檔案中配置國際化資原始檔
<!--讓 SpringMVC 管理國際化資原始檔;配置一個資原始檔管理器 --> <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource"> <!-- basename 指定基礎名--> <property name="basename" value="i18n"></property> </bean>
- 直接去頁面使用
<fmt:message>
<h1><fmt:message key="welcomeinfo"/></h1> <form action="#"> <fmt:message key="username"/>:<input /><br/> <fmt:message key="password"/>:<input /><br/> <input type="submit" value='<fmt:message key="loginBtn"/>'/> </form>
- 請求一定要過 SpringMVC 的檢視解析流程,人家會建立一個 JstlView 幫你快速國際化(也不能寫
forward:
)if (viewName.startsWith(FORWARD_URL_PREFIX)) { String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length()); return new InternalResourceView(forwardUrl); }
- 若希望直接響應通過 SpringMVC 渲染的頁面,可以使用
<mvc:view-controller>
標籤實現<!-- 傳送一個請求("toLoginPage") 直接來到 WEB-INF 下的 login.jsp path 指定哪個請求 view-name 指定對映給哪個檢視 ······························· 走了 SpringMVC 的整個流程:檢視解析 ... 提供國際化 ... ······························· 副作用:其他請求就不好使了 → [解決方案] 開啟 MVC 註解驅動模式 <mvc:annotation-driven /> --> <mvc:view-controller path="/toLoginPage" view-name="login">
擴充套件
- Tips
- 檢視解析器根據方法的返回值得到檢視物件
- 多個檢視解析器都會嘗試能否得到檢視物件
- 檢視物件不同就可以具有不同功能
- 自定義檢視和檢視解析器
- 編寫自定義檢視解析器和檢視實現類
- 檢視解析器必須放在 IOC 容器中,讓其工作,能創建出我們的自定義檢視物件
- 檢視物件自定義渲染邏輯
Restful-CRUD
思路
- CRUD 的 URL 地址:/資源名/資源標識
- /emp/1 GET:查詢id為1的員工
- /emp/1 PUT:更新id為1的員工
- /emp/1 DELETE:刪除id為1的員工
- /emp POST:新增員工
- /emp GET:員工列表
- 功能
- 員工列表
-> 訪問 index.jsp,直接傳送 /emp[GET] -> 控制器收到請求,查詢所有員工,放入 request域 -> 轉發帶到 list.jsp 做展示
- 員工新增
-> 在 list.jsp 點選 ADD 傳送 /toAddPage 請求 -> 控制器查出所有部門資訊(部門下拉框表單項),存放到 request域 -> 轉發到 add.jsp 顯示錶單項 -> 輸入資訊後,表單提交到 /emp[POST] -> 控制器收到請求,儲存新新增員工資訊 -> 重定向到 list.jsp
- 員工修改
-> list.jsp 為每條記錄追加一個超連結EDIT,傳送 /toEditPage -> 處理器查出所有部門資訊和要修改員工的原資訊,存放到請求域 -> 轉發帶到修改頁面 edit.jsp 做回顯 -> 輸入員工資料(不可修改name,別用隱藏域帶,用@ModelAttribute提前查出來) -> 點選提交,處理器收到請求,儲存員工 -> 完畢後,重定向到員工列表頁面做展示
- 員工刪除
-> 在 list.jsp 新增一個表單,實現 DELETE 方式提交 -> 為 每條記錄後的 DELETE 超連結繫結點選事件 -> 將 {超連結href} 賦值給 {表單action} -> 取消超連結預設行為 -> 處理器刪除員工後,重定向到員工列表頁面做展示
- 員工列表
程式碼實現
springMVC.xml
<context:component-scan base-package="cn.edu.nuist"></context:component-scan>
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/pages/"></property>
<property name="suffix" value=".jsp"></property>
</bean>
<!--
前端控制器配置的'/',意為攔截除 Jsp 外所有請求,所以 JS 請求 404
而關於靜態資源的請求,都是 tomcat 的 DefaultServlet 在負責處理。
<mvc:default-servlet-handler/> 不能處理的請求交給 tomcat
副作用:靜態是可以訪問了,動態對映的完蛋了
-->
<mvc:default-servlet-handler/>
<!-- 保證動態|靜態請求都能訪問 -->
<mvc:annotation-driven/>
EmpController
@Controller
public class EmpController {
@Autowired
EmployeeDao empDao;
@Autowired
DepartmentDao deptDao;
@RequestMapping(value="emp", method=RequestMethod.GET)
public String selectAll(Model model) {
Collection<Employee> emps = empDao.getAll();
model.addAttribute("emps", emps);
return "list";
}
@RequestMapping(value="toAddPage")
public String toAddPage(Model model) {
Collection<Department> depts = deptDao.getDepartments();
model.addAttribute("depts", depts);
/* model.addAttribute("employee", new Employee(null, "張三"
, "[email protected]", 1, deptDao.getDepartment(103))); */
model.addAttribute("employee", new Employee());
return "add";
}
@RequestMapping(value="emp", method=RequestMethod.POST)
public String addEmp(Employee emp) {
System.out.println("要新增的員工:" + emp);
empDao.save(emp);
return "redirect:/emp";
}
@RequestMapping(value="/emp/{id}", method=RequestMethod.GET)
public String toEditPage(Model model, @PathVariable("id")Integer id) {
model.addAttribute("employee", empDao.get(id));
model.addAttribute("depts", deptDao.getDepartments());
return "edit";
}
@ModelAttribute
public void getUpdateEmpInfo(Model model
, @RequestParam(value="id", required = false)Integer id) {
System.out.println("@ModelAttribute: getUpdateEmpInfo");
/*
* 不能從 @PathVariable("id") 中拿, @ModelAttribute
* 註解會在所有目標方法執行前執行,而且,該註解只有一
* 個 value 屬性,如果請求沒帶該屬性,則會拋異常。
* ·····························
* 所以,使用 @RequestParam 給形參賦值,並可設定該註解
* 的 required 屬性為 false
*/
if(id != null)
model.addAttribute("employee", empDao.get(id));
}
@RequestMapping(value="/emp/{id}", method=RequestMethod.PUT)
public String updateEmp(@ModelAttribute("employee")Employee emp) {
System.out.println(emp);
empDao.save(emp);
return "redirect:/emp";
}
@RequestMapping(value="/emp/{id}", method=RequestMethod.DELETE)
public String deleteEmp(@PathVariable("id")Integer id) {
empDao.delete(id);
return "redirect:/emp";
}
}
頁面
index.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!-- 訪問專案就要展示員工列表頁面 -->
<jsp:forward page="/emp"></jsp:forward>
list.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>員工列表頁面</title>
<script src="${pageContext.request.contextPath }/scripts/jquery-1.9.1.min.js"></script>
</head>
<body>
<table border="1">
<tr>
<th>ID</th>
<th>LASTNAME</th>
<th>EMAIL</th>
<th>GENDER</th>
<th>DEPARTMENT</th>
<th>OPTION<a href="toAddPage">(ADD)</a></th>
</tr>
<c:forEach items="${emps }" var="emp">
<tr>
<td>${emp.id }</td>
<td>${emp.lastName }</td>
<td>${emp.email }</td>
<td>${emp.gender==1 ? '男' : '女' }</td>
<td>${emp.department.departmentName }</td>
<td>
<a href="${pageContext.request.contextPath }/emp/${emp.id}">EDIT</a>
<a href="${pageContext.request.contextPath }/emp/${emp.id}" class="del">DELETE</a>
</td>
</tr>
</c:forEach>
</table>
<form method="POST" id="delForm"><input type="hidden" name="_method" value="DELETE"/></form>
<script type="text/javascript">
$(function() {
$(".del").click(function() {
// 1. 改變表單的 action,並提交表單
$("#delForm").attr("action", this.href).submit();
// 2. 禁止超連結預設行為
return false;
});
});
</script>
</body>
</html>
add.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@taglib uri="http://www.springframework.org/tags/form" prefix="form"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>新增員工</title>
<!--
[SpringMVC表單標籤] 將模型資料中的屬性和HTML表單元素相繫結,以實現表單資料更便捷編輯和表單值的回顯。
[可能丟擲的異常] IllegalStateException: Neither BindingResult nor plain
target object for bean name 'command' available as request attribute.
1. SpringMVC 認為,表單資料的每一項最終都是要回顯的,path 指定的是一
個屬性,這個屬性是從隱含模型(請求域)中取出的某個物件中的屬性。
2. path 指定的每一個屬性,請求域中必須有一個物件,擁有這個屬性。預設去
請求域中找一個叫 'command' 對應的物件。
3. 可通過 modelAttribute 屬性來修改這個物件名,而不是去找 command
-->
</head>
<body>
<form:form action="${pageContext.request.contextPath }/emp"
method="POST" modelAttribute="employee">
<!--
path:
1. 當作原生 input~name
2. 會自動回顯隱含模型中某個物件對應的同名屬性的值
-->
lastName: <form:input path="lastName"/><br/>
email: <form:input path="email" /><br/>
gender: 男<form:radiobutton path="gender" value="1"/>
女<form:radiobutton path="gender" value="0"/><br/>
<!--
itmes: 指定要遍歷的集合;自動遍歷;遍歷出的每一個元素都是一個 Department 物件
itemLabel 指定一個屬性,遍歷到的物件的哪個屬性作為 option(提示資訊)
itemValue 指定一個屬性,遍歷到的物件的哪個屬性作為 value(提交資訊)
-->
dept: <form:select path="department.id" items="${depts }"
itemLabel="departmentName" itemValue="id"/><br/>
<input type="submit" value="儲存" />
</form:form>
<%-- <form>
lastName: <input type="text" name="lastName"/><br/>
email: <input type="text" name="email"/><br/>
gender: 男<input type="radio" name="gender" value="1"/>
女<input type="radio" name="gender" value="0"/><br/>
部門:<select name="department.id">
<c:forEach items="${depts }" var="dept">
<!-- 標籤體是在頁面的提示選項資訊,value值才是真正提交的值 -->
<option value="${dept.id }">${dept.departmentName }</option>
</c:forEach>
</select><br/>
<input type="submit" value="新增" />
</form> --%>
</body>
</html>
edit.jsp
<form:form action="${pageContext.request.contextPath }/emp/${employee.id }"
method="PUT" modelAttribute="employee">
<%-- <form:hidden path="lastName"/> --%>
<form:hidden path="id" />
email: <form:input path="email"/><br/>
gender: 男<form:radiobutton path="gender" value="1"/>
女<form:radiobutton path="gender" value="0"/><br/>
department: <form:select path="department.id" items="${depts }"
itemLabel="departmentName" itemValue="id"></form:select><br/>
<input type="submit" value="提交" />
</form:form>