Eclipse Maven依赖缺失问题解决

这段时间一直很忙,项目还有生活上的事,期间也看了一些书,主要是网络连接方面的,对TCP处理以及整个网络的架构有了一定的了解。但是简单的总结感觉意义不大,后面如果遇到问题再具体分析写下总结。此次写一个自己近期遇到的小问题,大体如下:

上周我们组的同事反映海外收付款工程编译报错,很多maven依赖的jar包在eclipse里面没有了,包括最常用的commons-lang3包,还有很多其他包,缺失的包也没什么明显特征。这个问题比较突然,一直以来都没问题,其他境内的工程也没问题。我们仔细想了一下,近期也没改什么东西,除了把这个海外收付款开发流重新复制一个之外(不过问题应该和这个无关)。

我尝试运行了mvn dependency:tree显示能看到commons-lang3包,其他包也有。那这个依赖关系和正常工程比对了一下,有个别公共基础包版本不一致,但缺失的包版本都是一致的。

为了进一步缩小问题的排查点,我决定将出错工程的parent pom文件改成正常工程的样子,这时发现还是报错,而且还出现了新的错误,有一个包由于版本不对,导致编译错误。看来问题不在这,我又仔细看了maven的build日志,发现个问题,日志中download的公共包的pom文件版本不太对,有些是3.3-hw版,有些是3.3.11-hw版本(maven有一个警告,版本号中包含表达式,不是常量)。看来很可能是版本的问题。

又进一步进行了排查,此海外工程的版本为3.3.11且parent pom文件版本也是3.3.11;不一致的3.3版本是因为海外工程显示声明依赖一个vo包,这个包的版本是3.3,且这个vo包的parent pom文件的版本也是3.3,但这个parent pom文件和海外工程的parent pom名称相同。也就是说海外工程同时依赖了两个不同的parent pom文件,所以日志中才会出现两个不同的版本号。具体如下图所示:

异常依赖示意图

问了下配置管理员,是因为当时图省事,没有对vo重新打包新版本。这个肯定有问题,我们把vo包重新打了新版本,海外出错工程的pom文件也修改正确。然后发现eclipse中maven缺失的依赖回来了,问题解决了 ^_^

看来这个是由于maven版本依赖冲突导致的问题,后来有研究了一些maven的资料1 2。发现其实maven有一套自己的版本冲突解决策略,所以在执行mvn dependency:tree时可以看到基本正确的依赖树,一般程序也没问题。但版本冲突总是有害的,那怎么才能显示出所有的冲突的依赖呢?

可以执行下面的命令:mvn dependency:tree -Dverbose,增加了这个选项,maven会显示出完整的依赖包,且会标出哪些是重复的依赖(omitted for duplicate),哪些是冲突的(omitted for conflict)。对于冲突的可以手工解决,例如使用exclude选项。

其实我感觉此次依赖缺失的问题更像是eclipse 或是maven插件的bug,不能正确处理依赖冲突。大家如果后面遇到这种问题也可以看看是不是maven版本依赖冲突导致的。

参考资料:

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《ITechLib》

Java agent简单介绍

近期计划做一个自动化回归测试的工具,大体实现的功能如下:

  1. 从测试环境抓取相关的日志、输入输出报文;
  2. 根据日志或报文来构造这个测试场景需要的数据,可以采用从数据库中抓取数据或从日志中解析数据;
  3. 根据输入输出报文来配置调用外部服务的挡板,挡板会负责校验输入项,并返回相应的输出;
  4. 将上述信息装配后,生成对应的测试案例。这个测试案例相当于我们应用自身的组件组装测试,可以用于回归我们自身的代码功能。

可以看出上面的测试主要为了还原测试环境的测试场景,通过使数据库、输入输出报文和测试环境完全一致来达到回归的目的。当然具体实现上还是有很多细节可以考虑的。目前考虑到的一个重要的问题就是时间的可重复性,很多测试案例都是依赖于特定的时间的,节假日、营业时间、夏令时等等,而每个测试案例自身对时间的需求又各异。这个问题是必须要解决的,其中一个方案是修改现有的代码,改为使用可测试性的时间类,例如springsideorg.springside.modules.utils.time.ClockUtil类;另一个方案是本文要介绍的,使用java agent的方式对应用的字节码进行修改,使其中的时间具有可测试性,这种方案的好处是无需修改应用代码,做到应用透明。

具体java agent的详细使用可以参考1 2,这种技术在很多优秀工具中都有使用,比如JmockitJaCoCoJRebel等场景下都有使用。

具体实现代码包括如下几部分: ## 1.代理类的main类

//file-name:FtmAgent.java
package com.zyh.ftmagent;

import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;

public class FtmAgent {
    public static void premain(String agentArgs, Instrumentation inst)
            throws ClassNotFoundException, UnmodifiableClassException{
            inst.addTransformer(new TimeTransformer());
        }
}

2.字节码转换类

这里的处理分为两部分,一个是找到需要加工的类的方法;另一个是对这些方法进行修改,将其中的调用一般方法和new形式的字节码进行修改。字节码修改部分采用Javassist3来编写,它相对于其他工具如ASM更加易用,相关教程可以参考Javassist Tutorial

//file-name:TimeTransformer.java
package com.zyh.ftmagent;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtBehavior;
import javassist.CtClass;
import javassist.NotFoundException;
import javassist.expr.ExprEditor;
import javassist.expr.MethodCall;
import javassist.expr.NewExpr;

public class TimeTransformer implements ClassFileTransformer {

    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
            ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        // 仅对于com.zyh.test包下的类进行替换
        if (className.startsWith("com/zyh/test")) {
            byte[] transformed = null;
            System.out.println("Transforming " + className);
            ClassPool pool = ClassPool.getDefault();
            CtClass cl = null;
            try {
                cl = pool.makeClass(new java.io.ByteArrayInputStream(classfileBuffer));
                if (cl.isInterface() == false) {
                    CtBehavior[] methods = cl.getDeclaredBehaviors();
                    for (int i = 0; i < methods.length; i++) {
                        if (methods[i].isEmpty() == false) {
                            doMethod(methods[i]);
                        }
                    }
                    transformed = cl.toBytecode();
                }
            } catch (Exception e) {
                System.err.println("Could not instrument    " + className + ",    exception : " + e.getMessage());
            } finally {
                if (cl != null) {
                    cl.detach();
                }
            }
            return transformed;
        }
        return null;
    }

    private void doMethod(CtBehavior method) throws NotFoundException, CannotCompileException {
        // 修改方法体的定义
        method.instrument(new ExprEditor() {

            // 修改一般的方法调用语句
            public void edit(MethodCall m) throws CannotCompileException {
                try {
                    if (m.getClassName().equals("java.lang.System")) {
                        if (m.getMethodName().equals("nanoTime")) {
                            m.replace("{long rslt = com.zyh.ftmagent.MySystemTime#nanoTime(); $_ = ($r)rslt; }");
                        } else if (m.getMethodName().equals("currentTimeMillis")) {
                            m.replace(
                                    "{long rslt = com.zyh.ftmagent.MySystemTime#currentTimeMillis(); $_ = ($r)rslt; }");
                        }
                    }
                } catch (Exception ex) {
                    ex.printStackTrace();
                    throw new RuntimeException(ex);
                }
            }

            // 修改new Date()这种格式的代码,替换其中的class类
            public void edit(NewExpr e) throws CannotCompileException {
                try {
                    if (e.getClassName().equals("java.util.Date")) {
                        e.replace("{$_ = new com.zyh.ftmagent.MyDate();}");
                    }
                } catch (Exception ex) {
                    ex.printStackTrace();
                    throw new RuntimeException(ex);
                }
            }
        });
    }
}

3.自定义的日期类

包括两个类,具体如下:

  • MyDate:对默认Date类进行了扩展,实现根据MysystemTime获取的当前时间进行构造Date类。
  • MySystemTime: 实现了currentTimeMillisnanoTime方法,会根据最初设置的当前时间间隔进行设置。为了保证线程安全,这里使用了ThreadLocal类,可以在程序中的公共部分调用setTimeDiff方法对时间进行重新定义。
//file-name: MyDate.java
package com.zyh.ftmagent;

import java.util.Date;

public class MyDate extends Date {
    private static final long serialVersionUID = 1L;

    public MyDate(){
        super(MySystemTime.currentTimeMillis());
    }
}

//file-name:MySystemTime.java
package com.zyh.ftmagent;

public class MySystemTime {
    //默认为当前时间
    private static final Long INIT_VALUE = 0l;

    private static ThreadLocal<Long> timeDiff = new ThreadLocal<Long>() {
        @Override
        protected Long initialValue() {
            return INIT_VALUE;
        }
    };

    public static long currentTimeMillis() {
        return System.currentTimeMillis() - timeDiff.get();
    }

    public static long nanoTime() {
        return System.nanoTime() - timeDiff.get() * 1000;
    }

    /**
     * 设置距离当前的时间,为正表示后退多少ms
     * 
     * @param diff
     */
    public static void setTimeDiff(long diff) {
        timeDiff.set(diff);
    }
}

4.MANIFEST.MF文件

其中的Premain-ClassFtmAgent的主类,在代码编译好后需要把上述的所有代码打成jar包(例如包名为ftm-agent.jar),以备使用。

Manifest-Version: 1.0
Premain-Class: com.zyh.ftmagent.FtmAgent
Boot-Class-Path: javassist-3.22.0-CR1.jar

测试及其他

测试类需要在com.zyh.test包下,测试前需要执行MySystemTime.setTimeDiff来将时钟进行回拨。如在eclipse中测试需要在vm的参数中增加 -javaagent=ftm-agent.jar,然后执行测试的main函数即可。完整的代码见FtmAgent-Gist

参考资料:

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《ITechLib》

iBatis锁机制导致的交易阻塞

前段时间生产上出现了数据库锁太多的告警,其中等待的锁短时间内达到了6000个,好在十分钟后锁很快解除,否则可能会有数据库宕机的风险。此问题的具体表现如下:

  1. 其中一台应用服务器在凌晨2点钟出现大量交易变慢,导致weblogic线程超时阻塞,进而导致服务重启1
  2. 其他服务器在2点钟某一个账务交易出现大量长交易,但其他账务交易、查询交易均正常;
  3. 数据库某个限额统计表在2点多出现大量的行锁,触发数据库告警。
  4. 数据库在2点10分恢复正常,行锁也跟着消失

根据数据库锁的信息,结合那段时间的交易流水,可以确定是由于某个大客户的同一帐号的转账导致的。进一步梳理了代码,没有发现死锁的问题;另外这个客户日间交易量更大,理论上不应该出现这种问题。

进一步分析出问题的应用服务器怀疑是由于iBatis的锁导致的(原来生产上也出现过由于IBatis导致的锁定问题),但在什么情况下会出现长时间的行锁呢?目前iBatis有三个相关参数:maxRequests、maxSessions、maxTransactions,目前生产上分别设置的是100,60, 30。但根据iBatis的文档看maxRequests的值建议是maxTransactions的是10倍。为了验证是否是由于此参数的问题,我们在非功能环境进行了测试。

1. maxSessions=90,数据库连接数60,只做查询:

为了看出问题我们去掉了索引使查询交易耗时边长。此测试发现当查询并发小于等于60时没问题,但大于60时会出现连接不够的错误。

2. maxSessions=90,数据库连接数100,只做查询:

发现查询并发数增大(超过数据库连接)并不会导致交易失败,但查询时长会边长。这说明ibatis自身有锁机制来进行等待。

3. maxSessions=90,数据库连接数100,同时做付款与查询:

转账交易10个并发,在转账交易稳定后开始发送查询交易,查询交易85个并发。发现在启动查询交易一段时间后转账交易开始出现超时和失败,但查询交易无失败和超时,但交易时间有增加。

转账交易10个并发,在转账交易稳定后开始发送查询交易,查询交易75个并发。发现在启动查询交易一段时间后转账交易正常无失败和超时,查询交易亦无失败和超时。

这个测试我们为了看出对账务交易的影响对账务交易代码进行了修改,伪码如下:

Enquire agreementInfo and other info; (no transaction)
Start transaction #spring逻辑
	Get session  #ibatis处理逻辑
		Get Request #ibatis处理逻辑
			Do update account limit
		Return Request #ibatis处理逻辑
	Return session #ibatis处理逻辑
	Sleep 1s #增加的代码,为了便于看出效果
	Get session #ibatis处理逻辑;在这里可能出现拿不到session而阻塞,但这时其实线程是有数据库连接的,因为transaction未断开,不应该出现阻塞
	    Get Request #ibatis处理逻辑
			Insert account transaction information  #预计账务流水
		Return Request #ibatis处理逻辑
	Return session #ibatis处理逻辑
End Transaction #spring逻辑

我们可以看出由于iBatis在每次数据库操作开始都会请求session,而在每次数据库执行完成都会释放session。如果拿不到锁就会阻塞等待。这在仅仅是查询时没问题,但是在更新等事务中就会有问题,会导致数据库行锁,而且会影响到这条记录相关的所有交易。

后来我们仔细阅读了iBatis的代码,包括spring对于iBatis处理封装的逻辑,发现其实:maxRequests、maxSessions、maxTransactions这三个参数只有maxSession是有用的,因为maxRequest都是在session中进行获取,所以其取值不会比session大,而transaction是spring负责管理的,所以此参数也无效。这三个参数在iBatis的2.3.1版本中已经去掉了,可能也是因为这个问题。

基于上述分析我们决定使用数据库连接作为限制条件(通过使maxSession值大于数据库连接数),放弃使用session来进行控制。这个事也让我明白了在事务中尽量不要使用锁(特别是无等待时间的锁),不管是显示的还是隐式的(框架的)。关于jdbc的超时设置可以见参考资料2 3 4

参考资料:

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《ITechLib》

Java测试那点事

最近公司在整顿大家的单元测试,会定期出coverage报告。我个人还是挺支持这事的,一方面能培养大家的开发习惯,也能提升代码质量(如果不考核就更好了,哈哈)。现在公司使用的是Spring框架,大家现在所说的单元测试其实更应该算是集成测试,因为很多人会测试交易报文的正确性、整个交易的执行正确性、数据库操作的正确性。

在测试过程中发现很多我以前没在意的问题,也让我对代码测试有了更深的理解。下面简要介绍一下:

1. Spring Bean中使用static变量:

使用Spring的初衷是使用它的IOC特性,能够装配出想要的bean,这些类只需要是普通的POJO对象就可以了,但是有些人却使用static类型来保存数据,更可气的是不支持多次加载(会判断如果static变量中有值时就报错),这样就把Spring的优点消失殆尽了。其实这样只是为了可以直接使用静态函数访问这个类。

这样做的缺点是显而易见的,在连续执行多个测试案例(suite)时,如果Spring需要重新加载(见我上一篇文章),则会报错,因为不支持多次加载。解决方法目前想到的只能使用ant通过fork出一个新的jvm的方式来进行测试。或者使用Powermock工具通过单独的classloader加载,但这样会导致Spring context不能复用、找不到某些类如log4j等、代码覆盖度报告无显示等很多其他问题。

2. 使用final、static等不可测试的方法:

普通的Mock方式一般是针对接口,或是通过继承来修改一个类的方法,如果遇到这些方法,想要进行单元测试,一般的Mock工具都不好使。但如果为了测试而修改代码风险又很大,好在现在发现一个好用的工具Jmockit,它可以解决上述不可测代码的问题。它是通过java.lang.instrument包结合ASM字节码工具来实现的,具体可以参考此博文。我原来是使用Powermock来做的,它是通过自己的Classloader在类加载时进行修改来实现的,相比之下Jmockit要更直接一些,功能也更加强大。

[注]: Jmockit官网的教程是最新的,如果要看历史版本的文档可以在这里下载,里面包括源码和文档。

3.测试覆盖度报告工具使用

目前有很多测试覆盖度工具,包括coberturajacocoJmockit code coverage。其中eclispe的代码覆盖度工具EclEmma使用的就是jacoco。每个代码覆盖度工具提供的覆盖度报告是不同的,包括行覆盖度、分支覆盖度等等,具体大家可以参考它们的文档。下面说下使用不同工具应该关注的点:

  1. 是否支持maven、ant工具,是否可以支持多个工程报告合并
  2. 你使用的mock方法和这些代码覆盖度工具是否兼容
  3. 能提供的报告内容是否满足你的需要,你是否认可它的指标
  4. 生成覆盖度报告的样式你是否满意

测试覆盖度工具、Mock工具建议大家了解一下它们的原理,可以在网上搜以下。这样大家在使用时可以更得心应手。

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《ITechLib》

Spring Test的一些小技巧

目前项目在使用Spring Test进行单元测试(其实算是集成测试了),有两个小知识想和大家分享下。

1.对于同时存在注解、xml配置的bean会以xml中的配置为准

例如spring配置文件如下:

    <!-- application.xml -->
    <context:component-scan base-package="com.itechlib.com.service" />
    
    <!-- 在测试目录下定义stub类 -->
    <bean id="userDao" class="com.itechlib.com.test.stub.UserDaoStubServiceImpl" />

Java代码如下:

@component("userService")
class UserServiceImpl implements IUserService {

@Resource(name="userDao")
private IUserDao userDao;

  public void doUpdate(){
    //...
    userDao.update();
    //...
  }
}

这样在测试时UserServiceImpl调用的就是stub包下定义的userDao的模拟类了。

2.同时运行多个测试类的时候,可以有不同的ContextConfiguration配置

例如使用Junit TestSuite来一次执行多个测试类,这时如果不同测试类的ContextConfiguration配置不一样会怎么样呢?其实这些测试类都能够正确运行,因为spring会针对不同的configuration创建不同的ApplicationContext,而且这些ApplicationContext都会进行缓存,只会加载一次。

下面这些key不同就会生成不同的ApplicationContext,具体见spring reference 文档 Context caching

  • locations (from @ContextConfiguration)
  • classes (from @ContextConfiguration)
  • contextInitializerClasses (from @ContextConfiguration)
  • contextLoader (from @ContextConfiguration)
  • activeProfiles (from @ActiveProfiles)
  • resourceBasePath (from @WebAppConfiguration)

如果测试中破坏了ApplicationContext,可以对这个测试类声明@DirtiesContext

例如下面的写法会生成两个独立的ApplicationContext。

@ContextConfiguration(loader = SpringBeanTestContextLoader.class, locations = {
    "classpath:test-resources/application/application.xml"})
class UserServiceTests extends AbstractJunit4SpringContextTests{
    @Test
    public void testUpdate(){
    //...    
    }
}

@ContextConfiguration(loader = SpringBeanTestContextLoader.class, locations = {
    "classpath:test-resources/application/application-custom.xml"})
class OrderServiceTests extends AbstractJunit4SpringContextTests{
    @Test
    public void testAdd(){
    //...    
    }
}

了解这些你可以更好的规划你的测试配置,更好地测试各种场景和分支。

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《ITechLib》