手撕Java Mybatis框架
发布:2023-12-28 16:39
更新:2024-01-11 10:40
作者:   0xdFFF
浏览:   260
分类:  Java
字数:15002

手撕JDBC框架

开源地址:JDBCToMybatis Git

JDBC封装Mybatis

Select 原生SQL语句

封装原生查询,返回ResultSet

  1. public ResultSet select(String sql, Object... values){
  2. try {
  3. Class.forName(driverPath);
  4. Connection connection = DriverManager.getConnection(sqlUrl,user,password);
  5. PreparedStatement pst = connection.prepareStatement(sql);
  6. for (int i =0;i<values.length;i++){
  7. //jdbc会自动转换类型
  8. pst.setObject(i+1,values[i]);
  9. }
  10. ResultSet set = pst.executeQuery();
  11. connection.close();
  12. pst.close();
  13. return set;
  14. }catch (Exception e){
  15. e.printStackTrace();
  16. return null;
  17. }finally {
  18. System.out.println("执行完毕");
  19. }
  20. }

在调用该方法时,只需传递 select(sql,参数);

  1. ("select * from `user` where id = ?",1)

返回一个范型对象,将ResultSet在方法内封装成对象:

  1. public <T> T selectOne(String sql,RowMapper rm,Object... values){
  2. T obj = null; //范型对象
  3. try {
  4. Class.forName(driverPath);
  5. Connection connection = DriverManager.getConnection(sqlUrl,user,password);
  6. PreparedStatement pst = connection.prepareStatement(sql);
  7. for (int i =0;i<values.length;i++){
  8. //jdbc会自动转换类型
  9. pst.setObject(i+1,values[i]);
  10. }
  11. ResultSet set = pst.executeQuery();
  12. while (set.next()){
  13. //对执行结果进行对象封装
  14. obj = rm.rowMapper(set);
  15. }
  16. set.close();
  17. connection.close();
  18. pst.close();
  19. return obj;
  20. }catch (Exception e){
  21. e.printStackTrace();
  22. return null;
  23. }finally {
  24. System.out.println("执行完毕");
  25. }
  26. }

测试类

  1. @Test
  2. public void TestSelectOne() {
  3. RowMapper rowMapper = new RowMapper() {
  4. //RowMapper是一个接口,实例
  5. @Override
  6. public <T> T rowMapper(ResultSet set) throws SQLException{
  7. //策略模式
  8. int id = set.getInt(1); //获取ID
  9. String name = set.getString(2);//获取用户名
  10. int age = set.getInt(3);
  11. User user = new User(id,name,age);
  12. return (T) user;
  13. }
  14. };
  15. User user = SqlSession.getInstance().selectOne("select * from `user` where id = ?",rowMapper,0);
  16. System.out.println(user);
  17. }

同理,如果查询结果包含多条,那么可以创建 List 接收范型集合。

Select 简化SQL调用

​ 在进行SQL调用时,我们的代码通常会有很多重复的部分。那么我们该怎么简化SQL语句的执行呢。上面我们通过创建使用原生jdbc获取结果集后,通过RowMapper中的接口来将该结果集转化为实现RowMapper.rowMapper()方法中返回的对象。虽然简化了SQL的调用,但是仍然需要自己实现rowMapper匿名类。

​ 于是我们可以在SqlSession,也就是在Dao层调用sql执行方法的那一层里可以实现对查询结果的封装:

  1. ResultSet set = pst.executeQuery();
  2. while (set.next()){
  3. //对执行结果进行对象封装
  4. obj = handler.mapperClass(set,cls);
  5. }

我们创建了Handler结果集处理层,将实现对SQL执行结果的自动化封装成对象/Map集合/普通类型。

1.我们可以通过Map集合的方式,获取到ResultSet中的列名以及对应的数据,将该数据放入K-V键值对中

2.我们可以传入Class,通过反射的形式获取到ResultSet中的列名对应的属性字段,然后再对其进行赋值即可。也可以通过反射的形式获取set方法来对属性值进行赋值。

  1. //通过反射的方法来获取cls对象的属性列表,在通过set集合的列名来给类设置属性值。
  2. private Object toObject(ResultSet set,Class cls) throws SQLException {
  3. ResultSetMetaData rsm = set.getMetaData();
  4. Object obj = null;
  5. try {
  6. obj = cls.newInstance();
  7. for(int i = 1;i<=rsm.getColumnCount();i++){
  8. // ············································
  9. // 方法一 反射直接修改字段
  10. // //通过反射获取类的字段(属性)信息
  11. // Field field = obj.getClass().getDeclaredField(rsm.getColumnName(i));
  12. // //设置私有属性是可修改的
  13. // field.setAccessible(true);
  14. // //直接设置属性值
  15. // field.set(obj,set.getObject(rsm.getColumnName(i)));
  16. // ·············································
  17. //通过反射获取类中的set方法
  18. String methodName = "set"+rsm.getColumnName(i).substring(0,1).toUpperCase()+rsm.getColumnName(i).substring(1);
  19. System.out.println(methodName);
  20. Field field = cls.getDeclaredField(rsm.getColumnName(i));//根据属性名称获取单个属性
  21. //通过属性的类型来调用类中的Setter方法,如setId(Integer.class);
  22. if (field.getGenericType().toString().equals("int")||field.getGenericType().toString().equals("class java.lang.Integer")){
  23. Method method = obj.getClass().getDeclaredMethod(methodName,Integer.class);
  24. method.invoke(obj,set.getInt(rsm.getColumnName(i)));
  25. }else if(field.getGenericType().toString().equals("string")||field.getGenericType().toString().equals("class java.lang.String")){
  26. Method method = obj.getClass().getDeclaredMethod(methodName,String.class);
  27. method.invoke(obj,set.getString(rsm.getColumnName(i)));
  28. }
  29. }
  30. } catch (Exception e) {
  31. e.printStackTrace();
  32. }
  33. return obj;
  34. }

Insert 简化SQL调用

​ 一条插入SQL语句会包含需要传入的参数。一般使用PrepareStatement来对该SQL语句预处理后在对问号部位进行赋值。那么就需要自己传入参数而且参数必须和SQL语句问号位置相对应。

  1. //insert user values(?,?,?)
  2. PreparedStatement pst = getPst(sql);
  3. pst.setInt(1,1001);
  4. pst.setString(2,"刘德华");
  5. pst.setInt(3,19);

​ 如果SQL语句中问号数量太多,如果我们手动设置这些参数非常麻烦。那么我们就需要简化SQL的操作,看怎么才能让程序自动给处理在哪里插入参数。

​ 思路:我们可以构造一个SQL语句如:select * from user where id = #{id},然后再传入一个User对象,该User的对象的id属性存在且不为空,那么可以通过传递sql语句和一个user对象即可完成语句的执行。我们可以通过StringBuilder对传入的SQL进行解析,解析完毕之后再使用反射获取到对象对应属性的值,随后使用原生jdbc对其进行预处理赋值执行即可。

  1. public List<Object> parseSQL(String sql){
  2. List<Object> params = new ArrayList<>();
  3. StringBuilder sb = new StringBuilder(sql);
  4. StringBuilder newSQL = new StringBuilder();
  5. while (true){
  6. int left = sb.indexOf("#{");
  7. int right = sb.indexOf("}");
  8. if(left!=-1 && right!=-1 && left<right){
  9. //取出在#{左侧的所有
  10. newSQL.append(sb.substring(0,left));
  11. //获取参数名 #{name}
  12. String paramName = sb.substring(left+2,right);
  13. //将获取的参数名放入List中
  14. params.add(paramName);
  15. //用问号填充参数
  16. newSQL.append("?");
  17. //从后面的再找
  18. sb = new StringBuilder(sb.substring(right+1));
  19. }else{
  20. //说明本次循环已经找到了,将剩余的字符填充
  21. newSQL.append(sb.substring(right+1));
  22. break;
  23. }
  24. }
  25. params.add(newSQL.toString());
  26. return params;
  27. }

解析完毕SQL之后,那就可以反射传入的对象预处理了。

  1. public boolean insert(String sql,Object obj) throws Exception {
  2. //从SQL语句中获得需要插入的参数。参数名和obj的属性相对应
  3. List<Object> list = handler.parseSQL(sql);
  4. //获取处理后的SQL语句
  5. PreparedStatement pst = getPst((String) list.get(list.size()-1));
  6. //反射对象获取对象的相应属性
  7. for (int i = 0;i<list.size()-1;i++){
  8. //通过反射获取相应字段
  9. Field field = obj.getClass().getDeclaredField((String) list.get(i));
  10. //设置字段可访问
  11. field.setAccessible(true);
  12. //获取字段对应的值
  13. Object value = field.get(obj);
  14. //通过jdbc原生对预处理赋值
  15. pst.setObject(i+1,value);
  16. }
  17. try {
  18. pst.execute();
  19. return true;
  20. }catch (Exception e){
  21. e.printStackTrace();
  22. return false;
  23. }
  24. }

测试代码

  1. @Test
  2. public void testInsert() throws Exception {
  3. String sql = "insert user values (#{id},#{name},#{age})";
  4. User user = new User(1010,"李三",29);
  5. if (SqlSession.getInstance().insert(sql,user)){
  6. System.out.println("insert succeed!");
  7. }else {
  8. System.out.println("insert failed!");
  9. }
  10. }

但是上面的代码有个Bug,就是只能传入对象而不能传入其他基本类型。那么我们可以对obj的类型进行判断再预处理:

  1. //给预处理的SQL语句自动赋值
  2. public void setParams(PreparedStatement pst,Object obj,List<Object> params) throws Exception {
  3. Class<?> cls = obj.getClass();
  4. if(cls == Integer.class || cls == String.class || cls == Float.class || cls == Double.class){
  5. //说明传入的是一个基本参数,直接赋值
  6. pst.setObject(1, obj);
  7. }else if(obj instanceof Map){
  8. //说明传入的是Map对象
  9. Map map = (Map) obj;
  10. for(int i = 0;i<params.size()-1;i++){
  11. //通过参数名来查询Map集合中的属性
  12. Object value = map.get(params.get(i));
  13. pst.setObject(i+1,value);
  14. }
  15. }else {
  16. //传入的是其他对象
  17. //反射对象获取对象的相应属性
  18. for (int i = 0;i<params.size()-1;i++){
  19. //通过反射获取相应字段
  20. Field field = obj.getClass().getDeclaredField((String) params.get(i));
  21. //设置字段可访问
  22. field.setAccessible(true);
  23. //获取字段对应的值
  24. Object value = field.get(obj);
  25. //通过jdbc原生对预处理赋值
  26. pst.setObject(i+1,value);
  27. }
  28. }
  29. }

通过注解执行SQL语句

注解写法

  1. package src.ORM.Annotation;
  2. import java.lang.annotation.ElementType;
  3. import java.lang.annotation.Retention;
  4. import java.lang.annotation.RetentionPolicy;
  5. import java.lang.annotation.Target;
  6. @Target(ElementType.METHOD)
  7. //Retention表示获取注解的段,RunTime表示在java源码、编译、运行都可以调用
  8. @Retention(RetentionPolicy.RUNTIME)
  9. public @interface Delete {
  10. String value();
  11. }

注解默认的方法为 value(); 访问该访问即可获取到注解中value对应的值

调用该方法的语句为 @Delete(value = "delete from user where id = 12")

注解目的

​ 通过注解 我们可以不用重写Dao接口的具体实现方法,通过注解即可实现简单SQL语句的具体实现,如Dao接口的方法为:

void insert(User user);那么我们可以通过对其进行注解:@Insert("insert users value(#{id},#{name},#{age}) ")来实现数据的插入。

实现原理

​ 通过代理Proxy类,代理Dao类中的方法来获取被执行的方法返回值和注解类型以及注解值。在通过上面已经封装好的CRUD方法来执行SQL语句。

​ a.获取需要反射的类,重写invoke方法。

​ b.通过反射中的参数获得注解名

​ c.根据注解名来判断执行哪种SQL语句

​ d.在通过反射注解类获得注解的注释值,即注解中的SQL语句

​ e.如果执行的SQL语句为select查询语句,带有返回值的。那么需要区分返回的是集合还是普通对象

​ e.1 通过反射被代理的方法获取该方法的返回值,如果该值为List集合,则需要获取List中的范型类型,再使用封装好的方法执行即可。

​ e.2 如果为基本类型、自定义类型,直接调用封装的方法即可。

Proxy代理类的方法类似于通过逆向Hook得到类的方法并进行调用。

  1. public <T> T getFuncProxy(Class<?> cls){
  2. //获得类的加载
  3. ClassLoader classLoader = cls.getClassLoader();
  4. Class<?>[] interfaces = new Class[]{cls};
  5. InvocationHandler invocationHandler = new InvocationHandler() {
  6. @Override
  7. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  8. //获取返回类型的类
  9. //method -> 代理类被调用的方法
  10. //args -> 代理类被调用方法的参数
  11. Class<?> returnCLass = method.getReturnType();
  12. System.out.println("returnType->"+returnCLass.getTypeName());
  13. System.out.println("MethodName->"+method.getName());
  14. if(String.class.equals(returnCLass)){
  15. return "返回结果已经被代理了";
  16. }else{
  17. return null;
  18. }
  19. }
  20. };
  21. //创建一个代理
  22. Object obj = Proxy.newProxyInstance(classLoader,interfaces,invocationHandler);
  23. return (T)obj;
  24. }

测试方法

  1. package Proxy;
  2. public interface MyFunction {
  3. String read();
  4. void write();
  5. }
  6. @Test
  7. public void test(){
  8. MyFunction mf = getFuncProxy(MyFunction.class);
  9. System.out.println(mf.read());
  10. mf.write();
  11. }

实现代码

  1. public <T> T getMapper(Class<?> cls){
  2. //此处提供代理Dao对象的功能,实现Dao层接口无需实现即可执行SQL语句的功能
  3. ClassLoader classLoader = cls.getClassLoader();
  4. Class[] interfaces = new Class[]{cls};
  5. InvocationHandler invocationHandler = new InvocationHandler() {
  6. /***
  7. * proxy -> 代理对象
  8. * method ->代理对象的方法
  9. * args -> 代理对象的方法的参数
  10. */
  11. @Override
  12. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  13. Annotation an = method.getAnnotations()[0]; //通过被代理的方法获取该方法的注解名
  14. Class<? extends Annotation> aClass = an.annotationType();//获取注解类型的类
  15. Method method1 = aClass.getMethod("value"); //通过反射该类调用value方法获取注解值
  16. String sql = (String)method1.invoke(an);
  17. if (Insert.class.equals(aClass)) {
  18. SqlSession. getInstance().insert(sql,args[0]);
  19. } else if (Select.class.equals(aClass)) {
  20. //a.第一步要判断是不是list类型
  21. System.out.println("log1->"+method.getReturnType());
  22. if(method.getReturnType() == List.class){
  23. //查询方法select2的参数为基本类型+Map+自定义类型,
  24. //所以需要获取被代理方法的返回类型才可以进行实体的映射创建
  25. //如果返回类型为List,那么就需要获取List中的范型类用于MapperClass的实体映射。
  26. Type wholeReturnType = method.getGenericReturnType(); //获取返回类型的全部->List<User>
  27. ParameterizedType realReturnTypes = (ParameterizedType)(wholeReturnType);
  28. Type realType = realReturnTypes.getActualTypeArguments()[0];
  29. System.out.println("getReturnType->"+method.getGenericReturnType()
  30. +",getGenericReturnType->"+wholeReturnType+",getActualTypeArguments->"+realType);
  31. if(args == null){
  32. return SqlSession.getInstance().select2(sql,(Class<?>) realType);
  33. }else{
  34. return SqlSession.getInstance().select2(sql,(Class<?>) realType,args[0]);
  35. }
  36. }else {
  37. //此时只需传入方法的返回值类型即可
  38. if(args == null){
  39. return SqlSession.getInstance().select2(sql,(Class<?>) method.getReturnType()).get(0);
  40. }else{
  41. return SqlSession.getInstance().select2(sql,(Class<?>) method.getReturnType(),args[0]).get(0);
  42. }
  43. }
  44. } else if (Delete.class.equals(aClass)) {
  45. SqlSession.getInstance().delete(sql,args[0]);
  46. } else if (Update.class.equals(aClass)) {
  47. SqlSession.getInstance().update(sql,args[0]);
  48. }else {
  49. System.err.println("未被注解定义的类型!");
  50. }
  51. return null;
  52. }
  53. };
  54. //开始创建类的代理
  55. Object obj = Proxy.newProxyInstance(classLoader,interfaces,invocationHandler);
  56. return (T)obj;
  57. }

加入Druid数据库连接线程池

通过静态代码块的方式,让该代码在类被加载时就执行来创建一个Properties对象

  1. {
  2. properties = new Properties();
  3. try {
  4. InputStream in = new BufferedInputStream(new FileInputStream("src/main/druid.properties"));
  5. properties.load(in);
  6. } catch (Exception e) {
  7. e.printStackTrace();
  8. }
  9. }

并由Druid自动创建线程池并获得

  1. public Connection getConnection() throws Exception {
  2. //通过druid工厂创建数据源
  3. dataSource = DruidDataSourceFactory.createDataSource(properties);
  4. return dataSource.getConnection();
  5. }
-- 完结 --