Всем привет. Меня зовут Сергей и я руковожу разработкой автоматизации внутренних бизнес-процессов в рамках Центра управления процессами Московской Биржи.
В эпоху повсеместного распространения инструментов на базе искусственного интеллекта (ИИ) вопрос применения доменно-специфичных языков (DSL, domain specific language) вызывает неоднозначную реакцию. С одной стороны, большие языковые модели прекрасно понимают широкое многообразие естественного языка и могут работать напрямую с ним. С другой стороны, присущая любому человеческому общению неоднозначность оставляет простор для неправильных трактовок и ошибок.
Введение
Как отмечают многие исследователи, кодовые агенты наиболее эффективно работают в хорошо отстроенной системе ограничений. Ограничения могут быть мягкими, рекомендательными, как промпты и скиллы, так и жёсткими, например, компилятор или автоматическое тестирование. Задача разработчика при использовании ИИ сводится не столько к написанию кода, сколько к формированию качественного контекста и набора ограничений. Ручное тестирование и выявление ошибок кардинально увеличивает цикл обратной связи для ИИ-агента и должно применяться только в крайних случаях, когда цена ошибки слишком высока.
Мы в компании MOEX внимательно относимся к процессу разработки и рекомендациям службы информационной безопасности, чтобы предупредить последствия публикации некачественного кода. Это значит, что все наши проекты проходят проверки статическими и динамическими анализаторами кода, проходят композиционный анализ и тесты на проникновение. Со своей стороны мы делаем всё возможное, чтобы соответствовать положению 757-П Центрального Банка РФ «Об установлении обязательных для некредитных финансовых организаций требований к обеспечению защиты информации при осуществлении деятельности в сфере финансовых рынков в целях противодействия осуществлению незаконных финансовых операций».
Наиболее быструю обратную связь ИИ-агент получает от компилятора (интерпретатора) языка. Если мы хотим обеспечить строгую семантику критической части нашей логики, то есть проверенный способ построения доменно-специфичных языков на основе языков программирования общего назначения (GPPL, general purpose programming language). В статье рассмотрен подход по формированию типо-безопасного доступа к данным в условиях нестандартного интерфейса источника данных и его логических ограничений.
Каждый бэкенд-разработчик, работавший с реляционными базами, знает соблазн «просто собрать SQL-строку конкатенацией». Соблазн этот опасен: динамический SQL — классический вектор SQL-инъекций, и специалисты по информационной безопасности справедливо бьют по рукам за подобный код. Разбираемый в этой статье демо-проект [1] показывает элегантное решение: типо-безопасный DSL на базе библиотеки jOOQ, который превращает Java-код в SQL-запросы к резидентному хранилищу данных (In-Memory Data Grid, IMDG) вроде Apache Ignite, доступному только через кастомный REST-контракт. Ниже — разбор исторического контекста DSL, детальный анализ архитектуры проекта, аналогия с JPA (Java Persistence API) и взгляд на будущее DSL в эпоху ИИ-ассистированной разработки.
Исторические потребности и подходы к разработке DSL
Идея предметно-ориентированных языков не нова — она восходит к Lisp 1958 года, разработанному Джоном Маккарти, чья гомоиконность и макросистема позволяли встраивать мини-языки прямо в код общего назначения [2]. С тех пор DSL прошли путь от академических экспериментов до промышленного стандарта: SQL остаётся, вероятно, самым успешным DSL в истории, специализированным исключительно на манипуляции данными [3].
Классическая методология разработки DSL, зафиксированная ещё в работах по предметно-ориентированному программированию, включает три фазы [2][4]:
-
анализ — определение границ предметной области, сбор знаний экспертов, построение семантических нотаций;
-
реализация — создание библиотеки, воплощающей эти нотации, и компилятора/интерпретатора, транслирующего DSL-программы в вызовы этой библиотеки;
-
применение — написание и выполнение DSL-программ конечными разработчиками.
Исторически DSL делятся на внешние (со своим синтаксисом и парсером, как BNF (форма грамматики Бэкуса-Наура) или регулярные выражения) и внутренние — встроенные в синтаксис языка-хозяина через классы, методы и fluent-интерфейсы [5]. Второй подход резко снижает стоимость разработки: не нужен собственный парсер, IDE «из коробки» даёт автодополнение и проверку типов на этапе компиляции. Именно на этом принципе построены jOOQ, LINQ в .NET и Criteria API в JPA — все они реализуют внутренний DSL для построения запросов средствами языка общего назначения, избегая при этом уязвимостей текстовой конкатенации SQL. Ключевая мотивация подобных решений всегда одна и та же: изолировать разработчика от синтаксических ошибок и рисков инъекций, зафиксировав грамматику допустимых операций на уровне API [6].
К 2020-м годам интерес к DSL пережил новую волну — от финансовых расчётов до биоинформатики и низкокодовых платформ, а инструментарий для их создания расширился: ANTLR, Xtext, JetBrains MPS, встроенные метапрограммные возможности Kotlin и Scala [7]. Разбираемый ниже проект — характерный пример именно внутреннего DSL, решающего узкую, но болезненную задачу: безопасный доступ к нестандартному хранилищу данных.
Практический пример: DSL для IMDG на базе jOOQ
Постановка задачи и ограничения предметной области
В качестве примера возьмём структуру данных пользователей торговой системы Spectra срочного рынка Московской Биржи. Её контракт публичный и доступен для изучения всем желающим [17].
Проект jooq-dsl-demo [1] моделирует ситуацию, типичную для крупных финансовых и биржевых инфраструктур: данные хранятся в резидентной сетке (IMDG на основе Apache Ignite), доступ к которой предоставляется не через стандартный JDBC-драйвер, а через кастомный REST-эндпоинт /api/v2/queries/adhoc, принимающий SQL-текст в теле POST-запроса. Такая архитектура типична для Ignite: даже нативный REST API движка построен вокруг команд qryfldexe с SQL-подобным синтаксисом, инкапсулированным в HTTP-запрос [8].
При этом IMDG предметной области накладывает существенные ограничения на диалект SQL [1]:
-
отсутствует Data Definition Language (DDL) — схему нельзя изменить средствами SQL;
-
отсутствуют операции записи — INSERT, UPDATE, DELETE;
-
отсутствует поддержка JOIN между таблицами в SELECT-запросах;
-
имена «таблиц» фактически являются полностью квалифицированными именами Java-классов, а не классическими SQL-идентификаторами.
Эти ограничения — прямое следствие природы Ignite как объектно-ориентированного in-memory хранилища, где «таблица» — это по сути кэш объектов одного типа, а не запись в нормализованной реляционной схеме [9][10]. Именно поэтому официальная документация Ignite явно предупреждает: SqlFieldsQuery поддерживает произвольный SELECT, но объединения между кэшами требуют явного префиксирования именами кэшей, а не стандартного JOIN-синтаксиса [10].
Аналогия с JPA: разделение модели, транспорта и языка запросов
Схема архитектуры демо-проекта наглядно показывает три слоя: DSL-код на Java, runtime jOOQ, транспортный адаптер к REST и сам сервис IMDG. Разработчикам, знакомым с Java Persistence API, архитектура проекта покажется узнаваемой. JPA и Hibernate решают концептуально ту же задачу — дать разработчику безопасный, типизированный способ работать с хранилищем данных, не обращаясь к «сырому» SQL напрямую. Сопоставление слоёв даёт наглядную аналогию.
|
Слой ответственности |
JPA / Hibernate |
jOOQ-DSL для IMDG |
|
Объектная модель |
|
Классы |
|
Источник схемы |
Аннотации в коде или обратная генерация из БД |
Файл |
|
Транспортный слой |
|
|
|
Язык запросов |
JPQL, Criteria API |
|
|
Контроль допустимых операций |
Ограничения диалекта JPQL, каскады, |
|
Центральная идея обеих экосистем — отделить бизнес-код от деталей транспорта и синтаксиса запроса. В JPA это достигается через EntityManager, который абстрагирует JDBC; в разбираемом проекте эту роль исполняет класс RestMockDataProvider, реализующий интерфейс MockDataProvider из тестового набора jOOQ [1]. Он подключается к MockConnection, которая реализует java.sql.Connection, поэтому весь остальной код — конфигурация DSLContext, построение запроса — работает так, будто использует настоящий JDBC-драйвер, хотя фактически SQL-текст, полученный из AST после inlineSql(), отправляется HTTP-клиентом в теле JSON-запроса на REST-эндпоинт сервиса.
Конвертация метаданных сервиса в XML-схему для jOOQ
Ключевой инженерный трюк проекта — обратная разработка (reverse engineering) схемы данных. Поскольку сервис не предоставляет стандартного механизма получения DDL, мы конвертируем метаданные ответа сервиса в формате JSON (файл user-data.json) в XML-описание таблицы user-table.xml, соответствующее XSD-схеме jooq-meta [1]. Указанная трансформация JSON → XML выполнена с применением LLM-модели [1] — то есть уже на этапе разработки инструмента искусственный интеллект использовался как ассистент кодогенерации метаданных.
Файл user-table.xml описывает каталог default, схему public и единственную таблицу user с двадцатью одной колонкой типов TIMESTAMP, BIGINT, INTEGER, SMALLINT и VARCHAR, а также первичный ключ по полю login. Именно этот XML-файл читает jooq-codegen-maven, встроенный в сборку проекта, и генерирует на его основе Java-классы USER (метаданные таблицы) и UserRecord (типизированная строка результата) — аналог того, как Hibernate по аннотациям @Entity строит внутреннюю объектную модель. Разница в том, что в мире IMDG обратный путь короче: нет команды SHOW CREATE TABLE, поэтому схему нужно либо получать из документации сервиса, либо — как в этом случае — синтезировать её из примера ответа API, распознав типы значений и структуру полей.
Реализация ограничений: только чтение и запрет JOIN
Ограничения предметной области — отсутствие записи и джойнов — реализованы не как декларативные настройки, а как явные перехватчики (listener), встроенные в конфигурацию DSLContext через провайдеры jOOQ [1]:
-
ReadOnlyExecuteListenerрасширяетDefaultExecuteListenerи в методеrenderStartпроверяет тип выполняемой операции (ExecuteContext.type()); если тип равенExecuteType.WRITE, выбрасываетсяDataAccessExceptionс сообщением о запрете INSERT/UPDATE/DELETE -
NoJoinVisitListenerрасширяетDefaultVisitListenerи в методеvisitStartанализирует узлы AST‑дерева запроса; при обнаружении узлаClause.TABLE_JOINтакже выбрасывается исключение, блокирующее компиляцию запроса до отправки на сервер
Оба перехватчика подключаются к DefaultConfiguration через DefaultExecuteListenerProvider и DefaultVisitListenerProvider соответственно — паттерн, типичный для jOOQ, где поведение библиотеки расширяется через слушатели жизненного цикла запроса, а не через модификацию генерируемого кода. Это принципиально отличает подход от «постфактум»-валидации SQL-строки регулярными выражениями: проверка происходит на уровне синтаксического дерева запроса, до его материализации в текст, что исключает как ложноположительные, так и ложноотрицательные срабатывания, характерные для парсинга готовой SQL-строки.
Third-party контракт REST API реализован в RestMockDataProvider.execute(): метод подставляет значения параметров прямо в SQL-текст (метод inlineSql), формирует JSON-тело с полем sql, отправляет POST-запрос на /api/v2/queries/adhoc через стандартный java.net.http.HttpClient и разбирает ответ, извлекая массив объектов из пути response.result в JSON. Далее полученные строки превращаются в jOOQ Result<Record> через вспомогательный DSLContext.newResult(), что позволяет использовать стандартный механизм result.into(UserRecord.class) для типизации итогового набора записей. Для локальной отладки без реального сервиса IMDG в проекте используется WireMock, эмулирующий REST-эндпоинт и возвращающий заранее подготовленный JSON-файл user-data.json [1].
В качестве развития инструмента можно рассмотреть доработку maven плагина кодогенерации, обеспечив возможность генерации кода из метаданных в формате JSON без промежуточного XML-представления.
Перспективы DSL в эпоху AI-ассистированной разработки
Использование LLM для генерации XML-схемы из JSON-примера в разбираемом проекте — не случайность, а симптом более широкой тенденции. Внутренние DSL традиционно были компромиссом между выразительностью внешнего языка и простотой встраивания в существующую кодовую базу; ограничение всегда заключалось в том, что написание самого DSL и правил его валидации требовало экспертизы и в предметной области, и в языковой инженерии одновременно [6].
AI-инструменты меняют это уравнение сразу в нескольких направлениях. Во-первых, LLM снижают порог входа в domain-анализ: там, где ранее аналитику требовались недели интервью с экспертами предметной области для формализации семантики (классический этап анализа в методологии DSL [4]), сейчас модель может за минуты предложить первичную схему данных на основе примеров вывода системы — именно так был получен user-table.xml из user-data.json в разбираемом проекте. Во-вторых, LLM способны генерировать сам код DSL-обёртки — классы записей, конфигурации RenderMapping, перехватчики валидации — по текстовому описанию ограничений домена, ускоряя фазу реализации.
Более глубокая перспектива касается роли DSL как «контракта» между человеком и AI-агентом, генерирующим код. Строго типизированный внутренний DSL, подобный jOOQ, естественно ограничивает пространство возможных ошибок AI-генерации: компилятор Java отклонит некорректный вызов select().from().where() на этапе сборки, тогда как ошибка в текстовой SQL-строке, сгенерированной LLM, обнаружится только в runtime или вовсе останется незамеченной до продакшена. Комбинация «AI генерирует вызовы DSL» + «DSL гарантирует безопасность на уровне типов и AST-валидации» — жизнеспособный паттерн для доверенной AI-кодогенерации в чувствительных доменах, включая финансовую инфраструктуру, где написан этот демо-проект.
Одновременно ограничения текущих LLM создают новые требования к дизайну DSL: языки и API должны быть спроектированы так, чтобы модели было легко их «понимать» и корректно применять — с предсказуемой, консистентной сигнатурой методов, богатой типизацией и минимумом неявных побочных эффектов. Это созвучно классическим принципам хорошего DSL — минимализму и ясности синтаксиса [7] — но добавляет новый критерий: предсказуемость для statistical pattern matching, лежащего в основе генерации кода современными моделями.
Перспективы DSL для языков программирования вне Java
Хотя разбираемый проект реализован на Java, сама идея — типобезопасная обёртка над нестандартным транспортом данных с проверкой AST на этапе построения запроса — универсальна и переносима на другие экосистемы.
|
Экосистема |
Аналог внутреннего DSL для запросов |
Особенности переноса подхода |
|
.NET / C# |
LINQ (Language Integrated Query) |
Компилятор C# нативно поддерживает выражения‑деревья ( |
|
Kotlin |
Exposed, Ktorm |
Kotlin DSL-builders на основе lambda-с-receiver дают синтаксис, близкий к SQL, при полной типобезопасности |
|
Scala |
Slick, Quill |
Богатая система типов и макросы Scala позволяют генерировать SQL на этапе компиляции, а не runtime |
|
Python |
SQLAlchemy Core, Django ORM QuerySet |
Динамическая типизация ограничивает compile-time проверки, но допускает runtime-валидацию через AST-подобные объекты выражений |
|
Rust |
Diesel, SeaORM |
Строгая система типов и трейты позволяют закодировать ограничения предметной области (например, «только SELECT») прямо в типах на этапе компиляции |
|
Go |
Squirrel, GORM |
Отсутствие обобщений в старых версиях ограничивало выразительность DSL; начиная с Go 1.18 обобщённые типы приближают библиотеки к возможностям jOOQ |
Для функциональных языков, как Scala, стоит отметить, что их экосистема традиционно предлагает наиболее развитый инструментарий для встроенных DSL благодаря выразительной системе типов, неявным преобразованиям (implicit conversions) и макросам, что делает библиотеки Slick и Quill естественным аналогом jOOQ для доступа к нестандартным источникам данных, включая REST-обёртки над Ignite или другими IMDG. Общий паттерн — «сгенерировать метамодель из схемы (реальной или восстановленной), обернуть транспорт в адаптер к стандартному интерфейсу платформы, добавить перехватчики для доменных ограничений» — переносим почти без изменений: в Rust роль перехватчиков могут взять на себя типы-обёртки (newtype pattern), запрещающие построение запроса с недопустимой клаузой уже на этапе компиляции, а в динамических языках типа Python — runtime-валидаторы AST, аналогичные NoJoinVisitListener.
Ключевой вывод для мультиязычных команд: чем более выразительна система типов языка, тем раньше — в идеале на этапе компиляции — можно перехватывать нарушения контракта предметной области, будь то запрет JOIN, запрет операций записи или иные бизнес-ограничения нестандартного хранилища данных. Это делает инвестиции в разработку внутреннего DSL оправданными именно для языков со статической типизацией и богатой системой типов — Scala, Kotlin, Rust, TypeScript — где безопасность, продемонстрированная в разбираемом Java-проекте, достигается «бесплатно» на этапе сборки, а не ценой дополнительных runtime-проверок.
Заключение
В статье описан метод борьбы с распространённой уязвимостью «Инъекция» в сетевых сервисах. По данным популярного рейтинга безопасности «OWASP Top 10» за 2025 год проблема инъекций, по-прежнему, занимает стабильное место в середине списка. С учётом взрывного увеличения количества кода, сгенерированного ИИ, решение проблем защиты усложняется в разы. Своей целью в MOEX мы видим постоянный поиск и применение новых технологий, но со зрячим контролем результата и нивелированием возникающих вызовов для информационной безопасности.
Источники
-
GitHub — Unlocker/jooq‑dsl‑demo — исходный репозиторий с демо‑проектом DSL для доступа к IMDG через jOOQ (README, исходный код Java‑классов, user‑table.xml, user‑data.json,pom.xml) — github
-
Domain Specific Language (DSL) Tools — методология анализа, реализации и применения DSL — 2006.secrus
-
Apache Ignite Quick Start Guide, O’Reilly — Field based query — описание SqlFieldsQuery и ограничений SELECT‑запросов в Ignite — oreilly
-
Концепция DSL и особенности практического применения — исторический и практический обзор DSL на русском языке — rusnauka
-
Apache Ignite.NET — Class SqlFieldsQuery (официальная документация) — ignite.apache
-
Apache Ignite — SQL API (официальная документация) — ignite.apache
-
Querying with Apache Ignite (LinkedIn, Soumya Jyoti Banerjee) — практический разбор SQL‑запросов в Ignite — LinkedIn
-
GridGain Documentation — REST API — описание REST‑контракта для доступа к данным IMDG — gridgain
-
How to run Apache Ignite SQL queries over a REST API — stackoverflow
-
Создание DSL: как разработать собственный язык для специфической предметной области — it‑classic
-
GridGain SDK Javadoc — SqlFieldsQuery (Collocated Flag) — gridgain
-
Apache Ignite SQL query composed primary key.NET — stackoverflow
-
Предметно‑ориентированный язык — Википедия — ru.wikipedia
-
Apache Ignite 3 Documentation — SQL API — ignite.apache
-
Example of SQL query — Apache Mail Archives — lists.apache
-
When and how to develop domain‑specific languages — академическая работа о паттернах разработки DSL — dl.acm
-
Plaza II Шлюз. Версия 9.6 — Публичный FTP Московской Биржи — ftp.moex.com
Автор: unlocker


