`

『原创』OSGI研究笔记1 - Equinox ServletBridge模式下调用Datasource

阅读更多

getInitialContext

 

 

最近3个星期一直在研究OSGi框架与现有项目的结合,由于该方案可能进度问题,不能够用得上,但研究过程艰辛,该经验需要记录下来。研究方向如下:

 

1. 与现有的SOA接口集成,包括EJB, WebServie的调用

2. 与J2EE容器集成, 包括开发环境Tomcat 6 和 生产环境 IBM Websphere 6.0

3. 与数据库接口的集成,包括JDBC 和 JNDI 形式调用

 

开发OSGI与传统的开发中不同,首先开发的环境不同,其次是Classloader。

  • 1.传统开发中,项目都集中在同一个Java Project/Dynamic Web Project,而且所有类都在同一个classloader中加载,即Web容器或者Application容器
  • 2.OSGi开发中,每个bundle都有各自的classloader, 参考<<OSGi 实战.pdf>>。由于classloader的问题,常常会引起ClassNotFoundException,尤其是当你把OSGi嵌入到Web容器的时候,当bundle中的类需要调用容器中的资源的时候,如用JNDI查找数据源,往往会遇到ClassNotFoundExceptionClassCastException问题。

 

请看以下的代码:

以下代码是摘自org.springframework.jndi.JndiTemplate。

public class JndiTemplate

 

 

protected Context createInitialContext() throws NamingException {
		Hashtable icEnv = null;
		Properties env = getEnvironment();
		if (env != null) {
			icEnv = new Hashtable(env.size());
			CollectionUtils.mergePropertiesIntoMap(env, icEnv);
		}
		return new InitialContext(icEnv);
	}

DAO代码 

 

 

public class UserDao extends SqlMapClientDaoSupport implements IUserDao 

 

 

以下是bundle中Spring-DM的配置文件

 

 

<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
		<property name="jndiName" value="${jndi.url}"/>
	</bean>
	

	<!-- Transaction manager for a single JDBC DataSource -->
	<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
		<property name="dataSource" ref="dataSource"/>
	</bean>

	<!-- SqlMap setup for iBATIS Database Layer -->
	<bean id="sqlMapClient" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
		<property name="configLocations" value="META-INF/sql-map-config*.xml"/>
		<property name="dataSource" ref="dataSource"/>
	</bean>

	<!-- ========================= DAO DEFINITIONS: IBATIS IMPLEMENTATIONS ========================= -->
	<bean id="userDao" class="org.amway.osgi.internal.dao.UserDao">
		<property name="sqlMapClient" ref="sqlMapClient"/>
	</bean>

	<!-- ========================= OSGI SERVICE DEFINITIONS ========================= -->
	<osgi:service ref="userDao" interface="com.amway.osgi.dao.test.IUserDao" />
 

 

 这里使用Spring-DM注册DAO服务,在spring-dm启动时候会扫描BundleContext中所有bundle的META-INF/spring/*.xml, 并且创建OsgiApplicationContext。

 

注意JndiTemplate中createInitialContext方法中new InitialContext(icEnv)。追踪源码,会看到Context是通过ContextFactory生成的,而这个ContextFactory由TCCL(Thread Context Class Loader)加载的。 

 

public static Context getInitialContext(Hashtable<?,?> env)
	throws NamingException {
	InitialContextFactory factory; 

	InitialContextFactoryBuilder builder = getInitialContextFactoryBuilder();
	if (builder == null) {
	    // No factory installed, use property
	    // Get initial context factory class name

	    String className = env != null ?
	        (String)env.get(Context.INITIAL_CONTEXT_FACTORY) : null;
	    if (className == null) {
		NoInitialContextException ne = new NoInitialContextException(
		    "Need to specify class name in environment or system " +
		    "property, or as an applet parameter, or in an " +
		    "application resource file:  " + 
		    Context.INITIAL_CONTEXT_FACTORY);
		throw ne;
	    }

	    try {
		factory = (InitialContextFactory)
		    helper.loadClass(className).newInstance();
	    } catch(Exception e) {
		NoInitialContextException ne = 
		    new NoInitialContextException(
			"Cannot instantiate class: " + className);
		ne.setRootCause(e);
		throw ne;
	    }
	} else {
	    factory = builder.createInitialContextFactory(env);
	}

	return factory.getInitialContext(env);
    }
 
final class VersionHelper12 extends VersionHelper { 

ClassLoader getContextClassLoader() {
	return (ClassLoader) AccessController.doPrivileged(
	    new PrivilegedAction() {
		public Object run() {
		    return Thread.currentThread().getContextClassLoader();
		}
	    }
	);

}
 
问题就在这里!!! 由于TCCL是当前Bundle的class loader,而不是容器的class loader,所以这里就会报ClassNotFoundException,那么就需要设置TCCL为Web容器的class loader!!参考《<<Manning.OSGi.in.Action.2011>> 的8.2.5章节。
ClassLoader oldTCCL = Thread.currentThread().getContextClassLoader();
try {
Thread.currentThread().setContextClassLoader(newTCCL);
...
} catch (Throwable e) {
...
} finally {
Thread.currentThread().setContextClassLoader(oldTCCL);
}
bi
这里我们需要通过继承JndiTemplate并且覆盖createInitialContext()来创建InitialContext
       @Override
	protected Context createInitialContext() throws NamingException {
		InitialContext context = null;
		// 1 Get current class loader(osgi bundle loader)
		Thread current = Thread.currentThread();
		ClassLoader old = current.getContextClassLoader();
		try {
			// 2 Get BridgeServlet class loader(web container class loader)
			ClassLoader newCl = BridgeServlet.class.getClassLoader();

			// 3 set current class loader to web ctx class loader
			current.setContextClassLoader(newCl);
			context = new InitialContext();
			
		} catch (NamingException e) {
			e.printStackTrace();
		} finally {
			current.setContextClassLoader(old);
		}
		return context;
	}
 
改写完之后,当Spring注入的时候就会调用相应的context.lookup(jndi),例如: jndi 为java:comp/env/jdbc/test,这时候后台就会报错:  java:comp 不能够解释。
通过 debug发现context中的java.naming.factory.initial 不是 tomcat中的org.apache.naming.java.javaURLContextFactory,这里发现就算通过TCCL去获取到外部容器的Context,但是当lookup的时候,会在当前bundle的class loader中查找,不是外部容器,故java:comp 不能够解释。
那么我们可以直接返回lookup的对象,不返回context,那么覆盖以下方法:
public Object lookup(final String name) throws NamingException {
		if (logger.isDebugEnabled()) {
			logger.debug("Looking up JNDI object with name [" + name + "]");
		}
		
		
		return JndiUtil.execute(new JndiCallback<Object>() {
			public Object doInContext(Context ctx) throws NamingException {
				Object located = ctx.lookup(name);
				if (located == null) {
					throw new NameNotFoundException(
							"JNDI object with [" + name + "] not found: JNDI implementation returned null");
				}
				
				return located;
			}
		});
		 
	}
 
public static <T> T execute(Hashtable environment,JndiCallback<T> contextCallback) throws NamingException {
		T result = null;
		InitialContext context = null;
		// 1 Get current class loader(osgi bundle loader)
		Thread current = Thread.currentThread();
		ClassLoader old = current.getContextClassLoader();
		try {
			// 2 Get BridgeServlet class loader(web container class loader)
			ClassLoader newCl = BridgeServlet.class.getClassLoader();

			// 3 set current class loader to web ctx class loader
			current.setContextClassLoader(newCl);
			context = new InitialContext(environment );

			result = contextCallback.doInContext(context);
			
		} catch (NamingException e) {
			e.printStackTrace();
		} finally {
			current.setContextClassLoader(old);
		}
		return result;
	}
 
这时,返回的对象就是Tomcat容器返回的Datasource。注意,这里的BridgeServlet,是个值得在这里展开的。可以参考该文章http://www.ibm.com/developerworks/cn/opensource/os-cn-eclosgisb/
通过Servlet Bridge形式嵌入的Web容器的Equinox OSGi 解决方案中,BridgeServlet是一个关键的类。
这个特殊的类,其实是在Web容器下的,通过org.eclipse.equinox.servletbridge.extensionbundle 这个extension bundle去export 容器中system中的类可以被Bundle引用
org.eclipse.equinox.servletbridge.extensionbundle的MANIFEST.MF文件

Fragment-Host: system.bundle; extension:=framework
Export-Package: javax.servlet;version="2.5",javax.servlet.htt
 p;version ="2.5",javax.servlet.resources;version="2.5",org.eclipse.eq
 uinox.servletbridge;version="1.1"
 ====================================================================================
接下来更加苦闷的事情发生了,Spring 发现该Datasource对象不是javax.sql.Datasource的实现,其实这个就提示了,该Tomcat Datasource 是由外部容器去加载的, 但当JVM在向上转型的时候,当前的引用javax.sql.Datasource是当前bundle中的,
那么这里引出一个问题:不同class loader的类,虽然类名一样,但是JVM都会认为是不用的类,这里会产生ClassCastException . 可以参考http://www.ibm.com/developerworks/cn/java/j-lo-classloader/
通过阅读servletbridge.jar的源码,org.eclipse.equinox.servletbridge.FrameworkLauncher中startup中
ClassLoader original = Thread.currentThread().getContextClassLoader();
		try {
			System.setProperty("osgi.framework.useSystemProperties", "false"); //$NON-NLS-1$ //$NON-NLS-2$

			URL[] frameworkURLs = findFrameworkURLs(initialPropertyMap);
			frameworkClassLoader = new ChildFirstURLClassLoader(frameworkURLs, this.getClass().getClassLoader());
			Class clazz = frameworkClassLoader.loadClass(STARTER);

			Method setInitialProperties = clazz.getMethod("setInitialProperties", new Class[] {Map.class}); //$NON-NLS-1$
			setInitialProperties.invoke(null, new Object[] {initialPropertyMap});

			registerRestartHandler(clazz);

			Method runMethod = clazz.getMethod("startup", new Class[] {String[].class, Runnable.class}); //$NON-NLS-1$
			runMethod.invoke(null, new Object[] {args, null});

			Method getSystemBundleContext = clazz.getMethod("getSystemBundleContext", new Class[0]); //$NON-NLS-1$
			Object bundleContext = getSystemBundleContext.invoke(null, new Object[0] );
			
			
			frameworkContextClassLoader = Thread.currentThread().getContextClassLoader();
		} catch (InvocationTargetException ite) {
			Throwable t = ite.getTargetException();
			if (t == null)
				t = ite;
			context.log("Error while starting Framework", t); //$NON-NLS-1$
			throw new RuntimeException(t.getMessage());
		} catch (Exception e) {
			context.log("Error while starting Framework", e); //$NON-NLS-1$
			throw new RuntimeException(e.getMessage());
		} finally {
			Thread.currentThread().setContextClassLoader(original);
		}
 这里受到了启发。 该处代码是容器里面启动Equinox OSGi的关键地方,该处ChildFirstURLClassLoader是在配置文件config.ini中的osgi.framework中指定的equinox的jar的class loader。之后通过反射去调用该class loader加载的 framework的startup 方法。注意,这是2个不同class loader交互的桥梁.
接下来我们通过这个思想去改进我们的代码。
1.创建一个可以跨class loader的接口:CrossClassloaderObjectWrapper
public interface CrossClassloaderObjectWrapper{
	
	public void setDelegate(Object object);
	
	public void setOutsideClassloader(ClassLoader classloader);
	
	public void setInsideClassloader(ClassLoader classloader);
}
 
2.实现一个的Datasource包装类DataSourceWrapper
public class DataSourceWrapper implements CrossClassloaderObjectWrapper,
		DataSource {

	private Object dataSource;

	private ClassLoader outsideClassloader;

	private Class clazz;

	public DataSourceWrapper() {

	}

	public void setDelegate(Object t) {
		dataSource = t;
	}

	private Class getOutsideClass() {
		if (clazz == null) {
			try {
				if (this.outsideClassloader != null) {
					clazz = (Class<DataSource>) this.outsideClassloader
							.loadClass("javax.sql.DataSource");
				}
			} catch (ClassNotFoundException e) {
				e.printStackTrace();
			}
		}
		return clazz;
	}

	public Connection getConnection() throws SQLException {
		Method m = ReflectionUtils.findMethod(getOutsideClass(), "getConnection");
		Connection connect = (Connection) ReflectionUtils.invokeMethod(m,
				dataSource);
		return connect;
	}

	public Connection getConnection(String username, String password)
			throws SQLException {
		Method m = ReflectionUtils.findMethod(getOutsideClass(), "getConnection",
				String.class, String.class);
		Connection connect = (Connection) ReflectionUtils.invokeMethod(m,
				dataSource, username, password);
		return connect;
	}

	public PrintWriter getLogWriter() throws SQLException {
		Method m = ReflectionUtils.findMethod(getOutsideClass(), "getLogWriter");
		return (PrintWriter) ReflectionUtils.invokeMethod(m, dataSource);
	}

	public void setLogWriter(PrintWriter out) throws SQLException {
		Method m = ReflectionUtils.findMethod(getOutsideClass(), "setLogWriter",
				PrintWriter.class);
		ReflectionUtils.invokeMethod(m, dataSource, out);
	}

	public void setLoginTimeout(int seconds) throws SQLException {
		Method m = ReflectionUtils.findMethod(getOutsideClass(), "setLoginTimeout",
				Integer.class);
		ReflectionUtils.invokeMethod(m, dataSource, seconds);

	}

	public int getLoginTimeout() throws SQLException {
		Method m = ReflectionUtils.findMethod(getOutsideClass(), "getLoginTimeout");
		return (Integer) ReflectionUtils.invokeMethod(m, dataSource);
	}

	public <T> T unwrap(Class<T> iface) throws SQLException {
		Method m = ReflectionUtils.findMethod(getOutsideClass(), "unwrap", Class.class);
		return (T) ReflectionUtils.invokeMethod(m, dataSource, iface);
	}

	public boolean isWrapperFor(Class<?> iface) throws SQLException {
		Method m = ReflectionUtils.findMethod(getOutsideClass(), "isWrapperFor",
				Class.class);
		return (Boolean) ReflectionUtils.invokeMethod(m, dataSource, iface);
	}

	public void setOutsideClassloader(ClassLoader classloader) {
		outsideClassloader = classloader;
	}

	public void setInsideClassloader(ClassLoader classloader) {
		// TODO Auto-generated method stub

	}
}
 
3.改写JndiTemplate继承类
public class ServletJndiTemplate extends JndiTemplate {


	@Override
	public Object lookup(final String name) throws NamingException {
		if (logger.isDebugEnabled()) {
			logger.debug("Looking up JNDI object with name [" + name + "]");
		}
		final DataSourceWrapper wrapper = new DataSourceWrapper();
		 Object object = JndiUtil.execute(new JndiCallback<Object>() {
			public Object doInContext(Context ctx) throws NamingException {
				Object located = ctx.lookup(name);
				if (located == null) {
					throw new NameNotFoundException(
							"JNDI object with [" + name + "] not found: JNDI implementation returned null");
				}
				wrapper.setDelegate(located);
				ClassLoader cl = Thread.currentThread().getContextClassLoader();
				System.out.println("JndiUtil.execute: current class loader:" + cl);
				wrapper.setOutsideClassloader(cl);
				return located;
			}
		});
		 
		 return wrapper;
	}

	
}
 
4.最终的Spring 配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
        xmlns:context="http://www.springframework.org/schema/context"
        xmlns:osgi="http://www.springframework.org/schema/osgi"
        xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
            http://www.springframework.org/schema/osgi http://www.springframework.org/schema/osgi/spring-osgi.xsd">


	<!-- ========================= RESOURCE DEFINITIONS ========================= -->
	<bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
		<property name="locations">
			<list>
				<value>META-INF/datasource.properties</value>
			</list>
		</property>
	</bean>
	
	<!-- ========================= Datasource: jdbc ========================= -->
<!-- 	<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
		<property name="driverClassName" value="${jdbc.driverClassName}"/>
		<property name="url" value="${jdbc.url}"/>
		<property name="username" value="${jdbc.username}"/>
		<property name="password" value="${jdbc.password}"/>
	</bean> -->
	
	<bean id="osgiJndiTemplate" class="org.amway.osgi.jndi.internal.ServletJndiTemplate">
	</bean>
	
	<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
		<property name="jndiTemplate" ref="osgiJndiTemplate"/>
		<property name="jndiName" value="${jndi.url}"/>
	</bean>
	
<!-- 	<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
		<property name="jndiName" value="${jndi.url}"/>
	</bean> -->
	

	<!-- Transaction manager for a single JDBC DataSource -->
	<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
		<property name="dataSource" ref="dataSource"/>
	</bean>

	<!-- SqlMap setup for iBATIS Database Layer -->
	<bean id="sqlMapClient" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
		<property name="configLocations" value="META-INF/sql-map-config*.xml"/>
		<property name="dataSource" ref="dataSource"/>
	</bean>

	<!-- ========================= DAO DEFINITIONS: IBATIS IMPLEMENTATIONS ========================= -->
	<bean id="userDao" class="org.amway.osgi.internal.dao.UserDao">
		<property name="sqlMapClient" ref="sqlMapClient"/>
	</bean>

	<!-- ========================= OSGI SERVICE DEFINITIONS ========================= -->
	<osgi:service ref="userDao" interface="com.amway.osgi.dao.test.IUserDao" />


</beans>
 
最后,spring-dm加载的时候不再报任何错误。通过action->service->dao调用测试,成功!!!下班。
分享到:
评论
1 楼 java_xiaoyi 2016-12-22  
,对我很有帮助!!

相关推荐

Global site tag (gtag.js) - Google Analytics