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).