Некоторое время назад мне довелось разрабатывать компоненты сред разработки для Netbeans и JDeveloper. Хм..., на самом деле довольно давно, и надо бы написать статью об этом пока не всё забыл и пока ещё облачные среды не захватили мир окончательно. Так вот, мне посчастливилось заглянуть во внутренности тех продуктов, которые мы используем каждый день, в данной статье я расскажу о некоторых аспектах устройства сред разработки и о принципах проектирования моделей используемых внутри джава IDE. В качестве примеров буду использовать Netbeans, но в других средах всё примерно также, ведь одинаковые проблемы порождают сходные решения.
IDE предоставляет инструменты редактирования документов (преимущественно текстовых файлов исходников) являющихся частью проекта. При этом мы не просто работаем с набором символов, как это обычно происходит в простом текстовом редакторе, а учитывается смысл содержимого документа, а также других документов в том же проекте, в проектах-зависимостях и в библиотеках-зависимостях.
При проектировании сред разработки активно используется архитектурный шаблон MVC. Слой V (View) — это то, что мы наблюдаем на экране, то, с чем взаимодействует пользователь: текстовые и графические редакторы, всевозможные навигаторы (по проекту, по дереву файлов проекта, по структуре файла), диалоговые окна и т.п… Под этим слоем скрыт невидимый слой M (Model) — это очень развитые объектные структуры представляющие данные, с которыми работает среда разработки т.е. документы в различных уровнях абстракции. Упомяну для порядка и C (Controller), это самый непонятный слой, под контроллерами понимают компоненты призванные менять данные, но нечасто получается выделить контроллер в виде независимого полноценного компонента, зачастую это всего лишь роль. Например, текстовой редактор — это, очевидно, вьюха документа, но в тоже время с помощью него вносятся изменения в содержимое, т.е. это вьюха вроде как ещё и контроллер.
Среда разработки работает с файлами, а значит нам нужны объекты, которые будут представлять эти файлы, а точнее положение этих файлов на диске. Тут есть исторический момент, длительное время в джаве не было API позволявшего отслеживать события об изменениях в файловой системе. Что же делать если нас интересуют события файловой системы, а эти события никто не рассылает? Есть стандартное решение: мы введём промежуточный слой — виртуальную файловую систему (VFS). Далее компоненты IDE работают с файловой системой исключительно через методы VFS, скажем, если вдруг нам надо создать новый файл мы обращаемся не к java.io.File, а просим виртуальную файловую систему создать файл, она в свою очередь создает файл используя тот самый java.io.File и затем рассылает событие о появлении нового файла. Все компоненты среды разработки заинтересованные в отслеживании событий появления файлов, подписываются на события VFS, и получают уведомление, что новый файл был создан.
Впрочем, осталась одна проблема: файлы в проекте могут меняться сторонними программами (фар, блокнот, git cli и т.п). Мне довелось наблюдать такое решение: чтобы изменить файл сторонней программой, надо в неё переключиться, а значит окно среды разработки потеряет фокус ввода, поэтому запустив рескан проекта, когда фокус вернется среде разработки, мы отловим почти все сторонние изменения. Сейчас, конечно, все IDE пытаются слушать события операционной системы где это возможно.
В Netbeans файлы на диске представлены с помощью класса FileObject.
Чтобы отобразить файл в каком-то редакторе, придётся загрузить его в память. А значит нам нужен объект, который будет представлять файл загружаемый в память. Зачастую в данном слое начинает учитываться содержимое файла. Ведь файлы хранят данные, причём данные разные в одном случае это джава код, в другом — XML в третьем JSON. XML данные тоже бывают разные это может быть мавеновский проект или файл настроек JPA. Можно заглянуть внутрь файла, посмотреть на его расширение и принять решение с помощью какого класса мы будем представлять файлы с данным содержимым. В среде Netbean файлы в памяти представлены с помощью класса DataObject.
Среда разрабоки кэширует эти объекты и каждый раз возвращает один и тот же инстанс для одного и того же физического файла. Т.е. эти объекты не создаются пользователем с помощью конструктора, а запрашиваются у платформы, например так:
Объектов данного слоя предоставляют разработчику доступ к содержимому физического файла, при этом, если это не было сделано ранее, содержимое прозрачно загружается средой разработки в память компьютера. Например:
Таким образом мы получили доступ к самой низкоуровневой модели представляющей содержимое текстового файла. Используемый в Netbeans BaseDocument реализует старый добрый свинговый интерфейс Document:
Иногда используют названия Text Buffer или Character Buffer, документ, но суть одна и та же — это просто массив символов переменной длины.
Тестовой буфер защищается от конкурентных изменений с помощью ассоциированного с ним лока. В Netbeans и JDeveloper это Read/Write лок, в эклипсе — обычный монитор.
Для внесения изменений необходимо явно или неявно захватить блокировку (блокировку на запись), далее модель изменяется и рассылаются события о внесенных изменениях. Тут, конечно, возможны варианты, но обычно события рассылаются в том же потоке, в котором осуществляются изменения, и до релиза лока, это создаёт определённую опасность, но и даёт пользователю больше свободы. (Нюанс: код обработчика события, выполняясь в треде владеющем write локом, может попытаться изменить эту модель, такое поведение иногда предпочитают запрещать) Вот так это всё выглядит для случая явной блокировки с использованием Read/Write лока:
Под контекстом компонента среды разработки понимаются: редактируемый файл (опционально), текущий проект (проект в рамках которого открыт данный файл), текущее выделение (selection). Соответственно, имеются модели проекта, выделения, а модель файла мы уже обсудили ранее.
При открытии редактора (или создании любого другого компонента) платформа среды разработки сообщает ему текущий контекст. Под «сообщает» понимается следующее: у редактора банально вызывается метод editor.setContext(context) или его аналог, а context это просто объект с тремя полями: файл, проект, выделение (selection).
Таким образом контекст — это отправная точка позволяющая редактору добыть всю информацию необходимую для работы инструментов таких как «автокомплит», «валидация» и т.п., использующих для своей работы информацию из других файлов. Эта информация представлена в виде высокоуровневых моделей и о их проектировании мы поговорим в следующей статье.
IDE предоставляет инструменты редактирования документов (преимущественно текстовых файлов исходников) являющихся частью проекта. При этом мы не просто работаем с набором символов, как это обычно происходит в простом текстовом редакторе, а учитывается смысл содержимого документа, а также других документов в том же проекте, в проектах-зависимостях и в библиотеках-зависимостях.
При проектировании сред разработки активно используется архитектурный шаблон MVC. Слой V (View) — это то, что мы наблюдаем на экране, то, с чем взаимодействует пользователь: текстовые и графические редакторы, всевозможные навигаторы (по проекту, по дереву файлов проекта, по структуре файла), диалоговые окна и т.п… Под этим слоем скрыт невидимый слой M (Model) — это очень развитые объектные структуры представляющие данные, с которыми работает среда разработки т.е. документы в различных уровнях абстракции. Упомяну для порядка и C (Controller), это самый непонятный слой, под контроллерами понимают компоненты призванные менять данные, но нечасто получается выделить контроллер в виде независимого полноценного компонента, зачастую это всего лишь роль. Например, текстовой редактор — это, очевидно, вьюха документа, но в тоже время с помощью него вносятся изменения в содержимое, т.е. это вьюха вроде как ещё и контроллер.
Файл на диске, виртуальная файловая система
Среда разработки работает с файлами, а значит нам нужны объекты, которые будут представлять эти файлы, а точнее положение этих файлов на диске. Тут есть исторический момент, длительное время в джаве не было API позволявшего отслеживать события об изменениях в файловой системе. Что же делать если нас интересуют события файловой системы, а эти события никто не рассылает? Есть стандартное решение: мы введём промежуточный слой — виртуальную файловую систему (VFS). Далее компоненты IDE работают с файловой системой исключительно через методы VFS, скажем, если вдруг нам надо создать новый файл мы обращаемся не к java.io.File, а просим виртуальную файловую систему создать файл, она в свою очередь создает файл используя тот самый java.io.File и затем рассылает событие о появлении нового файла. Все компоненты среды разработки заинтересованные в отслеживании событий появления файлов, подписываются на события VFS, и получают уведомление, что новый файл был создан.
Впрочем, осталась одна проблема: файлы в проекте могут меняться сторонними программами (фар, блокнот, git cli и т.п). Мне довелось наблюдать такое решение: чтобы изменить файл сторонней программой, надо в неё переключиться, а значит окно среды разработки потеряет фокус ввода, поэтому запустив рескан проекта, когда фокус вернется среде разработки, мы отловим почти все сторонние изменения. Сейчас, конечно, все IDE пытаются слушать события операционной системы где это возможно.
В Netbeans файлы на диске представлены с помощью класса FileObject.
Файл в памяти компьютера
Чтобы отобразить файл в каком-то редакторе, придётся загрузить его в память. А значит нам нужен объект, который будет представлять файл загружаемый в память. Зачастую в данном слое начинает учитываться содержимое файла. Ведь файлы хранят данные, причём данные разные в одном случае это джава код, в другом — XML в третьем JSON. XML данные тоже бывают разные это может быть мавеновский проект или файл настроек JPA. Можно заглянуть внутрь файла, посмотреть на его расширение и принять решение с помощью какого класса мы будем представлять файлы с данным содержимым. В среде Netbean файлы в памяти представлены с помощью класса DataObject.
Среда разрабоки кэширует эти объекты и каждый раз возвращает один и тот же инстанс для одного и того же физического файла. Т.е. эти объекты не создаются пользователем с помощью конструктора, а запрашиваются у платформы, например так:
DataObject myDataObject = DataObject.find(myFileObject);
Объектов данного слоя предоставляют разработчику доступ к содержимому физического файла, при этом, если это не было сделано ранее, содержимое прозрачно загружается средой разработки в память компьютера. Например:
BaseDocument myDocument = (BaseDocument) myDataObject.getCookie(EditorCookie.class).openDocument();
Текcтовое содержимое
Таким образом мы получили доступ к самой низкоуровневой модели представляющей содержимое текстового файла. Используемый в Netbeans BaseDocument реализует старый добрый свинговый интерфейс Document:
- воспользовавшись методом getLength() можно узнать количество символов в документе;
- getText(int offset, int length) — вернёт фрагмент документа в виде строки;
- а с помощью методов insertString(...) и remove(int offs, int len) можно вносить изменения.
Иногда используют названия Text Buffer или Character Buffer, документ, но суть одна и та же — это просто массив символов переменной длины.
Конкурентный доступ и нотификации
Модель — это представление некоторых данных в виде набора объектов. Вызывая методы данных объектов можно изменять модель. В момент изменений модель рассылает нотификации — по сути вызывает методы объектов зарегистрировавшихся в качестве наблюдателей.
Среда разработки многопоточна. Во-первых есть тред, в котором обрабатываются события нажатия клавиш, мышиные события, расскладываются и рендерятся UI-компоненты — GUI Event Dispatcher Thread (EDT). Набирая текст в редакторе мы вносим изменения в текстовой буфер именно в этом треде. А, скажем, выполняющий сложную операцию визард в этом треде будет крутить индикатор прогресса, а изменения в модели вносить в фоновом потоке. Вообще имеет смысл любую сколько-нибудь длительную операция запускать в фоновом потоке, чтобы избежать блокировок и подтормаживаний пользовательского интерфейса. Поэтому модели используемые в среде разработки как правило thread-safe.
Тестовой буфер защищается от конкурентных изменений с помощью ассоциированного с ним лока. В Netbeans и JDeveloper это Read/Write лок, в эклипсе — обычный монитор.
Для внесения изменений необходимо явно или неявно захватить блокировку (блокировку на запись), далее модель изменяется и рассылаются события о внесенных изменениях. Тут, конечно, возможны варианты, но обычно события рассылаются в том же потоке, в котором осуществляются изменения, и до релиза лока, это создаёт определённую опасность, но и даёт пользователю больше свободы. (Нюанс: код обработчика события, выполняясь в треде владеющем write локом, может попытаться изменить эту модель, такое поведение иногда предпочитают запрещать) Вот так это всё выглядит для случая явной блокировки с использованием Read/Write лока:
Платформа | Представление файла на диске | Представление файла в памяти | Текстовой буфер | Доступ к блокировкам |
---|---|---|---|---|
Netbeans | FileObject | DataObject | Document | Document readLock, writeLock |
JSR-198 | java.net.URI и статические методы класса VirtualFileSystem | javax.ide.model.Document | Document | javax.ide.model.Transaction |
Eclipse | IPath, IFileStore | IFileBuffer, ITextFileBuffer | IDocument | ISynchronizable |
JSR 198 (A Standard Extension API for Integrated Development Environments) — это попытка стандартизовать API и SPI среды разработки. Попытка провальная, имплементирована только в JDeveloper, и даже там никем не используется. Но эта штука имеет познавательную ценность — эдакая краткая выжимка айдэешных апи и концепций.
Контекст
Под контекстом компонента среды разработки понимаются: редактируемый файл (опционально), текущий проект (проект в рамках которого открыт данный файл), текущее выделение (selection). Соответственно, имеются модели проекта, выделения, а модель файла мы уже обсудили ранее.
При открытии редактора (или создании любого другого компонента) платформа среды разработки сообщает ему текущий контекст. Под «сообщает» понимается следующее: у редактора банально вызывается метод editor.setContext(context) или его аналог, а context это просто объект с тремя полями: файл, проект, выделение (selection).
Таким образом контекст — это отправная точка позволяющая редактору добыть всю информацию необходимую для работы инструментов таких как «автокомплит», «валидация» и т.п., использующих для своей работы информацию из других файлов. Эта информация представлена в виде высокоуровневых моделей и о их проектировании мы поговорим в следующей статье.