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 }");
}
}