I need to create a multitenanacy application with ability to switch between schemas inside my java-code (not based on a user request).
I've read articles:
https://fizzylogic.nl/2016/01/24/make-your-spring-boot-application-multi-tenant-aware-in-2-steps/
http://www.greggbolinger.com/tenant-per-schema-with-spring-boot/
Solution works fine, when the schema is passed in Rest-request.
However I need to implement the following logic:
public void compare(String originalSchema, String secondSchema){
TenantContext.setCurrentTenant(originalSchema);
List<MyObject> originalData = myRepository.findData();
TenantContext.setCurrentTenant(secondSchema);
List<MyObject> migratedData = myRepository.findData();
}
The point is, that connection is not switched, when I manually set up TenenantContext. MultiTenantConnectionProviderImpl.getConnection is invoked only on the first call to my repository.
#Component
public class MultiTenantConnectionProviderImpl implements MultiTenantConnectionProvider {
#Override
public Connection getConnection(String tenantIdentifier) throws SQLException {
final Connection connection = getAnyConnection();
try {
connection.createStatement().execute( "ALTER SESSION SET CURRENT_SCHEMA = " + tenantIdentifier );
}
catch ( SQLException e ) {
throw new HibernateException(
"Could not alter JDBC connection to specified schema [" + tenantIdentifier + "]",e);
}
return connection;
}
}
Is it possible to force switching sessions?
Found a hard-coded solution.
#Service
public class DatabaseSessionManager {
#PersistenceUnit
private EntityManagerFactory entityManagerFactory;
public void bindSession() {
if (!TransactionSynchronizationManager.hasResource(entityManagerFactory)) {
EntityManager entityManager = entityManagerFactory.createEntityManager();
TransactionSynchronizationManager.bindResource(entityManagerFactory, new EntityManagerHolder(entityManager));
}
}
public void unbindSession() {
EntityManagerHolder emHolder = (EntityManagerHolder) TransactionSynchronizationManager
.unbindResource(entityManagerFactory);
EntityManagerFactoryUtils.closeEntityManager(emHolder.getEntityManager());
}
}
Each block, loading data in a new tenantContext should execute the following:
databaseSessionManager.unbindSession();
TenantContext.setCurrentTenant(schema);
databaseSessionManager.bindSession();
//execute selects
Well, you need it
public interface Service {
List<MyObject> myObjects();
}
#Service
#Transactional(propagation = Propagation.REQUIRES_NEW)
public class ServiceImpl implements Service {
#Autowired
private MyRepository myRepository;
#Override
public List<MyObject> myObjects() {
return myRepository.findData();
}
}
#Service
public class AnotherService() {
#Autowired
private Service service;
public void compare(String originalSchema, String secondSchema){
TenantContext.setCurrentTenant(originalSchema);
List<MyObject> originalData = service.myObjects();
TenantContext.setCurrentTenant(secondSchema);
List<MyObject> migratedData = service.myObjects();
}
}
Try using
spring.jpa.open-in-view=false
in your application.properties file.
More info on this
What is this spring.jpa.open-in-view=true property in Spring Boot?
Hope this helps..
Related
I have set the following property
server.servlet.session.timeout=30s
in my application properties but the session time out is not triggerd.
but after setting
server.servlet.session.cookie.max-age=30s
the session time out got trigger but following code for updating logout time is not getting triggerd.
#Component
public class LogoutListener implements ApplicationListener<SessionDestroyedEvent> {
#Override
public void onApplicationEvent(SessionDestroyedEvent event)
{
List<SecurityContext> lstSecurityContext = event.getSecurityContexts();
UserDetails ud;
for (SecurityContext securityContext : lstSecurityContext)
{
ud = (UserDetails) securityContext.getAuthentication().getPrincipal();
us.findAllUsersByEmail(ud.getUsername()).get(0).setLastLogout(LocalDateTime.now());
System.out.println("lastloginspec : " + ud.getUsername() + " : 00 : " + LocalDateTime.now());
}
}
}
#Bean
public ServletListenerRegistrationBean<HttpSessionEventPublisher> httpSessionEventPublisher() {
return new ServletListenerRegistrationBean<HttpSessionEventPublisher>(new HttpSessionEventPublisher());
}
Could any one Help me out ?
I have implemented the session listener by following way.
Create a custom http session listener.
#Component
public class CustomHttpSessionListener implements HttpSessionListener{
private static final Logger LOG= LoggerFactory.getLogger(Test.class);
#Override
public void sessionCreated(HttpSessionEvent se) {
LOG.info("New session is created.");
UserPrincipal principal = (UserPrincipal) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}
#Override
public void sessionDestroyed(HttpSessionEvent se) {
LOG.info("Session destroyed.");
UserPrincipal principal = (UserPrincipal) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}}
Invoke new ServletListenerRegistrationBean and add CustomHttpListener to it and annotate it as #Bean.
#Autowired private CustomHttpSessionListener customHttpSessionListener;
#Bean
public ServletListenerRegistrationBean<CustomSessionListner>sessionListenerWithMetrics() { ServletListenerRegistrationBean<CustomSessionListner>
listenerRegBean = new ServletListenerRegistrationBean<>();
listenerRegBean.setListener(customHttpSessionListener);
return listenerRegBean;
}
Adding a property to application.properties
server.servlet.session.timeout = 15m
This is not a full answer, but a step to isolate and troubleshoot. Replace your LogoutListener with and see when you start the application if it is printing any events. If it is not printing your issue is not specific SessionDestroyedEvent instead generic to your listener.
#Component
public class LogoutListener
implements ApplicationListener<ApplicationEvent> {
#Override
public void onApplicationEvent(ApplicationEvent event)
{
System.out.println("event caught at LogoutListener: " + event);
}
}
And also add this to application.properties to see if event is fired as it should log Publishing event:
logging.level.org.springframework.security.web.session.HttpSessionEventPublisher=DEBUG
I have this test class:
#RunWith(SpringRunner.class)
#ContextConfiguration(classes = { CrimeServiceDBImpl.class, CrimeController.class, Crime.class })
#ComponentScan("com.springmiddleware")
#EntityScan(basePackages = {"com.springmiddleware.entities"})
#DataJpaTest
#AutoConfigureTestDatabase(replace = Replace.NONE)
#EnableJpaRepositories("com.springmiddleware")
public class TestCrimeServiceDB {
#Autowired
private CrimeServiceDBImpl service = new CrimeServiceDBImpl();
#Test
public void getAll() {
try {
List<Crime> list = this.service.getAllCrimes();
assertTrue(list.size()!=0);
} catch (IOException e) {
e.printStackTrace();
}
}
}
The method getAllCrimes() from the service class does just this:
#Service
public class CrimeServiceDBImpl implements CrimeService{
#Autowired
private CrimeRepository repository;
private List<Crime> list = new ArrayList<Crime>();
public CrimeServiceDBImpl() {
list = UtilityMethods.readFromCSV();
};
#Override
public List<Crime> getAllCrimes() throws IOException {
repository.saveAll(list);
return this.repository.findAll();
}
If I call this method when running the application, it correctly add all my objects to the database, but when it's called from the test it doesn't add anything, but no exception is thrown.
Which database are you using? Do you mean the data is not persisted in the database after the test has finished? That's because a test always perform a rollback/cleanup when it has finished its work.
Spring #Autowire field is null even though it works fine in other classes successfully.
public class SendRunner implements Runnable {
private String senderAddress;
#Autowired
private SubscriberService subscriberService;
public SendRunner(String senderAddress) {
this.senderAddress = senderAddress;
}
#Override
public void run() {
sendRequest();
}
private void sendRequest() {
try {
HashMap<String, String> dataMap = new HashMap<>();
dataMap.put("subscriberId", senderAddress);
HttpEntity<?> entity = new HttpEntity<Object>(dataMap, httpHeaders);
Subscriber subscriber = subscriberService.getSubscriberByMsisdn(senderAddress);
} catch (Exception e) {
logger.error("Error occurred while trying to send api request", e);
}
}
Also this class is managed as a bean in the dispatcher servlet :
<bean id="SendRunner" class="sms.dating.messenger.connector.SendRunner">
</bean>
In here i'm getting a null pointer exception for subscriberService. What would be the possible reason for this? Thanks in advance.
Can you please try with below code snippet
#Configuration
public class Someclass{
#Autowired
private SubscriberService subscriberService;
Thread subscriberThread = new Thread() {
#Override
public void run() {
try {
HashMap<String, String> dataMap = new HashMap<>();
dataMap.put("subscriberId", senderAddress);
HttpEntity<?> entity = new HttpEntity<Object>(dataMap, httpHeaders);
Subscriber subscriber = subscriberService.getSubscriberByMsisdn(senderAddress);
} catch (Exception e) {
logger.error("Error occurred while trying to send api request", e);
}
}
};
}
Can you please annotate your SendRunner class with #Component or #Service and include the SendRunner package in componentscanpackage
Your bean not in Spring Managed context, below can be the reasons.
Package sms.dating.messenger.connector not in Component scan.
You are moving out of the Spring context by creating an object with new (see below),
this way you will not get the autowired fields.
SendRunner sendRunner = new SendRunner () ,
sendRunner.sendRequest();
Just check how I implement. Hope this will help.
#RestController
public class RestRequest {
#Autowired
SendRunner sendRunner;
#RequestMapping("/api")
public void Uri() {
sendRunner.start();
}
}
SendRunner class
#Service
public class SendRunner extends Thread{
#Autowired
private SubscriberService subscriberService;
#Override
public void run() {
SendRequest();
}
private void SendRequest() {
System.out.println("Object is " + subscriberService);
String senderAddress = "address";
subscriberService.getSubscriberByMsisdn(senderAddress);
}
}
Below are the logs printed when I hit the REST api.
Object is com.example.demo.SubscriberService#40f33492
I would like to retrieve the value of a property in file application.properties in my service layer of my application, the value of setVersion is null
version=5.4.3
and the function for recovery the version
#Override
public ProductDto getVersionApp() {
ProductDto dto = new ProductDto();
Properties prop = new Properties();
try {
prop.load(new FileInputStream("/concerto-rest-api/src/main/resources/application.properties"));
dto.setVersion(prop.getProperty("version"));
LOG.info("version ",prop.getProperty("version"));
} catch (IOException ex) {}
return dto;
}
You can use #Value("${version}") in you service, provided you service is a spring bean.
If you are using the spring-boot framework, there are several ways you can get that property.
First:
#SpringBootApplication
public class SpringBoot01Application {
public static void main(String[] args) {
ConfigurableApplicationContext context=SpringApplication.run(SpringBoot01Application.class, args);
String str1=context.getEnvironment().getProperty("version");
System.out.println(str1);
}
}
Second:
#Component
public class Student {
#Autowired
private Environment env;
public void speak() {
System.out.println("=========>" + env.getProperty("version"));
}
}
Third:
#Component
#PropertySource("classpath:jdbc.properties")//if is application.properties,then you don't need to write #PropertyScource("application.properties")
public class Jdbc {
#Value("${jdbc.user}")
private String user;
#Value("${jdbc.password}")
private String password;
public void speack(){
System.out.println("username:"+user+"------"+"password:"+password);
}
}
Eclipse Link Multitenancy is not working properly.
Example Entity (the schema is being created by liquibase):
#Entity
#Table(name = "ENTITIES")
#Multitenant(MultitenantType.SINGLE_TABLE)
#TenantDiscriminatorColumn(name = "TENANT_ID", contextProperty = "eclipselink.tenant-id")
public class EntityClass
To set the multitenancy property on entity managers I use an aspect, like following:
#Around("execution(* javax.persistence.EntityManagerFactory.*(..))")
public Object invocate(ProceedingJoinPoint joinPoint) throws Throwable {
final Object result = joinPoint.proceed();
if (result instanceof EntityManager) {
EntityManager em = (EntityManager) result;
final String tenantId = TenantContext.getCurrentTenantId();
LOG.debug("Set EntityManager property for tenant {}.", tenantId);
em.setProperty(EntityManagerProperties.MULTITENANT_PROPERTY_DEFAULT,
tenantId);
return em;
}
return result;
}
When I start the Spring Boot application this works perfectly. To have tenant information available during integration tests, I defined an annotation:
#Target({ElementType.TYPE, ElementType.METHOD})
#Retention(RetentionPolicy.RUNTIME)
public #interface AsTenant {
String value();
}
To bind this value, I use a TestExecutionListener:
#Override
public void beforeTestMethod(TestContext testContext) throws Exception {
final Method testMethod = testContext.getTestMethod();
final AsTenant asTenantAnnotation = testMethod
.getAnnotation(AsTenant.class);
if (asTenantAnnotation != null) {
TenantContext.setCurrentTenantId(asTenantAnnotation.value());
}
}
By debugging I can clearly say that the TestExectionListener is called before any EM is created and that the property is properly set for the EMs. When persisting anything to the database, Eclipse Link does not set a value for the column.
Maybe anybody can help me out with this, I have no Idea why EclipseLink Multitenancy is not working.
Ok, I got it working. If anybody ever faces a similar problem, here is my solution to it.
If using transactions, the context property for the tenant discrimination has to be set after the transaction is started (http://www.eclipse.org/eclipselink/documentation/2.5/solutions/multitenancy002.htm).
EntityManager em = createEntityManager(MULTI_TENANT_PU);
em.getTransaction().begin();
em.setProperty(EntityManagerProperties.MULTITENANT_PROPERTY_DEFAULT, "my_id");
To realise this in Spring Boot/Data environment, I customized Spring's JpaTransactionManager. This stays in addition to the Aspect in the Question, since there is not Transaction for SELECT queries.
public class MultitenantJpaTransactionManager extends JpaTransactionManager {
/* (non-Javadoc)
* #see org.springframework.orm.jpa.JpaTransactionManager#doBegin(java.lang.Object, org.springframework.transaction.TransactionDefinition)
*/
#Override
protected void doBegin(Object transaction, TransactionDefinition definition) {
super.doBegin(transaction, definition);
final EntityManagerHolder emHolder = (EntityManagerHolder) TransactionSynchronizationManager.getResource(getEntityManagerFactory());
final EntityManager em = emHolder.getEntityManager();
final String tenantId = TenantContext.getCurrentTenantId();
if (tenantId != null) {
em.setProperty(EntityManagerProperties.MULTITENANT_PROPERTY_DEFAULT, tenantId);
}
}
}
This is easily wired via JpaConfiguration:
/**
* Configures Eclipse Link as JPA Provider.
*/
#Configuration
#EnableTransactionManagement
#AutoConfigureAfter({ DataSourceAutoConfiguration.class })
public class JpaConfiguration extends JpaBaseConfiguration {
#Bean
#Override
public PlatformTransactionManager transactionManager() {
return new MultitenantJpaTransactionManager();
}
#Override
protected AbstractJpaVendorAdapter createJpaVendorAdapter() {
EclipseLinkJpaVendorAdapter adapter = new EclipseLinkJpaVendorAdapter();
return adapter;
}
#Override
protected Map<String, Object> getVendorProperties() {
HashMap<String, Object> properties = new HashMap<String, Object>();
properties.put(PersistenceUnitProperties.WEAVING, detectWeavingMode());
return properties;
}
private String detectWeavingMode() {
return InstrumentationLoadTimeWeaver.isInstrumentationAvailable()
? "true" : "static";
}
}
Disclaimer: This does not answer the above query but provides an alternative.
Using bytecode instrumentation, I have created a java example on Multi-Tenancy (Table per Tenant) with Eclipse Link and Spring Data. This idea is chosen to utilize the complete power of Spring Data.
One can execute MultiTenantTest to see it working.
The idea is open-sourced and is available at Maven Central
Steps:
1.Include dependency
<dependency>
<groupId>org.bitbucket.swattu</groupId>
<artifactId>jpa-agent</artifactId>
<version>2.0.2</version>
</dependency>
2.Create a class as shown below. Package, Class and method has to be exactly same.
package org.swat.jpa.base;
import javax.persistence.EntityManager;
public class EntityManagerFactoryListener {
/**
* This method is called by JPA Agent.
*
* #param entityManager the entity manager
*/
public static void afterCreateEntityManager(EntityManager entityManager) {
//Business logic to set appropriate values in entityManager
final String tenantId = TenantContext.getCurrentTenantId();
if (tenantId != null) {
em.setProperty(EntityManagerProperties.MULTITENANT_PROPERTY_DEFAULT, tenantId);
}
}
}
3.Add javaagent when starting java
-javaagent:{path-to-jpa-agent-jar}