package com.yourpackage;
import org.springframework.beans.PropertyValue;
import org.springframework.beans.factory.config.*;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.SmartLifecycle;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.*;
public class SpringVisualizer implements ApplicationContextAware, SmartLifecycle {
private ConfigurableApplicationContext startContext;
private boolean isRunning = false;
private final Set<String> currentBeanManualDeps = new HashSet<>();
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.startContext = (ConfigurableApplicationContext) applicationContext;
}
@Override
public void start() {
StringBuilder dot = new StringBuilder("digraph G {\n");
dot.append(" rankdir=LR; node [shape=plain, fontname=\"Arial\", fontsize=10];\n");
dot.append(" edge [fontname=\"Arial\", fontsize=8];\n\n");
Set<String> processed = new HashSet<>();
ApplicationContext current = this.startContext;
while (current != null) {
ConfigurableListableBeanFactory factory = ((ConfigurableApplicationContext) current).getBeanFactory();
for (String name : factory.getBeanDefinitionNames()) {
if (processed.add(name)) {
BeanDefinition def = factory.getBeanDefinition(name);
currentBeanManualDeps.clear();
String color = name.toLowerCase().contains("comet") ? "#FFF9C4" :
(name.toLowerCase().contains("cpls") ? "#C8E6C9" : "#E1F5FE");
dot.append(String.format(" \"%s\" [label=<", name));
dot.append("<TABLE BORDER=\"0\" CELLBORDER=\"1\" CELLSPACING=\"0\" BGCOLOR=\"").append(color).append("\">");
dot.append("<TR><TD COLSPAN=\"2\"><B>").append(xmlEscape(name.trim())).append("</B></TD></TR>");
// 1. Process Properties (Correctly unwrap ValueHolders if present)
for (PropertyValue pv : def.getPropertyValues().getPropertyValues()) {
dot.append("<TR><TD ALIGN=\"LEFT\">").append(xmlEscape(pv.getName().trim())).append("</TD>");
dot.append("<TD ALIGN=\"LEFT\">").append(extractValue(pv.getValue(), 0)).append("</TD></TR>");
}
// 2. Process Constructor Args
ConstructorArgumentValues cav = def.getConstructorArgumentValues();
for (Map.Entry<Integer, ConstructorArgumentValues.ValueHolder> entry : cav.getIndexedArgumentValues().entrySet()) {
dot.append("<TR><TD ALIGN=\"LEFT\"><I>arg[").append(entry.getKey()).append("]</I></TD>");
dot.append("<TD ALIGN=\"LEFT\">").append(extractValue(entry.getValue().getValue(), 0)).append("</TD></TR>");
}
for (ConstructorArgumentValues.ValueHolder vh : cav.getGenericArgumentValues()) {
String type = vh.getType() != null ? vh.getType().substring(vh.getType().lastIndexOf('.') + 1) : "gen";
dot.append("<TR><TD ALIGN=\"LEFT\"><I>arg:").append(type.trim()).append("</I></TD>");
dot.append("<TD ALIGN=\"LEFT\">").append(extractValue(vh.getValue(), 0)).append("</TD></TR>");
}
dot.append("</TABLE>>];\n");
// 3. Draw Connections
Set<String> allDeps = new HashSet<>(Arrays.asList(factory.getDependenciesForBean(name)));
allDeps.addAll(currentBeanManualDeps);
for (String dep : allDeps) {
dot.append(String.format(" \"%s\" -> \"%s\";\n", name, dep));
}
}
}
current = current.getParent();
}
dot.append("}\n");
writeToFile(dot.toString());
this.isRunning = true;
}
private String extractValue(Object value, int depth) {
if (value == null) return "null";
if (depth > 6) return "<i>[Max Depth]</i>";
// Handle Bean References
if (value instanceof BeanReference) {
String beanName = ((BeanReference) value).getBeanName().trim();
currentBeanManualDeps.add(beanName);
return "@" + xmlEscape(beanName);
}
// Handle TypedStringValue
if (value instanceof TypedStringValue) {
String raw = ((TypedStringValue) value).getValue();
return xmlEscape(raw != null ? raw.trim() : "null");
}
// CRITICAL: Recursive Inner Bean Expansion
// We check for BeanDefinitionHolder or BeanDefinition
if (value instanceof BeanDefinitionHolder || value instanceof BeanDefinition) {
BeanDefinition innerDef = (value instanceof BeanDefinitionHolder) ?
((BeanDefinitionHolder) value).getBeanDefinition() : (BeanDefinition) value;
StringBuilder innerHtml = new StringBuilder("<TABLE BORDER=\"0\" CELLBORDER=\"1\" CELLSPACING=\"0\" BGCOLOR=\"#F5F5F5\">");
String className = innerDef.getBeanClassName();
// If class name is null, check parent name (common in complex CPLS/COMET configs)
String shortName = className != null ? className.substring(className.lastIndexOf('.') + 1) :
(innerDef.getParentName() != null ? "Parent:" + innerDef.getParentName() : "InnerBean");
innerHtml.append("<TR><TD COLSPAN=\"2\" BGCOLOR=\"#DCDCDC\"><B>").append(xmlEscape(shortName.trim())).append("</B></TD></TR>");
// Properties of inner bean
for (PropertyValue pv : innerDef.getPropertyValues().getPropertyValues()) {
innerHtml.append("<TR><TD><FONT POINT-SIZE=\"8\">").append(xmlEscape(pv.getName().trim())).append("</FONT></TD>");
innerHtml.append("<TD>").append(extractValue(pv.getValue(), depth + 1)).append("</TD></TR>");
}
// Constructor Args of inner bean
ConstructorArgumentValues cav = innerDef.getConstructorArgumentValues();
for (ConstructorArgumentValues.ValueHolder vh : cav.getGenericArgumentValues()) {
innerHtml.append("<TR><TD><I>arg:gen</I></TD>");
innerHtml.append("<TD>").append(extractValue(vh.getValue(), depth + 1)).append("</TD></TR>");
}
innerHtml.append("</TABLE>");
return innerHtml.toString();
}
// Handle Collections
if (value instanceof Iterable) {
List<String> cleaned = new ArrayList<>();
for (Object item : (Iterable<?>) value) {
cleaned.add(extractValue(item, depth + 1));
}
return String.join("<BR ALIGN=\"LEFT\"/>", cleaned);
}
// Handle Maps
if (value instanceof Map) {
StringBuilder mapStr = new StringBuilder();
for (Map.Entry<?, ?> entry : ((Map<?, ?>) value).entrySet()) {
mapStr.append(extractValue(entry.getKey(), depth + 1))
.append("=")
.append(extractValue(entry.getValue(), depth + 1))
.append("<BR ALIGN=\"LEFT\"/>");
}
return mapStr.toString();
}
// Default Trimming Fallback
String s = value.toString().trim();
return xmlEscape(s.length() > 60 ? s.substring(0, 57).trim() + "..." : s);
}
private void writeToFile(String content) {
try {
File projectRoot = findProjectRoot();
File buildDir = new File(projectRoot, "build");
if (!buildDir.exists()) buildDir.mkdirs();
File file = new File(buildDir, "spring-beans.dot");
try (FileWriter writer = new FileWriter(file)) {
writer.write(content);
}
System.out.println("\n============================================================");
System.out.println("GRAPH GENERATION SUCCESSFUL");
System.out.println("Click to open: " + file.toURI().toString());
System.out.println("============================================================\n");
} catch (IOException e) {
System.err.println("File Error: " + e.getMessage());
}
}
private File findProjectRoot() {
File current = new File(System.getProperty("user.dir"));
while (current != null) {
if (new File(current, "build.gradle").exists() || new File(current, "settings.gradle").exists()) return current;
current = current.getParentFile();
}
return new File(System.getProperty("user.dir"));
}
private String xmlEscape(String input) {
if (input == null) return "";
return input.replace("&", "&").replace("<", "<").replace(">", ">")
.replace("\"", """).replace("'", "'")
.replace("<BR ALIGN="LEFT"/>", "<BR ALIGN=\"LEFT\"/>").trim();
}
@Override public int getPhase() { return Integer.MAX_VALUE; }
@Override public boolean isAutoStartup() { return true; }
@Override public void stop() { this.isRunning = false; }
@Override public boolean isRunning() { return this.isRunning; }
@Override public void stop(Runnable c) { stop(); c.run(); }
}
For further actions, you may consider blocking this person and/or reporting abuse
Top comments (0)