Understand the principle of spring MVC this time

Don't talk at night 2022-02-13 07:26:32 阅读数:764

understand principle spring mvc time

Preface

The previous articles , To study the Spring IOC、Bean Instantiation process 、AOP、 Transaction source code and design idea , I understand Spring The overall operation process of , But if it is web Development , Then there is also Spring MVC, This article mainly analyzes the process of request invocation SpringMVC Implementation principle of , Through this article, we should understand how it solves the request 、 Parameters 、 Return value mapping and other problems .

Text

Request entry

We all know that when the front end calls the back-end interface , Will pass Servlet Forward , and Servlet The declaration cycle of includes the following four stages :

  • Instantiation (new)
  • initialization (init)
  • perform (service call doGet/doPost)
  • The destruction (destroy)

The first two stages are Spring The start-up phase is done (init Depending on the configuration, it may not be called until the first request ), Destruction occurs when the service is shut down , This paper mainly analyzes the request execution stage . We know SpringMVC The core of DispatcherServlet, This category is right Servlet An extension of , So directly from the service Method start , But not in this category service Method , That must be in its parent class , Let's first look at its inheritance system :
 Insert picture description here
Look up one by one , stay FrameworkServlet One of the methods service Method :

 protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {

HttpMethod httpMethod = HttpMethod.resolve(request.getMethod());
if (httpMethod == HttpMethod.PATCH || httpMethod == null) {

processRequest(request, response);
}
else {

super.service(request, response);
}
}
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException
{

String method = req.getMethod();
if (method.equals(METHOD_GET)) {

long lastModified = getLastModified(req);
if (lastModified == -1) {

doGet(req, resp);
} else {

long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
if (ifModifiedSince < lastModified) {

maybeSetLastModified(resp, lastModified);
doGet(req, resp);
} else {

resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
}
}
} else if (method.equals(METHOD_HEAD)) {

long lastModified = getLastModified(req);
maybeSetLastModified(resp, lastModified);
doHead(req, resp);
} else if (method.equals(METHOD_POST)) {

doPost(req, resp);
} else if (method.equals(METHOD_PUT)) {

doPut(req, resp);
} else if (method.equals(METHOD_DELETE)) {

doDelete(req, resp);
} else if (method.equals(METHOD_OPTIONS)) {

doOptions(req,resp);
} else if (method.equals(METHOD_TRACE)) {

doTrace(req,resp);
} else {

String errMsg = lStrings.getString("http.method_not_implemented");
Object[] errArgs = new Object[1];
errArgs[0] = method;
errMsg = MessageFormat.format(errMsg, errArgs);
resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
}
}

But it mainly calls the parent class HttpServlet The method in , And this class will be transferred to subclasses according to different request methods , The final core method is DispatcherServlet Medium doDispatch Method :

 protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {

HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
// Asynchronous management 
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {

ModelAndView mv = null;
Exception dispatchException = null;
try {

// Upload files 
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// This method is very important , The key to see 
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {

noHandlerFound(processedRequest, response);
return;
}
// Get the HandlerMethod Matching HandlerAdapter object 
// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {

long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {

return;
}
}
// Front filter , If false Then return directly 
if (!mappedHandler.applyPreHandle(processedRequest, response)) {

return;
}
// Call to Controller The specific methods , Core method call , Focus on 
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {

return;
}
applyDefaultViewName(processedRequest, mv);
// Center filter 
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {

dispatchException = ex;
}
catch (Throwable err) {

// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
// View rendering and post filter execution 
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {

triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {

triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {

if (asyncManager.isConcurrentHandlingStarted()) {

// Instead of postHandle and afterCompletion
if (mappedHandler != null) {

mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {

// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {

cleanupMultipart(processedRequest);
}
}
}
}

MVC All the processing logic of is in this method , First summarize the implementation logic of this method , First, according to the request url Get the cached HandlerMethod Objects and Execution chain object ,HandlerMethod Encapsulated controller object 、 Information such as method objects and method parameters , Execution chain It contains one by one HandlerInterceptor Interceptor ; And then through HandlerMethod Get the corresponding HandlerAdapter, The function of this object is to adapt to our controller; After the preparatory work , First of all, it will execute Prefilter , If intercepted, return directly , Otherwise, call controller The method in executes our business logic and returns a ModelView object ; Then perform Center filter , And handling exceptions caught by the global exception catcher ; The last part View rendering Return and execute Rear filter Carry out resource release and other work .
That's all MVC The overall implementation process of , Let's analyze it one by one , First of all to enter getHandler Method :

 protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {

//handlerMappering example 
if (this.handlerMappings != null) {

for (HandlerMapping mapping : this.handlerMappings) {

// obtain HandlerMethod And the packing class of the filter chain 
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {

return handler;
}
}
}
return null;
}

It is entrusted to HandlerMapping Object's , This is an interface , The main implementation classes are RequestMappingHandlerMapping, Similarly, let's take a look at its inheritance system :
 Insert picture description here
This class manages the mapping between the request and the processing class , Do you wonder where it was instantiated ? Let's take a look at MVC Initialization of components .

Component initialization

Here I will explain in the form of annotation of automatic configuration ,Spring Provides a @EnableWebMvc, Through the previous study, we know that a configuration class must be imported in this annotation , Click in and you can see yes DelegatingWebMvcConfiguration, This class is responsible for MVC Initialization of components and extension implementation , We won't look at it first , Look at its parent class first WebMvcConfigurationSupport, This class should be familiar to us , To do some custom extensions, you need to inherit this class ( Like an interceptor Interceptor), The same class also works WebMvcConfigurerAdapter, This class is for the former A relatively safe An extension of , Why is it relatively safe ? Because inheriting the former will lead to automatic configuration failure , With the latter, you don't have to worry about this problem , Just add... To the class @EnableWebMvc annotation .
stay WebMvcConfigurationSupport We can see a lot in @Bean The way to mark , That is to say mvc Instantiation of components , Here's a look at requestMappingHandlerMapping, The rest can be read and understood by yourself , That is, some Bean Registration of :

 public RequestMappingHandlerMapping requestMappingHandlerMapping() {

RequestMappingHandlerMapping mapping = createRequestMappingHandlerMapping();
mapping.setOrder(0);
mapping.setInterceptors(getInterceptors());
mapping.setContentNegotiationManager(mvcContentNegotiationManager());
mapping.setCorsConfigurations(getCorsConfigurations());
...... Omit
return mapping;
}

Here's the main thing getInterceptors Method how to get the interceptor's :

 protected final Object[] getInterceptors() {

if (this.interceptors == null) {

InterceptorRegistry registry = new InterceptorRegistry();
// Hook method , You need to define 
addInterceptors(registry);
registry.addInterceptor(new ConversionServiceExposingInterceptor(mvcConversionService()));
registry.addInterceptor(new ResourceUrlProviderExposingInterceptor(mvcResourceUrlProvider()));
this.interceptors = registry.getInterceptors();
}
return this.interceptors.toArray();
}

The first time I come in, I will call addInterceptors Add interceptor , This is a template method , In a subclass DelegatingWebMvcConfiguration To realize :

 private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();
protected void addInterceptors(InterceptorRegistry registry) {

this.configurers.addInterceptors(registry);
}
public void addInterceptors(InterceptorRegistry registry) {

for (WebMvcConfigurer delegate : this.delegates) {

delegate.addInterceptors(registry);
}
}

You can see that the final call is WebMvcConfigurer Of addInterceptors Method , That is to say, we are right about WebMvcConfigurerAdapter Custom extension for . Seeing this, we should understand MVC How are components added to IOC In container , however DispatcherServlet How did you get them ? Go back to the previous code , stay DispatcherServlet There is one in this class onRefresh Method , This method calls initStrategies The method is done MVC Registration of nine components :

 protected void onRefresh(ApplicationContext context) {

initStrategies(context);
}
protected void initStrategies(ApplicationContext context) {

initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context);
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}
private void initHandlerMappings(ApplicationContext context) {

this.handlerMappings = null;
if (this.detectAllHandlerMappings) {

// Find all HandlerMappings in the ApplicationContext, including ancestor contexts.
Map<String, HandlerMapping> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
if (!matchingBeans.isEmpty()) {

this.handlerMappings = new ArrayList<>(matchingBeans.values());
// We keep HandlerMappings in sorted order.
AnnotationAwareOrderComparator.sort(this.handlerMappings);
}
}
else {

try {

HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
this.handlerMappings = Collections.singletonList(hm);
}
catch (NoSuchBeanDefinitionException ex) {

// Ignore, we'll add a default HandlerMapping later.
}
}
if (this.handlerMappings == null) {

this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
}
}

With initHandlerMappings For example , The implementation logic of other components is basically the same . First of all, from the IOC From the container handlerMappings All implementation classes of (WebMvcConfigurationSupport The object injected in is obtained here ), If there is no , From DispatcherServlet.properties In profile ( This configuration is spring-webmvc Under the project org/springframework/web/servlet/DispatcherServlet.properties) Get the default configuration :

org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver
org.springframework.web.servlet.ThemeResolver=org.springframework.web.servlet.theme.FixedThemeResolver
org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping
org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\
org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter
org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver,\
org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\
org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver
org.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator
org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver
org.springframework.web.servlet.FlashMapManager=org.springframework.web.servlet.support.SessionFlashMapManager

however onRefresh When was it called ? There are two places , One is Servlet Called during initialization initWebApplicationContext Initialize the container , This method will trigger onRefresh; There's another one , stay FrameworkServlet There is one of them. onApplicationEvent Method , And this method will be used by the inner class ContextRefreshListener call , This class is implemented ApplicationListener Interface , Indicates that a container refresh event will be received .
That's it MVC HandlerMapping Initialization logic of components , The implementation logic of other components is the same , It will not be analyzed below .

call Controller

go back to getHandler Method , It calls AbstractHandlerMapping Class method :

 public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {

// On request uri Get the corresponding HandlerMethod object 
Object handler = getHandlerInternal(request);
if (handler == null) {

handler = getDefaultHandler();
}
if (handler == null) {

return null;
}
// Bean name or resolved handler?
if (handler instanceof String) {

String handlerName = (String) handler;
handler = obtainApplicationContext().getBean(handlerName);
}
// obtain HandlerMethod And the packing class of the filter chain 
HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
if (logger.isTraceEnabled()) {

logger.trace("Mapped to " + handler);
}
else if (logger.isDebugEnabled() && !request.getDispatcherType().equals(DispatcherType.ASYNC)) {

logger.debug("Mapped to " + executionChain.getHandler());
}
// Whether it is a cross domain request , It's about looking at request Whether there is... In the request header Origin attribute 
if (CorsUtils.isCorsRequest(request)) {

// Get cross domain configuration by using custom hook method 
CorsConfiguration globalConfig = this.corsConfigurationSource.getCorsConfiguration(request);
// Annotation get cross domain configuration 
CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
CorsConfiguration config = (globalConfig != null ? globalConfig.combine(handlerConfig) : handlerConfig);
// Cross domain filters are set here CorsInterceptor
executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
}
return executionChain;
}

First look at AbstractHandlerMethodMapping.getHandlerInternal

 protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {

// from request Get in object uri,/common/query2
String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
this.mappingRegistry.acquireReadLock();
try {

// according to uri Find the corresponding... From the mapping relationship HandlerMethod object 
HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
// hold Controller Class instantiation 
return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
}
finally {

this.mappingRegistry.releaseReadLock();
}
}
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {

List<Match> matches = new ArrayList<>();
// according to url Get the corresponding RequestMappingInfo
List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
if (directPathMatches != null) {

addMatchingMappings(directPathMatches, matches, request);
}
if (matches.isEmpty()) {

// No choice but to go through all mappings...
addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
}
if (!matches.isEmpty()) {

Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
matches.sort(comparator);
Match bestMatch = matches.get(0);
if (matches.size() > 1) {

if (logger.isTraceEnabled()) {

logger.trace(matches.size() + " matching mappings: " + matches);
}
if (CorsUtils.isPreFlightRequest(request)) {

return PREFLIGHT_AMBIGUOUS_MATCH;
}
Match secondBestMatch = matches.get(1);
// If two RequestMappinginfo Everything is the same , Report errors 
if (comparator.compare(bestMatch, secondBestMatch) == 0) {

Method m1 = bestMatch.handlerMethod.getMethod();
Method m2 = secondBestMatch.handlerMethod.getMethod();
String uri = request.getRequestURI();
throw new IllegalStateException(
"Ambiguous handler methods mapped for '" + uri + "': {" + m1 + ", " + m2 + "}");
}
}
request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.handlerMethod);
handleMatch(bestMatch.mapping, lookupPath, request);
return bestMatch.handlerMethod;
}
else {

return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request);
}
}
private void addMatchingMappings(Collection<T> mappings, List<Match> matches, HttpServletRequest request) {

for (T mapping : mappings) {

// Get a match RequestMappingInfo object , There may be url identical ,@RequestMapping Properties of ( Request mode 、 Parameters, etc. ) Can't match 
T match = getMatchingMapping(mapping, request);
if (match != null) {

//RequestMappingInfo Objects and HandlerMethod Objects are encapsulated in Match In the object , It's actually annotation attributes and Method Object mapping 
matches.add(new Match(match, this.mappingRegistry.getMappings().get(mapping)));
}
}
}

The logic here is very simple , By request url from urlLookup Get the corresponding RequestMappingInfo( every last @RequestMapping Corresponding to one RequestMappingInfo object ) object , According to RequestMappingInfo Objects from mappingLookup Get the corresponding HandlerMethod And back to .
But here you may be curious urlLookup and mappingLookup Where did you come from , If you look closely, you will find that the current class implements an interface InitializingBean, The class that implements this interface will be in the class Bean After completion of instantiation, call afterPropertiesSet Method , The above mapping relationship is done in this method . In fact, this method not only completes the above two mapping relationships , And the next two :

  • corsLookup:handlerMethod -> corsConfig
  • registry:RequestMappingInfo -> MappingRegistration( contain url、handlerMethod、RequestMappingInfo、name Etc )

There is no analysis here , Here is a sequence diagram , Readers can analyze by themselves according to the following sequence diagram :
 Insert picture description here
Get HandlerMethod After the object , It'll go through again getHandlerExecutionChain Method to get all HandlerInterceptor Interceptor object , And along with HandlerMethod Objects are encapsulated together as HandlerExecutionChain. After getting cross domain configuration , There is no detailed analysis here .
Get HandlerExecutionChain Object and return to doDispatch Method , Call again getHandlerAdapter
How to get HandlerAdapter

 protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {

// according to handlerMethod object , Find the right HandlerAdapter object , The policy pattern is used here 
if (this.handlerAdapters != null) {

for (HandlerAdapter adapter : this.handlerAdapters) {

if (adapter.supports(handler)) {

return adapter;
}
}
}
}

there handlerAdapters Where do variable values come from ? I believe I don't have to analyze it again , It mainly depends on the design idea here , Typical The strategy pattern .
After that, the call is complete. Front filter after , Is really calling us controller The logic of the method , adopt HandlerAdapter.handle To call , Will eventually be called to ServletInvocableHandlerMethod.invokeAndHandle

 public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {

// Specific call logic , The key to see 
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
setResponseStatus(webRequest);
if (returnValue == null) {

if (isRequestNotModified(webRequest) || getResponseStatus() != null || mavContainer.isRequestHandled()) {

mavContainer.setRequestHandled(true);
return;
}
}
else if (StringUtils.hasText(getResponseStatusReason())) {

mavContainer.setRequestHandled(true);
return;
}
mavContainer.setRequestHandled(false);
Assert.state(this.returnValueHandlers != null, "No return value handlers");
try {

// Return value processing 
this.returnValueHandlers.handleReturnValue(
returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
}
catch (Exception ex) {

if (logger.isTraceEnabled()) {

logger.trace(formatErrorForReturnValue(returnValue), ex);
}
throw ex;
}
}

This method mainly depends on invokeForRequest and handleReturnValue Call to , The former is to complete the parameter binding and call controller, The latter processes the return value and encapsulates it into ModelAndViewContainer in . First look invokeForRequest

 public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {

// Get parameter array 
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
if (logger.isTraceEnabled()) {

logger.trace("Arguments: " + Arrays.toString(args));
}
return doInvoke(args);
}

doInvoke Is to complete the reflection call , It mainly depends on the implementation logic of parameter binding , stay getMethodArgumentValues In the method :

 protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {

if (ObjectUtils.isEmpty(getMethodParameters())) {

return EMPTY_ARGS;
}
// The packaging class of the input parameter , Parameter types are packed inside , Parameter name , Parameter annotation and other information 
MethodParameter[] parameters = getMethodParameters();
Object[] args = new Object[parameters.length];
for (int i = 0; i < parameters.length; i++) {

MethodParameter parameter = parameters[i];
// Set the parameter name 
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
args[i] = findProvidedArgument(parameter, providedArgs);
if (args[i] != null) {

continue;
}
// A typical strategy pattern , according to parameter Can I find the processing class of the corresponding parameter , If you can find it, go back true
if (!this.resolvers.supportsParameter(parameter)) {

throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
}
try {

// Specific parameter value analysis process , Focus on 
args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
}
catch (Exception ex) {

// Leave stack trace for later, exception may actually be resolved and handled..
if (logger.isDebugEnabled()) {

String error = ex.getMessage();
if (error != null && !error.contains(parameter.getExecutable().toGenericString())) {

logger.debug(formatArgumentError(parameter, error));
}
}
throw ex;
}
}
return args;
}

Parameters 、 Return value resolution

Because there are many parameter types , It will also be accompanied by various annotations , Such as :@RequestBody、@RequestParam、@PathVariable etc. , Therefore, the work of parameter analysis is very complicated , And also consider Extensibility , therefore SpringMVC Still using The strategy pattern To complete the analytical binding of various parameter types , Its top-level interface is HandlerMethodArgumentResolver, And default SpringMVC The parsing method provided is up to 20 Varied :
 Insert picture description here
Above is the class diagram , Readers can find corresponding classes to analyze according to their familiar parameter types , The most important thing is to master the design idea here .
Then, after the method call is completed, the return value is processed , alike , There are also many return value types , You can also use various annotations to mark , So it's also using The strategy pattern Realization , Its top-level interface is HandlerMethodReturnValueHandler, The implementation classes are as follows :
 Insert picture description here
After the call is completed, the subsequent operations are executed : perform Center filter 、 Handling global exceptions 、 View rendering and execution Rear filter , These have little to do with the mainstream process , This article will not expand the analysis , And finally MVC Execution sequence diagram of :
 Insert picture description here

summary

This is Spring The last article in the core principles series , It took a month before and after , Finally, I have a general understanding of Spring The implementation principle and operation mechanism of , Understand how some pits in the previous project were produced , The most important thing is to learn how to use design patterns and how to use Spring Some common extension points for custom extension . But for the Spring For this huge system , There is still a lot to understand and learn , Especially the design idea , Only through long-term thinking can we deeply understand and master . In my previous articles, including this one, there are many details that have not been analyzed , I will share it from time to time later .

copyright:author[Don't talk at night],Please bring the original link to reprint, thank you. https://en.javamana.com/2022/02/202202130725599913.html