Servlet---ServletContext对象

转自: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部署的时候,我们可以使用一个或多个<init-param>标签为servlet配置一些初始化参数,然后我们通过ServletConfig对象获取这些参数,假如有如下的MyServlet,它的配置为:

<servlet>  
    <servlet-name>MyServlet</servlet-name>  
    <servlet-class>com.gavin.servlet.MyServlet</servlet-class>  
    <init-param>  
        <param-name>encoding</param-name>  
        <param-value>utf-8</param-value>  
    </init-param>  
</servlet>

可以看到它配置了一个初始化参数:encoding=utf-8,那么我们在MyServlet的源代码中需要这样去得到这个参数:

String encoding = this.getServletConfig().getInitParameter("encoding");

上述的参数配置方法只针对一个特定的Servlet有效,现在我们可以通过ServletContext来获取全局的、整个Web应用的初始化参数,全局的初始化参数是这样配置在web.xml文件中的:

<!-- 如果希望所有的Servlet都可以使用该配置,则必须这么做 -->
<context-param>
    <param-name>name</param-name>
    <param-value>gavin</param-value>
</context-param>

然后我们可以在任意一个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,比如

  1. 网站计数器
  2. 网站的在线用户显示
  3. 简单的聊天系统
    总之,如果是涉及到不同用户共享数据,而这些数据量不大,同时又不希望写入数据库中,我们就可以考虑使用ServletContext实现。

实际案例-网站计数器

在网站建设中,经常会统计某个网页被浏览的次数,那么这个网站计数器是怎么实现的?
怎样才算是一次有效的点击?各个网站有不同的标准:

  1. 只要访问过该网页,就算是一次,刷新一次也算,当然这是最简单的。不过这有点虚假的成分。
  2. 不同的IP访问该网页,算一次有效点击;如果是同一个IP在一定时间(比如一天),不管浏览该网页多少次都算一次
  3. 用户退出网站,再次访问也算一次

现在我们采用第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("<h1>管理页面</h1>");       
    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元素,让其自启动:

<servlet>
        <servlet-name>InitServlet</servlet-name>
        <servlet-class>com.gavin.InitServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
 </servlet>

最后,因为我们在每次服务器启动的时候自动装载了ServletContext,添加了nums属性,所以相应的LoginCl的代码也要修改,很简单,这里就不再赘述


最后的使用原则
因为存在ServletContext中的数据会长时间保存在服务器,会占用内存,因此我们建议不要向ServletContext中添加过大的数据!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,711评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,079评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,194评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,089评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,197评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,306评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,338评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,119评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,541评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,846评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,014评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,694评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,322评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,026评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,257评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,863评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,895评论 2 351

推荐阅读更多精彩内容

  • 她走出经理室,背对着大厅抹泪,却看见玻璃门晃出熟悉的身影,正冲她招手。她转过身,是南林,兴润公司的老总,不久前,他...
    shelley梅芷阅读 179评论 0 0
  • 晨间的寒风如刀刃般,拂过脸庞,仍有几分生疼,时间悄无声息的匆匆划过,转眼26个春秋已过,理想状态下这是人生1...
    江霞Eva阅读 175评论 0 1
  • 春到花自开 花开蝶自来 蝶去花儿落 落花暮春埋
    秋AldrichB果阅读 374评论 1 10
  • 一个人走在这喧嚣的街头,街角的音响店播放着陈奕迅的《十年》,那熟悉的旋律,那伤感的歌词,总会让人感触...
    念安思君阅读 920评论 4 16
  • 受时间、空间、经历、文化、认知等的局限,每个人都有偏见,即或多或少的“自以为是”,也就是狭隘的“我以为……我认为…...
    _德成阅读 175评论 1 5