1.写个简单的mybatis
今天写个简单版V1.0版本的mybatis,可以其实就是在jdbc的基础上一步步去优化的,网上各种帖子都是照着源码写,各种抄袭,没有自己的一点想法,写代码前要先思考,如果是你,你该怎么写?怎么去实现,为什么要这样写?而不是照着源码依葫芦画瓢。
2.思考
在手写mybatis之前,我们先来手写个jdbc,看看jdbc和mybatis有哪些不同,mybatis能解决哪些jdbc不能解决的问题?如果让你写,你应该从哪里开始写呢?
3.准备工作
CREATE TABLE `user` (
`id` bigint(32) NOT NULL AUTO_INCREMENT,
`name` varchar(40) DEFAULT NULL COMMENT '用户名',
`age` tinyint(3) DEFAULT NULL COMMENT '年龄',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8 COMMENT='用户表';
INSERT INTO `user` (`id`, `name`, `age`) VALUES ('1', '张三', '12');
INSERT INTO `user` (`id`, `name`, `age`) VALUES ('2', '李四', '33');
INSERT INTO `user` (`id`, `name`, `age`) VALUES ('3', '王五', '44');
INSERT INTO `user` (`id`, `name`, `age`) VALUES ('4', '陈贺', '88');
INSERT INTO `user` (`id`, `name`, `age`) VALUES ('5', '刘磊', '34');
INSERT INTO `user` (`id`, `name`, `age`) VALUES ('6', '刘磊', '86');
INSERT INTO `user` (`id`, `name`, `age`) VALUES ('7', '22', '22');
INSERT INTO `user` (`id`, `name`, `age`) VALUES ('8', '王子睿', '22');
INSERT INTO `user` (`id`, `name`, `age`) VALUES ('9', '陈陈陈', '22');
INSERT INTO `user` (`id`, `name`, `age`) VALUES ('10', '刘德华', '66');
INSERT INTO `user` (`id`, `name`, `age`) VALUES ('11', '周杰伦', '77');
新建一个web工程,里面只要引入mysql,不需要spring
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.18</version>
</dependency>
</dependencies>
4.jdbc
public static void main(String[] args) {
Connection conn = null;
PreparedStatement stmt = null;
ResultSet resultSet = null;
try {
//加载驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 获取数据库连接
conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/eom?useUnicode=true&characterEncoding=utf8&autoReconnect=true&serverTimezone=UTC","root","password");
//定义SQL语句
String sql = "select * from user where name = ?";
//获取执行SQL对象
stmt = conn.prepareStatement(sql);
//参数赋值,注意jdbc的下标 都是从1开始
stmt.setString(1,"刘磊");
//执行SQL语句
stmt.execute();
//获取结果
resultSet = stmt.getResultSet();
//结果集
List<User> list = new ArrayList<>();
//遍历结果
while (resultSet.next()){
//封装对象
User user = new User();
user.setId(resultSet.getInt("id"));
user.setName(resultSet.getString("name"));
user.setAge(resultSet.getInt("age"));
list.add(user);
}
System.out.println("-------------" + list);
}catch (Exception e){
}finally {
//释放资源
if(conn != null){
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(stmt != null){
try {
stmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(resultSet != null){
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
执行结果如下,可以获取查询结果
可以看到jdbc的执行过程就是:
- 加载驱动
- 获取连接
- 定义SQL
- 预编译SQL
- 赋值参数替换?
- 执行sql
- 获取结果集
- 释放资源
jdbc的不足
- 频繁创建连接释放资源,性能差
- sql不灵活,参数也不灵活
- 结果集返回的类型是写死的,不能动态的返回
- 可读性差,扩展性差
带着这几个问题,我们来一步一步解决这些问题,看看mybatis是如何解决的
5.DAO
首先我们准备一个接口,里面有一些增删改查方法,而且返回类型有List,有User对象,也有返回单个值的例如String,int这些,基本包含的大部分场景,这个写法就是按照mybatis的方式写的接口,往下看如何一步步实现这些方法
public interface UserMapper {
//这里为什么要搞2个name 参数呢?这是因为在处理获取参数,并赋值的时候,需要考虑这种情况
public List<User> getUserNameAndAge(String name, Integer age);
public List<User> getUserName(String name);
public User getUserById(Integer id);
public String getUserNameById(Integer id);
public int insertUser(String name,Integer age);
public int updateUserById(String name,Integer age,Integer id);
public int deleteUserById(Integer id);
}
那么有没有想过,在spring中我们是通过注入的方式来实现接口的调用
例如通过注解@Resource或者@Autowired来实现注入,因为这些接口都是依赖于spring,被spring统一管理的,但是我们这里是没有spring环境的,那么应该怎么去调用这些接口呢?
6.动态代理
通过jdk的动态代理,来生成一个UserMapper 的代理对象,通过代理对象来执行接口里面的方法
public class MapperProxyFactory {
static {
//注册驱动器
try {
Class.forName("com.mysql.cj.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
public static <T> T getMapper(Class<T> mapper){
//JDK动态代理,生成代理对象,也就是usermapper这个对象
Object proxyInstance = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{mapper}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
}
});
return (T) proxyInstance;
}
有了这个代理工厂就可以拿到UserMapper这个代理对象,然后调用他里面的方法了
public class MybatisApplication {
public static void main(String[] args) {
UserMapper userMapper = MapperProxyFactory.getMapper(UserMapper.class);
List<User> list = userMapper.getUserName("刘磊");
System.out.println("--------sql查询1返回--------" + list);
User user = userMapper.getUserById(2);
System.out.println("--------sql查询2返回--------" + user);
String name = userMapper.getUserNameById(2);
System.out.println("--------sql查询3返回--------" + name);
int insertResult = userMapper.insertUser("周杰伦",77);
System.out.println("--------sql新增1返回--------" + insertResult);
int updateResult = userMapper.updateUserById("陈陈陈",22,9);
System.out.println("--------sql修改1返回--------" + updateResult);
int deleteResult = userMapper.deleteUserById(18);
System.out.println("--------sql删除1返回--------" + deleteResult);
}
}
现在去执行这些方法,是无法实现的,因为这个代理对象里面执行方法还是空的,所有也就无法执行这些方法,那现在我们要怎么办呢?我们需要在代理对象中的invoke方法里面去实现我们的具体业务逻辑代码。
在getMapper的invoke方法中来封装一个方法叫做 doInvoke
public static <T> T getMapper(Class<T> mapper){
//JDK动态代理,生成代理对象,也就是usermapper这个对象
Object proxyInstance = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{mapper}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return doInvoke(proxy, method, args);
}
});
return (T) proxyInstance;
}
7.开始写最最核心的逻辑
我们在doInvoke方法里面来一步一步的实现业务逻辑,首先我们还是安装JDBC的方法来把整个流程先写出来,前面几步跟jdbc一样注册驱动,获取连接,但是这里的sql就不是我们手动写死了,而是需要获取接口上面注解@Select(只实现简单的注解方式,xml配置方式其实逻辑类似,但是过于复杂这里就按注解的方式来实现),以及@Param来实现参数的复制
8.@Select和@Param注解
由于时间关系这里只考虑select注解的方式,其他注解类似
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Select {
String value();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface Param {
String value();
}
有了这2个注解,我们就可以在接口方法上加上注解了
public interface UserMapper {
//这里为什么要搞2个name 参数呢?这是因为在处理获取参数,并赋值的时候,需要考虑这种情况
@Select("SELECT * FROM `user` WHERE name = #{name} and age = #{age}")
public List<User> getUserNameAndAge(@Param("name") String name, @Param("age") Integer age);
@Select("SELECT * FROM `user` WHERE name = #{name}")
public List<User> getUserName(@Param("name") String name);
@Select("SELECT * FROM `user` WHERE id = #{id}")
public User getUserById(@Param("id") Integer id);
@Select("SELECT name from `user` WHERE id = #{id}")
public String getUserNameById(@Param("id") Integer id);
@Select("INSERT INTO `user` (NAME, age) VALUES (#{name}, #{age})")
public int insertUser(@Param("name") String name,@Param("age") Integer age);
@Select("UPDATE USER SET `name` = #{name}, age = #{age} WHERE id = #{id}")
public int updateUserById(@Param("name") String name,@Param("age") Integer age,@Param("id") Integer id);
@Select("DELETE FROM `user` WHERE id = #{id}")
public int deleteUserById(@Param("id") Integer id);
}
9.获取sql
那么怎么拿到方法上的sql呢?
可以通过Method对象来获取注解上的值,注意下面代码都在doInvoke方法中
//通过method参数拿到select注解上的 sql
Select annotation = method.getAnnotation(Select.class);
//sql = select * from user where name = #{name} and age = #{age} and id = #{name}
String sql = annotation.value();
此时拿到的sql是这样的
select * from user where name = #{name} and age = #{age} and id = #{name}
很明显,这样的sql是无法执行的,我们需要把#{xxx}这样的变量替换成?,statement才能进行预编译执行sql,也就是变成下面这样的sql
select * from user where name = ? and age = ? and id = ?
那么应该怎么搞?大家想一想,如果是你写,你会怎么入手呢?
10.把#{}替换成?并赋值
我们可以通过method 来获取参数,然后遍历赋值,可以定义一个map,里面存放参数名和参数值
例如:
key=name,value=刘磊
key=age,value=66
key=id,value=刘磊
这里我为什么要搞2个#{name}呢?有没有想过呢?
//用来存放参数名和参数值
Map<String,Object> paramValueMapping = new HashMap<>();
//通过方法拿到入参
Parameter[] parameters = method.getParameters();
//遍历参数
for (int i = 0; i < parameters.length; i++) {
Parameter parameter = parameters[i];
//注意这里parameter.getName()拿到的 是arg0,arg1,并不是真正的参数名,这个时候就需要@param注解了
paramValueMapping.put(parameter.getName(),args[i]);
//通过@param注解的方式拿到参数名称,这里拿到的就是name,age,id的真正参数名
String paramName = parameter.getAnnotation(Param.class).value();
paramValueMapping.put(paramName,args[i]);
}
现在要做的就是如何把#{xxx}替换成?,来我们一步步实现
我们写个参数处理器接口,然后去实现他
TokenHandler 接口
public interface TokenHandler {
String handleToken(String content);
}
ParameterMapping对象,用来存放sql中#{xxx}对应的值
public class ParameterMapping {
//sql中 #{name} 的这个值
private String property;
public ParameterMapping(String property) {
this.property = property;
}
public String getProperty() {
return property;
}
public void setProperty(String property) {
this.property = property;
}
}
ParameterMappingTokenHandler实现类
public class ParameterMappingTokenHandler implements TokenHandler {
//存放sql 中#{} 变量的名称
private List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();
@Override
public String handleToken(String content) {
//把变量名称存到list中,然后返回?,这样即拿到了参数名称,也替换了?
parameterMappings.add(new ParameterMapping(content));
return "?";
}
public List<ParameterMapping> getParameterMappings() {
return parameterMappings;
}
}
好,那这个替换的逻辑写好了,我们是不是应该先解析这个sql,把这些#{}的内容找出来,这里节约时间之间把mybatis源码中的解析器拿过来之间用,有个叫GenericTokenParser的解析器,这个类里面逻辑并不复杂就是找到符合条件的字符串,然后替换成?
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) {
final StringBuilder builder = new StringBuilder();
final StringBuilder expression = new StringBuilder();
if (text != null && text.length() > 0) {
char[] src = text.toCharArray();
int offset = 0;
// search open token
int start = text.indexOf(openToken, offset);
while (start > -1) {
if (start > 0 && src[start - 1] == '\\') {
// this open token is escaped. remove the backslash and continue.
builder.append(src, offset, start - offset - 1).append(openToken);
offset = start + openToken.length();
} else {
// found open token. let's search close token.
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 {
//最最关键的地方是这里,通过刚刚我们定义的方法,把符合条件的替换成?
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();
}
}
最关键的就是这一行代码
//最最关键的地方是这里,通过刚刚我们定义的方法,把符合条件的替换成?
builder.append(handler.handleToken(expression.toString()));
好了,解析器也有了,替换?的逻辑也有了,我们来看下 如何去使用?
//生成解析器-就是把#{} 改成?
ParameterMappingTokenHandler tokenHandler = new ParameterMappingTokenHandler();
GenericTokenParser parse = new GenericTokenParser("#{","}",tokenHandler);
//解析之后的sql=select * from user where name = ? and age = ? and id = ?
String parseSql = parse.parse(sql);
GenericTokenParser这个方法有3个参数,分别是开始,结束,和要替换的处理器,不难理解
此时解析后的parseSql = select * from user where name = ? and age = ? and id = ?,正是我们想要的结果,好了sql解析完成,那么如何去赋值呢?想一想?
我们知道,statement的赋值方式 是setString,或者setInt,或者set其他类型来实现参数赋值的,那么这里我们是不是需要先拿到参数的类型,我才能知道 到底是setString,还是setInt呢?
刚刚上面的那个ParameterMappingTokenHandler里面的那个list其实已经拿到了参数名,我们来把这个list来遍历,这里面需要考虑一个问题,就是怎么获取参数的类型,到底是string还是int或者其他类型
//这里面就是存放的替换?的 那些变量名name,age,name
List<ParameterMapping> parameterMappings = tokenHandler.getParameterMappings();
//遍历赋值
for (int i = 0; i < parameterMappings.size(); i++) {
String property = parameterMappings.get(i).getProperty();
//注意这里赋值的时候,会有个类型的问题,有可能是字符串,也有可能是数字类型,所以需要知道参数的类型,然后才能给参数赋值,不然会报错
//stmt.setString(); ???
//stmt.setInt(); ???
//通过变量名获取变量的类型,刚刚上面的那个map里面存放的就是参数名和参数值,那么通过获取这个参数值,我们就能拿到这个参数值的类型
Object value = paramValueMapping.get(property);
Class<?> type = value.getClass();
}
此时这个type就是我们具体的参数类型了,那么拿到这个参数类型了,我是不是就可以赋值了?我们可以根据type来判断,if或者switch都可以实现,但是有个问题,java类型那么多,写那么多if else 可读性可扩展性是不是很差,而且也不符合java的设计模式,这个时候,我们可以使用策略模式来处理这里的逻辑
if(type.equals(String.class)){
stmt.setString();
}else if(type.equals(Integer.class)){
stmt.setInt();
}
首先我们定义个带泛型的类型处理器接口,里面有个赋值(setParameter)的方法,3个参数分别是statement,第几个参数赋值,要赋值的参数类型
还有个方法getResult是一会处理结果集时,找到对应字段的值
public interface TypeHandler<T> {
/**
* @param statement st
* @param i 第几个参数赋值
* @param value 参数值
* @return void
* @author WangPan
* @date 2022/12/7 15:37
*/
void setParameter(PreparedStatement statement,int i, T value) throws SQLException;
/**
* @description 在结果集里面获取对应字段的值
* @param resultSet
* @param columnName
* @return T
* @author WangPan
* @date 2022/12/7 17:45
*/
T getResult(ResultSet resultSet,String columnName) throws SQLException;
}
然后我们需要写string和int 的2个实现类,去实现这2个方法,这里节约时间只考虑string和int 类型,其他类型类似
IntegerTypeHandler 里面的赋值方法就可以写死setInt,泛型也是Integer
public class IntegerTypeHandler implements TypeHandler<Integer>{
/**
* @param statement st
* @param i 第几个参数赋值
* @param value 参数值
* @return void
* @author WangPan
* @date 2022/12/7 15:37
*/
@Override
public void setParameter(PreparedStatement statement, int i, Integer value) throws SQLException {
statement.setInt(i,value);
}
/**
* @param resultSet
* @param columnName
* @return T
* @description 在结果集里面获取对应字段的值
* @author WangPan
* @date 2022/12/7 17:45
*/
@Override
public Integer getResult(ResultSet resultSet, String columnName) throws SQLException {
return resultSet.getInt(columnName);
}
}
StringTypeHandler 里面的赋值方法就可以写死setString,泛型也是String
public class StringTypeHandler implements TypeHandler<String>{
/**
* @param statement st
* @param i 第几个参数赋值
* @param value 参数值
* @return void
* @author WangPan
* @date 2022/12/7 15:37
*/
@Override
public void setParameter(PreparedStatement statement, int i, String value) throws SQLException {
statement.setString(i,value);
}
/**
* @param resultSet
* @param columnName
* @return T
* @description 在结果集里面获取对应字段的值
* @author WangPan
* @date 2022/12/7 17:45
*/
@Override
public String getResult(ResultSet resultSet, String columnName) throws SQLException {
return resultSet.getString(columnName);
}
}
好了,然后我们把这个放在static静态块里面去初始化,一会就可以直接使用typeHandlerMap来赋值
//类型处理器,sql给变量赋值的时候,到底是setString,还是setInt,还是别的类型
private static Map<Class, TypeHandler> typeHandlerMap = new HashMap<>();
static {
typeHandlerMap.put(String.class, new StringTypeHandler());
typeHandlerMap.put(Integer.class,new IntegerTypeHandler());
}
接下来我们看看如何使用这个,接着上面的for循环写
for (int i = 0; i < parameterMappings.size(); i++) {
String property = parameterMappings.get(i).getProperty();
//通过变量名获取变量的类型
Object value = paramValueMapping.get(property);
Class<?> type = value.getClass();
//利用类型处理器,根据字段类型来给变量赋值
//通过type类型来获取对应的处理器,如果是string就执行StringTypeHandler这个处理器里面的赋值方法
//如果type=Integer,就执行IntegerTypeHandler 这个处理器里面的赋值方法
typeHandlerMap.get(type).setParameter(stmt,i+1,value);
}
注意这里jdbc的赋值要从1开始
这样写是不是要优雅的多,逼格一下子就上来了了,不必if else 一目了然,可读性更高吗?
执行sql获取结果集
这里有来了个问题,有的结果是返回List,有的是返回String,有的是返回User对象,有的是返回int,那么应该怎么办呢?
首先我们要明白一点的就是只有select查询语句是有结果集的,insert update delete是没有结果集
那么如果是insert update delete就简单了,只需要返回更新的条数就行
那么如果是select怎么办?
我们要考虑这下面3种情况
//查询多条数据返回list
List<User>
//查询单条数据返回对象
User
//查询具体某个字段返回string
String
还有一种情况需要考虑,就是如果不是User对象,例如返回Person对象,里面的字段不一样,那我们应该怎么把结果集转换成对应的java类型返回呢?又怎么去调用User对象里面set方法赋值呢?
这里是本次手写mybatis的最最复杂的地方,如果是你,你有没有思路呢?
//执行SQL语句
stmt.execute();
//获取结果
ResultSet resultSet = stmt.getResultSet();
//先定义一个要返回的对象,不管是User,或者list或者string最后都赋值给Object返回
Object result = null;
//返回的对象如果是list 就先存到list里面然后在放在Object里面返回
List<Object> list = new ArrayList<>();
//这里要判断返回结果是否为空,如果不是select,是insert或者update,delete的话就没有 返回结果集
if(resultSet != null){
//这里需要对返回的结果进行处理,要获取返回的类型到底是集合,还是的那个对象,还是别的其他类型
//定义一个返回对象类型
Class resultType = null;
//获取方法的返回类型,来判断是否是泛型
Type genericReturnType = method.getGenericReturnType();
if(genericReturnType instanceof Class){
//不是泛型
resultType = (Class) genericReturnType;
}else if(genericReturnType instanceof ParameterizedType){
//是泛型,这里只考虑list<User> 这种简单泛型,不考虑 List<User> 这种
Type[] actualTypeArguments = ((ParameterizedType) genericReturnType).getActualTypeArguments();
//取泛型的第一个List<User>
resultType = (Class) actualTypeArguments[0];
}
//结果集的元数据
ResultSetMetaData metaData = resultSet.getMetaData();
//存放sql查询的 有哪些字段,例如 select id,name,age那这个list 里面就是对应的 字段名
List<String> columnList = new ArrayList<>();
for (int i = 0; i < metaData.getColumnCount(); i++) {
columnList.add(metaData.getColumnName(i+1));
}
//记录User对象里面有哪些set方法
Map<String,Method> setterMethodMapping = new HashMap<>();
//获取User对象中所有的方法
for(Method declaredMethod : resultType.getDeclaredMethods()){
//找到set方法
if(declaredMethod.getName().startsWith("set")){
//获取set方法后截取set之后的字符串就是 对应的字段名
String propertyName = declaredMethod.getName().substring(3);
//然后把首字母改成小写
propertyName = propertyName.substring(0,1).toLowerCase(Locale.ROOT) + propertyName.substring(1);
setterMethodMapping.put(propertyName,declaredMethod);
}
}
while (resultSet.next()){
//反射创建返回对象
Object instance = resultType.newInstance();
//创建出user对象之后,需要调用set方法赋值,但是又不能调用所有的set方法,是根据sql里面查询的结果来set ,
//如果是select * 就要set所有,如果只查询了3个字段就只调用这3个字段的set方法
for (int i = 0; i < columnList.size(); i++) {
String column = columnList.get(i);
if(setterMethodMapping.size() > 0){
//如果有setter方法就说明返回类型 是对象类型
//获取这个字段对应的setter方法
Method setterMethod = setterMethodMapping.get(column);
//然后通过setter方法找到入参的类型,因为setter方法只有1个参数,所以取0个,渠道的结果是String,Int或者其他类型
Class clazz = setterMethod.getParameterTypes()[0];
//然后根据入参的类型来调用具体的处理器,意思就是到底是调用setString,还是setInt,还是别的类型
TypeHandler typeHandler = typeHandlerMap.get(clazz);
//获取结果集里面字段对应的值
Object resultValue = typeHandler.getResult(resultSet, column);
//setter方法执行(2个参数,一个是对象,一个是执行setter方法的参数值),这样User对象里的属性就有值了
setterMethod.invoke(instance,resultValue);
}else{
//如果没有setter方法,就说明返回类型是 数据类型
//获取结果集里面字段对应的值
Class clazz = (Class)method.getGenericReturnType();
//然后根据入参的类型来调用具体的处理器,意思就是到底是调用setString,还是setInt,还是别的类型
TypeHandler typeHandler = typeHandlerMap.get(clazz);
instance = typeHandler.getResult(resultSet, column);
}
}
list.add(instance);
}
}else{
//如果是insert或者update,delete,就获取更新条数
result = stmt.getUpdateCount();
}
这段结果集处理的代码比较复杂,我来把这块逻辑梳理一下
- 首先判断结果集是否为空,如果是空就说明是insert update delete语句直接返回更新行数就行
- 如果不为空,那么就需要获取方法返回类型,判断是否是泛型,
- 如果是泛型,就取泛型里的第0个对象,简单起见吗,这里不考虑List<List<对象>>这种情况
- 通过结果集的metaData获取select 的哪些字段,例如name age id这些字段存在list里面
- 然后获取到具体对象之后,我们需要知道这个对象里面有哪些setXx方法,用map存放这些方法
- 然后遍历结果集,这个时候需要通过反射来创建对象
- 然后循环刚刚metaData获取的所有列的 list
- 这个时候要考虑一种情况就是如果是User对象就肯定有set方法,如果是String或者Integer类型就没有set方法,所有需要加个判断
- 如果是User对象,就遍历这些字段去执行set方法,通过Method.invoke来执行set方法并赋值
- 如果是String或者Integer,就需要根据方法的返回类型来判断,应该结果集取值需要getInt或者getString,拿到方法的返回类型后,通过上面创建的处理器来实现从结果集里面取值
- 这样就完成对象的创建,赋值,最后添加到list里面
这样就完成了吗?
还差最后一步,那么到底是返回List,还是User对象,还是Stirng ,还是Integer呢?
//这里不能直接返回list,需要根据方法返回的类型来判断 到底是返回list,还是对象,还是其他类型
if(method.getReturnType().equals(List.class)){
//如果是list,就直接返回
result = list;
}else if(method.getReturnType().equals(Object.class)){
//如果是对象 就取第一个
result = list.get(0);
}else{
//如果是String 或者Integer或者其他类型
if(list.size() > 0){
result = list.get(0);
}
//其他类型,insert update delete不做处理
}
释放资源
//释放资源
if(conn != null){
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(stmt != null){
try {
stmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(resultSet != null){
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
完整代码
public static Object doInvoke(Object proxy, Method method, Object[] args) throws Throwable{
//要返回的对象
Object result = null;
//返回的对象如果是list 就先存到list里面然后在放在Object里面返回
List<Object> list = new ArrayList<>();
//动态代理执行方法
//获取连接----获取session----获取sql----获取参数----执行sql----返回数据
// 获取数据库连接
Connection conn = getConnection();
//通过method参数拿到select注解上的 sql
Select annotation = method.getAnnotation(Select.class);
//sql = select * from user where name = #{name} and age = #{age} and id = #{name}
String sql = annotation.value();
//获取入参
//这个map用来存入参和对应参数的值,例如这样的
//name=1
//age=2
//name=3
Map<String,Object> paramValueMapping = new HashMap<>();
Parameter[] parameters = method.getParameters();
for (int i = 0; i < parameters.length; i++) {
Parameter parameter = parameters[i];
//注意这里parameter.getName()拿到的 是arg0,arg1,并不是真正的参数名,这个时候就需要@param注解了
paramValueMapping.put(parameter.getName(),args[i]);
//通过@param注解的方式拿到参数名称
String paramName = parameter.getAnnotation(Param.class).value();
paramValueMapping.put(paramName,args[i]);
}
//生成解析器-就是把#{} 改成?
ParameterMappingTokenHandler tokenHandler = new ParameterMappingTokenHandler();
GenericTokenParser parse = new GenericTokenParser("#{","}",tokenHandler);
//解析之后的sql=select * from user where name = ? and age = ? and id = ?
String parseSql = parse.parse(sql);
//这里面就是存放的替换?的 那些变量名name,age,name
List<ParameterMapping> parameterMappings = tokenHandler.getParameterMappings();
//获取执行SQL对象
PreparedStatement stmt = conn.prepareStatement(parseSql);
//赋值
for (int i = 0; i < parameterMappings.size(); i++) {
String property = parameterMappings.get(i).getProperty();
//注意这里赋值的时候,会有个类型的问题,有可能是字符串,也有可能是数字类型,所以需要知道参数的类型,然后才能给参数赋值,不然会报错
//stmt.setString(); ???
//stmt.setInt(); ???
//通过变量名获取变量的类型
Object value = paramValueMapping.get(property);
Class<?> type = value.getClass();
//利用类型处理器,根据字段类型来给变量赋值
typeHandlerMap.get(type).setParameter(stmt,i+1,value);
}
//执行SQL语句
stmt.execute();
//获取结果
ResultSet resultSet = stmt.getResultSet();
//这里要判断返回结果是否为空,如果不是select,是insert或者update,delete的话就没有 返回结果集
if(resultSet != null){
//这里需要对返回的结果进行处理,要获取返回的类型到底是集合,还是的那个对象,还是别的其他类型
//返回对象类型
Class resultType = null;
//获取方法的返回类型,来判断是否是泛型
Type genericReturnType = method.getGenericReturnType();
if(genericReturnType instanceof Class){
//不是泛型
resultType = (Class) genericReturnType;
}else if(genericReturnType instanceof ParameterizedType){
//是泛型,这里只考虑list<User> 这种简单泛型,不考虑 List<User> 这种
Type[] actualTypeArguments = ((ParameterizedType) genericReturnType).getActualTypeArguments();
//取泛型的第一个List<User>
resultType = (Class) actualTypeArguments[0];
}
//结果集的元数据
ResultSetMetaData metaData = resultSet.getMetaData();
//存放sql查询的 有哪些字段,例如 select id,name,age那这个list 里面就是对应的 字段名
List<String> columnList = new ArrayList<>();
for (int i = 0; i < metaData.getColumnCount(); i++) {
columnList.add(metaData.getColumnName(i+1));
}
//记录User对象里面有哪些set方法
Map<String,Method> setterMethodMapping = new HashMap<>();
//获取User对象中所有的方法
for(Method declaredMethod : resultType.getDeclaredMethods()){
//找到set方法
if(declaredMethod.getName().startsWith("set")){
//获取set方法后截取set之后的字符串就是 对应的字段名
String propertyName = declaredMethod.getName().substring(3);
//然后把首字母改成小写
propertyName = propertyName.substring(0,1).toLowerCase(Locale.ROOT) + propertyName.substring(1);
setterMethodMapping.put(propertyName,declaredMethod);
}
}
while (resultSet.next()){
//反射创建返回对象
Object instance = resultType.newInstance();
//创建出user对象之后,需要调用set方法赋值,但是又不能调用所有的set方法,是根据sql里面查询的结果来set ,
//如果是select * 就要set所有,如果只查询了3个字段就只调用这3个字段的set方法
for (int i = 0; i < columnList.size(); i++) {
String column = columnList.get(i);
if(setterMethodMapping.size() > 0){
//如果有setter方法就说明返回类型 是对象类型
//获取这个字段对应的setter方法
Method setterMethod = setterMethodMapping.get(column);
//然后通过setter方法找到入参的类型,因为setter方法只有1个参数,所以取0个,渠道的结果是String,Int或者其他类型
Class clazz = setterMethod.getParameterTypes()[0];
//然后根据入参的类型来调用具体的处理器,意思就是到底是调用setString,还是setInt,还是别的类型
TypeHandler typeHandler = typeHandlerMap.get(clazz);
//获取结果集里面字段对应的值
Object resultValue = typeHandler.getResult(resultSet, column);
//setter方法执行(2个参数,一个是对象,一个是执行setter方法的参数值),这样User对象里的属性就有值了
setterMethod.invoke(instance,resultValue);
}else{
//如果没有setter方法,就说明返回类型是 数据类型
//获取结果集里面字段对应的值
Class clazz = (Class)method.getGenericReturnType();
//然后根据入参的类型来调用具体的处理器,意思就是到底是调用setString,还是setInt,还是别的类型
TypeHandler typeHandler = typeHandlerMap.get(clazz);
instance = typeHandler.getResult(resultSet, column);
}
}
list.add(instance);
}
}else{
//如果是insert或者update,delete,就获取更新条数
result = stmt.getUpdateCount();
}
//这里不能直接返回list,需要根据方法返回的类型来判断 到底是返回list,还是对象,还是其他类型
if(method.getReturnType().equals(List.class)){
//如果是list,就直接返回
result = list;
}else if(method.getReturnType().equals(Object.class)){
//如果是对象 就取第一个
result = list.get(0);
}else{
//其他类型,insert update delete只需要返回 更新的条数
if(list.size() > 0){
result = list.get(0);
}
}
//释放资源
if(conn != null){
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(stmt != null){
try {
stmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(resultSet != null){
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
return result;
}
执行一下
增删改查都没问题
有几个的问题?
- 这里实际上还是用的jdbc来创建连接并不是由会话来管理的,还是会频繁创建连接释放连接影响性能
- 可以加入连接池例如druid,hika等
- 这是通过注解的方式,那么要是通过xml的方式改怎么处理呢?
- mybatis的缓存机制怎么实现
- 如果让你来完成上面功能?你应该怎么处理呢?想一想
用到了哪些设计模式
- 工厂模式:MapperProxyFactory
- 代理模式:proxyInstance
- 策略模式:TypeHandler、IntegerTypeHandler、StringTypeHandler
mybatis源码中还用到的设计模式
- 建造者模式:SqlSessionFactoryBuilder
- 单例模式:Configuration
- 适配模式:Log4j、Slf4j适配Log接口
- 装饰器模式:Wrapper
- 模板模式:Executor–BaseExecutor–SimpleExecutor里面就有很多模板,代码复用