Cookie與Session,書城訂單模組
尚硅谷JavaWeb筆記-09

Cookie

因為HTTP是無狀態(stateless)的,當我們想保存資訊就要用到Cookie

  • 伺服器通知客戶端保存鍵值對的技術
    • 白話:叫用戶保存住某個值
  • 應用舉例:記住用戶名
  • 由用戶的瀏覽器維護,視生命週期存在記憶體或硬碟中
  • 單個最多4 KB

使用

很簡單就不多說了

  • 在Java中是Cookie類
protected void doCookie(HttpServletRequest request, HttpServletResponse response) throws ServletException,
        IOException {
    // 設定編碼
    request.setCharacterEncoding("UTF-8");
    response.setContentType("text/html; charset=UTF-8");
    // 在Java造Cookie類物件
    Cookie cookie1 = new Cookie("key1", "value1");
    // 增,注意是response叫客戶端造cookie
    response.addCookie(cookie1);
    response.getWriter().write("cookie創建成功!");

    // 取
    Cookie[] cookies = request.getCookies();
    // 取來的是所有cookies構成的數組
    for (Cookie cookie2 : cookies) {
        response.getWriter().write(cookie2.getName() + "=" + cookie2.getValue() + "\n");
        // 可以做些if判斷拿出想要的
    }

    // 改
    Cookie cookie3 = new Cookie("key1", "value3");
    response.addCookie(cookie3); // 用同樣的"key1"去覆蓋"value1",變成"value3"

    cookie3.setValue("value4"); // 或是用setValue
    response.addCookie(cookie3); // 變成"value4"

    // 控制生命,秒數
    cookie3.setMaxAge(60);

    // 刪
    cookie3.setMaxAge(0);

    // 生命跟隨瀏覽器
    Cookie cookie5 = new Cookie("key5", "value5");
    cookie5.setMaxAge(-1);
    response.addCookie(cookie5);

    // 設定路徑
    Cookie cookie6 = new Cookie("key6", "value6");
    cookie6.setPath("/abc");
    cookie6.setMaxAge(-1);

    request.getRequestDispatcher("http://localhost:8080/book_war_exploded/cookie.html");

}

編碼問題

  • 由於是跟HTTP協議走的,value基本上只用英數字,不要用符號跟中文
  • 想要中文可以用BASE64編碼之類的

設定路徑

  • 起到一個過濾的效果,Cookie預設的Path是工程路徑
  • 如果手動給一個Cookie指定Path,等於說用戶收到命令創建這個Cookie後,伺服器再想訪問這個Cookie,需要是符合這個路徑的請求,否則伺服器見不到該Cookie
    • 類似說我在安親班造一個小孩,該小孩自帶的屬性是指定只有從某條路來的人可以訪問這個小孩
      • 可以起到簡單的隔絕,防止接錯小孩
      • 也能省資源,不然每次訪問都有一堆小孩傳來傳去
        • 因為Cookie的傳遞是附加在每個HTTP請求中

Session

  • Session就是會話,是用來維護伺服器與用戶端之間的關聯
  • 每一個用戶端有自己的一個Session
  • Session由伺服器維護 (Cookie由客戶端的瀏覽器維護)
  • 應用舉例:保存用戶登入後的訊息

使用

  • 在Java中Session是一個接口(HttpSession)

  • getSession()會判斷當前連線會話是否已經存在Session,若無則新造一個

  • 每一個Session都有自己唯一的ID

// 增,注意是request造
HttpSession session = request.getSession();

// 判斷是否是新造的
boolean aNew = session.isNew();

// 獲取id
String id = session.getId();

// 生命週期
session.setMaxInactiveInterval(60);

// 獲取預設的最大生命,此值由web容器決定,可能自己在web.xml改
int maxInactiveInterval = session.getMaxInactiveInterval();

// 立刻殺死
session.invalidate();

在Session域存放資訊

request.getSession().setAttribute("key1", "value1");
request.getSession().getAttribute("key1");

與Cookie的關係

  • 我們說Session是用來維持伺服器與客戶端之間的關聯,既然事情涉及到兩端,那肯定就不只一邊的事

  • 已知Session由伺服器維護,存在於伺服器的記憶體中,我一個伺服器可能有無數的使用者,那怎樣知道某個Session連給哪個使用者?

  • 一個Session會話是由客戶端發起,最初是客戶端發來的請求,所以是request.getSession()

  • 伺服器收到這個請求,會在伺服器的記憶體開闢一個Session的空間,這個空間有自己的SessionID

  • 同時,伺服器會要求客戶端創造一個Cookie,此Cookie的KEY固定是JSESSIONID,值就是對應的SessionID,存在客戶端的瀏覽器

  • 前面說過Cookie會附加在每個HTTP請求,因此會話就成立了,只要用戶端存在該Cookie就可以錨定到伺服器中的指定Session,直到某一方過期為止


書城項目

登出入

  • 後端驗證登入成功同時將user物件保存到Session域,前端用sessionScope取出用戶資料
  • 登出就刪掉session,轉發到首頁

驗證碼

  • maven
<dependency>
    <groupId>com.github.penggle</groupId>
    <artifactId>kaptcha</artifactId>
    <version>2.3.2</version>
</dependency>
  • 在web.xml中可以設定
    <servlet>
        <servlet-name>Kaptcha</servlet-name>
        <servlet-class>com.google.code.kaptcha.servlet.KaptchaServlet</servlet-class>
        <!-- 定義驗證碼樣式 -->
        <!-- 是否有邊框 -->
        <init-param>
            <param-name>kaptcha.border</param-name>
            <!-- 沒有邊框 no -->
            <param-value>yes</param-value>
        </init-param>
        <!-- 字體顏色 -->
        <init-param>
            <param-name>kaptcha.textproducer.font.color</param-name>
            <param-value>red</param-value>
        </init-param>
        <!-- 圖片寬度 -->
        <init-param>
            <param-name>kaptcha.image.width</param-name>
            <param-value>80</param-value>
        </init-param>
        <!-- 使用哪些字符生成驗證碼 -->
        <init-param>
            <param-name>kaptcha.textproducer.char.string</param-name>
            <param-value>12456890</param-value>
        </init-param>
        <!-- 圖片高度 -->
        <init-param>
            <param-name>kaptcha.image.height</param-name>
            <param-value>40</param-value>
        </init-param>
        <!-- 字體大小 -->
        <init-param>
            <param-name>kaptcha.textproducer.font.size</param-name>
            <param-value>35</param-value>
        </init-param>
        <init-param>
            <description>文字間隔</description>
            <param-name>kaptcha.textproducer.char.space</param-name>
            <param-value>3</param-value>
        </init-param>
        <!--去掉干擾線com.google.code.kaptcha.impl.NoNoise-->
        <!--加干擾線com.google.code.kaptcha.impl.DefaultNoise-->
        <init-param>
            <description>干擾實現類</description>
            <param-name>kaptcha.noise.impl</param-name>
            <param-value>
                com.google.code.kaptcha.impl.NoNoise
            </param-value>
        </init-param>
        <!-- 幹擾線的顏色 -->
        <init-param>
            <param-name>kaptcha.noise.color</param-name>
            <param-value>green</param-value>
        </init-param>
        <init-param>
            <description>
                圖片樣式:
                水紋com.google.code.kaptcha.impl.WaterRipple
                魚眼com.google.code.kaptcha.impl.FishEyeGimpy
                陰影com.google.code.kaptcha.impl.ShadowGimpy
            </description>
            <param-name>kaptcha.obscurificator.impl</param-name>
            <param-value>
                com.google.code.kaptcha.impl.ShadowGimpy
            </param-value>
        </init-param>
        <!-- 驗證碼字符個數 -->
        <init-param>
            <param-name>kaptcha.textproducer.char.length</param-name>
            <param-value>4</param-value>
        </init-param>
        <!-- 字體 -->
        <init-param>
            <param-name>kaptcha.textproducer.font.names</param-name>
            <param-value>Aria</param-value>
        </init-param>
        <init-param>
            <description>
                session中存放驗證碼的key鍵
            </description>
            <param-name>kaptcha.session.key</param-name>
            <param-value>KAPTCHA_SESSION_KEY</param-value>
        </init-param>
        <init-param>
            <description>
                The date the kaptcha is generated is put into the
                HttpSession. This is the key value for that item in the
                session.
            </description>
            <param-name>kaptcha.session.date</param-name>
            <param-value>KAPTCHA_SESSION_DATE</param-value>
        </init-param>

    </servlet>
    <servlet-mapping>
        <servlet-name>Kaptcha</servlet-name>
        <url-pattern>/captcha.jpg</url-pattern>
    </servlet-mapping>

前端

圖片網址綁定上面的<url-pattern>/captcha.jpg</url-pattern>

 <img id="code_img" src="${pageScope.basePath}captcha.jpg">
 ...
<script type="text/javascript">
    // 頁面加載完成之後
    $(function () {
        // 綁定刷新驗證碼
        $("#code_img").click(function () {
            this.src = "${pageScope.basePath}captcha.jpg?d=" + new Date();
        });
  • 點擊刷新功能,用時間當URL的變數,讓每次圖片都重新請求
    • 防止URL相同,瀏覽器把圖片快取起來不顯示新的

後端

  • 前面發的請求會同時把KEY存在Session中,取出值:
request.getSession().getAttribute(KAPTCHA_SESSION_KEY);
  • 在servlet中驗證表單內容的方法中,當成功取出值就把此Session刪掉,這樣一來如果用戶多次提交表單,又來到這個servlet的這個方法,第二次去取值就取不到,就會因為驗證失敗而走不到資料庫那步
  • 如此就實現了以驗證碼阻止重複提交表單的問題

訂單模組

個人的踩坑紀錄

關於轉址

之前一直沒搞清楚url前面要不要寫/,這次好好釐清它

前端部分

jsp或html

ContextPath是會根據部屬改變的,例如tomcat預設部屬的首頁為
http://localhost:8080/book_war_exploded/

這個url對應的實體資源就是webapp/資料夾下
如果沒有指定,會自動拿根路徑的index作為首頁顯示

對於前端來說,如果沒有指定過<base href="">

<body>
<a href="TestServlet">GOTEST</a>
    相當於當前網址+/TestServlet
    可能包含工程路徑與網頁資源所在資料夾
    這個頁面位於webapp/pages/
    http://localhost:8080/book_war_exploded/pages/TestServlet

<a href="/TestServlet">GOTEST2</a>
    相當於http://localhost:8080/TestServlet
    即追本溯源到最頂,用這個的話切記注意部屬時設定的工程路徑

<a href="../TestServlet">GOTEST3</a>
    IDE會認為你是想訪問一個檔案
    但瀏覽器卻可以正確的拼接
    http://localhost:8080/book_war_exploded/TestServlet
</body>

不管前端的js或後端,使用request.getContextPath()得到的就是工程路徑
/book_war_exploded
在前端用可以用js拚接出完整的路徑
<%
    String basePath = request.getScheme()
            + "://"
            + request.getServerName()
            + ":"
            + request.getServerPort()
            + request.getContextPath()
            + "/";
    pageContext.setAttribute("basePath", basePath);
%>
<base href="<%=basePath%>">

使用<%=basePath%>就等同於預設部屬的首頁,注意已經包含了最尾的/
<a href="<%=basePath%>TestServlet">測試跳轉</a>
  • 使用base統一管理,最好是不要+ “/“結尾,讓用法跟後端統一
  • 單獨用/要注意部屬時的設定

web.xml

<servlet-mapping>
    <servlet-name>Kaptcha</servlet-name>
    <url-pattern>/captcha.jpg</url-pattern>
    = http://localhost:8080/book_war_exploded/captcha.jpg
</servlet-mapping>
<servlet-mapping>
    <servlet-name>TestServlet</servlet-name>
    <url-pattern>/TestServlet</url-pattern>
    = http://localhost:8080/book_war_exploded/TestServlet
</servlet-mapping>
  • 綁定 <url-pattern>/</url-pattern>強制要以/開頭,否則直接報錯

後端部分

在後端使用轉發
response.sendRedirect("test.jsp");
request.getRequestDispatcher("test.jsp").forward(request, response);
不以`/`開頭就會被認為是相對路徑,即當下的url/

而通常webapp第一層只會放index,其他要訪問的頁面會分類在pages資料夾下,例如
response.sendRedirect("pages/abc.jsp");
request.getRequestDispatcher("/pages/abc.jsp").forward(request, response);

兩者區別在於
    response.sendRedirect是兩個請求,已經轉發了,網址會變化
    request.getRequestDispatcher網址不會變

如果如果在網址前加了/,寫成"/url"
對於response.sendRedirect 因為它是可以轉到外面去的,不寫齊全會出事,要手動加上request.getContextPath()
http://localhost:8080/

request.getRequestDispatcher加了/比如("/pages/abc.jsp")
則表示Web資源的根路徑

小結

  • 前端不以/開頭就會被認為是相對路徑,即當下的url

  • 後端都加上/

    • 並且response.sendRedirect先以request.getContextPath()開頭
  • 為了方便統一,前端用base先拚出工程路徑(但不要包含最尾的/),這樣整個專案就都是用/打頭

    • web.xml不加/根本無法啟動,現在都用註解或框架也不太用管

SQL問題

SQL語句

  • 寫daoImpl的時候,忘記是從資料庫中取東西的時候才要加別名使其與pojo符合
  • 我竟然在insert into的地方寫別名還除錯了半天,有夠蠢

Date類問題

cannot convert java.time.LocalDateTime to java.util.Date

把mysql-connector-java8.0.26換成8.0.21版,問題消失
因為26版會將datatime轉換成java.time.LocalDateTime
而21版轉換成java.util.Date
The server time zone value '����зǮɶ�' is unrecognized
21版連mysql 8不指定時間又會出錯

jdbc:mysql://localhost:3306/bookstore?useSSL=false&serverTimezone=GMT%2B8&characterEncoding=utf-8&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true

另外mysql預設時區要注意可能是UTC可以去資料庫看
兩邊都設定+8,就是GMT%2B8就不會出現時差問題

點擊事件

  • 用id綁定,結果只有第一個生效,測試時靈時不靈還找半天原因
  • 教訓:記住可能有多個物件時用class.綁定

Http503

  • 具體報錯是Servlet is currently unavailable
  • 原因在我複製一個jsp頁面,IDEA它JSTL導入怪怪的,寫了一個<c:if它提示我沒導入,於是alt+enter修復,誰知道一修就壞了
  • 來回折騰很久,Maven引了又引搞了半天,確定有這兩個
<dependency>
    <groupId>org.apache.taglibs</groupId>
    <artifactId>taglibs-standard-impl</artifactId>
    <version>1.2.5</version>
</dependency>
<dependency>
    <groupId>org.apache.taglibs</groupId>
    <artifactId>taglibs-standard-spec</artifactId>
    <version>1.2.5</version>
</dependency>
  • 最後發現index.jsp明明沒導入卻不報錯,很詭異
    • 在最頂加上<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>就解決了
  • 教訓:不要輕易使用IDEA的複製跟移動文件功能,寧願造一個全新頁面然後全選複製貼上

上次修改於 2022-01-09