This article focuses on Spring’s component-scan tag, and understands that Spring can be used to register beans by scanning annotations. For NamespaceHandler, NamespaceHandlerSupport and BeanDefinitionParser, you need to configure the Spring. handlers file, which will be interpreted in the following source code. In this blog post, we will use ApplicationConntext as a starting point and start directly from the differences. If you want to see the source flow of ApplicationContext, seeLast blog.
GItHub:github.com/lantaoGitHu…
Here’s a breakdown of their relationship:
NamespaceHandlerSupport is an Abstract class that implements the NamespaceHandler interface. It implements the NamespaceHandler interface’s Parser and doCreate methods. NamespaceHandler inherits the NamespaceHandlerSupport class and needs to implement the init method of the NamespaceHandler interface. Init method needs to do the registration operation of the parser class, the code is as follows:
package org.springframework.context.config; import org.springframework.beans.factory.xml.NamespaceHandlerSupport; import org.springframework.context.annotation.AnnotationConfigBeanDefinitionParser; import org.springframework.context.annotation.ComponentScanBeanDefinitionParser; /** * {@link org.springframework.beans.factory.xml.NamespaceHandler} * for the '{@code context}'Namespace. * * @author Mark Fisher * @author Juergen Hoeller * @since 2.5 */ Public Class ContextNamespaceHandler extends NamespaceHandlerSupport { @Override public voidinit() { registerBeanDefinitionParser("property-placeholder", new PropertyPlaceholderBeanDefinitionParser()); registerBeanDefinitionParser("property-override", new PropertyOverrideBeanDefinitionParser()); registerBeanDefinitionParser("annotation-config", new AnnotationConfigBeanDefinitionParser()); registerBeanDefinitionParser("component-scan", new ComponentScanBeanDefinitionParser()); registerBeanDefinitionParser("load-time-weaver", new LoadTimeWeaverBeanDefinitionParser()); registerBeanDefinitionParser("spring-configured", new SpringConfiguredBeanDefinitionParser()); registerBeanDefinitionParser("mbean-export", new MBeanExportBeanDefinitionParser()); registerBeanDefinitionParser("mbean-server", new MBeanServerBeanDefinitionParser()); }}Copy the code
/** * Subclasses can call this to register the supplied {@link BeanDefinitionParser} to * handle the specified element. The element name is the local (non-namespace qualified) * name. */ protected final void registerBeanDefinitionParser(String elementName, BeanDefinitionParser parser) { this.parsers.put(elementName, parser); }Copy the code
BeanDefinitionParser class is the top-level interface of the Parser class. Custom Parser classes need to implement BeanDefinitionParser class Parser method. Registration of Parser classes is done in the init method of NameSpaceHandler;
- Let’s take a look at the test class:
package lantao.scan;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class ScanTest {
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-bean-scan.xml"); }}Copy the code
The default value of the use-default-filters attribute in the XML file is true, which means that packets are scanned using the default Filter, which is tagged with @service, @controller, The @Component and @repository annotation classes are scanned and, if set to false, a custom include-filter is required.
<? xml version="1.0" encoding="UTF-8"? > <beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <! -- use-default-filters property defaults totrue, that is, the default Filter is used for packet scanning. The default Filter scans classes marked with @service,@Controller, and @repository annotations --> <context:component-scan base-package="lantao.scan" use-default-filters="false"> <! -- Scan only the controller annotations of the base-package and exclude the corresponding exclude-filter tag; use-default-filters="false"Use with include-filter throw back exceptions with exclude-filter --> <context:include-filtertype="annotation" expression="org.springframework.stereotype.Controller"/>
<context:include-filter type="annotation" expression="org.springframework.stereotype.Component"/>
<context:include-filter type="annotation" expression="org.springframework.stereotype.Service"/>
<context:include-filter type="annotation" expression="org.springframework.stereotype.Repository"/>
</context:component-scan>
</beans>Copy the code
Because we use ApplicationContext, which was interpreted in the previous article, let’s go straight to the differences.
- Differences between the code in method of parseBeanDefinitions DefaultBeanDefinitionDocumentReader classes:
/**
* Parse the elements at the root level in the document:
* "import"."alias"."bean". * @param root the DOM root element of the document */ protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {/ / validate XML namespace, BeanDefinitionParserDelegate BEANS_NAMESPACE_URIif (delegate.isDefaultNamespace(root)) {
NodeList nl = root.getChildNodes();
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
if (node instanceof Element) {
Element ele = (Element) node;
if(delegate. IsDefaultNamespace (ele)) {/ / the default label treatment / / here only nade namespace is http://www.springframework.org/schema/beans parseDefaultElement(ele, delegate); }else{// Custom tag processing resolves <context:component-scan base-package="lantao.scan"/ > or custom dubbo delegate. ParseCustomElement (ele); }}}}else{/ / on the custom tag handler delegate. ParseCustomElement (root); }}Copy the code
In the main differences between the parseDefaultElement (ele, delegate) and delegate. ParseCustomElement (ele) method, ParseDefaultElement method can only deal with node namespace is: www.springframework.org/schema/bean… Tags, other tags, and custom tags are all parsed using this method;
- Delegate. ParseCustomElement source code:
@Nullable
public BeanDefinition parseCustomElement(Element ele) {
return parseCustomElement(ele, null);
}
@Nullable
public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) {
// 获取node的 NameSpaceURI
String namespaceUri = getNamespaceURI(ele);
if (namespaceUri == null) {
returnnull; Handlers add the spring.handlers file to the meta-INF file for example: http\://www.springframework.org/schema/context=org.springframework.context.config.ContextNamespaceHandler / / according to specified Handlers can refer to the Spring. handlers file // abstract NamespaceHandlerSupport implements NamespaceHandler Interface, which implements two NamespaceHandler methods (Parser, doCreate), // NamespaceHandler init method. Mainly do register BeanDefinitionParser (registerBeanDefinitionParser), Custom parser classes need to inherit BeanDefinitionParser class NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);if (handler == null) {
error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele);
returnnull; } // Parse operationsreturn handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
}Copy the code
The code here is very simple and does only three things:
1: Get Element’s NamespaceUri;
2: NameSpaceHandler is processed by the resolver method of NameSpaceHandler resolver.
3. Parse labels using the NameSpaceHandler Parse method.
- Let’s look directly at the resolve method:
/**
* Locate the {@link NamespaceHandler} for the supplied namespace URI
* from the configured mappings.
* @param namespaceUri the relevant namespace URI
* @return the located {@link NamespaceHandler}, or {@code null} ifNone found */ @override @nullable public NamespaceHandler resolve(String namespaceUri) // NamespaceHandler is resource/ meta-INF /spring. Handler's class key is namespaceUri, // These classes inherit NamespaceHandlerSupport, which implements BeanDefinitionParse (Map<String). Object> handlerMappings = getHandlerMappings(); // Get the corresponding handler or className in handlerMappings via namespaceUri. Object handlerOrClassName = handlerMapping.get (namespaceUri);if (handlerOrClassName == null) {
return null;
}
else if (handlerOrClassName instanceof NamespaceHandler) {
return (NamespaceHandler) handlerOrClassName;
}
else{ String className = (String) handlerOrClassName; Try {// instantiate Class<? > handlerClass = ClassUtils.forName(className, this.classLoader); // Determine if the instantiated class's superclass or superinterface is a NamespaceHandlerif(! NamespaceHandler.class.isAssignableFrom(handlerClass)) { throw new FatalBeanException("Class [" + className + "] for namespace [" + namespaceUri +
"] does not implement the [" + NamespaceHandler.class.getName() + "] interface"); } NamespaceHandler namespaceHandler = (NamespaceHandler) BeanUtils.instantiateClass(handlerClass); / / registered custom tags for parsing strategy class inherits the BeanDefinitionParser classes, such as ComponentScanBeanDefinitionParser namespaceHandler. The init (); // Add handlermapping.put (namespaceUri, namespaceHandler);return namespaceHandler;
}
catch (ClassNotFoundException ex) {
throw new FatalBeanException("Could not find NamespaceHandler class [" + className +
"] for namespace [" + namespaceUri + "]", ex);
}
catch (LinkageError err) {
throw new FatalBeanException("Unresolvable class definition for NamespaceHandler class [" +
className + "] for namespace [" + namespaceUri + "]", err); }}}Copy the code
/ / GetMappings (); / / GetMappings (); / / GetMappings (); / / GetMappings (); / / GetMappings (); / / GetMappings (); / / GetMappings (); / / GetMappings (); Then namespaceUri is used to obtain the corresponding NameSpaceHandler. If it is not instantiated, the instantiation operation is performed and init method is used to register and parse classes to parsers. Otherwise, it is returned directly. The NameSpaceHandler obtained by the getHandlerMappings method is mapped to the resource/ meta-INF /spring.handler file. Key is the namespaceUri,value is the custom NameSpaceHandler;
- GetHandlerMappings method
/**
* Load the specified NamespaceHandler mappings lazily.
*/
private Map<String, Object> getHandlerMappings() {
Map<String, Object> handlerMappings = this.handlerMappings;
if (handlerMappings == null) {
synchronized (this) {
handlerMappings = this.handlerMappings;
if (handlerMappings == null) {
if (logger.isTraceEnabled()) {
logger.trace("Loading NamespaceHandler mappings from [" + this.handlerMappingsLocation + "]"); Handler Properties mapping = // // // // // // // // // // PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader);if (logger.isTraceEnabled()) {
logger.trace("Loaded NamespaceHandler mappings: " + mappings);
}
handlerMappings = new ConcurrentHashMap<>(mappings.size());
CollectionUtils.mergePropertiesIntoMap(mappings, handlerMappings);
this.handlerMappings = handlerMappings;
}
catch (IOException ex) {
throw new IllegalStateException(
"Unable to load NamespaceHandler mappings from location [" + this.handlerMappingsLocation + "]", ex); }}}}return handlerMappings;
}Copy the code
/ / getHandlerMappings (); / / GetMappings (); / / GetMappings (); / / GetMappings (); / / GetMappings (); / / GetMappings (); / / GetMappings (); So NameSpaceHandler gets that so let’s look at the BeanDefinitionParser method that init registered;
- NameSpaceHandlerSupport parse method
/**
* Parses the supplied {@link Element} by delegating to the {@link BeanDefinitionParser} that is
* registered forthat {@link Element}. */ @Override @Nullable public BeanDefinition parse(Element element, ParserContext ParserContext) {// Get BeanDefinitionParser's implementation class from the Parser collection in NamespaceHandlerSupport BeanDefinitionParser parser = findParserForElement(element, parserContext);return(parser ! = null ? parser.parse(element, parserContext) : null); }Copy the code
The parse method does two things:
1. Obtain the corresponding BeanDefinitionParser class by defining tag attributes (e.g. Component-scan).
/**
* Locates the {@link BeanDefinitionParser} from the register implementations using
* the localname of the supplied {@link Element}. */ @Nullable private BeanDefinitionParser findParserForElement(Element element, ParserContext ParserContext) {// Here to determine the parsing strategy of various tags get the tag name StringlocalName = parserContext.getDelegate().getLocalName(element); // Get parsers from the corresponding parsing policy class parsers is initialized in the init method of NameSpaceHandler; BeanDefinitionParser parser = this.parsers.get(localName);
if (parser == null) {
parserContext.getReaderContext().fatal(
"Cannot locate BeanDefinitionParser for element [" + localName + "]", element); } // return the corresponding policy class for parsingreturn parser;
}Copy the code
2.
- Parse method source:
@Override @Nullable public BeanDefinition parse(Element element, String basePackage = element.getAttribute(BASE_PACKAGE_ATTRIBUTE); ParserContext ParserContext) {// Obtain the path to basePackage. // Parse in the given text${...}, replacing it with {@link#getProperty} resolves the corresponding property value using ${} and the value in propertiesbasePackage = parserContext.getReaderContext().getEnvironment().resolvePlaceholders(basePackage); / / we are here in setting up base - when the value of the package, you can through the separator instructions above ConfigurableApplicationContext. CONFIG_LOCATION_DELIMITERS to specify more than one package. You can use ", "";" "\ t \ n" (carriage return) to segment multiple package name String [] basePackages = StringUtils. TokenizeToStringArray (basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS); // Actually scanforBean definitions and register them. // The following code actually scans the bean definitions and registers them. / / configuration ClassPathBeanDefinitionScanner ClassPathBeanDefinitionScanner scanner = configureScanner (parserContext, element); Set<BeanDefinitionHolder> beanDefinitions = scanner.doscan (basePackages); // Set<BeanDefinitionHolder> beanDefinitions = scanner.doscan (basePackages); / / handle the annotation - config registerComponents (parserContext getReaderContext (), beanDefinitions, element).return null;
}Copy the code
The Parse method does five things:
1: Obtain the value of basePackage, which is the path address configured in XML.
2: basePackage can be configured as multiple, using ‘, ‘. ‘or carriage return to split;
3: initialize ClassPathBeanDefinitionScanner, behind the parsing operation have ClassPathBeanDefinitionScanner to complete;
4: Scan and register beans;
5: Processing annotation-config(more on this later, but not here)
- ConfigureScanner (configureScanner)
protected ClassPathBeanDefinitionScanner configureScanner(ParserContext parserContext, Element element) {
boolean useDefaultFilters = true; // Sets the default value of the use-default-filters tag to the use-default-filters attributetrueThat is, the default Filter is used for package scanning, and the default Filter is used for scanning classes marked with @service,@Controller, and @repository annotationsif(element.hasAttribute(USE_DEFAULT_FILTERS_ATTRIBUTE)) { useDefaultFilters = Boolean.valueOf(element.getAttribute(USE_DEFAULT_FILTERS_ATTRIBUTE)); } / / the Delegate bean definition registration to scanner class. / / registered bean will be commissioned to ClassPathBeanDefinitionScanner class. Initialize ClassPathBeanDefinitionScanner, ClassPathBeanDefinitionScanner is analytic conponentScanner class ClassPathBeanDefinitionScanner scanner = createScanner(parserContext.getReaderContext(), useDefaultFilters); scanner.setBeanDefinitionDefaults(parserContext.getDelegate().getBeanDefinitionDefaults()); scanner.setAutowireCandidatePatterns(parserContext.getDelegate().getAutowireCandidatePatterns()); //setRESOURCE_PATTERN_ATTRIBUTE sets the path of scanning Resource(Resource) to default"**/*.class"
if (element.hasAttribute(RESOURCE_PATTERN_ATTRIBUTE)) {
scanner.setResourcePattern(element.getAttribute(RESOURCE_PATTERN_ATTRIBUTE));
}
try {
// setName-generator // Initialize the bean name generator parseBeanNameGenerator(element, scanner); } catch (Exception ex) { parserContext.getReaderContext().error(ex.getMessage(), parserContext.extractSource(element), ex.getCause()); } try {// Set the bean scope to parseScope(element, scanner); } catch (Exception ex) { parserContext.getReaderContext().error(ex.getMessage(), parserContext.extractSource(element), ex.getCause()); } // Set up scanning for included and excluded annotations // Set up filters, which specify which classes to be processed and which classes to be ignored //set INCLUDE_FILTER_ELEMENT and EXCLUDE_FILTER_ELEMENT
parseTypeFilters(element, scanner, parserContext);
return scanner;
}Copy the code
The configureScanner method does five things:
1: Gets and sets use-default-filters. The use-default-filters property defaults to true to use the default Filter for packet scanning. By default, Filter scans classes marked with @service,@Controller, and @repository annotations. If set to false, add your own include-filter.
2: initialize ClassPathBeanDefinitionScanner, if use default – filters to true to include – filter for the add operation;
3: Initialize the bean name generator;
4: set the bean scope;
5: Set the scan to include and exclude annotations, include-filter and exclude-filter.
The above code is not shown, git code has corresponding comments;
- Next look at the scanner.doscan method:
/**
* Perform a scan within the specified base packages,
* returning the registered bean definitions.
* <p>This method does <i>not</i> register an annotation config processor
* but rather leaves this up to the caller.
* @param basePackages the packages to check for annotated classes
* @return set of beans registered if any for tooling registration purposes (never {@code null})
*/
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
Assert.notEmpty(basePackages, "At least one base package must be specified"); Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>(); // Loop scanfor(String basePackage : BasePackages) {// All BeanDefinition Set<BeanDefinition> candidates = findCandidateComponents(basePackage);for(BeanDefinition candidate : Candidates) {/ / get a ScopeMetadata object, the default is AnnotationScopeMetadataResolver / / if the target class is not @ the Scope annotations, Returns a default ScopeMetadata ScopeMetadata ScopeMetadata = this. ScopeMetadataResolver. ResolveScopeMetadata (candidate); candidate.setScope(scopeMetadata.getScopeName()); / / use the bean name generator generates bean name, the default generator for AnnotationBeanNameGenerator / / first by the value of annotation bean name, if the value is no value of the annotation, Use the default name String beanName = this. BeanNameGenerator. GenerateBeanName (candidate, enclosing the registry).if (candidate instanceof AbstractBeanDefinition) {
postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
}
if(candidate instanceof AnnotatedBeanDefinition) {// Process annotations defined on the target class, This includes @lazy, @primary, @dependson, @role, @description // this will check and set @lazy, @primary https://www.cnblogs.com/liaojie970/p/7885106.html) @ DependsOn (need to rely on, but don't need to hold) AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate); } // Check if beanName already exists in BeanDefinitionRegistry.if(checkCandidate(beanName, BeanDefinitionHolder(candidate, beanName)) {// BeanDefinitionHolder = new BeanDefinitionHolder(candidate, beanName); // Create a scoped proxy if necessary Return said proxy objects BeanDefinitionHolder definitionHolder = AnnotationConfigUtils. ApplyScopedProxyMode (scopeMetadata, definitionHolder, this.registry); beanDefinitions.add(definitionHolder); // Register Bean registerBeanDefinition(definitionHolder, this.registry); }}}return beanDefinitions;
}Copy the code
Take a look at what is done in the doScan method:
1: Get all beanDefinitions under the specified package (the specified basePackage);
2: get a ScopeMetadata object, the default for AnnotationScopeMetadataResolver, if the target class is not @ the Scope annotations, it returns a default ScopeMetadata;
3: use the bean name generator bean name, the default generator for AnnotationBeanNameGenerator, if the value on the annotation value is null, the need to generate;
4: set the AutowireCandidate autowire – candidate = “false”, said the object don’t participate in automatic injection, reference: blog.csdn.net/shangboerds…
5: Handle annotations defined on the target class, including @lazy, @primary, @dependson, @role, @description, Here will check and set AnnotatedBeanDefinition @ Lazy Lazy loading) @ Primary (main, www.cnblogs.com/liaojie970/.) @dependson annotation;
6. Check whether beanName already exists in beanDefinitionMap.
If scopedProxyMode is set, create proxy class and register proxy class.
Call registerBeanDefinition to register the bean, which is put to beanDefinitionMap.
- ScanCandidateComponents (scanCandidateComponents);
private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
Set<BeanDefinition> candidates = new LinkedHashSet<>();
try {
// ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX = "classpath*:"; // By observing the implementation of the resolveBasePackage() method, we can set basePackage with the following exampleThe ${}// this.resourcePattern defaults to"**/*.class"ResourcePattern can be configured in the XML String packageSearchPath = ResourcePatternResolver. CLASSPATH_ALL_URL_PREFIX + resolveBasePackage(basePackage) +'/'+ this.resourcePattern; // use the above concatenated shape"classpath*:xx/yyy/zzz/**/*.class"// Use ResourcePatternResolver's getResources method to retrieve all of the resources in the path: classpath*:lantao/scan/**/*.class Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath); boolean traceEnabled = logger.isTraceEnabled(); boolean debugEnabled = logger.isDebugEnabled();for (Resource resource : resources) {
if (traceEnabled) {
logger.trace("Scanning "+ resource); } // File is readableif(resource.isreadable ()) {try {// Get metadata metadata is the data that defines the data that defines the class MetadataReader MetadataReader = getMetadataReaderFactory().getMetadataReader(resource); // Make includeFilters excludeFilters according to the boiler to determine whether it meets the requirementsif(isCandidateComponent (metadataReader)) {/ / instantiate ScannedGenericBeanDefinition ScannedGenericBeanDefinition SBD = new ScannedGenericBeanDefinition(metadataReader); sbd.setResource(resource); sbd.setSource(resource); // The judgment class must be a concrete implementation class and its instantiation must be independentif (isCandidateComponent(sbd)) {
if (debugEnabled) {
logger.debug("Identified candidate component class: " + resource);
}
candidates.add(sbd);
}
else {
if (debugEnabled) {
logger.debug("Ignored because not a concrete top-level class: "+ resource); }}}else {
if (traceEnabled) {
logger.trace("Ignored because not matching any filter: " + resource);
}
}
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to read candidate component class: "+ resource, ex); }}else {
if (traceEnabled) {
logger.trace("Ignored because not readable: " + resource);
}
}
}
}
catch (IOException ex) {
throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex);
}
return candidates;
}Copy the code
GetResource of ResourcePatternResolver The MetadataReader is obtained through the Resource file Resource (metadata is used to define the data is used to define the attributes of the class). The isCandidateComponent method is used to do the core processing, because the resources obtained through the path are all. NcludeFilters excludeFilters is determined by isCandidateComponent(SBD). BeanDefinition must be an implementation class. It cannot be an interface.
- Let’s look at the core judgment method isCandidateComponent:
-
/** * Determine whether the given class does not match any exclude filter * and does match at least one include filter. * @param metadataReader the ASM ClassReader for the class * @returnwhether the class qualifies as a candidate component */ protected boolean isCandidateComponent(MetadataReader MetadataReader) throws IOException {// Judge the TypeFilter of excludeFiltersfor (TypeFilter tf : this.excludeFilters) { if (tf.match(metadataReader, getMetadataReaderFactory())) { return false; By default, TypeFilter in includeFilters contains @components and @service @controller @repositoryfor (TypeFilter tf : this.includeFilters) { if(tf.match(metadataReader, getMetadataReaderFactory())) {// Judge @Conditional, @conditional is a new annotation provided by Spring4, Its function is to judge according to certain conditions, meet the conditions to register beans to the container. And @conditionalonxxreturnisConditionMatch(metadataReader); }}return false; }Copy the code
/** * Determine whether the given bean definition qualifies as candidate. * <p>The default implementation checks whether the class is not an interface * and not dependent on an enclosing class. * <p>Can be overriddenin subclasses. * @param beanDefinition the bean definition to check * @returnwhether the bean definition qualifies as a candidate component */ protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { AnnotationMetadata metadata = beanDefinition.getMetadata(); // metadata.isindependent () isIndependent & // metadata.isconcrete () is an interface or an abstract class or // must be an abstract class with @lookup annotationsreturn (metadata.isIndependent() && (metadata.isConcrete() || (metadata.isAbstract() && metadata.hasAnnotatedMethods(Lookup.class.getName())))); }Copy the code
-
The source code of component-scan scan injection has been covered here. There is no detailed explanation about proxy and annotation-config. It will be done in a subsequent article.Blog.csdn.net/qq_30257149…