предисловие
Поскольку для абстрагирования бизнеса в проекте использовался Groovy, эффект был хороший, да и в процессе были некоторые подводные камни, поэтому я вкратце записал и поделился, как я это реализовал шаг за шагом.Здесь вы можете узнать о:
1. Зачем выбирать groovy в качестве обработчика правил
2. Понять основные принципы Groovy и способы интеграции Java
3. Анализ некоторых проблем и ям интеграции Groovy и Java
4. Какие оптимизации производительности были сделаны при использовании в проекте
5. Несколько советов, которые следует учитывать при реальном использовании
Проблемы, которые могут решить сценарии правил
В эпоху Интернета, с быстрым развитием бизнеса, скорость итерации и доступа к продукту становится все быстрее и быстрее, и требуются некоторые гибкие конфигурации. Методы обычно имеют следующие аспекты:
1. Наиболее традиционный способ заключается в том, что программа Java напрямую пишется непосредственно для предоставления нескольких настраиваемых конфигураций параметров, а затем инкапсулирует независимые компоненты бизнес-модуля.После добавления параметров или простых правил настройки выполняется повторная настройка.
2. Используйте решения с открытым исходным кодом, такие как механизм правил drools, который подходит для систем с более сложным бизнесом.
3. Используйте динамические скриптовые движки: groovy, simpleEl, QLExpress.
Внедрение сценариев правил для абстрагирования бизнеса может значительно повысить эффективность. Например, в системе проверки кредита, разработанной автором ранее, заказ на кредит будет отменен через несколько процессов после получения заказа: после получения заказа процесс заказа должен быть определен в соответствии с результатом, полученным от системы управления рисками. , а порядок разных товаров будет обратный.Правила несовместимы.Каждый раз при подключении нового товара фармеру кода приходится писать кучу логики процесса для этого товара,правила существующих продуктов часто нужно заменять. Поэтому я хочу использовать динамический анализ и выполнение скриптового движка, чтобы абстрагироваться от реверсирования процесса с помощью скрипта правил для повышения эффективности.
Как выбрать колеса
Учитывая сложность моего собственного бизнеса, традиционные решения с открытым исходным кодом, такие как Acitivities и drools, слишком тяжелы для моего бизнеса. На самом деле наиболее распространенным механизмом сценариев является groovy. У Ali есть несколько проектов с открытым исходным кодом. Для различных сценариев правил при выборе моделей необходимо учитывать производительность, стабильность и гибкость синтаксиса. После всестороннего рассмотрения Groovy был выбран по следующим причинам:
1. У него долгая история, широкий спектр применения и мало ям.
2, и совместимость с java: бесшовный код java, не важно даже понимать синтаксис groovy
3. Синтаксический сахар
4. Цикл проекта короткий, а сроки запуска срочные😢
Абстракция потока проекта
Потому что разные предприятия непоследовательно обрабатывают логику, когда процесс идет в обратном порядке. Сначала рассмотрим простой случай: Собственный проект изменит процесс различных кредитных заказов в бизнесе.Например, заказ может быть обращен от процесса A к процессу B или процессу C, в зависимости от выполнения каждой Стратегической единицы (как показано на рисунке ниже): выполняется каждый блок стратегии. Будет возвращено логическое значение. Конкретную логику можно определить самостоятельно, здесь мы предполагаем: если все условия Стратегии А выполнены (то есть каждая исполнительная единица возвращает true), то порядок будет обратным на Сценарий Б; если все условия Стратегии Единица B удовлетворена, тогда порядок изменится на Сценарий C.
Почему он разработан в виде нескольких StrategyLogicUnits? Это связано с тем, что в моем проекте для облегчения настройки конфигурация StrategyLogicUnit всего процесса отображается в пользовательском интерфейсе, который более удобочитаем и требует только изменения логики выполнения в определенном блоке при изменении.
Данные, от которых зависит каждый StrategyLogicUnit во время выполнения, могут быть абстрагированы в контекст, который содержит две части данных: одна часть — это бизнес-данные: например, продукты заказа, данные контроля рисков, зависящие от заказа, и т. д., а другая часть — правила. данные выполнения: включая исполняемый в данный момент узел, информацию о группе политик, к которой он принадлежит, текущий процесс, следующий процесс и т. д. Контекст данных выполнения этой части механизма правил может быть разработан в соответствии с различными службами. дизайн в основном учитывает повторный запуск точки останова, группу политик и т. д.: например, вы можете спроектировать связь между различными группами стратегий и продуктами. Эта часть бизнеса имеет относительно большую связь. Эта статья в основном посвящена groovy.
Вы можете понять контекст в качестве ввода и вывода StrategyLogicunit, а STRATEGYLOGICUNIT выполняет в Groovy, мы можем выполнять настраиваемый дисплей и конфигурацию для каждого выполненного StrategyLogicunit. Во время выполнения логика может быть логически определена в соответствии с различной информацией, содержащейся в контексте, или значение в контексте можно также быть изменено.
Интеграция Groovy с Java на основе процессов
Итак, основываясь на описанном выше процессе, как нам объединить Groovy и Java? В соответствии с описанной выше схемой выполнение сценария Groovy по существу принимает только объект контекста, делает логические выводы на основе ключевой информации в объекте контекста и выводит результаты. Результаты также сохраняются в контексте. Давайте посмотрим, как Groovy интегрируется с java:
GroovyClassLoader
Используйте GroovyClassLoader от Groovy, который динамически загружает сценарий и выполняет его. GroovyClassLoader — это настраиваемый загрузчик классов Groovy, который отвечает за синтаксический анализ и загрузку классов Groovy, используемых в классах Java.
GroovyShell
GroovyShell позволяет вычислять произвольные выражения Groovy в классах Java (даже в классах Groovy). Вы можете использовать объект Binding для ввода параметров в выражения и, наконец, вернуть результат вычисления выражения Groovy через GroovyShell.
GroovyScriptEngine
GroovyShell в основном используется для вывода противоположных сценариев или выражений.Если его заменить несколькими сценариями, связанными друг с другом, лучше использовать GroovyScriptEngine. GroovyScriptEngine загружает сценарии Groovy из указанного вами места (файловая система, URL-адрес, база данных и т. д.) и перезагружает их по мере изменения сценариев. Как и GroovyShell, GroovyScriptEngine также позволяет передавать значения параметров и возвращает значение сценария.
Возьмите GroovyClassLoader в качестве примера.
Все три метода могут быть реализованы.Теперь мы возьмем GroovyClassLoader в качестве примера, чтобы показать, как интегрироваться с java:
Например: мы предполагаем, что заказ с суммой заявки больше 20 000 поступает в процесс B. Представлено в maven в проекте SpringBoot
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>2.4.10</version>
</dependency>
Определите интерфейс Java, который выполняет Groovy:
public interface EngineGroovyModuleRule {
boolean run(Object context);
}
Абстрагируйте файл шаблона Groovy и поместите его в ресурс для загрузки:
import com.groovyexample.groovy.*
class %s implements EngineGroovyModuleRule {
boolean run(Object context){
%s //业务执行逻辑:可配置化
}
}
Следующим шагом является синтаксический анализ файлов шаблонов Groovy, которые можно кэшировать.Синтаксический анализ выполняется с помощью Spring PathMatchingResourcePatternResolver; следующая строка, StrategyLogicUnit, представляет собой логику определенных бизнес-правил, и логика этой части настраивается. Например: мы предполагаем, что логика, которая должна быть выполнена, такова: когда сумма примененного ордера больше 20 000, перейти к процессу A. Простой пример кода выглядит следующим образом:
//解析Groovy模板文件
ConcurrentHashMap<String,String> concurrentHashMap = new ConcurrentHashMap(128);
final String path = "classpath*:*.groovy_template";
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
Arrays.stream(resolver.getResources(path))
.parallel()
.forEach(resource -> {
try {
String fileName = resource.getFilename();
InputStream input = resource.getInputStream();
InputStreamReader reader = new InputStreamReader(input);
BufferedReader br = new BufferedReader(reader);
StringBuilder template = new StringBuilder();
for (String line; (line = br.readLine()) != null; ) {
template.append(line).append("\n");
}
concurrentHashMap.put(fileName, template.toString());
} catch (Exception e) {
log.error("resolve file failed", e);
}
});
String scriptBuilder = concurrentHashMap.get("ScriptTemplate.groovy_template");
String scriptClassName = "testGroovy";
//这一部分String的获取逻辑进行可配置化
String StrategyLogicUnit = "if(context.amount>=20000){\n" +
" context.nextScenario='A'\n" +
" return true\n" +
" }\n" +
" ";
String fullScript = String.format(scriptBuilder, scriptClassName, StrategyLogicUnit);
GroovyClassLoader classLoader = new GroovyClassLoader();
Class<EngineGroovyModuleRule> aClass = classLoader.parseClass(fullScript);
Context context = new Context();
context.setAmount(30000);
try {
EngineGroovyModuleRule engineGroovyModuleRule = aClass.newInstance();
log.info("Groovy Script returns:{} "+engineGroovyModuleRule.run(context));
log.info("Next Scenario is {}"+context.getNextScenario());
}
catch (Exception e){
log.error("error...")
}
Выполните приведенный выше код:
Groovy Script returns: true
Next Scenario is A
Ключевой частью является возможность настройки StrategyLogicUnit. Мы отображаем StrategyLogicUnit, соответствующий различным продуктам, в пользовательском интерфейсе управления и можем выполнять CRUD. Чтобы упростить настройку, вводятся группы стратегий, ассоциации репликации стратегии продукта и шаблоны репликации одним щелчком мыши. Функции.
Подводные камни и оптимизация производительности при интеграции
В ходе тестирования проекта было обнаружено, что с увеличением количества квитанций выполнялась частая полная сборка мусора.После воспроизведения тестовой среды в логе появилось:
[Full GC (Metadata GC Threshold) [PSYoungGen: 64K->0K(43008K)] [ParOldGen: 3479K->3482K(87552K)] 3543K->3482K(130560K), [Metaspace: 15031K->15031K(1062912K)], 0.0093409 secs] [Times: user=0.03 sys=0.00, real=0.01 secs]
Из лога видно, что места в матаспейсе недостаточно и его невозможно восстановить с помощью полной сборки мусора. Конкретную ситуацию можно посмотреть через JVisualVM:
Было обнаружено, что было слишком много классов, 2326, из-за чего метапространство было заполнено. Давайте сначала рассмотрим метапространство ##metaspace и permgen Это то, что jdk имеет только в 1.8, а 1.8 удаляет permgen, а область методов перемещается в Metaspace в не-куче.В этой области в основном хранятся: информация о классе хранения, постоянный пул, данные метода, код метода и т. д. В анализе есть две основные проблемы:Вопрос 1: Количество классов: Возможно, внедрение groovy приводит к загрузке слишком большого количества классов, но на самом деле в проекте настроено всего 10 StrategyLogicUnits, и одному и тому же классу должны соответствовать разные порядки при выполнении одного и того же СтратегияLogicUnit. Количество классов слишком необычно.
Вопрос 2: Даже если классов слишком много, почему нельзя переработать полный сборщик мусора?
Ниже мы берем задачу на обучение.
Загрузка GroovyClassLoader
Давайте сначала проанализируем процесс выполнения Groovy.Наиболее важным кодом являются следующие части:
GroovyClassLoader classLoader = new GroovyClassLoader();
Class<EngineGroovyModuleRule> aClass = classLoader.parseClass(fullScript);
EngineGroovyModuleRule engineGroovyModuleRule = aClass.newInstance();
engineGroovyModuleRule.run(context)
GroovyClassLoader — это настраиваемый загрузчик классов, который динамически загружает groovy-скрипты как объекты Java во время выполнения кода. Все знают родительское делегирование classloader.Давайте сначала проанализируем этот GroovyClassloader и посмотрим, каковы его предки:
def cl = this.class.classLoader
while (cl) {
println cl
cl = cl.parent
}
вывод:
groovy.lang.GroovyClassLoader$InnerLoader@13322f3
groovy.lang.GroovyClassLoader@127c1db
org.codehaus.groovy.tools.RootLoader@176db54
sun.misc.Launcher$AppClassLoader@199d342
sun.misc.Launcher$ExtClassLoader@6327fd
Это приводит к:
Bootstrap ClassLoader
↑
sun.misc.Launcher.ExtClassLoader // 即Extension ClassLoader
↑
sun.misc.Launcher.AppClassLoader // 即System ClassLoader
↑
org.codehaus.groovy.tools.RootLoader // 以下为User Custom ClassLoader
↑
groovy.lang.GroovyClassLoader
↑
groovy.lang.GroovyClassLoader.InnerLoader
Проверьте ключевой метод GroovyClassLoader.parseClass и найдите следующий код:
public Class parseClass(String text) throws CompilationFailedException {
return parseClass(text, "script" + System.currentTimeMillis() +
Math.abs(text.hashCode()) + ".groovy");
}
protected ClassCollector createCollector(CompilationUnit unit, SourceUnit su) {
InnerLoader loader = AccessController.doPrivileged(new PrivilegedAction<InnerLoader>() {
public InnerLoader run() {
return new InnerLoader(GroovyClassLoader.this);
}
});
return new ClassCollector(loader, unit, su);
}
Эти два кода означают: Каждый раз, когда groovy выполняет скрипт, он генерирует объект класса скрипта, имя этого объекта класса — «script» + System.currentTimeMillis() + Math.abs(text.hashCode(), для вопроса 1: каждый раз, когда ордер выполняет один и тот же StrategyLogicUnit, сгенерированный класс отличается, и новый класс генерируется каждый раз, когда выполняется сценарий правила.
Затем посмотрите на часть вопроса 2InnerLoader: Каждый раз, когда groovy выполняет скрипт, он создает новый InnerLoader для загрузки этого объекта, а для проблемы 2 мы можем предположить, что ни InnerLoader, ни объект скрипта не могут быть перезапущены во время fullGC, поэтому после запуска в течение определенного периода времени PERM будет заполнен, а fullGC всегда будет запускаться.
Зачем вам нужен внутренний загрузчик?
В сочетании с родительской моделью делегирования, поскольку ClassLoader может загрузить класс с одним и тем же именем только один раз, если оба загружены GroovyClassLoader, то, когда сценарий определяет класс C, другой сценарий определяет класс C, GroovyClassLoader не может загрузиться.
Потому что класс может быть GC только после того, как его ClassLoader станет GC.
Если все классы загружаются GroovyClassLoader, то только когда GroovyClassLoader GCed, все эти классы могут быть GCed, а если используется InnerLoader, так как после компиляции исходного кода на него нет внешних ссылок, кроме классов, которые он загружает, Поэтому, пока на загружаемые классы нет ссылок, он и загружаемые им классы могут быть проверены GCed.
Условия повторного использования классов (из «Глубокого понимания виртуальных машин JVM»)
Класс в JVM может быть переработан сборщиком мусора только в том случае, если он соответствует следующим трем условиям, то есть класс выгружен:
1. Все экземпляры этого класса прошли GCed, то есть экземпляра этого класса в JVM нет.
2. ClassLoader, загрузивший класс, прошел сборку мусора.
3. java.lang.Class класса
На объект нигде не ссылаются, т.е. к методам класса нельзя обращаться через рефлексию где угодно. Проанализируйте эти три пункта один за другим:
Первый пункт исключается:
Просмотрите код GroovyClassLoader.parseClass(), сводка: Groovy скомпилирует сценарий в класс с именем Scriptxx, этот класс сценария сгенерирует экземпляр путем отражения и вызовет его функцию MAIN для выполнения, это действие будет выполнено только один раз, в класс или созданные им экземпляры больше нигде в приложении не упоминаются;
Второй пункт исключен:
О InnerLoader: Groovy специально создает новый InnerLoader при компиляции каждого скрипта для решения проблемы GC, поэтому InnerLoader должен быть независимым и не будет упоминаться в приложении;
Остается только третья возможность:
Ссылка на объект Class этого класса, продолжайте просматривать код:
/**
* sets an entry in the class cache.
*
* @param cls the class
* @see #removeClassCacheEntry(String)
* @see #getClassCacheEntry(String)
* @see #clearCache()
*/
protected void setClassCacheEntry(Class cls) {
synchronized (classCache) {
classCache.put(cls.getName(), cls);
}
}
Вы можете воспроизвести проблему и проверить причину: конкретная идея состоит в том, чтобы разобрать сценарий в бесконечном цикле, jmap -clsstat проверить состояние загрузчика классов и объединить дамп экспорта для проверки отношения ссылок. Таким образом, общая причина такова: после каждого скрипта groovy parse класс скрипта будет кэшироваться, и при следующем анализе скрипта он будет сначала прочитан из кэша. Эта кэшированная карта хранится в GroovyClassLoader, ключ — это имя класса скрипта, значение — это класс, а правила именования объекта класса следующие:
"script" + System.currentTimeMillis() + Math.abs(text.hashCode()) + ".groovy"
Следовательно, каждый раз, когда имя компилитированного объекта отличается, в кэш-память будет добавлен объект класса, что приведет к более неконференцированному классу объекта. Поскольку количество раз увеличивается, объект Commonifed Class заполнит зону Пермь.
решение
В большинстве случаев Groovy выполняется после компиляции.На самом деле, в этом сценарии приложения, хотя сценарий передается в качестве параметра, содержимое большинства сценариев фактически одинаково. Решение состоит в том, чтобы кэшировать объект класса, сгенерированный после parseClass, через интерфейс InitializingBean при запуске проекта.Ключом является значение md5 скрипта groovyScript, а кэш можно обновить после изменения конфигурации на стороне конфигурации. В этом есть два преимущества:
1. Решить проблему полного метапространства
2. Поскольку нет необходимости компилировать и загружать во время выполнения, это может ускорить выполнение скрипта.
Суммировать
Groovy подходит для некоторой настраиваемой обработки, когда изменения в бизнесе происходят чаще и быстрее. Он прост в использовании: по сути, это код Java, работающий в jvm. При его использовании нам необходимо понимать механизм загрузки классов. Основы хранения в памяти: понятно, а кэширование решает некоторые потенциальные проблемы и повышает производительность. Подходит для механизмов правил с относительно небольшим количеством правил и правил, которые не обновляются часто.
Шаблон был организован на github:GitHub.com/люблю тебя загадывать желания/…