Интеграция 1С с сервисной шиной OpenESB
В данной статье рассматривается пример интеграции 1С с шиной сервисов OpenESB, и в более широком смысле — встраивание 1С в сервис-ориентированную архитектуру.
- Описание
- Подробнее
Описание
Эта статья является, в некотором роде, продолжением статьи «Интеграция 1С с шиной сообщений MSMQ», в ней будет рассмотрен пример организации взаимодействия 1С с шиной ESB, и, в конечном счете, ее встраивания в сервис-ориентированную архитектуру (SOA).
Общие соображения
С развитием стандартов веб-сервисов («сервисы») появилась идея организации универсального промежуточного ПО основанного на сервисах и являющегося универсальным фундаментом, позволяющим связывать и координировать работу разнообразных приложений и систем в сервис-ориентированной архитектуре.
Идея была простая и вытекала из желания объединить воедино всевозможные преобразователи, внешние запускали и пр. Собственно для организации шины ESB вполне можно использовать тот же подход, что и при использовании обычной шины сообщений, таким образом двумя главными элементами будут:
1. Некая промежуточная шина, которая позволяет принимать и передавать сообщения в согласованном формате.
2. Множества подключаемых модулей, которые принимают и передают сообщения — этими модулями могут быть коннекторы к внешнем системам или движок сервисов, получивший 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 ) и переписать все на С++.
Архив лежит здесь: //sale.itcity.ru/projects/5333/
P. S. Как обычно, жду конструктивной критики и вопросов. А вообще, чем больше копаю, тем больше прихожу к выводу, что «адынэс рулЕз» 🙂
P.P.S. Для тех кто хочет заглянуть чуть-чуть в будущее: http://wiki.open-esb.java.net/attach/FujiScreenCastsDemos/Fuji-overview-0629.pdf , хотя Фуджи пока на редкость глюкава и неустойчива.