Multitenancy MultitenantService
Multitenant Service
As all JOnAS services, multitenant service could be activated in jonas.properties. When this service is enabled, all applications which have a tenantId (i.e. tenantId is set in jonas-web.xml or jonas-application.xml) will be configured as multitenant application :
- First, recover the tenantId and bind it. (If an application has not a tenandId, default tenantId is used. However, following operations are not applied)
- Then, rename JNDI names by adding a prefix wich is the tenantId
- Next, update persistence units by adding eclipselink properties to configure EJB entities as multitenant and propagate the tenantId
- And finally, customize the context root by adding the tenantId in the URL.
This video explains how configure a sample application as multitenant.
Default tenant ID
When multitenant service is enabled, each instance of an application related to a tenant must have a tenant identifier. This identifier will be used thereafter to customize differents components of the application (JNDI names, context root, ...). In case of a missing tenant identifier, the default tenantId will be affected. This default tenantId is "T0" and can be accessible by multitenantService.getDefaultTenantId();
Naming strategy
When an application is deployed in multitenant mode, we take the risk of having a conflict between bound names of each tenant. A solution is to add a prefix before each name. This prefix is the tenantId of the tenant which names are related. During the deployment, a prefix is defined following the syntax : Tx with x a number, and will be added to application's names by :
newNamingStrategies.add(ejb3Service.getNamingStrategy(prefix, oldNamingStrategy));
Example: MyInitializerBean will be T17MyInitializerBean.
In addition, as for versioning service, a virtual JNDI binding is made. It will remove the prefix and rebind the old name to the same object. Then, we will have 2 names (MyInitializeBean and T17MyInitializerBean) linked to the same object.
Propagation of the tenantId to eclipselink
To persist the tenantId in database, we have to add a property in persistence.xml file (cf Prototype). To automize the propagation of the tenantId to eclipselink we need to add this property automaticly when the application is added. Then, we will use the method persistenceUnitManager.addProperty("eclipselink.tenant-id", tenantId);.
EJB Entity configured as multitenant
Entities must be configured as multitenant to enable adding tenant-id in the database. To do this, we have to add @Multitenant annotation in each class but we need to do that automatically (when multitenant service is activated). A solution is to use a Session Customizer (cf http://wiki.eclipse.org/Customizing_the_EclipseLink_Application_(ELUG)). It is a simple class with only one method (customize) and take one parameter (Session session). In this method, we will set all entity classes as multitenant as follows :
public void customize(Session session) throws Exception {
Map<Class, ClassDescriptor> descs = session.getDescriptors();
// For each entity class ...
for(Map.Entry<Class, ClassDescriptor> desc : descs.entrySet()){
// Create a multitenant policy (Single table)
SingleTableMultitenantPolicy policy = new SingleTableMultitenantPolicy(desc.getValue());
// Tell that column descriminator is TENANT_ID (it will be added in the database)
policy.addTenantDiscriminatorField("eclipselink.tenant-id", new DatabaseField("TENANT_ID"));
// Add this policy in class derscriptor
desc.getValue().setMultitenantPolicy(policy);
}
} Obviously, we can do other things in this session customizer (than enable multitenancy) since we have access to all class descriptor's properties.
When multitenante service is enabled, persistence unit manager is updated by adding these properties to eclipselink :
/**
* Add eclipselink properties
* @param persistenceUnitManager persistence unit manager
* @param tenantId tenant identifier
*/
public void updatePersistenceUnitManager(EZBPersistenceUnitManager persistenceUnitManager, String tenantId){
// This property will configure entities as multitenant
// It is the equivalent of @Multitenant
String sessionCustomizerProperty = "eclipselink.session.customizer";
String sessionCustomizerClass = "org.ow2.easybeans.persistence.eclipselink.MultitenantEntitiesSessionCustomizer";
persistenceUnitManager.addProperty(sessionCustomizerProperty, sessionCustomizerClass);
// This property will configure eclipselink to (only) create
// database.
String createTablesProperty = "eclipselink.ddl-generation";
String createTablesValue = "create-tables";
persistenceUnitManager.addProperty(createTablesProperty, createTablesValue);
// This property will propagte the tenantId to eclipselink
// This value will be added to entities tables
String tenantIdProperty = "eclipselink.tenant-id";
String tenantIdValue = tenantId;
persistenceUnitManager.addProperty(tenantIdProperty,tenantIdValue);
}
Customize context root
If an application is multitenant (i.e. its tenantId is different than null or default tenantId), the URL of the application will be modified by adding the tenantId.
Example : Deployment of javaee5-earsample with tenantId = T1
- Without multitenant service, context root => /javaee5-earsample
- With multitenant service, context root => /T1/javaee5-earsample
Customize logs
If you want to see tenantId in logs, add %T to the wanted handler (tty, logf, ...) in trace.properties. For tty handler for example we will have : handler.tty.pattern %T %d : %O{1}.%M : %m%n In order for not having dependency from Monolog to JOnAS, it was necessary to find a way to retrieve the correct tenantId for each tenant. One solution is to write an interface in monolog that will be implemented in JOnAS. This interface contains a method called getValue() which returns, in this case, the tenantId. The tenantId is retrieved from the context. A new ipojo component will track services which implements this interface and are registered in OSGi plateform. When such service is registered, a callback will register this service, with his pattern, in Monolog. In starting of multitenant service, it will register itself in OSGi saying that he implements the LogInfo interface and set his pattern as a property : <provides specifications="org.objectweb.util.monolog.api.LogInfo">
<property field="pattern" name="pattern" type="java.lang.Character"/>
</provides> iPojo component is situated in modules/librairies/externals/monolog: <ipojo xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="org.apache.felix.ipojo"
xsi:schemaLocation="org.apache.felix.ipojo http://felix.apache.org/ipojo/schemas/1.6.0/core.xsd" >
<component classname="org.ow2.jonas.monolog.MonologExtension"
immediate="false"
name="MonologExtension">
<requires optional="true"
specification="org.objectweb.util.monolog.api.LogInfo"
aggregate="true"
proxy="false"
nullable="false">
<callback type="bind" method="addExtension" />
<callback type="unbind" method="removeExtension" />
</requires>
<!-- LifeCycle Callbacks -->
<callback method="start" transition="validate" />
<callback method="stop" transition="invalidate" />
</component>
<instance component="MonologExtension" />
</ipojo> MonologExtension class contains method which are called as callback when a service is registered: /**
* Add an extension to Monolog
* @param logInfoProvider
*/
public void addExtension(final LogInfo logInfoProvider, ServiceReference ref) {
Character pattern = (Character) ref.getProperty("pattern");
try {
Monolog.monologFactory.addLogInfo(pattern, logInfoProvider);
} catch (Exception e) {
}
logger.info("Extension ''{0}'' was added by ''{1}'' to Monolog", pattern, logInfoProvider.getClass().getName());
}
/**
* Remove and extension from Monolog
*/
public void removeExtension(ServiceReference ref) {
Character pattern = (Character) ref.getProperty("pattern");
Monolog.monologFactory.removeLogInfo(pattern);
logger.info("Extension ''{0}'' was removerd from Monolog.", pattern);
} To use monolog extension, you need to make a dependency on : <dependency>
<groupId>org.ow2.monolog</groupId>
<artifactId>monolog</artifactId>
<version>2.2.1-SNAPSHOT</version>
</dependency> This is where monolog's extensions are handled.
Customize MBeans
When we deploy a same application two times for two differents tenants, the problem is that application's mbeans will have the same name which will create a case of conflict. To avoid it, a solution is to add an attribut in the mbean's ObjectName named tenantId : Domaine:name=MBeanName;tenantId=T1 To do that, we need to intercept all MBeanServer methods calls since majority of these methods use the ObjectName. A solution is to set a proxy of the principal MBeanServer (which is returned by ManagementFactory.getPlatformMBeanServer()) : Proxy.newProxyInstance(mbeanServer.getClass().getClassLoader(), new Class<?>[]{MBeanServer.class}, invocationHandler);
- Define a handler implementation , which implements InvocationHandler interface and create an InvocationContext for each method invocation
public class InvocationHandlerImpl implements InvocationHandler {
...
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
InvocationContextImpl invocationContextImpl = new InvocationContextImpl(method, args, interceptors);
return invocationContextImpl.proceed();
}
...
} - Define a context implementation, which implements InvocationContext interface and invoke interceptors before calling the initial method
public class InvocationContextImpl implements InvocationContext {
...
public Object proceed() throws Exception {
try {
return this.interceptors.get(index--).invoke(this);
} finally {
index++;
}
}
...
} - Define an Interceptor interface which contains invoke() method
public interface Interceptor {
/**
*
* @param invocationContext use to apply the interceptors sequence
* @return invocation return
*/
Object invoke (InvocationContext invocationContext) throws Exception;
} - Each interceptor must implements this interface
// Classes where mbean ObjectName is the first parameter
index = 0;
} else if (methodName.equals("method1")){
// Classes where mbean ObjectName is the second parameter
index = 1;
}
ObjectName oldObjectName = (ObjectName) parameters[index];
ObjectName newObjectName = updateObjectName(oldObjectName);
if (newObjectName != null) {
parameters[index] = newObjectName;
invocationContext.setParameters(parameters);
}
private ObjectName updateObjectName (ObjectName old) {
String tenantId = null;
if (TenantCurrent.getCurrent().getTenantContext() != null) {
tenantId = TenantCurrent.getCurrent().getTenantContext().getTenantId();
}
if (old != null && tenantId != null && tenantId != TenantContext.DEFAULT_TENANT_ID) {
try {
return new ObjectName(new StringBuilder(old.toString()).append(","+tenantIdAttributeName+"=").append(tenantId).toString());
} catch (MalformedObjectNameException e) {
throw new RuntimeException("Failed to format [" + old + "]", e);
}
}
return null;
} Once the "proxified" MBeanServer is set, we need to get it from anywhere. Then, we will define it as the platformMBeanServer. We have two solutions:
Introspection
Set our new MBeanServer impl. in the platformMBeanServer field of the java.lang.management.ManagementFactory and we will have permanently overriden the JVM agent's MBean registration : Field serverField = ManagementFactory.class.getDeclaredField("platformMBeanServer");serverField.setAccessible(true);
serverField.set(null, ourMBeanServer); This method works rightly but not really clean. (If one day, someone decides to change the field name, it will not work). The following is more elegant
Customized MBeanServerBuilder
Create our own MBeanServerBuilder with extends javax.management.MBeanServerBuilder. public class JOnASMBeanServerBuilder extends MBeanServerBuilder {...
public MBeanServer newMBeanServer(String defaultDomain,
MBeanServer outer,
MBeanServerDelegate delegate) {
// Create a MBeanServer
MBeanServer origin = super.newMBeanServer(defaultDomain, outer, delegate);
// Add default interceptor
invocationHandler = new InvocationHandlerImpl();
invocationHandler.addInterceptor(new MBeanServerDelegateInterceptor(origin));
// Return a proxy of MBeanServer
return (MBeanServer) Proxy.newProxyInstance(origin.getClass().getClassLoader(), new Class<?>[]{MBeanServer.class}, invocationHandler);
}
...
}
- Project
- Demos
- Services
- Documentation
- Downloads
- Community
- Developers