지난시간에 CGLIB를 배우기 위해 ASM을 사용해보았는데 이젠 CGLIB를 사용해봐야겠습니다.
2. CGLIB는 인터페이스가 아닌 클레스를 대상으로 동작 가능하고 바이트코드를 조작해서 프록시를 만들기에 Java Proxy에 비해 성능이 좋습니다. 일반적으로 스프링 프레임워크에서 AOP 기능을 사용하게 된다면 CGLIB를 사용하게 되지 않을까 싶습니다만...
CGLIB는 Byte Code Generation Library로서 동적 프록시 객체를 구성해주는 라이브러리입니다. 그럼 동적 프록시를 어떻게 생성해내는지 확인해보고 싶습니다. 실제 눈으로 보는것과 보지 않는 것의 차이는 어마어마하다고 생각합니다.
먼저 CGLIB를 임포트합니다. 가장 최근 버전이 3.2.5 버전이군요. 저 같은 경우 항상 임포트를 할 때는 https://mvnrepository.com에서 검색을 해서 찾습니다.
mvnrepository에서 CGLIB 검색
보이는 것들 중 CGLIB-NODEP을 사용하면 말 그대로 No Dependency 하다는 것이므로 다른 모듈을 추가할 필요가 없습니다. 바로 추가해봅니다.
간단한 사용법은 cglib의 github에 친절한 영문(?)으로 설명되어있네요. 일단 따라해봅니다.
아래와 같은 샘플클래스를 하나 만듭니다.
class SampleClass {
public String test(String input) {
return "Hello world!";
}
public String test2(String input) {
return "Hello world! 2";
}
}
자바 AOP CGLib - 클래스 프록시
그리고 CGLib의 Enhancer 클래스를 이용해서 프록시할 클래스를 등록하고 콜백 핸들러를 등록하는 방식으로 코딩이 됩니다. 가장 간단한 프록시 콜백을 등록해봅시다. FixedValue입니다. 모든 함수의 리턴값을 고정하는 방식입니다.
@Test
public void testFixedValue() throws Exception {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(SampleClass.class);
enhancer.setCallback(new FixedValue() {
@Override
public Object loadObject() throws Exception {
return "Hello cglib!";
}
});
SampleClass proxy = (SampleClass) enhancer.create();
assertEquals("Hello cglib!", proxy.test(null));
assertEquals("Hello cglib!", proxy.test2(null));
}
리턴을 String의 "Hello cglib!"로 고정한것이니 다른형을 리턴하는 함수를 호출하게 되면 cast 오류가 발생하게 됩니다. 좀 더 다양한 수단을 사용하기 위한 InvocationHandler를 이용한 콜백 사용방법입니다.
@Test
public void testInvocationHandler() throws Exception {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(SampleClass.class);
enhancer.setCallback(new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getDeclaringClass() != Object.class && method.getReturnType() == String.class) {
return "Hello cglib!";
} else {
throw new RuntimeException("Do not know what to do.");
}
}
});
SampleClass proxy = (SampleClass) enhancer.create();
assertEquals("Hello cglib!", proxy.test(null));
//assertNotEquals("Hello cglib!", proxy.toString());
}
만일 주석처리한 proxy.toString()를 풀게 되면 if 조건을 만족하지 않기에 "Do not know what to do." 오류를 발생시키게 됩니다. Method 클레스를 받을 수 있기에 여러가지 타입이나 형태를 판단하여 원하는 함수만 작업할 수 있겠지요.
드디어 접하고 싶었던 MethodProxy 차례입니다. 이녀석은 proxy의 슈퍼클레스의 함수를 콜할 수 있기에 더 다양하게 활용할 수 있을 듯 합니다.
Java AOP CGLIB Method Proxy 예제 샘플
@Test
public void testMethodInterceptor() throws Exception {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(SampleClass.class);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
if (method.getDeclaringClass() != Object.class && method.getReturnType() == String.class) {
return "Hello cglib!";
} else {
return proxy.invokeSuper(obj, args);
}
}
});
SampleClass proxy = (SampleClass) enhancer.create();
assertEquals("Hello cglib!", proxy.test(null));
assertNotEquals("Hello cglib!", proxy.toString());
System.out.println(proxy.hashCode()); // Does not throw an exception or result in an endless
System.out.println(proxy);
}
상세적인 내부 구현은 소스를 분석해봐야 알수 있겠으나 이런 과정으로 프록시를 생성하여 처리한다는 것을 알수 있었고 스프링 내부에서 이를 간단한 설정만으로 구현해준다는 것을 알 수 있습니다.
자바 CGLIB - 스프링 프레임워크 config xml 파일 설정
<aop:config proxy-target-class="true">
//proxy-targetclass=true인 경우 CGLIB 사용, false인 경우 proxy
------
------
</aop:config>
궁금해서 실제 스프링에 어떻게 적용되고 있는지 찾아봤습니다. org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder에 해당 내용을 찾을 수 있었습니다.
스프링 4.3의 MvcUriComponentsBuilder.java 에 적용된 cglib 내용.
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(type);
enhancer.setInterfaces(new Class<?>[] {MethodInvocationInfo.class});
enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
enhancer.setCallbackType(org.springframework.cglib.proxy.MethodInterceptor.class);
Class<?> proxyClass = enhancer.createClass();
Object proxy = null;
if (objenesis.isWorthTrying()) {
try {
proxy = objenesis.newInstance(proxyClass, enhancer.getUseCache());
}
catch (ObjenesisException ex) {
logger.debug("Unable to instantiate controller proxy using Objenesis, " +
"falling back to regular construction", ex);
}
}
if (proxy == null) {
try {
proxy = proxyClass.newInstance();
}
catch (Throwable ex) {
throw new IllegalStateException("Unable to instantiate controller proxy using Objenesis, " +
"and regular controller instantiation via default constructor fails as well", ex);
}
}
((Factory) proxy).setCallbacks(new Callback[] {interceptor});
return (T) proxy;
다음엔 컴파일 시 바이트코드를 조작하는 AspectJ 를 파헤쳐보도록 하겠습니다. AspectJ는 어떻게 쓰는지 스프링에는 어떻게 쓰여지고 있는지 궁금하네요. 아마도 aspectj를 마치면 자바의 aop 시리즈를 마무리 지을 듯 합니다.
'dev > java' 카테고리의 다른 글
자바 BCI란 뭘까? - Java BCI (Byte Code Instrumentation) #1 (1) | 2021.08.30 |
---|---|
자바(JDK, JRE) 쉽고 빠른 설치, 환경 설정 방법 (1) | 2021.08.10 |
자바 해시 테이블( Hashtable), 해시 맵(HashMap) 구조와 원리 (1) | 2021.07.10 |
자바 AOP - AspectJ에 대해서 - Java AOP #5 (0) | 2021.02.16 |
자바 AOP - CGLIB에 사용되는 ASM을 알아보자 - Java AOP #3 (0) | 2021.02.16 |
자바 AOP - JDK Dynamic Proxy 사용해보기 - Java AOP #2 (0) | 2021.02.15 |
java.net.MalformedURLException 원인 및 에러 해결 방법 (0) | 2020.11.03 |
자바 개발자를 위한 이클립스 단축키 top 30 (4) | 2020.10.26 |