These are my classes and configurations,
ServiceImpl.java
#Override
#Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
public InsertUpdateSuccessBean insertNewSiteEntry(SiteEntry newSiteBean) throws SQLException {
dao.insert1();
dao.insert2();
except();
return new InsertUpdateSuccessBean(true,LoggerConstants.NEW_SITE_ENTRY_SUCCESS);
}
private void except()throws SQLException{
throw new SQLException();
}
DAOImpl.java
#Repository
public class DAOImpl implements DAO
{
#Autowired
private DataSourceTransactionManager transManager;
#Autowired
private CommonDAOUtils commonUtils;
private static final Logger logger = Logger.getLogger(HunterDAOImpl.class) ;
#Override
public Integer insert1() throws SQLException {
Integer insertNumRows = 0;
Connection connectionObject = transManager.getDataSource().getConnection();
PreparedStatement psObject = connectionObject.prepareStatement(
SQLQuery );
insertNumRows = psObject.executeUpdate();
commonUtils.closer(null,psObject,connectionObject);
// Function to close open connection resultsets statement objects
return insertNumRows;
}
#Override
public Integer insert2() throws SQLException {
Integer insertNumRows = 0;
Connection connectionObject = transManager.getDataSource().getConnection();
PreparedStatement psObject = connectionObject.prepareStatement(
SQLQuery );
insertNumRows = psObject.executeUpdate();
commonUtils.closer(null,psObject,connectionObject);
// Function to close open connection resultsets statement objects
return insertNumRows;
}
}
AppConfig.java
import com.interceptors.AppInterceptor;
import com.utils.Constants;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor;
import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.*;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
#Component
#Configuration
#EnableWebMvc
#Configurable
#ComponentScan(basePackages = {Constants.BASE_PACKAGE})
#EnableAspectJAutoProxy(proxyTargetClass = true)
#PropertySource(Constants.DB_PROPERTIES_PATH)
#EnableTransactionManagement
public class AppConfig extends WebMvcConfigurerAdapter implements EnvironmentAware{
#Autowired
Environment environmentObject;
#Bean
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer(){
PropertySourcesPlaceholderConfigurer placeHolder =new PropertySourcesPlaceholderConfigurer();
placeHolder.setLocation(new ClassPathResource(Constants.PROP_FILE_NAME));
return placeHolder;
}
#Bean
public InternalResourceViewResolver viewResolver(){
InternalResourceViewResolver internalResourceViewResolver = new InternalResourceViewResolver();
internalResourceViewResolver.setPrefix(Constants.ROOT_PATH);
internalResourceViewResolver.setSuffix(Constants.JSP_DOT_EXTENSION);
return internalResourceViewResolver;
}
#Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
#Bean
public DriverManagerDataSource dataSource(){
DriverManagerDataSource driverManagerDataSource = new DriverManagerDataSource();
driverManagerDataSource.setDriverClassName(environmentObject.getProperty(Constants.JDBC_DRIVERCLASS_NAME_PROP_FILE));
driverManagerDataSource.setUrl(environmentObject.getProperty(Constants.JDBC_URL_PROP_FILE));
driverManagerDataSource.setUsername(environmentObject.getProperty(Constants.JDBC_USERNAME_PROP_FILE));
driverManagerDataSource.setPassword(new String(Constants.PASSWORD));
return driverManagerDataSource;
}
#Bean
public AutowiredAnnotationBeanPostProcessor postProcessorBean(){
return new AutowiredAnnotationBeanPostProcessor();
}
private ClientHttpRequestFactory getClientHttpRequestFactory() {
int timeout = 5000;
HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory();
clientHttpRequestFactory.setConnectTimeout(timeout);
return clientHttpRequestFactory;
}
#Bean
public DataSourceTransactionManager transactionManager(){
final DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(dataSource());
transactionManager.setRollbackOnCommitFailure(true);
return transactionManager;
}
}
And I also have an around advice for my project:
#Aspect
#Component
public class AroundAdvice
{
private static final Logger logger = Logger.getLogger(AroundAdvice.class) ;
// Add pointcuts here - package names here for around advice
#Around("execution(* com.beans.*.*(..)) || " +
"execution(* com.config.*.*(..)) || " +
"execution(* com.controller.*.*(..)) || " +
"execution(* com.dao.*.*(..)) || " +
"execution(* com.service.*.*(..)) || " +
"execution(* com.utils.*.*(..)) || "+
"execution(* com.interceptors.*.*(..))")
public Object aroundAdviceMethod(ProceedingJoinPoint proceedingObject)throws Throwable{
MethodSignature methodSignObject = (MethodSignature) proceedingObject.getSignature();
logger.debug(Constants.EXECUTING_METH_STR + methodSignObject.getMethod());
Object value = null;
try {
value = proceedingObject.proceed();
} catch (Exception e) {
e.printStackTrace();
logger.error(Constants.EXCEPTION_ASPECT_STR+methodSignObject.getMethod());
logger.error(Constants.EXCEPTION_MESSAGE,e);
throw e;
}
logger.debug(Constants.RETURN_STR+value);
return value;
}
}
On executing this flow, the inserts are successful, however when the exception is thrown, it is not rolling back. However, my logger reads that rolling back is initialized and done as follows,
14:11:51 DEBUG DataSourceTransactionManager:851 - Initiating transaction rollback
14:11:51 DEBUG DataSourceTransactionManager:325 - Rolling back JDBC transaction on Connection [org.postgresql.jdbc4.Jdbc4Connection#3b467b21]
14:11:51 DEBUG DataSourceTransactionManager:368 - Releasing JDBC Connection [org.postgresql.jdbc4.Jdbc4Connection#3b467b21] after transaction
Please let me know if I am missing something
The problem is your dao. You are opening connections yourself and therefor bypass all transaction management. Your dao is to complex just use a JdbcTemplate instead of your current code.
#Repository
public class DAOImpl implements DAO {
private static final Logger logger = Logger.getLogger(HunterDAOImpl.class) ;
private final JdbcTemplate jdbc;
public DAOImpl(DataSource dataSource) {
this.jdbc = new JdbcTemplate(dataSource);
}
#Override
public Integer insert1() throws SQLException {
return jdbc.update(SQLQuery);
}
#Override
public Integer insert2() throws SQLException {
return jdbc.update(SQLQuery);
}
}
This will do exactly the same as your code, with one main difference it will use the Connection opened when the transaction was started. Your sample used 3 separate connections and thus 3 individual transactions instead of 1 single transaction.
Related
I'm at a loss. I upgraded an application from Spring Boot 2.1 to 2.6 and from Wicket 8.0 to 9.6. I had two issues with circular-references that I fixed but now I get an infinite loop if I want to start the application with H2 database because of authenticate. And I'm not sure what is happening there.
So this is the part of the StackTrace that keeps repeating. IntelliJ cuts off the beginning, not sure what to do about that:
at com.xyz.ufa.app.TestUFSession.<init>(TestUFSession.java:15)
at jdk.internal.reflect.GeneratedConstructorAccessor102.newInstance(Unknown Source)
at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:490)
at org.apache.wicket.authroles.authentication.AuthenticatedWebApplication.newSession(AuthenticatedWebApplication.java:108)
at org.apache.wicket.Application.fetchCreateAndSetSession(Application.java:1527)
at org.apache.wicket.Session.get(Session.java:194)
at org.apache.wicket.protocol.http.WebSession.get(WebSession.java:41)
at com.giffing.wicket.spring.boot.starter.configuration.extensions.external.spring.security.SecureWebSession.authenticate(SecureWebSession.java:48)
at org.apache.wicket.authroles.authentication.AuthenticatedWebSession.signIn(AuthenticatedWebSession.java:66)
at com.xyz.ufa.app.TestUFSession.<init>(TestUFSession.java:15)
Here is the TestUFSession.class
import com.xyz.ufa.frontend.config.UFSession;
import org.apache.wicket.request.Request;
/**
* Helper session to login autmatically.
*
*/
public class TestUFSession extends UFSession {
public TestUFSession(Request request) {
super(request);
signIn("admin", "admin"); // this calls authenticate
}
}
And here the UFSession class
import com.giffing.wicket.spring.boot.starter.configuration.extensions.external.spring.security.SecureWebSession;
import java.util.Locale;
import lombok.Getter;
import org.apache.wicket.request.Request;
#Getter
public class UFSession extends SecureWebSession {
private String username;
private Locale locale;
public UFSession(Request request) {
super(request);
locale = request.getLocale();
}
#Override
public void signOut() {
username = null;
super.signOut();
}
}
And here the WebSecurityConfiguration class
import com.xyz.uf.common.ApplicationProfile;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Profile;
import org.springframework.ldap.core.support.LdapContextSource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configurers.userdetails.DaoAuthenticationConfigurer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.ldap.authentication.AbstractLdapAuthenticationProvider;
import org.springframework.security.ldap.authentication.LdapAuthenticationProvider;
import org.springframework.security.ldap.authentication.PasswordComparisonAuthenticator;
import org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider;
import org.springframework.security.ldap.search.FilterBasedLdapUserSearch;
import org.springframework.security.ldap.search.LdapUserSearch;
import org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator;
import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator;
import org.springframework.security.ldap.userdetails.LdapUserDetailsService;
#Slf4j
#Configuration
#Import(LdapProperties.class)
#ComponentScan(basePackages = {"com.xyz.ufa.security.userinfo"})
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
private final String AUTHENTICATION_PROVIDER_BEAN_NAME = "authenticationProvider";
#Autowired
private LdapProperties ldapProperties;
#Bean(name = "authenticationManager")
#Override
public AuthenticationManager authenticationManagerBean() throws AuthenticationServiceException {
try {
return super.authenticationManagerBean();
} catch (Exception e) {
log.error("Error in authenticationManagerBean", e);
throw new AuthenticationServiceException(e.getMessage(), e);
}
}
#Override
protected void configure(HttpSecurity httpSecurity) throws AuthenticationServiceException {
try {
httpSecurity
.csrf().disable()
.authorizeRequests()
.antMatchers("/*").permitAll()
.antMatchers("/restservice/**").hasAuthority(UFARole.TECHNICAL_ADMIN)
.and().httpBasic()
.and().logout().permitAll();
httpSecurity.headers().frameOptions().disable();
} catch (Exception e) {
throw new AuthenticationServiceException(String.format("Could not configure %s with csrf disabled and matching Pattern /*.", httpSecurity), e);
}
}
#Override
public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
try {
AbstractLdapAuthenticationProvider authentProvider = (AbstractLdapAuthenticationProvider) getApplicationContext().getBean(AUTHENTICATION_PROVIDER_BEAN_NAME);
authentProvider.setAuthoritiesMapper(authoritiesMapper());
DaoAuthenticationConfigurer<AuthenticationManagerBuilder, LdapUserDetailsService> configurer = authenticationManagerBuilder
.authenticationProvider(authentProvider)
.userDetailsService(ldapUserDetailsService());
passwordEncoder.ifPresent(configurer::passwordEncoder);
} catch (Exception e) {
throw new AuthenticationServiceException("Could not configure authentication manager ", e);
}
}
#Bean
public LdapUserDetailsService ldapUserDetailsService() {
LdapUserDetailsService userDetailsService = new LdapUserDetailsService(userSearch(), ldapAuthoritiesPopulator());
return userDetailsService;
}
#Bean
public LdapUserSearch userSearch() {
return new FilterBasedLdapUserSearch(ldapProperties.getUserSearchBase(), ldapProperties.getUserSearchFilter(), contextSource());
}
#Bean
public GrantedAuthoritiesMapper authoritiesMapper() {
return new GrantAuthoritiesMapperWithEnvTag(ldapProperties.getEnv());
}
#Bean
public LdapContextSource contextSource() {
LdapContextSource ldapContextSource = new LdapContextSource();
ldapContextSource.setUrl(ldapProperties.getServerUrl());
ldapContextSource.setAnonymousReadOnly(true);
return ldapContextSource;
}
#Bean
public LdapAuthoritiesPopulator ldapAuthoritiesPopulator() {
DefaultLdapAuthoritiesPopulator ldapAuthoritiesPopulator = new DefaultLdapAuthoritiesPopulator(contextSource(), ldapProperties.getGroupSearchBase());
ldapAuthoritiesPopulator.setGroupSearchFilter(ldapProperties.getGroupSearchFilter());
ldapAuthoritiesPopulator.setGroupRoleAttribute(ldapProperties.getGroupRoleAttribute());
ldapAuthoritiesPopulator.setRolePrefix("");
ldapAuthoritiesPopulator.setConvertToUpperCase(false);
return ldapAuthoritiesPopulator;
}
#Bean(name = AUTHENTICATION_PROVIDER_BEAN_NAME)
#Profile(value = { ApplicationProfile.Values.TEST, ApplicationProfile.Values.PROD })
public AuthenticationProvider activeDirectory() {
ActiveDirectoryLdapAuthenticationProvider authenticationProvider = new ActiveDirectoryLdapAuthenticationProvider("HRE.LOC", ldapProperties.getServerUrl());
authenticationProvider.setSearchFilter(ldapProperties.getUserSearchFilter());
return authenticationProvider;
}
#Bean(name = AUTHENTICATION_PROVIDER_BEAN_NAME)
#Profile(value = { ApplicationProfile.Values.DEFAULT, ApplicationProfile.Values.DEV })
public AuthenticationProvider defaultAuthenticationProvider() {
PasswordComparisonAuthenticator authenticator = new PasswordComparisonAuthenticator(contextSource());
authenticator.setPasswordAttributeName("userPassword");
passwordEncoder.ifPresent(authenticator::setPasswordEncoder);
authenticator.setUserSearch(userSearch());
LdapAuthenticationProvider authenticationProvider = new LdapAuthenticationProvider(authenticator, ldapAuthoritiesPopulator());
return authenticationProvider;
}
/**
* This bean is optional and not available for some profiles. Password encoder is only required for embedded LDAP, for productive Active Directory it is not used
*/
#Bean("passwordEncoder")
#Profile(value = { ApplicationProfile.Values.DEFAULT, ApplicationProfile.Values.DEV })
public static PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
#Autowired(required = false)
#Qualifier("passwordEncoder")
private Optional<PasswordEncoder> passwordEncoder;
}
Profile used for test is default.
Anyone any ideas?
Edit: So Martin helped me to understand the problem, but I'm too dumb to fix it. TestUFApplication is instantiated here via ReflectionTestUtils
ยดยดยดยด
#SpringBootApplication
public class TestApplicationWithH2Database {
#Autowired
private UFyWicketWebApplication webApplication;
#Value("${test.autologin.active}")
private boolean testAutologinActive;
public static void main(String[] args) {
new SpringApplicationBuilder()
.sources(Application.class)
.run(args);
}
#PostConstruct
public void PostConstruct() {
if (testAutologinActive) {
ReflectionTestUtils.setField(webApplication, "sessionClass", TestUFSession.class);
}
}
}
I tried to make signIn() a method in TestUFSession and call it like that
TestUFSession testUFSession = (TestUFSession) ReflectionTestUtils.getField(webApplication, "sessionClass");
testUFSession.signIn();
but got a ClassCastException
You will need to call signIn("admin", "admin"); after instantiating the TestUFSession.
From the stacktrace we can see that com.giffing.wicket.spring.boot.starter.configuration.extensions.external.spring.security.SecureWebSession.authenticate(SecureWebSession.java:48) tries to lookup the WebSession via its static #get() method and that leads to the infinite loop.
Question regarding our Spring Boot 2.3.8.RELEASE implementation of #Transactional. The requirement is to implement distributed transactions that writes to an instance of PostgreSQL and Artemis queues. If one commit fails, then so should the other. We are using Atomikos for our JTA Transaction Manager.
I think I have implemented everything I need, but clearly not. When I throw an Exception in my service code to test the rollback functionality, it clearly does not work: The message is written to Artemis even after I throw an exception in the service code.
Any help with diagnosing and fixing would be much appreciated. If any additional details are required, please let me know.
Please find the details of the implementation below:
Spring Boot Application:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
import org.springframework.boot.autoconfigure.jms.JmsAutoConfiguration;
import org.springframework.boot.autoconfigure.jms.activemq.ActiveMQAutoConfiguration;
import org.springframework.boot.autoconfigure.jms.artemis.ArtemisAutoConfiguration;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import xxx.xxx.Users;
import xxx.xxx.TransactionServiceImpl;
#SpringBootApplication
(
exclude = {
DataSourceAutoConfiguration.class,
HibernateJpaAutoConfiguration.class,
DataSourceTransactionManagerAutoConfiguration.class,
JmsAutoConfiguration.class,
ActiveMQAutoConfiguration.class,
ArtemisAutoConfiguration.class
}
)
public class BApplication implements CommandLineRunner
{
public static void main(String[] args) throws Exception
{
// SpringApplication.run(BoilerplateApplication.class, args);
ConfigurableApplicationContext ctx = SpringApplication.run(BApplication.class, args);
System.in.read();
ctx.close();
}
#Autowired
TransactionServiceImpl tsi;
#Override
public void run(String... args) throws Exception
{
Users user = new Users();
user.setFirstName("Moe");
user.setGender("M");
user.setLastName("Moe");
tsi.save(user);
}
}
Here is the JTA Configuration:
JTA Configuration
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.jms.ConnectionFactory;
import javax.jms.JMSException;
import javax.sql.DataSource;
import javax.transaction.SystemException;
import javax.transaction.UserTransaction;
import java.util.Properties;
import javax.annotation.PostConstruct;
import org.springframework.context.annotation.Bean;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.DependsOn;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import com.atomikos.icatch.config.UserTransactionService;
import com.atomikos.icatch.config.UserTransactionServiceImp;
import com.atomikos.icatch.jta.UserTransactionImp;
import com.atomikos.icatch.jta.UserTransactionManager;
import com.atomikos.jdbc.AtomikosDataSourceBean;
import com.atomikos.jms.AtomikosConnectionFactoryBean;
import org.apache.activemq.artemis.jms.client.ActiveMQConnectionFactory;
import org.postgresql.xa.PGXADataSource;
#Configuration("jtaConfig")
public class JtaConfig
{
private static final Logger appLogger = LoggerFactory.getLogger(JtaConfig.class);
#Value("${amq.broker.url}")
private String brokerUrl;
#Value("${amq.broker.username}")
private String brokerUsername;
#Value("${amq.broker.password}")
private String brokerPassword;
#Value("${postgresql.datasource.url}")
String dataSourceUrl;
#Value("${postgresql.datasource.username}")
String dsUsername;
#Value("${postgresql.datasource.password}")
String dsPassword;
#Value("${postgresql.datasource.driver.classname}")
String dsClassName;
#Value("${postgresql.initial.connections}")
int initialDSConnections;
#Value("${postgresql.max.connections}")
int maxDSConnections;
#Bean(initMethod = "init", destroyMethod = "shutdownForce")
public UserTransactionService userTransactionService()
{
Properties atProps = new Properties();
atProps.put("com.atomikos.icatch.service", "com.atomikos.icatch.standalone.UserTransactionServiceFactory");
return new UserTransactionServiceImp(atProps);
}
#Bean (initMethod = "init", destroyMethod = "close")
#DependsOn("userTransactionService")
public UserTransactionManager atomikosTransactionManager()
{
UserTransactionManager utm = new UserTransactionManager();
utm.setStartupTransactionService(false);
utm.setForceShutdown(true);
return utm;
}
#Bean
#DependsOn("userTransactionService")
public UserTransaction userTransaction()
{
UserTransactionImp ut = new UserTransactionImp();
try
{
ut.setTransactionTimeout(1000);
}
catch (SystemException _e)
{
appLogger.error("Configuration exception.", _e);
return null;
}
return ut;
}
#Bean
public Properties hibernateProperties()
{
Properties hibernateProp = new Properties();
hibernateProp.put("hibernate.dialect", "org.hibernate.dialect.PostgreSQLDialect");
hibernateProp.put("hibernate.hbm2ddl.auto", "create-drop");
hibernateProp.put("hibernate.show_sql", true);
hibernateProp.put("hibernate.max_fetch_depth", 3);
hibernateProp.put("hibernate.jdbc.batch_size", 10);
hibernateProp.put("hibernate.jdbc.fetch_size", 50);
return hibernateProp;
}
#Bean
public JpaVendorAdapter jpaVendorAdapter()
{
return new HibernateJpaVendorAdapter();
}
#Primary
#Bean(name = "pgDataSource1", initMethod = "init", destroyMethod = "close")
public DataSource pgDataSource1()
{
PGXADataSource primaryXaDataSource = new PGXADataSource();
primaryXaDataSource.setUrl(dataSourceUrl);
primaryXaDataSource.setUser(dsUsername);
primaryXaDataSource.setPassword(dsPassword);
AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean();
xaDataSource.setXaDataSource(primaryXaDataSource);
xaDataSource.setUniqueResourceName("primaryXaDs1");
xaDataSource.setMinPoolSize(initialDSConnections);
xaDataSource.setMaxPoolSize(maxDSConnections);
return xaDataSource;
}
#Primary
#Bean(name = "jmsConnectionFactory", initMethod = "init", destroyMethod = "close")
public ConnectionFactory connectionFactory()
{
AtomikosConnectionFactoryBean atomikosConnectionFactoryBean = new AtomikosConnectionFactoryBean();
ActiveMQConnectionFactory activeMqXaConnectionFactory = new ActiveMQConnectionFactory();
try
{
activeMqXaConnectionFactory.setBrokerURL(brokerUrl);
activeMqXaConnectionFactory.setUser(brokerUsername);
activeMqXaConnectionFactory.setPassword(brokerPassword);
atomikosConnectionFactoryBean.setUniqueResourceName("jmsXAConnectionFactory");
atomikosConnectionFactoryBean.setLocalTransactionMode(false);
atomikosConnectionFactoryBean.setXaConnectionFactory(activeMqXaConnectionFactory);
}
catch (JMSException _e)
{
appLogger.info("JMS Configuration Error: " + _e);
_e.printStackTrace();
}
return atomikosConnectionFactoryBean;
}
#PostConstruct
public void postConstructDetails()
{
appLogger.info("Post Construct Start: JtaConfig.");
appLogger.info(" - JMS: Artemis URL: {}", brokerUrl);
appLogger.info(" - Artemis Username: {}", brokerUsername);
appLogger.info(" - Artemis Password: {}", brokerPassword);
appLogger.info(" - DS: PostgreSQL URL: {}", dataSourceUrl);
appLogger.info(" - DS: PostgreSQL Username: {}", dsUsername);
appLogger.info(" - DS: PostgreSQL Password: {}", dsPassword);
appLogger.info(" - DS: PostgreSQL Min Conn: {}", initialDSConnections);
appLogger.info(" - DS: PostgreSQL Max Conn: {}", maxDSConnections);
appLogger.info("Post Construct End: JtaConfig.");
appLogger.info(" ");
}
}
Here is the implementation for Services Configuration:
Services Configuration:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.jta.JtaTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.annotation.PostConstruct;
import javax.persistence.EntityManagerFactory;
#Configuration
#EnableTransactionManagement
#ComponentScan(basePackages = "xxx.xxx.service")
public class ServicesConfig
{
private Logger appLogger = LoggerFactory.getLogger(ServicesConfig.class);
#Autowired
JtaConfig jtaConfig;
#Bean(name = "xaJmsTemplate")
public JmsTemplate jmsTemplate()
{
JmsTemplate jmsTemplate = new JmsTemplate();
jmsTemplate.setConnectionFactory(jtaConfig.connectionFactory());
jmsTemplate.setPubSubDomain(false);
return jmsTemplate;
}
#Bean(name = "entityManangerFactory")
public EntityManagerFactory entityManagerFactory()
{
LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean();
factoryBean.setPackagesToScan("xxx.xxx.model");
factoryBean.setDataSource(jtaConfig.pgDataSource1());
factoryBean.setJpaProperties(jtaConfig.hibernateProperties());
factoryBean.setPersistenceUnitName("entityManagerFactoryA");
factoryBean.setJpaVendorAdapter(jtaConfig.jpaVendorAdapter());
factoryBean.afterPropertiesSet();
return factoryBean.getNativeEntityManagerFactory();
}
#Bean(name = "transactionManager")
public PlatformTransactionManager transactionManager()
{
JtaTransactionManager ptm = new JtaTransactionManager();
ptm.setTransactionManager(jtaConfig.atomikosTransactionManager());
ptm.setUserTransaction(jtaConfig.userTransaction());
return ptm;
}
#PostConstruct
public void postConstructDetails()
{
appLogger.info("Post Construct Start: ServicesConfig.");
appLogger.info(" - JMS: Artemis URL: {}", jtaConfig);
appLogger.info(" - JMS Template: {}", jmsTemplate());
appLogger.info("Post Construct End: ServicesConfig.");
appLogger.info(" ");
}
}
Here is the Service implementation:
TransactionServiceImpl
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import xxx.xxx.Users;
#Service("transactionService")
#Transactional
public class TransactionServiceImpl implements TransactionServiceIntf
{
private static final Logger appLogger = LoggerFactory.getLogger(TransactionServiceImpl.class);
#Autowired
#Qualifier("xaJmsTemplate")
JmsTemplate jmsTemplate;
#Override
public Users save(Users _user)
{
appLogger.info("TransactionServiceImpl: save: Entered.");
Users user = _user;
try
{
if(user == null)
{
appLogger.info("User: Null.");
}
else
{
if(jmsTemplate == null)
{
appLogger.info("JMS Template: Null.");
}
else
{
appLogger.info("JMS Template: Saving.");
jmsTemplate.convertAndSend("crequests", user);
}
}
// The rollback should happen with the exception.
throw new Exception();
}
catch(Exception _e)
{
appLogger.error("Catching exception: " + _e);
}
appLogger.info("TransactionServiceImpl: save: Exiting.");
return user;
}
}
I have two database structures, example:
1- MAIN_DATABASE: USER PASSWORD
2: CUSTOMER DATABASE: CUSTOMER_A CUSTOMER_B CUSTOMER_C
I want to access the main database and after validating the data, redirect to the customer database.
I currently use spring and configure it in applicationContext.xml
Example:
<bean id = "encryptionPassword" class = "utils.EncryptionPasswordSpring" />
<bean id = "dataSource" class = "com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method = "close">
<property name = "driverClass" value = "com.mysql.jdbc.Driver" />
<property name = "user" value = "user" />
<property name = "password" value = "123456" />
<property name = "jdbcUrl" value = "jdbc:mysql://localhost/testdb?useSSL = false" />
</bean>
Any example, suggestion? Thanks.
The below is my code for dynamic datasource with mybatis. one is main ds. another is read ds. Hope it useful to you.
use AbstractRoutingDataSource to define a DynamicDataSource
import java.util.Map;
import javax.sql.DataSource;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class DynamicDataSource extends AbstractRoutingDataSource {
#Override
protected DataSource determineTargetDataSource() {
return super.determineTargetDataSource();
}
/**
*/
#Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDataSourceKey();
}
/**
* #param defaultDataSource
*/
public void setDefaultDataSource(Object defaultDataSource) {
super.setDefaultTargetDataSource(defaultDataSource);
}
/**
* #param dataSources
*/
public void setDataSources(Map<Object, Object> dataSources) {
super.setTargetDataSources(dataSources);
DynamicDataSourceContextHolder.addDataSourceKeys(dataSources.keySet());
}
}
use ThreadLocal to switch datasource in context
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
public class DynamicDataSourceContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>() {
/**
make main as default ds
*/
#Override
protected String initialValue() {
return "main";
}
};
/**
*
*/
private static List<Object> dataSourceKeys = Collections.synchronizedList(new ArrayList<>());
/**
* switch ds
*
* #param key
*/
public static void setDataSourceKey(String key) {
contextHolder.set(key);
}
/**
* get ds
*
* #return
*/
public static String getDataSourceKey() {
return contextHolder.get();
}
/**
* reset ds
*/
public static void clearDataSourceKey() {
contextHolder.remove();
}
/**
* judge if ds existed
*
* #param key
* #return
*/
public static boolean containDataSourceKey(String key) {
return dataSourceKeys.contains(key);
}
/**
* add ds
*
* #param keys
* #return
*/
public static boolean addDataSourceKeys(Collection<? extends Object> keys) {
return dataSourceKeys.addAll(keys);
}
}
inject different datasource via application.yml or application.properties
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import javax.sql.DataSource;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
#Configuration
public class DataSourceConfig {
#Primary
#Bean
#ConfigurationProperties("spring.datasource.main")
public DataSource main() {
return DataSourceBuilder.create().build();
}
#Bean
#ConfigurationProperties("spring.datasource.read")
public DataSource read() {
return DataSourceBuilder.create().build();
}
#Bean
public DataSource dynamicDataSource(
#Qualifier("main") DataSource main,
#Qualifier("read") DataSource read
) {
Map<Object, Object> targetDataSources = new HashMap<>(2);
targetDataSources.put("main", main);
targetDataSources.put("read", read);
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setDefaultTargetDataSource(main); //default
dynamicDataSource.setDataSources(targetDataSources);
return dynamicDataSource;
}
#Bean
public SqlSessionFactory sqlSessionFactory(
#Qualifier("dynamicDataSource") DataSource dynamicDataSource)
throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dynamicDataSource);
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mappings/**/*.xml"));
return bean.getObject();
}
#Bean(name = "sqlSessionTemplate")
public SqlSessionTemplate sqlSessionTemplate(
#Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory)
throws Exception {
return new SqlSessionTemplate(sqlSessionFactory);
}
#Bean
public PlatformTransactionManager transactionManager(
#Qualifier("dynamicDataSource") DataSource dynamicDataSource
) {
return new DataSourceTransactionManager(dynamicDataSource);
}
}
define an AOP to control which Dao method use which datasource. Dao is interface to access DB via mybatis.
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
#Aspect
#Order(-1)
#Component
public class DynamicDataSourceAspect {
private final static Logger logger = LoggerFactory.getLogger(DynamicDataSourceAspect.class);
private final String[] QUERY_PREFIX = {
"select","get","find","query","quickGet"
};
#Pointcut("execution( * com.biz.dao..*.*(..))")
public void daoAspect() {
}
#Before("daoAspect()")
public void beforeDao(JoinPoint point) {
boolean isQueryMethod = isQueryMethod(point.getSignature().getName());
if (isQueryMethod) {
switchDataSource("read");
}
}
#After("daoAspect()")
public void afterDao(JoinPoint point) {
restoreDataSource();
}
//===============================private method
private void switchDataSource(String key) {
if (!DynamicDataSourceContextHolder.containDataSourceKey(key)) {
logger.debug("======>DataSource [{}] doesn't exist, use default DataSource [{}] " + key);
} else {
// switch ds
DynamicDataSourceContextHolder.setDataSourceKey(key);
logger.debug("======>Switch DataSource to " + DynamicDataSourceContextHolder.getDataSourceKey());
}
}
private void restoreDataSource() {
// reset to default ds
DynamicDataSourceContextHolder.clearDataSourceKey();
}
private boolean isQueryMethod(String methodName) {
for (String prefix : QUERY_PREFIX) {
if (methodName.startsWith(prefix)) {
return true;
}
}
return false;
}
}
Configure two beans with two different set of configs in app.props
For one configuration you can use this (beanName = dataSource1):
#Configuration
#EnableRetry
public class DataSourceConfiguration {
#Value("${datasource1.username}")
private String username;
#Value("${datasource1.password}")
private String password;
#Value("${datasource1.url}")
private String connection;
#Bean(name = "dataSource1")
#Primary
public DataSource mainDataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl(connection);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
}
}
I have 1 spring context named PersistenceJPAConfig. Now I want to configure a spring batch and for that I have added a new class with #Configuration annotation and #EnableBatchProcessing. After adding new configuration class, I got error trying to use repository methods: nested exception is javax.persistence.TransactionRequiredException: no transaction is in progress. I know this is because I have a parent spring context and a child context which means I will have 2 instance for every repository and every service. I have tried to exclude repository scanning and service scanning with:
#ComponentScan(useDefaultFilters = false,
excludeFilters = {#ComponentScan.Filter(Repository.class), #ComponentScan.Filter(Service.class), #ComponentScan.Filter(Configuration.class)})
but it's not working. The only solution until now is to move all the beans from the second configuration to the first one, but I don't want that. How to solve this conflict between the contexts?
Main context:
package com.netoptics.server;
import java.util.Properties;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import com.websilicon.security.SecurityGlobals;
import com.websilicon.util.AppConfig;
#Configuration
#EnableTransactionManagement
#EnableJpaRepositories({"com.whitelist.manager.repositories", "com.wsnms", "com.websilicon"})
public class PersistenceJPAConfig {
#Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
entityManagerFactoryBean.setDataSource(dataSource());
entityManagerFactoryBean.setPackagesToScan("com.whitelist.manager", "com.wsnms", "com.websilicon");
JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
entityManagerFactoryBean.setJpaVendorAdapter(vendorAdapter);
entityManagerFactoryBean.setJpaProperties(additionalProperties());
return entityManagerFactoryBean;
}
#Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(entityManagerFactory);
return transactionManager;
}
#Bean
public DataSource dataSource() {
String databaseDriver = AppConfig.getInstance().getString("dataDatabaseDriver", "");
String databaseUrl = AppConfig.getInstance().getString("dataDatabaseUrl", "");
String databaseUsername = AppConfig.getInstance().getString("dataDatabaseUsername", "");
String dataDatabasePassword = AppConfig.getInstance().getPassword("dataDatabasePassword", SecurityGlobals.KEY, "");
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName(databaseDriver);
dataSource.setUrl(databaseUrl);
dataSource.setUsername(databaseUsername);
dataSource.setPassword(dataDatabasePassword);
return dataSource;
}
#Bean
public PersistenceExceptionTranslationPostProcessor exceptionTranslation() {
return new PersistenceExceptionTranslationPostProcessor();
}
private Properties additionalProperties() {
Properties properties = new Properties();
properties.setProperty("hibernate.hbm2ddl.auto", "none");
properties.setProperty("hibernate.dialect", "org.hibernate.dialect.PostgreSQL82Dialect");
properties.setProperty("hibernate.show_sql", "false");
properties.setProperty("hibernate.jdbc.batch_size", "1000");
return properties;
}
}
Second context for configuring spring batch:
#Configuration
#EnableBatchProcessing
#ComponentScan(useDefaultFilters = false,
excludeFilters = {#ComponentScan.Filter(Repository.class), #ComponentScan.Filter(Service.class)})
public class SaveImsiCSVBatchConfig {
#Autowired
public JobBuilderFactory jobBuilderFactory;
#Autowired
public StepBuilderFactory stepBuilderFactory;
#Autowired
public DataSource dataSource;
#Autowired
private Environment environment;
#Autowired
private WmAdminImsisResourceHelper wmAdminImsisResourceHelper;
#Bean
public JobRepository jobRepository() {
MapJobRepositoryFactoryBean factoryBean = new MapJobRepositoryFactoryBean(new ResourcelessTransactionManager());
try {
return factoryBean.getObject();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
#Bean
public JobLauncher jobLauncher(JobRepository jobRepository) {
SimpleJobLauncher jobLauncher = new SimpleJobLauncher();
jobLauncher.setJobRepository(jobRepository);
return jobLauncher;
}
#Bean
#StepScope
public JdbcCursorItemReader<WmPushedImsi> reader(#Value("#{jobParameters['sortProperty']}") String sortProperty,
#Value("#{jobParameters['sortValue']}") String sortValue, #Value("#{jobParameters['username']}") String usernameFilter,
#Value("#{jobParameters['imsinumber']}") String imsiNumberFilter) {
JdbcCursorItemReader<WmPushedImsi> reader = new JdbcCursorItemReader<>();
reader.setDataSource(dataSource);
String sql =
"select us.username, wp.imsinumber, wp.startdate, wp.expiredate, case when wp.failedpushbatchid is not null or wp.faileddeletebatchid is not null then 'Yes' ELSE 'No' end as dirty from\n"
+ "wm_pushed_imsi wp INNER JOIN wm_admin_user wu on wp.userid = wu.id INNER JOIN users us on wu.userid = us.id";
if (usernameFilter != null && imsiNumberFilter != null) {
sql += " AND us.username LIKE '%" + usernameFilter + "%' AND wp.imsinumber LIKE '%" + imsiNumberFilter + "%'";
} else if (usernameFilter != null) {
sql += " AND us.username LIKE '%" + usernameFilter + "%'";
} else if (imsiNumberFilter != null) {
sql += " AND wp.imsinumber LIKE '%" + imsiNumberFilter + "%'";
}
if (sortProperty != null) {
sql += " order by " + sortProperty + " " + sortValue;
}
reader.setSql(sql);
reader.setRowMapper(new ImsiRowMapper());
return reader;
}
#Bean
public ImsiProcessor processor() {
return new ImsiProcessor();
}
#Bean
#StepScope
public FlatFileItemWriter<WmPushedImsi> writer(#Value("#{jobParameters['currentDate']}") Date currentDate) {
wmAdminImsisResourceHelper.createDirectoryForSavingCsv();
String fileName = wmAdminImsisResourceHelper.createFileNameForCsv(currentDate) + environment.getProperty("CSVEXTENSION");
String columnsTitle = Arrays.toString(new String[] {environment.getProperty("CSV_IMSINUMBER"), environment.getProperty("CSV_USERNAME"),
environment.getProperty("CSV_STARTDATE"), environment.getProperty("CSV_EXPIREDATE"), environment.getProperty("CSV_DIRTY")});
FlatFileItemWriter<WmPushedImsi> writer = new FlatFileItemWriter<>();
writer.setResource(new FileSystemResource(fileName));
writer.setHeaderCallback(writerHeader -> writerHeader.write(columnsTitle.substring(1, columnsTitle.length() - 1)));
writer.setLineAggregator(new DelimitedLineAggregator<>() {
{
setDelimiter(",");
setFieldExtractor(new BeanWrapperFieldExtractor<>() {
{
setNames(new String[] {WmPushedImsi_.IMSI_NUMBER, "username", WmPushedImsi_.START_DATE, WmPushedImsi_.EXPIRE_DATE, "dirty"});
}
});
}
});
return writer;
}
#Bean
public Step stepToCreateCsvFile() {
return stepBuilderFactory.get(Objects.requireNonNull(environment.getProperty("CSV_STEP_CREATE_FILE"))).<WmPushedImsi, WmPushedImsi>chunk(50000)
.reader(reader("", "", "", "")).processor(processor()).writer(writer(null)).build();
}
#Bean
public Step stepToDeleteFileAndCreateArchive() {
FileArchiveAndDeletingTasklet task = new FileArchiveAndDeletingTasklet();
task.setWmAdminImsisResourceHelper(wmAdminImsisResourceHelper);
task.setDateString(environment.getProperty("CSV_DATE"));
return stepBuilderFactory.get(Objects.requireNonNull(environment.getProperty("CSV_STEP_CREATE_ARCHIVE"))).tasklet(task).build();
}
#Bean
public Job exportImsiCSVJob() {
return jobBuilderFactory.get(Objects.requireNonNull(environment.getProperty("CSV_EXPORT_JOB"))).incrementer(new RunIdIncrementer())
.start(stepToCreateCsvFile()).next(stepToDeleteFileAndCreateArchive()).build();
}
#Bean
public JobRegistryBeanPostProcessor jobRegistryBeanPostProcessor(JobRegistry jobRegistry) {
JobRegistryBeanPostProcessor jobRegistryBeanPostProcessor = new JobRegistryBeanPostProcessor();
jobRegistryBeanPostProcessor.setJobRegistry(jobRegistry);
return jobRegistryBeanPostProcessor;
}
public class ImsiRowMapper implements RowMapper<WmPushedImsi> {
#Override
public WmPushedImsi mapRow(ResultSet rs, int rowNum) throws SQLException {
WmPushedImsi wmPushedImsi = new WmPushedImsi();
wmPushedImsi.setImsiNumber(rs.getString(WmPushedImsi_.IMSI_NUMBER));
wmPushedImsi.setUsername(rs.getString("username"));
wmPushedImsi.setStartDate(rs.getDate(WmPushedImsi_.START_DATE));
wmPushedImsi.setExpireDate(rs.getDate(WmPushedImsi_.EXPIRE_DATE));
wmPushedImsi.setDirty(rs.getString("dirty"));
return wmPushedImsi;
}
}
}
You are using the MapJobRepository with a ResourcelessTransactionManager. With this configuration, there are no transactions on Spring Batch side. Hence the error no transaction is in progress.
You need to configure a JobRepository with the transaction manager you defined in your PersistenceJPAConfig. To do this, you have to define a bean of type BatchConfigurer and override getTransactionManager. Here is an example:
#Bean
public BatchConfigurer batchConfigurer() {
return new DefaultBatchConfigurer() {
#Override
public PlatformTransactionManager getTransactionManager() {
return new MyTransactionManager();
}
};
}
For more details, please check the Java config section of the reference documentation. Please note that this requires Spring Batch v4.1+.
I am using AbstractRoutingDataSource to create multitenancy in my application. I noticed that after a few redeployments of the webapp from my IDE I eventually get the MySQL error "Too many connections".
After further investigations, I found out that when I run the MySQL command show processlist;, I see that the open connection amount is increased by 10 after each deployment, which probably means that the connection pool is somehow still alive somewhere.
Before I used AbstractRoutingDataSource I used the default spring datasource configuration (using application.properties) and it worked fine.
Here's the Multitenant configuration class:
import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import java.util.stream.Collectors;
/**
* Created by Alon Segal on 16/03/2017.
*/
#Configuration
public class MultitenantConfiguration {
#Autowired
private DataSourceProperties properties;
/**
* Defines the data source for the application
*
* #return
*/
#Bean
#ConfigurationProperties(
prefix = "spring.datasource"
)
public DataSource dataSource() {
//Creating datasources map "resolvedDataSources" here
MultitenantDataSource dataSource = new MultitenantDataSource();
dataSource.setDefaultTargetDataSource(defaultDataSource());
dataSource.setTargetDataSources(resolvedDataSources);
// Call this to finalize the initialization of the data source.
dataSource.afterPropertiesSet();
return dataSource;
}
/**
* Creates the default data source for the application
*
* #return
*/
private DataSource defaultDataSource() {
.
.
.
}
}
And the datasource class:
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* Created by Alon Segal on 16/03/2017.
*/
public class MultitenantDataSource extends AbstractRoutingDataSource {
#Override
protected Object determineCurrentLookupKey() {
return TenantContext.getCurrentTenant();
}
}
I also tried to use #Bean(destroyMethod = "close") but there is not close method defined on AbstractRoutingDataSource.
I searched everywhere but could not find and answer. Can someone help me understand what's preventing the connection pool from being released between redeployments?
Thanks in advance.
Ok so I eventually solved the issue by giving up on using Spring's AbstractRoutingDataSource and instead using Hibernate's mechanism for multitenancy based on the solution that can be found in this article.
Long story short
You need to do 3 steps:
Step 1: Create a CurrentTenantIdentifierResolver
#Component
public class TenantIdentifierResolver implements CurrentTenantIdentifierResolver {
#Override
public String resolveCurrentTenantIdentifier() {
String tenantId = TenantContext.getCurrentTenant();
if (tenantId != null) {
return tenantId;
}
return DEFAULT_TENANT_ID;
}
#Override
public boolean validateExistingCurrentSessions() {
return true;
}
}
Step 2: Create a MultiTenantConnectionProvider
#Component
public class MultiTenantConnectionProviderImpl implements MultiTenantConnectionProvider {
#Autowired
private DataSource dataSource;
#Override
public Connection getAnyConnection() throws SQLException {
return dataSource.getConnection();
}
#Override
public void releaseAnyConnection(Connection connection) throws SQLException {
connection.close();
}
#Override
public Connection getConnection(String tenantIdentifie) throws SQLException {
String tenantIdentifier = TenantContext.getCurrentTenant();
final Connection connection = getAnyConnection();
try {
if (tenantIdentifier != null) {
connection.createStatement().execute("USE " + tenantIdentifier);
} else {
connection.createStatement().execute("USE " + DEFAULT_TENANT_ID);
}
}
catch ( SQLException e ) {
throw new HibernateException(
"Could not alter JDBC connection to specified schema [" + tenantIdentifier + "]",
e
);
}
return connection;
}
#Override
public void releaseConnection(String tenantIdentifier, Connection connection) throws SQLException {
try {
connection.createStatement().execute( "USE " + DEFAULT_TENANT_ID );
}
catch ( SQLException e ) {
throw new HibernateException(
"Could not alter JDBC connection to specified schema [" + tenantIdentifier + "]",
e
);
}
connection.close();
}
#SuppressWarnings("rawtypes")
#Override
public boolean isUnwrappableAs(Class unwrapType) {
return false;
}
#Override
public <T> T unwrap(Class<T> unwrapType) {
return null;
}
#Override
public boolean supportsAggressiveRelease() {
return true;
}
}
Step 3: Wire it up
#Configuration
public class HibernateConfig {
#Autowired
private JpaProperties jpaProperties;
#Bean
public JpaVendorAdapter jpaVendorAdapter() {
return new HibernateJpaVendorAdapter();
}
#Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource,
MultiTenantConnectionProvider multiTenantConnectionProviderImpl,
CurrentTenantIdentifierResolver currentTenantIdentifierResolverImpl) {
Map<String, Object> properties = new HashMap<>();
properties.putAll(jpaProperties.getHibernateProperties(dataSource));
properties.put(Environment.MULTI_TENANT, MultiTenancyStrategy.SCHEMA);
properties.put(Environment.MULTI_TENANT_CONNECTION_PROVIDER, multiTenantConnectionProviderImpl);
properties.put(Environment.MULTI_TENANT_IDENTIFIER_RESOLVER, currentTenantIdentifierResolverImpl);
LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
em.setDataSource(dataSource);
em.setPackagesToScan("com.autorni");
em.setJpaVendorAdapter(jpaVendorAdapter());
em.setJpaPropertyMap(properties);
return em;
}
}