[지속 업데이트] 자바 서열 화 대상 의문 풀기

24969 단어 D0004Java
자바 에 서 는 Seriableizable 인 터 페 이 스 를 대상 의 직렬 화 기능 에 사용 합 니 다. 직렬 화 는 더욱 편리 한 데이터 전송, 저장 을 위 한 것 이지 만 과도 하 게 사용 해 서 는 안 됩 니 다. 특히 깊이 의존 해 서 는 안 됩 니 다. 그렇지 않 으 면 호환성 (예 를 들 어 바 이 너 리 호환성), 의미 호환성 (Semantic Compatibility) 등 문제 가 자주 발생 하여 번 거 로 움 을 견 딜 수 없습니다.
Serial Version UID
UID 는 Unique Identifier 의 줄 임 말 입 니 다. 모든 직렬 화 클래스 는 하나의 UID 와 관련 이 있 습 니 다. (스 트림 유일한 식별 자 (Stream Unique Identifier) 와 관련 이 있 습 니 다. serial VersionUID 라 는 개인 정적 final 의 long 필드 에 이 표지 번 호 를 명시 적 으로 지정 하지 않 으 면 시스템 은 자동 으로 이 클래스 에 따라 복잡 한 연산 과정 을 호출 합 니 다.실행 할 때 이 표시 자 를 만 듭 니 다.
일반적인 IDE 는 자동 으로 UID 를 생 성 하 는 추가 기능 을 가지 고 있 으 며, Intellij 족 에 서 는 serialVersionUI 가 정의 되 지 않 은 것 을 오류 단계 로 설정 하여 문 제 를 발견 할 수 있 습 니 다.
'직렬 화 - 반 직렬 화' 의 과정 성공 을 확보 하기 위해 UID 는 없어 서 는 안 된다. 다음 과 같은 간단 한 유형 을 예 로 들 면:
public class Person implements Serializable{
    public Person() {
    }
    private String name;
    private Gender gender;
    private int age;
    private boolean alive;
}

현재 버 전에 서 writeObject 를 사용 하여 파일 에 기록 했다 면, 몇 시 후에 Person 에 새로운 Field 를 추가 한 사람 이 있 습 니 다. 예 를 들 어.
    private Gender aaa;

이 때 readObject 를 사용 하여 저 장 된 파일 을 읽 을 때 다음 과 같은 문제 가 발생 합 니 다.
java.io.InvalidClassException: models.Person; local class incompatible: stream classdesc serialVersionUID = 2990078061752767256, local class serialVersionUID = 5301378833195569126

Serializable
직렬 화 된 저장, 불 러 오기 가 쉽 습 니 다. 그러면 편리 성에 의존 하여 과도 하 게 의존 하 는 사람 이 있 습 니 다. 일부 사용자 정의 형식의 파일 저장 에서 사용 하면 파일 자체 가 직렬 화 된 클래스 에 심각 한 의존 을 하여 앞으로 호 환 되 는 여러 가지 어려움 이 발생 합 니 다.또한 직렬 화 대상 의 보존 형식 으로 인해 보이 지 않 고 문제 가 발생 하면 정확 한 출처 를 찾 을 수 없다.
'Effective Java' 에 서 는 Serializable 인터페이스 와 관련 된 논 의 를 했 으 며 자세 한 논 의 는 하지 않 았 다.필 자 는 Serializable 은 지속 적 인 대상 을 의미 하기 때문에 일부 약속 에 대해 장기 적 인 고정 이 필요 하고 수요 모델 이 끊임없이 변경 되 는 장면 에 사용 할 수 없다 고 생각한다.
Seriableizable 은 직렬 화 과정 에서 대량의 임시 변 수 를 발생 시 켜 빈번 한 GC 를 초래 할 수 있 으 므 로 성능 을 평가 해 야 하 는 상황 에서 신중하게 사용 해 야 한다.
Android 에 서 는 Serializable 대신 Parcelable 인 터 페 이 스 를 제공 합 니 다. 네트워크 나 프로 세 스 간 전달 대상 에 사용 할 수 있 지만 안정성 이 후자 보다 낮 기 때문에 파일 을 영구적 으로 저장 해 야 하 는 곳 에 서 는 Serializable 을 사용 해 야 합 니 다.
다시 한 번 주의해 야 할 상황:
public abstract class IGroup {
    public int getGroupID() {
        return groupID;
    }
    public void setGroupID(int groupID) {
        this.groupID = groupID;
    }
    private int groupID;
}

public class PersonGroup extends IGroup implements Serializable {
    private static final long serialVersionUID = -2581096266403253738L;

    public PersonGroup(String groupTitle, int aim, long startTime) {
        this.groupTitle = groupTitle;
        this.aim = aim;
        this.startTime = startTime;
    }
    private String groupTitle;
    private int aim;
    private long startTime;

}

상기 코드 에서 IGroup 은 계승 을 위해 설 계 된 불가 서열 화 클래스 이 고 Person Group 은 IGroup 을 계승 한 서열 화 클래스 이다.그러면 Person Group 을 직렬 화 할 때 IGroup 의 groupID 라 는 Field 는 처리 되 지 않 습 니 다.
부모 클래스 가 Serializable 인 터 페 이 스 를 실현 하지 않 으 려 면 Field 의 입 구 를 명확 하 게 설정 하여 하위 클래스 가 주동 적 으로 호출 할 수 있 도록 해 야 합 니 다.
Serializable 의 사용 에 있어 서 이른바 원칙 적 인 건의 가 많 지만 사실은 모두 무시 할 수 있다. 구체 적 인 사용 장면 이 디자인 모델 을 결정 하기 때문에 선인 들 의 경험 을 기준 으로 삼 는 것 이 아니 라 디자인 모델 을 결정 하기 때문이다.선인 들 이 그렇게 정리 한 것 은 게 으 른 사람들 이 기 존 코드 의 완전 성과 구조 성 을 파괴 하지 못 하 게 하고 처음에 알 수 없 었 던 사람들 이 먼저 하도록 하기 위해 서 였 다.그러나 결국 우 리 는 이렇게 하 는 원인 을 알 아야 지, 남 이 말 하 는 대로 따라 해 서 는 안 된다.
여기에 transient 키 워드 를 추가 합 니 다. 직렬 화 되 지 않 으 려 는 Field 를 걸 러 내 는 데 사 용 됩 니 다. hack 공격 을 방지 하 는 데 효과 가 있 습 니 다. Serializable 에 사용 되 는 곳 에서 Field 가 실제 운행 효과 와 관련 되 고 직렬 화 되 지 않 는 다 면 transient 로 수식 해 야 합 니 다. 그림 을 간략하게 해 서 는 안 됩 니 다.
사용자 정의 직렬 화 형식
에서 사용자 정의 서열 화 형식 에 대해 언급 했 는데 주로 다음 과 같은 몇 가 지 를 언급 했다.
1. 만약 에 기본 적 인 직렬 화 형식 이 적당 한 지 진지 하 게 고려 하지 않 으 면 경솔하게 받 아들 이지 마 세 요.2. 만약 에 한 대상 의 물리 적 표현 법 이 그의 논리 적 내용 과 같다 면 기본 적 인 직렬 화 형식 을 사용 하 는 데 적합 할 수 있 습 니 다.3. 기본 적 인 직렬 화 형식 이 적당 하 다 고 확정 하 더 라 도 보통 readObject 방법 을 제공 하여 제약 관계 와 안전성 을 확보 해 야 합 니 다.4. 한 대상 의 물리 적 표현 법 과 그의 논리 적 데이터 내용 이 실질 적 인 차이 가 있 을 때 기본 적 인 직렬 화 형식 을 사용 하면 다음 과 같은 네 가지 단점 이 있다.
  • 이 클래스 의 내 보 내기 API 를 이러한 내부 표현법 에 영원히 속박 합 니 다
  • 공간 을 너무 많이 소모 합 니 다
  • 너무 많은 시간 을 소모 합 니 다
  • 스 택 넘 침
  • 책 에서 다음 과 같은 코드 를 예 로 들 면:
        // Awful candidate for default serialized form
        public final class StringList implements Serializable {
            private int size = 0;
            private Entry head = null;
    
            private static class Entry implements Serializable {
                String data;
                Entry next;
                Entry previous;
            }
            ...// Remainder omitted
        }

    상기 코드 에서 기본 적 인 직렬 화 방법 을 사용 하면 Entry 를 처리 할 때 한 층 씩 옮 겨 다 니 며 깊이 를 알 수 없 으 면 알 수 없 는 재 귀적 호출 은 결국 스 택 이 넘 칠 수 있 습 니 다.그리고 기본 적 인 직렬 화 방법 은 반복 할 필요 가 없 는 디 테 일 을 많이 처리 하고 공간 소 모 를 낭비 했다.
    Serializable 인터페이스의 클래스 를 실 현 했 습 니 다. readObject 를 통 해 역 직렬 화 할 때 기본 구조 함 수 를 호출 하지 않 습 니 다. 즉, Object InputStream 을 통 해 readObject 를 할 때 새로운 대상 에 대해 다른 초기 화 작업 을 해 야 한다 면 기 존의 구조 함수 에서 초기 화 되 지 않 습 니 다. 특히 성명 과 정 의 를 통 해 분 리 된 final 속성 에 의존 할 수 없습니다.이런 상황 에서 그 값 을 부여 할 수 없다.
    상술 한 설정 은 사실 서열 화의 목적 을 더욱 명확 하 게 했다. 개발 자 는 모든 일 을 한 곳 에 섞 어 처리 해 서 는 안 되 고 서열 화 는 그 전속 기능 을 가진다.
    책 에서 사용자 정의 직렬 화 실현 방안 을 제시 했다.
        // StringList with a reasonable custom serialized form
        public final class StringList implements Serializable {
            private transient int size = 0;
            private transient Entry head = null;
    
            private static class Entry {
                String data;
                Entry next;
                Entry previous;
            }
    
            // Appends the specified string to the list
            public final void add(String s) {...}
            private void writeObject(ObjectOutputStream s) throws IOException {
                s.defaultWriteObject();
                s.writeInt(size);
    
                // Write out all elements in the proper order
                for (Entry e = head; e != null; e = e.next)
                    s.writeObject(e.data);
            }
    
            private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
                s.defaultReadObject();
                int numElements = s.readInt();
    
                // Read in all elements and insert them in list
                for (int i = 0; i < numElements; i++) 
                    add((String) s.readObject());
            }
            ...// Remainder omitted
        }

    책 에 서 는 외부 hack 가 기 존의 직렬 화 대상 을 파괴 하 는 것 을 방지 하기 위해 readObject 를 보호 적 으로 작성 하 는 방법 에 대해 토론 을 많이 하지 않 습 니 다. hack 의 방법 은 바이트 흐름 과 관련 된 문제 이 므 로 을 참고 하 십시오.
    Singleton
    다음 단일 모드 테스트 클래스:
    public class SingletonTest implements Serializable{
        private static final long serialVersionUID = -3764549935511906697L;
    
        public static final SingletonTest INSTANCE = new SingletonTest();
    
        private SingletonTest() {
            System.out.println("[SingletonTest.SingletonTest()] " + this.toString());
        }
    
        public void print() {
            System.out.println("[SingletonTest.print()] " + this.toString());
        }
    }
       public static void main(String[] args) {
    
            System.out.println("Hello World! Current Encoding = " + System.getProperty("file.encoding"));
    
    
            SingletonTest writeSingletonTest = SingletonTest.INSTANCE;
            Tester.SpaceSerial.saveSingletonTest(writeSingletonTest);
    
            SingletonTest readSingletonTest1 = Tester.SpaceSerial.loadSingletonTest();
            System.out.println("LoadSingletonTest1 => " + (readSingletonTest1 == null ? null : readSingletonTest1.toString()));
            SingletonTest readSingletonTest2 = Tester.SpaceSerial.loadSingletonTest();
            System.out.println("LoadSingletonTest2 => " + (readSingletonTest2 == null ? null : readSingletonTest2.toString()));
    
        }

    프로그램 실행 결 과 는:
    Hello World! Current Encoding = UTF-8
    [SingletonTest.SingletonTest()] models.SingletonTest@74a14482
    LoadSingletonTest1 => models.SingletonTest@7ba4f24f
    LoadSingletonTest2 => models.SingletonTest@3b9a45b3

    이 를 통 해 알 수 있 듯 이 반 직렬 화 된 통 로 는 단일 모델 (고정된 추측 가능 모델 이 없 기 때문에) 을 처리 하지 않 았 고 결국은 두 개의 서로 다른 정례 화 대상 이 발생 하여 최종 결과 에 영향 을 주 었 다.이 문 제 를 해결 하려 면 직렬 화 클래스 에서 다음 과 같은 방법 을 실현 하여 실례 의 유일 성 을 확보 해 야 한다.
        private Object readResolve() {
            System.out.println("[SingletonTest.readResolve()] " + this.toString());// + " ObjectInputStream=" + s.toString());
            return INSTANCE;
        }

    서열 화, 반 서열 화 는 내부 실현 통로 가 있 기 때문에 일부 문제 의 원인 을 완전히 이해 하려 면 그 설계 원 리 를 이해 해 야 한다.
    문제 가 발생 했 을 때 공식 문 서 를 조회 하면 복잡 한 코드 에서 답 을 찾 는 것 보다 더 빠 를 수도 있다.
    자바 Object Serialization Specification 의 3.7 절 은 readResolve 방법의 용 도 를 설명 합 니 다.
    3.7 The readResolve Method
    
    For Serializable and Externalizable classes, the readResolve method allows a class to replace/resolve the object read from the stream before it is returned to the caller. By implementing the readResolve method, a class can directly control the types and instances of its own instances being deserialized. The method is defined as follows:
    
       ANY-ACCESS-MODIFIER Object readResolve()
                    throws ObjectStreamException;
    The readResolve method is called when ObjectInputStream has read an object from the stream and is preparing to return it to the caller. ObjectInputStream checks whether the class of the object defines the readResolve method. If the method is defined, the readResolve method is called to allow the object in the stream to designate the object to be returned. The object returned should be of a type that is compatible with all uses. If it is not compatible, a ClassCastException will be thrown when the type mismatch is discovered.
    
    For example, a Symbol class could be created for which only a single instance of each symbol binding existed within a virtual machine. The readResolve method would be implemented to determine if that symbol was already defined and substitute the preexisting equivalent Symbol object to maintain the identity constraint. In this way the uniqueness of Symbol objects can be maintained across serialization.
    
    Note - The readResolve method is not invoked on the object until the object is fully constructed, so any references to this object in its object graph will not be updated to the new object nominated by readResolve. However, during the serialization of an object with the writeReplace method, all references to the original object in the replacement object's object graph are replaced with references to the replacement object. Therefore in cases where an object being serialized nominates a replacement object whose object graph has a reference to the original object, deserialization will result in an incorrect graph of objects. Furthermore, if the reference types of the object being read (nominated by writeReplace) and the original object are not compatible, the construction of the object graph will raise a ClassCastException.

    readResolve 는 매 거 진 유형 이 아 닌 클래스 에 만 사 용 됩 니 다. 아래 설명 에서 이 점 을 입증 합 니 다.
    Process potential substitutions by the class of the object and/or by a subclass of ObjectInputStream:   
    
    a.  If the class of the object is not an enum type and defines the appropriate readResolve method, the method is called to allow the object to replace itself. 
    
    b.  Then if previously enabled by enableResolveObject, the resolveObject method is called to allow subclasses of the stream to examine and replace the object. If the previous step did replace the original object, the resolveObject method is called with the replacement object.        
    
    If a replacement took place, the table of known objects is updated so the replacement object is associated with the handle. The replacement object is then returned from readObject.

    그러나 readResolve 의 존재 로 외부 공격 이 가능 해 졌 다.앞에서 설명 한 바 와 같이 일부 hack 수단 은 바로 이러한 구멍 을 이용 하 는 것 입 니 다.
    'Effective Java' 에 서 는 직렬 화 가능 한 인 스 턴 스 를 제어 할 수 있 는 클래스 를 매 거 진 으로 작성 하면 모든 성명 의 항상 밝 은 것 을 제외 하고 다른 인 스 턴 스 가 없 을 것 이 라 고 말 했다.JVM 은 이에 대해 보장 을 제공 했다.
    책 에서 readResolve 의 접근 성 (accessibility) 을 추가 로 설명 하고 일반적인 사용 규칙 을 정 리 했 습 니 다. 여기 서 참고 하 십시오.
    1. readResolve 방법 을 final 류 에 두 면 개인 적 인 것 이 어야 합 니 다.2. readResolve 방법 을 비 final 뇌 상 에 두 면 방문 성 을 진지 하 게 고려 해 야 합 니 다.
    - 그것 이 사유 라면 어떤 하위 클래스 에 도 적용 되 지 않 는 다. -가방 급 사유 라면 같은 가방 의 하위 클래스 에 만 적 용 됩 니 다. -만약 그것 이 보 호 받 거나 공유 되 는 것 이 라면 탑 을 덮 지 않 은 모든 하위 클래스 에 사용 하 는 것 입 니 다. -readResolve 방법 이 보호 되 거나 공유 되 고 하위 클래스 가 덮어 쓰 지 않 으 면 직렬 화 된 하위 클래스 인 스 턴 스 를 반 직렬 화 하면 초 클래스 인 스 턴 스 가 발생 하여 ClassCastExption 이상 을 초래 할 수 있 습 니 다.
    책 속 의 총화:
    가능 한 한 매 거 진 유형 을 사용 하여 인 스 턴 스 제어 의 제약 조건 을 실시 합 니 다.
    이 를 하지 못 하면 직렬 화 되 고 인 스 턴 스 제어 (intance - controlled) 클래스 가 필요 하 다 면 readResolve 방법 을 제공 하고 이 클래스 의 모든 인 스 턴 스 필드 가 기본 형식 이나 transient 인지 확인 해 야 합 니 다.

    좋은 웹페이지 즐겨찾기