Initialisation of Spring ConfigurationProperties

ぐ巨炮叔叔 提交于 2020-02-06 07:30:07

问题


I have the following class mapping parts of the properties from the application.properties:

@Component
@ConfigurationProperties(prefix = "city")
@Getter
@Setter
public class CityProperties {
    private int populationAmountWorkshop;
    private double productionInefficientFactor;
    private Loaner loaner = new Loaner();
    private Tax tax = new Tax();
    private Guard pikeman = new Guard();
    private Guard bowman = new Guard();
    private Guard crossbowman = new Guard();
    private Guard musketeer = new Guard();

    @Getter
    @Setter
    public static class Loaner {
        private int maxRequest;
        private int maxAgeRequest;
        private int maxNbLoans;
    }

    @Getter
    @Setter
    public static class Tax {
        private double poor;
        private double middle;
        private double rich;
        private int baseHeadTax;
        private int basePropertyTax;
    }

    @Getter
    @Setter
    public static class Guard {
        private int weeklySalary;
    }
}

A portion of application.properties:

#City
# Amount of inhabitants to warrant the city to have one workshop
city.populationAmountWorkshop=2500
# Factor that is applied on the efficient production to get the inefficient production
city.productionInefficientFactor=0.6
# Maximum requests per loaner
city.loaner.maxRequest=6
# Maximum  age of loan request in weeks
city.loaner.maxAgeRequest=4
# Maximum loan offers per loaner
city.loaner.maxNbLoans=3
# Weekly tax value factor for the various population per 100 citizens
city.tax.poor=0
city.tax.middle=0.6
city.tax.rich=2.0
city.tax.baseHeadTax=4
city.tax.basePropertyTax=280
city.pikeman.weeklySalary=3
city.bowman.weeklySalary=3
city.crossbowman.weeklySalary=4
city.musketeer.weeklySalary=6

Then this is the application for the test setup:

@SpringBootApplication
@Import({ServerTestConfiguration.class})
@ActiveProfiles("server")
@EnableConfigurationProperties
@PropertySource(value = {"application.properties", "server.properties", "bean-test.properties"})
public class SavegameTestApplication {
}

These are annotations on the ServerTestConfiguration class all other imported confugrations are the same as I use in the production case as well:

@Configuration
@EnableAutoConfiguration
@Import(value = {ClientServerInterfaceServerConfiguration.class, ServerConfiguration.class, ImageConfiguration.class})
public class ServerTestConfiguration {
  ...
}

And finally the constructor of my test class that initializes the Spring-Boot application:

public CityWallSerializationTest() {
    SpringApplicationBuilder builder = new SpringApplicationBuilder(SavegameTestApplication.class);
    DependentAnnotationConfigApplicationContext context = (DependentAnnotationConfigApplicationContext)
            builder.contextClass(DependentAnnotationConfigApplicationContext.class).profiles("server").run();
    setContext(context);
    setClientServerEventBus((AsyncEventBus) context.getBean("clientServerEventBus"));
    IConverterProvider converterProvider = context.getBean(IConverterProvider.class);
    BuildProperties buildProperties = context.getBean(BuildProperties.class);
    Archiver archiver = context.getBean(Archiver.class);
    IDatabaseDumpAndRestore databaseService = context.getBean(IDatabaseDumpAndRestore.class);
    TestableLoadAndSaveService loadAndSaveService = new TestableLoadAndSaveService(context, converterProvider,
            buildProperties, archiver, databaseService);
    setLoadAndSaveService(loadAndSaveService);
}

This works fine in my production code, however when I want to write some tests using a spring boot application the values are not initialized.

Printing out the CityProperties at the end of the constructor results in this output:

CityProperties(populationAmountWorkshop=0, productionInefficientFactor=0.0, loaner=CityProperties.Loaner(maxRequest=0, maxAgeRequest=0, maxNbLoans=0), tax=CityProperties.Tax(poor=0.0, middle=0.0, rich=0.0, baseHeadTax=0, basePropertyTax=0), pikeman=CityProperties.Guard(weeklySalary=0), bowman=CityProperties.Guard(weeklySalary=0), crossbowman=CityProperties.Guard(weeklySalary=0), musketeer=CityProperties.Guard(weeklySalary=0))

I would like to understand how Spring handles the initialization of these ConfigurationProperties annotated classes, how the magic happens so to speak. I want to know this in order to properly debug the application to figure out where it goes wrong.

The productive code is a JavaFX application, that makes the whole initialization a bit more complicated:

@Slf4j
@SpringBootApplication
@Import(StandaloneConfiguration.class)
@PropertySource(value = {"application.properties", "server.properties"})
public class OpenPatricianApplication extends Application implements IOpenPatricianApplicationWindow {

    private StartupService startupService;
    private GamePropertyUtility gamePropertyUtility;

    private int width;
    private int height;
    private boolean fullscreen;
    private Stage primaryStage;

    private final AggregateEventHandler<KeyEvent> keyEventHandlerAggregate;
    private final MouseClickLocationEventHandler mouseClickEventHandler;

    private ApplicationContext context;

    public OpenPatricianApplication() {
        width = MIN_WIDTH;
        height = MIN_HEIGHT;
        this.fullscreen = false;
        keyEventHandlerAggregate = new AggregateEventHandler<>();

        CloseApplicationEventHandler closeEventHandler = new CloseApplicationEventHandler();
        mouseClickEventHandler = new MouseClickLocationEventHandler();
        EventHandler<KeyEvent> fullScreenEventHandler = event -> {
            try {
                if (event.getCode().equals(KeyCode.F) && event.isControlDown()) {
                    updateFullscreenMode();
                }
            } catch (RuntimeException e) {
                log.error("Failed to switch to/from fullscreen mode", e);
            }
        };
        EventHandler<KeyEvent> closeEventWindowKeyHandler = event -> {
            if (event.getCode().equals(KeyCode.ESCAPE)) {
                log.info("Pressed ESC");
                context.getBean(MainGameView.class).closeEventView();
            }
        };
        addKeyEventHandler(closeEventHandler);
        addKeyEventHandler(fullScreenEventHandler);
        addKeyEventHandler(closeEventWindowKeyHandler);
    }

    /**
     * Add a key event handler to the application.
     * @param eventHandler to be added.
     */
    private void addKeyEventHandler(EventHandler<KeyEvent> eventHandler) {
        keyEventHandlerAggregate.addEventHandler(eventHandler);
    }

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void init() {
        SpringApplicationBuilder builder = new SpringApplicationBuilder(OpenPatricianApplication.class);
        context = builder.contextClass(DependentAnnotationConfigApplicationContext.class).profiles("standalone")
                .run(getParameters().getRaw().toArray(new String[0]));
        this.startupService = context.getBean(StartupService.class);
        this.gamePropertyUtility = context.getBean(GamePropertyUtility.class);
        if (startupService.checkVersion()) {
            startupService.logEnvironment();

            CommandLineArguments cmdHelper = new CommandLineArguments();
            Options opts = cmdHelper.createCommandLineOptions();
            CommandLine cmdLine = cmdHelper.parseCommandLine(opts, getParameters().getRaw().toArray(new String[getParameters().getRaw().size()]));
            if (cmdLine.hasOption(CommandLineArguments.HELP_OPTION)){
                cmdHelper.printHelp(opts);
                System.exit(0);
            }
            if (cmdLine.hasOption(CommandLineArguments.VERSION_OPTION)) {
                System.out.println("OpenPatrician version: "+OpenPatricianApplication.class.getPackage().getImplementationVersion());
                System.exit(0);
            }
            cmdHelper.persistAsPropertyFile(cmdLine);
        }
    }

    @Override
    public void start(Stage primaryStage) {
        this.primaryStage = primaryStage;
        this.primaryStage.setMinWidth(MIN_WIDTH);
        this.primaryStage.setMinHeight(MIN_HEIGHT);
        primaryStage.getIcons().add(new Image(getClass().getResourceAsStream("/icons/trade-icon.png")));
        UIFactory uiFactory = context.getBean(UIFactory.class);
        uiFactory.setApplicationWindow(this);
        BaseStartupScene startupS = uiFactory.getStartupScene();
        Scene defaultScene = new Scene(startupS.getRoot(), width, height);
        defaultScene.getStylesheets().add("/styles/font.css");

        this.fullscreen = Boolean.valueOf((String) gamePropertyUtility.getProperties().get("window.fullscreen"));
        startupS.setSceneChangeable(this);
        defaultScene.setOnMousePressed(mouseClickEventHandler);
        defaultScene.setOnKeyPressed(keyEventHandlerAggregate);
        try {
            CheatKeyEventListener cheatListener = context.getBean(CheatKeyEventListener.class);
            if (cheatListener != null) {
                addKeyEventHandler(cheatListener);
            }
        } catch (Exception e) {
            // the cheat listener is no defined for the context.
            e.printStackTrace();
        }

        setCursor(defaultScene);

        primaryStage.setFullScreen(fullscreen);
        primaryStage.setFullScreenExitHint("");
        primaryStage.setTitle("OpenPatrician");
        primaryStage.setScene(defaultScene);
        primaryStage.show();
    }

    private void setCursor(Scene scene) {
        URL url = getClass().getResource("/icons/64/cursor.png");
        try {
            Image img = new Image(url.openStream());
            scene.setCursor(new ImageCursor(img));
        } catch (IOException e) {
            log.warn("Failed to load cursor icon from {}", url);
        }
    }

    /**
     * @see SceneChangeable#changeScene(OpenPatricianScene)
     */
    @Override
    public void changeScene(final OpenPatricianScene scene) {
        primaryStage.getScene().setOnMousePressed(mouseClickEventHandler);
        primaryStage.getScene().setOnKeyPressed(keyEventHandlerAggregate);

        primaryStage.getScene().setRoot(scene.getRoot());
    }
    /**
     * Toggle between full screen and non full screen mode.
     */
    public void updateFullscreenMode() {
        fullscreen = !fullscreen;
        primaryStage.setFullScreen(fullscreen);
    }

    @Override
    public double getSceneWidth() {
        return primaryStage.getScene().getWidth();
    }

    @Override
    public double getSceneHeight() {
        return primaryStage.getScene().getHeight();
    }

    @Override
    public void stop() throws Exception {
        System.out.println("Stopping the UI Application");

        stopUIApplicationContext();
        super.stop();
    }

    /**
     * Closing the application context for the user interface.
     */
    private void stopUIApplicationContext() {
        AsyncEventBus eventBus = (AsyncEventBus) context.getBean("clientServerEventBus");
        eventBus.post(new GameStateChange(EGameStatusChange.SHUTDOWN));
        ((AbstractApplicationContext)context).close();
    }
}

回答1:


It seems like you are mismatching test code and production code. Let me explain:

  • @SpringBootApplication is intented to run your class with the main. Usually, this looks like:

    @SpringBootApplication
    public class MyApplication {
        public static void main(String[] args] {
            SpringApplication.run(MyApplication .class, args);
        }
    }

  • If you want to test an application, then your test class has to be annotated with @SpringBootTest. This annotation will automatically detect a class annotated by @SpringBootConfiguration (which is included by @SpringBootApplication).
  • Within your @SpringBootTest class, you can also use @Import to load other configuration classes.
  • @ActiveProfiles can only be used in a test context... You are not using any profile when running your "test" class, as this annotation is test purpose only. It will be if you switch to @SpringBootTest.
  • Still in a Test context, if you want to load properties files (other than application.properties), you have to use @TestPropertySource instead of @PropertySource.

Your "Main" test class is supposed to look something like that:


    @SpringBootTest
    @ActiveProfiles("server")
    @Import({ServerTestConfiguration.class})
    @TestPropertySource(locations = {"classpath:/server.properties, "classpath:/bean-test.properties"}})
    public class SavegameTestApplication {
    }

Fore more information on Spring Boot Application Test, check this: https://www.baeldung.com/spring-boot-testing#integration-testing-with-springboottest

Once you are done with test class, you should be removing your CityWallSerializationTest which is basically a re-implementation of @SpringBootTest. Please note that you can use @Autowired in your test classes as well.




回答2:


The class that handles the binding of ConfigurationProperties is ConfigurationPropertiesBindingPostProcessor.

In this particular case it turned out that the only properties file loaded was the application.properties that was present on the class path from the test project instead of the application.properties that actually contains the proper key value pairs.

This can be seen at startup when running the application with -Dlogging.level.org.springframework=DEBUG and then look in the output for:

2019-12-31 10:54:49,884 [main] DEBUG  o.s.b.SpringApplication : Loading source class ch.sahits.game.savegame.SavegameTestApplication
2019-12-31 10:54:49,908 [main] DEBUG  o.s.b.c.c.ConfigFileApplicationListener : Loaded config file 'file:<path>/OpenPatrician/OpenPatricianModel/target/classes/application.properties' (classpath:/application.properties)
2

The second line specifies the location of the application.properties that is loaded.



来源:https://stackoverflow.com/questions/59526999/initialisation-of-spring-configurationproperties

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!