Servlet是运行在Web服务器上的小程序,经过http协议和客户端进行交互。html
这里的客户端通常为浏览器,发送http请求(request)给服务器(如Tomcat)。服务器接收到请求后选择相应的Servlet进行处理,并给出响应(response)。java
从这里能够看出Servlet并非独立运行的程序,而是以服务器为宿主,由服务器进行调度的。一般咱们把可以运行Servlet的服务器称做Servlet容器,如Tomcat。web
这里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以后提供了注解WebServlet的方式配置servlet,这里就不作介绍了,感兴趣的能够自行去百度,只是配置的形式不一样而已,没有本质区别。因此下文仍是为web.xml为例)浏览器
TestServlet.java缓存
1 public class TestServlet extends HttpServlet { 2 private static final long serialVersionUID = 1L; 3 4 public TestServlet() { 5 } 6 7 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 8 response.getWriter().append("Served at: ").append(request.getContextPath()); 9 } 10 11 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 12 doGet(request, response); 13 } 14 }
web.xml服务器
1 <servlet> 2 <servlet-name>TestServlet</servlet-name> 3 <servlet-class>com.nantang.servlet.TestServlet</servlet-class> 4 </servlet> 5 <servlet-mapping> 6 <servlet-name>TestServlet</servlet-name> 7 <url-pattern>/test</url-pattern> 8 </servlet-mapping>
启动Tomcat,浏览器访问/test。将会访问TestServlet。返回客户端请求的上下文路径。并发
这里须要扩展的有几点:app
1.若是一个servlet须要映射多个url-pattern,那么就在<servlet-mapping></servlet-mapping>标签下写多个<url-pattern></url-pattern>,如:框架
1 <servlet-mapping> 2 <servlet-name>TestServlet</servlet-name> 3 <url-pattern>/test1</url-pattern> 4 <url-pattern>/test2</url-pattern> 5 </servlet-mapping>
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的继承层级(类图中的方法并无一一列举,由于下面会逐一解释):
下面咱们由上往下层层分析:
一个web应用对应一个ServletContext实例,关于ServletContext的详细介绍,能够参考另外一篇博文ServletContext。
ServletConfig实例是由servlet容器构造的,当须要初始化servlet的时候,容器根据web.xml中的配置以及运行时环境构造出ServletConfig实例,并经过回调servlet的init方法传递给servlet(这个方法后面会讲到)。因此一个servlet实例对应一个ServletConfig实例。
1 public interface ServletConfig { 2 3 public String getServletName(); 4 5 public ServletContext getServletContext(); 6 7 public String getInitParameter(String name); 8 9 public Enumeration getInitParameterNames(); 10 }
getServletName方法返回servlet实例的名称,这个就是咱们在web.xml中<servlet-name>标签中配置的名字,固然也能够在服务器控制台去配置。若是这两个地方都没有配置servlet名称,那么将会返回servlet的类名。
getServletContext方法返回ServletContext实例,也就是咱们上面说的应用上下文。
这两个方法是用来获取servlet的初始化参数的,这个参数是在web.xml里面配置的(以下所示)。getInitParameter是根据参数名获取参数值,getInitParameterNames获取参数名集合。
这里须要注意的是当须要配置多个初始化参数时,应该写多个<init-param></init-param>对,而不是在一个<init-param></init-param>对里面写多个<param-name></param-name>和<param-value></param-value>对。
1 <servlet> 2 <servlet-name>TestServlet</servlet-name> 3 <servlet-class>com.nantang.servlet.TestServlet</servlet-class> 4 <init-param> 5 <param-name>a</param-name> 6 <param-value>1</param-value> 7 </init-param> 8 <init-param> 9 <param-name>b</param-name> 10 <param-value>2</param-value> 11 </init-param> 12 </servlet>
最原始最简单的JaveWeb模型,就是一个servlet容器上运行着若干个servlet用来处理客户端的请求。因此说servlet是JavaWeb最核心的东西,咱们的业务逻辑基本上都是经过servlet实现的(虽然如今有各类框架,不用去直接编写servlet,但本质上仍是在使用servlet)。
1 public interface Servlet { 2 3 public void init(ServletConfig config) throws ServletException; 4 5 public ServletConfig getServletConfig(); 6 7 public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException; 8 9 public String getServletInfo(); 10 11 public void destroy(); 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相关信息的,须要根据业务逻辑进行实现和调用。
init方法是servlet的初始化方法,当客户端第一次请求servlet的时候,JVM对servlet类进行加载和实例化。(若是须要容器启动时就初始化servlet,能够在web.xml配置<load-on-startup>1</load-on-startup>)
这里须要注意的是,servlet会先执行默认的构造函数,而后回调servlet实例的init方法,传入ServletConfig参数。这个参数上面说过,是servlet容器根据web.xml中的配置和运行时环境构造的实例。经过init方法注入到servlet。init方法在servlet的生命周期中只会被调用一次,在客户端的后续请求中将不会再调用。
service方法是处理业务逻辑的核心方法。当servlet容器接收到客户端的请求后,会根据web.xml中配置的<url-pattern>找到相应的servlet,回调service方法处理客户端的请求并给出响应。
JDK文档解释这个方法说:这个方法会在全部的线程的service()方法执行完成或者超时后执行。这里只是说明了,当servlet容器要去调用destroy方式的时候,须要等待一会,等待全部线程都执行完或者达到超时的限制。
这里并无说清楚什么状况下servlet容器会触发这个动做。How Tomcat Works一书中对这个作了解释:当servlet容器关闭或须要更多内存的时候,会销毁servlet。这个方法就使得servlet容器拥有回收资源的能力。
一样地,destroy方法在servlet的生命周期中只会被调用一次。
这个方法返回ServletConfig实例,这个对象即为servlet容器回调init方法的时候传入的实例。因此自定义的Servlet通常的实现方式为:在init方法里面把传入的ServletConfig存储到servlet的属性字段。在getServletConfig的实现里返回该实例。这个在后续解释javax.servlet.GenericServlet的源码时,可以看到。
返回关于servlet的信息,这个由自定义的servlet自行实现,不过通常建议返回servlet的做者、版本号、版权等信息。
GenericServlet从名字就能看的出来是servlet的通常实现,实现了servlet具备的通用功能,因此咱们自定义的servlet通常不须要直接实现Servlet接口,只须要集成GenericServlet。GenericServlet实现了Servlet和ServletConfig接口。
1 private transient ServletConfig config; 2 3 public void init(ServletConfig config) throws ServletException { 4 this.config = config; 5 this.init(); 6 } 7 8 public void init() throws ServletException {} 9 10 public abstract void service(ServletRequest req, ServletResponse res) throws ServletException, IOException; 11 12 public void destroy() {} 13 14 public ServletConfig getServletConfig() { 15 return config; 16 } 17 18 public String getServletInfo() { 19 return ""; 20 }
能够说,GenericServlet对Servlet方法的实现逻辑很是简单。就是把一些必要的逻辑写了下。
1.init方法就是把容器传入的ServletConfig实力存储在类的私有属性conifg里面,而后调用一个init无参的空方法。这么作的意义在于,咱们若是想在自定义的servlet类里面在初始化的时候添加些业务逻辑,只须要重写无参的init方法就行了,咱们不须要关注ServletConfig实例的存储细节了。
2.service和destroy方法并未实现具体逻辑。
3.getServletConfig就是返回init方法里面存储的config。getServletInfo就是返回空字符串,若是有业务须要,能够在子类里面重写。
1 public String getServletName() { 2 ServletConfig sc = getServletConfig(); 3 if (sc == null) { 4 throw new IllegalStateException( 5 lStrings.getString("err.servlet_config_not_initialized")); 6 } 7 return sc.getServletName(); 8 } 9 10 public ServletContext getServletContext() { 11 ServletConfig sc = getServletConfig(); 12 if (sc == null) { 13 throw new IllegalStateException( 14 lStrings.getString("err.servlet_config_not_initialized")); 15 } 16 return sc.getServletContext(); 17 } 18 19 public String getInitParameter(String name) { 20 ServletConfig sc = getServletConfig(); 21 if (sc == null) { 22 throw new IllegalStateException( 23 lStrings.getString("err.servlet_config_not_initialized")); 24 } 25 return sc.getInitParameter(name); 26 } 27 28 public Enumeration getInitParameterNames() { 29 ServletConfig sc = getServletConfig(); 30 if (sc == null) { 31 throw new IllegalStateException( 32 lStrings.getString("err.servlet_config_not_initialized")); 33 } 34 return sc.getInitParameterNames(); 35 }
这四个方法的实现就跟一个模子刻出来的同样,都是取得ServletConfig实例,而后调用相应的方法。其实GenericServlet彻底没有必要实现ServletConfig,这么作仅仅是为了方便。当咱们集成GenericServlet写本身的servlet的时候,若是须要获取servlet的配置信息如初始化参数,就不须要写形如:“ServletConfig sc = getServletConfig();if (sc == null) ...;return sc.getInitParameterNames();”这些冗余代码了。除此以外,没有别的意义。
HttpServlet是一个针对HTTP协议的通用实现,它实现了HTTP协议中的基本方法get、post等,经过重写service方法实现方法的分派。
1 public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException{ 2 HttpServletRequest request; 3 HttpServletResponse response; 4 try { 5 request = (HttpServletRequest) req; 6 response = (HttpServletResponse) res; 7 } catch (ClassCastException e) { 8 throw new ServletException("non-HTTP request or response"); 9 } 10 service(request, response); 11 }
重写的service方法将参数转换成HttpServletRequest和HttpServletResponse,并调用本身的另外一个重载service方法。
1 protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException{ 2 String method = req.getMethod(); 3 if (method.equals(METHOD_GET)) { 4 long lastModified = getLastModified(req); 5 if (lastModified == -1) { 6 doGet(req, resp); 7 } else { 8 long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE); 9 if (ifModifiedSince < (lastModified / 1000 * 1000)) { 10 maybeSetLastModified(resp, lastModified); 11 doGet(req, resp); 12 } else { 13 resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED); 14 } 15 } 16 } else if (method.equals(METHOD_HEAD)) { 17 long lastModified = getLastModified(req); 18 maybeSetLastModified(resp, lastModified); 19 doHead(req, resp); 20 } else if (method.equals(METHOD_POST)) { 21 doPost(req, resp); 22 } else if (method.equals(METHOD_PUT)) { 23 doPut(req, resp); 24 } else if (method.equals(METHOD_DELETE)) { 25 doDelete(req, resp); 26 } else if (method.equals(METHOD_OPTIONS)) { 27 doOptions(req,resp); 28 } else if (method.equals(METHOD_TRACE)) { 29 doTrace(req,resp); 30 } else { 31 String errMsg = lStrings.getString("http.method_not_implemented"); 32 Object[] errArgs = new Object[1]; 33 errArgs[0] = method; 34 errMsg = MessageFormat.format(errMsg, errArgs); 35 resp.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对于判断资源修改时间的逻辑很是简单粗暴:
1 protected long getLastModified(HttpServletRequest req) { 2 return -1; 3 }
方法始终返回-1,这样就会致使每次都会调用doGet方法从服务器取资源而不会使用浏览器的本地缓存。因此若是咱们本身的servlet要使用浏览器的缓存,下降服务器的压力,就须要重写getLastModified方法。
最后咱们来看一下HttpServlet对http一些方法的实现,在全部的方法中,HttpServlet已经对doOptions和doTrace方法实现了通用的逻辑,因此咱们通常不用重写这两个方法,感兴趣的能够本身去看下源码。
这里咱们列举下最经常使用的两个方法doGet和doPost:
1 protected void doGet(HttpServletRequest req, HttpServletResponse resp)throws ServletException, IOException{ 2 String protocol = req.getProtocol(); 3 String msg = lStrings.getString("http.method_get_not_supported"); 4 if (protocol.endsWith("1.1")) { 5 resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, msg); 6 } else { 7 resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg); 8 } 9 } 10 11 protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException{ 12 String protocol = req.getProtocol(); 13 String msg = lStrings.getString("http.method_post_not_supported"); 14 if (protocol.endsWith("1.1")) { 15 resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, msg); 16 } else { 17 resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg); 18 } 19 }
其实这两个实现里面也没作什么有用的逻辑,因此通常状况下都要重写这两个方法,就像咱们最初的TestServlet那样。doHead、doPut、doDelete也是这样的代码模板,因此若是有业务须要的话,咱们都要重写对应的方法。
讲了这么多,能够这么说:Servlet是JavaWeb里面最核心的组件。只有对它彻底融会贯通,才能去进一步去理解上层框架Struts、Spring等。
另外须要明确的是:一个Web应用对应一个ServletContext,一个Servlet对应一个ServletConfig。每一个Servlet都是单例的,因此须要本身处理好并发的场景。
做者:南唐三少
出处:http://www.cnblogs.com/nantang
若是您以为阅读本文对您有帮助,请点一下“推荐”按钮,您的“推荐”将是咱们最大的写做动力!欢迎各位转载,可是未经做者本人赞成,转载文章以后必须在文章页面明显位置给出做者和原文连接,不然保留追究法律责任的权利。