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.
449 lines
18 KiB
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");
|
|
}
|
|
}
|
|
} |