性能文章>框架虽好,但不要丢了其背后的原理。​>

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

2年前
4693427

近期团队中同学遇到几个问题,想在这儿跟大家分享一波,虽说不是很有难度,但是背后也折射出一些问题,值得思考。

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

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

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

 

踩刀诗人

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

微信扫描二维码,关注我的公众号

 

点赞收藏
分类:标签:
踩刀诗人

聊聊技术,唠唠段子,偶尔做菜写诗,欢迎关注我的公众号 踩刀诗人

请先登录,查看4条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

一文讲透消息队列RocketMQ实现消费幂等

一文讲透消息队列RocketMQ实现消费幂等

一次 Rancher go 应用内存占用过高问题排查

一次 Rancher go 应用内存占用过高问题排查

27
4