My goal is to create a Webserver with Spring. It has to implement Multitenancy, which works great if you don't make it dynamic (adding, removing, changing). Is it possible to update the datasource bean in Spring?
My code:
#SpringBootApplication
public class MyApplication {
public static void main(String[] args) throws IOException {
SpringApplication.run(MyApplication.class, args);
}
//Multitenancy
#Bean
public DataSource dataSource(){
//implements AbstractRoutingDataSource
CustomRoutingDataSource customDataSource = new CustomRoutingDataSource();
//logic here
return customDataSource;
}
}
What I've tried:
CustomRoutingDataSource c = context.getBean(CustomRoutingDataSource.class);
c.setTargetDataSources(CustomRoutingDataSource.getCustomDatasources());
which updates the bean(?) but doesn't update Spring's datasources, database connections are still missing if added with this method.
Simple solution for those with the same problem:
Add #RefreshScope
#Bean
#RefreshScope
public DataSource dataSource() {
CustomRoutingDataSource customDataSource = new CustomRoutingDataSource();
...
return customDataSource;
}
Add spring actuator endpoint in pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
POST to /actuator/refresh to update datasources!
Related
I'm working on a spring boot (v 2.2.4) app, specifically to add integration tests which leverage Testcontainers to instantiate a docker container that runs a Postgres instance for tests to perform database transactions against. The tests push our database schema into the Postgres instance via Liquibase. I implemented this following this guide. The connection to the test time Postgres is managed by a class called TestPostgresConfig.java (See below). The liquibase operations are performed by a SpringLiquibase object defined in the same class. I run into a problem when I try running the application after successfully building. The issue is the Spring context tries to instantiate the SpringLiquibase bean at runtime (fails due to db.changelog-master.yaml not being found) and I don't want it to do so:
WARN [main]
org.springframework.context.support.AbstractApplicationContext:
Exception encountered during context initialization - cancelling
refresh attempt:
org.springframework.beans.factory.BeanCreationException:
Error creating bean with name 'liquibase' defined in class path
resource
[org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfiguration$LiquibaseConfiguration.class]:
Invocation of init method failed; nested exception is
liquibase.exception.ChangeLogParseException: Error parsing
classpath:db/changelog/changelog-master.yaml
Cause by java.io.FileNotFoundException class path resource
[db/changelog/changelog-master.yaml] cannot be resolved to URL because
it does not exist
This file does not exist, will never exist in this project, and liquibase should not be trying to push change logs at runtime in the first place. I need help figuring out why Spring tries to load the liquibase bean so I can keep that from happening at runtime.
My set up:
#SpringBootApplication
#EnableRetry
#EnableCommonModule
#EnableScheduling
#Slf4j
#EnableConfigurationProperties({
ExternalProperties.class,
ApplicationProperties.class
})
public class MyApplication implements WebMvcConfigurer, CommandLineRunner {
#Autowired
MyService myService;
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
public void run(String... args) throws Exception {
myService.doSomething();
}
}
TestPostgresConfig.java:
#TestConfiguration
#Profile("integration")
public class TestPostgresConfig {
#Bean
public DataSource dataSource() {
DriverManagerDataSource ds = new DriverManagerDataSource();
ds.setDriverClassName("org.postgresql.Driver");
ds.setUrl(format("jdbc:postgresql://%s:%s/%s", MyIT.psqlContainer.getContainerIpAddress(),
MyIT.psqlContainer.getMappedPort(5432), MyIT.psqlContainer.getDatabaseName()));
ds.setUsername(MyIT.psqlContainer.getUsername());
ds.setPassword(MyIT.psqlContainer.getPassword());
ds.setSchema(MyIT.psqlContainer.getDatabaseName());
return ds;
}
#Bean
public SpringLiquibase springLiquibase(DataSource dataSource) throws SQLException {
tryToCreateSchema(dataSource);
SpringLiquibase liquibase = new SpringLiquibase();
liquibase.setDropFirst(true);
liquibase.setDataSource(dataSource);
liquibase.setDefaultSchema("the_schema");
// This and all supported liquibase changelog files are copied onto my classpath
// via the maven assembly plugin. The config to do this has been omitted for the
// sake of brevity
// see this URL for how I did it:
// https://blog.sonatype.com/2008/04/how-to-share-resources-across-projects-in-maven/
liquibase.setChangeLog("classpath:/test/location/of/liquibase.changelog-root.yml");
return liquibase;
}
private void tryToCreateSchema(DataSource dataSource) throws SQLException {
String CREATE_SCHEMA_QUERY = "CREATE SCHEMA IF NOT EXISTS test";
dataSource.getConnection().createStatement().execute(CREATE_SCHEMA_QUERY);
}
}
MyIT.java:
#RunWith(SpringJUnit4ClassRunner.class)
#SpringBootTest(classes=CommonConfig.class)
#ActiveProfile("integration")
#Import(TestPostgresConfig.class)
public class MyIT {
#ClassRule
public static PostgreSQLContainer psqlContainer = new PostgreSQLContainer("postgres:13.1")
.withDatabseName("test-database-instance")
.withUsername("divdiff")
.withPassword("theseAreNotTheDroidsForYou123");
#BeforeClass
public static void init() {
System.setProperty("spring.datasource.url", "jdbc:postgresql://"
+ psqlContainer.getHost() + ":"
+ psqlContainer.getMappedPort(5432) + "/"
+ psqlContainer.getDatabaseName()));
System.setProperty("spring.datasource.username", psqlContainer.getUsername());
System.setProperty("spring.datasource.password", psqlContainer.getPassword());
}
#Before
public void setUp() {
// code to set up my test
}
#Test
public void testMyCodeEndToEnd() {
// my test implementation
}
}
MyConfig.java:
#Configuration
#ComponentScan(basePackages = "my.code")
#EntityScan("my.code")
#Slf4j
public class MyConfig {
#Bean
public KeyStore keyStore() {
//load keystore and set javax.net.ssl.keystore* properties
}
#Bean
public KeyStore trustStore() {
//load truststore and set javax.net.ssl.truststore* properties
}
#Bean
public RestTemplate restTemplate() {
//Set up and load SSL Context with key and trust store
//Create HTTPClient and connection stuff
//Look at this link for a similar set up
//https://www.baeldung.com/rest-template
}
}
application-integration.yml
spring:
jpa:
properties:
hibernate:
enable_lazy_load_no_trans: true
profiles:
active: default
server:
ssl:
# My key and trust store values
application:
unrelated-app-properties:
# propertie values below
Package structure:
app-project/src/main/java/com/my/code/MyApplication.java
app-project/src/main/java/com/my/code/service/MyService.java
app-project/src/test/java/my/code/OTHER-TEST-CLASSES-LIVE-HERE...
app-project/src/test/java/integration/MyIT.java
app-project/src/test/java/integration/TestPostgresConfig.java
app-project/src/test/resources/application-integration.yml
my-common-project/src/main/java/common/config/MyConfig.java
YOUR HELP IS MUCH APPRECIATED!!! :D
I'm an idiot. The maven dependency I brought into for my tests was using provided scope instead of test:
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>project-with-db-changelogs</artifactId>
<version>1.0-SNAPSHOT</version>
<classifier>resources</classifier>
<type>zip</type>
<scope>provided</scope>
</dependency>
When it should have been test scope:
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>project-with-db-changelogs</artifactId>
<version>1.0-SNAPSHOT</version>
<classifier>resources</classifier>
<type>zip</type>
<scope>test</scope>
</dependency>
Per this link, "This is available only in compile-classpath and test-classpath", hence the liquibase code was being run in both my tests and the resulting jar. #amateur-hour
You can defile liqubase context as test
<changeSet author="name" id="id-of-file" context="test">
and have an application property like:
spring.liquibase.contexts=test
and add a liquibase bean like:
#Value("${spring.liquibase.contexts}")
private String liquibaseContexts;
#Bean
public SpringLiquibase liquibase() {
SpringLiquibase liquibase = new SpringLiquibase();
liquibase.setDataSource(localDatabaseDataSource);
liquibase.setShouldRun(liquibaseEnabled);
liquibase.setChangeLog(localDatabaseLiquibaseChangeLog);
liquibase.setContexts(liquibaseContexts);
return liquibase;
}
I am connecting to multiple datasources but sometimes some datasources may be offline and at that time I am geting errors on app and application is failing at startup.
I want to skip datasource configuration at startup... I have tried several ways by adding
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
to the application.properties and also I have tried adding
#SpringBootApplication(exclude={DataSourceAutoConfiguration.class})
to the main class but still it tries to configure the datasource.
I also tried to use #Lazy annotation on all methods and on constructor as below but still getting error while creating fooEntityManagerFactory
#Lazy
#Configuration
#EnableJpaRepositories(basePackages = "com.heyo.tayo.repository.foo", entityManagerFactoryRef = "fooEntityManagerFactory", transactionManagerRef = "fooTransactionManager")
public class PersistencefooConfiguration {
#Autowired
private DbContextHolder dbContextHolder;
#Lazy
#Bean
#ConfigurationProperties("tay.datasource.foo")
public DataSourceProperties fooDataSourceProperties() {
return new DataSourceProperties();
}
#Lazy
#Bean
#ConfigurationProperties("tay.datasource.foo.configuration")
public DataSource fooDataSource() {
DataSource dataSource = fooDataSourceProperties().initializeDataSourceBuilder()
.type(BasicDataSource.class).build();
dbContextHolder.addNewAvailableDbType(DbTypeEnum.foo);
return dataSource;
}
#Lazy
#Bean(name = "fooEntityManagerFactory")
public LocalContainerEntityManagerFactoryBean fooEntityManagerFactory(
EntityManagerFactoryBuilder builder) {
//THE CODE IS FAILING AT BELOW RETURN CASE
return builder
.dataSource(fooDataSource())
.packages("com.heyo.tayo.model.foo")
.build();
}
#Lazy
#Bean
public PlatformTransactionManager fooTransactionManager(
final #Qualifier("fooEntityManagerFactory") LocalContainerEntityManagerFactoryBean fooEntityManagerFactory) {
return new JpaTransactionManager(fooEntityManagerFactory.getObject());
}
}
I have multiple classes like above for different configs for different datasources and I am adding them to available dbs static list at datasource Bean.
Here is my dbadapter factory class.
Here is my dbAdaptor factory that creates corresponding db adaptor
#Service
public class DbAdapterFactory {
#Autowired
private BeanFactory beanFactory;
#Autowired
private DbContextHolder dbContextHolder;
public DBAdapter dbAdapter(){
DbTypeEnum currentDb = dbContextHolder.getCurrentDb();
DBAdapter dbAdapter = null;
if(currentDb == DbTypeEnum.FOODB) {
dbAdapter = beanFactory.getBean(foodbadaptor.class);
} else {
dbAdapter = beanFactory.getBean(koodbadaptor.class);
}
return dbAdapter;
}
Here is db context holder that makes operation like setting default db or getting current db etc.:
#Component
public class DbContextHolder {
private DbTypeEnum dbType = DbTypeEnum.FOODB;
private Set<DbTypeEnum> availableDbTypes = new HashSet<>();
public void setCurrentDb(DbTypeEnum dbType) {
this.dbType = dbType;
}
public DbTypeEnum getCurrentDb() {
return this.dbType;
}
public List<DbTypeEnum> getAvailableDbTypes() {
return new ArrayList<>(availableDbTypes);
}
public void addNewAvailableDbType(DbTypeEnum dbTypeEnum) {
availableDbTypes.add(dbTypeEnum);
}
}
I made all #Lazy or tried #SpringBootApplication(exclude={DataSourceAutoConfiguration.class}) but still something is calling to create bean and getting error and app is closing. I want to use that config and datasource in a try-catch block and don't stop application at runtime. How can I achieve this or what am I missing on that configs or annotations ?
I believe that you can simply add in your application properties
spring.sql.init.continue-on-error=true
According to the Spring Boot 2.5.5 user guide:
https://docs.spring.io/spring-boot/docs/2.5.5/reference/htmlsingle/#howto-initialize-a-database-using-spring-jdbc
Spring Boot enables the fail-fast feature of its script-based database initializer. If the scripts cause exceptions, the application fails to start. You can tune that behavior by setting spring.sql.init.continue-on-error.
Depending on your spring boot version the property will be named either
spring.sql.init.continue-on-error
or before Spring Boot 2.5
spring.datasource.continue-on-error
It is so dumb but I solved the problem by adding following to application.properties.
spring.jpa.database=sql_server
I have no idea why I need to specify that explicitly in properties file but the problem is solved. I will search for it
I just started learning Spring, and now I try to crate Spring JDBC based DAO application.
I created config class in this way
#Configuration
#ComponentScan("com.foxminded.university")
public class SpringJdbcConfig {
#Bean
public DataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("org.postgresql.Driver");
dataSource.setUrl("jdbc:postgresql://127.0.0.1:5432/university");
dataSource.setUsername("maintainer");
dataSource.setPassword("12345678");
return dataSource;
}
}
And dao-class uses this bean
#Component
public class BuildingDao implements Dao<Building> {
#Autowired
private DataSource dataSource;
private final JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
private static final String SAVE_BUILDING = "Insert into buildings (name, floors) values (?,?)";
#Override
public void save(Building building) {
jdbcTemplate.update(SAVE_BUILDING, building.getName(), building.getFloors());
}
}
But when I try to run this query i get
Exception in thread "main" java.lang.IllegalArgumentException: Property 'dataSource' is required
How I can fix it? As I can see, I use #Autowired incorrectly, because everything works fine when I use
private DataSource dataSource = new SpringJdbcConfig().dataSource();
But it is extra relation and mistake in terms of IoC.
By the way in main I also have to use this in this way
public class Main {
public static void main(String[] args) {
Building building = new SpringJdbcConfig().building();
building.setName("hghgf");
building.setFloors(2);
BuildingDao buildingDao = new SpringJdbcConfig().buildingDao();
buildingDao.save(building);
}
}
I would be very grateful if you could explain how to use #autowired correctly and inject beans into the main class.
I would suggest you to use spring boot to configure your application as below. This will initialize and auto-configure most of your needs.
#SpringBootApplication
#EnableAutoConfiguration
#ComponentScan("com.foxminded.university")
public class SpringBootWebApp {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringBootWebApp.class, args);
context.registerShutdownHook();
}
}
After this, you can use #Autowire for all those spring managed beans you configure.
Write the ComponentScan attribute as follows and make sure SpringJdbcConfig class is under the package com.foxminded.university*
#ComponentScan(basePackages = "com.foxminded.university")
I am new to use Struts 2 Framework.
I need to use DataSource Object in Struts Action Class.
My platform is Tomcat 8 (Servlet 3.1) and I set Resource in context.xml.
I can inject Container managed DataSource Object in a servlet by using #Resource annotation.
I'd tried in this way.
I create a ServletContextListener and inject DataSource in this listener.
I set this datasource to application scope object in contextInitialized method.
#WebListener
public class ResourceListener implements ServletContextListener {
#Resource(name="jdbc/skill_db")
private DataSource ds;
public ResourceListener() { }
#Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("Start");
sce.getServletContext().setAttribute("Datasource", ds);
sce.getServletContext().setAttribute("dbConfigStream", sce.getServletContext().getResourceAsStream("/WEB-INF/database.properties"));
}
#Override
public void contextDestroyed(ServletContextEvent sce) { }
}
After that I access application scope and get this datasource from Struts Action methods.
public String welcome() {
Map<String, Object> application = ActionContext.getContext().getApplication();
DataSource ds = (DataSource) application.get("Datasource");
InputStream conf = (InputStream) application.get("dbConfigStream");
Model<Employee> empModel = new BaseModel<Employee>(Employee.class,
Employee::convert, ds, conf);
list = empModel.getAll();
return "welcome";
}
My question are :
Can I get DataSource object in a structs action object?
Is this way that I tried a correct way in struts?
I tried my requirements by Struts2-CDI Plugin
By using CDI I can inject my dependencies.
1. I edit POM of my project as follow.
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-cdi-plugin</artifactId>
<version>2.3.24</version>
</dependency>
<dependency>
<groupId>javax.enterprise</groupId>
<artifactId>cdi-api</artifactId>
<version>1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jboss.weld.servlet</groupId>
<artifactId>weld-servlet</artifactId>
<version>2.2.15.Final</version>
</dependency>
2. As I used Tomcat I need to add this codes to context.xml and web.xml to use CDI.
2.1 context.xml
<Resource name="BeanManager" auth="Container"
type="javax.enterprise.inject.spi.BeanManager"
factory="org.jboss.weld.resources.ManagerObjectFactory" />
2.2 web.xml
<resource-env-ref>
<resource-env-ref-name>BeanManager</resource-env-ref-name>
<resource-env-ref-type>javax.enterprise.inject.spi.BeanManager</resource-env-ref-type>
</resource-env-ref>
3. Produce Datasource
Inject directly DataSource object and ServletContext into ResourceProducer class. So that I don't need listener class to set DataSource to application scope and also don't need to access indirectly to servlet context object.
Using CDI make free the limitations of Struts.
#ApplicationScoped
public class ResourceProducer {
#Resource(name="jdbc/skill_db")
private DataSource datasource;
#Inject
private ServletContext servletContext;
#Produces
#DbResourse
public DataSource getDatasource() {
return datasource;
}
#Produces
#DbConfiguration
public InputStream getConfiguration() {
return servletContext.getResourceAsStream("/WEB-INF/database.properties");
}
}
4. Inject DataSource at Model Producer
#Inject
#DbResourse
private DataSource ds;
#Inject
#DbConfiguration
private InputStream dbConfig;
#Produces
#DataModel(Employee.class)
public Model<Employee> getEmployeeModel() {
return new BaseModel<Employee>(Employee.class, Employee::convert, ds, dbConfig);
}
5. Inject Model at Struts 2 Action Class
#Inject
#DataModel(Employee.class)
private Model<Employee> empModel;
public String welcome() {
list = empModel.getAll();
return "welcome";
}
Just following Spring Guides http://spring.io/guides#gs I took gs-rest-service and gs-accessing-data-jpa. Now I want to combine them in one application, and that is where like more understanding of new org.springframework.boot.SpringApplication is needed.
In gs-rest-service config looks emazing, that is almost absent
#ComponentScan
#EnableAutoConfiguration
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
gs-accessing-data-jpa is more like Spring XML JavaConfig based app.
#Configuration
#EnableJpaRepositories
public class CopyOfApplication {
#Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder().setType(H2).build();
}
// 2 other beans...
#Bean
public PlatformTransactionManager transactionManager() {
return new JpaTransactionManager();
}
public static void main(String[] args) {
AbstractApplicationContext context = new AnnotationConfigApplicationContext(CopyOfApplication.class);
CustomerRepository repository = context.getBean(CustomerRepository.class);
//...
}
}
How to combine them?
Does it mean that I need to re-write SpringApplication.run now on more detailed level ?
In the Spring Boot application simply add the dependencies from the JPA sample (the ones which you don't already have.
dependencies {
compile("org.springframework.boot:spring-boot-starter-web:0.5.0.M7")
compile("org.springframework:spring-orm:4.0.0.RC1")
compile("org.springframework.data:spring-data-jpa:1.4.1.RELEASE")
compile("org.hibernate:hibernate-entitymanager:4.2.1.Final")
compile("com.h2database:h2:1.3.172")
testCompile("junit:junit:4.11")
}
or instead of this you could also use the spring boot starter project for Spring Data JPA. In that case the dependencies would look like the following.
dependencies {
compile("org.springframework.boot:spring-boot-starter-web:0.5.0.M7")
compile("org.springframework.boot:spring-boot-starter-data-jpa:0.5.0.M7")
compile("com.h2database:h2:1.3.172")
testCompile("junit:junit:4.11")
}
This will pull in all needed dependencies.
Next copy the CustomerRepository to the Spring Boot application. That basically should be everything you need. Spring Boot auto-configure now detects Spring Data JPA and JPA and will bootstrap hibernate, spring data for you.
#ComponentScan
#EnableAutoConfiguration
public class Application {
public static void main(String[] args) {
ApplicationContext context= SpringApplication.run(Application.class, args);
CustomerRepository repository = context.getBean(CustomerRepository.class);
//...
}
}