【3月28日】手撕架构(三)service的优化之路,从三十行到两行

在上文中,已经基本实现了getCustomerList功能并顺利通过了的单元测试。但是也遗留了两个问题:
1.在CustomerService类中读取config.properties文件,这是不合理的。一个项目中不可能只有一个service,如果每个service都需要读取config,一来造成不必要的冗余,也存在安全风险。
2.执行一条select语句真的是好累啊,需要编写一大推代码,还必须使用try……catch……finally结构,开发效率明显不高。
第一个问题实质是数据库初始化代码冗余的问题,第二个问题是数据库查询操作复杂的问题。依次解决。

解决数据库初始化代码冗余的问题

创建一个helper包,在该包中创建一个DataBaseHelper类:

package org.smart4j.chapter2.helper;

import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.ResultSetHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import org.slf4j.LoggerFactory;
import org.smart4j.chapter2.model.Customer;
import org.smart4j.chapter2.util.PropsUtil;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.List;
import java.util.Properties;

/**
 * Created by Tree on 2017/3/27.
 * 将数据库连接的操作分离出来,这样避免在每一个service里面都要写
 * 很长的数据库连接的代码
 */
public class DataBaseHelper {

    private static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(PropsUtil.class);
    private static final QueryRunner QUERY_RUNNER = new QueryRunner();

    /**
     * 使用静态代码块初始化配置项定义的一些常量
     */
    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 drive", 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 =getConnection();
        if(conn!=null){
            try{
                conn.close();
            }catch (SQLException e){
                LOGGER.error("close connection failure", e);
            }
        }
    }

在DataBaseHelper中使用一些静态方法来封装数据库的相关操作,并且提供了”获取数据库连接以及”关闭数据库连接“的两个工具方法。

public List<Customer> getCustomerList(){
        Connection conn = null;
        try{
            List<Customer> customerList = new ArrayList<Customer>();
            String sql = "SELECT * FROM customer";
            conn = DataBaseHelper.getConnection();
            PreparedStatement stmt = conn.prepareStatement(sql);
            ResultSet rs = stmt.executeQuery();
            while (rs.next()){
                Customer customer = new Customer();
                customer.setId(rs.getLong("id"));
                customer.setName(rs.getString("name"));
                customer.setContract(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);
            return null;
        }finally {
            DataBaseHelper.closeConnection(conn);
        }
    }

在service中,开启数据库的操作由:

conn=DriveManager.getConnection(URL,USERNAME,PASSWORD);

可以转变为:

conn = DataBaseHelper.getConnection();

这样在service中就不必进行显示的读取配置文件的信息并进行配置项的操作了(USERNAME,PASSWORD)不会在该类中暴露出来,提高了程序的健壮性。同样,关闭数据库的操作也精简了很多,不必在最后的finally{}块中继续try……catch……的操作了。

解决数据库查询操作复杂的问题

这个我们还是用一下轮子,著名的Apache Common项目中有一款DbUtil的类库,封装了一些JDBC的操作,下面就借助这款工具来解决查询代码复杂的问题。
首先,在pom中添加依赖:

 <!--Apache Commons DbUtils-->
        <dependency>
            <groupId>commons-dbutils</groupId>
            <artifactId>commons-dbutils</artifactId>
            <version>1.6</version>
        </dependency>

在DataBaseHelper中使用以上工具:

 private static final QueryRunner QUERY_RUNNER = new QueryRunner();
 /**
     * 查询客户列表:使用工具DbUtil
     * Object... params:“Varargs”机制。借助这一机制,可以定义能和多个实参相匹配的形参。
     * 但是该方法每次操作都需要创建并关闭Connection,Connection对于开发人员并不完全透明
     */
    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;
    }

借助DbUtils提供的QueryRunner对象可以面向实体(Entity)进行查询。实际上。DbUtils首先执行SQL语句并返回一个ResultSet,随后通过反射区创建并初始化实体对象。关于反射的相关知识,在前面的博客有过初步的涉及,不过我现在也还是在学习阶段,对深层的反射机制,还不是很清楚。暂且理解为:根据ResultSet返回的信息获取实体的名称,再以其名称并配合其基本的方法(如model中属性的get和set)创建实体。
有了上述的操作,现在再回到CustomerService中的getCustomerList方法:

 public List<Customer> getCustomerList(){
        Connection conn = DataBaseHelper.getConnection();
        try{
            String sql = "SELECT * FROM customer";
            return DataBaseHelper.queryEntityList(Customer.class,conn,sql);
        }finally {
            DataBaseHelper.closeConnection(conn);
        }
    }

可见,现在的代码简洁了很多,不需要面对PreparedStatement和ResultSet了,只需要借助DataBaseHelper就能对数据库进行操作。但是,这仍然存在一点瑕疵,对于开发者而言,在service层总是希望更多的关注业务本身而不是其他的操作,然而在目前的service中,开发人员每开发一个业务功能首先要创建数据库,进行数据操作,关闭数据库。开发人员只关心数据操作而不关系数据库的创建和关闭。有必要进行进一步的封装,使得Connection在service层对于开发人员完全透明。

在service层完全透明化connection

如何使Connection对于service完全透明呢?是否说简单的隐藏掉就可以呢?答案是否定的,因为程序要保证在执行service中的数据库查询的方法必须是线性安全的,如果只是粗暴的在DataBaseHelper中封装数据库的创建和关闭操作,会影响到程序的健壮性。为了确保一个线程中只有一个Connection,我们可以使用ThreadLocal来存放本地线程变量。
也就是说,将当前的线程中的Connection放入ThreadLocal中存起来,这些Connection一定不会出现线程不安全的问题,可以将ThreadLocal理解为一个隔离线程的容器。

public class DataBaseHelper {

    private static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(PropsUtil.class);
    private static final QueryRunner QUERY_RUNNER = new QueryRunner();
    private static final ThreadLocal<Connection> CONNECTION_THREAD_LOCAL = new ThreadLocal<Connection>();

    /**
     * 使用静态代码块初始化配置项定义的一些常量
     */
    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 drive", e);
        }
    }

    /**
     * 获取数据库连接
     */
    public static Connection getConnection(){
        Connection conn = CONNECTION_THREAD_LOCAL.get();
        try{
            conn = DriverManager.getConnection(URL, USERNAME, PASSWORD);
        }catch (SQLException e){
            LOGGER.error("get connection failure", e);
            throw new RuntimeException(e);
        }finally {
            CONNECTION_THREAD_LOCAL.set(conn);
        }
        return conn;
    }

    /**
     * 关闭数据库连接
     */
    public static void closeConnection(){
        Connection conn = CONNECTION_THREAD_LOCAL.get()();
        if(conn!=null){
            try{
                conn.close();
            }catch (SQLException e){
                LOGGER.error("close connection failure", e);
                throw new RuntimeException(e);
            }finally {
                CONNECTION_THREAD_LOCAL.remove();
            }
        }
    }

    /**
     * 查询客户列表:使用工具DbUtil
     * Object... params:“Varargs”机制。借助这一机制,可以定义能和多个实参相匹配的形参。
     * 但是该方法每次操作都需要创建并关闭Connection,Connection对于开发人员并不完全透明
     * 通过引入本地线程变量CONNECTION_THREAD_LOCAL提供线程安全的保证。
     */
    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中寻找,如果不存在,则创建一个新的Connection,并将其放入到ThreadLocal中。当这个连接使用完毕后,就从ThreadLocal中移除。
至此,CustomerService中的getCustomerList方法又可以进一步简化了,只有短短的两行:

 public List<Customer> getCustomerList(){
            String sql = "SELECT * FROM customer";
            return DataBaseHelper.queryEntityList(Customer.class,sql);
    }

对比一下一开始的代码,感受下:

 public List<Customer> getCustomerList(){
        Connection conn = null;
        try{
            List<Customer> customerList = new ArrayList<Customer>();
            String sql = "SELECT * FROM customer";
            conn = DriveManager.getConnection(URL, USERNAME, PASSWORD);
            PreparedStatement stmt = conn.prepareStatement(sql);
            ResultSet rs = stmt.executeQuery();
            while (rs.next()){
                Customer customer = new Customer();
                customer.setId(rs.getLong("id"));
                customer.setName(rs.getString("name"));
                customer.setContract(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);
                }
            }

        }
    }

三十行的代码被压缩到两行啦。当然,除了看得到的简洁,更重要的是提高的程序的整体质量。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值