BinCat V5-支持SpringBoot应用

时至今日(2020年9月),SpringBoot因其配置非常简单功能强大,已经成为了绝大部分微服务项目的首选架构,Servlet 3+的新特性也为SpringBoot的便捷配置提供了非常大的帮助。我们将使用BinCat V5启动并运行一个用SpringBoot实现的Blog应用,从而来学习Servlet容器的工作原理。

创建基于SpringBoot的javasec-blog项目

首先我们在javaweb-sec项目下创建一个javasec-test的模块(用于存储javasec文章所用到的测试项目),然后我们在javasec-test模块中创建一个javasec-blog模块(一个标准的SpringBoot项目),javasec-blog项目是一个用于演示的博客项目。

image-20200917110907091

javasec-blog War项目构建

SpringBoot不但支持嵌入式部署也支持传统的war包部署方式,但需要注意的是war包部署的时候需要做一些特殊的修改。BinCat目前只实现了基于war部署的方式,所以我们需要将javasec-blog打成一个war包。

构建项目的时候可参考如下步骤(1-3步默认已修改,不需要关注):

  1. 修改pom.xml添加<packaging>war</packaging>,默认是jar

  2. 修改pom.xmlbuildplugins标签,添加:

    1. <plugin>
    2. <groupId>org.apache.maven.plugins</groupId>
    3. <artifactId>maven-war-plugin</artifactId>
    4. <version>${maven-deploy-plugin.version}</version>
    5. <configuration>
    6. <failOnMissingWebXml>false</failOnMissingWebXml>
    7. </configuration>
    8. </plugin>
  3. 修改SpringBoot启动类代码,示例中是:JavaWebBlogApplication。继承org.springframework.boot.web.servlet.support.SpringBootServletInitializer,然后重写configure方法。如下:

    1. package com.anbai.sec.blog.config;
    2. import org.springframework.boot.SpringApplication;
    3. import org.springframework.boot.autoconfigure.SpringBootApplication;
    4. import org.springframework.boot.builder.SpringApplicationBuilder;
    5. import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
    6. /**
    7. * @author yz
    8. */
    9. @SpringBootApplication(scanBasePackages = "com.anbai.sec.blog.*")
    10. public class JavaWebBlogApplication extends SpringBootServletInitializer {
    11. public static void main(String[] args) {
    12. SpringApplication.run(JavaWebBlogApplication.class, args);
    13. }
    14. @Override
    15. protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
    16. return builder.sources(JavaWebBlogApplication.class);
    17. }
    18. }
  4. 修改javaweb-sec-source/javasec-test/javasec-blog/src/main/resources/application.properties配置文件中的数据库信息。

  5. 新建Mysql数据库javaweb-blog,并导入javaweb-sec-source/javasec-test/javaweb-blog.sql

  6. 使用maven命令构建整个javaweb-sec项目(第二次构建的时候可以单独build blog项目),在javaweb-sec根目录执行:mvn clean install

项目构建成功后结构如下:

image-20200917143151435

BinCat V5实现

因为V5要实现正常的运行一个SpringBoot项目,所以我们需要写一个支持单应用的Servlet容器,而且还需要实现之前版本未实现的其他Servlet接口。

BinCatWebAppClassLoader实现

为了实现加载Web应用的资源和类文件,我们需要实现一个类简单的加载器,用于加载WEB-INF目录下的classeslibs中的所有文件。

BinCatWebAppClassLoader代码:

  1. package com.anbai.sec.server.loader;
  2. import java.net.URL;
  3. import java.net.URLClassLoader;
  4. public class BinCatWebAppClassLoader extends URLClassLoader {
  5. public BinCatWebAppClassLoader(URL[] urls, ClassLoader parent) {
  6. super(urls, parent);
  7. }
  8. }

为了统一加载Web应用的资源文件,我们还需要将BinCatWebAppClassLoader的类示例设置为当前线程的类加载器,如下:

  1. // 定义需要加载的Web应用路径,默认配置的路径必须先使用maven编译:javasec-blog项目
  2. String webAppFile = System.getProperty("user.dir") +
  3. "/javaweb-sec-source/javasec-test/javasec-blog/target/javasec-blog-1.0.0/";
  4. // 创建BinCatWebAppClassLoader,加载Web应用
  5. BinCatWebAppClassLoader appClassLoader = BinCatConfig.createAppClassLoader(webAppFile);
  6. // 设置当前线程的上下文类加载器
  7. Thread.currentThread().setContextClassLoader(appClassLoader);

这样一来在整个Web应用(示例中指的是javasec-blog项目)默认将会使用BinCatWebAppClassLoader来实现资源文件和类文件的加载了。

BinCatWebAppClassLoader初始化

BinCatWebAppClassLoader在初始化的时候需要加载所有应用的WEB-INF目录的类和资源文件,但由于V5版本我们只想实现单应用的部署,所以我们只需要加载我们预设好的Web应用目录就好了。在初始化BinCatWebAppClassLoader的时候将WEB-INF/classes目录和WEB-INF/libs目录下的所有jar文件都添加到BinCatWebAppClassLoader当中。

BinCatWebAppClassLoader创建并加载Web应用资源代码:

  1. public static BinCatWebAppClassLoader createAppClassLoader(String webAppFile) throws IOException {
  2. File webRoot = new File(webAppFile);
  3. File webInfoDir = new File(webRoot, "WEB-INF");
  4. File libDir = new File(webInfoDir, "lib");
  5. File classesDir = new File(webInfoDir, "classes");
  6. Set<URL> classPathURL = new HashSet<>();
  7. File[] libs = libDir.listFiles(new FilenameFilter() {
  8. @Override
  9. public boolean accept(File dir, String name) {
  10. return name.endsWith(".jar");
  11. }
  12. });
  13. // 加载lib目录下所有的jar文件
  14. for (File lib : libs) {
  15. classPathURL.add(lib.toURL());
  16. }
  17. // 加载classes目录的所有资源文件
  18. classPathURL.add(classesDir.toURL());
  19. // 创建Web应用的类加载器
  20. return new BinCatWebAppClassLoader(
  21. classPathURL.toArray(new URL[classPathURL.size()]), BinCatConfig.class.getClassLoader()
  22. );
  23. }

BinCatServletContext实现

V4版本中我们虽然已经实现了一个BinCatServletContext,但是我们并没有实现Servlet的动态注册功能,而且V4实现的Servlet注册和初始化过程都是静态的,我们需要将整个过程升级为动态的。

  1. 为了能够在BinCatServletContext中获取到我们自定义的Web应用的类加载器,我们需要在创建BinCatServletContext的时候将将其缓存到类对象中。
  2. 创建一个Map<String, Servlet> servletMapSet<BinCatServletRegistrationDynamic> registrationDynamics对象用于缓存动态注册的Servlet
  3. 创建一个Map<String, String> initParameterMap对象用于记录ServletContext初始化参数。
  4. 创建一个Map<String, Object> attributeMap,用于记录ServletContext中的属性对象(attribute)。

BinCatServletContext代码:

  1. package com.anbai.sec.server.servlet;
  2. import com.anbai.sec.server.loader.BinCatWebAppClassLoader;
  3. import javax.servlet.*;
  4. import javax.servlet.descriptor.JspConfigDescriptor;
  5. import java.io.File;
  6. import java.io.InputStream;
  7. import java.net.MalformedURLException;
  8. import java.net.URL;
  9. import java.util.*;
  10. public class BinCatServletContext implements ServletContext {
  11. // 创建一个装动态注册的Servlet的Map
  12. private final Map<String, Servlet> servletMap = new HashMap<>();
  13. // 创建一个装ServletContext初始化参数的Map
  14. private final Map<String, String> initParameterMap = new HashMap<>();
  15. // 创建一个装ServletContext属性对象的Map
  16. private final Map<String, Object> attributeMap = new HashMap<>();
  17. // 创建一个装Servlet动态注册的Set
  18. private final Set<BinCatServletRegistrationDynamic> registrationDynamics = new LinkedHashSet<>();
  19. // BinCatWebAppClassLoader,Web应用的类加载器
  20. private final BinCatWebAppClassLoader appClassLoader;
  21. // 此处省略ServletContext接口中的大部分方法,仅保留几个实现了的示例方法...
  22. public BinCatServletContext(BinCatWebAppClassLoader appClassLoader) throws Exception {
  23. this.appClassLoader = appClassLoader;
  24. }
  25. @Override
  26. public Servlet getServlet(String name) throws ServletException {
  27. return servletMap.get(name);
  28. }
  29. @Override
  30. public Enumeration<Servlet> getServlets() {
  31. Set<Servlet> servlets = new HashSet<Servlet>(servletMap.values());
  32. return Collections.enumeration(servlets);
  33. }
  34. @Override
  35. public Enumeration<String> getServletNames() {
  36. Set<String> servlets = new HashSet<String>(servletMap.keySet());
  37. return Collections.enumeration(servlets);
  38. }
  39. @Override
  40. public String getRealPath(String path) {
  41. return new File(System.getProperty("user.dir"), path).getAbsolutePath();
  42. }
  43. public Map<String, String> getInitParameterMap() {
  44. return initParameterMap;
  45. }
  46. @Override
  47. public String getInitParameter(String name) {
  48. return initParameterMap.get(name);
  49. }
  50. @Override
  51. public Enumeration<String> getInitParameterNames() {
  52. return Collections.enumeration(initParameterMap.keySet());
  53. }
  54. @Override
  55. public boolean setInitParameter(String name, String value) {
  56. if (!initParameterMap.containsKey(name)) {
  57. initParameterMap.put(name, value);
  58. return true;
  59. }
  60. return false;
  61. }
  62. @Override
  63. public Object getAttribute(String name) {
  64. return attributeMap.get(name);
  65. }
  66. @Override
  67. public Enumeration<String> getAttributeNames() {
  68. return Collections.enumeration(attributeMap.keySet());
  69. }
  70. @Override
  71. public void setAttribute(String name, Object object) {
  72. attributeMap.put(name, object);
  73. }
  74. @Override
  75. public void removeAttribute(String name) {
  76. attributeMap.remove(name);
  77. }
  78. @Override
  79. public ServletRegistration.Dynamic addServlet(String servletName, Servlet servlet) {
  80. servletMap.put(servletName, servlet);
  81. BinCatServletRegistrationDynamic dynamic = new BinCatServletRegistrationDynamic(servletName, servlet, this);
  82. registrationDynamics.add(dynamic);
  83. return dynamic;
  84. }
  85. @Override
  86. public ClassLoader getClassLoader() {
  87. return this.appClassLoader;
  88. }
  89. public Map<String, Servlet> getServletMap() {
  90. return servletMap;
  91. }
  92. public Set<BinCatServletRegistrationDynamic> getRegistrationDynamics() {
  93. return registrationDynamics;
  94. }
  95. }

BinCatServletContext初始化

在初始化BinCatServletContext时我们通过手动注册Servlet的方式初始化了几个容器内置的Servlet,并通过扫描初始化Servlet类注解的方式将ServletBinCatServletContext中注册。

BinCatServletContext初始化代码:

  1. /**
  2. * 手动注册Servlet并创建BinCatServletContext对象
  3. *
  4. * @param appClassLoader 应用的类加载器
  5. * @return ServletContext Servlet上下文对象
  6. */
  7. public static BinCatServletContext createServletContext(BinCatWebAppClassLoader appClassLoader) throws Exception {
  8. BinCatServletContext servletContext = new BinCatServletContext(appClassLoader);
  9. // 手动注册Servlet类
  10. Class<Servlet>[] servletClass = new Class[]{
  11. TestServlet.class,
  12. CMDServlet.class,
  13. QuercusPHPServlet.class
  14. };
  15. for (Class<Servlet> clazz : servletClass) {
  16. Servlet servlet = clazz.newInstance();
  17. WebServlet webServlet = clazz.getAnnotation(WebServlet.class);
  18. if (webServlet != null) {
  19. // 获取WebInitParam配置
  20. WebInitParam[] webInitParam = webServlet.initParams();
  21. // 动态创建Servlet对象
  22. ServletRegistration.Dynamic dynamic = servletContext.addServlet(webServlet.name(), servlet);
  23. // 动态设置Servlet映射地址
  24. dynamic.addMapping(webServlet.urlPatterns());
  25. // 设置Servlet启动参数
  26. for (WebInitParam initParam : webInitParam) {
  27. dynamic.setInitParameter(initParam.name(), initParam.value());
  28. }
  29. }
  30. }
  31. // 创建ServletContext
  32. return servletContext;
  33. }

BinCatServletRegistrationDynamic实现

BinCatServletContext中核心的是addServlet方法的动态注册Servlet,如下:

  1. @Override
  2. public ServletRegistration.Dynamic addServlet(String servletName, Servlet servlet) {
  3. servletMap.put(servletName, servlet);
  4. BinCatServletRegistrationDynamic dynamic = new BinCatServletRegistrationDynamic(servletName, servlet, this);
  5. registrationDynamics.add(dynamic);
  6. return dynamic;
  7. }

Web应用调用addServlet注册一个Servlet对象时会返回一个ServletRegistration.Dynamic对象,通过这个Dynamic对象可以设置Servlet的映射信息和初始化信息。但因为ServletRegistration.Dynamic是一个接口,所以我们必须实现此接口的所有方法。

BinCatServletRegistrationDynamic代码:

  1. package com.anbai.sec.server.servlet;
  2. import javax.servlet.MultipartConfigElement;
  3. import javax.servlet.Servlet;
  4. import javax.servlet.ServletRegistration;
  5. import javax.servlet.ServletSecurityElement;
  6. import java.util.*;
  7. public class BinCatServletRegistrationDynamic implements ServletRegistration.Dynamic {
  8. private final String servletName;
  9. private final Set<String> servletMapping = new LinkedHashSet<>();
  10. private final Map<String, String> initParametersMap = new HashMap<>();
  11. private final Servlet servlet;
  12. private final BinCatServletContext servletContext;
  13. public BinCatServletRegistrationDynamic(String servletName, Servlet servlet, BinCatServletContext servletContext) {
  14. this.servletName = servletName;
  15. this.servlet = servlet;
  16. this.servletContext = servletContext;
  17. }
  18. @Override
  19. public void setLoadOnStartup(int loadOnStartup) {
  20. }
  21. @Override
  22. public Set<String> setServletSecurity(ServletSecurityElement constraint) {
  23. return null;
  24. }
  25. @Override
  26. public void setMultipartConfig(MultipartConfigElement multipartConfig) {
  27. }
  28. @Override
  29. public void setRunAsRole(String roleName) {
  30. }
  31. @Override
  32. public void setAsyncSupported(boolean isAsyncSupported) {
  33. }
  34. @Override
  35. public Set<String> addMapping(String... urlPatterns) {
  36. Collections.addAll(servletMapping, urlPatterns);
  37. return servletMapping;
  38. }
  39. @Override
  40. public Collection<String> getMappings() {
  41. return servletMapping;
  42. }
  43. @Override
  44. public String getRunAsRole() {
  45. return null;
  46. }
  47. @Override
  48. public String getName() {
  49. return servletName;
  50. }
  51. @Override
  52. public String getClassName() {
  53. return servlet.getClass().getName();
  54. }
  55. @Override
  56. public boolean setInitParameter(String name, String value) {
  57. if (!initParametersMap.containsKey(name)) {
  58. initParametersMap.put(name, value);
  59. return true;
  60. }
  61. return false;
  62. }
  63. @Override
  64. public String getInitParameter(String name) {
  65. return initParametersMap.get(name);
  66. }
  67. @Override
  68. public Set<String> setInitParameters(Map<String, String> initParameters) {
  69. initParametersMap.putAll(initParameters);
  70. return initParametersMap.keySet();
  71. }
  72. @Override
  73. public Map<String, String> getInitParameters() {
  74. return initParametersMap;
  75. }
  76. public Servlet getServlet() {
  77. return servlet;
  78. }
  79. public String getServletName() {
  80. return servletName;
  81. }
  82. }

实现Web应用的启动

在启动我们的示例应用之前我们已经创建了一个BinCatWebAppClassLoaderBinCatServletContext,其中BinCatWebAppClassLoader已经加载了javasec-blog项目的WEB-INF信息、BinCatServletContext也初始化了几个BinCat内置的Servlet。万事俱备,我们现在只欠如何启动Servlet容器了。

Servlet3.0开始,Servlet除了可以从web.xml启动,还可以通过SPI机制动态获取ServletContainerInitializer(简称SCI)并通过SCI启动Servlet容器。因为V5并未打算实现传统的web.xml启动方式,而是只想实现SCI启动方式(这也是SpringBootwar包部署的启动方式)。

ServletContainerInitializer是一个接口类,仅定义了一个叫onStartup的方法,所有实现了该接口的类只要重写onStartup方法将可以实现Servlet容器的启动。

ServletContainerInitializer代码:

  1. package javax.servlet;
  2. import java.util.Set;
  3. /**
  4. * Interface which allows a library/runtime to be notified of a web
  5. * application's startup phase and perform any required programmatic
  6. * registration of servlets, filters, and listeners in response to it.
  7. *
  8. * <p>Implementations of this interface may be annotated with
  9. * {@link javax.servlet.annotation.HandlesTypes HandlesTypes}, in order to
  10. * receive (at their {@link #onStartup} method) the Set of application
  11. * classes that implement, extend, or have been annotated with the class
  12. * types specified by the annotation.
  13. *
  14. * <p>If an implementation of this interface does not use <tt>HandlesTypes</tt>
  15. * annotation, or none of the application classes match the ones specified
  16. * by the annotation, the container must pass a <tt>null</tt> Set of classes
  17. * to {@link #onStartup}.
  18. *
  19. * <p>When examining the classes of an application to see if they match
  20. * any of the criteria specified by the <tt>HandlesTypes</tt> annontation
  21. * of a <tt>ServletContainerInitializer</tt>, the container may run into
  22. * classloading problems if any of the application's optional JAR
  23. * files are missing. Because the container is not in a position to decide
  24. * whether these types of classloading failures will prevent
  25. * the application from working correctly, it must ignore them,
  26. * while at the same time providing a configuration option that would
  27. * log them.
  28. *
  29. * <p>Implementations of this interface must be declared by a JAR file
  30. * resource located inside the <tt>META-INF/services</tt> directory and
  31. * named for the fully qualified class name of this interface, and will be
  32. * discovered using the runtime's service provider lookup mechanism
  33. * or a container specific mechanism that is semantically equivalent to
  34. * it. In either case, <tt>ServletContainerInitializer</tt> services from web
  35. * fragment JAR files excluded from an absolute ordering must be ignored,
  36. * and the order in which these services are discovered must follow the
  37. * application's classloading delegation model.
  38. *
  39. * @see javax.servlet.annotation.HandlesTypes
  40. *
  41. * @since Servlet 3.0
  42. */
  43. public interface ServletContainerInitializer {
  44. /**
  45. * Notifies this <tt>ServletContainerInitializer</tt> of the startup
  46. * of the application represented by the given <tt>ServletContext</tt>.
  47. *
  48. * <p>If this <tt>ServletContainerInitializer</tt> is bundled in a JAR
  49. * file inside the <tt>WEB-INF/lib</tt> directory of an application,
  50. * its <tt>onStartup</tt> method will be invoked only once during the
  51. * startup of the bundling application. If this
  52. * <tt>ServletContainerInitializer</tt> is bundled inside a JAR file
  53. * outside of any <tt>WEB-INF/lib</tt> directory, but still
  54. * discoverable as described above, its <tt>onStartup</tt> method
  55. * will be invoked every time an application is started.
  56. *
  57. * @param c the Set of application classes that extend, implement, or
  58. * have been annotated with the class types specified by the
  59. * {@link javax.servlet.annotation.HandlesTypes HandlesTypes} annotation,
  60. * or <tt>null</tt> if there are no matches, or this
  61. * <tt>ServletContainerInitializer</tt> has not been annotated with
  62. * <tt>HandlesTypes</tt>
  63. *
  64. * @param ctx the <tt>ServletContext</tt> of the web application that
  65. * is being started and in which the classes contained in <tt>c</tt>
  66. * were found
  67. *
  68. * @throws ServletException if an error has occurred
  69. */
  70. public void onStartup(Set<Class<?>> c, ServletContext ctx)
  71. throws ServletException;
  72. }

因为BinCatWebAppClassLoader已经加载了示例项目的所有资源文件,所以我们可以使用BinCatWebAppClassLoader来获取到示例项目中的所有路径为:META-INF/services/javax.servlet.ServletContainerInitializer的资源文件。

  1. BinCatWebAppClassLoader classLoader = (BinCatWebAppClassLoader) servletContext.getClassLoader();
  2. String servletService = "META-INF/services/javax.servlet.ServletContainerInitializer";
  3. // 获取当前ClassLoader中的所有ServletContainerInitializer配置
  4. Enumeration<URL> resources = classLoader.getResources(servletService);

SCI资源文件中配置的是实现了SCI接口的Java类,一个SCI资源文件可能会配置多个实现类(多个类以换行分割)。读取到SCI资源文件后我们将可以使用反射的方式去加载资源文件中配置的Java类了。

读取SCI配置并加载SCI实现类示例:

image-20200917162508462

上图示例中可以看到我们通过读取BinCatWebAppClassLoader中的SCI资源时获取到了一个类名为:org.springframework.web.SpringServletContainerInitializer的类。SpringServletContainerInitializer实现了ServletContainerInitializer,所以想要启动示例的SpringBoot项目仅需要调用该类的onStartup方法将可以了。onStartup方法需要两个参数:Set<Class<?>> cServletContext ctx,即可SCI启动类对象集合Servlet上下文。因为ServletContext我们可以轻易的获取到,所以我们重点是如何获取到SCI启动类对象集合了。

通过阅读源码不难发现,在实现了SCI的接口类类名上会有一个叫做@HandlesTypes的注解,读取到这个注解的值将可以找到处理SCI启动类对象集合的类对象,然后反向去找该类对象的实现类将可以找到所有需要被SCI启动的类数组了。

SpringServletContainerInitializer示例:

image-20200917164429015

创建SCI实现类示例和获取该示例的HandlesTypes配置方式如下:

  1. Class<?> initClass = Class.forName(className, true, classLoader);
  2. HandlesTypes handlesTypes = initClass.getAnnotation(HandlesTypes.class);
  3. ServletContainerInitializer sci = (ServletContainerInitializer) initClass.newInstance();
  4. sciClassMap.put(sci, new HashSet<Class<?>>());
  5. if (handlesTypes != null) {
  6. Class[] handlesClass = handlesTypes.value();
  7. handlesTypesMap.put(sci, handlesClass);
  8. }

获取到@HandlesTypes配置的类名后剩下的就是如何在示例项目中的所有类中找出@HandlesTypes配置的类实例了,如上图中SpringServletContainerInitializer配置的@HandlesTypesWebApplicationInitializer,那么我们现在就必须通过扫包和类文件的方式找出所有WebApplicationInitializer的子类,然后再通过上一步创建出来的SCI实例调用onStartup方法完成Servlet容器启动。

为了扫描当前类加载所有的类对象,我们需要先获取出当前类加载加载的所有的类名称,然后再依次扫描这些类是否是@HandlesTypes中配置的类(如:WebApplicationInitializer)的子类。

获取BinCatWebAppClassLoader加载的所有的类名代码:

  1. /**
  2. * 获取BinCatWebAppClassLoader类加载器加载的所有class类名
  3. *
  4. * @param classLoader 类加载器
  5. * @param sciClassMap SCI类对象
  6. * @param handlesTypesMap SCI类对象配置的HandlesTypes对象映射Map
  7. * @return
  8. * @throws Exception
  9. */
  10. private static void findInitializerClass(
  11. BinCatWebAppClassLoader classLoader,
  12. Map<ServletContainerInitializer, Set<Class<?>>> sciClassMap,
  13. Map<ServletContainerInitializer, Class<?>[]> handlesTypesMap) throws Exception {
  14. // 创建一个存储所有被BinCatWebAppClassLoader加载的类名称对象
  15. Set<String> classList = new HashSet<>();
  16. // 获取BinCatWebAppClassLoader加载的所有URL地址
  17. URL[] urls = classLoader.getURLs();
  18. for (URL url : urls) {
  19. File file = new File(url.toURI());
  20. // 遍历所有的jar文件
  21. if (file.isFile() && file.getName().endsWith(".jar")) {
  22. JarFile jarFile = new JarFile(file);
  23. Enumeration<JarEntry> jarEntry = jarFile.entries();
  24. while (jarEntry.hasMoreElements()) {
  25. JarEntry entry = jarEntry.nextElement();
  26. String fileName = entry.getName();
  27. // 遍历jar文件中的所有class文件,并转换成java类名格式,如com/anbai/Test.class会转换成com.anbai.Test
  28. if (fileName.endsWith(".class")) {
  29. String className = fileName.replace(".class", "").replace("/", ".");
  30. classList.add(className);
  31. }
  32. }
  33. } else if (file.isDirectory()) {
  34. // 遍历所有classes目录下的.class文件,并转换成java类名格式
  35. Collection<File> files = FileUtils.listFiles(file, new String[]{"class"}, true);
  36. for (File classFile : files) {
  37. String className = classFile.toString().substring(file.toString().length())
  38. .replace(".class", "").replaceAll("^/", "").replace("/", ".");
  39. classList.add(className);
  40. }
  41. }
  42. }
  43. // 通过ASM方式获取所有Java类的继承关系,并判断是否是HandlesTypes配置中的类的子类
  44. for (String className : classList) {
  45. // 通过ASM的方式获取当前类的所有父类(包括继承和实现的所有类)
  46. Set<String> superClassList = ClassUtils.getSuperClassListByAsm(className, classLoader);
  47. // 遍历所有HandlesTypes配置
  48. for (ServletContainerInitializer sci : handlesTypesMap.keySet()) {
  49. // 获取HandlesTypes配置的类数组对象
  50. Class[] handlesTypesClass = handlesTypesMap.get(sci);
  51. // 遍历所有HandlesTypes配置的类数组对象
  52. for (Class typesClass : handlesTypesClass) {
  53. // 获取HandlesTypes配置的类名称
  54. String typeClassName = typesClass.getName();
  55. // 检测当前Java类是否是HandlesTypes配置的类的子类,如果是就记录下来
  56. if (superClassList.contains(typeClassName) && !className.equals(typeClassName)) {
  57. // 获取SCI启动类对象集合
  58. Set<Class<?>> sciClass = sciClassMap.get(sci);
  59. // 反射加载当前类对象
  60. Class clazz = classLoader.loadClass(className);
  61. // 将找到的SCI启动类添加到集合中
  62. sciClass.add(clazz);
  63. }
  64. }
  65. }
  66. }
  67. }

整个扫包过程会比较缓慢,因为一次性扫描了1万多个类,并且还是用ASM解析了这1万多个类的继承关系。最终我们将会得出一个Map<ServletContainerInitializer, Set<Class<?>>> sciClassMap对象,我们通过遍历这个sciClassMap并依次调用onStartup方法即可实现Servlet容器的启动。

启动所有SCI代码:

  1. for (ServletContainerInitializer initializer : sciClassMap.keySet()) {
  2. Set<Class<?>> initClassSet = sciClassMap.get(initializer);
  3. // 调用Servlet容器初始化的onStartup方法,启动容器
  4. initializer.onStartup(initClassSet, servletContext);
  5. }

initializer.onStartup(initClassSet, servletContext);因为传入了ServletContext,所以initializer很有可能会通过ServletContext动态注册一些FilterServletListener,而这些FilterServletListener和我们BinCat的内置的Servlet都处于未初始化的状态,这个时候我们就必须要做一些初始化工作(V5版本只支持Servlet,并不支持FilterListener)了。

初始化ServletContext中的所有Servlet代码:

  1. initServlet(servletContext);

完成以上所有逻辑后我们的BinCat也就算启动成功了,剩下的就是如何处理浏览器请求了。

image-20200917173402207

Servlet请求处理

V5依旧是根据浏览器请求的URL地址调用对应的Servletservice方法处理Servlet请求,访问javasec-blog首页测试:http://localhost:8080/

image-20200917174239016

请求的/最终会调用SpringMVCorg.springframework.web.servlet.DispatcherServlet类处理请求,如下:

image-20200917175431500

dynamic.getServlet().service(req, resp)最终会调用SpringDispatcherServlet类的父类org.springframework.web.servlet.FrameworkServletservice方法,如下:

image-20200917180111632

访问文章详情页测试:http://localhost:8080/?p=12

image-20200917173917790

至此,耗时大约一周时间,我们的BinCat从支持解析简单的HelloWorld到如今已经实现了启动单Web应用SpringBoot了,当然这里面充斥这各种各样的Bug和安全问题,我们的目标并不是实现一个国产化Java中间件,而是将BinCat变成一个存在各种各样漏洞的靶场。