摘要:顧名思義,其主要作用是解析標簽。本例中沒有用到上述的注解,所以均為。繼續追蹤這行代碼的內部實現獲取的名稱調用的方法注冊過程見處理的別名,本例中沒有別名,不進入循環的具體內容有待研究,不展開。到此為止,已經被注冊到中。
接上篇 3 reader 注冊配置類
該 part 的起點:
public AnnotationConfigApplicationContext(Class>... annotatedClasses) { this(); register(annotatedClasses); // 3 reader 注冊配置類 refresh(); }
該行代碼會將 iocConfig bean 注冊到 reader 中
AnnotationConfigApplicationContext 的 register 方法:
//AnnotationConfigApplicationContext.class public void register(Class>... annotatedClasses) { //參數非空效驗 Assert.notEmpty(annotatedClasses, "At least one annotated class must be specified"); //調用 AnnotatedBeanDefinitionReader 的 register 方法 this.reader.register(annotatedClasses); }
上述方法主要是調用了 AnnotatedBeanDefinitionReader 的 register 方法:
//AnnotatedBeanDefinitionReader.class public void register(Class>... annotatedClasses) { for (Class> annotatedClass : annotatedClasses) { registerBean(annotatedClass); } }
上述方法循環調用了 AnnotatedBeanDefinitionReader 的 registerBean 方法:
//AnnotatedBeanDefinitionReader.class public void registerBean(Class> annotatedClass) { doRegisterBean(annotatedClass, null, null, null); }
上述方法調用了 AnnotatedBeanDefinitionReader 的 doRegisterBean 方法,這個方法比較長,需要重點關注:
//AnnotatedBeanDefinitionReader.class3.1void doRegisterBean(Class annotatedClass, @Nullable Supplier instanceSupplier, @Nullable String name, @Nullable Class extends Annotation>[] qualifiers, BeanDefinitionCustomizer... definitionCustomizers) { //用 BeanDefinition 包裝 iocConfig AnnotatedGenericBeanDefinition abd = new AnnotatedGenericBeanDefinition(annotatedClass); //此段代碼用于處理 Conditional 注解,在特定條件下阻斷 bean 的注冊 //本例中此處不會 return //3.1 if (this.conditionEvaluator.shouldSkip(abd.getMetadata())) { return; } //用來創建 bean 的 supplier,會替代掉 bean 本身的創建方法 //instanceSupplier 一般情況下為 null abd.setInstanceSupplier(instanceSupplier); //此行代碼處理 scope 注解,本例中 scope 是默認值 singleton //3.2 ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(abd); abd.setScope(scopeMetadata.getScopeName()); //bean name 在本例中為自動生成的 iocConfig //3.3 String beanName = (name != null ? name : this.beanNameGenerator.generateBeanName(abd, this.registry)); //特定注解解析,本例中均不做操作 //3.4 AnnotationConfigUtils.processCommonDefinitionAnnotations(abd); //本例中 qualifiers 傳入的是 null //3.5 if (qualifiers != null) { for (Class extends Annotation> qualifier : qualifiers) { if (Primary.class == qualifier) { abd.setPrimary(true); } else if (Lazy.class == qualifier) { abd.setLazyInit(true); } else { abd.addQualifier(new AutowireCandidateQualifier(qualifier)); } } } //本例中 definitionCustomizers 傳入的是 null //3.6 for (BeanDefinitionCustomizer customizer : definitionCustomizers) { customizer.customize(abd); } //用 BeanDefinitionHolder 包裝 BeanDefinition BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(abd,beanName); //此行代碼與動態代理和 scope 注解有關,但是在本案例中沒有做任何操作,只是返回了傳入的 definitionHolder //3.7 definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry); //iocConfig 注冊 // 3.8 BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, this.registry); }
看一下上述方法的片段:
if (this.conditionEvaluator.shouldSkip(abd.getMetadata())) { return; }
首先需要了解到 abd 的 getMetadata() 方法會獲取到 abd 中的 metadata 對象。
該對象是一個 StandardAnnotationMetadata 的實例化對象,在創建的時候會利用 java.Class 中的 api 獲取 bean 中所有的注解,并保存為一個數組:
//StandardAnnotationMetadata.class public StandardAnnotationMetadata(Class> introspectedClass, boolean nestedAnnotationsAsMap) { //此處的 introspectedClass 即為 bean 的 class //父類的構造器用于內部保存 bean 的 class super(introspectedClass); //獲取所有的注解 this.annotations = introspectedClass.getAnnotations(); //nestedAnnotationsAsMap 暫時用不上,按下不表 //nestedAnnotationsAsMap = true this.nestedAnnotationsAsMap = nestedAnnotationsAsMap; }
conditionEvaluator 是一個注解解析器,在 AnnotatedBeanDefinitionReader 創建的時候在其構造方法內被創建:
this.conditionEvaluator = new ConditionEvaluator(registry, environment, null);
追蹤 conditionEvaluator 的 shouldSkip(...) 方法:
//ConditionEvaluator.class public boolean shouldSkip(AnnotatedTypeMetadata metadata) { return shouldSkip(metadata, null); //調用自身的重載方法 } //ConditionEvaluator.class public boolean shouldSkip(@Nullable AnnotatedTypeMetadata metadata, @Nullable ConfigurationPhase phase) { //metadata 在此處不為 null //判斷 bean 是否使用了 Conditional 注解 if (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) { //如果 metadata為空或者 bean 沒有使用 Conditional 注解,就會返回 false return false; } //第一次調用該方法的時候,phase 為 null if (phase == null) { //下列源碼規整一下,其實是四個條件: //1 bean.metadata 是 AnnotationMetadata 或其子類 //2 bean 使用了 Configuration 注解 //3 bean 不是一個接口 //4 bean 使用了 Component、ComponentScan、Import、ImportResource 這四個注解之一,或者使用了 Bean 注解 //這四個條件中滿足 1、2 或者 1、3、4 就會進入 if 語句中 //請注意,對于 config bean 來說,只要使用了 Conditional 注解,必然會進入到語句中 if (metadata instanceof AnnotationMetadata && ConfigurationClassUtils.isConfigurationCandidate((AnnotationMetadata) metadata)) { return shouldSkip(metadata, ConfigurationPhase.PARSE_CONFIGURATION); } return shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN); } Listconditions = new ArrayList<>(); //getConditionClasses(metadata) 會獲取到 Conditional 注解中的 value 數組 for (String[] conditionClasses : getConditionClasses(metadata)) { //遍歷數組 for (String conditionClass : conditionClasses) { //利用反射獲取實例化數組內的 class Condition condition = getCondition(conditionClass, this.context.getClassLoader()); conditions.add(condition); //獲取所有的 canditionClass 并以次存入到列表中 } } //利用了 List 自帶的排序 api 進行排序 AnnotationAwareOrderComparator.sort(conditions); for (Condition condition : conditions) { ConfigurationPhase requiredPhase = null; if (condition instanceof ConfigurationCondition) { requiredPhase = ((ConfigurationCondition) condition).getConfigurationPhase(); } //對于 Conditional 內的 value 并非是實現 ConfigurationCondition 接口的 class,requiredPhase == null 必然為 true;對于實現了該接口的 class,requiredPhase == phase 必然為 true //所以要注意,如果 value class 的 matches(...) 方法返回 false,則會在此處阻斷 bean 的注冊 if ((requiredPhase == null || requiredPhase == phase) && !condition.matches(this.context, metadata)) { return true; } } //正常情況下,做完所有檢查工作之后還是會返回 false return false; }
可以看到這個方法其實是 Conditional 注解的解析器,對于未使用這個注解的 bean,直接就返回了,不會繼續往下走。
先來看一下 Conditional 的源碼:
@Target({ElementType.TYPE, ElementType.METHOD}) //可以標注在類和方法上方 @Retention(RetentionPolicy.RUNTIME) //注解生命周期 @Documented //javadoc 相關 public @interface Conditional { //class 數組 //這個數組里的值必須要是實現了 Condition 接口的類 //注意這個 value 沒有默認值,如果要使用該注解就必須要填入 Class extends Condition>[] value(); }
順便來看一下 Condition 接口:
public interface Condition { //這個方法會返回一個 boolean 值,如果為 true,則將繼承該接口的類注入到 Conditional 修飾的 bean 中 boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata); }
conditional 的具體內容有待研究,不展開。
3.2看下方代碼片段:
ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(abd); abd.setScope(scopeMetadata.getScopeName());
scopeMetadataResolver 是一個定義在 AnnotatedBeanDefinitionReader 里的 AnnotationScopeMetadataResolver 對象。顧名思義,其主要作用是解析 scope 標簽。
先來看一下 Scope 注解的定義源碼:
@Target({ElementType.TYPE, ElementType.METHOD}) //可以標注在類和方法上方 @Retention(RetentionPolicy.RUNTIME) //注解生命周期 @Documented //javadoc 相關 public @interface Scope { //value 是平時開發中最常用到的 scope 屬性,用來設置是否是單例模式 //在處理注解的時候 value 屬性會被轉化成 scopeName 屬性來看待 //所以兩個屬性其實是一樣的 String value() default ""; String scopeName() default ""; //代理模式設置,默認為無代理 ScopedProxyMode proxyMode() default ScopedProxyMode.DEFAULT; }
ScopedProxyMode 是一個枚舉類,沒有任何處理業務邏輯的代碼,一同放在這里:
public enum ScopedProxyMode { DEFAULT, //不使用代理,default 和 no 是等價的 NO, INTERFACES, //使用 jdk 自帶的動態代理 api 進行創建 TARGET_CLASS; //target-class 模式,需要使用 cglib 進行 bean 的創建 }
AnnotationScopeMetadataResolver 的 resolveScopeMetadata(...) 方法具體實現如下:
//AnnotationScopeMetadataResolver.class public ScopeMetadata resolveScopeMetadata(BeanDefinition definition) { //創建一個 metadata 對象用于返回 ScopeMetadata metadata = new ScopeMetadata(); if (definition instanceof AnnotatedBeanDefinition) { AnnotatedBeanDefinition annDef = (AnnotatedBeanDefinition) definition; //從 bean 的注解里尋找 scope 這個注解 AnnotationAttributes attributes = AnnotationConfigUtils.attributesFor( annDef.getMetadata(), this.scopeAnnotationType); //如果 bean 確實是用了 scope 注解 if (attributes != null) { metadata.setScopeName(attributes.getString("value")); //存入 scope 的 value 屬性值 //獲取 proxyMode 屬性值 ScopedProxyMode proxyMode = attributes.getEnum("proxyMode"); //default 和 no 是等同的,默認會轉化成 no 進行處理 if (proxyMode == ScopedProxyMode.DEFAULT) { //this.defaultProxyMode = ScopedProxyMode.NO proxyMode = this.defaultProxyMode; } metadata.setScopedProxyMode(proxyMode); //存入 scope 的 proxyMode 屬性值 } } //沒有使用 scope 的情況下會返回一個新建的 metadata return metadata; }
annDef.getMetadata() 會獲取到一個 AnnotationMetadata 對象,里面包含了 bean 的所有注解信息。
scopeAnnotationType 是一個定義在 AnnotationScopeMetadataResolver 里的 Class 對象:
protected Class extends Annotation> scopeAnnotationType = Scope.class;
可見 AnnotationConfigUtils 的 attributesFor(...) 就是去注解集里查找 scope 注解,并且封裝成一個 AnnotationAttributes 返回。
AnnotationAttributes 是 Spring 用來存儲注解所定義的一種數據結構,本質上是一個 LinkedHashMap。
再回到本小節最上方的代碼:
abd.setScope(scopeMetadata.getScopeName());
最后其實 BeanDefinition 只接收了 scopeName,而沒有接收 proxyMode。proxyMode 屬性會在后面代碼中用到。
3.3看下方代碼片段:
String beanName = (name != null ? name : this.beanNameGenerator.generateBeanName(abd, this.registry));
beanNameGenerator 是一個定義在 AnnotatedBeanDefinitionReader 里的 AnnotationBeanNameGenerator 對象,顧名思義用來生成 bean 的名稱:
private BeanNameGenerator beanNameGenerator = new AnnotationBeanNameGenerator();
追蹤一下 generateBeanName(...) 方法:
//AnnotationBeanNameGenerator.class public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) { if (definition instanceof AnnotatedBeanDefinition) { //determineBeanNameFromAnnotation(...) 方法會從 bean 的所有注解里去遍歷搜尋 bean 名稱 String beanName = determineBeanNameFromAnnotation((AnnotatedBeanDefinition) definition); //如果此處的 beanName 非空,則表明在注解里找到了定義的 bean 名稱 if (StringUtils.hasText(beanName)) { // Explicit bean name found. return beanName; } } //沒有在前面 return,證明 bean 沒有被設置名稱,則在此處默認生成一個名稱 return buildDefaultBeanName(definition, registry); }
看一眼 buildDefaultBeanName(...) 方法:
//AnnotationBeanNameGenerator.class protected String buildDefaultBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) { return buildDefaultBeanName(definition); }
其實這個方法只用到了 definition,而沒有使用到傳入的 registry。
繼續追蹤代碼實現:
//AnnotationBeanNameGenerator.class protected String buildDefaultBeanName(BeanDefinition definition) { //該處返回的是 bean 的整個 class 路徑和名稱 String beanClassName = definition.getBeanClassName(); //beanClassName 非空判斷 Assert.state(beanClassName != null, "No bean class name set"); //截掉 class 的路徑,只取 class 名稱 String shortClassName = ClassUtils.getShortName(beanClassName); //將首字母小寫并返回 return Introspector.decapitalize(shortClassName); }3.4
看下方代碼實現:
AnnotationConfigUtils.processCommonDefinitionAnnotations(abd);
追蹤這行代碼:
//AnnotationConfigUtils.class public static void processCommonDefinitionAnnotations(AnnotatedBeanDefinition abd) { //調用重載方法 processCommonDefinitionAnnotations(abd, abd.getMetadata()); } //AnnotationConfigUtils.class static void processCommonDefinitionAnnotations(AnnotatedBeanDefinition abd, AnnotatedTypeMetadata metadata) { //檢查 lazy 注解 AnnotationAttributes lazy = attributesFor(metadata, Lazy.class); if (lazy != null) { abd.setLazyInit(lazy.getBoolean("value")); }else if (abd.getMetadata() != metadata) { //這里還有一個補充檢查,如果傳入的 metadata 不是 abd 內部的 metadata的話,還會繼續進來判斷一次 //在本例中沒什么必要 lazy = attributesFor(abd.getMetadata(), Lazy.class); if (lazy != null) { abd.setLazyInit(lazy.getBoolean("value")); } } //檢查 primary 注解 if (metadata.isAnnotated(Primary.class.getName())) { abd.setPrimary(true); } //檢查 dependsOn 注解 AnnotationAttributes dependsOn = attributesFor(metadata, DependsOn.class); if (dependsOn != null) { abd.setDependsOn(dependsOn.getStringArray("value")); } //檢查 role 注解 AnnotationAttributes role = attributesFor(metadata, Role.class); if (role != null) { abd.setRole(role.getNumber("value").intValue()); } //檢查 description 注解 AnnotationAttributes description = attributesFor(metadata, Description.class); if (description != null) { abd.setDescription(description.getString("value")); } }
其實上述代碼的主體都是類似的,步驟都是嘗試從 metadata 中獲取特定注解,如果獲取到了就將其作為一個屬性值 set 進 abd 中。
這里需要強調 abd 就是要注冊的 bean 的 BeanDefinition 包裝對象。
本例中沒有用到上述的注解,所以均為 null。
3.5看下方代碼:
if (qualifiers != null) { //在 qualifiers 不為 null 的情況下會遍歷該集合,并將當中的所有的元素解析出來,進行業務操作 for (Class extends Annotation> qualifier : qualifiers) { if (Primary.class == qualifier) { abd.setPrimary(true); }else if (Lazy.class == qualifier) { abd.setLazyInit(true); }else { abd.addQualifier(new AutowireCandidateQualifier(qualifier)); } } }
上述代碼是針對 qualifier 注解的解析,和 3.4 很類似。
AutowireCandidateQualifier 是注解的包裝類,儲存了一個特定注解的名字和 value。abd 的 addQualifier(...) 方法會將這個創建出來的包裝類存儲到一個 map 對象里。
3.6看下方代碼:
for (BeanDefinitionCustomizer customizer : definitionCustomizers) { customizer.customize(abd); }
這段代碼是 Spring5 中新加入的。根據注釋,官方應該是留下這個接口用以讓開發者通過 lambda 表達式去定義 bean。
3.7看下方代碼:
definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
先來追蹤 applyScopedProxyMode(...) 方法:
//AnnotationConfigUtils.class static BeanDefinitionHolder applyScopedProxyMode( ScopeMetadata metadata, BeanDefinitionHolder definition, BeanDefinitionRegistry registry) { //先判斷 scope 注解的使用 ScopedProxyMode scopedProxyMode = metadata.getScopedProxyMode(); if (scopedProxyMode.equals(ScopedProxyMode.NO)) { return definition; } //判斷具體使用哪種模式 boolean proxyTargetClass = scopedProxyMode.equals(ScopedProxyMode.TARGET_CLASS); //此行代碼會連向 spring-aop 包下的類來處理 return ScopedProxyCreator.createScopedProxy(definition, registry, proxyTargetClass); }
注意,此處傳入的 metadata 是上述 3.2 小節中新建出來并返回的對象:
ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(abd);
對于一般沒有使用 scope 注解或者 scope 注解為默認的 bean,此時 scopedProxyMode 是等于 ScopedProxyMode.NO 的。
對于 scopedProxyMode 不為 NO 的 bean,均為需要使用動態代理進行創建的對象,區別只是使用 jdk 自帶的 api 還是使用 cglib 包。
追蹤一下 ScopedProxyCreator 的 createScopedProxy(...) 方法:
//ScopedProxyCreator.class public static BeanDefinitionHolder createScopedProxy( BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry, boolean proxyTargetClass) { return ScopedProxyUtils.createScopedProxy(definitionHolder, registry, proxyTargetClass); }
繼續追蹤:
//ScopedProxyUtils.class public static BeanDefinitionHolder createScopedProxy(BeanDefinitionHolder definition, BeanDefinitionRegistry registry, boolean proxyTargetClass) { //bean 的名稱 String originalBeanName = definition.getBeanName(); //bean 的 BeanDefinition 包裝類 BeanDefinition targetDefinition = definition.getBeanDefinition(); //在 bean 的名稱前面加上字符串 "scopedTarget." ,拼成 targetBeanName //比如 scopedTarget.iocConfig String targetBeanName = getTargetBeanName(originalBeanName); //以下代碼用來組裝一個動態代理的工廠 bean,這個 bean 是用來動態代理的主體 RootBeanDefinition proxyDefinition = new RootBeanDefinition(ScopedProxyFactoryBean.class); proxyDefinition.setDecoratedDefinition(new BeanDefinitionHolder(targetDefinition, targetBeanName)); proxyDefinition.setOriginatingBeanDefinition(targetDefinition); proxyDefinition.setSource(definition.getSource()); proxyDefinition.setRole(targetDefinition.getRole()); proxyDefinition.getPropertyValues().add("targetBeanName", targetBeanName); if (proxyTargetClass) { targetDefinition.setAttribute(AutoProxyUtils.PRESERVE_TARGET_CLASS_ATTRIBUTE, Boolean.TRUE); }else { proxyDefinition.getPropertyValues().add("proxyTargetClass", Boolean.FALSE); } proxyDefinition.setAutowireCandidate(targetDefinition.isAutowireCandidate()); proxyDefinition.setPrimary(targetDefinition.isPrimary()); if (targetDefinition instanceof AbstractBeanDefinition) { proxyDefinition.copyQualifiersFrom((AbstractBeanDefinition) targetDefinition); } targetDefinition.setAutowireCandidate(false); targetDefinition.setPrimary(false); //此處的 targetDefinition 是傳入的 bean 的包裝類 //這一步會提前將該 bean 進行注冊 //注冊過程見 2.2 registry.registerBeanDefinition(targetBeanName, targetDefinition); //返回的其實是動態代理所需要的工廠 bean return new BeanDefinitionHolder(proxyDefinition, originalBeanName, definition.getAliases()); }
scope 的具體內容有待研究,不展開。
3.8在上例代碼中的這一行中:
BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, this.registry);
傳入的 definitionHolder 就是 iocConfig bean 的包裝對象;而傳入的 registry 就是在 ApplicationContext 中實例化的 BeanFactory,此處具體而言就是DefaultListableBeanFactory。
繼續追蹤這行代碼的內部實現:
//BeanDefinitionReaderUtils.class public static void registerBeanDefinition( BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry) throws BeanDefinitionStoreException { //獲取bean的名稱 String beanName = definitionHolder.getBeanName(); //調用 AnnotationConfigApplicationContext 的 registerBeanDefinition 方法 //注冊過程見 2.2 registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition()); //處理bean的別名,本例中沒有別名,不進入循環 String[] aliases = definitionHolder.getAliases(); if (aliases != null) { for (String alias : aliases) { registry.registerAlias(beanName, alias); } } }
alias 的具體內容有待研究,不展開。
到此為止,iocConfig bean 已經被注冊到 bean factory 中。
To Be Continued ...文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/72747.html
摘要:主要過程為調用自身的另一個有參構造器此處的即為本身此處的最終返回一個調用無參構造器創建出來的對象參數非空效驗保存創建一個用于注解解析器,后面會用到注冊需要用到的顧名思義,即為所處的環境,包括配置的讀取等。 零 前期準備 0 FBI WARNING 文章異常啰嗦且繞彎。 1 版本 spring版本 : spring 5.1.2.RELEASE IDE : idea 2018.3 2 Be...
摘要:為何重拾使用了多年,但是對其底層的一些實現還是一知半解,一些概念比較模糊故決定重新拾起,加深對的認識。小結是在完成創建后對其進行后置處理的接口是在完成實例化對其進行的后置處理接口是框架底層的核心接口,其提供了創建,獲取等核心功能。 為何重拾 使用了 Spring 多年,但是對其底層的一些實現還是一知半解,一些概念比較模糊;故決定重新拾起,加深對 Spring 的認識。 重拾計劃 spr...
摘要:何為簡單點來定義就是切面,是一種編程范式。定義一個切面的載體定義一個切點定義一個為,并指定對應的切點一個注冊配置類,啟動容器,初始化時期獲取對象,獲取對象時期,并進行打印好了,這樣我們整體的代理就已經完成。 問題:Spring AOP代理中的運行時期,是在初始化時期織入還是獲取對象時期織入? 織入就是代理的過程,指目標對象進行封裝轉換成代理,實現了代理,就可以運用各種代理的場景模式。 ...
摘要:前言以下源碼基于版本解析。實現源碼分析對于的實現,總結來說就是定位加載和注冊。定位就是需要定位配置文件的位置,加載就是將配置文件加載進內存注冊就是通過解析配置文件注冊。下面我們從其中的一種使用的方式一步一步的分析的實現源碼。 前言 以下源碼基于Spring 5.0.2版本解析。 什么是IOC容器? 容器,顧名思義可以用來容納一切事物。我們平常所說的Spring IOC容器就是一個可以容...
摘要:概述約定大于配置的功力讓我們如沐春風,在我之前寫的文章從到也對比過和這兩個框架,不過最終以超高的代碼信噪比和易上手性讓我們映像頗深。至于,我想在非時代大家應該不陌生吧,作用是配置容器,也即形式的容器的配置類所使用。 showImg(https://segmentfault.com/img/remote/1460000015822144); 概 述 SpringBoot 約定大于配置...
閱讀 1897·2021-11-24 09:39
閱讀 2134·2021-09-22 15:50
閱讀 1991·2021-09-22 14:57
閱讀 699·2021-07-28 00:13
閱讀 1064·2019-08-30 15:54
閱讀 2356·2019-08-30 15:52
閱讀 2686·2019-08-30 13:07
閱讀 3787·2019-08-30 11:27