The core concept of Spring is the IoC (Inversion of Control) container, which is responsible for managing beans. The IoC container is an object that creates other objects (beans) and injects dependencies in them. There are two primary interfaces that represent the IoC container: BeanFactory and ApplicationContext. These interfaces have many implementations for different purposes. In this topic, we will take a closer look at ApplicationContext, and consider some examples.
BeanFactory & ApplicationContext
BeanFactory is a root interface for accessing the Spring IoC container. As for ApplicationContext, it extends it. That is, ApplicationContext gets its basic functionality from BeanFactory and extends it with additional features. For example, ApplicationContext supports AOP integration, event publication, all types of bean scopes, and more. Since ApplicationContext provides more functionality, it is advisable to use it instead of BeanFactory.
In this diagram, you can see some of the implementations of BeanFactory and ApplicationContext:
As you know, there are two ways to create metadata (bean definitions): by using an XML configuration file or annotations. ApplicationContext and BeanFactory objects are created based on metadata. That's why their implementations contain such words as "Xml" or "Annotation", indicating the type of metadata.
The main difference between these interfaces is that BeanFactory doesn't support annotation-based configuration, while ApplicationContext does. This fact gives a significant advantage to ApplicationContext over BeanFactory, because it's recommended to use annotation-based configuration for all new Spring applications.
Creating an application context
Now let's create our own ApplicationContext based on annotation configuration.
Let's say we want to store objects of the Person type in our container. So, first of all, let's create a Person class:
Java
public class Person {
private String name;
public Person(String name) {
this.name = name;
}
}Kotlin
class Person(private val name: String)An application context is created based on a configuration class, which means we need a configuration class that describes what objects (beans) will be created inside the IoC container:
Java
@Configuration
public class Config {
@Bean
public Person personMary() {
return new Person("Mary");
}
}Kotlin
@Configuration
open class Config {
@Bean
open fun personMary() = Person("Mary")
}We filled this configuration class with one bean definition (metadata), based on which a bean will be created inside the container.
Here is what the above code "says" to our ApplicationContext:
Create a
Personobject with the propertynameset toMary.Call the created bean
personMary.Place the bean in the IoC container.
@Configuration contains the @Component annotation inside, which also tells ApplicationContext to create a bean based on the Config class. So, before creating the personMary bean, ApplicationContext also creates a config bean and places it in the IoC container.
Now it's time to create an application context based on the Config class and get all the bean names (just their string names, not objects) from it.
Java
public class Application {
public static void main(String[] args) {
var context = new AnnotationConfigApplicationContext(Config.class);
System.out.println(Arrays.toString(context.getBeanDefinitionNames()));
}
}Kotlin
fun main(args: Array<String>) {
val context = AnnotationConfigApplicationContext(Config::class.java)
println(context.beanDefinitionNames.contentToString())
}If we were using XML configuration, we would create, for example, a ClassPathXmlApplicationContext object.
In the output, among several internal beans necessary for the Spring application, you can see the names of our created beans: config and personMary.
[..., ..., config, personMary]ApplicationContext overloads the getBean() methods inherited from BeanFactory:
Java
T getBean(Class<T> requiredType)Object getBean(String name)T getBean(String name, Class<T> requiredType)
Kotlin
fun getBean(requiredType: Class<T>): Tfun getBean(name: String): Anyfun getBean(name: String, requiredType: Class<T>): T
Let's get our Person bean (whole object) from the container by passing the Person class:
Java
context.getBean(Person.class); // returns a Person objectKotlin
context.getBean(Person::class.java) // returns a Person objectIf there are several beans of the same class in the container, you need to specify a unique bean name in the getBean() method. Otherwise, an exception will be thrown.
Java
context.getBean("personMary"); // returns an Object object
context.getBean("personMary", Person.class); // returns a Person objectKotlin
context.getBean("personMary") // returns an Any object
context.getBean("personMary", Person::class.java) // returns a Person object@ComponentScan
Another way to create a bean is by using the @Component annotation placed above a class.
Let's create two @Component classes: Book and Movie:
Java
@Component
public class Book {
}
@Component
public class Movie {
}Kotlin
@Component
open class Book {
}
@Component
open class Movie {
}We aim to put these components into the same application context that is based on the Config class. For the configuration class to know about the existence of the @Component classes, the @ComponentScan annotation is used. We put this annotation above the configuration class name:
Java
@ComponentScan
@Configuration
public class Config {
// bean definitions
}Kotlin
@ComponentScan
@Configuration
open class Config {
// bean definitions
}By default, @ComponentScan scans all the classes in the current package and all its sub-packages, looking for @Component classes (and all other annotations containing @Component, such as @Service, @Configuration, and so on).
Now our context knows about the new beans: book and movie.
Java
public class Application {
public static void main(String[] args) {
var context = new AnnotationConfigApplicationContext(Config.class);
// [..., ..., book, config, movie, personMary, ..., ...]
System.out.println(Arrays.toString(context.getBeanDefinitionNames()));
}
}Kotlin
fun main(args: Array<String>) {
val context = AnnotationConfigApplicationContext(Config::class.java)
// [..., ..., book, config, movie, personMary, ..., ...]
println(context.beanDefinitionNames.contentToString())
}You can change the default behavior of @ComponentScan and explicitly specify one or more base packages for scanning:
Java
@ComponentScan(basePackages = "packageName")Kotlin
@ComponentScan(basePackages = ["packageName"])In @ComponentScan, the value attribute is defined as an alias of basePackages and vice versa. Therefore, you can use them interchangeably. All these variants work in the same way:
Java
@ComponentScan(basePackages = "packageName")
@ComponentScan(value = "packageName")
@ComponentScan("packageName")Kotlin
@ComponentScan(basePackages = ["packageName"])
@ComponentScan(value = ["packageName"])
@ComponentScan("packageName")ApplicationContext in Spring Boot
Usually, our class is the entry point of a Spring Boot application. Therefore, we annotate it with the @SpringBootApplication annotation. Additionally, we run the application in the main method.
Java
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}Kotlin
@SpringBootApplication
open class Application
fun main(args: Array<String>) {
runApplication<Application>(*args)
}During the execution of the run method, an application context is created. We can get the context and see what bean definitions it contains:
Java
@SpringBootApplication
public class Application {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(Application.class, args);
System.out.println(Arrays.toString(context.getBeanDefinitionNames()));
}
}Kotlin
@SpringBootApplication
open class Application
fun main(args: Array<String>) {
val context = runApplication<Application>(*args)
println(context.beanDefinitionNames.contentToString())
}In the output, you can see an array of the bean names used in the application. Even if we didn't create our own beans, we see a lot of internal beans created by Spring Boot to keep the application running.
@SpringBootApplication contains the @ComponentScan annotation, so our Spring Boot context will know about our custom @Component (@Configuration, @Service, @Repository, and so on) classes.
Another way to access the ApplicationContext is to use the @Autowired annotation. Just inject the context created in the SpringApplication.run(...) method into another component. This allows us to get the context anywhere in the application.
Let's get all bean names from ApplicationContext inside the Runner class:
Java
@Component
public class Runner implements CommandLineRunner {
@Autowired
ApplicationContext applicationContext;
@Override
public void run(String... args) throws Exception {
System.out.println(Arrays.toString(applicationContext.getBeanDefinitionNames()));
}
}Kotlin
@Component
open class Runner(var applicationContext: ApplicationContext) : CommandLineRunner {
override fun run(vararg args: String) {
println(applicationContext.beanDefinitionNames.contentToString())
}
}Conclusion
In this topic, we learned about the ApplicationContext interface representing the core Spring concept: the IoC container. It inherits from BeanFactory and has methods for managing beans in the container. There are many implementations of ApplicationContext, for example, AnnotationConfigApplicationContext for annotation-based configuration and ClassPathXmlApplicationContext for XML configuration. In Spring Boot, the SpringApplication.run() method creates a context based on found beans, making our life much easier!