From cca236a8b22095b887e0877d77a4b274805f0d0d Mon Sep 17 00:00:00 2001 From: XU JINCHUAN Date: Fri, 15 Jul 2016 22:36:44 +0800 Subject: [PATCH] Fix the tcnative lib loading problem in OSGi Motivation: As the issue #5539 say, the OpenSsl.class will throw `java.lang.UnsatisfiedLinkError: org.apache.tomcat.jni.Library.version(I)I` when it is invoked. This path try to resolve the problem by modifying the native library loading logic of OpenSsl.class. Modifications: The OpenSsl.class loads the tcnative lib by `NativeLibraryLoader.loadFirstAvailable()`. The native library will be loaded in the bundle `netty-common`'s ClassLoader, which is diff with the native class's ClassLoader. That is the root cause of throws `UnsatisfiedLinkError` when the native method is invoked. So, it should load the native library by the its bundle classloader firstly, then the embedded resources if failed. Result: First of all, the error threw by native method problem will be resolved. Secondly, the native library should work as normal in non-OSGi env. But, this is hard. The loading logic of `Library.class` in `netty-tcnative` bundle is simple: try to load the library in PATH env. If not found, it falls back to the originally logic `NativeLibraryLoader.loadFirstAvailable()`. Signed-off-by: XU JINCHUAN --- .../util/internal/NativeLibraryLoader.java | 123 ++++++++++++++++-- .../util/internal/NativeLibraryUtil.java | 45 +++++++ 2 files changed, 159 insertions(+), 9 deletions(-) create mode 100644 common/src/main/java/io/netty/util/internal/NativeLibraryUtil.java diff --git a/common/src/main/java/io/netty/util/internal/NativeLibraryLoader.java b/common/src/main/java/io/netty/util/internal/NativeLibraryLoader.java index 711efaf652..b39c2df3bc 100644 --- a/common/src/main/java/io/netty/util/internal/NativeLibraryLoader.java +++ b/common/src/main/java/io/netty/util/internal/NativeLibraryLoader.java @@ -19,12 +19,16 @@ import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; import java.io.Closeable; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.lang.reflect.Method; import java.net.URL; +import java.security.AccessController; +import java.security.PrivilegedAction; import java.util.Arrays; import java.util.Locale; @@ -159,7 +163,7 @@ public final class NativeLibraryLoader { public static void loadFirstAvailable(ClassLoader loader, String... names) { for (String name : names) { try { - NativeLibraryLoader.load(name, loader); + load(name, loader); return; } catch (Throwable t) { logger.debug("Unable to load the library: " + name + '.', t); @@ -170,7 +174,7 @@ public final class NativeLibraryLoader { } /** - * Load the given library with the specified {@link java.lang.ClassLoader} + * Load the given library with the specified {@link ClassLoader} */ public static void load(String name, ClassLoader loader) { String libname = System.mapLibraryName(name); @@ -187,7 +191,7 @@ public final class NativeLibraryLoader { if (url == null) { // Fall back to normal loading of JNI stuff - System.loadLibrary(name); + loadLibrary(loader, name, false); return; } @@ -209,13 +213,13 @@ public final class NativeLibraryLoader { } out.flush(); - System.load(tmpFile.getPath()); + loadLibrary(loader, tmpFile.getPath(), true); } catch (Exception e) { throw (UnsatisfiedLinkError) new UnsatisfiedLinkError( "could not load a native library: " + name).initCause(e); } finally { - safeClose(in); - safeClose(out); + closeQuietly(in); + closeQuietly(out); // After we load the library it is safe to delete the file. // We delete the file immediately to free up resources as soon as possible, // and if this fails fallback to deleting on JVM exit. @@ -225,10 +229,111 @@ public final class NativeLibraryLoader { } } - private static void safeClose(Closeable closeable) { - if (closeable != null) { + /** + * Loading the native library into the specified {@link ClassLoader}. + * @param loader - The {@link ClassLoader} where the native library will be loaded into + * @param name - The native library path or name + * @param absolute - Whether the native library will be loaded by path or by name + */ + private static void loadLibrary(final ClassLoader loader, final String name, final boolean absolute) { + try { + // Make sure the helper is belong to the target ClassLoader. + final Class newHelper = tryToLoadClass(loader, NativeLibraryUtil.class); + loadLibraryByHelper(newHelper, name, absolute); + } catch (Exception e) { // Should by pass the UnsatisfiedLinkError here! + logger.debug("Unable to load the library: " + name + '.', e); + NativeLibraryUtil.loadLibrary(name, absolute); // Fallback to local helper class. + } + } + + private static void loadLibraryByHelper(final Class helper, final String name, final boolean absolute) { + AccessController.doPrivileged(new PrivilegedAction() { + @Override + public Object run() { + try { + // Invoke the helper to load the native library, if succeed, then the native + // library belong to the specified ClassLoader. + Method method = helper.getMethod("loadLibrary", + new Class[] { String.class, boolean.class }); + method.setAccessible(true); + return method.invoke(null, name, absolute); + } catch (Exception e) { + throw new IllegalStateException("Load library failed!", e); + } + } + }); + } + + /** + * Try to load the helper {@link Class} into specified {@link ClassLoader}. + * @param loader - The {@link ClassLoader} where to load the helper {@link Class} + * @param helper - The helper {@link Class} + * @return A new helper Class defined in the specified ClassLoader. + * @throws ClassNotFoundException Helper class not found or loading failed + */ + private static Class tryToLoadClass(final ClassLoader loader, final Class helper) + throws ClassNotFoundException { + try { + return loader.loadClass(helper.getName()); + } catch (ClassNotFoundException e) { + // The helper class is NOT found in target ClassLoader, we have to define the helper class. + final byte[] classBinary = classToByteArray(helper); + return AccessController.doPrivileged(new PrivilegedAction>() { + @Override + public Class run() { + try { + // Define the helper class in the target ClassLoader, + // then we can call the helper to load the native library. + Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, + byte[].class, int.class, int.class); + defineClass.setAccessible(true); + return (Class) defineClass.invoke(loader, helper.getName(), classBinary, 0, + classBinary.length); + } catch (Exception e) { + throw new IllegalStateException("Define class failed!", e); + } + } + }); + } + } + + /** + * Load the helper {@link Class} as a byte array, to be redefined in specified {@link ClassLoader}. + * @param clazz - The helper {@link Class} provided by this bundle + * @return The binary content of helper {@link Class}. + * @throws ClassNotFoundException Helper class not found or loading failed + */ + private static byte[] classToByteArray(Class clazz) throws ClassNotFoundException { + String fileName = clazz.getName(); + int lastDot = fileName.lastIndexOf('.'); + if (lastDot > 0) { + fileName = fileName.substring(lastDot + 1); + } + URL classUrl = clazz.getResource(fileName + ".class"); + if (classUrl == null) { + throw new ClassNotFoundException(clazz.getName()); + } + byte[] buf = new byte[1024]; + ByteArrayOutputStream out = new ByteArrayOutputStream(4096); + InputStream in = null; + try { + in = classUrl.openStream(); + for (int r; (r = in.read(buf)) != -1;) { + out.write(buf, 0, r); + } + return out.toByteArray(); + } catch (IOException ex) { + throw new ClassNotFoundException(clazz.getName(), ex); + } finally { + closeQuietly(in); + closeQuietly(out); + } + } + + private static void closeQuietly(Closeable c) { + if (c != null) { try { - closeable.close(); + c.close(); } catch (IOException ignore) { // ignore } diff --git a/common/src/main/java/io/netty/util/internal/NativeLibraryUtil.java b/common/src/main/java/io/netty/util/internal/NativeLibraryUtil.java new file mode 100644 index 0000000000..1f9bf85457 --- /dev/null +++ b/common/src/main/java/io/netty/util/internal/NativeLibraryUtil.java @@ -0,0 +1,45 @@ +/* + * Copyright 2016 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.util.internal; + +/** + * A Utility to Call the {@link System#load(String)} or {@link System#loadLibrary(String)}. + * Because the {@link System#load(String)} and {@link System#loadLibrary(String)} are both + * CallerSensitive, it will load the native library into its caller's {@link ClassLoader}. + * In OSGi environment, we need this helper to delegate the calling to {@link System#load(String)} + * and it should be as simple as possible. It will be injected into the native library's + * ClassLoader when it is undefined. And therefore, when the defined new helper is invoked, + * the native library would be loaded into the native library's ClassLoader, not the + * caller's ClassLoader. + */ +final class NativeLibraryUtil { + /** + * Delegate the calling to {@link System#load(String)} or {@link System#loadLibrary(String)}. + * @param libName - The native library path or name + * @param absolute - Whether the native library will be loaded by path or by name + */ + public static void loadLibrary(String libName, boolean absolute) { + if (absolute) { + System.load(libName); + } else { + System.loadLibrary(libName); + } + } + + private NativeLibraryUtil() { + // Utility + } +}