package com.yourpackage;
import org.springframework.beans.PropertyValue;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.config.*;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;
import java.io.*;
import java.util.*;
public class SpringVisualizer implements ApplicationContextAware, InitializingBean {
private ConfigurableApplicationContext context;
private final Set<String> currentBeanManualDeps = new HashSet<>();
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.context = (ConfigurableApplicationContext) applicationContext;
}
@Override
public void afterPropertiesSet() throws Exception {
generateGraph();
}
public void generateGraph() {
// Trim the header to ensure no "Line 1" errors
StringBuilder dot = new StringBuilder("digraph G {\n");
dot.append(" rankdir=LR; nodesep=0.7; ranksep=2.0; splines=true;\n");
dot.append(" node [shape=none, fontname=\"Verdana\", fontsize=11];\n");
dot.append(" edge [fontname=\"Verdana\", fontsize=9, color=\"#666666\"];\n\n");
Set<String> processed = new HashSet<>();
int count = 0;
ApplicationContext currentCtx = this.context;
while (currentCtx != null) {
if (currentCtx instanceof ConfigurableApplicationContext) {
ConfigurableListableBeanFactory factory = ((ConfigurableApplicationContext) currentCtx).getBeanFactory();
for (String name : factory.getBeanDefinitionNames()) {
if (name.startsWith("org.springframework")) continue;
if (processed.add(name)) {
try {
BeanDefinition def = factory.getBeanDefinition(name);
currentBeanManualDeps.clear();
count++;
String color = name.toLowerCase().contains("comet") ? "#FFF9C4" :
(name.toLowerCase().contains("cpls") ? "#C8E6C9" : "#E1F5FE");
dot.append(" \"").append(name).append("\" [label=<");
dot.append("<TABLE BORDER=\"0\" CELLBORDER=\"1\" CELLSPACING=\"0\" CELLPADDING=\"4\" BGCOLOR=\"").append(color).append("\">");
// FIXED: Header now uses a single cell to prevent overflow
dot.append("<TR><TD BGCOLOR=\"#999999\" ALIGN=\"CENTER\"><B>").append(safeXml(name)).append("</B></TD></TR>");
for (PropertyValue pv : def.getPropertyValues().getPropertyValues()) {
dot.append("<TR><TD ALIGN=\"LEFT\">");
// We use a nested table with a fixed structure to keep labels and values aligned
dot.append("<TABLE BORDER=\"0\" CELLBORDER=\"0\" CELLSPACING=\"0\" CELLPADDING=\"0\">");
dot.append("<TR><TD ALIGN=\"LEFT\"><B>").append(safeXml(pv.getName())).append(": </B></TD>");
dot.append("<TD ALIGN=\"LEFT\">").append(extractValue(pv.getValue(), 0)).append("</TD></TR>");
dot.append("</TABLE></TD></TR>");
}
dot.append("</TABLE>>];\n");
Set<String> allDeps = new HashSet<>(Arrays.asList(factory.getDependenciesForBean(name)));
allDeps.addAll(currentBeanManualDeps);
for (String dep : allDeps) {
if (!dep.startsWith("org.springframework")) {
dot.append(" \"").append(name).append("\" -> \"").append(dep).append("\";\n");
}
}
} catch (Exception e) {}
}
}
}
currentCtx = currentCtx.getParent();
}
dot.append("}\n");
save(dot.toString(), count);
}
private String extractValue(Object value, int depth) {
if (value == null) return "null";
if (depth > 4) return "<i>[...]</i>";
if (value instanceof BeanReference) {
String bName = ((BeanReference) value).getBeanName();
currentBeanManualDeps.add(bName);
return "@" + safeXml(bName);
}
if (value instanceof TypedStringValue) {
return safeXml(((TypedStringValue) value).getValue());
}
if (value instanceof BeanDefinition || value instanceof BeanDefinitionHolder) {
BeanDefinition inner = (value instanceof BeanDefinitionHolder) ?
((BeanDefinitionHolder) value).getBeanDefinition() : (BeanDefinition) value;
// Nested Inner Bean Table
StringBuilder sb = new StringBuilder("<TABLE BORDER=\"1\" CELLBORDER=\"0\" CELLSPACING=\"0\" CELLPADDING=\"2\" BGCOLOR=\"#FFFFFF\">");
String cls = inner.getBeanClassName();
String label = (cls != null) ? cls.substring(cls.lastIndexOf('.') + 1) : "InnerBean";
sb.append("<TR><TD COLSPAN=\"2\" BGCOLOR=\"#EEEEEE\" ALIGN=\"CENTER\"><I>").append(safeXml(label)).append("</I></TD></TR>");
for (PropertyValue pv : inner.getPropertyValues().getPropertyValues()) {
sb.append("<TR><TD ALIGN=\"LEFT\"><B>").append(safeXml(pv.getName())).append("</B></TD>");
sb.append("<TD ALIGN=\"LEFT\">").append(extractValue(pv.getValue(), depth + 1)).append("</TD></TR>");
}
sb.append("</TABLE>");
return sb.toString();
}
if (value instanceof Iterable) {
StringBuilder sb = new StringBuilder();
for (Object item : (Iterable<?>) value) {
// FIXED: BR MUST BE INSIDE TD. We don't add it here, we let the parent handle layout.
sb.append(extractValue(item, depth + 1)).append(", ");
}
String result = sb.toString();
return result.isEmpty() ? "[]" : safeXml(result.substring(0, result.length() - 2));
}
if (value instanceof Map) {
return "Map(" + ((Map<?, ?>) value).size() + " items)";
}
return safeXml(value.toString());
}
private void save(String data, int count) {
try {
File f = new File(findRoot(), "build/spring-beans.dot");
f.getParentFile().mkdirs();
// Using OutputStreamWriter with UTF-8 to prevent character encoding errors
try (BufferedWriter out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(f), "UTF-8"))) {
out.write(data);
}
System.err.println("SUCCESS: File saved with " + count + " beans.");
} catch (Exception e) { e.printStackTrace(); }
}
private File findRoot() {
File f = new File(System.getProperty("user.dir"));
while (f != null && !new File(f, "build.gradle").exists()) f = f.getParentFile();
return f != null ? f : new File(System.getProperty("user.dir"));
}
private String safeXml(String s) {
if (s == null) return "";
// Extreme cleanup: remove non-printable characters that break Graphviz
String clean = s.replaceAll("[\\p{Cntrl}&&[^\r\n\t]]", "");
return clean.trim().replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """);
}
}
For further actions, you may consider blocking this person and/or reporting abuse
Top comments (0)