|
|
|
|
@ -13,7 +13,6 @@ 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.binding.MapperMethod;
|
|
|
|
|
import org.apache.ibatis.executor.Executor;
|
|
|
|
|
import org.apache.ibatis.mapping.BoundSql;
|
|
|
|
|
import org.apache.ibatis.mapping.MappedStatement;
|
|
|
|
|
@ -24,39 +23,25 @@ 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.text.SimpleDateFormat;
|
|
|
|
|
import java.time.format.DateTimeFormatter;
|
|
|
|
|
import java.util.Collection;
|
|
|
|
|
import java.util.Collections;
|
|
|
|
|
import java.util.Date;
|
|
|
|
|
import java.util.HashMap;
|
|
|
|
|
import java.util.LinkedHashMap;
|
|
|
|
|
import java.util.List;
|
|
|
|
|
import java.util.Map;
|
|
|
|
|
import java.util.regex.Matcher;
|
|
|
|
|
import java.util.regex.Pattern;
|
|
|
|
|
import java.util.stream.Collectors;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* MyBatis 쿼리 인터셉터
|
|
|
|
|
*
|
|
|
|
|
* 이 인터셉터는 MyBatis에서 실행되는 SQL 쿼리를 가로채서 상세 정보를 로깅합니다.
|
|
|
|
|
* 원본 SQL, 실제 실행될 SQL(파라미터 값 포함), 파라미터 값 등을 로그에 기록합니다.
|
|
|
|
|
* local과 dev 프로파일에서만 활성화됩니다.
|
|
|
|
|
*
|
|
|
|
|
* 주요 기능:
|
|
|
|
|
* - SQL 쿼리 로깅
|
|
|
|
|
* - 파라미터 값 추출 및 로깅
|
|
|
|
|
* - 쿼리 실행 시간 측정
|
|
|
|
|
* - include SQL 내부 파라미터 처리
|
|
|
|
|
* - 다양한 데이터 타입 지원 (String, Number, Boolean, Date, LocalDateTime 등)
|
|
|
|
|
* MyBatis Query Interceptor
|
|
|
|
|
*
|
|
|
|
|
* datasource-proxy와 함께 동작하여 상세한 쿼리 분석을 제공
|
|
|
|
|
* - 동적 쿼리의 처리 과정 분석
|
|
|
|
|
* - include된 복잡한 쿼리의 상세 정보
|
|
|
|
|
* - 파라미터 바인딩 전후 비교
|
|
|
|
|
*/
|
|
|
|
|
@Component
|
|
|
|
|
@Profile({"local", "dev"}) // local과 dev 프로파일에서만 활성화
|
|
|
|
|
//@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})
|
|
|
|
|
@ -64,299 +49,105 @@ import java.util.stream.Collectors;
|
|
|
|
|
@Slf4j
|
|
|
|
|
public class MyBatisQueryInterceptor implements Interceptor {
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 인터셉터를 대상 객체에 연결합니다.
|
|
|
|
|
*
|
|
|
|
|
* @param target 대상 객체
|
|
|
|
|
* @return 프록시 객체
|
|
|
|
|
*/
|
|
|
|
|
@Override
|
|
|
|
|
public Object plugin(Object target) {
|
|
|
|
|
return org.apache.ibatis.plugin.Plugin.wrap(target, this);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 인터셉터 속성을 설정합니다.
|
|
|
|
|
*
|
|
|
|
|
* @param properties 속성
|
|
|
|
|
*/
|
|
|
|
|
@Override
|
|
|
|
|
public void setProperties(java.util.Properties properties) {
|
|
|
|
|
// 필요한 경우 속성 설정
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* MyBatis 쿼리 실행을 가로채서 로깅하고 원래 쿼리를 실행합니다.
|
|
|
|
|
* include SQL 내부의 파라미터도 올바르게 처리합니다.
|
|
|
|
|
*
|
|
|
|
|
* @param invocation MyBatis 인터셉터 호출 정보
|
|
|
|
|
* @return 쿼리 실행 결과
|
|
|
|
|
* @throws Throwable 쿼리 실행 중 발생한 예외
|
|
|
|
|
*/
|
|
|
|
|
@Override
|
|
|
|
|
public Object intercept(Invocation invocation) throws Throwable {
|
|
|
|
|
// 로깅 레벨 확인으로 불필요한 처리 방지
|
|
|
|
|
boolean isDebugEnabled = log.isDebugEnabled();
|
|
|
|
|
boolean isInfoEnabled = log.isInfoEnabled();
|
|
|
|
|
|
|
|
|
|
if (!isInfoEnabled && !isDebugEnabled) {
|
|
|
|
|
// 로깅이 비활성화된 경우 바로 원래 쿼리 실행
|
|
|
|
|
return invocation.proceed();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
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();
|
|
|
|
|
|
|
|
|
|
// 쿼리 실행 시간 로깅
|
|
|
|
|
if (isDebugEnabled) {
|
|
|
|
|
log.debug("쿼리 실행 시간: {} ms", (endTime - startTime));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
// 인터셉터 내부 오류는 로깅하고 원래 쿼리 실행 (인터셉터 오류로 쿼리가 실패하지 않도록)
|
|
|
|
|
log.warn("쿼리 인터셉터 처리 중 오류 발생: {}", e.getMessage());
|
|
|
|
|
if (isDebugEnabled) {
|
|
|
|
|
log.debug("인터셉터 오류 상세 정보:", e);
|
|
|
|
|
}
|
|
|
|
|
return invocation.proceed();
|
|
|
|
|
}
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 실제 실행될 SQL 쿼리를 생성합니다.
|
|
|
|
|
* 원본 SQL의 '?' 플레이스홀더를 실제 파라미터 값으로 대체합니다.
|
|
|
|
|
* include SQL 내부의 파라미터도 올바르게 처리합니다.
|
|
|
|
|
* foreach 구문에서 컬렉션이 비어있거나 null인 경우 특별 처리합니다.
|
|
|
|
|
*/
|
|
|
|
|
private String getActualSql(BoundSql boundSql, Object parameter) {
|
|
|
|
|
String sql = boundSql.getSql();
|
|
|
|
|
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
|
|
|
|
|
|
|
|
|
|
if (parameterMappings == null || parameterMappings.isEmpty()) {
|
|
|
|
|
|
|
|
|
|
if (parameter == null) {
|
|
|
|
|
return sql;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SQL에서 실제 파라미터 바인딩 위치 확인
|
|
|
|
|
Pattern pattern = Pattern.compile("\\?");
|
|
|
|
|
Matcher matcher = pattern.matcher(sql);
|
|
|
|
|
StringBuffer result = new StringBuffer();
|
|
|
|
|
int paramIndex = 0;
|
|
|
|
|
|
|
|
|
|
// IN 절 패턴 (대소문자 구분 없이)
|
|
|
|
|
Pattern inClausePattern = Pattern.compile("\\s+IN\\s*\\(\\s*\\?\\s*\\)", Pattern.CASE_INSENSITIVE);
|
|
|
|
|
Matcher inClauseMatcher = inClausePattern.matcher(sql);
|
|
|
|
|
Map<Integer, Boolean> inClausePositions = new HashMap<>();
|
|
|
|
|
|
|
|
|
|
// IN 절 위치 찾기
|
|
|
|
|
while (inClauseMatcher.find()) {
|
|
|
|
|
// 해당 위치의 ? 인덱스 찾기
|
|
|
|
|
String subSql = sql.substring(0, inClauseMatcher.end());
|
|
|
|
|
int questionMarkCount = 0;
|
|
|
|
|
for (int i = 0; i < subSql.length(); i++) {
|
|
|
|
|
if (subSql.charAt(i) == '?') {
|
|
|
|
|
questionMarkCount++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// 인덱스는 0부터 시작하므로 -1
|
|
|
|
|
inClausePositions.put(questionMarkCount - 1, true);
|
|
|
|
|
|
|
|
|
|
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
|
|
|
|
|
|
|
|
|
|
if (parameterMappings.isEmpty()) {
|
|
|
|
|
return sql;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
while (matcher.find() && paramIndex < parameterMappings.size()) {
|
|
|
|
|
ParameterMapping parameterMapping = parameterMappings.get(paramIndex);
|
|
|
|
|
Object value = null;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// include SQL 내부의 파라미터 처리 개선
|
|
|
|
|
if (parameter instanceof MapperMethod.ParamMap) {
|
|
|
|
|
MapperMethod.ParamMap<?> paramMap = (MapperMethod.ParamMap<?>) parameter;
|
|
|
|
|
String propertyName = parameterMapping.getProperty();
|
|
|
|
|
|
|
|
|
|
// 직접 키로 접근
|
|
|
|
|
if (paramMap.containsKey(propertyName)) {
|
|
|
|
|
value = paramMap.get(propertyName);
|
|
|
|
|
}
|
|
|
|
|
// param1, param2 등으로 접근
|
|
|
|
|
else if (propertyName.matches("param\\d+")) {
|
|
|
|
|
value = paramMap.get(propertyName);
|
|
|
|
|
}
|
|
|
|
|
// 중첩 객체 내부 필드 접근
|
|
|
|
|
else if (propertyName.contains(".")) {
|
|
|
|
|
value = getNestedParameterValue(paramMap, propertyName);
|
|
|
|
|
}
|
|
|
|
|
// VO 객체 내부 필드 접근
|
|
|
|
|
else {
|
|
|
|
|
for (Map.Entry<String, ?> entry : paramMap.entrySet()) {
|
|
|
|
|
if (entry.getValue() != null) {
|
|
|
|
|
Object nestedValue = getParameterValue(entry.getValue(), propertyName);
|
|
|
|
|
if (nestedValue != null) {
|
|
|
|
|
value = nestedValue;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
value = getParameterValue(parameter, parameterMapping.getProperty());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// IN 절 특별 처리
|
|
|
|
|
boolean isInClause = inClausePositions.containsKey(paramIndex);
|
|
|
|
|
|
|
|
|
|
// IN 절이고 컬렉션이 비어있거나 null인 경우 특별 처리
|
|
|
|
|
if (isInClause) {
|
|
|
|
|
if (value == null) {
|
|
|
|
|
// IN (NULL) 대신 IN (0)으로 대체 (항상 false가 되도록)
|
|
|
|
|
matcher.appendReplacement(result, "0");
|
|
|
|
|
log.debug("IN 절 NULL 파라미터 감지: 0으로 대체함 (파라미터: {})", parameterMapping.getProperty());
|
|
|
|
|
} else if (value instanceof Collection && ((Collection<?>) value).isEmpty()) {
|
|
|
|
|
// 빈 컬렉션인 경우 IN (0)으로 대체
|
|
|
|
|
matcher.appendReplacement(result, "0");
|
|
|
|
|
log.debug("IN 절 빈 컬렉션 감지: 0으로 대체함 (파라미터: {})", parameterMapping.getProperty());
|
|
|
|
|
} else if (value.getClass().isArray() && java.lang.reflect.Array.getLength(value) == 0) {
|
|
|
|
|
// 빈 배열인 경우 IN (0)으로 대체
|
|
|
|
|
matcher.appendReplacement(result, "0");
|
|
|
|
|
log.debug("IN 절 빈 배열 감지: 0으로 대체함 (파라미터: {})", parameterMapping.getProperty());
|
|
|
|
|
} else {
|
|
|
|
|
// 정상적인 컬렉션/배열인 경우 일반 처리
|
|
|
|
|
String replacement = formatParameterValue(value);
|
|
|
|
|
matcher.appendReplacement(result, Matcher.quoteReplacement(replacement));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SQL 복사본 생성
|
|
|
|
|
String actualSql = sql;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 파라미터 값 추출
|
|
|
|
|
for (ParameterMapping parameterMapping : parameterMappings) {
|
|
|
|
|
String propertyName = parameterMapping.getProperty();
|
|
|
|
|
Object value;
|
|
|
|
|
|
|
|
|
|
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 {
|
|
|
|
|
// 일반 파라미터 처리
|
|
|
|
|
String replacement = formatParameterValue(value);
|
|
|
|
|
matcher.appendReplacement(result, Matcher.quoteReplacement(replacement));
|
|
|
|
|
value = getParameterValue(parameter, propertyName);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
log.debug("파라미터 값 추출 실패: {}, 기본값 사용", parameterMapping.getProperty());
|
|
|
|
|
matcher.appendReplacement(result, "?");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
paramIndex++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
matcher.appendTail(result);
|
|
|
|
|
return result.toString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 중첩된 파라미터 값을 추출합니다.
|
|
|
|
|
*/
|
|
|
|
|
private Object getNestedParameterValue(MapperMethod.ParamMap<?> paramMap, String propertyPath) {
|
|
|
|
|
String[] parts = propertyPath.split("\\.");
|
|
|
|
|
Object current = null;
|
|
|
|
|
|
|
|
|
|
// 첫 번째 키로 객체 찾기
|
|
|
|
|
for (Map.Entry<String, ?> entry : paramMap.entrySet()) {
|
|
|
|
|
if (entry.getKey().equals(parts[0]) ||
|
|
|
|
|
(entry.getValue() != null && entry.getValue().getClass().getSimpleName().toLowerCase().contains(parts[0].toLowerCase()))) {
|
|
|
|
|
current = entry.getValue();
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 중첩 필드 접근
|
|
|
|
|
for (int i = 1; i < parts.length && current != null; i++) {
|
|
|
|
|
current = getParameterValue(current, parts[i]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return current;
|
|
|
|
|
}
|
|
|
|
|
String valueStr = value != null ? value.toString() : "null";
|
|
|
|
|
// SQL 인젝션 방지를 위한 문자열 이스케이프 처리
|
|
|
|
|
valueStr = valueStr.replace("'", "''");
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 파라미터 값을 SQL용 문자열로 포맷팅합니다.
|
|
|
|
|
* 컬렉션이 비어있거나 null인 경우 특별 처리합니다.
|
|
|
|
|
*/
|
|
|
|
|
private String formatParameterValue(Object value) {
|
|
|
|
|
if (value == null) {
|
|
|
|
|
return "NULL";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 컬렉션 타입 처리 (List, Set, Array 등)
|
|
|
|
|
if (value instanceof Collection) {
|
|
|
|
|
Collection<?> collection = (Collection<?>) value;
|
|
|
|
|
if (collection.isEmpty()) {
|
|
|
|
|
// 빈 컬렉션인 경우 '0'을 반환하여 IN (0)으로 만듦
|
|
|
|
|
// 이는 항상 false가 되어 결과가 없음을 보장함 (어떤 ID도 0과 일치하지 않음)
|
|
|
|
|
return "0";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 컬렉션 요소를 쉼표로 구분된 문자열로 변환
|
|
|
|
|
return collection.stream()
|
|
|
|
|
.map(this::formatParameterValue)
|
|
|
|
|
.collect(Collectors.joining(", "));
|
|
|
|
|
} else if (value.getClass().isArray()) {
|
|
|
|
|
// 배열 타입 처리
|
|
|
|
|
Object[] array = convertToObjectArray(value);
|
|
|
|
|
if (array.length == 0) {
|
|
|
|
|
// 빈 배열인 경우 '0'을 반환
|
|
|
|
|
return "0";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
StringBuilder sb = new StringBuilder();
|
|
|
|
|
for (int i = 0; i < array.length; i++) {
|
|
|
|
|
if (i > 0) {
|
|
|
|
|
sb.append(", ");
|
|
|
|
|
// 다양한 데이터 타입에 대한 처리
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
sb.append(formatParameterValue(array[i]));
|
|
|
|
|
}
|
|
|
|
|
return sb.toString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (value instanceof String) {
|
|
|
|
|
return "'" + value.toString().replace("'", "''") + "'";
|
|
|
|
|
} else if (value instanceof Number || value instanceof Boolean) {
|
|
|
|
|
return value.toString();
|
|
|
|
|
} else if (value instanceof Date) {
|
|
|
|
|
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
|
|
|
|
return "'" + sdf.format((Date) value) + "'";
|
|
|
|
|
} else if (value instanceof java.time.LocalDateTime) {
|
|
|
|
|
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
|
|
|
|
return "'" + ((java.time.LocalDateTime) value).format(formatter) + "'";
|
|
|
|
|
} else if (value instanceof java.time.LocalDate) {
|
|
|
|
|
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
|
|
|
|
return "'" + ((java.time.LocalDate) value).format(formatter) + "'";
|
|
|
|
|
} else {
|
|
|
|
|
return "'" + value.toString().replace("'", "''") + "'";
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
log.warn("Failed to get actual SQL with parameters", e);
|
|
|
|
|
return sql;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 배열 객체를 Object[] 타입으로 변환합니다.
|
|
|
|
|
*/
|
|
|
|
|
private Object[] convertToObjectArray(Object array) {
|
|
|
|
|
if (array instanceof Object[]) {
|
|
|
|
|
return (Object[]) array;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 기본 타입 배열 처리
|
|
|
|
|
int length = java.lang.reflect.Array.getLength(array);
|
|
|
|
|
Object[] result = new Object[length];
|
|
|
|
|
for (int i = 0; i < length; i++) {
|
|
|
|
|
result[i] = java.lang.reflect.Array.get(array, i);
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
return actualSql;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Object getParameterValue(Object parameter, String propertyName) {
|
|
|
|
|
@ -380,308 +171,113 @@ private Object getParameterValue(Object parameter, String propertyName) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 파라미터 객체에서 상세 정보를 추출하여 맵으로 반환합니다.
|
|
|
|
|
* include SQL 내부의 파라미터도 올바르게 추출합니다.
|
|
|
|
|
*/
|
|
|
|
|
private Map<String, Object> extractDetailedParameters(Object parameter, List<ParameterMapping> parameterMappings) {
|
|
|
|
|
Map<String, Object> paramMap = new LinkedHashMap<>();
|
|
|
|
|
|
|
|
|
|
if (parameter == null || parameterMappings == null || parameterMappings.isEmpty()) {
|
|
|
|
|
Map<String, Object> paramMap = new HashMap<>();
|
|
|
|
|
|
|
|
|
|
if (parameter == null) {
|
|
|
|
|
return paramMap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 실제 사용되는 파라미터만 추출
|
|
|
|
|
for (ParameterMapping parameterMapping : parameterMappings) {
|
|
|
|
|
String propertyName = parameterMapping.getProperty();
|
|
|
|
|
Object value = null;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
if (parameter instanceof MapperMethod.ParamMap) {
|
|
|
|
|
MapperMethod.ParamMap<?> mapperParamMap = (MapperMethod.ParamMap<?>) parameter;
|
|
|
|
|
|
|
|
|
|
// 1. 직접 키로 접근
|
|
|
|
|
if (mapperParamMap.containsKey(propertyName)) {
|
|
|
|
|
value = mapperParamMap.get(propertyName);
|
|
|
|
|
}
|
|
|
|
|
// 2. 중첩 객체 내부 필드 접근 (include SQL에서 자주 발생)
|
|
|
|
|
else if (propertyName.contains(".")) {
|
|
|
|
|
value = getNestedParameterValue(mapperParamMap, propertyName);
|
|
|
|
|
}
|
|
|
|
|
// 3. VO 객체 내부 검색
|
|
|
|
|
else {
|
|
|
|
|
for (Map.Entry<String, ?> entry : mapperParamMap.entrySet()) {
|
|
|
|
|
if (entry.getValue() != null) {
|
|
|
|
|
Object nestedValue = getParameterValue(entry.getValue(), propertyName);
|
|
|
|
|
if (nestedValue != null) {
|
|
|
|
|
value = nestedValue;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4. 전체 맵 내용도 포함 (디버깅용)
|
|
|
|
|
paramMap.putAll(mapperParamMap);
|
|
|
|
|
} else {
|
|
|
|
|
value = getParameterValue(parameter, propertyName);
|
|
|
|
|
extractObjectFields(parameter, paramMap);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (value != null) {
|
|
|
|
|
paramMap.put(propertyName, value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
log.debug("파라미터 추출 실패: {} - {}", propertyName, e.getMessage());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (parameter instanceof Map) {
|
|
|
|
|
paramMap.putAll((Map<String, Object>) parameter);
|
|
|
|
|
} else {
|
|
|
|
|
// 객체의 필드 정보도 추출
|
|
|
|
|
paramMap.put("param", parameter);
|
|
|
|
|
extractObjectFields(parameter, paramMap);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return paramMap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 객체의 모든 필드를 추출하여 맵에 추가합니다.
|
|
|
|
|
* include SQL 내부의 파라미터도 올바르게 추출합니다.
|
|
|
|
|
*
|
|
|
|
|
* @param obj 필드를 추출할 객체
|
|
|
|
|
* @param paramMap 추출된 필드를 저장할 맵
|
|
|
|
|
*/
|
|
|
|
|
private void extractObjectFields(Object obj, Map<String, Object> paramMap) {
|
|
|
|
|
if (obj == null) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 기본 타입이나 래퍼 타입은 처리하지 않음
|
|
|
|
|
if (obj instanceof String || obj instanceof Number || obj instanceof Boolean ||
|
|
|
|
|
obj instanceof java.util.Date || obj instanceof java.time.temporal.Temporal) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
Class<?> currentClass = obj.getClass();
|
|
|
|
|
|
|
|
|
|
// 시스템 클래스는 처리하지 않음
|
|
|
|
|
if (currentClass.getName().startsWith("java.") ||
|
|
|
|
|
currentClass.getName().startsWith("javax.") ||
|
|
|
|
|
currentClass.getName().startsWith("sun.")) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 현재 클래스부터 상위 클래스까지 순회하면서 필드 추출
|
|
|
|
|
while (currentClass != null && !currentClass.getName().startsWith("java.")) {
|
|
|
|
|
while (currentClass != null) {
|
|
|
|
|
Field[] fields = currentClass.getDeclaredFields();
|
|
|
|
|
for (Field field : fields) {
|
|
|
|
|
try {
|
|
|
|
|
// static 또는 transient 필드는 건너뛰기
|
|
|
|
|
if (java.lang.reflect.Modifier.isStatic(field.getModifiers()) ||
|
|
|
|
|
java.lang.reflect.Modifier.isTransient(field.getModifiers())) {
|
|
|
|
|
// String 타입의 객체는 건너뛰기
|
|
|
|
|
if (obj instanceof String) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 시스템 클래스의 필드는 건너뛰기
|
|
|
|
|
if (field.getDeclaringClass().getName().startsWith("java.")) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
field.setAccessible(true);
|
|
|
|
|
Object value = field.get(obj);
|
|
|
|
|
|
|
|
|
|
// 값이 null이 아닌 경우만 맵에 추가
|
|
|
|
|
if (value != null) {
|
|
|
|
|
String fieldName = field.getName();
|
|
|
|
|
paramMap.put(fieldName, value);
|
|
|
|
|
|
|
|
|
|
// MANAGER_CD 특별 처리 (include SQL 내부에서도 적용)
|
|
|
|
|
if (fieldName.equalsIgnoreCase("currentUserId")) {
|
|
|
|
|
paramMap.put("MANAGER_CD", value);
|
|
|
|
|
paramMap.put("manager_cd", value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 중첩된 객체의 필드도 추출 (깊이 1까지만)
|
|
|
|
|
if (!(value instanceof Map) &&
|
|
|
|
|
!(value instanceof Collection) &&
|
|
|
|
|
!(value instanceof String) &&
|
|
|
|
|
!(value instanceof Number) &&
|
|
|
|
|
!(value instanceof Boolean) &&
|
|
|
|
|
!value.getClass().getName().startsWith("java.")) {
|
|
|
|
|
|
|
|
|
|
// 중첩된 객체의 필드를 추출하여 필드명.중첩필드명 형태로 저장
|
|
|
|
|
Class<?> nestedClass = value.getClass();
|
|
|
|
|
Field[] nestedFields = nestedClass.getDeclaredFields();
|
|
|
|
|
for (Field nestedField : nestedFields) {
|
|
|
|
|
try {
|
|
|
|
|
if (java.lang.reflect.Modifier.isStatic(nestedField.getModifiers()) ||
|
|
|
|
|
java.lang.reflect.Modifier.isTransient(nestedField.getModifiers())) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
nestedField.setAccessible(true);
|
|
|
|
|
Object nestedValue = nestedField.get(value);
|
|
|
|
|
if (nestedValue != null) {
|
|
|
|
|
String nestedFieldName = nestedField.getName();
|
|
|
|
|
paramMap.put(nestedFieldName, nestedValue);
|
|
|
|
|
}
|
|
|
|
|
} catch (IllegalAccessException | SecurityException e) {
|
|
|
|
|
// 중첩 필드 접근 실패는 무시하고 계속 진행
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
paramMap.put(field.getName(), value);
|
|
|
|
|
|
|
|
|
|
} catch (IllegalAccessException | SecurityException e) {
|
|
|
|
|
// 개별 필드 접근 실패는 무시하고 계속 진행
|
|
|
|
|
if (log.isDebugEnabled()) {
|
|
|
|
|
log.debug("필드 접근 실패: {} ({})", field.getName(), e.getMessage());
|
|
|
|
|
}
|
|
|
|
|
log.debug("필드 접근 실패: {} ({})", field.getName(), e.getMessage());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
currentClass = currentClass.getSuperclass();
|
|
|
|
|
}
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
log.warn("객체 필드 추출 중 오류 발생: {}", e.getMessage());
|
|
|
|
|
if (log.isDebugEnabled()) {
|
|
|
|
|
log.debug("상세 오류 정보:", e);
|
|
|
|
|
}
|
|
|
|
|
log.warn("객체 필드 추출 실패", e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 쿼리 실행 정보를 상세하게 로깅합니다.
|
|
|
|
|
*
|
|
|
|
|
* @param mapperMethod 매퍼 메서드 이름
|
|
|
|
|
* @param originalSql 원본 SQL 쿼리
|
|
|
|
|
* @param actualSql 실제 실행될 SQL 쿼리 (파라미터 값이 포함된)
|
|
|
|
|
* @param paramMap 파라미터 정보가 담긴 맵
|
|
|
|
|
*/
|
|
|
|
|
private void logDetailedQueryInfo(String mapperMethod, String originalSql, String actualSql, Map<String, Object> paramMap) {
|
|
|
|
|
// 로깅 레벨 확인으로 불필요한 문자열 연산 방지
|
|
|
|
|
if (!log.isInfoEnabled()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
StringBuilder logMessage = new StringBuilder();
|
|
|
|
|
logMessage.append("\n");
|
|
|
|
|
logMessage.append("┌─────────────── MyBatis 쿼리 상세 정보 ───────────────\n");
|
|
|
|
|
logMessage.append("│ 매퍼 메서드: ").append(mapperMethod).append("\n");
|
|
|
|
|
|
|
|
|
|
// 파라미터 정보 로깅 (너무 길면 축약)
|
|
|
|
|
String paramString = paramMap.toString();
|
|
|
|
|
if (paramString.length() > 1000) {
|
|
|
|
|
paramString = paramString.substring(0, 997) + "...";
|
|
|
|
|
}
|
|
|
|
|
logMessage.append("│ 파라미터: ").append(paramString).append("\n");
|
|
|
|
|
|
|
|
|
|
// 실제 실행 SQL 포맷팅 (local, dev 환경에서만)
|
|
|
|
|
logMessage.append("│ 실행 SQL:\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());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* SQL 쿼리를 로그 메시지에 포맷팅하여 추가합니다.
|
|
|
|
|
*
|
|
|
|
|
* @param logMessage 로그 메시지 빌더
|
|
|
|
|
* @param sql 포맷팅할 SQL 쿼리
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
private void formatSqlInLog(StringBuilder logMessage, String sql) {
|
|
|
|
|
try {
|
|
|
|
|
// SQL 키워드 하이라이트 및 들여쓰기
|
|
|
|
|
String formattedSql = formatSql(sql);
|
|
|
|
|
String[] lines = formattedSql.split("\n");
|
|
|
|
|
// SQL 키워드 하이라이트 및 들여쓰기
|
|
|
|
|
String formattedSql = formatSql(sql);
|
|
|
|
|
String[] lines = formattedSql.split("\n");
|
|
|
|
|
|
|
|
|
|
for (String line : lines) {
|
|
|
|
|
logMessage.append("│ ").append(line).append("\n");
|
|
|
|
|
}
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
// 포맷팅 실패 시 원본 SQL 출력
|
|
|
|
|
logMessage.append("│ ").append(sql).append("\n");
|
|
|
|
|
log.debug("SQL 포맷팅 실패: {}", e.getMessage());
|
|
|
|
|
for (String line : lines) {
|
|
|
|
|
//logMessage.append("│ ").append(line).append("\n");
|
|
|
|
|
logMessage.append(line).append("\n");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* SQL 쿼리를 포맷팅하여 가독성을 높입니다.
|
|
|
|
|
*
|
|
|
|
|
* @param sql 포맷팅할 SQL 쿼리
|
|
|
|
|
* @return 포맷팅된 SQL 쿼리
|
|
|
|
|
*/
|
|
|
|
|
private String formatSql(String sql) {
|
|
|
|
|
if (sql == null || sql.trim().isEmpty()) {
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// SQL 파서를 사용한 포맷팅
|
|
|
|
|
Statement statement = CCJSqlParserUtil.parse(sql);
|
|
|
|
|
return formatStatement(statement, 0);
|
|
|
|
|
} catch (JSQLParserException e) {
|
|
|
|
|
log.debug("SQL 파싱 실패. 기본 포맷팅으로 대체합니다.", e);
|
|
|
|
|
log.info("SQL 파싱 실패. 기본 포맷팅으로 대체합니다.");
|
|
|
|
|
// 파싱 실패 시 기본 포맷팅 사용
|
|
|
|
|
if (log.isDebugEnabled()) {
|
|
|
|
|
log.debug("SQL 파싱 실패. 기본 포맷팅으로 대체합니다: {}", e.getMessage());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 기본 포맷팅: 키워드 대문자화 및 줄바꿈 추가
|
|
|
|
|
String formattedSql = sql.replaceAll("\\s+", " ").trim();
|
|
|
|
|
|
|
|
|
|
// 주요 SQL 키워드 앞에 줄바꿈 추가
|
|
|
|
|
formattedSql = formattedSql
|
|
|
|
|
.replaceAll("(?i)\\s+SELECT\\s+", "\nSELECT ")
|
|
|
|
|
.replaceAll("(?i)\\s+FROM\\s+", "\nFROM ")
|
|
|
|
|
.replaceAll("(?i)\\s+WHERE\\s+", "\nWHERE ")
|
|
|
|
|
.replaceAll("(?i)\\s+AND\\s+", "\n AND ")
|
|
|
|
|
.replaceAll("(?i)\\s+OR\\s+", "\n OR ")
|
|
|
|
|
.replaceAll("(?i)\\s+GROUP BY\\s+", "\nGROUP BY ")
|
|
|
|
|
.replaceAll("(?i)\\s+HAVING\\s+", "\nHAVING ")
|
|
|
|
|
.replaceAll("(?i)\\s+ORDER BY\\s+", "\nORDER BY ")
|
|
|
|
|
.replaceAll("(?i)\\s+LIMIT\\s+", "\nLIMIT ")
|
|
|
|
|
.replaceAll("(?i)\\s+OFFSET\\s+", "\nOFFSET ")
|
|
|
|
|
.replaceAll("(?i)\\s+UNION\\s+", "\nUNION\n")
|
|
|
|
|
.replaceAll("(?i)\\s+INSERT\\s+INTO\\s+", "\nINSERT INTO ")
|
|
|
|
|
.replaceAll("(?i)\\s+VALUES\\s+", "\nVALUES ")
|
|
|
|
|
.replaceAll("(?i)\\s+UPDATE\\s+", "\nUPDATE ")
|
|
|
|
|
.replaceAll("(?i)\\s+SET\\s+", "\nSET ")
|
|
|
|
|
.replaceAll("(?i)\\s+DELETE\\s+FROM\\s+", "\nDELETE FROM ")
|
|
|
|
|
.replaceAll("(?i)\\s+JOIN\\s+", "\nJOIN ")
|
|
|
|
|
.replaceAll("(?i)\\s+LEFT\\s+JOIN\\s+", "\nLEFT JOIN ")
|
|
|
|
|
.replaceAll("(?i)\\s+RIGHT\\s+JOIN\\s+", "\nRIGHT JOIN ")
|
|
|
|
|
.replaceAll("(?i)\\s+INNER\\s+JOIN\\s+", "\nINNER JOIN ")
|
|
|
|
|
.replaceAll("(?i)\\s+OUTER\\s+JOIN\\s+", "\nOUTER JOIN ");
|
|
|
|
|
|
|
|
|
|
return formattedSql;
|
|
|
|
|
return sql.replaceAll("\\s+", " ").trim();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* SQL 문장을 유형에 따라 포맷팅합니다.
|
|
|
|
|
*
|
|
|
|
|
* @param statement SQL 문장 객체
|
|
|
|
|
* @param indent 들여쓰기 수준
|
|
|
|
|
* @return 포맷팅된 SQL 문자열
|
|
|
|
|
*/
|
|
|
|
|
private String formatStatement(Statement statement, int indent) {
|
|
|
|
|
if (statement == null) {
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
StringBuilder result = new StringBuilder();
|
|
|
|
|
String indentation = String.join("", Collections.nCopies(indent, " "));
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
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);
|
|
|
|
|
} else {
|
|
|
|
|
// 지원하지 않는 문장 유형
|
|
|
|
|
result.append(statement.toString());
|
|
|
|
|
}
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
// 포맷팅 중 오류 발생 시 원본 문장 반환
|
|
|
|
|
if (log.isDebugEnabled()) {
|
|
|
|
|
log.debug("SQL 문장 포맷팅 실패: {}", e.getMessage());
|
|
|
|
|
}
|
|
|
|
|
result.append(statement.toString());
|
|
|
|
|
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();
|
|
|
|
|
|