Tomcat & Spring Bootstrapping Sequence — 2편 Spring

Brant Hwang
QueryPie, Inc.
Published in
25 min readAug 16, 2015

1편에서는 Tomcat이 대략적으로 어떻게 실행되는지 살펴보았습니다.

톰캣 소스코드를 내려받은 후, IntelliJ나 Eclipse에서 디버그를 해보면, 어떻게 구동되는지 좀더 자세히 알 수 있으므로 한번 쯤 꼭 실행해보시기를 추천합니다.

1편에서 큰 그림으로 살펴봤던 톰캣의 시작과정은 init() -> load() -> start() 순이었습니다.

좀더 자세하게 Class 관점에서 살펴보고 어떤 원리로 톰캣이 스프링을 초기화 하는지도 한번 알아보겠습니다.

  • init() : org.apache.catalina.startup.Bootstrap => main() & init()
  • 외부에서 쉘스크립트(Bat)를 통해서 실행 main 함수 호출합니다.
  • load() : org.apache.catalina.startup.Catalina => load()
  • server.xml을 로딩하고, server.xml의 정보에 기반해서 org.apache.catalina.core.StandardServer 객체를 생성합니다.
  • start() : org.apache.catalina.startup.Catalina => start()
  • org.apache.catalina.core.StandardServer의 start() 호출합니다.

Catalina.java의 start() 부분을 보면 getServer().start() 를 호출 하고 있는데, start() 메소드는 Lifecycle Interface에 선언되어 있고, StandardServer 에서는 Lifecycle Interface를 구현한 LifecycleBase (LifecycleMBeanBase) 를 상속 받고 있습니다.

try {
getServer().start();
} catch (LifecycleException e) {
log.fatal(sm.getString("catalina.serverStartFail"), e);
try {
getServer().destroy();
} catch (LifecycleException e1) {
log.debug("destroy() failed for failed Server ", e1);
}
return;
}
public final class StandardServer extends LifecycleMBeanBase implements Serverpublic abstract class LifecycleMBeanBase extends LifecycleBase implements JmxEnabledpublic abstract class LifecycleBase implements Lifecycle
요렇게 구성되어 있습니다.
결국 getServer().start()를 호출하면 LifecycleBase의 start()가 호출되는데, start() 메소드 내부적으로는 다시 startInternal()을 호출합니다.try {
startInternal();
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
setStateInternal(LifecycleState.FAILED, null, false);
throw new LifecycleException(
sm.getString("lifecycleBase.startFail",toString()), t);
}
protected abstract void startInternal() throws LifecycleException;startInternal()은 추상 메소드로 하위클래스에서 구현 되어 있을것으로 보입니다.그렇다면 실제 구현체가 있을 것으로 보이는 StandardServer의 startInternal()로 가보겠습니다.@Override
protected void startInternal() throws LifecycleException {
fireLifecycleEvent(CONFIGURE_START_EVENT, null);
setState(LifecycleState.STARTING);
globalNamingResources.start();// Start our defined Services
synchronized (servicesLock) {
for (int i = 0; i < services.length; i++) {
services[i].start();
}
}
}

Bootstrap -> Catalina -> StandardServer -> StandardService -> StandardEngine -> StandardHost -> StandardContext ...
결국, Catalina -> getServer().start() 를 호출 한 순간 Service는 Engine을, Engine은 Host를, Host는 Context를 생성하면서 라이프사이클이 시작됩니다. (모두 startInternal()을 구현하고 있습니다 )StandardContext 의 startInternal() 을 살펴보면 스프링이 로드되기 직전 상태에서 어떤일이 벌어지는지 조금더 자세히 알 수 있을것 같은 느낌이 드네요.예상대로 StandardContext의 startInternal()은 굉장히 많은 로직들이 있고 (300라인 정도) 어느 부분에선가 Call ServletContainerInitializers 라는 주석이 보입니다. ( 5152라인 )// Call ServletContainerInitializers
for (Map.Entry<ServletContainerInitializer, Set<Class<?>>> entry :
initializers.entrySet()) {
try {
entry.getKey().onStartup(entry.getValue(),
getServletContext());
} catch (ServletException e) {
log.error(sm.getString("standardContext.sciFail"), e);
ok = false;
break;
}
}
Map에 있는 데이터를 보니 ServletContainerInitializer 타입 이고, ServletContainerInitializer클래스의 onStartup 메소드를 호출 하고 있는 것으로 보이네요.그렇다면 initializers Map에 ServetContainerInitializer 타입의 인스턴스들이 들어있을 것같은데 왠지 이안에 SpringServletContainerInitializer가 있지 않을까? 하는 생각이 듭니다.
Screen Shot 2015-08-16 at 9.36.15 PM
디버깅 해보니 예상했던 대로 org.springframework.web.SpringServletContainerInitializer가 들어있습니다!initializers에 SpringServletContainerInitializer를 넣어주는 부분을 찾아보겠습니다.// Notify our interested LifecycleListeners
fireLifecycleEvent(Lifecycle.CONFIGURE_START_EVENT, null);
StandardContext 5066라인을 보면 fireLifecycleEvent 라는 메소드를 호출하고 있는데, 이때 라이프 사이클(EventType)을 CONFIGURE_START_EVENT로 호출하고 있습니다.참고 : Lifecycle Interface/**
* The LifecycleEvent type for the "component before init" event.
*/
public static final String BEFORE_INIT_EVENT = "before_init";
/**
* The LifecycleEvent type for the "component after init" event.
*/
public static final String AFTER_INIT_EVENT = "after_init";
/**
* The LifecycleEvent type for the "component start" event.
*/
public static final String START_EVENT = "start";
/**
* The LifecycleEvent type for the "component before start" event.
*/
public static final String BEFORE_START_EVENT = "before_start";
/**
* The LifecycleEvent type for the "component after start" event.
*/
public static final String AFTER_START_EVENT = "after_start";
/**
* The LifecycleEvent type for the "component stop" event.
*/
public static final String STOP_EVENT = "stop";
/**
* The LifecycleEvent type for the "component before stop" event.
*/
public static final String BEFORE_STOP_EVENT = "before_stop";
/**
* The LifecycleEvent type for the "component after stop" event.
*/
public static final String AFTER_STOP_EVENT = "after_stop";
/**
* The LifecycleEvent type for the "component after destroy" event.
*/
public static final String AFTER_DESTROY_EVENT = "after_destroy";
/**
* The LifecycleEvent type for the "component before destroy" event.
*/
public static final String BEFORE_DESTROY_EVENT = "before_destroy";
/**
* The LifecycleEvent type for the "periodic" event.
*/
public static final String PERIODIC_EVENT = "periodic";
/**
* The LifecycleEvent type for the "configure_start" event. Used by those
* components that use a separate component to perform configuration and
* need to signal when configuration should be performed - usually after
* {@link #BEFORE_START_EVENT} and before {@link #START_EVENT}.
*/
public static final String CONFIGURE_START_EVENT = "configure_start";
아마도 StandardContext의 라이프 사이클 중 가장 마지막 단계로 보이는 CONFIGURE_START_EVENT 단계에서ServletContainerInitializer 를 찾는 것 같습니다.
위에서 설명했듯이 모든 Standard 클래스류는 Lifecycle 인터페이스를 구현하고 있는데, Lifecycle의 상태별로 구동되는 로직들을 각각 분리시켜서 구현해 놓은것 같습니다.정리하면, ServletContainerInitializer를 찾아서 initializer Map에 Key / Value를 추가 해주는 로직을 찾고 있고 StandardContext의 startInternal() 을 분석하다 보니 다음과 같이 Call Trace가 구성되어 있었습니다.
  • StandardContext 5066 Line : fireLifecycleEvent() 호출
  • LifecycleBase 90 Line : lifecycle.fireLifecycleEvent() 호출
  • LifecycleSupport 117 Line : lifecycleEvent() 호출
  • ContextConfig 293 Line : lifecycleEvent() 호출 => LifecycleListener Interface 구현체
계속 따라 들어가다보니 org.apache.catalina.startup.ContextConfig 클래스의 lifecycleEvent 메소드로 오게 되었습니다.public void lifecycleEvent(LifecycleEvent event) {// Identify the context we are associated with
try {
context = (Context) event.getLifecycle();
} catch (ClassCastException e) {
log.error(sm.getString("contextConfig.cce", event.getLifecycle()), e);
return;
}
// Process the event that has occurred
if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) {
configureStart();
} else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) {
beforeStart();
} else if (event.getType().equals(Lifecycle.AFTER_START_EVENT)) {
// Restore docBase for management tools
if (originalDocBase != null) {
context.setDocBase(originalDocBase);
}
} else if (event.getType().equals(Lifecycle.CONFIGURE_STOP_EVENT)) {
configureStop();
} else if (event.getType().equals(Lifecycle.AFTER_INIT_EVENT)) {
init();
} else if (event.getType().equals(Lifecycle.AFTER_DESTROY_EVENT)) {
destroy();
}
}

로직이 간단하네요. EventType 파라미터로 StandardContext 에서 CONFIGURE_START_EVENT를 넘겼으니 configureStart() 메소드를 호출 합니다.
protected synchronized void configureStart() {
// Called from StandardContext.start()
if (log.isDebugEnabled()) {
log.debug(sm.getString("contextConfig.start"));
}
if (log.isDebugEnabled()) {
log.debug(sm.getString("contextConfig.xmlSettings",
context.getName(),
Boolean.valueOf(context.getXmlValidation()),
Boolean.valueOf(context.getXmlNamespaceAware())));
}
webConfig();if (!context.getIgnoreAnnotations()) {
applicationAnnotationsConfig();
}
if (ok) {
validateSecurityRoles();
}
// Configure an authenticator if we need one
if (ok) {
authenticatorConfig();
}
// Dump the contents of this pipeline if requested
if (log.isDebugEnabled()) {
log.debug("Pipeline Configuration:");
Pipeline pipeline = context.getPipeline();
Valve valves[] = null;
if (pipeline != null) {
valves = pipeline.getValves();
}
if (valves != null) {
for (int i = 0; i < valves.length; i++) {
log.debug(" " + valves[i].getClass().getName());
}
}
log.debug("======================");
}
// Make our application available if no problems were encountered
if (ok) {
context.setConfigured(true);
} else {
log.error(sm.getString("contextConfig.unavailable"));
context.setConfigured(false);
}
}

저기서 눈에 띄는 메소드가 딱 하나 보입니다. 바로 webConfig() !
이후 내부 메소드 콜을 따라가보니 다음과 같았습니다.ContextConfig :: webConfig() -> ContextConfig :: processServletContainerInitializers() -> WebappServiceLoader :: load()WebappServiceLoader 클래스의 load()에 와보니, 드디어 클래스 리소스에서 무언가를 찾는 듯한 로직이 보입니다.public List<T> load(Class<T> serviceType) throws IOException {
String configFile = SERVICES + serviceType.getName();
LinkedHashSet<String> applicationServicesFound = new LinkedHashSet<>();
LinkedHashSet<String> containerServicesFound = new LinkedHashSet<>();
ClassLoader loader = servletContext.getClassLoader();// if the ServletContext has ORDERED_LIBS, then use that to specify the
// set of JARs from WEB-INF/lib that should be used for loading services
@SuppressWarnings("unchecked")
List<String> orderedLibs =
(List<String>) servletContext.getAttribute(ServletContext.ORDERED_LIBS);
if (orderedLibs != null) {
// handle ordered libs directly, ...
for (String lib : orderedLibs) {
URL jarUrl = servletContext.getResource(LIB + lib);
if (jarUrl == null) {
// should not happen, just ignore
continue;
}
String base = jarUrl.toExternalForm();
URL url;
if (base.endsWith("/")) {
url = new URL(base + configFile);
} else {
url = new URL("jar:" + base + "!/" + configFile);
}
try {
parseConfigFile(applicationServicesFound, url);
} catch (FileNotFoundException e) {
// no provider file found, this is OK
}
}
// and the parent ClassLoader for all others
loader = context.getParentClassLoader();
}
Enumeration<URL> resources;
if (loader == null) {
resources = ClassLoader.getSystemResources(configFile);
} else {
resources = loader.getResources(configFile);
}
while (resources.hasMoreElements()) {
parseConfigFile(containerServicesFound, resources.nextElement());
}
// Filter the discovered container SCIs if required
if (containerSciFilterPattern != null) {
Iterator<String> iter = containerServicesFound.iterator();
while (iter.hasNext()) {
if (containerSciFilterPattern.matcher(iter.next()).find()) {
iter.remove();
}
}
}
// Add the application services after the container services to ensure
// that the container services are loaded first
containerServicesFound.addAll(applicationServicesFound);
// load the discovered services
if (containerServicesFound.isEmpty()) {
return Collections.emptyList();
}
return loadServices(serviceType, containerServicesFound);
}

로직을 간단하게 정리하면 다음과 같습니다.
  • 클래스 로더에 로딩된 Class(JAR) 파일들 중에서 META-INF/services 위치에 javax.servlet.ServletContainerInitializer 라는 이름의 리소스 파일을 찾습니다.
  • 리소스 파일안에 들어있는 내용을 BufferedReader로 읽어들인 다음, 그 값(String)을 LinkedHashSet (servicesFound 변수) 에 추가합니다.
  • 리소스 파일의 내용에는 ServetInitializer Package + Class명이 들어있습니다. ( 예 :org.springframework.web.SpringServletContainerInitializer
  • 이후 loadServices 메소드를 호출해서 클래스를 로딩하고, List 에 담습니다.
List<T> loadServices(Class<T> serviceType, LinkedHashSet<String> servicesFound) throws IOException {
ClassLoader loader = servletContext.getClassLoader();
List<T> services = new ArrayList<>(servicesFound.size());
for (String serviceClass : servicesFound) {
try {
Class<?> clazz = Class.forName(serviceClass, true, loader);
services.add(serviceType.cast(clazz.newInstance()));
} catch (ClassNotFoundException | InstantiationException |
IllegalAccessException | ClassCastException e) {
throw new IOException(e);
}
}
return Collections.unmodifiableList(services);
}
멀리도 왔네요. StandardContext에서 부터 시작해서 ContextConfig, WebppServiceLoader를 거쳐 드디어 ClassLoader상에 있는 ServletContainerInitializer 들을 찾았습니다.
Screen Shot 2015-08-16 at 10.07.07 PM
다시한번더 정리해보겠습니다.
  • Bootstrap 단계에서 클래스 로더를 이용해서 ClassPath, JVM Library, WebApplication Class (JAR) 등을 클래스 로더로 로딩했습니다.
  • 이후 load() -> start() 단계를 거치면서 StandardContext가 시작되고, LifeCycle에 의해 Configure 모드로 들어갑니다.
  • Configure 모드에서 ContextConfig와 WebappServiceLoader가 개입해서 META-INF/services/javax.servlet.ServletContainerInitializer 파일을 클래스로더 리소스에서 찾게 됩니다.
  • 발견된 javax.servlet.ServletContainerInitializer 에는 ServletContainerInitializer 클래스 정보가 들어있고, 그정보를 기반으로 ServletContainerInitializer 구현체들을 확보했습니다.
  • 이시점에 spring-web-{RELEASE-VERSION}.jar에 있는 META-INF/services/javax.servlet.ServletContainerInitializer가 발견되고, 구현체인 org.springframework.web.SpringServletContainerInitializer가 ServletContainerInitializer 후보군으로 등록되게 됩니다.
Screen Shot 2015-08-16 at 10.17.00 PM
다시 StandardContext로 돌아와서 살펴 봤던 로직을 보면 initializers 에 3개의 ServletContainerInitializer가 들어가있고, 각 ServletContainerInitializer들의 onStartup이 호출되고 있습니다.for (Map.Entry<ServletContainerInitializer, Set<Class<?>>> entry :
initializers.entrySet()) {
try {
entry.getKey().onStartup(entry.getValue(),
getServletContext());
} catch (ServletException e) {
log.error(sm.getString("standardContext.sciFail"), e);
ok = false;
break;
}
}
Screen Shot 2015-08-16 at 10.19.48 PM
이렇게 톰캣의 시작부터, 톰캣이 어떻게 Spring Framework을 찾아내고 초기화 하는지까지 살펴보았습니다.3편에서는 Tomcat Connector -> Valve -> Servlet Dispatching -> ( Spring Framework 내에서의 처리 ) 과정들을 살펴보겠습니다.

--

--