Spring Series - Picking Up Spring-Managed Beans through Class Path Scanning

Introduction

In the previous article, we learned about the Spring Container. In this article, we check how to add new beans to the Spring container. There are two different approaches that we can use to add beans to our container.

  1. Declaratively (through annotations or XML).

  2. Registering components manually

Let’s first check how to use the declarative mechanism with the annotations approach.

Using Annotations

Let’s create a new file named, DemoComponent.java. To create that file, first create a new package named, demo and add the following file there.

public class DemoComponent{
    public void demoFunction(){
        System.out.println("Inside Demo Function!");
    }
}

The DemoComponent class has a parameterless constructor and we can create a new object of it with the keyword new. However, we need it to be created and registered by the Spring Framework. In Spring we can do that easily by using annotations.

@Component
public class DemoComponent{
    public void demoFunction(){
        System.out.println("Inside Demo Function!");
    }
}

This annotation comes from the org.springframework.stereotype package and if we investigate it more we can find the following.

@TargetType(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Indexed
public @interface Component{
...
}

As you can see, the annotation is itself annotated. We call these types of annotations meta-annotations.

  • @Target(ElementType.TYPE)→That @Component may only be attached to type declarations.

  • @Retention→ That the annotation is accessible at runtime via reflection.

  • @Documented→That a placed @Component annotation itself appears in the Java documentation of that class.

  • @Indexed →Indicate that the annotated element represents a stereotype for the index.

Now, if we start our application with the following, we should be able to see our component with the name, demoComponent.

@SpringBootApplication
public class DemoApplication{
    public static void main(String args[]){
        ApplicationContext ctx = SpringApplication.run(DemoApplication.class, args);
        Arrays.stream(ctx.getBeanDefinitions()).forEach(System.out::println);
    }
}

In summary, all classes that are annotated with @Component are automatically detected, instantiated, and come into the container as a Spring-managed bean. This is due to the @SpringBootApplication annotation in our DemoApplication class.

Let’s take a closer look at the @SpringBootApplication annotation to get an idea about it.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
    @Filter(type=FilterType.CUSTOM, classes=TypeExcludeFilter.class),
    @Filter(type=FilterType.CUSTOM, classes=AutoConfigurationExcludeFilter.class)
})
public @interface SpringBootApplication{
...
}

The annotation @ComponentScan is responsible for getting all the classes that are annotated with @Component (there are some other stereotypes as well, we’ll check them later)to the Spring container.

@Repository, @Service, @Controller

Besides the @Component annotation, there are other annotations, that lead to a new Spring-managed bean. @Component only says that this is just a component. But components have a purpose, and to better document them, we can use these other annotations.

  • @Service→ Classes that execute the business logic

  • @Repository→ Classes that go to data stores

  • @Controller → Classes that accept requests from the front end.

As these annotations are intended for proper documentation, there is no specific distinction between them if we consider their functionality. We can use @Repository for classes that execute business logic, and it will work without any issues.

@ComponentScan

The run(…) method in our DemoApplication is passed as a start configuration. The start configuration is either marked with @Configuration or annotated with @Component. Instead of passing it into run(…), this configuration can be passed to the constructor of SpringApplication.

  • SpringApplication(Class<?>… primarySources)

  • static ConfigurableApplicationContext run(Class<?> primarySource, String … args)

  • static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args)

The second option is the default option selected when we create our initial application through Spring Initializr.

Since our class, DemoApplication is used as the primary source for the run(…) method, it is the start configuration. The start configuration was the class annotated with @SpringBootApplication. And as we saw it has the following three annotations.

  • @SpringBootConfiguration

  • @EnableAutoConfiguration

  • @ComponentScan

The annotation @SpringBootConfiguration is a special @Configuration annotation, and that in turn is a @Component. And components are automatically recognized through @ComponentScan annotation.

basePackages

The @ComponentScan annotation scans all the components in our project by default. However, we can limit that by adding another @Configuration class, that uses @ComponentScan with the basePackages argument.

@Configuration
@ComponentScan(basePackages={"com.example.demo.demo", "com.example.demo.demo1"})

The basePackages is an alias in the context of @ComponentScan. Therefore, we can even use it as shown below.

@Configuration
@ComponentScan({"com.example.demo.demo", "com.example.demo.demo1"})

Similarly, we can use this with @SpringBootApplication annotation as well, since it has @SpringBootConfiguration annotation inside it. This will scan only the components in the mentioned packages.

@SpringBootApplication(scanBasePackages = {"com.example.demo.demo"})
...

basePackageClasses

Rather than specifying the packages as an array of strings, we can also specify classes as well.

@Configuration
@ComponentScan(basePackageClasses={A.class, B.class})

This is also possible with @SpringBootApplication annotation.

@SpringBootApplication(scanBasePackageClasses = {A.class, B.class})
...

However, the A and B classes can be moved to another package at some time. Therefore, to avoid any issues that may occur with that kind of scenario we can define an empty interface and use it in the @ComponentScan.

package com.example.demo.demo;
public @interface CoreModule{}
@ComponentScan(basePackageClasses = {CoreModule.class})

includeFilters

With basePackages and basePackageClasses,we can use filters to control which types should be included in the packages.

@Configuration
@ComponentScan(
    useDefaultFilters=false,
    includeFilters = @ComponentScan.Filter(
        type = FilterType.ASSIGNABLE_TYPE,
        classes = CoreModule.class
    )
)

The useDefaultFilters=false controls that not every @Component is automatically detected and logged in.

excludeFilters

Similar to the includeFilters, there is an argument which does the exact opposite of includeFilters. If we don’t want to include the types of specific classes, we can use this.

@Configuraion
@ComponentScan(
    excludeFilters = @ComponentScan.Filter(
        type = FilterType.ASSIGNABLE_TYPE,
        classes = CoreModule.class
    )
)

With both includeFilters and excludeFilters we can use FilterType.REGEX which uses regex patterns to find the classes.

@Configuraion
@ComponentScan(
    excludeFilters = @ComponentScan.Filter(
        type = FilterType.REGEX,
        pattern = ".*(Core)"
    )
)

If the mentioned filters do not work, you can create a custom filter and use it with FilterType.CUSTOM. For that, you need to create a class and implement the TypeFilter interface.

public class ComponentScanCustomFilter implements TypeFilter {

    @Override
    public boolean match(MetadataReader metadataReader,
      MetadataReaderFactory metadataReaderFactory) throws IOException {
        ClassMetadata classMetadata = metadataReader.getClassMetadata();
        String fullyQualifiedName = classMetadata.getClassName();
        String className = fullyQualifiedName.substring(fullyQualifiedName.lastIndexOf(".") + 1);
        return className.length() > 5 ? true : false;
    }
}
@Configuration
@ComponentScan(includeFilters = @ComponentScan.Filter(
        type = FilterType.CUSTOM,
        classes = ComponentScanCustomFilter.class
    )
)

So this is it regarding, the Picking Up Spring-Managed Beans through Class Path Scanning. In the next article, let’s look at how you can build an interactive application with Spring Shell.