Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -151,11 +151,14 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.3</version>
<configuration>
<argLine>@{argLine} -Dgithub.sttk.errs.notify=true</argLine>
</configuration>
</plugin>
<plugin>
<groupId>net.revelc.code.formatter</groupId>
<artifactId>formatter-maven-plugin</artifactId>
<version>2.26.0</version>
<groupId>net.revelc.code.formatter</groupId>
<artifactId>formatter-maven-plugin</artifactId>
<version>2.26.0</version>
</plugin>
</plugins>
</build>
Expand Down Expand Up @@ -187,6 +190,9 @@
<buildArg>--initialize-at-build-time=org.junit.platform.launcher.core.LauncherConfig</buildArg>
<buildArg>--initialize-at-build-time=org.junit.jupiter.engine.config.InstantiatingConfigurationParameterConverter</buildArg>
</buildArgs>
<systemProperties>
<github.sttk.errs.notify>true</github.sttk.errs.notify>
</systemProperties>
</configuration>
</plugin>
</plugins>
Expand Down
103 changes: 101 additions & 2 deletions src/main/java/com/github/sttk/errs/Exc.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,26 @@
*/
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.
* <p>
* 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.
* <p>
* 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.
* <p>
* The example code of creating and throwing an excepton is as follows:
*
Expand All @@ -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 }"
* }
* }</pre>
Expand Down Expand Up @@ -56,6 +62,8 @@ public Exc(final Record reason) {
this.reason = reason;

this.trace = getStackTrace()[0];

notifyExc(this);
}

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

this.trace = getStackTrace()[0];

notifyExc(this);
}

/**
Expand Down Expand Up @@ -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<ExcHandler> syncExcHandlers = new LinkedList<>();
private static final List<ExcHandler> 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 {
Expand Down
24 changes: 24 additions & 0 deletions src/main/java/com/github/sttk/errs/ExcHandler.java
Original file line number Diff line number Diff line change
@@ -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);
}
4 changes: 2 additions & 2 deletions src/main/java/com/github/sttk/errs/package-info.java
Original file line number Diff line number Diff line change
@@ -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.
*/

/**
Expand Down
1 change: 1 addition & 0 deletions src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@
*/
module com.github.sttk.errs {
exports com.github.sttk.errs;
requires java.management;
}
140 changes: 140 additions & 0 deletions src/test/java/com/github/sttk/errs/ExcHandlerTest.java
Original file line number Diff line number Diff line change
@@ -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<ExcHandler> getSyncExcHandlers() throws Exception {
var f = Exc.class.getDeclaredField("syncExcHandlers");
f.setAccessible(true);
var o = f.get(null);
return (List<ExcHandler>) o;
}

@SuppressWarnings("unchecked")
List<ExcHandler> getAsyncExcHandlers() throws Exception {
var f = Exc.class.getDeclaredField("asyncExcHandlers");
f.setAccessible(true);
var o = f.get(null);
return (List<ExcHandler>) 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<String> syncLogs = new LinkedList<>();
final List<String> 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]");
}
}
8 changes: 3 additions & 5 deletions src/test/java/com/github/sttk/errs/ExcTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -181,15 +179,15 @@ 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
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 }");
}
}

Expand Down