配色: 字号:
JavaWeb——Servlet
2016-09-23 | 阅:  转:  |  分享 
  
JavaWeb——Servlet

Servlet是运行在Web服务器上的小程序,通过http协议和客户端进行交互。

这里的客户端一般为浏览器,发送http请求(request)给服务器(如Tomcat)。服务器接收到请求后选择相应的Servlet进行处理,并给出响应(response)。



?

从这里可以看出Servlet并不是独立运行的程序,而是以服务器为宿主,由服务器进行调度的。通常我们把能够运行Servlet的服务器称作Servlet容器,如Tomcat。

这里Tomcat为什么能够根据客户端的请求去选择相应的Servlet去执行的呢?答案是:Servlet规范。因为Servlet和Servlet容器都是遵照Servlet规范去开发的。简单点说:我们要写一个Servlet,就需要直接或间接实现javax.servlet.Servlet。并且在web.xml中进行相应的配置。Tomcat在接收到客户端的请求时,会根据web.xml里面的配置去加载、初始化对应的Servlet实例。这个就是规范,就是双方约定好的。

二、样例分析

在进一步解释Servlet原理、分析源码之前,我们先介绍下如何在JavaWeb中使用Servlet。方法很简单:1.编写自己的Servlet类,这里可以使用开发工具(STS、Myeclipse等)根据向导快速的生成一个Servlet类。2.在web.xml中配置servlet。这里的知识很简单,所以不做过多赘述。直接上代码。(这里需要注意的是,servlet3.0之后提供了注解的方式配置servlet,这里就不做介绍了,感兴趣的可以自行去百度,只是配置的形式不同而已,没有本质区别。所以下文还是为web.xml为例)

TestServlet.java

1publicclassTestServletextendsHttpServlet{

2privatestaticfinallongserialVersionUID=1L;

3

4publicTestServlet(){

5}

6

7protectedvoiddoGet(HttpServletRequestrequest,HttpServletResponseresponse)throwsServletException,IOException{

8response.getWriter().append("Servedat:").append(request.getContextPath());

9}

10

11protectedvoiddoPost(HttpServletRequestrequest,HttpServletResponseresponse)throwsServletException,IOException{

12doGet(request,response);

13}

14}

web.xml

1

2TestServlet

3com.nantang.servlet.TestServlet

4


5

6TestServlet

7/test

8


启动Tomcat,浏览器访问/test。将会访问TestServlet。返回客户端请求的上下文路径。



这里需要扩展的有几点:

1.如果一个servlet需要映射多个url-pattern,那么就在标签下写多个,如:

1

2TestServlet

3/test1

4/test2

5


2.对于不同的servlet,不允许出现相同的url-pattern。

3.如果不同的servlet,它们的url-patter存在包含关系,那么容器会调用更具象的servlet去处理客户端请求。比如有两个servlet,servlet1的url-pattern是"/",servlet2的url-pattern是"/test"。那么这个时候如果客户端调用的url是http://localhost:8080/demo/test,容器会使用servlet2去处理客户端的请求。虽然说"/"和"/test"都匹配客户请求的url,但是容器会选择更贴切的。这里不会出现多个servlet处理同一个请求的现象。

三、源码分析

上面说过,我们自己编写的Servlet类都必须直接或间接实现javax.servlet.Servlet。可是上面的例子TestServlet继承的是HttpServlet,那是因为HttpServlet间接的实现了javax.servlet.Servlet。下面是HttpServlet的继承层级(类图中的方法并没有一一列举,因为下面会逐一解释):

下面我们由上往下层层分析:

1ServletContext

一个web应用对应一个ServletContext实例,这个实例包含了所有servlet共享的资源信息。通过提供一组方法给servlet使用,用来和servlet容器通讯,比如获取文件的MIME类型、分发请求、记录日志等。

这里需要注意一点,如果你的应用是分布式部署的,那么每台服务器实例上部署的应用实例都各自拥有一个ServletContext实例。



ServletContext实例是应用部署启动后,servlet容器为应用创建的。当需要初始化servlet的时候,servlet容器会用这个全局的ServletContext实例构造一个ServletConfig实例去初始化servlet。所以说,ServletContext实例是servlet容器为servlet提供的重要资源。

1publicinterfaceServletContext{

2

3publicStringgetContextPath();

4

5publicServletContextgetContext(Stringuripath);

6

7publicintgetMajorVersion();

8publicintgetMinorVersion();

9publicStringgetServerInfo();

10publicStringgetServletContextName();

11

12publicStringgetMimeType(Stringfile);

13

14publicvoidlog(Stringmsg);

15publicvoidlog(Stringmessage,Throwablethrowable);

16

17publicSetgetResourcePaths(Stringpath);

18

19publicURLgetResource(Stringpath)throwsMalformedURLException;

20publicInputStreamgetResourceAsStream(Stringpath);

21

22publicRequestDispatchergetRequestDispatcher(Stringpath);

23publicRequestDispatchergetNamedDispatcher(Stringname);

24

25publicStringgetRealPath(Stringpath);

26

27publicStringgetInitParameter(Stringname);

28publicEnumerationgetInitParameterNames(www.hunanwang.net);

29

30publicObjectgetAttribute(Stringname);

31publicEnumerationgetAttributeNames(www.visa158.com);

32publicvoidsetAttribute(Stringname,Objectobject);

33publicvoidremoveAttribute(Stringname);

34}

1.1getContextPath

方法返回web应用的上下文路径。就是我们部署的应用的根目录名称。拿Tomcat举例,我们在webapps部署了应用demo。那么方法返回"/demo"。如果是部署在ROOT下,那么方法返回空字符串""。这里的路径可以再server.xml里面修改,比如我们的demo应用路径修改为"/test":

1

那么方法将会返回"/test"。

1.2getContext

方法入参为uriPath(String),是一个资源定位符的路径。返回一个ServletContext实例。我们说过一个web应用对应一个ServletContext实例,那么这个方法根据资源的路径返回其servlet上下文。

比如说我们当前应用是demo,这个时候我们要访问servlet容器中的另外一个应用test中的资源index.jsp,假使资源的路径为/test/index.jsp。那么我们就可用通过调用getContext("/test/index.jsp")去获取test应用的上下文。如果在servlet容器中找不到该资源或者该资源限制了外部的访问,那么方法返回null。(这个方法一般配合RequestDispatcher使用,实现请求转发)。

1.3getMajorVersion、getMinorVersion、getServerInfo、getServletContextName

getMajorVersion和getMinorVersion分别返回当前servlet容器支持的ServletAPI最高版本和最低版本。

getServerInfo返回servlet容器的名称和版本,格式为servername/versionnumber。比如我在Tomcat下测试,输出的信息是:ApacheTomcat/9.0.0.M10。当然容器也可以多返回些额外的信息,这个就看各个servlet容器的实现了。

getServletContextName返回应用的名称,这里的名称是web.xml里面配置的display-name,如果没配置则返回null。

1ArchetypeCreatedWebApplication

1.4getMimeType

方法返回文件的MIME类型,MIME类型是容器配置的。可用通过web.xml进行配置,比如:

1

2doc

3application/vnd.ms-word

4


那么我们用浏览器打开文件的时候发现如果是doc文件,则会调用相应的word程序去打开。

1.5log

两个重载的log方法都是记录日志到servlet日志文件,这个对于有编程经验的来说没什么好解释的。需要注意的是servlet日志文件的路径由具体的servlet容器自己去决定。如果你是在MyEclipse、STS这类的IDE中跑应用的话,那么日志信息将在控制台(Console)输出。如果是发布到Tomcat下的话,日志文件是Tomcat目录下的/logs/localhost.yyyy-MM-dd.log。

1.6getResourcePaths

根据传入的路径,列出该路径下的所有资源路径。

比如我们的web应用下有这些资源:/welcome.html,/catalog/index.html,/catalog/products.html,/catalog/offers/books.html,/catalog/offers/music.html,/customer/login.jsp,/WEB-INF/web.xml,/WEB-INF/classes/com.acme.OrderServlet.class。

如果调用方法getResourcePaths("/"),那么返回的是{"/welcome.html","/catalog/","/customer/","/WEB-INF/"}。

如果调用方法getResourcePaths("/catalog/"),那么返回的是{"/catalog/index.html","/catalog/products.html","/catalog/offers/"}。

这里需要注意的是:1.路径一定要以"/"开头,结尾的"/"可要可不要。2.路径要从应用根目录下开始,如果调用方法getResourcePaths("/offers/"),此时返回的是null。

1.7getResource和getResourceAsStream

getResource将指定路径的资源封装成URL实例并返回,getResourceAsStream获取指定路径资源的输入流并返回。这里和上面一样,资源的路径以"/"开头,从应用根目录开始。关于URL和InputStream的解释和使用,不在本篇博文的关注点。

1.8getRequestDispatcher和getNamedDispatcher

讲指定的资源包装成RequestDispatcher实例并返回。区别是前者根据资源路径(和上面规则一致),后者根据资源的名称(通过服务器控制台或者web.xml里面配置的,比如web.xml里面配置servlet的名称)。

RequestDispatcher这个接口,看名字就知道用来进行请求分发的。所有的资源都可以包装成RequestDispatcher实例(主要是用于包装servlet),然后调用它的方法进行转发和包含。这里比较简单,不过过多赘述,直接上源码。

1publicinterfaceRequestDispatcher{

2

3publicvoidforward(ServletRequestrequest,ServletResponseresponse)throwsServletException,IOException;

4

5publicvoidinclude(ServletRequestrequest,ServletResponseresponse)throwsServletException,IOException;

6}

还拿我们上面的demo应用例子来说:客户端访问http://xxx.xxx.xxx.xxx:xxxx/demo/test,这个时候访问我们的TestServlet。这个时候如果要把请求转发到另外一个servlet,假使这个servlet的资源路径是/demo/test2。那么我们可以调用getRequestDispatcher("/demo/test2")把资源包装成RequestDispatcher实例,再调用forward的方法。就实现了请求的转发。

请求的转发使用起来很简单,因为servlet容器提供了ServletContext实例,我们在应用中只需要调用它的API就行。这个时候容器为我们做了很多事情,容器会根据资源的路径去获取ServletContext实例,正如上面的getContext方法。这里不一定就是当前ServletContext,可以使其他应用的。如果找到了ServletContext,容器再将资源包装成RequestDispatcher实例进行转发。

1.9getRealPath

根据资源虚拟路径,返回实际路径。

比如说应用中有个JSP页面index.jsp,调用getRealPath("index.jsp"),则返回index.jsp文件在文件系统中的绝对路径。在windows下或许是这样:D:\xxx\xxx\index.jsp,在linux下或许是这样:/root/xxx/index.jsp。

这里可能存在应用中有多个index.jsp,它们在不同的路径下。这时候servlet容器是从应用根目录下向下查找,把找到的第一个资源绝对路径返回。

1.10getInitParameter和getInitParameterNames

这两个方法是用来获取应用的初始化参数的,参数的作用域是整个应用。这个参数是在web.xml里面配置的(如下所示)。getInitParameter是根据参数名获取参数值,getInitParameterNames获取参数名集合。

这里需要注意的是当需要配置多个初始化参数时,应该写多个对,而不是在一个对里面写多个对。

1

2param1

31

4


5

6param2

72

8


1.11getAttribute、getAttributeNames、setAttribute、removeAttribute

应用的属性相关操作。建议属性名遵循java包名的风格,比如Tomcat默认初始化的属性有:

javax.servlet.context.tempdir=java.lang.Object@41f4cc21,

org.apache.catalina.resources=java.lang.Object@41f4cc21,

org.apache.tomcat.InstanceManager=java.lang.Object@41f4cc21,

org.apache.catalina.jsp_classpath=java.lang.Object@41f4cc21,

javax.websocket.server.ServerContainer=java.lang.Object@41f4cc21,

org.apache.jasper.compiler.TldCache=java.lang.Object@41f4cc21,

org.apache.tomcat.JarScanner=java.lang.Object@41f4cc21

这里需要注意的是,这些属性都是应用级的,在一个地方设置的属性可以被应用中其他地方使用。

2.ServletConfig

ServletConfig实例是由servlet容器构造的,当需要初始化servlet的时候,容器根据web.xml中的配置以及运行时环境构造出ServletConfig实例,并通过回调servlet的init方法传递给servlet(这个方法后面会讲到)。所以一个servlet实例对应一个ServletConfig实例。



?

1publicinterfaceServletConfig{

2

3publicStringgetServletName();

4

5publicServletContextgetServletContext();

6

7publicStringgetInitParameter(Stringname);

8

9publicEnumerationgetInitParameterNames();

10}

2.1?getServletName

getServletName方法返回servlet实例的名称,这个就是我们在web.xml中标签中配置的名字,当然也可以在服务器控制台去配置。如果这两个地方都没有配置servlet名称,那么将会返回servlet的类名。

2.2?getServletContext

getServletContext方法返回ServletContext实例,也就是我们上面说的应用上下文。

2.3?getInitParameter和getInitParameterNames

这两个方法是用来获取servlet的初始化参数的,这个参数是在web.xml里面配置的(如下所示)。getInitParameter是根据参数名获取参数值,getInitParameterNames获取参数名集合。

这里需要注意的是当需要配置多个初始化参数时,应该写多个对,而不是在一个对里面写多个对。

1

2TestServlet

3com.nantang.servlet.TestServlet

4

5a

61

7


8

9b

102

11


12


3Servlet

最原始最简单的JaveWeb模型,就是一个servlet容器上运行着若干个servlet用来处理客户端的请求。所以说servlet是JavaWeb最核心的东西,我们的业务逻辑基本上都是通过servlet实现的(虽然现在有各种框架,不用去直接编写servlet,但本质上还是在使用servlet)。

1publicinterfaceServlet{

2

3publicvoidinit(ServletConfigconfig)throwsServletException;

4

5publicServletConfiggetServletConfig();

6

7publicvoidservice(ServletRequestreq,ServletResponseres)throwsServletException,IOException;

8

9publicStringgetServletInfo();

10

11publicvoiddestroy();

12}

所有的servlet都是javax.servlet.Servlet的子类,就像Java里面所有的类都是Object的子类一样。Servlet类规定了每个servlet应该实现的方法,这个是遵循Servlet规范的。但是自定义的servlet一般不用直接实现Servlet,而是继承javax.servlet.GenericServlet或者javax.servlet.http.HttpServlet就行了。我们上面的TestServlet就是继承HttpServlet,这是因为HttpServlet间接实现了Servlet,提供了通用的功能。所以我们在自定义的TestServlet里面只需要专注实现业务逻辑就行了。

Servlet里面有三个比较重要的方法:init、service、destroy。它们被称作是servlet生命周期的方法,它们都是由servlet容器调用。另外两个方法用于获取servlet相关信息的,需要根据业务逻辑进行实现和调用。



3.1init

init方法是servlet的初始化方法,当客户端第一次请求servlet的时候,JVM对servlet类进行加载和实例化。这里需要注意的是,servlet会先执行默认的构造函数,然后回调servlet实例的init方法,传入ServletConfig参数。这个参数上面说过,是servlet容器根据web.xml中的配置和运行时环境构造的实例。通过init方法注入到servlet。init方法在servlet的生命周期中只会被调用一次,在客户端的后续请求中将不会再调用。

3.2service

service方法是处理业务逻辑的核心方法。当servlet容器接收到客户端的请求后,会根据web.xml中配置的找到相应的servlet,回调service方法处理客户端的请求并给出响应。

3.3destroy

JDK文档解释这个方法说:这个方法会在所有的线程的service()方法执行完成或者超时后执行。这里只是说明了,当servlet容器要去调用destroy方式的时候,需要等待一会,等待所有线程都执行完或者达到超时的限制。

这里并没有说清楚什么情况下servlet容器会触发这个动作。HowTomcatWorks一书中对这个做了解释:当servlet容器关闭或需要更多内存的时候,会销毁servlet。这个方法就使得servlet容器拥有回收资源的能力。

同样地,destroy方法在servlet的生命周期中只会被调用一次。

3.4?getServletConfig

这个方法返回ServletConfig实例,这个对象即为servlet容器回调init方法的时候传入的实例。所以自定义的Servlet一般的实现方式为:在init方法里面把传入的ServletConfig存储到servlet的属性字段。在getServletConfig的实现里返回该实例。这个在后续解释javax.servlet.GenericServlet的源码时,能够看到。

3.5getServletInfo

返回关于servlet的信息,这个由自定义的servlet自行实现,不过一般建议返回servlet的作者、版本号、版权等信息。

4.GenericServlet

GenericServlet从名字就能看的出来是servlet的一般实现,实现了servlet具有的通用功能,所以我们自定义的servlet一般不需要直接实现Servlet接口,只需要集成GenericServlet。GenericServlet实现了Servlet和ServletConfig接口。

4.1?GenericServlet对Servlet接口的实现

1privatetransientServletConfigconfig;

2

3publicvoidinit(ServletConfigconfig)throwsServletException{

4this.config=config;

5this.init();

6}

7

8publicvoidinit()throwsServletException{}

9

10publicabstractvoidservice(ServletRequestreq,ServletResponseres)throwsServletException,IOException;

11

12publicvoiddestroy(){}

13

14publicServletConfiggetServletConfig(){

15returnconfig;

16}

17

18publicStringgetServletInfo(){

19return"";

20}

可以说,GenericServlet对Servlet方法的实现逻辑非常简单。就是把一些必要的逻辑写了下。

1.init方法就是把容器传入的ServletConfig实力存储在类的私有属性conifg里面,然后调用一个init无参的空方法。这么做的意义在于,我们如果想在自定义的servlet类里面在初始化的时候添加些业务逻辑,只需要重写无参的init方法就好了,我们不需要关注ServletConfig实例的存储细节了。

2.service和destroy方法并未实现具体逻辑。

3.getServletConfig就是返回init方法里面存储的config。getServletInfo就是返回空字符串,如果有业务需要,可以在子类里面重写。

4.2?GenericServlet对于ServletConfig接口的实现

1publicStringgetServletName(){

2ServletConfigsc=getServletConfig();

3if(sc==null){

4thrownewIllegalStateException(

5lStrings.getString("err.servlet_config_not_initialized"));

6}

7returnsc.getServletName();

8}

9

10publicServletContextgetServletContext(){

11ServletConfigsc=getServletConfig();

12if(sc==null){

13thrownewIllegalStateException(

14lStrings.getString("err.servlet_config_not_initialized"));

15}

16returnsc.getServletContext();

17}

18

19publicStringgetInitParameter(Stringname){

20ServletConfigsc=getServletConfig();

21if(sc==null){

22thrownewIllegalStateException(

23lStrings.getString("err.servlet_config_not_initialized"));

24}

25returnsc.getInitParameter(name);

26}

27

28publicEnumerationgetInitParameterNames(){

29ServletConfigsc=getServletConfig();

30if(sc==null){

31thrownewIllegalStateException(

32lStrings.getString("err.servlet_config_not_initialized"));

33}

34returnsc.getInitParameterNames();

35}

这四个方法的实现就跟一个模子刻出来的一样,都是取得ServletConfig实例,然后调用相应的方法。其实GenericServlet完全没有必要实现ServletConfig,这么做仅仅是为了方便。当我们集成GenericServlet写自己的servlet的时候,如果需要获取servlet的配置信息如初始化参数,就不需要写形如:“ServletConfigsc=?getServletConfig();if(sc==?null)?...;return?sc.getInitParameterNames();”这些冗余代码了。除此之外,没有别的意义。

5HttpServlet

HttpServlet是一个针对HTTP协议的通用实现,它实现了HTTP协议中的基本方法get、post等,通过重写service方法实现方法的分派。



1publicvoidservice(ServletRequestreq,ServletResponseres)throwsServletException,IOException{

2HttpServletRequestrequest;

3HttpServletResponseresponse;

4try{

5request=(HttpServletRequest)req;

6response=(HttpServletResponse)res;

7}catch(ClassCastExceptione){

8thrownewServletException("non-HTTPrequestorresponse");

9}

10service(request,response);

11}

重写的service方法将参数转换成HttpServletRequest和HttpServletResponse,并调用自己的另一个重载service方法。

1protectedvoidservice(HttpServletRequestreq,HttpServletResponseresp)throwsServletException,IOException{

2Stringmethod=req.getMethod();

3if(method.equals(METHOD_GET)){

4longlastModified=getLastModified(req);

5if(lastModified==-1){

6doGet(req,resp);

7}else{

8longifModifiedSince=req.getDateHeader(HEADER_IFMODSINCE);

9if(ifModifiedSince<(lastModified/10001000)){

10maybeSetLastModified(resp,lastModified);

11doGet(req,resp);

12}else{

13resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);

14}

15}

16}elseif(method.equals(METHOD_HEAD)){

17longlastModified=getLastModified(req);

18maybeSetLastModified(resp,lastModified);

19doHead(req,resp);

20}elseif(method.equals(METHOD_POST)){

21doPost(req,resp);

22}elseif(method.equals(METHOD_PUT)){

23doPut(req,resp);

24}elseif(method.equals(METHOD_DELETE)){

25doDelete(req,resp);

26}elseif(method.equals(METHOD_OPTIONS)){

27doOptions(req,resp);

28}elseif(method.equals(METHOD_TRACE)){

29doTrace(req,resp);

30}else{

31StringerrMsg=lStrings.getString("http.method_not_implemented");

32Object[]errArgs=newObject[1];

33errArgs[0]=method;

34errMsg=MessageFormat.format(errMsg,errArgs);

35resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED,errMsg);

36}

37}

这个方法的的逻辑也很简单,就是解析出客户端的request是哪种中方法,如果是get方法则调用doGet,如果是post则调用doPost等等。这样我们在继承HttpServlet的时候就无需重写service方法,我们可以根据自己的业务重写相应的方法。一般情况下我们的应用基本就是get和post调用。那么我们只需要重写doGet和doPost就行了。

这里需要注意的是3-15行代码,这里对资源(比如页面)的修改时间进行验证,判断客户端是否是第一次请求该资源,或者该资源是否被修改过。如果这两个条件有一个被满足那么就调用doGet方法。否则返回状态304(HttpServletResponse.SC_NOT_MODIFIED),这个状态就是告诉客户端(浏览器),可以只用自己上一次对该资源的缓存。

不过HttpServlet对于判断资源修改时间的逻辑非常简单粗暴:

1protectedlonggetLastModified(HttpServletRequestreq){

2return-1;

3}

方法始终返回-1,这样就会导致每次都会调用doGet方法从服务器取资源而不会使用浏览器的本地缓存。所以如果我们自己的servlet要使用浏览器的缓存,降低服务器的压力,就需要重写getLastModified方法。

最后我们来看一下HttpServlet对http一些方法的实现,在所有的方法中,HttpServlet已经对doOptions和doTrace方法实现了通用的逻辑,所以我们一般不用重写这两个方法,感兴趣的可以自己去看下源码。

这里我们列举下最常用的两个方法doGet和doPost:

1protectedvoiddoGet(HttpServletRequestreq,HttpServletResponseresp)throwsServletException,IOException{

2Stringprotocol=req.getProtocol();

3Stringmsg=lStrings.getString("http.method_get_not_supported");

4if(protocol.endsWith("1.1")){

5resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED,msg);

6}else{

7resp.sendError(HttpServletResponse.SC_BAD_REQUEST,msg);

8}

9}

10

11protectedvoiddoPost(HttpServletRequestreq,HttpServletResponseresp)throwsServletException,IOException{

12Stringprotocol=req.getProtocol();

13Stringmsg=lStrings.getString("http.method_post_not_supported");

14if(protocol.endsWith("1.1")){

15resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED,msg);

16}else{

17resp.sendError(HttpServletResponse.SC_BAD_REQUEST,msg);

18}

19}

其实这两个实现里面也没做什么有用的逻辑,所以一般情况下都要重写这两个方法,就像我们最初的TestServlet那样。doHead、doPut、doDelete也是这样的代码模板,所以如果有业务需要的话,我们都要重写对应的方法。

四、总结

讲了这么多,可以这么说:Servlet是JavaWeb里面最核心的组件。只有对它完全融会贯通,才能去进一步去理解上层框架Struts、Spring等。

另外需要明确的是:一个Web应用对应一个ServletContext,一个Servlet对应一个ServletConfig。每个Servlet都是单例的,所以需要自己处理好并发的场景。

























献花(0)
+1
(本文系白狐一梦首藏)