随着SpringBoot2.7的发布,支持jdk8~jdk21。Springboot3.X发布,最低需要jdk17。升级jdk17是大势所趋。
本文升级适用于SpringBoot2.7.x从jdk8升级到jdk17操作指南,成功把ruoyi4.x微服务的jdk8升级到jdk17,应用到生产。
参考1:重磅!Spring Boot 2.7 正式发布、
参考2:hutool-希望Hutool能支持下JDK8~JDK17的所有版本
参考3:aliyun-一文详解|从JDK8飞升到JDK17,再到未来的JDK21
参考4:程序员DD-从 Java 8 升级到 Java 17 踩坑全过程,建议收藏!
参考5:老卫waylau-JDK
正式开发业务系统,使用反射修改jdk自带类场景较少。即使修改,通过–add-opens的jvm参数,即可解决该问题。
由于jdk9增加的模块化设计,导致对系统内置类反射受到限制,出现类似的错误,自己开发的类没有影响。
public static void main(String[] args) {
// jdk17使用反射,无需修改即可正常使用的示例
//在 Java8 中,没有人能阻止你访问特定的包;只要 setAccessible(true) 就可以了
NucPerson nucPerson = new NucPerson();
nucPerson.setPersonName("张三");
ReflectUtil.setFieldValue(nucPerson, "personPhone", "18800001111");//hutool5中工具类
System.out.println(nucPerson.getPersonPhone());
System.out.println(ReflectUtil.getFieldMap(NucPerson.class));
// jdk17使用反射系统模块的类,必须要增加--add-opens的反射示例
// Java9 模块化以后,一切都变了,只能通过 --add-exports 和 --add-opens 来打破模块封装
Object stringValue = ReflectUtil.getFieldValue(nucPerson.getPersonName(), "value");
System.out.println(stringValue);
}
java.lang.reflect.InaccessibleObjectException: Unable to make field private final java.util.Map sun.reflect.annotation.AnnotationInvocationHandler.memberValues accessible: module java.base does not "opens sun.reflect.annotation" to unnamed module @52d455b8
at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:354) ~[na:na]
at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:297) ~[na:na]
at java.base/java.lang.reflect.Field.checkCanSetAccessible(Field.java:178) ~[na:na]
at java.base/java.lang.reflect.Field.setAccessible(Field.java:172) ~[na:na]
at cn.hutool.core.util.ReflectUtil.setAccessible(ReflectUtil.java:966) ~[hutool-core-5.7.19.jar:na]
at cn.hutool.core.util.ReflectUtil.getFieldValue(ReflectUtil.java:266) ~[hutool-core-5.7.19.jar:na]
at cn.hutool.core.util.ReflectUtil.getFieldValue(ReflectUtil.java:234) ~[hutool-core-5.7.19.jar:na]
at cn.hutool.core.annotation.AnnotationUtil.setValue(AnnotationUtil.java:215) ~[hutool-core-5.7.19.jar:na]
解决:增加jvm启动参数(java虚拟机启动参数)
--add-opens java.base/sun.reflect.annotation=ALL-UNNAMED
汇总:
-Djdk.home=/Library/Java/JavaVirtualMachines/openjdk-18.0.1.1/Contents/Home
-Xms24m
-Xmx768m
-DTopSecurityManager.disable=true
--add-exports=java.desktop/com.sun.java.swing.plaf.gtk=ALL-UNNAMED
--add-exports=java.desktop/sun.awt=ALL-UNNAMED
--add-exports=jdk.internal.jvmstat/sun.jvmstat.monitor.event=ALL-UNNAMED
--add-exports=jdk.internal.jvmstat/sun.jvmstat.monitor=ALL-UNNAMED
--add-exports=java.desktop/sun.swing=ALL-UNNAMED
--add-exports=jdk.attach/sun.tools.attach=ALL-UNNAMED
--add-opens=java.desktop/sun.awt.X11=ALL-UNNAMED
--add-opens=java.desktop/javax.swing.plaf.synth=ALL-UNNAMED
--add-opens=java.base/java.net=ALL-UNNAMED
--add-opens=java.base/java.lang.ref=ALL-UNNAMED
--add-opens=java.base/java.lang=ALL-UNNAMED
--add-opens=java.desktop/javax.swing=ALL-UNNAMED
--add-opens=java.desktop/javax.swing.plaf.basic=ALL-UNNAMED
-XX:+IgnoreUnrecognizedVMOptions
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/Users/duandazhi/Library/Application Support/VisualVM/2.1.2/var/log/heapdump.hprof
--add-opens
和--add-exports
区别,opens是深度反射,可以打开对私有变量、成员的反射;exports只能打开公有变量的反射。总结:使用--add-opens
肯定没问题。
总结:模块化需要增加的JVM打开反射参数,对常规业务系统影响不大,毕竟对系统类反射操作很少,对自己开发的类进行反射操作并没有限制。
1、BeanUtils常用的Bean映射工具,包含apache的BeanUtils和spring的BeanUtils,底层使用简单,但底层反射效率比较低。
2、BeanCopier,基于cglib的BeanCopier直接生成字节码文件.class文件,效率有很大提升。但是,当属性名称和属性类型存在差异时,CGLib实现的BeanCopier会退化成暴力反射,性能就会低下。一般配合工具框架使用,如dubbo。
3、Orika,底层采用了javassist类库生成Bean映射的字节码,后直接加载执行生成的字节码文件,速度比反射快很多,支持嵌套。
参考1:Bean复制选型
BeanUtils、BeanCopier、Dozer、Orika 哪家强? 、
Java常见bean mapper的性能及原理分析1、
Java常见bean mapper的性能及原理分析2、
常见Bean拷贝框架使用姿势及性能对比、
对比性能:反射类(各类BeanUtils、Dozer)<<动态生成字节码(BeanCopier、Orika)<<直接GetSet(Mapstruct.plus)
参考2:报错参数
升级JDK1.8 到 JDK17 时访问报Unable to make protected native ??? .clone() 的异常、
DozerBeanMapper对象之间相同属性名赋值、
Orika User Guide、
//BeanMapper.java
import java.util.List;
/**
* 类型copy接口
*
* @author dazers
* @date 2017/12/4 下午5:27
* @see java.util.function.Function
* @see MapOrObject
*/
public interface BeanMapper {
/**
* 对象之间copy,可以是相同类型,也可以是不同类型;但不能是集合
*
* @param srcObj
* @param targetObj
* @return dest
*/
<S, D> D copy(S srcObj, D targetObj);
/**
* copy集合
*
* eg:List bssOrderVos = beanMapper.copyList(bssOrders, BssOrderVo.class);
*
* @param srcObjs 原始集合
* @param destClz 要转换之后的类型
* @param
* @param
* @return
*/
<S, D> List<D> copyList(List<S> srcObjs, Class<D> destClz);
}
@Configuration
class CustomConfig {
@Bean(name = "beanMapper")
public BeanMapper registUtilsBeanMapper() {
//多个Bean复制实现类,使用其中一个
///return new DozerBeanMapperImpl();
return new OrikaBeanMapperImpl();
}
}
//OrikaBeanMapperImpl.java
import ma.glasnost.orika.MapperFacade;
import ma.glasnost.orika.MapperFactory;
import ma.glasnost.orika.converter.builtin.CloneableConverter;
import ma.glasnost.orika.converter.builtin.PassThroughConverter;
import ma.glasnost.orika.impl.DefaultMapperFactory;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
/**
* 复制对象属性的工具类.
*
* @author Lingo
*
* orika
*/
public class OrikaBeanMapperImpl implements BeanMapper {
/**
* 实例.
*/
private static final MapperFacade MAPPER;
static {
//---------------------------这段代码jdk8运行没有问题,jdk17报错 没有使用反射copy的权限------------------
/**
* Caused by: java.lang.reflect.InaccessibleObjectException:
* Unable to make protected native java.lang.Object java.lang.Object.clone()
* throws java.lang.CloneNotSupportedException accessible:
* module java.base does not "opens java.lang" to unnamed module
* @see CloneableConverter#CloneableConverter 报错没有反射权限
*/
// 如果src中属性为null,就不复制到dest
MapperFactory mapperFactory = new DefaultMapperFactory.Builder()
.mapNulls(false).build();
// 如果属性是Object,就只复制引用,不复制值,可以避免循环复制
mapperFactory.getConverterFactory().registerConverter(
new PassThroughConverter(Object.class));
MAPPER = mapperFactory.getMapperFacade();
}
/**
* 把src中的值复制到dest中.
* src 和 dest 对象均不能为空
*/
@Override
public <S, D> D copy(S srcObj, D destObj) {
Objects.requireNonNull(srcObj, "copy src 不能为null");
Objects.requireNonNull(destObj, "copy dest 不能为null");
if (srcObj instanceof Collection || destObj instanceof Collection) {
throw new Exception("copy对象不能是集合,集合copy请使用 copyList方法");
}
MAPPER.map(srcObj, destObj);
return destObj;
}
/**
* 复制list.
*/
@Override
public <S, D> List<D> copyList(List<S> srcObjs, Class<D> destClz) {
return MAPPER.mapAsList(srcObjs, destClz);
}
}
//DozerBeanMapperImpl.java
import org.dozer.DozerBeanMapper;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
/**
* 简单封装Dozer, 实现深度转换Bean<->Bean的Mapper.实现:
*
* 1. 持有Mapper的单例. 2. 返回值类型转换. 3. 批量转换Collection中的所有对象. 4.
* 区分创建新的B对象与将对象A值复制到已存在的B对象两种函数.
*
* DozerBeanMapper对象之间相同属性名赋值 http://blog.csdn.net/luo201227/article/details/24021791
*
* @author calvin
*/
public class DozerBeanMapperImpl implements BeanMapper {
/**
* 持有Dozer单例, 避免重复创建DozerMapper消耗资源.
*/
private static DozerBeanMapper dozer = new DozerBeanMapper();
/**
* 基于Dozer转换对象的类型.
*/
public <T> T map(Object source, Class<T> destinationClass) {
return dozer.map(source, destinationClass);
}
/**
* 基于Dozer转换Collection中对象的类型.
*/
public <T> List<T> mapList(
@SuppressWarnings("rawtypes") Collection sourceList,
Class<T> destinationClass) {
List<T> destinationList = new ArrayList<T>();
for (Object sourceObject : sourceList) {
T destinationObject = dozer.map(sourceObject, destinationClass);
destinationList.add(destinationObject);
}
return destinationList;
}
/**
* 基于Dozer将对象A的值拷贝到对象B中.
* src 和 dest 对象均不能为空
*/
@Override
public <S, D> D copy(S sourceObj, D destinationObject) {
Objects.requireNonNull(sourceObj, "原对象不能为空!");
Objects.requireNonNull(destinationObject, "目标对象不能为空!");
if (sourceObj instanceof Collection || destinationObject instanceof Collection) {
throw new RrException("copy对象不能是集合,集合copy请使用 copyList方法");
}
dozer.map(sourceObj, destinationObject);
return destinationObject;
}
@Override
public <S, D> List<D> copyList(List<S> srcObjs, Class<D> destClz) {
return mapList(srcObjs, destClz);
}
}
从jdk8~jdk17 有删除部分类,但都有对应的开源平替库,只需要找到对应库的maven依赖即可,因此影响可控。
高版本最坑应该是这一条:
Java 11 :移除JavaEE和CORBA模块
对于Java EE和CORBA模块在Java 9开始就不推荐使用了。而从Java 11开始正式删除了这部分内容,所以当升级到Java 11或更高的版本的话,务必要先更急以下内容相关的代码:
移除的包:
移除的工具:
比如javax.xml.bind.annotation.XmlElement、javax.xml.soap.SOAPElement相关包的类就不存在了,需要手工引入。
我的项目里面就使用到了webservice,即:jaxb
(实体类和xml转换)、soap
webservice的http网络请求。
受到影响的hutool工具类cxf相关:JAXBUtil
、SoapClient
、JakartaServletUtil
、MailUtil
全部使用jakarta命名;
业务场景,系统请求外部接口使用是webservice,实体类和xml转换需要用JAXBUtil、发送Http请求使用SoapClient
<dependency>
<groupId>com.sun.xml.bindgroupId>
<artifactId>jaxb-implartifactId>
<version>2.3.9version>
dependency>
<dependency>
<groupId>org.glassfish.jaxbgroupId>
<artifactId>jaxb-runtimeartifactId>
<version>2.4.0-b180830.0438version>
dependency>
<dependency>
<groupId>javax.xml.soapgroupId>
<artifactId>javax.xml.soap-apiartifactId>
<version>1.4.0version>
dependency>
<dependency>
<groupId>com.sun.xml.messaging.saajgroupId>
<artifactId>saaj-implartifactId>
<version>1.5.1version>
dependency>
比如代码中使用了,代码中用到了 javax.annotation.* 下的包,目前就找不到了。
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
@Component
public class RedisLimit {
private DefaultRedisScript<String> redisLUAScript ;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@PostConstruct
public void init(){
redisLUAScript = new DefaultRedisScript<>();
redisLUAScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/limitRate.lua")));
redisLUAScript.setResultType(String.class);
}
}
解决
<dependency>
<groupId>javax.annotationgroupId>
<artifactId>javax.annotation-apiartifactId>
<version>1.3.2version>
dependency>
<dependency>
<groupId>jakarta.annotationgroupId>
<artifactId>jakarta.annotation-apiartifactId>
<version>1.3.5version>
dependency>
--import javax.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotBlank;
--import javax.validation.constraints.NotNull;
import jakarta.validation.constraints.NotNull;
import java.io.Serializable;
import java.util.Date;
@Data
@Accessors(chain = true)
public class ApptChildRelationshipVo implements Serializable {
private static final long serialVersionUID = 1L;
@NotNull(message = "被监护人ID不能为空")
private Long childId;
/**
* 01本人、02户主、10配偶、51父亲、52母亲、55岳父、56岳母、97其他亲属、98非亲属等等
*/
@NotBlank(message = "监护关系不能为空")
private String relationType;
}
<dependency>
<groupId>jakarta.validationgroupId>
<artifactId>jakarta.validation-apiartifactId>
<version>3.0.2version>
dependency>
1、比如 sun.misc.BASE64Encoder,替换掉这个类。
//jdk11被删除的库-encoder
sun.misc.BASE64Encoder base64Encoder = new BASE64Encoder();
base64Encoder.encode("xxxx");
//使用java.util.Base64中的提供的新库替换
java.util.Base64.Encoder encoder = Base64.getEncoder();
encoder.encode("xxxx".getBytes());
//--------------------------------------
//jdk11被删除的库-decoder
sun.misc.BASE64Decoder decoder = new BASE64Decoder();
byte[] buf = decoder.decodeBuffer(dataStr);
//使用java.util.Base64中的提供的新库替换
Base64.Decoder decoder = Base64.getDecoder();
byte[] buf = decoder.decode(data);
System.out.println(new String(buf));
2、netty 低版本使用了 sun.misc.*,编译错误信息如下
升级最新版本即可解决
Caused by: java.lang.NoClassDefFoundError: Could not initialize class io.netty.util.internal.PlatformDependent0
at io.netty.util.internal.PlatformDependent.getSystemClassLoader(PlatformDependent.java:694) ~[netty-all-4.0.42.Final.jar!/:4.0.42.Final]
3、 lombok 使用了 com.sun.tools.javac.* 下的包,升级到最新版本即可。
项目中使用验证码、图片、excel升级到jdk17报错
java.lang.NoClassDefFoundError: Could not initialize class sun.awt.X11FontManager
Exception java.lang.UnsatisfiedLinkError: /usr/local/jdk-17.0.9+9/lib/libfontmanager.so: libfreetype.so.6: cannot open shared object file: No such file or directory
sudo yum install freetype-devel -y
、JAVA_OPTS="$JAVA_OPTS -Djava.awt.headless=true"
开发可以使用高版本的JDK,字节码级别设置为低版本,就可以避免使用高版本被删除的API。低版本代码(只要是高版本未删除)都可以在高版本直接运行,兼容性可控。
适配jdk8、jdk17的的springCloud启动脚本
#!/bin/bash
APP_NAME=nuc-esquery.jar
PROFILE=dev #dev、test、prod、vfic等等
JVM_MODE=prod #test、prod ,启用不同的JVM配置
PID=$(ps -ef|grep $APP_NAME|grep -v grep|awk '{print $2}')
# 示例目录:`cd $(dirname .)/..; pwd`、`cd $(dirname .)/.; pwd`、`cd $(dirname $0)/..; pwd`
export BASE_DIR=`cd $(dirname .)/.; pwd`
export CUSTOM_SEARCH_LOCATIONS=file:${BASE_DIR}/conf/
#堆最大内存、最小内存、年轻代堆内存、
#老年代不用调,一般会是年轻代的2倍
#元空间一般也不用调整,元空间独立jvm内存,直接在物理内存上, 会自动增加
#输出:oom时候的,堆内存信息,可能:年轻代oom、元空间oom、老年代oom
#输出:gc时候的,gc日志
## 参数:-D:使用 Property形式获取;--:使用Spring里面通用的获取方式
#===========================================================================================
# JVM Configuration
#===========================================================================================
#设置测试、生产环境中,不同的JVM内存配置
if [[ "${JVM_MODE}" == "test" ]]; then
JAVA_OPTS="-Xms2g -Xmx2g -Xmn1g -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=640m "
JAVA_OPTS="${JAVA_OPTS} -Dmyserver.mode=test "
JAVA_OPTS="${JAVA_OPTS} -Dmyserver.name=application_xyz "
else
#JAVA_OPTS="-server -Xms2g -Xmx2g -Xmn1g -XX:MetaspaceSize=512m -XX:MaxMetaspaceSize=512m "
JAVA_OPTS="-server -Xms4g -Xmx4g -Xmn2g -XX:MetaspaceSize=512m -XX:MaxMetaspaceSize=512m "
#JAVA_OPTS="-server -Xms8g -Xmx8g -Xmn4g -XX:MetaspaceSize=512m -XX:MaxMetaspaceSize=512m "
JAVA_OPTS="${JAVA_OPTS} -Dmyserver.mode=prod "
JAVA_OPTS="${JAVA_OPTS} -Dmyserver.name=application_xyz "
fi
#设置JDK8、JDK9+不同JDK版本的JVM垃圾回收器的参数
## jdk1.8返回:1;jdk17返回:17
JAVA_MAJOR_VERSION=$(java -version 2>&1 | sed -E -n 's/.* version "([0-9]*).*$/\1/p')
if [[ "$JAVA_MAJOR_VERSION" -ge "9" ]] ; then
JAVA_OPTS="${JAVA_OPTS} -Xlog:gc*:file=gc_trace.log:time,tags:filecount=10,filesize=102400 "
#解决:Could not initialize class sun.awt.X11FontManage,设置无头模式
JAVA_OPTS="$JAVA_OPTS -Djava.awt.headless=true"
else
JAVA_OPTS="${JAVA_OPTS} -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:+ParallelRefProcEnabled -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=java_heapdump.hprof -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc_trace.log -verbose:gc -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=100M "
fi
# aliyun arms
## https://arms.console.aliyun.com/#/tracing/agentList/cn-hangzhou?tab=java
#ARMS_JVM="-javaagent:/root/arms/ArmsAgent/arms-bootstrap-1.7.0-SNAPSHOT.jar -Darms.licenseKey=1426648921313446@917412bed1df111 -Darms.appName=hesuan_app146 "
ARMS_JVM=" "
# --logging.config:自定义使用外置的logback,方便用户修改,不用重新打包
# --spring.config.additional-location:可以覆盖默认的配置,覆盖默认的加载顺序和优先级。application.properties.example
#JAVA_OPTS="${JAVA_OPTS} --logging.config=./conf/project-logback.xml"
#JAVA_OPTS="${JAVA_OPTS} --spring.config.additional-location=${CUSTOM_SEARCH_LOCATIONS}"
if [ "$PID" != "" ]; then
echo '================>停止服务..........'
kill -9 $PID
sleep 3s
fi
echo '================>nohup.out日志清空成功.........'
echo '' > nohup.out
echo '================>开始重启服务.........'
#启动参数
### 获取自定义启动参数的方式
### @Value("${ghouse.datacenterId:0}")
### private long datacenterId;
### 项目经常部署,开发、测试、生产、生产备用等多套环境,nacos相关会经常变动
START_ARGS="
--spring.profiles.active=${PROFILE}\
--custom=127.0.0.1:8850\
--evn=my-test-key\
--ghouse.datacenterId=1\
--spring.cloud.nacos.discovery.server-addr=10.16.58.146:8850\
--spring.cloud.nacos.config.server-addr=10.16.58.146:8850\
--spring.cloud.inetutils.preferred-networks=10.16\
--spring.cloud.nacos.config.namespace=xahs-dev\
--spring.cloud.nacos.discovery.namespace=xahs-dev
"
echo ''
java -version
echo ''
## 常规启动版本
## java [options] -jar [args...]
#nohup java $JAVA_OPTS -jar vaccine-epi.jar --spring.profiles.active=prod >> nohup.out 2>&1 &
#nohup java $JAVA_OPTS -jar $APP_NAME 2>&1 &
echo "java $ARMS_JVM $JAVA_OPTS -jar $APP_NAME $START_ARGS >> nohup.out 2>&1 &"
nohup java $ARMS_JVM $JAVA_OPTS -jar $APP_NAME $START_ARGS >> nohup.out 2>&1 &
tail -f nohup.out
运行打包报错:java.lang.NoSuchFieldError:com.sun.tools.javac.tree.JCTree$JCImport
参考:Java|IDEA 运行和打包报错解决
## 升级lombok到1.18.30
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</dependency>