base64是一种基本的加密算法,在Java中可以使用java自带的base64编码,也可以用apache 的commons-codec包。最近在使用commons-codec 1.10 版本能正常解密微信的消息,升级为1.13后出现了不能正常decode,出现异常

1
2
3
4
5
6
java.lang.IllegalArgumentException: Last encoded character (before the paddings if any) is a valid base 64 alphabet but not a possible value

at org.apache.commons.codec.binary.Base64.validateCharacter(Base64.java:798)
at org.apache.commons.codec.binary.Base64.decode(Base64.java:477)
at org.apache.commons.codec.binary.BaseNCodec.decode(BaseNCodec.java:411)
at org.apache.commons.codec.binary.BaseNCodec.decode(BaseNCodec.java:395)
  1. 具体场景

    处理微信公众号消息时,对消息内容进行必须的加解密,出现的问题是处理aesKey时出现的,具体demo如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public WxOpenCryptUtil(WxOpenConfigStorage wxOpenConfigStorage) {
    /*
    * @param token 公众平台上,开发者设置的token
    * @param encodingAesKey 公众平台上,开发者设置的EncodingAESKey
    * @param appId 公众平台appid
    */
    String encodingAesKey = wxOpenConfigStorage.getComponentAesKey();
    String token = wxOpenConfigStorage.getComponentToken();
    String appId = wxOpenConfigStorage.getComponentAppId();

    this.token = token;
    this.appidOrCorpid = appId;
    this.aesKey = Base64.decodeBase64(encodingAesKey + "=");
    }

    当升级commons-codec版本为1.13及以上时,会出现上述异常

  2. 出现的原因

    1.13出现异常的方法

    1
    2
    3
    4
    5
    6
    7
    private long validateCharacter(final int numBitsToDrop, final Context context) {
    if ((context.ibitWorkArea & numBitsToDrop) != 0) {
    throw new IllegalArgumentException(
    "Last encoded character (before the paddings if any) is a valid base 64 alphabet but not a possible value");
    }
    return context.ibitWorkArea >> numBitsToDrop;
    }

    仔细分析可以看出编解码是在BaseNCodec.java是Base64和Base32的基类

    image-20200330115937553

    可以看出唯一的差别就是在解码时对参数做了校验。有必要了解下这个参数校验做了些什么?

    1
    2
    > context.ibitWorkArea & numBitsToDrop
    >

    ibitWorkArea: 位处理的基本位数

    numBitsToDrop: 应该为空的低位数目

    可以看出当 context.ibitWorkArea & numBitsToDrop不为0时就会抛出异常,实际上只有base64严格模式编码下,才可能会为0,松散模式不会为0

  3. 解决办法

    降低版本到1.12以下可以解决该问题,或者等commons-codec版本更新到1.15,最新的源码已经处理了该问题

    1
    2
    3
    4
    5
    6
    7
    private void validateCharacter(final int emptyBitsMask, final Context context) {
    if (isStrictDecoding() && (context.ibitWorkArea & emptyBitsMask) != 0) {
    throw new IllegalArgumentException(
    "Strict decoding: Last encoded character (before the paddings if any) is a valid base 64 alphabet but not a possible encoding. " +
    "Expected the discarded bits from the character to be zero.");
    }
    }
  4. 扩展

    base64的严格模式和松散模式定义,直接引用源码了

    Lenient: Any trailing bits are composed into 8-bit bytes where possible.
    The remainder are discarded.
    Strict: The decoding will raise an {@link IllegalArgumentException} if trailing bits
    are not part of a valid encoding. Any unused bits from the final character must
    be zero. Impossible counts of entire final characters are not allowed.

References

使用java8的java.util.Base64报“java.lang.IllegalArgumentException: Illegal base64 character d”的问题

Base64笔记

Base64.decode fails on Java11 for certain valid base 64 encoded String

函数式接口

先看一下官方定义

Functional interfaces provide target types for lambda expressions and method references.

可以看出函数式接口主要用于lambda表达式,这类接口只定义了唯一的抽象方法的接口(除了隐含的Object对象的公共方法),一开始也称SAM类型接口(Single Abstract Method)。

阅读全文 »

概念[来自wikipedia]

原码(True form):一个二进制数左边加上符号位后所得到的码,且当二进制数大于0时,符号位为0;二进制数小于0时,符号位为1;二进制数等于0时,符号位可以为0或1(+0/-0)

反码(One’s complement):一种在计算机中数的机器码表示。对于单个数值(二进制的0和1)而言,对其进行取反操作就是将0变为1,1变为0

补码(2’s complement):一种用二进制表示有号数的方法,也是一种将数字的正负号变号的方式,主要优点是 不需因为数字的正负而使用不同的计算方式

计算

正数:原码、反码、补码相同

负数:反码 — 符号位不变化,其余位数取反*,补码 — *符号位不变化,其余各位原码取反+1, 即 反码+1

测试

代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
public void byteTest() {
reverse(0);
reverse(6);
reverse(-6);
reverse(128);
reverse(-128);
reverse(1280);
reverse(-1280);
}

private Integer reverse(Integer a) {
System.out.printf("%5d\t%32s\t%5d\t%32s\t%5d\t%32s%n", a, Integer.toBinaryString(a), ~a, Integer.toBinaryString((~a)), ~a + 1, Integer.toBinaryString((~a + 1)));
return ~a + 1;
}

结果

1
2
3
4
5
6
7
0	                               0	   -1	11111111111111111111111111111111	    0	                               0
6 110 -7 11111111111111111111111111111001 -6 11111111111111111111111111111010
-6 11111111111111111111111111111010 5 101 6 110
128 10000000 -129 11111111111111111111111101111111 -128 11111111111111111111111110000000
-128 11111111111111111111111110000000 127 1111111 128 10000000
1280 10100000000 -1281 11111111111111111111101011111111 -1280 11111111111111111111101100000000
-1280 11111111111111111111101100000000 1279 10011111111 1280 10100000000

可以看出:

  • 对一个数字直接取反,并不是其相反数,需要+1

  • [+0]原码=0000 0000, [-0]原码=1000 0000

    [+0]反码=0000 0000, [-0]反码=1111 1111

    [+0]补码=0000 0000, [-0]补码=0000 0000

众所周知,开源代码都有其开源许可证,详细选择参考如何选择开源许可证?。这里不是要讨论开源许可证的问题,而是开源贡献协议。最近在参加一个开源项目时,提到需要使用DCO贡献协议,所以就去了解当下主流的开源贡献协议。

CLA(Contributor License Agreement)协议

CLA时对开源License的法律性质补充,多由企业或者组织自行定义,作为开源协议的补充,一次性签署,比如alibaba CLA协议: Alibaba Open Source Individual CLA

使用上来说,CLA只需要签署一次,如阿里巴巴个人CLAGoogle CLAPivotal CLACNCF CLA

使用CLA的用户或者组织

  • Facebook
  • Eclipse
  • Go
  • Google
  • InfluxDB
  • Python
  • Elastic
  • CNCF
  • …..

DCO(Developer Certificate of Origin)协议

DCO时Linux Foundation提出的,只有四条简短条文, 具体内容如下: DCO,使用上只需要开发者提交信息时追加 Signed-off-by 即可。具体内容(version1.1)如下:

  1. 该贡献全部或部分由我创建,我有权根据文件中指明的开源许可提交;要么
  2. 该贡献是基于以前的工作,这些工作属于适当的开源许可,无论这些工作全部还是部分由我完成,我有权根据相同的开源许可证(除非我被允许根据不同的许可证提交)提交修改后的工作;要么
  3. 该贡献由1、2、或 3 证明的其他人直接提供给我,而我没有对其进行修改。
  4. 我理解并同意该项目和贡献是公开的,并且该贡献的记录(包括我随之提交的所有个人信息,包括我的签字)将无限期保留,并且可以与本项目或涉及的开源许可证保持一致或者重新分配。

使用方法详见 Probot: DCO, 其实就是提交代码(commit)时增加-s参数,然后你会在提交信息里看到

1
2
3
This is my commit message

Signed-off-by: DEVELOPER <[email protected]>

使用DCO的用户或者组织

  • Gitlab
  • Chef
  • TiKv
  • Apache SkyWalking
  • ……

总结

如果你只是一个commiter,请遵从开源项目的贡献者协议,或者发起issue请求变更,如果是自己的开源项目,更看重法律风险,建议使用CLA,看重社区合作,可以使用DCO。

References

  1. CLA vs. DCO: What’s the difference?
  2. 为何《贡献者许可协议》不利于开源社区?
  3. Move from CLA to DCO #2649 - github.com
  4. probot/dco

支付宝(ISV)

参考文档:https://docs.open.alipay.com/291/106115

签名方式:RSA2

具体实现

初始化 AlipayClient

1
AlipayClient alipayClient = new DefaultAlipayClient(gateway,app_id,private_key,"json",charset,alipay_public_key,sign_type);

具体签名过程

  1. 获取encryptor,加密关键参数部分

    1
    2
    String encryptContent = getEncryptor().encrypt(
    appParams.get(AlipayConstants.BIZ_CONTENT_KEY), this.encryptType, this.charset)

    加密算法

    1
    2
    3
    4
    5
    6
    7
    8
    Cipher cipher = Cipher.getInstance(AES_CBC_PCK_ALG);

    IvParameterSpec iv = new IvParameterSpec(AES_IV);
    cipher.init(Cipher.ENCRYPT_MODE,
    new SecretKeySpec(Base64.decodeBase64(aesKey.getBytes()), AES_ALG), iv);

    byte[] encryptBytes = cipher.doFinal(content.getBytes(charset));
    return new String(Base64.encodeBase64(encryptBytes));
  1. 签名

    1
    2
    3
    String signContent = AlipaySignature.getSignatureContent(requestHolder);
    protocalMustParams.put(AlipayConstants.SIGN,
    getSigner().sign(signContent, this.signType, charset));

    签名算法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    PrivateKey priKey = getPrivateKeyFromPKCS8(AlipayConstants.SIGN_TYPE_RSA,
    new ByteArrayInputStream(privateKey.getBytes()));

    java.security.Signature signature = java.security.Signature
    .getInstance(AlipayConstants.SIGN_SHA256RSA_ALGORITHMS);

    signature.initSign(priKey);

    if (StringUtils.isEmpty(charset)) {
    signature.update(content.getBytes());
    } else {
    signature.update(content.getBytes(charset));
    }

    byte[] signed = signature.sign();

    return new String(Base64.encodeBase64(signed));
  1. 增加accessToken等参数发起访问

微信(ISV)

主要依靠token机制,支持post,get,消息体明文传输

消息内返回需要加密:

加密机制:aes

主要加密逻辑

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
public String encrypt(String plainText) {
String encryptedXml = this.encrypt(genRandomStr(), plainText);
String timeStamp = Long.toString(System.currentTimeMillis() / 1000L);
String nonce = genRandomStr();
String signature = SHA1.gen(new String[]{this.token, timeStamp, nonce, encryptedXml});
return generateXml(encryptedXml, signature, timeStamp, nonce);
}
protected String encrypt(String randomStr, String plainText) {
ByteGroup byteCollector = new ByteGroup();
byte[]a randomStringBytes = randomStr.getBytes(CHARSET);
byte[] plainTextBytes = plainText.getBytes(CHARSET);
byte[] bytesOfSizeInNetworkOrder = number2BytesInNetworkOrder(plainTextBytes.length);
byte[] appIdBytes = this.appidOrCorpid.getBytes(CHARSET);
byteCollector.addBytes(randomStringBytes);
byteCollector.addBytes(bytesOfSizeInNetworkOrder);
byteCollector.addBytes(plainTextBytes);
byteCollector.addBytes(appIdBytes);
byte[] padBytes = PKCS7Encoder.encode(byteCollector.size());
byteCollector.addBytes(padBytes);
byte[] unencrypted = byteCollector.toBytes();

try {
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
SecretKeySpec keySpec = new SecretKeySpec(this.aesKey, "AES");
IvParameterSpec iv = new IvParameterSpec(this.aesKey, 0, 16);
cipher.init(1, keySpec, iv);
byte[] encrypted = cipher.doFinal(unencrypted);
return BASE64.encodeToString(encrypted);
} catch (Exception var14) {
throw new RuntimeException(var14);
}
}

java浮点型数值在运算中会出现精度损失的情况,在业务要求比较高比如交易等场景,一版使用BigDecimal来解决精度丢失的情况。最近一个同事在使用BigDecimal时仍然出现了精度损失,简略记录一下

测试用例

代码如下

1
2
3
4
5
6
7
@Test
public void fd() {
double abc = 0.56D;
System.out.println("abc: " + abc);
System.out.println("new BigDecimal(abc): " + new BigDecimal(abc));
System.out.println("BigDecimal.valueOf(abc): " + BigDecimal.valueOf(abc));
}

输出

1
2
3
abc: 0.56
new BigDecimal(abc): 0.560000000000000053290705182007513940334320068359375
BigDecimal.valueOf(abc): 0.56

可以看到在使用BigDecimal构造器转化浮点型仍然会有损失,而使用valueOf方法则不会出现精度损失。

深入源码

BigDecimal构造器,核心代码(BigDecimal(double val))如下

1
2
3
4
5
6
7
8
9
10
11
public BigDecimal(double val, MathContext mc) {
.....
long valBits = Double.doubleToLongBits(val);
int sign = ((valBits >> 63) == 0 ? 1 : -1);
int exponent = (int) ((valBits >> 52) & 0x7ffL);
long significand = (exponent == 0
? (valBits & ((1L << 52) - 1)) << 1
: (valBits & ((1L << 52) - 1)) | (1L << 52));
exponent -= 1075;
...
}

划重点, Double.doubleToLongBits返回根据IEEE754浮点“双精度格式”位布局,返回指定浮点值的表示

BigDecimal.valueOf核心代码

1
2
3
4
5
6
public static BigDecimal valueOf(double val) {
return new BigDecimal(Double.toString(val));
}
public BigDecimal(char[] in, int offset, int len, MathContext mc) {
....
}

可以看到使用valueOf方法实际上是吧double转为String,再调用string构造器的。

那么为什么使用Double.doubleToLongBits会出现精度损失,而使用string构造器不会呢。主要原因是BigDecimal使用十进制(BigInteger)+小数点(scale)位置来表示小数,而不是直接使用二进制,如101.001 = 101001 * 0.1^3,运算时会分成两部分,BigInteger间的运算以及小数点位置的更新,这里不再展开。

原理浅析

Double.doubleToLongBits为什么会出现精度损失呢,主要原因是因为浮点型不能用精确的二进制来表述,就如十进制不能准确描述无穷小数一样。

浮点型转化为二进制的算法是乘以2直到没有了小数为止,举个栗子,0.8表示成二进制

0.8*2=1.6 取整数部分 1

0.6*2=1.2 取整数部分 1

0.2*2=0.4 取整数部分 0

0.4*2=0.8 取整数部分 0

可以看到上述的计算过程出现循环了,所以说浮点型转化为二进制有时是不可能精确的。

结论

如果想要把浮点型转化为BigDecimal,尽量选择使用valueOf方法,而不是使用构造器。

References

JAVA程序中Float和Double精度丢失问题

罗技鼠标的自定义按键很好远,但自从升级系统到10.15 catalina后自定义按键就失灵了,重启,重装Logitech Options,重新授权…一番折腾后终于找到了彻底的解决办法

环境配置

系统:MacOS 10.15 Catalina

鼠标:MX720

Logitech Options 版本:8.02.86

排查步骤

  1. 修改Security & Privacy 里的 Logi Options Daemon 和 Logi Options 权限,发现已经勾选了
  2. 重新安装Logitech Options勾选权限,仍然无法使用

解决办法

  1. 删除Logi Options和Logi Options Daemon后,再次添加这两项

logi.png

  1. Security & Privacy -> Privacy 中添加Input Monitoring权限

    input.png

  2. 如果需要自定义鼠标截图,还需要添加 Screen Recording权限

结论

仔细看了Catalina的新特性,新系统对于安全和权限管理更加严格了,所以需要单独处理,至于Logitech Options需要的权限,可以参考Logitech Options permission prompts on macOS Catalina and macOS Mojave,新系统中其他软件遇到类似的问题,都可以通过这种方式解决

References

Logitech Options 在 Mac 下的自定义按键经常会失灵

Logitech Options permission prompts on macOS Catalina and macOS Mojave

Nacos 1.2.0版本以前是不支持MySQL8.0,如果出现配置保存不了,500的错误,多是由于数据源的问题,需要修改源码以支持MySQL8.0。

从github克隆源码

git clone https://github.com/alibaba/nacos.git

修改pom驱动版本(最外层pom)

mysql mysql-connector-java 8.0.19

修改源码引用

位置 nacos/naming/src/main/java/com/alibaba/nacos/naming/healthcheck/MysqlHealthCheckProcessor.java

// import com.mysql.jdbc.jdbc2.optional.MysqlDataSource;原本引用的类
import com.mysql.cj.jdbc.MysqlDataSource;

打包

1
2
3
cd nacos/
mvn -Prelease-nacos -Dmaven.test.skip=true clean install -U
ls -al distribution/target/nacos-server-1.2.0-SNAPSHOT/nacos

注意修改targeta下的jar包名为 nacos-server.jar

修改conf里的配置文件

启动

进入bin目录,以官方提供的方式启动 sh startup.sh -m standalone

异常

如果出现异常,可以通过logs/nacos.log查看具体的启动异常

References

Nacos 配置 MySQL8数据库

Nacos 快速开始

问题来源

问题来源于一次串行使用CompletableFuture和Stream导致CompletableFuture异步失效的问题,问题代码:

1
2
3
4
5
6
7
8
9
10
11
12
int size = 50;
List<Double> res = Stream.iterate(0, i -> i + 1).limit(size).map(i -> CompletableFuture.supplyAsync(() -> {
Double re = BigDecimal.valueOf(10 * (Math.sin(i) + 1)).setScale(2, RoundingMode.HALF_UP).stripTrailingZeros().doubleValue();
try {
TimeUnit.SECONDS.sleep(re.intValue());
} catch (InterruptedException e) {
e.printStackTrace();
}
return re;
}, ThreadUtil.fixed()))
.map(CompletableFuture::join)
.collect(Collectors.toList());

其中ThreadUtil.fixed()是自己封装的线程工具方法,也可以使用Executors.newFixedThreadPool代替,原本是想通过CompletableFuture拿到异步执行的结果并进行处理,串联使用stream后反而未起到作用,先说解决方法:

  1. 先收集 future 结果到list,再调用新的流运算,即 .map(CompletableFuture::join)方法
  2. limit(size).map 之间添加 parallel() 方法,形成 parallelStream()d的形式

原因分析:普通的stram可以理解为单纯的foreach循环,每生成一个future立即join,出现异步变同步的现象。

再进一步,既然CompletableFuture和parellStream都可以并行执行任务,有必要比较一下。

parallelStream

先试用 parallelStream 重写上述方法

1
2
3
List<Double> result = Stream.iterate(0, i -> i + 1).limit(50).parallel().map(i -> {
///.....
}).collect(Collectors.toList());

用时:54018ms

想要深入了解parallelStream,需要先了解ForkJoin框架和ForkJoinPool框架。这里简单介绍一下ForkJoinPool,真正了解 ParallelStream 还是需要先弄懂ForkJoinPool的,在此只是简单比较两者功能,不做深入探讨。

ForkJoinPool 使用分治法(Divide-and-Conquer Algorithm)来解决问题,实现了ExecutorService接口,线程数量可以通过构造器传入,默认使用机器的CPU数量。和ThreadPoolExecutor有一定区别,ForkJoinPool可以在运行线程中创建新的任务,并挂起当前的任务,此时线程就能够从队列中选择子任务执行,而ThreadPoolExecutor做不到这一点的。ForkJoinPool的核心算法是工作窃取算法,这样就可以在使用少量的线程来完成大量的任务。比如说ForkJoinPool 4个线程可以处理200完个任务,ThreadPoolExecutor显然是不可行的。

工作窃取算法的优点是充分利用线程进行并行计算,并减少了线程间的竞争,其缺点是在某些情况下还是存在竞争,比如双端队列里只有一个任务时。并且消耗了更多的系统资源,比如创建多个线程和多个双端队列。

ParallelStreams中java8为ForkJoinPool添加了通用线程池,默认线程数量为机器的处理器数量。可以通过 -Djava.util.concurrent.ForkJoinPool.common.parallelism=N来设置ForkJoinPool的线程数量。

拆分成两步重写上述方法

1
2
3
4
5
List<CompletableFuture<Double>> futures = Stream.iterate(0, i -> i + 1).limit(50).map(i -> CompletableFuture.supplyAsync(() -> {
///.....
}, ThreadUtil.fixed())).collect(Collectors.toList());
List<Double> ids = futures.stream().map(CompletableFuture::join)
.collect(Collectors.toList());

用时:57111ms, 通过增加线程数量可以减少执行时间。

References

Java 多线程中的任务分解机制-ForkJoinPool,以及CompletableFuture

CompletableFuture 组合式异步编程