目录
工作原理
整体项目结构
自定义注解
创建servlet类
创建启动类
线程池配置
测试阶段
工作原理
首先看流程图,搞清楚tomcat的工作原理
工作原理如下:
-
Tomcat使用一个叫作Catalina的核心组件来处理HTTP请求和响应。Catalina包含了一个HTTP连接器(Connector),负责监听和接收客户端请求。
-
当接收到一个HTTP请求时,Tomcat会解析请求头和请求体,提取出请求的URL、请求方法、请求参数等信息。
-
Tomcat中的Servlet容器负责管理Servlet的生命周期和处理Servlet请求。Servlet是以Java类的形式编写的服务器端程序,用于处理动态内容。
-
在Tomcat中,Servlet类需要使用注解(如@WebServlet)进行标记,以指定Servlet的URL映射和其他配置信息。
-
当Tomcat启动时,它会扫描应用程序中包含的所有类,找到带有Servlet注解的类,并将它们的URL映射和全类名存储在一个映射表中。
-
当接收到请求时,Tomcat会根据请求的URL在映射表中查找对应的Servlet类。利用反射机制,Tomcat可以实例化Servlet类并调用其相应的方法来处理请求。
-
处理请求的Servlet可以通过请求对象(HttpServletRequest)获取请求的URL、参数、请求头等信息,并通过响应对象(HttpServletResponse)来生成响应内容和设置响应头。
-
Tomcat使用Socket与浏览器进行通信,通过监听在8080端口(默认)来接收客户端的HTTP请求。
人话:它是利用注解对servlet类进行标记,之后将它标记中的请求地址和全类名存入map中,利用反射拿到类信息,调用类的方法,利用socket和浏览器交互,监听8080端口,之后将请求信息拿到,处理其中的信息拿到请求url和参数信息
整体项目结构
自定义注解
需要用到java中的元注解实现,如果不清楚的小伙伴可以参考这一篇:
java 元注解||自定义注解的使用_ADRU的博客-CSDN博客
import java.lang.annotation.*;
/**
* @author 小如
*
* @date 2023/08/15
*/
/** 请求映射该注解可以应用于类、接口(包括注解类型)、枚举*/
@Target(ElementType.TYPE)
/**该注解标记的元素可以被Javadoc 或类似的工具文档化*/
@Documented
/**该注解的生命周期,由JVM 加载,包含在类文件中,在运行时可以被获取到*/
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMap {
String path() default "";
}
创建servlet类
1.创建接收参数信息类
import java.util.HashMap;
/**
* http请求参数仿写
*
* @author 小如
* @date 2023/08/15
*/
public class HttpRequestDemo {
public HashMap<String, String> map = new HashMap<>();
public String getParameter(String key) {
return map.get(key);
}
}
import java.io.OutputStream;
public class HttpResponseDemo {
public OutputStream outputStream;
public static final String responsebody="HTTP/1.1 200+\r\n"+"Content-Type:text/html+\r\n"
+"\r\n";
public HttpResponseDemo(OutputStream outputStream){
this.outputStream=outputStream;
}
}
2.自定义两个servlet类(模拟请求处理)
import Tools.HttpRequestDemo;
import Tools.HttpResponseDemo;
import annotation.RequestMap;
import java.io.IOException;
/**
* 请求servlet
*
* @author 小如
* @date 2023/08/15
*/
@RequestMap(path = "hello")
public class RequestServlet {
public void doGet(HttpRequestDemo request, HttpResponseDemo response) throws IOException {
System.out.println("hello GET响应:");
System.out.println("a="+request.getParameter("a"));
String resp= HttpResponseDemo.responsebody+"<!DOCTYPE html>\n" +
"<html>\n" +
"<head>\n" +
" <meta charset=\"utf-8\" />\n" +
"<link rel=\"icon\" href=\"data:image/ico;base64,aWNv\">\n"+
"</head>\n" +
"<body>\n" +
" \n" +
" <form name=\"my_form\" method=\"POST\">\n" +
" <input type=\"button\" value=\"按下\" onclick=\"alert('你按下了按钮')\">\n" +
" </form>\n" +
" \n" +
"</body>\n" +
"</html>";
response.outputStream.write(resp.getBytes());
response.outputStream.flush();
response.outputStream.close();
}
public void doPost(HttpRequestDemo request,HttpResponseDemo response) throws IOException {
System.out.println("\n响应的http如下:");
String resp= HttpResponseDemo.responsebody+
"{\"sorry\":\"we only respond to method GET now\"},\r\n"+
"";
System.out.println(resp);
response.outputStream.write(resp.getBytes());
response.outputStream.flush();
response.outputStream.close();
}
}
import Tools.HttpRequestDemo;
import Tools.HttpResponseDemo;
import annotation.RequestMap;
import java.io.IOException;
/**
* 请求servlet
*
* @author 小如
* @date 2023/08/15
*/
@RequestMap(path = "nihao")
public class RequestServlet2 {
public void doGet(HttpRequestDemo request, HttpResponseDemo response) throws IOException {
System.out.println("nihao GET响应:");
System.out.println("a="+request.getParameter("a"));
String resp= HttpResponseDemo.responsebody+
"{\"success\":\"200\"},\r\n"+
"";
response.outputStream.write(resp.getBytes());
response.outputStream.flush();
response.outputStream.close();
}
public void doPost(HttpRequestDemo request,HttpResponseDemo response) throws IOException {
System.out.println("\n响应的http如下:");
String resp= HttpResponseDemo.responsebody+
"{\"sorry\":\"we only respond to method GET now\"},\r\n"+
"";
System.out.println(resp);
response.outputStream.write(resp.getBytes());
response.outputStream.flush();
response.outputStream.close();
}
}
3.定义一个异常servlet,当请求异常时,打到这个servlet
import Tools.HttpRequestDemo;
import Tools.HttpResponseDemo;
import annotation.RequestMap;
import java.io.IOException;
/**
* 未找到返回的页面
*
* @author 小如
* @date 2023/08/17
*/
@RequestMap(path = "404")
public class Html404 {
public void err(HttpRequestDemo request, HttpResponseDemo response) throws IOException {
String resp= HttpResponseDemo.responsebody+"<!DOCTYPE html>\n" +
"<html>\n" +
"<head>\n" +
"<link rel=\"icon\" href=\"data:image/ico;base64,aWNv\">\n"+
" <meta charset=\"utf-8\" />\n" +
" <title>404 - Not Found</title>\n" +
"</head>\n" +
"<body>\n" +
" <h1>404 - Not Found</h1>\n" +
" <p>Sorry, the page you are looking for does not exist.</p>\n" +
"</body>\n" +
"</html>";
response.outputStream.write(resp.getBytes());
response.outputStream.flush();
response.outputStream.close();
}
}
创建启动类
1.启动类框架
public class start {
private static ArrayList<String> arr = new ArrayList<>();
public static HashMap<String, Class> map = new HashMap<>();
2.在启动类中定义方法遍历文件,找到所有的java文件
/**
* 寻找带有RequestMap的注解,拿到相应的数据
*
* @param file 文件
*/
private static void func(File file) {
File[] fs = file.listFiles();
for (File f : fs) {
if (f.isDirectory()){ //若是目录,则递归打印该目录下的文件
func(f);
}
if (f.isFile()) { //若是文件,直接打印
String filepath = f.toString();
filepath = filepath.split("src")[1];
filepath = filepath.substring(1,filepath.length());
if( filepath.endsWith(".java")) {
arr.add(filepath.replace("\\", ".").replace(".java", "").replace("main.",""));
}
}
}
}
3.在找到的java文件中筛选出含有自定义注解的类,并将path和全类名存入map中
/**
* 找到servlet,将信息放入map
*
* @throws ClassNotFoundException 类没有发现异常
*/
public static void choseServlet() throws ClassNotFoundException {
for(int i = 0; i < arr.size(); i++) {
String path = arr.get(i);
Class<?> servletClass = Class.forName(path);
//System.out.println(servletClass);
//判断Class对象上是否有RequestMap的注解
if (servletClass.isAnnotationPresent(RequestMap.class)) {
//System.out.println("找到了一个Servlet,路径是:" + path);
//获取SystemConfig注解
RequestMap config = servletClass.getAnnotation(RequestMap.class);
//System.out.println("它的请求路径是:" + config.path() );
map.put( config.path(), servletClass);
}
}
}
以上扫描文件的操作应该在项目启动时就开始处理,当我们发送请求的时候可以直接调用。因此我们将以上两个方法调用放入static块中
static {
String inputPath = "F:\\javaDemo\\simple-tomcat2\\src\\servlet"; //要遍历的路径,改成自己的
File file = new File(inputPath); //获取其file对象
func(file);//遍历指定目录下的所有子文件以及子目录下的文件名字
try {
choseServlet();
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}// 根据注解筛选出servlet并存储到hashmap中
}
4.socket监听8080端口
//注册端口
InetAddress localHost = InetAddress.getLocalHost();
System.out.println("localhost:" + localHost);
ServerSocket serverSocket = new ServerSocket(8080, 10, localHost);
startUp(serverSocket);
5.建立连接,解析http请求
private static void startUp(ServerSocket serverSocket) throws IOException, InterruptedException, InvocationTargetException, NoSuchMethodException, IllegalAccessException, InstantiationException {
ExecutorService executorService = new ThreadPoolConfigPlus().executorService();
System.out.println("等待建立连接");
for(int i =0;i<Max_Request_Count;i++){
Socket server = serverSocket.accept();
while (true) {
// 尝试加锁
try {
synchronized (server) {
// 获取到锁
HttpAcceptThreadPlus httpAcceptThreadPlus = new HttpAcceptThreadPlus(server, executorService,new HttpAcceptThreadPlus.OnFinishListener() {
@Override
public void onFinish(List<String> strings) throws IOException, InterruptedException, InvocationTargetException, NoSuchMethodException, IllegalAccessException, InstantiationException {
//处理请求
if(!strings.get(0).equals("GET /favicon.ico HTTP/1.1")){
requestHttp(server,strings.get(0));
}
}
});
httpAcceptThreadPlus.start();
break;
}
}catch (Exception e){
// 加锁失败,线程睡眠两秒
Thread.sleep(2000);
}
}
}
}
6.处理http请求,判断是GET还是POST请求,调用相应的servlet方法
private static void requestHttp(Socket socket,String http) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException, IOException, InterruptedException {
//获取请求方式
String requestStyle=http.split(" ")[0];
if(requestStyle.equals("GET")){
String httpPathAndParameter=http.split(" ")[1];
String httpPath;
//创建HttpRequest对象
HttpRequestDemo httpRequestDemo=new HttpRequestDemo();
if(httpPathAndParameter.indexOf("?")!=-1){
httpPath=httpPathAndParameter.substring(1);
httpPath=httpPath.split("\\?")[0];
String parameterString=httpPathAndParameter.split("\\?")[1];
String[] parameters=parameterString.split("&");
for (int i=0;i<parameters.length;i++){
httpRequestDemo.map.put(parameters[i].split("=")[0],parameters[i].split("=")[1]);
}
}else{
httpPath=httpPathAndParameter.substring(1);
}
//创建HttpResponse对象
OutputStream outputStream=socket.getOutputStream();
HttpResponseDemo httpResponseDemo=new HttpResponseDemo(outputStream);
//反射调用doGet
if(map.containsKey(httpPath)){
Class<?> servletClass=map.get(httpPath);
Method method=servletClass.getMethod("doGet",HttpRequestDemo.class,HttpResponseDemo.class);
method.invoke(servletClass.newInstance(),httpRequestDemo,httpResponseDemo);
}else {
httpPath="404";
Class<?> servletClass=map.get(httpPath);
Method method=servletClass.getMethod("err",HttpRequestDemo.class,HttpResponseDemo.class);
method.invoke(servletClass.newInstance(),httpRequestDemo,httpResponseDemo);
throw new RuntimeException("无效的请求路径!");
}
}else{
String httpPath=http.split(" ")[1];
httpPath=httpPath.substring(1);
HttpRequestDemo httpRequestDemo=new HttpRequestDemo();
OutputStream outputStream=socket.getOutputStream();
HttpResponseDemo httpResponseDemo=new HttpResponseDemo(outputStream);
Class<?> servletClass=map.get(httpPath);
Method method=servletClass.getMethod("doPost",HttpRequestDemo.class,HttpResponseDemo.class);
method.invoke(servletClass.newInstance(),httpRequestDemo,httpResponseDemo);
}
}
完整的启动类如下:
//省略n个包
public class start {
private static ArrayList<String> arr = new ArrayList<>();
public static HashMap<String, Class> map = new HashMap<>();
public static void main(String[] args) throws Exception {
//启动Socket ---> 获取字符串协议数据(HTTP协议 --> requestHttp() )
try {
//注册端口
InetAddress localHost = InetAddress.getLocalHost();
System.out.println("localhost:" + localHost);
ServerSocket serverSocket = new ServerSocket(8080, 10, localHost);
startUp(serverSocket);
} catch (IOException e) {
e.printStackTrace();
}
}
private static void startUp(ServerSocket serverSocket) throws IOException, InterruptedException, InvocationTargetException, NoSuchMethodException, IllegalAccessException, InstantiationException {
ExecutorService executorService = new ThreadPoolConfigPlus().executorService();
System.out.println("等待建立连接");
for(int i =0;i<Max_Request_Count;i++){
Socket server = serverSocket.accept();
while (true) {
// 尝试加锁
try {
synchronized (server) {
// 获取到锁
HttpAcceptThreadPlus httpAcceptThreadPlus = new HttpAcceptThreadPlus(server, executorService,new HttpAcceptThreadPlus.OnFinishListener() {
@Override
public void onFinish(List<String> strings) throws IOException, InterruptedException, InvocationTargetException, NoSuchMethodException, IllegalAccessException, InstantiationException {
//处理请求
if(!strings.get(0).equals("GET /favicon.ico HTTP/1.1")){
requestHttp(server,strings.get(0));
}
}
});
httpAcceptThreadPlus.start();
break;
}
}catch (Exception e){
// 加锁失败,线程睡眠两秒
Thread.sleep(2000);
}
}
}
}
private static void requestHttp(Socket socket,String http) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException, IOException, InterruptedException {
//获取请求方式
String requestStyle=http.split(" ")[0];
if(requestStyle.equals("GET")){
String httpPathAndParameter=http.split(" ")[1];
String httpPath;
//创建HttpRequest对象
HttpRequestDemo httpRequestDemo=new HttpRequestDemo();
if(httpPathAndParameter.indexOf("?")!=-1){
httpPath=httpPathAndParameter.substring(1);
httpPath=httpPath.split("\\?")[0];
String parameterString=httpPathAndParameter.split("\\?")[1];
String[] parameters=parameterString.split("&");
for (int i=0;i<parameters.length;i++){
httpRequestDemo.map.put(parameters[i].split("=")[0],parameters[i].split("=")[1]);
}
}else{
httpPath=httpPathAndParameter.substring(1);
}
//创建HttpResponse对象
OutputStream outputStream=socket.getOutputStream();
HttpResponseDemo httpResponseDemo=new HttpResponseDemo(outputStream);
//反射调用doGet
if(map.containsKey(httpPath)){
Class<?> servletClass=map.get(httpPath);
Method method=servletClass.getMethod("doGet",HttpRequestDemo.class,HttpResponseDemo.class);
method.invoke(servletClass.newInstance(),httpRequestDemo,httpResponseDemo);
}else {
httpPath="404";
Class<?> servletClass=map.get(httpPath);
Method method=servletClass.getMethod("err",HttpRequestDemo.class,HttpResponseDemo.class);
method.invoke(servletClass.newInstance(),httpRequestDemo,httpResponseDemo);
throw new RuntimeException("无效的请求路径!");
}
}else{
String httpPath=http.split(" ")[1];
httpPath=httpPath.substring(1);
HttpRequestDemo httpRequestDemo=new HttpRequestDemo();
OutputStream outputStream=socket.getOutputStream();
HttpResponseDemo httpResponseDemo=new HttpResponseDemo(outputStream);
Class<?> servletClass=map.get(httpPath);
Method method=servletClass.getMethod("doPost",HttpRequestDemo.class,HttpResponseDemo.class);
method.invoke(servletClass.newInstance(),httpRequestDemo,httpResponseDemo);
}
}
/**
* 找到servlet,将信息放入map
*
* @throws ClassNotFoundException 类没有发现异常
*/
public static void choseServlet() throws ClassNotFoundException {
for(int i = 0; i < arr.size(); i++) {
String path = arr.get(i);
Class<?> servletClass = Class.forName(path);
//System.out.println(servletClass);
//判断Class对象上是否有RequestMap的注解
if (servletClass.isAnnotationPresent(RequestMap.class)) {
//System.out.println("找到了一个Servlet,路径是:" + path);
//获取SystemConfig注解
RequestMap config = servletClass.getAnnotation(RequestMap.class);
//System.out.println("它的请求路径是:" + config.path() );
map.put( config.path(), servletClass);
}
}
}
/**
* 寻找带有RequestMap的注解,拿到相应的数据
*
* @param file 文件
*/
private static void func(File file) {
File[] fs = file.listFiles();
for (File f : fs) {
if (f.isDirectory()){ //若是目录,则递归打印该目录下的文件
func(f);
}
if (f.isFile()) { //若是文件,直接打印
String filepath = f.toString();
filepath = filepath.split("src")[1];
filepath = filepath.substring(1,filepath.length());
if( filepath.endsWith(".java")) {
arr.add(filepath.replace("\\", ".").replace(".java", "").replace("main.",""));
}
}
}
}
static {
String inputPath = "F:\\javaDemo\\simple-tomcat2\\src\\servlet"; //要遍历的路径,改成自己的
File file = new File(inputPath); //获取其file对象
func(file);//遍历指定目录下的所有子文件以及子目录下的文件名字
try {
choseServlet();
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}// 根据注解筛选出servlet并存储到hashmap中
}
}
线程池配置
socket有个弊端,当我们建立一次连接处理完http请求之后,连接断开就会导致项目停止运行,因此我们加入线程池配置,开多个线程去接收建立连接
import java.util.concurrent.*;
import static Tools.config.*;
/**
* 自定义的线程池配置
*
* @author 小如
* @date 2023/08/17
*/
public class ThreadPoolConfigPlus {
/**
* 核心线程数量
*/
private int corePoolSize=Core_PoolSize;
/**
* 最大线程数量
*/
private int maxPoolSize=Max_PoolSize;
/**
* 队列容量
*/
private int queueCapacity=Queue_Capacity;
public ExecutorService executorService(){
//线程工厂
ThreadFactory factory=new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread=new Thread(r);
return thread;
}
};
//手动创建线程池
ThreadPoolExecutor threadPoolExecutor=new ThreadPoolExecutor(corePoolSize, maxPoolSize, 10, TimeUnit.SECONDS,
new LinkedBlockingDeque<>(queueCapacity), factory,
new ThreadPoolExecutor.CallerRunsPolicy()
);
return threadPoolExecutor;
}
}
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.InvocationTargetException;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
/**
* @author 小如
*/
public class HttpAcceptThreadPlus {
private Socket socket;
private ExecutorService executorService;
private OnFinishListener onFinishListener; // 回调接口
public interface OnFinishListener {
void onFinish(List<String> strings) throws IOException, InterruptedException, InvocationTargetException, NoSuchMethodException, IllegalAccessException, InstantiationException;
}
public HttpAcceptThreadPlus(Socket server, ExecutorService executorService, OnFinishListener onFinishListener) {
this.socket = server;
this.executorService = executorService;
this.onFinishListener = onFinishListener;
}
public void start(){
executorService.execute(new Runnable() {
@Override
public void run() {
try {
String s = null;
ArrayList<String> strings = new ArrayList<>();
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
while ((s = reader.readLine()).length() != 0) {
//每次循环接收一行的Http数据
try {
strings.add(s);
} catch (Exception e) {
System.out.println("接收Http进程结束");
break;
}
}
if(!strings.get(0).equals("GET /favicon.ico HTTP/1.1")){
System.out.println(Thread.currentThread().getName()+"连接成功");
System.out.println(Thread.currentThread().getName()+"接收Http进程结束,获取的请求为:"+strings.get(0));
}
onFinishListener.onFinish(strings);
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
}
线程池的配置文件
public class config {
/**
* 核心线程数量
*/
public static final int Core_PoolSize = 8;
/**
* 最大线程数量
*/
public static final int Max_PoolSize = 16;
/**
* 队列容量
*/
public static final int Queue_Capacity =2000;
public static final int Max_Request_Count = 2000;
}
测试阶段
至此,项目已经完成,启动start,开始测试
浏览器输入请求
测试多次请求,观察后台
发送错误的请求:
后台提示,抛出异常
我已将项目开源至Github
https://github.com/adpanru/easy-tomcat