Как контейнер в Tomcat обрабатывает запросы

Java Tomcat

предисловие

Предыдущий"Как устроены коннекторы в Tomcat"Представляет структуру коннектора в Tomcat. Мы знаем, что коннектор отвечает за мониторинг сетевых портов, получение запросов на подключение, а затем преобразование запросов, соответствующих стандарту Servlet, и передачу их контейнеру для обработки. Затем наша статья будет следуйте предыдущей статье Идея видеть запрос к контейнеру, как контейнер запрашивает.

Примечание. Версия этой статьи для Tomcat — 9.0.21, и ее не рекомендуется читать читателям с отсчетом от нуля.

Начать с адаптера

Продолжаем следить за предыдущей статьейAdapterИсходный код, продолжаем анализировать, исходный код в конце предыдущей статьи выглядит следующим образом:

  //源码1.类:  CoyoteAdapter implements Adapter
public void service(org.apache.coyote.Request req, org.apache.coyote.Response res)
            throws Exception {

        Request request = (Request) req.getNote(ADAPTER_NOTES);
        Response response = (Response) res.getNote(ADAPTER_NOTES);
        postParseSuccess = postParseRequest(req, request, res, response);
            if (postParseSuccess) {
                //check valves if we support async
                request.setAsyncSupported(
                        connector.getService().getContainer().getPipeline().isAsyncSupported());
                // Calling the container
                connector.getService().getContainer().getPipeline().getFirst().invoke(
                        request, response);
            }
            
    }

Основная функция приведенного выше исходного кода — получить контейнер, а затем вызватьgetPipeline()ПолучатьPipeline, и, наконец, идтиinvokeзвоните, давайте посмотрим на этоPipelineЧто это делает.

//源码2.Pipeline接口
public interface Pipeline extends Contained {
  public Valve getBasic();
  public void setBasic(Valve valve);
  public void addValve(Valve valve);
  public Valve[] getValves();
  public void removeValve(Valve valve);
  public Valve getFirst();
  public boolean isAsyncSupported();
  public void findNonAsyncValves(Set<String> result);
}
//源码3. Valve接口
public interface Valve {
 public Valve getNext();
 public void setNext(Valve valve);
 public void backgroundProcess();
 public void invoke(Request request, Response response)
        throws IOException, ServletException;
 public boolean isAsyncSupported();

мы буквально понимаемPipelineэто труба иValveИменно вентиль, собственно, роль в Tomcat аналогична буквальному значению. Каждый контейнер имеет трубу, которая, в свою очередь, имеет несколько клапанов. Докажем это с помощью следующего анализа.

Трубопровод-клапан

Мы видим, что исходный код вышеPipelineа такжеValveИнтерфейс,PipelineВ основном установленValve,а такжеValveэто связанный список, то вы можете сделатьinvokeвызов метода. Давайте рассмотрим этот исходный код:

//源码4
connector.getService().getContainer().getPipeline().getFirst().invoke(
                        request, response);

Вот конвейер, чтобы получить контейнер напрямую, а затем получить первыйValveпозвонить. мы упоминали ранееValveЭто связанный список, здесь вызывается только первый, то есть последний можно вызвать через Next. Давайте рассмотрим нашу первую статью »Как Tomcat запускается в SpringBoot》 упомянул, что контейнер разделен на 4 подконтейнера, которыеEngine,Host,Context,Wrapper, они также являются отношениями между родителем и ребенком,Engine>Host>Context>Wrapper.

Как я упоминал ранее, каждый контейнер имеет одинPipeline, Так как это проявляется? Это мы можем узнать, посмотрев на исходный код интерфейса контейнера,Pipelineявляется базовым свойством, определяемым интерфейсом контейнера:

//源码5.
public interface Container extends Lifecycle {
    //省略其他代码
  /**
     * Return the Pipeline object that manages the Valves associated with
     * this Container.
     *
     * @return The Pipeline
     */
    public Pipeline getPipeline();
    
}

Мы знаем, что у каждого контейнера есть труба (Pipeline), Есть много трубопроводных клапанов (Valve),ValveМожно сделать цепные вызовы, тогда проблема в конвейере родительского контейнераValveКак вызвать подконтейнерValveШерстяная ткань? существуетPipelineкласс реализацииStandardPipeline, мы нашли следующий исходный код:

 /**
// 源码6.
     * The basic Valve (if any) associated with this Pipeline.
     */
    protected Valve basic = null;
       /**
     * The first valve associated with this Pipeline.
     */
    protected Valve first = null;
    
     public void addValve(Valve valve) {

        //省略部分代码

        // Add this Valve to the set associated with this Pipeline
        if (first == null) {
            first = valve;
            valve.setNext(basic);
        } else {
            Valve current = first;
            while (current != null) {
                //这里循环设置Valve,保证最后一个是basic
                if (current.getNext() == basic) {
                    current.setNext(valve);
                    valve.setNext(basic);
                    break;
                }
                current = current.getNext();
            }
        }

        container.fireContainerEvent(Container.ADD_VALVE_EVENT, valve);
    }

Согласно приведенному выше коду, мы знаемbasicэто трубка(Pipeline), само собой разумеется, что пока последний клапан является первым клапаном следующего контейнера, весь цепной вызов может быть завершен. Мы используем запрос для отладки, чтобы увидеть, совпадает ли он с нашим предположением, мы находимся вCoyoteAdapterсерединаserviceСтавим точку останова в методе, эффект следующий:

Здесь мы можем знать, что когда адаптер вызывает контейнер, он вызываетсяEngineВ трубопроводе имеется только один вентиль, то есть основной, значение которого равноStandardEngineValve. Мы обнаружили, что метод вызова этого клапана выглядит следующим образом:

//源码7.
public final void invoke(Request request, Response response)
        throws IOException, ServletException {

        // Select the Host to be used for this Request
        Host host = request.getHost();
        if (host == null) {
            // HTTP 0.9 or HTTP 1.0 request without a host when no default host
            // is defined. This is handled by the CoyoteAdapter.
            return;
        }
        if (request.isAsyncSupported()) {
            request.setAsyncSupported(host.getPipeline().isAsyncSupported());
        }

        // Ask this Host to process this request
        host.getPipeline().getFirst().invoke(request, response);
    }

Мы продолжаем отлаживать, чтобы увидеть следующие результаты:

Итак, вотbasicна самом деле позвонитHostКонтейнерные трубы (Pipeline) и клапан (Valve), то есть в каждом контейнерном конвейереbasicклапан, отвечающий за вызов следующего подконтейнера. Я использую изображение для представления:

На этом рисунке четко показано, как контейнер внутри Tomcat передает запросы от коннектора (Connector) входящие запросы будут поступатьEngineконтейнер,Engineчерез трубу(Pieline) в клапане (Valve) для цепных вызовов, последнийbasicКлапан отвечает за вызов первого клапана следующего контейнера и вызывается до тех пор, покаWrapper,ПотомWrapperповторно выполнитьServlet.

Давайте посмотримWrapperИсходный код, это действительно так, как мы говорим:

//源码8.
 public final void invoke(Request request, Response response)
        throws IOException, ServletException {
            //省略部分源码
        Servlet servlet = null;
        if (!unavailable) {
            servlet = wrapper.allocate();
        }
            
        // Create the filter chain for this request
        ApplicationFilterChain filterChain =
                ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
                
         filterChain.doFilter(request.getRequest(),
                                    response.getResponse());        
        }

Увидев это, вы можете сказать, что это просто создание фильтра (Filter) и называть его, а не называтьServlet, да тут действительно не до звонкаServlet, но мы знаем, что фильтр (Filter) вServletвыполнено ранее, т.filterChain.doFilterПосле выполнения он будет выполненServlet. Давайте посмотримApplicationFilterChainИсходный код, как мы сказали:

//源码9.
 public void doFilter(ServletRequest request, ServletResponse response)
        throws IOException, ServletException {
        //省略部分代码
        internalDoFilter(request,response);
    }
//源码10.  
 private void internalDoFilter(ServletRequest request,
                                  ServletResponse response)
        throws IOException, ServletException {
        //省略部分代码
        // Call the next filter if there is one
        if (pos < n) {
         //省略部分代码
            ApplicationFilterConfig filterConfig = filters[pos++];
            Filter filter = filterConfig.getFilter();
            filter.doFilter(request, response, this);
            return;
        }
        //调用servlet
        // We fell off the end of the chain -- call the servlet instance
        servlet.service(request, response);
        
         

Через исходный код мы обнаружили, что после вызова всех фильтров (Filter)Позже,servletначать звонитьservice. Давайте посмотримservletкласс реализации

Здесь мы знакомыHttpServletа такжеGenericServletдаTomcatкласс пакета, который на самом деле имеет толькоHttpServlet,потому чтоGenericServletдаHttpServletродительский класс. Последний передается фреймворку для обработки, и здесь запрос внутри Tomcat завершается.

Реализация изоляции нескольких приложений в Tomcat

Мы знаем, что Tomcat поддерживает развертывание нескольких приложений, так как же Tomcat поддерживает развертывание нескольких приложений? Как убедиться, что не будет путаницы между несколькими приложениями? Чтобы понять это, нам все же придется вернуться к адаптеру, вернуться кserviceметод

//源码11.类:CoyoteAdapter
public void service(org.apache.coyote.Request req, org.apache.coyote.Response res)
            throws Exception {
            //省略部分代码
            // Parse and set Catalina and configuration specific
            // request parameters
            //处理URL映射
            postParseSuccess = postParseRequest(req, request, res, response);
            if (postParseSuccess) {
                //check valves if we support async
                request.setAsyncSupported(
                        connector.getService().getContainer().getPipeline().isAsyncSupported());
                // Calling the container
                connector.getService().getContainer().getPipeline().getFirst().invoke(
                        request, response);
            }
}

Мы говорили только об этом в предыдущем исходном кодеconnector.getService().getContainer().getPipeline().getFirst().invoke( request, response)Этот код, эта часть кода предназначена для вызова контейнера, но перед вызовом контейнера естьpostParseRequestЭтот метод используется для обработки запроса сопоставления, давайте посмотрим на исходный код:

//源码12.类:CoyoteAdapter
 protected boolean postParseRequest(org.apache.coyote.Request req, Request request,
            org.apache.coyote.Response res, Response response) throws IOException, ServletException {
        省略部分代码
        boolean mapRequired = true;
         while (mapRequired) {
            // This will map the the latest version by default
            connector.getService().getMapper().map(serverName, decodedURI,
                    version, request.getMappingData());
            //没有找到上下文就报404错误        
            if (request.getContext() == null) {
                // Don't overwrite an existing error
                if (!response.isError()) {
                    response.sendError(404, "Not found");
                }
                // Allow processing to continue.
                // If present, the error reporting valve will provide a response
                // body.
                return true;
            }        
            }

Вот цикл обработки сопоставления URL, еслиContextЕсли он не найден, возвращает ошибку 404, и мы продолжаем смотреть исходный код:

//源码13.类:Mapper
public void map(MessageBytes host, MessageBytes uri, String version,
                    MappingData mappingData) throws IOException {

        if (host.isNull()) {
            String defaultHostName = this.defaultHostName;
            if (defaultHostName == null) {
                return;
            }
            host.getCharChunk().append(defaultHostName);
        }
        host.toChars();
        uri.toChars();
        internalMap(host.getCharChunk(), uri.getCharChunk(), version, mappingData);
    }
    //源码14.类:Mapper
 private final void internalMap(CharChunk host, CharChunk uri,
            String version, MappingData mappingData) throws IOException {
        //省略部分代码
        // Virtual host mapping 处理Host映射
        MappedHost[] hosts = this.hosts;
        MappedHost mappedHost = exactFindIgnoreCase(hosts, host);
      
         //省略部分代码
        if (mappedHost == null) {
             mappedHost = defaultHost;
            if (mappedHost == null) {
                return;
            }
        }
    
        mappingData.host = mappedHost.object;
        
        // Context mapping 处理上下文映射
        ContextList contextList = mappedHost.contextList;
        MappedContext[] contexts = contextList.contexts;
        //省略部分代码
        if (context == null) {
            return;
        }
        mappingData.context = contextVersion.object;
        mappingData.contextSlashCount = contextVersion.slashCount;

        // Wrapper mapping 处理Servlet映射
        if (!contextVersion.isPaused()) {
            internalMapWrapper(contextVersion, uri, mappingData);
        }

    }    

Поскольку приведенный выше исходный код относительно велик, я пропустил большую часть кода и оставил код, который может понять основную логику.В общем, обработка URL включает в себя три части, сопоставлениеHost, отображениеContextи отображениеServlet(В целях экономии места заинтересованные студенты должны изучить конкретные детали исходного кода).

Здесь мы можем найти деталь, то есть три логики обработки тесно связаны, толькоHostОн будет обработан, если он не пуст.Context,дляServletТо же самое справедливо. Так вот мы простоHostЕсли конфигурация другая, то и все последующие подконтейнеры другие, что дополняет эффект изоляции приложения. Однако для встроенного в SpringBoot метода Tomcat (начиная с пакета jar) нет режима для реализации нескольких приложений, и само приложение является Tomcat.

Для простоты понимания я также нарисовал схему мультиприложения изоляции, здесь мы предполагаем, что есть два доменных имениadmin.luozhou.comа такжеweb.luozhou.comЗатем я развертываю 2 приложения под каждым доменным именем, а именноUser,log,blog,shop. Затем, когда я думаю о добавлении пользователей, я прошуadmin.luozhou.comпод доменным именемUserизContextпоследующийaddСервлет(Примечание: дизайн примера здесь не соответствует фактическому принципу разработки.Дробность добавления должна выполняться контроллером в фреймворке, а не сервлетом.).

Суммировать

В этой статье мы изучили, как контейнер в Tomcat обрабатывает запросы. Давайте рассмотрим следующее:

  • Коннектор бросает запрос адаптеру и вызывает контейнер (Engine)
  • Внутри контейнер через трубу (Pieline)-клапан(Valve) для завершения вызова контейнера родительский контейнер вызывает дочерний контейнер в основном черезbasicклапан до конца.
  • последний подконтейнерwrapperПосле завершения вызова будет создан фильтр для выполнения вызова фильтра.После завершения вызова последним шагом внутри Tomcat является вызов сервлета. Также можно понять наше обычно используемоеHttpServlet, все на основеServletЗдесь канонический фреймворк входит в процесс фреймворка (включая SpringBoot).
  • Наконец, мы также проанализировали, как Tomcat обеспечивает изоляцию нескольких приложений.Благодаря анализу изоляции нескольких приложений мы также понимаем, почему Tomcat необходимо проектировать так много подконтейнеров.Несколько подконтейнеров могут обеспечивать разную степень детализации уровней изоляции в зависимости от потребностей. Требования к сцене.

Заявление об авторских правах: Оригинальная статья, пожалуйста, укажите источник для перепечатки.