带你了解“Java新特性——模块化”

dceb7ec2f914f099eb105c3f85f49d7c.gif

Java平台从Java 8向Java 9及更高版本的进化,其中引入了一个重要的新特性——模块系统(Project Jigsaw)。模块系统的目的是解决大型应用的依赖管理问题,提升性能,简化JRE,增强兼容性和安全性,并提高开发效率。通过模块化,Java能够更好地支持微服务架构,提供更细粒度的封装和控制,以及更清晰的依赖关系。文章详细介绍了模块系统的概念,如MODULE DESCRIPTOR、主要参数、关键指令,以及模块化策略。此外,本文还提供了最佳实践建议,帮助开发者更好地理解和应用Java模块系统。

7fac1322203b9d48a459381a30a9cf10.png

模块系统简介

  发展历史

如果把 Java 8 比作单体应用,那么引入模块系统之后,从 Java 9 开始,Java 就华丽的转身为微服务。模块系统,项目代号 Jigsaw,最早于 2008 年 8 月提出(比 Martin Fowler 提出微服务还早 6 年),2014 年跟随 Java 9 正式进入开发阶段,最终跟随 Java 9 发布于 2017 年 9 月。

  模块系统定义

官方的定义:

A uniquely named, reusable group of related packages, as well as resources (such as images and XML files) and a module descriptor.

如图所示,模块的载体是 jar 文件,一个模块就是一个 jar 文件,但相比于传统的 jar 文件,模块的根目录下多了一个 module-info.class 文件,也即 module descriptor,包含信息:模块名称、依赖哪些模块、导出模块内的哪些包[允许直接 import 使用]、开放模块内的哪些包[允许通过 Java 反射访问]、提供哪些服务、依赖哪些服务。

7f12101de23f8d1e1fb0eec2e8896f91.png

  带来好处

任意一个 jar 文件,只要加上一个合法的 module descriptor,就可以升级为一个模块。这个看似微小的改变,在我看来,至少带来四方面的好处:

一、清晰的依赖管理

  • Java 可以根据 module descriptor 计算出各个模块间的依赖关系,一旦发现循环依赖,启动就会终止。

  • 由于模块系统不允许不同模块导出相同的包(即 split package,分裂包),所以在查找包时,Java 可以精准的定位到一个模块,从而获得更好的性能。

二、精简 JRE

引入模块系统之后,JDK 自身被划分为 94 个模块(参见图)。通过 Java 9 新增的 jlink 工具,开发者可以根据实际应用场景随意组合这些模块,去除不需要的模块,生成自定义 JRE,从而有效缩小 JRE 大小。得益于此,JRE 11 的大小仅为 JRE 8 的 53%,从 218.4 MB缩减为 116.3 MB,JRE 中广为诟病的巨型 jar 文件 rt.jar 也被移除。更小的 JRE 意味着更少的内存占用,这让 Java 对嵌入式应用开发变得更友好。

d69006fc98ec30f2125c02cdb9d1f20b.png

3fcd746e99495866dbef4f4a961d489c.png

三、更好的兼容性 & 安全性

Java一直以来,就只有 4 种包可见性,这让 Java 对面向对象的三大特征之一封装的支持大打折扣,类库维护者对此叫苦不迭,只能一遍又一遍的通过各种文档或者奇怪的命名来强调这些或者那些类仅供内部使用,擅自使用后果自负云云。Java 9 之后,利用 module descriptor 中的 exports 关键词,模块维护者就精准控制哪些类可以对外开放使用,哪些类只能内部使用,换句话说就是不再依赖文档,而是由编译器来保证。类可见性的细化,除了带来更好的兼容性,也带来了更好的安全性。

3fe1c0d06119db582574916ef92684fb.png

四、提升 Java 语言开发效率

Java 9 之后,Java 像开挂了一般,一改原先一延再延的风格,严格遵循每半年一个大版本的发布策略,从 2017 年 9 月到 2020 年 3 月,从 Java 9 到 Java 14,三年时间相继发布了 6 个版本,无一延期,参见图-4。这无疑跟模块系统的引入有莫大关系。前文提到,Java 9 之后,JDK 被拆分为 94 个模块,每个模块有清晰的边界(module descriptor)和独立的单元测试,对于每个 Java 语言的开发者而言,每个人只需要关注其所负责的模块,开发效率因此大幅提升。这其中的差别,就好比单体应用架构升级到微服务架构一般,版本迭代速度不快也难。

190ef5c8df3644fd764a7c26e7467ab0.png

  七大优势
  • 强封装性:模块化使得开发者可以明确指定哪些是模块的公开API,哪些是内部实现,从而加强了封装性。这样一来,开发者可以控制他们的代码对外部世界的可见性,降低了耦合度,并提升了代码的安全性。

  • 清晰的依赖管理:在模块化系统中,每个模块必须明确声明其依赖的其他模块。这种显式依赖声明促使了更清晰、更稳定的依赖管理机制,便于构建和维护大型项目。

  • 提高性能:模块系统可以帮助JVM和编译器作出更优化的决策,因为它们明确知道哪些模块将会被使用,哪些不会。这可能导致更快的启动时间和更小的内存占用,尤其是在微服务和云原生应用场景下。

  • 更易于构建大型系统:模块化系统鼓励开发者将大型复杂的程序拆分为更小、更易于管理的部分。这种方式使得大型系统的构建、测试和维护变得更容易,同时提高了代码的重用性。

  • 更好的安全性:模块化系统限制了不必要的对模块内部实现的访问,从而降低了安全风险。它允许应用程序明确地控制哪些部分是可以被外界访问的,从而增强了整个应用的安全性。

  • 减少了应用的体积:由于模块化允许应用仅包含所需的模块,因此可以去除未使用的模块,减少应用程序的总体积。这在需要部署到资源受限的环境中时特别有价值。

  • 促进了模块间的明确界限:通过强制执行模块间的清晰界限,模块化帮助避免了"jar地狱"(jar hell)问题,即多个版本的jar文件在项目中造成的冲突和混乱。 

核心概念
  模块描述符
  • 主要作用

模块的核心在于 module descriptor(模块描述符),对应根目录下的 module-info.class 文件,而这个 class 文件是由源代码根目录下的 module-info.java 编译生成。Java 为 module-info.java 设计了专用的语法,包含 module、 requires、exports 等多个关键词。

5ab0187b60369e20550405b0bae052de.png

  • 语法解读

  • [open] module <module>:声明一个模块,模块名称应全局唯一,不可重复。加上 open 关键词表示模块内的所有包都允许通过 Java 反射访问,模块声明体内不再允许使用 opens 语句。

  • requires [transitive] <module>: 声明模块依赖,一次只能声明一个依赖,如果依赖多个模块,需要多次声明。加上 transitive 关键词表示传递依赖,比如模块 A 依赖模块 B,模块 B 传递依赖模块 C,那么模块 A 就会自动依赖模块 C,类似于 Maven。

  • exports <package> [to <module1>[, <module2>...]]:导出模块内的包(允许直接 import 使用),一次导出一个包,如果需要导出多个包,需要多次声明。如果需要定向导出,可以使用 to 关键词,后面加上模块列表(逗号分隔)。

  • opens <package> [to <module>[, <module2>...]]:开放模块内的包(允许通过 Java 反射访问),一次开放一个包,如果需要开放多个包,需要多次声明。如果需要定向开放,可以使用 to 关键词,后面加上模块列表(逗号分隔)。

  • provides <interface | abstract class> with <class1>[, <class2> ...]:声明模块提供的 Java SPI 服务,一次可以声明多个服务实现类(逗号分隔)。

  • uses <interface | abstract class>:声明模块依赖的 Java SPI 服务,加上之后模块内的代码就可以通过ServiceLoader.load(Class) 一次性加载所声明的 SPI 服务的所有实现类。

  • 示例说明

e60352f4953428095ecfe3e8de53e33f.png

  • mod1 模块: 主模块,展示了使用服务实现类的两种方式。

module-info.java:

import mod3.exports.IEventListener;
module mod1 {
    requires mod2a;
    requires mod4;
    uses IEventListener;
}

EventCenter.java(主函数):

package mod1;
import mod2a.exports.EchoListener;
import mod3.exports.IEventListener;
import mod4.Events;
import java.util.ArrayList;
import java.util.ServiceLoader;
import java.util.stream.Collectors;
public class EventCenter {
   // 方式1:通过exports和opens
    System.out.println("Demo: Direct Mode");
    var listeners = new ArrayList<IEventListener>();
    // 使用导出类
    listeners.add(new EchoListener());
    // 使用开放类
    // compile error: listeners.add(new ReflectEchoListener());
    listeners.add((IEventListener<String>) Class.forName("mod2a.opens.ReflectEchoListener").getDeclaredConstructor().newInstance());
    var event = Events.newEvent();
    listeners.forEach(l -> l.onEvent(event));
    System.out.println();


    // 方式2:通过SPI
    System.out.println("Demo: SPI Mode");
    // 加载所有的IEventListener实现类,无视其导出/开放与否
    var listeners2 = ServiceLoader.load(IEventListener.class).stream().map(ServiceLoader.Provider::get).collect(Collectors.toList());
    // compile error: listeners.add(new InternalEchoListener());
    // compile error: listeners.add(new SpiEchoListener());
    var event2 = Events.newEvent();
    listeners2.forEach(l -> l.onEvent(event2));
}

shell 执行脚本:

#!/bin/zsh


# mod4
javac -d out/mods/mod4 mod4/src/**/*.java
jar -cvf out/mods/mod4.jar -C out/mods/mod4 .


# mod3
javac -d out/mods/mod3 mod3/src/**/*.java
jar -cvf out/mods/mod3.jar -C out/mods/mod3 .


# mod2b
javac -p out/mods/mod3.jar -d out/mods/mod2b mod2b/src/**/*.java
jar -cvf out/mods/mod2b.jar -C out/mods/mod2b .


# mod2a
javac -p out/mods/mod3.jar -d out/mods/mod2a mod2a/src/**/*.java
jar -cvf out/mods/mod2a.jar -C out/mods/mod2a .


# mod1
javac -p out/mods/mod2a.jar:out/mods/mod2b.jar:out/mods/mod3.jar:out/mods/mod4.jar -d out/mods/mod1 mod1/src/**/*.java
jar -cvf out/mods/mod1.jar -C out/mods/mod1 .


# run
java -p out/mods/mod1.jar:out/mods/mod2a.jar:out/mods/mod2b.jar:out/mods/mod3.jar:out/mods/mod4.jar -m mod1/mod1.EventCenter
  • mod2a 模块: 分别导出和开放了一个包,并声明了两个服务实现类。

module-info.java:

import mod3.exports.IEventListener;
module mod2a {
    requires transitive mod3;
    exports mod2a.exports;
    opens mod2a.opens;
    provides IEventListener
            with mod2a.exports.EchoListener, mod2a.opens.ReflectEchoListener;
}

EchoListener.java & ReflectEchoListener.java:

package mod2a.exports;
import mod3.exports.IEventListener;
public class EchoListener implements IEventListener<String> {


    @Override
    public void onEvent(String event) {
        System.out.println("[echo] Event received: " + event);
    }
}


package mod2a.opens;
import mod3.exports.IEventListener;
public class ReflectEchoListener implements IEventListener<String> {


    @Override
    public void onEvent(String event) {
        System.out.println("[reflect echo] Event received: " + event);
    }
}
  • mod2b 模块: 声明了一个未公开的服务实现类。

module-info.java:

import mod3.exports.IEventListener;
module mod2b {
    requires transitive mod3;
    provides IEventListener
            with mod2b.SpiEchoListener;
}

SpiEchoListener.java:

package mod2b;
import mod3.exports.IEventListener;
public class SpiEchoListener implements IEventListener<String> {


    @Override
    public void onEvent(String event) {
        System.out.println("[spi echo] Event received: " + event);
    }
}
  • mod3 模块: 定义 SPI 服务(IEventListener),并声明了一个未公开的服务实现类。

module-info.java:

import mod3.exports.IEventListener;
module mod1 {
    requires mod2a;
    requires mod4;
    uses IEventListener;
}

IEventListener.java & InternalEchoListener.java:

package mod3.exports;
public interface IEventListener<T> {
    void onEvent(T event);
}




package mod3.internal;
import mod3.exports.IEventListener;
public class InternalEchoListener implements IEventListener<String> {


    @Override
    public void onEvent(String event) {
        System.out.println("[internal echo] Event received: " + event);
    }
}
  • mod4 模块: 导出公共模型类。

module-info.java:

module mod4 {
    exports mod4;
}

Events.java:

package mod4;
import java.util.UUID;
public class Events {


    public static String newEvent() {
        return UUID.randomUUID().toString();
    }
}
  主要参数
  • Java 9 引入了一系列新的参数用于编译和运行模块,其中最重要的两个参数是 -p 和 -m。-p 参数指定模块路径,多个模块之间用 “:”(Mac, Linux)或者 “;”(Windows)分隔,同时适用于 javac 命令和 java 命令,用法和Java 8 中的 -cp 非常类似。-m 参数指定待运行的模块主函数,输入格式为模块名/主函数所在的类名,仅适用于 java 命令。两个参数的基本用法如下:

    • javac -p <module_path> <source>

    • java -p <module_path> -m <module>/<main_class>

  • 最快的速度判别它是不是一个模块:jar -d -f <jar_file> 

  关键指令
  • --add-exports 访问内部API

    • 如果旧代码迁移到JDK9+, 编译报package x.x.x is not visible 的错误时,是因为旧代码访问了模块的内部API;为了能够访问内部API,需要在编译时加上--add-exports java.xx/x.x.x=ALL-UNNAMED

  • --add-open 反射访问内部API

    • 但是,这种方式只是在编译期起作用;那些在x在运行时才知道访问了内部API的问题需要新的方式解决; 错误异常通常是 java.lang.reflect.InaccessibleObjectException..

    • 解决方式是运行时加上--add-opens x.x/x.x.x=All-UNNAMED通过反射调用的类和方法;

  • --add-modules 添加依赖模块

    • 如项目依赖Java EE的相关xml代码, 编译和运行时就需要添加对应的模块javac --add-moudles java.xml.bind

  • --patch-modules指定到特定模块

    • 当迁移过程中讨论拆包装,我们看到了一个使用注释的项目的例子 @ 生成(从java.xml.ws.annotation模块)和 @ 非空(从JSR 305实现)。我们发现了三件事:

      • 两个注释都在 javax .annotation包中,因此创建了一个分割

      • 需要手动添加模块,因为它是一个Java EE模块

      • 这样做会使拆分包的JSR 305部分不可见

我们可以使用 --patch-module来修补分割:

java --add-modules java.xml.ws.annotation
     --patch-module java.xml.ws.annotation=jsr305-3.0.2.jar
     --class-path $dependencies
     -jar $appjar
  • --add-reads 模块读取另一个模块

模块化策略

兼容老版本的应用,先来了解两个高级概念:未命名模块(unnamed module)和 自动模块(automatic module)

5bf3de4d7f55dfe6a113d561d1075504.png

判断:一个未经模块化改造的 jar 文件是转为未命名模块还是自动模块,取决于这个 jar 文件出现的路径,如果是类路径,那么就会转为未命名模块,如果是模块路径,那么就会转为自动模块。

  • 注意:自动模块也属于命名模块的范畴,其名称是模块系统基于 jar 文件名自动推导得出的,比如 com.foo.bar-1.0.0.jar 文件推导得出的自动模块名是 com.foo.bar。

  • 关键区别:分裂包规则适用于自动模块,但对未命名模块无效,也即多个未命名模块可以导出同一个包,但自动模块不允许。

未命名模块和自动模块存在的意义在于,无论传入的 jar 文件是否一个合法的模块(包含 module descriptor),Java 内部都可以统一的以模块的方式进行处理,这也是 Java 9 兼容老版本应用的架构原理。运行老版本应用时,所有 jar 文件都出现在类路径下,也就是转为未命名模块,对于未命名模块而言,默认导出所有包并且依赖所有模块,因此应用可以正常运行。进一步的解读可以参阅官方白皮书的相关章节。

  自底向上
  • 思路:

    • 根据 jar 包依赖关系( jdeps 工具进行分析),沿着依赖树自底向上对 jar 包进行模块化改造(在 jar 包的源代码根目录下添加合法的模块描述文件 module-info.java)

    • 初始时,所有 jar 包都是非模块化的,全部置于类路径下(转为未命名模块),应用以传统方式启动

    • 然后,开始自底向上对 jar 包进行模块化改造,改造完的 jar 包就移到模块路径下,这期间应用仍以传统方式启动

    • 最后,等所有 jar 包都完成模块化改造,应用改为 -m 方式启动,这也标志着应用已经迁移为真正的 Java 9 应用。

  • 举例说明:

9966d6d2467cedd74dbdbddc6c302ce6.png

    • 所有 jar 包都是非模块化的,运行命令为:java -cp mod1.jar:mod2a.jar:mod2b.jar:mod3.jar:mod4.jar mod1.EventCenter

    • mod3 和 mod4 模块化后,运行命令为:java -cp mod1.jar:mod2a.jar:mod2b.jar -p mod3.jar:mod4.jar --add-modules mod3,mod4 mod1.EventCenter

对比上一步的命令,首先 mod3.jar 和 mod4.jar 从类路径移到了模块路径,这个很好理解,因为这两个 jar 包已经改造成了真正的模块。其次,多了一个额外的参数 --add-modules mod3,mod4,这是为什么呢?这就要谈到模块系统的模块发现机制了。

不管是编译时,还是运行时,模块系统首先都要确定一个或者多个根模块(root module),然后从这些根模块开始根据模块依赖关系在模块路径中循环找出所有可观察到的模块(observable module),这些可观察到的模块加上类路径下的 jar 文件最终构成了编译时环境和运行时环境。那么根模块是如何确定的呢?对于运行时而言,如果应用是通过 -m 方式启动的,那么根模块就是 -m 指定的主模块;如果应用是通过传统方式启动的,那么根模块就是所有的 java.* 模块即 JRE。回到前面的例子,如果不加 --add-modules 参数,那么运行时环境中除了 JRE 就只有 mod1.jar、mod2a.jar、mod2b.jar,没有 mod3、mod4 模块,就会报 java.lang.ClassNotFoundException 异常。如你所想,--add-modules 参数的作用就是手动指定额外的根模块,这样应用就可以正常运行了。

  • mod2a、mod2b 的模块化后,运行命令为:java -cp mod1.jar -p mod2a.jar:mod2b.jar:mod3.jar:mod4.jar --add-modules mod2a,mod2b,mod4 mod1.EventCenter

    • 由于 mod2a、mod2b 都依赖 mod3,所以 mod3 就不用加到 --add-modules 参数里了。

  • 最后完成 mod1 的模块化改造,最终运行命令就简化为:java -p mod1.jar:mod2a.jar:mod2b.jar:mod3.jar:mod4.jar -m mod1/mod1.EventCenter

    • 注意此时应用是以 -m 方式启动,并且指定了 mod1 为主模块(也是根模块),因此所有其他模块根据依赖关系都会被识别为可观察到的模块并加入到运行时环境,应用可以正常运行。

  自上而下
  • 问题:自底向上策略很容易理解,实施路径也很清晰,但它存在一个有些模块无法进行模块化改造的问题。

  • 思路:根据 jar 包依赖关系,从主应用开始,沿着依赖树自上而下分析各个 jar 包模块化改造的可能性,将 jar 包分为两类:

    • 第一类:可以改造的,我们仍然采用自底向上策略进行改造,直至主应用完成改造;

    • 第二类:无法改造的,需要从一开始就放入模块路径,即转为自动模块。这里就要谈一下自动模块设计的精妙之处,首先,自动模块会导出所有包,这样就保证第一类 jar 包可以照常访问自动模块,其次,自动模块依赖所有命名模块,并且允许访问所有未命名模块的类(这一点很重要,因为除自动模块之外,其它命名模块是不允许访问未命名模块的类),这样就保证自动模块自身可以照常访问其他类。等到主应用完成模块化改造,应用的启动方式就可以改为 -m 方式。

  • 举例:还是以示例工程为例,假设 mod4 是一个第三方 jar 包,无法进行模块化改造,那么最终改造完之后,虽然应用运行命令和之前一样还是java -p mod1.jar:mod2a.jar:mod2b.jar:mod3.jar:mod4.jar -m mod1/mod1.EventCenter,但其中只有 mod1、mod2a、mod2b、mod3 是真正的模块,mod4 未做任何改造,借由模块系统转为自动模块。

be33450cfa5c991ab37e2c38590d88ff.png

  • 不完美之处:看上去很完美,不过等一下,如果有多个自动模块,并且它们之间存在分裂包呢?前面提到,自动模块和其它命名模块一样,需要遵循分裂包规则。对于这种情况,如果模块化改造势在必行,要么忍痛割爱精简依赖只保留其中的一个自动模块,要么自己动手丰衣足食 Hack 一个版本。当然,你也可以试试找到这些自动模块的维护者们,让他们 PK 一下决定谁才是这个分裂包的主人。

a8ca1661e71aa2f7f96268c3943d058b.png

最佳实践

  实践建议
  • 模块命名规范:给模块取一个有意义的名字,通常使用逆域名表示法(例如:com.example.myapp)。

  • 明确的依赖关系:在module-info.java文件中明确声明模块的依赖关系,以确保应用程序的模块之间的依赖关系清晰可见。

  • 最小依赖原则:尽量减少模块之间的依赖关系,只依赖于真正需要的模块。

  • 版本化的依赖关系:如果可能的话,使用版本化的依赖关系来确保模块依赖的是正确的版本。

  • 单一责任原则:将每个模块限制为一个特定的功能或领域,以提高可维护性和可重用性。

  • 测试和验证:确保模块之间的依赖关系和交互在编译时和运行时都能正常工作。

  • 模块路径管理:管理模块路径以确保应用程序能够正确加载和运行。

  注意事项
  • 模块依赖关系:仔细考虑您的模块之间的依赖关系。确保模块之间的依赖关系是明确的,避免循环依赖。使用requires语句声明依赖关系,并根据需要使用requires transitive或requires static。

  • 版本管理:了解模块之间的版本管理。Java 9引入了模块化版本的概念,允许模块依赖于特定版本的其他模块。考虑使用requires static来声明可选的、仅在特定版本下才有效的依赖关系。

  • 模块命名:为您的模块选择合适的名称。模块名称应该唯一且易于理解。遵循Java的包命名约定,使用反向域名(例如com.example.mymodule)。

  • 模块路径:在运行应用程序时,使用--module-path选项指定模块路径。确保正确设置模块路径,以便Java可以找到并加载您的模块。

  • 非模块化库:如果您使用了非模块化的JAR文件,将其包装为自动模块或创建模块化的版本。非模块化库的依赖关系可能会引入复杂性。

  • 模块化库:考虑使用已经模块化的库,以减少与模块路径和版本管理相关的问题。

  • 运行时镜像:如果您使用jlink创建自定义运行时镜像,请确保包括了所有必要的模块,并排除不必要的模块,以减小应用程序的大小。

  • 测试:编写单元测试以确保模块化应用程序的正确性。使用模块路径和--module选项来模拟模块化环境进行测试。

  • 模块描述文件:模块描述文件(module-info.java)是模块化应用程序的关键组成部分。确保正确声明依赖关系、导出和打包模块,以及使用其他关键字来管理可见性。

  • 模块间通信:模块之间的通信应该在依赖模块的基础上进行。不要尝试绕过模块系统的可见性控制。

  • 跨模块访问:如果需要在模块之间共享数据或访问非公开成员,请使用opens和opens...to语句,以允许受信任的模块进行反射操作。

  • 性能和内存开销:模块化应用程序的启动时间和内存开销可能会有所增加。在部署和测试应用程序时,要考虑性能方面的因素。

  • 迁移:如果您正在迁移现有的应用程序到模块化架构,确保逐步迁移,以减少中断和问题。

  • 文档和培训:为开发团队提供关于模块化的文档和培训,以确保所有开发人员都理解和遵守模块化的最佳实践。

  • 工具支持:使用Java 9及更高版本,以充分利用模块化系统和相关的工具,如jdeps、jlink和jmod。

d761ae3edc2751fe52798f404a7014cd.png

参考资料

  • 《关于 Java 模块系统,看这一篇就够了》:

    https://blog.csdn.net/eMac/article/details/107131444

  • 《JDK8升级JDK11最全实践干货来了》:

    https://mp.weixin.qq.com/s/BBJQXCbqcjhlaXGh7mta4Q

  • 《Java技术栈模块化的七大优势,你了解多少?》:

    https://blog.csdn.net/atgfg/article/details/139015388

¤ 拓展阅读 ¤

3DXR技术 | 终端技术 | 音视频技术

服务端技术 | 技术质量 | 数据算法

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/776295.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

一文了解常见DNS问题

当企业的DNS出现故障时&#xff0c;为不影响企业的正常运行&#xff0c;团队需要能够快速确定问题的性质和范围。那么有哪些常见的DNS问题呢&#xff1f; 域名解析失败&#xff1a; 当您输入一个域名&#xff0c;但无法获取到与之对应的IP地址&#xff0c;导致无法访问相应的网…

Blazor 逐键搜索并动态反馈到url

Blazor 逐键搜索并动态反馈到url 源码 前言: 今天打开了 spotify 网页版找歌, 突然发现这个功能很抓眼球,于是试试blazor能不能模仿一下. 1. 节省时间,直接用模板开搞 新建项目,使用 Bootstrap Blazor App 模板 , 命名为 b22dynamicURL BootstrapBlazor简介: BootstrapBlaz…

数据库概念题总结

1、 2、简述数据库设计过程中&#xff0c;每个设计阶段的任务 需求分析阶段&#xff1a;从现实业务中获取数据表单&#xff0c;报表等分析系统的数据特征&#xff0c;数据类型&#xff0c;数据约束描述系统的数据关系&#xff0c;数据处理要求建立系统的数据字典数据库设计…

SQL注入方法

文章目录 前言如何测试与利用注入点手工注入思路工具sqlmap-r-u-m--level--risk-v-p--threads-batch-smart--os-shell--mobiletamper插件获取数据的相关参数 前言 记录一些注入思路和经常使用的工具&#xff0c;后续有用到新的工具和总结新的方法再继续补充。 如何测试与利用注…

使用 OpenCV 和 Python 进行车道检测和物体检测(YOLO)

本项目旨在开发一个集车道检测与物体检测功能于一体的智能视觉分析系统&#xff0c;利用先进的计算机视觉技术和深度学习模型&#xff0c;实现实时的道路场景理解和目标识别。系统主要依托OpenCV这一强大的计算机视觉库&#xff0c;以及Python作为编程语言&#xff0c;融合了车…

CentOS7.9下yum升级Apache HTTP Server2.4.6到2.4.60

1、CentOS7.9系统默认的Apache版本 在CentOS7.9上&#xff0c;如果使用yum安装Apache HTTP Server是最多到2.4.6版本的&#xff0c;这是因为el7下官方仓库的最高版本就是2.4.6&#xff0c;证据如下&#xff1a; $ yum info httpd ...... Installed Packages Name : ht…

Jetpack Compose实战教程(五)

Jetpack Compose实战教程&#xff08;五&#xff09; 第五章 如何在Compose UI中使用基于命令式UI的自定义View 文章目录 Jetpack Compose实战教程&#xff08;五&#xff09;一、前言二、本章目标三、开始编码3.1 先让自定义控件能跑起来3.2给自定义控件使用compose的方式赋值…

【设计模式】工厂模式(定义 | 特点 | Demo入门讲解)

文章目录 定义简单工厂模式案例 | 代码Phone顶层接口设计Meizu品牌类Xiaomi品牌类PhoneFactory工厂类Customer 消费者类 工厂方法模式案例 | 代码PhoneFactory工厂类 Java高级特性---工厂模式与反射的高阶玩法方案&#xff1a;反射工厂模式 总结 其实工厂模式就是用一个代理类帮…

Java项目:基于SSM框架实现的学生公寓管理中心系统【ssm+B/S架构+源码+数据库+毕业论文】

一、项目简介 本项目是一套基于SSM框架实现的学生公寓管理中心系统 包含&#xff1a;项目源码、数据库脚本等&#xff0c;该项目附带全部源码可作为毕设使用。 项目都经过严格调试&#xff0c;eclipse或者idea 确保可以运行&#xff01; 该系统功能完善、界面美观、操作简单、…

【CentOS 7.6】Linux版本 portainer本地镜像导入docker安装配置教程,不需要魔法拉取!(找不着镜像的来看我)

吐槽 我本来根本不想写这篇博客&#xff0c;但我很不解也有点生气&#xff0c;CSDN这么大没有人把现在需要魔法才能拉取的镜像放上来。 你们都不放&#xff0c;根本不方便。我来上传资源。 portainer-ce-latest.tar Linux/amd64 镜像下载地址&#xff1a; 链接&#xff1a;h…

加法器的基本操作

基本单元 与门(AND) 全1为1&#xff0c;有0为0 或门(OR) 全0为0&#xff0c;有1为1 非门(NOT) 为1则0&#xff0c;为0则1 异或门(XOR) 两个输入端&#xff0c;相同为0&#xff0c;不同为1 与非门(NADD) 全1为0&#xff0c;有0为1 或非门(NOR) 全0为1&#xff0c;有1为0。刚…

H5 Canvas实现转盘效果,控制指定数字

效果图 实现思路&#xff1a; 用Canvas画圆&#xff0c;然后再画扇形&#xff0c;然后中奖的开始用一张图片代替&#xff0c;点击的时候触发转动效果。 实现代码&#xff1a; <!DOCTYPE html> <html> <head><meta charset"utf-8"><tit…

UNDO 表空间使用率高 active段占用高 无对应事务执行

UNDO表空间使用率告警&#xff0c;查看占用情况 active段占比很高 select tablespace_name,status,sum(bytes/1024/1024) mb from dba_undo_extents group by tablespace_name,status;不同状态的含义&#xff1a;**ACTIVE **&#xff1a;有活动事务在使用 Undo&#xff0c;这…

【JavaSE】数据类型与变量

目录 1. 字面常量 2. 数据类型 3. 变量 3.1 变量概念 3.2 语法格式 3.3 整型变量 3.3.1 整型变量 3.3.2 长整型变量 3.3.3 短整型变量 3.3.4 字节型变量 3.4 浮点型变量 3.4.1 双精度浮点型 3.4.2 单精度浮点型 3.5 字符型变量 3.6 布尔型变量 3.7 类型转换 3…

WEB安全-靶场

1 需求 2 语法 3 示例 男黑客|在线渗透测试靶场|网络安全培训基地|男黑客安全网 4 参考资料

windows 7 安装IPP协议,支持Internet打印

1 windows 7 安装IPP协议,支持Internet打印 #控制面板--打开或关闭Windows功能 3 复制Printers 文件夹 到 c:\inetpub\wwwroot\,复制msw3prt.dll到c:\windows\system32\ 4 打开IIs管理器 #报错:模块列表中不存在此处理程序所需的指定模块。如果您添加脚本映射处理程序映射&…

【力扣】数组中的第K个最大元素

一、题目描述 给定整数数组 nums 和整数 k&#xff0c;请返回数组中第 k 个最大的元素。 请注意&#xff0c;你需要找的是数组排序后的第 k 个最大的元素&#xff0c;而不是第 k 个不同的元素。 你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。 示例 1: 输入: [3,2,1,5,…

记一次EasyExcel的错误使用导致的频繁FullGC

记一次EasyExcel的错误使用导致的频繁FullGC 一、背景描述二、场景复现三、原因分析四、解决方案五、思考复盘 一、背景描述 繁忙的校招结束了&#xff0c;美好的大学四年也结束了&#xff0c;作者也有10个月没有更新了。拿到心仪的offer之后也开始了苦B的打工生活。 最近接到…

汽车信息安全--欧盟汽车法规

目录 General regulation 信息安全法规 R155《网络安全及网络安全管理系统》解析 R156《软件升级与软件升级管理系统》解析 General regulation 欧洲的汽车行业受到一系列法律法规的约束&#xff0c;包括 各个方面包括&#xff1a; 1.安全要求&#xff1a;《通用安全条例&a…

10.x86游戏实战-汇编指令lea

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 本次游戏没法给 内容参考于&#xff1a;微尘网络安全 工具下载&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1rEEJnt85npn7N38Ai0_F2Q?pwd6tw3 提…