Copyright © 2012 OW2 Consortium
This work is licensed under the Creative Commons Attribution-ShareAlike License. To view a copy of this license,visit http://creativecommons.org/licenses/by-sa/2.0/deed.en or send a letter to Creative Commons, 559 Nathan Abbott Way, Stanford, California 94305, USA.
JOnAS hosts and deploys applications written in Java. However, an application can not be natively deployed more than once in an instance of JOnAS. If necessary, the application will be deployed on another server instance because there is a risk of collision. One solution of this problem is multitenancy. This new feature provides the ability to deploy the same application multiple times on a single instance of JOnAS without prior configuration.
Tenants will run on the same application server (JOnAS) each with an instance of the application. Thus, in one instance of JOnAS, there will be many instances of an application than tenants using it. This multitenancy level is not without impact on the JOnAS application server because it will make changes in order to deploy the same application multiple times for multiple tenants in ensuring customization of resources and security among tenants.
According to JavaEE7 specifications, for each tenant, an instance of the application is deployed.
Each tenant is identified by a tenant identifier following the pattern T<id>
when
id
is numeric. This identifier is defined in web descriptor, application descriptor or even
addon descriptor with the hierarchy :
Addon > EAR > WAR
It was necessary to define a default identifier (defaultTenantId = T0
) for applications
that do not give a specific tenant-id. it is simply used to enforce the policy where each instance of an
application is linked to an identifier of tenant, and will not be used for customizing data (no changes will
be made).
The identifier of the tenant must be present when deploying the application but also during its execution. In fact, during deployment, several services operate to save the settings and application data in the JOnAS environment. These services have a dependence on the multitenant service and will use it for customizing data. Therefore, it is necessary that the information "tenant-id" is constantly present throughout the duration of the deployment.
The tenant context is composed of :
TenantId : tenant identifier
InstanceName
To access the context of the current tenant, use :
TenantCurrent.getCurrent().getTenantContext();
Tenant-id is stored in a variable associated to the ThreadLocal. When running the application, an HTTP filter is set up, it sets all contexts associated with the thread, including the context of tenant, before the server responds to the client request.
// Save the current context old = TenantCurrent.getCurrent().getTenantContext(); TenantCurrent.getCurrent().setTenantContext(this.ctx);
Next, execute the request. And finally, restore the old context :
// Restore the old context TenantCurrent.getCurrent().setTenantContext(old);
This filter is created by calling the multitenant service. The valve is set in Tomcat7Service as follows :
// For the tenantId Filter tenantIdHttpFilter = null; String tenantId = null; if (getMultitenantService() != null){ // get an instance of the filtre tenantId = super.getTenantId(war.getWarDeployable()); tenantIdHttpFilter = getMultitenantService().getTenantIdFilter(tenantId); // needs to add this filter on the context jStdCtx.addValve(new FilterValveWrapper(tenantIdHttpFilter)); }
Context root is defined in the web descriptor of the application. This context must be unique for each tenant. However, because tenants are instances of the same application, context root is the same for all. During deployment, context root of each instance is prefixed by the instance name and the tenant-id.
This customization is done during the deployment of the webapp.
protected String updateContextRoot(String contextRoot, IDeployable deployable) { String tenantId = getTenantId(deployable); String instanceName = multitenantService.getInstanceNameFromContext(); if (instanceName != null) { contextRoot = instanceName + "/" + contextRoot; } if (tenantId != null) { contextRoot = tenantId + "/" + contextRoot; } return contextRoot; }
To persist the tenantId in database, we have to set the eclipselink.tenant-id
property in
persistence.xml file. To automatize the propagation of the tenantId to eclipselink we need to add this
property automatically when the application is added. Then, we will use the method :
// This property will propagate the tenantId to eclipselink // This value will be added to entities tables String tenantIdProperty = "eclipselink.tenant-id"; String tenantIdValue = tenantId; persistenceUnitManager.setProperty(tenantIdProperty, tenantIdValue);
Entities must be configured as multitenant to enable adding tenant-id in the database. For that, 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); } }
Then, during the deployment of the application, an eclipselink property is set to use this session customizer :
// 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.setProperty(sessionCustomizerProperty, sessionCustomizerClass);
Because tenants share the same database and the same tables, it is important to ensure that a tenant does not
drop an create tables. For that, verify if the drop-and-create-tables
eclipselink property
is not set. Otherwise, change this property to create-tables
only :
// If eclipselink was enabled to drop and create tables // change this property to only create tables String createTablesProperty = "eclipselink.ddl-generation"; String dropAndCreateTablesValue = "drop-and-create-tables"; String createTablesValue = "create-tables"; Map<String, String> properties = persistenceUnitManager.getProperty(createTablesProperty); for (Map.Entry<String, String> property : properties.entrySet()){ if (property.getValue().equals(dropAndCreateTablesValue)) { logger.warn("This tenant was enabled to drop and create tables. Eclipselink property is changed to only create tables"); persistenceUnitManager.setProperty(createTablesProperty, createTablesValue, property.getKey()); } }
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 name is prefixed the syntax : T<id>/name
when
T<id>
is the application's tenant-id. The naming strategy is set by:
newNamingStrategies.add(ejb3Service.getNamingStrategy(prefix, oldNamingStrategy));
Example: MyInitializerBean will be T1/MyInitializerBean. 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 T1/MyInitializerBean) linked to the same object.
To customize JNDI names bound by the application by adding the tenant-id as prefix, an interceptor is set.
This interceptor (org.ow2.jonas.lib.tenant.interceptor.jndi.JNDITenantIdInterceptor
) is an
implementation of org.ow2.carol.jndi.intercept.ContextInterceptor
and is registered in
Carol Interceptor Manager when Multitenant service is activated. Then, all JNDI calls are intercepted.
// Add tenantId JNDI interceptor jndiTenantIdInterceptor = new JNDITenantIdInterceptor(JNDI_SEPARATOR); SingletonInterceptorManager.getInterceptorManager().registerContextInterceptor(jndiTenantIdInterceptor);
Two operations are made by this interceptor :
When we deploy a same application two times for two different tenants, the problem is that application's MBeans will have the same identifier which will create a case of conflict. To avoid it, a solution is to add an attribute in the MBean's ObjectName named tenantId :
Domaine:name=MBeanName;tenantId=T1
To do that, we need to intercept all MBeanServer methods call 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()
)
To customize MBeans, a JMX interceptor is set to add a tenant-id
property to the MBean ObjectName.
This interceptor (org.ow2.jonas.lib.tenant.interceptor.jmx.JMXTenantIdInterceptor
) implements
org.ow2.jonas.jmx.Interceptor
and is added to the InvocationHandler (
org.ow2.jonas.jmx.internal.interceptor.InvocationHandlerImpl
) by multitenant service :
// Add tenantId JMX interceptor jmxTenantIdInterceptor = new JMXTenantIdInterceptor(tenantIdAttributeName, allowToAccessPlatformMBeans); jmxService.addInterceptor(jmxTenantIdInterceptor);
and will be called before querying the MBeanServer.
In order to create a "proxified" MBeanServer, a new class
org.ow2.jonas.services.bootstrap.mbeanbuilder.JOnASMBeanServerBuilder
which extends
javax.management.MBeanServerBuilder
is used and set as a system property :
// MBeanServerBuilder System.setProperty("javax.management.builder.initial", "org.ow2.jonas.services.bootstrap.mbeanbuilder.JOnASMBeanServerBuilder");
Then, the first MBeanServer created in the platform is a proxy with a default interceptor. This default interceptor is always the last called and will call the MBeanServer method originally invoked (before interception).
// Create real MBeanServer with outerProxy MBeanServer origin = super.newMBeanServer(defaultDomain, outerProxy, delegate); // Create handler for MBeanServer proxy and add // the default interceptor InvocationHandlerImpl invocationHandler = new InvocationHandlerImpl(); invocationHandler.addInterceptor(new MBeanServerDelegateInterceptor(origin)); // Create the MBeanServer proxy MBeanServer proxy = (MBeanServer) Proxy.newProxyInstance(origin.getClass().getClassLoader(), new Class<?>[]{MBeanServer.class}, invocationHandler);
It is possible to add as many interceptors that it is desired and they will be called one by one.
Using a customized MBeanServerBuilder can be problematic. In fact, as described in
JONAS-867, if the system property
com.sun.management.jmxremote
is set before JOnAS startup, this has the effect of
creating some MBeans and then initialize the MBeanServer. However, the system property
javax.management.builder.initial
which is set when JOnAS starts and define the class
which is used to build the platform MBeanServer, this one is present in
org.ow2.jonas.services.bootstrap.mbeanbuilder.JOnASMBeanServerBuilder
and is not known by the classloader at this step.
If the system property com.sun.management.jmxremote
is not set, this error should not appear.
All administrators are defined in a special realm. Tenants administration isolation is done by defining two profiles :
This profile is define by the super-admin
role :
<role name="super-admin" description="Role allowing access to all tenants" />
and will be assigned to a unique user in the platform :
<user name="superAdmin" password="admin" roles="superAdmin"/>
This user will have a full access to all MBeans when connecting on a administration tool such as JConsole.
For each tenant, a specific role is defined according to the pattern TenantId[T<id>]
:
<role name="TenantId[T1]" description="Role allowing access to tenant T1 information" />
and will be assigned to a unique user in the group of user belonging to the tenant.
<user name="admin@client" password="admin" roles="TenantId[T1]"/>
This user will have access to MBeans which contain the property tenant-id=T1
.
This isolation is made when JMX Security is activated. For that, set JMX Security to true in jonas.properties :
jonas.service.jmx.secured true
Then, change authentication method and parameter to :
jonas.service.jmx.authentication.method jmx.remote.x.login.config jonas.service.jmx.authentication.parameter jaas-jmx
Next, activate TenantIdLoginModule for jaas-jmx in jaas.config :
jaas-jmx { // Use LoginModule for JMX authentication org.ow2.jonas.security.auth.spi.JResourceLoginModule required resourceName="memrlm_1" ; // Use the login module to add tenantId in TenantContext org.ow2.jonas.lib.tenant.loginmodule.TenantIdLoginModule required; };
During tenant administrator login, TenantIdLoginModule will propagate tenantContext in order to filter MBeans
by the tenant-id
property presents in the ObjectName. Only MBeans which their objectName
contains the same tenant-id than the connected user will appear.
JOnAS use Monolog for logging. Monolog is a very static
project and it was necessary to make it extensible for logging other information than those predefined (as
date, classname, etc). One solution is to write an interface in monolog
org.objectweb.util.monolog.api.LogInfo
:
package org.objectweb.util.monolog.api; /** * This interface allows to add an extension to Monolog * @author Mohammed Boukada */ public interface LogInfo { /** * Gets the info value * @return info value */ String getValue (); }
that will be implemented by JOnAS services.
In this case, this interface is implemented by multitenant service and provides the tenant-id of the current tenant.
An ipojo component is defined in modules/libraries/externals/monolog
and is responsible of
registration of monolog's extensions. When an implementation of this interface is registered in OSGi platform :
<provides specifications="org.objectweb.util.monolog.api.LogInfo"> <property field="pattern" name="pattern" type="java.lang.Character"/> </provides>
This component will add the extension to monolog :
<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>
MonologExtension class contains method which are called as callback when a service implementing LogInfo interface is registered:
/** * Add an extension to Monolog * @param logInfoProvider */ public void addExtension(final LogInfo logInfoProvider, ServiceReference ref) { Character pattern = (Character) ref.getProperty("pattern"); Monolog.monologFactory.addLogInfo(pattern, logInfoProvider); logger.info("Extension ''{0}'' was added by ''{1}'' to Monolog", pattern, logInfoProvider.getClass().getName()); } /** * Remove an 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>
or any later version.
For seeing tenant-id in log messages, add %T
to the wanted handler (tty, logf, ...) in
trace.properties. Example :
handler.tty.pattern %T %d : %O{1}.%M : %m%n
In this example, tenant-id will be added at the beginning of the log message. If tenant-id is not set in
tenantContext or its value is T0
(which is default tenant-id) then nothing will be printed.