框架虽好,但不要丢了其背后的原理。​

开始之前先简单介绍一下我所在团队的技术栈,基于这个背景再展开后面将提到的几个问题,将会有更深刻的体会。

控制层基于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   */@Nullablepublic <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方法逻辑):

//继承ProxyTpublic 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没有提高,那何不尝试着深挖框架背后的原理,深挖那些看似普通但背后并不简单的问题的本质。

框架虽好,但不要丢了其背后的原理。

EOF

踩刀诗人

聊聊技术,唠唠段子,偶然做菜写诗

(0)

相关推荐

  • psc (640×372)

    psc (640&#215;372)

  • jdk 动态代理源码分析

    闲来无事,撸撸源码 食用方法 直接看代码吧.. package com.test.demo.proxy;import java.lang.reflect.InvocationHandler;impor ...

  • .net core 基于Dapper 的分库分表开源框架(core-data)

    一.前言 感觉很久没写文章了,最近也比较忙,写的相对比较少,抽空分享基于Dapper 的分库分表开源框架core-data的强大功能,更好的提高开发过程中的效率: 在数据库的数据日积月累的积累下,业务 ...

  • 小水獭拼命抵抗不下水,当主人一把将它丢进去后,下一秒憋住别笑

    迎娱乐成 关注 2021-05-04 12:32 水獭是一种水陆两栖生物,因为它长相呆萌,所以现在水獭已经以宠物的身份进入了人类的世界,呆萌的水獭活泼活动,深受人们的喜爱,因为小水獭天性好水,所以宠物 ...

  • 要想种好“大树模式”的无花果,需要拥有整体框架思维?厉害了

    要想种好“大树模式”的无花果,需要拥有整体框架思维?厉害了

  • 【大国关系】美澳英外交政策框架下的“中国崛起” | 国政学人

    作品简介 [作者]David M. McCourt,加利福尼亚大学戴维斯分校社会学系副教授,研究方向为国际关系社会学:美国.英国和欧盟的外交政策:社科哲学. [编译]胡可怡(国政学人编译员,四川大学国 ...

  • 战略规划框架

    这些框架不仅提供了一个工具包,用于分析当前环境和确定备选方案,而且为战略的定义创建了一个完整的过程. 1.战略规划模型 在1970年代,许多大公司采用了正式的自上而下的过程,称为战略规划.在此过程中, ...

  • 战略框架:清单

    由于难以定义什么是战略,这也意味着存在许多不同的战略框架,通常会有不同的期望.公司会共同使用这些方法中的一种或多种,并且在许多情况下,每个公司都已将这些工具调整为适合其内部流程.但是,我们至少可以识别 ...

  • 如何利用IMPACT框架帮你解决业务难题?

    根据<2020年全球人才趋势>报告的数据,四分之三的人才专业人士表示,在未来五年中,人员分析将是其公司的首要任务.尽管如此,许多公司尚未做好准备.超过一半(55%)的受访者承认,他们仍然需 ...

  • 麦肯锡7S 框架

    在组织变革过程中需要使用工具的时候,我们看到了很多文章在写7S框架,大部分都是介绍一下框架内容而已,这里我们一起更加深入的把7S框架如何作用于组织变革,从哪里可以切入,之间的逻辑关系是什么一起进一步剖 ...

  • 如何突破职责分工框架“RACI”?

    RACI可以说是最受欢迎的责任计划工具,RAPID在该框架上进行了优化迭代.本文质疑这些模型的实用性,并说明如何消除复杂性,并介绍一种以批准和交付为中心的更有效的方法. 随着组织的发展,了解需要完成的 ...

  • 人力资源逻辑框架图

    前段时间有很多人问人力资源逻辑框架是什么样子的,刚好有机会系统梳理一下,不废话直奔主题,就是下图. 一.组织架构 组织是承接战略的唯一载体,有效的人力资源管理运作都是建立在健康良性且合理的组织设计基础 ...