1

I've been tearing my hair out with what should be a pretty common use case for a web application. I have a Spring-Boot application which uses REST Repositories, JPA, etc. The problem is that I have two data sources:

  • Embedded H2 data source containing user authentication information
  • MySQL data source for actual data which is specific to the authenticated user

Because the second data source is specific to the authenticated user, I'm attempting to use AbstractRoutingDataSource to route to the correct data source according to Principal user after authentication.

What's absolutely driving me crazy is that Spring-Boot is fighting me tooth and nail to instantiate this data source at startup. I've tried everything I can think of, including Lazy and Scope annotations. If I use Session scope, the application throws an error about no session existing at startup. @Lazy doesn't appear to help at all. No matter what annotations I use, the database is instantiated at startup by Spring Boot and doesn't find any lookup key which essentially crashes the entire application.

The other problem is that the Rest Repository API has IMO a terrible means of specifying the actual data source to be used. If you have multiple data sources with Spring Boot, you have to juggle Qualifier annotations which is a runtime debugging nightmare.

Any advice would be very much appreciated.

robross0606
  • 544
  • 1
  • 9
  • 19
  • What appears to happen here is that, the second Spring-Boot initializes the embedded data source (not lazy), it scans and finds the other data source. The EntityManager gets confused because there are multiple data sources found during a scan and, even though they have different qualifiers, it doesn't know about that and throws an error about multiple available sources. However, if I use Primary annotation to specify, that also overrides any @Lazy init and Spring-Boot attempts to init the Primary immediately. – robross0606 Feb 19 '15 at 15:47
  • I don't think `@Primary` and `@Lazy` are incompatible or anything. But I'm not really sure what you are trying to do either. Maybe if you create a small project with 2 `DataSources` and JPA and not much else and paste a link here someone could try and grok what you need. – Dave Syer Feb 19 '15 at 16:28
  • I will work up a project. However, what I need is a Spring-Boot project with two databases. One connection is immediately available. The other "Routing" data source should only be instantiated and used with session scope AFTER a user has authenticated. I should note that this works perfectly fine with regular Spring but gets completely borked with Spring-Boot because it's SO intent on auto-configuration of EVERYTHING. – robross0606 Feb 19 '15 at 17:36
  • If it works in a non-Boot app I guarantee it will work in a Boot app, once you figure out how to switch off the default behaviour. – Dave Syer Feb 19 '15 at 17:42
  • Sample project is posted at https://drive.google.com/file/d/0BzAaZ6knOprdendnWkJBR0Y1bDA/view?usp=sharing – robross0606 Feb 19 '15 at 17:55

1 Answers1

1

Your problem is with the authentication manager configuration. All the samples and guides set this up in a GlobalAuthenticationConfigurerAdapter, e.g. it would look like this as an inner class of your SimpleEmbeddedSecurityConfiguration:

@Configuration
public static class AuthenticationConfiguration extends GlobalAuthenticationConfigurerAdapter
{
    @Bean(name = Global.AUTHENTICATION_DATA_QUALIFIER + "DataSource")
    public DataSource dataSource()
    {
        return new EmbeddedDatabaseBuilder().setName("authdb").setType(EmbeddedDatabaseType.H2).addScripts("security/schema.sql", "security/data.sql").build();
    }

    @Override
    public void init(AuthenticationManagerBuilder auth) throws Exception
    {
            auth.jdbcAuthentication().dataSource(dataSource()).passwordEncoder(passwordEncoder());
    }
}

If you don't use GlobalAuthenticationConfigurerAdapter then the DataSource gets picked up by Spring Data REST during the creation of the Security filters (before the @Primary DataSource bean has even been registered) and the whole JPA initialization starts super early (bad idea).

UPDATE: the authentication manager is not the only problem. If you need to have a session-scoped @Primary DataSource (pretty unusual I'd say), you need to switch off everything that wants access to the database on startup (Hibernate and Spring Boot in various places). Example:

spring.datasource.initialize: false
spring.jpa.hibernate.ddlAuto: none
spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults: false
spring.jpa.properties.hibernate.dialect: H2

FURTHER UPDATE: if you're using the Actuator it also wants to use the primary data source on startup for a health indicator. You can override that by prividing a bean of the same type, e.g.

@Bean
@Scope(value="session", proxyMode=ScopedProxyMode.TARGET_CLASS)
@Lazy
public DataSourcePublicMetrics dataSourcePublicMetrics() {
    return new DataSourcePublicMetrics();
}

P.S. I believe the GlobalAuthenticationConfigurerAdapter might not be necessary in Spring Boot 1.2.2, but it is in 1.2.1 or 1.1.10.

Dave Syer
  • 56,583
  • 10
  • 155
  • 143
  • Thanks, I will check this out. More fundamentally though, how would this work if you had different data sources and one of them didn't happen to be needed for Authentication? What if I really had several different data sources I needed for REST? Also, I'm not sure this solution is going to solve my "early-init" Spring-Boot issue where it ignores @Lazy on the REST service, but I'll certainly give it a try. – robross0606 Feb 20 '15 at 17:07
  • Did you actually make the change to this project and run it? As far as I can tell, it didn't help the problem at all. The issue is fundamentally that I cannot get Spring-Boot to NOT instantiate the UserSpecificDataSource at startup. This should actually be a session scoped configuration but Spring-Boot fails if you set this scope because it tries to instantiate anyway. In fact, I cannot seem to get Spring-Boot to accept ANY @Configuration with a session scope. It triggers at startup and then fails. – robross0606 Feb 20 '15 at 17:30
  • `@Configuration` with session scope doesn't make sense (to me at least) so I either converted them to `@Component` or removed them. I can post your code (heavily modified, since it wasn't really "minimal") back to github if you like. (But the session scope thing has nothing to do with the JPA problem.) – Dave Syer Feb 20 '15 at 17:42
  • Actually I see what you mean (once I cleaned up the session scoped beans a bit). Your `@Primary` `DataSource` is session-scoped and Spring Boot wants to use it on start up (just to check that it works, but of course it doesn't). I'll see what the best way to switch that off is. – Dave Syer Feb 20 '15 at 17:46
  • Yep, reviewing the stack trace, it makes a connection() just to check and then fails. FYI, I even tried using a LazyConnectionDataSourceProxy wrapper on this datasource and that too failed. No matter what I do, Spring-Boot keeps trying to connect to the actual data source at startup. No such problem with straight Spring and a session-based AbstractRoutingDataSource implementation. – robross0606 Feb 20 '15 at 17:53
  • Looking at this further, it appears like PlatformRepository class keeps triggering the data source at startup. Tried making that session scoped and it still creates it at startup. – robross0606 Feb 20 '15 at 18:03
  • I think it's the Hibernate autoconfig that tries the connection. I'll have another look on Monday. – Dave Syer Feb 20 '15 at 18:41
  • Here's your project, stripped down a bit, and with some (apparent) errors fixed. The main thing that makes the session-scoped data source work is the application.properties settings to switch off early access to the DataSource: https://github.com/scratches/session-scoped-jpa. – Dave Syer Feb 21 '15 at 10:05
  • Goodness, I didn't even know that application.properties option was available. – robross0606 Feb 21 '15 at 13:42
  • I tested out your project and there's one big problem. You removed a huge number of my Gradle dependencies. The problem with this is that you're looking at this only from the perspective of this "simplified" project. However, you can't simply remove these dependencies or your solution doesn't solve the "non-simple" project. In fact, if I use my Gradle dependencies with your solution, your solution still fails with the exact same issue. I will be going through the additional spring-boot dependencies one at a time to see which one is causing the problem. – robross0606 Feb 21 '15 at 20:19
  • After doing some testing, I have found that it is actually Spring-Boot-Actuator causing the problem. Remove this dependency from the Gradle file and it works as expected. Add Actuator back in, and it bombs out on any session scoped components. – robross0606 Feb 21 '15 at 20:33
  • Right, but the important thing about simplifying the project is we can deal with issues one at a time in isolation. The Actuator has a `DataSourcePublicMetrics` bean that wants to initialize on startup. Youcan override it by adding a bean of that type and making it `@Lazy` `@Scope(value="session", proxyMode=ScopedProxyMode.TARGET_CLASS)` (as you can see for yourself if you look at the way it is declared). I updated the sample project in github. – Dave Syer Feb 22 '15 at 08:02
  • Great, thanks for your help! Didn't mean to imply your solution was incorrect. You definitely got me back on track. Now to figure out how to mark this question as answered when 99% of the answer is in the comments... – robross0606 Feb 22 '15 at 14:58
  • Just noticed that this breaks again if I set "logging.level.org.hibernate" to "DEBUG". Same error related to session scope. This is a very tenuous Spring configuration. – robross0606 Mar 04 '15 at 04:04
  • That last bit is nothing to do with Spring. Hibernate obviously wants to use the `Connection` if you ask it to DEBUG. – Dave Syer Mar 04 '15 at 17:59
  • I fail to see how DEBUG would automatically require the connection to be made. DEBUG is meant to provide details on what is happening, not change the things that happen. Moreover, I'm not using Hibernate directly. Any connection to Hibernate is happening through Spring-Boot. – robross0606 Mar 04 '15 at 20:49
  • I am hitting the exact same problem on another "legacy" Spring application that isn't using Spring boot and favors configuration XML over annotations. Any hints on how to resolve? – robross0606 May 08 '15 at 20:19