Spring 注解(二)注解工具類 AnnotationUtils 和 AnnotatedElementUtils

轉(zhuǎn)載自:https://www.cnblogs.com/binarylei/p/10415585.html

Spring 注解(二)注解工具類 AnnotationUtils 和 AnnotatedElementUtils

Spring 系列目錄(https://www.cnblogs.com/binarylei/p/10198698.html)

Spring 注解系列文章:

Spring 注解(一)Spring 注解編程模型

Spring 注解(二)注解工具類 AnnotationUtils 和 AnnotatedElementUtils

首先回顧一下 AnnotationUtils 和 AnnotatedElementUtils 這兩個注解工具類的用法:

@Test@GetMapping(value ="/GetMapping", consumes = MediaType.APPLICATION_JSON_VALUE)publicvoidtest()throwsNoSuchMethodException{? ? Method method = ReflectUtils.findDeclaredMethod(? ? ? ? ? ? AliasForTest.class,"test",null);// AnnotationUtils 不支持注解屬性覆蓋RequestMapping requestMappingAnn1 = AnnotationUtils.getAnnotation(method, RequestMapping.class);? ? Assert.assertEquals(newString[]{}, requestMappingAnn1.value());? ? Assert.assertEquals(newString[]{}, requestMappingAnn1.consumes());// AnnotatedElementUtils 支持注解屬性覆蓋RequestMapping requestMappingAnn2 = AnnotatedElementUtils.getMergedAnnotation(method, RequestMapping.class);? ? Assert.assertEquals(newString[]{"/GetMapping"}, requestMappingAnn2.value());? ? Assert.assertEquals(newString[]{MediaType.APPLICATION_JSON_VALUE}, requestMappingAnn2.consumes());}

一、AnnotationUtils 源碼分析

AnnotationUtils 解決注解別名,包括顯式別名、隱式別名、傳遞的隱式別名,還可以查的指定注解的屬性信息。

AnnotationUtils 底層使用動態(tài)代理的方式處理注解別名的問題。

1.1 get* 系列注解查找

get 遵循 JDK 的注解查找語義,只是增加了一級元注解的查找。

publicstaticAgetAnnotation(Annotation annotation, Class<A> annotationType){// 1. 直接查找本地注解if(annotationType.isInstance(annotation)) {returnsynthesizeAnnotation((A) annotation);? ? }// 2. 元注解上查找,注意相對于 find* 而言,這里只查找一級元注解Class annotatedElement = annotation.annotationType();try{? ? ? ? A metaAnn = annotatedElement.getAnnotation(annotationType);return(metaAnn !=null? synthesizeAnnotation(metaAnn, annotatedElement) :null);? ? }catch(Throwable ex) {? ? ? ? handleIntrospectionFailure(annotatedElement, ex);returnnull;? ? }}

1.2 find* 系列注解查找

遵循 JDK 的注解查找語義,只是增加了多級元注解的查找。

// visited 表示已經(jīng)查找的元素,Spring 的遞歸很多都用到了這個參數(shù)privatestaticAfindAnnotation(

? ? ? ? AnnotatedElement annotatedElement, Class<A> annotationType, Set<Annotation> visited){try{// 1. 本地注解查找A annotation = annotatedElement.getDeclaredAnnotation(annotationType);if(annotation !=null) {returnannotation;? ? ? ? }// 2. 元注解上查找for(Annotation declaredAnn : getDeclaredAnnotations(annotatedElement)) {? ? ? ? ? ? Class declaredType = declaredAnn.annotationType();if(!isInJavaLangAnnotationPackage(declaredType) && visited.add(declaredAnn)) {// 3. 元注解上遞歸查找annotation = findAnnotation((AnnotatedElement) declaredType, annotationType, visited);if(annotation !=null) {returnannotation;? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? }? ? }catch(Throwable ex) {? ? ? ? handleIntrospectionFailure(annotatedElement, ex);? ? }returnnull;}

1.3 synthesizeAnnotation 動態(tài)代理解決別名問題

staticAsynthesizeAnnotation(A annotation, @Nullable Object annotatedElement){// 1. SynthesizedAnnotation 為一個標記,表示已經(jīng)動態(tài)代理過了//? ? hasPlainJavaAnnotationsOnly 如果是 java 中的注解不可能有注解別名,直接返回if(annotationinstanceofSynthesizedAnnotation || hasPlainJavaAnnotationsOnly(annotatedElement)) {returnannotation;? ? }// 2. 判斷是否需要進行動態(tài)代理,即注解中存在別名,包括顯示別名、隱式別名、傳遞的隱式別名Class annotationType = annotation.annotationType();if(!isSynthesizable(annotationType)) {returnannotation;? ? }// 3. AnnotationAttributeExtractor 用于從注解 annotation 中提取屬性的值DefaultAnnotationAttributeExtractor attributeExtractor =newDefaultAnnotationAttributeExtractor(annotation, annotatedElement);// 4. SynthesizedAnnotationInvocationHandler 動態(tài)代理的類InvocationHandler handler =newSynthesizedAnnotationInvocationHandler(attributeExtractor);// 5. 接口中有 SynthesizedAnnotation,并返回動態(tài)代理的對象Class[] exposedInterfaces =newClass[] {annotationType, SynthesizedAnnotation.class};return(A) Proxy.newProxyInstance(annotation.getClass().getClassLoader(), exposedInterfaces, handler);}

1.4 SynthesizedAnnotationInvocationHandler

下面主要看一下動態(tài)代理的 invoke 實現(xiàn)是怎么實現(xiàn)的。

publicObjectinvoke(Object proxy, Method method, Object[] args)throwsThrowable{if(ReflectionUtils.isEqualsMethod(method)) {returnannotationEquals(args[0]);? ? }if(ReflectionUtils.isHashCodeMethod(method)) {returnannotationHashCode();? ? }if(ReflectionUtils.isToStringMethod(method)) {returnannotationToString();? ? }// 注解的 annotationType 返回注解的 Class 類型if(AnnotationUtils.isAnnotationTypeMethod(method)) {returnannotationType();? ? }if(!AnnotationUtils.isAttributeMethod(method)) {thrownewAnnotationConfigurationException(String.format("Method [%s] is unsupported for synthesized annotation type [%s]", method, annotationType()));? ? }// 真正獲取注解的屬性值returngetAttributeValue(method);}

getAttributeValue 的核心其實就一句話?this.attributeExtractor.getAttributeValue(attributeMethod);?委托給了對應(yīng)的 AnnotationAttributeExtractor 處理。

1.4.1 AnnotationAttributeExtractor

AbstractAliasAwareAnnotationAttributeExtractor(? ? ? ? ? ? Class annotationType,@NullableObject annotatedElement, S source) {? ? Assert.notNull(annotationType,"annotationType must not be null");? ? Assert.notNull(source,"source must not be null");this.annotationType = annotationType;this.annotatedElement = annotatedElement;this.source = source;this.attributeAliasMap = AnnotationUtils.getAttributeAliasMap(annotationType);}

在構(gòu)造方法中有一個很重要的方法 AnnotationUtils.getAttributeAliasMap(annotationType) 用于獲取其別名。

publicfinalObjectgetAttributeValue(Method attributeMethod){? ? String attributeName = attributeMethod.getName();// attributeValue 表示屬性的真實值Object attributeValue = getRawAttributeValue(attributeMethod);// 獲取所有的別名List aliasNames =this.attributeAliasMap.get(attributeName);if(aliasNames !=null) {// 屬性的默認值,默認值肯定是一樣的,因為在獲取別名的時候已經(jīng)校驗了默認值Object defaultValue = AnnotationUtils.getDefaultValue(this.annotationType, attributeName);for(String aliasName : aliasNames) {// 別名的真實值Object aliasValue = getRawAttributeValue(aliasName);// 如果兩個別名的值不相等,且都不等于默認值,直接拋異常if(!ObjectUtils.nullSafeEquals(attributeValue, aliasValue) &&? ? ? ? ? ? ? ? ? ? !ObjectUtils.nullSafeEquals(attributeValue, defaultValue) &&? ? ? ? ? ? ? ? ? ? !ObjectUtils.nullSafeEquals(aliasValue, defaultValue)) {thrownewAnnotationConfigurationException();? ? ? ? ? ? }if(ObjectUtils.nullSafeEquals(attributeValue, defaultValue)) {? ? ? ? ? ? ? ? attributeValue = aliasValue;? ? ? ? ? ? }? ? ? ? }? ? }returnattributeValue;}

1.5 AliasDescriptor

(1) getAttributeAliasMap

在 AbstractAliasAwareAnnotationAttributeExtractor 的構(gòu)造器中有一個很重要的方法 getAttributeAliasMap 獲取注解中所有屬性的別名。

staticMap> getAttributeAliasMap(@NullableClass annotationType) {? ? map =newLinkedHashMap<>();for(Method attribute : getAttributeMethods(annotationType)) {? ? ? ? List aliasNames = getAttributeAliasNames(attribute);if(!aliasNames.isEmpty()) {? ? ? ? ? ? map.put(attribute.getName(), aliasNames);? ? ? ? }? ? }returnmap;}staticListgetAttributeAliasNames(Method attribute){? ? AliasDescriptor descriptor = AliasDescriptor.from(attribute);return(descriptor !=null? descriptor.getAttributeAliasNames() : Collections.emptyList());}

可以別名獲取的所有的工作都是委托給了 AliasDescriptor 完成,這一小節(jié)我們就主要看一下這個類。

(2) AliasDescriptor 構(gòu)造及校驗

publicstaticAliasDescriptorfrom(Method attribute){? ? AliasFor aliasFor = attribute.getAnnotation(AliasFor.class);if(aliasFor ==null) {returnnull;? ? }? ? descriptor =newAliasDescriptor(attribute, aliasFor);? ? descriptor.validate();returndescriptor;}

構(gòu)建一個 AliasDescriptor 分為兩步:一是獲取注解信息(構(gòu)造器),二是校驗別名是否成立(validate)。@AliasFor 有以下的規(guī)約:

規(guī)約1:顯示別名可以不用配置 annotation 屬性

規(guī)約2:隱式別名默認和原注解屬性名稱一致,getAliasedAttributeName 中體現(xiàn)

規(guī)約3:隱式別名 @AliasFor 配置的注解必須出現(xiàn)在元注解中,可以是多級元注解

規(guī)約4:顯示別名必須成對配置

規(guī)約5:別名必須配置默認值,且默認值一致。注意別名可以為數(shù)組類型,而原屬性為數(shù)組的元素類型

privateAliasDescriptor(Method sourceAttribute, AliasFor aliasFor){? ? ? ? Class declaringClass = sourceAttribute.getDeclaringClass();// 1. 注解原字段的信息this.sourceAttribute = sourceAttribute;this.sourceAnnotationType = (Class) declaringClass;this.sourceAttributeName = sourceAttribute.getName();// 2. @AliasFor 注解的信息// 規(guī)約1:顯示的別名可以不用配置 annotation 屬性// 規(guī)約2:隱式別名默認和原注解屬性名稱一致,getAliasedAttributeName 中體現(xiàn)this.aliasedAnnotationType = (Annotation.class == aliasFor.annotation() ?this.sourceAnnotationType : aliasFor.annotation());this.aliasedAttributeName = getAliasedAttributeName(aliasFor, sourceAttribute);if(this.aliasedAnnotationType ==this.sourceAnnotationType &&this.aliasedAttributeName.equals(this.sourceAttributeName)) {thrownewAnnotationConfigurationException(...);? ? }try{// @AliasFor 配置的別名不存在直接拋出異常this.aliasedAttribute =this.aliasedAnnotationType.getDeclaredMethod(this.aliasedAttributeName);? ? }catch(NoSuchMethodException ex) {thrownewAnnotationConfigurationException(..., ex);? ? }// 3. isAliasPair=true 表示就同一個注解內(nèi)的顯示別名this.isAliasPair = (this.sourceAnnotationType ==this.aliasedAnnotationType);}

(3) getAttributeAliasNames 獲取別名

publicListgetAttributeAliasNames(){// 1. 顯示別名,直接返回if(this.isAliasPair) {returnCollections.singletonList(this.aliasedAttributeName);? ? }// 2. 隱式別名,包括可傳遞的隱式別名List aliases =newArrayList<>();// 2.1 遍歷注解中的其它屬性,一一判斷是否互為別名//? ? getOtherDescriptors 獲取其它的所有屬性//? ? isAliasFor 判斷兩個屬性是否互為別名,會遞歸向上查找for(AliasDescriptor otherDescriptor : getOtherDescriptors()) {if(this.isAliasFor(otherDescriptor)) {this.validateAgainst(otherDescriptor);? ? ? ? ? ? aliases.add(otherDescriptor.sourceAttributeName);? ? ? ? }? ? }returnaliases;}

(4) getAttributeOverrideName 獲取當(dāng)前屬性在元注解中對應(yīng)的別名

publicStringgetAttributeOverrideName(Class<? extends Annotation> metaAnnotationType){// 遞歸向上查找別名,如果 sourceAnnotationType==metaAnnotationType 則查找到了for(AliasDescriptor desc =this; desc !=null; desc = desc.getAttributeOverrideDescriptor()) {if(desc.isOverrideFor(metaAnnotationType)) {returndesc.aliasedAttributeName;? ? ? ? }? ? }returnnull;}

二、AnnotatedElementUtils 源碼分析

2.1 Processor 對匹配的注解進行后置處理

Processor 對匹配的注解進行后置處理,可以通過 process 方法的返回值來控制查找的流程:返回 null 時繼續(xù)查找,非 null 時直接返回。有一種情況例外就是 aggregates=true,這種情況要查找所有的注解,所以會繼續(xù)查找。

(1) 接口

privateinterfaceProcessor{// 兩個作用:一是根據(jù)返回值是否為 null 控制查詢的流程;二是對查詢的注解進行處理,主要是用于獲取該注解的屬性值Tprocess(@Nullable AnnotatedElement annotatedElement, Annotation annotation,intmetaDepth);// 只有 MergedAnnotationAttributesProcessor 有效,用于處理元注解屬性覆蓋// annotation 為當(dāng)前注解,result 為元注解屬性信息,annotation 會覆蓋元注解中的屬性信息voidpostProcess(@Nullable AnnotatedElement annotatedElement, Annotation annotation, T result);// 查詢所有元注解時有效,不管是否匹配都要執(zhí)行 process 方法booleanalwaysProcesses();// MergedAnnotationAttributesProcessor 查找所有的注解有效booleanaggregates();ListgetAggregatedResults();}

有兩個方法要特別關(guān)注:

process(@Nullable AnnotatedElement annotatedElement, Annotation annotation, int metaDepth)?有兩個作用:一是根據(jù)返回值來控制查找的流程;二是 MergedAnnotationAttributesProcessor 的 process 方法返回查找到的注解信息 AnnotationAttributes

postProcess(@Nullable AnnotatedElement element, Annotation annotation, AnnotationAttributes attributes)?只有 MergedAnnotationAttributesProcessor 有效,用來處理元注解屬性覆蓋。其中 annotation 表示當(dāng)前的注解,attributes 表示元注解的屬性信息,執(zhí)行時會用 annotation 覆蓋 attributes。

(2) 類圖

Processor 的有幾個實現(xiàn):SimpleAnnotationProcessor 相當(dāng)于一個簡單的適配器;AlwaysTrueBooleanAnnotationProcessor 的 process 方法永遠返回 TRUE;MergedAnnotationAttributesProcessor 用于處理元注解屬性覆蓋。

常用的方法對應(yīng)的 Processor 返回值如下:

getMetaAnnotationTypes?獲取指定注解上的所有元注解,所以 process 方法返回 null 且 alwaysProcesses=true

hasMetaAnnotationTypes?判斷指定的注解上是否有元注解,所以 process 方法返回 metaDepth > 0 ? Boolean.TRUE : CONTINUE,即當(dāng) metaDepth>0 表示有元注解就停止查詢

isAnnotated?是否存在指定的注解,所以只配匹配到 process 方法就返回 TRUE,使用 AlwaysTrueBooleanAnnotationProcessor

getMergedAnnotationAttributes?元注解會進行屬性覆蓋,使用 MergedAnnotationAttributesProcessor

getAllMergedAnnotations?查找所有的注解,使用 MergedAnnotationAttributesProcessor 且 aggregates=true

(3) MergedAnnotationAttributesProcessor

// AnnotationUtils#retrieveAnnotationAttributes 方法獲取當(dāng)前注解的屬性publicAnnotationAttributesprocess(@Nullable AnnotatedElement annotatedElement, Annotation annotation,intmetaDepth){returnAnnotationUtils.retrieveAnnotationAttributes(annotatedElement, annotation,this.classValuesAsString,this.nestedAnnotationsAsMap);}// annotation 為當(dāng)前注解,result 為元注解屬性信息,這個元注解的屬性信息是 process 方法提取的// annotation 會覆蓋元注解中的屬性信息publicvoidpostProcess(@Nullable AnnotatedElement element, Annotation annotation, AnnotationAttributes attributes){? ? annotation = AnnotationUtils.synthesizeAnnotation(annotation, element);? ? Class targetAnnotationType = attributes.annotationType();// 1. 已經(jīng)解析過的屬性,避免循環(huán)查找Set valuesAlreadyReplaced =newHashSet<>();for(Method attributeMethod : AnnotationUtils.getAttributeMethods(annotation.annotationType())) {? ? ? ? String attributeName = attributeMethod.getName();// 2. 查找 attributeMethod 屬性到底覆蓋了 targetAnnotationType 元注解的那個屬性String attributeOverrideName = AnnotationUtils.getAttributeOverrideName(attributeMethod, targetAnnotationType);// 3 顯示進行屬性覆蓋,通過 @AliasFor 注解if(attributeOverrideName !=null) {if(valuesAlreadyReplaced.contains(attributeOverrideName)) {continue;? ? ? ? ? ? }? ? ? ? ? ? List targetAttributeNames =newArrayList<>();? ? ? ? ? ? targetAttributeNames.add(attributeOverrideName);? ? ? ? ? ? valuesAlreadyReplaced.add(attributeOverrideName);// 確保所有的別名都要進行屬性覆蓋 (SPR-14069)List aliases = AnnotationUtils.getAttributeAliasMap(targetAnnotationType).get(attributeOverrideName);if(aliases !=null) {for(String alias : aliases) {if(!valuesAlreadyReplaced.contains(alias)) {? ? ? ? ? ? ? ? ? ? ? ? targetAttributeNames.add(alias);? ? ? ? ? ? ? ? ? ? ? ? valuesAlreadyReplaced.add(alias);? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? }? ? ? ? ? ? }// 將 targetAttributeNames 的屬性值設(shè)置為 attributeName 的值overrideAttributes(element, annotation, attributes, attributeName, targetAttributeNames);? ? ? ? }// 3.2 隱式的進行屬性覆蓋,只要字段與元注解的屬性字段一下致(規(guī)約)elseif(!AnnotationUtils.VALUE.equals(attributeName) && attributes.containsKey(attributeName)) {? ? ? ? ? ? overrideAttribute(element, annotation, attributes, attributeName, attributeName);? ? ? ? }? ? }}

2.2 searchWithGetSemantics

searchWithGetSemantics 有 7 個參數(shù):

element 注解標注的 AnnotatedElement

annotationTypes、annotationName、containerType 分別表示要查找的注解類型、注解名稱、以及可重復(fù)注解的容器對象

processor 后置的處理器,process 返回 null 繼續(xù)查找,否則停止查找。aggregates=true 時例外,因為此時查找全部的注解。

visited 已經(jīng)查找的元素,避免重復(fù)查找。

metaDepth 注解深度,普通注解為 0

// 用于查找 element 上的 annotationTypes、annotationName、containerType 類型注解// 返回后置處理器對查找后的注解 process 后的值privatestaticTsearchWithGetSemantics(AnnotatedElement element,? ? ? ? Set> annotationTypes, @Nullable String annotationName,? ? ? ? @Nullable Class containerType, Processor processor,? ? ? ? Set visited,intmetaDepth){if(visited.add(element)) {try{// 1. 本地注解查找 Start searching within locally declared annotationsList declaredAnnotations = Arrays.asList(AnnotationUtils.getDeclaredAnnotations(element));? ? ? ? ? ? T result = searchWithGetSemanticsInAnnotations(element, declaredAnnotations,? ? ? ? ? ? ? ? ? ? annotationTypes, annotationName, containerType, processor, visited, metaDepth);if(result !=null) {returnresult;? ? ? ? ? ? }// 2. @Inherited 類型查找if(elementinstanceofClass) {// otherwise getAnnotations doesn't return anything newClass superclass = ((Class) element).getSuperclass();if(superclass !=null&& superclass != Object.class) {? ? ? ? ? ? ? ? ? ? List inheritedAnnotations =newLinkedList<>();for(Annotation annotation : element.getAnnotations()) {if(!declaredAnnotations.contains(annotation)) {? ? ? ? ? ? ? ? ? ? ? ? ? ? inheritedAnnotations.add(annotation);? ? ? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? ? ? }// Continue searching within inherited annotationsresult = searchWithGetSemanticsInAnnotations(element, inheritedAnnotations,? ? ? ? ? ? ? ? ? ? ? ? ? ? annotationTypes, annotationName, containerType, processor, visited, metaDepth);if(result !=null) {returnresult;? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? }catch(Throwable ex) {? ? ? ? ? ? AnnotationUtils.handleIntrospectionFailure(element, ex);? ? ? ? }? ? }returnnull;}

searchWithGetSemanticsInAnnotations 真正用于在指定的注解集合 annotations 中查找指定的注解。

privatestaticTsearchWithGetSemanticsInAnnotations(@Nullable AnnotatedElement element,? ? ? ? List annotations, Set> annotationTypes,? ? ? ? @Nullable String annotationName, @Nullable Class containerType,? ? ? ? Processor processor, Set visited,intmetaDepth){// 1. 直接匹配 Search in annotationsfor(Annotation annotation : annotations) {? ? ? ? Class currentAnnotationType = annotation.annotationType();if(!AnnotationUtils.isInJavaLangAnnotationPackage(currentAnnotationType)) {// 1.1 注解類型或注解名相同if(annotationTypes.contains(currentAnnotationType) ||? ? ? ? ? ? ? ? ? ? currentAnnotationType.getName().equals(annotationName) ||? ? ? ? ? ? ? ? ? ? processor.alwaysProcesses()) {? ? ? ? ? ? ? ? T result = processor.process(element, annotation, metaDepth);if(result !=null) {if(processor.aggregates() && metaDepth ==0) {? ? ? ? ? ? ? ? ? ? ? ? processor.getAggregatedResults().add(result);? ? ? ? ? ? ? ? ? ? }else{returnresult;? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? }? ? ? ? ? ? }// 1.2 可重復(fù)注解,注意可重復(fù)注解不可能是組合注解 Repeatable annotations in container?elseif(currentAnnotationType == containerType) {for(Annotation contained : getRawAnnotationsFromContainer(element, annotation)) {? ? ? ? ? ? ? ? ? ? T result = processor.process(element, contained, metaDepth);if(result !=null) {? ? ? ? ? ? ? ? ? ? ? ? processor.getAggregatedResults().add(result);? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? }? ? }// 2. 遞歸查找元注解? Recursively search in meta-annotationsfor(Annotation annotation : annotations) {? ? ? ? Class currentAnnotationType = annotation.annotationType();if(!AnnotationUtils.hasPlainJavaAnnotationsOnly(currentAnnotationType)) {? ? ? ? ? ? T result = searchWithGetSemantics(currentAnnotationType, annotationTypes,? ? ? ? ? ? ? ? ? ? annotationName, containerType, processor, visited, metaDepth +1);if(result !=null) {// MergedAnnotationAttributesProcessor 用于元注解屬性覆蓋// annotation 表示當(dāng)前的注解,attributes 表示元注解的屬性信息,annotation 會覆蓋 attributes。processor.postProcess(element, annotation, result);if(processor.aggregates() && metaDepth ==0) {? ? ? ? ? ? ? ? ? ? processor.getAggregatedResults().add(result);? ? ? ? ? ? ? ? }else{returnresult;? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? }? ? }returnnull;}

參考:

《spring注解工具類AnnotatedElementUtils和AnnotationUtils》:https://blog.csdn.net/qq_22845447/article/details/83210559

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容