Michele SciabarràCorsoJava
testi corsi mailing
Symbian Java  Linux

Humor
-Psicopatologia Utenti Linux
-Documentario: Sistemista Linux
-Il Grande Mago Informatico
-Se le distro fossero ragazze
-La Calata Dei Barbari
-Programmatori al supermercato
-Clienti di ieri e di oggi
-Colloqui di Lavoro
-Il vero informatico 2005
-Regolamento per Software House
-Diario di un Open Source
-Non usate quel linguaggio
-Decifrare Offerta Lavoro
-Il lavoro in Italia
-I fantastici 4 degli SmartPhones
-Dalla Teoria Alla Pratica
-Cosa Vuole il Cliente
-Colloqui con gli utenti
-La Visione degli Esperti
-Di che pasta è il tuo codice
-Una Email Dal 2143

Tecnica
-JSF ClassLoader -Programmazione Cellulari Symbian
-L'invasione degli SmartPhone
-Intro Eclipse Video!
-ReadLine FrontEnd
-JspWiki

Opinioni
-C'era una volta il cellulare
-Business dell'OpenSource
-Le scuse del Linux World Expo
-Linux non è Comunismo
-Java e l'Open Source
-Chi ha scritto Linux
-Chi ha paura di XAML -Lavorare con Tanenbaum

Informazioni
-Questo sito
-L'autore








Scarica Omaggio
il Capitolo 2




Leggi Online
il Capitolo 6

Class Loader per JSF

Il 5 dicembre 2006 al JUG Milano, nel corso di una presentazione su JSF, ho fatto vedere dei backing bean che NON richiedevano il riavvio dell'application server. Poichè le JSP (il front-end) non lo richiedono nemmeno, si ottiene un sistema di sviluppo abbastanza "agile".

Notare che si tratta di una OTTIMIZZAZIONE per un caso particolare del ciclo di sviluppo (la implementazione dei backing bean) che comunque è una problematica importante quando si sviluppano JSF.

Ecco qui il codice del mio metodo:

Innanzitutti serve una classe jsfshell.AppClassLoader. Attenzione che questo class loader è MOLTO specifico per le JSF, e RE-ISTANZIA la classe ogni volta, eventualmente ricaricandola. E' utile per backing bean di tipo REQUEST, non lo è per altri casi.

Notare che le classi si specificano con un nome tipo demo_Demo dove uso la '_' per indicarle.

C'è anche qualche hack per farlo funzionare con JSF RI (tipo ignorare il BeanInfo)


package jsfshell.util;

import java.beans.Beans;
import java.io.File;
import java.io.FileInputStream;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class AppClassLoader extends ClassLoader {

	private static Log log = LogFactory.getLog(AppClassLoader.class);
	String classname;
	File classfile = null;
	long timestamp = -1;
	Class clazz = null;
	public AppClassLoader(String base, String classname)
			throws ClassNotFoundException, InstantiationException,
			IllegalAccessException {

		this.classname = classname.replace('_', '.');

		// configura il file
		String filename = base + File.separator
				+ classname.replace('_', File.separatorChar) + ".class";

		classfile = new File(filename);

		// imposta il timestamp
		log.debug("#### CARICATO file=" + classfile.getAbsolutePath());
		if (classfile.exists()) {
			timestamp = classfile.lastModified();
		} else
			throw new ClassNotFoundException();

	}

	public boolean isChanged() {
		return classfile.lastModified() > timestamp;
	}

	/* Questa e' la versione chiamata */
	public Class loadClass(String className) throws ClassNotFoundException {
		// System.out.println("LoadClass: "+className);
		if (className.endsWith("BeanInfo"))
			return null;
		return loadClass(className, true);
	}

	/* Questa e' la versione completa */
	public synchronized Class loadClass(String className, boolean resolveIt)
			throws ClassNotFoundException {

		// System.out.println("Caricamento della classe : " + className);
		byte[] classData = null;
		Class result;

		// cerchiamo nel primordial
		try {
			// result = super.findClass(className);
			result = Thread.currentThread().getContextClassLoader().loadClass(
					className);

			// System.out.println("Caricata classe di sistema (CLASSPATH)");
			return result;
		} catch (ClassNotFoundException ex) {
			/* Proviamo a caricare classe dinamicamente */
			classData = loadClassBytes();
			/* poi si prova a caricare la classe con il classloader primordiale */
			if (classData == null) {
				throw new ClassNotFoundException();
			}
		}

		/* viene eseguito il parsing, e costruito l'oggetto class */
		result = defineClass(className, classData, 0, classData.length);

		if (resolveIt) {
			resolveClass(result);
		}

		// System.out.println("Classe caricata : " + className);
		return result;
	}

	/* Carica il contenuto di un file .class */
	private byte[] loadClassBytes() {
		// System.out.println("Lettura dati da file per la classe " +
		// classname);
		byte result[] = null;
		FileInputStream fi = null;
		try {
			fi = new FileInputStream(classfile);
			result = new byte[(int) classfile.length()];
			fi.read(result);
		} catch (Exception e) {
			result = null;
		} finally {
			try {
				fi.close();
			} catch (Exception ex) {
			}
		}
		return result;
	}

	Object bean = null;

	/**
	 * Crea il bean
	 */
	public Object getBean() throws Exception {
		if (bean == null)
			bean = Beans.instantiate(this, classname);
		return bean;
	}
}


A questo punto il class loader che permette di carirare le classi viene usato con un oggetto chiamato "app" inserito nel faces-context.

Alcune precisazioni: innanzitutto mi aspetto che le classi Java compilate stiano in un posto ben noto (ed è per me la WEB-INF/app-classes). Le classi vengono compilare da eclipse quando le salvo.

Semplicemente sfrutto una feature di eclipse che mi permette di specificare la directory dove vengono messe le classi compilate. Quindi creo un nuovo source folder e specifico WEB-INF/app-classes come destinazione.

Notare che l'AppBean ricrea il backing bean all'inizio di una richiesta, poi lo recupera. Ripeto, è utile per backing bean di tipo REQUEST.

Inoltre mi aspetto che il bean implementi una interfaccia, ed effettuo una serie di inizializzazioni che mi sono utili.

Questo è il codice dell'App Bean.


package jsfshell.util;

import java.util.HashMap;
import java.util.Map;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.faces.event.ActionEvent;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import jsfshell.App;

public class AppBean extends HashMap {
	
	private static Log log = LogFactory.getLog(AppBean.class);

	public Object get(Object className) {

		FacesContext fc = FacesContext.getCurrentInstance();
		ExternalContext ec = fc.getExternalContext();
		ServletContext ctx = (ServletContext) ec.getContext();

		// recupera una coppia
		AppClassLoader entry = (AppClassLoader) super.get(className);
		if (entry == null || entry.isChanged()) {
			try {
                                // cambiare il base path se non vi piace
				String base = ctx.getRealPath("/WEB-INF/app-classes");
				entry = new AppClassLoader(base, className.toString());
				put(className, entry);
			} catch (Exception e) {
				e.printStackTrace();
				return null;
			}
		}

		Map rm = ec.getRequestMap();

		Object obj = rm.get("_$_" + className + "_$_");
		if (obj == null) {
			try {
				obj = entry.getBean();
				rm.put("_$_" + className + "_$_", obj);
				log.debug("#### CREATO " + className+" - ");

				// e adesso lo inizializziamo inizializzato
				if (obj instanceof App) {
					initApp((App) obj, fc, ec, rm);
				}
			} catch (Exception ex) {
				ex.printStackTrace();
				log.warn("#### NON POSSO CREARE " + className);
			}
		} else {
			log.trace("#### RECUPERATO " + className);
			// e se è recuperato NON VA reinizializzato ...
			// almeno... non dovrebbe...
		}

		//System.out.println();
		return obj;
	}

	private void initApp(App app, FacesContext fc, ExternalContext ec, Map rm) {

		app.setFacesContext(fc);
		app.setExternalContext(ec);
		app.setRequestMap(rm);
		app.setParameterMap(ec.getRequestParameterMap());

		Map session = ec.getSessionMap();
		app.setSessionMap(session);
		
		HttpServletRequest req = (HttpServletRequest)ec.getRequest();

		// chiama la init
		app.init();
		System.out.println();
	}

}


Va inserito nel faces-config.xml :

	<managed-bean>
		<managed-bean-name>app</managed-bean-name>
		<managed-bean-class>jsfshell.util.AppBean</managed-bean-class>
		<managed-bean-scope>application</managed-bean-scope>
	</managed-bean>


Infine ogni backing bean che viene caricato deve estendere questa classe astratta (non è strettamente obbligatorio, ma è utile, perchè così la classe viene inizializzata dal loader passandogli un sacco di proprietà che vanno altrimenti ricercate nel Faces Context.

package jsfshell;

import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.component.UIData;
import javax.faces.component.UIInput;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.faces.event.ActionEvent;

public abstract class App {

	public App() {
	}

	private Map dialogMap = null;

	/**
	 * Imposta l'intero gruppo di dati del dialogo.
	 * 
	 * @param map
	 */
	public void setGroupMap(Map map) {
		dialogMap = map;
	}

	private Map parameterMap;

	/**
	 * Imposta l'hashmap dei parametri
	 * 
	 * @param map
	 */
	public void setParameterMap(Map map) {
		parameterMap = map;
	}

	private Map requestMap = null;

	/**
	 * Imposta l'hashmap della richiesta
	 * 
	 * @param map
	 */
	public void setRequestMap(Map map) {
		requestMap = map;
	}

	private Map sessionMap;

	/**
	 * Imposta l'hashmap della richiesta
	 * 
	 * @param map
	 */
	public void setSessionMap(Map map) {
		sessionMap = map;
	}

	/**
	 * Inizializza lil bean
	 * 
	 * 
	 * @return
	 */

	public abstract void init();


	/**
	 * Ritorna il parametro
	 * 
	 * @param key
	 * @return
	 */
	public String getParameter(String key) {
		return (String) parameterMap.get(key);
	}

	/**
	 * Controlla se esiste il parametro
	 */
	public boolean existParameter(String key) {
		return parameterMap.containsKey(key);
	}

	/**
	 * Controlla se esiste la richiesta
	 */
	public boolean existRequest(String key) {
		return requestMap.containsKey(key);
	}

	/**
	 * Controlla se esiste in sessione
	 */
	public boolean existSession(String key) {
		return sessionMap.containsKey(key);
	}

	/**
	 * Ritorna i parametri associati
	 * 
	 * @param key
	 * @return
	 */
	public String[] getParameters(String key) {
		return (String[]) parameterMap.get(key);
	}

	/**
	 * Ritorna il valore associato alla richiesta.
	 * 
	 * @param key
	 * @return
	 */
	public Object getRequest(Object key) {
		return requestMap.get(key);
	}

	/**
	 * Ritorna il valore associato alla sessione.
	 * 
	 * @param key
	 * @return
	 */
	public Object getSession(Object key) {
		return sessionMap.get(key);
	}

	/**
	 * Salva il valore nella sessione.
	 * 
	 * @param key
	 * @param value
	 * @return
	 */
	public Object setSession(Object key, Object value) {
		return sessionMap.put(key, value);
	}

	private FacesContext facesContext = null;

	private ExternalContext externalContext = null;

	/**
	 * Ritorna l'External Context
	 * 
	 * @return
	 */
	public ExternalContext getExternalContext() {
		return externalContext;
	}

	public void setExternalContext(ExternalContext externalContext) {
		this.externalContext = externalContext;
	}

	/**
	 * Ritorna il FacesContext
	 * 
	 * @return
	 */
	public FacesContext getFacesContext() {
		return facesContext;
	}

	public void setFacesContext(FacesContext facesContext) {
		this.facesContext = facesContext;
	}

	public String toString() {
		return getClass().getName();
	}

	/**
	 * Estrazione di un bean.
	 * 
	 * @param bean
	 * @return
	 */
	public Object getBean(String bean) {
		FacesContext fc = getFacesContext();
		return fc.getApplication()
		        .createValueBinding("#{" + bean + "}")
				.getValue(fc);
	}

	
}


A questo punto:

  1. Create una classe Backing Bean che estende jsfshell.App, esempio demo.Demo
  2. Fate in modo che venga compilata in WEB-INF/app-classes
  3. La vostra JSF (con o senza Facelets) deve riferire un metodo o una proprietà del backing bean con questa sintassi: #{app.demo_Demo.prorietaOMetodo}
  4. La classe verrà ricaricata se modificata, e istanziata ad ogni richiesta
  5. Se la modificate e la salvate, alla richiesta successiva verrà ricarivata modificata.

Non mi stancherò di ripetere che questo metodo è buono per backing bean di tipo REQUEST, che vengono reinstanziati ad ogni passo.

Fortunatamente, questi sono quelli che implementano la maggior parte della logica di front-end, e quella di creare e testare il backing bean per ogni pagina è la parte più tediosa dello sviluppo JSF.

Notare che ho preso il mio codice, l'ho semplificato (le mie hanno una gran quantità di accozzaglie che ho tolto e potrebbe contenere qualche bug dato questo processo. In tal caso segnalatemelo.


Commenti(aggiungi il tuo):

Contatto: michele at sciabarra dot com