MyBatis原理系列(一)-手把手帶你閱讀MyBatis源碼
MyBatis原理系列(二)-手把手帶你了解MyBatis的啟動(dòng)流程
MyBatis原理系列(三)-手把手帶你了解SqlSession,SqlSessionFactory,SqlSessionFactoryBuilder的關(guān)系
MyBatis原理系列(四)-手把手帶你了解MyBatis的Executor執(zhí)行器
MyBatis原理系列(五)-手把手帶你了解Statement、StatementHandler、MappedStatement間的關(guān)系
MyBatis原理系列(六)-手把手帶你了解BoundSql的創(chuàng)建過程
MyBatis原理系列(七)-手把手帶你了解如何自定義插件
MyBatis原理系列(八)-手把手帶你了解一級(jí)緩存和二級(jí)緩存
MyBatis原理系列(九)-手把手帶你了解MyBatis事務(wù)管理機(jī)制
在上篇文章中寡喝,我們舉了一個(gè)例子如何使用MyBatis,但是對(duì)其中dao層历造,entity層,mapper層間的關(guān)系不得而知勿锅,從此篇文章開始帕膜,筆者將從MyBatis的啟動(dòng)流程著手,真正的開始研究MyBatis源碼了溢十。
1. MyBatis啟動(dòng)代碼示例
在上篇文章中垮刹,介紹了MyBatis的相關(guān)配置和各層代碼編寫,本文將以下代碼展開描述和介紹MyBatis的啟動(dòng)流程张弛,并簡(jiǎn)略的介紹各個(gè)模塊的作用荒典,各個(gè)模塊的細(xì)節(jié)部分將在其它文章中呈現(xiàn)。
回顧下上文中使用mybatis的部分代碼吞鸭,包括七步寺董。每步雖然都是一行代碼,但是隱藏了很多細(xì)節(jié)刻剥。接下來我們將圍繞這起步展開了解遮咖。
@Slf4j
public class MyBatisBootStrap {
public static void main(String[] args) {
try {
// 1. 讀取配置
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
// 2. 創(chuàng)建SqlSessionFactory工廠
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 3. 獲取sqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
// 4. 獲取Mapper
TTestUserMapper userMapper = sqlSession.getMapper(TTestUserMapper.class);
// 5. 執(zhí)行接口方法
TTestUser userInfo = userMapper.selectByPrimaryKey(16L);
System.out.println("userInfo = " + JSONUtil.toJsonStr(userInfo));
// 6. 提交事物
sqlSession.commit();
// 7. 關(guān)閉資源
sqlSession.close();
inputStream.close();
} catch (Exception e){
log.error(e.getMessage(), e);
}
}
}
2. 讀取配置
// 1. 讀取配置
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
在mybatis-config.xml中我們配置了屬性,環(huán)境造虏,映射文件路徑等御吞,其實(shí)不僅可以配置以上內(nèi)容,還可以配置插件漓藕,反射工廠陶珠,類型處理器等等其它內(nèi)容。在啟動(dòng)流程中的第一步我們就需要讀取這個(gè)配置文件享钞,并獲取一個(gè)輸入流為下一步解析配置文件作準(zhǔn)備揍诽。
mybatis-config.xml 內(nèi)容如下
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!--一些重要的全局配置-->
<settings>
<setting name="cacheEnabled" value="true"/>
<!--<setting name="lazyLoadingEnabled" value="true"/>-->
<!--<setting name="multipleResultSetsEnabled" value="true"/>-->
<!--<setting name="useColumnLabel" value="true"/>-->
<!--<setting name="useGeneratedKeys" value="false"/>-->
<!--<setting name="autoMappingBehavior" value="PARTIAL"/>-->
<!--<setting name="autoMappingUnknownColumnBehavior" value="WARNING"/>-->
<!--<setting name="defaultExecutorType" value="SIMPLE"/>-->
<!--<setting name="defaultStatementTimeout" value="25"/>-->
<!--<setting name="defaultFetchSize" value="100"/>-->
<!--<setting name="safeRowBoundsEnabled" value="false"/>-->
<!--<setting name="mapUnderscoreToCamelCase" value="false"/>-->
<!--<setting name="localCacheScope" value="STATEMENT"/>-->
<!--<setting name="jdbcTypeForNull" value="OTHER"/>-->
<!--<setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>-->
<!--<setting name="logImpl" value="STDOUT_LOGGING" />-->
</settings>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://10.255.0.50:3306/volvo_bev?useUnicode=true"/>
<property name="username" value="appdev"/>
<property name="password" value="FEGwo3EzsdDYS9ooYKGCjRQepkwG"/>
</dataSource>
</environment>
</environments>
<mappers>
<!--這邊可以使用package和resource兩種方式加載mapper-->
<!--<package name="包名"/>-->
<!--<mapper resource="./mappers/SysUserMapper.xml"/>
<package name="com.example.demo.dao"/> -->
<mapper resource="./mapper/TTestUserMapper.xml"/>
</mappers>
</configuration>
3. 創(chuàng)建SqlSessionFactory工廠
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
我們?cè)趯W(xué)習(xí)Java的設(shè)計(jì)模式時(shí),會(huì)學(xué)到工廠模式栗竖,工廠模式又分為簡(jiǎn)單工廠模式暑脆,工廠方法模式,抽象工廠模式等等狐肢。工廠模式就是為了創(chuàng)建對(duì)象提供接口添吗,并將創(chuàng)建對(duì)象的具體細(xì)節(jié)屏蔽起來,從而可以提高靈活性处坪。
public interface SqlSessionFactory {
SqlSession openSession();
SqlSession openSession(boolean autoCommit);
SqlSession openSession(Connection connection);
SqlSession openSession(TransactionIsolationLevel level);
SqlSession openSession(ExecutorType execType);
SqlSession openSession(ExecutorType execType, boolean autoCommit);
SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);
SqlSession openSession(ExecutorType execType, Connection connection);
Configuration getConfiguration();
}
由此可知SqlSessionFactory工廠是為了創(chuàng)建一個(gè)對(duì)象而生的根资,其產(chǎn)出的對(duì)象就是SqlSession對(duì)象。SqlSession是MyBatis面向數(shù)據(jù)庫的高級(jí)接口同窘,其提供了執(zhí)行查詢sql玄帕,更新sql,提交事物想邦,回滾事物裤纹,獲取映射代理類等等方法。
在此筆者列出了主要方法丧没,一些重載的方法就過濾掉了鹰椒。
public interface SqlSession extends Closeable {
/**
* 查詢一個(gè)結(jié)果對(duì)象
**/
<T> T selectOne(String statement, Object parameter);
/**
* 查詢一個(gè)結(jié)果集合
**/
<E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds);
/**
* 查詢一個(gè)map
**/
<K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey, RowBounds rowBounds);
/**
* 查詢游標(biāo)
**/
<T> Cursor<T> selectCursor(String statement, Object parameter, RowBounds rowBounds);
void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler);
/**
* 插入
**/
int insert(String statement, Object parameter);
/**
* 修改
**/
int update(String statement, Object parameter);
/**
* 刪除
**/
int delete(String statement, Object parameter);
/**
* 提交事物
**/
void commit(boolean force);
/**
* 回滾事物
**/
void rollback(boolean force);
List<BatchResult> flushStatements();
void close();
void clearCache();
Configuration getConfiguration();
/**
* 獲取映射代理類
**/
<T> T getMapper(Class<T> type);
/**
* 獲取數(shù)據(jù)庫連接
**/
Connection getConnection();
}
回到開始,SqlSessionFactory工廠是怎么創(chuàng)建的出來的呢呕童?SqlSessionFactoryBuilder就是創(chuàng)建者漆际,以Builder結(jié)尾我們很容易想到了Java設(shè)計(jì)模式中的建造者模式,一個(gè)對(duì)象的創(chuàng)建是由眾多復(fù)雜對(duì)象組成的夺饲,建造者模式就是一個(gè)創(chuàng)建復(fù)雜對(duì)象的選擇奸汇,它與工廠模式相比,建造者模式更加關(guān)注零件裝配的順序往声。
public class SqlSessionFactoryBuilder {
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
}
其中XMLConfigBuilder就是解析mybatis-config.xml中每個(gè)標(biāo)簽的內(nèi)容擂找,parse()方法返回的就是一個(gè)Configuration對(duì)象.Configuration也是MyBatis中一個(gè)很重要的組件,包括插件浩销,對(duì)象工廠贯涎,反射工廠,映射文件慢洋,類型解析器等等都存儲(chǔ)在Configuration對(duì)象中塘雳。
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
private void parseConfiguration(XNode root) {
try {
// issue #117 read properties first
// 解析properties節(jié)點(diǎn)
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
在獲取到Configuration對(duì)象后,SqlSessionFactoryBuilder就會(huì)創(chuàng)建一個(gè)DefaultSqlSessionFactory對(duì)象且警,DefaultSqlSessionFactory是SqlSessionFactory的一個(gè)默認(rèn)實(shí)現(xiàn)粉捻,還有一個(gè)實(shí)現(xiàn)是SqlSessionManager。
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
4. 獲取sqlSession
// 3. 獲取sqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
在前面我們講到斑芜,sqlSession是操作數(shù)據(jù)庫的高級(jí)接口肩刃,我們操作數(shù)據(jù)庫都是通過這個(gè)接口操作的。獲取sqlSession有兩種方式杏头,一種是從數(shù)據(jù)源中獲取的盈包,還有一種是從連接中獲取。
獲取到的都是DefaultSqlSession對(duì)象醇王,也就是sqlSession的默認(rèn)實(shí)現(xiàn)呢燥。
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
private SqlSession openSessionFromConnection(ExecutorType execType, Connection connection) {
try {
boolean autoCommit;
try {
autoCommit = connection.getAutoCommit();
} catch (SQLException e) {
// Failover to true, as most poor drivers
// or databases won't support transactions
autoCommit = true;
}
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
final Transaction tx = transactionFactory.newTransaction(connection);
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
5. 獲取Mapper代理類
在上一步獲取到sqlSession后,我們接下來就獲取到了mapper代理類寓娩。
// 4. 獲取Mapper
TTestUserMapper userMapper = sqlSession.getMapper(TTestUserMapper.class);
這個(gè)getMapper方法叛氨,我們看看DefaultSqlSession是怎么做的
DefaultSqlSession 的 getMapper 方法
public <T> T getMapper(Class<T> type) {
return this.configuration.getMapper(type, this);
}
Configuration 的 getMapper 方法
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return this.mapperRegistry.getMapper(type, sqlSession);
}
MapperRegistry 中有個(gè)getMapper方法呼渣,實(shí)際上是從成員變量knownMappers中獲取的,這個(gè)knownMappers是個(gè)key-value形式的緩存寞埠,key是mapper接口的class對(duì)象屁置,value是MapperProxyFactory代理工廠,這個(gè)工廠就是用來創(chuàng)建MapperProxy代理類的仁连。
public class MapperRegistry {
private final Configuration config;
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap();
public MapperRegistry(Configuration config) {
this.config = config;
}
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory)this.knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
} else {
try {
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception var5) {
throw new BindingException("Error getting mapper instance. Cause: " + var5, var5);
}
}
}
}
如果對(duì)java動(dòng)態(tài)代理了解的同學(xué)就知道蓝角,Proxy.newProxyInstance()方法可以創(chuàng)建出一個(gè)目標(biāo)對(duì)象一個(gè)代理對(duì)象。由此可知每次調(diào)用getMapper方法都會(huì)創(chuàng)建出一個(gè)代理類出來饭冬。
public class MapperProxyFactory<T> {
private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap();
public MapperProxyFactory(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
public Class<T> getMapperInterface() {
return this.mapperInterface;
}
public Map<Method, MapperMethod> getMethodCache() {
return this.methodCache;
}
protected T newInstance(MapperProxy<T> mapperProxy) {
return Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{this.mapperInterface}, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
MapperProxy<T> mapperProxy = new MapperProxy(sqlSession, this.mapperInterface, this.methodCache);
return this.newInstance(mapperProxy);
}
}
回到上面使鹅,那這個(gè)MapperProxyFactory是怎么加載到MapperRegistry的knownMappers緩存中的呢?
在上面的Configuration類的parseConfiguration方法中昌抠,我們會(huì)解析 mappers標(biāo)簽患朱,mapperElement方法就會(huì)解析mapper接口。
private void parseConfiguration(XNode root) {
try {
// issue #117 read properties first
// 解析properties節(jié)點(diǎn)
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
解析完后炊苫,就講這個(gè)mapper接口加到 mapperRegistry中麦乞,
configuration.addMapper(mapperInterface);
Configuration的addMapper方法
public <T> void addMapper(Class<T> type) {
mapperRegistry.addMapper(type);
}
最后還是加載到了MapperRegistry的knownMappers中去了
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
knownMappers.put(type, new MapperProxyFactory<>(type));
// It's important that the type is added before the parser is run
// otherwise the binding may automatically be attempted by the
// mapper parser. If the type is already known, it won't try.
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
6. 執(zhí)行mapper接口方法
// 5. 執(zhí)行接口方法
TTestUser userInfo = userMapper.selectByPrimaryKey(16L);
selectByPrimaryKey是TTestUserMapper接口中定義的一個(gè)方法,但是我們沒有編寫TTestUserMapper接口的的實(shí)現(xiàn)類劝评,那么Mybatis是怎么幫我們執(zhí)行的呢姐直?前面講到,獲取mapper對(duì)象時(shí)蒋畜,是會(huì)獲取到一個(gè)MapperProxyFactory工廠類声畏,并創(chuàng)建一個(gè)MapperProxy代理類,在執(zhí)行Mapper接口的方法時(shí)姻成,會(huì)調(diào)用MapperProxy的invoke方法插龄。
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else {
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
如果是Object的方法就直接執(zhí)行,否則執(zhí)行cachedInvoker(method).invoke(proxy, method, args, sqlSession); 這行代碼科展,到這里均牢,想必有部分同學(xué)已經(jīng)頭暈了吧。怎么又來了個(gè)invoke方法才睹。
cachedInvoker 是返回緩存的MapperMethodInvoker對(duì)象徘跪,MapperMethodInvoker的invoke方法會(huì)執(zhí)行MapperMethod的execute方法。
public class MapperMethod {
private final SqlCommand command;
private final MethodSignature method;
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
this.command = new SqlCommand(config, mapperInterface, method);
this.method = new MethodSignature(config, mapperInterface, method);
}
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional()
&& (result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}
}
然后根據(jù)執(zhí)行的接口找到mapper.xml中配置的sql琅攘,并處理參數(shù)垮庐,然后執(zhí)行返回結(jié)果處理結(jié)果等步驟。
7. 提交事物
// 6. 提交事物
sqlSession.commit();
事物就是將若干數(shù)據(jù)庫操作看成一個(gè)單元坞琴,要么全部成功哨查,要么全部失敗,如果失敗了剧辐,則會(huì)執(zhí)行執(zhí)行回滾操作寒亥,恢復(fù)到開始執(zhí)行的數(shù)據(jù)庫狀態(tài)邮府。
8. 關(guān)閉資源
// 7. 關(guān)閉資源
sqlSession.close();
inputStream.close();
sqlSession是種共用資源,用完了要返回到池子中溉奕,以供其它地方使用挟纱。
9. 總結(jié)
至此我們已經(jīng)大致了解了Mybatis啟動(dòng)時(shí)的大致流程,很多細(xì)節(jié)都還沒有詳細(xì)介紹腐宋,這是因?yàn)樯婕暗降膶用嬗稚钣謴V,如果在一篇文章中介紹檀轨,反而會(huì)讓讀者如置云里霧里胸竞,不知所云。因此参萄,在接下來我將每個(gè)模塊的詳細(xì)介紹卫枝。如果文章有什么錯(cuò)誤或者需要改進(jìn)的,希望同學(xué)們指出來讹挎,希望對(duì)大家有幫助校赤。