0
点赞
收藏
分享

微信扫一扫

使用JDBCTemplate时自动生成RowMapper的两种方式

在开始之前,我们首先回顾下RowMapper怎么用的,示例代码如下:

public class UserEntityMapper implements RowMapper<UserEntityMapper> {
@Override
public UserEntity mapRow(ResultSet rs, int rowNum) throws SQLException {
UserEntity user = new UserEntity();
user.setId(rs.getLong("id"));
user.setName(rs.getString("name"));
// 省略....
return user;
}
}

假设有1000个表的时候,这样的代码要写1000次。

接下来一起看一下怎么来生成这个类,介绍两种方式,一种是基于javassist的字节码生成,另一种是jdk的代理。


一 基于javassist的字节码生成

这种方式再jdk9及以后编译会报一个和模块化有关的异常(java.base。。。),当然也能够绕过去,具体可参考https://www.logicbig.com/tutorials/core-java-tutorial/modules/illegal-access-operations.html。

这里来看一下怎么生成?需要一个前提条件:bean与数据库表的对应关系、bean的成员变量与数据库字段的对应关系。这里使用两个自定义注解@Table(用于类的注解)和@Column(用于字段的注解),注解代码和使用注解的bean代码如下。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Table {
String value();
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Column {
String value();
}
@Table(value = "t_user")
public class UserEntity extends BaseEntity {
@Column("name")
private String name;
@Column("age")
private Integer age;
}

有了上面的基础之后,就是在使用RowMapper的时候怎么来生成了。实践中提供了几个方法来共同完成这件事。

下面是一个获取和Entity对应的RowMapper的入口方法:

public static <T extends BaseEntity> RowMapper getRowMapper(Class<T> clazz) {
if (!rowMapperMap.containsKey(clazz.getName())) {
synchronized (Object.class) {
if (!rowMapperMap.containsKey(clazz.getName())) {
rowMapperMap.put(clazz.getName(), newInstance(clazz));
}
}
}
return rowMapperMap.get(clazz.getName());
}
private static RowMapper newInstance(Class clazz) {
try {
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(clazz));
CtClass ctInterface = pool.get(RowMapper.class.getName());
CtClass ctClass = pool.makeClass(clazz.getName() + "RowMapper");
ctClass.setModifiers(Modifier.PUBLIC);
ctClass.addInterface(ctInterface);
StringBuilder builder = new StringBuilder();
builder.append("public ").append(clazz.getName()).append(" mapRow(java.sql.ResultSet rs, int index) throws java.sql.SQLException {\n");
builder.append(" ").append(clazz.getName()).append(" entity = new ").append(clazz.getName()).append("(); \n");
getColumnFieldMapper(clazz).getColumnFieldMap().forEach((column, field) -> {
builder.append(" entity.set");
if (field.getType().isAssignableFrom(String.class)) {
builder.append(upperCaseFirstChar(field.getName())).append("(").append("rs.getString(\"").append(column).append("\"));\n");
} else if (field.getType().isAssignableFrom(Long.class)) {
builder.append(upperCaseFirstChar(field.getName())).append("(").append("Long.valueOf(rs.getLong(\"").append(column).append("\")));\n");
} else if (field.getType().isAssignableFrom(Integer.class)) {
builder.append(upperCaseFirstChar(field.getName())).append("(").append("Integer.valueOf(rs.getInt(\"").append(column).append("\")));\n");
} else if (field.getType().isAssignableFrom(Date.class)) {
builder.append(upperCaseFirstChar(field.getName())).append("(").append("new Date(rs.getTimestamp(\"").append(column).append("\").getTime());\n");
} else if (field.getType().isAssignableFrom(java.math.BigDecimal.class)) {
builder.append(upperCaseFirstChar(field.getName())).append("(").append("rs.getBigDecimal(\"").append(column).append("\"));\n");
} else if (field.getType().isAssignableFrom(Double.class)) {
builder.append(upperCaseFirstChar(field.getName())).append("(").append("Double.valueOf(rs.getDouble(\"").append(column).append("\")));\n");
} else if (field.getType().isAssignableFrom(Float.class)) {
builder.append(upperCaseFirstChar(field.getName())).append("(").append("Float.valueOf(rs.getFloat(\"").append(column).append("\")));\n");
} else if (field.getType().isAssignableFrom(Boolean.class)) {
builder.append(upperCaseFirstChar(field.getName())).append("(").append("Boolean.valueOf(rs.getBoolean(\"").append(column).append("\")));\n");
} else {
throw new MyRuntimeException("Type " + field.getType().getName() + " not supported.");
}
});
builder.append(" return entity;\n");
builder.append("}");
CtMethod newCtMethod = CtNewMethod.make(builder.toString(), ctClass);
ctClass.addMethod(newCtMethod);
Object instance = ctClass.toClass().getDeclaredConstructor().newInstance();
return (RowMapper) instance;
} catch (Exception e) {
logger.error("...");
throw new MyRuntimeException(e);
}
}
private static String upperCaseFirstChar(String source) {
return source.replaceFirst(source.substring(0, 1), source.substring(0, 1).toUpperCase());
}

上面这段代码有两个地方需要说明一下,14行的getColumnFieldMapper返回的就是下面这个map,而ColumnFieldMapper这个类的主要代码如下所示:

Map<Class, ColumnFieldMapper> clazzMapper = new ConcurrentHashMap<>();
public static class ColumnFieldMapper<T extends BaseEntity> {
private final Map<String, Field> ColumnFieldMap = new LinkedHashMap<>();
private final String insertSql;
private final String fromSql;
private final String deleteSql;
private final String countSql;
LabelFieldMapper(Class clazz) {
}
}

最后我们结合BaseDAO的数据库插入方法来看下具体使用(BaseDAO是在前一篇文章“使用AbstractRoutingDataSource同时支持多种数据库及主从基础上扩展而来):

public <T extends BaseEntity> int save(T entity) {
if (null == entity) {
throw new MyRuntimeException("BaseDAO save exception, input entity is null.");
}
String sql = getInsertSql(entity.getClass());
LocalDateTime now = LocalDateTime.now();
entity.setCreateTime(now);
entity.setUpdateTime(now);
try {
KeyHolder keyHolder = new GeneratedKeyHolder();
int result = getJdbcTemplate(true).update(sql, new MapSqlParameterSource(entityToMap(entity)), keyHolder);
entity.setId(keyHolder.getKey().longValue());
return result;
} catch (Exception e) {
throw new MyRuntimeException("BaseDAO save exception.", e);
} finally {
clear();
}
}

以上就是基于javassist的一种实现方式。


二 基于jdk proxy的代理方式

有了前面的内容做基础,接下来看下另一种方式的实现,其实需要替换掉的只有RowMapper newInstance(Class clazz)这个方法。

private RowMapper<T extends BaseEntity> newInstance(Class<T> clazz) {
RowMapper<T> rowMapper = (RowMapper<T>)Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{RowMapper.class}, new InvocationHandler() {
@Override
public E invoke(Object proxy, Method method, Object[] args) throws Throwable {
ResultSet rs = (ResultSet) args[0];
TE entity = clazz.getDeclaredConstructor().newInstance();
for (Map.Entry<String, Field> entry : getColumnFieldMapper(clazz).getColumnFieldMap().entrySet()) {
String column = entry.getKey();
Field field = entry.getValue();
Method method = ...;
if (null == method) {
throw new MyRuntimeException("...");
}
if (field.getType().isAssignableFrom(String.class)) {
method.invoke(instance, rs.getString(column));
} else if (field.getType().isAssignableFrom(Long.class)) {
method.invoke(instance, rs.getLong(column));
} else if (field.getType().isAssignableFrom(Integer.class)) {
method.invoke(instance, rs.getInt(column));
} else if (field.getType().isAssignableFrom(Date.class)) {
method.invoke(instance, new Date(rs.getTimestamp(column).getTime()));
} else if (field.getType().isAssignableFrom(java.math.BigDecimal.class)) {
method.invoke(instance, rs.getBigDecimal(column));
} else if (field.getType().isAssignableFrom(Double.class)) {
method.invoke(instance, rs.getDouble(column));
} else if (field.getType().isAssignableFrom(Float.class)) {
method.invoke(instance, rs.getFloat(column));
} else if (field.getType().isAssignableFrom(Boolean.class)) {
method.invoke(instance, rs.getBoolean(column));
} else {
throw new MyRuntimeException("...");
}
}
return entity;
}
});
return rowMapper;
}


这里面还有一些细节的问题,为了避免篇幅过程省略了。举几个例子:

1.update语句生成,根据可以根据实体更新,也可以根据其他方式更新;

2.示例代码里提供了column和field的映射,可以根据实际需要增加更多的映射关系;

3.BaseDAO只提供了save方法,其他方法类似(包括批量操作);

4.两种方式都只提供了有限个字段的从rs里取值并set到Entity里,可根据需要进行扩展,增加更多的字段类型;

5.异常、日志都被简化处理了,真实使用需要完善;


结束

如有问题和遗漏,欢迎指正错误和讨论。


     原文在我的公众号“平凡的程序员”。

使用JDBCTemplate时自动生成RowMapper的两种方式_RowMapper

举报

相关推荐

0 条评论