Spring ‐ 동적 프록시 기술 - thought-corner/Backend-PlayGround GitHub Wiki
@Test
void reflection2() throws Exception {
// 클래스 정보 메타데이터 획득
Class classHello = Class.forName("hello.proxy.jdkdynamic.ReflectionTest$Hello");
Hello target = new Hello();
// callA 메서드 정보 획득 및 공통 로직 실행
Method methodCallA = classHello.getMethod("callA");
dynamicCall(methodCallA, target);
// callB 메서드 정보 획득 및 공통 로직 실행
Method methodCallB = classHello.getMethod("callB");
dynamicCall(methodCallB, target);
}
/**
* 리플렉션을 활용한 공통 로직 추상화
* 메서드 정보(Method)와 실행 대상(target)을 파라미터로 받아 동적으로 실행한다.
*/
private void dynamicCall(Method method, Object target) throws Exception {
log.info("start");
// 획득한 메서드 메타데이터를 이용해 실제 인스턴스의 메서드를 동적으로 호출
Object result = method.invoke(target);
log.info("result={}", result);
}- 리플렉션을 사용하면 클래스와 메서드 메타 정보를 사용해서 애플리케이션을 동적으로 유연하게 만들 수 있다.
- 하지만 리플렉션 기술은 런타임에 동작하기 때문에 컴파일 시점에 오류를 잡을 수 없다.
- Java Reflection 정리
Method methodCallB = classHello.getMethod("callBBBB"); // 컴파일 시점에 오류를 잡을 수 없다.- JDK 동적 프록시는 인터페이스를 기반으로 동적으로 만들어준다.
public interface InvocationHandler {
/**
* Processes a method invocation on a proxy instance and returns
* the result. This method will be invoked on an invocation handler
* when a method is invoked on a proxy instance that it is
* associated with.
*
* @param proxy the proxy instance that the method was invoked on
*
* @param method the {@code Method} instance corresponding to
* the interface method invoked on the proxy instance. The declaring
* class of the {@code Method} object will be the interface that
* the method was declared in, which may be a superinterface of the
* proxy interface that the proxy class inherits the method through.
*
* @param args an array of objects containing the values of the
* arguments passed in the method invocation on the proxy instance,
* or {@code null} if interface method takes no arguments.
* Arguments of primitive types are wrapped in instances of the
* appropriate primitive wrapper class, such as
* {@code java.lang.Integer} or {@code java.lang.Boolean}.
*
* @return the value to return from the method invocation on the
* proxy instance. If the declared return type of the interface
* method is a primitive type, then the value returned by
* this method must be an instance of the corresponding primitive
* wrapper class; otherwise, it must be a type assignable to the
* declared return type. If the value returned by this method is
* {@code null} and the interface method's return type is
* primitive, then a {@code NullPointerException} will be
* thrown by the method invocation on the proxy instance. If the
* value returned by this method is otherwise not compatible with
* the interface method's declared return type as described above,
* a {@code ClassCastException} will be thrown by the method
* invocation on the proxy instance.
*
* @throws Throwable the exception to throw from the method
* invocation on the proxy instance. The exception's type must be
* assignable either to any of the exception types declared in the
* {@code throws} clause of the interface method or to the
* unchecked exception types {@code java.lang.RuntimeException}
* or {@code java.lang.Error}. If a checked exception is
* thrown by this method that is not assignable to any of the
* exception types declared in the {@code throws} clause of
* the interface method, then an
* {@link UndeclaredThrowableException} containing the
* exception that was thrown by this method will be thrown by the
* method invocation on the proxy instance.
*
* @see UndeclaredThrowableException
*/
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
/**
* Invokes the specified default method on the given {@code proxy} instance with
* the given parameters. The given {@code method} must be a default method
* declared in a proxy interface of the {@code proxy}'s class or inherited
* from its superinterface directly or indirectly.
* <p>
* Invoking this method behaves as if {@code invokespecial} instruction executed
* from the proxy class, targeting the default method in a proxy interface.
* This is equivalent to the invocation:
* {@code X.super.m(A* a)} where {@code X} is a proxy interface and the call to
* {@code X.super::m(A*)} is resolved to the given {@code method}.
* <p>
* Examples: interface {@code A} and {@code B} both declare a default
* implementation of method {@code m}. Interface {@code C} extends {@code A}
* and inherits the default method {@code m} from its superinterface {@code A}.
*
* <blockquote><pre>{@code
* interface A {
* default T m(A a) { return t1; }
* }
* interface B {
* default T m(A a) { return t2; }
* }
* interface C extends A {}
* }</pre></blockquote>
*
* The following creates a proxy instance that implements {@code A}
* and invokes the default method {@code A::m}.
*
* <blockquote><pre>{@code
* Object proxy = Proxy.newProxyInstance(loader, new Class<?>[] { A.class },
* (o, m, params) -> {
* if (m.isDefault()) {
* // if it's a default method, invoke it
* return InvocationHandler.invokeDefault(o, m, params);
* }
* });
* }</pre></blockquote>
*
* If a proxy instance implements both {@code A} and {@code B}, both
* of which provides the default implementation of method {@code m},
* the invocation handler can dispatch the method invocation to
* {@code A::m} or {@code B::m} via the {@code invokeDefault} method.
* For example, the following code delegates the method invocation
* to {@code B::m}.
*
* <blockquote><pre>{@code
* Object proxy = Proxy.newProxyInstance(loader, new Class<?>[] { A.class, B.class },
* (o, m, params) -> {
* if (m.getName().equals("m")) {
* // invoke B::m instead of A::m
* Method bMethod = B.class.getMethod(m.getName(), m.getParameterTypes());
* return InvocationHandler.invokeDefault(o, bMethod, params);
* }
* });
* }</pre></blockquote>
*
* If a proxy instance implements {@code C} that inherits the default
* method {@code m} from its superinterface {@code A}, then
* the interface method invocation on {@code "m"} is dispatched to
* the invocation handler's {@link #invoke(Object, Method, Object[]) invoke}
* method with the {@code Method} object argument representing the
* default method {@code A::m}.
*
* <blockquote><pre>{@code
* Object proxy = Proxy.newProxyInstance(loader, new Class<?>[] { C.class },
* (o, m, params) -> {
* if (m.isDefault()) {
* // behaves as if calling C.super.m(params)
* return InvocationHandler.invokeDefault(o, m, params);
* }
* });
* }</pre></blockquote>
*
* The invocation of method {@code "m"} on this {@code proxy} will behave
* as if {@code C.super::m} is called and that is resolved to invoking
* {@code A::m}.
* <p>
* Adding a default method, or changing a method from abstract to default
* may cause an exception if an existing code attempts to call {@code invokeDefault}
* to invoke a default method.
*
* For example, if {@code C} is modified to implement a default method
* {@code m}:
*
* <blockquote><pre>{@code
* interface C extends A {
* default T m(A a) { return t3; }
* }
* }</pre></blockquote>
*
* The code above that creates proxy instance {@code proxy} with
* the modified {@code C} will run with no exception and it will result in
* calling {@code C::m} instead of {@code A::m}.
* <p>
* The following is another example that creates a proxy instance of {@code C}
* and the invocation handler calls the {@code invokeDefault} method
* to invoke {@code A::m}:
*
* <blockquote><pre>{@code
* C c = (C) Proxy.newProxyInstance(loader, new Class<?>[] { C.class },
* (o, m, params) -> {
* if (m.getName().equals("m")) {
* // IllegalArgumentException thrown as {@code A::m} is not a method
* // inherited from its proxy interface C
* Method aMethod = A.class.getMethod(m.getName(), m.getParameterTypes());
* return InvocationHandler.invokeDefault(o, aMethod params);
* }
* });
* c.m(...);
* }</pre></blockquote>
*
* The above code runs successfully with the old version of {@code C} and
* {@code A::m} is invoked. When running with the new version of {@code C},
* the above code will fail with {@code IllegalArgumentException} because
* {@code C} overrides the implementation of the same method and
* {@code A::m} is not accessible by a proxy instance.
*
* @apiNote
* The {@code proxy} parameter is of type {@code Object} rather than {@code Proxy}
* to make it easy for {@link InvocationHandler#invoke(Object, Method, Object[])
* InvocationHandler::invoke} implementation to call directly without the need
* of casting.
*
* @param proxy the {@code Proxy} instance on which the default method to be invoked
* @param method the {@code Method} instance corresponding to a default method
* declared in a proxy interface of the proxy class or inherited
* from its superinterface directly or indirectly
* @param args the parameters used for the method invocation; can be {@code null}
* if the number of formal parameters required by the method is zero.
* @return the value returned from the method invocation
*
* @throws IllegalArgumentException if any of the following conditions is {@code true}:
* <ul>
* <li>{@code proxy} is not {@linkplain Proxy#isProxyClass(Class)
* a proxy instance}; or</li>
* <li>the given {@code method} is not a default method declared
* in a proxy interface of the proxy class and not inherited from
* any of its superinterfaces; or</li>
* <li>the given {@code method} is overridden directly or indirectly by
* the proxy interfaces and the method reference to the named
* method never resolves to the given {@code method}; or</li>
* <li>the length of the given {@code args} array does not match the
* number of parameters of the method to be invoked; or</li>
* <li>any of the {@code args} elements fails the unboxing
* conversion if the corresponding method parameter type is
* a primitive type; or if, after possible unboxing, any of the
* {@code args} elements cannot be assigned to the corresponding
* method parameter type.</li>
* </ul>
* @throws IllegalAccessException if the declaring class of the specified
* default method is inaccessible to the caller class
* @throws NullPointerException if {@code proxy} or {@code method} is {@code null}
* @throws Throwable anything thrown by the default method
* @since 16
* @jvms 5.4.3. Method Resolution
*/
@CallerSensitive
public static Object invokeDefault(Object proxy, Method method, Object... args)
throws Throwable {
Objects.requireNonNull(proxy);
Objects.requireNonNull(method);
return Proxy.invokeDefault(proxy, method, args, Reflection.getCallerClass());
}
}- 프록시 인스턴스에서 메서드가 호출될 때 실행되는 로직을 정의한다.
1. JDK 동적 프록시란?
- JDK 동적 프록시는 자바의 리플렉션 API를 사용하여 컴파일 타임이 아닌 런타임에 프록시 객체를 동적으로 생성해주는 기술이다.
- 타겟 클래스마다 수동으로 프록시 클래스를 만들지 않아도 된다.
2. 동작 메커니즘과 핵심 요소
- 인터페이스 필수 : JDK 동적 프록시는 인터페이스가 반드시 있어야만 생성이 가능하다. 프록시 객체가 해당 인터페이스를 구현하는 방식으로 생성되기 때문이다.
- InovcationHandler : 프록시의 모든 호출은
invoke()메서드로부터 시작된다. 여기서 리플렉션을 사용하여 실제 타겟의 메서드를 동적으로 호출한다.- 리플렉션 기반 : 전달받은
Method객체와target인스턴스를 이용해method.invoke(target, args)를 실행함으로써 로직을 위임한다.
- CGLIB(Code Generator Library)
- CGLIB는 바이트코드를 조작해서 동적으로 클래스를 생성하는 기술을 제공하는 라이브러리이다.
- CGLIB를 사용하면 인터페이스가 없어도 구체 클래스만 가지고 동적 프록시를 만들 수 있다.
- Spring - Singleton Container