Servlet补充
Javaweb — Servlet 的cookie和监听器、过滤器 117天
Servlet的cookie【共享数据】、监听器和过滤器;
疫情形势下的Servlet学习计划:happy:;冲一冲,进框架
Cookie
cookie – 饼干;emm… tomcat猫 ; cookie就像医院的就诊卡,第一次去医院办卡,录入信息;比如治疗的流程很长,第一次就诊卡中就录入了病情,还会将用药、预约之类的都录入就诊卡;这样第二次去的时候,就算不去找之前负责的护士;找其他的护士,那么也可以直接根据就诊卡查出病情并提供优质的服务。
这里的就诊卡就提高了效率,病人进入医院就不需要再次去挂号,告诉医生病情,直接根据卡片的信息找到治疗的地点然后接受治疗服务;这样病人的体验就很好,医院的治疗效率也提高了 ------ 这里的就诊卡就是servlet;医院就是服务器,接待的医生就是servlet;就诊卡实现了数据共享
Cookie是浏览器提供的一种技术,通过服务器的程序能将一些只需要保存在客户端,或者在客户端进行处理的数据,放在本机的计算机上,不需要进行网络传输; ---- Http是无状态协议,前一次的请求和后一次请求没有任何关系,用户如果访问了某网站,那么第二次访问的时候就不需要再次发送庞大的请求了;但是由于Cookie是服务端保存在客户端的信息,所以安全性很差;比如常见的记住密码就是cookie实现的
cookie的特点与原理
- Cookie来自Servlet规范中的一个工具类,存放在Tomcat中的servlet-api.jar中
- 如果两个Servlet来自同一个网站,并且为同一个浏览器/用户提供服务,这个时候就可以实现数据共享
- Cookie存放当前用户的私人数据,比如用户名和密码,手机号等,在共享数据的过程中提高服务的质量
Cookie的原理
用户通过浏览器第一次向myWeb网站发送请求申请OneServlet,OneServlet在运行期间创建一个Cookie存储与当前用户相关数据,OneServlet工作完毕后,【将Cookie写入到响应头header】交还给当前的浏览器
浏览器得到响应的响应包之后,将cookie存储在浏览器的缓存,一段时间之后,用户通过同一个浏览器再次访问网站申请TwoServlet时 ,【浏览器需要无条件将myWeb网站之前推送过来的Cookie写入到请求头中】,发送过去;此时TwoServlet运行的时候,就可以通过读取请求头中的cookie信息,得到OneServlet提供的共享数据
所以同一网站的Oneservlet和TwoServlet借助于Cookie实现数据共享
cookie的创建和发送
比如在OneServlet中需要使用cookie记录用户的信息;这个和请求转发的数据共享不同,请求转发时同一次请求,但是这里两个请求时没有任何关系的 String在各种编程语言中都是统一的
public class OneServlet extends HttpServelt{
public void doGet(HttpServletRequest req,HttpServletResponse resp) {
Cookie card = new Cookie("key","value");
resp.addCookie(card); -----这是一个cookie
}
}
之后这个resp传递会浏览器的时候,【请求封装的是请求的内容,响应封装的是响应的内容,resp的writer就是书写的响应正文的内容】响应包中的内容就包括响应行,响应头,空白行,响应正文 ; cookie就存放在响应头中
之后浏览器再次向web网站发出请求,不一定是之前的资源;这个时候发送的请求的请求头中就包括cookie;这个cookie就是上一次响应传输过来保存在浏览器中的
之后访问的资源比如ServletTwo就可以取出Cookie中的内容
public class TwoServlet extends HttpServelt{
public void doGet(HttpServletRequest req,HttpServletResponse resp) {
Cookie cookieArray[] = request.getCookies();
for(Cookie cok : cookieArray) {
String key = cok.getName();
Strig value = cok.getValue();
}
}
}
一个浏览器可以对应多个cookie;一个cookie只能存放一个key和value;所以需要多个cookie实现数据共享;这样就可以实现不同的请求得数据共享,因为HTTP协议是无状态协议,正常情况下是不能实现数据共享的
cookie使用的模拟
会员卡点餐的应用---- 用户向餐厅申请会员卡,保存的信息有用户名,预存的金额; 然后用户可以直接在点餐的界面点餐;【 这里的服务器就会根据用户的信息创建两个Cookie;并且将这个信息给放到响应头中给浏览器返回,
这里就是这个简单程序的模拟,所以按照步骤,首先设计前端界面,看到想要达到的效果;
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>OA点餐系统</title>
</head>
<body>
<h1 align="center">新用户注册</h1>
<hr color="aquamarine"/>
<form action="order.html" method="post">
<table style="border: aquamarine;">
<tr>
<td>用户名</td>
<td><input type="text" name="user"/></td>
</tr>
<tr>
<td>密码</td>
<td><input type="password" name="pwd"/></td>
</tr>
<tr>
<td>预存金额</td>
<td><input type="text" name="money"/></td>
</tr>
<tr align="center">
<td colspan="2"><input type="submit" name="注册"/></td>
</tr>
</table>
</form>
</body>
</html>
这里就是界面的设计,经过跳转,发现是正常的,第一个过程就是点击注册之后,访问loginServlet进行cookie的放入和页面的响应
注意这里返回的点餐界面都是静态的,所以这里就不需要使用writer来写到浏览器,直接使用重定向【请求转发是需要共享请求域时才使用,防止恶意提交】 重定向可以调用其他应用中的资源,这里其实就是服务器Tomcat调用相关的资源,所以这里不仅可以时动态的Servlet;也可以是静态的页面
package oa;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class loginServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
String user = req.getParameter("user");
String pwd = req.getParameter("pwd");
String money = req.getParameter("money");
Cookie usercok = new Cookie("user", user);
Cookie pwdcok = new Cookie("pwd",pwd);
Cookie moneycok = new Cookie("money", money);
resp.addCookie(usercok);
resp.addCookie(pwdcok);
resp.addCookie(moneycok);
resp.sendRedirect("order.html");
}
}
这里就使用的重定向让Tomcat调用order.html;使用的是资源路径方式
这里使用重定向,使用HttpWatch会观察到多了一个Get,所以使用请求转发来完整模拟上面的图片
这里使用HttpWatch来观察这个结果
首先就是第一次的POST请求
POST /Test/login HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:35.0) Gecko/20100101 Firefox/35.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Referer: http://localhost:8080/Test/
Cookie: user=寮犱笁; pwd=1234; money=100
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 98
user=%E5%BC%A0%E4%B8%89&pwd=1234&money=100&%E6%B3%A8%E5%86%8C=%E6%8F%90%E4%BA%A4%E6%9F%A5%E8%AF%A2
对应的第一次的响应,也就是上面图中的响应
HTTP/1.1 200
Set-Cookie: user=寮犱笁
Set-Cookie: pwd=1234
Set-Cookie: money=100
Accept-Ranges: bytes
ETag: W/"564-1640438335734"
Last-Modified: Sat, 25 Dec 2021 13:18:55 GMT
Content-Type: text/html
Content-Length: 564
Date: Sat, 25 Dec 2021 13:30:49 GMT
Keep-Alive: timeout=20
Connection: keep-alive
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>鐐归鐣岄潰</title>
</head>
<body>
<h1 align="center">娆㈣繋浣跨敤OA鐐归绯荤粺</h1>
<hr color="aquamarine" />
<form action="">
閰歌彍鑲変笣 20<input type="radio" name="price" value="20"/><br/>鍦熻眴鑲変笣 20<input type="radio" name="price" value="20"/><br/>
鍥涘窛娉¤彍 10<input type="radio" name="price" value="10"/><br/>鏂扮枂澶х洏楦?30<input type="radio" name="price" value="30"/><br/>
<input type="submit" value="涓嬪崟" />
</form>
</body>
</html>
这下子就是只有一个请求和响应了,这里的响应和上面的预计是相同的,这里没有使用writer,不然因为forward,这里的流还没有开启;
观察响应,空白行下面的响应正文【响应体】就是返回的order.html界面;而响应头中,放入了cookie
Set-Cookie: user=寮犱笁
Set-Cookie: pwd=1234
Set-Cookie: money=100
虽然这里的中文乱码了,但是在显示中没有乱码;HTTP传输使用的是TCP协议,是字节流传输的
第二次的请求
GET /Test/order?price=10 HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:35.0) Gecko/20100101 Firefox/35.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*
可以看到这里的请求头中就有一个参数cookie了,这样就完成了数据的共享;第二个servlet就可以不必再连接数据库或者其他的操作来让用户重新进行操作
第二次的响应
HTTP/1.1 200
Content-Type: text/html;charset=UTF-8
Content-Length: 146
Date: Sat, 25 Dec 2021 13:59:27 GMT
Keep-Alive: timeout=20
Connection: keep-alive
<h1 align="center">娆㈣繋浣跨敤涓嬫鍏変复</h1>
<hr color="aquamarine" />
浣犲ソ锛屽皧鏁殑寮犱笁
鏈浣犱竴鍏辨秷璐?0鍏?浣欓涓?0鍏?
这里的响应和请求都是标准的四个部分;从这个简单的应用就可以看出来cookie的作用就是实现不同的请求直接能够使用用户的cookie来进行数据共享
cookie的生命周期 setMaxAge
上面已经分享过,在默认的情况下,cookie对象存放在浏览器的缓存中
因此只要浏览器关闭,cookie对象就会被销毁掉
- 但是在默认的情况下,可以要求浏览器接受的cookie存放在客户端计算机的硬盘上,同时需要指定cookie在硬盘上的存活时间,在存活时间范围内,关闭浏览器,cookie不会被销毁掉;存活时间到达后,cookie就自动从硬盘中删除
usercok.setAge(60);
cookie的评价
上面只是说到了cookie的用处就是共享数据,通过实际应用就知道是将用户提交的相关数据或者其他的数据放到cookie中打到用户的浏览器中,实现了本地存储
本地存储的好处: 避免取回数据前页面一片空白,也就是如果不需要最新数据就可以减少向浏览器发送请求,减少了等待服务器响应的时间
同时可以在网络状态不佳的时候显示离线数据 – 数据都在用户的端系统上;直接可以加载了
cookie就是一种本地存储的方式,它会在存活的时间内跟随任意一次HTTP请求一起发送
优点是兼容性好,但是缺点就是存储的数据有限,仅有几kb;并且会增大网络的流量【因为在请求头中】,不安去
- cookie作为全局变量,涌入就是帮助web站点访问者的信息
- 保存用户的登录信息,比如访问微博,登录过就有下次自动登录,勾选之后就会将id等作为cookie存储在硬盘中
- 创建购物车。购物网站通常将已选择的物品保存在cookie中,这样可以实现不同页面的数据的同步,提交订单的时候将cookie传到后台
- 跟踪用户行为。比如百度通过cookie记录用户的偏好信息,向用户推荐个性化信息。也就i是旁边的小广告,这是可以禁用的
所以cookie的重要作用:1.就是实现了本地存储,这样就可以保存用户的相关信息到这个;2.从Servlet的角度来说,如果在SetAge的生命周期中,那么浏览器对同一个应用的访问请求都会携带放入的cookie,所以就实现了数据的共享
HttpSession接口
HttpSession接口来自Servlet规范下的一个接口,存在Tomcat的servlet-api.jar中,实现类也是Http服务器提供,Tomcat提供的实现类也存在于servlet-api.jar中
它和cookie相同,如果两个Servlet,并且为同一个浏览器/用户提供服务,此时借助于HttpSession对象进行数据共享
HttpSession叫做【会话作用域对象】 作为域,操作的就是域属性,所以就是Attribute;和req获取参数不同
HttpSession和Cookie的区别
- 存储位置不同 : Cookie存放在客户端计算机中【浏览器内存/硬盘中】;HttpSession存放在服务端计算机的内存中
- 数据类型不同: Cookie对象存储共享的数据类型只能是String,键值都是String类型的,但是HttpSession可以存储任意类型的共享数据Object
- 数据数量: 一个cookie只能存储一个共享数据 ; HttpSession使用map集合存储共享数据,可以存储任意数量的共享数据
- 参照物: Cookie相当于客户在服务端【会员卡】 ,HttpSession客户在服务端的【私人保险柜】
一个是本地存储,不安全;另外一个是服务端存储,安全一些
HttpSession的创建实现
使用的是request的方法的getSession;相当于cookie是一个小格子,然后创建cookie就直接new 一个小格子;但是Session是服务器的,所以是关联着服务器的Request的,所以使用该方法就可以在服务器的域中开辟一个私人的保险柜
可以看一下接口中对于这个方法的说明
public HttpSession getSession();
也就是说,这里返回的是和这个现在的这个请求相关联的会话【保险柜】,如果这个当前的请求没有session;就创建一个session
public class OneServlet extends HttpServelt{
public void doGet(HttpServletRequest req,HttpServletResponse resp) {
HttpSession session = req.getSession();
session.setAttribute("key1",共享数据);
}
}
如果浏览器还是访问的是现在的这个应用的另外一个页面,那么
public class TwoServlet extends HttpServelt{
public void doGet(HttpServletRequest req,HttpServletResponse resp) {
HttpSession session = request.getSession();
Object 共享数据 = session.getAttribute("key1");
}
}
Session就是一种记录用户状态的对象;因为Http是无状态的;一个session对应的是一个用户,那么这里session是如何实现绑定用户的呢?一般会有一个cookie,这个cookie就是sessionID;如果客户端禁用了cookie;这个时候一般会使用URL机制,在后面加上后缀来表明session; 【这里的Cookie–sessionid是Tomcat自动帮助创建的】,同时需要注意的是,session保存在服务器的缓存中,也可以持久化到数据库或者文件中
这里就用画图工具来模拟了这个程序的一个简单的流程,所以这个程序就很好写了
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>OA购物商城</title>
</head>
<body>
<h1 align="center">OA购物商城</h1>
<hr color="aquamarine" />
<table border="1px" width="50%">
<tr>
<th>商品名称</th>
<th>商品单价</th>
<th>商品评价</th>
<th>加入购物车</th>
</tr>
<tr>
<td>华为nova</td>
<td>4000</td>
<td>非常好用</td>
<td><a href="/Test/addCart?goodsName='HuaWeinova'">加入购物车</a></td>
</tr>
<tr>
<td>洗发水</td>
<td>10</td>
<td>一般般</td>
<td><a href="/Test/addCart?goodsName='shampoo'">加入购物车</a></td>
</tr>
<tr>
<td>坚果</td>
<td>30</td>
<td>好</td>
<td><a href="/Test/addCart?goodsName='nut'">加入购物车</a></td>
</tr>
</table>
</body>
</html>
首先设计的时候还是先设计前端界面,看到界面的效果看是否符合预期,之后就是设计配置web.xml;并且同时设计Servlet
需要注意的一点是,这里的get提交的数据name=value,在地址栏中就直接写value,不需要加上单引号或者双引号 比如goodsName=HuaWeinova 不需要对HuaWei使用单引号
还有一个思想主要转变,就是画流程图的时候,比如第一次响应之后,用户看到的界面不应该是空白,而是之前的静态界面,这个时候就不要再想什么跳转不执行之类的,而是想该servlet得到信息后怎么操作,使用req得到,resp返回什么; 静态界面也是Tomcat调用,所以需要使用请求转发或者重定向;所以直接请求转发即可
package oa;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
public class AddCartServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String goodsName = req.getParameter("goodsName");
HttpSession session = req.getSession();
Integer goodNum = (Integer)session.getAttribute(goodsName);
if(goodNum == null) {
session.setAttribute(goodsName, 1);
}else {
session.setAttribute(goodsName, goodNum + 1);
}
req.getRequestDispatcher("/index.html").forward(req, resp);
}
}
这里主要是返回的是之前的界面,不需要再画,画的界面是动态的才需要画,比如需要从数据库中取数据,那个时候才需要画,全部是静态界面就直接请求转发或者重定向就可以了
package oa;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Enumeration;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
public class ViewCartServlet 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("<h1 align=\"center\">我的购物车</h1>");
out.println("<hr color=\"aquamarine\" />");
HttpSession session = req.getSession();
Enumeration<String> attriNames = session.getAttributeNames();
while(attriNames.hasMoreElements()) {
String good = attriNames.nextElement();
out.println(good + " : " + session.getAttribute(good) + "件\n");
}
out.println("现在下单吗?");
}
}
这里就还是使用了Enumeration工具类
Httpsession如何与用户关联
这里可以使用工具来看一下请求和响应的内容来观察HttpSession是如何充当某个用户的保密柜的
第一次请求
GET /Test/addCart?goodsName=HuaWeinova HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:35.0) Gecko/20100101 Firefox/35.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Referer: http://localhost:8080/Test/
Connection: keep-alive
第一次响应
HTTP/1.1 200
Set-Cookie: JSESSIONID=FCF2630161D138D6098F26955772E929; Path=/Test; HttpOnly
Accept-Ranges: bytes
ETag: W/"1106-1640526920051"
Last-Modified: Sun, 26 Dec 2021 13:55:20 GMT
Content-Type: text/html
Content-Length: 1106
Date: Sun, 26 Dec 2021 14:12:20 GMT
Keep-Alive: timeout=20
Connection: keep-alive
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>OA璐墿鍟嗗煄</title>
</head>
</body>
</html>
这里的主要需要注意的地方是
Set-Cookie: JSESSIONID=FCF2630161D138D6098F26955772E929; Path=/Test; HttpOnly
这里就说明Tomcat自动给创建了一个cookie,这个cookie中的键值对为JSEESIONID = …… 代表的就是用户对应的session的编号,下次用户访问的时候通过携带这个cookie,然后就可以定位到用户的session;注意:HttpOnly代表只能浏览器访问,安全性高,path指明了这个cookie,保密柜对应的程序
第二次请求
GET /Test/viewCart HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:35.0) Gecko/20100101 Firefox/35.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Referer: http://localhost:8080/Test/addCart?goodsName=HuaWeinova
Cookie: JSESSIONID=FCF2630161D138D6098F26955772E929
Connection: keep-alive
第二次响应
HTTP/1.1 200
Content-Type: text/html;charset=UTF-8
Content-Length: 110
Date: Sun, 26 Dec 2021 14:12:23 GMT
Keep-Alive: timeout=20
Connection: keep-alive
<h1 align="center">鎴戠殑璐墿杞?/h1>
<hr color="aquamarine" />
HuaWeinova : 1浠?
鐜板湪涓嬪崟鍚楋紵
可以看到和预想的相同,第二次请求的请求头中就携带了第一次响应打过来的cookie :
Cookie: JSESSIONID=FCF2630161D138D6098F26955772E929
getSession()和getSession(false)区别
无参的 : 如果当前在服务器中已经有了自己的私人储物柜;要求tomacat将这个储物柜进行返回 ;如果当前用户在服务端还没有自己的session作用域,那么tomcat就会给该用户创造一个全新的session
false : 相当于关闭自动创建的机制,如果有session就返回; 如果没有就不会创建了,而是返回null
一般情况下,这种都是需要登录的,如果用户的身份合法,那么就使用无参的,没有session就创建;但是如果用户是不合法的,比如是游客登录淘宝,那么如果他想获取购物车,那么就会返回一个空的购物车
HttpSession的生命周期
刚刚的用户实践中,就发现如果没有设置,默认情况下,存在服务器计算机的内存中,也就是关闭了服务器,那么这个session就不存在了
销毁的时机
- 用户与HttpSession关联使用的Cookie只能放在浏览器的缓存中,浏览器关闭的时候,意为着用户与他的HttpSesssion关系被切断
- 由于Tomcat是不能主动检测浏览器的状态的,所以不知道浏览器何时关闭,因此在浏览器关闭的时候不会导致Tomcat将浏览器关联的HtttpSession进行销毁
- 为解决销毁的问题,Tomcat为每一HttpSession对象设置【空闲时间】,这个空闲时间默认为30分钟,如果当前的HttpSession对象空闲的时间达到了30分钟,此时Tomcat就认为用户放弃了HttpSession;此时Tomcat就会销毁这个HttpSession
之前的Cookie的setMaxAge就是设置的是生命周期,也就是空闲的,如果一旦被操作,就要重新操作
HttpSession空闲时间手动设置
在当前网站的WEB-INF的web.xml中进行配置【不是在java代码中修改】
<session-config>
<session-timeout>5</session-timeout>
</session-config>
req请求转发请求域共享
就是可以使用的请求对象进行数据共享,就是请求域,其实就是请求转发可以共享数据;就是请求域还是attribute;如果是同一个请求,就可以实现共享;所以之前分享会话(作用域)就说过类似请求(作用)域
注意必须是同一个请求【直接使用set,get,remove即可】还有一个ServletContext;context就可以代表一个应用了,可以用这个设置参数;该应用的所有的都可以共享,但是ServletConfig中的参数只能该Servlet看到
Servlet规范扩展-- 监听器接口
之前最开始的时候就提到过Servlet规范下除了狭义的Servlet接口之外,还有监听器等接口;监听器接口一共有8个接口;其也存在与servlet-api.jar ; 监听器接口需要由开发人员亲自实现,Http服务器提供的jar包并没有对应的实现类,在java的界面编程的对象中也有监听器Listener,监听事件
监听器接口用于监控作用域对象生命周期变化时期,以及作用域对象共享数据的变化时刻;就是监控的是作用域对象发生变化的时候就会触发
作用域对象
- 在Servlet规范中,认为在服务端内存中可以在某些条件下为两个Servlet之间提供数据共享方案的对象
-
- ServletContext 全局作用域对象【这里要避免一个误区:
全局作用域对象在服务器启动的时候就会创建,在服务器关闭的时候就会销毁,context就是代表的应用 】 ---- 全局对象的初始化和销毁与是否向其中放入数据没有关系 - HttpSession 会话作用域对象
- HttpServletRequest 请求作用域对象
监听器接口实现类的开发规范:
一般来说,一项技术的使用在三步之内就是优秀的技术,像JDBC编程六步,需要整整6个步骤,所以之后就会被框架给取代了
- 一般根据监听的实际情况,选择对应的监听器接口进行
- 这些接口都没有进行实现,所以需要重写监听器接口声明【监听事件处理方法】
- 在web.xml文件中将监听器接口实现类注册到Http服务器 ---- 注意: 监听器是监听器,不是servlet,继承的是EventLister接口,是java SE中的,和Servlet没有关系,配置的方法就是
<listener>
<listener-class>oa.TestContextListener</listener-class>
</listener>
最后两步和Servlet的开发的步骤是相同的,但是Servlet可以直接创建,直接使用注解即可webServlet
ServletContextListener接口 — context的生命周期【lifecycle】
作用: 通过名称就可以知道这个接口是用来监控ServletContext全局作用域对象发生变化的时刻(包括被初始化的时刻以及被销毁的时刻 ----- 生命周期)
这个接口实现的是 java的util中的EventListener接口
监听器的实现类一般都放在监听器包下面listener,就像dao包下面都是数据库访问的类
监听事件处理的方法:
public void contextInitlized() : 在全局作用域对象被Http服务器初始化的时候触发
public void contextDestory() : 在全局作用域对象被Http服务器销毁的时候触发
所以这里的测试,就是服务器,这个应用运行了,就会创建初始化全局作用域对象,当应用停止的时候就会销毁全局作用域对象
package cfeng; --------------- Test1
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
public class ContextListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
ServletContextListener.super.contextInitialized(sce);
System.out.println("创建了context对象");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
ServletContextListener.super.contextDestroyed(sce);
System.out.println("销毁了context对象");
}
}
package oa;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
@WebListener
public class TestContextListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("初始化了全局作用域对象");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("销毁全局作用域对象");
}
}
分别是两种注册方式,第一种就是在web.xml中写listener标签; 第二种就是使用注解@Weblistener
Servlet的注册也有两种方式,一种是在web.xml中配置,一种就是使用@Webservlet注解
当服务器打开的时候,就会创建一个全局作用域对象,当服务器stop的时候,就会销毁全局作用域对象
信息: 正在启动 Servlet 引擎:[Apache Tomcat/9.0.55]
创建了context对象
TLD的完整JAR列表。 在扫描期间跳过不需要的JAR可以缩短启动时间和JSP编译时间。
初始化了全局作用域对象
12月 27, 2021 2:04:05 下午 org.apache.coyote.AbstractProtocol start
销毁了context对象
12月 27, 2021 2:05:14 下午 org.apache.catalina.loader.WebappClassLoaderBase checkThreadLocalsForLeaks
12月 27, 2021 2:05:14 下午 org.apache.catalina.loader.WebappClassLoaderBase
销毁全局作用域对象
12月 27, 2021 2:05:1
这里分别使用了两个应用,都有ServletContextListener监听器;所以可以看到服务器关闭和开启的时候都是正常监听了;
ServletContextAttributeListener 数据变化【changes to attribute】
作用: 通过这个接口合法的检测全局作用域对象共享的数据变化的时刻
监听事件处理方法:
public void contextAdd(); 在全局作用域对象中添加共享数据
public void contextReplace(); 在全局作用域对象更新共享数据的时候触发
puclic void contextRemove(); 在犬奴作用域对象删除共享数据
HttpServlet继承的是GenericServlet;实现了ServletConfig接口
@WebServlet("/listen")
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.getWriter().append("Served at: ").append(request.getContextPath());
ServletContext application = request.getServletContext();
application.setAttribute("user", "张三");
application.setAttribute("user", "李四");
application.removeAttribute("user");
}
监听器监听全局作用域对象
@WebListener
public class ContextAttListener implements ServletContextAttributeListener {
public ContextAttListener() {
}
public void attributeAdded(ServletContextAttributeEvent scae) {
System.out.println("添加了属性到全局作用域");
}
public void attributeRemoved(ServletContextAttributeEvent scae) {
System.out.println("删除了全局作用域中的属性");
}
public void attributeReplaced(ServletContextAttributeEvent scae) {
System.out.println("修改加了全局作用域中的属性");
}
所以结果就可以看到一旦域属性发生改变,就会触发相应的事件;分别就对应上面的servlet的三个操作
信息: [1049]毫秒后服务器启动
添加了属性到全局作用域
修改加了全局作用域中的属性
删除了全局作用域中的属性
监听器的运用 : 减少数据库的处理时间
这里可以看一下之前的单表操作中修改操作的时间是
Date startDate = new Date();
StudentDao stuDao = new StudentDao();
int count = stuDao.editModifyStudent(stuno, stuname, stuclass);
Date endDate = new Date();
System.out.println((endDate.getTime() - startDate.getTime()) + "ms");
26ms
21ms
这里我将第一项的数据从张三改为了张四,又改了回去;消耗的时间都是20多毫秒,时间有点长;这里使用的是工具包中的日期类型,可以获取当前的时间,这里和mysql中类似【之前出现的问题是忘记提交commit,还有就是sql语句写的有问题】
JDBC规范中,Connection的创建和销毁是最浪费时间的,
那么如何解决这个问题呢?
这里的问题就是比如每次进行修改,那么就会调用Dao的方法,改方法重新建立一个连接通道,并且修改之后就会关闭通道,这两个过程是最浪费时间的;
上面的通道就相当于是一次性拖鞋,买拖鞋和销毁拖鞋是最耗费时间的
其实可以联想有的宾馆的做法 : 开业的时候购置一批拖鞋,来了一个用户使用,用户走之后不销毁;然后下一个用户进来继续使用;直到宾馆停业的时候才会销毁拖鞋。
所以这里的思路使用ServletContextListener监听器;比如当应用被初始化的时候,就创建与数据库的20个通道,这样就是操作的时候,有空闲的就可以使用
开发中,最常用的集合就是Map;Map的键值对非常好用
package cfeng.oa.listener;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
import cfeng.oa.utils.DBUtil;
@WebListener
public class DBconnListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
Map connMap = new HashMap();
for(int i = 0; i < 20; i++) {
try {
Connection conn = DBUtil.getConnection();
System.out.println("新开辟了一个通道" + conn);
connMap.put(conn, true);
} catch (SQLException e) {
e.printStackTrace();
}
}
ServletContext application = sce.getServletContext();
application.setAttribute("connMap", connMap);
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
Map connMap = (Map)sce.getServletContext().getAttribute("connMap");
Iterator iterator = connMap.keySet().iterator();
while(iterator.hasNext()) {
Connection conn = (Connection)iterator.next();
if(conn != null) {
System.out.println("销毁通道: " + conn);
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
}
这里就是可以看到建立了很多通道
12月 27, 2021 4:58:27 下午 org.apache.jasper.servlet.TldScanner scanJars
信息: 至少有一个JAR被扫描用于TLD但尚未包含TLD。 为此记录器启用调试日志记录,以获取已扫描但未在其中找到TLD的完整JAR列表。 在扫描期间跳过不需要的JAR可以缩短启动时间和JSP编译时间。
新开辟了一个通道com.mysql.cj.jdbc.ConnectionImpl@42b6d0cc
新开辟了一个通道com.mysql.cj.jdbc.ConnectionImpl@750ff7d3
新开辟了一个通道com.mysql.cj.jdbc.ConnectionImpl@2620e717
新开辟了一个通道com.mysql.cj.jdbc.ConnectionImpl@2b34e38c
新开辟了一个通道com.mysql.cj.jdbc.ConnectionImpl@7fd26ad8
新开辟了一个通道com.mysql.cj.jdbc.ConnectionImpl@14b0e127
新开辟了一个通道com.mysql.cj.jdbc.ConnectionImpl@7cea0110
新开辟了一个通道com.mysql.cj.jdbc.ConnectionImpl@78010562
新开辟了一个通道com.mysql.cj.jdbc.ConnectionImpl@38aafb53
新开辟了一个通道com.mysql.cj.jdbc.ConnectionImpl@73010765
新开辟了一个通道com.mysql.cj.jdbc.ConnectionImpl@52169758
新开辟了一个通道com.mysql.cj.jdbc.ConnectionImpl@459b187a
新开辟了一个通道com.mysql.cj.jdbc.ConnectionImpl@7eae3764
新开辟了一个通道com.mysql.cj.jdbc.ConnectionImpl@7e744f43
新开辟了一个通道com.mysql.cj.jdbc.ConnectionImpl@6a4ccef7
新开辟了一个通道com.mysql.cj.jdbc.ConnectionImpl@70cccd8f
新开辟了一个通道com.mysql.cj.jdbc.ConnectionImpl@77ec6a3d
新开辟了一个通道com.mysql.cj.jdbc.ConnectionImpl@71d9cb05
新开辟了一个通道com.mysql.cj.jdbc.ConnectionImpl@36bf84e
新开辟了一个通道com.mysql.cj.jdbc.ConnectionImpl@25b52284
12月 27, 2021 4:58:28
然后正常关闭服务器,而不是非正常死亡
在Java 9上运行时,需要在JVM命令行参数中添加“-add opens=Java.base/Java.lang=ALL-UNNAMED”,以启用线程本地内存泄漏检测。或者,可以通过禁用ThreadLocal内存泄漏检测来抑制此警告。
销毁通道: com.mysql.cj.jdbc.ConnectionImpl@459b187a
销毁通道: com.mysql.cj.jdbc.ConnectionImpl@78010562
销毁通道: com.mysql.cj.jdbc.ConnectionImpl@70cccd8f
销毁通道: com.mysql.cj.jdbc.ConnectionImpl@73010765
销毁通道: com.mysql.cj.jdbc.ConnectionImpl@36bf84e
销毁通道: com.mysql.cj.jdbc.ConnectionImpl@7fd26ad8
销毁通道: com.mysql.cj.jdbc.ConnectionImpl@7eae3764
销毁通道: com.mysql.cj.jdbc.ConnectionImpl@52169758
销毁通道: com.mysql.cj.jdbc.ConnectionImpl@77ec6a3d
销毁通道: com.mysql.cj.jdbc.ConnectionImpl@25b52284
销毁通道: com.mysql.cj.jdbc.ConnectionImpl@2620e717
销毁通道: com.mysql.cj.jdbc.ConnectionImpl@14b0e127
销毁通道: com.mysql.cj.jdbc.ConnectionImpl@7e744f43
销毁通道: com.mysql.cj.jdbc.ConnectionImpl@2b34e38c
销毁通道: com.mysql.cj.jdbc.ConnectionImpl@38aafb53
销毁通道: com.mysql.cj.jdbc.ConnectionImpl@42b6d0cc
销毁通道: com.mysql.cj.jdbc.ConnectionImpl@7cea0110
销毁通道: com.mysql.cj.jdbc.ConnectionImpl@6a4ccef7
销毁通道: com.mysql.cj.jdbc.ConnectionImpl@750ff7d3
销毁通道: com.mysql.cj.jdbc.ConnectionImpl@71d9cb05
12月 27, 2021 5:01:53
这里可以看到,销毁的顺序和创建的顺序无关,因为使用迭代器只是保证了局部有序
重载JDBC工具类
这里为什么是重载而不是修改呢?因为设计的一个重要原则就是开闭原则对扩展开放,对修改关闭;所以这里就重在方法
public static Connection getConnection(HttpServletRequest req) throws SQLException{
Connection conn = null;
Map<Connection,Boolean> connMap= (Map)req.getServletContext().getAttribute("connMap");
Iterator it = connMap.keySet().iterator();
while(it.hasNext()) {
conn = (Connection)it.next();
if((Boolean) connMap.get(conn)) {
connMap.put(conn, false);
break;
}
}
return conn;
}
public static void close(Connection conn, Statement state,ResultSet result,HttpServletRequest req) {
if(result != null) {
try {
result.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(state != null) {
try {
state.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
ServletContext application = req.getServletContext();
Map connMap = (Map)application.getAttribute("connMap");
connMap.put(conn, true);
}
从底层向上层依次重载
public int editModifyStudent(String stuno,String stuname,String stuclass,HttpServletRequest req) {
Connection conn = null;
PreparedStatement state = null;
int count = 0;
try {
conn = DBUtil.getConnection(req);
conn.setAutoCommit(false);
String sql = "UPDATE student SET stuname=?,stuclass=? WHERE stuno = ?";
state = conn.prepareStatement(sql);
state.setString(1, stuname);
state.setString(2, stuclass);
state.setString(3, stuno);
count = state.executeUpdate();
conn.commit();
} catch (SQLException e) {
if(conn != null) {
try {
conn.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
}
e.printStackTrace();
}finally {
DBUtil.close(conn, state, null, req);
}
return count;
}
最后就是调用的时候,调用重载的方法
Date startDate = new Date();
StudentDao stuDao = new StudentDao();
int count = stuDao.editModifyStudent(stuno, stuname, stuclass, req);
Date endDate = new Date();
System.out.println((endDate.getTime() - startDate.getTime()) + "ms");
换了这种统一开发和关闭的方式之后,可以验证一下修改时间
新开辟了一个通道com.mysql.cj.jdbc.ConnectionImpl@71d9cb05
新开辟了一个通道com.mysql.cj.jdbc.ConnectionImpl@36bf84e
新开辟了一个通道com.mysql.cj.jdbc.ConnectionImpl@25b52284
12月 27, 2021 5:51:15 下午 org.apache.coyote.AbstractProtocol start
信息: 开始协议处理句柄["http-nio-8080"]
12月 27, 2021 5:51:15 下午 org.apache.catalina.startup.Catalina start
信息: [2306]毫秒后服务器启动
12ms
12ms
可以发现时间从之前的26ms变为了现在的12ms,时间提高了一倍不止
至于其他的操作是因为只是在DBUtil中封装了简单的这两个方法,如果将其他的封装了,应该还可以提高一些时间,毕竟这里任何操作都是需要时间的,只是时间长短的问题
之前已经放了一张图了,其他的监听器都是相同的用法,其中的时间就是在域的生命周期变化或者域属性变化的时候就会触发事件,按照理解,这里应该涉及了多线程
过滤器Filter
过滤器和监听器相同,都是Servlet规范下的一套接口,这套接口也存在于Tomcat-api中,Filter接口的实现类也是开发人员提供,Http服务器不负责提供
Filter接口是在Http服务器调用资源文件之前,对Http服务器进行拦截
- 拦截Http服务器,帮助Http服务器检测当前请求的合法性
- 拦截Http服务器,对当前请求进行增强操作
【上面的监听器开发就两步,如果使用注解就一步,直接new 一个weblistener即可】
Filter接口的实现类开发步骤为三步:
- 创建一个类实现Filter接口
- 重写Filter接口的doFilter方法
- 在web.xml中配置,注册过滤器
<filter>
<filter-name> </filter-name>
<filter-class> </filter-class>
</filter>
<filter-mapping>
<filter-name> </filter-name>
<url-pattern> </url-pattern>
</filter-mapping>
listener都放在listener包下,同理filter都放在filter包下
传入了三个参数,请求,响应,filterchain链;请求用于获取相关信息来作为拦截判断的依据,响应用于当拦截之后返回给客户端的响应,filterchain用于放行【将请求交给相关的资源】
@WebFilter("/shopping.html")
public class LoginFilter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
request.setCharacterEncoding("UTF-8");
String user = request.getParameter("user");
if("张三".equals(user)) {
chain.doFilter(request, response);
}else {
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
out.println("<h1 align=\"center\">拒绝访问</h1>");
out.println("<hr color=\"aquamarine\" />");
out.println("对不起,你不是张三,本页面只能张三访问");
}
}
}
这样就顺利拦截了不是张三的所有的用户,只有张三成功访问了
这里的页面也可以放出来
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>OA购物商城用户登录</title>
</head>
<body>
<h1 align="center">用户登录</h1>
<hr color="aquamarine" />
<form action="shopping.html" method="post">
<table >
<tr>
<td>用户名</td>
<td><input type="text" name="user"/></td>
</tr>
<tr>
<td>密码</td>
<td><input type="password" name="pwd"/></td>
</tr>
<tr>
<td colspan="2"><input type="submit" value="登录"/></td>
</tr>
</table>
</form>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>OA购物商城</title>
</head>
<body>
<h1 align="center">OA购物商城</h1>
<hr color="aquamarine" />
<table border="1px" width="50%" aligh="center">
<tr>
<th>商品名称</th>
<th>商品单价</th>
<th>商品评价</th>
<th>加入购物车</th>
</tr>
<tr>
<td>华为nova</td>
<td>4000</td>
<td>非常好用</td>
<td><a javascript:void(0) href="/Test/addCart?goodsName=HuaWeinova">加入购物车</a></td>
</tr>
<tr>
<td>洗发水</td>
<td>10</td>
<td>一般般</td>
<td><a javascript:void(0) href="/Test/addCart?goodsName=shampoo">加入购物车</a></td>
</tr>
<tr>
<td>坚果</td>
<td>30</td>
<td>好</td>
<td><a javascript:void(0) href="/Test/addCart?goodsName=nut">加入购物车</a></td>
</tr>
<tr>
<td colspan="4" align="center"><a href="/Test/viewCart">查看我的购物车</a></td>
</tr>
</table>
</body>
</html>
可以简单看一下结果
请求增强
Filter除了能够拦截请求之外,还能进行请求增强,可以对所有的请求进行处理;增强之后再转发
这里可以以一个例子,就是将所有的访问的请求的只会编码方式设置为UTF-8;不用一个一个去设置,可以直接使用一个过滤器,直接拦截对于该应用所有资源的访问
package cfeng.oa.filter;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
@WebFilter("/*")
public class CharsetFilter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
request.setCharacterEncoding("UTF-8");
chain.doFilter(request, response);
}
}
代码很简单;直接两行,第一行设置编码方式,第二行进行放行
Filter拦截地址的格式
命令的格式
<filter-mapping>
<filter-name></filter-name>
<url-pattern></url-pattern>
</filter-mapping>
命令的作用:
? 拦截地址通知Tomcat在调用何种资源的时候要启用该过滤器进行拦截过滤
这里的拦截地址的写法,有模糊的写法,还有*开头的
/Test/index.html 某一具体资源
/img
所以就是拦截的不同的形式
防止用户的恶意登录
发放令牌JsessionID
什么是用户的恶意登录? 也就是用户不登录,直接通过地址栏的方式,猜测服务器的资源,通过get的方式直接在地址栏访问,这造成了资源不安全的行为,早期解决的办法,就是给用户开辟一个私人的session;session之前操作的时候就知道是每一个正常的用户都会有的,所以用户如果是合法登录的,那么就给其创建一个session;然后目标资源位置就监测是否有sessionID
因此,真正使用空参的getSession只有登录的界面;其他的后台资源的判断都是使用的有参的false的getSession;这样才能真正起到令牌的作用
loginServlet {
if(可以登录) {
HttpSession session = req.getSession();
}
}
资源servlet
rrsourceServlet{
HttpSession session = req.getSession(false);
if(session == null) {
resp.sendRedirect("err.html");
return;
}
}
但是有一个问题,那就是每一个都要写这段代码;和设置请求中文不乱吗不同一样
还有一个重要的缺点就是令牌机制不能保护静态资源,只有之前的动态资源才使用几行代码拦截,但是静态资源根本就不能写,所以就有问题
第一个缺点就是重复书写代码,java的代码设计就是要提高代码的复用性,所以就不能重复在每一个servlet中都检查,所以就联想到过滤器可以;它可以拦截目标路径的请求,所以只要使用/*,和之前的charserFilter一样,就可以对所有资源访问的请求进行拦截,检查令牌
package cfeng.filter;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
@WebFilter("/shopping.html")
public class LoginFilter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest)request;
HttpSession session = req.getSession(false);
if(session == null) {
req.getRequestDispatcher("/err.html").forward(request, response);
return;
}
chain.doFilter(request, response);
}
}
这里就将所有的资源的访问都拦截了下来,并且验证了令牌,避免了重复的代码
但是有一个问题就是这里拦截了登录页面:happy:
全部拦截的解决
可以通过req获取到用户的请求中的URI,得到用户到底需要访问什么类型的资源
所以就首先判断访问的是否为login的资源,如果是,就无条件放行
package cfeng.filter;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
@WebFilter("/shopping.html")
public class LoginFilter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest)request;
String uri = req.getRequestURI();
if(uri.indexOf("login") != -1 || "/Test/".equals(uri)) {
chain.doFilter(request, response);
return;
}
HttpSession session = req.getSession(false);
if(session == null) {
req.getRequestDispatcher("/err.html").forward(request, response);
return;
}
chain.doFilter(request, response);
}
}
这里主要就是登录界面要无条件放行,这里的登录界面可能是欢迎页面,所以一共有两种情况是无条件放行的
这里就是用req获取到请求行中的URI,这个字符串中如果有login子串,那就放行,或者这个URI是欢迎页面的写法,只有程序的名称; 所以就是用equals方法和另外一个indexOf方法,index就是索引,就是子串出现的位置索引,没有就返回的是-1
|