Servlet
Servlet是一个运行在Web服务器中的一个Java小程序,它能够接受和处理客户端的请求,并完成对客户端的响应。Servlet是Web应用程序中的核心类,它可以直接处理请求,也可以将请求委托给应用程序中的别的部分进行处理(请求转发、包含),所有的web服务器都内建了一个或者多个servlet,用来处理JSP、HTML页面、图片等等,所以说所有的请求都是要基于servlet的,即使在请求JSP也需要servlet的处理才能够完成,一般我们不需要编写处理JSP、HTML的servlet,因为web容器已经帮我们完成了这个任务。
可以在tomcat的conf目录下的web.xml文件中查看,有关处理JSP的servlet的配置,下面这段配置是tomcat默认配置(这里省略了一些servlet的配置),这个文件相当于写在了每一个使用tomcat做容器的web应用程序之中, org.apache.jasper.servlet.JspServlet这个类会处理所有访问JSP的请求,并且这个servlet在服务器启动时,就会被初始化.
12 3 10 11 12 13jsp 4 5org.apache.jasper.servlet.JspServlet 6 73 8 914 15 jsp 16 17*.jsp 18 19*.jspx 20 21
还有一个默认的servlet,这个servle在服务器启动时就会被创建,如果没有任何servlet处理请求,那么这个servlet会处理,这个servlet匹配一切路径,这个servlet的service方法会给客户端发送状态码404
12 3 10 11 12 13default 4 5org.apache.catalina.servlets.DefaultServlet 6 71 8 914 15 default 16 17/ 18 19
所有的Servlet都实现了javax.servlet.Servlet接口,接口中有如下五个方法:
1 public class HelloServlet implements Servlet { 2 private ServletConfig servletConfig; 3 @Override 4 public void init(ServletConfig servletConfig) throws ServletException { 5 this.servletConfig = servletConfig; 6 System.out.println("init()..."); 7 } 8 @Override 9 public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {10 System.out.println("service()..,.");11 }12 @Override13 public void destroy() {14 System.out.println("destroy()..");15 }16 @Override17 public ServletConfig getServletConfig() {18 return this.servletConfig;19 }20 @Override21 public String getServletInfo() {22 return "";23 }24 }
其中有三个方法是servlet的生命周期方法:
-
init(ServletConfig config)
-
这个方法会在servlet被创建之后立即被调用,只会调用一次
-
服务器会给这个方法传递参数,即 ServletConfig的一个实例
-
-
service(ServletRequest request, ServletResponse response)
-
每次处理请求都是调用这个方法,
-
参数也是由服务器传递
-
-
destroy();
-
servlet在被销毁之前会调用这个方法
-
当然上面的这些方法都不是由我们来调用,都是服务器在调用,我们也不会创建servlet对象,只编写servlet,servlet实例也由服务器创建。
GenericServlet
GenericServlet实现了javax.servlet.Servlet和javax.servlet.ServletConfig接口,我们可以来模仿一下GenericServlet类的实现,重写这两个接口中的方法,其中init()方法中的参数是ServletConfig,我们可以在类中保存这个这个参数,然后使用服务器传递过来的ServletConfig完成ServletConfig接口中的方法,
1 import java.io.IOException; 2 import java.util.Enumeration; 3 4 public abstract class MyGenericServlet implements Servlet, ServletConfig { 5 private ServletConfig servletConfig; 6 7 @Override 8 public void init(ServletConfig servletConfig) throws ServletException { 9 this.init();10 this.servletConfig = servletConfig;11 }12 13 public void init() {14 // 在这里去做一些初始化操作15 }16 17 @Override18 public ServletConfig getServletConfig() {19 return this.servletConfig;20 }21 22 @Override23 public abstract void service(ServletRequest servletRequest, ServletResponse servletResponse) throws24 ServletException, IOException;25 26 @Override27 public String getServletInfo() {28 return null;29 }30 31 @Override32 public void destroy() {33 System.out.println("destroy()");34 }35 36 @Override37 public String getServletName() {38 return this.getServletConfig().getServletName();39 }40 41 @Override42 public ServletContext getServletContext() {43 return this.getServletConfig().getServletContext();44 }45 46 @Override47 public String getInitParameter(String s) {48 return this.getServletConfig().getInitParameter(s);49 }50 51 @Override52 public EnumerationgetInitParameterNames() {53 return this.getServletConfig().getInitParameterNames();54 }55 }
需要注意的是我们在类中重载了一个init()方法,这个方法是无参数的,并且在有参的init方法中调用了这个方法。
为什么要添加这个方法?假设一个场景,我们继承了GenericServlet类重写了service方法,但是我们还想做一些初始化操作,于是我们重写了有参的init()方法,注意这里就出问题了,我们重写了init方法,但是在父类中的init方法中将ServletConfig保存了下来,现在一重写,我们的私有成员变量servletConfig将的得不到赋值,此时如果我们调用getServletContxt()等等方法都会出错,所以重载了一个无参的init方法就防止了此类错误的发生,我们应该重写无参的init方法,来做一些初始化操作,
HttpServlet
前面的Servlet接口,抽象类GenericServlet都是与具体的应用层协议无关的类,我们在编写web应用程序时,一般都是使用与Http协议相关的这个类HttpServlet,这个类继承于GenericServlet,它重写了GenericServlet中的唯一的抽象方法service(),并且在类中重载了一个service(HttpServletRequest request , HttpServletResponse response)方法,注意这个方法的的两个参数也是与Http协议相关的, HttpServletRequest继承于ServletRequest, HttpServletResponse继承于ServletResponse
需要注意的是重载的这个方法虽然名字与service(ServletRequest request, ServletResponse response) 方法一样,但是重载的方法不是生命周期方法,也就是说服务器在处理请求时,是不会调用重载的service方法的,只会调用service(ServletRequest request, ServletResponse response) 这个生命周期方法,我们看HttpServlet的原码可以发现:
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 var6){ 8 throw new ServletException("non-HTTP request or response"); 9 }10 this.service(request,response);11 }
在生命周期service方法中,声明Http类型的request和response,然后进行了强转,并调用了重载的service方法,这里强转能够成功说明服务器给service方法传递的实际还是HttpServletRequest和HttpServletResponse,不然不会强转成功。我们再来看重载的service方法,
1 protected void service(HttpServletRequest req,HttpServletResponse resp)throws ServletException,IOException{ 2 String method=req.getMethod(); 3 long lastModified; 4 if(method.equals("GET")){ 5 6 }else if(method.equals("HEAD")){ 7 lastModified=this.getLastModified(req); 8 this.maybeSetLastModified(resp,lastModified); 9 this.doHead(req,resp);10 }else if(method.equals("POST")){11 this.doPost(req,resp);12 }else if(method.equals("PUT")){13 this.doPut(req,resp);14 }else if(method.equals("DELETE")){15 this.doDelete(req,resp);16 }else if(method.equals("OPTIONS")){17 this.doOptions(req,resp);18 }else if(method.equals("TRACE")){19 this.doTrace(req,resp);20 }else{21 String errMsg=lStrings.getString("http.method_not_implemented");22 Object[]errArgs=new Object[]{method};23 errMsg=MessageFormat.format(errMsg,errArgs);24 resp.sendError(501,errMsg); 25 }
在重载的service方法中,首先会获取请求的方式,这与Http协议有关,所谓的请求方法指的是Http请求报文中的请求首行第一个字段所表示的方法,Http协议规定的请求方法有:
-
GET
-
请求读取由URL所标志的信息(资源),一般没有请求体
-
POST
-
给服务器添加信息,通常用在表单的提交方式中,这种方法会产生请求体
-
-
OPTION
-
请求一些选项信息,比如支持的HTTP方法,
-
-
HEAD
-
与GET方法类似,不过该请求只会返回页面的头部数据
-
-
PUT
-
在指明的URL下存储一个文档
-
-
DELETE
-
删除由URL所标识的资源
-
-
TRACE
-
用于进行环回检测的请报文的请求方法,通常用于诊断的目的
-
-
CONNECT
-
由于代理服务器,HttpServlet中没有这个请求方式对应的方法,所以如果客户端使用这个方法请求服务器将会得到一个501的状态码
-
上述的方法除了CONNECT,其余的方法在HttpServlet都有相应的处理方法,名字都差不多,doXXX,一系列方法,对应了相应的请求方法。
service方法在获取到请求方法之后,就调用相应的doXXX方法来完成响应,所以我们应该重写一系列的doXXX方法,而不应该重写service方法,这才是最简单的方式。
再来看一下在HttpServlet中的doXXX一系列方法的默认实现,这些方法都不是抽象方法,都是有默认实现的,
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(405, msg);6 } else {7 resp.sendError(400, msg);8 }9 }
上面是doGet方法的源码,实现很简单,先获取协议名,若是http1.1版本发送状态码405,并附带一个信息”http.method_get_not_supported”,若是其他版本如http1.0发送状态码400,可见如果我们不重写doGet等一系列方法将会得到一个405或者400的错误页面
但是等等,我们如何使用浏览器访问servlet?这是一个重要的问题。
在web.xml中配置servlet
前面说到了我们可以重写HttpServlet的一系列doXXX方法来完成对客户端请求的响应,但是忘记了一个重要的问题,在浏览器的地址栏输入什么来访问servlet?这么多servlet,如何访问特定的一个servlet?我们可以在web.xml中对servlet进行配置,每个web项目的WEB-INF都有一个web.xml文件,我们在文件中添加如下配置:
12 3 10 11 12 13HelloServlet 4 5yu.servlet.HelloServlet 6 71 8 914 15 HelloServlet 16 17/HelloServlet 18 19
分别进行解释,这段配置分为了两个部分,一个servlet标签,一个 servlet-mapping标签,一个servlet对应web.xml中的这两段配置:
servlet标签中:
<servlet-name>HelloServlet</servlet-name>,这段配置是表示给servlet取个名字 <servlet-class>yu.servlet.HelloServlet</servlet-class> ,指定servlet的类名(带包名)
<load-on-startup>1</load-on-startup> ,在服务器启动时就创建这个servlet的实例,如果没有这段配置,那么服务器会在第一次收到请求这个servlet的请求时,才会创建这个类的实例
servlet-mapping标签中:
<servlet-name>HelloServlet</servlet-name>,必须与servlet标签中配置的name一致,
<url-pattern>/HelloServlet</url-pattern>,这个就是我们在浏览器中可以输入的标识,可以为一个servlet配置多个 url-pattern
这段配置将一个servlet类与一个 url-pattern关联起来,客户端在发出请求时,服务器将解析请求报文中请求首行的URL,若发现与某个servlet的 url-pattern一致,就调用该servlet的service方法完成响应,服务器可以解析xml文件找出匹配的servlet的配置,在通过配置中的 servlet-class,反射创建对象,并调用其service方法。
另外一个配置的servlet的方式是使用注解,这种方式比较简单,但是有一定的缺陷,比如无法控制过滤器的执行顺序?可以查看注解类,查看可以配置的信息,
@WebServlet(name = "AServlet",urlPatterns = {"/AServlet"})