开发者社区> 凌力> 正文
阿里云
为了无法计算的价值
打开APP
阿里云APP内打开

从JDK8飞升到JDK17,再到未来的JDK21

简介: 2022年,Spring6和 SpringBoot3都推出了,在此之前,Java社区很坚挺,一直是"新版任你发,我用Java 8",不管新版本怎么出,很少有人愿意升级。 这一次,Spring 直接来了个大招,SpringBoot3和Spring6的最低依赖就是JDK17!跨过 JDK 8-16,直接升级到 JDK 17。那么为什么是 JDK 17呢?
+关注继续查看

背景

2022年,Spring6和 SpringBoot3都推出了,在此之前,Java社区很坚挺,一直是"新版任你发,我用Java 8",不管新版本怎么出,很少有人愿意升级。

这一次,Spring 直接来了个大招,SpringBoot3和Spring6的最低依赖就是JDK17!跨过 JDK 8-16,直接升级到 JDK 17。那么为什么是 JDK 17呢?


为什么是JDK17

这么多新版本的 JDK,而且2022年还会推出 JDK 18 和 JDK 19,为什么 Spring 选择了 JDK 17呢。

主要是因为他是一个 Oracle官宣可以免费商用的LTS版本,所谓 LTS,是 Long Term Support,也就是官方保证会长期支持的版本。


image

上面这张图是 Oracle 官方给出的 Oracle JDK 支持的时间线。可以看得到,JDK 17 最多可以支持到 2029 年 9 月份。按照技术更新迭代的速度,这次免费商用 8 年可谓是良苦用心,为的就是让使用者放心大胆地将 JDK 升级到 JDK 17(不过JDK 8 支持的时间更长,可以延长到 2030 年 12 月,JDK8可谓是YYDS!)

从 JDK 诞生到现在,还在长期支持的版本主要有 JDK 7、JDK 8 、JDK 11以及 JDK 1,JDK 17 将是继 Java 8 以来最重要的LTS版本,是 Java 社区八年努力的成果。

一直以来,Java8 都是 Java 社区心头的痛,Java8提供了很多特性,比如Lambda 表达式、Optional 类,加上Java8超长的支持时间,都导致了JDK8的使用至今。它代表着以稳定性为主的企业管理层与拥抱变化为主的程序猿之间的拉锯战。不升!成为各大厂心照不宣的选择。现在,这种平衡或将打破。因为 Java 届的霸主框架 SpringBoot,选择了最小支持的 Java lts 版本,就是最新的 Java17。

那么接下来,让我们看看,从JDK8到JDK17,Java 社区八年努力的成果有哪些?


从JDK8到JDK17的新特性


JDK9新特性(2017年9月)

  • 模块化
  • 提供了List.of()、Set.of()、Map.of()和Map.ofEntries()等工厂方法
  • 接口支持私有方法
  • Optional 类改进
  • 多版本兼容Jar包
  • JShell工具
  • try-with-resources的改进
  • Stream API的改进
  • 设置G1为JVM默认垃圾收集器
  • 支持http2.0和websocket的API

重要特性:主要是API的优化,如支持HTTP2的Client API、JVM采用G1为默认垃圾收集器

JDK10新特性(2018年3月)

  • 局部变量类型推断,类似JS可以通过var来修饰局部变量,编译之后会推断出值的真实类型
  • 不可变集合的改进
  • 并行全垃圾回收器 G1,来优化G1的延迟
  • 线程本地握手,允许在不执行全局VM安全点的情况下执行线程回调,可以停止单个线程,而不需要停止所有线程或不停止线程
  • Optional新增orElseThrow()方法
  • 类数据共享
  • Unicode 语言标签扩展
  • 根证书

重要特性:通过var关键字实现局部变量类型推断,使Java语言变成弱类型语言、JVM的G1垃圾回收由单线程改成多线程并行处理,降低G1的停顿时间

JDK11新特性(2018年9月)(LTS版本)

  • 增加一些字符串处理方法
  • 用于 Lambda 参数的局部变量语法
  • Http Client重写,支持HTTP/1.1和HTTP/2 ,也支持 websockets
  • 可运行单一Java源码文件,如:java Test.java
  • ZGC:可伸缩低延迟垃圾收集器,ZGC可以看做是G1之上更细粒度的内存管理策略。由于内存的不断分配回收会产生大量的内存碎片空间,因此需要整理策略防止内存空间碎片化,在整理期间需要将对于内存引用的线程逻辑暂停,这个过程被称为"Stop the world"。只有当整理完成后,线程逻辑才可以继续运行。(并行回收)
  • 支持 TLS 1.3 协议
  • Flight Recorder(飞行记录器),基于OS、JVM和JDK的事件产生的数据收集框架
  • 对Stream、Optional、集合API进行增强


重要特性:对于JDK9和JDK10的完善,主要是对于Stream、集合等API的增强、新增ZGC垃圾收集器

JDK12新特性(2019年3月)

  • Switch 表达式扩展,可以有返回值
  • 新增NumberFormat对复杂数字的格式化
  • 字符串支持transform、indent操作
  • 新增方法Files.mismatch(Path, Path)
  • Teeing Collector
  • 支持unicode 11
  • Shenandoah GC,新增的GC算法
  • G1收集器的优化,将GC的垃圾分为强制部分和可选部分,强制部分会被回收,可选部分可能不会被回收,提高GC的效率

重要特性:switch表达式语法扩展、G1收集器优化、新增Shenandoah GC垃圾回收算法

JDK13新特性(2019年9月)

  • Switch 表达式扩展,switch表达式增加yield关键字用于返回结果,作用类似于return,如果没有返回结果则使用break
  • 文本块升级 """ ,引入了文本块,可以使用"""三个双引号表示文本块,文本块内部就不需要使用换行的转义字符
  • SocketAPI 重构,Socket的底层实现优化,引入了NIO
  • FileSystems.newFileSystem新方法
  • ZGC优化,增强 ZGC 释放未使用内存,将标记长时间空闲的堆内存空间返还给操作系统,保证堆大小不会小于配置的最小堆内存大小,如果堆最大和最小内存大小设置一样,则不会释放内存还给操作系统


重要特性:ZGC优化,释放内存还给操作系统、socket底层实现引入NIO

JDK14新特性(2020年3月)

  • instanceof模式匹配,instanceof类型匹配语法简化,可以直接给对象赋值,如if(obj instanceof String str),如果obj是字符串类型则直接赋值给了str变量
  • 引入Record类型,类似于Lombok 的@Data注解,可以向Lombok一样自动生成构造器、equals、getter等方法;
  • Switch 表达式-标准化
  • 改进 NullPointerExceptions提示信息,打印具体哪个方法抛的空指针异常,避免同一行代码多个函数调用时无法判断具体是哪个函数抛异常的困扰,方便异常排查;
  • 删除 CMS 垃圾回收器

JDK15新特性(2020年9月)

  • EdDSA 数字签名算法
  • Sealed Classes(封闭类,预览),通过sealed关键字修饰抽象类限定只允许指定的子类才可以实现或继承抽象类,避免抽象类被滥用
  • Hidden Classes(隐藏类)
  • 移除 Nashorn JavaScript引擎
  • 改进java.net.DatagramSocket 和 java.net.MulticastSocket底层实现


JDK16新特性(2021年3月)

  • 允许在 JDK C ++源代码中使用 C ++ 14功能
  • ZGC性能优化,去掉ZGC线程堆栈处理从安全点到并发阶段
  • 增加 Unix 域套接字通道
  • 弹性元空间能力
  • 提供用于打包独立 Java 应用程序的 jpackage 工具

JDK16相当于是将JDK14、JDK15的一些特性进行了正式引入,如instanceof模式匹配(Pattern matching)、record的引入等最终到JDK16变成了final版本


JDK17新特性(2021年9月)(LTS版本)

  • Free Java License
  • JDK 17 将取代 JDK 11 成为下一个长期支持版本
  • Spring 6 和 Spring Boot 3需要JDK17
  • 移除实验性的 AOT 和 JIT 编译器
  • 恢复始终执行严格模式 (Always-Strict) 的浮点定义
  • 正式引入密封类sealed class,限制抽象类的实现
  • 统一日志异步刷新,先将日志写入缓存,然后再异步刷新


虽然JDK17也是一个LTS版本,但是并没有像JDK8和JDK11一样引入比较突出的特性,主要是对前几个版本的整合和完善。


重要特性详解

Java 模块化

JPMS(Java Platform Module System)是Java 9发行版的核心亮点。它也被称为Jigshaw项目。模块是新的结构,就像我们已经有包一样。使用新的模块化编程开发的应用程序可以看作是交互模块的集合,这些模块之间具有明确定义的边界和依赖关系。

JPMS包括为编写模块化应用程序提供支持,以及将JDK源代码模块化。JDK 9 附带了大约 92 个模块(在 GA 版本中可以进行更改)。Java 9 Module System有一个"java.base"模块。它被称为基本模块。它是一个独立的模块,不依赖于任何其他模块。默认情况下,所有其他模块都依赖于"java.base"。

在java模块化编程中:

  • 一个模块通常只是一个 jar 文件,在根目录下有一个文件module-info.class
  • 要使用模块,请将 jar 文件包含到modulepath而不是classpath. 添加到类路径的模块化 jar 文件是普通的 jar 文件,module-info.class文件将被忽略。


典型的module-info.java类如下所示:

module helloworld {     
    exports com.alibaba.eight; 
} 
module test {     
    requires helloworld; 
}


总结:模块化的目的,是让jdk的各个组件可以被分拆,复用和替换重写,比如对java的gui不满意,可以自己实现一个gui,对java的语法不满意,可以把javac替换成其他语言和其他语言的编译器,比如kotlin和kotlinc等,没有模块化,几乎很难实现,每次修改某个模块,总不能把整个jdk给重新编译一遍,再发布一个整个sdk吧,模块化可以帮助更有效的定制化和部署

本地变量类型推断

在Java 10之前版本中,我们想定义定义局部变量时。我们需要在赋值的左侧提供显式类型,并在赋值的右边提供实现类型:

MyObject value = new MyObject();

在Java 10中,提供了本地变量类型推断的功能,可以通过var声明变量:

var value = new MyObject();

本地变量类型推断将引入“var”关键字,而不需要显式的规范变量的类型。

其实,所谓的本地变量类型推断,也是Java 10提供给开发者的语法糖。

虽然我们在代码中使用var进行了定义,但是对于虚拟机来说他是不认识这个var的,在java文件编译成class文件的过程中,会进行解糖,使用变量真正的类型来替代var

HTTP客户端API-响应式流实现的HttpClient

Java 使用HttpURLConnection进行HTTP通信已经很长一段时间了。但随着时间的推移,要求变得越来越复杂,应用程序的要求也越来越高。在 Java 11 之前,开发人员不得不求助于功能丰富的库,如Apache HttpComponentsOkHttp等。

我们看到Java 9发布包含一个HttpClient实现作为实验性功能。它随着时间的推移而发展,现在是 Java 11 的最终功能。现在 Java 应用程序可以进行 HTTP 通信,而无需任何外部依赖。

作为JDK11中正式推出的新Http连接器,支持的功能还是比较新的,主要的特性有:

  • 完整支持HTTP 2.0 或者HTTP 1.1
  • 支持 HTTPS/TLS
  • 有简单的阻塞使用方法
  • 支持异步发送,异步时间通知
  • 支持WebSocket
  • 支持响应式流

HTTP2.0其他的客户端也能支持,而HttpClient使用CompletableFuture作为异步的返回数据。WebSocket的支持则是HttpClient的优势。响应式流的支持是HttpClient的一大优势。

HttpClient中的NIO模型、函数式编程、CompletableFuture异步回调、响应式流让HttpClient拥有极强的并发处理能力,所以其性能极高,而内存占用则更少。


语法糖

Stream API 改进

Collectors.teeing()

teeing 收集器已公开为静态方法Collectors::teeing。该收集器将其输入转发给其他两个收集器,然后将它们的结果使用函数合并。

示例:

List<Student> list = Arrays.asList(
        new Student("唐一", 55),
        new Student("唐二", 60),
        new Student("唐三", 90));
//平均分 总分
String result = list.stream().collect(Collectors.teeing(
        Collectors.averagingInt(Student::getScore),
        Collectors.summingInt(Student::getScore),
        (s1, s2) -> s1 + ":" + s2));
//最低分  最高分
String result2 = list.stream().collect(Collectors.teeing(
        Collectors.minBy(Comparator.comparing(Student::getScore)),
        Collectors.maxBy(Comparator.comparing(Student::getScore)),
        (s1, s2) -> s1.orElseThrow() + ":" + s2.orElseThrow()
));
System.out.println(result);
System.out.println(result2);
添加Stream.toList方法(jdk16)
List<String> list = Arrays.asList("1", "2", "3");
//之前这样写
List<Integer> oneList = list.stream()
    .map(Integer::parseInt)
    .collect(Collectors.toList());
//现在可以这样写
List<Integer> twoList = list.stream()
    .map(Integer::parseInt)
    .toList();


Switch表达式改进

支持箭头表达式(jdk12预览 jdk14标准)

此更改扩展了switch 语句以便它可以用作语句或表达式。不必为break每个 case 块定义一个语句,我们可以简单地使用箭头语法

boolean isWeekend = switch (day) {
  case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> false; 
  case SATURDAY, SUNDAY -> true;
  default -> throw new IllegalStateException("Illegal day entry :: " + day);
};
int size = 3;
String cn = switch (size) {
    case 1 -> "壹";
    case 2 -> "贰";
    case 3, 4 -> "叁";
    default -> "未知";
};
System.out.println(cn);

//要使用此预览功能,我们必须在应用程序启动期间使用–enable-preview标志明确指示 JVM。

yield关键字(jdk13)

使用yield,我们现在可以有效地从 switch 表达式返回值,并能够更容易实现策略模式。

public class SwitchTest {
    public static void main(String[] args) {
        var me = 4;
        var operation = "平方";
        var result = switch (operation) {
            case "加倍" -> {
                yield me * 2;
            }
            case "平方" -> {
                yield me * me;
            }
            default -> me;
        };
        System.out.println(result);
    }
}

字符串

文本块改进(jdk13)

早些时候,为了在我们的代码中嵌入 JSON,我们将其声明为字符串文字:

String json  = "{\r\n" + "\"name\" : \"lingli\",\r\n" + "\"website\" : \"https://www.alibaba.com/\"\r\n" + "}";


现在让我们使用字符串文本块编写相同的 JSON :

String json = """ 
{     
    "name" : "Baeldung",     
    "website" : "https://www.alibaba.com/" 
} 
""";

很明显,不需要转义双引号或添加回车。通过使用文本块,嵌入的 JSON 更易于编写,更易于阅读和维护。

更多的API
  • isBlank():如果字符串为空或字符串仅包含空格(包括制表符),则返回 true。注意与isEmpty() 不同,isEmpty()仅在长度为 0 时返回 true。
  • lines():将字符串拆分为字符串流,每个字符串包含一行。
  • strip() : 分别从开头和结尾;
  • stripLeading()/stripTrailing()仅开始和仅结束删除空格。
  • repeat(int times):返回一个字符串,该字符串采用原始字符串并按指定的次数重复该字符串。
  • readString():允许从文件路径直接读取到字符串。
  • writeString(Path path):将字符串直接写入指定路径处的文件。
  • indent(int level):缩进字符串的指定量。负值只会影响前导空格。
  • transform(Function f):将给定的 lambda 应用于字符串。


instanceof 的模式匹配(jdk14出预览,jdk16最终确认)

之前:
Object obj = "大阳";
if (obj instanceof String) {
    String t = (String) obj;
    // TODO
}
现在:
Object obj = "大阳";
if (obj instanceof String t) {
    // TODO 此时t已经是String类型了
}

record记录类(jdk16正式)

传统的Java应用程序通过创建一个类,通过该类的构造方法实例化类,并通过getter和setter方法访问成员变量或者设置成员变量的值。有了record关键字,你的代码会变得更加简洁。

/**
 * record 记录类
 * 你也可以覆写equals() hashCode() toString()方法,不用写get、set了
 * @author DAYANG
 */
record User(String name, Integer age) {
    
    @Override
    public String toString() {
        return "User[" +
                "name='" + name + '\'' +
                ", age=" + age +
                ']';
    }
    @Override
    public boolean equals(Object obj) {
        return false;
    }
    @Override
    public int hashCode() {
        return 0;
    }
}

JVM

GC变化

JDK9: 设置G1为JVM默认垃圾收集器

JDK10:并行全垃圾回收器 G1,通过并行Full GC, 改善G1的延迟。目前对G1的full GC的实现采用了单线程-清除-压缩算法。JDK10开始使用并行化-清除-压缩算法。

JDK11:推出ZGC新一代垃圾回收器(实验性),目标是GC暂停时间不会超过10ms,既能处理几百兆的小堆,也能处理几个T的大堆。

JDK14 :删除CMS垃圾回收器;弃用 ParallelScavenge + SerialOld GC 的垃圾回收算法组合;将 zgc 垃圾回收器移植到 macOS 和 windows 平台

JDk 15 : ZGC (JEP 377) 和Shenandoah (JEP 379) 不再是实验性功能。默认的 GC 仍然是G1

JDK16:增强ZGC,ZGC获得了 46个增强功能 和25个错误修复,控制stw时间不超过10毫秒

指标测试

吞吐量比较

image

在吞吐量方面,Parallel 中 JDK 8 和 JDK 11 差距不大,JDK 17 相较 JDK 8 提升 15% 左右;G1 中 JDK 17 比 JDK 8 提升 18%;ZGC 在 JDK 11引入,JDK 17 对比JDK 11 提升超过 20%。

延迟比较

image

在 GC 延迟方面,JDK 17 的提升更为明显。我们可以看到为缩短 GC 暂停时间所做的努力都得到了回报,很多提升都是因为 GC 的改进。

在 Parallel 中 JDK 17 对比 JDK 8 和JDK 11 提升 40%;在 G1 中,JDK 11 对比 JDK 8 提升 26%,JDK 17 对比 JDK 8 提升接近 60%!ZGC 中 JDK 17 对比 JDK 11 提升超过 40%。

暂停时间对比

image

我们可以看到JDK 17 中的 ZGC 远低于目标:亚毫秒级的暂停时间。G1 的目标是在延迟和吞吐量之间保持平衡,远低于其默认的目标:200 毫秒的暂停时间。ZGC 的设计会保证暂停时间不随堆的大小而改变,我们可以清楚地看到当堆扩大到 128GB 时的情况。从暂停时间的角度来看,G1比Parallel 更善于处理更大的堆,因为它能够保证暂停时间满足特定目标。

资源占用

image

上图比较了三个不同收集器原生内存的使用峰值。由于从这个角度来看 Parallel 和 ZGC 都非常稳定,因此我们应该看一看原始数字。我们可以看到 G1 在这方面确实有所改进,主要原因是所有功能和增强功能都提高了记忆集管理的效率 。

总结:无论使用哪种收集器,与旧版本相比,JDK 17 的整体性能都有很大的提升。在 JDK 8 中,Parallel是默认设置,但在 JDK 9 中改为了 G1。从那以后,G1 的改进速度就超过了 Parallel,但在有些情况下可能 Parallel 仍然是最佳选择。而 ZGC(JDK 15 正式使用)的加入,成为了第三种高性能替代方案。



其他

密封类和接口(15预览 17正式)

在Java15之前,所有的类都可以没有限制地继承其他类--除非被继承类被声明为final类型,任何类都可以实现公共接口。

现在在Java15中,一个类或者接口可以使用修饰符sealed声明为密封类或者接口,来限制其继承类。

/**
 * 定义一个抽象密封类Pet,它的实现类只能是Dog, Cat这两个,其他的实现类均不允许
 */
public abstract sealed class Pet
    permits Dog, Cat {}
final class Dog extends Pet {
}
final class Cat extends Pet {
    
}
//密封的类和接口限制了其他类或接口可以扩展或实现它们
public sealed interface Shape{
    final class Planet implements Shape {}
    final class Star   implements Shape {}
    final class Comet  implements Shape {}
}
public abstract sealed class Test{
    final class A extends Test {}
    final class B extends Test {}
    final class C extends Test {}
}

EdDSA 算法

EdDSA (Edwards-Curve Digital Signature Algorithm) 是在 Java 15 中通过JEP 339添加的另一种附加数字签名方案。与其他可用的签名方案相比,它提供了更好的性能和安全的签名。




总结和展望

总结

1.Spring带头猛冲,直接上JDK17。如果Spring6还支持Java8的话,那很多技术框架都要跟着Java8的兼容,与其这样不如由Spring带头,一起飞升Java17,不过有些框架还不支持JDK17

2.性能升级,从java8换到java11,啥也没干性能直接就提升了10%(nio底层的重写),更何况一路到jdk17过程中的JVM相关优化。 不过光是性能的优化还不足以吸引企业进行JDK升级,毕竟加机器就能解决,费不着各种升级改造,还可能有安全问题

3.JDK21可能成为真正的经典版本。目前还没有Project loom功能,代表着没有协程,性能方面比有协程jdk差远了。比如阿里开源的jdk8,11,就有非侵入式协程,这方面比JDK17强。

从发展趋势看,Project loom功能在JDK19已经可预览了,可以发现该版本许多的java工具都开始针对loom进行升级,Project loom大概在JDK21进行正式推出,而JDK21又是一个长期支持版本 (LTS) ,值得期待。

各种servlet容器,还有jetty,netty,vert.x等,在它们最新版本的release note找到对应的升级标注,说,我们添加了某某支持,其中最重要的就是loom,或者叫做虚拟线程的支持, 可以预见一旦JDK21发行,很多软件都会跟上投入生产!

Project loom参考:https://open.atatech.org/articles/249741

展望

JDK的升级是必然趋势。

不升级的人说,目前来说国内很多程序猿可能觉得升级会造成额外工作,出了问题费力不讨好,要是出了安全问题,更要提桶跑路。也有说没有实质性的好处,而且还有风险,还有从企业角度说,未来也不升级,因为去Oracle化。但考虑到未来oracle不再维护JDK8,Spring也不再维护过去版本的时候,为了跟上时代,使用最新技术,必然会助推JDK的升级。
当越来越多的公司加入到JDK17以上的大军中,未来更多的框架新版本都会最低支持JDK17,因为兼容旧JDK实在不值得,当大部分框架和社区、论坛都是讨论JDK17的技术和各种解决问题的方法时,必然会反推企业进行升级。



参考链接:

https://zhuanlan.zhihu.com/p/480293185

https://blog.oxings.com/article/31.html

https://blog.csdn.net/best_luxi/article/details/122543074



版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
PHP、JS、Python逻辑运算符||和位运算符|
PHP、JS、Python逻辑运算符||和位运算符|
19 0
【CSS】你还记得组合选择器怎么用吗?
有关组合选择器的学习记录。
36 0
8、算术运算符
JavaScript 共提供10个算术运算符,用来完成基本的算术运算。
43 0
AHAS+ACK流量防护最佳实践
AHAS应用防护以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度来保障业务的稳定性,提供更专业稳定的流量防护手段、秒级的流量水位分布分析功能,是阿里巴巴双十一技术体系中的核心组件,同时也是开源框架Sentinel的商业化产品。 AHAS应用防护广泛用于秒杀场景、消息削峰填谷、集群流量控制、实时熔断等场景中,从多个维度保障您的业务稳定性。
81 0
从Java8到Java17
背景Java8的前世今生作为一名程序员,笔者从大学开始接触Java这门编程语言,还记得当时的版本是1.4,不过这个版本的寿命还挺长,目前在一些金融行业的存量系统中依然还有1.4版本的影子。从C/C++到Java,Java给我的第一印象,就是这门语言的语法非常之啰嗦,比如内部类,像下面这段排序的代码,在C语言里面可以很方便地通过函数指针来解决,C++可以通过运算符重载来解决,唯独Java语言的写法最
1220 0
超级计算,超级未来
超级计算,超级未来
65 0
PHP、JS、Python逻辑运算符||和位运算符|
PHP、JS、Python逻辑运算符||和位运算符|
41 0
dedecms提取某栏目及子栏目名称到首页怎么弄
  我们建网站时有不同的需求,例如为页面创建一个栏目导航,用dedecms如何提取某栏目及子栏目名称和链接呢?如下图所示,先列出指定的顶级栏目,在下方再列出此栏目的所有子栏目。   之前ytkah说过dedecms调用当前栏目的子栏目怎么操作,我们可不可以发散一下思维进行扩展呢?   先列出...
975 0
+关注
文章
问答
文章排行榜
最热
最新
相关课程
更多
相关电子书
更多
JDK8新特性与生产
立即下载
低代码开发师(初级)实战教程
立即下载
阿里巴巴DevOps 最佳实践手册
立即下载