使用maven version:set分离服务的发布版本


开发过程中需要分离测试环境依赖和线上环境依赖。

举个简单的例子,有个依赖包hexo-api,在正式环境中是version=hexo-api-1.0,测试环境中是version=hexo-api-1.0-SNAPSHOT。

但实际项目中pom.xml是这样配的,如:

1
2
3
4
5
6
7
...
<groupId>com.qinguan</groupId>
<artifactId>hexo</artifactId>
<name>hexo-api</name>
<packaging>jar</packaging>
<version>1.0</version>
...

我不想因为发布一个测试版本1.0-SNAPSHOT的jar而去提交一个如下的变更。

1
<version>1.0-SNAPSHOT</version>

那怎么办呢?可以试试maven-version-plugin

具体用法:

1
2
3
4
5
//假定之前打包命令
clean compile -Papi deploy

//测试包打包命令
clean versions:set -DnewVersion=1.0-SNAPSHOT compile -Papi deploy

通过versions:set更改pom.xml中的version值,newVersion参数为目标版本号。

此时打开pom.xml,可以看到文件中的version值已由1.0更新为1.0-SNAPSHOT了,jar包也变成了hexo-api-1.0-SNAPSHOT.jar。

Thrift枚举对象兼容性问题


最近在一个基于Thrift框架的RPC服务中碰到一个枚举值的兼容性问题。
简单描述如下:
有一个位置定义,起初只有两个枚举值,LEFT和RIGHT。然后发布schema定义版本1.0,客户端服务引用version = 1.0。

1
2
3
4
5
// commom.thrift
enum Position {
LEFT =0,
RIGHT =1,
}

经过一段时间,Position需要扩展,增加UP和DOWN,发布schema定义版本2.0。

1
2
3
4
5
6
7
// commom.thrift
enum Position {
LEFT =0,
RIGHT =1,
UP =2,
DOWN =3,
}

此时,客户端还在使用schema=1.0版本。若服务端返回了Position.UP或者Position.DOWN数据到客户端,客户端是不识别这两个枚举值的。问题就这样产生了,客户端有可能因为没有对未知数据进行兼容处理而直接报错异常了。

实际上,有几种设计是违反向后兼容原则的,上述情况就是典型的一种,即向枚举类型中新增枚举值。这种场景下,原则上要求所有客户端均先于服务端进行升级到最新版本。鉴于分布式服务应用场景,要求所有客户端都统一升级,是几乎不可能实现的。

针对向已有枚举类型中新增枚举值的情况,Thrift和Protobuf各有一些处理:

  1. Thrift:针对不认识的枚举值,默认返回null(通过findByValue实现)
  2. Protobuf:针对不认识的枚举值,默认返回枚举类型的第一个值(或者返回default value,如有)
    • 注: 所以Protobuf定义的schema enum一般都会设第一个值为UNKNOWN以兼容低版本客户端

在阿里巴巴的Java编码规范中也有相关规则:强制“接口返回值不允许使用枚举类型或者包含枚举类型的POJO对象”

Thrift/Protobuf协议本身对新增枚举值的处理没有问题(至少在反序列化方法中没有简单粗暴地直接抛出异常),但对业务层的处理及使用提出了一定的要求,即业务层有可能读到null(Thrift)或者UNKONWN类型枚举(Protobuf),需要做一定的防御性判断。

进一步阅读资料:

  1. thrift-versioning-doc
  2. Updating A Message Type

一个内部API高度耦合的Java实现案例


某项目使用了“一套API,多种底层存储”的设计方式,可以灵活切换多种数据源,但同时能够保持暴露的API接口定义一致。这种设计方式,优点是显而易见的。

随着业务的增长,数据量越来越大。单种类存储已经捉襟见肘,需要依赖多种存储共同保障服务的状况下,早期高度耦合的内部API实现使得存储的改造工作量增大了不少。

以某项目为例,原本业务数据量没那么大,选择了Redis作为线上业务的主要存储,以Couchbase为辅,同时利用HBase实现了一套在线冷备服务,其中RedisAPI和HBaseAPI互斥。但随着数据量的突飞猛进,Redis的容量已不堪重负,计划全部迁移到容量更大的分布性缓存Couchbase。同时,在Couchbase存储未命中时,回源到HBase服务进行兜底读取。

由于上层业务逻辑基于同一套存储API实现的,为同时使用多种存储,要求对这套存储API进行改造。

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
//common jar
public interface BaseTestRepo {

default public <T> T get(String key) {
//TODO
throw new NotImplementedException("not implement!");
}

default public void set(String key, byte[] value) {
//TODO
}
}

//couchbase.repo jar
@Repository("cbDataRepo")
public class CocuhbaseRepo implements BaseTestRepo {
}

//hbase.repo jar
@Repository("dataRepo")
public class HBaseRepo implements BaseTestRepo {
//TODO
}

//redis.repo jar
@Component("dataRepo")
public class RedisRepo implements BaseTestRepo {
//TODO
}

//serviceA
@Service
public class TestAService {
@Autowired
BaseTestRepo dataRepo;

@Autowired
BaseTestRepo cbDataRepo;

public <T> T getEntity(String key) {
throw new NotImplementedException("not implement!");
}

public void setEntity(String key, Object value) {
//TODO
}
}

//serviceB
@Service
public class TestBService {
@Autowired
BaseTestRepo dataRepo;

@Autowired
BaseTestRepo cbDataRepo;

//通用API
public <T> T getEntity(String key) {
return dataRepo.get(key);
}

//通用API
public void setEntity(String key, Object value) {
dataRepo.set(key, toByte(value))
}

//TODO
}

上层业务模块A需要依赖redis的高效部署业务,则可以引入service.jar、redis.repo jar、common.jar、couchbase.repo jar来实现;
上层业务模块B需要依赖hbase来实现离线任务,则可引入service.jar、hbase.repo jar、common.jar、couchbase.repo jar来实现。

如上所示,上层业务模块只需要按需导入hbase.repo jar或者redis.repo jar,然后利用Spring自动注入机制,获取dataRepo实例,基于同一套get/set API进行存储的读写操作。对上层业务而言,它是无需感知底层业务代码使用了哪一套存储。

此时,若将RedisRepo全部迁移到CouchbaseRepo,则会涉及到dataRepoAPI的使用变更,redis部分变化,但必须保持HBase不变。

解决方案:
1、新增Proxy,将底层RepoAPI再加一层代理,按需路由到不同的repo实现。
2、基于Spring Condition条件化构建Repo Bean。

话说回来,这类还是场景考虑不全,实现的不够理想。

Netty版本冲突引起Cassandra访问异常


Java RPC服务单元测试访问Cassandra时,发现始终报如下错误:

1
2
3
java.lang.AbstractMethodError: 
io.netty.util.concurrent.MultithreadEventExecutorGroup.newChild(Ljava/util/
concurrent/Executor;[Ljava/lang/Object;)Lio/netty/util/concurrent/EventExecutor

排查了一圈,最终发现是Netty引起的。
Java RPC服务依赖的是Netty3.X版本,Cassandra驱动测试包依赖的是Netty4.X版本,将Netty版本调整成一致解决。

RMI反序列化漏洞


接到公司安全组提的漏洞单,说部分Java机器开了jmxremote端口,存在安全隐患。

查了下原来有不少Java服务默认开启了非安全模式的jmxremote端口。

修复(降低风险)方案有如下几种方式:

  1. 使用SerialKiller,详细的在RMI反序列化漏洞文章中有介绍,略复杂
  2. 对于jmxremote没有强依赖的服务,可以直接禁用jmxremote端口,删除相关配置项
  3. 授权访问:配置认证密码或者ssl,或者配置ip白名单 -Dcom.sun.management.jmxremote.host=serverXXX

关于RMI反序列化漏洞的介绍:
http://blog.nsfocus.net/java-deserialization-vulnerability-overlooked-mass-destruction/
关于JMX配置的一些介绍:
https://docs.oracle.com/javase/7/docs/technotes/guides/management/agent.html
https://docs.oracle.com/javase/8/docs/technotes/guides/management/agent.html

使用charles抓取iOS数据包


由于分析竞手的项目涉及到iOS端的请求抓包,翻阅并验证了一下,以下是整理的大致抓包流程。

工具软件: charles 3.11.5版本

  1. 安装mac端证书
    • Help->SSL Proxy->Install Charles Root Certificate.
  2. 开启代理设置
    • Proxy->Proxy Setting…->Proxies
    • HTTP Proxy 可以默认一个端口如8888,并勾选Enable transparent HTTP proxying
    • Socks Proxy可以Enable,并且Enable HTTP proxying over SOCKS,设置端口如80,443等
  3. 开启SSL代理(支持HTTPS抓包)
    • Proxy->SSL Proxying Setting,选中Enable SSL Proxying
    • 在Locations里面添加*:443,表示匹配所有的HTTPS网站
  4. 给iPhone安装证书
    • Help->SSL Proxy->Install Charles Root Certificate On a Mobile Device or remote Browers…
    • 根据弹出的提示框进行操作,也就是用iPhone的Safari打开地址http://charlesproxy.com/getssl 下载安装。
    • iOS11以上系统需要在设置 > 关于本机 > 证书信任设置,信任该证书。
  5. 手机连接wifi(同电脑一个局域网或者直接连电脑的热点),配置HTTP代理,选择’手动’,输入电脑的ip地址和端口号(默认8888)

Java import 失效


下午碰到一个小问题,IDEA 中的springboot项目里所有的import都无效了,显示unused import statment。

解决方案:File->Invalidate Caches/Restart,选择Invalidate and Restart。

看不见的字符-<200b>


今天配置一个数据库链接地址时,报URL地址有问题,但在sublime Text里明明是正常的。

1
http://shgq.1.abc:1234/pools;http://shgq.2.abc:1234/pools#xxf

打开debug日志发现是地址解析错误,无法识别<200b>,但字符串中没有这个<200b>,是哪里冒出来的呢?

于是,我重新把配置贴到用vim打开的文本中,才发现了隐藏字符。

1
http://shgq.1.abc:1234/pools;http://shgq.2.abc:1234<200b>/pools#xxf

搜了一把这个<200>,学名zero-width space(ZWSP),是一种不可见的打印字符。

wikipedia给出一种实际的例子,可以很明显地看出区别,如下:

1
Lorem​Ipsum​Dolor​Sit​Amet​Consectetur​Adipiscing​Elit​Sed​Do​Eiusmod​Tempor​Incididunt​Ut​Labore​Et​Dolore​Magna​Aliqua​Ut​Enim​Ad​Minim​Veniam​Quis​Nostrud​Exercitation​Ullamco​Laboris​Nisi​Ut​Aliquip​Ex​Ea​Commodo​Consequat​Duis​Aute​Irure​Dolor​In​Reprehenderit​In​Voluptate​Velit​Esse​Cillum​Dolore​Eu​Fugiat​Nulla​Pariatur​Excepteur​Sint​Occaecat​Cupidatat​Non​Proident​Sunt​In​Culpa​Qui​Officia​Deserunt​Mollit​Anim​Id​Est​Laborum

通过vim编辑时变成如下文本,中间多了很多<200b>

1
Lorem<200b>Ipsum<200b>Dolor<200b>Sit<200b>Amet<200b>Consectetur<200b>Adipiscing<200b>Elit<2b>Sed<200b>Do<200b>Eiusmod<200b>Tempor<200b>Incididunt<200b>Ut<200b>Labore<200b>Et<200b>Dole<200b>Magna<200b>Aliqua<200b>Ut<200b>Enim<200b>Ad<200b>Minim<200b>Veniam<200b>Quis<200b>Norud<200b>Exercitation<200b>Ullamco<200b>Laboris<200b>Nisi<200b>Ut<200b>Aliquip<200b>Ex<200ba<200b>Commodo<200b>Consequat<200b>Duis<200b>Aute<200b>Irure<200b>Dolor<200b>In<200b>Reprehderit<200b>In<200b>Voluptate<200b>Velit<200b>Esse<200b>Cillum<200b>Dolore<200b>Eu<200b>Fugi<200b>Nulla<200b>Pariatur<200b>Excepteur<200b>Sint<200b>Occaecat<200b>Cupidatat<200b>Non<20>Proident<200b>Sunt<200b>In<200b>Culpa<200b>Qui<200b>Officia<200b>Deserunt<200b>Mollit<200bnim<200b>Id<200b>Est<200b>Laborum

如果使用chrome浏览器,打开检查功能查看wikipedia的这个例子,在Elements Tab页下面,找到上述字符串对应的html页面代码,可以看到上面<200b>均表示成了&#8203;

好了,再测试一下,​前面隐藏了一个分隔符!不信的话,试试复制一下,用vim编辑看看。:-D

零宽字符有什么用呢,可以作为内容的水印,这是另外一个话题了,可以发掘利用一下。

以后粘贴复制文本时需要多长一个心眼了。

nginx resolver数据缓存及解析超时时间参数


在配置nginx的resolver时,有两个与时间相关的参数。

一、valid

1
2
3
4
5
6
Syntax: resolver address ... [valid=time] [ipv6=on|off];
Default: —
Context: http, server, location

如:
resolver 127.0.0.1 [::1]:5353 valid=30s;

valid flag means how long nginx will consider answer from resolver as valid and will not ask resolver for that period.
即DNS返回结果的缓存时间,在缓存有效时间内,nginx不会再次向DNS请求解析结果。

二、resolver_timeout

1
2
3
4
Syntax: resolver_timeout time;
Default:
resolver_timeout 30s;
Context: http, server, location

resolve_timeout sets how long nginx will wait for anwser from resolver (DNS).
即nginx发起的DNS查询请求超时时间。

更多参考:

SLF4J及日志栈溢出问题


新接手一项目,在本地测试运行时,发现报如下错误:

1
Detected both log4j-over-slf4j.jar AND slf4j-log4j12.jar on the class path, preempting StackOverflowError.

很明显,是项目的classpath中同时存在log4j-over-slf4j.jar和slf4j-log4j12.jar,引起死循环。但为什么项目在线上能好好运行呢?
原来线上运行的程序在打包时做了手脚,在最终的依赖包中,删除了log4j和slf4j-log4j12,如下:

1
2
3
4
5
6
7
8
9
10
<dependencySets>
<dependencySet>
<outputDirectory>lib</outputDirectory>
<excludes>
<exclude>log4j:log4j</exclude>
<exclude>org.slf4j:slf4j-log4j12</exclude>
</excludes>
<useProjectArtifact>false</useProjectArtifact>
</dependencySet>
</dependencySets>

让我们来重头捋一下Java日志框架及该问题的成因。

我们用的最多的几个日志框架有Log4j、Jakarta Commons Logging以及Logback-classic。尤其值得一提的是Logback-classic,它是Log4j作者写的又一个开源日志组件,由于实现了SLF4J’s的Logger接口,native,性能优越,可以直接作为SLF4J的实现。

SLF4J,全称Simple Logging Facade for Java,是一个抽象的日志框架,支持程序动态绑定日志框架实现,如绑定到java.util.logginglogback以及 log4j等,使用时需要在应用中添加依赖slf4j-api。

SLF4J对应用日志做了封装抽象,因此,业务可以针对SLF4J进行编程,再按需绑定具体的日志框架。无论是灵活性还是可扩展性,都有很多的提升。

为了支持动态绑定不同的日志框架,SLF4J提供了几个绑定工具,如下:

绑定 说明
slf4j-log4j12 Log4j绑定,Log4j是一个使用非常广泛的日志组件
slf4j-jcl Jakarta Commons Logging绑定
slf4j-jdk14 java.util.logging, JDK绑定
slf4j-simple 输出会定向到System.err,只有INFO级别以上的才会输出
slf4j-nop 丢弃日志绑定

为了方便jcl、log4j、jul用户迁移到SLF4J,SLF4J提供了几个桥接模块。

模块 目标对象 使用说明 实现原理
log4j-over-slf4j log4j用户 将log4j.jar替换为log4j-over-slf4j.jar 直接替换了log4j中的大量同名类,如Logger、Category、Level等
jcl-over-slf4j jcl用户 将commons-logging.jar替换为jcl-over-slf4j.jar 保留了jcl的接口,但底层实现采用了slf4j
jul-to-slf4j jul用户 直接依赖 由于jul在java.*namespace下,无法替换。该模块采用了翻译手段,将LogRecord转换为slf4j类似对象。 该方案存在一定的性能损失,不推荐使用

按上面的介绍,使用slf4j时就需要注意一下几个问题:

  1. jcl-over-slf4j.jarslf4j-jcl.jar不能同时依赖!

    • 原因:jcl会将实现委托到slf4j,而slf4j又会绑定到jcl。
  2. log4j-over-slf4j.jarslf4j-log4j12.jar不能同时依赖!

    • 原因: log4j会将实现委托到slf4j,而slf4j又会绑定到log4j实现。
  3. jul-to-slf4j.jarslf4j-jdk14.jar不能同时依赖!

    • 原因:jul会将实现委托到slf4j,而slf4j又会绑定到jdk实现。

上述三个情况下,若同时依赖均会产生循环依赖问题,导致StackOverflowError!

回到开头的问题,即上述第2个注意问题,怎么解决也已经很明显了。

参考资料

  1. SLF4J user manual
  2. Bridging legacy APIs