框架雖好,但不要丟了其背後的原理

踩刀詩人發表於2021-01-31

近期團隊中同學遇到幾個問題,想在這兒跟大家分享一波,雖說不是很有難度,但是背後也折射出一些問題,值得思考。

 

開始之前先簡單介紹一下我所在團隊的技術棧,基於這個背景再展開後面將提到的幾個問題,將會有更深刻的體會。

控制層基於SpringMvc,資料持久層基於JdbcTemplate自己封裝了一套類MyBatis的Dao框架,檢視層基於Velocity模板技術,其餘元件基於SpringCloud全家桶。

 

問題1

某應用釋出以後開始報資料庫連線池不夠用異常,日誌如下:

com.alibaba.druid.pool.GetConnectionTimeoutException: wait millis 60000, active 500, maxActive 500, creating 0   

很明顯這是資料庫連線池滿了,當時處於業務低峰期,所以很顯然並不是由於流量突發造成的,另一種可能性是長事務導致,一般是事務中摻雜了外部網路呼叫,最終跟業務負責人一起排除了長事務的可能性。

 

還有什麼可能呢?我隨即想到了是不是沒有釋放連線導致,我跟業務負責人說了這個想法,他說這種可能性不大,連線的獲取和釋放都是由框架完成的,如果這塊有問題早反映出來了,我想也是。

 框架的確給我們帶來了很大的便利性,將業務中一些重複性的工作下沉到框架中,提高了研發效率,不誇張的說有些人脫離了Spring,MyBatis,SpringMvc這些框架,都不會寫程式碼了。

 

那會是什麼原因呢?我又冒出來一個想法,有沒有可能是某些功能框架支援不了,所以開發繞過了框架自己實現,進而導致連線沒有釋放,我跟業務負責人說了這個想法以後,他說:“這個有可能,這次有個功能需要獲取到資料庫名,所以自己通過Connection物件獲取的”,說到這兒答案大概已經出來了,一起看下這段程式碼:

public String getSchema(String tablename, boolean cached) throws Exception {
    return this.getJdbcTemplate(tablename).getDataSource().getConnection().getCatalog();
}

程式碼很簡單通過JdbcTemplate獲取DataSource,再通過DataSource獲取Connection,最終通過Connection獲取資料庫名,就是這一行簡單的程式碼將資料庫連線耗盡,因為這裡並沒有釋放連線的動作,之前的為什麼都沒有問題呢,因為普通的查詢都是委派給JdbcTemplate來實現的,它內部會釋放連線,找一個簡單的query方法看下:

public <T> T query(PreparedStatementCreator psc, @Nullable final PreparedStatementSetter pss, final ResultSetExtractor<T> rse) throws DataAccessException {
        Assert.notNull(rse, "ResultSetExtractor must not be null");
        this.logger.debug("Executing prepared SQL query");
        return this.execute(psc, new PreparedStatementCallback<T>() {
            @Nullable
            public T doInPreparedStatement(PreparedStatement ps) throws SQLException {
                ResultSet rs = null;
​
                Object var3;
                try {
                    if (pss != null) {
                        pss.setValues(ps);
                    }
​
                    rs = ps.executeQuery();
                    var3 = rse.extractData(rs);
                } finally {
                    JdbcUtils.closeResultSet(rs);
                    if (pss instanceof ParameterDisposer) {
                        ((ParameterDisposer)pss).cleanupParameters();
                    }
​
                }
​
                return var3;
            }
        });
    }

    public <T> T execute(PreparedStatementCreator psc, PreparedStatementCallback<T> action) throws DataAccessException {
        Assert.notNull(psc, "PreparedStatementCreator must not be null");
        Assert.notNull(action, "Callback object must not be null");
        if (this.logger.isDebugEnabled()) {
            String sql = getSql(psc);
            this.logger.debug("Executing prepared SQL statement" + (sql != null ? " [" + sql + "]" : ""));
        }
​
        Connection con = DataSourceUtils.getConnection(this.obtainDataSource());
        PreparedStatement ps = null;
​
        Object var13;
        try {
            ps = psc.createPreparedStatement(con);
            this.applyStatementSettings(ps);
            T result = action.doInPreparedStatement(ps);
            this.handleWarnings((Statement)ps);
            var13 = result;
        } catch (SQLException var10) {
            if (psc instanceof ParameterDisposer) {
                ((ParameterDisposer)psc).cleanupParameters();
            }
​
            String sql = getSql(psc);
            psc = null;
            JdbcUtils.closeStatement(ps);
            ps = null;
            DataSourceUtils.releaseConnection(con, this.getDataSource());
            con = null;
            throw this.translateException("PreparedStatementCallback", sql, var10);
        } finally {
            if (psc instanceof ParameterDisposer) {
                ((ParameterDisposer)psc).cleanupParameters();
            }
​
            JdbcUtils.closeStatement(ps);
            DataSourceUtils.releaseConnection(con, this.getDataSource());
        }
​
        return var13;
    }
​

query方法基於execute這個模板方法實現,在execute內部會通過finally來確保連線的釋放

DataSourceUtils.releaseConnection,所以不會有連線耗盡的問題,問題已經很清晰了,改造也很簡單,大概有幾下幾種方法:

1.顯示的關閉連線,這裡可以藉助jdk的try resource語句,簡單明瞭。

 public String getSchema(String tablename, boolean cached) throws Exception {
      try(Connection connection = this.getJdbcTemplate(tablename).getDataSource().getConnection()){
                return connection.getCatalog();
      }        
}    

2.藉助於JdbcTemplate的模板方法設計思想來解決,它提供了一個execute方法,使用者只要實現ConnectionCallback這個介面就可以獲取到Connection物件,在內部執行獲取資料庫名的邏輯,最終關閉連線由finally完成。

/**
   * Execute a JDBC data access operation, implemented as callback action
   * working on a JDBC Connection. This allows for implementing arbitrary
   * data access operations, within Spring's managed JDBC environment:
   * that is, participating in Spring-managed transactions and converting
   * JDBC SQLExceptions into Spring's DataAccessException hierarchy.
   * <p>The callback action can return a result object, for example a domain
   * object or a collection of domain objects.
   * @param action a callback object that specifies the action
   * @return a result object returned by the action, or {@code null} if none
   * @throws DataAccessException if there is any problem
   */
@Nullable
public <T> T execute(ConnectionCallback<T> action) throws DataAccessException {
    Assert.notNull(action, "Callback object must not be null");
    Connection con = DataSourceUtils.getConnection(this.obtainDataSource());
​
    Object var10;
    try {
        Connection conToUse = this.createConnectionProxy(con);
        var10 = action.doInConnection(conToUse);
    } catch (SQLException var8) {
        String sql = getSql(action);
        DataSourceUtils.releaseConnection(con, this.getDataSource());
        con = null;
        throw this.translateException("ConnectionCallback", sql, var8);
    } finally {
        DataSourceUtils.releaseConnection(con, this.getDataSource());
    }
​
        return var10;
 }
jdbcTemplate.execute(new ConnectionCallback<Object>() {
      @Override
      public Object doInConnection(Connection connection) throws SQLException, DataAccessException {
          return connection.getCatalog();
      }
});

雖然兩種都能解決問題,但我還是更推崇第二種方式,因為這種更貼合框架的設計思想,將一些重複性的邏輯繼續交給框架去實現,這裡也體現出框架很重要的一個特點,就是對使用者提供擴充套件。

 

問題2

前幾天寫了一個Spring AOP的攔截功能,發現怎麼也進不到攔截邏輯中,表示式確定沒問題,讓我百思不得其解,最終冷靜下來逐步排錯。

 

第一個很明顯的錯誤是被攔截的物件並沒有納入Spring管理,所以當即把物件交由Spring管理,問題依然沒有解決,我開始回想代理的原理。

Spring代理提供了兩種實現方式,一種是jdk的動態代理,另一種是cglib代理,這兩種方式分別適用於代理類實現了介面和代理類未實現介面的情況,其內部思想都是基於某種規約(介面或者父類)來生成一個Proxy物件,在Proxy物件方法呼叫時先呼叫InvocationHandler的invoke方法,在invoke方法內部先執行代理邏輯,再執行被代理物件的真實邏輯,這裡貼一段jdk動態代理生成的Proxy物件的原始檔供大家閱讀:

public class ProxyTest {
   /**
  定義目標介面,內部包含一個hello方法(這其實就是一個規約)
  */
    public interface ProxyT{
        void hello();
    }
​
    /**
    實現類,實現了ProxyT介面
    */
    public static class ProxyTImpl implements ProxyT{
        @Override
        public void hello() {
            System.out.println("aaaa");
        }
    }
​
    public static void main(String[] args) {
        //設定生成Proxy物件的原始檔
        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
​
        ProxyT proxyT1 = (ProxyT)Proxy.newProxyInstance(ProxyT.class.getClassLoader(),new Class[]{ProxyT.class}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("invoke");
                return method.invoke(proxyT,args);
            }
        });
​
        proxyT1.hello();
    }
}

最終生成的Proxy原始檔如下:

package com.sun.proxy;
​
import coding4fun.ProxyTest.ProxyT;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
​
/**
生成的proxy原始碼,繼承jdk的Proxy類,實現了ProxyT介面
(這裡其實也解釋了為什麼jdk的動態代理只能基於介面實現,不能基於父類,因為Proxy
必須繼承jdk的Proxy,而java又是單繼承,所以Proxy只能基於介面這個規約來生成)
*/
public final class $Proxy0 extends Proxy implements ProxyT {
    private static Method m1;
    private static Method m3;
    private static Method m2;
    private static Method m0;
​
    public $Proxy0(InvocationHandler var1) throws  {
        super(var1);
    }
​
    public final boolean equals(Object var1) throws  {
        try {
            return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }
​
    //hello方法將呼叫權交給了InvocationHandler
    public final void hello() throws  {
        try {
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }
​
    public final String toString() throws  {
        try {
            return (String)super.h.invoke(this, m2, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }
​
    public final int hashCode() throws  {
        try {
            return (Integer)super.h.invoke(this, m0, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }
​
    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m3 = Class.forName("coding4fun.ProxyTest$ProxyT").getMethod("hello");
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

到這裡其實我已經有了答案,是我給Spring的規約(介面或者父類)出現了問題,首先我要代理的類並沒有實現介面,所以這裡的規約不是介面,而是我這個類本身,從cglib的原理來講,它是將要代理的類作為父類來生成一個Proxy類,重寫要代理的方法,進而新增代理邏輯,問題就在於我那個類的方法是static的,而static方法是沒法重寫的,所以導致一直沒有進攔截邏輯,將static方法改為例項方法就解決了問題,這裡貼一段cglib動態代理生成的Proxy物件的原始檔供大家閱讀:

public class cglibtest {
    //定義被代理的類ProxyT,內部有一個hello方法
    public static class ProxyT{
        public void hello() {
            System.out.println("aaaa");
        }
    }
​
    //定義一個方法攔截器,和jdk的InvocationHandler類似
    public static class Interceptor implements MethodInterceptor {
        @Override
        public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
            //簡單的列印
            System.out.println("before invoke hello");
            //執行被代理類的方法(hello)
            return methodProxy.invokeSuper(o,objects);
        }
    }
​
    public static void main(String[] args) {
        // 設定CGLib代理類的生成位置
        System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "./cg");
        // 設定JDK代理類的輸出
        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
​
        MethodInterceptor methodInterceptor = new Interceptor();
​
        Enhancer enhancer = new Enhancer();
        //設定父類
        enhancer.setSuperclass(ProxyT.class);
        //設定方法回撥
        enhancer.setCallback(methodInterceptor);
​
        ProxyT proxy = (ProxyT)enhancer.create();
        proxy.hello();
    }
}
​

最終生成的Proxy原始檔如下(刪除了部分程式碼,只保留了重寫hello方法邏輯):

//繼承ProxyT
public class cglibtest$ProxyT$$EnhancerByCGLIB$$8b3109a3 extends ProxyT implements Factory {
   final void CGLIB$hello$0() {
        super.hello();
    }
​
    //重寫hello方法
    public final void hello() {
        //方法攔截器
        MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
        if (var10000 == null) {
            CGLIB$BIND_CALLBACKS(this);
            var10000 = this.CGLIB$CALLBACK_0;
        }
​
        if (var10000 != null) {
            //執行方法攔截器
            var10000.intercept(this, CGLIB$hello$0$Method, CGLIB$emptyArgs, CGLIB$hello$0$Proxy);
        } else {
            super.hello();
        }
    }
}

總結

前面描述了筆者近期工作中遇到的兩個問題,不能說多麼有難度,但是我相信應該有不少人都碰到過,不知道你是怎麼解決的呢?解決了以後有沒有深挖其背後的原理呢,好多人說自己的工作都是簡單的crud沒有提高,那何不嘗試著深挖框架背後的原理,深挖那些看似普通但背後並不簡單的問題的本質。

 

框架雖好,但不要丟了其背後的原理。

 

 

來我的公眾號與我交流

                                                               

 

相關文章