【吃透Java手写】Tomcat-简易版-源码解析
- [1 准备工作](#1 准备工作)
-
- [1.1 引入依赖](#1.1 引入依赖)
- [1.2 创建一个Tomcat的启动类](#1.2 创建一个Tomcat的启动类)
- [2 线程池技术回顾](#2 线程池技术回顾)
-
- [2.1 线程池的使用流程](#2.1 线程池的使用流程)
- [2.2 线程池的参数](#2.2 线程池的参数)
-
- [2.2.1 任务队列(workQueue)](#2.2.1 任务队列(workQueue))
- [2.2.2 线程工厂(threadFactory)](#2.2.2 线程工厂(threadFactory))
- [2.2.3 拒绝策略(handler)](#2.2.3 拒绝策略(handler))
- [2.3 功能线程池](#2.3 功能线程池)
-
- [2.3.1 定长线程池(FixedThreadPool)](#2.3.1 定长线程池(FixedThreadPool))
- [2.3.2 定时线程池(ScheduledThreadPool )](#2.3.2 定时线程池(ScheduledThreadPool ))
- [2.3.3 可缓存线程池(CachedThreadPool)](#2.3.3 可缓存线程池(CachedThreadPool))
- [2.3.4 单线程化线程池(SingleThreadExecutor)](#2.3.4 单线程化线程池(SingleThreadExecutor))
- [2.3.5 对比](#2.3.5 对比)
- [3 Tomcat逻辑](#3 Tomcat逻辑)
-
- [3.1 请求](#3.1 请求)
-
- [3.1.1 单次请求](#3.1.1 单次请求)
- [3.1.2 多次请求](#3.1.2 多次请求)
- [3.1.3 线程池处理请求](#3.1.3 线程池处理请求)
- [3.2 处理socket连接](#3.2 处理socket连接)
-
- [3.2.1 http协议格式](#3.2.1 http协议格式)
- [3.2.2 具体解析](#3.2.2 具体解析)
- [3.2.3 Request](#3.2.3 Request)
- [3.3 请求类Request](#3.3 请求类Request)
- [3.4 响应类Response](#3.4 响应类Response)
-
- [3.4.1 请求体输出流ResponseOutputStream](#3.4.1 请求体输出流ResponseOutputStream)
- [3.5 servlet SJBServlet](#3.5 servlet SJBServlet)
- [3.6 测试](#3.6 测试)
- [3.7 Tomcat的部署应用](#3.7 Tomcat的部署应用)
-
- [3.7.1 @WebServlet 注解 和 web.xml 的区别](#3.7.1 @WebServlet 注解 和 web.xml 的区别)
- [3.7.2 目录存放](#3.7.2 目录存放)
- [3.7.3 部署](#3.7.3 部署)
- [3.7.4 部署测试](#3.7.4 部署测试)
1 准备工作
1.1 引入依赖
xml
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.0.1</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
</dependencies>
1.2 创建一个Tomcat的启动类
创建com.sjb.Tomcat
java
public class Tomcat {
public void start(){
}
public static void main(String[] args) {
Tomcat tomcatApplication = new Tomcat();
tomcatApplication.start();
}
}
2 线程池技术回顾
线程池的真正实现类是 ThreadPoolExecutor
2.1 线程池的使用流程
java
// 创建线程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(CORE_POOL_SIZE,
MAXIMUM_POOL_SIZE,
KEEP_ALIVE,
TimeUnit.SECONDS,
sPoolWorkQueue,
sThreadFactory);
// 向线程池提交任务
threadPool.execute(new Runnable() {
@Override
public void run() {
... // 线程执行的任务
}
});
// 关闭线程池
threadPool.shutdown(); // 设置线程池的状态为SHUTDOWN,然后中断所有没有正在执行任务的线程
threadPool.shutdownNow(); // 设置线程池的状态为 STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表
2.2 线程池的参数
主要参数:
-
corePoolSize(必需)
核心线程数。默认情况下,核心线程会一直存活,但是当将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
-
maximumPoolSize(必需)
线程池所能容纳的最大线程数。当活跃线程数达到该数值后,后续的新任务将会阻塞
-
keepAliveTime(必需)
线程闲置超时时长。如果超过该时长,非核心线程就会被回收。如果将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
-
unit(必需)
指定 keepAliveTime 参数的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。
-
workQueue(必需)
任务队列。通过线程池的 execute() 方法提交的 Runnable 对象将存储在该参数中。其采用阻塞队列实现。
-
threadFactory(可选)
线程工厂。用于指定为线程池创建新线程的方式。
-
handler(可选)
拒绝策略。当达到最大线程数时需要执行的饱和策略。
2.2.1 任务队列(workQueue)
任务队列是基于阻塞队列实现的,即采用生产者消费者模式,在 Java 中需要实现 BlockingQueue 接口。但 Java 已经为我们提供了 7 种阻塞队列的实现:
- ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列(数组结构可配合指针实现一个环形队列)。
- LinkedBlockingQueue: 一个由链表结构组成的有界阻塞队列,在未指明容量时,容量默认为 Integer.MAX_VALUE。
- PriorityBlockingQueue: 一个支持优先级排序的无界阻塞队列,对元素没有要求,可以实现 Comparable 接口也可以提供 Comparator 来对队列中的元素进行比较。跟时间没有任何关系,仅仅是按照优先级取任务。
- DelayQueue: 类似于PriorityBlockingQueue,是二叉堆实现的无界优先级阻塞队列。要求元素都实现 Delayed 接口,通过执行时延从队列中提取任务,时间没到任务取不出来。
- SynchronousQueue: 一个不存储元素的阻塞队列,消费者线程调用 take() 方法的时候就会发生阻塞,直到有一个生产者线程生产了一个元素,消费者线程就可以拿到这个元素并返回;生产者线程调用 put() 方法的时候也会发生阻塞,直到有一个消费者线程消费了一个元素,生产者才会返回。
- LinkedBlockingDeque: 使用双向队列实现的有界双端阻塞队列。双端意味着可以像普通队列一样 FIFO(先进先出),也可以像栈一样 FILO(先进后出)。
- LinkedTransferQueue: 它是ConcurrentLinkedQueue、LinkedBlockingQueue 和 SynchronousQueue 的结合体,但是把它用在 ThreadPoolExecutor 中,和 LinkedBlockingQueue 行为一致,但是是无界的阻塞队列。
注意有界队列和无界队列的区别:如果使用有界队列,当队列饱和时并超过最大线程数时就会执行拒绝策略;而如果使用无界队列,因为任务队列永远都可以添加任务,所以设置 maximumPoolSize 没有任何意义。
2.2.2 线程工厂(threadFactory)
线程工厂指定创建线程的方式,需要实现 ThreadFactory 接口,并实现 newThread(Runnable r) 方法。该参数可以不用指定,Executors 框架已经为我们实现了一个默认的线程工厂
2.2.3 拒绝策略(handler)
当线程池的线程数达到最大线程数时,需要执行拒绝策略。拒绝策略需要实现 RejectedExecutionHandler 接口,并实现 rejectedExecution(Runnable r, ThreadPoolExecutor executor) 方法。不过 Executors 框架已经为我们实现了 4 种拒绝策略:
- AbortPolicy(默认):丢弃任务并抛出 RejectedExecutionException 异常。
- CallerRunsPolicy:由调用线程处理该任务。
- DiscardPolicy:丢弃任务,但是不抛出异常。可以配合这种模式进行自定义的处理方式。
- DiscardOldestPolicy:丢弃队列最早的未处理任务,然后重新尝试执行任务。
2.3 功能线程池
Executors已经为我们封装好了 4 种常见的功能线程池,如下:
- 定长线程池(FixedThreadPool)
- 定时线程池(ScheduledThreadPool )
- 可缓存线程池(CachedThreadPool)
- 单线程化线程池(SingleThreadExecutor)
2.3.1 定长线程池(FixedThreadPool)
源码:
java
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
- 特点:只有核心线程,线程数量固定,执行完立即回收,任务队列为链表结构的有界队列。
- 应用场景:控制线程最大并发数。
使用示例:
java
// 1. 创建定长线程池对象 & 设置线程池线程数量固定为3
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
public void run() {
System.out.println("执行任务啦");
}
};
// 3. 向线程池提交任务
fixedThreadPool.execute(task);
2.3.2 定时线程池(ScheduledThreadPool )
源码:
java
private static final long DEFAULT_KEEPALIVE_MILLIS = 10L;
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue());
}
public static ScheduledExecutorService newScheduledThreadPool(
int corePoolSize, ThreadFactory threadFactory) {
return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}
public ScheduledThreadPoolExecutor(int corePoolSize,
ThreadFactory threadFactory) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue(), threadFactory);
}
- 特点:核心线程数量固定,非核心线程数量无限,执行完闲置 10ms 后回收,任务队列为延时阻塞队列。
- 应用场景:执行定时或周期性的任务。
java
// 1. 创建 定时线程池对象 & 设置线程池线程数量固定为5
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
public void run() {
System.out.println("执行任务啦");
}
};
// 3. 向线程池提交任务
scheduledThreadPool.schedule(task, 1, TimeUnit.SECONDS); // 延迟1s后执行任务
scheduledThreadPool.scheduleAtFixedRate(task,10,1000,TimeUnit.MILLISECONDS);// 延迟10ms后、每隔1000ms执行任务
2.3.3 可缓存线程池(CachedThreadPool)
源码:
java
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
- 特点:无核心线程,非核心线程数量无限,执行完闲置 60s 后回收,任务队列为不存储元素的阻塞队列。
- 应用场景:执行大量、耗时少的任务。
java
// 1. 创建可缓存线程池对象
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
public void run() {
System.out.println("执行任务啦");
}
};
// 3. 向线程池提交任务
cachedThreadPool.execute(task);
2.3.4 单线程化线程池(SingleThreadExecutor)
源码:
java
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
- 特点:只有 1 个核心线程,无非核心线程,执行完立即回收,任务队列为链表结构的有界队列。
- 应用场景:不适合并发但可能引起 IO 阻塞性及影响 UI 线程响应的操作,如数据库操作、文件操作等。
java
// 1. 创建单线程化线程池
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
public void run() {
System.out.println("执行任务啦");
}
};
// 3. 向线程池提交任务
singleThreadExecutor.execute(task);
2.3.5 对比
3 Tomcat逻辑
3.1 请求
3.1.1 单次请求
在com.sjb.Tomcat#start
java
public class Tomcat {
public void start(){
//socket连接TCP
try {
ServerSocket serverSocket = new ServerSocket(8080);
Socket socket = serverSocket.accept();
processSocket(socket);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void processSocket(Socket socket) {
//处理socket请求
}
public static void main(String[] args) {
Tomcat tomcatApplication = new Tomcat();
tomcatApplication.start();
}
}
使用了 ServerSocket
类来监听8080端口上的连接。
通过serverSocket.accept()
阻塞监听8080端口,一直等待直到有一个客户端连接请求到达。一旦有连接请求到达,accept()
方法会返回一个新的 Socket
对象,该对象表示服务器和客户端之间建立的连接。然后你就可以使用这个 Socket
对象来进行通信,发送和接收数据。
每当有一个连接到达,它会调用 processSocket()
方法来处理该连接。
3.1.2 多次请求
但是这个有个只处理单次请求,我们需要加上一个while来不断地进行阻塞监听
java
try {
ServerSocket serverSocket = new ServerSocket(8080);
while(true){
Socket socket = serverSocket.accept();
processSocket(socket);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
但是这个是线性的,一次只能处理一个
3.1.3 线程池处理请求
引入线程池,提升并行处理能力
java
public class Tomcat {
public void start(){
//socket连接TCP
try {
ExecutorService executorService = Executors.newFixedThreadPool(20);
ServerSocket serverSocket = new ServerSocket(8080);
while(true){
Socket socket = serverSocket.accept();
executorService.execute(new SocketProcessor(socket));
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) {
Tomcat tomcatApplication = new Tomcat();
tomcatApplication.start();
}
}
创建com.sjb.SocketProcessor实现Runnable
java
public class SocketProcessor implements Runnable{
private Socket socket;
public SocketProcessor(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
processSocket(socket);
}
private void processSocket(Socket socket) {
//处理socket
}
}
3.2 处理socket连接
3.2.1 http协议格式
tomcat需要按照这个格式进行解析,这里先把他全部输出就不解析了。
在com.sjb.SocketProcessor#processSocket中
java
private void processSocket(Socket socket) {
//处理socket
try {
InputStream inputStream = socket.getInputStream();
byte[] bytes = new byte[1024];
//循环读取数据
while (true){
int read = inputStream.read(bytes);
if(read == -1){
break;
}
System.out.println(new String(bytes,0,read));
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
GET / HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Cache-Control: max-age=0
sec-ch-ua: "Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: zh-CN,zh;q=0.9
Cookie: Idea-7b055fc6=c09dc312-ccaa-43d4-bf1f-95a39406b2e6; username-localhost-60524="2|1:0|10:1713800933|24:username-localhost-60524|196:eyJ1c2VybmFtZSI6ICJjMTdjZmFhYjA0MGQ0Njg3YWFjNzZiYzc1MmQ3Zjc0ZCIsICJuYW1lIjogIkFub255bW91cyBUaHlvbmUiLCAiZGlzcGxheV9uYW1lIjogIkFub255bW91cyBUaHlvbmUiLCAiaW5pdGlhbHMiOiAiQVQiLCAiY29sb3IiOiBudWxsfQ==|d3
42be023b8e3ab274c8019dd4a41dc7f3102301b8faf7ea83435c06dd9b614b"; _xsrf=2|0e634e08|aaad0368070a9f2267481456f73e2dd6|1713800933; username-localhost-61085=2|1:0|10:1713801376|24:username-localhost-61085|204:eyJ1c2VybmFtZSI6ICJhM2IzOTQwNjhjYzM0OWM4OTYyYzUxODQ2Y2IwNzU1YiIsICJuYW1lIjogIkFub255bW91cyBDYWxsaXJyaG9lIiwgImRpc3BsYXlfbmFtZSI6ICJBbm9ueW1vdXMgQ2FsbGlycmhvZSIsICJpbml0aWFscyI6ICJBQyIsICJjb2xvciI6IG51bGx9|e49adff6d260df7ee49848807369b565e82e36906318b9546796c968f21a9361; username-localhost-61138="2|1:0|10:1713801443|24:username-localhost-61138|200:eyJ1c2VybmFtZSI6ICI4YzVkOWRkYTBmNGE0YTk1YjQ0MjFkMzYwZDRmNTFhMyIsICJuYW1lIjogIkFub255bW91cyBMeXNpdGhlYSIsICJkaXNwbGF5X25hbWUiOiAiQW5vbnltb3VzIEx5c2l0aGVhIiwgImluaXRpYWxzIjogIkFMIiwgImNvbG9yIjogbnVsbH0=|587e609fe7fc575e84763d39c6fd8b9feb8cdd0da05046d03dec57ba58307aef"
3.2.2 具体解析
我们需要获得method、url、protocl来创建Request对象
java
//处理socket
try {
InputStream inputStream = socket.getInputStream();
//解析请求头,获得method、url、protocol
byte[] bytes = new byte[1024];
int read = inputStream.read(bytes);
String request = new String(bytes, 0, read);
String[] split = request.split("\r\n");
String[] split1 = split[0].split(" ");
String method = split1[0];
String url = split1[1];
String protocol = split1[2];
System.out.println("method: " + method);
System.out.println("url: " + url);
System.out.println("protocol: " + protocol);
} catch (IOException e) {
throw new RuntimeException(e);
}
输出
method: GET
url: /abc
protocol: HTTP/1.1
请求头和请求体同理
3.2.3 Request
创建com.sjb.Request
java
public class Request {
private String method;
private String url;
private String protocol;
public Request(String method, String url, String protocol) {
this.method = method;
this.url = url;
this.protocol = protocol;
}
public String getMethod() {
return method;
}
public String getUrl() {
return url;
}
public String getProtocol() {
return protocol;
}
}
在com.sjb.SocketProcessor#processSocket中创建Request对象,再进行之后的匹配servlet之后执行对应的doGet方法、doPost方法等等
java
//处理socket
try {
InputStream inputStream = socket.getInputStream();
//解析请求头,获得method、url、protocol
byte[] bytes = new byte[1024];
int read = inputStream.read(bytes);
String request = new String(bytes, 0, read);
String[] split = request.split("\r\n");
String[] split1 = split[0].split(" ");
String method = split1[0];
String url = split1[1];
String protocol = split1[2];
Request tomcatrequest = new Request(method, url, protocol);
//根据url找到对应的servlet
} catch (IOException e) {
throw new RuntimeException(e);
}
3.3 请求类Request
Request类需要实现接口HttpServletResponse,里面有很多方法,我们避免麻烦创建一个com.sjb.AbstractHttpServletResponse类实现接口,我们的Request类只需要继承AbstractHttpServletResponse即可
创建com.sjb.AbstractHttpServletResponse
java
public class AbstractHttpServletRequest implements HttpServletRequest {
@Override
public String getAuthType() {
return null;
}
@Override
public Cookie[] getCookies() {
return new Cookie[0];
}
.................
创建com.sjb.Request
java
public class Request extends AbstractHttpServletRequest {
private String method;
private String url;
private String protocol;
private Socket socket;
public Socket getSocket() {
return socket;
}
public Request(String method, String url, String protocol, Socket socket) {
this.method = method;
this.url = url;
this.protocol = protocol;
this.socket = socket;
}
public String getMethod() {
return method;
}
public StringBuffer getRequestURL() {
return new StringBuffer(url);
}
public String getProtocol() {
return protocol;
}
}
public Request(String method, String url, String protocol, Socket socket)
一个socket对应一个Request对应一个response
3.4 响应类Response
与3.3类似
创建com.sjb.AbstractHttpServletResponse
java
public class AbstractHttpServletResponse implements HttpServletResponse {
@Override
public void addCookie(Cookie cookie) {
}
@Override
public boolean containsHeader(String s) {
return false;
}
...................
创建com.sjb.Response
java
public class Response extends AbstractHttpServletResponse {
private String message="OK";
private int status=200;
private Map<String,String> headers = new HashMap<>();
private Request request;
private OutputStream socketOutputStream;
private ResponseOutputStream responseOutputStream=new ResponseOutputStream();
public Response(Request request) {
this.request = request;
try {
this.socketOutputStream = request.getSocket().getOutputStream();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void setStatus(int status, String message) {
this.status = status;
this.message = message;
}
@Override
public int getStatus() {
return status;
}
@Override
public void addHeader(String s, String s1) {
headers.put(s, s1);
}
@Override
public ResponseOutputStream getOutputStream() throws IOException {
return responseOutputStream;
}
public void complete() {
//发送响应
sendResponseLine();
sendResponseHeader();
sendResponseBody();
}
private void sendResponseBody() {
try {
byte[] body = getOutputStream().getBody();
int pos = getOutputStream().getPos();
socketOutputStream.write(body, 0, pos);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void sendResponseHeader() {
try {
for (Map.Entry<String, String> entry : headers.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
socketOutputStream.write(key.getBytes());
socketOutputStream.write(": ".getBytes());
socketOutputStream.write(value.getBytes());
socketOutputStream.write("\r\n".getBytes());
}
socketOutputStream.write("\r\n".getBytes());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void sendResponseLine() {
try {
socketOutputStream.write(request.getProtocol().getBytes());
socketOutputStream.write(" ".getBytes());
socketOutputStream.write(String.valueOf(status).getBytes());
socketOutputStream.write(" ".getBytes());
socketOutputStream.write(message.getBytes());
socketOutputStream.write("\r\n".getBytes());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
属性:
int status
:状态码String message
:说明Map<String,String> header
:存储响应头,例如一些长度、编码信息等Request request
:一个response对应一个requestOutputStream socketOutputStream
:socket的整个输出流ResponseOutputStream responseOutputStream
:响应体的输出流,因为响应体可能有很多,所以需要用一个输出流进行缓存,并且如果响应失败的话,也只有响应行和响应头返回,响应体不返回。
private ResponseOutputStream responseOutputStream=new ResponseOutputStream();
一开始就创建好响应响应体的输出流,保证public ResponseOutputStream getOutputStream()
拿到的都是同一个响应体的输出流
3.4.1 请求体输出流ResponseOutputStream
因为响应体可能有很多,所以需要用一个输出流进行缓存,并且如果响应失败的话,也只有响应行和响应头返回,响应体不返回。
创建com.sjb.ResponseOutputStream
java
public class ResponseOutputStream extends ServletOutputStream {
public byte[] getBody() {
return body;
}
public int getPos() {
return pos;
}
private byte[] body=new byte[1024];
private int pos=0;
@Override
public void write(int b) throws IOException {
//响应体
body[pos++]=(byte)b;
}
}
3.5 servlet SJBServlet
根据Request创建对应的Response并且创建servlet,调用servlet的service方法,匹配doGet或者doPost
在com.sjb.SocketProcessor#processSocket中
java
try {
InputStream inputStream = socket.getInputStream();
//解析请求头,获得method、url、protocol
byte[] bytes = new byte[1024];
int read = inputStream.read(bytes);
String request = new String(bytes, 0, read);
String[] split = request.split("\r\n");
String[] split1 = split[0].split(" ");
String method = split1[0];
String url = split1[1];
String protocol = split1[2];
Request tomcatrequest = new Request(method, url, protocol, socket);
//根据url找到对应的servlet
Response response = new Response(tomcatrequest);
SJBServlet servlet = new SJBServlet();
//调用servlet的service方法,匹配doGet或者doPost
servlet.service(tomcatrequest, response);
//输出响应
response.complete();
}
创建com.sjb.SJBServlet,service方法自动给匹配请求方法,我们只需要重写对应的doGet、doPost方法即可
java
public class SJBServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println(req.getMethod());
resp.addHeader("Content-Type", "text/html;charset=utf-8");
resp.addHeader("Content-Length", "15");
resp.getOutputStream().write("Hello World sjb".getBytes());
}
}
在doGet方法调用response.addHeader()
中设置请求头,编码方式、请求体长度等。
也不一定在doGet方法设置请求头,亦可以在com.sjb.Response#sendResponseHeader中设置请求头
java
private void sendResponseHeader() {
try {
if(!headers.containsKey("Content-Type")){
headers.put("Content-Type", "text/html;charset=utf-8");
}
if(!headers.containsKey("Content-Length")){
headers.put("Content-Length", String.valueOf(responseOutputStream.getPos()));
}
resp.getOutputStream().write("Hello World sjb".getBytes());
设置请求体。
最后调用response.complete();
写请求行、请求头、请求体。完成输出流的输出。
3.6 测试
访问
3.7 Tomcat的部署应用
在tomcat中有一个专门的目录webapps用来存放tomcat的项目,一个tomcat项目中的classes中可以有多个servlet
如果要部署tomcat需要在对应的servlet上加上@WebServlet 注解
在SJBServlet上添加注解
java
@WebServlet(
urlPatterns = {"/sjb"}
)
public class SJBServlet extends HttpServlet {
public SJBServlet() {
}
@WebServlet 属于类级别的注解,标注在继承了 HttpServlet 的类之上。常用的写法是将 Servlet 的相对请求路径(即 value)直接写在注解内,@WebServlet(urlPatterns = {"/sjb"})。@WebServlet(urlPatterns = {"/sjb"})省略了 urlPatterns 属性名
如果 @WebServlet 中需要设置多个属性,则属性之间必须使用逗号隔开。
通过实现 Serlvet 接口或继承 GenericServlet 创建的 Servlet 类无法使用 @WebServlet 注解。
使用 @WebServlet 注解配置的 Servlet 类,不要在 web.xml 文件中再次配置该 Servlet 相关属性。若同时使用 web.xml 与 @WebServlet 配置同一 Servlet 类,则 web.xml 中 的值与注解中 name 取值不能相同,否则容器会忽略注解中的配置。
3.7.1 @WebServlet 注解 和 web.xml 的区别
使用 web.xml 或 @WebServlet 注解都可以配置 Servlet
-
@WebServlet 注解配置 Servlet
- 优点:@WebServlet 直接在 Servlet 类中使用,代码量少,配置简单。每个类只关注自身业务逻辑,与其他 Servlet 类互不干扰,适合多人同时开发。
- 缺点:Servlet 较多时,每个 Servlet 的配置分布在各自的类中,不便于查找和修改。
-
web.xml 配置文件配置 Servlet
- 优点:集中管理 Servlet 的配置,便于查找和修改。
- 缺点:代码较繁琐,可读性不强,不易于理解。
3.7.2 目录存放
要将重新编译好的SJBServlet.class放入Tomcat/webapps/hello/classes/com/sjb目录下,并且删除之前的SJBServlet.class
所以在com.sjb.SocketProcessor#processSocket中就不能直接调用SJBServlet,需要对环境进行解析找到对应的Servlet
3.7.3 部署
在com.sjb.Tomcat#main中需要部署apps
java
public static void main(String[] args) {
Tomcat tomcatApplication = new Tomcat();
tomcatApplication.deployApps();
tomcatApplication.start();
}
deployApps() :获取当前父项目目录下所有的Tomcat/webapps下的所有目录
java
private void deployApps() {
//拿到当前模块的目录的子目录
//webapps = D:\Code\JavaCode\handwith-Spring\handwith-Spring\Tomcat\webapps
File webapps = new File(System.getProperty("user.dir"),"Tomcat/webapps");
for(String app: webapps.list()){
//app: hello
deployApp(webapps,app);
}
}
针对app: hello进行部署,deployApp(webapps,app):
- webapps父目录,app子目录
- context负责存放当前项目。一个context项目下可能有多个app(servlet)
java
private void deployApp(File webapps,String app) {
Context context = new Context(app);
//appDir = D:\Code\JavaCode\handwith-Spring\handwith-Spring\Tomcat\webapps\hello
File appDir = new File(webapps, app);
//classesDir = D:\Code\JavaCode\handwith-Spring\handwith-Spring\Tomcat\webapps\hello\classes
File classesDir = new File(appDir, "classes");
List<File> files = getAllFilePath(classesDir);
for(File file:files){
//name: D:\Code\JavaCode\handwith-Spring\handwith-Spring\Tomcat\webapps\hello\classes\com\sjb\SJBServlet.class
String name=file.getPath();
//name: com\sjb\SJBServlet.class
name=name.replace(classesDir.getPath()+"\\","");
//name: com\sjb\SJBServlet
name=name.replace(".class","");
//name: com.sjb.SJBServlet
name=name.replace("\\",".");
System.out.println(name);
//加载类加载器
try {
WebappClassLoader webappClassLoader = new WebappClassLoader(new URL[]{classesDir.toURL()});
Class<?> aClass = webappClassLoader.loadClass(name);
if(HttpServlet.class.isAssignableFrom(aClass)){
if(aClass.isAnnotationPresent(WebServlet.class)){
WebServlet webServlet = aClass.getAnnotation(WebServlet.class);
//urlPatterns:["/sjb"]
String[] urlPatterns = webServlet.urlPatterns();
for(String urlPattern:urlPatterns){
context.addServlet(urlPattern, (Servlet) aClass.newInstance());
}
}
}
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} catch (MalformedURLException e) {
throw new RuntimeException(e);
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
contextMap.put(app,context);
}
List<File> files = getAllFilePath(classesDir);
获取hello\classes下所有的文件,递归搜索
java
private List<File> getAllFilePath(File classesDir) {
List<File> result = new ArrayList<>();
File[] files = classesDir.listFiles();
if(files!=null){
for(File file:files){
if(file.isDirectory()){
result.addAll(getAllFilePath(file));
}else{
result.add(file);
}
}
}
return result;
}
获取到类名name:com.sjb.SJBServlet
后,通过类加载器进行加载。
这里不能使用Class<?> aClass1 = Thread.currentThread().getContextClassLoader().loadClass(name);
因为这里name是webapps目录下的com.sjb.SJBServlet,而用Thread.currentThread().getContextClassLoader().loadClass()
只能加载当前项目下的target目录下的类。aClass.newInstance()
通过类加载器new一个对象,和url一起组成Entry放入context下的map里,最后将context放入context的map里
因此需要创建一个新的类加载器com.sjb.WebappClassLoader,继承URLClassLoader,通过url来进行加载
java
public class WebappClassLoader extends URLClassLoader {
public WebappClassLoader(URL[] urls) {
super(urls);
}
}
HttpServlet.class.isAssignableFrom(aClass)
然后再判断这个类是不是HttpServlet,aClass.isAnnotationPresent(WebServlet.class)
判断有没有@WebServlet,然后获取注解的内容,并且获取url,,然后把他存入<string,context>的map中,context用来存放每个项目/应用(hello),每个项目中可能有多个servlet
创建com.sjb.Context
java
public class Context {
private String appName;
private Map<String, Servlet> servletMap=new HashMap<>();
public Context(String appName) {
this.appName = appName;
}
public void addServlet(String url, Servlet servlet){
servletMap.put(url,servlet);
}
public Servlet getServletByUrl(String url){
return servletMap.get(url);
}
}
在com.sjb.Tomcat中创建一个Map<String, Context>的map
java
public class Tomcat {
public Map<String, Context> getContextMap() {
return contextMap;
}
private Map<String,Context> contextMap=new HashMap<>();
这样就在解析阶段就已经找到了对应的servlet
在com.sjb.SocketProcessor#processSocket中
java
Request tomcatrequest = new Request(method, url, protocol, socket);
Response response = new Response(tomcatrequest);
//根据url找到对应的servlet
// url: "/hello/sjb"
url=url.substring(1);
// parts: ["abc"]
String[] parts= url.split("/");
String appName=parts[0];
Context context = tomcat.getContextMap().get(appName);
Servlet servlet = context.getServletByUrl("/"+parts[1]);
servlet.service(tomcatrequest, response);
3.7.4 部署测试
访问localhost:8080/hello/sjb,成功访问到