Статьи

Взгляд внутрь JBoss Microcontainer, часть 3 — Виртуальная файловая система

Мы наконец вернулись с нашей следующей статьей в серии Microcontainer. В первых двух статьях мы продемонстрировали, как Microcontainer поддерживает различные модели компонентов , и продемонстрировали его мощные функции внедрения зависимостей . В этой статье мы расскажем о загрузке классов и развертывании, но сначала мы должны ознакомиться с VFS.

VFS стоит, как и ожидалось, для виртуальной файловой системы. Что VFS решает для нас, или почему это полезно?

Здесь, в JBoss, мы увидели, что много похожего кода обработки ресурсов было разбросано / продублировано повсюду.
В большинстве случаев это был код, который пытался определить, к какому типу ресурса относится конкретный ресурс, например, это файл, каталог или файл JAR, загружающий ресурсы через URL-адреса. Обработка вложенных архивов также была повторно реализована, и снова в разных библиотеках.

Прочитайте другие части эксклюзивной серии микроконтейнеров JBoss от DZone :

 

Пример:
 

public static URL[] search(ClassLoader cl, String prefix, String suffix) throws IOException
{
Enumeration[] e = new Enumeration[]{
cl.getResources(prefix),
cl.getResources(prefix + "MANIFEST.MF")
};
Set all = new LinkedHashSet();
URL url;
URLConnection conn;
JarFile jarFile;
for (int i = 0, s = e.length; i < s; ++i)
{
while (e[i].hasMoreElements())
{
url = (URL)e[i].nextElement();
conn = url.openConnection();
conn.setUseCaches(false);
conn.setDefaultUseCaches(false);
if (conn instanceof JarURLConnection)
{
jarFile = ((JarURLConnection)conn).getJarFile();
}
else
{
jarFile = getAlternativeJarFile(url);
}
if (jarFile != null)
{
searchJar(cl, all, jarFile, prefix, suffix);
}
else
{
boolean searchDone = searchDir(all, new File(URLDecoder.decode(url.getFile(), "UTF-8")), suffix);
if (searchDone == false)
{
searchFromURL(all, prefix, suffix, url);
}
}
}
}
return (URL[])all.toArray(new URL[all.size()]);
}

private static boolean searchDir(Set result, File file, String suffix) throws IOException
{
if (file.exists() && file.isDirectory())
{
File[] fc = file.listFiles();
String path;
for (int i = 0; i < fc.length; i++)
{
path = fc[i].getAbsolutePath();
if (fc[i].isDirectory())
{
searchDir(result, fc[i], suffix);
}
else if (path.endsWith(suffix))
{
result.add(fc[i].toURL());
}
}
return true;
}
return false;
}

Также было много проблем с блокировкой файлов в системах Windows, что вынудило нас скопировать все архивы с возможностью оперативного развертывания в другое место, чтобы предотвратить их блокировку в папках развертывания (что предотвратит их удаление и отмену развертывания на основе файловой системы).

Блокировка файлов была основной проблемой, которую можно было решить только путем централизации всего кода загрузки ресурсов в одном месте.

Признавая необходимость решения всех этих проблем в одном месте, оборачивая все это в простой и полезный API, мы создали проект VFS.

Открытый API VFS

Базовое использование в VFS можно разделить на две части:

  • простая навигация по ресурсам
  • API шаблона посетителя

Как уже упоминалось, в простой обработке ресурсов JDK навигация по ресурсам далеко не тривиальна. Вы всегда должны проверять, с каким ресурсом вы сейчас работаете, а это очень громоздко.

В VFS мы хотели ограничить это одним типом ресурса — VirtualFile.

public class VirtualFile implements Serializable
{
/**
* Get certificates.
*
* @return the certificates associated with this virtual file
*/
Certificate[] getCertificates()

/**
* Get the simple VF name (X.java)
*
* @return the simple file name
* @throws IllegalStateException if the file is closed
*/
String getName()

/**
* Get the VFS relative path name (org/jboss/X.java)
*
* @return the VFS relative path name
* @throws IllegalStateException if the file is closed
*/
String getPathName()

/**
* Get the VF URL (file://root/org/jboss/X.java)
*
* @return the full URL to the VF in the VFS.
* @throws MalformedURLException if a url cannot be parsed
* @throws URISyntaxException if a uri cannot be parsed
* @throws IllegalStateException if the file is closed
*/
URL toURL() throws MalformedURLException, URISyntaxException

/**
* Get the VF URI (file://root/org/jboss/X.java)
*
* @return the full URI to the VF in the VFS.
* @throws URISyntaxException if a uri cannot be parsed
* @throws IllegalStateException if the file is closed
* @throws MalformedURLException for a bad url
*/
URI toURI() throws MalformedURLException, URISyntaxException

/**
* When the file was last modified
*
* @return the last modified time
* @throws IOException for any problem accessing the virtual file system
* @throws IllegalStateException if the file is closed
*/
long getLastModified() throws IOException

/**
* Returns true if the file has been modified since this method was last called
* Last modified time is initialized at handler instantiation.
*
* @return true if modifed, false otherwise
  * @throws IOException for any error
*/
boolean hasBeenModified() throws IOException

/**
  * Get the size
*
* @return the size
  * @throws IOException for any problem accessing the virtual file system
* @throws IllegalStateException if the file is closed
*/
long getSize() throws IOException

/**
* Tests whether the underlying implementation file still exists.
* @return true if the file exists, false otherwise.
* @throws IOException - thrown on failure to detect existence.
*/
boolean exists() throws IOException

/**
* Whether it is a simple leaf of the VFS,
* i.e. whether it can contain other files
*
* @return true if a simple file.
* @throws IOException for any problem accessing the virtual file system
* @throws IllegalStateException if the file is closed
*/
boolean isLeaf() throws IOException

/**
* Is the file archive.
*
* @return true if archive, false otherwise
* @throws IOException for any error
*/
boolean isArchive() throws IOException

/**
* Whether it is hidden
*
* @return true when hidden
* @throws IOException for any problem accessing the virtual file system
* @throws IllegalStateException if the file is closed
*/
boolean isHidden() throws IOException

/**
* Access the file contents.
*
* @return an InputStream for the file contents.
* @throws IOException for any error accessing the file system
* @throws IllegalStateException if the file is closed
*/
InputStream openStream() throws IOException

/**
* Do file cleanup.
*
* e.g. delete temp files
*/
void cleanup()

/**
* Close the file resources (stream, etc.)
*/
void close()

/**
* Delete this virtual file
*
* @return true if file was deleted
* @throws IOException if an error occurs
*/
boolean delete() throws IOException

/**
* Delete this virtual file
*
* @param gracePeriod max time to wait for any locks (in milliseconds)
* @return true if file was deleted
* @throws IOException if an error occurs
*/
boolean delete(int gracePeriod) throws IOException

/**
* Get the VFS instance for this virtual file
*
* @return the VFS
* @throws IllegalStateException if the file is closed
*/
VFS getVFS()

/**
* Get the parent
*
* @return the parent or null if there is no parent
* @throws IOException for any problem accessing the virtual file system
* @throws IllegalStateException if the file is closed
*/
VirtualFile getParent() throws IOException

/**
* Get a child
*
* @param path the path
* @return the child or <code>null</code> if not found
* @throws IOException for any problem accessing the VFS
* @throws IllegalArgumentException if the path is null
* @throws IllegalStateException if the file is closed or it is a leaf node
*/
VirtualFile getChild(String path) throws IOException

/**
* Get the children
*
* @return the children
* @throws IOException for any problem accessing the virtual file system
* @throws IllegalStateException if the file is closed
*/
List<VirtualFile> getChildren() throws IOException

/**
* Get the children
*
* @param filter to filter the children
* @return the children
* @throws IOException for any problem accessing the virtual file system
* @throws IllegalStateException if the file is closed or it is a leaf node
*/
List<VirtualFile> getChildren(VirtualFileFilter filter) throws IOException

/**
* Get all the children recursively<p>
*
* This always uses {@link VisitorAttributes#RECURSE}
*
* @return the children
* @throws IOException for any problem accessing the virtual file system
* @throws IllegalStateException if the file is closed
*/
List<VirtualFile> getChildrenRecursively() throws IOException

/**
* Get all the children recursively<p>
*
* This always uses {@link VisitorAttributes#RECURSE}
*
* @param filter to filter the children
* @return the children
* @throws IOException for any problem accessing the virtual file system
* @throws IllegalStateException if the file is closed or it is a leaf node
*/
List<VirtualFile> getChildrenRecursively(VirtualFileFilter filter) throws IOException

/**
* Visit the virtual file system
*
* @param visitor the visitor
* @throws IOException for any problem accessing the virtual file system
* @throws IllegalArgumentException if the visitor is null
* @throws IllegalStateException if the file is closed
*/
void visit(VirtualFileVisitor visitor) throws IOException
}

Как видите, у вас есть все обычные операции файловой системы, доступные только для чтения, плюс несколько опций для очистки или удаления ресурса. Обработка очистки или удаления необходима, когда мы имеем дело с некоторыми внутренними временными файлами; например, от обработки вложенных банок.

Чтобы переключиться с обработки файловых или URL-ресурсов JDK на новый VirtualFile, нам нужен root. Это класс VFS, который знает, как его создать с помощью параметра URL или URI.

public class VFS
{
/**
* Get the virtual file system for a root uri
*
* @param rootURI the root URI
* @return the virtual file system
* @throws IOException if there is a problem accessing the VFS
* @throws IllegalArgumentException if the rootURL is null
*/
static VFS getVFS(URI rootURI) throws IOException

/**
* Create new root
*
* @param rootURI the root url
* @return the virtual file
* @throws IOException if there is a problem accessing the VFS
* @throws IllegalArgumentException if the rootURL
*/
static VirtualFile createNewRoot(URI rootURI) throws IOException

/**
* Get the root virtual file
*
* @param rootURI the root uri
* @return the virtual file
* @throws IOException if there is a problem accessing the VFS
* @throws IllegalArgumentException if the rootURL is null
*/
static VirtualFile getRoot(URI rootURI) throws IOException

/**
* Get the virtual file system for a root url
*
* @param rootURL the root url
* @return the virtual file system
* @throws IOException if there is a problem accessing the VFS
* @throws IllegalArgumentException if the rootURL is null
*/
static VFS getVFS(URL rootURL) throws IOException

/**
* Create new root
*
* @param rootURL the root url
* @return the virtual file
* @throws IOException if there is a problem accessing the VFS
* @throws IllegalArgumentException if the rootURL
*/
static VirtualFile createNewRoot(URL rootURL) throws IOException

/**
* Get the root virtual file
*
* @param rootURL the root url
* @return the virtual file
* @throws IOException if there is a problem accessing the VFS
* @throws IllegalArgumentException if the rootURL
*/
static VirtualFile getRoot(URL rootURL) throws IOException

/**
* Get the root file of this VFS
*
* @return the root
* @throws IOException for any problem accessing the VFS
*/
VirtualFile getRoot() throws IOException
}

Вы можете увидеть три разных метода, которые похожи друг на друга — getVFS, createNewRoot и getRoot. Метод getVFS возвращает экземпляр VFS, и, что важно, он еще не создает экземпляр VirtualFile. Почему это важно? Потому что есть методы, которые помогают нам настроить экземпляр VFS (см. Javadocs API класса VFS), прежде чем указывать ему создавать корень VirtualFile.

Другие два метода, с другой стороны, используют настройки по умолчанию для создания корня. Разница между createNewRoot и getRoot заключается в деталях кэширования, о которых мы поговорим позже.

URL rootURL = ...; // get root url
VFS vfs = VFS.getVFS(rootURL);
// configure vfs instance
VirtualFile root1 = vfs.getRoot();
// or you can get root directly
VirtualFile root2 = VFS.crateNewRoot(rootURL);
VirtualFile root3 = VFS.getRoot(rootURL);

Другая полезная вещь в VFS API — это реализация правильного шаблона посетителя. Таким образом, рекурсивно собирать разные ресурсы очень просто, что совершенно невозможно сделать с простой загрузкой JDK-ресурсов.

public interface VirtualFileVisitor
{
/**
* Get the search attribues for this visitor
*
* @return the attributes
*/
VisitorAttributes getAttributes();

/**
* Visit a virtual file
*
* @param virtualFile the virtual file being visited
*/
void visit(VirtualFile virtualFile);
}

VirtualFile root = ...; // get root
VirtualFileVisitor visitor = new SuffixVisitor(".class"); // get all classes
root.visit(visitor);

Архитектура VFS

Хотя общедоступный API довольно интуитивно понятен, реальные детали реализации немного сложнее. Мы постараемся объяснить концепции в кратчайшие сроки.

Каждый раз, когда вы создаете экземпляр VFS, создается соответствующий ему экземпляр VFSContext. Это создание сделано через VFSContextFactory. Различные протоколы отображаются на разные экземпляры VFSContextFactory — например, файл / vfsfile отображается на FileSystemContextFactory, zip / vfszip — на ZipEntryContextFactory.

Кроме того, каждый раз, когда создается экземпляр VirtualFile, создается соответствующий ему VirtualFileHandler. Именно этот экземпляр VirtualFileHandler знает, как правильно обрабатывать различные типы ресурсов — VirtualFile API просто делегирует вызовы своей ссылке на VirtualFileHandler.

Как и следовало ожидать, экземпляр VFSContext — это тот, который знает, как создавать экземпляры VirtualFileHandler в соответствии с типом ресурса — например, ZipEntryContextFactory создает ZipEntryContext, который затем создает ZipEntryHandler.

Существующие реализации

Помимо файлов, каталогов (FileHandler) и zip-архивов (ZipEntryHandler) мы также поддерживаем другие более экзотические способы использования.

Первый — это Assembled, который похож на то, что Eclipse называет Linked Resources. Его идея состоит в том, чтобы взять существующие ресурсы из разных деревьев и «смоделировать» их в единое дерево ресурсов.

AssembledDirectory sar = AssembledContextFactory.getInstance().create("assembled.sar");

URL url = getResource("/vfs/test/jar1.jar");
VirtualFile jar1 = VFS.getRoot(url);
sar.addChild(jar1);

url = getResource("/tmp/app/ext.jar");
VirtualFile ext1 = VFS.getRoot(url);
sar.addChild(ext);

AssembledDirectory metainf = sar.mkdir("META-INF");

url = getResource("/config/jboss-service.xml");
VirtualFile serviceVF = VFS.getRoot(url);
metainf.addChild(serviceVF);

AssembledDirectory app = sar.mkdir("app.jar");
url = getResource("/app/someapp/classes");
VirtualFile appVF = VFS.getRoot(url);
app.addPath(appVF, new SuffixFilter(".class"));

Другая реализация — файлы в памяти. В нашем случае это связано с необходимостью легко обрабатывать сгенерированные AOP байты. Вместо того, чтобы возиться с временными файлами, мы просто помещаем байты в VirtualFileHandlers в памяти.

URL url = new URL("vfsmemory://aopdomain/org/acme/test/Test.class");
byte[] bytes = ...; // some AOP generated class bytes
MemoryFileFactory.putFile(url, bytes);

VirtualFile classFile = VFS.getVirtualFile(new URL("vfsmemory://aopdomain"), "org/acme/test/Test.class");
InputStream bis = classFile.openStream(); // e.g. load class from input stream

Хуки для расширения.

Очень просто расширить VFS с помощью нового протокола, подобного тому, что мы сделали с Assembled и Memory.
Все, что вам нужно, — это сочетание реализаций VFSContexFactory, VFSContext, VirtualFileHandler, FileHandlerPlugin и URLStreamHandler. Первый из них тривиален, а остальные зависят от сложности вашей задачи — например, вы можете реализовать rar, tar, gzip или даже удаленный доступ.

В конце вы просто регистрируете этот новый VFSContextFactory в VFSContextFactoryLocator.

Посмотрите демонстрацию этой статьи для простого примера gzip

 

Особенности

Одной из первых серьезных проблем, с которыми мы столкнулись, было правильное использование вложенных ресурсов, точнее, вложенных jar-файлов.

например, нормальное развертывание уха: gema.ear / ui.war / WEB-INF / lib / struts.jar

Для чтения содержимого struts.jar у нас есть два варианта:

  • обрабатывать ресурсы в памяти
  • создавать временные копии вложенных банок верхнего уровня, рекурсивно

Первый вариант проще в реализации, но он занимает очень много памяти — просто представьте огромные приложения в памяти.
Другой подход оставляет кучу временных файлов, которые должны быть невидимы для простого пользователя. Следовательно, ожидая, что они исчезнут после развертывания.

Теперь представьте следующий сценарий: пользователь получает экземпляр URL-адреса VFS, который указывает на некоторый вложенный ресурс.

Простой способ VFS справится с этим — воссоздать весь путь с нуля, то есть снова и снова распаковывать вложенные ресурсы. Это привело бы (и это сделало) к огромной куче временных файлов.
Как этого избежать? Мы подошли к этому с помощью VFSRegistry, VFSCache и TempInfo.

Когда вы запрашиваете VirtualFile поверх VFS (getRoot, а не createNewRoot), VFS запрашивает реализацию файла VFSRegistry. Существующий DefaultVFSRegistry сначала проверяет, существует ли соответствующий корневой VFSContext для указанного URI. Если это так, он сначала пытается перейти к существующей TempInfo (ссылка на временные файлы), возвращаясь к обычной навигации, если такой временный файл не существует. Таким образом, мы полностью повторно используем любые уже распакованные временные файлы, экономя время и место на диске. Если в кэше не найдено соответствующего VFSContext, мы создаем новую запись VFSCache и продолжаем навигацию по умолчанию.

Затем применяется реализация VFSCache, как она обрабатывает кэшированные записи VFSContext. VFSCache настраивается через VFSCacheFactory — по умолчанию мы ничего не кешируем, но есть несколько полезных реализаций VFSCache, начиная от LRU и заканчивая таймерным кешем.

Вариант использования API

Существует класс VFSUtils, который является частью общедоступного API и является своего рода дампом полезной функциональности. Он содержит множество полезных методов и настроек конфигурации (собственно, ключей системных свойств). Проверьте API Javadocs для более подробной информации.

Существующие проблемы / обходные пути

Еще одна проблема, которая возникла — как и ожидалось, — это неспособность некоторых сред работать должным образом поверх VFS. Проблема заключалась в пользовательских URL-адресах VFS, таких как: vfsfile, vfszip, vfsmemory.

В большинстве случаев вы все еще могли бы обойти это с помощью простого URL или использования URLConnection, но многие фреймворки строго соответствуют протоколу файла или jar, что, конечно, не помогает.

Мы смогли исправить некоторые фреймворки (например, Facelets) и предоставить расширения для других (например, Spring).
Если вы разработчик библиотеки, и в вашей библиотеке есть простой подключаемый механизм загрузки ресурсов, мы предлагаем вам просто расширить его с помощью реализации на основе VFS. Если нет никаких хуков, попробуйте ограничить свои предположения более общим использованием на основе URL или URLConnection.


Заключение

Хотя VFS очень приятна в использовании, она стоит своей цены. Он добавляет дополнительный уровень поверх обработки ресурсов JDK, а это означает, что при работе с ресурсами всегда присутствуют дополнительные вызовы.

Мы также храним некоторую информацию о работе с jar-файлами в памяти, чтобы упростить доступ к конкретному ресурсу, но за счет некоторого дополнительного потребления памяти.

В целом VFS оказалась очень полезной библиотекой, поскольку она скрывает многие варианты использования, которые являются болезненными для простого JDK, и предоставляет всеобъемлющий API для работы с ресурсами — то есть реализацию шаблонов посетителей.
Мы постоянно следим за отзывами пользователей о проблемах VFS, с которыми они сталкиваются, делая каждую версию немного лучше.

Теперь, когда мы познакомились с VFS, пришло время перейти к новому слою Classloading MC!

 

об авторе

Семь лет назад он влюбился в Java и большую часть времени занимался разработкой информационных систем — от обслуживания клиентов до управления энергопотреблением. Он присоединился к JBoss в 2006 году, чтобы полностью посвятить себя работе над проектом Microcontainer, который в настоящее время является его руководителем. Он также участвует в JBoss AS и является специалистом по интеграции Seam и Spring. Он представляет JBoss в группах экспертов «Поддержка динамических компонентов JSR-291 для Java SE» и «OSGi».