Spring扩展原理

  • 前言
  • 案例
  • 问题
  • 紧接下文


前言

上次小编主要将了spring的refresh中invokeBeanFactoryPostProcessors方法的整体流程。大家可以回顾一下:这篇博客。那这次小编结合实际案例,到底第三方框架是如何进行扩展的。本期内容会比较复杂,希望小编能够讲清楚讲明白。

案例

spring是如何整合mybatis,首先我们看一下ssm整合示例。然后慢慢深究。
数据库表创建语句比较简单

CREATE TABLE `employee` (
  `id` bigint NOT NULL,
  `name` varchar(50) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

接着是pom文件,这边看主要引入jar包,spring-boot的然后是mybatis,mybatis-spring,以及数据库连接池spring-jdbc和mysql

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.2</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo</name>
    <description>Demo project for Spring Boot</description>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.1</version>
        </dependency>

        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>2.0.1</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

配置类

@MapperScan("自己的包名扫描mapper所在的包")
@Configuration
public class DataSourceConfig {

    @Bean
    public DataSource dataSource() {
        DriverManagerDataSource driverManagerDataSource = new DriverManagerDataSource();
        driverManagerDataSource.setDriverClassName("com.mysql.jdbc.Driver");
        //这里因为版本问题,得配后面的serverTimezone否则会报错哦
        driverManagerDataSource.setUrl("jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai");
        driverManagerDataSource.setUsername("root");
        driverManagerDataSource.setPassword("123456");
        return driverManagerDataSource;
    }

    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource());
        return sqlSessionFactoryBean.getObject();
    }


}

mapper接口,不要在意为什么没有用对象接受啊

public interface EmployeeMapper {
    @Select("select * from employee")
    List<Map<String, Object>> selectAll();
}

service层

@Service
public class EmployeeService {
    @Resource
    private EmployeeMapper employeeMapper;

    public void selectAll(){
        List<Map<String, Object>> maps =
                employeeMapper.selectAll();
        System.out.println(maps);
    }

测试类

@ComponentScan(basePackages = ""自己的包名"")
public class ExtendMybatisTest {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext =
                new AnnotationConfigApplicationContext();
        applicationContext.register(ExtendMybatisTest.class);
        applicationContext.refresh();
        EmployeeService employeeService = (EmployeeService)applicationContext.getBean("employeeService");
        employeeService.selectAll();
    }
}

打印

[{name=zhangsan, id=1}]

完美运行。这边对于大家应该很熟悉并且没有任何难点那么小编要提出问题了

问题

不知道有没有和小编一样有这样的疑问,接口为什么可以注入,谁实现了接口,以及实现之后怎么被注入到spring容器中的。看注释

@Service
public class EmployeeService {
    /**
    * 1、EmployeeMapper注入的必须是个实例化好后的对象
    * 2、这里必须实现了EmployeeMapper 接口
    * 3、它必须注册到这个spring容器中
    * 
    *
    */
    @Resource
    private EmployeeMapper employeeMapper;

    public void selectAll(){
        List<Map<String, Object>> maps =
                employeeMapper.selectAll();
        System.out.println(maps);
    }

其实1,和2 两步骤都是mybatis完成的,到时候小编写mybatis的时候将会着重说一下,待会儿小编也会模拟一个mybatis进行说明,关键第三步被spring容器管理是怎么管理的。

首先是mybatis如何将接口变成一个可使用的对象,主要原理为动态代理。大家可以上官网查看

https://mybatis.org/mybatis-3/getting-started.html 官方start示例
DataSource dataSource = BlogDataSourceFactory.getBlogDataSource();
TransactionFactory transactionFactory =
new JdbcTransactionFactory();
Environment environment =
new Environment(“development”, transactionFactory, dataSource);
Configuration configuration = new Configuration(environment);
configuration.addMapper(BlogMapper.class);
SqlSessionFactory sqlSessionFactory =
new SqlSessionFactoryBuilder().build(configuration);

try (SqlSession session = sqlSessionFactory.openSession()) {
BlogMapper mapper = session.getMapper(BlogMapper.class);
Blog blog = mapper.selectBlog(101);
}
这里的mapper就是从接口变成了一个实例

下面小编写一个模拟mybatis的代码的代码并且运行。
我们先编写一个生产sqlSession的类

public class ForgeMybatisSqlSessionFactory {


    public static <T> T getMapper(Class<T> clazz) {
        Class[] classes = new Class[]{clazz};
        InvocationHandler invocationHandler = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                //执行数据库连接,参数组装等等
                System.out.println("-----data base conn and param ok------");
                Select annotation = method.getAnnotation(Select.class);
                String s = annotation.value()[0];
                System.out.println("-----execute sql-----" + s);
                return null;
            }
        };
        T t = (T) Proxy.newProxyInstance(ForgeMybatisSqlSessionFactory.class.getClassLoader(), classes, invocationHandler);
        return t;
    }


}

测试类

public class ExtendMybatisTest {

    public static void main(String[] args) {
        EmployeeMapper employeeMapper = ForgeMybatisSqlSessionFactory.getMapper(EmployeeMapper.class);
        employeeMapper.selectAll();
    }
}

打印结果

-----data base conn and param ok------
-----execute sql-----select * from employee

现在我们差不多以及完成了代理类实例了那接下来怎么交由spring 管理?(扩展问题如果将第三方jar中对象或自己的对象让spring管理)
小编想到方法有以下几点:

  1. applicationContext.getBeanFactory().registerSingleton(“employeeMapper”,employeeMapper);
  2. @Bean 直接在配置文件中写入
  3. 使用factoryBean注入

1方案

@ComponentScan(basePackages = "自己的包")
public class ExtendMybatisTest {

    public static void main(String[] args) {
		//直接做一个然后放入到beanFactory中
        EmployeeMapper employeeMapper = ForgeMybatisSqlSessionFactory.getMapper(EmployeeMapper.class);

        AnnotationConfigApplicationContext applicationContext =
                new AnnotationConfigApplicationContext();
        applicationContext.register(ExtendMybatisTest.class);
        applicationContext.getBeanFactory().registerSingleton("employeeMapper",employeeMapper);
        applicationContext.refresh();
        EmployeeService employeeService = (EmployeeService)applicationContext.getBean("employeeService");
        employeeService.selectAll();


    }
}

2 方案

@Configuration
public class DataSourceConfig {
    @Bean
    public EmployeeMapper employeeMapper(){
        return ForgeMybatisSqlSessionFactory.getMapper(EmployeeMapper.class);
    }

1和2显然不是mybatis-spring整合jar包的方案,每次都得自己加不得累死啊。
那显然是第三种方案,利用factoryBean使用,那先讲讲factoryBean,它首先得是一个bean,可以放入beanDefinitionMap中,然后必须实现一个接口,并且当前这个bean还可以产生一个bean对象。
小编先写个示例:
伪造factoryBean

@Component
public class ForgeMybatisFactoryBean implements FactoryBean {
    @Override
    public Object getObject() throws Exception {
        return new Forge();
    }

    @Override
    public Class<?> getObjectType() {
        return Forge.class;
    }
}
//没有被加入spring管理的类
public class Forge {
}
//测试
@ComponentScan(basePackages = "自己的包")
public class ExtendMybatisTest {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext =
                new AnnotationConfigApplicationContext();
        applicationContext.register(ExtendMybatisTest.class);
        applicationContext.refresh();
        Object forgeMybatisFactory = applicationContext.getBean("forgeMybatisFactory");
        System.out.println(forgeMybatisFactory);
        Object forgeMybatisFactoryObject = applicationContext.getBean("&forgeMybatisFactory");
        System.out.println(forgeMybatisFactoryObject);

    }
}

打印结果

//这个根据名字获得的实例竟然是里面产生的object
com.dtyunxi.yundt.extend.util.Forge@61009542
//获取自己则是需要前面加&符号的,这个下次讲源码的时候小编告诉大家
com.dtyunxi.yundt.extend.util.ForgeMybatisFactory@77e9807f

示例结束,然后将上面示例中new Forge()换成ForgeMybatisSqlSessionFactory.getMapper(EmployeeMapper.class);然后下面的Forge.class换成EmployeeMapper.class,这样也就可以运行了,这样大致轮廓就出来了,然后我们看下mybatis官网 ,这里是怎么整合的

<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>

https://mybatis.org/spring/getting-started.html 这边主要用xml形式做的,这样就将factoryBean注入到spring容器中的,当然我们用了代码的方式并且用了@Component注解,注解这样是不可取,因为如果第三方jar包需要扫描,则在启动类或配置中放入扫描的包,这样扩展做得极差,且不易维护,而且各个咱们自己扩展的factoryBean只能实例化一个,看完xml后我们可以继续扩展,还有一个问题如何扫描多个mapper并且扫描到spring容器中。

通过以上解释大家大概明白了吧,然后小编继续改进他,改完他之后,看最开始的示例代码大家就明白了,为什么要这么写。我们慢慢扩展
首先factoryBean修改

public class ForgeMybatisFactoryBean<T> implements FactoryBean<T> {

    private Class<T> mapperInterface;

    public ForgeMybatisFactory(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }

    public void setMapperInterface(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }

    @Override
    public T getObject() throws Exception {
        return ForgeMybatisSqlSessionFactory.getMapper(mapperInterface);
    }


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

将自己的factoryBean注入到spring容器,变成beanDefiniton扔进beanDefinitonMap中去

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

        /**
         * 扫描bean 并注入模拟
         *
         */
        List<Class> classList = new ArrayList<>();
        classList.add(EmployeeMapper.class);
        for (Class aClass : classList) {
            BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(ForgeMybatisFactoryBean.class);
            AbstractBeanDefinition beanDefinition = beanDefinitionBuilder.getBeanDefinition();
            //属性注入方式 
//        MutablePropertyValues propertyValues = beanDefinition.getPropertyValues();
//        propertyValues.add("mapperInterface", aClass);
            //构造方法注入,需要参数的构造方法
            ConstructorArgumentValues constructorArgumentValues = beanDefinition.getConstructorArgumentValues();
            constructorArgumentValues.addIndexedArgumentValue(0,aClass);
            registry.registerBeanDefinition(aClass.getSimpleName, beanDefinition);
        }

    }
}

测试类修改

@ComponentScan(basePackages = "com.dtyunxi.yundt.extend")
@Import(ForgeMybatisImportRegistry.class)
public class ExtendMybatisTest {

    public static void main(String[] args) {

        AnnotationConfigApplicationContext applicationContext =
                new AnnotationConfigApplicationContext();
        applicationContext.register(ExtendMybatisTest.class);


        applicationContext.refresh();

        EmployeeService employeeService = (EmployeeService)applicationContext.getBean("employeeService");
        employeeService.selectAll();


    }
}

这样是可以顺利运行的当然还有点东西了,就是注解扫描我们来模拟一个自己的扫描类

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import(ForgeMybatisImportRegistry.class)
public @interface ForgeMybatisScan {

    String[] basePackages() default {};
}

这样整体都差不多了,然后扫描这部我们就不做了。接下来我们看spring-mybateis源码。基本和小编编写的差不多

紧接下文

由于文章比较长大家肯定有视觉疲劳,接着看下一篇文章。
Spring与mybatis整合原理2