書城項目:Maven管理依賴、圖書模塊
尚硅谷JavaWeb筆記-08

Maven管理依賴

  • 傳統作法是建一個lib資料夾然後手動引入

  • 項目越多就越麻煩,打包的時候還要注意引用的有沒有包到

專案重構

把專案整個重構,可以先把src下做的那些頁面跟java檔複製出來

  • 第一層最好先造一個空的專案方便命名與分類

    • 如果直接拿第一層的project當同名的module用,我的IDEA偶爾會有奇怪的BUG,生出兩個同名的module
    • 後來知道原因了,就是模組根目錄下的.iml檔案裡面有重復的句子導致,備份後把它刪了讓IDEA重創一份就可
  • 我這邊放尚硅谷課程的練習,所以project就叫atguigu

image-20211230010435600

  • 因為是空的專案,進去之後IDEA馬上會叫你建一個Module,選javaEE,模板選到webapp

image-20211230010618550

  • 如果不想重造,在專案設定 > Facets

    • 右上設定部屬的描述檔案,就是那些web.xml之類
    • 右下決定資源根目錄,如果顯示紅字可以點它自動生成
  • 由於管理選的maven,造出來會自帶一個pom.xml,這個就是maven的設定檔,一個maven專案的標準目錄結構是這樣

image-20211230011401367

  • 點開pom.xml,裡面有一些描述,往下拉到<dependencies>標籤,在這邊告知項目要引用那些包

image-20211230011941709

  • 不像golang在終端輸入go get xxx就完事,maven導包需要寫標籤代碼,但還是能偷懶的,點開中央庫搜尋頁面 https://search.maven.org/#browse

image-20211230011602494

  • 查到想要找的包,對他一通按,點進去要引用的版本

image-20211230012351643

  • 看到右邊,貼心的代碼已經準備好了

image-20211230012403339

  • 愉快的一頓複製貼上之後,回到IDEA刷新maven,他會把這些從中央倉庫引用的jar包下載一份到(預設是我的文件中.m2資料夾)放好
    • 所以第一次可能會比較久一點

image-20211230012825123

  • 之後有引用都是同理,從此不需要手動把jar包放進lib目錄,部屬打包的時候它也會自動去準備配菜

路徑優化

  • 現在Servlet用註解方式引用,解放了web.xml
  • 前端頁面用共通的head.jsp指定了動態的basePath,跳轉主頁的引用直接留空就是指向basePath,例如:
<a href="">跳轉主頁</a>
<!-- 注意不是"/",如果用/會回到http://localhost:8080/ -->
  • 綜合以上,tomcat的部屬就不需要修改部屬的地址book_war_exploded/那些了

BeanUtils

  • 用於把前端拿到的參數map,透過反射直接塞進一個bean物件,但是要注意一個字都不能差,大小寫都要對
    • 他賦參數是透過set方法,前面講過bean物件一定要提供set,如果沒有set會失敗
String username=request.getParameter("username");
String password=request.getParameter("password");
String email=request.getParameter("email");
String code=request.getParameter("code");
User user = new User();
BeanUtils.populate(user,request.getParameterMap());
  • 比如這邊如果沒有特別需要使用參數去驗證啥的,String username那些都可以省略掉,反正收到ParameterMap就塞成一個bean,完事

圖書模塊

書城項目中有關書籍後台的部分,以MVC模式劃分

Model部分

相當於資料層+業務邏輯層

JavaBean

pojo與dao包與他們的Impl實現類

  1. 建立資料庫表、決定需要那些欄位、設定約束
  2. 建立pojo,也就是對應的bean
  3. 建立Dao接口(xxxDao),列出需要與資料庫互動的功能
  4. 實現接口(XxxDaoImpl extends BaseDao implements XxxDao),實際寫出sql語句、操作資料庫
  5. 測試,造一個private XxxDao xxxDao =new XxxDaoImpl()調用看看功能

重點筆記

  • book的bean
// 預設的圖片
private String imgPath="static/img/default.jpg";

// 在有參構造器與set加入一個判斷
// 如果上傳的是空則不給賦,讓它顯示預設的圖片路徑
        if (imgPath!=null && !"".equals(imgPath)){
            this.imgPath = imgPath;
        }
  • BookDaoImpl
關於書的路徑,pojo叫imgPath,資料庫裡叫img_path

之前寫的queryForOne方法,其中調用的queryRunner.query(conn,sql,new BeanHandler<T>(tClass),args)
這個BeanHandler將查到的資料填充bean的方法是用查詢到結果的欄位名去反射賦值
所以sql必須要起別名img_path imgPath
"select id, name, author, price, sales, stock, img_path imgPath from t_book where id=?"

測試的時候因為sql語句換行導致了一些bug,排查了好久,記住這個教訓
String sql="語句"最好不要換行

Service包

  • 建立Service接口(XxxService),列出前端需要的業務功能(如登入、增刪改查…等等)
    • 這邊方法的返回值最好是boolean或int,方便判斷是否成功
  • 實現接口XxxServiceImpl implements XxxService
    • 造一個它的Dao實例物件private XxxDao xxxDao =new XxxDaoImpl()
    • 用此Dao實例物件調用它的方法間接與資料庫互動,這步非常輕鬆只是套層皮而已

Controller部分

處理請求、轉發,即是在web包下的XxxServlet.java

  • 建立Servlet(XxxServlet),接收參數
    • 造一個它的Service實例物件private XxxService xxxService = new XxxServiceImpl()
    • 用此Service實例物件,配合接收到的請求與參數,完成業務邏輯,將結果用JSP或request.setAttribute("msg","錯誤代碼")之類的方法回傳給前端

doGet偷雞

  • 在doGet裡面放一個doPost,不管來哪種請求,反正都看action標籤來決定調用的方法
  • 由於是繼承BaseServlet中的方法,IDEA新造的XxxServlet預設是繼承httpServlet記得修改
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException,
        IOException {
    doPost(request,response);
}

亂碼問題

  • doPost收到的請求有可能亂碼,給他指定一下編碼就OK了
  • 看到有人說可以在web.xml加入characterEncodingFilter,先插眼
request.setCharacterEncoding("UTF-8")

表單重複提交

  • 當用戶提交了某請求,瀏覽器會記下最後一次請求的全部訊息,這時候按F5刷新就會再次提交
  • 比如新增圖書這個功能,如果存好書之後用request.getRequestDispatcher轉回去檢視圖書的頁面,那會視為一個請求,用戶手賤F5刷新就會重複上傳一次那本書
  • 所以改用response.sendRedirect讓它變成另一個新的請求,轉去檢視圖書頁面,這時候用戶最後一個請求是檢視圖書頁面,就不怕他F5了
    protected void add(HttpServletRequest request, HttpServletResponse response) throws Exception {
        // 封裝成book物件
        Book book = new Book();
        BeanUtils.populate(book, request.getParameterMap());

        // 存到資料庫
        bookService.addBook(book);

        // request.getRequestDispatcher 會是同一個請求

        // 轉發到另一個請求
        response.sendRedirect(request.getContextPath() + "/manager/bookServlet?action=list");
    }

刪除提示

  • 跳不出提示的在瀏覽器按F12看是不是JS載入的問題
    • 看下面console有沒有報錯找不到jquery-1.7.2.js
    • 或是無法識別$
	<script type="text/javascript">
		$(function () {
			// 給刪除的a標籤綁定單擊事件,用於刪除的確認提示操作
			$("a.deleteClass").click(function () {
				return confirm("你確定要刪除【" + $(this).parent().parent().find("td:first").text() + "】?");
				// return false// 阻止元素的預設行為===不提交請求
			});
		});
	</script>
...
	<td><a class="deleteClass" href="BookServlet?action=delete&id=${book.id}">刪除</a></td>

修改資料

  • 為了實現回顯功能,修改其實要拆成2步驟,第一個步驟是發起請求時,進到修改頁面,資料欄位要填上原先的值,所以需要先請求一次
    • 我原先自己做用網址參數把值都帶過去,欄位少還行,欄位多就顯得很蠢了
    • 前端用<a href="BookServlet?action=getBook&id=${book.id}">先把要修改的id傳過去
    • BookServlet先造一個getBook方法,用id從資料庫找到對應的書本資訊,用setAttribute把整個book再送回前端,同時轉址到修改頁面,這樣就完成了資料回顯
  • 接著要實現提交修改,這邊故意挖了一個坑,修改跟新增是用同一份表單,為了讓提交的請求可以分辨,有3種作法:
<!-- 方案1 在發起請求的網址後面指定參數method=xxx -->
<a href="pages/manager/book_edit.jsp?method=add">添加圖書</a>
<a href="BookServlet?action=getBook&id=${book.id}?method=update">修改</a>
<input type="hidden" name="action" value="${param.method}"/>

<!-- 方案2 提交表單的時候,判斷id是否存在,空的表示要新增 -->
<input type="hidden" name="action" value="${empty param.id?"add":"update"}"/>

<!-- 方案3 提交表單的時候,判斷請求域的book物件是否存在,空的表示要新增 -->
<input type="hidden" name="action" value="${empty requestScope.book?"add":"update"}"/>
  • 最後提交修改還有一個坑,表單裡面是不包含id的,而修改功能需要id到資料庫定位,所以再新增一行隱藏標籤,把id裝在requestScope.book再送回去

    • 這一個id還真是來來回回跑了好多趟
  • book_manager.jsp

<div id="main">
	<table>
		<tr>
			<td>名稱</td>
			<td>價格</td>
			<td>作者</td>
			<td>銷量</td>
			<td>庫存</td>
			<td colspan="2">操作</td>
		</tr>
		<c:forEach items="${requestScope.books}" var="book">
			<tr>
				<td>${book.name}</td>
				<td>${book.price}</td>
				<td>${book.author}</td>
				<td>${book.sales}</td>
				<td>${book.stock}</td>
				<td><a href="BookServlet?action=getBook&id=${book.id}">修改
				</a></td>
				<td><a class="deleteClass" href="BookServlet?action=delete&id=${book.id}">刪除</a></td>
			</tr>
		</c:forEach>
		<tr>
			<td></td>
			<td></td>
			<td></td>
			<td></td>
			<td></td>
			<td></td>
			<td><a href="pages/manager/book_edit.jsp">添加圖書</a></td>
		</tr>
	</table>
</div>
  • book_edit.jsp
<div id="main">
    <form action="BookServlet" method="post">
        <input type="hidden" name="action" value="${empty param.id?"add":"update"}"/>
        <input type="hidden" name="id" value="${requestScope.book.id}"/>
        <table>
            <tr>
                <td>名稱</td>
                <td>價格</td>
                <td>作者</td>
                <td>銷量</td>
                <td>庫存</td>
                <td colspan="2">操作</td>
            </tr>
            <tr>
                <td><input name="name" type="text" value="${requestScope.book.name}"/></td>
                <td><input name="price" type="text" value="${requestScope.book.price}"/></td>
                <td><input name="author" type="text" value="${requestScope.book.author}"/></td>
                <td><input name="sales" type="text" value="${requestScope.book.sales}"/></td>
                <td><input name="stock" type="text" value="${requestScope.book.stock}"/></td>
                <td><input type="submit" value="提交"/></td>
            </tr>
        </table>
    </form>
</div>

View部分

負責給人看得東西,即webapp資源目錄下的Jsp與html

  • 建構網頁畫面,排版、引用圖片、靜態包含等等
  • 將請求綁定到對應的Servlet,用EL表達式呈現資訊

SJTL遍歷資料

  • 記得要引包
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<tr>
	<td>名稱</td>
	<td>價格</td>
	<td>作者</td>
	<td>銷量</td>
	<td>庫存</td>
	<td colspan="2">操作</td>
</tr>
<c:forEach items="${requestScope.books}" var="book">
	<tr>
		<td>${book.name}</td>
		<td>${book.price}</td>
		<td>${book.author}</td>
		<td>${book.sales}</td>
		<td>${book.stock}</td>
		<td><a href="pages/manager/book_edit.jsp">修改</a></td>
		<td><a href="#">刪除</a></td>
	</tr>
</c:forEach>

分頁

一個不起眼的分頁顯示功能,要實現卻挺繁瑣的

思路

  • 造一個Page類,裝著這是第幾頁、一頁有幾筆、這一頁的內容有哪些(實際呈現的項目,比如這邊就是Book的List)、總共有幾頁(幾筆)
  • 前端請求看第幾頁就丟那個page過去,不指定就第1頁

這邊換用三層結構,專案結構目錄分類,由後往前走

Pojo

  • 造一個pojo為Page,泛型方便可以塞進Book類以外的,復用性++
public class Page<T> {
    public static final int PAGE_SIZE = 4;

    private int pageNo;
    private int pageTotal;
    private int pageSize = PAGE_SIZE;
    private int pageTotalCount;
    private String url; // 網址,這是轉發的伏筆
    private List<T> items;
    ...


public void setPageNo(int pageNo) {
    // 檢查邊界,比如有人手賤想查第-5頁或9999999頁
    if (pageNo < 1) {
        pageNo = 1;
    }
    if (pageNo > pageTotal) {
        pageNo = pageTotal;
    }
    this.pageNo = pageNo;
}

Dao

由於是用來分頁顯示Book的,寫在BookDao之下

  • SQL中查找的索引大多都是從1開始,但頁數例外,直覺上認為的"第一頁"是從0開始,但不急可以在service解決這個隱患
select * from t_book limit begin,pageSize
public List<Book> queryForPageItems(int begin, int pageSize) {
	String sql="select id, name, author, price, sales, stock, img_path imgPath from t_book limit ? , ?";
	return (List<Book>) queryForList(Book.class,sql,begin,pageSize);
}
  • SQL查到有關數字的結果都是long,直接強轉int會報錯,可以用intValue()方法
public int queryForPageTotalCount() {
    String sql = "select count(*) from t_book";
    long l = (long) queryForSingleValue(sql);
    return new Long(l).intValue();
}

Service

  • 到這邊要思路清晰,業務邏輯是:

    • 前端告知"想看第幾頁"與"一頁顯示幾筆"
    • 要去資料庫撈對應的資料與總頁數
    • 封裝成一個Page物件丟回給Servlet
  • 數字的加減乘除不要搞錯了,順便解決SQL分頁查找begin從0開始的問題

public Page<Book> page(int pageNo, int pageSize) {
    // 收到前端來的調用請求,準備一個Page物件來裝
    Page<Book> page = new Page<Book>();
    page.setPageSize(pageSize);

    // 先看資料庫總共有幾筆資料,算共會裝成幾頁
    int pageTotalCount = bookDao.queryForPageTotalCount();
    page.setPageTotalCount(pageTotalCount);

    // 求總頁碼
    int pageTotal = pageTotalCount / pageSize;
    if (pageTotalCount % pageSize > 0) {
        // 除不盡要加一頁,也包含不到一頁的情況
        pageTotal += 1;
    }
    page.setPageTotal(pageTotal);

    page.setPageNo(pageNo);
    // 當前頁面的資料
    int begin = (page.getPageNo() - 1) * pageSize;
    List<Book> items = bookDao.queryForPageItems(begin, pageSize);
    // 封裝
    page.setItems(items);

    return page;
}

Servlet

  • 處理轉發,由於圖書管理跟首頁的顯示商品都用到分頁顯示功能,而前端的網址控制上下頁是用這邊setAttribute的page物件中的屬性

  • 重新理一下,同一個請求內

    • 後端的:request.setAttribute("page", page)
    • 前端對應是:${requestScope.page},這個能用EL表達式取出的物件是由web容器建立
  • 我在這個page物件中封裝了url屬性,用戶發上下頁請求時就可以動態的綁定住本身來源的Servlet

    • 為了把前端分頁導航的代碼抽出所以這麼做
  • 於是我造了2個Servlet,一個負責圖書管理的,一個負責首頁商品的,他們的page方法設定的轉址都是指向自己那個Servlet

protected void page(HttpServletRequest request, HttpServletResponse response) throws ServletException,
        IOException {
    int pageNo = WebUtils.parseInt(request.getParameter("pageNo"), 1);
    int pageSize = WebUtils.parseInt(request.getParameter("pageSize"), Page.PAGE_SIZE);
    Page<Book> page = bookService.page(pageNo, pageSize);
    // 屬於誰的servlet就把網址設給誰
    page.setUrl("BookServlet?action=page");
    request.setAttribute("page", page);
    // 屬於誰的servlet就轉發給誰
    request.getRequestDispatcher("/pages/manager/book_manager.jsp").forward(request, response);
}

Pages

  • 先改兩個地方
    • 本來訪問Servlet中list改成page
    • 遍歷顯示圖書列表,books改成page.items
<a href="BookServlet?action=page">圖書管理</a>

<c:forEach items="${requestScope.page.items}" var="book">
  • 現在後臺管理應該可以成功顯示第一頁與4個項目了,接著處理跳轉分頁功能
<div id="page_nav">
	共${requestScope.page.pageTotalCount}條記錄
	<c:if test="${requestScope.page.pageNo>1}">
		<a href="BookServlet?action=page&pageNo=1">首頁</a>
		<a href="BookServlet?action=page&pageNo=${requestScope.page.pageNo-1}">上一頁</a>
	</c:if>
		第【${requestScope.page.pageNo}】頁
	<c:if test="${requestScope.page.pageNo<requestScope.page.pageTotal}">
		<a href="BookServlet?action=page&pageNo=${requestScope.page.pageNo+1}">下一頁</a>
		<a href="BookServlet?action=page&pageNo=${requestScope.page.pageTotal}">末頁</a>
	</c:if>
	共${requestScope.page.pageTotal}頁
	到第<input value="${param.pageNo}" name="pn" id="pn_input"/>頁
	<input id="searchPageBtn" type="button" value="確定">
	<script type="text/javascript">
		$(function (){
			$("#searchPageBtn").click(function (){
				var pageNo = $("#pn_input").val();
				var pageTotal=${requestScope.page.pageTotal};
				// 限制pageNo,比1小就是1,比pageTotal大就是pageTotal
				pageNo = Math.max(1, Math.min(pageNo, pageTotal));
				location.href="${pageScope.basePath}BookServlet?action=page&pageNo="+pageNo;
			});
		});
	</script>
</div>
  • 讓修改/刪除功能紀錄頁數,操作完後跳轉回同一頁
發起請求的地方加上參數
...&pageNo=${requestScope.page.pageNo}">刪改
...?pageNo=${requestScope.page.pageTotal}">添加圖書

提交修改表單的地方加傳隱藏參數
<input type="hidden" name="pageNo" value="${param.pageNo}"/>

最後修改servlet中轉發地址
response.sendRedirect(request.getContextPath() +
"/BookServlet?action=page&pageNo="+request.getParameter("pageNo"));

新增的則是要跳到最末頁,給他強制+1
int pageNo = WebUtils.parseInt(request.getParameter("pageNo"), 0);
pageNo+=1;
response.sendRedirect(request.getContextPath() + "/BookServlet?action=page&pageNo="+pageNo);

綁定首頁

  • 為了解決用戶一來訪問的首頁,還沒辦法透過servlet獲取資訊的問題
  • 直接讓工程目錄的index.jsp全部只有一行轉址
<jsp:forward page="/IndexServlet?action=page&pageNo=1"></jsp:forward>
  • 這個IndexServlet實現了顯示商品的page方法
protected void page(HttpServletRequest request, HttpServletResponse response) throws ServletException,
        IOException {
    int pageNo = WebUtils.parseInt(request.getParameter("pageNo"), 1);
    int pageSize = WebUtils.parseInt(request.getParameter("pageSize"), Page.PAGE_SIZE);
    Page<Book> page = bookService.page(pageNo, pageSize);
    page.setUrl("IndexServlet?action=page");
    request.setAttribute("page", page);
    request.getRequestDispatcher("/pages/client/index.jsp").forward(request, response);
    }
  • 他轉址到"/pages/client/index.jsp"這個才是真正的首頁,這邊遍歷顯示商品跟分頁導航功能

抽取分頁導航

把本來寫死的BookServlet?action=page換成從{requestScope.page.url}動態獲取
<a href="${requestScope.page.url}&pageNo=${requestScope.page.pageNo-1}">上一頁</a>

...
<%--靜態包含分頁導航--%>
<%@include file="/pages/common/page_nav.jsp"%>

價格區間篩選

  • 按照同樣的思路自己練習,沒問題

知識點

最大值

  • 我還傻傻地在那邊999999,想了一會int的最大是多少
  • 結果有現成的Integer.MAX_VALUE,哭阿
protected void pageByPrice(HttpServletRequest request, HttpServletResponse response) throws ServletException,
        IOException {
    int pageNo = WebUtils.parseInt(request.getParameter("pageNo"), 1);
    int pageSize = WebUtils.parseInt(request.getParameter("pageSize"), Page.PAGE_SIZE);
    int min = WebUtils.parseInt(request.getParameter("min"), 0);
    int max = WebUtils.parseInt(request.getParameter("max"),Integer.MAX_VALUE);
    if (min>max){
        min=0;
    }

排序

沒有想到SQL查還可以順便排序,傻了我

        String sql="select id, name, author, price, sales, stock, img_path imgPath from t_book " +
                "where price between ? and ? order by price limit ? , ?";

回顯

  • 這個沒啥問題
<form action="IndexServlet" method="get">
<input type="hidden" name="action" value="pageByPrice"/>
價格:<input id="min" type="text" name="min" value="${param.min}"> 元 -
<input id="max" type="text" name="max" value="${param.max}"> 元
<input type="submit" value="查詢" />
</form>

分頁導航

  • 我是直接拼接了
page.setUrl("IndexServlet?action=pageByPrice&min=" + min + "&max=" + max);
  • 這樣確實會有一個問題,就是當用戶手賤空值點搜尋,這時候min會被預設為0且max=2147483647,雖然顯示的東西一樣,但回顯那會多出超大的數字很突兀
  • 修正
int min = WebUtils.parseInt(request.getParameter("min"), 0);
int max = WebUtils.parseInt(request.getParameter("max"), Integer.MAX_VALUE);
if (min > max) {
    min = 0;
}
Page<Book> page = bookService.pageByPrice(pageNo, pageSize, min, max);
StringBuilder sb = new StringBuilder("IndexServlet?action=pageByPrice");
// 如果有用戶從前端填入的參數
if (request.getParameter("min")!=null){
    sb.append("&min=").append(min);
}
if (request.getParameter("max")!=null){
    sb.append("&max=").append(request.getParameter("max"));
}
page.setUrl(sb.toString());
  • 因為我在上面判斷過一次,就是用戶搜尋的min>max的話,我就把min=0,感覺這樣是最人性的
  • 其實我想了一下,min可以拼接,因為用戶手賤空值去按搜尋,結果顯示最小從0開始很合理,只要max獲取用戶輸入就好

上次修改於 2022-01-08