2009-06-16

EJB-Konfiguration auf glassfish manipulieren ohne Reboot mittels MBeans

Hintergrund: Nachdem ein EJB (Web-)Service auf dem Application Server (glassfish) deployed wurde, soll die Konfiguration geändert werden ohne den Service neu zu deployen oder gar den Server neu starten zu müssen.

Daher scheidet ein config.xml im jar-File des Service aus. Ebenso wie die irreführend als dynamisch benannten Server-Properties (hier ist vor dem Wirksamwerden immer ein kompletter Serverneustart nötig)

Lösung: Custom MBeans

Und so kann es gehen (schöner geht immer):
* Ein Interface ConfigMBean
* Eine abstrakte Klasse Config
* Zwei Implementationen Development und Production
* Einen ConfigReader (der nur mit Config hantiert)
* Ein EJB namens DemoBusiness (der via ConfigReader auf ein MBean zugreift)

Als Schmankerl cached der ConfigReader und alle ConfigMBean Implementierungen [Development und Production] besitzen eine hardverdrahtete DefaultConfig. Somit funktioniert dieser auch, wenn gerade kein MBean registriert/deployed ist.

Der ConfigReader registriert sich beim Config(MBean), um über Änderungen von Attributen benachrichtigt zu werden. Der Einfachheit halber wird dann immer die gesamte Config (Cache) ungültig und beim nächsten getConfig erneuert.

Zusätzlich registriert sich der ConfigReader noch beim Application Server direkt als Listener für unregister-Nachrichten, damit bei unregister/undeploy ebenfalls die Config ungültig wird.

Wichtig: Am Ende (der Benutzung) muß man sich als Listener mittels releaseConfigListeners wieder deregistrieren (z.B. in ejbRemove oder einem finally-Block).

* EJB normal deployen
* Alle class-Files aus de.isolvedit.config.category nach domain-dir/applications/mbeans kopieren
* folgendes Kommando (zu beachten: Nur via asadmin oder glassfish-GUI gesetzte Attribute werden gespeichert - bleiben also auch nach einem Serverneustart erhalten)
asadmin create-mbean --user adminuser --name de.isolvedit.config.category.Development de.isolvedit.config.category.Development --attributes SalesOrganisation=1100:DistributionChannel=10: Category=10:ShipCondition=ST:ChangeUser=unknown:Unit=ST: OrderTypeReturnFlag=ZRE: PositionTypeBuy=?:PositionTypeRent=!
Anmerkung: attr=value:attr=value ohne Leerzeichen(!)

So sieht das dann auf dem glassfish aus:


Hier schonmal die Ausgabe im Logfile:
[DemoBusiness.doSomething()] : start
[ConfigReader.connectConfigListener()] : add to notification listeners...
[ConfigReader.connectConfigListener()] : ...done:
[ConfigReader.getConfig()] : MBean from server
[ConfigReader.getConfig()] : MBean name: user:impl-class-name=de.isolvedit.config.category.Development, name=de.isolvedit.config.category.Development, server=server
[ConfigReader.getConfig()] : unit=ST
[ConfigReader.getConfig()] : shipCondition=ST
[ConfigReader.getConfig()] : positionTypeRent=FASEL
[ConfigReader.getConfig()] : category=10
[ConfigReader.getConfig()] : distributionChannel=10
[ConfigReader.getConfig()] : positionTypeBuy=BLA
[ConfigReader.getConfig()] : orderTypeReturnFlag=ZRE
[ConfigReader.getConfig()] : changeUser=unknown
[ConfigReader.getConfig()] : salesOrganisation=1100
[ConfigReader.handleNotification()] : notification message: config changed sequence: 10



Config

package de.isolvedit.config.category;

public interface ConfigMBean {
public String getSalesOrganisation();
public void setSalesOrganisation(String salesOrganisation);
public String getDistributionChannel();
public void setDistributionChannel(String distributionChannel);
public String getCategory();
public void setCategory(String category);
public String getShipCondition();
public void setShipCondition(String shipCondition);
public String getChangeUser();
public void setChangeUser(String changeUser);
public String getUnit();
public void setUnit(String unit);
public String getOrderTypeReturnFlag();
public void setOrderTypeReturnFlag(String orderTypeReturnFlag);
public String getPositionTypeBuy();
public void setPositionTypeBuy(String positionTypeBuy);
public String getPositionTypeRent();
public void setPositionTypeRent(String positionTypeRent);
}



ConfigBean

package de.isolvedit.config.category;

import javax.management.AttributeChangeNotification;
import javax.management.Notification;
import javax.management.NotificationBroadcasterSupport;

public abstract class Config extends NotificationBroadcasterSupport implements ConfigMBean {

public static final String CONFIG_CHANGED = "config changed";

// to set attributes use asadmin or glassfish-GUI !!
// the following properties are set to null, if deployed via glassfish!
// further details: http://docs.sun.com/app/docs/doc/820-4336/gbdzi?a=view

protected String category;
protected String changeUser;
protected String distributionChannel;
protected String orderTypeReturnFlag;
protected String positionTypeBuy;
protected String positionTypeRent;
protected String salesOrganisation;
protected String shipCondition;
protected String unit;

private long sequenceNumber = 1L;

// there is no abstract static -> so this is the method to override in subclasses!
public static Config DefaultConfig() {
return null;
};

public synchronized void configChanged() {
Notification n = new AttributeChangeNotification(this, sequenceNumber++, System.currentTimeMillis(),
CONFIG_CHANGED, "*", "String", null, null);
sendNotification(n);
}

public String getSalesOrganisation() {
return salesOrganisation;
}
public void setSalesOrganisation(String salesOrganisation) {
this.salesOrganisation = salesOrganisation;
configChanged();
}
public String getDistributionChannel() {
return distributionChannel;
}
public void setDistributionChannel(String distributionChannel) {
this.distributionChannel = distributionChannel;
configChanged();
}
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
configChanged();
}
public String getShipCondition() {
return shipCondition;
}
public void setShipCondition(String shipCondition) {
this.shipCondition = shipCondition;
configChanged();
}
public String getChangeUser() {
return changeUser;
}
public void setChangeUser(String changeUser) {
this.changeUser = changeUser;
configChanged();
}
public String getUnit() {
return unit;
}
public void setUnit(String unit) {
this.unit = unit;
configChanged();
}
public String getOrderTypeReturnFlag() {
return orderTypeReturnFlag;
}
public void setOrderTypeReturnFlag(String orderTypeReturnFlag) {
this.orderTypeReturnFlag = orderTypeReturnFlag;
configChanged();
}
public String getPositionTypeBuy() {
return positionTypeBuy;
}
public void setPositionTypeBuy(String positionTypeBuy) {
this.positionTypeBuy = positionTypeBuy;
configChanged();
}
public String getPositionTypeRent() {
return positionTypeRent;
}
public void setPositionTypeRent(String positionTypeRent) {
this.positionTypeRent = positionTypeRent;
configChanged();
}
}



Development

package de.isolvedit.config.category;

public class Development extends Config {

public static Development DefaultConfig() {
Development config = new Development();
config.setSalesOrganisation("1100");
config.setDistributionChannel("10");
config.setCategory("10");
config.setShipCondition("ST");
config.setChangeUser("unknown");
config.setUnit("ST");
config.setOrderTypeReturnFlag("ZRE");
config.setPositionTypeBuy("?!?");
config.setPositionTypeRent("!??");

return config;
}
}



Production

package de.isolvedit.config.category;

public class Production extends Config {

public static Production DefaultConfig() {
Production config = new Production();
config.setSalesOrganisation("1100");
config.setDistributionChannel("10");
config.setCategory("10");
config.setShipCondition("ST");
config.setChangeUser("uwEE");
config.setUnit("ST");
config.setOrderTypeReturnFlag("ZRE");
config.setPositionTypeBuy("BUY");
config.setPositionTypeRent("RNT");

return config;
}
}



ConfigReader

package de.isolvedit.config;

import java.util.Map;

import javax.management.AttributeChangeNotification;
import javax.management.MBeanServerConnection;
import javax.management.Notification;
import javax.management.NotificationListener;
import javax.management.ObjectName;

import org.apache.commons.beanutils.BeanUtils;
import org.apache.log4j.ConsoleAppender;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.apache.log4j.PatternLayout;

import com.sun.appserv.management.base.Util;
import com.sun.enterprise.admin.server.core.jmx.AppServerMBeanServerFactory;

import de.isolvedit.config.category.Config;

public class ConfigReader implements NotificationListener {
public final static String MBEAN_SERVER_DELEGATE = "JMImplementation:type=MBeanServerDelegate";
public final static String MBEAN_UNREGISTERED = "JMX.mbean.unregistered";

/**
* static log4j-logger
*/
private static final Logger LOGGER = Logger.getLogger(ConfigReader.class);

private MBeanServerConnection server = null;
private Config configCache = null;
private boolean useCachedConfig = false; // force update
private boolean connectedToServerAsListener = false;
private ObjectName configObjectName = null;

public ConfigReader(Config defaultConfig) {
this.configCache = defaultConfig;
this.configObjectName = getObjectName();
// connect to glassfish
server = AppServerMBeanServerFactory.getMBeanServerInstance();
}

// for testing purposes only!
public ConfigReader(Config defaultConfig, MBeanServerConnection server) {
this.configCache = defaultConfig;
this.configObjectName = getObjectName();
this.server = server;
LOGGER.addAppender(new ConsoleAppender(new PatternLayout()));
LOGGER.setLevel(Level.ALL);
}

// react on config changes and mbean unregister events
// if attributes changed invalidate cached config (force update on next getConfig call)
// if config mbean is unregistered invalidate listenmode (force reconnect on next getConfig call)
public synchronized void handleNotification(Notification notification, Object handback) {
try {
if (notification instanceof AttributeChangeNotification) {
LOGGER.info("notification message: " + notification.getMessage() + " sequence: "
+ notification.getSequenceNumber());
if (Config.CONFIG_CHANGED.equals(notification.getMessage())) {
useCachedConfig = false; // force update
}
} else if (MBEAN_SERVER_DELEGATE.equals(notification.getSource().toString())
&& MBEAN_UNREGISTERED.equals(notification.getType()) && !server.isRegistered(configObjectName)) {
releaseConfigListener();
}
} catch (Exception e) {
LOGGER.error("caught exception", e);
}
}

// return config
// try to connect config mbean and register as attribute change and unregister listener
// try to read from config mbean else use default/cached version
public Config getConfig(boolean cachedVersion) {
connectConfigListener();
if (cachedVersion || useCachedConfig) {
LOGGER.info("MBean from cache");
} else {
LOGGER.info("MBean from server");
try {
LOGGER.info("MBean name: " + configObjectName);

@SuppressWarnings("unchecked")
Map properties = BeanUtils.describe(configCache);

for (String property : properties.keySet()) {
if (!property.equalsIgnoreCase("Class") && !property.equalsIgnoreCase("NotificationInfo")) {
Object value = server.getAttribute(configObjectName, initCap(property));
LOGGER.info(property + "=" + value.toString());
BeanUtils.setProperty(configCache, property, value);
}
}

useCachedConfig = true;

} catch (Exception e) {
LOGGER.error("caught exception", e);
useCachedConfig = false;
}
}
return configCache;
}

public boolean connectConfigListener() {
boolean result = false;
if (!connectedToServerAsListener) {
LOGGER.info("add to notification listeners...");
try {
server.addNotificationListener(configObjectName, this, null, null);
server.addNotificationListener(getMBeanServerDelegateObjectName(), this, null, null);
connectedToServerAsListener = true;
useCachedConfig = false; // force update
LOGGER.info("...done: ");
result = true;
} catch (Exception e) {
LOGGER.warn("...failed: " + e.getMessage());
e.printStackTrace();
result = false;
}
} else {
result = true;
}
return result;
}

public boolean releaseConfigListener() {
boolean result = false;
if (connectedToServerAsListener) {
try {
LOGGER.info("remove from notification listeners...");
if (server.isRegistered(configObjectName)) {
server.removeNotificationListener(configObjectName, this);
}
server.removeNotificationListener(getMBeanServerDelegateObjectName(), this);
LOGGER.info("...done");
result = true;
} catch (Exception e) {
LOGGER.error("...fail", e);
result = false;
}
connectedToServerAsListener = false;
} else {
result = true;
}
return result;
}

public static String initCap(String input) {
if (input == null || input.isEmpty()) {
return input;
}
return input.substring(0, 1).toUpperCase() + input.substring(1);
}

public ObjectName getObjectName() {
try {
return new ObjectName("user:impl-class-name=" + configCache.getClass().getName() + ",name="
+ configCache.getClass().getName() + ",server=server");
} catch (Exception e) {
return null;
}
}

public static ObjectName getMBeanServerDelegateObjectName() {
return (Util.newObjectName(MBEAN_SERVER_DELEGATE));
}
}



DemoBusiness

package de.isolvedit.business;

import de.isolvedit.config.category.Config;
import de.isolvedit.config.category.ConfigReader;
import de.isolvedit.config.category.Development;

@Stateless
public class DemoBusiness implements DemoBusinessLocal {

private static final Logger LOGGER = Logger.getLogger(ConfigReader.class);

private ConfigReader conf = new ConfigReader(Development.DefaultConfig());
//private ConfigReader conf = new ConfigReader(Production.DefaultConfig());

public String doSomething(){
LOGGER.info("start");
String result = null;
try {
try {
Config config = conf.getConfig(false);
result = config.getSalesOrganisation();
} catch (Exception e) {
LOGGER.error("caught exception", e);
}
} finally {
conf.releaseConfigListener();
}

return result;
}

}



Nachtrag: Spannend wäre sicher auch sich direkt als Listener für Attributänderungen beim Application Server zu registrieren ... Kommentare sind willkommen.
Nachtrag 2: Obiges funktioniert erst ab glassfish 2.1