JavaWeb之Servlet技术

Servlet技术

Java Servlet 是运行在 Web 服务器或应用服务器上的程序,它是作为来自 Web 浏览器或其他 HTTP 客户端的请求和 HTTP 服务器上的数据库或应用程序之间的中间层。

使用 Servlet,可以收集来自网页表单的用户输入,呈现来自数据库或者其他源的记录,还可以动态创建网页。换句话说,当请求到达服务器时,web容器会将请求交给对应的Servlet处理,Servlet处理完毕后对客户端的请求进行响应。查看Servlet继承可以看出JavaEE提供了Servlet接口,实现类GenericServlet、HttpServlet,如果要创建自己的Servlet就需要从这些类和接口入手:

新建web项目

新建项目

第一步:Eclipse菜单栏点击File——>New,出现如图界面:

第二步:选择Dynamic web Project,进入如图界面,在图示位置中填写项目名称,项目位置选择默认即可。

如果此时没有Target runtime则点击New Runtime,进入如下界面,选择Tomcat 9。

点击Next按钮,进入如下界面,点击Browser选择Tomcat根目录并点击Finish按钮即可

第三步:如果已经有Target runtime则可以直接点击Finish按钮等待Eclipse开发工具对项目配置。

添加项目运行环境

创建好web项目后,因为项目中还没有web所需的依赖,因此我们要将相关依赖添加进项目,添加过程如下图:

第一步:在项目上点击鼠标右键,按照下图进行选择

第二步:在如图选项卡中点击Libraries选项卡,并点击Add Library。

第三部:在弹出对话框中选择Server Runtime。

第四步:选择Tomcat服务器并点击finish

第五步:在如图界面中Libraries选项卡中如果出现图示内容,即添加成功。

创建Servlet

Servlet接口

要编写一个Servlet,可以继承javax.servlet.Servlet接口,该接口定义了如下5个方法。

  • public void init(ServletConfig config) throws ServletException
  • public void service(ServletRequest req, ServletResponse res) throwsServletException, java.io.IOException
  • public void destroy()
  • public ServletConfig getServletConfig()
  • public java.lang.String getServletInfo()

下面我们依次介绍这5个方法:

  • init():在Servlet实例化之后,Servlet容器会调用init()方法,来初始化该对象,主要是为了让Servlet对象在处理客户请求前可以完成一些初始化的工作,例如,建立数据库的连接,获取配置信息等。对于每一个Servlet实例,init()方法只能被调用一次。init()方法有一个类型为ServletConfig的参数,Servlet容器通过这个参数向Servlet传递配置信息。Servlet使用ServletConfig对象从Web应用程序的配置信息中获取以名-值对形式提供的初始化参数。另外,在Servlet中,还可以通过ServletConfig对象获取描述Servlet运行环境的ServletContext对象,使用该对象,Servlet可以和它的Servlet容器进行通信。
  • service():容器调用service()方法来处理客户端的请求。要注意的是,在service()方法被容器调用之前,必须确保init()方法正确完成。容器会构造一个表示客户端请求信息的请求对象(类型为ServletRequest)和一个用于对客户端进行响应的响应对象(类型为ServletResponse)作为参数传递给service()方法。在service()方法中,Servlet对象通过ServletRequest对象得到客户端的相关信息和请求信息,在对请求进行处理后,调用ServletResponse对象的方法设置响应信息。
  • destroy():当容器检测到一个Servlet对象应该从服务中被移除的时候,容器会调用该对象的destroy()方法,以便让Servlet对象可以释放它所使用的资源,保存数据到持久存储设备中,例如,将内存中的数据保存到数据库中,关闭数据库的连接等。当需要释放内存或者容器关闭时,容器就会调用Servlet对象的destroy()方法。在Servlet容器调用destroy()方法前,如果还有其他的线程正在service()方法中执行,容器会等待这些线程执行完毕或等待服务器设定的超时值到达。一旦Servlet对象的destroy()方法被调用,容器不会再把其他的请求发送给该对象。如果需要该Servlet再次为客户端服务,容器将会重新产生一个Servlet对象来处理客户端的请求。在destroy()方法调用之后,容器会释放这个Servlet对象,在随后的时间内,该对象会被Java的垃圾收集器所回收。
  • getServletConfig():该方法返回容器调用init()方法时传递给Servlet对象的ServletConfig对象,ServletConfig对象包含了Servlet的初始化参数。
  • getServletInfo():返回一个String类型的字符串,其中包括了关于Servlet的信息,例如,作者、版本和版权。该方法返回的应该是纯文本字符串,而不是任何类型的标记(HTML、XML等)

接下来,创建一个Java类,以实现Servlet接口的方式来创建Servlet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package cn.bytecollege.controller;

import java.io.IOException;

import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

public class IndexController implements Servlet{

@Override
public void init(ServletConfig config) throws ServletException {
System.out.println("Servlet init");
}

@Override
public ServletConfig getServletConfig() {
// TODO Auto-generated method stub
return null;
}

@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
res.getWriter().write("Hello,This is IndexController");
}

@Override
public String getServletInfo() {
// TODO Auto-generated method stub
return null;
}

@Override
public void destroy() {
System.out.println("Servlet Destory");

}

}

按照上例代码创建好后可以发现需要重写Servlet接口中的所有方法,这里先不需要关注其他方法,只关注service方法即可。当请求到达web容器后,web容器寻找对应的Servlet,Servlet处理请求的业务都将在Service中进行。

虽然继承Servlet可以实现Servlet的创建,但是这么做有以下几个弊端:

  1. 继承接口就需要重写接口中的方法,在不需要重写的情况的造成代码的冗余。
  2. service方法本身并不具备区分请求类型的能力,需要开发者自行判断是哪种请求类型并作出相应的处理。

继承GenericServlet

除了继承Servlet接口创建Servlet以外,也可以继承GenericServlet,查看GenericServlet源码可以发现该类是一个抽象类,重写了Servlet接口中的方法(虽然方法内什么都没做),并且新增了若干方法。但是继承该类以后可以不重写Servlet中的方法。

同样继承GenericServlet抽象类需要重写Service方法,将Servlet处理请求的业务写在这个方法内。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package cn.bytecollege.controller;

import java.io.IOException;

import javax.servlet.GenericServlet;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

public class MyController extends GenericServlet{

@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
res.getWriter().print("This is MyController");
}
}

继承HttpServlet

这种方式是推荐的创建Servlet的方式,因为HttpServlet继承了GenericServlet后重写了Service方法,并且在Service方法中对请求类型进行了判断,并且提供了doXxx()方法,也就是说,针对不同的请求类型有了不同的处理逻辑,如果需要实现自己的Servlet,重写对应的doXxx()方法即可,通常情况下,重写doGet()方法和doPost()方法即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package cn.bytecollege.controller;

import java.io.IOException;

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

public class MyController2 extends HttpServlet{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.getWriter().write("This is MyController2");
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

}
}

Servlet配置

Servlet编写完毕后,需要对Servlet进行注册,注册后Servlet才能正常服务。这就相当于一个孩子诞生后需要在公安机关登记在册,才能享受应有的社会服务和行使应有的社会职责。

注册Servlet

web.xml配置

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0">
<servlet>
<servlet-name>IndexController</servlet-name>
<servlet-class>cn.bytecollege.controller.IndexController</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>IndexController</servlet-name>
<url-pattern>/index</url-pattern>
</servlet-mapping>
</web-app>
  • servlet-name:只当前servlet的名称,通过该名称可以找到对应的servlet
  • url-pattern:指该servlet访问的路径
  • servlet-class:指该servlet对应的类,需要配置完全限定名

注意:同一个Servlet可以有不同的url,但是一个url只能对应一个Servlet

注解

@WebServlet
1
2
@WebServlet(value = { "/index", "/index2", "/index3" },initParams = {@WebInitParam(name="token",value = "123"),@WebInitParam(name="username",value = "admin")},loadOnStartup = 1)
@WebServlet(urlPatterns="/index")

查看@WebServlet注解源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface WebServlet {

/**
* 等价于web.xml中servlet-name标签,使用注解时该配置可以省略,默认值是当前类名
*/
String name() default "";

/**
* 用于配置Servlet请求路径,其作用等价于urlPatterns
* @see #urlPatterns()
*/
String[] value() default {};

/**
* 等价于web.xml中url-pattern标签,用于配置Servlet请求路径
*/
String[] urlPatterns() default {};

/**
* 设置Servlet是否在容器启动时实例化及初始化,默认值是-1,代表请求到达Servlet时再去实例化和初始化
* 当值大于等于0值,值越小优先级越高,越先被实例化和初始化
*/
int loadOnStartup() default -1;

/**
* 用于配置Servlet初始化参数
*/
WebInitParam[] initParams() default {};

/**
* 设置Servlet是否是异步
*/
boolean asyncSupported() default false;

}

动态注册

JavaEE不但提供了Servlet,还提供了Filter和Listener等组件,而Servlet的动态注册就需要用到Listener,在项目启动时进行注册。注册的步骤如下:

  1. 自定义Listener继承ServletContextListener
  2. 重写contextInitialized()方法
  3. 获取ServletContext对象
  4. 利用createServlet创建Servlet对象
  5. 注册Servlet
  6. 注册Servlet-mapping
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@WebListener
public class MyListener implements ServletContextListener {

public void contextDestroyed(ServletContextEvent sce) {
}
public void contextInitialized(ServletContextEvent sce) {
//1.获取ServletContext
ServletContext sc = sce.getServletContext();

MyServlet servlet = null;
//2.利用createServlet创建Servlet对象
try {
servlet = sc.createServlet(MyServlet.class);
} catch (ServletException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//3.注册Servlet
ServletRegistration.Dynamic dynamic = sc.addServlet("myServlet", servlet);
//4.注册Servlet-mapping
dynamic.addMapping("/myServlet");
}

}

Servlet路径匹配规则

当编写完Servlet后,需要在web.xml中注册Servlet以便访问,也就是说需要在xml中配置请求路径。配置好的路径有如下几种匹配方式,下面将就这几种方式进行讲解

完全匹配

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0">
<!-- 完全匹配 -->
<servlet-mapping>
<servlet-name>IndexController</servlet-name>
<url-pattern>/controller/index</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>IndexController</servlet-name>
<servlet-class>cn.bytecollege.controller.IndexController</servlet-class>
</servlet>
</web-app>

路径匹配

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0">
<!-- 路径匹配 -->
<servlet-mapping>
<servlet-name>OneController</servlet-name>
<url-pattern>/controller/*</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>OneController</servlet-name>
<servlet-class>cn.bytecollege.controller.OneController</servlet-class>
</servlet>
</web-app>

扩展匹配

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0">
<!-- 扩展匹配 -->
<servlet-mapping>
<servlet-name>ThreeController</servlet-name>
<url-pattern>*.jsp</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>ThreeController</servlet-name>
<servlet-class>cn.bytecollege.controller.ThreeController</servlet-class>
</servlet>

</web-app>

注意:扩展匹配前不能有”/“

在上述匹配中的优先级如下

完全匹配>最长路径匹配>路径匹配>扩展匹配

部署Servlet

当我们配置好Servlet后,我们需要添加Tomcat服务器,添加流程如下:

第一步:在Eclipse上方菜单栏中点击window按钮,选择preference。

第二步:在弹出的对话框中搜索server,并在对话框做成点击Runtime Environment,并点击右侧Add按钮

第三步:选择Apache Tomcat v9.0(因为在本教程内使用的是Tomcat9.0),并点击Next按钮

第四步:点击Browser按钮,选择解压后Tomcat的根目录,然后点击finish即可。

至此,我们已经在Eclipse中配置好了web服务器。

接下来,我们在项目上点击右键,选择Run as,并选择服务器即可

Servlet请求流程和生命周期

下面的图示将演示Servlet的请求流程及生命周期

从上图可以看出Servlet生命周期包含以下几步:

  1. 实例化:客户端向服务器发送请求,web容器产生request对象和response对象,并根据URL内存中寻找是否存在对应Servlet的对象,如果不存在则调用Servlet构造方法创建Servlet类的对象,如果存在则调用Service方法,并根据请求类型调用doGet或者doPost方法。对请求进行响应。
  2. 初始化:当Servlet对象创建完成以后调用init方法加载初始参数,该方法只调用1次
  3. 服务:web容器根据请求类型调用sevice方法,对请求进行响应。
  4. 销毁:当容器关闭时调用destory方法,该方法只调用1次

ServletRequest

当客户端浏览器向服务器发送HTTP请求以后,可以看出一个完整的请求报文包含以下内容:请求行,请求头,请求体。

服务器接收到该请求以后,就会对该请求进行解析,并封装为一个ServletRequest子类对象。下面我们来学习ServletRequest中的方法:

方法 描述
public java.lang.Object getAttribute(java.lang.String name) 返回以name为名字的属性的值。如果该属性不存在,这个方法将返回null
public java.util.Enumeration getAttributeNames() 返回请求中所有可用的属性的名字。如果在请求中没有属性,这个方法将返回一个空的枚举集合。
public void removeAttribute(java.lang.String name) 移除请求中名字为name的属性。
public void setAttribute(java.lang.String name, java.lang.Object o) 在请求中保存名字为name的属性。如果第二个参数o为null,那么相当于调用removeAttribute(name)。
public java.lang.String getCharacterEncoding() 返回请求正文使用的字符编码的名字。如果请求没有指定字符编码,这个方法将返回null。
public int getContentLength() 以字节为单位,返回请求正文的长度。如果长度不可知,这个方法将返回-1。
public java.lang.String getContentType() 返回请求正文的MIME类型。如果类型不可知,这个方法将返回null。
public java.lang.String getLocalAddr() 返回接收到请求的网络接口的IP地址
public java.lang.String getLocalName() 返回接收到请求的IP接口的主机名
public int getLocalPort() 返回接收到请求的网络接口的IP端口号
public java.lang.String getParameter(java.lang.String name) 返回请求中name参数的值。如果name参数有多个值,那么这个方法将返回值列表中的第一个值。如果在请求中没有找到这个参数,这个方法将返回null。
public java.util.Enumeration getParameterNames() 返回请求中包含的所有的参数的名字。如果请求中没有参数,这个方法将返回一个空的枚举集合。
public java.lang.String[] getParameterValues(java.lang.String name) 返回请求中name参数所有的值。如果这个参数在请求中并不存在,这个方法将返回null。
public java.lang.String getProtocol() 返回请求使用的协议的名字和版本,例如:HTTP/1.1。
public java.lang.String getRemoteAddr() 返回发送请求的客户端或者最后一个代理服务器的IP地址。
public java.lang.String getRemoteHost() 返回发送请求的客户端或者最后一个代理服务器的完整限定名。
public int getRemotePort() 返回发送请求的客户端或者最后一个代理服务器的IP源端口
public RequestDispatcher getRequestDispatcher(java.lang.Stringpath) 返回RequestDispatcher对象,作为path所定位的资源的封装。
public java.lang.String getServerName() 返回请求发送到的服务器的主机名。通常获取到域名,没有域名则获取IP
public int getServerPort() 返回请求发送到的服务器的端口号。通常获取到URL中的端口号,获取不到重定向后的端口号
public void setCharacterEncoding (java.lang.String env) throwsjava.io.Unsupported EncodingException 覆盖在请求正文中所使用的字符编码的名字。只针对POST请求

请求乱码解决

GET请求乱码

在Tomcat 7 以前Get请求会出现乱码,但是从Tomcat 7 以后解决了这个问题,Get请求很少出现乱码的情况,如出现则可以通过以下两种方式解决。

方式一:

1
String str = new String(province.getBytes("iso-8859-1"), "utf-8");

方式二:

  1. 进入Tomcat下conf目录,找到Server.xml
  2. 搜索8080端口,找到Connector标签,并且在标签后添加URIEncoding属性
1
<Connector  connectionTimeout="20000" port="8080" protocol="HTTP/1.1" redirectPort="8443" URIEncoding="utf-8"/>

POST请求

Post请求出现乱码也是开发中经常遇到的问题,下面这种方式可以解决。

1
2
//获取请求参数前设置
request.setCharacterEncoding("utf-8");

ServletResponse

同理,当服务器要向客户端响应时,也会向客户端浏览器发送响应报文。我们在这里可以简单的理解为web容器会将响应报文封装进ServletResponse子类对象,下面我们学习ServletResponse的方法。

方法 描述
public java.lang.String getContentType() 返回在响应中发送的正文所使用的MIME类型。
public java.lang.String getCharacterEncoding() 返回在响应中发送的正文所使用的字符编码(MIME字符集)。
public ServletOutputStream getOutputStream() throwsjava.io.IOException 返回ServletOutputStream对象,用于在响应中写入二进制数据。javax.servlet.ServletOutputStream是一个抽象类,继承自java.io.OutputStream。
public java.io.PrintWriter getWriter() throws java.io.IOException 返回PrintWriter对象,用于发送字符文本到客户端。PrintWriter对象使用getCharacterEncoding()方法返回的字符编码。如果没有指定响应的字符编码方式,默认将使用ISO-8859-1。
public void setCharacterEncoding(java.lang.String charset) 设置发送到客户端的响应的字符编码,例如,UTF-8。
public void setContentType(java.lang.String type) 设置要发送到客户端的响应的内容类型,此时响应应该还没有提交。给出的内容类型可以包括字符编码说明,例如:text/html;charset=UTF-8。如果这个方法在getWriter()方法被调用之前调用,那么响应的字符编码将仅从给出的内容类型中设置。

响应乱码

Servlet在响应时也有可能出现乱码,这是因为向浏览器响应后,浏览器并不知晓服务端让浏览器以何种字符编码进行显示,浏览器通常会以默认的编码进行解析响应内容,解决办法就是服务端告诉浏览器以何种编码解析即可。

1
response.setContentType("text/hmlt;charset=utf-8");

ServletConfig

该对象用于获取Servlet配置项中配置的初始化参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0">
<servlet-mapping>
<servlet-name>Index</servlet-name>
<url-pattern>/index</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>Index</servlet-name>
<servlet-class>cn.bytecollege.controller.IndexController</servlet-class>
<init-param>
<param-name>token</param-name>
<param-value>21232f297a57a5a743894a0e4a801fc3</param-value>
</init-param>
</servlet>
</web-app>

注意:init-param用于配置初始化的参数,可以理解为其是以键值对的形式存在,其中param-name用于配置初始化参数的名称,param-value用于配置参数的值。该键值对会在Servlet实例化以后被解析并封装进ServletConfig对象。

ServletConfig对象具有以下方法:

方法名 方法描述
public String getServletName(); 获取Servlet实例的名称
public ServletContext getServletContext(); 获取当前应用的上下文
public String getInitParameter(String name); 根据名称获取配置的初始化参数指
public Enumeration<String> getInitParameterNames(); 获取所有的初始化参数的名称,返回值是一个枚举类型

下面我们通过示例来学习ServletConfig接口的方法使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public void init(ServletConfig config) throws ServletException {
System.out.println("--------------------INIT----------------------");
//获取Servlet实例的名称,该值可以在web.xml中配置,也可以在@WebServlet中配置
String servletName = config.getServletName();
//获取当前应用的上下文
ServletContext context = config.getServletContext();
//获取所有初始化参数的名称
Enumeration<String> enumeration = config.getInitParameterNames();
while(enumeration.hasMoreElements()) {
String key = enumeration.nextElement();
//获取初始参数的值
String value = config.getInitParameter(key);
System.out.println(key+"==============="+value);
}
}