일을 하다보니 java.nio.file.Files
를 알게 되었다.
꽤나 유용한 거 같아서 이 기회에 간단한 사용법을 익혀보려고 이 글을 썼다.
지금부터 이 패키지에 어떤 기능이 있는지 간단하게 알아보자.
그리고 java.nio.file.Files
로만 하기 힘들거나, 더 심플하게 코딩할 수 있는
다른 API 들도 중간중간 소개하겠다.
참고: 주로 사용하는 패키지 목록. import 시 헷갈리면 여기를 참조하세요!
- import java.util.stream.Collectors;
- import java.io.BufferedInputStream;
- import java.io.BufferedOutputStream;
- import java.io.BufferedReader;
- import java.io.BufferedWriter;
- import java.io.InputStream;
- import java.io.OutputStream;
- import java.nio.file.Files;
- import java.nio.file.Paths;
- import java.nio.file.attribute.BasicFileAttributes;
- import java.time.Instant;
- import java.time.LocalDateTime;
- import java.time.ZoneId;
- 시간 관련된 건 다 java.time 패키지 하위에 있는 걸 사용한다.
@Test
void FileFindFilteringTest() throws IOException {
String filePath = "D:/upload/wow.txt";
BasicFileAttributes basicFileAttributes
= Files.readAttributes(Paths.get(filePath), BasicFileAttributes.class);
System.out.println("isRegularFile = " + basicFileAttributes.isRegularFile());
System.out.println("creationTime = " + basicFileAttributes.creationTime());
System.out.println("lastModifiedTime = " + basicFileAttributes.lastModifiedTime());
}
출력 결과:
isRegularFile() = true
creationTime() = 2021-12-21T11:03:45.21Z
lastModifiedTime() = 2021-12-21T10:04:22Z
Files.newInputStream(Paths.get(realPath))
Files.newOutputStream(Paths.get(writePath))
@Test
void testMe() {
// 그냥 경로를 지정하는 코드다 신경쓰지 말자
String realPath = "D:/[Java] Read And Write File/123.txt";
String writePath = realPath.substring(0, realPath.lastIndexOf("/")) + "/wow.txt" ;
// 여기서부터 집중
try(InputStream is = Files.newInputStream(Paths.get(realPath));
OutputStream os = Files.newOutputStream(Paths.get(writePath))
) {
// 어떤 java 버전에서도 가능한 정석 코드
byte[] buffer = new byte[8192];
int read;
while ((read = is.read(buffer)) >= 0) {
os.write(buffer, 0, read);
}
// java 9 이상 사용시 Inputstream 의 transferTo(OutputStream out); 사용
// is.transferTo(os);
// Spring 을 사용 중이라면 아래 2가지 방식 모두 고려해보자.
// StreamUtils.copy(is, os); // is, os 를 close 하지 않음, 내부 버퍼 4096 사용
// FileCopyUtils.copy(is, os); // is, os 를 자동으로 close 함, 내부 버퍼 8192 사용
} catch (Exception e) {
e.printStackTrace();
}
}
위에서 사용한 것은 InputStream, OutputStream 이였다면
이번에는 Reader, Writer로 파일을 제어할 수 있는 방법도 알아보자.
Files.newBufferedReader(Paths.get("파일경로"), StandardCharsets.UTF_8);
Files.newBufferedWriter(Paths.get("파일경로"), StandardCharsets.UTF_8);
이 방법으로 다시 위의 동일한 파일 복제 기능을 구현하려면 아래처럼 코딩한다.
String realPath = "D:/[Java] Read And Write File/123.txt";
String writePath = realPath.substring(0, realPath.lastIndexOf("/")) + "/wow.txt" ;
try(BufferedReader reader = Files.newBufferedReader(Paths.get(realPath), StandardCharsets.UTF_8);
BufferedWriter writer = Files.newBufferedWriter(Paths.get(writePath), StandardCharsets.UTF_8)
) {
// 어떤 java 버전에서도 가능한 정석 코드
char[] buffer = new char[8192];
int read;
while ((read = reader.read(buffer)) >= 0) {
writer.write(buffer, 0, read);
}
} catch (Exception e) {
e.printStackTrace();
}
지금까지 Stream 을 사용한 파일 복제 방법을 알아봤다.
그런데 단순 파일 복제는 아래에서 볼 경로를 통한 파일 복제
방법이 더 편하다.
Stream 방식은 HttpServletRequest
, HttpServletResponse
와 작업을 할 때 더 유용하다.
정말 간단하다.
public static void main(String[] args) throws IOException {
String realPath = "D:/[Java] Read And Write File/123.txt";
String writePath = realPath.substring(0, realPath.lastIndexOf("/")) + "/wow.txt" ;
Files.copy(Paths.get(realPath), Paths.get(writePath));
}
Files.copy(); 는 오버라이드로 3가지 종류가 있다
Files.copy(Path path, OutputStream out)
Files.copy(InputStream in, Path path, CopyOption... option)
Files.copy(Path path, Path target, CopyOption... option)
대충보면 어떻게 쓰는지 감이 잡히리라 생각한다. 더 이상의 설명은 안하겠다.
참고로 java.nio.file.CopyOption 인자 값으로 몇가지 추가적인 옵션을 줄 수 있다.
다른 블로그에서는 StandardCopyOption.REPLACE_EXISTING 를 흔히 쓰는 걸 봤다. 필요하다면 사용하자.
Files
에는 재귀적으로 Directory 내의 모든 것을 복사하는 기능은 없다.
Directory의 내부 모든 파일을 재귀적으로 복사하고 싶다면
org.springframework.util.FileSystemUtils.copyRecursively(~)
를 사용하자.
방법1: Files.newBufferedReader
public static void main(String[] args) {
String temp = "D:/[Java] Read And Write File/123.txt";
try(BufferedReader br = Files.newBufferedReader(Paths.get(temp), StandardCharsets.UTF_8)) {
StringBuilder builder = new StringBuilder();
String str = null;
while((str = br.readLine()) != null) {
builder.append(str).append("\n");
}
System.out.print(builder.toString());
} catch (IOException e) {
e.printStackTrace();
}
}
방법2: Files.lines
public static void main(String[] args) {
String temp = "D:/[Java] Read And Write File/123.txt";
try(Stream<String> lines = Files.lines(Paths.get(temp), StandardCharsets.UTF_8)) {
StringBuilder builder = new StringBuilder();
lines.forEach(s -> builder.append(s).append("\n"));
System.out.println(builder.toString());
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
String temp = "D:/[Java] Read And Write File/123.txt";
ArrayList<String> messageList = new ArrayList<>(Arrays.asList("hello", "world"));
// 방법1: Files.write
// 두번째 파라미터로 Iterable<? extends CharSequence> iterable 를 받는다.
Files.write(Paths.get(temp), messageList, StandardCharsets.UTF_8);
// 방법2: Files.writeString
Files.writeString(Paths.get("C:/upload/jackson/ex1.txt"),
"안녕하세요", StandardCharsets.UTF_8);
}
위 2가지 방법 모두 기존 텍스트 파일의 내용은 다 지워지고, 새로이 작성되는 점 주의하기 바란다.
@Test
@Order(4)
@DisplayName("실제 Java 파일의 내용을 템플릿화한다")
void createJavaFileContentTemplateTest() {
try (BufferedWriter bufferedWriter
= Files.newBufferedWriter(
Paths.get("C:/study/file_create/Employee.java"),
StandardCharsets.UTF_8,
StandardOpenOption.CREATE)
) {
// StandardOpenOption.CREATE 덕분에 파일 생성과 동시에 글쓰기가 가능하다.
bufferedWriter.write("""
package me.dailycode.vo;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
public class %s {
/**
* %s
*/
private %s %s;
}
""".formatted("Employee", "comment", "type", "fieldName"));
} catch (IOException e) {
fail(e.getMessage(), e);
}
}
테스트를 위하여 C:\upload\directory1 경로에 아래와 같은 디렉토리 및 파일 구조를 잡았다.
\---directory1
+---directory1_depth1
| | directory1_dept1_file1.txt
| | directory1_dept1_file2.txt
| |
| \---directory1_dept2
| directory1_dept2_file1.txt
| directory1_dept2_file2.txt
|
\---directory2_depth1
directory2_dept1_file1.txt
directory2_dept1_file2.txt
이제 directory 모든 파일들을 조회해보자.
@Test
void FilesWalkTest() {
String directoryPath = "C:/upload/directory1";
List <Path> list = Collections.emptyList();
try(Stream<Path> walk = Files.walk(Paths.get(directoryPath))) {
list = walk.filter(Files::isRegularFile)
.collect(Collectors.toList());
} catch (IOException e) {
e.printStackTrace();
}
list.forEach(System.out::println);
}
출력은 아래와 같이 나온다.
폴더의 depth 를 지정하고 싶다면 Files.walk(Paths.get(directoryPath), 2)
;
처럼 maxDepth를 지정할 수도 있다.
그리고 반드시 사용할 때는 try-with-resource를 사용해서 close 가 되도록 해주자.
API Note:
This method must be used within a try-with-resources statement or similar control structure to ensure that the stream's open directories are closed promptly after the stream's operations have completed.
참고:https://stackoverflow.com/questions/54096143/how-to-close-implicit-stream-in-java
테스트를 위하여 C:\upload\directory1 경로에 아래와 같은 디렉토리 및 파일 구조를 잡았다.
\---directory1
| dIZwF9gV0Z.jpg
| Hpdf_ghlRA7mJEb.jpg
|
+---directory1_depth1
| | directory1_dept1_file1.txt
| | directory1_dept1_file2.txt
| |
| \---directory1_dept2
| directory1_dept2_file1.txt
| directory1_dept2_file2.txt
|
\---directory2_depth1
directory2_dept1_file1.txt
directory2_dept1_file2.txt
idea64_w4zW6Yf1T3.jpg
idea64_W8HLV3vgyV.jpg
idea64_ZbmjlckX4b.jpg
Java 코드는 아래와 같다.
@Test
void FileFindTest() {
String[] fileExtensions = {"png", "jpg", "gif"};
String directoryPath = "C:/upload/directory1";
Path path = Paths.get(directoryPath);
List<Path> collect = Collections.emptyList();
if (Files.isDirectory(path)) {
try(Stream<Path> walk = Files.walk(path)) {
collect = walk.filter(p -> !Files.isDirectory(p))
.filter(f -> Arrays.stream(fileExtensions)
.anyMatch(s -> f.toString().endsWith(s))
).collect(Collectors.toList());
} catch (IOException e) {
e.printStackTrace();
}
}
collect.forEach(System.out::println);
}
출력 결과
위에서는 Files.walk
를 사용했지만, 이번에는 Files.find
API를 사용해보자.
아래 코드는 어제 자정(= 00:00)부터 현재까지 변경 이력이 있는 파일 목록을 조회하는 코드다.
/*
디렉토리 구조
C:\UPLOAD\DIRECTORY1
| 23day.png ===> 2021-12-23 에 수정됨
| dodo.png
| lala.png
|
+---directory1_depth1
| | directory1_dept1_file1.txt ===> 2021-12-22 에 수정됨
| | directory1_dept1_file2.txt
| |
| \---directory1_dept2
| directory1_dept2_file1.txt
| directory1_dept2_file2.txt ===> 2021-12-22 에 수정됨
|
+---directory2_depth1
| directory2_dept1_file1.txt
| directory2_dept1_file2.txt
|
\---img
at_12_17(2).png
at_12_17.png
image1.png
*/
public static void main(String[] args) {
// 필터링할 파일을 갖고 있는 디렉토리
Path directoryPath = Paths.get("C:/upload/directory1");
// 앞으로 자주 쓸 DateTimeFormatter 를 선언 및 할당
DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
ZonedDateTime yesterdayTruncate
= ZonedDateTime.now(ZoneId.of("Asia/Seoul")) // 내가 사는 한국 시간 기준으로
.minusDays(1) // 하루를 빼고
.truncatedTo(ChronoUnit.DAYS); // 거기에 자정으로 시간을 맞춘다.
// 참고) 현재 코드를 작성한 시간은 2021-12-23 오후 12 시 29분
System.out.println("today : "
+ ZonedDateTime.now(ZoneId.of("Asia/Seoul")).format(dateFormat));
// 2021-12-22 00:00:00 이 출력된다.
System.out.println("yesterdayTruncate : "
+ yesterdayTruncate.format(dateFormat));
// Instant를 쓰면 TimeZone을 신경 쓰지 않고 시간 연산을 할 수 있기 때문에 편리하다.
Instant yesterdayInstant = yesterdayTruncate.toInstant();
// 필터링 한 파일 목록을 저장할 리스트
List<Path> result = Collections.emptyList();
// Files.find의 인자값은 아래와 같다.
// 첫번째 인자값은 디렉토리명
// 두번째 파라미터는 디렉토리 내부에서 검색할 때 몇 레벨까지 검색할지 결정
// 세번째 인자값은 필터링 콜백
try(Stream<Path> pathStream = Files.find(directoryPath,
Integer.MAX_VALUE,
(path, basicFileAttributes) -> {
// 폴더는 무시 + 읽을 수 없는 파일도 무시
if(Files.isDirectory(path) || !Files.isReadable(path)) {
return false;
}
// 최종 수정된 날짜를 조회
FileTime fileModifedTime = basicFileAttributes.lastModifiedTime();
// 조회된 날짜에서 Instant 를 뽑아냄
Instant fileInstant = fileModifedTime.toInstant();
// 최종적으로 위에서 작성한 yesterdayInstant 와 비교한다.
// 만약 0 이상이면 2021-12-22 00:00:00 시간을 포함한 그 이후의 시간에
// 파일이 수정되었다는 것을 의미한다.
return fileInstant.compareTo(yesterdayInstant) >= 0;
})) {
// 결과를 수집한다.
result = pathStream.collect(Collectors.toList());
} catch (IOException e) {
e.printStackTrace();
}
// 결과를 조회한다.
result.forEach(p -> {
try {
// 속성 조회
BasicFileAttributes readAttributes =
Files.readAttributes(p, BasicFileAttributes.class);
// 파일 수정시간 조회
FileTime lastModifiedTime = readAttributes.lastModifiedTime();
// 수정 시간에서 ZonedDateTime을 뽑아냄
ZonedDateTime zonedDateTime =
lastModifiedTime.toInstant().atZone(ZoneId.of("Asia/Seoul"));
// 출력
System.out.println(String.format("%-100s [%s]",
p.toAbsolutePath().toString(),
zonedDateTime.format(dateFormat)));
} catch (IOException e) {
e.printStackTrace();
}
});
}
Files.find
도 Files.walk
와 마찬가지로 사용할 때는 try-with-resource를 사용해야 한다.
Files.walk
와 Files.find
API는 굉장히 유사하다.
그러니 자신이 더 편리하다 생각하는 API를 사용하면 될 듯하다.
참고로 위 코드에서 ZoneDateTime 과 Instant 에 대해서 잘 모르겠다면
이 블로그를 참고하면 좋다.
2023-12-24 내용 추가
디렉토리 내에서 특정 파일의 prefix
와 확장자
를
확인하고 복사하는 기능을 만들고 싶다면 Files.walkFileTree
통해서 가능합니다.
테스트 코드 작성 환경
JDK
:Azul 17
Spring version
:6
아래 코드의 목표는 다음과 같습니다.
위 디렉토리 내부에 있는 파일들 중에서
copy_
로 시작되면서,txt,csv
인 것만 복사해가고 싶은 겁니다.지금부터 이걸 가능케 하는 코드를 짜보겠습니다.
참고로 jdk 17 의 record 문법을 약간 씁니다!
record 가 생소하면 가볍게 검색하고 오시기 바랍니다.
package me.dailycode.apachecamel.filecopy;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.util.StringUtils;
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.EnumSet;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
@Slf4j
public class FileCopyTests {
@Test
void testCopyRecursive() throws IOException {
String destinationPath = "C:/test_data/to";
String sourcePath = "C:/test_data/from";
copyRecursively(
Paths.get(sourcePath),
Paths.get(destinationPath),
new AdditionalOptions("copy_", "txt,csv")
);
}
private void copyRecursively(Path src, Path dest) throws IOException {
copyRecursively(src, dest, new AdditionalOptions("",""));
}
private void copyRecursively(Path src, Path dest, AdditionalOptions options) throws IOException {
// src 로 준 디렉토리 경로 "내부"의 파일들만 검색하기 위해서 고의적으로 1을 줬다.
// 참고로 이렇게 하면 src 디렉토리 하부의 subdirectory 는 뒤져보지 않는다!
// 딱 src 디렉토리의 "파일"만 뒤져본다.
final int MAX_SEARCH_DEPTH = 1;
// 필수값 체크
checkRequiredArguments(src, dest);
// 앞서서 "zip,txt" 처럼 받은 확장자 문자열을 Set 으로 변환시킵니다.
Set<String> setOfFileExtensions = options.getFileExtSet();
// 맨 처음 src 에 대해 접근할 때 디렉토리인지 파일인지를 먼저 알아야겠죠?
// 이를 위해서 Files.readAttributes() 메소드를 호출하는 겁니다.
BasicFileAttributes srcAttr = Files.readAttributes(src, BasicFileAttributes.class);
if (srcAttr.isDirectory()) { // 디렉토리인 지 확인합니다.
//EnumSet.of(FOLLOW_LINKS) // 만약 링크 디렉토리도 타깃으로 잡고 싶다면 이렇게!
// 링크 디렉토리까지는 파일 복사 타깃을 하고 싶지 않다면 이렇게!
Files.walkFileTree(src, EnumSet.noneOf(FileVisitOption.class), MAX_SEARCH_DEPTH, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
Files.createDirectories(dest.resolve(src.relativize(dir)));
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
// 파일의 Prefix 체크 - 체크 조건(filteringFilePrefix 여부)
if (StringUtils.hasText(options.filteringFilePrefix())) {
// 파일 prefix 가 맞지 않다면? return FileVisitResult.CONTINUE;
String pureFileName = StringUtils.stripFilenameExtension(file.getFileName().toString());
if (!pureFileName.startsWith(options.filteringFilePrefix())) {
return FileVisitResult.CONTINUE;
}
}
// 파일의 확장자 체크 - 체크 조건(setOfFileExtensions 가 비었는지 아닌지 여부)
if (!setOfFileExtensions.isEmpty()) {
String fileAbsolutePath = file.toAbsolutePath().toString();
String filenameExtension = StringUtils.getFilenameExtension(fileAbsolutePath);
// 파일 확장자가 맞지 않다면? return FileVisitResult.CONTINUE;
if (!setOfFileExtensions.contains(filenameExtension)) {
return FileVisitResult.CONTINUE;
}
}
// 만약 위 2개의 조건을 모두 넘어왔다면 Files.copy + fileList.add
Path destFile = dest.resolve(src.relativize(file));
Files.copy(file, destFile, StandardCopyOption.REPLACE_EXISTING);
return FileVisitResult.CONTINUE;
}
});
}
else if (srcAttr.isRegularFile()) {
// 혹여 단일 파일에 대한 처리도 필요하다면 여기에 작성.
throw new IllegalArgumentException("파일에 대해서는 처리하지 않습니다.");
}
else {
throw new IllegalArgumentException("존재하지 않는 디렉토리입니다");
}
}
private record AdditionalOptions(String filteringFilePrefix, String filteringFileExt) {
public AdditionalOptions {
if (!StringUtils.hasText(filteringFilePrefix)) {
filteringFilePrefix = "";
}
if (!StringUtils.hasText(filteringFileExt)) {
filteringFileExt = "";
}
}
public Set<String> getFileExtSet() {
// comma word to Java Set Instance
Set<String> strings = StringUtils.commaDelimitedListToSet(filteringFileExt);
// 잘못된 확장자 사용을 방지하기 위한 재파싱 과정
return strings.stream()
.map(String::trim) // 일단 양쪽에 있는 공백 지우고~
.filter(s -> !StringUtils.containsWhitespace(s)) // 글자 사이에 공백이 있으면, ignore!
.collect(Collectors.toSet());
}
}
private static void checkRequiredArguments(Path src, Path dest) {
Objects.requireNonNull(src, "Source Path required!");
Objects.requireNonNull(dest, "destination Path required!");
}
}
Source Directory
Destination Directory
copy_
"txt,csv"
... 인 것만 복사됐네요! 성공입니다!
package coding.toast.bread.converting;
import org.springframework.util.StringUtils;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.UUID;
public class FileEncodeChanger {
static void changeEncode(String filePath, String inputEncoding, String outputEncoding) throws IOException {
String tempFileName = UUID.randomUUID().toString();
Path inputFile = Paths.get(filePath).toAbsolutePath();
String fileExtension = StringUtils.getFilenameExtension(filePath);
Path tempOutputFile = Paths.get(inputFile.getParent().toString(), tempFileName + "." + fileExtension);
try (BufferedReader br = Files.newBufferedReader(inputFile, Charset.forName(inputEncoding));
BufferedWriter bw = Files.newBufferedWriter(tempOutputFile, Charset.forName(outputEncoding))) {
String oneLine;
while ((oneLine = br.readLine()) != null) {
bw.append(oneLine).append("\n").flush();
}
}
Files.deleteIfExists(inputFile);
Files.move(tempOutputFile, inputFile);
}
public static void main(String[] args) throws IOException {
changeEncode("D:\\java-playground\\src\\test\\resources\\customers-100.csv",
"UTF-8",
"EUC-KR");
}
}
package coding.toast.bread.file_move;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
public class FileMoveTests {
@Test
void moveTest() throws IOException {
Files.move(
Path.of("D:\\ndata\\batch\\moved_source2"),
Path.of("D:\\ndata\\moved_source"),
StandardCopyOption.REPLACE_EXISTING);
}
}