Mybatis generator代碼生成

背景

項目中使用Mybatis作持久層框架,但因爲開發成員水平不一,寫dao的時候,各有各的偏好,有時候還會寫出帶sql注入漏洞的代碼。java

出現sql注入漏洞,通常是#和$的區別沒弄明白:
$ 直接把字符串原封不動的搬進sql,有sql注入的風險
# 是預留一個問號,做爲參數插入的,便可經過預編譯sql的方式避免sql注入

因而想使用Mybatis generator這個工具來統一輩子成代碼(java bean,mapper,xml)mysql

使用

Mybatis generator能夠經過以下方式運行git

  • 命令行
下載mybatis-generator-core.jar,而後配置generatorConfig.xml文件,執行以下命令
java -jar mybatis-generator-core-1.3.7.jar -configfile generatorConfig.xml -overwrite
  • IDE插件,run as
安裝eclipse/idea插件
  • 經過main方法執行
public static void main(String[] args) throws Exception {
    List<String> warnings = new ArrayList<String>();
    boolean overwrite = true;
    ConfigurationParser cp = new ConfigurationParser(warnings);

    Configuration config = cp.parseConfiguration(MainGenerate.class.getClassLoader().getResourceAsStream("generatorConfig.xml"));

    DefaultShellCallback callback = new DefaultShellCallback(overwrite);
    MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config, callback, warnings);
    myBatisGenerator.generate(null);
    System.out.println("----done----");
}

問題及解決方法

分頁問題

默認生成的xml是沒有分頁查詢的,可經過github

<plugin type="org.mybatis.generator.plugins.RowBoundsPlugin">

來實現分頁,不過...sql

  • 在低版本的generator插件裏是不包含這個的。
  • 使用這個插件生成分頁代碼後,會多一個selectByExampleWithRowbounds(XxxExample example, RowBounds rowBounds) 的方法,可是XxxMapper.xml文件中的selectByExampleWithRowbounds元素,能夠發現select語句並無使用limit 或者 rownum。
    實際上RowBounds原理是經過ResultSet的遊標來實現分頁,容易出現性能問題
解決辦法

可以使用pagehelper來解決,See Github
shell

除此以外,咱們也能夠經過自定義分頁插件來解決數據庫

  • Oracle插件
package com.yejg.mybatis.generator.plugins;

// 省略import

public class OraclePaginationPlugin extends PluginAdapter {

    public boolean modelExampleClassGenerated(TopLevelClass topLevelClass, IntrospectedTable introspectedTable) {
        PrimitiveTypeWrapper integerWrapper = FullyQualifiedJavaType.getIntInstance().getPrimitiveTypeWrapper();

        Field begin = new Field();
        begin.setName("begin");
        begin.setVisibility(JavaVisibility.PRIVATE);
        begin.setType(integerWrapper);
        topLevelClass.addField(begin);
        context.getCommentGenerator().addFieldComment(begin, introspectedTable);

        Method setBegin = new Method();
        setBegin.setVisibility(JavaVisibility.PUBLIC);
        setBegin.setName("setBegin");
        setBegin.addParameter(new Parameter(integerWrapper, "begin"));
        setBegin.addBodyLine("this.begin = begin;");
        topLevelClass.addMethod(setBegin);
        context.getCommentGenerator().addGeneralMethodComment(setBegin, introspectedTable);
        
        Method getBegin = new Method();
        getBegin.setVisibility(JavaVisibility.PUBLIC);
        getBegin.setReturnType(integerWrapper);
        getBegin.setName("getBegin");
        getBegin.addBodyLine("return begin;");
        topLevelClass.addMethod(getBegin);
        context.getCommentGenerator().addGeneralMethodComment(getBegin, introspectedTable);

        Field end = new Field();
        end.setName("end");
        end.setVisibility(JavaVisibility.PRIVATE);
        end.setType(integerWrapper);
        topLevelClass.addField(end);
        context.getCommentGenerator().addFieldComment(end, introspectedTable);

        Method setEnd = new Method();
        setEnd.setVisibility(JavaVisibility.PUBLIC);
        setEnd.setName("setEnd");
        setEnd.addParameter(new Parameter(integerWrapper, "end"));
        setEnd.addBodyLine("this.end = end;");
        topLevelClass.addMethod(setEnd);
        context.getCommentGenerator().addGeneralMethodComment(setEnd, introspectedTable);

        Method getEnd = new Method();
        getEnd.setVisibility(JavaVisibility.PUBLIC);
        getEnd.setReturnType(integerWrapper);
        getEnd.setName("getEnd");
        getEnd.addBodyLine("return end;");
        topLevelClass.addMethod(getEnd);
        context.getCommentGenerator().addGeneralMethodComment(getEnd, introspectedTable);

        return true;
    }

    @Override
    public boolean sqlMapDocumentGenerated(Document document, IntrospectedTable introspectedTable) {
        XmlElement parentElement = document.getRootElement();

        XmlElement paginationPrefixElement = new XmlElement("sql");
        paginationPrefixElement.addAttribute(new Attribute("id", "Oracle_Paging_Prefix"));
        XmlElement pageStart = new XmlElement("if");
        pageStart.addAttribute(new Attribute("test", "begin != null and end != null"));
        pageStart.addElement(new TextElement("select * from ( select row_.*, rownum rownum_ from ( "));
        context.getCommentGenerator().addComment(paginationPrefixElement);
        paginationPrefixElement.addElement(pageStart);
        parentElement.addElement(paginationPrefixElement);

        XmlElement paginationSuffixElement = new XmlElement("sql");
        paginationSuffixElement.addAttribute(new Attribute("id", "Oracle_Paging_Suffix"));
        XmlElement pageEnd = new XmlElement("if");
        pageEnd.addAttribute(new Attribute("test", "begin != null and end != null"));
        pageEnd.addElement(new TextElement("<![CDATA[ ) row_ ) where rownum_ > #{begin} and rownum_ <= #{end} ]]>"));
        context.getCommentGenerator().addComment(paginationSuffixElement);
        paginationSuffixElement.addElement(pageEnd);
        parentElement.addElement(paginationSuffixElement);

        return super.sqlMapDocumentGenerated(document, introspectedTable);
    }

    @Override
    public boolean sqlMapSelectByExampleWithoutBLOBsElementGenerated(XmlElement element, IntrospectedTable introspectedTable) {

        XmlElement pageStart = new XmlElement("include"); //$NON-NLS-1$
        pageStart.addAttribute(new Attribute("refid", "Oracle_Paging_Prefix"));
        // context.getCommentGenerator().addComment(pageStart);
        element.getElements().add(0, pageStart);

        XmlElement isNotNullElement = new XmlElement("include"); //$NON-NLS-1$
        isNotNullElement.addAttribute(new Attribute("refid", "Oracle_Paging_Suffix"));
        // context.getCommentGenerator().addComment(isNotNullElement);
        element.getElements().add(isNotNullElement);

        return super.sqlMapUpdateByExampleWithoutBLOBsElementGenerated(element, introspectedTable);
    }

    /**
     * This plugin is always valid - no properties are required
     */
    public boolean validate(List<String> warnings) {
        return true;
    }
}
  • MySQl插件
package com.yejg.mybatis.generator.plugins;

// 省略import

public class MySQLPaginationPlugin extends PluginAdapter {

    @Override
    public boolean validate(List<String> list) {
        return true;
    }

    /**
     * 爲每一個Example類添加limit和offset屬性已經set、get方法
     */
    @Override
    public boolean modelExampleClassGenerated(TopLevelClass topLevelClass, IntrospectedTable introspectedTable) {

        PrimitiveTypeWrapper integerWrapper = FullyQualifiedJavaType.getIntInstance().getPrimitiveTypeWrapper();

        Field limit = new Field();
        limit.setName("limit");
        limit.setVisibility(JavaVisibility.PRIVATE);
        limit.setType(integerWrapper);
        topLevelClass.addField(limit);

        Method setLimit = new Method();
        setLimit.setVisibility(JavaVisibility.PUBLIC);
        setLimit.setName("setLimit");
        setLimit.addParameter(new Parameter(integerWrapper, "limit"));
        setLimit.addBodyLine("this.limit = limit;");
        topLevelClass.addMethod(setLimit);

        Method getLimit = new Method();
        getLimit.setVisibility(JavaVisibility.PUBLIC);
        getLimit.setReturnType(integerWrapper);
        getLimit.setName("getLimit");
        getLimit.addBodyLine("return limit;");
        topLevelClass.addMethod(getLimit);

        Field offset = new Field();
        offset.setName("offset");
        offset.setVisibility(JavaVisibility.PRIVATE);
        offset.setType(integerWrapper);
        topLevelClass.addField(offset);

        Method setOffset = new Method();
        setOffset.setVisibility(JavaVisibility.PUBLIC);
        setOffset.setName("setOffset");
        setOffset.addParameter(new Parameter(integerWrapper, "offset"));
        setOffset.addBodyLine("this.offset = offset;");
        topLevelClass.addMethod(setOffset);

        Method getOffset = new Method();
        getOffset.setVisibility(JavaVisibility.PUBLIC);
        getOffset.setReturnType(integerWrapper);
        getOffset.setName("getOffset");
        getOffset.addBodyLine("return offset;");
        topLevelClass.addMethod(getOffset);

        return true;
    }

    /**
     * 爲Mapper.xml的selectByExample添加limit
     */
    @Override
    public boolean sqlMapSelectByExampleWithoutBLOBsElementGenerated(XmlElement element, IntrospectedTable introspectedTable) {

        XmlElement ifLimitNotNullElement = new XmlElement("if");
        ifLimitNotNullElement.addAttribute(new Attribute("test", "limit != null"));

        XmlElement ifOffsetNotNullElement = new XmlElement("if");
        ifOffsetNotNullElement.addAttribute(new Attribute("test", "offset != null"));
        ifOffsetNotNullElement.addElement(new TextElement("limit ${offset}, ${limit}"));
        ifLimitNotNullElement.addElement(ifOffsetNotNullElement);

        XmlElement ifOffsetNullElement = new XmlElement("if");
        ifOffsetNullElement.addAttribute(new Attribute("test", "offset == null"));
        ifOffsetNullElement.addElement(new TextElement("limit ${limit}"));
        ifLimitNotNullElement.addElement(ifOffsetNullElement);

        element.addElement(ifLimitNotNullElement);

        return true;
    }
}

生成的xml不是覆蓋舊文件,有時還有重複的段

問題緣由在於:
在IntrospectedTableMyBatis3Impl.getGeneratedXmlFiles方法中,isMergeable值被寫死爲true了。mybatis

GeneratedXmlFile gxf = new GeneratedXmlFile(document,
        getMyBatis3XmlMapperFileName(), getMyBatis3XmlMapperPackage(),
        context.getSqlMapGeneratorConfiguration().getTargetProject(),
        true, context.getXmlFormatter());

而MyBatisGenerator.writeGeneratedXmlFile方法中使用到該屬性了。代碼以下:oracle

if (targetFile.exists()) {
    if (gxf.isMergeable()) {
        source = XmlFileMergerJaxp.getMergedSource(gxf, targetFile);
    } else if (shellCallback.isOverwriteEnabled()) {
        source = gxf.getFormattedContent();
        warnings.add(getString("Warning.11", targetFile.getAbsolutePath()));
    } else {
        source = gxf.getFormattedContent();
        targetFile = getUniqueFileName(directory, gxf.getFileName());
        warnings.add(getString("Warning.2", targetFile.getAbsolutePath()));
    }
} else {
    source = gxf.getFormattedContent();
}
解決辦法

方法一:可直接修改源碼,把isMergeable寫成false

方法二:拿到GeneratedXmlFile對象,經過反射把isMergeable改爲falseapp

// 能夠在前面自定義的Plugin中,sqlMapGenerated方法中拿到GeneratedXmlFile對象
public boolean sqlMapGenerated(GeneratedXmlFile sqlMap, IntrospectedTable introspectedTable) {
    try {
        java.lang.reflect.Field field = sqlMap.getClass().getDeclaredField("isMergeable");
        field.setAccessible(true);
        field.setBoolean(sqlMap, false);
    } catch (Exception e) {
        
    }
    return true;
}

註釋問題

默認的註釋徹底沒什麼用,不如自定義註釋,把數據庫表字段的註釋做爲bean字段的註釋

解決辦法
public class MyCommentGenerator implements CommentGenerator {

    private Properties systemPro;
    private boolean suppressAllComments;
    private SimpleDateFormat dateFormat;

    public MyCommentGenerator() {
        super();
        systemPro = System.getProperties();
        suppressAllComments = false;
        dateFormat = new SimpleDateFormat("yyyy-MM-dd");
    }

    /**
     * 生成java model的類頭上的註釋
     */
    @Override
    public void addModelClassComment(TopLevelClass topLevelClass, IntrospectedTable introspectedTable) {
        if (suppressAllComments) {
            return;
        }

        topLevelClass.addJavaDocLine("/**");
        StringBuffer sb = new StringBuffer();
        sb.append(" * ");
        sb.append(introspectedTable.getRemarks());
        sb.append(" [");
        sb.append(introspectedTable.getFullyQualifiedTable().toString().toLowerCase());
        sb.append("]");
        topLevelClass.addJavaDocLine(sb.toString());
        topLevelClass.addJavaDocLine(" * @author " + systemPro.getProperty("user.name"));
        topLevelClass.addJavaDocLine(" */");
    }

    /**
     * 添加字段註釋
     */
    @Override
    public void addFieldComment(Field field, IntrospectedTable introspectedTable, IntrospectedColumn introspectedColumn) {
        if (suppressAllComments) {
            return;
        }
        StringBuilder sb = new StringBuilder();
        sb.append("/** ").append(introspectedColumn.getRemarks().replace("\n", " ")).append(" */");
        field.addJavaDocLine(sb.toString());
    }

    // 省略了其餘方法
}

不過在使用的時候發現經過
introspectedColumn.getRemarks()
獲取到的註釋爲null,此問題可經過修改xml配置文件來處理

<jdbcConnection driverClass="oracle.jdbc.driver.OracleDriver" connectionURL="jdbc:oracle:thin:@127.0.0.1:1521:YEJG" userId="XXX" password="XXX">
      <!-- 針對oracle數據庫 -->
      <property name="remarksReporting" value="true" />
      
      <!-- 針對mysql數據庫 -->
      <!-- <property name="useInformationSchema" value="true" /> -->
</jdbcConnection>

序列問題

其實這算不算什麼問題,xml配置一下就能夠了

<table tableName="users">
    <property name="useActualColumnNames" value="true" />
    <generatedKey type="pre" column="SERIAL_NO" sqlStatement="select users_seq.nextval from dual"></generatedKey>
</table>

這裏須要注意下,generatedKey不要寫在property前面了,mybatis generator對順序有要求的。

字段命名方式問題

數據庫表字段是USER_ID形式,生成的bean的字段變成userId形式

解決辦法

可在generatorConfig.xml中添加以下配置

<property name="useActualColumnNames" value="true" />

不過這麼一來,bean中的字段就都變成大寫的了,指望生成user_id的形式,可經過修改源碼來解決

// DatabaseIntrospector#getColumns,把column_name先toLowerCase處理一下
introspectedColumn.setActualColumnName(rs.getString("COLUMN_NAME").toLowerCase());

關於代碼

以上代碼已上傳到Github

相關文章
相關標籤/搜索