The Brief Introduction of Spring
For the Spring Framework, there are two most important concepts: IoC and AOP.
This post will introduce these two concepts, some other important concepts in Spring and some commonly used annotations in Spring.
What is a Bean?
A bean is an object that is instantiated, assembled, and managed by a Spring IoC container.
Bean and IoC
Without using Spring for development, if you need to create an object, you usually create it with the new
keyword. In this case, the creation and destruction of the object are controlled by the programmer (or your source code). In contrast, IoC refers to Inversion of Control, which means that the creation and destruction of objects are no longer controlled by the programmer (or your source code).
In Spring, the creation and destruction of objects are controlled by the Spring’s IoC container. Programmers only need to specify the configuration of the object, and the Spring container will create or destroy the object automatically.
With IoC, we can finish some work easily. For example, if we implement an interface with different implementations, and then inject one implementation into the class that uses it. We can easily switch between different implementations without modifying the code of the class that uses it.
Dependency Injection
Dependency Injection is a design pattern used to implement IoC. It allows a class to receive its dependencies from an external source rather than creating them itself.
Use Annotations to Add a Bean
@Configuration
@Configuration
is an annotation used to specify that the current class is a configuration class, which is equivalent to a Spring
configuration xml
file. In the configuration class, you can use the @Bean
annotation to register beans into the Spring
container.
@Import
@Import
annotation in Spring is used to include additional configuration classes or components into the application context. It allows you to modularize your configuration by combining multiple configuration sources into a single context.
We rarely use this annotation in Spring Boot, because Spring Boot automatically scans the package of the class where @SpringBootApplication
is located and its sub-packages. We usually use @Configuration
or @Component
to register those classes as beans.
@Bean
@Bean
is an annotation used to specify that a method is a bean producer, and the return value is a bean.
@Bean
can specify the name of the bean. If the name of the bean is not specified, the name of the bean defaults to the method name.
@Named
and @ManagedBean
These two annotations are used to specify that the current class is a bean, which are similar to @Bean
but from JSR330
.
@Order
This is used to specify the order of beans in the IoC container.
NOTE: @Order
can not be used to specify the injection priority of beans.
@Priority
This is used to specify the priority of a bean. Smaller values indicate higher priority.
When injecting beans, the bean with the highest priority will be injected first.
@Primary
and @Fallback
@Primary
is used to specify that the current bean is the primary bean, which means that when there are multiple beans of the same type, the bean marked with @Primary
will be injected.
@Fallback
is used to specify that the current bean is a fallback bean, which means that when there are multiple beans of the same type, the bean marked with @Fallback
will not be injected.
NOTE: @Primary
is higher priority than @Priority
.
@DependsOn
This annotation is used to mark this bean will depend on another bean, which means the depended bean will be loaded before current bean.
@Lazy
When marked with @Lazy
, the bean will be loaded lazily, which means it will be loaded when it is first needed.
@Component
@Component
annotation is used to specify that the current class is a bean. The default name of the bean is the class name with the first letter in lowercase.
@Controller
@Controller
is similar to @Component
, but it indicates a controller.
@RestController
@RestController
is a specialized version of @Controller
. It is used to indicate that the class is a controller and that the methods in the class will return JSON or XML data.
This annotation is a combination of @Controller
and @ResponseBody
.
@Service
@Service
is similar to @Component
, but it indicates a service.
@Repository
@Repository
is similar to @Component
, but it indicates a repository (DAO).
@ComponentScan
@ComponentScan
is an annotation used to specify which classes will be scanned by Spring. For example, @ComponentScan("com.example")
indicates scanning com.example
.
In most cases, we do not need to use this annotation in Sprint Boot. This is because Spring Boot will automatically scan the package where the class with @SpringBootApplication
is located and its sub-packages. This is because @SpringBootApplication
contains the @ComponentScan
annotation.
@Description
@Description
is an annotation used to add a description to a bean. We can use Spring
’s BeanFactory
to get the description of the bean.
@Profile
@Profile
is an annotation used to specify the environment of a bean. For example, @Profile("dev")
indicates that this bean will only be effective in the dev
environment.
@Nullable
@Nullable
is an annotation used to mark a field that can be null
. This means that when Spring
injects the bean, if it does not find the corresponding bean, it will not throw an exception.
@NonNull
@NonNull
is an annotation used to mark a field that cannot be null
. When Spring
injects the bean and does not find the corresponding bean, it will throw an exception.
Bean Scopes
The scopes of Spring beans are as follows:
Scope | Description |
---|---|
singleton | The default, only one instance |
prototype | Every injection will create a new instance |
request | Each HTTP request will create a new instance |
session | Each HTTP session will create a new instance |
application | Each ServletContext will create a new instance |
websockt | Each WebSocket will create a new instance |
You can use those annotations below to specify the scope of a bean:
-
@Scope("singleton")
or@Singleton
-
@Scope("prototype")
or@Prototype
-
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
or@RequestScope
-
@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
or@SessionScope
-
@Scope(value = "application", proxyMode = ScopedProxyMode.TARGET_CLASS)
or@ApplicationScope
@Scope(value = "websocket", proxyMode = ScopedProxyMode.TARGET_CLASS)
For request
, session
, application
, and websocket
, we must specify proxyMode = ScopedProxyMode.TARGET_CLASS
when using @Scope
. This is because the life cycle of these scopes is shorter. Singleton objects may depend on these objects, and if proxyMode
is not used, the dependencies in the singleton may be expired.
Autowiring
The autowiring is the process of automatically injecting dependencies into a bean. Those below are the common autowiring modes in Spring:
Mode | Description |
---|---|
byName | Autowired by the name of the bean |
byType | Autowired by the type of the bean |
@Autowired
@Autowired
is an annotation used to specify that a field, constructor, or method is to be autowired.
By default, @Autowired
will use byType
to autowire the bean. If there are multiple beans of the same type or none at all, Spring will throw an exception.
When using @Autowired
to an array or collection type field, Spring will inject all matching beans, and the order of the beans is determined by the priority of the beans.
If the @Autowired
annotation is placed on a Map
type field, Spring will inject all matching beans, with the bean name as the key and the bean as the value.
You can use @Autowired(required = false)
to indicate that the dependency is optional. This means that if the bean is not found, Spring will not throw an exception.
If there are multiple beans of the same type, the bean annotated with @Primary
will be injected.
NOTE: As for Spring Framework 4.3, you can omit the @Autowired
annotation on a constructor, if the class contains only one constructor.
NOTE: A non-required method will not be called at all if its dependency (or one of its dependencies, in case of multiple arguments) is not available. A non-required field will not get populated at all in such cases, leaving its default value in place.
@Qualifier
You can use @Qualifier
with @Autowired
to specify the name of the bean to be injected.
@Inject
@Inject
is an annotation from JSR330
, which is similar to @Autowired
to some extent.
@Resource
This annotation is from JSR250
, you can use this annotation to inject a bean by its name. However, @Resource
is only allowed to be used on fields and setter methods. When the name is not explicitly specified, if the annotation is placed on a field, the name of the field will be used as the bean name. If the annotation is placed on a setter method, the name of the setter method (with set
removed and the first letter in lowercase) will be used as the bean name.
Besides, @Resource
can also be used to inject a bean by its type
property.
When only the name
is specified, Spring
will inject the bean by its name. When only the type
is specified, Spring
will inject the bean by its type. When both name
and type
are specified, Spring
will first try to inject the bean by its name; if the bean is not found, Spring
will then try to inject the bean by its type.
@Value
@Value
is an annotation used to inject values into fields, methods, or constructor parameters. Those below are the common usages of @Value
:
-
@Value("#{}")
: Spring Expression Language (SpEL) -
@Value("${}")
: Properties
Before using @Value
, you may need to write a configuration file. The following is a brief introduction to how to read configuration files. For a Spring Boot
project, after initialization, there will be an application.properties
file in the src/main/resources
directory. This file is the configuration file for Spring Boot
, and you can write some configuration information in this file. Then, you can read it using the @Value
annotation. If this file does not exist, you can create it, and Spring
will automatically load the application.properties
or application.yml
files under the following paths (the priority of these paths is from high to low):
file:./config
file:./
classpath:/config/
classpath:/
When there are both application.properties
and application.yml
files, application.properties
will override application.yml
.
We can place the configuration file in the src/main/resources
directory. For example, we can place XXXConfig.properties
in the src/main/resources
directory, and then we can read the configuration file using the @PropertySource
:
@Configuration
@PropertySource("classpath:XXXConfig.properties")
public class XXXConfig {}
After that, we can use @Value
to read the configuration file. As for Spring 6, we can assign yml
files to @PropertySource
without specifying the factory
attribute.
Other Annotations
@NonNullApi
@NonNullApi
is an annotation used to mark that all parameters and return values of the methods in the current package are not allowed to be null
. This annotation is usually placed above the package
statement.
@NonNullFields
@NonNullFields
is an annotation used to mark that all fields in the current package are not allowed to be null
. This annotation is usually placed above the package
statement.
AOP
AOP (Aspect-Oriented Programming) is a programming paradigm that allows you to separate cross-cutting concerns from the main business logic.
In Spring, AOP is used to provide declarative transaction management, logging, security, and other cross-cutting concerns.
AOP allows you to define aspects, which are reusable modules that encapsulate cross-cutting concerns.
@Aspect
This annotation is used to define a class as an aspect.
@PointCut
This annotation is used to define a method as a pointcut.
This annotation can reduce the code duplication. For example, if you want to add multiple aspect to a same pointcut, you can use @PointCut
to define the pointcut once, then add the aspect to the name of the pointcut (the method name).
@Aspect
@Component
public Class MyAspect {
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceLayer() {}
@Before("serviceLayer()")
public void beforeServiceLayer(JoinPoint joinPoint) {
System.out.println("Before method: " + joinPoint.getSignature());
}
@After("serviceLayer()")
public void afterServiceLayer(JoinPoint joinPoint) {
System.out.println("After method: " + joinPoint.getSignature());
}
}
@Before
This annotation is used to define a method as a before advice.
@After
This annotation is used to define a method as an after advice.
@Around
This annotation is used to define a method as an around advice.
@AfterReturning
This advice will be executed after the target method returns successfully, which means it will not be executed if the target method throws an exception.
@AfterThrowing
This advice will be executed after the target method throws an exception.
NOTE: When there is multiple advice for the same join point, The order of execution is shown below:
-
@Around
(beforeproceed()
) @Before
- Target method execution
@After
-
@AfterReturning
or@AfterThrowing
-
@Around
(afterproceed()
)
Transaction
ACID
ACID is a set of properties that guarantee that database transactions are processed reliably. ACID stands for:
- Atomicity: The transaction is all-or-nothing.
- Consistency: The transaction brings the database from one valid state to another.
- Isolation: The transaction is isolated from other transactions.
- Durability: Once a transaction is committed, its changes persist permanently even after system failures.
Problems
There are four types of problems that may occur:
- Dirty Read: A transaction reads uncommitted data from another transaction.
- Non-Repeatable Read: A transaction reads the same data multiple times, but gets different results because another transaction modified/deleted it.
- Phantom Read: A transaction re-runs a range query and gets new rows inserted by another committed transaction.
- Lost Update: Two transactions read the same data and modify it, but one transaction’s changes are lost due to the other transaction’s changes
- Serialization Anomaly: The result of successfully committing a group of transactions is inconsistent with all possible orderings of running those transactions one at a time.
Isolation Level
There are different isolation levels in transaction management to solve these problems.
From the standard SQL perspective, the isolation levels and their properties are:
Isolation Level | Dirty Read | Non-Repeatable Read | Phantom Read | Serialization Anomaly |
---|---|---|---|---|
Read Uncommitted | Possible | Possible | Possible | Possible |
Read Committed | Not Possible | Possible | Possible | Possible |
Repeatable Read | Not Possible | Not Possible | Possible | Possible |
Serializable | Not Possible | Not Possible | Not Possible | Not Possible |
In PostgreSQL, Dirty Read will not happen even in Read Uncommitted isolation level, Phantom Read will not happen in Repeatable Read isolation level, and the default isolation level is Read Committed.
In MySQL (InnoDB), Pantom Read will not happen in Repeatable Read isolation level with consistent non-locking reads, the default isolation level is Repeatable Read.
Types of Lock-Based Protocols
Lock-based protocols are used to implement transaction isolation levels.
There are two types of lock:
- Shared Lock: Also known as read lock and S-lock. It can be acquired when no other transaction holds a write lock on the same data.
- Exclusive Lock: Also known as write lock and X-lock. It can be acquired when no other transaction holds a shared or write lock on the same data.
First-Level Locking Protocol
The first-level locking protocol requires that a transaction must acquire an X-lock before modifying a data item, and release the lock after the transaction is committed or rolled back.
This is roughly equivalent to Read Uncommitted
isolation level, which means that dirty reads, non-repeatable reads, and phantom reads may occur, but lost updates will not occur.
Second-Level Locking Protocol
The second-level locking protocol extends the first-level locking protocol by requiring S-locks for read operations. S-locks are acquired before reading a data item, and released immediately after the read operation is completed.
This protocol is equivalent to Read Committed
isolation level, which means that dirty reads and lost updates will not occur, but non-repeatable reads and phantom reads may occur.
Third-Level Locking Protocol
The third-level locking protocol extends the second-level locking protocol by requiring that a transaction must hold all S-locks until it commits or rolls back, which means that once a transaction reads a data item, it will hold the S-lock until the transaction is completed.
This protocol is equivalent to Repeatable Read
isolation level, which means that dirty reads, non-repeatable reads, and lost updates will not occur, but phantom reads may occur.
The third-level locking protocol can not prevent phantom reads, because transactions only hold locks for existing data items. For example, if a transaction reads a range of data items another transaction inserts a new data item then commits, and after that the first transaction may read the new data item.
Two-Phase Locking
The two-phase locking is a locking protocol that guarantees conflict-serializable.
NOTE: If a schedule is conflict-serializable, the schedule is equivalent to some serial schedule based solely on conflicting operations.
The core idea of the two-phase locking is that transactions must follow two strict phases for lock management:
- Growing Phase: In this phase, a transaction can acquire locks but cannot release any locks.
- Shrinking Phase: In this phase, a transaction can release locks but cannot acquire any new locks.
There are some variants of the two-phase locking protocol:
- Conservative Two-Phase Locking: In this variant, a transaction must acquire all locks (at one time) before it starts executing. This can prevent deadlocks.
- Strict Two-Phase Locking: In this variant, a transaction must hold X-locks until it commits or rolls back.
- Rigorous Two-Phase Locking: In this variant, a transaction must hold all locks until it commits or rolls back.
@Transactional
@Transactional
is an annotation used to specify that a method or class is transactional.
You can specify the isolation level of the transaction using the isolation
attribute.
There is a propagation
attribute that specifies the transaction propagation behavior. This attribute is used to specify how the transaction should behave when it is called from another transaction. The possible values are:
-
Required
: The default behavior, if a transaction already exists, the method will run within that transaction; otherwise, a new transaction will be created. In this options, no matter the caller or callee throws an exception (even you have catch the exception thrown by the callee in the caller), the whole transaction will be rolled back. -
Requires New
: A new transaction will always be created, and it is independent of the caller’s transaction. In this options, if the caller throws an exception, the callee’s transaction will not be affected; if the callee throws an exception and the caller catches it, the callee’s transaction will be rolled back, but the caller’s transaction will not be affected; if the callee throws an exception and the caller does not catch it, both the callee’s and caller’s transactions will be rolled back. -
Nested
: A new transaction will be created, and it will be a nested transaction of the caller’s transaction. In this options, if the caller throws an exception, both the callee’s and caller’s transactions will be rolled back; if the callee throws an exception and the caller catches it, the callee’s transaction will be rolled back, but the caller’s transaction will not be affected; if the callee throws an exception and the caller does not catch it, both the callee’s and caller’s transactions will be rolled back. -
Supports
: If a transaction already exists, the method will run within that transaction; otherwise, it will run without a transaction. -
Mandatory
: If a transaction already exists, the method will run within that transaction; Otherwise, an exception will be thrown. -
Not Supported
: If a transaction already exists, it will be suspended, and the method will run without a transaction. -
Never
: If a transaction already exists, an exception will be thrown;
Lock in Transaction
When you want to use Java locks in Spring transaction, you should be careful.
From Spring, we know that the Spring transaction is implemented by AOP, and AOP is implemented by dynamic proxy. Be more specific, if a class has implemented at least one interface, Spring will use JDK dynamic proxy to create a proxy object, otherwise, Spring will use CGLIB to create a proxy object.
When you use @Transactional
on a method, the method will be wrapped in a proxy object. You should be aware that the sequence of the method calls is important. The lock may be released before the transaction is committed, which may cause unexpected behavior.
- Start a transaction.
- Call the actual method.
- Acquire a lock in the actual method.
- Perform some operations on the database.
- Release the lock in the actual method. (Other methods may acquire the lock to update)
- Commit or rollback the transaction.
However, in some situations, you can use lock in transaction. For example, if you want to update the shared data rather than the data in the database, you can use lock in transaction.
- Start a transaction.
- Call the actual method.
- Acquire a lock in the actual method.
- Perform some operations on the shared data (such as a shared object).
- Release the lock in the actual method. (It’s OK)
- Perform some operations on the database.
- Commit or rollback the transaction.
Enjoy Reading This Article?
Here are some more articles you might like to read next: