tomcat源码解读三(2) tomcat中JMX的源码分析

     在这里我是将tomcat中的jmx给拆分出来进行单独分析,希望通过此种方式能够尽可能的出现更多的问题,以便对其有更多的了解,首先需要声明的是tomcat的JMX是在jsvase原有的基础上做了一些复用,这就必须了解一些JMX的实现过程

1.1.1 tomcat中JMX的UML图

1.1.2 启动代码解析      注意:本人是在剥离下来的代码上分析的,跟源代码可能有所出入,但不会太大,主要是将它的思想分析一下在这个分析过程中以LifecycleMBeanBase类的register方法为入口分析

1.1.2.1 register方法       这个方法是总共分为三步逻辑如下:           第一步:构建ObjectName           第二步:获取Mbean的注册表           第三步 : 注册当前Mbean组件

代码如下:

protected final ObjectName register(Object obj, String objectNameKeyProperties) {
    //根据domain构造一个对象名 形式一般 domain:type=className 这个最终构成 jmxStudy:type=mainTest
    //StringBuilder name = new StringBuilder(getDomain());
    StringBuilder name = new StringBuilder("jmxStudy");
    name.append(':');
    name.append(objectNameKeyProperties);
    ObjectName on = null;
    try {
        //将上面构建的对象名字符串转化为对应的对象
        on = new ObjectName(name.toString());
        //获取MBeans建模注册表并注册组件
        Registry.getRegistry(null, null).registerComponent(obj, on, null);
    } catch (MalformedObjectNameException e) {
        throw new RuntimeException(e.toString());
    } catch (Exception e) {
        throw new RuntimeException(e.toString());
    }
    return on;
}

     就这样tomcat的JMX是注册成功的,但是既然分析源码,我们肯定要知根问底,下面就看看如何获取Mbean注册表以及注册组件

1.1.2.2 获取Mbean注册表

     主要调用Registry类的静态方法getRegistry

/**
 * tomcat中的JMX传入的两个参数都是null
 * 所以最终返回registry这个静态句柄的值 当然第一次为空是实例化了一个Registry实例
 * */
public static synchronized Registry getRegistry(Object key, Object guard) {
    Registry localRegistry;
    //perLoaderRegistries是一个HashMap集合
    if( perLoaderRegistries!=null ) {
        if( key==null ){
            //获取当前线程加载器
            key=Thread.currentThread().getContextClassLoader();
        }
        //如果key不为空 则从perLoaderRegistries中获取,如果没有的话实例化一个并放入perLoaderRegistries句柄
        if( key != null ) {
            localRegistry = perLoaderRegistries.get(key);
            if( localRegistry == null ) {
                localRegistry=new Registry();
                localRegistry.guard=guard;
                perLoaderRegistries.put( key, localRegistry );
                return localRegistry;
            }
            if( localRegistry.guard != null &&
                    localRegistry.guard != guard ) {
                return null;
            }
            return localRegistry;
        }
    }
    //实例化一个静态的Registry
    if (registry == null) {
        registry = new Registry();
    }
    //这里的逻辑就是guard不为空则必须与传入的相同
    if( registry.guard != null && registry.guard != guard ) {
        return null;
    }
    return (registry);
}

1.1.2.3 注册Mbean组件

     注册Mbean组件即注册当前实例,在验证注册实例不为空之后,根据其全限定类型在mbean管理器中找到相应的ManagedBean实例,如果找不到则创建一个,并在验证ObjectName(如果有则将原有的注册的取消掉)情况下将当前Mbean注册进去

public void registerComponent(Object bean, ObjectName oname, String type)
        throws Exception
{
    //如果要注册的bean为空 则直接返回
    if( bean ==null ) {
        return;
    }
    try {
        //如果类型为空则获取bean的全限定类名
        if( type==null ) {
            type=bean.getClass().getName();
        }
        //mbean的管理器
        ManagedBean managed = findManagedBean(null, bean.getClass(), type);

        //真实的mbean
        DynamicMBean mbean = managed.createMBean(bean);

        //如果当前oname被注册先解除其注册
        if( getMBeanServer().isRegistered( oname )) {
            getMBeanServer().unregisterMBean( oname );
        }
        //传入的mbean==>JMX.MBeanTest  oname==>mainTest1:type=MBeanTest
        getMBeanServer().registerMBean( mbean, oname);
    } catch( Exception ex) {
        ex.printStackTrace();
        throw ex;
    }
}

1.1.2.4 查找Mbean管理器

     根据类型从descriptors和descriptorsByClass这两个HashMap结构中去寻找,优先级descriptors>descriptorsByClass。在没有找到的情况下会进行一下操作:      1. findDescriptor 方法根据bean找到对应描述文件,将实例加载到Registry类的registry句柄中去,然后再进行查找(后文描述),一般这种情况是找的到的      2. 在1中没有找到的情况下,修改ModelerSource再进行查找 依上面顺序找到了就返回,没找到则返回空

public ManagedBean findManagedBean(Object bean, Class<?> beanClass, String type) throws Exception {
    //如果bean不为空 beanClass为空 获取beanClass
    if( bean!=null && beanClass==null ) {
        beanClass=bean.getClass();
    }
    //如果type为空 获取beanClass的name
    if( type==null ) {
        type=beanClass.getName();
    }
    //从descriptors和descriptorsByClass中获取相应的ManagedBean实例 这里首次回去的为空
    ManagedBean managed = findManagedBean(type);
    // 寻找相同包下的描述符
    if( managed==null ) {
        // check package and parent packages
        findDescriptor( beanClass, type );
        managed=findManagedBean(type);
    }
    // 还是没有找到 再根据beanClass来load一遍
    if( managed==null ) {
        // introspection
        load("MbeansDescriptorsIntrospectionSource", beanClass, type);
        managed=findManagedBean(type);
        if( managed==null ) {
            return null;
        }
        managed.setName( type );
        addManagedBean(managed);
    }
    return managed;
}

1.1.2.5 创建最终使用的Mbean      这个过程中最终创建的是BaseModelMBean实例其继承了DynamicMBean接口,并将mbean管理器注入到其句柄

public DynamicMBean createMBean(Object instance) throws InstanceNotFoundException, MBeanException, RuntimeOperationsException {

    BaseModelMBean mbean = null;
    // 如果当前ManagedBean继承了BASE_MBEAN 则实例化一个BaseModelMBean tomcat的默认实现方式就是这种方式
    if(getClassName().equals(BASE_MBEAN)) {
        mbean = new BaseModelMBean();
    } else {
        //跟还有全限定类名实例化mbean
        Class<?> clazz = null;
        Exception ex = null;
        try {
            clazz = Class.forName(getClassName());
        } catch (Exception e) {
        }

        if( clazz==null ) {
            try {
                ClassLoader cl= Thread.currentThread().getContextClassLoader();
                if ( cl != null){
                    clazz= cl.loadClass(getClassName());
                }
            } catch (Exception e) {
                ex=e;
            }
        }

        if( clazz==null) {
            throw new MBeanException
                    (ex, "Cannot load ModelMBean class " + getClassName());
        }
        try {
            // Stupid - this will set the default minfo first....
            mbean = (BaseModelMBean) clazz.newInstance();
        } catch (RuntimeOperationsException e) {
            throw e;
        } catch (Exception e) {
            throw new MBeanException
                    (e, "Cannot instantiate ModelMBean of class " +
                            getClassName());
        }
    }

    //设置当前对象为实例化mbean的managedBean句柄
    mbean.setManagedBean(this);
    try {
        if (instance != null){
            mbean.setManagedResource(instance, "ObjectReference");
        }
    } catch (InstanceNotFoundException e) {
        throw e;
    }

    return mbean;
}

1.1.2.6 registerMBean注册组件      从管理工厂ManagementFactory获取MbeanServer,并通过registerMBean方法将属性和操作注册到Mbean 栈帧如下:

registerComponent(Object, ObjectName, String):127, Registry (JMX), Registry.java

registerMBean(Object, ObjectName):522, JmxMBeanServer (com.sun.jmx.mbeanserver), JmxMBeanServer.java

registerMBean(Object, ObjectName):319, DefaultMBeanServerInterceptor (com.sun.jmx.interceptor), DefaultMBeanServerInterceptor.java

getNewMBeanClassName(Object):333, DefaultMBeanServerInterceptor (com.sun.jmx.interceptor), DefaultMBeanServerInterceptor.java

getMBeanInfo():88, BaseModelMBean (JMX), BaseModelMBean.java

getMBeanInfo():160, ManagedBean (JMX), ManagedBean.java

     通过getMBeanInfo方法会将属性、操作和通知注册到对应实例MBeanAttributeInfo、MBeanOperationInfo以及NotificationInfo然后统一注入到MBeanInfo,最终其会注入到Mbean的管理器从而实现在jconsole等上进行使用

MBeanInfo getMBeanInfo() {
    mBeanInfoLock.readLock().lock();
    try {
        if (info != null) {
            return info;
        }
    } finally {
        mBeanInfoLock.readLock().unlock();
    }
    mBeanInfoLock.writeLock().lock();
    try {
        if (info == null) {
            //创建必要的信息说明
            AttributeInfo attrs[] = getAttributes();
            MBeanAttributeInfo attributes[] =
                    new MBeanAttributeInfo[attrs.length];
            for (int i = 0; i < attrs.length; i++){
                attributes[i] = attrs[i].createAttributeInfo();
            }

            OperationInfo opers[] = getOperations();
            MBeanOperationInfo operations[] =
                    new MBeanOperationInfo[opers.length];
            for (int i = 0; i < opers.length; i++){
                operations[i] = opers[i].createOperationInfo();
            }

            //获取所有的通知对象
            NotificationInfo notifs[] = getNotifications();
            //MBeanNotificationInfo类用于描述由MBean发出的不同通知实例的特征
            MBeanNotificationInfo notifications[] =
                    new MBeanNotificationInfo[notifs.length];
            for (int i = 0; i < notifs.length; i++){
                notifications[i] = notifs[i].createNotificationInfo();
            }


            //创建一个MBeanInfo对象实例 注入相关属性和操作
            info = new MBeanInfo(getClassName(),
                    getDescription(),
                    attributes,
                    new MBeanConstructorInfo[] {},
                    operations,
                    notifications);
        }

        return info;
    } finally {
        mBeanInfoLock.writeLock().unlock();
    }
}

1.1.2.7 加载资源描述      这是一个比较核心的方法,其获取相应的类加载器,找到相应包下的mbeans-descriptors.xml,然后获取模型资源实例,根据字符串MbeansDescriptorsIntrospectionSource的到其实例,注入相应registry,然后在其execute方法中根据createManagedBean 创建ManagedBean,也就是在这里根据对象方法设置属相的的具体操作(如:是否可读,可写),根据initMethods方法将相关属相操作进行区分,下面展示execute和initMethods方法代码 execute代码如下:

public ManagedBean createManagedBean(Registry registry, String domain, Class<?> realClass, String type) {
    ManagedBean mbean= new ManagedBean();
    Method methods[]=null;
    Hashtable<String,Method> attMap = new Hashtable<>();
    // key: attribute val: getter method
    Hashtable<String,Method> getAttMap = new Hashtable<>();
    // key: attribute val: setter method
    Hashtable<String,Method> setAttMap = new Hashtable<>();
    // key: operation val: invoke method
    Hashtable<String,Method> invokeAttMap = new Hashtable<>();
    methods = realClass.getMethods();
    //初始化属性与操作 在这个过程主要将方法加载到对应Hashtable集合 从而分成属性 操作 以及后面在JMX中设置值调用的setAttMap
    initMethods(realClass, methods, attMap, getAttMap, setAttMap, invokeAttMap );

    try {
        //将所有的attMap中的属性添加到ManagedBean的attributes句柄中
        Enumeration<String> en = attMap.keys();
        while( en.hasMoreElements() ) {
            String name = en.nextElement();
            AttributeInfo ai=new AttributeInfo();
            ai.setName( name );
            //根据name从getAttMap获取相关方法 如果不为空 给属性设置这个get方法 如果返回类型不为空 设置相应的返回类型
            Method gm = getAttMap.get(name);
            if( gm!=null ) {
                ai.setGetMethod( gm.getName());
                Class<?> t=gm.getReturnType();
                if( t!=null ){
                    ai.setType(t.getName() );
                }
            }
            //根据name从setAttMap获取相关方法 如果不为空 给属性设置这个set方法 如果返回类型不为空 设置相应的返回类型
            Method sm = setAttMap.get(name);
            if( sm!=null ) {
                Class<?> t = sm.getParameterTypes()[0];
                if( t!=null ){
                    ai.setType( t.getName());
                    ai.setSetMethod( sm.getName());
                }

            }
            ai.setDescription("自省属性" + name);

            //如果gm为空 设置当前属性不可读
            if( gm==null ){
                ai.setReadable(false);
            }
            //如果sm为空 设置当前属性不可写
            if( sm==null ){
                ai.setWriteable(false);
            }
            //主要sm和gm中有一个不为 则像mbean中添加当前属性
            if( sm!=null || gm!=null ){
                mbean.addAttribute(ai);
            }
        }

        //遍历所有invokeAttMap中的方法 这些方法排除的有setter getter方法 静态方法 非public方法 object类中的方法
        for (Map.Entry<String,Method> entry : invokeAttMap.entrySet()) {
            String name = entry.getKey();
            Method m = entry.getValue();
            if(m != null) {
                OperationInfo op=new OperationInfo();
                op.setName(name);
                op.setReturnType(m.getReturnType().getName());
                op.setDescription("自省操作 " + name);
                Class<?> parms[] = m.getParameterTypes();
                for(int i=0; i<parms.length; i++ ) {
                    ParameterInfo pi=new ParameterInfo();
                    pi.setType(parms[i].getName());
                    pi.setName( "参数名" + i);
                    pi.setDescription("参数说明" + i);
                    op.addParameter(pi);
                }
                mbean.addOperation(op);
            } else {
                throw new RuntimeException("Null arg method for [" + name + "]");
            }
        }

        //设置mbean的name
        mbean.setName( type );

        return mbean;
    } catch( Exception ex ) {
        ex.printStackTrace();
        return null;
    }
}

initMethods方法代码:

private void initMethods(Class<?> realClass,
                         Method methods[],
                         Hashtable<String,Method> attMap,
                         Hashtable<String,Method> getAttMap,
                         Hashtable<String,Method> setAttMap,
                         Hashtable<String,Method> invokeAttMap)
{
    for (int j = 0; j < methods.length; ++j) {
        String name=methods[j].getName();

        //如果是一个静态方法则跳过
        if( Modifier.isStatic(methods[j].getModifiers())){
            continue;
        }
        //不是public方法 跳过
        if( ! Modifier.isPublic( methods[j].getModifiers() ) ) {
            continue;
        }
        //获取该方法所在的类这是因为Object类中的方法都不需要注册到Mbean
        if( methods[j].getDeclaringClass() == Object.class ){
            continue;
        }
        Class<?> params[] = methods[j].getParameterTypes();
        //如果方法以get开始并且参数个数为0,其返回类型是支持的返回类型 则获取其添加到attMap和getAttMap
        if( name.startsWith( "get" ) && params.length==0) {
            Class<?> ret = methods[j].getReturnType();
            if(!supportedType(ret) ) {
                continue;
            }
            name=unCapitalize( name.substring(3));
            getAttMap.put( name, methods[j] );
            attMap.put( name, methods[j] );
        } else if( name.startsWith( "is" ) && params.length==0) {
            //如果方法是is开头 则如果其返回类型为Boolean 则获取其添加到attMap和getAttMap
            Class<?> ret = methods[j].getReturnType();
            if( Boolean.TYPE != ret  ) {
                continue;
            }
            name=unCapitalize( name.substring(2));
            getAttMap.put( name, methods[j] );
            // just a marker, we don't use the value
            attMap.put( name, methods[j] );

        } else if( name.startsWith( "set" ) && params.length==1) {
            //如果方法是set开头 则如果其返回类型为Boolean 则获取其添加到attMap和setAttMap
            if( ! supportedType( params[0] ) ) {
                continue;
            }
            name=unCapitalize( name.substring(3));
            setAttMap.put( name, methods[j] );
            attMap.put( name, methods[j] );
        } else {
            //如果参数长度为0,根据方法名从specialMethods中获取,如果不为空则直接返回 反之将其添加到invokeAttMap
            //默认去掉preDeregister postDeregister
            if( params.length == 0 ) {
                if( specialMethods.get( methods[j].getName() ) != null ){
                    continue;
                }
                invokeAttMap.put( name, methods[j]);
            } else {
                //如果参数长度不为空 满足所有参数类型是支持类型将其添加到invokeAttMap中
                boolean supported=true;
                for( int i=0; i<params.length; i++ ) {
                    if( ! supportedType( params[i])) {
                        supported=false;
                        break;
                    }
                }
                if( supported ){
                    invokeAttMap.put( name, methods[j]);
                }
            }
        }
    }
}

1.1.3 调用代码解析      在这例结合jconsole的Mbean对tomcat代码中的设置属性值、获取属性值、调用方法、发送通知四种方法进行分析。为减少篇幅在这里只是展示入口方法,核心调用的方法都标红

1.1.3.1 设置属性值      设置属性值是BaseModelMBean中setAttribute方法作为入口根据方法名获取相关属性,根据Mbean实例来获取相应的方法,并进行调用

@Override
public void setAttribute(Attribute attribute) throws AttributeNotFoundException, MBeanException, ReflectionException
{
    //如果是动态Mbean并且不是BaseModelMBean 将属性直接设置到资源
    if( (resource instanceof DynamicMBean) &&
            ! ( resource instanceof BaseModelMBean )) {
        try {
            ((DynamicMBean)resource).setAttribute(attribute);
        } catch (InvalidAttributeValueException e) {
            throw new MBeanException(e);
        }
        return;
    }
    // 验证输入参数
    if (attribute == null){
        throw new RuntimeOperationsException(new IllegalArgumentException("Attribute is null"), "Attribute is null");
    }

    String name = attribute.getName();
    Object value = attribute.getValue();
    if (name == null){
        throw new RuntimeOperationsException(new IllegalArgumentException("Attribute name is null"), "Attribute name is null");
    }

    Object oldValue=null;

    //根据name获取指定的方法并调用相应的方法
    Method m=managedBean.getSetter(name,this,resource);

    try {
        //检查这个方法所在的类是否与当前实例类相同或是当前实例的超类或接口 如果是调用当前实例的方法 反之调用资源类的方法
        if( m.getDeclaringClass().isAssignableFrom( this.getClass()) ) {
            m.invoke(this, new Object[] { value });
        } else {
            m.invoke(resource, new Object[] { value });
        }
    } catch (InvocationTargetException e) {
      。。。。
    }
}

1.1.3.2 获取属性值      获取属性入口 BaseModelMBean—》getAttribute      获取属性是点击到管理界面具体属性的时候进行显示然后会调用到当前方法

public Object getAttribute(String name) throws AttributeNotFoundException, MBeanException, ReflectionException {
    //如果name为空扔出异常
    if (name == null){
        throw new RuntimeOperationsException(new IllegalArgumentException("Attribute name is null"), "Attribute name is null");
    }
    //如果实例是继承DynamicMBean并且不是BaseModelMBean则调用其自己获取属性的方式
    //这种情况在tomcat比较常见 如ConnectorMBean它利用自己的setter/getter属性 resource是注册的实例
    if( (resource instanceof DynamicMBean) && ! ( resource instanceof BaseModelMBean )) {
        return ((DynamicMBean)resource).getAttribute(name);
    }

    //这个方法的功能是根据name获取的相关属性,再根据属性实例找到方法名,利用反射获取这个方法
    Method m=managedBean.getGetter(name, this, resource);

    Object result = null;
    try {
        //获取这个方法所在的类 可能是当前类也有可能是其父类
        Class<?> declaring = m.getDeclaringClass();
        //如果条件为真,declaring是其父类 这直接通过当前实例调用 这样完全java继承方法的实现思想
        //这种情况出现于Mbean实例继承BaseModelMBean
        if( declaring.isAssignableFrom(this.getClass()) ) {
            result = m.invoke(this, NO_ARGS_PARAM );
        } else {
            //利用Mbean实例直接调用方法 这种情况是常见的
            result = m.invoke(resource, NO_ARGS_PARAM );
        }
    } catch (InvocationTargetException e) {
     。。。。。。

    return (result);
}

1.1.3.3 调用方法      调用入口: BaseModelMBean—》invoke      点击相应操作则会调用

@Override
public Object invoke(String name, Object[] params, String[] signature) throws MBeanException, ReflectionException {
    if( (resource instanceof DynamicMBean) &&
            ! ( resource instanceof BaseModelMBean )) {
        return ((DynamicMBean)resource).invoke(name, params, signature);
    }


    if (name == null){
        throw new RuntimeOperationsException(new IllegalArgumentException("Method name is null"), "Method name is null");
    }

    //根据参数和签名获取相应的方法
    Method method= managedBean.getInvoke(name, params, signature, this, resource);

    Object result = null;
    try {
        if( method.getDeclaringClass().isAssignableFrom( this.getClass()) ) {
            result = method.invoke(this, params );
        } else {
            result = method.invoke(resource, params);
        }
    } catch (InvocationTargetException e) {
     。。。。。。
    return (result);

}

1.1.3.4 发送通知      发送通知需要从两方面进行考虑,第一方面是客户端进行连接要将相应的监听器加入另一方面是在调用相应事件则通过相应方法发送给注入的监听器,这样就实现了相应的消息通知

     接受接听器: BaseModelMBean –》addNotificationListener

public void addNotificationListener(NotificationListener listener, NotificationFilter filter, Object handback) throws IllegalArgumentException {
    //如果监听器为空 则扔出异常不合法参数
    if (listener == null){
        throw new IllegalArgumentException("Listener is null");
    }

    //广播实例句柄为空则实例化一个BaseNotificationBroadcaster实例
    if (generalBroadcaster == null){
        generalBroadcaster = new BaseNotificationBroadcaster();
    }

    generalBroadcaster.addNotificationListener(listener, filter, handback);

    if (attributeBroadcaster == null){
        attributeBroadcaster = new BaseNotificationBroadcaster();
    }

    attributeBroadcaster.addNotificationListener(listener, filter, handback);

}

     发送消息: BaseModelMBean –-> sendAttributeChangeNotification

@Override
public void sendAttributeChangeNotification(AttributeChangeNotification notification) throws MBeanException, RuntimeOperationsException {
    //这个通知是在做了修改操作之后构建的一个操作 如果为空 则必然扔出异常
    if (notification == null){
        throw new RuntimeOperationsException(new IllegalArgumentException("Notification is null"), "Notification is null");
    }
    //如果广播为空则意味着没有监听器 其是在连接的时候实例化了一个BaseNotificationBroadcaster
    if (attributeBroadcaster == null){
        //意味着没有注册监听器
        return;
    }
    attributeBroadcaster.sendNotification(notification);
}

     由上可值Mbean得动态操作都是在BaseModelMBean这个类中,JMX的分析到这里告一段落 要想更清除的理解则需要再次到tomcat这个环境以及从底层rmi实现方面进行了解,后期会补上这些内容

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏阮一峰的网络日志

读懂 ECMAScript 规格

一、概述 规格文件(specification)是计算机语言的官方标准,详细描述语法规则和实现方法。 ? 一般来说,没有必要阅读规格,除非你要写编译器。因为规格...

2574
来自专栏技术与生活

Lambda 深入浅出

非常简单,那么问题来了,如果这个时候需求变动,要求选择的是红色的并且重量大于10的,那么怎么办。小 case,不就一行代码的事

802
来自专栏一个会写诗的程序员的博客

《Kotin 编程思想·实战》

1.2 程序执行的三种方式 1.2.1 编译执行 1.2.2 解释执行 1.2.3 虚拟机执行

651
来自专栏程序员宝库

JavaScript 深拷贝性能分析

作者:justjavac 链接:https://segmentfault.com/a/1190000013107871 如何在 JavaScript 中拷贝一个...

37413
来自专栏Java帮帮-微信公众号-技术文章全总结

【选择题】Java基础测试九(16道)

【选择题】Java基础测试九(16道) 117.下列说法正确的有() A. class中的constructor不可省略 B. constructo...

3437
来自专栏漫漫全栈路

ASP.NET MVC 行为详解

前面分别介绍了MVC中的三个重要部分,而行为,则是其中C-Controller中的重要内容,下面详解一二。 一般继承自Controller类,类Controll...

2684
来自专栏攻城狮的动态

iOS面试题梳理(二)

33310
来自专栏java架构师

Unit断言学习

[TestMethod]—用于把一个方法标记为一个测试方法。当你运行你的测试时,仅标记有这个属性的方法才能够运行。 [TestClass]—用于把一个类标记为...

26311
来自专栏黄Java的地盘

should.js源码分析与学习

为了研究与学习某些测试框架的工作原理,同时也为了完成培训中实现一个简单的测试框架的原因,我对should.js的代码进行了学习与分析,现在与大家来进行交流下。

611
来自专栏贺贺的前端工程师之路

正则表达式-学习2 - 语法语法学习重点详解

Character classes match a character from a specific set. There are a number of p...

863

扫码关注云+社区