Servlet實戰(2)
在實戰1中自己對Servlet的使用已經慢慢的熟悉了很多,而且自己在學習中有對比、有發散,也學到了很多東西。在實戰2中,我拋棄了Servlet3.0之前的方式,開始全面使用Servlet3.0的規則,即使用註解方式。注意Servlet3.0中Filter上的註解無法排序問題,排序的話可以根據Filter名進行排序A-Z
Servlet Cookie處理
在閱讀了大量的cookie與session的文章後,在回過頭來看以下菜鳥教程的cookie介紹,就顯得簡單許多了,cookie並不是在伺服器端建立的,伺服器端只是向客戶端傳送建立指令(Set-Cookie),將要建立的cookie放在請求頭中,傳送(多個cookie,則是傳送一組cookie)到客戶端(一般是瀏覽器),瀏覽器解析後建立cookie,如果這些cookie聲明瞭存活時間,則會被寫入客戶端文字檔案中,如果沒有宣告則僅僅存在於一次對話中,當客戶端瀏覽器關閉cookie失效。
如果要想客戶端存入中文或者獲取客戶端存入的中文,都需要進行處理。
String str1 = java.net.URLEncoder.encode("中文","UTF-8"); // 轉碼 String str2 = java.net.URLDecoder.decode("%E4%B8%AD%E6%96%87","UTF-8"); // 解碼
總結:cookie的name、value、path、domain都不可以使用中文!對於name不能使用TSPECIALS宣告的字元。如果需要使用中文就像上一節那樣進行轉碼。
其實我在想,在
package servlet; import java.io.IOException; import java.io.PrintWriter; import java.net.URLEncoder; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @WebServlet("/SetCookieServlet") public class SetCookieServlet extends HttpServlet{ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); resp.setContentType("text/html;charset=UTF-8"); PrintWriter out = resp.getWriter(); String name = req.getParameter("name"); String site = req.getParameter("site"); Cookie nameCookie = new Cookie("name", name);//URLEncoder.encode(name, "UTF-8") Cookie siteCookie = new Cookie("site", site); resp.addCookie(nameCookie); resp.addCookie(siteCookie); out.println(name); out.println(site); } }
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Insert title here</title> </head> <style> div { width:200px; height:200px; border:1px solid black; margin:10px; } </style> <body> <div> <!-- 這裡的action就是web.xml裡配置的url-pattern,不加/,如果是註解,則直接些Servlet名 --> <form action="SetCookieServlet" method="post"> 網站名:<input type="text" name="name"><br> 站點:<input type="text" name="site"> <input type="submit"> </form> </div> </body> </html>
啟動專案,在網站名中輸入中文,會發現後臺報錯了,跟蹤後發現在第28行丟擲了異常。
java.lang.IllegalArgumentException: Control character in cookie value or attribute. at org.apache.tomcat.util.http.LegacyCookieProcessor.needsQuotes(LegacyCookieProcessor.java:412) at org.apache.tomcat.util.http.LegacyCookieProcessor.generateHeader(LegacyCookieProcessor.java:284) at org.apache.catalina.connector.Response.generateCookieString(Response.java:940) at org.apache.catalina.connector.Response.addCookie(Response.java:888) ...
我們將異常資訊定位一下,從上面列印的堆疊資訊可以看出,異常是在response.addCookie導致的,繼續向上定位:
// org.apache.catalina.connector.Response @Override public void addCookie(final Cookie cookie) { // Ignore any call from an included servlet if (included || isCommitted()) { return; } cookies.add(cookie); String header = generateCookieString(cookie); addHeader("Set-Cookie", header, getContext().getCookieProcessor().getCharset()); } public String generateCookieString(final Cookie cookie) { // Web application code can receive a IllegalArgumentException // from the generateHeader() invocation if (SecurityUtil.isPackageProtectionEnabled()) { return AccessController.doPrivileged(new PrivilegedAction<String>() { @Override public String run(){ return getContext().getCookieProcessor().generateHeader(cookie); } }); } else { return getContext().getCookieProcessor().generateHeader(cookie); } }
在response的addCookie方法裡可以看出使用了流的方式對cookie進行了處理,繼續跟蹤到generateCookieString(cookie)方法在到generateHeader:
// org.apache.tomcat.util.http.LegacyCookieProcessor public String generateHeader(Cookie cookie) { int version = cookie.getVersion(); String value = cookie.getValue(); String path = cookie.getPath(); String domain = cookie.getDomain(); String comment = cookie.getComment(); if (version == 0) { // Check for the things that require a v1 cookie if (needsQuotes(value, 0) || comment != null || needsQuotes(path, 0) || needsQuotes(domain, 0)) { version = 1; } } ... } private boolean needsQuotes(String value, int version) { if (value == null) { return false; } int i = 0; int len = value.length(); if (alreadyQuoted(value)) { i++; len--; } for (; i < len; i++) { char c = value.charAt(i); if ((c < 0x20 && c != '\t') || c >= 0x7f) { throw new IllegalArgumentException( "Control character in cookie value or attribute."); } if (version == 0 && !allowedWithoutQuotes.get(c) || version == 1 && isHttpSeparator(c)) { return true; } } return false; }
閱讀needsQuotes原始碼可以發現,這是一個對value引數進行中文校驗的函式,如果value引數是中文就會丟擲IllegalArgumentException,在回到generateHeader方法,查詢對needsQuotes方法的呼叫,就會發現在generateHeader方法,對cookie的value、path、domain都進行了中文校驗。
這我們就明白了,在cookie的value、path、domain都是不能使用中文的,那cookie的name能不能使用中文呢?我們來看以下cookie的建構函式就知道了:
// javax.servlet.http.Cookie public Cookie(String name, String value) { if (name == null || name.length() == 0) { throw new IllegalArgumentException( lStrings.getString("err.cookie_name_blank")); } if (!isToken(name) || name.equalsIgnoreCase("Comment") || // rfc2019 name.equalsIgnoreCase("Discard") || // 2019++ name.equalsIgnoreCase("Domain") || name.equalsIgnoreCase("Expires") || // (old cookies) name.equalsIgnoreCase("Max-Age") || // rfc2019 name.equalsIgnoreCase("Path") || name.equalsIgnoreCase("Secure") || name.equalsIgnoreCase("Version") || name.startsWith("$")) { String errMsg = lStrings.getString("err.cookie_name_is_token"); Object[] errArgs = new Object[1]; errArgs[0] = name; errMsg = MessageFormat.format(errMsg, errArgs); throw new IllegalArgumentException(errMsg); } this.name = name; this.value = value; } private boolean isToken(String value) { int len = value.length(); for (int i = 0; i < len; i++) { char c = value.charAt(i); if (c < 0x20 || c >= 0x7f || TSPECIALS.indexOf(c) != -1) { return false; } } return true; }
在上面的isToken方法裡我想你應該看到了有一段if判斷和needsQuotes方法是差不多的,也是進行中文校驗,校驗不通過也會丟擲異常。除此之外在isToken方法裡還有TSPECIALS這個final 常量需要注意:
static { if (Boolean.valueOf(System.getProperty("org.glassfish.web.rfc2109_cookie_names_enforced", "true"))) { TSPECIALS = "/()<>@,;:\\\"[]?={} \t"; } else { TSPECIALS = ",; "; } }
它規定了cookie的name值不能包含TSPECIALS所宣告的這些字元。菜鳥教程這一點是有誤的,在此測試記錄。
總結:cookie的name、value、path、domain都不可以使用中文!對於name不能使用TSPECIALS宣告的字元。如果需要使用中文就上上一節那樣進行轉碼。
@WebServlet("/SetCookieServlet") public class SetCookieServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); resp.setContentType("text/html;charset=UTF-8"); PrintWriter out = resp.getWriter(); // 轉碼 String name = URLEncoder.encode(req.getParameter("name"), "UTF-8"); String site = URLEncoder.encode(req.getParameter("site"), "UTF-8"); Cookie nameCookie = new Cookie("name", name); Cookie siteCookie = new Cookie("site", site); // 設定過期時間,以秒為單位,下面是一個有效期為1小時的cookie nameCookie.setMaxAge(60*60*1); resp.addCookie(nameCookie); resp.addCookie(siteCookie); } }
1 package servlet; 2 3 import java.io.IOException; 4 import java.io.PrintWriter; 5 import java.net.URLDecoder; 6 import javax.servlet.ServletException; 7 import javax.servlet.annotation.WebServlet; 8 import javax.servlet.http.Cookie; 9 import javax.servlet.http.HttpServlet; 10 import javax.servlet.http.HttpServletRequest; 11 import javax.servlet.http.HttpServletResponse; 12 13 @WebServlet("/GetCookieServlet") 14 public class getCookieServlet extends HttpServlet { 15 16 protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { 17 req.setCharacterEncoding("UTF-8"); 18 resp.setContentType("text/html;charset=UTF-8"); 19 PrintWriter out = resp.getWriter(); 20 21 Cookie[] cookies = req.getCookies(); 22 if(cookies != null) { 23 for (Cookie cookie : cookies) { 24 out.println(URLDecoder.decode(cookie.getName(), "UTF-8") + ":" + URLDecoder.decode(cookie.getValue(), "UTF-8") + ",expire:" + cookie.getMaxAge()); 25 } 26 }else { 27 out.println("請先設定cookie"); 28 } 29 30 } 31 32 }
當關閉瀏覽器後siteCookie為過期,但是nameCookie依然存在,因為我們設定了1個小時的存活時間。
對cookie的更多操作,可參見【Session Cookie筆記】
由於HTTP是一種"無狀態"協議,這就意味著伺服器端不會保留之前客戶端請求的任何記錄。如果是來自同一使用者短時間內的相同請求,伺服器也無法判斷是否是同一使用者的操作,那到底該怎麼樣才能識別使用者和保持使用者資訊呢?這就是要說的session。
一個Web伺服器可以分配一個唯一的session會話ID作為每個Web客戶端的cookie,對於客戶端的後續請求可以使用接收到的cookie來識別。
這種方式的實現要分析一下,如果客戶端請求的是一個JSP檔案:
<%@ page language="java" contentType="text/html; charset=ISO-8859-1" pageEncoding="ISO-8859-1"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"> <title>Insert title here</title> </head> <body> Hello </body> </html>
啟動專案,直接在客戶端訪問index.jsp,我們知道,對於這個JSP檔案我們並沒有在web.xml裡做任何load的配置,也就是說,只有在第一次訪問這個index.jsp時才會編譯它,編譯請求這個index_jsp.java的service方法,返回結果,你可以在瀏覽器端看到"Hello"。
現在開啟EditThisCookie外掛,即可看到有一個name為JSESSIONID的cookie,其值為一段碼。如果你的客戶端禁止了cookie的話,就不會出現JSESSIONID這個cookie。
package servlet; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; @WebServlet("/LoginServlet") public class LoginServlet extends HttpServlet{ @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 設定post請求編碼問題 req.setCharacterEncoding("UTF-8"); // 設定返回頁面的格式和編碼 resp.setContentType("text/html; charset=UTF-8"); HttpSession session = req.getSession(); session.setMaxInactiveInterval(60*60*1); if(req.getCookies() != null) { System.out.println("瀏覽器端可以使用cookie!"); }else { System.out.println("瀏覽器端不能使用cookie!"); } String username = req.getParameter("username"); String pwd = req.getParameter("pwd"); // 校驗使用者名稱密碼 if(username.equals("毛毛") && pwd.equals("123456")) { System.out.println("登陸成功"); req.setAttribute("username", username); req.getRequestDispatcher("/welcome.jsp").forward(req, resp); }else { System.out.println("登陸失敗,返回重新登陸!"); req.getRequestDispatcher("/index.jsp").forward(req, resp); } } }
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Insert title here</title> </head> <body> Hello! <%=request.getAttribute("username") %> </body> </html>
啟動專案,上面執行的程式碼,我在測,我也在想,上面的程式碼並不能滿足讓伺服器記住我的功能,雖然伺服器關閉後,我的JSESSIONID被寫入了.metadata\.plugins\org.eclipse.wst.server.core\tmp0\work\Catalina\localhost\Servlet目錄下的SESSIONS.ser裡,但是我再次開啟頁面訪問還是要輸入使用者名稱和密碼,哦哦,好像儲存使用者名稱和密碼這是cookie要乾的事情,使用者在第一次登陸後,建立2個長久的cookie即使用者名稱和密碼,在建立一個短暫的cookie(session),這個cookie要和伺服器端session存活時間保持一致才行(session的存活時間可以被重新整理,但是cookie不行,所以每次重新整理session時,cookie也要手動重新整理)。但有個問題就是當客戶端關閉在次從開,訪問首頁,伺服器根據客戶端的JSESSIONID在次訪問伺服器,伺服器認識不認識這個JSESSIONID呢?我們來測試一下:
package servlet; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; @WebServlet("/SessionAtBrower") public class SessionAtBrower extends HelloServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { HttpSession session = req.getSession(); System.out.println(session); Cookie scookie = new Cookie("JSESSIONID", session.getId()); scookie.setMaxAge(60 * 60 * 1); resp.addCookie(scookie); } }
在這個SessionAtBrower.java裡我們將第一次請求時建立的JSESSIONID寫入了cookie中,而且設定了這個cookie的過期時間為一個小時後,訪問得到JSESSIONID=4EF6EE406AA8EA47A6AC7C036AE68B76。猜想,我現在關閉chrome瀏覽器,再次開啟它訪問這個地址,要麼再次獲取的cookie裡的和上面這個相等,相等就說明了一個問題:伺服器端是先根據客戶端中cookie為JSESSIONID新建或獲取已存在的session,如果cookie中JSESSIONID存在,就去伺服器端找這個session.id = JSESSIONID的session,找到了(代表存活)將這個session返回,沒找到就新建一個session並將該session.id = JSESSIONID將這個新建的session返回。不相等就伺服器端使用session並不能儲存會話!!!
測試發現,兩次瀏覽器端cookie裡的JSESSIONID是一致的,都是4EF6EE406AA8EA47A6AC7C036AE68B76,而且伺服器端列印的session物件也是一樣的!
總結:伺服器端是先根據客戶端中cookie為JSESSIONID新建或獲取已存在的session。
1.如果cookie中JSESSIONID不存在:第一次訪問伺服器如果呼叫了request.getSession()則會在伺服器端生成一個session然後將這個session物件的id傳送的瀏覽器的cookei中(JSESSIONID=session.id)。
2.如果cookie中JESSIONID存在:就去伺服器端找這個session.id = JESSIONID的session,找到了(代表存活)將這個session返回,沒找到就新建一個session並將該session.id = JESSIONID將這個新建的session返回。
經過上面的測試發現當兩次使用相同瀏覽器去訪問伺服器時(第一次關閉瀏覽器後第二次在啟動訪問),伺服器時認是你的,認識你就好辦多了,這樣我們寫登陸這塊首先在第一次訪問時要往客戶端儲存3個cookie,username,pwd,JSESSIONID,有了JSESSIONID我在關閉開啟瀏覽器訪問時,就用這個JESSIONID去校驗一下子,這樣校驗,還是request.getSession()返回一個session然後呼叫這個session.isNew()判斷這個session是不是新建的,如果是說明之前的session過期了,過期了的話就重新登陸校驗一下,沒過期就不在登陸校驗了,直接變為已登入。
@WebServlet("/SessionAtBrower") public class SessionAtBrower extends HelloServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { HttpSession session = req.getSession(); if(session.isNew()) { System.out.println("上次訪問的session過期啦,要再次驗證一下使用者名稱密碼,成功後轉到首頁"); }else { System.out.println("重定向到首頁"); } System.out.println(session); Cookie scookie = new Cookie("JSESSIONID", session.getId()); scookie.setMaxAge(60*60*1); resp.addCookie(scookie); } }
在測一測,第一次訪問,關閉瀏覽器,再次開啟瀏覽器訪問得到結果如下:
資訊: 攔截的地址:http://localhost:8080/Servlet/SessionAtBrower 上次訪問的session過期啦,要再次驗證一下使用者名稱密碼,成功後轉到首頁 org.apache.catalina.session.StandardSessionFacade@1f9b3ca8 五月 08, 2019 8:05:47 下午 filter.LoggerFilter doFilter 資訊: 攔截的地址:http://localhost:8080/Servlet/SessionAtBrower 重定向到首頁 org.apache.catalina.session.StandardSessionFacade@1f9b3ca8
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Insert title here</title> </head> <body> <%@ page import="java.net.*" %> <% if(session.isNew()) { %> <form action="LoginServlet" method="post"> 使用者名稱:<input type="text" name="username" /> 密碼:<input type="password" name="pwd" /> <input type="submit" /> </form> <% }else { System.out.println("重定向到首頁"); Cookie[] cookies = request.getCookies(); String username = ""; if(cookies != null) { for (Cookie cookie : cookies) { if(URLDecoder.decode(cookie.getName(), "UTF-8").equals("username")){ username = URLDecoder.decode(cookie.getValue(), "UTF-8"); } } } request.setAttribute("username", username); request.getRequestDispatcher("/welcome.jsp").forward(request, response); } %> </body> </html>
如果是初次登陸,直接訪問http://localhost:8080/Servlet/則tomcat會自動找到我們專案下的index檔案,解析JSP後這個session肯定是new的,因為它之前沒有登陸過,然後就跳轉到登陸頁面。當它點選登陸提交後,就是我們LoginServlet要乾的事了:
package servlet; import java.io.IOException; import java.net.URLDecoder; import java.net.URLEncoder; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; @WebServlet("/LoginServlet") public class LoginServlet extends HttpServlet{ @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 設定post請求編碼問題 req.setCharacterEncoding("UTF-8"); // 設定返回頁面的格式和編碼 resp.setContentType("text/html; charset=UTF-8"); HttpSession session = req.getSession(); System.out.println(session.getId()); String username = req.getParameter("username"); String pwd = req.getParameter("pwd"); // 校驗使用者名稱密碼 if(username.equals("毛毛") && pwd.equals("123456")) { System.out.println("登陸成功"); req.setAttribute("username", username); // 第一次登陸,將cookie儲存在瀏覽器 Cookie[] cookies = req.getCookies(); if(cookies != null) { for (Cookie cookie : cookies) { if(URLDecoder.decode(cookie.getName(), "UTF-8").equals("JSESSIONID")) { cookie.setMaxAge(60*60*1); resp.addCookie(cookie); } } } Cookie usernameC = new Cookie("username",URLEncoder.encode(username, "UTF-8")); Cookie pwdC = new Cookie("pwd",URLEncoder.encode(pwd, "UTF-8")); usernameC.setMaxAge(60*60*1); pwdC.setMaxAge(60*60*1); resp.addCookie(usernameC); resp.addCookie(pwdC); req.getRequestDispatcher("/welcome.jsp").forward(req, resp); }else { System.out.println("登陸失敗,返回重新登陸!"); req.getRequestDispatcher("/index.jsp").forward(req, resp); } } }
登陸成功,將使用者名稱,密碼重寫到cookie,並將JSESSIONID的過期時間更新,將請求轉發到welcome.jsp,這個檔案還是我們之前的那個,沒有任何改動。登陸成功後頁面會顯示:Hello 毛毛
現在我們關閉瀏覽器,再次開啟,再次訪問http://localhost:8080/Servlet/,你會在頁面上看到:Hello 毛毛。這段處理邏輯在index.jsp裡,如果session不是新的,就說明剛登陸過,那獲取cookie裡的資訊,直接顯示出來即可。
你可以在每一個URL末尾追加一個額外的標識session會話,伺服器會把該session會話識別符號與已儲存的有關session會話的資料相關聯【菜鳥教程】(補充:要現有這個session才行)
session一般是要和cookie一起工作的,如果瀏覽器不支援cookie怎麼辦?當我嘗試了將chrome瀏覽器的cookie禁用後,在執行上面的例項,在瀏覽器端就讀取不到任何的cookie了。
URL重寫和直接使用cookie類似,使用cookie的方式是瀏覽器幫我們加的(當且僅當瀏覽器支援cookie),如果瀏覽器不知道cookie,我們可以手動加在URL上,格式如下:
"/ProjectName/Servlet;JSESSIONID=***;key1=value1?id=10"
一開始我使用的還是之前用過的getCookieServlet.java
package servlet; import java.io.IOException; import java.io.PrintWriter; import java.net.URLDecoder; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; @WebServlet("/GetCookieServlet") public class getCookieServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); resp.setContentType("text/html;charset=UTF-8"); System.out.println(req.getRequestURL()); PrintWriter out = resp.getWriter(); System.out.println(req.getSession().getId()); Cookie[] cookies = req.getCookies(); if(cookies != null) { for (Cookie cookie : cookies) { out.println(URLDecoder.decode(cookie.getName(), "UTF-8") + ":" + URLDecoder.decode(cookie.getValue(), "UTF-8") + ",expire:" + cookie.getMaxAge()); } }else { out.println("請先設定cookie"); } } }
http://localhost:8080/Servlet/GetCookieServlet;JSESSIONID=4EF6EE406AA8EA47A6AC7C036AE68B76;
結果並不像菜鳥教程裡說的那樣,伺服器並沒有按照我傳入的這個sessionid幫我關聯一個session,而是建立了一個新的session。原因可想而知,就是我理解錯誤,菜鳥教程上面說的那句話,還有一點就是它關聯的是一個已存在的session,首先我是第一次訪問,伺服器端肯定是沒有我之前的任何session的,所以即使我傳入了一個新的sessionid,它也關聯不到某個seesion.id==sessionid的session,所以,我上面這樣測試是不對的。
按照思路,是要現有一個session,然後我拿著這個session的id在URL地址裡訪問,才能被關聯,那就簡單了。我們先寫一個index.jsp,讓它幫我們生成一個sessionid
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Insert title here</title> </head> <body> <form action="<%=response.encodeURL("welcome.jsp") %>" method="post"> 使用者名稱:<input type="text" name="username" /> <input type="submit" /> </form> </body> </html>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Insert title here</title> </head> <body> Hello!<%=request.getParameter("username") %> <a href="<%=response.encodeURL("index.jsp") %>" >重新登陸</a> <a href="<%=response.encodeURL("logout.jsp") %>" >登出</a> </body> </html>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Insert title here</title> </head> <body> <% session.invalidate(); %> 登出成功! <a href="<%=response.encodeURL("index.jsp") %>" >返回登陸</a> </body> </html>
用這三個靜態頁面即可測試,首先在載入index.jsp的時候,隱式物件session已經存在,encodURL方法的作用是將session.id包含在url地址中,並進行編碼,也就是說在index.jsp被生成時表單的action已經確定
在表單提交到welcome.jsp時,session.id=F4BD08A13DA7BE1B3E7B8B6C16238A84的session已經存在,所以url位址列中的sessionid還是F4BD0...,當在welcome.jsp中點選重新登陸或登出時還是在當前會話中,再次重新登陸(沒登出)session沒過期時還是當前這個session,如果session過期了會生成新的sessionid。點選登出時,就是將該session設定為過期,這和上一句是一樣的,會生成新的sessionid,不在是當前會話了。
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Insert title here</title> </head> <body> <form action="GetCookieServlet" method="post"> <input type="hidden" value="<%=session.getId() %>>" /> 使用者名稱:<input type="text" name="username" /> <input type="submit" /> </form> </body> </html>
在第一次訪問首頁jsp檔案時,就將生成的sessionid儲存。但是我們不可能在每個頁面裡都寫一個這個隱藏的input吧?如果請求資源是一個超文字連結,點選的時候,並不會導致表單的提交,所以使用隱藏表單欄位你的形式並不支援常規的session會話跟蹤。
好久沒寫過最原始的資料庫連結和使用了,趁此回顧一下,使用mysql連線操作資料庫,我只記得在普通的java類中寫資料庫連線基本是下面這樣的:
1.引包,目前我還是在Servlet專案裡寫,所以我就用pom檔案幫我下載最新的mysql驅動包了:
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.13</version> </dependency>
import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; public class DBTest { public static final String url = "jdbc:mysql://localhost:3306/test"; public static final String user = "root"; public static final String password = "root"; public static void main(String[] args) { // TODO Auto-generated method stub Connection conn = null; Statement statement = null; ResultSet resultSet = null; try { Class.forName("com.mysql.jdbc.Driver"); conn = DriverManager.getConnection(url, user, password); statement = conn.createStatement(); resultSet = statement.executeQuery("SELECT * FROM book"); while (resultSet.next()) { int id = resultSet.getInt("id"); int user_id = resultSet.getInt("user_id"); String name = resultSet.getString("name"); System.out.println("id:" + id + " user_id:" + user_id + " name:" + name); } } catch (ClassNotFoundException e) { e.printStackTrace(); }catch (SQLException e) { e.printStackTrace(); }finally { try { if(resultSet != null) { resultSet.close(); } if(statement != null) { statement.close(); } if(conn != null) { conn.close(); } } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }
1.Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary. 2.java.sql.SQLException: The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized or represents more than one time zone. You must configure either the server or JDBC driver (via the serverTimezone configuration property) to use a more specifc time zone value if you want to utilize time zone support. ... at DBTest.main(DBTest.java:17) 3.Caused by: com.mysql.cj.exceptions.InvalidConnectionAttributeException: The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized or represents more than one time zone. You must configure either the server or JDBC driver (via the serverTimezone configuration property) to use a more specifc time zone value if you want to utilize time zone support. ... ... 6 more
第一個異常是說,使用”com.mysql.jdbc.Driver“載入驅動的方式已經被棄用了,新的載入驅動方式是”com.mysql.cj.jdbc.Driver“,雖然被棄用了,但是mysql目前依舊支持者,只是提倡你是要新方式。
第二個和第三個異常是說:mysql伺服器時區有問題,要麼你去配置一下mysql伺服器的時區:
// 在mysql中執行命令 set global time_zone='+8:00'
或者是在JDBC驅動的url地址加上serverTimezone引數指明詳細的時區,通常是serverTimezone=UTC。
好久沒使用的mysql驅動了,mysql驅動也更新了,自己也隨即更新了一下程式碼:
public static final String url = "jdbc:mysql://localhost:3306/test?serverTimezone=UTC"; Class.forName("com.mysql.cj.jdbc.Driver");
好了,接下來就是把上面的程式碼遷移到我們的Servlet就行。
package servlet; import java.io.IOException; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @WebServlet("/DBServlet") public class DBServlet extends HttpServlet { public static final String url = "jdbc:mysql://localhost:3306/test?serverTimezone=UTC"; public static final String user = "root"; public static final String password = "root"; @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doGet(req,resp); } @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // TODO Auto-generated method stub Connection conn = null; Statement statement = null; ResultSet resultSet = null; try { Class.forName("com.mysql.cj.jdbc.Driver"); conn = DriverManager.getConnection(url, user, password); statement = conn.createStatement(); resultSet = statement.executeQuery("SELECT * FROM book"); while (resultSet.next()) { int id = resultSet.getInt("id"); int user_id = resultSet.getInt("user_id"); String name = resultSet.getString("name"); System.out.println("id:" + id + " user_id:" + user_id + " name:" + name); } } catch (ClassNotFoundException e) { e.printStackTrace(); }catch (SQLException e) { e.printStackTrace(); }finally { try { if(resultSet != null) { resultSet.close(); } if(statement != null) { statement.close(); } if(conn != null) { conn.close(); } } catch (SQLException e) { e.printStackTrace(); } } } }
資訊: 攔截的地址:http://localhost:8080/Servlet/DBServlet java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1352) at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1180) ... at java.lang.Thread.run(Thread.java:748)
說找不到資料庫驅動???怎麼回事?我的資料庫驅動明明在啊,剛才通過普通的java類測試的時候沒問題,怎麼到web專案裡就報找不到資料庫驅動了?在網上找到答案,
普通的java專案,可以把資料庫驅動放在專案裡就能使用到。對於web專案,並不是依賴在專案裡的資料庫驅動,而是要把資料庫驅動放在tomcat的lib目錄下。
try { statement = conn.prepareStatement("insert into comment(pid,title,comment) values(?,?,?)",PreparedStatement.RETURN_GENERATED_KEYS); statement.setInt(1, 1); statement.setString(2, title); statement.setString(3, comment); statement.executeUpdate(); resultSet = statement.getGeneratedKeys(); if(resultSet.next()) { id = resultSet.getInt(1); } } catch (SQLException e) { e.printStackTrace(); }
在建立Statement(或PreparedStatement)是加入Statement(或PreparedStatement).RETURN_GENERATED_KEY即可,獲取結果集,在結果集中拿到主鍵。
我本來還想使用最原始的方式通過流去讀取request請求裡面的檔案,上傳到本地呢,但是後來才發現,在Servlet3.0之前中根本不提供上傳的功能,要想上傳檔案需要依賴第三方框架。
<dependency> <groupId>com.liferay</groupId> <artifactId>org.apache.commons.fileupload</artifactId> <version>1.2.2.LIFERAY-PATCHED-1</version> </dependency>
這個包依賴的commons.io.jar也會新增到專案裡,你也可以去官網下,官網上也有使用說明。
在Servlet3.0中,已經內建了上傳的功能,我們主要以Servlet3.0做開發,在搞這個上傳檔案的時候路徑是個問題,我們來看一下:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Insert title here</title> </head> <body> <form action="UploadServlet" method="post" enctype="multipart/form-data"> <input type="file" name="file" /> <input type="submit" /> </form> </body> </html>
package servlet; import java.io.File; import java.io.IOException; import java.io.PrintWriter; import javax.servlet.ServletException; import javax.servlet.annotation.MultipartConfig; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.Part; @WebServlet("/UploadServlet") @MultipartConfig(location = "", maxFileSize = 1024 * 1024 * 20) public class UploadServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException { PrintWriter out = null; Part part = null; String uploadpath = getServletConfig().getServletContext().getRealPath("/"); System.out.println(uploadpath); try { out = resp.getWriter(); part = req.getPart("file"); if (part != null) { System.out.println(part.getName()); part.write("123.md"); } out.println("上傳成功!"); } catch (IOException e) { e.printStackTrace(); } } }
啟動專案,訪問upload.html,提交一個檔案測試,發現儲存的路徑不是在我上面列印的uploadpath裡
// uploadpath C:\Users\admin\Desktop\ServletBase\.metadata\.plugins\org.eclipse.wst.server.core\tmp0\wtpwebapps\Servlet // 實際儲存目錄 C:\Users\admin\Desktop\ServletBase\.metadata\.plugins\org.eclipse.wst.server.core\tmp0\work\Catalina\localhost\Servlet
而且如果我在Multipart註解的location裡寫任何路徑,比如:location="/upload",都會報錯說找不到這個路徑。找不到那簡單啊,我獲取這個實際儲存路徑的地址,然後新建一個upload不就行了,但是我怎麼獲取這個實際儲存路徑的地址?在一系列路徑混亂的情況下,我寫了一個專門列印路徑的RoadServlet.java
package servlet; import java.io.IOException; import java.io.PrintWriter; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @WebServlet("/RoadServlet") public class RoadServlet extends HttpServlet{ @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=UTF-8"); PrintWriter out = resp.getWriter(); out.println("<table border='1px'>"); out.println(" <tr><td>req.getContextPath()</td><td>"+req.getContextPath()+"</td></tr>"); out.println(" <tr><td>req.getRequestURI()</td><td>"+req.getRequestURI()+"</td></tr>"); out.println(" <tr><td>req.getRequestURL()</td><td>"+req.getRequestURL()+"</td></tr>"); out.println(" <tr><td>req.getServletPath()</td><td>"+req.getServletPath()+"</td></tr>"); out.println(" <tr><td>req.getSession().getServletContext().getContextPath()</td><td>"+req.getSession().getServletContext().getContextPath()+"</td></tr>"); out.println(" <tr><td>getServletContext().getContextPath()</td><td>"+getServletContext().getContextPath()+"</td></tr>"); out.println(" <tr><td>getServletConfig().getServletContext().getContextPath()</td><td>"+getServletConfig().getServletContext().getContextPath()+"</td></tr>"); out.println(" <tr><td>getServletConfig().getServletContext().getRealPath(\"/\")</td><td>"+getServletConfig().getServletContext().getRealPath("/")+"</td></tr>"); out.println(" <tr><td>getServletConfig().getServletContext().getRealPath(\"\")</td><td>"+getServletConfig().getServletContext().getRealPath("")+"</td></tr>"); out.println(" <tr><td>getServletConfig().getServletContext().getRealPath(\"/../../temp\") </td><td>"+getServletConfig().getServletContext().getRealPath("/../../temp") +"</td></tr>"); out.println(" <tr><td>getServletConfig().getServletContext().getRealPath(\"/../../temp\") </td><td>"+ Thread.currentThread().getContextClassLoader().getResource("").getPath() +"</td></tr>"); out.println("</table>"); } }
到此,我們先不在追究檔案上傳路徑的問題,我們來看下一節【專案部署路徑】,一個重要的知識點。
在閱讀過專案部署路徑和eclipse中的Server工程和tomcat的關係這兩節後,我們可以繼續我們的上傳了。
package servlet; import java.io.File; import java.io.IOException; import java.io.PrintWriter; import javax.servlet.ServletException; import javax.servlet.annotation.MultipartConfig; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.Part; import org.apache.commons.lang3.StringUtils; @WebServlet("/UploadServlet") @MultipartConfig(maxFileSize = 1024 * 1024 * 20) public class UploadServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException { PrintWriter out = null; Part part = null; String uploadpath = getServletConfig().getServletContext().getRealPath("/WEB-INF/upload"); try { out = resp.getWriter(); part = req.getPart("file"); if (part != null) { String fileName = getFileName(part); part.write(uploadpath + File.separator + fileName); } out.println("上傳成功!"); } catch (IOException e) { e.printStackTrace(); } } /** * 如何得到上傳的檔名, API沒有提供直接的方法,只能從content-disposition屬性中獲取 * * @param part * @return */ protected static String getFileName(Part part) { if (part == null) return null; String fileName = part.getHeader("content-disposition"); if (StringUtils.isBlank(fileName)) { return null; } return StringUtils.substringBetween(fileName, "filename=\"", "\""); } }
<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.8.1</version> </dependency>
啟動專案,訪問upload.html,在這裡我遇到了一個大坑。
那就是專案一直報StringUtils類ClassNotFoundException!我以為是因為我使用了源伺服器部署導致了,但是我把他改成克隆伺服器部署還是報這個錯誤,後來到網上找到了原因,我把它記錄在【工具-eclipse筆記中了】,主要是因為我的專案所依賴的包,也要一起釋出到專案裡,也就是放到WEB-INF下的lib目錄下,你可以手動去一個個拷貝,也可以到【eclipse筆記中看一下快捷新增方式】。
現在再次訪問upload.html上傳,你就會在你的***\Servlet\WEB-INF\upload目錄下看到你上傳的檔案了(***取決於你使用的是哪種tomcat伺服器)。
到此我們大概也就知道了,使用註解方式不說明上傳地址時,不管我們把部署路徑配置在哪,預設時都是上傳到克隆伺服器的work目錄中。但是如果我們不使用預設上傳地址的方式時,我們可以使用下面方式來指定上傳地址。
getServletConfig().getServletContext().getRealPath("/")
獲取克隆伺服器/源伺服器中專案地址(到底是哪個,取決你eclipse中server的配置),不管使用哪個,遷移到源伺服器是不影響的!
說到上面那一堆路徑,我不得不說一下我們的專案部署路徑。預設的我們在eclipse新建的web專案,如果使用eclipse整合tomcat,那當你在new一個tomcat Server後,開啟Server會看到這麼一張圖:
在這裡有一個Deploy path=wtpwebapps,這是eclipse整合tomcat後的預設部署位置
也就是在你的eclipse的工作目錄下該專案的部署位置。但是,你細想我們之前沒有使用eclipse整合tomcat時是怎麼把專案部署到tomcat裡的?之前是直接把專案的war包,放到tomcat的E:\apache-tomcat-8.0.52\webapps目錄下,然後啟動tomcat即可。最開始時我們是修改了E:\apache-tomcat-8.0.52\conf\server.xml改成了下面這樣:
<?xml version='1.0' encoding='utf-8'?> <Server port="8005" shutdown="SHUTDOWN"> <Service name="Catalina"> <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" URIEncoding="UTF-8"/> <Engine name="Catalina" defaultHost="localhost"> <Host name="localhost"> <Context path="" docBase="C:\Users\admin\Desktop\ServletBase\ServletDemo\webapp" reloadable="true"/> </Host> </Engine> </Service> </Server>
上面這種都是在tomcat自身的部署目錄裡部署的,但是用eclipse整合tomcat的話預設就是在eclipse專案檔案下部署專案。我們可以在新建tomcat Server時修改【只有新建時能修改,當新增專案進去後就不能修改了】,Deploy path為tomcat自身的部署目錄即:
好了,現在我們更新了專案的部署路徑,再次去訪問RoadServlet,得到的圖是下面這樣的:
而且如果我們使用tomcat自身的部署環境而不是eclipse整合tomcat的部署環境,那麼檔案的上傳和下載就簡單多了!因為我們現在使用的是tomcat自身的部署環境,你在去C:\Users\admin\Desktop\ServletBase\.metadata\.plugins\org.eclipse.wst.server.core\tmp1下就再也找不到讓人頭疼的wtpwebapps檔案夾了,而且你也不用在關心temp1下的work,webapps這些東西,因為你現在是在tomcat自身的部署環境了,這個環境很乾淨。
你可以很明確的知道你要上傳到哪?還有就是我們再來測一下我們使用預設不配置@MultipartConfig中location時也不寫其他路徑,它會存在哪裡?結果是:
C:\Users\admin\Desktop\ServletBase\.metadata\.plugins\org.eclipse.wst.server.core\tmp1\work\Catalina\localhost\Servlet。你會意外,噯?我們不是切換成tomcat自身的部署環境了嗎?而且當你把eclipse整合的tomcat刪除之後org.eclipse.wst.server.core下面就沒有temp1檔案夾了啊,但是為什麼我們在新建一個部署在tomcat環境裡的Server,org.eclipse.wst.server.core下又會生成temp1,而且之前的哪些檔案除了wtpwebapps資料夾沒有了,其他的依舊還在。想想你也應該明白,雖然我們在eclipse中切換了tomcat的部署環境,但是我們實際上使用的還是eclipse中整合的tomcat。我們配置eclipse中的Deploy path只是把專案部署在tomcat裡,但是執行時依靠的tomcat還是eclipse中的tomcat,而不是E:\apache-tomcat-8.0.52這個。說到這可能已經暈的不行了。重啟一段來說明一下eclipse中的Server工程和tomcat的關係。【請閱讀下一節:eclipse中的Server工程和tomcat的關係】
看過下一節後,我們就可以清楚的定義源伺服器和克隆伺服器了,上面說的就是在eclipse中使用的時克隆伺服器,但是專案的部署地址放到了源伺服器下,但是檔案上傳註解location引數不寫時,它上傳在了克隆伺服器下org.eclipse.wst.server.core\tmp1\work\Catalina\localhost\Servlet內。【這裡的temp0和temp1請不要糾結,它們只是我之前建立的克隆伺服器沒刪掉而已,不是重點】。
最開始的時候,如果我們的eclipse中沒有任何專案,也沒new server時,在我們的C:\Users\admin\Desktop\ServletBase\.metadata\.plugins\org.eclipse.wst.server.core資料夾下是這樣的:
但當我們在eclipse中建立一個web專案後,new server並新增一個server伺服器即tomcat然後指定tomcat目錄和JRE環境點選next-finish,此時我們只是建立了一個空的server,還沒向裡面新增任何web專案。現在我們執行一下這個空的server,看看org.eclipse.wst.server.core資料夾下會發生什麼變化。
C:\Users\admin\Desktop\ServletBase\.metadata\.plugins\org.eclipse.wst.server.core
目錄下建立了一個temp0,如果你仔細對比E:\apache-tomcat-8.0.52資料夾下內容和temp0下內容你會發現
這兩個目錄下的內容基本時相似的。只是temp0更多了一個wptwebapps資料夾,而bin、lib只有E:\apache-tomcat-8.0.52才有。
C:\Users\admin\Desktop\ServletBase\.metadata\.plugins\org.eclipse.wst.server.core\temp0
只是E:\apache-tomcat-8.0.52目錄的一個克隆,所以上面程式碼段所描述的這個目錄也就具備了源伺服器的功能。如果你在new幾個server,就會在org.eclipse.wst.server.core目錄下依次出現temp1,temp2,temp3等多個克隆伺服器,但是這裡每次只能啟動上面一個克隆伺服器,因為它們都是使用相同的啟動埠。
這樣的機制就保證了你eclipse裡的專案不會影響原先tomcat裡的配置,每次都用不同的引數來啟動tomcat。這樣會有一個問題,就是如果你原先的tomcat配置檔案有錯的話,eclipse會先拷貝你原有的tomcat下的配置,然後在這個配置的基礎上修改。所以,遇到這種問題,先保證原有的配置沒問題,然後再去修改eclipse新生成的,或者直接刪除重配。