The
spring-boot-loader
modules lets Spring Boot support executable jar and war files. If you use the Maven plugin or the Gradle plugin, executable jars are automatically generated, and you generally do not need to know the details of how they work.
Spring boot에서 Maven이나 Gradle 플러그인을 사용해 프로젝트를 빌드하면 자동으로 jar파일이 생성되고, java -jar xxx.jar
명령어를 사용하면 스프링부트 프로젝트가 실행된다.
공식문서에는 일반적으로 작동 방식에 대한 세부 정보를 알 필요가 없어도 사용할 수 있도록 편리한 기능을 자체적으로 지원하고 있다.
오늘은 빌드된 스프링부트 프로젝트의 jar 파일 에 대해 알아보자
이전에 다루었던 여러 프로젝트 중 하나를 빌드해 jar 파일을 생성 후 Dcompiler를 사용해 해당 파일의 내부를 살펴보았다.
xxx.jar
ㄴBOOT-INF
ㄴMETA-INF
ㄴorg
일반적인 스프링부트 jar 파일의 내부 구조이다.
BOOT-INF 폴더의 내부 구조이다. BOOT-INF 폴더는 애플리케이션의 자바 코드를 컴파일 한 .class파일과 여러 리소스 파일을 가지고 있다. jar 파일이 실행되면 해당 폴더에 있는 파일들을 읽도록 지정되어 있다.
clesses 폴더는 직접 작성한 자바 코드를 컴파일 한 바이트 코드(.cless)를 가지고 있는 영역이다.
libs 폴더에는 코드 실행에 필요한 외부 라이브러리들이 jar형태로 존재한다. 스프링부트는 중첩된 Jar 파일(Jar파일 안의 Jar파일을 말함)을 로드하는 데 사용되는 클래스인 org.springframework.boot.loader.jar.NestedJarFile
를 사용해 외부 라이브러리에 해당하는 jar 파일을 읽어온다.
- "BOOT-INF/lib/mysql-connector-j-8.3.0.jar" // 제일 먼저 클래스로더에 로드됨
- "BOOT-INF/lib/hibernate-core-6.4.1.Final.jar"
- "BOOT-INF/lib/spring-data-jpa-3.2.2.jar"
- "BOOT-INF/lib/spring-aspects-6.1.3.jar"
- "BOOT-INF/lib/jmustache-1.15.jar"
- "BOOT-INF/lib/spring-security-config-6.2.1.jar"
- "BOOT-INF/lib/spring-security-web-6.2.1.jar"
- "BOOT-INF/lib/spring-webmvc-6.1.3.jar"
- "BOOT-INF/lib/spring-boot-autoconfigure-3.2.2.jar"
- "BOOT-INF/lib/spring-boot-3.2.2.jar"
- "BOOT-INF/lib/spring-security-core-6.2.1.jar"
- "BOOT-INF/lib/spring-context-6.1.3.jar"
- "BOOT-INF/lib/spring-aop-6.1.3.jar"
- "BOOT-INF/lib/spring-web-6.1.3.jar"
- "BOOT-INF/lib/aspectjweaver-1.9.21.jar"
- "BOOT-INF/lib/HikariCP-5.0.1.jar"
classpath.idx파일은 lib에 있는 외부 라이브러리 및 의존성의 경로를 명시하고 있다.
해당 경로의 정렬 순서는 해당 Jar 파일이 클래스로더에 의해 로드될 때의 순서를 나타낸다고 한다.
- "dependencies":
- "BOOT-INF/lib/"
- "spring-boot-loader":
- "org/"
- "snapshot-dependencies":
- "application":
- "BOOT-INF/classes/"
- "BOOT-INF/classpath.idx"
- "BOOT-INF/layers.idx"
- "META-INF/"
layers.idx 파일은 도커에서 jar 이미지를 들 때 쓰는 정보이다. 이 파일은 OCI (Open Container Initiative) 이미지의 레이어를 설명하는 파일이다.
OCI(Open Container Initiative)는 컨테이너 관련 표준을 개발하는 단체로서 OCI 이미지란 컨테이너 개발 관련 표준 스을 준수했다는 의미로 생각하자
해당 패키지는 스프링부트 애플리케이션을 실행하기 위한 로더와 관련된 클래스를 포함하고 있다. 해당 패키지 내에 있는 클래드들이 Jar 파일을 처리하고 애플리케이션을 실행시키는 역할을 한다. 해당 패키지 하위에 애플리케이션에 대한 정보를 읽어와 실행시키는 Launcher 클래스 등이 있다.
META-INF
ㄴservice
ㄴMANIFEST.MF
META-INF는 java 애플리케이션에서 사용되는 메타 데이터와 설정 정보를 저장하는 폴더이다.
jar이든 war이든 META-INF 폴더를 가지고 있으며 애플리케이션의 구현과 실행에 관한 정보를 담고 있는 MANIFEST.MF
를 가지고 있는 폴더인 만큼 매우 중요하다.
Manifest-Version: 1.0
Main-Class: org.springframework.boot.loader.launch.JarLauncher
Start-Class: com.core.testspringsecurity.TestSpringSecurityApplication // 실제로 시작하려는 클래스
Spring-Boot-Version: 3.2.2
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Spring-Boot-Layers-Index: BOOT-INF/layers.idx
Build-Jdk-Spec: 17
Implementation-Title: TestSpringSecurity
Implementation-Version: 0.0.1-SNAPSHOT
MANIFEST.MF
파일은 해당 애플리케이션의 구현과 실행에 필요한 메타 데이터를 가지고 있다.
해당 파일의 내용 중 특히 중요한 정보인 Main-Class
는 애플리케이션이 실행될 때 시작되는 진입점으로 사용되는 클래스를 지정한다.
org.springframework.boot.loader.launch.JarLauncher
클래스를 진입점으로 지정하고 있는데 Jar 파일로 패키징 된 스프링부트 애플리케이션은 내부적으로 필요한 모든 의존성을 포함하고 있어 외부에서 별도의 웹서버 없이 단독으로 실행이 가능하다.
이러한 특성으로 인해 JAR 파일이 실행될 때 특정 클래스가 시작점으로 사용되는데 JarLauncher
클래스가 애플리케이션을 실행하는데 필요한 초기화 작업을 처리하고, 실제로 실행되는 클래스인 Start-Class: com.core.testspringsecurity.TestSpringSecurityApplication
정보를 읽어와 main 메서드를 실행시킨다.
Jar의 실행 과정을 더 자세하게 다루어 보자
public class JarLauncher extends ExecutableArchiveLauncher {
public JarLauncher() throws Exception {
}
protected JarLauncher(Archive archive) throws Exception {
super(archive);
}
protected boolean isIncludedOnClassPath(Archive.Entry entry) {
return isLibraryFileOrClassesDirectory(entry); // BOOT-INF/classes/와 BOOT-INF/lib/를 클래스 경로에 포함시킴
}
protected String getEntryPathPrefix() {
return "BOOT-INF/"; // 경로 접두사 반환 -> Spring Boot의 실행 가능한 JAR 구조를 나타냄
}
static boolean isLibraryFileOrClassesDirectory(Archive.Entry entry) { // 주어진 항목이 clesses 디렉토리인지 lib 디렉토리인지 판단
String name = entry.name();
return entry.isDirectory() ? name.equals("BOOT-INF/classes/") : name.startsWith("BOOT-INF/lib/");
}
public static void main(String[] args) throws Exception {
(new JarLauncher()).launch(args); // 애플리케이션 호출
}
}
먼저 Java -jar 명령어를 실행하게 되면 MANIFEST.MF
파일의 main-class 정보를 내부적으로 읽어 JarLauncher
클래스를 실행시킨다.
JarLauncher
클래스에서 Jar파일의 소스 코드 정보와 의존성 등을 포함하는 경로를 판단하고 등록해 launch 메서드로 애플리케이션을 호출한다.
public abstract class ExecutableArchiveLauncher extends Launcher {
private static final String START_CLASS_ATTRIBUTE = "Start-Class";
protected static final String BOOT_CLASSPATH_INDEX_ATTRIBUTE = "Spring-Boot-Classpath-Index";
protected static final String DEFAULT_CLASSPATH_INDEX_FILE_NAME = "classpath.idx";
private final Archive archive;
private final ClassPathIndexFile classPathIndex;
protected ExecutableArchiveLauncher(Archive archive) throws Exception {
this.archive = archive;
this.classPathIndex = this.getClassPathIndex(this.archive);
}
ClassPathIndexFile getClassPathIndex(Archive archive) throws IOException {
if (!archive.isExploded()) {
return null;
} else {
String location = this.getClassPathIndexFileLocation(archive);
return ClassPathIndexFile.loadIfPossible(archive.getRootDirectory(), location);
}
}
private String getClassPathIndexFileLocation(Archive archive) throws IOException {
Manifest manifest = archive.getManifest();
Attributes attributes = manifest != null ? manifest.getMainAttributes() : null;
String location = attributes != null ? attributes.getValue("Spring-Boot-Classpath-Index") : null;
return location != null ? location : this.getEntryPathPrefix() + "classpath.idx";
}
protected String getMainClass() throws Exception { // mainclass 정보 반환
Manifest manifest = this.archive.getManifest();
String mainClass = manifest != null ? manifest.getMainAttributes().getValue("Start-Class") : null;
if (mainClass == null) {
throw new IllegalStateException("No 'Start-Class' manifest entry specified in " + this);
} else {
return mainClass;
}
/// .... 이하 생략
}
JarLauncher
의 상위 클래스인 ExecutableArchiveLauncher
이다. 아카이브를 직접 생성하거나 하위 객체로부터 아카이브 객체(Spring boot 애플리케이션을 포함하는 Jar파일을 말함)를 받아 정의한다. 주어진 아카이브에서 classpath.idx 파일을 로드하는 로직도 담고 있고, 실제로 사용되는 메인 클래스 정보를 반환하는 로직도 가지고 있다.
protected void launch(String[] args) throws Exception {
if (!this.isExploded()) {
Handlers.register();
}
try {
ClassLoader classLoader = this.createClassLoader((Collection)this.getClassPathUrls()); // 클래스로더 생성
String jarMode = System.getProperty("jarmode");
String mainClassName = this.hasLength(jarMode) ? JAR_MODE_RUNNER_CLASS_NAME : this.getMainClass(); // start-class 정보를 읽어옴
this.launch(classLoader, mainClassName, args);
} catch (UncheckedIOException var5) {
throw var5.getCause();
}
}
protected void launch(ClassLoader classLoader, String mainClassName, String[] args) throws Exception {
Thread.currentThread().setContextClassLoader(classLoader); // 스레드에 클래스로더를 지정 - 해당 클래스 로더의 리소스 및 클래스 사용
Class<?> mainClass = Class.forName(mainClassName, false, classLoader); // 지정한 이름의 클래스 로드 - main class 명을 로드함
Method mainMethod = mainClass.getDeclaredMethod("main", String[].class); // main 메서드 찾아옴
mainMethod.setAccessible(true);
mainMethod.invoke((Object)null, args); // main 메서드 호출
}
최상위 클래스인 Launcher
의 launch메서드이다. 클래스 로더를 생성하고 MANIFEST.MF
파일에서 start-class 정보를 읽어 main class를 실행시킨다.
https://docs.spring.io/spring-boot/docs/current/reference/html/executable-jar.html
👍