zoukankan      html  css  js  c++  java
  • [Re] SpringMVC-3(视图解析+RESTful CRUD)

    1. 视图解析

    1.1 使用

    @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 次重定向
        }
    }
    

    1.2 源码

    1.2.1 得到 View 对象

    (1) 任何方法的返回值,最终都会被包装成 ModelAndView 对象。

    (2) 来到页面的方法:processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException)

    (3) 视图解析器得到 View 对象的流程:所有配置的视图解析器都来根据视图名(返回值)得到 View 对象。如果能得到,就返回;得不到就换下一个视图解析器来试试能不能得到。

    <<DispatcherServlet>>
    
    [1012] render(mv, request, response);
      ||
      /
    [1204] view = resolveViewName(mv.getViewName(), mv.getModelInternal(), locale, request);
    
    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;
    }
    

    ViewResolver<I> 作用是根据视图名得到视图对象 View

    (4) InternalResourceViewResolver 的 resolveViewName 实现细节:

    @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.2.2 View 渲染视图

    View 和 ViewResolver

    视图解析器只是为了得到视图对象,视图对象才能真正的渲染页面 // 转发(并将隐含模型中的数据放入请求域)或者重定向到页面

    调用 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() + "'");
                }
            }
        }
    }
    

    1.3 视图和视图解析器

    • 请求处理方法执行完成后,最终返回一个 ModelAndView 对象。对于那些返回 String,View 或 ModelMap 等类型的处理方法,Spring MVC 也会在内部将它们装配成一个 ModelAndView 对象,它包含了逻辑名和模型对象的视图。
    • Spring MVC 借助视图解析器(ViewResolver)得到最终的视图对象(View),最终的视图可以是 JSP ,也可能是 Excel、JFreeChart 等各种表现形式的视图。
    • 对于最终究竟采取何种视图对象对模型数据进行渲染,处理器并不关心,处理器工作重点聚焦在生产模型数据的工作上,从而实现 MVC 的充分解耦。

    1.3.1 视图

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

    1.3.2 视图解析器

    • 视图解析器的作用比较单一:将逻辑视图解析为一个具体的视图对象。
    • 所有的视图解析器都必须实现 ViewResolver 接口。SpringMVC 为逻辑视图名的解析提供了不同的策略,可以在 Spring WEB 上下文中配置一种或多种解析策略,并指定他们之间的先后顺序。每一种映射策略对应一个具体的视图解析器实现类。
    • 常用的视图解析器实现类
      • 程序员可以选择一种视图解析器或混用多种视图解析器。
      • 每个视图解析器都实现了 Ordered 接口并开放出一个 order 属性,可以通过 order 属性指定解析器的优先顺序,order 越小优先级越高
      • SpringMVC 会按视图解析器的优先顺序对逻辑视图名进行解析,直到解析成功并返回视图对象,否则将抛出 ServletException 异常。

    1.3.3 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">
      

    1.3.4 自定义视图和视图解析器

    • Tips
      • 视图解析器根据方法的返回值得到视图对象
      • 多个视图解析器都会尝试能否得到视图对象
      • 视图对象不同就可以具有不同功能
    • 自定义视图和视图解析器
      • 编写自定义视图解析器和视图实现类(为了让自定义的视图解析器先执行,还要让解析器类实现 Ordered 接口)
      • 视图解析器必须放在 IOC 容器中,让其工作,能创建出我们的自定义视图对象;
        <bean class="cn.edu.nuist.view.MyMeiNvViewResolver">
            <property name="order" value="1"></property>
        </bean>
        
      • 视图对象自定义渲染逻辑

    2. Restful-CRUD

    2.1 思路

    • 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}
            -> 取消超链接默认行为
        -> 处理器删除员工后,重定向到员工列表页面做展示
        

    2.2 代码实现

    2.2.1 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/>
    

    2.2.2 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, "张三"
                    , "123@163.com", 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";
        }
    }
    

    2.2.3 页面

    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>
    
  • 相关阅读:
    phpspider爬虫框架的使用
    【php设计模式】责任链模式
    【php设计模式】策略模式
    【php设计模式】观察者模式
    【php设计模式】模板模式
    【温故知新】php 魔术方法
    【php设计模式】享元模式
    Java50道经典习题-程序18 乒乓球赛
    Java50道经典习题-程序19 输入行数打印菱形图案
    Java50道经典习题-程序20 求前20项之和
  • 原文地址:https://www.cnblogs.com/liujiaqi1101/p/13674467.html
Copyright © 2011-2022 走看看