0

我想测量将由 MyBatis(Spring Boot 项目)运行的 sql 执行时间并将其与其他请求参数绑定,这样我就可以获得有关特定请求的性能问题的完整信息。对于这种情况,我以以下方式使用了 MyBatis 拦截器:

@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;
    }
}

现在,当涉及到请求绑定时,我想将这些指标作为属性存储在请求中。我已经尝试过这个简单的解决方案来获取 request,但这不起作用,因为 request 总是为 null (我已经读过这个解决方案在异步方法中不起作用,但是对于 MyBatis Interceptor 及其方法,我认为情况并非如此):

@Autowired
private HttpServletRequest request;

那么,问题是如何在 MyBatis 拦截器中正确获取请求?

4

1 回答 1

1

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();
        }
    }

}
于 2018-07-16T14:58:11.467 回答