MyBatis源码分析(1)

编程入门 行业动态 更新时间:2024-10-26 08:25:49

MyBatis<a href=https://www.elefans.com/category/jswz/34/1770099.html style=源码分析(1)"/>

MyBatis源码分析(1)

1.手写持久层框架-IMybatis

1.1 JDBC操作数据库_问题分析

JDBC API 允许应用程序访问任何形式的表格数据,特别是存储在关系数据库中的数据

代码示例:

public static void main(String[] args) { 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", "root");// 定义sql语句?表示占位符String sql = "select * from user where username = ?";// 获取预处理statementpreparedStatement = connection.prepareStatement(sql);// 设置参数,第一个参数为sql语句中参数的序号(从1开始),第二个参数为设置的参数值 preparedStatement.setString(1, "tom");// 向数据库发出sql执行查询,查询出结果集resultSet = preparedStatement.executeQuery();// 遍历查询结果集while (resultSet.next()) {int id = resultSet.getInt("id");String username = resultSet.getString("username");// 封装Useruser.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();}}}
}

1.2 JDBC问题分析&解决思路

剖开代码,逐个分析:

(1)加载驱动,获取链接:

硬编码:原生的JDBC在执行SQL操作时,常常需要编写SQL语句以及与数据库连接相关的配置信息,这些配置信息通常以硬编码的方式直接写在代码中。这种做法被认为是硬编码,因为配置信息与代码紧密耦合在一起,无法在运行时进行动态修改。

  • 存在问题1:数据库配置信息存在硬编码问题

    优化思路:使用配置文件!

  • 存在问题2:频繁创建、释放数据库连接问题。

    优化思路:使用数据连接池

(2)定义sql、设置参数、执行查询:

  • 存在问题3:SQL语句、设置参数、获取结果集参数均存在硬编码问题 。

    优化思路:使用配置文件!

(3)遍历查询结果集:

  • 存在问题4:手动封装返回结果集,较为繁琐

    优化思路:使用Java反射、内省!

针对JDBC各个环节中存在的不足,现在,我们整理出对应的优化思路,

统一汇总:
存在问题优化思路
数据库配置信息存在硬编码问题使用配置文件
频繁创建、释放数据库连接问题使用数据连接池
SQL语句、设置参数、获取结果集参数均存在硬编码问题使用配置文件
手动封装返回结果集,较为繁琐使用Java反射、内省

1.3 自定义持久层框架_思路分析

JDBC是个人作战,凡事都是亲力亲为,低效而高险,自己加载驱动,自己建连接,自己手动创建和释放数据库资源,处理事务和异常等,是一种相对原始且底层的数据库访问方式。虽然JDBC提供了直接与数据库交互的API,但它需要开发者编写大量冗余的代码来处理数据库连接、结果集的封装等,容易出现重复性代码和错误,使得开发变得繁琐且容易出错。

持久层框架好比是多工种协作,分工明确,执行高效,有专门负责解析注册驱动建立连接的,有专门管理数据连接池的,有专门执行sql语句的,有专门做预处理参数的,有专门装配结果集的 …

优化思路: 框架的作用,就是为了帮助我们减去繁重开发细节与冗余代码,使我们能更加专注于业务应用开发。

使用JDBC和使用持久层框架区别:

通过上图可以发现,拥有一套持久层框架是如此的舒适,我们仅仅需要干两件事:

  • 配置数据源(地址/数据名/用户名/密码)
  • 编写SQL与参数准备(SQL语句/参数类型/返回值类型)

框架,除了思考本身的工程设计,还需要考虑到实际项目端的使用场景,干系方涉及两端:

  • 使用端(实际项目)
  • 持久层框架本身

以上两步,我们通过一张架构图《 手写持久层框架基本思路 》来梳理清楚:

核心接口/类重点说明:
分工协作角色定位类名定义
负责读取配置文件资源辅助类Resources
负责存储数据库连接信息数据库资源类Configuration
负责存储SQL映射定义、存储结果集映射定义SQL与结果集资源类MappedStatement
负责解析配置文件,创建会话工厂SqlSessionFactory会话工厂构建者SqlSessionFactoryBuilder
负责创建会话SqlSession会话工厂SqlSessionFactory
指派执行器Executor会话SqlSession
负责执行SQL (配合指定资源Mapped Statement)执行器Executor

正常来说项目只对应一套数据库环境,一般对应一个SqlSessionFactory实例对象,我们使用单例模式只创建一个SqlSessionFactory实例。

如果需要配置多套数据库环境,那需要做一些拓展,例如Mybatis中通过environments等配置就可以支持多套测试/生产数据库环境进行切换。

项目使用端:

(1)调用框架API,除了引入自定义持久层框架的jar包

(2)提供两部分配置信息:1.sqlMapConfig.xml : 数据库配置信息(地址/数据名/用户名/密码),以及mapper.xml的全路径

​ 2.mapper.xml : SQL配置信息,存放SQL语句、参数类型、返回值类型相关信息

自定义框架本身:

1、加载配置文件:根据配置文件的路径,加载配置文件成字节输入流,存储在内存中。

创建Resource类,提供加载流方法:InputStream resourceAsStream(String path)

2、 创建两个javaBean(容器对象):存放配置文件解析出来的内容

Configuration核心配置类: 解析存储sqlMapConfig.xml里面的内容
MappedStatement 配置映射类:存储mapper.xml解析出来的内容

3、解析配置文件(使用dom4j) ,并创建SqlSession会话对象

创建类:SqlSessionFactoryBuilder方法:build(Inputstream in)
>使用dom4j解析配置文件,将解析出来的内容封装到容器对象中
>创建SqlSessionFactory对象,生产sqlSession会话对象(工厂模式)

4、创建SqlSessionFactory接口以及实现类DefaultSqlSessionFactory

创建openSession()方法,生产sqlSession

5、创建SqlSession接口以及实现类DefaultSqlSession

定义对数据库的CRUD操作:
> selectList();
> selectOne();
> update();
> delete();

6、创建Executor接口以及实现类SimpleExecutor

创建query(Configuration configuration, MappedStatement mappedStatement, Object param)
实际执行的就是JDBCD代码。

基本过程我们已经清晰,我们再细化一下类图,更好的助于我们实际编码:

最终手写的持久层框架结构参考:

1.4 自定义持久层框架_编码

    <properties><!-- Encoding --><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><mavenpiler.encoding>UTF-8</mavenpiler.encoding><java.version>8</java.version><mavenpiler.source>8</mavenpiler.source><mavenpiler.target>8</mavenpiler.target></properties>
使用端

在使用端项目中创建配置配置文件

创建 sqlMapConfig.xml
<configuration><!--1.数据库配置信息(数据源相关信息)--><dataSource><property name="driverClassName" value="com.mysql.jdbc.Driver"></property><property name="url" value="jdbc:mysql://localhost:3306/mybatis?useSSL=false&amp;characterEncoding=UTF-8&amp;serverTimezone=UTC"></property><property name="username" value="root"></property><property name="password" value="root"></property></dataSource><!--2.引入映射配置文件--><mappers><mapper resource="mapper/UserMapper.xml"></mapper><mapper resource="mapper/ProductMapper.xml"></mapper></mappers></configuration>
编写mapper.xml

(此时namespace还不是全类名的形式,如:“com.xc.mapper.UserMapper”)

<mapper namespace="User"><!--根据条件查询单个--><select id="selectOne" resultType="com.xc.pojo.User" parameterType="com.xc.pojo.User">select * from user where id= #{id} and username= #{username}</select><!--查询所有--><select id="selectList" resultType="com.xc.pojo.User">select * from user</select>
</mapper>
User实体
public class User {//主键标识private Integer id;//用户名private 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;}@Overridepublic String toString() {return "User{" +"id=" + id +", username='" + username + '\'' + '}';}
}
自定义框架本身
pom文件引入依赖

新建一个Maven工程在pom文件引入需要用到的依赖

    <properties><!-- Encoding --><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><mavenpiler.encoding>UTF-8</mavenpiler.encoding><java.version>8</java.version><mavenpiler.source>8</mavenpiler.source><mavenpiler.target>8</mavenpiler.target></properties><dependencies><!-- mysql 依赖--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.6</version></dependency><!--junit 依赖--><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.12</version><!--作用域测试范围--><scope>test</scope></dependency><!--dom4j 依赖--><dependency><groupId>dom4j</groupId><artifactId>dom4j</artifactId><version>1.6.1</version></dependency><!--xpath 依赖--><dependency><groupId>jaxen</groupId><artifactId>jaxen</artifactId><version>1.1.6</version></dependency><!--druid连接池--><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.1.21</version></dependency><!-- log日志 --><dependency><groupId>log4j</groupId><artifactId>log4j</artifactId><version>1.2.17</version></dependency></dependencies>

Resources

Configuration

/*** @author xingchen* @description 核心配置类: 解析存储sqlMapConfig.xml里面的内容*/
public class Configuration {// 数据源对象private DataSource dataSource;// key: statementId  value :封装好的MappedStatement对象private Map<String, MappedStatement> mappedStatementMap = new HashMap<>();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;}
}

MappedStatement

/*** @author xingchen* @description 配置映射类:存储mapper.xml解析出来的内容* mapper.xml中 <select | update | delete | insert> 封装成一个mappedStatement对象* <select id="selectOne" resultType="com.xc.pojo.user" parameterType="com.xc.pojo.user">* select * from user where id= #{id} and username= #{userName}* </select>*/
public class MappedStatement {// 1.唯一标识 (statementId 由namespace.id组成)private String statementId;// 2.返回结果类型private String resultType;// 3.参数类型private String parameterType;// 4.要执行的sql语句private String sql;// 5.mapper代理:sqlCommandType 标签类型,如selectprivate String sqlCommandType;public String getStatementId() {return statementId;}public void setStatementId(String statementId) {this.statementId = statementId;}public String getResultType() {return resultType;}public void setResultType(String resultType) {this.resultType = resultType;}public String getParameterType() {return parameterType;}public void setParameterType(String parameterType) {this.parameterType = parameterType;}public String getSql() {return sql;}public void setSql(String sql) {this.sql = sql;}public String getSqlCommandType() {return sqlCommandType;}public void setSqlCommandType(String sqlCommandType) {this.sqlCommandType = sqlCommandType;}
}

SqlSessionFactoryBuilder

XMLMapperBuilder

/*** @author xingchen* @description*/
public class XMLMapperBuilder {private Configuration configuration;public XMLMapperBuilder(Configuration configuration) {this.configuration = configuration;}public void parse(InputStream inputStream) throws DocumentException {Document document = new SAXReader().read(inputStream);Element rootElement = document.getRootElement();List<Element> selectList = rootElement.selectNodes("//select");/***     <select id="selectOne" resultType="com.xc.pojo.user" parameterType="com.xc.pojo.user">*         select * from user where id= #{id} and username= #{userName}*     </select>*/String namespace = rootElement.attributeValue("namespace");for (Element element : selectList) { //id的值//解析标签,封装mappedStatementString id = element.attributeValue("id");//返回结果classString resultType = element.attributeValue("resultType");//输入参数classString parameterType = element.attributeValue("parameterType");//sql语句String sql = element.getTextTrim();//statementIdString statementId = namespace + "." + id;//封装 mappedStatementMappedStatement mappedStatement = new MappedStatement();mappedStatement.setStatementId(statementId);mappedStatement.setResultType(resultType);mappedStatement.setParameterType(parameterType);mappedStatement.setSql(sql);mappedStatement.setSqlCommandType("select");//存到Configuration里面的map集合configuration.getMappedStatementMap().put(statementId, mappedStatement);}}
}

sqlSessionFactory 接口及DefaultSqlSessionFactory 实现类

/*** @author xingchen* @description*/
public interface SqlSessionFactory {/*** 生产sqlSession :封装着与数据库交互的方法*/SqlSession openSession();
}
/*** @author xingchen* @description*/
public class DefaultSqlSessionFactory implements SqlSessionFactory {private Configuration configuration;public DefaultSqlSessionFactory(Configuration configuration) {this.configuration = configuration;}/*** (1)创建执行器对象 (2)创建SqlSession对象** @return*/@Overridepublic SqlSession openSession() {// 执行器创建出来Executor executor = new SimpleExecutor();return new DefaultSqlSession(configuration, executor);}
}

sqlSession 接口及 DefaultSqlSession 实现类(初步实现)

/*** @author xingchen* @description*/
public interface SqlSession {/*** 查询所有的方法 select * from user where username like '%aaa%' and sex = ''* 参数1:唯一标识* 参数2:入参*/public <E> List<E> selectList(String statementId,Object parameter) throws Exception;/*** 查询单个的方法*/public <T> T selectOne(String statementId,Object parameter) throws Exception;
}
/*** @author xingchen* @description*/
public class DefaultSqlSession implements SqlSession {private Configuration configuration;private Executor executor;public DefaultSqlSession(Configuration configuration, Executor executor) {this.configuration = configuration;this.executor = executor;}@Override                    // user.selectList      1 tom userpublic <E> List<E> selectList(String statementId, Object params) throws Exception {MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);// 将查询操作委派给底层的执行器List<E> list = executor.query(configuration,mappedStatement,params);return list;}@Overridepublic <T> T selectOne(String statementId, Object params) throws Exception {List<Object> list = this.selectList(statementId, params);if(list.size() == 1){return (T) list.get(0);}else if(list.size() > 1){throw new RuntimeException("返回结果过多");}else {return null;}}
} 

Executor

/*** @author xingchen* @description*/
public interface Executor {<E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object param) throws Exception;
}

SimpleExecutor

/*** @author xingchen* @description*/
public class SimpleExecutor implements Executor {/*** 真正执行底层的JDBC操作** @param configuration* @param mappedStatement* @param param* @param <E>* @return*/@Overridepublic <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object param) throws Exception {//1.获取数据库链接Connection connection = configuration.getDataSource().getConnection();//2.获取预编译对象 prepareStatement//select * from user where id= #{id} and username= #{userName}//select * from user where id= ? and username= ?//获取出来的sql还需要处理 转换占位符: (1) #{}转换成? (2) #{}里面的值要保存下来String sql = mappedStatement.getSql();BoundSql boundSql = getBoundSql(sql);String finalSql = boundSql.getFinalSql();PreparedStatement preparedStatement = connection.prepareStatement(finalSql);//3.设置参数//问题1:param类型是不确定? map|user|String|productString parameterType = mappedStatement.getParameterType();if (parameterType != null) {//userClass<?> parameterTypeClass = Class.forName(parameterType);//问题2:该把user对象里面哪个属性赋值给哪个占位符呢List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();for (int i = 0; i < parameterMappings.size(); i++) {ParameterMapping parameterMapping = parameterMappings.get(i);// #{} id or usernameString fileName = parameterMapping.getContent();//通过字段名获取属性对象Field declaredField = parameterTypeClass.getDeclaredField(fileName);// 暴力访问declaredField.setAccessible(true);//获取到参数的值Object fileValue = declaredField.get(param);preparedStatement.setObject(i + 1, fileValue);}}//4.执行sqlResultSet resultSet = preparedStatement.executeQuery();//5.封装返回结果集String resultType = mappedStatement.getResultType();Class<?> resultTypeClass = Class.forName(resultType);ResultSetMetaData metaData = resultSet.getMetaData();List<E> list = new ArrayList<>();while (resultSet.next()) {Object o = resultTypeClass.getConstructor().newInstance();//比如说此处查询有id username字段// 整个for循环的遍历针对表中的属性依次处理 id对象封装到user对象的id属性值,username封装到user对象的username属性值for (int i = 1; i <= metaData.getColumnCount(); i++) {String columnName = metaData.getColumnName(i);Object columnValue = resultSet.getObject(columnName);//根据映射关系进行封装Field declaredField = resultTypeClass.getDeclaredField(columnName);declaredField.setAccessible(true);declaredField.set(o, columnValue);}list.add((E) o);}return list;}/*** (1) #{}转换成? (2) #{}里面的值要保存下来** @param sql* @return*/private BoundSql getBoundSql(String sql) {//标记处理器: 配合标记解析器完成占位符的解析ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();//标记解析器GenericTokenParser genericTokenParser = new GenericTokenParser("#{", "}", parameterMappingTokenHandler);//替换成了?的sqlString finalSql = genericTokenParser.parse(sql);//#{}里面的值集合封装List<ParameterMapping> parameterMappings = parameterMappingTokenHandler.getParameterMappings();return new BoundSql(finalSql, parameterMappings);}
}

BoundSql

/*** @author xingchen* @description*/
public class BoundSql {//解析过后的sql语句private String finalSql;//解析出来的参数private List<ParameterMapping> parameterMappings;public BoundSql(String finalSql, List<ParameterMapping> parameterMappings) {this.finalSql = finalSql;this.parameterMappings = parameterMappings;}public String getFinalSql() {return finalSql;}public void setFinalSql(String finalSql) {this.finalSql = finalSql;}public List<ParameterMapping> getParameterMappings() {return parameterMappings;}public void setParameterMappings(List<ParameterMapping> parameterMappings) {this.parameterMappings = parameterMappings;}
}

BoundSql

/*** @author xingchen* @description*/
public class BoundSql {//解析过后的sql语句private String finalSql;//解析出来的参数private List<ParameterMapping> parameterMappings;public BoundSql(String finalSql, List<ParameterMapping> parameterMappings) {this.finalSql = finalSql;this.parameterMappings = parameterMappings;}public String getFinalSql() {return finalSql;}public void setFinalSql(String finalSql) {this.finalSql = finalSql;}public List<ParameterMapping> getParameterMappings() {return parameterMappings;}public void setParameterMappings(List<ParameterMapping> parameterMappings) {this.parameterMappings = parameterMappings;}
}

需要用到的工具类

GenericTokenParser

用于解析 ${}#{} 这样的占位符。在 MyBatis 的 SQL 语句中,我们可以使用 ${} 来直接替换变量的值,而 #{} 则会被解析为预编译的占位符。这个类通过传入的 openTokencloseToken 参数来标识占位符的开始标记和结束标记,然后通过传入的 TokenHandler 参数来处理解析后的占位符的值。GenericTokenParserparse 方法会遍历 SQL 语句中的占位符,将占位符解析为实际的参数,并通过 TokenHandler 处理后返回最终的 SQL 语句。

/*** Copyright 2009-2017 the original author or authors.* <p>* Licensed under the Apache License, Version 2.0 (the "License");* you may not use this file except in compliance with the License.* You may obtain a copy of the License at* <p>* .0* <p>* Unless required by applicable law or agreed to in writing, software* distributed under the License is distributed on an "AS IS" BASIS,* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.* See the License for the specific language governing permissions and* limitations under the License.*/
package com.xc.utils;/*** @author Clinton Begin*/
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;}/*** 解析${}和#{}* @param text* @return* 该方法主要实现了配置文件、脚本等片段中占位符的解析、处理工作,并返回最终需要的数据。* 其中,解析工作由该方法完成,处理工作是由处理器handler的handleToken()方法来实现*/public String parse(String text) {// 验证参数问题,如果是null,就返回空字符串。if (text == null || text.isEmpty()) {return "";}// 下面继续验证是否包含开始标签,如果不包含,默认不是占位符,直接原样返回即可,否则继续执行。int start = text.indexOf(openToken, 0);if (start == -1) {return text;}// 把text转成字符数组src,并且定义默认偏移量offset=0、存储最终需要返回字符串的变量builder,// text变量中占位符对应的变量名expression。判断start是否大于-1(即text中是否存在openToken),如果存在就执行下面代码char[] src = text.toCharArray();int offset = 0;final StringBuilder builder = new StringBuilder();StringBuilder expression = null;while (start > -1) {// 判断如果开始标记前如果有转义字符,就不作为openToken进行处理,否则继续处理if (start > 0 && src[start - 1] == '\\') {builder.append(src, offset, start - offset - 1).append(openToken);offset = start + openToken.length();} else {//重置expression变量,避免空指针或者老数据干扰。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] == '\\') {//如果结束标记前面有转义字符时// this close token is escaped. remove the backslash and continue.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) {// close token was not found.builder.append(src, start, src.length - start);offset = src.length;} else {//首先根据参数的key(即expression)进行参数处理,返回?作为占位符builder.append(handler.handleToken(expression.toString()));offset = end + closeToken.length();}}start = text.indexOf(openToken, offset);}if (offset < src.length) {builder.append(src, offset, src.length - offset);}return builder.toString();}
}

ParameterMapping

用于存储 SQL 语句中的参数映射信息。在 MyBatis 解析占位符时,会将占位符的内容作为 ParameterMapping 的实例进行存储,以便后续处理。

package com.xc.utils;public class ParameterMapping {// id || usernameprivate String content;public ParameterMapping(String content) {this.content = content;}public String getContent() {return content;}public void setContent(String content) {this.content = content;}
}

ParameterMappingTokenHandler

实现了 TokenHandler 接口,主要用于处理解析后的占位符。在 handleToken 方法中,它会将解析后的占位符内容构建成 ParameterMapping 实例,并添加到 parameterMappings 列表中。这样,parameterMappings 列表中就存储了 SQL 语句中的所有参数映射信息。

package com.xc.utils;import java.util.ArrayList;
import java.util.List;public class ParameterMappingTokenHandler implements TokenHandler {private List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();// context是参数名称 #{id} #{username}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;}}

TokenHandler

这个接口定义了一个 handleToken 方法,用于处理解析后的占位符。具体的处理逻辑由实现 TokenHandler 接口的类来实现。

/***    Copyright 2009-2015 the original author or authors.**    Licensed under the Apache License, Version 2.0 (the "License");*    you may not use this file except in compliance with the License.*    You may obtain a copy of the License at**       .0**    Unless required by applicable law or agreed to in writing, software*    distributed under the License is distributed on an "AS IS" BASIS,*    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.*    See the License for the specific language governing permissions and*    limitations under the License.*/
package com.xc.utils;/*** @author Clinton Begin*/
public interface TokenHandler {String handleToken(String content);
}

综合起来,这些工具类在 MyBatis 中的配合使用,可以将 SQL 语句中的占位符解析为真正的参数,并存储参数映射信息,方便后续的数据库操作。通过这种方式,MyBatis 可以实现动态 SQL 的功能,使得 SQL 语句更加灵活和易于维护。

1.5 自定义持久层框架_优化

通过上述的自定义框架,已经解决了JDBC操作数据库带来的一些问题:例如频繁创建释放数据库连 接,硬编码,手动封装返回结果集等问题,但是现在继续来分析刚刚完成的自定义框架代码,还有一些问题

问题如下:

  • dao的实现类中存在重复的代码,整个操作的过程模板重复(创建sqlsession,调用sqlsession方法,关闭 sqlsession)

  • dao的实现类中存在硬编码,调用sqlsession的方法时,参数statement的id硬编码,如下所示

        @Testpublic void test1() throws Exception {//根据配置文件,加载成字节输入流InputStream inputStream = Resources.resourceAsStream("sqlMapConfig.xml");SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);SqlSession sqlSession = sqlSessionFactory.openSession();User user1 = new User();user1.setUsername("tom");user1.setId(1);List<User> list1 = sqlSession.selectList("user.selectList");List<User> list2 = sqlSession.selectList("user.selectOne", user1);System.out.println(list1);System.out.println(list2);}
    

解决:使用代理模式来创建接口的代理对象

    @Testpublic void test2() throws Exception {//根据配置文件,加载成字节输入流InputStream inputStream = Resources.resourceAsStream("sqlMapConfig.xml");SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);SqlSession sqlSession = sqlSessionFactory.openSession();User user1 = new User();user1.setUsername("tom");user1.setId(1);// proxy 代理对象 jdk动态代理生成的!//代理对象调用接口中的任意方法都会被拦截执行底层的invoke()UserMapper mapper = sqlSession.getMapper(UserMapper.class);List<User> allUser = mapper.findAllUser();User userObj = mapper.findUserByCondition(user1);System.out.println(userObj);System.out.println(allUser);}

用户端UserMapper.java更改

public interface UserMapper {/*** 查询所有*/List<User> findAllUser();/*** 查询单个*/User findUserByCondition(User user);
}

UserMapper.xml

<mapper namespace="com.xc.mapper.UserMapper"><!--sql语句的唯一标识:namespace值.id值 : statementId  可以定位到具体的sql语句resultType封装结果集类型约定:mapper代理开发:namespace要和类全路径相同id 要和方法吗相同--><!--查询所有--><select id="findAllUser" resultType="com.xc.pojo.User">select * from user</select><!--User user = new User();user.setId(1);user.setUserName("tom");--><!--根据条件查询单个--><select id="findUserByCondition" resultType="com.xc.pojo.User" parameterType="com.xc.pojo.User">select * from user where id= #{id} and username= #{username}</select></mapper>

在sqlSession中添加getMappper()方法

public interface SqlSession {/*** 查询所有的方法 select * from user where username like '%aaa%' and sex = ''* 参数1:唯一标识* 参数2:入参*/<E> List<E> selectList(String statementId) throws Exception;<E> List<E> selectList(String statementId, Object param) throws Exception;/*** 根据条件查询*/<T> T selectOne(String statementId, Object param) throws Exception;/*** mapper代理方式*/<T> T getMapper(Class<?> mapperClass);
}

DefaultSqlSession实现类

/*** @author xingchen* @description*/
public class DefaultSqlSession implements SqlSession {private Configuration configuration;private Executor executor;public DefaultSqlSession(Configuration configuration, Executor executor) {this.configuration = configuration;this.executor = executor;}@Overridepublic <E> List<E> selectList(String statementId) throws Exception {return selectList(statementId, null);}@Overridepublic <E> List<E> selectList(String statementId, Object param) throws Exception {MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);List<E> list = executor.query(configuration, mappedStatement, param);return list;}@Overridepublic <T> T selectOne(String statementId, Object param) throws Exception {List<Object> list = this.selectList(statementId, param);if (list.size() == 1) {return (T) list.get(0);} else if (list.size() > 1) {throw new RuntimeException("返回结果过多");}return null;}/*** mapperClass接口,使用动态代理生成代理对象 jdk动态代理 cglib动态代理** @param mapperClass* @param <T>* @return*/@Overridepublic <T> T getMapper(Class<?> mapperClass) {Object proxyInstance = Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{mapperClass},new InvocationHandler() {/*** proxy:代理对象 method:当前被调用的方法对象 例:findAllUser args : 查询参数* @param proxy* @param method* @param args* @return* @throws Throwable*/@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// 查询数据库 增删改查 执行JDBC//调用像selectOne或者selectList//问题1:能不能获取到statementId// 当前 statementId = 类名.方法名//获取当前对象的类名 全路径,和UserMapper.xml中的namespace对应了String className = method.getDeclaringClass().getName();//获取正在调用的方法名String methodName = method.getName();String statementId = className + "." + methodName;MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);String sqlCommandType = mappedStatement.getSqlCommandType();//问题2:怎么知道调用增删改查的哪个方法?//解决:根据sqlCommandType判断switch (sqlCommandType) {case "select"://问题3:现在是调用selectOne还是SelectList//解决:根据返回值类型Class<?> returnType = method.getReturnType();boolean assignableFrom = Collection.class.isAssignableFrom(returnType);if (assignableFrom) {//查询所有if (args != null) {//判断是否有查询参数return selectList(statementId, args[0]);} else {return selectList(statementId);}}return selectOne(statementId, args[0]);case "update"://TODO 更新操作break;case "delete"://TODO 更新操作break;case "insert"://TODO 更新操作break;}return null;}});return (T) proxyInstance;}
}

1.6 编写测试代码

1.6.1执行mapper.findAllUser()方法

1.6.2执行mapper.findUserByCondition()方法

1.6.3执行结果

2. MyBatis架构原理&主要组件

2.1 MyBatis的架构设计

mybatis架构四层作用是什么呢?

  • Api接口层:提供API 增加、删除、修改、查询等接口,通过API接口对数据库进行操作。
  • 数据处理层:主要负责SQL的 查询、解析、执行以及结果映射的处理,主要作用解析sql根据调用请求完成一次数据库操作.
  • 框架支撑层:负责通用基础服务支撑,包含事务管理、连接池管理、缓存管理等共用组件的封装,为上层提供基础服务支撑.
  • 引导层:引导层是配置和启动MyBatis 配置信息的方式

2.2 MyBatis主要组件及其相互关系

组件关系如下图所示:

组件介绍:

  • SqlSession:是Mybatis对外暴露的核心API,提供了对数据库的DRUD操作接口。

  • Executor:执行器,由SqlSession调用,负责数据库操作以及Mybatis两级缓存的维护

  • StatementHandler:封装了JDBC Statement操作,负责对Statement的操作,例

  • PrepareStatement参数的设置以及结果集的处理。

  • ResultSetHandler:是StatementHandler内部一个组件,主要负责对ResultSet结果集的处理,封装成目标对象返回

  • TypeHandler:用于Java类型与JDBC类型之间的数据转换,ParameterHandler和ResultSetHandler会分别使用到它的类型转换功能

  • MappedStatement:是对Mapper配置文件或Mapper接口方法上通过注解申明SQL的封装

  • Configuration:Mybatis所有配置都统一由Configuration进行管理,内部由具体对象分别管理各自的小功能模块

更多推荐

MyBatis源码分析(1)

本文发布于:2024-03-08 14:56:46,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1721195.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:源码   MyBatis

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!