- J3
- Spring (parent container # BUG)
1, the cause of the matter
One day, J3 received a small request to intercept data clues coming into the database. The data entering the database needs to be checked through three channels A, B and C, and the check rules of these three channels are different. As long as one of these channels does not meet the requirements of warehousing, it cannot be stored in the database.
See this, J3 heart is incomparable secretly happy, because it is too simple. Say J3 and write the following code for the requirement:
@RestController
@RequestMapping("/insert")
public class InsertController {
@Autowired
private ApplicationContext applicationContext;
@PostMapping("/")
public void doInsert(@RequestBody Entity entity){
// Check the core logic
Map<String, CheckChain> beansOfType = applicationContext.getBeansOfType(CheckChain.class);
beansOfType.entrySet().forEach(entry -> {
if(! entry.getValue().check(entity)){return; }});// Perform the insert to the business class
// ...}}// Check the interface
public interface CheckChain {
Boolean check(Entity e1);
}
// A channel check logic
@Component
public class ACheckChain implements CheckChain{
@Override
public Boolean check(Entity e1) {
// Check logic
returnBoolean.TRUE; }}// B channel check logic
@Component
public class BCheckChain implements CheckChain{
@Override
public Boolean check(Entity e1) {
// Check logic
returnBoolean.TRUE; }}// C channel check logic
@Component
public class CCheckChain implements CheckChain{
@Override
public Boolean check(Entity e1) {
// Check logic
returnBoolean.TRUE; }}Copy the code
The above code is run in the Spring environment. I tested each verification rule separately. After all the rules were tested, I directly used them online.
The catch is that each validation rule is tested individually, without the logic going through the entire process from the Controller to the final data drop. At this point, J3 didn’t know it, but he had written a big Bug that was discovered four months later.
Just a few days ago, the operation side of the company reported that there was a problem with data interception, which was not stopped when it should have been. Then I noticed this problem and immediately went to check the relevant implementation code. After a series of checks, I finally found the clue, see the following code:
Can the Controller get the bean scanned by @Component from the ApplicationContext? In Spring, the answer is no.
So, this is the code that was written by two, and I immediately looked at the code submission record and stood there for a few seconds, but IT was me who wrote the code, and it was four months ago.
After locating the problem, I have a general understanding of the root cause of the problem, so I will fix the online problem first (afraid of delaying the boss’s earnings), and then I will find out the details of the problem.
2. Spring parent container
From the above description, I’m sure many of you have already guessed what the key problem is: the Spring parent-child container.
Then since this, let’s dig a dig its bottom (principle)!
Before I get to the rationale, let me explain what we’re going to do.
- Analyze the parent container startup process;
- The parent container holds those beans;
- Analyze the subcontainer startup process;
- The child container holds those beans;
- Whether the IOC container injected in Controller is a child or a parent;
- Whether the IOC container injected into the Service is a child or a parent;
- How to get beans through the IOC container;
2.1. Environment construction
The parent-child container problem, I think it is best to build a Spring and Spring MVC integration shelf out, so as to locate and analyze the principle.
This is my good environment (JDK11), we do not want to build their own can be directly clone a copy of 👉 : address
Start with the basic structure of the project
Web. XML: Configuration file for web project startup, which configures information about Spring and key classes for Spring MVC startup.
Spring-service. XML: parent container configuration file.
Spring-web. XML: subcontainer configuration file.
2.2 Parent container startup analysis
Once the environment is ready, take a look at how the parent container is started.
Web projects are usually packaged and run in Tomcat, which reads the web.xml file in the WebAPP web-INF file in each project, so this is the source of our analysis.
Remember what we configured in the web. XML file when we set up Spring and Spring MVC projects. Yes, it’s a listener: Org. Springframework. Web. Context. ContextLoaderListener, It implements the ServletContextListener interface and ServletContextListener is part of the Servlet API, and when the Servlet container starts or terminates a Web application, The ServletContextEvent event is emitted, which is handled by the ServletContextListener. Two methods for handling ServletContextEvent events are defined in the ServletContextListener interface:
- ContextInitialized (ServletContextEvent sCE) : This method is called when the Servlet container starts the Web application.
- ContextDestroyed (ServletContextEvent SCE) : Called when the Servlet container terminates the Web application.
ContextInitialized (contextInitialized); contextInitialized (contextInitialized); contextInitialized (contextInitialized); contextInitialized (contextInitialized);
org.springframework.web.context.ContextLoaderListener # contextInitialized
@Override
public void contextInitialized(ServletContextEvent event) {
// The parent container starts the entry
initWebApplicationContext(event.getServletContext());
}
Copy the code
From the name of the method, we know that is an entry to initialize the Web context application, which does two main things:
- Create the parent container
- Initialize the created parent container
Enter the initWebApplicationContext method
org.springframework.web.context.ContextLoader # initWebApplicationContext
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
// ...
if (this.context == null) {
// create parent container
this.context = createWebApplicationContext(servletContext);
}
if (this.context instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
if(! cwac.isActive()) {if (cwac.getParent() == null) {
// Set the parent to null, meaning that the Spring parent has no parent
ApplicationContext parent = loadParentContext(servletContext);
cwac.setParent(parent);
}
// initialize the parent containerconfigureAndRefreshWebApplicationContext(cwac, servletContext); }}// ...
}
Copy the code
There is no need to create a parent container, just create a container object by reflection. Key points in the second part, the initialization of the parent container configureAndRefreshWebApplicationContext contains the contents of this method is very much, this I also again only analyze its beans related part of the life cycle, This is the life cycle of the two beans MyTestController and MyTestService in this project.
Enter the configureAndRefreshWebApplicationContext method
org.springframework.web.context.ContextLoader # configureAndRefreshWebApplicationContext
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
// ...
// Refresh the core method of the entire container
wac.refresh();
}
Copy the code
Anyone familiar with the Spring framework is familiar with the Refresh method, which is arguably the main method for the entire Spring startup process.
Enter refresh method
org.springframework.context.support.AbstractApplicationContext # refresh
public void refresh(a) throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// ...
// Create beanFactory to read the spring-service.xml configuration file
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// ...
// Create and initialize all scanned single-instance beansfinishBeanFactoryInitialization(beanFactory); }}Copy the code
Considering the length and the content involved in this case, I will only analyze these two methods.
1. ObtainFreshBeanFactory () method analysis
This method basically does two things:
- Create the Bean factory
- Read the configuration file, find out the Bean information to be created and save it to the corresponding location
These two things are represented by the refreshBeanFactory() method, so let’s go inside:
org.springframework.context.support.AbstractRefreshableApplicationContext # refreshBeanFactory
protected final void refreshBeanFactory(a) throws BeansException {
// Destroy the factory if it exists
if (hasBeanFactory()) {
destroyBeans();
closeBeanFactory();
}
try {
// create a Bean factory
DefaultListableBeanFactory beanFactory = createBeanFactory();
beanFactory.setSerializationId(getId());
customizeBeanFactory(beanFactory);
// Add BeanDefinitions to beanFactory. // Add BeanDefinitions to beanFactory
loadBeanDefinitions(beanFactory);
synchronized (this.beanFactoryMonitor) {
this.beanFactory = beanFactory; }}catch (IOException ex) {
throw new ApplicationContextException("I/O error parsing bean definition source for "+ getDisplayName(), ex); }}Copy the code
The first point of creating or nothing analysis, he just created a DefaultListableBeanFactory the beanFactory type.
The second point is the focus of this analysis. It loads the Spring configuration file, reads the scan rules defined in the configuration file, and encapsulates the Bean definitions that meet the rules into BeanDefinitions one by one and stores them in the created Bean factory.
Go ahead and see the loadBeanDefinitions(beanFactory) method!
org.springframework.web.context.support.XmlWebApplicationContext # loadBeanDefinitions
@Override
protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {
// Create a reader for XML
XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);
// ...
// Initialize the reader
initBeanDefinitionReader(beanDefinitionReader);
// Load BeanDefinitions using the XML reader according to the configuration file
loadBeanDefinitions(beanDefinitionReader);
}
Copy the code
I’m not going to talk much about the creation of the reader, but if I move on, Program will eventually by creating Xml reader eventually went down to the doLoadBeanDefinitions (inputSource, encodedResource getResource () method to the real loading the configuration file.
Look at the doLoadBeanDefinitions method.
org.springframework.beans.factory.xml.XmlBeanDefinitionReader # doLoadBeanDefinitions
protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
throws BeanDefinitionStoreException {
// ...
// Wrap the Spring configuration file as a Document object
Document doc = doLoadDocument(inputSource, resource);
// Define BeanDefinitions as a Document object
return registerBeanDefinitions(doc, resource);
// ...
}
Copy the code
The first point of parsing XML files to generate the corresponding Document object implementation is a bit complicated, SO I will briefly summarize it with my understanding. All tag elements configured in spring-service.xml are parsed in this section as an attribute of the Document object.
The BeanDefinitions method reads the attributes of the Document object to registerBeanDefinitions.
So with this approach, we’re going to focus on the second part, because the first part you can think of as turning our spring-service.xml configuration file into something that Spring can read, the configuration information hasn’t actually taken effect yet, RegisterBeanDefinitions (doc, Resource) is what is starting to take effect for our configuration.
Visit the registerBeanDefinitions(DOC, Resource) method
org.springframework.beans.factory.xml.XmlBeanDefinitionReader # registerBeanDefinitions
public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
// Create a reader that reads Document
BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
int countBefore = getRegistry().getBeanDefinitionCount();
// Start reading
documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
return getRegistry().getBeanDefinitionCount() - countBefore;
}
Copy the code
Here’s another reader that creates something, which is a no-guesswork to parse or read a Document object. Here would be to parse through BeanDefinitionDocumentReader BeanDefinition related Document object registration.
DocumentReader. RegisterBeanDefinitions will appoint a representative method, special processing parsing from the Document object nodes, according to the different node call different analytical methods.
RegisterBeanDefinitions has a number of method calls. I’ll list the steps, but I won’t post the code, because it’s not the focus of my analysis and it’s too long to list all of them, so I’ll skip them here.
- org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader # registerBeanDefinitions
- org.springframework.beans.factory.xml.BeanDefinitionParserDelegate # parseCustomElement
- org.springframework.context.annotation.ComponentScanBeanDefinitionParser # parse
- org.springframework.context.annotation.ClassPathBeanDefinitionScanner#doScan
The doScan method is the end we have followed step by step. Its main function is to encapsulate our defined Bean as BeanDefinitionHolder and return it to the Bean factory.
// basePackages we configure packages to scan cn.j3.myspring
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
Assert.notEmpty(basePackages, "At least one base package must be specified");
// Define BeanDefinitionHolder
Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
// Iterate over the path to scan
for (String basePackage : basePackages) {
// Get all classes in this path that match the @Components annotation of the XXXFilters rule
Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
// Iterate over the scanned BeanDefinition
for (BeanDefinition candidate : candidates) {
// Generate the Bean's name and return it with BeanDefinitionHolder
ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
candidate.setScope(scopeMetadata.getScopeName());
/ / generated beanName
String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
if (candidate instanceof AbstractBeanDefinition) {
postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
}
if (candidate instanceof AnnotatedBeanDefinition) {
AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
}
if (checkCandidate(beanName, candidate)) {
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
definitionHolder =
AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
// Store it in the set set
beanDefinitions.add(definitionHolder);
// Register with beanFactory
registerBeanDefinition(definitionHolder, this.registry); }}}/ / return
return beanDefinitions;
}
Copy the code
The main function of this class is to scan the package path defined by the configuration file and find out and encapsulate beans that meet the definition to form BefinitionHolder and register it in beanFactory.
At this point, our beans are registered with the BeanFactory, and the instantiation will have to wait until later. Instantiation is not a big step. The MyTestController Bean is not found in the MyTestController Bean.
<! Set the package for scanning components -->
<context:component-scan base-package="cn.j3.myspring">
<! Tell Spring not to scan Controller annotated packages -->
<context:exclude-filter type="annotation"
expression="org.springframework.stereotype.Controller"/>
<context:exclude-filter type="annotation"
expression="org.springframework.web.bind.annotation.ControllerAdvice"/>
</context:component-scan>
Copy the code
Take a look at our configuration file (spring-service.xml). It already defines the class that Spring tells it not to scan the @Controller annotation, and this is the code: IsCandidateComponent (metadataReader) in the findCandidateComponents(basePackage) method determines.
At this point, we have the Bean definition information stored in the BeanFactory. Now we just need to instantiate these beans. That the feature is that it (finishBeanFactoryInitialization (the beanFactory)) it’s time to work.
2, finishBeanFactoryInitialization analysis (the beanFactory) method
The main function of this method is to instantiate all non-lazy-loaded beans registered in the beanFactory. The core code is as follows:
org.springframework.context.support.AbstractApplicationContext # finishBeanFactoryInitialization
protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
// ...
// Instantiate all non-lazy-loaded beans
beanFactory.preInstantiateSingletons();
}
Copy the code
Since the bean information is stored in the beanFactory, it should be instantiated by calling the beanFactory as follows:
org.springframework.beans.factory.support.DefaultListableBeanFactory#preInstantiateSingletons
public void preInstantiateSingletons(a) throws BeansException {
// ...
// Get all defined beannames
List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);
// Loop instantiation
for (String beanName : beanNames) {
// ...
// A series of Bean lifecycle processes
getBean(beanName);
// ...
}
// The loop triggers post-initialization callbacks for all beans
}
Copy the code
Because we know what beans are supposed to be in the parent container, Bean instantiation is not our focus, but one thing we do know.
Remember that we injected an ApplicationContext object into MyTestService. Spring will eventually inject the IOC container when instantiating MyTestService!
This right here, you don’t even think about it, it’s a hundred percent sure, is the parent container. Since the spring-MVC child hasn’t started yet, the IOC container injected by the Bean in the parent container must be the parent container.
Drawing master to the parent container start process to do the execution flow chart, as follows:
2.3. Startup analysis of sub-containers
For child container startup, we also look at the web.xml configuration file.
In, the SpringMVC framework we only on the web. The XML configuration file is configured with org. Springframework. Web. Servlet. The DispatcherServlet, so our entry is that it.
Let’s start with the class inheritance structure:
As you can see from the above figure, the DispatcherServlet indirectly inherits the Servlet so it is a Servlet that executes the Servlet lifecycle methods when the Web container is started.
The DispatcherServlet is no exception, so our child container start analysis is the Init () method of the DispatcherServlet class.
Let’s go to the init() method and see:
org.springframework.web.servlet.HttpServletBean # init
public final void init(a) throws ServletException {
// ...
// Let subclasses do whatever initialization they like.
// Subclass implementation of HttpServletBean initializes the servletBean
initServletBean();
// ...
}
Copy the code
The init method doesn’t do anything real, but it does reserve an initServletBean method for its subclasses to extend, and that’s what we want, so let’s play around.
org.springframework.web.servlet.FrameworkServlet # initServletBean
protected final void initServletBean(a) throws ServletException {
// ...
// Start the entry of the child container
this.webApplicationContext = initWebApplicationContext();
// The method is reserved without any logic
initFrameworkServlet();
// ...
}
Copy the code
Finally saw the we want initWebApplicationContext, it is the entrance, children start point.
org.springframework.web.servlet.FrameworkServlet # initWebApplicationContext
protected WebApplicationContext initWebApplicationContext(a) {
// Get the parent container, which we analyzed above
WebApplicationContext rootContext =
WebApplicationContextUtils.getWebApplicationContext(getServletContext());
WebApplicationContext wac = null;
/ / the first initialization, enclosing webApplicationContext must be null
if (this.webApplicationContext ! =null) {
wac = this.webApplicationContext;
if (wac instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
if(! cwac.isActive()) {if (cwac.getParent() == null) {
// Set the parent container for the child container
cwac.setParent(rootContext);
}
// Same as the parent container initializationconfigureAndRefreshWebApplicationContext(cwac); }}}if (wac == null) {
// WebApplicationContext cannot be found because it has not been created
wac = findWebApplicationContext();
}
if (wac == null) {
// The first time you execute it, you will definitely go here and create the child container !!!!!!! (Emphasis) !!!!
wac = createWebApplicationContext(rootContext);
}
if (!this.refreshEventReceived) {
onRefresh(wac);
}
if (this.publishContext) {
String attrName = getServletContextAttributeName();
getServletContext().setAttribute(attrName, wac);
if (this.logger.isDebugEnabled()) {
this.logger.debug("Published WebApplicationContext of servlet '" + getServletName() +
"' as ServletContext attribute with name [" + attrName + "]"); }}return wac;
}
Copy the code
Initializing the subcontainer method has two main logics, the first is that the subcontainer was created, and the second is that the subcontainer was not created.
- Child container is created, access to the parent container it is set as the children of the parent container, and perform the initialization logic as the parent container, namely configureAndRefreshWebApplicationContext method.
- Children did not create, try to find time, such as has not been found, in the creation, go createWebApplicationContext (rootContext) method.
Obviously we this time, is the second kind of logic, enter createWebApplicationContext method.
org.springframework.web.servlet.FrameworkServlet # createWebApplicationContext()
protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
// ...
// Create the child container first
ConfigurableWebApplicationContext wac =
(ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
// Set the environment for the child container
wac.setEnvironment(getEnvironment());
// Set the parent container for the child container
wac.setParent(parent);
// Get the spring-web. XML configuration file
String configLocation = getContextConfigLocation();
if(configLocation ! =null) {
// This is the child container configuration file
wac.setConfigLocation(configLocation);
}
// Start initializing the child container
configureAndRefreshWebApplicationContext(wac);
// return the initialized child container
return wac;
}
Copy the code
You can see that this method is again the usual two big steps, create and initialize.
There is a difference between the Parent container and the child container. The child container sets the Parent property of the newly created child container.
This is of course for automatic assembly, we often write the Controller is not to assemble various Sertvice beans, and these beans, of course, are from the parent container. Although we could have done it from a child container, that is, in spring-web.xml, we generally don’t do this.
Finally, after we initialize our child container, let’s guess if the ApplicatuionContext injected into MyTestController in our project is a child or a parent. It’s a child.
Drawing master to the child container startup process to do the execution flow chart, as follows:
3, discuss applicationContext. GetBeansOfType (Class)
On it we have to strip the spring container bottom pants, father and son that we can now to the bare spring analysis to analyze why the Controller using pplicationContext. GetBeansOfType method will get less than problems.
So in MyTestController you go to the getBeansOfType method first.
org.springframework.beans.factory.support.DefaultListableBeanFactory#getBeansOfType
public <T> Map<String, T> getBeansOfType(@Nullable Class<T> type, boolean includeNonSingletons, boolean allowEagerInit)
throws BeansException {
// Get the bean name according to type
String[] beanNames = getBeanNamesForType(type, includeNonSingletons, allowEagerInit);
Map<String, T> result = new LinkedHashMap<>(beanNames.length);
// If beanNames is empty, there is no process below
for (String beanName : beanNames) {
// Get the bean according to beanName
}
return result;
}
Copy the code
As you can see, the focus of this method is on getBeanNamesForType. If the method does not get the beanName, there is no further step to get the Bean by name. So let’s look at the implementation logic of getBeanNamesForType.
org.springframework.beans.factory.support.DefaultListableBeanFactory # getBeanNamesForType
public String[] getBeanNamesForType(@NullableClass<? > type,boolean includeNonSingletons, boolean allowEagerInit) {
// I'm not sure what that means
if(! isConfigurationFrozen() || type ==null| |! allowEagerInit) {return doGetBeanNamesForType(ResolvableType.forRawClass(type), includeNonSingletons, allowEagerInit);
}
// The following steps are important, the top step will not be taken
// Get it from the cache. The first time you get it, it must be emptyMap<Class<? >, String[]> cache = (includeNonSingletons ?this.allBeanNamesByType : this.singletonBeanNamesByType);
String[] resolvedBeanNames = cache.get(type);
if(resolvedBeanNames ! =null) {
return resolvedBeanNames;
}
// Get the bean by type. The first time you get the bean, you must go here
resolvedBeanNames = doGetBeanNamesForType(ResolvableType.forRawClass(type), includeNonSingletons, true);
if (ClassUtils.isCacheSafe(type, getBeanClassLoader())) {
// Get a bean of the same type from the cache next time
cache.put(type, resolvedBeanNames);
}
// Return the result
return resolvedBeanNames;
}
Copy the code
The main thing about this method is doGetBeanNamesForType. Click on it.
org.springframework.beans.factory.support.DefaultListableBeanFactory # doGetBeanNamesForType
private String[] doGetBeanNamesForType(ResolvableType type, boolean includeNonSingletons, boolean allowEagerInit) {
List<String> result = new ArrayList<>();
// Loop over all beans in the BeanFactory, check if the type is passed in, store it in the list and return it
for (String beanName : this.beanDefinitionNames) {
// You can skip the analysis}}Copy the code
See note do not know everybody again did not suddenly realize, here want to take an examination of everybody talent cough up!
Notice the for loop, which only loops through the this.beanDefinitionNames collection, meaning that the getBeansOfType method will only look for matching beans in this container based on the Class type.
Let’s recall that when the parent container is started, each container has those beans. MyTestService in our project is defined in the parent container, so it will be stored in the parent container. And MyTestController is defined in a child container, so it’s going to be stored in a child container.
Since the ApplicationContext injected into MyTestController is a child container, you can’t find the Bean in the parent container by using getBeansOfType.
Of course, if you go to MyTestService and get MyTestController based on getBeansOfType, you can’t get MyTestController either, same thing.
If you think about it more, you suddenly see the light on the Spring parent-child container problem.
4, discuss applicationContext. GetBean (Class)
So let’s move on to the genBean method, because in MyTestController we get the bean from the parent container through the child container.
Enter the getBean code.
@Override
public <T> T getBean(Class<T> requiredType, @Nullable Object... args) throws BeansException {
// By type, in this container
NamedBeanHolder<T> namedBean = resolveNamedBean(requiredType, args);
if(namedBean ! =null) {
// There is one in this container
return namedBean.getBeanInstance();
}
// This container does not exist
BeanFactory parent = getParentBeanFactory();
if(parent ! =null) {
// Find the parent container
return(args ! =null ? parent.getBean(requiredType, args) : parent.getBean(requiredType));
}
// Not found
throw new NoSuchBeanDefinitionException(requiredType);
}
Copy the code
The code is very simple. First look for the bean of type type in the container. If you can’t find it, look for the parent of the container.
This makes sense because we use the child container in MyTestController to get the Bean from the parent container via getBean.
MyTestController = MyTestController; MyTestController = MyTestController; MyTestController = MyTestController; MyTestController = MyTestController
The answer is, no.
As for why can not find, you can carefully think about it, well, the length of this article is also a little long, the first analysis to this.
If you don’t understand my final answer, please leave a comment in the comment section or contact me directly. I am happy to discuss with you.
When the end of this story, I know that the three-day holiday will be gone, at this time my mood is like this…
Well, that's it for today, so follow me and we'll see you next time
If you have any questions, please contact me at:
QQ: 1491989462
WeChat: 13207920596
- Due to the lack of knowledge of the blogger, there will be mistakes, if you find mistakes or bias, please comment to me, I will correct it.
- If you think the article is good, your retweets, shares, likes and comments are the biggest encouragement to me.
- Thank you for reading. Welcome and thank you for your attention.