1

We are fully annotation driven and do not use XML files for spring configuration.

Default scope of spring beans is singleton which many developers forget and end up in creating beans that should be differently scoped. Added to the complexity of problems mix and match of various scoped beans.

Is there any maven plugin that can check if any class that has @Component annotation also has @Scope annotation and fail the build if its missing. This will force developers to think about the scope and usage patterns. If something similar does not exists, I can write the plugin or have a custom tool that can check this and fire during jenkins build. Can any of spring code help me to do this?

Additionally if there is @Autowire annotation in spring bean, is there a way to validate that beans being injected have right scopes. I am working with the assumption if you inject prototype scoped bean in singleton scoped bean, most likely that is not what you wan. Though there might be use cases where this is what developer want, in our case, so far this is mostly been developer mistake.

Ben Minton
  • 118
  • 5
user871199
  • 1,420
  • 19
  • 28
  • I do not know about doing the validation with Spring itself. You may be able to do this with a custom PMD or FindBugs rule. – user944849 Nov 21 '14 at 20:28
  • You might take a look to maven-enforcer-plugin where you can implement your own rule which might be a path for you... – khmarbaise Nov 22 '14 at 17:05

3 Answers3

0

You can use AspectJ's ability to declare errors and/or warnings based on pointcuts.

Disclaimer: I have never used Spring, so I am not an expert there and just making up an example without much sense for demonstration.

Spring bean with prototype scope:

package de.scrum_master.app;

import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

@Component
@Scope("prototype")
public class ScopedBean {}

Spring bean with missing scope declaration:

package de.scrum_master.app;

import org.springframework.stereotype.Component;

@Component
public class UnscopedBean {}

Spring bean with using different types of auto-wiring:

This bean uses constructor and setter method wiring. If you uncomment the annotation on the field declaration you can even use another type of wiring. This does not make sense, but we want to provoke compilation errors in an aspect later.

package de.scrum_master.app;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

@Component
@Scope("singleton")
public class BeanWithAutowire {
    //@Autowired
    private ScopedBean scopedBean;

    @Autowired
    public BeanWithAutowire(ScopedBean scopedBean) {
        this.scopedBean = scopedBean;
    }

    @Autowired
    public void setScopedBean(ScopedBean scopedBean) {
        this.scopedBean = scopedBean;
    }
}

Aspect for static annotation consistency checking:

package de.scrum_master.aspect;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

public aspect BeanAnnotationChecker {
    declare error :
        @annotation(Component) && !@annotation(Scope) :
        "Spring component without scope declaration found";

    declare error :
        execution(@Autowired *.new(.., @Scope("prototype") *, ..)) && within(@Scope("singleton") *) :
        "singleton bean auto-wired into prototype container via constructor";

    declare error :
        execution(@Autowired * *(.., @Scope("prototype") *, ..)) && within(@Scope("singleton") *) :
        "singleton bean auto-wired into prototype container via setter method";

    declare error :
        set(@Autowired * *) && within(@Scope("singleton") *) :
        "singleton bean auto-wired into prototype container via field assignment";
}

Maven POM using AspectJ compiler:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>de.scrum-master.stackoverflow</groupId>
    <artifactId>aspectj-fail-build</artifactId>
    <version>1.0-SNAPSHOT</version>

    <name>AspectJ - fail build for wrong/missing annotations</name>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.source-target.version>1.7</java.source-target.version>
        <aspectj.version>1.8.4</aspectj.version>
        <main-class>de.scrum_master.app.ScopedBean</main-class>
    </properties>

    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.1</version>
                    <configuration>
                        <source>${java.source-target.version}</source>
                        <target>${java.source-target.version}</target>
                        <!-- IMPORTANT -->
                        <useIncrementalCompilation>false</useIncrementalCompilation>
                    </configuration>
                </plugin>
                <plugin>
                    <groupId>org.codehaus.mojo</groupId>
                    <artifactId>aspectj-maven-plugin</artifactId>
                    <version>1.7</version>
                    <configuration>
                        <showWeaveInfo>true</showWeaveInfo>
                        <source>${java.source-target.version}</source>
                        <target>${java.source-target.version}</target>
                        <Xlint>ignore</Xlint>
                        <complianceLevel>${java.source-target.version}</complianceLevel>
                        <encoding>UTF-8</encoding>
                        <verbose>true</verbose>
                    </configuration>
                    <executions>
                        <execution>
                            <!-- IMPORTANT -->
                            <phase>process-sources</phase>
                            <goals>
                                <goal>compile</goal>
                                <goal>test-compile</goal>
                            </goals>
                        </execution>
                    </executions>
                    <dependencies>
                        <dependency>
                            <groupId>org.aspectj</groupId>
                            <artifactId>aspectjtools</artifactId>
                            <version>${aspectj.version}</version>
                        </dependency>
                    </dependencies>
                </plugin>
            </plugins>
        </pluginManagement>

        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>aspectj-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.aspectj</groupId>
                <artifactId>aspectjrt</artifactId>
                <version>${aspectj.version}</version>
                <scope>runtime</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>4.0.7.RELEASE</version>
        </dependency>
    </dependencies>

</project>

Console output for mvn clean package:

(...)
[INFO] ------------------------------------------------------------------------
[INFO] Building AspectJ - fail build for wrong/missing annotations 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
(...)
[ERROR] singleton bean auto-wired into prototype container via constructor
    C:\Users\Alexander\Documents\java-src\SO_AJ_MavenFailBuildOnWrongAnnotation\src\main\java\de\scrum_master\app\BeanWithAutowire.java:14
public BeanWithAutowire(ScopedBean scopedBean) {
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

[ERROR] singleton bean auto-wired into prototype container via setter method
    C:\Users\Alexander\Documents\java-src\SO_AJ_MavenFailBuildOnWrongAnnotation\src\main\java\de\scrum_master\app\BeanWithAutowire.java:19
public void setScopedBean(ScopedBean scopedBean) {
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

[ERROR] Spring component without scope declaration found
    C:\Users\Alexander\Documents\java-src\SO_AJ_MavenFailBuildOnWrongAnnotation\src\main\java\de\scrum_master\app\UnscopedBean.java:6
public class UnscopedBean {}
             ^^^^^^^^^^^

[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
(...)

I think the example is somewhat self-explanatory except for the AspectJ syntax, but you can read more about that in AspectJ manuals or tutorials. If you uncomment the @Autowired annotation on the field declaration, you will see even more errors for explicit field assignments. AspectJ cannot match on a mere field declaration (without assignment), though. So whenever your developers rely on field annotations instead of annotated constructors or setter methods, i.e. you do not have any explicit field assignment in your code, there will be no compilation error. You can work around that by matching on getter methods or field read access in your code to indirectly match on fields. Feel free to ask how to do that if you cannot figure it out by yourself.

kriegaex
  • 63,017
  • 15
  • 111
  • 202
0

The simplest idea I can think of is using Checkstyle's RegexpSinglelineJava check to ensure 1 and only 1 of @Scope exists per file: http://checkstyle.sourceforge.net/config_regexp.html#RegexpSinglelineJava

Since you need to check for two annotations, perhaps the RegexpMultiline check will work (would require the annotations ordered consistently): http://checkstyle.sourceforge.net/config_regexp.html#RegexpMultiline

Jeff
  • 956
  • 8
  • 10
0

I recently also needed to verify the scope of @Autowired beans and could not find any suitable out of the box solution. Thus I've created small project that allows for the validation of bean scopes at runtime. By default, it allows for the following injections:

  • Singletons can be injected into everything
  • Everything can be injected into prototypes
  • AOP proxies can be injected into everything
  • Everything can be injected into beans of the same scope

If you want to allow a bean to be injected into another scope, it needs to be explicitly allowed by using a respective annotation:

@Bean
@Scope("prototype")
@InjectableInto("singleton")
MyBean getMyBean(){
 //...
} 

If a bean at runtime uses dependencies of a not allowed scope, it can either log it, throw an exception (and thus prevent the creation of the bean) or perform any custom action.

Jan Gassen
  • 3,406
  • 2
  • 26
  • 44