The bottom principle and simple implementation of SpringBoot integrated with MyBatis

MyBatis can be said to be the most mainstream Spring persistence layer framework at present. This paper mainly discusses the underlying principle of Spring boot integration with MyBatis. Complete code can be moved Github.

How to use MyBatis

In general, how should we integrate MyBatis in a spring boot project?

  1. Introducing MyBatis dependency
<dependency>
	<groupId>org.mybatis.spring.boot</groupId>
	<artifactId>mybatis-spring-boot-starter</artifactId>
	<version>2.1.2</version>
</dependency>
  1. Configure data sources
  2. Add @ MapperScan annotation on the startup class and pass in the package path of dao layer to be scanned
  3. Create an interface in the dao layer, pass in the corresponding SQL statement on the method, or use Mapper.xml file for configuration. For example:
public interface UserDao {
    @Select("insert into user xxx")
    void insert();
}
  1. After that, we can call the new UserDao().insert() method to operate on the database.

So the question is, how does MyBatis complete the "seamless connection" and data persistence with Spring through such a simple configuration?

Spring's BeanDefinition

As we all know, one of the major features of Spring is IoC, which is inversion of control. When we give an object to Spring management, we don't need to manually create the object through the new keyword, just through @ Autowired or @ Resource automatic injection. So how does this process work?

In short, Spring will generate a bean definition (BD) object through the class information declared as a bean, and then create a singleton (not declared as prototype) through the BD object, which will be stored in the singleton pool and called when necessary.

When creating a beadDefinition, Spring will call a series of postprocessors to process BD. we can also customize the postProcessor to modify the properties of BD by simply implementing the BeanFactoryPostProcessor interface, for example:

@Component
public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        GenericBeanDefinition userDaoBD = (GenericBeanDefinition)beanFactory.getBeanDefinition("userDao");
        userDaoBD.setBeanClassName("userDaoChanged");
        userDaoBD.setAbstract(false);
        // more...
    }
}

In this postProcessor, we get the BD object of userdao and change its name to "userDaoChanged", so that we can get the original userdao bean by calling the getBean("userDaochanged") method of ApplicationContext.

About MyBatis

Now we know that when we give a class to Spring management, Spring builds bean singletons through bean definition. Now we have two new questions:

  1. How does MyBatis hand over dao to Spring?
  2. The dao we wrote is an interface. How is the interface instantiated?

How does MyBatis hand over dao to Spring management?

In Spring, there are three ways to give an object to Spring management:

  1. @Bean
  2. beanFactory.registerSingleton(String beanName, Object singletonObject)
  3. FactoryBean

Among them, MyBatis uses the FactoryBean method. Implementing the FactoryBean interface can inject our userDao into Spring.

beanFactory, which manages all bean of Spring, is a large Factory. FactoryBean is also a bean, but it has the function of Factory. When we call the getBean() method of spring context and pass in the custom FactoryBean, the returned bean is not the FactoryBean itself, but the object returned in the overridden getObject() method.

So FactoryBean is a "small factory".

@Component
public class UserFactoryBean implements FactoryBean {
    
    UserDao userDao;

    @Override
    public Object getObject() throws Exception {
        return userDao;
    }

    @Override
    public Class<?> getObjectType() {
        return UserDao.class;
    }
}

Just writing like this certainly can't meet our requirements. At this time, we will call getBean() method and report an error. We can't pass in a userdao parameter because userdao can't be instantiated. But in MyBatis, we can get an instantiated object of userdao through sqlSession.getMapper(UserDao.class). How does MyBatis do this?

How to instantiate an interface?

Why can an interface be instantiated? Looking at the source code of MyBatis, we can see that through the technology of dynamic Proxy, MyBatis wraps an object based on the interface, and then gives the object to Spring. Following the getMapper method, we can find such a method:

protected T newInstance(MapperProxy<T> mapperProxy) {
        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
    }

MyBatis is OK, so are we. Let's rewrite UserFatoryBean:

@Component
public class UserFactoryBean implements FactoryBean {

    @Override
    public Object getObject() throws Exception {
        Class[] classes = new Class[]{UserDao.class};
        return Proxy.newProxyInstance(MyFactoryBean.class.getClassLoader(),classes, new MyInvokationHandler());
    }

    @Override
    public Class<?> getObjectType() {
        return UserDao.class;
    }
}

The Proxy.newProxyInstance() method receives three parameters, which are:

  1. ClassLoader loader: decide which classloader to use to load the generated proxy object
  2. Class <? > [] interfaces: determine which interfaces and functions the proxy object should implement
  3. InvocationHandler h: determines the specific logic to execute when calling a method of this proxy object

Then write an InvokationHandler class to execute the specific method logic of the proxy object:

public class MyInvokationHandler implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Pretend to query the database:" + method.getAnnotation(Select.class).value()[0]);
        return null;
    }
}

In this handler, we get the information in the @ Select annotation and print it out.

OK, now let's run it:

@Test
void contextLoads() {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(Appconfig.class);
    UserDao userDao = (UserDao) ac.getBean("userFactoryBean");
    userDao.insert();
}

Console output:

Pretend to query the database: insert into user xxx

So far, we have completed a small part of the simple implementation of MyBatis. But there is another important problem: our FactoryBean is write dead and can only return the proxy object of UserDao. In the actual situation, how can we return different proxy objects according to the parameters passed in by users?

Dynamically generate proxy objects

To dynamically generate proxy objects, first we need to modify the code of UserFactoryBean so that it can adapt to various types of dao. We might as well directly change its name to MyFactoryBean:

public class MyFactoryBean implements FactoryBean {

    Class mapperInterface;

    // In order to support XML configuration, a default constructor must be provided
    public MyFactoryBean(){}

    // MapperScan mode
    public MyFactoryBean(Class mapperInterface){
        this.mapperInterface = mapperInterface;
    }


    @Override
    public Object getObject() throws Exception {
        Class[] classes = new Class[]{mapperInterface};
        return Proxy.newProxyInstance(MyFactoryBean.class.getClassLoader(),classes, new MyInvokationHandler());
    }

    @Override
    public Class<?> getObjectType() {
        return mapperInterface;
    }
}

We changed UserDao.class to dynamic mappinterface, so how can we pass this parameter to the constructor of MyFactoryBean? This goes back to the bean definition we talked about at the beginning. In Spring, you can modify various properties of beans during bd, including the parameters of construction methods. We modify the MyBeanFactoryPostProcessor we wrote at the beginning:

@Component
public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {

        List<Class> daos = new ArrayList<>();
        // Get all Daos
        daos.add(UserDao.class);
        daos.add(AnchorDao.class);

        for(Class dao:daos){
            // Get an empty bean definition
            BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MyFactoryBean.class);
            GenericBeanDefinition beanDefinition = (GenericBeanDefinition) builder.getBeanDefinition();

            // Add parameters for construction method
            beanDefinition.setBeanClass(MyFactoryBean.class);
            beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(dao);
        }
    }
}

In this way, we have constructed the Bean definition we want, and now we need to add it to the Spring container. Note: Bean definition is added to the Spring container, not beans. The method in @ Bean we mentioned earlier is not applicable.

Two other knowledge points are needed here: @ Import and importbeandefinitionregister

In the application, sometimes a class is not injected into the IOC container, but the bean corresponding to the class needs to be obtained during the application, and @ Import annotation is needed at this time.

@The most powerful thing about Import is that it provides an extension point for users. When we implement the importbeandefinitionregister interface with the @ Import imported class, Spring will not directly wrap this class as a bean, but execute its internal registerBeanDefinitions method. This is a bit like FactoryBean, which can execute its own logic in the class.

We write a registerBeanDefinitions like this:

public class ImportBeanDefinitionRegister implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry){

        // Get package name, traverse to get all class names
        String packagePath = Appconfig.class.getAnnotation(MyScan.class).path();
        List<String> classNames = SelectClassName.selectByPackage(packagePath);

        for(String className:classNames){

            BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MyFactoryBean.class);
            GenericBeanDefinition genericBeanDefinition = (GenericBeanDefinition) builder.getBeanDefinition();
            registry.registerBeanDefinition(SelectClassName.getShortName(className),genericBeanDefinition);
            // Add constructor parameter
            genericBeanDefinition.getConstructorArgumentValues().addGenericArgumentValue(className);
        }
    }
}

And write a MyScan annotation class to get the package name to be scanned:

@Retention(RetentionPolicy.RUNTIME)
public @interface MyScan {
    String path();
}

Then add @ MyScan annotation on the AppConfig class, pass in the package name, and finally write a tool class to get all the class names under the package. MyBeanFactoryPostProcessor class can also be deleted, and ImportBeanDefinitionRegister replaces its work.

(the complete code can access my Github)

@Test
void contextLoads() {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(Appconfig.class);
    ac.getBean(AnchorDao.class).query();
    ac.getBean(UserDao.class).insert();
}

Console output:

Pretend to query the database: select * from anchor limit 5
 Pretend to query the database: insert into user xxx

be accomplished! Let's go back and see how we use MyBatis: @ MapperScan, write dao interface, @ Select - almost exactly what we do now. You only need to encapsulate JDBC in MyInvokationHandler to implement specific access database logic, and you can use the "MyBatis" written by yourself in the project.

summary

Spring provides a good environment for the development of third-party frameworks, which is one of the reasons why spring can develop such a large and perfect ecosystem. For example, AOP is another major feature of spring. It is related to the postprocessor and dynamic agent mentioned in this article. My study and Research on the underlying principle of spring boot integration with MyBatis have given me a deeper understanding of both spring and MyBatis. (I'm so tired. Take a rest.)

Tags: Spring Mybatis Database github

Posted on Thu, 23 Apr 2020 22:06:52 -0700 by thines