1

前言

之前我的微服务版本是java8、spring-boot2.4.2、spring-cloud2020.0.0、spring-cloud-alibaba2021.1,但是最近随着spring-boot3.0版本的发布,再加上官方已经说明最低版本支持java17,所以就有了这篇文章。

<properties>
    <java.version>1.8</java.version>
    <spring-boot.version>2.4.2</spring-boot.version>
    <spring-cloud.version>2020.0.0</spring-cloud.version>
    <spring-cloud-alibaba.version>2021.1</spring-cloud-alibaba.version>
</properties>

目前,Oracle 官方支持的最新 LTS(长期支持)版本是 JDK 17,而 OpenJDK 社区也正在积极开发和维护 JDK 17。

因此,如果你考虑升级 Java 版本,并且希望使用一个稳定和可靠的版本,那么建议选择 JDK 17。JDK 17 包含了很多新特性和改进,例如:

新的垃圾回收器:ZGC 和 Shenandoah;
增强的 Switch 表达式;
改进的异常处理;
新的日期和时间 API;
...
此外,JDK 17 也提供了广泛的操作系统、硬件平台和云平台支持,可以适用于各种不同的应用场景。同时,由于 JDK 17 是一个 LTS 版本,因此可以获得更长时间的更新和支持,从而提供更好的稳定性和安全性。

总之,建议选择 JDK 17 作为升级目标,以获得最佳的兼容性、稳定性和安全性。

版本参照

下面列出了spring-boot、spring-cloud和spring-cloud-alibaba的版本对应关系,选择合适的版本不要出现兼容性问题。
spring-boot和spring-cloud版本对应关系
image.png

spring和elasticsearch版本对应关系
image.png

spring-cloud和spring-cloud-alibaba版本对应关系
image.png

有了上面的版本对应关系,于是我们选择了如下的版本:

<properties>
    <java.version>17</java.version>
    <spring-boot.version>3.0.2</spring-boot.version>
    <spring-cloud.version>2022.0.0</spring-cloud.version>
    <spring-cloud-alibaba.version>2022.0.0.0-RC2</spring-cloud-alibaba.version>
</properties>

升级微服务

安装java17
这里不做详细介绍了,这个应该是必备的,我给出下载地址:java17下载jdk安装

安装maven 3.9
这里不做详细介绍了,这个应该是必备的,我给出下载地址:maven下载maven安装

配置idea

设置版本都为java17

image.png

添加jdk17

image.png

language level选择jdk17

image.png

modules中设置sources的language level都是project default即可

image.png

保存并退出即可!

- 配置pom.xml

下面是我的pom.xml大家可以做为升级参考:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.0.2</version>
    <relativePath/>
</parent>

<properties>
    <java.version>17</java.version>
    <spring-boot.version>3.0.2</spring-boot.version>
    <spring-cloud.version>2022.0.0</spring-cloud.version>
    <spring-cloud-alibaba.version>2022.0.0.0-RC2</spring-cloud-alibaba.version>
</properties>
<dependencyManagement>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-dependencies</artifactId>
        <version>${spring-boot.version}</version>
        <type>pom</type>
        <scope>import</scope>
    </dependency>
    <!-- spring cloud 依赖 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-dependencies</artifactId>
        <version>${spring-cloud.version}</version>
        <type>pom</type>
        <scope>import</scope>
    </dependency>
    <!-- spring cloud alibaba 依赖 -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-alibaba-dependencies</artifactId>
        <version>${spring-cloud-alibaba.version}</version>
        <type>pom</type>
        <scope>import</scope>
    </dependency>
</dependencies>
</dependencyManagement>
<dependencies>
    <!-- 必须添加的 依赖 -->
    <dependency>
        <groupId>jakarta.platform</groupId>
        <artifactId>jakarta.jakartaee-api</artifactId>
        <version>10.0.0</version>
        <scope>provided</scope>
    </dependency>
    <!-- 必须添加的 依赖(如果你有的话) -->
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>1.18.28</version>
      <scope>provided</scope>
      </dependency>
    <!-- 必须添加的 依赖(如果你有的话) -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-stream</artifactId>
        <version>4.0.1</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-stream-binder-kafka</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-stream-binder-rabbit</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
    </dependency>
</dependencies>
<build>
    <plugins>
        <plugin>
            <groupId>com.spotify</groupId>
            <artifactId>dockerfile-maven-plugin</artifactId>
            <version>1.4.10</version>
            <configuration>
                <dockerfile>src/main/docker/Dockerfile</dockerfile>
                <!--                    <repository>spotify/foobar</repository>-->
                <!--<tag>${project.version}</tag>-->
                <buildArgs>
                    <!--提供参数向Dockerfile传递-->
                    <JAR_FILE>target/${project.build.finalName}.jar</JAR_FILE>
                </buildArgs>
            </configuration>
        </plugin>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

上面引入了jakartaee依赖,这是有历史原因的,感兴趣的可以看看[这篇文章 ]()

- 以javax开头的全部换成jakarta开头

修改前:

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;

修改后:

import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;

- openfeign换成webclient
在升级后的新环境远程调用接口默认使用的是无阻塞的响应式形式调用接口,所以不管是openfeign还是webclient。

添加spring-cloud-starter-loadbalancer依赖包

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

WebClient配置

@Component
public class WebClientConfig {
    @Bean
    @LoadBalanced
    public WebClient.Builder webClientBuilder() {
        return WebClient.builder();
    }
}

WebClient调用

@Service
public class AccountWebClient {

    @Resource
    private WebClient.Builder webClientBuilder;

    public Mono<User> getAccountInfoByUserName(String userName) {
        WebClient webClient = webClientBuilder.build();
        return webClient.get()
                .uri("http://account-service/userInfo/{userName}", userName)
                .retrieve()
                .bodyToMono(User.class);
    }
}

注意:
如果不添加spring-cloud-starter-loadbalancer依赖包就会报下面的错:

org.springframework.web.reactive.function.client.WebClientRequestException: Failed to resolve 'xxx-service' after 3 queries

- spring.redis换成spring.data.redis
如果你使用了redis,那么你就要调整成下面的形式:
调整前:

spring:
  redis:
  database: 0
  host: xxx.xxx.xxx.xxx
  password: password
  port: 6379
  timeout: 5000ms
  lettuce:
    pool:
      max-active: 500
      max-wait: 500ms
      max-idle: 500
      min-idle: 0

调整后:

spring:
  data:
    redis:
    database: 0
    host: xxx.xxx.xxx.xxx
    password: password
    port: 6379
    timeout: 5000ms
    lettuce:
      pool:
        max-active: 500
        max-wait: 500ms
        max-idle: 500
        min-idle: 0

- spring-cloud-stream 换成4.0.1版本

这是spring-cloud-stream的bug,如果不换成4.0.1的话会报错:

2023-06-18T11:25:07.771+08:00  WARN 68477 --- [           main] o.s.c.s.binder.DefaultBinderFactory      : Failed to propagate child beans. This may cause issues in your application

java.lang.IllegalStateException: kafka-binder_context has not been refreshed yet

所以换成下面的包就没问题:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-stream</artifactId>
    <version>4.0.1</version>
</dependency>

- 配置webflux版本的websocket

注意:

1、webflux包自带websocket,所以不用显示引入websocket包
2、webflux的websocket不能使用这个tomcat包,因为webflux会使用netty,而webmvc使用的是tomcat

引入webflux包

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

WebSocketConfig

@Configuration
public class WebSocketConfig  {
    @Bean
    public HandlerMapping handlerMapping(MyWebSocketHandler echoHandler) {
        return new SimpleUrlHandlerMapping(Map.of("/websocket", echoHandler), -1);
    }

    @Bean
    public WebSocketHandlerAdapter webSocketHandlerAdapter() {
        return new WebSocketHandlerAdapter();
    }
}

MyWebSocketHandler

@Component
public class MyWebSocketHandler implements WebSocketHandler {

    private static final Logger logger = LoggerFactory.getLogger(MyWebSocketHandler.class);

    @Override
    public Mono<Void> handle(WebSocketSession session) {
        return session.receive()
            .flatMap(message -> {
                // 打印接收到的消息
                logger.info("Received message: " + message.getPayloadAsText());

                // 创建两个新的消息并打印
                String reply1 = "Echo 1: " + message.getPayloadAsText();
                logger.info("Sent message: " + reply1);
                String reply2 = "Echo 2: " + message.getPayloadAsText();
                logger.info("Sent message: " + reply2);

                // 在接收到每条消息后发送两个响应
                return session.send(Flux.just(session.textMessage(reply1), session.textMessage(reply2)));
            })
            .then();  // 在发送响应消息后关闭连接
    }
}

- 配置webflux版本的文件上传(包含aliyun的oss上传)

因为使用了webflux,在响应式编程中,你不能立即得到结果,因为操作是异步的。你必须返回一个Mono或Flux,然后在流中处理结果。

uploadImage修改前:

@PostMapping("/upload/image")
public Map<String, String> uploadImage(@RequestPart("upload") MultipartFile file) throws IOException {
    Map<String, String> result = new HashMap<>();
    if (file == null || "".equals(file.getOriginalFilename().trim())) {
        result.put("uploaded", "false");
        result.put("url", "#");
        return result;
    }
    String endpath = ossService.uploadImgByInputStream(file, "space/note/editor/images/" + System.currentTimeMillis());
    result.put("uploaded", "true");
    result.put("url", endpath);
    return result;
}

uploadImage修改后:

@PostMapping(value = "/info/note/editor/upload/image")
public Mono<Map<String, String>> uploadImage(@RequestPart("upload") FilePart file) {
    Map<String, String> result = new HashMap<>();
    if (file == null || "".equals(file.filename().trim())) {
        result.put("uploaded", "false");
        result.put("url", "#");
        return Mono.just(result);
    }

    return ossService.uploadImgByInputStreamNoBlock(file, "space/note/editor/images/" + System.currentTimeMillis())
            .map(endpath -> {
                result.put("uploaded", "true");
                result.put("url", endpath);
                return result;
            })
            .onErrorResume(ex -> {
                log.info("uploadImage ex={}", ex.getMessage());
                result.put("uploaded", "false");
                result.put("url", "#");
                return Mono.just(result);
            });
}

修改前:

@Override
public String uploadImgByInputStream(MultipartFile file, String fullPath) throws IOException {
    ObjectMetadata objectMeta = new ObjectMetadata();
    objectMeta.setContentLength(file.getBytes().length);

    //将字节码转换成流
    InputStream input = new ByteArrayInputStream(file.getBytes());

    // yourEndpoint填写Bucket所在地域对应的Endpoint。以华东1(杭州)为例,Endpoint填写为https://oss-cn-hangzhou.aliyuncs.com。
    String endpoint = aliyunOssConfig.getOssBucketEndpoint();
    // 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
    String accessKeyId = aliyunOssConfig.getAccessKeyId();
    String accessKeySecret = aliyunOssConfig.getAccessKeySecret();
    String bucket = aliyunOssConfig.getOssBucket();

    // 创建OSSClient实例。
    OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);

    // 创建PutObjectRequest对象。
    // 依次填写Bucket名称(例如examplebucket)和Object完整路径(例如exampledir/exampleobject.txt)。Object完整路径中不能包含Bucket名称。
    PutObjectRequest putObjectRequest = new PutObjectRequest(bucket, fullPath, input, objectMeta);

    // 上传字符串。
    ossClient.putObject(putObjectRequest);

    // 关闭OSSClient。
    ossClient.shutdown();

    return aliyunOssConfig.getOssBucketCdnHost() + "/" + fullPath;
}

修改后:

@Override
public Mono<String> uploadImgByInputStream(FilePart file, String fullPath) {
    return DataBufferUtils.join(file.content())
            .flatMap(dataBuffer -> {
                byte[] bytes = new byte[dataBuffer.readableByteCount()];
                dataBuffer.read(bytes);
                DataBufferUtils.release(dataBuffer);
                InputStream input = new ByteArrayInputStream(bytes);
                String endpoint = aliyunOssConfig.getOssBucketEndpoint();
                // 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
                String accessKeyId = aliyunOssConfig.getAccessKeyId();
                String accessKeySecret = aliyunOssConfig.getAccessKeySecret();
                String bucket = aliyunOssConfig.getOssBucket();
                // 创建OSSClient实例。
                OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);

                // 创建PutObjectRequest对象。
                PutObjectRequest putObjectRequest = new PutObjectRequest(bucket, fullPath, input);

                // 上传字符串。
                ossClient.putObject(putObjectRequest);

                // 关闭OSSClient。
                ossClient.shutdown();

                return Mono.just(aliyunOssConfig.getOssBucketCdnHost() + "/" + fullPath);
            })
            .subscribeOn(Schedulers.boundedElastic()); // 使用Schedulers.boundedElastic()来在另一个线程上执行阻塞操作
}

注意:springmvc和springwebflux对应的上传文件类是:MultipartFile和FilePart

- 修改dockerfile
修改前:

FROM openjdk:8-jdk-alpine
MAINTAINER "zhangwei"<zhangwei900808@126.com>
RUN mkdir -p /usr/local/spaceservice
ARG JAR_FILE
ADD ${JAR_FILE} /usr/local/spaceservice/app.jar
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/usr/local/spaceservice/app.jar"]
EXPOSE 8902

修改后:

FROM openjdk:17-jdk-slim
MAINTAINER "zhangwei"<zhangwei900808@126.com>
RUN mkdir -p /usr/local/spaceservice
ARG JAR_FILE
ADD ${JAR_FILE} /usr/local/spaceservice/app.jar
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/usr/local/spaceservice/app.jar"]
EXPOSE 8902

注意:
这里的openjdk:17-jdk-slim也可以换成openjdk:17-jdk-alpine

openjdk:17-jdk-slim和openjdk:17-jdk-alpine这两个镜像都是OpenJDK 17的Docker镜像,但是他们的底层操作系统不同,他们的主要区别在以下几个方面:

基础操作系统:openjdk:17-jdk-slim基于Debian(slim版)操作系统构建,而openjdk:17-jdk-alpine基于Alpine Linux构建。Alpine Linux是一种轻量级的Linux发行版,相比于Debian,它的镜像大小更小,启动更快。

镜像大小:通常,Alpine基础的镜像会比Debian slim基础的镜像更小。这是因为Alpine Linux使用了BusyBox和musl libc,这使得其基础系统更小更轻量。
引用
二进制兼容性:由于Alpine Linux使用的是musl libc,而不是大多数Linux发行版使用的glibc,这可能导致一些针对glibc的二进制程序在Alpine上运行出现问题。如果你的应用依赖于某些特定的glibc特性,那么使用Debian slim可能会更好。

包管理:Debian使用APT作为包管理工具,而Alpine使用APK。这可能会影响你在Dockerfile中如何安装软件包。

- 修改k8s file
修改前:

...
  template:
    metadata:
      labels:
        app: <podname>
    spec:
      containers:
      - env:
        - name: LANG
          value: C.UTF-8
        - name: JAVA_HOME
          value: /usr/lib/jvm/java-1.8-openjdk
...

修改后:

...
  template:
    metadata:
      labels:
        app: <podname>
    spec:
      containers:
      - env:
        - name: LANG
          value: C.UTF-8
        - name: JAVA_HOME
          value: /usr/lib/jvm/java-17-openjdk
...

这样就完成了升级!

- Jenkins安装java17和maven3.9
因为要使用jdk17的maven,所以要替换一下jdk8的maven镜像

修改前:

stage("Build") {
    agent {
        docker {
            image 'maven:3-alpine'
            args '-v /root/.m2:/root/.m2'
        }
    }
}

修改后:

stage("Build") {
    agent {
        docker {
            image 'maven:3.8.4-openjdk-17-slim'
            args '-v /root/.m2:/root/.m2'
        }
    }
}

总结

1、我也是查看别人的文章和自己摸索出来的,基于本人能力有限,如果出现问题请不吝赐教。

参考

在 idea 中配置 jdk17


Awbeci
3.1k 声望213 粉丝

Awbeci