How to get request in MyBatis Interceptor

3.6k Views Asked by At

I want to measure time of sql execution which will be run by MyBatis (Spring Boot project) and bind that with other request parameters, so I can get full info about performance issues regarding specific requests. For that case I have used MyBatis Interceptor on following way:

@Intercepts({
    @Signature(
            type = Executor.class,
            method = "query",
            args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
    @Signature(
            type = Executor.class,
            method = "query",
            args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class QueryMetricsMybatisPlugin implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Stopwatch stopwatch = Stopwatch.createStarted();
        Object result = invocation.proceed();
        stopwatch.stop();
        logExectionTime(stopwatch, (MappedStatement) invocation.getArgs()[0]);
        return result;
    }
}

Now when it come to binding with request, I want to store those metrics in request as attribute. I have tried this simple solution to get request, but that was not working since request was always null (I have read that this solution won't work in async methods, but with MyBatis Interceptor and its methods I think that's not the case):

@Autowired
private HttpServletRequest request;

So, the question is how properly get request within MyBatis interceptor?

1

There are 1 best solutions below

0
On

One important note before I answer your question: it is a bad practice to access UI layer in the DAO layer. This creates dependency in the wrong direction. Outer layers of your application can access inner layers but in this case this is other way round. Instead of this you need to create a class that does not belong to any layer and will (or at least may) be used by all layers of the application. It can be named like MetricsHolder. Interceptor can store values to it, and in some other place where you planned to get metrics you can read from it (and use directly or store them into request if it is in UI layer and request is available there).

But now back to you question. Even if you create something like MetricsHolder you still will face the problem that you can't inject it into mybatis interceptor.

You can't just add a field with Autowired annotation to interceptor and expect it to be set. The reason for this is that interceptor is instantiated by mybatis and not by spring. So spring does not have chance to inject dependencies into interceptor.

One way to handle this is to delegate handling of the interception to a spring bean that will be part of the spring context and may access other beans there. The problem here is how to make that bean available in interceptor.

This can be done by storing a reference to such bean in the thread local variable. Here's example how to do that. First create a registry that will store the spring bean.

public class QueryInterceptorRegistry {

    private static ThreadLocal<QueryInterceptor> queryInterceptor = new ThreadLocal<>();

    public static QueryInterceptor getQueryInterceptor() {
        return queryInterceptor.get();
    }

    public static void setQueryInterceptor(QueryInterceptor queryInterceptor) {
        QueryInterceptorRegistry.queryInterceptor.set(queryInterceptor);
    }

    public static void clear() {
        queryInterceptor.remove();
    }

}

Query interceptor here is something like:

public interface QueryInterceptor {
    Object interceptQuery(Invocation invocation) throws InvocationTargetException, IllegalAccessException;
}

Then you can create an interceptor that will delegate processing to spring bean:

@Intercepts({
        @Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class,
                RowBounds.class, ResultHandler.class }),
        @Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class,
                RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}) })
public class QueryInterceptorPlugin implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        QueryInterceptor interceptor = QueryInterceptorRegistry.getQueryInterceptor();
        if (interceptor == null) {
            return invocation.proceed();
        } else {
            return interceptor.interceptQuery(invocation);
        }
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
    }

}

You need to create an implementation of the QueryInterceptor that does what you need and make it a spring bean (that's where you can access other spring bean including request which is a no-no as I wrote above):

@Component
public class MyInterceptorDelegate implements QueryInterceptor {

    @Autowired
    private SomeSpringManagedBean someBean;

    @Override
    public Object interceptQuery(Invocation invocation) throws InvocationTargetException, IllegalAccessException {
      // do whatever you did in the mybatis interceptor here
      // but with access to spring beans
   }
}

Now the only problem is to set and cleanup the delegate in the registry.

I did this via aspect that was applied to my service layer methods (but you can do it manually or in spring mvc interceptor). My aspect looks like this:

@Aspect
public class SqlSessionCacheCleanerAspect {

    @Autowired MyInterceptorDelegate myInterceptorDelegate;

    @Around("some pointcut that describes service methods")
    public Object applyInterceptorDelegate(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        QueryInterceptorRegistry.setQueryInterceptor(myInterceptorDelegate);
        try {
            return proceedingJoinPoint.proceed();
        } finally {
            QueryInterceptorRegistry.clear();
        }
    }

}