通过javassist操作字节码实现MyBatis动态生成DAO的实现类功能

说明

实践通过javassist方式实现mybatis通过接口操作数据增删改查的原理实现。在MyBatis框架中我们可以直接通过Dao接口和XML直接操作,而并没有具体的实现类,那么这个的原理是什么呢?mybatis帮我们简化了通用的实现类的代码,并通过字节码技术在运行期间根据接口和xml文件自动生成了对应的实现类。当前就通过javassist来实现类似的过程。
在这里插入图片描述

注意

【1】这里使用的javassist类是mybatis所自行封装提供的类,其所提供的api和javassist所提供的api相同。
【2】如果测试过程中代码没有问题但是执行ctClass.toClass()出现NullPointerException可以调整升级下mybatis的版本。

准备

【1】创建一个SpringBoot项目,并添加如下依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.13</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.12</version>
        </dependency>

【2】创建一个测试数据表employees,建表语句如下

CREATE TABLE `employees` (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
  `email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=25 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

【3】创建Dao接口

public interface EmployeeDao {
    
    

    Employee selectById(Long id);

    void insert(Employee employee);

    void update(Employee employee);

    void delete(Long employee);
}

【4】创建对应的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">

<mapper namespace="com.zy.mybatis.generatedaoproxy.EmployeeDao">
    <insert id="insert">
        insert into employees(name,email)
        values (#{
    
    name},#{
    
    email})
    </insert>

    <select id="selectById" resultType="com.zy.mybatis.generatedaoproxy.Employee">
        select * from employees where id=#{
    
    id}
    </select>

    <update id="update">
        update employees set name=#{
    
    name},email=#{
    
    email} where id =#{
    
    id}
    </update>

    <delete id="delete">
        delete from  employees  where id =#{
    
    id}
    </delete>
</mapper>

【5】创建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>
        <property name="driver" value="com.mysql.cj.jdbc.Driver" />
        <property name="url" value="jdbc:mysql://127.0.0.1:3306/test" />
        <property name="username" value="root" />
        <property name="password" value="root" />
    </properties>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC" />
            <dataSource type="POOLED">
                <property name="driver" value="${driver}" />
                <property name="url" value="${url}" />
                <property name="username" value="${username}" />
                <property name="password" value="${password}" />
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="mapper/EmployeeDao.xml" />
    </mappers>
</configuration>

【6】创建一个SqlSessionUtil,用于获取数据库连接


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;
import java.io.InputStream;


public class SqlSessionUtil {
    
    

    private SqlSessionUtil() {
    
    
    }

    private static SqlSessionFactory sqlSessionFactory;

    static {
    
    
        String resource = "mybatis-config.xml";
        InputStream inputStream = null;
        try {
    
    
            inputStream = Resources.getResourceAsStream(resource);
        } catch (IOException e) {
    
    
            e.printStackTrace();
        }
        sqlSessionFactory =
                new SqlSessionFactoryBuilder().build(inputStream);
    }

    private static ThreadLocal<SqlSession> t = new ThreadLocal<>();

    public static SqlSession openSession() {
    
    
        SqlSession session = t.get();
        if (session == null) {
    
    
            session = sqlSessionFactory.openSession(true);
            t.set(session);
        }
        return session;
    }
}

MyBatisGenerateDaoProxy工具

MyBatisGenerateDaoProxy是本次测试的主要类,通过mybatis的javassist实现通过接口和xml文件动态生成字节码文件并运行数据库操作。具体的过程详见代码注释。

import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.javassist.CannotCompileException;
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;

/**
 * 测试mybatis的javassist实现通过接口和xml文件动态生成字节码文件并运行数据库操作
 *
 * @author zhangyu
 * @date 2023/4/5
 */
@Slf4j
public class MyBatisGenerateDaoProxy {
    
    

    /**
     * 根据数据库连接和接口创建实现类
     */
    public static Object generate(SqlSession session, Class daoInterface) {
    
    
        // 类池
        ClassPool pool = ClassPool.getDefault();
        // 构造类,动态生成的实现类名称拼接Proxy字符串
        CtClass ctClass = pool.makeClass(daoInterface.getName() + "Proxy");
        // 制造接口
        CtClass ctInterface = pool.makeInterface(daoInterface.getName());
        ctClass.addInterface(ctInterface);
        // 反射获取目标接口的所有方法并去实现子类的逻辑
        Method[] declaredMethods = daoInterface.getDeclaredMethods();
        Arrays.stream(declaredMethods).forEach(method -> {
    
    
            try {
    
    
                StringBuffer methodCode = new StringBuffer();
                //添加修饰符
                methodCode.append("public ");
                //添加返回值
                methodCode.append(method.getReturnType().getName() + " ");
                methodCode.append(method.getName());
                methodCode.append("(");
                // 添加方法参数
                Class<?>[] parameterTypes = method.getParameterTypes();
                for (int i = 0; i < parameterTypes.length; i++) {
    
    
                    methodCode.append(parameterTypes[i].getName() + " ");
                    methodCode.append("arg").append(i);
                    if (i != parameterTypes.length - 1) {
    
    
                        methodCode.append(",");
                    }
                }
                methodCode.append("){");
                // 括号中间需要写对应的session.insert或session.select方法
                String sqlId = daoInterface.getName() + "." + method.getName();
                SqlCommandType sqlCommandType = session.getConfiguration().getMappedStatement(sqlId).getSqlCommandType();
                // com.zy.mybatis.generatedaoproxy.SqlSessionUtil 工具为自定义的获取数据库连接的方法
                methodCode.append("org.apache.ibatis.session.SqlSession session = com.zy.mybatis.generatedaoproxy.SqlSessionUtil.openSession();");
                // 针对增删改查调用Mybatis的手动处理API
                if (sqlCommandType == SqlCommandType.INSERT) {
    
    
                    methodCode.append(" session.insert(\"" + sqlId + "\", arg0);");
                }
                if (sqlCommandType == SqlCommandType.DELETE) {
    
    
                    methodCode.append(" session.delete(\"" + sqlId + "\", arg0);");
                }
                if (sqlCommandType == SqlCommandType.UPDATE) {
    
    
                    methodCode.append("return session.update(\"" + sqlId + "\", arg0);");
                }
                if (sqlCommandType == SqlCommandType.SELECT) {
    
    
                    String resultType = method.getReturnType().getName();
                    methodCode.append("return (" + resultType + ")session.selectOne(\"" + sqlId + "\", arg0);");
                }
                methodCode.append("}");
                log.info("动态生成的方法体内容信息:{}", methodCode);
                CtMethod ctMethod = CtMethod.make(methodCode.toString(), ctClass);
                ctClass.addMethod(ctMethod);
            } catch (CannotCompileException e) {
    
    
                e.printStackTrace();
            }
        });
        Object obj = null;
        try {
    
    
            Class<?> toClass = ctClass.toClass();
            log.info("动态生成的类:{}", toClass);
            obj = toClass.newInstance();
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
        return obj;
    }
}

模拟测试

通过以下CURD接口测试实现类接口是否可以正常运行

import org.springframework.web.bind.annotation.*;

/**
 * 测试通过字节码生成实现类操作数据库
 *
 * @author zhangyu
 * @date 2023/4/6
 */
@RestController
@RequestMapping("/emp")
public class EmployeeController {
    
    

    private EmployeeDao employeeDao = (EmployeeDao) MyBatisGenerateDaoProxy.generate(SqlSessionUtil.openSession(), EmployeeDao.class);

    @GetMapping("/{id}")
    public Object get(@PathVariable("id") Long id) {
    
    
        return employeeDao.selectById(id);
    }


    @PutMapping("/{id}")
    public Object update(@PathVariable("id") Long id) {
    
    
        employeeDao.update(new Employee(id, "zhangsan-new", "[email protected]"));
        return "OK";
    }

    @DeleteMapping("/{id}")
    public Object delete(@PathVariable("id") Long id) {
    
    
        employeeDao.delete(id);
        return "OK";
    }

    @PostMapping
    public Object insert() {
    
    
        employeeDao.insert(new Employee(100, "zhangsan", "[email protected]"));
        return "OK";
    }

}

注意上面的代码重点在于原来通过IOC注入的方式是由mybatis提供其实现类,但是现在是通过我们自定义的MyBatisGenerateDaoProxy工具类根据接口创建对应的实现类代码。

 EmployeeDao employeeDao = (EmployeeDao) MyBatisGenerateDaoProxy.generate(SqlSessionUtil.openSession(), EmployeeDao.class);

我们可以看下通过javassist操作字节码对dao接口中的方法以及XML代码中的SQL语句所生成的实现类方法内容,代码如下所示:

    public void update(com.zy.mybatis.generatedaoproxy.Employee arg0) {
    
    
        org.apache.ibatis.session.SqlSession session = com.zy.mybatis.generatedaoproxy.SqlSessionUtil.openSession();
        return session.update("com.zy.mybatis.generatedaoproxy.EmployeeDao.update", arg0);
    }

    public void delete(java.lang.Long arg0) {
    
    
        org.apache.ibatis.session.SqlSession session = com.zy.mybatis.generatedaoproxy.SqlSessionUtil.openSession();
        session.delete("com.zy.mybatis.generatedaoproxy.EmployeeDao.delete", arg0);
    }

    public void insert(com.zy.mybatis.generatedaoproxy.Employee arg0) {
    
    
        org.apache.ibatis.session.SqlSession session = com.zy.mybatis.generatedaoproxy.SqlSessionUtil.openSession();
        session.insert("com.zy.mybatis.generatedaoproxy.EmployeeDao.insert", arg0);
    }

    public com.zy.mybatis.generatedaoproxy.Employee selectById(java.lang.Long arg0) {
    
    
        org.apache.ibatis.session.SqlSession session = com.zy.mybatis.generatedaoproxy.SqlSessionUtil.openSession();
        return (com.zy.mybatis.generatedaoproxy.Employee) session.selectOne("com.zy.mybatis.generatedaoproxy.EmployeeDao.selectById", arg0);
    }

代码参考:

GitHub代码地址

猜你喜欢

转载自blog.csdn.net/Octopus21/article/details/129999122