Почему Spring @ConstructorBinding не привязан для класса Kotlin @ConfigurationProperties? - PullRequest
0 голосов
/ 03 мая 2020

У меня есть библиотека автоконфигурации Spring, я разработчик для Swagger. Он написан на Kotlin с использованием Spring Boot 2.2.6.

Моя основная автоматическая конфигурация определена следующим образом:

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. Я хотел использовать новый @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 = ".*"
    }
}

Исходный код хранится в моем репозитории GitHub по адресу https://github.com/opengoodio/swagger-auto-configuration .

Основной проект автоматической настройки находится под 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 основном классе, но с той же ошибкой.

Я искал решения за последние несколько месяцев и не могу найти solid причину, по которой это происходит.

Что заставляет @ConstructorBinding неправильно связываться?

1 Ответ

0 голосов
/ 03 мая 2020

Полагаю, ваш аннотированный класс @SpringBootApplication находится в пакете io.opengood.autoconfig.swagger.app и сканирует компоненты в этом пакете и все подпакеты. Ваш файл конфигурации в библиотеке находится в пакете io.opengood.autoconfig.swagger, и поэтому он может не проверяться. Таким образом, у вас есть два варианта:

  1. Изменить пакет вашего @SpringBootApplication аннотированного класса
  2. Добавить scanBasePackages в @SpringBootApplication, то есть @SpringBootApplication(scanBasePackages = {"io.opengood.autoconfig.swagger", io.opengood.autoconfig.swagger.app})
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...