You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
clean-parking/src/main/java/egovframework/config/MyBatisQueryInterceptor.java

449 lines
18 KiB
Java

package egovframework.config;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.delete.Delete;
import net.sf.jsqlparser.statement.insert.Insert;
import net.sf.jsqlparser.statement.select.Join;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.Select;
import net.sf.jsqlparser.statement.select.SelectItem;
import net.sf.jsqlparser.statement.update.Update;
import net.sf.jsqlparser.statement.update.UpdateSet;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* MyBatis Query Interceptor
*
* This interceptor logs detailed information about SQL queries executed by MyBatis,
* including the original SQL, actual SQL with parameters, and parameter values.
*/
@Component
@Profile({"local", "dev"}) // local과 dev 프로파일에서만 활성화
@Intercepts({
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
@Slf4j
public class MyBatisQueryInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
Object parameter = invocation.getArgs()[1];
BoundSql boundSql = ms.getBoundSql(parameter);
String sql = boundSql.getSql();
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
// 실제 실행될 쿼리 추출 (MyBatis 내부에서 처리된 결과)
String actualSql = getActualSql(boundSql, parameter);
// 파라미터 정보 추출
Map<String, Object> paramMap = extractDetailedParameters(parameter, parameterMappings);
logDetailedQueryInfo(ms.getId(), sql, actualSql, paramMap);
// 원래 쿼리 실행
long startTime = System.currentTimeMillis();
Object result = invocation.proceed();
long endTime = System.currentTimeMillis();
// 쿼리 실행 시간 로깅
log.debug("Query execution time: {} ms", (endTime - startTime));
return result;
}
private String getActualSql(BoundSql boundSql, Object parameter) {
String sql = boundSql.getSql();
if (parameter == null) {
return sql;
}
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings.isEmpty()) {
return sql;
}
// SQL 복사본 생성
String actualSql = sql;
try {
// 파라미터 값 추출
for (ParameterMapping parameterMapping : parameterMappings) {
String propertyName = parameterMapping.getProperty();
Object value = null;
if (boundSql.hasAdditionalParameter(propertyName)) {
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameter instanceof Map) {
value = ((Map<?, ?>) parameter).get(propertyName);
} else if (parameter instanceof String) {
// String 타입 파라미터 처리
value = parameter;
} else if (parameter instanceof Number) {
// Number 타입 파라미터 처리 (Integer, Long, Double 등)
value = parameter;
} else if (parameter instanceof Boolean) {
// Boolean 타입 파라미터 처리
value = parameter;
} else {
value = getParameterValue(parameter, propertyName);
}
String valueStr = value != null ? value.toString() : "null";
// SQL 인젝션 방지를 위한 문자열 이스케이프 처리
valueStr = valueStr.replace("'", "''");
// 다양한 데이터 타입에 대한 처리
if (value instanceof String) {
// 문자열 타입은 따옴표로 감싸기
actualSql = actualSql.replaceFirst("\\?", "'" + valueStr + "'");
} else if (value instanceof Number) {
// 숫자 타입 (Integer, Long, Double 등)은 그대로 사용
actualSql = actualSql.replaceFirst("\\?", valueStr);
} else if (value instanceof Boolean) {
// Boolean 타입은 그대로 사용
actualSql = actualSql.replaceFirst("\\?", valueStr);
} else if (value instanceof java.util.Date) {
// 표준 타임스탬프 포맷 사용
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
actualSql = actualSql.replaceFirst("\\?", "'" + sdf.format(value) + "'");
} else if (value instanceof java.time.LocalDateTime) {
// Java 8 LocalDateTime 처리
actualSql = actualSql.replaceFirst("\\?", "'" + value.toString().replace('T', ' ') + "'");
} else {
// 기타 타입은 null이 아닌 경우 따옴표로 감싸기
actualSql = actualSql.replaceFirst("\\?", value != null ? "'" + valueStr + "'" : valueStr);
}
}
} catch (Exception e) {
log.warn("Failed to get actual SQL with parameters", e);
return sql;
}
return actualSql;
}
private Object getParameterValue(Object parameter, String propertyName) {
try {
// 현재 클래스부터 상위 클래스까지 순회하면서 필드 찾기
Class<?> currentClass = parameter.getClass();
while (currentClass != null) {
try {
Field field = currentClass.getDeclaredField(propertyName);
field.setAccessible(true);
return field.get(parameter);
} catch (NoSuchFieldException e) {
// 현재 클래스에서 필드를 찾지 못하면 상위 클래스로 이동
currentClass = currentClass.getSuperclass();
}
}
throw new NoSuchFieldException(propertyName);
} catch (Exception e) {
log.warn("필드 값 추출 실패: {} ({})", propertyName, e.getMessage());
return null;
}
}
private Map<String, Object> extractDetailedParameters(Object parameter, List<ParameterMapping> parameterMappings) {
Map<String, Object> paramMap = new HashMap<>();
if (parameter == null) {
return paramMap;
}
if (parameter instanceof Map) {
paramMap.putAll((Map<String, Object>) parameter);
} else {
// 객체의 필드 정보도 추출
paramMap.put("param", parameter);
extractObjectFields(parameter, paramMap);
}
return paramMap;
}
private void extractObjectFields(Object obj, Map<String, Object> paramMap) {
try {
Class<?> currentClass = obj.getClass();
while (currentClass != null) {
Field[] fields = currentClass.getDeclaredFields();
for (Field field : fields) {
try {
// String 타입의 객체는 건너뛰기
if (obj instanceof String) {
continue;
}
// 시스템 클래스의 필드는 건너뛰기
if (field.getDeclaringClass().getName().startsWith("java.")) {
continue;
}
field.setAccessible(true);
Object value = field.get(obj);
paramMap.put(field.getName(), value);
} catch (IllegalAccessException | SecurityException e) {
// 개별 필드 접근 실패는 무시하고 계속 진행
log.debug("필드 접근 실패: {} ({})", field.getName(), e.getMessage());
}
}
currentClass = currentClass.getSuperclass();
}
} catch (Exception e) {
log.warn("객체 필드 추출 실패", e);
}
}
private void logDetailedQueryInfo(String mapperMethod, String originalSql, String actualSql, Map<String, Object> paramMap) {
StringBuilder logMessage = new StringBuilder();
logMessage.append("\n");
logMessage.append("┌─────────────── MyBatis Query Details ───────────────\n");
logMessage.append("│ Mapper Method: ").append(mapperMethod).append("\n");
logMessage.append("│ Parameters: ").append(paramMap).append("\n");
// 원본 SQL 포맷팅, prd, 운영
//logMessage.append("│ Original SQL:\n");
//formatSqlInLog(logMessage, originalSql);
// 실제 실행 SQL 포맷팅, local, dev 로컬 개발
logMessage.append("│ Actual SQL:\n");
formatSqlInLog(logMessage, actualSql);
logMessage.append("└──────────────────────────────────────────────────────");
log.info(logMessage.toString());
}
private void formatSqlInLog(StringBuilder logMessage, String sql) {
// SQL 키워드 하이라이트 및 들여쓰기
String formattedSql = formatSql(sql);
String[] lines = formattedSql.split("\n");
for (String line : lines) {
//logMessage.append("│ ").append(line).append("\n");
logMessage.append(line).append("\n");
}
}
private String formatSql(String sql) {
try {
// SQL 파서를 사용한 포맷팅
Statement statement = CCJSqlParserUtil.parse(sql);
return formatStatement(statement, 0);
} catch (JSQLParserException e) {
log.debug("SQL 파싱 실패. 기본 포맷팅으로 대체합니다.", e);
log.info("SQL 파싱 실패. 기본 포맷팅으로 대체합니다.");
// 파싱 실패 시 기본 포맷팅 사용
return sql.replaceAll("\\s+", " ").trim();
}
}
private String formatStatement(Statement statement, int indent) {
StringBuilder result = new StringBuilder();
String indentation = String.join("", java.util.Collections.nCopies(indent, " "));
if (statement instanceof Select) {
formatSelect((Select) statement, result, indent);
} else if (statement instanceof Insert) {
formatInsert((Insert) statement, result, indent);
} else if (statement instanceof Update) {
formatUpdate((Update) statement, result, indent);
} else if (statement instanceof Delete) {
formatDelete((Delete) statement, result, indent);
}
return result.toString();
}
private void formatSelect(Select select, StringBuilder result, int indent) {
String indentation = String.join("", Collections.nCopies(indent, " "));
PlainSelect plainSelect = (PlainSelect) select.getSelectBody();
// SELECT 절
result.append(indentation).append("SELECT\n");
formatSelectItems(plainSelect.getSelectItems(), result, indent + 2);
// FROM 절
if (plainSelect.getFromItem() != null) {
result.append(indentation).append("FROM\n");
result.append(indentation).append(" ").append(plainSelect.getFromItem()).append("\n");
}
// JOIN 절
if (plainSelect.getJoins() != null) {
for (Join join : plainSelect.getJoins()) {
result.append(indentation).append(join.isLeft() ? "LEFT JOIN " : "JOIN ")
.append(join.getRightItem()).append("\n");
if (join.getOnExpression() != null) {
result.append(indentation).append(" ON ").append(join.getOnExpression()).append("\n");
}
}
}
// WHERE 절
if (plainSelect.getWhere() != null) {
result.append(indentation).append("WHERE\n");
result.append(indentation).append(" ").append(plainSelect.getWhere()).append("\n");
}
// GROUP BY 절
if (plainSelect.getGroupBy() != null) {
result.append(indentation).append("GROUP BY\n");
result.append(indentation).append(" ")
.append(plainSelect.getGroupBy().getGroupByExpressions().stream()
.map(Object::toString)
.collect(Collectors.joining(", ")))
.append("\n");
}
// HAVING 절
if (plainSelect.getHaving() != null) {
result.append(indentation).append("HAVING\n");
result.append(indentation).append(" ").append(plainSelect.getHaving()).append("\n");
}
// ORDER BY 절
if (plainSelect.getOrderByElements() != null) {
result.append(indentation).append("ORDER BY\n");
result.append(indentation).append(" ")
.append(plainSelect.getOrderByElements().stream()
.map(Object::toString)
.collect(Collectors.joining(", ")))
.append("\n");
}
}
private void formatSelectItems(List<SelectItem> items, StringBuilder result, int indent) {
String indentation = String.join("", Collections.nCopies(indent, " "));
for (int i = 0; i < items.size(); i++) {
result.append(indentation).append(items.get(i));
if (i < items.size() - 1) {
result.append(",");
}
result.append("\n");
}
}
private void formatInsert(Insert insert, StringBuilder result, int indent) {
String indentation = String.join("", Collections.nCopies(indent, " "));
result.append(indentation).append("INSERT INTO ").append(insert.getTable()).append("\n");
// 컬럼 목록
if (insert.getColumns() != null) {
result.append(indentation).append("(")
.append(insert.getColumns().stream()
.map(Column::getColumnName)
.collect(Collectors.joining(", ")))
.append(")\n");
}
result.append(indentation).append("VALUES\n");
// VALUES 절 포맷팅
if (insert.getItemsList() != null) {
result.append(indentation).append(" ").append(insert.getItemsList()).append("\n");
}
}
private void formatUpdate(Update update, StringBuilder result, int indent) {
String indentation = String.join("", Collections.nCopies(indent, " "));
// UPDATE 절
result.append(indentation).append("UPDATE\n");
result.append(indentation).append(" ").append(update.getTable()).append("\n");
// SET 절
result.append(indentation).append("SET\n");
List<UpdateSet> updateSets = update.getUpdateSets();
for (int i = 0; i < updateSets.size(); i++) {
UpdateSet updateSet = updateSets.get(i);
result.append(indentation).append(" ")
.append(updateSet.getColumns().get(0))
.append(" = ")
.append(updateSet.getExpressions().get(0));
if (i < updateSets.size() - 1) {
result.append(",");
}
result.append("\n");
}
// WHERE 절
if (update.getWhere() != null) {
result.append(indentation).append("WHERE\n");
result.append(indentation).append(" ").append(update.getWhere()).append("\n");
}
// ORDER BY 절 (일부 데이터베이스에서 지원)
if (update.getOrderByElements() != null) {
result.append(indentation).append("ORDER BY\n");
result.append(indentation).append(" ")
.append(update.getOrderByElements().stream()
.map(Object::toString)
.collect(Collectors.joining(", ")))
.append("\n");
}
// LIMIT 절 (일부 데이터베이스에서 지원)
if (update.getLimit() != null) {
result.append(indentation).append("LIMIT ")
.append(update.getLimit()).append("\n");
}
}
private void formatDelete(Delete delete, StringBuilder result, int indent) {
String indentation = String.join("", Collections.nCopies(indent, " "));
// DELETE 절
result.append(indentation).append("DELETE FROM\n");
result.append(indentation).append(" ").append(delete.getTable()).append("\n");
// WHERE 절
if (delete.getWhere() != null) {
result.append(indentation).append("WHERE\n");
result.append(indentation).append(" ").append(delete.getWhere()).append("\n");
}
// ORDER BY 절 (일부 데이터베이스에서 지원)
if (delete.getOrderByElements() != null) {
result.append(indentation).append("ORDER BY\n");
result.append(indentation).append(" ")
.append(delete.getOrderByElements().stream()
.map(Object::toString)
.collect(Collectors.joining(", ")))
.append("\n");
}
// LIMIT 절 (일부 데이터베이스에서 지원)
if (delete.getLimit() != null) {
result.append(indentation).append("LIMIT ")
.append(delete.getLimit()).append("\n");
}
}
}