從模板方法模式到JDBCTemplate

將大象裝進冰箱須要三步,那麼老虎了?如何優雅的將大象裝進冰箱?前端

把大象裝進冰箱

Step 大象 老虎 ...
First 打開冰箱門 打開冰箱門 打開冰箱門
Second 把大象放進去 把老虎放進去 ...
Third 關閉冰箱門 關閉冰箱門 關閉冰箱門

大象類java

public class Elephant {
        public void putRefrigerator() {
            openDoor();
            putElephant();
            closeDoor();
        }
        public void openDoor() {
            System.out.println("open the door");
        }
        public void putElephant() {
            System.out.println("put in the Elephant");
        }
        public void closeDoor() {
            System.out.println("close the door");
        }
    }
複製代碼

老虎類mysql

public class Tiger {
        public void putRefrigerator() {
            openDoor();
            putTiger();
            closeDoor();
        }
        public void openDoor() {
            System.out.println("open the door");
        }
        public void putTiger() {
            System.out.println("put in the Tiger");
        }
        public void closeDoor() {
            System.out.println("close the door");
        }
    }
複製代碼

能夠看出咱們將大象和老虎放進冰箱的過程當中出現了大量的重複代碼,這顯然不是一個好的設計,若是咱們在之後的系統升級過程當中須要再放入長頸鹿怎麼辦,咱們應該如何從咱們的設計中刪除這些重複代碼?經過觀察咱們發現放大象和放老虎之間有不少共同點,都須要進行開關門的操做,只是放的過程不盡相同,咱們是否能夠將共同點抽離?咱們一塊兒試試看算法

抽象超類spring

public abstract class AbstractPutAnyAnimal {
        //這是一個模板方法,它是一個算法的模板,描述咱們將動物放進冰箱的步驟,每個方法表明了一個步驟
        public void putRefrigerator() {
            openDoor();
            putAnyAnimal();
            closeDoor();
        }
        //在超類中實現共同的方法,由超類來處理
        public void openDoor() {
            System.out.println("open the door");
        }
        public void closeDoor() {
            System.out.println("close the door");
        }
        //每一個子類可能有不一樣的方法,咱們定義成抽象方法讓子類去實現
        abstract void putAnyAnimal();
    }
複製代碼

大象類sql

public class Elephant extends AbstractPutAnyAnimal {
        //子類實現本身的業務邏輯
        @Override
        void putAnyAnimal() {
            System.out.println("put in the Elephant");
        }
    }
複製代碼

老虎類數據庫

public class Tiger extends AbstractPutAnyAnimal {
        //子類實現本身的業務邏輯
        @Override
        void putAnyAnimal() {
            System.out.println("put in the Tiger");
        }
    }
複製代碼

經過將相同的方法抽離到超類中,並定義一個抽象方法供子類提供不一樣的實現,事實上咱們剛剛實現了一個模板方法模式。編程

模板方法模式定義?

模板方法模式定義了一個算法的步驟,並容許子類爲一個或多個步驟提供實現,putRefrigerator 方法定義了咱們將大象裝進冰箱的步驟它就是一個模板方法。模板方法模式在一個方法中定義一個算法的骨架,而將一些步驟延遲到子類中,模板方法使得子類能夠不在改變算法結構的狀況下,從新定義算法的某些步驟(子類提供本身的實現)設計模式

模板方法模式中的鉤子

咱們能夠在超類中定義一個空方法,咱們稱這種方法爲鉤子(hook)。子類能夠依據狀況選擇覆蓋,鉤子的存在可讓子類有能力對算法的不一樣點進行掛載;鉤子可讓子類實現算法中的可選部分,鉤子也可讓子類爲抽象類作一些決定咱們將大象裝進冰箱後可能會想調整冰箱溫度,也可能什麼都不作使用默認溫度,咱們能夠經過定義一個鉤子,讓子類來選擇是否調整溫度,以下:markdown

抽象父類

public abstract class AbstractPutAnyAnimal {
        public void putRefrigerator() {
            openDoor();
            putAnyAnimal();
            closeDoor();
            //默認爲false,從新這個方法決定是否執行addTemperature();方法
            if (isAdd()) {
                addTemperature();
            }
        }
        public void openDoor() {
            System.out.println("open the door");
        }
        public void closeDoor() {
            System.out.println("close the door");
        }
        abstract void putAnyAnimal();
        void addTemperature(){
            System.out.println("plus one");
        };
        //定義一個空實現,由子類決定是否對其進行實現
        boolean isAdd(){
            return false;
        }
    }
複製代碼

大象類

public class Elephant extends AbstractPutAnyAnimal {
        @Override
        void putAnyAnimal() {
            System.out.println("put in the Elephant");
        }
        //子類實現鉤子方法
        @Override
        boolean isAdd() {
            return true;
        }
    }
複製代碼

咱們經過定義一個鉤子方法,子類選擇是否實現這個鉤子方法,來決定是否調整溫度;固然鉤子方法的用途不止如此,它還能讓子類有機會對模板中即將發生或剛剛發生的步驟作出反應,這在JDK中有不少的例子,甚至在前端開發領域也有不少例子,我就不具體展開代碼演示了,後面在模板方法模式的更多應用中展開。

JDK以及Spring中使用了不少的設計模式,下面咱們經過比較傳統JDBC編程和JDBCTemplate來看看模板方法模式是如何幫咱們消除樣板代碼的

傳統JDBC編程

JDBC編程之新增

String driver = "com.mysql.jdbc.Driver";
        String url = "jdbc:mysql://localhost:3306/test?characterEncoding=UTF-8&useSSL=true";
        String username = "root";
        String password = "1234";
        Connection connection = null;
        Statement statement = null;
        try {
            Class.forName(driver);
            connection = DriverManager.getConnection(url, username, password);
            String sql = "insert into users(nickname,comment,age) values('小小譚','I love three thousand times', '21')";
            statement = connection.createStatement();
            int i = statement.executeUpdate(sql);
            return i;
        } catch (SQLException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            try {
                if (null != statement) {
                    statement.close();
                }
                if (null != connection) {
                    connection.close();
                }
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
        return 0;
複製代碼

JDBC編程之查詢

String driver = "com.mysql.jdbc.Driver";
        String url = "jdbc:mysql://localhost:3306/test?characterEncoding=UTF-8&useSSL=true";
        String username = "root";
        String password = "1234";
        Connection connection = null;
        Statement statement = null;
        try{
            Class.forName(driver);
            connection = DriverManager.getConnection(url, username, password);
            String sql = "select nickname,comment,age from users";
            statement = connection.createStatement();
            ResultSet resultSet = statement.executeQuery(sql);
            List<Users> usersList = new ArrayList<>();
            while (resultSet.next()) {
                Users users = new Users();
                users.setNickname(resultSet.getString(1));
                users.setComment(resultSet.getString(2));
                users.setAge(resultSet.getInt(3));
                usersList.add(users);
            }
            return usersList;
        } catch (SQLException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            try {
                if (null != statement) {
                    statement.close();
                }
                if (null != connection) {
                    connection.close();
                }
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
        return null;
複製代碼

上面給出了咱們在傳統JDBC編程中的兩個案例,能夠看到傳統JDBC的不少缺點,固然在實際項目中咱們可能不會這麼原始的進行數據庫開發,可能會對JDBC進行必定的封裝,方便咱們的使用。Spring 官方爲了簡化JDBC的開發也發佈了JDBCTemplate,下面咱們就看一下它是如何簡化開發的,以及模板方法模式在其中的應用

JDBCTemplate是個啥,它到底簡化了什麼?

從JDBCTemplate的名字咱們就不難看出,它簡化了咱們JDBC的開發,並且極可能大量應用了模板方法模式,它到底爲咱們提供了什麼?它提供了與平臺無光的異常處理機制。使用過原生JDBC開發的同窗可能有經歷,幾乎全部的操做代碼都須要咱們強制捕獲異常,可是在出現異常時咱們每每沒法經過異常讀懂錯誤。Spring解決了咱們的問題它提供了多個數據訪問異常,而且分別描述了他們拋出時對應的問題,同時對異常進行了包裝不強制要求咱們進行捕獲,同時它爲咱們提供了數據訪問的模板化,從上面的傳統JDBC編程咱們能夠發現,不少操做實際上是重複的不變得好比事務控制、資源的獲取關閉以及異常處理等,同時結果集的處理實體的綁定,參數的綁定這些東西都是特有的。所以Spring將數據訪問過程當中固定部分和可變部分劃分爲了兩個不一樣的類(Template)和回調(Callback),模板處理過程當中不變得部分,回調處理自定義的訪問代碼;下面咱們具體經過源碼來學學習一下

模板方法模式在JDBCTemplate中的應用

我所使用的版本是5.1.5.RELEASE

打開JdbcTemplate類(我這裏就不截圖了,截圖可能不清晰我直接將代碼copy出來):

JdbcTemplate

public class JdbcTemplate extends JdbcAccessor implements JdbcOperations {
        //查詢前綴
        private static final String RETURN_RESULT_SET_PREFIX = "#result-set-";
        //計數前綴
        private static final String RETURN_UPDATE_COUNT_PREFIX = "#update-count-";
        //是否跳過警告
        private boolean ignoreWarnings = true;
        //查詢大小
        private int fetchSize = -1;
        //最大行
        private int maxRows = -1;
        //查詢超時
        private int queryTimeout = -1;
        //是否跳過結果集處理
        private boolean skipResultsProcessing = false;
        //是否跳過非公共結果集處理
        private boolean skipUndeclaredResults = false;
        //map結果集是否大小寫敏感
        private boolean resultsMapCaseInsensitive = false;
    
        public JdbcTemplate() {
        }
        //調用父類方法設置數據源和其餘參數
        public JdbcTemplate(DataSource dataSource) {
            this.setDataSource(dataSource);
            this.afterPropertiesSet();
        }
        //調用父類方法設置數據源,懶加載策略和其餘參數
        public JdbcTemplate(DataSource dataSource, boolean lazyInit) {
            this.setDataSource(dataSource);
            this.setLazyInit(lazyInit);
            this.afterPropertiesSet();
        }
    }
複製代碼

JdbcTemplate 繼承了JdbcAccessor實現了JdbcOperations,JdbcAccessor主要封裝了數據源的操做,JdbcOperations主要定義了一些操做接口。咱們一塊兒看一下JdbcOperations類;

public abstract class JdbcAccessor implements InitializingBean {
        protected final Log logger = LogFactory.getLog(this.getClass());
        //數據源
        @Nullable
        private DataSource dataSource;
        //異常翻譯
        @Nullable
        private volatile SQLExceptionTranslator exceptionTranslator;
        //懶加載策略
        private boolean lazyInit = true;
        public JdbcAccessor() {
        }
        public void setDataSource(@Nullable DataSource dataSource) {
            this.dataSource = dataSource;
        }
        @Nullable
        public DataSource getDataSource() {
            return this.dataSource;
        }
        protected DataSource obtainDataSource() {
            DataSource dataSource = this.getDataSource();
            Assert.state(dataSource != null, "No DataSource set");
            return dataSource;
        }
        public void setDatabaseProductName(String dbName) {
            this.exceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(dbName);
        }
        public void setExceptionTranslator(SQLExceptionTranslator exceptionTranslator) {
            this.exceptionTranslator = exceptionTranslator;
        }
    }
複製代碼

之因此前面提到spring讓咱們更方便的處理異常就是這裏他包裝了一個SQLExceptionTranslator,其餘的代碼都是作數據源的檢查之類的設置數據源,咱們看一下其中getExceptionTranslator()方法

public SQLExceptionTranslator getExceptionTranslator() {
        SQLExceptionTranslator exceptionTranslator = this.exceptionTranslator;
        if (exceptionTranslator != null) {
            return exceptionTranslator;
        } else {
            synchronized(this) {
                SQLExceptionTranslator exceptionTranslator = this.exceptionTranslator;
                if (exceptionTranslator == null) {
                    DataSource dataSource = this.getDataSource();
                    if (dataSource != null) {
                        exceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
                    } else {
                        exceptionTranslator = new SQLStateSQLExceptionTranslator();
                    }
                    this.exceptionTranslator = (SQLExceptionTranslator)exceptionTranslator;
                }
                return (SQLExceptionTranslator)exceptionTranslator;
            }
        }
    }
複製代碼

這是一個標準的單例模式,咱們在學習模板方法模式的路途中有捕獲了一個野生的單例;咱們繼續看JdbcOperations接口咱們調其中一個接口進行解析;

@Nullable
    <T> T execute(StatementCallback<T> var1) throws DataAccessException;
複製代碼

StatementCallback 接口

@FunctionalInterface
    public interface StatementCallback<T> {
        @Nullable
        T doInStatement(Statement var1) throws SQLException, DataAccessException;
    }
複製代碼

execute實現

@Nullable
    public <T> T execute(StatementCallback<T> action) throws DataAccessException {
        //參數檢查
        Assert.notNull(action, "Callback object must not be null");
        //獲取鏈接
        Connection con = DataSourceUtils.getConnection(this.obtainDataSource());
        Statement stmt = null;
        Object var11;
        try {
            //建立一個Statement
            stmt = con.createStatement();
            //設置查詢超時時間,最大行等參數(就是一開始那些成員變量)
            this.applyStatementSettings(stmt);
            //執行回調方法獲取結果集
            T result = action.doInStatement(stmt);
            //處理警告
            this.handleWarnings(stmt);
            var11 = result;
        } catch (SQLException var9) {
            //出現錯誤優雅退出
            String sql = getSql(action);
            JdbcUtils.closeStatement(stmt);
            stmt = null;
            DataSourceUtils.releaseConnection(con, this.getDataSource());
            con = null;
            throw this.translateException("StatementCallback", sql, var9);
        } finally {
            JdbcUtils.closeStatement(stmt);
            DataSourceUtils.releaseConnection(con, this.getDataSource());
        }
        return var11;
    }
複製代碼

這一個方法可謂是展示的淋漓盡致,這是一個典型的模板方法+回調模式,咱們不須要再寫過多的重複代碼只須要實現本身獲取result的方法就好(StatementCallback)事實上咱們本身也不須要實現這個方法,繼續向上看,咱們是如何調用execute方法的,以查詢爲例,咱們看他是如何一步步調用的:

查詢方法

public List<Users> findAll() {
        JdbcTemplate jdbcTemplate = DataSourceConfig.getTemplate();
        String sql = "select nickname,comment,age from users";
        return jdbcTemplate.query(sql, new BeanPropertyRowMapper<Users>(Users.class));
    }
複製代碼

query實現

public <T> List<T> query(String sql, RowMapper<T> rowMapper) throws DataAccessException {
        return (List)result(this.query((String)sql, (ResultSetExtractor)(new RowMapperResultSetExtractor(rowMapper))));
    }
複製代碼

這裏的RowMapper是負責將結果集中一行的數據映射成實體返回,用到了反射技術,這裏就不展開了,有興趣的同窗能夠本身打開源碼閱讀,繼續向下:

query實現

@Nullable
    public <T> T query(final String sql, final ResultSetExtractor<T> rse) throws DataAccessException {
        Assert.notNull(sql, "SQL must not be null");
        Assert.notNull(rse, "ResultSetExtractor must not be null");
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Executing SQL query [" + sql + "]");
        }
        //實現回調接口
        class QueryStatementCallback implements StatementCallback<T>, SqlProvider {
            QueryStatementCallback() {
            }
            @Nullable
            public T doInStatement(Statement stmt) throws SQLException {
                ResultSet rs = null;
                Object var3;
                try {
                    //這裏真正的執行咱們的sql語句
                    rs = stmt.executeQuery(sql);
                    //處理對象映射
                    var3 = rse.extractData(rs);
                } finally {
                    JdbcUtils.closeResultSet(rs);
                }
                return var3;
            }
            public String getSql() {
                return sql;
            }
        }
        //調用execute接口
        return this.execute((StatementCallback)(new QueryStatementCallback()));
    }
複製代碼

看到這裏相信你也不得拍手稱奇,Spring處理的很是巧妙,請繼續向下看:

update詳解

protected int update(PreparedStatementCreator psc, @Nullable PreparedStatementSetter pss) throws DataAccessException {
        this.logger.debug("Executing prepared SQL update");
        return updateCount((Integer)this.execute(psc, (ps) -> {
            Integer var4;
            try {
                if (pss != null) {
                    pss.setValues(ps);
                }
                int rows = ps.executeUpdate();
                if (this.logger.isTraceEnabled()) {
                    this.logger.trace("SQL update affected " + rows + " rows");
                }
                var4 = rows;
            } finally {
                if (pss instanceof ParameterDisposer) {
                    ((ParameterDisposer)pss).cleanupParameters();
                }
            }
            return var4;
        }));
    }
複製代碼

爲何我要把update函數拎出來說了,由於update這裏使用了lambda函數,回想咱們StatementCallback定義只有一個方法的接口,他就是一個函數是接口,因此他是一個函數式接口,因此這裏直接使用lambda語法,lambda函數容許你直接內連,爲函數接口的抽象方法提供實現,而且整個表達式做爲函數接口的一個實例。咱們在平時學習中可能知道了lambda語法可是可能使用的較少,或者不知道如何用於實戰,那麼多閱讀源碼必定能夠提高你的實戰能力。 咱們能夠看到JDBCTemplate使用了不少回調。爲何要用回調(Callback)?若是父類有多個抽象方法,子類須要所有實現這樣特別麻煩,而有時候某個子類只須要定製父類中的某一個方法該怎麼辦呢?這個時候就要用到Callback回調了就能夠完美解決這個問題,能夠發現JDBCTemplate並無徹底拘泥於模板方法,很是靈活。咱們在實際開發中也能夠借鑑這種方法。

模板方法模式的更多應用

事實上不少有關生命週期的類都用到了模板方法模式,最典型的也是可能咱們最熟悉的莫過於Servlet了,廢話很少說上源碼

public abstract class HttpServlet extends GenericServlet {
    }
複製代碼

HttpServlet的全部方法,咱們看到HttpServlet繼承了GenericServlet,咱們繼續看:

public abstract class GenericServlet implements Servlet, ServletConfig, java.io.Serializable {
    private static final String LSTRING_FILE = "javax.servlet.LocalStrings";
    private static ResourceBundle lStrings =
        ResourceBundle.getBundle(LSTRING_FILE);

    private transient ServletConfig config;
    
    public GenericServlet() { }
    
    //沒有實現鉤子
    public void destroy() {
    }
    
    public String getInitParameter(String name) {
        ServletConfig sc = getServletConfig();
        if (sc == null) {
            throw new IllegalStateException(
                lStrings.getString("err.servlet_config_not_initialized"));
        }

        return sc.getInitParameter(name);
    }
    
    public Enumeration<String> getInitParameterNames() {
        ServletConfig sc = getServletConfig();
        if (sc == null) {
            throw new IllegalStateException(
                lStrings.getString("err.servlet_config_not_initialized"));
        }

        return sc.getInitParameterNames();
    }   
     
    public ServletConfig getServletConfig() {
	return config;
    }
    
    public ServletContext getServletContext() {
        ServletConfig sc = getServletConfig();
        if (sc == null) {
            throw new IllegalStateException(
                lStrings.getString("err.servlet_config_not_initialized"));
        }

        return sc.getServletContext();
    }

    public String getServletInfo() {
	return "";
    }

    public void init(ServletConfig config) throws ServletException {
	this.config = config;
	this.init();
    }
    
    public void init() throws ServletException {

    }
    
    public void log(String msg) {
	getServletContext().log(getServletName() + ": "+ msg);
    }
  
    public void log(String message, Throwable t) {
	getServletContext().log(getServletName() + ": " + message, t);
    }
    
    public abstract void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;
    
    public String getServletName() {
        ServletConfig sc = getServletConfig();
        if (sc == null) {
            throw new IllegalStateException(
                lStrings.getString("err.servlet_config_not_initialized"));
        }

        return sc.getServletName();
    }
}
複製代碼

能夠看到這就是個典型的模板方法類蠻,並且鉤子函數也在這裏展示的淋漓盡致,如init、destroy方法等,JDK中不少類都是用了模板方法等着你發現哦。

模板方法模式在Vue.js中的應用

模板方法模式在其餘語言中也有實現好比Vue.js、React中;好比Vue生命週期確定使用了模板方法,我就不對源碼展開分析了。

總結

設計模式在Spring中獲得了大量的應用,感興趣的同窗能夠看看Spring源碼加以學習,若是你以爲我寫的還不錯的話點個贊吧,若是你發現了錯誤,或者很差的地方也能夠及時告訴我加以改正,謝謝!您的讚揚和批評是進步路上的好夥伴。

相關文章
相關標籤/搜索