I am using the below code to connect with cassandra using spring data. But it's painful to create connection everytime.
try {
cluster = Cluster.builder().addContactPoint(host).build();
session = cluster.connect("digitalfootprint");
CassandraOperations cassandraOps = new CassandraTemplate(session);
Select usersQuery = QueryBuilder.select(userColumns).from("Users");
usersQuery.where(QueryBuilder.eq("username", username));
List<Users> userResult = cassandraOps
.select(usersQuery, Users.class);
userList = userResult;
} catch(Exception e) {
e.printStackTrace();
} finally {
cluster.close();
}
Is there any way we can have a common static connection or utility kind of stuff. I am using this in web application where lots of CRUD operation will be there. SO it will be painful to repeat the code every where.
Just instantiate appropriate beans at the startup time of your spring web application. An example would be :
#Configuration
public class CassandraConfig {
#Bean
public CassandraClusterFactoryBean cluster() throws UnknownHostException {
CassandraClusterFactoryBean cluster = new CassandraClusterFactoryBean();
cluster.setContactPoints(InetAddress.getLocalHost().getHostName());
cluster.setPort(9042);
return cluster;
}
#Bean
public CassandraMappingContext mappingContext() {
return new BasicCassandraMappingContext();
}
#Bean
public CassandraConverter converter() {
return new MappingCassandraConverter(mappingContext());
}
#Bean
public CassandraSessionFactoryBean session() throws Exception {
CassandraSessionFactoryBean session = new CassandraSessionFactoryBean();
session.setCluster(cluster().getObject());
session.setKeyspaceName("mykeyspace");
session.setConverter(converter());
session.setSchemaAction(SchemaAction.NONE);
return session;
}
#Bean
public CassandraOperations cassandraTemplate() throws Exception {
return new CassandraTemplate(session().getObject());
}
}
Now Inject or Autowire CassandraOperations bean , any time you want
Related
I am trying to seed my multiple tenant (single database, multiple schemas) system with data but running into an issue that wasn't present when I was using the same code with a single database. I fully expect during my research that I have missed something obvious.
Each schema will contain the exact same table structure.
Here is my Tenant Context
public class TenantContext {
public static final String DEFAULT_TENANT_IDENTIFIER = "public";
private static final ThreadLocal<String> TENANT_IDENTIFIER = new ThreadLocal<>();
public static void setTenant(String tenantIdentifier) {
TENANT_IDENTIFIER.set(tenantIdentifier);
}
public static void reset(String tenantIdentifier) {
TENANT_IDENTIFIER.remove();
}
#Component
public static class TenantIdentifierResolver implements CurrentTenantIdentifierResolver {
#Override
public String resolveCurrentTenantIdentifier() {
String currentTenantId = TENANT_IDENTIFIER.get();
return currentTenantId != null ?
currentTenantId :
DEFAULT_TENANT_IDENTIFIER;
}
#Override
public boolean validateExistingCurrentSessions() {
return false;
}
}
}
And my HibernateConfig
#Configuration
public class HibernateConfig {
#Autowired
private JpaProperties jpaProperties;
#Bean
public JpaVendorAdapter jpaVendorAdapter() {
return new HibernateJpaVendorAdapter();
}
#Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource,
MultiTenantConnectionProvider multiTenantConnectionProvider, CurrentTenantIdentifierResolver currentTenantIdentifierResolver) {
Map<String, Object> jpaPropertiesMap = new HashMap<>();
jpaPropertiesMap.putAll(jpaProperties.getProperties());
jpaPropertiesMap.put(Environment.MULTI_TENANT, MultiTenancyStrategy.SCHEMA);
jpaPropertiesMap.put(Environment.MULTI_TENANT_CONNECTION_PROVIDER, multiTenantConnectionProvider);
jpaPropertiesMap.put(Environment.MULTI_TENANT_IDENTIFIER_RESOLVER, TenantContext.TenantIdentifierResolver.class);
LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
entityManagerFactoryBean.setDataSource(dataSource);
entityManagerFactoryBean.setPackagesToScan(UppStudentAppBeApplication.class.getPackage().getName());
entityManagerFactoryBean.setJpaVendorAdapter(jpaVendorAdapter());
entityManagerFactoryBean.setJpaPropertyMap(jpaPropertiesMap);
return entityManagerFactoryBean;
}
}
And my TenantConenctionProvider
#Component
public class TenantConnectionProvider implements MultiTenantConnectionProvider {
private static Logger logger = LoggerFactory.getLogger(TenantConnectionProvider.class);
#Autowired
private DataSource dataSource;
public TenantConnectionProvider(DataSource dataSource) {
this.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 tenantIdentifier) throws SQLException {
logger.info("Get connection for tenant " + String.join(":", tenantIdentifier ));
final Connection connection = getAnyConnection();
try {
//connection.createStatement().execute( String.format("SET SCHEMA \"%s\";", tenantIdentifier));
connection.setSchema(tenantIdentifier);
} 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( String.format("SET SCHEMA \"%s\";", TenantContext.DEFAULT_TENANT_IDENTIFIER) );
connection.setSchema(TenantContext.DEFAULT_TENANT_IDENTIFIER);
} catch ( SQLException e ) {
throw new HibernateException(
"Could not alter JDBC connection to specified schema [" +
tenantIdentifier + "]",
e
);
}
releaseAnyConnection(connection);
}
#Override
public boolean supportsAggressiveRelease() {
return false;
}
#Override
public boolean isUnwrappableAs(Class unwrapType) {
return false;
}
#Override
public <T> T unwrap(Class<T> unwrapType) {
return null;
}
}
I call my seeding class that builds out my tenants and schemas using flyway migration.
I then try to loop through the saved tenants switching the TenantContext. Which when debugging appears to work. However when I try and do anything with the repo I get the following error.
o.h.engine.jdbc.spi.SqlExceptionHelper : ERROR: column campus0_.createdat does not exist
Hint: Perhaps you meant to reference the column "campus0_.created_at".
Position: 32
As I said earlier this worked fine previously when it was a single database and schema. I am not 100% sure on where I have gone wrong. Am I supposed to register the schemas some how? If so how can I onboard new tenants without redeploying? Should I use a custom query at this stage that uses the schema in the repo?
Thank you in advance for any help or advice.
EDIT 1
So I have now got past my initial hurdle by checking the hibernate properties so by changing the hibernate config as follows
#Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource,
MultiTenantConnectionProvider multiTenantConnectionProvider,
HibernateProperties hibernateProperties) {
Map<String, Object> jpaPropertiesMap = hibernateProperties.determineHibernateProperties(jpaProperties.getProperties(), new HibernateSettings());
//jpaPropertiesMap.putAll(jpaProperties.getProperties());
jpaPropertiesMap.put(Environment.MULTI_TENANT, MultiTenancyStrategy.SCHEMA);
jpaPropertiesMap.put(Environment.MULTI_TENANT_CONNECTION_PROVIDER, multiTenantConnectionProvider);
jpaPropertiesMap.put(Environment.MULTI_TENANT_IDENTIFIER_RESOLVER, TenantContext.TenantIdentifierResolver.class);
LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
entityManagerFactoryBean.setDataSource(dataSource);
entityManagerFactoryBean.setPackagesToScan(UppStudentAppBeApplication.class.getPackage().getName());
entityManagerFactoryBean.setJpaVendorAdapter(jpaVendorAdapter());
entityManagerFactoryBean.setJpaPropertyMap(jpaPropertiesMap);
return entityManagerFactoryBean;
}
This has now removed the above naming error. However now it is saving to my default schema rather than the schema set in the TenantIdentifierResolver.
Have you implemented AsyncHandlerInterceptor - interceptor of Spring. Should be registered also in WebMvcConfigurer.
#Component
public class TenantRequestInterceptor implements AsyncHandlerInterceptor{
private SecurityDomain securityDomain;
public TenantRequestInterceptor(SecurityDomain securityDomain) {
this.securityDomain = securityDomain;
}
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
return Optional.ofNullable(request)
.map(req -> securityDomain.getTenantIdFromJwt(req))
.map(tenant -> setTenantContext(tenant))
.orElse(false);
}
#Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
TenantContext.reset();
}
private boolean setTenantContext(String tenant) {
TenantContext.setCurrentTenant(tenant);
return true;
}
}
This is important, because here you fill TenantContext with tenant.
Have you debuged method getConnection(String tenantIdentifier) what value is as tenantIdentifier?
My application is based on spring-boot 2.1.6 equipped with spring-batch (chunks approach) and spring-integration to handle the SFTP.
High level functionality is to fetch data from DB, generate a text file then send it through SFTP and this task run every 30 mins.
This application already running in production for some time, but if I see the logs there are error about ssh_msg_disconnect 11 idle connection. It will keep like that until I restart the app.
Below is my application code :
SftpConfig.java
#Configuration
public class SftpConfig {
#Autowired
ApplicationProperties applicationProperties;
#Bean
public SessionFactory<LsEntry> sftpSessionFactory() {
final DefaultSftpSessionFactory factory = new DefaultSftpSessionFactory(true);
factory.setHost(applicationProperties.getSftp().getHost());
factory.setUser(applicationProperties.getSftp().getUser());
factory.setPassword(applicationProperties.getSftp().getPass());
factory.setAllowUnknownKeys(true);
return new CachingSessionFactory<>(factory);
}
#Bean
#ServiceActivator(inputChannel = "toSftpChannel", adviceChain = "retryAdvice")
public MessageHandler handler() {
final SftpMessageHandler handler = new SftpMessageHandler(this.sftpSessionFactory());
handler.setRemoteDirectoryExpression(new LiteralExpression(applicationProperties.getSftp().getPath()));
handler.setFileNameGenerator((final Message<?> message) -> {
if (message.getPayload() instanceof File) {
return ((File) message.getPayload()).getName();
} else {
throw new IllegalArgumentException("File expected as payload.");
}
});
return handler;
}
#Bean
public RequestHandlerRetryAdvice retryAdvice() {
final RequestHandlerRetryAdvice advice = new RequestHandlerRetryAdvice();
final RetryTemplate retryTemplate = new RetryTemplate();
final SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
retryPolicy.setMaxAttempts(NumberConstants.FIVE);
retryTemplate.setRetryPolicy(retryPolicy);
advice.setRetryTemplate(retryTemplate);
return advice;
}
#MessagingGateway
public interface UploadGateway {
#Gateway(requestChannel = "toSftpChannel")
void upload(File file);
}
}
step for sending file to sftp
#Autowired
UploadGateway uploadGateway;
private boolean uploadToSharedFolderSuccess(final PaymentStatus paymentStatus, final String strLocalTmpPath) {
try {
final File fileLocalTmpFullPath = new File(strLocalTmpPath);
uploadGateway.upload(fileLocalTmpFullPath);
} catch (final Exception e) {
paymentStatus.setStatus(ProcessStatus.ERROR.toString());
paymentStatus.setRemark(StringUtil.appendIfNotEmpty(paymentStatus.getRemark(),
"Error during upload to shared folder - " + e.getMessage()));
}
return !StringUtils.equalsIgnoreCase(ProcessStatus.ERROR.toString(), paymentStatus.getStatus());
}
From the error, I know that seems like I opened too many connection. But I'm not sure how to check if the connection are closed every end of the spring-batch.
If you don't wrap the session factory in a CachingSessionFactory, the session will be closed after each use.
#Bean
public DefaultSftpSessionFactory sftpSessionFactory() {
final DefaultSftpSessionFactory factory = new DefaultSftpSessionFactory(true);
factory.setHost(applicationProperties.getSftp().getHost());
factory.setUser(applicationProperties.getSftp().getUser());
factory.setPassword(applicationProperties.getSftp().getPass());
factory.setAllowUnknownKeys(true);
return factory;
}
I am trying to integrate Spring Boot and Shiro. When I tried to call SecurityUtils.getSubject() in one of my controllers, an exception occurred:
org.apache.shiro.UnavailableSecurityManagerException: No SecurityManager accessible to the calling code, either bound to the org.apache.shiro.util.ThreadContext or as a vm static singleton. This is an invalid application configuration.
I just followed some tutorials and docs to configure Shiro and here is my ShiroConfig class:
#Configuration
public class ShiroConfig {
#Bean
public Realm realm() {
return new UserRealm();
}
#Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName(PasswordEncoder.getALGORITHM());
hashedCredentialsMatcher.setHashIterations(PasswordEncoder.getITERATION());
return hashedCredentialsMatcher;
}
#Bean
public UserRealm shiroRealm() {
UserRealm userRealm = new UserRealm();
userRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return userRealm;
}
#Bean
public SessionsSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(shiroRealm());
return securityManager;
}
#Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setUsePrefix(true);
return defaultAdvisorAutoProxyCreator;
}
#Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition definition = new DefaultShiroFilterChainDefinition();
definition.addPathDefinition("/login", "anon");
definition.addPathDefinition("/register", "anon");
definition.addPathDefinition("/api/**", "user");
return definition;
}
}
And this is the code which caused exception:
#PostMapping("/login")
#ResponseBody
public Object login(#RequestParam("username") String username,
#RequestParam("password") String password) {
if (username.equals("") || password.equals("")) {
return "please provide complete information";
}
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
Subject subject = SecurityUtils.getSubject(); // this line caused exception
...
}
I am very confused about this exception. Could anyone help?
EDIT
I am using Spring Boot 2.1.6.RELEASE and shiro-spring-boot-starter 1.4.0.
Are you using the shiro-spring-boot-web-starter dependency instead of the shiro-spring-boot-starter dependency?
It looks like that is required for spring boot web applications according to this doc.
https://shiro.apache.org/spring-boot.html#Spring-WebApplications
I am trying yo implement a multi tenant application using hibernate's MultiTenantConnectionProvider and CurrentTenantIdentifierResolver. I am not sure how the pooling is managed by hibernate and whether it is a good practice to leave it to Hibernate. Can I use C3P0 in this multi tenant application.
Here is my initial code:
#Bean(name = "dataSources" )
public Map<String, DataSource> dataSources() {
Map<String, DataSource> result = new HashMap<>();
for (DataSourceProperties dsProperties : this.multiTenantProperties.getDataSources()) {
DataSourceBuilder factory = DataSourceBuilder
.create()
.url(dsProperties.getUrl())
.username(dsProperties.getUsername())
.password(dsProperties.getPassword())
.driverClassName(dsProperties.getDriverClassName());
result.put(dsProperties.getTenantId(), factory.build());
}
return result;
}
#Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean(MultiTenantConnectionProvider DataSourceMultiTenantConnectionProviderImpl ,
CurrentTenantIdentifierResolver currentTenantIdentifierResolver) {
Map<String, Object> hibernateProps = new LinkedHashMap<>();
hibernateProps.putAll(this.jpaProperties.getProperties());
hibernateProps.put(Environment.MULTI_TENANT, MultiTenancyStrategy.DATABASE);
hibernateProps.put(Environment.MULTI_TENANT_CONNECTION_PROVIDER, multiTenantConnectionProvider);
hibernateProps.put(Environment.MULTI_TENANT_IDENTIFIER_RESOLVER, TenantIdentifierResolverImpl );
// No dataSource is set to resulting entityManagerFactoryBean
LocalContainerEntityManagerFactoryBean result = new LocalContainerEntityManagerFactoryBean();
result.setPackagesToScan(new String[] { Test.class.getPackage().getName() });
result.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
result.setJpaPropertyMap(hibernateProps);
return result;
}
This is the Connection provider configured in hibernate
public class DataSourceMultiTenantConnectionProviderImpl extends
AbstractDataSourceBasedMultiTenantConnectionProviderImpl {
private static final long serialVersionUID = 1L;
#Autowired
private Map<String, DataSource> dataSources;
#Override
protected DataSource selectAnyDataSource() {
return this.dataSources.values().iterator().next();
}
#Override
protected DataSource selectDataSource(String tenantIdentifier) {
return this.dataSources.get(tenantIdentifier);
}
}
This is the tenant resolver provided to hibernate
public class TenantIdentifierResolverImpl implements
CurrentTenantIdentifierResolver {
private static String DEFAULT_TENANT_ID = "tenant_1";
#Override
public String resolveCurrentTenantIdentifier() {
String currentTenantId = TenantContext.getTenantId();
return (currentTenantId != null) ? currentTenantId : DEFAULT_TENANT_ID;
}
#Override
public boolean validateExistingCurrentSessions() {
return true;
}
}
How to handle connection pooling here?
You might need to change your datasources bean to this:
#Bean(name = "dataSources" )
public Map<String, ComboPooledDataSource> dataSources() throws PropertyVetoException {
Map<String, ComboPooledDataSource> result = new HashMap<>();
for (DataSourceProperties dsProperties : this.multiTenantProperties.getDataSources()) {
ComboPooledDataSource ds = new ComboPooledDataSource();
ds.setDriverClass(dsProperties.getDriverClassName());
ds.setJdbcUrl(dsProperties.getUrl());
ds.setUser(dsProperties.getUsername());
ds.setPassword(dsProperties.getPassword());
ds.setInitialPoolSize(5);
ds.setMinPoolSize(1);
ds.setAcquireIncrement(1);
ds.setMaxPoolSize(5);
result.put(dsProperties.getTenantId(), ds);
}
return result;
}
And add this method to your DataSourceMultiTenantConnectionProviderImpl
#Override
public Connection getConnection(String tenantIdentifier) throws SQLException {
return this.dataSources.get(tenantIdentifier).getConnection();
}
This should be enough to start connection pooling using C3P0.
I have a multi-tenant mongoDB application and let's assume that
right connection to right database is chosen from tenant name from HTTP request header(i usage earlier prepared properties file with tenant name).
When application is started mongoDB is configuring and i don't have information about tenant, because none request to application hasn't been sent, so i don't know to which database i should be connect. Is a possibility that mongoDB connection to database would be configured dynamicly, when I try to get some data from mongo repository(then I have tenant name from HTTP request)?
MongoDbConfiguration:
#Configuration
public class MongoDbConfiguration {
private final MongoConnector mongoConnector;
#Autowired
public MongoDbConfiguration(MongoConnector mongoConnector) {
this.mongoConnector = mongoConnector;
}
#Bean
public MongoDbFactory mongoDbFactory() {
return new MultiTenantSingleMongoDbFactory(mongoConnector, new MongoExceptionTranslator());
}
#Bean
public MongoTemplate mongoTemplate() {
return new MongoTemplate(mongoDbFactory());
}
}
#Component
#Slf4j
public class MultiTenantMongoDbFactory extends SimpleMongoDbFactory {
private static final Logger logger = LoggerFactory.getLogger(MultiTenantMongoDbFactory.class);
private Map<String, DbConfig> tenantToDbConfig;
private Map<String, MongoDatabase> tenantToMongoDatabase;
#Autowired
public MultiTenantMongoDbFactory(
final #Qualifier("sibTenantContexts") Map<String, DbConfig> dbConfigs,
final SibEnvironment env) {
super(new MongoClientURI(env.getDefaultDatabase()));
this.tenantToDbConfig = dbConfigs;
// Initialize tenantToMongoDatabase map.
buildTenantDbs();
}
#Override
public MongoDatabase getDb() {
String tenantId = (!StringUtils.isEmpty(TenantContext.getId()) ? TenantContext.getId()
: SibConstant.DEFAULT_TENANT);
return this.tenantToMongoDatabase.get(tenantId);
}
/**
* Create tenantToMongoDatabase map.
*/
#SuppressWarnings("resource")
private void buildTenantDbs() {
log.debug("Building tenantDB configuration.");
this.tenantToMongoDatabase = new HashMap<>();
/*
* for each tenant fetch dbConfig and intitialize MongoClient and set it to
* tenantToMongoDatabase
*/
for (Entry<String, DbConfig> idToDbconfig : this.tenantToDbConfig.entrySet()) {
try {
this.tenantToMongoDatabase.put(idToDbconfig.getKey(),
new MongoClient(new MongoClientURI(idToDbconfig.getValue()
.getUri())).getDatabase(idToDbconfig.getValue()
.getDatabase()));
} catch (MongoException e) {
log.error(e.getMessage(), e.getCause());
}
}
}
}
In this, tenantToDbConfig is a bean which I have created at the time of application boot where I store DBConfiguration like (Url/database name) against every tenant. There is one default database which is required at boot time and for every request, I am expecting tenantId in request Header.