Java脚本的语法解析示例

早期搞了一个解析Java脚本的功能,其中有一处是需要解析Java语法,下面的代码贴了一下主要功能, 主要用到了tools中的一些类,但是代码使用tools.jar需要在maven中做一些额外配置:

<dependency>
	<groupId>com.sun</groupId>
	<artifactId>tools</artifactId>
	<version>1.8</version>
	<scope>system</scope>
	<systemPath>${JAVA_HOME}/lib/tools.jar</systemPath>
</dependency>

上面的maven配置就是为了能够在代码中使用tools.jar中的类。

import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.Maps;
import com.sun.tools.javac.file.JavacFileManager;
import com.sun.tools.javac.parser.JavacParser;
import com.sun.tools.javac.parser.ParserFactory;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.Name;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.CollectionUtils;

import javax.tools.JavaFileManager;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;

public class JavaSourceCodeParser {

	private static final Logger logger = LoggerFactory.getLogger(JavaSourceCodeParser.class);

	private static final Joiner JOINER = Joiner.on("\n").skipNulls();

	public static String parseFullyQualifiedClassName(String fileName, String scriptSourceCode) {
		Preconditions.checkArgument(!Strings.isNullOrEmpty(scriptSourceCode));

		JCTree.JCCompilationUnit jcCompilationUnit;
		try {
			Context context = new Context();
			JavacFileManager javacFileManager = new JavacFileManager(context, false, Charsets.UTF_8);
			context.put(JavaFileManager.class, javacFileManager);
			JavacParser javacParser = ParserFactory.instance(context).newParser(scriptSourceCode, false, true, false);
			jcCompilationUnit = javacParser.parseCompilationUnit();
		}
		catch (Exception e) {
			throw new RuntimeException("脚本语法解析失败,请检查脚本内容是否正确");
		}

		String packageName = parsePackageName(jcCompilationUnit);
		String className = parseClassName(fileName, jcCompilationUnit);
		return Strings.isNullOrEmpty(packageName) ? className : packageName + "." + className;
	}

	private static String parsePackageName(JCTree.JCCompilationUnit jcCompilationUnit) {
		JCTree.JCExpression packageName = jcCompilationUnit.getPackageName();
		if (packageName == null) {
			return "";
		}
		return packageName.toString();
	}

	private static String parseClassName(String fileName, JCTree.JCCompilationUnit jcCompilationUnit) {
		com.sun.tools.javac.util.List<JCTree> typeDecls = jcCompilationUnit.getTypeDecls();
		if (!CollectionUtils.isEmpty(typeDecls)) {
			//如果脚本中只有一个class,就使用这个class
			if (typeDecls.size() == 1) {
				for (JCTree typeDecl : typeDecls) {
					if (typeDecl instanceof JCTree.JCClassDecl) {
						JCTree.JCClassDecl classDecl = (JCTree.JCClassDecl) typeDecl;
						Name simpleName = classDecl.getSimpleName();
						return simpleName.toString();
					}
				}
			}
			else {
				Map<String, JCTree.JCClassDecl> jcClassDeclMap = Maps.newHashMap();
				for (JCTree typeDecl : typeDecls) {
					if (typeDecl instanceof JCTree.JCClassDecl) {
						JCTree.JCClassDecl jcClassDecl = (JCTree.JCClassDecl) typeDecl;
						jcClassDeclMap.put(jcClassDecl.getSimpleName().toString(), jcClassDecl);
					}
				}

				//如果脚本中有多个同级的class定义,则先看谁有public
				for (Map.Entry<String, JCTree.JCClassDecl> entry : jcClassDeclMap.entrySet()) {
					//public/public abstract 。。。
					String modifiers = entry.getValue().getModifiers().toString();
					if (modifiers.contains("public")) {
						return entry.getKey();
					}
				}

				//如果都没有public的话,就使用文件名来判断, 之所以不一开始就使用文件名判断是为了降低文件名权重,避免写错
				JCTree.JCClassDecl jcClassDecl = jcClassDeclMap.get(tryRemoveJavaSuffix(fileName));
				if (jcClassDecl != null) {
					return jcClassDecl.getSimpleName().toString();
				}
				throw new RuntimeException("脚本中定义了多个class,而且都没有public修饰符,同时基于脚本文件名称也无法精确确定主类,建议修改一下脚本内容,便于系统识别!");
			}
		}

		throw new RuntimeException("从脚本内容中不能正确的解析出className,请检查脚本内容的正确性");
	}

	public static String readSourceCode(Path sourceCodeFilePath) {
		Preconditions.checkNotNull(sourceCodeFilePath);
		try {
			List<String> allLines = Files.readAllLines(sourceCodeFilePath);
			return JOINER.join(allLines);
		}
		catch (Exception e) {
			String error = String.format("readSourceCode error, sourceCodeFilePath:[%s]", sourceCodeFilePath);
			logger.error(error);
			throw new RuntimeException(error);
		}
	}

	private static String tryRemoveJavaSuffix(String fileName) {
		fileName = Strings.nullToEmpty(fileName).trim();
		if (fileName.endsWith(".java")) {
			try {
				return fileName.substring(0, fileName.length() - 5);
			}
			catch (Exception e) {
				return fileName;
			}
		}
		return fileName;
	}
}
comments powered by Disqus