Hi, I think I found an issue for a rather specific case. I'm not sure if this issue belongs to this repo, Spring integration, or MyBatis core. I put it here for now, because the demo is using TypeHandler's component scanning.

When trying to map an enum with an interface using the interface's TypeHandler Spring bean, there are cases which throws the following exception:

...
Caused by: org.apache.ibatis.type.TypeException: Unable to find a usable constructor for class com.example.demo.mybatis.TheInterfaceTypeHandler
    at org.apache.ibatis.type.TypeHandlerRegistry.getInstance(TypeHandlerRegistry.java:457) ~[mybatis-3.5.5.jar:3.5.5]
    at org.apache.ibatis.type.TypeHandlerRegistry.getJdbcHandlerMapForEnumInterfaces(TypeHandlerRegistry.java:286) ~[mybatis-3.5.5.jar:3.5.5]
    at org.apache.ibatis.type.TypeHandlerRegistry.getJdbcHandlerMap(TypeHandlerRegistry.java:262) ~[mybatis-3.5.5.jar:3.5.5]
    at org.apache.ibatis.type.TypeHandlerRegistry.getTypeHandler(TypeHandlerRegistry.java:237) ~[mybatis-3.5.5.jar:3.5.5]
    ...
Caused by: java.lang.NoSuchMethodException: com.example.demo.mybatis.TheInterfaceTypeHandler.<init>()
    at java.lang.Class.getConstructor0(Class.java:3082) ~[na:1.8.0_265]
    at java.lang.Class.getConstructor(Class.java:1825) ~[na:1.8.0_265]

As I understand it, MyBatis is trying to instantiate a new TypeHandler instead of using the one registered as Spring bean, which it failed because the type handler class requires another Spring bean (therefore no no-arg constructor).


Here's the sample code for demonstrating the case: https://github.com/KniveX/bugreport-springboot-mybatis-200816 - Java 8 (OpenJDK 1.8.0_265-8u265-b01-0ubuntu2~18.04-b01) - Spring Boot (2.3.3.RELEASE) - mybatis-spring-boot-starter (2.1.3)

Thank you, and I apologize if my english is not clear.

Comment From: kazuki43zoo

Hi @KniveX ,

Could explain an actual usage for interface based enum type handler that injected an other spring bean on your application?

Comment From: kazuki43zoo

This issue belong mybatis core module.

Comment From: KniveX

The project I'm working on has two enums in different JARs (core JAR & project JAR) which when combined, maps to 1 table in database.

So we're using interface as the common ground for these two enums. As a result, we need to declare a custom TypeHandler for mapping the interface.

The injected bean in custom TypeHandler is used to cache the enums' values and return the correct enum instance, given a String argument (for example, in type handler's getNullableResult method)

I put up some code modification to illustrate this actual use case: https://github.com/KniveX/bugreport-springboot-mybatis-200816/tree/real_use_case


I know this is somewhat convulted & is a very uncommon case, but it just sorta happened... 🤷‍♂️

Comment From: kazuki43zoo

I think you don't need to use the spring bean in this case. I've modified as follow, it work fine. WDYT?

package com.example.demo.mybatis;

import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;

import com.example.demo.obj.TheInterface;
import com.example.demo.obj.TheRequiredBean;

public class TheInterfaceTypeHandler extends BaseTypeHandler<TheInterface> {

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, TheInterface parameter, JdbcType jdbcType)
            throws SQLException {
        ps.setString(i, parameter.name());
    }

    @Override
    public TheInterface getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return TheRequiredBean.findByName( rs.getString(columnName) );
    }

    @Override
    public TheInterface getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        return TheRequiredBean.findByName( rs.getString(columnIndex) );
    }

    @Override
    public TheInterface getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        return TheRequiredBean.findByName( cs.getString(columnIndex) );
    }

}
package com.example.demo.obj;

import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

import org.apache.ibatis.io.ResolverUtil;

public class TheRequiredBean {

  private static final Map<String, TheInterface> mapping;

  private TheRequiredBean() {
    // NOP
  }

  static {
    mapping = new ResolverUtil<>().find(new ResolverUtil.IsA(TheInterface.class), "com.example.demo")
        .getClasses().stream()
        .filter(Class::isEnum)
        .map(x -> ((Class<? extends Enum<?>>) x))
        .flatMap(x -> Arrays.stream(x.getEnumConstants()))
        .collect(Collectors.toUnmodifiableMap(Enum::name, x -> (TheInterface) x));
  }

  public static TheInterface findByName(String enumName) {
    return mapping.get(enumName);
  }

}
  • application.properties
mybatis.type-handlers-package=com.example.demo.mybatis

Comment From: KniveX

Whoa, looks neat! TIL you don't need application context to do classpath scanning. Give me some time to implement that on the project, and see if this issue can be closed.

1 quick question: Is there any difference between this (the way you declare it):

new ResolverUtil<>().find(new ResolverUtil.IsA(TheInterface.class), "com.example.demo")
        .getClasses();

and this?

new ResolverUtil<TheInterface>().findImplementations(TheInterface.class, "com.example.demo")
        .getClasses();

(Didn't try that on IDE, so pardon if it doesn't actually compile)

Comment From: kazuki43zoo

1 quick question:

It is same mean!

Comment From: KniveX

Your solution works great! I guess this issue can be safely closed.

Thanks a lot for your help @kazuki43zoo. Cheers