深入理解框架背后的原理及源碼分析
近期團隊中同學遇到幾個問題,想在這兒跟大家分享一波,雖說不是很有難度,但是背后也折射出一些問題,值得思考。
開始之前先簡單介紹一下我所在團隊的技術(shù)棧,基于這個背景再展開后面將提到的幾個問題,將會有更深刻的體會。
控制層基于SpringMvc,數(shù)據(jù)持久層基于JdbcTemplate自己封裝了一套類MyBatis的Dao框架,視圖層基于Velocity模板技術(shù),其余組件基于SpringCloud全家桶。
問題1
某應用發(fā)布以后開始報數(shù)據(jù)庫連接池不夠用異常,日志如下:
com.alibaba.druid.pool.GetConnectionTimeoutException: wait millis 60000, active 500, maxActive 500, creating 0
很明顯這是數(shù)據(jù)庫連接池滿了,當時處于業(yè)務(wù)低峰期,所以很顯然并不是由于流量突發(fā)造成的,另一種可能性是長事務(wù)導致,一般是事務(wù)中摻雜了外部網(wǎng)絡(luò)調(diào)用,最終跟業(yè)務(wù)負責人一起排除了長事務(wù)的可能性。
還有什么可能呢?我隨即想到了是不是沒有釋放連接導致,我跟業(yè)務(wù)負責人說了這個想法,他說這種可能性不大,連接的獲取和釋放都是由框架完成的,如果這塊有問題早反映出來了,我想也是。
框架的確給我們帶來了很大的便利性,將業(yè)務(wù)中一些重復性的工作下沉到框架中,提高了研發(fā)效率,不夸張的說有些人脫離了Spring,MyBatis,SpringMvc這些框架,都不會寫代碼了。
那會是什么原因呢?我又冒出來一個想法,有沒有可能是某些功能框架支持不了,所以開發(fā)繞過了框架自己實現(xiàn),進而導致連接沒有釋放,我跟業(yè)務(wù)負責人說了這個想法以后,他說:“這個有可能,這次有個功能需要獲取到數(shù)據(jù)庫名,所以自己通過Connection對象獲取的”,說到這兒答案大概已經(jīng)出來了,一起看下這段代碼:
public String getSchema(String tablename, boolean cached) throws Exception {
return this.getJdbcTemplate(tablename).getDataSource().getConnection().getCatalog();
}代碼很簡單通過JdbcTemplate獲取DataSource,再通過DataSource獲取Connection,最終通過Connection獲取數(shù)據(jù)庫名,就是這一行簡單的代碼將數(shù)據(jù)庫連接耗盡,因為這里并沒有釋放連接的動作,之前的為什么都沒有問題呢,因為普通的查詢都是委派給JdbcTemplate來實現(xiàn)的,它內(nèi)部會釋放連接,找一個簡單的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這個模板方法實現(xiàn),在execute內(nèi)部會通過finally來確保連接的釋放
DataSourceUtils.releaseConnection,所以不會有連接耗盡的問題,問題已經(jīng)很清晰了,改造也很簡單,大概有幾下幾種方法:
1.顯示的關(guān)閉連接,這里可以借助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的模板方法設(shè)計思想來解決,它提供了一個execute方法,用戶只要實現(xiàn)ConnectionCallback這個接口就可以獲取到Connection對象,在內(nèi)部執(zhí)行獲取數(shù)據(jù)庫名的邏輯,最終關(guān)閉連接由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();
}
});雖然兩種都能解決問題,但我還是更推崇第二種方式,因為這種更貼合框架的設(shè)計思想,將一些重復性的邏輯繼續(xù)交給框架去實現(xiàn),這里也體現(xiàn)出框架很重要的一個特點,就是對使用者提供擴展。
問題2
前幾天寫了一個Spring AOP的攔截功能,發(fā)現(xiàn)怎么也進不到攔截邏輯中,表達式確定沒問題,讓我百思不得其解,最終冷靜下來逐步排錯。
第一個很明顯的錯誤是被攔截的對象并沒有納入Spring管理,所以當即把對象交由Spring管理,問題依然沒有解決,我開始回想代理的原理。
Spring代理提供了兩種實現(xiàn)方式,一種是jdk的動態(tài)代理,另一種是cglib代理,這兩種方式分別適用于代理類實現(xiàn)了接口和代理類未實現(xiàn)接口的情況,其內(nèi)部思想都是基于某種規(guī)約(接口或者父類)來生成一個Proxy對象,在Proxy對象方法調(diào)用時先調(diào)用InvocationHandler的invoke方法,在invoke方法內(nèi)部先執(zhí)行代理邏輯,再執(zhí)行被代理對象的真實邏輯,這里貼一段jdk動態(tài)代理生成的Proxy對象的源文件供大家閱讀:
public class ProxyTest {
/**
定義目標接口,內(nèi)部包含一個hello方法(這其實就是一個規(guī)約)
*/
public interface ProxyT{
void hello();
}
/**
實現(xiàn)類,實現(xiàn)了ProxyT接口
*/
public static class ProxyTImpl implements ProxyT{
@Override
public void hello() {
System.out.println("aaaa");
}
}
public static void main(String[] args) {
//設(shè)置生成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類,實現(xiàn)了ProxyT接口
(這里其實也解釋了為什么jdk的動態(tài)代理只能基于接口實現(xiàn),不能基于父類,因為Proxy
必須繼承jdk的Proxy,而java又是單繼承,所以Proxy只能基于接口這個規(guī)約來生成)
*/
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方法將調(diào)用權(quán)交給了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());
}
}
}到這里其實我已經(jīng)有了答案,是我給Spring的規(guī)約(接口或者父類)出現(xiàn)了問題,首先我要代理的類并沒有實現(xiàn)接口,所以這里的規(guī)約不是接口,而是我這個類本身,從cglib的原理來講,它是將要代理的類作為父類來生成一個Proxy類,重寫要代理的方法,進而添加代理邏輯,問題就在于我那個類的方法是static的,而static方法是沒法重寫的,所以導致一直沒有進攔截邏輯,將static方法改為實例方法就解決了問題,這里貼一段cglib動態(tài)代理生成的Proxy對象的源文件供大家閱讀:
public class cglibtest {
//定義被代理的類ProxyT,內(nèi)部有一個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");
//執(zhí)行被代理類的方法(hello)
return methodProxy.invokeSuper(o,objects);
}
}
public static void main(String[] args) {
// 設(shè)置CGLib代理類的生成位置
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "./cg");
// 設(shè)置JDK代理類的輸出
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
MethodInterceptor methodInterceptor = new Interceptor();
Enhancer enhancer = new Enhancer();
//設(shè)置父類
enhancer.setSuperclass(ProxyT.class);
//設(shè)置方法回調(diào)
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) {
//執(zhí)行方法攔截器
var10000.intercept(this, CGLIB$hello$0$Method, CGLIB$emptyArgs, CGLIB$hello$0$Proxy);
} else {
super.hello();
}
}
}總結(jié)
前面描述了筆者近期工作中遇到的兩個問題,不能說多么有難度,但是我相信應該有不少人都碰到過,不知道你是怎么解決的呢?解決了以后有沒有深挖其背后的原理呢,好多人說自己的工作都是簡單的crud沒有提高,那何不嘗試著深挖框架背后的原理,深挖那些看似普通但背后并不簡單的問題的本質(zhì)。
框架雖好,但不要丟了其背后的原理。
以上就是深入理解框架背后的原理及源碼分析的詳細內(nèi)容,更多關(guān)于框架原理及源碼分析的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java的微信開發(fā)中使用XML格式和JSON格式數(shù)據(jù)的示例
這篇文章主要介紹了Java微信開發(fā)中使用XML格式和JSON格式數(shù)據(jù)的示例,注意一下json-lib所需要的jar包,需要的朋友可以參考下2016-02-02
Hystrix?Dashboard斷路監(jiān)控儀表盤的實現(xiàn)詳細介紹
這篇文章主要介紹了Hystrix?Dashboard斷路監(jiān)控儀表盤的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2022-09-09
kafka啟動報錯(Cluster ID)不匹配問題以及解決
這篇文章主要介紹了kafka啟動報錯(Cluster ID)不匹配問題以及解決方案,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-12-12
懶人 IDEA 插件推薦: EasyCode 一鍵幫你生成所需代碼(Easycode用法)
這篇文章主要介紹了懶人 IDEA 插件推薦: EasyCode 一鍵幫你生成所需代碼,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-08-08

