javassist是什么?
Javassist是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶 滋)所创建的。它已加入了开放源代码JBoss 应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态AOP框架。javassist是jboss的一个子项目,其主要的优点,在于简单,而且快速。直接使用java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成类。它可以在一个已经编译好的类中添加新的方法,或者是修改已有的方法,并且不需要对字节码方面有深入的了解。同时也可以通过完全手动的方式去生成一个新的类对象。
首先引入依赖
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.29.1-GA</version>
</dependency>
使用javassist动态地在内存中生成一个类
public static void javassistCreateClass() throws Exception{
// 首先获取类池
ClassPool classPool = ClassPool.getDefault();
// 使用类池创建类
CtClass ctClass = classPool.makeClass("org.dongmu.mapper.CarMapperImpl");
// 创建方法
// 需要 返回值类型 方法名 形式参数列表 所属的类
CtMethod ctMethod = new CtMethod(CtClass.voidType,"execute",new CtClass[]{
},ctClass );
// 设置方法的修饰符列表
ctMethod.setModifiers(Modifier.PUBLIC);
ctMethod.setBody("System.out.println(\"Hello World\");");
// 上面只是设置了这个方法属于哪个类,只是方法知道了自己是哪个类的但是这个类不知道自己有这个方法,所以还要为这个类添加方法
ctClass.addMethod(ctMethod);
// 下面通过反射获取内存中生成的这个类然后调用类里面的方法
Class<?> aClass = ctClass.toClass();
Object o = aClass.newInstance();
Method method = aClass.getDeclaredMethod("execute");
// 调用类中的方法
method.invoke(o);
}
由于我这里使用的是spring17的版本所以这里如果允许以上代码需要添加两个参数
--add-opens java.base/java.lang=ALL-UNNAMED
--add-opens java.base/sun.net.util=ALL-UNNAMED
然后运行即可输出Hello World
javassist生成mapper接口的实现类
这里会首先和上面一样获取接口,然后创建实现类,遍历接口里面的方法然后在类中进行实现,只是在mybatis的mapper文件中,我们不知道用户的namespace是什么,所以获取不到对应的sql语句,这里就是因为获取不到,所以mybatis规定namespace必须是接口的全限定类名,并且要求mapper接口的路径和mapper.xml文件的路径要一样,接口里面的方法的名字必须和sql语句的id保持一致,这样我们就可以通过接口的信息以及接口中的方法命名获取到xml配置及文件中的内容了,这就是mybatis底层的实现原理。获取到了mappe文件中的标签封装成的MappedStatement对象了之后,就可以调用sqlsession进行相应的操作了。
public static Object javassistCreateClassImpl(Class mapperInterface) throws Exception{
// 获取类池
ClassPool pool = ClassPool.getDefault();
// ⽣成代理类
CtClass ctClass = pool.makeClass(mapperInterface.getPackageName() + ".impl." + mapperInterface.getSimpleName() + "Impl");
// 在内存中创建接口
CtClass ctClassInterface = pool.makeClass(mapperInterface.getName());
// 把代理类作为接口的实现类
ctClass.addInterface(ctClassInterface);
// 获取接口中的所有需要实现的方法
Method[] declaredMethods = mapperInterface.getDeclaredMethods();
// 在类中实现所有的方法
Arrays.stream(declaredMethods).forEach(method -> {
StringBuilder methodStr = new StringBuilder();
// 获取方法的返回值类型
String name = method.getReturnType().getName();
methodStr.append(name);
methodStr.append(" ");
// 获取方法名
String methodName = method.getName();
methodStr.append(methodName);
methodStr.append("(");
// 获取方法需要传递的参数
Class<?>[] parameterTypes = method.getParameterTypes();
for (int i = 0; i < parameterTypes.length; i++) {
// 拼接需要传递的形式参数的类型
methodStr.append(parameterTypes[i].getName());
methodStr.append(" arg");
methodStr.append(i);
if (i!=parameterTypes.length-1){
methodStr.append(",");
}
}
methodStr.append("){");
// 下面我们就需要写方法体中的参数了,但是具体的方法体中的内容是什么
// 在mybatis读取mapper的配置文件的时候,里面都是需要执行的sql语句,所以我们需要获取这些sql语句然后执行,但是怎么获取?
// 这里就是因为获取不到,所以mybatis规定namespace必须是接口的全限定类名,并且要求mapper接口的路径和mapper.xml文件的路径要一样,
// 接口里面的方法的名字必须和sql语句的id保持一致,这样我们就可以通过接口的信息以及接口中的方法命名获取到xml配置及文件中的内容了,这就是mybatis底层的原理
// 获取sql语句的id
String sqlId = mapperInterface.getName() + "." + methodName;
// 前面我们说过mybatis把读取到的mapper文件里面的内容都会封装成一个MappedStatement对象放在一个map集合当中。map集合的key就是这里的sqlId,
// 所以在实际的程序当中这里可以获取到MappedStatement对象然后执行对应的sql语句。
// 这里为了测试方便只是简单地实现接口里面的方法,但是其实在mybatis底层原理上就是这么实现的
methodStr.append("System.out.println(\"调用sqlSession实现了mapper配置文件中的sql语句\");}");
try {
// 创建这个类的方法
CtMethod ctMethod = CtMethod.make(methodStr.toString(), ctClass);
// 设置方法的修饰符
ctMethod.setModifiers(Modifier.PUBLIC);
// 将方法添加到类中
ctClass.addMethod(ctMethod);
} catch (CannotCompileException e) {
throw new RuntimeException(e);
}
});
// 创建代理对象
Class<?> aClass = ctClass.toClass();
Constructor<?> declaredConstructor = aClass.getDeclaredConstructor();
Object o = declaredConstructor.newInstance();
return o;
}
编写程序调用上面的函数进行测试,结果输出和预期相符。