00 Introduction 5 Minutes Easy Understanding of Spring Fundamentals

00 Introduction 5 Minutes Easy Understanding of Spring Fundamentals #

Hello, I’m Fu Jian.

Before we begin with the first chapter of our study, I want to summarize the most basic knowledge about Spring for you. This can help facilitate our learning progress in the following chapters.

In the first chapter, we focus on various error cases in using Spring’s core functionalities. In our explanations, we mostly get straight to the point, as this is the approach taken in this column. So, for many basic knowledge and processes, we won’t repeatedly introduce them during the analysis process. However, they are still important and serve as the basis for problem-solving. With the help of this introduction, I will guide you through.

Looking back at Spring itself, what are the most fundamental knowledge about Spring?

Actually, it refers to the fundamental implementations and principles of Spring. When you first start learning, you may be confused about why we use Spring. However, as you delve into the principles and the application of Spring, you will gradually discover that the greatest benefit lies in understanding this initial confusion. Now, let me explain it to you.

When doing “traditional” Java programming, the relationships between objects are tightly coupled. For example, if a service class, Service, uses a component, ComponentA, you may write code like this:

public class Service {
    private ComponentA component = new ComponentA("first component");
}

Before Spring, you might not see any major problems with this code, as everyone writes code like this and there doesn’t seem to be a better way. It’s like when there is only one road to take, and everyone is going in the same direction, you’re unlikely to question whether there’s a shortcut.

As the project progresses, you will find that one of the key criteria for evaluating whether a method is good or not is whether it has the ability to embrace change. For example, if one day the constructor of our ComponentA class needs more parameters, you will find that the above code needs to be improved everywhere with this line of code:

private ComponentA component = new ComponentA(“first component”);

At this point, you may think, can I construct the Service in the following way instead?

public class Service {
    private ComponentA component;
    public Service(ComponentA component){
      this.component = component;
    }
}

Of course, this won’t work. You overlooked one thing - when constructing the Service object, don’t you still need to use the new keyword to create the Component? There are many places where you need to make modifications!

Clearly, this is a nightmare. Besides this issue, are there any other problems? The above example is for the non-singleton case, but what if ComponentA is a singleton itself? Would that be better? After all, we might just create one instance of ComponentA in one place, but you might also encounter other problems.

Here is an example of implementing ComponentA using “double-checked locking”:

public class ComponentA{  
    private volatile static ComponentA INSTANCE;  
     
    private ComponentA() {}  
     
    public static ComponentA getInstance(){  
        if (INSTANCE== null) {  
            synchronized (ComponentA.class) {  
                if (INSTANCE== null) {  
                    INSTANCE= new ComponentA();  
                }  
            }  
        }  
        return INSTANCE;  
    }  
}

In the end, we only need a singleton by writing so much code. What’s more, if we have ComponentB, ComponentC, ComponentD, etc., don’t we have to write the repetitive code mentioned above for each of them? It’s just too annoying, isn’t it?

Besides these two typical issues, there are other disadvantages such as difficulties in testing and extending functionality (such as AOP support). In essence, one of the root causes of all these problems is that the coupling between objects is too strong.

This is where Spring comes in - to solve these various problems. So how does it solve them?

Here’s an analogy to renting a house. Why do we like to rent a house through a real estate agent? It’s because it’s convenient! By paying a small fee, we don’t have to directly deal with the landlord.

That’s exactly the idea of Spring - it acts like a “real estate agent” company. When you need a dependent object (a house), you simply tell Spring (the agent) your requirements, and it will handle these dependency objects for you, creating them as needed, without requiring any additional action from you.

However, in Spring, both the landlord and the tenant are object instances, but they are referred to as beans.

You can say that by following a stable production process, Spring, as a “real estate agent”, completes the tasks of production and pre-assembly (bridging the gap) of these beans. At this point, you may want to know more. For example, how exactly does it work when a bean (the tenant) needs to use another bean (the house)?

In essence, you can only rely on the Spring “real estate agent” to find them. Sometimes, we look for them directly based on their names (the name of the housing estate), and sometimes based on their types (the floor plan). The approach can vary greatly. You can think of Spring as a company that works like a Map, with the following implementation:

public class BeanFactory {

    private Map<String, Bean> beanMap = new HashMap<>();
    
    public Bean getBean(String key){
      return beanMap.get(key) ;
    }

}

As shown in the above code, the Bean company provides operations for manipulating maps to perform lookups. After finding the Bean, it is assembled to other objects. This is the process of dependency lookup and autowiring.

Now let’s look back, how are these Beans created?

For a project, there are inevitably two situations: some objects need to be managed by Spring, while others (such as other classes in the project and classes in dependent JARs) do not. Therefore, we need a way to identify which ones need to become Spring Beans, hence various annotations such as the Component annotation.

With these annotations, who is responsible for “discovering” them? Manual configuration is not a problem, but obviously “automatic discovery” is more convenient. At this time, we often need a scanner. We can simulate writing such a scanner:

public class AnnotationScan {

    // Scan packages to find Beans
    void scan(String packages) {
        // 
    }

}

With the scanner, we know which classes need to become Beans.

How are they instantiated as Beans (just an object instance)? Obviously, reflection is the only way. However, there may be multiple ways to accomplish this:

  • java.lang.Class.newInstance()
  • java.lang.reflect.Constructor.newInstance()
  • ReflectionFactory.newConstructorForSerialization()

Having created and assembled, a Bean can become what it wants to be.

And there are always continuous demands. Sometimes we want to record the performance of a method call, and sometimes we want to output uniform log messages during method calls. We certainly don’t want frequent scattered modifications. So we have AOP to intercept method calls and perform functionality extensions. Who to intercept? Naturally, it is the Bean in Spring.

In fact, AOP is not magical. From the perspective of the Bean (intermediary) company we just mentioned, suppose we determine that a Bean needs to be “enhanced”. We can simply use a proxy object as the return object when it returns from the company, right? For example:

public class BeanFactory {

    private Map<String, Bean> beanMap = new HashMap<>();

    public Bean getBean(String key){
        // Check if it has been created
        Bean bean = beanMap.get(key);
        if(bean != null){
            return bean;
        }
        // Create a new Bean
        Bean bean = createBean();
        // Check if AOP is needed
        boolean needAop = judgeIfNeedAop(bean);
        try{
            if(needAop)
                // Create proxy object
                bean = createProxyObject(bean);
                return bean;
            else:
                return bean
        }finally{
            beanMap.put(key, bean);
        }
    }
}

How do we know if an object needs AOP? If an object needs AOP, it must have been marked with some “rules”, such as intercepting a certain method of a certain class. For example:

@Aspect
@Service
public class AopConfig {
    @Around("execution(* com.spring.puzzle.ComponentA.execute()) ")
    public void recordPayPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
        //
    }
}

At this point, it’s obvious that if your Bean name is ComponentA, you should return a proxy object of type ComponentA. As for how these rules are established, you can understand the rules by looking at the various annotations used, which are basically scanning the annotations and creating rules based on the annotations.

The above are some core concepts of Spring, including Bean construction, autowiring, and AOP. There are countless details in between, but they are not important. Grasping these core concepts will be very beneficial for understanding various types of error cases in the future!

Hello, I’m Fu Jian. In this lesson, let’s talk about some issues related to the initialization and destruction processes of Spring Beans.

Although the Spring container is easy to use and can be quickly started by learning a few limited annotations, we still encounter some common errors in engineering practice. Especially when you don’t have a deep understanding of Spring’s lifecycle, the implicit conventions during class initialization and destruction processes may not be very clear.