MyBatis 是一款优秀的持久层框架,是一款半 ORM(对象关系映射)框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生类型、接口和 Java 的 POJO(Plain Old Java Objects)为数据库中的记录。
基本思路:
1.自定义注解
2.实现对Mapper接口的代理,生成MapperProxy代理对象
3.利用反射绑定参数
4.执行SQL语句
本文重点
1.模拟实现mybatis框架基于注解式的开发**
2.梳理mybatis运行时执行流程
3.巩固反射,JDBC的基本细节
在基于传统的JDBC开发过程中我们会频繁创建数据库连接对象、释放,容易造成系统资源浪费,影响系统性能。
这个问题我们可以引入连接池来解决,但是使用JDBC需要自己实现连接池。Sql语句定义、参数设置、结果集处理存在硬编码。同时在实际项目中Sql语句变化的可能性较大,一旦发生变化,需要修改java代码,系统需要重新编译,重新发布,不好维护。
为了进一步提升性能我们用PrepareStatement对象进行Sql语句的预编译处理,其可以提升Sql语句的执行效率、避免Sql注入的攻击等一系列优势。然而,使用PrepareStatement占有位符号传参数已然存在硬编码的问题,虽然使但是当面对多条件查询时PreparedStatement语句也显得相形见绌。因为Sql语句的where条件拼接的个数不一定确定,可能多也可能少,此时预留占位符就成了一个新的问题。此时修改Sql还要修改代码,系统不易维护。
执行Sql语句后会获得到结果集ResultSet,原始的方法致使我们面临一个问题:结果集操作大同小异,唯一不同的就是每次拼接的对象实例不同。最初我们根据我们所想返回对象的不同来进行单独处理,面对小规模系统设计这完全没什么问题,但是当系统规模变得庞大,我们不断重复书写遍历ResulSet的逻辑就显得有些不理智了,相当于重复在书写相同的代码逻辑,而最终结果仅是返回所需实体类。
面临上述种种的问题,Mybatis的诞生皆在解决上述问题,Mybatis核心功能的实现基本就是:**反射、JDBC操作。**这些都是java基础范畴内容,自己在闲暇之余也可以利用所学进行抽象、总结。
本文所说手写框架并不是去完整的构建出一个框架,而是学习框架的一种设计思路,设计思想。发现框架中的核心有价值内容。进行模仿、应用。从而提升能力。
本文皆在叙述如何手工实现Mybatis基于注解形式的开发, 模拟框架的总体执行流程,当然其中还有很多细节并不是短短数字就叙述清楚的。
基于注解式开发的好处在于避免了繁琐的XML文档信息的编写,将待执行的Sql语句写至注解之中,从而达到简化开发的目的。
目前所需注解:@Param,@Select,@Insert
@Param:主要作用在于绑定注解参数信息,在Mybatis框架中,参数信息的保存一般使用一个Map集合来保存对应信息,如果不指定@Param信息,将以参数所在位置作为key来作为传入参数的索引值项如果不使用@Param指定参数名则可通过 #{param1}获取对应参数的位置
@Select:主要作用在于定义查询的Sql语句信息
@Insert:主要作用在于定义插入的Sql语句信息
即 param(index)的形式获取信息 index:表示参数所在位置 //实例: // 接口层代码信息 User findById(Integer id); // mapper层的映射 <select id="findById" parameterType="Integer" resultMap="userMap"> select * from user where id = #{ param1} </select>
package mybatisAnnioation.annotation;
import java.lang.annotation.*;
/**参数注解信息
* @program: XMind
* @description
* @author: yihang
**/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@Documented
public @interface Param {
String value() default "";
}
package mybatisAnnioation.annotation;
import java.lang.annotation.*;
/**查询信息
* @program: XMind
* @description
* @author: yihang
**/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface Select {
String value() default "";
}
package mybatisAnnioation.annotation;
import java.lang.annotation.*;
/**
* @program: XMind
* @description
* @author: yihang
**/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface Insert {
// 插入数据注解
String value() default "";
}
在mybatis框架中,底层框架binding模块会将用户自定义的 Mapper 接口与映射配置文件关联起来,在本模拟过程中将该过程简化为注解同方法间的映射。以此完成Sql语句同方法的映射。
#数据库
create database mybatis;
#所需用户表
CREATE TABLE `stu` (
`id` int(11) DEFAULT NULL,
`username` varchar(50) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*Data for the table `stu` */
# 测试数据信息 可自行添加
insert into `stu`(`id`,`username`) values (1,'张三'),(2,'李四'),(NULL,'张三'),(NULL,'张三'),(NULL,'张三'),(NULL,'张三'),(NULL,'张三'),(NULL,'张三'),(NULL,'张三'),(NULL,'张三'),(23,'张三'),(23,'张三');
public class User {
private Integer id;
private String username;
// 出于精简略去get/set方法
}
package mybatisAnnioation.mapper;
import mybatisAnnioation.annotation.Insert;
import mybatisAnnioation.annotation.Param;
import mybatisAnnioation.annotation.Select;
import mybatisAnnioation.bean.User;
/**
* @program: XMind
* @description
* @author: yihang
**/
public interface UserMapper {
// 查询信息
@Select("select id,username from stu where id = #{id} and username = #{name}")
User selectUser(@Param("id")Integer id , @Param("name")String name);
// 插入数据信息
@Insert("insert into stu(id,username) values (#{id},#{username})")
int insertUser(@Param("id")Integer id , @Param(value="username")String username);
}
在为对应Mapper接口生成代理对象的MapperProxy的本质核心在于:java动态代理的应用,由于UserMapper属于接口,根据java语法规范接口中并允许有方法的具体实现,所以Mybatis利用动态代理的方式,对Mapper接口中的方法进行了增强。
此时要补充一点的就是:Mybatis在生成MapperProxy时所用到的代理形式,其实并不是一种“正统的动态代理”,一般动态,都会有一个具体的实际对象作为增强对象信息,但是由于Mapper属于接口层面,其并没有办法进行实例化,所以其动态代理的形式就退化成了如下形式。
以查询方法为例进行分析:
核心方法解析
作用:返回待查询对象信息 参数类型:(Method method ,Select query, Object [] args) getResult逻辑 1.获取sql语句信息 String sql = query.value(); 2.获取sql参数信息 此时sql 信息为主注解上的形如:"select id,username from stu where id = #{id} and username = #{name}" List<Object> paramNames = SQLUtils.getSelectParams(sql); 4.替换sql信息,将占位符#{ xxx} 替换为预编译SQL中的? // 形如select id,username from stu where id = ? and username = ? 5.获取方法实参信 形式为 key -value ,用@Param中指定的value值作为key ConcurrentHashMap<String, Object> paramMap = getMethodParam(method, args); 6.参数进行封装处理,将实参信息重新拼接值sql中 List<Object> paramValueList = new ArrayList<>(); 7.执行sql,返回结果集 8.利用反射,获取方法返回值类型信息,遍历结果集封装对象信息 9.返回信息
package mybatisAnnioation.handler;
import mybatisAnnioation.annotation.Insert;
import mybatisAnnioation.annotation.Param;
import mybatisAnnioation.annotation.Select;
import mybatisAnnioation.utils.JDBCUtils;
import mybatisAnnioation.utils.SQLUtils;
import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
/**
* @program: XMind
* @description
* @author: yihang
**/
public class UserMapperInvocationHandler implements InvocationHandler {
private Class userMapperClazz;
public UserMapperInvocationHandler(Class userMapperClazz) {
this.userMapperClazz = userMapperClazz;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws IllegalAccessException, SQLException, InstantiationException {
// 获取查询注解信息
Select query = method.getAnnotation(Select.class);
if(query != null) {
return getResult(method,query,args);
}
Insert insert = method.getAnnotation(Insert.class);
if ( insert != null) {
String insertSql = insert.value();
//插入参数
String[] insertParam = SQLUtils.getInsertParams(insertSql);
//参数绑定
ConcurrentHashMap<String, Object> paramMap = getMethodParam(method, args);
System.out.println("paramList"+ Arrays.toString(insertParam));
// 进行值的封装
List<Object> paramValueList = new ArrayList<>();
//将参数值装入list集合
for (String param : insertParam) {
Object paramValue = paramMap.get(param.trim());
paramValueList.add(paramValue);
}
insertSql = SQLUtils.replaceParam(insertSql, insertParam);
//更优化的处理在于再次设定一个类型转换
return JDBCUtils.insert(insertSql, false, paramValueList);
}
return null;
}
/**
* 返回结果集对象信息
* @param method
* @param query
* @param args
* @return
*/
private Object getResult(Method method ,Select query, Object [] args) throws IllegalAccessException, SQLException, InstantiationException {
// 获取sql语句信息
String sql = query.value();
// 获取sql参数信息
List<Object> paramNames = SQLUtils.getSelectParams(sql);
// 将sql语句进行替换,替换成预编译的sql
// 形如select id,username from stu where id = ? and username = ?
sql = SQLUtils.replaceParam(sql,paramNames);
//获取方法参数,绑定值。
ConcurrentHashMap<String, Object> paramMap = getMethodParam(method, args);
List<Object> paramValueList = new ArrayList<>();
//将参数值装入list集合
for (Object param : paramNames) {
Object paramValue = paramMap.get(param);
paramValueList.add(paramValue);
}
// 生成预编译sql为对应位置设定对应值信息
ResultSet rs = JDBCUtils.query(sql,paramValueList);
if( rs == null) {
System.out.println("userMapper null");
}
// 处理异常情况
if (!rs.next()) {
return null;
}
// 获取到方法的返回值类型信息
Class<?> returnTypeClazz = method.getReturnType();
// 实例化一个返回值的对象
Object obj = returnTypeClazz.newInstance();
// 光标往前移动一位
rs.previous();
while (rs.next()) {
Field[] fields = returnTypeClazz.getDeclaredFields();
for (Field field : fields) {
String fieldName = field.getName();
String fieldValue = rs.getString(fieldName);
Type type = field.getGenericType();
field.setAccessible(true);
// 简单的处理类型映射
if ( type.toString().equals("class java.lang.Integer")){
field.set(obj, Integer.valueOf(fieldValue));
}else{
field.set(obj, fieldValue);
}
}
}
return obj;
}
/***
* 获取方法上参数值的信息
* @param method
* @param args
* @return
*/
private ConcurrentHashMap getMethodParam(Method method, Object[] args) {
ConcurrentHashMap paramMap = new ConcurrentHashMap();
Parameter[] parameters = method.getParameters();
// 获取所有注解信息
Annotation [][] annotations = method.getParameterAnnotations();
for( int i = 0 ; i < parameters.length ; i ++ ) {
for ( Annotation annotation : annotations[i]){
if ( annotation instanceof Param) {
String key = ((Param)annotation).value();
paramMap.put(key,args[i]);
}
}
}
return paramMap;
}
}
SQLUTILS
package mybatisAnnioation.utils;
import org.apache.ibatis.jdbc.SQL;
import java.util.ArrayList;
import java.util.List;
/**
* @program: XMind
* @description
* @author: yihang
**/
public class SQLUtils {
// where 关键字 and关键字
private static final int IGNORE_WHERE = 5;
private static final int IGNORE_AND = 3;
private static final int IGNORE_VALUES = 6;
/***
* 替换参数信息
* #{param} ---> ?
*/
public static String replaceParam(String sql , List<Object> params){
for ( int i = 0 ; i < params.size() ; i ++ ) {
// 获取参数名
Object paramName = params.get(i);
// 替换信息
sql = sql.replace("#{" + paramName +"}","?" );
}
return sql;
}
/***
* 用于处理insert语句时所需
* @param sql
* @param parameterName
* @return
*/
public static String replaceParam(String sql, String[] parameterName) {
for (int i = 0; i < parameterName.length; i++) {
String string = parameterName[i].trim();
sql = sql.replace("#{" + string + "}", "?");
}
return sql;
}
/***
*
*获取sql的中的参数信息名
* sql形式:select id,username from stu where id = #{id} and username = #{name}
* @param sql
* @return
*/
public static List<Object> getSelectParams(String sql) {
// 存放信息
List<Object> paramList = new ArrayList<>();
//定位到where的位置信息
int startIndex = sql.indexOf("where");
// 跳过where关键字 截取到where关键字的内容信息
String whereAfter = sql.substring(startIndex + IGNORE_WHERE);
// 通过and分隔
String [] params = whereAfter.split("and");
for(String param : params) {
// id = #{id}
// 通过定位#{ 和}的位置来截取信息
int start = param.indexOf("{");
int end = param.indexOf("}");
String paramName = param.substring(start + 1 , end);
System.out.println(paramName.trim());
//加入到集合 利用trim避免多用空信息
paramList.add(paramName.trim());
}
return paramList;
}
/***
* 处理insert的sql语句信息
* @param insertSql
* @return
*/
public static String[] getInsertParams(String insertSql) {
int startIndex = insertSql.indexOf("values");
insertSql = insertSql.substring(startIndex + IGNORE_VALUES);
//(#{id},#{username})
// 转变为 id,username 随后根据进行分隔处理
insertSql = insertSql.replaceAll("#\\{","")
.replaceAll("\\}","")
.replaceAll("\\(","").replaceAll("\\)","");
// 注意此处,截取后的信息会多个接连空格信息!!!! 因为用空字符进行了替换
// 在处理映射是需要注意去掉多用空格信息
return insertSql.split(",");
}
}
TIPS:
public static String[] getInsertParams(String insertSql) {
int startIndex = insertSql.indexOf("values");
insertSql = insertSql.substring(startIndex + IGNORE_VALUES);
//(#{id},#{username})
// 转变为 id,username 随后根据进行分隔处理
insertSql = insertSql.replaceAll("#\\{","")
.replaceAll("\\}","")
.replaceAll("\\(","").replaceAll("\\)","");
// 注意此处,截取后的信息会多个接连空格信息!!!! 因为用空字符进行了替换
// 在处理映射是需要注意去掉多用空格信息
return insertSql.split(",");
}
以上方法分析:
作用:用于拆解insert插入语句信息
插入语句形如:"insert into stu(id,username) values (#{id},#{username})"
基本拆解逻辑
1.定位values的位置信息
2.截取values信息后的内容信息
注意此时获取到的信息为: " (#{id},#{username})"
此处处理一定要注意这个多余的空格信息!
3.利用正则进行替换处理替换后的新信息为:" id,username"
4.分隔处理返回String数组作为参数信息
注:如果不加处理此时在含有id这个字符串的内容中会包含有一个多余空格~~~~,所以在使用时必须去掉多用的空格信息
此处就是基本SQL语句的执行,数据库基本JDBC语句的编写,以查询为例
public static ResultSet query(String sql, List<Object> paramValueList) throws SQLException { //执行sql语句 // 仅执行查询语句信息 if (sql == null || sql.trim().isEmpty() || !sql.trim().toLowerCase().startsWith("select")) { throw new RuntimeException("你的SQL语句为空或不是查询语句"); } ResultSet rs = null; // 说明执行带参数信息 if( paramValueList.size() > 0 && paramValueList != null) { // 生成预编译sql PreparedStatement preparedStatement = prepare(sql,false); // 为sql设定内容 for(int i = 0 ; i < paramValueList.size() ; i ++ ) { // 这里注意位置从1开始计算 preparedStatement.setObject(i+1,paramValueList.get(i)); } // 执行 rs = preparedStatement.executeQuery(); }else{ // 无参方法 Statement statement = createStatement(); // 执行查询语句信息 try { rs = statement.executeQuery(sql); } catch (SQLException e) { System.out.println("执行无参SQL失败: " + e.getMessage()); } } return rs; }
package mybatisAnnioation.utils;
import com.mysql.jdbc.Driver;
import java.sql.*;
import java.util.List;
/**
* @program: XMind
* @description
* @author: yihang
**/
public class JDBCUtils {
private static String connect;
private static String driverClassName;
private static String URL;
private static String username;
private static String password;
private static boolean autoCommit;
// 连接超时时间
private static final int TIME_OUT = 3;
/** 声明一个 Connection类型的静态属性,用来缓存一个已经存在的连接对象 */
private static Connection conn;
static {
initConfig();
}
/***
* 加载初始信息
*/
private static void initConfig(){
// 设定信息
driverClassName = PropertiesUtil.getProperty("jdbc.driver","");
URL = PropertiesUtil.getProperty("jdbc.url","");
username = PropertiesUtil.getProperty("jdbc.user","");
password = PropertiesUtil.getProperty("jdbc.password","");
// 自动提交
autoCommit = false;
}
/***
* 获取连接
* @return
*/
public static Connection getConnection(){
// 在连接失效的情况下获取连接
if ( isValid() ){
try {
//加载驱动
Class.forName(driverClassName);
// 创建连接
conn = DriverManager.getConnection(URL,username,password);
} catch (Exception e) {
e.printStackTrace();
}
}
return conn;
}
/**
* 判断连接是否有效
* 失效则返回 true
* 有效返回 false
* @return true / false
*/
private static boolean isValid() {
if (conn != null) {
try {
// 超时 或者关闭都认定为连接失效
//isValid方法是判断Connection是否有效,如果连接尚未关闭并且仍然有效,
if (conn.isClosed() || !conn.isValid(TIME_OUT)) {
return true;
}
} catch (SQLException e) {
e.printStackTrace();
}
//conn 既不是 null 且也没有关闭 ,且 isValid 返回 true,说明是可以使用的
// 返回 false
return false;
}
// 连接为空 为获取连接
return true;
}
/**
* 事务控制
* 设置是否自动提交事务
*/
private static void transaction() {
try {
conn.setAutoCommit(autoCommit);
} catch (SQLException e) {
System.out.println("设置事务的提交方式为 : " + (autoCommit ? "自动提交" : "手动提交") + " 时失败: " + e.getMessage());
}
}
/***
* 事务提交
* @param con
*/
private static void commit(Connection con) {
if (con != null && !autoCommit) {
try {
con.commit();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
/***
* 事务回滚
* @param con
*/
private static void rollback(Connection con) {
if (con!= null && !autoCommit) {
try {
con.rollback();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
/***
* 执行查询语句
* @param sql
* @param paramValueList
* @return
* @throws SQLException
*/
public static ResultSet query(String sql, List<Object> paramValueList) throws SQLException {
//执行sql语句
// 仅执行查询语句信息
if (sql == null || sql.trim().isEmpty()
|| !sql.trim().toLowerCase().startsWith("select")) {
throw new RuntimeException("你的SQL语句为空或不是查询语句");
}
ResultSet rs = null;
// 说明执行带参数信息
if( paramValueList.size() > 0 && paramValueList != null) {
// 生成预编译sql
PreparedStatement preparedStatement = prepare(sql,false);
// 为sql设定内容
for(int i = 0 ; i < paramValueList.size() ; i ++ ) {
// 这里注意位置从1开始计算
preparedStatement.setObject(i+1,paramValueList.get(i));
}
// 执行
rs = preparedStatement.executeQuery();
}else{
// 无参方法
Statement statement = createStatement();
// 执行查询语句信息
try {
rs = statement.executeQuery(sql);
} catch (SQLException e) {
System.out.println("执行无参SQL失败: " + e.getMessage());
}
}
return rs;
}
/***
* 处理insert语句信息
* @param insertSql
* @param autoGeneratedKeys 是否获取生成的键集
* @param paramValueList 参数信息
* @return
*/
public static Object insert(String insertSql, boolean autoGeneratedKeys, List<Object> paramValueList) {
// 记录键信息
int keys = -1;
if (insertSql == null || insertSql.trim().isEmpty()) {
throw new RuntimeException("你没有指定SQL语句,请检查是否指定了需要执行的SQL语句");
}
// 如果不是 insert 开头开头的语句
if (!insertSql.trim().toLowerCase().startsWith("insert")) {
System.out.println(insertSql.toLowerCase());
throw new RuntimeException("你指定的SQL语句不是插入语句,请检查你的SQL语句");
}
if( paramValueList != null && paramValueList.size() > 0){
PreparedStatement ps = prepare(insertSql,false);
// 设定参数
for (int i = 0 ; i < paramValueList.size() ; i ++ ) {
try {
ps.setObject(i+1,paramValueList.get(i));
} catch (SQLException e) {
e.printStackTrace();
}
}
// 数据保存
try {
int count = ps.executeUpdate();
// 如果希望获得数据库产生的键
if (autoGeneratedKeys) {
// 获得数据库产生的键集
ResultSet rs = ps.getGeneratedKeys();
// 因为是保存的是单条记录,因此至多返回一个键
// 获得值并赋值给 var 变量
if (rs.next()) {
keys = rs.getInt(1);
}
} else {
// 如果不需要获得,则将受SQL影像的记录数赋值给 keys 变量
keys = count;
}
// 提交
commit(conn);
} catch (SQLException e) {
System.out.println("数据保存失败");
rollback(conn);
}
}else{
// insert语句未提供参数信息
Statement st = createStatement();
try {
keys = st.executeUpdate(insertSql);
commit(conn);
} catch (SQLException e) {
System.out.println("检查inser语句的参数情况,并未提供参数占位信息!!!");
rollback(conn);
}
}
return keys;
}
/***
* 创建无参statment语句信息
* @return
*/
private static Statement createStatement() {
Statement statement = null;
// 创建连接
getConnection();
transaction();
try {
statement = conn.createStatement();
} catch (SQLException e) {
e.printStackTrace();
}
return statement;
}
/**
* 根据给定的带参数占位符的SQL语句,创建 PreparedStatement 对象
*
* @param sql
* 带参数占位符的SQL语句
* @return 返回相应的 PreparedStatement 对象
*/
private static PreparedStatement prepare(String sql, boolean autoGeneratedKeys) {
// 创建连接
getConnection();
// 设置事务控制信息
transaction();
PreparedStatement ps = null;
/* 设置事务的提交方式 */
try {
if (autoGeneratedKeys) {
//使用 PreparedStatement.RETURN_GENERATED_KEYS 可以获取刚刚插入自增ID值
ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
} else {
ps = conn.prepareStatement(sql);
}
} catch (SQLException e) {
System.out.println("创建 PreparedStatement 对象失败: " + e.getMessage());
}
return ps;
}
}
为了避免出现SQL信息的编码处理,将连接数据库所需的信息全部存放到配置文件db.properties中便于维护
package mybatisAnnioation.utils;
import org.apache.commons.lang3.StringUtils;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
/****
* 装载配置信息
*
* prop文件的初始化借助于
* 1. 实例化对象
* 2. 通过load装载内容信息
*/
/**
* @program: XMind
* @description
* @author: yihang
**/
public class PropertiesUtil {
private static Properties props;
// 静态初始化
static {
String fileName = "JdbcConfig.properties";
// 实例化一个对象后,装载内容
props = new Properties();
InputStream inputStream = PropertiesUtil.class.getClassLoader().getResourceAsStream(fileName);
try {
props.load(inputStream);
} catch (IOException e) {
e.printStackTrace();
}
}
public static String getProperty(String key,String defaultValue){
String value = props.getProperty(key.trim());
if(StringUtils.isBlank(value)){
value = defaultValue;
}
return value.trim();
}
private static String getProperty(String key) {
String value = props.getProperty(key.trim());
if (StringUtils.isBlank(value)) {
return null;
}
return value.trim();
}
}
Mybatis框架在解析参数时,所做远比本文实现过程相对复杂。具体可参见源码SqlSourceBuilder 在经过其SqlNode.apply()方法的解析之后,SQL 语句会被传递到 SqlSourceBuilder 中进行进一步的解析。
SqlSourceBuilder 主要完成了两方面的操作。一方面是解析 SQL 语句中的“#{}”占位符 中定义的属性,格式类似于#{frc item_ 0, javaType= int, jdbcType=NUMERIC, typeHandler=MyTypeHandler }--->这种解析主要正对
标签参数信息解析 一方面是将 SQL 语句中的中#{}。”占位符替换成“? ” 占位符。 如果有兴趣可以参见源码,本文仅仅是利用正则替换的形式对不同的sql语句的占位符信息进行了提取替换和提取。实现相对简单,但是基本完整实现了注解开发的基本流程。
参考资料:
《Mybatis技术内幕》
博客:纯手写mybatis(注解版)
文中项目地址:https://github.com/ThansCode/Mybatis-Annotation