KafkaProducer.send()在获取metadata时会阻塞主线程

根据Kakfa的文档,KafkaProducer.send()方法是异步的,一直以来我也是这样认为的:

* The {@link #send(ProducerRecord) send()} method is asynchronous. When called it adds the record to a buffer of pending record sends and immediately returns. This allows the producer to batch together individual records for efficiency.

最近我们改为使用阿里云提供的Kafka服务。迁移后发现开发环境中调用了KafkaProducer.send()的方法执行时间有时会变得异常长,排查后发现是KafkaProducer.send()在获取metadata时导致的。

之前使用自己搭建的Kafka集群时,连接速度快,因而没有发现这一情况。改为使用阿里云的Kafka集群后,由于开发环境和Kafka集群不在一个VPC下,需要使用公网地址访问,连接速度较慢(时间长达1~2秒),这一情况才暴露出来。

查看KafkaProducer的代码,发现doSend()方法中,第一步就是调用waitOnMetadata()方法获取topic的metadata信息:

private Future<RecordMetadata> doSend(ProducerRecord<K, V> record, Callback callback) {
    TopicPartition tp = null;

    try {
        KafkaProducer.ClusterAndWaitTime clusterAndWaitTime = this.waitOnMetadata(record.topic(), record.partition(), this.maxBlockTimeMs);
        …………

而waitOnMetadata()方法中使用do-while循环来获取metadata信息,直至获取成功或抛出异常,因而阻塞在此:

private KafkaProducer.ClusterAndWaitTime waitOnMetadata(String topic, Integer partition, long maxWaitMs) throws InterruptedException {
    this.metadata.add(topic);
    Cluster cluster = this.metadata.fetch();
    Integer partitionsCount = cluster.partitionCountForTopic(topic);
    if (partitionsCount == null || partition != null && partition >= partitionsCount) {
        long begin = this.time.milliseconds();
        long remainingWaitMs = maxWaitMs;

        long elapsed;
        do {
            log.trace("Requesting metadata update for topic {}.", topic);
            int version = this.metadata.requestUpdate();
            this.sender.wakeup();

            try {
                this.metadata.awaitUpdate(version, remainingWaitMs);
            } catch (TimeoutException var15) {
                throw new TimeoutException("Failed to update metadata after " + maxWaitMs + " ms.");
            }

            cluster = this.metadata.fetch();
            elapsed = this.time.milliseconds() - begin;
            if (elapsed >= maxWaitMs) {
                throw new TimeoutException("Failed to update metadata after " + maxWaitMs + " ms.");
            }

            if (cluster.unauthorizedTopics().contains(topic)) {
                throw new TopicAuthorizationException(topic);
            }

            remainingWaitMs = maxWaitMs - elapsed;
            partitionsCount = cluster.partitionCountForTopic(topic);
        } while(partitionsCount == null);

        if (partition != null && partition >= partitionsCount) {
            throw new KafkaException(String.format("Invalid partition given with record: %d is not in the range [0...%d).", partition, partitionsCount));
        } else {
            return new KafkaProducer.ClusterAndWaitTime(cluster, elapsed);
        }
    } else {
        return new KafkaProducer.ClusterAndWaitTime(cluster, 0L);
    }
}

解决方法很简单,自己开一个线程来调用KafkaProducer.send()就可以了。只不过这里不得不吐槽一下Kafka的文档,在这点上写的实在是有误导。

Spring MVC项目改为Spring Boot后上传文件MultipartFile为空

前因:

原有一Spring MVC项目,因公司整体改为Kubernetes+Docker的容器化部署方式,而Spring MVC项目制作Docker镜像过于繁琐,故将该项目改为Spring Boot项目方便打包为Docker镜像。

表征:

改造后其他功能全部正常,唯上传文件功能不能正常使用,排查后发现上传文件的Controller接收的MultipartFile为空。

原因:

项目虽已改造为Spring Boot项目,但仍使用xml文件进行配置,也没有修改过这些配置文件,因而仍然在applicationContext.xml中配置了CommonsMultipartResolver Bean。而Spring Boot会自动配置一个MultipartResolver Bean,两者冲突,导致获取MultipartFile为空。

前后端时区不一致导致的问题

出于安全考量,项目前后端在进行数据交互时,对数据进行了签名校验,算法可以归纳为:sign=MD5(要传输的数据+指定的特殊字符串+今天的第一秒)。为了防止前后端时间出现偏差(如前端在前一天23:59:59发送请求,后端在第二天0:0:0接收到请求),后端在校验时,如拼接今天的第一秒校验不通过,则还会尝试拼接昨天的第一秒和前天的第一秒,都不通过时才报错。

这套方法一直运作的很好,直到今天为了测试其他一个问题,修改了客户端的时区——当客户端时区修改后,便无论如何都无法通过校验了。

原因在于,每个时区的“今天的第一秒”的时间戳是不同的,服务器的时区是东八区,一旦客户端不是东八区,两者必然不相同。解决方法倒也很是简单,两边强制用一样的时区就好了——两边都转换为格林尼治时间,或者将客户端的时间转换为东八区时间都可以。

修改表结构前未关闭Druid PreparedStatementCache导致异常的总结

问题现象:

本次需求需要在表中增加一个字段,DBA操作后,系统立刻出现大量报警。查看日志后,发现异常出现在数据库查询阶段:

image2018-11-8_15-31-57

在对报警信息统计后,异常有如下三种:

java.sql.SQLException: 违反协议

java.sql.SQLException: OALL8 处于不一致状态

java.sql.SQLException: Io 异常

重启应用后,异常不再抛出。

 

问题原因:

  1. 为提高访问数据库的效率,开启了druid的PreparedStatementCache(以下简称为PSCache),在修改表结构前未将其关闭。
  2. 修改表结构后,因为未关闭PSCache,其中缓存的PreparedStatement与修改后的表结构不一致,因而抛出异常,其中,java.sql.SQLException: 违反协议是SQL中包含修改了表结构的表时报出的;java.sql.SQLException: OALL8 处于不一致状态是SQL不包含修改了表结构的表时报出的。
  3. 系统中使用的druid版本较低(1.0.21),在PreparedStatement执行出错后,不能将错误的PreparedStatement释放掉,而仍将其放回池子。
  4. 另外,理论上java.sql.SQLException: 违反协议异常会导致整个Connection不可用,druid有一套释放Connection的机制,如果这套机制正常执行,无论错误的PreparedStatement是否被释放,Connection都会被释放,异常也不会一直发生。然而由于低版本druid(1.0.29前)的这套释放Connection机制不兼容系统所配置的低版本Oracle Driver(oracle.jdbc.driver.OracleDriver),Connection未能被释放,最终导致异常一直发生。

 

源码分析:

在1.0.27版本前,closePoolableStatement方法的代码如下:

image2018-11-8_16-35-48

可以看到,druid不关注PreparedStatement在执行时是否出现异常,关闭时都会将它放回PreparedStatementPool中。

在1.0.27版本后,在DruidPooledStatement中增加了一个用于记录exception数量的int,在catch住异常后调用的checkException方法中自增:

image2018-11-8_18-33-14

image2018-11-8_18-27-39

在closePoolableStatement时,若exception数量不等于0,则不将其放回PreparedStatementPool中:

image2018-11-8_18-26-10

一切看起来很美好对不对?然而并不,即使更新了1.0.27版本的druid,如果使用了旧版本的Oracle Driver(oracle.jdbc.driver.OracleDriver),这一问题仍然存在。原因在于java.sql.SQLException: 违反协议(ORA-17401)这一异常是导致Connection无法修复的Fatal异常,应当将Connection也抛弃,然而1.0.27版本的druid存在Bug,仍然将应当抛弃的这个Connection扔回了池子。

仔细阅读源码,在对PreparedStatement执行时的expection进行计数+1后,还调用了DataSource的handleCollectionException方法进行处理:

image2018-11-8_19-57-19

image2018-11-8_19-58-47

而在这个handleCollectionException方法中,会调用ExceptionSorter,判断发生的异常是不是指定错误码的、会导致Connection无法修复的Fatal异常,若是,druid会将这个Connection抛弃掉:

image2018-11-8_20-5-4

而修改表结构导致的java.sql.SQLException: 违反协议(ORA-17401)异常是包含在这一列表中的,理论上应该将这个Connection直接抛弃。然而在1.0.29版本的druid之前,druid在生成ExceptionSorter时只对新版本的Oracle Driver(oracle.jdbc.OracleDriver)指定了对应的ExceptionSorter,对于旧版本的Oracle Driver(oracle.jdbc.driver.OracleDriver)则并未指定,如果使用旧版本的Oracle Driver,就不能获取到这个判断Connection是否需要抛弃的ExceptionSorter,自然也就无法根据错误码将Connection抛弃了:

image2018-11-8_20-19-4

在1.0.29版本后,这段代码中增加了对旧版本的Oracle Driver的兼容,解决了这一问题:

image2018-11-8_20-19-4

 

解决方案:

  1. 执行修改表结构前,务必将相关系统的PSCache关闭。目前使用了PSCache却未提供开关的系统,需增加PSCache的开关。
  2. 升级druid连接池的版本至1.0.29以上,这样即使修改表结构前忘记关闭PSCache,出现异常后,出错的PreparedStatement和Connection也会被直接抛弃,不会一直报错。
  3. 将配置文件中的Oracle Driver统一为新版本的oracle.jdbc.OracleDriver

Java默认不支持AES256的解决方法(java.security.InvalidKeyException: Illegal key size or default parameters)

近日在做微信支付相关的业务,微信支付对部分接口的返回值(如退款结果通知回调接口)做了AES256的加密。然而Java默认是不支持256位的AES的,因而在解密时会报java.security.InvalidKeyException: Illegal key size or default parameters异常。

Oracle做这个限制的原因非常令人无语:美国在《武器出口管制法》中,将一定长度位数以上的加密算法视为武器,因而除非获得国防部和国家安全局的批准,否则禁止出口——即使这种算法是公开的。

在网上搜寻到了解决方法后,让运维对开发、测试环境JDK中的jar包做了替换,本来以为问题就此解决了,在线上环境操作时却发现了新的问题:为什么线上的JDK8里没有要替换的两个jar包?再次搜寻之后才发现,原来对于不同小版本的JDK8,解决方法是不一样的,而网上大部分解决方法只介绍了低版本JDK8的。

因而,在这里贴一下针对不同版本JDK的详细解决方案:

JDK 1.8.0_151前的版本:

JDK中只内置了不支持AES256的jar包,需要手动下载支持AES256的jar包并进行替换:

  1. 下载Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files:
    https://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.htmlJDK6、JDK7的下载链接不同,在此一并贴出:
    JDK6:https://www.oracle.com/technetwork/java/javase/downloads/jce-6-download-429243.html
    JDK7:https://www.oracle.com/technetwork/java/javase/downloads/jce-7-download-432124.html
  2. 解压缩,将US_export_policy.jarlocal_policy.jar覆盖JDK安装目录\jre\lib\security\下的同名文件。

JDK 1.8.0_151后的版本:

JDK中内置了两种jar包,但默认启用不支持AES256的,需要修改配置文件以启用支持AES256的jar包:

修改JDK安装目录\jre\lib\security\java.security,将crypto.policy=unlimited这条配置前的注释#号删除:

从:

QQ截图20190426184840

改为:

QQ截图20190426184919

Java字节码操纵框架ASM入门

一、ASM是什么

ASM是一个Java字节码操纵框架,能被用来动态生成新类,或对既有类的内容进行增、删、改。
ASM可以直接产生二进制class文件,也可以在类被加载入Java虚拟机之前动态改变类行为。

二、使用ASM配合JavaAgent,在类被加载入Java虚拟机之前动态改变类

1、实现一个含有premain(String args, Instrumentation inst)方法的类,在premain(String args, Instrumentation inst)方法中,调用Instrumentation.addTransformer(ClassFileTransformer transformer)方法。

package cn.houseyoung.asm;

import java.lang.instrument.Instrumentation;

/**
* Created by houseyoung on 2018/9/13.
*/
public class StartAgent {
    public static void premain(String args, Instrumentation inst) {
        inst.addTransformer(new MyClassFileTransformer());
    }
}

2、实现ClassFileTransformer类及其中的byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer)方法,在该方法中,使用ASM框架对传入的字节数组形式的class文件classfileBuffer进行操纵,返回操纵完毕后的class文件字节数组。

3、在manifest.mf文件中指定premain()方法所在的类:

Premain-Class: cn.houseyoung.asm.StartAgent

4、将包含premain()方法的类打为jar包。

5、用如下方式运行Java程序:

java -javaagent:jar 文件的位置 [= 传入 premain 的参数 ]

 

三、ASM使用的设计模式——访问者模式简介

元素提供accept方法,该方法传入访问者对象,执行访问者提供的visit方法。
访问者提供visit方法,用于访问具体元素,并对之进行操作。
这种模式的优势在于,能在不修改元素的情况下添加对于元素访问的操作,只需要让元素accept一个新的访问者即可。
但在增加/修改元素内容后,所有的访问者类都需要修改相应的visit方法,所以访问者模式并不适合元素类频繁变动的场景,这也是访问者模式自身最大的缺陷。
对于ASM来说,ClassReader元素类来源于Java class,而Java class的格式几乎不变,因而规避了这一缺陷。

四、ASM API三大核心组件

ASM提供了三个基于ClassVisitor API的核心组件,用于生成和变化类:

ClassReader类分析以字节数组形式给出的已编译类,并针对在其accept()方法参数中传送的ClassVisitor实例,调用相应的visitXXX()方法。

ClassWriter类是ClassVisitor抽象类的一个子类,它直接以二进制形式生成编译后的类,它会生成一个字节数组形式的输出,其中包含了已编译类,可以用toByteArray()方法来提取。

ClassVisitor类将它收到的所有方法调用都委托给另一个ClassVisitor类。

时序图:

asm

 

五、ASM中的类信息访问次序

ASM依据对类信息的遍历顺序进而调用不同的visit方法,其访问顺序为:

visit
visitSource?
visitOuterClass?
( visitAnnotation | visitAttribute)*
( visitInnerClass | visitField | visitMethod)*
visitEnd

即,必须先调用visit()方法;然后调用0或1次visitSource()方法;接着调用0或1次visitOuterClass()方法;然后以任意顺序和任意次数(0、1或多次)调用visitAnnotation()和visitAttribute()方法;接着以任意顺序和任意次数(0、1或多次)调用visitInnerClass()、visitField()和visitMethod()方法;最后,调用visitEnd()方法结束对类的访问。

这一顺序不能被打破,例如,不能在visitAttribute()方法前调用visitField()。

六、使用ClassWriter生成新类/新抽象类/新接口

例如如下的一个接口:

package pkg;
public interface Comparable extends Mesurable {
    int LESS = -1;
    int EQUAL = 0;
    int GREATER = 1;
    int compareTo(Object o);
}

可以以如下的方式使用ClassWriter生成:

ClassWriter cw = new ClassWriter(0);
cw.visit(V1_5), ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE, “pkg/Comparable”, null, “java/lang/Object”, new String[] { “pkg/Mesurable” });
cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, “LESS”, “I”, null, new Integer(-1)).visitEnd();
cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, “EQUAL”, “I”, null, new Integer(0)).visitEnd();
cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, “GREATER”, “I”, null, new Integer(1)).visitEnd();
cw.visitMethod(ACC_PUBLIC + ACC_ABSTRACT, “compareTo”, “(Ljava/lang/Object;)I”, null, null).visitEnd();
cw.visitEnd();

 

其中,第二行的visit()方法:

public final void visit(int version, int access, String name, String signature, String superName, String[] interfaces)

第一个参数version指的是Java版本,这里V1_5的常量指的是Java 1.5。

第二个参数access指的是修饰符,这里是一个接口,虽然Java代码中仅声明了public interface,但其隐含的abstract修饰符在这里也要被写出。

第三个参数name指的是类名,已编译类中没有import和package,因而这里必须写完全限定名。另外,在这里需要以Java字节码格式的类型描述符写出。具体Java字节码类型描述符与Java类型的对应,在下一节详细讲解。

第四个参数signature是泛型擦除时使用的,这里不涉及,因而为null。

第五个参数superName指的是父类名,这里是一个接口,虽然Java代码中不需要指定其父类,但其隐含的父类java.lang.Object必须写出。另外,在这里需要以Java字节码格式的类型描述符写出。具体Java字节码类型描述符与Java类型的对应,在下一节详细讲解。

第五个参数interfaces指的是实现/继承的接口名,因为Java支持接口的多实现,因而该参数为一个String数组。另外,在这里需要以Java字节码格式的类型描述符写出。具体Java字节码类型描述符与Java类型的对应,在下一节详细讲解。

第三、四、五行的visitField()方法:

public final FieldVisitor visitField(int access, String name, String desc, String signature, Object value)

第一个参数access指的是修饰符,这里是接口中的变量,虽然Java代码不需要指定,但其隐含的public static final修饰符在这里也要被写出。

第二个参数name指的是变量名。

第三个参数desc指的是类型描述符,I代表Java类型中的int,具体Java字节码类型描述符与Java类型的对应,在下一节详细讲解。

第四个参数signature是泛型擦除时使用的,这里不涉及,因而为null。

第五个参数value指的是变量的值,在Java字节码中,只有static final这种永不会改变的常量字段,才会在这里指定value,其他情况下该字段必须为null。|

第六行的visitMethod()方法:

public final MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions)

第一个参数access指的是修饰符,这里是接口中的方法,虽然Java代码不需要指定,但其隐含的public abstract修饰符在这里也要被写出。

第二个参数name指的是方法名。

第三个参数desc指的是方法描述符,(Ljava/lang/Object;)I代表Java中的返回类型int、入参Object,具体Java字节码方法描述符与Java方法声明的对应,在下一节详细讲解。

第四个参数signature是泛型擦除时使用的,这里不涉及,因而为null。

第五个参数exceptions是异常,这里不涉及,因而为null。

七、Java字节码中的类型描述符与方法描述符

类型描述符与Java类型的对应列表:

Java类型 类型描述符
boolean Z
char C
byte B
short S
int I
float F
long J
double D
Object Ljava/lang/Object;
int[] [I
Object[][] [[Ljava/lang/Object;

方法描述符与Java中的方法声明的对应列表:

Java中的方法声明 方法描述符
void m(int i, float f) (IF)V
int m(Object o) (Ljava/lang/Object;)I
int[] m(int i, String s) (ILjava/lang/String;)[I
Object m(int[] i) ([I)Ljava/lang/Object;

 

八、如何使用自定义的ClassVisitor对类进行操控

1、移除成员

在visitXXX方法中直接return null,不把它继续转发下去。

@Override
public MethodVisitor visitMethod(final int access, final String name, final String desc, final String signature, final String[] exceptions) {
    if (name.equals("XXXXXX")) {
        return null;
    }
    return cv.visitMethod(access, name, desc, signature, exceptions);
}

 

2、增加类成员

首先在这里回顾一下类信息访问次序

visit
visitSource?
visitOuterClass?
( visitAnnotation | visitAttribute)*
( visitInnerClass | visitField | visitMethod)*
visitEnd

因为类信息访问次序的存在,因而只能在visitInnerClass | visitField | visitMethod或者visitEnd中增加类成员。一般选择在visitEnd增加,因为可以保证肯定可以执行,而且只执行一次。但有特殊需求(例如需要计算该类一共有多少个方法)时,也可以选择在visitInnerClass | visitField | visitMethod增加。

增加时,重写visitEnd()方法,在其中调用visitField()、visitMethod()等方法即可。下面的例子是在类中加入public static final String addField = “added field”:

@Override
public void visitEnd() {
    FieldVisitor fv = cv.visitField(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC + Opcodes.ACC_FINAL, "addField", "Ljava/lang/String;", null, "added field");
    if (fv != null) {
        fv.visitEnd();
    }
}

 

3、修改类成员

重写visitField()、visitMethod()等方法,在其中使用自己实现的MethodVisitor、FieldVisitor等类对类成员进行操作。

下面的例子是对类中以test开头的方法使用自定义的TestMethodVisitor操作:

@Override
public MethodVisitor visitMethod(final int access, final String name,
final String desc, final String signature, final String[] exceptions) {
    MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
    MethodVisitor wrappedMv = mv;
    if (mv != null) {
        if (name.startsWith("test")) {
            wrappedMv = new TestMethodVisitor(api, mv);
        }
    }

    return wrappedMv;
}

 

九、介绍Java字节码指令

在ASM中新增/修改方法时,需要使用Java字节码指令才可以实现各种操作。
具体的介绍,在《ASM使用指南》中已有详细的介绍,在此不再赘述,只粘贴过来:

字节代码指令由一个标识该指令的操作码和固定数目的参数组成:
操作码是一个无符号字节值——即字节代码名,由助记符号标识。例如,操作码0用助记符号NOP表示,对应于不做任何操作的指令。
参数是静态值,确定了精确的指令行为。它们紧跟在操作码之后给出。比如GOTO标记指令(其操作码的值为167)以一个指明下一条待执行指令的标记作为参数标记。不要将指令参数与指令操作数相混淆:参数值是静态已知的,存储在编译后的代码中,而操作数值来自操作数栈,只有到运行时才能知道。
字节代码指令可以分为两类:一小组指令,设计用来在局部变量和操作数栈之间传送值;其他一些指令仅用于操作数栈:它们从栈中弹出一些值,根据这些值计算一个结果,并将它压回栈中。

ILOAD, LLOAD, FLOAD, DLOADALOAD 指令读取一个局部变量,并将它的值压到操 作数栈中。它们的参数是必须读取的局部变量的索引 i
ILOAD 用于加载一个 booleanbytecharshortint 局部变量。LLOADFLOADDLOAD 分别用于加载 longfloatdouble 值。(LLOADDLOAD 实际加载两个槽 ii+1)。最后,ALOAD 用于加载任意非基元值,即对 象和数组引用。与之对应,ISTORELSTOREFSTOREDSTOREASTORE 指令从操作数栈 中弹出一个值,并将它存储在由其索引 i 指定的局部变量中。
可以看到,xLOAD 和 xSTORE 指令被赋入了类型(事实上,下面将要看出,几乎所有指令 都被赋予了类型)。它用于确保不会执行非法转换。实际上,将一个值存储在局部变量中,然后再以不同类型加载它,是非法的。例如,ISTORE 1 ALOAD 1 序列是非法的——它允许将一个任意内存位置存储在局部变量 1 中,并将这个地址转换为对象引用!但是,如果向一个局部变量中存储一个值,而这个值的类型不同于该局部变量中存储的当前值,却是完全合法的。这意味着一个局部变量的类型,即这个局部变量中所存值的类型可以在方法执行期间发生变化。
上面已经说过,所有其他字节代码指令都仅对操作数栈有效。它们可以划分为以下类别:

这些指令用于处理栈上的值:POP弹出栈顶部的值,DUP压入顶部栈值的一个副本, SWAP 弹出两个值,并按逆序压入它们,等等。

常量 这些指令在操作数栈压入一个常量值:ACONST_NULL压入nullICONST_0压入 int 值 0,FCONST_0 压入 0fDCONST_0 压入 0dBIPUSH b 压入字节值 bSIPUSH s 压入 shortsLDC cst 压入任意 intfloatlongdoubleStringclass1 常量 cst,等等。

算术与逻辑 这些指令从操作数栈弹出数值,合并它们,并将结果压入栈中。它们没有任何参数。xADDxSUBxMULxDIVxREM 对应于+-*/%运算,其中 xILFD 之一。类似地,还有其他对应于<<>>>>>|&^运算的指令,用于 处理intlong值。
类型变换这些指令从栈中弹出一个值,将其转换为另一类型,并将结果压入栈中。它们对应于 Java 中的类型转换表达式。I2F, F2D, L2D 等将数值由一种数值类型转换为另一种类型。CHECKCAST t 将一个引用值转换为类型 t
对象这些指令用于创建对象、锁定它们、检测它们的类型,等等。例如,NEWtype指令将一个 type 类型的新对象压入栈中(其中 type 是一个内部名)。

字段 这些指令读或写一个字段的值。GETFIELD owner name desc 弹出一个对象引用,并压和其 name 字段中的值。PUTFIELD owner name desc 弹出一个值和一个对象引用,并将这个值存储在它的 name 字段中。在这两种情况下,该对象都必须是 owner 类型,它的字段必须为 desc 类型。GETSTATICPUTSTATIC 是类似指令,但用于静态字段。

方法 这些指令调用一个方法或一个构造器。它们弹出值的个数等于其方法参数个数加 1 (用于目标对象),并压回方法调用的结果。INVOKEVIRTUAL owner name desc 调用在类 owner 中定义的 name 方法,其方法描述符为 descINVOKESTATIC 用于静态方法, INVOKESPECIAL 用于私有方法和构造器,INVOKEINTERFACE 用于接口中定义的方法。最后,对于 Java 7 中的类,INVOKEDYNAMIC 用于新动态方法调用机制。

数组 这些指令用于读写数组中的值。xALOAD指令弹出一个索引和一个数组,并压入此索引处数组元素的值。xASTORE 指令弹出一个值、一个索引和一个数组,并将这个值存 储在该数组的这一索引处。这里的 x 可以是 ILFDA,还可以是 BCS

跳转 这些指令无条件地或者在某一条件为真时跳转到一条任意指令。它们用于编译if、 for、do、while、break 和 continue 指令。例如,IFEQ label 从栈中弹出一个 int 值,如果这个值为 0,则跳转到由这个 label 指定的指令处(否则,正常执行下一条指令)。还有许多其他跳转指令,比如 IFNE 或 IFGE。最后,TABLESWITCH 和 LOOKUPSWITCH 对应于 switch Java 指令。

返回 最后,xRETURN和RETURN指令用于终止一个方法的执行,并将其结果返回给调用者。RETURN 用于返回 void 的方法,xRETURN 用于其他方法。

附录、《ASM4使用指南》中文版

ASM4使用指南

BASE64Encoder的坑:自动添加回车符

最近需要把图片Base64之后传到前端,一开始直接用的sun.misc.BASE64Encoder,但发现这个类有点问题,会自动往转换出来的String里添加换行符。无奈之下用replaceAll把\r\n都去掉,这下在本机运行的时候终于没问题了。

等到放到服务器上之后,还是不行。检查一下输出,发现竟然还有换行符……再一查,得,每个平台的换行符还不一样,Windows下是\r\n,Linux下是\n,Mac下是\r。服务器是Linux,难怪还有换行符……

知道了问题,解决起来也简单,一种是把\r\n都去掉,另一种就是干脆把这个倒霉的sun.misc.BASE64Encoder换掉。用Java 1.8的话,直接用自带的java.util.Base64;JDK版本低的话,可以用Apache commons里的Base64类。

Spring AOP不能拦截同一个类中内部的调用

例如,A中的方法a使用Spring AOP插入了一些功能(例如使用声明式事务):

@Transactional(rollbackFor = Exception.class)
public void a() {
 ……
 ……
}

在其他类中调用a方法时,一切正常。但A内部的其他方法调用a方法时,会发现Spring AOP并没有起作用,插入的功能无效了。

解决方法比较简单粗暴,在A内部调用a方法时,获取A的代理对象,然后调用这个代理对象的a方法即可,像这样:

((A) AopContext.currentProxy()).a();

BeanUtils.copyProperties的一个大坑:为null的包装类(Integer等)复制后自动变为0

最近业务里经常要在两个对象间复制属性,于是想当然的就用了BeanUtils.copyProperties来复制。然而有一天突然发现,用复制后的对象调用一个方法一直失败,用被复制的那个去调用却没有任何问题。检查了很久才发现,原来BeanUtils.copyProperties方法在遇到包装类时,竟然会把原来为null的属性,复制后赋值为0…(对于Boolean,复制后会变为false),难怪用复制后的对象会出错……

解决方法就很简单了,换用PropertyUtils.copyProperties即可
不过这两者之间还是有一定区别的,PropertyUtils.copyProperties比起BeanUtils.copyProperties来说,少了类型转换的支持,也就是说必须要类型和name一样才可以复制。比如源对象有一个类型为Long的属性,而目标对象同名属性的类型为Date,BeanUtils.copyProperties可以复制,而PropertyUtils.copyProperties会报错。

解决Spring Boot自定义错误页导致文件无法上传的问题

网上的Spring Boot自定义错误页教程,基本上都是自己自定义一个ServletRegistrationBean,把DispatcherServlet里的setThrowExceptionIfNoHandlerFound设置为true,代码样例如下:

@Configuration
public class ErrorConfiguration {
    @Bean
    public ServletRegistrationBean dispatcherRegistration(DispatcherServlet dispatcherServlet) {
        ServletRegistrationBean registration = new ServletRegistrationBean(
                dispatcherServlet);
        dispatcherServlet.setThrowExceptionIfNoHandlerFound(true);
        return registration;
    }

    @Bean
    public EmbeddedServletContainerCustomizer containerCustomizer() {
        return (container -> {
            ErrorPage error401Page = new ErrorPage(HttpStatus.UNAUTHORIZED, "/401.html");
            ErrorPage error404Page = new ErrorPage(HttpStatus.NOT_FOUND, "/404.html");
            ErrorPage error500Page = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/500.html");
            container.addErrorPages(error401Page, error404Page, error500Page);
        });
    }
}

但这种配置方式存在一个问题,我们自定义了ServletRegistrationBean之后,无法使用系统自带的DispatcherServletAutoConfiguration自动配置,于是缺少了很多配置,其中就包含上传文件所需要的配置。

解决方法很简单。只为了自定义错误页的话,根本无需像上面代码段中粗体部分那样自己自定义ServletRegistrationBean。直接在application.yml(或application.properties)里配置一句spring.mvc.throw-exception-if-no-handler-found: true,就可以把DispatcherServlet里的setThrowExceptionIfNoHandlerFound设置为true了。