最近刚买回一本关于java-web轻量级框架的设计书籍,希望能开阔视野,同时也复习一下快忘完的知识(其实也是在进入公司之后,所做的本质工作比较基础,在公司较老的技术框架,成熟的产品下,越干,越会感受到一种焦虑,很多贴近行业的开源的东西,一些基础的技术知识长时间不用,也有些遗忘),一遍看书,一遍敲代码。对于一些公用的工具,作为菜鸟的我,总希望自己也能完成一些基础的实现(有人说重复早轮子是无意义的,但是闲的时候自己去写一写,总会有一些收获,当然,工作之中为了稳定性和开发效率,当然会直接选用成熟的),于是有了本文。
在编写书中基础实现的部分内容时,看到在做jdbc的编码部分,作者由最直白的代码,逐步重构,减少代码冗余,逐步将重复的编码部分向上提取,逐步变得可重用和透明,大大简化业务层的数据操作代码,看的十分爽快。jdbc部分的编码已有很多的成熟的框架,各种设计模式和方法都包含其中,是一个值得模仿的轮子。自己便写了一些简单的实现。
jdbc部分的编码是模式化,重复的,开启连接,预编译sql,执行数据库操作,封装返回的结果集。各种不同的业务主体的操作并无二致,十分累人。要简化开发,就需要将那些冗长的模式化的东西向上提取作为公共的内容,而针对每个模块下的业务内容只编写与具体业务相关的,那些有差异,有变化的内容。将变化的内容传入上层的模板化的方法之中。大大简化开发。
数据库连接的获得:
一、数据库驱动应该是可配置,便于数据库的迁移,获取连接,关闭连接的方法封装起来可以减少处理异常的麻烦,使用静态定义能更方便的进行操作
private static final Logger LOGGER = LoggerFactory.getLogger(DBHelper.class);
private static final ThreadLocal<Connection> CONNECTION_THREAD_LOCAL = new ThreadLocal<>();
private static final String DRIVER;
private static final String URL;
private static final String USERNAME;
private static final String PASSWORD;
static {
Properties props = PropUtil.loadProperties("dbconfig.properties");
DRIVER = props.getProperty("jdbc.driver");
URL = props.getProperty("jdbc.url");
USERNAME = props.getProperty("jdbc.username");
PASSWORD = props.getProperty("jdbc.password");
}
/**
* 获得数据库连接
* @return
*/
public static Connection getConnection() {
Connection conn = CONNECTION_THREAD_LOCAL.get();
if (conn == null) {
try {
Class.forName(DRIVER);
conn = DriverManager.getConnection(URL, USERNAME, PASSWORD);
} catch (ClassNotFoundException e) {
LOGGER.error(DRIVER + "load database driver failure", e);
} catch (SQLException e) {
LOGGER.error("get connection failure", e);
} finally {
CONNECTION_THREAD_LOCAL.set(conn);
}
}
return conn;
}
/**
* 关闭数据库连接
* @param statement
* @param resultSet
*/
public static void close(Statement statement, ResultSet resultSet) {
if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException e) {
LOGGER.error("close resultset failure", e);
} finally {
resultSet = null;
}
}
if (statement != null) {
try {
statement.close();
} catch (SQLException e) {
LOGGER.error("close statement failure", e);
} finally {
statement = null;
}
}
Connection conn = CONNECTION_THREAD_LOCAL.get();
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
LOGGER.error("close connection failure", e);
} finally {
conn = null;
CONNECTION_THREAD_LOCAL.remove();
}
}
}
二、操作连接进行查询,套路完全固定,将变化的sql,查询参数项,实体类型作为参数传递进去,定义泛型方法实现g
/**
* 获取实体的列表
* @param sql
* @param params
* @param entityClass
* @param <T>
* @return
*/
public <T> List<T> getBeanList(String sql, Map<String, Object> params, Class entityClass) {
Connection conn = DBHelper.getConnection();
PreparedStatement pstmt = null;
ResultSet rs = null;
List<T> entityList = new ArrayList<T>();
try {
pstmt = conn.prepareStatement(sql);
LOGGER.info("execute " + sql + " ? ==>" + params);
rs = pstmt.executeQuery();
entityList = rs2BeanList(sql,rs,entityClass);
} catch (SQLException e) {
LOGGER.error("查询数据失败 sql==> " + sql, e);
} finally {
DBHelper.close(pstmt,rs);
}
return entityList;
}
/**
* 获取单个实体
* @param sql
* @param params
* @param entityClass
* @param <T>
* @return
*/
public <T> T getSingleBean(String sql, Map<String, Object> params, Class entityClass) {
// TODO
Connection conn = DBHelper.getConnection();
PreparedStatement pstmt = null;
T entity = null;
ResultSet rs = null;
try {
pstmt = conn.prepareStatement(sql);
setQueryParams(pstmt,params);
LOGGER.info("execute : " + sql + " ?==>" + params);
rs = pstmt.executeQuery();
String[] cols = getColsName(sql);
if (rs.next()) {
entity = rsCol2Bean(cols,rs,entityClass);
}
} catch (SQLException e) {
LOGGER.error("查询数据失败 sql==>" + sql + "?==>" + params, e);
} finally {
DBHelper.close(pstmt,rs);
}
return entity;
}
三、查询参数的赋值是一个变化的项,统一起来用容器封装,方便进行赋值操作,当然一下可能是直白的方式,还需要更多的设计和考虑,但是一定程度起到了简化开发的目的
/**
* 为sql查询赋值
* @param pstmt
* @param params
* @throws SQLException
*/
private void setQueryParams(PreparedStatement pstmt, Map<String, Object> params) throws SQLException {
// where key1 = value1 and key2 = value2
int index = 1;
for (String key : params.keySet()) {
pstmt.setObject(index++,params.get(key));
}
}
四、结果集的处理虽然不同业务实体具体实现不同,但概况的看依然是有其规律性和重复性的,这里利用java语音重要的动态特性:反射技术,实现对单表实体的自动映射,其他多表连接复杂查询可以通过Map容器进行结果的接收返回,思路大致一样,当然这只是简单的技术实践,可能在稳定性,安全性都有所欠缺,但是对于技术点的连习还是有益处的(个人感受)
/**
* 结果集行转为bean
* @param rs
* @param entityClass
* @param <T>
* @return
* @throws SQLException
*/
private <T> T rsCol2Bean(String[] cols, ResultSet rs, Class entityClass) throws SQLException {
T entity = null;
try {
entity = (T) entityClass.newInstance();
} catch (InstantiationException e) {
LOGGER.error("实例化 " + entityClass.toString() + "失败", e);
} catch (IllegalAccessException e) {
LOGGER.error("实例化 " + entityClass.toString() + "非法访问", e);
}
//反射获得field进行set
if (cols != null) {
initFieldByCols(cols, rs, entityClass, entity);
} else {
initFieldByEntity(rs, entityClass, entity);
}
return entity;
}
/**
* 根据bean属性名初始化bean
* @param rs
* @param entityClass
* @param entity
* @param <T>
* @throws SQLException
*/
private <T> void initFieldByEntity(ResultSet rs, Class entityClass, T entity) throws SQLException {
Field[] fields = entityClass.getDeclaredFields();
for (Field field : fields) {
Object obj = rs.getObject(field.getName());
if (obj != null) {
field.setAccessible(true);
try {
field.set(entity,obj);
} catch (IllegalAccessException e) {
LOGGER.error("字段映射异常" + field + "非法访问", e);
}
}
}
}
/**
* 根据查询列名初始化bean属性
* @param cols
* @param rs
* @param entityClass
* @param entity
* @param <T>
* @throws SQLException
*/
private <T> void initFieldByCols(String[] cols, ResultSet rs, Class entityClass, T entity) throws SQLException {
Object value = null;
for (String str : cols) {
value = rs.getObject(str);
if (value != null) {
Field field = null;
try {
field = entityClass.getDeclaredField(str);
field.setAccessible(true);
field.set(entity,value);
} catch (IllegalAccessException e) {
LOGGER.error("字段映射异常" + field + "非法访问", e);
} catch (NoSuchFieldException e) {
LOGGER.error("字段映射异常" + field + "不存在", e);
}
}
}
}
/**
* 获取查询的列名
* @param sql
* @return
*/
private String[] getColsName(String sql) {
String colName = sql.substring(sql.indexOf("select") + 6, sql.lastIndexOf("from")).trim();
String[] colsName = null;
if (!colName.equals("*")) {
String[] cols = colName.split(",");
colsName = new String[cols.length];
for (int i = 0; i < cols.length; i++) {
colsName[i] = cols[i].trim();
}
return colsName;
}
return null;
}
这里编写了采用查询列名进行实体封装和通过javabean本身属性进行封装的两种方式,主要考虑到可能会写“select * ...”之类的sql语句,采用列名反射的方式需要遵循table列名定义和javabean属性定义一致的规则,否则会有问题,此种方式开发更方便,数据库执行sql语句效率也会更高,但也有偷懒一个“*”搞定的人哦。
完成以上重构操作后,业务层的代码就很清晰,简介啦
private static final Logger LOGGER = LoggerFactory.getLogger(CustomerService.class);
/**
* 获取客户列表
* @param keyword
* @return
*/
public List<Customer> getCustomerList(String keyword) {
// TODO
String sql = "select id,name , telephone from customer";
Map<String,Object> fieldMap = null;
if (keyword != null) {
sql += " where remark like ?";
fieldMap = new HashMap<>();
fieldMap.put("remark",keyword);
}
return new DBHelper().getBeanList(sql,fieldMap,Customer.class);
}
/**
* 获取客户
* @param id
* @return
*/
public Customer getCustomer(long id) {
// TODO
String sql = "select * from customer where id = ?";
Map<String,Object> fieldMap = new HashMap<>();
fieldMap.put("id",id);
return new DBHelper().getSingleBean(sql, fieldMap, Customer.class);
}
以上是看书过程中的一点小练习,一次记录一下,可能也能有益于他人,原书采用的是apache提供的DBUtil包进行了封装,笔者计划接下来就进行实践,便研究一下apache大神们的实现哦!相信会有很大收益呢!
初次写文,毫无经验,有点啰嗦了,也不知道有些东西表达清楚没,技术的大海,无边无界,一只小虾米在其中,难免有不足,错误之处,欢迎批评,建议,指正。━(*`∀´*)ノ亻!