Nicksxs's Blog

What hurts more, the pain of hard work or the pain of regret?

昨天同学问我是不是数据库主从延迟有点高,可能有一分多钟,然后我就去看了rds 的监控,发现主实例上的监控显示的延迟才 1.2 秒,而且是最高 1.2 秒,感觉这样的话应该就没啥问题,然后同学跟我说他加了日志,大致的逻辑是主库数据落库以后就会发一条 mq 消息出来,然后消费者接收到以后回去从库查一下这个数据,结果发现延迟了 90 多秒才查到数据,这种情况比较可能的猜测是阿里云这个监控的逻辑可能是从库在获得第一条同步数据的时候,而不是最终同步完成,但是跟阿里云咨询了并不是,使用的就是 show slave status 结果里的 Seconds_Behind_Master 指标,那这第一种情况就否定掉了,这里其实是犯了个错误,应该去从库看这个延迟的,不过对于阿里云来说在 rds 监控是看不到从库的监控的,只能到 das,也就是阿里云的数据库自治服务可以看到,这里能看到从库的延迟监控,发现的确有这么高,这样就要考虑为啥会出现这种情况,阿里云同学反馈的是这段时间的 iops 很高,并且 cpu 也比较高,让我排查下 binlog,这里就碰到一个小问题,阿里云 rds 的线上实例我们没法在本地连接,并且密码也是在代码里加密过的,去服务器上连接一方面需要装 mysql 客户端,另一方面是怕拉取日志会有性能影响,幸好阿里云这点做的比较好,在 rds 的”备份恢复”–> “日志备份”菜单里可以找到binlog 文件, 在这里其实大致就发现了问题,因为出问题的时间段内升成 binlog 的量大大超过其他时间段,然后通过内网下载后就可以对 binlog 进行分析了,这里我们用到了 mysqlbinlog 工具,主要就是找具体是哪些写入导致这个问题,mysqlbinlog 可以在 mysql 官网下载 mysql 的压缩包,注意是压缩包,这样解压了直接用就好了,不用完整安装 mysql,一般我们需要对 binlog 进行 base64 的反编码,

./mysqlbinlog -vv --base64-output=decode-rows mysql-bin.xxxx | less

这样查看里面的具体信息,发现是大量的 insert 数据,再经过排查发现同一个 binlog 文件近 500M 全是同一个表的数据插入,再根据表对应的业务查找发现是有个业务逻辑会在这个时间点全量删除后在生成插入数据,后续需要进行优化

最近同学在把 springboot 升级到 2.x 版本的过程中碰到了小问题,可能升级变更里能找到信息,不过我们以学习为目的,可以看看代码是怎么样的
报错是在这段代码里的
org.apache.tomcat.util.http.fileupload.util.LimitedInputStream#checkLimit

private void checkLimit() throws IOException {
    if (count > sizeMax) {
        raiseError(sizeMax, count);
    }
}

其中的 raiseError 是个抽象方法

protected abstract void raiseError(long pSizeMax, long pCount)
            throws IOException;

具体的实现是在

public FileItemStreamImpl(FileItemIteratorImpl pFileItemIterator, String pName, String pFieldName, String pContentType, boolean pFormField, long pContentLength) throws FileUploadException, IOException {
        this.fileItemIteratorImpl = pFileItemIterator;
        this.name = pName;
        this.fieldName = pFieldName;
        this.contentType = pContentType;
        this.formField = pFormField;
        long fileSizeMax = this.fileItemIteratorImpl.getFileSizeMax();
        if (fileSizeMax != -1L && pContentLength != -1L && pContentLength > fileSizeMax) {
            FileSizeLimitExceededException e = new FileSizeLimitExceededException(String.format("The field %s exceeds its maximum permitted size of %s bytes.", this.fieldName, fileSizeMax), pContentLength, fileSizeMax);
            e.setFileName(pName);
            e.setFieldName(pFieldName);
            throw new FileUploadIOException(e);
        } else {
            final MultipartStream.ItemInputStream itemStream = this.fileItemIteratorImpl.getMultiPartStream().newInputStream();
            InputStream istream = itemStream;
            if (fileSizeMax != -1L) {
                istream = new LimitedInputStream(itemStream, fileSizeMax) {
                    protected void raiseError(long pSizeMax, long pCount) throws IOException {
                        itemStream.close(true);
                        FileSizeLimitExceededException e = new FileSizeLimitExceededException(String.format("The field %s exceeds its maximum permitted size of %s bytes.", FileItemStreamImpl.this.fieldName, pSizeMax), pCount, pSizeMax);
                        e.setFieldName(FileItemStreamImpl.this.fieldName);
                        e.setFileName(FileItemStreamImpl.this.name);
                        throw new FileUploadIOException(e);
                    }
                };
            }

            this.stream = (InputStream)istream;
        }
    }

后面也会介绍到,这里我们其实主要是要找到这个 pSizeMax 是哪里来的
通过阅读代码会发现跟这个类 MultipartConfigElement 有关系
而在升级后的 springboot 中这个类已经有了自动装配类,也就是
org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration

有了这个自动装配

@Configuration(
    proxyBeanMethods = false
)
@ConditionalOnClass({Servlet.class, StandardServletMultipartResolver.class, MultipartConfigElement.class})
@ConditionalOnProperty(
    prefix = "spring.servlet.multipart",
    name = {"enabled"},
    matchIfMissing = true
)
@ConditionalOnWebApplication(
    type = Type.SERVLET
)
@EnableConfigurationProperties({MultipartProperties.class})
public class MultipartAutoConfiguration {
    private final MultipartProperties multipartProperties;

    public MultipartAutoConfiguration(MultipartProperties multipartProperties) {
        this.multipartProperties = multipartProperties;
    }

    @Bean
    @ConditionalOnMissingBean({MultipartConfigElement.class, CommonsMultipartResolver.class})
    public MultipartConfigElement multipartConfigElement() {
        return this.multipartProperties.createMultipartConfig();
    }

而这个 MultipartProperties 类中

@ConfigurationProperties(
    prefix = "spring.servlet.multipart",
    ignoreUnknownFields = false
)
public class MultipartProperties {
    private boolean enabled = true;
    private String location;
    private DataSize maxFileSize = DataSize.ofMegabytes(1L);
    private DataSize maxRequestSize = DataSize.ofMegabytes(10L);
    private DataSize fileSizeThreshold = DataSize.ofBytes(0L);
    private boolean resolveLazily = false;

并且在前面 createMultipartConfig 中就使用了这个maxFileSize 的默认值

public MultipartConfigElement createMultipartConfig() {
    MultipartConfigFactory factory = new MultipartConfigFactory();
    PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
    map.from(this.fileSizeThreshold).to(factory::setFileSizeThreshold);
    map.from(this.location).whenHasText().to(factory::setLocation);
    map.from(this.maxRequestSize).to(factory::setMaxRequestSize);
    map.from(this.maxFileSize).to(factory::setMaxFileSize);
    return factory.createMultipartConfig();
}

而在 org.apache.catalina.connector.Request#parseParts 中,会判断 mce 的配置

private void parseParts(boolean explicit) {

        // 省略一部分代码

            ServletFileUpload upload = new ServletFileUpload();
            upload.setFileItemFactory(factory);
            upload.setFileSizeMax(mce.getMaxFileSize());
            upload.setSizeMax(mce.getMaxRequestSize());

            parts = new ArrayList<>();
            try {
                List<FileItem> items =
                        upload.parseRequest(new ServletRequestContext(this));
                int maxPostSize = getConnector().getMaxPostSize();
                int postSize = 0;
                Charset charset = getCharset();

主要 org.apache.tomcat.util.http.fileupload.FileUploadBase#parseRequest

public List<FileItem> parseRequest(final RequestContext ctx)
            throws FileUploadException {
        final List<FileItem> items = new ArrayList<>();
        boolean successful = false;
        try {
            final FileItemIterator iter = getItemIterator(ctx);
            final FileItemFactory fileItemFactory = Objects.requireNonNull(getFileItemFactory(),
                    "No FileItemFactory has been set.");
            final byte[] buffer = new byte[Streams.DEFAULT_BUFFER_SIZE];
            while (iter.hasNext()) {
                final FileItemStream item = iter.next();
                // Don't use getName() here to prevent an InvalidFileNameException.
                final String fileName = item.getName();
                final FileItem fileItem = fileItemFactory.createItem(item.getFieldName(), item.getContentType(),
                                                   item.isFormField(), fileName);
                items.add(fileItem);
                try {
                    Streams.copy(item.openStream(), fileItem.getOutputStream(), true, buffer);
                } catch (final FileUploadIOException e) {

其中 org.apache.tomcat.util.http.fileupload.FileUploadBase#getItemIterator

public FileItemIterator getItemIterator(final RequestContext ctx)
throws FileUploadException, IOException {
    try {
        return new FileItemIteratorImpl(this, ctx);
    } catch (final FileUploadIOException e) {
        // unwrap encapsulated SizeException
        throw (FileUploadException) e.getCause();
    }
}

这里就创建了 org.apache.tomcat.util.http.fileupload.impl.FileItemIteratorImpl

public FileItemIteratorImpl(final FileUploadBase fileUploadBase, final RequestContext requestContext)
    throws FileUploadException, IOException {
    this.fileUploadBase = fileUploadBase;
    sizeMax = fileUploadBase.getSizeMax();
    fileSizeMax = fileUploadBase.getFileSizeMax();
    ctx = Objects.requireNonNull(requestContext, "requestContext");
    skipPreamble = true;
    findNextItem();
}

内部使用了前面给 upload 设置的文件大小上限 upload.setFileSizeMax(mce.getMaxFileSize());

然后在 findNextItem 里执行了初始化

private boolean findNextItem() throws FileUploadException, IOException {
        if (eof) {
            return false;
        }
        if (currentItem != null) {
            currentItem.close();
            currentItem = null;
        }
        final MultipartStream multi = getMultiPartStream();
        for (;;) {
            final boolean nextPart;
            if (skipPreamble) {
                nextPart = multi.skipPreamble();
            } else {
                nextPart = multi.readBoundary();
            }
            if (!nextPart) {
                if (currentFieldName == null) {
                    // Outer multipart terminated -> No more data
                    eof = true;
                    return false;
                }
                // Inner multipart terminated -> Return to parsing the outer
                multi.setBoundary(multiPartBoundary);
                currentFieldName = null;
                continue;
            }
            final FileItemHeaders headers = fileUploadBase.getParsedHeaders(multi.readHeaders());
            if (currentFieldName == null) {
                // We're parsing the outer multipart
                final String fieldName = fileUploadBase.getFieldName(headers);
                if (fieldName != null) {
                    final String subContentType = headers.getHeader(FileUploadBase.CONTENT_TYPE);
                    if (subContentType != null
                            &&  subContentType.toLowerCase(Locale.ENGLISH)
                                    .startsWith(FileUploadBase.MULTIPART_MIXED)) {
                        currentFieldName = fieldName;
                        // Multiple files associated with this field name
                        final byte[] subBoundary = fileUploadBase.getBoundary(subContentType);
                        multi.setBoundary(subBoundary);
                        skipPreamble = true;
                        continue;
                    }
                    final String fileName = fileUploadBase.getFileName(headers);
                    currentItem = new FileItemStreamImpl(this, fileName,
                            fieldName, headers.getHeader(FileUploadBase.CONTENT_TYPE),
                            fileName == null, getContentLength(headers));
                    currentItem.setHeaders(headers);
                    progressNotifier.noteItem();
                    itemValid = true;
                    return true;
                }
            } else {
                final String fileName = fileUploadBase.getFileName(headers);
                if (fileName != null) {
                    currentItem = new FileItemStreamImpl(this, fileName,
                            currentFieldName,
                            headers.getHeader(FileUploadBase.CONTENT_TYPE),
                            false, getContentLength(headers));
                    currentItem.setHeaders(headers);
                    progressNotifier.noteItem();
                    itemValid = true;
                    return true;
                }
            }
            multi.discardBodyData();
        }
    }

这里面就会 new 这个 FileItemStreamImpl

currentItem = new FileItemStreamImpl(this, fileName,
                            fieldName, headers.getHeader(FileUploadBase.CONTENT_TYPE),
                            fileName == null, getContentLength(headers));

构造方法比较长

public FileItemStreamImpl(final FileItemIteratorImpl pFileItemIterator, final String pName, final String pFieldName,
            final String pContentType, final boolean pFormField,
            final long pContentLength) throws FileUploadException, IOException {
        fileItemIteratorImpl = pFileItemIterator;
        name = pName;
        fieldName = pFieldName;
        contentType = pContentType;
        formField = pFormField;
        final long fileSizeMax = fileItemIteratorImpl.getFileSizeMax();
        if (fileSizeMax != -1 && pContentLength != -1
                && pContentLength > fileSizeMax) {
            final FileSizeLimitExceededException e =
                    new FileSizeLimitExceededException(
                            String.format("The field %s exceeds its maximum permitted size of %s bytes.",
                                    fieldName, Long.valueOf(fileSizeMax)),
                            pContentLength, fileSizeMax);
            e.setFileName(pName);
            e.setFieldName(pFieldName);
            throw new FileUploadIOException(e);
        }
        // OK to construct stream now
        final ItemInputStream itemStream = fileItemIteratorImpl.getMultiPartStream().newInputStream();
        InputStream istream = itemStream;
        if (fileSizeMax != -1) {
            istream = new LimitedInputStream(istream, fileSizeMax) {
                @Override
                protected void raiseError(final long pSizeMax, final long pCount)
                        throws IOException {
                    itemStream.close(true);
                    final FileSizeLimitExceededException e =
                        new FileSizeLimitExceededException(
                            String.format("The field %s exceeds its maximum permitted size of %s bytes.",
                                   fieldName, Long.valueOf(pSizeMax)),
                            pCount, pSizeMax);
                    e.setFieldName(fieldName);
                    e.setFileName(name);
                    throw new FileUploadIOException(e);
                }
            };
        }
        stream = istream;
    }

fileSizeMax != 0 的时候就会初始化 LimitedInputStream,这就就是会在前面的

org.apache.tomcat.util.http.fileupload.FileUploadBase#parseRequest

Streams.copy(item.openStream(), fileItem.getOutputStream(), true, buffer);

这里的 item

final FileItemIterator iter = getItemIterator(ctx);
            final FileItemFactory fileItemFactory = Objects.requireNonNull(getFileItemFactory(),
                    "No FileItemFactory has been set.");
            final byte[] buffer = new byte[Streams.DEFAULT_BUFFER_SIZE];
            while (iter.hasNext()) {
                final FileItemStream item = iter.next();

调用了 FileItemIterator 迭代器的 next

@Override
    public FileItemStream next() throws FileUploadException, IOException {
        if (eof  ||  (!itemValid && !hasNext())) {
            throw new NoSuchElementException();
        }
        itemValid = false;
        return currentItem;
    }

这个 currentItem 就是前面 new 的 FileItemStreamImpl

然后在 Streams.copy 的时候调用 openStream 也就是 org.apache.tomcat.util.http.fileupload.impl.FileItemStreamImpl#openStream

@Override
    public InputStream openStream() throws IOException {
        if (((Closeable) stream).isClosed()) {
            throw new FileItemStream.ItemSkippedException();
        }
        return stream;
    }

这里的 stream 就是 FileItemStreamImpl 构造方法最后赋值的 stream,会在大小超过限制时抛出错误

而这个可以通过设置 properties 来修改,spring.servlet.multipart.max-file-size 和 spring.servlet.multipart.max-request-size

spring.servlet.multipart.max-file-size=100MB
spring.servlet.multipart.max-request-size=100MB

而老版本的 spring.http.multipart.maxFileSize
其实就是配置名称改了下,但是能看一下代码也是有点收获的。

这部分其实之前在讲线程池的时候也有点带到了, 主要是在这个类里
org.apache.catalina.core.ContainerBase.ContainerBackgroundProcessor

protected class ContainerBackgroundProcessor implements Runnable {

        @Override
        public void run() {
            processChildren(ContainerBase.this);
        }

        protected void processChildren(Container container) {
            ClassLoader originalClassLoader = null;

            try {
                if (container instanceof Context) {
                    Loader loader = ((Context) container).getLoader();
                    // Loader will be null for FailedContext instances
                    if (loader == null) {
                        return;
                    }

                    // Ensure background processing for Contexts and Wrappers
                    // is performed under the web app's class loader
                    originalClassLoader = ((Context) container).bind(false, null);
                }
                // 调用 Container 的 backgroundProcess
                container.backgroundProcess();
                // 然后寻找 children
                Container[] children = container.findChildren();
                for (Container child : children) {
                    // 如果 backgroundProcessorDelay <= 0 就调用执行
                    // 否则代表这个 Container 有之前第八篇说的 StartChild 这种
                    if (child.getBackgroundProcessorDelay() <= 0) {
                        processChildren(child);
                    }
                }
            } catch (Throwable t) {
                ExceptionUtils.handleThrowable(t);
                log.error(sm.getString("containerBase.backgroundProcess.error"), t);
            } finally {
                if (container instanceof Context) {
                    ((Context) container).unbind(false, originalClassLoader);
                }
            }
        }
    }

这个触发方式是在 ContainerBase 里的

protected class ContainerBackgroundProcessorMonitor implements Runnable {
        @Override
        public void run() {
            if (getState().isAvailable()) {
                threadStart();
            }
        }
    }

而在这个 threadStart 里

protected void threadStart() {
    if (backgroundProcessorDelay > 0
            && (getState().isAvailable() || LifecycleState.STARTING_PREP.equals(getState()))
            && (backgroundProcessorFuture == null || backgroundProcessorFuture.isDone())) {
        if (backgroundProcessorFuture != null && backgroundProcessorFuture.isDone()) {
            // There was an error executing the scheduled task, get it and log it
            try {
                backgroundProcessorFuture.get();
            } catch (InterruptedException | ExecutionException e) {
                log.error(sm.getString("containerBase.backgroundProcess.error"), e);
            }
        }
        backgroundProcessorFuture = Container.getService(this).getServer().getUtilityExecutor()
                .scheduleWithFixedDelay(new ContainerBackgroundProcessor(),
                        backgroundProcessorDelay, backgroundProcessorDelay,
                        TimeUnit.SECONDS);
    }
}

就调用了线程池的 scheduleWithFixedDelay 方法提交了这个 ContainerBackgroundProcessor
仔细看代码会发现,

public StandardEngine() {

    super();
    pipeline.setBasic(new StandardEngineValve());
    /* Set the jmvRoute using the system property jvmRoute */
    try {
        setJvmRoute(System.getProperty("jvmRoute"));
    } catch(Exception ex) {
        log.warn(sm.getString("standardEngine.jvmRouteFail"));
    }
    // By default, the engine will hold the reloading thread
    backgroundProcessorDelay = 10;

}

这个就不用开启后台热加载,而主要的热加载同学应该是
org.apache.catalina.core.StandardContext#backgroundProcess

public void backgroundProcess() {

        if (!getState().isAvailable()) {
            return;
        }

        Loader loader = getLoader();
        if (loader != null) {
            try {
                // 这里就用了 loader 的 backgroundProcess
                loader.backgroundProcess();
            } catch (Exception e) {
                log.warn(sm.getString(
                        "standardContext.backgroundProcess.loader", loader), e);
            }
        }
        Manager manager = getManager();
        if (manager != null) {
            try {
                manager.backgroundProcess();
            } catch (Exception e) {
                log.warn(sm.getString(
                        "standardContext.backgroundProcess.manager", manager),
                        e);
            }
        }
        WebResourceRoot resources = getResources();
        if (resources != null) {
            try {
                resources.backgroundProcess();
            } catch (Exception e) {
                log.warn(sm.getString(
                        "standardContext.backgroundProcess.resources",
                        resources), e);
            }
        }
        InstanceManager instanceManager = getInstanceManager();
        if (instanceManager != null) {
            try {
                instanceManager.backgroundProcess();
            } catch (Exception e) {
                log.warn(sm.getString(
                        "standardContext.backgroundProcess.instanceManager",
                        resources), e);
            }
        }
        super.backgroundProcess();
    }

loader 的后台处理就是

@Override
    public void backgroundProcess() {
        if (reloadable && modified()) {
            try {
                Thread.currentThread().setContextClassLoader
                    (WebappLoader.class.getClassLoader());
                if (context != null) {
                    context.reload();
                }
            } finally {
                if (context != null && context.getLoader() != null) {
                    Thread.currentThread().setContextClassLoader
                        (context.getLoader().getClassLoader());
                }
            }
        }
    }

然后又会回到 context 的 reload,也就是 StandardContext 的 reload

@Override
public synchronized void reload() {

    // Validate our current component state
    if (!getState().isAvailable()) {
        throw new IllegalStateException
            (sm.getString("standardContext.notStarted", getName()));
    }

    if(log.isInfoEnabled()) {
        log.info(sm.getString("standardContext.reloadingStarted",
                getName()));
    }

    // Stop accepting requests temporarily.
    setPaused(true);

    try {
        stop();
    } catch (LifecycleException e) {
        log.error(
            sm.getString("standardContext.stoppingContext", getName()), e);
    }

    try {
        start();
    } catch (LifecycleException e) {
        log.error(
            sm.getString("standardContext.startingContext", getName()), e);
    }

    setPaused(false);

    if(log.isInfoEnabled()) {
        log.info(sm.getString("standardContext.reloadingCompleted",
                getName()));
    }

}

这样就是线程池结合后台处理,还是有些复杂的。

之前在 Windows 里用 vmware workstation 搭了个黑裙,然后硬盘直通,硬盘跑着倒还好,但是宿主机 Windows 隔一段时间就会重启,就去搜索了下,发现其实 Windows 里的事件查看器就有点像是 Linux 系统里的 dmesg 或者 journal 日志,然后根据重启的时间排查事件查看器里,Windows 日志 –> 系统,

然后可以在右侧 “筛选当前日志”里选择 eventlog 类型的,发现有这样一条事件
进程 C:\Windows\system32\svchost.exe (APP) 为用户 NT AUTHORITY\SYSTEM 开始计算机 APP 的 重新启动,原因如下: 操作系统: 恢复(计划内)
然后在网上搜索的时候发现有一些相关解答
可能的一种原因是系统在应用一些更新失败时,默认设置的策略是失败后重启,我们可以自己选择不重启,因为老是重启在我虚拟机里的黑裙没关机的情况下直接就断电重启了,还是可能会造成一些影响,
可以在资源管理器里右键计算机,选左侧的”高级系统设置” –> 然后在”高级” tab 下面有个”启动和故障恢复” –> “设置”
在弹出弹窗里将 “自动重新启动” 取消勾选

这样到目前还没继续发生过重启

Mapper 顾名思义是作一个映射作用,在 Tomcat 中会根据域名找到 host 组件,再根据 uri 可以找到对应的 context 和 wrapper 组件,但是对于当前这个环境 (Springboot) 会有一点小区别
之前说到
请求会经过 coyote 适配器进行进一步处理,
org.apache.coyote.http11.Http11Processor#service

// Process the request in the adapter
if (getErrorState().isIoAllowed()) {
    try {
        rp.setStage(org.apache.coyote.Constants.STAGE_SERVICE);
        getAdapter().service(request, response);

然后到 coyoteAdapter 的 service
org.apache.catalina.connector.CoyoteAdapter#service

try {
    // Parse and set Catalina and configuration specific
    // request parameters
    postParseSuccess = postParseRequest(req, request, res, response);
    if (postParseSuccess) {
        //check valves if we support async
        request.setAsyncSupported(
                connector.getService().getContainer().getPipeline().isAsyncSupported());
        // Calling the container
        // ----------> 到这就是调用 pipeline 去处理了,我们要关注上面的 postParseRequest
        connector.getService().getContainer().getPipeline().getFirst().invoke(
                request, response);
    }

主要先看到 postParseRequest
在 postParseRequest 的代码里会调用 Mapper 的 map 方法

while (mapRequired) {
    // This will map the the latest version by default
    connector.getService().getMapper().map(serverName, decodedURI,
            version, request.getMappingData());

    // If there is no context at this point, either this is a 404
    // because no ROOT context has been deployed or the URI was invalid
    // so no context could be mapped.
    if (request.getContext() == null) {
        // Allow processing to continue.

而后面的 context 就是在 map 方法里处理塞进去的
往里看就是 org.apache.catalina.mapper.Mapper#internalMap
第一步找的是 host

// Virtual host mapping
        MappedHost[] hosts = this.hosts;
        MappedHost mappedHost = exactFindIgnoreCase(hosts, host);

而在后续代码里继续设置 context

if (contextVersion == null) {
            // Return the latest version
            // The versions array is known to contain at least one element
            contextVersion = contextVersions[versionCount - 1];
        }
        mappingData.context = contextVersion.object;

然后是 wrapper

// Wrapper mapping
if (!contextVersion.isPaused()) {
    internalMapWrapper(contextVersion, uri, mappingData);
}

在这个方法 org.apache.catalina.mapper.Mapper#internalMapWrapper

// Rule 7 -- Default servlet
if (mappingData.wrapper == null && !checkJspWelcomeFiles) {
    if (contextVersion.defaultWrapper != null) {
        mappingData.wrapper = contextVersion.defaultWrapper.object;
        mappingData.requestPath.setChars
            (path.getBuffer(), path.getStart(), path.getLength());
        mappingData.wrapperPath.setChars
            (path.getBuffer(), path.getStart(), path.getLength());
        mappingData.matchType = MappingMatch.DEFAULT;

这里就设置了 wrapper ,因为我们是在 Springboot 中,所以只有默认的 dispatchServlet
上面主要是在请求处理过程中的查找映射过程,一开始的注册是从 MapperListener 开始的
MapperListener 继承了 LifecycleMbeanBase,也就是有了 Lifecycle 状态变化那一套

@Override
public void startInternal() throws LifecycleException {

    setState(LifecycleState.STARTING);

    Engine engine = service.getContainer();
    if (engine == null) {
        return;
    }

    findDefaultHost();

    addListeners(engine);

    Container[] conHosts = engine.findChildren();
    for (Container conHost : conHosts) {
        Host host = (Host) conHost;
        if (!LifecycleState.NEW.equals(host.getState())) {
            // Registering the host will register the context and wrappers
            registerHost(host);
        }
    }
}

在启动过程中就会去把 engine 的子容器 host 找出来进行注册,就是调用 registerHost 方法

private void registerHost(Host host) {

    String[] aliases = host.findAliases();
    mapper.addHost(host.getName(), aliases, host);

    for (Container container : host.findChildren()) {
        if (container.getState().isAvailable()) {
            registerContext((Context) container);
        }
    }

    // Default host may have changed
    findDefaultHost();

    if(log.isDebugEnabled()) {
        log.debug(sm.getString("mapperListener.registerHost",
                host.getName(), domain, service));
    }
}

在这里面会添加 host 组件,注册 context 等,注册context 里还会处理 wrapper 的添加记录

private void registerContext(Context context) {

    String contextPath = context.getPath();
    if ("/".equals(contextPath)) {
        contextPath = "";
    }
    Host host = (Host)context.getParent();

    WebResourceRoot resources = context.getResources();
    String[] welcomeFiles = context.findWelcomeFiles();
    List<WrapperMappingInfo> wrappers = new ArrayList<>();

    for (Container container : context.findChildren()) {
        prepareWrapperMappingInfo(context, (Wrapper) container, wrappers);

        if(log.isDebugEnabled()) {
            log.debug(sm.getString("mapperListener.registerWrapper",
                    container.getName(), contextPath, service));
        }
    }

    mapper.addContextVersion(host.getName(), host, contextPath,
            context.getWebappVersion(), context, welcomeFiles, resources,
            wrappers);

    if(log.isDebugEnabled()) {
        log.debug(sm.getString("mapperListener.registerContext",
                contextPath, service));
    }
}

就是在 org.apache.catalina.mapper.Mapper#addContextVersion 方法中

public void addContextVersion(String hostName, Host host, String path,
        String version, Context context, String[] welcomeResources,
        WebResourceRoot resources, Collection<WrapperMappingInfo> wrappers) {

    hostName = renameWildcardHost(hostName);

    MappedHost mappedHost  = exactFind(hosts, hostName);
    if (mappedHost == null) {
        addHost(hostName, new String[0], host);
        mappedHost = exactFind(hosts, hostName);
        if (mappedHost == null) {
            log.error(sm.getString("mapper.addContext.noHost", hostName));
            return;
        }
    }
    if (mappedHost.isAlias()) {
        log.error(sm.getString("mapper.addContext.hostIsAlias", hostName));
        return;
    }
    int slashCount = slashCount(path);
    synchronized (mappedHost) {
        ContextVersion newContextVersion = new ContextVersion(version,
                path, slashCount, context, resources, welcomeResources);
        if (wrappers != null) {
            addWrappers(newContextVersion, wrappers);
        }

大致介绍了下 Mapper 的逻辑

0%