Spring的@Configuration使用與原理
@Configuration的使用
@Configuration是一個(gè)被@Component注解修飾的注解,先來看一個(gè)現(xiàn)象:
package com.morris.spring.config; import com.morris.spring.entity.Message; import com.morris.spring.entity.Order; import com.morris.spring.entity.User; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.stereotype.Component; //@Component @Configuration public class UserConfig { @Bean public User user() { System.out.println("create user"); return new User(); } @Bean public Message message() { return new Message(user()); } }
現(xiàn)象:
- UserConfig類上面使用@Configuration,user()只會(huì)被調(diào)用一次。
- UserConfig類上面使用@Component,user()會(huì)被調(diào)用兩次,也就是會(huì)創(chuàng)建兩個(gè)user對(duì)象,破壞了Spring中Bean的單例。
為什么會(huì)出現(xiàn)上面這種現(xiàn)象呢?初步猜測(cè)使用@Configuration會(huì)使用動(dòng)態(tài)代理,下面從源碼來分析原理。
@Configuration的源碼分析
標(biāo)識(shí)@Configuration為full
在BeanDefinition會(huì)有一個(gè)屬性來標(biāo)識(shí)是否有@Configuration注解:
org.springframework.context.annotation.ConfigurationClassUtils#checkConfigurationClassCandidate
public static boolean checkConfigurationClassCandidate( BeanDefinition beanDef, MetadataReaderFactory metadataReaderFactory) { ... ... Map<String, Object> config = metadata.getAnnotationAttributes(Configuration.class.getName()); if (config != null && !Boolean.FALSE.equals(config.get("proxyBeanMethods"))) { // 這里對(duì)@Configuration做了特殊處理,往BD中設(shè)置了屬性full beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_FULL); } else if (config != null || isConfigurationCandidate(metadata)) { // 帶有@Component、@ComponentScan、@Import、@ImportResource、@Bean注解的BD,設(shè)置屬性lite beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_LITE); } else { return false; } // It's a full or lite configuration candidate... Let's determine the order value, if any. Integer order = getOrder(metadata); if (order != null) { beanDef.setAttribute(ORDER_ATTRIBUTE, order); } return true; }
@Bean注解的收集
org.springframework.context.annotation.ConfigurationClassParser#doProcessConfigurationClass
... Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass); for (MethodMetadata methodMetadata : beanMethods) { configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass)); } ...
將@Bean的方法封裝為MethodMetadata對(duì)象放入到ConfigClass中。
解析BeanMethod注入BeanDefinition
org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader#loadBeanDefinitionsForBeanMethod
private void loadBeanDefinitionsForBeanMethod(BeanMethod beanMethod) { ConfigurationClass configClass = beanMethod.getConfigurationClass(); MethodMetadata metadata = beanMethod.getMetadata(); String methodName = metadata.getMethodName(); ... ... if (metadata.isStatic()) { // static @Bean method // 靜態(tài) @Bean method if (configClass.getMetadata() instanceof StandardAnnotationMetadata) { beanDef.setBeanClass(((StandardAnnotationMetadata) configClass.getMetadata()).getIntrospectedClass()); } else { beanDef.setBeanClassName(configClass.getMetadata().getClassName()); } beanDef.setUniqueFactoryMethodName(methodName); } else { // 實(shí)例 @Bean method // instance @Bean method beanDef.setFactoryBeanName(configClass.getBeanName()); beanDef.setUniqueFactoryMethodName(methodName); } ... ... this.registry.registerBeanDefinition(beanName, beanDefToRegister); }
從上面的源碼可以發(fā)現(xiàn)@Bean注解注入的BeanDefinition都會(huì)有個(gè)FactoryMethodName,也就是后面實(shí)例化時(shí)將會(huì)使用factory-method流程進(jìn)行實(shí)例化。
替換BeanClass為CGlib代理類
org.springframework.context.annotation.ConfigurationClassPostProcessor#postProcessBeanFactory
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) { int factoryId = System.identityHashCode(beanFactory); if (this.factoriesPostProcessed.contains(factoryId)) { throw new IllegalStateException( "postProcessBeanFactory already called on this post-processor against " + beanFactory); } this.factoriesPostProcessed.add(factoryId); if (!this.registriesPostProcessed.contains(factoryId)) { // BeanDefinitionRegistryPostProcessor hook apparently not supported... // Simply call processConfigurationClasses lazily at this point then. processConfigBeanDefinitions((BeanDefinitionRegistry) beanFactory); } // 對(duì)@Configuration注解的類做增強(qiáng),CGlib代理 enhanceConfigurationClasses(beanFactory); beanFactory.addBeanPostProcessor(new ImportAwareBeanPostProcessor(beanFactory)); } public void enhanceConfigurationClasses(ConfigurableListableBeanFactory beanFactory) { Map<String, AbstractBeanDefinition> configBeanDefs = new LinkedHashMap<>(); // 收集所有帶有@Configuration的BD,也就是有個(gè)attribute為full的屬性 for (String beanName : beanFactory.getBeanDefinitionNames()) { BeanDefinition beanDef = beanFactory.getBeanDefinition(beanName); Object configClassAttr = beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE); MethodMetadata methodMetadata = null; if (beanDef instanceof AnnotatedBeanDefinition) { methodMetadata = ((AnnotatedBeanDefinition) beanDef).getFactoryMethodMetadata(); } if ((configClassAttr != null || methodMetadata != null) && beanDef instanceof AbstractBeanDefinition) { // Configuration class (full or lite) or a configuration-derived @Bean method // -> resolve bean class at this point... AbstractBeanDefinition abd = (AbstractBeanDefinition) beanDef; if (!abd.hasBeanClass()) { try { abd.resolveBeanClass(this.beanClassLoader); } catch (Throwable ex) { throw new IllegalStateException( "Cannot load configuration class: " + beanDef.getBeanClassName(), ex); } } } if (ConfigurationClassUtils.CONFIGURATION_CLASS_FULL.equals(configClassAttr)) { if (!(beanDef instanceof AbstractBeanDefinition)) { throw new BeanDefinitionStoreException("Cannot enhance @Configuration bean definition '" + beanName + "' since it is not stored in an AbstractBeanDefinition subclass"); } else if (logger.isInfoEnabled() && beanFactory.containsSingleton(beanName)) { logger.info("Cannot enhance @Configuration bean definition '" + beanName + "' since its singleton instance has been created too early. The typical cause " + "is a non-static @Bean method with a BeanDefinitionRegistryPostProcessor " + "return type: Consider declaring such methods as 'static'."); } configBeanDefs.put(beanName, (AbstractBeanDefinition) beanDef); } } if (configBeanDefs.isEmpty()) { // nothing to enhance -> return immediately return; } ConfigurationClassEnhancer enhancer = new ConfigurationClassEnhancer(); // 遍歷所有帶有@Configuration的BD for (Map.Entry<String, AbstractBeanDefinition> entry : configBeanDefs.entrySet()) { AbstractBeanDefinition beanDef = entry.getValue(); // If a @Configuration class gets proxied, always proxy the target class beanDef.setAttribute(AutoProxyUtils.PRESERVE_TARGET_CLASS_ATTRIBUTE, Boolean.TRUE); // Set enhanced subclass of the user-specified bean class Class<?> configClass = beanDef.getBeanClass(); // 生成CGlib代理類 Class<?> enhancedClass = enhancer.enhance(configClass, this.beanClassLoader); if (configClass != enhancedClass) { if (logger.isTraceEnabled()) { logger.trace(String.format("Replacing bean definition '%s' existing class '%s' with " + "enhanced class '%s'", entry.getKey(), configClass.getName(), enhancedClass.getName())); } beanDef.setBeanClass(enhancedClass); } } }
org.springframework.context.annotation.ConfigurationClassEnhancer#enhance
public Class<?> enhance(Class<?> configClass, @Nullable ClassLoader classLoader) { if (EnhancedConfiguration.class.isAssignableFrom(configClass)) { if (logger.isDebugEnabled()) { logger.debug(String.format("Ignoring request to enhance %s as it has " + "already been enhanced. This usually indicates that more than one " + "ConfigurationClassPostProcessor has been registered (e.g. via " + "<context:annotation-config>). This is harmless, but you may " + "want check your configuration and remove one CCPP if possible", configClass.getName())); } return configClass; } // 創(chuàng)建代理類 Class<?> enhancedClass = createClass(newEnhancer(configClass, classLoader)); if (logger.isTraceEnabled()) { logger.trace(String.format("Successfully enhanced %s; enhanced class name is: %s", configClass.getName(), enhancedClass.getName())); } return enhancedClass; } private Enhancer newEnhancer(Class<?> configSuperClass, @Nullable ClassLoader classLoader) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(configSuperClass); enhancer.setInterfaces(new Class<?>[] {EnhancedConfiguration.class}); enhancer.setUseFactory(false); enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE); enhancer.setStrategy(new BeanFactoryAwareGeneratorStrategy(classLoader)); // 這里會(huì)注入幾個(gè)Callback enhancer.setCallbackFilter(CALLBACK_FILTER); enhancer.setCallbackTypes(CALLBACK_FILTER.getCallbackTypes()); return enhancer; } private Class<?> createClass(Enhancer enhancer) { Class<?> subclass = enhancer.createClass(); // Registering callbacks statically (as opposed to thread-local) // is critical for usage in an OSGi environment (SPR-5932)... Enhancer.registerStaticCallbacks(subclass, CALLBACKS); return subclass; } private static final Callback[] CALLBACKS = new Callback[] { new BeanMethodInterceptor(), // 重點(diǎn) new BeanFactoryAwareMethodInterceptor(), NoOp.INSTANCE }; private static final ConditionalCallbackFilter CALLBACK_FILTER = new ConditionalCallbackFilter(CALLBACKS);
上面一堆代碼就是為了給@Configuration注解的類生成CGlib代理類,所以關(guān)鍵就是這個(gè)類長什么樣,是怎么做增強(qiáng)的,重點(diǎn)就在上面的BeanMethodInterceptor中。
@Bean方法的調(diào)用
org.springframework.context.annotation.ConfigurationClassEnhancer.BeanMethodInterceptor#intercept
public Object intercept(Object enhancedConfigInstance, Method beanMethod, Object[] beanMethodArgs, MethodProxy cglibMethodProxy) throws Throwable { ConfigurableBeanFactory beanFactory = getBeanFactory(enhancedConfigInstance); String beanName = BeanAnnotationHelper.determineBeanNameFor(beanMethod); // Determine whether this bean is a scoped-proxy if (BeanAnnotationHelper.isScopedProxy(beanMethod)) { String scopedBeanName = ScopedProxyCreator.getTargetBeanName(beanName); if (beanFactory.isCurrentlyInCreation(scopedBeanName)) { beanName = scopedBeanName; } } // To handle the case of an inter-bean method reference, we must explicitly check the // container for already cached instances. // First, check to see if the requested bean is a FactoryBean. If so, create a subclass // proxy that intercepts calls to getObject() and returns any cached bean instance. // This ensures that the semantics of calling a FactoryBean from within @Bean methods // is the same as that of referring to a FactoryBean within XML. See SPR-6602. if (factoryContainsBean(beanFactory, BeanFactory.FACTORY_BEAN_PREFIX + beanName) && factoryContainsBean(beanFactory, beanName)) { Object factoryBean = beanFactory.getBean(BeanFactory.FACTORY_BEAN_PREFIX + beanName); if (factoryBean instanceof ScopedProxyFactoryBean) { // Scoped proxy factory beans are a special case and should not be further proxied } else { // It is a candidate FactoryBean - go ahead with enhancement return enhanceFactoryBean(factoryBean, beanMethod.getReturnType(), beanFactory, beanName); } } if (isCurrentlyInvokedFactoryMethod(beanMethod)) { // The factory is calling the bean method in order to instantiate and register the bean // (i.e. via a getBean() call) -> invoke the super implementation of the method to actually // create the bean instance. if (logger.isInfoEnabled() && BeanFactoryPostProcessor.class.isAssignableFrom(beanMethod.getReturnType())) { logger.info(String.format("@Bean method %s.%s is non-static and returns an object " + "assignable to Spring's BeanFactoryPostProcessor interface. This will " + "result in a failure to process annotations such as @Autowired, " + "@Resource and @PostConstruct within the method's declaring " + "@Configuration class. Add the 'static' modifier to this method to avoid " + "these container lifecycle issues; see @Bean javadoc for complete details.", beanMethod.getDeclaringClass().getSimpleName(), beanMethod.getName())); } // 第一次調(diào)用走這 return cglibMethodProxy.invokeSuper(enhancedConfigInstance, beanMethodArgs); } // 第二次調(diào)用走這 return resolveBeanReference(beanMethod, beanMethodArgs, beanFactory, beanName); }
isCurrentlyInvokedFactoryMethod何時(shí)返回true?
private boolean isCurrentlyInvokedFactoryMethod(Method method) { // SimpleInstantiationStrategy這個(gè)值哪里設(shè)置進(jìn)去的 /** * @see SimpleInstantiationStrategy#instantiate(org.springframework.beans.factory.support.RootBeanDefinition, java.lang.String, org.springframework.beans.factory.BeanFactory, java.lang.Object, java.lang.reflect.Method, java.lang.Object...) */ Method currentlyInvoked = SimpleInstantiationStrategy.getCurrentlyInvokedFactoryMethod(); return (currentlyInvoked != null && method.getName().equals(currentlyInvoked.getName()) && Arrays.equals(method.getParameterTypes(), currentlyInvoked.getParameterTypes())); }
第一次調(diào)用發(fā)生在Spring容器啟動(dòng)時(shí)實(shí)例化user對(duì)象,@Bean注解注入的對(duì)象會(huì)通過factory-method進(jìn)行實(shí)例化,此處是UserConfig.user(),調(diào)用此方法前會(huì)往ThreadLocal中放入當(dāng)前的方法:
org.springframework.beans.factory.support.SimpleInstantiationStrategy#instantiate
... try { // 放入ThreadLocal,@Bean走cglib代理會(huì)使用這個(gè)ThreadLocal currentlyInvokedFactoryMethod.set(factoryMethod); Object result = factoryMethod.invoke(factoryBean, args); if (result == null) { result = new NullBean(); } return result; } ...
然后再反射調(diào)用factoryMethod,此時(shí)調(diào)用的是代理對(duì)象,進(jìn)入BeanMethodInterceptor#intercept,然后判斷當(dāng)前ThreadLocal中有FactoryMethod,就會(huì)調(diào)用父類也就是目標(biāo)類的目標(biāo)方法,此處是UserConfig.user()創(chuàng)建對(duì)象。
第二次進(jìn)入BeanMethodInterceptor#intercept時(shí),是實(shí)例化Message,此時(shí)雖然ThreadLocal中有FactoryMethod,但是這個(gè)Method是message(),而不是user(),所以會(huì)調(diào)用下面的方法從BeanFactory中獲得對(duì)象,而不是調(diào)用FactoryMethod創(chuàng)建對(duì)象。
org.springframework.context.annotation.ConfigurationClassEnhancer.BeanMethodInterceptor#resolveBeanReference
private Object resolveBeanReference(Method beanMethod, Object[] beanMethodArgs, ConfigurableBeanFactory beanFactory, String beanName) { // The user (i.e. not the factory) is requesting this bean through a call to // the bean method, direct or indirect. The bean may have already been marked // as 'in creation' in certain autowiring scenarios; if so, temporarily set // the in-creation status to false in order to avoid an exception. boolean alreadyInCreation = beanFactory.isCurrentlyInCreation(beanName); try { if (alreadyInCreation) { beanFactory.setCurrentlyInCreation(beanName, false); } boolean useArgs = !ObjectUtils.isEmpty(beanMethodArgs); if (useArgs && beanFactory.isSingleton(beanName)) { // Stubbed null arguments just for reference purposes, // expecting them to be autowired for regular singleton references? // A safe assumption since @Bean singleton arguments cannot be optional... for (Object arg : beanMethodArgs) { if (arg == null) { useArgs = false; break; } } } // 第二次調(diào)用直接從BeanFactory中取 Object beanInstance = (useArgs ? beanFactory.getBean(beanName, beanMethodArgs) : beanFactory.getBean(beanName)); if (!ClassUtils.isAssignableValue(beanMethod.getReturnType(), beanInstance)) { // Detect package-protected NullBean instance through equals(null) check if (beanInstance.equals(null)) { if (logger.isDebugEnabled()) { logger.debug(String.format("@Bean method %s.%s called as bean reference " + "for type [%s] returned null bean; resolving to null value.", beanMethod.getDeclaringClass().getSimpleName(), beanMethod.getName(), beanMethod.getReturnType().getName())); } beanInstance = null; } else { String msg = String.format("@Bean method %s.%s called as bean reference " + "for type [%s] but overridden by non-compatible bean instance of type [%s].", beanMethod.getDeclaringClass().getSimpleName(), beanMethod.getName(), beanMethod.getReturnType().getName(), beanInstance.getClass().getName()); try { BeanDefinition beanDefinition = beanFactory.getMergedBeanDefinition(beanName); msg += " Overriding bean of same name declared in: " + beanDefinition.getResourceDescription(); } catch (NoSuchBeanDefinitionException ex) { // Ignore - simply no detailed message then. } throw new IllegalStateException(msg); } } Method currentlyInvoked = SimpleInstantiationStrategy.getCurrentlyInvokedFactoryMethod(); if (currentlyInvoked != null) { String outerBeanName = BeanAnnotationHelper.determineBeanNameFor(currentlyInvoked); beanFactory.registerDependentBean(beanName, outerBeanName); } return beanInstance; } finally { if (alreadyInCreation) { beanFactory.setCurrentlyInCreation(beanName, true); } } }
如果將user()方法改為static,那么又會(huì)創(chuàng)建兩個(gè)user對(duì)象,因?yàn)閟pring并沒有為@Configuration修飾的類創(chuàng)建代理,只是為@Configuration修飾的類的對(duì)象創(chuàng)建了代理。
FactoryBean的代理
如果@Bean注入的是一個(gè)FactoryBean,Spring會(huì)為FactoryBean生成代理,保證getObject()方法只會(huì)被調(diào)用一次。
package com.morris.spring.config; import com.morris.spring.entity.Message; import com.morris.spring.entity.UserFactoryBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class UserFactoryBeanConfig { @Bean public UserFactoryBean userFactoryBean() { System.out.println("userFactoryBean"); return new UserFactoryBean(); } @Bean public Message message() throws Exception { return new Message(userFactoryBean().getObject()); } }
關(guān)鍵源碼如下: org.springframework.context.annotation.ConfigurationClassEnhancer.BeanMethodInterceptor#intercept
... if (factoryContainsBean(beanFactory, BeanFactory.FACTORY_BEAN_PREFIX + beanName) && factoryContainsBean(beanFactory, beanName)) { Object factoryBean = beanFactory.getBean(BeanFactory.FACTORY_BEAN_PREFIX + beanName); if (factoryBean instanceof ScopedProxyFactoryBean) { // Scoped proxy factory beans are a special case and should not be further proxied } else { // It is a candidate FactoryBean - go ahead with enhancement // 為FactoryBean創(chuàng)建代理 return enhanceFactoryBean(factoryBean, beanMethod.getReturnType(), beanFactory, beanName); } } ...
org.springframework.context.annotation.ConfigurationClassEnhancer.BeanMethodInterceptor#enhanceFactoryBean
private Object enhanceFactoryBean(final Object factoryBean, Class<?> exposedType, final ConfigurableBeanFactory beanFactory, final String beanName) { try { Class<?> clazz = factoryBean.getClass(); boolean finalClass = Modifier.isFinal(clazz.getModifiers()); boolean finalMethod = Modifier.isFinal(clazz.getMethod("getObject").getModifiers()); if (finalClass || finalMethod) { if (exposedType.isInterface()) { if (logger.isTraceEnabled()) { logger.trace("Creating interface proxy for FactoryBean '" + beanName + "' of type [" + clazz.getName() + "] for use within another @Bean method because its " + (finalClass ? "implementation class" : "getObject() method") + " is final: Otherwise a getObject() call would not be routed to the factory."); } // 創(chuàng)建jdk動(dòng)態(tài)代理 return createInterfaceProxyForFactoryBean(factoryBean, exposedType, beanFactory, beanName); } else { if (logger.isDebugEnabled()) { logger.debug("Unable to proxy FactoryBean '" + beanName + "' of type [" + clazz.getName() + "] for use within another @Bean method because its " + (finalClass ? "implementation class" : "getObject() method") + " is final: A getObject() call will NOT be routed to the factory. " + "Consider declaring the return type as a FactoryBean interface."); } return factoryBean; } } } catch (NoSuchMethodException ex) { // No getObject() method -> shouldn't happen, but as long as nobody is trying to call it... } // 使用cglib創(chuàng)建代理 return createCglibProxyForFactoryBean(factoryBean, beanFactory, beanName); }
到此這篇關(guān)于Spring的@Configuration使用與原理的文章就介紹到這了,更多相關(guān)Spring的@Configuration內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot Jpa企業(yè)開發(fā)示例詳細(xì)講解
這篇文章主要介紹了SpringBoot Jpa企業(yè)開發(fā)示例,Jpa可以通過實(shí)體類生成數(shù)據(jù)庫的表,同時(shí)自帶很多增刪改查方法,大部分sql語句不需要我們自己寫,配置完成后直接調(diào)用方法即可,很方便2022-11-11基于SpringMVC實(shí)現(xiàn)網(wǎng)頁登錄攔截
SpringMVC的處理器攔截器類似于Servlet開發(fā)中的過濾器Filter,用于對(duì)處理器進(jìn)行預(yù)處理和后處理。因此,本文將為大家介紹如何通過SpringMVC實(shí)現(xiàn)網(wǎng)頁登錄攔截功能,需要的小伙伴可以了解一下2021-12-128個(gè)Spring事務(wù)失效場(chǎng)景詳解
相信大家對(duì)Spring種事務(wù)的使用并不陌生,但是你可能只是停留在基礎(chǔ)的使用層面上。今天,我們就簡單來說下Spring事務(wù)的原理,然后總結(jié)一下spring事務(wù)失敗的場(chǎng)景,并提出對(duì)應(yīng)的解決方案,需要的可以參考一下2022-12-12IntelliJ IDEA 詳細(xì)圖解最常用的配置(適合剛剛用的新人)
這篇文章主要介紹了IntelliJ IDEA 詳細(xì)圖解最常用的配置,本篇教程非常適合剛剛用的新人,本文圖文并茂給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-08-08簡單了解java volatile關(guān)鍵字實(shí)現(xiàn)的原理
這篇文章主要介紹了簡單了解volatile關(guān)鍵字實(shí)現(xiàn)的原理,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-08-08Java手動(dòng)創(chuàng)建線程池代碼實(shí)例
這篇文章主要介紹了Java手動(dòng)創(chuàng)建線程池代碼實(shí)例,FixedThreadPool或者SingleThreadPool,允許的請(qǐng)求隊(duì)列長度為Integer.MAX_VALUE,可能會(huì)堆積大量的請(qǐng)求,從而導(dǎo)致OOM,需要的朋友可以參考下2023-12-12Java過濾器doFilter里chain.doFilter()函數(shù)的理解
這篇文章主要介紹了Java過濾器doFilter里chain.doFilter()函數(shù)的理解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11java面向?qū)ο笤O(shè)計(jì)原則之接口隔離原則示例詳解
這篇文章主要為大家介紹了java面向?qū)ο笤O(shè)計(jì)原則之接口隔離原則的示例詳解,有需要的朋友可以借鑒參考下希望能夠有所幫助,祝大家多多進(jìn)步早日升職加薪2021-10-10