Servlet基础

HTTP协议相关

HTTP协议的特征

  1. 单向性:客户端和服务端建立连接依靠于客户端发送请求,如果客户端不发送请求,服务端不会主动发送主句到客户端
  2. 无状态:HTTP对于事务处理没有记忆能力。无法“断点续传”
  3. 灵活:HTTP允许传输任意类型的数据对象。正在传输的类型有Content-Type加以标记
  4. 简单快速:客户端向服务器发送请求时,只需传送请求方法和路径。请求方法常用的有GET,POST,HEAD,PUT,DELETE。每种方法规定了客户端与服务器建立连接的类型不同。由于HTTP协议简单,使得HTTP服务器的程序规模小,因而通信速度快

请求类型

GET和POST的区别

  1. GET方式提交表单时,表单数据会在地址栏显示。而POST不会
  2. GET方式提交表单时,表单数据长度是有限的(地址栏长度有限)。而POST理论上没有限制
  3. GET方式提交表单时,表单数据都是以字符方式提交。而POST既可以用字符也可以用字节,默认用字符。
  4. GET方式提交表单会在Http数据包中的第一行出现,而POST提交表单会在空一行的body中出现
  5. GET请求能够被缓存,POST请求不能被缓存下来

HTTP数据包

GET方式提交(Java代码控制台打印为例)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class App {
public static void main(String[] args) {
try {
ServerSocket ss = new ServerSocket(8080);
Socket s = ss.accept();
BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
String str;
while ((str = br.readLine()) != null){
System.out.println(str);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

运行以上代码后,在浏览器输入localhost:8080
则在控制台打印以下内容

1
2
3
4
5
6
7
8
9
GET / HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: Idea-7798877d=aa86ef31-e4df-4cc6-bf8d-66407314771c


Servlet继承结构


Servlet的生命周期

Servlet接口中定义了作为一个Servlet在整个生命周期中应该拥有三个阶段

  1. 初始化
  2. 服务 service
  3. 销毁 destroy
1
Servlet的生命周期是由容器管理的,Servlet初始化以后立即调用init()方法,开发者可以重写该方法让Servlet初始化以后执行相应的操作

Tomcat执行Servlet过程(伪代码)

当客户端请求Servlet的时,Tomcat获取HTTP数据包信息,得到了头部信息中的

1
GET /myservlet/hello.do HTTP/1.1

假如有一个类存储HTTP相关信息

1
2
3
4
5
6
7
public class HttpInfo{
private String method;
private String uri;
private String protocol;
....
/* getter and setter */
}

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
43
44
45
46
47
48
49
50
51
52
53
// Tomcat新建一个ServerSocket对象,监听端口
ServerSocket socket = new ServerSocket(监听端口号);
Socket s;
boolean flag = true;
while(flag){
// 获取对应客户端的Socket对象
s = socket.accept();
}
// 通过IO流去读取数据
BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream(),"iso-8859-1");
String line;
HttpInfo info = new HttpInfo();
HttpServletRequest request;
HttpServletResponse response;
while((line = br.readLine()) != null){
// Tomcat解析HTTP数据包
// 例:拆分头部第一行的信息,获取请求方法,URI,协议版本
String[] arr = line.split("\\s");
info.setMethod(arr[0]);
// 存储的已经处理字符串后得到的uri
info.setUri(arr[1]);
info.setProtocol(arr[2]);
// 处理第一行以外的其他信息
...
request = new HttpServetRequest();
request.setMethod(arr[0]);
// 例如获取表单数据
...

// Tomcat解析客户端的相关信息,比如发送请求的客户端的IP地址
reponse.setSocket(s);
}

// 对请求的HTTP数据包解析完毕后,处理请求
// Tomcat在启动时就会解析项目下的部署描述文件web.xml
// 在里面我们在<servlet-class>标签里配置了我们Servlet类的全路径,通过反射获取到Class对象,进而通过Class.newInstance()方法生成实例化对象.
Class clazz = Class.forName(配置文件中Servlet类的全路径);
// 多态性,父类引用指向子类对象
Servlet obj = clazz.newInstance();
// Servlet对象初始化之后的相关操作
// 比如,人到了公司打卡后,职工对象就初始化好了,但是你还不能直接去工作(service)
// 还要去找到你的办公位置,还要把电脑开机等操作,这就是init()里应该做的事情
// 调用的是重写后的init()方法
obj.init();
// 说明是多线程的,只是例子,这种方法开启线程很蠢
new Thread(new St(request, response)).start();
...
...
// 到了容器要销毁Servlet对象之前
// 容器就会去调用destroy()方法
// 还是那个上班的例子
// 办公楼晚上要锁门了,要把所有人都赶走,但是职工必须得在赶走之前,把文档,代码这些保存好,然后把电脑关机,关掉办公室的灯等动作
// 这就是destroy()方法做的类似事情

模拟Tomcat里多线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public St implements Runnable{
private Servlet obj;
private HttpServletRequest request;
private HttpServletResponse response;

public St(Servlet obj, HttpServletRequest request, HttpServletResponse response){
this.obj = obj;
this.request = request;
this.response = response;
}
@Override
public void run(){
// 执行HttpServlet里的service()方法
this.obj.service(request, response);
// 执行后续操作
// 判断request里保存的请求方式
// GET POST ...
// 根据不同的方式请求不同的方法,以GET为例
// 会去调用重写后的doGet方法
...
}
}

总结:

Servlet的生命周期是由容器管理的,分为init,service和destroy三个阶段.
当有客户端第一次访问这个Servlet时,容器会立即初始化这个Servlet对象,初始化结束以后立即调用init()方法,并且在新的线程中调用service()方法.
容器会将初始化后的Servlet对象进行缓存,当有客户端再次请求该Servlet时,容器不会再进行创建,而是直接在新的线程里调用service()方法.
当容器关闭时,容器会在销毁这个Servlet对象之前,调用一次destroy()方法.


Servlet的作用

  1. 获取表单数据
  2. 获取浏览器的附加信息
  3. 处理数据(本身不具备处理数据的能力,比如持久化.它是通过调用其他的处理数据的方式来实现的,比如JDBC…)
  4. 给客户端产生一个响应
  5. 在响应中添加附加信息

Servlet如何获取表单数据

在HttpServletRequest里有几个方法.

1
2
3
4
5
6
7
8
1. String getParameter(String name)
这个方法处理键值对比如key=value,通过key来获取对应的value.但是这个方法不能获取页面上checkbox的value,因为他的数据格式是key=value1&key=value2&key=value3
2. String[] getParameterValues(String name)
该方法就可以处理checkbox的value,它可以获取对应key的所有value
3. String getQueryingString(String str)
这个方法可以获取URL中的查询字符串(?后面的字符串)
4. Map getParameterMap()
这个方法可以获取请求参数将其封装成Map数据格式


解决Servlet中的中文乱码问题

首先要知道的是Tomcat的默认字符集是ISO-8859-1

  1. 通用,通过jdk的new String产生新的对应编码的String对象
1
2
3
4
5
6
7
8
9
/*解决中文乱码问题 第一种方式
GET和POST都有效,但不推荐,会有大量冗余代码*/

String name = request.getParameter("name");
// 乱码
System.out.println(name);
name = new String(name.getBytes("iso-8859-1"),"utf-8");
// 正常
System.out.println(name);
  1. 只适用POST,通过HttpServletRequest的API
1
2
3
4
5
6
7
/*解决中文乱码问题 第二种方式
只适用于POST请求,使用request里的内置方法*/

request.setCharacterEncoding("utf-8");
String name = request.getParameter("name");
// 正常
System.out.println(name);
  1. 通用,修改Tomcat配置文件server.xml
1
2
3
4
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443"
URIEncoding="utf-8" />
1
2
3
4
5
6
7
/*解决中文乱码问题 第三种方式
* 修改Tomcat配置文件server.xml
* 在Connector节点添加URIEncoding="utf-8"
* */
String value = request.getParameter("name");
// 正常
System.out.println(value);

关于Servlet的线程安全的问题

Servlet是一个线程不安全的技术,在Servlet中定义成员变量时,如果一定要定义成员变量,那么以读取为主.尽量不要同时读同时写.如果一定有这样的需求,则需要加锁.
interface SingeThreadModel 是Servlet提供的一个标识接口,该接口标识实现了该接口的Servlet的运行方式会由并行化改为串行化.效率低下,所以该接口在解决线程安全的问题时,已经不推荐使用了.


Servlet文件上传

  1. 在实现文件上传是,表单的提交方式必须是POST方式提交,因为POST请求才支持让表单数据以字节的方式提交,而GET只能是字符提交.
  2. 在form表单中修改请求的信息,将原来默认的字符提交修改为字节提交:通过修改form标签中的enctype属性,将其值改为multipart/form-data,该属性表示当前表单为字节格式,当服务器接收到当前数据包的时候,则不会去解析,所以也无法使用request.getParameter()去获取表单数据.
  3. 在2的情况下,如果需要处理表单中的内容,需要通过request对象的getInputStream()方法来获取一个通信流,通过对流对象的操作完成相应的表单处理.但是相比更推荐使用Apache的common-fileupload组件.

自启动的Servlet

自启动的Servlet的实例化不依赖于客户端请求,而是依赖于容器.当容器启动时就回去初始化这个Servlet.
在web.xml文件中在对应Servlet的节点最后一行添加

1
<load-on-startup>1</load-on-startup>

其中数字表示启动优先级,数字越小,优先级越高


Servlet的常见对象

ServletContext

ServletContext的主要用法
  1. 相对路径转绝对路径

    1
    2
    3
    ServletContext sc = this.getServletContext();
    String rootPath = sc.getRealPath("/file.txt");
    File file = new File(rootPath);
  2. 获取容器附加信息

    1
    2
    3
    4
    5
    6
    7
    8
    // 获取Servlet的主版本号
    int getMajorVersion();
    // 获取Servlet的副版本号
    int getMinorVersion();
    // 获取服务器的版本信息
    String getServerInfo();
    ...
    ...
  3. 全局容器
    Servlet通过以下两个API完成对自身的添加和读取数据的操作.

    1
    2
    3
    4
    // 添加数据
    void setAttribute(String name, Object value);
    // 获取数据
    Object getAttribute(String name);

注意:
建议不要存储业务数据,数据会随着生命周期一直在内存中,增大了服务器的负担.其次也避免了与数据库数据同步的问题.

  1. 读取web.xml里的配置信息
    1
    2
    3
    4
    <context-param>
    <param-name>key</param-name>
    <param-value>value</param-value>
    </context-param>

通过以下方式可以获取该节点信息

1
2
ServletContext sc = this.getServletContext();
String value = sc.getInitParameter("key");

该配置信息是全局可访问,可以配置多个,但是在一个里只能有一个key/value配置.

ServletConfig

作用:用于读取在web.xml的节点中的配置信息.在节点中可以加入节点.

1
2
3
4
5
6
7
8
<servlet>
<servlet-name>MyServlet</servlet-name>
<servlet-class>com.ray.MyServlet</servlet-class>
<init-param>
<param-name>key</param-name>
<param-value>value</param-value>
</init-param>
</servlet>

通过以下方式可以获取配置的key/value

1
2
3
4
5
6
7
8
9
10
11
12
// 手动写key获得value
ServletConfig config = this.getServletConfig();
String value = sc.getInitParameter("key")

// 可以使用另一个方法来获得所有的key/value
ServletConfig config = this.getServletConfig();
Enumeration parameterNames = sc.getInitParameterNames();
String value = "";
while (parameterNames.hasMoreElements()){
value = sc.getInitParameter(parameterNames.nextElement());
System.out.println(value);
}

同样在一个节点中可以配置多个,但是一个只能配置一个key/value信息.
但是这个配置信息是只能在对应配置的Servlet才能访问到.其他Servlet无法访问.

什么是Cookie

Cookie是一个依赖于客户端维持会话状态的对象
Cookie的特点:如果程序需要给客户端浏览器返回一个Cookie,那么则需要自己来创建;Cookie的结构为key/value结构.
Cookie分为两种:

  1. 状态Cookie: 随着浏览器的生命周期存在,浏览器关闭,则对象消失
  2. 持久化Cookie: Cookie对象持久化到磁盘中,当Cookie的存活时间到达时,浏览器不会再在请求中传递该Cookie.对于这些Cookie文件,浏览器会自己管理.通过setMaxAge(int s)方法来将Cookie持久化.

当需要使用Cookie对象向客户端浏览器传递数据,数据本身不能是中文.
当客户端浏览器请求Servlet是,客户端浏览器会将该服务器以前写回的所有Cookie对象在请求中传递.

可以通过以下简单的实例来实现判断用户访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setContentType("text/html;charset=utf-8");
PrintWriter pw = response.getPrintWriter();
Cookie[] cookies = request.getCookies();
if(cookies == null || cookies.length <= 0){
pw.println("欢迎光临!");
Cookie c = new Cookie("cookie","cookie");
// 持久化Cookie,如果没有这一句则是状态Cookie
c.setMaxAge(120);
response.addCookie(c);
} else {
pw.println("欢迎回来!");
}
}

@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doPost(request, response);
}

Cookie中如何传递中文信息
  1. 通过可逆的加密算法

    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
    /* 使用一个Java的加密字符串工具包 EncryptUtils,自行百度或编写 */
    ....
    Cookie[] cookies = request.getCookies();
    if(cookies == null || cookies.length<=0){
    String str = "今天是二零一八年七月十二日";
    // 加密字符串
    String ciphertext = EncryptUtils.encrypt(str);
    Cookie c = new Cookie("cookie", ciphertext);
    c.setMaxAge(60*60*24);
    response.addCookie(c);
    } else {
    pw.println("欢迎回来!");
    Cookie c = null;
    for(Cookie cookie : cookies){
    if("cookie".equals(cookie.getName())){
    c = cookie;
    break;
    }
    }
    if(c != null){
    // 解密字符串
    String text = EncryptUtils.decrypt(c.getValue());
    pw.println("欢迎回来!" + text);
    }
    }
  2. 通过字符编码

    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
    /* 
    使用了java.net包下的操作URL编码的包
    修改了上面代码的加密解密两行,其他完全一致 */
    ....
    Cookie[] cookies = request.getCookies();
    if(cookies == null || cookies.length<=0){
    String str = "今天是二零一八年七月十二日";
    // 加密字符串
    String ciphertext = URLEndoder.encode(str, "utf-8");
    Cookie c = new Cookie("cookie", ciphertext);
    // 单位是秒,60 * 60 * 24 = 1天
    c.setMaxAge(60*60*24);
    response.addCookie(c);
    } else {
    pw.println("欢迎回来!");
    Cookie c = null;
    for(Cookie cookie : cookies){
    if("cookie".equals(cookie.getName())){
    c = cookie;
    break;
    }
    }
    if(c != null){
    // 解密字符串
    String text = URLDecoder.decode(c.getValue(), "utf-8");
    pw.println("欢迎回来!" + text);
    }
    }

HttpSession

什么是HttpSession

HttpSession对象可以建立客户端与服务器之间的对话,但会话是否建立.取决于服务器是否为这个客户端创建了HttpSession对象.如果是,则HttpSession就表示当前客户端与服务器的会话已经建立.进而该HttpSession对象只为该客户端使用,不会与其他客户端共享.

HttpSession的运行过程

首先HttpServletRequest中有两个获取HttpSession的方法,源码如下,删除了部的注释

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
/**
*
* Returns the current HttpSession associated with
* this request or, if there is no
* current session and create is true, returns
* a new session.
*
* If create is false
* and the request has no valid HttpSession,
* this method returns null.
*
* @param create true to create a new session for
* this request if necessary;
* false to return null if there's no current session
*
* @return the HttpSession associated with this request or null
* if create is false and the request has no valid session
*
*/

public HttpSession getSession(boolean create);

/**
*
* Returns the current session associated with this request,
* or if the request does not have a session, creates one.
*
* @return the HttpSession associated with this request
*/
public HttpSession getSession();

阅读了上面的代码可以知道,getSession()和getSession(true)是同一个执行结果.
当客户端浏览器访问Servlet时,如果代码中调用了getSession(boolean create)或者getSession(),那么服务器会根据传递的参数来执行不同的逻辑.如果create参数是true或者没有参数则会在底层的SessionId/HttpSession对象映射中寻找是否有该客户端的HttpSession对象,如果没有则创建一个新的HttpSession对象并且把该对象的SessionId通过状态Cookie返回给客户端.如果create参数是false,同样也会在底层的SessionId/HttpSession对象映射中寻找是否有该客户端的HttpSession对象,但是如果没有对应的HttpSession对象,则会返回null.


HttpSession的生命周期
  1. 创建: 当有客户端浏览器第一次请求Servlet时,该Servlet中调用了HttpServletRequest里的getSession(boolean create)或者request.getSession()方法时,则会创建一个HttpSession对象.
  2. 销毁:

    1. 使用HttpSession里的invalidate()方法,该方法会直接是该Session直接销毁,并且取消绑定的任何对象.

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      /**
      *
      * Invalidates this session then unbinds any objects bound
      * to it.
      *
      * @exception IllegalStateException if this method is called on an
      * already invalidated session
      *
      */

      public void invalidate();
    2. 修改Tomcat里的conf目录下的web.xml文件,默认是30分钟,超过配置时间则会销毁,对Tomcat里所有项目有效

      1
      2
      3
      <session-config>
      <session-timeout>30</session-timeout>
      </session-config>
    3. 修改本项目的web.xml,同理也是超过配置时间则会自动销毁,但是仅对本项目有效.

      1
      2
      3
      <session-config>
      <session-timeout>1</session-timeout>
      </session-config>

编写一个自定义Servlet

自定义Servlet类继承HttPServlet,并且重写doGet和doPost方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class MyServlet extends HttpServlet {

public MyServlet() {
super();
}

@Override
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
PrintWriter pw = response.getWriter();
pw.println(
"<html><head><title>HelloWorld</title><head><body>" +
"<font color='blue'>HelloWorld</font>"+
"</body><html>");
pw.close();
}

@Override
public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

}
}

编写到运行Servlet的步骤(不使用IDE的方法)

  1. 编写自定义Servlet类继承HttpServlet
  2. 重写doGet和doPost方法
  3. 使用javac工具编译代码
  4. 在Tomcat的webapp目录下创建项目名称文件夹,在该文件夹下创建WEB-INF目录,继续在WEB-INF下创建classes目录,在里面创建包结构
  5. 把javac生成的.class文件放入对应的包结构目录下
  6. 在WEB-INF目录下创建web.xml
  7. 编辑web.xml,配置Servlet

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <web-app>
    <display-name>ArchetypeCreatedWebApplication</display-name>

    <servlet>
    <servlet-name>myServlet</servlet-name>
    <servlet-class>com.ray.servlet.myservlet.MyServlet</servlet-class>
    </servlet>

    <servlet-mapping>
    <servlet-name>myServlet</servlet-name>
    <url-pattern>*.do</url-pattern>
    </servlet-mapping>
    </web-app>
  8. 启动Tomcat容器

  9. 在浏览器地址栏输入地址即可访问
    1
    2
    localhost:8080/(servlet名字)/(请求路径)
    本例为:localhost:8080/myServlet/hello.do
您的支持将让服务器运行更长久