我有一个Spring自动配置库,我是Swagger的开发人员。它是使用Spring Boot 2.2.6
用Kotlin编写的。
我的主要自动配置定义为:
package io.opengood.autoconfig.swagger
import org.apache.commons.lang3.StringUtils
import org.slf4j.LoggerFactory.getLogger
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import springfox.documentation.builders.AuthorizationCodeGrantBuilder
import springfox.documentation.builders.OAuthBuilder
import springfox.documentation.builders.PathSelectors
import springfox.documentation.service.*
import springfox.documentation.spi.DocumentationType
import springfox.documentation.spi.service.contexts.SecurityContext
import springfox.documentation.spring.web.plugins.Docket
import springfox.documentation.swagger.web.SecurityConfiguration
import springfox.documentation.swagger.web.SecurityConfigurationBuilder
import springfox.documentation.swagger2.annotations.EnableSwagger2
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
import java.sql.Date as SqlDate
import java.sql.Time as SqlTime
import java.util.Date as UtilDate
@Configuration
@ConditionalOnProperty("swagger.enabled")
@EnableConfigurationProperties(value = [SwaggerProperties::class, OAuth2Properties::class])
@EnableSwagger2
class SwaggerAutoConfiguration(
val swaggerProperties: SwaggerProperties = SwaggerProperties(),
val swaggerVersion: SwaggerVersion = DefaultSwaggerVersion(),
val oAuth2Properties: OAuth2Properties = OAuth2Properties()
) {
val paths = swaggerProperties.paths
.takeIf { !it.isNullOrEmpty() }
.let { it?.joinToString(",") } ?: SwaggerProperties.DEFAULT_PATH
val version = swaggerVersion.version
.takeIf { it.isNotBlank() } ?: swaggerProperties.version
val authUri = oAuth2Properties.resource.authorizationServerUri
.takeIf { it.isNotBlank() } ?: OAuth2Properties.DEFAULT_AUTH_URI
val tokenUri = oAuth2Properties.tokenUri
.takeIf { it.isNotBlank() } ?: OAuth2Properties.DEFAULT_TOKEN_URI
@Bean
fun productApi(): Docket {
log.info("Setup Swagger product configuration")
val productApi = Docket(DocumentationType.SWAGGER_2)
.groupName(swaggerProperties.groupName)
.directModelSubstitute(LocalDateTime::class.java, UtilDate::class.java)
.directModelSubstitute(LocalDate::class.java, SqlDate::class.java)
.directModelSubstitute(LocalTime::class.java, SqlTime::class.java)
.apiInfo(apiInfo())
.select()
.paths(PathSelectors.regex(paths))
.build()
if (oAuth2Properties.enabled && !authUri.contains("localhost")) {
productApi.securitySchemes(listOf(securitySchemes()))
productApi.securityContexts(listOf(securityContext()))
}
return productApi
}
@Bean
fun apiInfo(): ApiInfo {
log.info("Setup Swagger API configuration")
return ApiInfo(
swaggerProperties.title,
swaggerProperties.description,
version,
swaggerProperties.termsOfServiceUrl,
Contact(
swaggerProperties.contact.name,
swaggerProperties.contact.url,
swaggerProperties.contact.email),
swaggerProperties.license.type,
swaggerProperties.license.url,
listOf())
}
@Bean
@ConditionalOnProperty("swagger.security.oauth2.enabled")
fun securityInfo(): SecurityConfiguration {
log.info("Setup Swagger security configuration")
return if (OAuth2Properties.GrantType.CLIENT_CREDENTIALS == oAuth2Properties.grantType) {
SecurityConfigurationBuilder.builder()
.clientId(StringUtils.EMPTY)
.clientSecret(StringUtils.EMPTY)
.scopeSeparator(" ")
.build()
} else {
SecurityConfigurationBuilder.builder()
.useBasicAuthenticationWithAccessCodeGrant(true)
.build()
}
}
private fun securitySchemes(): SecurityScheme {
return if (OAuth2Properties.GrantType.CLIENT_CREDENTIALS == oAuth2Properties.grantType) {
OAuthBuilder()
.name(SECURITY_REFERENCE_NAME)
.grantTypes(listOf(ClientCredentialsGrant(authUri)))
.scopes(scopes())
.build()
} else {
OAuthBuilder()
.name(SECURITY_REFERENCE_NAME)
.grantTypes(listOf(AuthorizationCodeGrantBuilder()
.tokenEndpoint(TokenEndpoint(tokenUri, TOKEN_NAME))
.tokenRequestEndpoint(TokenRequestEndpoint(authUri, "", ""))
.build()))
.scopes(scopes())
.build()
}
}
private fun securityContext(): SecurityContext {
return SecurityContext.builder()
.securityReferences(listOf(SecurityReference(SECURITY_REFERENCE_NAME, scopes().toTypedArray())))
.forPaths(PathSelectors.regex(paths))
.build()
}
private fun scopes(): List<AuthorizationScope> {
return oAuth2Properties.client.scopes
.takeIf { it.isNotEmpty() }
.let { it?.values?.map { s -> AuthorizationScope(s, "") } }
?: emptyList()
}
companion object {
const val SECURITY_REFERENCE_NAME = "spring_oauth2"
const val TOKEN_NAME = "oauth2_token"
@Suppress("JAVA_CLASS_ON_COMPANION")
@JvmStatic
private val log = getLogger(javaClass.enclosingClass)
}
}
我有几个类,请参见下面的主要类,它们使用@ConfigurationProperties
作为bean注入到上一类中。我想使用新的@ConstructorBinding
从主自动配置类中删除难看的lateint var
。
package io.opengood.autoconfig.swagger
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.ConstructorBinding
@ConfigurationProperties(prefix = "swagger")
@ConstructorBinding
data class SwaggerProperties(
val enabled: Boolean = true,
val groupName: String = "",
val paths: List<String> = listOf(DEFAULT_PATH),
val title: String = "",
val description: String = "",
val version: String = "",
val termsOfServiceUrl: String = "",
val contact: Contact = Contact(),
val license: License = License()
) {
@ConstructorBinding
data class Contact(
val name: String = "",
val url: String = "",
val email: String = ""
)
@ConstructorBinding
data class License(
val type: String = "",
val url: String = ""
)
companion object {
const val DEFAULT_PATH = ".*"
}
}
源代码存储在https://github.com/opengoodio/swagger-auto-configuration的GitHub存储库中。
主自动配置项目在lib/src/main/kotlin/io/opengood/autoconfig/swagger
下。
我有另一个项目test-app
,其项目名为test-app/src/test/kotlin/io/opengood/autoconfig/swagger/app
,测试类为AccessSwaggerTest
:
package io.opengood.autoconfig.swagger.app
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.context.junit.jupiter.SpringExtension
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
@SpringBootTest(classes = [SwaggerTestApplication::class])
@ExtendWith(SpringExtension::class)
@AutoConfigureMockMvc
class AccessSwaggerTest {
@Autowired
lateinit var mockMvc: MockMvc
@Test
fun `swagger UI endpoint is accessible`() {
mockMvc.perform(get("/swagger-ui.html"))
.andExpect(status().is2xxSuccessful)
.andReturn();
}
@Test
fun `swagger API docs endpoint is accessible`() {
mockMvc.perform(get("/v2/api-docs?group=test-group"))
.andExpect(status().is2xxSuccessful)
.andReturn();
}
}
如果运行第一个测试,它将失败并显示:
java.lang.IllegalStateException: Failed to load ApplicationContext
at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:132)
at org.springframework.test.context.support.DefaultTestContext.getApplicationContext(DefaultTestContext.java:123)
at org.springframework.test.context.web.ServletTestExecutionListener.setUpRequestContextIfNecessary(ServletTestExecutionListener.java:190)
at org.springframework.test.context.web.ServletTestExecutionListener.prepareTestInstance(ServletTestExecutionListener.java:132)
at org.springframework.test.context.TestContextManager.prepareTestInstance(TestContextManager.java:244)
at org.springframework.test.context.junit.jupiter.SpringExtension.postProcessTestInstance(SpringExtension.java:98)
at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$invokeTestInstancePostProcessors$5(ClassBasedTestDescriptor.java:337)
at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.executeAndMaskThrowable(ClassBasedTestDescriptor.java:342)
at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$invokeTestInstancePostProcessors$6(ClassBasedTestDescriptor.java:337)
at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:195)
at java.base/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:177)
at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1654)
at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:484)
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:474)
at java.base/java.util.stream.StreamSpliterators$WrappingSpliterator.forEachRemaining(StreamSpliterators.java:312)
at java.base/java.util.stream.Streams$ConcatSpliterator.forEachRemaining(Streams.java:735)
at java.base/java.util.stream.Streams$ConcatSpliterator.forEachRemaining(Streams.java:734)
at java.base/java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:658)
at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.invokeTestInstancePostProcessors(ClassBasedTestDescriptor.java:336)
at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.instantiateAndPostProcessTestInstance(ClassBasedTestDescriptor.java:259)
at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$testInstancesProvider$2(ClassBasedTestDescriptor.java:252)
at java.base/java.util.Optional.orElseGet(Optional.java:369)
at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$testInstancesProvider$3(ClassBasedTestDescriptor.java:251)
at org.junit.jupiter.engine.execution.TestInstancesProvider.getTestInstances(TestInstancesProvider.java:29)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$prepare$0(TestMethodTestDescriptor.java:106)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.prepare(TestMethodTestDescriptor.java:105)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.prepare(TestMethodTestDescriptor.java:69)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$prepare$1(NodeTestTask.java:107)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.prepare(NodeTestTask.java:107)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:75)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1540)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:139)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1540)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:139)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:32)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:51)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:229)
at org.junit.platform.launcher.core.DefaultLauncher.lambda$execute$6(DefaultLauncher.java:197)
at org.junit.platform.launcher.core.DefaultLauncher.withInterceptedStreams(DefaultLauncher.java:211)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:191)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:128)
at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:69)
at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230)
at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'io.opengood.autoconfig.swagger.OAuth2Properties': @EnableConfigurationProperties or @ConfigurationPropertiesScan must be used to add @ConstructorBinding type io.opengood.autoconfig.swagger.OAuth2Properties
at org.springframework.boot.context.properties.ConfigurationPropertiesBeanDefinitionValidator.validate(ConfigurationPropertiesBeanDefinitionValidator.java:66)
at org.springframework.boot.context.properties.ConfigurationPropertiesBeanDefinitionValidator.postProcessBeanFactory(ConfigurationPropertiesBeanDefinitionValidator.java:45)
at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:286)
at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:174)
at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:706)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:532)
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:747)
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:315)
at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:126)
at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:99)
at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:124)
... 63 more
如果我启动运行简单应用程序,它将失败并显示类似错误。
主自动配置具有@EnableConfigurationProperties(value = [SwaggerProperties::class, OAuth2Properties::class])
,错误提示,但仍然失败。我在@ConfigurationPropertiesScan
主类上尝试过test-app
,但有同样的错误。
在过去的几个月中,我一直在寻找解决方案,但找不到确切的原因。
是什么导致@ConstructorBinding
无法正确绑定?
答案 0 :(得分:0)
我的猜测是:您的@SpringBootApplication
带注释的类在程序包io.opengood.autoconfig.swagger.app
中,它将扫描该程序包中的所有子程序包。 lib中的配置文件位于软件包io.opengood.autoconfig.swagger
中,因此可能不会被扫描。因此,您有两种选择:
@SpringBootApplication
带注释类的程序包scanBasePackages
添加到@SpringBootApplication
,即@SpringBootApplication(scanBasePackages = {"io.opengood.autoconfig.swagger", io.opengood.autoconfig.swagger.app})