当前位置: 首页 > news >正文

【吃透Java手写】4-Tomcat-简易版

【吃透Java手写】Tomcat-简易版-源码解析

  • 1 准备工作
    • 1.1 引入依赖
    • 1.2 创建一个Tomcat的启动类
  • 2 线程池技术回顾
    • 2.1 线程池的使用流程
    • 2.2 线程池的参数
      • 2.2.1 任务队列(workQueue)
      • 2.2.2 线程工厂(threadFactory)
      • 2.2.3 拒绝策略(handler)
    • 2.3 功能线程池
      • 2.3.1 定长线程池(FixedThreadPool)
      • 2.3.2 定时线程池(ScheduledThreadPool )
      • 2.3.3 可缓存线程池(CachedThreadPool)
      • 2.3.4 单线程化线程池(SingleThreadExecutor)
      • 2.3.5 对比
  • 3 Tomcat逻辑
    • 3.1 请求
      • 3.1.1 单次请求
      • 3.1.2 多次请求
      • 3.1.3 线程池处理请求
    • 3.2 处理socket连接
      • 3.2.1 http协议格式
      • 3.2.2 具体解析
      • 3.2.3 Request
    • 3.3 请求类Request
    • 3.4 响应类Response
      • 3.4.1 请求体输出流ResponseOutputStream
    • 3.5 servlet SJBServlet
    • 3.6 测试
    • 3.7 Tomcat的部署应用
      • 3.7.1 @WebServlet 注解 和 web.xml 的区别
      • 3.7.2 目录存放
      • 3.7.3 部署
      • 3.7.4 部署测试


1 准备工作

1.1 引入依赖

<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

public class Tomcat {public void start(){}public static void main(String[] args) {Tomcat tomcatApplication = new Tomcat();tomcatApplication.start();}
}

2 线程池技术回顾

线程池的真正实现类是 ThreadPoolExecutor

2.1 线程池的使用流程

// 创建线程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(CORE_POOL_SIZE,MAXIMUM_POOL_SIZE,KEEP_ALIVE,TimeUnit.SECONDS,sPoolWorkQueue,sThreadFactory);
// 向线程池提交任务
threadPool.execute(new Runnable() {@Overridepublic 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 种阻塞队列的实现:

  1. ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列(数组结构可配合指针实现一个环形队列)。
  2. LinkedBlockingQueue: 一个由链表结构组成的有界阻塞队列,在未指明容量时,容量默认为 Integer.MAX_VALUE。
  3. PriorityBlockingQueue: 一个支持优先级排序的无界阻塞队列,对元素没有要求,可以实现 Comparable 接口也可以提供 Comparator 来对队列中的元素进行比较。跟时间没有任何关系,仅仅是按照优先级取任务。
  4. DelayQueue: 类似于PriorityBlockingQueue,是二叉堆实现的无界优先级阻塞队列。要求元素都实现 Delayed 接口,通过执行时延从队列中提取任务,时间没到任务取不出来。
  5. SynchronousQueue: 一个不存储元素的阻塞队列,消费者线程调用 take() 方法的时候就会发生阻塞,直到有一个生产者线程生产了一个元素,消费者线程就可以拿到这个元素并返回;生产者线程调用 put() 方法的时候也会发生阻塞,直到有一个消费者线程消费了一个元素,生产者才会返回。
  6. LinkedBlockingDeque: 使用双向队列实现的有界双端阻塞队列。双端意味着可以像普通队列一样 FIFO(先进先出),也可以像栈一样 FILO(先进后出)。
  7. 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 种拒绝策略:

  1. AbortPolicy(默认):丢弃任务并抛出 RejectedExecutionException 异常。
  2. CallerRunsPolicy:由调用线程处理该任务。
  3. DiscardPolicy:丢弃任务,但是不抛出异常。可以配合这种模式进行自定义的处理方式。
  4. DiscardOldestPolicy:丢弃队列最早的未处理任务,然后重新尝试执行任务。

2.3 功能线程池

Executors已经为我们封装好了 4 种常见的功能线程池,如下:

  • 定长线程池(FixedThreadPool)
  • 定时线程池(ScheduledThreadPool )
  • 可缓存线程池(CachedThreadPool)
  • 单线程化线程池(SingleThreadExecutor)

2.3.1 定长线程池(FixedThreadPool)

源码:

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);
}
  • 特点:只有核心线程,线程数量固定,执行完立即回收,任务队列为链表结构的有界队列。
  • 应用场景:控制线程最大并发数。

使用示例:

// 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 )

源码:

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 后回收,任务队列为延时阻塞队列。
  • 应用场景:执行定时或周期性的任务。
// 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)

源码:

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 后回收,任务队列为不存储元素的阻塞队列。
  • 应用场景:执行大量、耗时少的任务。
// 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)

源码:

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 线程响应的操作,如数据库操作、文件操作等。
// 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

public class Tomcat {public void start(){//socket连接TCPtry {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来不断地进行阻塞监听

try {ServerSocket serverSocket = new ServerSocket(8080);while(true){Socket socket = serverSocket.accept();processSocket(socket);}
} catch (IOException e) {throw new RuntimeException(e);
}

但是这个是线性的,一次只能处理一个

3.1.3 线程池处理请求

引入线程池,提升并行处理能力

public class Tomcat {public void start(){//socket连接TCPtry {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

public class SocketProcessor implements Runnable{private Socket socket;public SocketProcessor(Socket socket) {this.socket = socket;}@Overridepublic void run() {processSocket(socket);}private void processSocket(Socket socket) {//处理socket}
}

3.2 处理socket连接

3.2.1 http协议格式

在这里插入图片描述

tomcat需要按照这个格式进行解析,这里先把他全部输出就不解析了。

在com.sjb.SocketProcessor#processSocket中

private void processSocket(Socket socket) {//处理sockettry {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);}
}

访问http://localhost:8080/,输出

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对象

访问http://localhost:8080/abc

//处理socket
try {InputStream inputStream = socket.getInputStream();//解析请求头,获得method、url、protocolbyte[] 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

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方法等等

//处理socket
try {InputStream inputStream = socket.getInputStream();//解析请求头,获得method、url、protocolbyte[] 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

public class AbstractHttpServletRequest implements HttpServletRequest {@Overridepublic String getAuthType() {return null;}@Overridepublic Cookie[] getCookies() {return new Cookie[0];}.................

创建com.sjb.Request

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

public class AbstractHttpServletResponse implements HttpServletResponse {@Overridepublic void addCookie(Cookie cookie) {}@Overridepublic boolean containsHeader(String s) {return false;}...................

创建com.sjb.Response

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);}}@Overridepublic void setStatus(int status, String message) {this.status = status;this.message = message;}@Overridepublic int getStatus() {return status;}@Overridepublic void addHeader(String s, String s1) {headers.put(s, s1);}@Overridepublic 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对应一个request
  • OutputStream socketOutputStream:socket的整个输出流
  • ResponseOutputStream responseOutputStream:响应体的输出流,因为响应体可能有很多,所以需要用一个输出流进行缓存,并且如果响应失败的话,也只有响应行和响应头返回,响应体不返回。

private ResponseOutputStream responseOutputStream=new ResponseOutputStream();一开始就创建好响应响应体的输出流,保证public ResponseOutputStream getOutputStream()拿到的都是同一个响应体的输出流

3.4.1 请求体输出流ResponseOutputStream

因为响应体可能有很多,所以需要用一个输出流进行缓存,并且如果响应失败的话,也只有响应行和响应头返回,响应体不返回。

创建com.sjb.ResponseOutputStream

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;@Overridepublic 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中

try {InputStream inputStream = socket.getInputStream();//解析请求头,获得method、url、protocolbyte[] 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找到对应的servletResponse response = new Response(tomcatrequest);SJBServlet servlet = new SJBServlet();//调用servlet的service方法,匹配doGet或者doPostservlet.service(tomcatrequest, response);//输出响应response.complete();
}

创建com.sjb.SJBServlet,service方法自动给匹配请求方法,我们只需要重写对应的doGet、doPost方法即可

public class SJBServlet extends HttpServlet {@Overrideprotected 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中设置请求头

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上添加注解

@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

public static void main(String[] args) {Tomcat tomcatApplication = new Tomcat();tomcatApplication.deployApps();tomcatApplication.start();
}

deployApps() :获取当前父项目目录下所有的Tomcat/webapps下的所有目录

private void deployApps() {//拿到当前模块的目录的子目录//webapps = D:\Code\JavaCode\handwith-Spring\handwith-Spring\Tomcat\webappsFile webapps = new File(System.getProperty("user.dir"),"Tomcat/webapps");for(String app: webapps.list()){//app: hellodeployApp(webapps,app);}
}

针对app: hello进行部署,deployApp(webapps,app):

  • webapps父目录,app子目录
  • context负责存放当前项目。一个context项目下可能有多个app(servlet)
private void deployApp(File webapps,String app) {Context context = new Context(app);//appDir = D:\Code\JavaCode\handwith-Spring\handwith-Spring\Tomcat\webapps\helloFile appDir = new File(webapps, app);//classesDir = D:\Code\JavaCode\handwith-Spring\handwith-Spring\Tomcat\webapps\hello\classesFile 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.classString name=file.getPath();//name: com\sjb\SJBServlet.classname=name.replace(classesDir.getPath()+"\\","");//name: com\sjb\SJBServletname=name.replace(".class","");//name: com.sjb.SJBServletname=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下所有的文件,递归搜索

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来进行加载

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

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

public class Tomcat {public Map<String, Context> getContextMap() {return contextMap;}private Map<String,Context> contextMap=new HashMap<>();

这样就在解析阶段就已经找到了对应的servlet

在com.sjb.SocketProcessor#processSocket中

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,成功访问到

在这里插入图片描述

相关文章:

【吃透Java手写】4-Tomcat-简易版

【吃透Java手写】Tomcat-简易版-源码解析 1 准备工作1.1 引入依赖1.2 创建一个Tomcat的启动类 2 线程池技术回顾2.1 线程池的使用流程2.2 线程池的参数2.2.1 任务队列&#xff08;workQueue&#xff09;2.2.2 线程工厂&#xff08;threadFactory&#xff09;2.2.3 拒绝策略&…...

开发中的一些专业术语,POJO、PO...

在 Java 开发中&#xff0c;以下是常见的设计模式和概念&#xff1a; PO&#xff08;Persistent Object&#xff09;&#xff1a;持久化对象&#xff0c;也称为实体类或数据对象。它是与数据库表结构对应的类&#xff0c;通常用于表示持久化数据的实体。PO 类的属性与数据库表的…...

79.网络游戏逆向分析与漏洞攻防-移动系统分析-利用数据包实现人物走路

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 如果看不懂、不知道现在做的什么&#xff0c;那就跟着做完看效果&#xff0c;代码看不懂是正常的&#xff0c;只要会抄就行&#xff0c;抄着抄着就能懂了 内容…...

JS基础:输出信息的5种方式详解

你好&#xff0c;我是云桃桃。 一个希望帮助更多朋友快速入门 WEB 前端的程序媛。 云桃桃-大专生&#xff0c;一枚程序媛&#xff0c;感谢关注。回复 “前端基础题”&#xff0c;可免费获得前端基础 100 题汇总&#xff0c;回复 “前端基础路线”&#xff0c;可获取完整web基础…...

教你解决PUBG绝地求生延迟高 网络延迟高的问题

在《绝地求生》&#xff08;PUBG&#xff09;这款备受全球玩家追捧的战术竞技游戏中&#xff0c;其逼真的战场环境和心跳加速的生存竞赛无不让人为之痴迷。不过&#xff0c;有些玩家在经历了一场惊心动魄的对局后&#xff0c;却面临了一个不大不小的麻烦&#xff1a;比赛圆满落…...

【QT教程】QT6与C++17 QT与C++新特性

QT6与C17 使用AI技术辅助生成 QT界面美化视频课程 QT性能优化视频课程 QT原理与源码分析视频课程 QT QML C扩展开发视频课程 免费QT视频课程 您可以看免费1000个QT技术视频 免费QT视频课程 QT统计图和QT数据可视化视频免费看 免费QT视频课程 QT性能优化视频免费看 免费QT视频…...

多线程三种实现

多线程 线程 线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中&#xff0c;是进程中的实际运作单位。 &#xff08;理解&#xff1a;应用软件中互相独立&#xff0c;可以同时运行的功能&#xff09; 进程 进程是程序的基本执行实体。&#xff08;理解&#…...

【前端】HTML实现个人简历信息填写页面

文章目录 前言一、综合案例&#xff1a;个人简历信息填写页面 前言 这篇博客仅仅是对HTML的基本结构进行了一些说明&#xff0c;关于HTML的更多讲解以及CSS、Javascript部分的讲解可以关注一下下面的专栏&#xff0c;会持续更新的。 链接&#xff1a; Web前端学习专栏 下面我对…...

岩点×数说故事×小红书 | 发布《中国攀岩行业分析报告》

从下班健身到下班攀岩&#xff0c;从“鸡娃”到岩馆“溜娃”&#xff0c;被奥运“正名”的攀岩运动&#xff0c;在国内熬过了萌芽阶段&#xff0c;悄然开出了花。2023年&#xff0c;各类重磅攀岩赛事重启、线下岩馆疯狂扩张&#xff0c;小众攀岩正式进入大众视野&#xff0c;风…...

DPDK+PKTGEN环境搭建

【环境准备】 1、python python版本,需要3.6以上版本,若存在3.6版本,可以通过软链接指明目标。 ln -sf /usr/bin/python3.6 /usr/bin/python 2、meson sudo pip3 install meson==0.63.3 【代码下载】 1、dpdk https://fast.dpdk.org/rel/dpdk-22.11.4.tar.xz 2、dpdk-kmo…...

【面试干货】HTTP和HTTPS之间的主要区别

【面试干货】HTTP和HTTPS之间的主要区别 1、URL前缀2、安全性3、端口4、工作层级5、加密6、证书 &#x1f496;The Begin&#x1f496;点点关注&#xff0c;收藏不迷路&#x1f496; 1、URL前缀 HTTP 的URL以http://开头&#xff0c;而 HTTPS 的URL以https://开头&#xff0c;…...

04、Kafka集群安装

1、准备工作 首先准备一台虚拟机&#xff0c;centos7系统&#xff0c;先在一台上配置安装后&#xff0c;最后克隆成多台机器。 1.1 安装JDK &#xff08;1&#xff09;下载JDK&#xff0c;上传到 /root/software 路径 下载地址&#xff1a;https://www.oracle.com/cn/java/…...

Vue自定义封装音频播放组件(带拖拽进度条)

Vue自定义封装音频播放组件&#xff08;带拖拽进度条&#xff09; 描述 该款自定义组件可作为音频、视频播放的进度条&#xff0c;用于控制音频、视频的播放进度、暂停开始、拖拽进度条拓展性极高。 实现效果 具体效果可以根据自定义内容进行位置调整 项目需求 有播放暂停…...

php中常见的运算符和使用方法

PHP中常见的运算符包括算术运算符、赋值运算符、比较运算符、逻辑运算符、位运算符、字符串运算符、三元条件运算符&#xff08;也称为三目运算符&#xff09;、递增/递减运算符等。以下是这些运算符的简要说明和使用方法&#xff1a; 算术运算符&#xff1a; &#xff1a;加法…...

信息与未来2017真题笔记

T1. 龟兔赛跑 题目描述 兔子又来找乌龟赛跑啦&#xff01;同样的错误兔子不会犯两次&#xff0c;所以兔子提出赛跑的时候&#xff0c;乌龟就觉得这场比赛很不公平。于是兔子进一步放宽了条件&#xff0c;表示他可以在比赛开始以后先睡 t t t 分钟再开始追乌龟。 乌龟这下没…...

前端基础知识-ES6解构赋值(将数组内元素、字符串内字符、对象内属性值快速赋值给其他变量)

前言&#xff1a; 将数组、字符串、对象进行展开&#xff0c;并将展开的数据赋值给指定变量&#xff0c;以达到语法简化的目的&#xff0c;日常开发中可以大大提升我们的效率。 主要语法&#xff1a; 一、[变量1,变量2。。。]目标数组 将数组里面的内容赋给其他变量 场景1…...

【SpringBoot整合系列】SpringBoot整合RabbitMQ-消息可靠性

目录 确保消息的可靠性RabbitMQ 消息发送可靠性分析解决方案开启事务机制发送方确认机制单条消息处理消息批量处理 失败重试自带重试机制业务重试 RabbitMQ 消息消费可靠性如何保证消息在队列RabbitMQ 的消息消费&#xff0c;整体上来说有两种不同的思路&#xff1a;确保消费成…...

Hbase 常用shell操作

目录 1、创建表 1.1、启动HBase Shell 1.2、创建表 1.3、查看表 1.4、删除表 2、插入数据 2.1、put命令 3、查看数据 3.1、get命令 3.2、查询数据中文显示 4、更新数据 4.1、使用put来更新数据 5、删除数据 5.1、delete命令 5.2、删除指定列的数据 5.3、delete…...

数据库被攻击后出现1044 - access denied for user ‘root‘@‘% ‘ to database table

MySQL数据库被攻击后&#xff0c;数据库全部被删除&#xff0c;并且加一个一个勒索的数据&#xff0c;向我索要btc&#xff0c; 出现这个问题就是我的数据库密码太简单了&#xff0c;弱密码&#xff0c;被破解了&#xff0c;并且把我权限也给修改了 导致我操作数据库时&#…...

在哪里打印资料比较便宜

在数字时代&#xff0c;我们常常需要在各种文档、资料之间穿梭&#xff0c;然而&#xff0c;有时候我们需要的并不是数字版&#xff0c;而是纸质版。那么&#xff0c;在哪里打印资料比较便宜呢&#xff1f; 琢贝云打印以其超低的价格&#xff0c;优质的打印服务&#xff0c;赢…...

leetcode 2606.找到最大开销的子字符串

思路&#xff1a;dp 这道题是不是很像最大子数组和那道题呢?从这里我们其实能看出来一类题的蹊跷规律来&#xff1a; 也就是说&#xff0c;在涉及到子字符串&#xff0c;子数组这样的字眼的时候&#xff0c;并且有最值问题&#xff0c;我们可以基本上确定是动态规划&#xf…...

超标量处理器设计:重排序缓存(ROB)

★超标量处理器的很多地方用到了重排序缓存&#xff0c;但是我对它不是很了解&#xff0c;所以我整理一下重排序缓存的知识点。 重排序缓存(ROB)在确保乱序执行的指令能够正确地完成和提交(Commit)&#xff0c;也可以用来寄存器重命名。 ROB是一个先进先出的表&#xff0c;每个…...

nginx常用内置变量

名称说明$arg_name请求中的name参数$args请求中的参数$content_lengthhttp请求信息里的"Content-Length"$content_type请求信息里的"Content-Type"$host请求信息中的"Host"&#xff0c;如果请求中没有Host&#xff0c;则等于设置的服务器名$host…...

MySQL技能树学习——数据库组成

数据库组成&#xff1a; 数据库是一个组织和存储数据的系统&#xff0c;它由多个组件组成&#xff0c;这些组件共同工作以确保数据的安全、可靠和高效的存储和访问。数据库的主要组成部分包括&#xff1a; 数据库管理系统&#xff08;DBMS&#xff09;&#xff1a; 数据库管理系…...

OpenCV入门1:Python基础编程

目录 环境配置 Python基础语法 import 与 from...import If ... Else 语句 While 循环 For 循环 集合数据类型 列表 函数 类和对象 环境配置 详情请参考&#xff1a;Pycharm环境配置完整教程 Python基础语法 import 与 from...import 在 python 用 import 或者 f…...

C++ Builder XE EnumWindowsProc遍历所有窗口的名称

BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam) { // 这里可以添加你的处理逻辑 // 例如&#xff0c;将句柄添加到列表中或者其他操作 // 这里我们仅仅输出到调试窗口 OutputDebugString(L"枚举窗口句柄: "); char windowHandle[10];…...

Qt QInputDialog详解

1.简介 QInputDialog是一个对话框类&#xff0c;用于从用户那里获取一个单一的值。这个值可以是字符串、数字、或者一个列表中的选项。QInputDialog提供了一个方便的方式来快速创建一个输入对话框&#xff0c;无需自己从头开始构建。 QInputDialog支持多种输入类型&#xff1…...

最新盘点!2024年20大好用的项目管理软件(后续持续更新)

项目管理软件&#xff0c;作为一种高效的项目管理工具&#xff0c;正逐渐成为企业运营中不可或缺的一环。它包括任务分配、进度跟踪、团队协作、风险预测等多个方面&#xff0c;为企业提供了全方位的项目管理解决方案。 在如今竞争激烈的市场环境下&#xff0c;企业要想在有限…...

Linux:配置客户端默认autofs服务

Linux&#xff1a;配置客户端autofs服务 安装autofs软件 [rootserver200 ~]# dnf install autofs -y开启并设置开机自启autofs服务 [rootserver200 ~]# systemctl enable --now autofs访问默认autofs挂载机制 当autofs启动后系统默认会在/net目录中访问nfs服务器 [rootser…...

Kotlin版本的Gradle全局配置init.gradle.kts及参考文档

工欲善其事&#xff0c; 必先利其器。 文章目录 init.gradle.ktsGroovy版本的init.gradle其他有用的settings.gradle.ktskotlin 与 compose 版本对应关系agp 与 gradle 版本对应关系gradle下载器 直接在.gradle文件夹下添加文件init.gradle / init.gradle.kt for kotlin dsl. …...