MyBatis在web开发中使用面向接口的方式动态生成代理类的原理

开发web前的准备

完成数据库表的设计

MyBatis在web开发中使用面向接口的方式动态生成代理类的原理_第1张图片

向表中插入数据两条数据

MyBatis在web开发中使用面向接口的方式动态生成代理类的原理_第2张图片

功能页面

MyBatis在web开发中使用面向接口的方式动态生成代理类的原理_第3张图片

index.html

DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>银⾏账户转账title>
    head>
    <body>
        
        <form action="/bank/transfer" method="post">
            转出账户:<input type="text" name="fromActno"/><br>
            转⼊账户:<input type="text" name="toActno"/><br>
            转账⾦额:<input type="text" name="money"/><br>
            <input type="submit" value="转账"/>
        form>
    body>
html>

开发步骤

环境搭建

第一步: 创建Module , 使用Maven Archetype原型创建WEB应用mybatis-crud , 因为我们这个项⽬不使⽤JSP, 可以删除index.jsp⽂件

  • 自动生成的web.xml⽂件的版本较低,可以从tomcat10的样例⽂件中复制,然后修改

web.xml

  • metadata-complete属性: true表示只支持xml配置文件不支持注解 , false表示既支持xml配置文件又支持注解 , 我们一般改为false

<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
 https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
 version="5.0"
 metadata-complete="false">
web-app>

第二步: 配置 pom.xml 文件, 因为是web应用所以需要指定打包⽅式为war包 , 并导入相关依赖:mybatis依赖 , mysql驱动依赖 , logback依赖 ,servlet依赖

第三步: 配置Tomcat服务器,这⾥Tomcat使⽤10+版本 , 并部署应⽤到Tomcat


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w
                                                              3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://mave
                             n.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0modelVersion>
    <groupId>com.powernodegroupId>
    <artifactId>mybatis-004-webartifactId>
    <version>1.0-SNAPSHOTversion>
    <packaging>warpackaging>
    
    <name>mybatis-004-web Maven Webappname>
    <url>http://localhost:8080/bankurl>
    <properties>
        <project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
        <maven.compiler.source>17maven.compiler.source>
        <maven.compiler.target>17maven.compiler.target>
    properties>
    
    <dependencies>
        
        <dependency>
            <groupId>org.mybatisgroupId>
            <artifactId>mybatisartifactId>
            <version>3.5.10version>
        dependency>
        
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
            <version>8.0.30version>
        dependency>
        
        <dependency>
            <groupId>junitgroupId>
            <artifactId>junitartifactId>
            <version>4.13.2version>
            <scope>testscope>
        dependency>
        
        <dependency>
            <groupId>ch.qos.logbackgroupId>
            <artifactId>logback-classicartifactId>
            <version>1.2.11version>
        dependency>
        
        <dependency>
            <groupId>jakarta.servletgroupId>
            <artifactId>jakarta.servlet-apiartifactId>
            <version>5.0.0version>
            <scope>providedscope>
        dependency>
    dependencies>
    <build>
        <finalName>mybatis-004-webfinalName>
        <pluginManagement>
            <plugins>
                <plugin>
                    <artifactId>maven-clean-pluginartifactId>
                    <version>3.1.0version>
                plugin>
                <plugin>
                    <artifactId>maven-resources-pluginartifactId>
                    <version>3.0.2version>
                plugin>
                <plugin>
                    <artifactId>maven-compiler-pluginartifactId>
                    <version>3.8.0version>
                plugin>
                <plugin>
                    <artifactId>maven-surefire-pluginartifactId>
                    <version>2.22.1version>
                plugin>
                <plugin>
                    <artifactId>maven-war-pluginartifactId>
                    <version>3.2.2version>
                plugin>
                <plugin>
                    <artifactId>maven-install-pluginartifactId>
                    <version>2.5.2version>
                plugin>
                <plugin>
                    <artifactId>maven-deploy-pluginartifactId>
                    <version>2.8.2version>
                plugin>
            plugins>
        pluginManagement>
    build>
project>

第四步: 在类的根路径下(resources⽬录)创建配置文件: mybatis-config.xml , XxxMapper.xml , logback.xml

jdbc.properties

jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/powernode
jdbc.username=root
jdbc.password=root

**mybatis-config.xml **


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="dev">
        <environment id="dev">
            <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>
    environments>
    <mappers>
        
        <mapper resource="AccountMapper.xml"/>
    mappers>
configuration>

**AccountMapper.xml **


DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="account">
    <select id="selectByActno" resultType="com.powernode.bank.pojo.Account">
        select * from t_act where actno = #{actno}
    select>

    <update id="updateByActno">
        update t_act set balance = #{balance} where actno = #{actno}
    update>

mapper>

编写核心代码并控制事务

不同功能的类放在不同的包

  • 创建pojo包放实体类、service放业务类的接口、dao包放dao类的接口、web包放控制类、utils包放工具类、exceptions包放异常类

层与层使用接口进行衔接, 让层与层之间的耦合度降低

  • 在dao包和service包的子包impl包中提供service和dao接口的实现类 , 在控制层调用业务层,业务层调用持久层之间都是面向接口编程

java.lang.ThreadLocal类中里面有个Map集合 , 这个集合中的key是当前线程 , value是我们想要存储的对象

  • Map集合中的每个线程都有自己的对应的对象 , 需要的时候根据当前正在执行的线程获取对应的存储对象
  • 每次获取的存储对象是从ThreadLocal中的Map集合中拿的 , 但是第一次获取的时候需要我们手动创建并添加到Map集合中

在执行transfe转账⽅法时开启事务时需要保证service层开启的sqlSession对象和在Dao层执行SQL语句的是同一个sqlSession对象,才能真正控制事务

  • sqlSession对象的关闭也应该交给业务逻辑层 , 不能在DAO中的方法关闭

第一步: 编写使用ThreadLocal的SqlSessionUtil工具类

  • 放到ThreadLocal当中可以使一个线程对应一个SqlSession对象 , 保证service和dao中使⽤的SqlSession对象是同⼀个
  • 关闭sqlSession对象后要从大Map集合中移除 , 因为Tomcat服务器支持线程池 , 如果你不关闭 , 别人会拿到你已经关闭的Connection对象
public class SqlSessionUtil {
    private SqlSessionUtil(){}

    private static SqlSessionFactory sqlSessionFactory;

    static {
        try {
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    // 全局的,服务器级别的,一个服务器当中定义一个即可
    private static ThreadLocal<SqlSession> local = new ThreadLocal<>();

    /**
     * 获取会话对象。
     * @return 会话对象
     */
    public static SqlSession openSession(){
        SqlSession sqlSession = local.get();
        if (sqlSession == null) {
            sqlSession = sqlSessionFactory.openSession();
            // 将sqlSession对象绑定到当前线程上
            local.set(sqlSession);
        }
        return sqlSession;
    }

    /**
     * 关闭SqlSession对象(从当前线程中移除SqlSession对象)
     * @param sqlSession
     */
    public static void close(SqlSession sqlSession){
        if (sqlSession != null) {
            sqlSession.close();
            // 注意移除SqlSession对象和当前线程的绑定关系
            // 因为Tomcat服务器支持线程池,也就是说用过的线程对象t1,可能下一次还会使用这个t1线程
            local.remove();
        }
    }

}

第二步: 在pojo包下定义Account实体类封装账户数据

public class Account {
    private Long id;
    private String actno;
    private Double balance;
    @Override
    public String toString() {
        return "Account{" +
                "id=" + id +
                ", actno='" + actno + '\'' +
                ", balance=" + balance +
                '}';
    }

   //set和get以及toString方法
    public Account(Long id, String actno, Double balance) {
        this.id = id;
        this.actno = actno;
        this.balance = balance;
    }

    public Account() {
    }
}

第三步: 在dao包下编写AccountDao接口,dao的子包impl下编写它的实现类AccountDaoImpl(利用MyBatis执行SQL映射文件中的SQL语句)

public interface AccountDao {
    /**
     * 根据账号查询账户信息
     * @param actno 账号
     * @return 账户信息
     */
    Account selectByActno(String actno);

    /**
     * 更新账户信息
     * @param act  被更新的账户对象
     * @return 1表示更新成功,其他值表示失败。
     */
    int updateByActno(Account act);
   
    //增删的方法...
}
public class AccountDaoImpl implements AccountDao {
    @Override
    public Account selectByActno(String arg0) {
        SqlSession sqlSession = SqlSessionUtil.openSession();
        Account account = (Account) sqlSession.selectOne("account.selectByActno", actno);
        sqlSession.commit();
        sqlSession.close();
        return account;
    }

    @Override
    public int updateByActno(Account arg0) {
        SqlSession sqlSession = SqlSessionUtil.openSession();
        int count = sqlSession.update("account.updateByActno", act);
        sqlSession.commit();
        sqlSession.close();
        return count;
    }
}

第四步: 将自定义异常放在exception包下

//余额不足异常
public class MoneyNotEnoughException extends Exception{
    public MoneyNotEnoughException(){}
    public MoneyNotEnoughException(String msg){
        super(msg);
    }
}
//App异常
public class AppException extends Exception{
    public AppException(){}
    public AppException(String msg){
        super(msg);
    }
}

第五步: 在service包下编写AccountService 接口提供处理业务的方法,在service的子包impl下编写它的实现类AccountServiceImpl

  • 事务一定是在业务逻辑层进行控制 , 一般一个业务方法对应一个完整的事务(但也有一个业务方法调用其他业务方法的情况)
public interface AccountService {
    /**
     * @param fromActno 转出账号
     * @param toActno 转入账号
     * @param money 转账金额
     */
    
    //子类不能比父类抛出更宽泛的异常,子类抛父类必须也要抛
    void transfer(String fromActno, String toActno, double money) throws MoneyNotEnoughException, TransferException;
}
public class AccountServiceImpl implements AccountService {
    //业务类可能有多个业务方法,面向接口编程
    private AccountDao accountDao = new AccountDaoImpl();

    @Override
    public void transfer(String fromActno, String toActno, double money) throws MoneyNotEnoughException, TransferException {

        // 添加事务控制代码
        SqlSession sqlSession = SqlSessionUtil.openSession();

        // 1. 判断转出账户的余额是否充足(select)
        Account fromAct = accountDao.selectByActno(fromActno);
        if (fromAct.getBalance() < money) {
            // 2. 如果转出账户余额不足,提示用户
            throw new MoneyNotEnoughException("对不起,余额不足!");
        }
        // 3. 如果转出账户余额充足,更新转出账户余额(update)
        // 先更新内存中java对象account的余额
        Account toAct = accountDao.selectByActno(toActno);
        fromAct.setBalance(fromAct.getBalance() - money);
        toAct.setBalance(toAct.getBalance() + money);
        int count = accountDao.updateByActno(fromAct);

        // 模拟异常
        /*String s = null;
        s.toString();*/

        // 4. 更新转入账户余额(update)
        count += accountDao.updateByActno(toAct);
        if (count != 2) {
            throw new TransferException("转账异常,未知原因");
        }

        // 提交事务
        sqlSession.commit();
        // 关闭事务
        SqlSessionUtil.close(sqlSession);
    }
}

第六步: 在web包下定义AccountServlet类作为Controller,用来接收用户的请求,负责调度其他组件来完成任务,面向接口编程

@WebServlet("/transfer")
public class AccountServlet extends HttpServlet {
    // 为了让这个对象在其他方法中也可以用。声明为实例变量
    //面向接口编程
    private AccountService accountService = new AccountServiceImpl();

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        // 获取表单数据
        String fromActno = request.getParameter("fromActno");
        String toActno = request.getParameter("toActno");
        double money = Double.parseDouble(request.getParameter("money"));
        try {
            // 调用service的转账方法完成转账(调业务层)
            accountService.transfer(fromActno, toActno, money);
            // 程序能够走到这里,表示转账一定成功了
            // 调用View完成展示结果
            response.sendRedirect(request.getContextPath() + "/success.html");
        } catch (MoneyNotEnoughException e) {
            response.sendRedirect(request.getContextPath() + "/error1.html");
        } catch (TransferException e) {
            response.sendRedirect(request.getContextPath() + "/error2.html");
        } catch (Exception e){
            response.sendRedirect(request.getContextPath() + "/error2.html");
        }
    }
}

javassist动态⽣成XxxDaoImpl类

dao实现类中的⽅法代码很固定,通过SqlSession对象调⽤增删改查等⽅法,这个类中的⽅法没有任何业务逻辑,我们可以动态的⽣成这个类

使⽤javassist

Javassist是由东京⼯业⼤学的数学和计算机科学系的千叶滋所创建的⼀个开源的分析、编辑和创建Java字节码的类库 ,它已加⼊了开放源代码JBoss 应⽤服务器项⽬,通过使⽤Javassist对字节码操作为JBoss实现动态"AOP"框架

第一步:⾸先要引⼊javassist的依赖

<dependency>
    <groupId>org.javassistgroupId>
    <artifactId>javassistartifactId>
    <version>3.29.1-GAversion>
dependency>

解决运⾏时JDK版本过高的问题:加两个参数要不然会有异常, 第一次时点击Modify options->Add VM options

  • add-opens java.base/java.lang=ALL-UNNAMED
  • 环境变量: add-opens java.base/sun.net.util=ALL-UNNAMED

MyBatis在web开发中使用面向接口的方式动态生成代理类的原理_第4张图片

javassist的常用方法

ClassPool类制造类和接口

方法名 功能
public static ClassPool getDefault() 获取类池对象,这个类池就是用来给我生成class的
public CtClass makeClass(“类名”) 制造类 , 需要告诉javassist制造类的类名是啥
public CtClass makeInterface() 制造接口 , 需要告诉javassist制造接口的全类名是啥

CtMethod制造方法

方法名 功能
public static CtMethod make(“方法代码片段”,CtClass) 制造方法对象 , 第一个参数是方法的代码片段 , 第二个参数指定要把方法添加到哪个制造类中

CtClass接口实现类,方法添加到类

方法名
public CtClass addMethod(CtMethod) 将方法添加到类中 , 参数是要添加的方法对象
public CtClass toClass(); 在内存先中生成类手动类加载到Jvm执行 , 然后到制造类的字节码对象 , 也可以在内存中生成类的同时将生成的类加载到JVM当中的得到制造类的字节码对象 , 一步生成
public CtClass addInterface(ctInterface) 将接口添加到类中 , 参数是要添加的接口对象

在内存中制造类并创建方法

@Test
public void testGenerateFirstClass() throws Exception{
    // 获取类池,这个类池就是用来给我生成class的
    ClassPool pool = ClassPool.getDefault();
    
    // 制造类(需要告诉javassist,类名是啥)
    CtClass ctClass = pool.makeClass("com.powernode.bank.dao.impl.AccountDaoImpl");
    
    // 制造方法
    String methodCode = "public void insert(){System.out.println(123);}";
    CtMethod ctMethod = CtMethod.make(methodCode, ctClass);
    
    // 将方法添加到类中
    ctClass.addMethod(ctMethod);
    
    // 在内存中生成class
    ctClass.toClass();

    // 类加载到JVM当中,返回AccountDaoImpl类的字节码
    Class<?> clazz = Class.forName("com.powernode.bank.dao.impl.AccountDaoImpl");
    
    // 创建对象
    Object obj = clazz.newInstance();
    
    // 获取AccountDaoImpl中的insert方法
    Method insertMethod = clazz.getDeclaredMethod("insert");
    
    // 调用方法insert
    insertMethod.invoke(obj);
}

创建方法的另一种形式

// 创建⽅法
// 1.返回值类型 2.⽅法名 3.形式参数列表 4.所属类
CtMethod ctMethod = new CtMethod(CtClass.voidType, "execute", new
                                 CtClass[]{}, ctClass);
// 设置⽅法的修饰符列表
ctMethod.setModifiers(Modifier.PUBLIC);
// 设置⽅法体
ctMethod.setBody("{System.out.println(\"hello world\");}");

在内存中动态生成类并实现AccountDao接口的方法(知道接口中的方法)

public interface AccountDao {
    void delete();
} 
@Test
public void testGenerateImpl() throws Exception{
    // 获取类池
    ClassPool pool = ClassPool.getDefault();
    // 制造类
    CtClass ctClass = pool.makeClass("com.powernode.bank.dao.impl.AccountDaoImpl");
    // 制造接口
    CtClass ctInterface = pool.makeInterface("com.powernode.bank.dao.AccountDao");
    // 添加接口到类中,相当于让AccountDaoImpl implements AccountDao
    ctClass.addInterface(ctInterface);
    // 实现接口中的方法,这里假设我们已经知道接口中的方法
    // 制造方法
    CtMethod ctMethod = CtMethod.make("public void delete(){System.out.println(\"hello delete!\");}", ctClass);
    // 将方法添加到类中
    ctClass.addMethod(ctMethod);
    // 在内存中生成类,同时将生成的类加载到JVM当中
    Class<?> clazz = ctClass.toClass();
    //面向接口编程 , 强制类型转换
    AccountDao accountDao = (AccountDao)clazz.newInstance();
    accountDao.delete();
}

动态生成类并实现接口中的所有方法(不知道接口中的方法),但是方法中的代码片段固定

public interface AccountDao {
    void delete();
    int insert(String actno);
    int update(String actno, Double balance);
    String selectByActno(String actno)
}
@Test
public void testGenerateAccountDaoImpl() throws Exception{
    // 获取类池
    ClassPool pool = ClassPool.getDefault();
    // 制造类
    CtClass ctClass = pool.makeClass("com.powernode.bank.dao.impl.AccountDaoImpl");
    // 制造接口
    CtClass ctInterface = pool.makeInterface("com.powernode.bank.dao.AccountDao");
    // 实现接口
    ctClass.addInterface(ctInterface);
    // 要实现接口中所有的方法 , 先获取接口中所有的抽象方法
    Method[] methods = AccountDao.class.getDeclaredMethods();
    Arrays.stream(methods).forEach(method -> {
        // 实现接口中的抽象方法
        try {
            // public void delete(){}
            // public int update(String actno, Double balance){}
            StringBuilder methodCode = new StringBuilder();
            // 追加修饰符列表
            methodCode.append("public "); 
            // 追加返回值类型
            methodCode.append(method.getReturnType().getName()); 
            // 追加空格
            methodCode.append(" ");
            // 追加方法名
            methodCode.append(method.getName());
            
            // 拼接参数 String actno, Double balance
            methodCode.append("(");
            //parameterType是每一个形参的类型
            Class<?>[] parameterTypes = method.getParameterTypes();
            for (int i = 0; i < parameterTypes.length; i++) {
                Class<?> parameterType = parameterTypes[i];
                methodCode.append(parameterType.getName());
                methodCode.append(" ");
                methodCode.append("arg" + i);
                //如果不是最后一个形参就要在它的后面加上一个逗号
                if(i != parameterTypes.length - 1){
                    methodCode.append(",");
                }
            }
            
            //往方法体中添加代码
            methodCode.append("){System.out.println(11111); ");
            
            // 动态的添加return语句
            //获取返回值类型的简类名
            String returnTypeSimpleName = method.getReturnType().getSimpleName();
            
            //判断返回值类型
            if ("void".equals(returnTypeSimpleName)) {

            }else if("int".equals(returnTypeSimpleName)){
                methodCode.append("return 1;");
            }else if("String".equals(returnTypeSimpleName)){
                methodCode.append("return \"hello\";");
            }
            methodCode.append("}");
            System.out.println(methodCode);
            //制造方法
            CtMethod ctMethod = CtMethod.make(methodCode.toString(), ctClass);
            ctClass.addMethod(ctMethod);
        } catch (Exception e) {
            e.printStackTrace();
        }

    });

    // 在内存中生成class,并且加载到JVM当中
    Class<?> clazz = ctClass.toClass();
    // 创建对象
    AccountDao accountDao = (AccountDao) clazz.newInstance();
    // 调用动态生产类的方法
    accountDao.insert("aaaaa");
    accountDao.delete();
    accountDao.update("aaaa", 1000.0);
    accountDao.selectByActno("aaaa");
}

MyBatis生成DAO接口实现类原理

MyBatis号称轻量级 , 只需要一个jar包就行 , 所以它对javassist进行了二次包装

  • 凡是使用GenerateDaoProxy的,SQLMapper.xml映射文件中namespace必须是dao接口的全名,id必须是dao接口中的方法名

SqlSession类的方法

方法名 功能
getConfiguration() 获取MyBaits核心配置文件的configuration标签内的信息
getMappedStatement(sqlId) 通过sql语句id获取mapper配置文件中的sql语句
getSqlCommandType() 获取sql语句的枚举类型(增,删,改,查)

GenerateDaoProxy工具类(MyBatis框架的开发者已写好),可以动态生成dao接口实现类并创建对象返回, 生成类主要有两部分: 方法本身和方法中的代码片段

  • 方法中代码片段的数据类型必须使用全类名: 对于javassist来说必须要知道SqlSession是哪个包下的SqlSession类
  • 确定sql语句类型: 需要传入一个sqlSession对象 ,获取核心配置文件中mapper标签引入的sql语句
  • 确定sql语句id: id由框架使用者提供, 具有多变性, MyBatis框架的开发者规定所以凡是使用GenerateDaoProxy机制的,sqlId必须是dao接口中方法名,namespace必须是dao接口的全限定名称

遵守了MaBatis的规定创建XxxMapper.xml文件, 就可以使用它提供的GenerateDaoProxy机制,为你动态生成dao接口的实现类


DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">


<mapper namespace="com.powernode.bank.dao.AccountDao">
    
    <select id="selectByActno" resultType="com.powernode.bank.pojo.Account">
        select * from t_act where actno = #{actno}
    select>

    <update id="updateByActno">
        update t_act set balance = #{balance} where actno = #{actno}
    update>
mapper>
public class GenerateDaoProxy { 
    /**
      * @param daoInterface dao接口
      * @return dao接口实现类的实例化对象
      */
    public static Object generate(SqlSession sqlSession, Class daoInterface){
        // 类池
        ClassPool pool = ClassPool.getDefault();
        // 制造类(com.powernode.bank.dao.AccountDao --> com.powernode.bank.dao.AccountDaoProxy)
        // 实际本质上就是在内存中动态生成一个代理类
        CtClass ctClass = pool.makeClass(daoInterface.getName() + "Proxy"); 
        // 制造接口
        CtClass ctInterface = pool.makeInterface(daoInterface.getName());
        // 实现接口
        ctClass.addInterface(ctInterface);
        // 实现接口中所有的方法
        Method[] methods = daoInterface.getDeclaredMethods();
        // method是接口中的抽象方法
        Arrays.stream(methods).forEach(method -> {
            // 将method这个抽象方法进行实现
            try {
                // Account selectByActno(String actno)--->public Account selectByActno(String arg0){ 代码; }
                StringBuilder methodCode = new StringBuilder();
                methodCode.append("public ");
                methodCode.append(method.getReturnType().getName());
                methodCode.append(" ");
                methodCode.append(method.getName());
                methodCode.append("(");
                // 需要方法的形式参数列表
                Class<?>[] parameterTypes = method.getParameterTypes();
                for (int i = 0; i < parameterTypes.length; i++) {
                    Class<?> parameterType = parameterTypes[i];
                    methodCode.append(parameterType.getName());
                    methodCode.append(" ");
                    methodCode.append("arg" + i);
                    if(i != parameterTypes.length - 1){
                        methodCode.append(",");
                    }
                }
                methodCode.append(")");
                methodCode.append("{");

                //实现SqlSession sqlSession = SqlSessionUtil.openSession();对于javassist来说必须知道SqlSession是哪个包下的SqlSession类
 methodCode.append("org.apache.ibatis.session.SqlSession sqlSession = com.powernode.bank.utils.SqlSessionUtil.openSession();");
                
                // 实现return (接口的方法返回值类型) sqlSession.selectOne("sqlId", arg0),拼接方法时的形式参数用的是arg0
                // 想要知道是什么类型的sql语句,方法需要接收一个sqlSession对象
                // sql语句的id是框架使用者提供的,具有多变性,mybatis框架的开发者不知道sqlId
                // MyBatis规定凡是使用GenerateDaoProxy机制的,sqlId必须是dao接口中方法名,namespace必须是dao接口的全限定名称
                String sqlId = daoInterface.getName() + "." + method.getName();
                SqlCommandType sqlCommandType = sqlSession.getConfiguration().getMappedStatement(sqlId).getSqlCommandType();
                if (sqlCommandType == SqlCommandType.INSERT) {

                }
                if (sqlCommandType == SqlCommandType.DELETE) {

                }
                if (sqlCommandType == SqlCommandType.UPDATE) 
                    // 拼接方法时的形式参数用的是arg0,就一个参数
                    // return sqlSession.update("account.updateByActno", arg0);
                    methodCode.append("return sqlSession.update(\""+sqlId+"\", arg0);");
                }
                if (sqlCommandType == SqlCommandType.SELECT) {
                    // return (Account) sqlSession.selectOne("account.selectByActno", agr0);
                    String returnType = method.getReturnType().getName();
                    //sqlSession.selectOne(\""+sqlId+"\", arg0);")方法的返回值类型默认是object类型,需要我们强转
                    methodCode.append("return ("+returnType+")sqlSession.selectOne(\""+sqlId+"\", arg0);");
                }

                methodCode.append("}");
                CtMethod ctMethod = CtMethod.make(methodCode.toString(), ctClass);
                ctClass.addMethod(ctMethod);
            } catch (Exception e) {
                e.printStackTrace();
            }

        });

        // 创建对象
        Object obj = null;
        try {
            Class<?> clazz = ctClass.toClass();
            obj = clazz.newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return obj;
    }
}

**通过自己封装的GenerateDaoProxy工具类动态生成AccountDao的实现类AccountServiceImpl **

public class AccountServiceImpl implements AccountService {


    //面向接口编程,自己手动编写AccountDao的实现类
    //private AccountDao accountDao = new AccountDaoImpl();

    // 通过自己封装的GenerateDaoProxy工具类动态生成AccountDao的实现类
      private AccountDao accountDao = (AccountDao) GenerateDaoProxy.generate(SqlSessionUtil.openSession(), AccountDao.class);
 
    @Override
    public void transfer(String fromActno, String toActno, double money) throws MoneyNotEnoughException, TransferException {

        // 添加事务控制代码
        SqlSession sqlSession = SqlSessionUtil.openSession();

        // 1. 判断转出账户的余额是否充足(select)
        Account fromAct = accountDao.selectByActno(fromActno);
        if (fromAct.getBalance() < money) {
            // 2. 如果转出账户余额不足,提示用户
            throw new MoneyNotEnoughException("对不起,余额不足!");
        }
        // 3. 如果转出账户余额充足,更新转出账户余额(update)
        // 先更新内存中java对象account的余额
        Account toAct = accountDao.selectByActno(toActno);
        fromAct.setBalance(fromAct.getBalance() - money);
        toAct.setBalance(toAct.getBalance() + money);
        int count = accountDao.updateByActno(fromAct);

        // 模拟异常
        /*String s = null;
        s.toString();*/

        // 4. 更新转入账户余额(update)
        count += accountDao.updateByActno(toAct);
        if (count != 2) {
            throw new TransferException("转账异常,未知原因");
        }

        // 提交事务
        sqlSession.commit();
        // 关闭事务
        SqlSessionUtil.close(sqlSession);
    }
}

面向接口的方式进行CRUD

MyBatis的代理机制

在MyBatis当中提供了相关的机制, 可以动态为我们生成mapper接口的实现类(代理类实现了mapper接口中的所有方法)

  • 在使用MyBatis的项目中我们一般将dao包替换成mapper包 , Xxxdao接口一般写出Xxxmapper接口,创建SQL语句的映射文件XxxMapper.xml
  • 使用MyBatis代理机制的前提:SqlMapper.xml文件中namespace必须是mapper接口的全限定名称,id必须是mapper接口中的方法名

SqlSession类的方法

方法名 功能
getMapper() 在内存中生成dao接口的代理类,然后创建代理类的实例 , 参数是dao接口的字节码对象

封装汽车相关信息的pojo类

public class Car {
    // 数据库表当中的字段应该和pojo类的属性一一对应,建议使用包装类,这样可以防止null的问题
    private Long id;
    private String carNum;
    private String brand;
    private Double guidePrice;
    private String produceTime;
    private String carType;
    
   //set和get方法以及toString方法
    public Car(Long id, String carNum, String brand, Double guidePrice, String produceTime, String carType) {
        this.id = id;
        this.carNum = carNum;
        this.brand = brand;
        this.guidePrice = guidePrice;
        this.produceTime = produceTime;
        this.carType = carType;
    }
    public Car() {
    }
}

mapper接口负责表的CRUD

public interface CarMapper { 
    /**
     * 新增Car
     * @param car
     * @return
     */
    int insert(Car car);

    /**
     * 根据id删除Car
     * @param id
     * @return
     */
    int deleteById(Long id);

    /**
     * 修改汽车信息
     * @param car
     * @return
     */
    int update(Car car);

    /**
     * 根据id查询汽车信息
     * @param id
     * @return
     */
    Car selectById(Long id);

    /**
     * 获取所有的汽车信息。
     * @return
     */
    List<Car> selectAll();
}

SQL语句的映射文件XxxMapper.xml


DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.powernode.mybatis.mapper.CarMapper">
    <insert id="insert">
        insert into t_car values(null, #{carNum},#{brand},#{guidePrice},#{produceTime},#{carType})
    insert>
    <delete id="deleteById">
        delete from t_car where id = #{id}
    delete>
    <update id="update">
        update t_car set
        car_num=#{carNum},brand=#{brand},guide_price=#{guidePrice},produce_time=#{produceTime},car_type=#{carType}
        where id = #{id}
    update>
    <select id="selectById" resultType="com.powernode.mybatis.pojo.Car">
        select
            id,
            car_num as carNum,
            brand,
            guide_price as guidePrice,
            produce_time as produceTime,
            car_type as carType
        from t_car where id = #{id}
    select>
    <select id="selectAll" resultType="com.powernode.mybatis.pojo.Car">
        select
            id,
            car_num as carNum,
            brand,
            guide_price as guidePrice,
            produce_time as produceTime,
            car_type as carType
        from t_car
    select>
mapper>

面向接口编程完成的数据库表的CRUD

public class CarMapperTest {
    @Test
    public void testInsert(){
        SqlSession sqlSession = SqlSessionUtil.openSession();
        // 面向接口获取接口的代理对象 , 执行代理对象实现的方法
        // mapper实际上就是daoImpl对象,底层不但为CarMapper接口生成了字节码,并且还new实现类对象了
        CarMapper mapper = sqlSession.getMapper(CarMapper.class);
        Car car = new Car(null, "4444", "奔驰C200", 32.0, "2000-10-10", "新能源");
        int count = mapper.insert(car);
        System.out.println(count);
        sqlSession.commit();
    }
    @Test
    public void testDeleteById(){
        SqlSession sqlSession = SqlSessionUtil.openSession();
        CarMapper mapper = sqlSession.getMapper(CarMapper.class);
        int count = mapper.deleteById(154L);
        System.out.println(count);
        sqlSession.commit();
    }
    @Test
    public void testUpdate(){
        SqlSession sqlSession = SqlSessionUtil.openSession();
        CarMapper mapper = sqlSession.getMapper(CarMapper.class);
        Car car = new Car(155L, "2222", "凯美瑞222", 3.0, "2000-10-10", "新能源");
        mapper.update(car);
        sqlSession.commit();
    }
    @Test
    public void testSelectById(){
        SqlSession sqlSession = SqlSessionUtil.openSession();
        CarMapper mapper = sqlSession.getMapper(CarMapper.class);
        Car car = mapper.selectById(155L);
        System.out.println(car);
    }
    @Test
    public void testSelectAll(){
        SqlSession sqlSession = SqlSessionUtil.openSession();
        CarMapper mapper = sqlSession.getMapper(CarMapper.class);
        List<Car> cars = mapper.selectAll();
        cars.forEach(car -> System.out.println(car));
    }
}

你可能感兴趣的:(MyBatis,mybatis,前端,java)