Gangmax Blog

MyBatis 'Invalid bound statement' Issue Caused by Spring Loading Sequence

This was a really weird issue and took me some time to fix.

Background

  1. This is a web application using Spring + MyBatis.

  2. The basic mapper xml files/Java classes are generated using Maven. It worked well.

  3. I added some customized content into the existing mapper xml files/Java classes. It worked well.

  4. I was told that the customized content should not be added the generated mapper xml files/Java classes. Instead, I should create new mapper xml files to contain the DB operation content, and create new Java interface classes which extend the existing generated ones and contain the new added methods. Then I can use the new Java interface classes directly in my code. This is a valid suggestion and I did so.

But the following error happened when Spring starts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Caused by: org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.jcloud.blockchain.orm.mapper.EnhancedChannelMapper.selectByExample
at org.apache.ibatis.binding.MapperMethod$SqlCommand.<init>(MapperMethod.java:223)
at org.apache.ibatis.binding.MapperMethod.<init>(MapperMethod.java:48)
at org.apache.ibatis.binding.MapperProxy.cachedMapperMethod(MapperProxy.java:59)
at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:52)
at com.sun.proxy.$Proxy41.selectByExample(Unknown Source)
at a.b.c.impl.FabricConfigDBHelper.getChannels(FabricConfigDBHelper.java:66)
at a.b.c.impl.FabricConfigDBImpl.initChannels(FabricConfigDBImpl.java:82)
at a.b.c.impl.FabricConfigDBImpl.initialize(FabricConfigDBImpl.java:54)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor$LifecycleElement.invoke(InitDestroyAnnotationBeanPostProcessor.java:354)
at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor$LifecycleMetadata.invokeInitMethods(InitDestroyAnnotationBeanPostProcessor.java:305)
at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor.postProcessBeforeInitialization(InitDestroyAnnotationBeanPostProcessor.java:133)
... 41 more

Solution

First, I confirm the mapper xml files/Java classes I created are all correct. Then I find if I add the autowired mapper instance in code directly, it also works:

1
2
3
4
5
6
7
8
9
10
11
12
@Autowired
private EnhancedChannelMapper channelMapper;

@Test
public void testQueryChannels() {
DBChannelExample example = new DBChannelExample();
example.setOrderByClause("id ASC");
List<DBChannel> channels = channelMapper.selectByExample(example);
for (DBChannel channel : channels) {
logger.info("Channel name is: {}", channel.getName());
}
}

However, in the following code it does not work:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
public class FabricConfigDBHelper {

@Autowired
private EnhancedChannelMapper channelMapper;

public List<DBChannel> getChannels() {
DBChannelExample example = new DBChannelExample();
example.setOrderByClause("id ASC");
List<DBChannel> channels = channelMapper.selectByExample(example);
return channels;
}

}

From code perspective, they are just the same. But why the latter does not work?

I notice the exception is thrown from “FabricConfigDBImpl.initialize()” method. This method looks like below:

1
2
3
4
5
6
7
@PostConstruct
public void initialize() {
this.store = this.initStore(MessageFormat.format(DEFAULT_STORE_FILE_NAME,
String.valueOf(System.currentTimeMillis())));
this.organizations = this.initOrganizations();
this.channels = this.initChannels();
}

Note the “@PostConstruct” annotation. This class needs some initialization after Spring autowiring the dependency. And the “FabricConfigDBHelper” instance is used in the initialization phase where the exception happens. So I doubt the problem is: when the initialization of FabricConfigDBImpl happens, the mapper classes of MyBatis is not fully ready. Instead, it only finishes the basic mapper classes without parsing the parent mapper classes if there is any. So only the methods defined in the child mapper interface class can be recognized but the ones defined in the parent mapper interface cannot. To fix it, lazy initialization should be used: only when the “FabricConfigDBImpl” instance is about to be used, the initialization should be done.

To verify my speculation, I update the code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Note the "FabricConfigDBImpl" class implements the "FabricConfig" interface.
@Component
public class FabricConfigFactory {

@Autowired
private FabricConfig fabricConfig;

public FabricConfig getInstance(LoginUser user) throws MalformedURLException {
if (!fabricConfig.isInitialized()) {
fabricConfig.initialize();
}
return this.fabricConfig;
}
}

After this change, the initialization of “FabricConfigDBImpl” does not happen during Spring starts. Instead, it’s done when about to be used.

Then it works.

Comments