Skip to content

17 更大的框架:MiniSpring与MiniTomcat集成

你好,我是郭屹。今天我们继续手写MiniTomcat。

上节课我们实现了Context类的预加载,并引入server.xml和web.xml,分别对服务端和Servlet进行自定义配置。到目前为止我们的MiniTomcat已经是一个完整的小型Tomcat了。

考虑到在目前大部分的场景中,依托于Spring框架编写的项目大多使用Tomcat服务器进行启动,而我们之前已经完成过MiniSpring框架的构建,所以很自然地,我们就会想到 将现有的MiniTomcat与之前完成的MiniSpring进行集成。

从原理上来说,只要是按照Servlet规范实现的服务器,就可以将MiniSpring直接放在webapps目录下运行,不用做任何额外的工作。所以从道理上讲,MiniTomcat和MiniSpring的集成也是这样的。

不过,因为MiniTomcat毕竟是一个麻雀版的Tomcat,并没有完整地实现Servlet规范,所以现在这么直接绑在一起是不能顺利运行的,我们还需要做一点完善的工作。

我们先回顾一下,MiniSpring启动的时候,依赖Tomcat环境做什么事情。一切的起点都在web.xml文件中,里面定义了Listener、Filter和Servlet。为了让MiniSpring启动起来,我们要实现一个ContextLoaderListener,这个Listener的目的是启动MiniSpring的IoC容器。然后用一个DispatcherServlet来拦截一切路径,通过这个DispatcherServlet来使用MiniSpring的MVC。

图片

但我们现在的MiniTomcat有局限性,所以需要做一些调整。

  1. 首先表现在对web.xml的解析中,Listener读不了初始化参数context-param,而且Servlet还配置不了url-pattern和init-param。
  2. 还有我们的Listener接口不符合servlet规范,不支持ServletContextListener,所以MiniTomcat还不能通过Listener把MiniSpring启动起来。
  3. 另外,以前在HttpProcessor里只是简单地判断路径,带有/servlet/路径的就认为是要调用一个后台的Servlet,这点也需要调整一下。同时Servlet的名字是直接从URI中截取的,现在应该改用查找url-pattern通过路径匹配某个Servlet了。

现在我们一起来动手改造。

MiniSpring中的Bean

以前在MiniSpring项目中,我们自己实现了@RequestMapping等Spring中的常用注解,并且利用注解在HelloWorldBean类中对接口进行了定义。这个bean也仍然是我们的实现目标,也就是说通过这个bean来验证MiniTomcat和MiniSpring的集成。之前bean的代码主体如下:

package com.test.controller;
public class HelloWorldBean {
    @Autowired
    BaseService baseservice;
    @Autowired
    UserService userService;

    @RequestMapping("/test2")
    public void doTest2(HttpServletRequest request, HttpServletResponse response) {
       String str = "test 2, hello world!";
       try {
          response.getWriter().write(str);
       } catch (IOException e) {
          // TODO Auto-generated catch block
          e.printStackTrace();
       }
    }
    @RequestMapping("/test5")
    public ModelAndView doTest5(User user) {
       ModelAndView mav = new ModelAndView("test","msg",user.getName());
       return mav;
    }

    @Autowired
    IAction action;
    @RequestMapping("/testaop2")
    public void doTestAop2(HttpServletRequest request, HttpServletResponse response) {
       action.doSomething();

       String str = "test aop 2, hello world!";
       try {
          response.getWriter().write(str);
       } catch (IOException e) {
          // TODO Auto-generated catch block
          e.printStackTrace();
       }
    }
}

之前我们通常是采用嵌入式的方式使Spring业务项目与Tomcat结合在一起,即项目中内嵌一个Tomcat的Web服务器,这是因为Tomcat本身提供了内嵌这一种模式。

而目前这种内嵌模式我们还没有实现,我们实现的是依托于MiniTomcat项目本身,将业务服务代码打包成App的形式,在项目中运行。这个时候MiniTomcat是一个独立运行的容器,管理多个webapp。

在后续集成过程中,我们会将MiniSpring这一项目整体编译,然后通过类似Servlet调用方式,例如 http://localhost:8080/app/test2 进行接口测试。

项目结构

这节课我们把所有的ClassLoader移到/loader目录下,然后使用server.xml和web.xml分别管理Host启动配置与Servlet加载配置,这也是我们这节课的重点,你可以看一下参考目录。

MiniTomcat
├─ conf
│  ├─ server.xml
├─ src
│  ├─ main
│  │  ├─ java
│  │  │  ├─ com
│  │  │  │  ├─ minit
│  │  │  │  │  ├─ connector
│  │  │  │  │  │  ├─ http
│  │  │  │  │  │  │  ├─ DefaultHeaders.class
│  │  │  │  │  │  │  ├─ HttpConnector.class
│  │  │  │  │  │  │  ├─ HttpHeader.class
│  │  │  │  │  │  │  ├─ HttpProcessor.class
│  │  │  │  │  │  │  ├─ HttpRequestImpl.class
│  │  │  │  │  │  │  ├─ HttpRequestLine.class
│  │  │  │  │  │  │  ├─ HttpResponseImpl.class
│  │  │  │  │  │  │  ├─ ServletProcessor.class
│  │  │  │  │  │  │  ├─ SocketInputStream.class
│  │  │  │  │  │  │  ├─ StatisResourceProcessor.class
│  │  │  │  │  │  ├─ HttpRequestFacade.class
│  │  │  │  │  │  ├─ HttpResponseFacade.class
│  │  │  │  │  ├─ core
│  │  │  │  │  │  ├─ ApplicationFilterChain.class
│  │  │  │  │  │  ├─ ApplicationFilterConfig.class
│  │  │  │  │  │  ├─ ContainerBase.class
│  │  │  │  │  │  ├─ ContainerListenerDef.class
│  │  │  │  │  │  ├─ FilterDef.class
│  │  │  │  │  │  ├─ FilterMap.class
│  │  │  │  │  │  ├─ StandardContext.class
│  │  │  │  │  │  ├─ StandardContextValve.class
│  │  │  │  │  │  ├─ StandardHost.class
│  │  │  │  │  │  ├─ StandardHostValve.class
│  │  │  │  │  │  ├─ StandardPipeline.class
│  │  │  │  │  │  ├─ StandardServletConfig.class
│  │  │  │  │  │  ├─ StandardServletContext.class
│  │  │  │  │  │  ├─ StandardWrapper.class
│  │  │  │  │  │  ├─ StandardWrapperValve.class
│  │  │  │  │  ├─ loader
│  │  │  │  │  │  ├─ CommonClassLoader.class
│  │  │  │  │  │  ├─ CommonLoader.class
│  │  │  │  │  │  ├─ WebappClassLoader.class
│  │  │  │  │  │  ├─ WebappLoader.resources.class
│  │  │  │  │  ├─ logger
│  │  │  │  │  │  ├─ Constants.class
│  │  │  │  │  │  ├─ FileLogger.class
│  │  │  │  │  │  ├─ LoggerBase.class
│  │  │  │  │  │  ├─ SystemErrLogger.class
│  │  │  │  │  │  ├─ SystemOutLogger.class
│  │  │  │  │  ├─ session
│  │  │  │  │  │  ├─ StandardSession.class
│  │  │  │  │  │  ├─ StandardSessionFacade.class
│  │  │  │  │  ├─ startup
│  │  │  │  │  │  ├─ BootStrap.class
│  │  │  │  │  ├─ util
│  │  │  │  │  │  ├─ CookieTools.class
│  │  │  │  │  │  ├─ StringManager.class
│  │  │  │  │  │  ├─ URLDecoder.class
│  │  │  │  │  ├─ valves
│  │  │  │  │  │  ├─ AccessLogValve.class
│  │  │  │  │  │  ├─ ValveBase.class
│  │  │  │  ├─ Connector.class
│  │  │  │  ├─ Container.class
│  │  │  │  ├─ ContainerEvent.class
│  │  │  │  ├─ ContainerListener.class
│  │  │  │  ├─ Context.class
│  │  │  │  ├─ InstanceEvent.class
│  │  │  │  ├─ InstanceListener.class
│  │  │  │  ├─ Loader.class
│  │  │  │  ├─ Logger.class
│  │  │  │  ├─ Pipeline.class
│  │  │  │  ├─ Request.class
│  │  │  │  ├─ Response.class
│  │  │  │  ├─ Session.class
│  │  │  │  ├─ SessionEvent.class
│  │  │  │  ├─ SessionListener.class
│  │  │  │  ├─ Valve.class
│  │  │  │  ├─ ValveContext.class
│  │  │  │  ├─ Wrapper.class
│  │  ├─ resources
│  ├─ test
│  │  ├─ java
│  │  │  ├─ test
│  │  │  │  ├─ HelloServlet.class
│  │  │  │  ├─ TestFilter.class
│  │  │  │  ├─ TestListener.class
│  │  │  │  ├─ TestServlet.class
│  │  ├─ resources
├─ webapps
│  ├─ app-minispring
│  │  ├─ WEB-INF
│  │  │  ├─ lib
│  │  │  ├─ classes
│  │  │  │  ├─ com
│  │  │  │  │  ├─ minis
│  │  │  │  │  ├─ test
│  │  │  │  ├─ applicationContext.xml
│  │  │  ├─ minisMVC-servlet.xml
│  │  │  ├─ web.xml
├─ pom.xml

其中app-minispring下的代码是我们从minis最终代码中编译得到的,都放置在WEB-INF/classes/com目录下,这里就不再依次列出了。

调整web.xml的解析

我们知道,一个应用的启动,依赖于解析web.xml,从这个XML配置文件中指定要启动的Listener和Servlet。我们的目标是通过Listener启动minis的IoC,随后所有的调用都通过DispatcherServlet,转发到应用内部进行处理。

接下来我们先看看一个标准的web.xml具备哪些元素。

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0"
         xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
    http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>applicationContext.xml</param-value>
    </context-param>
    <listener>
        <listener-class>com.minis.web.context.ContextLoaderListener</listener-class>
    </listener>
    <servlet>
        <servlet-name>minisMVC</servlet-name>
        <servlet-class>com.minis.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>minisMVC-servlet.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>minisMVC</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

参考目前我们对web.xml的解析可以发现, <context-param><servlet> 标签下的 <init-param><servlet-mapping> 标签都没有解析,所以这是我们要增加的解析部分。

一个web.xml代表一个应用的配置,对应的容器是Context,所以我们要先在StandardContext中的start()方法内进行改造。

    public void start(){
        Logger logger = new FileLogger();
        setLogger(logger);

        //scan web.xml
        String file = System.getProperty("minit.base") + File.separator +
               this.docbase + File.separator + "WEB-INF" + File.separator + "web.xml";

        SAXReader reader = new SAXReader();
        Document document;
       try {
          document = reader.read(file);
          Element root = document.getRootElement();
          // 解析context-param
          List<Element> contextParams = root.elements("context-param");
          for (Element contextParam : contextParams) {
             Element element = contextParam.element("param-name");
             String paramName = element.getText();
             Element paramValueElement = contextParam.element("param-value");
             String paramValue = paramValueElement.getText();
             initParametersMap.put(paramName, paramValue);
          }
          servletContext = new StandardServletContext(this.docbase, initParametersMap);

          //解析servlet
          List<Element> servlets = root.elements("servlet");
            for (Element servlet : servlets) {
                Element servletname = servlet.element("servlet-name");
                String servletnamestr = servletname.getText();
                Element servletclass = servlet.element("servlet-class");
                String servletclassstr = servletclass.getText();
                //解析init-param
             Element servletInitParamElement = servlet.element("init-param");
             Element servletInitParamNameElement = servletInitParamElement.element("param-name");
             String servletInitParamName = servletInitParamNameElement.getText();
             Element servletInitParamValueElement = servletInitParamElement.element("param-value");
             String servletInitParamValue = servletInitParamValueElement.getText();
             Map<String, String> servletInitParamMap = new ConcurrentHashMap<>();
             servletInitParamMap.put(servletInitParamName, servletInitParamValue);
             servletInitParametersMap.put(servletclassstr, servletInitParamMap);
                Element loadonstartup = servlet.element("load-on-startup");
                String loadonstartupstr = null;
                if (loadonstartup != null) {
                   loadonstartupstr = loadonstartup.getText();
                }

                System.out.println("servlet " + servletnamestr + servletclassstr);
                this.servletClsMap.put(servletnamestr, servletclassstr);
                if (loadonstartupstr != null) {
                   getWrapper(servletnamestr);
                }

            }
          // 解析servlet-mapping
          List<Element> servletMappings = root.elements("servlet-mapping");
          for (Element servletMapping : servletMappings) {
             Element servletname = servletMapping.element("servlet-name");
             String servletnamestr = servletname.getText();
             Element servletclass = servletMapping.element("url-pattern");
             String urlPatternStr = servletclass.getText();
             servletMappingMap.put(urlPatternStr, servletnamestr);
          }
       } catch (DocumentException e) {
          e.printStackTrace();
       }
        System.out.println("Context started.........");
 }

上面的代码列出了对 <context-param><servlet> 标签下的 <init-param><servlet-mapping> 标签的解析,我们分别定义initParametersMap、servletInitParamMap、servletMappingMap三个Map数据结构,来存储解析的数据。后续的程序就可以从这里获取到这些配置信息了。

然后我们再来看看Listener,我们已经说过,Listener是Tomcat和Spring的初始结合点,那么MiniSpring也是在这里与MiniTomcat进行第一次接触的。MiniSpring遵从了Servlet的规范,所以为了让我们的Minit能顺利运行MiniSpring,我们也要适配Servlet规范,支持ServletContextListener。

我们将StandardContext里用到自定义ContainerListener的地方全部替换成ServletContextListener,ServletContextListener是javax.servlet包中定义的接口。所以我们可以启动MiniSpring的ContextLoaderListener,进而启动Minis的IoC。

你可以看一下MiniSpring里对ServletContextListener的contextInitialized方法的实现。

@Override
public void contextInitialized(ServletContextEvent event) {
    initWebApplicationContext(event.getServletContext());
}

在代码实现中,initWebApplicationContext传入了一个ServletContext类型的参数,因此在MiniTomcat中我们需要考虑做一个ServletContext接口的实现类。

在MiniTomcat中定义了StandardServletContext,你可以看一下代码的主体部分。

package com.minit.core;
public class StandardServletContext implements ServletContext {
    private Map<String, String> initParametersMap = new ConcurrentHashMap<>();
    private Map<String, Object> attributeMap = new ConcurrentHashMap<>();
    private String docbase;
    public StandardServletContext() {
    }
    public StandardServletContext(String docbase, Map<String, String> initParametersMap) {
        this.initParametersMap = initParametersMap;
        this.docbase = docbase;
    }
    @Override
    public URL getResource(String s) throws MalformedURLException {
        try {
            URLStreamHandler urlStreamHandler = null;
            File classPath = new File(System.getProperty("minit.base"));
            String repository = (new URL("file", null, classPath.getCanonicalPath() + File.separator)).toString() ;
            repository = repository + this.docbase + File.separator;
            repository = repository + "WEB-INF" + File.separator;
            return new URL(null, repository + s, urlStreamHandler);
        } catch (IOException e) {
            throw new MalformedURLException(e.getMessage());
        }
    }
    @Override
    public String getInitParameter(String s) {
        return initParametersMap.get(s);
    }
    @Override
    public Object getAttribute(String s) {
        return this.attributeMap.get(s);
    }
    @Override
    public void setAttribute(String s, Object o) {
        this.attributeMap.put(s, o);
    }
}

为了遵从规范,需要定义的方法比较多,但我们的实际需求只是简单地将它们运行起来,以此来说明原理,我们只需要实现getResource、getInitParameter、getAttribute、setAttribute就可以了,其他的暂时用不到。后续MiniSpring启动时调用的ServletContext,其内部就是调用MiniTomcat的StandardServletContext。

同理,在MiniToimcat的StandardWrapper里,需要调用loadServlet加载应用的Servlet,在web.xml中我们配置了DispatcherServlet。因而在loadServlet中,有一行 servlet.init(null),此时我们需要进行一定的改造,传入具体的ServletConfig实现,里面包含一个Servlet的元信息,如名字、初始化参数、上下文context等。

所以我们在MiniTomcat中也就要实现一个StandardServletConfig。

package com.minit.core;
public class StandardServletConfig implements ServletConfig {
    private Map<String, String> servletInitParamMap = new ConcurrentHashMap<>();
    private ServletContext servletContext;
    private String servletName;
    public StandardServletConfig(String servletName, ServletContext servletContext, Map<String, String> servletInitParamMap) {
        this.servletInitParamMap = servletInitParamMap;
        this.servletContext = servletContext;
        this.servletName = servletName;
    }
    @Override
    public String getServletName() {
        return servletName;
    }
    @Override
    public ServletContext getServletContext() {
        return this.servletContext;
    }
    @Override
    public String getInitParameter(String s) {
        return servletInitParamMap.get(s);
    }
    @Override
    public Enumeration<String> getInitParameterNames() {
        return null;
    }
}

这个时候StandardWrapper里的loadServlet方法需要改造一下。

public Servlet loadServlet() throws ServletException {
    // Call the initialization method of this servlet
    try {
      servlet.init(new StandardServletConfig(servletClass,
            standardContext.getServletContext(),
            standardContext.getServletInitParametersMap().get(servletClass)));
    }
    catch (Throwable f) {
      throw new ServletException("Failed initialize servlet.");
    }
}

主要改动在于在调用servlet的init()时,将原来传入的null,改为了传入一个ServletConfig,通过这个方法的调用,我们就可以加载MiniSpring中的DispatcherServlet。注意了,这也是系统中唯一一个Servlet。

还有一处需要改动,就是我们之前调用应用的Servlet,会以路径中最后一个分隔符(/)为界,最后面一段字符串代表Servlet的名称。而我们现在调用MiniSpring的时候,则是考虑最后路径为@RequestMapping注解里配置的值,而这个时候这个值并不是Servlet的名称,其实整个系统现在只有一个Servlet,也就是DispatcherServlet,其他的都被MiniSpring封装了。因此在MiniTomcat里,解析请求路径的StandardContextValve类里的invoke方法也需要调整。

你可以看一下这个方法调整之后的样子。

package com.minit.core;
final class StandardContextValve extends ValveBase {
    public void invoke(Request request, Response response, ValveContext valveContext)
        throws IOException, ServletException {
        System.out.println("StandardContextValve invoke()");
        StandardWrapper servletWrapper = null;
        String uri = ((HttpRequestImpl)request).getUri();
        //通过uri拿到pattern
        String servletPattern = uri.substring(uri.lastIndexOf("/"));
        //通过pattern找到合适的servlet名
        String servletName = this.urlMatch(servletPattern);
        StandardContext context = (StandardContext)getContainer();

        servletWrapper = (StandardWrapper)context.getWrapper(servletName);
        try {
           System.out.println("Call service()");
           servletWrapper.invoke(request, response);
        }
        catch (Exception e) {
           System.out.println(e.toString());
        }
        catch (Throwable e) {
           System.out.println(e.toString());
        }
    }
    //简单的匹配规则,以url-pattern开头继任为匹配上
    private String urlMatch(String urlPattern) {
       Map<String, String> servletMappingMap = standardContext.getServletMappingMap();
       Set<String> keySet = servletMappingMap.keySet();
       for (Map.Entry<String, String> entry : servletMappingMap.entrySet()) {
          String key = entry.getKey();
          if (urlPattern.startsWith(key)) {
             return entry.getValue();
          }
       }
       return null;
    }
}

关键的改动在于我们解析了ServletMapping,并要求ServletMapping中定义的ServletName与Servlet标签匹配。这里如果我们指定url-pattern为斜杠(/),表示不做任何拦截。

因此在invoke方法中,我们获取到了最后一段字符串,类似 ”/test2“ 这种,就是调用到Minis的DispatcherServlet中,之后代码逻辑都转到MiniSpring中去处理了。

在这些主要的改动调整完毕后,我们编译部署,你可以看一下运行目录。

MiniTomcat
├─ conf
│  ├─ server.xml
├─ classes
│  ├─ com
│  │  ├─ minit
│  │  │  ├─ connector
│  │  │  │  ├─ http
│  │  │  ├─ core
│  │  │  ├─ loader
│  │  │  ├─ logger
│  │  │  ├─ session
│  │  │  ├─ startup
│  │  │  ├─ util
│  │  │  ├─ valves
├─ lib
├─ logs
├─ webapps
│  ├─ app-minispring
│  │  ├─ WEB-INF
│  │  │  ├─ lib
│  │  │  ├─ classes
│  │  │  │  ├─ com
│  │  │  │  │  ├─ minis
│  │  │  │  │  ├─ test
│  │  │  │  ├─ applicationContext.xml
│  │  │  ├─ minisMVC-servlet.xml
│  │  │  ├─ web.xml
├─ startup.bat

startup.bat是启动程序,直接双击startup.bat启动MiniTomcat和MiniSpring。你从控制台的输出中就能看到IoC和MVC分别启动了。

测试

在浏览器地址栏上输入 http://localhost:8080/app-minispring/test2,可以看到浏览器页面上输出 ”test2,hello world!“,这样就表示MiniSpring顺利地在MiniTomcat中运行起来了。

小结

这节课我们将MiniSpring与MiniTomcat进行集成,构成了一个更大的框架。我们采取的方案是将MiniTomcat作为独立服务器启动,然后把MiniSpring作为一个应用装载在MiniTomcat里。

为了能够集成,我们做的改造工作是按照规范解析web.xml,增加context-param、servlet-mapping、init-param的解析。然后我们实现了ServletContextListener接口,通过Listener MiniTomcat和MiniSpring有了一个最初的结合点。后面我们又实现了ServletConfig,以便初始化DispatcherServlet,同时也不再是通过/servlet/目录来判断是否是一个servlet了,而是通过servlet mapping去获得的。

总之只要符合Servlet规范,就能将Tomcat和Spring集成起来。

这节课代码参见: https://gitee.com/yaleguo1/minit-learning-demo/tree/geek_chapter17

思考题

学完了这节课的内容,你来思考一个问题:我们现在集成后的MiniTomcat + MiniSpring跟实际的Tomcat+Spring结构上还有一些什么区别?你可以举例说明。

欢迎你把你的想法分享到评论区,也欢迎你把这节课的内容分享给其他朋友,我们下节课再见!