深入理解 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() 实现方法的调用。

相关推荐
拆房老料2 小时前
从 Euro-Office 说起:Office 不是编辑器,是一套复杂系统工程
前端·编辑器·开源软件·开源协议
就叫飞六吧2 小时前
jsplumb 审批流前端库案例
前端
踩着两条虫2 小时前
从“降门槛”到“提效率”:VTJ.PRO与百度秒哒的差异化路径分析
前端·vue.js·ai编程
一名优秀的码农2 小时前
vulhub系列-59-Web-Machine-N72(超详细)
前端·安全·web安全·网络安全·网络攻击模型·安全威胁分析
色空大师2 小时前
网站搭建实操(十)前端搭建
前端·webpack·vue·网站·论坛
ApjRvH3vg2 小时前
什么是Skills
前端
꧁꫞꯭零꯭点꯭꫞꧂2 小时前
JavaScript模块化规范
开发语言·前端·javascript
三万棵雪松2 小时前
【Linux 物联网网关主控系统-Web部分(四)】
linux·前端·物联网·嵌入式linux
希望永不加班2 小时前
SpringBoot 整合 Elasticsearch 实现全文检索
java·spring boot·后端·elasticsearch·全文检索