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:

  1. change DefaultJdbcClient internals so it always calls the JdbcTemplate method that accepts a String (the sql query) for the first argument.
  2. 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.