使用javassist编写一个简单的agentClassTransformer

2017/11/09 15:56:32 No Comments

前段时间找到一个很好的工具Greys-anatomy, 相应的参考地址为参考地址:https://github.com/oldmanpushcart/greys-anatomy. 为此,专门研究了一下基工作原理.并简单研究了一下基于Instrumentation进行运行时代码调整的一些实现手法. 本文, 简单介绍一下如何使用javassist来简单对一个代码作编织, 实现简单的一些监控指标手段.

附: 另外,前几年淘宝也简单作了一个通过启动时agent达到运行方法监控的目的, 称之为TProfiler,对研究一些实现手法很有用.

一个主要的ClassFileTransformer定义如下:

    byte[]
    transform(  ClassLoader         loader,
                String              className,
                Class<?>            classBeingRedefined,
                ProtectionDomain    protectionDomain,
                byte[]              classfileBuffer)
        throws IllegalClassFormatException;

传递到当前实现的主要有用的信息即为className, classfileBuffer两个数据. className即为当前正准备加载的类, 而classfileBuffer即传递给当前处理的字节码, 需要做的即是将这个字节码进行处理,然后返回一个已经处理过的字节码,即完成instrument操作.

(more…)

一种批量插入数据saveIgnore并返回主键的方法

2017/05/25 08:52:18 No Comments

在mysql中,提供了save ignore语法,用于在插入数据如果出现冲突时忽略信息的处理方式.在这种情况下,一般主键id是通过自动生成,ignore通过一些惟一索引进行控制.在程序中的期望即如果惟一索引不冲突就插入新的数据,如果冲突则不再插入. 但都希望能够在处理成功之后拿到这些数据的主键(不管是之前的还是新插入的),以便于后续进行处理.相应的简单业务逻辑如下所示.

        List<T> personList = xxxList;
        mysql.saveIgnore(personList);
        
        //进行后续逻辑,如转账
        personList.forEach(t-> {
            mysql.addMoney(t.getId(), 100);
        });

在标准的jdbc中,如果是save,或者是mysql的save values(value1) (value2) 这种语法,是能够通过 statement.getGeneratedKeys()返回自动生成的主键.但对于save ignore无效,mysql并不是返回由于冲突处理的之前的主键信息.

常规的作法就是在save ignore之后,再通过相应的惟一索引来进行查询. 类似如下的sql方式

select id from t where t.name in(name1,name2);

这种方式,对于如果惟一索引仅有一列是没有问题的.但如果是多列组成的,则生成如下的sql

select id from t where (t.name,t.code) in ((name1,code1),(name2,code2));

理论上,这种方式也没有问题,但是在mysql中,这种语法并不能命中相应的索引,会造成全局扫描(验证版本5.5,5.6)
修改in版本为如下sql

select id from t where (t.name = name1 and t.code = code1) or (t.name = name2 and t.code = code2);

这种方式可以命中索引,但是整个sql会变得很长,可以看出,相应的惟一索引列会出现多次.如果是5000个数据处理(批量处理肯定数据会很多才有意义),则整个sql会很长.

本文的方式是通过临时表,提前插入索引数据,再通过表关联来获取相应的数据.这样可以避免上面sql过长的问题,并且利用mysql内存表快速处理数据.整个原理可以理解为以下几个步骤

  1. 创建临时表,表数据仅为惟一索引要求列,并建立相应的惟一索引
  2. 插入待处理数据中指定属性到临时表当中
  3. 使用save ignore语法插入数据到实际表中
  4. 两表关联,使用临时表关联实际表,指定相应的惟一列进行关联条件,查询出主键信息
  5. 程序中将主键处理到相应的对象中

(more…)

结合javassist进行通用性代码扩展修改的思路

2017/04/10 22:29:05 No Comments

最近产品中有一种想法,即在保证源项目不动的基础之上,提供一种通用的扩展原产品逻辑的方法.对于这种情况,有很多方法可以做,比如直接上aspectj,或者是spring aop,也可以是单个javassist修改.但对于实施方扩展源码,还是从相应的几个问题入手.

1. 修改的实现手段是否有难度,比如aspectj的切面语法
2. 相应的应用技术是否对项目运行时有性能影响,比如aspectj中*号匹配模式
3. 过多的修改类是否容易进行管理和处理,比如过多的切面是否好处理

考虑到以上的思路,想做一种简单的修改方式,即将相应的场景固定下来.比如,修改参数信息的,修改结果,以及替换实现等几个固定场景.并且,这些场景都是针对单一的修改场景,并不是一种范围匹配方式的.因为根据实际经验,进行实施时,往往是修改特定的场景中的特定的数据信息. 至于常规的类似日志记录,信息拦截等,还是通过切面来统一实现的好.

以下以一种修改参数信息的场景进行举例:
相应的实际效果,如下:

//源类
public class T8 {
    public static void abc(int a, int b) {
        System.out.println("->" + a + "," + b);
    }
}

//修改类
public class T81 {
    @Param(clazz = "t2.T8", method = "abc", params = {"int", "int"}, order = 2)
    public Map<Integer, Object> xchg1(int a, int b) {
        System.out.println("xchg1->" + a + "," + b);
        return ImmutableMap.of(1, a + 2, 2, b + 3);
    }
}

以上的代码,基于以下的工作方式.

1 获取相应的原始调用参数信息
2 将相应的参数传递给相应的修改处理器
3 如果有多个处理器,即链式调用
4 处理完之后,将参数信息重新传递回原来的方法调用

整个工作方式,基于javassist.因为它提供一种代码式的插入方式,相比asm,在应用层面更加方便.相应的文档参考地址如: http://jboss-javassist.github.io/javassist/tutorial/tutorial.html
因为是,处理参数信息,即在整个方法调用前插入相应的修改代码,使用的即是相应的ctmethod.insertBefore(code) 处理.

(more…)

排查redisson中订阅connection无故消失的问题

2016/12/13 20:52:41 No Comments

最近在项目中使用了redis结合spring cache一起作了一个缓存,并使用了订阅功能来达到进程间的数据同步。但在测试使用过程中,发现第二天一来,本来应该工作的订阅同步并不能进行。当时没在意,简单重启了事。但后来发现,每天早上相应的同步都不能进行,并且经测试。每个进程的同步都不进行,感觉是redis的订阅出问题了。

1. 验证问题

表现出来就是A程序修改了数据,B程序并没有识别到这次更新。那么就先尝试手动在redis中进行相应的set操作,因为订阅的是redis的空间事件,即key space event. 在控制台单独连接redis,执行相应的set 命令,在B程序中并没有任何表示。
怀疑是不是redis的发布订阅出问题了,就另开终端,手动地进行订阅,一切正常。
初步确定是程序出问题了。

2. 查看程序

把进程的stack打印一份,查看里面的线程信息,发现并没有redisson的线程信息。一般来说,没收到消息也可能是线程内阻塞了,但是直接就没有相应的线程,这就有问题了。
怀疑是不是相应的连接被断开之后就没有再连,也可能是redis server被重启了。

重新连接redis,使用status查看相应的状态,显示server端并没有重启过,其运行时间长达X天,即没有间断过。

回过来再看相应的redisson程序,里面有一个watch dog,是负责连接断开重连的。即如果连接被断掉了,它会尝试重连,但每一次的重连都会迟后一定时间,如 1 2 4 8 秒这样。这也是为什么要看stack的原因。简单看了下watchdog的实现,表示并没有明显的问题(或者就没问题)。

进一步怀疑是不是出现了某个异常,导致相应的watchdog重连直接被中断了。比如Error级错误。将相应的日志拉下来一份,因为一晚上都没人操作,日志信息本身也很少。直接在里面使用grep Exception查找日志信息,但一切也很正常,甚至没有异常发生。

再重新查看了redisson的源码,里面使用netty的channelInActive来进行重连尝试。在本地测试了一下这个机制,redis重启或者断开连接,它都能检测到。开始怀疑是不是日志信息不全,被吞掉了。重新调整日志级别,将redisson和netty的级别调到TRACE级别,结果中午过去一会,再回来看。相应的连接又不见了,订阅又不能正常工作。查看了日志,里面也没有任何信息,因为这期间无任何操作。

(more…)

使用guava Striped中的lock导致线程死锁的问题分析

2016/11/19 15:04:00 No Comments

在最近的开发调试当中,一个多线程的项目出现了线程死锁的问题。按照正常的解决思路,上监控工具yourkit或者自己打印出线程信息,最终的显示信息均为lock对象先拿到了自己的锁,然后去拿对方的锁,现状是这样的。但与一些正常的项目相比,按照正常的执行情况,是不可能出现死锁的。

项目使用了程序锁来保证并发情况下,一些操作只能由单个来执行,如一些不允许多次运行的场景。所谓程序锁,即通过由锁池获取相应的lock对象,然后进行lock操作,在相应的方法体内进行相应的控制。
项目中使用基于guava的striped来获取不同的锁,本来原来的代码是自己封装实现,但考虑到guava已经提供,因此直接使用了,相应的伪代码如下所示:

        Striped<Lock> lockStriped = Striped.lazyWeakLock(1024);

        Lock lock = lockStriped.get("动态生成的临界资源标识符");
        lock.lock();
        try{
            //执行业务操作
        } finally {
            lock.unlock();
        }

以上的代码因为足够的通用,因此被封装为一个LockAdvice,以实现通用的锁方法操作,只需要在需要加锁的方法上加上Lockable注解,就可以达到所要的效果。出现死锁的2个线程表现如下:

//线程1
加锁获取资源类型1
加锁获取资源类型2

//线程2
加锁获取资源类型1
加锁获取类型类型2

从上面的线程操作来看,加锁的顺序是一样的,释放锁的顺序也一致,因此理论上不会出现死锁。为保证锁切面的简单实现,上面的锁资源类型1 被表现为一个字符串,即通过不同的字符串来进行描述。也就是说,在上面的现象中,会有4个字符串进行锁的key进行操作。

问题场景被发现了,并且这个问题肯定会产生,多线程运行一段时间之后就会发生。查找问题产生的原因却很漫长。不知道为什么,使用jstack却没有打印出每个线程在哪一个环节持有哪个锁(是不是本身就没有),通过jstack的信息表示,相应的线程肯定持有对方的锁,而对方也在等级已方的锁,并且相应的锁的发生点也确定是在相应的锁切面准备执行的时候。剩下的问题就是看是否是相应的锁生成器striped本身就返回了相同的锁,如果由锁生成器本身就返回相同的锁,那么就肯定有问题了。

(more…)

gc过程中reference对象的处理

2016/09/18 14:50:03 No Comments

之前从java的角度描述了Reference对象的处理,因为整个处理过程并不是完全由java这边来处理,另一个方面JVM在整个gc过程中,还需要负责来维护整个reference对象以及正确地往相应的pending对象里面进行处理。因此这里从jvm的角度来描述相应的reference对象是如何处理的。

引用对象结构

数据结构定义在referenceProcessor.hpp。定义了以下4种类型的结构。

_discoveredSoftRefs
_discoveredWeakRefs
_discoveredFinalRefs
_discoveredPhantomRefs

每一个结构相对应的数据结构为 DiscoveredList,可以理解为与具体的Reference相同的结构,类似一个处理链表,里面的每一个节点都对应着java中的reference对象。这里仍然采用头指针+length的结构来持有所有需要处理的reference对象。在这里面存放的对象都表示在相应的处理过程中还没有被放入java Reference中pending结构的对象。

从总体上的处理逻辑来看,可以理解为。在整个gc过程中,首先在jvm内部维护一套需要被放到pending中的引用链,然后处理这些引用链,处理完之后将相应的数据重新附到pending中,清除jvm内部数据。这样达到一个reference的处理过程。

(more…)