加快效率 - 最简单的代码生成器实现
加快效率 - 最简单的代码生成器实现
为什么需要代码生成器?
当工作中需要频繁复制粘贴来写程序的时候,更好的选择可能是写一个代码生成器来生成基础的内容,然后在此基础上进行修改和完善。
复制粘贴虽然简单,但是有很多不方便和潜在的BUG。复制粘贴许多时候还是需要我们修改一些变量名,做许多修改,有时候如果粗心没有修改全,反而会引入很多bug。
像前端页面,如果我们需要自己写后台的管理页面,我们会发现大多数的CRUD操作都很相似,通常复制粘贴修改就能解决,但是如果有多个人在同时开发,可能会出现多个不同的风格,不同人进行维护的时候需要了解不同人的风格。所以如果这种前台页面,我们也通过代码生成器来生成,效果会更统一,实现会更简单。
为什么是最简单的代码生成器?
通常代码生成器的起点可能会是数据库的表,需要从数据库获取表的信息然后生成一些相关的类。这种从表开始进行的代码生成器虽然不算复杂,但是还不够简单易用。
这里要说的代码生成器,是从单个(不存在继承的情况)实体类开始进行的,而且整个代码生成器完全独立于项目执行。从数据库生成实体对象后,我们的许多操作都是针对实体类进行的。代码生成器完全独立,所以不会通过反射来获取类的各种信息,一般的实体类,我们通过直接解析源码完全可以提取出我们需要的大多数信息。这种通过文件流读取实体类源码进行解析可以使得这个代码生成器足够简单易用。
模板工具类 FreemarkerUtil
随便那种模板框架都可以,我这里由于只能获取到freemarker的jar包,因此使用了freemarker。
这个类完全就是读取指定位置的模板,然后将渲染的结果输出到指定位置,代码如下:
public class FreemarkerUtil {
private static final Version VERSION = Configuration.VERSION_2_3_0;
private static final Configuration config = new Configuration(VERSION);
private static final String ENCODING = "utf-8";
static {
config.setLocale(Locale.CHINA);
config.setDefaultEncoding(ENCODING);
config.setEncoding(Locale.CHINA, ENCODING);
config.setClassForTemplateLoading(FreemarkerUtil.class, "");
config.setObjectWrapper(new DefaultObjectWrapper(VERSION));
}
public static Writer newWriter(String filePath) throws Exception {
return new BufferedWriter(new OutputStreamWriter(
new FileOutputStream(filePath), ENCODING));
}
public static BufferedReader newReader(String filePath) throws Exception {
return new BufferedReader(new InputStreamReader(
new FileInputStream(filePath), ENCODING));
}
public static boolean processTemplate(String templateName, Map<String, Object> params,
String outputPath) {
try(Writer out = newWriter(outputPath){
StringBuilder nameBuilder = new StringBuilder();
nameBuilder.append("//template/")
.append(templateName).append(".ftl");
Template template = config.getTemplate(nameBuilder.toString(), ENCODING);
template.process(params, out);
out.flush();
return true;
} catch(Exception e) {
e.printStackTrace();
return false;
}
}
}
字段信息类 Fd
该类根据个人需要定义即可,一般只需要字段名和字段含义,由于我前端JSP要特殊处理日期类型,所以属性根据需要进行了调整,代码如下:
public class Fd {
private String name;
private String text;
private boolean date;
public Fd(String name, String text, boolean date){
this.name = name;
this.date = date;
if(text != null && text.length() > 0){
this.text = text;
} else {
this.text = name;
}
}
//省略getter-setter
}
生成器配置
在说生成器类之前,先看看生成器有那些配置:
#生成代码的基础路径
gen.basePath=d:/isea533/template
#基础配置
gen.className=com.github.abel533.model.SysUser
gen.EntityName=用户信息
gen.author=isea533
#字段对应的中文含义
entity.id=主键
entity.orgid=机构ID
entity.username=用户名
#...等等
#除了通过上面方法指定外,程序会自动读取下面形式的注释作为中文含义
#/**
# * 用户密码
# */
#private String password;
#
#说明:程序中对这里的读取设计的比较死,由于代码简单,可以随时根据项目进行调整
#java文件的路径
#本程序完全通过读取java文件来提取信息
gen.java=D:/isea533/workspace/xxxx/xxxx/SysUser.java
#下面是具体的模板配置,等号后面是具体的模板名,都在template目录中
#针对同一类型的模板可以有多个不同的模板,直接在这里配置就行
#如果模板名留空,则不生成该模板
#Controller
template.controller=controller
#Dao
template.dao=dao
template.dao_xml=dao_xml
#Service
template.service=service
template.serviceImpl=serviceImpl
#JSP
template.pageList=pageList
template.pageModify=pageModify
template.pageView=pageView
#想支持更多的模板,在源码中修改即可
上面这些配置完全可以根据需要添加或者修改。
生成器类 Generator
该类代码比较长,先从无关紧要的方法一个个说。
public static void print(String str) {
System.out.println(str);
}
public static boolean isNotEmpty(String str){
return !isEmpty(str);
}
public static boolean isEmpty(String str) {
return str == null || str.length() == 0;
}
private static String mkdirs(String filePath) {
File folder = new File(filePath);
if(!folder.exists() || !folder.isDirectory()){
folder.mkdirs();
}
return filePath;
}
上面几个方法很简单,不细说了,后面的方法会使用上面的这些方法。
private static String getText(Properties properties, String field){
String text = properties.getProperty("entity." + field);
if(isNotEmpty(text)){
return text;
}
return null;
}
这个方法就是从上面的配置中读取配置的字段含义,如果存在,就使用。这里就是简单的 getProperty("entity." + field)
。
public static List<Fd> readFromEntityFile(
String filePath,
Properties properties) throws Exception {
List<String> lines = new ArrayList<String>();
BufferedReader reader = FreemarkerUtil.newReader(filePath);
String line = reader.readLine();
while(line != null){
lines.add(line.trim());
line.reader.readLine();
}
reader.close();
List<Td> fieldList = new ArrayList<Fd>();
print("\n============================================");
print("\n从Java文件中提取属性字段:");
for(int i = 0; i < lines.size(); i++){
line = lines.get(i);
if(line.startWith("private") && line.endWith(";")){
String[] ls = l.split(" ");
if(ls.length != 3){
continue;
}
String name = ls[2].substring(0, ls[2].length() - 1);
String type = ls[1];
String text = getText(properties, name);
if(text == null && i - 3 > 0){
int num = 2;
if(lines.get(i - 1).trim().startWith("@")){
num = 3;
}
String c = lines.get(i - num);
if(c.startsWith("*")){
text = c.substring(c.indexOf(" ") + 1).trim();
}
}
//如果有特殊日期类型,可以增加判断
fieldList.add(new Fd(name, text, type.startsWith("Date")));
}
}
print("\n提取完成");
print("============================================");
return fieldList;
}
上面这个方法是比较关键的一个方法,就是简单的把实体.java文件读取进来,通过简单的逻辑提取其中的字段和注释信息,这里优先使用上一个方法获取到的字段含义。
public static void main(String[] args) {
print("\n当前路径:" + new File("").getAbsolutePath());
String propertiesPath = null;
if(args.length != 1){
propertiesPath = "template.properties";
print("\n使用默认的模板配置文件:" + propertiesPath);
print("\n============================================");
print("\n你可以在运行命令时,指定配置文件的位置\n\n使用方法如:" +
"\n\njava -java gen.jar sys_role" +
"\n\n注:sys_role.properties可以不写\".properties\"后缀," +
"但是文件必须有这个后缀!");
print("\n============================================");
} else {
propertiesPath = args[0];
if(!propertiesPath.toLowerCase().endsWith(".properties")){
propertiesPath += ".properties";
}
print("\n模板配置文件为:" + propertiesPath);
}
File file = new File(propertiesPath);
if(!file.exists() || !file.isFile()){
print(propertiesPath + "文件不存在");
return;
} else {
print("\n配置文件地址:" + file.getAbsolutePath());
}
Properties properties = new Properties();
try {
properties.load(FreemarkerUtil.newReader(file.getAbsolutePath()));
print("\n读取配置文件成功");
generator(properties);
} catch (Exception e) {
e.printStackTrace();
print("出错了:" + e.getMessage());
}
}
上面是main方法,该方法主要目的是获取模板配置文件,可以通过参数来指定,获取配置文件后,调用 generator(properties)
方法来生成模板,我们接着看这个方法。
public static void generator(Properties properties) throws Exception {
String basePath = properties.getProperty("gen.basePath");
String className = properties.getProperty("gen.className");
String EntityName = properties.getProperty("gen.EntityName");
String author = properties.getProperty("gen.author");
String java = properties.getProperty("gen.java");
List<Fd> fieldList = readFromEntityFile(java, properties);
Date createTime = new Date();
String Entity = className.substring(className.lastIndexOf(".") + 1);
String entity = Introspector.decapitalize(Entity);
String folder = className.substring(0, className.lastIndexOf("."));
folder = folder.substring(0, folder.lastIndexOf("."));
String basePck = folder.replaceAll("\\.", "/");
if(!basePck.startWith("/")){
basePck = "/" + basePck;
}
if(!basePck.endsWith("/")){
basePck += "/";
}
folder = folder.substring(folder.lastIndexOf(".") + 1);
String mapping = folder + Entity;
//参数
Map<String, Object> params = new HashMap<String, Object>();
params.put("folder", folder);
params.put("Entity", Entity);
params.put("entity", entity);
params.put("EntityName", EntityName);
params.put("mapping", mapping);
params.put("author", author);
params.put("createTime", createTime);
params.put("fields" fieldList);
//路径
if(!basePath.endsWith("/")){
basePath += "/";
}
basePath = basePath + Entity + "/";
basePath = basePath.replaceAll("\\\\", "/");
String javaPath = mkdirs(basePath + "src/main/java" + basePck);
String daoPath = mkdirs(javaPath + "dao/");
String controllerPath = mkdirs(javaPath + "controller/");
String servicePath = mkdirs(javaPath + "service/");
String serviceImplPath = mkdirs(servicePath + "impl/");
String daoXmlPath = mkdirs(basePath + "src/main/resources" + basePck + "dao/");
String webPath = mkdirs(basePath + "src/main/webapp/WEB-INF/views/" + folder + "/");
print("\n开始生成模板\n\n生成文件的基础路径:\n\n" + basePath);
//根据配置生成模板
String controller = properties.getProperty("template.controller");
if(isNotEmpty(controller)){
FreemarkerUtil.processTemplate(controller, params, controllerPath + Entity + "Controller.java");
print("\n生成Controller.java:\n" + controllerPath + Entity + "Container.java");
}
//其他模板...
//...
}
这里主要就是根据配置文件提取了一些模板中会用到的各种信息。后面根据maven的项目结构创建了不同的目录。
Generator
这段代码的整体逻辑还是很简单的,有java基础的都应该能看懂,有疑问的可以留言。
模板示例
针对通用Mapper的一个接口模板
dao.ftl
:
package com.github.abel533.blog.${folder}.dao;
import com.github.abel533.blog.${folder}.model.${Entity};
import com.github.abel533.common.Isea533Mapper;
/**
* ${Entity} Mapper
*
* @author ${author}
* @since ${createTime?string('yyyy-MM-dd HH:mm:ss')}
*/
public interface ${Entity}Mapper extends Isea533Mapper<${Entity}> {
//TODO 请确认该包已经配置到mybatis的扫描路径下
//自定义的方法可以写在下面
}
如果你用模板来生成,你就不会纠结如何生成XXXDao或者XXXMapper样子的接口了。
dao_xml.ftl
:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.github.abel533.blog.${folder}.dao.${Entity}Dao">
</mapper>
JSP页面
由于JSP要符合自己项目的特点和要求,这里就举个字段循环的例子:
<div class="table-index">
<table width="96.4%" border="0" cellspacing="0" cellpadding="0">
<thead>
<tr style="background:#e2e2e2;line-height:25px;">
<#list fields as field>
<th>${field.text}</th>
</#list>
<th>操作</th>
</tr>
</thead>
<tbody>
<c:foreach items="${r'${page.list}'}" var="entity">
<tr>
<#list fields as field>
<#if field.date>
<td><fmt:formatDate value="${"$"}{entity.${field.name}}"
pattern="yyyy-MM-dd HH:mm:ss" /></td>
<#else>
<td>${"$"}{entity.${field.name}}</td>
<#/if>
</#list>
<td>
<a href="${"$"}{ctx }/${mapping}/view?id=${"$"}{entity.id}">详细信息</a>
<a href="${"$"}{ctx }/${mapping}/edit?id=${"$"}{entity.id}">修改${EntityName}</a>
<a href="${"$"}{ctx }/${mapping}/delete?id=${"$"}{entity.id}">删除${EntityName}</a>
</td>
</tr>
</c:foreach>
</tbody>
</table>
</div>
运行
为了方便,我会创建一个基础的模板,然后每次要生成某个对象的时候,我就复制文件,然后修改模板。
假设上述代码最后打包为 gen.jar
文件,配置文件为 SysRole.properties
,那么只需要在CMD执行:
java -jar gen.jar SysRole
注: .properties
后缀可以省略,但是配置文件必须是这个后缀
最后
如果你用过几次代码生成器相信你一定会喜欢上这种方式,如果你有大量工作可以用生成器自动生成,那么一定动手写一个生成器,不要以为写生成器会浪费时间,这在之后会给你节约大量的时间,说不定能摆脱加班的命运。
对一个新项目来说,如果提供一个代码生成器,整个项目会有一个更一致的风格,也能对项目效率起到至关重要的作用。
如果你还没习惯这种方式,找时间试试吧,写一个可以写代码的程序!
本篇博客只是一个最简代码生成器的例子,功能未必全面,但是已经可以实现很多代码的生成,如果你想写一个从获取数据库表信息开始生成实体代码和各种模板代码的例子,你可以参考下面这个项目:
http://git.oschina.net/free/DBMetadata
这个项目支持获取多种数据库的注释和其他基本信息,并且能更好的应用于MyBatis相关的代码生成。