├── .gitignore ├── README.md ├── assets └── image.png ├── pom.xml └── src └── main ├── java └── com │ └── diff │ └── core │ ├── Common │ ├── Code.java │ └── Config.java │ ├── Recorders │ ├── AbstractRecord.java │ ├── ApiRecord.java │ ├── ControllerRecord.java │ ├── DubboRecord.java │ └── InterfaceRecord.java │ ├── Utils │ ├── ChainUtils.java │ ├── FileTreeUtil.java │ ├── FilterUtils.java │ ├── FullMethodNameUtil.java │ ├── ParseUtil.java │ ├── ProjectChainUtils.java │ └── XmlDiffUtils.java │ ├── Visitors │ ├── AsmAnnotationVisitor.java │ ├── AsmClassVisitor.java │ ├── AsmMethodAdapter.java │ ├── AsmSearchFilterClass.java │ ├── AsmSearchFilterMethod.java │ ├── MethodVisitorAdapter.java │ └── VisitorAdapter.java │ └── main.java └── resources └── config.json /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### 这是什么: 2 | 3 | 用于静态分析Java工程的调用链,对比新老分支代码的差异,并给出受到影响的接口作为建议,通俗一点地说:能够通过一段修改过的代码,推断出测试要回归哪些接口用例 4 | 5 | ### 设计思想: 6 | 7 | * 名词解释: 8 | * 调用关系:在A方法中调用了B方法,则形成一条调用关系 A->B 9 | * 调用链:从方法A开始,一层层追溯其所有调用关系,如:A->B->C->D,这样一条完整的调用关系链,在此称之为调用链 10 | * 抽象语法树:是源代码**语法**结构的一种**抽象**表示。 它以**树**状的形式表现编程语言的**语法**结构,**树**上的每个节点都表示源代码中的一种结构。 11 | * 核心原理: 12 | * 通过字节码工具,检索工程项目内的所有编译好的字节码文件(.class),获取项目内所有方法的「调用关系」; 13 | * 定义项目内的Controller类的方法为入口,由「调用关系」递归或循环,得到每一个api的「调用链」 14 | * 通过语法树工具,将项目内的源码文件(.java)解析为语法树,可以很容易地对比两个分支的语法树,并得到新分支内被修改过,或者新增的方法; 15 | * 遍历所有「调用链」,检查每一条调用链内是否存在被修改或更新的方法,若存在则认为本次代码更新影响到了此条调用链; 16 | * 收集所有被影响到的「调用链」,我们有理由认为,这些调用链的入口,也就是对应的Controller类的api在本次需求迭代中有更新,需要参与测试或回归测试 17 | 18 | ### 如何使用: 19 | 20 | (注:仅用于分析springboot的项目) 21 | 22 | 1.首先把你需要分析的项目代码clone两份,一份checkout到master,另一份可以checkout到dev(或者其他分支),这样表示对比这两个分支的代码 23 | 24 | 2.打开 src/main/resources/config.json 25 | 26 | ``` 27 | { 28 | "oldProjectPath": "/Users/xiaoandi/github/qqbot/qqbot", 29 | "newProjectPath": "/Users/xiaoandi/github/qqbot/diff_test/qqbot", 30 | "source": "src/main/java", 31 | "target": "target/classes", 32 | "resources": "src/main/resources" 33 | } 34 | ``` 35 | 36 | 设置好以上路径,比如: 37 | 38 | * oldProjectPath填master分支的路径(会自动检索项目下的所有子模块); 39 | * newProjectPath填dev分支的路径(同上) 40 | * source:代码路径,不用改 41 | * target:maven编译出来的路径,不用改 42 | * resources:资源路径,不用改 43 | * 编译newProjectPath项目,得到target(需要把所有子模块全部编译,oldProject不需要编译) 44 | * 运行 main 即可得到结果 45 | * 如果要打包,执行mvn命令:[mvn assembly:assembly] 得到 `static-chain-analysis-1.0-SNAPSHOT-jar-with-dependencies.jar` 46 | * 这样表示: 对于dev分支新增、修改过的源码将会被分析调用链关系。注意,相较于oldProject,newProject里删除的源码文件将不会加入分析,因为那没多大意义。 47 | * oldProjectPath和newProjectPath路径最后不需要加上"/" 48 | * 可能存在某些代码结构未考虑到,如果报错了可以沟通交流解决 49 | 50 | ### 更新计划 51 | 52 | * [X] 支持检测mybatis的xml配置变动 53 | * [X] 支持dubbo接口的调用链(从注解) 54 | * [ ] 支持dubbo接口的调用链(从xml) 55 | * [ ] 支持SpringCloud的rpc接口 56 | * [X] 为这个垃圾工具增加了一个web页面,可手动选择分支以及精确到具体的commitId来分析[static-chain-analysis-web](https://github.com/F-JH/static-chain-analysis-web) 57 | 58 | ##### 效果图如下: 59 | 60 | ![image.png](./assets/image.png) 61 | -------------------------------------------------------------------------------- /assets/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/F-JH/static-chain-analysis/208382857bcae1591245ed50cbebd18eaddaff57/assets/image.png -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.shopline 8 | static-chain-analysis 9 | 1.0-SNAPSHOT 10 | 11 | 12 | 11 13 | 11 14 | 15 | 16 | 17 | 18 | 19 | org.ow2.asm 20 | asm 21 | 7.3.1 22 | 23 | 24 | org.ow2.asm 25 | asm-commons 26 | 7.3.1 27 | 28 | 29 | org.ow2.asm 30 | asm-util 31 | 7.2-beta 32 | 33 | 34 | asm-analysis 35 | org.ow2.asm 36 | 37 | 38 | asm 39 | org.ow2.asm 40 | 41 | 42 | asm-tree 43 | org.ow2.asm 44 | 45 | 46 | 47 | 48 | com.github.javaparser 49 | javaparser-symbol-solver-core 50 | 3.24.2 51 | 52 | 53 | com.alibaba 54 | fastjson 55 | 1.2.79 56 | 57 | 58 | 59 | commons-io 60 | commons-io 61 | 2.11.0 62 | 63 | 64 | 65 | dom4j 66 | dom4j 67 | 1.6.1 68 | 69 | 70 | jaxen 71 | jaxen 72 | 1.1.6 73 | 74 | 75 | 76 | 77 | 78 | 79 | org.apache.maven.plugins 80 | maven-assembly-plugin 81 | 2.2-beta-5 82 | 83 | 84 | 85 | 86 | true 87 | com.diff.core.main 88 | 89 | 90 | 91 | jar-with-dependencies 92 | 93 | 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /src/main/java/com/diff/core/Common/Code.java: -------------------------------------------------------------------------------- 1 | package com.diff.core.Common; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | public class Code { 7 | // 各种分隔符 8 | public final static String METHOD_SPLIT = "."; 9 | public final static String METHOD_SIGNATURE_SPLIT = "|"; 10 | public final static String URL_SPLIT; 11 | public final static String SUB_CLASS_SPLIT = "$"; 12 | public final static String PACKAGE_SPLIT = "/"; 13 | public final static String POM = "pom.xml"; 14 | public final static String DUBBO = "[dubbo]"; 15 | public final static String HTTP = "[http]"; 16 | 17 | 18 | public final static Map descriptorMap = new HashMap<>(); 19 | static { 20 | // java基本类型与标识符对照表 21 | descriptorMap.put("B", "byte"); 22 | descriptorMap.put("C", "char"); 23 | descriptorMap.put("D", "double"); 24 | descriptorMap.put("F", "float"); 25 | descriptorMap.put("I", "int"); 26 | descriptorMap.put("J", "long"); 27 | descriptorMap.put("S", "short"); 28 | descriptorMap.put("Z", "boolean"); 29 | descriptorMap.put("V", "void"); 30 | descriptorMap.put("L", ""); // 表示单个类型 31 | descriptorMap.put("[", "[]"); // 表示数组类型 32 | 33 | if(System.getProperty("os.name").toLowerCase().startsWith("win")){ 34 | URL_SPLIT = "\\"; 35 | }else{ 36 | URL_SPLIT = "/"; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/diff/core/Common/Config.java: -------------------------------------------------------------------------------- 1 | package com.diff.core.Common; 2 | 3 | import com.alibaba.fastjson.JSONArray; 4 | import com.alibaba.fastjson.JSONObject; 5 | 6 | import java.io.*; 7 | import java.nio.charset.Charset; 8 | 9 | /** 10 | * 读取项目配置 11 | */ 12 | public class Config { 13 | private final static Config instance = new Config(); 14 | private JSONObject configJson; 15 | private Config(){ 16 | try{ 17 | // 读取jar包内的config.json 18 | InputStream is = this.getClass().getResourceAsStream("/config.json"); 19 | BufferedReader in = new BufferedReader((new InputStreamReader(is, Charset.forName("UTF-8")))); 20 | StringBuffer buffer = new StringBuffer(); 21 | String line; 22 | while ((line = in.readLine()) != null){ 23 | buffer.append(line); 24 | } 25 | configJson = JSONObject.parseObject(buffer.toString()); 26 | }catch (Exception e){ 27 | e.printStackTrace(); 28 | } 29 | } 30 | 31 | public static Config getInstance(){ 32 | return instance; 33 | } 34 | 35 | public void setValue(String key, String value) throws NoSuchFieldException { 36 | configJson.put(key, value); 37 | } 38 | public String getString(String key){ 39 | return configJson.getString(key); 40 | } 41 | public JSONObject getJSONObject(String key){ 42 | return configJson.getJSONObject(key); 43 | } 44 | public JSONArray getJSONArray(String key){ 45 | return configJson.getJSONArray(key); 46 | } 47 | public Integer getInteger(String key){ 48 | return configJson.getInteger(key); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/diff/core/Recorders/AbstractRecord.java: -------------------------------------------------------------------------------- 1 | package com.diff.core.Recorders; 2 | 3 | import java.util.*; 4 | 5 | /** 6 | * 保存所有抽象类,暂时用不到? 7 | */ 8 | public class AbstractRecord { 9 | private Map> record = new HashMap<>(); 10 | private final static AbstractRecord instance = new AbstractRecord(); 11 | private final Map> abstractMethodList = new HashMap<>(); 12 | 13 | private AbstractRecord(){} 14 | public static AbstractRecord getInstance(){ 15 | return instance; 16 | } 17 | public void putAbstractClass(String className){ 18 | record.computeIfAbsent(className, k -> new ArrayList<>()); 19 | abstractMethodList.computeIfAbsent(className, k -> new HashMap<>()); 20 | } 21 | public void putAbstractEntry(String abstractClassName, String entryClassName){ 22 | record.computeIfAbsent(abstractClassName, k -> new ArrayList<>()); 23 | abstractMethodList.computeIfAbsent(abstractClassName, k -> new HashMap<>()); 24 | record.get(abstractClassName).add(entryClassName); 25 | } 26 | public void putMethod(String abstractClassName, String methodName, boolean isAbstract){ 27 | abstractMethodList.get(abstractClassName).put(methodName, isAbstract); 28 | } 29 | public boolean containAbstract(String abstractClassName){ 30 | return record.containsKey(abstractClassName); 31 | } 32 | public List getEntries(String className){ 33 | return record.get(className); 34 | } 35 | public Map getMethod(String abstractClassName){ 36 | return abstractMethodList.get(abstractClassName); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/diff/core/Recorders/ApiRecord.java: -------------------------------------------------------------------------------- 1 | package com.diff.core.Recorders; 2 | 3 | import java.util.*; 4 | 5 | /** 6 | * 保存所有Controller类方法与Api路径的对应关系 7 | */ 8 | public class ApiRecord { 9 | private Map> record = new HashMap<>(); 10 | private final static ApiRecord instance = new ApiRecord(); 11 | 12 | private ApiRecord(){} 13 | 14 | public static ApiRecord getInstance() { 15 | return instance; 16 | } 17 | 18 | public void putApi(String fullMethodName, Set api){ 19 | // record.computeIfAbsent(fullMethodName, k -> new HashSet<>()); 20 | record.put(fullMethodName, api); 21 | } 22 | 23 | public Set getApis(String fullMethodName){ 24 | return record.get(fullMethodName); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/diff/core/Recorders/ControllerRecord.java: -------------------------------------------------------------------------------- 1 | package com.diff.core.Recorders; 2 | 3 | import java.util.*; 4 | 5 | /** 6 | * 保存所有Controller方法,在调用链入口可使用 7 | */ 8 | public class ControllerRecord { 9 | private Map> controllers = new HashMap<>(); 10 | private static final ControllerRecord instance = new ControllerRecord(); 11 | 12 | private ControllerRecord(){} 13 | public static ControllerRecord getInstance(){ 14 | return instance; 15 | } 16 | 17 | public void putControlClass(String ControlClassName){ 18 | controllers.computeIfAbsent(ControlClassName, k -> new ArrayList<>()); 19 | } 20 | 21 | public void putControlMethod(String ControlClassName, String ControlMethodName){ 22 | controllers.computeIfAbsent(ControlClassName, k -> new ArrayList<>()); 23 | controllers.get(ControlClassName).add(ControlMethodName); 24 | } 25 | 26 | public List getApiFromControlClassName(String ControlClassName){ 27 | return controllers.get(ControlClassName); 28 | } 29 | 30 | public Set getControllers(){ 31 | return controllers.keySet(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/diff/core/Recorders/DubboRecord.java: -------------------------------------------------------------------------------- 1 | package com.diff.core.Recorders; 2 | 3 | import java.util.ArrayList; 4 | import java.util.HashMap; 5 | import java.util.List; 6 | import java.util.Map; 7 | 8 | /** 9 | * 保存所有dubbo的类 10 | */ 11 | public class DubboRecord { 12 | private static final List dubboMethods = new ArrayList<>(); 13 | 14 | 15 | public static void putDubboMethod(String fullMethodName){ 16 | dubboMethods.add(fullMethodName); 17 | } 18 | 19 | public static boolean contains(String fullMethodName){ 20 | return dubboMethods.contains(fullMethodName); 21 | } 22 | 23 | public static List getList(){ 24 | return dubboMethods; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/diff/core/Recorders/InterfaceRecord.java: -------------------------------------------------------------------------------- 1 | package com.diff.core.Recorders; 2 | 3 | import java.util.ArrayList; 4 | import java.util.HashMap; 5 | import java.util.List; 6 | import java.util.Map; 7 | 8 | /** 9 | * 保存所有Interface与实体类的映射 10 | */ 11 | public class InterfaceRecord { 12 | private final Map> interfaceList = new HashMap<>(); 13 | private final Map> interfaceMethodList = new HashMap<>(); 14 | private static final InterfaceRecord instance = new InterfaceRecord(); 15 | 16 | 17 | private InterfaceRecord(){} 18 | public static InterfaceRecord getInstance(){ 19 | return instance; 20 | } 21 | 22 | public void putInterfaceClass(String interfaceClassName){ 23 | interfaceList.computeIfAbsent(interfaceClassName, k -> new ArrayList<>()); 24 | interfaceMethodList.computeIfAbsent(interfaceClassName, k -> new HashMap<>()); 25 | } 26 | public void putInterfaceEntry(String interfaceClassName, String entryClassName){ 27 | interfaceList.computeIfAbsent(interfaceClassName, k -> new ArrayList<>()); 28 | interfaceMethodList.computeIfAbsent(interfaceClassName, k -> new HashMap<>()); 29 | interfaceList.get(interfaceClassName).add(entryClassName); 30 | } 31 | 32 | public void putMethod(String interfaceClassName, String methodName, boolean isAbstract){ 33 | interfaceMethodList.get(interfaceClassName).put(methodName, isAbstract); 34 | } 35 | 36 | public boolean containInterface(String interfaceClassName){ 37 | return interfaceList.containsKey(interfaceClassName); 38 | } 39 | 40 | public List getEntries(String interfaceClassName){ 41 | return interfaceList.get(interfaceClassName); 42 | } 43 | 44 | public Map getMethod(String interfaceClassName){ 45 | return interfaceMethodList.get(interfaceClassName); 46 | } 47 | 48 | public Map> getInterfaceList() { 49 | return interfaceList; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/diff/core/Utils/ChainUtils.java: -------------------------------------------------------------------------------- 1 | package com.diff.core.Utils; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.diff.core.Recorders.AbstractRecord; 5 | import com.diff.core.Recorders.InterfaceRecord; 6 | import com.diff.core.Visitors.AsmClassVisitor; 7 | import com.diff.core.Visitors.AsmSearchFilterClass; 8 | import org.objectweb.asm.ClassReader; 9 | import org.objectweb.asm.ClassVisitor; 10 | import org.objectweb.asm.ClassWriter; 11 | 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | import java.util.Map; 15 | import java.util.Stack; 16 | 17 | import static com.diff.core.Common.Code.*; 18 | 19 | public class ChainUtils { 20 | /** 21 | * { 22 | * "botApi|(Lcom/alibaba/fastjson/JSONObject;)Ljava/lang/String;":[ 23 | * "com/bot/server/qqBot/envGet:getByEnv|(Ljava/lang/String;)Ljava/lang/String;", 24 | * "com/bot/server/qqBot/mapper/postMethod:run|(Lcom/alibaba/fastjson/JSONObject;Lcom/alibaba/fastjson/JSONObject;Ljava/lang/Integer;)Ljava/lang/String;" 25 | * ], 26 | * "saveToMysql|()Z":[ 27 | * "com/bot/server/qqBot/server/msgManage:saveToMysqlSignal|()V" 28 | * ], 29 | * "test|(Ljava/lang/String;)Ljava/lang/String;":[ 30 | * 31 | * ], 32 | * "|()V":[ 33 | * 34 | * ], 35 | * "showList|()Ljava/lang/String;":[ 36 | * "com/bot/server/qqBot/server/msgManage:getMsgList|()Ljava/util/Map;" 37 | * ], 38 | * "showEnvs|()Ljava/util/Map;":[ 39 | * "com/bot/server/qqBot/envGet:getByEnv|(Ljava/lang/String;)Ljava/lang/String;" 40 | * ], 41 | * "checkThread|(Ljava/lang/String;)Ljava/util/Map;":[ 42 | * "com/bot/server/qqBot/server/msgManage:getAllThreadStatus|()Ljava/util/Map;", 43 | * "com/bot/server/qqBot/server/msgManage:getGroupThreadStatus|(Ljava/lang/String;)Ljava/util/Map;" 44 | * ] 45 | * } 46 | * @param classfileBuffer 47 | * @return 48 | */ 49 | public static Map> getRelationShipFromClassBuffer(byte[] classfileBuffer){ 50 | ClassReader cr = new ClassReader(classfileBuffer); 51 | ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); 52 | AsmClassVisitor cv = new AsmClassVisitor(cw); 53 | cr.accept(cv, ClassReader.SKIP_FRAMES); 54 | 55 | return cv.getMethodRelations(); 56 | } 57 | 58 | 59 | /** 60 | * 先visit一遍,把项目的所有className存到FilterUtils中 61 | * @param classfileBuffer 62 | */ 63 | public static void scanForClassName(byte[] classfileBuffer){ 64 | ClassReader cr = new ClassReader(classfileBuffer); 65 | ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); 66 | ClassVisitor cv = new AsmSearchFilterClass(cw); 67 | cr.accept(cv, ClassReader.SKIP_FRAMES); 68 | } 69 | 70 | /** 71 | * { 72 | * "com/bot/server/qqBot/envGet:getByEnv|(Ljava/lang/String;)Ljava/lang/String;":{ 73 | * "com/bot/server/qqBot/mapper/postMethod:run|(Lcom/alibaba/fastjson/JSONObject;Lcom/alibaba/fastjson/JSONObject;Ljava/lang/Integer;)Ljava/lang/String":{ 74 | * ... 75 | * } 76 | * }, 77 | * "com/bot/server/qqBot/envGet:test|(Ljava/lang/String;)Ljava/lang/String;":{ 78 | * ... 79 | * } 80 | * } 81 | * @param relationShips 82 | * @param startFullMethodName 83 | * @return 84 | */ 85 | public static JSONObject getJSONChainFromRelationShip(Map>> relationShips, String startFullMethodName){ 86 | InterfaceRecord interfaceRecord = InterfaceRecord.getInstance(); 87 | AbstractRecord abstractRecord = AbstractRecord.getInstance(); 88 | 89 | String className = startFullMethodName.substring(0, startFullMethodName.indexOf(METHOD_SPLIT)); 90 | String methodName = startFullMethodName.substring(startFullMethodName.indexOf(METHOD_SPLIT)+1); 91 | Stack stack = new Stack<>(); 92 | JSONObject relation = new JSONObject(); 93 | List startChain = new ArrayList<>(); 94 | startChain.add(startFullMethodName); 95 | StackJSONNode initNode = new StackJSONNode(className, methodName, startChain, relation); 96 | stack.push(initNode); 97 | while(!stack.empty()){ 98 | StackJSONNode currentNode = stack.pop(); 99 | String currentClassName = currentNode.getClassName(); 100 | // if(currentClassName.equals("com/hiido/controllers/center/UserCenterController")) 101 | // System.out.println("haha"); 102 | String currentMethodName = currentNode.getMethodName(); 103 | List currentChain = currentNode.getChain(); 104 | List methodRelationShip = relationShips.get(currentClassName).get(currentMethodName); 105 | JSONObject currentRelation = currentNode.getChainNode(); 106 | // 处理接口或抽象类 107 | if(interfaceRecord.containInterface(currentClassName) || abstractRecord.containAbstract(currentClassName)){ 108 | List entries = interfaceRecord.getEntries(currentClassName); 109 | Map abstractMethod = interfaceRecord.getMethod(currentClassName); 110 | if(entries==null){ 111 | entries = abstractRecord.getEntries(currentClassName); 112 | abstractMethod = abstractRecord.getMethod(currentClassName); 113 | } 114 | for(String entryClassName:entries){ 115 | // 存在default方法,需要判断是否被实体类复写 116 | if(!abstractMethod.get(currentMethodName)){ 117 | // default或者抽象类中有实体的方法 118 | if(relationShips.get(entryClassName).containsKey(currentMethodName)){ 119 | // 实体类有复写这个方法,但是静态分析下无法判断实际跑的是哪一个方法,所以都要加进去 120 | methodRelationShip.add(entryClassName + METHOD_SPLIT + currentMethodName); 121 | } 122 | }else{ 123 | methodRelationShip.add(entryClassName + METHOD_SPLIT + currentMethodName); 124 | } 125 | } 126 | } 127 | if(methodRelationShip.size()>0){ 128 | for(String fullMethodName:methodRelationShip){ 129 | // 解开methodRelationShip,加到stack中,并与往json里添加 130 | JSONObject tmpRelation = new JSONObject(); 131 | currentRelation.put(fullMethodName, tmpRelation); 132 | if(!currentChain.contains(fullMethodName)){ 133 | String tmpClassName = fullMethodName.substring(0, fullMethodName.indexOf(METHOD_SPLIT)); 134 | String tmpMethodName = fullMethodName.substring(fullMethodName.indexOf(METHOD_SPLIT)+1); 135 | List tmpChain = new ArrayList(currentChain); 136 | tmpChain.add(fullMethodName); 137 | stack.add(new StackJSONNode(tmpClassName, tmpMethodName, tmpChain, tmpRelation)); 138 | } 139 | } 140 | } 141 | } 142 | return relation; 143 | } 144 | 145 | private static class StackJSONNode{ 146 | private String className; 147 | private String methodName; 148 | private List chain; 149 | private JSONObject chainNode; 150 | public StackJSONNode(String className, String methodName, List chain, JSONObject chainNode){ 151 | this.className = className; 152 | this.methodName = methodName; 153 | this.chain = chain; 154 | this.chainNode = chainNode; 155 | } 156 | 157 | public List getChain() { 158 | return chain; 159 | } 160 | 161 | public String getClassName() { 162 | return className; 163 | } 164 | 165 | public String getMethodName() { 166 | return methodName; 167 | } 168 | 169 | public JSONObject getChainNode() { 170 | return chainNode; 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/main/java/com/diff/core/Utils/FileTreeUtil.java: -------------------------------------------------------------------------------- 1 | package com.diff.core.Utils; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.diff.core.Common.Config; 5 | import org.apache.commons.io.FileUtils; 6 | import org.dom4j.DocumentException; 7 | import org.w3c.dom.Document; 8 | import org.w3c.dom.NodeList; 9 | import org.xml.sax.SAXException; 10 | 11 | import javax.xml.parsers.DocumentBuilder; 12 | import javax.xml.parsers.DocumentBuilderFactory; 13 | import javax.xml.parsers.ParserConfigurationException; 14 | import java.io.File; 15 | import java.io.IOException; 16 | import java.nio.file.NotDirectoryException; 17 | import java.security.MessageDigest; 18 | import java.security.NoSuchAlgorithmException; 19 | import java.util.*; 20 | 21 | import static com.diff.core.Common.Code.*; 22 | 23 | /** 24 | * 文件结构树工具 25 | */ 26 | public class FileTreeUtil { 27 | /** 28 | * 对比两个项目的所有pom.xml,得到新增的子模块和非新增的子模块,不关注删除的模块 29 | * @param oldProject 30 | * @param newProject 31 | * @return 32 | * { 33 | * "newModules": [ 34 | * // 新的子模块 35 | * "/Users/xiaoandi/github/qqbot/diff_test/qqbot", 36 | * ... 37 | * ], 38 | * "normalModules": [ 39 | * // 不是新的子模块 40 | * "/Users/xiaoandi/github/qqbot/diff_test/qqbot", 41 | * ... 42 | * ] 43 | * } 44 | * @throws Exception 45 | */ 46 | public static Map> compireToPomTree(String oldProject, String newProject) throws Exception{ 47 | File oldRoot = new File(oldProject); 48 | File newRoot = new File(newProject); 49 | if(!oldRoot.exists() || !newRoot.exists() || !oldRoot.isDirectory() || !newRoot.isDirectory()) 50 | throw new NotDirectoryException("需要传入完整路径"); 51 | Config config = Config.getInstance(); 52 | File newRootPom = new File(newProject + URL_SPLIT + POM); 53 | String oldRootPomPath = oldProject + URL_SPLIT + POM; 54 | String newRootPomPath = newProject + URL_SPLIT + POM; 55 | 56 | List newModules = new ArrayList<>(); // 新增的模块,不需要走parser对比,直接检索并加入 new Method 列表 57 | List normalModules = new ArrayList<>(); // 非新增模块,需要走正常流程 58 | File newRootSource = new File(newProject + URL_SPLIT + config.getString("source")); 59 | File oldRootSource = new File(oldProject + URL_SPLIT + config.getString("source")); 60 | 61 | Stack start = new Stack<>(); 62 | if(newRootSource.exists() && newRootSource.listFiles().length > 0){ 63 | // 根目录有源码 64 | if(oldRootSource.exists() && oldRootSource.listFiles().length > 0){ 65 | // 旧项目根目录也TM有源码 66 | normalModules.add(newProject); 67 | }else{ 68 | newModules.add(newProject); 69 | } 70 | } 71 | for(String module:listModules(newRootPom)){ 72 | // 初始化start 73 | start.push(new FileNode( 74 | new StringBuilder(""), 75 | new File(newProject + URL_SPLIT + module + URL_SPLIT + POM) 76 | )); 77 | } 78 | while(!start.empty()){ 79 | FileNode topItem = start.pop(); 80 | String currentModuleName = topItem.fileNode.getAbsolutePath(); 81 | currentModuleName = currentModuleName.substring(0, currentModuleName.lastIndexOf(URL_SPLIT)); 82 | currentModuleName = currentModuleName.substring(currentModuleName.lastIndexOf(URL_SPLIT)+1); 83 | File currentModuleSource = new File(newProject + topItem.relativePath.toString() + URL_SPLIT + currentModuleName + URL_SPLIT + config.getString("source")); 84 | File oldModuleSource = new File(oldProject + topItem.relativePath.toString() + URL_SPLIT + currentModuleName + URL_SPLIT + config.getString("source")); 85 | // 判断当前module是否有源码 86 | if(currentModuleSource.exists() && currentModuleSource.listFiles().length > 0){ 87 | if(oldModuleSource.exists() && oldModuleSource.listFiles().length > 0){ 88 | normalModules.add(newProject + topItem.relativePath.toString() + URL_SPLIT + currentModuleName); 89 | }else{ 90 | newModules.add(newProject + topItem.relativePath.toString() + URL_SPLIT + currentModuleName); 91 | } 92 | } 93 | List list = listModules(topItem.fileNode); 94 | 95 | for(String subModule:list){ 96 | if(!URL_SPLIT.equals(PACKAGE_SPLIT)){ 97 | subModule = subModule.replace(PACKAGE_SPLIT, URL_SPLIT); 98 | } 99 | String currentPath = topItem.relativePath.append(URL_SPLIT).append(currentModuleName).toString(); 100 | start.push(new FileNode( 101 | new StringBuilder(currentPath), 102 | new File(newProject + currentPath + URL_SPLIT + subModule + URL_SPLIT + POM) 103 | )); 104 | } 105 | } 106 | Map> result = new HashMap<>(); 107 | result.put("newModules", newModules); 108 | result.put("normalModules", normalModules); 109 | return result; 110 | } 111 | 112 | /** 113 | * 获取目录下所有文件的完整路径 114 | * @param dirPath 115 | * @return 116 | * @throws NotDirectoryException 117 | */ 118 | public static List scanForDirectory(String dirPath) throws NotDirectoryException { 119 | List result = new ArrayList<>(); 120 | File dirFile = new File(dirPath); 121 | if (!dirFile.exists() || !dirFile.isDirectory()) 122 | throw new NotDirectoryException("「" + dirPath + "」不是文件夹"); 123 | File[] files = dirFile.listFiles(); 124 | if(files==null) 125 | return null; 126 | Stack stack = new Stack<>(); 127 | for(File file:files) 128 | stack.push(new FileNode(new StringBuilder(""), file)); 129 | while(!stack.empty()){ 130 | FileNode topItem = stack.pop(); 131 | if(topItem.fileNode.isFile()){ 132 | String fileName = topItem.fileNode.getName(); 133 | if(fileName.substring(fileName.lastIndexOf('.')).equals(".java") || fileName.substring(fileName.lastIndexOf('.')).equals(".class")){ 134 | String topItemPath = topItem.relativePath.append(URL_SPLIT).append(fileName).toString(); 135 | result.add(dirPath + topItemPath); 136 | } 137 | }else{ 138 | String dirName = topItem.fileNode.getName(); 139 | String topItemPath = topItem.relativePath.append(URL_SPLIT).append(dirName).toString(); 140 | File[] fs = topItem.fileNode.listFiles(); 141 | for(File f:fs) 142 | stack.push(new FileNode(new StringBuilder(topItemPath), f)); 143 | } 144 | } 145 | return result; 146 | } 147 | 148 | /** 149 | * 比较两个文件夹的目录结构,得到所有被修改、新增的文件和文件夹 150 | * @param oldPath 旧路径,需要传入完整路径 151 | * @param newPath 新路径,需要传入完整路径 152 | * @return 只会记录新建的java文件、修改的java文件以及新建的文件夹,不会返回删除的内容 153 | * @throws NotDirectoryException 154 | */ 155 | public static Map> compireToPath(String oldPath, String newPath) throws NotDirectoryException { 156 | File oldRoot = new File(oldPath); 157 | File newRoot = new File(newPath); 158 | if(!oldRoot.exists() || !newRoot.exists() || !oldRoot.isDirectory() || !newRoot.isDirectory()) 159 | throw new NotDirectoryException("需要传入完整路径"); 160 | 161 | Map> result = new HashMap<>(); 162 | List newFiles = new ArrayList<>(); 163 | List modifyFiles = new ArrayList<>(); 164 | List newDirectorys = new ArrayList<>(); 165 | 166 | Stack newStack = new Stack<>(); 167 | // 初始化Stack 168 | File[] newRootFiles = newRoot.listFiles(); 169 | if(newRootFiles==null) 170 | return null; 171 | for(File f:newRootFiles) 172 | newStack.push(new FileNode(new StringBuilder(""), f)); 173 | while(!newStack.empty()){ 174 | FileNode topItem = newStack.pop(); 175 | if(topItem.fileNode.isFile()){ 176 | // 文件类型只处理.java文件,其他的不管 177 | // 能来到这说明至少在oldPath中有同个文件夹 178 | String fileName = topItem.fileNode.getName(); 179 | if(fileName.substring(fileName.lastIndexOf('.')).equals(".java")){ 180 | // 检查 oldPath 中是否有此java文件,有的话再对比byte是否有修改 181 | String topItemPath = topItem.relativePath.append(URL_SPLIT).append(fileName).toString(); 182 | File oldFile = new File(oldPath + topItemPath); 183 | if(oldFile.exists()){ 184 | if(oldFile.length() != topItem.fileNode.length() || !Arrays.equals(getFileBytes(oldFile), getFileBytes(topItem.fileNode))) 185 | modifyFiles.add(topItemPath); 186 | }else{ 187 | newFiles.add(topItemPath); 188 | } 189 | } 190 | }else{ 191 | // 如果是文件夹,先判断oldPath里是否存在,不存在就是新增的文件夹,直接添加到newDirectory中,不用遍历文件 192 | // 如果不是新增的,则把文件夹内的元素解开,添加到Stack中 193 | String dirName = topItem.fileNode.getName(); 194 | String topItemPath = topItem.relativePath.append(URL_SPLIT).append(dirName).toString(); 195 | File oldDir = new File(oldPath + topItemPath); 196 | if(oldDir.exists()){ 197 | File[] files = topItem.fileNode.listFiles(); 198 | for(File file:files) 199 | newStack.push(new FileNode(new StringBuilder(topItemPath), file)); 200 | }else{ 201 | newDirectorys.add(topItemPath); 202 | } 203 | } 204 | } 205 | 206 | result.put("newFiles", newFiles); 207 | result.put("modifyFiles", modifyFiles); 208 | result.put("newDirectorys", newDirectorys); 209 | return result; 210 | } 211 | 212 | public static Map scanMybatisXml(String path) throws NotDirectoryException, DocumentException { 213 | Map result = new HashMap<>(); 214 | File dirFile = new File(path); 215 | if (!dirFile.exists() || !dirFile.isDirectory()) 216 | throw new NotDirectoryException("「" + path + "」不是文件夹"); 217 | File[] files = dirFile.listFiles(); 218 | if(files==null) 219 | return null; 220 | Stack stack = new Stack<>(); 221 | for(File file:files) 222 | stack.push(new FileNode(new StringBuilder(""), file)); 223 | while(!stack.empty()){ 224 | FileNode topItem = stack.pop(); 225 | if(topItem.fileNode.isFile()){ 226 | String fileName = topItem.fileNode.getName(); 227 | if(fileName.substring(fileName.lastIndexOf('.')).equals(".xml")){ 228 | XmlDiffUtils xmlDiffUtils = new XmlDiffUtils(topItem.fileNode); 229 | if(xmlDiffUtils.isMapper()){ 230 | String topItemPath = topItem.relativePath.append(URL_SPLIT).append(fileName).toString(); 231 | result.put(xmlDiffUtils.getMapperNameSpace(), path + topItemPath); 232 | } 233 | } 234 | }else{ 235 | String dirName = topItem.fileNode.getName(); 236 | String topItemPath = topItem.relativePath.append(URL_SPLIT).append(dirName).toString(); 237 | File[] fs = topItem.fileNode.listFiles(); 238 | for(File f:fs) 239 | stack.push(new FileNode(new StringBuilder(topItemPath), f)); 240 | } 241 | } 242 | return result; 243 | } 244 | 245 | private static List listModules(File pom) throws ParserConfigurationException, IOException, SAXException { 246 | List result = new ArrayList<>(); 247 | DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); 248 | Document document; 249 | DocumentBuilder db = dbf.newDocumentBuilder(); 250 | document = db.parse(pom); 251 | NodeList modules = document.getElementsByTagName("module"); 252 | if(modules.getLength() == 0){ 253 | return result; 254 | }else{ 255 | for(int i=0; i projectPackage = new HashSet<>(); 8 | private static final Set projectMehtods = new HashSet<>(); 9 | private static final Set requestAnnotation = new HashSet<>(); 10 | private static final Set controllerAnnotation = new HashSet<>(); 11 | private static final Set dubboAnnotation = new HashSet<>(); 12 | private static final Set mybatisTagName = new HashSet<>(); 13 | static { 14 | // Request的注解 15 | requestAnnotation.add("Lorg/springframework/web/bind/annotation/RequestMapping;"); 16 | requestAnnotation.add("Lorg/springframework/web/bind/annotation/GetMapping;"); 17 | requestAnnotation.add("Lorg/springframework/web/bind/annotation/PostMapping;"); 18 | requestAnnotation.add("Lorg/springframework/web/bind/annotation/DeleteMapping;"); 19 | requestAnnotation.add("Lorg/springframework/web/bind/annotation/PatchMapping;"); 20 | requestAnnotation.add("Lorg/springframework/web/bind/annotation/PutMapping;"); 21 | requestAnnotation.add("Lorg/springframework/web/bind/annotation/MessageMapping;"); 22 | requestAnnotation.add("Lorg/springframework/web/bind/annotation/SubscribeMapping;"); 23 | 24 | // Controller 的注解 25 | controllerAnnotation.add("Lorg/springframework/web/bind/annotation/RestController;"); 26 | controllerAnnotation.add("Lorg/springframework/stereotype/Controller;"); 27 | 28 | // DubboService 的注解 29 | dubboAnnotation.add("Lorg/apache/dubbo/config/annotation/DubboService;"); 30 | dubboAnnotation.add("Lcom/alibaba/dubbo/config/annotation/Service;"); 31 | dubboAnnotation.add("Lorg/apache/dubbo/config/annotation/Service;"); 32 | 33 | // mybatis的xml配置的tagName 34 | mybatisTagName.add("insert"); 35 | mybatisTagName.add("delete"); 36 | mybatisTagName.add("update"); 37 | mybatisTagName.add("select"); 38 | } 39 | 40 | public static boolean isNeedInject(String className){ 41 | if(null == className) 42 | return false; 43 | for(String prefix : projectPackage){ 44 | if(className.equals(prefix)) 45 | return true; 46 | } 47 | return false; 48 | } 49 | 50 | public static boolean isNeedInjectMethod(String methodName){ 51 | if(null == methodName) 52 | return false; 53 | return projectMehtods.contains(methodName); 54 | } 55 | 56 | public static boolean isControllerAnnotation(String annoDesc){ 57 | if(null == annoDesc) 58 | return false; 59 | return controllerAnnotation.contains(annoDesc); 60 | } 61 | 62 | public static boolean isRequestAnnotation(String annoDesc){ 63 | if(null == annoDesc) 64 | return false; 65 | return requestAnnotation.contains(annoDesc); 66 | } 67 | 68 | public static boolean isDubboAnnotation(String annoDesc){ 69 | if(null == annoDesc) 70 | return false; 71 | return dubboAnnotation.contains(annoDesc); 72 | } 73 | 74 | public static boolean isCURD(String tagName){ 75 | if(null == tagName) 76 | return false; 77 | return mybatisTagName.contains(tagName); 78 | } 79 | 80 | public static void addProjectPackage(String className){ 81 | projectPackage.add(className); 82 | } 83 | public static void addProjectMethod(String fullMethodName){ 84 | projectMehtods.add(fullMethodName); 85 | } 86 | 87 | public static Set getProjectPackage() { 88 | return projectPackage; 89 | } 90 | 91 | public static Set getProjectMehtods() { 92 | return projectMehtods; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/com/diff/core/Utils/FullMethodNameUtil.java: -------------------------------------------------------------------------------- 1 | package com.diff.core.Utils; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.Stack; 6 | import java.util.regex.Matcher; 7 | import java.util.regex.Pattern; 8 | 9 | import static com.diff.core.Common.Code.PACKAGE_SPLIT; 10 | import static com.diff.core.Common.Code.descriptorMap; 11 | 12 | public class FullMethodNameUtil { 13 | public static void main(String[] args){ 14 | String test = "Ljava/lang/String;Ljava/lang/String;"; 15 | String test1 = "Ljava/lang/String;Ljava/lang/String;[Ljava/util/Map;"; 16 | String test2 = "Ljava/lang/String;[Ljava/lang/String;[Ljava/util/Map;>;"; 17 | System.out.println(genericsParameter(test2)); 18 | } 19 | 20 | public static String getMethodSignatureName(String name, String descriptor){ 21 | StringBuilder signa = new StringBuilder(); 22 | signa.append(name); 23 | signa.append("("); 24 | String patter = "(L.*?;|\\[{0,2}L.*?;|[ZCBSIFJDV]|\\[{0,2}[ZCBSIFJDV]{1})"; 25 | Matcher parameterMatcher = Pattern.compile(patter).matcher(descriptor.substring(0, descriptor.lastIndexOf(')') + 1)); 26 | while(parameterMatcher.find()){ 27 | String param = parameterMatcher.group(1); 28 | if(param.length()==1){ 29 | // V 30 | signa.append(descriptorMap.get(param)).append(", "); 31 | }else{ 32 | String type = param.substring(0,1); 33 | if(descriptorMap.get(param.substring(1)) != null){ 34 | // [V 35 | signa.append(descriptorMap.get(param.substring(1))).append(", "); 36 | }else{ 37 | // Ljava/lang/Object; [Ljava/lang/Object; 38 | String typeName = param.substring(param.lastIndexOf(PACKAGE_SPLIT) + 1, param.length()-1); 39 | signa.append(typeName).append(descriptorMap.get(type)).append(", "); 40 | } 41 | } 42 | } 43 | if(!descriptor.startsWith("()")) 44 | signa.delete(signa.length()-2, signa.length()); 45 | signa.append(")"); 46 | String returnType = descriptor.substring(descriptor.lastIndexOf(')') + 1); 47 | if(returnType.length()==1){ 48 | signa.append(descriptorMap.get(returnType)); 49 | }else{ 50 | String type = returnType.substring(0,1); 51 | if(descriptorMap.get(returnType.substring(1)) != null){ 52 | signa.append(descriptorMap.get(returnType.substring(1))).append("[]"); 53 | }else{ 54 | String typeName = returnType.substring(returnType.lastIndexOf(PACKAGE_SPLIT) + 1, returnType.length()-1); 55 | signa.append(typeName).append(descriptorMap.get(type)); 56 | } 57 | } 58 | 59 | return signa.toString(); 60 | } 61 | 62 | public static String getMethodSignatureName(String name, String descriptor, String signature){ 63 | StringBuilder fullName = new StringBuilder(""); 64 | fullName.append(name).append("("); 65 | String pattern = "(L[^;]+<.*?(>;)+|\\[{0,2}L[^;]+<.*?(>;)+|L.*?;|\\[{0,2}L.*?;|[ZCBSIFJDV]|\\[{0,2}[ZCBSIFJDV]{1})"; 66 | Matcher matcher; 67 | if(signature != null){ 68 | matcher = Pattern.compile(pattern).matcher(signature); 69 | }else{ 70 | matcher = Pattern.compile(pattern).matcher(descriptor); 71 | } 72 | while(matcher.find()){ 73 | String parameter = matcher.group(1); 74 | if(parameter.length()==1){ 75 | // V 76 | fullName.append(descriptorMap.get(parameter)).append(", "); 77 | }else{ 78 | String type = parameter.substring(0,1); 79 | if(descriptorMap.get(parameter.substring(1)) != null){ 80 | // [V 81 | fullName.append(descriptorMap.get(parameter.substring(1))).append(", "); 82 | }else{ 83 | // Ljava/lang/Object; [Ljava/lang/Object; [Ljava/util/Map; 84 | if(parameter.contains("<")){ 85 | // [Ljava/util/Map; 86 | String tmp = parameter.substring(0, parameter.indexOf("<")); 87 | String typeName = tmp.substring(tmp.lastIndexOf(PACKAGE_SPLIT) + 1) 88 | + FullMethodNameUtil.genericsParameter(parameter.substring( 89 | parameter.indexOf("<")+1, parameter.length()-2 90 | )); 91 | 92 | } 93 | } 94 | } 95 | } 96 | 97 | return fullName.toString(); 98 | } 99 | 100 | /** 101 | * 栈+循环处理泛型的入参 102 | * @param signature "Ljava/lang/String;Ljava/lang/String;" 103 | * @return "" ">" 104 | */ 105 | public static String genericsParameter(String signature){ 106 | // <*> <> 107 | if(signature.length() < 2) 108 | return signature; 109 | StringBuilder result = new StringBuilder("<"); 110 | List parameterList = new ArrayList<>(); 111 | Stack mainStack = new Stack<>(); 112 | Stack subStack = new Stack<>(); 113 | Pattern pattern = Pattern.compile("(L[^;]+<.*?(>;)+|\\[{0,2}L[^;]+<.*?(>;)+|L.*?;|\\[{0,2}L.*?;|[ZCBSIFJDV]|\\[{0,2}[ZCBSIFJDV]{1})"); 114 | Matcher matcher = pattern.matcher(signature); 115 | // 初始化stack 116 | while(matcher.find()) 117 | mainStack.push(matcher.group(1)); 118 | while(!mainStack.empty()){ 119 | // 遍历stack处理泛型的入参 120 | String parameter = mainStack.pop(); 121 | if(parameter.contains("<")){ 122 | // 又是带泛型的参数,解包并加入mainStack,subStack相应地加入本节点的className Node 123 | Matcher subMatcher = pattern.matcher(parameter.substring(parameter.indexOf('<')+1, parameter.length()-2)); 124 | String className = parameter.substring(0, parameter.indexOf("<")); 125 | int count = 0; 126 | while(subMatcher.find()){ 127 | count++; 128 | String find = subMatcher.group(1); 129 | mainStack.push(find); 130 | } 131 | subStack.push(new Node(className, count)); 132 | }else{ 133 | // 普通参数 134 | String name = parameter.substring(parameter.lastIndexOf(PACKAGE_SPLIT)+1, parameter.length()-1); 135 | if(descriptorMap.get(parameter.substring(0,1)).equals("[]")) 136 | name += "[]"; 137 | if(!subStack.empty()){ 138 | Node top = subStack.peek(); 139 | top.push(name); 140 | if(top.isEnd()){ 141 | subStack.pop(); 142 | if(!subStack.empty()) 143 | subStack.peek().push(top.getFullName()); 144 | else 145 | parameterList.add(top.getFullName()); 146 | } 147 | }else{ 148 | parameterList.add(name); 149 | } 150 | } 151 | } 152 | 153 | for(int i=parameterList.size()-1;i>=0;i--){ 154 | if(result.toString().equals("<")) 155 | result.append(parameterList.get(i)); 156 | else 157 | result.append(", ").append(parameterList.get(i)); 158 | } 159 | result.append(">"); 160 | return result.toString(); 161 | } 162 | 163 | private static class Node{ 164 | private final String className; 165 | private int count = 0 ; 166 | private List subParameter = new ArrayList<>(); 167 | public Node(String className, int count){ 168 | this.className = className; 169 | this.count = count; 170 | } 171 | 172 | public String getClassName() { 173 | return className; 174 | } 175 | 176 | public int getCount() { 177 | return count; 178 | } 179 | 180 | public void push(String parameter){ 181 | subParameter.add(parameter); 182 | count--; 183 | } 184 | 185 | public String getFullName() { 186 | StringBuilder stringBuilder = new StringBuilder("<"); 187 | for(int i=subParameter.size()-1;i>=0;i--){ 188 | if(stringBuilder.toString().equals("<")) 189 | stringBuilder.append(subParameter.get(i)); 190 | else 191 | stringBuilder.append(", ").append(subParameter.get(i)); 192 | } 193 | stringBuilder.append(">"); 194 | 195 | if(descriptorMap.get(className.substring(0,1)).equals("[]")){ 196 | stringBuilder.append("[]"); 197 | } 198 | return className.substring(className.lastIndexOf(PACKAGE_SPLIT)+1) + stringBuilder; 199 | } 200 | 201 | public boolean isEnd(){ 202 | return count == 0; 203 | } 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/main/java/com/diff/core/Utils/ParseUtil.java: -------------------------------------------------------------------------------- 1 | package com.diff.core.Utils; 2 | 3 | import com.diff.core.Common.Config; 4 | import com.diff.core.Visitors.MethodVisitorAdapter; 5 | import com.github.javaparser.StaticJavaParser; 6 | import com.github.javaparser.ast.CompilationUnit; 7 | import com.github.javaparser.ast.Node; 8 | import com.github.javaparser.ast.body.MethodDeclaration; 9 | import com.github.javaparser.printer.YamlPrinter; 10 | 11 | import java.io.File; 12 | import java.io.FileNotFoundException; 13 | import java.util.ArrayList; 14 | import java.util.HashMap; 15 | import java.util.List; 16 | import java.util.Map; 17 | 18 | import static com.diff.core.Common.Code.*; 19 | 20 | public class ParseUtil { 21 | 22 | /** 23 | * test 24 | * @param args 25 | * @throws FileNotFoundException 26 | */ 27 | public static void main(String[] args) throws FileNotFoundException { 28 | CompilationUnit cu1 = StaticJavaParser.parse(new File(Config.getInstance().getString("oldProjectPath") + "/src/main/java/com/bot/server/qqBot/server/msgManage.java")); 29 | YamlPrinter printer = new YamlPrinter(true); 30 | System.out.println(printer.output(cu1)); 31 | } 32 | 33 | /** 34 | * 对比两个方法之间是否存在差异,弃用 35 | * @param oldFile 36 | * @param newFile 37 | * @return 38 | * @throws FileNotFoundException 39 | */ 40 | public static Map compireToMethod(File oldFile, File newFile) throws FileNotFoundException { 41 | Config config = Config.getInstance(); 42 | CompilationUnit oldCompilationUnit = StaticJavaParser.parse(oldFile); 43 | CompilationUnit newCompilationUnit = StaticJavaParser.parse(newFile); 44 | 45 | MethodVisitorAdapter oldMethodVisitor = new MethodVisitorAdapter(); 46 | MethodVisitorAdapter newMethodVisitor = new MethodVisitorAdapter(); 47 | // 清除注释 48 | oldCompilationUnit.getAllContainedComments().forEach(Node::remove); 49 | newCompilationUnit.getAllContainedComments().forEach(Node::remove); 50 | 51 | String oldArg = oldFile.getAbsolutePath().substring( 52 | (config.getString("oldProjectPath") + config.getString("source")).length() + 2, 53 | oldFile.getAbsolutePath().lastIndexOf(URL_SPLIT) + 1 54 | ); 55 | String newArg = newFile.getAbsolutePath().substring( 56 | (config.getString("newProjectPath") + config.getString("source")).length() + 2, 57 | newFile.getAbsolutePath().lastIndexOf(URL_SPLIT) + 1 58 | ); 59 | if(!URL_SPLIT.equals(PACKAGE_SPLIT)){ 60 | // 兼容windows的路径,arg需要输入包名 61 | oldArg = oldArg.replace(URL_SPLIT, PACKAGE_SPLIT); 62 | newArg = newArg.replace(URL_SPLIT, PACKAGE_SPLIT); 63 | } 64 | 65 | oldMethodVisitor.visit(oldCompilationUnit, oldArg); 66 | newMethodVisitor.visit(newCompilationUnit, newArg); 67 | 68 | Map compireResult = new HashMap<>(); 69 | for(String name:newMethodVisitor.getMds().keySet()){ 70 | MethodDeclaration methodDeclaration = oldMethodVisitor.getMds().get(name); 71 | if(methodDeclaration != null) 72 | compireResult.put(name, methodDeclaration.equals(newMethodVisitor.getMds().get(name))); 73 | else 74 | compireResult.put(name, Boolean.FALSE); 75 | } 76 | 77 | return compireResult; 78 | } 79 | 80 | /** 81 | * 对比两个方法之间是否存在差异 82 | * @param oldFile 83 | * @param newFile 84 | * @return 85 | * @throws FileNotFoundException 86 | */ 87 | public static Map compireToMethod(File oldFile, File newFile, String moduleName) throws FileNotFoundException { 88 | Config config = Config.getInstance(); 89 | CompilationUnit oldCompilationUnit = StaticJavaParser.parse(oldFile); 90 | CompilationUnit newCompilationUnit = StaticJavaParser.parse(newFile); 91 | 92 | MethodVisitorAdapter oldMethodVisitor = new MethodVisitorAdapter(); 93 | MethodVisitorAdapter newMethodVisitor = new MethodVisitorAdapter(); 94 | // 清除注释 95 | oldCompilationUnit.getAllContainedComments().forEach(Node::remove); 96 | newCompilationUnit.getAllContainedComments().forEach(Node::remove); 97 | 98 | int offset = moduleName.equals("") ? 2:3; 99 | String oldArg = oldFile.getAbsolutePath().substring( 100 | (config.getString("oldProjectPath") + moduleName + config.getString("source")).length() + offset, 101 | oldFile.getAbsolutePath().lastIndexOf(URL_SPLIT) + 1 102 | ); 103 | String newArg = newFile.getAbsolutePath().substring( 104 | (config.getString("newProjectPath") + moduleName + config.getString("source")).length() + offset, 105 | newFile.getAbsolutePath().lastIndexOf(URL_SPLIT) + 1 106 | ); 107 | if(!URL_SPLIT.equals(PACKAGE_SPLIT)){ 108 | // 兼容windows的路径,arg需要输入包名 109 | oldArg = oldArg.replace(URL_SPLIT, PACKAGE_SPLIT); 110 | newArg = newArg.replace(URL_SPLIT, PACKAGE_SPLIT); 111 | } 112 | 113 | oldMethodVisitor.visit(oldCompilationUnit, oldArg); 114 | newMethodVisitor.visit(newCompilationUnit, newArg); 115 | 116 | Map compireResult = new HashMap<>(); 117 | for(String name:newMethodVisitor.getMds().keySet()){ 118 | MethodDeclaration methodDeclaration = oldMethodVisitor.getMds().get(name); 119 | if(methodDeclaration != null) 120 | compireResult.put(name, methodDeclaration.equals(newMethodVisitor.getMds().get(name))); 121 | else 122 | compireResult.put(name, Boolean.FALSE); 123 | } 124 | 125 | return compireResult; 126 | } 127 | 128 | /** 129 | * 用于检索新项目中的java方法 130 | * @param classFilePath 131 | * @return 132 | * @throws FileNotFoundException 133 | */ 134 | public static List scanMethods(String classFilePath)throws FileNotFoundException{ 135 | File file = new File(classFilePath); 136 | Config config = Config.getInstance(); 137 | CompilationUnit compilationUnit = StaticJavaParser.parse(file); 138 | MethodVisitorAdapter methodVisitor = new MethodVisitorAdapter(); 139 | String arg = classFilePath.substring( 140 | classFilePath.indexOf(config.getString("source")) + config.getString("source").length() + 1, 141 | classFilePath.lastIndexOf(URL_SPLIT) + 1 142 | ); 143 | if(!URL_SPLIT.equals(PACKAGE_SPLIT)) 144 | arg = arg.replace(URL_SPLIT, PACKAGE_SPLIT); 145 | methodVisitor.visit(compilationUnit, arg); 146 | return new ArrayList<>(methodVisitor.getMds().keySet()); 147 | } 148 | } -------------------------------------------------------------------------------- /src/main/java/com/diff/core/Utils/ProjectChainUtils.java: -------------------------------------------------------------------------------- 1 | package com.diff.core.Utils; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.diff.core.Common.Config; 5 | import com.diff.core.Recorders.ControllerRecord; 6 | import com.diff.core.Recorders.DubboRecord; 7 | import org.apache.commons.io.FileUtils; 8 | import org.dom4j.DocumentException; 9 | 10 | import java.io.File; 11 | import java.io.FileNotFoundException; 12 | import java.io.IOException; 13 | import java.net.URL; 14 | import java.nio.file.NotDirectoryException; 15 | import java.util.*; 16 | 17 | import static com.diff.core.Common.Code.*; 18 | 19 | public class ProjectChainUtils { 20 | public static Map getProjectChainFromPath() throws IOException { 21 | Config config = Config.getInstance(); 22 | String rootDir = config.getString("newProjectPath") + URL_SPLIT + config.getString("target"); 23 | List filePaths = FileTreeUtil.scanForDirectory(rootDir); 24 | Map>> relationShips = new HashMap<>(); 25 | 26 | for(String filePath:filePaths){ 27 | // 扫描项目全部class 28 | ChainUtils.scanForClassName(FileUtils.readFileToByteArray(new File(filePath))); 29 | } 30 | 31 | for(String filePath:filePaths){ 32 | // 扫描方法之间的调用关系 33 | String className = filePath.substring(rootDir.length()+1,filePath.lastIndexOf('.')); 34 | if(!URL_SPLIT.equals(PACKAGE_SPLIT)) 35 | className = className.replace(URL_SPLIT, PACKAGE_SPLIT); 36 | relationShips.put(className, ChainUtils.getRelationShipFromClassBuffer(FileUtils.readFileToByteArray(new File(filePath)))); 37 | } 38 | Map result = new HashMap<>(); 39 | ControllerRecord controllerRecord = ControllerRecord.getInstance(); 40 | for(String controllerName:controllerRecord.getControllers()){ 41 | for(String methodName:controllerRecord.getApiFromControlClassName(controllerName)){ 42 | String fullMethodName = controllerName + METHOD_SPLIT + methodName; 43 | result.put(fullMethodName, ChainUtils.getJSONChainFromRelationShip(relationShips, fullMethodName)); 44 | } 45 | } 46 | return result; 47 | } 48 | 49 | public static List getProjectUpdateMethod(String oldProject, String newProject) throws NotDirectoryException, FileNotFoundException { 50 | List result = new ArrayList<>(); 51 | Config config = Config.getInstance(); 52 | Map> classDiff = FileTreeUtil.compireToPath( 53 | oldProject + URL_SPLIT + config.getString("source"), 54 | newProject + URL_SPLIT + config.getString("source") 55 | ); 56 | List scanFiles = new ArrayList<>(); 57 | 58 | List newDirectorys = classDiff.get("newDirectorys"); 59 | List newFiles = classDiff.get("newFiles"); 60 | List modifyFiles = classDiff.get("modifyFiles"); 61 | 62 | for(String directory:newDirectorys){ 63 | List files = FileTreeUtil.scanForDirectory(newProject + URL_SPLIT + config.getString("source") + directory); 64 | if(files != null) 65 | scanFiles.addAll(files); 66 | } 67 | // 对比 modifyFiles 获取有修改的method 68 | for(String file:modifyFiles){ 69 | Map modifyMethods = ParseUtil.compireToMethod( 70 | new File(oldProject + URL_SPLIT + config.getString("source") + file), 71 | new File(newProject + URL_SPLIT + config.getString("source") + file) 72 | ); 73 | for(String method:modifyMethods.keySet()){ 74 | if(!modifyMethods.get(method)){ 75 | result.add(method); 76 | } 77 | } 78 | } 79 | // 新目录下的所有method 80 | for(String file:scanFiles){ 81 | List scanMethods = ParseUtil.scanMethods(file); 82 | result.addAll(scanMethods); 83 | } 84 | // 新java文件的所有method 85 | for(String file:newFiles){ 86 | List newMethods = ParseUtil.scanMethods( 87 | newProject + URL_SPLIT + config.getString("source") + file 88 | ); 89 | result.addAll(newMethods); 90 | } 91 | return result; 92 | } 93 | 94 | public static Map getProjectChainFromPath(List modules) throws Exception { 95 | Config config = Config.getInstance(); 96 | Map result = new HashMap<>(); 97 | Map>> relationShips = new HashMap<>(); 98 | 99 | for(String module:modules){ 100 | String rootDir = module + URL_SPLIT + config.getString("target"); 101 | List filePaths = FileTreeUtil.scanForDirectory(rootDir); 102 | for(String filePath:filePaths){ 103 | // 扫描当前模块全部class 104 | ChainUtils.scanForClassName(FileUtils.readFileToByteArray(new File(filePath))); 105 | } 106 | } 107 | 108 | for(String module:modules){ 109 | String rootDir = module + URL_SPLIT + config.getString("target"); 110 | List filePaths = FileTreeUtil.scanForDirectory(rootDir); 111 | for(String filePath:filePaths){ 112 | // 扫描方法之间的调用关系 113 | String className = filePath.substring(rootDir.length()+1,filePath.lastIndexOf('.')); 114 | if(!URL_SPLIT.equals(PACKAGE_SPLIT)) 115 | className = className.replace(URL_SPLIT, PACKAGE_SPLIT); 116 | relationShips.put(className, ChainUtils.getRelationShipFromClassBuffer(FileUtils.readFileToByteArray(new File(filePath)))); 117 | } 118 | } 119 | ControllerRecord controllerRecord = ControllerRecord.getInstance(); 120 | for(String controllerName:controllerRecord.getControllers()){ 121 | for(String methodName:controllerRecord.getApiFromControlClassName(controllerName)){ 122 | String fullMethodName = controllerName + METHOD_SPLIT + methodName; 123 | result.put(fullMethodName, ChainUtils.getJSONChainFromRelationShip(relationShips, fullMethodName)); 124 | } 125 | } 126 | // 扫描dubbo调用链 127 | for(String dubboMethodName: DubboRecord.getList()){ 128 | result.put(dubboMethodName, ChainUtils.getJSONChainFromRelationShip(relationShips, dubboMethodName)); 129 | } 130 | return result; 131 | } 132 | 133 | public static List getProjectUpdateMethod(List modules) throws NotDirectoryException, FileNotFoundException, DocumentException { 134 | Config config = Config.getInstance(); 135 | List result = new ArrayList<>(); 136 | String newProject = config.getString("newProjectPath"); 137 | String oldProject = config.getString("oldProjectPath"); 138 | for(String module:modules){ 139 | String oldModule = oldProject + module.substring(newProject.length()); 140 | Map> classDiff = FileTreeUtil.compireToPath( 141 | oldModule + URL_SPLIT + config.getString("source"), 142 | module + URL_SPLIT + config.getString("source") 143 | ); 144 | List newDirectorys = classDiff.get("newDirectorys"); 145 | List newFiles = classDiff.get("newFiles"); 146 | List modifyFiles = classDiff.get("modifyFiles"); 147 | List scanFiles = new ArrayList<>(); 148 | 149 | for(String directory:newDirectorys){ 150 | List files = FileTreeUtil.scanForDirectory(module + URL_SPLIT + config.getString("source") + directory); 151 | if(files != null) 152 | scanFiles.addAll(files); 153 | } 154 | // 对比 modifyFiles 获取有修改的method 155 | for(String file:modifyFiles){ 156 | String moduleName = module.equals(newProject) ? "" : module.substring(newProject.length()+1); 157 | Map modifyMethods = ParseUtil.compireToMethod( 158 | new File(oldModule + URL_SPLIT + config.getString("source") + file), 159 | new File(module + URL_SPLIT + config.getString("source") + file), 160 | moduleName 161 | ); 162 | for(String method:modifyMethods.keySet()){ 163 | if(!modifyMethods.get(method)){ 164 | result.add(method); 165 | } 166 | } 167 | } 168 | // 新目录下的所有method 169 | for(String file:scanFiles){ 170 | List scanMethods = ParseUtil.scanMethods(file); 171 | result.addAll(scanMethods); 172 | } 173 | // 新java文件的所有method 174 | for(String file:newFiles){ 175 | List newMethods = ParseUtil.scanMethods( 176 | module + URL_SPLIT + config.getString("source") + file 177 | ); 178 | result.addAll(newMethods); 179 | } 180 | // 检索所有mybatis xml配置 181 | String oldResource = oldModule + URL_SPLIT + config.getString("resources"); 182 | String newResource = module + URL_SPLIT + config.getString("resources"); 183 | 184 | if(new File(oldResource).exists() && new File(newResource).exists()){ 185 | Map oldMybatisXml = FileTreeUtil.scanMybatisXml(oldResource); 186 | Map newMybatisXml = FileTreeUtil.scanMybatisXml(newResource); 187 | 188 | for(String xml:newMybatisXml.keySet()){ 189 | if(oldMybatisXml.containsKey(xml)){ 190 | List xmlDiff = XmlDiffUtils.compireXml(oldMybatisXml.get(xml), newMybatisXml.get(xml)); 191 | for(String methodName:xmlDiff){ 192 | List methods = ParseUtil.scanMethods( 193 | module + URL_SPLIT + config.getString("source") + URL_SPLIT + methodName.substring(0, methodName.lastIndexOf(METHOD_SPLIT)) + ".java" 194 | ); 195 | for(String fullMethodName:methods){ 196 | if(fullMethodName.startsWith(methodName)){ 197 | result.add(fullMethodName); 198 | } 199 | } 200 | } 201 | } 202 | } 203 | } 204 | } 205 | return result; 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/main/java/com/diff/core/Utils/XmlDiffUtils.java: -------------------------------------------------------------------------------- 1 | package com.diff.core.Utils; 2 | 3 | import com.alibaba.fastjson.JSONArray; 4 | import com.alibaba.fastjson.JSONObject; 5 | import org.dom4j.Attribute; 6 | import org.dom4j.DocumentException; 7 | import org.dom4j.Element; 8 | import org.dom4j.io.SAXReader; 9 | import org.dom4j.Document; 10 | 11 | import java.io.File; 12 | import java.util.*; 13 | 14 | import static com.diff.core.Common.Code.*; 15 | 16 | public class XmlDiffUtils { 17 | Element root; 18 | 19 | public XmlDiffUtils(File file) throws DocumentException { 20 | SAXReader reader = new SAXReader(); 21 | Document document = reader.read(file); 22 | root = document.getRootElement(); 23 | } 24 | 25 | /** 26 | * 只记录有修改的,新增、删除的不记录,因为这种在代码层面已经会被记录 27 | * @param oldFile 28 | * @param newFile 29 | * @throws DocumentException 30 | */ 31 | public static List compireXml(String oldFile, String newFile) throws DocumentException { 32 | SAXReader reader = new SAXReader(); 33 | Document oldDocument = reader.read(new File(oldFile)); 34 | Document newDocument = reader.read(new File(newFile)); 35 | Element oldRoot = oldDocument.getRootElement(); 36 | Element newRoot = newDocument.getRootElement(); 37 | String className = newRoot.attribute("namespace").getStringValue(); 38 | className = className.replace(".", PACKAGE_SPLIT); 39 | List result = new ArrayList<>(); 40 | 41 | Iterator oldIterator = oldRoot.elementIterator(); 42 | Iterator newIterator = newRoot.elementIterator(); 43 | 44 | Map oldCURD = new HashMap<>(); 45 | Map newCURD = new HashMap<>(); 46 | 47 | while(oldIterator.hasNext()){ 48 | Element child = (Element) oldIterator.next(); 49 | if(FilterUtils.isCURD(child.getName())){ 50 | JSONObject info = getElementInfo(child); 51 | oldCURD.put(info.getString("id"), info); 52 | } 53 | } 54 | while(newIterator.hasNext()){ 55 | Element child = (Element) newIterator.next(); 56 | if(FilterUtils.isCURD(child.getName())){ 57 | JSONObject info = getElementInfo(child); 58 | newCURD.put(info.getString("id"), info); 59 | } 60 | } 61 | for(String id:newCURD.keySet()){ 62 | if( 63 | oldCURD.keySet().contains(id) 64 | && !oldCURD.get(id).toJSONString().equals(newCURD.get(id).toJSONString()) 65 | ){ 66 | result.add(className + METHOD_SPLIT + id); 67 | } 68 | } 69 | return result; 70 | } 71 | 72 | public static JSONObject getElementInfo(Element element){ 73 | JSONObject result = new JSONObject(); 74 | result.put("tagName", element.getName()); 75 | result.put("innerText", element.getStringValue()); 76 | List attributeList = element.attributes(); 77 | for(Attribute attribute:attributeList){ 78 | result.put(attribute.getName(), attribute.getValue()); 79 | } 80 | 81 | JSONArray children = new JSONArray(); 82 | 83 | Stack recursion = new Stack<>(); 84 | Iterator iterator = element.elementIterator(); 85 | while(iterator.hasNext()){ 86 | recursion.push(new ElementNode((Element) iterator.next(), children)); 87 | } 88 | 89 | while(!recursion.empty()){ 90 | ElementNode topItem = recursion.pop(); 91 | JSONObject descriptor = new JSONObject(); 92 | descriptor.put("tagName", topItem.element.getName()); 93 | List tmpAttributeList = topItem.element.attributes(); 94 | for(Attribute attribute:tmpAttributeList){ 95 | descriptor.put(attribute.getName(), attribute.getValue()); 96 | } 97 | Iterator tmpIterator = topItem.element.elementIterator(); 98 | JSONArray tmpArray = new JSONArray(); 99 | while(tmpIterator.hasNext()){ 100 | recursion.push(new ElementNode((Element) tmpIterator.next(), tmpArray)); 101 | } 102 | descriptor.put("children", tmpArray); 103 | topItem.json.add(descriptor); 104 | } 105 | result.put("children", children); 106 | return result; 107 | } 108 | 109 | public boolean isMapper(){ 110 | return root.getName().equals("mapper"); 111 | } 112 | public String getMapperNameSpace(){ 113 | return root.attribute("namespace").getValue(); 114 | } 115 | 116 | private static class ElementNode{ 117 | private Element element; 118 | private JSONArray json; 119 | private ElementNode(Element element, JSONArray json){ 120 | this.element = element; 121 | this.json = json; 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/main/java/com/diff/core/Visitors/AsmAnnotationVisitor.java: -------------------------------------------------------------------------------- 1 | package com.diff.core.Visitors; 2 | 3 | import org.objectweb.asm.AnnotationVisitor; 4 | 5 | import java.util.Set; 6 | 7 | import static org.objectweb.asm.Opcodes.ASM7; 8 | import static com.diff.core.Common.Code.*; 9 | 10 | public class AsmAnnotationVisitor extends AnnotationVisitor { 11 | // 记录自己的requestMappingValue 12 | private Set paths; 13 | // 上一级的requestMappingValue(一般是类的RequestMapping) 14 | private Set parentPaths; 15 | // 判断是否有value 16 | private boolean hasValue = false; 17 | 18 | public AsmAnnotationVisitor(AnnotationVisitor av, Set paths, Set parentPaths){ 19 | super(ASM7, av); 20 | this.paths = paths; 21 | this.parentPaths = parentPaths; 22 | } 23 | 24 | @Override 25 | public AnnotationVisitor visitArray(String name){ 26 | // 主要提供给 annotationVisitor0 访问 27 | if(name.equals("value")){ 28 | hasValue = true; 29 | return new AsmAnnotationVisitor(super.visitArray(name), paths, parentPaths); 30 | } 31 | return super.visitArray(name); 32 | } 33 | 34 | @Override 35 | public void visit(String name, Object value){ 36 | // 主要提供给 annotationVisitor1 访问 37 | if(parentPaths.size() > 0){ 38 | for(String parentPath:parentPaths) 39 | paths.add(parentPath + (String) value); 40 | }else{ 41 | paths.add((String) value); 42 | } 43 | super.visit(name, value); 44 | } 45 | 46 | @Override 47 | public void visitEnd(){ 48 | if(!hasValue && paths.size() == 0){ 49 | paths.addAll(parentPaths); 50 | } 51 | super.visitEnd(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/diff/core/Visitors/AsmClassVisitor.java: -------------------------------------------------------------------------------- 1 | package com.diff.core.Visitors; 2 | 3 | import com.diff.core.Recorders.AbstractRecord; 4 | import com.diff.core.Recorders.ControllerRecord; 5 | import com.diff.core.Recorders.InterfaceRecord; 6 | import com.diff.core.Utils.FilterUtils; 7 | import com.diff.core.Utils.FullMethodNameUtil; 8 | import org.objectweb.asm.AnnotationVisitor; 9 | import org.objectweb.asm.ClassVisitor; 10 | import org.objectweb.asm.MethodVisitor; 11 | 12 | import java.util.*; 13 | 14 | import static org.objectweb.asm.Opcodes.*; 15 | 16 | public class AsmClassVisitor extends ClassVisitor { 17 | private String className; 18 | 19 | private Map> methodRelations = new HashMap<>(); 20 | private InterfaceRecord interfaceRecord = InterfaceRecord.getInstance(); 21 | private AbstractRecord abstractRecord = AbstractRecord.getInstance(); 22 | private boolean isController = false; 23 | private boolean hasRequestMapping = false; 24 | private boolean isInterface; 25 | // 记录自己的 requestMapping::Value 26 | private Set requestMappingValue; 27 | // 记录全部子方法的api入口 28 | private Map> recordMapping = new HashMap<>(); 29 | 30 | public AsmClassVisitor(ClassVisitor cv){ 31 | super(ASM7, cv); 32 | } 33 | 34 | @Override 35 | public void visit(int version, int access, String name, String signature, String superName, String[] interfaces){ 36 | // if(name.contains("testInterface") || name.contains("testAbstract")) 37 | // System.out.println("haha"); 38 | className = name; 39 | // 如果是接口的实体类,则加入interfaceRecord 40 | if(interfaces.length > 0){ 41 | for(String interfaceClassName:interfaces) { 42 | if (FilterUtils.isNeedInject(interfaceClassName)) { 43 | interfaceRecord.putInterfaceEntry(interfaceClassName, name); 44 | } 45 | } 46 | } 47 | // 如果是abstract类的实体类... 48 | if(abstractRecord.containAbstract(superName)){ 49 | abstractRecord.putAbstractEntry(superName, name); 50 | } 51 | super.visit(version, access, name, signature, superName, interfaces); 52 | } 53 | 54 | @Override 55 | public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions){ 56 | List methodRelation = new ArrayList<>(); 57 | // 处理一下方法名,适配javaparser 58 | String signa = FullMethodNameUtil.getMethodSignatureName(name, descriptor); 59 | methodRelations.put(signa, methodRelation); 60 | if(hasRequestMapping){ 61 | return new AsmMethodAdapter(access, name, descriptor, super.visitMethod(access,name,descriptor,signature,exceptions), methodRelation, className, requestMappingValue, recordMapping); 62 | }else{ 63 | // 类没有 RequestMapping 则parentPath为空的Set 64 | return new AsmMethodAdapter(access, name, descriptor, super.visitMethod(access,name,descriptor,signature,exceptions), methodRelation, className, new HashSet<>(), recordMapping); 65 | } 66 | } 67 | 68 | @Override 69 | public AnnotationVisitor visitAnnotation(String descriptor, boolean visiable){ 70 | if(FilterUtils.isControllerAnnotation(descriptor)){ 71 | // controller类 72 | ControllerRecord.getInstance().putControlClass(className); 73 | isController = true; 74 | } 75 | if(FilterUtils.isRequestAnnotation(descriptor)){ 76 | hasRequestMapping = true; 77 | requestMappingValue = new HashSet<>(); 78 | Set parentPath = new HashSet<>(); 79 | // 这里去获取类的 requestMappingValue 80 | return new AsmAnnotationVisitor(super.visitAnnotation(descriptor, visiable), requestMappingValue, parentPath); 81 | } 82 | return super.visitAnnotation(descriptor, visiable); 83 | } 84 | 85 | @Override 86 | public void visitEnd(){ 87 | super.visitEnd(); 88 | } 89 | 90 | public Map> getMethodRelations() { 91 | return methodRelations; 92 | } 93 | 94 | public Map> getRecordMapping() { 95 | return recordMapping; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/main/java/com/diff/core/Visitors/AsmMethodAdapter.java: -------------------------------------------------------------------------------- 1 | package com.diff.core.Visitors; 2 | 3 | import com.diff.core.Recorders.ApiRecord; 4 | import com.diff.core.Recorders.ControllerRecord; 5 | import com.diff.core.Utils.FilterUtils; 6 | import com.diff.core.Utils.FullMethodNameUtil; 7 | import org.objectweb.asm.AnnotationVisitor; 8 | import org.objectweb.asm.MethodVisitor; 9 | import org.objectweb.asm.commons.AdviceAdapter; 10 | 11 | import java.util.*; 12 | 13 | import static com.diff.core.Common.Code.*; 14 | 15 | public class AsmMethodAdapter extends AdviceAdapter { 16 | private String className; 17 | private String methodName; 18 | private String desc; 19 | private List relationShip; 20 | private Set parentPath; 21 | private Set requestMappingValue; 22 | private Map> recordMapping; 23 | 24 | public AsmMethodAdapter(int access, String methodName, String desc, MethodVisitor mv, List relationShip, String className){ 25 | super(ASM7, mv, access, methodName, desc); 26 | this.methodName = methodName; 27 | this.relationShip = relationShip; 28 | this.desc = desc; 29 | this.className = className; 30 | } 31 | 32 | public AsmMethodAdapter(int access, String methodName, String desc, MethodVisitor mv, List relationShip, String className, Set parentPath, Map> recordMapping){ 33 | super(ASM7, mv, access, methodName, desc); 34 | this.methodName = methodName; 35 | this.relationShip = relationShip; 36 | this.desc = desc; 37 | this.className = className; 38 | this.parentPath = parentPath; 39 | this.recordMapping = recordMapping; 40 | } 41 | 42 | @Override 43 | public void visitMethodInsn(int opcode, String owner, String name, String descript, boolean isInterface){ 44 | String fullMethodName = owner + METHOD_SPLIT + FullMethodNameUtil.getMethodSignatureName(name, descript); 45 | if(FilterUtils.isNeedInject(owner) && FilterUtils.isNeedInjectMethod(fullMethodName) && !relationShip.contains(fullMethodName)) 46 | relationShip.add(fullMethodName); 47 | super.visitMethodInsn(opcode, owner, name, descript, isInterface); 48 | } 49 | 50 | @Override 51 | public AnnotationVisitor visitAnnotation(String descriptor, boolean visiable){ 52 | if(FilterUtils.isRequestAnnotation(descriptor)){ 53 | // request的方法 54 | ControllerRecord.getInstance().putControlMethod(className, FullMethodNameUtil.getMethodSignatureName(methodName, desc)); 55 | requestMappingValue = new HashSet<>(); 56 | // 这里去获取方法的 requestMappingValue 57 | // 故而:requestMappingValue记录自己的,parentPath上一级(比如类)传过来的 requestMappingValue 58 | return new AsmAnnotationVisitor(super.visitAnnotation(descriptor, visiable), requestMappingValue, parentPath); 59 | } 60 | return super.visitAnnotation(descriptor, visiable); 61 | } 62 | 63 | @Override 64 | public void visitTypeInsn(int opcode, String type){ 65 | super.visitTypeInsn(opcode, type); 66 | } 67 | 68 | @Override 69 | public void visitMaxs(int a, int b){ 70 | try{ 71 | super.visitMaxs(a, b); 72 | }catch (TypeNotPresentException e){ 73 | return; 74 | } 75 | } 76 | 77 | @Override 78 | public void visitEnd(){ 79 | // 处理一下Mapping 80 | if(requestMappingValue!=null){ 81 | String fullMethodName = className + METHOD_SPLIT + FullMethodNameUtil.getMethodSignatureName(methodName, desc); 82 | recordMapping.put(fullMethodName, requestMappingValue); 83 | ApiRecord.getInstance().putApi(fullMethodName, requestMappingValue); 84 | } 85 | 86 | super.visitEnd(); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/com/diff/core/Visitors/AsmSearchFilterClass.java: -------------------------------------------------------------------------------- 1 | package com.diff.core.Visitors; 2 | 3 | import com.diff.core.Recorders.AbstractRecord; 4 | import com.diff.core.Recorders.DubboRecord; 5 | import com.diff.core.Recorders.InterfaceRecord; 6 | import com.diff.core.Utils.FilterUtils; 7 | import com.diff.core.Utils.FullMethodNameUtil; 8 | import org.objectweb.asm.AnnotationVisitor; 9 | import org.objectweb.asm.ClassVisitor; 10 | import org.objectweb.asm.MethodVisitor; 11 | 12 | import static com.diff.core.Common.Code.*; 13 | import static org.objectweb.asm.Opcodes.*; 14 | 15 | /** 16 | * 第一次遍历所有class,记录下项目的所有className到FilterUtil中; 17 | * 记录下所有interface以及abstract类,以及它们与实体类的对应关系 18 | */ 19 | public class AsmSearchFilterClass extends ClassVisitor{ 20 | private final InterfaceRecord interfaceRecord = InterfaceRecord.getInstance(); 21 | private final AbstractRecord abstractRecord = AbstractRecord.getInstance(); 22 | private String className; 23 | private boolean isInterface = false; 24 | private boolean isAbstract = false; 25 | private boolean isDubbo = false; 26 | 27 | public AsmSearchFilterClass(ClassVisitor cv){ 28 | super(ASM7, cv); 29 | } 30 | 31 | @Override 32 | public void visit(int version, int access, String name, String signature, String superName, String[] interfaces){ 33 | // 记录className 34 | FilterUtils.addProjectPackage(name); 35 | className = name; 36 | // 如果是interface 37 | if((access & ACC_INTERFACE)==ACC_INTERFACE){ 38 | interfaceRecord.putInterfaceClass(name); 39 | isInterface = true; 40 | } 41 | // 如果是abstract类 42 | if((access & ACC_ABSTRACT)==ACC_ABSTRACT){ 43 | abstractRecord.putAbstractClass(name); 44 | isAbstract = true; 45 | } 46 | super.visit(version, access, name, signature, superName, interfaces); 47 | } 48 | 49 | @Override 50 | public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions){ 51 | // 保存方法名 52 | String methodName = FullMethodNameUtil.getMethodSignatureName(name, descriptor); 53 | FilterUtils.addProjectMethod(className + METHOD_SPLIT + methodName); 54 | // 接口或抽象类的abstract方法需要记录 55 | if(isInterface){ 56 | if((access & ACC_ABSTRACT)==ACC_ABSTRACT){ 57 | interfaceRecord.putMethod(className, methodName, true); 58 | }else{ 59 | interfaceRecord.putMethod(className, methodName, false); 60 | } 61 | } 62 | if(isAbstract){ 63 | if((access & ACC_ABSTRACT)==ACC_ABSTRACT){ 64 | abstractRecord.putMethod(className, methodName, true); 65 | }else{ 66 | abstractRecord.putMethod(className, methodName, false); 67 | } 68 | } 69 | if(isDubbo){ 70 | DubboRecord.putDubboMethod(className + METHOD_SPLIT + methodName); 71 | } 72 | return new AsmSearchFilterMethod( 73 | super.visitMethod(access, name, descriptor, signature, exceptions), 74 | access, name, descriptor 75 | ); 76 | } 77 | 78 | @Override 79 | public AnnotationVisitor visitAnnotation(String descriptor, boolean visiable){ 80 | if(FilterUtils.isDubboAnnotation(descriptor)){ 81 | isDubbo = true; 82 | // DubboRecord.putDubboClass(descriptor); 83 | } 84 | return super.visitAnnotation(descriptor, visiable); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/com/diff/core/Visitors/AsmSearchFilterMethod.java: -------------------------------------------------------------------------------- 1 | package com.diff.core.Visitors; 2 | 3 | import org.objectweb.asm.MethodVisitor; 4 | import org.objectweb.asm.commons.AdviceAdapter; 5 | 6 | public class AsmSearchFilterMethod extends AdviceAdapter { 7 | public AsmSearchFilterMethod(MethodVisitor mv, int access, String methodName, String desc){ 8 | super(ASM7, mv, access, methodName, desc); 9 | } 10 | 11 | @Override 12 | public void visitMaxs(int a, int b){ 13 | // 这里不关心Type的报错,直接忽略 14 | try{ 15 | super.visitMaxs(a, b); 16 | }catch (TypeNotPresentException ignored){ 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /src/main/java/com/diff/core/Visitors/MethodVisitorAdapter.java: -------------------------------------------------------------------------------- 1 | package com.diff.core.Visitors; 2 | 3 | import com.github.javaparser.ast.NodeList; 4 | import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; 5 | import com.github.javaparser.ast.body.MethodDeclaration; 6 | import com.github.javaparser.ast.body.Parameter; 7 | import com.github.javaparser.ast.visitor.VoidVisitorAdapter; 8 | 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | 12 | import static com.diff.core.Common.Code.*; 13 | 14 | public class MethodVisitorAdapter extends VoidVisitorAdapter { 15 | private final Map mds = new HashMap<>(); 16 | 17 | @Override 18 | public void visit(MethodDeclaration n, String arg){ 19 | StringBuilder stringBuilder = new StringBuilder(); 20 | stringBuilder.append("("); 21 | NodeList parameters = n.getParameters(); 22 | for(Parameter parameter:parameters){ 23 | String parameterType = parameter.getType().toString(); 24 | if(parameterType.contains("<")) 25 | parameterType = parameterType.replace(parameterType.substring(parameterType.indexOf('<'), parameterType.lastIndexOf('>')+1), ""); 26 | if(!stringBuilder.toString().equals("(")) 27 | stringBuilder.append(", "); 28 | stringBuilder.append(parameterType); 29 | } 30 | stringBuilder.append(")"); 31 | String returnType = n.getType().toString(); 32 | if(returnType.contains("<")) 33 | returnType = returnType.replace(returnType.substring(returnType.indexOf('<'), returnType.lastIndexOf('>')+1), ""); 34 | stringBuilder.append(returnType); 35 | mds.put(arg + METHOD_SPLIT + n.getName().toString() + stringBuilder, n); 36 | } 37 | 38 | @Override 39 | public void visit(ClassOrInterfaceDeclaration n, String arg){ 40 | if(arg.substring(arg.length()-1).equals(PACKAGE_SPLIT)) 41 | super.visit(n, arg + n.getName().toString()); 42 | else 43 | super.visit(n, arg + SUB_CLASS_SPLIT + n.getName().toString()); 44 | } 45 | 46 | public Map getMds() { 47 | return mds; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/diff/core/Visitors/VisitorAdapter.java: -------------------------------------------------------------------------------- 1 | package com.diff.core.Visitors; 2 | 3 | import com.alibaba.fastjson.JSONArray; 4 | import com.alibaba.fastjson.JSONObject; 5 | import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; 6 | import com.github.javaparser.ast.body.MethodDeclaration; 7 | import com.github.javaparser.ast.visitor.VoidVisitorAdapter; 8 | 9 | public class VisitorAdapter extends VoidVisitorAdapter { 10 | @Override 11 | public void visit(MethodDeclaration n, JSONArray arg){ 12 | JSONObject descript = new JSONObject(); 13 | descript.put("name", n.getName().toString()); 14 | descript.put("type", "method"); 15 | descript.put("signature", n.getSignature().toString()); 16 | arg.add(descript); 17 | super.visit(n, arg); 18 | } 19 | 20 | /** 21 | * [ 22 | * { 23 | * "name": "app", 24 | * "type": "class", 25 | * "member": [ 26 | * { 27 | * "name": "main", 28 | * "type": "method", 29 | * "signate": [Stirng[] args, int cnt] 30 | * }, 31 | * { 32 | * "name": "main", 33 | * "type": "method", 34 | * "signate": "[int a, int b]" 35 | * }, 36 | * { 37 | * "name": "tt", 38 | * "type": "class", 39 | * "member": [...] 40 | * } 41 | * ] 42 | * }, 43 | * {...} 44 | * ] 45 | * @param n 46 | * @param arg 47 | */ 48 | @Override 49 | public void visit(ClassOrInterfaceDeclaration n, JSONArray arg){ 50 | JSONObject descript = new JSONObject(); 51 | descript.put("name", n.getName().toString()); 52 | descript.put("type", "class"); 53 | JSONArray Members = new JSONArray(); 54 | descript.put("member", Members); 55 | arg.add(descript); 56 | super.visit(n, Members); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/diff/core/main.java: -------------------------------------------------------------------------------- 1 | package com.diff.core; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.diff.core.Common.Config; 5 | import com.diff.core.Recorders.ApiRecord; 6 | import com.diff.core.Utils.FileTreeUtil; 7 | import com.diff.core.Utils.ParseUtil; 8 | import com.diff.core.Utils.ProjectChainUtils; 9 | 10 | import static com.diff.core.Common.Code.*; 11 | 12 | import java.util.*; 13 | 14 | public class main { 15 | 16 | public static void main(String[] args) throws Exception { 17 | Config config = Config.getInstance(); 18 | Map> modules = FileTreeUtil.compireToPomTree(config.getString("oldProjectPath"), config.getString("newProjectPath")); 19 | List newModules = modules.get("newModules"); 20 | List normalModules = modules.get("normalModules"); 21 | 22 | List allModules = new ArrayList<>(); 23 | allModules.addAll(newModules); 24 | allModules.addAll(normalModules); 25 | Map chains = ProjectChainUtils.getProjectChainFromPath(allModules); 26 | 27 | List update = new ArrayList<>(); 28 | for(String module:newModules){ 29 | // 新模块所有文件加入update 30 | List scan = FileTreeUtil.scanForDirectory(module); 31 | for(String file:scan){ 32 | List scanMethods = ParseUtil.scanMethods(file); 33 | update.addAll(scanMethods); 34 | } 35 | // 检索mybatis xml配置 36 | } 37 | update.addAll(ProjectChainUtils.getProjectUpdateMethod(normalModules)); 38 | 39 | System.out.println("涉及到更新的方法: "); 40 | for(String method:update){ 41 | System.out.println(method); 42 | } 43 | System.out.println("建议回归的API: "); 44 | Set needToTestApi = new HashSet<>(); 45 | for(String updateMethod:update){ 46 | for(String startChain:chains.keySet()){ 47 | if(startChain.equals(updateMethod) || chains.get(startChain).toJSONString().contains(updateMethod)){ 48 | Set apis = ApiRecord.getInstance().getApis(startChain); 49 | if(apis == null){ 50 | needToTestApi.add(DUBBO + METHOD_SIGNATURE_SPLIT + startChain); 51 | }else{ 52 | for(String api:apis){ 53 | needToTestApi.add(HTTP + METHOD_SIGNATURE_SPLIT + api); 54 | } 55 | } 56 | } 57 | } 58 | } 59 | for(String api:needToTestApi) 60 | System.out.println(api); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/resources/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "oldProjectPath": "/Users/xiaoandi/github/qqbot/qqbot", 3 | "newProjectPath": "/Users/xiaoandi/github/qqbot/diff_test/qqbot", 4 | "source": "src/main/java", 5 | "target": "target/classes", 6 | "resources": "src/main/resources" 7 | } --------------------------------------------------------------------------------