JDK11下J2Cache序列化器反射异常及--illegal-access解决方案
创始人
2025-05-28 22:19:06
0

问题现象

最近线上部署应用时,发现如下异常:

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 getProperties(Object object) throws IllegalAccessException {Class theClass = object.getClass();Field[] declaredFields = theClass.getDeclaredFields();Map 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.appopens com.module2 to module1.app;requires module1.app;
}

而使用Jdk8开发的代码(没有模块概念)运行在Java9+中,Jdk8代码会被自动归到未命名的模块 unnamed module中,如之前日志中的unnamed module @1b70203f,可通过ALL-UNNAMED表示所有的未命名模块。

Java 9之后引入了一个新的JVM选项 --illegal-access,该选项有四个可选值:

  • permit:Jdk9、Jdk11的默认值,允许不同模块间进行non-public反射访问,且仅在首次非法访问会给出警告,后续不再警告。在该模式下会自动将Jdk8(或更低版本)的代码进行opens设置,即允许Jdk8的代码被其他模块进行non-public反射访问,也允许Jdk8的代码对其他模块进行non-public反射访问,如此可保证高、低版本混合的Java程序如往常一样正常运行。
  • warn:类似permit,但是每次非法访问都会警告
  • debug:类似warn,但在warn的基础上加入了类似e.printStackTrace()的功能
  • deny未来的默认值,禁止所有的不同模块间的non-public反射访问,出现非法反射则抛出异常,除了使用特别的命令行参数排除的模块,比如使用 –add-opens排除某些模块使其能够通过非法反射访问

线上使用了--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     

将关键提示日志拆解后如下表:

被访问模块名被访问包名发起非法访问的模块名
modulejava.basedoes not“opens java.lang”tounnamed 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());}@Overridepublic String name() {return "jackson";}@Overridepublic byte[] serialize(Object obj) throws IOException {return om.writeValueAsBytes(obj);}@Overridepublic 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以上的非法反射访问警告

相关内容

热门资讯

喜欢穿一身黑的男生性格(喜欢穿... 今天百科达人给各位分享喜欢穿一身黑的男生性格的知识,其中也会对喜欢穿一身黑衣服的男人人好相处吗进行解...
发春是什么意思(思春和发春是什... 本篇文章极速百科给大家谈谈发春是什么意思,以及思春和发春是什么意思对应的知识点,希望对各位有所帮助,...
网络用语zl是什么意思(zl是... 今天给各位分享网络用语zl是什么意思的知识,其中也会对zl是啥意思是什么网络用语进行解释,如果能碰巧...
为什么酷狗音乐自己唱的歌不能下... 本篇文章极速百科小编给大家谈谈为什么酷狗音乐自己唱的歌不能下载到本地?,以及为什么酷狗下载的歌曲不是...
华为下载未安装的文件去哪找(华... 今天百科达人给各位分享华为下载未安装的文件去哪找的知识,其中也会对华为下载未安装的文件去哪找到进行解...
家里可以做假山养金鱼吗(假山能... 今天百科达人给各位分享家里可以做假山养金鱼吗的知识,其中也会对假山能放鱼缸里吗进行解释,如果能碰巧解...
四分五裂是什么生肖什么动物(四... 本篇文章极速百科小编给大家谈谈四分五裂是什么生肖什么动物,以及四分五裂打一生肖是什么对应的知识点,希...
怎么往应用助手里添加应用(应用... 今天百科达人给各位分享怎么往应用助手里添加应用的知识,其中也会对应用助手怎么添加微信进行解释,如果能...
一帆风顺二龙腾飞三阳开泰祝福语... 本篇文章极速百科给大家谈谈一帆风顺二龙腾飞三阳开泰祝福语,以及一帆风顺二龙腾飞三阳开泰祝福语结婚对应...
美团联名卡审核成功待激活(美团... 今天百科达人给各位分享美团联名卡审核成功待激活的知识,其中也会对美团联名卡审核未通过进行解释,如果能...