Very neat.
Would you consider a donation to the Pico/Nano team (to be designated as PicoContainer or NanoContainer licenses) in additional to your offer to the Avalon community ?
- Paul
...in 5 minutes, without needing pico. It is almost a shame I wrote this, because it means we might not be using pico as the avalon core after all, but it's useful in the meantime.
Putting my code where my mouth is --------------------------------- Leo Simons wrote:
I must tell you, I strongly feel like moving away from the avalon lifecycle at this point. I also have an idea how to do that in a non-radical way.
Huh-hum. It took longer than expected (an hour and a bit). I just wrote a single class (though I haven't tested it yet, I'm guessing it will work) that uses JDK 1.3+ dynamic proxies to support the deployment of any pico-style component into an existing avalon container. See attachment.
I estimate the effort needed to fully support deployment of any pico-compatible component into any avalon-container, *fully* transparently, is about half an hour per existing avalon container, maybe less.
Usage ----- From the javadoc:
// write your pico component public class MyComponentImpl implements MyComponent { Logger m_logger; Configuration m_config; MyOtherComponent m_moc;
public MyComponentImpl( Logger logger, Configuration config, MyOtherComponent moc ) { m_logger = logger; m_configuration = config; m_moc = moc; } public void doStuff() { moc.callMe(); } }
// and then somewhere.... MyComponent comp = (MyComponent) AvalonInvocationHandler.getProxy( MyComponentImpl.class );
// the container will set up your logger, configuration and // all other dependencies for you myAvalonContainer.add( comp );
// you can only use methods specified by interfaces here, but // that's already true for all avalon components comp.doStuff();
How it works
------------
The getProxy() method creates a dynamic proxy around the provided class that intercepts calls to enableLogging(), configure(), etc etc. Once initialize() is called, it actually creates the component instance, calls any avalon-framework lifecycle methods that the component _does_ implement, and from then on just routes all method calls to the created instance. Bam. Slam. Dunk. That's it.
Of course, a setup like this is hideously twisted and completely the 'wrong' way around, but I am confident in predicting it'll be immensely popular if we implement it ;)
The near future
---------------
Based on the code in the com.leosimons.jicarilla.container.factories package, it should be relatively straightforward to not only support pico-style components, but also Xwork and any number of other IoC-style lifecycle setups.
Who was it that said he never learned anything around here? ;)
cheers,
- Leo
PS: yes, I'll contribute the class under ASF license if someone else volunteers to write unit tests and fix bugs :D
------------------------------------------------------------------------
/* ==================================================================== The Jicarilla Software License
Copyright (c) 2003 Leo Simons. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ==================================================================== */ package com.leosimons.jicarilla.container;
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Constructor; import java.lang.reflect.Proxy; import java.lang.reflect.Modifier; import java.lang.reflect.Field; import java.util.List; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator;
import org.apache.avalon.framework.logger.LogEnabled; import org.apache.avalon.framework.logger.Logger; import org.apache.avalon.framework.context.Context; import org.apache.avalon.framework.context.Contextualizable; import org.apache.avalon.framework.service.ServiceManager; import org.apache.avalon.framework.service.ServiceException; import org.apache.avalon.framework.service.Serviceable; import org.apache.avalon.framework.configuration.Configuration; import org.apache.avalon.framework.configuration.Configurable; import org.apache.avalon.framework.activity.Initializable; import org.apache.avalon.framework.container.ContainerUtil;
/** * An InvocationHandler that intercepts calls to the avalon-framework * lifecycle methods and saves their arguments for later use. During * initialize, an instance is created with the constructor arguments * populated from stuff retrieved from the avalon-framework lifecycle * arguments. * * TODO: fully support the complete lifecycle * * Usage: * * <pre> * public class MyComponentImpl implements MyComponent * { * Logger m_logger; * Configuration m_config; * MyOtherComponent m_moc; * * public MyComponentImpl( Logger logger, Configuration config, * MyOtherComponent moc ) * { * m_logger = logger; * m_configuration = config; * m_moc = moc; * } * * public void doStuff() { * moc.callMe(); * } * } * * // and then somewhere.... * MyComponent comp = (MyComponent) * AvalonInvocationHandler.getProxy( MyComponentImpl.class ); * * // the container will set up your logger, configuration and * // all other dependencies for you * myAvalonContainer.add( comp ); * * // you can only use methods specified by interfaces here, but * // that's already true for all avalon components * comp.doStuff(); * * </pre> * * Yes, indeed, this allows you to deploy any PicoContainer-compatible * component into an existing avalon container! This class can also be * used by avalon containers internally to automagicallly support * PicoContainer-compatible components. * * @author <a href="mail at leosimons dot com">Leo Simons</a> * @version $Id$ */ public class AvalonInvocationHandler implements InvocationHandler { // ---------------------------------------------------------------------- // Properties // ---------------------------------------------------------------------- private boolean initialized = false; private boolean badstate = false; private Class m_targetClass; private Object m_target;
private Logger m_log; private Context m_context; private ServiceManager m_serviceManager; private Configuration m_configuration;
public final static Method ENABLE_LOGGING; static { Method m; try { m = LogEnabled.class.getMethod( "enableLogging", new Class[] { Logger.class } ); } catch( NoSuchMethodException nsme ) { // won't happen m = null; } ENABLE_LOGGING = m; }
public final static Method SERVICE; static { Method m; try { m = Serviceable.class.getMethod( "service", new Class[] { ServiceManager.class } ); } catch( NoSuchMethodException nsme ) { // won't happen m = null; } SERVICE = m; }
public final static Method CONTEXTUALIZE; static { Method m; try { m = Contextualizable.class.getMethod( "contextualizable", new Class[] { Context.class } ); } catch( NoSuchMethodException nsme ) { // won't happen m = null; } CONTEXTUALIZE = m; }
public final static Method CONFIGURE; static { Method m; try { m = Configurable.class.getMethod( "configure", new Class[] { Configuration.class } ); } catch( NoSuchMethodException nsme ) { // won't happen m = null; } CONFIGURE = m; }
public final static Method INITIALIZE; static { Method m; try { m = Initializable.class.getMethod( "initialize", new Class[0] ); } catch( NoSuchMethodException nsme ) { // won't happen m = null; } INITIALIZE = m; }
// ---------------------------------------------------------------------- // Constructors // ----------------------------------------------------------------------
AvalonInvocationHandler( Class target ) { setTargetClass( target ); }
/** * Create a proxy that will redirect calls to avalon-framework * lifecycle methods to a multi-argument constructor. * * @param targetClass * @return */ public static Object getProxy( Class targetClass ) { // all interfaces implemented by the class, // and all lifecycle interfaces implemented // by the handler Class[] intf = targetClass.getInterfaces(); List i = new ArrayList( Arrays.asList(intf) ); i.add( LogEnabled.class ); i.add( Serviceable.class ); i.add( Contextualizable.class ); i.add( Configurable.class ); i.add( Initializable.class ); intf = (Class[])i.toArray( new Class[i.size()] );
return Proxy.newProxyInstance( targetClass.getClassLoader(), intf, new AvalonInvocationHandler( targetClass ) ); }
// ---------------------------------------------------------------------- // Methods // ----------------------------------------------------------------------
protected Class getTargetClass() { return m_targetClass; }
protected void setTargetClass( Class targetClass ) { m_targetClass = targetClass; }
protected Logger getLog() { return m_log; }
protected void setLog( Logger log ) { m_log = log; }
protected Context getContext() { return m_context; }
protected void setContext( Context context ) { m_context = context; }
protected ServiceManager getServiceManager() { return m_serviceManager; }
protected void setServiceManager( ServiceManager serviceManager ) { m_serviceManager = serviceManager; }
protected Configuration getConfiguration() { return m_configuration; }
protected void setConfiguration( Configuration configuration ) { m_configuration = configuration; }
public Object invoke( Object proxy, Method method, Object[] args ) throws Throwable { if(initialized) return method.invoke( m_target, args ); if(badstate) throw new IllegalStateException( "Initialization did not complete without errors!" );
if( ENABLE_LOGGING.equals( method ) ) { setLog( (Logger)args[0] ); return null; } if( SERVICE.equals( method ) ) { setServiceManager( (ServiceManager)args[0] ); return null; } if( CONTEXTUALIZE.equals( method ) ) { setContext( (Context)args[0] ); return null; } if( CONFIGURE.equals( method ) ) { setConfiguration( (Configuration)args[0] ); } if( INITIALIZE.equals( method ) ) { try { createInstance(); } catch( Exception e ) { badstate = true; throw e; } return null; }
if( m_target == null ) throw new IllegalStateException( "You need to call initialize() first!" );
return method.invoke( m_target, args ); }
protected void createInstance() throws Exception { // select the longest constructor (argument-wise) Constructor[] constructors = getTargetClass().getConstructors(); sortConstructors( constructors ); Constructor constructor = constructors[0];
// call it m_target = constructor.newInstance( getArguments( constructor ) );
// for any lifecycle interfaces already implemented by the target ContainerUtil.enableLogging( m_target, getLog() ); ContainerUtil.contextualize( m_target, getContext() ); ContainerUtil.service( m_target, getServiceManager() ); ContainerUtil.contextualize( m_target, getContext() ); ContainerUtil.configure( m_target, getConfiguration() ); ContainerUtil.initialize( m_target );
initialized = true; }
protected Object[] getArguments( Constructor c ) throws Exception { Class[] paramTypes = c.getParameterTypes(); List args = new ArrayList();
for( int i = 0; i < paramTypes.length; i++ ) { if( paramTypes[i].isAssignableFrom( Logger.class ) ) { args.add( i, getLog() ); break; } if( paramTypes[i].isAssignableFrom( Context.class ) ) { args.add( i, getContext() ); break; } if( paramTypes[i].isAssignableFrom( Configuration.class ) ) { args.add( i, getConfiguration() ); break; } if( paramTypes[i].isAssignableFrom( ServiceManager.class ) ) { args.add( i, getServiceManager() ); break; }
if( getServiceManager().hasService( getRole( paramTypes[i] ) ) ) { Object comp = getServiceManager().lookup( paramTypes[i].getName() ); args.add( i, comp ); break; }
Object comp = null; if( (comp = getContext().get( getRole( paramTypes[i] ) ) ) != null ) { args.add( i, comp ); }
throw new ServiceException( paramTypes[i].getName(), "Missing dependency!" ); }
return args.toArray(); }
protected String getRole( Class clazz ) { try { Field field = clazz.getField( "ROLE" ); int mods = field.getModifiers(); if( Modifier.isPublic( mods ) && Modifier.isStatic( mods ) && Modifier.isFinal( mods ) ) return field.get( null ).toString(); else return clazz.getName(); } catch( NoSuchFieldException nsfe ) { return clazz.getName(); } catch( IllegalAccessException iae ) { return clazz.getName(); } }
/** * Sort constructors in the order to be tried. This * implementation sorts by whether a constructor is * public, next by the number of arguments (descending). * * @return */ protected static void sortConstructors( Constructor[] constr ) { Arrays.sort( constr, new Comparator() { public int compare( Object o1, Object o2 ) { Constructor c1 = (Constructor) o1; Constructor c2 = (Constructor) o2;
// compare accessibility if( Modifier.isPublic( c1.getModifiers() ) ) { if( Modifier.isPublic( c2.getModifiers() ) ) return -1; // more important } else if( Modifier.isPublic( c2.getModifiers() ) ) { return 1; // more important }
// both accessible, compare length int a1 = c1.getParameterTypes().length; int a2 = c2.getParameterTypes().length;
if( a1 > a2 ) return -1; // more important if( a1 == a2 ) return 0;
return 1; } } ); }
}
------------------------------------------------------------------------
--------------------------------------------------------------------- To unsubscribe, e-mail: [EMAIL PROTECTED] For additional commands, e-mail: [EMAIL PROTECTED]
-- http://www.thoughtworks.com -> The art of heavy lifting. Home for many Agile practicing, Open Source activists...
--------------------------------------------------------------------- To unsubscribe, e-mail: [EMAIL PROTECTED] For additional commands, e-mail: [EMAIL PROTECTED]
