一.MyBatis配置解析
今天到了SSM三个框架的最后一个MyBatis,MyBatis是一个针对数据库进行操作的框架,基于jdbc产生的一个便捷的操作数据库的框架,底层是对jdbc的封装,简化对数据库的操作。
主要通过对配置文件的解析来连接数据库,然后通过XML文件得到SQL,有了数据库连接+SQL语句,就可以去操作数据库了。
我们先来看一下MyBatis-config.xml这个配置文件里的基础参数设置:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 全局参数 -->
<settings>
<!-- 使全局的映射器启用或禁用缓存。 -->
<setting name="cacheEnabled" value="true"/>
<!-- 全局启用或禁用延迟加载。当禁用时,所有关联对象都会即时加载。 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 当启用时,有延迟加载属性的对象在被调用时将会完全加载任意属性。否则,每种属性将会按需要加载。 -->
<setting name="aggressiveLazyLoading" value="true"/>
<!-- 是否允许单条sql 返回多个数据集 (取决于驱动的兼容性) default:true -->
<setting name="multipleResultSetsEnabled" value="true"/>
<!-- 是否可以使用列的别名 (取决于驱动的兼容性) default:true -->
<setting name="useColumnLabel" value="true"/>
<!-- 允许JDBC 生成主键。需要驱动器支持。如果设为了true,这个设置将强制使用被生成的主键,有一些驱动器不兼容不过仍然可以执行。 default:false -->
<setting name="useGeneratedKeys" value="false"/>
<!-- 指定 MyBatis 如何自动映射 数据基表的列 NONE:不映射 PARTIAL:部分 FULL:全部 -->
<setting name="autoMappingBehavior" value="PARTIAL"/>
<!-- 这是默认的执行类型 (SIMPLE: 简单; REUSE: 执行器可能重复使用prepared statements语句;BATCH: 执行器可以重复执行语句和批量更新) -->
<setting name="defaultExecutorType" value="SIMPLE"/>
<!-- 使用驼峰命名法转换字段。 -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!-- 设置本地缓存范围 session:就会有数据的共享 statement:语句范围 (这样就不会有数据的共享 ) defalut:session -->
<setting name="localCacheScope" value="SESSION"/>
<!-- 设置但JDBC类型为空时,某些驱动程序 要指定值,default:OTHER,插入空值时不需要指定类型 -->
<setting name="jdbcTypeForNull" value="NULL"/>
<!-- 打印SQL -->
<!-- <setting name="logImpl" value="STDOUT_LOGGING" /> -->
</settings>
<!-- 插件配置 -->
<plugins>
<plugin interceptor="com.xxx.lrt.frame.core.dao.interceptor.PaginationInterceptor">
<property name="jdbcType" value="mysql"/>
</plugin>
</plugins>
</configuration>
我们都知道,在SSM层级里,service实现层调用DAO层方法,其实就等同于在执行SQL,最终就会从数据库里得到数据,那么这个DAO层里的方法,是如何跟XML里的SQL语句对应起来的呢?哪个dao对应哪个映射文件,哪个方法又对应哪个语句。我们再来看一个mapping文件的开头:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xxx.lrt.modules.app.dao.CheckOrderAppDao">
这里的mapper使用项目绝对路径指向了一个java类,这个java类就对应起了这个映射xml文件,这是文件之间的对应,再来看一下sql语句和方法的对应:
<select id="selectUserRoleAndLineId" parameterType="WorkOrderApp" resultType="WorkOrderApp">
SELECT
b.role_id AS userRole,
a.line_id AS lineId
FROM
t_sys_user a,
t_sys_user_role b,
t_sys_role_app c
WHERE
CODE = #{userId}
AND b.ROLE_ID=c.ROLE_ID
AND a.user_id=b.USER_ID
AND c.APP_SYS_CODE=#{sysCode}
AND a.DEL_FLAG=0;
</select>
这是xml文件里的一个sql语句,有一个id叫做selectUserRoleAndLineId,而在对应的dao层文件中方法声明如下:
// 查询用户权限+线路
WorkOrderApp selectUserRoleAndLineId(WorkOrderApp workorderapp);
方法名、入参形式、返回值形式这三点都需要对应上,这样就形成了文件对文件、方法对语句的映射,我们在service层调用dao层文件+方法时候,就可以准确地找到对应的sql语句执行。
讲过了对应的sql找寻方法,我们来看一下,数据库连接是怎么执行的,毕竟我们需要先打开数据库,才能去执行语句。
在spring的配置文件spring-context.xml中,有着很多我们配置好的bean,其中就有关于mybatis的配置:
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="typeAliasesPackage" value="com.xxx.lrt"/>
<property name="typeAliasesSuperType" value="com.xxx.lrt.frame.core.entity.BaseEntity"/>
<property name="mapperLocations" value="classpath:/mappings/**/**/*.xml"/>
<property name="configLocation" value="classpath:/mybatis-config.xml"></property>
</bean>
这是一个名为sqlSessionFactory的bean,里面有五个属性,typeAliases不需要关心,我们重点看一下其余三个,第一个dataSorce,这就是数据源的配置,它ref指向了一个叫做dataSorce的bean,我们看到,每一个property都有对应的指向,有的直接给值value,有个给一个ref指向。注意看这个mapperLocations,他的值指向了一个路径,这个路径是项目里映射文件的路径,而基础配置的加载configLocation则指向了mybatis-config.xml,我们顺便看一下项目整体路径:
大家可以看到映射xml的文件位置,在mappings里面,其余的配置文件跟mappings同级。
那么这个dataSource又是怎么注册的呢:
<!-- 动态配置数据源 -->
<bean id="dataSource" class="com.xxx.lrt.frame.core.datasource.DynamicDataSource">
<property name="targetDataSources">
<map key-type="java.lang.String">
<entry value-ref="defaultDataSource" key="defaultDataSource"></entry>
<entry value-ref="dcDataSource" key="dcDataSource"></entry>
<entry value-ref="ztmDataSource" key="ztmDataSource"></entry>
</map>
</property>
<property name="defaultTargetDataSource" ref="defaultDataSource"></property>
</bean>
可以看到这是一个多数据源,有defaultDataSource、dcDataSource、ztmDataSource三个数据源,他们都有一个key。再来看key指向什么(以defaultDataSource为例):
<!-- 数据源配置, 使用 druid 数据库连接池 -->
<bean id="defaultDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<!-- 数据源驱动类可不写,Druid默认会自动根据URL识别DriverClass -->
<property name="driverClassName" value="${jdbc.driver}" />
<!-- 基本属性 url、user、password -->
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
<!-- 配置初始化大小、最小、最大 -->
<property name="initialSize" value="${jdbc.pool.init}" />
<property name="minIdle" value="${jdbc.pool.minIdle}" />
<property name="maxActive" value="${jdbc.pool.maxActive}" />
<!-- 配置获取连接等待超时的时间 -->
<property name="maxWait" value="60000" />
<!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
<property name="timeBetweenEvictionRunsMillis" value="60000" />
<!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
<property name="minEvictableIdleTimeMillis" value="300000" />
<property name="validationQuery" value="${jdbc.testSql}" />
<property name="testWhileIdle" value="true" />
<property name="testOnBorrow" value="false" />
<property name="testOnReturn" value="false" />
<!-- 打开PSCache,并且指定每个连接上PSCache的大小(Oracle使用)-->
<property name="poolPreparedStatements" value="true" />
<property name="maxPoolPreparedStatementPerConnectionSize" value="20" />
<!-- 配置监控统计拦截的filters,stat,slf4j -->
<property name="filters" value="stat,config" />
<property name="connectionProperties" value="${config.decrypt}" />
</bean>
大家看到,这里面并没有直接把数据库地址、账号、密码等信息写在spring-context.xml里,而是采用了加载配置文件的方式,这个jdbc是从哪来的呢?spring-context.xml又是怎么获取其他配置文件的信息的呢?因为在spring-context.xml一开始,就有一个属性配置:
<context:property-placeholder ignore-unresolvable="true" location="classpath:config.properties" />
大家看到,context已经对config.properties进行了加载,大家去回看以下我上面截图的项目路径,配置文件里第一个就是config.properties,这里面包含了jdbc、web、redis的一些配置,看下jdbc的部分:
#mysql database setting
jdbc.type=mysql
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=${db-url}
jdbc.username=${db-username}
jdbc.password=${db-password}
没想到吧,明文的地址和账号密码还是没写在这里。到底在哪儿呢,这里的{db-url}又是怎么找过去的呢?在config.properties文件里,我没有做任何的声明和指向,按照惯性思维,就类似于spring-context.xml需要用到config.properties的变量一样,需要先在spring-context.xml里面加载一下。现在config.properties我没有去加载其他的文件,那么我又是怎么取到其他文件的变量的呢?首先,这里的EL符指向的是POM.XML,看下pom.xml里对应的变量:
<profile>
<id>dev</id>
<properties>
<db-url><![CDATA[jdbc:mysql://aaaaaaa:3306/db_drds?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true]]></db-url>
<db-username>aaaaaa</db-username>
<db-password>aaaaaa</db-password>
<db-url1><![CDATA[jdbc:mysql://aaaaaaaaaa:3308/db_dc?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true]]></db-url1>
<db-username1>aaaaaa</db-username1>
<db-password1>aaaaaa</db-password1>
<db-url2><![CDATA[jdbc:mysql://aaaaaa:3306/db_psd_bs?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true]]></db-url2>
<db-username2>aaaaaa</db-username2>
<db-password2>aaaaaa</db-password2>
<ds-decrypt>config.decrypt=false</ds-decrypt>
<!-- redis -->
<redis.host>aaaaaa</redis.host>
<!-- log4j -->
<logs.basedir>${catalina.base}</logs.basedir>
<!-- logback level -->
<log.level>INFO</log.level>
</properties>
<activation>
<activeByDefault>true</activeByDefault>
<jdk>${jdk.version}</jdk>
</activation>
</profile>
为了保密起见,里面的url和账号密码我都用a代替,端口号和数据库名称就无所谓了。
可以看到,明文的SQL连接、账号密码确实就写在这里了,那么config.properties又是怎么取到这里的内容的呢,因为在pom.xml里有以下代码:
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
</build>
大家往上翻一下,看一下项目的路径,就可以发现config.properties的路径就位于 src/main/resources这个路径下,这几行代码的意思就是,在编译的时候,pom.xml允许此路径下所有文件对pom.xml配置变量进行访问,当然也就包括了config.properties,有些写法是精准指向config.properties文件,意思就它一个可以访问,但是由于我这类路径下诸如spring-context、spring-mvc此类的文件都需要访问pom文件,所以比较粗暴的写法就是精确到路径,大家都能取。这就是config.properties与maven的pom.xml文件之间的取值。
至此,整个DAO层方法对mapping映射的关系、mybaties的配置、sql配置的读取都通过实际项目配置方式讲完了。接下来讲讲mybatis是怎么运行的。
二.Mybatis在SSM中的运行原理
前文提过了,其实mybatis很早就装载了名为“SqlSessionFactory”的bean,这个类是mybatis的核心类,其最主要的功能就是提供创建mybatis核心接口:SqlSession。至于怎么给参数来创建SqlSessionFactory,我们前面已经讲过,也给出了mybatis-config.xml配置文件的内容,我们讲一下SqlSession的运行过程。
SqlSession的作用相当于jdbc的一个connection对象,代表着一个连接资源。具体来说,他有三大功能:①获取mapper接口②发送sql语句给数据库③控制数据库事务
先看一下,mybatis是怎么获取到SqlSession的,很简单,通过SqlSessionFactory,前文提到了sqlSessionFacoty的bean装载,看下sqlSessionFacoty结构:
public interface SqlSessionFactory {
SqlSession openSession();
SqlSession openSession(boolean autoCommit);
SqlSession openSession(Connection connection);
SqlSession openSession(TransactionIsolationLevel level);
SqlSession openSession(ExecutorType execType);
SqlSession openSession(ExecutorType execType, boolean autoCommit);
SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);
SqlSession openSession(ExecutorType execType, Connection connection);
Configuration getConfiguration();
}
这样我们就得到了一个SqlSession,SqlSession是通过获取mapper接口,然后发送sql的,mapper接口并不能单独直接运行,mybatis运用了动态代理技术使得mapper接口得以运行,mybatis为mapper生成一个代理对象,由这个代理对象去处理相关的逻辑。那么mybatis是如何实现动态代理的呢?
sqlSession的有四大对象Executor、StatementHandler、ParameterHandler、ResultSetHandler:
Executor代表执行器,由它调度StatementHandler、ParameterHandler、ResultSetHandler等来执行对应的SQL
。其中StatementHandler是最重要的。
StatementHandler的作用是使用数据库的Statement
执行操作。
ParameterHandler是用来处理SQL
参数的。
ResultSetHandler是进行数据集ResultSet
的封装返回处理的,它相当复杂,好在我们不常用它。
运行过程是通过执行器Executor调度StatementHandler,而StatementHandler分为三个步骤:
①prepared预编译②parameterize设置参数③query/update执行sql
其中parameterize是调用parameterHandler的方法来进行设置,参数则根据类型处理器typeHandler处理。query/update方法通过ResultSetHandler进行处理结果的封装,如果是update语句,就返回整数,以下举个实际update语句代码例子:
<update id="distributeCheckPlan" parameterType="WorkOrderApp" >
UPDATE t_ms_check_plan
SET is_revisable = 0,
p_state = #{pState},
p_team_id = #{teamId},
oper_id=#{userId},
oper_time=now()
WHERE
id = #{planId}
</update>
结合之前的sql语句代码,会发现,我这个update语句缺了一个东西,就是resultType,如果是个查询语句,则resultType必须要有,里面是数据返回的类型,但是update就可以不写,再看下这个sql对应的DAO层方法:
// 派发任务
int distributeCheckPlan(WorkOrderApp workorderapp);
这个方法,我用的是int来定义的,这就是因为update语句执行过后,mybatis会返回回来整数。
如果不是update语句,sqlSession就会通过typeHandler取处理结果类型,也就是我给的resultType参数,然后用objectFactory提供的规则组装类型,最终返回给调用者。
以上就粗略得介绍完了mybatis的运行原理,没有太过深入地讲,一是因为我尚且有些东西没弄清,摊开来讲也过于复杂,例如为了提高效率mybatis的缓存机制之类的。二是在我们实际使用中,其实不需要了解底层的原理(好像被我之前说的《工作内容梳理》打脸了),等有时间了,单独开篇讲一下mybatis的底层实现。