1. 实现功能:52

银⾏账户转账

1.1 使⽤技术:

HTML + Servlet + MyBatis

1.2 WEB应⽤的名称:

bank

2. 数据库表的设计和准备数据  52

在web应用中使用mybatis_java

在web应用中使用mybatis_web_02

3. 注意MyBatis对象作⽤域以及事务问题   57

3.1 MyBatis核⼼对象的作⽤域   57

3.1.1 SqlSessionFactoryBuilder

这个类可以被实例化、使⽤和丢弃,⼀旦创建了 SqlSessionFactory,就不再需要它了。 因此 

SqlSessionFactoryBuilder 实例的最佳作⽤域是⽅法作⽤域(也就是局部⽅法变量)。 你可以重⽤

SqlSessionFactoryBuilder 来创建多个 SqlSessionFactory 实例,但最好还是不要⼀直保留着它,以保证所有的 XML 解析资源可以被释放给更重要的事情。

3.1.2 SqlSessionFactory

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

3.1.3 SqlSession

每个线程都应该有它⾃⼰的 SqlSession 实例。SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作⽤域是请求或⽅法作⽤域。 绝对不能将 SqlSession 实例的引⽤放在⼀个类的静态域,甚⾄⼀个类的实例变量也不⾏。 也绝不能将 SqlSession 实例的引⽤放在任何类型的托管作⽤域中,⽐如 Servlet 框架中的HttpSession。 如果你现在正在使⽤⼀种 Web 框架,考虑将 SqlSession 放在⼀个和 HTTP 请求相似的作⽤域中。 换句话说,每次收到 HTTP 请求,就可以打开⼀个 SqlSession,返回⼀个响应后,就关闭它。 这个关闭操作很重要,为了确保每次都能执⾏关闭操作,你应该把这个关闭操作放到 finally 块中。

3.2 事务问题   57

在之前的转账业务中,更新了两个账户,我们需要保证它们的同时成功或同时失败,这个时候就需要使⽤事务机制,在transfer⽅法开始执⾏时开启事务,直到两个更新都成功之后,再提交事务,我们尝试将transfer⽅法进

package com.powernode.bank.service.impl;

import com.powernode.bank.dao.AccountDao;
import com.powernode.bank.dao.impl.AccountDaoImpl;
import com.powernode.bank.exceptions.MoneyNotEnoughException;
import com.powernode.bank.exceptions.TransferException;
import com.powernode.bank.pojo.Account;
import com.powernode.bank.service.AccountService;
import com.powernode.bank.utils.SqlSessionUtil;
import org.apache.ibatis.session.SqlSession;

//这是业务层接口的实现类 负责处理业务逻辑  54-55
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)   55
        Account fromAct = accountDao.selectByActno(fromActno);
        if (fromAct.getBalance() < money) {
            // 2. 如果转出账户余额不足,提示用户
            throw new MoneyNotEnoughException("对不起,余额不足!");
        }
        // 3. 如果转出账户余额充足,更新转出账户余额(update)
        Account toAct = accountDao.selectByActno(toActno);

        // 先更新内存中java对象account的余额
        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);

    }
}

为了保证service和dao中使⽤的SqlSession对象是同⼀个,可以将SqlSession对象存放到

ThreadLocal当中。修改SqlSessionUtil⼯具类:

package com.powernode.bank.utils;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.IOException;

//mybatis的工具类  53
public class SqlSessionUtil {
    // 工具类的构造方法一般都是私有化的。
    // 工具类中所有的方法都是静态的,直接采用类名即可调用。不需要new对象。
    // 为了防止new对象,构造方法私有化。
    private SqlSessionUtil(){}

    private static SqlSessionFactory sqlSessionFactory;

    // 类加载时执行
    // SqlSessionUtil工具类在进行第一次加载的时候,解析mybatis-config.xml文件。创建SqlSessionFactory对象。
    static {
        try {
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }


    // 全局的,服务器级别的,一个服务器当中定义一个即可。    57
    // 为什么把SqlSession对象放到ThreadLocal当中呢?为了保证一个线程对应一个SqlSession。
    private static ThreadLocal local = new ThreadLocal<>();

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

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

还是转账失败但是数据没有问题

在web应用中使用mybatis_web_03

在web应用中使用mybatis_web_04

4. 分析当前程序存在的问题   58

4.1 分析AccountDapImpl

package com.powernode.bank.dao.impl;

import com.powernode.bank.dao.AccountDao;
import com.powernode.bank.pojo.Account;
import com.powernode.bank.utils.SqlSessionUtil;
import org.apache.ibatis.session.SqlSession;


//持久层接口实现类  54
public class AccountDaoImpl implements AccountDao {

    @Override
    public Account selectByActno(String actno) {
        SqlSession sqlSession = SqlSessionUtil.openSession();
        Account account = (Account) sqlSession.selectOne("account.selectByActno", actno);
        return account;
    }

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

我们不难发现,这个dao实现类中的⽅法代码很固定,基本上就是⼀⾏代码,通过SqlSession对象调⽤ insert、delete、update、select等⽅法,这个类中的⽅法没有任何业务逻辑,既然是这样,这个类我们 能不能动态的⽣成,以后可以不写这个类吗?答案:可以。

4.2 使⽤javassist⽣成类   58

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

4.2.1 Javassist的使⽤    59,63-64

我们要使⽤javassist,⾸先要引⼊它的依赖

<dependency>
 <groupId>org.javassist</groupId>
 <artifactId>javassist</artifactId>
 <version>3.29.1-GA</version>
</dependency>

4.2.2 利用Javassist实现在dao层的 AccountDao接口    62

接口如下:【包含参数和返回值类型】

package com.powernode.bank.dao;

import com.powernode.bank.pojo.Account;

/**
 * 持久层接口
 * 账户的DAO对象。负责t_act表中数据的CRUD.    54
 * 强调一下:DAO对象中的任何一个方法和业务不挂钩。没有任何业务逻辑在里面。
 * DAO中的方法就是做CRUD的。所以方法名大部分是:insertXXX deleteXXX updateXXX selectXXX
 */
public interface AccountDao {

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

    /**
     * 更新账户信息
     * @param act  被更新的账户对象
     * @return 1表示更新成功,其他值表示失败。
     */
    int updateByActno(Account act);

}

(2)我们通过 GenerateDaoProxy 的 generate 方法实现在内存中根据接口创建实现类

package com.powernode.bank.utils;


import org.apache.ibatis.javassist.ClassPool;
import org.apache.ibatis.javassist.CtClass;
import org.apache.ibatis.javassist.CtMethod;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.session.SqlSession;
import java.lang.reflect.Method;
import java.util.Arrays;

/**
 * 工具类:可以动态的生成DAO的实现类。(或者说可以动态生成DAO的代理类)    62
 * 注意注意注意注意注意!!!!!!:
 *      凡是使用GenerateDaoProxy的,SQLMapper.xml映射文件
 *      中namespace必须是dao接口的全名,id必须是dao接口中的方法名。
 */
public class GenerateDaoProxy { // 假想GenerateDaoProxy是mybatis框架的开发者写的。

    /**
     * 生成dao接口实现类,并且将实现类的对象创建出来并返回。
     * @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();
        Arrays.stream(methods).forEach(method -> {
            // method是接口中的抽象方法
            // 将method这个抽象方法进行实现
            try {
                // Account selectByActno(String actno);
                // public Account selectByActno(String actno){ 代码; }
                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("{");
                // 需要方法体当中的代码        63
                //// SqlSession sqlSession = SqlSessionUtil.openSession();
                //javassist需要知道你是哪个包所以这样写org.apache.ibatis.session.SqlSession
                methodCode.append("org.apache.ibatis.session.SqlSession sqlSession = com.powernode.bank.utils.SqlSessionUtil.openSession();");
                // 需要知道是什么类型的sql语句
                // sql语句的id是框架使用者提供的,具有多变性。对于我框架的开发人员来说。我不知道。
                // 既然我框架开发者不知道sqlId,怎么办呢?mybatis框架的开发者于是就出台了
                // 一个规定:凡是使用GenerateDaoProxy机制的。
                // sqlId都不能随便写。namespace必须是dao接口的全限定名称。id必须是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) {
                    methodCode.append("return sqlSession.update(\""+sqlId+"\", arg0);");
                }
                if (sqlCommandType == SqlCommandType.SELECT) {
                    String returnType = method.getReturnType().getName();
                    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;
    }

}

5. MyBatis中接⼝代理机制及使⽤  64

直接调⽤以下代码即可获取dao接⼝的代理类

AccountDao accountDao = (AccountDao)sqlSession.getMapper(AccountDao.class);

使⽤以上代码的前提是:AccountMapper.xml⽂件中的namespace必须和dao接⼝的全限定名称⼀致,id必须和dao接⼝中⽅法名⼀致。

5. 代码

com.powernode.bank.dao

持久层接口AccountDao

package com.powernode.bank.dao;

import com.powernode.bank.pojo.Account;

/**
 * 持久层接口
 * 账户的DAO对象。负责t_act表中数据的CRUD.    54
 * 强调一下:DAO对象中的任何一个方法和业务不挂钩。没有任何业务逻辑在里面。
 * DAO中的方法就是做CRUD的。所以方法名大部分是:insertXXX deleteXXX updateXXX selectXXX
 */
public interface AccountDao {

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

    /**
     * 更新账户信息
     * @param act  被更新的账户对象
     * @return 1表示更新成功,其他值表示失败。
     */
    int updateByActno(Account act);

}

com.powernode.bank.dao.impl

持久层AccountDaoImpl

分析dao中⾄少要提供⼏个⽅法,才能完成转账: 

转账前需要查询余额是否充⾜:selectByActno 

转账时要更新账户:update

package com.powernode.bank.dao.impl;

import com.powernode.bank.dao.AccountDao;
import com.powernode.bank.pojo.Account;
import com.powernode.bank.utils.SqlSessionUtil;
import org.apache.ibatis.session.SqlSession;


//持久层接口实现类  54
public class AccountDaoImpl implements AccountDao {

    @Override
    public Account selectByActno(String arg0) {
        SqlSession sqlSession = SqlSessionUtil.openSession();
//        Account account = (Account) sqlSession.selectOne("account.selectByActno", actno);
//        return account;
        return (Account) sqlSession.selectOne("account.selectByActno", arg0);
        //return (Account) sqlSession.selectOne("com.powernode.bank.dao.AccountDao.selectByActno", arg0);
    }

    @Override
    public int updateByActno(Account arg0) {
        SqlSession sqlSession = SqlSessionUtil.openSession();
//        int count = sqlSession.update("account.updateByActno", act);
//        return count;
        return sqlSession.update("account.updateByActno", arg0);
        //return sqlSession.update("com.powernode.bank.dao.AccountDao.updateByActno", arg0);
    }
}

com.powernode.bank.pojo

一个javabean    Account

package com.powernode.bank.pojo;

/**
 * 账户类,封装账户数据。   53
 * @author 动力节点
 */
public class Account {
    private Long id;
    private String actno;
    private Double balance;

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

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getActno() {
        return actno;
    }

    public void setActno(String actno) {
        this.actno = actno;
    }

    public Double getBalance() {
        return balance;
    }

    public void setBalance(Double balance) {
        this.balance = balance;
    }

    public Account(Long id, String actno, Double balance) {
        this.id = id;
        this.actno = actno;
        this.balance = balance;
    }

    public Account() {
    }
}

com.powernode.bank.service

业务层接口AccountService

package com.powernode.bank.service;

import com.powernode.bank.exceptions.MoneyNotEnoughException;
import com.powernode.bank.exceptions.TransferException;

//这是业务层的接口  54
//负责处理账户业务
public interface AccountService {
    /**
     * 账户转账业务。
     * @param fromActno 转出账号
     * @param toActno 转入账号
     * @param money 转账金额
     */
    void transfer(String fromActno, String toActno, double money) throws MoneyNotEnoughException, TransferException;
}

com.powernode.bank.service.impl

业务成AccountServiceImpl

package com.powernode.bank.service.impl;

import com.powernode.bank.dao.AccountDao;
import com.powernode.bank.dao.impl.AccountDaoImpl;
import com.powernode.bank.exceptions.MoneyNotEnoughException;
import com.powernode.bank.exceptions.TransferException;
import com.powernode.bank.pojo.Account;
import com.powernode.bank.service.AccountService;
import com.powernode.bank.utils.GenerateDaoProxy;
import com.powernode.bank.utils.SqlSessionUtil;
import org.apache.ibatis.session.SqlSession;

//这是业务层接口的实现类 负责处理业务逻辑  54-55
public class AccountServiceImpl implements AccountService {

    //private AccountDao accountDao = new AccountDaoImpl();

    //动态的获取dao实现类   64
    // 这是咱们自己封装的。
    //private AccountDao accountDao = (AccountDao) GenerateDaoProxy.generate(SqlSessionUtil.openSession(), AccountDao.class);

    // 在mybatis当中,mybatis提供了相关的机制。也可以动态为我们生成dao接口的实现类。(代理类:dao接口的代理)
    // mybatis当中实际上采用了代理模式。在内存中生成dao接口的代理类,然后创建代理类的实例。
    // 使用mybatis的这种代理机制的前提:SqlMapper.xml文件中namespace必须是dao接口的全限定名称,id必须是dao接口中的方法名。
    // 怎么用?代码怎么写?AccountDao accountDao = sqlSession.getMapper(AccountDao.class);
    private AccountDao accountDao = SqlSessionUtil.openSession().getMapper(AccountDao.class);

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

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

        // 1. 判断转出账户的余额是否充足(select)   55
        Account fromAct = accountDao.selectByActno(fromActno);
        if (fromAct.getBalance() < money) {
            // 2. 如果转出账户余额不足,提示用户
            throw new MoneyNotEnoughException("对不起,余额不足!");
        }
        // 3. 如果转出账户余额充足,更新转出账户余额(update)
        Account toAct = accountDao.selectByActno(toActno);

        // 先更新内存中java对象account的余额
        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);

    }
}

com.powernode.bank.web

表示层AccountServlet

package com.powernode.bank.web;

import com.powernode.bank.exceptions.MoneyNotEnoughException;
import com.powernode.bank.exceptions.TransferException;
import com.powernode.bank.service.AccountService;
import com.powernode.bank.service.impl.AccountServiceImpl;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

//表示层
//这是转账页面  54
@WebServlet("/transfer")
public class AccountServlet extends HttpServlet {

    // 为了让这个对象在其他方法中也可以用。声明为实例变量。
    private AccountService accountService = new AccountServiceImpl();

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        // 获取表单数据   54
        String fromActno = request.getParameter("fromActno");
        String toActno = request.getParameter("toActno");
        //Double.parseDouble这里涉及到了类型转换
        double money = Double.parseDouble(request.getParameter("money"));

        try {
            //调用service的转账方法完成转账(调业务层)    55
            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");
        }

    }
}

工具类com.powernode.bank.utils

SqlSessionUtil

package com.powernode.bank.utils;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.IOException;

//mybatis的工具类  53
public class SqlSessionUtil {
    // 工具类的构造方法一般都是私有化的。
    // 工具类中所有的方法都是静态的,直接采用类名即可调用。不需要new对象。
    // 为了防止new对象,构造方法私有化。
    private SqlSessionUtil(){}

    private static SqlSessionFactory sqlSessionFactory;

    // 类加载时执行
    // SqlSessionUtil工具类在进行第一次加载的时候,解析mybatis-config.xml文件。创建SqlSessionFactory对象。
    static {
        try {
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }


    // 全局的,服务器级别的,一个服务器当中定义一个即可。    57
    // 为什么把SqlSession对象放到ThreadLocal当中呢?为了保证一个线程对应一个SqlSession。
    private static ThreadLocal local = new ThreadLocal<>();

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

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

GenerateDaoProxy

package com.powernode.bank.utils;


import org.apache.ibatis.javassist.ClassPool;
import org.apache.ibatis.javassist.CtClass;
import org.apache.ibatis.javassist.CtMethod;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.session.SqlSession;
import java.lang.reflect.Method;
import java.util.Arrays;

/**
 * 工具类:可以动态的生成DAO的实现类。(或者说可以动态生成DAO的代理类)    62
 * 注意注意注意注意注意!!!!!!:
 *      凡是使用GenerateDaoProxy的,SQLMapper.xml映射文件
 *      中namespace必须是dao接口的全名,id必须是dao接口中的方法名。
 */
public class GenerateDaoProxy { // 假想GenerateDaoProxy是mybatis框架的开发者写的。

    /**
     * 生成dao接口实现类,并且将实现类的对象创建出来并返回。
     * @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();
        Arrays.stream(methods).forEach(method -> {
            // method是接口中的抽象方法
            // 将method这个抽象方法进行实现
            try {
                // Account selectByActno(String actno);
                // public Account selectByActno(String actno){ 代码; }
                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("{");
                // 需要方法体当中的代码        63
                //// SqlSession sqlSession = SqlSessionUtil.openSession();
                //javassist需要知道你是哪个包所以这样写org.apache.ibatis.session.SqlSession
                methodCode.append("org.apache.ibatis.session.SqlSession sqlSession = com.powernode.bank.utils.SqlSessionUtil.openSession();");
                // 需要知道是什么类型的sql语句
                // sql语句的id是框架使用者提供的,具有多变性。对于我框架的开发人员来说。我不知道。
                // 既然我框架开发者不知道sqlId,怎么办呢?mybatis框架的开发者于是就出台了
                // 一个规定:凡是使用GenerateDaoProxy机制的。
                // sqlId都不能随便写。namespace必须是dao接口的全限定名称。id必须是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) {
                    methodCode.append("return sqlSession.update(\""+sqlId+"\", arg0);");
                }
                if (sqlCommandType == SqlCommandType.SELECT) {
                    String returnType = method.getReturnType().getName();
                    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;
    }

}

com.powernode.bank.exceptions

MoneyNotEnoughException

package com.powernode.bank.exceptions;

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

TransferException

package com.powernode.bank.exceptions;

/**
 * 转账异常
 */
public class TransferException extends Exception{
    public TransferException(){}
    public TransferException(String msg){

    }
}

resources目录下

AccountMapper.xml

<?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">


<!--sqlMapper.xml文件的编写者,提供者是谁?使用你mybatis框架的java程序员负责提供的。-->
<!--要想使用这种机制:namespace必须是dao接口的全限定名称。-->
<mapper namespace="com.powernode.bank.dao.AccountDao">

    <!--要想使用这种机制:id必须是dao接口的方法名。-->
    <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>

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="powernodeDB">
        <environment id="powernodeDB">
            <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>

logback.xml

<?xml version="1.0" 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>[%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>

jdbc.properties

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

web.xml

注意metadata-complete="false"为false是支持注解也支持配置文件,为true时表示只支持配置文件

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
                      http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0"
         metadata-complete="false">

    
</web-app>

error1.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>转账报告</title>
</head>
<body>
<h1>余额不足!!!</h1>
</body>
</html>

error2.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>转账报告</title>
</head>
<body>
<h1>转账失败,未知原因!!!</h1>
</body>
</html>

success.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>转账报告</title>
</head>
<body>
<h1>转账成功!</h1>
</body>
</html>

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>

pom.xml

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.powernode</groupId>
  <artifactId>course10</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>war</packaging>

  <name>course10 Maven Webapp</name>
  <!-- FIXME change it to the project's website -->
  <url>http://www.example.com</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.7</maven.compiler.source>
    <maven.compiler.target>1.7</maven.compiler.target>
  </properties>

  <dependencies>
    <!--mybatis依赖-->
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis</artifactId>
      <version>3.5.10</version>
    </dependency>
    <!--mysql驱动依赖-->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>8.0.30</version>
    </dependency>
    <!--logback依赖-->
    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-classic</artifactId>
      <version>1.2.11</version>
    </dependency>
    <!--servlet依赖-->
    <dependency>
      <groupId>jakarta.servlet</groupId>
      <artifactId>jakarta.servlet-api</artifactId>
      <version>5.0.0</version>
      <scope>provided</scope>
    </dependency>
  </dependencies>

  <build>
    <finalName>course10</finalName>
    <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
      <plugins>
        <plugin>
          <artifactId>maven-clean-plugin</artifactId>
          <version>3.1.0</version>
        </plugin>
        <!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging -->
        <plugin>
          <artifactId>maven-resources-plugin</artifactId>
          <version>3.0.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>3.8.0</version>
        </plugin>
        <plugin>
          <artifactId>maven-surefire-plugin</artifactId>
          <version>2.22.1</version>
        </plugin>
        <plugin>
          <artifactId>maven-war-plugin</artifactId>
          <version>3.2.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-install-plugin</artifactId>
          <version>2.5.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-deploy-plugin</artifactId>
          <version>2.8.2</version>
        </plugin>
      </plugins>
    </pluginManagement>
      <plugins>
          <plugin>
              <groupId>org.apache.maven.plugins</groupId>
              <artifactId>maven-compiler-plugin</artifactId>
              <configuration>
                  <source>8</source>
                  <target>8</target>
              </configuration>
          </plugin>
      </plugins>
  </build>
</project>