Skip to content

Commit 4e7643b

Browse files
authored
new: added exception notification feature (#3)
1 parent 2021580 commit 4e7643b

7 files changed

Lines changed: 280 additions & 12 deletions

File tree

pom.xml

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,11 +151,14 @@
151151
<groupId>org.apache.maven.plugins</groupId>
152152
<artifactId>maven-surefire-plugin</artifactId>
153153
<version>3.5.3</version>
154+
<configuration>
155+
<argLine>@{argLine} -Dgithub.sttk.errs.notify=true</argLine>
156+
</configuration>
154157
</plugin>
155158
<plugin>
156-
<groupId>net.revelc.code.formatter</groupId>
157-
<artifactId>formatter-maven-plugin</artifactId>
158-
<version>2.26.0</version>
159+
<groupId>net.revelc.code.formatter</groupId>
160+
<artifactId>formatter-maven-plugin</artifactId>
161+
<version>2.26.0</version>
159162
</plugin>
160163
</plugins>
161164
</build>
@@ -187,6 +190,9 @@
187190
<buildArg>--initialize-at-build-time=org.junit.platform.launcher.core.LauncherConfig</buildArg>
188191
<buildArg>--initialize-at-build-time=org.junit.jupiter.engine.config.InstantiatingConfigurationParameterConverter</buildArg>
189192
</buildArgs>
193+
<systemProperties>
194+
<github.sttk.errs.notify>true</github.sttk.errs.notify>
195+
</systemProperties>
190196
</configuration>
191197
</plugin>
192198
</plugins>

src/main/java/com/github/sttk/errs/Exc.java

Lines changed: 101 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,26 @@
44
*/
55
package com.github.sttk.errs;
66

7+
import java.lang.management.ManagementFactory;
78
import java.io.ObjectInputStream;
89
import java.io.ObjectOutputStream;
910
import java.io.Serializable;
1011
import java.io.IOException;
1112
import java.io.NotSerializableException;
1213
import java.io.InvalidObjectException;
14+
import java.time.OffsetDateTime;
15+
import java.util.List;
16+
import java.util.LinkedList;
1317

1418
/**
1519
* Is the exception class with a reason.
1620
* <p>
1721
* This class has a record field which indicates a reason for this exception. The class name of the reason record
1822
* represents the type of reason, and the fields of the reason record hold the situation where the exception occurred.
1923
* <p>
20-
* Optionally, this exception class can notify its instance creation to pre-registered exception handlers.
24+
* Optionally, this exception class can notify its instance creation to pre-registered exception handlers. This
25+
* notification feature can be enabled by specifying the system property {@code -Dgithub.sttk.errs.notify=true} when the
26+
* JVM is started.
2127
* <p>
2228
* The example code of creating and throwing an excepton is as follows:
2329
*
@@ -27,7 +33,7 @@
2733
*
2834
* try {
2935
* throw new Exc(new FailToDoSomething("abc", 123));
30-
* } catch (Err e) {
36+
* } catch (Exc e) {
3137
* System.out.println(e.getMessage()); // => "FailToDoSomething { name=abc, value=123 }"
3238
* }
3339
* }</pre>
@@ -56,6 +62,8 @@ public Exc(final Record reason) {
5662
this.reason = reason;
5763

5864
this.trace = getStackTrace()[0];
65+
66+
notifyExc(this);
5967
}
6068

6169
/**
@@ -77,6 +85,8 @@ public Exc(final Record reason, final Throwable cause) {
7785
this.reason = reason;
7886

7987
this.trace = getStackTrace()[0];
88+
89+
notifyExc(this);
8090
}
8191

8292
/**
@@ -194,6 +204,95 @@ private void readObject(ObjectInputStream in) throws ClassNotFoundException, IOE
194204
throw new InvalidObjectException("reason is null or invalid.");
195205
}
196206
}
207+
208+
//// Notification ////
209+
210+
private static final boolean useNotification;
211+
212+
static {
213+
boolean b = false;
214+
for (String arg : ManagementFactory.getRuntimeMXBean().getInputArguments()) {
215+
if ("-Dgithub.sttk.errs.notify=true".equals(arg)) {
216+
b = true;
217+
break;
218+
}
219+
}
220+
useNotification = b;
221+
}
222+
223+
private static boolean isHandlersFixed = false;
224+
private static final List<ExcHandler> syncExcHandlers = new LinkedList<>();
225+
private static final List<ExcHandler> asyncExcHandlers = new LinkedList<>();
226+
227+
/**
228+
* Adds an {@link ExcHandler} object which is executed synchronously just after an {@link Exc} is created. Handlers
229+
* added with this method are executed in the order of addition and stop if one of the handlers throws a
230+
* {@link RuntimeException} or an {@link Error}. NOTE: This feature is enabled via the system property:
231+
* {@code github.sttk.errs.notify=true}
232+
*
233+
* @param handler
234+
* An {@link ExcHandler} object.
235+
*/
236+
public static void addSyncHandler(final ExcHandler handler) {
237+
if (!useNotification)
238+
return;
239+
if (isHandlersFixed)
240+
return;
241+
syncExcHandlers.add(handler);
242+
}
243+
244+
/**
245+
* Adds an {@link ExcHandler} object which is executed asynchronously just after an {@link Exc} is created. Handlers
246+
* don't stop even if one of the handlers throw a {@link RuntimeException} or an {@link Error}. NOTE: This feature
247+
* is enabled via the system property: {@code github.sttk.errs.notify=true}
248+
*
249+
* @param handler
250+
* An {@link ExcHandler} object.
251+
*/
252+
public static void addAsyncHandler(final ExcHandler handler) {
253+
if (!useNotification)
254+
return;
255+
if (isHandlersFixed)
256+
return;
257+
asyncExcHandlers.add(handler);
258+
}
259+
260+
/**
261+
* Prevents further addition of {@link ExcHandler} objects to synchronous and asynchronous exception handler lists.
262+
* Before this is called, no {@code Exc} is notified to the handlers. After this is called, no new handlers can be
263+
* added, and {@code Exc}(s) is notified to the handlers. NOTE: This feature is enabled via the system property:
264+
* {@code github.sttk.errs.notify=true}
265+
*/
266+
public static void fixHandlers() {
267+
if (!useNotification)
268+
return;
269+
if (isHandlersFixed)
270+
return;
271+
isHandlersFixed = true;
272+
}
273+
274+
private static void notifyExc(Exc exc) {
275+
if (!useNotification)
276+
return;
277+
if (!isHandlersFixed)
278+
return;
279+
280+
if (syncExcHandlers.isEmpty() && asyncExcHandlers.isEmpty()) {
281+
return;
282+
}
283+
284+
final var tm = OffsetDateTime.now();
285+
286+
for (var handler : syncExcHandlers) {
287+
handler.handle(exc, tm);
288+
}
289+
290+
for (var handler : asyncExcHandlers) {
291+
Thread.ofVirtual().start(() -> {
292+
handler.handle(exc, tm);
293+
});
294+
}
295+
}
197296
}
198297

199298
final class RuntimeExc extends RuntimeException {
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* ExcHandler class.
3+
* Copyright (C) 2025 Takayuki Sato. All Rights Reserved.
4+
*/
5+
package com.github.sttk.errs;
6+
7+
import java.time.OffsetDateTime;
8+
9+
/**
10+
* {@code ExcHandler} is a handler of an {@link Exc} object creation.
11+
*/
12+
@FunctionalInterface
13+
public interface ExcHandler {
14+
15+
/**
16+
* Handles an {@link Exc} object creation.
17+
*
18+
* @param exc
19+
* The {@link Exc} object.
20+
* @param tm
21+
* The creation time of the {@link Exc} object.
22+
*/
23+
void handle(Exc exc, OffsetDateTime tm);
24+
}

src/main/java/com/github/sttk/errs/package-info.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
/*
22
* Copyright (C) 2024 Takayuki Sato. All Rights Reserved.
33
*
4-
* This program is free software under MIT License.
5-
* See the file LICENSE in this distribution for more details.
4+
* This program is free software under MIT License. See the file LICENSE in this distribution for
5+
* more details.
66
*/
77

88
/**

src/main/java/module-info.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@
1212
*/
1313
module com.github.sttk.errs {
1414
exports com.github.sttk.errs;
15+
requires java.management;
1516
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package com.github.sttk.errs;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
import static org.junit.jupiter.api.Assertions.fail;
5+
import org.junit.jupiter.api.Nested;
6+
import org.junit.jupiter.api.Test;
7+
import org.junit.jupiter.api.BeforeEach;
8+
9+
import static java.time.format.DateTimeFormatter.ISO_INSTANT;
10+
import java.util.List;
11+
import java.util.LinkedList;
12+
13+
public class ExcHandlerTest {
14+
private ExcHandlerTest() {
15+
}
16+
17+
@BeforeEach
18+
void reset() throws Exception {
19+
var f = Exc.class.getDeclaredField("isHandlersFixed");
20+
f.setAccessible(true);
21+
f.setBoolean(null, false);
22+
23+
f = Exc.class.getDeclaredField("syncExcHandlers");
24+
f.setAccessible(true);
25+
var o = f.get(null);
26+
var m = LinkedList.class.getMethod("clear");
27+
m.invoke(o);
28+
29+
f = Exc.class.getDeclaredField("asyncExcHandlers");
30+
f.setAccessible(true);
31+
o = f.get(null);
32+
m = LinkedList.class.getMethod("clear");
33+
m.invoke(o);
34+
}
35+
36+
@SuppressWarnings("unchecked")
37+
List<ExcHandler> getSyncExcHandlers() throws Exception {
38+
var f = Exc.class.getDeclaredField("syncExcHandlers");
39+
f.setAccessible(true);
40+
var o = f.get(null);
41+
return (List<ExcHandler>) o;
42+
}
43+
44+
@SuppressWarnings("unchecked")
45+
List<ExcHandler> getAsyncExcHandlers() throws Exception {
46+
var f = Exc.class.getDeclaredField("asyncExcHandlers");
47+
f.setAccessible(true);
48+
var o = f.get(null);
49+
return (List<ExcHandler>) o;
50+
}
51+
52+
@Test
53+
void should_add_sync_handlers_and_fix() throws Exception {
54+
var handlers = getSyncExcHandlers();
55+
assertThat(handlers).isEmpty();
56+
57+
ExcHandler handler1 = (exc, tm) -> {
58+
};
59+
Exc.addSyncHandler(handler1);
60+
61+
handlers = getSyncExcHandlers();
62+
assertThat(handlers).containsExactly(handler1);
63+
64+
ExcHandler handler2 = (exc, tm) -> {
65+
};
66+
Exc.addSyncHandler(handler2);
67+
68+
handlers = getSyncExcHandlers();
69+
assertThat(handlers).containsExactly(handler1, handler2);
70+
71+
Exc.fixHandlers();
72+
73+
ExcHandler handler3 = (exc, tm) -> {
74+
};
75+
Exc.addSyncHandler(handler3);
76+
77+
handlers = getSyncExcHandlers();
78+
assertThat(handlers).containsExactly(handler1, handler2);
79+
}
80+
81+
@Test
82+
void should_add_async_handlers_and_fix() throws Exception {
83+
var handlers = getAsyncExcHandlers();
84+
assertThat(handlers).isEmpty();
85+
86+
ExcHandler handler1 = (exc, tm) -> {
87+
};
88+
Exc.addAsyncHandler(handler1);
89+
90+
handlers = getAsyncExcHandlers();
91+
assertThat(handlers).containsExactly(handler1);
92+
93+
ExcHandler handler2 = (exc, tm) -> {
94+
};
95+
Exc.addAsyncHandler(handler2);
96+
97+
handlers = getAsyncExcHandlers();
98+
assertThat(handlers).containsExactly(handler1, handler2);
99+
100+
Exc.fixHandlers();
101+
102+
ExcHandler handler3 = (exc, tm) -> {
103+
};
104+
Exc.addAsyncHandler(handler3);
105+
106+
handlers = getAsyncExcHandlers();
107+
assertThat(handlers).containsExactly(handler1, handler2);
108+
}
109+
110+
@Test
111+
void should_notify_exception() throws Exception {
112+
final List<String> syncLogs = new LinkedList<>();
113+
final List<String> asyncLogs = new LinkedList<>();
114+
115+
Exc.addSyncHandler((exc, tm) -> {
116+
syncLogs.add(String.format("%s:%s(%d):%s", tm.format(ISO_INSTANT), exc.getFile(), exc.getLine(),
117+
exc.getReason().toString()));
118+
});
119+
Exc.addAsyncHandler((exc, tm) -> {
120+
asyncLogs.add(String.format("%s:%s(%d):%s", tm.format(ISO_INSTANT), exc.getFile(), exc.getLine(),
121+
exc.getReason().toString()));
122+
});
123+
124+
record FailToDoSomething(String name) {
125+
}
126+
127+
new Exc(new FailToDoSomething("abc"));
128+
129+
assertThat(syncLogs).isEmpty();
130+
assertThat(asyncLogs).isEmpty();
131+
132+
Exc.fixHandlers();
133+
134+
new Exc(new FailToDoSomething("abc"));
135+
assertThat(syncLogs.get(0)).endsWith(":ExcHandlerTest.java(134):FailToDoSomething[name=abc]");
136+
137+
Thread.sleep(100);
138+
assertThat(asyncLogs.get(0)).endsWith(":ExcHandlerTest.java(134):FailToDoSomething[name=abc]");
139+
}
140+
}

src/test/java/com/github/sttk/errs/ExcTest.java

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,8 @@
22

33
import static org.assertj.core.api.Assertions.assertThat;
44
import static org.junit.jupiter.api.Assertions.fail;
5-
import org.junit.jupiter.api.Disabled;
65
import org.junit.jupiter.api.Nested;
76
import org.junit.jupiter.api.Test;
8-
import org.junit.jupiter.api.condition.DisabledInNativeImage;
97

108
import java.io.ByteArrayInputStream;
119
import java.io.ByteArrayOutputStream;
@@ -153,7 +151,7 @@ void getFile() {
153151
@Test
154152
void getLine() {
155153
var exc = new Exc(new IndexOutOfRange("data", 4, 0, 3));
156-
assertThat(exc.getLine()).isEqualTo(155);
154+
assertThat(exc.getLine()).isEqualTo(153);
157155
}
158156
}
159157

@@ -181,15 +179,15 @@ class TestToString {
181179
void with_reason() {
182180
var exc = new Exc(new IndexOutOfRange("data", 4, 0, 3));
183181
assertThat(exc.toString()).isEqualTo(
184-
"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 }");
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 }");
185183
}
186184

187185
@Test
188186
void with_reason_and_cause() {
189187
var cause = new IndexOutOfBoundsException(4);
190188
var exc = new Exc(new IndexOutOfRange("data", 4, 0, 3), cause);
191189
assertThat(exc.toString()).isEqualTo(
192-
"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 }");
190+
"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 }");
193191
}
194192
}
195193

0 commit comments

Comments
 (0)