结合javassist进行通用性代码扩展修改的思路

最近产品中有一种想法,即在保证源项目不动的基础之上,提供一种通用的扩展原产品逻辑的方法.对于这种情况,有很多方法可以做,比如直接上aspectj,或者是spring aop,也可以是单个javassist修改.但对于实施方扩展源码,还是从相应的几个问题入手.

1. 修改的实现手段是否有难度,比如aspectj的切面语法
2. 相应的应用技术是否对项目运行时有性能影响,比如aspectj中*号匹配模式
3. 过多的修改类是否容易进行管理和处理,比如过多的切面是否好处理

考虑到以上的思路,想做一种简单的修改方式,即将相应的场景固定下来.比如,修改参数信息的,修改结果,以及替换实现等几个固定场景.并且,这些场景都是针对单一的修改场景,并不是一种范围匹配方式的.因为根据实际经验,进行实施时,往往是修改特定的场景中的特定的数据信息. 至于常规的类似日志记录,信息拦截等,还是通过切面来统一实现的好.

以下以一种修改参数信息的场景进行举例:
相应的实际效果,如下:

//源类
public class T8 {
    public static void abc(int a, int b) {
        System.out.println("->" + a + "," + b);
    }
}

//修改类
public class T81 {
    @Param(clazz = "t2.T8", method = "abc", params = {"int", "int"}, order = 2)
    public Map<Integer, Object> xchg1(int a, int b) {
        System.out.println("xchg1->" + a + "," + b);
        return ImmutableMap.of(1, a + 2, 2, b + 3);
    }
}

以上的代码,基于以下的工作方式.

1 获取相应的原始调用参数信息
2 将相应的参数传递给相应的修改处理器
3 如果有多个处理器,即链式调用
4 处理完之后,将参数信息重新传递回原来的方法调用

整个工作方式,基于javassist.因为它提供一种代码式的插入方式,相比asm,在应用层面更加方便.相应的文档参考地址如: http://jboss-javassist.github.io/javassist/tutorial/tutorial.html
因为是,处理参数信息,即在整个方法调用前插入相应的修改代码,使用的即是相应的ctmethod.insertBefore(code) 处理.

考虑到需要有一种通用的处理方式来工作,因此设计了@Param注解,用于描述修改方法将要处理哪个源方法,并且此修改方法的排序号(允许多个修改方法作用于同一个源方法). 同时,考虑到原始类型的存在,设计了map的返回方式.如果修改方法返回map,即表示需要将相应的结果重新进行回复,以避免修改结果丢失的问题.(因为基本类型是传值方式,而是传引用值)

同时,考虑到同一个修改源类可能会被多次修改的问题(如修改多个方法),因此在一次修改之后,并没有马上toClass加载,而是先暂存起来,最终一次性处理.

整个工作如下所示:

1. 通过类扫描扫描所有的修改类以及相应的方法(本文采用reflections,一个很实用的小工作)
2. 按修改方法分组,同一个分组内按序号排序处理
3. 针对修改方法,依次调用相应的修改处理,生成相应的修改代码
4. 使用insertBefore插入修改代码,并返回ctClass
5. 最终所有ctClass进行toClass化

代码参考如下(因为是示意代码,因此也不想再作加工):

主工作代码:

private static Map<ParamV, List<Method>> methodMap = Maps.newConcurrentMap();
    public static void doParam() {
        Reflections reflections = new Reflections("t2", MethodAnnotationsScanner.class);
        List<Method> methodList = Lists.newArrayList(reflections.getMethodsAnnotatedWith(Param.class));
        methodList.sort(Comparator.comparing(t -> t.getAnnotation(Param.class).order()));

        methodMap = MapUtils.asListMap(methodList, t -> new ParamV(t.getAnnotation(Param.class)));

        Set<CtClass> ctClassSet = methodMap.entrySet().stream().map(t -> instead(t.getKey(), t.getValue())).collect(Collectors.toSet());
        ctClassSet.forEach(t -> {
            try{
                t.toClass();
            } catch(CannotCompileException e) {
                throw Throwables.propagate(e);
            }
        });
    }

修改逻辑

    public static CtClass instead(ParamV paramV, List<Method> methodList) {
            ClassPool classPool = ClassPool.getDefault();
            classPool.importPackage("t2");

            CtClass ctClass = classPool.get(paramV.first);
            CtClass[] methodParamCtClass = paramV.third.stream().map(t -> {
                    return classPool.get(t);
            }).toArray(CtClass[]::new);
            CtMethod ctMethod = ctClass.getDeclaredMethod(paramV.second, methodParamCtClass);

            StringBuilder builder = new StringBuilder();

            int mIdx = 1;
            for(Method method : methodList) {
                val isMapMethod = method.getReturnType() != void.class;

                String methodClazz = method.getDeclaringClass().getName();
                val mStr = "_m" + mIdx;
                if(isMapMethod)
                    builder.append("java.util.Map ").append(mStr).append("=");

                //生成 obj.method(a,b,c)类调用
                builder.append("((").append(methodClazz).append(")");
                builder.append("ParamV.instance(\"").append(methodClazz).append("\")");//obj对象
                builder.append(")");
                builder.append(".");//调用
                builder.append(method.getName());//方法名
                builder.append("($$)");//具体参数
                builder.append(";");

                //转换类型信息
                if(isMapMethod) {
                    for(int i = 0; i < paramV.third.size(); i++) {
                        builder.append(x2v(i + 1, mStr, paramV.third.get(i)));
                    }
                }

                mIdx++;
            }

            String code = builder.toString();
            log.debug("插入代码:{}", code);

            ctMethod.insertBefore(code);

            return ctClass;
    }

基本类型转换处理

public static String x2v(int i, String mapStr, String type) {
        String prefix = "$" + i + "=";
        String postfix = ";";
        String middle = mapStr + ".get(Integer.valueOf(" + i + "))";
        switch(type) {
            case "byte":
                return prefix + "((Byte)" + middle + ").byteValue()" + postfix;
......
}

后记:

与aspectj协作
此文中涉及到的属于静态代码修改,因此它和aspectj是相辅相成的,即在整个系统中. javassist工作在静态代码处理层,而aspectj工作在代码加载这一层(ltw). 因此, 实际的service代码, 可以先由本文中的思路进行修改,再在loadClass时由aspectj进行二次增加, 二者是没有冲突的.

本文之所以成文,还是考虑到如何将开发的难度最小化. 其实有什么新的东西,还是没有, 本身还是字节码修改那回事.只不过,用在什么地方,怎么化简化使用者, 是需要考虑的. 对于使用者,能够怀着感恩的心完成自己的工作,再来谈论技术难度如何.
不过,查看javassist中间insert那段代码, 编译代码那段还有点意思,值得一看.

转载请标明出处:i flym
本文地址:https://www.iflym.com/index.php/code/201704100001.html

相关文章:

作者: flym

I am flym,the master of the site:)

发表评论

邮箱地址不会被公开。 必填项已用*标注