简体   繁体   中英

SpringBootServletInitializer and @ConfigurationProperties not working with root properties on .war deployments

I'm using Spring Boot 2.0.0.RC1 to build my REST service. In order to provide jar execution and .war deployment I extend SpringBootServletInitializer like this:

@Configuration
@SpringBootApplication
@EnableWebFlux
@EnableConfigurationProperties({ RbsConfiguration.class, 
JwtConfiguration.class })
public class RbsApplication extends SpringBootServletInitializer 
implements WebFluxConfigurer {

    public static void main(String[] args) throws Exception {
        SpringApplication.run(RbsApplication.class, args);
    }

    ...
}

I'm also using @ConfigurationProperties to bind my application.yml config to a bean like this:

@ConfigurationProperties
@Validated
public class RbsConfiguration {

    private Map<String, String> users;

    @NotEmpty
    public Map<String, String> getUsers() {
        return users;
    }

    public void setUsers(Map<String, String> users) {
        this.users = users;
    }
}

Using this application.yml :

users:
  user1:
    password: secret

When I start the application using java -jar everything works as expected and I can access the users via RbsConfiguration . But If I deploy it as a .war to a Tomcat, I get this exception:

***************************
APPLICATION FAILED TO START
***************************

Description:

Failed to bind properties under '' to foo.bar.RbsConfiguration:

    Reason: PropertyName must not be empty

Action:

Update your application's configuration

...

Caused by: org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under '' to foo.bar.RbsConfiguration
    at org.springframework.boot.context.properties.bind.Binder.handleBindError(Binder.java:227)
    at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:203)
    at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:187)
    at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:169)
    at org.springframework.boot.context.properties.ConfigurationPropertiesBinder.bind(ConfigurationPropertiesBinder.java:79)
    at org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor.postProcessBeforeInitialization(ConfigurationPropertiesBindingPostProcessor.java:167)
    ... 100 more
Caused by: java.lang.IllegalArgumentException: PropertyName must not be empty
    at org.springframework.util.Assert.hasLength(Assert.java:233)
    at org.springframework.boot.origin.PropertySourceOrigin.<init>(PropertySourceOrigin.java:41)
    at org.springframework.boot.origin.PropertySourceOrigin.get(PropertySourceOrigin.java:79)
    at org.springframework.boot.context.properties.source.SpringConfigurationPropertySource.find(SpringConfigurationPropertySource.java:121)
    at org.springframework.boot.context.properties.source.SpringConfigurationPropertySource.find(SpringConfigurationPropertySource.java:104)
    at org.springframework.boot.context.properties.source.SpringConfigurationPropertySource.getConfigurationProperty(SpringConfigurationPropertySource.java:86)
    at org.springframework.boot.context.properties.bind.Binder.lambda$findProperty$3(Binder.java:294)
    at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)
    at java.util.Spliterators$IteratorSpliterator.tryAdvance(Spliterators.java:1812)
    at java.util.stream.ReferencePipeline.forEachWithCancel(ReferencePipeline.java:126)
    at java.util.stream.AbstractPipeline.copyIntoWithCancel(AbstractPipeline.java:498)
    at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:485)
    at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)
    at java.util.stream.FindOps$FindOp.evaluateSequential(FindOps.java:152)
    at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
    at java.util.stream.ReferencePipeline.findFirst(ReferencePipeline.java:464)
    at org.springframework.boot.context.properties.bind.Binder.findProperty(Binder.java:295)
    at org.springframework.boot.context.properties.bind.Binder.bindObject(Binder.java:239)
    at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:198)
    ... 104 more

So I'm wondering what's the difference here. It seems when started as a .war, it requires a prefix, when started via Spring Boot directly, it is ok leaving it unprefixed. In addition to the RbsConfiguration I have further config classes (eg JwtConfiguration ) that use a prefix and seem to work fine with .war deployment.

Any hint why I'm seeing this behavior?

Extending SpringBootServletInitializer didn't work out for me in the end. It didn't bind root properties properly (I think it didn't even load application.yml ) and it ignored my Spring Security setup somehow and came up with its own default setup.

What worked for me was dropping the extends in RbsApplication and simply provide a Webflux initializer by myself, setting up the SpringBoot application manually - heavily inspired by SpringBootServletInitializer .

public class WebfluxInitializer extends AbstractReactiveWebInitializer {

    private ServletContext servletContext;

    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        this.servletContext = servletContext;
        super.onStartup(servletContext);
    }

    @Override
    protected ApplicationContext createApplicationContext() {

        SpringApplicationBuilder builder = new SpringApplicationBuilder();

        StandardServletEnvironment environment = new StandardServletEnvironment();
        environment.initPropertySources(servletContext, null);

        builder.environment(environment);
        builder.sources(getConfigClasses());
        builder.web(WebApplicationType.NONE);
        builder.main(RbsApplication.class);

        return builder.build().run();
    }

    @Override
    protected Class<?>[] getConfigClasses() {
        return new Class[] { RbsApplication.class };
    }
}

With this Webflux and Security works as defined AND binding root properties to RbsConfiguration is also working in .war deployments.

I hope this helps anybody having similar problems (providing a .war hybrid and trying to get root properies correctly bound).

If someone found an easier way to get this done, I would appreciate any hint!

You need to provide a prefix in your @ConfigurationProperties definition. Otherwise, how would you expect things to work? Everything should be mapped to your configuration class, including spring.* namespace?

I guess the difference you might see between those deployments might have to do with values present in the environment when you start the application.

You can load properties from application.yaml when use WAR mode:

@Bean
public PropertySourcesPlaceholderConfigurer properties() {
    PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer = new PropertySourcesPlaceholderConfigurer();
    propertySourcesPlaceholderConfigurer.setIgnoreResourceNotFound(true);
    YamlPropertiesFactoryBean yaml = new YamlPropertiesFactoryBean();
    List<Resource> resources = new ArrayList<>();
    resources.add(new ClassPathResource("application.yaml"));
    yaml.setResources(resources.toArray(new Resource[0]));
    propertySourcesPlaceholderConfigurer.setProperties(yaml.getObject());
    return propertySourcesPlaceholderConfigurer;
}

WAR mode is not supported by SpringBoot for WebFlux stack out-of-the-box. Using aforementioned approach you get and ability to use @EnableConfigurationProperties annotation. But it is still a hack.
The point is that this bean created too late and therefore many well documented features spring boot features are not working. For instance property encryption (jasypt) or conditional bean loading etc.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM