本系列文章旨在記錄和總結自己在Java Web開發之路上的知識點、經驗、問題和思考,希望能幫助更多碼農和想成為碼農的人。
本文轉發自頭條号【普通的碼農】的文章,大家可以關注一下,直接在今日頭條的移動端APP中閱讀。因為平台不同,會出現有些格式、圖檔、連結無效方面的問題,我盡量保持一緻。
文章目錄
-
- 介紹
- 分層思維和單一職責思維
- 思路
- 配置Spring MVC和Spring IoC
- 展示層
- 模型層
- 控制器層
- 驗證
- 中文亂碼問題再現
- 總結
介紹
我們的租房網應用從剛開始的使用Servlet建立,到使用Filter來消除重複,再到使用JSP将展示層初步分離,再到使用JSTL和EL表達式徹底分離展示層,現在看起來已經很不錯了。
不過,還存在以下問題:
• 控制器層和模型層尚未分離;
• 如果要使用JSP以外的展示層技術,則還要做很多工作;
• 等等
而要解決這些問題,MVC架構和IoC架構是一個很好的選擇,Spring MVC和Spring IoC就是其中一個很流行的組合。
我們前面已經介紹過Spring MVC的核心原理、使用XML配置和整合IoC、使用Java配置和整合IoC、整合基于注解和Java的IoC、基于注解的控制器。
也介紹過Spring IoC的核心原理、基于XML生産和裝配Bean、使用注解自動裝配Bean、使用注解生産Bean、使用Java生産和裝配Bean
是以,我們繼續使用Spring MVC和Spring IoC來改造租房網應用吧。
分層思維和單一職責思維
分層思維和單一職責思維緊密聯系、密不可分。
分層思維,也可以叫做層級、級别等等;
單一職責思維,就是隻幹一件事;
它們都可謂是無處不在。
自然界中,螞蟻、蜜蜂這兩個團隊都是依據分層和單一職責來劃分的。
國家行政機關有省部廳司局等,組織機構也有類似的分層,這些都是管理範疇的。
公路網、鐵路網、電信網絡等都有主幹、次級主幹、分支等,這也是一種分層。
做事情的規劃也可以分層,百年規劃、五十年規劃、三十年規劃、十年規劃、五年規劃。
在我們IT行業中,作業系統中也有硬體層、核心層、應用層等;計算機網絡中有實體層、資料鍊路層、網絡層、傳輸層、會話層、表示層、應用層的OSI七層模型;而應用程式開發中,又具有更細的分層,比如MVC,甚至每一個類/接口、每一個方法/函數都可以視之為分層和單一職責的展現。
分層最大的好處是什麼?就是每一層隻關心和解決一件事,這實際上又是單一職責思維、高内聚的展現。而層與層之間的通信隻通過規範好的接口來實作,這就是低耦合的展現。
我們依據分層思維(具體到這就是MVC了),将JSP頁面用作展示層,即隻是取資料進行展示,而不關心資料是如何取得的。
而資料的查找、計算和存儲是由模型層來解決。
控制器層攔截請求、分派請求、串聯模型層和展示層進行請求的處理(轉換、綁定、校驗、調用模型層、轉發到展示層或其他元件)、生成響應并發送,我們采用Servlet來實作。具體到Spring MVC,又分為三個層級(DispatcherServlet、Controller、Handler)。我們把對一個請求的處理叫做一個action,是以規定送出給控制器層的請求必須以 .action 為字尾。
再加上JSP頁面是以 .jsp 為字尾的,它們都可以稱之為資源,隻不過JSP頁面是資料的展示,控制器層是資料的處理。即請求某個資源,實際上是指請求某些資料進行展示,或者請求某些資料進行處理。
當然,處理結果最後還是要進行展示,是以,最終傳回給浏覽器端的都是某種資料的某種展示。
思路
展示層往往是從最終使用者(可能是普通使用者,也可能是開發者使用者哦)使用的角度去分析,我們這裡主要是JSP頁面,仍然是登入頁面login.html、登入後會展示使用者感興趣的房源清單頁面houses.jsp、可以檢視某個房源詳情的頁面house-details.jsp、可以修改某個房源資訊的編輯頁面house-form.jsp。
這幾個JSP頁面因為之前已經改造成了純粹展示,是以代碼基本不用改變。不過,這幾個JSP頁面内部的連結需要修改為 .action 為字尾的,因為每個連結都需要請求服務端執行查找或處理某些資料的動作啊。
我們再來看看控制器層如何來設計。
Spring MVC的核心是DispatcherServlet,首先要在部署描述符web.xml中配置它。當然,必須配置它攔截所有 .action 為字尾的請求(上面設計的:))。而它又依賴于Spring IoC來配置和管理各種元件,比如控制器、視圖解析器等等,是以要提供Spring IoC的配置中繼資料(基于XML、注解、Java均可)。
那麼進一步看看需要幾個Controller和Handler。因為我們的租房網應用現在還比較簡單,是以把所有動作都放在一個Controller中就行了。
因為請求登入頁面仍然是不需要任何資料,是以不需要為它設計任何動作,使用靜态頁面就可以了。但是,送出登入請求的時候需要服務端驗證使用者名和密碼,這需要一個動作,就叫login.action吧。
登入之後,就需要服務端找到我感興趣的房源啊,這需要一個動作,就叫houses.action吧。
然後,我想看某個房源的詳細資訊,也需要服務端去查找那個房源的詳細資訊(為什麼不在找房源清單的同時把每一個房源的詳細資訊就找到呢?大家也可以想一想),這也需要一個動作,就叫house-details.action吧。
最後,我想編輯某個房源的詳細資訊,首先需要服務端把該房源的詳細資訊找到,并以表單的形式展示出來,這需要一個動作,就叫house-form.action吧;而修改完表單中該房源的詳細資訊之後,需要送出給服務端儲存起來,這也需要一個動作,就叫house-form.action吧。哎,等等,怎麼這兩個動作都叫house-form.action啊?!那該怎麼映射到Spring MVC中的Handler呢?這就需要将這兩個動作在HTTP層面使用HTTP方法來區分,前者使用GET方法将house-form.action映射到一個Handler;後者使用POST方法映射到另一個Handler。本質上還是兩個動作。
租房網目前隻用到了模拟的房源資料,連最基本的使用者資料都還沒有,其實是不太妥當的。不過,這用來做示範已經足夠了,何況我們可以慢慢的添加其他資料,一步一步來,這就是演化/疊代思維。
盡管如此,我們把模拟的房源資料也抽象出來,作為我們的模型層(按照目前流行的分層技術,又可以分為服務層和資料通路層,但我們暫時還沒有必要這麼分)。
根據上面分析,模型層中最主要的服務就是查找某個使用者感興趣的房源清單、根據房源ID查找該房源的詳細資訊。
下面,我們就一步一步來使用Spring MVC和Spring IoC來改造我們的租房網應用。
配置Spring MVC和Spring IoC
首先,我們要在我們的租房網應用中配置Spring MVC和Spring IoC。
配置的第一步,是把它們的JAR包先引入到工程結構中,可以參考這篇文章(實際上就是直接拷貝)。
第二步,是要配置Spring MVC的DispatcherServlet,可以使用XML(參考這篇文章),也可以使用Java(參考這篇文章),我這裡選擇的是使用XML的方式,web.xml的内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
id="WebApp_ID" version="3.0">
<display-name>house-renter</display-name>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.htm</welcome-file>
<welcome-file>index.jsp</welcome-file>
<welcome-file>default.html</welcome-file>
<welcome-file>default.htm</welcome-file>
<welcome-file>default.jsp</welcome-file>
</welcome-file-list>
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/dispatcher.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>*.action</url-pattern>
</servlet-mapping>
</web-app>
需要關注的是:
• DispatcherServlet的名稱可以随意取,不過最好取個好聽的名字;
• 整合Spring IoC,使用的也是基于XML的方式;
• DispatcherServlet攔截的請求是:
*.action
,可千萬不能寫成:
/*.action
,否則Tomcat會啟動不了。具體的映射規則可以參考Servlet規範(比如,servlet-4_0_FINAL.pdf)中的“Mapping Requests to Servlets”這一部分。
第三步是整合Spring IoC,使用的也是基于XML的方式。根據DispatcherServlet的配置,建立dispatcher.xml檔案:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="houserenter"/>
</beans>
對于Spring IoC的配置,我開啟了元件掃描,這樣我們就能夠使用Spring MVC的基于注解的控制器了。當然,我把所有元件都放在houserenter這個包和它的子包當中。
展示層
展示層的幾個HTML、JSP頁面的代碼隻需将連結和表單的URL改為 .action 字尾即可。
• login.html中表單的action屬性值改為:login.action;
• include.jsp不用修改;
• houses.jsp中連結的href屬性值改為:house-details.action(後面的參數仍然不變,下同);
• house-details.jsp中有兩個連結,編輯連結的href屬性值改為:house-form.action;回到清單連結的href屬性值改為:houses.action;
• house-form.jsp中表單的action屬性值改為:house-form.action,同時,method屬性值必須是:post,以免跟上面的編輯連結沖突!
改為 .action 字尾的意義對開發人員來說還是挺清晰的,就是要請求服務端執行某個動作啊,當然,這種 .action 字尾的請求都将由DispatcherServlet攔截并分派給各個Controller中的各個Handler。
每個JSP頁面中的資料(即綁定到該頁面的資料對象的名稱)仍然沿用原來的,比如房源清單頁面houses.jsp使用mockHouses(它是一個House對象的List);房源詳情頁面和房源編輯頁面使用target(它是一個House對象)。
模型層
因為控制器層依賴于模型層,是以我們還是先來實作模型層吧。
根據前面分析,我們隻需要實作一個房源服務即可,就叫HouseService吧,它有兩個方法,一個是查找使用者感興趣的房源清單,就叫findHousesInterested();一個是根據房源ID查找房源詳情的方法,就叫findHouseById()吧。
因為HouseService将為持續到來的請求提供服務,是以應該在租房網應用運作期間初始化一個執行個體之後常駐記憶體,這樣才能保證請求更加快速地得到處理。我們不就可以使用Spring IoC的@Service注解自動生成Bean并交給Spring IoC容器來管理了嗎?!
它使用模拟的房源資料,當然在該元件初始化的時候裝載它們即可。
當然,它仍然使用的是原來entity包中的House類。實際上,實體類也應該屬于模型層。而控制器層應該将傳輸對象轉換為實體對象,但我這裡都是使用的實體對象,因為它們是一緻的。
同時,為了展現分層思維,需要另外建立一個service包,将HouseService類放入此包中。
package houserenter.service;
import java.util.ArrayList;
import java.util.List;
import org.springframework.stereotype.Service;
import houserenter.entity.House;
@Service
public class HouseService {
private List<House> mockHouses;
public HouseService() {
mockHouses = new ArrayList<House>();
mockHouses.add(new House("1", "金科嘉苑3-2-1201", "詳細資訊"));
mockHouses.add(new House("2", "萬科橙9-1-501", "詳細資訊"));
}
public List<House> findHousesInterested(String userName) {
// 這裡查找該使用者感興趣的房源,省略,改為用模拟資料
return mockHouses;
}
public House findHouseById(String houseId) {
for (House house : mockHouses) {
if (houseId.equals(house.getId())) {
return house;
}
}
return null;
}
}
控制器層
根據前面的思路,我把所有動作都放在了一個控制器中,就叫HouseRenterController吧。當然,運作時需要生成它的Bean,我們仍然可以使用Spring IoC。因為我們已經開啟了元件掃描,是以隻要為該類加上注解@Controller即可。
然後,它依賴于模型層的HouseService,而HouseService也使用Spring IoC來自動生成Bean,是以可以使用@Autowired注解自動裝配它們。
然後,就是各個Handler的聲明,可以參考這篇文章。Handler的聲明采用@RequestMapping及其衍生注解如@GetMapping和@PostMapping等。
各個Handler與動作是一一對應的:
• 處理登入請求:@PostMapping("/login.action")
• 擷取房源清單:@GetMapping("/houses.action")
• 擷取房源詳情:@GetMapping("/house-details.action")
• 擷取房源編輯表單:@GetMapping("/house-form.action")
• 處理房源更新請求:@PostMapping("/house-form.action")
雖然,擷取房源編輯表單和處理房源更新請求的URL是一樣的,但是可以通過HTTP方法來比對這兩個不同的請求。
而各個Handler的實作,也可以參考這篇文章。我主要是使用ModelAndView這個類來作為Handler的傳回值;而請求中攜帶的參數直接使用字元串類型,是以可以省略@RequestParam注解,隻要保證參數名與請求中的參數名一緻即可。
ModelAndView可以綁定我們希望視圖層展示的資料對象,也是保證資料的名字與視圖中(這裡是JSP頁面)中使用的名字一緻即可。它使用的是addObject()等方法。
ModelAndView可以設定希望将請求轉發到哪一個視圖(這裡是JSP頁面)或動作,或者重定向到哪一個視圖(這裡是JSP頁面)或動作。它使用的是setViewName()等方法,重定向的視圖名需要帶一個字首:“redirect:”。
至于各個Handler具體的業務處理規則,還是相當簡單的。
package houserenter.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;
import houserenter.entity.House;
import houserenter.service.HouseService;
@Controller
public class HouseRenterController {
@Autowired
private HouseService houseService;
@GetMapping("/test.action")
@ResponseBody
public String test() {
return "hello";
}
@PostMapping("/login.action")
public ModelAndView postLogin(String userName, String password) {
//這裡需要驗證使用者是否已經注冊,省略
System.out.println("userName: " + userName + ", password: " + password);
ModelAndView mv = new ModelAndView();
//重定向到查找感興趣房源清單的動作
mv.setViewName("redirect:houses.action?userName=" + userName);
return mv;
}
@GetMapping("/houses.action")
public ModelAndView getHouses(String userName) {
//這裡需要驗證使用者是否登入,省略
ModelAndView mv = new ModelAndView();
//查找感興趣房源并綁定到相應JSP頁面,然後将請求轉發到該頁面
mv.addObject("mockHouses", houseService.findHousesInterested(userName));
mv.setViewName("houses.jsp?userName=" + userName);
return mv;
}
@GetMapping("/house-details.action")
public ModelAndView getHouseDetails(String userName, String houseId) {
// 這裡需要驗證使用者是否登入,省略
ModelAndView mv = new ModelAndView();
//查找房源詳情并綁定到相應JSP頁面,然後将請求轉發到該頁面
mv.addObject("target", houseService.findHouseById(houseId));
mv.setViewName("house-details.jsp?userName=" + userName);
return mv;
}
@GetMapping("/house-form.action")
public ModelAndView getHouseForm(String userName, String houseId) {
// 這裡需要驗證使用者是否登入,省略
ModelAndView mv = new ModelAndView();
//查找房源詳情并綁定到相應JSP頁面,然後将請求轉發到該頁面
mv.addObject("target", houseService.findHouseById(houseId));
mv.setViewName("house-form.jsp?userName=" + userName);
return mv;
}
@PostMapping("/house-form.action")
public ModelAndView postHouseForm(String userName, String houseId, String houseName, String houseDetail) {
// 這裡需要驗證使用者是否登入,省略
//更新指定房源的詳情
House target = houseService.findHouseById(houseId);
target.setName(houseName);
target.setDetail(houseDetail);
//将請求轉發到查找房源詳情的動作
ModelAndView mv = new ModelAndView();
mv.setViewName("redirect:house-details.action?userName=" + userName + "&houseId=" + houseId);
return mv;
}
}
在Handler方法的取名上,我也是根據HTTP方法加了get、post等字首。
需要注意的是,處理登入請求和處理房源更新請求這兩個Handler最後都采用了重定向。因為它們的目标資源都是GET動作,而這兩個請求都是POST請求,如果采用轉發,則這兩個請求直接交由目标資源的GET動作來處理,這就不比對了;重定向則是将這兩個請求先傳回響應給浏覽器,浏覽器再重新發起對目标資源的GET請求。
驗證
我們可以在Eclipse中釋出租房網應用到Tomcat伺服器,然後啟動Tomcat伺服器(可以參考這篇文章)。
我們打開浏覽器,輸入登入頁面的URL:
http://localhost:8080/house-renter/login.html
登入後即可看到房源清單頁面,URL也相應變為:
http://localhost:8080/house-renter/houses.action?userName=a
點選某個房源,可以打開該房源的詳細資訊頁面,然後可以進一步編輯該房源的詳細資訊,送出後,卻發現可惡的中文亂碼問題再次出現!
中文亂碼問題再現
此次出現中文亂碼問題還是在送出房源詳情請求的處理上,我們可以先通過日志列印的方式把送出的資料(房源名稱、詳情等)列印出來,看是否是亂碼。
果然,在這一步已經是亂碼,我們可以大膽猜想Spring MVC和Servlet一樣,對資料的編碼方式預設都是ISO-8859-1,而String類預設編碼和解碼是UTF-8。即Spring MVC收到的資料的二進制是ISO-8859-1編碼方案的二進制,而輸出的時候卻使用UTF-8去解碼。
是以,我們可以使用以下方式進行轉換:
new String(houseName.getBytes("iso-8859-1"), "utf-8")
結果還真是解決了亂碼問題。唯一的問題是把這個轉換代碼放到此處有點不太合适,我們是否可以像之前一樣使用Filter來解決這種通用次元的問題?
答案當然是可以的。不過這個Filter不需要我們來實作了,Spring MVC已經提供了這麼一個Filter,就是:
org.springframework.web.filter.CharacterEncodingFilter
我們隻需要在部署描述符web.xml中配置它即可:
<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>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>characterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
總結
這次我們真正的把Spring MVC和Spring IoC用到了看起來有用一點的項目上,從代碼上來看也還不賴(可以把原來的filter包和servlet包删除),層次還是比較清晰的。
• controller包
• entity包
• service包
• 展示層的HTML頁面和JSP頁面放在WebContent下
• 配置檔案放在WebContent/WEB-INF下
代碼也比較幹淨:
• 展示層和控制器層的資料都通過名字來綁定和解綁,Spring MVC幫我們做了,我們就無需使用getParameter()這樣的方法來自己轉換了。
• 一些通用次元的功能比如字元編碼,Spring MVC也提供了相應的Filter,我們隻要配置即可,無需在代碼中實作。
• 等等。
當然,也還存在很多改進之處:
• 異常處理還沒有;
• 使用者登入驗證也沒有;
• 像房源編輯的資料能否直接綁定到一個House對象中;
• 使用的還是模拟資料;
• 等等。
不過,以後我們再繼續介紹其他技術的時候,我們就可以一直使用租房網這個應用,慢慢改進和完善吧。