Affects: 6.1
With JdbcTemplate
it was easy to apply AOP advice around query* method executions:
@Around("execution(* org.springframework.jdbc.core.JdbcTemplate.query*(String, ..)) && args(query, ..)")
Object intercept(ProceedingJoinPoint pjp, String query) throws Throwable {
// code
}
It is useful for example to measure query execution time and record it with a micrometer Timer
. I want to do it like that (instead of using a profiler at the datasource level) because:
- not all queries are concerned, I use a special characters at the end (/ query-name /) to check if execution time should be measured or not. The query-name is used to create a dedicated timer.
- doing it in a spring bean allows
MetricRegistry
injection.
Full code:
private final MeterRegistry registry;
private final Map<String, Timer> timers = new HashMap<>();
public SqlQueriesTimer(MeterRegistry registry) {
this.registry = registry;
}
@Around("execution(* org.springframework.jdbc.core.JdbcTemplate.query*(String, ..)) && args(query, ..)")
Object intercept(ProceedingJoinPoint pjp, String query) throws Throwable {
boolean useTimer = query.endsWith("*/"); // inspired by Nick Craver
if(!useTimer) {
return pjp.proceed();
}
Timer timer = this.timers.computeIfAbsent(query, q -> {
String queryName = query.substring(query.lastIndexOf("/*")+2, query.lastIndexOf("*/"));
return this.registry.timer(queryName);
});
Object result = timer.recordCallable(()->{
try {
return pjp.proceed();
}catch (Throwable throwable){
throw new RuntimeException(throwable);
}
});
return result;
}
It is not possible do to AOP on the JdbcClient
because of its fluent style (the methods that trigger the query execution does not belong to JdbcClient
spring bean).
But since JdbcClient
uses JdbcTemplate
internally I thought that AOP would still work. However, calls to
public <T> T query(ResultSetExtractor<T> rse) {
T result = (useNamedParams() ?
namedParamOps.query(this.sql, this.namedParamSource, rse) :
classicOps.query(statementCreatorForIndexedParams(), rse));
Assert.state(result != null, "No result from ResultSetExtractor");
return result;
}
with indexed parameter results in a call to <T> T query(PreparedStatementCreator psc, ResultSetExtractor<T> rse)
method on the JdbcTemplate
, which bypass AOP poincut. A pointcut for this method does not help since it is not possible to get the sql query from psc
.
I think that query execution interception should be possible. Suggestions:
- change
DefaultJdbcClient
internals so it always calls theJdbcTemplate
method that accepts aString
(the sql query) for the first argument. - provide, through a
JdbcClient.Builder
a way to register interceptors:
JdbcClient jdbcClient = JdbcClient.builder().jdbcOperation(jdbcTemplate).interceptors(/*add interceptors*/).build()
The second suggestion is more complicated and would not reuse existing AOP and interceptions infrastructure but would still work if JdbcClient
does not rely on JdbcTemplate
in the future. They are not exclusive: suggestion 1 could be implemented immediately (correct me if I am wrong) and suggestion 2 in the future if JdbcTemplate
becomes deprecated and if DefaultJdbcClient
directly uses JDBC API.
Comment From: jhoeller
You should be able to get the SQL String from the PreparedStatementCreator
by casting it to SqlProvider
and calling getSql()
on it.
In general, we'd like to hold on to this direct invocation since we can specify the parameters as a List there, not having to convert it to an intermediate array.
Comment From: ah1508
Indeed:
@Around("execution(* org.springframework.jdbc.core.JdbcTemplate.query(*, ..)) && args(arg0, ..)")
Object intercept(ProceedingJoinPoint proceedingJoinPoint, Object arg0) throws Throwable {
return switch (arg0) {
case String s when s.endsWith("*/") -> profileQuery(s, proceedingJoinPoint);
case SqlProvider sp when sp.getSql().endsWith("*/") -> profileQuery(sp.getSql(), proceedingJoinPoint);
default -> proceedingJoinPoint.proceed();
};
}
private Object profileQuery(String query, ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
// code
}
why not !
Comment From: sbrannen
Thanks for the feedback, @ah1508.
Since, @jhoeller's proposal seems to meet your needs, I am closing this issue.