JDK11下J2Cache序列化器反射异常及--illegal-access解决方案

问题现象

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

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,该选项有四个可选值:

  • 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     

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

被访问模块名 被访问包名 发起非法访问的模块名
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以上的非法反射访问警告

你可能感兴趣的:(java,j2cache,jdk11)