疑問点
Tomcatにwarをデプロイする方式では、Tomcatのログはconf/logging.properties で設定し、アプリケーションからはTomcatのログ設定を変更することができない。
参考 tomcat ロギング リファレンス
しかし組み込みTomcatを利用する方式では、アプリケーションからTomcatに関するログの設定を変更することができ、アプリケーションのログと同じようにTomcatのログが出力される。
例えば以下のログでは、Spring Frameworkに混じってTomcatのログも出力されている。
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.0.2.RELEASE)
2018-05-31 20:14:14.158 INFO 7188 --- [ main] com.example.demo.DemoApplication : Starting DemoApplication on kimura-pc with PID 7188 (C:\Users\pbreh_000\Desktop\study\demo\target\classes started by pbreh_000 in C:\Users\pbreh_000\Desktop\study\demo)
2018-05-31 20:14:14.158 INFO 7188 --- [ main] com.example.demo.DemoApplication : No active profile set, falling back to default profiles: default
2018-05-31 20:14:14.205 INFO 7188 --- [ main] ConfigServletWebServerApplicationContext : Refreshing org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@6b419da: startup date [Thu May 31 20:14:14 JST 2018]; root of context hierarchy
2018-05-31 20:14:15.360 INFO 7188 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2018-05-31 20:14:15.375 INFO 7188 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2018-05-31 20:14:15.391 INFO 7188 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet Engine: Apache Tomcat/8.5.31
warをデプロイするときと、組み込みTomcatで、どういう違いがあるのか?
結論
- warをデプロイするときは、アプリケーションからTomcatのログを制御できない
- 組み込みTomcatのときは、アプリケーションからTomcatのログを制御できる
理由
以下の話を読む前の前提知識。ライブラリで実装しているログをアプリケーションから設定したいときは、アダプタ用のjar(log4j-over-slf4j や jul-to-slf4j )に、実行時に差し替える必要がある。
参考 Java ログ出力パターン
Tomcatの内部ロガー
リファレンスによると
- Tomcatは JULI という独自ロガーの実装を持っている
- JULI は Apache Commons Logging をforkしてパッケージ名を変えている
- JULI は 実装クラスにjava.util.loggingを使うようにハードコードされている
- なぜ上記のようになっているかというと、java.util.logging はデフォルトだと VMごとにしか設定が切り替えられないため、独自のLogManager(ClassLoaderLogManager)によってクラスローダごとに設定を読み込めるようにしたいから
Tomcatにwarをデプロイするとき
catalina.shを利用したときの起動クラスは org.apache.catalina.startup.Bootstrap
Tomcatのクラスローダはwarごとにライブラリを管理できるように階層化されている。
参考 Class Loader HOW-TO
Bootstrap
|
System
|
Common
/ \
Webapp1 Webapp2 ...
親のクラスローダ(複数の場合あり)が要求されたクラスやリソースを見つけられなかった場合だけ、自分のリポジトリを見にいくことになる。
例えば、Tomcatのログをアプリケーションからコントロールするためにアダプタを設定しようとしてwarに jul-to-slf4j.jar を含めた場合でも、Tomcatのクラスを読み込んでいるクラスローダ(Common)から、WEBアプリケーションのクラスは参照できない。そのため、ログ実装をアプリケーションから差し替えることができない。
組み込みTomcatとして使うとき
組み込みTomcatの起動クラスは org.apache.catalina.startup.Tomcat
org.apache.catalina.startup.Bootstrap で起動したときとは違い、Catalina系のクラスとWEBアプリケーションのクラスが同一クラスローダに読み込まれる。
そのため jul-to-slf4j.jar をクラスパスに通せば、ロガーの差し替えができる。
検証する
pom.xmlを用意する。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<groupId>example.com</groupId>
<artifactId>embedded-tomcat-logging</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>8.5.23</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jul-to-slf4j</artifactId>
<version>1.7.25</version>
</dependency>
</dependencies>
<properties>
<java.version>1.8</java.version>
<maven.compiler.target>${java.version}</maven.compiler.target>
<maven.compiler.source>${java.version}</maven.compiler.source>
</properties>
</project>
logback.xmlを用意する。
<configuration>
<contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator">
<resetJUL>true</resetJUL>
</contextListener>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%c %d [%thread] %-5level - %msg%n</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
組み込みTomcatを起動する。
public class Main {
public static void main(String[] args) throws LifecycleException, InterruptedException {
SLF4JBridgeHandler.removeHandlersForRootLogger();
SLF4JBridgeHandler.install();
Tomcat tomcat = new Tomcat();
tomcat.setPort(8080);
Context ctx = tomcat.addContext("/sample", new File(".").getAbsolutePath());
Logger logger = LoggerFactory.getLogger(Main.class);
logger.info("start...");
Tomcat.addServlet(ctx, "hello", new HttpServlet() {
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
Writer w = resp.getWriter();
w.write("Hello, World!");
w.flush();
}
});
ctx.addServletMappingDecoded("/*", "hello");
tomcat.start();
tomcat.getServer().await();
}
}
コンソール出力を見ると、Logbackに実装が切り替えられている。
org.apache.coyote.http11.Http11NioProtocol 2018-05-31 20:37:47,682 [main] INFO ---- Initializing ProtocolHandler ["http-nio-8080"]
org.apache.tomcat.util.net.NioSelectorPool 2018-05-31 20:37:47,745 [main] INFO ---- Using a shared selector for servlet write/read
org.apache.catalina.core.StandardService 2018-05-31 20:37:47,747 [main] INFO ---- Starting service [Tomcat]
org.apache.catalina.core.StandardEngine 2018-05-31 20:37:47,748 [main] INFO ---- Starting Servlet Engine: Apache Tomcat/8.5.23
org.apache.coyote.http11.Http11NioProtocol 2018-05-31 20:37:47,889 [main] INFO ---- Starting ProtocolHandler ["http-nio-8080"]
実際にクラスローダを確認してみると、Tomcat関連のクラスとWEBアプリケーションのクラスが同一のクラスローダに読み込まれている。
Spring Bootの場合
動作確認バージョンは以下。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
Spring Bootはもう少し複雑なことをしていて、Java標準のクラスローダで spring boot loader classes を読み込み、そこから依存する各jarをロードしている。
example.jar
|
+-META-INF
| +-MANIFEST.MF
+-org
| +-springframework
| +-boot
| +-loader
| +-<spring boot loader classes>
+-BOOT-INF
+-classes
| +-mycompany
| +-project
| +-YourClasses.class
+-lib
+-dependency1.jar
+-dependency2.jar
参考 E.1.1 The Executable Jar File Structure
まあどっちにしろ、Tomcat関連のクラスからWEBアプリケーションのクラスが参照できているので同じことかなと。
SpringBootの組み込みTomcatの注意点
「Tomcat関連のログはアプリケーションのログ設定で一元管理できる」といきたいところだが、Tomcatのアクセスログは上記のログでは出力されない。Tomcatのアクセスログは独自のValveで実装されているため、ログ部分をアダプタで差し替えできないから。
Access logging is a related but different feature, which is implemented as a Valve. It uses self-contained logic to write its log files. 参考 Logging in Tomcat
ただし、Spring Boot だとプロパティで設定できる。
参考 Appendix A. Common application properties
server.tomcat.accesslog.buffered=true # Whether to buffer output such that it is flushed only periodically.
server.tomcat.accesslog.directory=logs # Directory in which log files are created. Can be absolute or relative to the Tomcat base dir.
server.tomcat.accesslog.enabled=false # Enable access log.
server.tomcat.accesslog.file-date-format=.yyyy-MM-dd # Date format to place in the log file name.
server.tomcat.accesslog.pattern=common # Format pattern for access logs.
server.tomcat.accesslog.prefix=access_log # Log file name prefix.
server.tomcat.accesslog.rename-on-rotate=false # Whether to defer inclusion of the date stamp in the file name until rotate time.
server.tomcat.accesslog.request-attributes-enabled=false # Set request attributes for the IP address, Hostname, protocol, and port used for the request.
server.tomcat.accesslog.rotate=true # Whether to enable access log rotation.
server.tomcat.accesslog.suffix=.log # Log file name suffix.
...