Spring Boot: Multiple WAR deployment in same tomcat different properties

2.9k views Asked by At

So I have to deploy the same springboot app as multiple apps in the same tomcat server. eg /app1 /app2 /app3.

They share most of the same configuration except for datasource configuration.

I've been searching for a way to externalise the datasource configuration based on the servlet-context or something like that.

Using springs externalised configuration, I am able to get it to load the same external data source file for all apps, but they need to be different. eg.

@PropertySource(value = "file:${HOME}/datasource-override.properties", ignoreResourceNotFound=false)

Using the embedded tomcat mode, ie via say .\gradlew bootRun I think I can achieve it. I just need to use the following as the application.properties for that profile sets the server.context-path property. (as this is a single app) eg.

@PropertySource(value = "file:${HOME}/${server.context-path}/datasource-override.properties", ignoreResourceNotFound=false),

Searching around, I thought it might be something like (and combinations of) the following, but it didn't work. eg

@PropertySource(value = "file:${HOME}/${server.servlet.context-path}/datasource-override.properties", ignoreResourceNotFound=false)

All examples I've found so far deal with either the embedded tomcat, or a single externalised property file for a single app.

Ideally I would like it to find the file in either it's own directory

file:${HOME}/${server.servlet.context-path}/datasource.properties

So for the three apps it would be something like the following, where it detects from it's deployed context, what the location for it's property file is. eg:

file:${HOME}/app1/datasource.properties
file:${HOME}/app2/datasource.properties
file:${HOME}/app3/datasource.properties

Obviously if the app was deployed as /funky_chicken then it would have a matching funky_chicken/datasource.properties

Any thoughts ? I know I am probably close, and I've tried dumping all the environmental properties. (you are probably are going to tell me to get it from JNDI as it's the only one I haven't dumped looking for the context)

Now I know ${HOME} is not the best place to store config items, it really is just to make it easier to describe the issue.

Update:

Thanks to the suggestions to use profiles, is there a way to have three active profiles in the one tomcat server, /app1, /app2 and /app3 from the same WAR file?

4

There are 4 answers

1
Oleh Kurpiak On

You can solve problem with spring profiles and there is no need to use @PropertySource

for application 1 just activate profiles: spring.profiles.active=app1 - this assume that in classpath you have application-app1.properties file. Same for app2, app3..appN. And file application.properies will contains common properties for all of services

0
jacky-neo On

Why you want to deploy in tomcat? Springboot app can work lonely. Hope below steps helpful to you.

  1. add application.yml(application.properties is ok too) in /resources. In this file, you configure common setting here.
  2. Then you add files named from application-app1.yml to application-app3.yml in /resources too. In these files, you configure different db setting.
  3. launch your app: for example, I suppose app1 using port 10000, app2 using port 10001...

after maven well,

app1: java -jar target/[***SNAPSHOT].jar --server.port=10000 --spring.profiles.active=app1

app2: java -jar target/[***SNAPSHOT].jar --server.port=10001 --spring.profiles.active=app2

app3: java -jar target/[***SNAPSHOT].jar --server.port=10002 --spring.profiles.active=app3

0
pshinde31 On

You can try the RoutingDataSource approach, This lets you switch the datasource at runtime/realtime.

To implement this you have to pass some datasource identifier initially (You can set it in your auth token for rest based requests or in a session)

eg -

  • localhost:80/yourcontext/{app1}
  • localhost:80/yourcontext/{app2}
  • localhost:80/yourcontext/app3

Here app1, app2, app3 will be your datasource identifiers

App controller

@Controller
public class AppController extends SuperController{
@GetMapping("/{client}")
    public String login(ModelMap map, @PathVariable("client") String client, HttpServletRequest request){
//Logic to set the path variable 
return "loginPage";
}

}

Routing datasource Config

@Configuration
    @EnableTransactionManagement
    @ComponentScan({ "com.components_package" })
    @PropertySource(value = { "classpath:database.properties" })
        public class RoutingDataSource extends AbstractRoutingDataSource {
            @Override
            protected Object determineCurrentLookupKey() {
                String tenant = DbContextHolder.getDbType();
                LoginDetails LoginDetails = currentUser();
                if(LoginDetails != null){
                    tenant = LoginDetails.getTenantId();
                }
                logger.debug("tenant >>>> "+tenant);
                return tenant;
            }
            
            private LoginDetails currentUser() {
                final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
                if (authentication instanceof UserAuthentication) {
                    return ((UserAuthentication) authentication).getDetails();
                }
                //If not authenticated return anonymous user
                return null; 
            }
        }

Hibernate config

    @PropertySource(value = { "classpath:database.properties" })
    public class HibernateConfiguration {
        @Autowired
        private Environment environment;
        
        @Bean
        @Primary
        public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
              LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
              em.setDataSource(dynamicDataSource());
              em.setPackagesToScan("com.entities_package");
              JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
              em.setJpaVendorAdapter(vendorAdapter);
              em.setJpaProperties(hibernateProperties());
              return em;
        }
        
        /** RoutingDataSource datasource initialized enabling the application
         *  to switch multiple databases at runtime
         * @return
         */
        private RoutingDataSource dynamicDataSource(){
            RoutingDataSource routingDataSource = new RoutingDataSource();
            /*Set default datasource*/
            routingDataSource.setDefaultTargetDataSource(defaultDataSource());
            /*Set all datasource map*/
            routingDataSource.setTargetDataSources(fetchDatasourceMap());
            routingDataSource.afterPropertiesSet();
            return routingDataSource;
        }
        
        /** This is a default datasource
         * @return
         */
        private DataSource defaultDataSource() {
            final JndiDataSourceLookup dsLookup = new JndiDataSourceLookup();
            DataSource dataSource = dsLookup.getDataSource("jdbc/defaultDs");
            return dataSource;
        }
        
        /** This method sets all predefined client specific datasources in a map
         * @return Map<Object, Object>
         */
        private Map<Object, Object> fetchDatasourceMap(){
            Map<Object, Object> dataSourcesMap = new HashMap<>();
//database.clients=app1,app2,app3
            String client = environment.getRequiredProperty("database.clients");
            String[] allClients = client.split(",");
            if(allClients != null && allClients.length > 0){
                for (Integer i = 0; i < allClients.length; i++) {
                    String clientKey = allClients[i].trim();
                    dataSourcesMap.put(clientKey, dataSource(clientKey));
                }
            }
            return dataSourcesMap;
        }
        
        private DataSource dataSource(String clientKey) {
            final JndiDataSourceLookup dsLookup = new JndiDataSourceLookup();
            String lookupKey = "jdbc/"+clientKey;
            DataSource dataSource = dsLookup.getDataSource(lookupKey);
            return dataSource;
        }
        
        private Properties hibernateProperties() {
            Properties properties = new Properties();
            properties.put("hibernate.dialect", environment.getRequiredProperty("hibernate.dialect"));
            properties.put("hibernate.show_sql", environment.getRequiredProperty("hibernate.show_sql"));
            // properties.put("hibernate.format_sql",
            // environment.getRequiredProperty("hibernate.format_sql"));
            return properties;
        }
    
        @Bean
        JpaTransactionManager transactionManager() {
            JpaTransactionManager transactionManager = new JpaTransactionManager();
            transactionManager.setEntityManagerFactory(entityManagerFactory().getObject());
            return transactionManager;
        }
    }

You have to define your datasource properties in context.xml for JNDI lookup.

Hope this helps

0
Ali Akkaya On

I had a similar requirement. My approach is to pass the config directory to tomcat as environment variable, lets assume you pass

spring.config.location=/home/user/config

Then under /home/user/config folder you should have files matching the contextPath for each application instance. In your case you should have

  • app1.properties
  • app2.properties
  • app3.properties

If you don't want to duplicate common parameters, you can put all common properties in a separate file and use "spring.config.import" to import it from each application specific configuration file. Note that importing another file is supported since Spring Boot 2.4 See section "Importing Additional Configuration"

For Spring Boot application to load the properties file according to the context path, you should override "createRootApplicationContext" to get the context path, and override "configure" to set it as the properties file name as below.

@SpringBootApplication
public class TestApp extends SpringBootServletInitializer {

    private static Class<TestApp> applicationClass = TestApp.class;
    private String contextPath;

    public static void main(String[] args) {
        try {
            SpringApplication.run(applicationClass, args);
        }
        catch (Exception e) {
           // Handle error
        }
    }

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(applicationClass).properties("spring.config.name:" + contextPath);
    }

    @Override
    protected WebApplicationContext createRootApplicationContext(ServletContext servletContext) {
        contextPath = servletContext.getContextPath();
        return super.createRootApplicationContext(servletContext);
    }
}