Ahatius
Ahatius

Reputation: 4875

Spring JPA: External database configuration from property file does not work

I want to use an external properties file to load database and REST endpoint information. I'm trying to avoid the XML configuration and favor the annotation based configuration.

I have created 2 classes both of which are annotated with @Configuration and use the @Value annotation in their constructors to load the properties:

RestConfiguration.java

@Configuration
public class RestConfiguration {
  private final String grantType;
  private final AuthenticationScheme authenticationScheme;
  private final String clientId;
  private final String clientSecret;
  private final String accessTokenUri;

  private final boolean useProxy;
  private final String proxyHost;
  private final int proxyPort;

  @Autowired
  public RestConfiguration(
      @Value("${api.oauth2.grant-type}") String grantType,
      @Value("${api.oauth2.authentication-scheme}") AuthenticationScheme authenticationScheme,
      @Value("${api.oauth2.client-id}") String clientId,
      @Value("${api.oauth2.client-secret}") String clientSecret,
      @Value("${api.oauth2.url}") String accessTokenUri,
      @Value("${net.proxy}") boolean useProxy,
      @Value("${net.proxy.host}") String proxyHost,
      @Value("${net.proxy.port}") int proxyPort) {
    this.grantType = grantType;
    this.authenticationScheme = authenticationScheme;
    this.clientId = clientId;
    this.clientSecret = clientSecret;
    this.accessTokenUri = accessTokenUri;
    this.useProxy = useProxy;
    this.proxyHost = proxyHost;
    this.proxyPort = proxyPort;
  } 
}

PersistenceConfiguration.java

@Configuration
public class PersistenceConfiguration {
  private final String host;
  private final String port;
  private final String database;
  private final String schema;
  private final String user;
  private final String password;

  @Autowired
  public PersistenceConfiguration(
      @Value("${db.host}") String host,
      @Value("${db.port}") String port,
      @Value("${db.database}") String database,
      @Value("${db.schema}") String schema,
      @Value("${db.user}") String user,
      @Value("${db.password}") String password) {
    this.host = host;
    this.port = port;
    this.database = database;
    this.schema = schema;
    this.user = user;
    this.password = password;
  }

  @Bean
  public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) {
    LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
    em.setDataSource(dataSource);
    em.setPackagesToScan("ch.example.rest.entities");

    JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
    em.setJpaVendorAdapter(vendorAdapter);
    em.setJpaProperties(additionalProperties());

    return em;
  }

  @Bean
  public DataSource dataSource() {
    DriverManagerDataSource dataSource = new DriverManagerDataSource();
    dataSource.setDriverClassName("org.postgresql.Driver");
    dataSource.setUrl("jdbc:postgresql://" + host + ":" + port + "/" + database);
    dataSource.setUsername(user);
    dataSource.setPassword(password);
    dataSource.setSchema(schema);
    return dataSource;
  }

  @Bean
  public PlatformTransactionManager transactionManager(EntityManagerFactory emf) {
    JpaTransactionManager transactionManager = new JpaTransactionManager();
    transactionManager.setEntityManagerFactory(emf);

    return transactionManager;
  }

  @Bean
  public PersistenceExceptionTranslationPostProcessor exceptionTranslation() {
    return new PersistenceExceptionTranslationPostProcessor();
  }

  Properties additionalProperties() {
    Properties properties = new Properties();
    properties.setProperty("hibernate.hbm2ddl.auto", "none");
    properties.setProperty("hibernate.dialect", "org.hibernate.dialect.PostgreSQL95Dialect");
    properties.setProperty(
        "hibernate.physical_naming_strategy",
        "ch.example.rest.configurations.SnakeCaseNamingStrategy");
    properties.setProperty(
        "spring.datasource.hikari.data-source-properties", "stringtype=unspecified");

    return properties;
  }
}

Both configuration files reside in the same package (sub-package configurations).

The class that initializes the Spring context looks like this:

@Configuration
@ComponentScan(basePackages = "ch.example.rest")
@EnableJpaRepositories("ch.example.rest.repositories")
@PropertySource("classpath:application.properties")
public class RestClient {
    private CommandLineController commandLineController;

    @Autowired
    public RestClient(CommandLineController commandLineController) {
        this.commandLineController = commandLineController;
    }

    private static void main(String[] args) {
        // ... some parsing of command line arguments

        // Initialize context
        ApplicationContext ctx = new AnnotationConfigApplicationContext(RestClient.class);
        RestClient restClient = ctx.getBean(RestClient.class, uploadCommand);
        restClient.runCommand(parsedCommand, uploadCommand);
    }

    public void runCommand(String command, UploadBillsCommand uploadCommand) {
        // Some calls to a controller
        commandLineController....;
    }
}

Interestingly the RestConfiguration class receives the properties, but the PersistenceConfiguration does not. During debugging I noticed that the PersistenceConfiguration class is constructed almost immediately, whereas the RestConfiguration is loaded some time later, when the first call to the RestTemplate is made.

I suspect that this might have something to do with the fact that Spring JPA tries to wire up the repositories and therefore requires the SQL connection to be made at startup.

I found this question that seems to suggest that it is not possible to supply the database configuration externally without additional boilerplate code. As that question is already 5 years old, I was wondering if maybe there is another elegant solution to fix this problem without having to create a second context.

Upvotes: 0

Views: 1143

Answers (1)

Ahatius
Ahatius

Reputation: 4875

Alright, so the answer seems pretty simple. Instead of letting Spring load the properties using the expression language with the @Value annotation, I just had to inject an instance of Environment and then get the properties directly from it:

@Configuration
public class PersistenceConfiguration {
  private String host;
  private String port;
  private String database;
  private String schema;
  private String user;
  private String password;

  @Autowired
  public PersistenceConfiguration(Environment environment) {
    this.host = environment.getProperty("db.host");
    this.port = environment.getProperty("db.port");
    this.database = environment.getProperty("db.database");
    this.schema = environment.getProperty("db.schema");
    this.user = environment.getProperty("db.user");
    this.password = environment.getProperty("db.password");
  }
}

Upvotes: 1

Related Questions