MVC 設計概述
在早期 Java Web 的開發中,統一把顯示層、控制層、數據層的操作全部交給 JSP 或者 JavaBean 來進行處理,我們稱之為 Model1:
- 出現的弊端:
- JSP 和 Java Bean 之間嚴重耦合,Java 代碼和 HTML 代碼也耦合在了一起
- 要求開發者不僅要掌握 Java ,還要有高超的前端水平
- 前端和後端相互依賴,前端需要等待後端完成,後端也依賴前端完成,才能進行有效的測試
- 代碼難以複用
正因為上面的種種弊端,所以很快這種方式就被 Servlet + JSP + Java Bean 所替代了,早期的 MVC 模型(Model2)就像下圖這樣:
首先用戶的請求會到達 Servlet,然後根據請求調用相應的 Java Bean,並把所有的顯示結果交給 JSP 去完成,這樣的模式我們就稱為 MVC 模式。
- M 代表 模型(Model)
模型是什麼呢? 模型就是數據,就是 dao,bean - V 代表 視圖(View)
視圖是什麼呢? 就是網頁, JSP,用來展示模型中的數據 - C 代表 控制器(controller)
控制器是什麼? 控制器的作用就是把不同的數據(Model),顯示在不同的視圖(View)上,Servlet 扮演的就是這樣的角色。
擴展閱讀:Web開發模式
Spring MVC 的架構
為解決持久層中一直未處理好的數據庫事務的編程,又為了迎合 NoSQL 的強勢崛起,Spring MVC 給出了方案:
傳統的模型層被拆分為了業務層(Service)和數據訪問層(DAO,Data Access Object)。 在 Service 下可以通過 Spring 的聲明式事務操作數據訪問層,而在業務層上還允許我們訪問 NoSQL ,這樣就能夠滿足異軍突起的 NoSQL 的使用了,它可以大大提高互聯網系統的性能。
- 特點:
結構鬆散,幾乎可以在 Spring MVC 中使用各類視圖
松耦合,各個模塊分離
與 Spring 無縫集成
Hello Spring MVC
讓我們來寫一下我們的第一個 Spring MVC 程序:
第一步:在 IDEA 中新建 Spring MVC 項目
並且取名為 【HelloSpringMVC】,點擊【Finish】:
IDEA 會自動幫我們下載好必要的 jar 包,並且為我們創建好一些默認的目錄和文件,創建好以後項目結構如下:
第二步:修改 web.xml
我們打開 web.xml ,按照下圖完成修改:
把<url-pattern>元素的值改為 / ,表示要攔截所有的請求,並交由Spring MVC的後臺控制器來處理,改完之後:/<url-pattern>
<code><servlet-mapping>
<servlet-name>dispatcher/<servlet-name>
<url-pattern>//<url-pattern>
/<servlet-mapping>
/<code>
第三步:編輯 dispatcher-servlet.xml
這個文件名的開頭 dispatcher 與上面 web.xml 中的 <servlet-name> 元素配置的 dispatcher 對應,這是 Spring MVC 的映射配置文件(xxx-servlet.xml),我們編輯如下:/<servlet-name>
<code>
<beans> xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean> class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property>
<props>
<prop>helloController/<prop>
/<props>
/<property>
/<bean>
<bean>
/<beans>
/<code>
第四步:編寫 HelloController
在 Package【controller】下創建 【HelloController】類,並實現 org.springframework.web.servlet.mvc.Controller 接口:
<code>package controller;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
public class HelloController implements Controller{
@Override
public ModelAndView handleRequest(javax.servlet.http.HttpServletRequest httpServletRequest, javax.servlet.http.HttpServletResponse httpServletResponse) throws Exception {
return null;
}
}
/<code>
- 出現了問題: javax.servlet 包找不到
- 解決: 將本地 Tomcat 服務器的目錄下【lib】文件夾下的 servlet-api.jar 包拷貝到工程【lib】文件夾下,添加依賴
Spring MVC 通過 ModelAndView 對象把模型和視圖結合在一起
<code>ModelAndView mav = new ModelAndView("index.jsp");
mav.addObject("message", "Hello Spring MVC");
/<code>
這裡表示視圖的是index.jsp模型數據的是 message,內容是 “Hello Spring MVC”
<code>package controller;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
public class HelloController implements Controller {
public ModelAndView handleRequest(javax.servlet.http.HttpServletRequest httpServletRequest, javax.servlet.http.HttpServletResponse httpServletResponse) throws Exception {
ModelAndView mav = new ModelAndView("index.jsp");
mav.addObject("message", "Hello Spring MVC");
return mav;
}
}
/<code>
第五步:準備 index.jsp
將 index.jsp 的內容修改為:
<code> pageEncoding="UTF-8" isELIgnored="false"%>
${message}
/<code>
內容很簡單,用El表達式顯示 message 的內容。
第六步:部署 Tomcat 及相關環境
在【Run】菜單項下找到【Edit Configurations】
配置 Tomcat 環境:
選擇好本地的 Tomcat 服務器,並改好名字:
在 Deployment 標籤頁下完成如下操作:
點擊 OK 就好了,我們點擊右上角的三角形將 Tomcat 服務器運行起來。
- 出現的問題: Tomcat 服務器無法正常啟動
- 原因: Tomcat 服務器找不到相關的 jar 包
- 解決方法: 將【lib】文件夾整個剪貼到【WEB-INF】下,並重新建立依賴:
第七步:重啟服務器
重啟服務器,輸入地址:localhost/hello
參考資料:Spring MVC 教程(how2j.cn)
跟蹤 Spring MVC 的請求
每當用戶在 Web 瀏覽器中點擊鏈接或者提交表單的時候,請求就開始工作了,像是郵遞員一樣,從離開瀏覽器開始到獲取響應返回,它會經歷很多站點,在每一個站點都會留下一些信息同時也會帶上其他信息,下圖為 Spring MVC 的請求流程:
第一站:DispatcherServlet
從請求離開瀏覽器以後,第一站到達的就是 DispatcherServlet,看名字這是一個 Servlet,通過 J2EE 的學習,我們知道 Servlet 可以攔截並處理 HTTP 請求,DispatcherServlet 會攔截所有的請求,並且將這些請求發送給 Spring MVC 控制器。
<code><servlet>
<servlet-name>dispatcher/<servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet/<servlet-class>
<load-on-startup>1/<load-on-startup>
/<servlet>
<servlet-mapping>
<servlet-name>dispatcher/<servlet-name>
<url-pattern>//<url-pattern>
/<servlet-mapping>
/<code>
- DispatcherServlet 的任務就是攔截請求發送給 Spring MVC 控制器。
第二站:處理器映射(HandlerMapping)
- 問題: 典型的應用程序中可能會有多個控制器,這些請求到底應該發給哪一個控制器呢?
所以 DispatcherServlet 會查詢一個或多個處理器映射來確定請求的下一站在哪裡,處理器映射會根據請求所攜帶的 URL 信息來進行決策,例如上面的例子中,我們通過配置 simpleUrlHandlerMapping 來將 /hello 地址交給 helloController 處理:
<code><bean> class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property>
<props>
<prop>helloController/<prop>
/<props>
/<property>
/<bean>
<bean>
/<code>
第三站:控制器
一旦選擇了合適的控制器, DispatcherServlet 會將請求發送給選中的控制器,到了控制器,請求會卸下其負載(用戶提交的請求)等待控制器處理完這些信息:
<code>public ModelAndView handleRequest(javax.servlet.http.HttpServletRequest httpServletRequest, javax.servlet.http.HttpServletResponse httpServletResponse) throws Exception {
// 處理邏輯
....
}
/<code>
第四站:返回 DispatcherServlet
當控制器在完成邏輯處理後,通常會產生一些信息,這些信息就是需要返回給用戶並在瀏覽器上顯示的信息,它們被稱為模型(Model)。僅僅返回原始的信息時不夠的——這些信息需要以用戶友好的方式進行格式化,一般會是 HTML,所以,信息需要發送給一個視圖(view),通常會是 JSP。
控制器所做的最後一件事就是將模型數據打包,並且表示出用於渲染輸出的視圖名(邏輯視圖名)。它接下來會將請求連同模型和視圖名發送回 DispatcherServlet。
<code>public ModelAndView handleRequest(javax.servlet.http.HttpServletRequest httpServletRequest, javax.servlet.http.HttpServletResponse httpServletResponse) throws Exception {
// 處理邏輯
....
// 返回給 DispatcherServlet
return mav;
}
/<code>
第五站:視圖解析器
這樣以來,控制器就不會和特定的視圖相耦合,傳遞給 DispatcherServlet 的視圖名並不直接表示某個特定的 JSP。(實際上,它甚至不能確定視圖就是 JSP)相反,它傳遞的僅僅是一個邏輯名稱,這個名稱將會用來查找產生結果的真正視圖。
DispatcherServlet 將會使用視圖解析器(view resolver)來將邏輯視圖名匹配為一個特定的視圖實現,它可能是也可能不是 JSP
上面的例子是直接綁定到了 index.jsp 視圖
第六站:視圖
既然 DispatcherServlet 已經知道由哪個視圖渲染結果了,那請求的任務基本上也就完成了。
它的最後一站是視圖的實現,在這裡它交付模型數據,請求的任務也就完成了。視圖使用模型數據渲染出結果,這個輸出結果會通過響應對象傳遞給客戶端。
<code> pageEncoding="UTF-8" isELIgnored="false"%>${message}
/<code>
使用註解配置 Spring MVC
上面我們已經對 Spring MVC 有了一定的瞭解,並且通過 XML 配置的方式創建了第一個 Spring MVC 程序,我們來看看基於註解應該怎麼完成上述程序的配置:
第一步:為 HelloController 添加註解
<code>package controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
@Controller
public class HelloController{
@RequestMapping("/hello")
public ModelAndView handleRequest(javax.servlet.http.HttpServletRequest httpServletRequest, javax.servlet.http.HttpServletResponse httpServletResponse) throws Exception {
ModelAndView mav = new ModelAndView("index.jsp");
mav.addObject("message", "Hello Spring MVC");
return mav;
}
}
/<code>
把實現的接口也給去掉。
- 簡單解釋一下:
- @Controller 註解:
很明顯,這個註解是用來聲明控制器的,但實際上這個註解對 Spring MVC 本身的影響並不大。(Spring 實戰說它僅僅是輔助實現組件掃描,可以用 @Component 註解代替,但我自己嘗試了一下並不行,因為上述例子沒有配置 JSP 視圖解析器我還自己配了一個仍沒有成功...) - @RequestMapping 註解:
很顯然,這就表示路徑 /hello 會映射到該方法上
第二步:取消之前的 XML 註釋
在 dispatcher-servlet.xml 文件中,註釋掉之前的配置,然後增加一句組件掃描:
<code>
<beans> xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<component-scan>
/<beans>
/<code>
第三步:重啟服務器
當配置完成,重新啟動服務器,輸入 localhost/hello 地址仍然能看到效果:
@RequestMapping 註解細節
如果 @RequestMapping 作用在類上,那麼就相當於是給該類所有配置的映射地址前加上了一個地址,例如:
<code>@Controller
@RequestMapping("/wmyskxz")
public class HelloController {
@RequestMapping("/hello")
public ModelAndView handleRequest(....) throws Exception {
....
}
}
/<code>
- 則訪問地址: localhost/wmyskxz/hello
配置視圖解析器
還記得我們 Spring MVC 的請求流程嗎,視圖解析器負責定位視圖,它接受一個由 DispaterServlet 傳遞過來的邏輯視圖名來匹配一個特定的視圖。
- 需求: 有一些頁面我們不希望用戶用戶直接訪問到,例如有重要數據的頁面,例如有模型數據支撐的頁面。
- 造成的問題:
我們可以在【web】根目錄下放置一個【test.jsp】模擬一個重要數據的頁面,我們什麼都不用做,重新啟動服務器,網頁中輸入 localhost/test.jsp 就能夠直接訪問到了,這會造成數據洩露...
另外我們可以直接輸入 localhost/index.jsp 試試,根據我們上面的程序,這會是一個空白的頁面,因為並沒有獲取到 ${message} 參數就直接訪問了,這會影響用戶體驗
解決方案
我們將我們的 JSP 文件配置在【WEB-INF】文件夾中的【page】文件夾下,【WEB-INF】是 Java Web 中默認的安全目錄,是不允許用戶直接訪問的(也就是你說你通過 localhost/WEB-INF/ 這樣的方式是永遠訪問不到的)
但是我們需要將這告訴給視圖解析器,我們在 dispatcher-servlet.xml 文件中做如下配置:
<code><bean> class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property>
<property>
/<bean>
/<code>
這裡配置了一個 Spring MVC 內置的一個視圖解析器,該解析器是遵循著一種約定:會在視圖名上添加前綴和後綴,進而確定一個 Web 應用中視圖資源的物理路徑的。讓我們實際來看看效果:
第一步:修改 HelloController
我們將代碼修改一下:
第二步:配置視圖解析器:
按照上述的配置,完成:
<code>
<beans> xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<component-scan>
<bean> class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property>
<property>
/<bean>
/<beans>
/<code>
第三步:剪貼 index.jsp 文件
在【WEB-INF】文件夾下新建一個【page】文件夾,並將【index.jsp】文件剪貼到裡面:
第四步:更新資源重啟服務器
訪問 localhost/hello 路徑,看到正確效果:
- 原理:
我們傳入的邏輯視圖名為 index ,再加上 “/WEB-INF/page/” 前綴和 “.jsp” 後綴,就能確定物理視圖的路徑了,這樣我們以後就可以將所有的視圖放入【page】文件夾下了!
- 注意:此時的配置僅是 dispatcher-servlet.xml 下的
控制器接收請求數據
使用控制器接收參數往往是 Spring MVC 開發業務邏輯的第一步,為探索 Spring MVC 的傳參方式,為此我們先來創建一個簡單的表單用於提交數據:
<code>
pageEncoding="UTF-8" import="java.util.*" isELIgnored="false"%>
<title>Spring MVC 傳參方式/<title>
/<code>
醜就醜點兒吧,我們就是來測試一下:
使用 Servlet 原生 API 實現:
我們很容易知道,表單會提交到 /param 這個目錄,我們先來使用 Servlet 原生的 API 來看看能不能獲取到數據:
<code>@RequestMapping("/param")
public ModelAndView getParam(HttpServletRequest request,
HttpServletResponse response) {
String userName = request.getParameter("userName");
String password = request.getParameter("password");
System.out.println(userName);
System.out.println(password);
return null;
}
/<code>
測試成功:
使用同名匹配規則
我們可以把方法定義的形參名字設置成和前臺傳入參數名一樣的方法,來獲取到數據(同名匹配規則):
<code>@RequestMapping("/param")
public ModelAndView getParam(String userName,
String password) {
System.out.println(userName);
System.out.println(password);
return null;
}
/<code>
測試成功:
- 問題: 這樣又會和前臺產生很強的耦合,這是我們不希望的
- 解決: 使用 @RequestParam("前臺參數名") 來注入:
- @RequestParam 註解細節:
該註解有三個變量:value、required、defaultvalue - value :指定 name 屬性的名稱是什麼,value 屬性都可以默認不寫
- required :是否必須要有該參數,可以設置為【true】或者【false】
- defaultvalue :設置默認值
使用模型傳參
- 要求: 前臺參數名字必須和模型中的字段名一樣
讓我們先來為我們的表單創建一個 User 模型:
<code>package pojo;
public class User {
String userName;
String password;
/* getter and setter */
}
/<code>
然後測試仍然成功:
中文亂碼問題
- 注意: 跟 Servlet 中的一樣,該方法只對 POST 方法有效(因為是直接處理的 request)
我們可以通過配置 Spring MVC 字符編碼過濾器來完成,在 web.xml 中添加:
<code><filter>
<filter-name>CharacterEncodingFilter/<filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter/<filter-class>
<init-param>
<param-name>encoding/<param-name>
<param-value>utf-8/<param-value>
/<init-param>
/<filter>
<filter-mapping>
<filter-name>CharacterEncodingFilter/<filter-name>
<url-pattern>/*/<url-pattern>
/<filter-mapping>
/<code>
控制器回顯數據
通過上面,我們知道了怎麼接受請求數據,並能解決 POST 亂碼的問題,那麼我們怎麼回顯數據呢?為此我們在【page】下創建一個【test2.jsp】:
<code>
pageEncoding="UTF-8" import="java.util.*" isELIgnored="false" %>
<title>Spring MVC 數據回顯/<title>回顯數據:${message}
/<code>
使用 Servlet 原生 API 來實現
我們先來測試一下 Servlet 原生的 API 是否能完成這個任務:
<code>@RequestMapping("/value")
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) {
request.setAttribute("message","成功!");
return new ModelAndView("test1");
}
/<code>
在瀏覽器地址欄中輸入:localhost/value 測試
使用 Spring MVC 所提供的 ModelAndView 對象
使用 Model 對象
在 Spring MVC 中,我們通常都是使用這樣的方式來綁定數據,
- 使用 @ModelAttribute 註解:
<code>@ModelAttribute
public void model(Model model) {
model.addAttribute("message", "註解成功");
}
@RequestMapping("/value")
public String handleRequest() {
return "test1";
}
/<code>
這樣寫就會在訪問控制器方法 handleRequest() 時,會首先調用 model() 方法將 message 添加進頁面參數中去,在視圖中可以直接調用,但是這樣寫會導致該控制器所有的方法都會首先調用 model() 方法,但同樣的也很方便,因為可以加入各種各樣的數據。
客戶端跳轉
前面不管是地址 /hello 跳轉到 index.jsp 還是 /test 跳轉到 test.jsp,這些都是服務端的跳轉,也就是 request.getRequestDispatcher("地址").forward(request, response);
那我們如何進行客戶端跳轉呢?我們繼續在 HelloController 中編寫:
<code>@RequestMapping("/hello")
public ModelAndView handleRequest(javax.servlet.http.HttpServletRequest httpServletRequest, javax.servlet.http.HttpServletResponse httpServletResponse) throws Exception {
ModelAndView mav = new ModelAndView("index");
mav.addObject("message", "Hello Spring MVC");
return mav;
}
@RequestMapping("/jump")
public ModelAndView jump() {
ModelAndView mav = new ModelAndView("redirect:/hello");
return mav;
}
/<code>
我們使用 redirect:/hello 就表示我們要跳轉到 /hello 這個路徑,我們重啟服務器,在地址欄中輸入:localhost/jump ,會自動跳轉到 /hello 路徑下:
也可以這樣用:
<code>@RequestMapping("/jump")
public String jump() {
return "redirect: ./hello";
}
/<code>
文件上傳
我們先來回顧一下傳統的文件上傳和下載:這裡
我們再來看一下在 Spring MVC 中如何實現文件的上傳和下載
- 注意: 需要先導入 commons-io-1.3.2.jar 和 commons-fileupload-1.2.1.jar 兩個包
第一步:配置上傳解析器
在 dispatcher-servlet.xml 中新增一句:
<code><bean>
/<code>
開啟對上傳功能的支持
第二步:編寫 JSP
文件名為 upload.jsp,仍創建在【page】下:
<code>
<title>測試文件上傳/<title>
/<code>
第三步:編寫控制器
在 Package【controller】下新建【UploadController】類:
<code>package controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.ModelAndView;
@Controller
public class UploadController {
@RequestMapping("/upload")
public void upload(@RequestParam("picture") MultipartFile picture) throws Exception {
System.out.println(picture.getOriginalFilename());
}
@RequestMapping("/test2")
public ModelAndView upload() {
return new ModelAndView("upload");
}
}
/<code>
第四步:測試
在瀏覽器地址欄中輸入:localhost/test2 ,選擇文件點擊上傳,測試成功:
可以關注我,私信回覆“資料”裡面會分享一些資深架構師錄製的視頻錄像:馬士兵JVM精講視頻,有Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化、分佈式架構等這些成為架構師必備的知識體系。還能領取免費的學習資源,目前受益良多,有人成功通過這些資料跳槽,面試成功,薪資提升1.5倍,2倍。
如何獲取?
轉發這篇文章,關注我,私信回覆“資料”即可獲取高清大綱,以上 spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化、分佈式架構
如何私信?
關注我後,在手機,點進我的主頁,主頁上方右上角有個私信,點擊私信,如何回覆關鍵字“資料”即可
閱讀更多 java架構java 的文章