Spoon是分析,生成和转换Java代码的工具。
在本文中,我们将看到通过使用以编程方式处理代码的技术可以实现什么。 我认为这些技术不是很广为人知或使用,这很遗憾,因为它们可能非常有用。 谁知道,即使您不想使用Spoon甚至不处理Java代码,而是使用C#,Python,Kotlin或其他语言,某些想法对于您当前的项目也可能有用。 让我们学习如何以更智能的方式编程。
Spoon具有一些与JavaParser重叠的功能, JavaParser是我贡献的框架。 对于某些任务,Spoon可能是更好的选择,而对于另一些任务,JavaParser具有明显的优势。 稍后,我们将深入探讨这些工具之间的差异。
本文与随附的存储库与所有代码配对: ftomassetti / spoon-examples
使用代码处理技术可以实现什么?
勺子和一般的代码处理工具可用于:
- 代码分析
- 计算源代码指标,例如找出多少类具有比一定数量的方法更多的类
- 代码生成
- 以编程方式生成重复代码。
- 代码转换
- 自动重构,例如在构造函数中指定的字段中转换几种方法中使用的参数
这三大家族与我们与代码交互的方式大致不同:
- 在代码分析中,代码是我们用来产生非代码输出的输入
- 在代码生成中,我们使用一些通常不是代码的输入,或者使用与我们输出的语言相同的代码。 输出是代码
- 在代码转换中,相同的代码库是输入和输出
设置汤匙
要设置汤匙,您需要提供:
- 要分析的代码
- 所有依赖关系(当然还有依赖关系的依赖关系)
利用此信息,Spoon可以构建代码模型。 在该模型上,您可以执行相当高级的分析。 这与JavaParser的工作方式不同。 如果需要,在JavaParser中,您可以构建代码的轻量级模型,而无需考虑依赖关系。 当您没有可用的依赖项或需要执行简单快速的操作时,这将很有用。 您也可以通过启用符号解析来进行更高级的分析,但这是可选的,并且在仅某些依赖项可用时也可以使用。
我喜欢Spoon的一件事是支持从Maven进行配置。 我认为这是一个非常有用的功能。 但是,我只想得到Gradle的支持。
在我们的示例中,我们不使用Maven配置,我们仅指定一个包含代码的目录。 在我们的例子中,我们正在检查JavaParser的核心模块,该模块的依赖项为零,因此我们无需指定任何JAR即可构建代码模型。
fun main(args: Array<String>) {val launcher = Launcher()launcher.addInputResource("codebases/jp/javaparser-core/src/main/java")launcher.environment.noClasspath = trueval model = launcher.buildModel()...
}
现在我们有了一个模型,让我们看看如何使用它。
顺便说一句,示例是用Kotlin编写的,因为它是一种简洁明了的语言,我认为它非常适合教程。 你同意吗?
使用Spoon执行代码分析
让我们开始使用20多种方法打印类列表:
fun examineClassesWithManyMethods(ctModel: CtModel, threshold: Int = 20) {val classes = ctModel.filterChildren<CtClass<*>> {it is CtClass<*> && it.methods.size > threshold}.list<CtClass<*>>()printTitle("Classes with more than $threshold methods")printList(classes.asSequence().sortedByDescending { it.methods.size }.map { "${it.qualifiedName} (${it.methods.size})"})println()
}fun main(args: Array<String>) {val launcher = Launcher()launcher.addInputResource("codebases/jp/javaparser-core/src/main/java")launcher.environment.noClasspath = trueval model = launcher.buildModel()examineClassesWithManyMethods(model)
}
在本示例中,我们在主要函数中设置模型,然后在inspectClassesWithManyMethods中 ,按方法数量过滤类,然后使用几个实用程序函数来打印这些类的列表(printTitle , printList )。
运行此代码,我们获得以下输出:
=====================================
| Classes with more than 20 methods |
=====================================* com.github.javaparser.ast.expr.Expression (141)* com.github.javaparser.printer.PrettyPrintVisitor (105)* com.github.javaparser.ast.visitor.EqualsVisitor (100)* com.github.javaparser.ast.visitor.NoCommentEqualsVisitor (98)* com.github.javaparser.ast.visitor.CloneVisitor (95)* com.github.javaparser.ast.visitor.GenericVisitorWithDefaults (94)* com.github.javaparser.ast.visitor.ModifierVisitor (94)* com.github.javaparser.ast.visitor.VoidVisitorWithDefaults (94)* com.github.javaparser.ast.visitor.HashCodeVisitor (93)* com.github.javaparser.ast.visitor.NoCommentHashCodeVisitor (93)* com.github.javaparser.ast.visitor.ObjectIdentityEqualsVisitor (93)* com.github.javaparser.ast.visitor.ObjectIdentityHashCodeVisitor (93)* com.github.javaparser.ast.stmt.Statement (92)* com.github.javaparser.ast.visitor.GenericListVisitorAdapter (92)* com.github.javaparser.ast.visitor.GenericVisitorAdapter (92)* com.github.javaparser.ast.visitor.VoidVisitorAdapter (92)* com.github.javaparser.ast.Node (62)* com.github.javaparser.ast.NodeList (62)* com.github.javaparser.ast.type.Type (55)* com.github.javaparser.ast.body.BodyDeclaration (50)* com.github.javaparser.ast.modules.ModuleDirective (44)* com.github.javaparser.ast.CompilationUnit (44)* com.github.javaparser.JavaParser (39)* com.github.javaparser.resolution.types.ResolvedReferenceType (37)* com.github.javaparser.utils.SourceRoot (34)* com.github.javaparser.ast.body.CallableDeclaration (29)* com.github.javaparser.ast.body.MethodDeclaration (28)* com.github.javaparser.printer.PrettyPrinterConfiguration (27)* com.github.javaparser.metamodel.PropertyMetaModel (26)* com.github.javaparser.ast.type.WildcardType (25)* com.github.javaparser.ast.expr.ObjectCreationExpr (24)* com.github.javaparser.ast.type.PrimitiveType (24)* com.github.javaparser.printer.lexicalpreservation.NodeText (24)* com.github.javaparser.utils.VisitorList (24)* com.github.javaparser.printer.lexicalpreservation.Difference (23)* com.github.javaparser.ast.comments.Comment (22)* com.github.javaparser.ast.expr.FieldAccessExpr (22)* com.github.javaparser.ast.type.ClassOrInterfaceType (22)* com.github.javaparser.utils.Utils (22)* com.github.javaparser.JavaToken (22)* com.github.javaparser.ast.body.ClassOrInterfaceDeclaration (21)* com.github.javaparser.ast.body.FieldDeclaration (21)* com.github.javaparser.ast.expr.MethodCallExpr (21)* com.github.javaparser.ast.stmt.ExplicitConstructorInvocationStmt (21)* com.github.javaparser.ast.stmt.IfStmt (21)* com.github.javaparser.ParserConfiguration (21)
现在让我们尝试其他的东西。 让我们尝试查找所有测试类,并确保其名称以“ Test”结尾。 测试类将是至少具有用org.unit.Test
注释的方法的类。
fun CtClass<*>.isTestClass() = this.methods.any { it.annotations.any { it.annotationType.qualifiedName == "org.junit.Test" } }fun verifyTestClassesHaveProperName(ctModel: CtModel) {val testClasses = ctModel.filterChildren<CtClass<*>> { it is CtClass<*> && it.isTestClass() }.list<CtClass<*>>()val testClassesNamedCorrectly = testClasses.filter { it.simpleName.endsWith("Test") }val testClassesNotNamedCorrectly = testClasses.filter { it !in testClassesNamedCorrectly }printTitle("Test classes named correctly")println("N Classes named correctly: ${testClassesNamedCorrectly.size}")println("N Classes not named correctly: ${testClassesNotNamedCorrectly.size}")printList(testClassesNotNamedCorrectly.asSequence().sortedBy { it.qualifiedName }.map { it.qualifiedName })
}fun main(args: Array<String>) {val launcher = Launcher()launcher.addInputResource("codebases/jp/javaparser-core/src/main/java")launcher.addInputResource("codebases/jp/javaparser-core-testing/src/test/java")launcher.addInputResource("libs/junit-vintage-engine-4.12.3.jar")launcher.environment.noClasspath = trueval model = launcher.buildModel()verifyTestClassesHaveProperName(model)
}
构建模型与以前几乎相同,我们只是添加了更多的源目录和JAR,作为测试模块对JUnit的依赖。
在verifyTestClassesHaveProperName中,我们:
- 过滤所有属于测试类的类 (它们至少具有一个用org.junit.Test注释的方法)
- 查找所有名称以Test结尾的测试类,以及所有不包含
- 我们打印要修复的类的列表以及有关它们的一些统计信息
让我们运行这段代码,我们得到以下结果:
================================
| Test classes named correctly |
================================
N Classes named correctly: 124
N Classes not named correctly: 2* com.github.javaparser.wiki_samples.CreatingACompilationUnitFromScratch* com.github.javaparser.wiki_samples.removenode.RemoveDeleteNodeFromAst
当然,这些只是相当简单的示例,但希望它们足以显示Spoon和代码分析的潜力。 处理代表您的代码的模型,提取有趣的信息以及验证是否遵守某些语义规则是相当容易的。
有关更高级的用法,您还可以查看有关使用Spoon进行体系结构实施的本文。
使用Spoon执行代码生成
让我们来看一个考虑一个常见任务的代码生成示例:JSON的代码序列化和反序列化。 我们将从采用JSON模式开始,然后我们将生成表示JSON模式描述的实体的类。
这是一个相当高级的示例,我花了一些时间熟悉Spoon才能编写它。 我还必须向他们的团队提出一些问题,以解决一些问题。 的确,这段代码绝非易事,但是我认为我们应该认为这是一个非常复杂的功能,因此对我来说听起来很公平。
好的,现在让我们进入代码。
这是一个JSON模式:
{"$id": "https://example.com/arrays.schema.json","$schema": "http://json-schema.org/draft-07/schema#","description": "A representation of a person, company, organization, or place","type": "object","properties": {"fruits": {"type": "array","items": {"type": "string"}},"vegetables": {"type": "array","items": { "$ref": "#/definitions/veggie" }}},"definitions": {"veggie": {"type": "object","required": [ "veggieName", "veggieLike" ],"properties": {"veggieName": {"type": "string","description": "The name of the vegetable."},"veggieLike": {"type": "boolean","description": "Do I like this vegetable?"}}}}
}
在顶层,我们可以看到整个架构所代表的实体。 我们知道它将被表示为一个对象并具有两个属性:
- 水果 :字符串数组
- 蔬菜 :一组蔬菜 ,其中蔬菜是下面描述的另一个对象,在“定义”部分中
在定义部分,我们可以看到素食是具有两个属性的对象:
- veggieName :字符串
- veggieLike :布尔值
我们应该得到什么
我们想要得到的是两个java类:一个代表整个模式,一个代表单个蔬菜。 这两个类应允许读取和写入单个字段,将实例序列化为JSON以及从JSON反序列化实例。
我们的代码应生成两个类:
package com.thefruit.company;public class FruitThing implements com.strumenta.json.JsonSerializable {private java.util.List<java.lang.String> fruits;public java.util.List<java.lang.String> getFruits() {return fruits;}public void setFruits(java.util.List<java.lang.String> fruits) {this.fruits = fruits;}private java.util.List<com.thefruit.company.Veggie> vegetables;public java.util.List<com.thefruit.company.Veggie> getVegetables() {return vegetables;}public void setVegetables(java.util.List<com.thefruit.company.Veggie> vegetables) {this.vegetables = vegetables;}public com.google.gson.JsonObject serialize() {com.google.gson.JsonObject res = new com.google.gson.JsonObject();res.add("fruits", com.strumenta.json.SerializationUtils.serialize(fruits));res.add("vegetables", com.strumenta.json.SerializationUtils.serialize(vegetables));return res;}public static com.thefruit.company.FruitThing unserialize(com.google.gson.JsonObject json) {com.thefruit.company.FruitThing res = new com.thefruit.company.FruitThing();res.setFruits((java.util.List) com.strumenta.json.SerializationUtils.unserialize(json.get("fruits"), com.google.gson.reflect.TypeToken.getParameterized(java.util.List.class, java.lang.String.class)));res.setVegetables((java.util.List) com.strumenta.json.SerializationUtils.unserialize(json.get("vegetables"), com.google.gson.reflect.TypeToken.getParameterized(java.util.List.class, com.thefruit.company.Veggie.class)));return res;}
}
和:
package com.thefruit.company;public class Veggie implements com.strumenta.json.JsonSerializable {private java.lang.String veggieName;public java.lang.String getVeggieName() {return veggieName;}public void setVeggieName(java.lang.String veggieName) {this.veggieName = veggieName;}private boolean veggieLike;public boolean getVeggieLike() {return veggieLike;}public void setVeggieLike(boolean veggieLike) {this.veggieLike = veggieLike;}public com.google.gson.JsonObject serialize() {com.google.gson.JsonObject res = new com.google.gson.JsonObject();res.add("veggieName", com.strumenta.json.SerializationUtils.serialize(veggieName));res.add("veggieLike", com.strumenta.json.SerializationUtils.serialize(veggieLike));return res;}public static com.thefruit.company.Veggie unserialize(com.google.gson.JsonObject json) {com.thefruit.company.Veggie res = new com.thefruit.company.Veggie();res.setVeggieName((java.lang.String) com.strumenta.json.SerializationUtils.unserialize(json.get("veggieName"), com.google.gson.reflect.TypeToken.get(java.lang.String.class)));res.setVeggieLike((boolean) com.strumenta.json.SerializationUtils.unserialize(json.get("veggieLike"), com.google.gson.reflect.TypeToken.get(boolean.class)));return res;}
}
这是我们如何使用这两个类的示例:
package com.thefruit.company;import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;import java.util.Arrays;public class Example {public static void main(String[] args) {FruitThing ft = new FruitThing();ft.setFruits(Arrays.asList("Banana", "Pear", "Apple"));Veggie cucumber = new Veggie();cucumber.setVeggieLike(false);cucumber.setVeggieName("Cucumber");Veggie carrot = new Veggie();carrot.setVeggieLike(true);carrot.setVeggieName("Carrot");ft.setVegetables(Arrays.asList(cucumber, carrot));Gson gson = new GsonBuilder().setPrettyPrinting().create();System.out.println(gson.toJson(ft.serialize()));JsonElement serialized = ft.serialize();FruitThing unserializedFt = FruitThing.unserialize(serialized.getAsJsonObject());System.out.println("Fruits: " + unserializedFt.getFruits());}
}
在该示例中,我们构建了FruitThing和几个Veggies的实例。 然后,我们对它们进行序列化并反序列化它们,以便我们可以证明序列化和反序列化都可以工作。
生成过程:一般组织
生成过程将生成一组GeneratedJavaFile实例,每个实例都有自己的文件名和代码。 以后我们可以将它们写入文件或在内存中进行编译。
在程序的主要功能中,我们将读取JSON模式并将其传递给函数generateJsonSchema 。 我们将其与两个参数一起传递:首先在其中生成我们的类的包的名称,然后是代表整个架构的类的名称。
一旦我们获得了生成的类,我们就将它们打印在屏幕上以快速浏览。
data class GeneratedJavaFile(val filename: String, val code: String)fun main(args: Array<String>) {Dummy::class.java.getResourceAsStream("/a_json_schema.json").use {val generatedClasses = generateJsonSchema(it, "com.thefruit.company", "FruitThing")generatedClasses.forEach {println("*".repeat(it.filename.length))println(it.filename)println("*".repeat(it.filename.length))println(it.code)}}
}
好的,所以魔术发生在generateJsonSchema中,对吗?
fun generateJsonSchema(jsonSchema: InputStream, packageName: String, rootClassName: String) : List<GeneratedJavaFile> {val rawSchema = JSONObject(JSONTokener(jsonSchema))val schema = SchemaLoader.load(rawSchema) as ObjectSchemaval cus = generateClasses(schema, packageName, rootClassName)val pp = DefaultJavaPrettyPrinter(StandardEnvironment())return cus.map { cu ->pp.calculate(cu, cu.declaredTypes)val filename = cu.declaredTypes[0].qualifiedName.replace('.', File.separatorChar) + ".java"GeneratedJavaFile(filename, pp.result)}
}
在generateJsonSchema中,我们解析提供模式的InputStream,并调用generateClasses ,这将返回一堆CompilationUnits。 基本上,每个CompilationUnit都是单个Java文件的抽象语法树。
一旦获得了这些编译单元,就将它们打印为Java代码。 我们还计算适当的文件名并实例化GeneratedJavaFile实例。
因此,看来我们现在来看一下generateClasses 。
fun generateClasses(schema: ObjectSchema, packageName: String, rootClassName: String) : List<CompilationUnit> {// First we create the classesval pack = CtPackageImpl()pack.setSimpleName<CtPackage>(packageName)val classProvider = ClassProvider(pack)schema.generateClassRecursively(classProvider, rootClassName)// Then we put them in compilation units and we generate themreturn classProvider.classesForObjectSchemas.map {val cu = CompilationUnitImpl()cu.isAutoImport = truecu.declaredPackage = packcu.declaredTypes = listOf(it.value)cu}.toList()
}
在generateClasses中,我们首先创建包( CtPackageImpl类)。 我们将使用它来生成所有类。 我们将其保留在ClassProvider类中。 它将用于生成和跟踪我们将生成的类。 然后,我们调用添加到架构的扩展方法,称为generateClassRecursively 。
最后,我们将从classProvider中获取类,并将其放入CompilationUnits中。
private fun Schema.generateClassRecursively(classProvider: ClassProvider, name: String? = null) {when (this) {is ObjectSchema -> {classProvider.register(this, this.generateClass(classProvider, name))this.propertySchemas.forEach { it.value.generateClassRecursively(classProvider) }}is ArraySchema -> this.allItemSchema.generateClassRecursively(classProvider)is StringSchema, is BooleanSchema -> nullis ReferenceSchema -> this.referredSchema.generateClassRecursively(classProvider)else -> TODO("not implemented: ${this.javaClass}")}
}
generateClassRecursively会发生什么? 基本上,我们寻找定义对象的模式,并为每个对象生成一个类。 我们还对模式进行爬网以查找属性,以查看它们是否间接定义或使用了我们可能要为其生成类的其他对象模式。
在ObjectSchema的扩展方法generateClass中生成一个类。 当它产生一个类时,我们将其传递给classProvider以便对其进行记录。
private fun ObjectSchema.generateClass(classProvider: ClassProvider, name: String? = null): CtClass<Any> {return CtClassImpl<Any>().let { ctClass ->val packag = classProvider.packpackag.types.add(ctClass)ctClass.setParent(packag)ctClass.setVisibility<CtModifiable>(ModifierKind.PUBLIC)ctClass.setSimpleName<CtClass<Any>>(name ?: this.schemaLocation.split("/").last().capitalize())ctClass.setSuperInterfaces<CtType<Any>>(setOf(createTypeReference(JsonSerializable::class.java)))this.propertySchemas.forEach {ctClass.addProperty(it.key, it.value, classProvider)}addSerializeMethod(ctClass, this, classProvider)addUnserializeMethod(ctClass, this, classProvider)ctClass}
}
到目前为止,我们已经设置了对架构进行爬网并确定生成内容的逻辑,但是我们还没有看到很多Spoon特定的API。 这在generateClass中发生了变化。
在这里,我们首先实例化CtClassImpl,然后:
- 设置适当的包(从classProvider获得)
- 将课程设为公开
- 指定类的名称:如果类代表整个模式,我们可以将其作为参数接收,否则我们可以从模式本身派生它
- 查看单个属性并在addProperty中处理它们
- 调用addSerializeMethod添加一个序列化方法,我们将使用该方法从此类的实例生成JSON
那么,我们如何添加属性?
fun CtClass<*>.addProperty(name: String, schema: Schema, classProvider: ClassProvider) {val field = CtFieldImpl<Any>().let {it.setSimpleName<CtField<Any>>(name)it.setType<CtField<Any>>(schema.toType(classProvider))it.setVisibility<CtField<Any>>(ModifierKind.PRIVATE)}this.addField<Any, Nothing>(field)addGetter(this, field)addSetter(this, field)
}
我们只需添加一个字段( CtField )。 我们设置正确的名称,类型和可见性,并将其添加到类中。 目前我们还没有生成getter或setter。
生成过程:序列化
在本节中,我们将看到如何生成类的serialize方法。 对于我们的两个类,它们看起来像这样:
public class FruitThing implements com.strumenta.json.JsonSerializable {...public com.google.gson.JsonObject serialize() {com.google.gson.JsonObject res = new com.google.gson.JsonObject();res.add("fruits", com.strumenta.json.SerializationUtils.serialize(fruits));res.add("vegetables", com.strumenta.json.SerializationUtils.serialize(vegetables));return res;}...
}public class Veggie implements com.strumenta.json.JsonSerializable {...public com.google.gson.JsonObject serialize() {com.google.gson.JsonObject res = new com.google.gson.JsonObject();res.add("veggieName", com.strumenta.json.SerializationUtils.serialize(veggieName));res.add("veggieLike", com.strumenta.json.SerializationUtils.serialize(veggieLike));return res;}...
}
这是生成这种方法的切入点:
fun addSerializeMethod(ctClass: CtClassImpl<Any>, objectSchema: ObjectSchema, classProvider: ClassProvider) {val method = CtMethodImpl<Any>().let {it.setVisibility<CtModifiable>(ModifierKind.PUBLIC)it.setType<CtTypedElement<Any>>(jsonObjectType)it.setSimpleName<CtMethod<Any>>("serialize")val statements = LinkedList<CtStatement>()statements.add(createLocalVar("res", jsonObjectType, objectInstance(jsonObjectType)))objectSchema.propertySchemas.forEach { statements.addAll(addSerializeStmts(it, classProvider)) }statements.add(returnStmt(localVarRef("res")))it.setBodyBlock(statements)it}ctClass.addMethod<Any, CtType<Any>>(method)
}
我们实例化CtMethodImpl然后:
- 我们设置方法的可见性
- 我们将返回类型设置为JSONObject
- 我们将名称设置为序列化
- 我们创建JSONObject类型的res变量
- 对于每个属性,我们将生成序列化语句,以将该属性的值添加到res中
- 最后,我们添加一个return语句并将此块设置为方法的主体
在这里,我们使用了一堆实用程序方法来简化代码,因为Spoon API非常冗长。
例如,我们使用createLocalVar和objectInstance ,如下所示:
fun createLocalVar(name: String, type: CtTypeReference<Any>, value: CtExpression<Any>? = null) : CtLocalVariable<Any> {return CtLocalVariableImpl<Any>().let {it.setSimpleName<CtNamedElement>(name)it.setType<CtTypedElement<Any>>(type)if (value != null) {it.setAssignment<CtRHSReceiver<Any>>(value)}it}
}fun objectInstance(type: CtTypeReference<Any>) : CtConstructorCall<Any> {return CtConstructorCallImpl<Any>().let {it.setType<CtTypedElement<Any>>(type)it}
}
现在,我们来看看如何为特定属性生成序列化方法的语句。
un addSerializeStmts(entry: Map.Entry<String, Schema>,classProvider: ClassProvider): Collection<CtStatement> {return listOf(instanceMethodCall("add", listOf(stringLiteral(entry.key),staticMethodCall("serialize",listOf(fieldRef(entry.key)),createTypeReference(SerializationUtils::class.java))), target= localVarRef("res")))
}
基本上,我们委托给SerializationUtils.serialize 。 该方法将包含在运行时库中,以与我们生成的代码一起使用。
它是这样的:
public class SerializationUtils {public static JsonElement serialize(Object value) {if (value instanceof JsonSerializable) {return ((JsonSerializable) value).serialize();}if (value instanceof Iterable<?>) {com.google.gson.JsonArray jsonArray = new com.google.gson.JsonArray();for (Object element : (Iterable<?>)value) {jsonArray.add(com.strumenta.json.SerializationUtils.serialize(element));}return jsonArray;}if (value instanceof Boolean) {return new JsonPrimitive((Boolean)value);}if (value instanceof String) {return new JsonPrimitive((String)value);}throw new UnsupportedOperationException("Value: " + value + " (" + value.getClass().getCanonicalName() + ")");}public static Object unserialize(JsonElement json, TypeToken<?> expectedType) {...to be discussed later...}
}
我们序列化某个属性的方式取决于其类型。 简单值(字符串和布尔值)很容易,而数组则比较棘手。 对于任何可通过JsonSerializable进行调用的对象,我们都调用相应的serialize方法。 我们为什么要这样做? 这样我们就可以使用为类( FruitThing和Veggie )生成的序列化方法。
生成过程:反序列化
让我们看看我们应该能够生成的反序列化方法:
public class FruitThing implements com.strumenta.json.JsonSerializable {...public static com.thefruit.company.FruitThing unserialize(com.google.gson.JsonObject json) {com.thefruit.company.FruitThing res = new com.thefruit.company.FruitThing();res.setFruits((java.util.List) com.strumenta.json.SerializationUtils.unserialize(json.get("fruits"), com.google.gson.reflect.TypeToken.getParameterized(java.util.List.class, java.lang.String.class)));res.setVegetables((java.util.List) com.strumenta.json.SerializationUtils.unserialize(json.get("vegetables"), com.google.gson.reflect.TypeToken.getParameterized(java.util.List.class, com.thefruit.company.Veggie.class)));return res;}...
}public class Veggie implements com.strumenta.json.JsonSerializable {...public static com.thefruit.company.Veggie unserialize(com.google.gson.JsonObject json) {com.thefruit.company.Veggie res = new com.thefruit.company.Veggie();res.setVeggieName((java.lang.String) com.strumenta.json.SerializationUtils.unserialize(json.get("veggieName"), com.google.gson.reflect.TypeToken.get(java.lang.String.class)));res.setVeggieLike((boolean) com.strumenta.json.SerializationUtils.unserialize(json.get("veggieLike"), com.google.gson.reflect.TypeToken.get(boolean.class)));return res;}...
}
负责生成此类方法的代码是哪一部分? 毫不奇怪,它称为addUnserializeMethod :
fun addUnserializeMethod(ctClass: CtClassImpl<Any>, objectSchema: ObjectSchema, classProvider: ClassProvider) {val method = CtMethodImpl<Any>().let {it.setType<CtTypedElement<Any>>(createTypeReference(ctClass))it.setModifiers<CtModifiable>(setOf(ModifierKind.STATIC, ModifierKind.PUBLIC))it.setSimpleName<CtMethod<Any>>("unserialize")it.setParameters<CtExecutable<Any>>(listOf(CtParameterImpl<Any>().let {it.setSimpleName<CtNamedElement>("json")it.setType<CtTypedElement<Any>>(jsonObjectType)it}))val thisClass = createTypeReference(ctClass.qualifiedName)val statements = LinkedList<CtStatement>()statements.add(createLocalVar("res", thisClass, objectInstance(thisClass)))objectSchema.propertySchemas.forEach { statements.addAll(addUnserializeStmts(it, classProvider)) }statements.add(returnStmt(localVarRef("res")))it.setBodyBlock(statements)it}ctClass.addMethod<Any, CtType<Any>>(method)
}
结构与我们之前所见非常相似。 当然,这里涉及的是对addUnserializeStmts的调用。
fun addUnserializeStmts(entry: Map.Entry<String, Schema>,classProvider: ClassProvider): Collection<CtStatement> {// call to get the field, e.g. `json.get("veggieName")`val getField = instanceMethodCall("get",listOf(stringLiteral(entry.key)),target = localVarRef("json"))// call to create the TypeToken, e.g., `TypeToken.get(String.class)`// or `TypeToken.getParameterized(List.class, String.class)`val ctFieldType = entry.value.toType(classProvider)val createTypeToken = if (ctFieldType is CtTypeReference<Any> && ctFieldType.actualTypeArguments.isNotEmpty()) {staticMethodCall("getParameterized",(listOf(classField(ctFieldType)) + ctFieldType.actualTypeArguments.map { classField(it) }).toList() as List<CtExpression<Any>>,createTypeReference(TypeToken::class.java))} else {staticMethodCall("get",listOf(classField(ctFieldType)),createTypeReference(TypeToken::class.java))}val callToUnserialize = staticMethodCall("unserialize",listOf(getField, createTypeToken),createTypeReference("com.strumenta.json.SerializationUtils"))val castedCallToUnserialize = cast(callToUnserialize, entry.value.toType(classProvider))return listOf(instanceMethodCall("set" + entry.key.capitalize(), listOf(castedCallToUnserialize), target= localVarRef("res")))
}
现在,事情变得复杂了。 我们基本上必须为每个属性调用设置方法。 给设置器,我们将使用适当的类型转换将反序列化的结果传递给属性类型。 要调用反序列化,我们需要一个TypeToken,用于引导反序列化过程。 我们想以不同的方式对同一值进行反序列化,具体取决于我们是否要获取整数或字符串:类型标记告诉我们要获取的内容。
生成过程:注释
要构建此示例,我们必须编写许多实用程序方法。 我们在本文中未显示的整个示例的某些部分,但是您可以在协同存储库中找到所有这些代码。
还要注意,我们可以将代码保存到文件中,并使用编译器API进行编程编译。 如果需要,我们甚至可以在内存中编译它。 在实际情况下,我建议这样做,而不是像我在本教程中所做的那样,将代码手动复制粘贴到文件中。
使用Spoon执行代码转换
在使用大型代码库或防止重复性任务出现人为错误时,代码转换可能非常有用。
例如,假设您决定更改必须实施特定模式的方式。 假设您在代码库中使用了数十次单例模式,并且您想确保每次懒惰地创建实例(即,仅在第一次需要时)。 您可以自动执行此转换。
或者假设您正在更新正在使用的库,并且您所依赖的特定方法已重命名,或者其参数顺序已更改。 同样,您可以通过使用代码转换来解决此问题。
对于我们的示例,我们将采取一些简单的措施。 我们将重构一个类。 在此类中,我们有几种方法可以接收特定的参数。 鉴于基本上每个操作都需要此参数,因此我们决定将其移至构造函数并将其另存为字段实例。 然后,我们要转换获取该参数的所有方法,以使它们不再需要它,而是访问相应的字段。
让我们看一下转换的样子:
// original code
class MyClass {MyClass() {} void foo(MyParam param, String otherParam) {param.doSomething();}int bar(MyParam param) {return param.count();}}// transformed code
class MyClass {MyParam param;MyClass(MyParam param) {this.param = param;} void foo(String otherParam) {this.param.doSomething();} int bar() { return this.param.count(); }}
在这个例子中,我们只转换定义方法的类; 在实际情况下,我们可能还希望转换这些方法的调用。
我们如何实现此代码转换
让我们先看一下代码转换示例的主要方法,以便我们可以看到常规结构:
fun main(args: Array<String>) {val originalCode = """class MyClass {MyClass() {}void foo(MyParam param, String otherParam) {param.doSomething();}int bar(MyParam param) {return param.count();}}"""val parsedClass = Launcher.parseClass(originalCode)ParamToFieldRefactoring("param", createTypeReference("com.strumenta.MyParam")).refactor(parsedClass)println(parsedClass.toCode())
}
如您所见,我们:
- 解析代码
- 应用我们的类ParamToFieldRefactoring中定义的重构
- 我们打印结果代码
有趣的地方当然是ParamToFieldRefactoring
class ParamToFieldRefactoring(val paramName: String, val paramType: CtTypeReference<Any>) {fun refactor(clazz: CtClass<*>) {// Add field to the classclazz.addField<Any, Nothing>(CtFieldImpl<Any>().let {it.setSimpleName<CtNamedElement>(paramName)it.setType<CtTypedElement<Any>>(paramType)it})// Receive the value for the field in each constructorclazz.constructors.forEach {it.addParameter<Nothing>(CtParameterImpl<Any>().let {it.setSimpleName<CtNamedElement>(paramName)it.setType<CtTypedElement<Any>>(paramType)it})it.body.statements.add(CtAssignmentImpl<Any, Any>().let {it.setAssigned<CtAssignment<Any, Any>>(qualifiedFieldAccess(paramName, clazz.qualifiedName))it.setAssignment<CtRHSReceiver<Any>>(localVarRef(paramName))it})}clazz.methods.filter { findParamToChange(it) != null }.forEach {val param = findParamToChange(it)!!CtIterator(it).forEach {if (it is CtParameterReference<*> && it.simpleName == paramName) {val cfr = CtFieldReferenceImpl<Any>()cfr.setSimpleName<CtReference>(paramName)cfr.setDeclaringType<CtFieldReference<Any>>(createTypeReference(clazz.qualifiedName))it.replace(cfr)}}param.delete()}}fun findParamToChange(method: CtMethod<*>) : CtParameter<*>? {return method.parameters.find { it.simpleName == paramName }}
}
首先,我们将新字段添加到类中:
clazz.addField<Any, Nothing>(CtFieldImpl<Any>().let {it.setSimpleName<CtNamedElement>(paramName)it.setType<CtTypedElement<Any>>(paramType)it})
然后,向所有构造函数添加一个参数,以便我们可以接收该值并将其分配给该字段:
// Receive the value for the field in each constructorclazz.constructors.forEach {it.addParameter<Nothing>(CtParameterImpl<Any>().let {it.setSimpleName<CtNamedElement>(paramName)it.setType<CtTypedElement<Any>>(paramType)it})it.body.statements.add(CtAssignmentImpl<Any, Any>().let {it.setAssigned<CtAssignment<Any, Any>>(qualifiedFieldAccess(paramName, clazz.qualifiedName))it.setAssignment<CtRHSReceiver<Any>>(localVarRef(paramName))it})}
请注意,在实际的应用程序中,我们可能还需要考虑该类过去仅具有默认构造函数的情况,并添加一个采用将单个值分配给字段的全新构造函数。 为了简单起见,我们在示例中忽略了这一点。
最后,我们要修改所有方法。 如果他们使用的参数名称被考虑,我们将删除该参数。 我们还将查找对该参数的所有引用,并将其替换为对新字段的引用:
clazz.methods.filter { findParamToChange(it) != null }.forEach {val param = findParamToChange(it)!!CtIterator(it).forEach {if (it is CtParameterReference<*> && it.simpleName == paramName) {val cfr = CtFieldReferenceImpl<Any>()cfr.setSimpleName<CtReference>(paramName)cfr.setDeclaringType<CtFieldReference<Any>>(createTypeReference(clazz.qualifiedName))it.replace(cfr)}}param.delete()}
就是这样! 现在,我们应该只打印代码,我们就完成了。
我们如何打印代码? 通过一个名为toCode的扩展方法:
fun CtClass<*>.toCode() : String {val pp = DefaultJavaPrettyPrinter(StandardEnvironment())val cu = CompilationUnitImpl()pp.calculate(cu, listOf(this))return pp.result
}
有关代码转换的更多信息
如果您想了解有关使用Spoon进行代码转换的更多信息,请看以下内容:
- CocoSpoon ,用于检测Java代码以计算代码覆盖率的工具
- Trebuchet ,一个概念证明,展示如何使用Spoon将Java代码转换为C ++。
这篇文章是如何诞生的
Spoon是处理Java代码的工具。 在某种程度上,它可以看作是JavaParser的竞争对手。 我一直想研究了很长一段时间,但我有许多事情一大堆 ,我想看看和勺子从未到列表的顶部。 然后,JavaParser的一些用户指出了关于Spoon项目的讨论,讨论了JavaParser和Spoon之间的区别。 在我看来,存在一些误解,Spoon的贡献者卖出的JavaParser有点短……在成千上万的开发人员和知名公司都使用JavaParser并对此感到非常满意之后。 另外,JavaParser可能是最著名的Java解析器。 因此,我开始与Spoon的贡献者进行讨论,这引发了撰写本文的想法。
当这篇文章是在Spoon的贡献者的帮助下写的时,我是这篇文章的作者,而且我还是JavaParser的贡献者,所以这是我的“偏见警报”!
比较Spoon和JavaParser
Spoon是JavaParser的学术替代品。 虽然JavaParser本身实现符号解析(这是最难的部分),但Spoon却充当Eclipse Java编译器的包装,然后在其之上构建一些高级API。 那么,这种选择会有什么后果呢?
- Eclipse Java编译器已经成熟,尽管并非没有错误,但它相当可靠
- Eclipse Java编译器是一个大型野兽,它具有依赖性和复杂的配置
- Eclipse Java编译器是……编译器,它不是用于符号解析的库,因此它不如我们在JavaParser上拥有的本地解决方案灵活。
就个人而言,我对成为JavaParser的贡献者有很大的偏见。 我已经习惯了JavaParser,Spoon的某些行为对我来说似乎是不自然的。 例如,对片段表达式的类型强制转换似乎不起作用。 类访问(例如,“ String.class”)不是由特定表达式表示,而是由字段访问表示。 但是,某些功能确实很有用,我们也应该在JavaParser中获得它们。
总而言之,它们是不同的工具,具有不同的功能集,而且我认为也有不同的理念,如下所述。
关于文档,对于JavaParser来说似乎更好一些:我们有一本书,可以免费下载,并下载了数千次,还拥有一套教程。
不同的哲学
现在,Spoon是在学术环境和法国创立的。 以我的经验,法国工程师非常有才华,但他们倾向于以“疯狂的方式”重新发明事物。 以该项目采用的许可证为例:那是Apache许可证吗? GPL? LGPL? Eclipse许可证? 不,这是CeCILL-C免费软件许可协议 。 我从未听说过的许可证,专门为遵守某些法国法规而创建。 现在,这可能是有史以来最伟大的许可证,但是对于想要采用该项目的公司,他们需要仔细研究一下,弄清楚这意味着什么,意味着什么,如果它与他们正在使用的其他许可证兼容,并且以此类推。 我认为,如果他们只是选择一个现有许可证,事情可能会简单得多。 因为存在现实 ,在这种现实中,公司不想仅使用Spoon就必须学习此许可。 这与我们非常务实的 JavaParser中的方法截然不同。 我们与公司讨论并确定了他们需要哪些许可证,然后我们努力为用户提供双重许可证(Apache许可证或LGPL)。 为什么? 因为他们是他们熟悉的选择。
总的来说,当我与Spoon的家伙交谈时,我有不同的哲学感受。 他们清楚地意识到他们的产品要好得多,并且坦率地说,JavaParser如此受欢迎,让他们有些失望。 我们讨论了一些合作的可能性,但在我看来,这是从我们正确的角度出发。 在JavaParser中,我们不认为我们是对的。 我们只是听取用户意见,在彼此之间进行讨论,然后再努力向前迈进,使用户的生活更加轻松。 一个很大的优势就是我们会收到很多反馈,因此当我们出错时,用户可以帮助我们纠正方向。
关于依赖关系,到目前为止,在JavaParser上,我们一直在努力保持核心模块没有任何依赖关系。 我们将来可能会放宽此约束,但总的来说,我们将依赖管理视为重要方面。 相反,在Spoon中,您需要添加一个Maven存储库以使用甚至不在Maven Central或任何知名Maven存储库上的库。 为什么? 为什么要让用户的生活变得更加艰难?
结论
我认为代码处理功能非常强大:它允许使用我们开发人员的技能来自动化部分工作,从而减少工作量和错误。 如果您使用大型代码库,那么它是在工具箱中的好工具。 至少我认为,更多的开发人员应该意识到它提供的可能性。
在Java代码上执行代码处理时,Spoon是有效的解决方案。 因此,我邀请您熟悉并考虑使用它,我想您会帮自己一个忙。
翻译自: https://www.javacodegeeks.com/2019/03/analyze-generate-transform-java-spoon.html