项目目标
避免重复性工作
使用 Spring MVC、Spring Boot、Spring Cloud 都不可避免的会用到 entity、mapper/dao、service、controller 层代码的设计,各种业务代码在结构上都是十分相近的。
比如我们写一个权限管理系统,用户管理模块由
User.java、UserDao.java、UserService.java、UserController
几个部分组成,我们添加一个角色管理模块,就挨个开始写
Role.java、RoleDao.java、RoleService.java、RoleController.java。
而这些代码的结构相似,一次一次重复着写,感觉很是心累,和搬砖没啥区别。心累了,就想去避免这种情况的发生,想搞个代码生成器,一键生成这些基础的框架,我们安安心心写业务逻辑就好了。
统一代码风格
还拿权限管理系统举例,可能你自己的代码是一套风格,你走了,别人来维护这个系统,新增些功能,他有他的风格。或者你几个月后自己新增了个功能,写的代码风格和以前的也不一样。
不同的代码风格对以后的维护难免会造成一些困扰,我们也希望能通过一个固定风格的代码生成器维护系统的代码风格一致性。
工具选用
Beetl 简介
经过一番寻找,我选择使用 Beetl 来完成这项工作。
官方介绍:
Beetl 是一款 6 倍于 Freemarker 的超高性能的 java 模板引擎,来自于中国!!BeetlSQL
是一款功能强大,以 SQL 为中心,使用简单的全功能的 ORMapping 工具,支持自动生成代码,内置大量 SQL
语句,支持市面上绝大多数主流数据库。Beetl 和 BeetlSQL 均能与目前市面上的大多数流行框架无缝整合。
官网地址:http://ibeetl.com/
Beetl 安装
如果使用 maven:
<dependency>
<groupId>com.ibeetl</groupId>
<artifactId>beetl</artifactId>
<version>2.9.3</version>
</dependency>
如果非 maven 工程,直接下载http://git.oschina.net/xiandafu/beetl2.0/attach_files
在本项目中,我使用 maven 的方式。
我们在这个项目中只用到 Beetl 简单的几个功能,如果你想了解更多关于这个模板引擎的操作,请参阅官方文档:http://ibeetl.com/guide/#beetl
项目实战
编写启动类
新建一个 maven 工程,引入上面提到的 Beetl。
新建一个 Class ,main 方法中写如下代码(PS:对应项目源码中的 beetl.BeetlUtils.java):
//指定模板路径
String root = System.getProperty("user.dir") + File.separator + "newTemplate" + File.separator;
//创建模板资源加载器
FileResourceLoader resourceLoader = new FileResourceLoader(root,"utf-8");
//创建配置类
Configuration cfg = Configuration.defaultConfiguration();
//创建GroupTemplate
GroupTemplate gt = new GroupTemplate(resourceLoader, cfg);
//指定要加载的模板
Template t = gt.getTemplate(myTemplate.getTemplateName());
//绑定全局变量
t.binding("myUtil",myTemplate);
//读取模板输出的文本
String str = t.render();
System.out.println(str);
讲解一下这几行代码,这也是 Beetl 的核心应用。
Beetl 的核心是 GroupTemplate,是一个重量级对象,实际使用的时候建议使用单模式创建,创建 GroupTemplate 需要两个参数,一个是模板资源加载器,一个是配置类。
模板资源加载器 Beetl 内置了 6 种, FileResourceLoader 是其中的一种:文件模板加载器,需要一个根目录作为参数构造,传入 getTemplate 方法的 String 是模板文件相对于 Root 目录的相对路径。
代码第 6 行将变量 myTemplate 传入模板里,在模板中的名称是“myUtil”。 代码第 7 行是渲染模板,得到输出,template.render() 返回渲染结果。
讲到这里大家可能会对 Beetl 的使用有个模糊的印象了:
Beetl 需要一个模板文件;
我们要把想要处理的对象传入模板文件;
我们可以使用 Beetl 渲染模板;
我们要把渲染后的结果保存成 Java 文件。
定义配置对象
上一步我们提到:要把想要处理的对象传入模板文件。
那么我们要传入什么样的对象呢,我们要生成的是 Java 文件,需要知道:
项目根目录
包名
表名
实体对象名
……
整理后,我定义的对象如下(PS:对应源码中的beetl.MyTemplate.java):
public class MyTemplate {
//文件路径
private String rootPath;
//包名
private String packageName;
//实体类名
private String entityClassName;
//实体对象名
private String entityName;
//模板名称
private String templateName;
//Controller 上的 RequestMapping 对应的 value
private String packageMapPath;
//表名
private String collectionName;
//字段列表
private List<DataBaseFields> list = new ArrayList<DataBaseFields>();
}
其中,DataBaseFields 是定义的数据字段对象,主要包含 (PS:代码见源码中的beetl.DataBaseFields.java):
字段名:用来声明实体类的属性名;
字段类型:用来声明实体类的属性类型;
是否是索引字段:如果是索引字段,我们为其生成根据索引字段查询的接口;
是否要求非空:如果设置该字段为非空,我们在Insert和Update时要做出相应判断;
默认值:为实体类的属性设置默认值;
字段描述:作为实体类中属性的文档注释使用。
添加配置文件
定义好对象后,为了实例化对象,我们要用配置文件将各种参数保存起来,DataBaseFields 对象对应的是数据表的字段,我们就用 JSON 来保存,格式如下(PS:参考源码中 /sql 目录下的 json 文件):
[
{
"name":"id",
"type":"String",
"isIndex":false,
"isNull":true,
"defaultValue":null,
"description":"角色ID"
},
{
"name":"roleName",
"type":"String",
"isIndex":true,
"isNull":false,
"defaultValue":null,
"description":"角色名称"
}
]
至于 MyTemplate 对象,在这里我们用一个 .properties 配置文件来初始化(PS:源码中的src/main/config/myBeetl.properties)
#项目总包名路径
RootPath=/Users/yangkaile/Projects/IdeaProjects/beetl/src/main/java/
#实体类名
EntityClassName=Role
#实体对象名
EntityName=role
#实体包名 支持单级包名和多级包名 当指定多级包名时,分级之间请用点号隔开 必填
PackageName=com.originaldreams.logcenter
#实体对象文件名
SqlFile=Role.json
#Collention名 表名
CollectionName=role
创建模板
以下模板中用到的语法均可参阅官方文档:http://ibeetl.com/guide/#beetl
entity 层模板
(PS:在源码中/newTemplate/Entity.txt)
比如我要生成这样的代码,先分析一下代码结构:
经过上图的分析我们可以发现,在类的声明中,只有项目包名和类名是变化的,根据需要,我们可以选择引入 Date 和 List 这些常用对象,模板结构如下,其中 myUtil 就是模板中引入的变量,这个变量定义了模板中需要的各个参数。
类的声明
我们这样声明一个类:
package ${myUtil.packageName}.entity;
import java.util.Date;
import java.util.List;
public class ${myUtil.entityClassName} {
}
知识点:Beetl 基础语法
占位符:在模板中使用 ${} 的形式引用变量
模板中固定的字符原样书写即可
成员变量
属性名和属性类型读取 myUtil.list 进行遍历,代码如下:
<%
for(attr in myUtil.list){
if(attr.description != null){
println(" /**");
println(" * " + attr.description);
println(" */");
}
if(attr.defaultValue == null){
println(" private " + attr.type + " " + attr.name + ";");
}else{
println(" private " + attr.type + " " + attr.name + " = " + attr.defaultValue + ";");
}
}
%>
知识点:for-in 循环
定界符:Beetl 模板语言类似 JS 语言和习俗,只需要将 Beetl 语言放入定界符号里即可,如默认的是<% %>
在定界符中可以使用 Beetl 语法书写代码
for-in 循环:for-in 循环支持遍历集合对象,对于 List 和数组来说以及Iterator,对象就是集合对象,对于 Map 来说,对象就是 Map.entry
println 输出:Beetl 使用 println() 方法输出代码中处理好的字符串
Getter/Setter
Getter/Setter 是遍历 myUtil.list 实现的,代码如下:
<%
for(attr in myUtil.list){
println(" public " + attr.type + " get" + strutil.toUpperCase(strutil.subStringTo(attr.name,0,1)) + strutil.subString(attr.name,1) + "(){");
println(" return this." + attr.name + ";");
println(" }");
println(" public void set" + strutil.toUpperCase(strutil.subStringTo(attr.name,0,1)) + strutil.subString(attr.name,1) + "(" + attr.type + " " + attr.name + "){");
println(" this." + attr.name + " = " + attr.name + ";");
println(" }");
}
%>
知识点:Beetl 内置方法
strutil.toUpperCase: ${ strutil.toUpperCase ("hello")},输出是 HELLO
strutil.subString: ${ strutil.subString ("hello",1)},输出是"ello"
strutil.subStringTo: ${ strutil.subStringTo ("hello",1,2)},输出是"e"
toString
遍历 myUtil.list:
@Override
public String toString() {
return "${myUtil.entityClassName}{" +
<%
print(" ");
for(attr in myUtil.list){
print('" ' + attr.name + ':" + ' + attr.name + " + ");
}
%>
"}";
}
没有新的知识点。
dao 层模板
(PS:在源码中 /newTemplate/Mapper.txt)
package ${myUtil.packageName}.mapper;
import ${myUtil.packageName}.entity.${myUtil.entityClassName};
import org.apache.ibatis.annotations.*;
import java.util.List;
@Mapper
public interface ${myUtil.entityClassName}Mapper {
String tableName = "${myUtil.collectionName}";
}
@Select("SELECT <%
for(attr in myUtil.list)
{
if(attrLP.last)
print(attr.name);
else
print(attr.name + ", ");
}
%> FROM " + tableName + " WHERE id = #{id}")
${myUtil.entityClassName} getById(<%
for(attr in myUtil.list){
if(attr.name == "id")
print(attr.type);
}
%> Id);
知识点:if-else
if-else : Beetl 中的 if-else 同 Java 和 JavaScript 中的用法一样
<% for(attr_p in myUtil.list){
if(attr_p.isIndex == true&&attr_p.name!='id'){ %>
@Select("SELECT <%
for(attr in myUtil.list)
{
if(attrLP.last)
print(attr.name);
else
print(attr.name + ", ");
}
%> FROM " + tableName + " WHERE ${attr_p.name} = #{${attr_p.name}}")
${myUtil.entityClassName} getBy${attr_p.camelCaseName}(${attr_p.type} ${attr_p.name});
<% }
} %>
知识点:Beetl 隐含对象
循环体中的隐含对象:其命名规范是循环体名称后加上LP,提供了当前循环的信息,如本例中 循环体是 attr:
attrLP.index 当前的索引,从1开始
attrLP.size 集合的长度
attrLP.first 是否是第一个
attrLP.last 是否是最后一个
attrLP.even 索引是否是偶数
attrLP.odd 索引是否是奇数
service 层模板
(PS:在源码中 /newTemplate/Service.txt)
public ${myUtil.entityClassName} getById(<%
for(attr in myUtil.list){
if(attr.name == "id")
print(attr.type);
}
%> id){
return ${myUtil.entityName}Mapper.getById(id);
}
<% for(attr in myUtil.list){
if(attr.isIndex == true&&attr.name!='id'){ %>
public ${myUtil.entityClassName} getBy${attr.camelCaseName}(${attr.type} ${attr.name}){
return ${myUtil.entityName}Mapper.getBy${attr.camelCaseName}(${attr.name});
}
<% }
} %>
没有新的知识点。
controller 层模板
(PS:在源码中/newTemplate/Controller.txt)
@RestController
@RequestMapping("/${myUtil.entityName}")
public class ${myUtil.entityClassName}Controller {
private Logger logger = LoggerFactory.getLogger(${myUtil.entityClassName}Controller.class);
@Resource
private ${myUtil.entityClassName}Service ${myUtil.entityName}Service;
@RequestMapping(value = "/getById",method = RequestMethod.GET)
ResponseEntity getById(<%
for(attr in myUtil.list){
if(attr.name == "id")
print(attr.type);
}
%> id){
${myUtil.entityClassName} result = ${myUtil.entityName}Service.getById(id);
return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(result);
}
}
<% for(attr in myUtil.list){
if(attr.isIndex == true && attr.name!='id'){ %>
@RequestMapping(value = "/getBy${attr.camelCaseName}",method = RequestMethod.GET)
ResponseEntity getBy${attr.camelCaseName}(${attr.type} ${attr.name}){
${myUtil.entityClassName} result = ${myUtil.entityName}Service.getBy${attr.camelCaseName}(${attr.name});
return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(result);
}
<% }
} %>
没有新的知识点。
项目运行
通过以上的操作,我们定义好了数据源、配置文件参数、模板,接下来,我们直接运行 BeetlUtils 类中的 main 方法就可可以看到指定位置产生了我们自动生成的 Java 文件,风格如下:
运行结果
entity 层
package com.originaldreams.logcenter.entity;
import java.util.Date;
import java.util.List;
public class Role {
/**
* 角色ID
*/
private String id;
/**
* 角色名称
*/
private String roleName;
/**
* 角色功能ID
*/
private String nodeIds;
/**
* 角色描述
*/
private String description;
public String getId(){
return this.id;
}
public void setId(String id){
this.id = id;
}
public String getRoleName(){
return this.roleName;
}
public void setRoleName(String roleName){
this.roleName = roleName;
}
public String getNodeIds(){
return this.nodeIds;
}
public void setNodeIds(String nodeIds){
this.nodeIds = nodeIds;
}
public String getDescription(){
return this.description;
}
public void setDescription(String description){
this.description = description;
}
@Override
public String toString() {
return "Role{" +
" id:" + id + " roleName:" + roleName + " nodeIds:" + nodeIds + " description:" + description +
"}";
}
}
dao 层
package com.originaldreams.logcenter.mapper;
import com.originaldreams.logcenter.entity.Role;
import org.apache.ibatis.annotations.*;
import java.util.List;
@Mapper
public interface RoleMapper {
String tableName = "role";
@Select("SELECT id, roleName, nodeIds, description FROM " + tableName + " WHERE id = #{id}")
Role getById(String Id);
@Select("SELECT id, roleName, nodeIds, description FROM " + tableName + " WHERE roleName = #{roleName}")
Role getByRoleName(String roleName);
@Select("SELECT id, roleName, nodeIds, description FROM " + tableName)
List<Role> getAll();
@Insert("INSERT INTO " + tableName + "(id, roleName, nodeIds, description) VALUES (#{id}, #{roleName}, #{nodeIds}, #{description})")
Integer insert(Role role);
@Delete("DELETE FROM " + tableName + " WHERE id = #{id}")
Integer deleteById(String id);
@Update("UPDATE " + tableName + " SET roleName=#{roleName}, nodeIds=#{nodeIds}, description=#{description} WHERE id = #{id}")
Integer update(Role role);
}
service 层
package com.originaldreams.logcenter.service;
import javax.annotation.Resource;
import org.springframework.stereotype.Service;
import com.originaldreams.logcenter.entity.Role;
import com.originaldreams.logcenter.mapper.RoleMapper;
import java.util.List;
@Service
public class RoleService {
@Resource
private RoleMapper roleMapper;
public Role getById(String id){
return roleMapper.getById(id);
}
public Role getByRoleName(String roleName){
return roleMapper.getByRoleName(roleName);
}
public List<Role> getAll(){
return roleMapper.getAll();
}
public Integer insert(Role role){
return roleMapper.insert(role);
}
public Integer deleteById(String id){
return roleMapper.deleteById(id);
}
public Integer update(Role role){
return roleMapper.update(role);
}
}
controller 层
package com.originaldreams.logcenter.controller;
import com.originaldreams.logcenter.entity.SMSLog;
import com.originaldreams.logcenter.service.SMSLogService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.List;
@RestController
@RequestMapping("/smsLog")
public class SMSLogController {
private Logger logger = LoggerFactory.getLogger(SMSLogController.class);
@Resource
private SMSLogService smsLogService;
@RequestMapping(value = "/getById",method = RequestMethod.GET)
ResponseEntity getById(Integer id){
SMSLog result = smsLogService.getById(id);
return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(result);
}
@RequestMapping(value = "/getByPhone",method = RequestMethod.GET)
ResponseEntity getByPhone(String phone){
SMSLog result = smsLogService.getByPhone(phone);
return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(result);
}
@RequestMapping(value = "/getByType",method = RequestMethod.GET)
ResponseEntity getByType(Integer type){
SMSLog result = smsLogService.getByType(type);
return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(result);
}
@RequestMapping(value = "/getAll",method = RequestMethod.GET)
ResponseEntity getAll(){
List<SMSLog> result = smsLogService.getAll();
return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(result);
}
@RequestMapping(value = "/insert",method = RequestMethod.POST)
ResponseEntity insert(SMSLog smsLog){
Integer result = smsLogService.insert(smsLog);
return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(result);
}
@RequestMapping(value = "/deleteById",method = RequestMethod.DELETE)
ResponseEntity deleteById(Integer id){
Integer result = smsLogService.deleteById(id);
return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(result);
}
@RequestMapping(value = "/update",method = RequestMethod.PUT)
ResponseEntity update(SMSLog smsLog){
Integer result = smsLogService.update(smsLog);
return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(result);
}
}
小结
至此,我们完成了自己定义的代码生成器,我们来总结一下它的操作步骤:
根据数据表的定义,写好 json 格式的数据源文件,建议放在 /sql 文件夹下;
修改 /src/main/config/myBeetl.properties 文件中的项目路径、包名、数据源、实体类名等配置;
如果想要改变风格,按照 Beetl 的语法格式修改 /newTemplate 文件夹下的模板;
如果保持现在的风格,直接运行 BeetlUtils 类中的 main 方法即可。
项目源码
不套路,直接上源码:https://github.com/tianlanlandelan/beetl喜欢请点小星星,多谢。
网友评论