Mybatis generator代碼生成


背景

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

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

於是想使用Mybatis generator這個工具來統一生成代碼(java bean,mapper,xml)

使用

Mybatis generator可以通過如下方式運行

  • 命令行
下載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是沒有分頁查詢的,可通過

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

來實現分頁,不過...

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

可使用pagehelper來解決,See Github

除此之外,我們也可以通過自定義分頁插件來解決

  • 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了。

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

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

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改成false

// 可以在前面自定義的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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM