使用centraldogma 作为easy-rules spring boot starter的规则存储
centraldogma 前边有介绍过,同时也说明过部署以及使用,以下是基于centraldogma 扩展easy-rules 的spring boot starter
方便快捷的支持rule 的修改以及实时更新,代码已经push github了,可以参考使用
开发流程
借鉴了以前easy-rules spring boot starter的开发,只是调整添加了对于centraldogma的支持
- 核心代码结构
├── README.md
├── pom.xml
└── src
├── main
│ ├── java
│ │ └── com
│ │ └── github
│ │ └── rongfengliang
│ │ ├── DefaultRuleEngineListener.java
│ │ ├── DefaultRulesListener.java
│ │ ├── EasyRulesAutoConfiguration.java
│ │ ├── EasyRulesEngineConfiguration.java
│ │ ├── EasyRulesEntities.java
│ │ ├── RuleExpressionType.java
│ │ ├── RulesConfig.java
│ │ ├── RulesContentType.java
│ │ ├── SimpleBeanResovler.java
│ │ ├── SpringBeanUtil.java
│ │ └── rulelocator
│ │ ├── RulesLocator.java
│ │ └── impl
│ │ └── FileRulesLocator.java
│ └── resources
│ └── META-INF
│ └── spring.factories
└── test
└── java
└── com
└── github
└── rongfengliang
└── CentraldogmaStringReader.java
- 代码说明
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.github.rongfengliang</groupId>
<artifactId>easy-rules-centraldogma-spring-boot-starter</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<encoding>UTF-8</encoding>
<java.version>1.8</java.version>
<spring-boot.version>2.2.3.RELEASE</spring-boot.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<easy-rules>4.1.0</easy-rules>
<centraldogma-client>0.51.1</centraldogma-client>
<slf4j-simple>1.7.32</slf4j-simple>
<lombok.version>1.18.20</lombok.version>
</properties>
<scm>
<developerConnection>scm:git:https://github.com/rongfengliang/easy-rules-centraldogma-spring-boot-starer.git</developerConnection>
<tag>HEAD</tag>
</scm>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>${spring-boot.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jeasy</groupId>
<artifactId>easy-rules-core</artifactId>
<version>${easy-rules}</version>
</dependency>
<dependency>
<groupId>org.jeasy</groupId>
<artifactId>easy-rules-mvel</artifactId>
<version>${easy-rules}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.linecorp.centraldogma</groupId>
<artifactId>centraldogma-client-spring-boot-starter</artifactId>
<version>${centraldogma-client}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.jeasy</groupId>
<artifactId>easy-rules-spel</artifactId>
<version>${easy-rules}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring-boot.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>${slf4j-simple}</version>
<optional>true</optional>
<scope>provided</scope>
</dependency>
</dependencies>
<distributionManagement>
<repository>
<id>sonatype-releases</id>
<name>sonatype Release Repository</name>
<url>https://oss.sonatype.org/service/local/staging/deploy/maven2</url>
</repository>
<snapshotRepository>
<id>sonatype-snapshots</id>
<name>sonatype Snapshot Repository</name>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
</snapshotRepository>
</distributionManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-release-plugin</artifactId>
<version>3.0.0-M4</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<attach>true</attach>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
配置入口
package com.github.rongfengliang;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.linecorp.centraldogma.client.CentralDogma;
import com.linecorp.centraldogma.client.Watcher;
import com.linecorp.centraldogma.common.Entry;
import com.linecorp.centraldogma.common.Query;
import com.linecorp.centraldogma.common.Revision;
import org.jeasy.rules.api.*;
import org.jeasy.rules.core.DefaultRulesEngine;
import org.jeasy.rules.spel.SpELRuleFactory;
import org.jeasy.rules.support.reader.JsonRuleDefinitionReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.expression.BeanResolver;
import java.io.StringReader;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
@Configuration
@EnableConfigurationProperties(EasyRulesEngineConfiguration.class)
public class EasyRulesAutoConfiguration {
private Logger log = LoggerFactory.getLogger(DefaultRulesListener.class);
private final EasyRulesEngineConfiguration properties; // 配置
private Map<String, Rules> easyRules; // 基于map 存储bean 中间状态数据
private EasyRulesEntities easyRulesEntities; // 基于map 存储bean 中间状态数据
EasyRulesAutoConfiguration(EasyRulesEngineConfiguration properties) {
this.properties = properties;
this.easyRules = null;
this.easyRulesEntities =new EasyRulesEntities();
}
@Bean
@ConditionalOnMissingBean
public RuleListener defaultRulesListener() {
return new DefaultRulesListener();
}
@Bean
@ConditionalOnMissingBean
public RulesEngineListener defaultRuleEngineListener() {
return new DefaultRuleEngineListener();
}
@Bean
@ConditionalOnMissingBean
public BeanResolver defaultedResolver(SpringBeanUtil springBeanUtil) {
return new SimpleBeanResovler(SpringBeanUtil.getApplicationContext());
}
@Bean
@ConditionalOnMissingBean
public SpringBeanUtil springBeanUtil() {
return new SpringBeanUtil();
}
@Bean
@ConditionalOnClass(value = {CentralDogma.class, ObjectMapper.class}) // 需要依赖jackson以及CentralDogma bean,此bean 主要实现加载之后的配置加载以及变动数据的实时更新,同时转换为对应的easy-rules
public CommandLineRunner commandLineRunner(CentralDogma dogma, ObjectMapper objectMapper, BeanResolver beanResolver) {
log.info("project:{}, repo:{},filename:{}", properties.getProject(), properties.getRepo(), properties.getConfName());
Watcher watcher = dogma.fileWatcher(properties.getProject(), properties.getRepo(), Query.ofText(properties.getConfName()));
watcher.watch((revision, value) -> {
log.info("Updated to {} at {}", value, revision);
try {
List<RulesConfig> rulesConfigs = objectMapper.readValue(value.toString(), new TypeReference<List<RulesConfig>>() {
});
easyRulesEntities.setRulesConfigs(rulesConfigs);
this.easyRules = rulesContent2EasyRules(beanResolver);
} catch (JsonProcessingException e) {
e.printStackTrace();
log.error("json convert error", e);
}
});
return args -> {
log.info("init CentralDogma load easy rules conf");
};
}
/**
* centralDogmaRules with prototype for fetch new conf rules
* bean name is <b>centralDogmaRules</b>
*
* @param beanResolver
* @param centralDogma
* @return
* @throws Exception
*/
@Bean(name = "centralDogmaRules")
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@ConditionalOnClass(value = {CentralDogma.class, ObjectMapper.class}) // 为了安全以及实时变动支持,基于了原型模式
public Map<String, Rules> centralDogmaRules(BeanResolver beanResolver, CentralDogma centralDogma, ObjectMapper objectMapper) throws Exception {
Map<String, Rules> rules = new HashMap<>();
if (Objects.isNull(easyRules)) {
CompletableFuture<Entry<String>> future =
centralDogma.getFile(properties.getProject(), properties.getRepo(), Revision.HEAD, Query.ofText(properties.getConfName()));
log.info("load rule content", future.join().content());
List<RulesConfig> rulesConfigs = objectMapper.readValue(future.join().content(), new TypeReference<List<RulesConfig>>() {
});
easyRulesEntities.setRulesConfigs(rulesConfigs);
return getStringRulesMap(beanResolver, rules);
} else {
return this.easyRules;
}
}
/**
* common method for Map<String, Rules> convert
* @param beanResolver
* @param rules
* @return
*/
private Map<String, Rules> getStringRulesMap(BeanResolver beanResolver, Map<String, Rules> rules) {
easyRulesEntities.getRulesConfigs().forEach(new Consumer<RulesConfig>() {
@Override
public void accept(RulesConfig rulesConfig) {
log.info("load rule conf to easy rules engine:{}", rulesConfig.getRulesContent());
StringReader stringReader = new StringReader(rulesConfig.getRulesContent().toPrettyString());
SpELRuleFactory jsonRuleFactory = new SpELRuleFactory(new JsonRuleDefinitionReader(), beanResolver);
Rules jsonRules = null;
try {
jsonRules = jsonRuleFactory.createRules(stringReader);
} catch (Exception e) {
e.printStackTrace();
}
rules.put(rulesConfig.getRulesId(), jsonRules);
}
});
return rules;
}
/**
* convert rulesContent2EasyRules
*
* @param beanResolver
* @return
*/
private Map<String, Rules> rulesContent2EasyRules(BeanResolver beanResolver) {
Map<String, Rules> rules = new HashMap<>();
return getStringRulesMap(beanResolver, rules);
}
/**
* 获取配置额规则列表
*
* @param beanResolver spring beanResolver
* @return Map<String, Rules>
* @throws Exception
*/
/**
* 为了安全使用原型模式
*
* @param defaultRulesListener
* @param defaultRuleEngineListener
* @return RulesEngine
*/
@Bean
@ConditionalOnMissingBean(RulesEngine.class)
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public RulesEngine rulesEngine(RuleListener defaultRulesListener, RulesEngineListener defaultRuleEngineListener) {
log.info("create rule Engine");
RulesEngineParameters parameters = new RulesEngineParameters();
if (this.properties.getPriorityThreshold() > 0) {
parameters.setPriorityThreshold(this.properties.getPriorityThreshold());
}
if (this.properties.isSkipOnFirstAppliedRule()) {
parameters.setSkipOnFirstAppliedRule(this.properties.isSkipOnFirstAppliedRule());
}
if (this.properties.isSkipOnFirstFailedRule()) {
parameters.setSkipOnFirstFailedRule(this.properties.isSkipOnFirstFailedRule());
}
if (this.properties.isSkipOnFirstNonTriggeredRule()) {
parameters.setSkipOnFirstNonTriggeredRule(this.properties.isSkipOnFirstNonTriggeredRule());
}
DefaultRulesEngine rulesEngine = new DefaultRulesEngine(parameters);
rulesEngine.registerRuleListener(defaultRulesListener);
rulesEngine.registerRulesEngineListener(defaultRuleEngineListener);
return rulesEngine;
}
}
EasyRulesEngineConfiguration 配置主要包含了一些通用参数的支持
package com.github.rongfengliang;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
@Data
@ConfigurationProperties(prefix = "easyrules")
public class EasyRulesEngineConfiguration {
private boolean skipOnFirstAppliedRule;
private boolean skipOnFirstNonTriggeredRule;
private boolean skipOnFirstFailedRule;
private int priorityThreshold;
private String project;
private String repo;
private String contentType;
private String confName;
}
规则存储实体定义RulesConfig(因为数据结构的多变,使用了jsonnode,实现灵活扩展)
package com.github.rongfengliang;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Data;
/**
* @author dalong
* rules文件配置信息
*/
@Data
public class RulesConfig {
private String rulesId;
private JsonNode rulesContent;
}
自定义beanresovler SimpleBeanResovler 主要是spel 需要
package com.github.rongfengliang;
import org.springframework.context.ApplicationContext;
import org.springframework.expression.BeanResolver;
import org.springframework.expression.EvaluationContext;
/**
* @author dalong
* SimpleBeanResovler
*/
public class SimpleBeanResovler implements BeanResolver {
private ApplicationContext applicationContext;
public SimpleBeanResovler(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
@Override
public Object resolve(EvaluationContext context, String beanName) {
return applicationContext.getBean(beanName);
}
}
SpringBeanUtil bean 工具类,主要简化原型的处理
package com.github.rongfengliang;
import org.jeasy.rules.api.Rules;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import java.util.Map;
public class SpringBeanUtil implements ApplicationContextAware {
/**
* 上下文对象实例
*/
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
public static Map<String, Rules> centralDogmaRules(){
return SpringBeanUtil.getBean("centralDogmaRules",Map.class);
}
/**
* 获取applicationContext
*
* @return
*/
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
/**
* 通过name获取 Bean.
*
* @param name
* @return
*/
public static Object getBean(String name) {
return getApplicationContext().getBean(name);
}
/**
* 通过class获取Bean.
*
* @param clazz
* @param <T>
* @return
*/
public static <T> T getBean(Class<T> clazz) {
return getApplicationContext().getBean(clazz);
}
/**
* 通过name,以及Clazz返回指定的Bean
*
* @param name
* @param clazz
* @param <T>
* @return
*/
public static <T> T getBean(String name, Class<T> clazz) {
return getApplicationContext().getBean(name, clazz);
}
}
说明
一以上是具体的代码部分,关于使用,后边会有介绍
参考资料
https://github.com/rongfengliang/easy-rules-centraldogma-spring-boot-starter
https://github.com/line/centraldogma