各位不知道有没有遇到过如下情况,在一个 7,8 百个接口的大项目中:
项目早期开发比较混乱,没有留下接口文档。然而开发也不太想去帮你整理 (或者你也不太指挥的动开发)
无法理清前端调用接口与对应的表的关系
无法理清前端调用·接口与下游系统的调用关系

分享一下我的处理方法,抛砖引玉。

先来个简单的项目 demo,项目。

Controller 层:

@RestController
@RequestMapping("user")
public class Manger {
    @Autowired
    TestService testService;
    @RequestMapping("login.do")
    public List test1(@RequestBody MyRequest request, UserModel userModel) {
        return testService.test1(request);
    }
}

Service 层,注意这里是 service 接口的实现,service 接口我就不贴了,没东西

@Service
public class TestServiceImpl implements TestService{
    @Autowired
    private BannerFloorMapper bannerFloorMapper;
    public List test1(MyRequest myRequest){
        List list = bannerFloorMapper.queryEntityByBannerOrder(1, myRequest.getPassword(), null);
        return list;
    }
}

Mapper 层

@Mapper
public interface BannerFloorMapper<T extends FloorBannerModel> {
    public List<T> queryEntityByBannerOrder(@Param("bannerOrder") Integer bannerOrder, @Param("floorCode") String floorCode, @Param("id")Long id);
}

大体的项目 demo 如此,下面说说怎么去解析。

1.什么是 ASM

这里是抄的百度的介绍
ASM 是一个通用的 Java 字节码操作和分析框架。 它可以用于修改现有类或直接以二进制形式动态生成类。 ASM 提供了一些常见的字节码转换和分析算法,可以从中构建自定义复杂转换和代码分析工具。
题外话,这玩意和 JSR269 应该是 java agent 插桩技术的 2 个前置知识点了,能鼓捣出好多有意思的功能。我感觉学习一下还是蛮有意思的。
我认识的一个朋友好像拿这个玩意写 我的世界 的游戏插件😁
推荐一下我在 B 站学习的一个 up 主,以及他的相关教程。https://space.bilibili.com/1321054247/video

2.前置条件

这里说一下需要的前置条件
+ 你能拿到源码,并且编译成字节码文件
因为解析是通过字节码来进行解析的,所以这一步是必须的。idea 插件相关的技术是支持解析 java 源码的,有兴趣的同学也可以看看,这里不做过多表述。
+ 你要对项目代码比较熟悉,有个初步的认知
为什么没有一款这样的插件出来,即使是 swagger 也需要开发去写一堆注解,就是因为不同的开发,不同的公司,代码习惯,水平高低,基础架构包的封装方式等等千差万别,没有办法拿出一套放之四海而皆准的插件出来,所以就需要我们在此基础上进行针对当下项目的定制开发解析。

###3.正式开始。
递归获取项目文件夹下的所有.class 结尾文件

//递归获取文件夹下所有以suffix结尾的文件
public static void findFileList(List results, File dir, String suffix) throws IOException {
    if (!dir.exists() || !dir.isDirectory()) {
        return;
    }
    String[] files = dir.list();
    for (String file : files) {
        File fileName = new File(dir, file);
        if (fileName.isFile() && fileName.toString().endsWith(suffix) && !fileName.toString().endsWith("Test.class")) {
            results.add(fileName);
        } else {
            findFileList(results, fileName, suffix);
        }
    }
}

挨个去遍历 class 文件

//这里是遍历的入口,具体的逻辑在ControllerClassVisitor内
    for (File s : results) {
            FileInputStream fileInputStream = new FileInputStream(s);
            ClassReader classReader = new ClassReader(fileInputStream);
            ControllerClassVisitor controllerClassVisitor = new ControllerClassVisitor(ASM9);
            classReader.accept(controllerClassVisitor, ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG);
    }

controllerClassVisitor 类,用于解析类

    //我们的切入口是controller层,所以我们先去遍历class文件,只解析controller层类,其他类跳过,等方法中调用到的时候再去解析
public class ControllerClassVisitor extends ClassVisitor {
    boolean isController = false;  //是否为controller类的标记
    String[] secondUrlPath = new String[1];  //class上面的requestmapping内的第二段url地址,用于最后拼接出最终的url地址

    public ControllerClassVisitor(int api) {
        super(api);
    }
    @Override
    //visitAnnotation,访问类的注解,如果存在多注解就会多访问几次,我们需要访问类注解,来判断当前类是什么类。
    //descriptor为注解的类型
    public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
        if("Lorg/springframework/web/bind/annotation/RestController;".equals(descriptor)){
            //如果使用了RestController注解,则我们判断他是一个controller类,并打上controller类标签
            //这里注解不止RestController一种,常用的还有Lorg/springframework/stereotype/Controller;,注意最后;不能丢
            //我还遇到过一个项目里面的注解为自己基础jar包内自定义的,这里说明了熟悉自家代码的重要性
            isController = true;
            return null;
        }
        if("Lorg/springframework/web/bind/annotation/RequestMapping;".equals(descriptor)){
            //如果是类上有RequestMapping注解,那就说明,类上也有url的地址需要拼接,就需要我们进入这个注解的visitor里面去获取url的部分地址
            return new ControllerClassAnnotationVisitor(api, secondUrlPath);
        }
        //啥事没发生,直接return父类的相同方法
        return super.visitAnnotation(descriptor, visible);
    }

    @Override
        //visitMethod方法,访问类中方法的方法,acess为访问修饰符,name为方法名,descriptor为传参,用来判断方法的重载,signature为泛型,exceptions为抛出的异常
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        if(isController && !"<init>".equals(name) && !"<clinit>".equals(name)){
         //如果isController 为true,即当前类是controller类,且方法名字不为"<init>(构造方法)",和"<clinit>(静态代码初始化)"
        //那么就进入controller层方法的visitor
           return new ControllerMethodVisitor(ASM9, secondUrlPath, constant, esbList);
        }
        return super.visitMethod(access, name, descriptor, signature, exceptions);
    }
}

ControllerClassAnnotationVisitor 类,用于解析类上的注解
如果进到这里,我们是已经确定了当前的注解为 RequestMapping,所以我们需要去取 url 拼接的地址
具体的地址值是在 ControllerClassAnnotationTypeVisitor 内(这里有点绕)

public class ControllerClassAnnotationVisitor extends AnnotationVisitor {
    String[] secondUrlPath;
    public ControllerClassAnnotationVisitor(int api, String secondUrlPath[]) {
        super(api);
        this.secondUrlPath = secondUrlPath;
    }
    @Override
    //访问RequestMapping注解内的值
    public AnnotationVisitor visitArray(String name) {
        return new ControllerClassAnnotationTypeVisitor(api, secondUrlPath);
    }
}

ControllerClassAnnotationTypeVisitor 类 ,解析 RequestMapping 注解内的值

public class ControllerClassAnnotationTypeVisitor extends AnnotationVisitor {
    String[] secondUrlPath;
    public ControllerClassAnnotationTypeVisitor(int i, String secondUrlPath[], Constant constant) {
        super(i);
        this.secondUrlPath = secondUrlPath;
    }
    @Override
    //o就是注解内value的具体对象了,我们这边是url地址的一部分,所以直接toString获取
    public void visit(String s, Object o) {
        secondUrlPath[0] = o.toString();
        super.visit(s, o);
    }
}

ControllerMethodVisitor 类,用于解析 controller 类中的方法

public class ControllerMethodVisitor extends MethodVisitor {
    String[] secondUrlPath;
    boolean isUrlMethod = false;    //是否为指向外部接口的方法
    String interfaceUrl = null;          //最终的对外接口的url

    public ControllerMethodVisitor(int api, String[] secondUrlPath) {
        super(api);
        this.secondUrlPath = secondUrlPath;
    }
    @Override
    //访问controller类中的方法的注解,controller类中的方法,并一定都是指向对外接口的方法,所以这里也要加一层判断
    public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
        if ("Lorg/springframework/web/bind/annotation/RequestMapping;".equals(descriptor)) {
            //和controller类的判断一样,如果有RequestMapping注解,则认为是指向对外接口的方法,打上标识
            isUrlMethod = true;
            //这里的逻辑与controller类上的注解解析方法相同,
            //注意这里会遇到一个方法对应多个对外接口的写法,如 @RequestMapping(value = {"login.do", "haha.do"}
            return new ControllerMethodAnnotationVisitor(api, secondUrlPath);
        }
        return null;
    }
    @Override
    //这里是访问controller类中的方法内,会调用的方法
    //opcode目前先不管,owner为调用的方法的所在类的名字,name为方法名,descriptor为传参,用来判断方法的重载,isInterface为是否为接口
    public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
        if (owner.startsWith(com.barry) && isUrlMethod) {
            //可以预见到,代码内会引用一些第三方的依赖的方法,那么我们这边做一下过滤
            //只处理类名为以com.barry开头的类,即我们自己的业务代码,第三方和的代码不管

            //这里我们能确定,我们所在的方法必然是controller类中对外接口的方法,那么,这里的传参descriptor即为我们需要的
            //在demo中我们这里的传参为MyRequest 和UsewrModel2个类,则对应的descriptor如下
            //"(Lcom/barry/MyRequest;Lcom/barry/UserModel;)Ljava/util/List"
            //括号内的2个为传参全限定名,括号外的为方法的返回值全限定名
            //这里我们可以知道,第一个就是request本体,拿到全限定名后就可以拼接出类.class文件地址,从而拿到接口与请求对象的对应关系
            try {
                FileInputStream fileInputStream = new FileInputStream(owner.replace(".", "/"));
                //文件地址不为null,则开始递归挖掘controller类中的方法内的方法的调用
                //这里往下挖就可以挖到service层,然后再到mapper层,mapper层与xml存在对应关系,只需要解析好xml,就能推导到接口与表的关系
                //我们公司所有下游系统调用都是走的统一的企业服务总线,有个通用的sendmsg方法,也可以通过此方法来进行对外接口与内联接口的关系梳理
                //上面我们拿到了接口的请求对象的全限定名了,那么我们只要在接下来的所有方法调用中,找到get了哪些字段,就能得到哪些字段是必要字段了
                //以上本文不做过多梳理了,大家有兴趣可以自己试试
                ClassReader classReader = new ClassReader(fileInputStream);
                FunctionClassVisitor myClassVisitor = new FunctionClassVisitor(ASM9, name, descriptor);
                classReader.accept(myClassVisitor, ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG);
            } catch (NullPointerException e) {
                //有时候引入的是公司的基础架构的依赖内的方法,但是因为是jar包形式的,所以没办法解析到,就会走到这里,需要人工确认了。
                System.out.println(owner + "找不到,咋回事呢?");
            }
        }
    }
}

到这里为止,controller 层的类的解析就结束了。我们来整理一下顺序。
1.通过注解来确定 controller 类以及对外接口的第一段地址
2.通过注解确认对外接口对应的 controller 类内方法以及第二段地址,进行拼接从而得到接口地址
3.通过 controller 类内的对外接口的关联方法的传参,确认接口的请求对象

最重要的是理清楚层级关系,即
controller 的 visitor
annotation 的 visitor
method 的 visitor
service 的 visitor

那么,到这里总结下可能出现的问题和我踩过的一些坑
1.如果我们的服务直接是个 servlet 怎么办?
那就盯具体的方法,比如 servlet 里面 request 的 getParameter 方法

2.如果我们的接口方法的传参是个 json 字符串怎么办
这种一般也会用 JSONObject 之类的把 json 字符串转为对象,你就盯对应的转换方法即可。

3.继续往下解析,会遇到调用为 testService.test1(),实际的逻辑在 testServiceImpl.test1() 内的情况,怎么办
当获取到调用的方法的 owner 为 interface 时,如果代码比较规范,可以直接通过名字拼接的方式来确认具体的类
如果代码不规范,则需要去确认这个接口被哪些类实现了,来确认。

一些坑
1.类的全限定名不一定等于类文件的路径名!!!
遇到过一个类的全限定名为 a.b.c.TEST 的,但是他的 class 文件的实际路径确在 x/y/z 下,当时搞得我都想报个 bug 了。。。最后忍住了。。。好像也没规定不能这么写。

2.注意递归,小心陷入死循环
解析是一层层往下解析的,而当遇到方法内调用自己的时候,极易发生递归,需要设定跳出条件

3.注意方法的重载
结合上调,有时候遇到方法的重载 + 递归一起来的问题,这时候就不能只靠 name 名字来判断方法了,还要加上 descriptor,注意调用链路上的递归与重载,不要搞混。

4.通过反射来进行方法调用
这个没什么好的预防方法,我遇到的不多,但是确实有。
真遇到了你只能根据他采用的反射工具来确定他反射后的调用方法以及涉及到的类

以上就是全部内容了,我尽量把相关的用法思路写在代码注释里面了,欢迎大家讨论。


↙↙↙阅读原文可查看相关链接,并与作者交流