在Spring Web应用程序(Tomcat)中注册自定义URLStreamHandler

dpr*_*dpr 7 java url spring tomcat

我正在尝试注册一个自定义URLStreamHandler来以通用方式处理对Amazon S3 URL的请求.处理程序的实现看起来非常像S3-URLStreamHandler(github).我没有将我的Handler课程放入sun.net.www.protocol.s3包中,而是使用自定义包com.github.dpr.protocol.s3.为了使Java拾取这个包,我提供了-Djava.protocol.handler.pkgs="com.github.dpr.protocol"遵循URL类文档的系统属性.但是,如果我尝试处理类似的s3-URL s3://my.bucket/some-awesome-file.txt,我会得到MalformedURLException:

Caused by: java.net.MalformedURLException: unknown protocol: s3
    at java.net.URL.<init>(URL.java:600)
    at java.net.URL.<init>(URL.java:490)
    at java.net.URL.<init>(URL.java:439)
    at java.net.URI.toURL(URI.java:1089)
    ...
Run Code Online (Sandbox Code Playgroud)

我的应用程序是一个基于Spring的Web应用程序,目前在tomcat中运行,但不应该对底层应用程序容器的任何知识混乱.

我已经调试了相应的代码,发现我URLStreamHandler无法初始化,因为用于加载类的类加载器不知道它.这是java.net.URL(jdk 1.8.0_92)的相应代码:

1174: try {
1175:   cls = Class.forName(clsName);
1176: } catch (ClassNotFoundException e) {
1177:   ClassLoader cl = ClassLoader.getSystemClassLoader();
1178:   if (cl != null) {
1179:     cls = cl.loadClass(clsName);
1180:   }
1181: }
Run Code Online (Sandbox Code Playgroud)

类的类加载器java.net.URL(由...使用Class.forName)null表示引导类加载器,系统类加载器不知道我的类.如果我在那里放置一个断点并尝试使用当前线程的类加载器加载我的处理程序类,它可以正常工作.这是我的类显然存在于应用程序的类路径中,但Java使用"错误的"类加载器来查找处理程序.

在SOF上知道这个问题,但我不能注册一个自定义,URLStreamHandlerFactory因为tomcat org.apache.naming.resources.DirContextURLStreamHandlerFactory在应用程序启动时注册它自己的factory(),并且只能为一个JVM注册一个工厂.Tomcat DirContextURLStreamHandlerFactory允许添加可用于处理自定义协议的用户工厂,但我不想在我的应用程序代码中向Tomcat添加依赖项,因为应用程序应该在不同的容器中运行.

有没有办法以独立于容器的方式为自定义URL注册处理程序?


更新2017-01-25:
我给了不同的选择@Nicolas Filotto提出了一个尝试:

选项1 - 使用反射设置自定义URLStreamHandlerFactory
此选项按预期工作.但是通过使用反射,它引入了对java.net.URL类的内部工作的紧密依赖性.幸运的是,Oracle并不急于修复基本java类中的便利问题 - 实际上这个相关的错误报告已经开放了近14年(伟大的工作Sun/Oracle) - 这很容易进行单元测试.

选项2 - 将处理程序JAR放入{JAVA_HOME}/jre/lib/ext
这个选项也可以.但是,只添加处理程序jar作为系统扩展将无法做到 - 当然.人们还需要添加处理程序的所有依赖项.由于这些类对使用此Java安装的所有应用程序都是可见的,因此可能会由于类路径中相同库的不同版本而导致不希望的影响.

选项3 - 将处理程序JAR放入{CATALINA_HOME}/lib
这不起作用.根据Tomcat的类加载器文档,将使用Tomcat的Common类加载器加载放入此目录的资源.java.net.URL查找协议处理程序不会使用此类加载器.

鉴于这些选项,我将继续使用反射变体.所有选项都不是很好,但至少第一个选项可以轻松测试,不需要任何部署逻辑.但是我稍微调整了代码以使用相同的锁对象进行同步java.net.URL:

public static void registerFactory() throws Exception  {
  final Field factoryField = URL.class.getDeclaredField("factory");
  factoryField.setAccessible(true);
  final Field lockField = URL.class.getDeclaredField("streamHandlerLock");
  lockField.setAccessible(true);

  // use same lock as in java.net.URL.setURLStreamHandlerFactory
  synchronized (lockField.get(null)) {
    final URLStreamHandlerFactory urlStreamHandlerFactory = (URLStreamHandlerFactory) factoryField.get(null);
    // Reset the value to prevent Error due to a factory already defined
    factoryField.set(null, null);
    URL.setURLStreamHandlerFactory(new AmazonS3UrlStreamHandlerFactory(urlStreamHandlerFactory));
  }
}
Run Code Online (Sandbox Code Playgroud)

Nic*_*tto 7

你可以:

1. 使用装饰器

设置自定义URLStreamHandlerFactory, 的一种方法可能是使用类型的装饰器URLStreamHandlerFactory来包装URLStreamHandlerFactory可能已经定义的(在这种情况下是由 tomcat 定义的)。棘手的部分是您需要使用反射(这很hacky)来获取和重置当前factory潜在定义的事实。

这是您的装饰器的伪代码:

public class S3URLStreamHandlerFactory implements URLStreamHandlerFactory {

    // The wrapped URLStreamHandlerFactory's instance
    private final Optional<URLStreamHandlerFactory> delegate;

    /**
     * Used in case there is no existing URLStreamHandlerFactory defined
     */
    public S3URLStreamHandlerFactory() {
        this(null);
    }

    /**
     * Used in case there is an existing URLStreamHandlerFactory defined
     */
    public S3URLStreamHandlerFactory(final URLStreamHandlerFactory delegate) {
        this.delegate = Optional.ofNullable(delegate);
    }

    @Override
    public URLStreamHandler createURLStreamHandler(final String protocol) {
        if ("s3".equals(protocol)) {
            return // my S3 URLStreamHandler;
        }
        // It is not the s3 protocol so we delegate it to the wrapped 
        // URLStreamHandlerFactory
        return delegate.map(factory -> factory.createURLStreamHandler(protocol))
            .orElse(null);
    }
}
Run Code Online (Sandbox Code Playgroud)

这是定义它的代码:

// Retrieve the field "factory" of the class URL that store the 
// URLStreamHandlerFactory used
Field factoryField = URL.class.getDeclaredField("factory");
// It is a package protected field so we need to make it accessible
factoryField.setAccessible(true);
// Get the current value
URLStreamHandlerFactory urlStreamHandlerFactory 
    = (URLStreamHandlerFactory) factoryField.get(null);
if (urlStreamHandlerFactory == null) {
    // No factory has been defined so far so we set the custom one
    URL.setURLStreamHandlerFactory(new S3URLStreamHandlerFactory());
} else {
    // Retrieve the field "streamHandlerLock" of the class URL that
    // is the lock used to synchronize access to the protocol handlers 
    Field lockField = URL.class.getDeclaredField("streamHandlerLock");
    // It is a private field so we need to make it accessible
    lockField.setAccessible(true);
    // Use the same lock to reset the factory
    synchronized (lockField.get(null)) {
        // Reset the value to prevent Error due to a factory already defined
        factoryField.set(null, null);
        // Set our custom factory and wrap the current one into it
        URL.setURLStreamHandlerFactory(
            new S3URLStreamHandlerFactory(urlStreamHandlerFactory)
        );
    }
}
Run Code Online (Sandbox Code Playgroud)

注意:从 Java 9 开始,您需要添加--add-opens java.base/java.net=myModuleName到启动命令中以允许对java.net包含URL模块中类的包进行深度反射,myModuleName否则调用setAccessible(true)将引发RuntimeException.


2. 将其部署为扩展

应该避免此ClassLoder问题的另一种方法可能是将您的自定义移动URLStreamHandler到专用 jar 并将其部署${JAVA-HOME}/jre/lib/ext为已安装的扩展(及其所有依赖项),以便它在 Extension 中可用ClassLoader,这样您的类将被定义在ClassLoader在层次结构中足够高,可以看到。