前言
最近由于公司项目的需要,对Redisson进行重新封装,封装的同时需要将日志和报警监控等信息暴露给监控平台。但是由于封装的方法较多,如果对每个方法都使用try-catch盖住的话,代码无疑会显得非常臃肿。因此,考虑使用字节码注入的方式实现无入侵插入日志。
实现过程
字节码注入
- 通过CtMethod获取所需要注入代码的返回值
- 通过methodName + "(?);\n" 实现对未注入之前的方法的调用。 在注入的时候,查阅资料发现大部分的注入都是针对无返回值的注入方式。但是如果需要用try-catch盖住每个方法,那么势必会出现有返回的值的方式。因此可以在调用原来的方法之前先声明一个常量,然后通过1中的方法进行初始化,记住一定要给初始化,因为在idea中声明一个变量的时候,不初始化好像程序也不会报错,那是因为JVM帮助做了初始化,这个部分可以参考JVM中类的生命周期。而在Javassist中,Class本来就已经在JVM中了,也就不会在给默认值了。 然后通过body.append(type + " result = null;\n")这样的方式,可以获取未注入之前的运行结果,进一步可以在finally块结束之后返回。
- 在StringBuffer中拼接所需要注入的代码。 经历过以上3个步骤之后,代码注入的过程就完成了。
private static void injectCode(StringBuffer body, String methodName /*原来方法中的代码*/, boolean returnVoid,CtMethod mold) throws NotFoundException {
String type = mold.getReturnType().getName();
body.append("{ int res = 7;\n");
body.append(" long start = System.currentTimeMillis();\n" +
" long cost = -1;\n" +
" long end = start;\n" +
" boolean isSuccess = true;\n" +
" System.out.println(System.currentTimeMillis()); \n" );
if (!type.equals("void")){
if (type.equals("boolean")){
body.append(type + " result = false;\n");
}
else if (type.equals("String")){
body.append(type + " result = null;\n");
}
else if (type.equals("long")){
body.append(type + " result = 0;\n");
}
else if (type.equals("int")){
body.append(type + " result = 0;\n");
}
else{
body.append(type + " result = null;\n");
}
}
body.append("\n try{" + "\n");
// 调用原有代码,类似于method();(?)表示所有的参数
if (!"void".equals(type)){
body.append("result = ");
}
body.append(methodName + "(?);\n");
body.append("}catch (Exception e){\n" +
" e.printStackTrace();" +
" }finally {\n" +
" end = System.currentTimeMillis();\n" +
" cost = start - end;\n" +
" System.out.println(res + 111111111);"+
" }");
if (!"void".equals(type)){
body.append("return result;\n");
}
body.append("}");
}
字节码替换
但是显然刚刚仅仅是完成了向目标方法中添加了所需要的代码。那么注入之后的代码又该如何生效呢? CtMethod方法提供了一个setBody的方法可以将上一过程中的代码插入原来的方法中。并通过CtClass添加一个新的方法。
private static void addLog(String methodName,CtClass ctClass) throws CannotCompileException, NotFoundException, IllegalAccessException, InstantiationException {
CtMethod mold = ctClass.getDeclaredMethod(methodName);
// 修改原有的方法名称
String newName = methodName + "$impl";
mold.setName(newName);
//创建新的方法,复制原来的方法
// 主要的注入代码
CtMethod mnew = CtNewMethod.copy(mold, methodName, ctClass, null);
StringBuffer body = new StringBuffer();
try {
injectCode(body,newName,mold.getReturnType() == CtClass.voidType,mold);
}catch (Exception e){
e.printStackTrace();
}
// 替换新方法
mnew.setBody(body.toString());
// 增加新方法
ctClass.addMethod(mnew);
}
整体注入
上面介绍了对单个方法如何进行字节码注入的过程,但是尚未实现我们的目标-对工具类进行无入侵注入。要实现对工具类中的供开发同学使用的方法进行注入,需要用反射的方式获取工具类中的方法,将其中的public方法进行注入。 那么问题来了,如果直接使用RedissonTool.class.getMethods()的方法的话,在字节码替换的时候会提醒JVM中已经含有此类不能修改的问题。因为当在调用RedissonTool.class的时候,JVM就会加载class信息,而我们又是在原先的class中直接修改的,JVM在运行时期是不运行进行class修改的。 JVM是如何判断类是否已经加载了呢?我们知道,在JVM中判断一个类是否相同首先是判断类加载器是否相同,进一步判断类名是否相同。因此为了避免上述问题,可以通过自定义类加载器的方式进行加载(主要目的是打破双亲委派)。
public class MyClassLoader extends ClassLoader{
private String path;
public MyClassLoader(String clazzPath){
this.path = clazzPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class log = null;
byte[] bytes = gatDate();
if (bytes != null){
log = defineClass(name,bytes,0,bytes.length);
}
return log;
}
private byte[] gatDate(){
File file = new File(path);
if (file.exists()){
FileInputStream inputStream = null;
ByteArrayOutputStream outputStream = null;
try{
inputStream = new FileInputStream(file);
outputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int size = 0;
while ((size = inputStream.read(buffer)) != -1){
outputStream.write(buffer,0,size);
}
}catch (Exception e){
e.printStackTrace();
}finally {
try {
inputStream.close();
outputStream.close();
}catch (Exception e){
e.printStackTrace();
}
}
return outputStream.toByteArray();
}else{
return null;
}
}
}
接下来的工作就是涉及到具体的业务了,根据自己的需求进行判断。
// 定义类
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.get("redisson.RedissonTool");
List<String> methodList = new ArrayList<>();
URL path = Thread.currentThread().getContextClassLoader ().getResource("");
MyClassLoader loader = new MyClassLoader(path.getPath() + "redisson/RedissonTool.class");
Class<?>redissonTool = null;
try {
redissonTool = loader.findClass("redisson.RedissonTool");
System.out.println(redissonTool.getClassLoader() + "11111");
redissonTool.newInstance();
}catch (Exception e){
e.printStackTrace();
}
Method[] methods = redissonTool.getMethods();
excludeObjectMethod(methods);
for (Method i : methods){
if (i != null){
addLog(i.getName(),ctClass);
}
}
// addLog("set",ctClass);
RedissonTool redissonTool1 = (RedissonTool) ctClass.toClass().newInstance();
问题以及优化
其实这样的实现注入的方式同样稍显的臃肿,针对JVM中不能出现重复的类的问题Javassist中应该会有给出解决方法,无需自己创建类加载器。但目前尚未对Javassist进行研究,所以这一块需要深入挖掘一下。