1. 程式人生 > 實用技巧 >[Re] SpringMVC-3

[Re] SpringMVC-3

檢視解析

使用

@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檢視解析:

  1. 方法執行後的返回值會作為頁面地址參考,轉發或者重定向到頁面
  2. 檢視解析器可能會進行頁面地址的拼串

  1. 任何方法的返回值,最終都會被包裝成 ModelAndView 物件。
  2. 來到頁面的方法:processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException)
  3. 檢視渲染流程:將域中的資料在頁面中展示,頁面就是用來渲染模型資料的。
    [1012] render(mv, request, response);
    [1204] view = resolveViewName(mv.getViewName(), mv.getModelInternal(), locale, request);
    
  4. View 和 ViewResolver
  5. 根據檢視名得到檢視物件:
    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);
    }
    

  1. 檢視解析器得到 View 物件的流程:所有配置的檢視解析器都來根據檢視名(返回值)得到 View 物件。如果能得到,就返回;得不到就換下一個檢視解析器。
  2. 呼叫 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);
    }
    
  3. 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);
        }
    }
    
  4. 為什麼隱含模型中的資料能在 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>