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声明的字符。如果需要使用中文就像上一节那样进行转码。
其实我在想,在tomcat7以后,tomcat的编码格式默认都使用UTF-8了,还有必要这样在设置一遍吗?之前在Servet实战(1)中也有说到编码问题,如果依赖的是tomcat,我想设置返回页面的编码格式应该也是可以的,测一测吧:
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 { 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 != ' ') || 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 = "/()<>@,;:\"[]?={} "; } 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.pluginsorg.eclipse.wst.server.core mp0workCatalinalocalhostServlet目录下的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:UsersadminDesktopServletBase.metadata.pluginsorg.eclipse.wst.server.core mp0wtpwebappsServlet // 实际保存目录 C:UsersadminDesktopServletBase.metadata.pluginsorg.eclipse.wst.server.core mp0workCatalinalocalhostServlet
而且如果我在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上传,你就会在你的***ServletWEB-INFupload目录下看到你上传的文件了(***取决于你使用的是哪种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.52webapps目录下,然后启动tomcat即可。最开始时我们是修改了E:apache-tomcat-8.0.52confserver.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:UsersadminDesktopServletBaseServletDemowebapp" reloadable="true"/> </Host> </Engine> </Service> </Server>
上面这种都是在tomcat自身的部署目录里部署的,但是用eclipse集成tomcat的话默认就是在eclipse项目文件下部署项目。我们可以在新建tomcat Server时修改【只有新建时能修改,当添加项目进去后就不能修改了】,Deploy path为tomcat自身的部署目录即:
好了,现在我们更新了项目的部署路径,再次去访问RoadServlet,得到的图是下面这样的:
而且如果我们使用tomcat自身的部署环境而不是eclipse集成tomcat的部署环境,那么文件的上传和下载就简单多了!因为我们现在使用的是tomcat自身的部署环境,你在去C:UsersadminDesktopServletBase.metadata.pluginsorg.eclipse.wst.server.core mp1下就再也找不到让人头疼的wtpwebapps文件夹了,而且你也不用在关心temp1下的work,webapps这些东西,因为你现在是在tomcat自身的部署环境了,这个环境很干净。
你可以很明确的知道你要上传到哪?还有就是我们再来测一下我们使用默认不配置@MultipartConfig中location时也不写其他路径,它会存在哪里?结果是:
C:UsersadminDesktopServletBase.metadata.pluginsorg.eclipse.wst.server.core mp1workCatalinalocalhostServlet。你会意外,嗳?我们不是切换成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 mp1workCatalinalocalhostServlet内。【这里的temp0和temp1请不要纠结,它们只是我之前创建的克隆服务器没删掉而已,不是重点】。
最开始的时候,如果我们的eclipse中没有任何项目,也没new server时,在我们的C:UsersadminDesktopServletBase.metadata.pluginsorg.eclipse.wst.server.core文件夹下是这样的:
但当我们在eclipse中创建一个web项目后,new server并添加一个server服务器即tomcat然后指定tomcat目录和JRE环境点击next-finish,此时我们只是创建了一个空的server,还没向里面添加任何web项目。现在我们运行一下这个空的server,看看org.eclipse.wst.server.core文件夹下会发生什么变化。
C:UsersadminDesktopServletBase.metadata.pluginsorg.eclipse.wst.server.core
目录下创建了一个temp0,如果你仔细对比E:apache-tomcat-8.0.52文件夹下内容和temp0下内容你会发现
这两个目录下的内容基本时相似的。只是temp0更多了一个wptwebapps文件夹,而bin、lib只有E:apache-tomcat-8.0.52才有。
C:UsersadminDesktopServletBase.metadata.pluginsorg.eclipse.wst.server.core emp0
只是E:apache-tomcat-8.0.52目录的一个克隆,所以上面代码段所描述的这个目录也就具备了源服务器的功能。如果你在new几个server,就会在org.eclipse.wst.server.core目录下依次出现temp1,temp2,temp3等多个克隆服务器,但是这里每次只能启动上面一个克隆服务器,因为它们都是使用相同的启动端口。
这样的机制就保证了你eclipse里的项目不会影响原先tomcat里的配置,每次都用不同的参数来启动tomcat。这样会有一个问题,就是如果你原先的tomcat配置文件有错的话,eclipse会先拷贝你原有的tomcat下的配置,然后在这个配置的基础上修改。所以,遇到这种问题,先保证原有的配置没问题,然后再去修改eclipse新生成的,或者直接删除重配。