问题发生
近期,在线上有出现问题;截图如下:
仔细一看,是 /tmp/tomcat......
;大家的第一反应是,呃,是不是 basedir
没有设置,跑到临时目录去了;所以,请运维同学设置上 basedir 的配置。之后过了N天,依然出现了这样的问题;
问题分析
注意,我们看看错误截图中的内容,是 /tmp/tomcat-docbase.xxxx ;注意,是tomcat-docbase ,而不是 tomcat ;tomcat-docbase 是怎么出来的呢?是否和 tomcat.basedir 有关系呢?需要继续排查;
我们也检查了代码的位置,发现代码中,是将文件输出到了一个目录下;而这个目录的代码:
String outFilePath = request.getServletContext().getRealPath("/") + "upload/";
那么,是否:request.getServletContext().getRealPath("/")
这部分和basedir没有关系的呢?
tomcat的临时目录设置
大家都知道在springboot 1.x 中的临时目录设置方法;配置文件中加上一段:
server.tomcat.basedir=xxxxx
也都知道,如果不设置这个的话,会在 java.io.tmpdir
环境变量目录下,出现一个tomcat.xxxx.port(服务端口号) 的目录作为临时目录; 同时我们也都清楚,在linux 下 java.io.tmpdir
,如果没有特殊设置,默认是指向到: /tmp
目录下,而这个目录是在linux 下是有定时任务 定期清理的;也就是说,如果我们不设置basedir ,那么真的是会出现以上的类似的错误,就是提醒:xxx 目录不存在之类的;
而没有设置 basedir ,那么在 临时目录下出现的 tomcat 目录应该是:
tomat-docbase 和 basedir有关系吗?
看这部分,自然翻代码;springboot 是内嵌tomcat的;找到TomcatEmbeddedServletContainerFactory
这个类,这个是tomcat 内嵌的时候,相关的启动类;找到getEmbeddedServletContainer
的方法,如下;
这部分方法中,可以看到; 1)setBaseDir ,这个上下翻一下逻辑,是我们设置的 basedir ,那如果我们不设置 basedir,会自动创建一个 tomcat 开头的临时目录:createTempDir(String),这个方法,入参是:tomcat ;而createTempDir 如下所示:
在这儿,我们可以确认,所谓的 tomcat-docbase.xxx ,不是我们的 basedir 没设置引起的;因为即使我们不设置,那么目录也不会是tomcat-docbase;
继续
接着,还是TomcatEmbeddedServletContainerFactory
,我们继续;看到了 prepareContext 的方法;这个方法,是初始化Context的,是否和我们 要找的 tomcat-docbase有关系;代码如下:
确实是在这里了;翻了一下代码逻辑:getValidDocumentRoot(),在内嵌tomcat容器启动的时候,返回是空的;所以这部分的目录就变成临时目录下 tomcat-docbase.xxx 的目录;
而 request.getServletContext().getRealpath("/"),获取的就是这个目录;
自此,我们找到了源头;
如何设置docbase
查看:getValidDocumentRoot方法:
/**
* Returns the absolute document root when it points to a valid directory, logging a
* warning and returning {@code null} otherwise.
* @return the valid document root
*/
protected final File getValidDocumentRoot() {
File file = getDocumentRoot();
// If document root not explicitly set see if we are running from a war archive
file = file != null ? file : getWarFileDocumentRoot();
// If not a war archive maybe it is an exploded war
file = file != null ? file : getExplodedWarFileDocumentRoot();
// Or maybe there is a document root in a well-known location
file = file != null ? file : getCommonDocumentRoot();
if (file == null && this.logger.isDebugEnabled()) {
this.logger
.debug("None of the document roots " + Arrays.asList(COMMON_DOC_ROOTS)
+ " point to a directory and will be ignored.");
}
else if (this.logger.isDebugEnabled()) {
this.logger.debug("Document root: " + file);
}
return file;
}
实际上,在整个使用中,我们虽然找到了 setDocumentRoot() 方法,但没有找到调用方;因此默认的DocumentRoot也就无法设置,当然我们可以自己重写这部分代码来实现;
docbase 的测试验证
- 默认,仅设置base.dir
说明,baseDir 的设置,不影响docBase
- 在部署目录中,增加了一个 public 文件夹 部署目录如下:
输出如下:
看代码中的 getCommonDocumentRoot() 实现可以知道原因;
- 设置 java.io.tmpdir 变量 输出如下:
有个前提条件,上面第2个的 public 目录要删除掉;否则还会在那个public 目录下;
问题定位
由于我们没有设置 java.io.tmpdir 环境变量,而又使用了request.getServletContext().getRealPath("/")
来获取目录;导致文件目录被定位到:/tmp/tomcat-docbase.xxx 目录下;
根据linux的规则,/tmp 目录下文件,会定期清理,导致一段时间之后,写入:request.getServletContext().getRealPath("/")
目录下,出现异常:文件或者目录找不到;
之前一直认为的 basedir 参数设置,并不会影响到 request.getServletContext().getRealPath("/")
的取值,因此原来的解决方法并没有效果;
问题的解决方法
-
最简单的方式 启动脚本加上对 java.io.tmpdir 的参数设置; 同样的,我们也知道 java.io.tmpdir ,在不设置 basedir 下的时候,也会调整basedir的目录;因此,可以通过java.io.tmpdir进行统一设置; 其实,在tomcat 自身的启动脚本中,也有类似的控制,如下:(tomcat bin\catalina.sh文件)
所以,可以在启动脚本中,获取到应用的当前目录,并将 java.io.tmpdir 设置到当前目录下的某个文件,例如:tmp;
-
在部署目录下增加 public 文件夹 这种方式下,需要重新修改提供的部署包,暂时不推荐
-
修改代码 由于是下载文件中,需要临时生成一个文件,而临时文件用了
request.getServletContext().getRealPath("/")
来获取目录,导致了问题; 这部分代码修改掉,可以直接使用File.createTempFile()
,来创建临时文件;这样就可以完全避免上面的问题;当然这个只是解决我们目前的这个问题;
tomcat的 docBase 、workDir 关系
如果我们将应用打包为 war 部署到tomcat 应用下的时候,最简单的方式是,将war包复制到 webapp 目录下,tomcat自行加载就可以了; 但其实,还有一种方式,就是通过 tomcat的 server.xml 文件,而这个server.xml文件中,是可以配置应用目录及上下文;
<Context docBase="D:\samples\WebRoot" path="/sample" reloadable="true" />
<!-- Context 表示运行在虚拟主机上的一个web应用程序,通常为WAR文件 -->
<!-- docBase 应用程序的路径或者是WAR文件存放的路径 -->
<!-- path 表示此web应用程序的url的前缀,就我们所说的请求上下文,这样请求的url为http://localhost:8080/**** -->
<!-- reloadable 这个属性非常重要,如果为true,则tomcat会自动检测应用程序的/WEB-INF/lib 和/WEB-INF/classes目录的变化,自动装载应用程序,我们可以在不重起tomcat的情况下改变应用程序 -->
docBase 其实是用于设置我们的应用部署的目录;而内置的tomcat 容器,启动之后,也会自行创建一个这个目录(默认就在 java.io.tmpdir 指定的 tomcat-docbase.xxx 下);
workDir,是指tomcat 在运行中,会生成一些临时文件(比如 jsp 编译之后的 java / class 文件);而这些是放在了 workDir 下;如果是应用部署在tomcat 服务中,则在tomcat 目录下,有个:/work/Catalina/localhost 目录是用于保存这个;而在springboot 的内嵌tomcat 中,则是通过 basedir 来指定目录;