Spring源碼------AOP源碼分析

Spring源碼------AOP源碼分析

目錄

Spring源碼------AOP源碼分析

1、AOP簡介

2、AOP時序圖

3、源碼分析(簡約版)

3.1 準備工作 

3.2 源碼分析


1、AOP簡介

AOP是什麼?

AOP技術利用一種稱爲“橫切”的技術,剖解開封裝的對象內部,並將那些影響了多個類的公共行爲封裝到一個可重用模塊,並將其名爲“Aspect”,即切面。所謂“切面”,簡單地說,就是將那些與業務無關,卻爲業務模塊所共同調用的邏輯或責任封裝起來,便於減少系統的重複代碼,降低模塊間的耦合度,並有利於未來的可操作性和可維護性。AOP代表的是一個橫向的關係,如果說“對象”是一個空心的圓柱體,其中封裝的是對象的屬性和行爲;那麼面向方面編程的方法,就彷彿一把利刃,將這些空心圓柱體剖開,以獲得其內部的消息。而剖開的切面,也就是所謂的“方面”了。然後它又以巧奪天功的妙手將這些剖開的切面復原,不留痕跡。

AOP相關概念

  • 方面(Aspect):一個關注點的模塊化,這個關注點實現可能另外橫切多個對象。事務管理是一個很好的橫切關注點例子。方面用Spring的Advisor或攔截器實現。
  • 連接點(Joinpoint): 程序執行過程中明確的點,如方法的調用或特定的異常被拋出。
  • 通知(Advice): 在特定的連接點,AOP框架執行的動作。各種類型的通知包括“around”、“before”和“throws”通知。通知類型將在下面討論。許多AOP框架包括Spring都是以攔截器做通知模型,維護一個“圍繞”連接點的攔截器鏈。Spring中定義了四個advice: BeforeAdvice, AfterAdvice, ThrowAdvice和DynamicIntroductionAdvice
  • 切入點(Pointcut): 指定一個通知將被引發的一系列連接點的集合。AOP框架必須允許開發者指定切入點:例如,使用正則表達式。 Spring定義了Pointcut接口,用來組合MethodMatcher和ClassFilter,可以通過名字很清楚的理解, MethodMatcher是用來檢查目標類的方法是否可以被應用此通知,而ClassFilter是用來檢查Pointcut是否應該應用到目標類上
  • 目標對象(Target Object): 包含連接點的對象。也被稱作被通知或被代理對象。
  • AOP代理(AOP Proxy): AOP框架創建的對象,包含通知。 在Spring中,AOP代理可以是JDK動態代理或者CGLIB代理。

AOP原理

AOP 實現的關鍵就在於 AOP 框架自動創建的 AOP 代理,AOP 代理則可分爲靜態代理和動態代理兩大類

  • 靜態代理是指使用 AOP 框架提供的命令進行編譯,從而在編譯階段就可生成 AOP 代理類,因此也稱爲編譯時增強
  • 而動態代理則在運行時藉助於 JDK 動態代理、CGLIB 等在內存中“臨時”生成 AOP 動態代理類,因此也被稱爲運行時增強Spring AOP則採用的是動態代理來實現

2、AOP時序圖

3、源碼分析(簡約版)

3.1 準備工作 

首先定義一個Spring AOP的配置文件spring-aop.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <aop:config>
        <aop:aspect id="TestAspect" ref="aspect">
            <aop:pointcut id="pointcut" expression="execution(* org.study.spring.aop.*.*(..))"/>
            <aop:before method="doBefore" pointcut-ref="pointcut"/>
        </aop:aspect>
    </aop:config>

    <bean id="aspect" class="org.study.spring.aop.Aspect"/>
    <bean id="test" class="org.study.spring.aop.TestAOP"/>

</beans>

由於我們只分析JDK動態代理的實現方式,所以需要定義一個接口。

public interface ITestAOP{
    public void doSomething();
}

目標對象實現上面定義的接口。

public class Test implements ITestAOP {
    public void doSomething() {
        System.out.println("do something");
    }
}

定義Aspect,這裏我們以前置通知爲例

public class Aspect {
    public void doBefore(JoinPoint jp) {
        System.out.println("do before");
    }
}

編寫程序入口代碼,可以直接打斷點進行調試。

ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
Test bean = context.getBean("test", TestAOP.class);
bean.doSomething();

3.2 源碼分析

1.進入AbstractAutowireCapableBeanFactory的initializeBean()

   從源碼中可以看到,AOP其實就是通過後置處理對bean的一種增強,而它用到的是代理技術

//初始容器創建的Bean實例對象,爲其添加BeanPostProcessor後置處理器
	protected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd) {
		//JDK的安全機制驗證權限
		if (System.getSecurityManager() != null) {
			//實現PrivilegedAction接口的匿名內部類
			AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
				invokeAwareMethods(beanName, bean);
				return null;
			}, getAccessControlContext());
		}
		else {
			//爲Bean實例對象包裝相關屬性,如名稱,類加載器,所屬容器等信息
			invokeAwareMethods(beanName, bean);
		}

		Object wrappedBean = bean;
		//對BeanPostProcessor後置處理器的postProcessBeforeInitialization
		//回調方法的調用,爲Bean實例初始化前做一些處理
		if (mbd == null || !mbd.isSynthetic()) {
			wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
		}

		//調用Bean實例對象初始化的方法,這個初始化方法是在Spring Bean定義配置
		//文件中通過init-method屬性指定的
		try {
			invokeInitMethods(beanName, wrappedBean, mbd);
		}
		catch (Throwable ex) {
			throw new BeanCreationException(
					(mbd != null ? mbd.getResourceDescription() : null),
					beanName, "Invocation of init method failed", ex);
		}
		//對BeanPostProcessor後置處理器的postProcessAfterInitialization
		//回調方法的調用,爲Bean實例初始化之後做一些處理
		if (mbd == null || !mbd.isSynthetic()) {
			wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
		}

		return wrappedBean;
	}

2. 在applyBeanPostProcessorsAfterInitialization方法上,

    按住Ctrl+鼠標左鍵進入applyBeanPostProcessorsAfterInitialization方法

	//調用BeanPostProcessor後置處理器實例對象初始化之後的處理方法
	public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName)
			throws BeansException {
		Object result = existingBean;
		//遍歷容器爲所創建的Bean添加的所有BeanPostProcessor後置處理器
		for (BeanPostProcessor beanProcessor : getBeanPostProcessors()) {
			//調用Bean實例所有的後置處理中的初始化後處理方法,爲Bean實例對象在
			//初始化之後做一些自定義的處理操作
			Object current = beanProcessor.postProcessAfterInitialization(result, beanName);
			if (current == null) {
				return result;
			}
			result = current;
		}
		return result;
	}

3. 在postProcessAfterInitialization方法上,

   按住Ctrl+Alt+B選擇AbstractAutoProxyCreator進入postProcessAfterInitialization()

	@Override
	public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) throws BeansException {
		if (bean != null) {
			Object cacheKey = getCacheKey(bean.getClass(), beanName);
			if (!this.earlyProxyReferences.contains(cacheKey)) {
				return wrapIfNecessary(bean, beanName, cacheKey);
			}
		}
		return bean;
	}

4.wrapIfNecessary方法上,按住Ctrl+鼠標左鍵進入自身wrapIfNecessary方法

protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
		if (beanName != null && this.targetSourcedBeans.contains(beanName)) {
			return bean;
		}
		if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
			return bean;
		}
		if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
			this.advisedBeans.put(cacheKey, Boolean.FALSE);
			return bean;
		}

		// 獲取通知列表
		Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
//判斷是否有通知,如果有,繼續進行代理
		if (specificInterceptors != DO_NOT_PROXY) {
			this.advisedBeans.put(cacheKey, Boolean.TRUE);
        //創建代理對象
			Object proxy = createProxy(
					bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
			this.proxyTypes.put(cacheKey, proxy.getClass());
			return proxy;
		}
        
		this.advisedBeans.put(cacheKey, Boolean.FALSE);
		return bean;
	}

從上面我們可以看到,Spring在這裏先獲取配置好的Advisor信息,然後調用createProxy方法爲目標對象創建了代理,接着將創建的代理對象返回。

5. 在getAdvicesAndAdvisorsForBean方法上,按住Ctrl+Alt+B選擇AbstractAdvisorAutoProxyCreatorgetAdvicesAndAdvisorsForBean

protected Object[] getAdvicesAndAdvisorsForBean(Class<?> beanClass, String beanName, @Nullable TargetSource targetSource) {

        //獲取通知列表
		List<Advisor> advisors = findEligibleAdvisors(beanClass, beanName);
		if (advisors.isEmpty()) {
			return DO_NOT_PROXY;
		}
		return advisors.toArray();
	}


protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
		List<Advisor> candidateAdvisors = findCandidateAdvisors();
		List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
		extendAdvisors(eligibleAdvisors);
		if (!eligibleAdvisors.isEmpty()) {
			eligibleAdvisors = sortAdvisors(eligibleAdvisors);
		}
		return eligibleAdvisors;
	}

protected List<Advisor> sortAdvisors(List<Advisor> advisors) {
		AnnotationAwareOrderComparator.sort(advisors);
		return advisors;
	}

public static void sort(List<?> list) {
		if (list.size() > 1) {
			Collections.sort(list, INSTANCE);
		}
	}

從上面可以知道,代理前需要先拿到通知的列表(包括前置通知,後置通知,異常通知等等)

6.按住Ctrl+Alt+左方向鍵返回到第4步的wrapIfNecessary方法,

    找到createProxy方法,按住Ctrl+鼠標左鍵進入createProxy方法

protected Object createProxy(Class<?> beanClass, @Nullable String beanName,
			@Nullable Object[] specificInterceptors, TargetSource targetSource) {
		if (this.beanFactory instanceof ConfigurableListableBeanFactory) {AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass);
		}
		ProxyFactory proxyFactory = new ProxyFactory();
		proxyFactory.copyFrom(this);
		if (!proxyFactory.isProxyTargetClass()) {
			if (shouldProxyTargetClass(beanClass, beanName)) {
				proxyFactory.setProxyTargetClass(true);
			}
			else {
				evaluateProxyInterfaces(beanClass, proxyFactory);
			}
		}
		Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
		proxyFactory.addAdvisors(advisors);
		proxyFactory.setTargetSource(targetSource);
		customizeProxyFactory(proxyFactory);
		proxyFactory.setFrozen(this.freezeProxy);
		if (advisorsPreFiltered()) {
			proxyFactory.setPreFiltered(true);
		}
		return proxyFactory.getProxy(getProxyClassLoader());
	}

代理前各種屬性操作------

8.直接進入JdkDynamicAopProxy 的 getProxy()方法

	/**
	 * 獲取代理類要實現的接口,除了Advised對象中配置的,還會加上SpringProxy, Advised(opaque=false)
	 * 檢查上面得到的接口中有沒有定義 equals或者hashcode的接口
	 * 調用Proxy.newProxyInstance創建代理對象
	 */
	@Override
	public Object getProxy(@Nullable ClassLoader classLoader) {
		if (logger.isDebugEnabled()) {
			logger.debug("Creating JDK dynamic proxy: target source is " + this.advised.getTargetSource());
		}
		Class<?>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true);
		findDefinedEqualsAndHashCodeMethods(proxiedInterfaces);
		return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
	}

看到這裏,大家一定非常的興奮,Proxy.newProxyInstance(classLoader, proxiedInterfaces, this)不就是反射獲取實例對象嗎?沒錯,那麼疑問來了,Aop切面是如何織入的呢?


 


9. 在proxyFactory.getProxy方法上,Ctrl+Alt+B進入ProxyFactorygetProxy方法

	protected final synchronized AopProxy createAopProxy() {
		if (!this.active) {
			activate();
		}
		return getAopProxyFactory().createAopProxy(this);
	}

10. 在createAopProxy(this)方法上,按住Ctrl+Alt+B進入DefaultAopProxyFactorycreateAopProxy方法
    從下面源碼可以得出:會根據判斷進入不同的代理模式

@Override
	public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
		if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
			Class<?> targetClass = config.getTargetClass();
			if (targetClass == null) {
				throw new AopConfigException("TargetSource cannot determine target class: " +
						"Either an interface or a target is required for proxy creation.");
			}

            //如果目標類實現的是接口,走jdk動態代理
			if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
				return new JdkDynamicAopProxy(config);
			}
            //cglib動態代理
			return new ObjenesisCglibAopProxy(config);
		}
		else {
			return new JdkDynamicAopProxy(config);
		}
	}

11.進入JdkDynamicAopProxyinvoke方法

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		
        MethodInvocation invocation;
		Object oldProxy = null;
		boolean setProxyContext = false;

		TargetSource targetSource = this.advised.targetSource;
		Object target = null;

		try {
			//eqauls()方法,具目標對象未實現此方法
			if (!this.equalsDefined && AopUtils.isEqualsMethod(method)) {
				// The target does not implement the equals(Object) method itself.
				return equals(args[0]);
			}
			//hashCode()方法,具目標對象未實現此方法
			else if (!this.hashCodeDefined && AopUtils.isHashCodeMethod(method)) {
				// The target does not implement the hashCode() method itself.
				return hashCode();
			}
			else if (method.getDeclaringClass() == DecoratingProxy.class) {
				// There is only getDecoratedClass() declared -> dispatch to proxy config.
				return AopProxyUtils.ultimateTargetClass(this.advised);
			}
			//Advised接口或者其父接口中定義的方法,直接反射調用,不應用通知
			else if (!this.advised.opaque && method.getDeclaringClass().isInterface() &&
					method.getDeclaringClass().isAssignableFrom(Advised.class)) {
				// Service invocations on ProxyConfig with the proxy config...
				return AopUtils.invokeJoinpointUsingReflection(this.advised, method, args);
			}

			Object retVal;

			if (this.advised.exposeProxy) {
				oldProxy = AopContext.setCurrentProxy(proxy);
				setProxyContext = true;
			}
			//獲得目標對象的類
			target = targetSource.getTarget();
			Class<?> targetClass = (target != null ? target.getClass() : null);
			//獲取可以應用到此方法上的Interceptor列表
			List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
			//如果沒有可以應用到此方法的通知(Interceptor),此直接反射調用 method.invoke(target, args)
			if (chain.isEmpty()) {
				Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
				retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
			}
			else {
				//創建MethodInvocation
				invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
				// Proceed to the joinpoint through the interceptor chain.
				retVal = invocation.proceed();
			}

			// Massage return value if necessary.
			Class<?> returnType = method.getReturnType();
			if (retVal != null && retVal == target &&
					returnType != Object.class && returnType.isInstance(proxy) &&
					!RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) {
				// Special case: it returned "this" and the return type of the method
				// is type-compatible. Note that we can't help if the target sets
				// a reference to itself in another returned object.
				retVal = proxy;
			}
			else if (retVal == null && returnType != Void.TYPE && returnType.isPrimitive()) {
				throw new AopInvocationException(
						"Null return value from advice does not match primitive return type for: " + method);
			}
			return retVal;
		}
		finally {
			if (target != null && !targetSource.isStatic()) {
				// Must have come from TargetSource.
				targetSource.releaseTarget(target);
			}
			if (setProxyContext) {
				// Restore old proxy.
				AopContext.setCurrentProxy(oldProxy);
			}
		}
	}

這裏有一個很重要的方法:獲取可以應用到此方法上的Interceptor列表

List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);

  • 所以 按住Ctrl+Alt+B****getInterceptorsAndDynamicInterceptionAdvice方法,進入AdvisedSupportgetInterceptorsAndDynamicInterceptionAdvice方法
public List<Object> getInterceptorsAndDynamicInterceptionAdvice(Method method, @Nullable Class<?> targetClass) {
		MethodCacheKey cacheKey = new MethodCacheKey(method);
		List<Object> cached = this.methodCache.get(cacheKey);
		if (cached == null) {
			cached = this.advisorChainFactory.getInterceptorsAndDynamicInterceptionAdvice(
					this, method, targetClass);
			this.methodCache.put(cacheKey, cached);
		}
		return cached;
	}
  • 繼續進入在advisorChainFactory.getInterceptorsAndDynamicInterceptionAdvice方法上,

      按住Ctrl+Alt+B進入DefaultAdvisorChainFactorygetInterceptorsAndDynamicInterceptionAdvice方法

/**
	 * 從提供的配置實例config中獲取advisor列表,遍歷處理這些advisor.如果是IntroductionAdvisor,
	 * 則判斷此Advisor能否應用到目標類targetClass上.如果是PointcutAdvisor,則判斷
	 * 此Advisor能否應用到目標方法method上.將滿足條件的Advisor通過AdvisorAdaptor轉化成Interceptor列表返回.
	 */
	@Override
	public List<Object> getInterceptorsAndDynamicInterceptionAdvice(
			Advised config, Method method, @Nullable Class<?> targetClass) {
		// This is somewhat tricky... We have to process introductions first,
		// but we need to preserve order in the ultimate list.
		List<Object> interceptorList = new ArrayList<>(config.getAdvisors().length);
		Class<?> actualClass = (targetClass != null ? targetClass : method.getDeclaringClass());
		//查看是否包含IntroductionAdvisor
		boolean hasIntroductions = hasMatchingIntroductions(config, actualClass);
		//這裏實際上註冊一系列AdvisorAdapter,用於將Advisor轉化成MethodInterceptor
		AdvisorAdapterRegistry registry = GlobalAdvisorAdapterRegistry.getInstance();

		for (Advisor advisor : config.getAdvisors()) {
			if (advisor instanceof PointcutAdvisor) {
				// Add it conditionally.
				PointcutAdvisor pointcutAdvisor = (PointcutAdvisor) advisor;
				if (config.isPreFiltered() || pointcutAdvisor.getPointcut().getClassFilter().matches(actualClass)) {
					//這個地方這兩個方法的位置可以互換下
					//將Advisor轉化成Interceptor
					MethodInterceptor[] interceptors = registry.getInterceptors(advisor);
					//檢查當前advisor的pointcut是否可以匹配當前方法
					MethodMatcher mm = pointcutAdvisor.getPointcut().getMethodMatcher();
					if (MethodMatchers.matches(mm, method, actualClass, hasIntroductions)) {
						if (mm.isRuntime()) {
							// Creating a new object instance in the getInterceptors() method
							// isn't a problem as we normally cache created chains.
							for (MethodInterceptor interceptor : interceptors) {
								interceptorList.add(new InterceptorAndDynamicMethodMatcher(interceptor, mm));
							}
						}
						else {
							interceptorList.addAll(Arrays.asList(interceptors));
						}
					}
				}
			}
			else if (advisor instanceof IntroductionAdvisor) {
				IntroductionAdvisor ia = (IntroductionAdvisor) advisor;
				if (config.isPreFiltered() || ia.getClassFilter().matches(actualClass)) {
					Interceptor[] interceptors = registry.getInterceptors(advisor);
					interceptorList.addAll(Arrays.asList(interceptors));
				}
			}
			else {
				Interceptor[] interceptors = registry.getInterceptors(advisor);
				interceptorList.addAll(Arrays.asList(interceptors));
			}
		}

		return interceptorList;
	}

上面搞了這麼多,就是爲了拿到一條將通知封裝成MethodInvoation的通知鏈,爲了以下使用責任鏈模式進行invoke時可以順序的織入所有的通知。繼續搞下去:

12. 返回11步,進入invocation.proceed方法,進入到 ReflectiveMethodInvocation 的proceed() 方法

Override
	@Nullable
	public Object proceed() throws Throwable {
		//	We start with an index of -1 and increment early.
		//如果Interceptor執行完了,則執行joinPoint
		if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
			return invokeJoinpoint();
		}

		Object interceptorOrInterceptionAdvice =
				this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
		//如果要動態匹配joinPoint
		if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
			// Evaluate dynamic method matcher here: static part will already have
			// been evaluated and found to match.
			InterceptorAndDynamicMethodMatcher dm =
					(InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice;
			//動態匹配:運行時參數是否滿足匹配條件
			if (dm.methodMatcher.matches(this.method, this.targetClass, this.arguments)) {
				return dm.interceptor.invoke(this);
			}
			else {
				// Dynamic matching failed.
				// Skip this interceptor and invoke the next in the chain.
				//動態匹配失敗時,略過當前Intercetpor,調用下一個Interceptor
				return proceed();
			}
		}
		else {
			// It's an interceptor, so we just invoke it: The pointcut will have
			// been evaluated statically before this object was constructed.
			//執行當前Intercetpor
			return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
		}
	}

13.進入dm.interceptor.invoke(this);按住Ctrl+Alt+B選擇選擇MethodBeforeAdviceInterceptor的invoke方法

@Override
	public Object invoke(MethodInvocation mi) throws Throwable {

		this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis() );

		return mi.proceed();

	}

14. 在.invoke(this)方法上,按住Ctrl+Alt+B選擇AfterReturningAdviceInterceptorinvoke方法

@Override
	public Object invoke(MethodInvocation mi) throws Throwable {
		Object retVal = mi.proceed();
		this.advice.afterReturning(retVal, mi.getMethod(), mi.getArguments(), mi.getThis());
		return retVal;
	}

從12、13、14可以知道Spring AOP多個通知的執行順序應該像一個同心圓,

 

從12、13、14可以知道Spring AOP多個通知的執行順序應該像一個同心圓:

spring aop就是一個同心圓,要執行的方法爲圓心,最外層的order最小。從最外層按照AOP1、AOP2的順序依次執行doAround方法,doBefore方法。然後執行method方法,最後按照AOP2、AOP1的順序依次執行doAfter、doAfterReturn方法。也就是說對多個AOP來說,先before的,一定後after。

        如果我們要在同一個方法事務提交後執行自己的AOP,那麼把事務的AOP order設置爲2,自己的AOP order設置爲1,然後在doAfterReturn裏邊處理自己的業務邏輯。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章