Java reachability is harder than it looks and easier than the JVM's reputation suggests. The classpath model is forgiving in ways that make static analysis painful, but the underlying bytecode is regular and the popular frameworks have well-understood reflection patterns that modern tools have learned to model. The result is that Java reachability today is usable in production, with caveats, and the false-positive reduction numbers are large enough to justify the investment for any shop running a non-trivial JVM footprint.
This post walks through what works, where the abstractions leak, and which CVEs benefit most from reachability filtering. It is aimed at platform engineers who have been burned by SCA noise on Spring Boot services and want to know whether the latest tools have caught up.
What makes Java reachability fundamentally hard?
Three things. First, reflection: Java code can invoke methods by name at runtime, and a static analyzer that does not model reflective calls will miss real edges. Second, dependency injection: Spring, Guice, and CDI construct object graphs from configuration metadata, not from explicit new calls, so the call graph in the bytecode does not match the call graph in the running application. Third, classpath ambiguity: the same class name can exist in multiple JARs, and the JVM picks one at runtime according to classloader rules that static tools have to approximate.
Each of these is solvable in principle. CodeQL and Endor Labs both model Spring autowiring by parsing configuration annotations and bean definitions, treating @Autowired fields as call graph edges. Reflection is handled by pattern matching on common reflective call sites and assuming the worst case for unrecognized ones. Classpath ambiguity is resolved by reading the actual built artifact rather than the source tree. None of this is perfect, but it is good enough that the reachability signal is meaningfully better than the alternative of treating every classpath CVE as live.
How did Log4Shell reshape Java reachability tooling?
CVE-2021-44228, Log4Shell, was the event that forced the Java reachability conversation into the mainstream. Within a week of disclosure, every SCA vendor was claiming reachability analysis for Log4j, and within a quarter, several of those claims turned out to be marketing rather than engineering. The lesson the industry absorbed was that reachability for a logging library is harder than it looks because the vulnerable code path runs on every log statement that includes user-controlled data, which is essentially every web request handler.
The tools that handled Log4Shell well were the ones that did two things: identified whether the vulnerable JNDI lookup feature was enabled, and traced taint from request entry points into log statements. Snyk Code and CodeQL both did this competently by 2022. Less rigorous tools just flagged the library version and called it reachability, which produced no useful filtering. The lasting effect was that buyers got more sophisticated about what they were paying for, and vendors invested in real call graph infrastructure rather than dependency graph wrappers.
How do modern tools handle Spring autowiring?
Spring autowiring is the make-or-break test for Java reachability tools because almost every modern Java service uses it. A vulnerable method on RestTemplate is reachable on a Spring Boot service that autowires a RestTemplate bean, even though no source file contains new RestTemplate(). Tools that walk only explicit constructor calls will miss the edge and produce a false negative.
The good tools parse Spring's bean definition metadata, including XML, Java config, and annotation-based configuration, and add the resulting wiring relationships as call graph edges. Endor Labs ships a Spring-specific analyzer that handles @Component, @Service, @Repository, and @Configuration correctly. CodeQL has a similar capability through its dataflow library. Semgrep's pattern-based approach is less precise here because it does not build a full bean graph, but it is improving. The practical effect is that reachability reports on Spring services are now within a few percent of ground truth for the common DI patterns, which was not true two years ago.
What about shaded JARs and Maven Shade Plugin?
Shaded JARs are a persistent source of confusion. When a library is shaded into another JAR with a relocated package, the vulnerable code is still present but at a different fully-qualified class name. A reachability tool that matches CVEs by class name will miss the shaded copy. Conversely, a tool that scans bytecode signatures will find it but may not connect it to the original CVE record.
The right approach is to compute file-level hashes of class bytecode and match against a database of known vulnerable hashes. This is what Black Duck and JFrog Xray do under the hood, and what Syft includes in its SBOM output. It catches the shaded-Log4j problem and similar issues. Without this layer, your reachability analysis is looking at the wrong artifact. We have measured roughly 15% of Java services in financial services environments to contain at least one shaded vulnerable library that name-based scanning would miss.
What false-positive reductions are realistic?
For a typical Spring Boot service, reachability filtering reduces CVE alert volume by 65 to 80%. The wider range than Go reflects the fact that Java services have larger and more variable dependency trees, and the reachability precision depends heavily on whether the framework is well-modeled. For a plain Java SE service without DI, the numbers are closer to Go, around 80%. For a heavy Spring service with extensive reflection and dynamic proxying, expect 65% and treat the remaining alerts as the ones worth your attention.
These numbers come from internal data across about 1,200 Java services in mid-size enterprises that adopted reachability filtering in 2024 and 2025. The team-level effect is a sustainable on-call rotation, which is the actual business outcome you are buying.
How Safeguard Helps
Safeguard builds Java call graphs from compiled JARs and WARs, with first-class modeling of Spring, Quarkus, and Micronaut DI patterns. Reachability runs against NVD, GitHub Advisory, and CISA KEV, and Griffin AI explains the specific bean wiring or reflective call that makes a CVE reachable. SBOMs are generated with class-level hashes that catch shaded JAR cases, and policy gates can block deployments on reachable-critical CVEs only. TPRM monitors the maintainer health of your top Maven and Gradle dependencies, and our zero-CVE Eclipse Temurin and Liberica base images give Java workloads a clean JVM foundation.