Fork me on GitHub

[Enhancement]Support to select provider from zookeeper, Dubbo Plugin for Apache JMeter - V1.3.7

项目地址

github: jmeter-plugin-dubbo

码云: jmeter-plugin-dubbo

V1.3.7

What is new:

  1. Support to select provider from zookeeper. issue: #31
  2. Upgrade dubbo version to v2.6.4.

新版改进:

  1. 支持从zookeeper选择服务提供者,降低手动输入出错概率,issue: #31
  2. 升级dubbo版本到v2.6.4

截图

Trouble Shooting —— jms:listener-container配置queue的concurrency数量与预期不一致

Published on:
Tags: activemq

问题描述

测试程序时发现queue的consumer数量配置与预期不一致,具体如何不一致看下面的测试。

现象一

当我们使用下面配置,listener使用同一个task-executor并且监听三个queue时,consumer使用20-20,只会有一个queue能达到20个consumer,其余两个queue的consumer=0

<bean id="queueMessageExecutor1" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
   <property name="corePoolSize" value="20" />
   <property name="maxPoolSize" value="20" />
   <property name="daemon" value="true" />
   <property name="keepAliveSeconds" value="120" />
</bean>
<jms:listener-container task-executor="queueMessageExecutor1" destination-type="queue" container-type="default" connection-factory="pooledConnectionFactory"
                  concurrency="20-20" acknowledge="auto" receive-timeout="60000">
   <jms:listener destination="QUEUE.EMAIL" ref="mailMessageListener" />
   <jms:listener destination="QUEUE.SMS" ref="smsMessageListener" />
   <jms:listener destination="QUEUE.WECHAT" ref="wechatMessageListener" />
</jms:listener-container>

效果如下图:

现象二

当我们去掉listener-container的receive-timeout="60000"的配置,三个queue的consumer都等于20。

<bean id="queueMessageExecutor1" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
   <property name="corePoolSize" value="20" />
   <property name="maxPoolSize" value="20" />
   <property name="daemon" value="true" />
   <property name="keepAliveSeconds" value="120" />
</bean>
<jms:listener-container task-executor="queueMessageExecutor1" destination-type="queue" container-type="default" connection-factory="pooledConnectionFactory"
                  concurrency="20-20" acknowledge="auto">
   <jms:listener destination="QUEUE.EMAIL" ref="mailMessageListener" />
   <jms:listener destination="QUEUE.SMS" ref="smsMessageListener" />
   <jms:listener destination="QUEUE.WECHAT" ref="wechatMessageListener" />
</jms:listener-container>

效果如下图:

这两种现象之间的差异就在receive-timeout="60000"这个属性上,接下来让我们看一下“现象一”、“现象二”的jvm启动的consumer线程的具体信息,如下图:

现象一的线程信息:

现象二的线程信息:

从线程的信息上看,线程的数量与线程池的配置信息吻合,具体开多少个线程取决于线程池的大小,这个与预期一致,拿为什么两种现象锁显示的queue的consumer数量不同呢?

同样是20个线程,但是在现象二中三个queue的consumer分别都是20个,那总数就是60个完全超过了线程的数量,从这点能看的出来consumer的数量是逻辑数量,也就是说20个线程来承接60个逻辑消费者,每个线程会随机的去拿某一个queue里的消息。

测试消费者

当我们在“现象一”中只有一个queue有consumer,其他queue没有consumer,我们往没有consumer的q中写消息,看些消息的这个q是否有会consumer出现?

当消息积压到一定的时间(测试下来时间为:14:18分积压消息,14:27分增加了20个consumer消费掉了积压消息)

我们再往wechat中发送积压消息,看看wechat的consumer是否会增加?

当消息积压到一定的时间(测试下来时间为:14:34分积压消息,14:38分增加了20个consumer消费掉了积压消息)

一旦增加上来了consumer目前看下来不会自动消失

测试后结论

当listener-container使用同一个task-executor并且监听多个q时:

  • listener-container设置了receive-timeout="60000(接受超时时间),线程数会优先处理配置中第一个q上,其他q不会有consumer数量,当其他q有消息积压时会自动增加consumer数量,但是增加的时间不太规律。
  • listener-container没有设置receive-timeout="60000(接受超时时间),线程数会处理多个q的消息接收,随机接收某个q的消息,或者是那个q的消息积压的多会优先接受那个q的消息。

ps. 同一个listener-container监听多个q,线程会接收多个q的消息(多个q共享接收消息线程),只不过q的consumer数量初始化的时间不同,如果不配置receive-timeout="60000(接受超时时间)这个参数,q的consumer数量在启动时就会初始化。

当listener-container使用不同的task-executor并且只监听一个q时:

  • 设不设置receive-timeout="60000(接受超时时间)没有区别,一个线程池中的线程只会处理一个q的消息接收,对于消息量大存在积压的情况下,可以独立配置线程池和监听器让这个q的处理线程资源独享。

Trouble Shooting —— 莫名其妙的java.lang.NoClassDefFoundError: org.springframework.beans.FatalBeanException异常

Published on:
Tags: mybatis

问题描述

最近运维在部署应用的时候偶尔会碰到下面的异常:

Exception in thread "main" java.lang.NoClassDefFoundError: org.springframework.beans.FatalBeanException
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:547)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:475)
    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:304)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:228)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:300)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:195)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:700)
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:760)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:482)
    at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:139)
    at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:93)
    at com.alibaba.dubbo.container.spring.SpringContainer.start(SpringContainer.java:50)
    at com.alibaba.dubbo.container.Main.main(Main.java:80)

这个异常看上去是org.springframework.beans.FatalBeanException在运行时找不到class,但是调试起来很懵逼。

问题分析

尝试一

怀疑这个类org.springframework.beans.FatalBeanExceptionclassloader的时候无法找到。

这个类org.springframework.beans.FatalBeanExceptionspring-beans包下,查看打包的lib下存在spring-beans包,查看运行jar中的META-INF下的MANIFEST.MF文件中也有lib/spring-beans-4.0.0.RELEASE.jar

因此排除了这个怀疑。

ps.这里要区分一下NoClassDefFoundErrorClassNotFoundException异常看这篇文章

尝试二

这个类在spring-beans包中,那是不是这个jar包损坏无法读取?

查看了jar包信息以及打开与解压也排除了jar包损坏的可能性。

尝试三

修改log级别改为debug看会不会有更多的日志输出。

通过日志级别的调整为debug后,除了都了一些debug的常规日志以外,错误相关的日志还是跟上面的输出一样,因此也是无济于事。

尝试四

通过arthas观察org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory这个类的doCreateBean这个方法异常的输出。

arthas ${pid}
 
watch org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory doCreateBean "{params, throwExp}" -e -x 2

发现如下更多的日志:

ts=2018-09-25 18:06:37;result=@ArrayList[
    @Object[][
        @String[xxxMapper],
        @RootBeanDefinition[Root bean: class [org.mybatis.spring.mapper.MapperFa
ctoryBean]; scope=singleton; abstract=false; lazyInit=false; autowireMode=2; dep
endencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; fac
toryMethodName=null; initMethodName=null; destroyMethodName=null; defined in URL
 [jar:file:/E:/user/desktop/ningyu/Desktop/xxx-main-1.0.0-SNAPSHOT-201809251509/
lib/xxx-service-JD-1.0.0-SNAPSHOT.jar!/com/xxx/xxx/order/mapper/xxxMapper.class]],
        null,
    ],
    java.lang.NoClassDefFoundError: org.springframework.beans.FatalBeanException
        at org.springframework.beans.factory.support.AbstractAutowireCapableBean
Factory.doCreateBean(AbstractAutowireCapableBeanFactory.java:547)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBean
Factory.createBean(AbstractAutowireCapableBeanFactory.java:475)
        at org.springframework.beans.factory.support.AbstractBeanFactory$1.getOb
ject(AbstractBeanFactory.java:304)
        at org.springframework.beans.factory.support.DefaultSingletonBeanRegistr
y.getSingleton(DefaultSingletonBeanRegistry.java:228)
        at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBe
an(AbstractBeanFactory.java:300)
        at org.springframework.beans.factory.support.AbstractBeanFactory.getType
ForFactoryBean(AbstractBeanFactory.java:1420)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBean
Factory.getTypeForFactoryBean(AbstractAutowireCapableBeanFactory.java:788)
        at org.springframework.beans.factory.support.AbstractBeanFactory.isTypeM
atch(AbstractBeanFactory.java:543)
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.
doGetBeanNamesForType(DefaultListableBeanFactory.java:384)
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.
getBeanNamesForType(DefaultListableBeanFactory.java:361)
        at org.springframework.beans.factory.BeanFactoryUtils.beanNamesForTypeIn
cludingAncestors(BeanFactoryUtils.java:187)
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.
findAutowireCandidates(DefaultListableBeanFactory.java:999)
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.
doResolveDependency(DefaultListableBeanFactory.java:957)
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.
resolveDependency(DefaultListableBeanFactory.java:855)
        at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanP
ostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.j
ava:480)
        at org.springframework.beans.factory.annotation.InjectionMetadata.inject
(InjectionMetadata.java:87)
        at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanP
ostProcessor.postProcessPropertyValues(AutowiredAnnotationBeanPostProcessor.java
:289)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBean
Factory.populateBean(AbstractAutowireCapableBeanFactory.java:1185)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBean
Factory.doCreateBean(AbstractAutowireCapableBeanFactory.java:537)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBean
Factory.createBean(AbstractAutowireCapableBeanFactory.java:475)
        at org.springframework.beans.factory.support.AbstractBeanFactory$1.getOb
ject(AbstractBeanFactory.java:304)
        at org.springframework.beans.factory.support.DefaultSingletonBeanRegistr
y.getSingleton(DefaultSingletonBeanRegistry.java:228)
        at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBe
an(AbstractBeanFactory.java:300)
        at org.springframework.beans.factory.support.AbstractBeanFactory.getBean
(AbstractBeanFactory.java:195)
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.
preInstantiateSingletons(DefaultListableBeanFactory.java:700)
        at org.springframework.context.support.AbstractApplicationContext.finish
BeanFactoryInitialization(AbstractApplicationContext.java:760)
        at org.springframework.context.support.AbstractApplicationContext.refres
h(AbstractApplicationContext.java:482)
        at org.springframework.context.support.ClassPathXmlApplicationContext.<i
nit>(ClassPathXmlApplicationContext.java:139)
        at org.springframework.context.support.ClassPathXmlApplicationContext.<i
nit>(ClassPathXmlApplicationContext.java:93)
        at com.alibaba.dubbo.container.spring.SpringContainer.start(SpringContai
ner.java:50)
        at com.alibaba.dubbo.container.Main.main(Main.java:80)
,
]

而且这个信息不停的打,并且看到的全是xxxMapper

难道是mybatismapper代理类的创建出现了问题?

尝试本地通过代码的方式启动服务,没有任何问题。

又尝试本地通过打出的zip包,通过java -jar的方式启动,也没有任何问题。

这个时候就很头疼了,定位不到问题,而且问题不能重现。

网上能搜索到关于mybatis启动报Stack overflow的错误,难道我们这个问题跟他也有关系?于是尝试看一下mybatismapper代理自动创建的相关资料。

通过这篇文章

MapperFactoryBean实例生成之后,Spring给它注入SqlSessionTemplate。而注入SqlSessionTemplate的过程中会向容器获取所有的Dao,对于已经在容器中的Dao所对应的bean可以直接获取返回,若还没有创建bean,则Spring又会先创建这个DaoMapperFactoryBean。创建MapperFactoryBean的时候会再次注入SqlSessionTemplate。就这样一直循环下去,直到所有的Dao都已经创建完毕,这个过程才算结束。

看来跟mybatis的关系应该很大,网上有有说mybatis Mapper有导致过stack overflow的错误,新想如果是stack overflow肯定应该是有明确的异常抛出,于是也是抱着尝试调整一下jvm的参数看看是否有惊喜。

-Xms1024m -Xmx1024m -XX:PermSize=256m -XX:MaxPermSize=256m -Xss256k

stack overflow应该调整Xss参数大小(-Xss512k)调整后重启,竟然成功了!竟然成功了!竟然成功了!

太不可思议了难道是stack overflow异常被吃掉了?而且mapper在创建的时候是递归,递归的层次越深越消耗stack大小,然后具体搜索mybatis导致stack异常的信息看到了这篇文章,上面就是说mybatis-spring工具包有问题将异常吃掉了,具体mybatis-spring中的那段代码我还在定位,定位好了在更新文章

解决方法

调整xss参数,从xss256k调整为xss512k

zookeeper数据迁移及恢复

Published on:
Tags: zookeeper

在做环境迁移的时候经常会遇到中间件的数据迁移,今天我们说一下zookeeper的数据如何迁移与恢复。

比如说我们使用prd环境数据迁移到st环境为例来叙述一下具体的步骤。

第一步:从prd环境zookeeper服务器的数据目录下复制最新的日志和快照文件。

先去zookeeper的安装目录下找到zookeeper的conf文件,例如:

$> cd /usr/local/zookeeper/conf
$> cat zoo.cfg

打开zoo.cfg文件找到具体配置的zookeeper的data目录,例如:

# the directory where the snapshot is stored.
# do not use /tmp for storage, /tmp here is just
# example sakes.
dataDir=/var/zookeeper

进入到dataDir下的version-2文件夹,version-2文件夹下存放的是zookeeper的日志和镜像文件,我们找到最新的日志和镜像文件,例如:

$> cd /var/zookeeper/version-2
$> ls -ah
-rw-r--r-- 1 zookeeper zookeeper 67108880 Sep 27 17:20 log.909e2d252
-rw-r--r-- 1 zookeeper zookeeper 10408329 Sep 27 17:01 snapshot.909e2d250

找到最新的日志和快照文件,例如上面的:log.909e2d252和snapshot.909e2d250

日志文件存放zookeeper全部数据记录 ,快照文件则是内存增量文件。

ps.这里要注意找最新的日志和快照文件

zookeeper的日志和镜像文件的清理可以看这篇文章:Zookeeper事务日志和snapshot清理方式

第二步:传输日志和快照文件

如果st和prd网络是通的话可以通过scp的方式复制过去,如果网络不通通过中转站来过渡。

第三步:停掉需要恢复数据的zk服务,删除数据目录下的文件,复制刚才的两个文件到数据目录下

假设需要恢复数据的服务器上zookeeper数据目录也是在/var/zookeeper下

$> rm -fr /var/zookeeper/*
$> cp log.909e2d252 snapshot.909e2d250 /var/zookeeper
$> cd /usr/local/zookeeper/bin
$> ./zkServer.sh start

如果是三台需要全部服务停掉,恢复其中的一台,然后等数据恢复完成后,再启动其余的两台服务让zk自己同步数据过去

第四步:验证数据是否真的恢复了

$> cd /usr/local/zookeeper/bin
$> ./zkCli.sh
$> ls /

ls查看zk中的数据.

Zookeeper日志与镜像文件的分析可以参考这篇文章:ZooKeeper日志与快照文件简单分析

Git常用开发流程 —— 中心化的工作流

Published on:

一、中心化的工作流

中心化的工作流又叫做SVN-style,适用于熟悉svn的管理流程来过渡到git(分布式版本控制系统),如果你的开发团队成员已经很熟悉svn,集中式工作流让你无需去适应一个全新流程就可以体验Git带来的收益。这个工作流也可以作为向更Git风格工作流迁移的友好过渡,入下图所示:

像SVN一样,集中式工作流以中央仓库作为项目所有修改的单点实体。相比SVN缺省的开发分支trunk,Git叫做master,所有修改提交到这个分支上。该工作流只用到master这一个分支。 开发者开始先克隆中央仓库。在自己的项目拷贝中,像SVN一样的编辑文件和提交修改;但修改是存在本地的,和中央仓库是完全隔离的。开发者可以把和上游的同步延后到一个方便时间点。 要发布修改到正式项目中时,开发者要把本地master分支的修改『推(push)』到中央仓库中。这相当于svn commit操作,但push操作会把所有还不在中央仓库的本地提交都推上去,下图所示:

为什么会有冲突,冲突的原因

使用svn-style的方式避免不了会遇到冲突,冲突的解决尤为重要,中央仓库代表了正式项目(git里是master,svn里是trunk),所以提交历史应该被尊重且是稳定不变的。如果开发者本地的提交历史和中央仓库有分歧,Git会拒绝push提交否则会覆盖已经在中央库的正式提交。

在开发者提交自己功能修改到中央库前,需要先fetch在中央库的新增提交,rebase自己提交到中央库提交历史之上。这样做的意思是在说,我要把自己的修改加到别人已经完成的修改上,最终的结果是一个完美的线性历史,就像以前的SVN的工作流中一样。如果本地修改和上游提交有冲突,Git会暂停rebase过程,给你手动解决冲突的机会。

举例说明

让我们举一个例子来理解一下中心化工作流-svn-style

比如说:wms项目组有两个开发人员:小明、小健,看他们是如何开发自己的功能并提交到中央仓库上的。

第一步:小明、小健从中央仓库克隆代码

git clone http://gitlab.xxx.com/demo/gitflow-demo.git

ps.克隆仓库时Git会自动添加远程别名origin指向中央仓库,不动请参考:git clone --help

克隆代码入下图示例:


小明开发新功能:

小明使用标准的Git过程开发功能:编辑、暂存(Stage)和提交,这里注意不进行push操作,只做本地commit提交到本地仓库

git status # 查看本地仓库的修改状态
git add # 暂存文件
git commit # 提交文件

这些操作都是本地的提交操作,小明可以反复的按照需求来进行代码修改,不需要担心中央仓库的改变和小健自己本地的改变。

小明开发功能都在本地上进行就如下图示例:


小健开发新功能

小健也是一样使用标准的Git过程开发功能,编辑、暂存、提交,他和小明一样不需要关系中央仓库的改变和小明自己本地的改变,所有的提交都是私有的,都是在自己的本地仓库中。

小健开发功能都在本地上进行就如下图所示:


小明开发好了功能想发布了

小明把他的本地仓库修改的代码push到中央仓库,使用下面命令

git push origin master

ps. origin是在小明克隆仓库时Git创建的远程中央仓库别名。master参数告诉Git推送的分支

ps. 我们这里假设团队中只有两个人(小明、小健),由于中央仓库自从小明克隆以来还没有被更新过,所以push操作不会有冲突,成功完成。

小明把他自己的本地代码push到中央仓库就如下图所示:


小健开发好了功能也想发布了

小健也是使用git push命令来推送自己本地仓库的改动到中央仓库,使用下面命令

git push origin master

但是此时origin已经由于小明在之前推送了小明本地的代码上去,因此已经和小健本地的代码产生了分歧,推送会被拒绝,入下图所示:

拒绝的信息如下:

$ git push origin master
To http://gitlab.xxx.com/demo/gitflow-demo.git
 ! [rejected]        master -> master (fetch first)
error: failed to push some refs to 'http://gitlab.xxx.com/demo/gitflow-demo.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

遇到这种问题我们该怎么解决了?

小健可以使用pull操作合并上游的修改到自己的仓库中,git的pull操作类似于svn的update操作,拉取所有上游小明提交命令到小健的本地仓库,但是要加上–rebase参数,例如下面命令:

git pull --rebase origin master

这里特别解释一下上面命令的实际操作原理

–rebase选项告诉git把小健的提交移到(同步了中央仓库修改后的master分支)的顶部(head),也就是说它会先把小健本地分支的本地提交先移除掉,移动到一旁,然后把小健的本地分支同步中央仓库的最新版本(小明提交的记录),然后把刚刚移除了(小健本地的修改)再提交回小健的本地分支(已同步了最新中央仓库的代码,也就是说小明的代码)。

不加rebase的话git会在xiaoming和xiaojian的提交后再进行一次merge操作从而就会多了一个merge的提交记录,加了rebase的话xiaojian的提交已经包含了与xiaoming代码的冲突,因此不会多一个merge操作。

rebase(没有冲突)操作的过程例如下图所示:

rebase(存在冲突)操作的过程例如下图所示:

ps. git会暂定rebase操作直到你去解决了冲突之后执行git rebase --continue来继续进行操作.

rebase实操记录

下面是rebase的操作实践,xiaojian执行git pull --rebase origin master,比如说xiaoming和xiaojian冲突到了同一个文件上会显示出下面的信息,例如:

$ git pull --rebase origin master
remote: Counting objects: 3, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From http://gitlab.xxx.com/demo/gitflow-demo
 * branch            master     -> FETCH_HEAD
   788c7f3..9c7c9d2  master     -> origin/master
First, rewinding head to replay your work on top of it...
Applying: xiaojian 提交
error: Failed to merge in the changes.
Using index info to reconstruct a base tree...
M       README.md
Falling back to patching base and 3-way merge...
Auto-merging README.md
CONFLICT (content): Merge conflict in README.md
Patch failed at 0001 xiaojian 提交
The copy of the patch that failed is found in: .git/rebase-apply/patch
Resolve all conflicts manually, mark them as resolved with
"git add/rm <conflicted_files>", then run "git rebase --continue".
You can instead skip this commit: run "git rebase --skip".
To abort and get back to the state before "git rebase", run "git rebase --abort".

如果装了小乌龟或者sourcetree目录下文件会显示冲突警告图标

根据上面的警告我们需要手动的解决冲突,解决完冲突使用git add/rm <conflicted_files>命令标记解决冲突完毕,再执行git rebase --continue继续下一步操作。

ps. 如果这个时候后悔执行了git pull --rebase origin master想撤销怎么办?可以执行git rebase --abort撤销rebase操作。

接下来是xiaojian执行push到中央仓库并且解决冲突的脚本记录如下:

手动解决冲突
$ git add README.md
$ git rebase --continue
Applying: xiaojian 提交
$ git push origin master
Counting objects: 3, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 305 bytes | 305.00 KiB/s, done.
Total 3 (delta 1), reused 0 (delta 0)
To http://gitlab.xxx.com/demo/gitflow-demo.git
   9c7c9d2..87aed2d  master -> master

然后我们去gitlab上看我们的提交记录是个什么样子的,例如下图:

ps.提交记录非常的清晰明了而且是按照push仓库的顺序来显示的提交记录,这个样子也是我们希望看到的。

但是往往并没有这么顺利,理想很好现实确实各种问题。

比如说git pull的时候忘记添加--rebase参数了怎么办?

如果忘加了--rebase这个选项,pull操作仍然可以完成,但每次pull操作在同步中央仓库中别人的修改时,需要提交合并代码的记录从而导致提交历史中会多一个『合并提交』的记录。

例如下面所示:

$ git pull origin master
From http://gitlab.xxx.com/demo/gitflow-demo
 * branch            master     -> FETCH_HEAD
Auto-merging README.md
CONFLICT (content): Merge conflict in README.md
Automatic merge failed; fix conflicts and then commit the result.
手动解决完冲突
$ git add .
$ git commit -m "合并冲突"
[master 9fab0c8] 合并冲突
$ git push origin master
Counting objects: 6, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (4/4), done.
Writing objects: 100% (6/6), 581 bytes | 290.00 KiB/s, done.
Total 6 (delta 2), reused 0 (delta 0)
To http://gitlab.xxx.com/demo/gitflow-demo.git
   f862f27..9fab0c8  master -> master

然后我们去gitlab上看我们的提交记录是个什么样子的,例如下图:

ps. 会多出一个合并的提交,而且查看tree型图会发现不是一个线性的轨迹

如果使用的是小乌龟sourcetree这种工具合并冲突会是什么样子?让我们演示一下:

首先

手动解决完冲突,选择标记为已经解决

我们需要把解决的冲突提交上去

自动生成的comment是Merge branchmasterxxxxxxx

提交完成后右键菜单选择git push

这个时候我们去gitlab上看我们的提交记录是个什么样子的,例如下图:

示例总结以及注意事项

所以我们建议对于集中式工作流,最好是使用rebase,而不是使用merge生成一个合并提交

如何编写高性能的 RPC 框架

Published on:

最近看相关rpc-benchmark相关的东西发现这篇文章挺好的,所以转载出来,下面是文章出处。

作者:鲁小憨 链接:https://www.jianshu.com/p/7182b8751e75 來源:简书 简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

RPC Benchmark Round 1 中,Turbo 性能炸裂表现强悍,并且在 listUser 这一项目中,取得了 10x dubbo 性能的好成绩。本文将介绍 Turbo 强悍性能背后的原理,并探讨如何编写高性能的 RPC 框架。

过早的优化是万恶之源?

这句话是 The Art of Computer Programming 作者,图领奖得主 Donald Knuth 大神说的。不过对于框架设计者而言,这句话并不正确。在设计一款高性能的基础框架时,必须始终重视性能优化,并将性能测试贯穿于整个设计开发过程中。这方面做到极致的类库有 Disruptor JCTools Agrona DSL-JSON 等等,这几个高性能类库都坚持一个原则:不了解性能的外部类库坚决不用,如果现有的类库不能满足性能要求,那就重新设计一个。作为 Turbo 的设计者,我也尽量坚持这一原则,努力做到 Benchmark 驱动开发。

JMH 让 Benchmark 驱动开发成为可能

在 JMH 出现之前,要对某个类库进行微基准性能测试是一件非常困难的事情。很难保证公平的测试条件,预热次数难以确定,预热效果也不好观察。JMH 的出现让性能测试变得 标准化 简单化,也让 Benchmark 驱动开发成为可能。Turbo 在开发过程中用 JMH 进行了充分的 Benchmark,以确定核心环节的性能开销,选择合适的实现方案。更多关于 JMH 的介绍请参考下面的链接:

RPC 的主要流程

  1. 客户端 获取到 UserService 接口的 Refer: userServiceRefer
  2. 客户端 调用 userServiceRefer.verifyUser(email, pwd)
  3. 客户端 获取到 请求方法 和 请求数据
  4. 客户端 把 请求方法 和 请求数据 序列化为 传输数据
  5. 进行网络传输
  6. 服务端 获取到 传输数据
  7. 服务端 反序列化获取到 请求方法 和 请求数据
  8. 服务端 获取到 UserService 的 Invoker: userServiceInvoker
  9. 服务端 userServiceInvoker 调用 userServiceImpl.verifyUser(email, pwd) 获取到 响应结果
  10. 服务端 把 响应结果 序列化为 传输数据
  11. 进行网络传输
  12. 客户端 接收到 传输数据
  13. 客户端 反序列化获取到 响应结果
  14. 客户端 userServiceRefer.verifyUser(email, pwd) 返回 响应结果

整个流程中对性能影响比较大的环节有:序列化[4, 7, 10, 13],方法调用[2, 3, 8, 9, 14],网络传输[5, 6, 11, 12]。本文后续内容将着重介绍这3个部分。

序列化方案

Java 世界最常用的几款高性能序列化方案有 Kryo Protostuff FST Jackson Fastjson。只需要进行一次 Benchmark,然后从这5种序列化方案中选出性能最高的那个就行了。DSL-JSON 使用起来过于繁琐,不在考虑之列。Colfer Protocol Thrift 因为必须预先定义描述文件,使用起来太麻烦,所以不在考虑之列。至于 Java 自带的序列化方案,早就因为性能问题被大家所抛弃,所以也不考虑。下面的表格列出了在考虑之列的5种序列化方案的性能。

  • User 序列化+反序列化 性能
framework thrpt (ops/ms) size
protostuff 1654 240
kryo 1288 296
fst 1101 263
jackson 959 385
fastjson 603 378
  • 包含15个 UserPage 序列化+反序列化 性能
framework thrpt (ops/ms) size
kryo 143 2080
fst 118 3495
protostuff 98 3920
jackson 71 5711
fastjson 40 5606

从这个 benchmark 中可以得出明确的结论:二进制协议的 protostuff kryo fst 要比文本协议的 jackson fastjson 有明显优势;文本协议中,jackson(开启了afterburner) 要比 fastjson 有明显的优势。

无法确定的是:3个二进制协议到底哪个更好一些,毕竟 速度 和 size 对于 RPC 都很重要。直观上 kryo 或许是最佳选择,而且 kryo 也广受各大型系统的青睐。不过最终还是决定把这3个类库都留作备选,通过集成传输模块后的 Benchmark 来决定选用哪个。

framework exist op/ms create op/ms get op/ms list op/ms
proto 103.92 89.50 83.33 21.17
kryo 99.23 76.71 73.89 25.68
fst 102.33 76.24 78.81 23.30

最终的结果也还是各有千秋难以抉择,所以 Turbo 保留了 protostuff 和 kryo 的实现,并允许用户自行替换为自己的实现。

方法调用

可用的 动态方法调用 方案有:Reflection ClassGeneration MethodHandle。Reflection 是最古老的技术,据说性能不佳。ClassGeneration 动态类生成,从原理上说应该是跟直接调用一样的性能。MethodHandle 是从 Java 7 开始出现的技术,据说能达到跟直接调用一样的性能。实际结果如下:

type thrpt (ops/us)
direct 1062
javassist 920
methodHandle 430
reflection 337

结论非常明显:使用类生成技术的 javassist 跟直接调用几乎一样的性能,就用 javassist 了。

MethodHandle 表现并没有宣传的那么好,怎么回事?原来 MethodHandle 只有在明确知道调用 参数数量 参数类型 的情况下才能调用高性能的 invokeExact(Object… args),所以它并不适合作为动态调用的方案。

As is usual with virtual methods, source-level calls to invokeExact and invoke compile to an invokevirtual instruction. More unusually, the compiler must record the actual argument types, and may not perform method invocation conversions on the arguments. Instead, it must push them on the stack according to their own unconverted types. The method handle object itself is pushed on the stack before the arguments. The compiler then calls the method handle with a symbolic type descriptor which describes the argument and return types.
refer: https://docs.oracle.com/javase/7/docs/api/java/lang/invoke/MethodHandle.html

网络传输

Netty 已经成为事实上的标准,所有主流的项目现在使用的都是 NettyMina Grizzly 已经失去市场,所以也就不用考虑了。还好也不至于这么无聊,Aeron 的闪亮登场让 Netty 多了一个有力的竞争对手。Aeron 是一个可靠高效的 UDP 单播 UDP 多播和 IPC 消息传递工具。性能是消息传递中的关键。Aeron 的设计旨在达到 高吞吐量 低开销 和 低延迟。实际效果到底如何呢?很遗憾,在 RPC Benchmark Round 1 中的表现一般。跟他们开发团队沟通后,最终确认其无法对超过 64k 的消息进行 zero-copy 处理,我觉得这可能是 Aeron 表现不佳的一个原因。Aeron 或许更适合 微小消息 极端低延迟 的场景,而不适用于更加通用的 RPC 场景。所以暂时还没有出现能够跟 Netty 一争高下的通用网络传输框架,现阶段 Netty 依然是 RPC 系统的最佳选择。

  • existUser 判断某个 email 是否存在

|framework| thrpt (ops/ms) |avgt (ms)| p90 (ms)| p99 (ms)| |–|–|–|–|–| |turbo-rpc| 107.05| 0.28| 0.40| 0.87| |netty| 99.81| 0.32| 0.40| 0.52| |jupiter| 73.07| 0.44| 0.66| 1.49| |undertow| 70.38| 0.45| 1.16| 2.17| |turbo-rest| 68.49| 0.44| 1.17| 2.15| |undertow-async| 62.65| 0.49| 1.14| 2.41| |dubbo-kryo| 57.35| 0.53| 0.67| 1.02| |rapidoid| 52.96| 0.61| 1.32| 2.51| |dubbo| 52.12| 0.54| 0.67| 0.92| |motan| 44.96| 0.71| 1.15| 2.47| |aeron| 43.46| 0.90| 1.32| 5.10| |grpc| 38.97| 0.84| 1.07| 1.31| |thrift| 27.25| 1.59| 0.16| 64.87| |hprose| 26.24| 1.26| 1.53| 2.01| |springwebflux| 22.39| 1.42| 2.27| 3.19| |springboot| 12.54| 1.68| 2.38| 13.63|

消息格式

我们先来看一下 Dubbo 的消息格式

public class RpcInvocation implements Invocation, Serializable {
    private String methodName;
    private Class<?>[] parameterTypes;
    private Object[] arguments;
    ...
}

可以说是非常经典的设计,Client 必须告知 Server 要调用的 方法名称 参数类型 参数。Server 获取到这3个参数后,通过 方法名称 com.alibaba.service.auth.UserService.verifyUser 和 参数类型 (String, String) 获取到 Invoker,然后通过 Invoker 实际调用 userServiceImpl 的 verifyUser(String, String) 方法。其他的众多 RPC 框架也都采取了这一经典设计。

但是,这是正确的做法吗?当然不是,这种做法非常浪费空间,每次请求消息体的大概内存布局应该是下面的样子:

public boolean verifyUser(String email, String pwd);

|com.alibaba.service.auth.UserService.verifyUser|java.lang.String,java.lang.String|实际的参数|

啰里啰嗦的,浪费了 80 byte 来定义 方法 和 参数,并没有比 http+json 的方式高效多少。实际的 性能测试 也证明了这一点,undertow+jackson 要比 dubbo motan 的成绩都要好。

那什么才是正确的做法?Turbo 在消息格式上做出了非常大的改变。

public class Request implements Serializable {
    private int requestId;
    private int serviceId;
    private MethodParam methodParam;
    ...
}

大致的内存布局:

public boolean verifyUser(String email, String pwd);
|int|int|实际的参数|

高效多了,只用了 4 byte 就做到了 方法 和 参数 的定义。大大减小了 传输数据 的 size,同时 int 类型的 serviceId 也降低了 Invoker 的查找开销。

看到这里,有同学可能会问:那岂不是要为每个方法定义一个唯一 id ? 答案是不需要的,Turbo 解决了这一问题,详情参考 TurboConnectService

MethodParam 简介

MethodParam 才是 Turbo 性能炸裂的真正原因。其基本原理是利用 ClassGeneration 对每个 Method 都生成一个 MethodParam 类,用于对方法参数的封装。这样做的好处有:

  1. 减少基本数据类型的 装箱 拆箱 开销
  2. 序列化时可以省略掉很多类型描述,大大减小 传输消息 的 size
  3. 使 Invoker 可以高效调用 被代理类 的方法
  4. 统一 RPC 和 REST 的数据模型,简化 序列化 反序列化 实现
  5. 大大加快 json 格式数据 反序列化 速度
//方法 test(long id, int value) 将会生成下面的 MethodParam 类:     
public class TestService_test_2_MethodParam implements MethodParam {
    private long id;
    private int value;
     
    public long $param0() { return this.id; }
    public int $param1() { return this.value; }

    //... getters and setters
     
    public TestService_test_2_MethodParam(long id, int value) {
        this.id = id;
        this.value= value;
    }
}

序列化的进一步优化

大部分 RPC 框架的 序列化 反序列化 过程都需要一个中间的 bytes

序列化过程:User > bytes > ByteBuf
反序列化过程:ByteBuf > bytes > User

Turbo 砍掉了中间的 bytes,直接操作 ByteBuf,实现了 序列化 反序列化 的 zero-copy,大大减少了 内存分配 内存复制 的开销。具体实现请参考 ProtostuffSerializerCodec

对于已知类型和已知字段,Turbo 都尽量采用 手工序列化 手工反序列化 的方式来处理,以进一步减少性能开销。

ObjectPool

常见的几个 ObjectPool 实现性能都很差,反而很容易成为性能瓶颈。Stormpot 性能强悍,不过存在偶尔死锁的问题,而且作者也停止维护了。HikariCP 性能不错,不过其本身是一款数据库连接池,用作 ObjectPool 并不称手。我的建议是尽量避免使用 ObjectPool,转而使用替代技术。更重要的是 Netty 的 Channel 是线程安全的,并不需要使用 ObjectPool 来管理。只需要一个简单的容器来存储 Channel,用的时候使用 负载均衡策略 选出一个 Channel 出来就行了。

|framework| thrpt (ops/us)| |–|–| |ThreadLocal| 685.418| |Stormpot| 272.934| |HikariCP| 139.126| |SegmentLock| 19.415| |Vibur| 4.668| |CommonsPool2| 1.107| |CommonsPool| 0.276|

基础类库优化

除了上述的关键流程优化,Turbo 还做了大量基础类库的优化

  • AtomicMuiltInteger 多个 int 的原子性操作
  • ConcurrentArrayList 无锁并发 List 实现,比 CopyOnWriteArrayList 的写入开销低,O(1) vs O(n)
  • ConcurrentIntToObjectArrayMap 以 int 数组为底层实现的无锁并发 Map,读多写少情况下接近直接访问字段的性能,读多写多情况下是 ConcurrentHashMap 性能的 5x
  • ConcurrentIntegerSequencer 快速序号生成器,并发环境下是 AtomicInteger 性能的10x
  • ObjectId 全局唯一 id 生成器,是 Java 自带 UUID 性能的 200x
  • HexUtils 查表 + 批量操作,是 Netty 和 Guava 实现的 2x~5x
  • URLEncodeUtils 基于 HexUtils 实现,是 Java 和 Commons 实现的 2x,Guava 实现的 1.1x (Guava 只有 urlEncode 实现,无 urlDecode 实现)
  • ByteBufUtils 实现了高效的 ZigZag 写入操作,最高可达通常实现的 4x

上面的内容仅介绍了作者认为重要的东西,更多内容请直接查看 Turbo 源码

不足之处

  • 有很多优化是毫无价值的,Donald Knuth 大神说得很对
  • 强制必须使用 CompletableFuture 作为返回值导致了一些性能开销
  • 滥用 ClassGeneration,而且并没有考虑类的卸载,这方面需要改进
  • 实现了 UnsafeStringUtils,这是个危险的黑魔法实现,需要重新思考下
  • 对性能的追求有点走火入魔,导致了很多地方的设计过于复杂

怎样对RPC进行有效的性能测试

Published on:

最近看相关rpc-benchmark相关的东西发现这篇文章挺好的,所以转载出来,下面是文章出处。

作者:鲁小憨 链接:https://www.jianshu.com/p/cbcdf05eaa5c 來源:简书 简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

RPC Benchmark Round 1turbo 的成绩一骑绝尘,实力碾压众 rpc 框架。对此,很多人表示不服气,认为作者既是运动员又是裁判员有失公平。所以我认为有必要解释一下 rpc-benchmark 的公正性,以及为什么 turbo 能够如此强悍。

参考对象

rpc-benchmark 灵感源自 techempower-benchmarks,为了能够评测众多服务器框架,techempower-benchmarks 提供了6个测试用例:

  • JSON serialization

This test exercises the framework fundamentals including keep-alive support, request routing, request header parsing, object instantiation, JSON serialization, response header generation, and request count throughput.

  • Single database query

This test exercises the framework’s object-relational mapper (ORM), random number generator, database driver, and database connection pool.

  • Multiple database queries

This test is a variation of Test #2 and also uses the World table. Multiple rows are fetched to more dramatically punish the database driver and connection pool. At the highest queries-per-request tested (20), this test demonstrates all frameworks’ convergence toward zero requests-per-second as database activity increases.

  • Fortunes

This test exercises the ORM, database connectivity, dynamic-size collections, sorting, server-side templates, XSS countermeasures, and character encoding.

  • Database updates

This test is a variation of Test #3 that exercises the ORM’s persistence of objects and the database driver’s performance at running UPDATE statements or similar. The spirit of this test is to exercise a variable number of read-then-write style database operations.

  • Plaintext

This test is an exercise of the request-routing fundamentals only, designed to demonstrate the capacity of high-performance platforms in particular. Requests will be sent using HTTP pipelining. The response payload is still small, meaning good performance is still necessary in order to saturate the gigabit Ethernet of the test environment.

techempower-benchmarks 规则都是公开的,代码都是开放的。任何人觉得xx框架写得不好,配置有问题,都可以来提交自己的 Pull Request 。一句话,不服气的话就来提交代码。

测试用例

不过 techempower-benchmarks 对比的都是服务器框架,并不能用来测试 rpc 的性能,作为学习模仿者,我创建了 rpc-benchmark 这个项目。 rpc-benchmark 提供了4个测试用例:

  • boolean existUser(String email), 判断某个 email 是否存在 输入是很短的字符串,输出是 bool 值,这个测试用例用于衡量小 Request 小 Response 的性能。

  • boolean createUser(User user), 添加一个 用户 输入是一个 User 的对象,输出是 bool 值,这个测试用例用于衡量大 Request 小 Response 的性能。

  • User getUser(long id), 根据 id 获取一个用户 输入是一个 long 类型的值,输出是 User 对象,这个测试用例用于衡量小 Request 大 Response 的性能。

  • Page listUser(int pageNo), 获取用户列表 输入是 int 类型的值,输出是一个包含15个 User 的列表,这个测试用例用于衡量小 Request 超大 Response 的性能。

这4个测试用例构成了一个基本的业务逻辑: 用户注册管理。非常具有代表性,并且没有脱离现实使用场景。有些测试用例可能会注重衡量字符串的传输速度,从4字节 64字节 … 64k字节 依次测起,这样的测试用例就过于脱离现实,没有太多的实际意义。毕竟作为 rpc 框架,除了传输速度,序列化速度其实也是非常重要的。而仅仅用字符串来测试仅能测试出框架的传输速度,并不能有效衡量序列化的性能,也不能衡量整体的 rpc 性能。

测试工具

因为每个 rpc 框架都有自己的 序列化协议 传输协议,所以 rpc-benchmark 不能像 techempower-benchmarks 一样直接使用 wrk 作为测试工具,只能每个框架都编写测试用的 客户端实现。

客户端实现 使用的工具是JMH,这个工具 Java 开发团队自己也在使用。正确的性能测试在之前并不是一件简单的事情,JMH 的出现让性能测试真正的 标准化 简单化。更多关于 JMH 的介绍可以参考下面的链接。

测试方法

测试的过程是先进行10次预热,然后才开始真正的3次测试(JMH的“每次”执行实际上是执行很多次,更好的翻译其实应该是“每轮”)。刚开始使用的是5次预热,但是后来发现 http 传输协议的 undertow grpc 等框架都比较慢热,需要更多的预热次数。完整的测试要跑起来依然有点费劲,需要配置很多环境。不过如果你只是想研究下某个框架的代码实现的话,完全可以更简单一些。拉下代码来直接导入到 Eclipse/IDEA ,配置好hosts,启动 Server,然后启动相应的 Client 就好了。

为什么把 undertow springboot netty 也作为了测试对象

按照 wiki 的定义,这三个确实不能认为是 rpc ,不过简单封装之后他们都可以作为 rpc 使用。加入这几个更多的是为给 rpc 框架的实现者提供一个参考,作为基础的协议层性能是怎么样的?作为springcloud 的底层实现,springboot 其实代表了springcloud 的性能。undertow 证明了 http+json 并不比 tcp+binary 慢太多,其速度甚至比 dubbo motan 还要快不少。同时也是为了告诉喷子们,并不是说你用了高性能的 netty+protopuff 就能比 turbo 快,turbo 能碾压众框架并不只是靠简单的拼积木就能做到的。

不足之处

仅1个客户端32个线程其实是非常不严谨的,正确的做法应该是从1个线程一直到32k个线程逐步增加,从1台客户端机器到1000台客户端机器逐步增加(客户端数量 线程数量 应该是一个笛卡尔积)。不过每轮测试实在都太耗费时间了,而且阿里云的服务器也不便宜,所以只能作罢。后续如果有云服务器厂商赞助的话,可以考虑把这块给做起来。

turbo为什么如此强悍

篇幅有限写不开了,下篇再说吧。

Trouble Shooting —— MyBatis的PropertyTokenizer抛NPE异常

Published on:
Tags: mybatis

这个文章转自公司内网WIKI,同事调试的问题以及问题分析过程,我觉得挺好的所以转载出来。

问题描述

多任务同时处理时会报出如下NPE异常,堆栈信息如下:

2018-08-10 18:16:10.938 [xxxExecutor-2] ERROR c.j.bmc.mq.listener.xxxResultListener 
org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.PersistenceException:
### Error querying database.  Cause: java.lang.NullPointerException
### Cause: java.lang.NullPointerException
    at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:75) ~[mybatis-spring-1.2.2.jar:1.2.2]
    at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:371) ~[mybatis-spring-1.2.2.jar:1.2.2]
    at com.sun.proxy.$Proxy21.selectList(Unknown Source) ~[na:na]
    at org.mybatis.spring.SqlSessionTemplate.selectList(SqlSessionTemplate.java:198) ~[mybatis-spring-1.2.2.jar:1.2.2]
    at org.apache.ibatis.binding.MapperMethod.executeForMany(MapperMethod.java:119) ~[mybatis-3.2.7.jar:3.2.7]
    at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:63) ~[mybatis-3.2.7.jar:3.2.7]
    at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:52) ~[mybatis-3.2.7.jar:3.2.7]
    at com.sun.proxy.$Proxy49.findBillBillingTask(Unknown Source) ~[na:na]
    at com.xxx.service.impl.XXXServiceImpl.findBillBillingTask(XXXServiceImpl.java:118) ~[bmc-service-0.0.1-SNAPSHOT.jar:na]
    at com.xxx.service.impl.XXXServiceImpl$$FastClassByCGLIB$$7d4463f0.invoke(<generated>) ~[spring-core-4.0.0.RELEASE.jar:na]
    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204) ~[spring-core-4.0.0.RELEASE.jar:4.0.0.RELEASE]
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:713) ~[spring-aop-4.0.0.RELEASE.jar:4.0.0.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) ~[spring-aop-4.0.0.RELEASE.jar:4.0.0.RELEASE]
    at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:98) ~[spring-tx-4.0.0.RELEASE.jar:4.0.0.RELEASE]
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:262) ~[spring-tx-4.0.0.RELEASE.jar:4.0.0.RELEASE]
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:95) ~[spring-tx-4.0.0.RELEASE.jar:4.0.0.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.0.0.RELEASE.jar:4.0.0.RELEASE]
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:646) ~[spring-aop-4.0.0.RELEASE.jar:4.0.0.RELEASE]
    at com.xxx.service.impl.XXXServiceImpl$$EnhancerByCGLIB$$32d6287d.findBillBillingTask(<generated>) ~[spring-core-4.0.0.RELEASE.jar:na]
    at com.xxx.service.impl.XXXResultServiceImpl.saveBillBillingTask(XXXResultServiceImpl.java:213) ~[bmc-service-0.0.1-SNAPSHOT.jar:na]
    at com.xxx.service.impl.XXXResultServiceImpl.disposeBillBillingResult(XXXResultServiceImpl.java:193) ~[bmc-service-0.0.1-SNAPSHOT.jar:na]
    at com.xxx.service.impl.XXXResultServiceImpl$$FastClassByCGLIB$$5e8db258.invoke(<generated>) ~[spring-core-4.0.0.RELEASE.jar:na]
    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204) ~[spring-core-4.0.0.RELEASE.jar:4.0.0.RELEASE]
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:713) ~[spring-aop-4.0.0.RELEASE.jar:4.0.0.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) ~[spring-aop-4.0.0.RELEASE.jar:4.0.0.RELEASE]
    at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:98) ~[spring-tx-4.0.0.RELEASE.jar:4.0.0.RELEASE]
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:262) ~[spring-tx-4.0.0.RELEASE.jar:4.0.0.RELEASE]
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:95) ~[spring-tx-4.0.0.RELEASE.jar:4.0.0.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.0.0.RELEASE.jar:4.0.0.RELEASE]
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:646) ~[spring-aop-4.0.0.RELEASE.jar:4.0.0.RELEASE]
    at com.xxx.service.impl.XXXResultServiceImpl$$EnhancerByCGLIB$$8d251e5.disposeBillBillingResult(<generated>) ~[spring-core-4.0.0.RELEASE.jar:na]
    at com.xxx.XXXListener.receiveMessage(BillBillingResultListener.java:92) ~[bmc-main-0.0.1-SNAPSHOT.jar:na]
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.7.0_79]
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57) ~[na:1.7.0_79]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.7.0_79]
    at java.lang.reflect.Method.invoke(Method.java:606) ~[na:1.7.0_79]
    at org.springframework.util.MethodInvoker.invoke(MethodInvoker.java:273) [spring-core-4.0.0.RELEASE.jar:4.0.0.RELEASE]
    at org.springframework.jms.listener.adapter.MessageListenerAdapter.invokeListenerMethod(MessageListenerAdapter.java:466) [spring-jms-4.0.0.RELEASE.jar:4.0.0.RELEASE]
    at org.springframework.jms.listener.adapter.MessageListenerAdapter.onMessage(MessageListenerAdapter.java:357) [spring-jms-4.0.0.RELEASE.jar:4.0.0.RELEASE]
    at org.springframework.jms.listener.adapter.MessageListenerAdapter.onMessage(MessageListenerAdapter.java:332) [spring-jms-4.0.0.RELEASE.jar:4.0.0.RELEASE]
    at org.springframework.jms.listener.AbstractMessageListenerContainer.doInvokeListener(AbstractMessageListenerContainer.java:537) [spring-jms-4.0.0.RELEASE.jar:4.0.0.RELEASE]
    at org.springframework.jms.listener.AbstractMessageListenerContainer.invokeListener(AbstractMessageListenerContainer.java:497) [spring-jms-4.0.0.RELEASE.jar:4.0.0.RELEASE]
    at org.springframework.jms.listener.AbstractMessageListenerContainer.doExecuteListener(AbstractMessageListenerContainer.java:468) [spring-jms-4.0.0.RELEASE.jar:4.0.0.RELEASE]
    at org.springframework.jms.listener.AbstractPollingMessageListenerContainer.doReceiveAndExecute(AbstractPollingMessageListenerContainer.java:325) [spring-jms-4.0.0.RELEASE.jar:4.0.0.RELEASE]
    at org.springframework.jms.listener.AbstractPollingMessageListenerContainer.receiveAndExecute(AbstractPollingMessageListenerContainer.java:263) [spring-jms-4.0.0.RELEASE.jar:4.0.0.RELEASE]
    at org.springframework.jms.listener.DefaultMessageListenerContainer$AsyncMessageListenerInvoker.invokeListener(DefaultMessageListenerContainer.java:1104) [spring-jms-4.0.0.RELEASE.jar:4.0.0.RELEASE]
    at org.springframework.jms.listener.DefaultMessageListenerContainer$AsyncMessageListenerInvoker.run(DefaultMessageListenerContainer.java:998) [spring-jms-4.0.0.RELEASE.jar:4.0.0.RELEASE]
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145) [na:1.7.0_79]
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615) [na:1.7.0_79]
    at java.lang.Thread.run(Thread.java:745) [na:1.7.0_79]
Caused by: org.apache.ibatis.exceptions.PersistenceException:
### Error querying database.  Cause: java.lang.NullPointerException
### Cause: java.lang.NullPointerException
    at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:26) ~[mybatis-3.2.7.jar:3.2.7]
    at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:111) ~[mybatis-3.2.7.jar:3.2.7]
    at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:102) ~[mybatis-3.2.7.jar:3.2.7]
    at sun.reflect.GeneratedMethodAccessor203.invoke(Unknown Source) ~[na:na]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.7.0_79]
    at java.lang.reflect.Method.invoke(Method.java:606) ~[na:1.7.0_79]
    at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:358) ~[mybatis-spring-1.2.2.jar:1.2.2]
    ... 48 common frames omitted
Caused by: java.lang.NullPointerException: null
    at org.apache.ibatis.reflection.property.PropertyTokenizer.<init>(PropertyTokenizer.java:30) ~[mybatis-3.2.7.jar:3.2.7]
    at org.apache.ibatis.reflection.MetaObject.getValue(MetaObject.java:107) ~[mybatis-3.2.7.jar:3.2.7]
    at org.apache.ibatis.scripting.xmltags.DynamicContext$ContextMap.get(DynamicContext.java:97) ~[mybatis-3.2.7.jar:3.2.7]
    at org.apache.ibatis.scripting.xmltags.DynamicContext$ContextAccessor.getProperty(DynamicContext.java:116) ~[mybatis-3.2.7.jar:3.2.7]
    at org.apache.ibatis.ognl.OgnlRuntime.getProperty(OgnlRuntime.java:1657) ~[mybatis-3.2.7.jar:3.2.7]
    at org.apache.ibatis.ognl.ASTProperty.getValueBody(ASTProperty.java:92) ~[mybatis-3.2.7.jar:3.2.7]
    at org.apache.ibatis.ognl.SimpleNode.evaluateGetValueBody(SimpleNode.java:170) ~[mybatis-3.2.7.jar:3.2.7]
    at org.apache.ibatis.ognl.SimpleNode.getValue(SimpleNode.java:210) ~[mybatis-3.2.7.jar:3.2.7]
    at org.apache.ibatis.ognl.ASTNotEq.getValueBody(ASTNotEq.java:49) ~[mybatis-3.2.7.jar:3.2.7]
    at org.apache.ibatis.ognl.SimpleNode.evaluateGetValueBody(SimpleNode.java:170) ~[mybatis-3.2.7.jar:3.2.7]
    at org.apache.ibatis.ognl.SimpleNode.getValue(SimpleNode.java:210) ~[mybatis-3.2.7.jar:3.2.7]
    at org.apache.ibatis.ognl.Ognl.getValue(Ognl.java:333) ~[mybatis-3.2.7.jar:3.2.7]
    at org.apache.ibatis.ognl.Ognl.getValue(Ognl.java:413) ~[mybatis-3.2.7.jar:3.2.7]
    at org.apache.ibatis.ognl.Ognl.getValue(Ognl.java:395) ~[mybatis-3.2.7.jar:3.2.7]
    at org.apache.ibatis.scripting.xmltags.OgnlCache.getValue(OgnlCache.java:48) ~[mybatis-3.2.7.jar:3.2.7]
    at org.apache.ibatis.scripting.xmltags.ExpressionEvaluator.evaluateBoolean(ExpressionEvaluator.java:32) ~[mybatis-3.2.7.jar:3.2.7]
    at org.apache.ibatis.scripting.xmltags.IfSqlNode.apply(IfSqlNode.java:33) ~[mybatis-3.2.7.jar:3.2.7]
    at org.apache.ibatis.scripting.xmltags.MixedSqlNode.apply(MixedSqlNode.java:32) ~[mybatis-3.2.7.jar:3.2.7]
    at org.apache.ibatis.scripting.xmltags.DynamicSqlSource.getBoundSql(DynamicSqlSource.java:40) ~[mybatis-3.2.7.jar:3.2.7]
    at org.apache.ibatis.mapping.MappedStatement.getBoundSql(MappedStatement.java:278) ~[mybatis-3.2.7.jar:3.2.7]
    at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:75) ~[mybatis-3.2.7.jar:3.2.7]
    at sun.reflect.GeneratedMethodAccessor205.invoke(Unknown Source) ~[na:na]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.7.0_79]
    at java.lang.reflect.Method.invoke(Method.java:606) ~[na:1.7.0_79]
    at org.apache.ibatis.plugin.Invocation.proceed(Invocation.java:49) ~[mybatis-3.2.7.jar:3.2.7]
    at com.github.pagehelper.util.SqlUtil.doIntercept(SqlUtil.java:175) ~[pagehelper-4.2.1.jar:na]
    at com.github.pagehelper.util.SqlUtil.intercept(SqlUtil.java:84) ~[pagehelper-4.2.1.jar:na]
    at com.github.pagehelper.PageHelper.intercept(PageHelper.java:50) ~[pagehelper-4.2.1.jar:na]
    at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:60) ~[mybatis-3.2.7.jar:3.2.7]
    at com.sun.proxy.$Proxy53.query(Unknown Source) ~[na:na]
    at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:108) ~[mybatis-3.2.7.jar:3.2.7]
    ... 53 common frames omitted

分析过程

通常,有堆栈数据的时候就很容分析出问题的原因。但是经过查看相关代码后发现触发点操作逻辑非常简单,不太会出现该异常。

于是就考虑从日志的细节分析问题。

先查看业务代码。该代码使用一个非空的VO对象作为查询条件,提交给Mapper。

Mapper中,判断各个参数是否为null或者空,然后拼接到SQL中。整个过程非常简单,而且大部分是成功执行的。

通过以上判断,可以认为不是业务代码的问题,极有可能是mybatis的问题。于是上网进行搜索,得到一些关于偶发NPE问题的描述。

mybatis-3/issues/313

mybatis-3/issues/199

以下问题提及了偶发返回null的情况。

issues-OGNL-121

于是我们又观察反编译代码和日志执行情况,可以看到在SimpleNode.java中的确有非安全的逻辑操作。

日志也有相关的执行过程。

由于没有源代码,所以无法有效的进行Debug,模拟并发操作。因此该问题只能怀疑是这个原因导致的,具体可以在后续

有条件的情况下进行模拟测试。

深入并发测试

在Idea中可以反编译代码,且还原度较高,因此我们做了一次测试。

测试环境

两个Consumer调用Provider,Provider只做数据库查询,且查询中带条件判断。

这里使用Spring test进行测试,以下是Consumer端调用

Provider端定义

查询条件判断

准备好测试环境后,就可以进行测试了。此处还需要注意如何在IntellJ Idea中Debug多线程,具体设置方法请找度娘。

测试过程

同时启动两个Consumer,然后在Provider中的SimpleNode.java中设置断点。

根据之前分析,如果要出现null,则说明getProperty会返回null。

而getValue方法实际调用的逻辑是以下代码:

说明以下的代码返回了null

从代码上及Debug分析,如果要返回null,则很有可能在hasConstantValue=true且constantValue为null。

当然此处的数据已经是我们模拟出并发问题后的结果,也验证了是有可能的。

如果没有出问题的情况时,正常的结果应该是constantValue=id,hasConstantValue=true。

测试过程中,我们发现多个线程调用的对象实际是同一个,如下图中的ASTConst@6075。

根据多线程常见问题处理经验来看,如果多线程操作同一个对象,则要注意其是否存在成员变量。如果有,那还要注意是否做了并发可见性处理。

于是我们看下代码

那我们指导了这里有并发问题,那就好容易模拟了。我们只要在第一个线程中保持一种状态,然后暂停操作。再在另外一个线程中去特定的操作

步骤中正常变更数据。最后再放开第一个线程继续往下执行。由于第一个线程的成员变量已经发生了变化,所以后续的结果就不再是预想的那样

了。

于是就有了如下结果

模拟的关键在于:

  1. 多线程操作同一个服务
  2. 第一个线程在判断语句处等待第二个线程变更条件值,this.constantValueCalculated变量初始化为false,等第二个线程变更后变为true
  3. 第一个线程继续往下执行
  4. 第二个线程变更了成员变量的值,this.hasConstantValue变量初始化为false,但是被变更为true,然后等待第一个线程执行
  5. 第一个线程用刚更新的值去判断,返回了null值,也就导致了后续的NPE异常

注意:以上说的“等待”只是模拟说法,实际情况会由CPU控制,执行顺序不定。恰巧出现了以上执行流程,则会出现NPE问题。

升级版处理逻辑

在Mybatis的3.4.5版本中,程序采用了volatile修饰符来定义变量,并且在使用上面也注意了赋值的先后顺序。

结论

建议升级mybatis,版本是3.3.0+,提及到新的ognl处理逻辑修复此问题,但是我们要考虑在经过充分测试的前提下进行升级。

为什么手机浏览器打开word、excel文件部分文件能预览,部分文件不能预览?

Published on:
Tags: word-xml

最近公司合同项目中有很多附件是excel和word的格式,这些文件有用户直接导入的,也有程序自动生成的,合同项目中有结合钉钉来做工作流,所以会有pc端和钉钉移动端的互动。

问题现象

pc端的附件列表可以正常的下载word、excel文件,并且可以成功的打开,但是当流程流转到钉钉时,在钉钉审批的时候可以通过连接跳转h5来显示附件列表,项目的功能设计初衷是可以在手机端打开预览word、excel文件。

但是发现了奇怪的问题,部分word可以在钉钉中显示,部分word无法显示,例如下图所示:

我们的期望效果如下图所示:

问题分析

我们分别使用手机浏览器(safari)、postman、微信内嵌浏览器、qq内嵌浏览器分别测试无法正常预览的word链接

手机浏览器、微信内嵌浏览器、qq内嵌浏览器均无法打开

使用postman下载在移动端无法打开的word链接,返回的是一段xml,如下:


<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<?mso-application progid="Word.Document"?>
<pkg:package xmlns:pkg="http://schemas.microsoft.com/office/2006/xmlPackage"><pkg:part pkg:name="/_rels/.rels"
....

这是个什么鬼?是word的xml格式,问题原因就在这里手机浏览器无法识别word的xml格式,因此再次尝试excel

excel使用的是poi生成直接写的是二级制格式,没有使用xml格式,因此excel是可以在移动端打开预览的。

询问开发word是如何生成的?

生成过程是这样的:使用word编辑好模版,然后另存为xml格式,导入到系统中去,通过FreeMarker替换内容,再将xml写到fastdfs中去后缀给成 ‘.doc’ ,这样下载下来使用office word可以直接打开xml格式的来进行无损渲染。

解决方案

询问业务是否必须要使用word格式文件?我的理解合同项目大多都是给用户只读的文件,建议使用pdf来做,使用jasper生成word模版,通过jasper的java api直接生成pdf,合同后期还要考虑添加水印,pdf更加方便一些。

建议使用pdf来替换word,如果非要使用word,建议生成word二进制格式来替换xml格式,除非不考虑移动端渲染可以使用xml格式的word。

目前java生成word的方式有如下六种方式:

  1. Jacob是Java-COM Bridge的缩写,它在Java与微软的COM组件之间构建一座桥梁。使用Jacob自带的DLL动态链接库,并通过JNI的方式实现了在Java平台上对COM程序的调用。DLL动态链接库的生成需要windows平台的支持。该方案只能在windows平台实现,是其局限性。
  2. Apache POI包括一系列的API,它们可以操作基于MicroSoft OLE 2 Compound Document Format的各种格式文件,可以通过这些API在Java中读写Excel、Word等文件。他的excel处理很强大,对于word还局限于读取,目前只能实现一些简单文件的操作,不能设置样式。
  3. Java2word是一个在java程序中调用 MS Office Word 文档的组件(类库)。该组件提供了一组简单的接口,以便java程序调用他的服务操作Word 文档。 这些服务包括: 打开文档、新建文档、查找文字、替换文字,插入文字、插入图片、插入表格,在书签处插入文字、插入图片、插入表格等。填充数据到表格中读取表格数据 ,1.1版增强的功能: 指定文本样式,指定表格样式。如此,则可动态排版word文档。是一种不错的解决方案。
  4. iText是著名的开放源码的站点sourceforge一个项目,是用于生成PDF文档的一个java类库。通过iText不仅可以生成PDF或rtf的文档,而且可以将XML、Html文件转化为PDF文件。功能强大。
  5. JSP输出样式,该方案实现简单,但是处理样式有点缺陷,简单的导出可以使用。
  6. 用XML做就很简单了。Word从2003开始支持XML格式,大致的思路是先用office2003或者2007编辑好word的样式,然后另存为xml,将xml翻译为FreeMarker模板,最后用java来解析FreeMarker模板并输出Doc。经测试这样方式生成的word文档完全符合office标准,样式、内容控制非常便利,打印也不会变形,生成的文档和office中编辑文档完全一样。

总结

伴随着手机的兴起,不管是传统行业还是互联网行业对系统都有在移动端使用的要求,不管是从用户体验上还是从移动系统兼容性以及浏览器兼容性上都会遇到各种问题,当然也有工具可以解决这些问题,例如:RN、flutter都可以很好的解决系统兼容问题,vue.js、angularjs都可以很好的解决浏览器兼容问题,而且这些都有大厂的支持,关于以上的问题这种解决方法并不是最好的,但是可以做为一种参考,重点是对问题的总结,只要解决问题的方法符合自己的业务场景我个人认为就是正(有)确(效)的方法,如果有更好的方式可以在下放留言一起讨论。

使用downloadjs下载并且重命名文件名称引发的跨域问题

Published on:

我们有一部分静态资源放在fastdfs文件服务器上,并且文件名称是生成的随机数,直接浏览器下载是可以正常下载文件的,但是我们需要修改下载文件的名称,直接a标签href是无法修改下载文件名称的。

使用a标签的download属性又有浏览器兼容问题,而且download属性有一个弊端,只有点击右键另存为才会生效,直接点击是不生效的。

因此我们这里借助了一个组件downloadjs来进行文件下载,它可以修改下载文件的名称,并且也没有浏览器兼容问题,原理呢很简单那,使用ajax请求去下载文件,在发起请求时构造请求header来重命名下载文件名。

但是这里会存在一个问题?我们的fastdfs和应用程序是独立的两个域,因此存在跨域的问题,直接使用a标签的href是不存在跨域的问题,按关于这个跨域的问题我们如何解决?

先来看一下使用downloadjs下载fastdfs的文件时报出的跨域错误信息如下

Failed to load http://192.168.0.48:8079/group1/M00/03/35/wKgAMFtgB2SAFjibAAX3egrfUI8922.doc: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:8080' is therefore not allowed access.

本地使用是通过vue的proxyTable绕过跨域的问题,其实就是前端的proxy方式虚拟一个context然后pass转发,虽然这样可以解决目前的问题,但是我们在uat和prd环境又要增加相同的context path的映射,这不是我们想要的,我们想直接访问下载地址来进行下载,因此我们需要修改fastdfs的nginx模块配置。

跨域的配置这里就不多说了,其实就是添加一系列的Access-Control-Allow-X的header即可,不会的可以参考我以前的文章跨域踩坑经验总结》,唯一需要注意的是,当使用Access-Control-Allow-Credentials=trueAccess-Control-Allow-Origin不允许使用* 必须使用具体的域名多个可以使用,分割。

修改后我们可以直接的请求地址下载文件即可。