从零开始写javaweb框架笔记8-细节完善与代码优化
2015-12-27 14:03
615 查看
在前面我们搭建了一个客户管理模块的代码框架,在代码中有很大的to do something或者to do,这些就是要逐渐完善的细节,我们将分别完善服务层,控制层,视图层,并且对代码进行优化。
1 完善服务层
添加SLF4J用于提供日志API,使用Log4J作为实现,配置如下:
上面把日志级别设置为ERROR,并且提供了两种日志appender,分别是console与file。需要对这两种日志分别配置,然后指定只有org.jack包下的类才能输出DEBUG级别的日志。
添加MySQL依赖,用于提供JDBC实现,配置如下:
添加两个Apache Commons 依赖,用于提供常用的工具类,配置如下:
打开CustomerService,下面开始实现getCustomerList方法。
该方法需要获取List,也就是所有的客户信息(此时并没有考虑分页,后续会增加此特性),我们需要执行一条select语句(Statement),获取相应的结果集(ResultSet),然后将其放入一个List中。不过在查询之前,我们必须获取数据库连接(Connection)
我们可使用JDBC来完成以上的操作,为了让数据库信息可配置,我们在classpath下创建一个config.properties文件,该文件位于src/main/resources目录中(注意:对于Maven目录结构而言,classpath指的就是java与resources这两个根目录。)
config.properties文件的内容如下:
属性文件准备好了,那么我们还需要一个类来读取该属性文件,改名编写一个PropsUtil工具类来进行读取config.properties里面的内容,下面是PropsUtil工具类的代码:
上面的类中,最关键的是loadProps方法,我们只需传递属性文件的名称,即可返回一个Properties对象,然后再根据getString,getInt,getBoolean这些方法由key获取指定类型的value,同时也可指定defaultValue作为默认值。
在PropsUtil类中,我们用到了CastUtil类,该类是为处理一些数据转型操作而作准备的,代码如下:
在CastUtil类中,我们用到了StringUtil类,它用于提供一些字符串操作,代码如下:
上面只是对Apache Commons类库做了一个简单的封装。同理,我们可以做一个CollectionUtil,用于提供一些集合操作,代码如下:
上面我们写了四个工具类,每个工具类的分工各不相同,它们在后面还会经常用到,并且会不断完善这些工具类。
现在回到CustomerService,我们需要在该类中执行数据库操作,也就是需要编写一些JDBC的代码,首先使用PropsUtil读取config.properties配置文件,获取与JDBC相关的配置项。
我们在CustomerService中为这些配置项定义一些常量,并提供一个“静态代码块”,来初始化这些常量,就像下面这样:
以getCustomerList为例,我们可以这样写JDBC代码:
选中getCustomerList方法右击,选择run进行单元测试,如果没有错误的话,应该是“绿条”,表示可以测试通过,相反,如果出现“红条”就表示测试失败,我们可以查看控制台以了解具体的错误原因。正确的测试结果如下图:
虽然以上的代码可以运行,基本的功能是可以实现的,但是问题还很多,具体包括以下2个方面:
1)在CustomerService类中读取config.properties文件,这是不合理的,毕竟将来还有很多其他Service类需要做同样的事情,我们最好能将这些公共性的代码提起出来。
2)执行一条select语句需要编写一大堆代码,而且必须使用try........catch.....finally结构,开发效率明显不高。
那么问题来了,我们该如何解决这两个问题?首先我们来解决第一个问题。
创建一个org.jack.com.helper包,在该包中创建一个DatabaseHelper类,代码如下:
现在CustomerService可以稍微的简化下了:
注意以上代码1,2两处的修改,的确简化了一些,至少每个Service类不会出现那些静态变量与代码块了,公共的代码放到了DatabaseHelper中从而得到了重用。
下面我们来解决第二个问题,实际上就是如何使代码变得更加精简。
著名的Apache Common项目中有一款名为DbUtils的类库,为我们提供了一个JDBC的封装,下面我们就用这款工具来解决该问题。
首先,需要在pom.xml中添加一个DbUtils的依赖:
然后,在DatabaseHelper中添加如下代码:
实际上,DbUtils首先执行SQL语句并返回一个ResultSet,随后通过反射去创建并初始化实体对象。由于我们需要返的是List,因此可以使用BeanListHandler。
最后,改写CustomerService的getCustomerList方法:
上面的代码,有问题,下面会进行改进。
现在的代码简单了很多,我们不再面对PrepareStatement与ResultSet了,只需要使用DatabaseHelper就能执行数据库操作。我们每次都需要创建一个Connection,然后进行数据库操作,最后关闭Connection。
如何能让Connection对开发人员透明?也就是说,如何隐藏掉创建与关闭Connection的代码呢?
为了确保一个线程只有一个Connection,我们可以使用ThreadLocal来存放本地线程变量。也就是说,将当前线程中的Connection放入ThreadLocal中存起来,这些Connection一定不会出现不安全问题,可以将ThreadLocal理解为一个隔离线程的容器。
现在对DatabaseHelper进行调整,具体代码如下:
每次获取Connection时,首先在ThreadLocal中寻找(见1),若不存在,则创建一个新的Connection,并将其放入ThreadLocal中(见2)。当Connection使用完毕后,需要移除ThreadLocal中持有的Connection(见3)
现在CustomerService的getCustomerList方法看起来更加简单了:
使用同样的技巧,我们可以在DatabaseHelper中添加一个queryEntity方法,用户查询单个实体对象。
需要注意的是,此时我们使用的BeanHandler,而不是BeanListHandler。实际上,DbUtils为我们提供了很多类似的Handler,包括:
BeanHandler-------------------------------返回Bean
BeanListHandler---------------------------返回List对象
BeanMapHandler--------------------------返回Map对象
ArrayHandler-----------------------------返回Object[]对象
ArrayListHandler---------------------------返回List对象
MapHandler--------------------------------返回Map对象
MapListHandler---------------------------返回List对象
ScalarHandler----------------------------返回某列的值
ColumnListHandler--------------------------返回某列的值列表
KeyedHandler-----------------------------------返回Map对象,需指定列名。
以上这些Handler都实现了ResultSetHandler。
查询并不一定是基于单表的,将来有可能连接多个表进行查询,因此我们有必要提供一个更为强大的查询方法,输入一个SQL与动态参数,输入一个List对象,其中的Map表示列名与列值得映射关系。
我们可以借助MapListHandler轻松实现:
除了查询语句以外,还包括几种更新语句,例如update,insert,delete等,我们再提供一个通用的执行更新语句的方法。
/**
* 执行更新语句(包括update,inert,delete)
*/
public static int executeUpdate(String sql,Object... params){
int rows=0;
try {
Connection conn=getConnection();
rows=QUERY_RUNNER.update(conn,sql,params);
}catch (SQLException e){
LOGGER.error("execute update failure",e);
throw new RuntimeException(e);
}finally {
closeConnection();
}
return rows;
}
该方法返回执行后受影响的行数,也就是说,更新了多少条记录。根据这个通用的方法,我们可以分别提供三种具体的数据库更新操作:
package org.jack.com.helper;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import org.apache.commons.dbutils.handlers.MapListHandler;
import org.jack.com.util.CollectionUtil;
import org.jack.com.util.PropsUtil;
import org.slf4j.LoggerFactory;
import org.slf4j.Logger;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
/**
* Created by jack on 2015/12/26.
* 数据库操作助手类
*/
public final class DatabaseHelper {
private static final Logger LOGGER = LoggerFactory.getLogger(DatabaseHelper.class);
private static final String DRIVER;
private static final String URL;
private static final String USERNAME;
private static final String PASSWORD;
static {
Properties conf = PropsUtil.loadProps("config.properties");
DRIVER = conf.getProperty("jdbc.driver");
URL = conf.getProperty("jdbc.url");
USERNAME = conf.getProperty("jdbc.username");
PASSWORD = conf.getProperty("jdbc.password");
try {
Class.forName(DRIVER);
} catch (ClassNotFoundException e) {
LOGGER.error("can not load jdbc driver", e);
}
}
private static final QueryRunner QUERY_RUNNER = new QueryRunner();
private static final ThreadLocal<Connection> CONNECTION_HOLDER = new ThreadLocal<Connection>();
/**
* 获取数据库连接
*/
public static Connection getConnection() {
Connection conn = CONNECTION_HOLDER.get();//1
if (conn == null) {
try {
conn = DriverManager.getConnection(URL, USERNAME, PASSWORD);
} catch (SQLException e) {
LOGGER.error("get connection failure", e);
} finally {
CONNECTION_HOLDER.set(conn);
}
}
return conn;
}
/**
* 关闭数据库连接
*/
public static void closeConnection() {
Connection conn=CONNECTION_HOLDER.get();//1
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
LOGGER.error("close connection failure", e);
throw new RuntimeException(e);
}finally {
CONNECTION_HOLDER.remove();
}
}
}
/**
* 查询实体列表
*/
public static <T> List<T> queryEntityList(Class<T> entityClass, String sql, Object... params) {
List<T> entityList;
try {
Connection conn = getConnection();
entityList = QUERY_RUNNER.query(conn, sql, new BeanListHandler<T>(entityClass), params);
} catch (SQLException e) {
LOGGER.error("query entity list failure", e);
throw new RuntimeException(e);
} finally {
closeConnection();
}
return entityList;
}
/**
* 查询实体
*/
public static <T> T queryEntity(Class<T> entityClass,String sql,Object... params){
T entity;
try {
Connection conn=getConnection();
entity=QUERY_RUNNER.query(conn,sql,new BeanHandler<T>(entityClass),params);
}catch (SQLException e){
LOGGER.error("query entity failure",e);
throw new RuntimeException(e);
}finally {
closeConnection();
}
return entity;
}
/**
* 执行查询
*/
public static List<Map<String,Object>> executeQuery(String sql,Object...params){
List<Map<String,Object>> result;
try {
Connection conn=getConnection();
result=QUERY_RUNNER.query(conn,sql,new MapListHandler(),params);
}catch (Exception e){
LOGGER.error("execute query failure",e);
throw new RuntimeException(e);
}
return result;
}
/**
* 执行更新语句(包括update,inert,delete)
*/
public static int executeUpdate(String sql,Object... params){
int rows=0;
try {
Connection conn=getConnection();
rows=QUERY_RUNNER.update(conn,sql,params);
}catch (SQLException e){
LOGGER.error("execute update failure",e);
throw new RuntimeException(e);
}finally {
closeConnection();
}
return rows;
}
/**
* 插入实体
*/
public static <T> boolean insertEntity(Class<T> entityClass,Map<String,Object> fieldMap){
if(CollectionUtil.isEmpty(fieldMap)){
LOGGER.error("can not insert entity:fieldMap is empty");
return false;
}
String sql="INSERT INTO "+getTableName(entityClass);
StringBuilder columns=new StringBuilder("(");
StringBuilder values=new StringBuilder("(");
for (String fieldName : fieldMap.keySet()){
columns.append(fieldName).append(", ");
values.append("?, ");
}
columns.replace(columns.lastIndexOf(", "),columns.length(),") ");
values.replace(values.lastIndexOf(", "),values.length(),") ");
sql +=columns + " VALUES " + values;
Object[] params=fieldMap.values().toArray();
return executeUpdate(sql,params) == 1;
}
/**
* 更新实体
* @param entityClass
* @return boolean
*/
public static <T> boolean updateEntity(Class<T> entityClass,long id,Map<String,Object> fieldMap){
if (CollectionUtil.isEmpty(fieldMap)){
LOGGER.error("can not update entity:fieldMap is empty");
return false;
}
String sql="UPDATE " + getTableName(entityClass) + " SET ";
StringBuilder columns=new StringBuilder();
for (String fieldName : fieldMap.keySet()){
columns.append(fieldName).append("=?, ");
}
sql += columns.substring(0,columns.lastIndexOf(", ")) +"WHERE id =?";
List<Object> paramList =new ArrayList<Object>();
paramList.addAll(fieldMap.values());
paramList.add(id);
Object[] params=paramList.toArray();
return executeUpdate(sql,params) == 1;
}
/**
* 删除实体
* @param entityClass
* @return
*/
public static <T> boolean deleteEntity(Class<T> entityClass,long id){
String sql="DELETE FROM "+getTableName(entityClass)+" WHERE id=?";
return executeUpdate(sql,id) == 1;
}
private static String getTableName(Class<?> entityClass){
return entityClass.getSimpleName();//返回类名
}
}
有了insertEntity,updateEntity,deleteEntity后,我们可以快速完成CustomerService中剩下的几个方法:
package org.jack.com.service;
import org.jack.com.helper.DatabaseHelper;
import org.jack.com.model.Customer;
import org.jack.com.util.PropsUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.*;
import java.util.*;
/**
* Created by jack on 2015/11/29.
* 提供客户数据服务
*/
public class CustomerService {
private static final Logger LOGGER = LoggerFactory.getLogger(CustomerService.class);
/*
* 获取客户列表
* */
public List<Customer> getCustomerList(){
//to do something
String sql="select * from customer";
return DatabaseHelper.queryEntityList(Customer.class,sql);
}
/*
* 获取客户
* */
public Customer getCustomer(long id){
//to do something
String sql = "SELECT * FROM customer WHERE id = ?";
return DatabaseHelper.queryEntity(Customer.class, sql, id);
}
/*
* 创建客户
* */
public boolean createCustomer(Map<String,Object> fieldMap){
//to do something
return DatabaseHelper.insertEntity(Customer.class,fieldMap);
}
/*
* 更新客户
* */
public boolean updateCustomer(long id,Map<String,Object> fieldMap){
//to do something
return DatabaseHelper.updateEntity(Customer.class,id,fieldMap);
}
/*
* 删除客户
* */
public boolean deleteCustomer(long id){
//to do something
return DatabaseHelper.deleteEntity(Customer.class,id);
}
}
现在CustomerService的实现相当精简了,只需要一两行代码即可完成表单的“增删改查”操作。像上面这样,每次需要数据库连接时,就调用getConnection方法,在数据库操作完毕以后,还需要调用closeConnection方法关闭数据库连接。虽然关闭数据库这件事情已经被DatabaseHelper类给封装了,当考虑到如果频繁调用getConnection方式就会频繁创建数据库连接,这样做一定会造成大量的系统开销,毕竟数据库的连接数是有限的。因此,需要考虑一种解决方案,将这些数据库连接进行“池话”,也就是说我们需要弄一个“数据库连接池”出来。Apache
DBCP是最好的数据库连接池之一,下面我们就使用这个数据库连接池。
首先,修改pom.xml文件,添加如下的Maven依赖:
<!--Apache DBCP数据库连接池依赖-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId>
<version>2.0.1</version>
</dependency> 然后修改DatabaseHelper的相关代码
我们使用Apache DBCP的org.apache.commons.dbcp2.BasicDataSource来获取数据库连接,只需要保证该对象是静态的就行了,通过设置driver,url,username,password来初始化BasicDataSource,并调用其getConnection方法即可获取数据库连接。
最后,删除以前的closeConnection方法,同时去掉所有closeConnection的finally代码块。
我们可以运行CustomerServiceTest单元测试类的相关测试方法来验证以上代码的正确性。现在的单元测试存在一个非常严重的问题,比如执行完deleteCustomerTest方法,就不能继续执行getCustomerTest方法了,原因很简单,在测试之前数据库的数据无法自动还原为初始状态。
JUnit在调用每个@Test方法前,都会调用@Before方法,也就是我们在单元测试类里定义的init,目前在该方法中预留一个to do something,需要在这里初始化数据库,为我们准备一个便于测试的数据库环境。
此外,为了测试数据库与开发数据库分离,也就是,应该是两个数据库,只是表结构相同而已。我们需要为单元测试单独创建一个数据库,不妨命名为demo_test,需要将demo数据库的customer表复制到demo_test数据库中。
在Navicat premium中创建demo_test数据库,具体的创建过程前面的笔记有讲解过。然后我们在进行如下具体操作:
1)进入demo数据库,选中customer表,使用ctr+c复制表。
2)进入demo_test数据库,使用ctrl+v键粘贴表,即可复制数据表。
3)在demo_test数据库中,选中customer表,单击右键,单击Truncate Table(或者清空表)菜单项,即可清空表中所有的数据(id将从1开始自增)
随后,我们需要准备一个customer_init.sql文件,用于存放所有的insert语句,该文件位于test/resources/sql目录下,具体内容如下:
<pre style="background-color:#2b2b2b;color:#a9b7c6;font-family:'Consolas';font-size:16pt;">TRUNCATE customer<span style="color:#cc7832;">;
</span><span style="color:#cc7832;"><strong>INSERT INTO </strong></span>customer (<span style="color:#cc7832;"><strong>name </strong></span><span style="color:#cc7832;">,</span>contact<span style="color:#cc7832;">,</span>telephone<span style="color:#cc7832;">,</span>email<span style="color:#cc7832;">,</span>remark) <span style="color:#cc7832;"><strong>VALUES </strong></span>(<span style="color:#a5c261;"><strong>'customer1'</strong></span><span style="color:#cc7832;">,</span><span style="color:#a5c261;"><strong>'jack'</strong></span><span style="color:#cc7832;">,</span><span style="color:#a5c261;"><strong>'13512345678'</strong></span><span style="color:#cc7832;">,</span><span style="color:#a5c261;"><strong>'jack@gmail.com'</strong></span><span style="color:#cc7832;">,</span><span style="color:#cc7832;"><strong>null</strong></span>)<span style="color:#cc7832;">;
</span><span style="color:#cc7832;"><strong>INSERT INTO </strong></span>customer (<span style="color:#cc7832;"><strong>name </strong></span><span style="color:#cc7832;">,</span>contact<span style="color:#cc7832;">,</span>telephone<span style="color:#cc7832;">,</span>email<span style="color:#cc7832;">,</span>remark) <span style="color:#cc7832;"><strong>VALUES </strong></span>(<span style="color:#a5c261;"><strong>'customer2'</strong></span><span style="color:#cc7832;">,</span><span style="color:#a5c261;"><strong>'rose'</strong></span><span style="color:#cc7832;">,</span><span style="color:#a5c261;"><strong>'13623456789'</strong></span><span style="color:#cc7832;">,</span><span style="color:#a5c261;"><strong>'rose@gmail.com'</strong></span><span style="color:#cc7832;">,</span><span style="color:#cc7832;"><strong>null</strong></span>)<span style="color:#cc7832;">;</span>
注意,上面的插入语句要全部在同一行,我们需要先执行TRUNCATE语句,清空左右数据,然后再执行INSERT语句,插入相关数据。这里并不需要提供id列,因为该列是自增的。
注意:默认情况下IDEA不会再test目录下自动创建resources目录,也就是我们需要手动创建,可使用Alt+Insert快捷键来完成。创建resources目录完毕后,需要右键单击该目录,单击Mark Directory As/Resources Root,将该目录设置为Maven的测试资源目录。需要注意的是,main/java, main/resources,
test/java, test/resources这四个目录都是classpath的根目录,当运行单元测试时,遵循“就近原则”,即优先从test/java,test/resources加载类或读取文件。
现在test目录结构如下图:
既然已经为单元测试准备了独立的测试数据库,那么就有必要在test/resources目录下提供一个config.properties文件,用于提供测试数据库的相关配置:
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/demo_test
jdbc.username=root
jdbc.password=root 这里使用的是demo_test数据库,而不是demo数据库。
下面我们利用DatabaseHelper的executeUpdate方法依次执行customer_init.sql中提供的SQL语句。
下面我们来完成CustomerServiceTest的init方法:
@Before
public void init() throws Exception{
//to do 初始化数据库
String file="sql/customer_init.sql";
InputStream is=Thread.currentThread().getContextClassLoader().getResourceAsStream(file);
BufferedReader reader=new BufferedReader(new InputStreamReader(is));
String sql;
while ((sql=reader.readLine())!=null){
DatabaseHelper.executeUpdate(sql);
}
}
从当前线程中获取线程上下文中的ClassLoader,通过classpath下的sql/customer_init.sql获取一个InputStream对象,通过该输入流来创建BufferedReader对象,循环读取其中的每一行,并调用DatabaseHelper的executeUpdate方法来执行每条SQL语句。
如果没有错误,测试CustomerServiceTest出现绿条结果如下:
但是没必要再所有单元测试类的init方法中都使用类似上面的代码init方法里面的代码,因此有必要将这些代码做一个封装。不妨在DatabaseHelper类中提供一个executeSqlFile方法,只需要提供filePath即可,如下:
/**
* 执行sql文件
*/
public static void executeSqlFile(String filePath){
InputStream is=Thread.currentThread().getContextClassLoader().getResourceAsStream(filePath);
BufferedReader reader=new BufferedReader(new InputStreamReader(is));
try {
String sql;
while((sql=reader.readLine())!=null){
executeUpdate(sql);
}
}catch (Exception e){
LOGGER.error("execute sql file failure",e);
throw new RuntimeException(e);
}
}
现在CustomerServiceTest的init方法如下:
@Before
public void init() throws Exception{
//to do 初始化数据库
DatabaseHelper.executeSqlFile("sql/customer_init.sql");
}
再次运行测试所有的方法,观察单元测试是否全部通过。
小技巧:在IDEA中,将光标定位在测试方法外部,单击工具栏上的Run(Shift+f10)或Debug(Shift+f9)按钮,可执行所有的测试方法,如果只是将光标定位在某个测试方法内部,那么只能执行当前光标所在的方法。
到这里,服务层开发完毕了。这些服务会在控制器中得到应用,在下一步我们就来完善控制层。
总结:
在进行下一步控制器层完善之前,我们先来进行总结下。
我使用LOGGER来进行写日志
使用QueryRunner对象来简化sql语句的查询,把表和类对应起来
使用ThreadLocal本地线程对象,使每个线程只有一个Connection,保证线程安全
使用BasicDataSource对象来管理线程池,简化数据库的连接操作。
DatabaseHelper的完整代码如下:
package org.jack.com.helper;
import org.apache.commons.dbcp2.BasicDataSource;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import org.apache.commons.dbutils.handlers.MapListHandler;
import org.jack.com.util.CollectionUtil;
import org.jack.com.util.PropsUtil;
import org.slf4j.LoggerFactory;
import org.slf4j.Logger;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
/**
* Created by jack on 2015/12/26.
* 数据库操作助手类
*/
public final class DatabaseHelper {
private static final Logger LOGGER = LoggerFactory.getLogger(DatabaseHelper.class);
//查询
private static final QueryRunner QUERY_RUNNER;
//保证一个线程一个Connection,线程安全
private static final ThreadLocal<Connection> CONNECTION_HOLDER ;
//线程池
private static final BasicDataSource DATA_SOURCE;
static {
CONNECTION_HOLDER = new ThreadLocal<Connection>();
QUERY_RUNNER = new QueryRunner();
Properties conf = PropsUtil.loadProps("config.properties");
String driver = conf.getProperty("jdbc.driver");
String url = conf.getProperty("jdbc.url");
String username = conf.getProperty("jdbc.username");
String passwrod = conf.getProperty("jdbc.password");
DATA_SOURCE=new BasicDataSource();
DATA_SOURCE.setDriverClassName(driver);
DATA_SOURCE.setUrl(url);
DATA_SOURCE.setUsername(username);
DATA_SOURCE.setPassword(passwrod);
}
/**
* 获取数据库连接
*/
public static Connection getConnection() {
Connection conn = CONNECTION_HOLDER.get();//1
if (conn == null) {
try {
conn = DATA_SOURCE.getConnection();
} catch (SQLException e) {
LOGGER.error("get connection failure", e);
throw new RuntimeException(e);
} finally {
CONNECTION_HOLDER.set(conn);
}
}
return conn;
}
/**
* 查询实体列表
*/
public static <T> List<T> queryEntityList(Class<T> entityClass, String sql, Object... params) {
List<T> entityList;
try {
Connection conn = getConnection();
entityList = QUERY_RUNNER.query(conn, sql, new BeanListHandler<T>(entityClass), params);
} catch (SQLException e) {
LOGGER.error("query entity list failure", e);
throw new RuntimeException(e);
}
return entityList;
}
/**
* 查询实体
*/
public static <T> T queryEntity(Class<T> entityClass,String sql,Object... params){
T entity;
try {
Connection conn=getConnection();
entity=QUERY_RUNNER.query(conn,sql,new BeanHandler<T>(entityClass),params);
}catch (SQLException e){
LOGGER.error("query entity failure",e);
throw new RuntimeException(e);
}
return entity;
}
/**
* 执行查询
*/
public static List<Map<String,Object>> executeQuery(String sql,Object...params){
List<Map<String,Object>> result;
try {
Connection conn=getConnection();
result=QUERY_RUNNER.query(conn,sql,new MapListHandler(),params);
}catch (Exception e){
LOGGER.error("execute query failure",e);
throw new RuntimeException(e);
}
return result;
}
/**
* 执行更新语句(包括update,inert,delete)
*/
public static int executeUpdate(String sql,Object... params){
int rows=0;
try {
Connection conn=getConnection();
rows=QUERY_RUNNER.update(conn,sql,params);
}catch (SQLException e){
LOGGER.error("execute update failure",e);
throw new RuntimeException(e);
}
return rows;
}
/**
* 插入实体
*/
public static <T> boolean insertEntity(Class<T> entityClass,Map<String,Object> fieldMap){
if(CollectionUtil.isEmpty(fieldMap)){
LOGGER.error("can not insert entity:fieldMap is empty");
return false;
}
String sql="INSERT INTO "+getTableName(entityClass);
StringBuilder columns=new StringBuilder("(");
StringBuilder values=new StringBuilder("(");
for (String fieldName : fieldMap.keySet()){
columns.append(fieldName).append(", ");
values.append("?, ");
}
columns.replace(columns.lastIndexOf(", "),columns.length(),") ");
values.replace(values.lastIndexOf(", "),values.length(),") ");
sql +=columns + " VALUES " + values;
Object[] params=fieldMap.values().toArray();
return executeUpdate(sql,params) == 1;
}
/**
* 更新实体
* @param entityClass
* @return boolean
*/
public static <T> boolean updateEntity(Class<T> entityClass,long id,Map<String,Object> fieldMap){
if (CollectionUtil.isEmpty(fieldMap)){
LOGGER.error("can not update entity:fieldMap is empty");
return false;
}
String sql="UPDATE " + getTableName(entityClass) + " SET ";
StringBuilder columns=new StringBuilder();
for (String fieldName : fieldMap.keySet()){
columns.append(fieldName).append("=?, ");
}
sql += columns.substring(0,columns.lastIndexOf(", ")) +"WHERE id =?";
List<Object> paramList =new ArrayList<Object>();
paramList.addAll(fieldMap.values());
paramList.add(id);
Object[] params=paramList.toArray();
return executeUpdate(sql,params) == 1;
}
/**
* 删除实体
* @param entityClass
* @return
*/
public static <T> boolean deleteEntity(Class<T> entityClass,long id){
String sql="DELETE FROM "+getTableName(entityClass)+" WHERE id=?";
return executeUpdate(sql,id) == 1;
}
private static String getTableName(Class<?> entityClass){
return entityClass.getSimpleName();//返回类名
}
/**
* 执行sql文件
*/
public static void executeSqlFile(String filePath){
InputStream is=Thread.currentThread().getContextClassLoader().getResourceAsStream(filePath);
BufferedReader reader=new BufferedReader(new InputStreamReader(is));
try {
String sql;
while((sql=reader.readLine())!=null){
executeUpdate(sql);
}
}catch (Exception e){
LOGGER.error("execute sql file failure",e);
throw new RuntimeException(e);
}
}
}
CustomerService的代码如下:
package org.jack.com.service;
import org.jack.com.helper.DatabaseHelper;
import org.jack.com.model.Customer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
/**
* Created by jack on 2015/11/29.
* 提供客户数据服务
*/
public class CustomerService {
/*
* 获取客户列表
* */
public List<Customer> getCustomerList(){
//to do something
String sql="select * from customer";
return DatabaseHelper.queryEntityList(Customer.class,sql);
}
/*
* 获取客户
* */
public Customer getCustomer(long id){
//to do something
String sql = "SELECT * FROM customer WHERE id = ?";
return DatabaseHelper.queryEntity(Customer.class, sql, id);
}
/*
* 创建客户
* */
public boolean createCustomer(Map<String,Object> fieldMap){
//to do something
return DatabaseHelper.insertEntity(Customer.class,fieldMap);
}
/*
* 更新客户
* */
public boolean updateCustomer(long id,Map<String,Object> fieldMap){
//to do something
return DatabaseHelper.updateEntity(Customer.class,id,fieldMap);
}
/*
* 删除客户
* */
public boolean deleteCustomer(long id){
//to do something
return DatabaseHelper.deleteEntity(Customer.class,id);
}
}
CustomerServiceTest的完整代码如下:
package org.jack.test;
import org.jack.com.helper.DatabaseHelper;
import org.jack.com.model.Customer;
import org.jack.com.service.CustomerService;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Created by jack on 2015/12/5.
* 单元测试类CustomerServiceTest
*/
public class CustomerServiceTest {
private final CustomerService customerService;
public CustomerServiceTest(){
customerService = new CustomerService();
}
@Before
public void init() throws Exception{
//to do 初始化数据库
DatabaseHelper.executeSqlFile("sql/customer_init.sql");
}
@Test
public void getCustomerListTest() throws Exception{
List<Customer> customerList = customerService.getCustomerList();
Assert.assertEquals(2,customerList.size());
}
@Test
public void getCustomerTest() throws Exception{
long id = 1;
Customer customer = customerService.getCustomer(id);
Assert.assertNotNull(customer);
}
@Test
public void createCustomerTest() throws Exception{
Map<String,Object> fieldMap = new HashMap<String, Object>();
fieldMap.put("name","customer100");
fieldMap.put("contact","John");
fieldMap.put("telephone","13512345678");
boolean result = customerService.createCustomer(fieldMap);
Assert.assertTrue(result);
}
@Test
public void updateCustomerTest() throws Exception{
long id = 1;
Map<String,Object> fieldMap = new HashMap<String, Object>();
fieldMap.put("contact","Eric");
boolean result = customerService.updateCustomer(id,fieldMap);
Assert.assertTrue(result);
}
@Test
public void deleteCustomerTest() throws Exception{
long id = 1;
boolean result = customerService.deleteCustomer(id);
Assert.assertTrue(result);
}
/*public static void main(String args[]){
CustomerServiceTest ct=new CustomerServiceTest();
try {
ct.getCustomerListTest();
}catch (Exception e){
e.printStackTrace();
}
}*/
}
Maven管理的pom.xml目前的代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion>
<groupId>org.jack.com</groupId>
<artifactId>chapter2</artifactId>
<version>1.0.0</version>
<packaging>war</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!--Servlet-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<!--JUnit-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<!--<version>3.8.1</version>-->
<version>4.11</version>
<scope>test</scope>
</dependency>
<!--SLF4J--> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.7</version> </dependency>
<!--Mysql--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.33</version> <scope>runtime</scope> </dependency>
<!--Apache Commons Lang--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.3.2</version> </dependency> <!--Apache Commons Collections--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-collections4</artifactId> <version>4.0</version> </dependency>
<!--Apache Commons DbUtils--> <dependency> <groupId>commons-dbutils</groupId> <artifactId>commons-dbutils</artifactId> <version>1.6</version> </dependency>
<!--Apache DBCP数据库连接池依赖-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId>
<version>2.0.1</version>
</dependency>
</dependencies>
</project>
接下来进行控制器层的完善。
1 完善服务层
添加SLF4J用于提供日志API,使用Log4J作为实现,配置如下:
<!--SLF4J--> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.7</version> </dependency>为了让Log4J起作用,必须在main/resources目录下创建一个名为log4j.properties的文件,内容如下:
log4j.rootLogger=ERROR,console,file log4j.appender.console=org.apache.log4j.ConsoleAppender log4j.appender.console.layout=org.apache.log4j.PatternLayout log4j.appender.console.layout.ConversionPattern=%m%n log4j.appender.file=org.apache.log4j.DailyRollingFileAppender log4j.appender.file.File=${user.home}/logs/book.log log4j.appender.file.DatePattern='_'yyyyMMDD log4j.appender.file.layout=org.apache.log4j.PatternLayout log4j.appender.file.layout.ConversionPattern=%d{HH:mm:ss,SSS} %p %c (%L) - %m%n log4j.logger.org.jack=DEBUG
上面把日志级别设置为ERROR,并且提供了两种日志appender,分别是console与file。需要对这两种日志分别配置,然后指定只有org.jack包下的类才能输出DEBUG级别的日志。
添加MySQL依赖,用于提供JDBC实现,配置如下:
<!--Mysql--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.33</version> <scope>runtime</scope> </dependency>
添加两个Apache Commons 依赖,用于提供常用的工具类,配置如下:
<!--Apache Commons Lang--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.3.2</version> </dependency> <!--Apache Commons Collections--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-collections4</artifactId> <version>4.0</version> </dependency>
打开CustomerService,下面开始实现getCustomerList方法。
该方法需要获取List,也就是所有的客户信息(此时并没有考虑分页,后续会增加此特性),我们需要执行一条select语句(Statement),获取相应的结果集(ResultSet),然后将其放入一个List中。不过在查询之前,我们必须获取数据库连接(Connection)
我们可使用JDBC来完成以上的操作,为了让数据库信息可配置,我们在classpath下创建一个config.properties文件,该文件位于src/main/resources目录中(注意:对于Maven目录结构而言,classpath指的就是java与resources这两个根目录。)
config.properties文件的内容如下:
jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/demo jdbc.username=root jdbc.password=root
属性文件准备好了,那么我们还需要一个类来读取该属性文件,改名编写一个PropsUtil工具类来进行读取config.properties里面的内容,下面是PropsUtil工具类的代码:
package org.jack.com.util; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.util.Properties; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Created by jack on 2015/12/26. * 属性文件工具类 */ public final class PropsUtil { private static final Logger LOGGER = LoggerFactory.getLogger(PropsUtil.class); /* * 加载属性文件 * * */ public static Properties loadProps(String fileName) { Properties properties = null; InputStream inputStream = null; try { inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(fileName); if (inputStream == null) { throw new FileNotFoundException(fileName + " file is not found!"); } properties = new Properties(); properties.load(inputStream); } catch (IOException e) { LOGGER.error("load properties file failure", e); } finally { if (inputStream != null) { try { inputStream.close(); } catch (IOException e) { LOGGER.error("close input stream failure", e); } } } return properties; } /* * 获取字符型属性(默认为空字符串) * * */ public static String getString(Properties props, String key) { return getString(props, key, ""); } /* * 获取字符型属性(可指定默认值) * */ public static String getString(Properties props, String key, String defaultValue) { String value = defaultValue; if (props.containsKey(key)) { value = props.getProperty(key); } return value; } /* * 获取数值类型属性(默认为0) * */ public static int getInt(Properties props, String key) { return getInt(props, key, 0); } /* * 获取数值类型属性(可指定默认值) * */ public static int getInt(Properties props, String key, int defaultValue) { int value = defaultValue; if (props.containsKey(key)) { value = CastUtil.castInt(props.getProperty(key)); } return value; } /* * 获取布尔型属性(默认值为false) * */ public static boolean getBoolean(Properties props, String key) { return getBoolean(props, key, false); } /* * 获取布尔型属性(可指定默认值) * */ public static boolean getBoolean(Properties props, String key, Boolean defaultValue) { boolean value = defaultValue; if (props.containsKey(key)) { value = CastUtil.castBoolean(props.getProperty(key)); } return value; } }
上面的类中,最关键的是loadProps方法,我们只需传递属性文件的名称,即可返回一个Properties对象,然后再根据getString,getInt,getBoolean这些方法由key获取指定类型的value,同时也可指定defaultValue作为默认值。
在PropsUtil类中,我们用到了CastUtil类,该类是为处理一些数据转型操作而作准备的,代码如下:
<pre name="code" class="java">package org.jack.com.util; import org.apache.commons.lang3.StringUtils; /** * Created by jack on 2015/12/26. * 转型操作工具类 */ public final class CastUtil { /* * 转为String型 * */ public static String castString(Object obj) { return CastUtil.castString(obj, ""); } /* * 转为String型(提供默认值) * */ public static String castString(Object obj, String defaultValue) { return obj != null ? String.valueOf(obj) : defaultValue; } /* * 转为double型 * */ public static double castDouble(Object obj) { return castDouble(obj, 0); } /* * 转为double型(提供默认值) * */ public static double castDouble(Object obj, Double defaultValue) { double doubleValue = defaultValue; if (obj != null) { String strValue = castString(obj); if (StringUtils.isNotEmpty(strValue)) { try { doubleValue = Double.parseDouble(strValue); } catch (NumberFormatException e) { defaultValue = defaultValue; } } } return doubleValue; } /* * 转为long型 * */ public static long castLong(Object obj) { return castLong(obj, 0); } /* * 转为long型(提供默认值) * */ public static long castLong(Object obj, long defaultValue) { long longValue = defaultValue; if (obj != null) { String strValue = castString(obj); if (StringUtils.isNotEmpty(strValue)) { try { longValue = Long.parseLong(strValue); }catch (NumberFormatException e){ longValue=defaultValue; } } } return longValue; } /* * 转为int型 * */ public static int castInt(Object obj){ return castInt(obj,0); } /* * 转为int型(提供默值) * */ public static int castInt(Object obj,int defaultValue){ int intValue=defaultValue; if (obj!=null){ String strValue=castString(obj); if(StringUtils.isNotEmpty(strValue)){ try { intValue=Integer.parseInt(strValue); }catch (NumberFormatException e){ intValue=defaultValue; } } } return intValue; } /* * 转为boolean型 * */ public static boolean castBoolean(Object obj){ return castBoolean(obj,false); } /* * 转为boolean型(提供默认值) * */ public static boolean castBoolean(Object obj,boolean defaultValue){ boolean booleanValue=defaultValue; if(obj!=null){ booleanValue=Boolean.parseBoolean(castString(obj)); } return booleanValue; } }
在CastUtil类中,我们用到了StringUtil类,它用于提供一些字符串操作,代码如下:
package org.jack.com.util; import org.apache.commons.lang3.StringUtils; /** * Created by jack on 2015/12/26. * 字符串工具类 */ public final class StringUtil { /* * 判断字符串是否为空 * */ public static boolean isEmpty(String str){ if(str != null){ str=str.trim(); } return StringUtils.isEmpty(str); } /* * 判断字符串是否非空 * */ public static boolean isNotEmpty(String str){ return !isEmpty(str); } }
上面只是对Apache Commons类库做了一个简单的封装。同理,我们可以做一个CollectionUtil,用于提供一些集合操作,代码如下:
package org.jack.com.util; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.MapUtils; import java.util.Collection; import java.util.Map; /** * Created by jack on 2015/12/26. * 集合工具类 */ public final class CollectionUtil { /** * 判断Collection是否为空 */ public static boolean isEmpty(Collection<?> collection){ return CollectionUtils.isEmpty(collection); } /** *判断Collection是否非空 */ public static boolean isNotEmpty(Collection<?> collection){ return !isEmpty(collection); } /** * 判断Map是否为空 */ public static boolean isEmpty(Map<?,?> map){ return MapUtils.isEmpty(map); } /** * 判断Map是否非空 */ public static boolean isNotEmpty(Map<?,?> map){ return !isEmpty(map); } }
上面我们写了四个工具类,每个工具类的分工各不相同,它们在后面还会经常用到,并且会不断完善这些工具类。
现在回到CustomerService,我们需要在该类中执行数据库操作,也就是需要编写一些JDBC的代码,首先使用PropsUtil读取config.properties配置文件,获取与JDBC相关的配置项。
我们在CustomerService中为这些配置项定义一些常量,并提供一个“静态代码块”,来初始化这些常量,就像下面这样:
private static final String DRIVER; private static final String URL; private static final String USERNAME; private static final String PASSWORD; static { Properties conf= PropsUtil.loadProps("config.properties"); DRIVER=conf.getProperty("jdbc:driver"); URL=conf.getProperty("jdbc.url"); USERNAME= conf.getProperty("jdbc.username"); PASSWORD=conf.getProperty("jdbc.password"); }
以getCustomerList为例,我们可以这样写JDBC代码:
<pre name="code" class="java"><pre name="code" class="java">package org.jack.com.service; import org.jack.com.model.Customer; import org.jack.com.util.PropsUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.sql.*; import java.util.*; /** * Created by jack on 2015/11/29. * 提供客户数据服务 */ public class CustomerService { private static final String DRIVER; private static final String URL; private static final String USERNAME; private static final String PASSWORD; private static final Logger LOGGER = LoggerFactory.getLogger(CustomerService.class); static { Properties conf= PropsUtil.loadProps("config.properties"); DRIVER=conf.getProperty("jdbc.driver"); URL=conf.getProperty("jdbc.url"); USERNAME= conf.getProperty("jdbc.username"); PASSWORD=conf.getProperty("jdbc.password"); try{ Class.forName(DRIVER); }catch (ClassNotFoundException e){ LOGGER.error("can not load jdbc driver",e); } } /* * 获取客户列表 * */ public List<Customer> getCustomerList(String keyword){ //to do something Connection conn=null; try{ List<Customer> customerList=new ArrayList<Customer>(); String sql="select * from customer"; conn= DriverManager.getConnection(URL,USERNAME,PASSWORD); PreparedStatement pstmt=conn.prepareStatement(sql); ResultSet rs=pstmt.executeQuery(); while(rs.next()){ Customer customer=new Customer(); customer.setId(rs.getLong("id")); customer.setName(rs.getString("name")); customer.setContact(rs.getString("contact")); customer.setTelephone(rs.getString("telephone")); customer.setEmail(rs.getString("email")); customer.setRemark(rs.getString("remark")); customerList.add(customer); } return customerList; }catch (SQLException e){ LOGGER.error("execute sql failure",e); }finally { if (conn !=null){ try { conn.close(); }catch (SQLException e){ LOGGER.error("close connection failure",e); } } } return null; } /* * 获取客户 * */ public Customer getCustomer(long id){ //to do something return null; } /* * 创建客户 * */ public boolean createCustomer(Map<String,Object> fieldMap){ //to do something return false; } /* * 更新客户 * */ public boolean updateCustomer(long id,Map<String,Object> fieldMap){ //to do something return false; } /* * 删除客户 * */ public boolean deleteCustomer(long id){ //to do something return false; } }
选中getCustomerList方法右击,选择run进行单元测试,如果没有错误的话,应该是“绿条”,表示可以测试通过,相反,如果出现“红条”就表示测试失败,我们可以查看控制台以了解具体的错误原因。正确的测试结果如下图:
虽然以上的代码可以运行,基本的功能是可以实现的,但是问题还很多,具体包括以下2个方面:
1)在CustomerService类中读取config.properties文件,这是不合理的,毕竟将来还有很多其他Service类需要做同样的事情,我们最好能将这些公共性的代码提起出来。
2)执行一条select语句需要编写一大堆代码,而且必须使用try........catch.....finally结构,开发效率明显不高。
那么问题来了,我们该如何解决这两个问题?首先我们来解决第一个问题。
创建一个org.jack.com.helper包,在该包中创建一个DatabaseHelper类,代码如下:
package org.jack.com.helper; import org.jack.com.util.PropsUtil; import org.slf4j.LoggerFactory; import org.slf4j.Logger; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.util.Properties; /** * Created by jack on 2015/12/26. * 数据库操作助手类 */ public final class DatabaseHelper { private static final Logger LOGGER= LoggerFactory.getLogger(DatabaseHelper.class); private static final String DRIVER; private static final String URL; private static final String USERNAME; private static final String PASSWORD; static { Properties conf= PropsUtil.loadProps("config.properties"); DRIVER=conf.getProperty("jdbc.driver"); URL=conf.getProperty("jdbc.url"); USERNAME=conf.getProperty("jdbc.username"); PASSWORD=conf.getProperty("jdbc.password"); try{ Class.forName(DRIVER); }catch (ClassNotFoundException e){ LOGGER.error("can not load jdbc driver",e); } } /** * 获取数据库连接 */ public static Connection getConnection(){ Connection conn=null; try { conn= DriverManager.getConnection(URL,USERNAME,PASSWORD); }catch (SQLException e){ LOGGER.error("get connection failure",e); } return conn; } /** * 关闭数据库连接 */ public static void closeConnection(Connection conn){ if(conn != null){ try { conn.close(); }catch (SQLException e){ LOGGER.error("close connection failure",e); } } } }在DatabaseHelper中包含一些静态方法,用它们来封装数据库的相关操作,目前提供了“获取数据库连接”与“关闭数据库连接”两个助手方法。
现在CustomerService可以稍微的简化下了:
package org.jack.com.service; import org.jack.com.helper.DatabaseHelper; import org.jack.com.model.Customer; import org.jack.com.util.PropsUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.sql.*; import java.util.*; /** * Created by jack on 2015/11/29. * 提供客户数据服务 */ public class CustomerService { /*private static final String DRIVER; private static final String URL; private static final String USERNAME; private static final String PASSWORD;*/ private static final Logger LOGGER = LoggerFactory.getLogger(CustomerService.class); /*static { Properties conf= PropsUtil.loadProps("config.properties"); DRIVER=conf.getProperty("jdbc.driver"); URL=conf.getProperty("jdbc.url"); USERNAME= conf.getProperty("jdbc.username"); PASSWORD=conf.getProperty("jdbc.password"); try{ Class.forName(DRIVER); }catch (ClassNotFoundException e){ LOGGER.error("can not load jdbc driver",e); } }*/ /* * 获取客户列表 * */ public List<Customer> getCustomerList(String keyword){ //to do something Connection conn=null; try{ List<Customer> customerList=new ArrayList<Customer>(); String sql="select * from customer"; conn= DatabaseHelper.getConnection();//1 PreparedStatement pstmt=conn.prepareStatement(sql); ResultSet rs=pstmt.executeQuery(); while(rs.next()){ Customer customer=new Customer(); customer.setId(rs.getLong("id")); customer.setName(rs.getString("name")); customer.setContact(rs.getString("contact")); customer.setTelephone(rs.getString("telephone")); customer.setEmail(rs.getString("email")); customer.setRemark(rs.getString("remark")); customerList.add(customer); } return customerList; }catch (SQLException e){ LOGGER.error("execute sql failure",e); }finally { /*if (conn !=null){ try { conn.close(); }catch (SQLException e){ LOGGER.error("close connection failure",e); } }*/ DatabaseHelper.closeConnection(conn);//2 } return null; } /* * 获取客户 * */ public Customer getCustomer(long id){ //to do something return null; } /* * 创建客户 * */ public boolean createCustomer(Map<String,Object> fieldMap){ //to do something return false; } /* * 更新客户 * */ public boolean updateCustomer(long id,Map<String,Object> fieldMap){ //to do something return false; } /* * 删除客户 * */ public boolean deleteCustomer(long id){ //to do something return false; } }
注意以上代码1,2两处的修改,的确简化了一些,至少每个Service类不会出现那些静态变量与代码块了,公共的代码放到了DatabaseHelper中从而得到了重用。
下面我们来解决第二个问题,实际上就是如何使代码变得更加精简。
著名的Apache Common项目中有一款名为DbUtils的类库,为我们提供了一个JDBC的封装,下面我们就用这款工具来解决该问题。
首先,需要在pom.xml中添加一个DbUtils的依赖:
<!--Apache Commons DbUtils--> <dependency> <groupId>commons-dbutils</groupId> <artifactId>commons-dbutils</artifactId> <version>1.6</version> </dependency>
然后,在DatabaseHelper中添加如下代码:
package org.jack.com.helper; import org.apache.commons.dbutils.QueryRunner; import org.apache.commons.dbutils.handlers.BeanListHandler; import org.jack.com.util.PropsUtil; import org.slf4j.LoggerFactory; import org.slf4j.Logger; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.util.List; import java.util.Properties; /** * Created by jack on 2015/12/26. * 数据库操作助手类 */ public final class DatabaseHelper { private static final Logger LOGGER= LoggerFactory.getLogger(DatabaseHelper.class); private static final String DRIVER; private static final String URL; private static final String USERNAME; private static final String PASSWORD; static { Properties conf= PropsUtil.loadProps("config.properties"); DRIVER=conf.getProperty("jdbc.driver"); URL=conf.getProperty("jdbc.url"); USERNAME=conf.getProperty("jdbc.username"); PASSWORD=conf.getProperty("jdbc.password"); try{ Class.forName(DRIVER); }catch (ClassNotFoundException e){ LOGGER.error("can not load jdbc driver",e); } } private static final QueryRunner QUERY_RUNNER=new QueryRunner(); /** * 获取数据库连接 */ public static Connection getConnection(){ Connection conn=null; try { conn= DriverManager.getConnection(URL,USERNAME,PASSWORD); }catch (SQLException e){ LOGGER.error("get connection failure",e); } return conn; } /** * 关闭数据库连接 */ public static void closeConnection(Connection conn){ if(conn != null){ try { conn.close(); }catch (SQLException e){ LOGGER.error("close connection failure",e); } } } /** * 查询实体列表 */ public static <T> List<T> queryEntityList(Class<T> entityClass,String sql,Object... params){ List<T> entityList; Connection conn=getConnection(); try{ entityList=QUERY_RUNNER.query(conn,sql,new BeanListHandler<T>(entityClass),params); }catch (SQLException e){ LOGGER.error("query entity list failure",e); throw new RuntimeException(e); }finally { closeConnection(conn); } return entityList; } }注意,上面的queryEntityList方法还有问题,在下面进行改进。使用DbUtils提供的QueryRunner对象可以面向实体(Entity)进行查询。
实际上,DbUtils首先执行SQL语句并返回一个ResultSet,随后通过反射去创建并初始化实体对象。由于我们需要返的是List,因此可以使用BeanListHandler。
最后,改写CustomerService的getCustomerList方法:
/* * 获取客户列表 * */ public List<Customer> getCustomerList(){ //to do something Connection conn=DatabaseHelper.getConnection();; try{ String sql="select * from customer"; return DatabaseHelper.queryEntityList(Customer.class,conn,sql); }catch (SQLException e){ LOGGER.error("execute sql failure",e); }finally { DatabaseHelper.closeConnection(conn);//2 } return null; }
上面的代码,有问题,下面会进行改进。
现在的代码简单了很多,我们不再面对PrepareStatement与ResultSet了,只需要使用DatabaseHelper就能执行数据库操作。我们每次都需要创建一个Connection,然后进行数据库操作,最后关闭Connection。
如何能让Connection对开发人员透明?也就是说,如何隐藏掉创建与关闭Connection的代码呢?
为了确保一个线程只有一个Connection,我们可以使用ThreadLocal来存放本地线程变量。也就是说,将当前线程中的Connection放入ThreadLocal中存起来,这些Connection一定不会出现不安全问题,可以将ThreadLocal理解为一个隔离线程的容器。
现在对DatabaseHelper进行调整,具体代码如下:
package org.jack.com.helper; import org.apache.commons.dbutils.QueryRunner; import org.apache.commons.dbutils.handlers.BeanListHandler; import org.jack.com.util.PropsUtil; import org.slf4j.LoggerFactory; import org.slf4j.Logger; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.util.List; import java.util.Properties; /** * Created by jack on 2015/12/26. * 数据库操作助手类 */ public final class DatabaseHelper { private static final Logger LOGGER = LoggerFactory.getLogger(DatabaseHelper.class); private static final String DRIVER; private static final String URL; private static final String USERNAME; private static final String PASSWORD; static { Properties conf = PropsUtil.loadProps("config.properties"); DRIVER = conf.getProperty("jdbc.driver"); URL = conf.getProperty("jdbc.url"); USERNAME = conf.getProperty("jdbc.username"); PASSWORD = conf.getProperty("jdbc.password"); try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { LOGGER.error("can not load jdbc driver", e); } } private static final QueryRunner QUERY_RUNNER = new QueryRunner(); private static final ThreadLocal<Connection> CONNECTION_HOLDER = new ThreadLocal<Connection>(); /** * 获取数据库连接 */ public static Connection getConnection() { Connection conn = CONNECTION_HOLDER.get();//1 if (conn == null) { try { conn = DriverManager.getConnection(URL, USERNAME, PASSWORD); } catch (SQLException e) { LOGGER.error("get connection failure", e); } finally { CONNECTION_HOLDER.set(conn); } } return conn; } /** * 关闭数据库连接 */ public static void closeConnection() { Connection conn=CONNECTION_HOLDER.get();//1 if (conn != null) { try { conn.close(); } catch (SQLException e) { LOGGER.error("close connection failure", e); throw new RuntimeException(e); }finally { CONNECTION_HOLDER.remove();//3 } } } /** * 查询实体列表 */ public static <T> List<T> queryEntityList(Class<T> entityClass, String sql, Object... params) { List<T> entityList; try { Connection conn = getConnection(); entityList = QUERY_RUNNER.query(conn, sql, new BeanListHandler<T>(entityClass), params); } catch (SQLException e) { LOGGER.error("query entity list failure", e); throw new RuntimeException(e); } finally { closeConnection(); } return entityList; } }
每次获取Connection时,首先在ThreadLocal中寻找(见1),若不存在,则创建一个新的Connection,并将其放入ThreadLocal中(见2)。当Connection使用完毕后,需要移除ThreadLocal中持有的Connection(见3)
现在CustomerService的getCustomerList方法看起来更加简单了:
package org.jack.com.service; import org.jack.com.helper.DatabaseHelper; import org.jack.com.model.Customer; import org.jack.com.util.PropsUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.sql.*; import java.util.*; /** * Created by jack on 2015/11/29. * 提供客户数据服务 */ public class CustomerService { private static final Logger LOGGER = LoggerFactory.getLogger(CustomerService.class); /* * 获取客户列表 * */ public List<Customer> getCustomerList(){ //to do something String sql="select * from customer"; return DatabaseHelper.queryEntityList(Customer.class,sql); } /* * 获取客户 * */ public Customer getCustomer(long id){ //to do something return null; } /* * 创建客户 * */ public boolean createCustomer(Map<String,Object> fieldMap){ //to do something return false; } /* * 更新客户 * */ public boolean updateCustomer(long id,Map<String,Object> fieldMap){ //to do something return false; } /* * 删除客户 * */ public boolean deleteCustomer(long id){ //to do something return false; } }
使用同样的技巧,我们可以在DatabaseHelper中添加一个queryEntity方法,用户查询单个实体对象。
/** * 查询实体 */ public static <T> T queryEntity(Class<T> entityClass,String sql,Object... params){ T entity; try { Connection conn=getConnection(); entity=QUERY_RUNNER.query(conn,sql,new BeanHandler<T>(entityClass),params); }catch (SQLException e){ LOGGER.error("query entity failure",e); throw new RuntimeException(e); }finally { closeConnection(); } return entity; }
需要注意的是,此时我们使用的BeanHandler,而不是BeanListHandler。实际上,DbUtils为我们提供了很多类似的Handler,包括:
BeanHandler-------------------------------返回Bean
BeanListHandler---------------------------返回List对象
BeanMapHandler--------------------------返回Map对象
ArrayHandler-----------------------------返回Object[]对象
ArrayListHandler---------------------------返回List对象
MapHandler--------------------------------返回Map对象
MapListHandler---------------------------返回List对象
ScalarHandler----------------------------返回某列的值
ColumnListHandler--------------------------返回某列的值列表
KeyedHandler-----------------------------------返回Map对象,需指定列名。
以上这些Handler都实现了ResultSetHandler。
查询并不一定是基于单表的,将来有可能连接多个表进行查询,因此我们有必要提供一个更为强大的查询方法,输入一个SQL与动态参数,输入一个List对象,其中的Map表示列名与列值得映射关系。
我们可以借助MapListHandler轻松实现:
/** * 执行查询 */ public static List<Map<String,Object>> executeQuery(String sql,Object...params){ List<Map<String,Object>> result; try { Connection conn=getConnection(); result=QUERY_RUNNER.query(conn,sql,new MapListHandler(),params) }catch (Exception e){ LOGGER.error("execute query failure",e); throw new RuntimeException(e); } return result; }
除了查询语句以外,还包括几种更新语句,例如update,insert,delete等,我们再提供一个通用的执行更新语句的方法。
/**
* 执行更新语句(包括update,inert,delete)
*/
public static int executeUpdate(String sql,Object... params){
int rows=0;
try {
Connection conn=getConnection();
rows=QUERY_RUNNER.update(conn,sql,params);
}catch (SQLException e){
LOGGER.error("execute update failure",e);
throw new RuntimeException(e);
}finally {
closeConnection();
}
return rows;
}
该方法返回执行后受影响的行数,也就是说,更新了多少条记录。根据这个通用的方法,我们可以分别提供三种具体的数据库更新操作:
package org.jack.com.helper;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import org.apache.commons.dbutils.handlers.MapListHandler;
import org.jack.com.util.CollectionUtil;
import org.jack.com.util.PropsUtil;
import org.slf4j.LoggerFactory;
import org.slf4j.Logger;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
/**
* Created by jack on 2015/12/26.
* 数据库操作助手类
*/
public final class DatabaseHelper {
private static final Logger LOGGER = LoggerFactory.getLogger(DatabaseHelper.class);
private static final String DRIVER;
private static final String URL;
private static final String USERNAME;
private static final String PASSWORD;
static {
Properties conf = PropsUtil.loadProps("config.properties");
DRIVER = conf.getProperty("jdbc.driver");
URL = conf.getProperty("jdbc.url");
USERNAME = conf.getProperty("jdbc.username");
PASSWORD = conf.getProperty("jdbc.password");
try {
Class.forName(DRIVER);
} catch (ClassNotFoundException e) {
LOGGER.error("can not load jdbc driver", e);
}
}
private static final QueryRunner QUERY_RUNNER = new QueryRunner();
private static final ThreadLocal<Connection> CONNECTION_HOLDER = new ThreadLocal<Connection>();
/**
* 获取数据库连接
*/
public static Connection getConnection() {
Connection conn = CONNECTION_HOLDER.get();//1
if (conn == null) {
try {
conn = DriverManager.getConnection(URL, USERNAME, PASSWORD);
} catch (SQLException e) {
LOGGER.error("get connection failure", e);
} finally {
CONNECTION_HOLDER.set(conn);
}
}
return conn;
}
/**
* 关闭数据库连接
*/
public static void closeConnection() {
Connection conn=CONNECTION_HOLDER.get();//1
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
LOGGER.error("close connection failure", e);
throw new RuntimeException(e);
}finally {
CONNECTION_HOLDER.remove();
}
}
}
/**
* 查询实体列表
*/
public static <T> List<T> queryEntityList(Class<T> entityClass, String sql, Object... params) {
List<T> entityList;
try {
Connection conn = getConnection();
entityList = QUERY_RUNNER.query(conn, sql, new BeanListHandler<T>(entityClass), params);
} catch (SQLException e) {
LOGGER.error("query entity list failure", e);
throw new RuntimeException(e);
} finally {
closeConnection();
}
return entityList;
}
/**
* 查询实体
*/
public static <T> T queryEntity(Class<T> entityClass,String sql,Object... params){
T entity;
try {
Connection conn=getConnection();
entity=QUERY_RUNNER.query(conn,sql,new BeanHandler<T>(entityClass),params);
}catch (SQLException e){
LOGGER.error("query entity failure",e);
throw new RuntimeException(e);
}finally {
closeConnection();
}
return entity;
}
/**
* 执行查询
*/
public static List<Map<String,Object>> executeQuery(String sql,Object...params){
List<Map<String,Object>> result;
try {
Connection conn=getConnection();
result=QUERY_RUNNER.query(conn,sql,new MapListHandler(),params);
}catch (Exception e){
LOGGER.error("execute query failure",e);
throw new RuntimeException(e);
}
return result;
}
/**
* 执行更新语句(包括update,inert,delete)
*/
public static int executeUpdate(String sql,Object... params){
int rows=0;
try {
Connection conn=getConnection();
rows=QUERY_RUNNER.update(conn,sql,params);
}catch (SQLException e){
LOGGER.error("execute update failure",e);
throw new RuntimeException(e);
}finally {
closeConnection();
}
return rows;
}
/**
* 插入实体
*/
public static <T> boolean insertEntity(Class<T> entityClass,Map<String,Object> fieldMap){
if(CollectionUtil.isEmpty(fieldMap)){
LOGGER.error("can not insert entity:fieldMap is empty");
return false;
}
String sql="INSERT INTO "+getTableName(entityClass);
StringBuilder columns=new StringBuilder("(");
StringBuilder values=new StringBuilder("(");
for (String fieldName : fieldMap.keySet()){
columns.append(fieldName).append(", ");
values.append("?, ");
}
columns.replace(columns.lastIndexOf(", "),columns.length(),") ");
values.replace(values.lastIndexOf(", "),values.length(),") ");
sql +=columns + " VALUES " + values;
Object[] params=fieldMap.values().toArray();
return executeUpdate(sql,params) == 1;
}
/**
* 更新实体
* @param entityClass
* @return boolean
*/
public static <T> boolean updateEntity(Class<T> entityClass,long id,Map<String,Object> fieldMap){
if (CollectionUtil.isEmpty(fieldMap)){
LOGGER.error("can not update entity:fieldMap is empty");
return false;
}
String sql="UPDATE " + getTableName(entityClass) + " SET ";
StringBuilder columns=new StringBuilder();
for (String fieldName : fieldMap.keySet()){
columns.append(fieldName).append("=?, ");
}
sql += columns.substring(0,columns.lastIndexOf(", ")) +"WHERE id =?";
List<Object> paramList =new ArrayList<Object>();
paramList.addAll(fieldMap.values());
paramList.add(id);
Object[] params=paramList.toArray();
return executeUpdate(sql,params) == 1;
}
/**
* 删除实体
* @param entityClass
* @return
*/
public static <T> boolean deleteEntity(Class<T> entityClass,long id){
String sql="DELETE FROM "+getTableName(entityClass)+" WHERE id=?";
return executeUpdate(sql,id) == 1;
}
private static String getTableName(Class<?> entityClass){
return entityClass.getSimpleName();//返回类名
}
}
有了insertEntity,updateEntity,deleteEntity后,我们可以快速完成CustomerService中剩下的几个方法:
package org.jack.com.service;
import org.jack.com.helper.DatabaseHelper;
import org.jack.com.model.Customer;
import org.jack.com.util.PropsUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.*;
import java.util.*;
/**
* Created by jack on 2015/11/29.
* 提供客户数据服务
*/
public class CustomerService {
private static final Logger LOGGER = LoggerFactory.getLogger(CustomerService.class);
/*
* 获取客户列表
* */
public List<Customer> getCustomerList(){
//to do something
String sql="select * from customer";
return DatabaseHelper.queryEntityList(Customer.class,sql);
}
/*
* 获取客户
* */
public Customer getCustomer(long id){
//to do something
String sql = "SELECT * FROM customer WHERE id = ?";
return DatabaseHelper.queryEntity(Customer.class, sql, id);
}
/*
* 创建客户
* */
public boolean createCustomer(Map<String,Object> fieldMap){
//to do something
return DatabaseHelper.insertEntity(Customer.class,fieldMap);
}
/*
* 更新客户
* */
public boolean updateCustomer(long id,Map<String,Object> fieldMap){
//to do something
return DatabaseHelper.updateEntity(Customer.class,id,fieldMap);
}
/*
* 删除客户
* */
public boolean deleteCustomer(long id){
//to do something
return DatabaseHelper.deleteEntity(Customer.class,id);
}
}
现在CustomerService的实现相当精简了,只需要一两行代码即可完成表单的“增删改查”操作。像上面这样,每次需要数据库连接时,就调用getConnection方法,在数据库操作完毕以后,还需要调用closeConnection方法关闭数据库连接。虽然关闭数据库这件事情已经被DatabaseHelper类给封装了,当考虑到如果频繁调用getConnection方式就会频繁创建数据库连接,这样做一定会造成大量的系统开销,毕竟数据库的连接数是有限的。因此,需要考虑一种解决方案,将这些数据库连接进行“池话”,也就是说我们需要弄一个“数据库连接池”出来。Apache
DBCP是最好的数据库连接池之一,下面我们就使用这个数据库连接池。
首先,修改pom.xml文件,添加如下的Maven依赖:
<!--Apache DBCP数据库连接池依赖-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId>
<version>2.0.1</version>
</dependency> 然后修改DatabaseHelper的相关代码
package org.jack.com.helper; import org.apache.commons.dbcp2.BasicDataSource; import org.apache.commons.dbutils.QueryRunner; import org.apache.commons.dbutils.handlers.BeanHandler; import org.apache.commons.dbutils.handlers.BeanListHandler; import org.apache.commons.dbutils.handlers.MapListHandler; import org.jack.com.util.CollectionUtil; import org.jack.com.util.PropsUtil; import org.slf4j.LoggerFactory; import org.slf4j.Logger; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Properties; /** * Created by jack on 2015/12/26. * 数据库操作助手类 */ public final class DatabaseHelper { private static final Logger LOGGER = LoggerFactory.getLogger(DatabaseHelper.class); //查询 private static final QueryRunner QUERY_RUNNER; //保证一个线程一个Connection,线程安全 private static final ThreadLocal<Connection> CONNECTION_HOLDER ; //线程池 private static final BasicDataSource DATA_SOURCE; static { CONNECTION_HOLDER = new ThreadLocal<Connection>(); QUERY_RUNNER = new QueryRunner(); Properties conf = PropsUtil.loadProps("config.properties"); String driver = conf.getProperty("jdbc.driver"); String url = conf.getProperty("jdbc.url"); String username = conf.getProperty("jdbc.username"); String passwrod = conf.getProperty("jdbc.password"); DATA_SOURCE=new BasicDataSource(); DATA_SOURCE.setDriverClassName(driver); DATA_SOURCE.setUrl(url); DATA_SOURCE.setUsername(username); DATA_SOURCE.setPassword(passwrod); } /** * 获取数据库连接 */ public static Connection getConnection() { Connection conn = CONNECTION_HOLDER.get();//1 if (conn == null) { try { conn = DATA_SOURCE.getConnection(); } catch (SQLException e) { LOGGER.error("get connection failure", e); throw new RuntimeException(e); } finally { CONNECTION_HOLDER.set(conn); } } return conn; } /** * 查询实体列表 */ public static <T> List<T> queryEntityList(Class<T> entityClass, String sql, Object... params) { List<T> entityList; try { Connection conn = getConnection(); entityList = QUERY_RUNNER.query(conn, sql, new BeanListHandler<T>(entityClass), params); } catch (SQLException e) { LOGGER.error("query entity list failure", e); throw new RuntimeException(e); } return entityList; } /** * 查询实体 */ public static <T> T queryEntity(Class<T> entityClass,String sql,Object... params){ T entity; try { Connection conn=getConnection(); entity=QUERY_RUNNER.query(conn,sql,new BeanHandler<T>(entityClass),params); }catch (SQLException e){ LOGGER.error("query entity failure",e); throw new RuntimeException(e); } return entity; } /** * 执行查询 */ public static List<Map<String,Object>> executeQuery(String sql,Object...params){ List<Map<String,Object>> result; try { Connection conn=getConnection(); result=QUERY_RUNNER.query(conn,sql,new MapListHandler(),params); }catch (Exception e){ LOGGER.error("execute query failure",e); throw new RuntimeException(e); } return result; } /** * 执行更新语句(包括update,inert,delete) */ public static int executeUpdate(String sql,Object... params){ int rows=0; try { Connection conn=getConnection(); rows=QUERY_RUNNER.update(conn,sql,params); }catch (SQLException e){ LOGGER.error("execute update failure",e); throw new RuntimeException(e); } return rows; } /** * 插入实体 */ public static <T> boolean insertEntity(Class<T> entityClass,Map<String,Object> fieldMap){ if(CollectionUtil.isEmpty(fieldMap)){ LOGGER.error("can not insert entity:fieldMap is empty"); return false; } String sql="INSERT INTO "+getTableName(entityClass); StringBuilder columns=new StringBuilder("("); StringBuilder values=new StringBuilder("("); for (String fieldName : fieldMap.keySet()){ columns.append(fieldName).append(", "); values.append("?, "); } columns.replace(columns.lastIndexOf(", "),columns.length(),") "); values.replace(values.lastIndexOf(", "),values.length(),") "); sql +=columns + " VALUES " + values; Object[] params=fieldMap.values().toArray(); return executeUpdate(sql,params) == 1; } /** * 更新实体 * @param entityClass * @return boolean */ public static <T> boolean updateEntity(Class<T> entityClass,long id,Map<String,Object> fieldMap){ if (CollectionUtil.isEmpty(fieldMap)){ LOGGER.error("can not update entity:fieldMap is empty"); return false; } String sql="UPDATE " + getTableName(entityClass) + " SET "; StringBuilder columns=new StringBuilder(); for (String fieldName : fieldMap.keySet()){ columns.append(fieldName).append("=?, "); } sql += columns.substring(0,columns.lastIndexOf(", ")) +"WHERE id =?"; List<Object> paramList =new ArrayList<Object>(); paramList.addAll(fieldMap.values()); paramList.add(id); Object[] params=paramList.toArray(); return executeUpdate(sql,params) == 1; } /** * 删除实体 * @param entityClass * @return */ public static <T> boolean deleteEntity(Class<T> entityClass,long id){ String sql="DELETE FROM "+getTableName(entityClass)+" WHERE id=?"; return executeUpdate(sql,id) == 1; } private static String getTableName(Class<?> entityClass){ return entityClass.getSimpleName();//返回类名 } }
我们使用Apache DBCP的org.apache.commons.dbcp2.BasicDataSource来获取数据库连接,只需要保证该对象是静态的就行了,通过设置driver,url,username,password来初始化BasicDataSource,并调用其getConnection方法即可获取数据库连接。
最后,删除以前的closeConnection方法,同时去掉所有closeConnection的finally代码块。
我们可以运行CustomerServiceTest单元测试类的相关测试方法来验证以上代码的正确性。现在的单元测试存在一个非常严重的问题,比如执行完deleteCustomerTest方法,就不能继续执行getCustomerTest方法了,原因很简单,在测试之前数据库的数据无法自动还原为初始状态。
JUnit在调用每个@Test方法前,都会调用@Before方法,也就是我们在单元测试类里定义的init,目前在该方法中预留一个to do something,需要在这里初始化数据库,为我们准备一个便于测试的数据库环境。
此外,为了测试数据库与开发数据库分离,也就是,应该是两个数据库,只是表结构相同而已。我们需要为单元测试单独创建一个数据库,不妨命名为demo_test,需要将demo数据库的customer表复制到demo_test数据库中。
在Navicat premium中创建demo_test数据库,具体的创建过程前面的笔记有讲解过。然后我们在进行如下具体操作:
1)进入demo数据库,选中customer表,使用ctr+c复制表。
2)进入demo_test数据库,使用ctrl+v键粘贴表,即可复制数据表。
3)在demo_test数据库中,选中customer表,单击右键,单击Truncate Table(或者清空表)菜单项,即可清空表中所有的数据(id将从1开始自增)
随后,我们需要准备一个customer_init.sql文件,用于存放所有的insert语句,该文件位于test/resources/sql目录下,具体内容如下:
<pre style="background-color:#2b2b2b;color:#a9b7c6;font-family:'Consolas';font-size:16pt;">TRUNCATE customer<span style="color:#cc7832;">;
</span><span style="color:#cc7832;"><strong>INSERT INTO </strong></span>customer (<span style="color:#cc7832;"><strong>name </strong></span><span style="color:#cc7832;">,</span>contact<span style="color:#cc7832;">,</span>telephone<span style="color:#cc7832;">,</span>email<span style="color:#cc7832;">,</span>remark) <span style="color:#cc7832;"><strong>VALUES </strong></span>(<span style="color:#a5c261;"><strong>'customer1'</strong></span><span style="color:#cc7832;">,</span><span style="color:#a5c261;"><strong>'jack'</strong></span><span style="color:#cc7832;">,</span><span style="color:#a5c261;"><strong>'13512345678'</strong></span><span style="color:#cc7832;">,</span><span style="color:#a5c261;"><strong>'jack@gmail.com'</strong></span><span style="color:#cc7832;">,</span><span style="color:#cc7832;"><strong>null</strong></span>)<span style="color:#cc7832;">;
</span><span style="color:#cc7832;"><strong>INSERT INTO </strong></span>customer (<span style="color:#cc7832;"><strong>name </strong></span><span style="color:#cc7832;">,</span>contact<span style="color:#cc7832;">,</span>telephone<span style="color:#cc7832;">,</span>email<span style="color:#cc7832;">,</span>remark) <span style="color:#cc7832;"><strong>VALUES </strong></span>(<span style="color:#a5c261;"><strong>'customer2'</strong></span><span style="color:#cc7832;">,</span><span style="color:#a5c261;"><strong>'rose'</strong></span><span style="color:#cc7832;">,</span><span style="color:#a5c261;"><strong>'13623456789'</strong></span><span style="color:#cc7832;">,</span><span style="color:#a5c261;"><strong>'rose@gmail.com'</strong></span><span style="color:#cc7832;">,</span><span style="color:#cc7832;"><strong>null</strong></span>)<span style="color:#cc7832;">;</span>
注意,上面的插入语句要全部在同一行,我们需要先执行TRUNCATE语句,清空左右数据,然后再执行INSERT语句,插入相关数据。这里并不需要提供id列,因为该列是自增的。
注意:默认情况下IDEA不会再test目录下自动创建resources目录,也就是我们需要手动创建,可使用Alt+Insert快捷键来完成。创建resources目录完毕后,需要右键单击该目录,单击Mark Directory As/Resources Root,将该目录设置为Maven的测试资源目录。需要注意的是,main/java, main/resources,
test/java, test/resources这四个目录都是classpath的根目录,当运行单元测试时,遵循“就近原则”,即优先从test/java,test/resources加载类或读取文件。
现在test目录结构如下图:
既然已经为单元测试准备了独立的测试数据库,那么就有必要在test/resources目录下提供一个config.properties文件,用于提供测试数据库的相关配置:
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/demo_test
jdbc.username=root
jdbc.password=root 这里使用的是demo_test数据库,而不是demo数据库。
下面我们利用DatabaseHelper的executeUpdate方法依次执行customer_init.sql中提供的SQL语句。
下面我们来完成CustomerServiceTest的init方法:
@Before
public void init() throws Exception{
//to do 初始化数据库
String file="sql/customer_init.sql";
InputStream is=Thread.currentThread().getContextClassLoader().getResourceAsStream(file);
BufferedReader reader=new BufferedReader(new InputStreamReader(is));
String sql;
while ((sql=reader.readLine())!=null){
DatabaseHelper.executeUpdate(sql);
}
}
从当前线程中获取线程上下文中的ClassLoader,通过classpath下的sql/customer_init.sql获取一个InputStream对象,通过该输入流来创建BufferedReader对象,循环读取其中的每一行,并调用DatabaseHelper的executeUpdate方法来执行每条SQL语句。
如果没有错误,测试CustomerServiceTest出现绿条结果如下:
但是没必要再所有单元测试类的init方法中都使用类似上面的代码init方法里面的代码,因此有必要将这些代码做一个封装。不妨在DatabaseHelper类中提供一个executeSqlFile方法,只需要提供filePath即可,如下:
/**
* 执行sql文件
*/
public static void executeSqlFile(String filePath){
InputStream is=Thread.currentThread().getContextClassLoader().getResourceAsStream(filePath);
BufferedReader reader=new BufferedReader(new InputStreamReader(is));
try {
String sql;
while((sql=reader.readLine())!=null){
executeUpdate(sql);
}
}catch (Exception e){
LOGGER.error("execute sql file failure",e);
throw new RuntimeException(e);
}
}
现在CustomerServiceTest的init方法如下:
@Before
public void init() throws Exception{
//to do 初始化数据库
DatabaseHelper.executeSqlFile("sql/customer_init.sql");
}
再次运行测试所有的方法,观察单元测试是否全部通过。
小技巧:在IDEA中,将光标定位在测试方法外部,单击工具栏上的Run(Shift+f10)或Debug(Shift+f9)按钮,可执行所有的测试方法,如果只是将光标定位在某个测试方法内部,那么只能执行当前光标所在的方法。
到这里,服务层开发完毕了。这些服务会在控制器中得到应用,在下一步我们就来完善控制层。
总结:
在进行下一步控制器层完善之前,我们先来进行总结下。
private static final Logger LOGGER = LoggerFactory.getLogger(DatabaseHelper.class);
我使用LOGGER来进行写日志
private static final QueryRunner QUERY_RUNNER;
使用QueryRunner对象来简化sql语句的查询,把表和类对应起来
private static final ThreadLocal<Connection> CONNECTION_HOLDER ;
使用ThreadLocal本地线程对象,使每个线程只有一个Connection,保证线程安全
private static final BasicDataSource DATA_SOURCE;
使用BasicDataSource对象来管理线程池,简化数据库的连接操作。
DatabaseHelper的完整代码如下:
package org.jack.com.helper;
import org.apache.commons.dbcp2.BasicDataSource;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import org.apache.commons.dbutils.handlers.MapListHandler;
import org.jack.com.util.CollectionUtil;
import org.jack.com.util.PropsUtil;
import org.slf4j.LoggerFactory;
import org.slf4j.Logger;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
/**
* Created by jack on 2015/12/26.
* 数据库操作助手类
*/
public final class DatabaseHelper {
private static final Logger LOGGER = LoggerFactory.getLogger(DatabaseHelper.class);
//查询
private static final QueryRunner QUERY_RUNNER;
//保证一个线程一个Connection,线程安全
private static final ThreadLocal<Connection> CONNECTION_HOLDER ;
//线程池
private static final BasicDataSource DATA_SOURCE;
static {
CONNECTION_HOLDER = new ThreadLocal<Connection>();
QUERY_RUNNER = new QueryRunner();
Properties conf = PropsUtil.loadProps("config.properties");
String driver = conf.getProperty("jdbc.driver");
String url = conf.getProperty("jdbc.url");
String username = conf.getProperty("jdbc.username");
String passwrod = conf.getProperty("jdbc.password");
DATA_SOURCE=new BasicDataSource();
DATA_SOURCE.setDriverClassName(driver);
DATA_SOURCE.setUrl(url);
DATA_SOURCE.setUsername(username);
DATA_SOURCE.setPassword(passwrod);
}
/**
* 获取数据库连接
*/
public static Connection getConnection() {
Connection conn = CONNECTION_HOLDER.get();//1
if (conn == null) {
try {
conn = DATA_SOURCE.getConnection();
} catch (SQLException e) {
LOGGER.error("get connection failure", e);
throw new RuntimeException(e);
} finally {
CONNECTION_HOLDER.set(conn);
}
}
return conn;
}
/**
* 查询实体列表
*/
public static <T> List<T> queryEntityList(Class<T> entityClass, String sql, Object... params) {
List<T> entityList;
try {
Connection conn = getConnection();
entityList = QUERY_RUNNER.query(conn, sql, new BeanListHandler<T>(entityClass), params);
} catch (SQLException e) {
LOGGER.error("query entity list failure", e);
throw new RuntimeException(e);
}
return entityList;
}
/**
* 查询实体
*/
public static <T> T queryEntity(Class<T> entityClass,String sql,Object... params){
T entity;
try {
Connection conn=getConnection();
entity=QUERY_RUNNER.query(conn,sql,new BeanHandler<T>(entityClass),params);
}catch (SQLException e){
LOGGER.error("query entity failure",e);
throw new RuntimeException(e);
}
return entity;
}
/**
* 执行查询
*/
public static List<Map<String,Object>> executeQuery(String sql,Object...params){
List<Map<String,Object>> result;
try {
Connection conn=getConnection();
result=QUERY_RUNNER.query(conn,sql,new MapListHandler(),params);
}catch (Exception e){
LOGGER.error("execute query failure",e);
throw new RuntimeException(e);
}
return result;
}
/**
* 执行更新语句(包括update,inert,delete)
*/
public static int executeUpdate(String sql,Object... params){
int rows=0;
try {
Connection conn=getConnection();
rows=QUERY_RUNNER.update(conn,sql,params);
}catch (SQLException e){
LOGGER.error("execute update failure",e);
throw new RuntimeException(e);
}
return rows;
}
/**
* 插入实体
*/
public static <T> boolean insertEntity(Class<T> entityClass,Map<String,Object> fieldMap){
if(CollectionUtil.isEmpty(fieldMap)){
LOGGER.error("can not insert entity:fieldMap is empty");
return false;
}
String sql="INSERT INTO "+getTableName(entityClass);
StringBuilder columns=new StringBuilder("(");
StringBuilder values=new StringBuilder("(");
for (String fieldName : fieldMap.keySet()){
columns.append(fieldName).append(", ");
values.append("?, ");
}
columns.replace(columns.lastIndexOf(", "),columns.length(),") ");
values.replace(values.lastIndexOf(", "),values.length(),") ");
sql +=columns + " VALUES " + values;
Object[] params=fieldMap.values().toArray();
return executeUpdate(sql,params) == 1;
}
/**
* 更新实体
* @param entityClass
* @return boolean
*/
public static <T> boolean updateEntity(Class<T> entityClass,long id,Map<String,Object> fieldMap){
if (CollectionUtil.isEmpty(fieldMap)){
LOGGER.error("can not update entity:fieldMap is empty");
return false;
}
String sql="UPDATE " + getTableName(entityClass) + " SET ";
StringBuilder columns=new StringBuilder();
for (String fieldName : fieldMap.keySet()){
columns.append(fieldName).append("=?, ");
}
sql += columns.substring(0,columns.lastIndexOf(", ")) +"WHERE id =?";
List<Object> paramList =new ArrayList<Object>();
paramList.addAll(fieldMap.values());
paramList.add(id);
Object[] params=paramList.toArray();
return executeUpdate(sql,params) == 1;
}
/**
* 删除实体
* @param entityClass
* @return
*/
public static <T> boolean deleteEntity(Class<T> entityClass,long id){
String sql="DELETE FROM "+getTableName(entityClass)+" WHERE id=?";
return executeUpdate(sql,id) == 1;
}
private static String getTableName(Class<?> entityClass){
return entityClass.getSimpleName();//返回类名
}
/**
* 执行sql文件
*/
public static void executeSqlFile(String filePath){
InputStream is=Thread.currentThread().getContextClassLoader().getResourceAsStream(filePath);
BufferedReader reader=new BufferedReader(new InputStreamReader(is));
try {
String sql;
while((sql=reader.readLine())!=null){
executeUpdate(sql);
}
}catch (Exception e){
LOGGER.error("execute sql file failure",e);
throw new RuntimeException(e);
}
}
}
CustomerService
CustomerService的代码如下:
package org.jack.com.service;
import org.jack.com.helper.DatabaseHelper;
import org.jack.com.model.Customer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
/**
* Created by jack on 2015/11/29.
* 提供客户数据服务
*/
public class CustomerService {
/*
* 获取客户列表
* */
public List<Customer> getCustomerList(){
//to do something
String sql="select * from customer";
return DatabaseHelper.queryEntityList(Customer.class,sql);
}
/*
* 获取客户
* */
public Customer getCustomer(long id){
//to do something
String sql = "SELECT * FROM customer WHERE id = ?";
return DatabaseHelper.queryEntity(Customer.class, sql, id);
}
/*
* 创建客户
* */
public boolean createCustomer(Map<String,Object> fieldMap){
//to do something
return DatabaseHelper.insertEntity(Customer.class,fieldMap);
}
/*
* 更新客户
* */
public boolean updateCustomer(long id,Map<String,Object> fieldMap){
//to do something
return DatabaseHelper.updateEntity(Customer.class,id,fieldMap);
}
/*
* 删除客户
* */
public boolean deleteCustomer(long id){
//to do something
return DatabaseHelper.deleteEntity(Customer.class,id);
}
}
CustomerServiceTest的完整代码如下:
package org.jack.test;
import org.jack.com.helper.DatabaseHelper;
import org.jack.com.model.Customer;
import org.jack.com.service.CustomerService;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Created by jack on 2015/12/5.
* 单元测试类CustomerServiceTest
*/
public class CustomerServiceTest {
private final CustomerService customerService;
public CustomerServiceTest(){
customerService = new CustomerService();
}
@Before
public void init() throws Exception{
//to do 初始化数据库
DatabaseHelper.executeSqlFile("sql/customer_init.sql");
}
@Test
public void getCustomerListTest() throws Exception{
List<Customer> customerList = customerService.getCustomerList();
Assert.assertEquals(2,customerList.size());
}
@Test
public void getCustomerTest() throws Exception{
long id = 1;
Customer customer = customerService.getCustomer(id);
Assert.assertNotNull(customer);
}
@Test
public void createCustomerTest() throws Exception{
Map<String,Object> fieldMap = new HashMap<String, Object>();
fieldMap.put("name","customer100");
fieldMap.put("contact","John");
fieldMap.put("telephone","13512345678");
boolean result = customerService.createCustomer(fieldMap);
Assert.assertTrue(result);
}
@Test
public void updateCustomerTest() throws Exception{
long id = 1;
Map<String,Object> fieldMap = new HashMap<String, Object>();
fieldMap.put("contact","Eric");
boolean result = customerService.updateCustomer(id,fieldMap);
Assert.assertTrue(result);
}
@Test
public void deleteCustomerTest() throws Exception{
long id = 1;
boolean result = customerService.deleteCustomer(id);
Assert.assertTrue(result);
}
/*public static void main(String args[]){
CustomerServiceTest ct=new CustomerServiceTest();
try {
ct.getCustomerListTest();
}catch (Exception e){
e.printStackTrace();
}
}*/
}
Maven管理的pom.xml目前的代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion>
<groupId>org.jack.com</groupId>
<artifactId>chapter2</artifactId>
<version>1.0.0</version>
<packaging>war</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!--Servlet-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<!--JUnit-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<!--<version>3.8.1</version>-->
<version>4.11</version>
<scope>test</scope>
</dependency>
<!--SLF4J--> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.7</version> </dependency>
<!--Mysql--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.33</version> <scope>runtime</scope> </dependency>
<!--Apache Commons Lang--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.3.2</version> </dependency> <!--Apache Commons Collections--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-collections4</artifactId> <version>4.0</version> </dependency>
<!--Apache Commons DbUtils--> <dependency> <groupId>commons-dbutils</groupId> <artifactId>commons-dbutils</artifactId> <version>1.6</version> </dependency>
<!--Apache DBCP数据库连接池依赖-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId>
<version>2.0.1</version>
</dependency>
</dependencies>
</project>
接下来进行控制器层的完善。
相关文章推荐
- java对世界各个时区(TimeZone)的通用转换处理方法(转载)
- java-注解annotation
- java-模拟tomcat服务器
- java-用HttpURLConnection发送Http请求.
- java-WEB中的监听器Lisener
- 分享微信开发Html5轻游戏中的几个坑
- Android IPC进程间通讯机制
- Android Native 绘图方法
- Android java 与 javascript互访(相互调用)的方法例子
- 介绍一款信息管理系统的开源框架---jeecg
- 聚类算法之kmeans算法java版本
- java实现 PageRank算法
- PropertyChangeListener简单理解
- c++11 + SDL2 + ffmpeg +OpenAL + java = Android播放器
- 插入排序
- 冒泡排序
- 堆排序
- 快速排序