Java Dynamic Proxy Classes

2023. 4. 28. 16:07High Level Programming Language/Reflection

튜토리얼

Introduction

다이나믹 프록시 클래스는 런타임에, 지정된 인터페이스 목록을 구현하는 클래스입니다. 이러한 클래스의 인스턴스를 통해 인터페이스의 메서드를 호출하면 해당 호출이 인코딩되어 다른 객체로 전달됩니다.

[structuring method calls]

이 객체는 일관된 인터페이스를 통해 호출을 처리하게 됩니다. 따라서 다이나믹 프록시 클래스는 컴파일 타임 도구를 사용하지 않고도 인터페이스 목록에 대한 타입 안전한 프록시 객체를 생성하는 데 사용할 수 있습니다.

[type-safe proxy object]

다이나믹 프록시 클래스의 인스턴스에서 메서드가 호출되면, 이 호출은 해당 인스턴스의 Invocation Handler에 있는 단일 메서드[invoke]로 전달되며, 호출된 메서드를 식별하는 java.lang.reflect.Method 객체와 메서드 아규먼트를 포함하는 Object 타입 배열로 인코딩됩니다.

다이나믹 프록시 클래스는 인터페이스 API를 제공하는 객체에 대해 타입 안전한 리플렉티브 디스패치를 제공해야 하는 애플리케이션이나 라이브러리에 유용합니다. 예를 들어, 애플리케이션은 다이나믹 프록시 클래스를 사용하여 다양한 유형의 이벤트를 균일하게 처리하기 위해 여러 임의의 이벤트 리스너 인터페이스(java.util.EventListener를 확장하는 인터페이스)를 구현하는 객체를 생성할 수 있으며, 예를 들어 모든 이벤트를 파일에 로깅[공통 관심사]할 수 있습니다.

Dynamic Proxy Class API

다이나믹 프록시 클래스(이하 프록시 클래스)는 런타임에 생성된 클래스가 구현하는 인터페이스 목록을 구현하는 클래스입니다.

  • 프록시 인터페이스: 프록시 클래스가 구현하는 인터페이스를 가리킵니다.
  • 프록시 인스턴스: 프록시 클래스의 인스턴스를 말합니다.

Creating a Proxy Instance

@CallerSensitive
    public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)

 

Proxy.newProxyInstance 메서드는 지정된 인터페이스에 대한 프록시 인스턴스를 리턴하며, 이 프록시 인스턴스는 메서드 호출을 지정된 InvocationHandler로 전달합니다. 이 메서드를 사용할 때 몇 가지 제약 조건을 준수해야 하며, 이를 위반하면 IllegalArgumentException이 발생할 수 있습니다. 주요 제약 사항은 다음과 같습니다:

  • interfaces 배열에 포함된 모든 Class 객체는 숨겨지지 않고[non-hidden:package-private] 봉인되지 않은[non-sealed] 인터페이스를 나타내야 하며, 클래스나 기본 타입이어서는 안 됩니다[참고].
  • interfaces 배열의 두 엘리먼트는 동일한 Class 객체를 참조할 수 없습니다.
Class<?>[] interfaces = {FirstInterface.class, SecondInterface.class, FirstInterface.class}; // 잘못된 예시

// 수정된 예시
Class<?>[] interfaces = {FirstInterface.class, SecondInterface.class}; // 올바른 예시
  • 모든 인터페이스 타입은 지정된 클래스 로더를 통해 이름으로 접근할 수 있어야 합니다. 즉, 클래스 로더 cl과 각 인터페이스 i에 대해 Class.forName(i.getName(), false, cl) == i가 참이어야 합니다.
  • 지정된 인터페이스의 모든 public 메서드 시그니처와 이들이 상속한 super 인터페이스에서 참조된 모든 타입은 지정된 클래스 로더를 통해 이름으로 접근할 수 있어야 합니다[참고].
  • 모든 non-public 인터페이스는 동일한 패키지와 모듈에 있어야 하며, 해당 클래스 로더에 의해 정의된 모듈에서 모든 인터페이스 타입에 접근할 수 있어야 합니다. 그렇지 않으면 프록시 클래스가 모든 인터페이스를 구현할 수 없습니다.
  • 동일한 시그니처를 가진 인터페이스 메서드의 집합에 대해:
    • 메서드의 리턴 타입이 기본 타입이나 void인 경우, 모든 메서드가 동일한 리턴 타입을 가져야 합니다.
    • 그렇지 않으면, 하나의 메서드 리턴 타입이 나머지 메서드의 리턴 타입에 할당 가능해야 합니다.
  • 생성된 프록시 클래스는 가상 머신이 클래스에 부과하는 제한을 초과해서는 안 됩니다. 예를 들어, VM이 클래스가 구현할 수 있는 인터페이스 수를 65535개로 제한하는 경우, 인터페이스 배열의 크기는 65535를 초과할 수 없습니다.

또한, 지정된 프록시 인터페이스의 순서가 중요합니다. 동일한 인터페이스 조합에 대해 순서가 다르면 두 개의 별개의 프록시 클래스가 생성됩니다.

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

// 첫 번째 인터페이스
interface FirstInterface {
    void firstMethod();
}

// 두 번째 인터페이스
interface SecondInterface {
    void secondMethod();
}

// InvocationHandler 구현
class MyInvocationHandler implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    	System.out.println("MyInvocationHandler:: invoke : class: " + proxy.getClass());
    	
    	if (method.getDeclaringClass().equals(FirstInterface.class)) {
            System.out.println("Invoked method from FirstInterface: " + method.getName());
        } else if (method.getDeclaringClass().equals(SecondInterface.class)) {
            System.out.println("Invoked method from SecondInterface: " + method.getName());
        } else {
            System.out.println("Invoked method from unknown interface: " + method.getName());
        }
        return null;
    }
}

public class ProxyExample {
    public static void main(String[] args) {
        // 클래스 로더
        ClassLoader classLoader = ProxyExample.class.getClassLoader();

        // 프록시 생성 - 인터페이스 순서: FirstInterface, SecondInterface
        Object proxy1 = Proxy.newProxyInstance(
                classLoader,
                new Class<?>[]{FirstInterface.class, SecondInterface.class},
                new MyInvocationHandler()
        );

        // 프록시 생성 - 인터페이스 순서: SecondInterface, FirstInterface
        Object proxy2 = Proxy.newProxyInstance(
                classLoader,
                new Class<?>[]{SecondInterface.class, FirstInterface.class},
                new MyInvocationHandler()
        );

        // 동일한 인터페이스 조합이지만 순서가 다르면 서로 다른 프록시 클래스가 생성됨
        System.out.println("proxy1 class: " + proxy1.getClass());
        System.out.println("proxy2 class: " + proxy2.getClass());
        
        FirstInterface firstProxy1 = (FirstInterface) proxy1;
        firstProxy1.firstMethod();
                
        SecondInterface secondProxy1 = (SecondInterface) proxy1;
        secondProxy1.secondMethod();
        
        FirstInterface firstProxy2 = (FirstInterface) proxy2;
        firstProxy2.firstMethod();
                
        SecondInterface secondProxy2 = (SecondInterface) proxy1;
        secondProxy2.secondMethod();
        
        // 두 프록시 클래스는 다름
        System.out.println("Are proxy1 and proxy2 classes the same? " + (proxy1.getClass() == proxy2.getClass()));
    }
}

 

실행결과는 다음과 같습니다:

proxy1 class: class com.sun.proxy.$Proxy0
proxy2 class: class com.sun.proxy.$Proxy1
Are proxy1 and proxy2 classes the same? false

 

프록시 클래스는 프록시 인터페이스라고 하는 지정된 인터페이스 목록을 구현하는 런타임에 생성된 클래스입니다. 프록시 인스턴스는 프록시 클래스의 인스턴스입니다. 각 프록시 인스턴스에는 연관된 invocation handler 객체가 있으며, 이 객체는 인터페이스 InvocationHandler를 구현합니다. 프록시 인터페이스 중 하나를 통한 프록시 인스턴스의 메서드 호출은 인스턴스의 invocation handler의 invoke 메서드로 전송되어 프록시 인스턴스, 호출된 메서드를 식별하는 java.lang.reflect.Method 객체, 아규먼트를 포함하는 Object 타입의 배열을 전달합니다. invocation handler는 인코딩된 메서드 호출을 적절하게 처리하고 리턴하는 결과는 프록시 인스턴스의 메서드 호출 결과로 리턴됩니다.

[sealed interface]

Proxy Class Properties

프록시 클래스는 다음과 같은 속성을 가집니다:

  • 프록시 클래스의 단순 이름(unqualified name)은 지정되지 않았습니다. 그러나 "$Proxy"로 시작하는 클래스 이름 공간은 프록시 클래스에 예약되어야 합니다.
  • 프록시 클래스가 정의되는 패키지와 모듈은 아래에서 설명됩니다.
  • 프록시 클래스는 final이며, 추상 클래스가 아닙니다.
  • 프록시 클래스는 java.lang.reflect.Proxy를 확장합니다.
  • 프록시 클래스는 생성 시 지정된 인터페이스만을 정확히 동일한 순서로 구현합니다. 클래스 객체의 getInterfaces 메서드를 호출하면 생성 시 지정된 인터페이스 목록이 포함된 배열을 반환하고, getMethods를 호출하면 해당 인터페이스에 포함된 모든 메서드의 Method 객체 배열을 반환합니다. getMethod를 호출하면 프록시 인터페이스에서 예상대로 메서드를 찾습니다.
  • 프록시 클래스의 ProtectionDomainjava.lang.Object와 같은 부트스트랩 클래스 로더에 의해 로드된 시스템 클래스와 동일합니다. 이는 프록시 클래스의 코드가 신뢰할 수 있는 시스템 코드에 의해 생성되기 때문입니다. 이 보호 도메인에는 일반적으로 java.security.AllPermission이 부여됩니다.
  • Proxy.isProxyClass 메서드를 사용하여 주어진 클래스가 프록시 클래스인지 확인할 수 있습니다.

[unqualified name]

Proxy Instance Properties

프록시 인스턴스는 다음과 같은 속성을 가집니다:

  • 프록시 인스턴스 proxy와 그 프록시 클래스가 구현한 인터페이스 중 하나인 Foo가 주어졌을 때, 다음 표현식은 true를 반환합니다:
  • proxy instanceof Foo
  • 또한 다음 캐스트 연산이 성공하며(ClassCastException을 발생시키지 않고):
  • (Foo) proxy
  • 각 프록시 인스턴스에는 생성자에 전달된 Invocation Handler가 연관되어 있습니다. 정적 메서드 Proxy.getInvocationHandler는 전달된 프록시 인스턴스에 연관된 Invocation Handler 를 반환합니다.
  • 프록시 인스턴스의 인터페이스 메서드를 호출하면, 이 호출은 해당 메서드에 대한 문서에 설명된 대로 InvocationHandlerinvoke 메서드로 인코딩되고 전달됩니다.
  • 프록시 인터페이스는 디폴트 메서드를 정의하거나 슈퍼 인터페이스에서 직접 또는 간접적으로 디폴트 메서드를 상속받을 수 있습니다. Invocation Handler는 InvocationHandler::invokeDefault를 호출하여 프록시 인터페이스의 디폴트 메서드를 호출할 수 있습니다.
  • 프록시 인스턴스의 java.lang.Object에 선언된 hashCode, equals, 또는 toString 메서드를 호출하면, 이 호출도 인터페이스 메서드 호출이 인코딩되고 전달되는 방식과 동일하게 InvocationHandlerinvoke 메서드로 인코딩되고 전달됩니다. invoke에 전달된 Method 객체의 선언 클래스는 java.lang.Object입니다. 프록시 클래스에서 상속된 java.lang.Object의 다른 공용 메서드는 오버라이드되지 않으므로, 이러한 메서드의 호출은 java.lang.Object 인스턴스에서 호출될 때와 동일하게 동작합니다.

Package and Module Membership of Proxy Class

프록시 클래스의 패키지 및 모듈 소속은 프록시 인터페이스의 접근성에 따라 결정됩니다. 구체적으로, getProxyClass(ClassLoader, Class[]) 또는 newProxyInstance(ClassLoader, Class[], InvocationHandler) 메서드를 통해 정의된 프록시 클래스의 패키지 및 모듈 소속은 다음과 같이 지정됩니다:

  1. 모든 프록시 인터페이스가 공개(exported) 또는 열린(open) 패키지에 있는 경우:
    • 모든 프록시 인터페이스가 public이라면, 프록시 클래스는 공개된 public 클래스가 되며, 이 클래스는 무조건적으로 공개된(exported) 패키지에 속합니다. 하지만 패키지와 모듈의 이름은 명시되지 않습니다.
    • 프록시 인터페이스 중 적어도 하나가 비공개(non-public)라면, 프록시 클래스는 비공개 클래스가 되며, 해당 비공개 인터페이스가 속한 패키지 및 모듈에 소속됩니다. 모든 비공개 인터페이스는 동일한 패키지 및 모듈에 있어야 하며, 그렇지 않으면 프록시 생성이 불가능합니다.
  2. 프록시 인터페이스 중 적어도 하나가 비공개된(non-exported) 또는 닫힌(non-open) 패키지에 있는 경우:
    • 모든 프록시 인터페이스가 public이라면, 프록시 클래스는 비공개된, 닫힌 패키지에 속한 공개 public 클래스가 됩니다. 이 경우 패키지와 모듈의 이름은 명시되지 않습니다.
    • 프록시 인터페이스 중 적어도 하나가 비공개(non-public)라면, 프록시 클래스는 해당 비공개 인터페이스가 속한 패키지 및 모듈에 속한 비공개 클래스가 됩니다. 이 경우에도 모든 비공개 인터페이스는 동일한 패키지 및 모듈에 있어야 하며, 그렇지 않으면 프록시 생성이 불가능합니다.
  3. 접근성이 혼합된 프록시 인터페이스 (예: 공개된 public 인터페이스와 비공개된 non-public 인터페이스)가 동일한 인스턴스에 의해 프록시되면, 프록시 클래스의 접근성은 가장 접근성이 낮은 프록시 인터페이스에 의해 결정됩니다.

추가로, 임의의 코드가 setAccessible을 통해 공개된 패키지의 프록시 클래스에 접근할 수 있는 경우, 닫힌 패키지에 있는 프록시 클래스는 해당 모듈 외부의 코드에서 접근할 수 없습니다.

이 설명에서 "비공개된 패키지"는 모든 모듈에 대해 공개되지 않은 패키지를 의미하며, "닫힌 패키지"는 모든 모듈에 대해 열리지 않은 패키지를 의미합니다. 구체적으로, 이 용어들은 패키지가 그 모듈에 의해 공개되지 않았거나, 특정 모듈에 대해서만 조건부로 공개된 것을 나타냅니다.

"Exported"와 "Open" 패키지는 Java의 모듈 시스템에서 모듈 간의 접근성을 제어하는 개념입니다. Java 9에서 도입된 모듈 시스템은 모듈 간의 의존성과 접근을 보다 엄격하게 관리할 수 있도록 설계되었습니다.

1. Exported Packages (공개된 패키지)
⦁ 정의: 모듈이 다른 모듈에서 사용할 수 있도록 패키지를 공개할 때 사용하는 개념입니다. 모듈의 module-info.java 파일에서 exports 키워드를 사용하여 패키지를 공개합니다.
⦁ 사용 예:
module my.module {
    exports com.example.mypackage;
}​
위 예시에서는 com.example.mypackage 패키지가 다른 모듈에서 접근 가능하도록 공개되어 있습니다.
⦁ 목적: 특정 패키지의 공개 클래스와 인터페이스가 다른 모듈에서 접근 가능하도록 설정합니다. 이로써 API를 제공할 수 있게 됩니다.

2. Open Packages (열린 패키지)
⦁ 정의: 패키지를 reflection을 통해 다른 모듈에서 접근 가능하도록 하는 개념입니다. 모듈의 module-info.java 파일에서 opens 키워드를 사용하여 패키지를 열 수 있습니다.
⦁ 사용 예:
module my.module {
    opens com.example.mypackage;
}​

 

위 예시에서는 com.example.mypackage 패키지가 다른 모듈에서 리플렉션을 통해 접근 가능하도록 열려 있습니다.
⦁ 목적: 리플렉션을 사용하여 패키지의 클래스, 필드, 메서드 등에 접근할 수 있도록 허용합니다. 이는 테스트, 프레임워크, 또는 런타임에 동적으로 접근해야 하는 경우에 유용합니다.

차이점 요약:
⦁ Exported 패키지는 다른 모듈에서 컴파일 및 런타임에 접근할 수 있는 반면, Open 패키지는 주로 리플렉션을 통해 런타임에 접근할 수 있습니다.
⦁ Exported는 패키지의 클래스와 인터페이스에 대한 일반적인 접근을 허용하고, Open은 리플렉션을 통한 동적 접근을 허용합니다.
모듈 선언 파일 예시:
module my.module {
    exports com.example.exportedpackage;  // 다른 모듈에 공개
    opens com.example.openpackage;        // 리플렉션을 위해 열림
}​

이런 패키지 개념을 통해 Java 모듈 시스템은 코드의 가시성과 접근성을 더 잘 제어할 수 있습니다.

 

Methods Duplicated in Multiple Proxy Interfaces

프록시 클래스의 두 개 이상의 인터페이스에 동일한 이름과 파라미터 시그니처를 가진 메서드가 포함된 경우, 프록시 클래스의 인터페이스 순서는 중요해집니다. 프록시 인스턴스에서 이러한 중복 메서드가 호출되면, Invocation Handler에 전달된 Method 객체는 반드시 호출된 인터페이스의 참조 타입에서 할당 가능한 클래스의 메서드 선언 클래스일 필요는 없습니다. 이 제한 사항은 생성된 프록시 클래스의 해당 메서드 구현이 어떤 인터페이스에서 호출되었는지 결정할 수 없기 때문에 존재합니다. 따라서, 프록시 인스턴스에서 중복 메서드가 호출되면, 해당 메서드를 포함한 첫 번째 인터페이스(직접 포함하거나 슈퍼 인터페이스를 통해 상속받은)의 Method 객체가 호출 핸들러의 invoke 메서드에 전달됩니다.

프록시 인터페이스에 java.lang.ObjecthashCode, equals, 또는 toString 메서드와 동일한 이름과 파라미터 시그니처를 가진 메서드가 포함된 경우, 이러한 메서드가 프록시 인스턴스에서 호출되면, invoke 메서드에 전달된 Method 객체는 java.lang.Object를 선언 클래스으로 가집니다. 즉, java.lang.Object의 공용, 비최종 메서드가 프록시 인터페이스보다 앞서서 Method 객체를 결정하는 기준이 됩니다.

또한, 중복된 메서드가 Invocation Handler로 전달될 때, invoke 메서드는 해당 메서드가 호출될 수 있는 모든 프록시 인터페이스에서 throws 절에 선언된 예외 타입 중 하나에 할당 가능한 체크드 예외 타입만을 던질 수 있습니다. invoke 메서드에서 던져진 체크드 예외가 호출될 수 있는 프록시 인터페이스 중 하나의 메서드에서 선언된 예외 타입에 할당되지 않으면, 프록시 인스턴스의 호출에서 UndeclaredThrowableException이 발생합니다. 이 제한 사항은 invoke 메서드에 전달된 Method 객체를 호출하여 리턴된 예외 타입 중 모든 예외 타입을 성공적으로 던질 수 있는 것은 아니며, 프록시 인터페이스에서 해당 메서드가 호출될 수 있는 예외 타입만 던질 수 있음을 의미합니다.

interface InterfaceA {
    void duplicateMethod();
}

interface InterfaceB {
    void duplicateMethod();
}

class ClassA implements InterfaceA {
    @Override
    public void duplicateMethod() {
        System.out.println("ClassA: duplicateMethod");
    }
}

class ClassB implements InterfaceB {
    @Override
    public void duplicateMethod() {
        System.out.println("ClassB: duplicateMethod");
    }
}

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

class MyInvocationHandler implements InvocationHandler {
    private final Object objA;
    private final Object objB;

    public MyInvocationHandler(Object objA, Object objB) {
        this.objA = objA;
        this.objB = objB;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Invoked method: " + method.getName());

        if (method.getDeclaringClass().isAssignableFrom(InterfaceA.class)) {
            return method.invoke(objA, args);
        } else if (method.getDeclaringClass().isAssignableFrom(InterfaceB.class)) {
            return method.invoke(objB, args);
        }

        return null;
    }
}

import java.lang.reflect.Proxy;

public class DynamicProxyExample {
    public static void main(String[] args) {
        InterfaceA classA = new ClassA();
        InterfaceB classB = new ClassB();

        Object proxyInstance = Proxy.newProxyInstance(
                DynamicProxyExample.class.getClassLoader(),
                new Class<?>[]{InterfaceA.class, InterfaceB.class},
                new MyInvocationHandler(classA, classB)
        );

        ((InterfaceA) proxyInstance).duplicateMethod();  // ClassA: duplicateMethod 출력
        // 프록시 인스턴스에서 중복 메서드가 호출되면, 
        // 해당 메서드를 포함한 첫 번째 인터페이스(직접 포함하거나 슈퍼 인터페이스를 통해 상속받은)의 
        // Method 객체가 호출 핸들러의 invoke 메서드에 전달됩니다.
        ((InterfaceB) proxyInstance).duplicateMethod();  // ClassA: duplicateMethod 출력
    }
}

Serialization

java.lang.reflect.Proxyjava.io.Serializable을 구현하므로, 프록시 인스턴스는 직렬화할 수 있습니다. 그러나 프록시 인스턴스에 java.io.Serializable에 할당할 수 없는 Invocation Handler가 포함되어 있으면, 이러한 인스턴스를 java.io.ObjectOutputStream에 기록하려고 할 때 java.io.NotSerializableException이 발생합니다. 프록시 클래스의 경우, java.io.Externalizable을 구현하면 직렬화와 관련하여 java.io.Serializable을 구현하는 것과 동일한 효과를 갖습니다: Externalizable 인터페이스의 writeExternalreadExternal 메서드는 프록시 인스턴스(또는 Invocation Handler)의 직렬화 과정의 일부로서 절대 호출되지 않습니다. 모든 Class 객체와 마찬가지로, 프록시 클래스의 Class 객체는 항상 직렬화 가능합니다.

프록시 클래스는 직렬화 가능한 필드를 가지지 않으며, serialVersionUID는 0L입니다. 즉, 프록시 클래스의 Class 객체가 java.io.ObjectStreamClass의 정적 lookup 메서드에 전달될 때, 반환된 ObjectStreamClass 인스턴스는 다음 속성을 가집니다:

  • getSerialVersionUID 메서드를 호출하면 0L이 반환됩니다.
  • getFields 메서드를 호출하면 길이가 0인 배열이 반환됩니다.
  • getField 메서드를 String 인수와 함께 호출하면 null이 반환됩니다.

객체 직렬화의 스트림 프로토콜은 TC_PROXYCLASSDESC라는 타입 코드를 지원하며, 이는 스트림 형식의 문법에서 종료 기호입니다. 이 타입과 값은 java.io.ObjectStreamConstants 인터페이스의 다음 상수 필드에 의해 정의됩니다:

final static byte TC_PROXYCLASSDESC = (byte)0x7D;

 

문법에는 원래의 newClassDesc 규칙에 대한 대체 확장으로 다음 두 가지 규칙이 포함됩니다:

newClassDesc:
    TC_PROXYCLASSDESC newHandle proxyClassDescInfo

proxyClassDescInfo:
    (int)<count> proxyInterfaceName[count] classAnnotation superClassDesc

proxyInterfaceName:
    (utf)

 

ObjectOutputStream이 프록시 클래스의 클래스 설명자를 직렬화할 때, 이는 Proxy.isProxyClass 메서드를 사용하여 해당 클래스가 프록시 클래스인지 확인하고, 위 규칙에 따라 TC_PROXYCLASSDESC 타입 코드를 사용합니다. proxyClassDescInfo의 확장에서, proxyInterfaceName 항목의 시퀀스는 Class 객체의 getInterfaces 메서드를 호출하여 반환된 인터페이스 이름들로 구성되며, 이 인터페이스들은 지정된 순서대로 나열됩니다. classAnnotationsuperClassDesc 항목은 classDescInfo 규칙에서와 동일한 의미를 가집니다. 프록

시 클래스의 경우, superClassDesc는 해당 슈퍼클래스 java.lang.reflect.Proxy의 클래스 설명자입니다. 이 설명자를 포함하면 프록시 인스턴스에 대한 클래스 Proxy의 직렬화 표현의 진화를 지원할 수 있습니다.

프록시가 아닌 클래스의 경우, ObjectOutputStream은 보호된 annotateClass 메서드를 호출하여 특정 클래스에 대한 스트림에 사용자 지정 데이터를 기록할 수 있도록 허용합니다. 프록시 클래스의 경우, annotateClass 대신 java.io.ObjectOutputStream의 다음 메서드가 프록시 클래스의 Class 객체와 함께 호출됩니다:

protected void annotateProxyClass(Class cl) throws IOException;

ObjectOutputStream의 기본 annotateProxyClass 구현은 아무 작업도 수행하지 않습니다.

ObjectInputStreamTC_PROXYCLASSDESC 타입 코드를 만나면, 프록시 클래스의 클래스 설명자를 위에 설명된 형식으로 스트림에서 역직렬화합니다. 클래스 설명자의 Class 객체를 해결하기 위해 resolveClass 메서드를 호출하는 대신, java.io.ObjectInputStream의 다음 메서드가 호출됩니다:

protected Class resolveProxyClass(String[] interfaces)
    throws IOException, ClassNotFoundException;

역직렬화된 프록시 클래스 설명자에서 인터페이스 이름 목록이 resolveProxyClassinterfaces 인수로 전달됩니다.

ObjectInputStream의 기본 resolveProxyClass 구현은 interfaces 매개변수에서 이름이 지정된 인터페이스에 대한 Class 객체 목록을 사용하여 Proxy.getProxyClass를 호출한 결과를 반환합니다. 인터페이스 이름 i에 대해 사용된 Class 객체는 다음을 호출하여 반환된 값입니다:

Class.forName(i, false, loader)

여기서 loader는 실행 스택에서 첫 번째로 null이 아닌 클래스 로더 또는 실행 스택에 null이 아닌 클래스 로더가 없는 경우 null입니다. 이는 resolveClass 메서드의 기본 동작에서 선택된 클래스 로더와 동일한 클래스 로더 선택입니다. 동일한 loader 값은 Proxy.getProxyClass에 전달되는 클래스 로더이기도 합니다. Proxy.getProxyClassIllegalArgumentException을 던지면, resolveClassIllegalArgumentException을 포함한 ClassNotFoundException을 던집니다.

프록시 클래스는 자체 직렬화 가능한 필드를 가지지 않으므로, 프록시 인스턴스의 스트림 표현의 classdata[]는 해당 슈퍼클래스 java.lang.reflect.Proxy의 인스턴스 데이터로만 구성됩니다. Proxy는 프록시 인스턴스의 호출 핸들러인 h라는 직렬화 가능한 필드를 하나 가집니다.

Examples

다음은 임의의 인터페이스 목록을 구현하는 객체의 메서드 호출 전후에 메시지를 출력하는 간단한 예시입니다:

package com.intheeast.jdkproxy;

public interface HelloWorld {
    
    void sayHello();

    void sayGoodbye();

    void greet(String name);

    void setLanguage(String language);

    String getGreeting();
}

/////////////////////////////////////////////////////////////
package com.intheeast.jdkproxy;

public class HelloWorldImpl implements HelloWorld {
    private String language = "English";
    private String greeting = "Hello";

    @Override
    public void sayHello() {
        System.out.println(greeting + ", world!");
    }

    @Override
    public void sayGoodbye() {
        System.out.println("Goodbye, world!");
    }

    @Override
    public void greet(String name) {
        System.out.println(greeting + ", " + name + "!");
    }

    @Override
    public void setLanguage(String language) {
        this.language = language;
        if ("English".equalsIgnoreCase(language)) {
            this.greeting = "Hello";
        } else if ("Spanish".equalsIgnoreCase(language)) {
            this.greeting = "Hola";
        } else if ("French".equalsIgnoreCase(language)) {
            this.greeting = "Bonjour";
        } else {
            this.greeting = "Hello"; 
        }
    }

    @Override
    public String getGreeting() {
        return greeting;
    }
}

///////////////////////////////////////////////////////
package com.intheeast.jdkproxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class MyInvocationHandler implements InvocationHandler {
    private Object target;

    public MyInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, 
    		Method method, 
    		Object[] args) throws Throwable {
    	
        System.out.println("Before method: " + method.getName());
        
        Object result = method.invoke(target, args);
        
        System.out.println("After method: " + method.getName());
        
        return result;
    }
}

///////////////////////////////////////////////////////////////////
package com.intheeast.jdkproxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;

public class Program {
	
	public static void callPrv17VCreationProxyAPI() throws Exception {
		// Step 1: Get the class loader of the target object
        ClassLoader classLoader = HelloWorldImpl.class.getClassLoader();

        // Step 2: Get the proxy class using Proxy.getProxyClass
        Class<?> proxyClass = Proxy.getProxyClass(classLoader, HelloWorld.class);

        // Step 3: Create an instance of the proxy class
        HelloWorld proxyInstance = (HelloWorld) proxyClass
            .getConstructor(InvocationHandler.class)
            .newInstance(new MyInvocationHandler(new HelloWorldImpl()));

        // Step 4: Use the proxy instance
        proxyInstance.sayHello();  // This will invoke the handler's invoke method
	}

	public static void main(String[] args) throws Exception{
		// Step 1: Use Proxy.newProxyInstance to create the proxy instance directly
        HelloWorld proxyInstance = (HelloWorld) Proxy.newProxyInstance(
            HelloWorldImpl.class.getClassLoader(),
            new Class<?>[]{HelloWorld.class},
            new MyInvocationHandler(new HelloWorldImpl())
        );
        
        // Step 2: Use the proxy instance
        proxyInstance.sayHello();  // This will invoke the handler's invoke method
        
        proxyInstance.sayGoodbye();         // Goodbye, world!
        proxyInstance.greet("Alice");       // Hello, Alice!
        
        proxyInstance.setLanguage("Spanish");
        proxyInstance.greet("Bob");         // Hola, Bob!

        String currentGreeting = proxyInstance.getGreeting();
        System.out.println("Current greeting: " + currentGreeting);  // Hola
		
	}

}

 

또 다른 예시입니다.

public class BazException extends Exception {
    public BazException() {
        super();
    }

    public BazException(String message) {
        super(message);
    }

    public BazException(String message, Throwable cause) {
        super(message, cause);
    }

    public BazException(Throwable cause) {
        super(cause);
    }
}
public interface Foo {
    Object bar(Object obj) throws BazException;
}

public class FooImpl implements Foo {
    Object bar(Object obj) throws BazException {
        if (obj == null) {
            throw new BazException("Input object cannot be null.");
        }

        // 추가 로직을 이곳에 작성할 수 있습니다.
        // 예를 들어, obj의 특정 작업을 수행할 수 있습니다.

        System.out.println("FooImpl의 bar 메서드입니다");
        return obj; // 또는 실제 작업 결과를 반환할 수 있습니다.
    }
}

public class DebugProxy implements java.lang.reflect.InvocationHandler {

    private Object obj;

    public static Object newInstance(Object obj) {
        return java.lang.reflect.Proxy.newProxyInstance(
            obj.getClass().getClassLoader(),
            obj.getClass().getInterfaces(),
            new DebugProxy(obj));
    }

    private DebugProxy(Object obj) {
        this.obj = obj;
    }

    public Object invoke(Object proxy, Method m, Object[] args)
        throws Throwable
    {
        Object result;
        try {
            System.out.println("before method " + m.getName());
            result = m.invoke(obj, args);
        } catch (InvocationTargetException e) {
            throw e.getTargetException();
        } catch (Exception e) {
            throw new RuntimeException("unexpected invocation exception: " +
                                       e.getMessage());
        } finally {
            System.out.println("after method " + m.getName());
        }
        return result;
    }
}

 

Foo 인터페이스 구현체를 위한 DebugProxy를 구성하고, 해당 메서드 중 하나를 호출하는 방법은 다음과 같습니다:

Foo foo = (Foo) DebugProxy.newInstance(new FooImpl());
foo.bar(null);

 

다음은 상속받은 java.lang.Object 메서드에 대한 기본 프록시 동작을 제공하고, 프록시 메서드 호출을 해당 메서드의 인터페이스에 따라 개별 객체에 위임하는 유틸리티 호출 핸들러 클래스의 예시입니다:

import java.lang.reflect.*;

public class Delegator implements InvocationHandler {

    // java.lang.Object 메서드에 대한 사전 로드된 Method 객체
    private static Method hashCodeMethod;
    private static Method equalsMethod;
    private static Method toStringMethod;
    static {
        try {
            hashCodeMethod = Object.class.getMethod("hashCode", null);
            equalsMethod =
                Object.class.getMethod("equals", new Class[] { Object.class });
            toStringMethod = Object.class.getMethod("toString", null);
        } catch (NoSuchMethodException e) {
            throw new NoSuchMethodError(e.getMessage());
        }
    }

    private Class[] interfaces;
    private Object[] delegates;

    public Delegator(Class[] interfaces, Object[] delegates) {
        this.interfaces = (Class[]) interfaces.clone();
        this.delegates = (Object[]) delegates.clone();
    }

    public Object invoke(Object proxy, Method m, Object[] args)
        throws Throwable
    {
        Class declaringClass = m.getDeclaringClass();

        if (declaringClass == Object.class) {
            if (m.equals(hashCodeMethod)) {
                return proxyHashCode(proxy);
            } else if (m.equals(equalsMethod)) {
                return proxyEquals(proxy, args[0]);
            } else if (m.equals(toStringMethod)) {
                return proxyToString(proxy);
            } else {
                throw new InternalError(
                    "unexpected Object method dispatched: " + m);
            }
        } else {
            for (int i = 0; i < interfaces.length; i++) {
                if (declaringClass.isAssignableFrom(interfaces[i])) {
                    try {
                        return m.invoke(delegates[i], args);
                    } catch (InvocationTargetException e) {
                        throw e.getTargetException();
                    }
                }
            }

            return invokeNotDelegated(proxy, m, args);
        }
    }

    protected Object invokeNotDelegated(Object proxy, Method m,
                                        Object[] args)
        throws Throwable
    {
        throw new InternalError("unexpected method dispatched: " + m);
    }

    protected Integer proxyHashCode(Object proxy) {
        return new Integer(System.identityHashCode(proxy));
    }

    protected Boolean proxyEquals(Object proxy, Object other) {
        return (proxy == other ? Boolean.TRUE : Boolean.FALSE);
    }

    protected String proxyToString(Object proxy) {
        return proxy.getClass().getName() + '@' +
            Integer.toHexString(proxy.hashCode());
    }
}

 

Delegator의 서브클래스는 invokeNotDelegated를 오버라이드하여 다른 객체에 직접 위임되지 않는 프록시 메서드 호출의 동작을 구현할 수 있으며, proxyHashCode, proxyEquals, 및 proxyToString을 오버라이드하여 java.lang.Object에서 상속받은 메서드의 기본 동작을 오버라이드할 수 있습니다.

Foo 인터페이스 구현체를 위한 Delegator를 구성하려면 다음과 같이 합니다:

Class[] proxyInterfaces = new Class[] { Foo.class };
Foo foo = (Foo) Proxy.newProxyInstance(Foo.class.getClassLoader(),
    proxyInterfaces,
    new Delegator(proxyInterfaces, new Object[] { new FooImpl() }));

 

Delegator 클래스 구현은 최적화보다는 설명에 중점을 두고 있으며, 예를 들어 hashCode, equals, 및 toString 메서드의 Method 객체를 캐싱하고 비교하는 대신, 해당 메서드 이름을 문자열로 비교할 수 있습니다. 이러한 메서드 이름은 java.lang.Object에서 오버로드되지 않기 때문입니다.

'High Level Programming Language > Reflection' 카테고리의 다른 글

클래스 로딩과 관련된 제약 사항  (0) 2023.05.19
Proxy Target Class 조건  (0) 2023.05.17
sealed interface  (0) 2023.05.08
Structuring method calls  (0) 2023.05.02
type-safe proxy object  (0) 2023.05.02