手写tomcat(二)-WEB服务器开发基础

1.WEB 服务器研发过程

1.1 使用 JDK 版本: JDK1.7 或更高版本

在这里插入图片描述

1.2 集成开发环境 IDE: Idea

在这里插入图片描述

1.3 安装浏览器: Chrom

在这里插入图片描述

1.4 安装数据库管理系统

MySQL

1.5 WEB 服务器名称:

mytomcat

2 WEB 服务器研发细节

2.1 mytomcat【工具类的定义】

(1) 设置工作区字符编码 UTF-8(File Encodings)

在这里插入图片描述
(2) 创建 Java Project,项目名称 mytomcat
在这里插入图片描述

(3) 创建软件包: com.bruceliu.httpserver.core,该包存放 WEB 服务器核心程序

A、 package 在 Java 开发中的作用可以很好的组织程序,便于查找与维护
B、 由于 WEB 服务器的研发公司我们自己
C、 所以包名为: com.bruceliu.httpserver.core

在这里插入图片描述
(4) 开始编写 WEB 服务器的第一个 Java 程序: BootStrap.java

A、 该 Java 程序是服务器启动入口,在该 Java 程序中编写 main 方法。
B、 客户端程序我们不需要编写,使用浏览器软件代替。
C、 在 core 包下新建 BootStrap.java 文件,并编写 main 主方法,包含 start()方法【启动入口】,提供相关注释

a、 mytomcat启动入口
b、 @author bruceliu
c、 @version 1.0
d、 @since 1.0

package com.bruceliu.httpserver.core;

/**
 * mytomcat 启动入口
 * @author bruceliu
 * @version 1.0
 * @since 1.0
 */
public class BootStrap {

    /**
     * 主方法
     * @param args
     */
    public static void main(String[] args) {
        

    }
    
    public static void start(){
        
        
    }
}

(5) 日志记录器 Logger.java
A、 需求说明:
在项目开发中都是需要记录日志,日志一般包括很多等级,例如错误 ERROR、警告 WARN、消息INFO 等,并且有专门负责记录日志的开源项目,例如 log4j 可以很好的完成日志管理,我们这里不使用 log4j,自定义日志记录器 Logger 进行日志记录,简单记录消息 INFO,对记录日志有初步认识即可,日志信息要求包括: [日志等级]发生时间 发生了什么,日志直接输出到控制台,日志记录器属于工具。
B、 新建软件包 com.bruceliu.httpserver.util
C、 在包中新建日志记录器 Logger,定义 static log(String info)方法完成日志记录

String datePattern = “yyyy-MM-dd HH:mm:ss SSS”;
SimpleDateFormate dateFormate = new SimpleDateFormate(datePattern);
String strTime = dateFormate.formate(new Date());
System.out.println([INFO]+strTime+” ”+info);

D、 工具类方法一般都是静态的,不需要创建对象, 直接使用类调用,所以工具类的构造方法一般都是私有的,但不是必须的

package com.bruceliu.httpserver.util;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @Auther: bruceliu
 * @Date: 2020/2/5 16:37
 * @QQ:1241488705
 * @Description:日志记录器
 */
public class Logger {
    
    //工具类的方法往往是静态的,直接通过类名调用,不需要去创建对象
    //工具类的构造方法往往也是私有,但不是必须的
    private Logger(){
    }

    /**
     * 普通日志记录器
     * @param msg
     */
    public static void log(String msg){
		SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
		Date nowTime = new Date();
		String nowTimeStr = dateFormat.format(nowTime);
        System.out.println("[INFO] " + nowTimeStr + " " + msg);
    }
}

(6) 定义日期工具类 DateUtil.java:获取系统当前时间,按照规定格式返回字符串日期

private static SimpleDateFormat dateFormat = new SimpleDateFormat(“yyyy-MM-dd HH:mm:ss SSS”);
private DateUtil(){
}
public static String getCurrentTime(){
   return dateFormat.format(new Date());
}

A、日志记录的时候需要获取系统当前时间,
B、 系统当前时间的使用可能不止在日志工具类中需要使用,可能在其它位置也需要使用
C、我们可以再定义一个日期工具类专门获取系统当前时间,这样代码可以复用DateUtil.java 中包含静态方法 getCurrentTime()

package com.bruceliu.httpserver.util;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @Auther: bruceliu
 * @Date: 2020/2/5 16:42
 * @QQ:1241488705
 * @Description:日期工具类
 */
public class DateUtil {

    private static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
    private DateUtil(){}

    /**
     * 获取系统当前时间
     * @return String [yyyy-MM-dd HH:mm:ss SSS]
     */
    public static String getCurrentTime(){
        return dateFormat.format(new Date());
    }
}

(7) 修改日志工具类 Logger 中的 log 方法

package com.bruceliu.httpserver.util;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @Auther: bruceliu
 * @Date: 2020/2/5 16:37
 * @QQ:1241488705
 * @Description:日志记录器
 */
public class Logger {

    //工具类的方法往往是静态的,直接通过类名调用,不需要去创建对象
    //工具类的构造方法往往也是私有,但不是必须的
    private Logger(){
    }

    /**
     * 普通日志记录器
     * @param msg
     */
    public static void log(String msg){
		/*SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
		Date nowTime = new Date();
		String nowTimeStr = dateFormat.format(nowTime);*/
        System.out.println("[INFO] " + DateUtil.getCurrentTime() + " " + msg);
    }
}

2.2 mytomcat【对日志工具进行单元测试】

(1) 软件测试技术
软件测试技术是软件开发过程中的一个重要组成部分,是贯穿整个软件开发生命周期、对软件产品(包括阶段性产品)进行验证和确认的活动过程,其目的是尽快尽早地发现在软件产品中所存在的各种问题-与用户需求、预先定义的不一致性。检查软件产品的 bug。写成测试报告,交于开发人员改。

(2)软件测试技术包括哪些

A、 黑盒测试
黑盒测试也称功能测试,它是通过测试来检测每个功能是否都能正常使用。在测试中,把程序看作一个不能打开的黑盒子,在完全不考虑程序内部结构和内部特性的情况下,在程序接口进行测试,它只检查程序功能是否按照需求规格说明书的规定正常使用,程序是否能适当地接收输入数据而产生正确的输出信息。黑盒测试着眼于程序外部结构,不考虑内部逻辑结构,主要针对软件界面和软件功能进行测试。黑盒测试是以用户的角度,从输入数据与输出数据的对应关系出发进行测试的。

B、 白盒测试
软件的白盒测试是对软件的过程性细节做细致的检查。这种方法是把测试对象看做一个打开的盒子,它允许测试人员利用程序内部的逻辑结构及有关信息,设计或选择测试用例,对程序所有逻辑路径进行测试。通过在不同点检查程序状态,确定实际状态是否与预期的状态一致。因此白盒测试又称为结构测试或逻辑驱动测试。 白盒测试一般有专门的白盒测试人员测试。

C、 单元测试
单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。对于单元测试中单元
的含义,一般来说,要根据实际情况去判定其具体含义,如 Java 里单元指一个类。总的来说,单元就
是人为规定的最小的被测功能模块。单元测试是在软件开发过程中要进行的最低级别的测试活动,软
件的独立单元将在与程序的其他部分相隔离的情况下进行测试。单元测试一般由程序员自己完成。 并
且测试完成之后需要提交单元测试报告。

D、 压力测试
压力测试属于性能测试的一部分, 测试系统的最大负载情况。

E、 其它测试
易用性测试
安全性测试
……

(3) 常见软件测试相关工具

软件错误管理工具 Bugzilla
功能测试工具 WinRunner
负载测试工具 LoadRunner
测试管理工具 TestDirector

(4) 怎么做单元测试

A、 Java 的单元测试有专门的开源项目,例如: JUnit,首先需要到 JUnit 官网下载 JUnit 相关 jar 包,我们这里使用 JUnit4-4.12,
JUnit 官网地址:http://junit.org/junit4/
JUnit 下载地址: https://github.com/junit-team/junit4/releases
在这里插入图片描述
B、 JUnit-4.12 下载完成后存放到本地硬盘中
在这里插入图片描述
C、 将 junit4-r4.12.zip 文件解压缩,将 junit4-r4.12/lib/hamcrest-core-1.3.jar 拷贝到junit4-r4.12 目录外,如图所示
在这里插入图片描述
D、 在集成开发环境 中创建 Java Project,起名 junit-001,将以上下载的junit-4.12.jar、 hamcrest-core-1.3.jar 包添加到 classpath 中
在这里插入图片描述
E、 编写一个普通的 java 类,随意定义一个方法,实现两个 int 类型数字的和,再编写一
个方法,实现两个 int 类型数字的商

package com.bruceliu.junit;

/**
 * @Auther: bruceliu
 * @Date: 2020/2/5 17:20
 * @QQ:1241488705
 * @Description:
 */
public class MyMath {
    
    public static int sum(int a,int b){
        return a+b;
    }

    public static int divide(int a,int b){
        return a/b;
    }
}

F、 单元测试的代码一般有规范的存放方式,在 junit-001 工程中新建“source folder”文件夹,并起名 test, 新建软件包,软件包的路径一般和被测试类所在路径相同, 单元测试代码一般存放到该目录下,新建单元测试类,类一般都是被测试类的类名后添加 Test,如图
在这里插入图片描述
G、 在单元测试类 MathTest 中编写两个方法, 都是 public 的方法,返回值类型 void, 分别testSum 和 testDivide,单元测试类中的测试方法的名字一般都是 test 加被测试方名,并且单元测试的方法需要使用@Test 注解标注

package com.bruceliu.junit;

/**
 * @Auther: bruceliu
 * @Date: 2020/2/5 17:23
 * @QQ:1241488705
 * @Description:
 */
import org.junit.Test;
import static org.junit.Assert.*;

public class MyMathTest {

    @Test
    public void testSum(){

    }

    @Test
    public void testDivide(){
 
    }
}

H、 在单元测试方法中编写单元测试程序

package com.bruceliu.junit;

/**
 * @Auther: bruceliu
 * @Date: 2020/2/5 17:23
 * @QQ:1241488705
 * @Description:
 */
import org.junit.Test;
import static org.junit.Assert.*;

public class MyMathTest {

    @Test
    public void testSum(){
        int actual = MyMath.sum(10, 10);//实际值
        int expected = 20;//期望值
        assertEquals(expected, actual);//断言
    }

    @Test
    public void testDivide(){
        int actual = MyMath.divide(100, 20);//实际值
        int expected = 5;//期望值
        assertEquals(expected, actual);//断言
    }
}

I、 运行单元测试中的方法
在这里插入图片描述
J、 运行结果,下图显示结果表示被测试的方法通过测试
在这里插入图片描述
在这里插入图片描述
K、 修改测试程序,将 Math.java 中的 sum 方法修改为如下代码
在这里插入图片描述
L、 再次运行测试程序,运行结果如下所示,表示被测试的方法存在错误
在这里插入图片描述
(5) 对日志工具类进行单元测试

A、 在 test 目录下新建软件包 com.bruceliu.httpserver.util
B、 新建单元测试类 LoggerTest,编写单元测试方法testLog

在这里插入图片描述

注:这里我们就不再使用断言了,我们这里就把单元测试方法当做 main 方法使用了
运行单元测试,结果如下所示,表示该记录日志方法可用

在这里插入图片描述

2.3 mytomcat【创建服务并绑定端口】

(1) 打开 BootStrap开始编码
在这里插入图片描述
(2) 在 start()中创建服务器端套接字对象,开启一个服务,并将服务绑定到 8080 端口上

A、 这里之所以使用 8080 端口,是因为 WEB 的默认端口号是 80,采用 8080 模拟 80 端口,便于以后的过渡
B、 开始编写 start():

a、 记录服务器启动开启时间:

long begin = System.currentTimeMillis();

b、 记录服务器开始日志

Logger.log(“httpserver start”);

c、 创建 ServerSocket 服务端套接字,并绑定端口后

ServerSocket serverSocket = new ServerSocket(8080);

d、 记录服务器启动结束时间:

long end = System.currentTimeMillis();

e、 记录日志:启动服务消耗时间

Logger.log(“httpserver started,+(end-start)+”ms”);
/**
 * 启动入口
 */
public static void start(){
    long begin=System.currentTimeMillis();
    Logger.log("httpserver start");
    ServerSocket serverSocket=null;
    try {
        //创建服务器套接字,并且绑定端口号:8080
        serverSocket = new ServerSocket(8080);
        long end=System.currentTimeMillis();
        Logger.log("httpserver started,"+(end-begin));
    } catch (Exception e) {
        e.printStackTrace();
    }finally {
        if(serverSocket!=null&&!serverSocket.isClosed()){
            try {
                serverSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

C、 运行 BootStrap 中的 main 方法启动 httpserver
在这里插入图片描述

2.3 mytomcat【端口号可配置】

端口号代表了计算机上的某个服务,端口最好是可配置的,就是说将端口号编写到一个配置文件
当中,通过修改配置文件做到端口号的可配置,那我们这里就选择功能强大的配置文件 xml,端口号需要在服务器启动阶段确定,那这个配置起一个什么样的名字呢?就叫做 server.xml 吧, 在工程根目录下新建 conf 文件夹, 将其放到 conf 目录下,便于以后的管理,一般来说 xml 文件应该有对应的 dtd或 schema 等约束文件,我们这里就不再使用这些约束文件了,直接创server.xml,编写server.xml文件如下所示
(1) 创建 conf 目录,将创建的 server.xml 文件放到 conf 中server.xml 端口号配置文件

<?xml version="1.0" encoding="UTF-8"?>
<server>
    <service>
        <connector port="8080"></connector>
    </service>
</server>

在这里插入图片描述
(2) 使用 dom4j + XPath 解析 server.xml 文件,获取端口号,先引入dom4j 的相关 jar 包
A、 在 core 包下编写 ServerParser 类,定义静态 getPort 方法,读取 server.xml 文件中的端口号
在这里插入图片描述

package com.bruceliu.httpserver.core;

import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

/**
 * @Auther: bruceliu
 * @Date: 2020/2/5 18:20
 * @QQ:1241488705
 * @Description:解析server.xml配置文件
 */
public class ServerParser {

    /**
     * 获取服务器的端口号
     * @return int port
     */
    public static int getPort(){
        //设置服务器默认端口号:8080
        int port = 8080;
        try {
            //创建解析器
            SAXReader saxReader = new SAXReader();
            //通过解析器的read方法将配置文件读取到内存中,生成一个Document[org.dom4j]对象树
            Document document = saxReader.read("conf/server.xml");
            //获取connector节点元素的路径:server -> service -> connector
            //获取connector节点元素的xpath路径:/server/service/connector
            //获取connector节点元素的xpath路径:server//connector
            //获取connector节点元素的xpath路径://connector
            Element connectorElt = (Element) document.selectSingleNode("//connector");
            //获取port属性的值
            port = Integer.parseInt(connectorElt.attributeValue("port"));

        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return port;
    }
}

(3) 在服务器启动程序中使用从配置文件中读取的端口号,并启动服务器,查看日志输出

A、 start()方法中,在绑定端口号之前要先获取端口号

int port = ServerParser.getPort();//获取端口号
Logger.log(“http-port-+port);//记录端口号日志
/**
 * 启动入口
 */
public static void start(){
    try {
        Logger.log("httpserver start");
        //获取当前时间
        long start = System.currentTimeMillis();
        //获取系统端口号
        int port = ServerParser.getPort();
        Logger.log("httpserver-port: " + port);
        //创建服务器套接字,并且绑定端口号:8080
        ServerSocket serverSocket = new ServerSocket(port);
        //获取结束时间
        long end = System.currentTimeMillis();
        Logger.log("httpserver started: " + (end-start) + " ms");
    } catch (Exception e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
}

2.3 mytomcat【接收打印: 浏览器客户端发送请求】

(1) 服务器接收浏览器的请求,当浏览器发送请求后,服务器端会接收到
客户端套接字对象

A、 服务器启动成功,执行 accept()方法,处于等待接收客户端发送请求状态

Socket clientSocket = serverSocket.accept();

(2) 接收到浏览器客户端套接字对象之后,从该套接字对象中获取输入流,从输入流中读取浏览器客户端发送的请求,将 HTTP 的请求协议全部内容读取到之后打印输出到控制台

BufferedReader br =new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
String msgFromClient = null;
while((msgFromClient = br.readLine()) != null){
   System.out.println(msgFromClient);
}

(3) 启动 httpserver 服务器
在这里插入图片描述
(4) 打开 FF 浏览器客户端,在浏览器地址栏上输入下面的 URL 并敲回车发起访问

http://localhost:8080/oa/login?username=zhangsan&password=123

(5) 服务器端接收到客户端套接字对象并获取输入流将请求发送的数据读取到之后,输出到控制台

[INFO] 2020-02-05 18:46:53 623 httpserver start
[INFO] 2020-02-05 18:46:53 762 httpserver-port: 8080
[INFO] 2020-02-05 18:46:53 762 httpserver started: 138 ms
GET /oa/login?username=zhangsan&password=123 HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9

GET /oa/login?username=zhangsan&password=123 HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9

(6) 目前服务器只能接收一次请求,我们可以加入循环语句,让服务器不断的接收客户端的请求

/**
	 * 主程序入口
	 */
	public static void start() {
		ServerSocket serverSocket = null;
		Socket clientSocket = null;
		BufferedReader br = null;
		try {
			Logger.log("httpserver start");
			//获取当前时间
			long start = System.currentTimeMillis();
			//获取系统端口号
			int port = ServerParser.getPort();
			Logger.log("httpserver-port: " + port);
			//创建服务器套接字,并且绑定端口号:8080
			serverSocket = new ServerSocket(port);
			//获取结束时间
			long end = System.currentTimeMillis();
			Logger.log("httpserver started: " + (end-start) + " ms");
			while(true){
				//开始监听网络,此时程序处于等待状态,等待接收客户端的消息
				clientSocket = serverSocket.accept();
				//接收客户端消息
				br = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
				String temp = null;
				while((temp = br.readLine()) != null){
					System.out.println(temp);
				}
			}
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}finally{
			//关闭资源
			if(br != null){
				try {
					br.close();
				} catch (IOException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
			if(clientSocket != null){
				try {
					clientSocket.close();
				} catch (IOException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
			if(serverSocket != null){
				try {
					serverSocket.close();
				} catch (IOException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		}
	}

2.4 mytomcat【多线程处理请求】

(1) 当前服务器存在的问题:

A、 当前服务器程序是单线程,只有一个 main 方法,只有一个主线程在运行
B、 当多个用户同时发送请求的时候,服务器在同一时间点只能处理一个请求
C、 这样会导致用户排队等待的现象

2) 解决方法:

A、 为了让服务器能够同时处理多个请求,可以引入一个线程处理一个请求
B、 多线程并发接收用户的请求,请求 A 和请求 B 之间互不干扰,互不等待,在内存角度来说,会有多个栈内存同时运行

在这里插入图片描述
3) 在 core 包下定义 HandlerRequest 类实现 Runnable 接口,实现 run方法,在 run 方法中处理当前请求

A、 因为定义单独的线程处理请求,处理请求时需要客户端套接字对象(一个请求会对应一个客户端套接字对象),所以线程类需要含有客户端套接字对象的引用

package com.bruceliu.httpserver.core;

import com.bruceliu.httpserver.util.Logger;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;

/**
 * @Auther: bruceliu
 * @Date: 2020/2/5 18:53
 * @QQ:1241488705
 * @Description: 处理客户端请求
 */
public class HandlerRequest implements Runnable {

    private Socket clientSocket = new Socket();
    
    public HandlerRequest(Socket clientSocket){
        this.clientSocket = clientSocket;
    }
    
    public void run(){
        try {
            //获取当前线程名称,记录开启线程日志
            Logger.log(Thread.currentThread().getName()+"handler request");
            Thread.sleep(1000*60);//处理请求
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            if(clientSocket!=null&&!clientSocket.isClosed()){
                try {
                    clientSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

(4) 修改 BootStrap.java 在接收到客户端套接字对象之后

A、 创建线程对象,将客户端套接字对象传递给该线程

new Thread(new HandlerRequest(clientSocket)).start();

B、 将接收和读取客户端消息的代码注释掉,以后要在 HandlerRequest 中处理

while(true){
    //开始监听网络,此时程序处于等待状态,等待接收客户端的消息
    clientSocket = serverSocket.accept();
	/*
	br = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
	String temp = null;
	while((temp = br.readLine()) != null){
		System.out.println(temp);
	}*/
    new Thread(new HandlerRequest(clientSocket)).start();
 }

(5) 启动服务器,测试服务器是否支持多线程并发处理

注:目前只要用户发送一次请求,则创建一个新的线程,这其实是一种不太好的设计方案,大家
有时间可以设计一个线程池,这样可以提高访问效率。

2.5 mytomcat【服务器截获请求 URI】

开始处理客户端的请求,首先我们需要知道客户端访问的是哪一个资源,我们怎么才能知道客户
端访问的哪个资源呢?答案当然是在服务器端获取客户端请求的 URI,那么请求的 URI 在哪里呢?

请求的 URI 在 HTTP 请求协议的请求行上。所以我们需要读取请求协议的第一行数据,然后从这些数据中解析出 URI,从而得出客户要访问的是服务器端的哪个资源。

(1)URL 和 URI 是什么?它们之间是什么关系?

A、 URL(Uniform Resource Locator)是统一资源定位符,例如在浏览器地址栏上直接输入的请求路径 http://127.0.0.1:8080/oa/index.html 就是一个 URL,通过它可以定位网络中的某
个资源。

B、 URI(Uniform Resource Identifier)是统一资源标识符,只是代表网络中某个资源的名称,不具备定位功能,或者说 URI 是 URL 的一部分。例如上面的 URL 中/oa/index.html 就是一个 URI。

(2) HTTP 协议请求协议的详细内容如下图所示,请求行由三部分组成:请求方式+URI+协议版本号,我们需要获取的就是请求行上的 URI
在这里插入图片描述
(3) 在 HandlerRequest.java 中编写程序读取请求行

A、 解析客户请求的字符串,截获 URI
B、 通过 BufferedReader 获取请求消息
@Override
public void run() {
        //处理客户端请求
        BufferedReader br = null;
        Logger.log("httpserver thread: " + Thread.currentThread().getName());
        try {
            //接收客户端消息
            br = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
            //打印客户端消息
			/*String temp = null;
			while((temp = br.readLine()) != null){
				System.out.println(br.readLine());
			}*/
            //获取请求协议的请求行
            String requestLine = br.readLine();// GET /oa/index.html HTTP/1.1
            //获取URI -> 请求行(requestLine) -> 请求方式  URI 请求协议版本号 -> 三者之间是通过一个空格进行连接
            String requestURI = requestLine.split(" ")[1];//{"GET","/oa/index.html","HTTP/1.1"}
            System.out.println(requestURI);


        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } finally {
            //关闭资源
            if (br != null) {
                try {
                    br.close();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
            if (clientSocket != null) {
                try {
                    clientSocket.close();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
    }
}

(4) 启动 httpserver,打开浏览器,输入 URL:

http://127.0.0.1:8080/oa/index.html 进行访问测试
在这里插入图片描述

2.6 mytomcat【响应静态: html 资源到浏览器】

(1) 需求分析:
截获到请求的 URI 之后,判断请求的是否为服务器中某个 webapp 的 html 资源,如果是服务器中
的某个 webapp 的 html 资源,服务器端程序应找到该文件,并且将该文件中的 html 代码读取到 JVM内存中,然后再将内存中的 html 代码响应到浏览器页面上,如果客户端请求的资源在服务器端 webapp中未找到,服务器应提示浏览器客户端“您访问的资源不存在”,这就是著名的 404 错误。 404 被称为HTTP 状态码,属于 HTTP 协议的一部分。

webapp 以及 webapp 中的 html 资源不是 web 服务器开发人员需要完成的, 应该是 webapp 的开发人员负责开发的。 web 服务器只负责读取该 html 资源然后响应到浏览器页面上。 所以此时我们的角色发生了改变,我们现在的角色变为 webapp 的开发人员。在 httpserver 的根目录下新建 web 应用起名 oa(办公系统),在 oa 文件夹下新建 index.html 文件,并编写简单的 html 代码。

(2) webapp 开发角色:
A、 在 httpserver 的根目录下新建 web 应用起名 oa(办公系统) 文件夹
B、 在 oa 文件夹下新建 index.html 文件,并编写简单的 html 代码,如下图所示

index.html 页面代码

<html>
<head>
    <title>OA 办公系统-首页</title>
</head>
<body>
  <center><font size="50px" color="blue">欢迎使用OA办公系统</font></center>
</body>
</html>

在这里插入图片描述
(3) 转变为服务器开发角色:
A、 在 HandlerRequest 类中 run()方法继续处理请求,目前已经截获到请求的 URI
B、 在 HandlerRequest 类中 run()方法中判断, 请求 URI 是否以 html 或 htm 结尾
a、 如果是以 html 或 htm 结尾,服务器定位到该 html 文件,将其读取到 JVM 内存中
b、 然后再将这些 html 代码响应到浏览器页面上,浏览器解释执行 html 代码展示效果

//在 run 方法中, 获取返回客户端消息对象 out
PrintWriter out = new PrintWriter(clientSocket.getOutPutStream());
if(requestURI.endWith(.html”) || requestURI.endWith(.htm”)){
    responseStaticPage(requestURI,out);//响应静态页面请求
}
out.flush();

c、 编写 public static void responseStaticPage(requestRUI,out)方法处理静态页面请求

   /**
	 * 处理静态页面
	 * @param requestURI 请求URI
	 * @param out	响应流对象
	 */
	public void responseStaticPage(String requestURI, PrintWriter out) {
		//requestURI:/oa/index.html
		//静态页面的路径:oa/index.html
		String htmlPath = requestURI.substring(1);
		BufferedReader br = null;
		try {
			//读取页面
			br = new BufferedReader(new FileReader(htmlPath));
			StringBuilder html = new StringBuilder();
			//拼接响应信息
			html.append("HTTP/1.1 200 OK\n");
			html.append("Content-Type:text/html;charset=utf-8\n\n");
			String temp = null;
			while((temp = br.readLine()) != null){
				html.append(temp);
			}
			//输出html
			out.print(html);
		} catch (FileNotFoundException e) {
			//404找不到资源
			StringBuilder html = new StringBuilder();
			html.append("HTTP/1.1 404 NotFound\n");
			html.append("Content-Type:text/html;charset=utf-8\n\n");
			html.append("<html>");
			html.append("<head>");
			html.append("<title>404-错误</title>");
			html.append("<meta content='text/html;charset=utf-8'/>");
			html.append("</head>");
			html.append("<body>");
			html.append("<center><font size='35px' color='red'>404-Not Found</font></center>");
			html.append("</body>");
			html.append("</html>");
			out.print(html);
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

(4) 打开浏览器,输入 URL 访问(访问的资源存在的时候)
在这里插入图片描述
(5) 打开浏览器,输入 URL 访问(访问的资源不存在的时候)
在这里插入图片描述

2.7 mytomcat【响应动态: 客户端访问小 Java 程序】

(1) 思路分析:

在上一个版本中,浏览器客户端发送的请求是 http://127.0.0.1:8080/oa/index.html,从请求路径上
看到浏览器客户端访问的是 WebApp 中的静态 html 页面,服务器找到该静态页面之后直接将 html 页面响应到浏览器即可.

但是如果浏览器发送这样的请http://127.0.0.1:8080/oa/login?username=admin&password=123表示他们要访问的是什么呢?从请求路径的字面意思上理解,这是浏览器向服务器发送了一个登录的请求,需要服务器端执行一段“Java 程序”来处理这次的登录请求, 负责处理登录的 Java 程序应当获取到浏览器提交的用户名和密码,并且负责连接数据库,验证用户名和密码是否正确,如果正确则响应给浏览器一条登录成功的信息,如果错误则响应给浏览器一条登录失败的信息。

处理登录的 Java 程序应该由谁来编写呢?编写完成之后,又应该由谁来管理调用呢?
我们一起来分析一下,将来系统中会存在很多功能,例如:银行账户转账、查询员工信息、保存
员工信息、银行账户开户等,这些功能中的每一个功能都需要执行对应的服务器端 Java 程序来完成的,都是和具体的业务逻辑紧密相关的,显然这种 Java 程序的编写不应该是服务器的开发人员,因为服务器的开发人员不知道具体的业务是什么,所以像以上列举的每一个功能的实现都需要 WebApp 的开发人员来完成开发。 开发完成之后,将处理某个功能的 Java 程序放到 Web 服务器中,由 Web 服务器来负责管理调用。

大家需要注意的是,浏览器向服务器发送请求并且提交数据的格式是什么?这个提交数据的格式
其实属于HTTP协议的一部分,这是W3C提前制定好的 ,格式是 :
http://ip:port/uri?name=value&name=value.....,并且这个数据在 HTTP 协议的请求行上提交,最终会显示在浏览器的地址栏上。

那么,接下来让我们一起实现上面的功能,该功能的实现需要 Web 服务器开发人员、 WebApp 开
发人员共同配合完成。我们现在的角色转变为 WebApp 的开发人员,开始编写 Java 程序处理用户的登录请求。服务器端的小 java 程序英文是:Server Applet,所以我们把服务器端的小 java 程序叫做:Servlet

(2) 角色: webapp 开发人员:

A、 新建软件包 org.bruceliu.oa.servlet
B、 该软件包中新建 LoginServlet.java,编写 LoginServlet 处理用户的登录请求

在这里插入图片描述

/**
 * @Auther: bruceliu
 * @Date: 2020/2/5 21:39
 * @QQ:1241488705
 * @Description:处理登录业务的java程序,该java程序由webApp开发人,由web服务器开人员负责调用
 */
public class LoginServlet {
    //处理业务的核心类
    public void service(){
        System.out.println("正在验证身份,请稍等....");
    }
}

(3) 角色: web 服务器开发人员
接下来我们的开发角色再次转变为 WEB 服务器开发人员,开发服务器端程序调用 LoginServlet 的
service 方法。大家设想一下,将来 WebApp 中不仅有一个 LoginServlet,还会有其它的小 Java 程序,每一个小 Java 程序都对应处理某一个业务,那么对于我们服务器的开发人员来说怎么确定调用哪个Servlet 呢?就像上一个版本中/oa/index.html 是服务器中的一个资源,像 html 这样的资源还有很多,服务器怎么知道浏览器客户端要访问的是哪个资源呢?当然是通过用户的请求路径了。其实LoginServlet 也是服务器端的一种资源,只不过这种资源不是 html,而是一段 Java 程序,所以服务器也可以先截获用户的请求路径,然后通过请求 URI 决定调用哪个 Servlet。 因此可以说,“请求 URI” 和对应要执行的“Servlet 程序”之间有一种绑定关系。 那么接下来就让我们编写 Web 服务器端的程序,截获请求 URI,根据请求 URI 来对应调用 Servlet。(HandlerRequest)

A、 在 HandlerRequest 截获请求 URI,根据请求 URI 来对应调用 Servlet。
B、 紧接上一版本,判断完是否为静态资源,如不是静态页面那肯定是动态资源
//判断用户请求是否为一个静态页面:以.html或.htm结尾的文件叫作html页面
if(requestURI.endsWith(".html") || requestURI.endsWith(".htm")){
	//处理静态页面的方法
	responseStaticPage(requestURI,out);
}else{//动态资源:java程序,业务处理类
	//requestURI: /oa/login?username=zhangsan&password=111
	//requestURI: /oa/login
	
	String servletPath = requestURI;
	//判断servletPath是否包含参数
	if(servletPath.contains("?")){
		servletPath = servletPath.split("[?]")[0];// /oa/login
	}
		
	if("/oa/login".equals(servletPath)){
		LoginServlet loginServlet = new LoginServlet();
		loginServlet.service();
	}
}

a、动态请求分两种情况
requestURI: /oa/login?username=zhangsan&password=123

//判断servletPath是否包含参数
if(servletPath.contains("?")){
	servletPath = servletPath.split("[?]")[0];// /oa/login
}

requestURI: /oa/login

if("/oa/login".equals(servletPath)){
		LoginServlet loginServlet = new LoginServlet();
		loginServlet.service();
}

(4) 启动 httpserver,打开浏览器,输入 URL,发送请求

http://localhost:8080/oa/login?username=zhangsan&password=123

在这里插入图片描述

http://localhost:8080/oa/login

在这里插入图片描述

2.8 mytomcat【Web 服务器代码和 WebApp 代码解耦合】

在这里插入图片描述
分析以上代码, LoginServlet 类是 JavaWeb 程序员开发的, 而 HandlerRequest 类是服务器开发人员开发的,在服务器中的代码关心了具体的 Servlet 类,显然服务器的程序和 JavaWeb 程序产生了依赖,具有高强度的耦合,实际上对于 Web 服务器来说, 根本就不知道 Web 应用中有一个 LoginServlet 类,上面的程序中还使用了“new LoginServlet();”,这显然是错误的。 另外在上面的 Web 服务器程序中编写了具体的请求路径/oa/login,这显然是不合理的, 对于 Web 服务器来说浏览器客户端发送的请求是未知的。 但是我们知道浏览器发送的请求路径/oa/login 是和底层WebApp 中的 LoginServlet 存在映射关系/绑定关系的。 而这种关系的指定必须由 WebApp 的开发人员来指定,我相信大家此时应该想到了“配置文件”,在配置文件中指定请求 URI 和对应要执行的 Servlet。 该配置文件的编写由 WebApp程序员来完成,但是该文件由 Web 服务器开发人员读取并解析,所以该文件的名字、该文件的存放位置、该文件中所编写的配置元素都不能随意编写,必须提前制定好一个规范,那么这个规范由谁来制定呢?当然是由 SUN 公司来负责制定。

接下来我们的角色再次发生了转变,我们现在是规范的制定者 SUN。制定规范如下所示,规范就
是规定,没有为什么,以后服务器开发人员和 WebApp 的开发人员只要严格按照规范开发即可。 下面这段规范是这样制定的:

(1) SUN 制定的配置文件规范:

A、 在配置文件 web.xml 中描述请求 URI 和对应的 Servlet 类之间的关系
B、 web.xml 文件统一存放到 WebAppRoot/WEB-INF/目录下
C、 web.xml 文件中采用这样的标签描述请求 URI 和对应 Servlet 类之间的关系

在这里插入图片描述
以上配置文件规定了什么呢?

 第一:配置文件的名字;
 第二:配置文件存放位置;
 第三:配置文件中编写哪些标签。 

接下来我们的角色转变为 WebApp 的开发人员,按照规范新建 web.xml 文件,并将其存放到规范中要求的路径下,在该文件中编写规范中规定的标签.

D、 在 OA 应用下创建 WEB-INF 文件夹,并且在该文件夹下创建 web.xml
在这里插入图片描述

<?xml version="1.0" encoding="UTF-8"?>
<web-app>

    <servlet>
        <servlet-name>loginServlet</servlet-name>
        <servlet-class>org.bruceliu.oa.servlet.LoginServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>loginServlet</servlet-name>
        <url-pattern>/login</url-pattern>
    </servlet-mapping>

    <servlet>
        <servlet-name>userSaveServlet</servlet-name>
        <servlet-class>org.bruceliu.oa.servlet.UserSaveServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>userSaveServlet</servlet-name>
        <url-pattern>/user/save</url-pattern>
    </servlet-mapping>

</web-app>

(2) 角色: web 服务器开发人员

A、 服务器开发者负责解析 web.xml 文件
a、 通过 Dom4j+xpath 解析 web.xml
在这里插入图片描述

B、 在 Web 服务器启动阶段解析 web.xml 文件

接下来我们的角色再次转变为 Web 服务器的开发人员,服务器开发者负责解析 web.xml 文件,那么这个文件在什么时候解析呢?解析出来的数据存放到哪里呢? 这些问题由服务器开发者决定,SUN 的规范中并没有特殊的要求。 web.xml 文件中主要配置了请求 URI 和对应的 Servlet 完整类名,请求URI 更像一个 Map 集合的 key,对应的 Servlet 完整类名更像一个 Map 集合的 value,所以我们决定采用一个 Map 集合存储解析出来的数据。在浏览器发送请求的时候再去解析 web.xml 文件时有点晚了,所以我们决定在 Web 服务器启动阶段解析 web.xml 文件,以下是 Web 服务器开发人员编写的解析web.xml 文件的代码
b、 解析 web.xml 类 WebParser 类如下:

package com.bruceliu.httpserver.core;

import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import java.io.File;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * @Auther: bruceliu
 * @Date: 2020/2/5 22:14
 * @QQ:1241488705
 * @Description:解析服务器中的web.xml配置文件
 */
public class WebParser {

    public static Map<String,Map<String,String>> servletMaps = new HashMap<String,Map<String,String>>();

    /**
     * 解析服务器所有应用的web.xml
     * @param webAppNames 服务器中所有应用的名称
     * @throws DocumentException
     */
    public static void parser(String[] webAppNames) throws DocumentException {
        for(String webAppName:webAppNames){
            Map<String,String> servletMap = parser(webAppName);
            servletMaps.put(webAppName, servletMap);
        }
    }

    /**
     * 解析单个应用的web.xml配置文件
     * @param webAppName 应用名称
     * @return servletMap<String,String>
     * @throws DocumentException
     */
    private static Map<String,String> parser(String webAppName) throws DocumentException{
        //获取web.xml的路径
        String webPath = webAppName + "/WEB-INF/web.xml";
        //创建解析器
        SAXReader saxReader = new SAXReader();
        //通过解析器的read方法将配置文件读取到内存中,生成一个Document【org.dom4j】对象树
        Document document = saxReader.read(new File(webPath));

        //获取servlet节点元素: web-app -> servlet
        List<Element> servletNodes = document.selectNodes("/web-app/servlet");
        //创建一个servletInfoMap集合,将servlet-name和servlet-class的值分别当作key和value存放到该集合中
        Map<String,String> servletInfoMap = new HashMap<String,String>();
        //开始遍历servletNodes
        for(Element servletNode:servletNodes){
            //获取servlet-name节点元素对象
            Element servletNameElt = (Element) servletNode.selectSingleNode("servlet-name");
            //获取servletNameElt节点元素对象的值
            String servletName = servletNameElt.getStringValue();

            //获取servlet-class节点元素对象
            Element servletClassElt = (Element) servletNode.selectSingleNode("servlet-class");
            //获取servletClassElt节点元素对象的值
            String servletClassName = servletClassElt.getStringValue();

            //将servletName和servletClassName分别当作key和value存放到servletInfoMap集合中
            servletInfoMap.put(servletName, servletClassName);
        }

        //获取servlet-mapping节点元素对象: web-app -> servlet-mapping
        List<Element> servletMappingNodes = document.selectNodes("/web-app/servlet-mapping");
        //创建一个servletMappingInfoMap集合,将servlet-name和url-pattern节点元素对象的值分别当作key和value存放到该map集合中
        Map<String,String> servletMappingInfoMap = new HashMap<String,String>();
        //开始遍历servletMappingNodes
        for(Element servletMappingNode:servletMappingNodes){
            //获取servlet-name节点元素对象
            Element servletNameElt = (Element) servletMappingNode.selectSingleNode("servlet-name");
            //获取servletNameElt节点元素对象的值
            String servletName = servletNameElt.getStringValue();

            //获取url-pattern节点元素对象
            Element urlPatternElt = (Element) servletMappingNode.selectSingleNode("url-pattern");
            //获取urlPatternElt节点元素对象的值
            String urlPattern = urlPatternElt.getStringValue();

            //将servletName和urlPattern分别当作key和value存放到servletMappingInfoMap集合中
            servletMappingInfoMap.put(servletName, urlPattern);
        }

        //获取servletInfoMap或者servletMappingInfoMap任何一个key值的集合
        Set<String> servletNames = servletInfoMap.keySet();
        //创建一个servletMap集合,将servletMappingInfoMap的value和servletInfoMap的value分别当作key和value存放到该map集合中
        Map<String,String> servletMap = new HashMap<String,String>();
        //开始遍历servletNames
        for(String servletName:servletNames){
            //获取servletMappingInfoMap集合中的value:urlPattern
            String urlPattern = servletMappingInfoMap.get(servletName);
            //获取servletInfoMap集合中的value:servletClass
            String servletClassName = servletInfoMap.get(servletName);

            //将urlPattern和servletClassName分别当作key和value存放到servletMap集合中
            servletMap.put(urlPattern, servletClassName);
        }
        return servletMap;
    }
}


(3) 在服务器启动阶段解析每个 WebApp的 web.xml文件,在 BootStrap中加入如下代码
A、 修改 BootStrap 类中 start()方法
a、 在服务端套接字绑定端口号之后, 添加如下代码, 开始读取 web.xml 文件
b、 WebServer.parser(new String[]{“oa”});
在这里插入图片描述
以上代码在服务器启动阶段解析每个 WebApp 的 web.xml 文件,其中 WebApp 的名字 oa 写死到
服务器程序中了,其实对于 Web 服务器来说根本就不知道该 Web 服务器下有哪些 WebApp。这个问题怎么解决呢?我们可以规定一个目录,例如该目录的名字叫做 webapps,让所有的 WebApp 都放到我们所指定的 webapps 目录下,这样我们就可以在 Web 服务器中动态获取所有 WebApp 的名字了,这个功能大家自己完成, 这里不再实现这个功能了。

现在我们的角色还是 Web 服务器开发人员, web.xml 文件已经在服务器启动阶段解析了,用户的
请求路径已经和对应的 Servlet 完整类名绑定在一起了,分析以下程序应该如何修改
在这里插入图片描述

(4) 我们在服务器端程序中可以获取到请求 URI,我们通过请求 URI 获取对应的 Servlet 完整类名
(5) 再通过反射机制,调用该 Servlet 类的无参数构造方法,完成 Servlet对象的创建,代码如下所示

//判断用户请求是否为一个静态页面:以.html或.htm结尾的文件叫作html页面
if(requestURI.endsWith(".html") || requestURI.endsWith(".htm")){
    //处理静态页面的方法
    responseStaticPage(requestURI,out);
}else{
//动态资源:java程序,业务处理类
//requestURI: /oa/login?username=zhangsan&password=111
//requestURI: /oa/login
String servletPath = requestURI;
//判断servletPath是否包含参数
if(servletPath.contains("?")){
 		servletPath = servletPath.split("[?]")[0];// /oa/login
}

/*if("/oa/login".equals(servletPath)){
		LoginServlet loginServlet = new LoginServlet();
		loginServlet.service();
}*/

//获取应用的名称:oa 在 uri里:/oa/login
String webAppName = servletPath.split("[/]")[1];
//获取servletMaps集合中的value值->servletMap -> key:urlPattern value:servletClassName
Map<String,String> servletMap = WebParser.servletMaps.get(webAppName);
//获取servletMap集合中的key值 -> 存在于uri中/oa/login /oa/use/xxx/xxx/xxx/xxx
String urlPattern = servletPath.substring(1 + webAppName.length());
//获取servletClassName
String servletClassName = servletMap.get(urlPattern);
//判断该业务处理的Servlet类是否存在
if(servletClassName != null){
	 //通过反射机制创建该业务处理类
	 Class c = Class.forName(servletClassName);
	 Object obj = c.newInstance();
	 //这个时候,服务开发人员不知道如何调用servlet业务处理类里的方法了?

}

(6) Web 服务器的开发人员又遇到了新的问题
A、 虽然我们在服务器端根据请求 URI 找到了对应的 Servlet 完整类名,也通过反射机制将 Servlet 对象创建成功了
B、 但是对于 Web 服务器的开发人员来说并不知道该 Servlet 对象中有什么方法
C、 这个时候只有 SUN 再次出面制定规范了

a、 接下来我们的角色发生了改变,我们现在是 SUN 公司,制定一个 Servlet 接口
b、 该接口的实现者是 WebApp, 调用者 Web 服务器,以下是 SUN 公司制定的 Servlet规范的核心接口
c、 新建 javax.servlet 包,创建一个 Servlet 接口
d、 该接口中包含:处理请求的核心方法 void service();
public interface Servlet{
     void service();//处理请求的核心方法
}

在这里插入图片描述
(7) 接下来我们的角色转变为 WebApp 的开发人员,让 LoginServlet 实现 Servlet 接口
在这里插入图片描述

/**
 * @Auther: bruceliu
 * @Date: 2020/2/5 21:39
 * @QQ:1241488705
 * @Description:处理登录业务的java程序,该java程序由webApp开发人,由web服务器开人员负责调用
 */
public class LoginServlet implements Servlet {
    //处理业务的核心类
    public void service(){
        System.out.println("正在验证身份,请稍等....");
    }
}

(8) 接下来我们的角色转变为 Web 服务器的开发人员
A、 在 HandlerRequest 类中面向 Servlet 接口调用 service 方法即可
B、 修改 HandlerRequest 方法,紧接着(5)继续编写代码

Servlet servlet = (Servlet)obj;
servlet.service();

在这里插入图片描述
(9) 启动 httpserver,打开浏览器, 输入 URL, 发送请求

http://127.0.0.1:8080/oa/login?user=zhangsan&password=123

在这里插入图片描述

2.9 mytomcat【访问的小 Java 不存在】

(1) 在以上程序的基础之上我们需要考虑请求的 URI 是否不存在,如果不存在需要提示客户端 404 错误,代码如下


/*if("/oa/login".equals(servletPath)){
	LoginServlet loginServlet = new LoginServlet();
	loginServlet.service();
}*/

//获取应用的名称:oa 在 uri里:/oa/login
String webAppName = servletPath.split("[/]")[1];
//获取servletMaps集合中的value值->servletMap -> key:urlPattern value:servletClassName
Map<String,String> servletMap = WebParser.servletMaps.get(webAppName);
//获取servletMap集合中的key值 -> 存在于uri中/oa/login /oa/use/xxx/xxx/xxx/xxx
String urlPattern = servletPath.substring(1 + webAppName.length());
//获取servletClassName
String servletClassName = servletMap.get(urlPattern);
//判断该业务处理的Servlet类是否存在 
if(servletClassName != null){
    //通过反射机制创建该业务处理类
    Class c = Class.forName(servletClassName);
    Object obj = c.newInstance();
    //这个时候,服务开发人员不知道如何调用servlet业务处理类里的方法了?
    Servlet servlet = (Servlet)obj;
    servlet.service();
}else{//找不到该业务处理类:404
    //404找不到资源
    StringBuilder html = new StringBuilder();
    html.append("HTTP/1.1 404 NotFound\n");
    html.append("Content-Type:text/html;charset=utf-8\n\n");
    html.append("<html>");
    html.append("<head>");
    html.append("<title>404-错误</title>");
    html.append("<meta content='text/html;charset=utf-8'/>");
    html.append("</head>");
    html.append("<body>");
    html.append("<center><font size='35px' color='red'>404-Not Found</font></center>");
    html.append("</body>");
    html.append("</html>");
    out.print(html);
}

(2) 启动 httpserver,打开浏览器,发送请求访问

http://127.0.0.1:8080/oa/test

在这里插入图片描述

2.10 mytomcat【小 Java 程序输出 html 到浏览器】

修改 Servlet 接口方法 service(PrintWriter out)
(1) 输出 html 的内容是由 WebApp 开发者决定的
(2) 所以对于 Web 服务器来说只需要将负责响应的标准输出流 out 传递给 Servlet 即可
(3) 将 HandlerRequest 类中的 servlet.service();代码修改为:

servlet.service(out);

(4) 在判断完该请求存在后, 添加将响应头的固定输出标准
在这里插入图片描述

out.print(“HTTP/1.1 200 OK\n”);//状态行
out.print(“Content-Type:html/text;charset=utf-8\n\n”);//响应头

(5) 以上程序编译失败, SUN 提供的 Servlet 接口需要改进,改进 Servlet接口中的 service 方法

/**
 * @Auther: bruceliu
 * @Date: 2020/2/6 10:29
 * @QQ:1241488705
 * @Description:
 */
public interface Servlet{

    void service(PrintWriter out);//处理请求的核心方法
}

(6) 此时 LoginServlet(WebApp 开发人员开发的)编译错误,因为LoginServlet 实现了 Servlet 接口,所以对 LoginServlet 进行修改,并响应消息到浏览器
A、 修改 LoginServlet 实现类中 service 方法

public class LoginServlet implements Servlet{
    //处理业务的核心类
    public void service(PrintWriter out){
        System.out.println("正在验证身份,请稍等....");
        out.print("<html>");
        out.print("<head>");
        out.print("<title>正在验证</title>");
        out.print("<meta content='text/html;charset=utf-8'/>");
        out.print("</head>");
        out.print("<body>");
        out.print("<center><font size='35px' color='blue'>正在验证身份,请稍等....</font></center>");
        out.print("</body>");
        out.print("</html>");
    }
}

(7) 启动 服务器,打开浏览器,发送请求,运行结果
在这里插入图片描述
在这里插入图片描述

2.11 mytomcat【引入 ServletResponse 接口】

(1) 目前 Servlet 接口的 service 方法参数类型是 PrintWriter,如图所示
在这里插入图片描述
(2) 问题分析:
A、 PrintWriter 参数属于负责响应的参数,将来可能还会有其它的参数也负责响应的
B、 参数过多的时候,会让 service 方法变的特别的臃肿,
C、 我们在这里做一个小的设计,将所有负责响应的参数统一都封装到负责响应的对象中。
D、 那么这个负责响应的对象应该由谁来实现呢?当然是 Web 服务器的开发人员,角色转变为 Web 服务器开发人员,代码如下

package com.bruceliu.httpserver.core;

import java.io.PrintWriter;

/**
 * @Auther: bruceliu
 * @Date: 2020/2/6 11:16
 * @QQ:1241488705
 * @Description:负责响应的对象
 */
public class ResponseObject {
    private PrintWriter out;

    public void setWriter(PrintWriter out) {
        this.out = out;
    }

    public PrintWriter getWriter() {
        return out;
    }
}

在这里插入图片描述
(3) 以后所有负责响应的参数都会被封装到 ResponseObject 中(但是需要注意的是 ResponseObject 是 Web 服务器开发人员编写的)

A、 修改 HandlerRequest 类 servlet.service 方法参数

ResponseObject responseObj = new ResponseObject();
responseObj.setWriter(out);
servlet.service(responseObj);

在这里插入图片描述
(4) 以上程序编译错误,显然是需要修改 SUN 的 Servlet 规范的,那么我们的角色转变为 SUN 公司,修改 Servlet 接口,代码如下
修改 Servlet.java 接口

/**
 * @Auther: bruceliu
 * @Date: 2020/2/6 10:29
 * @QQ:1241488705
 * @Description:
 */
public interface Servlet{

    void service(ResponseObject responseOb);//处理请求的核心方法
}

(5) 既然负责响应的 PrintWriter 参数已经被封装到 ResponseObject 类型中了
(6) 那么 Servlet 接口 service 方法的参数 PrintWriter 就需要修改为ResponseObject
A、 修改 Servlet 接口中 service 方法参数:ResponseObject responseObj

void service(ResponseObject responseObject);

(7) 观察以上代码:

A、 Servlet 是 SUN 公司编写的,但是在规范中却关联依赖了我们自己的编写的ResponseObject, 显然是不合理的,稍后我们再做调整。
B、 现在该去修改 WebApp 开发人员编写的 LoginServlet 了,因为 LoginServlet 实现了
Servlet 接口
C、 修改 LoginServlet.java 实现类
/**
 * @Auther: bruceliu
 * @Date: 2020/2/5 21:39
 * @QQ:1241488705
 * @Description:处理登录业务的java程序,该java程序由webApp开发人,由web服务器开人员负责调用
 */
public class LoginServlet implements Servlet{
    //处理业务的核心类
    public void service(ResponseObject responseObj){
        PrintWriter out = responseObj.getWriter();
        System.out.println("正在验证身份,请稍等....");
        out.print("<html>");
        out.print("<head>");
        out.print("<title>正在验证</title>");
        out.print("<meta content='text/html;charset=utf-8'/>");
        out.print("</head>");
        out.print("<body>");
        out.print("<center><font size='35px' color='blue'>正在验证身份,请稍等....</font></center>");
        out.print("</body>");
        out.print("</html>");
    }
}

(8) 启动 httpserver,打开浏览器,发送请求
在这里插入图片描述
在这里插入图片描述
(9) 我们应该解决下图中的问题了
在这里插入图片描述
(10) 怎么解决呢? Servlet 接口是 SUN 公司制定的规范,而ResponseObject 类型是具体的服务器中定义的类型

A、 为了解耦合, SUN 公司又制定了另一个接口 ServletResponse
B、 ResponseObject 类实现该接口,该接口的调用者是 WebApp 的开发人员,代码如下
public interface ServletResponse{
     PrintWriter getWriter();
}

(11) Servlet 核心接口 service 方法的参数类型 ResponseObject 修改为接口 ServletResponse

/**
 * @Auther: bruceliu
 * @Date: 2020/2/6 10:29
 * @QQ:1241488705
 * @Description:
 */
public interface Servlet {
    /**
     * 处理业务的核心方法
     */
    void service(ServletResponse response);
}

(12) ResponseObject 类型是 Web 服务器开发人员开发的,该类型实现 ServletResponse 接口

/**
 * @Auther: bruceliu
 * @Date: 2020/2/6 11:16
 * @QQ:1241488705
 * @Description:负责响应的对象
 */
public class ResponseObject implements ServletResponse {
    private PrintWriter out;
    public void setWriter(PrintWriter out){
        this.out = out;
    }

    public PrintWriter getWriter(){
        return out;
    }
}

(13) 对于 WebApp 的开发人员来说, Servlet 的 service 方法参数类型也需要修改,代码如下

/**
 * @Auther: bruceliu
 * @Date: 2020/2/5 21:39
 * @QQ:1241488705
 * @Description:处理登录业务的java程序,该java程序由webApp开发人,由web服务器开人员负责调用
 */
public class LoginServlet implements Servlet{
    //处理业务的核心类
    public void service(ServletResponse response){
        System.out.println("正在验证身份,请稍等....");
        //获取响应流对象
        PrintWriter out = response.getWriter();
        out.print("<html>");
        out.print("<head>");
        out.print("<title>正在验证</title>");
        out.print("<meta content='text/html;charset=utf-8'/>");
        out.print("</head>");
        out.print("<body>");
        out.print("<center><font size='35px' color='blue'>正在验证身份,请稍等....</font></center>");
        out.print("</body>");
        out.print("</html>");
    }
}

(14) 启动 httpserver,打开浏览器发送请求访问,执行结果如下
在这里插入图片描述
在这里插入图片描述

2.11 mytomcat【接收表单提交的数据】

(1) 需求分析:
到目前为止,服务器端已经可以完成响应了。接下来我们解决什么问题呢?大家都知道当用户登
录的时候会提交登录的用户名和密码,那么在服务器端的小 Java 程序中怎么去获取这些信息呢?显然需要 Web 服务器帮助我们将数据解析出来,放到某个对象中,然后 WebApp 的开发人员从该对象中直接获取数据使用。那么接下来我们的角色转变为 Web 服务器的开发人员,定义一个类专门完成数据的解析封装。

(2) 提交的数据在哪里呢?看下图
在这里插入图片描述

(3) 解决方案:
其实我们有必要将请求协议的全部内容封装到某个对象当中,以便 WebApp 开发人员的使用, 不过将请求协议全部内容封装到某个对象中,这将是一个非常繁琐的过程,我们这里就不再实现这个功能了,我们只将请求行上的 URI 部分的数据封装就行了。 由于该对象中封装了请求协议,所以
我们把这个对象叫做请求对象 request。

请求行上的 URI 部分提交的数据格式为 uri?name=value&name=value…,其中 name 是 form 表单控件的 name 属性, value 是 form 表单控件的 value 属性,这个格式是 W3C 制定的 HTTP 协议的一部分。 我们可以让表单提交的数据更复杂一些,来看一看提交了哪些数据?

(4) 编写如下的 userSave.html

<html>
<head>
    <title>用户信息-保存用户信息</title>
    <meta content="text/html;charset=utf-8"/>
</head>
<body>
<form name="userForm" action="/oa/user/save" method="get">
    用户名:
    <input type="text" name="username" />
    <br><br>&nbsp;&nbsp;&nbsp;别:
    <input type="radio" name="gender" value="1"/><input type="radio" name="gender" value="0"/><br><br>&nbsp;&nbsp;&nbsp;趣:
    <input type="checkbox" name="interest" value="music"/>音乐
    <input type="checkbox" name="interest" value="sport"/>运动
    <input type="checkbox" name="interest" value="food"/>美食
    <input type="checkbox" name="interest" value="sleep"/>睡觉
    <input type="checkbox" name="interest" value="travel"/>旅游
    <br><br>
    <input type="submit" value="保存"/>
    <input type="reset" value="重置"/>
</form>
</body>
</html>

在这里插入图片描述
(5) 启动服务器,打开浏览器,访问 userSave.html, 如下图所示
在这里插入图片描述
(6) 填写表单,点击保存按钮提交数据,查看浏览器地址栏的地址,如下图所示
在这里插入图片描述
在这里插入图片描述

A、 以上出现 404 错误是很正常的,因为底层/oa/user/save 请求 URI 对应要执行的 Servlet类并没有编写
B、 也没有在 web.xml 文件中进行相关配置,所以访问的资源不存在。 后面我们再解决这个 404 的错误。
C、 我们看到浏览器地址栏上提交了这样的数据

http://127.0.0.1:8080/oa/user/save?username=zhangsan&sex=m&interest=sport&interest=music&interest=trave

显然可以将这些提交的数据放到 Map 集合当中,如下图所示
在这里插入图片描述
(7) 接下来我们的角色转变为 Web 服务器的开发人员,
A、 开始进行数据的解析与封装,达到上图的效果
B、 我们单独定义一个负责请求协议封装的类,起名为 RequestObject
C、 在该类中定义 parameterMap 属性表示以上的 Map<String,String[]>
D、 代码如下图所示
E、 思路分析:

package com.bruceliu.httpserver.core;

import java.util.HashMap;
import java.util.Map;

/**
 * @Auther: bruceliu
 * @Date: 2020/2/6 11:50
 * @QQ:1241488705
 * @Description:负责封装请求参数
 */
public class RequestObject {

    public Map<String, String[]> parameterMap = new HashMap<String, String[]>();

    public RequestObject(String requestURI) {
        //requestURI有以下几种可能:
        // 1、/oa/user?

        // 2、/oa/user?username=zhangsan
        // 3、/oa/user?username=

        // 4、/oa/user?username=zhangsan&gender=1
        // 5、/oa/user?username=zhangsan&gender=1&interest=food
        // 6、/oa/user?username=zhangsan&gender=1&interest=food&interest=sleep

        //判断requestURI中是否包含?号:是否包含参数
        if (requestURI.contains("?")) {
            //将requestURI通过?号进行分隔
            String[] uriAndData = requestURI.split("[?]");
            //再次判断requestURI是否包含参数
            if (uriAndData.length > 1) {
                //获取参数部分
                String data = uriAndData[1];
                //通过&符号判断requestURI中是否包含多个参数
                if (data.contains("&")) {//说明有多个参数
                    // {"username=zhangsan","gender=1","interest=food","interest=sleep"}
                    //通过&符号进行分隔
                    String[] nameAndValues = data.split("&");
                    //开始遍历nameAndValueAttr
                    for (String nameAndValue : nameAndValues) {
                        //通过=号进行分隔:{"username","zhangsan"}
                        String[] nameAndValueAttr = nameAndValue.split("=");
                        //判断key值是否在parameterMap集合中存在:
                        // 1.如果存在:说明该参数为多选框
                        // 2.如果不存在:说明为普通的标签
                        if (parameterMap.containsKey(nameAndValueAttr[0])) {
                            //将之前多选框的值取出来
                            String[] values = parameterMap.get(nameAndValueAttr[0]);
                            //定义一个新的数组,新数组的长度永远比values数组的长度大1
                            String[] newValues = new String[values.length + 1];
                            System.arraycopy(values, 0, newValues, 0, values.length);
                            //判断该参数是否有值
                            if (nameAndValueAttr.length > 1) {
                                newValues[newValues.length - 1] = nameAndValueAttr[1];
                            } else {
                                newValues[newValues.length - 1] = "";
                            }
                            parameterMap.put(nameAndValueAttr[0], newValues);
                        } else {
                            //判断该参数是否有值
                            if (nameAndValueAttr.length > 1) {
                                parameterMap.put(nameAndValueAttr[0], new String[]{nameAndValueAttr[1]});
                            } else {
                                parameterMap.put(nameAndValueAttr[0], new String[]{""});
                            }
                        }

                    }

                } else {//单个参数
                    String[] nameAndValueAttr = data.split("=");
                    //data通过=号进行分隔,判断参数是否有值
                    if (nameAndValueAttr.length > 1) {//有值
                        parameterMap.put(nameAndValueAttr[0], new String[]{nameAndValueAttr[1]});
                    } else {//无值
                        parameterMap.put(nameAndValueAttr[0], new String[]{""});
                    }
                }
            }
        }
    }

    /**
     * 获取普通标签参数的值
     *
     * @param key 标签name属性的值
     * @return String 标签value值
     */
    public String getParameterValue(String key) {
        String[] value = parameterMap.get(key);
        return (value != null && value.length != 0) ? value[0] : null;
    }

    /**
     * 获取多选框的值
     *
     * @param key 标签name属性的值
     * @return String[] 多选框的值
     */
    public String[] getParameterValues(String key) {
        return parameterMap.get(key);
    }
}

(8) 对以上编写的 RequestObject 中的 getParameter 方法和getParameterValues 方法进行单元测试,建立单元测试类,编写测试程序,测试结果如下图所示

package com.bruceliu.httpserver.core;

import org.junit.Test;

/**
 * @Auther: bruceliu
 * @Date: 2020/2/6 11:53
 * @QQ:1241488705
 * @Description:
 */
public class RequestObjectTest {

    @Test
    public void testGetParameterValue() {
        RequestObject request = new RequestObject("/oa/user/save?");
        System.out.println(request.getParameterValue("username"));
        RequestObject request1 = new RequestObject("/oa/user/save?username=");
        System.out.println(request1.getParameterValue("username"));
        RequestObject request2 = new RequestObject("/oa/user/save?username=zhangsan");
        System.out.println(request2.getParameterValue("username"));
        RequestObject request3 = new RequestObject("/oa/user/save?username=zhangsan&gender=1");
        System.out.println(request3.getParameterValue("gender"));

    }

    @Test
    public void testGetParameterValues() {
        RequestObject request4 = new RequestObject("/oa/user/save?username=zhangsan&gender=1&interest=food&interest=music");
        String[] interests = request4.getParameterValues("interest");
        for(String interest:interests){
            System.out.println(interest);
        }
    }
}

(9) 测试通过之后,开始使用 RequestObject,继续保持我们的角色 Web服务器开发人员,在 HandlerRequest 中当截获到请求的 URI 之后,将请求的 URI 封装到 RequestObject 对象中,为了让 WebApp 的开发人员能够拿到表单的数据,将 RequestObject 传递给 Servlet 的 service 方法,代码如下所示
在这里插入图片描述
(10) 角色转变为 SUN 公司,修改 Servlet 接口中的 service 方法,代码如下

/**
 * @Auther: bruceliu
 * @Date: 2020/2/6 10:29
 * @QQ:1241488705
 * @Description:
 */
public interface Servlet {
    /**
     * 处理业务的核心方法
     */
    void service(RequestObject requestObject, ServletResponse response);
}

(11) 角色转变为 WebApp 开发人员,修改 LoginServlet,解决编译错误,代码如下:

/**
 * @Auther: bruceliu
 * @Date: 2020/2/5 21:39
 * @QQ:1241488705
 * @Description:处理登录业务的java程序,该java程序由webApp开发人,由web服务器开人员负责调用
 */
public class LoginServlet implements Servlet{
    //处理业务的核心类
    public void service(RequestObject requestObject, ServletResponse response){
        System.out.println("正在验证身份,请稍等....");
        //获取响应流对象
        PrintWriter out = response.getWriter();
        out.print("<html>");
        out.print("<head>");
        out.print("<title>正在验证</title>");
        out.print("<meta content='text/html;charset=utf-8'/>");
        out.print("</head>");
        out.print("<body>");
        out.print("<center><font size='35px' color='blue'>正在验证身份,请稍等....</font></center>");
        out.print("</body>");
        out.print("</html>");
    }
}

(12) 那么此时的问题又来了, Servlet 是 SUN 公司的规范,但是为什么规范中出现了 RequestObject 类型, RequestObject 类型是自己开发的 WEB 服务器程序, 看下图:
在这里插入图片描述
(13) 怎么解决这个耦合呢?当然,和我们以前的解决思路是一样的,Servlet 规范中 service 方法的第一个参数应该是一个接口,我们现在以 SUN公司的角色制定接口,如下图所示:

package javax.servlet;

/**
 * @Auther: bruceliu
 * @Date: 2020/2/6 12:03
 * @QQ:1241488705
 * @Description:负责封装请求参数对象
 */
public interface ServletRequest {

    /**
     * 获取单个参数的值
     * @param key
     * @return String
     */
    String getParameterValue(String key);


    /**
     * 获取多选框的值
     * @param key
     * @return String[]
     */
    String[] getParameterValues(String key);
}

在这里插入图片描述
(14) 角色转变为 Web 服务器的开发人员, RequestObject 类实现以上接口,如下图所示:

/**
 * @Auther: bruceliu
 * @Date: 2020/2/6 11:50
 * @QQ:1241488705
 * @Description:负责封装请求参数
 */
public class RequestObject implements ServletRequest {

    public Map<String, String[]> parameterMap = new HashMap<String, String[]>();

(15) 角色转变为 SUN 公司,修改核心接口 Servlet 中的 service 方法,使之解耦合, 如下图所示:

/**
 * @Auther: bruceliu
 * @Date: 2020/2/6 10:29
 * @QQ:1241488705
 * @Description:
 */
public interface Servlet {
    
    /**
     * 处理业务的核心方法
     * @param request
     * @param response
     */
    void service(ServletRequest request, ServletResponse response);
}

(16) SUN 将接口改动之后,该接口的实现类 LoginServlet 编译错误,角色转变为 WebApp 的开发人员, 解决编译错误,如下图所示:

package org.bruceliu.oa.servlet;

import com.bruceliu.httpserver.core.RequestObject;
import com.bruceliu.httpserver.core.ResponseObject;

import javax.servlet.Servlet;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.PrintWriter;

/**
 * @Auther: bruceliu
 * @Date: 2020/2/5 21:39
 * @QQ:1241488705
 * @Description:处理登录业务的java程序,该java程序由webApp开发人,由web服务器开人员负责调用
 */
public class LoginServlet implements Servlet{
    //处理业务的核心类
    public void service(ServletRequest request, ServletResponse response){
        System.out.println("正在验证身份,请稍等....");
        //获取响应流对象
        PrintWriter out = response.getWriter();
        out.print("<html>");
        out.print("<head>");
        out.print("<title>正在验证</title>");
        out.print("<meta content='text/html;charset=utf-8'/>");
        out.print("</head>");
        out.print("<body>");
        out.print("<center><font size='35px' color='blue'>正在验证身份,请稍等....</font></center>");
        out.print("</body>");
        out.print("</html>");
    }
}

(17) 那么接下来我们的角色转变为 WebApp 的开发人员,应该去解决如下图所示的 404 错误问题了:
在这里插入图片描述
(18) 上图中显示的 404 错误表示访问的资源不存在,请求的 URI 是/oa/user/save,那么接下来整个的开发过程就交给 WebApp的开发人员了,编写 web.xml 文件,然后编写对应的 Servlet 类,实现 service 方法,完成信息的保存,我们先来做第一步,编写 web.xml,如下图所示:

    <servlet>
        <servlet-name>userSaveServlet</servlet-name>
        <servlet-class>org.bruceliu.oa.servlet.UserSaveServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>userSaveServlet</servlet-name>
        <url-pattern>/user/save</url-pattern>
    </servlet-mapping>

(19) 第二步,编写 org.bruceliu.oa.servlet.UserSaveServlet,重写 service 方法,获取用户提交的信息,将用户提交的信息保存到数据库当中,保存成功或者失败,将最终的结果响应到浏览器,如下图所示:

package org.bruceliu.oa.servlet;

import javax.servlet.Servlet;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.PrintWriter;

/**
 * @Auther: bruceliu
 * @Date: 2020/2/6 12:10
 * @QQ:1241488705
 * @Description:
 */
public class UserSaveServlet implements Servlet {

    @Override
    public void service(ServletRequest request, ServletResponse response) {
        //-----------获取页面请求提交的参数的值-------
        String username = request.getParameterValue("username");
        String gender = request.getParameterValue("gender");
        String[] interest = request.getParameterValues("interest");
        StringBuilder interests = new StringBuilder();
        for(String interestValue:interest){
            interests.append(interestValue).append(" ");
        }

        //获取响应流对象
        PrintWriter out = response.getWriter();
        out.print("<html>");
        out.print("<head>");
        out.print("<title>用户信息</title>");
        out.print("<meta content='text/html;charset=utf-8'/>");
        out.print("</head>");
        out.print("<body>");
        out.print("用户名:" + username + "<br>");
        out.print("性别:" + gender + "<br>");
        out.print("兴趣:" + interests);
        out.print("</body>");
        out.print("</html>");
    }

}

(20) 启动 httpserver,打开浏览器,访问 userSave.html,在表单上填写数据然后提交,效果如下图所示:
在这里插入图片描述
在这里插入图片描述

2.12 mytomcat【Servlet 对象实现单实例】

(1) 需求分析:
在这里插入图片描述
从上图可以看出,每一次浏览器客户端发送请求的时候, WEB 服务器都会通过反射机制创建一个
新的 Servlet 对象, 当多线程同时并发访问的时候,在 WEB 服务器的 JVM 中会有大量的 Servlet 对象,使 JVM 的堆内存压力增大,耗费占用大量的堆内存空间。 并且每一次都创建 Servlet 对象,显然效率是比较低的, 怎么做既可以节省堆内存的开销又可以提高程序的执行效率呢?当然,此时你肯定想到了单实例的方式,也就是说当浏览器客户端第一次发送请求的时候创建 Servlet 对象,将 Servlet 对象和对应的请求 URI 存放到缓存当中,当浏览器客户端第 2+次发送请求的时候直接从缓存当中获取请求URI 对应的 Servlet 实例,直接调用 Servlet 对象的 service 方法。这样做我们就可以达到既节省了内存的开销,又提高了程序的执行速度。接下来我们做一个简单的设计,如下图所示:
在这里插入图片描述
(2) 接下来就让我们一步一步实现吧,我们的角色转变为 web 服务器的开发人员, 先来写一段伪代码(还不能马上执行的程序),如下图所示:
A、 修改 HandlerRequest 类,将 Servlet 对象创建变为单实例方式:

if(servletClassName != null){
	    //获取封装响应参数对象
		ResponseObject responseObject = new ResponseObject();
		responseObject.setWriter(out);
		//获取封装请求参数对象
		RequestObject requestObject = new RequestObject(requestURI);
		out.print("HTTP/1.1 200 OK\n");
		out.print("Content-Type:text/html;charset=utf-8\n\n");
		//创建Servlet对象之前,先从缓存池中查找
		//1.有:拿来直接使用
		//2.没有:创建servlet对象,放到缓存池中
		Servlet servlet = ServletCache.get(urlPattern);
		if(servlet == null){
			//通过反射机制创建该业务处理类
			Class c = Class.forName(servletClassName);
			Object obj = c.newInstance();
			//这个时候,服务开发人员不知道如何调用servlet业务处理类里的方法了?
			servlet = (Servlet)obj;
			//将创建好的Servlet对象放到缓存池中
			ServletCache.put(urlPattern, servlet);
		}
		Logger.log("Servlet 对象:" + servlet);
		servlet.service(requestObject,responseObject);
}

(3) 目前,我们的角色还是 WEB 服务器的开发人员,让我们一起来实现ServletCache 类,如下图所示:

package com.bruceliu.httpserver.core;

import javax.servlet.Servlet;
import java.util.HashMap;
import java.util.Map;

/**
 * @Auther: bruceliu
 * @Date: 2020/2/6 12:19
 * @QQ:1241488705
 * @Description:Servlet对象缓存池
 */
public class ServletCache {
    private static Map<String,Servlet> servletMap = new HashMap<String, Servlet>();

    public static void put(String urlPattern,Servlet servlet){
        servletMap.put(urlPattern, servlet);
    }

    public static Servlet get(String urlPattern){
        return servletMap.get(urlPattern);
    }
}

在这里插入图片描述
(4) 启动 httpserver,打开浏览器,发送多次请求进行测试,测试结果如下:

http://127.0.0.1:8080/oa/login

在这里插入图片描述
在这里插入图片描述
(5) 结论:
通过以上的测试结果可以看到 Servlet 对象已达到了单实例,但是这里所谓的单实例,并不属于单
例模式,真正的单例模式要求构造函数私有化的,这里之所以可以做到单实例,是因为 Servlet 对象的生命周期由服务器来管理,服务器只创建一次,所以可以达到单实例的效果,这种单实例可以被称为伪单例。

另外需要注意的是, Servlet 对象是单实例的,这个时候就应该注意多线程并发带来的安全问题,
尽量不要在 Servlet 类中使用实例变量。

发布了274 篇原创文章 · 获赞 80 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/BruceLiu_code/article/details/104185100
今日推荐