maven-fat-jar-test git:master 🍺🦑🍺🍕🍺 ❯ java -jar target/maven-fat-jar.jar 00:58:11.479 [main] INFO io.homo_efficio.ResourceLoader - *** getResource() + File 방식 00:58:11.481 [main] INFO io.homo_efficio.ResourceLoader - content root: /static 00:58:11.482 [main] INFO io.homo_efficio.ResourceLoader - resourceLocation: /folder1/sample1 00:58:11.484 [main] INFO io.homo_efficio.ResourceLoader - resourceURL: jar:file:/Users/1003604/gitRepo/study/maven-fat-jar-test/target/maven-fat-jar.jar!/static/folder1/sample1 00:58:11.484 [main] INFO io.homo_efficio.ResourceLoader - fileLocation from URL: file:/Users/1003604/gitRepo/study/maven-fat-jar-test/target/maven-fat-jar.jar!/static/folder1/sample1 Exception in thread "main" java.io.FileNotFoundException: file:/Users/1003604/gitRepo/study/maven-fat-jar-test/target/maven-fat-jar.jar!/static/folder1/sample1 (No such file or directory) at java.base/java.io.FileInputStream.open0(Native Method) at java.base/java.io.FileInputStream.open(FileInputStream.java:212) at java.base/java.io.FileInputStream.<init>(FileInputStream.java:154) at java.base/java.io.FileReader.<init>(FileReader.java:75) at io.homo_efficio.ResourceLoader.loadResourceAsFile(ResourceLoader.java:38) at io.homo_efficio.App.main(App.java:19)
에러 메시지를 보면 파일 경로(정확히는 URL)가 file:/Users/1003604/gitRepo/study/maven-fat-jar-test/target/maven-fat-jar.jar!/static/folder1/sample1 라고 표시된다. fat-jar 파일이 중간에 mavan-fat-jar.jar! 로 표시돼 있는데 이렇게 !가 포함된 경로는 실제로 존재하지 않기 때문에 위와 같은 에러가 발생하게 된다.
즉 IDE에서 실행할 때는 실제 파일시스템 기준 경로를 따르므로 에러가 발생하지 않지만, jar 파일을 읽을 때는 jar 파일이 !와 함께 표시되기 때문에 실제 파일시스템 경로에 맞지 않아 에러가 발생한다.
rsourceURL 값이 IDE 에서 실행할 때는 file: 로 시작했는데, jar 로 실행할 때는 jar:file:로 시작한다. 이것도 기억해두자.
어쨌든 자바 프로그램은 실제로는 대부분 jar 로 만들어져서 실행될텐데, jar 에서 제대로 실행이 안 된다면 이를 어쩐다?
getResourceAsStream()
getResource() 는 기본적으로 URL 을 반환한다. URL은 위와 같이 jar 파일을 !와 함께 표시하기 때문에, jar 실행 시 에러가 발생한다.
23:55:45.443 [main] INFO io.homo_efficio.ResourceLoader - OOO getResourceAsStream() 방식 23:55:45.443 [main] INFO io.homo_efficio.ResourceLoader - content root: /static 23:55:45.443 [main] INFO io.homo_efficio.ResourceLoader - resourceLocation: /folder1/sample1 23:55:45.443 [main] INFO io.homo_efficio.ResourceLoader - resource contents: Sample File 1
fat-jar 에서 동작 확인
fat-jar 실행 시에도 정상적으로 실행된다.
1 2 3 4 5
maven-fat-jar-test git:master 🍺🦑🍺🍕🍺 ❯ java -jar target/maven-fat-jar.jar 00:01:31.774 [main] INFO io.homo_efficio.ResourceLoader - OOO getResourceAsStream() 방식 00:01:31.775 [main] INFO io.homo_efficio.ResourceLoader - content root: /static 00:01:31.776 [main] INFO io.homo_efficio.ResourceLoader - resourceLocation: /folder1/sample1 00:01:31.778 [main] INFO io.homo_efficio.ResourceLoader - resource contents: Sample File 1
따라서 getResource() 보다는 getResourceAsStream()을 사용하자. 끝.
속사정
혹시 왜 이런 차이가 발생하는지 궁금한 사람들은 이어서 읽어보자.
getResourceAsStream() 호출을 따라가보면 Java 14 기준 BuiltinClassLoader 클래스에서 아래와 같은 코드를 만나게 되는데,
openStream() 을 따라가면 왜 되는지 알 수 있다. openConnection()은 URLConnection 을 반환하는데, 이 URLConnection 에는 여러가지 SubClass가 있어서 다형적으로 동작할 수 있다.
앞에서 IDE 에서 실행할 때는 URL 값이 file: 로 시작하고, jar 로 실행할 떄는 URL 값이 jar:file: 로 시작하는 것을 기억해두자고 한 것을 상기해보면 답이 보일 것이다.
URL 이 file: 로 시작하는 IDE 에서는 FileURLConnection 이 사용되고, URL 이 jar:file: 로 시작하는 jar 실행에서는 JarURLConnection 이 사용된다. getResourceAsStream()은 다형적으로 동작하도록 구현돼 있어서 두 상황 모두에서 잘 동작할 수 있다.
그럼 getResource()는?
사실 문제는 getResource() 가 아니라 getResource() 이 반환하는 URL 을 어떻게 쓰느냐에 있다. 똑같이 getResource() 를 사용하더라도 다음과 같이 openStream() 을 사용하면 getResource() 을 사용해도 jar 에서도 잘 동작한다.
즉, URL 에서 File 을 생성하면 다형성이 적용되지 않아 IDE 에서는 되지만 jar 에서는 안 되는 상황이 연출되고, URL 에서 InputStream 을 뽑아서 사용하면 다형성이 적용돼서 IDE, jar 모두에서 잘 동작한다.
try { ObjectMapper objectMapper = new ObjectMapper(); Config config = objectMapper.readValue(configURL, Config.class); log.info("title in config: {}", config.getTitle()); log.info("tags in config: [{}]", String.join(", ", config.getTags())); } catch (IOException e) { thrownew RuntimeException("설정 파일 로딩에 실패했습니다.", e); } }
IDE 실행 결과
1 2 3 4 5 6
12:09:01.465 [main] INFO io.homo_efficio.ResourceLoader - *** getResource() + Jackson 방식 12:09:01.465 [main] INFO io.homo_efficio.ResourceLoader - content root: /static 12:09:01.465 [main] INFO io.homo_efficio.ResourceLoader - resourceLocation: /folder1/config.json 12:09:01.466 [main] INFO io.homo_efficio.ResourceLoader - resourceURL: file:/Users/1003604/gitRepo/study/maven-fat-jar-test/target/classes/static/folder1/config.json 12:09:01.617 [main] INFO io.homo_efficio.ResourceLoader - title in config: Java Resource Handling 12:09:01.617 [main] INFO io.homo_efficio.ResourceLoader - tags in config: [Java, Resource, fat, jar]
jar 실행 결과
1 2 3 4 5 6 7
maven-fat-jar-test git:master 🍺🦑🍺🍕🍺 ❯ java -jar target/maven-fat-jar.jar 12:09:25.075 [main] INFO io.homo_efficio.ResourceLoader - *** getResource() + Jackson 방식 12:09:25.075 [main] INFO io.homo_efficio.ResourceLoader - content root: /static 12:09:25.075 [main] INFO io.homo_efficio.ResourceLoader - resourceLocation: /folder1/config.json 12:09:25.076 [main] INFO io.homo_efficio.ResourceLoader - resourceURL: jar:file:/Users/1003604/gitRepo/study/maven-fat-jar-test/target/maven-fat-jar.jar!/static/folder1/config.json 12:09:25.213 [main] INFO io.homo_efficio.ResourceLoader - title in config: Java Resource Handling 12:09:25.213 [main] INFO io.homo_efficio.ResourceLoader - tags in config: [Java, Resource, fat, jar]
Properties
.properties 파일을 읽을 때 사용하는 Properties 클래스에는 load(Reader r), load(InputStream i) 두 가지 메서드가 있다. IDE, jar 모두에서 동작하려면 어느 것을 써야할지 이젠 해보지 않아도 알 수 있을 것 같다.
디렉터리 내 파일 목록
개별 파일은 위와 같이 대응할 수 있다는 걸 알게 됐다. 그런데 디렉터리 내 파일 목록을 읽어서 원하는 대로 처리하는 것도 IDE, jar 에서 모두 가능할까?
이미 구구절절 많이 떠들었으니 바로 코드로 살펴보자. jar 파일 내에서 목록 단위로 처리하려면 JarFile이 필요하다는 점만 기억해두자. 나머지 주의해서 볼 점은 주석에 표시해놨다.
마지막에 나오는 코드 enumerationAsStream() 메서드는 Enumeration을 copy를 유발하지 않고 Stream 으로 쓸 수 있게 해주는 유틸 메서드다.(자바는 왜 이런 걸 공식 SDK에 포함하지 않는 건가..)
// IDE 에서는 잘 동작, jar 에서는 에러는 발생하지 않으나 esourceAsStream.readAllBytes() 값이 비어있음 log.info("USING naive getResourceAsStream(String directory) -----"); InputStream resourceAsStream = this.getClass().getResourceAsStream(root + dir); byte[] bytes = resourceAsStream.readAllBytes(); log.info("resource contents length: {}", bytes.length);
if (bytes.length > 0) { log.info("resource contents: {}", new String(bytes, StandardCharsets.UTF_8)); } else { // jar 에서는 잘 동작 // IDE 에서는 예외 발생: java.io.FileNotFoundException: /Users/1003604/gitRepo/study/maven-fat-jar-test/target/classes/io/homo_efficio (Is a directory) // 따라서 bytes.length = 0 일 때만 실행하도록 log.info("USING JarFile -----"); String path = this.getClass().getResource("").getPath(); log.info("resourcePath: {}", path); int exclamationIndex = path.lastIndexOf("!") > 0 ? path.lastIndexOf("!") : path.length(); String jarFilePath = path.substring(0, exclamationIndex).replaceAll("file:", ""); log.info("jarFilePath : {}", jarFilePath); LocalDateTime start = LocalDateTime.now(); log.info("jarFile start: {}", start); JarFile jarFile = new JarFile(jarFilePath); LocalDateTime end = LocalDateTime.now(); log.info("jarFile end : {}", end); enumerationAsStream(jarFile.entries()) .filter(entry -> entry.getRealName().startsWith((root + dir).substring(1))) .forEach(entry -> log.info("jarEntry: {}", entry.getRealName())); } }
// From https://stackoverflow.com/a/23276455 static <T> Stream<T> enumerationAsStream(Enumeration<T> e){ return StreamSupport.stream( Spliterators.spliteratorUnknownSize( new Iterator<T>() { @Override publicbooleanhasNext(){ return e.hasMoreElements(); }
@Override public T next(){ return e.nextElement(); } }, Spliterator.ORDERED ), false ); }