mybatis / mybatis-3

MyBatis SQL mapper framework for Java
http://mybatis.github.io/mybatis-3/
Apache License 2.0
19.7k stars 12.82k forks source link

When use mappers, Occurs NullPointerException on localCache #2985

Closed hossi97 closed 10 months ago

hossi97 commented 10 months ago

MyBatis version

3.5.13

Database vendor and version

Test case or example project

member.xml

<select id="duplicateId" parameterType="MemberDTO" resultType="Integer">
    select count(member_id) from member where member_id = #{memberId}
</select>

MemberMapper.java

@Mapper
public interface MemberMapper {
  Integer duplicateId(MemberDTO memberDTO);
}

MybatisConnectionFactory.java

public class MybatisConnectionFactory {
  private static SqlSessionFactory sqlSessionFactory;

  private MybatisConnectionFactory() {
  }

  static {
    try {
      String resource = "config/mybatis-config.xml";
      InputStream inputStream = Resources.getResourceAsStream(resource);
      sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    } catch (IOException e) {
      throw new RuntimeException("Failed to initialize MyBatis", e);
    }
  }

  public static SqlSession getSqlSession() {
    return sqlSessionFactory.openSession(false);
  }

  public static void closeSqlSession(SqlSession sqlSession) {
    sqlSession.close();
  }
}

MemberDao.java

public int idCheck(String id) {
    SqlSession sqlSession = MybatisConnectionFactory.getSqlSession();
    memberMapper = sqlSession.getMapper(MemberMapper.class);
    MemberDTO memberDTO = new MemberDTO.Builder().memberId(id).build();
    Integer result = 0;

    try {
      result = memberMapper.duplicateId(memberDTO);
      sqlSession.commit();
    } catch (Exception e) {
      e.printStackTrace();
      sqlSession.rollback();
    } finally {
        closeSql(sqlSession);
    }

    return result;
  }

Steps to reproduce

While load test on the idCheck function, error occurs intermittently when many requests come in at the same time.

Expected result

org.apache.ibatis.exceptions.PersistenceException: 
### Error querying database.  Cause: java.lang.NullPointerException: Cannot invoke "org.apache.ibatis.cache.impl.PerpetualCache.removeObject(Object)" because "this.localCache" is null
### The error may exist in mapper/member.xml
### The error may involve com.blanc.recrute.mybatis.MemberMapper.duplicateId
### The error occurred while executing a query
### Cause: java.lang.NullPointerException: Cannot invoke "org.apache.ibatis.cache.impl.PerpetualCache.removeObject(Object)" because "this.localCache" is null
   at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:30)
   at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:156)
   at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:147)
   at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:142)
   at org.apache.ibatis.session.defaults.DefaultSqlSession.selectOne(DefaultSqlSession.java:75)
   at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:87)
   at org.apache.ibatis.binding.MapperProxy$PlainMethodInvoker.invoke(MapperProxy.java:142)
   at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:86)
   at jdk.proxy7/jdk.proxy7.$Proxy23.duplicateId(Unknown Source)
   at com.blanc.recrute.member.dao.MemberDAOImpl.idCheck(MemberDAOImpl.java:74)
   at com.blanc.recrute.member.service.MemberServiceImpl.idCheck(MemberServiceImpl.java:31)
   at com.blanc.recrute.member.controller.IdCheckController.doPost(IdCheckController.java:31)
   at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:590)
   at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658)
   at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:205)
   at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)
   at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51)
   at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)
   at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)
   at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167)
   at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90)
   at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:482)
   at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115)
   at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93)
   at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:673)
   at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
   at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:341)
   at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:391)
   at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)
   at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:894)
   at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1740)
   at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)
   at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)
   at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
   at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
   at java.base/java.lang.Thread.run(Thread.java:833)

I debugged the code, and there seems to be a problem where the SqlSession is closed in advance while processing the localCache method.

In my case, this problem doen't occur when doen't use mappers. Only occurs when use mappers. But I'm not sure about this.

The only solution I found is to synchronize the idcheck method using the synchronized keyword.

public synchronized int idCheck(String id) {
    // ...
}

If do like this, no errors will occur. But I'm not sure that this is the right way.

I thought it was a bug. But if it's not a bug, is there a way to fix it?

harawata commented 10 months ago

Hello @hossi97 ,

I'm not sure how, but the only possibility is that the sqlSession is shared between multiple threads. As explained in the doc, SqlSession is not thread safe. https://mybatis.org/mybatis-3/getting-started.html#sqlsession

hossi97 commented 10 months ago

Hello @hossi97 ,

I'm not sure how, but the only possibility is that the sqlSession is shared between multiple threads. As explained in the doc, SqlSession is not thread safe. https://mybatis.org/mybatis-3/getting-started.html#sqlsession

@harawata Thanks for your comment. I already read that but It doesn't seem to be related.

hossi97 commented 10 months ago

As I examined the code structure, I've found the cause of exception.

BaseExecutor holds the localCache. SimpleExecutor inherits from BaseExecutor, and every SimpleExecutor created through openSession() shares the same localCache value.

In a scenario where multiple threads are working with their respective SqlSession, if a specific thread calls SqlSession.close(), it internally assigns null to the localCache.

Subsequently, when other threads invoke query method to process query, call the localCache method. As a result, NullPointerException occurs.

harawata commented 10 months ago

@hossi97 ,

I think you are confused. BaseExecutor.localCache is an instance field, not a static field.

As I wrote, it is only possible if the instance is accessed from multiple thread. Keep debugging and you will be able to find the cause soon. If you focus on your code instead of MyBatis', you'll be able to find it sooner.

I'm going to close this as it's clearly not a MyBatis bug.