Vernacular chat framework design (Introduction) | Chapter 4: simple implementation of IOC container

Posted by harty83 on Tue, 08 Feb 2022 01:55:02 +0100

1. Simple implementation of IOC container

[thinking]

How can the framework help us to reduce the coupling of object creation and code injection?
Next, let's take the following questions to the next tutorial:

  1. When does the container help us inject objects?
  2. How does the container scan the corresponding attributes and inject them?
  3. Why do I need to use IOC containers?
  4. Who will help us load the IOC container?
  5. What is an IOC container
  6. How to implement IOC container?
  7. How many instances does the IOC container need?

2. Brief introduction to IOC container

Java programmers know that each business logic in a java program needs at least two or more objects to cooperate. Usually, when each object uses its cooperation object, it should use syntax such as new object () to complete the application of cooperation object. You will find that the coupling between objects is high. The idea of IOC is to use containers to create and coordinate these interdependent objects. Objects only need the relational business logic itself. In this regard, the responsibility for how the object gets its collaboration object is reversed (IOC, DI)

Review the doFilter method in our previous DispatchFilter

ublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

//Filter execution filter

System.out.println("filter implement");

//Find the corresponding mapping class according to the request

ActionMapper actionMapper = new ActionMapper();

ActionMapping mapping =actionMapper.findMapping(request);

if(mapping!=null){

WebExecutor executor = new WebExecutor();

//Execute user request

webExecutor.execute(request,response,mapping);

}else {

//For the non framework processing part, continue to implement other links

chain.doFilter(request, response);

}

}

As can be seen from the above code, every time the dispatchfiler accepts a request, it will create an ActionMapper and WebExecutor object, and directly use the keyword class new to create an object. What's the disadvantage? If there are different ActionMapper implementation classes for different requests, you need to get a new one for each ActionMapping implementation, which will lead to the direct coupling of the current module with the new object, which is not conducive to future expansion, but also violates the interface oriented and abstract oriented programming recommended by Java.

Benefits of IOC?

Let's start with IOC. This concept is actually the opposite of our usual new object. When we usually use an object, we usually directly use the keyword class new object. What's the harm? In fact, it is obvious that using new means that the current module has been unknowingly coupled with the object of new, and we usually call the underlying implementation module from a higher-level Abstract module, which leads to that the module depends on the specific implementation, which is in conflict with the interface oriented and abstract oriented programming advocated in Java, Moreover, this also brings about the problem of module architecture of the system. For a very simple example, when we operate the database, we always call the DAO layer from the business layer. Of course, our DAO generally adopts interface development, which meets the loose coupling to a certain extent, so that the business logic layer does not depend on the specific database DAO layer. We can use the DAO in the specific DAO layer to get the database at one time unless we use the DAO in the specific DAO layer to implement the DAO abstraction, but we can also use the DAO in the database at one time.
What can we achieve with IOC? IOC is the implementation of DAO interface. It is no longer the business logic layer calling the factory class to obtain, but automatically setting the implementation class of DAO for our business layer through a container (such as spring). In this way, the whole process is reversed. In the past, our business layer to ok the initiative to obtain DAO, but now DAO is actively set in the business logic layer, which is the origin of reverse control. Through IOC, we can seamlessly realize the database exchange and migration without modifying any code. Of course, the premise is to write a DAO to realize a specific database. If we apply DAO to more cases, IOC will bring us greater convenience. For example, for multiple implementations of an interface, we only need to configure it, instead of having to write factories to obtain it. This is the loose coupling of modules and the convenience of application brought by IOC.
To put it bluntly, it is actually from our usual new to using reflection to obtain class instances.

Now I believe you have a general understanding of IOC. Next, in the next chapter, we will think about how to design an IOC container: how to use an IOC container? When? How to help us create and inject objects?

According to the past experience of using the framework, our object injection is implemented by the framework. We don't need to care which implementation class the referenced object comes from. We just need to declare its interface or abstract class and call the interface method. The framework is responsible for finding the corresponding implementation class injection for us.

private ActionMapper actionMapper;

private boolean flag;

We declare such a line of code in the current module. How can the framework help us find out which attributes we really want to inject? Obviously, the basic type of flag is not what we want the framework to help us inject. We just want the implementation class of ActionMapper. If we don't have an identification for the framework to recognize, the framework will instantiate and inject all the declared attributes of the current module for us, which is not what we want. At this time, we can add an annotation to the attributes that need to be injected into the framework to tell the framework what needs to be managed by the framework.

3. Create Inject annotation

We use the Inject annotation to tell the framework which attributes can be injected for us

[ideas]
Later, the framework will scan all the attributes marked by @ Inject, and then help us create and Inject objects

/**

* Created by liangyh on 2016/11/24.

* Email:10856214@163.com

*/

@Target(ElementType.FIELD)

@Retention(RetentionPolicy.RUNTIME)

public @interface Inject {

}

4. Create ioccontractor

The main responsibility of the IOC container is to manage the object life cycle, scan and inject objects. In order to enable the IOC container to find the attributes that need to be injected, we implemented the inject annotation in the previous chapter. If you want to inject the attributes that need the help of the IOC container, you only need to add the inject annotation on them, and the IOC container can help us inject them

@Inject
private ActionMapper actionMapper;

Next, let's start to implement an IOC container

[ideas]

  1. Provide a method to get the container instance
  2. Provides an injection method for injecting objects
  3. Provide a loading method to load class and its instance into the container

The life cycle of global objects is handed over to the IOC container. In order to ensure that the IOC container obtained by all modules is the same, the IOC container should only have one instance. Here, we can use the lazy mode and wait for someone to call before creating the container instance, and we can't let other modules instantiate this class, Therefore, we also need to privatize the constructor.

/**

* Ioc container

* Created by liangyh on 2016/11/24.

* Email:10856214@163.com

*/

public class IocContrainer {

private static IocContrainer INSTANCE;

private IocContrainer() {

}

/**

* Get Ioc container instance (lazy mode)

* @return

*/

public static synchronized IocContrainer getInstance() {

if (null == INSTANCE) {

INSTANCE = new IocContrainer();

}

return INSTANCE;

}


}

IOC container needs to inject all attributes with inject annotation, because it should provide an injct method for injecting objects. First, it will get all the declared properties of the current incoming object, and then judge whether there is an inject annotation on it. If it exists, it will load the current property with reflection and inject it into the current incoming object. Because some attributes may be decorated with private, we also need to crack their access permissions. Another thing to note is that the attributes of the current object may also have attributes that need to be injected, because here we still need to use recursive algorithm to inject layer by layer until all attributes and their own attributes are injected together.

We need to declare a context to store the bytecode of the object and its instance mapping, because loading the object is actually storing the object in memory

private static Map<Class,Object> context = new HashMap<Class,Object>();

Next, we implement the inject and loadClass methods to inject objects and load classes

public void inject(Object obj) {

//Get all declared properties

Field[] fields = obj.getClass().getDeclaredFields();

for(Field field : fields){

if(field.isAnnotationPresent(Inject.class)){

//Load instance

Object fieldObj = loadClass(field.getType());

//Access rights of brute force cracking attributes

field.setAccessible(true);

//Inject the attributes of the target object

try {

field.set(obj,fieldObj);

} catch (IllegalAccessException e) {

e.printStackTrace();

}

}

}

}

/**

* Class loading

* @param clazz

* @return

*/

private Object loadClass(Class clazz){

//Create instance

Object obj = context.get(clazz);

if(obj!=null){

return obj;

}

try {

//Create instance

obj = clazz.newInstance();

//Store the instance and its bytecode mapping in memory

context.put(clazz,obj);

//Continue injecting the current object and its properties

inject(obj);

} catch (InstantiationException e) {

e.printStackTrace();

} catch (IllegalAccessException e) {

e.printStackTrace();

}

return obj;

}

In the loadClass method above, it is necessary to first judge whether there is an instance of the current bytecode in the context in order to avoid repeated creation. The reason why the inject method is called again in the loadClass method is to inject the attributes of the currently passed in attribute itself. In short, the attribute itself is also an object, and the object itself also has the attributes identified by the inject annotation. Because it also needs to be injected, the idea of recursion is used here.

The complete code of IOC container is as follows:

/**

* Ioc container

* Created by liangyh on 2016/11/24.

* Email:10856214@163.com

*/

public class IocContrainer {

private static IocContrainer INSTANCE;

private static Map<Class,Object> context = new HashMap<Class,Object>();

private IocContrainer() {

}

/**

* Get Ioc container instance (lazy mode)

* @return

*/

public static synchronized IocContrainer getInstance() {

if (null == INSTANCE) {

INSTANCE = new IocContrainer();

}

return INSTANCE;

}

public void inject(Object obj) {

//Get all declared properties

Field[] fields = obj.getClass().getDeclaredFields();

for(Field field : fields){

if(field.isAnnotationPresent(Inject.class)){

//Load instance

Object fieldObj = loadClass(field.getType());

//Access rights of brute force cracking attributes

field.setAccessible(true);

//Inject the attributes of the target object

try {

field.set(obj,fieldObj);

} catch (IllegalAccessException e) {

e.printStackTrace();

}

}

}

}

/**

* Class loading

* @param clazz

* @return

*/

private Object loadClass(Class clazz){

//Create instance

Object obj = context.get(clazz);

if(obj!=null){

return obj;

}

try {

//Create instance

obj = clazz.newInstance();

//Store the instance and its bytecode mapping in memory

context.put(clazz,obj);

//Continue injecting the current object and its properties

inject(obj);

} catch (InstantiationException e) {

e.printStackTrace();

} catch (IllegalAccessException e) {

e.printStackTrace();

}

return obj;

}

}

5. Initialize IOC container

The IOC container is initialized when the project is started. Therefore, in this tutorial, the IOC container initialization will be placed in the init method of DispatchFilter. When the project is started, tomcat will call the init method in DispatchFilter (we have configured it in web.xml earlier). Therefore, at this time, the IOC container will be created and the attributes used by the current filter will be injected, The init method of DispatchFilter is as follows:

public void init(FilterConfig filterConfig) throws ServletException {

//Create IOC container

//In order to ensure that only one ioc container is used globally, the singleton mode is used to create objects

IocContrainer ioc = IocContrainer.getInstance();

//Injection attribute

ioc.inject(this);

}

6. Transform the original object mode

The DispatchFilter and ActionMapper classes we used in the previous chapters use the New method to reference their dependent module instances. In this chapter, we need to use our annotation method to help us implement the annotation through the IOC container.

1.DispatchFilter relies on ActionMapper and WebExecutor. Its reference method before transformation is as follows:

//Find the corresponding mapping class according to the request

ActionMapper actionMapper = new ActionMapper();
WebExecutor executor = new WebExecutor();

Now let's help us inject the instances of these two classes through the framework. After transformation, the code is as follows:

@Inject

private ActionMapper actionMapper;

@Inject

private WebExecutor webExecutor;

2.ActionMapper relies on the WebConfig class. The reference method before transformation is as follows:

//Get all rule matching mappings from the configuration file

WebConfig webConfig = new WebConfig();

Similarly, we use the framework to help us inject instances of this class. After transformation, the code is as follows:

@Inject

private WebConfig webConfig;

6.1 complete code of dispatchfilter after transformation

**

* Core request distribution filter

* Created by liangyh on 2016/9/21.

* Email:10856214@163.com

*/

public class DispatchFilter implements Filter {

@Inject

private ActionMapper actionMapper;

@Inject

private WebExecutor webExecutor;

public void init(FilterConfig filterConfig) throws ServletException {

//Create IOC container

//In order to ensure that only one ioc container is used globally, the singleton mode is used to create objects

IocContrainer ioc = IocContrainer.getInstance();

//Injection attribute

ioc.inject(this);

}

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

//Filter execution filter

System.out.println("filter implement");

//Find the corresponding mapping class according to the request

//ActionMapper actionMapper = new ActionMapper();

ActionMapping mapping =actionMapper.findMapping(request);

if(mapping!=null){

//WebExecutor executor = new WebExecutor();

//Execute user request

webExecutor.execute(request,response,mapping);

}else {

//For the non framework processing part, continue to implement other links

chain.doFilter(request, response);

}

}

public void destroy() {

}

}

6.2 complete code of actionmapper after transformation

/**

* Created by liangyh on 2016/11/19.

* Email:10856214@163.com

*/

public class ActionMapper {

@Inject

private WebConfig webConfig;

public ActionMapping findMapping(ServletRequest request) {

ActionMapping actionMapping = new ActionMapping();

//Convert the request entity type to HttpServletRequest

HttpServletRequest req = (HttpServletRequest) request;

//Get Uri from request entity

String uri = req.getRequestURI();

//Intercept the method name requested by the user according to the Uri address

String methodName = uri.substring(req.getContextPath().length() + 1);

//Get all rule matching mappings from the configuration file

// WebConfig webConfig = new WebConfig();

Map<String, Mapping> mappings = webConfig.getMappings();

//Find a matching mapping class based on the request method name

Mapping mapping = mappings.get(methodName);

//Finding mapping based on wildcards

if (mapping == null) {

//Traverse all key s in all mappings

Iterator<String> iter = mappings.keySet().iterator();

while (iter.hasNext()) {

String matchKey = iter.next();

//Match the current request method. If so, return the corresponding mapping

actionMapping = matchMapping(methodName, matchKey, mappings);

if (actionMapping != null) {

return actionMapping;

}

}

} else {

return convertActionMapping(methodName, mapping);

}

return actionMapping;

}

/**

* Convert ActionMapping

*

* @param methodName

* @param mapping

*/

private ActionMapping convertActionMapping(String methodName, Mapping mapping) {

//Convert mapping into corresponding actionMapping and return

ActionMapping actionMapping = new ActionMapping();

actionMapping.setClassName(mapping.getClassName());

actionMapping.setMethodName(methodName);

actionMapping.setResult(mapping.getResult());

return actionMapping;

}

/**

* Matching mapping

* <pre>

* methodName:helloPersonAction

* matchKey: *PersonAction

* mapping:new Mapping("com.eshare.action.PersonAction","{1}","SUCCESS.jsp")

* </pre>

*

* @param methodName Method name

* @param matchKey Match keyword

* @param mappings Mapping configuration

* @return

*/

private ActionMapping matchMapping(String methodName, String matchKey, Map<String, Mapping> mappings) {

String regexKey = matchKey;

//Check whether the current keyword contains wildcards*

if (matchKey.contains("*")) {

//Replace wildcards with regular expressions

regexKey = matchKey.replaceAll("\\*", "(\\.\\*)");

}

//Compile keywords into regular expression instances

Pattern pattern = Pattern.compile(regexKey);

Matcher matcher = pattern.matcher(methodName);

Mapping mapping;

//Match the method name through regular expression

if (matcher.find()) {

//In matching, you can find the corresponding mapping by keyword

mapping = mappings.get(matchKey);

//Method parameter name of framework user configuration

String configMethod = mapping.getMethodName();

//Request actual processing method

String method;

//Whether the configured method has parentheses. If it exists, it is a placeholder

if (configMethod.contains("{")) {

//Remove all brackets

configMethod = configMethod.replaceAll("\\{", "").replaceAll("\\}", "");

//Convert numbers into indexes

int index = Integer.valueOf(configMethod);

//Match and replace the request method with the placeholder through the group method of the matcher

method = matcher.group(index);

} else {

//If there is no placeholder for the configuration method parameter, it is directly the last execution method name

method = configMethod;

}

//Store the resolved processing method name in mapping and replace the original placeholder

return convertActionMapping(method, mapping);

}

return null;

}

}

7. Final project directory structure

8. Test the modified project

8.1 start tomcat

8.2 enter URL in browser

My context path is eshare

http://localhost:8080/eshare/sayHelloPersonAction

8.3 output results

Topics: Java