深入理解 Web 容器:从反射扫描到服务器启动的完整实现

1,类对象的获取,注解的实现

2,请求体,响应体和http协议

3,方法的实现和服务器的启动

一,类对象的获取与注解的实现

在web容器中我们不会像平常使用new来实现类的实例化,这里通过Java的反射来获取类对象,通过Class获取构造方法实时创建对象。

下一个问题就是我们怎么知道要创建的对象是什么,这就和注解有关了。注解写在类和方法的前面,作为一个标识,这个标识就作用于识别是否利用反射调用了。下面就是一个注解的实现:

复制代码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface WebAPI {
    String value();
}

可以看见注解中也可以包含参数,除了注解名注解中的参数也是一个重要的识别标准。通过扫描特定目录下的文件,识别注解是否需要进行调用。下面就来展示如何将反射和注解结合在一起:

复制代码
Class<?> clazz=Class.forName(webPath+"."+className);
if(clazz.isAnnotationPresent(WebAPI.class)){
   Object obj = clazz.newInstance();
   Annotation[] annotations = clazz.getAnnotations();
   for (int j = 0; j < annotations.length; j++) {
       Annotation a = annotations[j];
       if (a instanceof WebAPI webAPI) {
       String apiPath = webAPI.value();
       apiList.put(apiPath, obj);//将方法名称和对象存入容器
       System.out.println("加载成功:" + apiPath);
       }
   }
}

先获取对象存入clazz中,利用识别类是否含有特定的注解(也就是识别注解名)再实例化,一个类中会有很多个标签所以先用数组将类的标签存入,接着遍历找到我们需要注解,再获取注解里的属性存入容器中,这个属性通常就是需要调用的方法的路径。

这里的存入容器,这一步也和tomcat中servlet容器的管理类似,实现了路径到实现的映射。

二,请求体,响应体和http协议

Web 容器之所以能和浏览器通信,是因为双方都遵守HTTP/1.1协议。想要获取浏览器发送的请求就需要解析http协议,也就是要使用请求体来作为解析装置:

复制代码
class Request {
    private String method;
    private String path;
    private HashMap<String,String> map=new HashMap<>();
    public Request(BufferedReader br) throws IOException {
        String data = br.readLine();
        String[] datas = data.split(" ");
        this.method=datas[0];
        String[] paths = datas[1].split("\\?");
        this.path = paths[0];
        if(paths.length>1){
            String[] parms = paths[1].split("&");
            for (int i = 0; i < parms.length; i++) {
                String[] parm = parms[i].split("=");
                this.map.put(parm[0],parm[1]);
            }
        }
        String header;
        while ((header = br.readLine()) != null && !header.isEmpty()) {
            System.out.println(header);
        }
    }
    public String get(String key){
        return this.map.get(key);
    }
    public String getMethod(){
        return this.method;
    }
    public String getPath(){
        return this.path;
    }
}

通过拆分字符知道发来的请求,这里所使用的map是用于存储http协议中的参数。响应体则是将对应的字节流写作HTTP/1.1的格式让浏览器能够识别:

复制代码
class Response {
    private BufferedWriter bw;
    public Response(BufferedWriter bw){
        this.bw=bw;
    }
    public void write(String data) throws IOException {
        String head = "HTTP/1.1 200 OK\r\n" +
                "Content-Type: text/plain;charset=utf-8\r\n" +
                "Content-Length: " + data.getBytes().length + "\r\n" +
                "\r\n" + data;
        bw.write(head);
        bw.flush();
    }
}

3,方法的实现和服务器的启动

定义了一个抽象基类。它的核心作用是分发。实现如下:

复制代码
public abstract class Dispatcher {
    public void service(Request req,Response res) throws Exception {
        String method=req.getMethod();//查看网页发送的请求类型
        if(method.equals("GET")) {
            get(req, res);
        }
        if(method.equals("POST")){
            post(req,res);
        }

    }
    public abstract void get(Request req,Response res);
    public abstract void post(Request req,Response res);
}

首先通过服务器获取请求类型(如GET和POST),从而决定调用哪个方法。继承这个抽象类的子类,只需要重写get或post即可处理具体的业务。下面是其中的一个子类:

复制代码
@WebAPI(value = "/login")
public class Login extends Dispatcher {
    @Override
    public void get(Request req, Response res) {
        String name=req.get("name");
        String psw=req.get("password");
        try {
            if (name.equals("admin") && psw.equals("123")) {
                res.write("登录成功");
                System.out.println("登录成功");
            } else {
                res.write("登录失败");
                System.out.println("登录失败");
            }
            System.out.println(name+" "+psw);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    @Override
    public void post(Request req, Response res) {

    }
}

最后则是服务器的启动:

复制代码
public class Webserver {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket=new ServerSocket(8080);
        Servlet servlet=new Servlet();
        while(true){
            Socket socket=serverSocket.accept();
            BufferedWriter bw=new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
            BufferedReader reader=new BufferedReader(new InputStreamReader(socket.getInputStream()));
            Request req=new Request(reader);
            Response res=new Response(bw);
            String path=req.getPath();
            if(servlet.getServlet(path)!=null){
                Class<?> clazz=servlet.getServlet(path).getClass();
                try{
                    Method method = clazz.getMethod("service", Request.class, Response.class);
                    method.invoke(servlet.getServlet(path), req, res);
                }catch (Exception e){
                    throw new RuntimeException(e);
                }
            }
            socket.close();
        }
    }
}

先和浏览器建立连接:通过new ServerSocket(8080)开启服务器,不断通过accept() 阻塞等待浏览器的访问。当请求进来时,服务器实时创建Request 和 Response 实例。根据解析出的 path,从 Servlet 容器(即我们第一步存入的 apiList)中取出对应的对象。服务器并不提前知道你会调用哪个方法,它通过clazz.getMethod("service", ...) 地找到 service 方法,并使用 method.invoke() 实现方法的调用。

相关推荐
韦禾水1 小时前
记录一次项目部署到tomcat的异常
java·tomcat
曦月合一1 小时前
树莓派安装jdk、tomcat、vnc、谷歌浏览器开机自启等环境配置
java·tomcat·树莓派
excel1 小时前
如何解决 Nuxt DevTools 中关于 unstorage 包的报错
前端
Rust研习社1 小时前
使用 Axum 构建高性能异步 Web 服务
开发语言·前端·网络·后端·http·rust
此剑之势丶愈斩愈烈1 小时前
openssl 自建证书
java
面汤放盐1 小时前
何时使用以及何时不应使用微服务:没有银弹
java·运维·云计算
0xDevNull2 小时前
Spring Boot 自动装配:从原理到实践
java·spring boot·后端
C澒2 小时前
AI 生码 - API2Code:接口智能匹配与 API 自动化生码全链路设计
前端·低代码·ai编程
浔川python社2 小时前
HTML头部元信息避坑指南技术文章大纲
前端·html
IT_陈寒2 小时前
SpringBoot配置加载顺序把我坑惨了
前端·人工智能·后端