Programing

try-with-resources 블록에서 여러 체인 된 리소스를 관리하기위한 올바른 관용구?

lottogame 2020. 6. 2. 21:18
반응형

try-with-resources 블록에서 여러 체인 된 리소스를 관리하기위한 올바른 관용구?


Java 7 try-with-resources 구문 (ARM 블록 ( Automatic Resource Management ) 이라고도 함 )은 하나의 AutoCloseable자원 만 사용할 때 훌륭하고 짧으며 간단 합니다. 그러나 서로 의존하는 여러 리소스를 선언해야 할 때 올바른 관용구가 무엇인지 확실하지 않습니다 (예 : a FileWriter및 a) BufferedWriter. 물론이 질문 AutoCloseable은이 두 가지 특정 클래스뿐만 아니라 일부 리소스가 래핑 된 경우에도 관련이 있습니다.

나는 다음 세 가지 대안을 생각해 냈습니다.

1)

내가 본 순진한 관용구는 ARM 관리 변수에서 최상위 래퍼 만 선언하는 것입니다.

static void printToFile1(String text, File file) {
    try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) {
        bw.write(text);
    } catch (IOException ex) {
        // handle ex
    }
}

이것은 좋고 짧지 만 깨졌습니다. 기본 FileWriter변수는 변수에 선언되어 있지 않으므로 생성 된 finally블록 에서 직접 닫히지 않습니다 . close랩핑 방법을 통해서만 닫힙니다 BufferedWriter. 문제는 bw의 생성자 에서 예외가 발생 close하면 호출 FileWriter 되지 않고 기본 이 닫히지 않는다는 것 입니다.

2)

static void printToFile2(String text, File file) {
    try (FileWriter fw = new FileWriter(file);
            BufferedWriter bw = new BufferedWriter(fw)) {
        bw.write(text);
    } catch (IOException ex) {
        // handle ex
    }
}

여기서 기본 리소스와 래핑 리소스는 모두 ARM 관리 변수에서 선언되므로 두 변수 모두 확실히 닫히지 만 기본 fw.close() 래핑을 통해 직접뿐만 아니라 래핑을 통해 두 번 호출됩니다bw.close() .

계약에 여러 호출 이 허용되는 두 가지 특정 클래스 Closeable(하위 유형 AutoCloseable)를 구현하는 두 가지 특정 클래스에 대해서는 문제가되지 않습니다 close.

이 스트림을 닫고 이와 관련된 모든 시스템 리소스를 해제합니다. 스트림이 이미 닫혀 있으면이 메소드를 호출해도 효과가 없습니다.

그러나, 일반적인 경우에, 난 단지 구현 자원을 가질 수있다 AutoCloseable(그리고를 Closeable보장하지 않습니다), close여러 번 호출 할 수 있습니다 :

java.io.Closeable의 close 메소드와 달리,이 close 메소드는 dem 등성이 될 필요는 없습니다. 다시 말해,이 close 메소드를 두 번 이상 호출하면 Closeable.close와는 달리 한 번 이상 호출해도 아무런 영향을 미치지 않는 부작용이있을 수 있습니다. 그러나이 인터페이스의 구현자는 가까운 메소드를 dem 등원으로 만들 것을 강력히 권장합니다.

삼)

static void printToFile3(String text, File file) {
    try (FileWriter fw = new FileWriter(file)) {
        BufferedWriter bw = new BufferedWriter(fw);
        bw.write(text);
    } catch (IOException ex) {
        // handle ex
    }
}

이 버전은 이론적으로 정확해야합니다. 왜냐하면이 버전 만 fw정리해야하는 실제 자원을 나타 내기 때문 입니다. bw받는 사람, 그것은 단지 대표를 어떤 자원을 보유하지 자체 않습니다 fw단지 가까이 기본에 충분해야한다, 그래서 fw.

반면에 구문은 약간 불규칙하며 Eclipse는 경고를 발행하는데, 이는 잘못된 알람이라고 생각하지만 여전히 처리해야 할 경고입니다.

리소스 누출 : 'bw'는 닫히지 않습니다


그렇다면 어떤 접근 방식이 필요합니까? 아니면 올바른 다른 관용구를 놓쳤 습니까?


다음은 대안에 대한 것입니다.

1)

try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) {
    bw.write(text);
}

저에게, 15 년 전 전통적인 C ++에서 Java로 오는 가장 좋은 점은 프로그램을 신뢰할 수 있다는 것입니다. 일이 잘못되고 잘못되는 경우에도, 나머지 코드는 최상의 행동과 장미 냄새를 원합니다. 실제로 BufferedWriter여기에서 예외가 발생할 수 있습니다. 예를 들어 메모리가 부족한 것은 드문 일이 아닙니다. 다른 데코레이터의 경우 어떤 java.io래퍼 클래스에서 생성자에서 확인 된 예외를 발생시키는 지 알고 있습니까? 난 아니야 그런 종류의 모호한 지식에 의존한다면 코드 이해력이 그리 좋지 않습니다.

또한 "파괴"가 있습니다. 오류 조건이있는 경우 삭제해야하는 파일 (표시되지 않은 코드)로 쓰레기를 플러시하지 않을 수 있습니다. 물론 파일을 삭제하는 것도 오류 처리로하는 또 다른 흥미로운 작업입니다.

일반적으로 finally블록은 가능한 짧고 안정적이어야합니다. 플러시를 추가해도이 목표에 도움이되지 않습니다. 많은 릴리스의 경우 JDK에서 버퍼링 클래스의 일부에서 예외 버그했다 flush내에서 close발생 close장식 된 객체가되지 호출 할 수 있습니다. 한동안 수정되었지만 다른 구현에서도 기대할 수 있습니다.

2)

try (
    FileWriter fw = new FileWriter(file);
    BufferedWriter bw = new BufferedWriter(fw)
) {
    bw.write(text);
}

우리는 여전히 암시 적 finally 블록을 플러시하고 있습니다 (이제 반복적으로 close-꾸미기를 더 추가할수록 나빠짐). 구성은 안전하고 결국 블록을 암시해야하므로 실패하더라도 flush리소스 릴리스를 막을 수 없습니다.

삼)

try (FileWriter fw = new FileWriter(file)) {
    BufferedWriter bw = new BufferedWriter(fw);
    bw.write(text);
}

여기에 버그가 있습니다. 해야한다:

try (FileWriter fw = new FileWriter(file)) {
    BufferedWriter bw = new BufferedWriter(fw);
    bw.write(text);
    bw.flush();
}

제대로 구현되지 않은 일부 데코레이터는 실제로 리소스이므로 안정적으로 닫아야합니다. 또한 일부 스트림은 특정 방식으로 닫아야 할 수도 있습니다 (아마 압축을하고 있고 마무리하기 위해 비트를 작성해야하며 모든 것을 플러시 할 수는 없습니다).

평결

3은 기술적으로 우수한 솔루션이지만 소프트웨어 개발 이유는 2를 더 나은 선택으로 만듭니다. 그러나 try-with-resource는 여전히 부적절한 수정이므로 Java SE 8에서 클로저가있는 구문이 더 명확해야하는 Execute Around idiom 을 사용해야합니다.


첫 번째 스타일은 Oracle이 제안한 스타일입니다 . BufferedWriter확인 된 예외를 throw하지 않으므로 예외가 발생하면 프로그램에서 예외를 복구하지 않아도되므로 리소스를 거의 무질서하게 만듭니다.

스레드가 죽으면 서 스레드에서 발생할 수 있기 때문에 프로그램은 계속 진행됩니다. 예를 들어, 프로그램의 나머지 부분을 심각하게 손상시킬만큼 오래 걸리지 않은 일시적인 메모리 중단이있었습니다. 그러나 다소 모호한 경우이며 리소스 누수를 문제로 만들기에 충분할 경우 리소스 사용 시도가 가장 적은 문제입니다.


옵션 4

가능하면 자동 닫기가 아닌 닫기 가능한 리소스로 변경하십시오. 생성자가 연결될 수 있다는 사실은 리소스를 두 번 닫는 것을 들어 본 적이 없다는 것을 의미합니다. (이것은 ARM 이전에도 마찬가지였습니다.) 이에 대한 자세한 내용은 아래를 참조하십시오.

옵션 5

close ()가 두 번 호출되지 않도록 ARM과 코드를 매우 신중하게 사용하지 마십시오!

옵션 6

ARM을 사용하지 말고 try / catch 자체에서 최종 close () 호출을 수행하십시오.

이 문제가 ARM에 고유하지 않다고 생각하는 이유

이 모든 예제에서 finally close () 호출은 catch 블록에 있어야합니다. 가독성을 위해 생략했다.

fw를 두 번 닫을 수 있기 때문에 좋지 않습니다. (이것은 FileWriter에게는 좋지만 가상의 예에서는 아닙니다) :

FileWriter fw = null;
BufferedWriter bw = null;
try {
  fw = new FileWriter(file);
  bw = new BufferedWriter(fw);
  bw.write(text);
} finally {
  if ( fw != null ) fw.close();
  if ( bw != null ) bw.close();
}

BufferedWriter를 생성 할 때 예외가 발생하면 fw가 닫히지 않아 좋지 않습니다. (다시 일어날 수는 없지만 가상의 예에서) :

FileWriter fw = null;
BufferedWriter bw = null;
try {
  fw = new FileWriter(file);
  bw = new BufferedWriter(fw);
  bw.write(text);
} finally {
  if ( bw != null ) bw.close();
}

나는 Jeanne Boyarsky의 ARM 사용을 제안하지 않고 FileWriter가 항상 정확히 한 번만 닫히도록 제안하고 싶었습니다. 여기에 문제가 있다고 생각하지 마십시오 ...

FileWriter fw = null;
BufferedWriter bw = null;
try {
    fw = new FileWriter(file);
    bw = new BufferedWriter(fw);
    bw.write(text);
} finally {
    if (bw != null) bw.close();
    else if (fw != null) fw.close();
}

ARM은 단지 구문 설탕이기 때문에 finally 블록을 대체하기 위해 항상 그것을 사용할 수는 없습니다. 반복자 (iterator)로 가능한 일을하기 위해 for-each 루프를 항상 사용할 수있는 것처럼 말입니다.


To concur with earlier comments: simplest is (2) to use Closeable resources and declare them in order in the try-with-resources clause. If you only have AutoCloseable, you can wrap them in another (nested) class that just checks that close is only called once (Facade Pattern), e.g. by having private bool isClosed;. In practice even Oracle just (1) chains the constructors and doesn't correctly handle exceptions partway through the chain.

Alternatively, you can manually create a chained resource, using a static factory method; this encapsulates the chain, and handle cleanup if it fails part-way:

static BufferedWriter createBufferedWriterFromFile(File file)
  throws IOException {
  // If constructor throws an exception, no resource acquired, so no release required.
  FileWriter fileWriter = new FileWriter(file);
  try {
    return new BufferedWriter(fileWriter);  
  } catch (IOException newBufferedWriterException) {
    try {
      fileWriter.close();
    } catch (IOException closeException) {
      // Exceptions in cleanup code are secondary to exceptions in primary code (body of try),
      // as in try-with-resources.
      newBufferedWriterException.addSuppressed(closeException);
    }
    throw newBufferedWriterException;
  }
}

You can then use it as a single resource in a try-with-resources clause:

try (BufferedWriter writer = createBufferedWriterFromFile(file)) {
  // Work with writer.
}

The complexity comes from handling multiple exceptions; otherwise it's just "close resources that you've acquired so far". A common practice seems to be to first initialize the variable that holds the object that holds the resource to null (here fileWriter), and then include a null check in the cleanup, but that seems unnecessary: if the constructor fails, there's nothing to clean up, so we can just let that exception propagate, which simplifies the code a little.

You could probably do this generically:

static <T extends AutoCloseable, U extends AutoCloseable, V>
    T createChainedResource(V v) throws Exception {
  // If constructor throws an exception, no resource acquired, so no release required.
  U u = new U(v);
  try {
    return new T(u);  
  } catch (Exception newTException) {
    try {
      u.close();
    } catch (Exception closeException) {
      // Exceptions in cleanup code are secondary to exceptions in primary code (body of try),
      // as in try-with-resources.
      newTException.addSuppressed(closeException);
    }
    throw newTException;
  }
}

Similarly, you can chain three resources, etc.

As a mathematical aside, you could even chain three times by chaining two resources at a time, and it would be associative, meaning you would get the same object on success (because the constructors are associative), and same exceptions if there were a failure in any of the constructors. Assuming you added an S to the above chain (so you start with a V and end with an S, by applying U, T, and S in turn), you get the same either if you first chain S and T, then U, corresponding to (ST)U, or if you first chained T and U, then S, corresponding to S(TU). However, it would be clearer to just write out an explicit three-fold chain in a single factory function.


Since your resources are nested, your try-with clauses should also be:

try (FileWriter fw=new FileWriter(file)) {
    try (BufferedWriter bw=new BufferedWriter(fw)) {
        bw.write(text);
    } catch (IOException ex) {
        // handle ex
    }
} catch (IOException ex) {
    // handle ex
}

I would say don't use ARM and go on with Closeable. Use method like,

public void close(Closeable... closeables) {
    for (Closeable closeable: closeables) {
       try {
           closeable.close();
         } catch (IOException e) {
           // you can't much for this
          }
    }

}

Also you should consider calling close of BufferedWriter as it is not just delegating the close to FileWriter , but it does some cleanup like flushBuffer.


My solution is to do a "extract method" refactoring, as following:

static AutoCloseable writeFileWriter(FileWriter fw, String txt) throws IOException{
    final BufferedWriter bw  = new BufferedWriter(fw);
    bw.write(txt);
    return new AutoCloseable(){

        @Override
        public void close() throws IOException {
            bw.flush();
        }

    };
}

printToFile can be written either

static void printToFile(String text, File file) {
    try (FileWriter fw = new FileWriter(file)) {
        AutoCloseable w = writeFileWriter(fw, text);
        w.close();
    } catch (Exception ex) {
        // handle ex
    }
}

or

static void printToFile(String text, File file) {
    try (FileWriter fw = new FileWriter(file);
        AutoCloseable w = writeFileWriter(fw, text)){

    } catch (Exception ex) {
        // handle ex
    }
}

For class lib designers, I will suggest them extend the AutoClosable interface with an additional method to suppress the close. In this case we can then manually control the close behavior.

For language designers, the lesson is that adding a new feature could mean adding a lot others. In this Java case, obviously ARM feature will work better with a resource ownership transfer mechanism.

UPDATE

Originally the code above requires @SuppressWarning since the BufferedWriter inside the function requires close().

As suggested by a comment, if flush() to be called before close the writer, we need to do so before any return (implicit or explicit) statements inside the try block. There is currently no way to ensure the caller doing this I think, so this must be documented for writeFileWriter.

UPDATE AGAIN

The above update makes @SuppressWarning unnecessary since it require the function to return the resource to the caller, so itself does not necessary being closed. Unfortunately, this pull us back to the beginning of the situation: the warning is now moved back to the caller side.

So to properly solve this, we need a customised AutoClosable that whenever it closes, the underline BufferedWriter shall be flush()ed. Actually, this shows us another way to bypass the warning, since the BufferWriter is never closed in either way.

참고URL : https://stackoverflow.com/questions/12552863/correct-idiom-for-managing-multiple-chained-resources-in-try-with-resources-bloc

반응형