본문 바로가기

기술스택/ASM - [Bytecode]

[A Java bytecode engineering library] - [Core API] 2. Classes[2/2]

목차

    2. 클래스[2/2]

    2.2.4. 클래스 변환( Transforming classes)

    지금까지 ClassReader 및 ClassWriter 구성 요소는 독립적으로 사용되었다. 이벤트는 "수동으로" 생성되어 ClassWriter에 직접 소비되거나, 대칭적으로 ClassReader에 의해 생성되어 "수동으로", 즉 사용자 정의 ClassVisitor 구현에 의해 소비되었다. 이러한 구성 요소가 함께 사용될 때 진정한 흥미가 시작된다. 첫 번째 단계는 ClassReader에서 생성된 이벤트를 ClassWriter로 직접 전달하는 것이다. 결과는 클래스 리더에 의해 파싱된 클래스가 클래스 작성자에 의해 재구성된다는 것이다:

     

     

    이 자체로는 그다지 흥미롭지 않다(바이트 배열을 복사하는 더 쉬운 방법이 있다!), 하지만 기다려라.

    다음 단계는 클래스 리더와 클래스 작성자 사이에 ClassVisitor를 도입하는 것이다:

     

     

    위 코드에 해당하는 아키텍처는 그림 2.6에 나타나 있으며, 여기서 구성 요소는 사각형으로, 이벤트는 화살표로 표시된다(시퀀스 다이어그램과 같은 수직 타임라인으로).

     

    Figure 2.6.: A transformation chain

     

    결과는 변하지 않는다. 그러나 ClassVisitor 이벤트 필터는 아무 것도 필터링하지 않기 때문이다.

    그러나 이제 일부 이벤트를 필터링하기 위해 몇 가지 메소드를 재정의함으로써 클래스를 변환할 수 있게 된다. 예를 들어, 다음과 같은 ClassVisitor 하위 클래스를 고려해 보자:

     

     

    이 클래스는 ClassVisitor 클래스의 하나의 메소드만을 재정의한다.

    결과적으로 생성자에 전달된 클래스 방문자 cv에 대한 모든 호출은 변경되지 않은 채로 전달되며, visit 메소드에 대한 호출만 수정된 클래스 버전 번호로 전달된다. 해당 시퀀스 다이어그램은 그림 2.7에 표시된다.

     

    Figure 2.7.: Sequence diagram for the ChangeVersionAdapter

     

    visit 메소드의 다른 인수를 수정함으로써 클래스 버전을 변경하는 것 외에 다른 변환을 구현할 수 있다.

    예를 들어, 구현된 인터페이스 목록에 인터페이스를 추가할 수 있다. 클래스의 이름을 변경하는 것도 가능하지만, 이를 위해서는 visit 메소드에서 이름 인수를 변경하는 것보다 훨씬 더 많은 것이 필요하다. 실제로 클래스 이름은 컴파일된 클래스 내부의 많은 다른 장소에 나타날 수 있으며, 이러한 모든 발생을 변경하여 클래스의 이름을 실제로 변경해야 한다.

     

    최적화(Optimization)

     

    (이전 변환 코드)

     

    이전 변환은 원래 클래스에서 단지 네 바이트만 변경한다. 그러나 위 코드에서는 b1이 완전히 파싱되고 해당 이벤트가 b2를 처음부터 구성하는 데 사용된다는 것이 매우 비효율적이다. b1의 변환되지 않은 부분을 직접 b2로 복사하여, 이러한 부분을 파싱하고 해당 이벤트를 생성하지 않는 것이 훨씬 더 효율적일 것이다. ASM은 메소드에 대해 이 최적화를 자동으로 수행한다:

     

    • ClassReader 구성 요소가 accept 메소드에 전달된 ClassVisitor에서 반환된 MethodVisitor가 ClassWriter에서 온 것임을 감지하면, 이는 이 메소드의 내용이 변환되지 않고 실제로는 애플리케이션에서 볼 수 없다는 것을 의미한다.

    • 이 경우 ClassReader 구성 요소는 이 메소드의 내용을 파싱하지 않고, 해당 이벤트를 생성하지 않으며, 단지 이 메소드의 바이트 배열 표현을 ClassWriter에 복사한다. 이 최적화는 ClassReader 및 ClassWriter 구성 요소가 서로 참조를 가지고 있을 때 수행되며, 다음과 같이 설정할 수 있다:

     

     

    이 최적화 덕분에 위 코드는 ChangeVersionAdapter가 어떤 메소드도 변환하지 않기 때문에 이전 코드보다 두 배 빠르다. 일부 또는 모든 메소드를 변환하는 일반적인 클래스 변환의 경우 속도 향상은 더 작지만 여전히 눈에 띄며, 실제로 10~20% 정도이다.

    불행히도 이 최적화는 원래 클래스에 정의된 모든 상수를 변환된 클래스로 복사해야 한다. 이는 필드, 메소드 또는 명령어를 추가하는 변환의 경우 문제가 되지 않지만, 많은 클래스 요소를 제거하거나 이름을 변경하는 변환의 경우 최적화되지 않은 경우에 비해 더 큰 클래스 파일을 생성하기 때문에 문제가 된다.

    따라서 이 최적화는 "추가적인" 변환에만 사용하는 것이 권장된다.

     

    변환된 클래스 사용하기 (Using transformed classes)

     

    변환된 클래스 b2는 디스크에 저장되거나 이전 섹션에서 설명한 대로 ClassLoader를 사용하여 로드될 수 있다. 그러나 ClassLoader 내부에서 수행되는 클래스 변환은 이 클래스 로더에 의해 로드된 클래스만 변환할 수 있다. 모든 클래스를 변환하고자 한다면, 변환을 java.lang.instrument 패키지에 정의된 ClassFileTransformer 내부에 넣어야 한다(이 패키지에 대한 자세한 내용은 해당 문서를 참조하라):

     

     

     

    2.2.5. 클래스 멤버 제거

     

    이전 섹션에서 클래스 버전을 변환하는 데 사용된 방법은 물론 ClassVisitor 클래스의 다른 메소드에도 적용될 수 있다.

    예를 들어, visitField 및 visitMethod 메소드에서 access 또는 name 인수를 변경함으로써 필드 또는 메소드의 수정자 또는 이름을 변경할 수 있다. 또한 수정된 인수로 메소드 호출을 전달하는 대신, 이 호출을 전혀 전달하지 않을 수 있다. 그 결과 해당 클래스 요소가 제거된다.

    예를 들어, 다음 클래스 어댑터는 외부 및 내부 클래스에 대한 정보와 클래스가 컴파일된 소스 파일의 이름을 제거한다(결과 클래스는 이러한 요소가 디버깅 목적으로만 사용되기 때문에 완전히 기능적으로 남아 있다). 이는 적절한 방문 메소드에서 아무 것도 전달하지 않음으로써 수행된다:

     

     

    이 전략은 필드와 메소드에는 적용되지 않는데, visitField 및 visitMethod 메소드는 결과를 반환해야 하기 때문이다.

    필드나 메소드를 제거하려면, 메소드 호출을 전달하지 않고 호출자에게 null을 반환해야 한다. 예를 들어, 다음 클래스 어댑터는 이름과 설명자에 의해 지정된 단일 메소드를 제거한다(이름만으로는 메소드를 식별하기에 충분하지 않다. 클래스에는 같은 이름이지만 다른 매개변수를 가진 여러 메소드가 포함될 수 있다):

     

    public class RemoveMethodAdapter extends ClassVisitor {
        private String mName;
        private String mDesc;
    
        public RemoveMethodAdapter(
                ClassVisitor cv, String mName, String mDesc) {
            super(ASM4, cv);
            this.mName = mName;
            this.mDesc = mDesc;
        }
    
        @Override
        public MethodVisitor visitMethod(int access, String name,
                                         String desc, String signature, String[] exceptions) {
            if (name.equals(mName) && desc.equals(mDesc)) {
    // do not delegate to next visitor -> this removes the method
                return null;
            }
            return cv.visitMethod(access, name, desc, signature, exceptions);
        }
    }

     

     

    2.2.6. 클래스 멤버 추가

    받는 호출보다 적은 호출을 전달하는 대신, 더 많은 호출을 "전달"함으로써 클래스 요소를 추가할 수 있다. 새로운 호출은 원래 메소드 호출 사이의 여러 위치에 삽입될 수 있으며, 다양한 visitXxx 메소드가 호출되어야 하는 순서(섹션 2.2.1 참조)가 지켜져야 한다.

     

    예를 들어, 클래스에 필드를 추가하려면 원래 메소드 호출 사이에 visitField에 대한 새로운 호출을 삽입해야 하며, 이 새로운 호출은 클래스 어댑터의 visit 메소드 중 하나에 위치해야 한다. 예를 들어, visit 메소드에서 이 작업을 수행할 수 없는데, 이는 visitField 다음에 visitSource, visitOuterClass, visitAnnotation 또는 visitAttribute가 호출될 수 있기 때문이다. visitSource, visitOuterClass, visitAnnotation 또는 visitAttribute 메소드에 이 새로운 호출을 넣을 수도 없다. 유일한 가능성은 visitInnerClass, visitField, visitMethod 또는 visitEnd 메소드이다.

     

    (명시적 조건을 추가하지 않는 한) 새로운 호출을 visitEnd 메소드에 넣으면 필드가 항상 추가된다.

    이 메소드는 항상 호출되기 때문이다. visitField 또는 visitMethod에 넣으면 여러 필드가 추가된다: 원래 클래스의 필드 또는 메소드당 하나씩. 두 솔루션 모두 의미가 있을 수 있다; 필요한 것에 따라 다르다. 예를 들어, 객체의 호출 횟수를 세기 위한 단일 카운터 필드를 추가하거나, 각 메소드의 호출을 개별적으로 세기 위해 메소드당 하나의 카운터를 추가할 수 있다.

     

    Note: 실제로 유일하게 정확한 해결책은 visitEnd 메소드에서 추가 호출을 하여 새로운 멤버를 추가하는 것이다.

    클래스에는 중복된 멤버가 포함되어서는 안 되며, 새로운 멤버가 고유하다는 것을 확실히 하려면 기존 멤버와 비교해야 하며, 이는 모든 멤버를 방문한 후, 즉 visitEnd 메소드에서만 수행할 수 있다.

    는 상당히 제한적이다. _counter$ 또는 _4B7F_와 같은 프로그래머가 사용하기 어려운 생성된 이름을 사용하는 것은 visitEnd에서 추가하지 않고도 중복 멤버를 피하는 데 충분하다. 첫 장에서 논의한 바와 같이, 트리 API는 이러한 제한이 없다: 이 API를 사용하여 변환 중에 언제든지 새로운 멤버를 추가할 수 있다.

     

    위 논의를 설명하기 위해, 클래스에 필드를 추가하는 클래스 어댑터가 여기 있다:

     

     

    이 필드는 visitEnd 메소드에서 추가된다. visitField 메소드는 기존 필드를 수정하거나 필드를 제거하는 데 재정의되지 않았지만, 추가하려는 필드가 이미 존재하는지 여부를 감지하기 위해 사용된다. visitEnd 메소드에서 fv.visitEnd()를 호출하기 전에 fv != null 테스트가 있는 것에 주목하라. 이는 앞 섹션에서 보았듯이 클래스 방문자가 visitField에서 null을 반환할 수 있기 때문이다.

     

     

    2.2.7. (변환 체인 Transformation chains)

     

    지금까지 ClassReader, 클래스 어댑터, ClassWriter로 구성된 간단한 변환 체인을 보았다. 여러 클래스 어댑터를 연쇄적으로 사용하여 복잡한 변환을 수행하기 위해 여러 개별 클래스 변환을 조합할 수 있다는 것을 알아야 한다. 변환 체인은 반드시 선형일 필요는 없다. 모든 메소드 호출을 동시에 여러 ClassVisitor에 전달하는 ClassVisitor를 작성할 수 있다:

     

     

    대칭적으로 여러 클래스 어댑터가 동일한 ClassVisitor에 위임할 수 있다(이는 예를 들어 visit 및 visitEnd 메소드가 이 ClassVisitor에서 정확히 한 번씩 호출되는지 확인하기 위해 일부 주의가 필요하다). 따라서 그림 2.8에 표시된 변환 체인과 같은 것은 완전히 가능하다.

     

    2.3. 도구(Tools)

     

    ClassVisitor 클래스와 관련된 ClassReader 및 ClassWriter 구성 요소 외에도, ASM은 클래스 생성기 또는 어댑터의 개발 중에 유용할 수 있는 여러 도구를 org.objectweb.asm.util 패키지에서 제공하며, 런타임에는 필요하지 않다. ASM은 또한 런타임에 내부 이름, 타입 설명자 및 메소드 설명자를 조작하기 위한 유틸리티 클래스를 제공한다. 이러한 모든 도구는 아래에 소개된다.

     

    Figure 2.8.: A complex transformation chain

     

     

     

    2.3.1. Type

    이전 섹션에서 본 것처럼, ASM API는 Java 타입을 컴파일된 클래스에 저장된 형태, 즉 내부 이름이나 타입 설명자로 노출한다. 소스 코드에서 나타나는 대로 타입을 노출할 수도 있지만, 이렇게 하면 ClassReader와 ClassWriter에서 두 표현 사이의 체계적인 변환을 필요로 하며, 이는 성능을 저하시킬 것이다. 이것이 ASM이 내부 이름과 타입 설명자를 그들의 소스 코드 형태로 투명하게 변환하지 않는 이유이다. 그러나 필요할 때 수동으로 변환하기 위해 Type 클래스를 제공한다.

     

    Type 객체는 Java 타입을 나타내며, 타입 설명자나 Class 객체에서 생성될 수 있다.

    Type 클래스에는 기본 타입을 나타내는 정적 변수도 포함되어 있다. 예를 들어, Type.INT_TYPE은 int 타입을 나타내는 Type 객체이다.

     

    getInternalName 메소드는 Type의 내부 이름을 반환한다.

    예를 들어, Type.getType(String.class).getInternalName()은 String 클래스의 내부 이름, 즉 "java/lang/String"을 제공한다. 이 메소드는 클래스 또는 인터페이스 타입에 대해서만 사용해야 한다.

     

    getDescriptor 메소드는 Type의 설명자를 반환한다.

    따라서 예를 들어, "Ljava/lang/String;" 대신에 코드에서 Type.getType(String.class).getDescriptor()를 사용할 수 있다.

    또는 I 대신에 Type.INT_TYPE.getDescriptor()를 사용할 수 있다.

     

    Type 객체는 메소드 타입도 나타낼 수 있다.

    이러한 객체는 메소드 설명자나 Method 객체에서 생성될 수 있다. getDescriptor 메소드는 이 타입에 해당하는 메소드 설명자를 반환한다. 또한, getArgumentTypes 및 getReturnType 메소드는 메소드의 인수 타입과 반환 타입에 해당하는 Type 객체를 얻는 데 사용될 수 있다. 예를 들어, Type.getArgumentTypes("(I)V")는 Type.INT_TYPE 요소를 포함하는 배열을 반환한다. 비슷하게, Type.getReturnType("(I)V") 호출은 Type.VOID_TYPE 객체를 반환한다.

     

    2.3.2. TraceClassVisitor

     

    생성된 또는 변환된 클래스가 기대하는 대로 준수하는지 확인하기 위해 ClassWriter에서 반환하는 바이트 배열은 사람이 읽을 수 없기 때문에 도움이 되지 않는다. 텍스트 표현이 훨씬 더 사용하기 쉬울 것이다. 이것이 TraceClassVisitor 클래스가 제공하는 것이다. 이 클래스의 이름에서 알 수 있듯이, ClassVisitor 클래스를 확장하고 방문한 클래스의 텍스트 표현을 구축한다. 따라서 클래스를 생성하기 위해 ClassWriter를 사용하는 대신 TraceClassVisitor를 사용하여 실제로 생성된 것의 읽을 수 있는 추적을 얻을 수 있다. 또는, 더 좋은 방법은 둘을 동시에 사용하는 것이다. 실제로 TraceClassVisitor는 기본 동작 외에도 다른 방문자, 예를 들어 ClassWriter에 대한 메소드 호출을 모두 위임할 수 있다:

     

     

     

    이 코드는 cw에게 받은 모든 호출을 위임하고 printWriter에 이러한 호출의 텍스트 표현을 출력하는 TraceClassVisitor를 생성한다. 예를 들어, 섹션 2.2.3의 예제에서 TraceClassVisitor를 사용하면 다음과 같다:

     

     

    TraceClassVisitor는 생성 또는 변환 체인의 어느 지점에서나 사용할 수 있으며, ClassWriter 바로 앞에서만 사용하는 것은 아니다. 체인의 이 지점에서 무엇이 일어나고 있는지 보기 위해서다. 또한 이 어댑터에 의해 생성된 클래스의 텍스트 표현은 클래스를 쉽게 비교할 수 있도록 String.equals()를 사용할 수 있다.

     

    2.3.3. CheckClassAdapter

    ClassWriter 클래스는 메소드가 적절한 순서로 호출되고 유효한 인수로 호출되는지 확인하지 않는다. 따라서 Java 가상 머신 검증기에서 거부될 수 있는 잘못된 클래스를 생성할 수 있다.

     

    이러한 오류를 가능한 한 빨리 감지하기 위해 CheckClassAdapter 클래스를 사용할 수 있다. TraceClassVisitor와 마찬가지로, 이 클래스는 ClassVisitor 클래스를 확장하고 다른 ClassVisitor에게 메소드 호출을 모두 위임한다. (예를 들어, TraceClassVisitor 또는 ClassWriter.)

     

    그러나 방문한 클래스의 텍스트 표현을 인쇄하는 대신, 이 클래스는 다음 방문자에게 위임하기 전에 메소드가 적절한 순서로 호출되고 유효한 인수로 호출되는지 확인한다.

     

    오류가 발생하면 IllegalStateException 또는 IllegalArgumentException이 발생한다.

    클래스를 확인하고, 이 클래스의 텍스트 표현을 인쇄하고, 마지막으로 바이트 배열 표현을 생성하려면 다음과 같이 사용해야 한다:

     

     

    이러한 클래스 방문자를 다른 순서로 연결하면 수행하는 작업도 다른 순서로 수행된다. 예를 들어, 다음 코드를 사용하면 추적이 검사 후에 이루어진다:

     

     

    TraceClassVisitor와 마찬가지로, 생성 또는 변환 체인의 어느 지점에서나 CheckClassAdapter를 사용할 수 있으며, ClassWriter 바로 앞에서만 사용하는 것은 아니다. 체인의 이 지점에서 클래스를 확인하기 위해서다.

     

    2.3.4. ASMifier

    이 클래스는 TraceClassVisitor 도구에 대한 대안 백엔드를 제공한다(기본적으로 Textifier 백엔드를 사용하며, 위에서 보여준 종류의 출력을 생성한다).

    이 백엔드는 TraceClassVisitor 클래스의 각 메소드가 호출되었을 때 사용되었던 Java 코드를 출력하도록 만든다.

    예를 들어, visitEnd() 메소드를 호출하면 cv.visitEnd();가 출력된다.

     

    결과적으로, ASMifier 백엔드를 사용하여 클래스를 방문하는 TraceClassVisitor 방문자는 ASM을 사용하여 이 클래스를 생성하는 소스 코드를 출력한다. 이 방문자를 이미 존재하는 클래스를 방문하는 데 사용하면 유용하다.

    예를 들어, 어떤 컴파일된 클래스를 ASM으로 생성하는 방법을 모르는 경우, 해당 소스 코드를 작성하고 javac로 컴파일하고 ASMifier를 사용하여 컴파일된 클래스를 방문하면 된다. 그러면 ASM 코드를 얻을 수 있다!

     

    ASMifier 클래스는 명령줄에서 사용할 수 있다. 예를 들어, 다음을 사용하면:

     

     

    들여쓰기 후에 다음과 같은 코드가 생성된다:

     

     

    Reference

     

    https://asm.ow2.io/asm4-guide.pdf

     

    ASM USER GUIDE

     

    Copyright c 2007, 2011 Eric Bruneton All rights reserved. Redistribution and use in source (LYX format) and compiled forms (LATEX, PDF, PostScript, HTML, RTF, etc), with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code (LYX format) must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in compiled form (converted to LATEX, PDF, PostScript, HTML, RTF, and other formats) must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. The name of the author may not be used to endorse or promote products derived from this documentation without specific prior written permission.

     

    THIS DOCUMENTATION IS PROVIDED BY THE AUTHOR “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.