最近线上部署应用时,发现如下异常:
Failed to instantiate [net.oschina.j2cache.CacheChannel]: Factory method 'cacheChannel' threw exception; nested exception is java.lang.ExceptionInInitializerError
...
Caused by: java.lang.ExceptionInInitializerError: null
...
Caused by: java.lang.reflect.InaccessibleObjectException:
`Unable to make field private final byte[] java.lang.String.value accessible:
module java.base does not "opens java.lang" to unnamed module @1b70203f`
线上应用使用OpenJdk11,同时集成了J2cache且序列化方式使用的json,相关配置如下:
注:
起初序列化方式使用的fastjson,但是由于在序列化时未将myObj.md5属性序列化,再次读取时该属性为空,导致程序异常,
class MyObj {
//该属性未提供getter/setter方法
private String md5;
. . . . . .
}
后续依次尝试切换了序列化方法fst、json后,最终发现json方式支持序列化myObj.md5属性,故采用了json序列化方式。
j2cache:
# Cache Serialization Provider
# values:
# fst -> using fast-serialization (recommend)
# kryo -> using kryo serialization
# json -> using fst's json serialization (testing)
# fastjson -> using fastjson serialization (embed non-static class not support)
# java -> java standard
# fse -> using fse serialization
# [classname implements Serializer]
serialization: json
而该程序本地启动时,会出现如下警告,由于未影响程序运行,起初便没有过多关注:
WARNING: An illegal reflective access operation has occurred
WARNING: `Illegal reflective access by org.nustaq.serialization.FSTClazzInfo (file:/D:/mvn_repository/de/ruedigermoeller/fst/2.57/fst-2.57.jar) to field java.lang.String.value`
WARNING: Please consider reporting this to the maintainers of org.nustaq.serialization.FSTClazzInfo
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
而线上应用启动时设置了如下启动参数:
java -jar myApp.jar
--illegal-access=deny
--add-opens java.base/java.lang=ALL-UNNAMED
--add-opens java.base/java.lang.reflect=ALL-UNNAMED
--add-opens java.base/java.lang.invoke=ALL-UNNAMED
之所以线上应用会启动失败,就是因为设置了--illegal-access=deny
。
在Java9之后引入了module模块
的概念,而不同module模块
间是不允许使用反射来访问非public的字段/方法/构造函数(field/method/constructor)
,除非被访问的目标模块
为反射设置open
即允许自身被其他模块进行non-public反射访问。
使用反射访问非public(non-public)的代码示例如下:
package com.module1;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class PropertyUtil {
public static Map<String, Object> getProperties(Object object) throws IllegalAccessException {
Class<?> theClass = object.getClass();
Field[] declaredFields = theClass.getDeclaredFields();
Map<String, Object> fieldsMap = new HashMap<>();
for (Field declaredField : declaredFields) {
//此处即为non-public field的反射访问
//后续在其他模块如module2.app调用此getProperties(objInModule2)即存在跨模块non-public反射访问
declaredField.setAccessible(true);
Object o = declaredField.get(object);
fieldsMap.put(declaredField.getName(), o);
}
return fieldsMap;
}
}
模块间开放non-public反射访问示例如下:
//模块1
module module1.app {
exports com.module1;
}
//模块2
module module2.app {
//开放模块module2的com.module2包给模块module1.app,
//即模块module2.app允许模块module1.app对模块2的com.module2包下对象进行non-public反射访问,
//再简单点:[模块2]中的[包com.module2]允许被[模块1]进行non-public反射访问
//等同于: --add-opens module2.app/com.module2=module1.app
opens com.module2 to module1.app;
requires module1.app;
}
而使用Jdk8开发的代码(没有模块概念)运行在Java9+中,Jdk8代码会被自动归到未命名的模块 unnamed module
中,如之前日志中的unnamed module @1b70203f
,可通过ALL-UNNAMED
表示所有的未命名模块。
Java 9之后引入了一个新的JVM选项 --illegal-access
,该选项有四个可选值:
线上使用了
--illegal-access=deny
,所以出现非法反射时会导致程序抛异常而启动失败,
而本地开发环境运行程序时,未明确设置–illegal-access则使用默认--illegal-access=premit
,所以可以启动成功,但在第一次非法反射访问时给出了警告。
综上,在设置了--illegal-access=deny(推荐设置deny,兼容未来Java版本)
时,需同时添加--add-opens
以开启对应模块/包
允许被其他模块
进行非法(non-public)反射访问。
例如根据之前的日志:
Unable to make field private final byte[] java.lang.String.value accessible:
module java.base does not "opens java.lang" to unnamed module @1b70203f
将关键提示日志拆解后如下表:
被访问模块名 | 被访问包名 | 发起非法访问的模块名 | |||
---|---|---|---|---|---|
module | java.base | does not | “opens java.lang” | to | unnamed module @1b70203f |
具体转换格式:--add-opens 被访问模块名/被访问包名=发起非法访问的模块名
根据如上日志转换如下--add-opens
命令为:
--add-opens java.base/java.long=ALL-UNNAMED
注:
ALL-UNNAMED
表示所有的未命名模块
反复重启根据提示最终整理如下完整--add-opens
选项:
java -jar myApp.jar
--illegal-access=deny
--add-opens java.base/java.lang=ALL-UNNAMED
--add-opens java.base/java.lang.reflect=ALL-UNNAMED
--add-opens java.base/java.lang.invoke=ALL-UNNAMED
--add-opens java.base/java.math=ALL-UNNAMED
--add-opens java.base/java.util=ALL-UNNAMED
--add-opens java.base/java.util.concurrent=ALL-UNNAMED
--add-opens java.base/java.net=ALL-UNNAMED
--add-opens java.base/java.text=ALL-UNNAMED
添加如上完整启动选项后,线上程序可以正常启动了。
之前通过--add-opens
的确保证了线上程序成功启动,但是这个--add-opens
是不是有点太多了,
既然是J2Cache序列化器引入的非法反射,那就将其替换为自定义的不引入非法反射访问的序列化器。
自定义J2Cache Jackson序列化器代码如下:
package com.myapp.serializer;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import net.oschina.j2cache.util.Serializer;
import java.io.IOException;
/**
* J2Cache Jackson序列化器
*
* @author luohq
* @date 2023-03-15 15:48
*/
public class J2cacheJacksonSerializer implements Serializer {
private final ObjectMapper om;
public J2cacheJacksonSerializer() {
this.om = new ObjectMapper();
//设置可见性 - 全部属性、全部权限
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
//设置序列化Json中包含对象类型
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
//忽略空Bean转json的错误
om.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
//忽略未知属性,防止json字符串中存在,java对象中不存在对应属性的情况出现错误
om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
//注册一个时间序列化及反序列化的处理模块,用于解决jdk8中localDateTime等的序列化问题
om.registerModule(new JavaTimeModule());
}
@Override
public String name() {
return "jackson";
}
@Override
public byte[] serialize(Object obj) throws IOException {
return om.writeValueAsBytes(obj);
}
@Override
public Object deserialize(byte[] bytes) throws IOException {
return om.readValue(bytes, Object.class);
}
}
配置J2Cache使用自定义序列化器:
j2cache:
# Cache Serialization Provider
# values:
# fst -> using fast-serialization (recommend)
# kryo -> using kryo serialization
# json -> using fst's json serialization (testing)
# fastjson -> using fastjson serialization (embed non-static class not support)
# java -> java standard
# fse -> using fse serialization
# [classname implements Serializer]
serialization: com.myapp.serializer.J2cacheJacksonSerializer
综上,通过如下命令即可正常启动程序:
java -jar myApp.jar --illegal-access=deny
参考:
https://www.logicbig.com/tutorials/core-java-tutorial/modules/reflective-access.html
https://www.logicbig.com/tutorials/core-java-tutorial/modules/illegal-access-operations.html
https://www.logicbig.com/tutorials/core-java-tutorial/modules/unnamed-modules.html
解决JDK9以上的非法反射访问警告