Servlet4

回顾

重定向

  • Servlet3 中,介绍了重定向,以及这个重定向,它经典的使用场景是什么,早先呢,重定向,主要是解决互联网上,网站和网站之间跳转的问题,也举了例子,比如说,我们通过浏览器,访问百度,我从百度上呢,搜XXX,我也经搜到了达内的很多的结果,包括,XXX的官网,北京XXX等等,那么,我们所获得的这个网页,是百度给我提供的,这里面的每一个超链接呢,都是百度生成的,那事实上呢,这个超链接,它并没有呢,直接链到达内去,它其实呢,链的是百度,然后呢,我们点这个超链接的那一刻,它其实访问的是百度的服务器,然后呢,百度的服务器悄悄的,把这个你点击的这件事收集了一下,采集了一下,它记录下来,那么它记录我们个人喜好呢,是为了将来呢,向我们定向的投放广告,但是呢,这件事呢,就是说,悄悄的,它不想让你知道,最终呢还是,会让你访问到,你想访问的目标。那要想访问目标的话,需要呢,让这个程序,让这个请求啊,跳转到XXX的这个服务器,那么百度的服务器,不能直接访问XXX的服务器,因为这是两家公司,两个不同的软件,那怎么办呢,重定向就可以解决问题。
  • 那么所谓的重定向呢,是这样的一种意思,是服务器给浏览器一个建议,建议浏览器呢,自己去访问某人,访问某个服务器,那么浏览器呢,访问谁都可以,这样的话呢,通过这种方式,就可以呢,让两个服务器之间实现跳转,那这种使用场景,是重定向最经典的使用场景,后来呢,大家发现呢,这个很好用啊,就连互联网上两个不相关的这个项目,都能互相访问,那么同一个项目之内,就更能访问了,所以呢,后面也经常这样用,就是说,我们经常呢,在一个项目之内,然后呢,两个独立的组件之间跳转,也用重定向,这样的话呢,可以保证,这两个组件的独立,让它们之间呢,没有耦合度,这样呢,是便于呢,这个程序的维护。
  • 那么重定向,它的本质啊,其实就是服务器向浏览器发出一个特殊的响应,这个响应当中呢,包含一个特殊的状态码302,还包含了要访问的目标,如果你不写重定向那句话,你直接response.setStatus(302);response.setHeader("location","url");,你自己去设置这个编号,设置这个目标,也可以,但是呢,它给我们提供了这个简便的方式,我们用就是了。
    在这里插入图片描述

访问路径

目录结构

  • 下一个话题访问路径,那么访问路径,对我们这个以后呢,开发项目,影响呢,这个比较深远,所以说,我们说的呢,比较长,也是希望呢,大家对访问路径,能彻底理解,能彻底掌握,这样的话,以后我们做项目,遇到问题时呢,好解释啊。就怕什么呢,怕这个大家不理解,我们一次呢,没有讲透,后面的话,还经常会出现问题,因为我们将来呢,做项目,或者我们工作时呢,路径会越来越复杂,那么在复杂的时候,你能够把它搞清楚,所以,从这个根上要理解,只有理解透了,你才容易搞清楚。那再一个呢,不要小瞧路径啊,有人说,不就是个路径么,不要小瞧它啊,那将来我们在讲项目时呢,我们会深度啊,再挖掘一下, 这个路径背后的内容,其实,通过路径,你理解路径,能够理解很多本质,能够理解呢,这个怎么说呢,一个软件,或者说一个请求,它执行的一些细节,当然目前,我们还没讲太细的东西,还不够细,那一些细节,我们后面会讲,先别着急。
  • 那么讲路径之前呢,我们探讨了一下,那项目部署啊,是什么过程,因为呢,咱们所说的路径和这个项目部署,还是有关系的,那我们平时啊,所写的代码,在开发工具中写代码是源代码,源代码呢,存放到了workspace里,那这个代码是不能够直接运行的,我们所写的web项目的代码,必须呢,要在服务器中运行,所以代码呢,必须要进行部署。那么部署的代码是存放到了服务器里,tomcat里。那么部署呢,分为若干步,那么,在没有部署之前,代码是要编译的,那什么时候编译呢,我们一保存就编译,自动,如果你把开发工具上的开关啊,去掉了,那么它就不会自动编译了,你得手动编译,那就麻烦了,所以我们一般一定是勾选,这个要注意。
    在这里插入图片描述
  • 然后呢,编译的代码存放到了target之下,那正式代码放到了classes里,测试代码放到了test-classes里。然后呢,部署的时候,eclipse里呢,是把这个项目有所取舍,有的内容保留,有的不要了。然后呢,copy到tomcat之下,那实际上呢,eclipse呢,copy的是webapp,这个目录,其它的不要,就不是整个项目都copy,它只copy这个webapp, copy过去以后呢,立刻将其改名,改名为项目名,EmpManager,那我们看到tomcat下这个项目里,有的是WEB-INF ,web.xml,和WEB-INF平级,有html,是这样的内容。然后呢,还不够,那么这个目录结构之内呢,还缺少java代码,那eclipse呢,紧接着,它会把咱们源代码中编译后的文件,classes拷贝过去,而对于源文件,java文件,就丢弃不要了,因为不需要;再一个呢,对于这个测试代码不要了,其实也很容易想,就是我们开发完这个项目以后啊,我们给用户安装时,其实用户只需要,这个配置文件,静态页面和.class文件,它不需要java代码,而且一般我们给用户开发项目时呢,我们也不会给他这个java源文件,你把它都给他的话,那有的时候,就签约时啊,会要求说,你把java代码也给我,我将来自己维护,如果它不特意要的话,是不会给他的,给他以后的话,以后这个生意不好谈了。
  • 总之呢,经过3步以后,这里面的结构呢,就初步具备了,那然后呢,还有一点,这个我们在开发时,所导的包,那tomcat呢,运行时也是需要的,但javaee它不需要,因为它自带了,其它的,它需要,那我们导的包呢,这个eclipse呢,会把它放到这个lib目录下,而lib,在WEB-INF之内,那eclipse呢,会帮我们自动创建一个lib,把包放到这里来,但前提是什么呢,我们这个包是用maven引入的,就eclipse一看啊,maven引入的包,它会管,它会把这包呢,copy过来,反过来呢,如果你是手动导入的,这个是不标准不规范的,那么eclipse呢,是不会管,不会copy过来,所以啊,你课后回去练习的时候,一定要注意,手动导入的包,你要自己在这建个lib,放进来,这个一定要注意。所以呢,我们在开发web项目时有人,这样会感觉很奇怪,哎,我在本地写完代码了,我写了个方法进行了测试,比如测试了一个Dao,测了什么什么东西,哎,可以啊,一部署以后,哎,怎么就不行了呢,就报错了呢,说这个Dao有问题了呢,为什么呢,很多时候就是,你是手动导入的包,这个包没有部署过来,因为你心里要有这件事啊,就是说,我们写的代码和部署代码之间,有这样一个关系,这4步,哪一步有偏差,就会导致呢,运行结果有问题。

访问路径

  • 然后啊,我们平时啊,所说的访问路径啊,它都是指的是,部署代码的路径, 那么部署代码的这个路径的规则,就两点,第一点是对于静态资源,静态资源,什么是静态资源呢,除了Servlet以外都是静态资源,像网页啊,图片啊,以及网页所以来的css,js啊,或者是其它的文件,比如说word啊,excel啊,这些内容,它们都是静态资源。那么静态资源呢,它访问路径就是,它在服务器上存放的位置,而动态资源不同,动态资源Servlet,不是存放的位置,它在web.xml里呢,配置的路径。好,那么这个访问路径呢,咱们怎么获取呢,也演示了,有4个方式,第一个方法呢,是res.getContextPath(),得到的是项目名,第2个呢是,getServletPath(),得到的是Servlet访问路径,第3个是getRequestURI(),然后呢getRequestURL(),得到的是URI和URL ,那么前两者呢,没有什么歧义,而URI和URL从名字上,这个有所接近啊,所以我们讲了这两者的区别。
  • 有的时候呢,这个面试时也会问到这个话题,但一般都是笔试,简单的一道题,你按照你的理解去回答就好了,那我们最终呢,要记住一个广义的理解,那么这个程序的设计者,他在设计之初啊 ,它认为URI,它的本意啊,URI是一个资源的,网络上一个资源的名,而URL呢,是资源的真名。名字可以包含真名,所以URI是包含URL的,就是说,它和我们狭义的理解,和我们直接看到的表面现象,不是一样的,所以这块特意强调一下,然后呢,我们一般平时说这个路径,我们说URI,或者是URL都行,其实平时说的话,有的时候,喜欢说URI,有的人喜欢说URL,这个都可以啊,但是说面试提问的话,你要知道就行了。
  • 然后,这个Servlet的访问路径,是可以有多种配置方式的,3种,我们讲了这个配置方案,第一种方式啊,我们称之为精确匹配,斜线,比如说,/hello,那你这样配置的话呢,我们就必须呢,通过/hello,访问这个Servlet,这个Servlet呢,也只能处理hello这一个请求,所以此时啊,这个组件,处理请求的能力是很有限的,是单一的,那么如果是我们采用这种方式去做项目,一个请求得写一个组件,那我们做项目的话,可能是几百个,上千个请求,这个组件太多了,不好管理。所以呢,我们一般,稍微有点规模的项目,不会这么干,而后两种方式呢,会增强这个组件处理请求的能力,第二种方式呢,写的是斜线星,/*,星呢,是通配符,代表一切,表示说,任何路径都能访问这个组件,这个组件呢,能够处理一切请求 ,处理能力是很大的,但是呢,它有一个缺点,它不是很灵活,对吧,就必须是一个组件处理所有请求,我想再写两个组件呢,还不行,明白吧,不方便。
  • 然后呢,第3个,是后缀,比如说,*.abc,那意思呢是,以abc为后缀的请求,可以访问这个组件,这个组件能够处理这个后缀相关的请求,是多个啊。那么,具体能处理多少个,就看我们的这个,后缀的写的方式了,我们可以呢,让我们的软件当中,每一个请求后缀都是abc对吧,能处理一切请求,我们也可以呢,让我们的软件当中呢,一个模块一种后缀,这样呢,一个组件处理一个模块的请求,也可以,也可以这样,就是我们这个软件当中呢,绝大部分普通的,增删改查的请求,我们都是比如说.do,然后呢,有特殊的功能,比如说,发邮件啊,比如说这个发短信啊,这样特殊的请求,我们改一个特殊的后缀,然后呢,项目中有几个组件也可以。总而言之呢,这个不同的企业,可以根据自己的实际情况,灵活的运用这个后缀,所以,后缀的方式呢,其实是一个最优的方式,又具有灵活性,处理能力呢,又比较强,所以建议呢,用第3种。
  • 然后呢,那如何利用,这两种情况,去解决问题呢,就是如何使用一个组件处理多个请求,我们想用一个组件处理多个请求啊,我们在这个组件之内,要写出这样的代码,我们首先呢,调用get方法,得到路径,然后呢,对路径加以判断,如果是find,我就查询,如果是add,我就增加,那我们这个判断的依据,或者说前提从何而来呢,来源于开放规范啊,我们在开发这段代码之前,必须有人先写好开发规范,按照规范,去做这个判断,是这样的。那当然了,有的企业比较正规,它会有这个规范,有的企业呢,不是很正规,它通常是口头的,一说完了,没有明确的规范,但无论有没有那个文档,这个规范,无论它是在脑子里,还是在纸上,那它最终呢,是得有的啊,否则,这个代码没法写啊。

CMMI指标

  • 那一个企业呢,它开发软件是否正规,那依据是什么呢,你就看那个企业,它这个有一个指标叫CMMI,一个国际的软件开发成熟度的认证,5级是最高的,4321,越来越低了,如果说这个企业,CMMI5,有这个资质的话,说明它是比较规范的,因为那个资质里要求,它有很多规范文档,以前我所在的企业都是有这个样的规范的。好了,CMMI,具体啥意思,我也不知道,翻译过来叫企业软件成熟度,就企业软件开发成熟度,就你这个企业开发软件的能力,明白这意思吧,如果你想接一些个,怎么说呢,一些大企业,或者是大公司的项目,你得有这个资质,没有这资质,人家不让你承接,明白吧,你说我自己成立个公司,就俩人,我接个那个大项目,不可能的,你得有那个资质啊,好,这个有点扯远了啊,说到这了啊,规范是有标准的啊。
  • 然后呢,我们按照规范,这个处理完路径以后,这个用户在访问时,只要路径是对的,就能处理,路径是错的,就不能处理,然后呢,你要注意,这里我还强调一下,我们在使用软件时,往往是,只有首页,我们需要记住一个路径对吧,敲进去,然后呢,进到首页之后,以后的操作,都是点按钮,点超链接对吧,增加啊,点增加啊,点这个这个修改啊,点保存啊,都是点按钮,那从此以后,是不会再敲路径的,我们访问淘宝,访问百度,访问任何软件都是这样啊,所以呢,你不要养成习惯,说我们访问任何功能都敲路径,不是那样的,然后呢,如果是用户瞎敲路径,往往是错的,是处理不了的。那当然了,我们这样写这个代码,那在配置时呢,你要么选择第二种方式,要么选择第3种方式,看情况,咱们将来呢,做的电信计费项目,我们会采用第3种方式。
  • 好了,总之啊,这是路径相关的内容,那路径这个话题,很多人一下,这个理解不透,或者说现在感觉理解透了,我们做项目时,你看吧,还有很多问题,那将来我们做项目时,遇到问题再去解决,遇到问题,再去回顾,再去演练。

前言(Preface)

  • 从Servlet1到Servlet的内容,是Servlet一些基本的理解和应用,它还有一些深层次的原则原理,或者说特性,那这些内容的话,不是说必须要使用的,但是如果说,你想在工作中有一个更好的发展,能够做出一些,别人可能做不出来的东西的话,这些内容需要掌握,就是说它能够做一些高级的东西,高级的这个业务。总而言之吧,你想在工作中解决一切问题,这些内容都得掌握,如果你只想解决常规的一些问题,这些内容,那不掌握也可以,但还是最好还是掌握,因为面试的时候,你会发现,就是常规的问题,谁都知道的问题, 面试官是不屑于问的,面试官一般都是项目经理,或者是技术的负责人,它一般都是问他最近在工作中遇到的一些,他印象深刻的问题,或者是呢,在他的职业生涯里,他所遇到的一些个困扰的问题,他看看你是不是能够,很好的能够解决这个问题,都是这样的,所以这些内容,在面试时都是容易问的。

1.Servlet的生命周期

  • 那第一个话题是Servlet的生命周期,那么Servlet,它是一个对象,对象有创建的时候,有销毁的时候,就是对象和人一样,有出生的时刻,有死亡的时刻,所以我们说一个对象生命周期讲的是什么呢,讲的是这个对象,从生到死的过程,不过呢,对象比人简单多了,人的从生到死,这个时间太长,中间发生的事太多了,没法一一列举,对象不同,对象做的事,就是做的方法是有限的。那么Servlet的生命周期,就是它从生到死,它的方法调用的顺序,它的方法处于哪个阶段,在哪个阶段被调用的,是这样一个话题。那么这个话题在将来解决某些问题的时候会有所应用,但我们现在只探讨这个规则。一些书上呢,对Servlet生命周期的说明是很凌乱的,就感觉很头疼,乱糟糟的一大堆,全是字,一看就烦了,就不想看了。
  • 那画个图来说明这个话题,那Servlet的生命周期,就是这个对象的生命周期,那这个对象的生和死,由谁来负责,由谁来管理,由java,倒也是,说的有点大,能不能再明确一点啊,有人说request和response,是将数据传给Servlet,它们怎么能管理Servlet呢,是谁呢,是服务器。在Servlet2/Servlet的运行原理图 中,这个图明确告诉你,Servlet由谁来new,由谁来调,是不是由tomcat,它是由tomcat来创建,调用的,那显然,这个对象由tomcat管理,得从这个图中悟出这个道理来,你得有领悟力,这程序员需要悟性,像这个武林高手一样,你得会悟,你要悟不出来的话,你只能学个皮毛,你要悟得厉害的话,这一下,九阳神功就会了,那得悟。所以说,通过这个图就很直观的告诉你了,就是Servlet对象是由tomcat管理的,那Tomcat有个名字叫什么呢,叫Servlet容器,容器里面装的是Servlet,是管理这些东西的。所以说,这个Servlet由tomcat进行管理。
  • 现在画一个大的方块代表tomcat,标一下,这就是tomcat,但是我说tomcat,有点这个狭隘了,其实应该说的是什么呢,服务器,因为服务器不止是tomcat,我们当前用的是tomcat,当然说这个也可以吧,好理解(具体化方便理解,抽象化更加客观)。那Tomcat,它有这个通信组件,其实管理这个对象的时候,很多时候是要是依赖于这个通信组件的,另外呢,tomcat,它有这个启动和停止的命令,startup和shutdown,Tomcat管理Servlet和这个命令是有关系的。那我们就以Tomcat为例,在tomcat之内,探讨一下Servlet是怎么被它管理的。默认的情况下,我们第一次去访问Servlet,那么Servlet呢,会被实例化,比如说这是helloServlet,默认情况下,我第一次访问这个Servlet,Tomcat就会实例化它,但是呢,这件事可以加以改造,我们说改造以后的这个情况,因为改造以后的情况呢,比较容易理解。
  • 那我们改以后可以这样,我们一启动tomcat,就让tomcat实例化这个Servlet,那所谓实例化,就是new呗,我们可以这样,把它改为什么呢,一启动tomcat,tomcat就new这个Servlet,那怎么改,后面会演示,而且呢,tomcat不但去new它,还会调用这个Servlet的一个方法,这个方法是从父类继承过来的,这个方法是什么呢,不是service,要是一启动,就调service,你要处理什么业务,一启动就处理业务,哪有这么快,调的是init()方法,init啥意思啊,初始化,所以这个方法是用来给Servlet初始化一些数据的,那具体怎么用,后面会说。总而言之,就是我们可以一启动tomcat,就让它去new,Servlet,就让它调用它的init方法,init方法从父类继承过来,一启动可以这样,后面会演示。然后呢,什么时候去调用Servlet呢,是用户发出请求访问的时候,用户发出请求要访问Servlet,那么tomcat就调Servlet,而Servlet呢,会给服务器做出响应,是这样的。
  • 那咱们这个调用的话,是不是可以调多次,它是不是可以调任意多次,可以调多少次都行,可以调n次,标一下n次,都可以。然后再看,那还有一点,就是当我们shutdown这个服务器时,那么,服务器会调Servlet的另外一个方法,那个方法叫什么呢,叫destroy(),销毁的方法,那调这个方法的目的是为了销毁Servlet,会让这个Servlet里面呢,释放一些资源,那这个发方法怎么用,后面会演示。总而言之,Servlet是一个对象,那么,这个对象之内,有很多方法,然后呢,它这个方法的调用顺序是这样的,Tomcat一启动,startup,它就立刻new Servlet,所以new是第一个被执行的,构造器是最先调用的;然后呢,new完以后,Tomcat会立刻调它的init方法,这个方法从父类继承过来,自动调用,这是Tomcat管理Servlet组件的手段;然后呢,用户访问服务器中的Servlet时候呢,Tomcat会调用Servlet的service()方法,这里会调的是service方法,标一下,这个是第3个,服务器调用Servlet;然后呢,Tomcat在shutdown时,它会调Servlet销毁方法,销毁方法呢,也是从父类继承过来的,这是第四个,shutdown命令时,调用destroy()。这是tomcat管理Servlet对象的手段,它会自动的调这个对象的方法。
    在这里插入图片描述

2.Tomcat管理Servlet的意义:单例模式

  • Tomcat管理Servlet对象时会自动的调这个对象的方法,那调这个方法的意义是什么,就是下面探讨的话题,Tomcat保障了Servlet单例模式的实现。Servlet这个对象的生命周期,主要是这个对象它的这几个方法,new(),init(),service(),destroy(),的调用的顺序,那么我们在写代码的时候,有的时候呢,需要依赖于这个顺序,要理解这个顺序是什么。那么,Servlet这个对象,默认情况下是这样的,默认的是我们第一次调用它,tomcat就实例化它,但还有一种情况是,一启动就可以实例化,都可以;那么,如果你想tomcat启动时,实例化这个Servlet对象,我们需要加以配置。那按照这种(非默认)方式画,是为了好解释一件事,什么事呢,那我们启动tomcat,我们能启动几次,一次,tomcat只启动一次;那HelloServlet是不是只new了一次;所以呢,这个HelloServlet,或者说某一个类型的Servlet,它在tomcat之内的实例只有一个,它只有一个实例,所以是单例的,单例的对象就是单个实例的对象。
  • 那么因为,第一步new只执行一遍,那第二步init()方法和第4步destroy()方法,也是一次,只执行一遍;总之,某一个类型的Servlet,它是单例的,单个实例的;而第3步service()方法是反复调用的,是这样的;所以就是,如果把这几个方法做一个比喻的话,那new就是,这个人就出生了,而init(),满月了,这个service(),工作了,最后destroy(),挂了。那我们把这个逻辑概括一下,那第一个,默认情况下,第一次访问Servlet时,tomcat会实例化它,默认情况下是这样;那这件事可以改,也可以改为tomcat启动时,就实例化Servlet。那总体来说,这个第一步new(),第2步init(),第4步destroy(),只执行一次;那因为第一步new只执行一次,那所以这个Servlet对象只被创建一次,所以这个某类型的Servlet,只实例化一次,那么它是单例对象,单例指的是单个实例,那单例其实是一种设计模式。
  • 那么如果面试官问你,你懂不懂单例模式,你说我确实没写过,但是我了解,这个Servlet就是单例的,它怎么单例的呢,tomcat保障的,因为tomcat只创建一次。你可以这么说,虽然单例模式没有写过,但是呢,Servlet我懂,你可以把话题搞这上面去。但其实不是这样的,其实,咱们以前开发的时候,是写过单例模式的,未必是标准,但是写过的,你比如说我们以前学jdbc的时候,我写过那个DBUtil,DBUtil它主要提供一个方法是getConnection,用来创建连接,所以,这个类其实呢,它基本上是满足工厂模式的,就它是一个工厂,一个用以提供连接的工厂,所以它是满足工厂模式的。而在这个类当中,我们是读取参数,读完参数以后,我们是实例化了一个连接池,连接池是不是只实例化一次呢,而且那个连接池还是私有的,别人还改不了,所以DBUtil中,那个连接池的那个变量就是单例的,或者说之前我们没有连接池的时候,我们创建几个,那个参数,什么driver啊,什么url啊(Driver可以理解为某个数据库的驱动,认为是单例的,但是这个url怎么是单例的呢,没有这个类啊?是个疑问?可能老师说错了。),这都单例的,所以它里面的那个参数是单例的。

3.演示Servlet生命周期

  • 总而言之呢,其实,工厂模式也好,单例模式也好,其实我们都写过,只是没在意而已。这是Servlet,它的一些个基本原则吧,那么下面我们写一个案例来演示一下,看是不是这样,是不是一启动,就能new,一启动就能init,一关闭就能destroy。打开eclipse,我们再新建一个项目,这个项目呢,叫servlet4,打包方式war包,新建个项目,Target Runtimes/Apache Tomcat v7.0,不要忘记导包(依赖tomcat本身javaee的包)啊。创建项目导包,完成以后,我们在,Java Resourcessrc/main/java的下面,建个包,准备要写个Servlet,那包名还叫web,都养成习惯了,然后呢,web之下,随便写一个Servlet,主要演示一下它的生命周期,演示一下它里面的方法的调用的顺序。那我创建这个Servlet,取名叫什么呢,叫DemoServlet,就一个小例子,没什么具体的含义。总之创建DemoServlet继承于HttpServlet。

Servlet实例化:JavaBean规范之一无参构造器

  • 那么这个Servlet当中呢,它不仅仅是只有service方法,它还有其它的内容,比如说,它可不可以有无参构造器呢,可以有无参构造器,那我给它加一个无参构造器,我们要看一下,这个无参构造器的调用时刻(默认就有无参构造器,但自己写里面可以做些处理),加一个无参构造器给它啊,然后呢,写一句话给出提示,无参构造器里面输出了一句话,程序执行时,我想看这句话在什么时候输出的,从而验证一下这个图,这个1234的顺序是不是对的。

package web;

import javax.servlet.http.HttpServlet;

public class DemoServlet extends HttpServlet {
	public DemoServlet() {	
		System.out.println("实例化DemoServlet");
	}
}

  • 那想一下我这个是无参构造器,我能不能把这构造器加参数,这个是不能的,为啥呢,因为这个Servlet由谁去调用,是我们自己去调,如果我们自己调,我们想传什么参都可以传,但不是,这个对象是由tomcat调用的,tomcat比较傻,它只会调无参构造器,tomcat只会调无参构造器,所以呢,这也验证了一点,就是javabean要满足4个规范,第一个它得有包,第二个它得有无参构造器,为啥javabean要有无参构造器呢,因为有些,比如说服务器,有些框架,在帮你调用这个对象的时候,它只会调无参构造器,是因为这个原因,如果说你没有无参构造器,它调不了了。

Servlet的初始化,销毁,调用,配置与测试验证

  • 除了这个构造器以外呢,还有什么,init方法,除了init之外,还有destroy方法,我们把这两个方法呢,也写一下。那这两个方法,是在父类中声明的,是继承过来的,我们可以重写,那回到程序中来,右键,Source,Override,在弹出框中去找那两个方法,(在java类中 Source/Override,可以直接看到这个这个类,Override重写的方法,所继承的所有父类),一找父类HttpServlet中没有,父类中没有,看它的爷爷,GenericServlet,它爷爷里面有,然后看 这里面,init方法有俩,一个带参,一个不带参,那用哪一个呢,其实都行,就看你要不要那个参数,如果你想用这个参数就带上,不想用就不带,这个 问题都不大 ,那我们可以勾上,用不用再说,先带上,我先要着。选择这个带有参数的init(ServletConfig),另外同时呢,我们把这个 destroy(),也勾上算了,两个方法都重写一下,我们勾选了这个 init,还有这个destroy方法,然后呢,ok。
    在这里插入图片描述
  • 然后这两个方法,稍微整理一下 ,这个注释不要了,然后还有,那这个方法之内,以前写service的时候,我们会把这个super.service();删掉,那注意destroy和init,其他的方法,这句话就别删了,只有那个 service方法的super.service(...);,那句话,那里面抛了异常,如果 你不删的话,它抛个异常,使用不了,而其他的方法没抛异常,是有用的,这就别删了。那么除了这两个方法之外,还有一个方法就是service(...);,处理请求 的方法,我们再把这个方法也写一下,再重写父类的service方法:
    在这里插入图片描述
  • 这个service方法之内,这个代码都不要,那目前这个Servlet,已经把它的关键的4个方法都加上去了,都重写了,那每一个方法,我们分别输出一句话,好观察它们的执行顺序,那之前呢,构造器已经写过了,那销毁也写一下,输出一句话,说销毁DemoServlet,System.out.println("销毁DemoServlet");,同样的,init方法也加一句话,说初始化Servlet,初始化DemoServlet,写明确一点,System.out.println("初始化DemoServlet");,然后,在service里呢 ,也输出一句话,就是调用DemoServlet,System.out.println("调用DemoServlet");,那么这个service方法,就不做出响应了,因为我们只是呢,利用这个案例演示一下,这个方法的调用顺序,不输出响应也没关系,浏览器得到一片空白,没关系,那么这4个方法写完以后,我们把这个类呢,配置好,然后呢,执行一下,看下输出的顺序:

package web;

import java.io.IOException;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class DemoServlet extends HttpServlet {
	public DemoServlet() {	
		System.out.println("实例化DemoServlet");
	}

	@Override
	public void destroy() {
		super.destroy();
		System.out.println("销毁DemoServlet");
	}

	@Override
	public void init(ServletConfig config) throws ServletException {
		super.init(config);
		System.out.println("初始化DemoServlet");
	}

	@Override
	protected void service(
		HttpServletRequest req, 
		HttpServletResponse res) throws ServletException, IOException {
		System.out.println("调用DemoServlet");
	}
}

  • 那么打开这个配置文件,打开以后,对这个Servlet加以配置,因为 呢,这是个小案例,小案例的话,就不用大动干戈了,在配置访问路径时,就按照第一种方式,精确匹配的方式来配,这个怎么简单怎么来,将来我们做项目时,再用第2,第3种方式来配。

  <servlet>
  	<servlet-name>demo</servlet-name>
  	<servlet-class>web.DemoServlet</servlet-class>
  </servlet>
  <servlet-mapping>
  	<servlet-name>demo</servlet-name>
  	<url-pattern>/demo</url-pattern>
  </servlet-mapping>

  • 配置完以后呢 ,就可以测试了,把这个项目呢,再部署一下,部署完以后,我们启动Tomcat,启动的时候,看一下控制台,控制台里有没有什么输出呢,看一下有没有我们写的这几句话,输出了那几句话呢,并没有,那有人就想了,哎,你刚才说的不对,你不是说了吗,咱们startup一启动,就会new,那如果一new,肯定会输出那句话,实例化Servlet它,那为啥没new呢,注意,我还强调了,我说默认情况下,并不是这样,默认的话,是第一次访问Servlet时,才实例化它,才调用的,所以,咱们现在没有做特殊配置,是默认情况,那我们一访问Servlet,它就会实例化,就会调用,打开浏览器,我们访问一下,刚才的那个Servlet,localhost:8080/servlet4/demo,那访问完以后,我们看这个浏览器一片空白,因为没有做出响应,没关系,看控制台,这回控制台有新的输出了么,有了,它输出了,实例化Servlet,然后紧接着初始化,然后呢,调用,顺序确实是按照这个顺序来的,123,实例化,初始化,调用:
    在这里插入图片描述

Servlet中单例模式的应用体现

  • 然后我们再访问一次,看还输出什么,甚至你再访问多次,看一下,打开浏览器,多刷几遍,然后呢,看一下这个控制台,只有调用,就说明了一点什么呢,这个Servlet它,不论是怎么样,都只被实例化一次,所以DemoServlet在tomcat之内,它是单例的,只有一个实例:
    在这里插入图片描述

在Eclipse中销毁Servlet的方式

  • 那单例的对象有好处,单例的话,这个对象只实例化一次,节约内存,对象每次访问都new一个对象,和只new一个,它不一样, 这个节约内存。那什么时候会销毁呢,shutdown的时候,那你注意,shutdown,我说的是shutdown,那我们点控制台上的红色按钮,并不是shutdown,你点红色按钮,是强制把Servlet进程杀死了,是意外的死亡,tomcat没有机会留下任何的遗言,就没了,看不到了,你得点谁呢,你得点Servers下的这个红色按钮,这个才是shutdown,那么tomcat正常死亡,它会留下一些内容:
    在这里插入图片描述
  • 那我点了,那点完以后,看控制台,一堆内容。那我们去从众多内容去找,销毁DemoServlet这句输出了。那一点完,有的人控制台一片空白,到底是为什么啥也没有,因为这个控制台有多层,你刚好看到了空白的那一层,那如何切换控制台的层次呢,视图工具栏有个显示器的图标,点下拉切换一个层次,控制台有多层,所以,可能是刚好跳到了空白的那一层,切换一下就好了:
    在这里插入图片描述

Servlet配置之标签:load-on-startup

  • 那按照之前所述,可以修改配置文件,让tomcat一启动,就实例化这个Servlet,那改一下试试,再打开servlet4项目,然后再打开web.xml这个配置文件,那我们需要在这个servlet标签之内写一句话,这句话叫什么呢,load-on-startup,写个序号1,<load-on-startup>1</load-on-startup>

  <servlet>
  	<servlet-name>demo</servlet-name>
  	<servlet-class>web.DemoServlet</servlet-class>
  	<!-- 在启动服务器时第1个创建此Servlet -->
  	<load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
  	<servlet-name>demo</servlet-name>
  	<url-pattern>/demo</url-pattern>
  </servlet-mapping>

  • 那这句话,啥意思呢,也很容易理解对吧,startup启动,on在启动时,load加载,在启动时加载当前的Servlet,就是创建它,1,代表什么意思呢,1代表顺序,因为我们配置文件里可能会配很多个Servlet,有可能配置多个Servlet,那么,可能启动时都要创建,那总得有个顺序,这是顺序,因为我这个Servlet是第一个,所以我写了个1,写个注释,这句话的意思是,在启动服务器时,第一个创建此Servlet。写完之后,把项目再重新部署一下,部署以后,启动tomcat,在启动的时候,看一下控制台,先不要去访问这个Servlet,只看控制台有没有输出呢,你看,先实例化,在初始化,完成了,就这样:
    在这里插入图片描述
  • 那然后呢,我们再去调用这个Servlet,打开浏览器,多刷两下,多访问两次,然后呢,看控制台,控制台不会实例化,只会调用: 在这里插入图片描述

Servlet生命周期小总结

  • 当然,你在shutdown的时候,也会销毁,销毁都一样。那么关于这个Servlet生命周期就说完了,就是讲这个对象,什么时候实例化,什么时候初始化,什么时候调用,什么时候销毁,那么讲这几个方法的顺序,那我们后面有些地方的程序,依赖于这个顺序,基于这个顺序,那么new的话不用讲,这构造器没什么可讲的,你要想实例化对象,必须得new,那service没什么好讲的,就是处理请求,主要是这个init和destroy方法,那这两个方法有什么用呢,是这样的,如果呢,你想在实例化这个Servlet对象以后,在创建完对象以后,就给它传一些,就给它初始化一些参数,那用init,这个init呢,是为了给这个对象,初始化参数的,有人可能会想,那为什么需要这个方法初始化参数呢,能不能用构造器初始化参数呢,不行,因为刚才说了,构造器必须是无参的,tomcat才会调,如果是有参的,不会调用了,所以,构造器别指望了,所以我们就是这个加了一个方法init,专门是解决这个问题,给这个对象,初始化参数的,当然了,init它能初始化什么参数,怎么初始化,后面会有演示。
  • 然后呢,还有destroy方法,那销毁,那我们服务器关闭时,关闭就关闭了,服务器都关闭了,对象自然就会销毁,那你调用destroy有什么用呢,举个例子啊,你比如说,咱们当前的eclipse就是个软件,只不过它是个单机软件,那这个软件,我已经启动了,那我再次点eclipse图标,还能再重新启动么,它会给个提示,提示我说那个workspace被占用了,你有这个经验吧,你就是启动完以后,你再启动的时候,它会提示你,workspace被占用了,你不能启动两个,eclipse共用一个workspace,是这样吧,不允许,那它是怎么做的呢,它是加了锁,就是我们一启动eclipse的时候,它在init,就是初始化的环节,往那个一个文件里写了一行字,加锁。然后呢,我们再启动eclipse的时候呢,它会判断,有没有这个锁,有锁的话,就不让用这个workspace,不让用,然后呢,eclipse我们关闭时,它会销毁,有一个销毁的过程,但未必调destroy(),反正会销毁,那么在它的销毁方法里,它把那个锁清除了,所以你再启动时,就好使了,是这样的。
  • 所以,大概这个场景就这个意思,我们将来开发项目时,如果说,有的项目也是这样,我一登录以后,要锁这些数据,我可以呢,在init里,初始化时加锁,那么我在退出时,把这个销毁的方法里,我可以呢,把这个锁解除,可以这样,就是这么一个意思,那一般的话,这种业务比较少,就简单了解一下,那将来要用的话再说。那现在呢,我们大概的了解了这个Servlet,它这些方法的调用的次序,那下一个和它相关的话题,那么,下一个话题是什么呢,那下一个话题是这个参数,ServletConfig,在重写父类的init的方法,有这个参数,所以我们要讲这个config参数,然后呢,后面还有另外一个参数,叫做ServletContext,那么,这两个参数,这个两个都是对象,这两个对象呢,有相似之处,应该是对比的一起讲,很多书上呢,没有这个意识,所以分开学习还是有点问题,应该是对比的讲,config和context,有相似之处。

4.ServletConfig和ServletContext

  • 所以呢,那下一个话题,我们讲什么呢,写一下。然后,还是那句话,你复习时,看我的这个笔记就好了啊。下一个话题讲的是ServletConfig和ServletContext,我们讲这两个对象,那这两个对象,我们习惯于简称管ServletConfig简称叫config,这个ServletContext简称叫context,简称一下,要不然太麻烦,太长,那第一点先说一下,那它们有什么用,先说它们的作用,还得画个图,那其实呢,这两个对象呢,是一个辅助的,它是打辅助的,它不是主力,我们这段课程的主要的内容是什么呢,是Servlet,以及呢,request和response,这是主力;而我们要讲的这两个对象是辅助,它们辅助谁呢,辅助Servlet,是以Servlet为核心的。

对比分析config和context使用场景及作用

  • 先把这个Servlet画出来,比如说,DeomoServlet,那Servlet是一个对象,那你想啊,我们在调用这个对象的时候,我们在创建对象的时候,有没有可能我们需要给这个对象,预置一些参数呢,有可能吧,比如说我们做查询功能,查询的时候要有翻页,每页返回多少条数据,那个size就是参数对吧,就可以预置。所以,我们在调用这个对象的时候,在创建这个对象的时候呢,很有可能是需要呢,给它预置参数的,那我们如何给一个对象预置参数呢,我们完全可以自己搞定,怎么搞定呢,我们可以这样,给它预置参数,第一种方式,可以把这个参数写到这个Servlet类里,写死,但这样不好;第2种方式呢,我们可以把这个参数呢,写到一个接口里,接口中的变量自动的是常量,然后让这个类呢,实现这个接口,就有了这些个数据了,也可以,但不管怎么样吧,只要你把参数呢,写到了类,或接口当中呢,这个参数就是写死的,就是固定的,就是不利于去维护的。
  • 为什么说不利于维护呢,你可以想一下,咱们这个软件开发完了,我们要上线的时候,那这个参数有可能要改,比如说查询,查询时分页的页码,可能是要改的,那比如说我们这个软件卖给甲公司,甲公司穷,电脑都14寸,对吧,每页显示5行数据,我们把这个软件卖给乙公司,乙公司的话,有钱,电脑都是20多寸,每页显示,比如说30条数据,能理解吧,你要改,那改的时候,那软件是谁安装,谁去培训呢,实施,实施人员很多时候,它不懂技术,它不会改这个类明白吧,不会改,因为他不会编译,不会写,所以呢,我们有必要,把这个参数,写到哪去呢,配置文件里,配置文件可以改,不用编译对吧,其实写配置文件里比较好,所以啊,通常我们预置的参数,最好是写到配置文件里,可配,可改。
  • 那配置文件,有几种形式,我们可以用什么样的文件,做配置文件呢,可以用xml,还可以用什么啊,properties,是这样吧,那都可以,那你说,我们能不能用一个txt文件做配置文件,能不能用,word,excel做配置文件呢,可不可以呢,可以,其实你用什么都行,但是呢,我们java语言当中呢,一般都是用这俩,明白吧,我们读取这两个文件的API,比较方便,你用别的话,就不好读了,然后呢,那如果说我想读这个xml,我得写个啥,我得用哪项技术来读,用什么啊,dom4j是吧,用这项技术来读取xml文件,那么我想读properties,用什么技术来读呢,用哪个东西来读呢,就叫Properties,是这样吧,是这样的。就是说,我们可以呢,自己创建配置文件,我们自己调用这个类去读它对吧,就可以。然后呢,读到的数据,我们可以呢,让这个Servlet去调用,是这样吧,可以给它调用,给这个Servlet使用,可以这样。
  • 然后呢,这两种文件,xml和properties,有什么区别呢,有啥区别,properties简单,对吧,结构简单,读取也方便,dom4j读xml就麻烦了对吧,所以呢,一般简单参数,像那个数据库连接参数,用properties;复杂参数,你看我当前所学的Servlet的配置,它比较复杂,用什么呢,xml,咱们的web.xml,不就是配置文件么,对吧,就是一段配置,就是用的dom4j解析xml这个东西。那这是一种方式,就是说,我想给这个Servlet对象预置参数,可以自己写配置文件,自己调用某些技术去读取这个参数,可以。这是第一种情况啊,就是自己写配置文件,自己读取配置文件,这是第一种情况,我们可以这么干。这是第一种方案。但是呢,还有第2种方案。第2种方式是什么呢,你想一想,咱们这个项目之内,不是已经有了一个配置文件叫web.xml么,我们能不能利用这个东西呢,可以直接用对吧,可以直接用,我们没有必要自己创建个新的配置文件。
  • 所以我们可以啊,直接利用项目中的web.xml,我们可以在这里呢,配置参数,那这个参数,我们也可呢,自己用dom4j来读取明白吧,但是麻烦,那你想啊,咱们没有写任何的dom4j,这个文件中的那个内容,不也被自动读取了么,是吧,所以说其实呢,tomcat已经自动读取web.xml了,那么另外呢,Sun,也给我们提供了一个默认的API,来读取这个文件,那API是什么呢,一个叫ServletConfig,还有一个叫ServletContext,总之啊,Sun给我们提供了,就是默认给我们提供了两个对象,这两个对象,都具备同样的能力,它们都能够读取web.xml中的,我们配置的参数,明白吧,但这个参数的格式有要求,咱们一会会讲。好,总之啊,ServletConfig和ServletContext,它俩都能读这个web.xml中的参数,读到以后可以给Servlet去调用,怎么调,我们后面会讲,那它们的功能呢,就极为相似,那有什区别呢,这两者是有区别的,我说下这个区别 ,区别就在于什么呢,ServletConfig,它和Servlet是一个什么关系呢,是一个一对一的关系,那么,这个ServletContext和Servlet是什么关系呢,是一个一对多的关系。
  • 好,那这件事,可能不好理解啊,这我解释一下,咱们可以做一个,就是比喻吧,我们把这个Servlet比喻成,咱们教室中的一个学生,假设Servlet是一个学生,然后呢,我们在开课的时候,每个人都给你分了一个tts账号,是这样吧,每一个学生,都有一个tts账号,这个tts账号,能够为你提供一些资源,能够为你提供一些数据,是这么理解么,那这个tts账号,是不是和你一对一的关系啊,是这样吧,所以tts账号,相当于谁呢,ServletConfig,就是ServletConfig就相当于学生的tts账号,它和这个Servlet是一对一的关系,或者说呢,这个ServletConfig,只为这个Servlet服务,明白吧,你的tts账号,只为你自己服务,是这样一个对比。那ServletContext,相当于什么呢,相当于我们所处的教室,教室是一个共享的资源,是这样吧,教室中有很多资源,就是你可以理解为,从编程角度理解为数据,是可以被我们所共用的,比如说,教室中的这个投影,对吧,教室中的这个空调,教室中的饮水机,对吧,大家可以共用的。
  • 那好了,通过联想以后,那你能理解ServletConfig和ServletContext有什么区别么,就它们的区别,它们都能够给Servlet提供数据,而ServletConfig和Servlet是一对一的关系,对吧,ServletContext是一对多的关系。那么,如果你的数据只给一个人用,用ServletConfig,给所有人用,用ServletContext,能理解吧,看使用范围。再归纳一下,就是我们再给Servlet预置参数时,可以自己写配置文件,自己写读取配置文件这项技术,自己去弄,没关系,也可以呢,直接用我们项目中自带的配置文件,然后呢,利用Config和Context来读取它 ,那我们在这个时候,Config和Context,它俩都能读取,有什么区别呢,如果这个参数,只给一个人用,用Config,明白吧,私有的;如果这个参数是所有人用,用Context,公有的,明白吧。它们的范围不同,所以呢,就这么个区别。
  • 好,这是第一种方案啊,是我们可以自己写配置文件,自己读取配置文件,那么第二种方案,你可以这样,就是采用项目自带的web.xml做配置文件,然后呢,利用ServletConfig或ServletContext,读取此文件中的数据,这是第二种方案,那么,都能解决问题,所以呢,归根结底,那个Config和Context,不是你必须要用的,如果你不用,自己写这个东西也可以,所以,我说它是辅助,不是必要的。所以,我们呢,后讲。那下面呢,我把这个图呢,存一下,然后呢,我在笔记里再归纳一下。在这里插入图片描述
  • 那ServletConfig和ServletContext,它们的作用是什么,看这个图,然后概括一下,它们的作用就是,都能够读取web.xml中,为这个Servlet预置的参数,我们可以在这个配置文件里,预置参数给Servlet用,然后呢,用这两个对象去读取。那么,读取文件中的参数是它们的作用。那么了解它们的作用以后,还要说一下,那它们的区别是什么,其实都说过了啊,区别就在于什么呢,这个ServletConfig和Servlet是一对一的关系,然后呢,ServletContext和Servlet是一对多的关系,那根据它们的关系,我们最终能得出结论,就是说,什么样的数据用Config来读呢,就是说,若数据只给某个Servlet使用,则用什么啊,Config,若数据给,不能说所有,说给多个Servlet使用,那我们用什么啊,Context,这是一个结论。
  • 那至于说后面我们到底怎么用啊,到时候再说,再看,再演示,别着急,先把呢,它们的区别先搞清楚。然后有人可能会想,哎,说这俩对象,凭什么说,Config就一对一,Context就一对多,它们是怎么保证一对一,一对多的关系,怎么弄的呢,这件事解释一下啊,就是说,这个它们的关系,就一对一,或一对多的关系,由服务器来保障,那么,服务器在创建这个对象时,会保障它们的关系,那具体怎么保障,我们后面再讲这个细节时,我们会再说一下,告诉你什么时候,会创建这个Config对象,它怎么就一对一了,什么时候创建Context,怎么就一对多了,后面再说,先别着急,反正呢,是由服务器来保障。
  • 好了,这是,这个Config和Context,它们的作用和区别,大致了解了这两个对象以后,那下面我们再去详细看看,那config是怎么用的,那context又是怎么用的,我们先看一下这个config,所以呢,第3个小的话题就是,ServletConfig,它的使用场景,那你注意,毕竟啊,这个两个对象,它作用呢是,不像request,response,那么直观,这俩对象的作用呢,还是有点这个抽象,有些时候呢,不好想明白,那像这种情况呢,我们最好是想一个业务场景,我们把这个业务场景理解透了,在这个业务场景之内,去理解这个对象的使用的方式,所以我们这里呢,说一个业务场景,不要,永远不要脱离一个业务场景去探讨这个程序这样写,怎么怎么样,那么,我们程序是为业务服务的,一定要搞清楚业务。

ServletConfig案例演示:设置登录上限LoginServlet

案例场景分析:页游设置登录上限
  • 那我说一下,这个config的使用场景,我说什么场景呢,那比如说,我要开发一个网页游戏,页游,这个游戏分为好几种是吧,有什么呢,有这个页游,网页游戏,端游,PC端是吧,英雄联盟,还有什么呢,手游,手机端的游戏啊,好几种,我说的是页游,网页游戏,网页游戏适合在什么情况下玩呢,对,适合上班的时候,因为你去上班的时候啊,你会发现,公司呢,有的公司直接把这个外网就给禁了,不让上外网,因为啥呢,上外网,你整天老是聊天,老是上网,不好好干活,这是第一点;第二点呢,你上外网的话,经常会把公司的一些资料,通过外网传出去,明白吧,泄密。所以呢,外网禁了,只能上内网。然后呢,当然,开放一定的外网端口,可以开放些端口,你可以外网访问,比如说访问百度啊,这个常规的网站,到是可以的,你可以查东西明白吧,然后呢,很多是不让访问的,所以你那个游戏肯定是不让玩了,但网页游戏,因为可以访问那个公司的那个网页,那也可以,网页游戏都可以,就这个场景适合页游啊。
  • 当然这玩意,你还是别玩了啊,就是说这是假设啊,假设要开发一个,这个网页游戏,那游戏大家都玩过,一般都有这样的一个规则,不知道从哪年开始的啊,就是说,如果这个游戏太火了,同时访问的人数太多的话,那后登录的人,他是需要排队的,是吧,不然的话,你像早期的游戏啊,没有这个机制啊,以前好像,很早以前玩那个传奇什么,那个时候,没有排队的机制,大家都往里挤,最后把这个服务器挤死了,为止。服务器再重启一下,你们再接着玩,那时候都那样,后来不知道从哪个游戏开始了,大家都排队了,所以,我们开发一个网页游戏,要求什么呢,若在线,就是若,这个超过最大人数则,若超过这个人数上限吧,则要排队,这个业务能理解吧,要做这样一个业务,假设啊,我不可能真的开发一个网页游戏,要能开发出来的话,咱也不用在这,在这说了是吧,就直接就就业了哈。
  • 好,那么这个业务当中,我们怎么去实现呢,应该是这样,我们什么时候判断,有没有超过上限,什么时候去排队呢,是什么时候,做这个事呢,什么时候要判断在线人数,要排队,做这个事呢。是不是你在登录的时候,是不是啊,你登录时,系统会判断,那当前的人数到没到上线对吧,到了你就排队,是这意思吧,对吧,所以那,我们在登录时做这个事,我们要开发登录功能,要开发登录功能,那登录功能,我们要开发一个什么呢,LoginServlet,因为登录不是一个请求么,对吧,我们开发一个LoginServlet来处理这个请求。好,然后呢,还有一点,那在这个LoginServlet当中,我们需要判断上线,那人数上线是多少呢,这个人数上限,咱们是写死的么,人数上限一般呢,是可配的,因为我们开发一个网页游戏,这个网页你可能外包给别人运营,是这意思吧,未必自己运营,是这样吧,就即便是自己运营,那你运营的时候,你所租的服务器,它就不一定,如果你租了一个好的服务器,它能支持5千人在线,如果你租了个不好的服务器,就2千人,是这意思吧,有差异。所以上线时呢,这个在线的人数的上线,是不是得可调的,是这样么,那得可调啊。
  • 那么,人数上限应该是一个可配置的参数,那么这个参数显然是在LoginServlet当中使用,是这意思吧,这是该参数,那么,由LoginServlet使用,就这么一个场景,那你想啊,就这个场景,那我们这个参数,怎么去配,由谁来读取,怎么弄,就你看我们刚才的,画的这个图,我们可以自己写配置文件,自己配对吧,但这样麻烦,最好呢,还是用web.xml,自带的么,对吧,省事了。那不但如此,我们想读web.xml,有自带的工具对吧,config或context,那你看,我们这个场景用config读,还是context,用谁读呢,用谁读看什么呢。看一下这个就是我们这个数据,都谁要调,这个参数都谁要调,人数上限是不是就登录调用,你打副本的时候,就你进去了,你打副本,打单场,就不用再去调用了,是这样吧,就登录时调用。所以只是给LoginServlet使用,是这意思吧,所以由谁来读取呢,ServletConfig就可以啊,那么,由于该参数只是LoginServlet使用,所以呢,由ServletConfig读取即可。
  • 这就是Config它的典型使用场景,我们要开发一个网页游戏,当游戏的在线人数,达到上限了,你再登录要排队,那什么时候排队呢, 登录时,我们开发登录功能LoginServlet,那在登录功能当中呢,我们要取到上限,那个人数好做比较,而人数上限最好是能配,因为我们把它运行在不同的服务器上,好让这个可以改,所以呢,这个参数,我把它配到web.xml里,那我们用谁来读取呢,用Config,那为什么用Config读取呢,因为只有登录功能会用到,其他的功能不用,是这样的。
LoginServlet的代码实现与配置
  • 好了,那这个逻辑啊,我就说清楚了,下面我们写这个案例啊,但是我们写的是模拟,非常容易啊,我们去写一个LoginServlet。我们就利用,之前的这个项目servlet4,就利用这个,那在这个项目之下,我们再创建一个新的Servlet,这个名字就叫做LoginServlet,然后呢,打开这个类,我们要处理登录的业务,那我们需要重写父类的service方法,重写一下。那么,在这个service方法里面呢,我要这个,获取这个参数啊,那参数名叫啥,刚才忘说了啊,这个人数上限,应该是一个可配的参数,这个参数名,咱们叫maxOnline,行么,行吧,我们要读这个参数,由Config读取,那我们在这个方法之内,如何获取config,如何去读取那个参数呢,很容易啊,我们父类,给我们提供了一个方法,直接调就可以了。那方法叫getServletConfig(),即ServletConfig cfg = getServletConfig();
  • 我们可以调用父类的这个getServletConfig方法,直接得到Config对象,然后呢,能够从这个对象中读到数据,那当然了,那这个对象,是什么时候被创建的,这个都哪些地方,我们可以调它,这个我们后面再说,反正先这样用一下,先用明白了,然后再详细解释,别着急。那得到Config以后,我们怎么从它里头得到数据呢,非常容易啊,cfg.getInitParameter("maxOnline"); 。好了,那读到的参数,都是个字符,我们把那个,声明个变量接收一下,String maxOnline = cfg.getInitParameter("maxOnline");,那然后呢,我们把这个参数输出一下,看看对不对,是多少,System.out.println(maxOnline);,那我们虽然说,是模拟的登录业务,我们只模拟其中一个环节,就是怎么得这个参数,那至于说,怎么利用这个参数,怎么去判断,这个我们没法去模拟了,就是说读到这个参数就可以了,关键点就在于这。

package web;

import java.io.IOException;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class LoginServlet extends HttpServlet {

	//Tomcat创建Servlet的过程:
	//LoginServlet ls = new LoginServlet();
	//ServletConfig cfg = new ServletConfig();
	//ls.init(cfg);
	//ls.service();
	
	@Override
	protected void service(
		HttpServletRequest req,
		HttpServletResponse res) throws ServletException, IOException {
		ServletConfig cfg = getServletConfig();
		String maxOnline = cfg.getInitParameter("maxOnline");
		System.out.println(maxOnline);
	}
}

  • 好,那这个类就写完了,写完以后呢,我们需要呢,对这个Servlet啊,加以配置,那我们再打开web.xml,配置它啊,我在/demo,(web.xml的DemoServlet的配置信息)之后,配置这个LoginServlet,那配置完以后,我们还要给它配置参数,而这个参数呢,只给LoginServlet,自己用对吧,所以呢,这个参数在哪配呢,在这个LoginServlet的servlet标签(配置信息)内部来配,那相对来说,如果说参数给所有人用,在哪配呢,在外面。给一个对象用,在它只内配,给所有对象用,在外面配,我们现在是只给它自己用,所以呢,我在LoginServlet,这个配置信息的内部来配啊,写这样一句话,叫<init-param>...</init-param>,初始化参数。

<servlet>
  	<servlet-name>login</servlet-name>
  	<servlet-class>web.LoginServlet</servlet-class>
  	<init-param>
  		<param-name>maxOnline</param-name>
  		<param-value>3000</param-value>
  	</init-param>
  </servlet>
  <servlet-mapping>
  	<servlet-name>login</servlet-name>
  	<url-pattern>/login</url-pattern>
  </servlet-mapping>

  • 好,那么init-param这个标签,表示说,我在这要初始化一个参数,param-name呢,是参数名maxOnline,param-value,是参数值,3000啊,这个随便写啊,然后呢,如果说你想有多个参数,再写多份这个init-param,这个标签,明白么,一个参数maxOnline,写一个init-param,如果再有参数再写一份,以此类推。然后呢,我们在servlet标签的配置信息内部,所声明的参数,由谁来读取呢,ServletConfig,怎么读取呢,刚才在类中已经写了。好,写完以后咱们试一下啊。那把这个项目呢,再重新部署一下,那部署完以后,你去访问这个/login,看一下,输出了这个数了没有,localhost:8080/servlet4/login,访问之后看一下控制台,3000是吧,输出了两次,我这刚才卡住了,所以输出两次,所以说,你点几次输出几次啊(点了两次,刷新两次)。那当然,这只是演示了,config到底怎么用啊,那么这个Config到底什么时候被创建的,它到底怎么就和Servlet保证了一对一的关系,我们把这个逻辑呢,给它说清楚,加深理解。

  • 那么我想加一个参数,就需要在配置文件中对这个参数进行配置,我们可以自己写配置文件自己读取,但用自带的web.xml,更加方便。而且我们想读web.xml,有自带的工具,config或context。如果只给这一个DemoServlet组件用,那这个参数通过init-param标签,写在DemoServlet组件配置的信息的声明之内,只给它自己用,如果我要加个参数,给所有的组件用,把这个init-param,这个标签写在DemoServlet配置信息的外面去,写在内和外,有截然不同的区别。那么写到内部,只给它自己用,我们用Config读取;写到外面给所有人用,用Context读取,这是规则。

  • 然后我们再回到,那个登录的组件,LoginServlet,在这个组件之内,写一些注释,把这个Config和Servlet的关系,把它说一下,在@override这个service方法之前,写个注释。就是说tomcat在启动时,或者说在首次访问时,tomcat创建了Servlet,这个的过程,我们说个大概的过程啊,就是简化的过程,它底层在实现的时候呢,是非常的繁琐,那我们说个大概。那tomcat呢,它怎么创建的Servlet呢,是这样的,就是new吧,以当前的类为例,它是LoginServlet,LoginServlet ls = new LoginServlet();,就这样创建,那么tomcat实例化这个Servlet以后,它会立刻调用这个组件的init方法,然后呢,给我们传入参数,而这个参数 是通过Config传入的,所以呢,在调init之前,它还会先创建一个Config对象,所以下面呢,它会创建一个Config对象,ServletConfig cfg = new ServletConfig();,那实例化Config以后,它会通过init方法,把Config传给这个Servlet,那下一步是这样的啊,ls.init(cfg);,传入Config。然后呢,它才去调用这个service方法,ls.service();,就是说,这几句话是我写的一个伪代码,就是模拟的代码,只是为了演示一下:

    //Tomcat创建Servlet的过程:
    //LoginServlet ls = new LoginServlet();
    //ServletConfig cfg = new ServletConfig();
    //ls.init(cfg);
    //ls.service();

  • 那么实际上,这就是tomcat底层调用程序的过程,它先帮我们new Servlet,先实例化,实例化以后,调init,然后呢,再调service,就是Servlet的生命周期,先实例化,再初始化,再调用,但是呢,其实再初始化以前,还有一步,它会先创建一个Config,初始化时呢,把Config传入。那这里通过Config,读取到这个参数,我们在这个service里,getConfig得到一个Config,是这个cfg么,那tomcat怎么保证Config和Servlet一对一呢,tomcat在每实例化完一个Servlet以后,就一定会给它实例化一个Config,并且把这个Config传给init,那么在这个Servlet内部,它的父类当中,它记录了这个对象,我们随时调get,能得到这个对象,就是它,不是别人,就是它。所以呢,我们获取的Config读的参数,就是init方法传入的这个Config。

  • 那有人又想了,说那你Config,通过init传入,但是我这个类,没有重写init方法啊,我没有init,怎传入的呢, 怎么传的,别继承啊,我们老是,就是大家呢,我发现呢,对于继承,对于多态,理解的是极为不到位的,有人说我理解明白了,我已经理解的很透了,其实也不透,咱们要平时说,这个Person,是一个父类,Teacher继承于Person,明白,Student继承于Teacher,明白,我们换个话题,我们说,比如说Request,和别人的关系,怎么怎么样,我们说一个不是现实中的这种场景,就不明白了,还是没明白,就是很多时候呢,你心中呢,会把这个继承当作负担,其实就是说,还是没理解。当前的类是有父类的,父类中有init方法,我不重写也是有对吧,相当于孩子有,这个是没问题的,那我要重写的目的是什么呢,是我认为父类中的逻辑还不够完善,还需要加点东西对吧,那这里,我不需要加东西,就这样就可以,就不用重写,所以呢,tomcat调用LoginServlet的init,能调用到吧,可以,从父类继承过来,当前类相当于有init,这是没有问题的。就大概就这样。

  • 然后有人可能还会有疑问,tomcat调了init这个方法,但是我们在重写init的时候,那个init有俩,一个是带参数,一个是不带参数,对吧,那个不带参数,是什么时候调的,是怎么解释呢,那你注意,那个不带参数的init,是这个init(cfg),带参数的init里面自动调的,这样的,它那个init啊,底层是这样写的,好像没有返回值吧,我忘了啊,init(cfg),这是带参数啊,然后呢,一开始它这里有这个逻辑,就Config怎么怎么样,后面又有一句话,this.init();,所以,tomcat调了,带参数的init,也会连带的,间接的调这个无参的init,而且有参的init是先调,通过有参的init,调了无参的,它为啥这么搞呢,它希望继承者,就是程序的开发者方便,我们继承于这个父类,我们想重写父类的方法,如果你需要参数,就重写带参的这个,如果你不需要参数,重写无参的这个,它希望我们重写时方便,仅此而已,它希望开发者方便,尽量去体会吧。


void init(cfg){
	...
	this.init();
}

init()与init(cfg)的区别:如何查询API手册
  • 当然了就这这件事啊,你怎么去知道呢,你想知道的话,两个途径,第一个途径看源码,第2个途径看手册,API,带着你看下手册吧。大家呢,老是没养成看手册的习惯,咱们任何一个阶段的内容,都可以通过手册查到,那基础SE,我们看那个Java的API就可以,然后,我们前端,我们看的是w3c的那个官方手册,对吧,都有,那我们当前讲的是什么呢,是JavaEE,Java的企业级解决方案,它有专门的手册,这个手册呢,官方的话,你可以去那个ORACLE去找,因为,那个ORACLE是现在这个Java语言的拥有者对吧,在它那官方网站上有,或者是自己做个文档服务器:
    在这里插入图片描述
  • 文档服务器是一个宝库,这什么手册都有,平时可以多看看,如果有的话。这里面告诉你这个手册的名字,比如说,Java 6 API 中文版文档 HTML,你会不会百度搜一下呢,可不可以自己下载,你可以把它当作一个提纲对吧,自己下载。你找理由的话是千千万万的,但是解决方案,解决途径,自己想办法,你说领导,我这没有手册,家里没有,你说这合适么,自己想办法去,对吧。比如,可以自己建站保存。那在咱们这个中国啊,你要申请个域名,是要备案的,备案的话,你得提交申请,提交申请以后得等,大概20来天呢,才能ok的,然后呢,中国动不动,感觉呢,这个有问题,还给你封了,它不像国外,国外不需要备案。那咱们要看的文档在哪呢,WEB开发文档,当中的Java WEB文档,tomcat文档,Servelt 3.0 API HTML,这个就是JavaEE的文档,只不过呢,它是最新版的,咱们用的还没有用到3.0,我们用的是2啊(2017年时),没有用到3,3是比较新了,但没关系,因为新的内容也会涵盖旧的,它不会抛弃,是兼容的。
  • 我们点一下,Servlet 3.0 API HTML,那这是英文的,没有中文版,将就看吧,有总比没有强,你工作时,你遇到问题,这都是宝贝,有文档就不错了:
    在这里插入图片描述
  • 我们在All Class这个地方,很容易找到HttpServlet,我们所继承的父类不就是它么,我们看一下,点进去看一下,HttpServlet:
    在这里插入图片描述
  • 我们看一下,这个类继承于谁,实现了哪个接口,是不是继承于javax.servlet.GenericServlet,那个init方法,是在它的这个类当中声明的,所以我们到这个类当中去看,点GenericServlet,顺藤摸瓜,摸到这来了,GenericServlet:
    在这里插入图片描述
  • 然后呢,看它里面的方法,往后找啊,我们只找我们想看的,你注意,有人说这个手册,我不知道该怎么看,那是因为你没有目的,你不知道你要干什么,你瞎看,自然就不知道该从何看起,如果你带有目的的去看,我想看这个方法,怎么样,你就能找到,一定要有目的。往后看啊,构造器如何如何,我们不要看构造器,看的是方法,method对吧,往后看,有什么方法呢,有destroy(),有什么getInitParameter(java.lang.String name),等等,我要看的是init:
    在这里插入图片描述
  • 我要看的是无参的init,有什么说明,点进去看详细的解释,这有解释,有方法的这个声明,然后看这段,Instead of overriding init(ServletConfig), simply override this method and it will be called by GenericServlet. init(ServletConfig config). The ServletConfig object can still be retrieved via getServletConfig().,如果我们重新这个无参的init方法,这个无参的init方法,它会被调用,被有参的init调用,那么我们即便是只重新无参的int,也能够都得ServletConfig对象,这段就是解释。当然很多地方看不懂,但是我们大概看一下,然后,要养成一个习惯,我们尽可能去看一手的资料,没有经过污染的,没有经过加工的,那你百度到的那些内容,是经过加工的,是有偏差的,有可能会误导你,所以,你最好看这个东西:
    在这里插入图片描述
  • 这个看手册,到此为止,工作时再看,还是那句话,你得带着目的看,如果说我没有目的,你瞎看,这手册内容这么多对吧,你不知道该看什么,看什么都感觉看不懂啊,就是手册就是字典,我需要查哪个字,我就看那个字,其他的一概不看,就这个意思,你别说我浏览一遍,没有意义,越看越烦。回过头来我们再验证一下,我说的这个逻辑,那么tomcat,它会帮我们实例化Servlet,调用无参构造器,然后呢,它要调init方法,并且要传入Config,所以呢,它就必须在此之前创建一个Config,传给这个Servlet,然后才调用service方法,那么因为啊,它在init时就传入了config,那你想,我在service里,我们在destroy里,能不能得到这个Config呢,tomcat在在这个调init时,传了config进来,那我们在service里,在destroy里,能不能得到这个Config,可以,因为这个程序执行是按照顺序来的,它一定是先调init,再service,再调destroy对吧,那你在第二个阶段,就传入了这个对象,第3,第4阶段,就能得到这个对象,这有顺序的保障,所以我们在service里才能够调get方法,得到这个ServeltConfig对象,去使用,有顺序的保障。
  • 那关于Config就先说到这里,这个地方是比较抽象的,不直观,我们学编程,我们直观的地方要学,抽象的地方也要学,而且越是抽象的地方,越是提高的地方,越是这个能升一个档次的地方,所以尽量去理解,早晚的话,你的思维方式要达到这种程度。那Config说完了,再说下一个话题,下一个话题是这个Context。就是ServletConfig和ServletContext这个话题中,关于它们的作用,一是Config到底它怎么用,它的经典用法;那第二是这个Context,它的这个使用场景,还是那句话,对于抽象的东西,我们最好,从一个业务上去理解,我们理解了这个业务,将来才有可能理解更多的业务,如果业务也没理解,就光去看语法,语法会了,最后还是会忘,最后遇到相关的业务,你也记不住,也不知道该怎么用,所以,记住这个业务是很有必要的。

SevletContext案例演示:网站流量统计功能

  • 再说一个业务,这个业务非常常见,就是我们开发任何软件的话,它都会有查询功能,是吧,查询,你看淘宝,不得搜东西么,不就查询么,百度不可以搜东西么,不就是查询么,那几乎呢,每一个网站都会有这个查询功能,而且呢,有很多查询功能, 那么查询的话,一般是要分页的,是这样吧,通常是要分页的,因为数据多啊,如果你不分页展示的话,难看,是要分页的,那分页的话,每页显示多少条数据,这是个参数,每页显示几条数据,这是个参数,最好是可配,因为你比如说,我开发一个OA系统,开发完以后,我把这个产品卖给不同的公司,卖给甲公司,它要求我把这个,每页显示改为10行,卖给乙公司,改为20行,对吧,是需要调整的,那么,这个参数可配,这个参数用什么来配,什么读取呢,就说这段话题。
  • 写一下,就是说,大部分的查询都具备分页功能,那么分页需要一个参数,参数是什么呢,就是每页显示几条数据,这个参数我叫size,可以吧,之前在jdbc的时候,不也写过类似的,是吧,size,那么这个参数,该参数一般可配置 ,因为我们给系统上线时,需要改,需要调整,可配置,那这个参数,我可以把它配置到web.xml里,对吧,由谁来读取呢,一般就由Context读取,而为啥用Context读取呢,因为查询功能,一个项目中不止一个,比如说查询员工是一个查询,查询部门是一个查询吧,因为我们这个软件有很多个查询,所以很多个功能,可能都要用这个size,所以是要复用的,既然是要复用的,我们就一般用Context读取就可以了啊。该参数呢,就是一般可配置, 然后呢,由于被众多查询功能复用,所以使用Context读取是比较合理的。我们就演示这样一个场景,那我们就模拟啊,写俩个查询功能,比如说一个是查询部门,另外一个是查询员工,然后呢,我们看一看,我们用Context读到的参数,能不能在这两个组件之中去服用,就这个意思。
  • 那么回到开发工具里,打开这个eclipse,项目servlet4,那我们在web这个包下,再创建一个新的Servlet,创建一个新的,这个名字,我叫做FindDeptServlet。那我们创建这个类,要模拟查询的行为,在这个类当中呢,重写service方法,处理查询请求。那么,我重写了service方法,对这个方法呢,进行了一定的整理,在这个方法内部,我们模拟做查询,那查询时因为要分页,需要那个size参数,那这个参数我们用Context读取,怎么使用Context读取呢,那使用方式,基本上和Config是一模一样的,首先呢,我们调父类的方法,get方法,得到这个Context,得到它,ServletContext ctx = getServletContext();,得到它以后,我们想从它的内部读取一个参数,那个方法和Config读取参数的方法是一样的,String size = ctx.getInitParameter("size");,参数名比如说叫size吧,读到这个参数以后呢,咱们把它输出一下看看,System.out.println(size);,这就完成了啊,那读到这个参数以后,我们就可以利用这个参数,去实现分页 的逻辑,那后面的内容,我们就先不写了,总之,我们当前要演示的只是读取这个参数而已。
  • 那这个查询部门要用这个参数,查询员工也得用对吧,所有的查询都要用,我们再写一个查询,我们再写个查询员工的Servlet,也是要读这个参数,那它的逻辑和FindDeptServlet是一模一样的,那我们简化处理,简单来,怎么来呢,我选择FindDeptServlet,然后呢,copy一下,ctrl+C,复制一下这个类,复制完以后呢,你就直接在原来的这个包下,或者你选择web包啊,ctrl+v, 就直接呢,再把这个类,在这个包下,再粘贴一遍,一粘贴因为重名了,它有提示对吧,要求你改名,那我们改个名吧,改成叫,FindEmpServlet ,OK就行了。很简单,Ctrl+c,Ctrl+v就可以,改个名,FindEmpServlet,它里面的逻辑跟刚才一样,就是读那个参数对吧,输出看一下。那么这两个类呢,咱们都写完以后,那接下来,我们需要对它们进行配置,我们再打开配置文件,然后呢,迅速把这两个类配置好。

  <servlet>
  	<servlet-name>findDept</servlet-name>
  	<servlet-class>web.FindDeptServlet</servlet-class>
  </servlet>
  <servlet-mapping>
  	<servlet-name>findDept</servlet-name>
  	<url-pattern>/findDept</url-pattern>
  </servlet-mapping>
  
  <servlet>
  	<servlet-name>findEmp</servlet-name>
  	<servlet-class>web.FindEmpServlet</servlet-class>
  </servlet>
  <servlet-mapping>
  	<servlet-name>findEmp</servlet-name>
  	<url-pattern>/findEmp</url-pattern>
  </servlet-mapping>

  • 那配置好以后呢,还差点事啊,还得配参数对吧,那这个参数呢,因为是在多个组件之间共用的,它不是配在某一个组件之内,是配在所有组件之外,所以呢,我在这个Servlet配置信息之外啊,随便找个地方配这个参数,先后顺序没有关系啊,就在最后面吧,那这个参数这样写啊,叫context-param,是给Context读取的参数,是context-param,那name就是参数名,value就是参数值,这是一组参数,如果多个的话,你写多份:

  <!-- 
  	Tomcat启动时会给每个项目创建一个context对象,
  	并自动调用此对象加载对应项目的web.xml中的参数.
   -->
  <context-param>
  	<param-name>size</param-name>
  	<param-value>10</param-value>
  </context-param>

  • 写完以后呢,咱们测试一下,把这个项目呢,部署一下,部署以后啊,我们启动这个tomcat,启动以后呢,我们访问这些功能,比如说,我先访问这个查询部门的功能,localhost:8080/servlet4/findDept,findDept然后呢,看控制台,输出结果啊,10,然后你换一个访问,我再访问这个findEmp,localhost:8080/servlet4/findEmp,再看结果,还是10,是吧,所以,可以共用的。
  • 那么用这个Context去写,模拟做一个查询,确实发现在这个查询员工,查询部门里面,我们可以复用这个参数,那这个参数,配置时它的名字,标签名叫context-param,那这参数是,由context读取的,所以从名字上面也能看出来,然后呢,这个参数,context什么时候读取,那context什么时候被创建, 而Context呢,它这个怎么就是,保证和Servlet一对多呢,这个得解释一下,那在这个配置文件里,在这个地方,在它相关的位置写注释说明一下,那么tomcat,或者说服务器吧,启动时会给每个项目创建一个Context对象,并自动调用此对象加载对应项目的web.xml中的参数,就总之,这个Context对象呢,是在tomcat启动时创建的,因为tomcat呢,只启动一次,所以呢,每个对象,它只有一个Context对象,那么,这个对象呢,创建完以后,立刻就读到了这个参数,那么任何的Servlet都可以复用,就是这样的,就这样保障,总之,tomcat会在恰当的时刻,创建对应的对象,启动时创建一个Context,而每次创建完Servlet以后,创建一个Config,所以自然而然的,Context和Servlet是一对多的关系,Config和Servlet是一对一的关系,这个关系是由它们的创建时机保障的。
  • 那Config和Context有它们的使用场景,这两种场景啊,我们发现都是利用这个对象,读取web.xml中的参数,那这个参数我们可以理解为,它是一个常量,那在我们程序运行时,它就改不了了,它没有改的方法,所以,它是个常量,那么Context呢,还有一个作用,它不仅仅是能够,读取这个常量,它还能够读写变量,或者说存取变量,所以说下面我们讲的是,这个Context另外的一个用法,就是存取变量的用法,之前是读取常量,那它还有一个用途是能够存取变量,就是Context可以存取变量。那么要说这个话题呢,咱们也是举个例子,通过一个场景来说明一下,那我们什么时候可能会用到Context, 来存取变量,那这个地方也画一下图,就是说一下这个场景。
    在这里插入图片描述
  • 比如说,当前我们有了一个项目叫servlet4,当然这个项目是我们瞎写的,随便写的,别管是什么吧,反正是个项目。假如说呢,我想做一个统计软件流量的一个功能,什么叫统计软件流量呢,咱们以淘宝为例,任何人访问淘宝一次,流量就加一,当然一个网站流量越大,证明它越有价值,所以流量能体现这个网站的价值,所以有的时候,要统计网站流量,统计了流量以后一公布,你看我流量这么大,你向我投资吧,对吧,你来向我这个挂广告吧,对吧,是这样的。那么任何人,访问这个网站,任何功能一次,就是流量加一,比如说,我们全班每个人访问淘宝一次,就是,流量就是100个人,100个人就加100,对吧,那再一个,那我访问淘宝3次,流量就加3是吧,就任何人访问淘宝,任何功能一次,流量就加一啊,就这个意思。
  • 那么要想统计这个功能啊,咱们就是说一下,怎么来做呢。那一个软件呢,有很多功能,就以我们的软件来讲吧,我们现在有什么功能呢,首先有这个FindDeptServlet,有查询部门的功能,当然我们的软件呢,还有这个,FindEmpServlet,查询员工的功能,当然了,还有什么登录啊,等等其他的功能,我就不挨个说了,就说俩,查询部门,查询员工,等等,这么多功能,那无论是用户访问FindDeptServlet这个功能,还是说另外的FindEmpServlet的功能,我们都得将流量加1,记录起来,对吧,有章可循。那你看,我想记这个流量,这个数据,这份数据是不是个变量,肯定是变量,要变化,那这个变量,我把它存在哪合适呢,我存到FindDeptServlet这个组件里,好不好呢,不方便,因为FindEmpServlet访问不方便,对吧,而且你存到FindDeptServlet里,感觉给它自己用似的,对吧,不是很合理,存到FindEmpServlet里也不合适,应该把它存到一个公共的区域,放哪呢,对,就是Context,因为啥呢,Context对象,它是我们这个项目下唯一的这么一个对象,它具有惟一性,然后呢,任何的组件都能够从Context里面得到数据,它是被共用的,所以,我们将这个流量,这份数据,存到Context这里,是比较合理的。
  • 所以呢,我要往Context里存一份数据,流量的数据,这个流量就是数量,叫count吧,存到Context里一个变量,叫count,然后呢,用户访问FindDeptServlet这个功能时,我们需要呢,将count,进行累加是吧,访问FindEmpServlet时,也要累加,总之呢,无论访问哪个功能,我们都要将count累加,就是加加,两个功能都得加。但是有个问题,我们把变量存到count里可以,累加也行,但这个变量,我们什么时候存,一开始最初的值是几,默认值,最初的值应该是几啊,应是零对吧,那我们什么时候存进去比较合适呢,什么时候存比较合理。是不是应该在用户没有访问任何功能之前就存进去,就很早很早就存进去,如果用户已经访问了,你再存有点晚了,是吧,最好就是还没有做任何访问时,我们提前就存好,访问的时候再累加,这样比较合适,那你说在什么时刻存比较好呢,初始化的时候,就说白了,我们最好是服务器一启动,我就存进去,那服务器启动时,肯定没有访问它们,没有访问这些Servlet,这就已经有了,那访问Servlet时有了,我就加就可以了。
  • 那我想在服务器启动的时候,往这个Context对象里存一个值,这个代码,我要写在哪个地方才能实现呢,这个代码往哪里写呢。有人说init,因为init这个方法,是在启动时调用的,而且这个方法的作用,就是初始化数据,在init里写是很合适的,那我要在init里写,那这个init方法,任何Servlet都有,那我是把它写到哪个Servlet组件里,是FindDeptServlet,还是FindEmpServlet,还是我们再来一个组件,怎么写,在哪里写合适啊,就一般最好还是再写一个组件,这个组件呢,专门写个init方法,专门给这个软件,专门给这个项目初始化数据,通常这么干,所以我们单独再写一个Servlet,那这个类名,我叫什么呢,就叫InitServlet,那你注意,这个组件的名字很明显,它是初始化用的,对吧,那它里面只有init方法,InitServlet.init(),只有初始化这个数据的逻辑,它里面呢,不负责处理具体的请求,所以它里面呢,根本就没有重写service方法,可以这么干,将来工作时,你会发现呢,咱们这个项目中啊,一般都会有一到两个这样的类,还未必是一个,还有可能是两个,因为有的时候初始化,就是两种这个,类型不同的数据,可能有两个。
  • 那我们写这样一个东西,在这里初始化数据。那我们将在InitServlet.init()里呢,创建一个count变量,默认值为零,把它存入这个对象,Context,而Context呢,它可以被任何的Servlet共用对吧,所以,InitServlet也可以用,而且Context对象呢,是服务器启动时,最先创建的,一定是在所有Servlet之前就创建了,所以Context,它的创建时机也早,那在这个方法里呢,我们要实现这样一个逻辑,就是把count初始化为零,count=0,存进去就可以了。然后,那这个InitServlet,它的init方法,要想在启动时调用,我们还得做一个额外的操作,做一个额外的配置,那个配置叫什么,你得写一个<load-on-startup>1</load-on-startup>,得写这句话,然后,像这样的一个方法,我们希望它的启动,它的执行顺序是几,一般肯定是1,它优先级是最高的,就是它最先执行的,就是在所有Servlet之间,它是最先的,比较优先的,所以是1。那这是我们要做的事情,以及这个,做这个事情的这个思路。
  • 那下面我们就来做,首先呢,我们就写这样一个类,叫InitServlet,只有init方法,初始化变量到Context里,那么大家再回到开发工具当中,我们在这个web这个包下,创建一个新的类,叫InitServlet,继承于HttpServlet,创建好这个类以后,这个类仅仅是用来初始化参数的,我写个注释,此类仅仅是在服务器启动时,初始化参数的,那么它不负责处理任何具体的请求。那么要想呢,初始化参数,我们需要重写呢,父类的init方法,这个方法,一般就是做这样的事情,那我们在这里呢,右键,Source,Override/Implement Methods...,从父类中呢,选择init方法,当然,它父类HttpServlet中没有对吧,在它父类的父类GenericServlet里,然后有两个init,它们的关系是,其中呢,带有参数的init,tomcat会调用,那么不带参数的init,是这个带参数的init方法,自动调用,所以关系是这样,tomcat调用带有参数的init,带有参数的init,调用不带有参数的init。反正总之这一串,都被调用了,那都被调用了以后,你想重写谁都可以,你需要Config参数,就重写带参的init,你不需要,就重写不带参数的init也行,那我们这里用的是Context,不是Config,我们重写不带参数的就可以了:
    在这里插入图片描述
  • 我们重写这个无参的init,那么在这个init方法里,我们要给Context设置参数,我们可以在这个init里面,得到Context,那么要得到Context的话呢,我们可以通过get方法获得,ServletContext ctx = getServletContext();,得到这个Context对象以后,我们要往这个对象中呢,存入数据,存入变量,那我们用这个对象存取变量,调用的方法是这个方法,ctx.setAttribute("count", 0);,那我们呢,调用set方法是存,那将来呢要取,得调用什么呢,get,这很显然,相反的,相对的。

@Override
public void init() throws ServletException {
	super.init();
	ServletContext ctx = getServletContext();
	ctx.setAttribute("count", 0);
}

  • 那这个类就写完了,就两句话,写完以后呢,我们需要呢,对它加以配置,我们再打开配置文件,配置这个Servlet:

  <servlet>
  	<servlet-name>init</servlet-name>
  	<servlet-class>web.InitServlet</servlet-class>
  	<load-on-startup>1</load-on-startup>
  </servlet>

  • 那注意啊,这个Servelt,我们配置时,我只写了一半,另外一半配置网名,没有写,因为不用写,另外一半不用写,为什么呢,因为这个类的主要作用是什么呢, 是用来预置参数对吧,用来这个,初始化参数,还强调了,这个类不负责处理具体的请求,它连service方法都没有对吧,没有能力处理请求,所以,你给它配置网名,有用吗,有必要么,所以,没有必要,像这样的类,不需要配置网名,所以另外一半不用写,那么从这个角度也好理解,说为什么我们配置一个Servlet,需要写两部分,<servlet>一部分,<servlet-mapping>一部分,为啥要拆开呢,为啥不能一个整体就配完了呢,因为有些Servlet不需要写网名,分开配,有的时候可以不配<servlet-mapping>这一半,解耦,还是解耦。
  • 那还有一件事啊,我们配这个Servlet,它需要被第一个初始化,还得写一句话呢,在这个类上,在这个标签里,写上<load-on-startup>1</load-on-startup>,那这块写1以后啊,我们也会有一些想法,我我记得在之前,有一个servlet,也写上了1,有矛盾了,那一般呢,init是1,其他的都得往后排,我们再把以前那个改为2,改一下,我们找到很久以前的那个servlet,在最上面DemoServlet,这个就改成2吧,写注释第2个,对应上2。

  <servlet>
  	<servlet-name>demo</servlet-name>
  	<servlet-class>web.DemoServlet</servlet-class>
  	<!-- 在启动服务器时第1个创建此Servlet -->
   	<!-- 在启动服务器时第2个创建此Servlet,(改为2,下面init为1) -->
  	<load-on-startup>2</load-on-startup>
  </servlet>
  <servlet-mapping>
  	<servlet-name>demo</servlet-name>
  	<url-pattern>/demo</url-pattern>
  </servlet-mapping>

  • 那现在,这个环节就完成了,tomcat一启动,这个Context对象里,有会有这个count变量,那后续呢,用户访问任何的Servlet,我们都需要,将这个变量累加,那你要将变量累加啊,你要从这个对象里得到这个变量,get到,然后累加以后,再写回去,做这样一个处理啊,那我们以这个查询为例,我们在FindDeptServlet和FindEmpServlet,这两个类当中呢,写这段代码。
  • 我们再回到案例当中来,打开这个FindDeptServlet,那我接着写,之前呢,我们是得到size,现在我们做另外一件事,再干什么呢,要统计流量,那这个,getAttribute("count");setAttribute("count");,这个方法它呢,支持的类型呢是Object,返回内容是对象,Object,那但是我们存的时候,明确存的是整数,所以我就强转了,转为Integer,那我得到整数以后呢,把它累加,再设置回去,再写回去,那当然了,我们统计流量,最终啊,应该有一个专门,显示这个流量的界面,那我们这里就没有,那我就直接输出了,咱们就省点事,直接就输出这个流量,System.out.println(count);

//统计流量
Integer count = (Integer)ctx.getAttribute("count");
ctx.setAttribute("count", ++count);
System.out.println(count);

  • 然后呢,查询员工的时候也这么干,我们再把这段代码呢,再复制一下,粘贴到查询员工的里面,复制一下,然后呢,我们打开查询员工的Servlet,把这段代码呢,粘贴进来,那处理完以后,咱们测一下,我们把这个项目呢,再重新部署一下。然后呢,启动tomcat,,启动以后,我们先后访问一下,这个查询部门,查询员工,看一下流量呢,是否会递增。我先查询下员工,localhost:8080/servlet4/findEmp,查多次啊,2,3,查了好几遍,那么控制台呢,输出的结果是123,连续的,然后呢,我再查询部门,localhost:8080/servlet4/findDept,啊,看控制台,4啊,也是连续的:
    在这里插入图片描述

5.Servlet的线程安全问题

  • 这个Config和Context这两对象的内容基本就说完了,那总体而言呢,这两个对象,它里面能够呢,存点数据,它能够帮我们读取配置文件,读取参数,然后呢,这个数据给谁用,是给Servlet使用,当然了,还是那句话,这两个对象不是必须要用的,你想配置参数,你想读取参数,完全可以自己写代码,但是呢,如果你在做这件事的时候,能想到它们,能用上,那更方便,所以工作时看情况,你能想到用,就用,想不到,也没太大关系。然后呢,将来如果说我们的web项目里,用上的话,自然会体会到,为什么这么用。那么关于Servlet这段课,还有最后一个话题,是线程安全问题,然后,我们就可以讲这个,一个升级的解决方案,一项升级的技术了,那我们再说一下这个最后一个话题,Servlet线程安全问题,什么叫线程安全问题呢,就是我们这个软件肯定要支持并发的,它一定不是单机的,一定不是单线程的,是多线程的。

服务器底层支持多线程

  • 那有人觉得,哎,我们再写这个软件,写这个网页时,我也没有写线程啊,我也没有启动线程啊,为什么呢,因为这个tomcat底层,它帮我们创建了线程。其实是这样的,就是我们做好的软件,会部署到这个服务器里,我们部署的软件会部署到tomcat之内,那访问这个服务器的客户端,可以有任意多个,谁都能访问,比如说,有的人用IE访问,比如说,有的人用360访问,比如说有的人用Chrome访问,等等吧,那总之呢,访问这个服务器的客户端呢,有很多,那么每个浏览器去访问服务器tomcat,tomcat就会给这个浏览器创建一个线程Thread1,然后呢,用这个线程,解决这个浏览器的问题,如果360访问服务器,给360一个线程Thread2,如果呢,Chrome访问服务器,再给Chrome一个线程Thread3,就总之,其实呢,服务器它天生的,它就支持多线程,那么,每多一个浏览器访问我,我就给你创建一个线程,我用这个线程支持你的操作,所以它是支持并发的,但是呢,支持并发的这件事,不用我们去写代码,是自动的,所以,服务器的底层,它就用上了线程的技术,这是一方面。

服务器底层封装了I/O操作

  • 再一个呢,服务器它要读取浏览器传入的参数,服务器要像浏览器返回数据,那么它用的是I/O操作,I/O操作也封装了,我们直接就得到了,就request得到了参数,底层就是I/O,我们就直接response,就输出东西了对吧,底层就是I/O,所以服务器啊,它的底层由什么技术构成呢,线程和I/O,所以我们将来做web项目时,有人会想,哎,我在做项目时,普通的功能的时候,很多时候我没有写线程,我也没有写I/O,那线程和I/O就没有用吗,不是,是非常重要的,就SE那部分,我们讲的线程和I/O,是极为重要的,但是呢,这个重要内容,因为比较麻烦,比较啰嗦,由tomcat服务器封装好了,我们一般就不用写了,但是如果说,面试官想跟你探讨下,tomcat底层的一些原理的话,可能会聊到这样一个话题,这个基本原则,你至少要了解,总之啊,任何浏览器访问tomcat,tomcat就会创建一个新的线程,来解决这个浏览器的问题,就是一个浏览器就一个线程,就是这么一个对应关系,所以说,服务器是支持多线程的。

服务器底层实现了单例模式

  • 然后呢,这个服务器端的Servlet,比如说HelloServlet,是不是只有单个实例,就HelloSerlvet是单个实例,那么,Thread1可以调,Thread2可以调,Thread3可以调,这个HelloServlet实例在多个线程之间,是可以共享的,它是可以共享的,就是我们用任何浏览器都可以访问服务器,那服务器呢,会给每一个浏览器创建一个独立的线程,来解决这个浏览器的问题,每个浏览器都有一个线程,然后呢,服务器端,我们有Servlet,举个例子,比如说DemoServlet,那这个DemoServlet,它在服务器之内只有一个实例,那么,这个实例是被多个线程共享的,它们都可以调,是共享的,那你看,多个线程共享一个对象,有没有可能会出现问题呢,什么时候会出现问题呢,有人说并发,这就已经并发了对吧,3个线程同时访问,都有已经并发了,那并非就一定会有问题么,操作数据有问题,怎么操作有问题,我查询有没有问题,同时查有没有问题,没有问题,什么有问题呢,同时改,同时update就有问题。

Servlet线程安全问题的原因

  • 那再明确一下,那到底什么时候同时改会有问题呢,那注意,我们要同时改这个对象中的数据就有问题,那这个数据,还有不同的形态,有的数据,就这个这个对象,它是存到了内存里,然后对象中的数据,有的数据呢,存到了堆里,有的数据呢,存到了栈里,这个数据,这个变量,它是存到了堆或栈当中,那咱们是这个,存到哪块的数据,我们同时改可能会有问题,就是数据呢,存于或堆或栈当中,变量或存于堆里,或存于栈里,什么样的变量会存到栈里,是局部变量,什么样的变量存到堆里,成员变量或者说整个对象,就是对象以及对象的成员,会存到堆里,那我们去改哪份数据,可能会有问题呢,改堆成员(堆中的成员变量)会有问题,为啥呢,改栈没有问题。
  • 因为栈对每一个线程都有栈帧,每一个线程有自己的栈帧,是互相是隔离的,是分开的,是不交叉的,所以,局部变量存到了栈里,如果我们改的是局部变量,没事,因为每个线程有自己的栈帧,是区别的,是分开的,不是共享的,这没有问题;而堆有问题,因为什么呢,堆中的数据,如果说这数据只有一份,它是被共享的。就解释一下,这个栈是有,就是每个线程有自己栈帧,因为每个线程有自己栈帧,数据就被区分开了,被隔离了,所以同时访问也没事,这个没事,而堆里的数据,同时访问,是有事的,就是多个线程是共享堆中的数据,所以,如果说,我们改的是堆中的数据,就有问题。

案例演示:增长平均工资功能UpServlet

  • 因此啊,我们要想明确说,那到底什时候会出现线程安全问题呢,准确来讲是,如果我们多个人,多个浏览器同时访问服务器,同时,修改这个对象中的成员,就会产生问题,是这样的,所以,不是说时时刻刻都有问题,是这样的场景有问题。那下面呢,我想演示一下,就是说确认一下,是不是有问题,我们模拟一下,做一下,那怎么做呢,我说一下我的这个要求。比如说呀,比如说我们这个,做一个修改工资的,修改平均工资的,这么一个案例,比如说,我做的是一个人力资源的软件,那么,公司的人力访问这个软件,可以修改这个员工的平均工资,比如,今年要涨工资,我们平均涨了多少,就这个平均工资平均涨幅,那这个公司的人力呢,可能会很多的,有的大公司,那个总公司,分公司,加起来人力不少,那每个人可能都可以访问,都有权限访问这个功能,那如果它们同时访问的话,我们看会不会有问题,那这个平均工资呢,我们在写的时候,我这个Servlet,咱们新写一个,我叫涨工资,UpServlet,写这么一个Servlet,然后让多个浏览器同时访问,然后呢,要访问它里面的一个,平均工资的一个成员变量,比如说叫salary吧,有个默认值,比如说默认值是2000啊,salary=2000.0
  • 大概呢,就模拟做这样的一个功能,就是涨平均工资的这么一个功能,多个人同时访问,但现在呢,我们这样写有点小问题,就是说,我们即便是写出来这个类,工资2000,我们在方法里去改它的话,但是我们可能用,打开3个浏览器同时访问么,我们一个人可能实现这个并发么,不可能对吧,我不能说,同时我又点了IE ,又点了360,我毕竟,就是一只鼠标对吧,两个鼠标我也点不过来,我只能是先点IE,后点360,有个顺序,那怎么才能够,就是模拟出这个并发的场景呢,要想模拟出并发的场景,有多种办法,那我们在实际工作时呢,这个叫白盒测试,或者叫黑盒测试,所谓的白盒测试,指的是,测试人员他会编程,写出一些程序来,模拟一些数据去处理,这叫白盒测试,一般企业玩不起白盒测试,因为那些做白盒测试的人,他的技术含量还是挺高的,很多人都是做过开发的,一般都是大企业,才会有这样的这个测试方式,像什么ORACLE啊,像什么微软啊,会有,一般中小企业,或者说,一般的企业都没有,所以说,白盒测试,还挺牛的。
  • 然后呢,一般的企业呢,它测试的话,它觉得这玩意,没必要花那么多的精力,花那么大的价钱,去找人去测,所以它一般都是盲测, 或者叫黑盒测试,什么叫黑盒测试呢,就是大家一起点,我们就点,你只要点不出问题来,就算是OK了,不写程序,以前我们公司就是,我开发时就是这样的,我们就是黑盒测试,大家点,然后呢,这个共同测完了,最后要并发的话,怎么办呢,比如说今天要测并发了,领导说了,说今天我们要并发测试啊,今天晚上不许走,加班,吃完饭以后都坐好,那我们部门人也比较多,一两百号,都坐好,坐好以后呢,领导呢,拿个大喇叭在那喊啊,大家好,点开那个某软件的某个功能,准备好,填好数据,填好数据啊,好,我开始了,我喊到3,开始点啊,123点,我们卡点,那虽然说有的人,节奏快,有的慢,但100多个人,总有几个人,能同时点,明白吧,就这么来玩,这种的话,反正有点low啊,但是很多企业确实这么干的,这还算不错的,有的企业连这个都不做,就给你上线了,出了问题再说吧,有的是这样,所以呢,这个还有这种办法,还有一种办法,就是可以用专门的测试软件,就是有的人啊,开发出一些开源的测试软件,我们利用测试软件模拟数据也可以,就是说白了,那些企业的白盒测试的人编的程序,他把它公布出来了,明白吧,谁都能用,也可以用那个啊。
  • 但我们现在,想做这件事,我们还,用哪个都不太合适,我们用测试软件,咱们还不是干测试那个活,对吧,我们搞那个软件,搞半天,这也不太合适,那软件不是说一两天就能装好,就能整明白的,麻烦,这是第一点,第二点呢,我们去说我们自己写个测试软件,那就更麻烦了,那太罗嗦了,然后第3个呢,我们说,我们一起点,这倒是有可能,因为咱们全班人可以同时,访问我这个电脑对吧,一起点,这倒可以,但是,这个没法操作,因为还有远程班呢,对吧,这没法组织了,这怎么玩啊,还容易乱,太乱了啊。所以呢,我们还是换一个思路,换一个角度,我们可以这样,当,就是我还是自己去,自己去玩,自己去点,比如说,我先用IE吧,访问这个服务器,访问这个组件,访问的时候呢,我在组件里啊,立刻给这个阻塞一下,sleep一下,明白吧,假设就是网络延迟了,IE在这卡住了,同时,360再点一遍,它俩就汇合了,听明白了么,就模拟网络延迟,等着另外一个线程,听明白了吧,然后的得到一个结论,听懂了么,大概是利用这个网络延迟,就利用这个阻塞的方式,来解决问题啊,所以我们在一会啊,在写这个UpServlet类的时候,还要做一点特殊的处理。
  • 我再强调一下啊,就是我们这个类中呢,需要有这个成员变量,salary,然后呢,在这个service方法里面,我们还要就是,我们还要涨工资,模拟涨工资吧,然后呢,模拟涨工资以后呢,还要sleep一下,那sleep多久呢,就是你能够先后把这IE和360,这俩都点过一遍,这时间够就可以,明白吧,一般的话,你稍微长一点也没关系,我搞个8秒吧,8000毫秒,8秒之内,我应该点IE,点360,没问题啊,就这样,就可以了。然后呢,我们这样试一下,看一看,我们点完以后,会不会出现奇怪的情况,本来原来的工资是2000,我IE和360,分别点完以后,我们看一下,它俩得到的结果,明白吧,看看会不会有什么问题,有问题,我们再解决,大概是这个意思啊。那这个思路说完了。
    在这里插入图片描述
  • 这个线程安全问题,那我们在写程序的时候呢,只要你的程序是这个支持并发的,就有可能会出现这个线程安全问题, 那别管是访问Servlet,还是访问谁,都有可能出现,那么对于Servlet,它的访问,它是支持并发的,你想我们很多人,所有人都能访问淘宝对吧,那一定是支持并发的,那么这个并发呢,不用我们去实现,服务器已经 自动实现了,总之呢,服务器,它会给每一个浏览器创建一个线程,然后呢,用这个线程,处理这个浏览器的逻辑,服务器认为呢,每一个浏览器,就是一个客户,然后呢,在多个线程之间呢,它会访问同样的Servlet,因为可能多个人都要去涨工资对吧,可能多个人都要做同样的事情,就访问同一个Servlet,那么,这个Servlet呢,它在tomcat内,某一个类型,它只有一个实例,所有这么多线程都访问同一个对象,同一份数据,很用可能会出现问题,那具体什么时候出现问题呢,就是,如果多个人同时去改,对吧,不是读,读没事,同时改,改的是堆中的数据,比如说改成员变量,就会有问题。
  • 那么下面呢,我们写一个小例子,就体会一下,看一下有没有问题,如果确定有问题了,我们再去解决,那我们还是回到之前的这个项目,servlet4,我们在这个web包下,创建一个新的类,类名,按照我画图所讲的,叫UpServlet,那按照之前所说的这个思路啊,我要呢,在这个类中加一个成员变量,然后呢,我们多个人同时改,那这个成员变量啊,我给它叫,就是平均工资啊,我叫salary吧,给它一个默认值啊,默认值比如说2000,private Double salary = 2000.0;,然后呢,每个人访问这个对象的时候,它访问的是service方法,那么在这方法内部呢,我们去改变这个值。所有下面我们从写这个service,右键,Source,Override…,在这个service里,就是涨工资,把这个salary加一下,比如说,每次任何人访问这个方法,我们就将工资呢,加100,每次加一百。
  • 总之啊,我们这个业务逻辑非常简单,就是每次任何人访问,就是工资呢,就加一百,salary +=100.0;,那加完以后呢,为了一会我们这个,能够模拟出并发的场景,我在这将这个程序呢,阻塞一下,假设呢,就网络延迟了,服务器延迟了啊,然后呢,等着另外的一个线程,然后呢,让它俩汇合在一起,我们再输出内容,然后呢,就是用这种手段,模拟一个并发的现象。就是我们说白了,我手没那么快,我只能呢,让程序执行的慢一点,等等我,那写这句话啊,Thread.sleep(8000);,要try-catch下,这个阻塞,我觉得8秒应该够了,如果觉得不够,可以再长一点,那这句话就是,写个注释,我们是模拟网络延迟,假设网络延迟了 。那这个阻塞以后,我们最终呢,把这个结果输出给浏览器,让浏览器看到,修改这个数据以后的值是什么,那就输出了,输出:

public class UpServlet extends HttpServlet {
	
	private Double salary = 2000.0;

	@Override
	protected void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
		salary +=100.0;
		try {
			//模拟网络延迟
			Thread.sleep(8000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		res.setContentType("text/html");
		PrintWriter w = res.getWriter();
		w.print(salary);  
		w.close();
}

  • 那这就模拟完成了,那这个类写完以后,也得配置,我们再打开配置文件,再把它配置一下,打开配置文件啊、,配置好这个Servlet,

  <servlet>
  	<servlet-name>up</servlet-name>
  	<servlet-class>web.UpServlet</servlet-class>
  </servlet>
  <servlet-mapping>
  	<servlet-name>up</servlet-name>
  	<url-pattern>/up</url-pattern>
  </servlet-mapping>

  • 这是一个小案例,所有呢,它的访问路径,我们采用一个简单的方式,第一种方式来配,那配置完以后,咱们可以加以测试啊,那大家把这个项目重新部署一下,部署以后呢,你要启动你的tomcat,启动以后呢,别着急访问,我们需要准备两个服务器,因为我画了图说了,服务器认为呢,每一个浏览器,就是一个客户对吧,我们需要两个客户并发,需要两个浏览器啊。那我这个电脑上呢,首先有chrome,它算一个,我再找一个啊,我这里还有啥呢,360吧,我不想用IE啊,我就烦IE,那你们电脑上应该是,你们电脑上有这个火狐是吧,不过360,也挺恶心的,非让我默认啊,然后呢,因为这个手慢啊,我们把这两个浏览器啊,这个路径先敲好,明白吧,先准备好,然后呢,先点那个,再点这个,就能在8秒之内才能完成,那你看我在这个Chrome里,我先敲路径,项目名呢是,servlet4,然后访问路是/up,localhost:8080/servlet4/up,先敲好,准备好,别回车,准备好,360也是,地址栏把路径敲好,不要着急回车,先准备好了,那么,准备好以后,我先后呢点一下,我先点Chrome,再点360,就是你点一下那个地址栏,一回车,要8秒之内搞定啊。
  • 先来Chrome,我回车了,赶紧的360啊,8秒之内,360,然后呢,阻塞了,网络延迟了对吧,等会别着急,过了一会以后呢,终于出结果了,我看一下,两个结果都是什么啊,Chrome得到的是2200.0,360还是2200.0,你们是这样么,是的,这就不合适了对吧,就是你看啊,你可以想一下,比如说,张三,用chrome涨工资,李四,360,涨工资,两人同时访问对吧,结果得到的工资,得到的这个新的这个数据,都是2200.0,对吧,但原来是,2000.0,对吧,应该是两个人,应该有先有后对吧,应该有一个2100.0,一个2200.0,这样比较合适,就都一样,这就很别扭,就谁也不能接受。
    在这里插入图片描述

Servlet线程安全问题的解决

  • 那么这个问题产生的原因,就在于呢,并发了,同时改了一份数据导致的,那你想啊,如果说出现并发的问题,怎么解决,就是加锁对吧,加锁就可以,那下面呢,我们给刚才写的那段程序啊,加个锁,然后我们再重测,看一下加锁以后,会不会解决问题,我再打开那个Servlet,我将刚才呢,这一段代码加锁,加锁就是写这句话叫,synchronized(this){},所谓的加锁就是什么呢,就是排队对吧,就是谁先抢到了这个资源,把它锁住,别人必须得排队,就好像我们去洗手间一样,我进去了,锁上了,你就进不去对吧,你不能两人同时进去,就打架了,不合适啊,得锁上,我出去解锁,你再进去,排队就好了,加锁跟那个意思一样。那当然了,我们需要把刚才所写的这段代码,挪到这个synchronized,这个块之内对吧,往上挪一下,挪上去。

synchronized (this) {
	salary +=100.0;
	try {
		//模拟网络延迟
		Thread.sleep(8000);
	} catch (InterruptedException e) {
		e.printStackTrace();
	}
	res.setContentType("text/html");
	PrintWriter w = res.getWriter();
	w.print(salary);
	w.close();
}

  • 那么线程啊,再怎么并发,也不可能同时,抢到这个资源的,一定是只有一位,明白吧,它加了锁,别人就得排队,别同时抢,就这意思,那加完锁以后,我们再试一下啊,大家呢,把这个项目,再重新的部署一下,重新启动一下,然后呢,我还是Chrome和360,先后去点一下,看一下这回的结果啊。还是先Chrome,然后赶紧360,等个10几秒,再看结果,这回Chrome是2100.0对吧 ,360是2200.0,有先有后对吧,这就比较合理了啊。
    在这里插入图片描述
  • 那总之啊,我们写代码,写这个Servlet时啊,有可能也会出现线程安全问题,当你给它加上成员变量时,就可能出现这个问题,不过呢,其实,这种问题一般出现的少,我们将来呢,可能会在这个组件里加成员变量,但是呢,那个成员变量如果不是数据,如果只是算法的话,也没有问题,比如说啊,比如说我确实加个成员变量,如果说这个成员变量是个Dao,那Dao里封装的是方法,EmpDao dao;,不是数据,也没关系,所以呢,咱们将来写代码时呢,我们可能会加成员变量,但往往不是数据,顶多是个Dao这样的组件,这样的算法的对象,所以也没什么问题,所以,咱们平时工作时,这种情况比较少,也不用太担心,那万一遇到了问题,加锁就是了。
  • 当然了,这个也并不是说,加锁是万能的方法,有的场景其实还不太适合加锁,那怎么做呢,到企业里,企业里会有办法解决,但我先说一下这个场景啊,你比如说,咱们人人都会用到的那个12306,都用过吧,抢票,尤其到年底,到比如说这个节假日对吧,尤其到十一啊,年底的时候,抢票,那比如说,可能去杭州东站的票,同一批次车的票,可能同时很多人抢对吧,成千上万的人抢,那我们都有抢票的经验,我们发现呢,这个,我也下单了,就到那一刻开始放票了,我也抢下单了对吧,但是可能是我成功了,你失败了,有这种可能吧,每个人结果不一样,那它就不是用这个,加锁处理的问题,如果是加锁的话,就麻烦了。
  • 你同时比如说,几万个人,甚至几百万人,抢这一个车的票,那你加个锁让人排队的话,几百万人排队,那得排多久是吧,这就不合适了。或者说这个,比如说双十一啊,这个淘宝,有的商家有这个活动,有秒杀活动,这个商品1块钱对吧,就比如这个鼠标一块钱,就10个,瞬间就被秒了,对吧,如果你也是排队的话,那淘宝的访问量,上亿人次,对吧,上亿人排队,能受得了么,这种就不适合排队,怎么办呢,就大家一起抢,抢完之后呢,判断一下,你抢完之后这个数据,还在不在,如果你抢完之后,这个数据小于0了,对吧,小于1了,为0了,或小于0了,就不合理,不合理怎么办呢,抛异常,把这个数据回滚明白吧,就可以。所以,还有别的方式,也可以用这个抛异常的方式呢,把这个打断啊,也可以这么办。好了,那总之都是两种方案,常规就是加锁,特殊的可以抛异常,打断程序,然后让它回滚,将来你们工作时都会看到的,都有封装好的组件。
  • 那么关于这个线程安全问题啊,你了解一下,其实线程安全问题的解决非常容易,就是加个锁,主要是要理解,那为什么会有线程安全问题,什么时候会有,所以呢,关键点就在于什么呢,这个逻辑,关键点在于这个逻辑,这个图,一定要理解浏览器和线程的关系,服务器是怎么去管理这个线程的,线程和我们这个组件的关系,然后呢,什么情况下,会有安全问题,这个要理解。那到这啊,我们这个Servlet,这项内容就说完了,那说完以后呢,我们发现,那这项技术,我们可以用它呢,拼动态网页,对吧,能做,但是你会发现,我们在做哪个案例的时候,我们都没有把网页拼全,是不是这样,没有拼完整,我要么就拼一个段落对吧,一句话,要么就拼一个table,像那个doctype,head,body,我都省略了,为啥省略呢,因为没法拼,太麻烦,对吧,太罗嗦了,所以,这项技术呢,有一些缺点,就说太繁琐啊,那么后面呢,有一个替代的方案,能够把它的这个开发的方式加以改善,我们用另外的方式去开发这个动态网页的话呢,效率会更高,那么这项技术,叫 JSP

6.总结

  • 那这是Servlet最后一个内容,Servlet的生命周期,以及呢,和它相关的内容,那么这些内容是,Servlet它里面这些,是一个比较高级的部分,是探讨呢,这段课程啊,这个组件啊,它深层次的一些原理啊,因为呢,它的基本的使用方式,从Servlet1-Servlet3,我们已经掌握了,那它深层的原理是什么,我们能否呢,利用这些原理,将来呢,做一些这个特殊的需求,能否呢做一些这个,原创的功能,就看这一点了,那么Servlet生命周期呢,讲的是这个对象,它的从生到死的过程,我们说什么什么生命周期,讲的都是这个话题。那么Servlet,它其实有很多方法,它有构造器,有初始化方法,有销毁的方法,有工作的方法,那这些方法通常呢,都是从父类继承过来的,那这些方法呢,每一个方法都不用我们自己去调用,你不要自己去调用,调用没有用啊,都是由服务器自动调用的。
  • 那这么多方法,服务器是按照什么顺序调用的呢,我们在写代码时,要知道这个顺序,因为有些时候呢,要依赖于这个顺序,那默认的情况下,我们第一次访问Servlet这个组件时,tomcat就会实例化Servlet它,那么我们可以加以修改,可以改成呢,一启动时tomcat就实例化它,那tomcat实例化这个组件以后,还会初始化它,调用init方法,主要呢,是想给这个对象传入一些参数,初始化一些数据,然后呢,我们去访问这个组件,那么tomcat呢,会调用它,当tomcat在shutdown时,会销毁它,调用destroy销毁的方法,总之,1234,这个4个方法,是分别在这个时刻调用的。然后呢,第一步,第2步,第4步,只执行一遍,因为tomcat只启动一次,只关闭一次,所以呢,我说某一个类型的Servlet,只实例化一次,比如说HelloServlet,只有一个实例,然后呢,比如说HiServlet,也只有一个实例,再比如说,NihaoServlet,也只有一个实例。所以,每一个类型的Servlet,都只有一个实例,那我们tomcat内有一个Servlet,还是多个呢,加起来还是多个,但每一个类型的,只有一个实例,所以,这个关系要搞清楚啊,并不是说一共就只有一个,一共是多个,每个名字的只有一个,这样的。
  • 那这个话题说完以后,和这个话题相关的话题,还有ServletConfig和ServletContext。那么在做Config这个案例演示的时候,也看到了Config,它是init方法的参数,和这个方法是有关系的;那Context呢,不是参数,但是呢,其实它和Config,用途相似,所以我们一起说,对比着说。首先呢,这俩对象有什么用,他们的作用,那这两对象的作用,是起到了一个辅助的作用,辅助这个Servlet组件,那么我们在开发Servlet这个组件的时候,有可能呢,它需要我给它传入一些预置的参数,这个参数呢,往往最好是可配,可配就方便,就灵活,那么配置的时候呢,我们可以自己写配置文件.xml,或者是properties,然后呢,利用对应的技术去读取它,然后呢,给这个组件调用,是可以的。
  • 但是呢,这样做会有点麻烦,就是说,配置文件我得自己写,这项技术我得自己调,代码我得自己写,比较啰嗦,那我们仔细一看,我们的项目之内,都有这个配置文件web.xml,我们就可以直接利用它,而这个配置文件的读取,不用我们自己写代码,Config和Context,会自动读取,所以我们可以利用,Config或Context,那这两者都能读文件中的参数,然后呢,被Servlet组件调用,那它们的区别是什么呢,Config和DemoServlet是一个一对一的关系,每一个DomeServlet有唯一的对应的Config,而这个Config,只为这一个DemoServlet服务,它是一个私人的秘书,那这里面的数据呢,只为DemoServlet使用,别人不能用。那Context呢,相反,它和DemoServlet是一个一对多的关系,那么所有的Servlet,都能够共用同一个Context,那我们把数据放到这里来,所有的组件都能用,所以这两个对象,它里面的数据的这个适用的范围是不同的。
  • 所以,实际开发时,如果说这个参数,只给它自己用,我们存到Config里比较合适,避免别人误用;如果说,这个参数谁都能用,存到Context里比较合适,谁都可以调用。我们在写代码时呢,要管理好这个数据的使用范围,不要乱用,范围没必要的扩大可能会造成数据的污染,因为指不定别的地方对它产生影响。这是我们讲的它的作用,然后呢,又归纳了它们的区别,这个其实刚才都说了,Config和Servlet一对一,Context和Servlet一对多,然后呢,当然了,这个一对一也好,还是一对多也罢,这个由服务器来保障。
  • 那么,关于这个ServletContext,又说了两点内容,第一点是Context,它的使用场景,那么这个对象呢,它能够读取web.xml中的参数,然后呢再,读到的参数呢,可以给所有的Servlet共用。那一个经典的使用场景是,咱们分页的时候啊,要知道,这个每页显示多少行数据,这个size,那这个size呢,最好是可配,方便调整,然后呢,可以用这个对象来读取,读取之后呢,所有的Servlet都能共用。然后呢,Context这个对象啊,它读这个参数的方式,和Config一样,我们调的方法呢,也是ctx.getInitParameter(...);,比如在网站流量统计功能案例中的,在web.xml文件中配置的size参数就是这样读取的,String size = ctx.getInitParameter("size");
  • 那第二个呢,这个对象,还有另外的一种作用,它不但能够读这个配置文件中的这个参数,读常量,它还能够存取变量,那我们讲了一个这个经典的使用场景,就是说,我们想统计一个软件的流量,那么统计流量的话,就是用户呢,无论访问哪个功能,我们都得得将流量加1,那么流量呢,显然是个变量,而这个变量,因为需要给多给组件共用,所以呢,我们将它存到context里去,比较合适。那这个变量呢,我们应该是在很早就把它初始化,我们是专门写了一个Servlet,写了一个init方法,那么这个tomcat一启动,我们就调这个方法,然后呢,将变量初始化到这个对象里,然后呢,服务器启动完以后,用户呢,去访问任何功能,变量累加就可以了啊。
  • 那么,往这个对象中存,取,变量的方式,调的是它这个setAttributegetAttribute方法,然后呢,这个对象呢,它和Servlet,是一个一对多的关系,那么,它是什么时候被创建的呢,是tomcat启动时,tomcat启动时呢,会给每一个项目,创建一个Context对象,那一个项目中,有唯一的这个Context,那这个对象呢,会在创建所有的Servlet之前创建,是最早。那这段话题说完了以后啊,咱们又讲了Servlet的线程安全问题,那我们开发软件的时候呢,一般情况下,都是要支持并发的,这个很少有项目呢,是不支持并发的,那就是单机,或者是单线程的项目,是比较少见的啊,那么一般都要支持并发的,像这个tomcat其实本身就支持并发,tomcat底层啊,它就是封装了线程,封装了I/O ,这两项技术,当然也不只是这两项技术啊,就I/O和这个线程呢,是最基本的内容,然后呢,任何浏览器去访问服务器,服务器呢,会创建一个线程,来解决这个浏览器的问题,所以呢,我们多个用户,用多个浏览器去访问服务器是可以并发的,我们同时动能访问淘宝啊。
  • 那么这个线程呢,是它自动创建好的,然后呢,每个线程可能都会调用同样的一个Servlet,是有可能的。那你比如说这个UpServlet啊,它在tomcat之内,是单个实例,只实例化一次,那么多个线程呢,访问同一个对象,同一份数据,有可能会出现问题啊,那什么时候会有问题呢,就是同时改的时候,同时读取没有关系,同时增加也没有关系,你增加你的,我增加我的,是不同数据,就同时改,或者是我在改,你在删,总之呢,我们两个人同时去处理这一份数据,就有问题,那什么情况下,数据呢是一份数据呢,是我们存到堆中的数据,那么存到栈中的数据呢,是没有问题,因为,栈中的数据,每个线程有自己的栈帧,彼此呢是隔离的,不会交叉的,但堆中的数据呢,是线程共享的,那么,如果说我们这个对象,有一个成员变量,成员变量存入堆里,只有一份,那多个人同时改,就会出现问题啊。
  • 那我们也写了案例演示了,我们就是写了一段程序,然后呢,将它阻塞一下,然后呢,我们先后操作两次啊,然后呢,让这两个操作呢,汇合到一起,然后呢,发现这个结果,就有冲突了,有影响了,那么,要想解除影响呢,也很容易,就是说加锁就可以,那么,关于线程安全问题啊,就是我们解决方案,咱们以前SE时,就学过,,主要是呢,我们要理解,是通过这一点去理解,这个tomcat它是支持多线程的这件事,另外呢,就是说,你要知道,那这个浏览器,它和tomcat线程的关系,以及呢,这个tomcat内部的线程,和这个Servlet之间的关系,这个关系,你要大概有所了解。那么,说完这个话题以后啊,咱们这个Servlet就说完了。

参考文献(References)

文中如有侵权行为,请联系me。。。。。。。。。。。。。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值