转自:https://blog.csdn.net/gavin_john/article/details/51399425
提两个问题:
1.大家在访问某个网站的时候,往往都会看到网站的首页面显示您是第几位浏览者(网站计数器),这是怎么实现的?
2.我们在访问某个bbs网站的时候,往往会显示有多少人在线,这是怎么实现的?
可能我们会想到的常规实现思路:数据库或者文件。这种做法比较简单,但是却会对数据库或者文件访问过于频繁,开销比较大。
解决之道是用ServletContext
什么是ServletContext?
要理解ServletContext就必须和Cookie、Session做一个对比,如下图:你可以把它想象成一个公用的空间,可以被所有的客户访问,也就是说A客户端可以访问D,B客户端可以访问D,C客户端也可以访问D。
WEB容器在启动时,它会为每个Web应用程序都创建一个对应的ServletContext,它代表当前Web应用。并且它被所有客户端共享。
ServletContext对象可以通过ServletConfig.getServletContext()方法获得对ServletContext对象的引用,也可以通过this.getServletContext()方法获得其对象的引用。
由于一个WEB应用中的所有Servlet共享同一个ServletContext对象,因此Servlet对象之间可以通过ServletContext对象来实现通讯。ServletContext对象通常也被称之为context域对象。公共聊天室就会用到它。
当web应用关闭、Tomcat关闭或者Web应用reload的时候,ServletContext对象会被销毁
怎么使用ServletContext?
使用ServletContext:
(1) 如何得到ServletContext对象
this.getServletContext();
this.getServletConfig().getServletContext();
(2) 你可以把它想象成一张表,这个和Session非常相似:每一行就是一个属性,如下:
名字(name) | 值(object) |
---|---|
添加属性:setAttribute(String name, Object obj);
得到值:getAttribute(String name),这个方法返回Object
删除属性:removeAttribute(String name)
(3) 生命周期
ServletContext中的属性的生命周期从创建开始,到服务器关闭结束。
**一个快速入门的案例: **
我们创建Servlet1和Servlet2,Servlet1用于在ServletContext中创建属性,Servlet2用于从ServletContext读取属性:
Servlet1的doGet方法为:
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=utf-8");
PrintWriter out = response.getWriter();
// 获取ServletContext对象的引用
// 第一种方法
ServletContext servletContext = this.getServletContext();
// 第二种方法
// ServletContext servletContext2 = this.getServletConfig().getServletContext();
servletContext.setAttribute("name", "小明");
out.println("将 name=小明 写入了ServletContext");
}
Servlet2的doGet方法为:
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=utf-8");
PrintWriter out = response.getWriter();
// 取出ServletContext的某个属性
//1.首先获取到ServletContext
ServletContext servletContext = this.getServletContext();
//2.取出属性
String name = (String)servletContext.getAttribute("name");
out.println("name="+name);
}
以此访问Servlet1和Servlet2,我们可以分别看到输出如下:
运行结果似乎和Session,Cookie的应用没什么不同。其实看似相同,实则完全不一样。只要我们不关闭Tomcat或者reload该应用,这时候我们关闭当前的浏览器,或者是换一个浏览器,假设我们从Chrome换到IE再次访问Servlet2,依然可以看到结果!这就是它们最大的不同了,因为ServletContext是存在于服务器内存中的一个公共空间,它可以供所有的用户客户端访问。
ServletContext应用
(1)多个Servlet通过ServletContext对象实现数据共享
这个很好理解,类似于Session,我们也可以通过ServletContext对象来共享数据,但要注意的是,Session只能在一个客户端共享数据,它独占一个客户端。而ServletContext中的数据是可以供所有客户端共享的。
(2)实现Servlet的请求转发
之前我们学过的请求转发是通过request对象的:
request.getRequestDispatcher("/url").forward(request, response);
这里要说明的是,ServletContext也可以实现请求转发:
this.getServletContext().getRequestDispatcher("/url").forward(request, response);
这两个转发效果是一样的。
(3)获取Web应用的初始化参数
在【Servlet——开发细节+ServletConfig对象】中,我们介绍过在Servlet部署的时候,我们可以使用一个或多个
MyServlet
com.gavin.servlet.MyServlet
encoding
utf-8
可以看到它配置了一个初始化参数:encoding=utf-8,那么我们在MyServlet的源代码中需要这样去得到这个参数:
String encoding = this.getServletConfig().getInitParameter("encoding");
上述的参数配置方法只针对一个特定的Servlet有效,现在我们可以通过ServletContext来获取全局的、整个Web应用的初始化参数,全局的初始化参数是这样配置在web.xml文件中的:
name
gavin
然后我们可以在任意一个Servlet中使用ServletContext获取这个参数:
String name = this.getServletContext().getInitParameter("name");
**(4)利用ServletContext对象读取资源文件(比如properties文件) **
读取资源文件要根据资源文件所在的位置分为两种情况:
(1)文件在WebRoot文件夹下,即我们的Web应用的根目录下。这时候我们可以使用ServletContext来读取该资源文件。
假设我们Web根目录下有一个配置数据库信息的dbinfo.properties文件,里面配置了name和password属性,这时候可以通过ServletContext去读取这个文件:
// 这种方法的默认读取路径就是Web应用的根目录
InputStream stream = this.getServletContext().getResourceAsStream("dbinfo.properties");
// 创建属性对象
Properties properties = new Properties();
properties.load(stream);
String name = properties.getProperty("name");
String password = properties.getProperty("password");
out.println("name="+name+";password="+password);
(2)但是如果这个文件放在了src目录下,通过ServletContext是读不到的,必须要使用类加载器去读取。
// 类加载器的默认读取路径是src根目录
InputStream stream = MyServlet.class.getClassLoader().getResourceAsStream("dbinfo.properties")
但是如果这个文件此时还没有直接在src目录下,而是在src目录下的某个包下,比如在com.gavin包下,此时类加载器要加上包的路径,如下:
InputStream stream = MyServlet.class.getClassLoader().getResourceAsStream("com/gavin/dbinfo.properties")
另外,补充一点,ServletContext可以获取文件的全路径,当然这个也是在Web应用根目录下的文件。比如我们在WebRoot文件夹下有一个images文件夹,images文件夹下有一个Servlet.jpg图片,为了得到这个图片的全路径,如下:
// 如何读取到一个文件的全路径,这里会得到在Tomcat的全路径
String path = this.getServletContext().getRealPath("/images/Servlet.jpg");
在网站开发中,有很多功能要使用ServletContext,比如
- 网站计数器
- 网站的在线用户显示
- 简单的聊天系统
总之,如果是涉及到不同用户共享数据,而这些数据量不大,同时又不希望写入数据库中,我们就可以考虑使用ServletContext实现。
实际案例-网站计数器
在网站建设中,经常会统计某个网页被浏览的次数,那么这个网站计数器是怎么实现的?
怎样才算是一次有效的点击?各个网站有不同的标准:
- 只要访问过该网页,就算是一次,刷新一次也算,当然这是最简单的。不过这有点虚假的成分。
- 不同的IP访问该网页,算一次有效点击;如果是同一个IP在一定时间(比如一天),不管浏览该网页多少次都算一次
- 用户退出网站,再次访问也算一次
现在我们采用第3种来实现这个简单的案例。
首先,我们编写3个Servlet,Login、LoginCl和Manager,分别是登录表单,登录处理以及管理主页面,我们在用户登录成功之后将存在于ServletContext中的计数器加1,然后请求重定向到Manager页面,Manager页面显示网站的总访问次数。
Login页面的代码很简单,这里不再展示。LoginCl中我们根据用户的密码是否是“123”来判断,如下:
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=utf-8");
String passwd = request.getParameter("password");
if ("123".equals(passwd)) {
// 合法
// 向ServletContext添加属性
ServletContext servletContext = this.getServletContext();
int nums = 0;
try {
nums = Integer.parseInt((String) servletContext.getAttribute("nums"));
} catch (Exception e) {
nums = 0;
}
nums += 1;
servletContext.setAttribute("nums", String.valueOf(nums));
// request.getRequestDispatcher("/Manager").forward(request,response);
response.sendRedirect("/Counter/Manager");
} else {
// 非法
}
}
在Manager页面中取出nums这个计数值,如下:
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=utf-8");
PrintWriter out = response.getWriter();
out.println("管理页面
");
ServletContext servletContext = this.getServletContext();
String nums = (String) servletContext.getAttribute("nums");
out.println("该管理页面被访问了"+nums+"次");
}
1.注意点
我们为什么从LoginCl到Manager要用请求重定向sendRedirect而不用请求转发,这是因为当请求转发到Manager页
面之后,如果我们再次刷新Manger页面,相应的表单依然会提交一次。也就是说,我们没有退出重新登录,而只是在
Manager页面刷新就会让网站计数器nums的值加1。这显然是不符合要求的。
2.存在问题
问题:当服务器关闭后,我们的计数器就被清空了,那么如何才能够保证计数器的稳定增长呢?
解决:在服务器关闭之前,将存在于ServletContext中的计数值保存于文件之中,等下次服务器开启的时候再从文件中读出这个计数值,放入ServletContext中。
那么很自然的,我们想到了怎么才能在服务器开启和服务器关闭的时候去做这些工作呢?—> 要记得Servlet的生命周期,记得两个自动调用的方法init()和destroy()。
其中init()在这个Servlet初始化的时候调用,而destroy()方法在这个Servlet被销毁之前调用。那么我们就可以利用这两个方法来读取和保存我们的计数值。假设这个计数值保存在Web应用根目录下的record.txt文件中。
于是我们编写InitServlet代码如下:
public class InitServlet extends HttpServlet {
public void destroy() {
// 把ServletContext的值重新保存在文件中
ServletContext servletContext = this.getServletContext();
String nums = (String) servletContext.getAttribute("nums");
System.out.println("destroy.nums:"+nums);
String path = servletContext.getRealPath("record.txt");
FileWriter fileWriter = null;
BufferedWriter bw = null;
try {
fileWriter = new FileWriter(path);
bw = new BufferedWriter(fileWriter);
bw.write(nums);
bw.flush();
} catch (Exception e) {
e.printStackTrace();
}finally{
try {
bw.close();
fileWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 初始化方法
public void init() throws ServletException {
ServletContext servletContext = this.getServletContext();
// 1.首先得到该文件真实路径
String path = servletContext.getRealPath("record.txt");
// 2.打开文件
FileReader fileReader = null;
BufferedReader br = null;
try {
fileReader = new FileReader(path);
br = new BufferedReader(fileReader);
String nums = br.readLine();
// 把nums添加到ServletContext
servletContext.setAttribute("nums", nums);
} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭流,后打开先关闭
try {
br.close();
fileReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
然后,我们还要让这个InitServlet自动装载到服务器的内存,所以要配置load-on-startup元素,让其自启动:
InitServlet
com.gavin.InitServlet
1
最后,因为我们在每次服务器启动的时候自动装载了ServletContext,添加了nums属性,所以相应的LoginCl的代码也要修改,很简单,这里就不再赘述
最后的使用原则
因为存在ServletContext中的数据会长时间保存在服务器,会占用内存,因此我们建议不要向ServletContext中添加过大的数据!