一个简略的servlet容器代码规划

Servlet算是Java Web开发央求链路调用栈中底层的一个技能,当客户端发起一个央求后,抵达服务器内部,就会先进入Servlet(这儿不评论更底层的链路),SpringMVC的央求分发中心也是一个Servlet,名叫DispatcherServlet,一个央求首要会进入到这个Servlet,然后在通过SpringMVC的机制去分发到对应的Controller下。

但是再往上一层说,普通的开发人员或许不会关心Servlet是怎样被调用的,我们只需写一个@WebServlet注解在Servlet的类上,运转后,客户端的央求就会自动进入到相应的Servlet中,而做这些事的叫Servlet容器,Servlet容器必定是一个Web服务器,但Web服务器反过来可不必定是Servlet容器哦。

而了解一个Servlet容器的完结有助于更好的了解JavaWeb开发。

Github地址

项目终究的完结在Github上可以检查到

github.com/houxinlin/j…

容器的完结

在JavaWeb的开发世界,有很多都要遵循规范,JDBC也是,Servlet容器也是,Java很多不去做完结,只做接口,详细的完结留给各大厂商去做,而Servlet容器其中一个完结就是Tomcat。

Tomcat的完结仍是很杂乱的,这儿也不做研讨,我们只搞清楚一个小型的Servlet容器完结的步骤即可。

我们起一个容器名,叫JerryCat吧,他的完结功用只需一个,将央求交给对应的Servlet,并将其处理效果回来给客户端,因为这才是中心,而完结他的详细步骤如下。

  1. 解压war文件
  2. 收集Servlet信息
  3. 发起web服务器
  4. 央求映射 & 回来效果

解压war文件

当你在Tomcat的webapps目录下放入一个war文件,发起tomcat后,tomcat会自动把这个war文件解压了,后续全部的操作将会针对这个解压后的目录,而解压一个war文件很简略,代码如下。


public static void unzipWar(String warFilePath, String outputFolder) throws IOException {
    byte[] buffer = new byte[1024];
    try (ZipInputStream zis = new ZipInputStream(Files.newInputStream(Paths.get(warFilePath)))) {
        ZipEntry zipEntry;
        while ((zipEntry = zis.getNextEntry()) != null) {
            String entryName = zipEntry.getName();
            File newFile = new File(outputFolder + File.separator + entryName);
            if (zipEntry.isDirectory()) {
                newFile.mkdirs();
            } else {
                new File(newFile.getParent()).mkdirs();
                try (FileOutputStream fos = new FileOutputStream(newFile)) {
                    int len;
                    while ((len = zis.read(buffer)) > 0) {
                        fos.write(buffer, 0, len);
                    }
                }
            }
            zis.closeEntry();
        }
    }
}

收集Servlet信息

这一步是一个中心,因为Servlet容器必定要知道一个war项目中全部的Servlet信息,也就是要知道开发人员界说的央求途径和详细Servlet的映射联系,当央求进来的时分,才干根据这个映射联系调用到对应的Servlet下。

在Servlet 3.0规范曾经,全部的映射联系需求在web.xml中去配备,比如下面这样,这个配备用来奉告容器将/hello的央求映射到com.example.HelloServlet下,容器只需求读取一个配备即可。

<servlet>
    <servlet-name>HelloServlet</servlet-name>
    <servlet-class>com.example.HelloServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>HelloServlet</servlet-name>
    <url-pattern>/hello</url-pattern>
</servlet-mapping>

但是自从规范3.0开端,添加了@WebServlet等注解,如下,这也是奉告容器,这个类的央求途径是/hello

@WebServlet("/hello")
public class HelloServlet extends HttpServlet {}

那么容器的完结就会添加负担,因为要遍历全部的class,找出标有@WebServlet的类,并做收集,那问题是怎样找到这些符合的类呢? 首要不能通过反射,因为有两个问题。

第一个问题是类加载器的问题(这儿假定你现已了解了类加载器的概念),因为容器的类加载器是不能加载war项目中的class的,即使能加载,你要通过Class.forName()去加载类时,在这个收集信息阶段,容器是不或许知道有那些类名称的,虽然可以通过在web.xml直接奉告容器,但说回来,测验Class.forName()时会抛出ClassNotFoundException,而真实的容器完结都会自界说一个ClassLoader,专门去加载项目的class和资源。

那么就算有了自界说的ClassLoader,可以加载到项目的class,那么Class.forName会触发static代码块,假设项目中的Servlet正好写了static代码快,则会调用,虽然终究这个代码块都会被调用,但不应该在这个时分,会出一些问题。

而正确的做法是直接读取二进制class文件,从class文件规范中找到这个class是不是有@WebServlet注解,这是仅有的办法,Spring扫描注解的时分也是这样做的,而Tomcat也是这样,Tomcat解析class文件的类可以点击我检查。

Tomcat是纯自己手撸出一个解析器,假设熟悉class文件格式后,仍是比较简略的,所以这儿我们依托一个结构,比如用org.ow2.asm这个库,额外的知识:Spring也是靠第三方库来读取的。

详细比如如下

private void collectorServlet() {
    try {
        final Set<String> classFileSet = new HashSet<>();
        Files.walkFileTree(Paths.get(this.webProjectPath, WEB_CLASSES_PATH), new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                if (file.toString().endsWith(".class")) classFileSet.add(file.toString());
                return super.visitFile(file, attrs);
            }
        });
        ClassNode classNode = new ClassNode();
        for (String classFile : classFileSet) {
            ClassReader classReader = new ClassReader(Files.newInputStream(Paths.get(classFile)));
            classReader.accept(classNode, ClassReader.EXPAND_FRAMES);
            List<AnnotationNode> visibleAnnotations = classNode.visibleAnnotations;
            for (AnnotationNode visibleAnnotation : visibleAnnotations) {
                if ("Ljavax/servlet/annotation/WebServlet;".equalsIgnoreCase(visibleAnnotation.desc)) {
                    Map<String, Object> annotationValues = ClassUtils.getAnnotationValues(visibleAnnotation.values);
                    Object o = loaderClass(classReader.getClassName());
                    servletMap.put(annotationValues.get("value").toString(), ((HttpServlet) o));
                }
            }
        }
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}
    private Object loaderClass(String name) {
        try {
            Class<?> aClass = appClassloader.loadClass(name);
            return aClass.newInstance();
        } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

首要就是遍历/WEB-INF/classes/目录,运用ClassReader类解析这个class文件,并判别是不是标有WebServlet注解,假设存在,则通过自界说的类加载器加载并实例化他,而这个类加载器首要作用就是根据给定的类名,从/WEB-INF/classes/加载类,假设给定的类不存在,则交给父类加载器。

但向Tomcat都有一些公共的类区域,可以把全部项目所用到的共同库提取出来,放到一个目录下,别的war规范中,/WEB-INF/lib目录用来寄存第三方的jar文件库,类加载器也需求考虑这个目录。

那么这个类加载器加载途径顺次如下:

  1. /WEB-INF/classes/目录
  2. /WEB-INF/lib目录
  3. 公共区域
  4. 父类加载器

假设终究一个也加载不到,则抛出失常,具有一个公共区域其实是很有必要的,一般来说我们都会依赖很多的第三方库,或许自己的代码和资源都不到10M,但是很多的第三方库或许占到上百M,安置传输起来或许不方便,正确的做法应该是把用到的第三方库一次性上传到公共区域,安置时只传自己的代码。

并且类加载器还需求重写getResource、getResourceAsStream等这些办法用来在项目的类途径下查找资源。

发起web服务器

上面说到,Servlet容器也是一个Web服务器,只需发起一个Web服务器后,收到央求,才干传递给Servlet,并且,他还能处理静态资源,完结一个Web服务器重要的是解析HTTP报文,并且根据呼应效果生成HTTP报文。

这部分我们可以运用一个Java供给的现成库,如下。

HttpServer httpServer = HttpServer.create(new InetSocketAddress(4040), 10);
  1. HttpServer:是Java中用于创建HTTP服务器的类。它是Java SE 6引进的,用于支撑简略的HTTP服务端功用。
  2. HttpServer.create:用于创建一个新的HTTP服务器实例。
  3. new InetSocketAddress(4040)InetSocketAddress标明IP地址和端口号的类。这儿的4040是端口号,标明HTTP服务器将在本地计算机的4040端口上监听传入的HTTP央求。
  4. 10:这是服务器的等候行列的最大长度。当HTTP服务器在处理传入的央求时,假设同时有更多央求抵达,它们将被放入等候行列。这儿的10标明等候行列的最大长度为10,即最多允许同时有10个央求在等候处理。

央求映射 & 回来效果

这儿有一点比较麻烦,我们知道doGet和doPost的参数是HttpServletRequestHttpServletResponse,容器需求完结这两个接口,供给央求参数,这儿我们偷个懒,运用mockito这个库来结构一个央求。

下面代码中,createContext用来监听某个央求途径,当有央求过来时,HttpServer会把央求目标封装为HttpExchange,而我们做的事是把他转换为HttpServletRequest

当调用service时,javax.servlet.http.HttpServlet会自动根据央求拜访,调用doGet或者是doPost等。

try {
    HttpServer httpServer = HttpServer.create(new InetSocketAddress(4040), 10);
    httpServer.createContext("/", httpExchange -> {
        Servlet servlet = servletMap.get(httpExchange.getRequestURI().toString());
        JerryCatHttpServletResponse httpServletResponse = new JerryCatHttpServletResponse(Mockito.mock(HttpServletResponse.class));
        HttpServletRequest httpServletRequest = createHttpServletRequest(httpExchange);
        if (servlet != null) {
            try {
                servlet.service(httpServletRequest, httpServletResponse);
                byte[] responseByte = httpServletResponse.getResponseByte();
                httpExchange.sendResponseHeaders(200, responseByte.length);
                httpExchange.getResponseBody().write(responseByte);
                httpExchange.getResponseBody().flush();
            } catch (ServletException e) {
                e.printStackTrace();
            }
        }
    });
    httpServer.start();
} catch (IOException e) {
    throw new RuntimeException(e);
}

到这儿就完毕容器的使命了,只需求等候Servlet处理完结,将效果回来给客户端即可。

但这儿,央求映射显的有点简略,因为我们少了处理通配符的情况。

其他规范

其他特性我们不说,但属于Servlet规范的容器必定要完结,其他规范还有如ServletContainerInitializer、Filter等这儿我们都没有完结,ServletContainerInitializer是一个很有用的东西,SpringBoot打包成war后,就依托它去发起。

Filter相同的做法,也是通过ClassReader读取,在调用service前一步,先调用Filter。

完毕

这儿只完结了一个容器的雏形中的中心,一个无缺的容器,至少要做到供给无缺的HttpServletRequest的完结,还有HttpServletResponse,这儿只做演示,没有做太多处理,比如最重要的Cookie处理、Session处理,不然应用程序就无法完结用户登录状况维护。

HttpServletRequest是承继ServletRequest的,他们界说的办法加起来共有70多个,需求一一去完结,才干给用户供给一个无缺的央求信息供给,不然用户想拿一个央求头都拿不到,也没办法继续开发。

有无缺的信息供给后,就可以做额外的功用开发了,比如WebSocket,当央求过来时分,发现是一个WebSocket握手央求,那么相应的要做一个协议升级,转换为WebSocket协议。

别的,一个容器进程是可以加载多个war项目的,就像tomcat,一朝一夕,支撑的东西多了,就成了真实的容器。