前置解析编译单元(javaparser,CompilationUnit)

前置解析编译单元(CompilationUnit)

AST(抽象语法树)介绍:

抽象语法树(Abstract Syntax Tree,AST)是一种用于表示源代码语法结构的树状数据结构。它将源代码转化成一个树,其中每个节点代表一个语法结构或构造。AST是编译器和解析器中的重要组成部分,用于实现词法分析、语法分析和语义分析等编译过程。AST的节点通常表示源代码的不同部分,例如变量声明、函数调用、循环结构、条件语句等。每个节点在树中的位置和连接方式反映了代码的层次结构和语法关系。节点可能具有不同的属性,例如名称、类型、值等。

 

在许多编程语言中,AST通常是通过解析器将源代码转换成的中间表示形式。许多编程语言工具和框架,如编译器、静态分析工具和IDE等,都使用AST作为中间表示形式来进行代码的处理和分析。

 

 

推荐引擎中抽象语法树工具使用的是JavaParser,它可以帮我们把一个Java文件解析成“编译单元”,也就是把一个Java文件变成一颗Java语法树,类似于使用工具把汽车进行拆解,并按一定规则进行摆放,拆出来的零件是什么,零件之间有什么关系,则还需要我们来进一步分析。

 

加载编译单元

主要使用AST将所有目标文件(.java文件)解析成编译单元。解析时会根据排除/保留策略筛选出目标文件。一个编译单元就是一个.java文件,将编译单元暂存到“编译单元解析任务原信息(CompilationUnitParserTask)” 中,用于后续分析任务。

 

PS: 必须先解析完所有代码文件,才能进行方法调用链分析,因为不同文件之间存在的引用关系,先分析会导致获取不到依赖的问题。

...
CompilationUnit cu = StaticJavaParser.parse(FileUtils.read(file));
...
compilationUnitParserTasks.add(new CompilationUnitParserTask(typeMap, javaParses, packNames, packComments, cu, file));
  • 1.
  • 2.
  • 3.
  • 4.
 

 

 

 

分析编译单元(CompilationUnit)

分析编译单元时,采用并发模式进行分析,由于所有编译单元已经完成了加载,也就是说所有的代码文件都已经变成语法树了,这个时候就可以使用并发模式来提高分析效率了。

 

一个编译单元就是一个.java文件,我们以Java中的一个类(类|接口|枚举|注解声明)当做分析的主体,一个.java文件中存在一个或多个类,所以需要遍历拿到每个“类型声明(TypeDeclaration)”进行逐一分析:

// 此处已经在并发模式下运行了 
for (TypeDeclaration<?> type : parserTask.compilationUnit.getTypes()) {
    Step3Type.parseType(parserTask.typeMap, parserTask.javaParses, type, parserTask.packNames,
            parserTask.packComments, parserTask.compilationUnit);
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
 

 

 

 

分析类型声明(TypeDeclaration)

提取类级别http path前缀

由于Spring项目可以在“类、抽象类、接口”中定义http接口,所有需要从对应代码中进行提取。接口的最终path为 = 项目级别paht + 类级别path + 方法级别path。

 

PS: 代码中path不填写规范时需要平台兜底补全“/”  

图片图片

 

从 @RequestMapping 注解或 @FeignClient 注解中提取path,通过注解表达式AnnotationExpr获取注解名“an.getName().getIdentifier()”并判断是那种注解。

 

  • 提取注解的参数值:

由于注解的入参写法的不同分为以下两种表达式,不同表达式的提取方式不同,具体如下:

SingleMemberAnnotationExpr:单值无参数名表达式【获取逻辑相对简单】

如:@RequestMapping("/dubbo")

 

NormalAnnotationExpr:多值有参参数名表达式【获取逻辑较为复杂,且 RestController 和 FeignClient注解的有差异】

如:@RequestMapping(value = "/dubbo", method = RequestMethod.POST)

如:@RequestMapping(value = "/dubbo")

如:@RequestMapping()

 

RequestMapping注解:值存在两种字段名“value”和“path”,两种都代表接口路径,需遵循Spring的优先级规则来取值(value > path)

 

FeignClient注解:只存在path一个字段名

 

  • 整体逻辑:

首先提取到目标注解的值的“表达式(Expression)”:

 

Expression 表达式存多种子类,重点关注“字符串表达式(StringLiteralExpr)”和“数组表达(ArrayInitializerExpr)”,如:

@RequestMapping(value = "/dubbo")中的"/dubbo" 就是一个 StringLiteralExpr对象

@RequestMapping(value = {"/dubbo", "/dubbo2"})中的{"/dubbo", "/dubbo2"} 就是一个ArrayInitializerExpr对象

 

然后通过不同的处理逻辑来提取“StringLiteralExpr”或“ArrayInitializerExpr”表达式的具体值

 

最后按照接口路径前缀规则拼接完整类级别前缀。

/**
 * 提取接口信息
 *  - 提取http接口信息
 */
static void extractClassApiInfo(TypeInfo info) {
    if (info.annotations != null) {
        for (AnnotationExpr an : info.annotations) {
            if (an.getName().getIdentifier().equals("RestController")) {
                info.hasRestControllerAnnotation = true;
            }
            // 只提取http接口
            if (!httpMethodAnnotations.contains(an.getName().getIdentifier())) continue;


            // 源代码注解只有单个入参的写法,如 @RequestMapping("/dubbo")
            if (an.isSingleMemberAnnotationExpr()) {
                if (an.getName().getIdentifier().equals("RequestMapping")) {
                    SingleMemberAnnotationExpr san = an.asSingleMemberAnnotationExpr();
                    Expression valueExpr = san.getMemberValue();
                    String path = getHttpPathByValueExpression(valueExpr);
                    setHttpPathPrefix(info, path);
                }
            }
            // 源代码注解有多个值的写法,如 @RequestMapping(value = "/dubbo", method = RequestMethod.POST)
            // 或 @RequestMapping(value = "/dubbo") 或 @RequestMapping()
            else if (an.isNormalAnnotationExpr()) {
                NormalAnnotationExpr nan = an.asNormalAnnotationExpr();
                List<Node> childNodes = nan.getChildNodes();
                // 第一个元素为注解描述跳过(注解名称等信息)
                if (childNodes.size() == 1) continue;
                boolean flag = true;
                for (Node cn : childNodes) {
                    if (flag) {
                        flag = false;
                        continue;
                    }
                    MemberValuePair mvp = (MemberValuePair) cn;
                    // RequestMapping注解的参数value的值就是接口路径前缀
                    if (an.getName().getIdentifier().equals("RequestMapping")) {
                        String name = mvp.getName().getIdentifier();
                        if (name != null && (name.equals("value") || name.equals("path"))) {
                            Expression valueExpr = mvp.getValue();
                            String path = getHttpPathByValueExpression(valueExpr);
                            setHttpPathPrefix(info, path);
                            break;
                        }
                    }
                    // FeignClient(公司内部实现的注解)注解的参数path的值就是接口路径前缀
                    else if (an.getName().getIdentifier().equals("FeignClient")) {
                        String name = mvp.getName().getIdentifier();
                        if (name != null && name.equals("path")) {
                            Expression valueExpr = mvp.getValue();
                            String path = getHttpPathByValueExpression(valueExpr);
                            setHttpPathPrefix(info, path);
                            break;
                        }
                    }
                }
            }
        }
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
 
/**
 * 根据注解的值表达式获取接口路径
 */
static String getHttpPathByValueExpression(Expression valueExpr) {
    String path = null;
    if (valueExpr instanceof  StringLiteralExpr) {
        path = ((StringLiteralExpr) valueExpr).getValue();


    }
    else if (valueExpr instanceof ArrayInitializerExpr) {
        ArrayInitializerExpr _valueExpr = (ArrayInitializerExpr) valueExpr;
        List<Node> valueChildNodes = _valueExpr.getChildNodes();
        // 存在多个path时只取第一个
        path = valueChildNodes.size() > 0 ? ((StringLiteralExpr) valueChildNodes.get(0)).getValue() : "";
    }
    return path != null ? path : "";
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
 
/**
 * 设置http接口路径前缀
 *  - 确保路径开头和路径之间存在"/"字符
 */
static void setHttpPathPrefix(TypeInfo info, String path) {
    path = path != null ? path : "";
    if (FileUtils.getHttpRootPathPrefix().isEmpty()) {
        info.httpPathPrefix = path;
    } else {
        if (!path.isEmpty() && !path.startsWith("/")) {
            path = "/" + path;
        }
        info.httpPathPrefix = FileUtils.getHttpRootPathPrefix() + path;
    }


    if (!info.httpPathPrefix.isEmpty() && !info.httpPathPrefix.startsWith("/")) {
        info.httpPathPrefix = "/" + info.httpPathPrefix;
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
 

 

 

分析类成员

类成员主要有:

  • 方法声明:普通方法(MethodDeclaration)、构造方法(ConstructorDeclaration)
  • 字段声明(FieldDeclaration)
  • 代码块声明(InitializerDeclaration)
  • 类型声明(TypeDeclaration):一般是内部类

方法声明-普通方法(MethodDeclaration)分析

 

提取方法的关键信息

 

从法语树的方法节点中提取到方法关键信息存储到MemberInfo对象中,关键信息如下:

方法签名(sign):方法的唯一标识

所属类(class):

入参(parTypes):

返回值(retType):

接口标识(api): true 是接口,false 不是接口

接口路径(aPath):  htttp 路径 或 dubbo路径

接口协议(aPtl): http 或 dubbo

调用方法(calls): 当前方法调用的其他方法签名

被调方法(usages): 当前方法被哪些方法签名调用

资损标(zsScan):方法上添加注解,标记资损方法,通过@ZsScan注解进行打标

其他属性:...

 

PS:因调用链需要存储海量的数据,为减少网络传输开销和存储空间,传输和落库使用的是字段名简称,代码中使用的是全称 

图片图片

 

提取方法签名

由于Java存在方法重载,即方法名相同入参类型和个数不同,则代表不同的方法,所以必须使用完整签名才能唯一确定一个方法。sign = 包名.类名.方法名(所有参数的完整完整签名),如方法:

 

定义方法------------------------

build***Cmd(Outbo***ntity ohe, Listlist, boolean b)

 

完整签名(sign)------------------------

com.poi***sService.bu***erCmd(com.poiz***eaderEntity,java.util.List, boolean)

 

  • 正常情况:通过原生的 ResolvedMethodLikeDeclaration.getQualifiedSignature() 方法来获取方法的完整签名
/**
 * 解析方法签名
 */
static String methodSign(ResolvedMethodLikeDeclaration r, TypeInfo typeInfo) {
    try {
        return r.getQualifiedSignature().replace(" extends java.lang.Object", "");
    }catch (UnsolvedSymbolException e) {
        // 方法签名使用兜底字段值类型
        String sign = getPocketBottomQualifiedSignature(r, typeInfo);
        // LOG.warn("InfoUtils.methodSign(使用兜底类型-方法签名入参的类型)(无法提取jar包中的名字)newSign=" + sign);
        return sign.replace(" extends java.lang.Object", "");
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
 
  • 异常情况:由于存在排除代码和不分析依赖jar包中的代码的情况,所以推荐引擎已经加载的 编译单元 并不完整,即有些代码只是体现了被使用但没有具体的实体,如下图:如下图方法 queryCountByScene 的入参是 QueryCountBySceneRequest 类型,当前文件中也导入了这个类,但如果该类来自一个依赖jar包或者该类被排除了,这个时候已经加载的编译单元中是没有这个类的语法树的,所以原生的方式无法获取到完整签名,报错后会导致当前方法信息也丢失。

 

 

  • 解决方案:使用兜底策略提取方法签名,支持两种方案,当方案1异常是自动切到方案2。
  • 方案1:从导包中路径中提取出完整签名重写 MyResolvedMethodDeclaration类代替原 ResolvedMethodLikeDeclaration(方法声明)重写 MyReferenceTypeImpl类 替代 ResolvedType(方法入参声明)核心逻辑“从导包中路径中提取出完整签名”

图片图片

public class MyResolvedMethodDeclaration implements ResolvedMethodDeclaration {
    ...
    // 重写构造方法 
    public MyResolvedMethodDeclaration(ResolvedMethodDeclaration declaration, List<ResolvedType> paramTypes,
                                       ResolvedType returnType, TypeInfo typeInfo) {
        this.declaration = declaration;
        this.paramTypes = paramTypes;
        this.returnType = returnType;
        this.typeInfo = typeInfo;
    
        // 前置设置参数类型
        if (paramTypes == null) {
            this.paramTypes = new ArrayList<>();
            for (int i = 0; i < declaration.getNumberOfParams(); ++i) {
                ResolvedType paramType = getParamType(declaration.getParam(i));
                this.paramTypes.add(paramType);
            }
        }
    }


    // 重写获取签名方法 
    @Override
    public String getSignature() {
        if (shortSign != null) return shortSign;
        StringBuilder sb = new StringBuilder();
        sb.append(this.getName());
        sb.append("(");
    
        for(int i = 0; i < this.paramTypes.size(); ++i) {
            if (i != 0) {
                sb.append(", ");
            }
    
            if (this.paramTypes.get(i) instanceof MyReferenceTypeImpl) {
                MyReferenceTypeImpl myTypeImpl = (MyReferenceTypeImpl) this.paramTypes.get(i);
                sb.append(myTypeImpl.getSign());
            }
            else {
                sb.append(this.getParam(i).describeType());
            }
        }
    
        sb.append(")");
        shortSign = sb.toString();
        return shortSign;
    }
    
    // 新增兜底获取参数类型 
    public ResolvedType getParamType(ResolvedParameterDeclaration param) {
        try {
            // 正常获取
            return param.getType();
        } catch (Exception e) {...}
        if (param instanceof JavaParserParameterDeclaration) {
            JavaParserParameterDeclaration jPpd = (JavaParserParameterDeclaration) param;
            List<Node> nodes = jPpd.getWrappedNode().getChildNodes();
            Node node = nodes.get(0);
            if (node instanceof ClassOrInterfaceType) {
                ClassOrInterfaceType cIType = (ClassOrInterfaceType) node;
                String paramTypeName = cIType.getName().toString();
                String classSign = getClassSign(paramTypeName);
                MyReferenceTypeImpl paramType = new MyReferenceTypeImpl(param, paramTypeName, classSign);
                return paramType;
            }
        }
        return null;
    }
    
    // 重写获取类的完整类签名值
    public String getClassSign(String paramTypeName) {
        String matchValue = "." + paramTypeName;
        // 倒序查询目标导入类
        List<String> impSigns = typeInfo.getImportSignsByInit();
        for (int i = impSigns.size() - 1; i >=0; i--) {
            String impSign = impSigns.get(i);
            if (impSign.endsWith(matchValue) || impSign.equals(paramTypeName)) {
                return impSign;
            }
        }
        return null;
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
 
  • 方案2:生成残缺签名如果包时没有导入具体的类,而是通过*号导入的,那这时候将无法准确匹配到具体的类签名,只能兜底生成残缺的签名(极端情况下可能会不准)

 

 

方案1-结果:

com.shizhu***erProvider.que***cene(com.shi***est.Que***eRequest)

 

方案2-结果:

com.shizhu***erProvider.que***cene(Que***eRequest)

 

可以看到,方案1是完整的签名,方案2则参数类型名称不完整,如果项目中存在两个同名的类,则方案2的推荐结果就可能有偏差了。

 

提取方法资损标

通过@ZsScan注解对方法进行打标,如图 invoke2方法 有两个资损标,提取时从方法注解中找到@ZsScan注解,提取注解的 zsCases 字段的值表达式(ArrayInitializerExpr),将数组表达式中的多个值通过“;”拼接成字符串进行存储

 

 

图片图片

 

补全方法其他相关属性

标注方法是否是 静态、抽象、默认方法。由于Java8开始interface中允许有default方法,即interface不完全都是抽象方法,故需要根据default标识来设置interface中的方法类型。

static void addResolvedMethodInfo(MemberInfo info, ResolvedMethodLikeDeclaration r, TypeInfo typeInfo) {
    if (info.sign == null || info.sign.equals("")) {
        info.sign = methodSign(r, typeInfo);
    }
    info.name = r.getName();
    info.genLowFirstName();
    if (r instanceof JavaParserMethodDeclaration) {
        JavaParserMethodDeclaration md = (JavaParserMethodDeclaration) r;
        info.isStatic = md.isStatic();
        info.isAbstract = md.isAbstract();
        info.isDefault = md.isDefaultMethod();
    }
    else if (r instanceof MyResolvedMethodDeclaration) {
        MyResolvedMethodDeclaration rmd = (MyResolvedMethodDeclaration) r;
        info.isStatic = rmd.isStatic();
        info.isAbstract = rmd.isAbstract();
        info.isDefault = rmd.isDefaultMethod();
    }
    else if (r instanceof ReflectionMethodDeclaration) {
        ReflectionMethodDeclaration rmd = (ReflectionMethodDeclaration) r;
        info.isStatic = rmd.isStatic();
        info.isAbstract = rmd.isAbstract();
        info.isDefault = rmd.isDefaultMethod();
    }
    // 修正接口方法默认共有
    if (TypeEnum.INTERFACE.equals(info.typeInfo.type)) {
        info.access = AccessEnum.PUBLIC;
        info.isAbstract = !info.isDefault;  // 如果是interface中的默认方法,则不是抽象方法
    } else {
        info.access = AccessEnumUtils.toEnum(r.accessSpecifier());
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
 

提取方法的API信息(http path)

Spring中一般通过注解来标记一个方法是http接口的入口方法,支持:

 

RequestMapping、PostMapping、GetMapping、PutMapping等注解,同时由于注解中参数形式写法多样,对应的表达式类型也不一样,所以需要针对每种写法使用对应的方式才能提取到接口路径值,如下图:

 

 

 

  • 提取路径的核心思路:先从方法注解中选出目标注解。根据“注解写法”区分出 SingleMemberAnnotationExpr(单值无参数名表达式) 和 NormalAnnotationExpr(多值有参数名表达式) 两种参数表达式,根据不同表达式使用不同逻辑提取到path值的表达式 Expression(StringLiteralExpr or ArrayInitializerExpr) ,根据值表达式提取到具体的接口路径。
// 目标注解名称
private static final List<String> httpMethodAnnotations = Arrays.asList(
    "RequestMapping", "PostMapping", "GetMapping", "PutMapping", "PatchMapping",
    "DeleteMapping", "HeadMapping", "TraceMapping");
/**
 * 提取方法的接口信息
 */
static void extractMethodApiInfo(MemberInfo info) {
    if (info.annotations != null && info.memberType != MemberEnum.FIELD && info.memberType != MemberEnum.STATIC) {
        for (AnnotationExpr an : info.annotations) {
            // 只提取http接口
            if (!httpMethodAnnotations.contains(an.getName().getIdentifier())) continue;


            // 源代码注解只有单个入参的写法,如 @RequestMapping("/dubbo")
            if (an.isSingleMemberAnnotationExpr()) {
                SingleMemberAnnotationExpr san = an.asSingleMemberAnnotationExpr();
                Expression valueExpr = san.getMemberValue();
                String path = InfoUtils.getHttpPathByValueExpression(valueExpr);
                setHttpPath(info, path);
            }
            // 源代码注解有多个值的写法,如 @RequestMapping(value = "/dubbo", method = RequestMethod.POST)
            // 或 @RequestMapping(value = "/dubbo") 或 @RequestMapping()
            else if (an.isNormalAnnotationExpr()) {
                NormalAnnotationExpr nan = an.asNormalAnnotationExpr();


                List<Node> childNodes = nan.getChildNodes();
                // 第一个元素为注解描述跳过(注解名称等信息)
                if (childNodes.size() == 1) continue;
                boolean flag = true;
                for (Node cn : childNodes) {
                    if (flag) {
                        flag = false;
                        continue;
                    }
                    MemberValuePair mvp = (MemberValuePair) cn;
                    String name = mvp.getName().getIdentifier();
                    if (name != null && (name.equals("value") || name.equals("path"))) {
                        Expression valueExpr = mvp.getValue();
                        String path = InfoUtils.getHttpPathByValueExpression(valueExpr);
                        setHttpPath(info, path);
                        break;
                    }
                }
            }
        }
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
 

 

提取字段关键信息

字段的定义方式有多种,单行一个变量和单行多个变量这两种写法对应的定义表达式也不一样,需要通过FieldDeclaration.getVariables().size() 来区分,size == 1的就是单行一个变量,提取方式比较简单,size > 1的就是单行定义多个变量,提取方式具体如下:

图片图片

FieldDeclaration d = m.asFieldDeclaration();
// 一行定义一个字段的场景
if (d.getVariables().size() == 1) {
    ResolvedFieldDeclaration r = d.resolve();
    String sign = typeInfo.sign + "." + r.getName();
    memberInfoLock.lock();
    MemberInfo info = typeInfo.memberInfo.computeIfAbsent(sign, s -> new MemberInfo());
    memberInfoLock.unlock();
    info.sign = sign;
    InfoUtils.addResolvedFieldInfo(info, r);
    InfoUtils.addFieldInfo(info, d);
    info.memberType = MemberEnum.FIELD;
    infos.add(info);
}
// 一行定义多个字段的场景
else if (d.getVariables().size() > 1) {
    LOG.warn("Step4Members.parseMembers(兜底一行定义多个属性的情况)typeInfoSign=" + typeInfo.sign + " 一行属性个数=" + d.getVariables().size());
    for (VariableDeclarator vd : d.getVariables()) {
        String sign = typeInfo.sign + "." + vd.getName().getIdentifier();
        memberInfoLock.lock();
        MemberInfo info = typeInfo.memberInfo.computeIfAbsent(sign, s -> new MemberInfo());
        memberInfoLock.unlock();
        info.sign = sign;
        info.name = vd.getName().getIdentifier();
        info.genLowFirstName();
        try {
            // 设置完整类型-(int 等基础类型不会命中下方进行兜底)
            for (Node nd : vd.getChildNodes()) {
                if (nd instanceof ClassOrInterfaceType) {
                    info.returnType = ((ClassOrInterfaceType) nd).toDescriptor().substring(1).replace("/", ".").replace(";", "");
                    break;
                }
            }
        } catch (Exception e) {
            // 下方兜底时统一设置
        }
        // 设置兜底类型
        if (info.returnType == null) {
            info.returnType = vd.getType().toString();
        }
        // 不设置 info.access 用处不到,且难以拿到
        info.memberType = MemberEnum.FIELD;
        infos.add(info);
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
 

 

提取方法调用关系

通过解析方法体找到所有的MethodCallExpr(方法调用表达式),进而提取出当前方法都调用了哪些方法。

 

1、技术难点

  • 方法重载:即方法名相同但方法的参数个数和参数类型不同,则代表是不同的方法。所以不能只通过方法名来判断当前方法调用了哪些方法,需结合方法名和调用时每个位置上的参数类型,才能准确定位到具体调用的方法。
  • 参传方式多样:方法调用传参支持 直接传值、传变量、传方法调用、传链式调用、组合拼接值等,增加了解析入参类型的复杂度(需要拿到最终调用参数类型)。
  • lombok注解:如@Data 注解,Java支持通过注解实现编译时生成实体方法,也就是说源码中并不存在方法,编译之后字节码中才存在这些方法。我们做的是源码分析不是字节码分析,所以无法正常获取调用关系。
  • 反射调用:Java反射是通过字符串来动态指定调用的类和方法,源码中体现的调用关系并不是业务逻辑上的真实调用关系。

2、案例分析

案例1:

  • 图一 中调用 checkBrandOrCategoryNew 方法时,使用了链式调用,且链式调用中的方法源代码中并不存在,而是通过 图二 @Data 注解在编译时自动生成的方法字节码的。
  • 要提取到当前方法调用的到底是那个checkBrandOrCategoryNew方法,则需要获取到每个入参的最终类型,也就是逐步分析每个链式调用中每个节点的类型,以及这个节点下的属性或方法的最终返回值的类型,直到获取出最后一个节点的最终返回类型为止。

图片图片

图片图片

 

 

案例2:

  • MethodCallExpr(方法调用表达式)展示,不同调用方式对应的子表达式也不一样,分析难度也不一样,图三中可以看到,源代码的方法体中只有两行代码,第一行代码先调用String.substring方法,传参为 String.length()方法,最终会提取到两个MethodCallExpr(方法调用表达式),第一个是主调用,第二个是子调用,也就是说当前方法即调用了 java.lang.String.substring(int) 又调用了 java.lang.String.length() 方法

图片图片

 

 

调用关系核心逻辑介绍

核心思路通过 MethodCallExpr.resolve() 获取到 ResolvedMethodDeclaration(方法调用声明),并建立当前方法和被调用方法的绑定关系。同时检查是否存在反射调用,单独记录反射调用语法树节点,待整体分析结束后补充解析反射调用关系。

/**
 * 解析成员中的方法调用
 */
static void parseMethodCall(LinkedHashMap<String, TypeInfo> typeMap,
                            List<JavaParse> javaParses, BodyDeclaration<?> m,
                            TypeInfo typeInfo, MemberInfo usageInfo) {
    // 反射调用解析器
    ReflectionCallParser reflectionCallParser = null;
    // 判断是否是方法,不需要判断,属性赋初值时也存在方法调用
    for (MethodCallExpr expr : m.findAll(MethodCallExpr.class)) {
        ResolvedMethodDeclaration r;
        try {
            // 代码调用次数越多,源码量越多,这里花的时间越多
            r = expr.resolve();
        } catch (Throwable e) {
            // FIXME 目前已知解析失败:静态引用,::调用
            continue;
        }
        ...
        // 建立方法调用绑定关系 
        MemberInfo callInfo = InfoFactory.getOrCreateMethodInfo(callTypeInfo, r);
        usageInfo.callInfo.put(callInfo.sign, callInfo);
        usageInfo.typeInfo.callInfo.put(callInfo.typeInfo.sign, callInfo.typeInfo);
        callInfo.usageInfo.put(usageInfo.sign, usageInfo);
        if (!callInfo.typeInfo.sign.equals(usageInfo.typeInfo.sign)) {
            callInfo.typeInfo.usageInfo.put(usageInfo.typeInfo.sign, usageInfo.typeInfo);
        }
        ...
        // 提取反射调用 
        if (callInfo.sign.contains("java.lang.Class.forName(java.lang.String)")) {
            reflectionCallParser =  ReflectionCallParser.addForNameCallExpr(
                    callInfo, expr, reflectionCallParser, usageInfo);
        } else if (callInfo.sign.contains("java.lang.Class.getMethod(java.lang.String, java.lang.Class<?>...)")) {
            reflectionCallParser =  ReflectionCallParser.addGetMethodCallExpr(
                    callInfo, expr, reflectionCallParser, usageInfo);
        } else if (callInfo.sign.contains("java.lang.reflect.Method.invoke(")) {
            reflectionCallParser =  ReflectionCallParser.addInvokeCallExpr(
                    callInfo, expr, reflectionCallParser, usageInfo);
        }
    }


    // 存储反射调用解析器 
    ReflectionCallParser.saveReflectionCallParser(reflectionCallParser, usageInfo);
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
 

 

技术难点解决

方法重载和lombok注解

核心思路确保调方法时的所有入参都能够解析出最终的参数类型,同时确保所有因lombok注解而缺失的方法都能被找到,其实补全缺失方法后基本就能保证所有方法入参类型能正常解析出来了。

 

  • 补全lombok注解缺少代码:

目前只支持@Data注解自动生成的 get set 方法,其他自动生成的代码对调用链无影响。只补全未实现的get set方法,且排除static字段“@data注解只会生成对象属性的方法不会生成类属性的方法”

 

PS: 由于Java中存在内部类,所以也需要额外解析所有内部类和内部类的内部类,并生成缺失方法 

方法分析和最终结果方法分析和最终结果

写入缺失方法写入缺失方法

补全内部类缺失代码补全内部类缺失代码

写入新代码并重新加载编译单元(CompilationUnit)写入新代码并重新加载编译单元(CompilationUnit)

 

反射调用

Java反射是通过字符串来动态指定调用的类和方法,源码中体现的调用关系并不是业务逻辑上的真实调用关系,如下图,想要知道具体调的是那个方法,则需要先从入参提取出是那个类、那个方法和入参类型。

 

 

(1)技术难点

  • 获取创建反射对象的方法入参:存在变量:变量无法解析出运行时的值【不支持】参数拼接:相同类型参数拼接不同类型参数拼接多个参数混合类型拼接
  • 组合难点:调用多个反射方法变量定义顺序重复给变量赋值存在条件判断【无法准确处理】等...

 

 

(2)提取反射调用方法:

自研提取逻辑的核心逻辑,使用调用栈是思路确保安装正常代码执行的逻辑来提取反射调用方法,即使存在“不按顺序、调用多个方法、重复赋值”的情况,最终提取到的调用方法也是准确的。

 

  • 解析反射表达式:

将“反射类、反射方法、反射调用”三种表达式提取出来并生成对应的拓展表达式对象,对象中维护出现顺序索引、空间变量名、使用空间变量名、... 等关键信息,具体如下:

 

反射调用特征:方法调用中同时存在以下3种方法签名,才存在放射调用。

  1. java.lang.Class.forName(java.lang.String)
  2. java.lang.Class.getMethod(java.lang.String, java.lang.Class...)
  3. java.lang.reflect.Method.invoke(

反射类:Class.forName("com.shizhuang.xx.A")  ==> ForNameCallExprInfo

反射方法:clazz.getMethod("test", Integer.class, String.class) ==> GetMethodCallExprInfo

反射调用:method.invoke(obj, 1, "xxx", 2) ==> InvokeCallExprInfo

 

PS: 需要找到当前表达式对应的上级依赖表达式对象,寻找策略根据依赖变量名在依赖列表中的倒序查找查找,第一个 出现顺序索引 小于自己的变量,就是目标依赖对象。

 

  • 提取反射调用方法:

以“反射调用”为入口,通过其中的“使用空间变量名” + “出现顺序索引” 在“反射方法-变量空间”中找到“出现顺序索引”小于“反射调用”的“索引顺序”的第一个变量“反射方法”,同理以“反射方法”为入口找到“反射类”。从而提取出最终的反射方法签名。

 

图片图片

 

  • 代码片段:

由于反射调用提取整体流程过于复杂此处仅能体现部分逻辑,以下是已经解析和存储完所有信息之后,从对应变量空间中提取最终方法的案例:

图片图片

posted @ 2025-04-03 17:47  甜菜波波  阅读(50)  评论(0)    收藏  举报