spring源码分享---事务

核心逻辑

spring的核心逻辑是通过TransactionInterceptor来代理对应的方法,并根据TransactionInfo来维护当前方法下的对应的事务状态信息。下图展示了其核心逻辑。
spring源码分享---事务_第1张图片
其对应的代码为:

		final TransactionAttribute txAttr = getTransactionAttributeSource().getTransactionAttribute(method, targetClass);
		final PlatformTransactionManager tm = determineTransactionManager(txAttr);
		final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);

		if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
			// Standard transaction demarcation with getTransaction and commit/rollback calls.
			TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);

			Object retVal;
			try {
				// This is an around advice: Invoke the next interceptor in the chain.
				// This will normally result in a target object being invoked.
				retVal = invocation.proceedWithInvocation();
			}
			catch (Throwable ex) {
				// target invocation exception
				completeTransactionAfterThrowing(txInfo, ex);
				throw ex;
			}
			finally {
				cleanupTransactionInfo(txInfo);
			}
			commitTransactionAfterReturning(txInfo);
			return retVal;
		}

可以看到其首先是获取一个TransactionInfo,这个TransactionInfo主要是先从TransactionAttributeSource中获取TransactionAttribute,然后再根据这个TransactionAttribute获取PlatformTransactionManager,而PlatformTransactionManager则根据TransactionAttribute已经线程上下文中的数据生成当前的TransactionStatus数据,最后根据TransactionAttribute,TransactionStatus,PlatformTransactionManager共同组成TransactionInfo,并将其存入到线程上下文中。

TransactionAttributeSource

TransactionAttributeSource主要作用是提供TransactionAttribute,这个接口的方法也只有一个即为:**TransactionAttribute getTransactionAttribute(Method method, Class targetClass);**不过其获取的方式不同而有不同的实现。
spring源码分享---事务_第2张图片

  • MethodMapTransactionAttributeSource: MethodMapTransactionAttributeSource是直接以Method为key来存储对应的对象的,其通过方法的全限定名找出这个类对应的classLoader下的Method,那么对于不是和这个类同一个classLoader的对应的方法是无法从这个TransactionAttributeSource获取TransactionAttribute的。
  • NameMatchTransactionAttributeSource: 其是通过获取方法的name并和配置的规则比较获取对应的TransactionAttribute,下面配置中其对应的tx:method的配置则最终会放入到NameMatchTransactionAttributeSource中。
<tx:advice id="txAdvice" transaction-manager="transactionManager">
   <tx:attributes>
       <tx:method name="get*" propagation="SUPPORTS" isolation="DEFAULT"
                 read-only="true" />
   </tx:attributes>
</tx:advice>
  • MatchAlwaysTransactionAttributeSource: 这个TransactionAttributeSource所有的method都是返回的同一套的TransactionAttribute。

  • AnnotationTransactionAttributeSource: 其继承了AbstractFallbackTransactionAttributeSource,这个类主要是以[method,class]为key对其transactionAttribute做了缓存。其主要逻辑是根据注册的TransactionAnnotationParser找出对应的transactionAttribute,TransactionAnnotationParser主要有三种:

    • SpringTransactionAnnotationParser: 找对应方法的Transaction注解
    • JtaTransactionAnnotationParser: 找javax.transaction.Transactional注解
    • Ejb3TransactionAnnotationParser: 找javax.ejb.TransactionAttribute

TransactionInterceptor

注入方式

TransactionInterceptor是事务的核心类,其是一个实现了MethodInterceptor接口的类,其在spring容器中是以Advice的形式通过AOP到目标方法的代理调用链上,并在调用时进行相应的事务处理。对于TransactionInterceptor这个Advice的注入方式主要有两种,第一种是的形式,第二种则是会注册一个TransactionInterceptor到容器中,但是TransactionInterceptor注册是为一个singleton类,则如果两种方式都用的话会出错。
只是单纯的注册了一个TransactionInterceptor到容器中,而要想其进入代理链则需要的配合将其与pointcut组合成一个advisor注册到容器中。
则是注册了一个BeanFactoryTransactionAttributeSourceAdvisor到容器中。

主要逻辑

其主要逻辑刚开始描述过,先生成一个TransactionInfo并通过ThreadLocal的方式绑定到当前线程以便外部代码获取当前的TransactionStatus数据。并根据执行成功或者失败从当前的TransactionInfo中获取PlatformManager和TransactionStatus进行commit或者rollack操作。

PlatformTransactionManager

PlatformTransactionManager是spring事务的核心,如果不使用代理式的事务处理,可以直接使用这个类进行事务处理,其提供的主要方法是getTransaction,commit和rollback这三个方法,其主要作用是

public interface PlatformTransactionManager {
	TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
	void commit(TransactionStatus status) throws TransactionException;
	void rollback(TransactionStatus status) throws TransactionException;
}

在介绍三个方法之前,先介绍几个主要的方法suspend,resume,和savepoint和transaction这几件事。

  • suspend: 挂起当前的事务,对于DataSourcePlatformManager即主要是将当前线程下绑定的connection设置为空
  • resume: 根据创建的事务存档恢复到存储的事务,对于DataSourcePlatformManager即将connection还回来
  • savePoint: 这个是TransactionStatus内部的数据,即为一个事务的存储点,对于创建了存储点的事务,回滚起来也只是回滚到这个存储点。

下面来看一下这三个方法的代码

getTransaction

@Override
	public final TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException {
		Object transaction = doGetTransaction();

		// Cache debug flag to avoid repeated checks.
		boolean debugEnabled = logger.isDebugEnabled();

		if (definition == null) {
			// Use defaults if no transaction definition given.
			definition = new DefaultTransactionDefinition();
		}

		if (isExistingTransaction(transaction)) {
			// Existing transaction found -> check propagation behavior to find out how to behave.
			return handleExistingTransaction(definition, transaction, debugEnabled);
		}

		// Check definition settings for new transaction.
		if (definition.getTimeout() < TransactionDefinition.TIMEOUT_DEFAULT) {
			throw new InvalidTimeoutException("Invalid transaction timeout", definition.getTimeout());
		}

		// No existing transaction found -> check propagation behavior to find out how to proceed.
		if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) {
			throw new IllegalTransactionStateException(
					"No existing transaction found for transaction marked with propagation 'mandatory'");
		}
		else if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||
				definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||
				definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
			SuspendedResourcesHolder suspendedResources = suspend(null);
			if (debugEnabled) {
				logger.debug("Creating new transaction with name [" + definition.getName() + "]: " + definition);
			}
			try {
				boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
				DefaultTransactionStatus status = newTransactionStatus(
						definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
				doBegin(transaction, definition);
				prepareSynchronization(status, definition);
				return status;
			}
			catch (RuntimeException ex) {
				resume(null, suspendedResources);
				throw ex;
			}
			catch (Error err) {
				resume(null, suspendedResources);
				throw err;
			}
		}
		else {
			// Create "empty" transaction: no actual transaction, but potentially synchronization.
			if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT && logger.isWarnEnabled()) {
				logger.warn("Custom isolation level specified but no actual transaction initiated; " +
						"isolation level will effectively be ignored: " + definition);
			}
			boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
			return prepareTransactionStatus(definition, null, true, newSynchronization, debugEnabled, null);
		}
	}
private TransactionStatus handleExistingTransaction(
			TransactionDefinition definition, Object transaction, boolean debugEnabled)
			throws TransactionException {

		if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NEVER) {
			throw new IllegalTransactionStateException(
					"Existing transaction found for transaction marked with propagation 'never'");
		}

		if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NOT_SUPPORTED) {
			if (debugEnabled) {
				logger.debug("Suspending current transaction");
			}
			Object suspendedResources = suspend(transaction);
			boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
			return prepareTransactionStatus(
					definition, null, false, newSynchronization, debugEnabled, suspendedResources);
		}

		if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW) {
			if (debugEnabled) {
				logger.debug("Suspending current transaction, creating new transaction with name [" +
						definition.getName() + "]");
			}
			SuspendedResourcesHolder suspendedResources = suspend(transaction);
			try {
				boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
				DefaultTransactionStatus status = newTransactionStatus(
						definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
				doBegin(transaction, definition);
				prepareSynchronization(status, definition);
				return status;
			}
			catch (RuntimeException beginEx) {
				resumeAfterBeginException(transaction, suspendedResources, beginEx);
				throw beginEx;
			}
			catch (Error beginErr) {
				resumeAfterBeginException(transaction, suspendedResources, beginErr);
				throw beginErr;
			}
		}

		if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
			if (!isNestedTransactionAllowed()) {
				throw new NestedTransactionNotSupportedException(
						"Transaction manager does not allow nested transactions by default - " +
						"specify 'nestedTransactionAllowed' property with value 'true'");
			}
			if (debugEnabled) {
				logger.debug("Creating nested transaction with name [" + definition.getName() + "]");
			}
			if (useSavepointForNestedTransaction()) {
				// Create savepoint within existing Spring-managed transaction,
				// through the SavepointManager API implemented by TransactionStatus.
				// Usually uses JDBC 3.0 savepoints. Never activates Spring synchronization.
				DefaultTransactionStatus status =
						prepareTransactionStatus(definition, transaction, false, false, debugEnabled, null);
				status.createAndHoldSavepoint();
				return status;
			}
			else {
				// Nested transaction through nested begin and commit/rollback calls.
				// Usually only for JTA: Spring synchronization might get activated here
				// in case of a pre-existing JTA transaction.
				boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
				DefaultTransactionStatus status = newTransactionStatus(
						definition, transaction, true, newSynchronization, debugEnabled, null);
				doBegin(transaction, definition);
				prepareSynchronization(status, definition);
				return status;
			}
		}

		// Assumably PROPAGATION_SUPPORTS or PROPAGATION_REQUIRED.
		if (debugEnabled) {
			logger.debug("Participating in existing transaction");
		}
		if (isValidateExistingTransaction()) {
			if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) {
				Integer currentIsolationLevel = TransactionSynchronizationManager.getCurrentTransactionIsolationLevel();
				if (currentIsolationLevel == null || currentIsolationLevel != definition.getIsolationLevel()) {
					Constants isoConstants = DefaultTransactionDefinition.constants;
					throw new IllegalTransactionStateException("Participating transaction with definition [" +
							definition + "] specifies isolation level which is incompatible with existing transaction: " +
							(currentIsolationLevel != null ?
									isoConstants.toCode(currentIsolationLevel, DefaultTransactionDefinition.PREFIX_ISOLATION) :
									"(unknown)"));
				}
			}
			if (!definition.isReadOnly()) {
				if (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
					throw new IllegalTransactionStateException("Participating transaction with definition [" +
							definition + "] is not marked as read-only but existing transaction is");
				}
			}
		}
		boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
		return prepareTransactionStatus(definition, transaction, false, newSynchronization, debugEnabled, null);
	}

可以看到getTransaction方法首先是从子类中获取transaction对象,并然后分为两种情况以及不同的传播行为进行了不同的操作,下面表格则列出了在当前有transaction和没有transaction的情况下每种传播行为其对应的不同操作。

已经有transaction的情况下
名称 创建新的Transaction 创建savePoint 报错 挂起当前transaction
Required
Requires_new
Supports
nested
never
not_supported
mandatory
没有transaction的情况下
名称 创建新的Transactiton 报错 挂起
required
requeires_new
supports
not_supported
nested
never
mandatory

commit 和rollback

private void processCommit(DefaultTransactionStatus status) throws TransactionException {
		try {
			boolean beforeCompletionInvoked = false;
			try {
				prepareForCommit(status);
				triggerBeforeCommit(status);
				triggerBeforeCompletion(status);
				beforeCompletionInvoked = true;
				boolean globalRollbackOnly = false;
				if (status.isNewTransaction() || isFailEarlyOnGlobalRollbackOnly()) {
					globalRollbackOnly = status.isGlobalRollbackOnly();
				}
				if (status.hasSavepoint()) {
					if (status.isDebug()) {
						logger.debug("Releasing transaction savepoint");
					}
					status.releaseHeldSavepoint();
				}
				else if (status.isNewTransaction()) {
					if (status.isDebug()) {
						logger.debug("Initiating transaction commit");
					}
					doCommit(status);
				}
				// Throw UnexpectedRollbackException if we have a global rollback-only
				// marker but still didn't get a corresponding exception from commit.
				if (globalRollbackOnly) {
					throw new UnexpectedRollbackException(
							"Transaction silently rolled back because it has been marked as rollback-only");
				}
			}
			catch (UnexpectedRollbackException ex) {
				// can only be caused by doCommit
				triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK);
				throw ex;
			}
			catch (TransactionException ex) {
				// can only be caused by doCommit
				if (isRollbackOnCommitFailure()) {
					doRollbackOnCommitException(status, ex);
				}
				else {
					triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN);
				}
				throw ex;
			}
			catch (RuntimeException ex) {
				if (!beforeCompletionInvoked) {
					triggerBeforeCompletion(status);
				}
				doRollbackOnCommitException(status, ex);
				throw ex;
			}
			catch (Error err) {
				if (!beforeCompletionInvoked) {
					triggerBeforeCompletion(status);
				}
				doRollbackOnCommitException(status, err);
				throw err;
			}

			// Trigger afterCommit callbacks, with an exception thrown there
			// propagated to callers but the transaction still considered as committed.
			try {
				triggerAfterCommit(status);
			}
			finally {
				triggerAfterCompletion(status, TransactionSynchronization.STATUS_COMMITTED);
			}

		}
		finally {
			cleanupAfterCompletion(status);
		}
	}
private void processRollback(DefaultTransactionStatus status) {
		try {
			try {
				triggerBeforeCompletion(status);
				if (status.hasSavepoint()) {
					if (status.isDebug()) {
						logger.debug("Rolling back transaction to savepoint");
					}
					status.rollbackToHeldSavepoint();
				}
				else if (status.isNewTransaction()) {
					if (status.isDebug()) {
						logger.debug("Initiating transaction rollback");
					}
					doRollback(status);
				}
				else if (status.hasTransaction()) {
					if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure()) {
						if (status.isDebug()) {
							logger.debug("Participating transaction failed - marking existing transaction as rollback-only");
						}
						doSetRollbackOnly(status);
					}
					else {
						if (status.isDebug()) {
							logger.debug("Participating transaction failed - letting transaction originator decide on rollback");
						}
					}
				}
				else {
					logger.debug("Should roll back transaction but cannot - no transaction available");
				}
			}
			catch (RuntimeException ex) {
				triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN);
				throw ex;
			}
			catch (Error err) {
				triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN);
				throw err;
			}
			triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK);
		}
		finally {
			cleanupAfterCompletion(status);
		}
	}

可以看到commit和rollback的主要操作时看有没有savePoint和是否为newTransaction,对于commit操作,可以看到当有savePoint,其最终只是清除当前的savePoint,并且只有当前的transaction是newTransaction(对于DatasourcePlatformMananager即为创建connection的方法)才进行真正的commit的操作。对于rollback则是对于有savePoint则是回滚到savePoint,也只是newTransaction进行真正的rollback操作,其他情况则是将其标记为rollback等待创建这个transaction的方法进行真正的rollback操作。

DataSourceTransactionManager和TransactionSynchronizationManager

DataSourceTransactionManager是继承了AbstractPlatformTransactionManager,是我们常用的对于事务配置用的platformTransactionManager。
而TransactionSynchronizationManager则是一个维护了一系列ThreadLocal的将数据绑定到线程的类。
DataSourcetransactionManager将当前线程下的连接以ConnectionHolder包装起来放入TransactionSynchronizationManager中ThreadLocal中的资源对象中,在代码执行中则是直接从当前TransactionSynchronizationManager中获取对应的connection执行相应的sql,而DataSourceTransactionManager则是根据相关的逻辑最后调用connection的rollback或者commit操作。

关于事务传播行为的测试

下面代码是对事务传播行为的对应的测试。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import com.test.spring.dao.IUpdateDao;

@Component
public class TestService {
	
	@Autowired
	private IUpdateDao updateDao ; 
	
	@Autowired 
	private TestService1 ts1 ; 
	
	@Autowired
	private TestService2 ts2 ; 

	@Transactional()
	public void doUpdate() {
		ts1.doUpdate1();
		try {			
			ts2.doUpdate2();
		}catch(Exception e) {
			e.printStackTrace();
		}
	}
	@Transactional()
	public void doUpdate1() {
		updateDao.update("update test set num = num + 100000 where id = 1") ; 
		 
	}
	@Transactional
	public void doUpdate2() {
		updateDao.update("update test set num = num + 1 where id = 1") ; 
	}

}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import com.test.spring.dao.IUpdateDao;

@Component
public class TestService1 {
	
	@Autowired
	private IUpdateDao updateDao ; 
	
	@Transactional(propagation=Propagation.REQUIRED)
	public void doUpdate1() {
		updateDao.update("update test set num = num + 100000 where id = 1") ; 
	}

}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import com.test.spring.dao.IUpdateDao;

@Component
public class TestService2 {
	@Autowired
	private IUpdateDao updateDao ; 
	@Transactional(propagation = Propagation.REQUIRED)
	public void doUpdate2() {
		updateDao.update("update test set num = num + 1 where id = 1") ; 
		int t = 1/0 ;
	}

}

import java.sql.Connection;
import javax.sql.DataSource;
import org.springframework.jdbc.datasource.ConnectionHolder;
import org.springframework.transaction.support.TransactionSynchronizationManager;

public class IUpdateDao {
	
	private DataSource ds ; 
	
	public DataSource getDs() {
		return ds;
	}

	public void setDs(DataSource ds) {
		this.ds = ds;
	}


	public boolean update(String sql) {
		ConnectionHolder conHolder =
				(ConnectionHolder)TransactionSynchronizationManager.getResource(ds) ; 
		Connection connection = conHolder.getConnection() ; 
		try {
			return  connection.prepareStatement(sql).execute() ; 
		}catch(Exception e) {
			e.printStackTrace();
		}
		return false ; 
	}
	
}

import org.springframework.context.support.ClassPathXmlApplicationContext;

import com.test.spring.service.TestService;

public class TestTx {
	
	public static void main(String[] args) {
		ClassPathXmlApplicationContext application = new ClassPathXmlApplicationContext("classpath:spring-tx.xml") ; 
		application.refresh();
		TestService test = (TestService)application.getBean(TestService.class) ; 
		test.doUpdate();
	}

}

可以看到代码的逻辑其实很简单,就是调用TestService的doUpdate方法,而TestService的doUpdate调用了TestService1的doUpdate1和TestService2的doUpdate2方法。doUpdate1方法是对数据库中表test中id为1的数据加100000,而doUpdate2则是对同一行数据加1。其中doUpdate,doUpdate1和doUpdate2方法都加入了@Transactional注解。表示这三个方法都会进到TransactionInterceptor的对应的处理的方法中。其中doUpdate2方法中由于有1/0必定会抛出异常。
注意:这里没有用TestService的doUpdate1和doUpdate2方法,其主要原因是spring的Aop代理最终使用其原生的bean调用的最终的方法,即为同一个类中方法调用自身类的对应方法是不会进行代理的,即doUpdate如果调用的是自身类的doUpdate1和doUpdate2方法则doUpdate1和doUpdate2是没有经过代理的方法

假设num的初始值为0,下面分几种情况来看一下这个项目的结果:

1.doUpdate的传播行为为Required,doUpdate1上是Required,doUpdate2上是Required

其结果为0。
分析一下是doUpdate上创建了connection,doUpdate1正确执行,但是其commit操作时不是其创建的connection,即其TransactionStatus不是newTransaction,则其commit没有进行操作,而在doUpdate2中由于其有异常,所以回滚了,但是其也不是newTransaction,则只是将本身标记为rollback,最终在doUpdate中进行commit时发现有rollback标记时则将当前的connection进行rollback,将doUpdate1中的一起回滚了,故而数据没有发生变化。

2.doUpdate–>Requeired;doUpdate1–>Requeired;doUpdate2–>Requires-New

其结果为100000。
分析一下是doUpdate创建了一个connection为conn1,doUpdate1利用这个conn1更新了数据但是没有commit,而doUpdate2上市Requires_new,则会将当前的conn1挂起重新创建一个connection为conn2,这个conn2也会更新id为1这一行数据,但是由于conn1中的更新还没有提交,则doUpdate2中的conn1的提交,而conn1的提交是在doUpdate2方法执行之后的,因此会造成死锁,但是由于mysql设置了锁最长等待时间,则doUpdate2在等待一段时间会因报错而出来,最终conn1正确提交,结果为100000。

3.doUpdate–>Requeired;doUpdate1–>Requeired;doUpdate2–>Supports

其结果为0
其原因和1差不多,doUpdate2中设置了rollback标志让connect直接回滚。

4.doUpdate–>Requeired;doUpdate1–>Requeired;doUpdate2–>Not_supported

其结果为100000
其原因是doUpdate创建了connection,在doUpdate1中运行了sql,而doUpdate2由于not_supported挂起了当前的事务,其在IUpdateDao中的TransactionSynchronizationManager无法获取connection,故此无法执行sql,会报错,但是其由于没有事务,也不会进行回滚等操作,doUpdate1的写操作最终会成功。

5.doUpdate–>Requeired;doUpdate1–>Requeired;doUpdate2–>Nested

其结果为100000
原因是doUpdate创建了connection,doUpdate1中正常运行,doUpdate2没有新建connection,但是在执行之前创建了一个savePoint,在其最终回滚时其实是回滚到savePoint,即doUpdate1执行之后的结果,doUpdate正确执行commit后doUpdate1的写成功了。

6.doUpdate–>Requeired;doUpdate1–>Requeired;doUpdate2–>Never

结果为100000
doUpdate2直接抛出异常,但是没进行操作,但是doUpdate是正确commit,故而是100000

7.doUpdate–>Requeired;doUpdate1–>Requeired;doUpdate2–>MANDATORY

结果为0
doUpdate2和doUpdate1共用一个connection,而且也没有创建savePoint,回滚会直接将所有的回滚。

你可能感兴趣的:(spring学习笔记)