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 条评论
登录 后参与评论

相关文章

来自专栏西安-晁州

struts2随笔

1、struts.properties配置常量等同于struts.xml中配置(置于类加载路径下面) struts.multipart.maxSize文件上传最...

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

Java设计模式-模板方式模式

模板方法模式: 定义一个操作中的算法的骨架, 而将一些步骤延迟到子类中. 模板方法使得子类可以在不改变一个算法的结构的前提下重定义该算法的某些特定步骤. ? ...

4478
来自专栏java初学

java异常处理及自定义异常的使用

1017
来自专栏desperate633

设计模式之访问者模式(visitor模式)引入访问者模式visitor模式的实例visitor模式分析

Visitor是访问者的意思。 数据结构中保存着元素。一般我们需要对元素进行处理,那么处理元素的代码放在哪里呢?最显然的方法就是放在数据结构的类中,在类中添加...

673
来自专栏desperate633

Java中的异常处理1使用try,catch异常继承架构该抓还是该抛

这个程序可以让用户输入任意个整数,以0作为结束的标志,最后会显示输入整数的平均值。 下面我们进行简单的测试

582
来自专栏Java3y

纳税服务系统六(信息发布管理模块)【Ueditor、异步信息交互、抽取BaseService、条件查询、分页】

需求分析 我们现在来到了纳税服务系统的信息发布管理模块,首先我们跟着原型图来进行需求分析把: 一些普通的CRUD,值得一做的就是状态之间的切换了。停用和发布切换...

3786
来自专栏Java开发者杂谈

java如何获取一个对象的大小

When---什么时候需要知道对象的内存大小 在内存足够用的情况下我们是不需要考虑java中一个对象所占内存大小的。但当一个系统的内存有限,或者某块程序代码允许...

4006
来自专栏我是攻城师

Java中4大基本加密算法解析

4865
来自专栏Android 研究

Retrofit解析8之核心解析——ServiceMethod及注解1

上篇文章已经介绍了Retrofit里面的大多数类,今天就重点介绍ServiceMethod,本片文章主要内容如下:

674
来自专栏不会写文章的程序员不是好厨师

使用ASM实现简单的AOP

之前一直使用greys及其内部升级二次开发版来排查问题。最近周末刚好事情不多,作为一名程序员本能地想要弄懂这么神奇的greys到底是怎么实现的?周末从githu...

642

扫描关注云+社区