实践通过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是本次测试的主要类,通过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代码地址