Mybatis的底层原理
之前说明的都是Mybatis的应用(61章博客开始(包括其后面的相关博客)),现在来说明他为什么可以那样做
我们继续分析分析JDBC操作的问题:
首先给出具体sql语句:
链接:https://pan.baidu.com/s/1KVIkn4A48tJbkposXGiBLA
提取码:alsk
实体类:
package com ;
public class User {
Integer id;
String userName;
public Integer getId ( ) {
return id;
}
public void setId ( Integer id) {
this . id = id;
}
public String getUserName ( ) {
return userName;
}
public void setUserName ( String userName) {
this . userName = userName;
}
@Override
public String toString ( ) {
return "User{" +
"id=" + id +
", userName='" + userName + '\'' +
'}' ;
}
}
依赖:
< dependencies>
< dependency>
< groupId> mysql</ groupId>
< artifactId> mysql-connector-java</ artifactId>
< version> 5.1.8</ version>
</ dependency>
</ dependencies>
测试类:
package com ;
import java. sql. * ;
public class Mybatis {
public static void main ( String [ ] args) throws Exception {
Connection connection = null ;
PreparedStatement preparedStatement = null ;
ResultSet resultSet = null ;
try {
Class . forName ( "com.mysql.jdbc.Driver" ) ;
connection = DriverManager . getConnection ( "jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8" , "root" , "123456" ) ;
String sql = "select * from user where username = ?" ;
preparedStatement = connection. prepareStatement ( sql) ;
preparedStatement. setString ( 1 , "tom" ) ;
resultSet = preparedStatement. executeQuery ( ) ;
User user = new User ( ) ;
while ( resultSet. next ( ) ) {
int id = resultSet. getInt ( "id" ) ;
String username = resultSet. getString ( "username" ) ;
user. setId ( id) ;
user. setUserName ( username) ;
}
System . out. println ( user) ;
} catch ( Exception e) {
e. printStackTrace ( ) ;
} finally {
if ( resultSet != null ) {
try {
resultSet. close ( ) ;
} catch ( SQLException e) {
e. printStackTrace ( ) ;
}
}
if ( preparedStatement != null ) {
try {
preparedStatement. close ( ) ;
} catch ( SQLException e) {
e. printStackTrace ( ) ;
}
}
if ( connection != null ) {
try {
connection. close ( ) ;
} catch ( SQLException e) {
e. printStackTrace ( ) ;
}
}
}
}
}
首先说明jdbc的问题:
1:数据库连接创建、释放频繁造成系统资源浪费,从而影响系统性能
2:Sql语句连接信息或者sql语句(他们简称为sql)在代码中有硬编码,造成代码不易维护,实际应用中sql变化的可能较大,sql变动需要改变java代码,比如说,如果使用配置文件存放,那么若代码有自动识别是否修改而改变的,那么改动即可,若没有,那么他一个配置是可以给多个类使用的,即我们只需要改动一次即可部署完毕,而由于一般我们都只会提交class文件,原来的还要继续编译(找源代码),而不能直接的运行,即从上不难看出解决硬编码问题的好处
3:使用preparedStatement向占有位符号传参数存在硬编码,因为sql语句的where条件不一定,可能 多也可能少,修改sql还要修改代码,系统不易维护(也可以归属于sql语句)
4:对结果集解析存在硬编码(如查询列名),sql变化导致解析代码变化,系统不易维护,如果能将数据库记录封装成pojo对象解析比较方便
问题解决思路:
1:使用数据库连接池初始化连接资源
2:将sql语句抽取到xml配置文件中,注意:这里最好将sql语句和sql连接信息分开,虽然可以在一个配置文件中,但是由于sql会变动很大,可能造成在自动识别中需要识别更多的信息,虽然其他方面没有问题,但是也是一个解决的好处,除了这样且也便于维护
3:使用反射、内省等底层技术,自动将实体与表进行属性与字段的自动映射,内省:Java内省机制是指通过标准的get/set方法(利用反射调用成员方法)对成员属性进行操作(实际上也是反射,只是对这个操作进行另外一种称呼而已),而不是直接通过反射对属性操作,在Mybatis的sql语句中,就使用到了这个(说明set时就是这样,如类似的set后面名称可以忽略大小写等等,所以是利用了反射,要不然怎么判断忽略呢,当然,这里我们不操作这种情况,所以这里了解即可,所以配置文件中的名称最好要对应)
为了完成上面的要求,现在我们来自定义框架(部分)
自定义框架设计:
使用端(也就是项目):
提供核心配置文件:
sqlMapConfig.xml:存放数据源信息,并引入Mapper.xml
Mapper.xml:sql语句的配置文件信息
注意:上面配置文件名称是可以随便写的,只是为了对标Mybatis来弄成这样而已,让你更加的理解Mybatis的工作流程
框架端(可以认为我们编写的框架,或者说jar包):
1:读取配置文件
一般来说读取完成以后以流的形式存在(getResourceAsSteam(配置文件路径),配置文件是sqlMapConfig.xml),但是我们不能将读取到的配置信息以流的形式存放在内存中,因为不好操作,所以我们可以提取将他们的信息来进行保存好(当然,提取步骤在后面的解析配置文件中说明),即先进行操作完毕,而不是在使用时进行提取,那么我们可以创建javaBean来存储
第一个javaBean:Configuration,用来存放数据库基本信息(一般是sqlMapConfig.xml的信息)和Map<唯一标识,Mapper> ,唯一标识:namespace + “.” + id
第二个javaBean:MappedStatement(Mapper的简称,一般是Mapper.xml的信息),主要是操作sql语句、statement类型(一般是操作标识,即定位的,当然大多数是代表本身,比如select,delete等等的id属性)、输入参数java类型、输出参数java类型等等
2:解析配置文件
创建sqlSessionFactoryBuilder类:
方法:sqlSessionFactory build(流参数):
第一:使用dom4j解析配置文件,将解析出来的内容封装到Configuration和MappedStatement中
第二:创建SqlSessionFactory的实现类DefaultSqlSession
3:SqlSessionFactory:
方法:openSession(),来获取sqlSession接口的实现类实例对象
4:sqlSession接口及实现类:主要封装crud方法,可以任务是会话对象,即代码平台(或者说语句执行平台)
方法:selectList(String statementId,Object param):查询所有
selectOne(String statementId,Object param):查询单个
具体实现:封装JDBC完成对数据库表的查询操作
涉及到的设计模式:
Builder构建者设计模式、工厂模式、代理模式
上面具体流程只是大致说明完毕,具体细节建议看后面的代码就行 ,实际上可以看出我们就是来完成Mybatis的底层操作,现在开始实现:
在使用端项目中创建配置配置文件
创建 sqlMapConfig.xml(资源文件夹下):
< configuration>
< property name = " driverClass" value = " com.mysql.jdbc.Driver" > </ property>
< property name = " jdbcUrl" value = " jdbc:mysql:///mybatis" > </ property>
< property name = " user" value = " root" > </ property>
< property name = " password" value = " 123456" > </ property>
< mapper resource = " mapper.xml" > </ mapper>
</ configuration>
mapper.xml:
< mapper namespace = " User" >
< select id = " selectOne" paramterType = " com.User" resultType = " com.User" >
select * from user where id = #{id} and username =#{username}
< !--id最好就是id,因为我们并没有操作什么大小写,或者get和set的操作,这里主要是类似于aClass.getDeclaredField的方法,他是必须-要对应参数存在的,否则报错,并且我们也没有在之前进行设置忽略的相关操作(比如可以操作try的不报错的循环判断,循环退出后,再考虑是否存在,从而考虑报错)->
</ select>
< select id = " selectList" resultType = " com.User" >
select * from user
</ select>
</ mapper>
上面我们并没有操作什么约束,因为我们只是测试而已,所以只要符合xml格式即可,这里建议将mapper修改成UserMapper,这样可以有效的知道是服务于那个实体类的,有利于维护,那么对应的sqlMapConfig配置文件中对应位置也记得要修改
至此,我们与定义的读取配置文件的数据都操作完毕,现在,我们主要从后端代码中拿取这些数据了
User实体(与之前一样):
package com ;
public class User {
Integer id;
String username;
String password;
String birthday;
public Integer getId ( ) {
return id;
}
public void setId ( Integer id) {
this . id = id;
}
public String getUsername ( ) {
return username;
}
public void setUsername ( String username) {
this . username = username;
}
public String getPassword ( ) {
return password;
}
public void setPassword ( String password) {
this . password = password;
}
public String getBirthday ( ) {
return birthday;
}
public void setBirthday ( String birthday) {
this . birthday = birthday;
}
@Override
public String toString ( ) {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
", birthday='" + birthday + '\'' +
'}' ;
}
}
再创建一个Maven工程(与原来的同级,当然是子项目也行,这里简称为框架项目 )并且导入需要用到的依赖坐标(主要在以后作为jar包来使用的)
<?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" >
< parent>
< artifactId> mybatis</ artifactId>
< groupId> org.example</ groupId>
< version> 1.0-SNAPSHOT</ version>
</ parent>
< modelVersion> 4.0.0</ modelVersion>
< artifactId> myba</ artifactId>
< properties>
< maven.compiler.source> 11</ maven.compiler.source>
< maven.compiler.target> 11</ maven.compiler.target>
</ properties>
< dependencies>
< dependency>
< groupId> mysql</ groupId>
< artifactId> mysql-connector-java</ artifactId>
< version> 5.1.17</ version>
</ dependency>
< dependency>
< groupId> c3p0</ groupId>
< artifactId> c3p0</ artifactId>
< version> 0.9.1.2</ version>
</ dependency>
< dependency>
< groupId> log4j</ groupId>
< artifactId> log4j</ artifactId>
< version> 1.2.12</ version>
</ dependency>
< dependency>
< groupId> junit</ groupId>
< artifactId> junit</ artifactId>
< version> 4.10</ version>
</ dependency>
< dependency>
< groupId> dom4j</ groupId>
< artifactId> dom4j</ artifactId>
< version> 1.6.1</ version>
</ dependency>
< dependency>
< groupId> jaxen</ groupId>
< artifactId> jaxen</ artifactId>
< version> 1.1.6</ version>
</ dependency>
</ dependencies>
</ project>
Resources类(包路径记得在里面看):
package com. Res ;
import java. io. InputStream ;
public class Resources {
public static InputStream getResourceAsSteam ( String path) {
InputStream resourceAsStream = Resources . class . getClassLoader ( ) . getResourceAsStream ( path) ;
return resourceAsStream;
}
}
现在我们可以在主项目中进行测试:
但是首先我们需要导入依赖包:
< dependency>
< groupId> junit</ groupId>
< artifactId> junit</ artifactId>
< version> 4.10</ version>
</ dependency>
< dependency>
< groupId> org.example</ groupId>
< artifactId> test</ artifactId>
< version> 1.0-SNAPSHOT</ version>
</ dependency>
然后创建test类进行测试:
package com ;
import com. Res . Resources ;
import org. junit. Test ;
import java. io. InputStream ;
public class test {
@Test
public void res ( ) {
InputStream resourceAsSteam = Resources . getResourceAsSteam ( "sqlMapConfig.xml" ) ;
}
}
现在我们回到框架项目,编写如下类:
package com. pojo ;
public class MappedStatement {
private String id;
private String sql;
private Class < ? > paramterType;
private Class < ? > resultType;
public String getId ( ) {
return id;
}
public void setId ( String id) {
this . id = id;
}
public String getSql ( ) {
return sql;
}
public void setSql ( String sql) {
this . sql = sql;
}
public Class < ? > getParamterType ( ) {
return paramterType;
}
public void setParamterType ( Class < ? > paramterType) {
this . paramterType = paramterType;
}
public Class < ? > getResultType ( ) {
return resultType;
}
public void setResultType ( Class < ? > resultType) {
this . resultType = resultType;
}
}
再创建如下:
package com. pojo ;
import javax. sql. DataSource ;
import java. util. HashMap ;
import java. util. Map ;
public class Configuration {
private DataSource dataSource;
private Map < String , MappedStatement > mappedStatementMap = new HashMap < String , MappedStatement > ( ) ;
public DataSource getDataSource ( ) {
return dataSource;
}
public void setDataSource ( DataSource dataSource) {
this . dataSource = dataSource;
}
public Map < String , MappedStatement > getMappedStatementMap ( ) {
return mappedStatementMap;
}
public void setMappedStatementMap ( Map < String , MappedStatement > mappedStatementMap) {
this . mappedStatementMap = mappedStatementMap;
}
}
好了对应的类创建完毕,那么读取配置文件需要的步骤操作完毕,现在开始进行完成解析配置文件,我们继续再框架项目中添加如下的类:
package com. config ;
import com. Res . Resources ;
import com. mchange. v2. c3p0. ComboPooledDataSource ;
import com. pojo. Configuration ;
import org. dom4j. Document ;
import org. dom4j. DocumentException ;
import org. dom4j. Element ;
import org. dom4j. io. SAXReader ;
import java. beans. PropertyVetoException ;
import java. io. InputStream ;
import java. util. List ;
import java. util. Properties ;
public class XMLConfigerBuilder {
private Configuration configuration;
public XMLConfigerBuilder ( Configuration configuration) {
this . configuration = new Configuration ( ) ;
}
public Configuration parseConfiguration ( InputStream inputStream) throws Exception {
Document document = new SAXReader ( ) . read ( inputStream) ;
Element rootElement = document. getRootElement ( ) ;
List < Element > propertyElements = rootElement. selectNodes ( "//property" ) ;
Properties properties = new Properties ( ) ;
for ( Element propertyElement : propertyElements) {
String name = propertyElement. attributeValue ( "name" ) ;
String value = propertyElement. attributeValue ( "value" ) ;
properties. setProperty ( name, value) ;
}
ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource ( ) ;
comboPooledDataSource. setDriverClass ( properties. getProperty ( "driverClass" ) ) ;
comboPooledDataSource. setJdbcUrl ( properties. getProperty ( "jdbcUrl" ) ) ;
comboPooledDataSource. setUser ( properties. getProperty ( "user" ) ) ;
comboPooledDataSource. setPassword ( properties. getProperty ( "password" ) ) ;
configuration. setDataSource ( comboPooledDataSource) ;
List < Element > mapperElements = rootElement. selectNodes ( "//mapper" ) ;
XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder ( configuration) ;
for ( Element mapperElement : mapperElements) {
String mapperPath = mapperElement. attributeValue ( "resource" ) ;
InputStream resourceAsSteam = Resources . getResourceAsSteam ( mapperPath) ;
xmlMapperBuilder. parse ( resourceAsSteam) ;
}
return configuration;
}
}
需要的对应类:
package com. config ;
import com. pojo. Configuration ;
import com. pojo. MappedStatement ;
import org. dom4j. Document ;
import org. dom4j. DocumentException ;
import org. dom4j. Element ;
import org. dom4j. io. SAXReader ;
import java. io. InputStream ;
import java. util. List ;
public class XMLMapperBuilder {
private Configuration configuration;
public XMLMapperBuilder ( Configuration configuration) {
this . configuration = configuration;
}
public void parse ( InputStream inputStream) throws Exception {
Document document = new SAXReader ( ) . read ( inputStream) ;
Element rootElement = document. getRootElement ( ) ;
String namespace = rootElement. attributeValue ( "namespace" ) ;
List < Element > select = rootElement. selectNodes ( "select" ) ;
for ( Element element : select) {
String id = element. attributeValue ( "id" ) ;
String paramterType = element. attributeValue ( "paramterType" ) ;
String resultType = element. attributeValue ( "resultType" ) ;
Class < ? > paramterTypeClass = getClassType ( paramterType) ;
Class < ? > resultTypeClass = getClassType ( resultType) ;
String key = namespace + "." + id;
String textTrim = element. getTextTrim ( ) ;
MappedStatement mappedStatement = new MappedStatement ( ) ;
mappedStatement. setId ( id) ;
mappedStatement. setParamterType ( paramterTypeClass) ;
mappedStatement. setResultType ( resultTypeClass) ;
mappedStatement. setSql ( textTrim) ;
configuration. getMappedStatementMap ( ) . put ( key, mappedStatement) ;
}
}
private Class < ? > getClassType ( String paramterType) throws Exception {
if ( paramterType== null ) {
return null ;
}
Class < ? > aClass = Class . forName ( paramterType) ;
return aClass;
}
}
至此,上面的两个类,使得配置文件的信息得以保存,并且放在Configuration里面,现在我们需要如下的类来进行操作:
package com. sqlsession ;
import com. config. XMLConfigerBuilder ;
import com. pojo. Configuration ;
import org. dom4j. DocumentException ;
import java. beans. PropertyVetoException ;
import java. io. InputStream ;
public class SqlSessionFactoryBuilder {
private Configuration configuration;
public SqlSessionFactoryBuilder ( ) {
this . configuration = new Configuration ( ) ;
}
public SqlSessionFactory build ( InputStream inputStream) throws Exception {
XMLConfigerBuilder xmlConfigerBuilder = new XMLConfigerBuilder ( configuration) ;
Configuration configuration = xmlConfigerBuilder. parseConfiguration ( inputStream) ;
SqlSessionFactory sqlSessionFactory = new DefaultSqlSessionFactory ( configuration) ;
return sqlSessionFactory;
}
}
package com. sqlsession ;
public interface SqlSessionFactory {
public SqlSession openSession ( ) ;
}
package com. sqlsession ;
import java. sql. SQLException ;
import java. util. List ;
public interface SqlSession {
public < E > List < E > selectList ( String statementId, Object . . . param) throws Exception ;
public < T > T selectOne ( String statementId, Object . . . params) throws Exception ;
public void close ( ) throws SQLException ;
}
package com. sqlsession ;
import com. pojo. Configuration ;
public class DefaultSqlSessionFactory implements SqlSessionFactory {
private Configuration configuration;
public DefaultSqlSessionFactory ( Configuration configuration) {
this . configuration = configuration;
}
@Override
public SqlSession openSession ( ) {
return new DefaultSqlSession ( configuration) ;
}
}
实际上工厂模式除了将创建对象给方法外,还有一个最为重要的操作,即可以将操作分开,使得一个类不会有多个操作,这也是为什么类也有分工的原因,虽然大多数不是使用工厂来区分的,即工厂的核心还是没有在主操作中进行创建对象
这里就是主要的操作了:
package com. sqlsession ;
import com. pojo. Configuration ;
import com. pojo. MappedStatement ;
import java. sql. SQLException ;
import java. util. List ;
public interface Executor {
< E > List < E > query ( Configuration configuration, MappedStatement mappedStatement, Object [ ] param) throws Exception ;
void close ( ) throws SQLException ;
}
package com. sqlsession ;
import com. pojo. Configuration ;
import com. pojo. MappedStatement ;
import java. sql. SQLException ;
import java. util. List ;
public class DefaultSqlSession implements SqlSession {
private Configuration configuration;
public DefaultSqlSession ( Configuration configuration) {
this . configuration = configuration;
}
private Executor simpleExcutor = new SimpleExecutor ( ) ;
@Override
public < E > List < E > selectList ( String statementId, Object . . . param) throws Exception {
MappedStatement mappedStatement = configuration. getMappedStatementMap ( ) . get ( statementId) ;
List < E > query = simpleExcutor. query ( configuration, mappedStatement, param) ;
return query;
}
@Override
public < T > T selectOne ( String statementId, Object . . . params) throws Exception {
List < Object > objects = selectList ( statementId, params) ;
if ( objects. size ( ) == 1 ) {
return ( T ) objects. get ( 0 ) ;
} else if ( objects. size ( ) == 0 ) {
throw new RuntimeException ( "没有结果" ) ;
} else {
throw new RuntimeException ( "返回结果过多" ) ;
}
}
@Override
public void close ( ) throws SQLException {
simpleExcutor. close ( ) ;
}
}
很明显上面的主要操作就是simpleExcutor的操作,所以我们看看这个类:
package com. sqlsession ;
import com. pojo. Configuration ;
import com. pojo. MappedStatement ;
import com. utils. GenericTokenParser ;
import com. utils. ParameterMapping ;
import com. utils. ParameterMappingTokenHandler ;
import java. beans. PropertyDescriptor ;
import java. lang. reflect. Field ;
import java. lang. reflect. InvocationTargetException ;
import java. lang. reflect. Method ;
import java. sql. * ;
import java. util. ArrayList ;
import java. util. List ;
public class SimpleExecutor implements Executor {
private Connection connection = null ;
public < E > List < E > query ( Configuration configuration, MappedStatement mappedStatement, Object [ ] param) throws Exception {
connection = configuration. getDataSource ( ) . getConnection ( ) ;
String sql = mappedStatement. getSql ( ) ;
BoundSql boundsql = getBoundSql ( sql) ;
String finalSql = boundsql. getSqlText ( ) ;
Class < ? > paramterType = mappedStatement. getParamterType ( ) ;
PreparedStatement preparedStatement = connection. prepareStatement ( finalSql) ;
List < ParameterMapping > parameterMappingList = boundsql. getParameterMappingList ( ) ;
for ( int i = 0 ; i < parameterMappingList. size ( ) ; i++ ) {
ParameterMapping parameterMapping = parameterMappingList. get ( i) ;
String name = parameterMapping. getContent ( ) ;
Field declaredField = paramterType. getDeclaredField ( name) ;
declaredField. setAccessible ( true ) ;
Object o = declaredField. get ( param[ 0 ] ) ;
preparedStatement. setObject ( i + 1 , o) ;
}
ResultSet resultSet = preparedStatement. executeQuery ( ) ;
Class < ? > resultType = mappedStatement. getResultType ( ) ;
ArrayList < E > results = new ArrayList < E > ( ) ;
while ( resultSet. next ( ) ) {
ResultSetMetaData metaData = resultSet. getMetaData ( ) ;
E e = ( E ) resultType. newInstance ( ) ;
int columnCount = metaData. getColumnCount ( ) ;
for ( int i = 1 ; i <= columnCount; i++ ) {
String columnName = metaData. getColumnName ( i) ;
Object value = resultSet. getObject ( columnName) ;
PropertyDescriptor propertyDescriptor = new PropertyDescriptor ( columnName, resultType) ;
Method writeMethod = propertyDescriptor. getWriteMethod ( ) ;
writeMethod. invoke ( e, value) ;
}
results. add ( e) ;
}
return results;
}
@Override
public void close ( ) throws SQLException {
connection. close ( ) ;
}
private BoundSql getBoundSql ( String sql) {
ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler ( ) ;
GenericTokenParser genericTokenParser = new GenericTokenParser ( "#{" , "}" , parameterMappingTokenHandler) ;
String parse = genericTokenParser. parse ( sql) ;
List < ParameterMapping > parameterMappings = parameterMappingTokenHandler. getParameterMappings ( ) ;
BoundSql boundSql = new BoundSql ( parse, parameterMappings) ;
return boundSql;
}
}
package com. sqlsession ;
import com. utils. ParameterMapping ;
import java. util. ArrayList ;
import java. util. List ;
public class BoundSql {
private String sqlText;
private List < ParameterMapping > parameterMappingList = new ArrayList < ParameterMapping > ( ) ;
public BoundSql ( String sqlText, List < ParameterMapping > parameterMappingList) {
this . sqlText = sqlText;
this . parameterMappingList = parameterMappingList;
}
public String getSqlText ( ) {
return sqlText;
}
public void setSqlText ( String sqlText) {
this . sqlText = sqlText;
}
public List < ParameterMapping > getParameterMappingList ( ) {
return parameterMappingList;
}
public void setParameterMappingList ( List < ParameterMapping > parameterMappingList) {
this . parameterMappingList = parameterMappingList;
}
}
package com. utils ;
public interface TokenHandler {
String handleToken ( String content) ;
}
package com. utils ;
public class ParameterMapping {
private String content;
public ParameterMapping ( String content) {
this . content = content;
}
public String getContent ( ) {
return content;
}
public void setContent ( String content) {
this . content = content;
}
}
package com. utils ;
import java. util. ArrayList ;
import java. util. List ;
public class ParameterMappingTokenHandler implements TokenHandler {
private List < ParameterMapping > parameterMappings = new ArrayList ( ) ;
@Override
public String handleToken ( String content) {
parameterMappings. add ( buildParameterMapping ( content) ) ;
return "?" ;
}
private ParameterMapping buildParameterMapping ( String content) {
ParameterMapping parameterMapping = new ParameterMapping ( content) ;
return parameterMapping;
}
public List < ParameterMapping > getParameterMappings ( ) {
return parameterMappings;
}
public void setParameterMappings ( List < ParameterMapping > parameterMappings) {
this . parameterMappings = parameterMappings;
}
}
后续是mybatis的替换操作
package com. utils ;
public class GenericTokenParser {
private final String openToken;
private final String closeToken;
private final TokenHandler handler;
public GenericTokenParser ( String openToken, String closeToken, TokenHandler handler) {
this . openToken = openToken;
this . closeToken = closeToken;
this . handler = handler;
}
public String parse ( String text) {
if ( text == null || text. isEmpty ( ) ) {
return "" ;
}
int start = text. indexOf ( openToken, 0 ) ;
if ( start == - 1 ) {
return text;
}
char [ ] src = text. toCharArray ( ) ;
int offset = 0 ;
final StringBuilder builder = new StringBuilder ( ) ;
StringBuilder expression = null ;
while ( start > - 1 ) {
if ( start > 0 && src[ start - 1 ] == '\\' ) {
builder. append ( src, offset, start - offset - 1 ) . append ( openToken) ;
offset = start + openToken. length ( ) ;
} else {
if ( expression == null ) {
expression = new StringBuilder ( ) ;
} else {
expression. setLength ( 0 ) ;
}
builder. append ( src, offset, start - offset) ;
offset = start + openToken. length ( ) ;
int end = text. indexOf ( closeToken, offset) ;
while ( end > - 1 ) {
if ( end > offset && src[ end - 1 ] == '\\' ) {
expression. append ( src, offset, end - offset - 1 ) . append ( closeToken) ;
offset = end + closeToken. length ( ) ;
end = text. indexOf ( closeToken, offset) ;
} else {
expression. append ( src, offset, end - offset) ;
offset = end + closeToken. length ( ) ;
break ;
}
}
if ( end == - 1 ) {
builder. append ( src, start, src. length - start) ;
offset = src. length;
} else {
builder. append ( handler. handleToken ( expression. toString ( ) ) ) ;
}
}
start = text. indexOf ( openToken, offset) ;
}
if ( offset < src. length) {
builder. append ( src, offset, src. length - offset) ;
}
return builder. toString ( ) ;
}
}
在mybatis的3.5.4版本中,是这样的操作:
public String parse ( String text) {
if ( text != null && ! text. isEmpty ( ) ) {
int start = text. indexOf ( this . openToken) ;
if ( start == - 1 ) {
return text;
} else {
char [ ] src = text. toCharArray ( ) ;
int offset = 0 ;
StringBuilder builder = new StringBuilder ( ) ;
for ( StringBuilder expression = null ; start > - 1 ; start = text. indexOf ( this . openToken, offset) ) {
if ( start > 0 && src[ start - 1 ] == '\\' ) {
builder. append ( src, offset, start - offset - 1 ) . append ( this . openToken) ;
offset = start + this . openToken. length ( ) ;
} else {
if ( expression == null ) {
expression = new StringBuilder ( ) ;
} else {
expression. setLength ( 0 ) ;
}
builder. append ( src, offset, start - offset) ;
offset = start + this . openToken. length ( ) ;
int end;
for ( end = text. indexOf ( this . closeToken, offset) ; end > - 1 ; end = text. indexOf ( this . closeToken, offset) ) {
if ( end <= offset || src[ end - 1 ] != '\\' ) {
expression. append ( src, offset, end - offset) ;
break ;
}
expression. append ( src, offset, end - offset - 1 ) . append ( this . closeToken) ;
offset = end + this . closeToken. length ( ) ;
}
if ( end == - 1 ) {
builder. append ( src, start, src. length - start) ;
offset = src. length;
} else {
builder. append ( this . handler. handleToken ( expression. toString ( ) ) ) ;
offset = end + this . closeToken. length ( ) ;
}
}
}
if ( offset < src. length) {
builder. append ( src, offset, src. length - offset) ;
}
return builder. toString ( ) ;
}
} else {
return "" ;
}
}
测试(记得到另外一个窗口项目,与主项目一样,并导入安装的框架项目依赖来操作,或者在框架项目中加上对应的配置文件来操作):
package com ;
import com. Res . Resources ;
import com. sqlsession. SqlSession ;
import com. sqlsession. SqlSessionFactory ;
import com. sqlsession. SqlSessionFactoryBuilder ;
import java. io. InputStream ;
public class a {
public static void main ( String [ ] args) throws Exception {
InputStream resourceAsSteam = Resources . getResourceAsSteam ( "sqlMapConfig.xml" ) ;
SqlSessionFactory build = new SqlSessionFactoryBuilder ( ) . build ( resourceAsSteam) ;
SqlSession sqlSession = build. openSession ( ) ;
User user = new User ( ) ;
user. setId ( 2 ) ;
user. setUsername ( "tom" ) ;
User o = sqlSession. selectOne ( "User.selectOne" , user) ;
System . out. println ( o) ;
List < Object > list = sqlSession. selectList ( "User.selectList" ) ;
System . out. println ( list) ;
}
}
你可以发现,对应的与前面的我们写的替换操作基本是一样的,只是代码不同顺序而已
尝试细节问题:
package com. utils ;
public class m {
public static void main ( String [ ] args) {
String a = "select * from ee where id = #{s\\} && user = #{user}" ;
String parse = new GenericTokenParser ( "#{" , "}" , new ParameterMappingTokenHandler ( ) ) . parse ( a) ;
System . out. println ( parse) ;
}
}
实际上前面也操作了工厂模式,当然,我并没有指出,一般情况下,没有在主操作中创建的对象,我们都会认为是工厂模式,但是工厂模式一定好吗,其实我们可以发现,工厂模式虽然看起来好,但是最终类或者接口实在太多,也就是说,如果死板的按照设计模式来编写,在维护上,即在一定程度上,反而降低了维护效率,所以有时候,我们会直接的使用对应的方法,而不是返回出现,即定义成员变量或者静态变量等等,所以设计模式(不只是工厂模式哦,其他也算)一般也需要平衡类的多少或者存在的合理来进行设计,而不是看到一个类就使用设计模式
到这里mybatis的原理基本说明完毕,当然了,上面的功能或者细节,远不及真的mybatis,因为对于mybatis来说,他的标签可是非常多了,我们远不及他,因为mybatis可不是一人就能开发完毕的,就算可以,也是需要非常多的时间,因为就算你跟着他的源码敲也需要非常多的时间,更不要说自己完成了
当然,上面的细节问题,我们了解即可,并不需要修改,因为一般我们都会规范的写的,且他是对的
最后,要说明一下如下操作:
自定义框架优化:
通过上述我们的自定义框架,我们解决了JDBC操作数据库带来的一些问题:例如频繁创建释放数据库连 接,硬编码,⼿动封装返回结果集和参数等问题,但是现在我们继续来分析刚刚完成的自定义框架代码,有没 有什么问题?
问题如下:
这里的dao看后面的代码:
dao的实现类(认为的我们操作的流程,看后面的实现类即可)中存在重复的代码,整个操作的过程模板重复(读取配置文件,创建SqlSessionFactory,创建sqlsession)
dao的实现类中存在硬编码,调用sqlsession的方法时,参数statement的id(简称statementId)硬编码需要我们进行编写
解决:使用代理模式来创建接口的代理对象:
现在我们来进行操作,首先,就是在主项目中,添加如下的接口和类(包自己创建):
package com. dao ;
import com. User ;
import java. util. List ;
public interface IUserDao {
public List < User > findAll ( ) ;
public User findBy ( User user) ;
}
package com. dao ;
import com. Res . Resources ;
import com. User ;
import com. sqlsession. SqlSession ;
import com. sqlsession. SqlSessionFactory ;
import com. sqlsession. SqlSessionFactoryBuilder ;
import java. io. InputStream ;
import java. util. List ;
public class IUserDaoImpl implements IUserDao {
@Override
public List < User > findAll ( ) throws Exception {
InputStream resourceAsSteam = Resources . getResourceAsSteam ( "sqlMapConfig.xml" ) ;
SqlSessionFactory build = new SqlSessionFactoryBuilder ( ) . build ( resourceAsSteam) ;
SqlSession sqlSession = build. openSession ( ) ;
List < User > list = sqlSession. selectList ( "User.selectList" ) ;
System . out. println ( list) ;
return list;
}
@Override
public User findBy ( User user) throws Exception {
InputStream resourceAsSteam = Resources . getResourceAsSteam ( "sqlMapConfig.xml" ) ;
SqlSessionFactory build = new SqlSessionFactoryBuilder ( ) . build ( resourceAsSteam) ;
SqlSession sqlSession = build. openSession ( ) ;
User o = sqlSession. selectOne ( "User.selectOne" , user) ;
System . out. println ( o) ;
return o;
}
}
很明显,上面我们若要操作一个执行,必然会有重复的操作,而由于大多数我们并不会操作一个方法里面,所以有重复问题:
InputStream resourceAsSteam = Resources . getResourceAsSteam ( "sqlMapConfig.xml" ) ;
SqlSessionFactory build = new SqlSessionFactoryBuilder ( ) . build ( resourceAsSteam) ;
SqlSession sqlSession = build. openSession ( ) ;
所以我们需要进行提取,当然,实际上虽然他封装的,但是你调用他们还是执行的,即还是存在很多代码(这里包括mybatis中的接口操作,所以一般其只是操作包地址来解决后面的标识,而不是用方法包括而已,因为方法的名称是固定的,而他们不是)
其中statementId存在硬编码,当然,对于读取配置文件来说,必然需要存在的,因为代码直接总要有个联系吧,而我们只能降低联系,那么我们也要就解决这个硬编码,那么如何解决呢,我们使用代理来解决
对于提取来说,并不需要说明,但是这个硬编码我们需要进行操作,现在,我们将实现类删除,在框架项目的SqlSession中加上如下:
public < T > T getMapper ( Class < ? > mapperClass) ;
在DefaultSqlSession实现类中加上如下:
@Override
public < T > T getMapper ( Class < ? > mapperClass) {
Object o = Proxy . newProxyInstance ( DefaultSqlSession . class . getClassLoader ( ) , new Class [ ] { mapperClass} , new InvocationHandler ( ) {
@Override
public Object invoke ( Object proxy, Method method, Object [ ] args) throws Throwable {
String methodName = method. getName ( ) ;
String className = method. getDeclaringClass ( ) . getName ( ) ;
String key = className + "." + methodName;
MappedStatement mappedStatement = configuration. getMappedStatementMap ( ) . get ( key) ;
Type genericReturnType = method. getGenericReturnType ( ) ;
if ( genericReturnType instanceof ParameterizedType ) {
return selectList ( key, args) ;
}
return selectOne ( key, args) ;
}
} ) ;
return ( T ) o;
}
现在在另外一个窗口项目中,修改如下(记得同步主项目的接口或者类):
package com ;
import com. Res . Resources ;
import com. dao. UserMapper ;
import com. sqlsession. SqlSession ;
import com. sqlsession. SqlSessionFactory ;
import com. sqlsession. SqlSessionFactoryBuilder ;
import java. io. InputStream ;
import java. util. List ;
public class a {
public static void main ( String [ ] args) throws Exception {
InputStream resourceAsSteam = Resources . getResourceAsSteam ( "sqlMapConfig.xml" ) ;
SqlSessionFactory build = new SqlSessionFactoryBuilder ( ) . build ( resourceAsSteam) ;
SqlSession sqlSession = build. openSession ( ) ;
User user = new User ( ) ;
user. setId ( 2 ) ;
user. setUsername ( "tom" ) ;
UserMapper mapper = sqlSession. getMapper ( UserMapper . class ) ;
List < User > all = mapper. findAll ( ) ;
System . out. println ( all) ;
User by = mapper. findBy ( user) ;
System . out. println ( by) ;
}
}
且对应的配置文件信息记得对应哦,这样我们测试后,才不会出现错误
至此,我们的问题也解决完毕了
现在我们来补充添加,删除,修改等等操作
首先我们在框架项目XMLMapperBuilder中添加或者修改如下:
public void in ( String namespace, List < Element > list, Configuration configuration) throws Exception {
if ( list!= null ) {
for ( Element element : list) {
String id = element. attributeValue ( "id" ) ;
String paramterType = element. attributeValue ( "paramterType" ) ;
String resultType = element. attributeValue ( "resultType" ) ;
Class < ? > paramterTypeClass = getClassType ( paramterType) ;
Class < ? > resultTypeClass = getClassType ( resultType) ;
String key = namespace + "." + id;
String textTrim = element. getTextTrim ( ) ;
MappedStatement mappedStatement = new MappedStatement ( ) ;
mappedStatement. setId ( id) ;
mappedStatement. setParamterType ( paramterTypeClass) ;
mappedStatement. setResultType ( resultTypeClass) ;
mappedStatement. setSql ( textTrim) ;
configuration. getMappedStatementMap ( ) . put ( key, mappedStatement) ;
}
}
}
public void parse ( InputStream inputStream) throws Exception {
Document document = new SAXReader ( ) . read ( inputStream) ;
Element rootElement = document. getRootElement ( ) ;
String namespace = rootElement. attributeValue ( "namespace" ) ;
List < Element > select = rootElement. selectNodes ( "select" ) ;
in ( namespace, select, configuration) ;
List < Element > insert = rootElement. selectNodes ( "insert" ) ;
in ( namespace, insert, configuration) ;
List < Element > delete = rootElement. selectNodes ( "delete" ) ;
in ( namespace, delete, configuration) ;
List < Element > update = rootElement. selectNodes ( "update" ) ;
in ( namespace, update, configuration) ;
}
这样我们可以保存对应的添加,删除,修改,查询的信息
然后我们在SqlSession中添加如下:
public int insert ( String statementId, Object . . . params) throws Exception ;
public int delete ( String statementId, Object . . . params) throws Exception ;
public int update ( String statementId, Object . . . params) throws Exception ;
首先我们在SimpleExecutor中添加如下:
public int update ( Configuration configuration, MappedStatement mappedStatement, Object [ ] param) throws Exception {
connection = configuration. getDataSource ( ) . getConnection ( ) ;
String sql = mappedStatement. getSql ( ) ;
BoundSql boundsql = getBoundSql ( sql) ;
String finalSql = boundsql. getSqlText ( ) ;
Class < ? > paramterType = mappedStatement. getParamterType ( ) ;
PreparedStatement preparedStatement = connection. prepareStatement ( finalSql) ;
List < ParameterMapping > parameterMappingList = boundsql. getParameterMappingList ( ) ;
for ( int i = 0 ; i < parameterMappingList. size ( ) ; i++ ) {
ParameterMapping parameterMapping = parameterMappingList. get ( i) ;
String name = parameterMapping. getContent ( ) ;
Field declaredField = paramterType. getDeclaredField ( name) ;
declaredField. setAccessible ( true ) ;
Object o = declaredField. get ( param[ 0 ] ) ;
preparedStatement. setObject ( i + 1 , o) ;
}
int i1 = preparedStatement. executeUpdate ( ) ;
return i1;
}
然后在Executor中添加如下:
public int update ( Configuration configuration, MappedStatement mappedStatement, Object [ ] param) throws Exception ;
然后在DefaultSqlSession中添加如下:
@Override
public int insert ( String statementId, Object . . . params) throws Exception {
MappedStatement mappedStatement = configuration. getMappedStatementMap ( ) . get ( statementId) ;
int update = simpleExcutor. update ( configuration, mappedStatement, params) ;
return update;
}
@Override
public int delete ( String statementId, Object . . . params) throws Exception {
MappedStatement mappedStatement = configuration. getMappedStatementMap ( ) . get ( statementId) ;
int update = simpleExcutor. update ( configuration, mappedStatement, params) ;
return update;
}
@Override
public int update ( String statementId, Object . . . params) throws Exception {
MappedStatement mappedStatement = configuration. getMappedStatementMap ( ) . get ( statementId) ;
int update = simpleExcutor. update ( configuration, mappedStatement, params) ;
return update;
}
这样我们来进行测试,在另外一个窗口项目中进行测试:
首先在配置文件中加上如下:
< insert id = " insert" paramterType = " com.User" >
insert into user values(#{id},#{username},#{password},#{birthday})
</ insert>
< delete id = " delete" paramterType = " com.User" >
delete from user where id = #{id}
</ delete>
< update id = " update" paramterType = " com.User" >
update user set username = #{username} where id = #{id}
</ update>
然后进行测试:
package com ;
import com. Res . Resources ;
import com. sqlsession. SqlSession ;
import com. sqlsession. SqlSessionFactory ;
import com. sqlsession. SqlSessionFactoryBuilder ;
import java. io. InputStream ;
public class b {
public static void main ( String [ ] args) throws Exception {
InputStream resourceAsSteam = Resources . getResourceAsSteam ( "sqlMapConfig.xml" ) ;
SqlSessionFactory build = new SqlSessionFactoryBuilder ( ) . build ( resourceAsSteam) ;
SqlSession sqlSession = build. openSession ( ) ;
User user = new User ( ) ;
user. setId ( 3 ) ;
user. setUsername ( "m" ) ;
int insert = sqlSession. insert ( "com.dao.UserMapper.insert" , user) ;
System . out. println ( insert) ;
int update = sqlSession. update ( "com.dao.UserMapper.update" , user) ;
System . out. println ( update) ;
int delete = sqlSession. delete ( "com.dao.UserMapper.delete" , user) ;
System . out. println ( delete) ;
}
}
测试若没有问题,那么我们来操作对应的代理模式:
在这之前,我们首先需要修改MappedStatement类的属性,即添加对应标签的类型,添加如下属性:
private String qian;
public String getQian ( ) {
return qian;
}
public void setQian ( String qian) {
this . qian = qian;
}
然后在XMLMapperBuilder类里面修改如下:
MappedStatement mappedStatement = new MappedStatement ( ) ;
mappedStatement. setId ( id) ;
mappedStatement. setParamterType ( paramterTypeClass) ;
mappedStatement. setResultType ( resultTypeClass) ;
mappedStatement. setSql ( textTrim) ;
mappedStatement. setQian ( element. getName ( ) ) ;
现在我们到DefaultSqlSession里进行修改如下:
MappedStatement mappedStatement = configuration. getMappedStatementMap ( ) . get ( key) ;
if ( configuration. getMappedStatementMap ( ) . get ( key) . getQian ( ) == "select" ) {
Type genericReturnType = method. getGenericReturnType ( ) ;
if ( genericReturnType instanceof ParameterizedType ) {
return selectList ( key, args) ;
}
return selectOne ( key, args) ;
} else {
return update ( key, args) ;
}
然后在UserMapper接口中添加如下:
public int insert ( User user) throws Exception ;
public int delete ( User user) throws Exception ;
public int update ( User user) throws Exception ;
现在我们再来进行测试(记得测试时,都有进行安装哦):
package com ;
import com. Res . Resources ;
import com. dao. UserMapper ;
import com. sqlsession. SqlSession ;
import com. sqlsession. SqlSessionFactory ;
import com. sqlsession. SqlSessionFactoryBuilder ;
import java. io. InputStream ;
public class b {
public static void main ( String [ ] args) throws Exception {
InputStream resourceAsSteam = Resources . getResourceAsSteam ( "sqlMapConfig.xml" ) ;
SqlSessionFactory build = new SqlSessionFactoryBuilder ( ) . build ( resourceAsSteam) ;
SqlSession sqlSession = build. openSession ( ) ;
User user = new User ( ) ;
user. setId ( 3 ) ;
user. setUsername ( "mm" ) ;
UserMapper insert = sqlSession. getMapper ( UserMapper . class ) ;
int insert1 = insert. insert ( user) ;
System . out. println ( insert1) ;
int update = insert. update ( user) ;
System . out. println ( update) ;
int delete = insert. delete ( user) ;
System . out. println ( delete) ;
}
}
若操作成功,那么我们的getMapper的模式就操作完毕,至此,到这里你应该可以知道mybatis的大致底层原理了吧,并且但凡操作配置文件的,一般都与反射有关系,所以我们会说,框架基本都是反射来完成的
当然,上面并不代表mybatis是上面的逻辑,因为他存在很多可能,上面只是一个实现方式而已,甚至,他的代理是直接操作类似于simpleExcutor.query的,而不是对应的方法(因为他的直接方法中,对应参数不是可变参数)
Mybatis相关概念(了解):
对象/关系数据库映射(ORM)
ORM全称Object/Relation Mapping:表示对象-关系映射的缩写
ORM完成面向对象的编程语言到关系数据库的映射,当ORM框架完成映射后,程序员既可以利用面向 对象程序设计语言的简单易用性,⼜可以利用关系数据库的技术优势,ORM把关系数据库包装成面向对 象的模型,ORM框架是面向对象设计语言与关系数据库发展不同步时的中间解决方案,采用ORM框架 后,应用程序不再直接访问底层数据库,而是以面向对象的方式来操作持久化对象,而ORM框架则将这 些面向对象的操作转换成底层SQL操作
ORM框架实现的效果:把对持久化对象的保存、修改、删除 等操作,转换为对数据库的操作
Mybatis简介:
MyBatis是一款优秀的基于ORM的半自动轻量级持久层框架(轻量级一般代表所要的资源少,一般指配置文件,连接,以及类优化好处或者类多少的总体考量),它⽀持定制化SQL、存储过程以及⾼级映射,MyBatis避免了几乎所有的JDBC代码和⼿动设置参数以及获取结果集
MyBatis可以使用简单的 XML或注解来配置和映射原⽣类型、接口和Java的POJO(Plain Old Java Objects,普通老式Java对 象) 为数据库中的记录
Mybatis历史:
原是apache的一个开源项目iBatis, 2010年6⽉这个项目由apache software foundation 迁移到了google code,随着开发团队转投Google Code旗下,ibatis3.x正式更名为Mybatis ,代码于2013年11⽉迁移到Github
iBATIS一词来源于"internet"和"abatis"的组合,是一个基于Java的持久层框架,iBATIS提供的持久层框 架包括SQL Maps和Data Access Objects(DAO)
Mybatis优势:
Mybatis是一个半自动化的持久层框架,对开发人员开说,核心sql还是需要自己进行优化(所以最好是半自动,即sql自己写),sql和java编 码进行分离,功能边界清晰,一个专注业务(java),一个专注数据(sql)
分析图示如下:
Mybatis基本应用:
其中我们已经学习过mybatis了,你可以选择到61章博客(自然包括后面连续的博客)进行复习一遍,这是为了后面说明架构或者源码时,能更好的理解,其中在学习前面的原理后,你可以很容易的复习,并理解为什么会那样的操作了,这主要是因为我们自定义的接口或者类名称与mybatis基本类似,即这样可以更加的容易理解原理
MyBatis官⽹地址:http://www.mybatis.org/mybatis-3/
在63章博客中,有些是自己认为的原理,现在我们来看看源码是怎么操作的
这里我进行补充,即在63章博客中进行补充,在那里说明一级缓存时,基本上并没有去看看源码是如何形成或者操作的,现在开始说明(不同mybatis的版本,源码可能不同,但是最终实现还是相同的):
一级缓存原理探究与源码分析:
一级缓存到底是什么?一级缓存什么时候被创建、一级缓存的工作流程是怎样的?相信你现在应该会有 这几个疑问,那么我们就来研究一下一级缓存的本质
提到一级缓存就绕不开SqlSession,所以索性我们就直接从SqlSession,看看有没有创建缓存或者与缓存有关的属性或者方法
实际上经过我的观察,发现,基本好像只有clearCache()和缓存沾点关系,那么就直接从这个方 法入⼿,分析源码时,我们要看它(操作该clearCache方法的此类)是谁,然后分析它的⽗类和⼦类分别⼜是谁,对这些关系了解了,你才会对这个类有更深的认识,分析了一圈,你可能会得到如下这个流程图
public interface SqlSession extends Closeable {
void clearCache ( ) ;
}
public class DefaultSqlSession implements SqlSession {
private final Configuration configuration;
private final Executor executor;
private final boolean autoCommit;
public void clearCache ( ) {
this . executor. clearLocalCache ( ) ;
}
}
public interface Executor {
void clearLocalCache ( ) ;
}
public abstract class BaseExecutor implements Executor {
private static final Log log = LogFactory . getLog ( BaseExecutor . class ) ;
protected Transaction transaction;
protected Executor wrapper;
protected ConcurrentLinkedQueue < BaseExecutor. DeferredLoad > deferredLoads;
protected PerpetualCache localCache;
protected PerpetualCache localOutputParameterCache;
public void clearLocalCache ( ) {
if ( ! this . closed) {
this . localCache. clear ( ) ;
this . localOutputParameterCache. clear ( ) ;
}
}
}
public class PerpetualCache implements Cache {
private final String id;
private final Map < Object , Object > cache = new HashMap ( ) ;
public void clear ( ) {
this . cache. clear ( ) ;
}
}
从上面分析可知:缓存其实就是本地存放的一个map对象,每一个SqISession都会存放一个map对象的引用,那么这个cache是何 时创 建的呢?
最有可能创建缓存的地方是哪⾥呢?一般是Executor,为什么这么认为?因为Executor是执行器,用来执行SQL请求的,而且清除缓存的方法也在Executor(接口,作为父的)中执行,所以缓存的创建一般在Executor中,看了一圈发现Executor中有一个createCacheKey方法,这个方法一般就是创 建缓存的 方法,跟进去看看,你会发现createCacheKey方法是由BaseExecutor执行的,代码如下:
public abstract class BaseExecutor implements Executor {
public CacheKey createCacheKey ( MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if ( this . closed) {
throw new ExecutorException ( "Executor was closed." ) ;
} else {
CacheKey cacheKey = new CacheKey ( ) ;
cacheKey. update ( ms. getId ( ) ) ;
cacheKey. update ( rowBounds. getOffset ( ) ) ;
cacheKey. update ( rowBounds. getLimit ( ) ) ;
cacheKey. update ( boundSql. getSql ( ) ) ;
List < ParameterMapping > parameterMappings = boundSql. getParameterMappings ( ) ;
TypeHandlerRegistry typeHandlerRegistry = ms. getConfiguration ( ) . getTypeHandlerRegistry ( ) ;
Iterator var8 = parameterMappings. iterator ( ) ;
while ( var8. hasNext ( ) ) {
ParameterMapping parameterMapping = ( ParameterMapping ) var8. next ( ) ;
if ( parameterMapping. getMode ( ) != ParameterMode . OUT ) {
String propertyName = parameterMapping. getProperty ( ) ;
Object value;
if ( boundSql. hasAdditionalParameter ( propertyName) ) {
value = boundSql. getAdditionalParameter ( propertyName) ;
} else if ( parameterObject == null ) {
value = null ;
} else if ( typeHandlerRegistry. hasTypeHandler ( parameterObject. getClass ( ) ) ) {
value = parameterObject;
} else {
MetaObject metaObject = this . configuration. newMetaObject ( parameterObject) ;
value = metaObject. getValue ( propertyName) ;
}
cacheKey. update ( value) ;
}
}
if ( this . configuration. getEnvironment ( ) != null ) {
cacheKey. update ( this . configuration. getEnvironment ( ) . getId ( ) ) ;
}
return cacheKey;
}
}
}
public class CacheKey implements Cloneable , Serializable {
private List < Object > updateList;
public void update ( Object object) {
int baseHashCode = object == null ? 1 : ArrayUtil . hashCode ( object) ;
++ this . count;
this . checksum += ( long ) baseHashCode;
baseHashCode *= this . count;
this . hashcode = this . multiplier * this . hashcode + baseHashCode;
this . updateList. add ( object) ;
}
}
创建缓存key会经过一系列的update方法,update方法由一个CacheKey这个对象来执行的,这个 update方法最终由updateList的list来把五个值存进去,对照上面的代码和下面的图示,你应该能 理解 这五个值都是什么了:
很明显,就是包含了一系列的信息,那么key应该就是这些信息
这⾥需要注意一下最后一个值,configuration.getEnvironment().getId()这是什么,这其实就是 定义在mybatis-config.xml中的标签,⻅如下:
< environments default = " development" >
< environment id = " development" >
< transactionManager type = " JDBC" />
< dataSource type = " POOLED" >
< property name = " driver" value = " ${jdbc.driver}" />
< property name = " url" value = " ${jdbc.url}" />
< property name = " username" value = " ${jdbc.username}" />
< property name = " password" value = " ${jdbc.password}" />
</ dataSource>
</ environment>
</ environments>
我想应该是environments 的内容是否为null,然后来决定是否放入对应的id(id=“development”),即操作的环境
那么创建完缓存之后该用在何处呢?总不会凭空创建一个缓存不使用吧?绝对不会 的,经过对一级缓存的探究之后,我们发现一级缓存更多是用于查询操作,毕竟一级缓存也叫做查 询缓存吧,为什么叫查询缓存等下说明,我们先来看一下这个缓存到底用在哪了,我们跟踪到 BaseExecutor类的 query方法(他是执行器,自然他操作代码),看如下:
public abstract class BaseExecutor implements Executor {
public < E > List < E > query ( MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms. getBoundSql ( parameter) ;
CacheKey key = this . createCacheKey ( ms, parameter, rowBounds, boundSql) ;
return this . query ( ms, parameter, rowBounds, resultHandler, key, boundSql) ;
}
public < E > List < E > query ( MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext . instance ( ) . resource ( ms. getResource ( ) ) . activity ( "executing a query" ) . object ( ms. getId ( ) ) ;
if ( this . closed) {
throw new ExecutorException ( "Executor was closed." ) ;
} else {
if ( this . queryStack == 0 && ms. isFlushCacheRequired ( ) ) {
this . clearLocalCache ( ) ;
}
List list;
try {
++ this . queryStack;
list = resultHandler == null ? ( List ) this . localCache. getObject ( key) : null ;
if ( list != null ) {
this . handleLocallyCachedOutputParameters ( ms, key, parameter, boundSql) ;
} else {
list = this . queryFromDatabase ( ms, parameter, rowBounds, resultHandler, key, boundSql) ;
}
} finally {
-- this . queryStack;
}
if ( this . queryStack == 0 ) {
Iterator var8 = this . deferredLoads. iterator ( ) ;
while ( var8. hasNext ( ) ) {
BaseExecutor. DeferredLoad deferredLoad = ( BaseExecutor. DeferredLoad ) var8. next ( ) ;
deferredLoad. load ( ) ;
}
this . deferredLoads. clear ( ) ;
if ( this . configuration. getLocalCacheScope ( ) == LocalCacheScope . STATEMENT ) {
this . clearLocalCache ( ) ;
}
}
return list;
}
}
public < E > Cursor < E > queryCursor ( MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException {
BoundSql boundSql = ms. getBoundSql ( parameter) ;
return this . doQueryCursor ( ms, parameter, rowBounds, boundSql) ;
}
private < E > List < E > queryFromDatabase ( MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
this . localCache. putObject ( key, ExecutionPlaceholder . EXECUTION_PLACEHOLDER ) ;
List list;
try {
list = this . doQuery ( ms, parameter, rowBounds, resultHandler, boundSql) ;
} finally {
this . localCache. removeObject ( key) ;
}
this . localCache. putObject ( key, list) ;
if ( ms. getStatementType ( ) == StatementType . CALLABLE ) {
this . localOutputParameterCache. putObject ( key, parameter) ;
}
return list;
}
}
上面在queryFromDatabase中,会对localcache进行写入,localcache对象的put方法(简称)最终交给Map进行存放
至此,一级缓存底层的原理说明完毕,就是map集合的增加(也可以是修改),获取,删除等等操作
那么二级缓存呢,实际上你可以看成是更高等级的map缓存,这里可以这样认为,即还是一个map集合,即原理与一级缓存基本一样,前面的localOutputParameterCache一般并不是二级缓存的操作,但二级缓存的确是与一级缓存是一样的map操作(一般需要先开启,并且由于与一级缓存设置不同,所以是可以跨SqlSession的,至于他如何操作的,可能是resultHandler的原因(一般不是,以后面源码为主),具体可以百度,自己可以选择看看大致的源码,但只要知道他也是map即可,只是更加的先进行操作而已,我们只需要知道原理,可以选择死磕他的赋值细节,若只是了解,那么是没有必要的(就如10+10=20,你还要找资源来确定10+10=20一样),因为若了解,就算找到了又如何呢,只是确认一下而已,既然都确认一下了,那还不如将所有的赋值都确认吧,或者,直接阅读所有的源码吧,手动滑稽🤭,但是也最好不要,因为由于框架很多,所有基本终其一生也是看不完的,即我们只需要知道原理即可)
现在为了说明为什么缓存是查询的,我们看看这个代码:
public class BatchExecutor extends BaseExecutor {
public < E > List < E > doQuery ( MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null ;
List var10;
try {
this . flushStatements ( ) ;
Configuration configuration = ms. getConfiguration ( ) ;
StatementHandler handler = configuration. newStatementHandler ( this . wrapper, ms, parameterObject, rowBounds, resultHandler, boundSql) ;
Connection connection = this . getConnection ( ms. getStatementLog ( ) ) ;
stmt = handler. prepare ( connection, this . transaction. getTimeout ( ) ) ;
handler. parameterize ( stmt) ;
var10 = handler. query ( stmt, resultHandler) ;
} finally {
this . closeStatement ( stmt) ;
}
return var10;
}
}
我们从代码层面来看,他的确没有操作缓存,但是为什么我们不给增删改操作缓存呢,我认为有以下原因:
第一:缓存是服务于多次被操作的代码,由于查询操作是非常多的,因为任何数据都是查询出来的,比如说:你在拼多多上,出现的数据,他们都是查询出来的,而像删除,增加,修改等等操作,是较少的操作,所以缓存主要给查询来分担
第二:如果他们(增删改)都操作了缓存,那么缓存并不能保证,可以更好的给出,因为在一个服务器上,如果数据够多,那么响应也会相对慢一些,对查询这样多次的操作也就会有所影响,即响应慢些了
第三:增删改是少部分的操作,那么会导致缓存会冗余(即一直保留,占用空间)
第四:虽然缓存决定对应关系,但是如果在增删改上面,若出现了没有对应(是有可能的),使得你可能是像进行减少,但是缓存操作了增加,这是非常不好的情况,而查询,无论缓存是否正确,都不会对表进行改变,所以增删改比较不安全
综上所述:所以查询操作缓存,增删改不操作缓存,注意:其实二级缓存也会清空,但还有余地,在后面会说明,所以一级缓存和二级缓存都符合上面的四点,当然,可能还有其他的说明,这里可以选择百度查看
这里还进行补充一个注解:@Options(useCache = false),即默认不操作二级缓存,作用与接口上,也就是配置文件设置useCache=“false”
在63章博客有提到过三级缓存,现在这里进行说明:
二级缓存整合redis:
mybatis有自带的二级缓存,但是这个缓存是单服务器工作,⽆法实现分布式缓存,因为他是map集合,不能被多人进行操作,自然实现不了分布式多人操作他的情况,那么 什么是分布式缓存呢?假设现在有两个服务器1和2,用户访问的时候访问了 1服务器,查询后的缓 存就 会放在1服务器上,假设现在有个用户访问的是2服务器,那么他在2服务器上就⽆法获取刚刚那个缓存,如下图所示:
为了解决这个问题,就得找一个分布式的缓存,专⻔用来存储缓存数据的,这样不同的服务器要缓存数 据都往它那⾥存,取缓存数据也从它那⾥取,并且他要满足可以被多人进行操作的情况,如下图所示:
如上图所示,在几个不同的服务器之间,我们使用第三方缓存框架,将缓存都放在这个第三方框架中,然后⽆论有多少台服务器,我们都能从缓存中获取数据,我们一般将该缓存称为mybatis的三级缓存,也就是自定义缓存
这⾥我们介绍mybatis与redis的整合:
在前面我们知道,public class PerpetualCache implements Cache {
我们看如图:
mybatis提供了一个Cache接口,很明显,之前的一级缓存和二级缓存都操作了他们的实现类PerpetualCache ,因为:
public abstract class BaseExecutor implements Executor {
private static final Log log = LogFactory . getLog ( BaseExecutor . class ) ;
protected Transaction transaction;
protected Executor wrapper;
protected ConcurrentLinkedQueue < BaseExecutor. DeferredLoad > deferredLoads;
protected PerpetualCache localCache;
protected PerpetualCache localOutputParameterCache;
protected Configuration configuration;
所以如果要实现自己的缓存逻辑,实现cache接口(Cache的简称)并开发即可
mybatis本身默认实现了一个,但是这个缓存的实现⽆法实现分布式缓存,也就是上面的PerpetualCache类,所以我们要自己来实现
其中我们使用redis分布式缓存就可以,mybatis提供了一个针对或者说实现了cache接口的redis实现类,该类存在mybatis-redis包中:
实现(这里使用63章博客中操作的案例,可以自己进行选择,前提是学习过了,当然,自己实现或者准备基础代码也行):
pom文件:
< dependency>
< groupId> org.mybatis.caches</ groupId>
< artifactId> mybatis-redis</ artifactId>
< version> 1.0.0-beta2</ version>
</ dependency>
配置文件:
Mapper.xml:
< cache type = " org.mybatis.caches.redis.RedisCache" />
在资源文件夹下创建redis.properties:
redis.host=192.168.164.128
redis.port=6379
redis.connectionTimeout=5000
redis.password=
redis.database=0
注意:可能依赖版本不同,他可能并不会判断是否是redis.开头的,而是直接使用,从而导致出现错误(如并不识别redis.host而是直接识别host),所以若出现错误,可以修改如下,即:
host=192.168.164.128
port=6379
connectionTimeout=5000
password=
database=0
上面代表你安装了redis的地址,其中,若没有操作密码,可以直接上面这样写(可以这样写哦,复制粘贴即可)
测试:
@Test
public void testTwoCache ( ) throws IOException {
InputStream resourceAsStream = Resources . getResourceAsStream ( "sqlMapConfig.xml" ) ;
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder ( ) . build ( resourceAsStream) ;
SqlSession sqlSession1 = sqlSessionFactory. openSession ( ) ;
UserMapper userMapper1 = sqlSession1. getMapper ( UserMapper . class ) ;
User user = userMapper1. findById ( 11 ) ;
System . out. println ( user) ;
sqlSession1. close ( ) ;
SqlSession sqlSession2 = sqlSessionFactory. openSession ( ) ;
UserMapper userMapper2 = sqlSession2. getMapper ( UserMapper . class ) ;
User user2 = userMapper2. findById ( 11 ) ;
System . out. println ( user2) ;
多次进行执行,或者看看redis是否有数据,一般有了数据,说明二级缓存操作完毕
源码分析:
RedisCache和普遍实现Mybatis的缓存方案大同⼩异,⽆⾮是实现Cache接口,并使用jedis操作缓存,不过该项目在设计细节上有一些区别
public final class RedisCache implements Cache {
private final ReadWriteLock readWriteLock = new DummyReadWriteLock ( ) ;
private String id;
private static JedisPool pool;
public RedisCache ( String id) {
if ( id == null ) {
throw new IllegalArgumentException ( "Cache instances require an ID" ) ;
} else {
this . id = id;
RedisConfig redisConfig = RedisConfigurationBuilder . getInstance ( ) . parseConfiguration ( ) ;
pool = new JedisPool ( redisConfig, redisConfig. getHost ( ) , redisConfig. getPort ( ) , redisConfig. getConnectionTimeout ( ) , redisConfig. getSoTimeout ( ) , redisConfig. getPassword ( ) , redisConfig. getDatabase ( ) , redisConfig. getClientName ( ) ) ;
}
}
}
RedisCache在mybatis启动的时候,由MyBatis的CacheBuilder创建,创建的方式很简单,就是调用 RedisCache的带有String参数的构造方法,即RedisCache(String id)(所以在PerpetualCache中,也只有对应的一个String参数的构造方法,可以说是固定的),而在RedisCache的构造方法中, 调用了 RedisConfigurationBuilder 来创建 RedisConfig 对象,并使用 RedisConfig 来创建JedisPool
RedisConfig类继承了 JedisPoolConfig,并提供了 host,port等属性的包装,简单看一下RedisConfig的 属性:
public class RedisConfig extends JedisPoolConfig {
private String host = "localhost" ;
private int port = 6379 ;
private int connectionTimeout = 2000 ;
private int soTimeout = 2000 ;
private String password;
private int database = 0 ;
private String clientName;
public RedisConfig ( ) {
}
}
RedisConfig对象是由RedisConfigurationBuilder创建的,简单看下这个类的主要方法:
final class RedisConfigurationBuilder {
private static final RedisConfigurationBuilder INSTANCE = new RedisConfigurationBuilder ( ) ;
private static final String SYSTEM_PROPERTY_REDIS_PROPERTIES_FILENAME = "redis.properties.filename" ;
private static final String REDIS_RESOURCE = "redis.properties" ;
private final String redisPropertiesFilename = System . getProperty ( "redis.properties.filename" , "redis.properties" ) ;
public static RedisConfigurationBuilder getInstance ( ) {
return INSTANCE ;
}
public RedisConfig parseConfiguration ( ) {
return this . parseConfiguration ( this . getClass ( ) . getClassLoader ( ) ) ;
}
public RedisConfig parseConfiguration ( ClassLoader classLoader) {
Properties config = new Properties ( ) ;
InputStream input = classLoader. getResourceAsStream ( this . redisPropertiesFilename) ;
if ( input != null ) {
try {
config. load ( input) ;
} catch ( IOException var12) {
throw new RuntimeException ( "An error occurred while reading classpath property '" + this . redisPropertiesFilename + "', see nested exceptions" , var12) ;
} finally {
try {
input. close ( ) ;
} catch ( IOException var11) {
}
}
}
RedisConfig jedisConfig = new RedisConfig ( ) ;
this . setConfigProperties ( config, jedisConfig) ;
return jedisConfig;
}
}
上面的核心的方法就是parseConfiguration方法,该方法从classpath中读取一个redis.properties文件,并将该配置文件中的内容设置到RedisConfig对象中,并返回,接下来,就是RedisCache使用 RedisConfig类创建完成JedisPool,在RedisCache中实现了一个简单的模板方法,用来操作Redis
即一般RedisCache里面存在这个方法:
private Object execute ( RedisCallback callback) {
Jedis jedis = pool. getResource ( ) ;
Object var3;
try {
var3 = callback. doWithRedis ( jedis) ;
} finally {
jedis. close ( ) ;
}
return var3;
}
public interface RedisCallback {
Object doWithRedis ( Jedis var1) ;
}
模板接口为RedisCallback,这个接口中就只需要实现了一个doWithRedis方法而已
接下来看看Cache中最重要的两个方法:putObject和getObject,通过这两个方法来查看mybatis-redis 储存数据的格式:
public void putObject ( final Object key, final Object value) {
this . execute ( new RedisCallback ( ) {
public Object doWithRedis ( Jedis jedis) {
jedis. hset ( RedisCache . this . id. toString ( ) . getBytes ( ) , key. toString ( ) . getBytes ( ) , SerializeUtil . serialize ( value) ) ;
return null ;
}
} ) ;
}
public Object getObject ( final Object key) {
return this . execute ( new RedisCallback ( ) {
public Object doWithRedis ( Jedis jedis) {
return SerializeUtil . unserialize ( jedis. hget ( RedisCache . this . id. toString ( ) . getBytes ( ) , key. toString ( ) . getBytes ( ) ) ) ;
}
} ) ;
}
可以很清楚的看到,mybatis-redis在存储数据的时候,是使用的hash结构,把cache的id作为这个hash 的key (cache的id在mybatis中就是mapper的namespace),这个mapper中的查询缓存数据作为 hash的value(key-value结构,key是他传递的结果,具体多少这里不做说明),需要缓存的内容直接使用SerializeUtil存储,SerializeUtil和其他的序列化类差不多,负责 对象 的序列化和反序列化,并且,要明白,id是固定的,因为是包名加上接口名称,那么key一般就是对应配置文件的id了,很明显,他的这个结果,正好符合二级缓存的结果,即操作一个配置文件,所以put和get,就是缓存存放和获取缓存数据的操作
Mybatis插件介绍:
在62章博客中,只是大致说明了或者利用了插件,现在我们来深入了解
Mybatis作为一个应用⼴泛的优秀的ORM开源框架,这个框架具有强大的灵活性,在四大组件(Executor、StatementHandler、ParameterHandler、ResultSetHandler)处提供了简单易用的插件扩 展机制,Mybatis对持久层的操作就是借助于实现了他们的四大核心对象
MyBatis⽀持用插件对四大核心对象的方法进行拦截,对mybatis来说插件就是拦截器,用来增强核心对象的功能,增强功能本质上是借助于底层的动态代理实现的,换句话说,MyBatis中的四大对象都是被代理对象,即被增强的对象,如果包括插件的整体说明,那么他就是代理对象,只是分是否代理而已,即四大组件的实现对象是mybatis能操作sql的执行对象,而插件是增强的操作,简单来说,我们之前能够操作sql,就是利用了这个四大组件的对象来操作的
public interface Executor {
}
public interface StatementHandler {
}
public interface ParameterHandler {
}
public interface ResultSetHandler {
}
MyBatis所允许拦截的方法如下(给出部分):
执行器Executor (update、query、commit、rollback等方法)
SQL语法构建器StatementHandler(prepare、parameterize、batch、updates,query等方法)
参数处理器ParameterHandler(getParameterObject、setParameters方法)
结果集处理器ResultSetHandler(handleResultSets、handleOutputParameters等方法)
Mybatis插件原理:
在四大对象创建的时候:
1:每个创建出来的对象不是直接返回的,而是interceptorChain.pluginAll(parameterHandler)进行处理后再返回
2:获取到所有的Interceptor (拦截器)(插件需要实现的接口),调用 interceptor.plugin(target),返回 target 包装后的对象
3:插件机制,我们可以使用插件为目标对象创建一个代理对象,AOP (面向切面)使得我们的插件可 以 为四大对象创建出代理对象,代理对象就可以拦截到四大对象的每一个执行,所以我们甚至可以自定义结果来返回,即可以拦截最终的结果,然后进行修改
注意:这里直接的看说明,你自然不明白为什么,我们看后面:
拦截:
插件具体是如何拦截并附加额外的功能的呢?以ParameterHandler来说
public interface ParameterHandler {
Object getParameterObject ( ) ;
void setParameters ( PreparedStatement var1) throws SQLException ;
}
public class DefaultParameterHandler implements ParameterHandler {
}
public class Configuration {
protected final InterceptorChain interceptorChain;
public ParameterHandler newParameterHandler ( MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
ParameterHandler parameterHandler = mappedStatement. getLang ( ) . createParameterHandler ( mappedStatement, parameterObject, boundSql) ;
parameterHandler = ( ParameterHandler ) this . interceptorChain. pluginAll ( parameterHandler) ;
return parameterHandler;
}
}
public class InterceptorChain {
public Object pluginAll ( Object target) {
Interceptor interceptor;
for ( Iterator var2 = this . interceptors. iterator ( ) ; var2. hasNext ( ) ; target = interceptor. plugin ( target) ) {
interceptor = ( Interceptor ) var2. next ( ) ;
}
return target;
}
}
interceptorChain保存了所有的拦截器(interceptors),是mybatis初始化的时候创建的,调用拦截器链中的拦截器依次的对目标进行拦截或增强,interceptor.plugin(target)中的target就可以理解为mybatis 中的四大对象,返回的target是被重重代理后的对象
如果我们想要拦截Executor的query方法,那么可以这样定义插件:
@Intercepts ( {
@Signature (
type = Executor . class ,
method = "query" ,
args= { MappedStatement . class , Object . class , RowBounds . class , ResultHandler . class }
)
} )
public class ExeunplePlugin implements Interceptor {
}
除此之外,我们还需将插件配置到sqlMapConfig.xml中:
< plugins>
< plugin interceptor = " com.lagou.plugin.ExamplePlugin" > </ plugin>
</ plugins>
这样MyBatis在启动时可以加载插件,并保存插件实例到相关对象(InterceptorChain,拦截器链)中(pluginAll中有插件就操作增强执行),待准备工作做完后,MyBatis处于就绪状态,我们在执行SQL时,需要先通过DefaultSqlSessionFactory 创建 SqlSession,Executor实例会在创建 SqlSession 的过程中被创建, Executor实例创建完毕后,MyBatis可以通过JDK动态代理为实例⽣成代理类,这样,插件逻辑即可在 Executor相关方法被调用前执行(因为增强的,一般再创建完毕过程中就操作增强了(只是准备)),以上就是MyBatis插件机制的基本原理
自定义插件:
前面的说明,都是为这里的操作进行铺垫,所以现在我们开始进行主要操作(一般分页插件,通常存在注解的形式,所以看起来会认为只需要操作配置文件即可,依赖里面可以自己找,在62章博客就是这样)
插件接口:
Mybatis 插件接口:Interceptor
1:Intercept方法,插件的核心方法
2:plugin方法,⽣成target的代理对象
3:setProperties方法,传递插件所需参数
自定义插件过程:
设计实现一个自定义插件:
package com. lagou. test ;
import org. apache. ibatis. executor. statement. StatementHandler ;
import org. apache. ibatis. plugin. * ;
import java. sql. Connection ;
import java. util. Properties ;
@Intercepts ( {
@Signature (
type = StatementHandler . class ,
method = "prepare" ,
args = { Connection . class , Integer . class } ) ,
} )
public class MyPlugin implements Interceptor {
@Override
public Object intercept ( Invocation invocation) throws Throwable {
System . out. println ( "增强了" ) ;
return invocation. proceed ( ) ;
}
@Override
public Object plugin ( Object target) {
System . out. println ( "将要包装的目标对象:" + target) ;
return Plugin . wrap ( target, this ) ;
}
@Override
public void setProperties ( Properties properties) {
System . out. println ( "插件配置的初始化参数:" + properties) ;
}
}
sqlMapConfig.xml:
< plugins>
< plugin interceptor = " com.lagou.test.MyPlugin" >
< property name = " name" value = " Bob" />
</ plugin>
</ plugins>
在操作分页插件中,我们之所以只操作了上面的配置,是因为他对应的依赖的类中已经存在了@Intercepts的操作了,自己可以去看看,即分页插件,也是别人自定义的,所以所有的插件都是自定义的,只是他比较好用而已
源码分析:
上面插件只是我的理解,实际情况,还是需要源码来进行分析的,现在我们来分析为什么插件的操作没有错
执行插件逻辑:
Plugin实现了 InvocationHandler接口,因此它的invoke方法会拦截所有的方法调用,invoke方法会 对 所拦截的方法进行检测,以决定是否执行插件逻辑,该方法的逻辑如下:
public class Plugin implements InvocationHandler {
public Object invoke ( Object proxy, Method method, Object [ ] args) throws Throwable {
try {
Set < Method > methods = ( Set ) this . signatureMap. get ( method. getDeclaringClass ( ) ) ;
return methods != null && methods. contains ( method) ? this . interceptor. intercept ( new Invocation ( this . target, method, args) ) : method. invoke ( this . target, args) ;
} catch ( Exception var5) {
throw ExceptionUtil . unwrapThrowable ( var5) ;
}
}
}
invoke方法的代码⽐较少,逻辑不难理解,⾸先,invoke方法会检测被拦截方法是否配置在插件的 @Signature注解中,若是,则执行插件逻辑,否则执行被拦截方法(也就是没有操作增强了),插件逻辑封装在intercept中,该 方法的参数类型为Invocation,Invocation主要用于存储目标类,方法以及方法参数列表,下面简单看 一下该类的定义
public class Invocation {
private final Object target;
private final Method method;
private final Object [ ] args;
public Invocation ( Object target, Method method, Object [ ] args) {
this . target = target;
this . method = method;
this . args = args;
}
public Object proceed ( ) throws InvocationTargetException , IllegalAccessException {
return this . method. invoke ( this . target, this . args) ;
}
}
@Override
public Object intercept ( Invocation invocation) throws Throwable {
System . out. println ( "增强了" ) ;
return invocation. proceed ( ) ;
}
至此,关于为什么对应插件的执行操作没有错的逻辑就分析结束,也的确对应的plugin操作的是增强的(且是JDK方式的动态代理)
至于分页插件,可以选择到62章测试一下,这里不做说明了
通用 mapper:
什么是通用Mapper:
通用Mapper是主要为了解决单表增删改查的,比如说我要查询id为1,或者查询名称为1,或者他们的结合,像这样的每个字段都进行单独查询或者关联的,若是操作接口,那么需要写非常多的接口方法(字段可是有多个的),所以我们需要通用的mapper,即以类来自动的生成sql,基于Mybatis的插件机制,开发人员不需要编写SQL,不需要 在DAO中增加方法,只要写好实体类,就能⽀持相应的增删改查方法,这里可以选择参照mp,所以说mp就是mybatis的自定义的插件,这也是为什么mp是在mybatis上的只做增强,而不改变mybatis的主要原因,这里的说明也是为什么mp不好操作复杂sql的原因(因为是主要为了解决单表增删改查的),当然,既然是增强,mp(mybatis-plus)自然可以通过某些配置来操作复杂的,只是非常麻烦(具体可以百度,一般我们只是手动的进行关联而已)
如何使用:
⾸先在maven项目,在pom.xml中引入mapper的依赖
< dependency>
< groupId> tk.mybatis</ groupId>
< artifactId> mapper</ artifactId>
< version> 3.1.2</ version>
</ dependency>
Mybatis配置文件中完成配置:
< plugins>
< plugin interceptor = " tk.mybatis.mapper.mapperhelper.MapperInterceptor" >
< property name = " mappers" value = " com.lagou.mapper.UserMapper" />
</ plugin>
</ plugins>
实体类设置主键:
@Table ( name = "t_user" )
public class User {
@Id
@GeneratedValue ( strategy = GenerationType . IDENTITY )
private Integer id;
@Column ( name = "username" )
private String username;
}
定义通用mapper:
public interface UserMapper extends Mapper < User > {
}
测试:
package com. lagou. test ;
import com. lagou. domain. User ;
import com. lagou. mapper. UserMapper ;
import org. apache. ibatis. io. Resources ;
import org. apache. ibatis. session. SqlSession ;
import org. apache. ibatis. session. SqlSessionFactory ;
import org. apache. ibatis. session. SqlSessionFactoryBuilder ;
import org. junit. Test ;
import tk. mybatis. mapper. entity. Example ;
import java. io. IOException ;
import java. io. InputStream ;
import java. util. List ;
public class UserTest {
@Test
public void test1 ( ) throws IOException {
InputStream resourceAsStream = Resources . getResourceAsStream ( "sqlMapConfig.xml" ) ;
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder ( ) . build ( resourceAsStream) ;
SqlSession sqlSession = sqlSessionFactory. openSession ( ) ;
UserMapper userMapper = sqlSession. getMapper ( UserMapper . class ) ;
User user = new User ( ) ;
user. setId ( 4 ) ;
User user1 = userMapper. selectOne ( user) ;
List < User > users = userMapper. select ( null ) ;
userMapper. selectByPrimaryKey ( 1 ) ;
userMapper. selectCount ( user) ;
user. setUsername ( "null" ) ;
int insert = userMapper. insert ( user) ;
System . out. println ( insert) ;
System . out. println ( user) ;
sqlSession. commit ( ) ;
user = new User ( ) ;
user. setId ( 44 ) ;
int i = userMapper. insertSelective ( user) ;
sqlSession. commit ( ) ;
user = new User ( ) ;
user. setId ( 44 ) ;
user. setUsername ( "1" ) ;
int i1 = userMapper. updateByPrimaryKey ( user) ;
sqlSession. commit ( ) ;
user = new User ( ) ;
user. setId ( 44 ) ;
int delete = userMapper. delete ( user) ;
sqlSession. commit ( ) ;
userMapper. deleteByPrimaryKey ( 1 ) ;
sqlSession. commit ( ) ;
Example example = new Example ( User . class ) ;
example. createCriteria ( ) . andEqualTo ( "id" , 1 ) ;
example. createCriteria ( ) . andLike ( "username" , "1" ) ;
List < User > users1 = userMapper. selectByExample ( example) ;
System . out. println ( users1) ;
}
}
实际上会有些细节问题,而mp中就进行了解决,所以我们尽量使用mp,或者自己定义,而不是使用对应单纯的一个依赖来操作
Mybatis架构原理:
我们知道了mybatis的具体sql操作,但是他的整体架构是什么呢
架构设计:
我们把Mybatis的功能架构分为三层:
(1):API接口层:提供给外部使用的接口 API,开发人员通过这些本地API来操纵数据库,接口层接收 到 调用请求就会调用数据处理层来完成具体的数据处理
MyBatis和数据库的交互有两种方式:
1:使用传统的MyBatis提供的API
2:使用Mapper代理的方式(本质也是API,但解决了sql的定位硬编码)
(2):数据处理层:负责具体的SQL查找、SQL解析、SQL执行和执行结果映射处理等,它主要的目的是根 据调用的请求完成一次数据库操作
(3):基础⽀撑层:负责最基础的功能⽀撑,包括连接管理、事务管理、配置加载和缓存处理,这些都是 共 用的东⻄,将他们抽取出来作为最基础的组件,为上层的数据处理层提供最基础的⽀撑
主要构件及其相互关系:
总体流程:
(1):加载配置并初始化:
触发条件:加载配置文件
配置来源于两个地方,一个是配置文件(主配置文件conf.xml,mapper文件*.xml),—个是java代码中的注解,将主配置文件内容解析封装到Configuration,将sql的配置信息加载成为一个mappedstatement 对象,存储在内存之中
(2):接收调用请求:
触发条件:调用Mybatis提供的API
传入参数:为SQL的ID和传入参数对象
处理过程:将请求传递给下层的请求处理层进行处理
(3):处理操作请求
触发条件:API接口层传递请求过来
传入参数:为SQL的ID和传入参数对象
处理过程(这个过程可以认为是类似之前我们手写mybatis底层的query方法):
1:根据SQL的ID查找对应的MappedStatement对象
2:根据传入参数对象解析MappedStatement对象,得到最终要执行的SQL和执行传入参数
3:获取数据库连接,根据得到的最终SQL语句和执行传入参数到数据库执行,并得到执行结果
4:根据MappedStatement对象中的结果映射配置对得到的执行结果进行转换处理,并得到最终的处 理 结果
5:可以释放连接资源
(4):返回处理结果
将最终的处理结果返回
Mybatis源码剖析:
上面只是直接的说明流程,虽然我们可以将前面的手写的简化的mybatis流程是这样的认为,但是最好的方式,还是直接的看源码,所以我们现在直接开始进行源码的剖析
传统方式源码剖析:
源码剖析的初始化:
InputStream resourceAsStream = Resources . getResourceAsStream ( "sqlMapConfig.xml" ) ;
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder ( ) . build ( resourceAsStream) ;
上面很明显,首先读取配置文件,那么后面的那一行代码正是初始化工作的开始
实际上之所以我们先手写一个类似的,主要是为了源码更加的明白来做准备,当然,既然是类似的,那么具体的流程并非一定相同(一般大体相同的),所以只需要参照手写的即可,而不要将手写的看成是主体
public class SqlSessionFactoryBuilder {
public SqlSessionFactory build ( InputStream inputStream) {
return this . build ( ( InputStream ) inputStream, ( String ) null , ( Properties ) null ) ;
}
public SqlSessionFactory build ( InputStream inputStream, String environment, Properties properties) {
SqlSessionFactory var5;
try {
XMLConfigBuilder parser = new XMLConfigBuilder ( inputStream, environment, properties) ;
var5 = this . build ( parser. parse ( ) ) ;
} catch ( Exception var14) {
throw ExceptionFactory . wrapException ( "Error building SqlSession." , var14) ;
} finally {
ErrorContext . instance ( ) . reset ( ) ;
try {
inputStream. close ( ) ;
} catch ( IOException var13) {
}
}
return var5;
}
public SqlSessionFactory build ( Configuration config) {
return new DefaultSqlSessionFactory ( config) ;
}
}
public class DefaultSqlSessionFactory implements SqlSessionFactory {
private final Configuration configuration;
public DefaultSqlSessionFactory ( Configuration configuration) {
this . configuration = configuration;
}
}
MyBatis在初始化的时候,会将MyBatis的配置信息全部加载到内存中,使用 org.apache.ibatis.session.Configuration 实例来维护
下面进入对配置文件解析部分:
⾸先对Configuration对象进行介绍:
XMLConfigerBuilder xmlConfigerBuilder = new XMLConfigerBuilder ( configuration) ;
Configuration configuration = xmlConfigerBuilder. parseConfiguration ( inputStream) ;
SqlSessionFactory sqlSessionFactory = new DefaultSqlSessionFactory ( configuration) ;
public class XMLConfigBuilder extends BaseBuilder {
public XMLConfigBuilder ( InputStream inputStream, String environment, Properties props) {
this ( new XPathParser ( inputStream, true , props, new XMLMapperEntityResolver ( ) ) , environment, props) ;
}
private XMLConfigBuilder ( XPathParser parser, String environment, Properties props) {
super ( new Configuration ( ) ) ;
this . localReflectorFactory = new DefaultReflectorFactory ( ) ;
ErrorContext . instance ( ) . resource ( "SQL Mapper Configuration" ) ;
this . configuration. setVariables ( props) ;
this . parsed = false ;
this . environment = environment;
this . parser = parser;
}
public Configuration parse ( ) {
if ( this . parsed) {
throw new BuilderException ( "Each XMLConfigBuilder can only be used once." ) ;
} else {
this . parsed = true ;
this . parseConfiguration ( this . parser. evalNode ( "/configuration" ) ) ;
return this . configuration;
}
}
private void parseConfiguration ( XNode root) {
try {
this . propertiesElement ( root. evalNode ( "properties" ) ) ;
Properties settings = this . settingsAsProperties ( root. evalNode ( "settings" ) ) ;
this . loadCustomVfs ( settings) ;
this . loadCustomLogImpl ( settings) ;
this . typeAliasesElement ( root. evalNode ( "typeAliases" ) ) ;
this . pluginElement ( root. evalNode ( "plugins" ) ) ;
this . objectFactoryElement ( root. evalNode ( "objectFactory" ) ) ;
this . objectWrapperFactoryElement ( root. evalNode ( "objectWrapperFactory" ) ) ;
this . reflectorFactoryElement ( root. evalNode ( "reflectorFactory" ) ) ;
this . settingsElement ( settings) ;
this . environmentsElement ( root. evalNode ( "environments" ) ) ;
this . databaseIdProviderElement ( root. evalNode ( "databaseIdProvider" ) ) ;
this . typeHandlerElement ( root. evalNode ( "typeHandlers" ) ) ;
this . mapperElement ( root. evalNode ( "mappers" ) ) ;
} catch ( Exception var3) {
throw new BuilderException ( "Error parsing SQL Mapper Configuration. Cause: " + var3, var3) ;
}
}
}
至于解析后的操作,就不用说明了,因为非常麻烦,我们知道这里解析了即可,简单来说的确操作了类似于我们手写的:
XMLConfigerBuilder xmlConfigerBuilder = new XMLConfigerBuilder ( configuration) ;
Configuration configuration = xmlConfigerBuilder. parseConfiguration ( inputStream) ;
SqlSessionFactory sqlSessionFactory = new DefaultSqlSessionFactory ( configuration) ;
前面之所以要手写除了类似外,也是为了更好的理解,那么从这里开始,自己选择去找对应的,就不操作上面的直接说明了,当然,可以的话,我会顺便给出代码的
介绍一下 MappedStatement :
作用:MappedStatement与Mapper配置文件中的一个select/update/insert/delete节点相对应
mapper中配置的标签都被封装到了此对象中,主要用途是描述一条SQL语句
初始化过程:回顾刚开 始介绍的加载配置文件的过程中,会对sqlMapConfig.xml中的各个标签都进行 解析,其中有mappers 标签用来引入mapper.xml文件或者配置mapper接口的目录,如:
< select id = " getUser" resultType = " user" >
select * from user where id=#{id}
</ select>
这样的一个select标签会在初始化配置文件时被解析封装成一个MappedStatement对象,然后存储在 Configuration对象的mappedStatements属性中,mappedStatements 是一个HashMap,存储时key =全限定类名+方法名,value =对应的MappedStatement对象,正好与我们手写的是有对应的
在configuration中对应的属性为:
public class Configuration {
protected final Map < String , MappedStatement > mappedStatements;
}
源码剖析的执行SQL流程:
先简单介绍SqlSession :
SqlSession是一个接口,它有两个实现类:DefaultSqlSession (默认)和SqlSessionManager (弃用,不做介绍)
public interface SqlSession extends Closeable {
}
public class DefaultSqlSession implements SqlSession {
private final Configuration configuration;
private final Executor executor;
public DefaultSqlSession ( Configuration configuration, Executor executor, boolean autoCommit) {
this . configuration = configuration;
this . executor = executor;
this . dirty = false ;
this . autoCommit = autoCommit;
}
public DefaultSqlSession ( Configuration configuration, Executor executor) {
this ( configuration, executor, false ) ;
}
}
public class SqlSessionManager implements SqlSessionFactory , SqlSession {
}
SqlSession是MyBatis中用于和数据库交互的顶层类,通常将它与ThreadLocal绑定,一个会话使用一 个SqlSession,并且可以在使用完毕后需要close,具体的ThreadLocal使用可以参照65章博客,简单来说,一个线程有独有自己的一份数据
SqlSession中(实现类DefaultSqlSession)的两个最重要的参数,configuration是赋值的,Executor为执行器
Executor:
Executor也是一个接口,他有三个常用的实现类:
BatchExecutor (重用语句并执行批量更新)
ReuseExecutor (重用预处理语句 prepared statements)
SimpleExecutor (普通的执行器,默认)
public interface Executor {
}
public class BatchExecutor extends BaseExecutor {
}
public class ReuseExecutor extends BaseExecutor {
}
public class SimpleExecutor extends BaseExecutor {
}
public abstract class BaseExecutor implements Executor {
}
继续分析,初始化完毕后,我们就要执行SQL 了(假设是如下):
InputStream resourceAsStream = Resources . getResourceAsStream ( "sqlMapConfig.xml" ) ;
SqlSessionFactory build = new SqlSessionFactoryBuilder ( ) . build ( resourceAsStream) ;
SqlSession sqlSession = build. openSession ( ) ;
List < User > objects = sqlSession. selectList ( "userMapper.findAll" ) ;
获得 sqlSession:
public class Configuration {
protected ExecutorType defaultExecutorType;
public ExecutorType getDefaultExecutorType ( ) {
return this . defaultExecutorType;
}
public void setDefaultExecutorType ( ExecutorType defaultExecutorType) {
this . defaultExecutorType = defaultExecutorType;
}
}
public interface SqlSessionFactory {
}
public class DefaultSqlSessionFactory implements SqlSessionFactory {
private final Configuration configuration;
public DefaultSqlSessionFactory ( Configuration configuration) {
this . configuration = configuration;
}
public SqlSession openSession ( ) {
return this . openSessionFromDataSource ( this . configuration. getDefaultExecutorType ( ) , ( TransactionIsolationLevel ) null , false ) ;
}
private SqlSession openSessionFromDataSource ( ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null ;
DefaultSqlSession var8;
try {
Environment environment = this . configuration. getEnvironment ( ) ;
TransactionFactory transactionFactory = this . getTransactionFactoryFromEnvironment ( environment) ;
tx = transactionFactory. newTransaction ( environment. getDataSource ( ) , level, autoCommit) ;
Executor executor = this . configuration. newExecutor ( tx, execType) ;
var8 = new DefaultSqlSession ( this . configuration, executor, autoCommit) ;
} catch ( Exception var12) {
this . closeTransaction ( tx) ;
throw ExceptionFactory . wrapException ( "Error opening session. Cause: " + var12, var12) ;
} finally {
ErrorContext . instance ( ) . reset ( ) ;
}
return var8;
}
}
执行 sqlsession 中的 api:
public class DefaultSqlSession implements SqlSession {
private final Configuration configuration;
private final Executor executor;
private final boolean autoCommit;
private boolean dirty;
private List < Cursor < ? > > cursorList;
public DefaultSqlSession ( Configuration configuration, Executor executor, boolean autoCommit) {
this . configuration = configuration;
this . executor = executor;
this . dirty = false ;
this . autoCommit = autoCommit;
}
public DefaultSqlSession ( Configuration configuration, Executor executor) {
this ( configuration, executor, false ) ;
}
public < E > List < E > selectList ( String statement) {
return this . selectList ( statement, ( Object ) null ) ;
}
public < E > List < E > selectList ( String statement, Object parameter) {
return this . selectList ( statement, parameter, RowBounds . DEFAULT ) ;
}
public < E > List < E > selectList ( String statement, Object parameter, RowBounds rowBounds) {
List var5;
try {
MappedStatement ms = this . configuration. getMappedStatement ( statement) ;
var5 = this . executor. query ( ms, this . wrapCollection ( parameter) , rowBounds, Executor . NO_RESULT_HANDLER ) ;
} catch ( Exception var9) {
throw ExceptionFactory . wrapException ( "Error querying database. Cause: " + var9, var9) ;
} finally {
ErrorContext . instance ( ) . reset ( ) ;
}
return var5;
}
}
上面可以看成我们自定义框架中的DefaultSqlSession的selectList方法,虽然Executor是当成参数传递,而不是在里面当成成员变量
源码剖析的executor:
继续源码中的步骤,进入executor.query()
public abstract class BaseExecutor implements Executor {
public < E > List < E > query ( MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms. getBoundSql ( parameter) ;
CacheKey key = this . createCacheKey ( ms, parameter, rowBounds, boundSql) ;
return this . query ( ms, parameter, rowBounds, resultHandler, key, boundSql) ;
}
public < E > List < E > query ( MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext . instance ( ) . resource ( ms. getResource ( ) ) . activity ( "executing a query" ) . object ( ms. getId ( ) ) ;
if ( this . closed) {
throw new ExecutorException ( "Executor was closed." ) ;
} else {
if ( this . queryStack == 0 && ms. isFlushCacheRequired ( ) ) {
this . clearLocalCache ( ) ;
}
List list;
try {
++ this . queryStack;
list = resultHandler == null ? ( List ) this . localCache. getObject ( key) : null ;
if ( list != null ) {
this . handleLocallyCachedOutputParameters ( ms, key, parameter, boundSql) ;
} else {
list = this . queryFromDatabase ( ms, parameter, rowBounds, resultHandler, key, boundSql) ;
}
} finally {
-- this . queryStack;
}
if ( this . queryStack == 0 ) {
Iterator var8 = this . deferredLoads. iterator ( ) ;
while ( var8. hasNext ( ) ) {
BaseExecutor. DeferredLoad deferredLoad = ( BaseExecutor. DeferredLoad ) var8. next ( ) ;
deferredLoad. load ( ) ;
}
this . deferredLoads. clear ( ) ;
if ( this . configuration. getLocalCacheScope ( ) == LocalCacheScope . STATEMENT ) {
this . clearLocalCache ( ) ;
}
}
return list;
}
}
private < E > List < E > queryFromDatabase ( MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
this . localCache. putObject ( key, ExecutionPlaceholder . EXECUTION_PLACEHOLDER ) ;
List list;
try {
list = this . doQuery ( ms, parameter, rowBounds, resultHandler, boundSql) ;
} finally {
this . localCache. removeObject ( key) ;
}
this . localCache. putObject ( key, list) ;
if ( ms. getStatementType ( ) == StatementType . CALLABLE ) {
this . localOutputParameterCache. putObject ( key, parameter) ;
}
return list;
}
}
public class SimpleExecutor extends BaseExecutor {
public < E > List < E > doQuery ( MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null ;
List var9;
try {
Configuration configuration = ms. getConfiguration ( ) ;
StatementHandler handler = configuration. newStatementHandler ( this . wrapper, ms, parameter, rowBounds, resultHandler, boundSql) ;
stmt = this . prepareStatement ( handler, ms. getStatementLog ( ) ) ;
var9 = handler. query ( stmt, resultHandler) ;
} finally {
this . closeStatement ( stmt) ;
}
return var9;
}
private Statement prepareStatement ( StatementHandler handler, Log statementLog) throws SQLException {
Connection connection = this . getConnection ( statementLog) ;
Statement stmt = handler. prepare ( connection, this . transaction. getTimeout ( ) ) ;
handler. parameterize ( stmt) ;
return stmt;
}
}
上述的Executor.query()方法几经转折,最后会创建一个StatementHandler对象,然后将必要的参数传递给StatementHandler,使用StatementHandler来完成对数据库的查询,最终返回List结果集
从上面的代码中我们可以看出,Executor的功能和作用是:
源码剖析的StatementHandler:
StatementHandler对象主要完成两个工作:
对于JDBC的PreparedStatement类型的对象,创建的过程中,我们使用的是SQL语句字符串会包含若⼲个?占位符,我们其后再对占位符进行设值,StatementHandler通过parameterize(statement)方法对Statement 进行设值
StatementHandler 通过< E> List< E> query(Statement var1, ResultHandler var2) throws SQLException;方法来 完成执行Statement,和将Statement对象返回的resultSet封装成List
进入到 StatementHandler 的 parameterize(statement)方法的实现:
public void parameterize ( Statement statement) throws SQLException {
this . parameterHandler. setParameters ( ( PreparedStatement ) statement) ;
}
public class DefaultParameterHandler implements ParameterHandler {
public void setParameters ( PreparedStatement ps) {
ErrorContext . instance ( ) . activity ( "setting parameters" ) . object ( this . mappedStatement. getParameterMap ( ) . getId ( ) ) ;
List < ParameterMapping > parameterMappings = this . boundSql. getParameterMappings ( ) ;
if ( parameterMappings != null ) {
for ( int i = 0 ; i < parameterMappings. size ( ) ; ++ i) {
ParameterMapping parameterMapping = ( ParameterMapping ) parameterMappings. get ( i) ;
if ( parameterMapping. getMode ( ) != ParameterMode . OUT ) {
String propertyName = parameterMapping. getProperty ( ) ;
Object value;
if ( this . boundSql. hasAdditionalParameter ( propertyName) ) {
value = this . boundSql. getAdditionalParameter ( propertyName) ;
} else if ( this . parameterObject == null ) {
value = null ;
} else if ( this . typeHandlerRegistry. hasTypeHandler ( this . parameterObject. getClass ( ) ) ) {
value = this . parameterObject;
} else {
MetaObject metaObject = this . configuration. newMetaObject ( this . parameterObject) ;
value = metaObject. getValue ( propertyName) ;
}
TypeHandler typeHandler = parameterMapping. getTypeHandler ( ) ;
JdbcType jdbcType = parameterMapping. getJdbcType ( ) ;
if ( value == null && jdbcType == null ) {
jdbcType = this . configuration. getJdbcTypeForNull ( ) ;
}
try {
typeHandler. setParameter ( ps, i + 1 , value, jdbcType) ;
} catch ( SQLException | TypeException var10) {
throw new TypeException ( "Could not set parameters for mapping: " + parameterMapping + ". Cause: " + var10, var10) ;
}
}
}
}
}
}
从上述的代码可以看到,StatementHandler的parameterize(Statement)方法调用了ParameterHandler的setParameters(statement)方法
ParameterHandler的setParameters(Statement )方法负责根据我们输入的参数,对statement对象的 ?占位符处进行赋值
进入到StatementHandler 的< E> List< E> query(Statement var1, ResultHandler var2) throws SQLException;方法的 实现:
public < E > List < E > query ( Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = ( PreparedStatement ) statement;
ps. execute ( ) ;
return this . resultSetHandler. handleResultSets ( ps) ;
}
public class DefaultResultSetHandler implements ResultSetHandler {
public List < Object > handleResultSets ( Statement stmt) throws SQLException {
ErrorContext . instance ( ) . activity ( "handling results" ) . object ( this . mappedStatement. getId ( ) ) ;
List < Object > multipleResults = new ArrayList ( ) ;
int resultSetCount = 0 ;
ResultSetWrapper rsw = this . getFirstResultSet ( stmt) ;
List < ResultMap > resultMaps = this . mappedStatement. getResultMaps ( ) ;
int resultMapCount = resultMaps. size ( ) ;
this . validateResultMapsCount ( rsw, resultMapCount) ;
while ( rsw != null && resultMapCount > resultSetCount) {
ResultMap resultMap = ( ResultMap ) resultMaps. get ( resultSetCount) ;
this . handleResultSet ( rsw, resultMap, multipleResults, ( ResultMapping ) null ) ;
rsw = this . getNextResultSet ( stmt) ;
this . cleanUpAfterHandlingResultSet ( ) ;
++ resultSetCount;
}
String [ ] resultSets = this . mappedStatement. getResultSets ( ) ;
if ( resultSets != null ) {
while ( rsw != null && resultSetCount < resultSets. length) {
ResultMapping parentMapping = ( ResultMapping ) this . nextResultMaps. get ( resultSets[ resultSetCount] ) ;
if ( parentMapping != null ) {
String nestedResultMapId = parentMapping. getNestedResultMapId ( ) ;
ResultMap resultMap = this . configuration. getResultMap ( nestedResultMapId) ;
this . handleResultSet ( rsw, resultMap, ( List ) null , parentMapping) ;
}
rsw = this . getNextResultSet ( stmt) ;
this . cleanUpAfterHandlingResultSet ( ) ;
++ resultSetCount;
}
}
return this . collapseSingleResultList ( multipleResults) ;
}
}
因为是list集合,那么最终的selectList方法就这个这个返回了,而因为返回值类型是泛型方法的返回值,所以不用强转(自定义泛型方法一般不用强转的,只看最终的结果,但也只是以泛型为主的对应),至此传统方式大致说明完毕
Mapper代理方式:
UserMapper mapper = sqlSession. getMapper ( UserMapper . class ) ;
List < User > list = mapper. getUserByName ( "tom" ) ;
思考一个问题,通常的Mapper接口我们都没有实现的方法却可以使用,是为什么呢?答案很简单,是动态代理
开始之前介绍一下MyBatis初始化时对接口的处理:MapperRegistry是Configuration中的一个属性(一般是创建自身并传递this的对象值,this.mapperRegistry = new MapperRegistry(this);,且是在Configuration的无参构造方法里面操作的)
它内部维护一个HashMap用于存放mapper接口的工厂类,每个接口对应一个工厂类,mappers中可以 配置接口的包路径,或者某个具体的接口类
< mappers>
< package name = " com.lagou.mapper" />
</ mappers>
当解析mappers标签时,它会判断解析到的是mapper配置文件时,会再将对应配置文件中的增删改查标签 封装成MappedStatement对象,存入mappedStatements中(Configuration里面的,一般只有一个map),当判断解析到接口时(以getMapper作为判断,因为方法不同的吗),会建此接口对应的MapperProxyFactory对象(包含多个接口里的接口方法,所以对应的MapperProxyFactory里面还有一个map来定位方法,即总共两个map,即操作了分开,即namespace和id分开了,而不是一起了),存入HashMap中(MapperRegistry里面的,也在Configuration里面),key =接口的字节码对象,value =此接 口对应的MapperProxyFactory对象,内部最终还是原版操作的
源码剖析的getmapper():
进入 sqlSession.getMapper(UserMapper.class )中:
public class MapperRegistry {
private final Configuration config;
private final Map < Class < ? > , MapperProxyFactory < ? > > knownMappers = new HashMap ( ) ;
public < T > T getMapper ( Class < T > type, SqlSession sqlSession) {
MapperProxyFactory < T > mapperProxyFactory = ( MapperProxyFactory ) this . knownMappers. get ( type) ;
if ( mapperProxyFactory == null ) {
throw new BindingException ( "Type " + type + " is not known to the MapperRegistry." ) ;
} else {
try {
return mapperProxyFactory. newInstance ( sqlSession) ;
} catch ( Exception var5) {
throw new BindingException ( "Error getting mapper instance. Cause: " + var5, var5) ;
}
}
}
}
public class MapperProxyFactory < T > {
private final Class < T > mapperInterface;
private final Map < Method , MapperMethodInvoker > methodCache = new ConcurrentHashMap ( ) ;
protected T newInstance ( MapperProxy < T > mapperProxy) {
return Proxy . newProxyInstance ( this . mapperInterface. getClassLoader ( ) , new Class [ ] { this . mapperInterface} , mapperProxy) ;
}
public T newInstance ( SqlSession sqlSession) {
MapperProxy < T > mapperProxy = new MapperProxy ( sqlSession, this . mapperInterface, this . methodCache) ;
return this . newInstance ( mapperProxy) ;
}
}
public class MapperProxy < T > implements InvocationHandler , Serializable {
private static final long serialVersionUID = - 4724728412955527868L ;
private static final int ALLOWED_MODES = 15 ;
private static final Constructor < Lookup > lookupConstructor;
private static final Method privateLookupInMethod;
private final SqlSession sqlSession;
private final Class < T > mapperInterface;
private final Map < Method , MapperProxy. MapperMethodInvoker > methodCache;
public MapperProxy ( SqlSession sqlSession, Class < T > mapperInterface, Map < Method , MapperProxy. MapperMethodInvoker > methodCache) {
this . sqlSession = sqlSession;
this . mapperInterface = mapperInterface;
this . methodCache = methodCache;
}
}
源码剖析的invoke():
既然是jdk代理,那么必然是操作了invoke()方法来进行增强操作,使得可以通过接口来得到sql并执行(主要是可以自动定位的操作,而不是硬编码的出现),我们可以进行观察:
public Object invoke ( Object proxy, Method method, Object [ ] args) throws Throwable {
try {
return Object . class . equals ( method. getDeclaringClass ( ) ) ? method. invoke ( this , args) : this . cachedInvoker ( method) . invoke ( proxy, method, args, this . sqlSession) ;
} catch ( Throwable var5) {
throw ExceptionUtil . unwrapThrowable ( var5) ;
}
}
上面在动态代理返回了示例后,我们就可以直接调用mapper类中的方法了,但代理对象调用方法,执行是 在MapperProxy中的invoke方法中
我们进入如下:
public class MapperProxy < T > implements InvocationHandler , Serializable {
private MapperProxy. MapperMethodInvoker cachedInvoker ( Method method) throws Throwable {
try {
return ( MapperProxy. MapperMethodInvoker ) this . methodCache. computeIfAbsent ( method, ( m) -> {
if ( m. isDefault ( ) ) {
try {
return privateLookupInMethod == null ? new MapperProxy. DefaultMethodInvoker ( this . getMethodHandleJava8 ( method) ) : new MapperProxy. DefaultMethodInvoker ( this . getMethodHandleJava9 ( method) ) ;
} catch ( InstantiationException | InvocationTargetException | NoSuchMethodException | IllegalAccessException var4) {
throw new RuntimeException ( var4) ;
}
} else {
return new MapperProxy. PlainMethodInvoker ( new MapperMethod ( this . mapperInterface, method, this . sqlSession. getConfiguration ( ) ) ) ;
}
} ) ;
} catch ( RuntimeException var4) {
Throwable cause = var4. getCause ( ) ;
throw ( Throwable ) ( cause == null ? var4 : cause) ;
}
}
}
private static class PlainMethodInvoker implements MapperProxy. MapperMethodInvoker {
private final MapperMethod mapperMethod;
public PlainMethodInvoker ( MapperMethod mapperMethod) {
this . mapperMethod = mapperMethod;
}
public Object invoke ( Object proxy, Method method, Object [ ] args, SqlSession sqlSession) throws Throwable {
return this . mapperMethod. execute ( sqlSession, args) ;
}
}
进入execute方法:
public class MapperMethod {
public Object execute ( SqlSession sqlSession, Object [ ] args) {
Object result;
Object param;
switch ( this . command. getType ( ) ) {
case INSERT :
param = this . method. convertArgsToSqlCommandParam ( args) ;
result = this . rowCountResult ( sqlSession. insert ( this . command. getName ( ) , param) ) ;
break ;
case UPDATE :
param = this . method. convertArgsToSqlCommandParam ( args) ;
result = this . rowCountResult ( sqlSession. update ( this . command. getName ( ) , param) ) ;
break ;
case DELETE :
param = this . method. convertArgsToSqlCommandParam ( args) ;
result = this . rowCountResult ( sqlSession. delete ( this . command. getName ( ) , param) ) ;
break ;
case SELECT :
if ( this . method. returnsVoid ( ) && this . method. hasResultHandler ( ) ) {
this . executeWithResultHandler ( sqlSession, args) ;
result = null ;
} else if ( this . method. returnsMany ( ) ) {
result = this . executeForMany ( sqlSession, args) ;
} else if ( this . method. returnsMap ( ) ) {
result = this . executeForMap ( sqlSession, args) ;
} else if ( this . method. returnsCursor ( ) ) {
result = this . executeForCursor ( sqlSession, args) ;
} else {
param = this . method. convertArgsToSqlCommandParam ( args) ;
result = sqlSession. selectOne ( this . command. getName ( ) , param) ;
if ( this . method. returnsOptional ( ) && ( result == null || ! this . method. getReturnType ( ) . equals ( result. getClass ( ) ) ) ) {
result = Optional . ofNullable ( result) ;
}
}
break ;
case FLUSH :
result = sqlSession. flushStatements ( ) ;
break ;
default :
throw new BindingException ( "Unknown execution method for: " + this . command. getName ( ) ) ;
}
if ( result == null && this . method. getReturnType ( ) . isPrimitive ( ) && ! this . method. returnsVoid ( ) ) {
throw new BindingException ( "Mapper method '" + this . command. getName ( ) + " attempted to return null from a method with a primitive return type (" + this . method. getReturnType ( ) + ")." ) ;
} else {
return result;
}
}
}
很明显,与之前说的一样,他最终还是操作传统的方法,对应的参数并没有说明具体怎么来的,但是实际上他就是进行了操作,这是因为前面我只是大致的给出方向,并没有更加的细分,就如前面说的框架很多,终其一生也是看不完的,我们只需要知道原理即可
二级缓存源码剖析:
前面我们说明了一级缓存的操作,但是二级缓存并没有给出具体的源码实现,所以现在来进行分析
二级缓存构建在一级缓存之上,在收到查询请求时,MyBatis ⾸先会查询二级缓存,若二级缓存未命 中,再去查询一级缓存,一级缓存没有,再查询数据库
与一级缓存不同,二级缓存和具体的命名空间绑定,一个Mapper中有一个Cache,且相同Mapper中的MappedStatement共用一个Cache(该一个Cache),一级缓存则是和 SqlSession 绑定
启用二级缓存:
开启全局二级缓存配置:
< settings>
< setting name = " cacheEnabled" value = " true" />
</ settings>
在需要使用二级缓存的Mapper配置文件中配置< cache>标签
< cache> </ cache>
在具体CURD标签上配置 useCache=true:
< select id = " findById" resultType = " com.lagou.pojo.User" useCache = " true" >
select * from user where id = #{id}
</ select>
由于其他两个配置都默认为true,所以我们主要看< cache>标签
标签 < cache/> 的解析:
根据之前的mybatis源码剖析,xml的解析工作主要交给XMLConfigBuilder.parse()方法来实现
public class XMLConfigBuilder extends BaseBuilder {
public Configuration parse ( ) {
if ( this . parsed) {
throw new BuilderException ( "Each XMLConfigBuilder can only be used once." ) ;
} else {
this . parsed = true ;
this . parseConfiguration ( this . parser. evalNode ( "/configuration" ) ) ;
return this . configuration;
}
}
private void parseConfiguration ( XNode root) {
try {
this . propertiesElement ( root. evalNode ( "properties" ) ) ;
Properties settings = this . settingsAsProperties ( root. evalNode ( "settings" ) ) ;
this . loadCustomVfs ( settings) ;
this . loadCustomLogImpl ( settings) ;
this . typeAliasesElement ( root. evalNode ( "typeAliases" ) ) ;
this . pluginElement ( root. evalNode ( "plugins" ) ) ;
this . objectFactoryElement ( root. evalNode ( "objectFactory" ) ) ;
this . objectWrapperFactoryElement ( root. evalNode ( "objectWrapperFactory" ) ) ;
this . reflectorFactoryElement ( root. evalNode ( "reflectorFactory" ) ) ;
this . settingsElement ( settings) ;
this . environmentsElement ( root. evalNode ( "environments" ) ) ;
this . databaseIdProviderElement ( root. evalNode ( "databaseIdProvider" ) ) ;
this . typeHandlerElement ( root. evalNode ( "typeHandlers" ) ) ;
this . mapperElement ( root. evalNode ( "mappers" ) ) ;
} catch ( Exception var3) {
throw new BuilderException ( "Error parsing SQL Mapper Configuration. Cause: " + var3, var3) ;
}
}
private void mapperElement ( XNode parent) throws Exception {
if ( parent != null ) {
Iterator var2 = parent. getChildren ( ) . iterator ( ) ;
while ( true ) {
while ( var2. hasNext ( ) ) {
XNode child = ( XNode ) var2. next ( ) ;
String resource;
if ( "package" . equals ( child. getName ( ) ) ) {
resource = child. getStringAttribute ( "name" ) ;
this . configuration. addMappers ( resource) ;
} else {
resource = child. getStringAttribute ( "resource" ) ;
String url = child. getStringAttribute ( "url" ) ;
String mapperClass = child. getStringAttribute ( "class" ) ;
XMLMapperBuilder mapperParser;
InputStream inputStream;
if ( resource != null && url == null && mapperClass == null ) {
ErrorContext . instance ( ) . resource ( resource) ;
inputStream = Resources . getResourceAsStream ( resource) ;
mapperParser = new XMLMapperBuilder ( inputStream, this . configuration, resource, this . configuration. getSqlFragments ( ) ) ;
mapperParser. parse ( ) ;
} else if ( resource == null && url != null && mapperClass == null ) {
ErrorContext . instance ( ) . resource ( url) ;
inputStream = Resources . getUrlAsStream ( url) ;
mapperParser = new XMLMapperBuilder ( inputStream, this . configuration, url, this . configuration. getSqlFragments ( ) ) ;
mapperParser. parse ( ) ;
} else {
if ( resource != null || url != null || mapperClass == null ) {
throw new BuilderException ( "A mapper element may only specify a url, resource or class, but not more than one." ) ;
}
Class < ? > mapperInterface = Resources . classForName ( mapperClass) ;
this . configuration. addMapper ( mapperInterface) ;
}
}
}
return ;
}
}
}
}
public class XMLMapperBuilder extends BaseBuilder {
public void parse ( ) {
if ( ! this . configuration. isResourceLoaded ( this . resource) ) {
this . configurationElement ( this . parser. evalNode ( "/mapper" ) ) ;
this . configuration. addLoadedResource ( this . resource) ;
this . bindMapperForNamespace ( ) ;
}
this . parsePendingResultMaps ( ) ;
this . parsePendingCacheRefs ( ) ;
this . parsePendingStatements ( ) ;
}
private void configurationElement ( XNode context) {
try {
String namespace = context. getStringAttribute ( "namespace" ) ;
if ( namespace != null && ! namespace. equals ( "" ) ) {
this . builderAssistant. setCurrentNamespace ( namespace) ;
this . cacheRefElement ( context. evalNode ( "cache-ref" ) ) ;
this . cacheElement ( context. evalNode ( "cache" ) ) ;
this . parameterMapElement ( context. evalNodes ( "/mapper/parameterMap" ) ) ;
this . resultMapElements ( context. evalNodes ( "/mapper/resultMap" ) ) ;
this . sqlElement ( context. evalNodes ( "/mapper/sql" ) ) ;
this . buildStatementFromContext ( context. evalNodes ( "select|insert|update|delete" ) ) ;
} else {
throw new BuilderException ( "Mapper's namespace cannot be empty" ) ;
}
} catch ( Exception var3) {
throw new BuilderException ( "Error parsing Mapper XML. The XML location is '" + this . resource + "'. Cause: " + var3, var3) ;
}
}
private void cacheElement ( XNode context) {
if ( context != null ) {
String type = context. getStringAttribute ( "type" , "PERPETUAL" ) ;
Class < ? extends Cache > typeClass = this . typeAliasRegistry. resolveAlias ( type) ;
String eviction = context. getStringAttribute ( "eviction" , "LRU" ) ;
Class < ? extends Cache > evictionClass = this . typeAliasRegistry. resolveAlias ( eviction) ;
Long flushInterval = context. getLongAttribute ( "flushInterval" ) ;
Integer size = context. getIntAttribute ( "size" ) ;
boolean readWrite = ! context. getBooleanAttribute ( "readOnly" , false ) ;
boolean blocking = context. getBooleanAttribute ( "blocking" , false ) ;
Properties props = context. getChildrenAsProperties ( ) ;
this . builderAssistant. useNewCache ( typeClass, evictionClass, flushInterval, size, readWrite, blocking, props) ;
}
}
}
先来看看是如何构建Cache对象的:
public class MapperBuilderAssistant extends BaseBuilder {
public Cache useNewCache ( Class < ? extends Cache > typeClass, Class < ? extends Cache > evictionClass, Long flushInterval, Integer size, boolean readWrite, boolean blocking, Properties props) {
Cache cache = ( new CacheBuilder ( this . currentNamespace) ) . implementation ( ( Class ) this . valueOrDefault ( typeClass, PerpetualCache . class ) ) . addDecorator ( ( Class ) this . valueOrDefault ( evictionClass, LruCache . class ) ) . clearInterval ( flushInterval) . size ( size) . readWrite ( readWrite) . blocking ( blocking) . properties ( props) . build ( ) ;
this . configuration. addCache ( cache) ;
this . currentCache = cache;
return cache;
}
}
我们看到一个Mapper.xml只会解析一次标签,也就是只创建一次Cache对象,放进configuration中, 并将cache赋值给MapperBuilderAssistant.currentCache
我们回到这里:
this . cacheElement ( context. evalNode ( "cache" ) ) ;
this . parameterMapElement ( context. evalNodes ( "/mapper/parameterMap" ) ) ;
this . resultMapElements ( context. evalNodes ( "/mapper/resultMap" ) ) ;
this . sqlElement ( context. evalNodes ( "/mapper/sql" ) ) ;
this . buildStatementFromContext ( context. evalNodes ( "select|insert|update|delete" ) ) ;
进入最后的操作:
private void buildStatementFromContext ( List < XNode > list) {
if ( this . configuration. getDatabaseId ( ) != null ) {
this . buildStatementFromContext ( list, this . configuration. getDatabaseId ( ) ) ;
}
this . buildStatementFromContext ( list, ( String ) null ) ;
}
private void buildStatementFromContext ( List < XNode > list, String requiredDatabaseId) {
Iterator var3 = list. iterator ( ) ;
while ( var3. hasNext ( ) ) {
XNode context = ( XNode ) var3. next ( ) ;
XMLStatementBuilder statementParser = new XMLStatementBuilder ( this . configuration, this . builderAssistant, context, requiredDatabaseId) ;
try {
statementParser. parseStatementNode ( ) ;
} catch ( IncompleteElementException var7) {
this . configuration. addIncompleteStatement ( statementParser) ;
}
}
}
public class XMLStatementBuilder extends BaseBuilder {
public void parseStatementNode ( ) {
String id = this . context. getStringAttribute ( "id" ) ;
String databaseId = this . context. getStringAttribute ( "databaseId" ) ;
if ( this . databaseIdMatchesCurrent ( id, databaseId, this . requiredDatabaseId) ) {
String nodeName = this . context. getNode ( ) . getNodeName ( ) ;
SqlCommandType sqlCommandType = SqlCommandType . valueOf ( nodeName. toUpperCase ( Locale . ENGLISH ) ) ;
boolean isSelect = sqlCommandType == SqlCommandType . SELECT ;
boolean flushCache = this . context. getBooleanAttribute ( "flushCache" , ! isSelect) ;
boolean useCache = this . context. getBooleanAttribute ( "useCache" , isSelect) ;
boolean resultOrdered = this . context. getBooleanAttribute ( "resultOrdered" , false ) ;
XMLIncludeTransformer includeParser = new XMLIncludeTransformer ( this . configuration, this . builderAssistant) ;
includeParser. applyIncludes ( this . context. getNode ( ) ) ;
String parameterType = this . context. getStringAttribute ( "parameterType" ) ;
Class < ? > parameterTypeClass = this . resolveClass ( parameterType) ;
String lang = this . context. getStringAttribute ( "lang" ) ;
LanguageDriver langDriver = this . getLanguageDriver ( lang) ;
this . processSelectKeyNodes ( id, parameterTypeClass, langDriver) ;
String keyStatementId = id + "!selectKey" ;
keyStatementId = this . builderAssistant. applyCurrentNamespace ( keyStatementId, true ) ;
Object keyGenerator;
if ( this . configuration. hasKeyGenerator ( keyStatementId) ) {
keyGenerator = this . configuration. getKeyGenerator ( keyStatementId) ;
} else {
keyGenerator = this . context. getBooleanAttribute ( "useGeneratedKeys" , this . configuration. isUseGeneratedKeys ( ) && SqlCommandType . INSERT . equals ( sqlCommandType) ) ? Jdbc3KeyGenerator . INSTANCE : NoKeyGenerator . INSTANCE ;
}
SqlSource sqlSource = langDriver. createSqlSource ( this . configuration, this . context, parameterTypeClass) ;
StatementType statementType = StatementType . valueOf ( this . context. getStringAttribute ( "statementType" , StatementType . PREPARED . toString ( ) ) ) ;
Integer fetchSize = this . context. getIntAttribute ( "fetchSize" ) ;
Integer timeout = this . context. getIntAttribute ( "timeout" ) ;
String parameterMap = this . context. getStringAttribute ( "parameterMap" ) ;
String resultType = this . context. getStringAttribute ( "resultType" ) ;
Class < ? > resultTypeClass = this . resolveClass ( resultType) ;
String resultMap = this . context. getStringAttribute ( "resultMap" ) ;
String resultSetType = this . context. getStringAttribute ( "resultSetType" ) ;
ResultSetType resultSetTypeEnum = this . resolveResultSetType ( resultSetType) ;
if ( resultSetTypeEnum == null ) {
resultSetTypeEnum = this . configuration. getDefaultResultSetType ( ) ;
}
String keyProperty = this . context. getStringAttribute ( "keyProperty" ) ;
String keyColumn = this . context. getStringAttribute ( "keyColumn" ) ;
String resultSets = this . context. getStringAttribute ( "resultSets" ) ;
this . builderAssistant. addMappedStatement ( id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered, ( KeyGenerator ) keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets) ;
}
}
}
public class MapperBuilderAssistant extends BaseBuilder {
public MappedStatement addMappedStatement ( String id, SqlSource sqlSource, StatementType statementType, SqlCommandType sqlCommandType, Integer fetchSize, Integer timeout, String parameterMap, Class < ? > parameterType, String resultMap, Class < ? > resultType, ResultSetType resultSetType, boolean flushCache, boolean useCache, boolean resultOrdered, KeyGenerator keyGenerator, String keyProperty, String keyColumn, String databaseId, LanguageDriver lang, String resultSets) {
if ( this . unresolvedCacheRef) {
throw new IncompleteElementException ( "Cache-ref not yet resolved" ) ;
} else {
id = this . applyCurrentNamespace ( id, false ) ;
boolean isSelect = sqlCommandType == SqlCommandType . SELECT ;
org. apache. ibatis. mapping. MappedStatement. Builder statementBuilder = ( new org. apache. ibatis. mapping. MappedStatement. Builder( this . configuration, id, sqlSource, sqlCommandType) ) . resource ( this . resource) . fetchSize ( fetchSize) . timeout ( timeout) . statementType ( statementType) . keyGenerator ( keyGenerator) . keyProperty ( keyProperty) . keyColumn ( keyColumn) . databaseId ( databaseId) . lang ( lang) . resultOrdered ( resultOrdered) . resultSets ( resultSets) . resultMaps ( this . getStatementResultMaps ( resultMap, resultType, id) ) . resultSetType ( resultSetType) . flushCacheRequired ( ( Boolean ) this . valueOrDefault ( flushCache, ! isSelect) ) . useCache ( ( Boolean ) this . valueOrDefault ( useCache, isSelect) ) . cache ( this . currentCache) ;
ParameterMap statementParameterMap = this . getStatementParameterMap ( parameterMap, parameterType, id) ;
if ( statementParameterMap != null ) {
statementBuilder. parameterMap ( statementParameterMap) ;
}
MappedStatement statement = statementBuilder. build ( ) ;
this . configuration. addMappedStatement ( statement) ;
return statement;
}
}
}
我们看到将Mapper中创建的Cache对象,加入到了每个MappedStatement对象中,也就是同一个Mapper中,即的确相同Mapper中的MappedStatement共用一个Cache(该一个Cache),当然了源码只是大致的给出说明,具体为什么需要深入看很多,所以了解即可
现在我们直接找到之前的如下:
public class CachingExecutor implements Executor {
public < E > List < E > query ( MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms. getBoundSql ( parameterObject) ;
CacheKey key = this . createCacheKey ( ms, parameterObject, rowBounds, boundSql) ;
return this . query ( ms, parameterObject, rowBounds, resultHandler, key, boundSql) ;
}
public < E > List < E > query ( MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
Cache cache = ms. getCache ( ) ;
if ( cache != null ) {
this . flushCacheIfRequired ( ms) ;
if ( ms. isUseCache ( ) && resultHandler == null ) {
this . ensureNoOutParams ( ms, boundSql) ;
List < E > list = ( List ) this . tcm. getObject ( cache, key) ;
if ( list == null ) {
list = this . delegate. query ( ms, parameterObject, rowBounds, resultHandler, key, boundSql) ;
this . tcm. putObject ( cache, key, list) ;
}
return list;
}
}
return this . delegate. query ( ms, parameterObject, rowBounds, resultHandler, key, boundSql) ;
}
private void flushCacheIfRequired ( MappedStatement ms) {
Cache cache = ms. getCache ( ) ;
if ( cache != null && ms. isFlushCacheRequired ( ) ) {
this . tcm. clear ( cache) ;
}
}
}
如果设置了flushCache=“true”,则每次查询都会刷新缓存
< select id = " findbyId" resultType = " com.lagou.pojo.user" useCache = " true"
flushCache = " true" >
select * from user
</ select>
如上,注意二级缓存是从 MappedStatement 中获取的,由于 MappedStatement 存在于全局配置 中,若多个 CachingExecutor 获取到,那么容易出现线程安全问题(比如其中一个导致清除缓存,另外一个就没有了,或者他们同时获取,使得都没有操作缓存),除此之外,若不加以控制,多个 事务共用一个缓存实例,会导致脏读问题(其中一个事务改变缓存,导致另外一个事务读取了你改变的,而不是他原本的,且他没有提交),⾄于脏读问题,需要借助其他类来处理,也就是上面代码中 tcm 变量对应的类型(二级缓存最终是tcm操作,或者说是存放在tcm中),下面分析一下
public class TransactionalCacheManager {
private final Map < Cache , TransactionalCache > transactionalCaches = new HashMap ( ) ;
public TransactionalCacheManager ( ) {
}
public void clear ( Cache cache) {
this . getTransactionalCache ( cache) . clear ( ) ;
}
public Object getObject ( Cache cache, CacheKey key) {
return this . getTransactionalCache ( cache) . getObject ( key) ;
}
public void putObject ( Cache cache, CacheKey key, Object value) {
this . getTransactionalCache ( cache) . putObject ( key, value) ;
}
public void commit ( ) {
Iterator var1 = this . transactionalCaches. values ( ) . iterator ( ) ;
while ( var1. hasNext ( ) ) {
TransactionalCache txCache = ( TransactionalCache ) var1. next ( ) ;
txCache. commit ( ) ;
}
}
public void rollback ( ) {
Iterator var1 = this . transactionalCaches. values ( ) . iterator ( ) ;
while ( var1. hasNext ( ) ) {
TransactionalCache txCache = ( TransactionalCache ) var1. next ( ) ;
txCache. rollback ( ) ;
}
}
private TransactionalCache getTransactionalCache ( Cache cache) {
return ( TransactionalCache ) this . transactionalCaches. computeIfAbsent ( cache, TransactionalCache :: new ) ;
}
}
TransactionalCacheManager 内部维护了 Cache 实例与 TransactionalCache 实例间的映射关系,该类 也仅负责维护两者的映射关系,真正做事的还是 TransactionalCache,TransactionalCache 是一种缓 存装饰器,可以为 Cache 实例增加事务功能,在之前提到的脏读问题正是由该类进行处理的,下面分析一下该类的逻辑
public class TransactionalCache implements Cache {
private static final Log log = LogFactory . getLog ( TransactionalCache . class ) ;
private final Cache delegate;
private boolean clearOnCommit;
private final Map < Object , Object > entriesToAddOnCommit;
private final Set < Object > entriesMissedInCache;
public TransactionalCache ( Cache delegate) {
this . delegate = delegate;
this . clearOnCommit = false ;
this . entriesToAddOnCommit = new HashMap ( ) ;
this . entriesMissedInCache = new HashSet ( ) ;
}
public String getId ( ) {
return this . delegate. getId ( ) ;
}
public int getSize ( ) {
return this . delegate. getSize ( ) ;
}
public Object getObject ( Object key) {
Object object = this . delegate. getObject ( key) ;
if ( object == null ) {
this . entriesMissedInCache. add ( key) ;
}
return this . clearOnCommit ? null : object;
}
public void putObject ( Object key, Object object) {
this . entriesToAddOnCommit. put ( key, object) ;
}
public Object removeObject ( Object key) {
return null ;
}
public void clear ( ) {
this . clearOnCommit = true ;
this . entriesToAddOnCommit. clear ( ) ;
}
public void commit ( ) {
if ( this . clearOnCommit) {
this . delegate. clear ( ) ;
}
this . flushPendingEntries ( ) ;
this . reset ( ) ;
}
public void rollback ( ) {
this . unlockMissedEntries ( ) ;
this . reset ( ) ;
}
private void reset ( ) {
this . clearOnCommit = false ;
this . entriesToAddOnCommit. clear ( ) ;
this . entriesMissedInCache. clear ( ) ;
}
private void flushPendingEntries ( ) {
Iterator var1 = this . entriesToAddOnCommit. entrySet ( ) . iterator ( ) ;
while ( var1. hasNext ( ) ) {
Entry < Object , Object > entry = ( Entry ) var1. next ( ) ;
this . delegate. putObject ( entry. getKey ( ) , entry. getValue ( ) ) ;
}
var1 = this . entriesMissedInCache. iterator ( ) ;
while ( var1. hasNext ( ) ) {
Object entry = var1. next ( ) ;
if ( ! this . entriesToAddOnCommit. containsKey ( entry) ) {
this . delegate. putObject ( entry, ( Object ) null ) ;
}
}
}
private void unlockMissedEntries ( ) {
Iterator var1 = this . entriesMissedInCache. iterator ( ) ;
while ( var1. hasNext ( ) ) {
Object entry = var1. next ( ) ;
try {
this . delegate. removeObject ( entry) ;
} catch ( Exception var4) {
log. warn ( "Unexpected exception while notifiying a rollback to the cache adapter. Consider upgrading your cache adapter to the latest version. Cause: " + var4) ;
}
}
}
}
存储二级缓存对象的时候是放到了TransactionalCache.entriesToAddOnCommit这个map中,但是每 次查询的时候是直接从TransactionalCache.delegate中去查询的(他就是另外一个map,即二级缓存),这个二级缓存查询数据库后,设 置缓存值是没有⽴刻⽣效的,主要是因为直接存到 delegate 会导致脏数据问题,所以可以看到putObject方法并没有存放在delegate中,这也就解决了脏读问题
既然存放不会放在对应的delegate 中,那么怎么存放呢,这就是一级缓存刷新到二级缓存的原因了,当然,这个怎么解决脏读问题呢
一般sqlSession.commit或者sqlSession.close执行后,那么一级缓存中内容才会刷新到二级缓存
为何只有SqlSession提交或关闭之后才会进行刷新到二级缓存呢,我们看如下:
public class DefaultSqlSession implements SqlSession {
public void commit ( boolean force) {
try {
this . executor. commit ( this . isCommitOrRollbackRequired ( force) ) ;
this . dirty = false ;
} catch ( Exception var6) {
throw ExceptionFactory . wrapException ( "Error committing transaction. Cause: " + var6, var6) ;
} finally {
ErrorContext . instance ( ) . reset ( ) ;
}
}
}
public class CachingExecutor implements Executor {
public void commit ( boolean required) throws SQLException {
this . delegate. commit ( required) ;
this . tcm. commit ( ) ;
}
}
public void commit ( ) {
Iterator var1 = this . transactionalCaches. values ( ) . iterator ( ) ;
while ( var1. hasNext ( ) ) {
TransactionalCache txCache = ( TransactionalCache ) var1. next ( ) ;
txCache. commit ( ) ;
}
}
public void commit ( ) {
if ( this . clearOnCommit) {
this . delegate. clear ( ) ;
}
this . flushPendingEntries ( ) ;
this . reset ( ) ;
}
private void flushPendingEntries ( ) {
Iterator var1 = this . entriesToAddOnCommit. entrySet ( ) . iterator ( ) ;
while ( var1. hasNext ( ) ) {
Entry < Object , Object > entry = ( Entry ) var1. next ( ) ;
this . delegate. putObject ( entry. getKey ( ) , entry. getValue ( ) ) ;
}
var1 = this . entriesMissedInCache. iterator ( ) ;
while ( var1. hasNext ( ) ) {
Object entry = var1. next ( ) ;
if ( ! this . entriesToAddOnCommit. containsKey ( entry) ) {
this . delegate. putObject ( entry, ( Object ) null ) ;
}
}
}
二级缓存的清除:
一般情况下,增删改会清空缓存,包括一级和二级
比如我们可以看看更新操作:
public int update ( String statement, Object parameter) {
int var4;
try {
this . dirty = true ;
MappedStatement ms = this . configuration. getMappedStatement ( statement) ;
var4 = this . executor. update ( ms, this . wrapCollection ( parameter) ) ;
} catch ( Exception var8) {
throw ExceptionFactory . wrapException ( "Error updating database. Cause: " + var8, var8) ;
} finally {
ErrorContext . instance ( ) . reset ( ) ;
}
return var4;
}
public int update ( MappedStatement ms, Object parameterObject) throws SQLException {
this . flushCacheIfRequired ( ms) ;
return this . delegate. update ( ms, parameterObject) ;
}
private void flushCacheIfRequired ( MappedStatement ms) {
Cache cache = ms. getCache ( ) ;
if ( cache != null && ms. isFlushCacheRequired ( ) ) {
this . tcm. clear ( cache) ;
}
}
可以发现,更新操作不产生二级缓存,但是会清除二级缓存(前提是有提交,根据源码看就知道了),经过测试,在增删改中flushCache默认不是false,而是true
所以说,我们都会认为增删改是会清空一级缓存和二级缓存的
通过前面的源码分析,可以知道:
增删改都会清空一级和二级缓存,若设置了flushCache为false(增删改默认为true的),那么二级缓存不会清空,一级缓存是他自身就会清空,与flushCache没有关系,所以二级缓存还有余地(只要提交了修改的,就会进行清空(这个二级删除的是entriesToAddOnCommit),他会使得提交操作可以真正的删除二级缓存,只有提交后,才会真正删除二级缓存,而增删改一般我们都会提交,所以这里就默认清空二级缓存了)
若出现提交和关闭(二级缓存关闭操作里面一般会提交后关闭的,一般不会回滚(因为对应的return的其判断基本都是false),或者需要某些条件,具体可以百度,而一级缓存则会清空,包括提交也是),那么一级缓存会到二级缓存,并会清空一级缓存,而操作到二级缓存的前提是一级缓存(该一级缓存是对应缓存类的查询操作出来的,并不是真的一级缓存,或者说就是查询的结果,但是由于查询必然会产生一级缓存,所以可以这样说明)存在,而由于增删改没有一级缓存或者说缓存的操作,所以增删改的提交和关闭不会到二级缓存中,而查询就会,其中查询默认flushCache为false,查询与flushCache有关
其中SqlSession的clearCache方法一般只能清空一级缓存
可以发现MyBatis二级缓存只适用于不常进行增、删、改的数据,⽐如国家行政区省市区街道数据,一但数据变更,MyBatis会清空缓存,因此二级缓存不适用于经常进行更新的数据,那么为什么二级缓存还有余地呢,很明显,这个余地是给查询的,且是二级缓存(因为增删改没有一级,自然没有对应的缓存),说明你的清空还要考虑其他地方操作的缓存,因为你的操作总不能让我再次的查询的,但是一般我们也不会这样,因为虽然不会查询,但是数据基本不好对,所以增删改默认flushCache为true的,使得一直存在删除二级缓存的条件,而由于我们基本都会进行提交,所以,我们认为他就是删除了二级缓存
总结:
在二级缓存的设计上,MyBatis大量地运用了装饰者模式,如CachingExecutor,以及各种Cache接口的装饰器,该模式就是给出相同接口,并给出要装饰的值作为参数给他,并进行增强,他与代理模式不同的是:代理是一个新的类,并且该类里面是操作创建要代理的类的,而不是作为参数,而装饰器则是作为参数,一般来说装饰器就是合成复用原则的基础上,进行操作的增强,而代理就是工厂模式上进行的增强,只是这个工厂一般在构造方法中操作,我们回顾jdk代理,我们可以知道他需要传递一个接口的class,并生成对应的代理类,实际上该代理类有着对应的所有方法,并可以增强的操作,但是对方却是接口,而不是类,那么还能说是代理吗,答:可以,代理主要是增强,如果是类,我们创建即可,如果是接口,我们实现即可,而装饰器就是作为参数的
但是他们的区别并不大,那么为什么要这样的设计呢:这里给出原因,因为装饰器是装饰的,由于是参数,所以可以被多个装饰器装饰,重要的就是装饰,而代理是创建,那么只能由你一个人操作,也就是代你操作了,而不是装饰的被多人操作,即代理给一个人,装饰给多个人,代理重在自身的增强,而装饰考虑多方的增强,也就是说,如果以后有三级缓存,四级缓存,那么我们继续装饰即可,从四级开始,而代理就不能,那么代理的好处是什么,他的好处是他可以给任何操作该类的地方进行增强,而不用再次的创建装饰器,若有很多地方使用,而我只需要改动一处,而装饰器需要给对应的地方都进行创建装饰器,因为各个模块总不能都有相同的类吧,所以各有好处,简单来说,跨项目基本最好使用代理,而单纯的项目,可以利用装饰器(因为类可以合成复用)
但是我们建议最好多利用代理:因为代码是最少的,虽然在扩展上装饰器比较强大,但是跨模板不友好,需要空间多
总之:各有好处
二级缓存的完成说明:
二级缓存实现了Sqlsession之间的缓存数据共享,属于namespace级别
二级缓存具有丰富的缓存策略
二级缓存可由多个装饰器,与基础缓存组合而成
二级缓存工作由 一个缓存装饰执行器CachingExecutor和 一个事务型预缓存TransactionalCache 完成
实际上mybatis对细节的操作还是有很多的问题,比如,在select中操作delete语句,除了select的设置外,语句会执行的,当然,由于大多数操作,如二级缓存等等基本看标签,所以也会导致delete的语句进行了保存,即操作了二级缓存,使得二级缓存也保留了了增删改的操作,这是不好的情况(没有想到),简单来说,标签只是设置一些属性的地方,并不代表语句一定是标签的语句,这也是mybatis的一个细节问题,当然了,随着时间的推移,可能在以后的版本中,会进行修改(如判断语句是否是对应标签的对应语句),或者不用修改,因为我们只要根据规范编写即可,当然,根据我的测试,一般语句和标签不对,他就好像有特殊的操作,使得可能不执行,并返回null或者说规定的返回值(如增删改可能是-1)给缓存(一般增删改没有,只有赋值,而查询两者都有)或者进行赋值,所以我们也不要对这些问题进行太过在意,因为他还是解决的了,只不过并不是报错的解决,而是兜底的解决
延迟加载源码剖析:
在63章博客中,也说明了延迟缓存,但是并没有说明源码(在这里可能会改变对应63章博客的说明,因为这里是按照源码来分析的),所以现在来进行说明,至于配置文件中的某些xml,如动态sql,关联查询等等,实际上就是读取xml解析的过程(一般也不考虑顺序,除非有对应的约束,但是读取一般不用考虑,因为可以直接通过名称获取即可),所以并不需要剖析,即,我们主要看最重要的几个即可,当然,他们有些时候是有限制的,一般情况下,我们规范的写即可,比如关联collection中的column参数只有一个,那么得到的也最好写一个,否则可能报错(关联查询),当然,一般情况下,对应的参数都是传递的一个的值,这里与其他不关联的是不同的,即非常特殊,自己可以进行测试
什么是延迟加载?
在开发过程中很多时候我们并不需要总是在加载用户信息时就一定要加载他的订单信息,此时就是我 们所说的延迟加载,也就是并不需要加载某些东西
延迟加载原理实现:
它的原理是,使用 CGLIB 或 Javassist( 默认 ) 创建目标对象的代理对象,当调用代理对象的延迟加载属 性的 getting 方法时,进入拦截器方法,⽐如调用 a.getB().getName() 方法,进入拦截器的 invoke(…) 方法,发现 a.getB() 需要延迟加载时,那么就会单独发送事先保存好的查询关联 B 对象的 SQL ,把 B 查询上来,然后调用 a.setB(b) 方法,于是 a 对象 b 属性就有值了,接着完 成 a.getB().getName() 方法的调用,这就是延迟加载的基本原理
总结:延迟加载主要是通过动态代理的形式实现,通过代理拦截到指定方法,执行数据加载,而对应的配置就是保存关联代码的操作,简单来说,就是先保存sql,然后在操作对应方法时进行执行,并进行赋值操作,当然,一般会保留原来赋值的变量或者操作来进行获取的
延迟加载原理(源码剖析):
MyBatis延迟加载主要使用:Javassist,Cglib实现,类图展示:
package org. apache. ibatis. executor. loader ;
@Deprecated
public final class JavassistProxyFactory extends org. apache. ibatis. executor. loader. javassist. JavassistProxyFactory {
public JavassistProxyFactory ( ) {
}
}
public class JavassistProxyFactory implements ProxyFactory {
}
@Deprecated
public class CglibProxyFactory extends org. apache. ibatis. executor. loader. cglib. CglibProxyFactory {
public CglibProxyFactory ( ) {
}
}
public class CglibProxyFactory implements ProxyFactory {
}
public interface ProxyFactory {
default void setProperties ( Properties properties) {
}
Object createProxy ( Object var1, ResultLoaderMap var2, Configuration var3, ObjectFactory var4, List < Class < ? > > var5, List < Object > var6) ;
}
Setting 配置加载:
public class Configuration {
protected boolean aggressiveLazyLoading;
public Configuration ( ) {
this . lazyLoadTriggerMethods = new HashSet ( Arrays . asList ( "equals" , "clone" , "hashCode" , "toString" ) ) ;
this . lazyLoadingEnabled = false ;
}
public void setProxyFactory ( ProxyFactory proxyFactory) {
if ( proxyFactory == null ) {
proxyFactory = new JavassistProxyFactory ( ) ;
}
this . proxyFactory = ( ProxyFactory ) proxyFactory;
}
}
延迟加载代理对象创建:
Mybatis的查询结果是由ResultSetHandler接口的handleResultSets()方法处理的(具体在前面说明过了,可以全局搜索),ResultSetHandler接口只有一个实现,DefaultResultSetHandler,接下来看下延迟加载相关的一个核心的方法
public class DefaultResultSetHandler implements ResultSetHandler {
private Object createResultObject ( ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
this . useConstructorMappings = false ;
List < Class < ? > > constructorArgTypes = new ArrayList ( ) ;
List < Object > constructorArgs = new ArrayList ( ) ;
Object resultObject = this . createResultObject ( rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix) ;
if ( resultObject != null && ! this . hasTypeHandlerForResultObject ( rsw, resultMap. getType ( ) ) ) {
List < ResultMapping > propertyMappings = resultMap. getPropertyResultMappings ( ) ;
Iterator var9 = propertyMappings. iterator ( ) ;
while ( var9. hasNext ( ) ) {
ResultMapping propertyMapping = ( ResultMapping ) var9. next ( ) ;
if ( propertyMapping. getNestedQueryId ( ) != null && propertyMapping. isLazy ( ) ) {
resultObject = this . configuration. getProxyFactory ( ) . createProxy ( resultObject, lazyLoader, this . configuration, this . objectFactory, constructorArgTypes, constructorArgs) ;
break ;
}
}
}
this . useConstructorMappings = resultObject != null && ! constructorArgTypes. isEmpty ( ) ;
return resultObject;
}
private Object createResultObject ( ResultSetWrapper rsw, ResultMap resultMap, List < Class < ? > > constructorArgTypes, List < Object > constructorArgs, String columnPrefix) throws SQLException {
Class < ? > resultType = resultMap. getType ( ) ;
MetaClass metaType = MetaClass . forClass ( resultType, this . reflectorFactory) ;
List < ResultMapping > constructorMappings = resultMap. getConstructorResultMappings ( ) ;
if ( this . hasTypeHandlerForResultObject ( rsw, resultType) ) {
return this . createPrimitiveResultObject ( rsw, resultMap, columnPrefix) ;
} else if ( ! constructorMappings. isEmpty ( ) ) {
return this . createParameterizedResultObject ( rsw, resultType, constructorMappings, constructorArgTypes, constructorArgs, columnPrefix) ;
} else if ( ! resultType. isInterface ( ) && ! metaType. hasDefaultConstructor ( ) ) {
if ( this . shouldApplyAutomaticMappings ( resultMap, false ) ) {
return this . createByConstructorSignature ( rsw, resultType, constructorArgTypes, constructorArgs) ;
} else {
throw new ExecutorException ( "Do not know how to create an instance of " + resultType) ;
}
} else {
return this . objectFactory. create ( resultType) ;
}
}
}
默认采用javassistProxy进行代理对象的创建:
resultObject = this . configuration. getProxyFactory ( ) . createProxy ( resultObject, lazyLoader, this . configuration, this . objectFactory, constructorArgTypes, constructorArgs) ;
public ProxyFactory getProxyFactory ( ) {
return this . proxyFactory;
}
所以我们看看这个JavassistProxyFactory类:
public class JavassistProxyFactory implements ProxyFactory {
public Object createProxy ( Object target, ResultLoaderMap lazyLoader, Configuration configuration, ObjectFactory objectFactory, List < Class < ? > > constructorArgTypes, List < Object > constructorArgs) {
return JavassistProxyFactory. EnhancedResultObjectProxyImpl . createProxy ( target, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs) ;
}
static Object crateProxy ( Class < ? > type, MethodHandler callback, List < Class < ? > > constructorArgTypes, List < Object > constructorArgs) {
org. apache. ibatis. javassist. util. proxy. ProxyFactory enhancer = new org. apache. ibatis. javassist. util. proxy. ProxyFactory( ) ;
enhancer. setSuperclass ( type) ;
try {
type. getDeclaredMethod ( "writeReplace" ) ;
if ( JavassistProxyFactory. LogHolder . log. isDebugEnabled ( ) ) {
JavassistProxyFactory. LogHolder . log. debug ( "writeReplace method was found on bean " + type + ", make sure it returns this" ) ;
}
} catch ( NoSuchMethodException var10) {
enhancer. setInterfaces ( new Class [ ] { WriteReplaceInterface . class } ) ;
} catch ( SecurityException var11) {
}
Class < ? > [ ] typesArray = ( Class [ ] ) constructorArgTypes. toArray ( new Class [ constructorArgTypes. size ( ) ] ) ;
Object [ ] valuesArray = constructorArgs. toArray ( new Object [ constructorArgs. size ( ) ] ) ;
Object enhanced;
try {
enhanced = enhancer. create ( typesArray, valuesArray) ;
} catch ( Exception var9) {
throw new ExecutorException ( "Error creating lazy proxy. Cause: " + var9, var9) ;
}
( ( Proxy ) enhanced) . setHandler ( callback) ;
return enhanced;
}
private static class EnhancedResultObjectProxyImpl implements MethodHandler {
private EnhancedResultObjectProxyImpl ( Class < ? > type, ResultLoaderMap lazyLoader, Configuration configuration, ObjectFactory objectFactory, List < Class < ? > > constructorArgTypes, List < Object > constructorArgs) {
this . type = type;
this . lazyLoader = lazyLoader;
this . aggressive = configuration. isAggressiveLazyLoading ( ) ;
this . lazyLoadTriggerMethods = configuration. getLazyLoadTriggerMethods ( ) ;
this . objectFactory = objectFactory;
this . constructorArgTypes = constructorArgTypes;
this . constructorArgs = constructorArgs;
}
public static Object createProxy ( Object target, ResultLoaderMap lazyLoader, Configuration configuration, ObjectFactory objectFactory, List < Class < ? > > constructorArgTypes, List < Object > constructorArgs) {
Class < ? > type = target. getClass ( ) ;
JavassistProxyFactory. EnhancedResultObjectProxyImpl callback = new JavassistProxyFactory. EnhancedResultObjectProxyImpl ( type, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs) ;
Object enhanced = JavassistProxyFactory . crateProxy ( type, callback, constructorArgTypes, constructorArgs) ;
PropertyCopier . copyBeanProperties ( type, target, enhanced) ;
return enhanced;
}
public Object invoke ( Object enhanced, Method method, Method methodProxy, Object [ ] args) throws Throwable {
String methodName = method. getName ( ) ;
try {
synchronized ( this . lazyLoader) {
if ( "writeReplace" . equals ( methodName) ) {
Object original;
if ( this . constructorArgTypes. isEmpty ( ) ) {
original = this . objectFactory. create ( this . type) ;
} else {
original = this . objectFactory. create ( this . type, this . constructorArgTypes, this . constructorArgs) ;
}
PropertyCopier . copyBeanProperties ( this . type, enhanced, original) ;
if ( this . lazyLoader. size ( ) > 0 ) {
return new JavassistSerialStateHolder ( original, this . lazyLoader. getProperties ( ) , this . objectFactory, this . constructorArgTypes, this . constructorArgs) ;
}
return original;
}
if ( this . lazyLoader. size ( ) > 0 && ! "finalize" . equals ( methodName) ) {
if ( ! this . aggressive && ! this . lazyLoadTriggerMethods. contains ( methodName) ) {
String property;
if ( PropertyNamer . isSetter ( methodName) ) {
property = PropertyNamer . methodToProperty ( methodName) ;
this . lazyLoader. remove ( property) ;
} else if ( PropertyNamer . isGetter ( methodName) ) {
property = PropertyNamer . methodToProperty ( methodName) ;
if ( this . lazyLoader. hasLoader ( property) ) {
this . lazyLoader. load ( property) ;
}
}
} else {
this . lazyLoader. loadAll ( ) ;
}
}
}
return methodProxy. invoke ( enhanced, args) ;
} catch ( Throwable var10) {
throw ExceptionUtil . unwrapThrowable ( var10) ;
}
}
}
}
上面就给出了大致的说明了,至此延迟加载大致说明完毕
设计模式:
我们可以知道大多数的设计模式,如有3类23种设计模式,但是我们通常都不会刻意的去记住,因为设计模式无非就是对某种操作的简称而已,一般情况下,我们会根据场景来进行操作,可能你的操作就是某种设计模式而不自知,Mybatis源码中使用了大量的设计模 式,观察设计模式在其中的应用,能够更深入的理解设计模式
Mybatis至少用到了以下的设计模式的使用:
这里要特别注意:设计模式有些没有使用到的,可以不用去记住,只要你认为你的操作比较好就行了
大多数情况下,我们在mybatis中会优先记住如下设计模式:
Builder构建者模式、工厂模式、代理模式,现在我们来进行解读,先介绍模式自身的知识,然后解读在 Mybatis中怎样应用了该模式
Builder构建者模式:
Builder模式的定义是"将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示",它属于创建类的模式(会创建类对象的模式都可以称为创建类模式,如工厂模式,还有构建者模式),一般来说,如果一个对象的构建⽐较复杂,超出了构造函数所能包含的范围,就可以使用工厂模式和Builder模式,相对于工厂模式会产出一个完整的产品,Builder应用于更加复杂的对象的构建,甚⾄只会构建产品的一个部分,直⽩来说,就是使用多个简单的对象一步一步构建 成一个复杂的对象
为了区分工厂模式与他的区别,现在我们来看看例子
例⼦:使用构建者设计模式来⽣产computer(电脑)
主要步骤:
1:将需要构建的目标类分成多个部件(电脑可以分为主机、显示器、键盘、鼠标等部件)
2:创建构建类
3:依次创建部件
4:将部件组装成目标对象
定义computer:
package test ;
public class Computer {
private String displayer;
private String mainUnit;
private String mouse;
private String keyboard;
public String getDisplayer ( ) {
return displayer;
}
public void setDisplayer ( String displayer) {
this . displayer = displayer;
}
public String getMainUnit ( ) {
return mainUnit;
}
public void setMainUnit ( String mainUnit) {
this . mainUnit = mainUnit;
}
public String getMouse ( ) {
return mouse;
}
public void setMouse ( String mouse) {
this . mouse = mouse;
}
public String getKeyboard ( ) {
return keyboard;
}
public void setKeyboard ( String keyboard) {
this . keyboard = keyboard;
}
@Override
public String toString ( ) {
return "computer{" +
"displayer='" + displayer + '\'' +
", mainUnit='" + mainUnit + '\'' +
", mouse='" + mouse + '\'' +
", keyboard='" + keyboard + '\'' +
'}' ;
}
}
ComputerBuilder(构建类):
package test ;
public class ComputerBuilder {
private Computer target = new Computer ( ) ;
public void installDisplayer ( String displayer) {
target. setDisplayer ( displayer) ;
}
public void installMainUnit ( String mainUnit) {
target. setMainUnit ( mainUnit) ;
}
public void installMouse ( String mouse) {
target. setMouse ( mouse) ;
}
public void installKeybord ( String keyboard) {
target. setKeyboard ( keyboard) ;
}
public Computer build ( ) {
return target;
}
}
调用:
package test ;
public class test {
public static void main ( String [ ] args) {
ComputerBuilder computerBuilder = new ComputerBuilder ( ) ;
computerBuilder. installDisplayer ( "显万器" ) ;
computerBuilder. installMainUnit ( "主机" ) ;
computerBuilder. installKeybord ( "键盘" ) ;
computerBuilder. installMouse ( "⿏标" ) ;
Computer computer = computerBuilder. build ( ) ;
System . out. println ( computer) ;
}
}
简单来说,构建者模式与工厂模式的区别就是,构建者模式相当于工厂模式准备版本,工厂模式不是准备,而是直接的返回,可以通过构造方法来进行设置,而构建者模式很明显是实现准备一个对象,然后进行设置,总体来说,构建者模式一开始是需要一个空间占用的,这是一个缺点,好处就是,我可以更好的扩展,而不用创建多个构造方法,且可以继续设置值,即修改,即扩展性大
所以,如果不需要考虑空间的话,尽量使用构建者模式,大多数我们都会使用构建者模式来代替工厂模式,当然了,如果没有什么赋值的操作,那么工厂模式就比较简单了,直接操作构造方法即可,但还是可以使用构建者模式,虽然执行代码会多一点(长远来说要少,因为构造方法的参数是一直多的,那么对于代码来说,参数少可以优先考虑工厂,参数多优先考虑构建者,对于空间来说,空间少,考虑工厂,空间多考虑构建者,由于我们尽量会考虑维护,所以我们以代码为标志,然后考虑空间)
注意:上面的优缺点是建立在单个对象的情况下,如果是多个对象,我建议直接使用工厂模式,因为对于构建者来说,总不能将所有的方法都放在一起吧,这是比较麻烦的事情,所以后面的工厂模式为了进行突出,所以特别操作两个类来表示示例
Mybatis中的体现:
SqlSessionFactory 的构建过程:
Mybatis的初始化工作⾮常复杂,有些情况参数非常多,不是只用一个构造函数就能搞定的,所以使用了建造者模式,使用了 大 量的Builder,进行分层构造,核心对象Configuration使用了 XmlConfigBuilder来进行构造
在Mybatis环境的初始化过程中,SqlSessionFactoryBuilder会调用XMLConfigBuilder读取所有的Mybatis的sqlMapConfig.xml 和所有的 Mapper.xml 文件,构建 Mybatis 运行的核心对象 Configuration 对 象,然后将该Configuration对象作为参数构建一个SqlSessionFactory对象,其中你可以知道这个:
private void parseConfiguration ( XNode root) {
try {
this . propertiesElement ( root. evalNode ( "properties" ) ) ;
Properties settings = this . settingsAsProperties ( root. evalNode ( "settings" ) ) ;
this . loadCustomVfs ( settings) ;
this . loadCustomLogImpl ( settings) ;
this . typeAliasesElement ( root. evalNode ( "typeAliases" ) ) ;
this . pluginElement ( root. evalNode ( "plugins" ) ) ;
this . objectFactoryElement ( root. evalNode ( "objectFactory" ) ) ;
this . objectWrapperFactoryElement ( root. evalNode ( "objectWrapperFactory" ) ) ;
this . reflectorFactoryElement ( root. evalNode ( "reflectorFactory" ) ) ;
this . settingsElement ( settings) ;
this . environmentsElement ( root. evalNode ( "environments" ) ) ;
this . databaseIdProviderElement ( root. evalNode ( "databaseIdProvider" ) ) ;
this . typeHandlerElement ( root. evalNode ( "typeHandlers" ) ) ;
this . mapperElement ( root. evalNode ( "mappers" ) ) ;
} catch ( Exception var3) {
throw new BuilderException ( "Error parsing SQL Mapper Configuration. Cause: " + var3, var3) ;
}
}
这里你应该熟悉吧,这就是之前源码的说明
很明显,他操作了大多数的构建者工作
其中 XMLConfigBuilder 在构建 Configuration 对象时,也会调用 XMLMapperBuilder 用于读取 Mapper 文件,而XMLMapperBuilder会使用XMLStatementBuilder来读取和build所有的SQL语句
public void parse ( ) {
if ( ! this . configuration. isResourceLoaded ( this . resource) ) {
this . configurationElement ( this . parser. evalNode ( "/mapper" ) ) ;
this . configuration. addLoadedResource ( this . resource) ;
this . bindMapperForNamespace ( ) ;
}
this . parsePendingResultMaps ( ) ;
this . parsePendingCacheRefs ( ) ;
this . parsePendingStatements ( ) ;
}
在这个过程中,有一个相似的特点,就是这些Builder会读取文件或者配置,然后做大量的XpathParser 解析、配置或语法的解析、反射⽣成对象、存入结果缓存等步骤,这么多的工作都不是一个构造函数所能包括的,因此大量采用了 Builder模式来解决
即分开完成,最终得到Configuration 对象,SqlSessionFactoryBuilder类根据不同的输入参数来构建SqlSessionFactory这个工厂对象,我们都已输入流为主的说明
工厂模式:
在Mybatis中⽐如SqlSessionFactory使用的是工厂模式,该工厂没有那么复杂的逻辑,是一个简单工厂 模式
简单工厂模式(Simple Factory Pattern):⼜称为静态工厂方法(Static Factory Method)模式,它属于创建型(类)模式
既然是静态工厂,那么自然他是一个静态方法,大多数静态的说明都是来操作静态static关键字的
在简单工厂模式中,可以根据参数的不同返回不同类的实例,简单工厂模式专⻔定义一个类来负责创建 其他类的实例,被创建的实例通常都具有共同的⽗类(前提是被创建的)
例⼦:⽣产电脑
假设有一个电脑的代工⽣产商,它目前已经可以代工⽣产联想电脑了,随着业务的拓展,这个代工⽣产 商还要⽣产惠普的电脑,我们就需要用一个单独的类来专⻔⽣产电脑,这就用到了简单工厂模式
创建抽象产品类:
我们创建一个电脑的抽象产品类,他有一个抽象方法用于启动电脑:
package test1 ;
public abstract class Computer {
public abstract void start ( ) ;
}
创建具体产品类:
接着我们创建各个品牌的电脑,他们都继承了他们的⽗类Computer,并实现了⽗类的start方法:
package test1 ;
public class LenovoComputer extends Computer {
@Override
public void start ( ) {
System . out. println ( "联想电脑启动" ) ;
}
}
package test1 ;
public class HpComputer extends Computer {
@Override
public void start ( ) {
System . out. println ( "惠普电脑启动" ) ;
}
}
创建工厂类:
接下来创建一个工厂类,它提供了一个静态方法createComputer用来⽣产电脑,你只需要传入你 想⽣ 产的电脑的品牌,它就会实例化相应品牌的电脑对象
package test1 ;
public class ComputerFactory {
public static Computer createComputer ( String type) {
Computer mComputer = null ;
switch ( type) {
case "lenovo" :
mComputer = new LenovoComputer ( ) ;
break ;
case "hp" :
mComputer = new HpComputer ( ) ;
break ;
}
return mComputer;
}
}
客户端调用工厂类:
package test1 ;
public class CreatComputer {
public static void main ( String [ ] args) {
Computer lenovo = ComputerFactory . createComputer ( "lenovo" ) ;
System . out. println ( lenovo) ;
Computer hp = ComputerFactory . createComputer ( "hp" ) ;
System . out. println ( hp) ;
}
}
Mybatis 体现:
Mybatis中执行Sql语句、获取Mappers、管理事务的核心接口SqlSession的创建过程使用到了工厂模 式
有一个 SqlSessionFactory 来负责 SqlSession 的创建,SqlSessionFactory 的openSession ()方法重载了很多个,分别⽀ 持autoCommit、Executor、Transaction等参数的输入,来构建核心的SqlSession对象
在DefaultSqlSessionFactory的默认工厂实现⾥,有一个方法可以看出工厂怎么产出一个产品:
private SqlSession openSessionFromDataSource ( ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null ;
DefaultSqlSession var8;
try {
Environment environment = this . configuration. getEnvironment ( ) ;
TransactionFactory transactionFactory = this . getTransactionFactoryFromEnvironment ( environment) ;
tx = transactionFactory. newTransaction ( environment. getDataSource ( ) , level, autoCommit) ;
Executor executor = this . configuration. newExecutor ( tx, execType) ;
var8 = new DefaultSqlSession ( this . configuration, executor, autoCommit) ;
} catch ( Exception var12) {
this . closeTransaction ( tx) ;
throw ExceptionFactory . wrapException ( "Error opening session. Cause: " + var12, var12) ;
} finally {
ErrorContext . instance ( ) . reset ( ) ;
}
return var8;
}
这是一个openSession调用的底层方法,该方法先从configuration读取对应的环境配置,然后初始化 TransactionFactory 获得一个 Transaction 对象,然后通过 Transaction 获取一个 Executor 对象,最后通过configuration、Executor、是否autoCommit三个参数直接构建了 SqlSession
代理模式:
代理模式(Proxy Pattern):给某一个对象提供一个代理,并由代理对象控制对原对象的引用,代理模式的英文叫做Proxy,它是一种对象结构型模式,代理模式分为静态代理和动态代理,我们来介绍动态代理
举例:
创建一个抽象类,Person接口,使其拥有一个没有返回值的doSomething方法
package test2 ;
public interface Person {
void doSomething ( ) ;
}
创建一个名为Bob的Person接口的实现类,使其实现doSomething方法
package test2 ;
public class Bob implements Person {
@Override
public void doSomething ( ) {
System . out. println ( "你好,我的贵人" ) ;
}
}
创建JDK动态代理类,使其实现InvocationHandler接口,拥有一个名为target的变量,并创建 getTarget获取代理对象方法
package test2 ;
import java. lang. reflect. InvocationHandler ;
import java. lang. reflect. Method ;
import java. lang. reflect. Proxy ;
public class JDKDynamicProxy implements InvocationHandler {
Person target;
public JDKDynamicProxy ( Person person) {
this . target = person;
}
public Person getTarget ( ) {
return ( Person ) Proxy . newProxyInstance ( target. getClass ( ) . getClassLoader ( ) , target. getClass ( ) . getInterfaces ( ) , this ) ;
}
@Override
public Object invoke ( Object proxy, Method method, Object [ ] args) throws Throwable {
System . out. println ( "你好吗" ) ;
Person result = ( Person ) method. invoke ( target, args) ;
System . out. println ( "我很好" ) ;
return result;
}
}
创建JDK动态代理测试类JDKDynamicTest:
package test2 ;
public class JDKDynamicTest {
public static void main ( String [ ] args) {
System . out. println ( "不使用代理类,调用doSomething方法" ) ;
Person person = new Bob ( ) ;
person. doSomething ( ) ;
System . out. println ( "分割线-----------" ) ;
System . out. println ( "使用代理类,调用doSomething方法" ) ;
Person proxyPerson = new JDKDynamicProxy ( new Bob ( ) ) . getTarget ( ) ;
proxyPerson. doSomething ( ) ;
}
}
Mybatis中实现:
代理模式可以认为是Mybatis的核心使用的模式,正是由于这个模式,我们只需要编写Mapper接 口,不需要实现,由Mybatis后台帮我们完成具体SQL的执行
当我们使用Configuration的getMapper方法时,会调用mapperRegistry.getMapper方法,而该方法⼜ 会调用 mapperProxyFactory.newInstance(sqlSession)来⽣成一个具体的代理
public class MapperProxyFactory < T > {
private final Class < T > mapperInterface;
private final Map < Method , MapperMethodInvoker > methodCache = new ConcurrentHashMap ( ) ;
public MapperProxyFactory ( Class < T > mapperInterface) {
this . mapperInterface = mapperInterface;
}
public Class < T > getMapperInterface ( ) {
return this . mapperInterface;
}
public Map < Method , MapperMethodInvoker > getMethodCache ( ) {
return this . methodCache;
}
protected T newInstance ( MapperProxy < T > mapperProxy) {
return Proxy . newProxyInstance ( this . mapperInterface. getClassLoader ( ) , new Class [ ] { this . mapperInterface} , mapperProxy) ;
}
public T newInstance ( SqlSession sqlSession) {
MapperProxy < T > mapperProxy = new MapperProxy ( sqlSession, this . mapperInterface, this . methodCache) ;
return this . newInstance ( mapperProxy) ;
}
}
在这⾥,先通过T newInstance(SqlSession sqlSession)方法会得到一个MapperProxy对象,然后调用T newInstance(MapperProxy mapperProxy)⽣成代理对象然后返回,而查看MapperProxy的代码,可以看到如下内容:
public class MapperProxy < T > implements InvocationHandler , Serializable {
public Object invoke ( Object proxy, Method method, Object [ ] args) throws Throwable {
try {
return Object . class . equals ( method. getDeclaringClass ( ) ) ? method. invoke ( this , args) : this . cachedInvoker ( method) . invoke ( proxy, method, args, this . sqlSession) ;
} catch ( Throwable var5) {
throw ExceptionUtil . unwrapThrowable ( var5) ;
}
}
}
⾮常典型的jdk代理模式,该MapperProxy类实现了InvocationHandler接口,并且实现了该接口的invoke方法,通 过这种方式,我们只需要编写Mapper接口类,当真正执行一个Mapper接口的时候,就会转发给 MapperProxy.invoke方法,而该方法则会调用后续的一系列方法,完成 SQL 的执行和返回,在前面也说明了这些代码,具体里面的实现可以选择继续看看
至此,我们的mybatis的底层原理大致都说明完毕,但是注解的并没有说明,实际上只是读取注解信息进行相同的赋值而已,所以就不说明了,具体去百度查看吧
当然只是具体说明mybatis的实现,因为mybatis可不是一个人就能完成的,自然不可能全部说明,因为那样太浪费时间了