Mybatis-Plus Generate 原始碼分析
如果寫一個和Mybatis-Plus類似的程式碼生成框架,思路比較容易想到。核心的幾個步驟就是:
- 獲取資料庫表、欄位資訊;
- 新增相應的規則,補充額外的信心。
- 根據表、欄位資訊、新加的規則生成對應的程式碼;
Mybatis-Plus整個框架依賴於Spring、Mybatis、模板引擎(freemaker或者velocity)和日誌框架slf4j。
整個架構如下:
如上所示:
- core:是整個框架的核心。包含了對資料庫實體的反射提取,分析資料表的欄位的資訊,資料的CRUD
- support:定義了相關的介面
- generate:賦值對相關程式碼的生成。
本篇只針對Generate部分進行分析。
Generate (程式碼生成)
其使用方式為首先建立一個AutoGenerator
物件,此物件裡面包含了所有的相關的配置資訊,按照配置的不同型別組織為:
- 總配置ConfigBuilder,
會對下面的各個配置彙總。
- 注入配置InjectionConfig
- 資料來源配置DataSourceConfig
- 資料表配置StrategyConfig
- 包配置PackageConfig
- 模板配置TemplateConfig
- 全域性配置GlobalConfig
使用
AutoGenerator
生成程式碼的時候,外界設定好相關的配置類,然後賦值給AutoGenerator
相關屬性,最後呼叫execute
即可,非常簡單。但是各種各樣的配置非常多,如果沒有深入的去了解,很有可能不能充分利用這個框架。
Config(配置詳解)
ConfigBuilder
ConfigBuilder
會對所有的配置再一次封裝,比如對某些為null的配置設定為預設值,過引數進行過濾、驗證等等。各個配置都有了之後,呼叫對應的handler執行處理。整個處理過程一定要注意配置初始化的順序,不能打亂,比如最終的表生成策略依賴於模板配置、資料來源配置等等。
具體來講對應程式碼如下:
public ConfigBuilder(PackageConfig packageConfig,
DataSourceConfig dataSourceConfig,
StrategyConfig strategyConfig,
TemplateConfig template,
GlobalConfig globalConfig) {
// 全域性配置
if (null == globalConfig) {
this.globalConfig = new GlobalConfig();
} else {
this.globalConfig = globalConfig;
}
// 模板配置
if (null == template) {
this.template = new TemplateConfig();
} else {
this.template = template;
}
// 包配置
if (null == packageConfig) {
handlerPackage(this.template, this.globalConfig.getOutputDir(), new PackageConfig());
} else {
handlerPackage(this.template, this.globalConfig.getOutputDir(), packageConfig);
}
this.dataSourceConfig = dataSourceConfig;
handlerDataSource(dataSourceConfig);
// 策略配置
if (null == strategyConfig) {
this.strategyConfig = new StrategyConfig();
} else {
this.strategyConfig = strategyConfig;
}
handlerStrategy(this.strategyConfig);
}
這個類比較重要,對於各個配置的handle也是在其進行。比如獲取表屬性。
GlobalConfig
全域性配置主要是對於整個自定生成環境的配置。如目錄,開發人員名稱,是否使用基類,檔案命名等。具體來講包含有如下配置:
/**
* 生成檔案的輸出目錄【預設 D 盤根目錄】
*/
private String outputDir = "D://";
/**
* 是否覆蓋已有檔案
*/
private boolean fileOverride = false;
/**
* 是否開啟輸出目錄
*/
private boolean open = true;
/**
* 是否在xml中新增二級快取配置
*/
private boolean enableCache = true;
/**
* 開發人員
*/
private String author;
/**
* 開啟 Kotlin 模式
*/
private boolean kotlin = false;
/**
* 開啟 ActiveRecord 模式
*/
private boolean activeRecord = true;
/**
* 開啟 BaseResultMap
*/
private boolean baseResultMap = false;
/**
* 開啟 baseColumnList
*/
private boolean baseColumnList = false;
/**
* 各層檔名稱方式,例如: %Action 生成 UserAction
*/
private String mapperName;
private String xmlName;
private String serviceName;
private String serviceImplName;
private String controllerName;
/**
* 指定生成的主鍵的ID型別
*/
private IdType idType;
需要注意的幾個點:
生成檔案的輸出目錄因作業系統不同而不同。預設是Windows的D盤
生成的主鍵的ID型別有多種。
AUTO(0, "資料庫ID自增"), INPUT(1, "使用者輸入ID"), ID_WORKER(2, "全域性唯一ID"), UUID(3, "全域性唯一ID"), NONE(4, "該型別為未設定主鍵型別"), ID_WORKER_STR(5, "字串全域性唯一ID");
PackageConfig
包相關的配置,這個配置比較簡單。具體來講:
/**
* 父包名。如果為空,將下面子包名必須寫全部, 否則就只需寫子包名
*/
private String parent = "com.baomidou";
/**
* 父包模組名。
*/
private String moduleName = null;
/**
* Entity包名
*/
private String entity = "entity";
/**
* Service包名
*/
private String service = "service";
/**
* Service Impl包名
*/
private String serviceImpl = "service.impl";
/**
* Mapper包名
*/
private String mapper = "mapper";
/**
* Mapper XML包名
*/
private String xml = "mapper.xml";
/**
* Controller包名
*/
private String controller = "web";
其實也就是對最終生成的目錄結構的設定。一個簡單的例子:
TemplateConfig
模板配置類,主要是對生成程式碼檔案格式的配置。我們生成不用層次的類對應的模板是不同的,雖然可以通過字串的方式來實現具體的自動生成。但是使用模板技術更加簡單。對於每個層採用不同的模板:
private String entity = ConstVal.TEMPLATE_ENTITY_JAVA;
private String service = ConstVal.TEMPLATE_SERVICE;
private String serviceImpl = ConstVal.TEMPLATE_SERVICEIMPL;
private String mapper = ConstVal.TEMPLATE_MAPPER;
private String xml = ConstVal.TEMPLATE_XML;
private String controller = ConstVal.TEMPLATE_CONTROLLER;
這裡的ConstVal
裡面定義全域性常量。
以模板檔案mapper.java.vm為例:
package ${package.Mapper};
import ${package.Entity}.${entity};
import ${superMapperClassPackage};
/**
* <p>
* $!{table.comment} Mapper 介面
* </p>
*
* @author ${author}
* @since ${date}
*/
#if(${kotlin})
interface ${table.mapperName} : ${superMapperClass}<${entity}>
#else
public interface ${table.mapperName} extends ${superMapperClass}<${entity}> {
}
#end
其對應生成的程式碼
package com.baomidou.test.mapper;
import com.baomidou.test.entity.Permission;
import com.baomidou.mybatisplus.mapper.BaseMapper;
/**
* <p>
* 許可權表 Mapper 介面
* </p>
*
* @author Yanghu
* @since 2018-06-08
*/
public interface PermissionMapper extends BaseMapper<Permission> {
}
DataSourceConfig
資料庫配置主要就是定義好相關資料庫,使用者名稱,密碼等,便於連線到資料庫讀到想的表欄位。具體來講包含對如下資訊的配置:
/**
* 資料庫資訊查詢
*/
private IDbQuery dbQuery;
/**
* 資料庫型別
*/
private DbType dbType;
/**
* PostgreSQL schemaname
*/
private String schemaname = "public";
/**
* 型別轉換
*/
private ITypeConvert typeConvert;
/**
* 驅動連線的URL
*/
private String url;
/**
* 驅動名稱
*/
private String driverName;
/**
* 資料庫連線使用者名稱
*/
private String username;
/**
* 資料庫連線密碼
*/
private String password;
需要說明一下IDbQuery、ITypeConvert。
- IDbQuery是一個介面裡面對查詢資料庫表、欄位、註釋資訊的封裝。因為需要滿足多種資料庫的自動生成,所以需要正對不同資料庫實現IDbQuery不同的類。比如MySqlQuery就是對IDbQuery一種實現
@Override
public DbType dbType() {
return DbType.MYSQL;
}
@Override
public String tablesSql() {
return "show table status";
}
@Override
public String tableFieldsSql() {
return "show full fields from `%s`";
}
@Override
public String tableName() {
return "NAME";
}
@Override
public String tableComment() {
return "COMMENT";
}
@Override
public String fieldName() {
return "FIELD";
}
@Override
public String fieldType() {
return "TYPE";
}
@Override
public String fieldComment() {
return "COMMENT";
}
@Override
public String fieldKey() {
return "KEY";
}
@Override
public boolean isKeyIdentity(ResultSet results) throws SQLException {
return "auto_increment".equals(results.getString("Extra"));
}
- ITypeConvert介面是把資料庫中filed中的型別轉為java中對應的資料型別。具體來講MySqlTypeConvert實現如下:
@Override
public DbColumnType processTypeConvert(String fieldType) {
String t = fieldType.toLowerCase();
if (t.contains("char") || t.contains("text")) {
return DbColumnType.STRING;
} else if (t.contains("bigint")) {
return DbColumnType.LONG;
} else if (t.contains("int")) {
return DbColumnType.INTEGER;
} else if (t.contains("date") || t.contains("time") || t.contains("year")) {
return DbColumnType.DATE;
} else if (t.contains("text")) {
return DbColumnType.STRING;
} else if (t.contains("bit")) {
return DbColumnType.BOOLEAN;
} else if (t.contains("decimal")) {
return DbColumnType.BIG_DECIMAL;
} else if (t.contains("clob")) {
return DbColumnType.CLOB;
} else if (t.contains("blob")) {
return DbColumnType.BLOB;
} else if (t.contains("binary")) {
return DbColumnType.BYTE_ARRAY;
} else if (t.contains("float")) {
return DbColumnType.FLOAT;
} else if (t.contains("double")) {
return DbColumnType.DOUBLE;
} else if (t.contains("json") || t.contains("enum")) {
return DbColumnType.STRING;
}
return DbColumnType.STRING;
}
注意這裡為什麼用contains
作為判斷,因為mysql中可以設定資料型別具體大小。雖然大小不固定但是,其字首是固定。
- 其次還在DataSourceConfig中建立了資料庫連線物件提供給外界使用。
public Connection getConn() {
Connection conn = null;
try {
Class.forName(driverName);
conn = DriverManager.getConnection(url, username, password);
} catch (ClassNotFoundException | SQLException e) {
e.printStackTrace();
}
return conn;
}
StrategyConfig
對具體生成的表欄位進行配置。比如去掉哪些表字首,欄位字首;定義生成entity的公告欄位及相關基類;排除對哪些表自動生成,或者對哪些表自動生成;是否根據表生成對應的註釋。具體來講有如下設定:
/**
* 表名、欄位名、是否使用下劃線命名(預設 false)
*/
public static boolean DB_COLUMN_UNDERLINE = false;
/**
* 是否大寫命名
*/
private boolean isCapitalMode = false;
/**
* 是否跳過檢視
*/
private boolean skipView = false;
/**
* 資料庫表對映到實體的命名策略
*/
private NamingStrategy naming = NamingStrategy.nochange;
/**
* 資料庫表欄位對映到實體的命名策略<br/>
* 未指定按照 naming 執行
*/
private NamingStrategy columnNaming = null;
/**
* 表字首
*/
private String[] tablePrefix;
/**
* 欄位字首
*/
private String[] fieldPrefix;
/**
* 自定義繼承的Entity類全稱,帶包名
*/
private String superEntityClass;
/**
* 自定義基礎的Entity類,公共欄位
*/
private String[] superEntityColumns;
/**
* 自定義繼承的Mapper類全稱,帶包名
*/
private String superMapperClass = ConstVal.SUPERD_MAPPER_CLASS;
/**
* 自定義繼承的Service類全稱,帶包名
*/
private String superServiceClass = ConstVal.SUPERD_SERVICE_CLASS;
/**
* 自定義繼承的ServiceImpl類全稱,帶包名
*/
private String superServiceImplClass = ConstVal.SUPERD_SERVICEIMPL_CLASS;
/**
* 自定義繼承的Controller類全稱,帶包名
*/
private String superControllerClass;
/**
* 需要包含的表名(與exclude二選一配置)
*/
private String[] include = null;
/**
* 需要排除的表名
*/
private String[] exclude = null;
/**
* 【實體】是否生成欄位常量(預設 false)<br>
* -----------------------------------<br>
* public static final String ID = "test_id";
*/
private boolean entityColumnConstant = false;
/**
* 【實體】是否為構建者模型(預設 false)<br>
* -----------------------------------<br>
* public User setName(String name) { this.name = name; return this; }
*/
private boolean entityBuilderModel = false;
/**
* 【實體】是否為lombok模型(預設 false)<br>
* <a href="https://projectlombok.org/">document</a>
*/
private boolean entityLombokModel = false;
/**
* Boolean型別欄位是否移除is字首(預設 false)<br>
* 比如 : 資料庫欄位名稱 : 'is_xxx',型別為 : tinyint. 在對映實體的時候則會去掉is,在實體類中對映最終結果為 xxx
*/
private boolean entityBooleanColumnRemoveIsPrefix = false;
/**
* 生成 <code>@RestController</code> 控制器
* <pre>
* <code>@Controller</code> -> <code>@RestController</code>
* </pre>
*/
private boolean restControllerStyle = false;
/**
* 駝峰轉連字元
* <pre>
* <code>@RequestMapping("/managerUserActionHistory")</code> -> <code>@RequestMapping("/manager-user-action-history")</code>
* </pre>
*/
private boolean controllerMappingHyphenStyle = false;
/**
* 是否生成實體時,生成欄位註解
*/
private boolean entityTableFieldAnnotationEnable = false;
/**
* 樂觀鎖屬性名稱
*/
private String versionFieldName;
/**
* 邏輯刪除屬性名稱
*/
private String logicDeleteFieldName;
/**
* 表填充欄位
*/
private List<TableFill> tableFillList = null;
小結
相關配置介紹完了,可以看到每一個配置對應一個具體的層面。這樣的好處在於職責清晰。從程式碼層面上講也使用到了諸如觀面模式,策略模式等。
Handle(處理配置)
各個配置都設定好了之後就開始進行處理了。其實就呼叫了一個方法而已
public void execute() {
logger.debug("==========================準備生成檔案...==========================");
if(null == this.config) {
this.config = new ConfigBuilder(this.packageInfo, this.dataSource, this.strategy, this.template, this.globalConfig);
if(null != this.injectionConfig) {
this.injectionConfig.setConfig(this.config);
}
}
if(null == this.templateEngine) {
this.templateEngine = new VelocityTemplateEngine();
}
this.templateEngine.init(this.pretreatmentConfigBuilder(this.config)).mkdirs().batchOutput().open();
logger.debug("==========================檔案生成完成!!!==========================");
}
上面代理主要分為兩步。
- 第一步:根據配置資訊,例項化一個ConfigBuilder。它遮蔽了對配置如何處理的細節,初始化完成之後,ConfigBuilder就完成了對所有配置的載入,以及對應資料庫表、欄位的提取。
- 第二步:呼叫模板引擎,傳入ConfigBuilder。模板引擎根據ConfigBuilder填充對應的模板。最終生成程式碼。
構造ConfigBuilder
在構造ConfigBuilder中,會一一相關的配置進行handle。這裡主要講一下handlerStrategy
,因為這個方法包含了對資料庫資訊的提取過程,並且將資料庫表資訊對映為實體。
最終會走到一個名叫getTablesInfo
方法。裡面涉及到兩個基本的、對資料表抽象的實體TableField
與TableInfo
基礎實體
TableField
TableField
的內容:
/**
* 是否需要進行轉換
*/
private boolean convert;
/**
* 是否為主鍵
*/
private boolean keyFlag;
/**
* 主鍵是否為自增型別
*/
private boolean keyIdentityFlag;
/**
* 對應資料表的名稱
*/
private String name;
/**
* 轉換之後的型別
*/
private String type;
/**
* 轉換之後的屬性名
*/
private String propertyName;
/**
* 對應資料表的型別
*/
private DbColumnType columnType;
/**
* 該欄位的註釋資訊
*/
private String comment;
/**
* 填充資訊
*/
private String fill;
/**
* 自定義查詢欄位列表
*/
private Map<String, Object> customMap;
注意在設定setConvert的時候是傳入的一個StrategyConfig,根據StrategyConfig的某些配置確定是否需要轉換。
TableInfo
TableInfo是對資料表的抽象,具體來講:
/**
* 是否轉換
*/
private boolean convert;
/**
* 表名
*/
private String name;
/**
* 表註釋
*/
private String comment;
/**
* 表所對應的實體名
*/
private String entityName;
/**
* 表所對應的mapper名
*/
private String mapperName;
/**
* 表所對應的xml名
*/
private String xmlName;
/**
* 表所對應的service名
*/
private String serviceName;
/**
* 表所對應的serviceimpl名
*/
private String serviceImplName;
/**
* 表所對應的controller名
*/
private String controllerName;
/**
* 表所包含的所有field集合
*/
private List<TableField> fields;
/**
* 公共欄位
*/
private List<TableField> commonFields;
/**
* 所依賴的包名
*/
private List<String> importPackages = new ArrayList<>();
/**
* 說有欄位連在一起的字串,用於日誌資訊
*/
private String fieldNames;
這需要注意一點的就是在設定fields的時候需要根據fields的資料型別引入相應的包名。做法就是在設定fields的時候根據field型別引入。具體來講:
public void setFields(List<TableField> fields) {
if (CollectionUtils.isNotEmpty(fields)) {
this.fields = fields;
// 收集匯入包資訊,注意為什麼用HashSet。因為HashSetk可以自動去除重複的key
Set<String> pkgSet = new HashSet<>();
for (TableField field : fields) {
if (null != field.getColumnType() && null != field.getColumnType().getPkg()) {
pkgSet.add(field.getColumnType().getPkg());
}
if (field.isKeyFlag()) {
// 主鍵
if (field.isConvert() || field.isKeyIdentityFlag()) {
pkgSet.add("com.baomidou.mybatisplus.annotations.TableId");
}
// 自增
if (field.isKeyIdentityFlag()) {
pkgSet.add("com.baomidou.mybatisplus.enums.IdType");
}
} else if (field.isConvert()) {
// 普通欄位
pkgSet.add("com.baomidou.mybatisplus.annotations.TableField");
}
if (null != field.getFill()) {
// 填充欄位
pkgSet.add("com.baomidou.mybatisplus.annotations.TableField");
pkgSet.add("com.baomidou.mybatisplus.enums.FieldFill");
}
}
if (!pkgSet.isEmpty()) {
this.importPackages = new ArrayList<>(Arrays.asList(pkgSet.toArray(new String[]{})));
}
}
}
getTablesInfo
先介紹一下其中的處理邏輯:
判斷是否設定了同時設定了include和exclude
儲存所有表的資訊,包含排除的表,需要生成的表。
-
根據sql查詢表的資訊,然後依次將資料對映到對應的基礎實體上(TableInfo,FiledInfo)。
- 如果設定了include或者exclude,再進一步刪選。比如過濾使用者輸入不存在的表。
- 最終得到includeTableList表,禮包包含了需要轉換的表名稱
- 呼叫
convertTableFields
將表中的filed轉為基礎實體。 - 最後呼叫
processTable
將表、欄位資訊、其他資訊填充到TableInfo中。完成TableInfo基礎實體的構造。
其中用到的幾條sql語句如下:
-
show table status
:獲取表資訊,比如表名、建立(修改)時間、表註釋,數量條數等
-
show full fields from xxx
:從特定表中獲取表所有欄位的資訊,比如欄位名、欄位型別,欄位註釋以及該表的主鍵等。
其中使用的而是JDBC最為簡單的讀取資料庫。程式碼簡化一下
//對映Tables
preparedStatement = connection.prepareStatement(tablesSql);
ResultSet results = preparedStatement.executeQuery();
TableInfo tableInfo;
while (results.next()) {
......
includeTableList.add(tableInfo);
}
//對映Fields
for (TableInfo ti : includeTableList) {
PreparedStatement preparedStatement = connection.prepareStatement(tableFieldsSql);
ResultSet results = preparedStatement.executeQuery();
TableField field = new TableField();
while (results.next()) {
......
fieldList.add(field);
}
}
tableInfo.setFields(fieldList);
tableInfo.setCommonFields(commonFieldList);
}
至此所有表以及所有表對應的欄位已經完全對映到了基礎實體。接下來就是根據基礎實體的內容,填充對應的模板。
Template(模板生成)
有了上面所產生的實體,下面就是填模板的過程了。入口如下:
if(null == this.templateEngine) {
this.templateEngine = new VelocityTemplateEngine();
}
this.templateEngine.init(this.pretreatmentConfigBuilder(this.config)).mkdirs().batchOutput().open();
TemplateEngine(模板引擎)
MP現在支援兩種模板引擎Velocity、Freemarker。這裡以Velocity為例。
模板生成相關一共有三個類,分別是AbstractTemplateEngine、FreemarkerTemplateEngine、VelocityTemplateEngine。AbstractTemplateEngine是抽象類定義了相關的介面。具體來講,提供瞭如下資訊:
其中的writer
與templateFilePath
為抽象方法,根據不同的模板引擎,選擇不同的實現。最終是呼叫batchOutput來輸出所有自動生成的程式碼。
在batchOutPut中將各個層級的物件,根據模板路徑,生成最終的檔案。
//遍歷所有的表資訊,生成檔案
List<TableInfo> tableInfoList = this.getConfigBuilder().getTableInfoList();
for (TableInfo tableInfo : tableInfoList) {
Map<String, Object> objectMap = this.getObjectMap(tableInfo);
Map<String, String> pathInfo = this.getConfigBuilder().getPathInfo();
TemplateConfig template = this.getConfigBuilder().getTemplate();
// Mp.java
String entityName = tableInfo.getEntityName();
if (null != entityName && null != pathInfo.get(ConstVal.ENTITY_PATH)) {
String entityFile = String.format((pathInfo.get(ConstVal.ENTITY_PATH) + File.separator + "%s" + this.suffixJavaOrKt()), entityName);
if (this.isCreate(entityFile)) {
this.writer(objectMap, this.templateFilePath(template.getEntity(this.getConfigBuilder().getGlobalConfig().isKotlin())), entityFile);
}
}
......
// MpMapper.xml
// IMpService.java
// MpServiceImpl.java
// MpController.java
}
}
}
接下來看一下VelocityTemplateEngine中的write方法。首先會進行初始化配置
public VelocityTemplateEngine init(ConfigBuilder configBuilder) {
//將configBuilder傳給父類,在父類中需要用到
super.init(configBuilder);
if (null == velocityEngine) {
Properties p = new Properties();
p.setProperty(ConstVal.VM_LOADPATH_KEY, ConstVal.VM_LOADPATH_VALUE);
p.setProperty(Velocity.FILE_RESOURCE_LOADER_PATH, "");
p.setProperty(Velocity.ENCODING_DEFAULT, ConstVal.UTF8);
p.setProperty(Velocity.INPUT_ENCODING, ConstVal.UTF8);
p.setProperty("file.resource.loader.unicode", "true");
//初始化模板引擎
velocityEngine = new VelocityEngine(p);
}
return this;
}
父類中呼叫writer,並將objectMap(包含所有的對映資訊)傳入,根據templatePath(不同型別模板不一樣)建立template。最後將模板內容依據objectMap替換掉。其中的模板路徑則根據之前的TemplateConfig得到
@Override
public void writer(Map<String, Object> objectMap, String templatePath, String outputFile) throws Exception {
if (StringUtils.isEmpty(templatePath)) {
return;
}
Template template = velocityEngine.getTemplate(templatePath, ConstVal.UTF8);
FileOutputStream fos = new FileOutputStream(outputFile);
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(fos, ConstVal.UTF8));
template.merge(new VelocityContext(objectMap), writer);
writer.close();
logger.debug("模板:" + templatePath + "; 檔案:" + outputFile);
}
這裡以生存entity為例。
生成的entity
package com.baomidou.test.entity;
import com.baomidou.mybatisplus.annotations.TableId;
import com.baomidou.mybatisplus.enums.IdType;
import com.baomidou.mybatisplus.activerecord.Model;
import java.io.Serializable;
/**
* <p>
* 許可權表
* </p>
*
* @author wesly
* @since 2018-06-08
*/
public class Permission extends Model<Permission> {
private static final long serialVersionUID = 1L;
/**
* 主鍵
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 上級ID
*/
private Long pid;
......
private String description;
@Override
public String toString() {
return "Permission{" +
", id=" + id +
", pid=" + pid +
", title=" + title +
", type=" + type +
", state=" + state +
", sort=" + sort +
", url=" + url +
", permCode=" + permCode +
", icon=" + icon +
", description=" + description +
"}";
}
}
總結
Generate部分總體來講思路比價簡單。麻煩的部分在於如何去對各個部拆分。最後簡單畫出了怎個生成框架的類圖結構如下:
相關文章
- 淺析Vue原始碼(六)—— $mount中template的編譯–generateVue原始碼編譯
- 淺析Vue原始碼(六)—— $mount中template的編譯--generateVue原始碼編譯
- Retrofit原始碼分析三 原始碼分析原始碼
- 【JDK原始碼分析系列】ArrayBlockingQueue原始碼分析JDK原始碼BloC
- 集合原始碼分析[2]-AbstractList 原始碼分析原始碼
- 集合原始碼分析[3]-ArrayList 原始碼分析原始碼
- 集合原始碼分析[1]-Collection 原始碼分析原始碼
- Android 原始碼分析之 AsyncTask 原始碼分析Android原始碼
- Guava 原始碼分析之 EventBus 原始碼分析Guava原始碼
- MyBatis-Plus雪花演算法實現原始碼解析MyBatis演算法原始碼
- 以太坊原始碼分析(36)ethdb原始碼分析原始碼
- 以太坊原始碼分析(38)event原始碼分析原始碼
- 以太坊原始碼分析(41)hashimoto原始碼分析原始碼
- 以太坊原始碼分析(43)node原始碼分析原始碼
- 以太坊原始碼分析(51)rpc原始碼分析原始碼RPC
- 以太坊原始碼分析(52)trie原始碼分析原始碼
- 深度 Mybatis 3 原始碼分析(一)SqlSessionFactoryBuilder原始碼分析MyBatis原始碼SQLSessionUI
- k8s client-go原始碼分析 informer原始碼分析(4)-DeltaFIFO原始碼分析K8SclientGo原始碼ORM
- k8s client-go原始碼分析 informer原始碼分析(6)-Indexer原始碼分析K8SclientGo原始碼ORMIndex
- Mybatis3原始碼筆記(八)小窺MyBatis-plusMyBatisS3原始碼筆記
- 5.2 spring5原始碼--spring AOP原始碼分析三---切面原始碼分析Spring原始碼
- Spring原始碼分析——搭建spring原始碼Spring原始碼
- 精盡MyBatis原始碼分析 - MyBatis-Spring 原始碼分析MyBatis原始碼Spring
- 以太坊原始碼分析(35)eth-fetcher原始碼分析原始碼
- 以太坊原始碼分析(20)core-bloombits原始碼分析原始碼OOM
- 以太坊原始碼分析(24)core-state原始碼分析原始碼
- 以太坊原始碼分析(29)core-vm原始碼分析原始碼
- 以太坊原始碼分析(34)eth-downloader原始碼分析原始碼
- k8s client-go原始碼分析 informer原始碼分析(5)-Controller&Processor原始碼分析K8SclientGo原始碼ORMController
- ddos原始碼分析原始碼
- SpringBoot原始碼分析Spring Boot原始碼
- ucontext原始碼分析Context原始碼
- jQuery原始碼分析jQuery原始碼
- Express原始碼分析Express原始碼
- Eureka原始碼分析原始碼
- AbstractQueuedSynchronizer原始碼分析原始碼
- unbound原始碼分析原始碼
- Mybatis原始碼分析MyBatis原始碼