Ausgangssituation
Unser Projekt enthält eine komplexe Businesslogik mit mehreren asynchronen Schritten. Dies führt im konkreten Fall dazu, dass im Fehler teilweise als suppressed Exceptions übergeben werden. Da mehrere Ausnahmen auftreten können, möchten wir alle in eine hilfreiche Antwort für den Schnittstellennutzer transformieren.

Unser Ziel ist dabei, die Ursachen (Root Causes) der Exceptions zu ermitteln. Hierfür gibt es eine Hilfsmethode von Apache Commons, jedoch müssen wir in unserem Fall auch alle suppressed Exceptions in der Hierarchie berücksichtigen, was eine Eigenimplementierung erfordert.
Absicherung durch Generierung weiterer Tests
Unsere erste eigene Implementierung scheint vollständig zu funktionieren, alle bestehenden Tests sind grün. Zur Sicherheit nutzen wir ChatGPT, um für die gegebene Utility-Methode Unit-Tests zu schreiben.
Dies klappt fast vollständig, nur geringfügige Korrekturen im Naming sind notwendig.
Eine der Testmethoden erweitern wir manuell, da die Variante von ChatGPT bei einem komplexen Szenario mit einer Exception-Hierarchie, die auch suppressed Exceptions enthält, nur eine suppressed Exception berücksichtigte.
ChatGPT nutzte Standard-Assertions von JUnit, während wir AssertJ-Assertions bevorzugen. Durch den Prompt "Use AssertJ assertions" wird der Code korrekt angepasst.
Außerdem sollen lokaler Variablen möglichst final deklariert werden. Auch hier hilft ein Prompt weiter: "Use final wherever possible".
Dies führt letztendlich zu folgender Testklasse:
class RootCauseFinderTest {
@Test
public void testSingleThrowable() {
final Throwable t = new Throwable("Root cause message");
final List messages = RootCauseFinder.getRootErrorMessages(t);
assertThat(messages).containsExactly("Root cause message");
}
@Test
public void testMultipleThrowables() {
final Throwable t1 = new Throwable("Root cause message 1");
final Throwable t2 = new Throwable("Root cause message 2");
final List messages = RootCauseFinder.getRootErrorMessages(t1, t2);
assertThat(messages).containsExactlyInAnyOrder("Root cause message 1", "Root cause message 2");
}
@Test
public void testNestedThrowable() {
final Throwable rootCause = new Throwable("Root cause message");
final Throwable exception = new Throwable("My exception", rootCause);
final List messages = RootCauseFinder.getRootErrorMessages(exception);
assertThat(messages).containsExactly("Root cause message");
}
@Test
public void testSuppressedThrowable() {
final Throwable suppressed = new Throwable("Suppressed message");
final Throwable main = new Throwable("Main exception");
main.addSuppressed(suppressed);
final List messages = RootCauseFinder.getRootErrorMessages(main);
assertThat(messages).containsExactlyInAnyOrder("Main exception", "Suppressed message");
}
@Test
public void testDistinctMessages() {
final Throwable t1 = new Throwable("Same message");
final Throwable t2 = new Throwable("Same message");
final List messages = RootCauseFinder.getRootErrorMessages(t1, t2);
assertThat(messages).containsExactly("Same message");
}
@Test
public void testComplexExceptionHierarchy() {
final Throwable rootCause = new Throwable("Root cause message");
final Throwable rootCauseFromSuppressed1 = new Throwable("Root cause message from suppressed 1");
final Throwable rootCauseFromSuppressed2 = new Throwable("Root cause message from suppressed 2");
final Throwable suppressed1 = new Throwable("Suppressed message 1", rootCauseFromSuppressed1);
final Throwable suppressed2 = new Throwable("Suppressed message 2", rootCauseFromSuppressed2);
final Throwable exception = new Throwable("My exception", rootCause);
exception.addSuppressed(suppressed1);
exception.addSuppressed(suppressed2);
final List messages = RootCauseFinder.getRootErrorMessages(exception);
assertThat(messages).containsExactlyInAnyOrder("Root cause message", "Root cause message from suppressed 1", "Root cause message from suppressed 2");
}
@Test
public void testComplexExceptionHierarchy_duplicateRootCauseFromSuppressedExceptions() {
final Throwable rootCause = new Throwable("Root cause message");
final Throwable rootCauseFromSuppressed1 = new Throwable("Root cause message from suppressed");
final Throwable rootCauseFromSuppressed2 = new Throwable("Root cause message from suppressed");
final Throwable suppressed1 = new Throwable("Suppressed message", rootCauseFromSuppressed1);
final Throwable suppressed2 = new Throwable("Suppressed message 2", rootCauseFromSuppressed2);
final Throwable exception = new Throwable("My exception", rootCause);
exception.addSuppressed(suppressed1);
exception.addSuppressed(suppressed2);
final List messages = RootCauseFinder.getRootErrorMessages(exception);
assertThat(messages).containsExactlyInAnyOrder("Root cause message", "Root cause message from suppressed");
}
@Test
public void testEmptyInput() {
final List messages = RootCauseFinder.getRootErrorMessages();
assertThat(messages).isEmpty();
}
}
Fehler entdeckt
Beim Ausführen der Tests fällt uns auf, dass unsere Business-Logik nicht vollständig funktioniert.
Lassen wir ChatGPT coden
Anstatt die eigene Implementierung selbst zu korrigieren, versuchen wir,
die Methode neu mittels ChatGPT zu erzeugen.
Prompt: "Java: From a given throwable I need to go down the hierarchy and find the root cause. If there are suppressed exceptions their root causes must also be collected. Result is a list of Strings containing the root cause messages."
import java.util.ArrayList;
import java.util.List;
public class RootCauseFinder {
public static List getRootCauseMessages(Throwable throwable) {
List rootCauseMessages = new ArrayList<>();
collectRootCauseMessages(throwable, rootCauseMessages);
return rootCauseMessages;
}
private static void collectRootCauseMessages(Throwable throwable, List rootCauseMessages) {
if (throwable == null) {
return;
}
// Navigate to the root cause of the current throwable
Throwable rootCause = throwable;
while (rootCause.getCause() != null) {
rootCause = rootCause.getCause();
}
// Add the message of the root cause to the list
rootCauseMessages.add(rootCause.getMessage());
// Check for suppressed exceptions and process their root causes
for (Throwable suppressed : throwable.getSuppressed()) {
collectRootCauseMessages(suppressed, rootCauseMessages);
}
// Check for the cause of the current throwable and process its root causes
if (throwable.getCause() != null) {
collectRootCauseMessages(throwable.getCause(), rootCauseMessages);
}
}
}
Optimierung
Der generierte Code ist purer Java-Code. Da wir Apache Commons nutzen, lässt sich der Code mithilfe von ExceptionUtils.getRootCause verkürzen.
Prompt: "Use Apache Commons ExceptionUtils.getRootCause where possible":
import org.apache.commons.lang3.exception.ExceptionUtils;
import java.util.ArrayList;
import java.util.List;
public class RootCauseFinder {
public static List getRootCauseMessages(Throwable throwable) {
List rootCauseMessages = new ArrayList<>();
collectRootCauseMessages(throwable, rootCauseMessages);
return rootCauseMessages;
}
private static void collectRootCauseMessages(Throwable throwable, List rootCauseMessages) {
if (throwable == null) {
return;
}
// Use Apache Commons ExceptionUtils to get the root cause
Throwable rootCause = ExceptionUtils.getRootCause(throwable);
if (rootCause == null) {
rootCause = throwable;
}
// Add the message of the root cause to the list
rootCauseMessages.add(rootCause.getMessage());
// Check for suppressed exceptions and process their root causes
for (Throwable suppressed : throwable.getSuppressed()) {
collectRootCauseMessages(suppressed, rootCauseMessages);
}
// Check for the cause of the current throwable and process its root causes
if (throwable.getCause() != null) {
collectRootCauseMessages(throwable.getCause(), rootCauseMessages);
}
}
}
In unserer Businesslogik müssen wir die Root Causes mehrerer Throwables ermitteln. Das hatten wir im ursprünglichen Prompt weggelassen, damit die Beschreibung nicht zu komplex wird. Außerdem ist es trivial, dies manuell zu ergänzen.
public static List getRootErrorMessages(final Throwable... throwables) {
return Arrays.stream(throwables)
.flatMap((Throwable throwable) -> getRootCauseMessages(throwable).stream())
.distinct()
.toList();
}
Vereinfachung und Verbesserung
Durch den Prompt "Can this code be simplified?",
wobei wir den aktuellen Codestand der Klasse übergeben, wird der Code vereinfacht.
Allerdings sind Parametern und lokale Variablen nicht als final deklariert.
Ein weiterer Prompt "Add finals to local variables and parameters" führt zu folgendem Ergebnis:
public static List getRootErrorMessages(final Throwable... throwables) {
return Arrays.stream(throwables)
.flatMap((Throwable throwable) -> getRootCauseMessages(throwable).stream())
.distinct()
.toList();
}
public static List getRootCauseMessages(final Throwable throwable) {
final List rootCauseMessages = new ArrayList<>();
collectRootCauseMessages(throwable, rootCauseMessages);
return rootCauseMessages;
}
private static void collectRootCauseMessages(final Throwable throwable, final List rootCauseMessages) {
if (throwable == null) return;
final Throwable rootCause = ExceptionUtils.getRootCause(throwable);
rootCauseMessages.add((rootCause != null ? rootCause : throwable).getMessage());
for (final Throwable suppressed : throwable.getSuppressed()) {
collectRootCauseMessages(suppressed, rootCauseMessages);
}
collectRootCauseMessages(throwable.getCause(), rootCauseMessages);
}
Ein weiterer Prompt "Can the code be changed so collectRootCauseMessages collects the throwables instead. Get the message strings in getRootCauseMessages instead. Rename the method to reflect the change." führt das gewünschte Refactoring aus.
Außerdem wollen wir noch etwas Dokumentation, daher noch ein Prompt: "Add javadoc".
Dies führt zu folgender finaler Version:
/**
* Retrieves a list of root cause messages from a throwable.
*
* @param throwable the throwable to process
* @return a list of root cause messages
*/
public static List getRootCauseMessages(final Throwable throwable) {
final List rootCauses = new ArrayList<>();
collectRootCauses(throwable, rootCauses);
return rootCauses.stream()
.map(Throwable::getMessage)
.distinct()
.toList();
}
/**
* Recursively collects root causes from a throwable and adds them to the provided list.
*
* @param throwable the throwable to process
* @param rootCauses the list to add the root causes to
*/
private static void collectRootCauses(final Throwable throwable, final List rootCauses) {
if (throwable == null) return;
final Throwable rootCause = ExceptionUtils.getRootCause(throwable);
rootCauses.add(rootCause != null ? rootCause : throwable);
for (final Throwable suppressed : throwable.getSuppressed()) {
collectRootCauses(suppressed, rootCauses);
}
collectRootCauses(throwable.getCause(), rootCauses);
}
Tipp
Checken Sie den Code nach jedem Optimierungsschritt ein. So können Sie mithilfe der Diff-Funktion in der IDE die vorgenommenen Änderungen einfacher überprüfen und nachvollziehen.
Fazit
KI ist mittlerweile ein fester Bestandteil in der Entwicklung. Eigenes Denken wird hierbei nicht überflüssig, denn die Formulierung der Prompts ist entscheidend und die Ergebnisse müssen überprüft und teilweise manuell angepasst werden. Es ist eher wie beim Pair Programming, wo gemeinsam schrittweise eine bessere Lösung erarbeitet wird und die Stärken der KI zusammen mit der Erfahrung des Entwicklers genutzt wird.
Die KI nimmt eine Menge Fleißarbeit ab, wie z.B. das Schreiben von Javadoc oder das Ergänzen fehlender Tests. Letzteres ist bei Refactorings äußerst hilfreich, um eine sinnvolle und gute Testabdeckung zu erreichen.
Für die Qualitätssicherung ist es nützlich, den Code mithilfe von KI zu vereinfachen
und verschiedene Verbesserungen einfließen zu lassen,
wie z.B. das oben gezeigte Refactoring und Umbenennung oder das Ergänzen von finals.
Code und Bild generiert mithilfe von ChatGPT.



