From 5b0e427f126bf184e84416f4d8d84305144f71dc Mon Sep 17 00:00:00 2001 From: IlyaMuravjov Date: Fri, 21 Apr 2023 11:22:01 +0300 Subject: [PATCH 1/3] Stop using Spring Boot for pure Spring applications --- utbot-spring-analyzer/build.gradle.kts | 66 +++++++----------- .../analyzers/SpringApplicationAnalyzer.kt | 69 +++++++++++++++---- .../exception/UtBotSpringShutdownException.kt | 7 ++ .../loggers/RDApacheCommonsLogFactory.kt | 48 ++++++++----- .../UtBotBeanFactoryPostProcessor.kt | 16 +---- .../spring/process/SpringAnalyzerProcess.kt | 46 ++----------- .../org/utbot/spring/utils/FakeFileManager.kt | 2 +- .../org/utbot/spring/utils/SourceFinder.kt | 41 +++++++++++ 8 files changed, 168 insertions(+), 127 deletions(-) create mode 100644 utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/exception/UtBotSpringShutdownException.kt create mode 100644 utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/utils/SourceFinder.kt diff --git a/utbot-spring-analyzer/build.gradle.kts b/utbot-spring-analyzer/build.gradle.kts index 2f07a60fdc..8c2336741c 100644 --- a/utbot-spring-analyzer/build.gradle.kts +++ b/utbot-spring-analyzer/build.gradle.kts @@ -1,4 +1,3 @@ -import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar import com.github.jengelman.gradle.plugins.shadow.transformers.Log4j2PluginsCacheFileTransformer import com.github.jengelman.gradle.plugins.shadow.transformers.PropertiesFileTransformer @@ -19,62 +18,33 @@ java { targetCompatibility = JavaVersion.VERSION_1_8 } -val withoutSpringConfiguration by configurations.creating {} -val withSpringConfiguration by configurations.creating { - extendsFrom(withoutSpringConfiguration) -} -configurations.implementation.get().extendsFrom(withSpringConfiguration) +val shadowJarConfiguration: Configuration by configurations.creating {} +configurations.implementation.get().extendsFrom(shadowJarConfiguration) dependencies { // https://mvnrepository.com/artifact/org.springframework.boot/spring-boot - withSpringConfiguration("org.springframework.boot:spring-boot:$springBootVersion") - + implementation("org.springframework.boot:spring-boot:$springBootVersion") implementation("io.github.microutils:kotlin-logging:$kotlinLoggingVersion") fun ModuleDependency.excludeSlf4jApi() = exclude(group = "org.slf4j", module = "slf4j-api") - withoutSpringConfiguration(project(":utbot-rd")) { excludeSlf4jApi() } - withoutSpringConfiguration(project(":utbot-core")) { excludeSlf4jApi() } - withoutSpringConfiguration(project(":utbot-framework-api")) { excludeSlf4jApi() } - withoutSpringConfiguration("com.jetbrains.rd:rd-framework:$rdVersion") { excludeSlf4jApi() } - withoutSpringConfiguration("com.jetbrains.rd:rd-core:$rdVersion") { excludeSlf4jApi() } - withoutSpringConfiguration("commons-logging:commons-logging:$commonsLoggingVersion") { excludeSlf4jApi() } - withoutSpringConfiguration("commons-io:commons-io:$commonsIOVersion") { excludeSlf4jApi() } + // TODO stop putting dependencies that are only used in SpringAnalyzerProcess into shadow jar + shadowJarConfiguration(project(":utbot-rd")) { excludeSlf4jApi() } + shadowJarConfiguration(project(":utbot-core")) { excludeSlf4jApi() } + shadowJarConfiguration(project(":utbot-framework-api")) { excludeSlf4jApi() } + shadowJarConfiguration("com.jetbrains.rd:rd-framework:$rdVersion") { excludeSlf4jApi() } + shadowJarConfiguration("com.jetbrains.rd:rd-core:$rdVersion") { excludeSlf4jApi() } + shadowJarConfiguration("commons-logging:commons-logging:$commonsLoggingVersion") { excludeSlf4jApi() } + shadowJarConfiguration("commons-io:commons-io:$commonsIOVersion") { excludeSlf4jApi() } } application { mainClass.set("org.utbot.spring.process.SpringAnalyzerProcessMainKt") } -val shadowWithoutSpring by tasks.register("shadowJarWithoutSpring") { - configureShadowJar(withoutSpringConfiguration) - archiveFileName.set("utbot-spring-analyzer-shadow.jar") -} +tasks.shadowJar { + configurations = listOf(shadowJarConfiguration) -val shadowWithSpring by tasks.register("shadowJarWithSpring") { - configureShadowJar(withSpringConfiguration) - archiveFileName.set("utbot-spring-analyzer-with-spring-shadow.jar") -} - -val springAnalyzerJar: Configuration by configurations.creating { - isCanBeResolved = false - isCanBeConsumed = true -} - -artifacts { - add(springAnalyzerJar.name, shadowWithoutSpring) - add(springAnalyzerJar.name, shadowWithSpring) -} - -fun ShadowJar.configureShadowJar(configuration: Configuration) { - // see more details -- https://github.com/johnrengelman/shadow/blob/master/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.groovy - group = "shadow" - from(sourceSets.main.get().output) - exclude("META-INF/INDEX.LIST", "META-INF/*.SF", "META-INF/*.DSA", "META-INF/*.RSA", "module-info.class") - - configurations = listOf(configuration) - - // see more details -- https://github.com/spring-projects/spring-boot/issues/1828 isZip64 = true // Required for Spring mergeServiceFiles() @@ -87,4 +57,14 @@ fun ShadowJar.configureShadowJar(configuration: Configuration) { }) transform(Log4j2PluginsCacheFileTransformer::class.java) + archiveFileName.set("utbot-spring-analyzer-shadow.jar") +} + +val springAnalyzerJar: Configuration by configurations.creating { + isCanBeResolved = false + isCanBeConsumed = true +} + +artifacts { + add(springAnalyzerJar.name, tasks.shadowJar) } diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/SpringApplicationAnalyzer.kt b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/SpringApplicationAnalyzer.kt index c0c8b86947..17e2fc6e92 100644 --- a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/SpringApplicationAnalyzer.kt +++ b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/SpringApplicationAnalyzer.kt @@ -1,11 +1,17 @@ package org.utbot.spring.analyzers +import com.jetbrains.rd.util.error import com.jetbrains.rd.util.getLogger import com.jetbrains.rd.util.info -import org.springframework.boot.builder.SpringApplicationBuilder +import org.springframework.boot.SpringApplication +import org.springframework.boot.SpringBootVersion import org.springframework.context.ApplicationContextException -import org.utbot.spring.configurators.ApplicationConfigurator +import org.springframework.context.annotation.AnnotationConfigApplicationContext +import org.springframework.core.SpringVersion +import org.utbot.common.silent +import org.utbot.spring.utils.SourceFinder import org.utbot.spring.api.ApplicationData +import org.utbot.spring.exception.UtBotSpringShutdownException import org.utbot.spring.postProcessors.UtBotBeanFactoryPostProcessor val logger = getLogger() @@ -14,21 +20,58 @@ class SpringApplicationAnalyzer(private val applicationData: ApplicationData) { fun analyze(): List { logger.info { "Current Java version is: " + System.getProperty("java.version") } + logger.info { + "Current Spring version is: " + runCatching { SpringVersion.getVersion() }.getOrElse(logger::error) + } + + val isSpringBoot = try { + this::class.java.classLoader.loadClass("org.springframework.boot.SpringApplication") + logger.info { + "Current Spring Boot version is: " + runCatching { SpringBootVersion.getVersion() }.getOrElse(logger::error) + } + true + } catch (e: ClassNotFoundException) { + logger.info { "Spring Boot is not detected" } + false + } + + logger.info { "Configuration file is: ${applicationData.configurationFile}" } + val sources = SourceFinder(applicationData).findSources() + + if (isSpringBoot) analyzeSpringBootApplication(sources) + else analyzePureSpringApplication(sources) + + return UtBotBeanFactoryPostProcessor.beanQualifiedNames + } - val applicationBuilder = SpringApplicationBuilder(SpringApplicationAnalyzer::class.java) - val applicationConfigurator = ApplicationConfigurator(applicationBuilder, applicationData) + private fun analyzePureSpringApplication(sources: Array>) { + logger.info { "Analyzing pure Spring application" } + runExpectingUtBotSpringShutdownException { + AnnotationConfigApplicationContext(*sources) + } + } - applicationConfigurator.configureApplication() + private fun analyzeSpringBootApplication(sources: Array>) { + logger.info { "Analyzing Spring Boot application" } + try { + runExpectingUtBotSpringShutdownException { + SpringApplication(*sources).run() + } + } catch (e: Throwable) { + logger.error("Failed to analyze Spring Boot application, falling back to using pure Spring", e) + analyzePureSpringApplication(sources) + } + } + private fun runExpectingUtBotSpringShutdownException(block: () -> Unit) { try { - applicationBuilder.build() - applicationBuilder.run() - } catch (e: ApplicationContextException) { - // UtBotBeanFactoryPostProcessor destroys bean definitions - // to prevent Spring application from actually starting and - // that causes it to throw ApplicationContextException. - logger.info { "Bean analysis finished successfully" } + block() + throw IllegalStateException("Failed to shutdown Spring application with UtBotSpringShutdownException") + } catch (e: Throwable) { + if (generateSequence(e) { it.cause }.any { it is UtBotSpringShutdownException }) + logger.info { "Spring application has been successfully shutdown with UtBotSpringShutdownException" } + else + throw e } - return UtBotBeanFactoryPostProcessor.beanQualifiedNames } } diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/exception/UtBotSpringShutdownException.kt b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/exception/UtBotSpringShutdownException.kt new file mode 100644 index 0000000000..69571195c5 --- /dev/null +++ b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/exception/UtBotSpringShutdownException.kt @@ -0,0 +1,7 @@ +package org.utbot.spring.exception + +/** + * Use this exception to shutdown the application + * when all required analysis actions are completed. + */ +class UtBotSpringShutdownException(message: String): RuntimeException(message) diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/loggers/RDApacheCommonsLogFactory.kt b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/loggers/RDApacheCommonsLogFactory.kt index 9fd5f67c9c..0c78cb87a1 100644 --- a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/loggers/RDApacheCommonsLogFactory.kt +++ b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/loggers/RDApacheCommonsLogFactory.kt @@ -2,33 +2,45 @@ package org.utbot.spring.loggers import com.jetbrains.rd.util.LogLevel import com.jetbrains.rd.util.getLogger +import com.jetbrains.rd.util.info import org.apache.commons.logging.Log import org.apache.commons.logging.impl.LogFactoryImpl +import org.utbot.spring.exception.UtBotSpringShutdownException @Suppress("unused") // used via -Dorg.apache.commons.logging.LogFactory=org.utbot.spring.loggers.RDApacheCommonsLogFactory class RDApacheCommonsLogFactory : LogFactoryImpl() { override fun getInstance(name: String): Log { val logger = getLogger(category = name) return object : Log { - override fun trace(message: Any?) = logger.log(LogLevel.Trace, message, throwable = null) - override fun trace(message: Any?, t: Throwable?) = logger.log(LogLevel.Trace, message, throwable = t) - override fun debug(message: Any?) = logger.log(LogLevel.Debug, message, throwable = null) - override fun debug(message: Any?, t: Throwable?) = logger.log(LogLevel.Debug, message, throwable = t) - override fun info(message: Any?) = logger.log(LogLevel.Info, message, throwable = null) - override fun info(message: Any?, t: Throwable?) = logger.log(LogLevel.Info, message, throwable = t) - override fun warn(message: Any?) = logger.log(LogLevel.Warn, message, throwable = null) - override fun warn(message: Any?, t: Throwable?) = logger.log(LogLevel.Warn, message, throwable = t) - override fun error(message: Any?) = logger.log(LogLevel.Error, message, throwable = null) - override fun error(message: Any?, t: Throwable?) = logger.log(LogLevel.Error, message, throwable = t) - override fun fatal(message: Any?) = logger.log(LogLevel.Fatal, message, throwable = null) - override fun fatal(message: Any?, t: Throwable?) = logger.log(LogLevel.Fatal, message, throwable = t) + private fun log(level: LogLevel, message: Any?, throwable: Throwable?) { + if (throwable is UtBotSpringShutdownException) { + // avoid polluting logs with stack trace of expected exception + logger.info { message } + logger.info { "${throwable::class.java.name}: ${throwable.message}" } + } else + logger.log(level, message, throwable) + } + private fun isEnabled(level: LogLevel) = logger.isEnabled(level) - override fun isTraceEnabled() = logger.isEnabled(LogLevel.Trace) - override fun isDebugEnabled() = logger.isEnabled(LogLevel.Debug) - override fun isInfoEnabled() = logger.isEnabled(LogLevel.Info) - override fun isErrorEnabled() = logger.isEnabled(LogLevel.Error) - override fun isFatalEnabled() = logger.isEnabled(LogLevel.Fatal) - override fun isWarnEnabled() = logger.isEnabled(LogLevel.Warn) + override fun trace(message: Any?) = log(LogLevel.Trace, message, throwable = null) + override fun trace(message: Any?, t: Throwable?) = log(LogLevel.Trace, message, throwable = t) + override fun debug(message: Any?) = log(LogLevel.Debug, message, throwable = null) + override fun debug(message: Any?, t: Throwable?) = log(LogLevel.Debug, message, throwable = t) + override fun info(message: Any?) = log(LogLevel.Info, message, throwable = null) + override fun info(message: Any?, t: Throwable?) = log(LogLevel.Info, message, throwable = t) + override fun warn(message: Any?) = log(LogLevel.Warn, message, throwable = null) + override fun warn(message: Any?, t: Throwable?) = log(LogLevel.Warn, message, throwable = t) + override fun error(message: Any?) = log(LogLevel.Error, message, throwable = null) + override fun error(message: Any?, t: Throwable?) = log(LogLevel.Error, message, throwable = t) + override fun fatal(message: Any?) = log(LogLevel.Fatal, message, throwable = null) + override fun fatal(message: Any?, t: Throwable?) = log(LogLevel.Fatal, message, throwable = t) + + override fun isTraceEnabled() = isEnabled(LogLevel.Trace) + override fun isDebugEnabled() = isEnabled(LogLevel.Debug) + override fun isInfoEnabled() = isEnabled(LogLevel.Info) + override fun isErrorEnabled() = isEnabled(LogLevel.Error) + override fun isFatalEnabled() = isEnabled(LogLevel.Fatal) + override fun isWarnEnabled() = isEnabled(LogLevel.Warn) } } } \ No newline at end of file diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/postProcessors/UtBotBeanFactoryPostProcessor.kt b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/postProcessors/UtBotBeanFactoryPostProcessor.kt index d0cf641512..909b7e8813 100644 --- a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/postProcessors/UtBotBeanFactoryPostProcessor.kt +++ b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/postProcessors/UtBotBeanFactoryPostProcessor.kt @@ -4,11 +4,10 @@ import com.jetbrains.rd.util.getLogger import com.jetbrains.rd.util.info import com.jetbrains.rd.util.warn import org.springframework.beans.factory.BeanCreationException -import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition import org.springframework.beans.factory.config.BeanFactoryPostProcessor import org.springframework.beans.factory.config.ConfigurableListableBeanFactory -import org.springframework.beans.factory.support.BeanDefinitionRegistry import org.springframework.core.PriorityOrdered +import org.utbot.spring.exception.UtBotSpringShutdownException val logger = getLogger() @@ -27,11 +26,9 @@ object UtBotBeanFactoryPostProcessor : BeanFactoryPostProcessor, PriorityOrdered beanQualifiedNames = findBeanClassNames(beanFactory) logger.info { "Detected Beans: $beanQualifiedNames" } - // After desired post-processing is completed we destroy bean definitions - // to avoid further possible actions with beans that may be unsafe. - destroyBeanDefinitions(beanFactory) - logger.info { "Finished post-processing bean factory in UtBot" } + + throw UtBotSpringShutdownException("Finished post-processing bean factory in UtBot") } private fun findBeanClassNames(beanFactory: ConfigurableListableBeanFactory): List { @@ -45,11 +42,4 @@ object UtBotBeanFactoryPostProcessor : BeanFactoryPostProcessor, PriorityOrdered }.filterNot { it.startsWith("org.utbot.spring") } .distinct() } - - private fun destroyBeanDefinitions(beanFactory: ConfigurableListableBeanFactory) { - for (beanDefinitionName in beanFactory.beanDefinitionNames) { - val beanRegistry = beanFactory as BeanDefinitionRegistry - beanRegistry.removeBeanDefinition(beanDefinitionName) - } - } } diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/process/SpringAnalyzerProcess.kt b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/process/SpringAnalyzerProcess.kt index 922dd9d7d4..e98e418025 100644 --- a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/process/SpringAnalyzerProcess.kt +++ b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/process/SpringAnalyzerProcess.kt @@ -23,7 +23,6 @@ import org.utbot.spring.generated.SpringAnalyzerProcessModel import org.utbot.spring.generated.springAnalyzerProcessModel import java.io.File import java.net.URL -import java.net.URLClassLoader import java.nio.file.Files class SpringAnalyzerProcessInstantDeathException : @@ -32,10 +31,8 @@ class SpringAnalyzerProcessInstantDeathException : UtSettings.runSpringAnalyzerProcessWithDebug ) -private const val SPRING_ANALYZER_WITHOUT_SPRINGBOOT_JAR_FILENAME = "utbot-spring-analyzer-shadow.jar" -private const val SPRING_ANALYZER_WITH_SPRINGBOOT_JAR_FILENAME = "utbot-spring-analyzer-with-spring-shadow.jar" -private const val SPRING_ANALYZER_WITHOUT_SPRINBOOT_JAR_PATH = "lib/$SPRING_ANALYZER_WITHOUT_SPRINGBOOT_JAR_FILENAME" -private const val SPRING_ANALYZER_WITH_SPRINBOOT_JAR_PATH = "lib/$SPRING_ANALYZER_WITH_SPRINGBOOT_JAR_FILENAME" +private const val SPRING_ANALYZER_JAR_FILENAME = "utbot-spring-analyzer-shadow.jar" +private const val SPRING_ANALYZER_JAR_PATH = "lib/$SPRING_ANALYZER_JAR_FILENAME" private const val UNKNOWN_MODIFICATION_TIME = 0L @@ -47,23 +44,14 @@ private var classpathArgs = listOf() private val springAnalyzerDirectory = Files.createDirectories(utBotTempDirectory.toFile().resolve("spring-analyzer").toPath()).toFile() -private val springAnalyzerWithoutSpringBootJarFile = +private val springAnalyzerJarFile = springAnalyzerDirectory - .resolve(SPRING_ANALYZER_WITHOUT_SPRINGBOOT_JAR_FILENAME).also { jarFile -> - val resource = SpringAnalyzerProcess::class.java.classLoader.getResource(SPRING_ANALYZER_WITHOUT_SPRINBOOT_JAR_PATH) - ?: error("Unable to find \"$SPRING_ANALYZER_WITHOUT_SPRINBOOT_JAR_PATH\" in resources, make sure it's on the classpath") + .resolve(SPRING_ANALYZER_JAR_FILENAME).also { jarFile -> + val resource = SpringAnalyzerProcess::class.java.classLoader.getResource(SPRING_ANALYZER_JAR_PATH) + ?: error("Unable to find \"$SPRING_ANALYZER_JAR_PATH\" in resources, make sure it's on the classpath") updateJarIfRequired(jarFile, resource) } -private val springAnalyzerWithSpringBootJarFile = - springAnalyzerDirectory - .resolve(SPRING_ANALYZER_WITH_SPRINGBOOT_JAR_FILENAME).also { jarFile -> - val resource = SpringAnalyzerProcess::class.java.classLoader.getResource(SPRING_ANALYZER_WITH_SPRINBOOT_JAR_PATH) - ?: error("Unable to find \"$SPRING_ANALYZER_WITH_SPRINBOOT_JAR_PATH\" in resources, make sure it's on the classpath") - updateJarIfRequired(jarFile, resource) - } - - private fun updateJarIfRequired(jarFile: File, resource: URL) { val resourceConnection = resource.openConnection() val lastResourceModification = try { @@ -97,8 +85,7 @@ class SpringAnalyzerProcess private constructor( suspend operator fun invoke(classpathItems: List): SpringAnalyzerProcess = LifetimeDefinition().terminateOnException { lifetime -> - val requiredSpringAnalyzerJarPath = findRequiredSpringAnalyzerJarPath(classpathItems) - val extendedClasspath = listOf(requiredSpringAnalyzerJarPath) + classpathItems + val extendedClasspath = listOf(springAnalyzerJarFile.path) + classpathItems val rdProcess = startUtProcessWithRdServer(lifetime) { port -> classpathArgs = listOf( @@ -122,25 +109,6 @@ class SpringAnalyzerProcess private constructor( proc.loggerModel.setup(rdLogger, proc.lifetime) return proc } - - /** - * Finds the required version of `utbot-spring-analyzer`. - * - * If user project type is SpringBootApplication, we use his `spring-boot` version. - * If it is a "pure Spring" project, we have to add dependencies on `spring-boot` - * to manage to create our internal SpringBootApplication for bean definitions analysis. - */ - private fun findRequiredSpringAnalyzerJarPath(classpathItems: List): String { - val testClassLoader = URLClassLoader(classpathItems.map { File(it).toURI().toURL() }.toTypedArray(), null) - try { - testClassLoader.loadClass("org.springframework.boot.builder.SpringApplicationBuilder") - } catch (e: ClassNotFoundException) { - return springAnalyzerWithSpringBootJarFile.path - } - - // TODO: think about using different spring-boot versions depending on spring version in user project - return springAnalyzerWithoutSpringBootJarFile.path - } } private val springAnalyzerModel: SpringAnalyzerProcessModel = onSchedulerBlocking { protocol.springAnalyzerProcessModel } diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/utils/FakeFileManager.kt b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/utils/FakeFileManager.kt index 5b8ab23572..d1b5e66e57 100644 --- a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/utils/FakeFileManager.kt +++ b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/utils/FakeFileManager.kt @@ -5,7 +5,7 @@ import com.jetbrains.rd.util.info import java.io.File import java.io.IOException -val logger = getLogger() +private val logger = getLogger() class FakeFileManager(private val fakeFilesList: List) { diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/utils/SourceFinder.kt b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/utils/SourceFinder.kt new file mode 100644 index 0000000000..463ba5e260 --- /dev/null +++ b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/utils/SourceFinder.kt @@ -0,0 +1,41 @@ +package org.utbot.spring.utils + +import com.jetbrains.rd.util.getLogger +import com.jetbrains.rd.util.info +import org.utbot.spring.api.ApplicationData +import org.utbot.spring.config.TestApplicationConfiguration +import org.utbot.spring.configurators.ApplicationConfigurationType +import java.io.File +import kotlin.io.path.Path + +private val logger = getLogger() + +open class SourceFinder( + private val applicationData: ApplicationData +) { + private val classLoader: ClassLoader = this::class.java.classLoader + + fun findSources(): Array> = when (configurationType) { + ApplicationConfigurationType.XmlConfiguration -> { + logger.info { "Using xml Spring configuration" } + val configurationManager = ConfigurationManager(classLoader, TestApplicationConfiguration::class.java) + // Put `applicationData.configurationFile` in `@ImportResource` of `TestApplicationConfiguration` + configurationManager.patchImportResourceAnnotation(Path(applicationData.configurationFile).fileName) + arrayOf(TestApplicationConfiguration::class.java) + } + else -> { + logger.info { "Using java Spring configuration" } + arrayOf( + TestApplicationConfiguration::class.java, + classLoader.loadClass(applicationData.configurationFile) + ) + } + } + + //TODO: support Spring Boot Applications here. + private val configurationType: ApplicationConfigurationType + get() = when (File(applicationData.configurationFile).extension) { + "xml" -> ApplicationConfigurationType.XmlConfiguration + else -> ApplicationConfigurationType.JavaConfiguration + } +} From 7c6c685647102ac062cbfef8ecdf2627f7460f60 Mon Sep 17 00:00:00 2001 From: IlyaMuravjov Date: Mon, 24 Apr 2023 10:46:24 +0300 Subject: [PATCH 2/3] Set profiles when analyzing pure Spring application --- .../PureSpringApplicationAnalyzer.kt | 18 +++++ .../SpringApplicationAnalysisContext.kt | 11 +++ .../analyzers/SpringApplicationAnalyzer.kt | 78 +------------------ .../SpringApplicationAnalyzerFacade.kt | 46 +++++++++++ .../SpringBootApplicationAnalyzer.kt | 24 ++++++ .../configurators/ApplicationConfigurator.kt | 67 ---------------- .../exception/UtBotSpringShutdownException.kt | 26 ++++++- .../UtBotBeanFactoryPostProcessor.kt | 9 +-- .../process/SpringAnalyzerProcessMain.kt | 4 +- .../utbot/spring/utils/EnvironmentFactory.kt | 26 +++++++ 10 files changed, 158 insertions(+), 151 deletions(-) create mode 100644 utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/PureSpringApplicationAnalyzer.kt create mode 100644 utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/SpringApplicationAnalysisContext.kt create mode 100644 utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/SpringApplicationAnalyzerFacade.kt create mode 100644 utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/SpringBootApplicationAnalyzer.kt delete mode 100644 utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/configurators/ApplicationConfigurator.kt create mode 100644 utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/utils/EnvironmentFactory.kt diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/PureSpringApplicationAnalyzer.kt b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/PureSpringApplicationAnalyzer.kt new file mode 100644 index 0000000000..5cbff0fded --- /dev/null +++ b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/PureSpringApplicationAnalyzer.kt @@ -0,0 +1,18 @@ +package org.utbot.spring.analyzers + +import com.jetbrains.rd.util.getLogger +import com.jetbrains.rd.util.info +import org.springframework.context.annotation.AnnotationConfigApplicationContext +import org.utbot.spring.exception.UtBotSpringShutdownException + +private val logger = getLogger() + +class PureSpringApplicationAnalyzer : SpringApplicationAnalyzer { + override fun analyze(analysisContext: SpringApplicationAnalysisContext): List { + logger.info { "Analyzing pure Spring application" } + val applicationContext = AnnotationConfigApplicationContext() + applicationContext.register(*analysisContext.sources) + applicationContext.environment = analysisContext.createEnvironment() + return UtBotSpringShutdownException.catch { applicationContext.refresh() }.beanQualifiedNames + } +} \ No newline at end of file diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/SpringApplicationAnalysisContext.kt b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/SpringApplicationAnalysisContext.kt new file mode 100644 index 0000000000..0f2d54c0d2 --- /dev/null +++ b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/SpringApplicationAnalysisContext.kt @@ -0,0 +1,11 @@ +package org.utbot.spring.analyzers + +import org.springframework.core.env.ConfigurableEnvironment +import org.utbot.spring.utils.EnvironmentFactory + +class SpringApplicationAnalysisContext( + val sources: Array>, + private val environmentFactory: EnvironmentFactory +) { + fun createEnvironment(): ConfigurableEnvironment = environmentFactory.createEnvironment() +} \ No newline at end of file diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/SpringApplicationAnalyzer.kt b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/SpringApplicationAnalyzer.kt index 17e2fc6e92..cca4a522ee 100644 --- a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/SpringApplicationAnalyzer.kt +++ b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/SpringApplicationAnalyzer.kt @@ -1,77 +1,5 @@ package org.utbot.spring.analyzers -import com.jetbrains.rd.util.error -import com.jetbrains.rd.util.getLogger -import com.jetbrains.rd.util.info -import org.springframework.boot.SpringApplication -import org.springframework.boot.SpringBootVersion -import org.springframework.context.ApplicationContextException -import org.springframework.context.annotation.AnnotationConfigApplicationContext -import org.springframework.core.SpringVersion -import org.utbot.common.silent -import org.utbot.spring.utils.SourceFinder -import org.utbot.spring.api.ApplicationData -import org.utbot.spring.exception.UtBotSpringShutdownException -import org.utbot.spring.postProcessors.UtBotBeanFactoryPostProcessor - -val logger = getLogger() - -class SpringApplicationAnalyzer(private val applicationData: ApplicationData) { - - fun analyze(): List { - logger.info { "Current Java version is: " + System.getProperty("java.version") } - logger.info { - "Current Spring version is: " + runCatching { SpringVersion.getVersion() }.getOrElse(logger::error) - } - - val isSpringBoot = try { - this::class.java.classLoader.loadClass("org.springframework.boot.SpringApplication") - logger.info { - "Current Spring Boot version is: " + runCatching { SpringBootVersion.getVersion() }.getOrElse(logger::error) - } - true - } catch (e: ClassNotFoundException) { - logger.info { "Spring Boot is not detected" } - false - } - - logger.info { "Configuration file is: ${applicationData.configurationFile}" } - val sources = SourceFinder(applicationData).findSources() - - if (isSpringBoot) analyzeSpringBootApplication(sources) - else analyzePureSpringApplication(sources) - - return UtBotBeanFactoryPostProcessor.beanQualifiedNames - } - - private fun analyzePureSpringApplication(sources: Array>) { - logger.info { "Analyzing pure Spring application" } - runExpectingUtBotSpringShutdownException { - AnnotationConfigApplicationContext(*sources) - } - } - - private fun analyzeSpringBootApplication(sources: Array>) { - logger.info { "Analyzing Spring Boot application" } - try { - runExpectingUtBotSpringShutdownException { - SpringApplication(*sources).run() - } - } catch (e: Throwable) { - logger.error("Failed to analyze Spring Boot application, falling back to using pure Spring", e) - analyzePureSpringApplication(sources) - } - } - - private fun runExpectingUtBotSpringShutdownException(block: () -> Unit) { - try { - block() - throw IllegalStateException("Failed to shutdown Spring application with UtBotSpringShutdownException") - } catch (e: Throwable) { - if (generateSequence(e) { it.cause }.any { it is UtBotSpringShutdownException }) - logger.info { "Spring application has been successfully shutdown with UtBotSpringShutdownException" } - else - throw e - } - } -} +interface SpringApplicationAnalyzer { + fun analyze(analysisContext: SpringApplicationAnalysisContext): List +} \ No newline at end of file diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/SpringApplicationAnalyzerFacade.kt b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/SpringApplicationAnalyzerFacade.kt new file mode 100644 index 0000000000..cae212987a --- /dev/null +++ b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/SpringApplicationAnalyzerFacade.kt @@ -0,0 +1,46 @@ +package org.utbot.spring.analyzers + +import com.jetbrains.rd.util.error +import com.jetbrains.rd.util.getLogger +import com.jetbrains.rd.util.info +import org.springframework.boot.SpringBootVersion +import org.springframework.boot.builder.SpringApplicationBuilder +import org.springframework.context.annotation.AnnotationConfigApplicationContext +import org.springframework.core.SpringVersion +import org.utbot.spring.utils.SourceFinder +import org.utbot.spring.api.ApplicationData +import org.utbot.spring.exception.UtBotSpringShutdownException +import org.utbot.spring.postProcessors.UtBotBeanFactoryPostProcessor +import org.utbot.spring.utils.EnvironmentFactory + +private val logger = getLogger() + +class SpringApplicationAnalyzerFacade(private val applicationData: ApplicationData) { + + fun analyze(): List { + logger.info { "Current Java version is: " + System.getProperty("java.version") } + logger.info { + "Current Spring version is: " + runCatching { SpringVersion.getVersion() }.getOrElse(logger::error) + } + + val analyzer = try { + this::class.java.classLoader.loadClass("org.springframework.boot.SpringApplication") + logger.info { + "Current Spring Boot version is: " + runCatching { SpringBootVersion.getVersion() }.getOrElse(logger::error) + } + SpringBootApplicationAnalyzer() + } catch (e: ClassNotFoundException) { + logger.info { "Spring Boot is not detected" } + PureSpringApplicationAnalyzer() + } + + logger.info { "Configuration file is: ${applicationData.configurationFile}" } + + return analyzer.analyze( + SpringApplicationAnalysisContext( + sources = SourceFinder(applicationData).findSources(), + environmentFactory = EnvironmentFactory(applicationData) + ) + ) + } +} diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/SpringBootApplicationAnalyzer.kt b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/SpringBootApplicationAnalyzer.kt new file mode 100644 index 0000000000..8cf9521494 --- /dev/null +++ b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/SpringBootApplicationAnalyzer.kt @@ -0,0 +1,24 @@ +package org.utbot.spring.analyzers + +import com.jetbrains.rd.util.error +import com.jetbrains.rd.util.getLogger +import com.jetbrains.rd.util.info +import org.springframework.boot.builder.SpringApplicationBuilder +import org.utbot.spring.exception.UtBotSpringShutdownException + +private val logger = getLogger() + +class SpringBootApplicationAnalyzer : SpringApplicationAnalyzer { + override fun analyze(analysisContext: SpringApplicationAnalysisContext): List { + logger.info { "Analyzing Spring Boot application" } + return try { + val app = SpringApplicationBuilder(*analysisContext.sources) + .environment(analysisContext.createEnvironment()) + .build() + UtBotSpringShutdownException.catch { app.run() }.beanQualifiedNames + } catch (e: Throwable) { + logger.error("Failed to analyze Spring Boot application, falling back to using pure Spring", e) + PureSpringApplicationAnalyzer().analyze(analysisContext) + } + } +} \ No newline at end of file diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/configurators/ApplicationConfigurator.kt b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/configurators/ApplicationConfigurator.kt deleted file mode 100644 index 58d83c51f8..0000000000 --- a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/configurators/ApplicationConfigurator.kt +++ /dev/null @@ -1,67 +0,0 @@ -package org.utbot.spring.configurators - -import com.jetbrains.rd.util.getLogger -import com.jetbrains.rd.util.info -import org.springframework.boot.builder.SpringApplicationBuilder -import org.springframework.core.env.StandardEnvironment -import org.utbot.spring.api.ApplicationData -import org.utbot.spring.config.TestApplicationConfiguration -import org.utbot.spring.utils.ConfigurationManager -import java.io.File -import kotlin.io.path.Path - -const val DEFAULT_PROFILE_NAME = "default" -private val logger = getLogger() - -open class ApplicationConfigurator( - private val applicationBuilder: SpringApplicationBuilder, - private val applicationData: ApplicationData -) { - //private val classLoader: ClassLoader = URLClassLoader(applicationData.classpath) - private val classLoader: ClassLoader = this::class.java.classLoader - - fun configureApplication() { - // TODO: this may help to use file named `application.xml` as a config in Spring Boot. - //applicationBuilder.resourceLoader(DefaultResourceLoader().also { it.classLoader = classLoader }) - - when (findConfigurationType(applicationData)) { - ApplicationConfigurationType.XmlConfiguration -> { - logger.info { "Using xml Spring configuration" } - val configurationManager = ConfigurationManager(classLoader, TestApplicationConfiguration::class.java) - // Put `applicationData.configurationFile` in `@ImportResource` of `TestApplicationConfiguration` - configurationManager.patchImportResourceAnnotation(Path(applicationData.configurationFile).fileName) - applicationBuilder.sources(TestApplicationConfiguration::class.java) - } - else -> { - logger.info { "Using java Spring configuration" } - applicationBuilder.sources( - TestApplicationConfiguration::class.java, - classLoader.loadClass(applicationData.configurationFile) - ) - } - } - - setActiveProfile(applicationData.profileExpression ?: DEFAULT_PROFILE_NAME) - } - - private fun setActiveProfile(profileExpression: String) { - val profilesToActivate = parseProfileExpression(profileExpression) - - val environment = StandardEnvironment() - environment.setActiveProfiles(*profilesToActivate) - - applicationBuilder.environment(environment) - } - - //TODO: implement this, e.g. 'prod|web' -> listOf(prod, web) - private fun parseProfileExpression(profileExpression: String) : Array = arrayOf(profileExpression) - - private fun findConfigurationType(applicationData: ApplicationData): ApplicationConfigurationType { - //TODO: support Spring Boot Applications here. - val fileExtension = File(applicationData.configurationFile).extension - return when (fileExtension) { - "xml" -> ApplicationConfigurationType.XmlConfiguration - else -> ApplicationConfigurationType.JavaConfiguration - } - } -} diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/exception/UtBotSpringShutdownException.kt b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/exception/UtBotSpringShutdownException.kt index 69571195c5..6b3b30ce44 100644 --- a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/exception/UtBotSpringShutdownException.kt +++ b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/exception/UtBotSpringShutdownException.kt @@ -1,7 +1,31 @@ package org.utbot.spring.exception +import com.jetbrains.rd.util.getLogger +import com.jetbrains.rd.util.info + +private val logger = getLogger() + /** * Use this exception to shutdown the application * when all required analysis actions are completed. */ -class UtBotSpringShutdownException(message: String): RuntimeException(message) +class UtBotSpringShutdownException( + message: String, + val beanQualifiedNames: List +): RuntimeException(message) { + companion object { + fun catch(block: () -> Unit): UtBotSpringShutdownException { + try { + block() + throw IllegalStateException("UtBotSpringShutdownException has not been thrown") + } catch (e: Throwable) { + for(cause in generateSequence(e) { it.cause }) + if (cause is UtBotSpringShutdownException) { + logger.info { "UtBotSpringShutdownException has been successfully caught" } + return cause + } + throw e + } + } + } +} diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/postProcessors/UtBotBeanFactoryPostProcessor.kt b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/postProcessors/UtBotBeanFactoryPostProcessor.kt index 909b7e8813..476b43dbd5 100644 --- a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/postProcessors/UtBotBeanFactoryPostProcessor.kt +++ b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/postProcessors/UtBotBeanFactoryPostProcessor.kt @@ -12,9 +12,6 @@ import org.utbot.spring.exception.UtBotSpringShutdownException val logger = getLogger() object UtBotBeanFactoryPostProcessor : BeanFactoryPostProcessor, PriorityOrdered { - var beanQualifiedNames: List = emptyList() - private set - /** * Sets the priority of post processor to highest to avoid side effects from others. */ @@ -23,12 +20,12 @@ object UtBotBeanFactoryPostProcessor : BeanFactoryPostProcessor, PriorityOrdered override fun postProcessBeanFactory(beanFactory: ConfigurableListableBeanFactory) { logger.info { "Started post-processing bean factory in UtBot" } - beanQualifiedNames = findBeanClassNames(beanFactory) - logger.info { "Detected Beans: $beanQualifiedNames" } + val beanQualifiedNames = findBeanClassNames(beanFactory) + logger.info { "Detected ${beanQualifiedNames.size} bean qualified names" } logger.info { "Finished post-processing bean factory in UtBot" } - throw UtBotSpringShutdownException("Finished post-processing bean factory in UtBot") + throw UtBotSpringShutdownException("Finished post-processing bean factory in UtBot", beanQualifiedNames) } private fun findBeanClassNames(beanFactory: ConfigurableListableBeanFactory): List { diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/process/SpringAnalyzerProcessMain.kt b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/process/SpringAnalyzerProcessMain.kt index 5a19d80bb6..ab0c2378ce 100644 --- a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/process/SpringAnalyzerProcessMain.kt +++ b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/process/SpringAnalyzerProcessMain.kt @@ -13,7 +13,7 @@ import org.utbot.rd.RdSettingsContainerFactory import org.utbot.rd.generated.loggerModel import org.utbot.rd.generated.settingsModel import org.utbot.rd.loggers.UtRdRemoteLoggerFactory -import org.utbot.spring.analyzers.SpringApplicationAnalyzer +import org.utbot.spring.analyzers.SpringApplicationAnalyzerFacade import org.utbot.spring.api.ApplicationData import org.utbot.spring.generated.SpringAnalyzerProcessModel import org.utbot.spring.generated.SpringAnalyzerResult @@ -47,7 +47,7 @@ private fun SpringAnalyzerProcessModel.setup(watchdog: IdleWatchdog, realProtoco params.profileExpression, ) SpringAnalyzerResult( - SpringApplicationAnalyzer(applicationData).analyze().toTypedArray() + SpringApplicationAnalyzerFacade(applicationData).analyze().toTypedArray() ) } } \ No newline at end of file diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/utils/EnvironmentFactory.kt b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/utils/EnvironmentFactory.kt new file mode 100644 index 0000000000..baec9cb353 --- /dev/null +++ b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/utils/EnvironmentFactory.kt @@ -0,0 +1,26 @@ +package org.utbot.spring.utils + +import org.springframework.core.env.ConfigurableEnvironment +import org.springframework.core.env.StandardEnvironment +import org.utbot.spring.api.ApplicationData + +class EnvironmentFactory( + private val applicationData: ApplicationData +) { + companion object { + const val DEFAULT_PROFILE_NAME = "default" + } + + fun createEnvironment(): ConfigurableEnvironment { + val profilesToActivate = parseProfileExpression(applicationData.profileExpression ?: DEFAULT_PROFILE_NAME) + + val environment = StandardEnvironment() + environment.setActiveProfiles(*profilesToActivate) + + return environment + } + + //TODO: implement this, e.g. 'prod|web' -> listOf(prod, web) + private fun parseProfileExpression(profileExpression: String) : Array = arrayOf(profileExpression) + +} \ No newline at end of file From 1c0b6c1c434e035bd6f2b8ca2e34e6cce599c995 Mon Sep 17 00:00:00 2001 From: IlyaMuravjov Date: Mon, 24 Apr 2023 11:51:22 +0300 Subject: [PATCH 3/3] Refactor spring-analyzer --- utbot-spring-analyzer/build.gradle.kts | 1 + .../PureSpringApplicationAnalyzer.kt | 14 +++---- .../SpringApplicationAnalysisContext.kt | 11 ----- .../analyzers/SpringApplicationAnalyzer.kt | 5 ++- .../SpringApplicationAnalyzerFacade.kt | 41 ++++++++----------- .../SpringBootApplicationAnalyzer.kt | 29 ++++++------- .../exception/UtBotSpringShutdownException.kt | 2 + .../UtBotBeanFactoryPostProcessor.kt | 11 ++++- ...{FakeFileManager.kt => TempFileManager.kt} | 4 +- 9 files changed, 54 insertions(+), 64 deletions(-) delete mode 100644 utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/SpringApplicationAnalysisContext.kt rename utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/utils/{FakeFileManager.kt => TempFileManager.kt} (88%) diff --git a/utbot-spring-analyzer/build.gradle.kts b/utbot-spring-analyzer/build.gradle.kts index 8c2336741c..641e064318 100644 --- a/utbot-spring-analyzer/build.gradle.kts +++ b/utbot-spring-analyzer/build.gradle.kts @@ -42,6 +42,7 @@ application { mainClass.set("org.utbot.spring.process.SpringAnalyzerProcessMainKt") } +// see more details -- https://github.com/spring-projects/spring-boot/issues/1828 tasks.shadowJar { configurations = listOf(shadowJarConfiguration) diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/PureSpringApplicationAnalyzer.kt b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/PureSpringApplicationAnalyzer.kt index 5cbff0fded..6cd5343b67 100644 --- a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/PureSpringApplicationAnalyzer.kt +++ b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/PureSpringApplicationAnalyzer.kt @@ -1,18 +1,16 @@ package org.utbot.spring.analyzers -import com.jetbrains.rd.util.getLogger -import com.jetbrains.rd.util.info import org.springframework.context.annotation.AnnotationConfigApplicationContext +import org.springframework.core.env.ConfigurableEnvironment import org.utbot.spring.exception.UtBotSpringShutdownException -private val logger = getLogger() - class PureSpringApplicationAnalyzer : SpringApplicationAnalyzer { - override fun analyze(analysisContext: SpringApplicationAnalysisContext): List { - logger.info { "Analyzing pure Spring application" } + override fun analyze(sources: Array>, environment: ConfigurableEnvironment): List { val applicationContext = AnnotationConfigApplicationContext() - applicationContext.register(*analysisContext.sources) - applicationContext.environment = analysisContext.createEnvironment() + applicationContext.register(*sources) + applicationContext.environment = environment return UtBotSpringShutdownException.catch { applicationContext.refresh() }.beanQualifiedNames } + + override fun canAnalyze() = true } \ No newline at end of file diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/SpringApplicationAnalysisContext.kt b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/SpringApplicationAnalysisContext.kt deleted file mode 100644 index 0f2d54c0d2..0000000000 --- a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/SpringApplicationAnalysisContext.kt +++ /dev/null @@ -1,11 +0,0 @@ -package org.utbot.spring.analyzers - -import org.springframework.core.env.ConfigurableEnvironment -import org.utbot.spring.utils.EnvironmentFactory - -class SpringApplicationAnalysisContext( - val sources: Array>, - private val environmentFactory: EnvironmentFactory -) { - fun createEnvironment(): ConfigurableEnvironment = environmentFactory.createEnvironment() -} \ No newline at end of file diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/SpringApplicationAnalyzer.kt b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/SpringApplicationAnalyzer.kt index cca4a522ee..0de1723a2e 100644 --- a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/SpringApplicationAnalyzer.kt +++ b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/SpringApplicationAnalyzer.kt @@ -1,5 +1,8 @@ package org.utbot.spring.analyzers +import org.springframework.core.env.ConfigurableEnvironment + interface SpringApplicationAnalyzer { - fun analyze(analysisContext: SpringApplicationAnalysisContext): List + fun analyze(sources: Array>, environment: ConfigurableEnvironment): List + fun canAnalyze(): Boolean } \ No newline at end of file diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/SpringApplicationAnalyzerFacade.kt b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/SpringApplicationAnalyzerFacade.kt index cae212987a..705278a27b 100644 --- a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/SpringApplicationAnalyzerFacade.kt +++ b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/SpringApplicationAnalyzerFacade.kt @@ -4,14 +4,10 @@ import com.jetbrains.rd.util.error import com.jetbrains.rd.util.getLogger import com.jetbrains.rd.util.info import org.springframework.boot.SpringBootVersion -import org.springframework.boot.builder.SpringApplicationBuilder -import org.springframework.context.annotation.AnnotationConfigApplicationContext import org.springframework.core.SpringVersion -import org.utbot.spring.utils.SourceFinder import org.utbot.spring.api.ApplicationData -import org.utbot.spring.exception.UtBotSpringShutdownException -import org.utbot.spring.postProcessors.UtBotBeanFactoryPostProcessor import org.utbot.spring.utils.EnvironmentFactory +import org.utbot.spring.utils.SourceFinder private val logger = getLogger() @@ -19,28 +15,23 @@ class SpringApplicationAnalyzerFacade(private val applicationData: ApplicationDa fun analyze(): List { logger.info { "Current Java version is: " + System.getProperty("java.version") } - logger.info { - "Current Spring version is: " + runCatching { SpringVersion.getVersion() }.getOrElse(logger::error) - } + logger.info { "Current Spring version is: " + runCatching { SpringVersion.getVersion() }.getOrNull() } + logger.info { "Current Spring Boot version is: " + runCatching { SpringBootVersion.getVersion() }.getOrNull() } - val analyzer = try { - this::class.java.classLoader.loadClass("org.springframework.boot.SpringApplication") - logger.info { - "Current Spring Boot version is: " + runCatching { SpringBootVersion.getVersion() }.getOrElse(logger::error) + val sources = SourceFinder(applicationData).findSources() + val environmentFactory = EnvironmentFactory(applicationData) + + for (analyzer in listOf(SpringBootApplicationAnalyzer(), PureSpringApplicationAnalyzer())) { + if (analyzer.canAnalyze()) { + logger.info { "Analyzing with $analyzer" } + try { + return analyzer.analyze(sources, environmentFactory.createEnvironment()) + } catch (e: Throwable) { + logger.error("Analyzer $analyzer failed", e) + } } - SpringBootApplicationAnalyzer() - } catch (e: ClassNotFoundException) { - logger.info { "Spring Boot is not detected" } - PureSpringApplicationAnalyzer() } - - logger.info { "Configuration file is: ${applicationData.configurationFile}" } - - return analyzer.analyze( - SpringApplicationAnalysisContext( - sources = SourceFinder(applicationData).findSources(), - environmentFactory = EnvironmentFactory(applicationData) - ) - ) + logger.error { "All Spring analyzers failed, using empty bean list" } + return emptyList() } } diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/SpringBootApplicationAnalyzer.kt b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/SpringBootApplicationAnalyzer.kt index 8cf9521494..933a3034bc 100644 --- a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/SpringBootApplicationAnalyzer.kt +++ b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/analyzers/SpringBootApplicationAnalyzer.kt @@ -1,24 +1,21 @@ package org.utbot.spring.analyzers -import com.jetbrains.rd.util.error -import com.jetbrains.rd.util.getLogger -import com.jetbrains.rd.util.info import org.springframework.boot.builder.SpringApplicationBuilder +import org.springframework.core.env.ConfigurableEnvironment import org.utbot.spring.exception.UtBotSpringShutdownException -private val logger = getLogger() - class SpringBootApplicationAnalyzer : SpringApplicationAnalyzer { - override fun analyze(analysisContext: SpringApplicationAnalysisContext): List { - logger.info { "Analyzing Spring Boot application" } - return try { - val app = SpringApplicationBuilder(*analysisContext.sources) - .environment(analysisContext.createEnvironment()) - .build() - UtBotSpringShutdownException.catch { app.run() }.beanQualifiedNames - } catch (e: Throwable) { - logger.error("Failed to analyze Spring Boot application, falling back to using pure Spring", e) - PureSpringApplicationAnalyzer().analyze(analysisContext) - } + override fun analyze(sources: Array>, environment: ConfigurableEnvironment): List { + val app = SpringApplicationBuilder(*sources) + .environment(environment) + .build() + return UtBotSpringShutdownException.catch { app.run() }.beanQualifiedNames + } + + override fun canAnalyze(): Boolean = try { + this::class.java.classLoader.loadClass("org.springframework.boot.SpringApplication") + true + } catch (e: ClassNotFoundException) { + false } } \ No newline at end of file diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/exception/UtBotSpringShutdownException.kt b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/exception/UtBotSpringShutdownException.kt index 6b3b30ce44..86bdb46a5f 100644 --- a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/exception/UtBotSpringShutdownException.kt +++ b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/exception/UtBotSpringShutdownException.kt @@ -19,6 +19,8 @@ class UtBotSpringShutdownException( block() throw IllegalStateException("UtBotSpringShutdownException has not been thrown") } catch (e: Throwable) { + // Spring sometimes wraps exceptions in other exceptions, so we go over + // all the causes to determine if UtBotSpringShutdownException was thrown for(cause in generateSequence(e) { it.cause }) if (cause is UtBotSpringShutdownException) { logger.info { "UtBotSpringShutdownException has been successfully caught" } diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/postProcessors/UtBotBeanFactoryPostProcessor.kt b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/postProcessors/UtBotBeanFactoryPostProcessor.kt index 476b43dbd5..a8a604581b 100644 --- a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/postProcessors/UtBotBeanFactoryPostProcessor.kt +++ b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/postProcessors/UtBotBeanFactoryPostProcessor.kt @@ -6,6 +6,7 @@ import com.jetbrains.rd.util.warn import org.springframework.beans.factory.BeanCreationException import org.springframework.beans.factory.config.BeanFactoryPostProcessor import org.springframework.beans.factory.config.ConfigurableListableBeanFactory +import org.springframework.beans.factory.support.BeanDefinitionRegistry import org.springframework.core.PriorityOrdered import org.utbot.spring.exception.UtBotSpringShutdownException @@ -25,6 +26,7 @@ object UtBotBeanFactoryPostProcessor : BeanFactoryPostProcessor, PriorityOrdered logger.info { "Finished post-processing bean factory in UtBot" } + destroyBeanDefinitions(beanFactory) throw UtBotSpringShutdownException("Finished post-processing bean factory in UtBot", beanQualifiedNames) } @@ -39,4 +41,11 @@ object UtBotBeanFactoryPostProcessor : BeanFactoryPostProcessor, PriorityOrdered }.filterNot { it.startsWith("org.utbot.spring") } .distinct() } -} + + private fun destroyBeanDefinitions(beanFactory: ConfigurableListableBeanFactory) { + for (beanDefinitionName in beanFactory.beanDefinitionNames) { + val beanRegistry = beanFactory as BeanDefinitionRegistry + beanRegistry.removeBeanDefinition(beanDefinitionName) + } + } +} \ No newline at end of file diff --git a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/utils/FakeFileManager.kt b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/utils/TempFileManager.kt similarity index 88% rename from utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/utils/FakeFileManager.kt rename to utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/utils/TempFileManager.kt index d1b5e66e57..14c8e96a85 100644 --- a/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/utils/FakeFileManager.kt +++ b/utbot-spring-analyzer/src/main/kotlin/org/utbot/spring/utils/TempFileManager.kt @@ -5,9 +5,9 @@ import com.jetbrains.rd.util.info import java.io.File import java.io.IOException -private val logger = getLogger() +private val logger = getLogger() -class FakeFileManager(private val fakeFilesList: List) { +class TempFileManager(private val fakeFilesList: List) { fun createTempFiles() { for (fileName in fakeFilesList) {