框架 - mybatis(动力节点:老杜从零学mybatis入门到架构思维)(一)

一、MyBatis概述

  1. 框架

    • java常用框架:SSM三大框架、SpringBoot、SpringCloud等。

    • 框架其实就是对通用代码的封装,提前写好了一堆接口和类,我们可以在做项目的时候直接引入这些接口和类(引入框架),基于这些现有的接口和类进行开发,可以大大提高开发效率。

    • 框架一般都以jar包的形式存在。(jar包中有class文件以及各种配置文件等。)

    • SSM三大框架的学习顺序:MyBatis、Spring、SpringMVC(仅仅是建议)

  2. 三层架构:

    • 表现层(UI):直接跟前端打交互(一是接收前端ajax请求,二是返回json数据给前端)

    • 业务逻辑层(BLL):一是处理表现层转发过来的前端请求(也就是具体业务),二是将从持久层获取的数据返回到表现层。

    • 数据访问层(DAL):直接操作数据库完成CRUD,并将获得的数据返回到上一层(也就是业务逻辑层)。

  3. JDBC的不足

    • SQL语句写死在Java程序中,不灵活。改SQL的话就要改Java代码。违背开闭原则OCP。
    • PrepareStatment给?传值是繁琐的。能不能自动化???
    • 将结果集封装成Java对象是繁琐的。能不能自动化???
  4. Mybatis是完全开源的,现在迁移到github上面了。可以到那儿去下载源码、jar包等:https://github.com/mybatis/mybatis-3

  5. MyBatis简介

    • MyBatis本质上就是对JDBC的封装,通过MyBatis完成CRUD。MyBatis在三层架构中负责持久层的,属于持久层框架。

    • MyBatis的发展历程:MyBatis本是apache的一个开源项目iBatis,2010年这个项目由apache software foundation迁移到了google code,并且改名为MyBatis。2013年11月迁移到Github。

    • iBatis一词来源于“internet”和‘batis”的组合,是一个基于Java的持久层框架。iBATIS提供的持久层框架包括SQL Maps和Data Access Objects (DAOs)。

    • 打开mybatis代码可以看到它的包结构中包含:ibatis

  6. ORM:对象关系映射

    • O (Object):Java虚拟机中的Java对象

    • R(Relational):关系型数据库

    • M (Mapping):将Java虚拟机中的Java对象映射到数据库表中一行记录,或是将数据库表中一行记录映射成Java虚拟机中的一个Java对象。

    • 示意图:

    • MyBatis框架就是一个ORM框架。MyBatis是一个半自动化的ORM,因为MyBatis框架中SQL语句是需要程序员自己编写的。

    • Hibernate框架就是一个全自动化的ORM。使用Hibernate框架的时候,不需要程序员手动编写SQL语句,SQL语句可以自动生成。所以Hibernate是一个完全的全自动化的ORM框架。

  7. MyBatis框架特点:

    • 支持定制化SQL、存储过程、基本映射以及高级映射
    • 避免了几乎所有的JDBC代码中手动设置参数以及获取结果集
    • 支持XML开发,也支持注解式开发。【为了保证sql语句的灵活,所以mybatis大部分是采用XML方式开发。】
    • 将接口和Java 的POJOs(Plain Ordinary Java Object,简单普通的Java对象)映射成数据库中的记录
    • 体积小好学:两个jar包,两个XML配置文件。
    • 完全做到sql解耦合。
    • 提供了基本映射标签。
    • 提供了高级映射标签。
    • 提供了XML标签,支持动态SQL的编写。

二、MyBatis入门程序

  1. maven项目的resources目录:放在这个目录当中的,一般都是资源文件,配置文件。直接放到resources目录下的资源,等同于放到了类的根路径下。

  2. 具体步骤(参考官网)

    • 打包方式jar(web项目才需要打包为war)

    • 引入依赖:mybatis依赖、mysql驱动依赖

      <dependency>
          <groupId>org.mybatis</groupId>
          <artifactId>mybatis</artifactId>
          <version>3.5.14</version>
      </dependency>
      <dependency>
          <groupId>mysql</groupId>
          <artifactId>mysql-connector-java</artifactId>
          <version>8.0.33</version>
      </dependency>
      
    • 编写mybatis核心配置文件:mybatis-config.xml,注意:

      • 这个文件名不是必须叫做mybatis-config.xml,可以用其他的名字。只是大家都采用这个名字。

      • 这个文件存放的位置也不是固定的,可以随意,但一般情况下,会放到类的根路径下。

      • 核心内容:

        <?xml version="1.0" encoding="UTF-8" ?>
        <!DOCTYPE configuration
                PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
                "http://mybatis.org/dtd/mybatis-3-config.dtd">
        <configuration>
            <!--environments中可以配置多个数据源,也就是多个environment-->
            <environments default="development">
                <!--一个environment代表了一个数据源-->
                <environment id="development">
                    <transactionManager type="JDBC"/>
                    <dataSource type="POOLED">
                        <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                        <property name="url" value="jdbc:mysql://localhost:3306/test"/>
                        <property name="username" value="root"/>
                        <property name="password" value=""/>
                    </dataSource>
                </environment>
            </environments>
        </configuration>
        
    • 编写XxxxMapper.xml配置文件:(文件名和文件存放位置不是一定的)。注意这个文件中的一个select、insert、update、delete标签,就相当于在原生的JDBC中的Statement对象。

      <?xml version="1.0" encoding="UTF-8" ?>
      <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
      <mapper namespace="abcd">
          <select id="insertCar">
              insert into t_car values (null,'1003','丰田霸道',30.0,'2000-10-11','燃油车')
          </select>
      </mapper>
      
    • 需要将XxxxMapper.xml文件与mybatis-config.xml文件关联起来,只需要在mybatis-config.xml文件中加入以下配置:(resource属性自动会从类的根路径下开始查找资源)

      <mappers>
          <!--resource属性自动会从类的根路径下开始查找资源。-->
          <mapper resource="mapper/TCarDao.xml"/>
      </mappers>
      
    • 在MyBatis当中,负责执行SQL语句的那个对象是SqlSession,SqlSession是专门用来执行SQL语句的,是一个Java程序和数据库之间的一次会话。

      • 要想获取SqlSession对象,需要先获取SqlSessionFactory对象,通过SqlSessionFactory工厂来生产SqlSession对象。

        SqlSessionFactory对象,就代表了一个environments下的environment,也就是代表了一个配置的数据源。

      • 怎么获取SqlSessionFactory对象呢?需要首先获取SqlSessionFactoryBuilder对象。

      • 通过SqlSessionFactoryBuilder对象的build方法,来获取一个SqlSessionFactory对象。

      • mybatis的核心对象包括:SqlSessionFactoryBuilder、SqlSessionFactory、SqlSession。(SqlSessionFactoryBuilder --> SqlSessionFactory --> SqlSession

    • 编写MyBatis程序。(使用mybatis的类库,编写mybatis程序,连接数据库,做增删改查就行了。)

      public static void main(String[] args) throws IOException {
          SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
          // Resources.getResourceAsStream默认就是从类的根路径下开始查找资源。
          InputStream in = Resources.getResourceAsStream("mybatis-config.xml");
          SqlSessionFactory sessionFactory = sqlSessionFactoryBuilder.build(in);
          SqlSession sqlSession = sessionFactory.openSession();
          // 注意这个参数。
          int insertCar = sqlSession.insert("insertCar");
          System.out.println(insertCar);
          // mybatis默认不会自动提交,需要手动提交。
          sqlSession.commit();
      }
      
  3. mybatis中有两个主要的配置文件:

    • mybatis-config.xml:这是核心配置文件,主要配置连接数据库的信息等。(一个)

    • xxxxMapper.xml:这个文件是专门用来编写SQL语句的配置文件。(一个表一个,也可以只使用一个配置文件,但是一般不这么干)

      • t_user表,一般会对应一个UserMapper.xml
      • t_student表,一般会对应一个StudentMapper.xml
  4. 关于第一个程序的小细节

    • mybatis中sql语句的结尾";"可以省略。

    • Resources.getResourceAsStream:以后凡是遇到resource这个单词,大部分情况下,这种加载资源的方式就是从类的根路径下开始加载。(开始查找)采用这种方式来获取资源,代码可移植性比较强。

    • InputStream is = new FileInputStream("d:\mybatis-config.xml"):采用这种方式也可以。可移植性太差,程序不够健壮。可能会移植到其他的操作系统当中。导致以上路径无效,还需要修改java代码中的路径。这样违背了OCP原则。

    • 上面提到的,XxxxMapper.xml可以放到任意路径下,这个时候要加载该文件,就无法使用上述提到的方式,从类路径中加载了,所以就需要使用另一种方式来加载文件。

      <mappers>
          <!--语法格式:file:///绝对路径-->
          <mapper url="file:///d:/CarMapper.xml"/>
      </mappers>
      
  5. 关于Mybatis的事务管理机制(可参考官网)

    • 在mybatis-config.xml文件中,可以通过以下的配置进行mybatis的事务管理

      <!--environments中可以配置多个数据源,也就是多个environment-->
      <environments default="development"> 
          <environment id="development">
              <transactionManager type="JDBC"/> <!--事务管理方式配置-->
              <dataSource type="POOLED">
                  <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                  <property name="url" value="jdbc:mysql://localhost:3306/test"/>
                  <property name="username" value="root"/>
                  <property name="password" value=""/>
              </dataSource>
          </environment>
      </environments>
      
    • type属性的值包括两个:JDBC(jdbc)、MANAGED(managed),在mybatis中提供了两种事务管理机制,默认使用JDBC事务处理机制。

    • JDBC事务管理器:mybatis框架自己管理事务,自己采用原生的JDBC代码去管理事务;

      例如SqlSession sqlSession = sessionFactory.openSession();这里会关闭事务自动提交,然后后面需要使用sqlSession.commit();手动提交事务。
      注意其实可以开启自动提交事务,也就是没有开启事务:SqlSession sqlSession = sessionFactory.openSession(true);

    • MANAGED事务管理器:mybatis不再负责事务的管理了。事务管理交给其它容器来负责。例如spring。对于我们当前的单纯的只有mybatis的情况下,如果配置为MANAGED,那么事务这块是没人管的。没有人管理事务表示事务压根没有开启(也就是不用手动提交事务了)。

  6. 设置(settings):这是 MyBatis 中极为重要的调整设置,可以调整mybatis的一些参数配置,它们会改变 MyBatis 的运行时行为

    <settings>
        <setting name="cacheEnabled" value="true"/>
    </settings>
    
  7. Mybatis集成日志logback

    • 作用:让logback日志框架打印mybatis执行日志,方便之后的调试。

    • mybatis常见的集成的日志组件有哪些呢?SLF4J(沙拉风)、LOG4J、LOG4J2、STDOUT_LOGGING

    • 其中STDOUT_LOGGING是标准日志,mybatis已经实现了这种标准日志。mybatis框架本身已经实现了这种标准。只要开启即可。怎么开启呢?在mybatis-config.xml文件中使用settings标签进行配置开启。

      <settings>
          <setting name="logImpl" value="STDOUT_LOGGING "/>
      </settings>
      
    • 以下示例集成第三方的日志框架logback:

    • 首先需要导入logback依赖

    • 配置:

      <settings>
          <setting name="logImpl" value="SLF4J"/>
      </settings>
      
    • 引入logback的xml配置文件

      • 这个配置文件的名字必须叫做logback.xml或者logback-test.xml,不能是其它的名字。

      • 这个配置文件必须放到类的根路径下,不能是其他位置。

    • 配置文件示例:

      <?xml version="1" encoding="UTF-8"?>
      <configuration debug="false">
          <!-- 控制台输出 -->
          <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
              <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
                  <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
                  <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
              </encoder>
          </appender>
          
          <!--mybatis log configure-->
          <logger name="com.apache.ibatis" level="TRACE"/>
          <logger name="java.sql.Connection" level="DEBUG"/>
          <logger name="java.sql.Statement" level="DEBUG"/>
          <logger name="java.sql.PreparedStatement" level="DEBUG"/>
          <!-- 日志输出级别,logback日志级别包括五个:TRACE < DEBUG < INFO < WARN < ERROR -->
          <root level="DEBUG">
              <appender-ref ref="STDOUT"/>
              <appender-ref ref="FILE"/>
          </root>
      </configuration>
      
    • 这样就可以了,会自动打印mybatis的执行日志。

三、使用MyBatis完成CRUD

  1. 在上述提到的例子中,使用的sql,是直接写死在文件中的,这在真实项目中,肯定是不对的。一般而言,是类似JDBC代码,使用PrepareStatement,执行带有占位符?的代码,然后再插入需要的参数替换占位符,这才是正确的方式。那么在mybatis中,如何来实现?

  2. 使用正确的方式来实现insert操作(使用Map来传递参数)

    • 首先是XxxxMapper.xml的编写,使用占位符:#{Map中存放的键值对的key值}

      <mapper namespace="abcd">
          <update id="insertCar">
              insert into t_car values (null,#{key1},#{key2},#{key3},#{key4},#{key5});
          </update>
      </mapper>
      
    • 使用Map来传递参数

      public static void main(String[] args) throws Exception {
          // SessionUtils是一个工具类,封装了获取SqlSession的过程。
          SqlSession sqlSession = SessionUtils.getSqlSession();
          // 使用map保存参数
          HashMap hashMap = new HashMap();
          hashMap.put("key1","1003");
          hashMap.put("key2","丰田霸道");
          hashMap.put("key3",30.0);
          hashMap.put("key4","2000-10-11");
          hashMap.put("key5","燃油车");
          // 使用map传递参数,#{key}将获取到map中的值
          int insertCar = sqlSession.insert("insertCar",hashMap);
          System.out.println(insertCar);
          sqlSession.commit();
          sqlSession.close();
      }
      
  3. 实现insert操作(使用实体类来传递参数)

    • 定义一个实体类用来封装参数

      @Data
      @NoArgsConstructor
      @AllArgsConstructor
      public class TCar {
          private Integer id;
          private String car_num;
          private String brand;
          private double guide_price;
          private String produce_time;
          private String car_type;
      }
      
    • XxxxMapper.xml的编写,使用占位符:#{实体类属性名}

      <update id="insertCar">
          insert into t_car values (null,#{car_num},#{brand},#{guide_price},#{produce_time},#{car_type});
      </update>
      
    • 使用实体类来传递参数

      public static void main(String[] args) throws Exception {
          // SessionUtils是一个工具类,封装了获取SqlSession的过程。
          SqlSession sqlSession = SessionUtils.getSqlSession();
          // 使用自定义的实体类来保存参数
          TCar tCar = new TCar(null,"1003","丰田霸道",30.0,"2000-10-11","燃油车");
      
          // 使用map传递参数,#{属性名}将获取到实体类中的对应的属性值
          int insertCar = sqlSession.insert("insertCar",tCar);
          System.out.println(insertCar);
          // 释放资源
          sqlSession.commit();
          sqlSession.close();
      }
      
    • 如果在XxxxMapper.xml文件中,在#{属性名}中,将属性名修改为实体类中没有的,会报错:There is no getter for property named 'xyz' in 'class com.powernode.mybatis.pojo.Car',意思是找不到实体类中的getter方法,也就是说:#{属性名}获取属性值的操作,其实是去调用类的get方法。

      严格意义上来说:如果使用POJO对象传递值的话,#{}这个大括号中到底写什么?
      写的是get方法的方法名去掉get,然后将剩下的单词首字母小写,然后放进去。
      例如:getUsername() --> #{username}
      例如:getEmail() --> #

  4. delete操作

    • 与插入操作是差不多的路数,没啥好说的。也就是最后换成使用SqlSession.delete()方法来执行sql就是了

    • 注意:如果占位符只有一个,那么#{}的大括号里可以随意。但是最好见名知意。

  5. update操作

    • 与插入操作是差不多的路数,没啥好说的。也就是最后换成使用SqlSession.update()方法来执行sql就是了

    • 同样可以选择使用pojo来封装参数,或者使用map集合。一般都会选择使用pojo的。

  6. selete:查询一行数据

    • 数据库中有一张表 t_car:

    • 与表t_car对应的实体类:注意此处guidePrice、produceTime属性与表中的表项名不一致

      @Data
      @NoArgsConstructor
      @AllArgsConstructor
      public class TCar {
          private Integer id;
          private String car_num;
          private String brand;
          private Double guidePrice;
          private String produceTime;
          private String car_type;
      }
      
    • 编写XxxMapper.xml:注意这里需要一个resultType的属性来指定查询之后的结果类型,因为SqlSession执行完毕之后,会返回ResultSet对象,然后将对象封装为某个数据类型,而如果没有指定要封装的类型,就会报错。

      <mapper namespace="abcd">
      
          <select id="seletById" resultType="shh.pojo.TCar">
              select * from t_car where id = #{id}
          </select>
      </mapper>
      
    • 执行:

      public class psvm {
          public static void main(String[] args) throws Exception {
              // SessionUtils是一个工具类,封装了获取SqlSession的过程。
              SqlSession sqlSession = SessionUtils.getSqlSession();
      
              Object car = sqlSession.selectOne("selectById", 1);
              System.out.println(car);
              sqlSession.close();
          }
      }
      
    • 注意执行结果:TCar(id=1, car_num=100, brand=宝马520Li, guidePrice=null, produceTime=null, car_type=燃油车)。会发现guidePrice、produceTime属性的值没有成功查询出来,显示为null

      分析:这是因为mybatis去数据库中查询表t_car,表中的属性为guide_price、priduce_time,与实体类中的对应不上,所以就封装失败了。

    • 解决上述问题:可以采用取别名的方式,将表中的属性名取别名,与实体类中的属性名对应上即可。

      <mapper namespace="abcd">
          <!--必须使用resultType属性来指定查询结果的类型-->
          <select id="selectById" resultType="shh.pojo.TCar">
              select id,car_num,brand,guide_price as guidePrice,produce_time as produceTime,car_type
              from t_car where id = #{id}
          </select>
      </mapper>
      
  7. select:查询所有

    • 执行时,需要使用sqlSession.selectList()方法,mybatis通过这个方法就可以得知你需要一个List集合。它会自动给你返回一个List集合。

    • 编写XxxMapper的时候,resultType还是指定要封装的结果集的类型。不是指定List类型,因为selectList方法默认就是返回一个List类型的,所以是指定List集合中元素的类型。

  8. 在XxxxMapper.xml文件当中有一个namespace,这个属性是用来指定命名空间的。用来防止id重复

    • XxxxMapper.xml示例:

      <mapper namespace="aaaa">
          <!--必须使用resultType属性来指定查询结果的类型-->
          <select id="selectById" resultType="shh.pojo.TCar">
              select id,car_num,brand,guide_price as guidePrice,produce_time as produceTime,car_type
              from t_car where id = #{id}
          </select>
      </mapper>
      
    • 假设还有另一个XxxMapper,里面的namspace属性是"bbbb",而且也有一个select标签,id为selectById。

    • 这个时候如果以上面的sqlSession.selectOne("selectById", 1);来执行的话,会报错,因为selectById这个id重复了,mybatis不知道应该去执行哪一个。正确的执行方式是:sqlSession.selectOne("aaaa.selectById", 1);

四、MyBatis核心配置文件详解

  1. configuration是根标签,所有的配置都需要在这个标签中进行,不能放到外面去。

  2. environments标签

    • environments标签配置数据源,里面的一个environment标签代表一个数据源

    • 下方是一个environments的示例,并配置了两个environment,id分别为代表了两个数据源test和smbms,配置默认使用test

      <!-- environments中配置数据源,一个environment代表一个数据源-->
      <!--default属性是配置默认使用哪一个environment,使用environment的id值即可。-->
      <environments default="test">
          <!-- environment代表一个数据源,也就是代表了一个数据库-->
          <environment id="test">
              <transactionManager type="JDBC"/> <!--事务管理方式配置-->
              <dataSource type="POOLED">
                  <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                  <property name="url" value="jdbc:mysql://localhost:3306/test"/>
                  <property name="username" value="root"/>
                  <property name="password" value=""/>
              </dataSource>
          </environment>
          
          <environment id="smbmd">
              <transactionManager type="JDBC"/>
              <dataSource type="POOLED">
                  <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                  <property name="url" value="jdbc:mysql://localhost:3306/smbms"/>
                  <property name="username" value="root"/>
                  <property name="password" value=""/>
              </dataSource>
          </environment>
      </environment>
      
    • 一个environment 代表一个SqlSessionFactory,可以直接使用默认数据源,也可以指定数据源。(也就是指定一个environment,通过environment的id值)

      // 获取一个使用默认数据源的SqlSessionFactory
      factory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml"));
      // 获取一个指定使用“test”数据源的SqlSessionFactory
      factory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml"),"test");
      
  3. environments标签:在environment中配置事务管理机制(可参考官网)

    • 在mybatis-config.xml文件中,可以通过transactionManager标签配置进行mybatis的事务管理

      <environment id="development">
          <transactionManager type="JDBC"/> <!--事务管理方式配置-->
          <dataSource type="POOLED">
              <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
              <property name="url" value="jdbc:mysql://localhost:3306/test"/>
              <property name="username" value="root"/>
              <property name="password" value=""/>
          </dataSource>
      </environment>
      
    • type属性的值包括两个:JDBC(jdbc)、MANAGED(managed),在mybatis中提供了两种事务管理机制,默认使用JDBC事务处理机制。

    • 在mybatis中提供了一个事务管理器接口:Transaction。该接口下有两个实现类:

      • 如果type="JDBC",那么底层会实例化JdbcTransaction对象。mybatic自己来管理事务

      • 如果type="MANAGED",那么底层会实例化ManagedTransaction。让第三方容器来管理事务。

  4. environments标签:datasource配置(可以参考官网)

    • 数据源概述:

      • dataSource被称为数据源,数据源实际上是一套规范。JDK中有这套规范:javax.sql.DataSource(这个数据源的规范实际上是JDK规定的)

      • dataSource作用是什么?为程序提供Connection对象。(但凡是给程序提供Connection对象的,都叫做数据源。)

      • 我们自己也可以编写数据源组件,只要实现javax.sql.DataSource接口就行了。实现接口当中所有的方法。这样就有了自己的数据源。有了自己的数据源就可以在datasource标签中配置

      • 常见的数据源组件有哪些呢【常见的数据库连接池有哪些呢】?阿里巴巴的德鲁伊连接池 druid、c3p0、dbcp........

    • datasource标签的type属性,type属性用来指定数据源的类型,就是指定具体使用什么方式来获取Connection对象。type属性有三个值(必须是三选一)。

      • UNPOOLED:不使用数据库连接池技术。每一次请求过来之后,都是创建新的Connection对象。

      • POOLED:使用mybatis自己实现的数据库连接池。

        使用连接池和不使用连接池的区别:不使用连接池的话,每次访问数据库都会新建一个连接,用完之后断开连接。使用连接池的话,会新建一些连接放到连接池中,当需要的时候就从连接池中获取连接,用完之后再放回池中即可,不需要经常进行新建、断开连接池的操作。
        而建立连接和释放连接很影响性能,所以使用连接池性能较高,并且连接池中的连接数量有限且可控,不会造成建立过多连接导致数据库服务器宕机等危险。

      • JNDI:集成其它第三方的数据库连接池,如druid、c3p0等等。

    • 使用POOLED类型的话,还需要配置一些连接池参数,参数配置好的话,可以提高一些效率,至于可以配置哪些参数,则可以从官网参考。至于参数要配置成什么,则需要依据显示情况来确定。

    • JNDI(java命名目录接口)是一套规范。谁实现了这套规范呢?大部分的web容器都实现了JNDI规范。例如Tomcat、Jetty、WebLogic、WebSphere,这些服务器(容器)都实现了JNDI规范

    • 不同的数据源类型可以需要配置不同的属性。因为Tomcat实现了JNDI规范,所以可以直接使用Tomcat中集成的数据源,所以这种数据源配置只需要两个属性:initial_context 、data_source (参考官网吧)

  5. properties标签

    • properties标签可以用于配置一些属性,这些属性可以在其它配置中获取,或者在程序中使用。

    • 使用示例:

      • 添加properties配置

        <!--java.util.Properties类。是一个Map集合。key和value都是String类型-->
        <!--在properties标签中可以配置很多属性-->
        <properties>
            <!--这是其中的一个属性-->
            <!--<property name="属性名"value="属性值"/>-->
            <property name="jdbc.driver" value="com.mysql.cj.jdbc.Driver"/>
            <property name="jdbc.url" value="jdbc:mysql://localhost:3306/test"/>
            <property name="jdbc.username" value="root"/>
            <property name="jqbc.password" value=""/>
        </properties>
        
      • 配置的属性value可以在其他配置中使用${key}值来获取,如下:

        <dataSource type="POOLED">
            <property name="driver" value="${jdbc.driver}"/>
            <property name="url" value="${jdbc.url}"/>
            <property name="username" value="${jdbc.url}"/>
            <property name="password" value=""/>
        </dataSource>
        
    • propertie的另一个用法(也是最实用的方法):

      • 在resource目录下新建一个jdbc.properties文件:

        jdbc.driver=com.mysql.cj.jdbc.Driver
        jdbc.url=jdbc:mysql://localhost:3306/test
        jdbc.username=root
        jdbc.password=""
        
      • 使用properties标签引用该文件,就可以达到直接扩展配置文件的效果:

        <!--只要出现resource属性,那么就是从根路径下开始查找资源的。-->
        <properties resource="jdbc.properties"/>
        
        <!--从绝对路径当中加载资源。绝对路径怎么写?file:///路径-->
        <properties url="" />
        
      • 使用方式与上面一样,就是使用${key}来获取配置的值即可。

五、手写MyBatis框架(掌握原理)

  1. dom4j解析XML:

    • dom4j是java中用于解析xml的技术,需要引入以下依赖:

      <!--dom4j的依赖-->
      <dependency>
          <groupId>org.dom4j</groupId>
          <artifactId>dom4j</artifactId>
          <version>2.1.3</version>
      </dependency>
      <!--jaxen依赖-->
      <dependency>
          <groupId>jaxen</groupId>
          <artifactId>jaxen</artifactId>
          <version>1.2.0</version>
      </dependency>
      
    • 用于解析的mybatis-config.xml文件为:

      <?xml version="1.0" encoding="UTF-8" ?>
      <!DOCTYPE configuration
              PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
              "http://mybatis.org/dtd/mybatis-3-config.dtd">
      <configuration>
      
          <properties resource="jdbc.properties"/>
          <environments default="test">
              <environment id="test">
                  <transactionManager type="JDBC"/>
                  <dataSource type="POOLED">
                      <property name="driver" value="${jdbc.driver}"/>
                      <property name="url" value="${jdbc.url}"/>
                      <property name="username" value="${jdbc.username}"/>
                      <property name="password" value="${jdbc.password}"/>
                  </dataSource>
              </environment>
              <environment id="smbms">
                  <transactionManager type="JDBC"/>
                  <dataSource type="POOLED">
                      <property name="driver" value="${jdbc.driver}"/>
                      <property name="url" value="jdbc:mysql://localhost:3306/smbms"/>
                      <property name="username" value="${jdbc.username}"/>
                      <property name="password" value="${jdbc.password}"/>
                  </dataSource>
              </environment>
          </environments>
      
          <mappers>
              <mapper resource="mapper/TCarDao.xml"/>
          </mappers>
      </configuration>
      
    • dom4j的基本使用示例:

      @Test
      public void test01() throws Exception {
          // SAXReader对象用于读取xml文件
          SAXReader reader = new SAXReader();
      
          // 通过字节流读取mybatis-config.xml文档。封装到Document对象中。
          Document document = reader.read(Resources.getResourceAsStream("mybatis-config.xml"));
      
          // 获取文档中的根标签
          Element rootElement = document.getRootElement();
          String name = rootElement.getName();
          System.out.println(name);  // 输出 configuration
      
          /*
              通过路径匹配获取定位XML文件中的元素。
              xpath是做标签路径匹配的。能够让我们快速定位XML文件中的元素。
              以下的xpath代表了:从根下开始找configuration标签,然后找configuration标签下的子标签environments
           */
          String path = "/configuration/environments";
          Element environments = (Element)document.selectSingleNode(path);
          String defaultEnvironment = environments.attributeValue("default"); // 获取default属性值
          System.out.println(defaultEnvironment);
      
          // 获取/configuration/environments/路径下的,id为的defaultEnvironment的environment
          path = "/configuration/environments/environment[@id='"+defaultEnvironment+"']";
          Element environment  = (Element) document.selectSingleNode(path);
          System.out.println(environment.attributeValue("id"));
      
          // 试用Element.element()方法获取获取子节点的对象
          Element dataSource = environment.element("dataSource");
          // 使用elements()方法获取所有子节点的集合
          List<Element> properties = dataSource.elements();
          for (Element property : properties) {
              System.out.println(property.attributeValue("value"));
          }
      
          // 获取所有的mapper标签
          // 不想从根下开始获取,你想从任意位置开始,获取所有的某个标签,xpath该这样写
          path = "//mapper";
          List<Node> nodes = document.selectNodes(path);
          for (Node node : nodes) {
              System.out.println(((Element)node).attributeValue("resource"));
          }
      }
      
  2. 具体实现:略;

六、在WEB中应用MyBatis

  1. 场景示例:实现转账交易,使用MVC架构,使用mybatis框架。

  2. 编写servlet:

    public class HelloServlet extends HttpServlet {
        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            resp.setContentType("text/html;charset=utf-8");
            resp.setCharacterEncoding("utf-8");
            String fromActno = req.getParameter("fromActno");
            String toActno = req.getParameter("toActno");
            String money = req.getParameter("money");
            AccountService accountService = new AccountServiceImpl();
            String transferResult = accountService.transfer(fromActno, toActno, Double.parseDouble(money));
            resp.getWriter().write(transferResult);
        }
    
        @Override
        protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            doGet(req, resp);
        }
    }
    
  3. 编写service层:注意提交事务的操作

    public class AccountServiceImpl implements AccountService {
    
        AccountDao accountDao = new AccountDaoImpl();
    
        public String transfer(String from, String to, Double money) {
            Account from_account = accountDao.selectByNo(from);
            Account to_account = accountDao.selectByNo(to);
            if (from_account == null || to_account == null){
                return "转账失败,账户不存在";
            }
            if (from_account.getBalance() < money) {
                return "余额不足,转个jj的账啊";
            }
            from_account.setBalance(from_account.getBalance()-money);
            to_account.setBalance(from_account.getBalance()+money);
            accountDao.updateBalanceByNo(from_account);
            accountDao.updateBalanceByNo(to_account);
            // 在这里提交事务,防止出现数据不一致
            SqlSessionUtil.getSqlSession().commit();
            return "ok";
        }
    }
    
  4. 编写一个创建SqlSession的工具类,使用了ThreadLocal,保证一个线程使用一个SqlSession,方便事务管理。

    public class SqlSessionUtil {
        private static SqlSessionFactory factory;
    
        // 管理事务的时候,需要让一个线程只使用一个SqlSession,所以这个时候就可以使用ThreadLocal来实现了。
        private static ThreadLocal<SqlSession> threadLocal = new ThreadLocal<SqlSession>();
        static {
            try {
                SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
                factory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml"));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        // 为当前线程创建sqlSession或者获取当前线程创建的sqlSession
        public static SqlSession getSqlSession(){
            SqlSession sqlSession = threadLocal.get();
            if (sqlSession == null){
                sqlSession = factory.openSession();
                threadLocal.set(sqlSession);
            }
            return sqlSession;
        }
    
        // 关闭sqlSession
        public static void close(SqlSession sqlSession){
            sqlSession.close();
            threadLocal.remove();
        }
    }
    
  5. 当然还有mybatis-config.xml,XxxxMapper.xml文件要实现了,此处略过

  6. 编写Dao层接口,并编写实现类:

    public class AccountDaoImpl implements AccountDao {
    
        public Account selectByNo(String no) {
            SqlSession sqlSession = SqlSessionUtil.getSqlSession();
            Account account = (Account) SqlSessionUtil.getSqlSession().selectOne("account.selectByNo",no);
            return account;
        }
    
        public int updateBalanceByNo(Account account) {
            SqlSession sqlSession = SqlSessionUtil.getSqlSession();
            int update = sqlSession.update("account.updateBalanceByNo",account);
            return update;
        }
    }
    
  7. 注意mybatis三个核心类的生命周期

    • SqlSessionFactoryBuilder:这个类可以被实例化、使用和丢弃,一旦创建了SqlSessionFactory,就不再需要它了。因此SqlSessionFactoryBuilder实例的最佳作用域是方法作用域(也就是局部方法变量,不要一直保留着它,以保证所有的XML解析资源可以被释放给更重要的事情。

    • SqlSessidnFactory:SqlSessionFactory一旦被创建就应该在应用的运行期间一直存在,没有任何理由丢弃它或重新创建另一个实例。使用SqlSessionFactory的最佳实践是在应用运行期间不要重复创建多次,多次重建SqlSessionFactory被视为一种代码“坏习惯”。因此SqlSessionFactory的最佳作用域是应用作用域。有很多方法可以做到,最简单的就是使用单例模式或者静态单例模式。

    • SqlSession:每个线程都应该有它自己的SqlSession实例。SqlSession的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。SqlSession 放在一个和HTTP请求相似的作用域中。换句话说,每次收到HTTP请求,就可以打开一个SqlSession,返回一个响应后,就关闭它。这个关闭操作很重要,为了确保每次都能执行关闭

  8. 分析Dao层实现类,发现接口的方法实现有一定的规律,就是获取SqlSession对象,然后执行XxxxMapper.xml中定义的sql,所以Dao层实现类理论上可以想办法自动生成,Mybatis中确实实现了自动生成Dao层的实现类,就是使用javassist组件。

  9. 到此处,总结mybatis的使用方式:

    • 编写mybatis-config.xml核心配置文件

    • 定义一个工具类获取SqlSession对象。

    • 定义XxxxDao接口,定义方法实现某个数据库操作

    • 定义XxxMapper.xml配置文件,编写sql

    • 定义XxxxDao接口的实现类,在类中实现接口定义的方法,实现方式一般是:使用SqlSession对象的update、delete、selectOne、selectList、insert等方法,指定sqlId执行对应的sql操作。

      public class AccountDaoImpl implements AccountDao {
          public Account selectByNo(String no) {
              SqlSession sqlSession = SqlSessionUtil.getSqlSession();
              Account account = (Account) sqlSession.selectOne("account.selectByNo",no);
              return account;
          }
      }
      

七、使用javassist生成类

  1. Javassist是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的Shigeru Chiba (千叶滋)所创建的。它已加入了开放源代码JBoss应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态"AOP"框架。

    与java反射机制的区别就是,java反射机制是去读取java字节码文件,而javassist可以实现创建、编辑、分析读取字节码文件。

  2. Mybatis底层使用javassist来实现上面提到的Dao层的实现类。

  3. 首先需要引入javassist依赖:

    <!--javassist依赖-->
    <dependency>
      <groupId>org.javassist</groupId>
      <artifactId>javassist</artifactId>
      <version>3.29.1-GA</version>
    </dependency>
    
  4. 基本使用示例:

    public static void main(String[] args) throws Exception{
        // 获取类池,这个类池就是用来给我生成class的
        ClassPool pool = ClassPool.getDefault();
        // 制造类(需要告诉javassist,类名是啥)
        CtClass newClass = pool.makeClass( "shh.dao.impl.AccountDaoImpl01");
        // 制造方法
        String methodCode = "public void insert(){System.out.println(123);}";
        CtMethod newMethod01= CtMethod.make(methodCode,newClass);
        // 将方法添加到类中
        newClass .addMethod(newMethod01);
        // 在内存中生成class(其实同时会将类加载到JVM中)
        newClass .toClass();
    
        // === 到这里,已经使用javassist完成类生成了。==========
    
        // 类加载到JVM当中,返回AccountDaoImpl类的字节码
        Class<?> clazz = Class.forName("shh.dao.impl.AccountDaoImpl01");
        // 创建对象
        Object obj = clazz.newInstance();
        // 获取AccountDaoImpl中的insert方法
        Method insertMethod = clazz.getDeclaredMethod( "insert");
        // 调用方法insert
        insertMethod.invoke(obj);
    }
    
  5. 生成一个接口的实现类:

    • 接口已经定义了:

      public interface AInterface {
          void add(String name,int age);
          int delete(String no);
          int update(String no);
          String query();
      }
      
    • 生成类代码:

      public static void main(String[] args) throws Exception{
          // 获取用于生成类的类池
          ClassPool classPool = ClassPool.getDefault();
          // 生成类
          CtClass newClass = classPool.makeClass("shh.dao.impl.AInterfaceImpl");
          // 生成接口
          CtClass aInterface = classPool.makeInterface("shh.dao.AInterface");
          // 生成的newClass继承aInterface接口
          newClass.addInterface(aInterface);
      
          Class<AInterface> interfaceClass = AInterface.class;
          Method[] interfaceMethods = interfaceClass.getMethods();
          // 开始编写实现类的方法
          for (Method method : interfaceMethods) {
              // 拼接方法代码
              StringBuilder methodCode = new StringBuilder();
              methodCode.append("public ");
              methodCode.append(method.getReturnType().getSimpleName()+" ");
              methodCode.append(method.getName()+"(");
              Class<?>[] parameterTypes = method.getParameterTypes();
              // 拼接参数列表
              for (int i = 0; i < parameterTypes.length; i++) {
                  methodCode.append(parameterTypes[i].getSimpleName());
                  methodCode.append(" arg"+i);
                  if (i != parameterTypes.length -1){
                      methodCode.append(",");
                  }
              }
              methodCode.append("){");
              methodCode.append("System.out.println(\"我是方法\");");
              // 处理返回值。
              if (method.getReturnType().getSimpleName().equals("int")) {
                  methodCode.append("return 11;");
              }else if (method.getReturnType().getSimpleName().equals("String")){
                  methodCode.append("return \"我是你爸爸\";");
              }
              // 完成方法代码的拼接。
              methodCode.append("}");
              // 生成方法,在类中加入方法
              CtMethod newMethod = CtMethod.make(methodCode.toString(), newClass);
              newClass.addMethod(newMethod);
          }
          // 在内存和JVM中生成类。
          Class<?> aInterfaceImpl = newClass.toClass();
          // 创建类实例,利用多态执行类方法。
          AInterface hh = (AInterface) aInterfaceImpl.newInstance();
          hh.query();
          hh.add("afds",12);
          hh.update("sf");
          hh.delete("sfs");
      }
      
  6. 使用javassist自定义生成XxxDao接口的实现类:(实际上也是mybatis的底层实现)

    public static Object getMapper(Class daoInterface){
    
        ClassPool classPool = ClassPool.getDefault();
        CtClass daoImpl = classPool.makeClass(daoInterface.getName() + "Impl");
        CtClass anInterface = classPool.makeInterface(daoInterface.getName());
        daoImpl.addInterface(anInterface);  // 实现类继承接口
        Method[] methods = daoInterface.getMethods();
    
        // 开始实现daoInterface的所有方法:
        for (Method method : methods) {
            // 拼接方法代码
            StringBuilder methodCode = new StringBuilder();
            methodCode.append("public ");
            methodCode.append(method.getReturnType().getName()+" ");
            methodCode.append(method.getName()+"(");
            Class<?>[] parameterTypes = method.getParameterTypes();
            // 拼接参数列表
            String params[] = new String[parameterTypes.length];
            for (int i = 0; i < parameterTypes.length; i++) {
                methodCode.append(parameterTypes[i].getName());
                params[i] = "arg"+i;
                methodCode.append(" arg"+i);
                if (i != parameterTypes.length -1){
                    methodCode.append(",");
                }
            }
            methodCode.append("){");
            methodCode.append("org.apache.ibatis.session.SqlSession sqlSession = shh.Utils.SqlSessionUtil.getSqlSession();");
            Configuration configuration = getSqlSession().getConfiguration();
            // 需要获取sql的id。mybatis中规定这个id是类名+方法名。
            String sqlId = daoInterface.getName()+"."+method.getName();
            // 需要知道是什么类型的sql语句
            // sql语句的id是框架使用者提供的,具有多变性。对于我框架的开发人员来说。我不知道。
            // 既然我框架开发者不知道sqlId,怎么办呢?mybatis框架的开发者于是就出台了一个规定:凡是使用GenerateDaoProxy机制的。
            // sqlId都不能随便写。namespace必须是dao接口的全限定名称。id必须是dao接口中方法名。
            SqlCommandType sqlType = configuration.getMappedStatement(sqlId).getSqlCommandType();
            String arg = "";
            for (String param : params) {
                arg = arg+","+param;
            }
    
            switch (sqlType){
                case DELETE:
                    methodCode.append("return ");
                    break;
                case INSERT:
                    methodCode.append("");
                    break;
                case SELECT:
                    methodCode.append("return ("+method.getReturnType().getName()+") sqlSession.selectOne(\""+sqlId+"\""+arg+");");
                    break;
                case UPDATE:
                    methodCode.append("return sqlSession.update(\""+sqlId+"\""+arg+");");
                    break;
            }
    
            // 完成方法代码的拼接。
            methodCode.append("}");
            System.out.println(methodCode.toString());
            // 生成方法,在类中加入方法
            CtMethod newMethod = null;
            try {
                newMethod = CtMethod.make(methodCode.toString(), daoImpl);
                daoImpl.addMethod(newMethod);
            } catch (CannotCompileException e) {
               e.printStackTrace();
            }
        }
    
        // 创建对象并返回。
        Object o = null;
        try {
            Class<?> aClass = daoImpl.toClass();
            o = aClass.newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return o;
    }
    
  7. 那么如果要使用mybatis实现的自动生成实现类的机制,就需要注意:sqlId都不能随便写。

    • namespace必须是dao接口的全限定名称。

    • id必须是对应的dao接口中方法名。

  8. 到此处,总结mybatis的使用方式:

    • 编写mybatis-config.xml核心配置文件

    • 定义一个工具类获取SqlSession对象。

    • 定义XxxxDao接口,定义方法实现某个数据库操作

    • 定义XxxMapper.xml配置文件,编写sql

    • 使用SqlSession对象,自动生成XxxxDao的实现类即可(不再使用SqlSession对象的update、delete、selectOne、selectList、insert等方法)

    • 不过需要注意:sqlId都不能随便写。namespace必须是dao接口的全限定名称。id必须是对应的dao接口中方法名。

posted @ 2026-03-10 18:48  哈哈嗨  阅读(2)  评论(0)    收藏  举报