Hibernate(2)——Hibernate的实现原理总结和对其模仿的demo
俗话说,自己写的代码,6个月后也是别人的代码……复习!复习!复习!涉及的知识点总结如下:
- 开源框架的学习思路(个人总结)
- Hibernate的运行原理总结
- Hibernate实现原理中的两个主要技术
- Java的反射技术的原理
- 反射的应用和例子
- 反射的缺点
- 编写一个模拟Hibernate的demo(V1.0版本)
- 后续的模拟ORM框架的设计思路
开源框架的学习思路(个人经验,欢迎提出意见)
框架是为了解决开发中遇到的一个个问题而诞生的,程序员是为了解决问题而学习框架的,这才是正确的学习之道!一个框架的好与坏完全取决于其对问题解决程度和解决方式。个人的学习过程:
- 了解框架能解决的问题(为什么使用这个框架,使用前后的差异),并先了解其实现原理(看悟性和基础)
- 阅读开源框架自带的帮助文档(结合一些参考书和网络的搜索去理解自己不懂的地方,实在不行再问高手,否则学不会思考)
- 很重要的一步:必须亲手搭建环境,可以运行开源框架自带的示例项目,也可以自己写一个demo,让它跑起来!不能纸上谈兵!
- 亲自写实际的Demo体验,带着问题研究开源框架的源代码,并总结收获。
- 创建配置文件,和实体类,并使用Configuration的对象去生成SessionFactory对象,(默认)加载hibernate.cfg.xml这个核心配置文件,该核心文件里包含了对数据源的连接和一些属性的设置等,当然还包含有映射实体关系配置文件。
1 // 通过new一个Configuration实例,然后用该实例去调用configure返回一个配置实例 2 Configuration configuration = new Configuration().configure(); 3 // 通过 配置实例的buildSessionFactory方法 生成一个 sessionFactory 对象 4 // buildSessionFactory方法会默认的去寻找配置文件hibernate.cfg.xml并解析xml文件 5 // 解析完毕生成sessionFactory,负责连接数据库 6 SessionFactory sessionFactory = configuration.buildSessionFactory();
2. 生成的SessionFactory,等于是可以获取数据库的连接(能创建session),从而可以操作数据库
3. 因为核心配置Hibernate.cfg.xml里引入了实体关系映射配置文件,故该文件也会自动被解析——加载对象-关系映射文件:vo类.hbm.xml
4、然后是创建session对象,通过SessionFactory创建session。session可以操作数据库
// 通过 sessionFactory 获得一个数据库连接 session,可以操作数据库 Session session = sessionFactory.openSession();
5. 开启事务,也是通过session开启
// 把操作封装到数据库的事务,则需要开启一个事务 Transaction transaction = session.beginTransaction();
6. 调用session API,CRUD 对象
// 一般把对实体类和数据库的操作,放到try-catch-finally块 try { User user = new User(); user.setUserId(22); user.setUsername("dashuai"); user.setPassword("123456"); // 把user对象插入到数据库 session.save(user); // 提交操作事务 transaction.commit(); LOG.info("transaction.commit(); ok"); } catch (Exception e) { // 提交事务失败,必须要回滚 transaction.rollback(); // 打印日志 LOG.error("save user error......", e); } finally { // 不能丢这一步,要释放资源 if (session != null) { session.close(); LOG.info("session.close(); ok"); } }
7. 根据Dialect(之前在核心配置文件配置的数据库方言)生成和底层数据库平台相关的sql代码
Hibernate实现原理中使用的技术有什么?
针对主流的XML文件配置方式,Hibernate实现原理中使用的关键技术主要有两个。一是对XML文档的解析——使用DOM(文档对象模型)/SAX解析,Hibernate使用了常见的开源解析工具——dom4j(使用Java编写,很流行),二是Java的反射技术,比如我可以通过一个Java类的对象,通过反射机制来获取这个对象的类的属性,方法……简单说,就类似我自己照镜子,通过镜子,我可以看清楚我自己身体的各个部位。
当然了,还有基于注解的方式,那么就还要使用Java的注解技术,本质上大同小异,熟能生巧。
Java反射技术浅析
大白话就是:Java反射机制可以让程序员在程序的运行期(Runtime)检查类,接口,变量以及方法的信息,而检查Java类的信息往往是在使用Java反射机制的时候所做的第一件事情,通过获取类的信息可以获取以下相关的内容:Class对象,类名,修饰符,包信息,父类,实现的接口,构造器,方法,变量,注解……除了这些内容,还有很多的信息可以通过反射机制获得(查阅API即可)。进一步反射还可以让程序员在运行期实例化对象,调用类的方法,通过调用get/set方法获取变量的值等,所以,Java的反射机制功能非常强大而且非常实用。举个例子,我可以用反射机制把Java对象映射到数据库表(Hibernate的实现机制之一),或者把脚本中的一段语句在运行期映射到相应的对象调用方法上,就如解析配置脚本时所做的那样。
Java反射机制的原理
这涉及到了Java的类加载机制和原理,稍后会专题总结。这里简单说下,在说原理之前,必须先知道Java中一般经常用Class.forName(classname)来反射类。在之前的几篇学习JVM总结随笔中也有部分说到:JVM装载某类时,类装载器会定位相应的class文件,然后将其读入到虚拟机中,并提取class中的类型信息,而Java中类的信息一般我们认为是存储到JVM的方法区中了。
Java反射机制中涉及的类:
- Class:类的实例,表示正在运行的 Java 应用程序中的类和接口,这个是反射机制中最关键的一点,可以使用Object类的getClass()方法,Class类的getSuperClass()方法,Class类的静态forName()方法,对于包装器类型,通过类名.TYPE属性得到类的Class类型信息。因为Class 类十分特殊,其实例用以表达Java程序运行时的类和接口。
- Field:提供有关类或接口的属性的信息,以及对它的动态访问权限
- Constructor:提供关于类的单个构造方法的信息以及对它的访问权限
- Method:提供关于类或接口上单独某个方法的信息
看个demo,新建一个类:dashuai.generics.Dog。该类做为我们的实验类,通过反射机制创建该类的对象,并通过反射机制调用该类中的speak方法。
1 public class Dog{ 2 public void speak(String str) { 3 System.out.println("Dog speak! 汪汪" + str); 4 } 5 }
在main方法里通过反射机制创建Dog类的对象,并调用其方法
public class Main { public static void main(String[] args) { Object obj = null; try { Class clazz = Class.forName("dashuai.generics.Dog"); obj = clazz.newInstance(); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } Dog dog = (Dog) obj; dog.speak(”Hi”); } }
上面代码中,dog对象是通过Class类的forName方法创建的,再调用该对象的speak方法,在控制台打印一行字符串。注释上面代码最后一行:dog.speak(),我使用反射来调用dog对象的speak()方法。该类完整代码如下:
1 public class Main { 2 public static void main(String[] args) { 3 Object obj = null; 4 5 try { 6 Class clazz = Class.forName("dashuai.generics.Dog"); 7 obj = clazz.newInstance(); 8 } catch (ClassNotFoundException e) { 9 e.printStackTrace(); 10 } catch (InstantiationException e) { 11 e.printStackTrace(); 12 } catch (IllegalAccessException e) { 13 e.printStackTrace(); 14 } 15 16 Dog dog = (Dog) obj; 17 Class<?>[] parameterTypes = new Class<?>[1]; 18 parameterTypes[0] = String.class; 19 20 try { 21 Method method = dog.getClass().getDeclaredMethod("speak", parameterTypes); 22 method.invoke(dog, new Object[] { "Hi" }); 23 } catch (SecurityException e1) { 24 e1.printStackTrace(); 25 } catch (NoSuchMethodException e1) { 26 e1.printStackTrace(); 27 } catch (IllegalArgumentException e) { 28 e.printStackTrace(); 29 } catch (IllegalAccessException e) { 30 e.printStackTrace(); 31 } catch (InvocationTargetException e) { 32 e.printStackTrace(); 33 } 34 } 35 }
通过java.lang.reflect.Method类来构建方法,再通过invoke方法执行dog对象的speak方法。
简单说说反射的执行过程
JVM装载类的目的就是把Java
字节代码转换成JVM
中的java.lang.Class
类的对象。这样Java就可以对该对象进行一系列操作,而上面的例子:Class.forName(classname)方法,实际上是调用了Class类中的 Class.forName(classname, true, currentLoader)方法。参数:name - 所需类的完全限定名;initialize - 是否必须初始化类;loader - 用于加载类的类加载器。currentLoader则是通过调用ClassLoader.getCallerClassLoader()获取当前类加载器的。类要想使用,必须用类加载器加载,所以需要加载器。
还有一点:反射机制不是每次都去重新反射,而是提供了缓存,每次都需要类加载器去自己的缓存中查找,如果可以查到,则直接返回该类。Java类加载器大体分两类:前三者是一类,分为BootStrap Class Loader(引导类加载器),Extensions Class Loader (扩展类加载器),App ClassLoader(或System Class Loader系统类加载器),最后一个是另一类叫用户自定义类加载器。
类的加载过程有两个比较重要的特征:层次组织结构和代理模式。
层次组织结构指的是每个类加载器都有一个父类加载器(除了引导类加载器之外),通过getParent()方法可以获取到。类加载器通过这种父亲-后代的方式组织在一起,形成树状层次结构。系统类加载器的父类加载器是扩展类加载器,而扩展类加载器的父类加载器是引导类加载器,对于开发人员编写的类加载器来说,其父类加载器是加载此类加载器 Java 类的类加载器。因为类加载器 Java 类如同其它的 Java 类一样,也是要由类加载器来加载的。一般来说,开发人员编写的类加载器的父类加载器是系统类加载器。类加载器通过这种方式组织起来,形成树状结构。树的根节点就是引导类加载器。如图:
代理模式则指的是一个类加载器既可以自己完成Java类的定义工作,也可以代理给其它的类加载器来完成。由于代理模式的存在,启动一个类的加载过程的类加载器和最终定义这个类的类加载器可能并不是一个。
Java类的加载过程:
1.通过类的全名产生对应类的二进制数据流。(如果没找到对应类文件,只有在类实际使用时才抛出错误。)
2.分析并将这些二进制数据流转换为方法区特定的数据结构(这些数据结构是实现有关的,不同 JVM 有不同实现)。这里处理了部分检验,比如类文件的魔数的验证,检查文件是否过长或者过短,确定是否有父类(除了 Obecjt 类)。
3.创建对应类的 java.lang.Class 实例(注意,有了对应的 Class 实例,并不意味着这个类已经完成了加载!)。
而JVM在整个加载过程中,会先检查类是否被已加载,检查顺序是自底向上,从系统类加载器到引导类加载器逐层检查,只要某个classloader已加载就视为已加载此类,保证此类只被所有ClassLoader加载一次。
但是加载的顺序是自顶向下(和检测顺序反着,属于父类优先的顺序),也就是由上层来逐层尝试加载此类。类加载器的详细介绍后续专题总结。
只说一点,ClassLoader的加载类过程主要使用loadClass方法,该方法中封装了加载机制:双亲委派模式。在forName方法中,就是调用了ClassLoader.loadClass方法来完成类的反射的,正如前面说的,JVM先检查自己是否已经加载过该类,如果加载过,则直接返回该类,若没有则调用父类的loadClass方法,如果父类中没有,则执行findClass方法去尝试加载此类,也就是我们通常所理解的片面的"反射"了。
这个过程主要通过ClassLoader.defineClass方法来完成。defineClass 方法将一个字节数组转换为 Class 类的实例(任何类都是Class类的对象,在Java中,每个class都有一个相应的Class对象,也就是说,当我们编写一个类.java文件,编译完成后,在生成的.class文件中,就会产生一个Class对象,用于表示这个类的类型信息,既一切皆是对象)。这种新定义的类的实例需要使用 Class.newInstance 来创建,而不能使用new来实例化。
大白话:运行期间,如果我们要产生某个类的对象,JVM 会检查该类型的Class对象是否已被加载。如果没有被加载,JVM会根据类的名称找到.class文件并加载它。一旦某个类型的Class对象已被加载到内存,就可以用它来产生该类型的所有对象。
再ps点:类加载器的用途
类加载器除了加载类信息,获取类信息之外,还有一个重要用途是在JVM
中为相同名称的Java类创建隔离空间。在JVM
中,判断两个类是否相同,不仅是根据该类的二进制名称,还需要根据两个类的定义类加载器。只有两者完全一样,才认为两个类的是相同的。因此,即便是同样的Java
字节代码,被两个不同的类加载器定义之后,所得到的Java
类也是不同的。如果试图在两个类的对象之间进行赋值操作,会抛出java.lang.ClassCastException
。这个特性为同样名称的Java
类在JVM
中共存创造了条件。在实际的应用中,可能会要求同一名称的Java
类的不同版本在JVM
中可以同时存在。通过类加载器就可以满足这种需求。这种技术在OSGi
中得到了广泛的应用。
反射的应用
- 操作数据库,动态创建SQL语句
- 解析XML,properties配置文件,动态生成对象
- Java的动态代理
- 框架中使用的最多:Struts框架、Spring框架、Hibernate框架、MyBatis框架……
反射的缺点
- 仿照Hibernate,也在项目根下定义个xml配置文件,我的叫Students.xml文件
- 再定义一个实体类Students,对照数据库生成对应的字段和set,get方法。
- 创建一个Session接口,其中使用dom4j解析xml配置文件,解析的数据保存到变量中,核心是解析读取类的对象的属性和二维表的字段的对应关系,我使用map对象保存。之后读取map对象,通过字符串的拼接等技巧,拼接一个插入数据到表的SQL语句—— insert into students(sname,sid) values (?,?)
- 编写一个save方法,参数传入需要保存的对象,比如student,这里面使用反射技术得到对象的类信息,再通过之前解析配置文件而得到的get方法名集合,利用反射得出get的返回类型,之后通过返回类型的判断结果,得出我们需要插入到数据库的数据是什么,再利用反射调用get方法,得到恰当的数据,依靠jdbc把数据保存到数据库。这里本质是利用反射对jdbc进行封装,这里当然是比较基础的封装,没有涉及复杂的功能和其他可能的映射情景。
- 使用了log4j记录日志
2016-03-08 22:28:51,424 | INFO | main | dao.Session.save(Session.java:188) | save
2016-03-08 22:28:51,428 | INFO | main | dao.Session.save(Session.java:190) | SQL: insert into students(sname,sid) values (?,?)
Process finished with exit code 0
打算一步步在总结框架的时候完善和重构一个能用的ORM框架。
设计思路:
第一点:连接数据库。第一种是JDBC连接,第二种是采用数据源来连接(采用数据源连接的时候,可以采用任何的数据源,c3p0,dbcp。)。
第二点:操作数据:添加数据,删除数据,修改数据。
第三点:查询数据。
开发思路:
- 当JVM启动的时候,首先读取配置文件(读取是一个效率很低的过程,需要考虑性能,避免重复读取)。解析的数据变量是一个Map,Map中包含的数据类型,不是简单的数据类型,而是对配置信息进行了封装以后的对象。当读取了配置文件之后,我就会知道是否配置了数据源,如果配置了数据源,那么就把配置的参数获取到,然后实例化,在得到数据库连接的时候,采用数据源来获取连接。如果没有配置数据源,那么就采用jdbc来连接。当两种配置都存在的时候,肯定是采用数据源来连接。
- save(Object o):插入数据,insert into 表名(字段1,字段2,……) values(值1,值2,……),save方法当中,必须拼接一条SQL语句,然后放到数据库中执行。
问题:拼接SQL语句的时候,表名从哪里来?字段从哪里来?值从哪里来?
<class name=”User” table=”user”>,name就代表了user这张表。table标签对应的值,就是表名。我们在配置中,会对每一个字段进行配置,那么我当然可以取到字段的名字。最重要的是,值是怎么来的。前面说了,利用Java所提供的反射机制来获取
- update,del更新,删除,这两个方法与上面的save方法类似。update方法的SQL语句是:update 表名 set 字段1=值1, 字段2=值2 where 条件,del对应的SQL语句:delete from 表名 where 条件。
问题:如果一个表中有4个字段,我只需修改1个字段,那么在修改的时候,只是针对于这一个字段给实体对象赋值,这个对象的其他的字段属性,都是null,这个时候,怎么样才能够只修改对应的字段?
- 查询数据:查询是最困难的!
拼接一条SQL语句,比如说:form User。但是,这条语句数据库是不认识的。数据库认识的是这样的:select * from 表名。当然,我也可以添加一些条件。把拼接好的SQL语句,放到关系数据库中取执行,得到的是结果集:ResultSet。这个方法,返回给用户的是一个List,是一个直接可以使用的列表,但是这个列表中会有很多很多的对象,每一个对象,又都有对应的值。当拿到结果集以后,遍历结果集,然后根据上下文(比如表),把查询出来的值,利用反射的方法设置到对象中,再把对象添加到列表中,最后返回列表。
这个方法中有很多的细节需要处理:比如说,当数据库中的表,不是单一的表,是有连接关系的时候,拼接SQL语句会比较麻烦,而且在添加数据到列表中的时候,需要进行的处理也会特别的多。还有条件查询的情景……
- 分页的处理。
- 查询的默认配置选择(无连接的查询),懒加载的设置。
- 缓存的设计(也是一个难点):想到可以使用静态的map变量来模拟缓存功能,利用多线程控制缓存,还想到Java的软引用是不是也能实现ORM的缓存?
- 要复合OOP的原则,尽量的抽象,抽象的层次越高,复用性就越强。
- 对一些设计模式的应用,比如单例模式,动态代理,享元模式,工厂模式,命令模式等。