Tomcat 源码解析一请求处理的整体过程-黄泉天怒(下)

news2024/11/20 13:24:31

上一篇博客 Tomcat 源码解析一请求处理的整体过程-黄泉天怒(上)

NonLoginAuthenticator

  NonLoginAuthenticator并没有实现invoke()方法,而是由父类AuthenticatorBase实现了invoke()方法,AuthenticatorBase类关系如下。
在这里插入图片描述
  先弄清楚NonLoginAuthenticator这个管道是在何时被加入到StandardContext的Pipeline里的呢? 在StandardPipeline的addValve()方法中打断点。
在这里插入图片描述

  最终发现在configureStart()方法中调用了authenticatorConfig(),实现了向StandardContext的Pipeline中添加Valve。
在这里插入图片描述
  大家可能感到迷惑,什么时候调用configureStart()方法呢?这个需要去看之前的Tomcat 源码解析一容器加载-大寂灭指 相关的三篇博客了。 这里就不深入,先进入authenticatorConfig()方法 。

/**
 * Set up an Authenticator automatically if required, and one has not
 * already been configured.
 * 基于解析完Web容器,检测Web应用部署描述中使用的安全角色名,当发现使用示定义的角色时,提示警告将未定义的角色添加到Context 安全角色列表中。
 */
protected void authenticatorConfig() {
    LoginConfig loginConfig = context.getLoginConfig();
    if (loginConfig == null) {
        // Need an authenticator to support HttpServletRequest.login()
        loginConfig = DUMMY_LOGIN_CONFIG;
        context.setLoginConfig(loginConfig);
    }

    // Has an authenticator been configured already?
    if (context.getAuthenticator() != null)
        return;

    if (!(context instanceof ContainerBase)) {
        return;     // Cannot install a Valve even if it would be needed
    }

    // Has a Realm been configured for us to authenticate against?
    if (context.getRealm() == null) {
        log.error(sm.getString("contextConfig.missingRealm"));
        ok = false;
        return;
    }

    /*
     * First check to see if there is a custom mapping for the login
     * method. If so, use it. Otherwise, check if there is a mapping in
     * org/apache/catalina/startup/Authenticators.properties.
     */
    Valve authenticator = null;
    if (customAuthenticators != null) {
        authenticator = (Valve)
            customAuthenticators.get(loginConfig.getAuthMethod());
    }
    // 当Context 需要进行安全认证但是没有指定具体的Authenticator时,根据服务器配置自动创建默认的实例。
    if (authenticator == null) {
        if (authenticators == null) {
            log.error(sm.getString("contextConfig.authenticatorResources"));
            ok = false;
            return;
        }

        // Identify the class name of the Valve we should configure
        String authenticatorName = null;
        authenticatorName =
                authenticators.getProperty(loginConfig.getAuthMethod());
        if (authenticatorName == null) {
            log.error(sm.getString("contextConfig.authenticatorMissing",
                             loginConfig.getAuthMethod()));
            ok = false;
            return;
        }

        // Instantiate and install an Authenticator of the requested class
        try {
            Class<?> authenticatorClass = Class.forName(authenticatorName);
            authenticator = (Valve) authenticatorClass.newInstance();
        } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);
            log.error(sm.getString(
                                "contextConfig.authenticatorInstantiate",
                                authenticatorName),
                      t);
            ok = false;
        }
    }

    if (authenticator != null && context instanceof ContainerBase) {
        Pipeline pipeline = ((ContainerBase) context).getPipeline();
        if (pipeline != null) {
            ((ContainerBase) context).getPipeline().addValve(authenticator);
            if (log.isDebugEnabled()) {
                log.debug(sm.getString(
                                "contextConfig.authenticatorConfigured",
                                loginConfig.getAuthMethod()));
            }
        }
    }
}

  想弄清楚上面代码,第一要看懂LoginConfig的由来,默认LoginConfig为protected static final LoginConfigDUMMY_LOGIN_CONFIG =
new LoginConfig(“NONE”, null, null, null); ,第二点明白authenticators属性什么时候赋值,赋的值是什么?请看ContextConfig的静态代码块。

static {
    // Load our mapping properties for the standard authenticators
    Properties props = new Properties();
    InputStream is = null;
    try {
        is = ContextConfig.class.getClassLoader().getResourceAsStream(
                "org/apache/catalina/startup/Authenticators.properties");
        if (is != null) {
            props.load(is);
        }
    } catch (IOException ioe) {
        props = null;
    } finally {
        if (is != null) {
            try {
                is.close();
            } catch (IOException e) {
            }
        }
    }
	authenticators = props;
    // Load the list of JARS to skip
    addJarsToSkip(Constants.DEFAULT_JARS_TO_SKIP);
    addJarsToSkip(Constants.PLUGGABILITY_JARS_TO_SKIP);
}

  authenticators属性来源于org/apache/catalina/startup/Authenticators.properties文件,而Authenticators.properties的文件内容为
在这里插入图片描述
  此时再来看authenticatorConfig()方法就简单多了,默认情况下loginConfig的authMethod值为NONE,因此从authenticators获取到的配置类名为org.apache.catalina.authenticator.NonLoginAuthenticator,再用反射创建NonLoginAuthenticator对象,添加到StandardContext的Pipeline的Valve中。

public void invoke(Request request, Response response) throws IOException, ServletException {
    if (log.isDebugEnabled()) {
        log.debug("Security checking request " + request.getMethod() + " " +
                request.getRequestURI());
    }
    LoginConfig config = this.context.getLoginConfig();

    // Have we got a cached authenticated Principal to record?
    if (cache) {
        Principal principal = request.getUserPrincipal();
        if (principal == null) {
            Session session = request.getSessionInternal(false);
            if (session != null) {
                principal = session.getPrincipal();
                if (principal != null) {
                    if (log.isDebugEnabled()) {
                        log.debug("We have cached auth type " + session.getAuthType() +
                                " for principal " + principal);
                    }
                    request.setAuthType(session.getAuthType());
                    request.setUserPrincipal(principal);
                }
            }
        }
    }

    boolean authRequired = isContinuationRequired(request);

    Realm realm = this.context.getRealm();
    // Is this request URI subject to a security constraint?
    SecurityConstraint[] constraints = realm.findSecurityConstraints(request, this.context);

    if (constraints == null && !context.getPreemptiveAuthentication() && !authRequired) {
        if (log.isDebugEnabled()) {
            log.debug(" Not subject to any constraint");
        }
        getNext().invoke(request, response);
        return;
    }
    // Make sure that constrained resources are not cached by web proxies
    // or browsers as caching can provide a security hole
    if (constraints != null && disableProxyCaching &&
            !"POST".equalsIgnoreCase(request.getMethod())) {
        if (securePagesWithPragma) {
            // Note: These can cause problems with downloading files with IE
            response.setHeader("Pragma", "No-cache");
            response.setHeader("Cache-Control", "no-cache");
        } else {
            response.setHeader("Cache-Control", "private");
        }
        response.setHeader("Expires", DATE_ONE);
    }

    if (constraints != null) {
        // Enforce any user data constraint for this security constraint
        if (log.isDebugEnabled()) {
            log.debug(" Calling hasUserDataPermission()");
        }
        if (!realm.hasUserDataPermission(request, response, constraints)) {
            if (log.isDebugEnabled()) {
                log.debug(" Failed hasUserDataPermission() test");
            }
            /*
             * ASSERT: Authenticator already set the appropriate HTTP status
             * code, so we do not have to do anything special
             */
            return;
        }
    }

    // Since authenticate modifies the response on failure,
    // we have to check for allow-from-all first.
    boolean hasAuthConstraint = false;
    if (constraints != null) {
        hasAuthConstraint = true;
        for (int i = 0; i < constraints.length && hasAuthConstraint; i++) {
            if (!constraints[i].getAuthConstraint()) {
                hasAuthConstraint = false;
            } else if (!constraints[i].getAllRoles()) {
                String[] roles = constraints[i].findAuthRoles();
                if (roles == null || roles.length == 0) {
                    hasAuthConstraint = false;
                }
            }
        }
    }

    if (!authRequired && hasAuthConstraint) {
        authRequired = true;
    }

    if (!authRequired && context.getPreemptiveAuthentication()) {
        authRequired =
                request.getCoyoteRequest().getMimeHeaders().getValue("authorization") != null;
    }

    if (!authRequired && context.getPreemptiveAuthentication() &&
            HttpServletRequest.CLIENT_CERT_AUTH.equals(getAuthMethod())) {
        X509Certificate[] certs = getRequestCertificates(request);
        authRequired = certs != null && certs.length > 0;
    }

    if (authRequired) {
        if (log.isDebugEnabled()) {
            log.debug(" Calling authenticate()");
        }
        if (!authenticate(request, response, config)) {
            if (log.isDebugEnabled()) {
                log.debug(" Failed authenticate() test");
            }
            /*
             * ASSERT: Authenticator already set the appropriate HTTP status
             * code, so we do not have to do anything special
             */
            return;
        }

    }

    if (constraints != null) {
        if (log.isDebugEnabled()) {
            log.debug(" Calling accessControl()");
        }
        if (!realm.hasResourcePermission(request, response, constraints, this.context)) {
            if (log.isDebugEnabled()) {
                log.debug(" Failed accessControl() test");
            }
            /*
             * ASSERT: AccessControl method has already set the appropriate
             * HTTP status code, so we do not have to do anything special
             */
            return;
        }
    }

    // Any and all specified constraints have been satisfied
    if (log.isDebugEnabled()) {
        log.debug(" Successfully passed all security constraints");
    }
    getNext().invoke(request, response);
}

  因此对于NonLoginAuthenticator而方,走的是上面加粗代码。对于Tomcat权限这一块,目前不做深入分析,当然,如果想修改默认配置NonLoginAuthenticator,可以在WEB-INF/web.xml中添加login-config标签。如下图所示
在这里插入图片描述

  最终在解析web.xml时,添加到StandardContext中。
在这里插入图片描述
  大家可能感到好奇,我是怎么找到这样使用的呢?首先找到StandardContext的setLoginConfig()方法。 发现在WebXml使用了setLoginConfig()
在这里插入图片描述
  WebXml的loginConfig最终来源于其setLoginConfig方法 。我们知道WebXml是web.xml的封装,而在Tomcat中所有的xml的解析都是由Degister框架,根据Degister框架的特性,肯定配置了"setLoginConfig" 字符串,因此全局搜索"setLoginConfig"。
在这里插入图片描述
  终于找到了login-config的配置,本例中只配置了auth-method,还可以倒置realm-name, form-login-config/form-error-page, form-login-config/form-login-page等信息。 接下来分析StandardContextValve类。

StandardContextValve

  接下来看StandardContextValve的内部实现

public final void invoke(Request request, Response response)
    throws IOException, ServletException {

    // Disallow any direct access to resources under WEB-INF or META-INF
    // 当然禁止访问META-INF和WEB-INF的内容,如果访问,则抛出404异常
    MessageBytes requestPathMB = request.getRequestPathMB();
    if ((requestPathMB.startsWithIgnoreCase("/META-INF/", 0))
            || (requestPathMB.equalsIgnoreCase("/META-INF"))
            || (requestPathMB.startsWithIgnoreCase("/WEB-INF/", 0))
            || (requestPathMB.equalsIgnoreCase("/WEB-INF"))) {
        response.sendError(HttpServletResponse.SC_NOT_FOUND);
        return;
    }
    // Select the Wrapper to be used for this Request
    // 如果Wrapper不存在或者Wrapper不可用,则抛出404异常
    Wrapper wrapper = request.getWrapper();
    if (wrapper == null || wrapper.isUnavailable()) {
        response.sendError(HttpServletResponse.SC_NOT_FOUND);
        return;
    }
    
    // Acknowledge the request
    // 向客户端发送ack事件 
    try {
        response.sendAcknowledgement();
    } catch (IOException ioe) {
        container.getLogger().error(sm.getString(
                "standardContextValve.acknowledgeException"), ioe);
        request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, ioe);
        // 如果抛出异常,则返回505错误码
        response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        return;
    }

    if (request.isAsyncSupported()) {
        request.setAsyncSupported(wrapper.getPipeline().isAsyncSupported());
    }
    wrapper.getPipeline().getFirst().invoke(request, response);
}

  接下来进入StandardWrapperValve的invoke()方法。

StandardWrapperValve
/**
 * StandardWrapperValve 是 StandardWrapper 实例上的基本阀门,该阀门做两件 事情:
 *  1.提交 Servlet 的所有相关过滤器
 *  2.调用发送者的 service 方法要实现这些内容,下面是 StandardWrapperValve 在他的 invoke 方法要实现的:
 *  3.调用 StandardWrapper 的 allocate 的方法来获得一个 servlet 实例
 *  4.调用它的 private createFilterChain 方法获得过滤链
 *  5.调用过滤器链的 doFilter 方法。包括调用 servlet 的 service 方法
 *  6.释放过滤器链
 *  7.调用包装器的 deallocate 方法
 *  8.如果 Servlet 无法使用了,调用包装器的 unload 方法
 */
@Override
public final void invoke(Request request, Response response)
    throws IOException, ServletException {

    // Initialize local variables we may need
    boolean unavailable = false;
    Throwable throwable = null;
    // This should be a Request attribute...
    long t1=System.currentTimeMillis();
    requestCount.incrementAndGet();
    StandardWrapper wrapper = (StandardWrapper) getContainer(); // 属于哪个Wrapper
    Servlet servlet = null;
    Context context = (Context) wrapper.getParent();  // 属于哪个Context

    // Check for the application being marked unavailable
    if (!context.getState().isAvailable()) {
    	// 如果容器的状态不可用,则返回503
        response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE,
                       sm.getString("standardContext.isUnavailable"));
        unavailable = true;
    }
    
    // Check for the servlet being marked unavailable
    // 如果Context可用,但是Wrapper不可用, 在定义servlet时,可以设置enabled
    if (!unavailable && wrapper.isUnavailable()) {
        container.getLogger().info(sm.getString("standardWrapper.isUnavailable",
                wrapper.getName()));
        long available = wrapper.getAvailable();
        if ((available > 0L) && (available < Long.MAX_VALUE)) {
            response.setDateHeader("Retry-After", available);
            response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE,
                    sm.getString("standardWrapper.isUnavailable",
                            wrapper.getName()));
        } else if (available == Long.MAX_VALUE) {
            response.sendError(HttpServletResponse.SC_NOT_FOUND,
                    sm.getString("standardWrapper.notFound",
                            wrapper.getName()));
        }
        unavailable = true;
    }

    // Allocate a servlet instance to process this request
    try {
        if (!unavailable) {
        	// 实例化,初始化servlet 
            servlet = wrapper.allocate();
        }
    } catch (UnavailableException e) {
        container.getLogger().error(
                sm.getString("standardWrapper.allocateException",
                        wrapper.getName()), e);
        long available = wrapper.getAvailable();
        if ((available > 0L) && (available < Long.MAX_VALUE)) {
            response.setDateHeader("Retry-After", available);
            response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE,
                       sm.getString("standardWrapper.isUnavailable",
                                    wrapper.getName()));
        } else if (available == Long.MAX_VALUE) {
            response.sendError(HttpServletResponse.SC_NOT_FOUND,
                       sm.getString("standardWrapper.notFound",
                                    wrapper.getName()));
        }
    } catch (ServletException e) {
        container.getLogger().error(sm.getString("standardWrapper.allocateException",
                         wrapper.getName()), StandardWrapper.getRootCause(e));
        throwable = e;
        exception(request, response, e);
    } catch (Throwable e) {
        ExceptionUtils.handleThrowable(e);
        container.getLogger().error(sm.getString("standardWrapper.allocateException",
                         wrapper.getName()), e);
        throwable = e;
        exception(request, response, e);
        servlet = null;
    }

    // Identify if the request is Comet related now that the servlet has been allocated
    boolean comet = false;
    if (servlet instanceof CometProcessor && Boolean.TRUE.equals(request.getAttribute(
            Globals.COMET_SUPPORTED_ATTR))) {
        comet = true;
        request.setComet(true);
    }

    MessageBytes requestPathMB = request.getRequestPathMB();
    DispatcherType dispatcherType = DispatcherType.REQUEST;
    if (request.getDispatcherType()==DispatcherType.ASYNC) dispatcherType = DispatcherType.ASYNC;
    request.setAttribute(Globals.DISPATCHER_TYPE_ATTR,dispatcherType);
    request.setAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR,
            requestPathMB);
    // Create the filter chain for this request
    ApplicationFilterFactory factory =
        ApplicationFilterFactory.getInstance();

    // 最重要的方法是 createFilterChain 方法并调用过滤器链的 doFilter 方法。方 法 createFilterChain 创建了一个
    // ApplicationFilterChain 实例,并将所有的 过滤器添加到上面。ApplicationFilterChain 类将在下面的小节中介绍。
    // 要完 全的理解这个类,还需要理解 FilterDef 和 ApplicationFilterConfig 类。这些 内容将在下面介绍
    ApplicationFilterChain filterChain =
        factory.createFilterChain(request, wrapper, servlet);

    // Reset comet flag value after creating the filter chain
    request.setComet(false);

    // Call the filter chain for this request
    // NOTE: This also calls the servlet's service() method
    try {
        if ((servlet != null) && (filterChain != null)) {
            // Swallow output if needed
            if (context.getSwallowOutput()) {
                try {
                    SystemLogHandler.startCapture();
                    if (request.isAsyncDispatching()) {
                        request.getAsyncContextInternal().doInternalDispatch();
                    } else if (comet) {
                        filterChain.doFilterEvent(request.getEvent());
                        request.setComet(true);
                    } else {
                        filterChain.doFilter(request.getRequest(),
                                response.getResponse());
                    }
                } finally {
                    String log = SystemLogHandler.stopCapture();
                    if (log != null && log.length() > 0) {
                        context.getLogger().info(log);
                    }
                }
            } else {
                if (request.isAsyncDispatching()) {
                    request.getAsyncContextInternal().doInternalDispatch();
                } else if (comet) {
                    request.setComet(true);
                    filterChain.doFilterEvent(request.getEvent());
                } else {
                    filterChain.doFilter
                        (request.getRequest(), response.getResponse());
                }
            }

        }
    } catch (ClientAbortException e) {
        throwable = e;
        exception(request, response, e);
    } catch (IOException e) {
        container.getLogger().error(sm.getString(
                "standardWrapper.serviceException", wrapper.getName(),
                context.getName()), e);
        throwable = e;
        exception(request, response, e);
    } catch (UnavailableException e) {
        container.getLogger().error(sm.getString(
                "standardWrapper.serviceException", wrapper.getName(),
                context.getName()), e);
        //            throwable = e;
        //            exception(request, response, e);
        wrapper.unavailable(e);
        long available = wrapper.getAvailable();
        if ((available > 0L) && (available < Long.MAX_VALUE)) {
            response.setDateHeader("Retry-After", available);
            response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE,
                       sm.getString("standardWrapper.isUnavailable",
                                    wrapper.getName()));
        } else if (available == Long.MAX_VALUE) {
            response.sendError(HttpServletResponse.SC_NOT_FOUND,
                        sm.getString("standardWrapper.notFound",
                                    wrapper.getName()));
        }
        // Do not save exception in 'throwable', because we
        // do not want to do exception(request, response, e) processing
    } catch (ServletException e) {
        Throwable rootCause = StandardWrapper.getRootCause(e);
        if (!(rootCause instanceof ClientAbortException)) {
            container.getLogger().error(sm.getString(
                    "standardWrapper.serviceExceptionRoot",
                    wrapper.getName(), context.getName(), e.getMessage()),
                    rootCause);
        }
        throwable = e;
        exception(request, response, e);
    } catch (Throwable e) {
        ExceptionUtils.handleThrowable(e);
        container.getLogger().error(sm.getString(
                "standardWrapper.serviceException", wrapper.getName(),
                context.getName()), e);
        throwable = e;
        exception(request, response, e);
    }

    // Release the filter chain (if any) for this request
    if (filterChain != null) {
        if (request.isComet()) {
            // If this is a Comet request, then the same chain will be used for the
            // processing of all subsequent events.
            filterChain.reuse();
        } else {
            filterChain.release();
        }
    }

    // Deallocate the allocated servlet instance
    try {
        if (servlet != null) {
            wrapper.deallocate(servlet);
        }
    } catch (Throwable e) {
        ExceptionUtils.handleThrowable(e);
        container.getLogger().error(sm.getString("standardWrapper.deallocateException",
                         wrapper.getName()), e);
        if (throwable == null) {
            throwable = e;
            exception(request, response, e);
        }
    }

    // If this servlet has been marked permanently unavailable,
    // unload it and release this instance
    try {
        if ((servlet != null) &&
            (wrapper.getAvailable() == Long.MAX_VALUE)) {
            wrapper.unload();
        }
    } catch (Throwable e) {
        ExceptionUtils.handleThrowable(e);
        container.getLogger().error(sm.getString("standardWrapper.unloadException",
                         wrapper.getName()), e);
        if (throwable == null) {
            throwable = e;
            exception(request, response, e);
        }
    }
    long t2=System.currentTimeMillis();

    long time=t2-t1;
    processingTime += time;
    if( time > maxTime) maxTime=time;
    if( time < minTime) minTime=time;

}

  先来看Servlet的实例化初始化方法

public Servlet allocate() throws ServletException {
    // If we are currently unloading this servlet, throw an exception
    if (unloading) {
        throw new ServletException(sm.getString("standardWrapper.unloading", getName()));
    }
    boolean newInstance = false;
    // If not SingleThreadedModel, return the same instance every time
    if (!singleThreadModel) {
        // Load and initialize our instance if necessary
        if (instance == null || !instanceInitialized) {
            synchronized (this) {
                if (instance == null) {
                    try {
                        if (log.isDebugEnabled()) {
                            log.debug("Allocating non-STM instance");
                        }
                        // Note: We don't know if the Servlet implements
                        // SingleThreadModel until we have loaded it.
                        // 一个Servlet只有被加载后才能知道是不是实现了SingleThreadModel接口
                        instance = loadServlet();
                        newInstance = true;
                        // 如果没有继承singleThreadModel接口
                        if (!singleThreadModel) {
                            // For non-STM, increment here to prevent a race
                            // condition with unload. Bug 43683, test case
                            // #3
                            // 分配实例的次数+1
                            countAllocated.incrementAndGet();
                        }
                    } catch (ServletException e) {
                        throw e;
                    } catch (Throwable e) {
                        ExceptionUtils.handleThrowable(e);
                        throw new ServletException(sm.getString("standardWrapper.allocate"), e);
                    }
                }
                if (!instanceInitialized) {
                    initServlet(instance);
                }
            }
        }
        
        if (singleThreadModel) {
            // 新生成了一个实例后,把实例放入instancePool
            if (newInstance) {
                // Have to do this outside of the sync above to prevent a
                // possible deadlock
                synchronized (instancePool) {
                    instancePool.push(instance);
                    nInstances++;
                }
            }
        } else {
            if (log.isTraceEnabled()) {
                log.trace("  Returning non-STM instance");
            }
            // For new instances, count will have been incremented at the
            // time of creation
            if (!newInstance) {
                // 分配实例的次数加1,如果是新创的实例,在上面就会加1
                countAllocated.incrementAndGet();
            }
            return instance;
        }
    }

    synchronized (instancePool) {
        // countAllocated表示当前需要的实例数
        // nInstances表示当前已经生成的实例数
        // 如果需要的大于或等于存在的实例数,那么则要新生成了,如果已经超过了最大限制,就只能等其他线程释放servlet了
        while (countAllocated.get() >= nInstances) {
            // Allocate a new instance if possible, or else wait
            // 如果现在生成的实例小于最大限制,则继续生成
            if (nInstances < maxInstances) {
                try {
                    instancePool.push(loadServlet());
                    nInstances++;
                } catch (ServletException e) {
                    throw e;
                } catch (Throwable e) {
                    ExceptionUtils.handleThrowable(e);
                    throw new ServletException(sm.getString("standardWrapper.allocate"), e);
                }
            } else {
                // 否则等等
                try {
                    instancePool.wait();
                } catch (InterruptedException e) {
                    // Ignore
                }
            }
        }
        if (log.isTraceEnabled()) {
            log.trace("  Returning allocated STM instance");
        }
        // 分配次数+1,直接从instancePool取
        countAllocated.incrementAndGet();
        return instancePool.pop();
    }
}
Servlet对象池

  Servlet在不实现SingleThreadModel的情况下以单例实例模式运行,如图10.3所示,这种情况下,Wrapper容器只通过反射实例化一个Servlet对象,对此,Servlet的所有客户端请求都会共用此Servlet对象,而对于多个客户端请求Tomcat会使用多线程处理,所以要注意保证此Servlet对象的线程安全,多个线程不管执行顺序如何都能保证执行结果的正确性,关于线程安全问题,这里举一个刚做Web开发可能会犯的错误,在某个Servlet使用成员变量累加统计访问次数,这就存在线程安全问题。
在这里插入图片描述

  为了支持一个Servlet对象对一个线程,Servlet规范提出了一个SingleThreadModel接口,Tomcat容器必须要完成的机制是, 如果某个Servlet类实现了SingleThreadModel接口,则要保证一个线程独占一个Servlet对象,假如线程1正在使用Servlet1对象,则线程2不能再使用Servlet1对象,只能使用Servlet2对象 。

  针对SingleThreadModel模式,Tomcat 的Wrapper 容器使用了对象池策略, Wrapper容器会有一个Servlet堆 ,负责保证若干个Servlet对象,当需要Servlet对象时从堆中pop()出一个对象,而当用完后则push回堆中,Wrapper 容器中最多可以有20个某个Servlet类对象,例如XXXServlet类的对象池, 已有20个线程占用了20个对象,于是第21个线程执行时就会因此阻塞而等待,直至对象池中有可用的对象才继续执行。

  整个流程如图10.4所示,某个线程处理客户端请求时,它首先尝试从Servlet对象池中获取Servlet对象,此时如果对象池中可用的对象则直接返回一个对象,如果不够,则使用则继续实例化Servlet对象并push到对象池中, 但Servlet对象的总数量必须保证在20个以内, 如果20个Servlet对象都被其他线程使用了, 那么就必须要等到其他线程用完放回后才能获取,此时该线程会一直等待,从对象池中获取到Servlet对象后则调用Servlet对象的service()方法对客户端请求进行处理,处理完后将Servlet对象放回对象池中。
在这里插入图片描述

  本节介绍了Servlet对象池, 它是为了支持Servlet规范SingleThreadModel接口而引入的它就是一个栈结构,需要时就pop出一个对象,使用完就push回去,请看下面代码实现 。

@Override
public void deallocate(Servlet servlet) throws ServletException {
    // If not SingleThreadModel, no action is required
    if (!singleThreadModel) {
        // 分配次数-1
        countAllocated.decrementAndGet();
        return;
    }

    // Unlock and free this instance
    synchronized (instancePool) {
        // 分配次数-1,
        countAllocated.decrementAndGet();
        instancePool.push(servlet);
        instancePool.notify();
    }
}
public synchronized Servlet loadServlet() throws ServletException {

    if (unloading) {
        throw new ServletException(
                sm.getString("standardWrapper.unloading", getName()));
    }

    // Nothing to do if we already have an instance or an instance pool
    if (!singleThreadModel && (instance != null))
        return instance;

    PrintStream out = System.out;
    if (swallowOutput) {
        SystemLogHandler.startCapture();
    }

    Servlet servlet;
    try {
        long t1=System.currentTimeMillis();
        // Complain if no servlet class has been specified
        if (servletClass == null) {
            unavailable(null);
            throw new ServletException
                (sm.getString("standardWrapper.notClass", getName()));
        }

        InstanceManager instanceManager = ((StandardContext)getParent()).getInstanceManager();
        try {
            // 有了类加载器和要加载的 Servlet 名字,就可以使用 loadServlet 方法来加载类 了。
            // 1. 创建Servlet实例,如果添加了JNDI 注解,将进行依赖注入
            servlet = (Servlet) instanceManager.newInstance(servletClass);
        } catch (ClassCastException e) {
            unavailable(null);
            // Restore the context ClassLoader
            throw new ServletException
                (sm.getString("standardWrapper.notServlet", servletClass), e);
        } catch (Throwable e) {
            e = ExceptionUtils.unwrapInvocationTargetException(e);
            ExceptionUtils.handleThrowable(e);
            unavailable(null);

            // Added extra log statement for Bugzilla 36630:
            // https://bz.apache.org/bugzilla/show_bug.cgi?id=36630
            if(log.isDebugEnabled()) {
                log.debug(sm.getString("standardWrapper.instantiate", servletClass), e);
            }

            // Restore the context ClassLoader
            throw new ServletException
                (sm.getString("standardWrapper.instantiate", servletClass), e);
        }

        if (multipartConfigElement == null) {
            // 2. 读取javax.servlet.annotation.MultipartConfig配置,以用于multipart/form-data请求处理,包括临时文件存储路径 。
            // 上传文件最大字节数,请求最大字节数,文件大小阈值。
            MultipartConfig annotation =
                    servlet.getClass().getAnnotation(MultipartConfig.class);
            if (annotation != null) {
                multipartConfigElement =
                        new MultipartConfigElement(annotation);
            }
        }

        // Special handling for ContainerServlet instances
        // 如果通过了安全性检查,接下来检查该 Servlet 是否是一个 ContainerServlet。 ContainerServlet 是实现了 org.apache.catalina.ContainerServlet
        // 接口的 Servlet,它可以访问 Catalina 的内部函数。如果该 Servlet 是 ContainerServlet,loadServlet 方法调用 ContainerServlet 的 setWrapper
        // 方法,传递该 StandardWrapper 实例。
        if ((servlet instanceof ContainerServlet) &&
                // isContainerProvidedServlet 方法返回 true 值。classLoader 会获得另一个 ClassLoader 的实例,这样就可以访问 Catalina 的内部了。
                (isContainerProvidedServlet(servletClass) ||
                        ((Context) getParent()).getPrivileged() )) {
            ((ContainerServlet) servlet).setWrapper(this);
        }

        classLoadTime=(int) (System.currentTimeMillis() -t1);

        // 实现了SingleThreadModel接口
        if (servlet instanceof SingleThreadModel) {
            if (instancePool == null) {
                instancePool = new Stack<Servlet>();
            }
            singleThreadModel = true;
        }

        // 4. 初始化servlet
        initServlet(servlet);

        fireContainerEvent("load", this);

        loadTime=System.currentTimeMillis() -t1;
    } finally {
        if (swallowOutput) {
            String log = SystemLogHandler.stopCapture();
            if (log != null && log.length() > 0) {
                if (getServletContext() != null) {
                    getServletContext().log(log);
                } else {
                    out.println(log);
                }
            }
        }
    }
    return servlet;

}

  加载Servlet代码分为4步。

  1. 反射实例化Servlet。
  2. MultipartConfig注解处理
  3. 如果实现了ContainerServlet,则设置其Wrapper为this
  4. 初始化Servlet

  其实上面代码中通过实现管理器servlet = (Servlet) instanceManager.newInstance(servletClass); 这一行代码虽然内部是通过反射实现servletClass的实例化。

public Object newInstance(String className) throws IllegalAccessException,
InvocationTargetException, NamingException, InstantiationException,
ClassNotFoundException, IllegalArgumentException, NoSuchMethodException, SecurityException {
    Class<?> clazz = loadClassMaybePrivileged(className, classLoader);
    return newInstance(clazz.getDeclaredConstructor().newInstance(), clazz);
}

  细心的读者肯定会发现,竟然传了一个classLoader,那这个classLoader是什么呢? 在DefaultInstanceManager实例化时,传了了WebappClassLoader,因此在实例化Servlet时,其类加载器为StandardContext的类加载器,也就是WebappClassLoader加载器。 接下来就是initServlet()的实现了。

private synchronized void initServlet(Servlet servlet)
        throws ServletException {

    if (instanceInitialized && !singleThreadModel) return;

    // Call the initialization method of this servlet
    try {
        // 接下来 loadServlet 方法触发 BEFORE_INIT_EVENT 事件,并调用发送者的 init 方法。
        instanceSupport.fireInstanceEvent(InstanceEvent.BEFORE_INIT_EVENT,
                                          servlet);

        if( Globals.IS_SECURITY_ENABLED) {
            boolean success = false;
            try {
                Object[] args = new Object[] { facade };
                SecurityUtil.doAsPrivilege("init",
                                           servlet,
                                           classType,
                                           args);
                success = true;
            } finally {
                if (!success) {
                    // destroy() will not be called, thus clear the reference now
                    SecurityUtil.remove(servlet);
                }
            }
        } else {
            //因此,当 StandardWrapper 对象调用 Servlet 实例的 init 方法的时候,它传递 的是一个 StandardWrapperFacade 对象。
            // 在 Servlet 内部调用 ServletConfig 的 getServletName, getInitParameter, 和 getInitParameterNames 方法只需
            // 要调用它们在 StandardWrapper 的实现就行。
            servlet.init(facade);
        }

        instanceInitialized = true;

        instanceSupport.fireInstanceEvent(InstanceEvent.AFTER_INIT_EVENT,
                                          servlet);
    } catch (UnavailableException f) {
        instanceSupport.fireInstanceEvent(InstanceEvent.AFTER_INIT_EVENT,
                                          servlet, f);
        unavailable(f);
        throw f;
    } catch (ServletException f) {
        instanceSupport.fireInstanceEvent(InstanceEvent.AFTER_INIT_EVENT,
                                          servlet, f);
        // If the servlet wanted to be unavailable it would have
        // said so, so do not call unavailable(null).
        throw f;
    } catch (Throwable f) {
        ExceptionUtils.handleThrowable(f);
        getServletContext().log("StandardWrapper.Throwable", f );
        instanceSupport.fireInstanceEvent(InstanceEvent.AFTER_INIT_EVENT,
                                          servlet, f);
        // If the servlet wanted to be unavailable it would have
        // said so, so do not call unavailable(null).
        throw new ServletException
            (sm.getString("standardWrapper.initException", getName()), f);
    }
}

  instanceInitialized控制只初始化一次,而Servlet真正的初始化代码在servlet.init(facade);这一行。 其他的都是一些事件处理,因此Servlet在每次创建时都会调用其init()方法,有且仅有一次,在具体分析Servlet的init()方法时,先来看看Servlet的种类。

Servlet种类

  根据请求的的资源不同的种类,可以把Servlet分为三种类别,比如请求可能访问一个普通的Servlet,也可能访问一个JSP页面,也可以访问的是一个静态资源,根据对这些不同的类别的处理方式,可以分为三种Servlet,如图10.6所示,一个请求到达Tomcat后将由URI映射器根据请求的URI进行建模, 它会计算出请求发往哪个 Host 容器的哪个Context容器的哪个Wrapper 处理, 在路由的Wrapper 容器时会通过一定的算法选择不同的Servlet进行处理,比如,普通Servlet请求由路由到普通的Servlet,JSP则路由到JspServlet ,而静态资源则路由到DefaultServlet 。
Servlet路径的匹配规则如下
首先,尝试使用精确匹配法匹配精确的类型Servlet 的路径 。
然后,尝试使用前缀匹配通配符类型的Servlet。
接着,尝试使用扩展名匹配通配符类型的Servlet
最后匹配默认的Servlet。
如果一个请求到来,则通过以上规则匹配对应的Servlet,例如请求http://localhost:8080/test 精确匹配<url-pattern>test</url-pattern>的Servlet ,而http://localhost:8080/test.jsp,则会匹配<url-pattern>*.jsp</url-pattern> 的JspServlet ,下面分别讨论三种不同的Servlet 。

  1. 普通Servlet

  普通Servlet就是我们最常见的Servlet,做Web开发都会涉及Servlet,要处理业务逻辑就会自己定义Servlet进行处理, 这就是普通的Servlet,编写好后Servlet通过配置web.xml文件就可以完成部署。 配置格式如下。

<servlet>
    <servlet-name>my</servlet-name>
    <servlet-class>com.example.servelettest.HelloServlet</servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>my</servlet-name>
    <url-pattern>/test</url-pattern>
</servlet-mapping>
  1. JspServlet

  Web应用开发人员一般对这个Servlet比较陌生,因为他们不会直接与它打交道,既然是Servlet,那么肯定要声明后才被部署使用, 它被部署到Tomcat的安装目录下conf目录下的web.xml文件中,这里的web.xml文件是Tomcat的全局Web描述文件,JspServlet的配置如下 。

<servlet>
    <servlet-name>jsp</servlet-name>
    <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
    <init-param>
        <param-name>fork</param-name>
        <param-value>false</param-value>
    </init-param>
    <init-param>
        <param-name>xpoweredBy</param-name>
        <param-value>false</param-value>
    </init-param>
    <init-param>
        <param-name>trimSpaces</param-name>
        <param-value>false</param-value>
    </init-param>
    <load-on-startup>3</load-on-startup>

</servlet>


<servlet-mapping>
    <servlet-name>jsp</servlet-name>
    <url-pattern>*.jsp</url-pattern>
    <url-pattern>*.jspx</url-pattern>
</servlet-mapping>

  可以看到,所以以.jsp 和.jspx 结尾的请求都会被JspServlet处理,它包揽了所有的JSP页面的处理, 我们知道JSP页面最终也是会被Tomcat 编译成相应的Servlet,如果想看详细的编译过程,请看之前的博客 Tomcat 源码解析一JSP编译器Jasper-佛怒火莲 系列, 而这些Servlet的处理都次给了JspServlet 。
  JspServlet处理逻辑大致如下 。
  a) 判断是不是第一次访问Servlet,如果是,则会先编译JSP页面,殷富一定的包和类名规则生成相应的Servlet类。
  b) 加载刚刚编译好的JSP Serlvet类,并初始化它们
  c) 调用刚刚加载好的JSP Servlet的service方法,处理请求。
至此完成了JSP页面的请求。

  1. DefaultServlet

  同样是Tomcat内部使用的一个Servlet ,DefaultServlet是Tomcat专门提供用于处理静态资源的Servlet,它同样被部署到Tomcat安装目录下的conf目录下的web.xml文件中,DefaultServlet的配置如下 。

<servlet>
    <servlet-name>default</servlet-name>


    <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
    <init-param>
        <param-name>debug</param-name>
        <param-value>0</param-value>
    </init-param>
    <init-param>
        <param-name>listings</param-name>
        <!--<param-value>false</param-value>      -->
        <param-value>true</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>

</servlet>


<servlet-mapping>
    <servlet-name>default</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>

  可以看到所有的URI请求都会被匹配,但由于 Mapper组件匹配Servlet时将DefaultServlet放到最后才匹配,所以它并不会把所有的请求都拦截下来, 只有那些经过精确匹配,前缀匹配,扩展名匹配等还没有匹配上的,才会留给DefaultServlet,DefaultServlet通过JNDI根据URI在tomcat内部查找资源,然后以该资源响应客户端 。

  首先来看DefaultServlet的初始化。在之前的博客也分析过这一块。 Tomcat 源码解析一容器加载-大寂灭指(下),但今天继续回顾一下。在DefaultServlet的init()中打一个断点。启动Tomcat
在这里插入图片描述
  先看是哪里调用了DefaultServlet的初始化方法。

在这里插入图片描述
  从调用链中得知,是StandardContext在启动时调用了所有子容器的load()方法。
在这里插入图片描述

  进而调用了servlet.init(facade);方法,但StandardWrapper中,facade的默认值为StandardWrapperFacade facade = new StandardWrapperFacade(this); 而this就是StandardWrapper。
在这里插入图片描述
  getServletConfig()事实上就是StandardWrapper
在这里插入图片描述
  而里面的这些参数配置默认来源于catalina.base/conf/web.xml

<servlet>
    <servlet-name>default</servlet-name>


    <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
    <init-param>
        <param-name>debug</param-name>
        <param-value>0</param-value>
    </init-param>
    <init-param>
        <param-name>listings</param-name>
        <param-value>true</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>

</servlet>

  先不分析这些参数有什么作用,后面使用到再来分析。

  接下来看JspServlet的初始化。 同样JspServlet也是在StandardContext启动时进行初始化。

在这里插入图片描述

public void init(ServletConfig config) throws ServletException {

    super.init(config);
    this.config = config;
    this.context = config.getServletContext();

    // Initialize the JSP Runtime Context
    // Check for a custom Options implementation
    // 允许指定的类来配置 Jasper。如果没有指定,则使用默认的 Servlet 内置参数(EmbeddedServletOptions)。
    String engineOptionsName = config.getInitParameter("engineOptionsClass");
    if (Constants.IS_SECURITY_ENABLED && engineOptionsName != null) {
        log.info(Localizer.getMessage(
                "jsp.info.ignoreSetting", "engineOptionsClass", engineOptionsName));
        engineOptionsName = null;
    }
    if (engineOptionsName != null) {
        // Instantiate the indicated Options implementation
        try {
            ClassLoader loader = Thread.currentThread().getContextClassLoader();
            Class<?> engineOptionsClass = loader.loadClass(engineOptionsName);
            Class<?>[] ctorSig = { ServletConfig.class, ServletContext.class };
            Constructor<?> ctor = engineOptionsClass.getConstructor(ctorSig);
            Object[] args = { config, context };
            options = (Options) ctor.newInstance(args);
        } catch (Throwable e) {
            e = ExceptionUtils.unwrapInvocationTargetException(e);
            ExceptionUtils.handleThrowable(e);
            // Need to localize this.
            log.warn("Failed to load engineOptionsClass", e);
            // Use the default Options implementation
            options = new EmbeddedServletOptions(config, context);
        }
    } else {
        // Use the default Options implementation
        options = new EmbeddedServletOptions(config, context);
    }
    rctxt = new JspRuntimeContext(context, options);
    if (config.getInitParameter("jspFile") != null) {
        jspFile = config.getInitParameter("jspFile");
        try {
            if (null == context.getResource(jspFile)) {
                throw new ServletException("missing jspFile: [" + jspFile + "]");
            }
        } catch (MalformedURLException e) {
            throw new ServletException("Can not locate jsp file", e);
        }
        try {
            if (SecurityUtil.isPackageProtectionEnabled()){
               AccessController.doPrivileged(new PrivilegedExceptionAction<Object>(){
                    @Override
                    public Object run() throws IOException, ServletException {
                        serviceJspFile(null, null, jspFile, true);
                        return null;
                    }
                });
            } else {
                serviceJspFile(null, null, jspFile, true);
            }
        } catch (IOException e) {
            throw new ServletException("Could not precompile jsp: " + jspFile, e);
        } catch (PrivilegedActionException e) {
            Throwable t = e.getCause();
            if (t instanceof ServletException) throw (ServletException)t;
            throw new ServletException("Could not precompile jsp: " + jspFile, e);
        }
    }

    if (log.isDebugEnabled()) {
        log.debug(Localizer.getMessage("jsp.message.scratch.dir.is",
                options.getScratchDir().toString()));
        log.debug(Localizer.getMessage("jsp.message.dont.modify.servlets"));
    }
}

  我们可以在jsp的init-param参数配置

<init-param>
    <param-name>engineOptionsClass</param-name>
    <param-value>xxx</param-value>
</init-param>

  那配置这个参数有何意义呢?其实就是自定义EmbeddedServletOptions对象,那自定义这个对象有什么用呢?先来看EmbeddedServletOptions的构造方法。

public EmbeddedServletOptions(ServletConfig config,
        ServletContext context) {

    Enumeration<String> enumeration=config.getInitParameterNames();
    ...
    
    // 是否去掉模板文本中行为和指令间的空格,缺省为false
    String trimsp = config.getInitParameter("trimSpaces");
    ... 

    this.isPoolingEnabled = true;
    //  确定是否共享标签处理器,true 或 false,缺省为 true。
    String poolingEnabledParam = config.getInitParameter("enablePooling");
    ... 
    // 是否对每个输入行都用一条 print 语句来生成静态内容,以方便调试。true 或 false,缺省为 true。
    String mapFile = config.getInitParameter("mappedfile");
    ... 

    // 类文件在编译时是否显示调试(debugging)信息? true 或 false,缺省为 true。
    String debugInfo = config.getInitParameter("classdebuginfo");
    ... 

    // 如果“development”属性为 false 且“checkInterval”大于 0,则使用后台编译。“checkInterval”是查看 JSP 页面(包括其附属文件)
    String checkInterval = config.getInitParameter("checkInterval");
    ... 
    // 是否让 Jasper 用于开发模式?如果是,检查 JSPs 修改的频率,将通过设置 modificationTestInterval 参数来完成。 true 或 false, 缺省为 true。
    String development = config.getInitParameter("development");
    if (development != null) {
        if (development.equalsIgnoreCase("true")) {
            this.development = true;
        } else if (development.equalsIgnoreCase("false")) {
            this.development = false;
        } else {
            if (log.isWarnEnabled()) {
                log.warn(Localizer.getMessage("jsp.warning.development"));
            }
        }
    }

    //  是否禁止 JSR45 调试时生成 SMAP 信息?true 或 false,缺省为 false。
    String suppressSmap = config.getInitParameter("suppressSmap");
    ... 
    //  JSR45 调试的 SMAP 信息是否转存到文件?true 或 false,缺省为 false。当 suppressSmap 为 true 时,该参数为 false。
    String dumpSmap = config.getInitParameter("dumpSmap");
    ... 
    // 在一个 useBean action 中,当类属性的值不是一个合法的 bean class 时,Jasper 是否抛出异常?true
    ... 
    // 当使用标签时,发送给 Internet Explorer 的 class-id 的值。缺省为:8AD9C840-044E-11D1-B3E9-00805F499D93。
    ... 
    /*
     * scratchdir  当编译 JSP 页面时使用的 scratch 目录。缺省为当前 WEB 应用的工作目录。
     */
    String dir = config.getInitParameter("scratchdir");
    if (dir != null && Constants.IS_SECURITY_ENABLED) {
        log.info(Localizer.getMessage("jsp.info.ignoreSetting", "scratchdir", dir));
        dir = null;
    }
    if (dir != null) {
        scratchDir = new File(dir);
    } else {
        // First try the Servlet 2.2 javax.servlet.context.tempdir property
        scratchDir = (File) context.getAttribute(ServletContext.TEMPDIR);
        if (scratchDir == null) {
            // Not running in a Servlet 2.2 container.
            // Try to get the JDK 1.2 java.io.tmpdir property
            dir = System.getProperty("java.io.tmpdir");
            if (dir != null)
                scratchDir = new File(dir);
        }
    }
    ... 

    // 确定生成的 Servlet 是否加上 X-Powered-By 响应头?true 或 false,缺省为 false。
    String xpoweredBy = config.getInitParameter("xpoweredBy");
    if (xpoweredBy != null) {
        if (xpoweredBy.equalsIgnoreCase("true")) {
            this.xpoweredBy = true;
        } else if (xpoweredBy.equalsIgnoreCase("false")) {
            this.xpoweredBy = false;
        } else {
            if (log.isWarnEnabled()) {
                log.warn(Localizer.getMessage("jsp.warning.xpoweredBy"));
            }
        }
    }

    // 异常信息中是否包含出错的源代码片段?true 或 false,缺省为 true。
    String displaySourceFragment = config.getInitParameter("displaySourceFragment");
    ... 
}

  原来解析jsp生成 Servlet的相关配置都在EmbeddedServletOptions中,因此我们也可以自己定义一个类来设置这些配置参数,而不是通过XML配置文件。
  接下来看另外一个问题,我们可以在初始化参数中配置jspFile,这个用意是什么呢?例如。

<servlet>
    <servlet-name>jsp</servlet-name>
    <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
    ... 
    <init-param>
        <param-name>jspFile</param-name>
        <param-value>aservlet.jsp</param-value>
    </init-param>

    <load-on-startup>3</load-on-startup>

</servlet>

  在启动StandardContext时,也就是在初始化JspServlet时,会将aservlet.jsp这个文件解析编译成Servlet,并加载到内存中,当Tomcat提供服务时,就省去了解析和编译和生成Servlet的步骤,因此就不会出现第一次访问这个jsp文件时感到慢的情况。当然啦,最重要的还是serviceJspFile()这个方法 ,在之前的博客中 已经对jsp 的解析编译,生成Servlet 等做了详细的分析,有兴趣可以自行去研究 Tomcat 源码解析一JSP编译器Jasper-佛怒火莲(上)这一篇博客 。

  而普通的Servlet就是我们自己定义的Servlet,只要实现了init()方法,在Servlet创建时,会调用其init()方法初始化。
  在StandardWrapperValve的invoke()方法中,还有另外一情况需要考虑。 Servlet不仅支持同步,还支持异步,因此先来看Servlet异步模式支持。

Comet 模式的支持

  Comet模式是一种服务器端的推送技术,它的核心思想提供了一种能让服务器往客户端发送数据的方式,Comet模式为什么会出现,风开始人们在客户端通过不断的自动刷新整个页面更新数据,后来觉得体验不好, 又使用了Javax不断的从客户端轮询服务器以更新数据,然后使用Comet模式由服务器端通过长链接推送数据,Comet模式能大大的减少发送到服务器端的请求,从而避免了很多的开销, 而且它还具备了更好的实时性。

  如图10.7所示,客户端发送一个请求到服务器,服务器接收到连接后, 一直保持着连接不关闭, 接着,客户端发送一个操作报文告诉服务器做什么操作,服务器处理完事件1后会给客户端响应,然后,处理完事件2后又会给客户端响应,接着,客户端继续发送操作报文给服务器,服务器再进行响应。
在这里插入图片描述

  一般Comet模式需要NIO配合,而在BIO 中无法使用Commet 模式,在Tomcat内部集成Comet模式的思路比较清晰,引入了一个CometProcessor接口,此接口只有一个event方法,具体的接口代码如下 。

public interface CometProcessor extends Servlet{

    /**
     * Process the given Comet event.
     *
     * @param event The Comet event that will be processed
     * @throws IOException
     * @throws ServletException
     */
    public void event(CometEvent event)
        throws IOException, ServletException;

}

  而CometEvent则表示Comet相关的事件,它包含了BEGIN,READ,END,ERROR四个事件,其含义分别如下。

  1. BEGIN ,表示请求开始,此时客户端连接已经被接收 。
  2. READ ,表示客户端连接已经建立,可以读取数据了,读取过程不会阻塞 。
  3. END 表示请求结束,此时客户端连接将断开
  4. ERROR ,表示发生了I/O异常,一般将会结束此次请求并且连接断开 。

请看一个例子。

public class CometServlet extends HttpServlet implements CometProcessor {


    protected ArrayList connections = new ArrayList();

    @Override
    public void event(CometEvent event) throws IOException, ServletException {
        HttpServletRequest request = event.getHttpServletRequest();
        HttpServletResponse response = event.getHttpServletResponse();
        if (event.getEventType() == CometEvent.EventType.BEGIN) {
            synchronized (connections) {
                connections.add(response);
            }
        } else if (event.getEventType() == CometEvent.EventType.ERROR) {
            synchronized (connections) {
                connections.remove(response);
            }
        } else if (event.getEventType() == CometEvent.EventType.END) {
            synchronized (connections) {
                connections.remove(response);
            }
        } else if (event.getEventType() == CometEvent.EventType.READ) {
            synchronized (connections) {
                InputStream is = request.getInputStream();
                byte[] buf = new byte[512];
                do {
                    int n = is.read(buf);
                    if (n > 0) {
                        System.out.println(new String(buf, 0, n));
                    } else if (n < 0) {
                        return;
                    }
                } while (is.available() > 0);
            }
        }
    }
}

  这个例子中只是简单的接收客户端连接而不做任何处理,并且客户端发送过来的数据输出,就很容易理解,在BEGIN事件中接收连接并把响应对象放入到列表中,发生ERROR 或END事件时则将响应对象移除,当发生READ事件时则读取数据并输出 。

  有了CometProcessor接口后,Tomcat内部就可以识别Commet模式的Servlet了,我们知道Tomcat对请求的处理是管道模式的, 所以在Wrapper 容器的管道中判断加载的Servlet是否继承了CometProcessor,如果继承则说明是Comet模式,并使用Comet方式处理, 它的处理过程如图10.8所示,录一个客户端连接到来时,被接收器接收后注册到NioChannel队列中,Poller组件不断轮询是否有NioChannel需要处理, 如果有,则调用前面的实例化的Comet模式的Servlet,这里主要用到CometProcessor接口的event方法,Poller会将对应的请求对象,响应对象和事件封装成CometEvent对象并传入event方法,此时即执行event方法的逻辑,完成对不同事件的处理,从而实现Comet模式 。
在这里插入图片描述

public ApplicationFilterChain createFilterChain
    (ServletRequest request, Wrapper wrapper, Servlet servlet) {

    // get the dispatcher type
    DispatcherType dispatcher = null;
    if (request.getAttribute(Globals.DISPATCHER_TYPE_ATTR) != null) {
        dispatcher = (DispatcherType) request.getAttribute(
                Globals.DISPATCHER_TYPE_ATTR);
    }
    String requestPath = null;
    Object attribute = request.getAttribute(
            Globals.DISPATCHER_REQUEST_PATH_ATTR);

    if (attribute != null){
        requestPath = attribute.toString();
    }

    // If there is no servlet to execute, return null
    if (servlet == null)
        return (null);

    boolean comet = false;

    // Create and initialize a filter chain object
    ApplicationFilterChain filterChain = null;
    if (request instanceof Request) {
        Request req = (Request) request;
        comet = req.isComet();
        if (Globals.IS_SECURITY_ENABLED) {
            // Security: Do not recycle
            filterChain = new ApplicationFilterChain();
            if (comet) {
                req.setFilterChain(filterChain);
            }
        } else {
            filterChain = (ApplicationFilterChain) req.getFilterChain();
            if (filterChain == null) {
                filterChain = new ApplicationFilterChain();
                req.setFilterChain(filterChain);
            }
        }
    } else {
        // Request dispatcher in use
        filterChain = new ApplicationFilterChain();
    }

    filterChain.setServlet(servlet);

    filterChain.setSupport
        (((StandardWrapper)wrapper).getInstanceSupport());

    // Acquire the filter mappings for this Context
    StandardContext context = (StandardContext) wrapper.getParent();
    FilterMap filterMaps[] = context.findFilterMaps();

    // If there are no filter mappings, we are done
    // 没有Filter映射关系
    if ((filterMaps == null) || (filterMaps.length == 0))
        return (filterChain);

    // Acquire the information we will need to match filter mappings
    String servletName = wrapper.getName();

    // Add the relevant path-mapped filters to this filter chain
    // 根据servletName找到匹配的filter
    for (int i = 0; i < filterMaps.length; i++) {
        if (!matchDispatcher(filterMaps[i] ,dispatcher)) {
            continue;
        }
        if (!matchFiltersURL(filterMaps[i], requestPath))
            continue;
        ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
            context.findFilterConfig(filterMaps[i].getFilterName());
        if (filterConfig == null) {
           // FIXME - log configuration problem
            continue;
        }
        boolean isCometFilter = false;
        if (comet) {
            try {
                isCometFilter = filterConfig.getFilter() instanceof CometFilter;
            } catch (Exception e) {
                // Note: The try catch is there because getFilter has a lot of
                // declared exceptions. However, the filter is allocated much
                // earlier
                Throwable t = ExceptionUtils.unwrapInvocationTargetException(e);
                ExceptionUtils.handleThrowable(t);
            }
            if (isCometFilter) {
                filterChain.addFilter(filterConfig);
            }
        } else {
            filterChain.addFilter(filterConfig);
        }
    }

    // Add filters that match on servlet name second
    for (int i = 0; i < filterMaps.length; i++) {
        if (!matchDispatcher(filterMaps[i] ,dispatcher)) {
            continue;
        }
        if (!matchFiltersServlet(filterMaps[i], servletName))
            continue;
        ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
            context.findFilterConfig(filterMaps[i].getFilterName());
        if (filterConfig == null) {
           // FIXME - log configuration problem
            continue;
        }
        boolean isCometFilter = false;
        if (comet) {
            try {
                isCometFilter = filterConfig.getFilter() instanceof CometFilter;
            } catch (Exception e) {
                // Note: The try catch is there because getFilter has a lot of
                // declared exceptions. However, the filter is allocated much
                // earlier
            }
            if (isCometFilter) {
                filterChain.addFilter(filterConfig);
            }
        } else {
            filterChain.addFilter(filterConfig);
        }
    }

    // Return the completed filter chain
    return (filterChain);

}
过滤器链

  Context容器的过滤器模块包含了过滤器的相关信息, 过滤器链接的调用的思路其实也很简单,如图10.5 所示,请求通过管道流转到Wrapper 容器管道,经过若干阀门后达到基础阀门StandardWrapperValue ,它将创建一个过滤器链ApplicationFilterChain对象,创建时过滤器链对象做了如下逻辑处理。

  1. 从Context容器中获取所有过滤器相关的信息
  2. 通过URL匹配过滤器匹配加入的过滤链中
  3. 通过Servlet名称匹配过滤器,匹配加入过滤链中。

在这里插入图片描述
  创建ApplicationFilterChain对象后,StandardWrapperValue将调用它的doFilter方法,它就会开始一个一个的调用过滤器,请求被一层一层的处理,最后才调用Servlet处理,至此,针对某个请求,过滤器链将Context中所有的过滤器中对象的请求的过滤器串联起来 ,实现过滤器的功能 。

  先来看看url匹配算法。

public static boolean matchFiltersURL(String testPath, String requestPath) {

    if (testPath == null)
        return (false);

    // Case 1 - Exact Match
    if (testPath.equals(requestPath))
        return (true);

    // Case 2 - Path Match ("/.../*")
    if (testPath.equals("/*"))
        return (true);
    if (testPath.endsWith("/*")) {
        if (testPath.regionMatches(0, requestPath, 0,
                testPath.length() - 2)) {
            // testPath 为 /aaa/*
            // requestPath 为 /aaa 的情况
            if (requestPath.length() == (testPath.length() - 2)) {
                return (true);
                // testPath 为 /aaa/*
                // requestPath 为 /aaa
            } else if ('/' == requestPath.charAt(testPath.length() - 2)) {
                return (true);
            }
        }
        return (false);
    }

    // Case 3 - Extension Match
    // 如 testPath = *.jsp, requestPath = /aaa.jsp ,则匹配成功
    // 如 testPath = *.jsp, requestPath = /aaa.html 匹配失败
    // 如testPath = *.jsp, requestPath = /aaa. 匹配失败
    if (testPath.startsWith("*.")) {
        int slash = requestPath.lastIndexOf('/');
        int period = requestPath.lastIndexOf('.');
        if ((slash >= 0) && (period > slash)
                && (period != requestPath.length() - 1)
                && ((requestPath.length() - period)
                == (testPath.length() - 1))) {
            return (testPath.regionMatches(2, requestPath, period + 1,
                    testPath.length() - 2));
        }
    }

    // Case 4 - "Default" Match
    return (false); // NOTE - Not relevant for selecting filters

}

  url 匹配分三种情况,模糊匹配,前缀匹配,后缀匹配,具体情况看注释。
  接下来看servlet全称匹配,这就很简单了,如果servletName和过滤器中配置的servletName相等,当前过滤器需要添加到当前Servlet的过滤器链中。

private boolean matchFiltersServlet(FilterMap filterMap,
                                    String servletName) {
    if (servletName == null) {
        return (false);
    }
    // Check the specific "*" special servlet name
    else if (filterMap.getMatchAllServletNames()) {
        return (true);
    } else {
        String[] servletNames = filterMap.getServletNames();
        for (int i = 0; i < servletNames.length; i++) {
            if (servletName.equals(servletNames[i])) {
                return (true);
            }
        }
        return false;
    }
}

  接下来就是过滤器链的调用了,过滤器的实现逻辑也很简单,请看下面例子。

public interface IFilter {

    public void doFilter(FilterChain filterChain);
}


public class Filter1  implements IFilter{

    @Override
    public void doFilter(FilterChain filterChain) {
        System.out.println("过滤器1执行");
        filterChain.doFilter();
    }
}

public class Filter2 implements IFilter{

    @Override
    public void doFilter(FilterChain filterChain) {
        System.out.println("过滤器2执行");
        filterChain.doFilter();
    }
}


public class FilterChain {

    public static List<IFilter> filters = new ArrayList<>();
    public void addFilter(IFilter filter) {
        filters.add(filter);
    }

    private int index;

    public void doFilter() {
        if (index > filters.size() - 1) {
            System.out.println("过滤器 已经执行完了");
            return;
        }
        filters.get(index++).doFilter(this);
    }

    public static void main(String[] args) {
        Filter1 filter1 = new Filter1();
        Filter2 filter2 = new Filter2();
        FilterChain filterChain = new FilterChain();
        filterChain.addFilter(filter1);
        filterChain.addFilter(filter2);
        filterChain.doFilter();
    }
}

结果输出

过滤器1执行
过滤器2执行
过滤器 已经执行完了

/**
 * @exception IOException if an input/output error occurs
 * @exception ServletException if a servlet exception occurs
 * ApplicationFilterChain 的 doFilter 方法,并将它自己作为第三个参数传递给 它。
 * 在他的 doFilter 方法中,一个过滤器可以调用另一个过滤器链的 doFilter 来唤 醒另一个过来出去。这里是一个过滤器的 doFilter 实现的例子
 */
@Override
public void doFilter(ServletRequest request, ServletResponse response)
    throws IOException, ServletException {

    if( Globals.IS_SECURITY_ENABLED ) {
        final ServletRequest req = request;
        final ServletResponse res = response;
        try {
            java.security.AccessController.doPrivileged(
                new java.security.PrivilegedExceptionAction<Void>() {
                    @Override
                    public Void run()
                        throws ServletException, IOException {
                        internalDoFilter(req,res);
                        return null;
                    }
                }
            );
        } catch( PrivilegedActionException pe) {
            Exception e = pe.getException();
            if (e instanceof ServletException)
                throw (ServletException) e;
            else if (e instanceof IOException)
                throw (IOException) e;
            else if (e instanceof RuntimeException)
                throw (RuntimeException) e;
            else
                throw new ServletException(e.getMessage(), e);
        }
    } else {
        internalDoFilter(request,response);
    }
}

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 = null;
        try {
            filter = filterConfig.getFilter();
            support.fireInstanceEvent(InstanceEvent.BEFORE_FILTER_EVENT,
                                      filter, request, response);

            if (request.isAsyncSupported() && "false".equalsIgnoreCase(
                    filterConfig.getFilterDef().getAsyncSupported())) {
                request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR,
                        Boolean.FALSE);
            }
            if( Globals.IS_SECURITY_ENABLED ) {
                final ServletRequest req = request;
                final ServletResponse res = response;
                Principal principal =
                    ((HttpServletRequest) req).getUserPrincipal();

                Object[] args = new Object[]{req, res, this};
                SecurityUtil.doAsPrivilege
                    ("doFilter", filter, classType, args, principal);

            } else {
                // 执行filter的逻辑
                // 如你看到的,在 doFilter 方法最后一行,它调用过滤链的 doFilter 方法。如果 该过滤器是过滤链的最后一个过滤器,
                // 它叫调用请求的 Servlet 的 service 方法。 如果过滤器没有调用 chain.doFilter,下一个过滤器就不会被调用。
                filter.doFilter(request, response, this);
            }

            support.fireInstanceEvent(InstanceEvent.AFTER_FILTER_EVENT,
                                      filter, request, response);
        } catch (IOException e) {
            if (filter != null)
                support.fireInstanceEvent(InstanceEvent.AFTER_FILTER_EVENT,
                                          filter, request, response, e);
            throw e;
        } catch (ServletException e) {
            if (filter != null)
                support.fireInstanceEvent(InstanceEvent.AFTER_FILTER_EVENT,
                                          filter, request, response, e);
            throw e;
        } catch (RuntimeException e) {
            if (filter != null)
                support.fireInstanceEvent(InstanceEvent.AFTER_FILTER_EVENT,
                                          filter, request, response, e);
            throw e;
        } catch (Throwable e) {
            e = ExceptionUtils.unwrapInvocationTargetException(e);
            ExceptionUtils.handleThrowable(e);
            if (filter != null)
                support.fireInstanceEvent(InstanceEvent.AFTER_FILTER_EVENT,
                                          filter, request, response, e);
            throw new ServletException
              (sm.getString("filterChain.filter"), e);
        }
        return;
    }

    // We fell off the end of the chain -- call the servlet instance
    try {
        if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
            lastServicedRequest.set(request);
            lastServicedResponse.set(response);
        }

        support.fireInstanceEvent(InstanceEvent.BEFORE_SERVICE_EVENT,
                                  servlet, request, response);
        if (request.isAsyncSupported()
                && !support.getWrapper().isAsyncSupported()) {
            request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR,
                    Boolean.FALSE);
        }
        // Use potentially wrapped request from this point
        if ((request instanceof HttpServletRequest) &&
            (response instanceof HttpServletResponse)) {

            if( Globals.IS_SECURITY_ENABLED ) {
                final ServletRequest req = request;
                final ServletResponse res = response;
                Principal principal =
                    ((HttpServletRequest) req).getUserPrincipal();
                Object[] args = new Object[]{req, res};
                SecurityUtil.doAsPrivilege("service",
                                           servlet,
                                           classTypeUsedInService,
                                           args,
                                           principal);
            } else {
                // 执行servlet
                servlet.service(request, response);
            }
        } else {
            servlet.service(request, response);
        }
        support.fireInstanceEvent(InstanceEvent.AFTER_SERVICE_EVENT,
                                  servlet, request, response);
    } catch (IOException e) {
        support.fireInstanceEvent(InstanceEvent.AFTER_SERVICE_EVENT,
                                  servlet, request, response, e);
        throw e;
    } catch (ServletException e) {
        support.fireInstanceEvent(InstanceEvent.AFTER_SERVICE_EVENT,
                                  servlet, request, response, e);
        throw e;
    } catch (RuntimeException e) {
        support.fireInstanceEvent(InstanceEvent.AFTER_SERVICE_EVENT,
                                  servlet, request, response, e);
        throw e;
    } catch (Throwable e) {
        ExceptionUtils.handleThrowable(e);
        support.fireInstanceEvent(InstanceEvent.AFTER_SERVICE_EVENT,
                                  servlet, request, response, e);
        throw new ServletException
          (sm.getString("filterChain.servlet"), e);
    } finally {
        if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
            lastServicedRequest.set(null);
            lastServicedResponse.set(null);
        }
    }
}
普通的Servlet工作机制

  在研究Servlet在Tomcat中工作机制前, 必须先看看Servlet规范的一些重要规定,该规范提供了一个Servlet接口,接口中包含了重要的方法是init,service,destory等方法,Servlet在初始化时要调用init方法,在销毁时要调用destroy方法,而在对客户端请求处理时调用service方法,对于这些机制,都必须由Tomcat在内部提供支持,具体由Wrapper容器提供支持。

  对于 Tomcat 中消息流的流转机制,我们已经比较清楚了,4个不同级别的容器是通过管道机制进行流转的, 对于每个请求都是一层一层处理,如图10.2所示,当客户端请求达到服务端后, 请求被抽象成Request 对象后向4个容器进行传递,首先经过Engine容器的管道通过若干阀门,最后通过StandardEngineValve阀门流转到Host容器的管道,处理后继续往下流转,通过StandardContextValve阀门流转到Wrapper 容器的管道,而对Servlet的核心处理也正是StandardWrapperValve阀门中,StandardWrapperValve阀门先由ApplicationFilterChain组件执行过滤器,然后调用Servlet的service()方法进行请求处理,然后对客户端响应。
在这里插入图片描述
  下面更深入的讨论StandardWrapperValve阀门调用Servlet的过程 , Web应用的Servlet都依据Servlet接口,例如一般我们写业务处理Servlet类都会继承HttpServlet类,为了遵循Servlet规范,它其实最终也实现了Servlet接口,只是HttpServlet定义了HTTP协议的Servlet ,将协议共性的东西抽离出来复用。 Servlet处理客户端请求的核心方法为service方法,所以对于HttpServelt来说,它需要针对HTTP协议的GET,POST,PUT,DELETE, HEAD ,OPTIONS ,TRACE等请求方法做出不同的分发处理,为了方便理解,下面用个简单的代码展示 。

protected void service(HttpServletRequest req, HttpServletResponse resp)
    throws ServletException, IOException {

    String method = req.getMethod();

    if (method.equals(METHOD_GET)) {
        long lastModified = getLastModified(req);
        if (lastModified == -1) {
            // servlet doesn't support if-modified-since, no reason
            // to go through further expensive logic
            doGet(req, resp);
        } else {
            long ifModifiedSince;
            try {
                ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
            } catch (IllegalArgumentException iae) {
                // Invalid date header - proceed as if none was set
                ifModifiedSince = -1;
            }
            if (ifModifiedSince < (lastModified / 1000 * 1000)) {
                // If the servlet mod time is later, call doGet()
                // Round down to the nearest second for a proper compare
                // A ifModifiedSince of -1 will always be less
                maybeSetLastModified(resp, lastModified);
                doGet(req, resp);
            } else {
                resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
            }
        }

    } else if (method.equals(METHOD_HEAD)) {
        long lastModified = getLastModified(req);
        maybeSetLastModified(resp, lastModified);
        doHead(req, resp);

    } else if (method.equals(METHOD_POST)) {
        doPost(req, resp);

    } else if (method.equals(METHOD_PUT)) {
        doPut(req, resp);

    } else if (method.equals(METHOD_DELETE)) {
        doDelete(req, resp);

    } else if (method.equals(METHOD_OPTIONS)) {
        doOptions(req,resp);

    } else if (method.equals(METHOD_TRACE)) {
        doTrace(req,resp);

    } else {
        //
        // Note that this means NO servlet supports whatever
        // method was requested, anywhere on this server.
        //

        String errMsg = lStrings.getString("http.method_not_implemented");
        Object[] errArgs = new Object[1];
        errArgs[0] = method;
        errMsg = MessageFormat.format(errMsg, errArgs);

        resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
    }
}

  而如果是jsp ,则调用的是HttpJspBase的service()方法 。

public abstract class HttpJspBase extends HttpServlet implements HttpJspPage {

    private static final long serialVersionUID = 1L;

    protected HttpJspBase() {
    }

    @Override
    public final void init(ServletConfig config)
        throws ServletException
    {
        super.init(config);
        jspInit();
        _jspInit();
    }

    @Override
    public String getServletInfo() {
        return Localizer.getMessage("jsp.engine.info");
    }

    @Override
    public final void destroy() {
        jspDestroy();
        _jspDestroy();
    }

    /**
     * Entry point into service.
     */
    @Override
    public final void service(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException
    {
        _jspService(request, response);
    }

    @Override
    public void jspInit() {
    }

    public void _jspInit() {
    }

    @Override
    public void jspDestroy() {
    }

    protected void _jspDestroy() {
    }

    @Override
    public abstract void _jspService(HttpServletRequest request,
                                     HttpServletResponse response)
        throws ServletException, IOException;
}

  看一下index.jsp生成的servlet文件。

public final class index_jsp extends HttpJspBase implements JspSourceDependent {
    private static ProtectedFunctionMapper _jspx_fnmap_0 = ProtectedFunctionMapper.getMapForFunction("MyEL:getTestDto", MyEL.class, "getTestDto", new Class[0]);
    private static final JspFactory _jspxFactory = JspFactory.getDefaultFactory();
    private static Map<String, Long> _jspx_dependants = new HashMap(1);
    private volatile ExpressionFactory _el_expressionfactory;
    private volatile InstanceManager _jsp_instancemanager;

    static {
        _jspx_dependants.put("/WEB-INF/MyEL.tld", 1666688090000L);
    }

    public index_jsp() {
    }

    public Map<String, Long> getDependants() {
        return _jspx_dependants;
    }

    public ExpressionFactory _jsp_getExpressionFactory() {
        if (this._el_expressionfactory == null) {
            synchronized(this) {
                if (this._el_expressionfactory == null) {
                    this._el_expressionfactory = _jspxFactory.getJspApplicationContext(this.getServletConfig().getServletContext()).getExpressionFactory();
                }
            }
        }

        return this._el_expressionfactory;
    }

    public InstanceManager _jsp_getInstanceManager() {
        if (this._jsp_instancemanager == null) {
            synchronized(this) {
                if (this._jsp_instancemanager == null) {
                    this._jsp_instancemanager = InstanceManagerFactory.getInstanceManager(this.getServletConfig());
                }
            }
        }

        return this._jsp_instancemanager;
    }

    public void _jspInit() {
    }

    public void _jspDestroy() {
    }

    public void _jspService(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        JspWriter out = null;
        JspWriter _jspx_out = null;
        PageContext _jspx_page_context = null;

        try {
            response.setContentType("text/html; charset=UTF-8");
            PageContext pageContext = _jspxFactory.getPageContext(this, request, response, (String)null, true, 8192, true);
            _jspx_page_context = pageContext;
            pageContext.getServletContext();
            pageContext.getServletConfig();
            pageContext.getSession();
            out = pageContext.getOut();
            out.write("\n");
            out.write("<!-- tld中的uri和short-name -->\n");
            out.write("<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">\n");
            out.write("<html>\n");
            out.write("<head>\n");
            out.write("      <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n");
            out.write("      <title>Insert title here</title>\n");
            out.write("</head>\n");
            out.write("<body>\n");
            out.write("\n");
            out.write((String)PageContextImpl.proprietaryEvaluate("${MyEL:getTestDto().getMyUsername(\"小明\") }", String.class, pageContext, _jspx_fnmap_0, false));
            out.write("<br>\n");
            out.write("\n");
            out.write("</body>\n");
            out.write("</html>");
        } catch (Throwable var13) {
            if (!(var13 instanceof SkipPageException)) {
                out = (JspWriter)_jspx_out;
                if (_jspx_out != null && ((JspWriter)_jspx_out).getBufferSize() != 0) {
                    try {
                        if (response.isCommitted()) {
                            out.flush();
                        } else {
                            out.clearBuffer();
                        }
                    } catch (IOException var12) {
                    }
                }

                if (_jspx_page_context == null) {
                    throw new ServletException(var13);
                }

                _jspx_page_context.handlePageException(var13);
            }
        } finally {
            _jspxFactory.releasePageContext(_jspx_page_context);
        }

    }
}

  这个index_jsp.java文件继承了HttpJspBase,并重写了_jspService()方法。因此对于JspServlet的调用,实际上是调用生成的Servlet文件,关于jsp生成servlet的详细细节,请看之前的博客 。

protected void doGet(HttpServletRequest request,
                     HttpServletResponse response)
    throws IOException, ServletException {

    // Serve the requested resource, including the data content
    serveResource(request, response, true);

}



protected void serveResource(HttpServletRequest request,
                             HttpServletResponse response,
                             boolean content)
    throws IOException, ServletException {

    boolean serveContent = content;

    // Identify the requested resource path
    String path = getRelativePath(request, true);

    if (debug > 0) {
        if (serveContent)
            log("DefaultServlet.serveResource:  Serving resource '" +
                path + "' headers and data");
        else
            log("DefaultServlet.serveResource:  Serving resource '" +
                path + "' headers only");
    }

    if (path.length() == 0) {
        // Context root redirect
        doDirectoryRedirect(request, response);
        return;
    }

    CacheEntry cacheEntry = resources.lookupCache(path);
    boolean isError = DispatcherType.ERROR == request.getDispatcherType();

    if (!cacheEntry.exists) {
        // Check if we're included so we can return the appropriate
        // missing resource name in the error
        String requestUri = (String) request.getAttribute(
                RequestDispatcher.INCLUDE_REQUEST_URI);
        if (requestUri == null) {
            requestUri = request.getRequestURI();
        } else {
            // We're included
            // SRV.9.3 says we must throw a FNFE
            throw new FileNotFoundException(sm.getString(
                    "defaultServlet.missingResource", requestUri));
        }

        if (isError) {
            response.sendError(((Integer) request.getAttribute(
                    RequestDispatcher.ERROR_STATUS_CODE)).intValue());
        } else {
            response.sendError(HttpServletResponse.SC_NOT_FOUND, requestUri);
        }
        return;
    }

    // Check if the conditions specified in the optional If headers are
    // satisfied.
    if (cacheEntry.context == null) {
        // Checking If headers
        boolean included = (request.getAttribute(
                RequestDispatcher.INCLUDE_CONTEXT_PATH) != null);
        if (!included && !isError &&
        		// 每一次前端访问时,会带着这个文件的最后修改时间
        		// 如果服务器文件没有被修改过,此时直接返回304,这也是一种提升性能的考虑 
                !checkIfHeaders(request, response, cacheEntry.attributes)) {
            return;
        }
    }

    // Find content type.
    String contentType = cacheEntry.attributes.getMimeType();
    if (contentType == null) {
        contentType = getServletContext().getMimeType(cacheEntry.name);
        cacheEntry.attributes.setMimeType(contentType);
    }

    ArrayList<Range> ranges = null;
    long contentLength = -1L;

    if (cacheEntry.context != null) {
        if (!path.endsWith("/")) {
            doDirectoryRedirect(request, response);
            return;
        }

        // Skip directory listings if we have been configured to
        // suppress them
        // 如果前端访问的不是一个文件,而是一个目录时,如果list为false,则不允许访问一个目录,返回404 状态码
        if (!listings) {
            response.sendError(HttpServletResponse.SC_NOT_FOUND,
                               request.getRequestURI());
            return;
        }
        contentType = "text/html;charset=UTF-8";
    } else {
        if (!isError) {
            if (useAcceptRanges) {
                // Accept ranges header
                response.setHeader("Accept-Ranges", "bytes");
            }

            // Parse range specifier
            ranges = parseRange(request, response, cacheEntry.attributes);

            // ETag header
            response.setHeader("ETag", cacheEntry.attributes.getETag());

            // Last-Modified header
            response.setHeader("Last-Modified",
                    cacheEntry.attributes.getLastModifiedHttp());
        }

        // Get content length
        contentLength = cacheEntry.attributes.getContentLength();
        // Special case for zero length files, which would cause a
        // (silent) ISE when setting the output buffer size
        if (contentLength == 0L) {
            serveContent = false;
        }
    }

    ServletOutputStream ostream = null;
    PrintWriter writer = null;

    if (serveContent) {
        // Trying to retrieve the servlet output stream
        try {
            ostream = response.getOutputStream();
        } catch (IllegalStateException e) {
            // If it fails, we try to get a Writer instead if we're
            // trying to serve a text file
            if ( (contentType == null)
                    || (contentType.startsWith("text"))
                    || (contentType.endsWith("xml"))
                    || (contentType.contains("/javascript")) ) {
                writer = response.getWriter();
                // Cannot reliably serve partial content with a Writer
                ranges = FULL;
            } else {
                throw e;
            }
        }
    }

    // Check to see if a Filter, Valve or wrapper has written some content.
    // If it has, disable range requests and setting of a content length
    // since neither can be done reliably.
    ServletResponse r = response;
    long contentWritten = 0;
    while (r instanceof ServletResponseWrapper) {
        r = ((ServletResponseWrapper) r).getResponse();
    }
    if (r instanceof ResponseFacade) {
        contentWritten = ((ResponseFacade) r).getContentWritten();
    }
    if (contentWritten > 0) {
        ranges = FULL;
    }

    if ( (cacheEntry.context != null)
            || isError
            || ( ((ranges == null) || (ranges.isEmpty()))
                    && (request.getHeader("Range") == null) )
            || (ranges == FULL) ) {

        // Set the appropriate output headers
        if (contentType != null) {
            if (debug > 0)
                log("DefaultServlet.serveFile:  contentType='" +
                    contentType + "'");
            response.setContentType(contentType);
        }
        if ((cacheEntry.resource != null) && (contentLength >= 0)
                && (!serveContent || ostream != null)) {
            if (debug > 0)
                log("DefaultServlet.serveFile:  contentLength=" +
                    contentLength);
            // Don't set a content length if something else has already
            // written to the response.
            if (contentWritten == 0) {
                if (contentLength < Integer.MAX_VALUE) {
                    response.setContentLength((int) contentLength);
                } else {
                    // Set the content-length as String to be able to use a
                    // long
                    response.setHeader("content-length",
                            "" + contentLength);
                }
            }
        }

        InputStream renderResult = null;
        if (cacheEntry.context != null) {

            if (serveContent) {
                // Serve the directory browser
                renderResult = render(getPathPrefix(request), cacheEntry);
            }

        }

        // Copy the input stream to our output stream (if requested)
        if (serveContent) {
            try {
                response.setBufferSize(output);
            } catch (IllegalStateException e) {
                // Silent catch
            }
            if (ostream != null) {
                if (!checkSendfile(request, response, cacheEntry, contentLength, null))
                    copy(cacheEntry, renderResult, ostream);
            } else {
                copy(cacheEntry, renderResult, writer);
            }
        }

    } else {

        if ((ranges == null) || (ranges.isEmpty()))
            return;

        // Partial content response.

        response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
        if (ranges.size() == 1) {
            Range range = ranges.get(0);
            response.addHeader("Content-Range", "bytes "
                               + range.start
                               + "-" + range.end + "/"
                               + range.length);
            long length = range.end - range.start + 1;
            if (length < Integer.MAX_VALUE) {
                response.setContentLength((int) length);
            } else {
                // Set the content-length as String to be able to use a long
                response.setHeader("content-length", "" + length);
            }

            if (contentType != null) {
                if (debug > 0)
                    log("DefaultServlet.serveFile:  contentType='" +
                        contentType + "'");
                response.setContentType(contentType);
            }

            if (serveContent) {
                try {
                    response.setBufferSize(output);
                } catch (IllegalStateException e) {
                    // Silent catch
                }
                if (ostream != null) {
                    if (!checkSendfile(request, response, cacheEntry, range.end - range.start + 1, range))
                        copy(cacheEntry, ostream, range);
                } else {
                    // we should not get here
                    throw new IllegalStateException();
                }
            }
        } else {
            response.setContentType("multipart/byteranges; boundary="
                                    + mimeSeparation);
            if (serveContent) {
                try {
                    response.setBufferSize(output);
                } catch (IllegalStateException e) {
                    // Silent catch
                }
                if (ostream != null) {
                    copy(cacheEntry, ostream, ranges.iterator(),
                         contentType);
                } else {
                    // we should not get here
                    throw new IllegalStateException();
                }
            }
        }
    }
}

  如果访问的是一个文件,最终经过层层较验,通过copy()方法将文件内容写到前端。

protected void copy(CacheEntry cacheEntry, InputStream is,
                  ServletOutputStream ostream)
    throws IOException {

    IOException exception = null;
    InputStream resourceInputStream = null;

    // Optimization: If the binary content has already been loaded, send
    // it directly
    if (cacheEntry.resource != null) {
        byte buffer[] = cacheEntry.resource.getContent();
        if (buffer != null) {
            ostream.write(buffer, 0, buffer.length);
            return;
        }
        resourceInputStream = cacheEntry.resource.streamContent();
    } else {
        resourceInputStream = is;
    }

    InputStream istream = new BufferedInputStream
        (resourceInputStream, input);

    // Copy the input stream to the output stream
    exception = copyRange(istream, ostream);

    // Clean up the input stream
    istream.close();

    // Rethrow any exception that has occurred
    if (exception != null)
        throw exception;
}
protected IOException copyRange(InputStream istream,
                              ServletOutputStream ostream) {

    // Copy the input stream to the output stream
    IOException exception = null;
    byte buffer[] = new byte[input];
    int len = buffer.length;
    while (true) {
        try {
            len = istream.read(buffer);
            if (len == -1)
                break;
            ostream.write(buffer, 0, len);
        } catch (IOException e) {
            exception = e;
            len = -1;
            break;
        }
    }
    return exception;

}

在这里插入图片描述
访问效果如下
在这里插入图片描述

如果访问的是一个目录时

protected InputStream render(String contextPath, CacheEntry cacheEntry)
    throws IOException, ServletException {

    Source xsltSource = findXsltInputStream(cacheEntry.context);

    if (xsltSource == null) {
        return renderHtml(contextPath, cacheEntry);
    }
    return renderXml(contextPath, cacheEntry, xsltSource);

}


protected InputStream renderHtml(String contextPath, CacheEntry cacheEntry)
    throws IOException, ServletException {

    String name = cacheEntry.name;

    // Prepare a writer to a buffered area
    ByteArrayOutputStream stream = new ByteArrayOutputStream();
    OutputStreamWriter osWriter = new OutputStreamWriter(stream, "UTF-8");
    PrintWriter writer = new PrintWriter(osWriter);

    StringBuilder sb = new StringBuilder();

    // rewriteUrl(contextPath) is expensive. cache result for later reuse
    String rewrittenContextPath =  rewriteUrl(contextPath);

    // Render the page header
    sb.append("<html>\r\n");
    sb.append("<head>\r\n");
    sb.append("<title>");
    sb.append(sm.getString("directory.title", name));
    sb.append("</title>\r\n");
    sb.append("<STYLE><!--");
    sb.append(org.apache.catalina.util.TomcatCSS.TOMCAT_CSS);
    sb.append("--></STYLE> ");
    sb.append("</head>\r\n");
    sb.append("<body>");
    sb.append("<h1>");
    sb.append(sm.getString("directory.title", name));

    // Render the link to our parent (if required)
    String parentDirectory = name;
    if (parentDirectory.endsWith("/")) {
        parentDirectory =
            parentDirectory.substring(0, parentDirectory.length() - 1);
    }
    int slash = parentDirectory.lastIndexOf('/');
    if (slash >= 0) {
        String parent = name.substring(0, slash);
        sb.append(" - <a href=\"");
        sb.append(rewrittenContextPath);
        if (parent.equals(""))
            parent = "/";
        sb.append(rewriteUrl(parent));
        if (!parent.endsWith("/"))
            sb.append("/");
        sb.append("\">");
        sb.append("<b>");
        sb.append(sm.getString("directory.parent", parent));
        sb.append("</b>");
        sb.append("</a>");
    }

    sb.append("</h1>");
    sb.append("<HR size=\"1\" noshade=\"noshade\">");

    sb.append("<table width=\"100%\" cellspacing=\"0\"" +
                 " cellpadding=\"5\" align=\"center\">\r\n");

    // Render the column headings
    sb.append("<tr>\r\n");
    sb.append("<td align=\"left\"><font size=\"+1\"><strong>");
    sb.append(sm.getString("directory.filename"));
    sb.append("</strong></font></td>\r\n");
    sb.append("<td align=\"center\"><font size=\"+1\"><strong>");
    sb.append(sm.getString("directory.size"));
    sb.append("</strong></font></td>\r\n");
    sb.append("<td align=\"right\"><font size=\"+1\"><strong>");
    sb.append(sm.getString("directory.lastModified"));
    sb.append("</strong></font></td>\r\n");
    sb.append("</tr>");

    try {

        // Render the directory entries within this directory
        NamingEnumeration<NameClassPair> enumeration =
            resources.list(cacheEntry.name);
        boolean shade = false;
        while (enumeration.hasMoreElements()) {

            NameClassPair ncPair = enumeration.nextElement();
            String resourceName = ncPair.getName();
            String trimmed = resourceName/*.substring(trim)*/;
            if (trimmed.equalsIgnoreCase("WEB-INF") ||
                trimmed.equalsIgnoreCase("META-INF"))
                continue;

            CacheEntry childCacheEntry =
                resources.lookupCache(cacheEntry.name + resourceName);
            if (!childCacheEntry.exists) {
                continue;
            }

            sb.append("<tr");
            if (shade)
                sb.append(" bgcolor=\"#eeeeee\"");
            sb.append(">\r\n");
            shade = !shade;

            sb.append("<td align=\"left\">  \r\n");
            sb.append("<a href=\"");
            sb.append(rewrittenContextPath);
            resourceName = rewriteUrl(name + resourceName);
            sb.append(resourceName);
            if (childCacheEntry.context != null)
                sb.append("/");
            sb.append("\"><tt>");
            sb.append(RequestUtil.filter(trimmed));
            if (childCacheEntry.context != null)
                sb.append("/");
            sb.append("</tt></a></td>\r\n");

            sb.append("<td align=\"right\"><tt>");
            if (childCacheEntry.context != null)
                sb.append(" ");
            else
                sb.append(renderSize(childCacheEntry.attributes.getContentLength()));
            sb.append("</tt></td>\r\n");

            sb.append("<td align=\"right\"><tt>");
            sb.append(childCacheEntry.attributes.getLastModifiedHttp());
            sb.append("</tt></td>\r\n");

            sb.append("</tr>\r\n");
        }

    } catch (NamingException e) {
        // Something went wrong
        throw new ServletException("Error accessing resource", e);
    }

    // Render the page footer
    sb.append("</table>\r\n");

    sb.append("<HR size=\"1\" noshade=\"noshade\">");

    String readme = getReadme(cacheEntry.context);
    if (readme!=null) {
        sb.append(readme);
        sb.append("<HR size=\"1\" noshade=\"noshade\">");
    }

    if (showServerInfo) {
        sb.append("<h3>").append(ServerInfo.getServerInfo()).append("</h3>");
    }
    sb.append("</body>\r\n");
    sb.append("</html>\r\n");

    // Return an input stream to the underlying bytes
    System.out.println("返回的html:" + sb.toString());
    writer.write(sb.toString());
    writer.flush();
    return new ByteArrayInputStream(stream.toByteArray());

}

  当然,如果访问一个目录被允许的话,前提条件是
在这里插入图片描述
  listing参数为true。
  当然访问结果如下
在这里插入图片描述
  当然JNDI查找文件这一块的内容,可以去看之前的博客Tomcat 源码解析一JNDI

总节:

  历时半年,终于将Tomcat的源码过了一遍,当然关于集群,权限这一块没有深入研究,其中的理论知识来源于《Tomcat内核设计剖析》和《Tomcat架构解析.刘光瑞》这两本书,在没有研究Tomcat之前,唯一知道就是怎样用Tomcat,将WAR包放到Tomcat的webapps下,运行start脚本,启动tomcat,到现在,至少我觉得我能去定制化Tomcat了, 刚开始非常痛苦,根本弄不清Tomcat的Server,Engine,Host,Context, Wrapper ,在请求过程中又有管道的流转,Tomcat各种类加载器,都能将你弄晕, 刚开始非常迷茫,之前写了一篇 Tomcat 源码解析一初识 博客,当发现Degister框架时,终于解答了我多年的疑惑,原来xml的解析如此简单,之前解析xml那是一个难受,现在终于找到了XML的解析完美方案,那我觉得Tomcat原来也没有那么难嘛,当写 Tomcat 源码解析一请求处理的整体过程-黄泉天怒 这篇博客时,简直从入门到放弃,里面有太多的东西不知道是什么,也知道管道是什么,不知道StandardContext是什么,又是何时初始化,何时来的呢?因此这篇博客就搁置了,没有办法,既然弄不明白,那就先放弃吧, 后面就先从边边角角的地方去研究,之后先研究了 Tomcat 源码解析一JNDI , Tomcat 源码解析一EL表达式源码解析
Tomcat 源码解析一JSP编译器Jasper-佛怒火莲(上) ,当然将JSP研究明白后,已用时一个多月了,此时发现这些边边角角的东西已经不再是我的羁绊,向Tomcat的核心功能出发,Tomcat 源码解析一容器加载-大寂灭指 系列, 又花了一个多月的时间,当再来看 Tomcat 源码解析一请求处理的整体过程 时,已经是水到渠成的事情,我看网上有一些源码课,老师花几节课就将整个过程讲得差不多了,但那也是老师理解,你依然还是不理解,最多你也只是知道这个现象,当一个牛逼的面试官问你时,你自己都不自信,面试官会信吗?当遇到具体的业务问题时,你依然还是不会,遇到问题还是老办法百度,最终也没有形成自己的思想,如果想自己写一个框架,当然还是无法从Tomcat源码中得到借鉴,当遇到一些问题时,依然无法从源码中找到答案,当然这里还有一个误区,有人说研究懂Spring源码,再去看其他源码就两三个星期就明白了,我个人觉得,如果只是林林散散的看看,那肯定是没有问题的,如果想将博客写出来,并且对里面的细节有一定认知,肯定是不够的,因此在研究源码时,要耐得住枯燥,耐得住寂寞,最好做到没有时间观念,舍得花时间,最终你才会有所收获。

  历时半年,Tomcat的源码研究又告一段落了,我得到了什么呢? BIO,NIO的实现,Degister框架,Tomcat中对字节码结构解析,JSP 文件解析生成Servlet的过程,JSP的编译,SMAP修改字节码,EL 表达式的解析,以及EL表达式 从应用中获取值的过程,HostConfig的启动,StandardContext启动过程,管道的设计模式,过滤器的设计模式,Tomcat 热布署思想, JNDI框架的设计及应用,Tomcat中的类加载器等,当然Tomcat的权限,认证,集群,APR 通信方式,目前不做深入研究,因为我觉得目前市面上有很多的集群框架,如Double , Spring Clound 更值得研究,而APR 通信也是Tomcat 内部节点之间的通信,感觉没有太大的借鉴意义,目前放下,说不定以后我觉得他也很重要了,再来写相关博客。

  接下来要去研究Netty,希望后面的博客再次相遇。

github相关源码

https://github.com/quyixiao/tomcat.git

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/10780.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

07.URL调度器工作原理

1.工作原理 django通过urlconf来映射视图函数&#xff0c;只区分路径&#xff0c;不区分http方法 Django确定要使用的根URLconf模块&#xff0c;一般实在settings中的ROOT_URLCONF设置的值&#xff0c;但是如果传入HttpRequest对象具有一个urlconf属性&#xff08;由中间件设置…

直播预告|OceanBase 社区版 4.0 全解析

小就是大&#xff0c;OceanBase 社区版 4.0 | 单机到分布式一体化与最佳实践 随着云原生时代的到来&#xff0c;越来越多的业务系统会采用云原生架构。云原生数据库通过资源解耦和资源池化等技术支撑了不同业务领域的场景需求。 2022 年 11 月 3 日&#xff0c;OceanBase 社区版…

Linux结构目录说明以及相关作用【重点】

目录目录结构目录结构介绍&#xff1a;/bin [重点] (/usr/bin 、 /usr/local/bin)/home/root/sbin/lib/lost found/etc/usr/boot/dev/media/mnt/opt/usr/local/var目录结构 1&#xff1a;图解目录结构 2&#xff1a;用命令行看目录结构&#xff08;里面自己装了一些东西&…

1024程序员节过后,我才知道JVM有多重要

为什么要学习JVM? 和大部分程序员一样&#xff0c;我最初接触 Java 虚拟机只是因为面试官会问Java虚拟机的相关问题&#xff0c;所以不得不看&#xff0c;而且刷了很多面试题&#xff1b;那段时间刷面试题刷的心烦甚至会觉得&#xff0c;我面试的岗位平时也用不上Java虚拟机&…

奉加微蓝牙芯片PHY6222,支持mesh,SRAM、可选128K-8M

奉加微蓝牙芯片PHY6222&#xff0c;支持mesh&#xff0c;SRAM、可选128K-8M ​SIG mesh和ZigBee两种2.4G的无线mesh&#xff0c;有Nordic的NRF52840&#xff1b;泰凌微的TLSR825X、TLSR9X&#xff1b;还有奉加微的PHY62xx系列是一款支持BLE 5.2功能和IEEE 802.15.4&#xff08;…

【数据结构基础】时间复杂度和空间复杂度

&#x1f648;个人主页&#xff1a;阿伟t &#x1f449;系列专栏&#xff1a;【C语言–大佬之路】 &#x1f388;今日心语&#xff1a;你所看到的惊艳&#xff0c;都曾被平庸所历练。 数据结构指的是“一组数据的存储结构”&#xff0c;算法指的是“操作数据的一组方法”。 数…

Spring Boot快速入门

目录 1、Spring Boot概述 1.1、什么是Spring Boot 1.2、Spring Boot的特点 2、Spring Boot快速入门 2.1、起步依赖 2.2、创建启动类 2.3、测试启动类 2.4、自定义banner 2.5、自动配置原理 2.6、启动类注解 3、Spring Boot整合SpringMVC 3.1、Controller类 3.2、…

效率为王,居家办公必备的5款小工具

疫情反反复复&#xff0c;好多小伙伴依然处在居家办公的境地中&#xff0c;所以像小编一样早早的干完活躺床上是非常有必要的。 1.极速复制粘贴工具——TeraCopy TeraCopy是一款老牌免费的 Windows 文件复制/移动辅助加速工具软件&#xff0c;复制文件时若是遇上数据有问题也…

《FFmpeg Basics》中文版-03-比特率/帧率/文件大小

正文 比特率和帧速率是视频的基本特征&#xff0c;它们的正确设置对整体视频质量非常重要。 如果我们知道所有包含的媒体流的比特率和持续时间&#xff0c;我们可以计算输出文件的最终大小。 由于在使用FFmpeg工具时对帧速率和比特率的理解很重要&#xff0c;因此包含每个术语…

文件存储解决方案-云存储阿里 OSS

文件存储解决方案-云存储阿里 OSS 1.文件存储&#xff08;上传&#xff09;解决方案讨论 1.图解 文件存储解决方案-云存储阿里 OSS 解读上图 普通上传并不是分布式&#xff0c;也不是集群&#xff0c;可用性不高普通上传的分布式情况&#xff0c;使用了集群&#xff0c;但是…

数据结构每日亿题(七)

文章目录一题目二.思路2.1链表2.2数组三.代码一题目 原题传送门&#xff1a;力扣 题目&#xff1a; 题目的意思是让你写一个数据结构&#xff0c;这个结构的特点和队列一样先进先出&#xff0c;然后完成&#xff1a;判断是否为空&#xff0c;判断是否为满&#xff0c;添加一个…

实验29:循迹传感器实验

今天讲一个基本实验 循迹实验 循迹传感器的原理是: CTRT5000传感器的红外发射管不断发射红外光。由于黑色吸收光线,当红外发射管照射黑色表面时,反射光较少,接收管接收的红外线较少。这表明黑色吸收光线的强度大,那么比较器输出高电平,指示灯熄灭。同样,当它在白色表面…

MCE虚拟筛选化合物库

Discovery Diversity Sets 新颖的化合物库&#xff01; 药物筛选是发现药物先导物的重要途径&#xff0c;好的化合物库则是药物筛选的必备武器。MCE 拥有丰富的数据库资源&#xff0c;助力您的药物筛选研究&#xff01;药物筛选研究与化合物新颖性密切相关。Discovery Divers…

单目标应用:求解旅行商问题(TSP)的猎豹优化算法(The Cheetah Optimizer,CO)提供MATLAB代码

一、猎豹优化算法 猎豹优化算法&#xff08;The Cheetah Optimizer&#xff0c;CO&#xff09;由MohammadAminAkbari等人于2022年提出&#xff0c;该算法性能高效&#xff0c;思路新颖。 参考文献&#xff1a; Akbari, M.A., Zare, M., Azizipanah-abarghooee, R. et al. The…

Linux实战案例——使用LNMP+WordPress搭建个人博客网站

一、案例目标 了解 LNMP 环境的组成。 了解 LNMP 环境的部署与安装。 了解 WordPress 应用的部署与使用二、环境介绍 1.LNMP LNMP是指一组通常一起使用来运行动态网站或者服务器的自由软件名称首字母缩写。L指Linux&#xff0c;N指Nginx&#xff0c;M一般指MySQL&#xff0c;也…

技术分享 | TiUP工具 - TiDB集群滚动升级核心流程解析

作者&#xff1a;贲绍华 爱可生研发中心工程师&#xff0c;负责项目的需求与维护工作。其他身份&#xff1a;柯基铲屎官。 本文来源&#xff1a;原创投稿 *爱可生开源社区出品&#xff0c;原创内容未经授权不得随意使用&#xff0c;转载请联系小编并注明来源。 引言&#xff1a…

运维工程师怎么找兼职?什么样的兼职合适?

运维老哥们应该都知道&#xff0c;这个岗位其实是个很宽泛的定义&#xff0c;不同公司对运维的要求也不一样。有些公司所谓运维就是桌面helpdesk ,有些公司就是网管。基本上从修电脑到会写点脚本做自动化&#xff0c;各个层次的都有。现状就是&#xff0c;有少数公司或者大厂的…

基于Django的图书交易系统

摘 要 随着信息化时代的到来&#xff0c;管理系统都趋向于智能化、系统化&#xff0c;图书交易系统也不例外&#xff0c;但目前国内的有些公司仍然都使用人工管理&#xff0c;图书销量越来越大&#xff0c;同时信息量也越来越庞大&#xff0c;人工管理显然已无法应对时代的变化…

第五课 算术运算

一、加法与减法 经过上次课的学习&#xff0c;我们已经学会了该如何在控制台中输出我们想要的内容了&#xff0c;但是计算机计算机&#xff0c;最早其实是用来进行计算的&#xff0c;Python能做这事吗&#xff0c;我们来试试。 看到这结果&#xff0c;估计很多同学就该说了&…

【每周研报复现】基于阻力支撑相对强度(RSRS)的市场择时

原创文章第106篇&#xff0c;专注“个人成长与财富自由、世界运作的逻辑&#xff0c; AI量化投资”。 今天要复现的研报是&#xff1a;”光大证券_金融工程深度&#xff1a;基于阻力支撑相对强度&#xff08;RSRS&#xff09;的市场择时——技术择时系列报告之一“。 研报核心…