【问题标题】:Initialisation of Spring ConfigurationPropertiesSpring ConfigurationProperties 的初始化
【发布时间】:2019-12-30 07:09:55
【问题描述】:

我有来自 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;
    }
}

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

那么这是测试设置的应用程序:

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

这些是ServerTestConfiguration 类上的注释,所有其他导入的配置也与我在生产案例中使用的相同:

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

最后是初始化 Spring-Boot 应用程序的测试类的构造函数:

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);
}

这在我的生产代码中运行良好,但是当我想使用 Spring Boot 应用程序编写一些测试时,值没有被初始化。

在构造函数末尾打印出CityProperties 会产生以下输出:

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))

我想了解Spring 是如何处理这些ConfigurationProperties 注释类的初始化的,可以这么说,魔法是如何发生的。我想知道这一点,以便正确调试应用程序以找出问题所在。

生产代码是一个 JavaFX 应用程序,这使得整个初始化有点复杂:

@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();
    }
}

【问题讨论】:

  • Spring 使用您在 @configurationProperties 中给出的前缀选择类路径上文件中的属性,例如city.populationAmountWorkshop=value, city.loaner.maxRequest=value
  • 你能显示测试代码吗?还有示例 application.properties ?
  • 感谢OpenPatricianApplication。在您的测试中,如何调用 CityWallSerializationTest ?与生产文件相比,您的测试文件位于何处? server.properties 和 bean-test.properties 位于哪里?
  • 你想从你的CityWallSerializationTest开始什么样的子上下文?
  • @RUAROThibault 该项目设置为Maven 项目,这意味着测试在相应包中的src/test/java 下。 bean-test.properties 位于 src/test/resources 下,而 application.properties 和 server.properties 位于不同模块的 src/main/resources 中,它们是通过依赖项定义的。我不太清楚你所说的“子上下文”是什么意思?

标签: java spring-boot properties initialization


【解决方案1】:

您的测试代码和生产代码似乎不匹配。让我解释一下:

  • @SpringBootApplication 旨在使用 main 运行您的课程。通常,这看起来像:

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

  • 如果你想测试一个应用程序,那么你的测试类必须用@SpringBootTest注解。此注解将自动检测由@SpringBootConfiguration(包含在@SpringBootApplication 中)注解的类。
  • 在您的@SpringBootTest 类中,您还可以使用@Import 加载其他配置类。
  • @ActiveProfiles 只能在测试上下文中使用...运行“测试”类时,您没有使用任何配置文件,因为此注释仅用于测试目的。如果您切换到@SpringBootTest,它将是。
  • 仍然在测试上下文中,如果要加载属性文件(application.properties 除外),则必须使用 @TestPropertySource 而不是 @PropertySource

你的“主要”测试类应该是这样的:


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

有关 Spring Boot 应用程序测试的更多信息,请查看:https://www.baeldung.com/spring-boot-testing#integration-testing-with-springboottest

一旦你完成了测试类,你应该删除你的CityWallSerializationTest,这基本上是@SpringBootTest的重新实现。请注意,您也可以在测试类中使用@Autowired

【讨论】:

  • 这样做的原因在stackoverflow.com/questions/59123714/… 中有解释:我需要自定义实现ApplicationContext,因此我在测试类的构造函数中构造上下文以及活动配置文件的设置。
  • 好吧,我不知道。实例化CityWallSerializationTest 的主要在哪里?
  • 问题是,测试并没有真正启动 Spring Boot 应用程序,它只是启动 ApplicationContext,然后测试使用那里的 bean。所有这些都发生在测试类的构造函数中,测试本身只触发某些操作,这会导致创建额外的 bean(Lazy 和范围 prototype
  • 能否分享您的Main,启动生产代码的那个?
【解决方案2】:

处理ConfigurationProperties绑定的类是ConfigurationPropertiesBindingPostProcessor

在这种特殊情况下,加载的唯一属性文件是来自测试项目的类路径中存在的 application.properties,而不是实际包含正确键值对的 application.properties。

这可以在使用-Dlogging.level.org.springframework=DEBUG 运行应用程序时在启动时看到,然后在输出中查找:

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

第二行指定加载的 application.properties 的位置。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2015-12-26
    • 2011-05-22
    • 1970-01-01
    • 2010-09-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多