오늘은 CGLIB를 배우기 위해 ASM을 먼저 정리해봅니다.
2. CGLIB는 인터페이스가 아닌 클레스를 대상으로 동작 가능하고 바이트코드를 조작해서 프록시를 만들기에 Java Proxy에 비해 성능이 좋습니다. 일반적으로 스프링 프레임워크에서 AOP 기능을 사용하게 된다면 CGLIB를 사용하게 되지 않을까 싶습니다만...
CGLIB는 Byte Code Generation Library로서 동적 프록시 객체를 구성해주는 라이브러리입니다. 내부적으로 ASM 모듈을 사용하기에 ASM를 공부하지 않고 넘어갈수가 없습니다.
Java Byte Code 조작 및 분석 프레임워크입니다. 클레스를 동적으로 생성하거나 수정할 수 있습니다. 같은 기능을 하는 BCEL이라는 놈이 있지만 이놈은 아파치의 것이고 ASM은 Obejctweb의 것입니다. 역시 저는 속도충이기에... 더 빠르다는 ASM을 선택했습니다. CGLIB에서도 ASM을 사용한다는것 자체도 ASM의 성능이나 기능에 대한 검증이 된것이 아니겠습니까.
이미 수많은 사람들이 사용한 라이브러리의 경우 그것을 믿는 것은 어느정도 자연스러워보입니다. 우리가 소나타를 선택하듯이요. 그나저나 어떤식으로 생성하거나 수정할 수 있는지 직접 해봐야겠습니다. 항상 눈으로 보고 쳐봐야 남는것이니까요. (손으로)
http://asm.ow2.org/eclipse/index.html 이곳에 가면 다음과 같이 안내가 되어있습니다.
Installation
The Bytecode Outline plugin can be installed from the Eclipse Update Manager with the ObjectWeb Eclipse Update Site http://download.forge.objectweb.org/eclipse-update/
Alternatively, the plugin can be downloaded from the ObjectWeb Forge site, and manually installed in the Eclipse plugins directory.
대충 단어만 보더라도 수동으로 다운로드 해서 설치해볼수도 있고 이클립스의 업데이트 기능으로도 할수 있다는 말 같습니다. 이클립스의 플러그인을 설치해봅니다.
Help -> Install New Software --> add --> download.forge.ow2.org/eclipse-update/
이렇게 하고 설치를 쭉 진행하면 됩니다.
설치 하다가 에러가 발생하는 부분이 있는데요. 저기 Unknow Host 가 나오는 부분을 보면 jar 다운로드 주소가 잘못되어있는걸 확인해볼수가 있습니다.
download.forge.ow2.org/asm/de.loskutov.BytecodeOutline_2.0.2.jar
일단 맞는 주소를 첨부를 해놓습니다.
그리고 download.forge.ow2.org/eclipse-update/site.xml
이 파일을 열면 jar 파일 주소들이 전부 과거형인
http://download.forge.objectweb.org
인데 하도 오래된 자료다보니 주소도 모두 바뀌어있었고 최신 업데이트 주소는
raw.githubusercontent.com/iloveeclipse/plugins/latest/
로 하면 된다고 되어있었지만 결국 모두 실패했습니다.... 하긴 10년도 넘은 걸로 보여서
이런식으로 바이트 코드와 코드까지 매핑을 해주는 기능이 있어서 바이트코드를 매우 가시적으로 볼수 있는 플러그인이라는 것까지는 알았습니다... 경험을 해보면 좋았을텐데요.
ASM 라이브러리를 사용해보자.
우선 asm 라이브러리를 임포트하고...
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-all</artifactId>
<version>6.0_BETA</version>
</dependency>
ClassReader를 통해 읽어들여봅시다. 여러가지 정보들을 꺼내볼수 있습니다. 필드들... 함수들.. 그리고 접근제어가 public인지 private인지 protected인지... friendly인지도 나옵니다. 그 외에도 api를 열어보면 이것저것 잡다하게 보여주는 것을 알수 있습니다.
package test;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldNode;
class person{
public String name = "nhj12311";
protected int tall = 180;
private int age = 23;
}
public class Exam02_ASM {
public static void main(String[] args) throws Throwable {
ClassReader cr = new ClassReader("test.person");
ClassNode cn = new ClassNode();
cr.accept(cn, ClassReader.SKIP_CODE);
System.out.println("ClassName : " + cn.name);
System.out.println("SuperName : "+cn.superName);
System.out.println("Opcodes.ACC_PUBLIC : "+Opcodes.ACC_PUBLIC);
System.out.println("Opcodes.ACC_PROTECTED : "+Opcodes.ACC_PROTECTED);
System.out.println("Opcodes.ACC_PRIVATE : "+Opcodes.ACC_PRIVATE);
for(int i = 0; i < cn.fields.size();i++){
FieldNode fn = (FieldNode)cn.fields.get(i);
System.out.println(fn.name + " : " + fn.access);
}
}
}
출력 :
ClassName : test/person
SuperName : java/lang/Object
Opcodes.ACC_PUBLIC : 1
Opcodes.ACC_PROTECTED : 4
Opcodes.ACC_PRIVATE : 2
name : 1
tall : 4
age : 2
핵심적으로 알아야 할 내용은 리플렉션과 다르게 클레스를 클레스로더에 로딩하지 않고도 클레스의 구조를 파악할 수 있다는 점입니다. 어떻게 아냐구요? 그럼 클레스 로더에 정말 있는지 확인해봅시다. 클레스로더에 클레스들이 로딩 될때 적재가 되는 것이니까요.
ClassLoader에 classes라는 private한 Vector 변수로 담고 있습니다. 이녀석을 리플렉션을 이용해서 까서 볼수가 있습니다. 그러니 private한 필드를 만나더라도 두려워할 필요없습니다. 바로 이렇게...
// 리플렉션으로 private한 필드를 열어볼수 있다.
// 클레스로더에 로딩된 모든 클레스를 까보자.
java.lang.reflect.Field fieldClasses = ClassLoader.class.getDeclaredField("classes");
fieldClasses.setAccessible(true);
Vector classes = (Vector) fieldClasses.get(Exam02_ASM.class.getClassLoader());
for (Iterator iter = classes.iterator(); iter.hasNext();) {
System.out.println("Loaded :" + iter.next());
}
출력 : 클레스 person은 로딩되지 않은 걸 확인할 수 있습니다.
Loaded :class test.Exam02_ASM
Loaded :class org.objectweb.asm.ClassVisitor
Loaded :class org.objectweb.asm.MethodVisitor
-------- 중략 ----------
Loaded :class org.objectweb.asm.tree.JumpInsnNode
Loaded :class org.objectweb.asm.tree.FieldInsnNode
Loaded :class org.objectweb.asm.tree.InsnList
Loaded :class org.objectweb.asm.MethodWriter
이렇게 코드 내에서 person.class를 명시하기만 해도 person은 로딩됩니다.
--- 중략 ---
person.class.getName();
--- 중략 ---
출력 :
--- 생략 ---
Loaded :class org.objectweb.asm.tree.FieldInsnNode
Loaded :class org.objectweb.asm.tree.InsnList
Loaded :class org.objectweb.asm.MethodWriter
Loaded :class test.person
따라서 ASM은 class를 로딩하지 않으며 ClassReader를 통해 class 구조를 확인할 수 있다는 걸 알 수 있었습니다. 이제 ClassWriter와 MethodWriter, FieldWriter를 이용해서 클레스를 변경해보죠. 바이코드에 대한 지식이 있어야하지만 시간이 없기때문에 감만 잡고 넘어갑니다.http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html jvm에서 바이트코드를 읽는 스펙은 이곳을 참고하면 될듯 합니다.
다음은 "Hello Java"을 출력하는 예시입니다. gongon95님의 포스팅을 참고하여 만들었습니다. Exam02_ASM2.java 를 생성합니다.
package test;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
public class Exam02_ASM2 extends ClassLoader implements Opcodes {
public static void main(String[] args) throws Throwable {
String name = "test/Example02";
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
cw.visit(V1_1, ACC_PUBLIC, name, null, "java/lang/Object", null);
MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("Hello Java");
mv.visitMethodInsn(INVOKEVIRTUAL,"java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
mv.visitInsn(RETURN);
mv.visitMaxs(0, 0);
mv.visitEnd();
byte[] bytesClass = cw.toByteArray();
Exam02_ASM2 ea = new Exam02_ASM2();
Class cl = ea.defineClass(name.replace('/', '.'), bytesClass, 0, bytesClass.length);
cl.newInstance(); // 인스턴스 생성 시 Hello Java 출력
}
}
출력 : Hello Java
이렇게 직접적으로 바이트코드를 생성하여 만들어내고 조작할 수 있다는 사실을 알 수 있습니다. 이를 자유자재로 사용하기 위해서는 바이트코드에 대한 상당한 지식을 요하는것도 알수 있었습니다. CGLIB에는 이 기술을 응용하여 직접적으로 리플렉션이 아닌 MethodProxy 를 사용하는 방법으로 더욱 빠른 속도를 낸다고 합니다. 다음번에는 MethodProxy 를 사용해보고 왜 리플렉션 방식보다 빠를수밖에 없는지 이해해보는 시간을 가져보도록 하겠습니다.
'dev > java' 카테고리의 다른 글
자바(JDK, JRE) 쉽고 빠른 설치, 환경 설정 방법 (1) | 2021.08.10 |
---|---|
자바 해시 테이블( Hashtable), 해시 맵(HashMap) 구조와 원리 (1) | 2021.07.10 |
자바 AOP - AspectJ에 대해서 - Java AOP #5 (0) | 2021.02.16 |
자바 AOP - 이제 CGLIB를 사용해봅시다 - Java AOP #4 (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 |
IllegalAccessError 원인과 예외/에러 해결 방법(java.lang.IllegalAccessError) (0) | 2020.01.12 |