[JAVA] How to pass an object to Mapper in MyBatis without arguments

I couldn't find it at the time of posting the article, so I looked it up.

What I wanted to do

For those who just want a conclusion

Sample can be found here. I want to rewrite the addParameter method on line 66 of this source code and pass it. If you specify an object, you can access it in the same way as you passed it as an argument.

Code description

Overall picture

First of all, the corresponding source code is shown below.

Source code

CustomSqlSessionConfig.java


package mybatis_implicit_parameter.config;

import lombok.experimental.Delegate;
import org.apache.ibatis.binding.MapperMethod;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.Proxy;
import java.util.Map;

@Configuration
public class CustomSqlSessionConfig {
    @Bean
    public SqlSessionTemplate sqlSession(SqlSessionFactory sqlSessionFactory) throws Exception {
        return new SqlSessionTemplateExt(sqlSessionFactory);
    }

    static class SqlSessionTemplateExt extends SqlSessionTemplate {
        public SqlSessionTemplateExt(SqlSessionFactory sqlSessionFactory) {
            super(sqlSessionFactory);
        }

        Object invokeParentMethod(Method method, Object[] args) throws Throwable {
            return makeParentMethodHandle(method).bindTo(this).invokeWithArguments(args);
        }

        MethodHandle makeParentMethodHandle(Method method) throws Exception {
            return MethodHandles.lookup().findSpecial(SqlSessionTemplate.class, method.getName(),
                    MethodType.methodType(method.getReturnType(), method.getParameterTypes()), SqlSessionTemplateExt.class);
        }

        @Delegate
        final SqlSession proxy = (SqlSession)Proxy.newProxyInstance(SqlSessionTemplateExt.class.getClassLoader(),
                new Class[]{SqlSession.class}, new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        if(hasParameter(method)) {
                            args[1] = addParameter(args[1]);
                        }
                        return SqlSessionTemplateExt.this.invokeParentMethod(method, args);
                    }

                    boolean hasParameter(Method method) {
                        Parameter[] params = method.getParameters();
                        return (params.length >= 2 && params[1].getType().equals(Object.class));
                    }

                    Object addParameter(Object parameter) {
                        parameter = (parameter == null) ? new MapperMethod.ParamMap() : parameter;
                        if (parameter instanceof Map) {
                            return addParameter((Map) parameter);
                        } else {
                            throw new RuntimeException("Unexpected pattern Tell me if you come here ~:" + parameter.getClass().getName());
                        }
                    }

                    Object addParameter(Map map) {
                        //Implicit parameters set here
                        map.put("insertValue", "foo");
                        map.put("updateValue", "bar");
                        return map;
                    }
                });
    }
}

To briefly explain what you are doing, ...

MyBatis is a SqlSession class with String as the first argument named insert / select / update / delete. Analyze the contents of Mapper and issue SQL via the method that has Object as the second argument. In CustomSqlSessionConfig, extend `` `SQLSessionTemplate``` and add an object that seems inappropriate to override each method and pass it via an argument to the parameter of the second argument (actually Map) and execute it. I try to do it.

Sticking point: Combining lombok's @Delegate and dynamic proxy

This code is self-satisfying and can really be made simpler. If you try to implement the above in a straightforward manner, even if it is not such code, you can do it by overriding the target method as shown in ↓.

Stupid code

CustomSqlSessionConfig.java


(abridgement)
@Override 
public int insert(String statement,Object parameter) {
    return super.insert(statement, addParameter(parameter));
}
(abridgement)

But I didn't like to write the same thing over and over, so I tried to stick to it.

Dynamic proxy is a function implemented from the old version of Java that dynamically creates an object with the method of the interface (group) specified by the argument and aggregates the processing in the specified InvocationHandler. It is a function that can be done.

On the other hand, lombok's @Delegate is an annotation that expands all public methods of annotated fields as methods of the class that holds the field (you can fine-tune it). It may be difficult to get an image at first glance, but if you look at this site, it will be easier to get an image.

By combining these two, you can process arbitrary methods of your own class (this time it is insert or select) all at once. It tends to be a little black magic code, but if you like it, you should love it! I don't know w

The disadvantage of this method is that it is difficult to call the same method of the parent class (super.xxx-like call) from InvocationHandler. Since InvocationHandler receives Method object as an argument, it would be nice to be able to do `method.invoke (...)`, but when combined with @Delegate, it will cause StackOverflow. This is the same result if you specify the parent class and get the Method object with the same method signature. This is the Method # invoke API Reference (https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/lang/reflect/Method.html#invoke (https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/lang/reflect/Method.html#invoke) This is because the method call is realized by invokeVirtual as described in java.lang.Object, java.lang.Object ...)).

If the underlying method is an instance method, it is invoked using dynamic method lookup as documented in The Java Language Specification, section 15.12.4.4; in particular, overriding based on the runtime type of the target object may occur. (If the underlying method is an instance method, it will be invoked using the dynamic method reference described in the Java Language Specification section and will be overridden, in particular, based on the run-time type of the target object. )

To realize super.xxx by reflection

MethodHandles.lookup().findSpecial(SqlSessionTemplate.class, method.getName(),
    MethodType.methodType(method.getReturnType(), 
    method.getParameterTypes()), SqlSessionTemplateExt.class);

Within the child class, you need to get the parent class's method `MethodHandle``` with `findSpecial. MethodHandle is different from Java reflection so far, and the result depends on which class it is effective in. If you do this in the InvocationHandler``` implementation class, you will still get an error.

Suspicious point

  • Judgment as to whether it is a method to add a parameter is a little suspicious.
  • Does not work if the parameter object type is other than Map. It will not work if a case other than Map (confirmed that it may be null) comes. I haven't been able to catch up with anything other than Map.

Confirmed library version

Library name version
Spring-Boot 2.3.4.RELEASE
mybatis-spring-boot-starter 2.1.3
lombok 5.1.0

About sample code

You can use the sample code as you like, but we do not take any responsibility for the result.

Recommended Posts