Проблема с отображениями
В нашем проекте мы столкнулись с действительно большой проблемой, связанной с картированием. Имея две системы, которые изначально были определены BA как нечто подобное, мы выбрали простое XSLT (выполненное через Altova Mapforce) входного сообщения для выходного.
Впоследствии оказалось, что функции, необходимые для выполнения картирования, становятся огромными. Пример такого отображения:
Из входного сообщения возьмите список автомобилей итерируйте его и найдите автомобиль, у которого поле «prodcutionDate» является наименьшим, а атрибут «make» равен «Honda», а в качестве результата выведите «saleDate»
Поэтому, чтобы отобразить его, мы решили перейти на JBoss Drools. В
таблицах решения были вне вопроса , так как логика была сложной и настроена быть помещены в таблицу , чтобы мы закодированы все в DRL файлах. Вскоре правила стали действительно большими, и некоторые наши разработчики были вынуждены уделять много времени постоянному пересмотру правил, установленных БА.
Из-за разочарования и после того, как я увидел все удивительные вещи на конференции
33-й степени, я решил начать искать решения своих проблем:
- Файлы DRL имеют большой размер и стали недоступными (для одного поля у нас было, например, 4 правила)
- Поскольку БА в своей жизни никогда не кодировал ни единого правила Drools / XSLT, добавив простое утверждение if … else … для него не проблема
- БА должен ждать реализации картирования разработчиками, пока он не сможет проверить ее
- Разработчики тратят слишком много времени на кодирование правил отображения вместо разработки других функций
После постановки этих проблем было проведено исследование, касающееся картографических структур, и одной из концепций, над которой я начал работать, была попытка создать картографию в Groovy. Поскольку Groovy (благодаря, например, PropertyMissing и MethodMissing) является идеальным языком для создания DSL, я решил начать сразу же. Единственные две вещи, о которых я должен был помнить:
- Текущее приложение написано исключительно на Java
- Код сопоставления (чтобы выполнить быстрое тестирование) должен быть отделен от приложения как такового — он не может быть скомпилирован во время развертывания, потому что мы хотим иметь возможность частой замены сопоставлений
Структура проекта
Определив язык, я создал следующие ограничения:
Структура проекта
Как видите, структура проекта очень проста. Для начала он встроен в
Gradle . Основная функция может быть найдена в
XmlTransformer.java . Поток таков, что
TransformerFactory создает
Transformer на основе скрипта Groovy, который вышел из
ScriptFactory (в нашем проекте для разных типов продуктов, которые мы различаем по полю в файле XML, у нас есть разные файлы DRL). Сценарии Groovy находятся в пути к классам в
папке / groovy / (конечно, в конце дня эти сценарии должны быть размещены вне любых jar-файлов).
В
build.gradle
apply plugin: 'java' group = 'com.blogspot.toomuchcoding' version = '1.0' repositories { mavenCentral() } dependencies { compile 'org.codehaus.groovy:groovy-all:2.0.5' compile 'org.slf4j:slf4j-log4j12:1.7.2' compile 'log4j:log4j:1.2.16' compile 'com.google.guava:guava:14.0' testCompile group: 'junit', name: 'junit', version: '4.+' } task(executeMain, dependsOn: 'classes', type: JavaExec) { main = 'com.blogspot.toomuchcoding.XmlTransformer' classpath = sourceSets.main.runtimeClasspath }
мы можем видеть, что нет плагина Groovy — это было сделано намеренно, так как мы не хотим, чтобы наши скрипты компилировались. Теперь давайте взглянем на логику TransformerFactory, которая компилирует скрипт Groovy. Что действительно важно, так это тот факт, что наш класс Groovy реализует интерфейс, созданный в нашем проекте Java — мы хотим, чтобы с точки зрения Java не было проблем с выполнением кода Groovy.
package com.blogspot.toomuchcoding.factory; import com.blogspot.toomuchcoding.transformer.Transformer; import com.google.common.io.Resources; import groovy.util.GroovyScriptEngine; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.net.URL; /** * Created with IntelliJ IDEA. * User: mgrzejszczak * Date: 22.03.13 * Time: 23:54 */ public class TransformerFactoryImpl implements TransformerFactory<String, String> { private static final String GROOVY_SCRIPTS_CLASSPATH = "groovy/"; private static Logger LOGGER = LoggerFactory.getLogger(TransformerFactoryImpl.class); private ScriptFactory scriptFactory; private GroovyScriptEngine groovyScriptEngine; public TransformerFactoryImpl(ScriptFactory scriptFactory) { this.scriptFactory = scriptFactory; try { groovyScriptEngine = new GroovyScriptEngine(GROOVY_SCRIPTS_CLASSPATH); } catch (IOException e) { LOGGER.error("Exception occurred while trying to create the Groovy script engine", e); throw new RuntimeException(e); } } @Override public Transformer<String, String> createTransformer() { Transformer<String, String> transformerFromScript = null; try { File scriptFile = scriptFactory.createScript(); URL scriptAsAClasspathResource = Resources.getResource(GROOVY_SCRIPTS_CLASSPATH + scriptFile.getName()); Class classFromScript = groovyScriptEngine.loadScriptByName(scriptAsAClasspathResource.getFile()); transformerFromScript = (Transformer<String, String>) classFromScript.newInstance(); } catch (Exception e) { LOGGER.error("Exception occurred while trying to execute Groovy script", e); } return transformerFromScript; } }
GroovyScriptEngine (надеюсь, я использовал его хорошо;)) потому что:
GroovyScriptEngine — наиболее полное решение для людей, которые хотят встраивать скрипты Groovy в свои серверы и перезагружать их при модификации. Вы инициализируете GroovyScriptEngine с набором CLASSPATH, таких как корни, которые могут быть URL-адресами или именами каталогов. Затем вы можете выполнить любой скрипт Groovy внутри этих корней. GSE также будет отслеживать зависимости между сценариями, чтобы при изменении любого зависимого сценария все дерево было перекомпилировано и перезагружено.
Я хотел иметь какой-то способ кэширования скомпилированных классов, чтобы не было проблем с PermGen.
В любом случае вы можете видеть, что я делаю некоторые преобразования, чтобы получить URL ресурса сценария Groovy для classpath. В конце мы извлекаем класс из скрипта Groovy и приводим его к Transformer.
package groovy import com.blogspot.toomuchcoding.transformer.Transformer import groovy.util.slurpersupport.NodeChildren import groovy.xml.MarkupBuilder /** * Created with IntelliJ IDEA. * User: mgrzejszczak * Date: 23.03.13 * Time: 02:16 */ abstract class AbstractGroovyXmlTransformer implements Transformer<String, String> { static Map<String, Object> MISSING_PROPERTIES = ["convertDate": new DateConverter(), "map": new Mapper()] @Override String transform(String input) { def inputXml = new XmlSlurper().parseText input def writer = new StringWriter() def outputXml = new MarkupBuilder(writer) doTransform inputXml, outputXml writer.toString() } abstract void doTransform(inputXml, outputXml) def propertyMissing(String name) { Object property = MISSING_PROPERTIES[name] assert property != null, "There is no function like [$name]. The ones that are supported are ${MISSING_PROPERTIES.keySet()}" property } protected static class Mapper { private Map<String, String> inputParameters Mapper given(Map inputParameters) { this.inputParameters = inputParameters this } String from(NodeChildren nodeChildren) { assert inputParameters != null, "The mapping can't be null!" assert nodeChildren != null, "Node can't be null!" String nodeText = nodeChildren.text() String mappedValue = inputParameters[nodeText] mappedValue ?: inputParameters.default } static Mapper map(Map<String, String> inputParameters) { return new Mapper(inputParameters) } } protected static class DateConverter { private String inputDate private String inputDateFormat DateConverter from(NodeChildren nodeChildren) { this.inputDate = nodeChildren.text() this } DateConverter havingDateFormat(String inputDateFormat) { this.inputDateFormat = inputDateFormat this } String toOutputDateFormat(String outputDateFormat) { assert inputDate != null, "The input date for which you are trying to do the conversion can't be null" assert inputDateFormat != null, "The input date format for which you are trying to do the conversion can't be null" assert outputDateFormat != null, "The output date format for which you are trying to do the conversion can't be null" Date.parse(inputDateFormat, inputDate).format(outputDateFormat) } static DateConverter convertDate() { new DateConverter() } } }
В этом абстрактном классе Groovy я решил разместить всю логику, которая могла бы размыть изображение для BA. В дополнение к этому я создал несколько вспомогательных классов и методов. Чтобы полностью использовать возможности DSL в Groovy, я использовал метод propertyMissing для сопоставления слов
«map» и
«convertDate» для создания экземпляров вспомогательных классов, которые используются в
шаблоне проектирования Builder :
convertDate.from(inputXml.InputSystemContext.InputDate).havingDateFormat("dd/MM/yyyy").toOutputDateFormat("yy/MM/dd") or map.given("Some_action" : "Some_output_action", "default" : "something else").from(inputXml.AdditionalData.TypeOfAction)
Если такой «функции» не существует (например, BA делает опечатку или sth), тогда выдается ошибка подтверждения и список поддерживаемых «функций» (которые в действительности являются свойствами — но они являются функциями с точки зрения BA) печатается.
Теперь давайте перейдем к сценарию, который будет использоваться БА.
package groovy /** * Created with IntelliJ IDEA. * User: mgrzejszczak * Date: 22.03.13 * Time: 23:59 * * additional functions: * * convertDate.from(Node).havingDateFormat("DateFormat").toOutputDateFormat("AnotherDateFormat") * map.given("Value to be mapped from" : "Value to be mapped to", "default" : "default value").from(Node) * */ class GroovyXmlTransformer extends AbstractGroovyXmlTransformer { @Override void doTransform(inputXml, outputXml) { outputXml.OutputSystemEnvelope() { OutputSystemContext { ResponseID(inputXml.InputSystemContext.RequestID.text().reverse()) OutputSource('OUTPUT_SYSTEM') OutputDate(convertDate.from(inputXml.InputSystemContext.InputDate).havingDateFormat("dd/MM/yyyy").toOutputDateFormat("yy/MM/dd")) } OutputAdditionalData { OutputReferenceNo("SOME_PREFIX_${inputXml.AdditionalData.ReferenceNo.text()}_SOME_SUFIX") OutputTypeOfAction(map.given("Some_action" : "Some_output_action", "default" : "something else").from(inputXml.AdditionalData.TypeOfAction)) OutputTransactions { inputXml.AdditionalData.Transactions.Transaction.each { OutputTransaction(Client: it.Client, ProductType: it.ProductType, 'Done') } } OutputProducts { def minProduct = inputXml.AdditionalData.Products.Product.list().min { it.Value.text() } def maxProduct = inputXml.AdditionalData.Products.Product.list().max { it.Value.text() } MinProduct(name: minProduct.Name.text(), minProduct.Value.text()) MaxProduct(name: maxProduct.Name.text(), maxProduct.Value.text()) } } } } }
Этот фрагмент кода выполняет следующее сопоставление (вы можете проверить
/xml/SampleXml.xml ):
Сопоставлено с | Сопоставлен с |
---|---|
InputSystemEnvelope | OutputSystemEnvelope |
InputSystemContex | OutputSystemContex |
RequestID | ResponseId (идентификатор должен быть отменен) |
InputSource | OutputSoutce (константа «OUTPUT_SYSTEM») |
InputDate | OutputDate (конвертируется из дд / мм / гггг в гг / мм / дд) |
InputAdditionalData | OutputAdditionalData |
InputReferenceNo | OutputReferenceNo («SOME_PREFIX_» + значение из InputReferenceNo + «_SOME_SUFIX») |
InputTypeOfAction | OutputTypeOfAction (отображается таким образом, что если InputTypeOfAction равен «Some_action», то у нас будет «Some_output_action». В противном случае мы получим «что-то еще») |
операции | OutputTransactions |
Сделка | OutputTransaction (атрибут клиента из Transaction.Client, атрибут ProductType из Transaction.ProductType и значение «Готово») |
Продукты | OutputProducts |
Продукт, имеющий минимальное значение | MinProduct |
Продукт, имеющий максимальное значение | MaxProduct |
Выход
Converted from [<InputSystemEnvelope> <InputSystemContext> <RequestID>1234567890</RequestID> <InputSource>INPUT_SYSTEM</InputSource> <InputDate>22/03/2013</InputDate> </InputSystemContext> <AdditionalData> <ReferenceNo>Ref1234567</ReferenceNo> <TypeOfAction>Some_action</TypeOfAction> <Transactions> <Transaction> <Client>ACME</Client> <ProductType>IRS</ProductType> </Transaction> <Transaction> <Client>Oracle</Client> <ProductType>DB</ProductType> </Transaction> </Transactions> <Products> <Product> <Name>Book</Name> <Value>1</Value> </Product> <Product> <Name>Car</Name> <Value>10000</Value> </Product> <Product> <Name>Boat</Name> <Value>100000000</Value> </Product> <Product> <Name>Spaceship</Name> <Value>1000000000000000000</Value> </Product> </Products> </AdditionalData> </InputSystemEnvelope>] to [<OutputSystemEnvelope> <OutputSystemContext> <ResponseID>0987654321</ResponseID> <OutputSource>OUTPUT_SYSTEM</OutputSource> <OutputDate>13/03/22</OutputDate> </OutputSystemContext> <OutputAdditionalData> <OutputReferenceNo>SOME_PREFIX_Ref1234567_SOME_SUFIX</OutputReferenceNo> <OutputTypeOfAction>Some_output_action</OutputTypeOfAction> <OutputTransactions> <OutputTransaction Client='ACME' ProductType='IRS'>Done</OutputTransaction> <OutputTransaction Client='Oracle' ProductType='DB'>Done</OutputTransaction> </OutputTransactions> <OutputProducts> <MinProduct name='Book'>1</MinProduct> <MaxProduct name='Spaceship'>1000000000000000000</MaxProduct> </OutputProducts> </OutputAdditionalData> </OutputSystemEnvelope>]
Плюсы и минусы
Плюсы и минусы этого подхода следующие:
Плюсы:
- Отображение выполняется последовательно — поле за полем (проще отладить проблему)
- Отображение состоит из словарного запаса, понятного БА
- Большинство отображений может быть сделано БА
- Большая часть не отображающей грамматики скрыта в абстракции
- Компиляция скрипта Groovy происходит быстрее, чем создание баз знаний и компиляция скриптов Drools
- Независимость от схемы XML (каждое изменение схемы потребует перекомпиляции классов JAXB)
Минусы:
- Бакалавр должен иметь некоторые знания в области компьютерных наук
- Нет параллельного сопоставления
- Отображение может стать менее читабельным из-за того, что весьма вероятно, что BA (из-за нехватки времени) не создаст единственную функцию — вся логика окажется в замыканиях для данного узла.
- Могут быть проблемы с памятью при разборе и перекомпиляции скриптов Groovy.
- Отсутствие схемы XML может привести к неправильной настройке пути вывода / ввода XML
Резюме
Проблема с отображением, с которой мы столкнулись в нашем проекте, оказалась очень интересной проблемой. Пример, показанный в этом посте, является лишь предложением решения проблемы и, надеюсь, может стать отправной точкой для дальнейшего обсуждения этой темы. Если у вас есть идеи или мнения по этой теме, пожалуйста, оставьте комментарий под
этой статьей .