The bug caused by the thread safety of Mybatis Interceptor

    Let's first look at a background of the discovery of this bug, but the problem in the background is not caused by this bug:

    Recently, a development colleague in the business department came up and said that when he used the company's framework to add data to the database, the new data was inexplicably rolled back, and the local development environment was able to reproduce this problem. The company's framework is based on the integration of SpringBoot+Mybatis. It stands to reason that so many projects are already in use. If it is a bug, there should be a problem so early. My first thought was that something abnormal in his business logic caused the transaction to roll back, but there was no obvious abnormality, and the new data can be seen in the database. So guess that there is a scheduled task to delete data. I asked this colleague, but the answer was no . There is no way. Since it can be reproduced locally, it is the best solution. I decided to find the problem in the local development environment and the source code.
    At the beginning of debugging, only a few breakpoints were set. The code execution process is normal. The new data in the database does exist. However, when the code is executed, the data in the database does not exist, and the program does not exist. abnormal. Continue to go deep into the breakpoint debugging. After more than ten rounds of breakpoint debugging, I found that it occasionally appears org.apache.ibatis.executor.ExecutorException: Executor was closed., but when the program skips some breakpoints, everything is normal. After n rounds of debugging failed, it is still suspected that the database has a scheduled task or a problem with the database. So I re-created a test database to add new data. This time everything is normal. At this time, I am still full of joy. At least the general cause of the problem has been located. I quickly asked the DBA to check whether there is SQL deleting data. It really proved. own thoughs. Later, let the development colleague reconfirm whether there are scheduled tasks and data deletion services on the development environment machine. This time I did tell me that there was indeed a scheduled task to delete data , and the problem was solved. It turned out that he was new to the project and he was not very familiar with the project, really. . . . . .

    Now let’s go back to the title and focus on not considering Interceptor thread safety, which leads to bugs that only appear during breakpoint debugging.
    After work at night, I suddenly thought org.apache.ibatis.executor.ExecutorException: Executor was closed.of what happened during debugging ? Is there really a bug in this place? Double Eleven is coming soon. If this is a big bug on Double Eleven, the problem will be big. After going to work the next day, I decided to study this issue in depth. Since I don't know under what circumstances can this exception be triggered, I can only debug step by step with breakpoints.
First look at the implemented Mybatis interceptor, the main code is as follows:

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

    private static final String DB_URL = "DB_URL";

    private Executor target;

    private ConcurrentHashMap<Object, Object> cache = new ConcurrentHashMap<>();

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
    
    
        Object proceed = invocation.proceed();
        //Executor executor = (Executor) invocation.getTarget();
        Transaction transaction = target.getTransaction();
        if (cache.get(DB_URL) != null) {
    
    
            //其他逻辑处理
            System.out.println(cache.get(DB_URL));
        } else if (transaction instanceof SpringManagedTransaction) {
    
    
            Field dataSourceField = SpringManagedTransaction.class.getDeclaredField("dataSource");
            ReflectionUtils.makeAccessible(dataSourceField);
            DataSource dataSource = (DataSource) ReflectionUtils.getField(dataSourceField, transaction);
            String dbUrl = dataSource.getConnection().getMetaData().getURL();
            cache.put(DB_URL, dbUrl);
            //其他逻辑处理
            System.out.println(cache.get(DB_URL));
        }
        //其他逻辑略...
        return proceed;
    }

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

During the debugging process, step by step breakpoints, the following exceptions will appear:

Caused by: org.apache.ibatis.executor.ExecutorException: Executor was closed.
	at org.apache.ibatis.executor.BaseExecutor.getTransaction(BaseExecutor.java:78)
	at org.apache.ibatis.executor.CachingExecutor.getTransaction(CachingExecutor.java:51)
	at com.bruce.integration.mybatis.plugin.MybatisExecutorInterceptor.intercept(MybatisExecutorInterceptor.java:37)
	at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:61)

According to the exception information, locate the code to the org.apache.ibatis.executor.BaseExecutor.getTransaction() method

 @Override
  public Transaction getTransaction() {
    
    
    if (closed) {
    
    
      throw new ExecutorException("Executor was closed.");
    }
    return transaction;
  }

Found that closedan exception will be thrown when the variable is true. So as long as you locate closedthe method to modify the variable value, you don't know. Searching through the idea tool only found a place to modify the value of the variable. That's the org.apache.ibatis.executor.BaseExecutor#close()method

@Override
  public void close(boolean forceRollback) {
    
    
    try {
    
    
      ....省略
    } catch (SQLException e) {
    
    
      // Ignore. There's nothing that can be done at this point.
      log.warn("Unexpected exception on closing transaction.  Cause: " + e);
    } finally {
    
    
      ....省略
      closed = true; //只有该处修改为true
    }
  }

So add a breakpoint to the finally code block to see when it will come to this method. When debugging step by step, it is found that when the close method has not been reached, the value of closed has been modified to true, and the Executor was closed. exception is thrown. Strange? Is there any other code that will reflect and modify this variable? According to reason, if Mybatis modifies the value of the variable in its own code, it will not use this method. It is too inelegant and adds to the complexity of the code.

No way, it is after n times of step-by-step breakpoint debugging. Eventually, I found that such a prompt was displayed in the idea debug window.

Skipped breakpoint at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor:423 because it happened inside debugger evaluation

Insert picture description here
From the prompt, it just skipped a certain breakpoint. In fact, I have noticed this prompt before, but this time I searched for a solution with curiosity.
It turns out that idea will call the toString() of the object when displaying the member variables or method parameters of the class. With the mentality of trying it out, the toString option in idea is removed.
Insert picture description here
Breakpoint debugging again, this time there is no exception . It turns out that the toString() method of the object is called when the idea displays the variables. ? ? It's no wonder BaseExecutor#close()that the breakpoint in the method can't go in, but the variable value is modified.

Then why does idea display variables and calling the toString() method will cause the Executor used in the query to be closed?
According to the above tips, check the source code of org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor to see what the logic is.

private class SqlSessionInterceptor implements InvocationHandler {
    
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    
    
      SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
          SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
      try {
    
    
        Object result = method.invoke(sqlSession, args);
        if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
    
    
          // force commit even on non-dirty sessions because some databases require
          // a commit/rollback before calling close()
          sqlSession.commit(true);
        }
        return result;
      } catch (Throwable t) {
    
    
        Throwable unwrapped = unwrapThrowable(t);
        if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
    
    
          // release the connection to avoid a deadlock if the translator is no loaded. See issue #22
          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
          sqlSession = null;
          Throwable translated = SqlSessionTemplate.this.exceptionTranslator
              .translateExceptionIfPossible((PersistenceException) unwrapped);
          if (translated != null) {
    
    
            unwrapped = translated;
          }
        }
        throw unwrapped;
      } finally {
    
    
        if (sqlSession != null) {
    
    
          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
        }
      }
    }
  }

From the code point of view, this is an interceptor implementation class in the jdk dynamic proxy, because through the jdk dynamic proxy, the SqlSessioninterface in Mybatis is proxied, and the toString() method is called when the variable view is displayed in the idea, which causes it to be intercepted. In the invoke() method, the sqlSession associated with the current thread must be closed finally in the finally, causing the BaseExecutor.close()method to be called . In order to verify this idea, the intercepted toString() method is processed in the SqlSessionInterceptor as follows. If the toString() method does not continue to execute downwards, it only needs to return the code class of which interface.

private class SqlSessionInterceptor implements InvocationHandler {
    
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    
    

            if (args == null && "toString".equals(method.getName())) {
    
    
                return Arrays.toString(proxy.getClass().getInterfaces());
            }
           ... 其他代码省略
        }
}

Restore the settings in the idea, debug it again, and indeed the Executor was closed. exception will no longer occur.
This seems to be a bug caused by mybatis-spring's incomplete consideration when implementing SqlSessionInterceptor. In order not to reveal the company's framework code to restore this bug, the SpringBoot+Mybatis integration project was built separately and a similar logic interceptor was written. code show as below:

@Intercepts({
    
    
        @Signature(method = "query", type = Executor.class, args = {
    
    MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
        @Signature(method = "query", type = Executor.class, args = {
    
    MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
        @Signature(method = "update", type = Executor.class, args = {
    
    MappedStatement.class, Object.class})
})
public class MybatisExecutorInterceptor implements Interceptor {
    
    
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
    
    

        Object proceed = invocation.proceed();
        Executor executor = (Executor) invocation.getTarget();
        Transaction transaction = executor.getTransaction();
        //其他逻辑略...
        return proceed;
    }

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

SqlSessionInterceptorExecute again at the interruption point, after several debugging, when trying to restore this bug, the program passed smoothly and perfectly without any exception.
At this moment, I immediately remembered a piece of unreasonable code that I observed before, which Executorwas saved as a member variable in the example code at the beginning of the article , but the Interceptorimplementation class in mybatis was instantiated when the program was started, and it was a single-instance object. . And each time the SQL execution will go to create a new Executortarget and will go through Interceptorthe public Object plugin(Object target), needs to be used to determine whether the agency Executor object. The plugin method rewritten in the example re-assigns the Executor every time. In fact, this is thread-unsafe . Because the toString() method is called for the display variable during debugging in the idea, it will also be created SqlSessionand Executorpassed through the plugin method, resulting in the Executor member variable being actually replaced.
Insert picture description here
Solution : invocation.getTarget()Go directly to get the proxy object instead of using member variables.

Why did the online program not report a Executor was closedproblem???

  1. Because the toString() method is not called online like in idea
  2. Caching is used in the code. After the url is obtained by using Executor, the Executor object will not be used next time when the request comes, and there will be no exception.
  3. When the program first starts, the concurrency is not large enough. If there are enough requests immediately when the program is first started, an exception will still be thrown, but as long as the result is cached once, subsequent exceptions will not occur.

Summary:
In fact, the Executor is used as a member variable in MybatisExecutorInterceptor. If the Executor is changed, an exception caused by thread insecurity occurs. The toString() method of displaying the variable value in the idea is just the cause of the exception.

Guess you like

Origin blog.csdn.net/u013202238/article/details/108249483