diff --git a/src/main/java/pro/fessional/mirana/dync/OrderedSpi.java b/src/main/java/pro/fessional/mirana/dync/OrderedSpi.java new file mode 100644 index 0000000..28cc795 --- /dev/null +++ b/src/main/java/pro/fessional/mirana/dync/OrderedSpi.java @@ -0,0 +1,73 @@ +package pro.fessional.mirana.dync; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Iterator; +import java.util.ServiceLoader; + +/** + * order and get 1st service + * + * @author trydofor + * @since 2024-02-16 + */ +public interface OrderedSpi { + + @Nullable + static S first(@NotNull Class service) { + return first(service, (Comparator) null); + } + + @Nullable + static S first(@NotNull Class service, @NotNull ClassLoader loader) { + return first(service, loader, null); + } + + @Nullable + static S first(@NotNull Class service, @Nullable Comparator comparator) { + return first(ServiceLoader.load(service), comparator); + } + + @Nullable + static S first(@NotNull Class service, @NotNull ClassLoader loader, @Nullable Comparator comparator) { + return first(ServiceLoader.load(service, loader), comparator); + } + + @Nullable + static S first(@NotNull ServiceLoader loader, @Nullable Comparator comparator) { + S cur = null; + if (comparator == null) { + Iterator it = loader.iterator(); + if (it.hasNext()) { + cur = it.next(); + } + + if (cur instanceof Comparable) { // not null, not empty + ArrayList arr = new ArrayList<>(); + arr.add(cur); + while (it.hasNext()) { + arr.add(it.next()); + } + + arr.sort(null); + cur = arr.get(0); + } + } + else { + ArrayList arr = new ArrayList<>(); + for (S s : loader) { + arr.add(s); + } + + if (!arr.isEmpty()) { + arr.sort(comparator); + cur = arr.get(0); + } + } + + return cur; + } +} diff --git a/src/main/java/pro/fessional/mirana/evil/ThreadLocalProvider.java b/src/main/java/pro/fessional/mirana/evil/ThreadLocalProvider.java new file mode 100644 index 0000000..4226dff --- /dev/null +++ b/src/main/java/pro/fessional/mirana/evil/ThreadLocalProvider.java @@ -0,0 +1,22 @@ +package pro.fessional.mirana.evil; + +import org.jetbrains.annotations.NotNull; + +/** + * ThreadLocal without init value. + * + * @author trydofor + * @since 2024-02-16 + */ +public interface ThreadLocalProvider extends Comparable { + default int getOrder() { + return 0; + } + + @Override + default int compareTo(@NotNull ThreadLocalProvider o) { + return Integer.compare(this.getOrder(), o.getOrder()); + } + + @NotNull ThreadLocal get(); +} diff --git a/src/main/java/pro/fessional/mirana/evil/ThreadLocalProxy.java b/src/main/java/pro/fessional/mirana/evil/ThreadLocalProxy.java deleted file mode 100644 index 5d6eb7c..0000000 --- a/src/main/java/pro/fessional/mirana/evil/ThreadLocalProxy.java +++ /dev/null @@ -1,108 +0,0 @@ -package pro.fessional.mirana.evil; - -import org.jetbrains.annotations.NotNull; - -import java.lang.reflect.Field; -import java.lang.reflect.Method; - -/** - * Used only for static scenarios, which can be replaced after startup to avoid memory leaks. - * - * @author trydofor - * @since 2022-10-26 - */ -public class ThreadLocalProxy extends ThreadLocal { - - /** - * try to clear the values in threadLocal by reflection. - * WARNING: An illegal reflective access operation has occurred - */ - public static void tryClear(ThreadLocal threadLocal) throws ThreadLocalAttention { - if (threadLocal == null) return; - // try to clean by reflect - try { - // BGN copy from apache ThreadUtils#getAllThreads - ThreadGroup systemGroup = Thread.currentThread().getThreadGroup(); - while (systemGroup.getParent() != null) { - systemGroup = systemGroup.getParent(); - } - int count = systemGroup.activeCount(); - Thread[] threads; - do { - threads = new Thread[count + (count / 2) + 1]; //slightly grow the array size - count = systemGroup.enumerate(threads, true); - //return value of enumerate() must be strictly less than the array size according to javadoc - } while (count >= threads.length); - // END - - // clear the values in threadLocal by reflection. - final Field threadLocalsField = Thread.class.getDeclaredField("threadLocals"); - threadLocalsField.setAccessible(true); - - Class threadLocalMapClass = Class.forName("java.lang.ThreadLocal$ThreadLocalMap"); - Method removeMethod = threadLocalMapClass.getDeclaredMethod("remove", ThreadLocal.class); - removeMethod.setAccessible(true); - - for (int i = 0; i < count; i++) { - final Object threadLocalMap = threadLocalsField.get(threads[i]); - if (threadLocalMap != null) { - removeMethod.invoke(threadLocalMap, threadLocal); - } - } - } - catch (ReflectiveOperationException e) { - throw new ThreadLocalAttention(e); - } - } - - private volatile ThreadLocal backend; - - public ThreadLocalProxy() { - this.backend = new ThreadLocal<>(); - } - - public ThreadLocalProxy(@NotNull ThreadLocal tl) { - this.backend = tl; - } - - @NotNull - public ThreadLocal getBackend() { - return backend; - } - - /** - *
-     * change the internal ThreadLocal impl, return the old value. do NOT change it more than once.
-     * tryToCleanOld == true, will log
-     * WARNING: An illegal reflective access operation has occurred
-     *
-     * use tryToCleanOld=false if before starting any service or a few memory is in use.
-     * 
- */ - @NotNull - public ThreadLocal replaceBackend(@NotNull ThreadLocal threadLocal, boolean tryToCleanOld) throws ThreadLocalAttention { - final ThreadLocal old = backend; - backend = threadLocal; - if (tryToCleanOld) { - // Replace the value first, then clear. - // Clear does not affect the use of new values - tryClear(old); - } - return old; - } - - @Override - public T get() { - return backend.get(); - } - - @Override - public void set(T value) { - backend.set(value); - } - - @Override - public void remove() { - backend.remove(); - } -} diff --git a/src/main/java/pro/fessional/mirana/evil/TweakingContext.java b/src/main/java/pro/fessional/mirana/evil/TweakingContext.java index 526d69d..9aa9f5b 100644 --- a/src/main/java/pro/fessional/mirana/evil/TweakingContext.java +++ b/src/main/java/pro/fessional/mirana/evil/TweakingContext.java @@ -1,14 +1,14 @@ package pro.fessional.mirana.evil; import org.jetbrains.annotations.Contract; -import org.jetbrains.annotations.NotNull; +import pro.fessional.mirana.dync.OrderedSpi; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; /** *
- * init - should init before the service and called once.
+ * default - should init before the service and called once.
  * global - global scope, should be used at the system level.
  * thread - thread scope, should use try {tweak} finally{reset} pattern.
  * 
@@ -20,18 +20,22 @@ public class TweakingContext { private final AtomicReference> defaultValue = new AtomicReference<>(); private final AtomicReference> globalValue = new AtomicReference<>(); - private final ThreadLocalProxy> threadValue = new ThreadLocalProxy<>(); + private final ThreadLocal> threadValue; /** * without default value */ + @SuppressWarnings("unchecked") public TweakingContext() { + ThreadLocalProvider spi = OrderedSpi.first(ThreadLocalProvider.class); + threadValue = spi == null ? new ThreadLocal<>() : (ThreadLocal>) spi.get(); } /** * init with default value */ public TweakingContext(T initDefault) { + this(); initDefault(initDefault); } @@ -39,6 +43,7 @@ public TweakingContext(T initDefault) { * init with default value */ public TweakingContext(Supplier initDefault) { + this(); initDefault(initDefault); } @@ -73,22 +78,29 @@ public void initGlobal(Supplier value) { /** * init thread value */ - public void initThread(@NotNull ThreadLocal> threadLocal, boolean tryToCleanOld) throws ThreadLocalAttention { - threadValue.replaceBackend(threadLocal, tryToCleanOld); + public void initThread(T value) { + tweakThread(value); + } + + /** + * init thread value + */ + public void initThread(Supplier value) { + tweakThread(value); } /** * tweak global value */ - public void tweakGlobal(T stack) { - globalValue.set(() -> stack); + public void tweakGlobal(T value) { + globalValue.set(() -> value); } /** * tweak global value */ - public void tweakGlobal(Supplier stack) { - globalValue.set(stack); + public void tweakGlobal(Supplier value) { + globalValue.set(value); } /** @@ -101,25 +113,15 @@ public void resetGlobal() { /** * tweak thread value. should use try {tweak} finally{reset} pattern */ - public void tweakThread(T stack) { - if (stack == null) { - threadValue.remove(); - } - else { - threadValue.set(() -> stack); - } + public void tweakThread(T value) { + threadValue.set(() -> value); } /** * tweak thread value. should use try {tweak} finally{reset} pattern */ - public void tweakThread(Supplier stack) { - if (stack == null) { - threadValue.remove(); - } - else { - threadValue.set(stack); - } + public void tweakThread(Supplier value) { + threadValue.set(value); } /** diff --git a/src/test/java/pro/fessional/mirana/dync/OrderedSpiTest.java b/src/test/java/pro/fessional/mirana/dync/OrderedSpiTest.java new file mode 100644 index 0000000..a006c60 --- /dev/null +++ b/src/test/java/pro/fessional/mirana/dync/OrderedSpiTest.java @@ -0,0 +1,23 @@ +package pro.fessional.mirana.dync; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Arrays; + +/** + * @author trydofor + * @since 2024-02-17 + */ +class OrderedSpiTest { + + @Test + void orderNull() { + ArrayList arr = new ArrayList<>(); + arr.add(2); + arr.add(1); + arr.sort(null); + Assertions.assertEquals(Arrays.asList(1,2), arr); + } +} \ No newline at end of file diff --git a/src/test/java/pro/fessional/mirana/evil/TestThreadLocalProvider1.java b/src/test/java/pro/fessional/mirana/evil/TestThreadLocalProvider1.java new file mode 100644 index 0000000..43da593 --- /dev/null +++ b/src/test/java/pro/fessional/mirana/evil/TestThreadLocalProvider1.java @@ -0,0 +1,20 @@ +package pro.fessional.mirana.evil; + +import org.jetbrains.annotations.NotNull; + +/** + * @author trydofor + * @since 2024-02-16 + */ +public class TestThreadLocalProvider1 implements ThreadLocalProvider { + + @Override + public int getOrder() { + return 1; + } + + @Override + public @NotNull ThreadLocal get() { + return new ThreadLocal<>(); + } +} diff --git a/src/test/java/pro/fessional/mirana/evil/TestThreadLocalProvider2.java b/src/test/java/pro/fessional/mirana/evil/TestThreadLocalProvider2.java new file mode 100644 index 0000000..dc9da77 --- /dev/null +++ b/src/test/java/pro/fessional/mirana/evil/TestThreadLocalProvider2.java @@ -0,0 +1,19 @@ +package pro.fessional.mirana.evil; + +import org.jetbrains.annotations.NotNull; + +/** + * @author trydofor + * @since 2024-02-16 + */ +public class TestThreadLocalProvider2 implements ThreadLocalProvider { + @Override + public int getOrder() { + return 2; + } + + @Override + public @NotNull ThreadLocal get() { + return new ThreadLocal<>(); + } +} \ No newline at end of file diff --git a/src/test/java/pro/fessional/mirana/evil/TestThreadLocalProvider3.java b/src/test/java/pro/fessional/mirana/evil/TestThreadLocalProvider3.java new file mode 100644 index 0000000..67c7951 --- /dev/null +++ b/src/test/java/pro/fessional/mirana/evil/TestThreadLocalProvider3.java @@ -0,0 +1,19 @@ +package pro.fessional.mirana.evil; + +import org.jetbrains.annotations.NotNull; + +/** + * @author trydofor + * @since 2024-02-16 + */ +public class TestThreadLocalProvider3 implements ThreadLocalProvider { + @Override + public int getOrder() { + return 3; + } + + @Override + public @NotNull ThreadLocal get() { + return new ThreadLocal<>(); + } +} \ No newline at end of file diff --git a/src/test/java/pro/fessional/mirana/evil/ThreadLocalProviderTest.java b/src/test/java/pro/fessional/mirana/evil/ThreadLocalProviderTest.java new file mode 100644 index 0000000..68ed2a2 --- /dev/null +++ b/src/test/java/pro/fessional/mirana/evil/ThreadLocalProviderTest.java @@ -0,0 +1,34 @@ +package pro.fessional.mirana.evil; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import pro.fessional.mirana.dync.OrderedSpi; + +import java.util.ServiceLoader; + +/** + * @author trydofor + * @since 2024-02-16 + */ +class ThreadLocalProviderTest { + + @Test + void spi() { + ServiceLoader loader = ServiceLoader.load(ThreadLocalProvider.class); + Assertions.assertInstanceOf(TestThreadLocalProvider2.class, loader.iterator().next()); + + ThreadLocalProvider c1 = OrderedSpi.first(ThreadLocalProvider.class); + Assertions.assertInstanceOf(TestThreadLocalProvider1.class, c1); + + ThreadLocalProvider c2 = OrderedSpi.first(ThreadLocalProvider.class, ClassLoader.getSystemClassLoader()); + Assertions.assertInstanceOf(TestThreadLocalProvider1.class, c2); + + ThreadLocalProvider c3 = OrderedSpi.first(ThreadLocalProvider.class, (o1, o2) -> { + if (o1.getClass() == TestThreadLocalProvider3.class) return -1; + if (o2.getClass() == TestThreadLocalProvider3.class) return 1; + return o1.compareTo(o2); + }); + Assertions.assertInstanceOf(TestThreadLocalProvider3.class, c3); + + } +} \ No newline at end of file diff --git a/src/test/java/pro/fessional/mirana/evil/ThreadLocalProxyTest.java b/src/test/java/pro/fessional/mirana/evil/ThreadLocalProxyTest.java deleted file mode 100644 index d8277bd..0000000 --- a/src/test/java/pro/fessional/mirana/evil/ThreadLocalProxyTest.java +++ /dev/null @@ -1,37 +0,0 @@ -package pro.fessional.mirana.evil; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import pro.fessional.mirana.SystemOut; - -import java.util.concurrent.CountDownLatch; - -/** - * @author trydofor - * @since 2022-10-27 - */ -class ThreadLocalProxyTest { - - @Test - void replaceBackend() throws Exception { - final ThreadLocalProxy tlp = new ThreadLocalProxy<>(); - final CountDownLatch latch = new CountDownLatch(1); - for (int i = 0; i < 10; i++) { - new Thread(() -> { - try { - tlp.set(Boolean.TRUE); - Assertions.assertTrue(tlp.get()); - latch.await(); - Assertions.assertNull(tlp.get()); - } - catch (InterruptedException e) { - SystemOut.printStackTrace(e); - } - }).start(); - } - - Thread.sleep(1000); - tlp.replaceBackend(new ThreadLocal<>(), true); - latch.countDown(); - } -} diff --git a/src/test/java/pro/fessional/mirana/evil/TweakingContextTest.java b/src/test/java/pro/fessional/mirana/evil/TweakingContextTest.java index d49417f..1e891ef 100644 --- a/src/test/java/pro/fessional/mirana/evil/TweakingContextTest.java +++ b/src/test/java/pro/fessional/mirana/evil/TweakingContextTest.java @@ -2,6 +2,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import pro.fessional.mirana.data.Null; /** * @author trydofor @@ -10,7 +11,7 @@ class TweakingContextTest { @Test - void resetThread() throws ThreadLocalAttention { + void resetThread() { final TweakingContext ctx = new TweakingContext<>(() -> "d"); Assertions.assertEquals("d", ctx.defaultValue(false)); @@ -22,7 +23,7 @@ void resetThread() throws ThreadLocalAttention { Assertions.assertEquals("g", ctx.globalValue(false)); Assertions.assertEquals("g", ctx.current(true)); - ctx.initThread(new ThreadLocal<>(), true); + ctx.initThread(Null.Str); ctx.tweakThread(() -> "t"); Assertions.assertEquals("t", ctx.threadValue(true)); Assertions.assertEquals("t", ctx.current(true)); diff --git a/src/test/resources/META-INF/services/pro.fessional.mirana.evil.ThreadLocalProvider b/src/test/resources/META-INF/services/pro.fessional.mirana.evil.ThreadLocalProvider new file mode 100644 index 0000000..e244da6 --- /dev/null +++ b/src/test/resources/META-INF/services/pro.fessional.mirana.evil.ThreadLocalProvider @@ -0,0 +1,3 @@ +pro.fessional.mirana.evil.TestThreadLocalProvider2 +pro.fessional.mirana.evil.TestThreadLocalProvider3 +pro.fessional.mirana.evil.TestThreadLocalProvider1