Эта статья является, в некотором роде, продолжением статьи "Интеграция 1С с шиной сообщений MSMQ", в ней будет рассмотрен пример организации взаимодействия 1С с шиной ESB, и, в конечном счете, ее встраивания в сервис-ориентированную архитектуру (SOA).
Общие соображения
С развитием стандартов веб-сервисов ("сервисы") появилась идея организации универсального промежуточного ПО основанного на сервисах и являющегося универсальным фундаментом, позволяющим связывать и координировать работу разнообразных приложений и систем в сервис-ориентированной архитектуре.
Идея была простая и вытекала из желания объединить воедино всевозможные преобразователи, внешние запускали и пр. Собственно для организации шины ESB вполне можно использовать тот же подход, что и при использовании обычной шины сообщений, таким образом двумя главными элементами будут:
- Некая промежуточная шина, которая позволяет принимать и передавать сообщения в согласованном формате.
- Множества подключаемых модулей, которые принимают и передают сообщения - этими модулями могут быть коннекторы к внешнем системам или движок сервисов, получивший SOAP сообщение и переправляющий его движку BPEL (Busines Process Execution Language), который тоже оформлен в виде компонента.
Собственно нижеприведенная схема организации JBI ( http://en.wikipedia.org/wiki/Java_Business_Integration ) это иллюстрирует:
Таким образом получаем модульную среду, позволяющую объединить самые разные компоненты и системы. Однако не стоит идеализировать ESB, как и в любом другом ПО там могут быть свои собственные ошибки, например, в ранних версиях OpenESB где-то в глубине вылетал NullPointerException при попытке вызвать сервис на Mono и усе, кина не будет.
Другим важным вопросом является движок веб-сервисов, его задачи когда-то были довольно простыми: принять SOAP, приземлить вызов, забрать результат и отдать SOAP. Собственно встроенный движок 1С и находится на таком уровне, но сейчас от движка требуется, помимо этого, реализация множества стандартов, определяющих то или иное поведение, например, WS-ReliableMessaging (для гарантированной доставки сообщений), WS-Security (для шифрования, подписи и аутентификации - заметьте, имено это стандарт, а не поделка 1С с HTTP аутентификацией, потому что веб-сервисы в общем случе могут быть и не "веб"), WS-[Atomic]Transaction (для организации транзакционного поведения). Очевидно, что чем более развитым и надежным является движок, тем лучше. Эти соображения, а также желания сделать универсальный механизм, который можно легко прикрутить к любой конфигурации, сразу же ставят крест на встроенном движке 1С. Собственно выбор не богат, но это не значит, что плох - WCF под .NET.
Схема работы
Основная идея уже всплывала в комментариях к предыдущей статье: сделать универсальный windows сервис, который будет приземлять вызовы на 1С через COM. Собственно схему работы всей конструкции можно выразить одной строкой:
? <-n SOAP n-> OpenESB <-1 SOAP 1-> OneCService(WCF) <-1 COM n-> 1С
где ? - произвольные внешние системы, в примере их роль играют тесты
OneCService - собственно сервис, разработанный в рамках данного примера
в угловых скобках указаны способы взаимодействия и отношения
Такая схема позволит не вносить серьезных изменений в конфигурацию, а кроме того, получть универсальный механизм, в котором один промежуточный сервис позволяет взаимодействовать с разными базами 1С. В данном пример будет реализовано только взаимодействие с файловыми версиями, для взаимодействия с серверными версиями надо будет просто изменить механизм формирования строки соедиения для V8.Application и добавить дополнительные методы в интерфейс сервиса.
При этом, чтобы обеспечить универсальность, интерфейс сервиса должен содержать методы, позволяющие эффективно осуществлять все наиболее распространенные типы действий. В примере этими действиями будут: выполнения запроса и получения результата, выполнения скрипта на языке 1С и получение результата, а также - вызов произвольного метода с передачей параметров и получением результата. Также необходимо обеспечить возможность передачи сложных типов.
Инструментарий
В качестве инструментов для создания примера понадобятся:
- 1С 8.1 (две базы с одинаковой конфигурацией, не связаннные планом обмена, имитирующие две разнородные системы)
- .NET 3.5 SP1 + Sharpdevelop 3.1 (можно заменить на VisualStudio)
- Netbeans 6.5.1 + OpenESB 2.1 + жаба современного образца.
Тонкости с типами
Забегая вперед, рассмотрим один тонкий момент: при любом взаимодействие всегда надо обеспечить передачу данных в формате, понятным обеим сторонам. В 1С большую часть работы в этом плане за нас сделает СериализаторXDTO. Но есть ряд тонкостей:
1. Получение информации о принадлежности данного конкретного значения тому или иному типу.
Казалось бы все просто, все пользовались ТипЗнч(<значение>), но попробуйте выполнить простую строку:
ТипЗнч("ЙЦУКЕН");
ой, а это что такое:
Встроенная функция может быть использована только в выражении. (ТипЗнч)
ТипЗнч<>("ЙЦУКЕН");
при попытке вызвать ее через COM эффект будет не лучше.
Вот почему не получится полностью обойтись без модификации конфигурации, хотя типы можно получить и из результата запроса:
ВЫБРАТЬ 1, "А", Истина, Дата(1,1,1)
в этом случае доработка не требуется.
2. Отсутствие сериализации в XML универсальных коллекций штатными средствами.
А ведь так хочется сделать метод, который бы возвращал/принимал массив чего-то там, и дергать его из внешней системы...
Значит нужно обеспечить как минимум распознавание простых типов и универсальных коллекций (массив в данном примере) и их сериализацию/десериализацю. Со сложными типами проще - с элементом какого-нибудь справочника вполне управятся и штатные средства. При этом надо внести лишь минимальные дополнения в конфигурацию. Информация о типах потребуется в двух основных случаях - при формировании информации о типах колонок в ResultSet'е и при определении того, что значение относится к универсальной коллекции, все остальное будет отправляться в СериализоторXDTO.
При этом в примере реализовано сразу несколько способов определения типов, например, определить тип колонки в результате запроса при наличии образцов типов можно так:
public Type GetColumType(int _index)
{
object currentType = GetProperty(
Invoke(GetProperty(result, "Колонки"), "Получить", new object[] {_index}),
"ТипЗначения"
);
try
{
if ((bool)Invoke(currentType, "СодержитТип", new object[] {doubleType}))
{
return typeof(double);
}
else if ((bool)Invoke(currentType, "СодержитТип", new object[] {stringType}))
{
return typeof(string);
}
else if ((bool)Invoke(currentType, "СодержитТип", new object[] {boolType}))
{
return typeof(bool);
}
else if ((bool)Invoke(currentType, "СодержитТип", new object[] {dateType}))
{
return typeof(DateTime);
}
else
{
return null;
}
}
finally
{
Release(currentType);
}
}
В качестве примера использования, рассмотрим метод, возвращающий значение по индексу колонки, преобразованное в нужный формат:
public object GetValueByIndex(int _index)
{
if (GetColumType(_index) == null)
{
object o = Invoke(resultSet, "Получить", new object[] {_index});
o = GetObjectByRef(o);
return OneCObjectToXML(o);
}
else
{
return Invoke(resultSet, "Получить", new object[] {_index});
}
}
Как видно из кода, все довольно просто: определяется тип, если он примитивный, то значение возвращается как есть, а если нет - вызывается метод, сериализующий значение в XML, рассмотрим его:
public XmlElement OneCObjectToXML(object _o)
{
if (IsArray(_o))
{
XmlDocument doc = new XmlDocument();
XmlElement arrayElement = doc.CreateElement(OneCServiceArrayElement);
doc.AppendChild(arrayElement);
int count = (int)Invoke(_o, "Количество", new object[] {});
for (int i=0; i<count; i++)
{
object item = Invoke(_o, "Получить", new object[] {i});
if (item != null)
{
XmlElement itemElement = doc.CreateElement(OneCServiceArrayElement+"-item");
arrayElement.AppendChild(itemElement);
XmlElement itemValueElement = OneCObjectToXML(GetObjectByRef(item));
itemElement.AppendChild(doc.ImportNode(itemValueElement, true));
}
}
return doc.DocumentElement;
}
else
{
object writeXml = Invoke(connection, "NewObject", new object[] "ЗаписьXML"});
try
{
Invoke(writeXml, "УстановитьСтроку", new object[] {});
Invoke(xdtoSer, "ЗаписатьXML", new object[] {writeXml, _o});
//Заполнем буфер текстом xml представления 1с'овского объекта
string xmlString = (string)Invoke(writeXml, "Закрыть", new object[] {});
using (StringReader sr = new StringReader(xmlString))
{
XmlDocument doc = new XmlDocument();
doc.Load(sr);
return doc.DocumentElement;
}
}
finally
{
Release(writeXml);
}
}
}
Как видно из кода, здесь же происходит обработка ситуации, когда надо сериализовать универсальную коллекцию.
Также в примере используется и другие способы определения типов (вплоть до использования "is"), какой из них лучше - вопрос спорный.
Реализация
Ну а теперь, не отвлекаясь, можно рассмотреть собственно сервис. Начнем с интерфейса:
[ServiceContract(Name="onecservice", Namespace="http://onecservice")]
public interface IOneCWebService
{
[OperationContract(Name="ExecuteRequest")]
ResultSet ExecuteRequest(string _file, string _usr, string _pwd, string _request);
[OperationContract(Name="ExecuteScript")]
ResultSet ExecuteScript(string _file, string _usr, string _pwd, string _script);
[OperationContract(Name="ExecuteMethodWithXDTO")]
ResultSet ExecuteMethodWithXDTO(string _file, string _usr, string _pwd, string _methodName, XmlNode[] _parameters);
}
Как видно из кода, данный интерфейс обеспечивает сервису заявленную универсальную функциональность: выполнение запроса, выполнения скрипта, и выполнения метода с параметрами, которые могут быть сложными типами. Также все методы возвращают универсальный результат, рассмотрим его:
public class Value : IXmlSerializable
{
private XmlNode anyElement;
public XmlNode AnyElement
{
get { return anyElement; }
set { anyElement = value; }
}
public XmlSchema GetSchema()
{
return null;
}
public void ReadXml(XmlReader reader)
{
XmlDocument document = new XmlDocument();
anyElement = document.ReadNode(reader);
}
public void WriteXml(XmlWriter writer)
{
anyElement.WriteTo(writer);
}
}
[DataContract(Namespace="http://onecservice/types")]
public class Row
{
private List<XmlNode> values = new List<XmlNode>();
public List<XmlNode> ValuesList
{
get {return values;}
}
[DataMember]
public XmlNode[] Values
{
get {return values.ToArray();}
set {}
}
}
[DataContract(Namespace="http://onecservice/types")]
public class ResultSet
{
private List<string> columnNames = new List<string>();
private List<string> columnTypes = new List<string>();
private List<Row> rows = new List<Row>();
private string error = "";
[DataMember]
public string Error
{
get {return error;}
set {error = value;}
}
[DataMember]
public List<string> ColumnNames
{
get {return columnNames;}
}
[DataMember]
public List<string> ColumnTypes
{
get {return columnTypes;}
}
[DataMember]
public List<Row> Rows
{
get {return rows;}
}
public ResultSet()
{
}
}
Как видно из кода, универсальный результат может содержать как данные произвольного типа, так и метаданные в виде названия и простого текстового описания типа, также возможно хранения сообщения об ошибке. Собственно код реализующий функциональность сервиса здесь рассматриваться не будет, он приведен в архиве.
Рассмотрим также изменения, которые необходимо внести в конфигурацию 1С:
Функция ПринадлежитТипу(значение, тип) Экспорт
Возврат тип = ТипЗнч(значение);
КонецФункции
Функция ВыполнитьСтроку(стр) Экспорт
результат = Неопределено;
Выполнить стр;
Возврат результат;
КонецФункции
Две базы 1С с соответствующими изменениями в конфигурации и тестовым справочником приведены в архиве.
Таким образом у нас есть адаптированная конфигурация 1С и сервис, через который к ней можно достучаться откуда угодно. Собственно, результаты полученные на данном шаге уже имеют самостоятельную ценность, так как позволяют, например, выполнить запрос аля ВЫБРАТЬ * Из .... откуда угодно, например из веб приложения написанного на RoR'е.
Переходим к созданию так называемого композитного приложения, которое будет развернуто в OpenESB. В данное приложение будет входить один BPEL модуль, в котором будут располагаться несколько BPEL процессов, каждый из которых будет иллюстрировать работу с тем или иным методом сервиса OneCService. Там же будут располагаться wsdl, описывающий интерфейс OneCServic'а, а также wsdl'и описывающие точки входа BPEL процессов и xsd, описывающие типы данных. В композитном приложении будут созданы несколько тестов, которые сводятся к формированию запроса к тому или иному BPEL процессу и проверке на ожидаемый результат. Такая организация позволит разбить приложение на несколько небольших и понятных частей. Рассмотрим примеры этих частей более подробно.
В качестве примера BPEL процесса рассмотрим процесс, выполняющий запрос к 1С и возвращающий его результат, упакованный в ResultSet:
Множество модулей и сервисов, объединенных в одно композитной приложения образуют сборку сервисов:
Теперь рассмотрим более сложный пример в комплексе - обмен данными между двумя базами 1С. BPEL процесс имеет вид:
Как видим, здесь происходит сначала обращение к первой базе 1С ("ПолучитьСотрудников"), а потом ко второй ("ЗаписатьСотрудников"). Данные передаются в массиве, также здесь выполняется XSL преобразование чтобы извлечь массив из ResultSet'а, полученного в результате обращения к первой базе, для его последующей передачи во вторую базу.
Видео, иллюстрирующее работу схемы лежит здесь http://www.youtube.com/watch?v=NlvsvRpDf5o , в нем также демонстрируется пример пошаговой отладки BPEL процесса (пользуемся кнопочкой HD, так хоть что-то можно разглядеть).
Исходники композитного приложения также приведен в архиве.
Недостатки примера
1. Работа только с файловыми базой 1С.
2. Костыль для работы с типами (возможно я чего-то не понял).
Что можно улучшить
1. Добавить поддержку SQL баз.
2. Попытаться реализовать в поддержку транзакции средствами WCF и их приземление на 1С (не знаю насколько реально).
3. Возможно стоит переработать ResultSet.
4. Сделать возможность приземлять в 1С произвольные типы, объявленные во внешних схемах, добавленных в "Пакеты XDTO".
1C 8.2, Linux и Native API
Собственно соображение тут только одно: заменить WCF на Axis2/C ( http://ws.apache.org/axis2/c ) и переписать все на С++.
Архив лежит здесь: //infostart.ru/projects/5333/
P. S. Как обычно, жду конструктивной критики и вопросов. А вообще, чем больше копаю, тем больше прихожу к выводу, что "адынэс рулЕз" :)
P.P.S. Для тех кто хочет заглянуть чуть-чуть в будущее: http://wiki.open-esb.java.net/attach/FujiScreenCastsDemos/Fuji-overview-0629.pdf , хотя Фуджи пока на редкость глюкава и неустойчива.