加快效率 - 最简单的代码生成器实现

标签: 代码 生成器 | 发表时间:2015-08-21 18:26 | 作者:isea533
出处:http://blog.csdn.net

加快效率 - 最简单的代码生成器实现

为什么需要代码生成器?

当工作中需要频繁复制粘贴来写程序的时候,更好的选择可能是写一个代码生成器来生成基础的内容,然后在此基础上进行修改和完善。

复制粘贴虽然简单,但是有很多不方便和潜在的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相关的代码生成。

作者:isea533 发表于2015/8/21 10:26:00 原文链接
阅读:93 评论:0 查看评论

相关 [代码 生成器] 推荐:

python 代码自动生成的方法 (代码生成器)

- - 大CC
python 代码自动生成的方法 (代码生成器). 工作中遇到这么一个事,需要写很多C++的底层数据库类,但这些类大同小异,无非是增删改查,如果人工来写代码,既费力又容易出错;而借用python的代码自动生成,可以轻松搞定;. (类比JAVA中的Hibernate自动生成的数据库底层操作代码). 下面介绍使用python字符串替换的方法;.

加快效率 - 最简单的代码生成器实现

- - CSDN博客推荐文章
加快效率 - 最简单的代码生成器实现. 当工作中需要频繁复制粘贴来写程序的时候,更好的选择可能是写一个代码生成器来生成基础的内容,然后在此基础上进行修改和完善. 复制粘贴虽然简单,但是有很多不方便和潜在的BUG. 复制粘贴许多时候还是需要我们修改一些变量名,做许多修改,有时候如果粗心没有修改全,反而会引入很多bug.

MakuGenerator v2.1.1 发布,超好用的代码生成器

- - 开源中国-软件更新资讯
maku-generator 是一款低代码生成器,可根据自定义模板内容,快速生成代码,可实现项目的快速开发、上线,减少重复的代码编写,开发人员只需专注业务逻辑即可. 采用 MIT 开源协议,完全免费开源,可免费用于. 升级element-plus到2.2.28. 升级springboot到2.7.7.

LINQ技术、EF技术都出来蛮久了,软件开发者、软件公司是否还有必要有自己的代码生成器?

- cchitsiang - 博客园-首页原创精华区
   有一段时间,也怀疑自己,是否有必要继续维护代码生成器. 因为微软的LINQ技术、EF技术都出来了,而且资料也开始越来越多了,代码生成器的功能越来越被这些新技术取代了,是否有必要还继续维护代码生成器. 今年的3件事情给了一些启发、加强了对代码生成器的认识高度.    1:在国家某部委开发项目,数据库需要用的是Oracle,而且需要控制的数据有上千万条记录,也不用SQLServer数据库,LINQ技术、EF技术派不上用处,应该也是暂时无法用得那么熟练,记得只对SQLServer数据库的支持是很好.

数学公式生成器

- ArBing - 阮一峰的网络日志
上一篇文章《数学常数e的含义》,有很多数学公式. 但是,在网页上显示数学公式,是一件非常麻烦的事情. 怎样才能把这个公式放到网页上呢. 传统的方式是,先在相关软件中把公式做出来,然后截图,再把图片贴到网页上,这样既麻烦又耗时. 我就在想,有没有便捷的方法,可以生成数学公式. 我知道,Google Chart接受TeX语言,实时返回数学公式的图片.

lorempixum: 占位图片生成器

- JunChen - 黑客志
有些时候,对于网站设计或者排版工作,你可能会需要一些临时占位图片,如果手边没有足够的素材,或许可以试试lorempixum,lorempixum是一个占位图片生成器,只需要通过URL就可以得到一张符合要求的随机图片,比如:. http://lorempixum.com/400/200,400×200的随机图片.

代码重构

- - ITeye博客
随着程序的演化,我们有必要重新思考早先的决策,并重写部分代码. 代码需要演化;它不是静态的事物. 重写、重做和重新架构代码合起来,称为重构.    当你遇到绊脚石  ---  代码不在合适,你注意到有两样东西其实应该合并或是其他任何对你来说是"错误"的东西  -------- . 如果代码具备以下特征,你都应该考虑重构代码:.

关于线程Thread、协程Coroutine、生成器Generator、yield资料

- tangsty - 我的宝贝孙秀楠 ﹣C++, Lua, 大连,程序员
关于Green Thread(绿色环保线程)、Native Thread,以及线程的一些普及问题,下面这个presentation最为翔实. 另外毫无疑问要看看维基百科上的这一条 http://en.wikipedia.org/wiki/Thread_%28computer_science%29. 这一篇教程可以帮助你了解Lua协程的基本用法http://lua-users.org/wiki/CoroutinesTutorial .

让protobuf生成器支持时间戳检查

- Ease - C++博客-首页原创精华区
使用protobuf的生成器可以对proto文件进行解析后生成指定的目标语言代码.随着项目的不断扩大, 协议修改变的非常频繁, 因此每次重编变的异常耗时. 模仿C/C++编译器的时间戳检查生成机制,我给protobuf生成器添加了时间戳检查功能. 此功能不是必须的, 可以通过命令行指定—timestampfile FILE 来指定要生成的proto文件对应的时间戳.

GPU并行计算版函数图像生成器

- Lionheart - 博客园-装配中的脑袋
前几天技术大牛Vczh同学开发了一个函数图像绘制程序,可以画出方程f(x,y)=0的图像. 他的原理是用图像上每一点的坐标带入函数f得到针对x和y的两个方程,再用牛顿迭代法求解得到一组点集,然后画到图像上. 用他的程序可以画出各种各样令人惊叹的方程图形. 但是他的程序非常慢,因为对每一个点坐标都用牛顿迭代法求解是一项很费时的任务,即使采用了Parallel.For,CPU算起来也很吃力.