From 0ab221290063d0c3d3f678987a6d333b861944bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A6=82=E6=A2=A6=E6=8A=80=E6=9C=AF?= <596392912@qq.com> Date: Tue, 28 May 2024 11:09:48 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20=E8=AF=95=E7=8E=A9=20kotlin=20ksp?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 17 ++- .../auto/service/AutoServiceProcessor.java | 2 +- .../service/AutoServiceSymbolProcessor.java | 104 ++++++++++++++++ .../AutoServiceSymbolProcessorProvider.java | 39 ++++++ .../AutoServiceSymbolProcessorTest.java | 112 ++++++++++++++++++ 5 files changed, 270 insertions(+), 4 deletions(-) create mode 100644 src/main/java/net/dreamlu/mica/auto/service/AutoServiceSymbolProcessor.java create mode 100644 src/main/java/net/dreamlu/mica/auto/service/AutoServiceSymbolProcessorProvider.java create mode 100644 src/test/java/net/dreamlu/mica/auto/service/AutoServiceSymbolProcessorTest.java diff --git a/build.gradle b/build.gradle index 83d2282..fcf456f 100644 --- a/build.gradle +++ b/build.gradle @@ -1,19 +1,23 @@ -apply plugin: "java-library" +plugins { + id 'java-library' + id 'org.jetbrains.kotlin.jvm' version '1.9.24' apply false +} apply from: "${rootProject.projectDir}/gradle/publish-maven.gradle" ext { javaVersion = JavaVersion.VERSION_1_8 springBootVersion = "2.7.18" - lombokVersion = "1.18.30" + lombokVersion = "1.18.32" googleAutoVersion = "1.1.1" compileTestingVersion = '0.21.0' inCapVersion = "1.0.0" + kotlinVersion = "1.9.24" } group = GROUPID version = VERSION -allprojects { +projects { apply plugin: 'java' sourceCompatibility = javaVersion targetCompatibility = javaVersion @@ -27,6 +31,7 @@ tasks.withType(JavaCompile) { dependencies { api "org.springframework.boot:spring-boot-configuration-processor:$springBootVersion" api "org.springframework.boot:spring-boot-autoconfigure-processor:$springBootVersion" + implementation("com.google.devtools.ksp:symbol-processing-api:1.9.24-1.0.20") compileOnly "org.projectlombok:lombok:$lombokVersion" annotationProcessor "org.projectlombok:lombok:$lombokVersion" compileOnly "com.google.auto.service:auto-service:$googleAutoVersion" @@ -35,6 +40,12 @@ dependencies { annotationProcessor "net.ltgt.gradle.incap:incap-processor:$inCapVersion" testImplementation "com.google.testing.compile:compile-testing:$compileTestingVersion" testImplementation "net.ltgt.gradle.incap:incap:$inCapVersion" + testImplementation 'dev.zacsweers.kctfork:ksp:0.4.1' + testImplementation "org.jetbrains.kotlin:kotlin-compiler-embeddable:$kotlinVersion" + testImplementation "org.jetbrains.kotlin:kotlin-scripting-compiler:$kotlinVersion" + testImplementation "org.jetbrains.kotlin:kotlin-scripting-compiler-impl:$kotlinVersion" + testImplementation "org.jetbrains.kotlin:kotlin-scripting-common:$kotlinVersion" + testImplementation "org.jetbrains.kotlin:kotlin-scripting-jvm:$kotlinVersion" } repositories { diff --git a/src/main/java/net/dreamlu/mica/auto/service/AutoServiceProcessor.java b/src/main/java/net/dreamlu/mica/auto/service/AutoServiceProcessor.java index 1505775..31ba83f 100644 --- a/src/main/java/net/dreamlu/mica/auto/service/AutoServiceProcessor.java +++ b/src/main/java/net/dreamlu/mica/auto/service/AutoServiceProcessor.java @@ -48,7 +48,7 @@ public class AutoServiceProcessor extends AbstractMicaProcessor { /** * AutoService 注解名 */ - private static final String AUTO_SERVICE_NAME = net.dreamlu.mica.auto.annotation.AutoService.class.getName(); + public static final String AUTO_SERVICE_NAME = net.dreamlu.mica.auto.annotation.AutoService.class.getName(); /** * spi 服务集合,key 接口 -> value 实现列表 */ diff --git a/src/main/java/net/dreamlu/mica/auto/service/AutoServiceSymbolProcessor.java b/src/main/java/net/dreamlu/mica/auto/service/AutoServiceSymbolProcessor.java new file mode 100644 index 0000000..19bfb63 --- /dev/null +++ b/src/main/java/net/dreamlu/mica/auto/service/AutoServiceSymbolProcessor.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & www.dreamlu.net). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dreamlu.mica.auto.service; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; +import com.google.devtools.ksp.processing.*; +import com.google.devtools.ksp.symbol.*; +import kotlin.Pair; +import kotlin.sequences.Sequence; +import org.jetbrains.annotations.NotNull; + +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + + +/** + * ksp AutoService 处理器 + * + * @author L.cm + */ +public class AutoServiceSymbolProcessor implements SymbolProcessor { + private final SymbolProcessorEnvironment environment; + private final CodeGenerator codeGenerator; + private final KSPLogger logger; + + /** + * Maps the class names of service provider interfaces to the class names of the concrete classes + * which implement them plus their KSFile (for incremental processing). + *

+ * For example, + * ``` + * "com.google.apphosting.LocalRpcService" -> "com.google.apphosting.datastore.LocalDatastoreService" + * ``` + */ + private final Multimap> providers = HashMultimap.create(); + + private final boolean verify; + private final boolean verbose; + + public AutoServiceSymbolProcessor(SymbolProcessorEnvironment environment) { + this.environment = environment; + this.codeGenerator = environment.getCodeGenerator(); + this.logger = environment.getLogger(); + this.verify = getOrDefault(environment, "autoserviceKsp.verify"); + this.verbose = getOrDefault(environment, "autoserviceKsp.verbose"); + } + + @NotNull + @Override + public List process(@NotNull Resolver resolver) { + KSName autoServiceKsName = resolver.getKSNameFromString(AutoServiceProcessor.AUTO_SERVICE_NAME); + KSClassDeclaration classDeclaration = resolver.getClassDeclarationByName(autoServiceKsName); + if (classDeclaration == null) { + String message = "@AutoService type not found on the classpath, skipping processing."; + if (verbose) { + logger.warn(message, null); + } else { + logger.info(message, null); + } + return Collections.emptyList(); + } + KSType autoServiceType = classDeclaration.asType(Collections.emptyList()); + Sequence annotatedSequence = resolver.getSymbolsWithAnnotation(AutoServiceProcessor.AUTO_SERVICE_NAME, false); + Iterator it = annotatedSequence.iterator(); + if (it.hasNext()) { + KSAnnotated ksAnnotated = it.next(); + if (ksAnnotated instanceof KSClassDeclaration) { + System.out.println(ksAnnotated); + } + System.out.println(ksAnnotated); + } + return Collections.emptyList(); + } + + private void log(String message) { + if (verbose) { + logger.logging(message, null); + } + } + + private static boolean getOrDefault(SymbolProcessorEnvironment environment, String key) { + Map options = environment.getOptions(); + String value = options.get(key); + return value == null || "false".equals(value); + } + +} diff --git a/src/main/java/net/dreamlu/mica/auto/service/AutoServiceSymbolProcessorProvider.java b/src/main/java/net/dreamlu/mica/auto/service/AutoServiceSymbolProcessorProvider.java new file mode 100644 index 0000000..8606ce8 --- /dev/null +++ b/src/main/java/net/dreamlu/mica/auto/service/AutoServiceSymbolProcessorProvider.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & www.dreamlu.net). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.dreamlu.mica.auto.service; + +import com.google.auto.service.AutoService; +import com.google.devtools.ksp.processing.SymbolProcessor; +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment; +import com.google.devtools.ksp.processing.SymbolProcessorProvider; +import org.jetbrains.annotations.NotNull; + +/** + * AutoService SymbolProcessorProvider + * + * @author L.cm + */ +@AutoService(SymbolProcessorProvider.class) +public class AutoServiceSymbolProcessorProvider implements SymbolProcessorProvider { + + @NotNull + @Override + public SymbolProcessor create(@NotNull SymbolProcessorEnvironment environment) { + return new AutoServiceSymbolProcessor(environment); + } + +} diff --git a/src/test/java/net/dreamlu/mica/auto/service/AutoServiceSymbolProcessorTest.java b/src/test/java/net/dreamlu/mica/auto/service/AutoServiceSymbolProcessorTest.java new file mode 100644 index 0000000..d9d2a0d --- /dev/null +++ b/src/test/java/net/dreamlu/mica/auto/service/AutoServiceSymbolProcessorTest.java @@ -0,0 +1,112 @@ +package net.dreamlu.mica.auto.service; + +import com.tschuchort.compiletesting.CompilationResult; +import com.tschuchort.compiletesting.KotlinCompilation; +import com.tschuchort.compiletesting.KotlinCompilation.ExitCode; +import com.tschuchort.compiletesting.KspKt; +import com.tschuchort.compiletesting.SourceFile; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +import static com.google.common.truth.Truth.assertThat; + +@RunWith(Parameterized.class) +public class AutoServiceSymbolProcessorTest { + private final boolean incremental; + + public AutoServiceSymbolProcessorTest(boolean incremental) { + this.incremental = incremental; + } + + @Parameters(name = "incremental={0}") + public static Collection data() { + return Arrays.asList(new Object[][]{{false}, {true}}); + } + + @Test + public void smokeTest() { + SourceFile source = SourceFile.Companion.kotlin("CustomCallable.kt", + "package test;\n" + + "import net.dreamlu.mica.auto.annotation.AutoService;\n" + + "import java.util.concurrent.Callable;\n" + + "\n" + + "@AutoService(Callable.class)\n" + + "class CustomCallable : Callable {\n" + + " override fun call(): String = \"Hello world!\"\n" + + "}", true); + + KotlinCompilation compilation = new KotlinCompilation(); + compilation.setSources(Collections.singletonList(source)); + compilation.setInheritClassPath(true); + KspKt.setSymbolProcessorProviders(compilation, Collections.singletonList(new AutoServiceSymbolProcessorProvider())); + KspKt.setKspIncremental(compilation, incremental); + + CompilationResult result = compilation.compile(); + String messages = result.getMessages(); + System.out.println(messages); + + assertThat(result.getExitCode()).isEqualTo(ExitCode.OK); + File generatedSourcesDir = KspKt.getKspSourcesDir(compilation); + File generatedFile = new File(generatedSourcesDir, "resources/META-INF/services/java.util.concurrent.Callable"); + assertThat(generatedFile.exists()).isTrue(); + } + + @Test + public void smokeTestForJava() { + SourceFile source = SourceFile.Companion.java("CustomCallable.java", + "package test;\n" + + "import net.dreamlu.mica.auto.annotation.AutoService;\n" + + "import java.util.concurrent.Callable;\n" + + "\n" + + "@AutoService(Callable.class)\n" + + "public class CustomCallable implements Callable {\n" + + " @Override public String call() { return \"Hello world!\"; }\n" + + "}", false); + + KotlinCompilation compilation = new KotlinCompilation(); + compilation.setSources(Collections.singletonList(source)); + compilation.setInheritClassPath(true); + KspKt.setSymbolProcessorProviders(compilation, Collections.singletonList(new AutoServiceSymbolProcessorProvider())); + KspKt.setKspIncremental(compilation, incremental); + + CompilationResult result = compilation.compile(); + assertThat(result.getExitCode()).isEqualTo(ExitCode.OK); + + File generatedSourcesDir = KspKt.getKspSourcesDir(compilation); + File generatedFile = new File(generatedSourcesDir, "resources/META-INF/services/java.util.concurrent.Callable"); + assertThat(generatedFile.exists()).isTrue(); + } + + @Test + public void errorOnNoServiceInterfacesProvided() { + SourceFile source = SourceFile.Companion.kotlin("CustomCallable.kt", + "package test;\n" + + "import net.dreamlu.mica.auto.annotation.AutoService;\n" + + "import java.util.concurrent.Callable;\n" + + "\n" + + "@AutoService\n" + + "class CustomCallable : Callable {\n" + + " override fun call(): String = \"Hello world!\"\n" + + "}", false); + + KotlinCompilation compilation = new KotlinCompilation(); + compilation.setSources(Collections.singletonList(source)); + compilation.setInheritClassPath(true); + KspKt.setSymbolProcessorProviders(compilation, Collections.singletonList(new AutoServiceSymbolProcessorProvider())); + KspKt.setKspIncremental(compilation, incremental); + + CompilationResult result = compilation.compile(); + assertThat(result.getExitCode()).isEqualTo(ExitCode.COMPILATION_ERROR); + + assertThat(result.getMessages()) + .contains("No service interfaces specified by @AutoService annotation!\n" + + "You can provide them in annotation parameters: @AutoService(YourService::class)"); + } +}