Зарегистрировать пользовательский URLStreamHandler в веб-приложении Spring (Tomcat)

я пытаюсь зарегистрировать заказ URLStreamHandler для обработки запросов к URL-адресам Amazon S3 в общем виде. Реализация обработчика выглядит в значительной степени как S3-URLStreamHandler (github). Я не ставил свой Handler класса в sun.net.www.protocol.s3 пакет, но используется пользовательский пакет com.github.dpr.protocol.s3. Чтобы сделать Java pickup этим пакетом, я предоставил системное свойство -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)
    ...

мое приложение-это веб-приложение на основе Spring, которое в настоящее время работает в 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: }

загрузчик классов из java.net.URL класса (используется Class.forName) составляет null указание загрузчика класса начальной загрузки и загрузчика системного класса не знает мой класс. Если я помещаю туда точку останова и пытаюсь загрузить класс обработчика с помощью загрузчика класса текущего потока, он работает нормально. То есть мой класс, по-видимому, существует и находится в пути к классу приложения, но Java использует "неправильные" загрузчики классов для поиска обработчика.

я в курсе этот вопрос на SOF, но я не должен зарегистрируйте пользовательский URLStreamHandlerFactory поскольку tomcat регистрирует свою собственную фабрику (org.apache.naming.resources.DirContextURLStreamHandlerFactory) при запуске приложения и должна быть зарегистрирована только одна фабрика для одного JVM. Котяра это 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 установки, это может привести к нежелательным последствиям из-за разных версий одной библиотеки в classpath.

Вариант 3-Поместите обработчик JAR в {CATALINA_HOME} / lib
Это не работает. Согласно документация загрузчика классов Tomcat ресурсы, помещенные в этот каталог, будут загружены с помощью Tomcat's общие загрузчика. Этот загрузчик классов не будет использоваться 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));
  }
}

1 ответов


можно:

1. Используйте декоратор

один из способов установить пользовательскую URLStreamHandlerFactory, может быть, использовать декоратор типа URLStreamHandlerFactory для того чтобы обернуть URLStreamHandlerFactory это, возможно, уже было определено (в данном случае tomcat). Хитрая часть заключается в том, что вам нужно использовать отражение (которое довольно хаки), чтобы получить и сбросить текущий 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);
    }
}

здесь является ли код для его определения:

// 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)
        );
    }
}

NB: начиная с Java 9, вам нужно будет добавить --add-opens java.base/java.net=myModuleName к вашей команде запуска, чтобы позволить глубокое отражение на пакете java.net это включает в себя класс URL из вашего модуля myModuleName в противном случае вызов setAccessible(true) буду ставить RuntimeException.


2. Разверните его как расширение

еще один способ избежать этого ClassLoder проблема может заключаться в перемещении вашего пользовательского URLStreamHandler в специальный сосуд и разверните его в ${JAVA-HOME}/jre/lib/ext как установленное расширение (со всеми его зависимостями), так что он будет доступен в расширении ClassLoader, таким образом, ваш класс будет определен в ClassLoader достаточно высоко в иерархии, чтобы быть замеченными.