zoukankan      html  css  js  c++  java
  • Mybatis插件原理和整合Spring

    插件编写要求(分页插件PageHelper)

    自定义插件需要做到三点

    1)实现Interceptor接口

    public class PageInterceptor implements Interceptor{}
    

    2)实现对应的方法。最关键的是intercept()方法里面是拦截的逻辑,需要增强的代码写在此处。

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        return null;
    }
    
    @Override
    public Object plugin(Object o) {
        return null;
    }
    
    @Override
    public void setProperties(Properties properties) {
    
    
    }
    

    3)在拦截器类上加上注解。注解签名制定了需要拦截的对象、拦截的方法、参数(因为方法有不同的重载,所以要指定具体的参数)。

    @Intercepts({
            @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, 
    RowBounds.class, ResultHandler.class}),
            @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, 
    RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})
    })
    

    插件配置

    mybatis-config.xml中中注册插件,配置属性。

    <plugin interceptor="com.github.pagehelper.PageInterceptor">
        <property name="offsetAsPageNum" value="true"/>
        <property name="rowBoundsWithCount" value="true"/>
        <property name="pageSizeZero" value="true"/>
        <property name="reasonable" value="true"/>
        <property name="params" value="pageNum=start;pageSize=limit;"/>
        <property name="supportMethodsArguments" value="true"/>
        <property name="returnPageInfo" value="check"/>
    </plugin>
    
    

    插件解析注册

    Mybatis启动时扫描标签,注册到configuration对应那个的InterceptorChain中。Properties里面的参数,会调用setProperties()方法处理。

    XMLConfigBuilder.pluginElement();

    private void pluginElement(XNode parent) throws Exception {
        if (parent != null) {
            for (XNode child : parent.getChildren()) {
                String interceptor = child.getStringAttribute("interceptor");
                Properties properties = child.getChildrenAsProperties();
                Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
                interceptorInstance.setProperties(properties);
                configuration.addInterceptor(interceptorInstance);
            }
        }
    }
    

    启动解析的时候,把所有的插件全部存到Configuration的InterceptorChain中,它是一个List。

    QA:不修改代码怎么增强功能?多插件怎么拦截?

    1.采用的是代理模式,这个也是MyBatis插件的实现原理。
    2.插件是层层拦截,我们用到另一种设计模式--责任链模式。
    

    QA:什么对象可以被拦截?那些方法可以被拦截?

    这里注意的是,因为Executor有可能被二级缓存装饰,那么是先代理还是装饰,还是先装饰后代理呢?

    Executor会被拦截到CachingExecutor或者BaseExecutor。

    DefaultSqlSessionFactory.openSessionFromDataSource():

    if (ExecutorType.BATCH == executorType) {
        executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
        executor = new ReuseExecutor(this, transaction);
    } else {
        // 默认 SimpleExecutor
        executor = new SimpleExecutor(this, transaction);
    }
    // 二级缓存开关,settings 中的 cacheEnabled 默认是 true
    if (cacheEnabled) {
        executor = new CachingExecutor(executor);
    }
    // 植入插件的逻辑,至此,四大对象已经全部拦截完毕
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
    

    先创建基本类型,在创建二级缓存装饰,最后插件拦截。所以这里拦截的是CachingExecutor。

    插件实现原理

    代理类什么时候创建?

    对Executor拦截的代理类是openSession()的时候创建的。

     Executor executor = configuration.newExecutor(tx, execType);
    

    StatementHandler是SimpleExecutor.doQuery()创建的;里面包含了ParameterHandler和ResultSetHandler的创建和代理。

    代理怎么创建?

    调用interceptorChain的pluginAll()方法。

    public Object pluginAll(Object target) {
        for (Interceptor interceptor : interceptors) {
            target = interceptor.plugin(target);
        }
        return target;
    }
    

    遍历interceptorChain,使用Interceptor实现类的plugin()方法,对目标核心对象进行代理。

    default Object plugin(Object target) {
        //实现代理对象
    }
    

    这个plugin返回一个代理对象。JDK动态代理,我们需要写一个实现InvocationHandler接口的触发管理类。然后使用Proxy.newProxyInstance()创建一个代理对象。

    这里Mybatis的插件机制提供一个触发管理类Plugin,实现了InvocationHandler。

    创建代理对象的newProxyInstance()在这个类进行封装,就是wrap()方法。

    public static Object wrap(Object target, Interceptor interceptor) {
        Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
        Class<?> type = target.getClass();
        Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
        if (interfaces.length > 0) {
            return Proxy.newProxyInstance(
                type.getClassLoader(),
                interfaces,
                new Plugin(target, interceptor, signatureMap));
        }
        return target;
    }
    

    在wrap的时候创建了一个Plugin对象,Plugin是被代理对象、Interceptor的一个封装对象:

    new Plugin(target, interceptor, signatureMap)
    

    持有了被代理对象和interceptor的实例:

    private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
        this.target = target;
        this.interceptor = interceptor;
        this.signatureMap = signatureMap;
    }
    

    因为这里是for循环代理,所以某个核心对象有多个插件,会返回被代理多次的代理对象。

    被代理之后,调用的流程?

    ​ 在四大核心对象的一次执行过程中(可能被多次代理),因为已经被代理了,所以会触发管理类Plugin的invoke()方法。

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
            Set<Method> methods = signatureMap.get(method.getDeclaringClass());
            if (methods != null && methods.contains(method)) {
                return interceptor.intercept(new Invocation(target, method, args));
            }
            return method.invoke(target, args);
        } catch (Exception e) {
            throw ExceptionUtil.unwrapThrowable(e);
        }
    }
    

    如果被拦截的方法不为空,进入Plugin的invoke()方法,调用interceptor的intercept()方法:

    return interceptor.intercept(new Invocation(target, method, args));
    

    到了intercept()方法,也就走到了我们自己实现的拦截逻辑(例如PageInterceptor的intercept()方法)。

    其中Invocation,它是对被拦截对象、方法、参数的一个封装。

    当然,在执行逻辑完成后,继续执行被代理对象(四大核心对象)的原方法,需要使用method的invoke方法。

    method.invoke(target, args);
    

    拿到被代理的核心对象,继续执行它的方法(例如executor.query())。我们如何拿到被代理对象和参数呢?

    这个采用了上面创建的Invocation对象,简化了参数的传递,直接提供了一个proceed()方法。原方法也可写成如下方法:

    return invocation.proceed();
    
    public Object proceed() throws InvocationTargetException, IllegalAccessException {
        return method.invoke(target, args);
    }
    

    总结:

    DefaultSqlSession类select()方法流程

    如果对象被代理多次,这里会继续调用下一个插件的逻辑,再走一次Plugin的invoke()方法。这里需要注意多个插件的运行顺序。

    配置的顺序和执行的顺序?

    配置的顺序和执行的顺序是相反的。interceptorChain的List是按照插件从上往下的顺序解析、添加的。

    创建的时候按照list的顺序代理。执行的时候也需要从最后代理的对象开始。

    总结:

    PageHelper原理

    引入pageHelper的依赖,配置插件,如果需要分页,需要用到相关的工具类:

    <dependency>
        <groupId>com.github.pagehelper</groupId>
        <artifactId>pagehelper</artifactId>
        <version>5.0.0</version>
    </dependency>
    
    PageHelper.startPage(pn, 10); //pageNumber, pageSize,第几页,每页几条
    List<T> lists = Service.getAll();
    PageInfo page = new PageInfo(lists, 10);
    return page;
    

    插件的优点就是不用修改Mybatis本身的代码。

    QA:SQL改写的实现?PageHelper实现分页的原理?

    首先看一下拦截器,PageInterceptor类。

    首先判断是否需要count获取总数,默认是true。获得count之后,判断是否需要分页,如果pageSize > 0,就分页。

    这里通过getPageSql()方法生成了一个新的BoundSql:

    String pageSql = this.dialect.getPageSql(ms, boundSql, parameter, rowBounds, cacheKey);
    BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameter);
    

    getPageSql()对于不同的数据库有不同的实现:

    以MYSQL为例,实际上是添加了LIMIT语句,加上起始位置和结束位置。

    public String getPageSql(String sql, Page page, CacheKey pageKey) {
        StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
        sqlBuilder.append(sql);
        if (page.getStartRow() == 0) {
            sqlBuilder.append(" LIMIT ");
            sqlBuilder.append(page.getPageSize());
        } else {
            sqlBuilder.append(" LIMIT ");
            sqlBuilder.append(page.getStartRow());
            sqlBuilder.append(",");
            sqlBuilder.append(page.getPageSize());
            pageKey.update(page.getStartRow());
        }
    
        pageKey.update(page.getPageSize());
        return sqlBuilder.toString();
    }
    

    那么插件是怎么获取到页码和每页数量的,是怎么传递给插件的?

    ​ 这个在PageHelper.startPage()方法可以找到答案。startPage()调用了PageMethod的setLocalPage()方法,包装了一个Page对象,并把这个对象放到ThreadLocal变量中。

    protected static void setLocalPage(Page page) {
        LOCAL_PAGE.set(page);
    }
    

    而在AbstractHelperDialect中,Page对象中的翻页信息使用过getLocalPage()取出来的:

    public String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) {
        String sql = boundSql.getSql();
        Page page = this.getLocalPage();
        return this.getPageSql(sql, page, pageKey);
    }
    

    它调用的正式PageHelper的getLocalPage(),从ThreadLocal中获取到了翻页信息。

    public static <T> Page<T> getLocalPage() {
        return (Page)LOCAL_PAGE.get();
    }
    

    所以每次查询都会有一个线程私有的Page对象,它里面有页码和每页数量。

    关键类:

    使用场景
    作用 描述 实现方式
    水平分表 可以进行水平分表的查询 对query update进行拦截,在接口上添加注解,通过反射获取接口注解,根据主键上的配置进行分表,修改原SQL
    数据脱敏 手机号和身份证在数据库完整存储,屏蔽手机号的中间四位。 query--对结果集脱敏
    菜单权限控制 不同的用户登录,查询菜单权限表时获得不同的结果,在签单展示不同的菜单 对query方法进行拦截,在方法上添加注解,根据权限配置,以及用户登录信息,在SQL上加上权限过滤条件

    整合Spring

    关键配置

    pom依赖

    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis-spring</artifactId>
        <version>2.0.4</version>
    </dependency>
    
    <!-- mybatis -->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.1</version>
    </dependency>
    

    SqlSessionFactoryBean

    MapperScannerConfigurer

    第一种配置一个MapperSacnnerConfigurer.

    <bean id="mapperScanner" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.xx.dao"/>
    </bean>
    

    第二种配置一个标签:

    <mybatis-spring:scan #base-package="com.xx.dao"/>
    

    采用注解的方式@MapperScan注解,比如在Spring Boot的启动类上加上一个注解:

    @MapperScan("com.xx.dao")
    

    这三种效果都是一样的。

    经过这两步(SqlSessionFactoryBean + MapperScannerConfigurer)配置以后,Mapper就可以注入到Service层了,Mybatis其他的代码和配置不需要进行任何的改动。

    它是如何实现的呢?

    只要我们理解了SqlSessionFactory、sqlSession、MapperProxy这三个对象怎么创建的,就理解了Spring继承Mybatis的原因。

    1)SqlSessionFactory在哪里创建的。

    2)SqlSession在哪里创建的。

    3)代理类在哪里创建的。

    创建会话工厂SqlSessionFactory

    在springboot需要自己实现:

    @Configuration
    public class SqlSessionConfig {
    
        private Logger logger = LoggerFactory.getLogger(SqlSessionConfig.class);
    
        @Value("${spring.datasource.jndi-name}")
        private String dataSourceJndiName;
    
        @Value("${mybatis.mapper-locations}")
        private String mapperLocations;
    
        @Bean
        public SqlSessionFactoryBean createSqlSessionFactory() {
            SqlSessionFactoryBean sqlSessionFactoryBean = null;
            try {
                // 加载JNDI配置
                Context context = new InitialContext();
                DataSource dataSource = (DataSource)context.lookup(dataSourceJndiName);
    
                // 实例SessionFactory
                sqlSessionFactoryBean = new SqlSessionFactoryBean();
                // 配置数据源
                sqlSessionFactoryBean.setDataSource(dataSource);
    
                // 加载MyBatis配置文件
                PathMatchingResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
                // 能加载多个,所以可以配置通配符(如:classpath*:mapper/**/*.xml)
                sqlSessionFactoryBean.setMapperLocations(resourcePatternResolver.getResources(mapperLocations));
                // 配置mybatis的config文件
                sqlSessionFactoryBean.setConfigLocation("mybatis-config.xml");
            } catch (Exception e) {
                logger.error("创建SqlSession连接工厂错误:{}", e);
            }
            return sqlSessionFactoryBean;
        }
    }
    
    spring:
      # db
      datasource:
        jndi-name: 'java:comp/env/jdbc/spring_db'
        
    # mybatis config
    mybatis:
      mapper-locations: classpath*:mapper/**/*.xml
    

    sqlSessionFactoryBean的内容:

    public class SqlSessionFactoryBean implements FactoryBean<SqlSessionFactory>, InitializingBean, 
    ApplicationListener<ApplicationEvent> {
    }
    

    他实现了三个接口:FactoryBean、InitializingBean、ApplicationListener

    InitializingBean

    实现了InitializingBean接口,所以要实现afterPRopertiesSet()方法,这个方法会在bean的属性值设置完的时候被调用。

    public void afterPropertiesSet() throws Exception {
        Assert.notNull(this.dataSource, "Property 'dataSource' is required");
        Assert.notNull(this.sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");
        Assert.state(this.configuration == null && this.configLocation == null || this.configuration == null 
    || this.configLocation == null, "Property 'configuration' and 'configLocation' can not specified with together");
        this.sqlSessionFactory = this.buildSqlSessionFactory();
    }
    

    在afterPropertiesSet()方法里面,通过一些检查之后,调用buildSqlSessionFactory()方法。

    这里创建了一个Configuration对象,叫做targetConfiguration。还创建了一个用来解析全局配置文件的XMLConfigBuilder。

    XMLConfigBuilder xmlConfigBuilder = null;
    Configuration targetConfiguration;
    

    判断configuration对象是否已经存在,也就是判断是否解析过。如果已经有对象,就覆盖一下属性。

    if (this.configuration != null) {
        targetConfiguration = this.configuration;
        if (targetConfiguration.getVariables() == null) {
            targetConfiguration.setVariables(this.configurationProperties);
        } else if (this.configurationProperties != null) {
            targetConfiguration.getVariables().putAll(this.configurationProperties);
        }
    }
    

    如果Configuration不存在,但配置了configLocation属性,就根据mybatis-config.xml的文件路径,构建了一个xmlConfigBuilder对象。

    else if (this.configLocation != null) {
        xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), (String)null, this.configurationProperties);
        targetConfiguration = xmlConfigBuilder.getConfiguration();
    } 
    

    如果Configuration不存在,configLocation路径也没有,只能使用默认属性去构建去给configurationProperties赋值。

    else {
        LOGGER.debug(
            () -> "Property 'configuration' or 'configLocation' not specified, using default MyBatis Configuration");
        targetConfiguration = new Configuration();
        Optional.ofNullable(this.configurationProperties).ifPresent(targetConfiguration::setVariables);
    }
    
      创建一个用来解析Mapper.xml的XMLMapperBuilder,调用了它的parse()方法。这个步骤我们主要是做了两件事情,
    

    一是把增删改查标签注册成MapperStatement对象。第二个是把接口和对应的MapperProxyFactoty工厂类注册到MapperRegistry中。

    XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
                                                             targetConfiguration, mapperLocation.toString(), 
                                                               targetConfiguration.getSqlFragments());
    xmlMapperBuilder.parse();
    

    最后返回一个DefaultSqlSessionFactory。

    return this.sqlSessionFactoryBuilder.build(targetConfiguration);
    

    总结

    ​ 通过定义一个实现了InitializingBean接口的SqlSessionFactoryBean类,里面的有个afterPropertiesSet()方法会在bean的属性值设置完的时候被调用。Spring在启动初始化这个bean的时候,完成了解析和工厂类的创建工作。

    FactoryBean

    这个类作用是让用户可以自定义实例化bean的逻辑。如果从BeanFactory中根据Bean的ID获取一个bean,它获取的其实是FactoryBean的getObject()返回的对象。

    @Override
    public SqlSessionFactory getObject() throws Exception {
        if (this.sqlSessionFactory == null) {
            afterPropertiesSet();
        }
        return this.sqlSessionFactory;
    }
    

    ApplicationListener让SqlSessionFactoryBean有能力监控应用发出的一些事件通知。

    比如这里监听了ContextRefreshListener(上下文刷新事件),会在Spring容器加载完之后执行。

    这里是检查ms是否加载完毕。

    public void onApplicationEvent(ApplicationEvent event) {
        if (failFast && event instanceof ContextRefreshedEvent) {
            // fail-fast -> check all statements are completed
            this.sqlSessionFactory.getConfiguration().getMappedStatementNames();
        }
    }
    

    SqlSessionFactoryBean用到的Spring扩展点总结:

    创建会话SqlSession

    为什么不直接使用DefaultSqlSession?

    因为它是线程不安全的。

    Note that this class is not Thread-Safe.
    
      所以,在Spring里面,我们要保证SqlSession实例的线程安全,必须为每一次请求创建一个sqlSession。但是每一次请求用openSession()自己去创建,
    

    又会比较麻烦。
    在mybatis-spring的包中,提供了一个线程安全的SqlSession的包装类,用来代替SqlSession,这个类就是SqlSessionTemplate。
    因为它是线程安全的,所以可以在所有的Dao层共享一个实例(默认是单例的)。

    Thread safe, Spring managed,
    

    SqlSessionTemplate虽然和DefaultSqlSession一样定义了数据操作的接口,但是没有自己的实现,全部调用了一个代理对象的方法。

    public <E> List<E> selectList(String statement, Object parameter) {
        return this.sqlSessionProxy.selectList(statement, parameter);
    }
    

    那么,这个代理独享怎么来的?在构造方法里面通过JDK动态代理创建:

    this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),new Class[] { SqlSession.class },
    new SqlSessionInterceptor());
    

    它是对SqlSession实现类DefaultSqlSession的代理。既然是JDK动态代理,那对代理类任意方法的调用都会走到(第三个参数)实现了InvocationHandler接口的触发管理类SqlSessionInterceptor的invoke()方法。

    SqlSessionInterceptor是一个内部类

    private class SqlSessionInterceptor implements InvocationHandler {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            SqlSession sqlSession = getSqlSession(
                SqlSessionTemplate.this.sqlSessionFactory,
                SqlSessionTemplate.this.executorType,
                SqlSessionTemplate.this.exceptionTranslator);
    
            ...
    
        }
    }
    

    ​ 这里会用getSqlSession()方法创建一个SqlSession对象,把SqlSessionFactory、执行器类型、异常解析器传进去。

    获取到sqlSession实例(DefaultSqlSession)后,在调用它的增删改查方法。

    总结

    因为DefaultSqlSession自己做不到每次请求调用产生一个新的实例,我们干脆创建一个代理类,也实现SqlSession,提供了跟DefaultSqlSession实例,
    在调用被代理对象的相应方法。
    和JdbcTemplate、RedisTemplate一样,SqlSessionTemplate可以简化Mybatis在Spring中的使用,也是Spring和Mybatis整合的最关键的一个类。
    

    ​ 怎么拿到一个SqlSessionTemplate是线程安全的,可以替换DefaultSqlSession,那么在Dao层是怎么拿到SqlSessionTemplate呢?

    ​ 可以使用new一个创建的方式,但它有三个重载的构造函数。而且这个单例的SqlSessionTemplate必须存起来放在一个地方,可以在任何需要代替DefaultSqlSession的地方都可以拿到,不能重复创建,否则就不是单例了。

    因为需要存在一个地方,所以,我们是不是可以提供一个工具类来获取单例的SqlSessionTemplate呢?

    ​ Mybatis里面和Hibernate也是一样的,它提供了一个抽象的支持类SqlSessionDaoSupport(这里Hibernate使用HibernateDaoSupport)。

    SqlSessionDaoSupport类中持有一个SqlSessionTemplate对象,并且提供了一个getSqlSession()方法,让我们获得一个SqlSessionTemplate。

    public abstract class SqlSessionDaoSupport extends DaoSupport {
    
        private SqlSessionTemplate sqlSessionTemplate;
    
        public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
            if (this.sqlSessionTemplate == null || sqlSessionFactory != this.sqlSessionTemplate.getSqlSessionFactory()) {
                this.sqlSessionTemplate = createSqlSessionTemplate(sqlSessionFactory);
            }
        }
        //其他代码省略
    }
    

    ​ 也就是说让我们Dao层继承抽象类SqlSessionDaoSupport,就自动拥有了getSqlSession()方法。调用getSqlSession()就能拿到共享的SqlSessionTemplate。

    但Dao执行SQL格式还是不够简洁。

    getSqlSession.selectOne(statement, parameter);
    

    ​ 所以我们需要先创建一个BaseDao继承SqlSessionDaoSupport。在BaseDao里面封装对数据库的操作,包括selectOne()、
    selectList()、insert()、delete()这些方法,子类就可以直接调用。

    public class BaseDao extends SqlSessionDaoSupport {
        //使用sqlSessionFactory
        @Autowired
        private SqlSessionFactory sqlSessionFactory;
    
        @Autowired
        public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
            super.setSqlSessionFactory(sqlSessionFactory);
        }
    
        /**
         * 获取Object对象
         *
         * @param statement
         * @return
         */
        public Object selectOne(String statement) {
            return getSqlSession().selectOne(statement);
        }
    
        public Object selectOne(String statement, Object parameter) {
            return getSqlSession().selectOne(statement, parameter);
        }
        //其他代码省略
    }
    

    ​ 然后让我们Dao层继承BaseDao实现Mapper接口。在实现类加上@Repository注解就可以了。但这样还是比较麻烦的还需要实现DaoImpl。

    那有没有更好的方式呢?

    ​ 我们通过上面的方式操作数据库,繁琐,而且还会出现Statement ID的硬编码问题。没有使用到JDK动态代理。那么如何解决呢?

    当我们使用Spring来调用Mybatis的时候。只需要注入一个Mapper就可以使用,那么是怎么实现的?

    接口的扫描注册

    首先Spring可以通过配置或者是注解来扫描Mapper的接口。

    其中当使用xml的时候,需要配置:

    <bean id="mapperScanner" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.xx.dao"/>
    </bean>
    

    其中MapperScannerConfigurer是来做mapper的扫描的,由上面类图可以看出:

    MapperScannerConfigurer实现了BeanDefinitionRegistryPostProcessor接口。
    BeanDefinitionRegistryPostProcessor是BeanFactoryPostProcessor的子类,里面有一个postProcessBeanDefinitionRegistry()方法。
    	实现了这个接口,就可以在Spring创建Bean之前,修改某些Bean在容器中的定义。Spring创建Bean之前会调用这个方法。
    

    MapperScannerConfigurer重写了postProcessBeanDefinitionRegistry(),那他实现了什么功能呢?

    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
        if (this.processPropertyPlaceHolders) {
            processPropertyPlaceHolders();
        }
    
        ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
        scanner.setAddToConfig(this.addToConfig);
        scanner.setAnnotationClass(this.annotationClass);
        scanner.setMarkerInterface(this.markerInterface);
        scanner.setSqlSessionFactory(this.sqlSessionFactory);
        scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
        scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
        scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
        scanner.setResourceLoader(this.applicationContext);
        scanner.setBeanNameGenerator(this.nameGenerator);
        scanner.setMapperFactoryBeanClass(this.mapperFactoryBeanClass);
        if (StringUtils.hasText(lazyInitialization)) {
            scanner.setLazyInitialization(Boolean.valueOf(lazyInitialization));
        }
        scanner.registerFilters();
        scanner.scan(
            StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
    }
    

    ​ 在这个方法里面:创建了一个scanner对象,然后设置属性。

    ClassPathBeanDefinitionScanner的scan();

    public int scan(String... basePackages) {
        int beanCountAtScanStart = this.registry.getBeanDefinitionCount();
    
        doScan(basePackages);
    
        // Register annotation config processors, if necessary.
        if (this.includeAnnotationConfig) {
            AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
        }
    
        return (this.registry.getBeanDefinitionCount() - beanCountAtScanStart);
    }
    

    这里会调用它的子类方法ClassPathMapperScanner的doScan()方法:

    public Set<BeanDefinitionHolder> doScan(String... basePackages) {
        Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
    
        if (beanDefinitions.isEmpty()) {
            LOGGER.warn(() -> "No MyBatis mapper was found in '" + Arrays.toString(basePackages)
                        + "' package. Please check your configuration.");
        } else {
            processBeanDefinitions(beanDefinitions);
        }
    
        return beanDefinitions;
    }
    

    ​ 子类ClassPathMapperScanner又调用了父类ClassPathBeanDefinitionScanner的doScan()扫描所有的接口,把接口全部添加到beanDefinitions中。

    ​ processBeanDefinitions()方法里面,在注册beanDefinitions的时候。BeanClass被改为MapperFactoryBean。(这里有注释讲解)

    private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
        GenericBeanDefinition definition;
        for (BeanDefinitionHolder holder : beanDefinitions) {
            definition = (GenericBeanDefinition) holder.getBeanDefinition();
            String beanClassName = definition.getBeanClassName();
            LOGGER.debug(() -> "Creating MapperFactoryBean with name '" + holder.getBeanName() + "' and '" + beanClassName
                         + "' mapperInterface");
    
            // the mapper interface is the original class of the bean
            // but, the actual class of the bean is MapperFactoryBean
            definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName); // issue #59
            definition.setBeanClass(this.mapperFactoryBeanClass);
            
            //其他代码省略
        }
    }
    

    ​ 这就是说,所有的Mapper接口,在容器里面都被注册成一个支持泛型的MapperFactoryBean了。

    ​ 为什么要注册成它呢?那注入使用的时候,也是这个对象,这个对象有什么作用呢?

    MapperFactoryBean这个类:

    public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {}
    

    ​ 这个类继承了抽象类SqlSessionDaoSupport,这就解决了我们的第一个问题,现在每一个注入Mapper的地方,都可以拿到SqlSessionTemplate。

    ​ 那有没有使用到MapperProxy呢?如果注册的是MapperFactoryBean,难道注入使用的也是MapperFactoryBean吗?但这个类并不是代理类。

    接口注入使用

    ​ 所以注入的是一个什么对象呢?这里MapperFactoryBean也实现了FactoryBean。它可以在getObject()中获取Bean实例的行为。

    public T getObject() throws Exception {
        return getSqlSession().getMapper(this.mapperInterface);
    }
    

    ​ 它并没有直接返回一个MapperFactoryBean。而是调用了SqlSessionTemplate的getMapper()方法。SqlSessionTemplate的本质是一个代理,所以它最终会调用DefaultSqlSession的getMapper()方法,最后返回的还是一个JDK的动态代理。

    总结
    1.提供了SqlSession的代替品SqlSessionTemplate,里面有一个实现了InvocationHandler的内部SqlSessionIntercepter,本质是对SqlSession的代理。
    2.提供了获取SqlSessionTempldate的抽象类SqlSessionDaoSupport。
    3.扫描Mapper接口,注册到容器中的是MapperFactoryBean,它继承了SqlSessionDaoSupport,可以获得SqlSessionTempldate。
    4.把Mapper注入使用的时候,调用的是getObject()方法,它实际上是调用了SqlSessionTemplate的getMapper()方法,注入了一个人JDK动态代理对象。
    5.执行Mapper接口的任意操作,会走到触发管理类MapperProxy,进入SQL处理流程。
    

    Mybatis调用的设计模式总结

  • 相关阅读:
    mybatis的延时加载缓存机制
    mybatis03
    事务
    codeforces-200B
    codeforces-339B
    codeforces-492B
    codeforces-266B
    codeforces-110A
    codeforces-887B
    codeforces-69A
  • 原文地址:https://www.cnblogs.com/snail-gao/p/13275400.html
Copyright © 2011-2022 走看看