springboot學習10——springmvc(上)

Spring MVC定位於一個較爲鬆散的組合,展示給用戶的視圖(View)、控制器返回的數據模型(Model)、定位視圖的視圖解析器(ViewResolver)和處理適配器(HandlerAdapter)等內容都是獨立的。
換句話說,通過Spring MVC很容易把後臺的數據轉換爲各種類型的數據。

例如,Spring MVC可以十分方便地轉換爲目前最常用的JSON數據集,也可以轉換爲PDF、Excel和XML等。加之Spring MVC是基於Spring基礎框架派生出來的Web框架,所以它天然就可以十分方便地整合到Spring框架中。

基於這些趨勢,Spring MVC已經成爲當前最主流的Web開發框架。學習Spring MVC,首先是學習其基於MVC的分層的思想

一、Spring MVC框架的設計

MVC的巨大成功在於它的理念,所以有必要學習一下MVC框架。
先看下Spring MVC的示意圖:
在這裏插入圖片描述

MVC框架運行的流程:
1.處理請求先到達控制器(Controller),控制器的作用是進行請求分發,這樣它會根據請求的內容去訪問模型層(Model);
2.在現今互聯網系統中,數據主要從數據庫和NoSQL中來,而且對於數據庫而言往往還存在事務的機制,爲了適應這樣的變化,設計者會把模型層再細分爲兩層,即服務層(Service)和數據訪問層(DAO);
3.當控制器獲取到由模型層返回的數據後,就將數據渲染到視圖中,這樣就能夠展現給用戶了。

二、Spring MVC流程

Spring MVC的流程是圍繞DispatcherServlet而工作的,所以在Spring MVC中DispatcherServlet就是其最重要的內容。
在DispatcherServlet的基礎上,還存在其他的組件,掌握流程和組件就是Spring MVC開發的基礎。

上面是Spring MVC運行的全流程,但是嚴格地說,Spring MVC處理請求並非一定需要經過全流程,有時候一些流程並不存在。例如,在我們加入@ResponseBody時,是沒有經過視圖解析器和視圖渲染的。
在這裏插入圖片描述

1.首先,在Web服務器啓動的過程中,如果在Spring Boot機制下啓用Spring MVC,它就開始初始化一些重要的組件,如DispactherServlet、HandlerAdapter的實現類RequestMappingHandlerAdapter等組件對象。
關於這些組件的初始化,我們可以看到spring-webmvc-xxx.jar包的屬性文件DispatcherServlet.properties,
它定義的對象都是在Spring MVC開始時就初始化,並且存放在Spring IoC容器中。

DispatcherServlet.properties
# Default implementation classes


for DispatcherServlet's strategy interfaces.
# Used as fallback when no matching beans are found in the DispatcherServlet context.
# Not meant to be customized by application developers.

# 國際化解析器
org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver

# 主題解析器
org.springframework.web.servlet.ThemeResolver=org.springframework.web.servlet.theme.FixedThemeResolver

# HandlerMapping實例
org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.
BeanNameUrlHandlerMapping,\
    org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping

# 處理器適配器
org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.
HttpRequestHandlerAdapter,\
    org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\
    org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter

# 處理器異常解析器
org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.
servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver,\
    org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\
    org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver

# 策略視圖名稱轉換器,當你沒有返回視圖邏輯名稱的時候,通過它可以生成默認的視圖名稱
org.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.
servlet.view.DefaultRequestToViewNameTranslator

# 視圖解析器
org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.
InternalResourceViewResolver

# FlashMap管理器。不常用,不再討論
org.springframework.web.servlet.FlashMapManager=org.springframework.web.servlet.
support.SessionFlashMapManager

上述組件會在Spring MVC得到初始化,尤其是在Spring Boot中,更是如此,我們可以通過Spring Boot的配置來定製這些組件的初始化。

2.其次是開發控制器(Controller)

package com.springboot.chapter9.controller;
/**** imports ****/
@Controller
@RequestMapping("/user")
public class UserController {

    // 注入用戶服務類
    @Autowired
    private UserService userService = null;

    // 展示用戶詳情
    @RequestMapping("details")
    public ModelAndView details(Long id) {
        // 訪問模型層得到數據
        User user = userService.getUser(id);
        // 模型和視圖
        ModelAndView mv = new ModelAndView();
        // 定義模型視圖
        mv.setViewName("user/details");
        // 加入數據模型
        mv.addObject("user", user);
        // 返回模型和視圖
        return mv;
    }
}

3.註解@Controller表明這是一個控制器,然後@RequestMapping代表請求路徑和控制器(或其方法)的映射關係,它會在Web服務器啓動Spring MVC時,就被掃描到HandlerMapping的機制中存儲,
之後在用戶發起請求被DispatcherServlet攔截後,通過URI和其他的條件,通過HandlerMapper機制就能找到對應的控制器(或其方法)進行響應。
只是通過HandlerMapping返回的是一個HandlerExecutionChain對象。

HandlerExecutionChain源碼

package org.springframework.web.servlet;
/**** imports ****/
public class HandlerExecutionChain {
    // 日誌
    private static final Log logger = LogFactory.getLog(HandlerExecutionChain.class);
    // 處理器
    private final Object handler;
    // 攔截器數組
    private HandlerInterceptor[] interceptors;
    // 攔截器列表
    private List<HandlerInterceptor> interceptorList;
    // 攔截器當前下標
    private int interceptorIndex= -1;
    ......
}

4.HandlerExecutionChain對象包含一個處理器(handler)
這裏的處理器是對控制器(controller)的包裝,因爲我們的控制器方法可能存在參數,那麼處理器就可以讀入HTTP和上下文的相關參數,然後再傳遞給控制器方法。而在控制器執行完成返回後,處理器又可以通過配置信息對控制器的返回結果進行處理。

從這段描述中可以看出,處理器包含了控制器方法的邏輯,此外還有處理器的攔截器(interceptor),這樣就能夠通過攔截處理器進一步地增強處理器的功能。

5.得到了處理器(handler),還需要去運行,但是我們有普通HTTP請求,也有按BeanName的請求,甚至是WebSocket的請求,所以它還需要一個適配器去運行HandlerExecutionChain對象包含的處理器,
這就是HandlerAdapter接口定義的實現類。Spring MVC中最常用的HandlerAdapter的實現類,這是HttpRequestHandlerAdapter。

6.通過請求的類型,DispatcherServlet就會找到它來執行Web請求的HandlerExecutionChain對象包含的內容,這樣就能夠執行我們的處理器(handler)了

7.在處理器調用控制器時,它首先通過模型層得到數據,再放入數據模型中,最後將返回模型和視圖(ModelAndView)對象
這裏控制器設置的視圖名稱設置爲“user/details”,這樣就走到了視圖解析器(ViewResolver),去解析視圖邏輯名稱了。

8.視圖解析器(ViewResolver)的自動初始化是可定製的
爲了定製InternalResourceViewResolver初始化,可以在配置文件application.properties中進行配置。

spring.mvc.view.prefix=/WEB-INF/jsp/
spring.mvc.view.suffix=.jsp

通過修改這樣的配置,就能在Spring Boot的機制下定製InternalResourceViewResolver這個視圖解析器的初始化,
也就是在返回視圖名稱之後,它會以前綴(prefix)和後綴(suffix)以及視圖名稱組成全路徑定位視圖。例如,在控制器中返回的是“user/details”,那麼它就會找到/WEB-INF/jsp/user/details.jsp作爲視圖(View)。

嚴格地說,這一步也不是必需的,因爲有些視圖並不需要邏輯名稱,在不需要的時候,就不再需要視圖解析器工作了。

9.視圖解析器定位到視圖後,視圖的作用是將數據模型(Model)渲染,這樣就能夠響應用戶的請求。
這一步就是視圖將數據模型渲染(View)出來,用來展示給用戶查看。
按照我們控制器的返回,就是/WEB-INF/jsp/user/details.jsp作爲我們的視圖。

Spring Boot啓動文件

// 定製掃描路徑
@SpringBootApplication(scanBasePackages = "com.springboot.chapter9")
// 掃描MyBatis的DAO接口
@MapperScan(basePackages = "com.springboot.chapter9",
   annotationClass = Repository.class)
public class Chapter9Application {
    public static void main(String[] args) {
        SpringApplication.run(Chapter9Application.class, args);
    }
}

運行它得到如下日誌:

Mapped "{[/user/details]}" onto p
ublic org.springframework.web.servlet.ModelAndView 
com.springboot.chapter9.controller.UserController.details(java.lang.Long)
Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.
Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.
BasicErrorController.error(javax.servlet.http.HttpServletRequest)
Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.
ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml
(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.
servlet.resource.ResourceHttpRequestHandler]
Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.
resource.ResourceHttpRequestHandler]
Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.
web.servlet.resource.ResourceHttpRequestHandler]
Registering beans for JMX exposure on startup
Tomcat started on port(s): 8080 (http)
Started Chapter9Application in 3.275 seconds (JVM running for 3.608)

注意開頭的部分,證明我們配置的@RequestMapping的請求映射已經在服務器啓動時被掃描到了Spring的上下文中,所以當請求來到時就可以匹配找到對應的控制器去提供服務。

然後我們通過請求http://localhost:8080/user/details?id=1以及HandlerMapping的匹配機制就可以找到處理器提供服務。而這個處理器則包含我們開發的控制器,那麼進入這個控制器後,它就執行控制器的邏輯,通過模型和視圖(ModelAndView)綁定了數據模型,而且把視圖名稱修改爲了“user/details”,隨後返回。

模型和視圖(ModelAndView)返回後,視圖名稱爲“user/details”,而我們定義的視圖解析器(InternalResourceViewResolver)的前綴爲/WEB-INF/jsp/,且後綴爲.jsp,這樣它便能夠映射爲/WEB- INF/jsp/user/details.jsp,進而找到JSP文件作爲視圖,這便是視圖解析器的作用。然後將數據模型渲染到視圖中。

有時候,我們可能需要的只是JSON數據集,因爲目前前後臺分離的趨勢,使用JSON已經是主流的方式,正如我們之前使用的@ResponseBody標明方法一樣,
在後面Spring MVC會把數據轉換爲JSON數據集,但是這裏暫時不談@ResponseBody,因爲它會採用處理器內部的機制。本節暫時不討論處理器的內部機制,
而是先用MappingJackson2JsonView轉換出JSON
在這裏插入圖片描述

實例在Spring MVC下的流程圖

使用JSON視圖

@RequestMapping("/detailsForJson")
public ModelAndView detailsForJson(Long id) {
    // 訪問模型層得到數據
    User user = userService.getUser(id);
    // 模型和視圖
    ModelAndView mv = new ModelAndView();
    // 生成JSON視圖
    MappingJackson2JsonView jsonView = new MappingJackson2JsonView();
    mv.setView(jsonView);
    // 加入模型
    mv.addObject("user", user);
    return mv;
}

可以看到,在控制器的方法中模型和視圖(ModelAndView)中捆綁了JSON視圖(Mapping- Jackson2JsonView)和數據模型(User對象),然後返回,其結果也會轉變爲JSON,
只是需要注意的是這步與我們使用JSP作爲視圖是不一樣的。

之前我們給視圖設置了名稱,它會根據視圖解析器(InternalResourceViewResolver)的解析找到JSP,然後渲染數據到視圖中,從而展示最後的結果,而這裏的JSON視圖是沒有視圖解析器的定位視圖的,因爲它不是一個邏輯視圖,只是需要將數據模型(這裏是User對象)轉換爲JSON而已。

從流程圖中我們可以看到並沒有視圖解析器,那是因爲MappingJackson2JsonView是一個非邏輯視圖。它並不需要視圖解析器進行定位,它的作用只是將數據模型渲染爲JSON數據集來響應請求。

可見Spring MVC中,不是每一個步驟都是必需的,而是根據特別的需要會有不同的流程。
在這裏插入圖片描述

三、定製Spring MVC的初始化

在Servlet 3.0規範中,web.xml再也不是一個必需的配置文件。爲了適應這個規範,Spring MVC從3.1版本開始也進行了支持,也就是我們已經不再需要通過任何的XML去配置Spring MVC的運行環境。
爲了支持對於Spring MVC的配置,Spring提供了接口WebMvcConfigurer,這是一個基於Java 8的接口,所以其大部分方法都是default類型的,但是它們都是空實現,這樣開發者只需要實現這個接口,重寫需要自定義的方法即可,這樣就很方便進行開發了。

在Spring Boot中,自定義是通過配置類WebMvcAutoConfiguration定義的,它有一個靜態的內部類WebMvcAutoConfigurationAdapter,通過它Spring Boot就自動配置了Spring MVC的初始化。
在這裏插入圖片描述

在WebMvcAutoConfigurationAdapter類中,它會讀入Spring配置Spring MVC的屬性來初始化對應組件,這樣便能夠在一定程度上實現自定義。

Spring MVC可配置項
# SPRING MVC (WebMvcProperties)
spring.mvc.async.request-timeout=             # 異步請求超時時間(單位爲毫秒)
spring.mvc.contentnegotiation.favor-parameter=false 
                         #  是否使用請求參數(默認參數爲"format")來確定請求的媒體類型
spring.mvc.contentnegotiation.favor-path-extension=false 
                         # 是否使用URL中的路徑擴展來確定請求的媒體類型
spring.mvc.contentnegotiation.media-types.*= 
                         # 設置內容協商向媒體類型映射文件擴展名。例如,YML文本/YAML
spring.mvc.contentnegotiation.parameter-name= # 當啓用favor-parameter參數是,自定義參數名
spring.mvc.date-format=                 # 日期格式配置,如yyyy-MM-dd
spring.mvc.dispatch-trace-request=false # 是否讓FrameworkServlet doService方法支持TRACE請求
spring.mvc.dispatch-options-request=true 
                         # 是否啓用 FrameworkServlet doService 方法支持OPTIONS請求
spring.mvc.favicon.enabled=true         # spring MVC的圖標是否啓用
spring.mvc.formcontent.putfilter.enabled=true 
    # Servlet規範要求表格數據可用於HTTP POST而不是HTTP PUT或PATCH請求,這個選項將使得過濾器攔截
HTTP PUT和PATCH,且內容類型是application/x-www-form-urlencoded的請求,並且將其轉換爲POST請求
spring.mvc.ignore-default-model-on-redirect=true 
                         # 如果配置爲default,那麼它將忽略模型重定向的場景
spring.mvc.locale=                         # 默認國際化選項,默認取Accept-Language
spring.mvc.locale-resolver=accept-header   # 國際化解析器,如果需要固定可以使用fixed
spring.mvc.log-resolved-exception=false    # 是否啓用警告日誌異常解決
spring.mvc.message-codes-resolver-format=  # 消息代碼的格式化策略。例如,' prefix_error_code '
spring.mvc.pathmatch.use-registered-suffix-pattern=false 
    # 是否對spring.mvc.contentnegotiation.media-types.*註冊的擴展采用後綴模式匹配
spring.mvc.pathmatch.use-suffix-pattern=false  # 當匹配模式到請求時,是否使用後綴模式匹配(.*)
spring.mvc.servlet.load-on-startup=-1          # 啓用Spring Web服務Serlvet的優先順序配置
spring.mvc.static-path-pattern=/**             # 指定靜態資源路徑
spring.mvc.throw-exception-if-no-handler-found=false 
                         # 如果請求找不到處理器,是否拋出 NoHandlerFoundException異常
spring.mvc.view.prefix=                        # Spring MVC視圖前綴
spring.mvc.view.suffix=                        # Spring MVC視圖後綴

這些配置項將會被Spring Boot的機制讀入,然後使用WebMvcAutoConfigurationAdapter去定製初始化。
對於這些選項,我們還可以實現接口WebMvcConfigurer,然後加入自己定義的方法即可,畢竟這個接口是Java 8的接口,其本身已經提供了default方法,對其定義的方法做了空實現。

四、Spring MVC實例

Spring MVC的開發核心是控制器的開發,控制器的開發又分爲這麼幾個步驟,
首先是定義請求分發,讓Spring MVC能夠產生HandlerMapping,
其次是接收請求獲取參數,
再次是處理業務邏輯獲取數據模型,
最後是綁定視圖和數據模型。

下面演示一個用戶列表查詢的界面。假設可以通過用戶名稱(userName)和備註(note)進行查詢,但是一開始進入頁面需要載入所有的數據展示給用戶查看。
這裏分爲兩種常見的場景,
一種是剛進入頁面時,一般來說是不允許存在異步請求的,因爲異步請求會造成數據的刷新,對用戶不友好;
另一種是進入頁面後的查詢,這時可以考慮使用Ajax異步請求,只刷新數據而不刷新頁面,這纔是良好的UI體驗設計。

1.用戶控制器

@Controller
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService = null;   

    ......

    @RequestMapping("/table")
    public ModelAndView table() {
        // 訪問模型層得到數據
        List<User> userList = userService.findUsers(null, null);
        // 模型和視圖
        ModelAndView mv = new ModelAndView();
        // 定義模型視圖
        mv.setViewName("user/table");
        // 加入數據模型
        mv.addObject("userList", userList);
        // 返回模型和視圖
        return mv;
    }

    @RequestMapping("/list")
    @ResponseBody
    public List<User> list(
        @RequestParam(value = "userName", required = false) String userName, 
        @RequestParam(value = "note", required = false) String note) {
        // 訪問模型層得到數據
        List<User> userList = userService.findUsers(userName, note);
        return userList;
    }
}

開發控制器首先是指定請求分發,這個任務是交由註解@RequestMapping去完成的,這個註解可以標註類或者方法,當一個類被標註的時候,所有關於它的請求,都需要在@RequestMapping定義的URL下。
當方法被標註後,也可以定義部分URL,這樣就能讓請求的URL找到對應的路徑。配置了掃描路徑之後,Spring MVC掃描機制就可以將其掃描,並且裝載爲HandlerMapping,以備後面使用。

這裏的控制器存在兩個方法。
table:這個方法的任務是進入頁面時,首先查詢所有的用戶,是一個沒有條件的查詢,當它查詢出所有的用戶數據後,創建模型和視圖(ModeAndView),接着指定視圖名稱爲“user/table”,
然後將查詢到的用戶列表捆綁到模型和視圖中,最後返回模型和視圖。

2.視圖和視圖渲染

視圖/WEB-INF/jsp/user/table.jsp

<%@ page pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>用戶列表</title>
<link rel="stylesheet" type="text/css"
    href="../../easyui/themes/default/easyui.css">
<link rel="stylesheet" type="text/css"
    href="../../easyui/themes/icon.css">
<link rel="stylesheet" type="text/css" href="../../easyui/demo/demo.css">
<script type="text/javascript" src="../../easyui/jquery.min.js"></script>
<script type="text/javascript" src="../../easyui/jquery.easyui.min.js"></script>
<script type="text/javascript">
    // 定義事件方法
    function onSearch() {
        // 指定請求路徑
        var opts = $("#dg").datagrid("options");
        opts.url = "./list";
        // 獲取查詢參數
        var userName = $("#userName").val();
        var note = $("#note").val();
        // 組織參數
        var params = {};
        if (userName != null && userName.trim() != '') {
            params.userName = userName;
        }
        if (note != null && note.trim() != '') {
            params.note = note;
        }
        // 重新載入表格數據
        $("#dg").datagrid('load', params);
    }
</script>
</head>
<body>
    <div style="margin: 20px 0;">


</div>
    <div class="easyui-layout" style="width: 100%; height: 350px;">
        <div data-options="region:'north'" style="height: 50px">
            <form id="searchForm" method="post">
                <table>
                    <tr>
                        <td>用戶名稱:</td>
                        <td><input id="userName" name="userName"
                            class="easyui-textbox" 
                            data-options="prompt:'輸入用戶名稱...'"
                            style="width: 100%; height: 32px"></td>
                        <td>備註</td>
                        <td><input id="note" name="note" class="easyui-textbox"
                            data-options="prompt:'輸入備註...'" 
                             style="width: 100%; height: 32px">
                        </td>
                        <td>查詢</td>
                    </tr>
                </table>
            </form>
        </div>
        <div data-options="region:'center',title:'用戶列表',iconCls:'icon-ok'">
            <table id="dg" class="easyui-datagrid",
                data-options="border:false,singleSelect:true,
                fit:true,fitColumns:true">
                <thead>
                    <tr>
                        <th data-options="field:'id'" width="80">編號</th>
                        <th data-options="field:'userName'" width="100">
                                用戶名稱
                        </th>
                        <th data-options="field:'note'" width="80">備註</th>
                    </tr>
                </thead>
                <tbody>
                    <!--使用forEache渲染數據模型-->
                    <c:forEach items="${userList}" var="user">
                        <tr>
                            <td>${user.id}</td>
                            <td>${user.userName}</td>
                            <td>${user.note}</td>
                        </tr>
                    </c:forEach>
                </tbody>
            </table>
        </div>
    </div>
</body>

這裏使用了EasyUI以及它的控件DataGrid(數據網格),通過JSTL的forEach標籤進行循環將控制器返回的用戶列表渲染到這張JSP中,所以在剛剛進入頁面的時候,就可以展示用戶列表,
因爲這裏採用先取數據後渲染的方式,所以剛剛進入頁面的時候並不會出現Ajax的異步請求,這樣有助於提高UI(用戶接口)體驗。

頁面中還定義了兩個文本框,用來輸入用戶名和備註,然後通過查詢按鈕進行查詢。
這裏查詢按鈕的點擊事件定義爲onSearch,這樣就能夠找到onSearch函數來執行查詢,在這個函數中,首先定義DataGrid請求的URL,它指向了list方法,然後通過jQuery去獲取兩個文本框的參數值,再通過DataGrid的load方法,傳遞參數去後端查詢,得到數據後重新載入DataGrid的數據,這樣DataGrid就能夠得到查詢的數據了。

list方法:
首先它標註爲了@ResponseBody,這樣Spring MVC就知道最終需要把返回的結果轉換爲JSON。
然後是獲取參數,這裏使用了註解@RequestParam,通過指定參數名稱使得HTTP請求的參數和方法的參數進行綁定,只是這個註解的默認規則是參數不能爲空。
爲了克服這個問題,代碼將其屬性required設置爲false即可,其意義就是允許參數爲空。這樣就可以測試這個請求了。

測試用戶查詢
點擊查詢按鈕後,就會執行Ajax請求,把數據取回來,用來顯示在DataGrid控件中。
這樣就可以在互聯網系統中,第一次進入一個新的頁面,就可以無刷新地顯示數據,而在查詢等操作中使用Ajax,這樣就有效地提高了用戶的體驗。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章