diff --git a/sentinel-demo/sentinel-demo-dynamic-file-rule/src/main/java/com/alibaba/csp/sentinel/demo/file/rule/JarFileDataSourceDemo.java b/sentinel-demo/sentinel-demo-dynamic-file-rule/src/main/java/com/alibaba/csp/sentinel/demo/file/rule/JarFileDataSourceDemo.java new file mode 100644 index 00000000..a36f21da --- /dev/null +++ b/sentinel-demo/sentinel-demo-dynamic-file-rule/src/main/java/com/alibaba/csp/sentinel/demo/file/rule/JarFileDataSourceDemo.java @@ -0,0 +1,70 @@ +package com.alibaba.csp.sentinel.demo.file.rule; + +import com.alibaba.csp.sentinel.datasource.Converter; +import com.alibaba.csp.sentinel.datasource.FileInJarReadableDataSource; +import com.alibaba.csp.sentinel.datasource.ReadableDataSource; +import com.alibaba.csp.sentinel.property.PropertyListener; +import com.alibaba.csp.sentinel.property.SentinelProperty; +import com.alibaba.csp.sentinel.slots.block.Rule; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; +import com.alibaba.csp.sentinel.slots.system.SystemRuleManager; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.TypeReference; + +import java.util.List; + +/** + *

+ * This Demo shows how to use {@link FileInJarReadableDataSource} to read {@link Rule}s from jarfile. The + * {@link FileInJarReadableDataSource} will automatically fetches the backend file every 3 seconds, and + * inform the listener if the file is updated. + *

+ *

+ * Each {@link ReadableDataSource} has a {@link SentinelProperty} to hold the deserialized config data. + * {@link PropertyListener} will listen to the {@link SentinelProperty} instead of the datasource. + * {@link Converter} is used for telling how to deserialize the data. + *

+ *

+ * {@link FlowRuleManager#register2Property(SentinelProperty)}, + * {@link DegradeRuleManager#register2Property(SentinelProperty)}, + * {@link SystemRuleManager#register2Property(SentinelProperty)} could be called for listening the + * {@link Rule}s change. + *

+ *

+ * For other kinds of data source, such as Nacos, + * Zookeeper, Git, or even CSV file, We could implement {@link ReadableDataSource} interface to read these + * configs. + *

+ * + * @author dingq + * @date 2019-03-30 + */ +public class JarFileDataSourceDemo { + public static void main(String[] args) throws Exception { + JarFileDataSourceDemo demo = new JarFileDataSourceDemo(); + demo.listenRules(); + + /* + * Start to require tokens, rate will be limited by rule in FlowRule.json + */ + FlowQpsRunner runner = new FlowQpsRunner(); + runner.simulateTraffic(); + runner.tick(); + } + + private void listenRules() throws Exception { + String jarPath = System.getProperty("user.dir") + "/sentinel-demo/sentinel-demo-dynamic-file-rule/target/sentinel-demo-dynamic-file-rule-1.5.1-SNAPSHOT.jar"; + // eg: if flowRuleInJarName full path is 'sentinel-demo-dynamic-file-rule-1.5.1-SNAPSHOT.jar!/classes/FlowRule.json', + // your flowRuleInJarName is 'classes/FlowRule.json' + String flowRuleInJarPath = "FlowRule.json"; + + FileInJarReadableDataSource> flowRuleDataSource = new FileInJarReadableDataSource<>( + jarPath,flowRuleInJarPath, flowRuleListParser); + FlowRuleManager.register2Property(flowRuleDataSource.getProperty()); + } + + private Converter> flowRuleListParser = source -> JSON.parseObject(source, + new TypeReference>() {}); +} diff --git a/sentinel-extension/sentinel-datasource-extension/src/main/java/com/alibaba/csp/sentinel/datasource/FileInJarReadableDataSource.java b/sentinel-extension/sentinel-datasource-extension/src/main/java/com/alibaba/csp/sentinel/datasource/FileInJarReadableDataSource.java new file mode 100644 index 00000000..358653af --- /dev/null +++ b/sentinel-extension/sentinel-datasource-extension/src/main/java/com/alibaba/csp/sentinel/datasource/FileInJarReadableDataSource.java @@ -0,0 +1,140 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.csp.sentinel.datasource; + +import com.alibaba.csp.sentinel.log.RecordLog; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +/** + *

+ * A {@link ReadableDataSource} based on jarfile. This class can only read file when it + * run but will not automatically refresh if it is changed. + *

+ *

+ * Limitations: Default read buffer size is 1 MB. If file size is greater than + * buffer size, exceeding bytes will be ignored. Default charset is UTF-8. + *

+ * + * @author dingq + * @date 2019-03-30 + */ +public class FileInJarReadableDataSource extends AutoRefreshDataSource { + private static final int MAX_SIZE = 1024 * 1024 * 4; + private static final long DEFAULT_REFRESH_MS = 3000; + private static final int DEFAULT_BUF_SIZE = 1024 * 1024; + private static final Charset DEFAULT_CHAR_SET = Charset.forName("utf-8"); + + private byte[] buf; + private JarEntry jarEntry; + private JarFile jarFile; + private final Charset charset; + private final String jarName; + private final String fileInJarName; + + /** + * @param jarName the jar to read + * @param fileInJarName the file in jar to read + * @param configParser the config decoder (parser) + * @throws FileNotFoundException + */ + public FileInJarReadableDataSource(String jarName, String fileInJarName, Converter configParser) + throws IOException { + this(jarName, fileInJarName, configParser, DEFAULT_REFRESH_MS, DEFAULT_BUF_SIZE, DEFAULT_CHAR_SET); + } + + public FileInJarReadableDataSource(String jarName, String fileInJarName, Converter configParser, int bufSize) + throws IOException { + this(jarName, fileInJarName, configParser, DEFAULT_REFRESH_MS, bufSize, DEFAULT_CHAR_SET); + } + + public FileInJarReadableDataSource(String jarName, String fileInJarName, Converter configParser, Charset charset) + throws IOException { + this(jarName, fileInJarName, configParser, DEFAULT_REFRESH_MS, DEFAULT_BUF_SIZE, charset); + } + + public FileInJarReadableDataSource(String jarName, String fileInJarName, Converter configParser, long recommendRefreshMs, int bufSize, + Charset charset) throws IOException { + super(configParser, recommendRefreshMs); + if (bufSize <= 0 || bufSize > MAX_SIZE) { + throw new IllegalArgumentException("bufSize must between (0, " + MAX_SIZE + "], but " + bufSize + " get"); + } + if (charset == null) { + throw new IllegalArgumentException("charset can't be null"); + } + this.buf = new byte[bufSize]; + this.charset = charset; + this.jarName = jarName; + this.fileInJarName = fileInJarName; + refreshJar(); + firstLoad(); + } + + @Override + public String readSource() throws Exception { + if (null == jarEntry) { + // Will throw FileNotFoundException later. + RecordLog.warn(String.format("[FileInJarReadableDataSource] File does not exist: %s", jarFile.getName())); + } + InputStream inputStream = null; + try { + inputStream = jarFile.getInputStream(jarEntry); + if (inputStream.available() > buf.length) { + throw new IllegalStateException(jarFile.getName() + " file size=" + inputStream.available() + + ", is bigger than bufSize=" + buf.length + ". Can't read"); + } + int len = inputStream.read(buf); + return new String(buf, 0, len, charset); + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (Exception ignore) { + } + } + } + + } + + @Override + protected boolean isModified() { + return false; + } + + private void firstLoad() { + try { + T newValue = loadConfig(); + getProperty().updateValue(newValue); + } catch (Throwable e) { + RecordLog.info("loadConfig exception", e); + } + } + + @Override + public void close() throws Exception { + super.close(); + buf = null; + } + + private void refreshJar() throws IOException { + this.jarFile = new JarFile(jarName); + this.jarEntry = jarFile.getJarEntry(fileInJarName); + } +}