深入理解 Web 容器:从反射扫描到服务器启动的完整实现
1类对象的获取注解的实现2请求体响应体和http协议3方法的实现和服务器的启动一类对象的获取与注解的实现在web容器中我们不会像平常使用new来实现类的实例化这里通过Java的反射来获取类对象通过Class获取构造方法实时创建对象。下一个问题就是我们怎么知道要创建的对象是什么这就和注解有关了。注解写在类和方法的前面作为一个标识这个标识就作用于识别是否利用反射调用了。下面就是一个注解的实现Target(ElementType.TYPE) Retention(RetentionPolicy.RUNTIME) public interface WebAPI { String value(); }可以看见注解中也可以包含参数除了注解名注解中的参数也是一个重要的识别标准。通过扫描特定目录下的文件识别注解是否需要进行调用。下面就来展示如何将反射和注解结合在一起Class? clazzClass.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 HashMapString,String mapnew HashMap(); public Request(BufferedReader br) throws IOException { String data br.readLine(); String[] datas data.split( ); this.methoddatas[0]; String[] paths datas[1].split(\\?); this.path paths[0]; if(paths.length1){ 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.bwbw; } public void write(String data) throws IOException { String head HTTP/1.1 200 OK\r\n Content-Type: text/plain;charsetutf-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 methodreq.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 namereq.get(name); String pswreq.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 serverSocketnew ServerSocket(8080); Servlet servletnew Servlet(); while(true){ Socket socketserverSocket.accept(); BufferedWriter bwnew BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); BufferedReader readernew BufferedReader(new InputStreamReader(socket.getInputStream())); Request reqnew Request(reader); Response resnew Response(bw); String pathreq.getPath(); if(servlet.getServlet(path)!null){ Class? clazzservlet.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() 实现方法的调用。