├── src ├── test │ └── main │ │ ├── resources │ │ ├── app.properties │ │ └── logback.xml │ │ └── java │ │ ├── VChainTest.java │ │ ├── RecursionTest.java │ │ ├── LatchLockTest.java │ │ ├── CopierTest.java │ │ ├── UtilsTest.java │ │ ├── DBTest.java │ │ ├── CacheTest.java │ │ ├── DevourerTest.java │ │ └── AppTest.java └── main │ └── java │ └── cn │ └── xnatural │ └── app │ ├── v │ ├── VProcessor.java │ └── VChain.java │ ├── Inject.java │ ├── Pause.java │ ├── Lazier.java │ ├── LatchLock.java │ ├── Recursion.java │ ├── util │ ├── Tailer.java │ ├── Copier.java │ ├── Httper.java │ └── DB.java │ ├── ServerTpl.java │ ├── Devourer.java │ ├── Utils.java │ ├── CacheSrv.java │ └── AppContext.java ├── .gitignore ├── pom.xml ├── LICENSE └── README.md /src/test/main/resources/app.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xnat9/tiny/HEAD/src/test/main/resources/app.properties -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | build/ 3 | classes/ 4 | data/ 5 | out/ 6 | /build/ 7 | /target/* 8 | !/build/libs *.jar 9 | .gradle/ 10 | .settings/ 11 | .idea/* 12 | *.iml 13 | .project 14 | .classpath 15 | /log -------------------------------------------------------------------------------- /src/main/java/cn/xnatural/app/v/VProcessor.java: -------------------------------------------------------------------------------- 1 | package cn.xnatural.app.v; 2 | 3 | /** 4 | * V 链{@link VChain}中的处理器节点 5 | */ 6 | public interface VProcessor { 7 | 8 | /** 9 | * V 链中先执行 10 | * @param input 入参 11 | * @return 返回 12 | */ 13 | Object pre(Object input); 14 | 15 | /** 16 | * V 链中后执行 17 | * @param input 入参 18 | * @return 返回 19 | */ 20 | Object post(Object input); 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/cn/xnatural/app/Inject.java: -------------------------------------------------------------------------------- 1 | package cn.xnatural.app; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.Target; 6 | 7 | import static java.lang.annotation.ElementType.FIELD; 8 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 9 | 10 | /** 11 | *
12 |  * 对象注入
13 |  * 匹配规则:
14 |  *  1. 如果 {@link #name()} 为空
15 |  *      1.1. 匹配: 字段类型 和 字段名
16 |  *      1.2. 匹配: 字段类型
17 |  *  2. 匹配: 字段类型 和 {@link #name()}
18 |  *  
19 | */ 20 | @Target({ FIELD }) 21 | @Retention(RUNTIME) 22 | @Documented 23 | public @interface Inject { 24 | /** 25 | * 指定bean对象名 26 | */ 27 | String name() default ""; 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/cn/xnatural/app/Pause.java: -------------------------------------------------------------------------------- 1 | package cn.xnatural.app; 2 | 3 | import java.time.Duration; 4 | 5 | 6 | /** 7 | * 暂停器 8 | */ 9 | public class Pause { 10 | /** 11 | * 开始时间 12 | */ 13 | protected long start = System.currentTimeMillis(); 14 | /** 15 | * 暂停时长 16 | */ 17 | protected Duration duration; 18 | 19 | public Pause(Duration duration) { 20 | if (duration == null) throw new NullPointerException("Param duration required"); 21 | this.duration = duration; 22 | } 23 | 24 | 25 | /** 26 | * 暂停时间是否已过 27 | * @return true: 时间已过 28 | */ 29 | public boolean isTimeout() { return left() < 0; } 30 | 31 | 32 | /** 33 | * 剩余时长(单位:ms) 34 | * @return 小于等于0: 没有剩余时时, 大于0: 剩余时长 35 | */ 36 | public long left() { return (start + duration.toMillis()) - System.currentTimeMillis(); } 37 | } 38 | -------------------------------------------------------------------------------- /src/test/main/java/VChainTest.java: -------------------------------------------------------------------------------- 1 | import cn.xnatural.app.v.VChain; 2 | import cn.xnatural.app.v.VProcessor; 3 | import org.junit.jupiter.api.Test; 4 | 5 | public class VChainTest { 6 | 7 | 8 | @Test 9 | void testVChain() { 10 | new VChain().add(new VProcessor() { 11 | @Override 12 | public Object pre(Object input) { 13 | return null; 14 | } 15 | 16 | @Override 17 | public Object post(Object input) { 18 | return null; 19 | } 20 | }).add(new VProcessor() { 21 | @Override 22 | public Object pre(Object input) { 23 | return null; 24 | } 25 | 26 | @Override 27 | public Object post(Object input) { 28 | return null; 29 | } 30 | }).run("xx"); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/cn/xnatural/app/v/VChain.java: -------------------------------------------------------------------------------- 1 | package cn.xnatural.app.v; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import java.util.Iterator; 7 | import java.util.LinkedList; 8 | 9 | /** 10 | * V链: 执行过程先进后出(类似V型) 11 | */ 12 | public class VChain { 13 | protected static final Logger log = LoggerFactory.getLogger(VChain.class); 14 | protected final LinkedList ps = new LinkedList<>(); 15 | 16 | 17 | /** 18 | * 执行链 19 | * 按v型依次执行, 上个处理结果为下个处理器的入参 20 | * @param input 入参 21 | * @return 链路执行结果 22 | */ 23 | public Object run(Object input) { 24 | Object result = input; 25 | for (Iterator it = ps.iterator(); it.hasNext(); ) { 26 | result = it.next().pre(result); 27 | } 28 | for (Iterator it = ps.descendingIterator(); it.hasNext(); ) { 29 | result = it.next().post(result); 30 | } 31 | return result; 32 | } 33 | 34 | 35 | /** 36 | * 添加V处理器 37 | * @param vProcessor v处理器 38 | * @return {@link VChain} 39 | */ 40 | public VChain add(VProcessor vProcessor) { ps.offer(vProcessor); return this; } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/cn/xnatural/app/Lazier.java: -------------------------------------------------------------------------------- 1 | package cn.xnatural.app; 2 | 3 | import java.util.function.Supplier; 4 | 5 | /** 6 | * Groovy @Lazy 实现 7 | * @param 8 | */ 9 | public class Lazier implements Supplier { 10 | private final Supplier supplier; 11 | // 只执行一次 12 | private boolean once = false; 13 | private T result; 14 | 15 | public Lazier(Supplier supplier) { 16 | if (supplier == null) throw new NullPointerException("Param supplier is null"); 17 | this.supplier = supplier; 18 | } 19 | 20 | 21 | /** 22 | * 清除 23 | */ 24 | public void clear() { 25 | synchronized (this) { 26 | once = false; 27 | result = null; 28 | } 29 | } 30 | 31 | 32 | @Override 33 | public T get() { 34 | if (!once) { 35 | synchronized (this) { 36 | if (!once) { 37 | result = supplier.get(); 38 | if (result != null) once = true; //为空,则重新取 39 | } 40 | } 41 | } 42 | return result; 43 | } 44 | 45 | 46 | /** 47 | * 是否已执行 48 | */ 49 | public boolean done() { return once; } 50 | 51 | 52 | @Override 53 | public String toString() { return result == null ? null : result.toString(); } 54 | } 55 | -------------------------------------------------------------------------------- /src/test/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | [%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] [%-6thread] [%-36.36C :%-4L] => %m%n 11 | 12 | 13 | 14 | 15 | ${log_path}/${log_file_name}.log 16 | 17 | [%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] [%-6thread] [%-36.36C :%-4L] => %m%n 18 | utf-8 19 | 20 | 21 | ${log_path}/${log_file_name}.%d{yyyyMMdd}.%i.log 22 | ${log.maxHistory:-500} 23 | ${log.maxFileSize:-50MB} 24 | ${log.totalSizeCap:-100GB} 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/test/main/java/RecursionTest.java: -------------------------------------------------------------------------------- 1 | import cn.xnatural.app.Recursion; 2 | import org.junit.jupiter.api.Test; 3 | 4 | public class RecursionTest { 5 | 6 | 7 | @Test 8 | void factorial() throws Exception { 9 | System.out.println(factorialTailRecursion(1, 10_000_000).invoke()); 10 | } 11 | 12 | 13 | @Test 14 | void fibonacciTest() { 15 | System.out.println(fibonacciMemo(100)); 16 | } 17 | 18 | 19 | /** 20 | * 使用同一封装的备忘录模式 执行斐波那契策略 21 | * @param n 第n个斐波那契数 22 | * @return 第n个斐波那契数 23 | */ 24 | long fibonacciMemo(long n) { 25 | return Recursion.memo((fib, number) -> { 26 | if (number == 0 || number == 1) { 27 | new Exception().printStackTrace(); 28 | return 1L; 29 | } 30 | return fib.apply(number -1 ) + fib.apply(number-2); 31 | }, n); 32 | } 33 | 34 | 35 | /** 36 | * 阶乘计算 -- 使用尾递归接口完成 37 | * @param factorial 当前递归栈的结果值 38 | * @param number 下一个递归需要计算的值 39 | * @return 尾递归接口,调用invoke启动及早求值获得结果 40 | */ 41 | Recursion factorialTailRecursion(final long factorial, final long number) { 42 | if (number == 1) { 43 | // new Exception().printStackTrace(); 44 | return Recursion.done(factorial); 45 | } 46 | else { 47 | return Recursion.call(() -> factorialTailRecursion(factorial + number, number - 1)); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/test/main/java/LatchLockTest.java: -------------------------------------------------------------------------------- 1 | import cn.xnatural.app.LatchLock; 2 | import org.junit.jupiter.api.Test; 3 | 4 | import java.util.concurrent.ExecutorService; 5 | import java.util.concurrent.Executors; 6 | import java.util.concurrent.ThreadFactory; 7 | import java.util.concurrent.atomic.AtomicBoolean; 8 | import java.util.concurrent.atomic.AtomicInteger; 9 | 10 | public class LatchLockTest { 11 | 12 | 13 | @Test 14 | void testRelease() throws Exception { 15 | LatchLock lock = new LatchLock(); 16 | 17 | ExecutorService exec = Executors.newFixedThreadPool(5, new ThreadFactory() { 18 | final AtomicInteger i = new AtomicInteger(); 19 | @Override 20 | public Thread newThread(Runnable r) { 21 | return new Thread(r,"t-" + i.incrementAndGet()); 22 | } 23 | }); 24 | 25 | final AtomicBoolean stop = new AtomicBoolean(false); 26 | 27 | exec.execute(() -> { 28 | while (!stop.get()) { 29 | lock.tryLock(); 30 | } 31 | }); 32 | Runnable releaseFn = () -> { 33 | while (!stop.get()) { 34 | int i = lock.release(); 35 | if (i < 0) { 36 | System.out.println("error: after release lock < 0"); 37 | } 38 | } 39 | }; 40 | exec.execute(releaseFn); 41 | exec.execute(releaseFn); 42 | 43 | Thread.sleep(1000 * 60 * 2); 44 | stop.set(true); 45 | exec.shutdown(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/cn/xnatural/app/LatchLock.java: -------------------------------------------------------------------------------- 1 | package cn.xnatural.app; 2 | 3 | import java.util.concurrent.atomic.AtomicInteger; 4 | 5 | /** 6 | * 流量限制锁 7 | */ 8 | public class LatchLock { 9 | /** 10 | * 流量限制大小 11 | */ 12 | protected int limit = 1; 13 | /** 14 | * 当前并发大小 15 | */ 16 | protected final AtomicInteger latchSize = new AtomicInteger(); 17 | 18 | 19 | /** 20 | * 设置限制 21 | * @param limit >0 22 | */ 23 | public LatchLock limit(int limit) { 24 | if (limit < 1) throw new IllegalArgumentException("Param limit must >0"); 25 | this.limit = limit; 26 | return this; 27 | } 28 | 29 | 30 | /** 31 | * 获取一个锁 32 | *
33 |      * final LatchLock lock = new LatchLock();
34 |      * lock.limit(3); // 设置并发限制. 默认为1
35 |      * if (lock.tryLock()) { // 尝试获取一个锁
36 |      *     try {
37 |      *         // 被执行的代码块
38 |      *     } finally {
39 |      *         lock.release(); // 释放一个锁
40 |      *     }
41 |      * }
42 |      * 
43 | */ 44 | public boolean tryLock() { 45 | int latch = latchSize.get(); 46 | if (latch >= limit) return false; 47 | if (latchSize.compareAndSet(latch, latch + 1)) return true; 48 | return tryLock(); 49 | } 50 | 51 | 52 | /** 53 | * 释放一个锁 54 | * 一般是 {@link #tryLock()} 成功后 调一次 55 | * @return 释放后当前锁的个数 56 | */ 57 | public int release() { 58 | int latch = latchSize.get(); 59 | if (latch <= 0) return latch; 60 | if (latchSize.compareAndSet(latch, latch - 1)) { 61 | return latch - 1; 62 | } 63 | return latchSize.get(); 64 | } 65 | 66 | 67 | /** 68 | * 当前已获取锁个数 69 | */ 70 | public int getLatchSize() { return latchSize.get(); } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/cn/xnatural/app/Recursion.java: -------------------------------------------------------------------------------- 1 | package cn.xnatural.app; 2 | 3 | 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | import java.util.function.BiFunction; 7 | import java.util.function.Function; 8 | import java.util.stream.Stream; 9 | 10 | 11 | /** 12 | * java无限递归优化实现 13 | * https://www.cnblogs.com/invoker-/p/7723420.html 14 | * https://www.cnblogs.com/invoker-/p/7728452.html 15 | * @param 16 | */ 17 | @FunctionalInterface 18 | public interface Recursion { 19 | /** 20 | * 用于递归栈帧之间的连接,惰性求值 21 | * @return 下一个递归栈帧 22 | */ 23 | Recursion apply(); 24 | 25 | 26 | /** 27 | * 判断当前递归是否结束 28 | * @return 默认为false,因为正常的递归过程中都还未结束 29 | */ 30 | default boolean isFinished(){ return false; } 31 | 32 | 33 | /** 34 | * 获得递归结果,只有在递归结束才能调用,这里默认给出异常,通过工具类的重写来获得值 35 | * @return 递归最终结果 36 | */ 37 | default T getResult() { 38 | if (!isFinished()) throw new RuntimeException("递归还没有结束,调用获得结果异常!"); 39 | else return null; 40 | } 41 | 42 | 43 | /** 44 | * 及早求值,执行者一系列的递归,因为栈帧只有一个,所以使用findFirst获得最终的栈帧,接着调用getResult方法获得最终递归值 45 | * @return 及早求值,获得最终递归结果 46 | */ 47 | default T invoke() { 48 | return Stream.iterate(this, Recursion::apply) 49 | .filter(Recursion::isFinished) 50 | .findFirst() 51 | .map(Recursion::getResult) 52 | .orElse(null); 53 | } 54 | 55 | 56 | /** 57 | * 统一结构的方法,获得当前递归的下一个递归 58 | * 59 | * @param nextFrame 下一个递归 60 | * @param T 61 | * @return 下一个递归 62 | */ 63 | static Recursion call(final Recursion nextFrame) { return nextFrame; } 64 | 65 | 66 | /** 67 | * 结束当前递归,重写对应的默认方法的值,完成状态改为true,设置最终返回结果,设置非法递归调用 68 | * @param value 最终递归值 69 | * @param T 70 | * @return 一个isFinished状态true的尾递归, 外部通过调用接口的invoke方法及早求值, 启动递归求值。 71 | */ 72 | static Recursion done(T value) { 73 | return new Recursion() { 74 | @Override 75 | public Recursion apply() { throw new RuntimeException("递归已经结束,非法调用apply方法"); } 76 | 77 | @Override 78 | public boolean isFinished() { return true; } 79 | 80 | @Override 81 | public T getResult() { return value; } 82 | }; 83 | } 84 | 85 | 86 | /** 87 | * 备忘录模式 函数封装 88 | * @param function 递归策略算法 89 | * @param input 输入值 90 | * @param 输出值类型 91 | * @param 返回值类型 92 | * @return 将输入值输入递归策略算法,计算出的最终结果 93 | */ 94 | static R memo(final BiFunction, I, R> function, final I input) { 95 | final Map cache = new HashMap<>(); 96 | return new Function() { 97 | @Override 98 | public R apply(final I input) { 99 | return cache.computeIfAbsent(input, key -> function.apply(this, key)); 100 | } 101 | }.apply(input); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/test/main/java/CopierTest.java: -------------------------------------------------------------------------------- 1 | import cn.xnatural.app.Utils; 2 | import com.alibaba.fastjson.JSON; 3 | import org.junit.jupiter.api.Test; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | 7 | import java.math.BigInteger; 8 | import java.text.SimpleDateFormat; 9 | import java.util.Date; 10 | import java.util.HashMap; 11 | import java.util.Map; 12 | 13 | public class CopierTest { 14 | static final Logger log = LoggerFactory.getLogger(CopierTest.class); 15 | 16 | @Test 17 | void mapToMapTest() { 18 | Map src = new HashMap<>(); 19 | src.put("p1", "1111"); 20 | src.put("time", System.currentTimeMillis()); 21 | String result = Utils.copier( 22 | src, 23 | new HashMap<>() 24 | ) 25 | .addConverter("time", o -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date((long) o))) 26 | .mapProp( "p1", "prop1") 27 | .mapProp( "time", "date") 28 | // .showClassProp() 29 | .ignoreNull(true) 30 | .build() 31 | .toString(); 32 | log.info("map to map: " + result); 33 | } 34 | 35 | 36 | @Test 37 | void toMapTest() { 38 | String result = Utils.copier( 39 | new Object() { 40 | public String p1 = "xxx"; 41 | Integer p2 = 1; 42 | String p3 = "p3"; 43 | public String p4 = "p4"; 44 | public long time = System.currentTimeMillis(); 45 | public String p5; 46 | public BigInteger p6 = BigInteger.valueOf(6); 47 | 48 | public Integer getP2() { return p2; } 49 | private String getPri() { return "pri";} 50 | }, 51 | new HashMap<>() 52 | ) 53 | .addConverter("time", o -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date((long) o))) 54 | .addConverter("p4", o -> "pp4") 55 | .mapProp( "p1", "prop1") 56 | .add("pp", () -> "pp") 57 | //.showClassProp() 58 | .ignoreNull(true) 59 | .build() 60 | .toString(); 61 | log.info("javabean to map: " + result); 62 | } 63 | 64 | 65 | @Test 66 | void toJavabeanTest() { 67 | Object result = Utils.copier( 68 | new Object() { 69 | public String p1 = "xxx"; 70 | Integer p2 = 1; 71 | String p3 = "p3"; 72 | public String p4 = "p4"; 73 | public long time = System.currentTimeMillis(); 74 | public String p5; 75 | public BigInteger p6 = BigInteger.valueOf(6); 76 | 77 | public Integer getP2() { return p2; } 78 | private String getPri() { return "pri";} 79 | }, 80 | new Object() { 81 | public String p1; 82 | private Integer p2; 83 | public Date time; 84 | public void setP2(Integer p2) { this.p2 = p2; } 85 | public Integer getP2() { return p2; } 86 | public void setR(String r) {} 87 | public String getR() {return p1;} 88 | } 89 | ) 90 | .addConverter("time", o -> new Date((long) o)) 91 | .build(); 92 | log.info("javabean to javabean: " + JSON.toJSONString(result)); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/test/main/java/UtilsTest.java: -------------------------------------------------------------------------------- 1 | import cn.xnatural.app.Utils; 2 | import com.alibaba.fastjson.JSONObject; 3 | import org.junit.jupiter.api.Test; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | 7 | import java.io.*; 8 | import java.nio.charset.Charset; 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | 12 | public class UtilsTest { 13 | static final Logger log = LoggerFactory.getLogger(UtilsTest.class); 14 | 15 | 16 | public static void main(String[] args) { 17 | Utils.tailer().tail("/media/xnat/store/code_repo/gy/log/app.log", 10); 18 | } 19 | 20 | 21 | @Test 22 | void pid() { 23 | log.info(Utils.pid()); 24 | } 25 | 26 | 27 | @Test 28 | void isLinux() { 29 | log.info(Utils.isLinux() + ""); 30 | } 31 | 32 | 33 | @Test 34 | void baseDir() { 35 | log.info(Utils.baseDir(null).getAbsolutePath()); 36 | } 37 | 38 | 39 | @Test 40 | void ipv4() { 41 | log.info(Utils.ipv4()); 42 | } 43 | 44 | 45 | @Test 46 | void md5Hex() { 47 | log.info(Utils.md5Hex("000".getBytes(Charset.forName("utf-8")))); 48 | } 49 | 50 | 51 | @Test 52 | void nanoIdTest() { 53 | log.info("nanoId: " + Utils.nanoId()); 54 | log.info("nanoId: " + Utils.nanoId(1)); 55 | } 56 | 57 | 58 | @Test 59 | void toTest() { 60 | log.info("to Integer: " + Utils.to(1, Integer.class)); 61 | log.info("to String: " + Utils.to("s", String.class)); 62 | } 63 | 64 | 65 | @Test 66 | void http() throws Exception { 67 | Utils.http().get("http://xnatural.cn:9090/test/cus?p2=2") 68 | .param("p1", 1) 69 | .debug().execute(); 70 | Utils.http().post("http://xnatural.cn:9090/test/cus") 71 | .debug().execute(); 72 | Utils.http().post("http://xnatural.cn:9090/test/form") 73 | .param("p1", "p1") 74 | .debug().execute(); 75 | Utils.http().post("http://xnatural.cn:9090/test/json") 76 | .jsonBody(new JSONObject().fluentPut("p1", 1).toString()) 77 | .debug().execute(); 78 | Utils.http().post("http://xnatural.cn:9090/test/upload") 79 | .param("version", "1.0.1") 80 | .param("file", new File("d:/tmp/tmp.json")) 81 | .debug().execute(); 82 | Utils.http().post("http://xnatural.cn:9090/test/upload") 83 | .fileStream("file", "testfile.md", new FileInputStream("d:/tmp/test.md")) 84 | .debug().execute(); 85 | } 86 | 87 | 88 | @Test 89 | void buildUrl() { 90 | Map params = new HashMap<>(); 91 | params.put("p1", "111"); 92 | params.put("p2", 222); 93 | System.out.println( 94 | Utils.buildUrl("http://xnatural.cn:9090/test", null) 95 | ); 96 | System.out.println( 97 | Utils.buildUrl("http://xnatural.cn:9090/test", params) 98 | ); 99 | System.out.println( 100 | Utils.buildUrl("http://xnatural.cn:9090/test?", params) 101 | ); 102 | System.out.println( 103 | Utils.buildUrl("http://xnatural.cn:9090/test?test=", params) 104 | ); 105 | System.out.println( 106 | Utils.buildUrl("http://xnatural.cn:9090/test?test=aaa", params) 107 | ); 108 | } 109 | 110 | 111 | @Test 112 | void ioCopy() throws Exception { 113 | try (InputStream is = new FileInputStream("d:/tmp/1.txt"); OutputStream os = new FileOutputStream("d:/tmp/2.txt")) { 114 | Utils.ioCopy(is, os); 115 | } 116 | } 117 | } 118 | 119 | -------------------------------------------------------------------------------- /src/main/java/cn/xnatural/app/util/Tailer.java: -------------------------------------------------------------------------------- 1 | package cn.xnatural.app.util; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import java.io.RandomAccessFile; 7 | import java.util.LinkedList; 8 | import java.util.Queue; 9 | import java.util.Random; 10 | import java.util.concurrent.Executor; 11 | import java.util.function.Function; 12 | 13 | /** 14 | * 文件内容监控器(类linux tail) 15 | */ 16 | public class Tailer { 17 | protected static final Logger log = LoggerFactory.getLogger(Tailer.class); 18 | protected Thread th; 19 | protected boolean stopFlag; 20 | protected Function lineFn; 21 | protected Executor exec; 22 | 23 | /** 24 | * 处理输出行 25 | * @param lineFn 函数返回true 继续输出, 返回false则停止输出 26 | */ 27 | public Tailer handle(Function lineFn) {this.lineFn = lineFn; return this;} 28 | /** 29 | * 设置处理线程池 30 | * @param exec 线程池 31 | */ 32 | public Tailer exec(Executor exec) {this.exec = exec; return this;} 33 | /** 34 | * 停止 35 | */ 36 | public void stop() {this.stopFlag = true;} 37 | 38 | /** 39 | * tail 文件内容监控 40 | * @param file 文件全路径 41 | * @return {@link Tailer} 42 | */ 43 | public Tailer tail(String file) { return tail(file, 5); } 44 | /** 45 | * tail 文件内容监控 46 | * @param file 文件全路径 47 | * @param follow 从最后第几行开始 48 | * @return {@link Tailer} 49 | */ 50 | public Tailer tail(String file, Integer follow) { 51 | if (lineFn == null) lineFn = (line) -> {System.out.println(line); return true;}; 52 | Runnable fn = () -> { 53 | String tName = Thread.currentThread().getName(); 54 | try { 55 | Thread.currentThread().setName("Tailer-" + file); 56 | run(file, (follow == null ? 0 : follow)); 57 | } catch (Exception ex) { 58 | log.error("Tail file " + file + " error", ex); 59 | } finally { 60 | Thread.currentThread().setName(tName); 61 | } 62 | }; 63 | if (exec != null) { 64 | exec.execute(fn); 65 | } else { 66 | th = new Thread(fn, "Tailer-" + file); 67 | // th.setDaemon(true); 68 | th.start(); 69 | } 70 | return this; 71 | } 72 | 73 | private void run(String file, Integer follow) throws Exception { 74 | try (RandomAccessFile raf = new RandomAccessFile(file, "r")) { 75 | Queue buffer = (follow != null && follow > 0) ? new LinkedList<>() : null; // 用来保存最后几行(follow)数据 76 | 77 | // 当前第一次到达文件结尾时,设为true 78 | boolean firstEnd = false; 79 | String line; 80 | while (!stopFlag) { 81 | line = raf.readLine(); 82 | if (line == null) { // 当读到文件结尾时(line为空即为文件结尾) 83 | if (firstEnd) { 84 | Thread.sleep(100L * new Random().nextInt(10)); 85 | // raf.seek(file.length()) // 重启定位到文件结尾(有可能文件被重新写入了) 86 | continue; 87 | } 88 | firstEnd = true; 89 | if (buffer != null) { // 第一次到达结尾后, 清理buffer数据 90 | do { 91 | line = buffer.poll(); 92 | if (line == null) break; 93 | this.stopFlag = !lineFn.apply(line); 94 | } while (!stopFlag); 95 | buffer = null; 96 | } 97 | } else { // 读到行有数据 98 | line = new String(line.getBytes("ISO-8859-1"),"utf-8"); 99 | if (firstEnd) { // 直接处理行字符串 100 | stopFlag = !lineFn.apply(line); 101 | } else if (follow != null && follow > 0) { 102 | buffer.offer(line); 103 | if (buffer.size() > follow) buffer.poll(); 104 | } 105 | } 106 | } 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/test/main/java/DBTest.java: -------------------------------------------------------------------------------- 1 | import cn.xnatural.app.util.DB; 2 | import org.junit.jupiter.api.Assertions; 3 | import org.junit.jupiter.api.Test; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | 7 | import java.text.SimpleDateFormat; 8 | import java.util.Date; 9 | 10 | public class DBTest { 11 | static final Logger log = LoggerFactory.getLogger(DBTest.class); 12 | 13 | @Test 14 | void getJdbcUrlTest() throws Exception { 15 | try (DB repo = new DB("jdbc:mysql://localhost:3306/test?useSSL=false&user=root&password=root&allowPublicKeyRetrieval=true")) { 16 | log.info(repo.getJdbcUrl()); 17 | } 18 | } 19 | 20 | 21 | @Test 22 | void rowTest() throws Exception { 23 | try (DB repo = new DB("jdbc:mysql://localhost:3306/test?useSSL=false&user=root&password=root&allowPublicKeyRetrieval=true")) { 24 | log.info(repo.row("select * from test order by id desc").toString()); 25 | } 26 | } 27 | 28 | 29 | @Test 30 | void rowsTest() throws Exception { 31 | try (DB repo = new DB("jdbc:mysql://localhost:3306/test?useSSL=false&user=root&password=root&allowPublicKeyRetrieval=true")) { 32 | log.info(repo.rows("select * from test where id=? limit 0, ?", 2, 10).toString()); 33 | } 34 | } 35 | 36 | 37 | @Test 38 | void inTest() throws Exception { 39 | try (DB repo = new DB("jdbc:mysql://localhost:3306/test?useSSL=false&user=root&password=root&allowPublicKeyRetrieval=true")) { 40 | log.info(repo.rows("select * from test where id in (?, ?)", 2, 7).toString()); 41 | } 42 | } 43 | 44 | 45 | @Test 46 | void singleTest() throws Exception { 47 | try (DB repo = new DB("jdbc:mysql://localhost:3306/test?useSSL=false&user=root&password=root&allowPublicKeyRetrieval=true")) { 48 | Assertions.assertNotNull(repo.single("select count(1) from test", Integer.class)); 49 | } 50 | } 51 | 52 | 53 | @Test 54 | void insertTest() throws Exception { 55 | try (DB repo = new DB("jdbc:mysql://localhost:3306/test?useSSL=false&user=root&password=root&allowPublicKeyRetrieval=true")) { 56 | log.info("插入一条: " + repo.execute("insert into test(name, age, create_time) values(?, ?, ?)", 57 | "nn" + new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()), new Date().getSeconds(), new Date())); 58 | } 59 | } 60 | 61 | 62 | @Test 63 | void updateTest() throws Exception { 64 | try (DB repo = new DB("jdbc:mysql://localhost:3306/test?useSSL=false&user=root&password=root&allowPublicKeyRetrieval=true")) { 65 | log.info("更新数据: " + repo.execute("update test set age=? where id=?", 30, 7)); 66 | } 67 | } 68 | 69 | 70 | @Test 71 | void insertWithGeneratedKeyTest() throws Exception { 72 | try (DB repo = new DB("jdbc:mysql://localhost:3306/test?useSSL=false&user=root&password=root&allowPublicKeyRetrieval=true")) { 73 | Object id = repo.insertWithGeneratedKey("insert into test(name, age, create_time) values(?, ?, ?)", 74 | "nn" + new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()), new Date().getSeconds(), new Date()); 75 | Assertions.assertNotNull(id); 76 | log.info("插入一条返回主键: " + id); 77 | } 78 | } 79 | 80 | 81 | @Test 82 | void transTest() throws Exception { 83 | try (DB repo = new DB("jdbc:mysql://localhost:3306/test?useSSL=false&user=root&password=root&allowPublicKeyRetrieval=true")) { 84 | Integer count = repo.single("select count(1) from test", Integer.class); 85 | try { 86 | repo.trans(() -> { 87 | repo.execute("insert into test(name, age, create_time) values(?, ?, ?)", 88 | "nn" + new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()), new Date().getSeconds(), new Date()); 89 | throw new RuntimeException("xxxx"); 90 | }); 91 | } catch (Exception ex) {} 92 | Assertions.assertTrue( 93 | repo.single("select count(1) from test", Integer.class) > count, 94 | "trans test fail" 95 | ); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/test/main/java/CacheTest.java: -------------------------------------------------------------------------------- 1 | import cn.xnatural.app.AppContext; 2 | import cn.xnatural.app.CacheSrv; 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.time.Duration; 7 | import java.util.Objects; 8 | 9 | public class CacheTest { 10 | 11 | 12 | @Test 13 | void cacheGet() { 14 | final AppContext app = new AppContext(); 15 | app.addSource(new CacheSrv()); 16 | app.start(); 17 | 18 | CacheSrv cache = app.bean(CacheSrv.class, null); 19 | cache.set("c1", "aaa"); 20 | Assertions.assertTrue(Objects.equals(cache.get("c1"), "aaa")); 21 | Assertions.assertTrue(Objects.equals(app.ep().fire("cacheSrv.get", "c1"), "aaa")); 22 | cache.hset("key", "dataKey", 1); 23 | Assertions.assertTrue(Objects.equals(cache.hget("key", "dataKey"), 1)); 24 | Assertions.assertTrue(Objects.equals(app.ep().fire("cacheSrv.hget", "key", "dataKey"), 1)); 25 | } 26 | 27 | 28 | @Test 29 | void cacheLimit() { 30 | int limit = 10; 31 | final AppContext app = new AppContext(); 32 | app.addSource(new CacheSrv()); 33 | app.env().put("cacheSrv.itemLimit", limit); 34 | app.start(); 35 | 36 | CacheSrv cache = app.bean(CacheSrv.class, null); 37 | for (int i = 0; i < 200; i++) { 38 | cache.set("key_" + i, i); 39 | int v = i; 40 | cache.hset("key", "" + i, (AutoCloseable) () -> System.out.println("=====h close " + v)); 41 | } 42 | Assertions.assertTrue(cache.count() <= limit); 43 | } 44 | 45 | 46 | @Test 47 | void cacheExpire() throws Exception { 48 | int limit = 10; 49 | final AppContext app = new AppContext(); 50 | app.addSource(new CacheSrv()); 51 | app.env().put("cacheSrv.itemLimit", limit); 52 | app.start(); 53 | 54 | String key = "a"; 55 | 56 | CacheSrv cache = app.bean(CacheSrv.class, null); 57 | cache.set(key, 1, Duration.ofSeconds(5)); 58 | Thread.sleep(4000); 59 | Assertions.assertTrue((Integer) cache.get(key) == 1, ""); 60 | Thread.sleep(1000); 61 | Assertions.assertTrue(cache.get(key) == null, ""); 62 | 63 | long now = System.currentTimeMillis(); 64 | cache.set(key, 1, r -> 5000 + now); 65 | Thread.sleep(4000); 66 | Assertions.assertTrue((Integer) cache.get(key) == 1, ""); 67 | Thread.sleep(1000); 68 | Assertions.assertTrue(cache.get(key) == null, ""); 69 | 70 | 71 | String dataKey = "dataKey"; 72 | cache.hset(key, dataKey, 1, Duration.ofSeconds(5)); 73 | cache.hset(key, dataKey + "1", 1, Duration.ofSeconds(5)); 74 | cache.hset(key, dataKey + "2", 1, Duration.ofSeconds(5)); 75 | Thread.sleep(4000); 76 | Assertions.assertTrue((Integer) cache.hget(key, dataKey) == 1, ""); 77 | Thread.sleep(1000); 78 | Assertions.assertTrue(cache.hget(key, dataKey) == null, ""); 79 | Assertions.assertTrue(cache.hget(key, dataKey + "1") == null, ""); 80 | } 81 | 82 | 83 | @Test 84 | void cleanAllExpireRecord() throws Exception { 85 | int limit = 10; 86 | final AppContext app = new AppContext(); 87 | app.addSource(new CacheSrv()); 88 | app.env().put("cacheSrv.itemLimit", limit); 89 | app.start(); 90 | 91 | CacheSrv cache = app.bean(CacheSrv.class, null); 92 | 93 | for (int i = 0; i < 4; i++) { 94 | int v = i; 95 | cache.set("a" + i, (AutoCloseable) () -> System.out.println("=====close " + v), Duration.ofSeconds(4)); 96 | cache.hset("a" + i, "" + i, (AutoCloseable) () -> System.out.println("=====h close " + v), Duration.ofSeconds(4)); 97 | } 98 | Thread.sleep(3000L); 99 | cache.getAndUpdate("a0"); 100 | Thread.sleep(1000L); 101 | Assertions.assertTrue(cache.count() == 8); 102 | cache.set("a", "a", Duration.ofSeconds(2)); // 此时会清理所有过期的缓存 103 | Thread.sleep(3000L); 104 | Assertions.assertTrue(cache.count() == 2); 105 | } 106 | 107 | 108 | @Test 109 | void testClose() { 110 | final AppContext app = new AppContext(); 111 | app.addSource(new CacheSrv()); 112 | app.start(); 113 | 114 | String key = "a"; 115 | 116 | CacheSrv cache = app.bean(CacheSrv.class, null); 117 | cache.set(key, (AutoCloseable) () -> System.out.println("=====close")); 118 | cache.remove(key); 119 | 120 | String dataKey = "a"; 121 | cache.hset(key, dataKey, (AutoCloseable) () -> System.out.println("=====h close")); 122 | cache.hremove(key, dataKey); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/test/main/java/DevourerTest.java: -------------------------------------------------------------------------------- 1 | import cn.xnatural.app.Devourer; 2 | import org.junit.jupiter.api.Test; 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import java.time.Duration; 7 | import java.util.concurrent.ExecutorService; 8 | import java.util.concurrent.Executors; 9 | import java.util.concurrent.ThreadFactory; 10 | import java.util.concurrent.atomic.AtomicBoolean; 11 | import java.util.concurrent.atomic.AtomicInteger; 12 | 13 | public class DevourerTest { 14 | static final Logger log = LoggerFactory.getLogger(DevourerTest.class); 15 | 16 | @Test 17 | void testParallel() throws Exception { 18 | Devourer devourer = new Devourer("parallel"); 19 | devourer.parallel(3); 20 | long start = System.currentTimeMillis(); 21 | AtomicBoolean stop = new AtomicBoolean(false); 22 | while (System.currentTimeMillis() - start < 1000 * 15 && !stop.get()) { 23 | devourer.offer(() -> { 24 | int p = devourer.getParallel(); 25 | if (p > 3) { 26 | log.info("========================" + p); 27 | stop.set(true); 28 | } 29 | log.info("wait " + devourer.getWaitingCount() + ", " + p); 30 | // Utils.http().get("http://xnatural.cn:9090/test/cus?p1=" + count.getAndIncrement()).debug().execute(); 31 | // try { 32 | // Thread.sleep(2000 + new Random().nextInt(10000)); 33 | // } catch (InterruptedException e) { 34 | // e.printStackTrace(); 35 | // } 36 | }); 37 | // if (devourer.getWaitingCount() > 40) Thread.sleep(100 + new Random().nextInt(10000)); 38 | } 39 | Thread.sleep(1000 * 60 * 30); 40 | } 41 | 42 | 43 | @Test 44 | void testParallel2() throws Exception { 45 | Devourer devourer = new Devourer("parallel2"); 46 | devourer.parallel(3); 47 | for (int i = 0; i < 10; i++) { 48 | int j = i + 1; 49 | devourer.offer(() -> { 50 | log.info("start-" + j); 51 | try { 52 | Thread.sleep(1000 * 5); 53 | } catch (InterruptedException e) { 54 | e.printStackTrace(); 55 | } 56 | log.info("end-" + j); 57 | }); 58 | } 59 | Thread.sleep(1000 * 60 * 3); 60 | } 61 | 62 | 63 | @Test 64 | void testSuspend() throws Exception { 65 | Devourer devourer = new Devourer("suspend"); 66 | devourer.offer(() -> { 67 | log.info("执行"); 68 | devourer.suspend(Duration.ofMillis(1500)); 69 | }); 70 | 71 | for (int i = 0; i < 50; i++) { 72 | int finalI = i; 73 | Thread.sleep(100); 74 | devourer.offer(() -> { 75 | log.info("执行 " + finalI); 76 | }); 77 | log.info("added" + i); 78 | } 79 | devourer.resume(); 80 | Thread.sleep(60 * 1000); 81 | } 82 | 83 | 84 | @Test 85 | void testUseLast() throws Exception { 86 | Devourer devourer = new Devourer("useLast").useLast(true); 87 | for (int i = 0; i < 20; i++) { 88 | Thread.sleep(100); 89 | int finalI = i; 90 | devourer.offer(() -> { 91 | log.info("==========" + finalI); 92 | try { 93 | Thread.sleep(500); 94 | } catch (InterruptedException e) { 95 | log.error("", e); 96 | } 97 | }); 98 | } 99 | Thread.sleep(20 * 1000); 100 | } 101 | 102 | 103 | @Test 104 | void testSpeed() throws Exception { 105 | Devourer devourer = new Devourer(); 106 | devourer.speed("8/s"); 107 | 108 | ExecutorService exec = Executors.newFixedThreadPool(2, new ThreadFactory() { 109 | final AtomicInteger i = new AtomicInteger(); 110 | @Override 111 | public Thread newThread(Runnable r) { 112 | return new Thread(r,"t-" + i.incrementAndGet()); 113 | } 114 | }); 115 | 116 | final AtomicBoolean stop = new AtomicBoolean(false); 117 | 118 | exec.execute(() -> { 119 | while (!stop.get()) { 120 | devourer.offer(() -> { 121 | log.info("=====left: " + devourer.getWaitingCount()); 122 | }); 123 | try { 124 | Thread.sleep(100); 125 | } catch (InterruptedException e) { 126 | log.error("offer error", e); 127 | } 128 | } 129 | }); 130 | 131 | 132 | Thread.sleep(1000 * 60); 133 | stop.set(true); 134 | Thread.sleep(1000 * 20); 135 | exec.shutdown(); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/test/main/java/AppTest.java: -------------------------------------------------------------------------------- 1 | import cn.xnatural.app.AppContext; 2 | import cn.xnatural.app.Inject; 3 | import cn.xnatural.app.ServerTpl; 4 | import cn.xnatural.app.Utils; 5 | import cn.xnatural.app.util.DB; 6 | import cn.xnatural.enet.event.EL; 7 | import cn.xnatural.http.HttpServer; 8 | import cn.xnatural.remoter.Remoter; 9 | import cn.xnatural.sched.Sched; 10 | 11 | import java.time.Duration; 12 | import java.util.Random; 13 | 14 | public class AppTest { 15 | 16 | public static void main(String[] args) { 17 | new AppContext() 18 | .addSource( 19 | new ServerTpl("server1") { 20 | @Inject DB db; 21 | 22 | @EL(name = "sys.starting") 23 | void start() { log.info("{} start", name); } 24 | 25 | @EL(name = "sys.started", async = true) 26 | void started() { 27 | db.execute("create table IF NOT EXISTS test(id int auto_increment, name varchar(255));"); 28 | db.execute("insert into test(name) values('aa');"); 29 | log.info("测试sql: " + db.single("select count(1) from test", Integer.class)); 30 | } 31 | } 32 | , sched() 33 | , web() 34 | , db() 35 | , remoter() 36 | , 压测() 37 | ).start(); 38 | } 39 | 40 | 41 | /** 42 | * 创建数据库操作 43 | */ 44 | static ServerTpl db() { 45 | return new ServerTpl("db_local") { //数据库 46 | DB DB; 47 | @EL(name = "sys.starting", async = true) 48 | void start() { 49 | DB = new DB(getStr("url", null)); 50 | attrs().forEach((k, v) -> DB.dsAttr(k, v)); 51 | exposeBean(DB, "db_local"); 52 | ep.fire(name + ".started"); 53 | } 54 | 55 | @EL(name = "sys.stopping", async = true, order = 2f) 56 | void stop() { 57 | if (DB != null) { 58 | try { DB.close(); } catch (Exception e) { 59 | log.error("", e); 60 | } 61 | } 62 | } 63 | }; 64 | } 65 | 66 | 67 | /** 68 | * 创建 web服务 69 | */ 70 | static ServerTpl web() { 71 | return new ServerTpl("web") { //添加http服务 72 | HttpServer server; 73 | 74 | @EL(name = "sys.starting", async = true) 75 | void start() { 76 | server = new HttpServer(attrs(), exec()); 77 | server.buildChain(chain -> { 78 | chain.get("test", hCtx -> hCtx.render("xxxxxx")); 79 | }); 80 | server.start(); 81 | server.enabled = false; 82 | } 83 | 84 | @EL(name = "sys.started", async = true) 85 | void started() { 86 | for (Object ctrl : server.getCtrls()) exposeBean(ctrl); 87 | server.enabled = true; 88 | } 89 | 90 | @EL(name = "sys.stop") 91 | void stop() { if (server != null) server.close(); } 92 | }; 93 | } 94 | 95 | 96 | /** 97 | * 添加时间调度服务 98 | */ 99 | static ServerTpl sched() { 100 | return new ServerTpl("sched") { // 定时任务 101 | Sched sched; 102 | @EL(name = "sys.starting", async = true) 103 | void start() { 104 | sched = new Sched(attrs(), exec()).init(); 105 | exposeBean(sched); 106 | ep.fire(name + ".started"); 107 | } 108 | 109 | @EL(name = "sched.after") 110 | void after(Duration duration, Runnable fn) {sched.after(duration, fn);} 111 | 112 | @EL(name = "sys.stopping", async = true) 113 | void stop() { if (sched != null) sched.stop(); } 114 | }; 115 | } 116 | 117 | 118 | static ServerTpl remoter() { 119 | return new ServerTpl("remoter") { 120 | @EL(name = "sched.started") 121 | void start() { 122 | Remoter remoter = new Remoter(app().name(), app().id(), attrs(), exec(), ep, bean(Sched.class)); 123 | exposeBean(remoter); 124 | exposeBean(remoter.xioClient, "xioClient"); 125 | ep.fire(name + ".started"); 126 | } 127 | }; 128 | } 129 | 130 | 131 | static ServerTpl 压测() { 132 | return new ServerTpl("testExec") { 133 | @Inject Sched sched; 134 | 135 | @EL(name = "sys.started", async = true) 136 | void test() { 137 | // 新增任务的速度必须是任务执行的时长的step倍, 才会新增线程 138 | sched.fixedDelay(Duration.ofMillis(300), () -> { 139 | async(() -> { 140 | Utils.http().get("http://39.104.28.131:8080/test/timeout?wait=" + (800 + (new Random().nextInt(500)))).debug().execute(); 141 | log.info("===== " + exec()); 142 | }); 143 | }); 144 | } 145 | }; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/main/java/cn/xnatural/app/ServerTpl.java: -------------------------------------------------------------------------------- 1 | package cn.xnatural.app; 2 | 3 | import cn.xnatural.enet.event.EC; 4 | import cn.xnatural.enet.event.EL; 5 | import cn.xnatural.enet.event.EP; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | 9 | import java.util.HashMap; 10 | import java.util.Iterator; 11 | import java.util.Map; 12 | import java.util.concurrent.ExecutorService; 13 | import java.util.function.Consumer; 14 | 15 | /** 16 | * 服务模板类 17 | */ 18 | public class ServerTpl { 19 | protected final Logger log = LoggerFactory.getLogger(getClass().getName().contains("$") ? getClass().getSuperclass() : getClass()); 20 | /** 21 | * 服务名字标识.(保证唯一) 22 | * 可用于命名空间: 23 | * 1. 可用于属性配置前缀 24 | * 2. 可用于事件名字前缀 25 | */ 26 | protected final String name; 27 | /** 28 | * 1. 当此服务被加入核心时, 此值会自动设置为核心的EP. 29 | * 2. 如果要服务独立运行时, 请手动设置 30 | */ 31 | @Inject protected EP ep; 32 | private final Lazier _app = new Lazier<>(() -> bean(AppContext.class)); 33 | private final Lazier _exec = new Lazier<>(() -> bean(ExecutorService.class)); 34 | 35 | 36 | public ServerTpl(String name) { 37 | if (name == null || name.isEmpty()) throw new IllegalArgumentException("Param name required"); 38 | this.name = name; 39 | } 40 | public ServerTpl() { 41 | String n = getClass().getName().contains("$") ? getClass().getName() : getClass().getSimpleName(); 42 | this.name = Character.toLowerCase(n.charAt(0)) + (n.length() > 1 ? n.substring(1) : ""); 43 | } 44 | 45 | /** 46 | * bean 容器. {@link #localBean} 47 | */ 48 | protected Map beanCtx; 49 | @EL(name = {"bean.get", "{name}.bean.get"}) 50 | protected T localBean(EC ec, Class bType, String bName) { 51 | if (beanCtx == null) return null; 52 | 53 | Object bean = null; 54 | if (bName != null && bType != null) { 55 | bean = beanCtx.get(bName); 56 | if (bean != null && !bType.isAssignableFrom(bean.getClass())) bean = null; 57 | } else if (bName != null && bType == null) { 58 | bean = beanCtx.get(bName); 59 | } else if (bName == null && bType != null) { 60 | if (bType.isAssignableFrom(getClass())) bean = this; 61 | else { 62 | for (Iterator> it = beanCtx.entrySet().iterator(); it.hasNext(); ) { 63 | Map.Entry e = it.next(); 64 | if (bType.isAssignableFrom(e.getValue().getClass())) { 65 | bean = e.getValue(); break; 66 | } 67 | } 68 | } 69 | } 70 | return (T) bean; 71 | } 72 | 73 | 74 | /** 75 | * 本地查找 对象 76 | * @param bType 对象类型 77 | * @param bName 对象名字 78 | * @return bean 79 | */ 80 | protected T localBean(Class bType, String bName) { return localBean(null, bType, bName); } 81 | 82 | /** 83 | * 本地查找 对象 84 | * @param bType 对象类型 85 | * @return bean 86 | */ 87 | protected T localBean(Class bType) { return localBean(null, bType, null); } 88 | 89 | 90 | /** 91 | * 全局查找 bean 92 | * @param type bean类型 93 | * @param name bean名字 94 | * @return bean 95 | */ 96 | protected T bean(Class type, String name) { 97 | return ep == null ? null : (T) ep.fire(new EC("bean.get", this).sync().args(type, name)); 98 | } 99 | 100 | /** 101 | * 全局查找 bean 102 | * @param type bean类型 103 | * @return bean 104 | */ 105 | protected T bean(Class type) { return bean(type, null); } 106 | 107 | 108 | /** 109 | * 异步执行. 拦截异常 110 | * @param fn 异步执行的函数 111 | * @param exFn 错误处理函数 112 | */ 113 | public ServerTpl async(Runnable fn, Consumer exFn) { 114 | _exec.get().execute(() -> { 115 | try {fn.run();} catch (Throwable ex) { 116 | if (exFn != null) exFn.accept(ex); 117 | else log.error("", ex); 118 | } 119 | }); 120 | return this; 121 | } 122 | /** 123 | * 异步执行. 拦截异常 124 | * @param fn 异步执行的函数 125 | */ 126 | public ServerTpl async(Runnable fn) { return async(fn, null); } 127 | 128 | 129 | /** 130 | * 对列执行 131 | * @param qName 对列名, 默认当前server名称 132 | * @param fn 要执行的函数 133 | * @return {@link Devourer}当前对列 134 | */ 135 | public Devourer queue(String qName, Runnable fn) { return _app.get().queue(qName, fn); } 136 | /** 137 | * 获取执行对列 138 | * @param qName 对列名 139 | * @return {@link Devourer}当前对列 140 | */ 141 | public Devourer queue(String qName) { return _app.get().queue(qName, null); } 142 | /** 143 | * 对列执行, 加入到当前{@link ServerTpl#name}为对列名的对列 144 | * @param fn 要执行的函数 145 | * @return {@link Devourer}当前对列 146 | */ 147 | public Devourer queue(Runnable fn) { return _app.get().queue(name, fn); } 148 | 149 | 150 | /** 151 | * 暴露 bean 给其它模块用. {@link #localBean} 152 | * @param bean bean实例 153 | * @param names bean 名字 154 | */ 155 | protected ServerTpl exposeBean(Object bean, String...names) { 156 | if (bean == null) return this; 157 | if (beanCtx == null) beanCtx = new HashMap<>(7); 158 | if (names == null || names.length < 1) { 159 | String n = bean.getClass().getName().contains("$") ? bean.getClass().getName() : bean.getClass().getSimpleName(); 160 | names = new String[]{Character.toLowerCase(n.charAt(0)) + (n.length() > 1 ? n.substring(1) : "")}; 161 | } 162 | for (String n : names) { 163 | if (beanCtx.get(n) != null) log.warn("Override exist bean name '{}'", n); 164 | beanCtx.put(n, bean); 165 | } 166 | ep.addListenerSource(bean); _app.get().inject(bean); 167 | return this; 168 | } 169 | 170 | 171 | /** 172 | * 当前应用上下文 173 | * @return {@link AppContext} 174 | */ 175 | public AppContext app() { return _app.get(); } 176 | 177 | 178 | /** 179 | * 线程池 180 | * @return {@link ExecutorService} 181 | */ 182 | protected ExecutorService exec() { return _exec.get(); } 183 | 184 | 185 | /** 186 | * 服务名 187 | * @return 服务名 188 | */ 189 | public String getName() { return name; } 190 | 191 | 192 | /** 193 | * 当前服务的属性集 194 | * @return 属性集 195 | */ 196 | public Map attrs() { return app().attrs(name); } 197 | 198 | 199 | /** 200 | * 设置属性 201 | * @param aName 属性名 202 | * @param aValue 属性值 203 | * @return {@link ServerTpl} 204 | */ 205 | public ServerTpl setAttr(String aName, Object aValue) { 206 | app().env().put(name+ "." +aName, aValue); 207 | return this; 208 | } 209 | 210 | 211 | /** 212 | * 获取属性 213 | * @param key 属性key 214 | * @param type 值类型 215 | * @param defaultValue 默认值 216 | * @return 属性值 217 | */ 218 | public T getAttr(String key, Class type, T defaultValue) { 219 | Object obj = null; 220 | for (Map.Entry entry : app().env().entrySet()) { 221 | if ((name + "." + key).equals(entry.getKey())) { 222 | obj = entry.getValue(); break; 223 | } 224 | } 225 | T v = Utils.to(obj, type); 226 | if (v == null) return defaultValue; 227 | return v; 228 | } 229 | 230 | 231 | protected Long getLong(String key, Long defaultValue) { return getAttr(key, Long.class, defaultValue); } 232 | 233 | protected Integer getInteger(String key, Integer defaultValue) { return getAttr(key, Integer.class, defaultValue); } 234 | 235 | protected Double getDouble(String key, Double defaultValue) { return getAttr(key, Double.class, defaultValue); } 236 | 237 | protected Float getFloat(String key, Float defaultValue) { return getAttr(key, Float.class, defaultValue); } 238 | 239 | protected String getStr(String key, String defaultValue) { return getAttr(key, String.class, defaultValue); } 240 | 241 | protected Boolean getBoolean(String key, Boolean defaultValue) { return getAttr(key, Boolean.class, defaultValue); } 242 | } 243 | -------------------------------------------------------------------------------- /src/main/java/cn/xnatural/app/Devourer.java: -------------------------------------------------------------------------------- 1 | package cn.xnatural.app; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import java.time.Duration; 7 | import java.util.Arrays; 8 | import java.util.Deque; 9 | import java.util.concurrent.*; 10 | import java.util.concurrent.atomic.AtomicInteger; 11 | import java.util.function.BiConsumer; 12 | import java.util.function.Predicate; 13 | 14 | /** 15 | * 对列执行器 16 | * 按顺序执行添加的Runnable 17 | * 当需要按顺序控制任务 一个一个, 两个两个... 的执行任务时 18 | */ 19 | public class Devourer { 20 | protected static final Logger log = LoggerFactory.getLogger(Devourer.class); 21 | /** 22 | * 线程池 23 | */ 24 | protected final Executor exec; 25 | /** 26 | * 队列标识 27 | */ 28 | protected final String key; 29 | /** 30 | * 对列中失败的最大保留个数, 31 | * 如果 大于0 执行失败, 暂时保留, 直至 排对的个数大于此值 32 | * 否则 失败丢弃 33 | * 默认: 执行失败,丢弃 34 | */ 35 | protected Integer failMaxKeep = 0; 36 | /** 37 | * 流量限制锁 38 | */ 39 | protected final LatchLock lock = new LatchLock(); 40 | /** 41 | * 任务执行对列 42 | */ 43 | protected final Deque waiting = new ConcurrentLinkedDeque<>(); 44 | /** 45 | * 错误处理函数 46 | */ 47 | protected BiConsumer errorHandler; 48 | /** 49 | * 暂停执行条件 50 | */ 51 | protected Predicate pauseCondition; 52 | /** 53 | * 使用最后入队想任务/任务最后有效 {@link #useLast(boolean)} 54 | */ 55 | protected boolean useLast; 56 | /** 57 | * 速度限制 58 | * 平均每个占用执行时间 59 | */ 60 | protected Long perSpend; 61 | 62 | 63 | /** 64 | * 创建对列 65 | * @param key 对列标识 66 | * @param exec 线程池 67 | */ 68 | public Devourer(String key, Executor exec) { 69 | this.key = (key == null || key.isEmpty()) ? Devourer.class.getSimpleName() + "@" + Integer.toHexString(hashCode()) : key; 70 | this.exec = exec == null ? new ThreadPoolExecutor(2, 4, 6L, TimeUnit.HOURS, 71 | new LinkedBlockingQueue(100000) { 72 | boolean threshold() { // 让线程池创建(除核心线程外)新的线程的临界条件 73 | int size = super.size(); 74 | // 这个 size > 1: 有时 添加任务后 size==1, 但还没被poll, 但线程还有空闲,就是取size的值有时稍快于线程poll任务 75 | if (size <= 1) return false; 76 | ThreadPoolExecutor e = ((ThreadPoolExecutor) Devourer.this.exec); 77 | if (lock.limit <= e.getCorePoolSize()) return false; 78 | int ps = e.getPoolSize(); 79 | // 不超过最大线程数 80 | if (ps >= e.getMaximumPoolSize()) return false; 81 | // 所有线程都在忙并且有超过一定比例的当前忙的线程的任务在等待, 非关闭状态 82 | return size >= (int) (ps * 0.5); 83 | } 84 | @Override 85 | public boolean offer(Runnable r) { return !threshold() && super.offer(r); } 86 | }, 87 | new ThreadFactory() { 88 | final AtomicInteger i = new AtomicInteger(); 89 | @Override 90 | public Thread newThread(Runnable r) { return new Thread(r, Devourer.this.key + "-" + i.incrementAndGet()); } 91 | }) : exec; 92 | } 93 | 94 | /** 95 | * 创建对列 96 | * @param key 对列标识 97 | */ 98 | public Devourer(String key) { this(key, null); } 99 | 100 | public Devourer() { this(null, null); } 101 | 102 | 103 | /** 104 | * 任务入对列 105 | * @param fn 任务函数 106 | * @return {@link Devourer} 107 | */ 108 | public Devourer offer(Runnable fn) { 109 | if (fn == null) return this; 110 | if (useLast) waiting.clear(); 111 | waiting.offer(fn); 112 | trigger(); 113 | return this; 114 | } 115 | 116 | 117 | /** 118 | * 不断的从 {@link #waiting} 对列中取出执行 119 | */ 120 | protected void trigger() { 121 | Predicate condition = this.pauseCondition; // 重新弄个本地变量是为了防止多线程下边可能为空的情况 122 | if (condition != null) { // 是否设置了暂停 123 | if (condition.test(this)) return; 124 | else this.pauseCondition = null; 125 | } 126 | if (waiting.isEmpty()) return; 127 | // 1.必须保证正在执行的函数不超过 parallelLimit 128 | // 2.必须保证这里waiting对列中不为空 129 | // 3.必须保证不能出现情况: waiting 对列中有值, 但没有被执行 130 | if (!lock.tryLock()) return; 131 | exec.execute(() -> { 132 | final long start = perSpend == null ? 0 : System.currentTimeMillis(); // 执行开始时间, 用于计算执行花费时长 133 | Runnable task = null; 134 | try { 135 | task = waiting.poll(); 136 | if (task != null) task.run(); // 5个并发添加5个任务, 锁限制3, 第一次执行完3个任务, 第2次再同时获取3个锁, 但任务只有2个, 所以有可能poll出来为空 137 | } catch (Throwable ex) { 138 | // 不用担心顺序, 因为如果并发为1, 一定是顺序的; 如果并发大于1, 执行顺序就不一定顺序了 139 | if (task != null && failMaxKeep != null && failMaxKeep > 0 && (getWaitingCount() < failMaxKeep)) waiting.addFirst(task); 140 | if (errorHandler != null) { 141 | try { 142 | errorHandler.accept(ex, this); 143 | } catch (Throwable exx) { 144 | log.error(key + " errorHandler error", exx); 145 | } 146 | } else { 147 | log.error(key, ex); 148 | } 149 | } finally { 150 | // 速度限制 151 | if (perSpend != null && perSpend > 0) { 152 | // 剩余时长 = 应该花费时长 - 已花费时长 153 | long left = perSpend - (System.currentTimeMillis() - start); 154 | if (left > 1) { 155 | try { 156 | Thread.sleep(left - 1); 157 | } catch (InterruptedException e) { 158 | log.error(key + " speed sleep error", e); 159 | } 160 | } 161 | } 162 | lock.release(); 163 | if (!waiting.isEmpty()) trigger(); // 持续不断执行对列中的任务 164 | } 165 | }); 166 | } 167 | 168 | 169 | /** 170 | * 设置并发数 171 | * @param parallel >=1 172 | * @return {@link Devourer} 173 | */ 174 | public Devourer parallel(int parallel) { 175 | if (parallel < 1) throw new IllegalArgumentException("Param parallel >= 1"); 176 | lock.limit(parallel); 177 | return this; 178 | } 179 | 180 | 181 | /** 182 | * 速度限制 183 | * 线程会按一定频率sleep 184 | * @param speed /s, /m, /h, /d; null: 不阻速 185 | * @return {@link Devourer} 186 | */ 187 | public Devourer speed(String speed) { 188 | if (speed == null) { 189 | perSpend = null; 190 | return this; 191 | } 192 | String[] arr = speed.split("/"); 193 | // 速度大小 194 | final int limit = Integer.valueOf(arr[0].trim()); 195 | if (limit < 1) throw new IllegalArgumentException("speed must > 0"); 196 | // 速度单位 197 | final String unit = arr[1].trim().toLowerCase(); 198 | if (!Arrays.asList("s", "m", "h", "d").contains(unit)) { 199 | throw new IllegalArgumentException("speed format 10/s, 10/m, 10/h, 10/d"); 200 | } 201 | // 单位时间长 202 | long unitDuration = 0; 203 | if ("s".equals(unit)) unitDuration = 1000; 204 | else if ("m".equals(unit)) unitDuration = 1000 * 60; 205 | else if ("h".equals(unit)) unitDuration = 1000 * 60 * 60; 206 | else if ("d".equals(unit)) unitDuration = 1000 * 60 * 60 * 24; 207 | perSpend = unitDuration / limit; 208 | return this; 209 | } 210 | 211 | 212 | /** 213 | * 排对个数 214 | * @return 排对个数 215 | */ 216 | public int getWaitingCount() { return waiting.size(); } 217 | 218 | 219 | /** 220 | * 错误处理 221 | * @param handler 错误处理器 222 | * @return {@link Devourer} 223 | */ 224 | public Devourer errorHandle(BiConsumer handler) { 225 | this.errorHandler = handler; 226 | return this; 227 | } 228 | 229 | 230 | /** 231 | * 执行失败时, 保留最大个数 232 | * NOTE: 失败的任务会不断的重试执行, 直到成功或者对列中的个数大于此值被删除 233 | * 典型应用: 数据上报场景 234 | * @return {@link Devourer} 235 | */ 236 | public Devourer failMaxKeep(Integer maxKeep) { this.failMaxKeep = maxKeep; return this; } 237 | 238 | 239 | /** 240 | * 暂停一段时间 241 | * NOTE 继续执行条件: 必须有新的任务入对, 或者手动调用 {@link #resume()} 242 | * @param duration 一段时间 243 | * @return {@link Devourer} 244 | */ 245 | public Devourer suspend(Duration duration) { 246 | pauseCondition = new Predicate() { 247 | final Pause pause = new Pause(duration); 248 | @Override 249 | public boolean test(Devourer devourer) { return !pause.isTimeout(); } 250 | }; 251 | return this; 252 | } 253 | 254 | 255 | /** 256 | * 设置暂停条件 257 | * 使用 {@link #resume()} 恢复 258 | * @param pauseCondition 暂停条件 259 | * @return {@link Devourer} 260 | */ 261 | public Devourer suspend(Predicate pauseCondition) { 262 | this.pauseCondition = pauseCondition; 263 | return this; 264 | } 265 | 266 | 267 | /** 268 | * 手动恢复执行 269 | * @return {@link Devourer} 270 | */ 271 | public Devourer resume() { pauseCondition = null; trigger(); return this; } 272 | 273 | 274 | /** 275 | * 是否只使用队列最后一个, 清除队列前面的任务 276 | * 适合: 入队的频率比出队高, 前面的任务可有可无 277 | */ 278 | public Devourer useLast(boolean useLast) { this.useLast = useLast; return this; } 279 | 280 | 281 | /** 282 | * 是否使用了 userLast 特性 283 | */ 284 | public boolean isUseLast() { return useLast; } 285 | 286 | 287 | /** 288 | * 是否是暂停状态 289 | * @return true: 暂停中 290 | */ 291 | public boolean isSuspended() { 292 | try { 293 | if (pauseCondition != null && pauseCondition.test(this)) return true; 294 | } catch (NullPointerException npt) { /** trigger方法并发有可能把pause置null **/ } 295 | return false; 296 | } 297 | 298 | 299 | /** 300 | * 正在执行的任务数 301 | */ 302 | public int getParallel() { return lock.getLatchSize(); } 303 | 304 | 305 | /** 306 | * 关闭 307 | */ 308 | public void shutdown() { 309 | waiting.clear(); 310 | if (exec instanceof ExecutorService) ((ExecutorService) exec).shutdown(); 311 | } 312 | 313 | 314 | @Override 315 | public String toString() { 316 | return key + "{parallel: " + getParallel() + ", waitingCount: " + getWaitingCount() + ", suspended: "+ isSuspended() + ", useLast: " + isUseLast() +"}"; 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /src/main/java/cn/xnatural/app/Utils.java: -------------------------------------------------------------------------------- 1 | package cn.xnatural.app; 2 | 3 | import cn.xnatural.app.util.Copier; 4 | import cn.xnatural.app.util.Httper; 5 | import cn.xnatural.app.util.Tailer; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | 9 | import java.io.*; 10 | import java.lang.management.ManagementFactory; 11 | import java.lang.reflect.Field; 12 | import java.lang.reflect.Method; 13 | import java.math.BigDecimal; 14 | import java.math.BigInteger; 15 | import java.net.*; 16 | import java.security.MessageDigest; 17 | import java.security.NoSuchAlgorithmException; 18 | import java.security.SecureRandom; 19 | import java.util.*; 20 | import java.util.function.Consumer; 21 | import java.util.stream.Collectors; 22 | 23 | /** 24 | * 常用工具集 25 | */ 26 | public class Utils { 27 | protected static final Logger log = LoggerFactory.getLogger(Utils.class); 28 | /** 29 | * 得到jvm进程号 30 | * @return pid 31 | */ 32 | public static String pid() { return ManagementFactory.getRuntimeMXBean().getName().split("@")[0]; } 33 | 34 | 35 | /** 36 | * 判断系统是否为 linux 系统 37 | * 判断方法来源 io.netty.channel.epoll.Native#loadNativeLibrary() 38 | * @return true: linux 39 | */ 40 | public static boolean isLinux() { 41 | return System.getProperty("os.name").toLowerCase(Locale.UK).trim().startsWith("linux"); 42 | } 43 | 44 | 45 | /** 46 | * 项目根目录下边找目录或文件 47 | * @param child 项目根目录下的子目录/文件 48 | */ 49 | public static File baseDir(String child) { 50 | File p = new File(System.getProperty("user.dir")); 51 | if (child != null) {return new File(p, child);} 52 | return p; 53 | } 54 | 55 | 56 | /** 57 | * 本机ipv4地址 58 | */ 59 | public static String ipv4() { 60 | try { 61 | for (Enumeration en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements(); ) { 62 | NetworkInterface current = en.nextElement(); 63 | if (!current.isUp() || current.isLoopback() || current.isVirtual()) continue; 64 | Enumeration addresses = current.getInetAddresses(); 65 | while (addresses.hasMoreElements()) { 66 | InetAddress addr = addresses.nextElement(); 67 | if (addr.isLoopbackAddress()) continue; 68 | if (addr instanceof Inet4Address) { 69 | return addr.getHostAddress(); 70 | } 71 | } 72 | } 73 | } catch (SocketException e) { 74 | throw new RuntimeException(e); 75 | } 76 | return null; 77 | } 78 | 79 | 80 | /** 81 | * sha1 加密 82 | * @param bs 被加密码byte[] 83 | * @return 加密后 byte[] 84 | */ 85 | public static byte[] sha1(byte[] bs) { 86 | try { 87 | MessageDigest digest = MessageDigest.getInstance("SHA-1"); 88 | digest.update(bs); 89 | return digest.digest(); 90 | } catch (NoSuchAlgorithmException e) { 91 | throw new RuntimeException(e); 92 | } 93 | } 94 | 95 | 96 | /** 97 | * md5 hex 98 | * @param bs 被加密的byte[] 99 | * @return md5加密后的字符串 100 | */ 101 | public static String md5Hex(byte[] bs) { 102 | try { 103 | byte[] secretBytes = MessageDigest.getInstance("md5").digest(bs); 104 | String md5code = new BigInteger(1, secretBytes).toString(16); 105 | for (int i = 0; i < 32 - md5code.length(); i++) { 106 | md5code = "0" + md5code; 107 | } 108 | return md5code; 109 | } catch (NoSuchAlgorithmException e) { 110 | throw new RuntimeException(e); 111 | } 112 | } 113 | 114 | 115 | public static final char[] CS = new char[]{'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'}; 116 | public static final SecureRandom SR = new SecureRandom(); 117 | 118 | public static String nanoId() { return nanoId(21); } 119 | 120 | /** 121 | * nano id 生成 122 | * @param length 生成的长度 123 | */ 124 | public static String nanoId(int length) { 125 | if (length < 1) throw new IllegalArgumentException("Param length must >= 1"); 126 | final int mask = (2 << (int)Math.floor(Math.log(CS.length - 1) / Math.log(2))) - 1; 127 | final int step = (int)Math.ceil(1.6 * mask * length / CS.length); 128 | final StringBuilder sb = new StringBuilder(); 129 | while(true) { 130 | byte[] bytes = new byte[step]; 131 | SR.nextBytes(bytes); 132 | for(int i = 0; i < step; ++i) { 133 | int alphabetIndex = bytes[i] & mask; 134 | if (alphabetIndex < CS.length) { 135 | sb.append(CS[alphabetIndex]); 136 | if (sb.length() == length) { 137 | return sb.toString(); 138 | } 139 | } 140 | } 141 | } 142 | } 143 | 144 | 145 | /** 146 | * 类型转换 147 | * @param v 值 148 | * @param type 转换的类型 149 | * @return 转换后的结果 150 | */ 151 | public static T to(Object v, Class type) { 152 | if (type == null) return (T) v; 153 | if (v == null) { 154 | if (boolean.class.equals(type)) return (T) Boolean.FALSE; 155 | if (short.class.equals(type)) return (T) Short.valueOf("0"); 156 | if (byte.class.equals(type)) return (T) Byte.valueOf("0"); 157 | if (int.class.equals(type)) return (T) Integer.valueOf(0); 158 | if (long.class.equals(type)) return (T) Long.valueOf(0); 159 | if (double.class.equals(type)) return (T) Double.valueOf(0); 160 | if (float.class.equals(type)) return (T) Float.valueOf(0); 161 | if (char.class.equals(type)) return (T) Character.valueOf('\u0000'); 162 | return null; 163 | } 164 | else if (type.isAssignableFrom(v.getClass())) return (T) v; 165 | else if (String.class.equals(type)) return (T) v.toString(); 166 | else if (Boolean.class.equals(type) || boolean.class.equals(type)) return (T) Boolean.valueOf(v.toString()); 167 | else if (Short.class.equals(type) || short.class.equals(type)) return (T) Short.valueOf(v.toString()); 168 | else if (Byte.class.equals(type) || byte.class.equals(type)) return (T) Byte.valueOf(v.toString()); 169 | else if (Integer.class.equals(type) || int.class.equals(type)) return (T) Integer.valueOf(v.toString()); 170 | else if (BigInteger.class.equals(type)) return (T) new BigInteger(v.toString()); 171 | else if (Long.class.equals(type) || long.class.equals(type)) return (T) Long.valueOf(v.toString()); 172 | else if (Double.class.equals(type) || double.class.equals(type)) return (T) Double.valueOf(v.toString()); 173 | else if (Float.class.equals(type) || float.class.equals(type)) return (T) Float.valueOf(v.toString()); 174 | else if (BigDecimal.class.equals(type)) return (T) new BigDecimal(v.toString()); 175 | else if (URI.class.equals(type)) return (T) URI.create(v.toString()); 176 | else if (type.isEnum()) return Arrays.stream(type.getEnumConstants()).filter((o) -> v.equals(((Enum) o).name())).findFirst().orElse(null); 177 | return (T) v; 178 | } 179 | 180 | 181 | /** 182 | * 遍历所有方法并处理 183 | * @param clz Class 184 | * @param fn 函数 185 | */ 186 | public static void iterateMethod(final Class clz, Consumer fn) { 187 | if (fn == null) return; 188 | Class c = clz; 189 | do { 190 | for (Method m : c.getDeclaredMethods()) fn.accept(m); 191 | c = c.getSuperclass(); 192 | } while (c != null); 193 | } 194 | 195 | 196 | /** 197 | * 查找字段 198 | * @param clz Class 199 | * @param fn 函数 200 | */ 201 | public static void iterateField(final Class clz, Consumer fn) { 202 | if (fn == null) return; 203 | Class c = clz; 204 | do { 205 | for (Field f : c.getDeclaredFields()) fn.accept(f); 206 | c = c.getSuperclass(); 207 | } while (c != null); 208 | } 209 | 210 | 211 | /** 212 | * 构建一个 http 请求, 支持 get, post. 文件上传. 213 | * @return {@link Httper} 214 | */ 215 | public static Httper http() { return new Httper(); } 216 | 217 | 218 | /** 219 | * 把查询参数添加到 url 后边 220 | * @param urlStr url 221 | * @param params 参数 222 | * @return 完整url 223 | */ 224 | public static String buildUrl(String urlStr, Map params) { 225 | if (params == null || params.isEmpty()) return urlStr; 226 | String queryStr = params.entrySet().stream() 227 | .map(e -> { 228 | try { 229 | return e.getKey() + "=" + URLEncoder.encode(e.getValue().toString(), "utf-8"); 230 | } catch (UnsupportedEncodingException ex) { 231 | throw new RuntimeException(ex); 232 | } 233 | }) 234 | .collect(Collectors.joining("&")); 235 | if (urlStr.endsWith("?") || urlStr.endsWith("&")) urlStr += queryStr; 236 | else if (urlStr.contains("?")) urlStr += "&" + queryStr; 237 | else urlStr += "?" + queryStr; 238 | return urlStr; 239 | } 240 | 241 | 242 | /** 243 | * 文件内容监控器(类linux tail) 244 | * @return {@link Tailer} 245 | */ 246 | public static Tailer tailer() { return new Tailer(); } 247 | 248 | 249 | /** 250 | * 对象 复制器 251 | * @param src 源对象 252 | * @param target 目标对象 253 | * @param 源对象类型 254 | * @param 目标对象类型 255 | * @return {@link Copier} 256 | */ 257 | public static Copier copier(S src, T target) { return new Copier(src, target); } 258 | 259 | 260 | /** 261 | * io 流 copy 262 | * 263 | * try (InputStream is = new FileInputStream("d:/tmp/1.txt"); OutputStream os = new FileOutputStream("d:/tmp/2.txt")) { 264 | * Utils.ioCopy(is, os); 265 | * } 266 | * 267 | * @param is 输入流 268 | * @param os 输出流 269 | * @param bufSize 每次读取字节大小 270 | * @return 总复制大小 271 | * @throws IOException {@link OutputStream#write(byte[], int, int)} 272 | */ 273 | public static long ioCopy(InputStream is, OutputStream os, Integer bufSize) throws IOException { 274 | byte[] buf = new byte[bufSize == null || bufSize < 1 ? 1024 : bufSize]; 275 | long count = 0; 276 | int n = 0; 277 | while (-1 != (n = is.read(buf))) { 278 | os.write(buf, 0, n); 279 | count += n; 280 | } 281 | return count; 282 | } 283 | 284 | /** 285 | * io 流 copy 286 | * @param is 输入流 287 | * @param os 输出流 288 | * @return 总复制大小 289 | * @throws IOException {@link OutputStream#write(byte[], int, int)} 290 | */ 291 | public static long ioCopy(InputStream is, OutputStream os) throws IOException {return ioCopy(is, os, 4096);} 292 | } 293 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | cn.xnatural 8 | tiny 9 | jar 10 | 1.1.9 11 | 12 | tiny 13 | 小巧的java应用微内核并发框架 14 | https://gitee.com/xnat/tiny 15 | 16 | 17 | 18 | cn.xnatural 19 | enet 20 | 1.1.0 21 | 22 | 23 | ch.qos.logback 24 | logback-classic 25 | 1.2.11 26 | 27 | 28 | 29 | 30 | com.alibaba 31 | druid 32 | 1.2.11 33 | test 34 | 35 | 36 | org.junit.jupiter 37 | junit-jupiter-api 38 | 5.9.0 39 | test 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | cn.xnatural 49 | http 50 | 1.1.5 51 | test 52 | 53 | 54 | cn.xnatural 55 | sched 56 | 1.0.3 57 | test 58 | 59 | 60 | cn.xnatural 61 | remoter 62 | 1.1.1 63 | test 64 | 65 | 66 | mysql 67 | mysql-connector-java 68 | 8.0.29 69 | test 70 | 71 | 72 | com.h2database 73 | h2 74 | 2.1.212 75 | test 76 | 77 | 78 | 79 | 80 | 81 | The Apache Software License, Version 2.0 82 | http://www.apache.org/licenses/LICENSE-2.0.txt 83 | repo 84 | 85 | 86 | 87 | 88 | https://gitee.com/xnat/tiny 89 | https://gitee.com/xnat/tiny.git 90 | https://gitee.com/xnat/tiny 91 | 92 | 93 | 94 | 95 | hubert 96 | dlr 97 | xnatural@msn.cn 98 | xnatural 99 | 100 | manager 101 | 102 | 103 | 104 | 105 | 106 | hubert 107 | 108 | 109 | 110 | 111 | 112 | UTF-8 113 | UTF-8 114 | 1.8 115 | 1.8 116 | 117 | 118 | 119 | 120 | aliyun 121 | https://maven.aliyun.com/repository/public/ 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | org.apache.maven.plugins 180 | maven-source-plugin 181 | 3.0.1 182 | 183 | 184 | attach-sources 185 | 186 | jar 187 | 188 | 189 | 190 | 191 | 192 | org.apache.maven.plugins 193 | maven-scm-publish-plugin 194 | 3.0.0 195 | 196 | 197 | javadocs 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | release 207 | 208 | 209 | 210 | 211 | org.apache.maven.plugins 212 | maven-source-plugin 213 | 3.0.1 214 | 215 | 216 | package 217 | 218 | jar-no-fork 219 | 220 | 221 | 222 | 223 | 224 | 225 | org.apache.maven.plugins 226 | maven-javadoc-plugin 227 | 3.1.0 228 | 229 | 230 | package 231 | 232 | jar 233 | 234 | 235 | 236 | 237 | false 238 | none 239 | 240 | 241 | 242 | 243 | org.apache.maven.plugins 244 | maven-gpg-plugin 245 | 1.5 246 | 247 | 248 | verify 249 | 250 | sign 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | oss 260 | https://oss.sonatype.org/content/repositories/snapshots/ 261 | 262 | 263 | oss 264 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 265 | 266 | 267 | 268 | 269 | 270 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/main/java/cn/xnatural/app/util/Copier.java: -------------------------------------------------------------------------------- 1 | package cn.xnatural.app.util; 2 | 3 | import java.lang.reflect.Field; 4 | import java.lang.reflect.Method; 5 | import java.lang.reflect.Modifier; 6 | import java.util.*; 7 | import java.util.function.*; 8 | 9 | import static cn.xnatural.app.Utils.iterateField; 10 | import static cn.xnatural.app.Utils.iterateMethod; 11 | 12 | /** 13 | * 对象拷贝工具 14 | * 支持: 15 | * 对象 转 map 16 | * 对象 转 javabean 17 | * @param 源对象类型 18 | * @param 目标对象类型 19 | */ 20 | public class Copier { 21 | /** 22 | * 源对象 23 | */ 24 | protected final S src; 25 | /** 26 | * 目标对象 27 | */ 28 | protected final T target; 29 | /** 30 | * 忽略空属性 31 | * 默认不忽略空值属性 32 | */ 33 | protected boolean ignoreNull = false; 34 | /** 35 | * 客外属性源 计算器 36 | */ 37 | protected Map> valueGetter; 38 | /** 39 | * 属性值转换器配置 40 | * 源属性名 -> 值转换函数 41 | */ 42 | protected Map valueConverter; 43 | /** 44 | * 属性别名 45 | * 目标属性名 -> 源属性名 46 | */ 47 | protected Map mapProps; 48 | /** 49 | * 忽略属性 50 | */ 51 | protected final Set ignore = new HashSet<>(Arrays.asList("class")); 52 | 53 | 54 | public Copier(S src, T target) { 55 | this.src = src; 56 | this.target = target; 57 | } 58 | 59 | 60 | /** 61 | * 忽略属性 62 | * @param propNames 属性名 63 | * @return {@link Copier} 64 | */ 65 | public Copier ignore(String... propNames) { 66 | if (propNames == null) return this; 67 | for (String name : propNames) ignore.add(name); 68 | return this; 69 | } 70 | 71 | /** 72 | * 包含 class 属性 73 | * @return {@link Copier} 74 | */ 75 | public Copier showClassProp() { ignore.remove("class"); return this; } 76 | 77 | /** 78 | * 属性名 映射 79 | * {@link #ignore} 也能控制别名 80 | * @param srcPropName 源属性名 81 | * @param targetPropName 目标对象属性名 82 | * @return {@link Copier} 83 | */ 84 | public Copier mapProp(String srcPropName, String targetPropName) { 85 | if (targetPropName == null || targetPropName.isEmpty()) throw new IllegalArgumentException("Param targetPropName required"); 86 | if (srcPropName == null || srcPropName.isEmpty()) throw new IllegalArgumentException("Param srcPropName required"); 87 | if (mapProps == null) mapProps = new HashMap<>(7); 88 | mapProps.put(targetPropName, srcPropName); 89 | mapProps.put(srcPropName, targetPropName); 90 | return this; 91 | } 92 | 93 | /** 94 | * 忽略空值属性 95 | * @param ignoreNull 是否忽略 96 | * @return {@link Copier} 97 | */ 98 | public Copier ignoreNull(boolean ignoreNull) { this.ignoreNull = ignoreNull; return this; } 99 | 100 | /** 101 | * 手动添加额外属性源 102 | * NOTE: 会优先于 源对象中的属性 103 | * @param srcPropName 属性源名 104 | * @param valuer 属性值计算器 105 | * @return {@link Copier} 106 | */ 107 | public Copier add(String srcPropName, Supplier valuer) { 108 | if (valuer == null) throw new IllegalArgumentException("Param valuer required"); 109 | return add(srcPropName, (s, t) -> valuer.get()); 110 | } 111 | 112 | /** 113 | * 手动添加额外属性源 114 | * NOTE: 会优先于 源对象中的属性 115 | * @param srcPropName 属性源名 116 | * @param valuer 属性值计算器 117 | * @return {@link Copier} 118 | */ 119 | public Copier add(String srcPropName, BiFunction valuer) { 120 | if (srcPropName == null || srcPropName.isEmpty()) throw new IllegalArgumentException("Param srcPropName required"); 121 | if (valuer == null) throw new IllegalArgumentException("Param valuer required"); 122 | if (valueGetter == null) valueGetter = new HashMap<>(7); 123 | valueGetter.put(srcPropName, valuer); 124 | return this; 125 | } 126 | 127 | /** 128 | * 添加属性值转换器 129 | * @param srcPropName 源属性名 130 | * @param converter 值转换器 131 | * @return {@link Copier} 132 | */ 133 | public Copier addConverter(String srcPropName, Function converter) { 134 | if (srcPropName == null || srcPropName.isEmpty()) throw new IllegalArgumentException("Param srcPropName required"); 135 | if (converter == null) throw new IllegalArgumentException("Param converter required"); 136 | if (valueConverter == null) valueConverter = new HashMap<>(7); 137 | valueConverter.put(srcPropName, converter); 138 | return this; 139 | } 140 | 141 | 142 | /** 143 | * 对象属性之前的copy 144 | * @return 目标对象 145 | */ 146 | public T build() { 147 | if (target == null || src == null) return target; 148 | // map 转 map 149 | if (src instanceof Map && target instanceof Map) { 150 | // 属性赋值函数 151 | final Consumer setFn = srcPropName -> { 152 | if (srcPropName == null || ignore.contains(srcPropName)) return; 153 | try { 154 | String targetPropName = mapProps != null && mapProps.containsKey(srcPropName) ? mapProps.get(srcPropName) : srcPropName; 155 | if (ignore.contains(targetPropName)) return; 156 | Object v = get(srcPropName); 157 | if (ignoreNull && v == null) return; 158 | ((Map) target).put(targetPropName, v); 159 | } catch (Exception ex) { /** ignore **/ } 160 | }; 161 | // 遍历源属性集map 162 | for (Map.Entry entry : ((Map) src).entrySet()) { 163 | setFn.accept(entry.getKey().toString()); 164 | } 165 | // 遍历 额外属性集 166 | if (valueGetter != null) valueGetter.forEach((propName, getter) -> setFn.accept(propName)); 167 | } 168 | // javabean 转 map 169 | else if (target instanceof Map) javabeanToMap(); 170 | // 转 javabean 171 | else toJavabean(); 172 | return target; 173 | } 174 | 175 | 176 | protected void javabeanToMap() { 177 | final Set alreadyNames = new HashSet<>(); //防止 getter 和 字段重复 设值 178 | // 属性赋值函数 179 | final Consumer setFn = srcPropName -> { 180 | if (srcPropName == null || ignore.contains(srcPropName) || alreadyNames.contains(srcPropName)) return; 181 | alreadyNames.add(srcPropName); 182 | try { 183 | String targetPropName = mapProps != null && mapProps.containsKey(srcPropName) ? mapProps.get(srcPropName) : srcPropName; 184 | if (ignore.contains(targetPropName)) return; 185 | Object v = get(srcPropName); 186 | if (ignoreNull && v == null) return; 187 | ((Map) target).put(targetPropName, v); 188 | } catch (Exception ex) { /** ignore **/ } 189 | }; 190 | // 遍历 getter 191 | iterateMethod(src.getClass(), method -> setFn.accept(getGetterName(method))); 192 | // 遍历 public 字段 193 | iterateField(src.getClass(), field -> setFn.accept(getPropFieldName(field))); 194 | // 遍历 额外属性集 195 | if (valueGetter != null) valueGetter.forEach((propName, getter) -> setFn.accept(propName)); 196 | } 197 | 198 | 199 | protected void toJavabean() { 200 | final Set alreadyNames = new HashSet<>(); //防止 setter 和 字段重复 设值 201 | // 属性赋值函数 202 | final BiConsumer> setFn = (targetPropName, fn) -> { 203 | if (targetPropName == null || ignore.contains(targetPropName) || alreadyNames.contains(targetPropName)) return; 204 | alreadyNames.add(targetPropName); 205 | try { 206 | String srcPropName = mapProps != null && mapProps.containsKey(targetPropName) ? mapProps.get(targetPropName) : targetPropName; 207 | if (ignore.contains(srcPropName)) return; 208 | Object v = get(srcPropName); 209 | if (ignoreNull && v == null) return; 210 | fn.accept(v); 211 | } catch (Exception ex) { /** ignore **/ } 212 | }; 213 | // 遍历 setter 214 | iterateMethod(target.getClass(), method -> { 215 | setFn.accept(getSetterName(method), (v) -> { 216 | try { method.setAccessible(true); method.invoke(target, v); } catch (Exception ex) { /** ignore **/ } 217 | }); 218 | }); 219 | // 遍历 public 字段 220 | iterateField(target.getClass(), field -> { 221 | setFn.accept(getPropFieldName(field), (v) -> { 222 | try { field.setAccessible(true); field.set(target, v); } catch (Exception ex) { /** ignore **/ } 223 | }); 224 | }); 225 | } 226 | 227 | 228 | /** 229 | * 属性取值函数 230 | * @param srcPropName 源属性名 231 | * @return 属性值 232 | * @throws Exception 233 | */ 234 | protected Object get(String srcPropName) throws Exception { 235 | Object v = null; 236 | if (valueGetter != null && valueGetter.containsKey(srcPropName)) v = valueGetter.get(srcPropName).apply(src, target); 237 | else if (src instanceof Map) v = ((Map) src).get(srcPropName); 238 | else { 239 | Class c = src.getClass(); 240 | out: do { 241 | for (Method m : c.getDeclaredMethods()) { 242 | if (Objects.equals(getGetterName(m), srcPropName)) { 243 | m.setAccessible(true); 244 | v = m.invoke(src); 245 | break out; 246 | } 247 | } 248 | for (Field f : c.getDeclaredFields()) { 249 | if (Objects.equals(getPropFieldName(f), srcPropName)) { 250 | f.setAccessible(true); 251 | v = f.get(src); 252 | break out; 253 | } 254 | } 255 | c = c.getSuperclass(); 256 | } while (c != null); 257 | } 258 | if (valueConverter != null && valueConverter.containsKey(srcPropName)) { 259 | v = valueConverter.get(srcPropName).apply(v); 260 | } 261 | return v; 262 | } 263 | 264 | 265 | /** 266 | * 获取 setter 方法 属性名 267 | * @param method 方法 268 | * @return 属性名 269 | */ 270 | protected String getSetterName(Method method) { 271 | boolean isSetter = method.getName().startsWith("set") && method.getName().length() > 3 && method.getParameterCount() == 1 && 272 | Modifier.isPublic(method.getModifiers()) && !Modifier.isStatic(method.getModifiers()); 273 | if (isSetter) { 274 | String tmp = method.getName().replace("set", ""); 275 | if (tmp.length() == 1) return tmp.toUpperCase(); 276 | return Character.toLowerCase(tmp.charAt(0)) + tmp.substring(1); 277 | } 278 | return null; 279 | } 280 | 281 | 282 | /** 283 | * 获取 getter 方法 属性名 284 | * @param method 方法 285 | * @return 属性名 286 | */ 287 | protected String getGetterName(Method method) { 288 | boolean isGetter = method.getName().startsWith("get") && method.getName().length() > 3 && 289 | method.getParameterCount() == 0 && !void.class.equals(method.getReturnType()) && 290 | Modifier.isPublic(method.getModifiers()) && !Modifier.isStatic(method.getModifiers()); 291 | if (isGetter) { 292 | String tmp = method.getName().replace("get", ""); 293 | if (tmp.length() == 1) return tmp.toUpperCase(); 294 | return Character.toLowerCase(tmp.charAt(0)) + tmp.substring(1); 295 | } 296 | return null; 297 | } 298 | 299 | 300 | /** 301 | * 如果是个属性字段就返回 字段名 作属性名 302 | * @param field 字段 303 | * @return 属性名 304 | */ 305 | protected String getPropFieldName(Field field) { 306 | if (!Modifier.isPublic(field.getModifiers()) || Modifier.isStatic(field.getModifiers())) return null; 307 | return field.getName(); 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /src/main/java/cn/xnatural/app/CacheSrv.java: -------------------------------------------------------------------------------- 1 | package cn.xnatural.app; 2 | 3 | import cn.xnatural.enet.event.EL; 4 | 5 | import java.time.Duration; 6 | import java.util.Iterator; 7 | import java.util.LinkedList; 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.concurrent.ConcurrentHashMap; 11 | import java.util.concurrent.atomic.AtomicInteger; 12 | import java.util.concurrent.atomic.AtomicReference; 13 | import java.util.function.Consumer; 14 | import java.util.function.Function; 15 | 16 | /** 17 | * 简单内存缓存服务 18 | */ 19 | public class CacheSrv extends ServerTpl { 20 | // 触发清理多余数据的条件 21 | protected final Lazier _limit = new Lazier(() -> { 22 | queue(name).useLast(true); 23 | return getInteger("itemLimit", 1000); 24 | }); 25 | 26 | /** 27 | * 数据存放 28 | */ 29 | protected final Lazier> _data = new Lazier(() -> new ConcurrentHashMap( _limit.get() / 3)); 30 | /** 31 | * hash 数据缓存 32 | */ 33 | protected final Lazier>> _hashdata = new Lazier(() -> new ConcurrentHashMap( _limit.get() / 3)); 34 | 35 | 36 | public CacheSrv(String name) { super(name); } 37 | 38 | public CacheSrv() {} 39 | 40 | 41 | /** 42 | * 设置缓存 43 | * 取默认过期时间 30 分钟 44 | * @param key 缓存key 45 | * @param value 缓存值 46 | * @return 以前的值 47 | */ 48 | public Object set(String key, Object value) { 49 | return set(key, value, Duration.ofMinutes(getInteger("defaultExpire", getInteger("expire." + key, 30)))); 50 | } 51 | 52 | 53 | /** 54 | * 设置缓存 55 | * 取默认过期时间 30 分钟 56 | * @param key 缓存key 57 | * @param dataKey 数据key 58 | * @param value 缓存值 59 | * @return 以前的值 60 | */ 61 | public Object hset(String key, String dataKey, Object value) { 62 | return hset(key, dataKey, value, Duration.ofMinutes(getInteger("defaultExpire", getInteger("expire." + key, 30)))); 63 | } 64 | 65 | 66 | /** 67 | * 设置缓存 68 | * @param key 缓存key 69 | * @param value 缓存值 70 | * @param expire 过期时间 71 | * @return 以前的值 72 | */ 73 | @EL(name = "{name}.set") 74 | public Object set(String key, Object value, Duration expire) { 75 | return set(key, value, record -> expire == null ? Long.MAX_VALUE : expire.toMillis() + record.updateTime); 76 | } 77 | 78 | 79 | /** 80 | * 设置缓存 81 | * @param key 缓存key 82 | * @param dataKey 数据key 83 | * @param value 缓存值 84 | * @param expire 过期时间 85 | * @return 以前的值 86 | */ 87 | @EL(name = "{name}.hset") 88 | public Object hset(String key, String dataKey, Object value, Duration expire) { 89 | return hset(key, dataKey, value, record -> expire == null ? Long.MAX_VALUE : expire.toMillis() + record.updateTime); 90 | } 91 | 92 | 93 | /** 94 | * 设置缓存 95 | * @param key 缓存key 96 | * @param value 缓存值 97 | * @param expireFn 过期时间计算函数 98 | * 函数返回过期时间点(时间缀), 返回null(不过期,除非达到缓存限制被删除) 99 | * @return 以前的值 100 | */ 101 | @EL(name = "{name}.set2") 102 | public Object set(String key, Object value, Function expireFn) { 103 | log.trace("Set cache. key: {}, value: {}", key, value); 104 | Record old = _data.get().put(key, new Record(expireFn, value)); 105 | if (old != null) old.close(key); 106 | clean(); 107 | return old == null ? null : old.value; 108 | } 109 | 110 | 111 | /** 112 | * 设置缓存 113 | * @param key 缓存key 114 | * @param dataKey 数据key 115 | * @param value 缓存值 116 | * @param expireFn 过期时间计算函数 117 | * 函数返回过期时间点(时间缀), 返回null(不过期,除非达到缓存限制被删除) 118 | * @return 以前的值 119 | */ 120 | @EL(name = "{name}.hset2") 121 | public Object hset(String key, String dataKey, Object value, Function expireFn) { 122 | log.trace("HSet cache. key: {}, dataKey: {}, value: {}", key, dataKey, value); 123 | Map data = _hashdata.get().get(key); 124 | if (data == null) { 125 | synchronized (_hashdata) { 126 | data = _hashdata.get().get(key); 127 | if (data == null) { 128 | data = new ConcurrentHashMap<>(); 129 | _hashdata.get().put(key, data); 130 | } 131 | } 132 | } 133 | Record old = data.put(dataKey, new Record(expireFn, value)); 134 | if (old != null) old.close(dataKey); 135 | clean(); 136 | return old == null ? null : old.value; 137 | } 138 | 139 | 140 | /** 141 | * 移除缓存 142 | * @param key 缓存key 143 | * @return 缓存值 144 | */ 145 | @EL(name = "{name}.remove") 146 | public Object remove(String key) { 147 | Record record = _data.get().remove(key); 148 | if (record == null) return null; 149 | record.close(key); 150 | return record.value; 151 | } 152 | 153 | 154 | /** 155 | * 移除缓存 156 | * @param key 缓存key 157 | * @param dataKey 数据key 158 | * @return 缓存值 159 | */ 160 | @EL(name = "{name}.hremove") 161 | public Object hremove(String key, String dataKey) { 162 | Map data = _hashdata.get().get(key); 163 | if (data == null) return null; 164 | Record record = data.remove(dataKey); 165 | if (record == null) return null; 166 | record.close(dataKey); 167 | if (data.isEmpty()) { 168 | synchronized (_hashdata) { 169 | if (data.isEmpty()) { 170 | _hashdata.get().remove(key); 171 | } 172 | } 173 | } 174 | return record.value; 175 | } 176 | 177 | 178 | /** 179 | * 获取缓存值 180 | * @param key 缓存key 181 | * @return 缓存值 182 | */ 183 | @EL(name = "{name}.get") 184 | public Object get(String key) { 185 | Record record = _data.get().get(key); 186 | if (record == null) return null; 187 | if (record.valid()) return record.value; 188 | remove(key); 189 | return null; 190 | } 191 | 192 | 193 | /** 194 | * 获取缓存值 195 | * @param key 缓存key 196 | * @param dataKey 数据key 197 | * @return 缓存值 198 | */ 199 | @EL(name = "{name}.hget") 200 | public Object hget(String key, String dataKey) { 201 | Map data = _hashdata.get().get(key); 202 | if (data == null) return null; 203 | Record record = data.get(dataKey); 204 | if (record == null) return null; 205 | if (record.valid()) return record.value; 206 | hremove(key, dataKey); 207 | return null; 208 | } 209 | 210 | 211 | /** 212 | * 获取缓存值, 并更新缓存时间(即从现在开始重新计算过期时间) 213 | * @param key 缓存key 214 | * @return 缓存值 215 | */ 216 | @EL(name = "{name}.getAndUpdate") 217 | public Object getAndUpdate(String key) { 218 | Record record = _data.get().get(key); 219 | if (record == null) return null; 220 | if (record.valid()) { 221 | record.updateTime = System.currentTimeMillis(); 222 | return record.value; 223 | } 224 | remove(key); 225 | return null; 226 | } 227 | 228 | 229 | /** 230 | * 获取缓存值, 并更新缓存时间(即从现在开始重新计算过期时间) 231 | * @param key 缓存key 232 | * @param dataKey 数据key 233 | * @return 缓存值 234 | */ 235 | @EL(name = "{name}.hgetAndUpdate") 236 | public Object hgetAndUpdate(String key, String dataKey) { 237 | Map data = _hashdata.get().get(key); 238 | if (data == null) return null; 239 | Record record = data.get(dataKey); 240 | if (record == null) return null; 241 | if (record.valid()) { 242 | record.updateTime = System.currentTimeMillis(); 243 | return record.value; 244 | } 245 | hremove(key, dataKey); 246 | return null; 247 | } 248 | 249 | 250 | 251 | /** 252 | * 清理过期和多余的数据 253 | * 一次最多清一条 254 | * 1. 如果没有达到缓存限制, 则只清理过期数据 255 | * 2. 优先清理过期时间越近的 256 | * 3. 过期时间一样则先清更新时间最早的 257 | */ 258 | protected void clean() { 259 | // 是否只清理已过期的数据 260 | final boolean onlyCleanExpired = count() < _limit.get(); 261 | final long now = System.currentTimeMillis(); 262 | final Runnable clean = () -> { 263 | AtomicInteger cleanCnt = new AtomicInteger(); //清理的过期缓存条数统计 264 | AtomicReference> oldestData = new AtomicReference<>(); 265 | AtomicReference oldestKey = new AtomicReference<>(); 266 | AtomicReference oldestRecord = new AtomicReference<>(); 267 | final Consumer> doClean = data -> { 268 | long oldLeft = 0; 269 | for (Iterator> iter = data.entrySet().iterator(); iter.hasNext(); ) { //遍历选出最应该被移出的缓存记录 270 | Map.Entry entry = iter.next(); 271 | long left = entry.getValue().left(now); 272 | if (left < 1) { // 删除所有过期数据 273 | iter.remove(); 274 | entry.getValue().close(entry.getKey()); 275 | oldestData.set(null); 276 | cleanCnt.incrementAndGet(); 277 | if (!onlyCleanExpired) break; // 如果是同步删除, 为了减少遍历时间, 则只要有一个删除就退出遍历 278 | } 279 | if (onlyCleanExpired || cleanCnt.get() > 0) continue; 280 | 281 | if ( 282 | oldestRecord.get() == null || 283 | left < oldLeft || 284 | (oldLeft == left && oldestRecord.get().updateTime < entry.getValue().updateTime) 285 | ) { 286 | oldestData.set(data); 287 | oldestKey.set(entry.getKey()); 288 | oldestRecord.set(entry.getValue()); 289 | oldLeft = left; 290 | } 291 | } 292 | }; 293 | doClean.accept(_data.get()); 294 | for (Map data : _hashdata.get().values()) { 295 | if (cleanCnt.get() > 0 && !onlyCleanExpired) break; // 如果是同步删除, 为了减少遍历时间, 则只要有一个删除就退出遍历 296 | doClean.accept(data); 297 | } 298 | // 同步删除, 必须删除一个最老的缓存 299 | if (!onlyCleanExpired && cleanCnt.get() < 1 && oldestData.get() != null) { 300 | // 有可能性: 在删除的时候此条记录已被重新更新了 暂时先不管 301 | Record removed = oldestData.get().remove(oldestKey.get()); 302 | if (removed != null) removed.close(oldestKey.get()); 303 | } 304 | }; 305 | if (onlyCleanExpired) queue(clean); // 异步清除多余的缓存 306 | else clean.run(); // 同步清理: 避免异步排对太多而不能及时清理造成内存占用过多而溢出 307 | } 308 | 309 | 310 | /** 311 | * 当前缓存的个数统计 312 | */ 313 | @EL(name = "{name}.count") 314 | public int count() { return _data.get().size() + _hashdata.get().values().stream().mapToInt(Map::size).sum(); } 315 | 316 | 317 | @Override 318 | public String toString() { 319 | return "CacheSrv@" + Integer.toHexString(hashCode()) + "[size=" + count() + ", limit=" + _limit.get() + "]"; 320 | } 321 | 322 | 323 | /** 324 | * 缓存记录 325 | */ 326 | public class Record { 327 | // 过期时间点计算函数, 小于当前时间即过期, 返回null不过期 328 | protected final Function expireFn; 329 | // 更新时间 330 | protected long updateTime = System.currentTimeMillis(); 331 | // 缓存的对象 332 | public final Object value; 333 | 334 | protected Record(Function expireFn, Object value) { 335 | this.expireFn = expireFn; 336 | this.value = value; 337 | } 338 | 339 | /** 340 | * 是否有效 341 | */ 342 | protected boolean valid() { return left(System.currentTimeMillis()) > 0; } 343 | 344 | /** 345 | * 相对于某个时间, 还剩多长时间才失效 346 | */ 347 | protected long left(long timePoint) { 348 | Long expireTime = expireFn == null ? null : expireFn.apply(this); 349 | return expireTime == null ? Long.MAX_VALUE : expireTime - timePoint; 350 | } 351 | 352 | 353 | public long getUpdateTime() { return updateTime; } 354 | 355 | 356 | /** 357 | * 如果缓存值是 AutoCloseable,则在失效时 执行 close 358 | * @param key 缓存key 359 | */ 360 | protected void close(String key) { 361 | if (value instanceof AutoCloseable) { 362 | try { 363 | ((AutoCloseable) value).close(); 364 | } catch (Exception e) { 365 | log.error("Remove cache: " +key+ " close error", e); 366 | } 367 | } 368 | } 369 | } 370 | } 371 | -------------------------------------------------------------------------------- /src/main/java/cn/xnatural/app/util/Httper.java: -------------------------------------------------------------------------------- 1 | package cn.xnatural.app.util; 2 | 3 | import cn.xnatural.app.Utils; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | 7 | import javax.net.ssl.HttpsURLConnection; 8 | import javax.net.ssl.SSLContext; 9 | import javax.net.ssl.TrustManager; 10 | import javax.net.ssl.X509TrustManager; 11 | import java.io.*; 12 | import java.net.HttpURLConnection; 13 | import java.net.URL; 14 | import java.net.URLEncoder; 15 | import java.nio.charset.Charset; 16 | import java.security.SecureRandom; 17 | import java.security.cert.CertificateException; 18 | import java.security.cert.X509Certificate; 19 | import java.util.*; 20 | import java.util.function.Consumer; 21 | 22 | /** 23 | * http 请求 24 | */ 25 | public class Httper { 26 | protected static final Logger log = LoggerFactory.getLogger(Httper.class); 27 | protected String urlStr; 28 | protected String contentType; 29 | protected String method; 30 | protected String bodyStr; 31 | protected Map params; 32 | // 文件流:(属性名, 文件名, 文件内容) 33 | protected Map> fileStreams; 34 | protected Map cookies; 35 | protected Map headers; 36 | protected int connectTimeout = 5000; 37 | protected int readTimeout = 15000; 38 | protected int respCode; 39 | protected Consumer preFn; 40 | protected boolean debug; 41 | protected String tlsVersion = "TLSv1.2"; 42 | protected Charset charset = Charset.forName("utf-8"); 43 | 44 | public Httper get(String url) { this.urlStr = url; this.method = "GET"; return this; } 45 | public Httper post(String url) { this.urlStr = url; this.method = "POST"; return this; } 46 | public Httper put(String url) { this.urlStr = url; this.method = "PUT"; return this; } 47 | public Httper delete(String url) { this.urlStr = url; this.method = "DELETE"; return this; } 48 | /** 49 | * 设置 content-type 50 | * @param contentType application/json, multipart/form-data, application/x-www-form-urlencoded, text/plain 51 | */ 52 | public Httper contentType(String contentType) { this.contentType = contentType; return this; } 53 | public Httper jsonBody(String jsonStr) { this.bodyStr = jsonStr; if (contentType == null) contentType = "application/json"; return this; } 54 | public Httper textBody(String bodyStr) { this.bodyStr = bodyStr; if (contentType == null) contentType = "text/plain"; return this; } 55 | public Httper readTimeout(int timeout) { this.readTimeout = timeout; return this; } 56 | public Httper connectTimeout(int timeout) { this.connectTimeout = timeout; return this; } 57 | public Httper preConnect(Consumer preConnect) { this.preFn = preConnect; return this; } 58 | public Httper debug() { this.debug = true; return this; } 59 | public Httper charset(String charset) { this.charset = Charset.forName(charset); return this; } 60 | public Httper tlsVersion(String tlsVersion) { this.tlsVersion = tlsVersion; return this; } 61 | /** 62 | * 添加参数 63 | * @param name 参数名 64 | * @param value 支持 {@link File} 65 | */ 66 | public Httper param(String name, Object value) { 67 | if (params == null) params = new LinkedHashMap<>(); 68 | params.put(name, value); 69 | if (value instanceof File) { contentType = "multipart/form-data"; } 70 | return this; 71 | } 72 | 73 | /** 74 | * 添加文件流 75 | * @param pName 属性名 76 | * @param filename 文件名 77 | * @param fileStream 文件流 78 | */ 79 | public Httper fileStream(String pName, String filename, InputStream fileStream) { 80 | if (pName == null) throw new NullPointerException("pName == null"); 81 | if (fileStream == null) throw new NullPointerException("fileStream == null"); 82 | if (filename == null) throw new NullPointerException("filename == null"); 83 | if (fileStreams == null) fileStreams = new LinkedHashMap<>(); 84 | contentType = "multipart/form-data"; 85 | Map entry = fileStreams.computeIfAbsent(pName, s -> new HashMap<>(2)); 86 | entry.put("filename", filename); entry.put("fileStream", fileStream); 87 | return this; 88 | } 89 | public Httper header(String name, String value) { 90 | if (headers == null) headers = new LinkedHashMap<>(7); 91 | headers.put(name, value); 92 | return this; 93 | } 94 | public Httper cookie(String name, Object value) { 95 | if (cookies == null) cookies = new LinkedHashMap<>(7); 96 | cookies.put(name, value); 97 | return this; 98 | } 99 | public Map cookies() {return cookies;} 100 | public int getResponseCode() {return respCode;} 101 | 102 | /** 103 | * 执行 http 请求 104 | * @return http请求结果 105 | */ 106 | public String execute() { 107 | String ret = null; 108 | HttpURLConnection conn = null; 109 | boolean isMulti = false; // 是否为 multipart/form-data 提交 110 | Exception ex = null; 111 | try { 112 | URL url = null; 113 | if (urlStr == null || urlStr.isEmpty()) throw new IllegalArgumentException("url不能为空"); 114 | if ("GET".equals(method)) url = new URL(Utils.buildUrl(urlStr, params)); 115 | else url = new URL(urlStr); 116 | conn = (HttpURLConnection) url.openConnection(); 117 | if (conn instanceof HttpsURLConnection) { // 如果是https, 就忽略验证 118 | SSLContext sc = SSLContext.getInstance(tlsVersion); // "TLS" 119 | sc.init(null, new TrustManager[] {new X509TrustManager() { 120 | @Override 121 | public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { } 122 | @Override 123 | public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { } 124 | @Override 125 | public X509Certificate[] getAcceptedIssuers() { return null; } 126 | }}, new SecureRandom()); 127 | ((HttpsURLConnection) conn).setHostnameVerifier((s, sslSession) -> true); 128 | ((HttpsURLConnection) conn).setSSLSocketFactory(sc.getSocketFactory()); 129 | } 130 | conn.setRequestMethod(method); 131 | conn.setConnectTimeout(connectTimeout); 132 | conn.setReadTimeout(readTimeout); 133 | conn.setUseCaches(false); 134 | 135 | // header 设置 136 | conn.setRequestProperty("Accept", "*/*"); // 必加 137 | conn.setRequestProperty("Charset", charset.toString()); 138 | conn.setRequestProperty("Accept-Charset", charset.toString()); 139 | if (contentType != null) conn.setRequestProperty("Content-Type", contentType + ";charset=" + charset); 140 | // conn.setRequestProperty("Connection", "close") 141 | // conn.setRequestProperty("Connection", "keep-alive") 142 | // conn.setRequestProperty("http_user_agent", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.26 Safari/537.36 Core/1.63.6726.400 QQBrowser/10.2.2265.400") 143 | if (headers != null) { 144 | for (Map.Entry entry : headers.entrySet()) { 145 | conn.setRequestProperty(entry.getKey(), entry.getValue()); 146 | } 147 | } 148 | 149 | String boundary = null; 150 | if ("POST".equals(method)) { 151 | conn.setDoOutput(true); 152 | if ("multipart/form-data".equalsIgnoreCase(contentType)) { 153 | isMulti = true; 154 | boundary = "----CustomFormBoundary" + UUID.randomUUID().toString().replace("-", ""); 155 | contentType = "multipart/form-data;boundary=" + boundary; 156 | conn.setRequestProperty("Content-Type", contentType + ";charset=" + charset); 157 | } 158 | } 159 | 160 | // cookie 设置 161 | if (cookies != null && !cookies.isEmpty()) { 162 | StringBuilder sb = new StringBuilder(); 163 | for (Map.Entry entry : cookies.entrySet()) { 164 | if (entry.getValue() != null) { 165 | sb.append(entry.getKey()).append("=").append(entry.getValue()).append(";"); 166 | } 167 | } 168 | conn.setRequestProperty("Cookie", sb.toString()); 169 | } 170 | 171 | if (preFn != null) preFn.accept(conn); 172 | conn.connect(); // 连接 173 | 174 | if ("POST".equals(method)) { 175 | try (DataOutputStream os = new DataOutputStream(conn.getOutputStream())) { 176 | if (("application/json".equals(contentType) || "text/plain".equals(contentType)) && bodyStr != null) { 177 | os.write(bodyStr.getBytes(charset)); 178 | } else if (isMulti) { 179 | String end = "\r\n"; 180 | String twoHyphens = "--"; 181 | if (params != null) { 182 | for (Map.Entry entry : params.entrySet()) { 183 | os.writeBytes(twoHyphens + boundary + end); 184 | if (entry.getValue() instanceof File) { 185 | String s = "Content-Disposition: form-data; name=\"" +entry.getKey()+"\"; filename=\"" +((File) entry.getValue()).getName()+ "\"" + end; 186 | os.write(s.getBytes(charset)); // 这样写是为了避免中文文件名乱码 187 | os.writeBytes(end); 188 | try (FileInputStream is = new FileInputStream((File) entry.getValue())) { // copy 189 | byte[] bs = new byte[Math.min(is.available(), 4028)]; 190 | int n; 191 | while (-1 != (n = is.read(bs))) {os.write(bs, 0, n);} 192 | } 193 | } else { 194 | os.write(("Content-Disposition: form-data; name=\"" +entry.getKey()+"\"" + end).getBytes(charset)); 195 | os.writeBytes(end); 196 | os.write(entry.getValue() == null ? "".getBytes(charset) : entry.getValue().toString().getBytes(charset)); 197 | } 198 | os.writeBytes(end); 199 | } 200 | } 201 | if (fileStreams != null) { 202 | for (Map.Entry> entry : fileStreams.entrySet()) { 203 | os.writeBytes(twoHyphens + boundary + end); 204 | Map fileInfo = entry.getValue(); 205 | String s = "Content-Disposition: form-data; name=\"" +entry.getKey()+"\"; filename=\"" +fileInfo.get("filename")+ "\"" + end; 206 | os.write(s.getBytes(charset)); // 这样写是为了避免中文文件名乱码 207 | os.writeBytes(end); 208 | try (InputStream is = (InputStream) fileInfo.get("fileStream")) { // copy 209 | byte[] bs = new byte[Math.min(is.available(), 4028)]; 210 | int n; 211 | while (-1 != (n = is.read(bs))) {os.write(bs, 0, n);} 212 | } 213 | os.writeBytes(end); 214 | } 215 | } 216 | 217 | os.writeBytes(twoHyphens + boundary + twoHyphens + end); 218 | } else if ((params != null && !params.isEmpty())) { 219 | StringBuilder sb = new StringBuilder(); 220 | for (Map.Entry entry : params.entrySet()) { 221 | if (entry.getValue() != null) { 222 | sb.append(entry.getKey() + "=" + URLEncoder.encode(entry.getValue().toString(), charset.toString()) + "&"); 223 | } 224 | } 225 | os.write(sb.toString().getBytes(charset)); 226 | } 227 | } 228 | } 229 | // 支持 get 传body 230 | else if ("GET".equals(method) && bodyStr != null && !bodyStr.isEmpty()) { 231 | try (DataOutputStream os = new DataOutputStream(conn.getOutputStream())) { 232 | os.write(bodyStr.getBytes(charset)); 233 | } 234 | } 235 | // http 状态码 236 | respCode = conn.getResponseCode(); 237 | // 保存cookie 238 | if (conn.getHeaderFields() != null) { 239 | conn.getHeaderFields().entrySet().stream().filter(e -> "Set-Cookie".equalsIgnoreCase(e.getKey())) 240 | .map(Map.Entry::getValue) 241 | .findFirst() 242 | .ifPresent(cs -> { 243 | for (String c : cs) { 244 | String[] arr = c.split(";")[0].split("="); 245 | cookie(arr[0], arr[1]); 246 | } 247 | }); 248 | } 249 | 250 | // 取结果 251 | StringBuilder sb = new StringBuilder(); 252 | try (Reader reader = new InputStreamReader(conn.getInputStream(), charset)) { 253 | char[] buf = new char[1024]; 254 | int len = 0; 255 | while((len = reader.read(buf)) != -1) { 256 | sb.append(buf, 0, len); 257 | } 258 | } 259 | ret = sb.toString(); 260 | } 261 | catch (Exception e) { 262 | ex = e; 263 | } finally { 264 | if (conn != null) conn.disconnect(); 265 | } 266 | if (debug) { 267 | String logMsg = "Send http: ("+method+")" +urlStr+ ", params: " +params+ ", bodyStr: "+ bodyStr + ", result: " + ret; 268 | if (ex == null) { 269 | log.info(logMsg); 270 | } else { 271 | log.error(logMsg, ex); 272 | } 273 | } 274 | if (ex != null) throw new RuntimeException(ex); 275 | return ret; 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /src/main/java/cn/xnatural/app/util/DB.java: -------------------------------------------------------------------------------- 1 | package cn.xnatural.app.util; 2 | 3 | import javax.sql.DataSource; 4 | import java.beans.Introspector; 5 | import java.beans.PropertyDescriptor; 6 | import java.lang.reflect.Field; 7 | import java.math.BigDecimal; 8 | import java.sql.*; 9 | import java.util.Date; 10 | import java.util.*; 11 | import java.util.concurrent.atomic.AtomicInteger; 12 | import java.util.function.Function; 13 | import java.util.function.Supplier; 14 | 15 | /** 16 | * 数据库 sql 操作工具 17 | */ 18 | public class DB implements AutoCloseable { 19 | protected static final AtomicInteger count = new AtomicInteger(); 20 | /** 21 | * 名字标识 22 | */ 23 | public final String name = "DB-" + count.getAndIncrement(); 24 | /** 25 | * 数据源 26 | */ 27 | protected volatile DataSource dataSource; 28 | /** 29 | * 最大返回条数限制 30 | */ 31 | protected Integer maxRows = 5000; 32 | /** 33 | * dataSource 属性集 34 | */ 35 | protected final Map dsAttr = new HashMap<>(); 36 | /** 37 | * 事务连接,保存在线程上下文 38 | */ 39 | protected static final ThreadLocal txConn = new ThreadLocal<>(); 40 | 41 | /** 42 | * 创建一个 {@link DB} 43 | * @param dataSource 外部数据源 44 | */ 45 | public DB(DataSource dataSource) { 46 | if (dataSource == null) throw new IllegalArgumentException("Param dataSource required"); 47 | this.dataSource = dataSource; 48 | } 49 | 50 | /** 51 | * 创建一个 {@link DB} 52 | * @param dsAttr 属性集 53 | */ 54 | public DB(Map dsAttr) { 55 | if (dsAttr == null) throw new IllegalArgumentException("Param dsAttr required"); 56 | this.dsAttr.putAll(dsAttr); 57 | } 58 | 59 | /** 60 | * 创建一个{@link DB} 61 | * @param jdbcUrl jdbc 连接地址 62 | */ 63 | public DB(String jdbcUrl) { this(jdbcUrl, null, null, 1, 8); } 64 | 65 | /** 66 | * 创建一个{@link DB} 67 | * @param jdbcUrl jdbc 连接地址 68 | * @param username 连接用户名 69 | * @param password 连接密码 70 | */ 71 | public DB(String jdbcUrl, String username, String password) { this(jdbcUrl, username, password, null, null); } 72 | 73 | /** 74 | * 创建一个{@link DB} 75 | * @param jdbcUrl jdbc 连接地址 76 | * @param minIdle 最小连接 77 | * @param maxActive 最大活动连接 78 | */ 79 | public DB(String jdbcUrl, Integer minIdle, Integer maxActive) { this(jdbcUrl, null, null, minIdle, maxActive); } 80 | 81 | /** 82 | * 创建一个{@link DB} 83 | * @param jdbcUrl jdbc 连接地址 84 | * @param username 连接用户名 85 | * @param password 连接密码 86 | * @param minIdle 最小连接 87 | * @param maxActive 最大活动连接 88 | */ 89 | public DB(String jdbcUrl, String username, String password, Integer minIdle, Integer maxActive) { 90 | if (jdbcUrl == null || jdbcUrl.isEmpty()) throw new IllegalArgumentException("Param jdbcUrl required"); 91 | if (minIdle == null || minIdle < 0) throw new IllegalArgumentException("Param minIdle must >= 0"); 92 | if (maxActive == null || maxActive <= 0) throw new IllegalArgumentException("Param maxActive must > 0"); 93 | dsAttr.put("url", jdbcUrl); dsAttr.put("jdbcUrl", jdbcUrl); 94 | if (username != null) dsAttr.put("username", username); 95 | if (password != null) dsAttr.put("password", password); 96 | dsAttr.put("minIdle", minIdle); dsAttr.put("minimumIdle", minIdle); 97 | dsAttr.put("maxActive", maxActive); dsAttr.put("maximumPoolSize", maxActive); 98 | } 99 | 100 | 101 | 102 | /** 103 | * 设置 {@link DataSource} 属性 104 | * @param attrName 属性名 105 | * @param attrValue 属性值 106 | * @return {@link DB} 107 | */ 108 | public DB dsAttr(String attrName, Object attrValue) { 109 | if (dataSource != null) throw new RuntimeException("dataSource already created"); 110 | dsAttr.put(attrName, attrValue); 111 | return this; 112 | } 113 | 114 | 115 | /** 116 | * 设置限制最大返回条数 117 | * @param maxRows > 0 118 | * @return {@link DB} 119 | */ 120 | public DB setMaxRows(int maxRows) { 121 | if (maxRows < 1) throw new IllegalArgumentException("Param maxRows must > 0"); 122 | this.maxRows = maxRows; 123 | return this; 124 | } 125 | 126 | 127 | /** 128 | * 执行连接 129 | * @param fn 函数 130 | * @return {@link DB} 131 | */ 132 | public T withConn(Function fn) { 133 | init(); 134 | Connection conn = null; 135 | try { 136 | conn = txConn.get() == null ? dataSource.getConnection() : txConn.get(); 137 | return fn.apply(conn); 138 | } catch (SQLException ex) { 139 | throw new RuntimeException(ex); 140 | } finally { 141 | if (txConn.get() == null) { // 证明当前线程没有事务, 需要直接释放连接 142 | try { 143 | conn.close(); 144 | } catch (SQLException ex) { 145 | throw new RuntimeException(ex); 146 | } 147 | } 148 | } 149 | } 150 | 151 | 152 | /** 153 | * 开启一个事务 154 | * @param fn 事务执行函数 155 | * @param 返回类型 156 | * @return 返回值 157 | */ 158 | public T trans(Supplier fn) { 159 | return withConn(conn -> { 160 | try { 161 | boolean ac = conn.getAutoCommit(); 162 | try { 163 | txConn.set(conn); conn.setAutoCommit(false); 164 | T t = fn.get(); conn.commit(); 165 | return t; 166 | } catch (Exception ex) { 167 | conn.rollback(); 168 | throw ex; 169 | } finally { 170 | txConn.set(null); 171 | conn.setAutoCommit(ac); 172 | conn.close(); 173 | } 174 | } catch (SQLException ex) { 175 | throw new RuntimeException(ex); 176 | } 177 | }); 178 | } 179 | 180 | 181 | /** 182 | * 插入一条数据/更新数据 183 | * @param sql sql 语句 184 | * @param params 参数 185 | * @return 成功条数 186 | */ 187 | public int execute(String sql, Object...params) { 188 | return withConn(conn -> { 189 | try (PreparedStatement pst = conn.prepareStatement(sql)) { 190 | fillParam(pst, params); 191 | return pst.executeUpdate(); 192 | } catch (SQLException ex) { 193 | throw new RuntimeException(ex); 194 | } 195 | }); 196 | } 197 | 198 | 199 | /** 200 | * 执行一个存储过程 201 | * @param sql sql语句 202 | * @param params 参数 203 | * @return 影响条数 204 | */ 205 | public int call(String sql, Object...params) { 206 | return withConn(conn -> { 207 | try (CallableStatement cst = conn.prepareCall(sql)) { 208 | fillParam(cst, params); 209 | return cst.executeUpdate(); 210 | } catch (SQLException ex) { 211 | throw new RuntimeException(ex); 212 | } 213 | }); 214 | } 215 | 216 | 217 | /** 218 | * 插入一条数据 并 返回第一个 数据库自生成字段 219 | * @param sql sql 语句 220 | * @param params 参数 221 | * @return 自生成字段的值 222 | */ 223 | public Object insertWithGeneratedKey(String sql, Object...params) { 224 | return withConn(conn -> { 225 | try (PreparedStatement pst = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { 226 | fillParam(pst, params); 227 | pst.executeUpdate(); 228 | try (ResultSet rs = pst.getGeneratedKeys()) { 229 | if (rs.next()) return rs.getObject(1); 230 | } 231 | } catch (SQLException ex) { 232 | throw new RuntimeException(ex); 233 | } 234 | return null; 235 | }); 236 | } 237 | 238 | 239 | /** 240 | * 查询多条数据 241 | * @param sql sql 语句 242 | * @param params 参数 243 | * @return 多条数据结果 244 | */ 245 | public List> rows(String sql, Object...params) { 246 | final List> result = new LinkedList<>(); 247 | return withConn(conn -> { 248 | try (PreparedStatement pst = conn.prepareStatement(sql)) { 249 | fillParam(pst, params); 250 | try (ResultSet rs = pst.executeQuery()) { 251 | while (rs.next()) { 252 | ResultSetMetaData metadata = rs.getMetaData(); 253 | Map row = new LinkedHashMap<>(metadata.getColumnCount(), 1); 254 | result.add(row); 255 | for (int i = 1; i <= metadata.getColumnCount(); i++) { 256 | row.put(metadata.getColumnLabel(i), rs.getObject(i)); 257 | } 258 | } 259 | } 260 | } catch (SQLException ex) { 261 | throw new RuntimeException(ex); 262 | } 263 | return result; 264 | }); 265 | } 266 | 267 | 268 | /** 269 | * 返回一条数据 270 | * @param sql sql语句 271 | * @param params 参数 272 | * @return 一条数据 273 | */ 274 | public Map row(String sql, Object...params) { 275 | final Map result = new LinkedHashMap<>(); 276 | return withConn(conn -> { 277 | try (PreparedStatement pst = conn.prepareStatement(sql)) { 278 | fillParam(pst, params); 279 | try (ResultSet rs = pst.executeQuery()) { 280 | if (rs.next()) { 281 | ResultSetMetaData metadata = rs.getMetaData(); 282 | for (int i = 1; i <= metadata.getColumnCount(); i++) { 283 | result.put(metadata.getColumnLabel(i), rs.getObject(i)); 284 | } 285 | } 286 | } 287 | } catch (SQLException ex) { 288 | throw new RuntimeException(ex); 289 | } 290 | return result; 291 | }); 292 | } 293 | 294 | 295 | /** 296 | * 查询单个值 297 | * @param sql sql 语句 298 | * @param retType 返回的类型 299 | * Integer.class, Long.class, String.class, Double.class, BigDecimal.class, Boolean.class, Date.class 300 | * @param params 参数 301 | * @param 类型 302 | * @return 单个值 303 | */ 304 | public T single(String sql, Class retType, Object...params) { 305 | return (T) withConn(conn -> { 306 | try (PreparedStatement pst = conn.prepareStatement(sql)) { 307 | fillParam(pst, params); 308 | try (ResultSet rs = pst.executeQuery()) { 309 | if (rs.next()) { 310 | if (String.class.equals(retType)) return rs.getString(1); 311 | if (Integer.class.equals(retType)) return rs.getInt(1); 312 | if (Long.class.equals(retType)) return rs.getLong(1); 313 | if (Double.class.equals(retType)) return rs.getDouble(1); 314 | if (BigDecimal.class.equals(retType)) return rs.getBigDecimal(1); 315 | if (Boolean.class.equals(retType)) return rs.getBoolean(1); 316 | if (Date.class.equals(retType)) return rs.getDate(1); 317 | return rs.getObject(1); 318 | } 319 | } 320 | } catch (SQLException ex) { 321 | throw new RuntimeException(ex); 322 | } 323 | return null; 324 | }); 325 | } 326 | 327 | 328 | protected void fillParam(PreparedStatement pst, Object...params) throws SQLException { 329 | ParameterMetaData metaData = pst.getParameterMetaData(); 330 | if (metaData != null && params != null) { 331 | for (int i = 0; i < params.length; i++) { 332 | Object v = params[i]; 333 | if (v instanceof Date) v = new java.sql.Date(((Date) v).getTime()); 334 | pst.setObject(i + 1, v); 335 | } 336 | } 337 | pst.setMaxRows(maxRows); 338 | } 339 | 340 | 341 | protected DB init() { 342 | if (dataSource == null) { 343 | synchronized (this) { 344 | if (dataSource == null) { 345 | dataSource = createDataSource(dsAttr); 346 | } 347 | } 348 | } 349 | return this; 350 | } 351 | 352 | 353 | @Override 354 | public void close() throws Exception { 355 | try { 356 | dataSource.getClass().getMethod("close").invoke(dataSource); 357 | } catch (Exception e) {} 358 | } 359 | 360 | 361 | /** 362 | * 获取 jdbc 连接地址 363 | */ 364 | public String getJdbcUrl() { 365 | if (dataSource == null) init(); 366 | try { 367 | Class c = dataSource.getClass(); 368 | do { 369 | for (Field f : c.getDeclaredFields()) { 370 | if ("jdbcUrl".equals(f.getName()) || "url".equals(f.getName())) { 371 | f.setAccessible(true); 372 | return (String) f.get(dataSource); 373 | } 374 | } 375 | c = c.getSuperclass(); 376 | } while (c != null); 377 | } catch (Exception ex) {} 378 | return null; 379 | } 380 | 381 | 382 | @Override 383 | public String toString() { 384 | return "DB{" + 385 | "name='" + name + '\'' + 386 | "jdbcUrl='" + getJdbcUrl() + '\'' + 387 | '}'; 388 | } 389 | 390 | 391 | /** 392 | * 创建一个 数据源 393 | * @param dsAttr 连接池属性 394 | * @return {@link DataSource} 数据源 395 | */ 396 | public static DataSource createDataSource(Map dsAttr) { 397 | DataSource ds = null; 398 | // druid 数据源 399 | try { 400 | Map props = new HashMap(); 401 | dsAttr.forEach((s, o) -> props.put(s, Objects.toString(o, ""))); 402 | // if (!props.containsKey("validationQuery")) props.put("validationQuery", "select 1") // oracle 不行 403 | if (!props.containsKey("filters")) { // 默认监控慢sql 404 | props.put("filters", "stat"); 405 | } 406 | if (!props.containsKey("connectionProperties")) { 407 | // com.alibaba.druid.filter.stat.StatFilter 408 | props.put("connectionProperties", "druid.stat.logSlowSql=true;druid.stat.slowSqlMillis=5000"); 409 | } 410 | ds = (DataSource) Class.forName("com.alibaba.druid.pool.DruidDataSourceFactory").getMethod("createDataSource", Map.class).invoke(null, props); 411 | } 412 | catch(ClassNotFoundException ex) {} 413 | catch(Exception ex) { throw new RuntimeException(ex); } 414 | if (ds != null) return ds; 415 | 416 | // Hikari 数据源 417 | try { 418 | Class clz = Class.forName("com.zaxxer.hikari.HikariDataSource"); 419 | ds = (DataSource) clz.newInstance(); 420 | for (PropertyDescriptor pd : Introspector.getBeanInfo(clz).getPropertyDescriptors()) { 421 | Object v = dsAttr.get(pd.getName()); 422 | if (v != null) { 423 | if (Integer.class.equals(pd.getPropertyType()) || int.class.equals(pd.getPropertyType())) pd.getWriteMethod().invoke(ds, Integer.valueOf(v.toString())); 424 | else if (Long.class.equals(pd.getPropertyType()) || long.class.equals(pd.getPropertyType())) pd.getWriteMethod().invoke(ds, Long.valueOf(v.toString())); 425 | else if (Boolean.class.equals(pd.getPropertyType()) || boolean.class.equals(pd.getPropertyType())) pd.getWriteMethod().invoke(ds, Boolean.valueOf(v.toString())); 426 | else pd.getWriteMethod().invoke(ds, v); 427 | } 428 | } 429 | } 430 | catch(ClassNotFoundException ex) {} 431 | catch(Exception ex) { throw new RuntimeException(ex); } 432 | if (ds != null) return ds; 433 | 434 | // dbcp2 数据源 435 | try { 436 | Properties props = new Properties(); 437 | dsAttr.forEach((s, o) -> props.put(s, Objects.toString(o, ""))); 438 | // if (!props.containsKey("validationQuery")) props.put("validationQuery", "select 1"); 439 | ds = (DataSource) Class.forName("org.apache.commons.dbcp2.BasicDataSourceFactory").getMethod("createDataSource", Properties.class).invoke(null, props); 440 | } 441 | catch(ClassNotFoundException ex) {} 442 | catch(Exception ex) { throw new RuntimeException(ex); } 443 | 444 | if (ds == null) throw new RuntimeException("No found DataSource impl class"); 445 | return ds; 446 | } 447 | } 448 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 介绍 2 | 小巧的java应用微内核框架. 基于 [enet](https://gitee.com/xnat/enet) 事件环型框架结构 3 | 4 | 目前大部分高级语言都解决了编程内存自动管理的问题(垃圾回收), 但并没有解决cpu资源自动管理的问题。 5 | 基本上都是程序员主动创建线程或线程池,这些线程是否被充分利用了,在应用不同的地方创建是否有必要, 6 | 太多的线程是否造成竞争性能损耗。毕竟线程再多,而决定并发的是cpu的个数。 7 | 8 | 所以需要框架来实现一个智能执行器(线程池):根据应用的忙碌程度自动创建和销毁线程, 9 | 即线程池会自己根据排对的任务数和当前池中的线程数的比例判断是否需要新创建线程(和默认线程池的行为不同)。 10 | 会catch所有异常, 不会被业务异常给弄死(永动机)。 11 | 程序只需通过配置最大最小资源自动适配, 类似现在的数据库连接池。这样既能充分利用线程资源, 12 | 也减少线程的空转和线程过多调度的性能浪费。 13 | 14 | > 所以系统性能只由线程池大小属性 sys.exec.corePoolSize=8, sys.exec.maximumPoolSize=16 和 jvm内存参数 -Xmx1024m 控制 15 | 16 | 框架设计一种轻量级执行器(伪协程): __Devourer__ 来控制执行模式(并发,暂停/恢复,速度等) 17 | 上层服务应该只关注怎么组装执行任务,然后提交给 __Devourer__ 或直接给执行线程池。如下图: 18 | 19 | 106 | ![Image text](https://www.plantuml.com/plantuml/png/dLHVRnf747-_Jx5o7t9dsJZc4QIgKjktFjMHMgdfmwKipSddvNf_ebfL9SUj0pW6ktRy7yiXmZ529IHjH37OuSlSx77VeWjpXkDGBPfxGEpipFn-C_ERjPOrPgYcka8-px2KPciPzYLBBTchEYMFTOrHIP8Il5I0pJAyMr-YvXDgFZ3qf2HPXgxP4X7V_ATaCKRScwxteWgDAyWTylnbhxm5nrNv2_eauvZKL983ryHF3dMeFBo7dOAu6Lm95lO0dypyPv8Pyej4Wc-8Zn_ouCLBo3NXgWdRVoJ7BXEnVfcwJdMPASbe79j_j3hF-FQEswuano68-gEgiMY0ltOExTS85mMo34fJyapy_e8rCma5PrdOMeFSCsZz1gKgRsnxbxk8_93nqXfKJkBttLRDxNH4qwSYmq_MuMbfWeOZYB2Kp0-R_k7x1NvMTZlDIJxywIke5AB19hMS5IehqpNZwBm_By5zfuYqUIdFztFHf8v5lr8jMxPDXquIwMLhi5dbhGt_k88P8L_mprvv9xzZqeSCjclOALI8sweZwD1bb5HK7aX4Gl1CEarD6Tq2y5ybwLwuBd6AAF7R1wgrdC0W__JP0lxphWzuSHVQUqAF68C5zfs_CLN3e6OoP6SPc-9n-64UD0xfHloGFPv3RSAruFKBlrP1bB6XszIuNQ_i3Tz_guHy8hL6fvWj227SdTwaItq0b6aGy6TPmCoH4AWuGQx23-hyg04xhz7l_zB19SQieQ3eCeTX5-V2f_XSB1P3l0bDt1lhw3zY62zxtTDaT9ZYJRJfp_PKmmn4yUOAZgk1JW8sr_jyhtZNh75mGdKoqaLfXhiJC8t7Y7VwdXSlWYsuNXU32Yi_eLghx8UHogNG6aWXNNj_TxpE-V25NV3QNQ_wdBzxUVl23dwqD0bIoLyxZAcFTAeen7vC6P7zmxyKX1IzWuqBhvw73nkujyWbdJ6NfL2RZOoka-YQ9X2PB8vYiEIf8-CV7SdG95equafrYuuIVFU9ILFVzyNOFKwGsLcusODy0Kl5h49diBk5TamhBE8vueqNxeVdlUP6hvjrMsjGs9Jzpb7lJKMPdGqnWTmfTDhyu2t63WbfQUpjhVo573uJPcT5b_u5) 107 | 108 | # 安装教程 109 | ```xml 110 | 111 | cn.xnatural 112 | tiny 113 | 1.1.6 114 | 115 | ``` 116 | 117 | # 初始化 118 | ```java 119 | // 创建一个应用 120 | final AppContext app = new AppContext(); 121 | 122 | // 添加服务 server1 123 | app.addSource(new ServerTpl("server1") { 124 | @EL(name = "sys.starting") 125 | void start() { 126 | log.info("{} start", name); 127 | } 128 | }); 129 | // 添加自定义服务 130 | app.addSource(new TestService()); 131 | 132 | // 应用启动(会依次触发系统事件) 133 | app.start(); 134 | ``` 135 | 136 | > 基于事件环型微内核框架结构图 137 | > 138 | > > 以AppContext#EP为事件中心的挂载服务结构 139 | 257 | 258 | ![Image text](http://www.plantuml.com/plantuml/png/vLJDJjjE7BpxANw2Iw9_Y0JS0dz4ItEeH5LKZbKF9ju4LuutjHqKH960a0gIK884z8EK3wGsaI1y50aARiJBPDVXBRhs6kDGe4YLUk6stfsPdPsTTRzkY9gHJYf2J15r7HwbKWDODL36W0a1e3qw12Xb3vw9gTvXGvFLH0YUZxn6CQCFT9pMOeYjN0SyGMDi2Mbzy2QDqaWN6E0_KPA67KA0yrrwq5vpN0I2mgGWgDX0eA2u0JZkinE9E3uQPuM6UTpuKIFdMG6f4jXmbwJ9YT7VM7wFT7wAbkURsplqn2JvJUlpx33Inf3c2TsnEp-8NuHpsvq5eAkddYW3aVrJClU1pbUQMqNogNelfru-ZC-rQ7c1vBVkuyx9J-WCGxFoZImkyPH07zV3iYeRI0BhoBJCZOlSWbNVOyhDUfti5UbSAGJMsRbLBT33pL1Bk6Jk2waKI76Ld7pdKA7h1dbdOtRdq3p8MijL7WxtpSQac2EbdVxeO40LamZ-XpO_foq8B2rhROcKTbb8TeH7Aq9tk5MOIt8K3vJR8QNtpBnPiSmQTtL5Gv9x55zqlDxH8LxhYRYC56aI_AKTb7K3gNPf5PtDzzYzd4WYOnGpO5pMK80ZNMrIMhXy2U5mc2pEq9M3OC_r1hCT8n55z_Vlwi0VDyd1R0H8xgWvlSnIuWdSKXOkPVlmdW4_jm_lUxszRpiw64E83l4XRsidH80k7r-ilVDSN4Dq_H7HVGF2pTVRnGxPJgMqJ_9L3cEVRFBsBh35CInBSFah070rfikqjdst1awbMZNO39Dm1NB7P2zxcq0cuz2yYtRucSmLU-ECbdT9VgEPhTjiFtO4YMfWm3wuCxGEJR9U206lYJF5IXBqK_Z_q2sI-vTmYlGYhQhY25AWOPhixRIIH7rSZGKuH450VixGsjURW0bal7og6YY1DDPdRBVwCSOAC-AuUkLjVBXEfogEkSdMg-k2lx-x-mMFSMlmhZMC7shqtKpj7vLU55kp5yK757e_KgLqKla5) 259 | 260 | 261 | ## 系统事件: app.start() 后会依次触发 sys.inited, sys.starting, sys.started 262 | + sys.inited: 应用始化完成(环境配置, 系统线程池, 事件中心) 263 | + sys.starting: 通知所有服务启动. 一般为ServerTpl 264 | + sys.started: 应用启动完成 265 | + sys.stopping: 应用停止事件(kill pid) 266 | 267 | ## 配置 268 | > 配置文件加载顺序(优先级从低到高): 269 | * classpath: app.properties, classpath: app-[profile].properties 270 | * file: ./app.properties, file: ./app-[profile].properties 271 | * configdir: app.properties, configdir: app-[profile].properties 272 | * 自定义环境属性配置(重写方法): AppContext#customEnv 273 | * System.getProperties() 274 | 275 | >+ 系统属性(-Dconfigname): configname 指定配置文件名. 默认: app 276 | >+ 系统属性(-Dprofile): profile 指定启用特定的配置 277 | >+ 系统属性(-Dconfigdir): configdir 指定额外配置文件目录 278 | 279 | * 只读取properties文件. 按顺序读取app.properties, app-[profile].properties 两个配置文件 280 | * 配置文件支持简单的 ${} 属性替换 281 | 282 | 283 | ## 添加 [xhttp](https://gitee.com/xnat/xhttp) 服务 284 | ```properties 285 | ### app.properties 286 | web.hp=:8080 287 | ``` 288 | ```java 289 | app.addSource(new ServerTpl("web") { //添加web服务 290 | HttpServer server; 291 | 292 | @EL(name = "sys.starting", async = true) 293 | void start() { 294 | server = new HttpServer(attrs(), exec()); 295 | server.buildChain(chain -> { 296 | chain.get("get", hCtx -> { 297 | hCtx.render("xxxxxxxxxxxx"); 298 | }); 299 | }).start(); 300 | } 301 | 302 | @EL(name = "sys.stopping") 303 | void stop() { 304 | if (server != null) server.stop(); 305 | } 306 | }); 307 | ``` 308 | 309 | ## 添加 [xjpa](https://gitee.com/xnat/xjpa) 数据库操作服务 310 | ```properties 311 | ### app.properties 312 | jpa_local.url=jdbc:mysql://localhost:3306/test?useSSL=false&user=root&password=root&allowPublicKeyRetrieval=true 313 | ``` 314 | ```java 315 | app.addSource(new ServerTpl("jpa_local") { //数据库 jpa_local 316 | Repo repo; 317 | 318 | @EL(name = "sys.starting", async = true) 319 | void start() { 320 | repo = new Repo(attrs()).init(); 321 | exposeBean(repo); // 把repo暴露给全局, 即可以通过@Inject注入 322 | ep.fire(name + ".started"); 323 | } 324 | 325 | @EL(name = "sys.stopping", async = true, order = 2f) 326 | void stop() { if (repo != null) repo.close(); } 327 | }); 328 | ``` 329 | 330 | ## 添加 [sched](https://gitee.com/xnat/jpa) 时间调度服务 331 | ```java 332 | app.addSource(new ServerTpl("sched") { 333 | Sched sched; 334 | @EL(name = "sys.starting", async = true) 335 | void start() { 336 | sched = new Sched(attrs(), exec()).init(); 337 | exposeBean(sched); 338 | ep.fire(name + ".started"); 339 | } 340 | 341 | @EL(name = "sched.after") 342 | void after(Duration duration, Runnable fn) {sched.after(duration, fn);} 343 | 344 | @EL(name = "sys.stopping", async = true) 345 | void stop() { if (sched != null) sched.stop(); } 346 | }); 347 | ``` 348 | 349 | ## 动态按需添加服务 350 | ```java 351 | @EL(name = "sys.inited") 352 | void sysInited() { 353 | if (!app.attrs("redis").isEmpty()) { //根据配置是否有redis,创建redis客户端工具 354 | app.addSource(new RedisClient()) 355 | } 356 | } 357 | ``` 358 | 359 | ## 让系统心跳(即:让系统安一定频率触发事件 sys.heartbeat) 360 | > 需要用 [sched](https://gitee.com/xnat/sched) 添加 _sched.after_ 事件监听 361 | ```java 362 | @EL(name = "sched.after") 363 | void after(Duration duration, Runnable fn) {sched.after(duration, fn);} 364 | ``` 365 | > 每隔一段时间触发一次心跳, 1~4分钟(两个配置相加)随机心跳 366 | > + 配置(sys.heartbeat.minInterval) 控制心跳最小时间间隔 367 | > + 配置(sys.heartbeat.randomInterval) 控制心跳最大时间间隔 368 | 369 | ```java 370 | // 心跳事件监听器 371 | @EL(name = "sys.heartbeat", async = true) 372 | void myHeart() { 373 | System.out.println("咚"); 374 | } 375 | ``` 376 | 377 | ## 服务基础类: ServerTpl 378 | > 推荐所有被加入到AppContext中的服务都是ServerTpl的子类 379 | ```properties 380 | ### app.properties 381 | 服务名.prop=1 382 | ``` 383 | ```java 384 | app.addSource(new ServerTpl("服务名") { 385 | 386 | @EL(name = "sys.starting", async = true) 387 | void start() { 388 | // 初始化服务 389 | } 390 | }) 391 | ``` 392 | 393 | ### bean注入 @Inject(name = "beanName") 394 | > 注入匹配规则: (已经存在值则不需要再注入) 395 | > > 1. 如果 @Inject name 没配置 396 | > > > 先按 字段类型 和 字段名 匹配, 如无匹配 再按 字段类型 匹配 397 | > > 2. 则按 字段类型 和 @Inject(name = "beanName") beanName 匹配 398 | ```java 399 | app.addSource(new ServerTpl() { 400 | @Inject Repo repo; //自动注入 401 | 402 | @EL(name = "sys.started", async = true) 403 | void init() { 404 | List rows = repo.rows("select * from test") 405 | log.info("========= {}", rows); 406 | } 407 | }); 408 | ``` 409 | 410 | ### 动态bean获取: 方法 bean(Class bean类型, String bean名字) 411 | ```java 412 | app.addSource(new ServerTpl() { 413 | @EL(name = "sys.started", async = true) 414 | void start() { 415 | String str = bean(Repo.class).firstRow("select count(1) as total from test").get("total").toString(); 416 | log.info("=========" + str); 417 | } 418 | }); 419 | ``` 420 | 421 | ### bean依赖注入原理 422 | > 两种bean容器: AppContext是全局bean容器, 每个服务(ServerTpl)都是一个bean容器 423 | > > 获取bean对象: 先从全局查找, 再从每个服务中获取 424 | 425 | * 暴露全局bean 426 | ```java 427 | app.addSource(new TestService()); 428 | ``` 429 | * 服务(ServerTpl)里面暴露自己的bean 430 | ```java 431 | Repo repo = new Repo("jdbc:mysql://localhost:3306/test?user=root&password=root").init(); 432 | exposeBean(repo); // 加入到bean容器,暴露给外部使用 433 | ``` 434 | 435 | ### 属性直通车 436 | > 服务(ServerTpl)提供便捷方法获取配置.包含: getLong, getInteger, getDouble, getBoolean等 437 | ```properties 438 | ## app.properties 439 | testSrv.prop1=1 440 | testSrv.prop2=2.2 441 | ``` 442 | ```java 443 | app.addSource(new ServerTpl("testSrv") { 444 | @EL(name = "sys.starting") 445 | void init() { 446 | log.info("print prop1: {}, prop2: {}", getInteger("prop1"), getDouble("prop2")); 447 | } 448 | }) 449 | ``` 450 | 451 | ### 对应上图的两种任务执行 452 | #### 异步任务 453 | ```java 454 | async(() -> { 455 | // 异步执行任务 456 | }) 457 | ``` 458 | #### 创建任务对列 459 | ```java 460 | queue("队列名", () -> { 461 | // 执行任务 462 | }) 463 | ``` 464 | 465 | ## _对列执行器_: Devourer 466 | 会自旋执行完队列中所有任务 467 | 当需要控制任务最多 一个一个, 两个两个... 的执行时 468 | 服务基础类(ServerTpl)提供方法创建: queue 469 | 470 | ### 添加任务到队列 471 | ```java 472 | // 方法1 473 | queue("save", () -> { 474 | // 执行任务 475 | }); 476 | // 方法2 477 | queue("save").offer(() -> { 478 | // 执行任务 479 | }); 480 | ``` 481 | ### 队列特性 482 | #### 并发控制 483 | 最多同时执行任务数, 默认1(one-by-one) 484 | ```java 485 | queue("save").parallel(2) 486 | ``` 487 | > 注: parallel 最好小于 系统最大线程数(sys.exec.maximumPoolSize), 即不能让某一个执行对列占用所有可用的线程 488 | 489 | #### 执行速度控制 490 | 把任务按速度均匀分配在时间线上执行 491 | 支持: 每秒(10/s), 每分(10/m), 每小时(10/h), 每天(10/d) 492 | ```java 493 | // 例: 按每分钟执行30个任务的频率 494 | queue("save").speed("30/m") 495 | ``` 496 | ```java 497 | // 清除速度控制(立即执行) 498 | queue("save").speed(null) 499 | ``` 500 | 501 | #### 队列 暂停/恢复 502 | ```java 503 | // 暂停执行, 一般用于发生错误时 504 | // 注: 必须有新的任务入对, 重新触发继续执行. 或者resume方法手动恢复执行 505 | queue("save") 506 | .errorHandle {ex, me -> 507 | // 发生错误时, 让对列暂停执行(不影响新任务入对) 508 | // 1. 暂停一段时间 509 | me.suspend(Duration.ofSeconds(180)); 510 | // 2. 条件暂停(每个新任务入队都会重新验证条件) 511 | // me.suspend(queue -> true); 512 | }; 513 | 514 | // 手动恢复执行 515 | // queue("save").resume() 516 | ``` 517 | #### 队列最后任务有效 518 | 是否只使用队列最后一个, 清除队列前面的任务 519 | 适合: 入队的频率比出队高, 前面的任务可有可无 520 | ```java 521 | // 例: increment数据库的一个字段的值 522 | Devourer q = queue("increment").useLast(true); 523 | for (int i = 0; i < 20; i++) { 524 | // 入队快, 任务执行慢, 中间的可以不用执行 525 | q.offer(() -> repo.execute("update test set count=?", i)); 526 | } 527 | ``` 528 | 529 | ```java 530 | // 例: 从服务端获取最新的数据 531 | Devourer q = queue("newer").useLast(true); 532 | // 用户不停的点击刷新 533 | q.offer(() -> { 534 | Utils.http().get("http://localhost:8080/data/newer").execute(); 535 | }) 536 | ``` 537 | 538 | #### 原理: 并发流量控制锁 LatchLock 539 | 当被执行代码块需要控制同时线程执行的个数时 540 | ```java 541 | final LatchLock lock = new LatchLock(); 542 | lock.limit(3); // 设置并发限制. 默认为1 543 | if (lock.tryLock()) { // 尝试获取一个锁 544 | try { 545 | // 被执行的代码块 546 | } finally { 547 | lock.release(); // 释放一个锁 548 | } 549 | } 550 | ``` 551 | 552 | ## 数据库操作工具 553 | #### 创建一个数据源 554 | ```java 555 | DB repo = new DB("jdbc:mysql://localhost:3306/test?useSSL=false&user=root&password=root&allowPublicKeyRetrieval=true"); 556 | ``` 557 | #### 查询单条记录 558 | ```java 559 | repo.row("select * from test order by id desc"); 560 | ``` 561 | #### 查询多条记录 562 | ```java 563 | repo.rows("select * from test limit 10"); 564 | repo.rows("select * from test where id in (?, ?)", 2, 7); 565 | ``` 566 | #### 查询单个值 567 | ```java 568 | // 只支持 Integer.class, Long.class, String.class, Double.class, BigDecimal.class, Boolean.class, Date.class 569 | repo.single("select count(1) from test", Integer.class); 570 | ``` 571 | #### 插入一条记录 572 | ```java 573 | repo.execute("insert into test(name, age, create_time) values(?, ?, ?)", "方羽", 5000, new Date()); 574 | ``` 575 | #### 更新一条记录 576 | ```java 577 | repo.execute("update test set age = ? where id = ?", 10, 1) 578 | ``` 579 | #### 事务 580 | ```java 581 | // 执行多条sql语句 582 | repo.trans(() -> { 583 | // 插入并返回id 584 | Object id = repo.insertWithGeneratedKey("insert into test(name, age, create_time) values(?, ?, ?)", "方羽", 5000, new Date()); 585 | repo.execute("update test set age = ? where id = ?", 18, id); 586 | return null; 587 | }); 588 | ``` 589 | 590 | ## http客户端 591 | ```java 592 | // get 593 | Utils.http().get("http://xnatural.cn:9090/test/cus?p2=2") 594 | .header("test", "test") // 自定义header 595 | .cookie("sessionId", "xx") // 自定义 cookie 596 | .connectTimeout(5000) // 设置连接超时 5秒 597 | .readTimeout(15000) // 设置读结果超时 15秒 598 | .param("p1", 1) // 添加参数 599 | .debug().execute(); 600 | ``` 601 | ```java 602 | // post 603 | Utils.http().post("http://xnatural.cn:9090/test/cus") 604 | .debug().execute(); 605 | ``` 606 | ```java 607 | // post 表单 608 | Utils.http().post("http://xnatural.cn:9090/test/form") 609 | .param("p1", "p1") 610 | .debug().execute(); 611 | ``` 612 | ```java 613 | // post 上传文件 614 | Utils.http().post("http://xnatural.cn:9090/test/upload") 615 | .param("file", new File("d:/tmp/1.txt")) 616 | .debug().execute(); 617 | 618 | // post 上传文件流. 一般上传大文件 可配合 汇聚流 使用 619 | Utils.http().post("http://xnatural.cn:9090/test/upload") 620 | .fileStream("file", "test.md", new FileInputStream("d:/tmp/test.md")) 621 | .debug().execute(); 622 | ``` 623 | ```java 624 | // post json 625 | Utils.http().post("http://xnatural.cn:9090/test/json") 626 | .jsonBody(new JSONObject().fluentPut("p1", 1).toString()) 627 | .debug().execute(); 628 | ``` 629 | ```java 630 | // post 普通文本 631 | Utils.http().post("http://xnatural.cn:9090/test/string") 632 | .textBody("xxxxxxxxxxxxxxxx") 633 | .debug().execute(); 634 | ``` 635 | 636 | ## 对象拷贝器 637 | #### javabean 拷贝到 javabean 638 | ```java 639 | Utils.copier( 640 | new Object() { 641 | public String name = "徐言"; 642 | }, 643 | new Object() { 644 | private String name; 645 | public void setName(String name) { this.name = name; } 646 | public String getName() { return name; } 647 | } 648 | ).build(); 649 | ``` 650 | #### 对象 转换成 map 651 | ```java 652 | Utils.copier( 653 | new Object() { 654 | public String name = "方羽"; 655 | public String getAge() { return 5000; } 656 | }, 657 | new HashMap() 658 | ).build(); 659 | ``` 660 | #### 添加额外属性源 661 | ```java 662 | Utils.copier( 663 | new Object() { 664 | public String name = "云仙君"; 665 | }, 666 | new Object() { 667 | private String name; 668 | public Integer age; 669 | public void setName(String name) { this.name = name; } 670 | public String getName() { return name; } 671 | 672 | } 673 | ).add("age", () -> 1).build(); 674 | ``` 675 | #### 忽略属性 676 | ```java 677 | Utils.copier( 678 | new Object() { 679 | public String name = "徐言"; 680 | public Integer age = 22; 681 | }, 682 | new Object() { 683 | private String name; 684 | public Integer age = 33; 685 | public void setName(String name) { this.name = name; } 686 | public String getName() { return name; } 687 | 688 | } 689 | ).ignore("age").build(); // 最后 age 为33 690 | ``` 691 | #### 属性值转换 692 | ```java 693 | Utils.copier( 694 | new Object() { 695 | public long time = System.currentTimeMillis(); 696 | }, 697 | new Object() { 698 | private String time; 699 | public void setTime(String time) { this.time = time; } 700 | public String getTime() { return time; } 701 | 702 | } 703 | ).addConverter("time", o -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date((long) o))) 704 | .build(); 705 | ``` 706 | #### 忽略空属性 707 | ```java 708 | Utils.copier( 709 | new Object() { 710 | public String name; 711 | }, 712 | new Object() { 713 | private String name = "方羽"; 714 | public void setName(String name) { this.name = name; } 715 | public String getName() { return name; } 716 | 717 | } 718 | ).ignoreNull(true).build(); // 最后 name 为 方羽 719 | ``` 720 | #### 属性名映射 721 | ```java 722 | Utils.copier( 723 | new Object() { 724 | public String p1 = "徐言"; 725 | }, 726 | new Object() { 727 | private String pp1 = "方羽"; 728 | public void setPp1(String pp1) { this.pp1 = pp1; } 729 | public String getPp1() { return pp1; } 730 | 731 | } 732 | ).mapProp( "p1", "pp1").build(); // 最后 name 为 徐言 733 | ``` 734 | 735 | ## 文件内容监控器(类linux tail) 736 | ```java 737 | Utils.tailer().tail("d:/tmp/tmp.json", 5); 738 | ``` 739 | 740 | ## nanoId(长度): nano算法生成动态唯一字符 741 | ```java 742 | String id = Utils.nanoId(); 743 | ``` 744 | 745 | ## ioCopy(输入流, 输出流, 速度) 746 | ```java 747 | // 文件copy 748 | try (InputStream is = new FileInputStream("d:/tmp/陆景.png"); OutputStream os = new FileOutputStream("d:/tmp/青子.png")) { 749 | Utils.ioCopy(is, os); 750 | } 751 | ``` 752 | 753 | ## 简单缓存 CacheSrv 754 | ```java 755 | // 添加缓存服务 756 | app.addSource(new CacheSrv()); 757 | ``` 758 | ```properties 759 | ## app.properties 缓存最多保存100条数据 760 | cacheSrv.itemLimit=100 761 | ``` 762 | ### 缓存操作 763 | ```java 764 | // 1. 设置缓存 765 | bean(CacheSrv).set("缓存key", "缓存值", Duration.ofMinutes(30)); 766 | // 2. 过期函数 767 | bean(CacheSrv).set("缓存key", "缓存值", record -> { 768 | // 缓存值: record.value 769 | // 缓存更新时间: record.getUpdateTime() 770 | return 函数返回过期时间点(时间缀), 返回null(不过期,除非达到缓存限制被删除); 771 | }); 772 | // 3. 获取缓存 773 | bean(CacheSrv).get("缓存key"); 774 | // 4. 获取缓存值, 并更新缓存时间(即从现在开始重新计算过期时间) 775 | bean(CacheSrv).getAndUpdate("缓存key"); 776 | // 5. 手动删除 777 | bean(CacheSrv).remove("缓存key"); 778 | ``` 779 | 780 | ### hash缓存操作 781 | ```java 782 | // 1. 设置缓存 783 | bean(CacheSrv).hset("缓存key", "数据key", "缓存值", Duration.ofMinutes(30)); 784 | // 2. 过期函数 785 | bean(CacheSrv).hset("缓存key", "数据key", "缓存值", record -> { 786 | // 缓存值: record.value 787 | // 缓存更新时间: record.getUpdateTime() 788 | return 函数返回过期时间点(时间缀), 返回null(不过期,除非达到缓存限制被删除); 789 | }); 790 | // 3. 获取缓存 791 | bean(CacheSrv).hget("缓存key", "数据key"); 792 | // 4. 获取缓存值, 并更新缓存时间(即从现在开始重新计算过期时间) 793 | bean(CacheSrv).hgetAndUpdate("缓存key", "数据key"); 794 | // 5. 手动删除 795 | bean(CacheSrv).hremove("缓存key", "数据key"); 796 | ``` 797 | 798 | ## 无限递归优化实现 Recursion 799 | > 解决java无尾递归替换方案. 例: 800 | ```java 801 | System.out.println(factorialTailRecursion(1, 10_000_000).invoke()); 802 | ``` 803 | ```java 804 | /** 805 | * 阶乘计算 806 | * @param factorial 当前递归栈的结果值 807 | * @param number 下一个递归需要计算的值 808 | * @return 尾递归接口,调用invoke启动及早求值获得结果 809 | */ 810 | Recursion factorialTailRecursion(final long factorial, final long number) { 811 | if (number == 1) { 812 | // new Exception().printStackTrace(); 813 | return Recursion.done(factorial); 814 | } 815 | else { 816 | return Recursion.call(() -> factorialTailRecursion(factorial + number, number - 1)); 817 | } 818 | } 819 | ``` 820 | > 备忘录模式:提升递归效率. 例: 821 | ```java 822 | System.out.println(fibonacciMemo(47)); 823 | ``` 824 | ```java 825 | /** 826 | * 使用同一封装的备忘录模式 执行斐波那契策略 827 | * @param n 第n个斐波那契数 828 | * @return 第n个斐波那契数 829 | */ 830 | long fibonacciMemo(long n) { 831 | return Recursion.memo((fib, number) -> { 832 | if (number == 0 || number == 1) return 1L; 833 | return fib.apply(number-1) + fib.apply(number-2); 834 | }, n); 835 | } 836 | ``` 837 | 838 | 843 | 844 | ## 延迟对象 Lazier 845 | > 封装是一个延迟计算值(只计算一次) 846 | ```java 847 | final Lazier _id = new Lazier<>(() -> { 848 | String id = getHeader("X-Request-ID"); 849 | if (id != null && !id.isEmpty()) return id; 850 | return UUID.randomUUID().toString().replace("-", ""); 851 | }); 852 | ``` 853 | * 延迟获取属性值 854 | ```java 855 | final Lazier _name = new Lazier<>(() -> getAttr("sys.name", String.class, "app")); 856 | ``` 857 | * 重新计算 858 | ```java 859 | final Lazier _num = new Lazier(() -> new Random().nextInt(10)); 860 | _num.get(); 861 | _num.clear(); // 清除重新计算 862 | _num.get(); 863 | ``` 864 | 865 | 866 | ## 应用例子 867 | 最佳实践: [Demo(java)](https://gitee.com/xnat/appdemo) 868 | , [Demo(scala)](https://gitee.com/xnat/tinyscalademo) 869 | , [GRule(groovy)](https://gitee.com/xnat/grule) 870 | 871 | 872 | # 1.1.9 ing 873 | - [x] fix: Utils#nanoId(0) 卡死的问题 874 | - [x] feat: Httper 工具支持 get 传body 875 | - [ ] upgrade: enet:1.1.1 876 | - [ ] refactor: 心跳新配置 60~180 877 | - [ ] feat: 空闲任务 878 | - [ ] feat: 增加日志级别配置 879 | - [ ] fix: Copier is开头的属性被忽略了 880 | - [ ] feat: Httper 工具支持 websocket 881 | - [ ] feat: 自定义注解 882 | 883 | 884 | # 参与贡献 885 | 886 | xnatural@msn.cn 887 | -------------------------------------------------------------------------------- /src/main/java/cn/xnatural/app/AppContext.java: -------------------------------------------------------------------------------- 1 | package cn.xnatural.app; 2 | 3 | import cn.xnatural.enet.event.EC; 4 | import cn.xnatural.enet.event.EL; 5 | import cn.xnatural.enet.event.EP; 6 | import cn.xnatural.enet.event.Listener; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | 10 | import java.io.*; 11 | import java.lang.management.ManagementFactory; 12 | import java.nio.charset.StandardCharsets; 13 | import java.time.Duration; 14 | import java.util.*; 15 | import java.util.concurrent.*; 16 | import java.util.concurrent.atomic.AtomicInteger; 17 | import java.util.concurrent.atomic.AtomicLong; 18 | import java.util.function.BiFunction; 19 | import java.util.function.Function; 20 | import java.util.function.Supplier; 21 | import java.util.regex.Matcher; 22 | import java.util.regex.Pattern; 23 | 24 | import static java.util.Collections.emptyList; 25 | 26 | /** 27 | * 应用执行上下文 28 | * 1. 应用执行环境属性 {@link #env} 29 | * 2. 应用执行公用唯一线程池 {@link #exec} 30 | * 3. 应用事件中心 {@link #ep} 31 | * 4. 应用所有服务实例 {@link #sourceMap} 32 | */ 33 | public class AppContext { 34 | protected static final Logger log = LoggerFactory.getLogger(AppContext.class); 35 | /** 36 | * 服务对象源 37 | */ 38 | protected final Map sourceMap = new ConcurrentHashMap<>(); 39 | /** 40 | * 对列执行器映射 41 | */ 42 | protected final Map queues = new ConcurrentHashMap<>(); 43 | /** 44 | * 启动时间 45 | */ 46 | public final Date startup = new Date(); 47 | /** 48 | * jvm关闭钩子. kill 49 | * System.exit(0) 50 | */ 51 | protected final Thread shutdownHook = new Thread(() -> { 52 | // 通知各个模块服务关闭 53 | CountDownLatch latch = new CountDownLatch(1); 54 | ep().fire(new EC("sys.stopping", AppContext.this).completeFn(ec -> latch.countDown())); 55 | try { 56 | latch.await(getAttr("sys.stopWait", Long.class, 30L), TimeUnit.SECONDS); 57 | } catch (InterruptedException e) { 58 | throw new RuntimeException(e); 59 | } 60 | }, "stop"); 61 | 62 | /** 63 | * 初始化一个 {@link java.util.concurrent.ThreadPoolExecutor} 64 | * NOTE: 如果线程池在不停的创建线程, 有可能是因为 提交的 Runnable 的异常没有被处理. 65 | * see: {@link java.util.concurrent.ThreadPoolExecutor#runWorker} 这里面当有异常抛出时 1128行代码 {@link java.util.concurrent.ThreadPoolExecutor#processWorkerExit} 66 | */ 67 | protected final Lazier _exec = new Lazier<>(() -> { 68 | log.debug("init sys executor ..."); 69 | int processorCount = Runtime.getRuntime().availableProcessors(); 70 | Integer corePoolSize = Math.max(2, getAttr("sys.exec.corePoolSize", Integer.class, processorCount >= 4 ? 8 : 4)); 71 | final ThreadPoolExecutor exec = new ThreadPoolExecutor(corePoolSize, 72 | Math.max(corePoolSize, getAttr("sys.exec.maximumPoolSize", Integer.class, processorCount <= 8 ? 16 : Math.min(processorCount * 2, 64))), 73 | getAttr("sys.exec.keepAliveTime", Long.class, 6L), TimeUnit.HOURS, 74 | new LinkedBlockingQueue(getAttr("sys.exec.queueCapacity", Integer.class, 100000)) { 75 | double upThreadThreshold = getAttr("sys.exec.upThreadThreshold", Double.class, 0.5d); 76 | /** 77 | * 让线程池创建(除核心线程外)新的线程的临界条件 78 | * 核心线程已满才会触发此方法 79 | * 慢创建: 按当前池中的线程数和等待任务数增长 80 | * 考虑点1: 系统内部添加任务, 有可能会被等待, 造成没有那么多任务的假象. 所以不能用size去比较 81 | * 即: 当所有线程都处于执行状态时, 刚好有一个添加任务添加后也只是等待执行, 没有突破添加线程的条件(除非有多个添加任务) 82 | * super.size() > 1 && _exec.get().getPoolSize() < _exec.get().getMaximumPoolSize(); 83 | * @return true: 创建新线程 84 | */ 85 | boolean threshold() { 86 | int size = super.size(); 87 | // 这个 size > 1: 有时 添加任务后 size==1, 但还没被poll, 但线程还有空闲,就是取size的值有时稍快于线程poll任务 88 | if (size <= 1) return false; 89 | int ps = _exec.get().getPoolSize(); 90 | // 不超过最大线程数 91 | if (ps >= _exec.get().getMaximumPoolSize()) return false; 92 | // 所有线程都在忙并且有超过一定比例的当前忙的线程的任务在等待, 非关闭状态 93 | return size >= (int) (ps * upThreadThreshold) && (!shutdownHook.isAlive()); 94 | } 95 | @Override 96 | public boolean offer(Runnable r) { return !threshold() && super.offer(r); } 97 | }, 98 | new ThreadFactory() { 99 | final AtomicLong i = new AtomicLong(1); 100 | @Override 101 | public Thread newThread(Runnable r) { 102 | log.trace("New thread: {}", i.get()); 103 | return new Thread(r, "sys-" + i.getAndIncrement()); 104 | } 105 | }, 106 | new ThreadPoolExecutor.CallerRunsPolicy() 107 | ) { 108 | @Override 109 | public void execute(Runnable cmd) { 110 | super.execute(() -> { 111 | try { cmd.run(); } catch (Throwable ex) { 112 | log.error("", ex); // 只有这能拦截未知异常 ThreadPoolExecutor#runWorker 113 | } 114 | }); 115 | } 116 | }; 117 | if (getAttr("sys.exec.allowCoreThreadTimeOut", Boolean.class, false)) { 118 | exec.allowCoreThreadTimeOut(true); 119 | } 120 | return exec; 121 | }); 122 | /** 123 | * 系统线程池 124 | * @return {@link ExecutorService} 125 | */ 126 | public ExecutorService exec() { return _exec.get(); } 127 | 128 | /** 129 | * 初始化 事件中心 130 | */ 131 | protected final Lazier _ep = new Lazier<>(() -> { 132 | log.debug("init ep ..."); 133 | EP ep = new EP(exec(), LoggerFactory.getLogger(EP.class)) { 134 | @Override 135 | public Object fire(EC ec, List ls) { 136 | if ("sys.inited".equals(ec.eName) || "sys.starting".equals(ec.eName) || "sys.stopping".equals(ec.eName) || "sys.started".equals(ec.eName)) { 137 | if (ec.source() != AppContext.this) throw new UnsupportedOperationException("not allow fire event '" + ec.eName + "'"); 138 | } 139 | return super.fire(ec, ls); 140 | } 141 | 142 | @Override 143 | public String toString() { return "coreEp"; } 144 | }; 145 | // 添加 ep 跟踪事件 146 | String track = getAttr("ep.track", String.class, null); 147 | if (track != null) { 148 | Arrays.stream(track.split(",")).filter(s -> s != null && !s.trim().isEmpty()).forEach(s -> ep.addTrackEvent(s.trim())); 149 | } 150 | ep.addListenerSource(AppContext.this); 151 | return ep; 152 | }); 153 | /** 154 | * 事件中心 155 | * @return {@link EP} 156 | */ 157 | public EP ep() { return _ep.get(); } 158 | 159 | /** 160 | * 环境属性配置.只支持properties文件, 支持${}属性替换 161 | * 加载顺序(优先级从小到大): 162 | * classpath:app.properties, classpath:app-[profile].properties 163 | * file:./app.properties, file:./app-[profile].properties 164 | * configdir:app.properties, configdir:app-[profile].properties 165 | * {@link #customEnv(Map)} 166 | * System.getProperties() 167 | */ 168 | private final Lazier> _env = new Lazier<>(() -> { 169 | final Map result = new ConcurrentHashMap<>(); // 结果属性集 170 | System.getProperties().forEach((k, v) -> result.put(k.toString(), v)); 171 | String configname = (String) result.getOrDefault("configname", "app");// 配置文件名. 默认app 172 | String profile = (String) result.get("profile"); 173 | String configdir = (String) result.get("configdir"); // 指定额外配置文件的目录 174 | 175 | //1. classpath 176 | try (InputStream is = getClass().getClassLoader().getResourceAsStream(configname + ".properties")) { 177 | if (is != null) { 178 | Properties p = new Properties(); 179 | p.load(new InputStreamReader(is, StandardCharsets.UTF_8)); 180 | p.forEach((k, v) -> result.put(k.toString(), v)); 181 | } 182 | } catch (IOException e) { 183 | log.error("Load classpath config file '" +configname + ".properties"+ "' error", e); 184 | } 185 | if (profile != null) { 186 | String fName = configname + "-" + profile + ".properties"; 187 | try (InputStream is = getClass().getClassLoader().getResourceAsStream(fName)) { 188 | if (is != null) { 189 | Properties p = new Properties(); 190 | p.load(new InputStreamReader(is, StandardCharsets.UTF_8)); 191 | p.forEach((k, v) -> result.put(k.toString(), v)); 192 | } 193 | } catch (IOException e) { 194 | log.error("Load classpath config file '" +fName+ "' error", e); 195 | } 196 | } 197 | //2. file:./ 198 | try (InputStream is = new FileInputStream(configname + ".properties")) { 199 | Properties p = new Properties(); 200 | p.load(new InputStreamReader(is, StandardCharsets.UTF_8)); 201 | p.forEach((k, v) -> result.put(k.toString(), v)); 202 | } catch (FileNotFoundException e) { 203 | log.trace("Load config file './" +configname + ".properties"+ "' not found"); 204 | } catch (IOException e) { 205 | log.error("Load config file './" +configname + ".properties"+ "' error", e); 206 | } 207 | if (profile != null) { 208 | String fName = configname + "-" + profile + ".properties"; 209 | try (InputStream is = new FileInputStream(fName)) { 210 | Properties p = new Properties(); 211 | p.load(new InputStreamReader(is, StandardCharsets.UTF_8)); 212 | p.forEach((k, v) -> result.put(k.toString(), v)); 213 | } catch (FileNotFoundException e) { 214 | log.trace("Load config file './" +fName+ "' not found"); 215 | } catch (IOException e) { 216 | log.error("Load config file './" +fName+ "' error", e); 217 | } 218 | } 219 | //3. configdir 220 | if (configdir != null) { 221 | File targetFile = new File(configdir, configname + ".properties"); 222 | try (InputStream is = new FileInputStream(targetFile)) { 223 | Properties p = new Properties(); 224 | p.load(new InputStreamReader(is, StandardCharsets.UTF_8)); 225 | p.forEach((k, v) -> result.put(k.toString(), v)); 226 | } catch (FileNotFoundException e) { 227 | log.trace("Load config file '" +targetFile.getAbsolutePath()+ "' not found"); 228 | } catch (IOException e) { 229 | log.error("Load config file '" +targetFile.getAbsolutePath()+ "' error", e); 230 | } 231 | if (profile != null) { 232 | targetFile = new File(configdir, configname + "-" + profile + ".properties"); 233 | try (InputStream is = new FileInputStream(targetFile)) { 234 | Properties p = new Properties(); 235 | p.load(new InputStreamReader(is, StandardCharsets.UTF_8)); 236 | p.forEach((k, v) -> result.put(k.toString(), v)); 237 | } catch (FileNotFoundException e) { 238 | log.trace("Load config file '" +targetFile.getAbsolutePath()+ "' not found"); 239 | } catch (IOException e) { 240 | log.error("Load config file '" +targetFile.getAbsolutePath()+ "' error", e); 241 | } 242 | } 243 | } 244 | customEnv(result); 245 | 246 | // 替换 ${} 247 | new Runnable() { 248 | final Pattern pattern = Pattern.compile("(\\$\\{(?[\\w\\._]+)\\})+"); 249 | final AtomicInteger count = new AtomicInteger(0); 250 | @Override 251 | public void run() { 252 | if (count.getAndIncrement() >= 3) return; 253 | boolean f = false; 254 | for (Map.Entry e : result.entrySet()) { 255 | if (e.getValue() == null) continue; 256 | Matcher m = pattern.matcher(e.getValue().toString()); 257 | if (!m.find()) continue; 258 | f = true; 259 | result.put(e.getKey(), e.getValue().toString().replace(m.group(0), result.getOrDefault(m.group("attr"), "").toString())); 260 | } 261 | if (f) run(); // 一直解析直到所有值都被替换完成 262 | } 263 | }.run(); 264 | 265 | System.getProperties().forEach((k, v) -> result.put(k.toString(), v)); 266 | return result; 267 | }); 268 | /** 269 | * 环境属性配置 270 | */ 271 | public Map env() { return _env.get(); } 272 | 273 | 274 | /** 275 | * 启动 276 | */ 277 | public AppContext start() { 278 | log.info("Starting Application with PID {}, active profile: {}", Utils.pid(), getProfile()); 279 | // 1. 初始化 280 | ep().fire(new EC("sys.inited", this)); 281 | // 2. 通知所有服务启动 282 | ep().fire(new EC("sys.starting", this).completeFn(ec -> { 283 | Runtime.getRuntime().addShutdownHook(shutdownHook); 284 | sourceMap.forEach((s, o) -> inject(o)); // 自动注入 285 | log.info("Started Application '{}' in {} seconds (JVM running for {})", name() + ":" + id(), (System.currentTimeMillis() - startup.getTime()) / 1000.0, ManagementFactory.getRuntimeMXBean().getUptime() / 1000.0); 286 | ep().fire(new EC("sys.started", this).completeFn(ec1 -> { 287 | Supplier nextTimeFn = () -> { 288 | Integer minInterval = getAttr("sys.heartbeat.minInterval", Integer.class, 60); 289 | Integer randomInterval = getAttr("sys.heartbeat.randomInterval", Integer.class, 120); 290 | return Duration.ofSeconds(minInterval + new Random().nextInt(randomInterval)); 291 | }; 292 | // 每隔一段时间触发一次心跳, 1~3分钟随机心跳 293 | final Runnable fn = new Runnable() { 294 | @Override 295 | public void run() { 296 | ep().fire(new EC("sys.heartbeat", this)); 297 | ep().fire("sched.after", nextTimeFn.get(), this); 298 | } 299 | }; 300 | fn.run(); 301 | })); 302 | })); 303 | return this; 304 | } 305 | 306 | 307 | /** 308 | * 添加对象源 309 | * {@link #ep} 会找出source对象中所有其暴露的功能. 即: 用 {@link EL} 标注的方法 310 | * 注: 为每个对象源都配一个 name 属性标识 311 | * @param source bean 对象 312 | * @param name bean 名字 313 | * @return {@link AppContext} 314 | */ 315 | public AppContext addSource(Object source, String name) { 316 | if (name == null || name.isEmpty()) throw new IllegalArgumentException("Param name required"); 317 | if (source == null) throw new IllegalArgumentException("Param source required"); 318 | if ("sys".equalsIgnoreCase(name) || "env".equalsIgnoreCase(name) || "log".equalsIgnoreCase(name) || "bean".equalsIgnoreCase(name)) { 319 | log.error("Name not allowed [sys, env, log, bean]. source: {}", source); return this; 320 | } 321 | if (sourceMap.containsKey(name)) { 322 | log.error("Already exist bean '{}': {}", name, sourceMap.get(name)); return this; 323 | } 324 | sourceMap.put(name, source); 325 | inject(source); ep().addListenerSource(source); 326 | return this; 327 | } 328 | 329 | 330 | /** 331 | * 添加对象源 332 | * {@link #ep} 会找出source对象中所有其暴露的功能. 即: 用 @EL 标注的方法 333 | * 注: 为每个对象源都配一个 name 属性标识 334 | * @param sources bean 对象 335 | * @return {@link AppContext} 336 | */ 337 | public AppContext addSource(Object... sources) { 338 | for (Object source : sources) { 339 | addSource(source, source instanceof ServerTpl ? ((ServerTpl) source).name : source.getClass().getName().contains("$") ? source.getClass().getName() : source.getClass().getSimpleName()); 340 | } 341 | return this; 342 | } 343 | 344 | 345 | /** 346 | * 加入到对列行器执行函数 347 | * 每个对列里面的函数同一时间只执行一个, 各对列相互执行互不影响 348 | * @param qName 对列名 349 | * @param fn 要执行的函数 350 | * @return {@link Devourer} 351 | */ 352 | public Devourer queue(String qName, Runnable fn) { 353 | if (qName == null || qName.isEmpty()) throw new IllegalArgumentException("Param qName required"); 354 | Devourer devourer = queues.get(qName); 355 | if (devourer == null) { 356 | synchronized (queues) { 357 | devourer = queues.get(qName); 358 | if (devourer == null) { 359 | devourer = new Devourer(qName, exec()); 360 | queues.put(qName, devourer); 361 | } 362 | } 363 | } 364 | if (fn != null) devourer.offer(fn); 365 | return devourer; 366 | } 367 | 368 | 369 | /** 370 | * 为bean对象中的{@link Inject}注解字段注入对应的bean对象 371 | * @param source bean 372 | */ 373 | @EL(name = "inject") 374 | public void inject(Object source) { 375 | Utils.iterateField(source.getClass(), (field) -> { 376 | Inject inject = field.getAnnotation(Inject.class); 377 | if (inject == null) return; 378 | try { 379 | field.setAccessible(true); 380 | Object v = field.get(source); 381 | if (v != null) return; // 已经存在值则不需要再注入 382 | 383 | // 取值 384 | if (EP.class.isAssignableFrom(field.getType())) v = wrapEpForSource(source); 385 | else { 386 | if (inject.name().isEmpty()) { 387 | v = bean(field.getType(), field.getName()); 388 | if (v == null) v = bean(field.getType(), null); 389 | } 390 | else { 391 | v = bean(field.getType(), inject.name()); 392 | } 393 | } 394 | 395 | if (v == null) return; 396 | field.set(source, v); 397 | log.trace("Inject field '{}' for object '{}'", field.getName(), source); 398 | } catch (Exception ex) { 399 | log.error("Inject field '" + field.getName() + "' error!", ex); 400 | } 401 | }); 402 | } 403 | 404 | 405 | /** 406 | * 全局查找 bean 对象 407 | * @param type 对象类型 408 | * @param name 对象名字 409 | * @return bean 410 | */ 411 | public T bean(Class type, String name) { 412 | return (T) ep().fire(new EC("bean.get", this).sync().args(type, name)); 413 | } 414 | 415 | 416 | /** 417 | * {@link #sourceMap}中查找对象 418 | * @param ec 事件上下文 419 | * @param bType bean 对象类型 420 | * @param bName bean 对象名字 421 | * @return bean 对象 422 | */ 423 | @EL(name = {"bean.get", "sys.bean.get"}, order = -1f) 424 | protected T localBean(EC ec, Class bType, String bName) { 425 | Object bean = null; 426 | if (bName != null && bType != null) { 427 | bean = sourceMap.get(bName); 428 | if (bean != null && !bType.isAssignableFrom(bean.getClass())) bean = null; 429 | } else if (bName != null && bType == null) { 430 | bean = sourceMap.get(bName); 431 | } else if (bName == null && bType != null) { 432 | if (Executor.class.isAssignableFrom(bType)) bean = wrapExecForSource(ec.source()); 433 | else if (AppContext.class.isAssignableFrom(bType)) bean = this; 434 | else if (EP.class.isAssignableFrom(bType)) bean = wrapEpForSource(ec.source()); 435 | else { 436 | for (Iterator> it = sourceMap.entrySet().iterator(); it.hasNext(); ) { 437 | Map.Entry e = it.next(); 438 | if (bType.isAssignableFrom(e.getValue().getClass())) { 439 | bean = e.getValue(); break; 440 | } 441 | } 442 | } 443 | } 444 | return (T) bean; 445 | } 446 | 447 | 448 | /** 449 | * 为 source 包装 Executor 450 | * 当系统线程池忙的时候, 会为创建服务创建一个独用的线程池: 451 | * 抵御流量突发,同时保证各个业务的任务隔离(即使流量突发也不会影响其他业务导致整个系统被拖垮), 452 | * 另外还可以抵御线程池隔离时各个业务设置不合理导致的资源分配不均,任务阻塞或者空转问题 453 | * 分发新任务策略: 当系统线程池忙, 则使用服务自己的线程池; 默认都用系统线程池 454 | * @param source 源对象 455 | * @return {@link Executor} 456 | */ 457 | protected Executor wrapExecForSource(Object source) { 458 | return new ExecutorService() { 459 | @Override 460 | public void shutdown() {} 461 | @Override 462 | public List shutdownNow() { return emptyList(); } 463 | @Override 464 | public boolean isShutdown() { return _exec.get().isShutdown(); } 465 | @Override 466 | public boolean isTerminated() { return _exec.get().isTerminated(); } 467 | @Override 468 | public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { 469 | return _exec.get().awaitTermination(timeout, unit); 470 | } 471 | @Override 472 | public Future submit(Callable task) { return _exec.get().submit(task); } 473 | @Override 474 | public Future submit(Runnable task, T result) { return _exec.get().submit(task, result); } 475 | @Override 476 | public Future submit(Runnable task) { return _exec.get().submit(task); } 477 | @Override 478 | public List> invokeAll(Collection> tasks) throws InterruptedException { 479 | return _exec.get().invokeAll(tasks); 480 | } 481 | @Override 482 | public List> invokeAll(Collection> tasks, long timeout, TimeUnit unit) throws InterruptedException { 483 | return _exec.get().invokeAll(tasks, timeout, unit); 484 | } 485 | @Override 486 | public T invokeAny(Collection> tasks) throws InterruptedException, ExecutionException { 487 | return _exec.get().invokeAny(tasks); 488 | } 489 | @Override 490 | public T invokeAny(Collection> tasks, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { 491 | return _exec.get().invokeAny(tasks, timeout, unit); 492 | } 493 | @Override 494 | public void execute(Runnable cmd) { _exec.get().execute(cmd); } 495 | public int getCorePoolSize() { return _exec.done() ? _exec.get().getCorePoolSize() : 0; } 496 | public int getMaximumPoolSize() { return _exec.done() ? _exec.get().getMaximumPoolSize() : 0; } 497 | public int getWaitingCount() { return _exec.done() ? _exec.get().getQueue().size() : 0; } 498 | 499 | @Override 500 | public String toString() { return _exec.done() ? _exec.get().toString() : "uninitialized"; } 501 | }; 502 | } 503 | 504 | 505 | /** 506 | * 为每个Source包装EP 507 | * @param source 源对象 508 | * @return {@link EP} 509 | */ 510 | protected EP wrapEpForSource(Object source) { 511 | return new EP() { 512 | @Override 513 | protected void init(Executor exec, Logger log) {} 514 | @Override 515 | public EP addTrackEvent(String... eNames) { ep().addTrackEvent(eNames); return this; } 516 | @Override 517 | public EP delTrackEvent(String... eNames) { ep().delTrackEvent(eNames); return this; } 518 | @Override 519 | public EP removeEvent(String eName, Object s) { 520 | if (source != null && s != null && source != s) throw new UnsupportedOperationException("Only allow remove event of this source: " + source); 521 | ep().removeEvent(eName, s); return this; 522 | } 523 | @Override 524 | public EP addListenerSource(Object s) { ep().addListenerSource(s); return this; } 525 | @Override 526 | public EP listen(String eName, boolean async, float order, int limit, Runnable fn) { return ep().listen(eName, async, order, limit, fn); } 527 | @Override 528 | public EP listen(String eName, boolean async, float order, int limit, Function fn) { return ep().listen(eName, async, order, limit, fn); } 529 | @Override 530 | public EP listen(String eName, boolean async, float order, int limit, BiFunction fn) { return ep().listen(eName, async, order,limit, fn); } 531 | @Override 532 | public boolean exist(String... eNames) { return ep().exist(eNames); } 533 | @Override 534 | public Object fire(EC ec) { return ep().fire(ec); } 535 | @Override 536 | public Object fire(EC ec, List ls) { 537 | if (ec.source() == null) ec.source(source); 538 | return ep().fire(ec, ls); 539 | } 540 | @Override 541 | public String toString() { return "wrappedCoreEp: " + source; } 542 | }; 543 | } 544 | 545 | 546 | /** 547 | * 额外自定义环境配置 548 | * @param already 已加载的属性集 549 | */ 550 | protected void customEnv(Map already) { } 551 | 552 | 553 | /** 554 | * 属性集:一组属性 555 | * @return 属性集 556 | */ 557 | public Map attrs(String key) { 558 | Map result = new ConcurrentHashMap<>(); 559 | for (Map.Entry entry : env().entrySet()) { 560 | if (entry.getKey().startsWith(key + ".")) { 561 | result.put(entry.getKey().replace(key + ".", ""), entry.getValue()); 562 | } 563 | } 564 | return result; 565 | } 566 | 567 | 568 | /** 569 | * 获取属性 570 | * @param key 属性key 571 | * @param type 值类型 572 | * @param defaultValue 默认值 573 | * @return 属性值 574 | */ 575 | public T getAttr(String key, Class type, T defaultValue) { 576 | T v = Utils.to(env().get(key), type); 577 | if (v == null) return defaultValue; 578 | return v; 579 | } 580 | 581 | 582 | /** 583 | * get profile 584 | */ 585 | public String getProfile() { return (String) env().get("profile"); } 586 | 587 | 588 | /** 589 | * 系统名字. 用于多个系统启动区别 590 | */ 591 | protected final Lazier _name = new Lazier<>(() -> getAttr("sys.name", String.class, "tiny")); 592 | public String name() { return _name.get(); } 593 | 594 | 595 | /** 596 | * 实例Id 597 | * NOTE: 保证唯一 598 | */ 599 | protected final Lazier _id = new Lazier<>(() -> getAttr("sys.id", String.class, Utils.nanoId())); 600 | public String id() { return _id.get(); } 601 | } --------------------------------------------------------------------------------