https://sundaland.tistory.com/50
다이나믹 프록시 클래스는 런타임에, 지정된 인테페이스 목록을 구현하는 클래스이다. 이러한 클래스의 인스턴스를 통해 인터페이스의 메서드를 호출하면 해당 호출이 인코딩되어 다른 객체로 전달된다.
https://blank001.tistory.com/43
이 객체는 일관된 인터페이스를 통해 호출을 처리하게 된다. 따라서 다이나믹 프록시 클래스는 컴파일 타임 도구를 사용하지 않고도 인터페이스 목록에 대한 타입이 안전한 프록시 객체를 생성하는데 사용할 수 있다.
https://blank001.tistory.com/44
다이나믹 프록시 클래스의 인스턴스에서 메서드가 호출되면, 이 호출은 해당 인스턴스의 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 메서드는 인터페이스에 대한 프록시 인스턴스를 리턴하며, 이 프록시 인스턴스는 메서드 호출을 지정한 Invocation Handler로 전달한다. 이 메서드를 사용할 때 몇 가지 제약 조건을 준수해야 하며, 이를 위반하면 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인 경우, 모든 메서드가 동일한 리턴 타입을 가져야 한다. 그렇지 않으면 하나의 메서드 리턴타입이 나머지 메서드의 리턴 타입에 할당 가능해야 한다.
- 생성된 프록시 클래스는 가상 머신이 클래스에 부과하는 제한을 초과해서는 안된다. 예를 들어 가상 머신이 클래스가 구현할 수 있는 인터페이스 수를 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 객체가 있으며, 이 객체는 인터페이스 Invocation Handler를 구현한다. 프록시 인터페이스 중 하나를 통한 프록시 인스턴스 메서드 호출은 인스턴스의 invocation handler의 invoke 메서드로 전송되어 프록시 인스턴스, 호출된 메서드를 식별하는 java.lang.reflect.Method 객체, 아규먼트를 포함하는 Object 타입의 배열을 전달한다.
invocation handler는 인코딩된 메서드 호출을 적절하게 처리하고 리턴하는 결과는 프록시 인스턴스의 메서드 호출 결과로 리턴된다.
https://blank001.tistory.com/45
[ ▷ Proxy Class Properties ]
- 프록시 클래스의 단순 이름 (unqualified name)은 지정되지 않았다. 그러나 $Proxy로 시작하는 클래스 이름 공간은 프록시 클래스에 예약되어야 한다.
- 프록시 클래스는 final이며, 추상 클래스가 아니다.
- 프록시 클래스는 java.lang.reflect.Proxy를 확장한다.
- 프록시 클래스는 생성 시 지정된 인터페이스만을 정확히 동일한 순서로 구현한다. 클래스 객체의 getInterfaces 메서드를 호출하면 생성 시 지정된 인터페이스에 포함된 모든 메서드의 Method 객체 배열을 반환한다. getMethod를 호출하면 프록시 인터페이스에서 예상대로 메서드를 찾는다.
- 프록시 클래스의 ProtectionDomani은 java.lang.Object와 같은 부트스트랩 클래스 로더에 의해 로드된 시스템 클래스와 동일하다. 이는 프록시 클래스의 코드가 신뢰할 수 있는 시스템 코드에 의해 생성되기 때문이다. 이 보호 도메인에는 일반적인 java.security.AllPermission이 부여된다.
- Proxy.isProxyClass 메서드를 사용하여 주어진 클래스가 프록시 클래스인지 확인할 수 있다.
[ ▷ Proxy Instance Properties ]
- 프록시 인스턴스 proxy와 그 프록시 클래스가 구현한 인테페이스 중 하나인 Foo가 주어졌을 때 proxy instanceof Foo는 true를 반환한다.
- 또한 (Foo) proxy 캐스트 연산이 성공한다. (ClassCastException 발생시키지 않는다)
- 각 프록시 인스턴스에는 생성자에 전달된 Invocation Handler가 연관되어 있다. 정적 메서드 Proxy.getInvocationHandler는 전달된 프록시 인스턴스에 연관된 InvocationHandler를 반환한다.
- 프록시 인스턴스의 인터페이스 메서드를 호출하면, 이 호출은 InvocationHandler의 invoke 메서드로 인코딩되고 전달된다.
- 프록시 인터페이스는 디폴트 메서드를 정의하거나 슈퍼 인터페이스에서 직접 또는 간접적으로 디폴트 메서드를 상속받을 수 있다. Invocation Handler는 InvocationHandler::invokeDeafult를 호출하여 프록시 인터페이스의 디폴트 메서드를 호출할 수 있다.
- 프록시 인스턴스의 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) 메서드를 통해 정의된 프록시 클래스의 패키지 및 모듈 소속은 아래와 같다.
1. 모든 프록시 인터페이스가 공개 (exported) 또는 열린 (open) 패키지에 있는 경우
- 모든 프록시 인터페이스가 public이라면, 프록시 클래스는 공개된 public 클래스가 되며, 이 클래스는 무조건적으로 공개된 (exported) 패키지에 속한다. 하지만 패키지와 모듈의 이름은 명시되지 않는다.
- 프록시 인터페이스 중 적어도 하나가 비공개 (non-public)라면, 프록시 클래스는 비공개 클래스가 되며, 해당 비공개 인터페이스가 속한 패키지 및 모듈에 소속된다. 모든 비공개 인터페이스는 동일한 패키지 및 모듈에 있어야 하며, 그렇지 않으면 프록시 생성이 불가능하다.
2. 프록시 인터페이스 중 적어도 하나가 비공개 (non-exported) 또는 닫힌 (non-open) 패키지에 있는 경우
- 모든 프록시 인터페이스가 public이라면, 프록시 클래스는 비공개된, 닫힌 패키지에 속한 공개 public 클래스가 된다. 이 경우 패키지 모듈의 이름은 명시되지 않는다.
- 프록시 인터페이스 중 적어도 하나가 비공개라면, 프록시 클래스는 해당 비공개 인터페이스가 속한 패키지 및 모듈에 속한 비공개 클래스가 된다. 이 경우에도 모든 비공개 인터페이스는 동일한 패키지 및 모듈에 있어야 하며, 그렇지 않으면 프록시 생성이 불가능하다.
3. 접근성이 혼합된 프록시 인터페이스
- 공개된 public 인터페이스와 비공개된 non-public 인터페이스가 동일한 인스턴스에 의해 프록시되면, 프록시 클래스의 접근성은 가장 접근성이 낮은 프록시 인터페이스에 의해 결정된다.
추가로, 임의의 코드가 setAccessible를 통해 공개된 패키지의 프록시 클래스에 접근할 수 있는 경우, 닫힌 패키지에 있는 프록시 클래스는 해당 모듈 외부의 코드에서 접근할 수 없다.
이 설명에서 비공개된 패키지는 모든 모듈에 대해 공개되지 않는 패키지를 의미하며, 닫힌 패키지는 모든 모듈에 대해 열리지 않는 패키지를 의미한다. 구체적으로 이 용어들은 패키지가 그 모듈에 의해 공개되지 않았거나, 특정 모듈에 대해서만 조건부로 공개된 것을 나타낸다.
Exported와 Open 패키지는 자바의 모듈 간의 접근성을 제어하는 개념이다. 자바 9에서 도입된 모듈 시스템은 모듈간의 의존성과 접근을 보다 엄격하게 관히라 수 있도록 설계되었다.
1. Exported Packages (공개된 패키지)
- 모둘이 다른 모듈에서 사용할 수 있도록 패키지를 공개할 때 사용하는 개념이다. 모듈의 moduble-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; // 리플렉션을 위해 열림
}
이러한 패키지 개념을 통해 자바 모듈 시스템은 코드의 가시성과 접근성을 더 잘 제어할 수 있다.
[ ▷ Methods Duplicated in Multiple Proxy Interfaces ]
프록시 클래스의 두 개 이상의 인터페이스에 동일한 이름과 파라미터 시그니처를 가진 메서드가 포함된 경우, 프록시 클래스의 인터페이스 순서는 중요해진다. 프록시 인스턴스에서 이러한 중복 메서드가 호출되면 Invocation Handler에 전달된 Method 객체는 반드시 호출된 인터페이스의 참조 타입에서 할당 가능한 클래스의 메서드 선언 클래스일 필요는 없다. 이 제한 사항은 생성된 프록시 클래스의 해당 메서드 구현이 어떤 인터페이스에서 호출되었는지 결정할 수 없기 때문에 존재한다. 따라서 프록시 인스턴스에서 중복 메서드가 호출되면, 해당 메서드를 포함한 첫 번째 인터페이스 (직접 포함하거나 인터페이스를 통해 상속받은)의 Method 객체가 호출 핸들러의 invoke 메서드에 전달된다.
프록시 인터페이스에 java.lang.Object의 hashCode, equal, toString 메서드와 동일한 이름과 파라미터 시그니처를 가진 메서드가 포함된 경우, 이러한 메서드가 프록시 인스턴스에서 호출되면, invoke 메서드에 전달된 Method 객체는 java.lang.Object를 선언 클래스로 가진다. 즉 java.lang.Object의 공용, 비최종 메서드가 프록시 인터페이스보다 앞서서 Method 객체를 결정하는 기준이 된다.
또한 중복된 메서드가 Invocation Handler로 전달될 때, invoke 메서드는 해당 메서드가 호출될 수 있는 모든 프록시 인터페이스에서 throw 절에 선언된 예외 타입 중 하나에 할당 가능한 체크드 예외 타입만을 던질 수 있다. 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 출력
((InterfaceB) proxyInstance).duplicateMethod(); // ClassB: duplicateMethod 출력
}
}
[ ▷ Serialization ]
java.lang.reflect.Proxy는 java.io.Serializable을 구현하므로, 프록시 인스턴스는 직렬화 할 수 있다. 그러나 프록시 인스턴스에 java.io.Serializable에 할당할 수 없는 Invocation Handler가 포함되어 있으면, 이러한 인스턴스를 java.io.ObjectOutputStream에 기록하려고 할 때 java.io.NotSerializableExcetpion이 발생한다. 프록시 클래스의 경우, java.io.Externalizable을 구현하면 직렬화와 관련하여 java.io.Serializable을 구현하는 것과 동일한 효과를 갖는다.
Externalizable 인터페이스의 writeExternal 및 readExternal 메서드는 프록시 인스턴스 (또는 Invocation Handler)의 직렬화 과정의 일부로서 절대 호출되지 않는다. 모든 Class 객체와 마찬가지로, 프록시 클래스의 Class 객체는 항상 직렬화 가능하다.
프록시 클래스는 직렬화 가능한 필드를 가지지 않으며, serialVersionUID는 OL이다. 즉 프록시 클래스의 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);
▼ 상속받은 메서드에 대한 기본 프록시 동작을 제공하고, 프록시 메서드 호출을 해당 메서드의 인터페이스에 따라 개별 객체에 위임하는 유틸리티 호출 핸들러 클래스의 예시
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에서 오버로드되지 않기 때문이다.
'Reflection' 카테고리의 다른 글
Arrays and Enumerated Types (0) | 2024.08.21 |
---|---|
unqualified name (0) | 2024.08.20 |
sealed interface (0) | 2024.08.20 |
type-safe proxy object (0) | 2024.08.20 |
Structuring method calls (0) | 2024.08.20 |