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..4698fc6 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 isHandlersFixed = 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 (isHandlersFixed) + 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 (isHandlersFixed) + 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 (isHandlersFixed) + return; + isHandlersFixed = true; + } + + private static void notifyExc(Exc exc) { + if (!useNotification) + return; + if (!isHandlersFixed) + 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..ac1ec98 --- /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("isHandlersFixed"); + 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 }"); } }