Visualizing a Spring Context Graph

Lack of package design can lead to unintended large Spring contexts due to the ease of using component scanning. This post will look at how to visualize the runtime state of a Spring application context with a view to refactoring application package design. Good package design will simplify both application composition configuration and integration testing through scanning module orientated packages.

Background - Uninvited Guests At The Party

Spring significantly improved the context configuration part of the framework since the early XML based versions. Once component scanning was introduced it greatly reduced the amount of manual configuration. Scanning a package would include all the annotated components contained therein and voila, the application is all wired up. The problem with this powerful feature arises when little thought goes into how components are packaged (grouped) together. Adding a new component to an existing package means that it will be loaded wherever that package is scanned and that is not always desired. The configuration "smell" is usually observed in a lot of exclusion filters where a package is scanned.

Spring Graph - Identifying The Gate Crashers

Spring Graph was developed to help with identifying unwanted components by visualizing the runtime state of a Spring application context in the form of an Object Graph.

Setup

The dependencies are not hosted on a public maven repository, using this will require checking out the project and building it using gradle (Node and Grunt also needs to be installed).

git clone git@github.com:monkey-codes/spring-graph.git
cd spring-graph && ./gradlew install

This will install a few JAR's in the local maven repository under codes.monkey.springgraph:spring-graph-*

The spring-graph-web artifact doubles as a Spring Boot Starter, simply adding it to the classpath of a Spring Boot Application will automatically configure it.

Spring MVC Applications require a manual setup, below is an example of a basic MVC configuration in Groovy.

package codes.monkey.sampleapp

import codes.monkey.springgraph.SpringGraphAutoConfiguration
// other imports ...
@Import(SpringGraphAutoConfiguration)
@Configuration
class MVC extends WebMvcConfigurationSupport {

    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        String staticPathPattern = '/**'
        if (!registry.hasMappingForPattern(staticPathPattern)) {
            registry.addResourceHandler(staticPathPattern,)
                    registry.addResourceHandler(staticPathPattern)
                            .addResourceLocations('classpath:/public/')
        }
    }
}

Usage

Spring Graph will add a few URL's to the application:

  • http://${host}:${port}/${context}/spring-graph-ui/index.html - Renders the current application's context graph using visjs. Large contexts might take some time to render.
  • http://${host}:${port}/${context}/spring-graph-ui/api/visjs - Context graph data exposed in a visjs JSON format.
  • http://${host}:${port}/${context}/spring-graph-ui/api/dot - Graph data in DOT format.
  • http://${host}:${port}/${context}/spring-graph-ui/image/png - Generates a PNG from the graph data. Note that the Graphviz binary needs to be installed and on the PATH for this functionality to work.
  • http://${host}:${port}/${context}/spring-graph-ui/image/svg - Same as above but in SVG format.

Screenshots

The context graph in the visjs interface

The context graph as a generated PNG

Unusual Suspects

Armed with the graph information a developer, with good knowledge of the application, might be able to identify unwanted components that are being included in the context. In my experience an unwanted component usually shows up because it has been added to an existing package that is being scanned. A classic example of this is when packages are used to group components together by type instead of by the functionality it supports. The aim should be to group components that work together in the same package. The package then represents a module, a group of components that work together to achieve a specific goal.

Consider the following oversimplified package designs:

Grouped By Type

It is clear to see that scanning the controllers, services and models packages will over time include more components as the application grows. To avoid loading unwanted components one has to rely on exclusion filters to ensure that the components are only loaded where it's needed. IMHO packaging components together by the role that it plays is rarely a useful grouping. This design also shows its shortcomings when certain functionality needs to be extracted. Imagine having to load the search functionality in a separate context (or application), each dependency will have to be cherry picked from the individual packages.

Grouped By Functionality


The 2nd package structure simplifies application configuration. Only modules (packages) that are required by the application are scanned. Over time only new components that are added to a module, as it evolves, will be automatically included. The chances of including unwanted components by accident are far less. This design also holds better under the challenge of separating out a module, all related components are grouped in a single package.

Conclusion

The same principles that apply to good object orientated design also applies to package design. Classes in a package should be cohesive while coupling between packages should be loose.