How does Spring integrate Mybatis to register Mapper interface?

See for complete project code https://codechina.csdn.net/dreaming_coder/mybatis-spring

1. Introduction

In the past, when using Spring to integrate Mybatis, the Mapper interface should be added to Spring in the following way:

<bean id="userMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">
  <property name="mapperInterface" value="org.mybatis.spring.sample.mapper.UserMapper" />
  <property name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>

You can see that the type is not XXXMapper, but MapperFactoryBean. Do you know why it matches so well?

Let's create a Maven project to see:

[pom.xml]

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.ice</groupId>
    <artifactId>demo</artifactId>
    <version>1.0</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.3.8</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.7</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.25</version>
        </dependency>
    </dependencies>
</project>

[UserMapper.java]

package com.ice.mapper;

import org.apache.ibatis.annotations.Select;

public interface UserMapper {

    @Select("select 'user'")
    String selectById();
}

[UserService.java]

package com.ice.service;

import com.ice.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;


@Component
public class UserService {

    @Autowired
    private UserMapper userMapper;

    public void test() {
        System.out.println(userMapper.selectById());
    }
}

[IceApplication.java]

import com.ice.service.UserService;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;


@ComponentScan("com.ice")
public class IceApplication {
    public static void main(String[] args) {
        // Start Spring
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
        applicationContext.register(IceApplication.class);
        applicationContext.refresh();

        // Get bean
        UserService userService = applicationContext.getBean("userService", UserService.class);
        userService.test();
    }
}

Can the main method be executed normally now? The answer is no!

There is no bean named userService in the container, because UserMapper is an interface and cannot be instantiated, let alone create a bean! Then it cannot be injected into the properties of userService. Naturally, the bean named userService was not created successfully (pay attention to the difference between object and bean).

2. Dependency injection of usermapper

Obviously, there are two options:

  • Write an implementation class injection
  • Proxy object

Of course, the proxy object is more convenient, because you can use dynamic proxy to save code

So, who generates this proxy object, spring V.S. mybatis? The answer is mybatis, otherwise why can it be used without spring?

3. Create proxy object bean

There are two ways to create bean s:

  • Declarative, @ Bean, @ Component
  • Beandefinition

Declarative should be used a lot. Here's a look at programmatic.

For example, we have a User class:

public class User {
    
}

Then, write this in the main method:

public static void main(String[] args) {
    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
    applicationContext.register(IceApplication.class);

    AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();
    beanDefinition.setBeanClass(User.class);

    applicationContext.refresh();

    System.out.println(applicationContext.getBean("user", User.class));
}

Can it run successfully? No!

Because just defining a bean is not enough. You have to register it in the container.

public static void main(String[] args) {
    // Start Spring
    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
    applicationContext.register(IceApplication.class);

    AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();
    beanDefinition.setBeanClass(User.class);
    // Register in the container and set the bean name to user
    applicationContext.registerBeanDefinition("user", beanDefinition); 

    applicationContext.refresh();

    System.out.println(applicationContext.getBean("user", User.class));
}

However, you cannot register UserMapper directly in this way:

public static void main(String[] args) {
    // Start Spring
    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
    applicationContext.register(IceApplication.class);

    AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();
    beanDefinition.setBeanClass(UserMapper.class);
    applicationContext.registerBeanDefinition("userMapper",beanDefinition);

    applicationContext.refresh();

    System.out.println(applicationContext.getBean("userMapper", UserMapper.class));
}

After all, UserMapper is an interface without a constructor, so we can create a proxy object through FactoryBean.

Let's see how FactoryBean works first:

[IceFactoryBean.java]

package org.mybatis.spring;


import com.ice.service.User;
import org.springframework.beans.factory.FactoryBean;

public class IceFactoryBean implements FactoryBean {
    @Override
    public Object getObject() throws Exception {
        User user = new User();
        return user;
    }

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

[IceApplication.java]

import org.mybatis.spring.IceFactoryBean;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;


@ComponentScan("com.ice")
public class IceApplication {
    public static void main(String[] args) {
        // Start Spring
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
        applicationContext.register(IceApplication.class);

        AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();
        beanDefinition.setBeanClass(IceFactoryBean.class);
        applicationContext.registerBeanDefinition("user",beanDefinition);

        applicationContext.refresh();

        System.out.println(applicationContext.getBean("user"));
        System.out.println(applicationContext.getBean("&user"));
    }
}

Because we can only specify one name, that is, the name user of the bean created by FactoryBean. FactoryBean itself is also a bean, and the default name is & user

So we can return the proxy object of UserMapper in the getObject() method of IceFactoryBean.

[IceFactoryBean.java]

package org.mybatis.spring;


import com.ice.mapper.UserMapper;
import org.springframework.beans.factory.FactoryBean;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;


public class IceFactoryBean implements FactoryBean {
    @Override
    public Object getObject() throws Exception {
        Object o = Proxy.newProxyInstance(IceFactoryBean.class.getClassLoader(), new Class[]{UserMapper.class}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                return null;
            }
        });
        return o;
    }

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

At this point, execute the main method at the beginning:

public static void main(String[] args) {
    // Start Spring
    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
    applicationContext.register(IceApplication.class);

    AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();
    beanDefinition.setBeanClass(IceFactoryBean.class);
    applicationContext.registerBeanDefinition("userMapper",beanDefinition);

    applicationContext.refresh();

    // Get bean
    UserService userService = applicationContext.getBean("userService", UserService.class);
    userService.test();
}

It can be found that no error is reported:

Why null? Because executing the test() method will call the selectById() method, which will be executed through the invoke() method, and the invoke() method returns null, the result is null.

At this time, there is another problem. IceFactoryBean cannot only be used for UserMapper interface, which is too extravagant. What if I have OrderMapper and MemberMapper?

Let's modify IceFactoryBean

package org.mybatis.spring;


import org.springframework.beans.factory.FactoryBean;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;


public class IceFactoryBean implements FactoryBean {

    private Class mapperClass;

    public IceFactoryBean(Class mapperClass) {
        this.mapperClass = mapperClass;
    }

    @Override
    public Object getObject() throws Exception {
        Object o = Proxy.newProxyInstance(IceFactoryBean.class.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println(method.getName() + "--------" + mapperClass);
                return null;
            }
        });
        return o;
    }

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

Now the question is how to assign a value to an attribute with a construction method.

Spring provides methods to pass parameters:

beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(UserMapper.class);

Now we modify the main method as follows:

import com.ice.mapper.MemberMapper;
import com.ice.mapper.OrderMapper;
import com.ice.mapper.UserMapper;
import com.ice.service.UserService;
import org.mybatis.spring.IceFactoryBean;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;


@ComponentScan("com.ice")
public class IceApplication {
    public static void main(String[] args) {
        // Start Spring
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
        applicationContext.register(IceApplication.class);

        AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();
        beanDefinition.setBeanClass(IceFactoryBean.class);
        beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(UserMapper.class);
        applicationContext.registerBeanDefinition("userMapper",beanDefinition);

        AbstractBeanDefinition beanDefinition1 = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();
        beanDefinition1.setBeanClass(IceFactoryBean.class);
        beanDefinition1.getConstructorArgumentValues().addGenericArgumentValue(OrderMapper.class);
        applicationContext.registerBeanDefinition("orderMapper",beanDefinition1);

        AbstractBeanDefinition beanDefinition2 = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();
        beanDefinition2.setBeanClass(IceFactoryBean.class);
        beanDefinition2.getConstructorArgumentValues().addGenericArgumentValue(MemberMapper.class);
        applicationContext.registerBeanDefinition("memberMapper",beanDefinition2);

        applicationContext.refresh();

        // Get bean
        UserService userService = applicationContext.getBean("userService", UserService.class);
        userService.test();
    }
}

The output results are as follows:

This method is reflected in mybatis:

However, every time you add an XXXMapper interface, you need to add a piece of code in the main method. It's too troublesome

4. ImportBeanDefinitionRegistrar rewriting

Spring provides the ImportBeanDefinitionRegistrar interface to facilitate us to register beans. We use it to rewrite the cumbersome code in main:

[IceImportBeanDefinitionRegistrar.java]

package org.mybatis.spring;

import com.ice.mapper.MemberMapper;
import com.ice.mapper.OrderMapper;
import com.ice.mapper.UserMapper;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;


public class IceImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) {
        AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();
        beanDefinition.setBeanClass(IceFactoryBean.class);
        beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(UserMapper.class);
        registry.registerBeanDefinition("userMapper", beanDefinition);

        AbstractBeanDefinition beanDefinition1 = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();
        beanDefinition1.setBeanClass(IceFactoryBean.class);
        beanDefinition1.getConstructorArgumentValues().addGenericArgumentValue(OrderMapper.class);
        registry.registerBeanDefinition("orderMapper", beanDefinition1);

        AbstractBeanDefinition beanDefinition2 = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();
        beanDefinition2.setBeanClass(IceFactoryBean.class);
        beanDefinition2.getConstructorArgumentValues().addGenericArgumentValue(MemberMapper.class);
        registry.registerBeanDefinition("memberMapper", beanDefinition2);
    }
}

The changes of startup class are as follows:

import com.ice.service.UserService;
import org.mybatis.spring.IceImportBeanDefinitionRegistrar;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;


@ComponentScan("com.ice")
@Import(IceImportBeanDefinitionRegistrar.class)
public class IceApplication {
    public static void main(String[] args) {
        // Start Spring
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
        applicationContext.register(IceApplication.class);
        applicationContext.refresh();

        // Get bean
        UserService userService = applicationContext.getBean("userService", UserService.class);
        userService.test();
    }
}

Execution result:·

5. Scan rewrite

Although the previous method is refreshing when starting, it is still a little cumbersome, because ImportBeanDefinitionRegistrar should also be the content of middleware and cannot couple business classes, so it should be registered by scanning.

[IceMapperScanner.java]

package org.mybatis.spring;


import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.ClassPathBeanDefinitionScanner;

import java.util.Set;

public class IceMapperScanner extends ClassPathBeanDefinitionScanner {

    public IceMapperScanner(BeanDefinitionRegistry registry) {
        super(registry);
    }

    @Override
    protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
        Set<BeanDefinitionHolder> beanDefinitionHolders = super.doScan(basePackages);
        for (BeanDefinitionHolder beanDefinitionHolder : beanDefinitionHolders) {
            BeanDefinition beanDefinition = beanDefinitionHolder.getBeanDefinition();
            beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(beanDefinition.getBeanClassName());
            beanDefinition.setBeanClassName(IceFactoryBean.class.getName());
        }
        return beanDefinitionHolders;
    }

    @Override
    // We only care about interfaces, so we need to override Spring's default rules when rewriting
    protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
        return beanDefinition.getMetadata().isInterface();
    }
}

[IceImportBeanDefinitionRegistrar.java] some changes need to be made:

package org.mybatis.spring;

import com.ice.mapper.MemberMapper;
import com.ice.mapper.OrderMapper;
import com.ice.mapper.UserMapper;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.filter.TypeFilter;

import java.io.IOException;


public class IceImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) {

        String path = "com.ice.mapper";
        IceMapperScanner iceMapperScanner = new IceMapperScanner(registry);
        // This is because Spring excludes some files by default. We want all files to be scanned
        iceMapperScanner.addIncludeFilter(new TypeFilter() {
            @Override
            public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
                return true;
            }
        });
        iceMapperScanner.scan(path);
    }
}

When we add another XXXMapper, the output result is:

At present, there are several problems:

  • The scan path is dead
  • The proxy object of XXXMapper is currently generated by ourselves. We need the proxy object generated by mybatis

How does Mybatis generate proxy objects?

sqlSession.getMapper(XXMapper.class);

Let's rewrite it to generate the proxy object generated by Mybatis:

[IceFactoryBean.java]

package org.mybatis.spring;


import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.annotation.Autowired;


public class IceFactoryBean implements FactoryBean {

    private Class mapperClass;

    private SqlSession sqlSession;

    @Autowired
    public void setSqlSession(SqlSessionFactory sqlSessionFactory) {
        sqlSessionFactory.getConfiguration().addMapper(mapperClass);
        this.sqlSession = sqlSessionFactory.openSession();
    }

    public IceFactoryBean(Class mapperClass) {
        this.mapperClass = mapperClass;
    }

    @Override
    public Object getObject() throws Exception {
        Object mapper = sqlSession.getMapper(mapperClass);
        return mapper;
    }

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

Then you need to configure the bean

@Bean
public SqlSessionFactory sqlSessionFactory() throws IOException {
    InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    return sqlSessionFactory;
}

In this way, when we execute again, we will call real SQL to query the database. The statement result print in the test() method just now can be seen:

Next, solve the problem of scanning path, because the path is still dead.

We define an annotation:

[IceScan.java]

package org.mybatis.spring;

import org.springframework.context.annotation.Import;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import(IceImportBeanDefinitionRegistrar.class)
public @interface IceScan {
    String value();
}

Then specify the scanning path based on the annotation in the startup class:

@ComponentScan("com.ice")
@IceScan("com.ice.mapper")
public class IceApplication {
    // ...
}

Note that the @ Import position is moved to the annotation, because Spring startup will scan the annotation of the configuration class, so that the value of IceScan interface will be scanned. At the same time, Spring will continue to see whether there is @ Import annotation on the annotation. If so, it will execute the method of registering bean s. At this time, the value of IceScan annotation will be passed in.

At this point, the IceImportBeanDefinitionRegistrar class should be changed as follows:

[IceImportBeanDefinitionRegistrar.java]

package org.mybatis.spring;

import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.filter.TypeFilter;

import java.io.IOException;
import java.util.Map;


public class IceImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) {

        Map<String, Object> annotationAttributes = importingClassMetadata.getAnnotationAttributes(IceScan.class.getName());
        String path = (String) annotationAttributes.get("value");

        IceMapperScanner iceMapperScanner = new IceMapperScanner(registry);
        // This is because Spring excludes some files by default. We want all files to be scanned
        iceMapperScanner.addIncludeFilter(new TypeFilter() {
            @Override
            public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
                return true;
            }
        });
        iceMapperScanner.scan(path);
    }
}

You can still run:

6. Replace all Spring annotations

As a middleware, it is not suitable to use some annotations of other packages. How to inject set?

First, delete the @ Autowired annotation in IceFactoryBean, and then make the following modifications:

public class IceMapperScanner extends ClassPathBeanDefinitionScanner {

    // ...
    @Override
    protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
        Set<BeanDefinitionHolder> beanDefinitionHolders = super.doScan(basePackages);
        for (BeanDefinitionHolder beanDefinitionHolder : beanDefinitionHolders) {
            GenericBeanDefinition beanDefinition = (GenericBeanDefinition) beanDefinitionHolder.getBeanDefinition();
            beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(beanDefinition.getBeanClassName());
            beanDefinition.setBeanClassName(IceFactoryBean.class.getName());
            // Configuration injection model
            beanDefinition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
        }
        return beanDefinitionHolders;
    }

    // ...
}

Keywords: Java Mybatis Spring Middleware

Added by Dizzee15 on Sat, 29 Jan 2022 07:22:27 +0200