From 848e8c50739b1bf0894bdfd931ee45246ebce411 Mon Sep 17 00:00:00 2001 From: sttk Date: Sun, 27 Apr 2025 18:21:27 +0900 Subject: [PATCH 1/2] new: added exception notification feature --- pom.xml | 12 +- src/main/java/com/github/sttk/errs/Exc.java | 103 ++++++++++++- .../java/com/github/sttk/errs/ExcHandler.java | 24 +++ .../com/github/sttk/errs/package-info.java | 4 +- src/main/java/module-info.java | 1 + .../com/github/sttk/errs/ExcHandlerTest.java | 140 ++++++++++++++++++ .../java/com/github/sttk/errs/ExcTest.java | 8 +- 7 files changed, 280 insertions(+), 12 deletions(-) create mode 100644 src/main/java/com/github/sttk/errs/ExcHandler.java create mode 100644 src/test/java/com/github/sttk/errs/ExcHandlerTest.java diff --git a/pom.xml b/pom.xml index a3566aa..6d0b203 100644 --- a/pom.xml +++ b/pom.xml @@ -151,11 +151,14 @@ org.apache.maven.plugins maven-surefire-plugin 3.5.3 + + @{argLine} -Dgithub.sttk.errs.notify=true + - net.revelc.code.formatter - formatter-maven-plugin - 2.26.0 + net.revelc.code.formatter + formatter-maven-plugin + 2.26.0 @@ -187,6 +190,9 @@ --initialize-at-build-time=org.junit.platform.launcher.core.LauncherConfig --initialize-at-build-time=org.junit.jupiter.engine.config.InstantiatingConfigurationParameterConverter + + true + diff --git a/src/main/java/com/github/sttk/errs/Exc.java b/src/main/java/com/github/sttk/errs/Exc.java index 95f4e4b..97676af 100644 --- a/src/main/java/com/github/sttk/errs/Exc.java +++ b/src/main/java/com/github/sttk/errs/Exc.java @@ -4,12 +4,16 @@ */ package com.github.sttk.errs; +import java.lang.management.ManagementFactory; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.io.IOException; import java.io.NotSerializableException; import java.io.InvalidObjectException; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.LinkedList; /** * Is the exception class with a reason. @@ -17,7 +21,9 @@ * This class has a record field which indicates a reason for this exception. The class name of the reason record * represents the type of reason, and the fields of the reason record hold the situation where the exception occurred. *

- * Optionally, this exception class can notify its instance creation to pre-registered exception handlers. + * Optionally, this exception class can notify its instance creation to pre-registered exception handlers. This + * notification feature can be enabled by specifying the system property {@code -Dgithub.sttk.errs.notify=true} when the + * JVM is started. *

* The example code of creating and throwing an excepton is as follows: * @@ -27,7 +33,7 @@ * * try { * throw new Exc(new FailToDoSomething("abc", 123)); - * } catch (Err e) { + * } catch (Exc e) { * System.out.println(e.getMessage()); // => "FailToDoSomething { name=abc, value=123 }" * } * } @@ -56,6 +62,8 @@ public Exc(final Record reason) { this.reason = reason; this.trace = getStackTrace()[0]; + + notifyExc(this); } /** @@ -77,6 +85,8 @@ public Exc(final Record reason, final Throwable cause) { this.reason = reason; this.trace = getStackTrace()[0]; + + notifyExc(this); } /** @@ -194,6 +204,95 @@ private void readObject(ObjectInputStream in) throws ClassNotFoundException, IOE throw new InvalidObjectException("reason is null or invalid."); } } + + //// Notification //// + + private static final boolean useNotification; + + static { + boolean b = false; + for (String arg : ManagementFactory.getRuntimeMXBean().getInputArguments()) { + if ("-Dgithub.sttk.errs.notify=true".equals(arg)) { + b = true; + break; + } + } + useNotification = b; + } + + private static boolean isFixed = false; + private static final List syncExcHandlers = new LinkedList<>(); + private static final List asyncExcHandlers = new LinkedList<>(); + + /** + * Adds an {@link ExcHandler} object which is executed synchronously just after an {@link Exc} is created. Handlers + * added with this method are executed in the order of addition and stop if one of the handlers throws a + * {@link RuntimeException} or an {@link Error}. NOTE: This feature is enabled via the system property: + * {@code github.sttk.errs.notify=true} + * + * @param handler + * An {@link ExcHandler} object. + */ + public static void addSyncHandler(final ExcHandler handler) { + if (!useNotification) + return; + if (isFixed) + return; + syncExcHandlers.add(handler); + } + + /** + * Adds an {@link ExcHandler} object which is executed asynchronously just after an {@link Exc} is created. Handlers + * don't stop even if one of the handlers throw a {@link RuntimeException} or an {@link Error}. NOTE: This feature + * is enabled via the system property: {@code github.sttk.errs.notify=true} + * + * @param handler + * An {@link ExcHandler} object. + */ + public static void addAsyncHandler(final ExcHandler handler) { + if (!useNotification) + return; + if (isFixed) + return; + asyncExcHandlers.add(handler); + } + + /** + * Prevents further addition of {@link ExcHandler} objects to synchronous and asynchronous exception handler lists. + * Before this is called, no {@code Exc} is notified to the handlers. After this is called, no new handlers can be + * added, and {@code Exc}(s) is notified to the handlers. NOTE: This feature is enabled via the system property: + * {@code github.sttk.errs.notify=true} + */ + public static void fixHandlers() { + if (!useNotification) + return; + if (isFixed) + return; + isFixed = true; + } + + private static void notifyExc(Exc exc) { + if (!useNotification) + return; + if (!isFixed) + return; + + if (syncExcHandlers.isEmpty() && asyncExcHandlers.isEmpty()) { + return; + } + + final var tm = OffsetDateTime.now(); + + for (var handler : syncExcHandlers) { + handler.handle(exc, tm); + } + + for (var handler : asyncExcHandlers) { + Thread.ofVirtual().start(() -> { + handler.handle(exc, tm); + }); + } + } } final class RuntimeExc extends RuntimeException { diff --git a/src/main/java/com/github/sttk/errs/ExcHandler.java b/src/main/java/com/github/sttk/errs/ExcHandler.java new file mode 100644 index 0000000..e3e5278 --- /dev/null +++ b/src/main/java/com/github/sttk/errs/ExcHandler.java @@ -0,0 +1,24 @@ +/* + * ExcHandler class. + * Copyright (C) 2025 Takayuki Sato. All Rights Reserved. + */ +package com.github.sttk.errs; + +import java.time.OffsetDateTime; + +/** + * {@code ExcHandler} is a handler of an {@link Exc} object creation. + */ +@FunctionalInterface +public interface ExcHandler { + + /** + * Handles an {@link Exc} object creation. + * + * @param exc + * The {@link Exc} object. + * @param tm + * The creation time of the {@link Exc} object. + */ + void handle(Exc exc, OffsetDateTime tm); +} diff --git a/src/main/java/com/github/sttk/errs/package-info.java b/src/main/java/com/github/sttk/errs/package-info.java index cd6dfd3..4e00300 100644 --- a/src/main/java/com/github/sttk/errs/package-info.java +++ b/src/main/java/com/github/sttk/errs/package-info.java @@ -1,8 +1,8 @@ /* * Copyright (C) 2024 Takayuki Sato. All Rights Reserved. * - * This program is free software under MIT License. - * See the file LICENSE in this distribution for more details. + * This program is free software under MIT License. See the file LICENSE in this distribution for + * more details. */ /** diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index d82c48d..c6c6ade 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -12,4 +12,5 @@ */ module com.github.sttk.errs { exports com.github.sttk.errs; + requires java.management; } diff --git a/src/test/java/com/github/sttk/errs/ExcHandlerTest.java b/src/test/java/com/github/sttk/errs/ExcHandlerTest.java new file mode 100644 index 0000000..41d3020 --- /dev/null +++ b/src/test/java/com/github/sttk/errs/ExcHandlerTest.java @@ -0,0 +1,140 @@ +package com.github.sttk.errs; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.fail; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeEach; + +import static java.time.format.DateTimeFormatter.ISO_INSTANT; +import java.util.List; +import java.util.LinkedList; + +public class ExcHandlerTest { + private ExcHandlerTest() { + } + + @BeforeEach + void reset() throws Exception { + var f = Exc.class.getDeclaredField("isFixed"); + f.setAccessible(true); + f.setBoolean(null, false); + + f = Exc.class.getDeclaredField("syncExcHandlers"); + f.setAccessible(true); + var o = f.get(null); + var m = LinkedList.class.getMethod("clear"); + m.invoke(o); + + f = Exc.class.getDeclaredField("asyncExcHandlers"); + f.setAccessible(true); + o = f.get(null); + m = LinkedList.class.getMethod("clear"); + m.invoke(o); + } + + @SuppressWarnings("unchecked") + List getSyncExcHandlers() throws Exception { + var f = Exc.class.getDeclaredField("syncExcHandlers"); + f.setAccessible(true); + var o = f.get(null); + return (List) o; + } + + @SuppressWarnings("unchecked") + List getAsyncExcHandlers() throws Exception { + var f = Exc.class.getDeclaredField("asyncExcHandlers"); + f.setAccessible(true); + var o = f.get(null); + return (List) o; + } + + @Test + void should_add_sync_handlers_and_fix() throws Exception { + var handlers = getSyncExcHandlers(); + assertThat(handlers).isEmpty(); + + ExcHandler handler1 = (exc, tm) -> { + }; + Exc.addSyncHandler(handler1); + + handlers = getSyncExcHandlers(); + assertThat(handlers).containsExactly(handler1); + + ExcHandler handler2 = (exc, tm) -> { + }; + Exc.addSyncHandler(handler2); + + handlers = getSyncExcHandlers(); + assertThat(handlers).containsExactly(handler1, handler2); + + Exc.fixHandlers(); + + ExcHandler handler3 = (exc, tm) -> { + }; + Exc.addSyncHandler(handler3); + + handlers = getSyncExcHandlers(); + assertThat(handlers).containsExactly(handler1, handler2); + } + + @Test + void should_add_async_handlers_and_fix() throws Exception { + var handlers = getAsyncExcHandlers(); + assertThat(handlers).isEmpty(); + + ExcHandler handler1 = (exc, tm) -> { + }; + Exc.addAsyncHandler(handler1); + + handlers = getAsyncExcHandlers(); + assertThat(handlers).containsExactly(handler1); + + ExcHandler handler2 = (exc, tm) -> { + }; + Exc.addAsyncHandler(handler2); + + handlers = getAsyncExcHandlers(); + assertThat(handlers).containsExactly(handler1, handler2); + + Exc.fixHandlers(); + + ExcHandler handler3 = (exc, tm) -> { + }; + Exc.addAsyncHandler(handler3); + + handlers = getAsyncExcHandlers(); + assertThat(handlers).containsExactly(handler1, handler2); + } + + @Test + void should_notify_exception() throws Exception { + final List syncLogs = new LinkedList<>(); + final List asyncLogs = new LinkedList<>(); + + Exc.addSyncHandler((exc, tm) -> { + syncLogs.add(String.format("%s:%s(%d):%s", tm.format(ISO_INSTANT), exc.getFile(), exc.getLine(), + exc.getReason().toString())); + }); + Exc.addAsyncHandler((exc, tm) -> { + asyncLogs.add(String.format("%s:%s(%d):%s", tm.format(ISO_INSTANT), exc.getFile(), exc.getLine(), + exc.getReason().toString())); + }); + + record FailToDoSomething(String name) { + } + + new Exc(new FailToDoSomething("abc")); + + assertThat(syncLogs).isEmpty(); + assertThat(asyncLogs).isEmpty(); + + Exc.fixHandlers(); + + new Exc(new FailToDoSomething("abc")); + assertThat(syncLogs.get(0)).endsWith(":ExcHandlerTest.java(134):FailToDoSomething[name=abc]"); + + Thread.sleep(100); + assertThat(asyncLogs.get(0)).endsWith(":ExcHandlerTest.java(134):FailToDoSomething[name=abc]"); + } +} diff --git a/src/test/java/com/github/sttk/errs/ExcTest.java b/src/test/java/com/github/sttk/errs/ExcTest.java index 350c7ce..609bb99 100644 --- a/src/test/java/com/github/sttk/errs/ExcTest.java +++ b/src/test/java/com/github/sttk/errs/ExcTest.java @@ -2,10 +2,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.fail; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.DisabledInNativeImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -153,7 +151,7 @@ void getFile() { @Test void getLine() { var exc = new Exc(new IndexOutOfRange("data", 4, 0, 3)); - assertThat(exc.getLine()).isEqualTo(155); + assertThat(exc.getLine()).isEqualTo(153); } } @@ -181,7 +179,7 @@ class TestToString { void with_reason() { var exc = new Exc(new IndexOutOfRange("data", 4, 0, 3)); assertThat(exc.toString()).isEqualTo( - "com.github.sttk.errs.Exc { reason = com.github.sttk.errs.ExcTest$IndexOutOfRange { name=data, index=4, min=0, max=3 }, file = ExcTest.java, line = 182 }"); + "com.github.sttk.errs.Exc { reason = com.github.sttk.errs.ExcTest$IndexOutOfRange { name=data, index=4, min=0, max=3 }, file = ExcTest.java, line = 180 }"); } @Test @@ -189,7 +187,7 @@ void with_reason_and_cause() { var cause = new IndexOutOfBoundsException(4); var exc = new Exc(new IndexOutOfRange("data", 4, 0, 3), cause); assertThat(exc.toString()).isEqualTo( - "com.github.sttk.errs.Exc { reason = com.github.sttk.errs.ExcTest$IndexOutOfRange { name=data, index=4, min=0, max=3 }, file = ExcTest.java, line = 190, cause = java.lang.IndexOutOfBoundsException: Index out of range: 4 }"); + "com.github.sttk.errs.Exc { reason = com.github.sttk.errs.ExcTest$IndexOutOfRange { name=data, index=4, min=0, max=3 }, file = ExcTest.java, line = 188, cause = java.lang.IndexOutOfBoundsException: Index out of range: 4 }"); } } From f2a25a1290917e89552af4594bae24099a1fc070 Mon Sep 17 00:00:00 2001 From: sttk Date: Sun, 27 Apr 2025 18:34:52 +0900 Subject: [PATCH 2/2] fix: rename isFixed flag to isHandlersFixed --- src/main/java/com/github/sttk/errs/Exc.java | 12 ++++++------ .../java/com/github/sttk/errs/ExcHandlerTest.java | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/github/sttk/errs/Exc.java b/src/main/java/com/github/sttk/errs/Exc.java index 97676af..4698fc6 100644 --- a/src/main/java/com/github/sttk/errs/Exc.java +++ b/src/main/java/com/github/sttk/errs/Exc.java @@ -220,7 +220,7 @@ private void readObject(ObjectInputStream in) throws ClassNotFoundException, IOE useNotification = b; } - private static boolean isFixed = false; + private static boolean isHandlersFixed = false; private static final List syncExcHandlers = new LinkedList<>(); private static final List asyncExcHandlers = new LinkedList<>(); @@ -236,7 +236,7 @@ private void readObject(ObjectInputStream in) throws ClassNotFoundException, IOE public static void addSyncHandler(final ExcHandler handler) { if (!useNotification) return; - if (isFixed) + if (isHandlersFixed) return; syncExcHandlers.add(handler); } @@ -252,7 +252,7 @@ public static void addSyncHandler(final ExcHandler handler) { public static void addAsyncHandler(final ExcHandler handler) { if (!useNotification) return; - if (isFixed) + if (isHandlersFixed) return; asyncExcHandlers.add(handler); } @@ -266,15 +266,15 @@ public static void addAsyncHandler(final ExcHandler handler) { public static void fixHandlers() { if (!useNotification) return; - if (isFixed) + if (isHandlersFixed) return; - isFixed = true; + isHandlersFixed = true; } private static void notifyExc(Exc exc) { if (!useNotification) return; - if (!isFixed) + if (!isHandlersFixed) return; if (syncExcHandlers.isEmpty() && asyncExcHandlers.isEmpty()) { diff --git a/src/test/java/com/github/sttk/errs/ExcHandlerTest.java b/src/test/java/com/github/sttk/errs/ExcHandlerTest.java index 41d3020..ac1ec98 100644 --- a/src/test/java/com/github/sttk/errs/ExcHandlerTest.java +++ b/src/test/java/com/github/sttk/errs/ExcHandlerTest.java @@ -16,7 +16,7 @@ private ExcHandlerTest() { @BeforeEach void reset() throws Exception { - var f = Exc.class.getDeclaredField("isFixed"); + var f = Exc.class.getDeclaredField("isHandlersFixed"); f.setAccessible(true); f.setBoolean(null, false);