如何以及为什么使用Spoon分析,生成和转换Java代码

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中 ,按方法数量过滤类,然后使用几个实用程序函数来打印这些类的列表(printTitleprintList )。

运行此代码,我们获得以下输出:

=====================================
| 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非常冗长。

例如,我们使用createLocalVarobjectInstance ,如下所示:

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方法。 我们为什么要这样做? 这样我们就可以使用为类( FruitThingVeggie )生成的序列化方法。

生成过程:反序列化

让我们看看我们应该能够生成的反序列化方法:

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/343527.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

PDH-SDH光端机指示灯具体含义介绍

最近有很多客户朋友来咨询有关PDH/SDH光端机各个指示灯含义的问题&#xff0c;其实在很多故障发生的时候我们都可以通过光端机各个指示灯的状态来分析故障问题&#xff0c;从而帮助解决问题。接下来我们就来为大家详细介绍一下PDH/SDH光端机指示灯的具体含义&#xff0c;感兴趣…

【渝粤教育】21秋期末考试工程项目管理10324k2

1、()是项目投产前由建设单位进行的一项重要工作&#xff0c;是在完成建设阶段任务以后转入到建筑产品的生产以及产品经营的必要条件 &#xff08;2.5 分&#xff09; A&#xff0e;可行性研究 B&#xff0e;资金筹备 C&#xff0e;招标 D&#xff0e;生产准备 2、基础设施工程…

【渝粤教育】21秋期末考试建设工程法规10221k1

1、当事人既约定违约金&#xff0c;又约定定金的&#xff0c;一方违约时&#xff0c;这两种违约责任&#xff08;  &#xff09;。&#xff08;1 分&#xff09; A&#xff0e;可合并使用 B&#xff0e;适用数值较小者 C&#xff0e;适用数值较大者 D&#xff0e;只能选择其一…

PDH光端机的主要作用以及特点有哪些?

在之前小编为大家介绍了光端机的详细分类&#xff0c;相信很多看过的朋友对pdh光端机这一块不陌生了吧&#xff0c;但是对于pdh光端机的作用以及特点你还了解吗&#xff0c;今天&#xff0c;飞畅科技的小编就来为大家详细介绍下pdh光端机的作用以及特点&#xff0c;感兴趣的朋友…

【渝粤教育】21秋期末考试服务标准化10011k1

1、采用国际及国外先进标准时&#xff0c;表示“等同采用”的代号是&#xff1f;&#xff08;2 分&#xff09; A&#xff0e;MOD B&#xff0e;NEQ C&#xff0e;ISO D&#xff0e;IDT 2、从命名规则上判断&#xff0c;GB/Z 30556.1-2017《电磁兼容 安装和减缓导则 一般要求》…

嵌入式开发环境构建_设计模式:不可变的嵌入式构建器

嵌入式开发环境构建上周&#xff0c;我写了关于什么使图案成为反图案。 本周&#xff0c;我提出一种设计模式…或等待……也许这是一种反模式。 还是&#xff1f; 让我们看看&#xff01; 当有一个类可以构建另一个实例时&#xff0c;构建器模式是一种编程风格。 构建器模式的…

【渝粤教育】21秋期末考试财务管理10164k2

1、发放股票股利后&#xff0c;不会引起如下情形的有(   )。请在以下&#xff08;1&#xff09;-&#xff08;4&#xff09;中选出符合题意的全部选项&#xff1a;&#xff08;1&#xff09;改变股东的股权比例 &#xff1b;&#xff08;2&#xff09;增加企业的资产&#xf…

python将html存为pdf_用Python将HTML转为PDF。

上期提到了如何获取公众号文章信息&#xff0c;这回就说下怎么将网页转为PDF&#xff0c;方便日常学习。html而后扩散一下以前一个比赛的结果(华章计算机抖音大赛)。数据库顺便谈谈本身对从此送书的一个想法。微信另外以前的送书活动&#xff0c;有中奖的读者没有联系我&#x…

SDH光端机和PDH光端机有什么区别?

通过之前的了解&#xff0c;我们都知道光端机有很多不同的分类&#xff0c;在这其中就有PDH光端机和SDH光端机&#xff0c;那么&#xff0c;PDH光端机和SDH光端机有什么区别&#xff1f;SDH光端机较PDH光端机相比有什么优势呢&#xff1f;接下来就由飞畅科技小编来为大家详细介…

【渝粤教育】国家开放大学2018年春季 0004-22T有机合成单元反应 参考试题

科目编号&#xff1a;0004 座位号&#xff1a; 2017-2018学年度第二学期期末考试 有机合成单元过程 2018年6月 一、选择题&#xff08;每小题3分&#xff0c;共45分&#xff09; 1.&#xff08; )是可以再生的自然资源。 煤 B.农副产品 C.石油 D.天然气 下列有机化合物中的&a…

【渝粤教育】国家开放大学2018年春季 0034-22T现代管理原理 参考试题

科目编号&#xff1a;0034 现代管理原理 试题 2018年7月 单项选择&#xff1a;&#xff08;每小题3分&#xff0c;共计30分&#xff09; 1&#xff0e;下列各选项中&#xff0c;( )属于管理的对象。 A&#xff0e;组织资源和组织目标 B&#xff0e;组织资源和组织活动 C&…

使用Gradle – 2019版从Travis可靠发布到Maven Central

得益于在2018年和2019年末实现的显式登台存储库创建功能集&#xff0c;使您&#xff08;自动&#xff09;从Travis&#xff08;不仅是&#xff09;发布到Maven Central更加可靠。 背景 如果您仅想获取信息&#xff0c;如何使您的工件从Travis发布更可靠&#xff0c;请前进到另…

spring admin mysql_easyadmin: 简易的java后台管理框架,基于SpringBoot+FreeMark+ace admin+mysql...

easyadmin数据库一定要用utf8mb4编码&#xff0c;记得先执行manager.sql&#xff0c;再执行initdata.sql,其中tenant表不用管&#xff0c;这是我打算做的另外一个项目设计的表&#xff0c;因为直接复制的本项目&#xff0c;怕到时候忘记&#xff0c;所以没有删除拦截器相关文档…

【渝粤教育】国家开放大学2018年春季 0050-21T民族理论与民族政策 参考试题

科目编号&#xff1a;0050 座位号 2017-2018学年度第二学期期末考试 民族理论与民族政策 试题 2018年 7 月 一、单项选择题&#xff08;在各题的备选答案中&#xff0c;只有1项是正确的&#xff0c;请将正确答案的序号&#xff0c;填写在题中的括号内。每小题2分&#xff0c;共…

SDI高清视频光端机特点有哪些?

SDI视频光端机设计和生产是适用于电视行业的高可靠、高性能的串行数字光传输设备&#xff0c;信号格式从19.4Mbps到1.485Gbps, 系统支持SMPTE292、SMPTE259M、SMPTE297M、SMPTE305M、SMPTE310M以及DVB-ASI(EN50083-9)等数字电视格式的信号,满足工业级产品的要求。那么&#xff…

【渝粤教育】国家开放大学2018年春季 0089-21TInternet和Intranet应用 参考试题

编号&#xff1a;0089 座位号&#xff1a; 17-18学年第1学期期末考试 Internet和Intranet应用试题 一、填空题&#xff1a;&#xff08;每空4分&#xff0c;共40分&#xff09; 1&#xff0e;Internet上各种网络和各种不同计算机间相互通信的基础是___________协议。 是WWW中…

SDI光端机是什么?SDI光端机产品介绍及监控方案!

SDI光端机就是光信号传输的中端设备。光纤价格的降低使它在各个领域得到很好的应用&#xff0c;因此各个光端机的厂家就好比是雨后春笋般发展起来。但是这里的厂家大部分技术并不是完全成熟&#xff0c;开发新技术需要耗资和人力、物力等&#xff0c;这就产生厂家多是中小企业&…

【渝粤教育】国家开放大学2018年春季 0105-21T酒店营销实务 参考试题

科目编号&#xff1a;0105 座位号 2017年度第二学期期末考试 酒店营销实务 试题 2018年7月 一、名词解释&#xff1a;&#xff08;每题5分&#xff0c;共20分&#xff09; 1&#xff0e;酒店的营销渠道&#xff1a; 产品生命周期&#xff1a; 市场营销&#xff1a; 酒店产品…

【渝粤教育】国家开放大学2018年春季 0177-21T电机学(二) 参考试题

编号&#xff1a;0177 a 2017-2018年度第二学期期末考试 电机学&#xff08;2&#xff09; 试 题 2018年 7 月 一、填空题&#xff08;每空 4分 &#xff0c;共40分&#xff09;1.一台隐极三相同步发电机时空向量中&#xff0b;j轴与&#xff0b;A轴重合&#xff0c;某瞬间转子…