学习笔记——使用javaparser批量修改代码

本文介绍了Javaparser库,如何通过其解析Java源码生成抽象语法树,以及如何使用Visitor模式访问和修改代码,包括实践中的方法名收集和控制器请求路径修改示例。

一、简介

Javaparser是一个通过生成语法树来读取或操作java代码的库。它的github地址https://github.com/javaparser/javaparser,官方文档在https://leanpub.com/javaparservisited。

二、基本的类和概念

1.CompilationUnit

Javaparser解析java代码后会生成AST(abstract syntax tree,抽象语法树),CompilationUnit(编译单元)是每个java文件被解析后直接生成的对象,是读取和操作java文件的入口。CompilationUnit包含了整棵AST的Node,可以理解为AST的根节点。
在这里插入图片描述
上图根据代码画出了CompilationUnit的结构,每个节点都继承自Node。

2.Node

AST的Node,对于java中的类、接口、注解、方法、入参、赋值语句、注释、if条件、注释等都是一种Node,如果Node表示的代码块能继续细化分割,就在其子节点列表NodeList中,Node是读取和操作AST的基本单元。

Node的部分子类:
在这里插入图片描述
这些Node类和其表示的java代码(每个类的注释中有,官方文档附录B中有全部的Node类及其示例)
在这里插入图片描述

3.Visitor

Javaparser使用访问者模式来访问或修改Node,当需要修改Node时,Node本身不需要额外增加方法,而是通过创建一个Visitor,在Visitor中定义好需要修改什么,用Node调用方法接收我们创建的Visitor完成修改。

3.1 Visitable和两类Visitor接口

为了实现这种模式,作者设计了两类接口,一个是Visitable,一个是Visitor(根据有无返回值,分为GenericVisitor和VoidVisitor)。

(1)所有可访问的Node都实现了Visitable接口,这个接口有两个accept方法,用于接收Visitor以及外部参数arg,外部参数可用于收集遍历到的东西。
在这里插入图片描述
(2)所有Node实现Visitable接口的方法都是传递自身实例和外部arg。

@Generated是作者生成accept代码后加上的,作者比较“懒”,编写了很多generator生成代码,下图这段代码是由AcceptGenerator生成的。

在这里插入图片描述
(3)针对不同类型Node,实现GenericVisitor接口中对应的方法就能访问这种类型的Node。
在这里插入图片描述
也就是说我们用Node实例调用它的accept方法,传入一个编写好的Visitor,Visitor中的实现方法就能访问这个Node。

3.2 VoidVisitorAdapter

当我们只需要访问java代码而不需要做修改时,直接继承VoidVisitorAdapter。这个抽象类对VoidVisitor做了默认实现,通过递归执行accept方法来达到遍历整个AST的目的。
在这里插入图片描述
上图中对CompilationUnit的import(引包)、module(java高版本模块)、package(所属包)、type(定义的类型class、interface、enum、annotation)、comment(注释)分别遍历执行accept,而其中的每一种Node又会遍历子节点执行accept。例如下图访问class和interface的ClassOrInterfaceDeclaration的方法,分别遍历它涵盖的节点。
在这里插入图片描述

3.3 ModifierVisitor

与VoidVisitorAdapter不同的是,ModifierVisitor继承带返回值的GenericVisitor,其返回值用于返回修改后的节点。如下图所示,遍历各项子节点以后,将返回值作为修改后的对象重新赋值。
在这里插入图片描述

三、实践

1.依赖

gradle

    // https://mvnrepository.com/artifact/com.github.javaparser/javaparser-core
    implementation 'com.github.javaparser:javaparser-core:3.23.1'

maven

	<!-- https://mvnrepository.com/artifact/com.github.javaparser/javaparser-core -->
	<dependency>
	    <groupId>com.github.javaparser</groupId>
	    <artifactId>javaparser-core</artifactId>
	    <version>3.23.1</version>
	</dependency>

2.收集java文件中的全部方法名

如果我们想打印并收集一个java文件中所有的方法名,继承VoidVisitorAdapter。

public static void main(String[] args) throws IOException {
        //FIXME
        Path path = Paths.get("your_java_file.java");

        //解析java文件生成AST
        CompilationUnit cu = StaticJavaParser.parse(path);

        //打印并收集所有方法的Visitor
        VoidVisitor<List<String>> printMethodVisitor = new VoidVisitorAdapter<List<String>>() {
            @Override
            public void visit(MethodDeclaration n, List<String> arg) {
                //打印方法名称
                String methodName = n.getName().getIdentifier();
                arg.add(methodName);
                System.out.println(methodName);
                //调用父类方法是为了递归调用下去,可能子节点还有当前类型的Node
                super.visit(n, arg);
            }
        };

        //方法集合
        List<String> methodNames = new ArrayList<>();
        //接收Visitor执行
        cu.accept(printMethodVisitor, methodNames);

        for (String name : methodNames) {
            System.out.println(name);
        }
    }

3.修改特定的controller请求路径

对使用了忽略token注解@Ignoretoken的controller方法,请求路径上添加一个/ignore。

简单描述下需求:
首先需要找到全部Controller,然后获取controller的请求path,再遍历所有带有RequestMapping、GetMapping或PostMapping的方法,对有忽略token注解的方法,修改其请求path的值,记录新旧路径,最后输出到excel中。

import com.github.javaparser.StaticJavaParser;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.github.javaparser.ast.body.MethodDeclaration;
import com.github.javaparser.ast.expr.*;
import com.github.javaparser.ast.visitor.ModifierVisitor;
import com.github.javaparser.ast.visitor.Visitable;
import com.github.javaparser.printer.DefaultPrettyPrinter;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

/**
 * 修改controller请求路径
 *
 * @author HetFrame
 */
public class ModifyControllerPath {

    //TODO 修改为你的项目地址
    private static final Path ROOT_PATH = Paths.get("your_project\\src\\main\\java");
    //TODO excel导出路径
    private static final String outPath = "D:\\ModifiedController.xlsx";

    public static void main(String[] args) throws IOException {
        List<ControllerPathInfo> result = new ArrayList<>();
        Files.walk(ROOT_PATH)
                //只获取controller文件
                .filter(path -> path.toString().endsWith("Controller.java"))
                .forEach(path -> {
                    try {
                        result.addAll(modifyFile(path));
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                });

        // 在遍历文件后,导出到Excel
        exportToExcel(result);
    }

    private static List<ControllerPathInfo> modifyFile(Path file) throws IOException {
        CompilationUnit cu = StaticJavaParser.parse(file);

        //统计修改的接口
        List<ControllerPathInfo> list = new ArrayList<>();

        //遍历所有类,文件中不止定义一个类
        cu.findAll(ClassOrInterfaceDeclaration.class).forEach(cls -> {

            //获取controller上RequestMapping的路径
            String controllerPath = getControllerPath(cls);
            if (controllerPath == null) {
                return;
            }
            controllerPath = controllerPath.replace("\"", "");

            String finalControllerPath = controllerPath;

            //修改每个controller中的方法
            cls.accept(new ModifierVisitor<List<ControllerPathInfo>>() {
                @Override
                public Visitable visit(MethodDeclaration n, List<ControllerPathInfo> arg) {
                    //方法有忽略token注解
                    if (hasIgnoreTokenAnnotation(n)) {
                        String methodPath = getMethodPath(n).replace("\"", "");

                        //修改请求路径
                        String modifiedPath = modifyPath(methodPath);

                        //注解赋值有两种情况所以实现了两个方法
                        n.accept(new ModifierVisitor<Void>() {
                            @Override
                            public Visitable visit(SingleMemberAnnotationExpr n, Void arg) {
                                String annotationName = n.getName().getIdentifier();
                                if (annotationName.equals("RequestMapping") || annotationName.equals("GetMapping") || annotationName.equals("PostMapping")) {
                                    Expression expression = n.getMemberValue().clone();
                                    expression.asStringLiteralExpr().setString(modifiedPath);
                                    n.setMemberValue(expression);
                                }
                                return super.visit(n, arg);
                            }

                            @Override
                            public Visitable visit(NormalAnnotationExpr n, Void arg) {
                                String annotationName = n.getName().getIdentifier();
                                if (annotationName.equals("RequestMapping") || annotationName.equals("GetMapping") || annotationName.equals("PostMapping")) {
                                    MemberValuePair pair = n.getPairs().stream().filter(e -> "value".equals(e.getName().toString())).findFirst().orElse(new MemberValuePair());
                                    if (pair.getValue() != null) {
                                        Expression expression = pair.getValue().clone();
                                        expression.asStringLiteralExpr().setString(modifiedPath);
                                        pair.setValue(expression);
                                    }
                                }
                                return super.visit(n, arg);
                            }
                        }, null);

                        //将改动信息存入list
                        String fullOriginalPath = finalControllerPath + methodPath;
                        String fullModifiedPath = finalControllerPath + modifiedPath;
                        String controllerName = cls.getNameAsString();
                        String requestType = getRequestType(n);

                        arg.add(new ControllerPathInfo(controllerName, requestType, fullOriginalPath, fullModifiedPath));
                    }

                    return super.visit(n, arg);
                }
            }, list);
        });

        //获取修改后的java代码并写入文件 DefaultPrettyPrinter用于格式化代码
        String outputStr = new DefaultPrettyPrinter().print(cu);
        FileWriter writer = new FileWriter(file.toFile());
        writer.write(outputStr);
        writer.close();

        return list;
    }

    private static String getControllerPath(ClassOrInterfaceDeclaration cls) {
        Optional<AnnotationExpr> optional = cls.getAnnotations()
                .stream()
                .filter(a -> a.getNameAsString().equals("RequestMapping"))
                .findFirst();

        if (optional.isPresent()) {
            AnnotationExpr expr = optional.get();
            if (expr.isSingleMemberAnnotationExpr()) {
                return expr.asSingleMemberAnnotationExpr().getMemberValue().toString();
            }
            if (expr.isNormalAnnotationExpr()) {
                return expr.asNormalAnnotationExpr().getPairs().stream().filter(pair -> "value".equals(pair.getName().toString())).findFirst().get().getValue().toString();
            }
        }

        return null;
    }

    private static boolean hasIgnoreTokenAnnotation(MethodDeclaration method) {
        return method.getAnnotationByName("IgnoreToken").isPresent();
    }

    private static String getMethodPath(MethodDeclaration method) {
        Optional<AnnotationExpr> optional = method.getAnnotations()
                .stream()
                .filter(a -> a.getNameAsString().equals("RequestMapping") || a.getNameAsString().equals("GetMapping") || a.getNameAsString().equals("PostMapping"))
                .findFirst();

        if (optional.isPresent()) {
            AnnotationExpr expr = optional.get();
            if (expr.isSingleMemberAnnotationExpr()) {
                return expr.asSingleMemberAnnotationExpr().getMemberValue().toString();
            }
            if (expr.isNormalAnnotationExpr()) {
                return expr.asNormalAnnotationExpr().getPairs().stream().filter(pair -> "value".equals(pair.getName().toString())).findFirst().get().getValue().toString();
            }
        }

        throw new RuntimeException("未找到method请求路径");
    }

    private static String modifyPath(String originalPath) {
    	//请求路径风格为/v1/xxx 或 /{version}/xxx
        return originalPath.replaceAll("/(v1|\\{version\\})/", "/$1/ignore/");
    }

    private static String getRequestType(MethodDeclaration method) {
        if (method.getAnnotationByName("GetMapping").isPresent()) {
            return "GET";
        }
        if (method.getAnnotationByName("PostMapping").isPresent()) {
            return "POST";
        }
        Optional<AnnotationExpr> expr = method.getAnnotationByName("RequestMapping");
        if (expr.isPresent()) {
            Expression type = expr.get().asNormalAnnotationExpr().getPairs().stream().filter(pair -> "method".equals(pair.getName().toString())).findFirst().get().getValue();
            if (type.toString().contains("GET")) {
                return "GET";
            }
            if (type.toString().contains("POST")) {
                return "POST";
            }
        }

        // add other request types as needed
        return "UNKNOWN";
    }

    private static void exportToExcel(List<ControllerPathInfo> result) throws IOException {
        Workbook workbook = new XSSFWorkbook();
        Sheet sheet = workbook.createSheet("Modified Interfaces");

        Row headerRow = sheet.createRow(0);
        headerRow.createCell(0).setCellValue("接口controller");
        headerRow.createCell(1).setCellValue("请求类型");
        headerRow.createCell(2).setCellValue("旧接口地址");
        headerRow.createCell(3).setCellValue("新接口地址");

        for (int i = 0; i < result.size(); i++) {
            ControllerPathInfo interfaceInfo = result.get(i);
            Row row = sheet.createRow(i + 1);
            row.createCell(0).setCellValue(interfaceInfo.getController());
            row.createCell(1).setCellValue(interfaceInfo.getRequestType());
            row.createCell(2).setCellValue(interfaceInfo.getOldPath());
            row.createCell(3).setCellValue(interfaceInfo.getNewPath());
        }

        try (FileOutputStream outputStream = new FileOutputStream(outPath)) {
            workbook.write(outputStream);
        }

        workbook.close();
    }

    public static class ControllerPathInfo {
        private String controller;
        private String requestType;
        private String oldPath;
        private String newPath;

        public ControllerPathInfo(String controller, String requestType, String oldPath, String newPath) {
            this.controller = controller;
            this.requestType = requestType;
            this.oldPath = oldPath;
            this.newPath = newPath;
        }

        public String getController() {
            return controller;
        }

        public String getRequestType() {
            return requestType;
        }

        public String getOldPath() {
            return oldPath;
        }

        public String getNewPath() {
            return newPath;
        }
    }
}
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值