Spring事務專題(四)Spring中事務的使用、抽象機制及模擬Spring事務實現
Spring中事務的使用示例、屬性及使用中可能出現的問題
前言
本專題大綱如下:

對於專題大綱我又做了調整哈,主要是希望專題的內容能夠更豐富,更加詳細,本來是想在源碼分析的文章中附帶講一講事務使用中的問題,這兩天想了想還是單獨寫一篇並作為事務專題的收尾篇,也是我Spring源碼專題的收尾篇。
本文大綱如下:

在看這篇文章,以及下篇源碼分析的文章我希望你對Spring AOP以及有充分的了解,不然一些細節問題你可能看不明白,關於Spring AOP如果你能看完這三篇文章基本上就沒什麼問題了
Spring中AOP相關的API及源碼解析,原來AOP是這樣子的
你知道Spring是怎麼將AOP應用到Bean的生命周期中的嗎?
編程式事務
Spring提供了兩種編程式事務管理的方法
- 使用
TransactionTemplate或者TransactionalOperator. - 直接實現
TransactionManager介面
如果是使用的是命令式編程,Spring推薦使用TransactionTemplate 來完成編程式事務管理,如果是響應式編程,那麼使用TransactionalOperator更加合適。
TransactionTemplate
使用示例(我這裡直接用的官網提供的例子了)
public class SimpleService implements Service {
private final TransactionTemplate transactionTemplate;
// 使用構造對transactionTemplate進行初始化
// 需要提供一個transactionManager
public SimpleService(PlatformTransactionManager transactionManager) {
this.transactionTemplate = new TransactionTemplate(transactionManager);
}
public Object someServiceMethod() {
return transactionTemplate.execute(new TransactionCallback() {
public Object doInTransaction(TransactionStatus status) {
// 這裡實現自己的相關業務邏輯
updateOperation1();
return resultOfUpdateOperation2();
}
});
}
}
在上面的例子中,我們顯示的使用了TransactionTemplate來完成事務管理,通過實現TransactionCallback介面並在其doInTransaction方法中完成了我們對業務的處理。我們可以大概看下TransactionTemplate的execute方法的實現:
public <T> T execute(TransactionCallback<T> action) throws TransactionException {
Assert.state(this.transactionManager != null, "No PlatformTransactionManager set");
if (this.transactionManager instanceof CallbackPreferringPlatformTransactionManager) {
return ((CallbackPreferringPlatformTransactionManager) this.transactionManager).execute(this, action);
}
else {
// 1.通過事務管理器開啟事務
TransactionStatus status = this.transactionManager.getTransaction(this);
T result;
try {
// 2.執行傳入的業務邏輯
result = action.doInTransaction(status);
}
catch (RuntimeException | Error ex) {
// 3.出現異常,進行回滾
rollbackOnException(status, ex);
throw ex;
}
catch (Throwable ex) {
// 3.出現異常,進行回滾
rollbackOnException(status, ex);
throw new UndeclaredThrowableException(ex, "TransactionCallback threw undeclared checked exception");
}
// 4.正常執行完成的話,提交事務
this.transactionManager.commit(status);
return result;
}
}
這些方法具體的實現我們暫且不看,後續進行源碼分析時都會詳細介紹,之所以將這個程式碼貼出來是讓大家更好的理解TransactionTemplate的工作機制:實際上就是通過一個TransactionCallback封裝了業務邏輯,然後TransactionTemplate會在事務的上下文中調用。
在上面的例子中doInTransaction是有返回值的,而實際上有時候並不需要返回值,這種情況下,我們可以使用TransactionCallbackWithoutResult提代TransactionCallback。
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
protected void doInTransactionWithoutResult(TransactionStatus status) {
updateOperation1();
updateOperation2();
}
});
實際上我們還可以通過
TransactionTemplate指定事務的屬性,例如隔離級別、超時時間、傳播行為等等
TransactionTemplate是執行緒安全的,我們可以全局配置一個TransactionTemplate,然後所有的類都共享這個TransactionTemplate。但是,如果某個類需要特殊的事務配置,例如需要訂製隔離級別,那麼我們就有必要去創建不同的TransactionTemplate。
TransactionOperator
TransactionOperator適用於響應式編程的情況,這裡就不做詳細介紹了
TransactionManager
實際上TransactionTemplate內部也是使用TransactionManager來完成事務管理的,我們之前也看過它的execute方法的實現了,其實內部就是調用了TransactionManager的方法,實際上就是分為這麼幾步
- 開啟事務
- 執行業務邏輯
- 出現異常進行回滾
- 正常執行則提交事務
這裡我還是直接用官網給出的例子
// 定義事務
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setName("SomeTxName");
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
// txManager,事務管理器
// 通過事務管理器開啟一個事務
TransactionStatus status = txManager.getTransaction(def);
try {
// 完成自己的業務邏輯
}
catch (MyException ex) {
// 出現異常,進行回滾
txManager.rollback(status);
throw ex;
}
// 正常執行完成,提交事務
txManager.commit(status);
我們在後邊的源碼分析中其實重點分析的也就是TransactionManager的源碼。
申明式事務
在對編程式事務有一定了解之後我們會發現,編程式事務存在下面幾個問題:
- 我們的業務程式碼跟事務管理的程式碼混雜在一起。
- 每個需要事務管理的地方都需要寫重複的程式碼
如何解決呢?這就要用到申明式事務了,實現申明式事務一般有兩種方式
- 基於XML配置
- 基於註解
申明式事務事務的實現原理如下(圖片來源於官網):

實際上就是結合了APO自動代理跟事務相關API。通過開啟AOP自動代理並向容器中註冊了事務需要的通知(Transaction Advisor),在Transaction Advisor調用了事務相關API,其實內部也是調用了TransactionManager的方法。
基於XML配置這種方式就不講了,筆者近兩年時間沒用過XML配置,我們主要就看看通過註解方式來實現申明式事務。主要涉及到兩個核心註解
@EnableTransactionManagement@Transactional
@EnableTransactionManagement這個註解主要有兩個作用,其一是,開啟AOP自動代理,其二是,添加事務需要用到的通知(Transaction Advisor),如果你對AOP有一定了解的話那你應該知道一個Advisor實際上就是一個綁定了切點(Pointcut)的通知(Advice),通過@EnableTransactionManagement這個註解導入的Advisor所綁定的切點就是通過@Transactional來定義的。
申明式事務的例子我這裡就省去了,我相信沒幾個人不會用吧…..
Spring對事務的抽象
Spring事務抽象的關鍵就是事務策略的概念,事務策略是通過TransactionManager介面定義的。TransactionManager本身只是一個標記介面,它有兩個直接子介面
ReactiveTransactionManager,這個介面主要用於在響應式編程模型下,不是我們要討論的重點PlatformTransactionManager,命令式編程模型下我們使用這個介面。
關於響應式跟命令式編程都可以單獨寫一篇文章了,本文重點不是討論這兩種編程模型,可以認為平常我們使用的都是命令式編程
PlatformTransactionManager
PlatformTransactionManager介面定義
public interface PlatformTransactionManager extends TransactionManager {
// 開啟事務
TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
// 提交事務
void commit(TransactionStatus status) throws TransactionException;
// 回滾事務
void rollback(TransactionStatus status) throws TransactionException;
}
PlatformTransactionManager是命令式編程模型下Spring事務機制的中心介面,定義了完成一個事務必須的三個步驟,也就是說定義了事務實現的規範
- 開啟事務
- 提交事務
- 回滾事務
通常來說,我們不會直接實現這個介面,而是通過繼承AbstractPlatformTransactionManager,這個類是一個抽象類,主要用作事務管理的模板,這個抽象類已經實現了事務的傳播行為以及跟事務相關的同步管理。
回頭看介面中定義的三個方法,首先是開啟事務的方法,從方法簽名上來看,其作用就是通過一個TransactionDefinition來獲取一個TransactionStatus類型的對象。為了更好的理解Spring中事務的抽象我們有必要了解下這兩個介面
TransactionDefinition
介面定義如下:
public interface TransactionDefinition {
// 定義了7中事務的傳播機制
int PROPAGATION_REQUIRED = 0;
int PROPAGATION_SUPPORTS = 1;
int PROPAGATION_MANDATORY = 2;
int PROPAGATION_REQUIRES_NEW = 3;
int PROPAGATION_NOT_SUPPORTED = 4;
int PROPAGATION_NEVER = 5;
int PROPAGATION_NESTED = 6;
// 4種隔離級別,-1代表的是使用資料庫默認的隔離級別
// 比如在MySQL下,使用的就是ISOLATION_REPEATABLE_READ(可重複讀)
int ISOLATION_DEFAULT = -1;
int ISOLATION_READ_UNCOMMITTED = 1;
int ISOLATION_READ_COMMITTED = 2;
int ISOLATION_REPEATABLE_READ = 4;
int ISOLATION_SERIALIZABLE = 8;
// 事務的超時時間,默認不限制時間
int TIMEOUT_DEFAULT = -1;
// 提供了對上面三個屬性的get方法
default int getPropagationBehavior() {
return PROPAGATION_REQUIRED;
}
default int getIsolationLevel() {
return ISOLATION_DEFAULT;
}
default int getTimeout() {
return TIMEOUT_DEFAULT;
}
// 事務是否是只讀的,默認不是
default boolean isReadOnly() {
return false;
}
// 事務的名稱
@Nullable
default String getName() {
return null;
}
// 返回一個只讀的TransactionDefinition
// 只對屬性提供了getter方法,所有屬性都是介面中定義的默認值
static TransactionDefinition withDefaults() {
return StaticTransactionDefinition.INSTANCE;
}
}
從這個介面的名字上我們也能知道,它的主要完成了對事務定義的抽象,這些定義有些是資料庫層面本身就有的,例如隔離級別、是否只讀、超時時間、名稱。也有些是Spring賦予的,例如事務的傳播機制。Spring中一共定義了7種事務的傳播機制
- TransactionDefinition.PROPAGATION_REQUIRED:如果當前存在事務,則加入該事務;如果當前沒有事務,則創建一個新的事務。
- TransactionDefinition.PROPAGATION_REQUIRES_NEW:創建一個新的事務,如果當前存在事務,則把當前事務掛起。
- TransactionDefinition.PROPAGATION_SUPPORTS:如果當前存在事務,則加入該事務;如果當前沒有事務,則以非事務的方式繼續運行。
- TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事務方式運行,如果當前存在事務,則把當前事務掛起。
- TransactionDefinition.PROPAGATION_NEVER:以非事務方式運行,如果當前存在事務,則拋出異常。
- TransactionDefinition.PROPAGATION_MANDATORY:如果當前存在事務,則加入該事務;如果當前沒有事務,則拋出異常。
- TransactionDefinition.PROPAGATION_NESTED:如果當前存在事務,則創建一個事務作為當前事務的嵌套事務來運行;如果當前沒有事務,則該取值等價於TransactionDefinition.PROPAGATION_REQUIRED。
關於事務的傳播在源碼分析的文章中我會重點介紹,現在大家留個印象即可。
我們在使用申明式事務的時候,會通過@Transactional這個註解去申明某個方法需要進行事務管理,在@Transactional中可以定義事務的屬性,這些屬性實際上就會被封裝到一個TransactionDefinition中,當然封裝的時候肯定不是直接使用的介面,而是這個介面的一個實現類RuleBasedTransactionAttribute。RuleBasedTransactionAttribute,該類的繼承關係如下:

-
DefaultTransactionDefinition,實現了TransactionDefinition,並為其中的定義的屬性提供了默認值// 默認的傳播機製為required,沒有事務新建一個事務 // 有事務的話加入當前事務 private int propagationBehavior = PROPAGATION_REQUIRED; // 隔離級別跟資料庫默認的隔離級別一直 private int isolationLevel = ISOLATION_DEFAULT; // 默認為-1,不設置超時時間 private int timeout = TIMEOUT_DEFAULT; // 默認不是只讀的 private boolean readOnly = false; -
TransactionAttribute,擴展了“DefaultTransactionDefinition`,新增了兩個事務的屬性// 用於指定事務使用的事務管理器的名稱 String getQualifier(); // 指定在出現哪種異常時才進行回滾 boolean rollbackOn(Throwable ex); -
DefaultTransactionAttribute,繼承了DefaultTransactionDefinition,同時實現了TransactionAttribute介面,定義了默認的回滾異常// 拋出RuntimeException/Error才進行回滾 public boolean rollbackOn(Throwable ex) { return (ex instanceof RuntimeException || ex instanceof Error); } -
RuleBasedTransactionAttribute,@Transactional註解的rollbackFor等屬性就會被封裝到這個類中,允許程式設計師自己定義回滾的異常,如果沒有指定回滾的異常,默認拋出RuntimeException/Error才進行回滾
TransactionStatus
這個介面主要用於描述Spring事務的狀態,其繼承關係如下:

-
TransactionExecution,這個介面也是用於描述事務的狀態,TransactionStatus是在其上做的擴展,內部定義了以下幾個方法// 判斷當前事務是否是一個新的事務 // 不是一個新事務的話,那麼需要加入到已經存在的事務中 boolean isNewTransaction(); // 事務是否被標記成RollbackOnly // 如果被標記成了RollbackOnly,意味著事務只能被回滾 void setRollbackOnly(); boolean isRollbackOnly(); // 是否事務完成,回滾或提交都意味著事務完成了 boolean isCompleted(); -
SavepointManager,定義了管理保存點(Savepoint)的方法,隔離級別為NESTED時就是通過設置回滾點來實現的,內部定義了這麼幾個方法// 創建保存點 Object createSavepoint() throws TransactionException; // 回滾到指定保存點 void rollbackToSavepoint(Object savepoint) throws TransactionException; // 移除回滾點 void releaseSavepoint(Object savepoint) throws TransactionException; -
TransactionStatus,繼承了上面這些介面,額外提供了兩個方法//用於判斷當前事務是否設置了保存點 boolean hasSavepoint(); // 這個方法複寫了父介面Flushable中的方法 // 主要用於刷新會話 // 對於Hibernate/jpa而言就是調用了其session/entityManager的flush方法 void flush();
小總結:
通過上面的分析我們會發現,
TransactionDefinition的主要作用是給出一份事務屬性的定義,然後事務管理器根據給出的定義來創建事務,TransactionStatus主要是用來描述創建後的事務的狀態
在對TransactionDefinition跟TransactionStatus有一定了解後,我們再回到PlatformTransactionManager介面本身,PlatformTransactionManager作為事務管理器的基礎介面只是定義管理一個事務必須的三個方法:開啟事務,提交事務,回滾事務,介面僅僅是定義了規範而已,真正做事的還是要依賴它的實現類,所以我們來看看它的繼承關係
PlatformTransactionManager的實現類

-
AbstractPlatformTransactionManager,Spring提供的一個事務管理的基類,提供了事務管理的模板,實現了Spring事務管理的一個標準流程- 判斷當前是否已經存在一個事務
- 應用合適的事務傳播行為
- 在必要的時候掛起/恢復事務
- 提交時檢查事務是否被標記成為
rollback-only - 在回滾時做適當的修改(是執行真實的回滾/還是將事務標記成
rollback-only) - 觸發註冊的同步回調
-
在
AbstractPlatformTransactionManager提供了四個常見的子類,其說明如下
關於事務管理器的詳細程式碼分析放到下篇文章,本文對其有個大概了解即可。
Spring中事務的同步機制
Spring中事務相關的同步機制可以分為兩類
- 資源的同步
- 行為的同步
什麼是資源的同步呢?在一個事務中我們往往會一次執行多個SQL(如果是單條的SQL實際上沒有必要開啟事務),為了保證事務所有的SQL都能夠使用一個資料庫連接,這個時候我們需要將資料庫連接跟事務進行同步,這個時候資料庫連接就是跟這個事務同步的一個資源。
那什麼又是行為的同步呢?還是以資料庫連接為例子,在事務開啟之前我們需要先獲取一個資料庫連接,同樣的在事務提交時我們需要將連接關閉(不一定是真正的關閉,如果是連接池只是歸還到連接池中),這個時候關閉連接這個行為也需要跟事務進行同步
那麼Spring是如何來管理同步的呢?同樣的,Spring也提供了一個同步管理器TransactionSynchronizationManager,這是一個抽象類,其中所有的方法都是靜態的,並且所有的方法都是圍繞它所申明的幾個靜態常量欄位,如下:
// 這就是同步的資源,Spring就是使用這個完成了連接的同步
private static final ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<>("Transactional resources");
// TransactionSynchronization完成了行為的同步
// 關於TransactionSynchronization在後文進行分析
private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
new NamedThreadLocal<>("Transaction synchronizations");
// 事務的名稱
private static final ThreadLocal<String> currentTransactionName =
new NamedThreadLocal<>("Current transaction name");
// 事務是否被標記成只讀
private static final ThreadLocal<Boolean> currentTransactionReadOnly =
new NamedThreadLocal<>("Current transaction read-only status");
// 事物的隔離級別
private static final ThreadLocal<Integer> currentTransactionIsolationLevel =
new NamedThreadLocal<>("Current transaction isolation level");
// 是否真實開啟了事務
private static final ThreadLocal<Boolean> actualTransactionActive =
new NamedThreadLocal<>("Actual transaction active");
可以看到所有的同步都是通過ThreadLocal實現的,對於ThreadLocal本文不做詳細分析,如果對ThreadLocal還不了解的同學也沒有關係,對於本文而言你只需要知道ThreadLocal能將資源跟當前執行緒綁定即可,例如 ThreadLocal<Map<Object, Object>> resources這個屬性就代表要將一個map綁定到當前執行緒,它提供了set跟get方法,分別用於將屬性綁定到執行緒上以及獲取執行緒上綁定的屬性。
上面的幾個變數中除了synchronizations之外其餘的應該都很好理解,synchronizations中綁定的是一個TransactionSynchronization的集合,那麼這個TransactionSynchronization有什麼用呢?我們來看看它的介面定義
public interface TransactionSynchronization extends Flushable {
// 事務完成的狀態
// 0 提交
// 1 回滾
// 2 異常狀態,例如在事務執行時出現異常,然後回滾,回滾時又出現異常
// 就會被標記成狀態2
int STATUS_COMMITTED = 0;
int STATUS_ROLLED_BACK = 1;
int STATUS_UNKNOWN = 2;
// 我們綁定的這些TransactionSynchronization需要跟事務同步
// 1.如果事務掛起,我們需要將其掛起
// 2.如果事務恢復,我們需要將其恢復
default void suspend() {
}
default void resume() {
}
@Override
default void flush() {
}
// 在事務執行過程中,提供的一些回調方法
default void beforeCommit(boolean readOnly) {
}
default void beforeCompletion() {
}
default void afterCommit() {
}
default void afterCompletion(int status) {
}
}
可以看到這個介面就是定義了一些方法,這些個方法可以在事務達到不同階段後執行,可以認為定義了事務執行過程的一些回調行為,這就是我之前說的行為的同步。
模擬Spring事務的實現
本文的最後一部分希望大家模擬一下Spring事務的實現,我們利用現有的AOP來實現事務的管理。資料庫訪問我們直接使用jdbc,在模擬之前我們先明確兩點
- 切點應該如何定義?
- 通知要實現什麼功能?
我們先說第一個問題,因為是我們自己模擬,所以關於切點的定義我們就設置的盡量簡單一些,不妨就直接指定某個包下的所有類。對於第二個問題,我們也不做的過於複雜,在方法執行前開啟事務,在方法執行後提交事務並關閉連接,所以我們需要定義一個環繞通知。同時,我們也需要將連接跟事務同步,保證事務中的所有SQL共用一個事務是實現事務管理的必要條件。基於此,我們開始編寫程式碼
我們只需要引入Spring相關的依賴跟JDBC相關依賴即可,該項目僅僅是一個Spring環境下的Java項目,沒有Web依賴,也不是SpringBoot項目,項目結構如下:
POM文件:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="//maven.apache.org/POM/4.0.0"
xmlns:xsi="//www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="//maven.apache.org/POM/4.0.0 //maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.dmz.framework</groupId>
<artifactId>mybatis</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.15</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.2.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.5</version>
</dependency>
</dependencies>
</project>
配置類:
// 開啟AOP跟掃描組件即可
@EnableAspectJAutoProxy
@ComponentScan("com.dmz.mybatis.tx_demo")
public class Config {
}
完成事務管理的核心類:
public class TransactionUtil {
public static final ThreadLocal<Connection> synchronousConnection =
new ThreadLocal<Connection>();
private TransactionUtil() {
}
public static Connection startTransaction() {
Connection connection = synchronousConnection.get();
if (connection == null) {
try {
// 這裡替換成你自己的連接地址即可
connection = DriverManager
.getConnection("jdbc:mysql://localhost:3306/test?serverTimezone=GMT%2B8", "root", "123");
synchronousConnection.set(connection);
connection.setAutoCommit(false);
} catch (SQLException e) {
e.printStackTrace();
}
}
return connection;
}
public static int execute(String sql, Object... args) {
Connection connection = startTransaction();
try (PreparedStatement preparedStatement = connection.prepareStatement(sql)) {
if (args != null) {
for (int i = 1; i < args.length + 1; i++) {
preparedStatement.setObject(i, args[i - 1]);
}
}
return preparedStatement.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
}
return 0;
}
public static void commit() {
try (Connection connection = synchronousConnection.get()) {
connection.commit();
synchronousConnection.remove();
} catch (SQLException e) {
e.printStackTrace();
}
}
public static void rollback() {
try (Connection connection = synchronousConnection.get()) {
connection.rollback();
synchronousConnection.remove();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
實際需要事務管理的類
@Component
public class UserService {
public void saveUser() {
TransactionUtil.execute
("INSERT INTO `test`.`user`(`id`, `name`) VALUES (?, ?)", 100, "dmz");
// 測試回滾
// throw new RuntimeException();
}
}
切面:
@Component
@Aspect
public class TxAspect {
@Pointcut("execution(public * com.dmz.mybatis.tx_demo.service..*.*(..))")
private void pointcut() {
}
@Around("pointcut()")
public Object around(JoinPoint joinPoint) throws Throwable {
// 在方法執行前開啟事務
TransactionUtil.startTransaction();
// 執行業務邏輯
Object proceed = null;
try {
ProceedingJoinPoint method = (ProceedingJoinPoint) joinPoint;
proceed = method.proceed();
} catch (Throwable throwable) {
// 出現異常進行回滾
TransactionUtil.rollback();
return proceed;
}
// 方法執行完成後提交事務
TransactionUtil.commit();
return proceed;
}
}
用於測試的主函數:
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext ac =
new AnnotationConfigApplicationContext(Config.class);
UserService userService = ac.getBean(UserService.class);
userService.saveUser();
}
}
具體的測試過程跟測試結果我就不放了,大家把程式碼拷貝過去自行測試就好了
總結
本文主要介紹了Spring中的事務相關內容,對Spring中的事務抽象機製做了介紹,主要是為了讓大家在接下來一篇源碼文章中能減輕負擔,希望大家可以根據自己理解動手模擬下Spring中事務的實現哦,當你自己去實現的時候肯定會碰到一系列的問題,然後帶著這些問題看源碼你才能知道Spring為什麼要做這些事情!
如果本文對你由幫助的話,記得點個贊吧!也歡迎關注我的公眾號,微信搜索:程式設計師DMZ,或者掃描下方二維碼,跟著我一起認認真真學Java,踏踏實實做一個coder。
我叫DMZ,一個在學習路上匍匐前行的小菜鳥!



