import java.lang.reflect.Field;
import java.sql.Connection;
import java.sql.Statement;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
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;
@Slf4j
@Intercepts({
@Signature(type = StatementHandler.class, method = "parameterize", args = { Statement.class })
})
public class MybatisLogInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler handler = (StatementHandler) invocation.getTarget();
BoundSql boundSql = handler.getBoundSql();
// 쿼리문을 가져온다(이 상태에서의 쿼리는 값이 들어갈 부분에 ?가 있다)
String sql = boundSql.getSql();
// 쿼리실행시 맵핑되는 파라미터를 구한다
Object param = handler.getParameterHandler().getParameterObject();
if (param == null) { // 파라미터가 아무것도 없을 경우
sql = sql.replaceFirst("\\?", "''");
}
else { // 해당 파라미터의 클래스가 Integer, Long, Float, Double 클래스일 경우
if (param instanceof Integer || param instanceof Long || param instanceof Float || param instanceof Double) {
sql = sql.replaceFirst("\\?", param.toString());
}
else if (param instanceof String) { // 해당 파라미터의 클래스가 String 일 경우(이 경우는 앞뒤에 '(홑따옴표)를 붙여야해서 별도 처리
sql = sql.replaceFirst("\\?", "'" + param + "'");
}
else if (param instanceof Map) { // 해당 파라미터가 Map 일 경우
/*
* 쿼리의 ?와 매핑되는 실제 값들의 정보가 들어있는 ParameterMapping 객체가 들어간 List 객체로 return이 된다.
* 이때 List 객체의 0번째 순서에 있는 ParameterMapping 객체가 쿼리의 첫번째 ?와 매핑이 된다
* 이런 식으로 쿼리의 ?과 ParameterMapping 객체들을 Mapping 한다
*/
List<ParameterMapping> paramMapping = boundSql.getParameterMappings();
for (ParameterMapping mapping : paramMapping) {
String propValue = mapping.getProperty(); // 파라미터로 넘긴 Map의 key 값이 들어오게 된다
Object value = ((Map) param).get(propValue); // 넘겨받은 key 값을 이용해 실제 값을 꺼낸다
if (value instanceof String) { // SQL의 ? 대신에 실제 값을 넣는다. 이때 String 일 경우는 '를 붙여야 하기땜에 별도 처리
sql = sql.replaceFirst("\\?", "'" + value + "'");
}
else {
sql = sql.replaceFirst("\\?", value.toString());
}
}
}
else { // 해당 파라미터가 사용자 정의 클래스일 경우
/*
* 쿼리의 ?와 매핑되는 실제 값들이 List 객체로 return이 된다.
* 이때 List 객체의 0번째 순서에 있는 ParameterMapping 객체가 쿼리의 첫번째 ?와 매핑이 된다
* 이런 식으로 쿼리의 ?과 ParameterMapping 객체들을 Mapping 한다
*/
List<ParameterMapping> paramMapping = boundSql.getParameterMappings();
Class<? extends Object> paramClass = param.getClass();
// logger.debug("paramClass.getName() : {}", paramClass.getName());
for (ParameterMapping mapping : paramMapping) {
String propValue = mapping.getProperty(); // 해당 파라미터로 넘겨받은 사용자 정의 클래스 객체의 멤버변수명
Field field = paramClass.getDeclaredField(propValue); // 관련 멤버변수 Field 객체 얻어옴
field.setAccessible(true); // 멤버변수의 접근자가 private일 경우 reflection을 이용하여 값을 해당
// 멤버변수의 값을 가져오기 위해 별도로 셋팅
Class<?> javaType = mapping.getJavaType(); // 해당 파라미터로 넘겨받은 사용자 정의 클래스 객체의 멤버변수의 타입
if (String.class == javaType) { // SQL의 ? 대신에 실제 값을 넣는다. 이때 String 일 경우는 SQL string으로 변환 시 ' 를 붙여야 하기때문에 별도 처리
sql = sql.replaceFirst("\\?", "'" + field.get(param) + "'");
}
else {
sql = sql.replaceFirst("\\?", field.get(param).toString());
}
}
}
}
log.info("sql : \n{}", sql);
long start = System.currentTimeMillis();
Object retVal = invocation.proceed(); // 쿼리 실행
long end = System.currentTimeMillis();
log.info("elapsed time : {} ms", end - start);
log.info("=====================================================================");
return retVal;
}
@Override
public Object plugin(Object target) {
return Interceptor.super.plugin(target);
}
@Override
public void setProperties(Properties properties) {
Interceptor.super.setProperties(properties);
}
}
다음과 같은 프로퍼티를 정의했다고 가정하자.
myproperties:
execution-location: "myhome"
위와 같은 yml을 저장하는 configuration class
는 다음과 같이 정의할 수 있다.
@ConfigurationPropertis(prefix = "myproperties")
public record MyProperties(
String executionLocation
) {}
이 configuration
은 spring bean
으로 정의된다. executionLocation
을 mybatis mapper 내 어느 쿼리에서든 사용하고 싶을 때, interceptor
를 사용해볼 수 있다.
일단 interceptor
를 정의해보자.
LocationInterceptor
@Slf4j
@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 })
})
@RequiredArgsConstructor
@Component
public class LocationInterceptor implements Interceptor {
private final MyProperties myProperties;
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
Object param = args[1];
Object newParam = setAuditing(param);
args[1] = newParam;
final String sqlId = ((MappedStatement) args[0]).getId();
// MybatisLogInterceptor에서는 StatementHandler 계층을 intercept하기 때문에 MappedStatement를 가져올 수 없다.
// 그러므로 현 시점에서 sql id를 찍는다
log.info("====== sqlId: {}", sqlId);
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Interceptor.super.plugin(target);
}
@Override
public void setProperties(Properties properties) {
Interceptor.super.setProperties(properties);
}
private Object setAuditing(Object param) {
MapperMethod.ParamMap<Object> newParam = null;
if (param instanceof MapperMethod.ParamMap<?>) {
newParam = (MapperMethod.ParamMap<Object>) param;
newParam.put("executionLocation", myProperties.executionLocation());
}
else if (param == null) {
// 파라미터가 없는 경우 강제로 넣어준다
newParam = new MapperMethod.ParamMap<>();
newParam.put("executionLocation", myProperties.executionLocation());
}
return newParam;
}
}
다음으로는 mybatis session을 자바 코드로 설정해준다. 스프링 빈을 가져와야하기 때문에 xml설정보다는 java 설정으로 진행하는 편이 간편하다.
MybatisConfig
@Configuration
@RequiredArgsConstructor
public class MybatisConfig {
private final LocationInterceptor interceptor;
@Bean
public SqlSessionFactory sqlSessionFactory() throws
}