現(xiàn)在很多開源的腳手架項目一般都會有自己的代碼生成器,能夠幫助快速生成代碼,一般都是根據(jù)表結(jié)構(gòu)生成實體,和實體對應(yīng)的操作類,比如controller、service、mapper等。這些初始新建的類都有個共同點,除了對應(yīng)的操作實體不一樣之外,其他沒啥區(qū)別,所以我們可以統(tǒng)一生成。除了后端的代碼,有些生成器還可以生成頁面,如表單、列表等。
說到這里,不經(jīng)想問一下,這代碼生成的原理是啥?
我們來分析一下,因為涉及到表反向生成代碼,所以需要先設(shè)計好表結(jié)構(gòu)。有了表結(jié)構(gòu)之后我們就可以生成對應(yīng)的實體類,這個過程是怎么完成的呢?也就是說,后端怎么知道我有哪些數(shù)據(jù)表?然后每個數(shù)據(jù)表又長什么樣子的呢?
這里給大家介紹兩種方法:
1、通過默認(rèn)數(shù)據(jù)庫information_schema獲取
2、通過show命令查看結(jié)構(gòu)或狀態(tài)
首先來說說第一種:
information_schema中獲取
大家在安裝完mysql之后,mysql并不是空的,而是默認(rèn)自帶了4個數(shù)據(jù)庫,分別如下:
information_schema
performance_schema
mysql
sys
保存整個mysql所有數(shù)據(jù)庫、表、索引的信息
主要用于收集數(shù)據(jù)庫服務(wù)器性能參數(shù)
提供進(jìn)程等待的詳細(xì)信息,包括鎖、互斥變量、文件信息;
保存MySQL的權(quán)限、參數(shù)、對象和狀態(tài)信息。
sys模式,這是一組幫助DBA和開發(fā)人員解釋性能模式收集的數(shù)據(jù)的對象。sys模式對象可用于典型的調(diào)優(yōu)和診斷用例。
這個sys數(shù)據(jù)庫雖然只有一個表,但是卻有大量的視圖:
ok,上面我們對mysql中的幾個默認(rèn)數(shù)據(jù)庫做了一番認(rèn)識,那么現(xiàn)在我們找到答案了嗎?
information_schema數(shù)據(jù)庫中是不是存有所有的數(shù)據(jù)表和表字段信息,由此,我們就可以根據(jù)數(shù)據(jù)庫名稱獲取出所有的表,又可以通過表名稱獲取出具體的字段信息。
得出的sql如下:
SELECT
*
FROM
information_schema. TABLES
WHERE
TABLE_SCHEMA = (SELECT DATABASE());
因為我們項目都是指定數(shù)據(jù)庫的,所以select database()就是連接的數(shù)據(jù)庫。結(jié)果如下:
有了表名稱我們是不是就可以生成實體啦?只需要去掉表前綴(如“t”等),然后下劃線轉(zhuǎn)駝峰,再首字母大寫,這樣useraction就轉(zhuǎn)成了UserAction實體了。
現(xiàn)在我們獲取到了所有表,那么接下來就循環(huán)獲取出所有表的字段信息。依然還是利用information_schema數(shù)據(jù)庫。
以user_action表為例子:
SELECT
*
FROM
information_schema. COLUMNS
WHERE
TABLE_SCHEMA = (SELECT DATABASE())
AND TABLE_NAME = 'user_action';
第一種方法總結(jié)如下:通過mysql的默認(rèn)數(shù)據(jù)庫information_schema中的TABLES和COLUMNS表的特性,通過條件查詢出對應(yīng)的數(shù)據(jù)表和字段的信息。
show命令查看
ok,接下來我們來看下show命令怎么來查看到指定數(shù)據(jù)庫的數(shù)據(jù)表和字段信息。首先來獲取third-homework數(shù)據(jù)庫下表的信息:
show table status;
結(jié)果如下:
這里比較巧妙,這條命令的本意是查看所有表的一些狀態(tài)信息,所以就有字段是專門說明表名稱的,這樣我們就可以理解成是獲取所有表信息。
那有如何獲取表的字段呢?
show full fields from `user_action`;
結(jié)果如下:
所以,我們又可以獲取到表的字段了。是不是挺簡單的?
好啦,上面都是教如何去獲取數(shù)據(jù)庫表和字段的,那么獲取到表結(jié)構(gòu)之后又是如何生成實體的呢?帶著這個問題我們繼續(xù)往下面去分析。
~~實體類是個java文件,一般來說我們要生成一個文件,我們需要一些文件操作類,先定義好模板,然后再傳參進(jìn)去,進(jìn)行渲染。比如我們要生成Excel、PDF等。但java文件不需要這么麻煩,java文件其實和一個txt文件的文本格式一樣,通常我們都可以直接把txt文件的后綴改成java后綴。然后html文件其實也算是一個txt文件,他們之間其實都可以相互強(qiáng)轉(zhuǎn)后綴,不影響打開和使用。~~
不知道大家有沒用過頁面靜態(tài)化?頁面靜態(tài)化的意思就是把原本需要動態(tài)加載和渲染的節(jié)點預(yù)先渲染成一個完全靜態(tài)的html頁面,這樣我們打開頁面的時候就完全是個靜態(tài)的html頁面,不再需要經(jīng)過后端的動態(tài)渲染,這樣可以大大減輕后端服務(wù)的壓力,同時提高響應(yīng)速度。
我們先來看下頁面靜態(tài)化是怎么做到的。首先,我們定義一個動態(tài)頁面,controller中傳參過去:
com.example.IndexController#index
@GetMapping({'', '/', 'index'})
public String index(HttpServletRequest req) {
req.setAttribute('name', '呂一明');
return 'index';
}
然后頁面如下:
templates/index.ftl
<!DOCTYPE html>
<html lang='zh-CN'>
<head>
<meta charset='utf-8'>
<title>公眾號:java思維導(dǎo)圖</title>
</head>
<body>
<div>我是:<strong>${name}</strong></div>
</body>
</html>
得到的頁面效果如下:
那么有沒有辦法,不需要經(jīng)過controller,然后直接得出最后的這個頁面渲染結(jié)果呢?答案就是我們剛才說到的提前靜態(tài)化。因為我們頁面用的是模板引擎freemaker,所以用起來就簡單了。
百科介紹:FreeMarker是一款模板引擎 :即一種基于模板和要改變的數(shù)據(jù), 并用來生成輸出文本( HTML 網(wǎng)頁、 電子郵件 、 配置文件 、 源代碼 等)的通用工具。
所以我們使用freemaker,其實底層原理就是在后端預(yù)先把參數(shù)和模板進(jìn)行渲染之后得到網(wǎng)頁再傳給瀏覽器顯示的。所以我們?nèi)藶橄劝训讓拥匿秩静襟E先提取出來,代碼如下:
com.example.IndexController#toHtml
@ResponseBody
@RequestMapping('/toHtml/{id}')
public Object toHtml(@PathVariable Long id, HttpServletRequest req) throws IOException {
Template template = configuration.getTemplate('/index.ftl');
String fileName = id '.html';
String htmlDir = 'D:\\git-job\\open-demo\\src\\main\\resources\\static\\html';
Map<String, Object> params = new HashMap<>();
params.put('name', '呂一明' id);
return FreemarkerUtil.printToFile(template, htmlDir, fileName, params);
}
而FreemarkerUtil.printToFile代碼比較長,就不貼出來了,原理就是先定義一個輸出流,然后使用模板把參數(shù)和流渲染得到文件。關(guān)鍵代碼如下:
//創(chuàng)建輸出流
File file = new File(fileDir File.separator fileName);
Writer writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file),'UTF-8'));
//輸出模板和數(shù)據(jù)模型都對應(yīng)的文件
template.process(params, writer);
梳理一下,上面的代碼意思是我從templates中獲取index.ftl頁面,然后指定了一個params的map交給Template進(jìn)行渲染,得到的html頁面放在指定的static/html文件夾下。
于是我們訪問http://localhost:8080/toHtml/1234就得到如下結(jié)果:
就可以通過http://localhost:8080/html/1234.html訪問到靜態(tài)頁面。
以上就是利用模板引擎頁面靜態(tài)化的原理。那么這和我們代碼生成有什么關(guān)聯(lián)呢?其實大部分的代碼都是類似這樣生成的,優(yōu)先定義好模板,然后再往模板中塞字段信息等,最后渲染出一個java文件。
mybatis plus代碼生成器
接下來,我們?nèi)シ治鲆幌耺ybatis plus的代碼生成器。官網(wǎng)代碼生成器說明:
https://mybatis.plus/guide/generator.html
(這里有個動圖,但插入不進(jìn)來,大家進(jìn)入鏈接看吧)
上面是代碼生成的演示,可以看到輸入一些參數(shù)之后,自動幫我們生成controller、service等。
我們先貼出生成器代碼,比較長:
// 演示例子,執(zhí)行 main 方法控制臺輸入模塊表名回車自動生成對應(yīng)項目目錄中
public class CodeGenerator {
/**
* <p>
* 讀取控制臺內(nèi)容
* </p>
*/
public static String scanner(String tip) {
Scanner scanner = new Scanner(System.in);
StringBuilder help = new StringBuilder();
help.append('請輸入' tip ':');
System.out.println(help.toString());
if (scanner.hasNext()) {
String ipt = scanner.next();
if (StringUtils.isNotEmpty(ipt)) {
return ipt;
}
}
throw new MybatisPlusException('請輸入正確的' tip '!');
}
public static void main(String[] args) {
// 代碼生成器
AutoGenerator mpg = new AutoGenerator();
// 全局配置
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty('user.dir');
gc.setOutputDir(projectPath '/src/main/java');
gc.setAuthor('jobob');
gc.setOpen(false);
// gc.setSwagger2(true); 實體屬性 Swagger2 注解
mpg.setGlobalConfig(gc);
// 數(shù)據(jù)源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl('jdbc:mysql://localhost:3306/ant?useUnicode=true&useSSL=false&characterEncoding=utf8');
// dsc.setSchemaName('public');
dsc.setDriverName('com.mysql.jdbc.Driver');
dsc.setUsername('root');
dsc.setPassword('密碼');
mpg.setDataSource(dsc);
// 包配置
PackageConfig pc = new PackageConfig();
pc.setModuleName(scanner('模塊名'));
pc.setParent('com.baomidou.ant');
mpg.setPackageInfo(pc);
// 自定義配置
InjectionConfig cfg = new InjectionConfig() {
@Override
public void initMap() {
// to do nothing
}
};
// 如果模板引擎是 freemarker
String templatePath = '/templates/mapper.xml.ftl';
// 如果模板引擎是 velocity
// String templatePath = '/templates/mapper.xml.vm';
// 自定義輸出配置
List<FileOutConfig> focList = new ArrayList<>();
// 自定義配置會被優(yōu)先輸出
focList.add(new FileOutConfig(templatePath) {
@Override
public String outputFile(TableInfo tableInfo) {
// 自定義輸出文件名 , 如果你 Entity 設(shè)置了前后綴、此處注意 xml 的名稱會跟著發(fā)生變化!!
return projectPath '/src/main/resources/mapper/' pc.getModuleName()
'/' tableInfo.getEntityName() 'Mapper' StringPool.DOT_XML;
}
});
/*
cfg.setFileCreate(new IFileCreate() {
@Override
public boolean isCreate(ConfigBuilder configBuilder, FileType fileType, String filePath) {
// 判斷自定義文件夾是否需要創(chuàng)建
checkDir('調(diào)用默認(rèn)方法創(chuàng)建的目錄');
return false;
}
});
*/
cfg.setFileOutConfigList(focList);
mpg.setCfg(cfg);
// 配置模板
TemplateConfig templateConfig = new TemplateConfig();
// 配置自定義輸出模板
//指定自定義模板路徑,注意不要帶上.ftl/.vm, 會根據(jù)使用的模板引擎自動識別
// templateConfig.setEntity('templates/entity2.java');
// templateConfig.setService();
// templateConfig.setController();
templateConfig.setXml(null);
mpg.setTemplate(templateConfig);
// 策略配置
StrategyConfig strategy = new StrategyConfig();
strategy.setNaming(NamingStrategy.underline_to_camel);
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
strategy.setSuperEntityClass('com.baomidou.ant.common.BaseEntity');
strategy.setEntityLombokModel(true);
strategy.setRestControllerStyle(true);
// 公共父類
strategy.setSuperControllerClass('com.baomidou.ant.common.BaseController');
// 寫于父類中的公共字段
strategy.setSuperEntityColumns('id');
strategy.setInclude(scanner('表名,多個英文逗號分割').split(','));
strategy.setControllerMappingHyphenStyle(true);
strategy.setTablePrefix(pc.getModuleName() '_');
mpg.setStrategy(strategy);
mpg.setTemplateEngine(new FreemarkerTemplateEngine());
mpg.execute();
}
}
上面做了一些配置,比如全局配置基本參數(shù)、數(shù)據(jù)源配置、輸出路徑配置、自定義模板配置、策略配置(配置超類、公共字段等)。有了這些配置,就可以定義自己想要生成的效果,我們再來看看底層核心代碼。mpg.execute();方法里面核心的代碼如下:
// 模板引擎初始化執(zhí)行文件輸出
templateEngine.init(this.pretreatmentConfigBuilder(config)).mkdirs().batchOutput().open();
init方法初始化環(huán)境,
mkdirs方法創(chuàng)建文件夾,
batchOutput批量生成代碼
open打開文件夾
所以核心的代碼再batchOutput方法中,我們?nèi)タ纯矗?/p>
上面代碼中,開頭就一個for循環(huán),循環(huán)所有的表,所以每個表都會生成controller等類,下面是獲取自定義的那些配置getObjectMap(tableInfo)方法;可以看下獲取到的參數(shù):
上面初始化了很多參數(shù),有了這些參數(shù),然后我們進(jìn)入writer方法中進(jìn)行渲染。我們看看writer方法:
@Override
public void writer(Map<String, Object> objectMap, String templatePath, String outputFile) throws Exception {
Template template = configuration.getTemplate(templatePath);
try (FileOutputStream fileOutputStream = new FileOutputStream(outputFile)) {
template.process(objectMap, new OutputStreamWriter(fileOutputStream, ConstVal.UTF8));
}
logger.debug('模板:' templatePath '; 文件:' outputFile);
}
看到這個方法,是不是和之前我們頁面靜態(tài)化的處理方式一樣的,通過template.process處理模板和參數(shù)得到文件。我們看看其中一個模板是怎么定義的,比如
mapper.java.ftl
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}> {
}
</#if>
我們再對比一下mapper.java.ftl還有之前初始化的參數(shù),渲染之后輸出為java文件,是不是就達(dá)到了我們代碼生成的效果了。
結(jié)束
好了,今天文章先到這里了。