Spring系列-Spring AOP原理分析

先了解一些AOP的概念:
1.Advice(通知)
定义在连接点做什么,为切面增强提供织入接口。比如BeforeAdvice,AfterAdvice等。可以定义在方法执行前或执行后需要做的操作。
在这里插入图片描述
2.Pointcut(切点)
定义Advice通知应该作用于哪个连接点。
在这里插入图片描述
比如这个JdkRegexpMethodPointcut,就是通过正则表达式是否匹配来判断是否作用于这个Adivce的。

3.Advisor(通知器)
Advisor是Advice和Pointcut的结合,定义了应该使用哪个Advice,并且定义了在哪个Pointcut使用。也就是说,定义了在哪个条件用哪个Advice。

Spring AOP的实现,用到了动态代理,所以还要对动态代理有个了解,我之前在一篇帖子中也总结过JDK动态代理的用法:动态代理总结

Java已经提供了Proxy类,一般情况下,我们通过Proxy.newProxyInstance方法,就可以创建一个代理对象了。

那Spring AOP的原理是啥呢,我们定义了Advisor之后,如何起作用呢。
先说下结论吧,我们用了Advisor,来定义了我们要为某个类在满足某些条件的时候增强,当我们在使用这个类的对象时候,Spring AOP就会为这个类生成一个代理对象,并且把我们在Advice里面定义的操作都封装成一个个的拦截器,当我们调用方法的时候,就会先经过这些拦截器,然后再通过反射来调用真正的方法。

Spring AOP模块关于生成代理对象时,会涉及到下面这些类。
在这里插入图片描述
对于需要使用AspectJ的AOP应用,AspectJProxyFactory就可以集成Spring和AspectJ,对于使用Spring AOP的应用,ProxyFactoryBean和ProxyFactory都提供了AOP的封装,不同的是,ProxyFactoryBean可以在IoC容器中完成声明式的配置,而ProxyFactory需要编程式的使用。

接下来看下ProxyFactoryBean,使用ProxyFactoryBean之前需要配置,定义使用的通知器Advisor,定义target等,这里就不再详细说明,关键去看代码。

接下来,看一下ProxyFactoryBean如何生成代理对象的,看ProxyFactoryBean类的代码,很明显可以看到一个getObject方法,注释写的是返回一个proxy,那就是它了:

/**
 * Return a proxy. Invoked when clients obtain beans from this factory bean.
 * Create an instance of the AOP proxy to be returned by this factory.
 * The instance will be cached for a singleton, and create on each call to
 * {@code getObject()} for a proxy.
 * @return a fresh AOP proxy reflecting the current state of this factory
 */
@Override
public Object getObject() throws BeansException {
	initializeAdvisorChain();
	if (isSingleton()) {
		return getSingletonInstance();
	}
	else {
		if (this.targetName == null) {
			logger.warn("Using non-singleton proxies with singleton targets is often undesirable. " +
					"Enable prototype proxies by setting the 'targetName' property.");
		}
		return newPrototypeInstance();
	}
}

getObject里面,能够看到主要有三个方法,第一个是initializeAdvisorChain,这个是初始化Advisor链的,接下来,还需要判断一下,对于singleton和prototype来说,需要调用不同的方法来生成proxy。

那就先看下initializeAdvisorChain的逻辑:

/**
 * Create the advisor (interceptor) chain. Advisors that are sourced
 * from a BeanFactory will be refreshed each time a new prototype instance
 * is added. Interceptors added programmatically through the factory API
 * are unaffected by such changes.
 */
private synchronized void initializeAdvisorChain() throws AopConfigException, BeansException {
	// 判断通知器链是否已经初始化,如果已经初始化了,就直接返回。
	if (this.advisorChainInitialized) {
		return;
	}

	if (!ObjectUtils.isEmpty(this.interceptorNames)) {
		if (this.beanFactory == null) {
			throw new IllegalStateException("No BeanFactory available anymore (probably due to serialization) " +
					"- cannot resolve interceptor names " + Arrays.asList(this.interceptorNames));
		}

		// Globals can't be last unless we specified a targetSource using the property...
		if (this.interceptorNames[this.interceptorNames.length - 1].endsWith(GLOBAL_SUFFIX) &&
				this.targetName == null && this.targetSource == EMPTY_TARGET_SOURCE) {
			throw new AopConfigException("Target required after globals");
		}

		// Materialize interceptor chain from bean names.
		for (String name : this.interceptorNames) {
			if (logger.isTraceEnabled()) {
				logger.trace("Configuring advisor or advice '" + name + "'");
			}

			if (name.endsWith(GLOBAL_SUFFIX)) {
				if (!(this.beanFactory instanceof ListableBeanFactory)) {
					throw new AopConfigException(
							"Can only use global advisors or interceptors with a ListableBeanFactory");
				}
				addGlobalAdvisor((ListableBeanFactory) this.beanFactory,
						name.substring(0, name.length() - GLOBAL_SUFFIX.length()));
			}

			else {
				// If we get here, we need to add a named interceptor.
				// We must check if it's a singleton or prototype.
				Object advice;
				if (this.singleton || this.beanFactory.isSingleton(name)) {
					// Add the real Advisor/Advice to the chain.
					advice = this.beanFactory.getBean(name);
				}
				else {
					// It's a prototype Advice or Advisor: replace with a prototype.
					// Avoid unnecessary creation of prototype bean just for advisor chain initialization.
					advice = new PrototypePlaceholderAdvisor(name);
				}
				addAdvisorOnChainCreation(advice, name);
			}
		}
	}

	this.advisorChainInitialized = true;
}

这个方法是初始化通知器链的,我们定义的那些Advisor,就会在这里进行处理,顺着代码一直跟下去的话,就会发现,会把Advisor添加到AdvisedSupport类的advisors字段。

/**
 * List of Advisors. If an Advice is added, it will be wrapped
 * in an Advisor before being added to this List.
 */
private List<Advisor> advisors = new LinkedList<Advisor>();

通知器链初始化完成之后,再看下如何返回singleton的proxy,进去getSingletonInstance方法看一下:

/**
 * Return the singleton instance of this class's proxy object,
 * lazily creating it if it hasn't been created already.
 * @return the shared singleton proxy
 */
private synchronized Object getSingletonInstance() {
	// 先从缓存中获取,如果获取到的话,就可以直接返回了。
	if (this.singletonInstance == null) {
		this.targetSource = freshTargetSource();
		if (this.autodetectInterfaces && getProxiedInterfaces().length == 0 && !isProxyTargetClass()) {
			// Rely on AOP infrastructure to tell us what interfaces to proxy.
			Class<?> targetClass = getTargetClass();
			if (targetClass == null) {
				throw new FactoryBeanNotInitializedException("Cannot determine target class for proxy");
			}
			// 设置代理对象的接口
			setInterfaces(ClassUtils.getAllInterfacesForClass(targetClass, this.proxyClassLoader));
		}
		// Initialize the shared singleton instance.
		super.setFrozen(this.freezeProxy);
		this.singletonInstance = getProxy(createAopProxy());
	}
	return this.singletonInstance;
}

这里比较重要的是this.singletonInstance = getProxy(createAopProxy());这行代码,这里就是生成代理对象的地方。
其中,createAopProxy()先返回一个代理类,getProxy方法再根据这个代理类,返回一个代理对象。
先进去createAopProxy方法看下,createAopProxy方法在ProxyCreatorSupport类中。

/**
 * Subclasses should call this to get a new AOP proxy. They should <b>not</b>
 * create an AOP proxy with {@code this} as an argument.
 */
protected final synchronized AopProxy createAopProxy() {
	if (!this.active) {
		activate();
	}
	return getAopProxyFactory().createAopProxy(this);
}

@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.");
		}
		if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
			return new JdkDynamicAopProxy(config);
		}
		return new ObjenesisCglibAopProxy(config);
	}
	else {
		return new JdkDynamicAopProxy(config);
	}
}

createAopProxy()中先调用了getAopProxyFactory()方法获取AopProxyFactory,然后再调用createAopProxy(AdvisedSupport config)方法创建一个代理类。

createAopProxy()方法返回AopProxy类型的对象,AopProxy是个接口,JdkDynamicAopProxy和CglibAopProxy都是它的实现类:
在这里插入图片描述
在createAopProxy(AdvisedSupport config)中,返回AopProxy的时候,也会进行判断,如果目标类是个接口,则返回JdkDynamicAopProxy,还有一些情况返回ObjenesisCglibAopProxy。

顾名思义,JdkDynamicAopProxy是利用的jdk的proxy来创建的代理类,实现了InvocationHandler接口,重写了invoke方法:

/**
 * Implementation of {@code InvocationHandler.invoke}.
 * <p>Callers will see exactly the exception thrown by the target,
 * unless a hook method throws an exception.
 */
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
	MethodInvocation invocation;
	Object oldProxy = null;
	boolean setProxyContext = false;

	TargetSource targetSource = this.advised.targetSource;
	Class<?> targetClass = null;
	Object target = null;

	try {
		if (!this.equalsDefined && AopUtils.isEqualsMethod(method)) {
			// The target does not implement the equals(Object) method itself.
			return equals(args[0]);
		}
		if (!this.hashCodeDefined && AopUtils.isHashCodeMethod(method)) {
			// The target does not implement the hashCode() method itself.
			return hashCode();
		}
		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) {
			// Make invocation available if necessary.
			oldProxy = AopContext.setCurrentProxy(proxy);
			setProxyContext = true;
		}

		// May be null. Get as late as possible to minimize the time we "own" the target,
		// in case it comes from a pool.
		// 获取目标对象
		target = targetSource.getTarget();
		if (target != null) {
			// 获取目标类
			targetClass = target.getClass();
		}

		// Get the interception chain for this method.
		// 获取拦截器链,我们一开始初始化了通知器链,这里会把通知器链转换为拦截器链。
		List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);

		// Check whether we have any advice. If we don't, we can fallback on direct
		// reflective invocation of the target, and avoid creating a MethodInvocation.
		// 如果没有定义拦截器,则直接调用目标类的相应方法
		if (chain.isEmpty()) {
			// We can skip creating a MethodInvocation: just invoke the target directly
			// Note that the final invoker must be an InvokerInterceptor so we know it does
			// nothing but a reflective operation on the target, and no hot swapping or fancy proxying.
			Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
			retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
		}
		else {
			// We need to create a method invocation...
			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.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);
		}
	}
}

所以,Spring AOP的主要实现逻辑,在这里就能看出来了,生成代理类的时候,在invoke方法里面,把定义的那些通知器转换为拦截器,然后在调用目标对象的方法之前,先调用拦截器中的方法,面向切面编程的效果就有了。
把我们定义的通知器,转换为拦截器,是在getInterceptorsAndDynamicInterceptionAdvice方法中实现的:

/**
 * Determine a list of {@link org.aopalliance.intercept.MethodInterceptor} objects
 * for the given method, based on this configuration.
 * @param method the proxied method
 * @param targetClass the target class
 * @return List of MethodInterceptors (may also include InterceptorAndDynamicMethodMatchers)
 */
public List<Object> getInterceptorsAndDynamicInterceptionAdvice(Method method, 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;
}


@Override
public List<Object> getInterceptorsAndDynamicInterceptionAdvice(
		Advised config, Method method, 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<Object>(config.getAdvisors().length);
	Class<?> actualClass = (targetClass != null ? targetClass : method.getDeclaringClass());
	boolean hasIntroductions = hasMatchingIntroductions(config, actualClass);
	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)) {
				MethodInterceptor[] interceptors = registry.getInterceptors(advisor);
				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;
}


这个方法会把我们的通知器链,转换为拦截器链返回。接下来,还是看invoke中的逻辑,获取到拦截器链之后,如果拦截器链为空,则通过反射,调用目标类的方法:

/**
 * Invoke the given target via reflection, as part of an AOP method invocation.
 * @param target the target object
 * @param method the method to invoke
 * @param args the arguments for the method
 * @return the invocation result, if any
 * @throws Throwable if thrown by the target method
 * @throws org.springframework.aop.AopInvocationException in case of a reflection error
 */
public static Object invokeJoinpointUsingReflection(Object target, Method method, Object[] args)
		throws Throwable {

	// Use reflection to invoke the method.
	try {
		ReflectionUtils.makeAccessible(method);
		return method.invoke(target, args);
	}
	catch (InvocationTargetException ex) {
		// Invoked method threw a checked exception.
		// We must rethrow it. The client won't see the interceptor.
		throw ex.getTargetException();
	}
	catch (IllegalArgumentException ex) {
		throw new AopInvocationException("AOP configuration seems to be invalid: tried calling method [" +
				method + "] on target [" + target + "]", ex);
	}
	catch (IllegalAccessException ex) {
		throw new AopInvocationException("Could not access method [" + method + "]", ex);
	}
}

如果拦截器链不为空的话,则调用MethodInvocation的proceed方法,我们这里的MethodInvocation的实现类是ReflectiveMethodInvocation:

@Override
public Object proceed() throws Throwable {
	//	We start with an index of -1 and increment early.
	if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
		return invokeJoinpoint();
	}

	Object interceptorOrInterceptionAdvice =
			this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
	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.
			return proceed();
		}
	}
	else {
		// It's an interceptor, so we just invoke it: The pointcut will have
		// been evaluated statically before this object was constructed.
		return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
	}
}

这个proceed方法从索引为-1的拦截器开始调用,对当前的拦截器,先调用matches方法判断是否匹配(这个就是每个Pointcut定义的条件),如果匹配的话,就调用拦截器的invoke方法,不匹配的话,就继续调用proceed方法。

看到这里,基本上AOP的实现逻辑就差不多了,以JdkDynamicAopProxy的逻辑来说,就是创建了一个代理类,代理类中的invoke方法里面去调用拦截器。然后通过Proxy.newProxyInstance方法,创建一个代理对象。

关于JdkDynamicAopProxy还有个细节说下,就是把我们定义的通知器转换成拦截器的地方,在getInterceptorsAndDynamicInterceptionAdvice方法中,主要是这句话:

MethodInterceptor[] interceptors = registry.getInterceptors(advisor);

这里是调用了AdvisorAdapterRegistry的getInterceptors方法,把Advisor转换成了拦截器列表。这里的AdvisorAdapterRegistry是用的DefaultAdvisorAdapterRegistry,进去DefaultAdvisorAdapterRegistry类里看下:

public class DefaultAdvisorAdapterRegistry implements AdvisorAdapterRegistry, Serializable {

	private final List<AdvisorAdapter> adapters = new ArrayList<AdvisorAdapter>(3);


	/**
	 * Create a new DefaultAdvisorAdapterRegistry, registering well-known adapters.
	 */
	public DefaultAdvisorAdapterRegistry() {
		registerAdvisorAdapter(new MethodBeforeAdviceAdapter());
		registerAdvisorAdapter(new AfterReturningAdviceAdapter());
		registerAdvisorAdapter(new ThrowsAdviceAdapter());
	}


	@Override
	public Advisor wrap(Object adviceObject) throws UnknownAdviceTypeException {
		if (adviceObject instanceof Advisor) {
			return (Advisor) adviceObject;
		}
		if (!(adviceObject instanceof Advice)) {
			throw new UnknownAdviceTypeException(adviceObject);
		}
		Advice advice = (Advice) adviceObject;
		if (advice instanceof MethodInterceptor) {
			// So well-known it doesn't even need an adapter.
			return new DefaultPointcutAdvisor(advice);
		}
		for (AdvisorAdapter adapter : this.adapters) {
			// Check that it is supported.
			if (adapter.supportsAdvice(advice)) {
				return new DefaultPointcutAdvisor(advice);
			}
		}
		throw new UnknownAdviceTypeException(advice);
	}

	@Override
	public MethodInterceptor[] getInterceptors(Advisor advisor) throws UnknownAdviceTypeException {
		List<MethodInterceptor> interceptors = new ArrayList<MethodInterceptor>(3);
		Advice advice = advisor.getAdvice();
		if (advice instanceof MethodInterceptor) {
			interceptors.add((MethodInterceptor) advice);
		}
		for (AdvisorAdapter adapter : this.adapters) {
			if (adapter.supportsAdvice(advice)) {
				interceptors.add(adapter.getInterceptor(advisor));
			}
		}
		if (interceptors.isEmpty()) {
			throw new UnknownAdviceTypeException(advisor.getAdvice());
		}
		return interceptors.toArray(new MethodInterceptor[interceptors.size()]);
	}

	@Override
	public void registerAdvisorAdapter(AdvisorAdapter adapter) {
		this.adapters.add(adapter);
	}

}

在getInterceptors中,先从通知器中获取定义的Advice,如果Advice是MethodInterceptor类型的话,就直接强转,如果不是的话,就调用adapter.getInterceptor(advisor)方法进行转换。在DefaultAdvisorAdapterRegistry构造方法中可以看到对adapters的赋值,把MethodBeforeAdviceAdapter、AfterReturningAdviceAdapter、ThrowsAdviceAdapter这三个Adapter添加到了adapters中。

class MethodBeforeAdviceAdapter implements AdvisorAdapter, Serializable {

	@Override
	public boolean supportsAdvice(Advice advice) {
		return (advice instanceof MethodBeforeAdvice);
	}

	@Override
	public MethodInterceptor getInterceptor(Advisor advisor) {
		MethodBeforeAdvice advice = (MethodBeforeAdvice) advisor.getAdvice();
		return new MethodBeforeAdviceInterceptor(advice);
	}

}


class AfterReturningAdviceAdapter implements AdvisorAdapter, Serializable {

	@Override
	public boolean supportsAdvice(Advice advice) {
		return (advice instanceof AfterReturningAdvice);
	}

	@Override
	public MethodInterceptor getInterceptor(Advisor advisor) {
		AfterReturningAdvice advice = (AfterReturningAdvice) advisor.getAdvice();
		return new AfterReturningAdviceInterceptor(advice);
	}

}


class ThrowsAdviceAdapter implements AdvisorAdapter, Serializable {

	@Override
	public boolean supportsAdvice(Advice advice) {
		return (advice instanceof ThrowsAdvice);
	}

	@Override
	public MethodInterceptor getInterceptor(Advisor advisor) {
		return new ThrowsAdviceInterceptor(advisor.getAdvice());
	}

}


可以看到这三个Adapter的实现,MethodBeforeAdviceAdapter的getInterceptor方法会返回一个MethodBeforeAdviceInterceptor,AfterReturningAdviceAdapter的getInterceptor方法会返回一个AfterReturningAdviceInterceptor,ThrowsAdviceAdapter的getInterceptor方法会返回一个ThrowsAdviceInterceptor。
返回这些过滤器后,在invoke方法的proceed方法中,如果matches了,就会调用interceptor的invoke方法,所以,可以看下MethodBeforeAdviceInterceptor类的代码:

public class MethodBeforeAdviceInterceptor implements MethodInterceptor, Serializable {

	private MethodBeforeAdvice advice;


	/**
	 * Create a new MethodBeforeAdviceInterceptor for the given advice.
	 * @param advice the MethodBeforeAdvice to wrap
	 */
	public MethodBeforeAdviceInterceptor(MethodBeforeAdvice advice) {
		Assert.notNull(advice, "Advice must not be null");
		this.advice = advice;
	}

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

}

MethodBeforeAdviceInterceptor的invoke方法,会先调用Advice的before方法,然后再调用mi.proceed()方法,继续拦截器链的逻辑。其它Interceptor的代码逻辑也类似,这里就不详细看了。

看到现在,Spring AOP的原理实现基本就清楚了。我们定义的各种不同的Advice,会转换成不同的拦截器,在invoke中依次被调用。通过CGLib返回代理对象的逻辑虽然有些不同,但是基本上都一样的,这里也就不再详细分析了。

参考资料:
1.《Spring技术内幕》 计文柯 著

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值