PrepareStatement Search
transformer를 통해 prepareStatement를 조회해보자.
public class MyClassTransformer implements ClassFileTransformer {
//이 인터페이스의 구현체(transform)은 JVM이 존재하는 클래스를 로드할 때마다 호출되며, 이 시점에서 바이트코드를 조사하고 변경할 수 있다.
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) {
if (className.contains("PreparedStatement") || className.contains("preparedStatement") ) {
System.out.println("Detected PreparedStatement implementation: " + className);
// Apply bytecode manipulation logic here
}
return classfileBuffer;
}
다음과 같은 경로 중 ProxyPreparedStatement를 바이트코드 조작하는 것이 낫다는 결론을 지었다.
이 클래스는 PreparedStatement의 프록시 구현으로, 실제 SQL 쿼리 실행 메서드(execute, executeQuery, executeUpdate 등)를 오버라이딩한다. HikariCP 커넥션 풀 라이브러리에 의해 사용되며, SQL 쿼리 실행 시점을 가로채는 데 이상적인 지점을 제공한다.
이 중 execute, executeQuery, executeUpdate 메서드 에 바이트코드를 삽입하여 SQL 쿼리 실행 전후에 트랜잭션 데이터를 캡처하고 로깅 또는 외부 시스템으로 전송하는 로직을 추가하는 것이 이상적이다.
코드 적용(Agent)
package org.agent.util.asm.testcode;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
/**
* 테스트용 ClassVisitor 구현체
* psmt 변조
*/
public class PreparedStatementModifyVisitor extends ClassVisitor {
private String currentClassName;
public PreparedStatementModifyVisitor(ClassVisitor cv) {
// 부모 클래스의 생성자에 ASM API 버전과 ClassVisitor 인스턴스(ClassWriter)를 전달
super(Opcodes.ASM9, cv);
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
currentClassName = name.replace('/', ',');
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
// execute, executeQuery, executeUpdate 메서드 확인
if ("execute".equals(name) || "executeQuery".equals(name) || "executeUpdate".equals(name)) {
System.out.println("check method : " + name);
return new AddLogVisitor(mv, name, currentClassName);
}
return mv; // 기본 MethodVisitor 반환
}
//메서드 최상단에 로그 생성
private static class AddLogVisitor extends MethodVisitor {
private final String methodName;
private final String className;
public AddLogVisitor(MethodVisitor mv, String methodName, String className) {
super(Opcodes.ASM9, mv);
this.methodName = methodName;
this.className = className;
}
@Override
public void visitCode() {
super.visitCode();
/*
System.out.println("Method start: ")
- Opcodes.GETSTATIC : 정적 필드 접근 / "out"은 접근하려는 필드 / "Ljava/io/PrintStream;"은 필드의 타입
- mv.visitLdcInsn("Method start: "); - 상수 풀에서 문자열 상수를 로드하는 바이트코드 생성
- Opcodes.INVOKEVIRTUAL : 가상 메서드 호출 / "java/io/PrintStream" : 메서드가 속한 클래스의 내부 이름 / println : 메서드 / 서술자
*/
mv.visitFieldInsn(Opcodes.GETSTATIC,"java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("Method start : " + methodName + " \t Class name : " + className);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,"java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
}
}
check method : executeQuery
check method : executeUpdate
2024-02-06T16:58:26.822+09:00 INFO 19760 --- [nio-8080-exec-5] org.agent.util.asm.AsmCodeFactory : done print
Method start : executeUpdate Class name : com,zaxxer,hikari,pool,HikariProxyPreparedStatement
Method start : executeUpdate Class name : com,zaxxer,hikari,pool,ProxyPreparedStatement
2024-02-06T16:58:31.428+09:00 WARN 19760 --- [nio-8080-exec-1] c.d.j.restapi.service.SqlServiceImpl : SQL insert, nowTime = 2024-02-06T16:58:31.428271200
Hibernate: insert into sql_dummy (etc,send_time) values (?,?)
Method start : executeUpdate Class name : com,zaxxer,hikari,pool,HikariProxyPreparedStatement
Method start : executeUpdate Class name : com,zaxxer,hikari,pool,ProxyPreparedStatement
2024-02-06T16:58:36.434+09:00 WARN 19760 --- [nio-8080-exec-4] c.d.j.restapi.service.SqlServiceImpl : SQL insert, nowTime = 2024-02-06T16:58:36.434740600
Hibernate: insert into sql_dummy (etc,send_time) values (?,?)
Method start : executeUpdate Class name : com,zaxxer,hikari,pool,HikariProxyPreparedStatement
Method start : executeUpdate Class name : com,zaxxer,hikari,pool,ProxyPreparedStatement
2024-02-06T16:58:41.123+09:00 INFO 19760 --- [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2024-02-06T16:58:41.126+09:00 INFO 19760 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 -
로그 확인시 executeUpdate 메서드가 ProxyPreparedStatement와 HikariProxyPreparedStatement에서 각각 호출되고 있음을 알 수 있다.
이것의 이유는 다음과 같다.
- 프록시 체이닝
HikariProxyPreparedStatement는 ProxyPreparedStatement를 상속받아 구현된다고 한다.
그렇기에 executeUpdate가 호출되면, 먼저 HikariProxyPreparedStatement의 오버라이드된 메서드가 호출되고, 이 메서드 내부에서 super.executeUpdate() 형식으로 부모 클래스인 ProxyPreparedStatement의 executeUpdate 메서드를 명시적으로 호출된다.
- 데코레이터 패턴
HikariCP는 데코레이터 패턴을 사용하여 JDBC 커넥션과 관련된 클래스들을 래핑할 수 있다. 이 패턴은 각각의 프록시 클래스가 동작을 수행한 후 다음 프록시 또는 실제 대상 객체로 작업을 전달하는 방식으로 작동다.
한계
하지만 이는 SQL 쿼리문을 직접 볼 수는 없다.
표준 라이브러리에서 실쿼리인 executeQuery()를 확인할 수 있는 방법이 없기 때문이다. 이를 위해 다른 방법을 고민해야 한다.
이슈
생성자 super 선언 :
super.visit(version, access, name, signature, superName, interfaces); .. 등
super를 호출하지 않는다면, ASM이 내부적으로 수행해야 하는 작업들이 실행되지 않아 방문자 패턴(visitor pattern)이 제대로 동작하지 않을 수 있다.
이는 visitor가 방문자 패턴이자, 체이닝 방식으로 동작하기 때문인데, 한 방문자(visitor)가 작업을 수행한 후 다음 방문자로 작업을 넘겨주는 방식으로 설계되어 있다.
super를 호출하지 않으면, 체인 중 다음 방문자로의 작업 전달이 누락되어 문제가 발생할 수 있다.
'프로젝트 > APM MiddleWare' 카테고리의 다른 글
작업 중 발생한 Redis 동시성 문제 발생과 처리 (1) | 2024.09.03 |
---|---|
[Agent / ASM] agent의 transform 메서드에 ASM 바이트코드 적용하기 2 / ASM 라이브러리 이슈 (0) | 2024.04.04 |
[Agent] java.lang.management (0) | 2024.04.03 |
[Agent] Agent 동작 구성 및 이해 (0) | 2024.04.03 |
[A Java bytecode engineering library]chap02 실습파일 (0) | 2024.04.02 |