bootJar(spring-boot)에서 package의 class 목록 가져오기
ruinnel
springspring-bootjavakotlinreflection
2890 Words CHANGE ME READ TIME 13 Minutes, 8 Seconds
2020-04-25 13:49 +0000
들어가기전에…
spring boot로 만든 어플리케이션에서 package에 속한 class 목록을 전부 가져오고 싶었습니다만… 실패하는 과정에서 알게된 bootJar과 일반 jar의 다른점에 대해서 정리한 글입니다.
결론은 이런 경우에는 좀 더 spring 답게 Annotation으로 해결하라
입니다.
테스트용 Spring boot application 만들기
- https://start.spring.io/ 에서 아래 설정으로 gradle + kotlin을 사용하도록 생성 합니다.
- IntelliJ에서 Import 합니다.
bootJar에서 특정 package 하위의 class 목록 가져오기.
DemoApplication.kt
에 아래와 같이 package에 속한 class 목록을 가져오는 코드를 추가 합니다.
보통 이런 경우 ClassLoader의 getResource를 이용합니다.
가져온 package(resource..?)를 Directory로 보고 File 객체를 만든 후 listFiles()
(shell에서 치면 ls
)를 실행해서 파일 목록을 가져 온 후, Class.forName()
혹은 ClassLoader.loadClass()
메소드를 이용해 Class 객체를 가져 옵니다.
java get class list in package
같은 키워드로 검색하면 java 샘플코드도 많이 나옵니다.
- https://dzone.com/articles/get-all-classes-within-package
- 위 페이지의 java 소스를 kotlin으로 변환 한 코드를 추가했습니다.
fun main(args: Array<String>) {
val cls = getClasses("net.ruinnel.demo")
println("classes - [${cls.joinToString(", ") { it.name }}]")
runApplication<DemoApplication>(*args)
}
fun getClasses(packageName: String): List<Class<*>> {
val classLoader = Thread.currentThread().contextClassLoader
val path = packageName.replace('.', '/')
val resources = classLoader.getResources(path)
return resources.toList()
.map {
println("url - $it")
File(it.file)
}
.mapNotNull { findClasses(it, packageName, classLoader) }
.flatten()
}
fun findClasses(directory: File, packageName: String, loader: ClassLoader): List<Class<*>>? {
if (!directory.exists()) {
return null
}
val files = directory.listFiles()
return files.mapNotNull { file ->
when {
file.isDirectory -> {
assert(!file.name.contains("."))
findClasses(file, packageName + "." + file.name, loader)
}
file.name.endsWith(".class") -> {
val className = packageName + '.'.toString() + file.name.substring(0, file.name.length - 6)
listOf(loader.loadClass(className))
}
else -> null
}
}
.flatten()
}
- IntelliJ에서
main()
함수를 실행해 봅니다.
url - file:/Users/ruinnel/develop/test/demo/build/classes/kotlin/main/net/ruinnel/demo
classes - [net.ruinnel.demo.DemoApplication, net.ruinnel.demo.DemoApplicationKt$main$1, net.ruinnel.demo.DemoApplicationKt]
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.2.6.RELEASE)
2020-04-26 16:09:37.860 INFO 36075 --- [ main] net.ruinnel.demo.DemoApplicationKt : Starting DemoApplicationKt on MacBook-Pro.local with PID 36075 (/Users/ruinnel/develop/test/demo/build/classes/kotlin/main started by ruinnel in /Users/ruinnel/develop/test/demo)
2020-04-26 16:09:37.862 INFO 36075 --- [ main] net.ruinnel.demo.DemoApplicationKt : No active profile set, falling back to default profiles: default
2020-04-26 16:09:39.484 INFO 36075 --- [ main] net.ruinnel.demo.DemoApplicationKt : Started DemoApplicationKt in 2.023 seconds (JVM running for 2.709)
Process finished with exit code 0
- project root에서
./gradlew bootJar
를 실행해 bootJar을 생성 후 실행해봅니다.
MacBook-Pro:libs ruinnel$ java -jar demo-0.0.1-SNAPSHOT.jar
url - jar:file:/Users/ruinnel/develop/test/demo/build/libs/demo-0.0.1-SNAPSHOT.jar!/BOOT-INF/classes!/net/ruinnel/demo
classes - []
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.2.6.RELEASE)
2020-04-26 16:19:05.980 INFO 37964 --- [ main] net.ruinnel.demo.DemoApplicationKt : Starting DemoApplicationKt on MacBook-Pro.local with PID 37964 (/Users/ruinnel/develop/test/demo/build/libs/demo-0.0.1-SNAPSHOT.jar started by ruinnel in /Users/ruinnel/develop/test/demo/build/libs)
2020-04-26 16:19:05.984 INFO 37964 --- [ main] net.ruinnel.demo.DemoApplicationKt : No active profile set, falling back to default profiles: default
2020-04-26 16:19:07.506 INFO 37964 --- [ main] net.ruinnel.demo.DemoApplicationKt : Started DemoApplicationKt in 2.681 seconds (JVM running for 3.459)
MacBook-Pro:libs ruinnel$
음? bootJar에서 실행한 결과에서는 Class 목록을 가져오지 못합니다.!!?
분명히 같은 소스인데 IntelliJ에서 실행한 것과 bootJar로 만들어서 실행한 결과가 다릅니다.
# IntelliJ 에서 실행한 결과
url - file:/Users/ruinnel/develop/test/demo/build/classes/kotlin/main/net/ruinnel/demo
classes - [net.ruinnel.demo.DemoApplication, net.ruinnel.demo.DemoApplicationKt$main$1, net.ruinnel.demo.DemoApplicationKt]
# bootJar을 실행한 결과
url - jar:file:/Users/ruinnel/develop/test/demo/build/libs/demo-0.0.1-SNAPSHOT.jar!/BOOT-INF/classes!/net/ruinnel/demo
classes - []
url
은 package를 resource로 가져 온 값을 찍은 것인데 이 값도 서로 다릅니다.
IntelliJ에서 실행한 것은 file:
, bootJar은 jar:
로 시작합니다.
아무리 봐도 File 클래스의 생성자가 jar:file:/...!/
이런 path로 제대로 된 원하는 위치에 접근 가능한 객체를 생성해 줄거 같지 않습니다.
그러면 bootJar에서도 Class를 가져오려면 jar
파일 내의 Class를 가져오는 방법으로 시도해 보면 될거 같습니다.
Get list of classes in package
같은 키워드로 검색해보면 jar 파일에서 Class를 가져오는 법에 대한 예제도 많습니다.
ex) https://www.rgagnon.com/javadetails/java-0513.html
그런데… 시도하지 마세요 안됩니다.
bootJar을 실행했을때 로그를 다시 살펴보면…
url - jar:file:/Users/ruinnel/develop/test/demo/build/libs/demo-0.0.1-SNAPSHOT.jar!/BOOT-INF/classes!/
url의 뒷쪽을 보면 BOOT-INF/
라는게 보입니다.
일반적인 jar 파일의 경우 zip으로 확장자를 변환 후 압축을 풀어보면 아래와 같은 구조를 가집니다.
.
├── META-INF
│ └── MANIFEST.MF
└── net
└── ruinnel
└── demo
└── package
├── YourClass1.class
└── YourClass2.class
위에서 실행했던 bootJar을 동일하게 압축을 풀어봅시다.
.
├── BOOT-INF
│ ├── classes
│ │ ├── META-INF
│ │ │ └── demo.kotlin_module
│ │ ├── application.properties
│ │ └── net
│ │ └── ruinnel
│ │ └── demo
│ │ ├── DemoApplication.class
│ │ ├── DemoApplicationKt$main$1.class
│ │ └── DemoApplicationKt.class
│ └── lib
│ ├── spring-core-5.2.5.RELEASE.jar
│ └── ....
├── META-INF
│ └── MANIFEST.MF
└── org
└── springframework
└── boot
└── loader
├── ExecutableArchiveLauncher.class
├── JarLauncher.class
├── LaunchedURLClassLoader$UseFastConnectionExceptionsEnumeration.class
├── LaunchedURLClassLoader.class
├── Launcher.class
├── MainMethodRunner.class
├── PropertiesLauncher$1.class
├── PropertiesLauncher$ArchiveEntryFilter.class
├── PropertiesLauncher$PrefixMatchingArchiveFilter.class
├── PropertiesLauncher.class
├── WarLauncher.class
└── .....
내가 짠 코드들이 컴파일된 class 파일들이 BOOT-INF
아래에 들어가 있고 거기다 META-INF/MANIFEST.MF
를 열어 Main-Class
를 확인해보면… org.springframework.boot.loader.JarLauncher
라는 값이 지정되어 있습니다.
MacBook-Pro:demo ruinnel$ cat META-INF/MANIFEST.MF
Manifest-Version: 1.0
Start-Class: net.ruinnel.demo.DemoApplicationKt
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Version: 2.2.6.RELEASE
Main-Class: org.springframework.boot.loader.JarLauncher
사실 당연합니다. bootJar은 웹서버도 embedded 되어있으니 그것도 실행하고 내가 만든건 그 위에 돌아가야 될거니까 내가 만든 Class가 시작점이 될 수는 없으니까요.
그건 그렇다고 치고… java에서 jar 파일 spec에는 BOOT-INF
같은거 눈씻고 찾아봐도 안보입니다… 그러니 일반적인 jar 파일에서 class를 찾는 방법으로는 BOOT-INF
아래에 있는 class들은 접근이 불가능 합니다.
DemoApplication.kt
의 getClasses
함수에 print 문을 하나 추가합니다.
fun getClasses(packageName: String): List<Class<*>> {
val classLoader = Thread.currentThread().contextClassLoader
println("class-loader - $classLoader")
// ... 중략..
}
당연히 IntelliJ에서 실행결과와 bootJar을 실행한 결과는 서로 다르게 나옵니다. 그리고 bootJar의 경우
org.springframework.boot.loader.LaunchedURLClassLoader
라는 값이 나옵니다.
그러니까.. URLClassLoader를 상속해 'BOOT-INF'안에 있는 class들을 사용 할 수 있게 별도의 ClassLoader를 사용하고 있는 것
입니다.
그리고 기본적으로 ClassLoader 접근가능한 메소드들을 보면 package 아래의 class 목록을 가져오는 메소드는 보이지 않습니다.
LaunchedURLClassLoader
의 메소드 목록을 봐도 역시 없습니다.
bootJar의 압축을 풀고 BOOT-INF 하위의 클래스를 읽어들여서 가져오는 방법도 있겠으나… 런타임에 그런 방법까지 쓰고 싶지는 않습니다. 여기까지 파고 들었는데 깔끔한 해결 방법이 안나온 다는건 접근 방법이 잘못된 거라고 보고 깔끔하게 포기해야지요.
결국 커스텀 Annotation을 만들고 Class에 붙이고 그 커스텀 Annotation이 붙은 객체(Bean)를 가져오는 방식
으로 해결 하였습니다.
사실 이 방법이 더 Spring 스러운?
방법 인건 처음부터 알았습니다만… 정확히는 객체가 아니라 Class
를 가져오려고 시작한 삽질이었습니다.
객체에서 Class를 가져오는 건 간단하지만…. 이런 구조면 Spring이 Class를 읽어와 new 해서 객체를 만들어서 Bean으로 등록하고 그 Bean을 가져와서 거기서 Class를 가져오는 형태가 되므로 불필요한 객체가 생성되게 됩니다.
하지만… 삽질끝에 불필요한 객체 생성를 무시하는 것이 차라리 낫다고 판단 했습니다.
Spring에서 Bean으로 Class의 객체를 등록해 볼까도 생각했지만… 이건 또 다른 삽질의 시작일거 같아서 접었습니다.
결론
- reflection 코드는 IntelliJ에서 돌아가는 코드라고 배포 되어서(bootJar에서)도 잘 동작 할 거라 확신 하지 마라.
- bootJar은 일반적인 Jar 파일과 구성이 다르다.(BOOT-INF)
- bootJar의 실행 클래스(Main-Class)는 내가 만든 클래스가 아니다. (당연하다)
- spring에서 Class 목록을 가져 오려면 그냥 커스텀 Annotation 붙이고
applicationContext.getBeansWithAnnotation
로 긁어 와라.