아래와같은 폴더 구조를 가지고 있다
.
├── BOOT-INF
│ ├── classes
│ │ ├── application-dev.yml
│ │ ├── application-local.yml
│ │ ├── application-test.yml
│ │ ├── application.yml
│ │ ├── com
│ │ │ └── example
│ │ ├── data.sql
│ │ ├── log4j2-dev.xml
│ │ └── log4j2-local.xml
│ ├── classpath.idx
│ ├── layers.idx
│ └── lib
│ ├── HikariCP-5.0.1.jar
│ ├── angus-activation-2.0.1.jar
│ ├── spring-data-commons-3.1.3.jar
│ ├── spring-data-jpa-3.1.3.jar
│ ├── spring-data-keyvalue-3.1.3.jar
...
│ ├── spring-web-6.0.11.jar
│ ├── spring-webmvc-6.0.11.jar
│ └── webjars-locator-core-0.52.jar
├── META-INF
│ └── MANIFEST.MF
├── jartest-0.0.1-SNAPSHOT-plain.jar
├── jartest-0.0.1-SNAPSHOT.jar
└── org
└── springframework
└── boot
└── loader
BOOT-INF
libs
BOOT-INF/classes
META-INF
META-INF/MANIFEST.MF
를 읽는다
Manifest-Version: 1.0
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.example.jartest.JarTestApplication
Spring-Boot-Version: 3.1.3
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: jartest
Implementation-Version: 0.0.1-SNAPSHOT
JarLauncher
실행한다.
메인 메서드 실행
public static void main(String[] args) throws Exception {
new JarLauncher().launch(args);
}
JarLauncher.launch()
protected void launch(String[] args) throws Exception {
if (!isExploded()) {
Handlers.register();
}
try {
ClassLoader classLoader = createClassLoader(getClassPathUrls()); //여기서 BOOT-INF주소 가져옴
String jarMode = System.getProperty("jarmode");
String mainClassName = hasLength(jarMode) ? JAR_MODE_RUNNER_CLASS_NAME : getMainClass();
launch(classLoader, mainClassName, args);
}
catch (UncheckedIOException ex) {
throw ex.getCause();
}
}
LaunchedClassLoader
로 jar들 불러온다(extends JarUrlClassLoader
)
/BOOT-INF/lib
를 기반으로 ClassLoader 생성하고 Jar들 불러온다Launcher
의 launch()
사용
/**
* Launch the application. This method is the initial entry point that should be
* called by a subclass {@code public static void main(String[] args)} method.
* @param args the incoming arguments
* @throws Exception if the application fails to launch
*/
protected void launch(String[] args) throws Exception {
if (!isExploded()) {
Handlers.register();
}
try {
ClassLoader classLoader = createClassLoader(getClassPathUrls());
String jarMode = System.getProperty("jarmode");
String mainClassName = hasLength(jarMode) ? JAR_MODE_RUNNER_CLASS_NAME : getMainClass();
launch(classLoader, mainClassName, args);
}
catch (UncheckedIOException ex) {
throw ex.getCause();
}
}
getMainClass
내부에서 호출하는것을 보자PropertiesLauncher
에서 override 된 getMainClass를 호출@Override
protected String getMainClass() throws Exception {
String mainClass = getProperty(MAIN, "Start-Class");
if (mainClass == null) {
throw new IllegalStateException("No '%s' or 'Start-Class' specified".formatted(MAIN));
}
return mainClass;
}
Start-Class
어디서 많이 보지 않았었나?MANIFEST.MF
에 우리가 만들어놓은 스프링 애플리케이션의 시작점이 적혀있다!Launcher
로 필요한 jar들 모두 읽어들이고나서 시작함을 확인myapp.jar
+-------------------+-------------------------+
| /BOOT-INF/classes | /BOOT-INF/lib/mylib.jar |
|+-----------------+||+-----------+----------+|
|| A.class ||| B.class | C.class ||
|+-----------------+||+-----------+----------+|
+-------------------+-------------------------+
^ ^ ^
0063 3452 3980
위의 예시는 myapp.jar
내부의 /BOOT-INF/classes
가 0063 포지션에 있음을 알려준다. B.class
는 myapp.jar
의 3452 포지션. 그리고 C.class
는 3980에 있다.JarFile
의 구현체인 NestedJarFile
로 load해온다.JarEntry
의 위치는 바깥 jar의 물리 주소 offset으로 매핑된다.Armed with this information, we can load specific nested entries by seeking to the appropriate part of the outer jar. We do not need to unpack the archive, and we do not need to read all entry data into memory.
- jar의 전체 data를 가져올 필요도 없고, jar를 압축해제 할 필요도 없음. 그냥 필요한 부분 jar 자체에서 뽑아옴
공식문서
참고자료
Understanding the JAR Manifest File | Baeldung
공식 github