2023. 4. 28. 16:07ㆍHigh Level Programming Language/Reflection
튜토리얼
Introduction
다이나믹 프록시 클래스는 런타임에, 지정된 인터페이스 목록을 구현하는 클래스입니다. 이러한 클래스의 인스턴스를 통해 인터페이스의 메서드를 호출하면 해당 호출이 인코딩되어 다른 객체로 전달됩니다.
이 객체는 일관된 인터페이스를 통해 호출을 처리하게 됩니다. 따라서 다이나믹 프록시 클래스는 컴파일 타임 도구를 사용하지 않고도 인터페이스 목록에 대한 타입 안전한 프록시 객체를 생성하는 데 사용할 수 있습니다.
다이나믹 프록시 클래스의 인스턴스에서 메서드가 호출되면, 이 호출은 해당 인스턴스의 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는 인코딩된 메서드 호출을 적절하게 처리하고 리턴하는 결과는 프록시 인스턴스의 메서드 호출 결과로 리턴됩니다.
Proxy Class Properties
프록시 클래스는 다음과 같은 속성을 가집니다:
- 프록시 클래스의 단순 이름(unqualified name)은 지정되지 않았습니다. 그러나 "$Proxy"로 시작하는 클래스 이름 공간은 프록시 클래스에 예약되어야 합니다.
- 프록시 클래스가 정의되는 패키지와 모듈은 아래에서 설명됩니다.
- 프록시 클래스는
final
이며, 추상 클래스가 아닙니다. - 프록시 클래스는
java.lang.reflect.Proxy
를 확장합니다. - 프록시 클래스는 생성 시 지정된 인터페이스만을 정확히 동일한 순서로 구현합니다. 클래스 객체의
getInterfaces
메서드를 호출하면 생성 시 지정된 인터페이스 목록이 포함된 배열을 반환하고,getMethods
를 호출하면 해당 인터페이스에 포함된 모든 메서드의Method
객체 배열을 반환합니다.getMethod
를 호출하면 프록시 인터페이스에서 예상대로 메서드를 찾습니다. - 프록시 클래스의
ProtectionDomain
은java.lang.Object
와 같은 부트스트랩 클래스 로더에 의해 로드된 시스템 클래스와 동일합니다. 이는 프록시 클래스의 코드가 신뢰할 수 있는 시스템 코드에 의해 생성되기 때문입니다. 이 보호 도메인에는 일반적으로java.security.AllPermission
이 부여됩니다. Proxy.isProxyClass
메서드를 사용하여 주어진 클래스가 프록시 클래스인지 확인할 수 있습니다.
Proxy Instance Properties
프록시 인스턴스는 다음과 같은 속성을 가집니다:
- 프록시 인스턴스
proxy
와 그 프록시 클래스가 구현한 인터페이스 중 하나인Foo
가 주어졌을 때, 다음 표현식은true
를 반환합니다: proxy instanceof Foo
- 또한 다음 캐스트 연산이 성공하며(
ClassCastException
을 발생시키지 않고): (Foo) proxy
- 각 프록시 인스턴스에는 생성자에 전달된 Invocation Handler가 연관되어 있습니다. 정적 메서드
Proxy.getInvocationHandler
는 전달된 프록시 인스턴스에 연관된 Invocation Handler 를 반환합니다. - 프록시 인스턴스의 인터페이스 메서드를 호출하면, 이 호출은 해당 메서드에 대한 문서에 설명된 대로
InvocationHandler
의invoke
메서드로 인코딩되고 전달됩니다. - 프록시 인터페이스는 디폴트 메서드를 정의하거나 슈퍼 인터페이스에서 직접 또는 간접적으로 디폴트 메서드를 상속받을 수 있습니다. Invocation Handler는
InvocationHandler::invokeDefault
를 호출하여 프록시 인터페이스의 디폴트 메서드를 호출할 수 있습니다. - 프록시 인스턴스의
java.lang.Object
에 선언된hashCode
,equals
, 또는toString
메서드를 호출하면, 이 호출도 인터페이스 메서드 호출이 인코딩되고 전달되는 방식과 동일하게InvocationHandler
의invoke
메서드로 인코딩되고 전달됩니다.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)
메서드를 통해 정의된 프록시 클래스의 패키지 및 모듈 소속은 다음과 같이 지정됩니다:
- 모든 프록시 인터페이스가 공개(exported) 또는 열린(open) 패키지에 있는 경우:
- 모든 프록시 인터페이스가 public이라면, 프록시 클래스는 공개된
public
클래스가 되며, 이 클래스는 무조건적으로 공개된(exported) 패키지에 속합니다. 하지만 패키지와 모듈의 이름은 명시되지 않습니다. - 프록시 인터페이스 중 적어도 하나가 비공개(non-public)라면, 프록시 클래스는 비공개 클래스가 되며, 해당 비공개 인터페이스가 속한 패키지 및 모듈에 소속됩니다. 모든 비공개 인터페이스는 동일한 패키지 및 모듈에 있어야 하며, 그렇지 않으면 프록시 생성이 불가능합니다.
- 모든 프록시 인터페이스가 public이라면, 프록시 클래스는 공개된
- 프록시 인터페이스 중 적어도 하나가 비공개된(non-exported) 또는 닫힌(non-open) 패키지에 있는 경우:
- 모든 프록시 인터페이스가 public이라면, 프록시 클래스는 비공개된, 닫힌 패키지에 속한 공개
public
클래스가 됩니다. 이 경우 패키지와 모듈의 이름은 명시되지 않습니다. - 프록시 인터페이스 중 적어도 하나가 비공개(non-public)라면, 프록시 클래스는 해당 비공개 인터페이스가 속한 패키지 및 모듈에 속한 비공개 클래스가 됩니다. 이 경우에도 모든 비공개 인터페이스는 동일한 패키지 및 모듈에 있어야 하며, 그렇지 않으면 프록시 생성이 불가능합니다.
- 모든 프록시 인터페이스가 public이라면, 프록시 클래스는 비공개된, 닫힌 패키지에 속한 공개
- 접근성이 혼합된 프록시 인터페이스 (예: 공개된
public
인터페이스와 비공개된non-public
인터페이스)가 동일한 인스턴스에 의해 프록시되면, 프록시 클래스의 접근성은 가장 접근성이 낮은 프록시 인터페이스에 의해 결정됩니다.
추가로, 임의의 코드가 setAccessible
을 통해 공개된 패키지의 프록시 클래스에 접근할 수 있는 경우, 닫힌 패키지에 있는 프록시 클래스는 해당 모듈 외부의 코드에서 접근할 수 없습니다.
이 설명에서 "비공개된 패키지"는 모든 모듈에 대해 공개되지 않은 패키지를 의미하며, "닫힌 패키지"는 모든 모듈에 대해 열리지 않은 패키지를 의미합니다. 구체적으로, 이 용어들은 패키지가 그 모듈에 의해 공개되지 않았거나, 특정 모듈에 대해서만 조건부로 공개된 것을 나타냅니다.
"Exported"와 "Open" 패키지는 Java의 모듈 시스템에서 모듈 간의 접근성을 제어하는 개념입니다. Java 9에서 도입된 모듈 시스템은 모듈 간의 의존성과 접근을 보다 엄격하게 관리할 수 있도록 설계되었습니다.
1. Exported Packages (공개된 패키지)
⦁ 정의: 모듈이 다른 모듈에서 사용할 수 있도록 패키지를 공개할 때 사용하는 개념입니다. 모듈의 module-info.java 파일에서 exports 키워드를 사용하여 패키지를 공개합니다.
⦁ 사용 예:
위 예시에서는 com.example.mypackage 패키지가 다른 모듈에서 접근 가능하도록 공개되어 있습니다.module my.module { exports 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.Object
의 hashCode
, 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.Proxy
는 java.io.Serializable
을 구현하므로, 프록시 인스턴스는 직렬화할 수 있습니다. 그러나 프록시 인스턴스에 java.io.Serializable
에 할당할 수 없는 Invocation Handler가 포함되어 있으면, 이러한 인스턴스를 java.io.ObjectOutputStream
에 기록하려고 할 때 java.io.NotSerializableException
이 발생합니다. 프록시 클래스의 경우, java.io.Externalizable
을 구현하면 직렬화와 관련하여 java.io.Serializable
을 구현하는 것과 동일한 효과를 갖습니다: Externalizable
인터페이스의 writeExternal
및 readExternal
메서드는 프록시 인스턴스(또는 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
메서드를 호출하여 반환된 인터페이스 이름들로 구성되며, 이 인터페이스들은 지정된 순서대로 나열됩니다. classAnnotation
및 superClassDesc
항목은 classDescInfo
규칙에서와 동일한 의미를 가집니다. 프록
시 클래스의 경우, superClassDesc
는 해당 슈퍼클래스 java.lang.reflect.Proxy
의 클래스 설명자입니다. 이 설명자를 포함하면 프록시 인스턴스에 대한 클래스 Proxy
의 직렬화 표현의 진화를 지원할 수 있습니다.
프록시가 아닌 클래스의 경우, ObjectOutputStream
은 보호된 annotateClass
메서드를 호출하여 특정 클래스에 대한 스트림에 사용자 지정 데이터를 기록할 수 있도록 허용합니다. 프록시 클래스의 경우, annotateClass
대신 java.io.ObjectOutputStream
의 다음 메서드가 프록시 클래스의 Class
객체와 함께 호출됩니다:
protected void annotateProxyClass(Class cl) throws IOException;
ObjectOutputStream
의 기본 annotateProxyClass
구현은 아무 작업도 수행하지 않습니다.
ObjectInputStream
이 TC_PROXYCLASSDESC
타입 코드를 만나면, 프록시 클래스의 클래스 설명자를 위에 설명된 형식으로 스트림에서 역직렬화합니다. 클래스 설명자의 Class
객체를 해결하기 위해 resolveClass
메서드를 호출하는 대신, java.io.ObjectInputStream
의 다음 메서드가 호출됩니다:
protected Class resolveProxyClass(String[] interfaces)
throws IOException, ClassNotFoundException;
역직렬화된 프록시 클래스 설명자에서 인터페이스 이름 목록이 resolveProxyClass
의 interfaces
인수로 전달됩니다.
ObjectInputStream
의 기본 resolveProxyClass
구현은 interfaces
매개변수에서 이름이 지정된 인터페이스에 대한 Class
객체 목록을 사용하여 Proxy.getProxyClass
를 호출한 결과를 반환합니다. 인터페이스 이름 i
에 대해 사용된 Class
객체는 다음을 호출하여 반환된 값입니다:
Class.forName(i, false, loader)
여기서 loader
는 실행 스택에서 첫 번째로 null
이 아닌 클래스 로더 또는 실행 스택에 null
이 아닌 클래스 로더가 없는 경우 null
입니다. 이는 resolveClass
메서드의 기본 동작에서 선택된 클래스 로더와 동일한 클래스 로더 선택입니다. 동일한 loader
값은 Proxy.getProxyClass
에 전달되는 클래스 로더이기도 합니다. Proxy.getProxyClass
가 IllegalArgumentException
을 던지면, resolveClass
는 IllegalArgumentException
을 포함한 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 |