How did Spring - IOC come from

Inversion of Control (IoC for short), yes object-oriented programming A design principle in that can be used to reduce the cost of computers code Between Coupling degree . The most common way is called Dependency injection (Dependency Injection, DI for short), another method is called Dependency Lookup. Through control inversion, when an object is created, the external entities of all objects in a control system pass the references of the objects it depends on to it. In other words, dependencies are injected into objects.

1. Three layer architecture in the era of native Servlet

Let's actually build a project of MVC three-tier architecture in the era of native Servlet as the background board.

(to facilitate the subsequent content demonstration, create an empty project before using IDEA to create a project   spring-framework-projects  , (used to store all subsequent works)

1.1 build Maven based native Servlet project

Using Maven to build projects is the most basic ability. I use IDEA to quickly build a native Servlet project.

In pom dependency, you only need to introduce the servlet api: (here I use servlet 3.1, which doesn't matter, just use   Servlet3.0+   The version of can be developed based on annotations, which is more efficient)

<dependencies>
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>3.1.0</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

Of course, in order to make the compilation level of the project at level 1.8, Maven's compilation plug-in needs to be added: (the version should not be too old, I choose version 3.2 here)

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.2</version>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
                <encoding>UTF-8</encoding>
            </configuration>
        </plugin>
    </plugins>
</build>

Finally, don't forget to adjust the packaging method to war package:

<packaging>war</packaging>

1.2 deploy project to Servlet container

After creating the project, don't worry about writing code. Let's deploy the project to the Servlet container to ensure normal operation. Here, we use Tomcat as a Servlet container to run the project.

Open one by one in IDEA   “File -> Project Structure”  , Select   Artifacts   Label and add   Web Application: Exploded   Type. After configuring the corresponding path and name, you can set the compilation and packaging output configuration. As shown in the figure below:

Next, select from the run bar of the IDEA   Add Configuration...  , And add local Tomcat:

Next, select from the new Tomcat   Deployment  , And add the newly configured   Artifact  :

After adding, you can save OK.

1.3 writing Servlet test is available

stay   src/main/java   Create a new one in   DemoServlet  , tagging  @ WebServlet   Annotation and inheritance   HttpServlet  , rewrite   doGet   method:

@WebServlet(urlPatterns = "/demo1")
public class DemoServlet1 extends HttpServlet {
    
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        response.getWriter().println("DemoServlet1 run ......");
    }
    
}

After writing, start Tomcat directly. At this time, IDEA will automatically compile the project and deploy it to Tomcat.

Open the browser and enter the address in the address bar   http://localhost:8080/spring_00_introduction_architecture/demo1   (the project names built by each person may be different, and remember to modify the context path). It is found that it can be printed normally   DemoServlet1 run ......   It proves that the project is built and configured successfully.

1.4 write Service and Dao

At the beginning, the dependency related to the database was not imported into the pom, so the Dao here is just an empty shell and there is no actual jdbc related operation.

Create the following classes and interfaces in the project directory. These are commonplace and simple:

The corresponding components and dependencies in the three-tier architecture should be as follows: (Dao's connection to the database is not implemented)

1.4.1 Dao and DaoImpl

Simply define a   DemoDao   Interface and declare a   findAll   Method simulates querying a set of data from a database:

public interface DemoDao {
    List<String> findAll();
}

Write its corresponding implementation class   DemoDaoImpl  , Since the relevant drivers of the database are not introduced, the interaction between Dao and the database is simulated only with the written temporary data:

public class DemoDaoImpl implements DemoDao {
    
    @Override
    public List<String> findAll() {
        // This should be the operation of accessing the database, instead of temporary data
        return Arrays.asList("aaa", "bbb", "ccc");
    }
}

So far, the interface and implementation class definition of Dao layer are completed.

1.4.2 Service and ServiceImpl

Write a   DemoService   Interface and declare   findAll   method:

public interface DemoService {
    List<String> findAll();
}

Write its corresponding implementation class   DemoServiceImpl  , And rely on it internally   DemoDao   Interface:

public class DemoServiceImpl implements DemoService {
    
    private DemoDao demoDao = new DemoDaoImpl();
    
    @Override
    public List<String> findAll() {
        return demoDao.findAll();
    }
}

So far, the interface and implementation class of the Service layer are defined.

1.5 modify DemoServlet

To simulate the overall three-tier architecture, it is necessary to   DemoServlet1   To rely on   DemoService  :

@WebServlet(urlPatterns = "/demo1")
public class DemoServlet1 extends HttpServlet {
    
    DemoService demoService = new DemoServiceImpl();
    
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().println(demoService.findAll().toString());
    }
}

1.6 re run the application and test the availability

Redeploy to Tomcat and run, access  / demo1   The path is printed in the browser   ['aaa', 'bbb', 'ccc']  , The instructions are written correctly and run normally.

The above parts are the most familiar things in Java Web foundation. Well, let's stop here and replace it with a scenario.

2. [question] demand change

Now you have basically completed the development on hand. MySQL for the database is very comfortable. Near the delivery project, the customer called:

Oh, I'll go here and see who can't afford it? I'm a big boss. Change my Oracle database!

Ten thousand grass mud horses roared away in your heart when you hung up the phone:

No move. The customer is God. We also want the right meal. We have to change what the customer wants! Then change it:

2.1 modifying the database

We all know that MySQL is different from Oracle in some specific SQL (such as paging), so I can't just change the relevant configuration of the database connection pool, and each DaoImpl has to be changed! So you began to modify all DaoImpl in the project:

public class DemoDaoImpl implements DemoDao {
    
    @Override
    public List<String> findAll() {
        // Simulate modifying SQL
        return Arrays.asList("oracle", "oracle", "oracle");
    }
}

2.2 requirements change again

It's not easy for you to stay up late for two nights. Your hair fell off one after another. Finally, you have to deploy the project for the customer. The customer smiled and said to you:

Well, I've been speculating in stocks recently... Er, no, the financial expenditure is a little serious. It's not a little shy. Just switch back to MySQL!

You must be:

You've had enough of changing the past. After all, the dog's life matters (the simplest way to kill the program Ape: change it three times). At that time, you have to think, how to solve this problem?

2.3 [scheme] import static factory

After thinking for a long time, you finally think of a good way: if I write all these Dao in advance, and then use a static factory to create a specific type of implementation class, in case of demand change, can I change the code only once!

Therefore, according to this idea, there are the following modifications:

2.3.1 construction of static plant

Declare a static factory and give it a unique name: BeanFactory   (don't ask me why I'm so chic, it's a foreshadow)

public class BeanFactory {
    public static DemoDao getDemoDao() {
        // return new DemoDaoImpl();
        return new DemoOracleDao();
    }
}

2.3.2 modification of ServiceImpl

Dao referenced in ServiceImpl is no longer manual new, but by   BeanFactory   Returned by the static method of:

public class DemoServiceImpl implements DemoService {
    
    DemoDao demoDao = BeanFactory.getDemoDao();
    
    @Override
    public List<String> findAll() {
        return demoDao.findAll();
    }
}

In this way, even if there are more ServiceImpl and Dao and the requirements change, I only need to change the static method return value in BeanFactory!

The problem was solved, everyone was happy, the customer was also very satisfied, and the project delivery was completed.

3. [problem] the source code is missing

After the project has been online for a period of time, customers have put forward optimization and expansion requirements for some functions in the system. At that time, you can maintain it. After all, you are most familiar with the project. However, you have been responsible for other projects for a long time, and your colleagues are responsible for the maintenance work.

When you reopen the project, you want to pull it up to see the specific location of the requirements to be extended. Unexpectedly, you find that the project can't even pass the compilation! (for the phenomenon that the demo cannot be compiled, delete it.)   DemoDaoImpl.java  )

At this time, you must have a black question mark on your face! Why did it work well before, but it doesn't work now? Read the wrong position again, BeanFactory  ! Ah, something's wrong. The static factory I encapsulated was lazy. How could there be compilation errors? I opened the code and saw that one was missing   DemoDaoImpl   The code can't be compiled at all!

Here's the scene. Let's pause a little and experience the problems.

3.1 dependencies between [concept] classes - tight coupling

public class BeanFactory {
    public static DemoDao getDemoDao() {
        return new DemoDaoImpl(); // The compilation failed because DemoDaoImpl.java does not exist
    }
}

In the current code, because this is really missing in the source code   DemoDaoImpl   Class, resulting in compilation failure. This phenomenon can be described as   “   BeanFactory   Strongly dependent on   DemoDaoImpl  ” , That is, we may have heard or often say "tight coupling".

3.2 solution: tight coupling

Back to the scene just now, you're so confused. Without this. java file, I can't compile, so I don't have to work? No, we can't delay the whole because of this problem! So you use your brain to think about whether there is a way to solve the problem that compilation can't be compiled in the existing knowledge?

Reflection! Reflection can declare the fully qualified name of a class to obtain its bytecode description, which can also construct objects!

therefore   BeanFactory   It can be transformed into:

public class BeanFactory {
    
    public static DemoDao getDemoDao() {
        try {
            return (DemoDao) Class.forName("com.linkedbear.architecture.c_reflect.dao.impl.DemoDaoImpl").newInstance();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("DemoDao instantiation error, cause: " + e.getMessage());
        }
    }
}

If you write like this, will the problem of compilation be solved   DemoService   There will still be problems during the initialization of, but at least you can pull up the project!

So the problem was solved temporarily and put aside...

3.3 [concept comparison] weak dependence

After using reflection, the error phenomenon no longer appears in the compiler, but after the project starts, due to   BeanFactory   To construct   DemoDaoImpl   The class does not exist at the time, so it is thrown   ClassNotFoundException   Abnormal. This   BeanFactory   yes   DemoDaoImpl   The degree of dependence is reduced, which can be regarded as "weak dependence".

4. [problem] hard coding

You can avoid the first day of junior high school, but you can't avoid the fifteenth day. This problem has to be solved finally. You have to work hard to finally solve it   DemoDaoImpl.java   After finding it, the runtime will finally report no error. However, there will still be a problem when switching MySQL and Oracle libraries: because the fully qualified name of the class is written dead in the   BeanFactory   In the source code of, the project has to be recompiled after switching the database every time before it can run normally, which seems very unnecessary. There should be a better processing scheme.

4.1 [improvement] import externalization configuration file

Witty, you can immediately think of: Hey, I can use IO to realize file storage configuration! Every time   BeanFactory   When it is initialized, let it read the configuration file, so that there will be no hard coding!

Therefore, the following modifications can be made:

4.1.1 add the factory.properties file

stay   src/main/resource   New under directory   factory.properties   And declare the following in it:

demoService=com.linkedbear.architecture.d_properties.service.impl.DemoServiceImpl
demoDao=com.linkedbear.architecture.d_properties.dao.impl.DemoDaoImpl

In order to retrieve the fully qualified names of these classes, I give each class name a "small name" (alias), so that I can find the corresponding fully qualified class name according to the small name.

4.1.2 transformation of BeanFactory

Since the configuration file is of type properties, there happens to be an API called   Properties  , It can be parsed  . properties   File.

So you can   BeanFactory   Add a static variable to:

public class BeanFactory {
    
    private static Properties properties;

The following is to be initialized when the project is just started   Properties  , This can be implemented using static code blocks:

    private static Properties properties;
    
    // Initialize properties using static code blocks and load the factory.properties file
    static {
        properties = new Properties();
        try {
            // You must use the classloader to read the configuration file under the resource folder
            properties.load(BeanFactory.class.getClassLoader().getResourceAsStream("factory.properties"));
        } catch (IOException e) {
            // The static initialization of BeanFactory class has failed, and there is no need to continue the subsequent implementation
            throw new ExceptionInInitializerError("BeanFactory initialize error, cause: " + e.getMessage());
        }
    }

After the configuration file is read, the following   getDao   The method can also be further changed:

    public static DemoDao getDemoDao() {
        try {
            Class<?> beanClazz = Class.forName(properties.getProperty("demoDao"));
            return beanClazz.newInstance();
        } catch (ClassNotFoundException e) {
            throw new RuntimeException("BeanFactory have not [" + beanName + "] bean!", e);
        } catch (IllegalAccessException | InstantiationException e) {
            throw new RuntimeException("[" + beanName + "] instantiation error!", e);
        }
    }

It's strange to write here... It's abstracted to this point. Is it necessary to write "demoDao" in it? It's certainly not necessary. Just make it a general purpose. What alias do you pass, BeanFactory   Find the corresponding fully qualified class name from the configuration file, and the reflection construction object returns:

    public static Object getBean(String beanName) {
        try {
            // Read the fully qualified name of the class corresponding to the specified name from the properties file and reflect the instantiation
            Class<?> beanClazz = Class.forName(properties.getProperty(beanName));
            return beanClazz.newInstance();
        } catch (ClassNotFoundException e) {
            throw new RuntimeException("BeanFactory have not [" + beanName + "] bean!", e);
        } catch (IllegalAccessException | InstantiationException e) {
            throw new RuntimeException("[" + beanName + "] instantiation error!", e);
        }
    }

4.1.3 modification of ServiceImpl

DemoServiceImpl   There is no need to adjust in   getDao   Method (because it has been deleted...), but to use it instead   getBean   Method and specify the object of the class with the specified name to be obtained:

public class DemoServiceImpl implements DemoService {
    
    DemoDao demoDao = (DemoDao) BeanFactory.getBean("demoDao");

Here, you suddenly find a phenomenon: now you can make all the components you want to extract into external configuration!

4.2 [thought] externalized configuration

Such configurations and properties that may change are usually not hard coded in the source code, but extracted in the form of some configuration files (properties, xml, json, yml, etc.) to cooperate with the program to load and parse the configuration files, so as to achieve the purpose of dynamic configuration and reduce configuration coupling.

5. [question] multiple construction

If you change here, you may feel that there is something wrong and there is still room for improvement. In this way, we are   ServiceImpl   In the construction method of   DemoDaoImpl  :

public class DemoServiceImpl implements DemoService {
    
    DemoDao demoDao = (DemoDao) BeanFactory.getBean("demoDao");
    
    public DemoServiceImpl() {
        for (int i = 0; i < 10; i++) {
            System.out.println(BeanFactory.getBean("demoDao"));
        }
    }

Let's just look at the printed ones   DemoDao   Memory address:

com.linkedbear.architecture.d_properties.dao.impl.DemoDaoImpl@44548059
com.linkedbear.architecture.d_properties.dao.impl.DemoDaoImpl@5cab632f
com.linkedbear.architecture.d_properties.dao.impl.DemoDaoImpl@24943e59
com.linkedbear.architecture.d_properties.dao.impl.DemoDaoImpl@3f66e016
com.linkedbear.architecture.d_properties.dao.impl.DemoDaoImpl@5f50e9eb
com.linkedbear.architecture.d_properties.dao.impl.DemoDaoImpl@58e55b35
com.linkedbear.architecture.d_properties.dao.impl.DemoDaoImpl@5d06d086
com.linkedbear.architecture.d_properties.dao.impl.DemoDaoImpl@55e8ed60
com.linkedbear.architecture.d_properties.dao.impl.DemoDaoImpl@daf5987
com.linkedbear.architecture.d_properties.dao.impl.DemoDaoImpl@7f6187f4

It can be found that the memory addresses printed each time are different, which proves that 10 different memory addresses have been created   DemoDaoImpl  ! But is it really necessary...

5.1 [improvement] introduction of cache

If there is no need to create multiple objects for these components, if there is a mechanism to ensure that there is only one object during the operation of the whole project, the resource consumption can be greatly reduced. So you can   BeanFactory   Add a cache to:

public class BeanFactory {
    // Cache to save the created objects
    private static Map<String, Object> beanMap = new HashMap<>();
    
    // ......

Later in   getBean   In the method, in order to control thread concurrency, double check locks need to be introduced to ensure that there is only one object:

public static Object getBean(String beanName) {
    // Double check lock ensures that there is no object corresponding to beanName in beanMap
    if (!beanMap.containsKey(beanName)) {
        synchronized (BeanFactory.class) {
            if (!beanMap.containsKey(beanName)) {
                // After the double check lock, it is proved that there is no, and reflection creation can be performed
                try {
                    Class<?> beanClazz = Class.forName(properties.getProperty(beanName));
                    Object bean = beanClazz.newInstance();
                    // After the reflection is created, it is put into the cache and returned
                    beanMap.put(beanName, bean);
                } catch (ClassNotFoundException e) {
                    throw new RuntimeException("BeanFactory have not [" + beanName + "] bean!", e);
                } catch (IllegalAccessException | InstantiationException e) {
                    throw new RuntimeException("[" + beanName + "] instantiation error!", e);
                }
            }
        }
    }
    return beanMap.get(beanName);
}

After improvement, retest and observe the results of this printing:

com.linkedbear.architecture.e_cachedfactory.dao.impl.DemoDaoImpl@4a667700
com.linkedbear.architecture.e_cachedfactory.dao.impl.DemoDaoImpl@4a667700
com.linkedbear.architecture.e_cachedfactory.dao.impl.DemoDaoImpl@4a667700
......

Sure enough, there will only be one object, and the ultimate goal will be achieved.

Here, even if the whole scene is over, let's summarize some key points.

  • Static factories can separate multiple dependencies
  • Externalizing configuration file + reflection can solve the hard coding problem of configuration
  • Cache can control the number of object instances

Next, it's time to lead to the theme of this chapter.

6. Introduction of IOC ideas [key points]

Compare the above two coding methods:

private DemoDao dao = new DemoDaoImpl();

private DemoDao dao = (DemoDao) BeanFactory.getBean("demoDao");

The above is strong dependency / tight coupling, which must be guaranteed at compile time   DemoDaoImpl   Existence; The following is weak dependency / loose coupling, which is not known until the runtime reflection is created   DemoDaoImpl   Whether it exists.

By comparison, the above wording is an active declaration   DemoDao   As long as the implementation class of is compiled, it will run correctly; The following method does not specify the implementation class, but by   BeanFactory   Go find a name for us   demoDao   Object, if   factory.properties   If there is an error in the fully qualified class name declared in, a strong conversion failure exception will appear   ClassCastException  .

Carefully understand the following object acquisition methods. Originally, our developers can use the above methods to actively declare the implementation class, but if we choose the following methods, we will no longer declare ourselves, but give the method of acquiring objects to us   BeanFactory  . This idea of giving control to others can be called inverse of control (IOC). and   BeanFactory   According to the specified   beanName   The process of obtaining and creating objects can be called dependency lookup (DL).

Keywords: Spring ioc

Added by Monkey-Moejo on Tue, 23 Nov 2021 22:42:59 +0200