Mobility

聚沙成塔

在命令行运行java程序时,如果想引入第三方jar包该怎么办呢。方法其实有很多,一般都是去折腾classloader.

这里介绍一种操作相对简单的方法,就是在运行之前把需要的jar包都加入到classpath中。

具体来说,就是写一个shell脚本,定义一个参数,可以就叫CLASSPATH, 也可以叫别的。

CLASSPATH=yourownjar.jar:xxx.jar:/xx/xx/xxx1.jar:”$CLASSPATH”

需要注意的是,自己写的主类所在的jar也要包含在自己定义的classpath中.

然后使用java -classpath命令运行即可:

java -classpath ${CLASSPATH} xx.Main

最近试图运行一个fatjar的时候报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Exception in thread "main" java.lang.SecurityException: Invalid signature file digest for Manifest main attributes
at sun.security.util.SignatureFileVerifier.processImpl(SignatureFileVerifier.java:287)
at sun.security.util.SignatureFileVerifier.process(SignatureFileVerifier.java:240)
at java.util.jar.JarVerifier.processEntry(JarVerifier.java:317)
at java.util.jar.JarVerifier.update(JarVerifier.java:228)
at java.util.jar.JarFile.initializeVerifier(JarFile.java:348)
at java.util.jar.JarFile.getInputStream(JarFile.java:415)
at sun.misc.URLClassPath$JarLoader$2.getInputStream(URLClassPath.java:775)
at sun.misc.Resource.cachedInputStream(Resource.java:77)
at sun.misc.Resource.getByteBuffer(Resource.java:160)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:436)
at java.net.URLClassLoader.access$100(URLClassLoader.java:71)
at java.net.URLClassLoader$1.run(URLClassLoader.java:361)
at java.net.URLClassLoader$1.run(URLClassLoader.java:355)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:354)
at java.lang.ClassLoader.loadClass(ClassLoader.java:425)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:308)
at java.lang.ClassLoader.loadClass(ClassLoader.java:358)
at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:482)

查了一下,跟jar包的签名有关。关于签名可以参考: http://www.cnblogs.com/jackofhearts/p/jar_signing.html

有些Jar包会在metainf里包含一个.SF:包含原Jar包内的class文件和资源文件的Hash, 用来校验文件的完整度等验证。

但是在打fat-jar的时候,我们是把很多jar包合成了一个,这样fatjar下就会存在各个jar包中的签名文件,但是他们显然无法跟最终的fatjar作校验。

解决方法就是打包时把签名文件全都去掉,如果是使用maven的话,可以使用shade插件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>1.7.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
</execution>
</executions>
</plugin>

Redis 集群是redis官方提供的一种集群方案,从3.0开始提供稳定版,应用也已经比较广泛,也经受住了时间考验,个人感觉完全可以取代codis,tweemproxy等集群方案。

cluster原理介绍

cluster是使用数据分片的形式实现的,一个 Redis cluster集群包含 16384 个哈希槽, 任意一个key都可以通过 CRC16(key) % 16384 这个公式计算出应当属于哪个槽。每个槽应当落在哪个节点上,也是事先定好。这样,进行任一操作时,首先会根据key计算出对应的节点,然后操作相应的节点就可以了。

所以说,其实cluster跟单点相比,只是多了一个给key计算sharding值的过程,并没有增加多少复杂度,完全可以放心使用。

在这样的设计下,一些对节点的操作也很方便,操作过程中对client端也不会有影响。
1. 增加节点:将原有节点的某些slot转移到新节点上
2. 删除节点: 将节点上的槽转移到其他节点上以后,移除空节点
3. 节点故障:落到故障节点上的操作会失败,而集群其他部分可以正常访问,这样就不会出现一致性哈希方案的“雪崩”问题。单点的故障也可以通过配置slave解决,节点故障时可以由对应的slave顶替
4. 节点重启: 正常重启即可,节点会自动读之前的配置然后加入集群中。

安装操作流程

然后,我们可以实际安装一次,由于只是测试,装个两三个节点就可以,而如果是生产使用的话,至少需要3个master节点,也推荐同时至少有3个slave节点才能使用。

安装redis

到redis官网(https://redis.io/download)上下载最新的stable版本。

以3.2.8为例,下载完以后执行以下命令

1
2
3
$ tar xzf redis-3.2.8.tar.gz
$ cd redis-3.2.8
$ make

然后解压缩,make就可以了.

redis配置与启动

make之后会在redis的src目录下多出几个命令,包括redis-server, redis-cli等,可以把他们拷到/usr/local/bin下,这样就可以在任意路径下执行。也可以自己指到src目录下进行操作。

启动之前需要先修改一下配置,redis的根目录下有一个redis.conf文件,可以直接改它,也可以拷到别的路径下进行修改。

主要就是把cluster模式打开,然后配一下cluster配置文件。

1
2
cluster-enabled yes
cluster-config-file nodes.conf

其他的配置,port,bind,protect-mode之类的,可以根据需求改一下,不改也没关系。如果是想在一台机器上启动多个实例的话,需要给每个实例一个配置文件, port, cluster-config-file这些配置也需要区分一下。

然后执行以下命令,分别指定不同的配置文件启动。

1
2
redis-server redis-6379.conf &
redis-server redis-6380.conf &

开启cluster

需要的节点都启动以后,就可以开始配置cluster了。推荐的办法是使用官方提供的redis-trib.rb工具包,这个具体操作可以参考官方文档。我们在这里试一下手动操作,这样其实可以加深对cluster原理的理解。

首先通过redis-cli命令进入任意一个redis实例中,然后一次对其他几个节点执行meet命令。

1
Cluster meet xxx:6380

然后执行cluster nodes命令就可以看到节点被加入到集群中了。

这之后还有一步是分配slot, 可以使用cluster setslot命令,不过由于slot比较多,分配起来很麻烦。还有另外一种方式是修改nodes.conf这个配置文件。

Nodes.conf这个配置包含了cluster相关的几乎所有信息,一般情况下无需自己去操作,redis会自动生成,可以打开看一下,里边每个节点都对应一行,一行是类似这种格式:

1
8301106a1a0b7472650f22503abea23024df17fb 127.0.0.1:6379 myself,master - 0 0 1 connected

然后slot的分配情况是加在最后

1
8301106a1a0b7472650f22503abea23024df17fb 127.0.0.1:6379 myself,master - 0 0 1 connected 0-8000

需要注意的是,全部16384(0-16383)个slot都要分配出去,不能留空;集群中每个节点都对应一个Nodes.conf文件,每个文件里的slot配置要一致。

改完以后重启所有节点,在redis-cli中执行cluster info 命令,可以看到一些关键信息

1
2
3
4
5
6
7
cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:2
cluster_size:1

所有slot已经分配出去,Cluster stat也变成了ok. 这样一个cluster就算搭建完毕了。

原文地址:http://lichuanyang.top/posts/9329/

我们知道,hbase中存放的数据就是二进制的键值对,不像redis一样提供了各种各样数据结构的支持。如果我们想在hbase中存储set型的数据,该怎么做呢?当然,一种方法就是把这个set当作一个对象整体的序列化之后存到hbase上,但这样后续无论增删改查,都需要先把存储内容拿回来,做相应的修改后,再整体覆盖原有值。这么做显然不太合理。

我们希望的效果至少要包括这么几个特点:
1. 元素自动去重,这是一个set的基本要求;
2. 增删改这些对集合单个元素的操作,无须处理其他元素;
3. 方便查询,包括查询整个集合,和判断某一特定元素是否存在

这里提供一种思路实现这种效果,就是把元素的值存放在hbase的qualifier中,作为一个键,而value则随便塞一个值,不实际使用。

这样,由于hbase本身会对qualifier判重,所以元素不会重复,所有对单个元素的操作,都只需要操作这个qualifier, 需要获取整个集合时,也可以通过直接查这一行来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?xml version="1.0" encoding="UTF-8" ?>
<configuration status="warn">

<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="[%p] %d %c %l - %m%n"/>
</Console>

<RollingFile name="activity" fileName="/opt/fox.log"
filePattern="/opt/fox.log.%d{yyyy-MM-dd}.gz">
<Policies>
<TimeBasedTriggeringPolicy interval="1" modulate="true"/>
</Policies>
<PatternLayout pattern="[%p] %d - %m%n" charset="UTF-8"/>
</RollingFile>
<RollingFile name="fox_err" fileName="/opt/fox_err.log"
filePattern="/opt/fox_err..log.%d{yyyy-MM-dd}.gz">
<Policies>
<TimeBasedTriggeringPolicy interval="1" modulate="true"/>
</Policies>
<PatternLayout pattern="[%p] %d %l - %m%n" charset="UTF-8"/>
</RollingFile>
</Appenders>
<Loggers>
<logger name="com.fox" additivity="false" level="info">
<appender-ref ref="activity" />
<appender-ref ref="activity_err" level="error"/>
</logger>
<Root level="error">
<AppenderRef ref="Console"/>
</Root>
</Loggers>

</configuration>

和log4j相比,主要有这么一些变化,

首先整体结构上变化很大,appender、logger都被集中到了各自的一个根节点下。

xml各节点的名称也采用了新设计,名称直接就是有用信息,不再是之前appender xxx=”xxx”, param xxx=”xxx”的形式。

然后一些属性,包括fileName等,只能作为节点属性配置,不能像log4j那样配置成子节点。

此外,log4j2归档时支持压缩,在RollingFile节点的filePattern属性里将文件名后缀写成gz,zip等压缩格式,log4j2会自动选择相应压缩算法进行压缩。

现在发现的就这些,引入这个xml配置,再引用log4j-core, log4j-api包,就可以使用log4j2了。此外,如果有需要,可以用log4j-slf4j-impl,log4j-jcl,log4j-1.2-api分别实现对slf4j, jcl,log4j的兼容。

java里比较两个list的值是否一致,不考虑顺序,有多种方法,比如排序后直接用equals比较,相互之间执行两次containsAll等,这些办法都需要我们给list的元素类实现equals和hashcode方法。但是有一种特殊情况,如果我们并不方便去实习类的equals方法,例如是一个古老的第三方jar包,改代码会带来很多未知问题,这时候该怎么办呢。

其实很简单,万能的apache-commons早就想到了这一点,所以在commons-collections4中增加了外部输入equals和hashcode的方法,甚至equals和hashcode方法本身也不需要我们自己写代码,可以用comons-lang包实现,具体代码如下

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.1</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.5</version>
</dependency>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public static <T> boolean isEqualCollection(Collection<T> l1, Collection<T> l2, final String... exludedFields) {
Equator<T> equator = generateEquator(exludedFields);
return CollectionUtils.isEqualCollection(l1, l2, equator);
}

private static <T> Equator<T> generateEquator(final String... exludedFields) {
Equator<T> equator = new Equator<T>() {
@Override
public boolean equate(T o1, T o2) {
if (o1 == null && o2 == null) {
return true;
}
if (o1 == null || o2 == null) {
return false;
}
if (o1.getClass() != o2.getClass()) {
return false;
}
return EqualsBuilder.reflectionEquals(o1, o2, exludedFields);
}

@Override
public int hash(T o) {
return HashCodeBuilder.reflectionHashCode(o, exludedFields);
}
};
return equator;
}

redis中set系列命令(包括set,hset等等),基本上都包括两个版本,纯粹的set和setnx, setnx即set not exist, 也就是只有Key不存在时才会执行set, 而不会覆盖原有的值。

但是hmset这个命令,包括redis本身,jedis都没有提供nx版本的支持。当然,hset这个命令是有对应的hsetnx版本的,hmset意思就是multi hset,一次可以操作多个key, 从而减小网络开销。

所以,为了在使用hmset时也能降低网络的消耗,用lua写了一个脚本,实现hmsetnx的效果,即:向Hash表中set键值对时,只有键不存在时才会写入,不会覆盖原有值。

1
2
3
4
5
6
7
8
9
10
local key
for i,j in ipairs(ARGV)
do if i%2 == 0
then
redis.call('hsetnx', KEYS[1], key,j)
else
key = j
end
end
return 1

脚本的原理还是比较简单,脚本中使用的参数和hmset完全一致。依次读入参数列表,迭代器i是奇数时给key赋值,偶数时执行一次hsetnx,循环结束后也就完成了。

之后再调用jedis封装好的eval接口,

Object eval(final String script, final List keys, final List args)

或者

Object eval(final byte[] script, final List<byte[]> keys, final List<byte[]> args)

都可以,这两个接口的区别就是是否对参数进行序列化

keys中只放一个元素,就是hash表本身的key, 然后把键值对按照一个key,一个value的顺序依次放到args里。

当然,也可以用evalsha命令避免每次操作都要传输脚本本身,这里就不细说了。

为了方便,最近用vitualbox搭了一个centos7的虚拟机,整个过程比较简单,在这里记录一下。

下载vitualbox

直接去官网(https://www.virtualbox.org/wiki/Downloads)下载即可

下载centos安装包

同样官网下载(https://www.centos.org/download/),我下载的是minimal iso

安装

安装过程很简单,一路默认点下去就可以,中间内存、分区什么的可以根据需要调一下

配置本机ssh访问

vitualbox默认的分辨率非常低,可以通过安装增强工具进行优化。不过由于我们不需要图形化界面,其实可以通过其他方式解决这一问题,就是用xshell或者putty通过ssh远程登陆到虚拟机上。

打开ssh服务

1
2
service sshd start
chkconfig sshd on

分别启动ssh服务,并将ssh设定为自启动

关闭防火墙

由于只是弄着玩的,直接把防火墙关掉,方便。

centos7的防火墙操作和之前版本区别很大:

1
2
sudo systemctl stop firewalld.service
sudo systemctl disable firewalld.service

关闭防火墙和自动启动

配置端口转发

在VitualBox下配置端口转发:设置-网络-高级-端口转发,将22端口转发到主机的端口上,可以同样是22,也可以配置成其他端口.

如果需要在主机上访问虚拟机的其他端口,例如tomcat的8080,activemq的61616,8161,也可以在这儿一并配了。

查看虚拟机ip

在主机上执行ipconfig,找sudo systemctl disable firewalld.service对应的ip, 然后就可以在xshell中配置对应的ip和端口,访问虚拟机了。

配置自启动服务

centos下配置自启动的方式很多,我们在这里提供一种最简单的方式。

写一个脚本

例如vim /opt/app/service.sh

1
2
3
#!/bin/bash
export JAVA_HOME=/opt/app/jdk1.8.0_121
sh /opt/app/apache-activemq-5.14.4/bin/activemq start >/opt/app/start.log

把需要自启动的脚本全都放这儿,以后想增加自启动服务的时候,也只需要操作这个脚本。

配置

chmod +x /opt/app/service.sh

centos7下/etc/rc.d/rc.local也需要自己去加执行权限:

chmod +x /etc/rc.d/rc.local

然后打开/etc/rc.d/rc.local,在最后把自己写的脚本加上:

/opt/app/service.sh

保存,就完成自启动服务的配置了。

之后,我们可以通过vitualbox的无界面方式启动,然后在xshell中自由操作。

hawtio(hawt.io)是一个开源的监控系统,它提供了多种启动方式,可以运行单独的jar包、war包,然后远程连接其他应用进行监控,也可以将它直接嵌到我们自己的应用中。

本文会介绍在一个单独的java进程(java standalone application)中嵌入hawtio,对应官方文档(http://hawt.io/getstarted/index.html)的 “Using hawtio inside a stand alone Java application”,不过这一节文档问题是比较多的,如果你只看这段,会遇到各种问题。

下面介绍具体步骤

引入jar包

除了官方文档里说的hawtio-embedded外,hawtio-insight-log,hawtio-core,hawtio-web这几个包都是必需的,我们都引入当前的最新版本.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<dependency>
<groupId>io.hawt</groupId>
<artifactId>hawtio-embedded</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>io.hawt</groupId>
<artifactId>hawtio-insight-log</artifactId>
<version>1.5.0</version>
</dependency>
<dependency>
<groupId>io.hawt</groupId>
<artifactId>hawtio-core</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>io.hawt</groupId>
<artifactId>hawtio-web</artifactId>
<version>2.0.0</version>
</dependency>

下载war包

同样在官方start文档(http://hawt.io/getstarted/index.html)中下载hawtio-default.war包,放到任意位置,war包的名字也可以随便改

代码

基本的代码,如果不需要其他配置,非常简单

1
2
3
Main main = new Main();
main.setWar("hawtio.war");
main.run();

使用main.setWar配置的是war包具体路径,可以在工程内,也可以在工程外,但是并非官方文档所说的“包含war包的目录路径(somePathOrDirectoryContainingHawtioWar)”
之后运行main.run就可以启动hawtio了.

权限

新版本的hawtio默认是要密码的,如果想简单,可以配置一条jvm参数: -Dhawtio.authenticationEnabled=false, 关掉权限验证。

示例代码可以在github (https://github.com/lcy362/CamelDemo/blob/master/src/main/java/com/mallow/demo/camel/MainWithHawtio.java) 上看,可以直接下载运行,不过需要在本地启动一个activemq. 这里的例子是一个使用hawtio监控apache-camel的简单例子,启动后,在hawtio页面上栏可以直接看到camel的标签,使用非常方便。

目前,java下应用最广泛的日志系统主要就是两个系列: log4j和slf4j+logback 。

其中,slf4j只包含日志的接口,logback只包括日志的具体实现,两者加起来才是一个完整的日志系统。Log4j则同时包含了日志接口和实现。

这两套日志系统之间有可以相互兼容的组件,分别是slf4j-log4j12和 log4j-over-slf4j,引入之后就可以用log4j打出slf4j接口的日志,或者用logback打出log4j接口的日志。

背景知识介绍到这里,再简单说一下标题里提到的问题。问题的现象就是我们在war包里配置了log4j的日志级别为info, 但在catalina里却一直在打大量的debug日志。初看现象肯定很诡异,前期各种研究tomcat配置也没什么头绪。直到磁盘压力太大,去看jstack发现大量进程是等待在Logback代码中,才发现之前关注错了重点。再去具体了解了java下的日志系统后,问题也就很明了了。

先把几个事实摆出来:
1. 打出的debug日志都是用slf4j写的,根据堆栈得知logback具体执行了日志打印
2. logback在无配置文件时默认debug级别
3. 我们的war包中同时包含logback,log4j和slf4j-log4j12
4. Slf4j无法主动选择具体的日志实现
想必看到这里,大家也明白了问题所在。根据我们引入的包,log4j和logback都可以实现打印slf4j日志,而具体谁来打,不是一个用正常办法可以控制的事情,在这个具体案例下,logback就成了具体的日志打印者。而因为我们其实是想用lo4j打日志,所以没有配logback配置,所以logback就按默认的debug级别打了大量日志。

解决办法也很简单,就是把logback的包全去掉。看似有些暴力,但确实是最合理的一个解决办法。

最后提供一个排查日志问题的通用套路,免得找不到方向乱看。
1. 找几行不符合自己日志配置的具体日志,翻阅对应代码,看看是哪个日志接口打的
2. 查jar包,看看这套日志框架有哪些具体实现
3. 把多的jar包去掉

0%