背景
項目中使用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