【吃透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() {
    @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 种阻塞队列的实现:

  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连接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来不断地进行阻塞监听

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连接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

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中

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);
    }
}

访问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、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

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、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

public class AbstractHttpServletRequest implements HttpServletRequest {
    @Override
    public String getAuthType() {
        return null;
    }

    @Override
    public 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 {
    @Override
    public void addCookie(Cookie cookie) {

    }

    @Override
    public 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);
        }
    }
    @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对应一个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;
    @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中

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方法即可

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中设置请求头

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\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)
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下所有的文件,递归搜索

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

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/613328.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

JAVA使用Apache POI动态导出Word文档

文章目录 一、文章背景二、实现步骤2.1 需要的依赖2.2 创建模板2.3 书写java类2.3.1 模板目录2.3.2 Controller类2.3.2 工具类 2.4 测试2.4.1 浏览器请求接口2.4.2 下载word 三、注意事项四、其他导出word实现方式 一、文章背景 基于Freemarker模版动态生成并导出word文档存在弊…

vue----- watch监听$attrs 的注意事项

目录 前言 原因分析 解决方案 总结 前言 在 Vue 开发过程中&#xff0c;如遇到祖先组件需要传值到孙子组件时&#xff0c;需要在儿子组件接收 props &#xff0c;然后再传递给孙子组件&#xff0c;通过使用 v-bind"$attrs" 则会带来极大的便利&#xff0c;但同时…

酷开科技丨母亲节,别让有爱瞬间轻易溜走

在母亲节这个充满温情的节日里&#xff0c;酷开科技以“健健康康才能长长久久”为主题&#xff0c;推出了一系列关怀活动&#xff0c;旨在通过科技的力量&#xff0c;提升母亲们的身体素质和生活质量&#xff0c;同时也为儿女们提供了表达孝心和关怀的机会。 酷开系统特别上线…

【Vue】Vue的核心

目录 计算属性-computed插值语法实现methods实现计算属性实现使用使用总结&#xff1a; 监视属性-watch监视的两种写法&#xff1a;深度监视备注&#xff1a; computed和watch之间的区别 绑定样式class样式绑定字符串写法数组写法对象写法 style样式绑定对象式1对象式2数组式 条…

5/11后面部分:+顺序排序+元素交换+计算每门课程的各种成绩+存放规律的数据 注意:一味的复制肯定要出问题,第2个的最后一部分有修改,注意观察

目录 第一个已经输出过一次&#xff1a; 第二个: 编程实现&#xff1a;程序功能是用起泡法对数组中n个元素按从大到小的顺序进行排序。 ​编辑的确出现了一些问题哦&#xff1a; ​编辑目前是可以运行&#xff0c;但AI不给我们通过&#xff1a; 最后还是我的代码获胜&#x…

网络安全专业岗位详解+自学学习路线图

很多网安专业同学一到毕业就开始迷茫&#xff0c;不知道自己能去做哪些行业&#xff1f;其实网络安全岗位还是蛮多的&#xff0c;下面我会介绍一些网络安全岗位&#xff0c;大家可以根据自身能力与喜好决定放哪个方向发展。 渗透测试/Web安全工程师 主要是模拟黑客攻击&#…

vue3 antd-vue 超简单方式实现a-table跨页勾选

一、效果如下&#xff1a; 第一页勾选了2&#xff0c; 3&#xff0c; 4 翻到第三页勾选24&#xff0c; 25 回显&#xff0c;如比返回第一页的时候触发分页改变&#xff0c; 在映射中的第一页的数据给到a-table绑定的state.selectedRowKeys即可&#xff0c;如下方法 二、勾选思路…

初识多线程

1. 前置知识——进程 在学习多线程前需要了解操作系统中的基本知识&#xff0c;这里简单回顾下。 1.1 进程控制块 一个进程对应着一个进程控制块PCB&#xff0c;PCB是一个用于管理和维护进程信息的数据结构&#xff0c;这个数据结构中大致包含下面内容&#xff08;并不完整&…

头歌实践教学平台:CG1-v1.0-点和直线的绘制

第1关&#xff1a;OpenGL点的绘制 一. 任务描述 根据下面要求&#xff0c;在右侧修改代码&#xff0c;绘制出预期输出的图片。平台会对你编写的代码进行测试。 1.本关任务 熟悉编程环境&#xff1b; 了解光栅图形显示器的特点&#xff1b; 了解计算机绘图的特点&#xff1b…

Redis是单线程吗?为什么6.0之后引入了多线程?

Redis是单线程吗&#xff1f;为什么6.0之后引入了多线程&#xff1f; Redis 是单线程吗&#xff1f;Redis 单线程模式是怎样的&#xff1f;Redis 采用单线程为什么还这么快&#xff1f;Redis 6.0 之前为什么使用单线程&#xff1f;Redis 6.0 之后为什么引入了多线程&#xff1f…

geotrust dv通配符证书800

Geotrust是成立时间较久的正规CA认证机构&#xff0c;在过去的几十年间颁发了无数的SSL证书&#xff0c;这些SSL证书被各个开发者使用&#xff0c;受到大多数浏览器的信任。而Geotrust旗下的DV通配符证书因其广泛的应用范围受到了用户的青睐。今天就随SSL盾小编了解Geotrust旗下…

C语言(指针)2

Hi~&#xff01;这里是奋斗的小羊&#xff0c;很荣幸各位能阅读我的文章&#xff0c;诚请评论指点&#xff0c;关注收藏&#xff0c;欢迎欢迎~~ &#x1f4a5;个人主页&#xff1a;小羊在奋斗 &#x1f4a5;所属专栏&#xff1a;C语言 本系列文章为个人学习笔记&#x…

XWiki 服务没有正确部署在tomcat中,如何尝试手动重新部署?

1. 停止 Tomcat 服务 首先&#xff0c;您需要停止正在运行的 Tomcat 服务器&#xff0c;以确保在操作文件时不会发生冲突或数据损坏&#xff1a; sudo systemctl stop tomcat2. 清空 webapps 下的 xwiki 目录和 work 目录中相关的缓存 删除 webapps 下的 xwiki 目录和 work …

游戏行业被攻击的原因、攻击种类及合适的服务器

很多游戏刚上线没多久就频繁遭到同行恶意攻击。在相关数据报告中&#xff0c;2023年上半年遭受DDoS攻击的行业中&#xff0c;游戏行业占到40%&#xff0c;而且攻击方式、攻击频率、攻击峰值呈明显上升趋势。很多充满创意的游戏开发公司刚才开发上线一个很有特色的产品&#xff…

Electron学习笔记(三)

文章目录 相关笔记笔记说明 五、界面1、获取 webContents 实例&#xff08;1&#xff09;通过窗口对象的 webContent 属性获取 webContent 实例&#xff1a;&#xff08;2&#xff09;获取当前激活窗口的 webContents 实例&#xff1a;&#xff08;3&#xff09;在渲染进程中获…

IDEA 好用的插件

图标插件&#xff1a;Atom Material Icons 此插件的作用就是更好的显示各种文件的类别&#xff0c;使之一目了然 汉化包 Chinese ​(Simplified)​ Language Pack / 中文语言包 作用就是 汉化 AI编码助手 GitHub Copilot AI编码助手&#xff1a;提示代码很好用 缺点&#xff1a…

H5 云商城 file.php 文件上传致RCE漏洞复现

0x01 产品简介 H5 云商城是一个基于 H5 技术的电子商务平台,旨在为用户提供方便快捷的在线购物体验。多平台适配:H5 云商城采用 H5 技术开发,具有良好的跨平台适配性。无论是在电脑、手机还是平板等设备上,用户都可以通过网页浏览器访问和使用云商城,无需安装额外的应用程…

CH340 RTS DTR引脚编程驱动OLED

运行结果 硬件连接&#xff08;在连接线上串接300R电阻&#xff09; 下面是c#实现代码 using System; using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks;using uint8 System.Byt…

2024年数维杯B题完整代码和思路论文讲解与分析

2024数维杯数学建模完整代码和成品论文已更新&#xff0c;获取↓↓↓↓↓ https://www.yuque.com/u42168770/qv6z0d/bgic2nbxs2h41pvt?singleDoc# 2024数维杯数学建模B题45页论文和代码已完成&#xff0c;代码为全部问题的代码 论文包括摘要、问题重述、问题分析、模型假设、…

QT--2

Qt界面设计 #include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent) {//窗口相关设置this->resize(680,520);this->setFixedSize(680,520);this->setWindowTitle("Tim");this->setWindowFla…