spring-boot-maven-plugin:repackage分析

news2025/1/11 13:01:35

springboot项目打包插件为:spring-boot-maven-plugin,使用如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.2.11.RELEASE</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
    </build>

</project>

可以看出这里调用的是repackage的实现(repackage能够将mvn package生成的软件包,再次打包为可执行的软件包,并将mvn package生成的软件包重命名为*.original),我们来看实现代码:

@Mojo(name = "repackage", defaultPhase = LifecyclePhase.PACKAGE, requiresProject = true, threadSafe = true,
      requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME,
      requiresDependencyCollection = ResolutionScope.COMPILE_PLUS_RUNTIME)
public class RepackageMojo extends AbstractDependencyFilterMojo {

默认执行execute方法:

@Override
public void execute() throws MojoExecutionException, MojoFailureException {
    if (this.project.getPackaging().equals("pom")) {
        getLog().debug("repackage goal could not be applied to pom project.");
        return;
    }
    //这里设置为true的话,不会进行重新打包,仍然是原始的maven包
    if (this.skip) {
        getLog().debug("skipping repackaging as per configuration.");
        return;
    }
    repackage();
}
private void repackage() throws MojoExecutionException {
    Artifact source = getSourceArtifact();
	//最终为可执行的jar 即fat jar
    File target = getTargetFile();
	//获取重新打包器,将maven生成的jar重新打包成可执行jar
    Repackager repackager = getRepackager(source.getFile());
	//查找并过滤项目运行时的依赖
    Set<Artifact> artifacts = filterDependencies(this.project.getArtifacts(), getFilters(getAdditionalFilters()));
    //将 artifacts 转换成 libraries
	Libraries libraries = new ArtifactsLibraries(artifacts, this.requiresUnpack, getLog());
    try {
        //获取springboot启动脚本
        LaunchScript launchScript = getLaunchScript();
        //重新执行打包,生成fat jar
        repackager.repackage(target, libraries, launchScript);
    }
    catch (IOException ex) {
        throw new MojoExecutionException(ex.getMessage(), ex);
    }
	//将maven生成的jar更新为.original文件
    updateArtifact(source, target, repackager.getBackupFile());
}

Artifact:这里我觉得可以理解为对应的原始的maven打包的jar包的描述性文件,获取如下

private Artifact getSourceArtifact() {
    Artifact sourceArtifact = getArtifact(this.classifier);
    return (sourceArtifact != null) ? sourceArtifact : this.project.getArtifact();
}

private Artifact getArtifact(String classifier) {
if (classifier != null) {
    for (Artifact attachedArtifact : this.project.getAttachedArtifacts()) {
        if (classifier.equals(attachedArtifact.getClassifier()) && attachedArtifact.getFile() != null
            && attachedArtifact.getFile().isFile()) {
            return attachedArtifact;
        }
    }
}
return null;
}

关于classifier可以参考:Apache Maven JAR Plugin – How to create an additional attached jar artifact from the project

https://www.baeldung.com/maven-artifact-classifiers,这里我理解为maven打包时候可能会打包出多个jar包,我们根据classifier去多个jar包进行寻找对应的目标包,倘若没找到,则采用maven默认打包的jar包作为原始包进行重新打包处理

private File getTargetFile() {
    String classifier = (this.classifier != null) ? this.classifier.trim() : "";
    if (!classifier.isEmpty() && !classifier.startsWith("-")) {
        classifier = "-" + classifier;
    }
    if (!this.outputDirectory.exists()) {
        this.outputDirectory.mkdirs();
    }
    return new File(this.outputDirectory,
                    this.finalName + classifier + "." + this.project.getArtifact().getArtifactHandler().getExtension());
}

这里创建打包之后存放的默认文件夹并且根据classifier创建最终jar包文件的文件句柄,这里指File对象

private Repackager getRepackager(File source) {
Repackager repackager = new Repackager(source, this.layoutFactory);
repackager.addMainClassTimeoutWarningListener(new LoggingMainClassTimeoutWarningListener());
repackager.setMainClass(this.mainClass);
if (this.layout != null) {
    getLog().info("Layout: " + this.layout);
    repackager.setLayout(this.layout.layout());
}
return repackager;
}

private class LoggingMainClassTimeoutWarningListener implements MainClassTimeoutWarningListener {

    @Override
    public void handleTimeoutWarning(long duration, String mainMethod) {
        getLog().warn("Searching for the main-class is taking some time, "
                + "consider using the mainClass configuration parameter");
    }

}

这里主要是进行Repackager类的构建,Repackager存储了重新打包所需的信息,比如打包类型、mainClass等等,内部还有repackage即重新打包的具体实现。这里的layout代表了打包文件的格式,目前支持JAR, WAR, ZIP, DIR类型

protected Set<Artifact> filterDependencies(Set<Artifact> dependencies, FilterArtifacts filters)
throws MojoExecutionException {
    try {
        Set<Artifact> filtered = new LinkedHashSet<>(dependencies);
        filtered.retainAll(filters.filter(dependencies));
        return filtered;
    }
    catch (ArtifactFilterException ex) {
        throw new MojoExecutionException(ex.getMessage(), ex);
    }
}

protected final FilterArtifacts getFilters(ArtifactsFilter... additionalFilters) {
    FilterArtifacts filters = new FilterArtifacts();
    for (ArtifactsFilter additionalFilter : additionalFilters) {
        filters.addFilter(additionalFilter);
    }
    filters.addFilter(new MatchingGroupIdFilter(cleanFilterConfig(this.excludeGroupIds)));
    if (this.includes != null && !this.includes.isEmpty()) {
        filters.addFilter(new IncludeFilter(this.includes));
    }
    if (this.excludes != null && !this.excludes.isEmpty()) {
        filters.addFilter(new ExcludeFilter(this.excludes));
    }
    return filters;
}

private ArtifactsFilter[] getAdditionalFilters() {
    List<ArtifactsFilter> filters = new ArrayList<>();
    if (this.excludeDevtools) {
        Exclude exclude = new Exclude();
        exclude.setGroupId("org.springframework.boot");
        exclude.setArtifactId("spring-boot-devtools");
        ExcludeFilter filter = new ExcludeFilter(exclude);
        filters.add(filter);
    }
    if (!this.includeSystemScope) {
        filters.add(new ScopeFilter(null, Artifact.SCOPE_SYSTEM));
    }
    return filters.toArray(new ArtifactsFilter[0]);
}

这里主要进行的符合条件的jar的过滤,代码简单不多说

private LaunchScript getLaunchScript() throws IOException {
if (this.executable || this.embeddedLaunchScript != null) {
    return new DefaultLaunchScript(this.embeddedLaunchScript, buildLaunchScriptProperties());
}
return null;
}

private Properties buildLaunchScriptProperties() {
    Properties properties = new Properties();
    if (this.embeddedLaunchScriptProperties != null) {
        properties.putAll(this.embeddedLaunchScriptProperties);
    }
    putIfMissing(properties, "initInfoProvides", this.project.getArtifactId());
    putIfMissing(properties, "initInfoShortDescription", this.project.getName(), this.project.getArtifactId());
    putIfMissing(properties, "initInfoDescription", removeLineBreaks(this.project.getDescription()),
                 this.project.getName(), this.project.getArtifactId());
    return properties;
}

这里支持执行脚本,可以进行配置,默认会进行initInfoProvides、initInfoShortDescription、initInfoDescription三个属性的设置

//destination代表最终生成的文件 libraries代表项目的依赖包 launchScript需要执行的脚本文件
public void repackage(File destination, Libraries libraries, LaunchScript launchScript) throws IOException {
    if (destination == null || destination.isDirectory()) {
        throw new IllegalArgumentException("Invalid destination");
    }
    if (libraries == null) {
        throw new IllegalArgumentException("Libraries must not be null");
    }
    if (this.layout == null) {
        this.layout = getLayoutFactory().getLayout(this.source);
    }
    destination = destination.getAbsoluteFile();
    File workingSource = this.source;
    //如果Manifest文件已经有Spring-Boot-Version属性,说明已经重新打包过了 文件名也一样的话就不用继续了 说明是一个
    if (alreadyRepackaged() && this.source.equals(destination)) {
        return;
    }
    //2个文件名相同的话 说明打包过一次了,获取之前的.original即原始jar包删除,将新包重新命名为 原始包名.original
    if (this.source.equals(destination)) {
        workingSource = getBackupFile();
        workingSource.delete();
        renameFile(this.source, workingSource);
    }
    //删除上一次的重打包文件
    destination.delete();
    try {
        try (JarFile jarFileSource = new JarFile(workingSource)) {
            //这里执行的是真正的打包过程
            repackage(jarFileSource, destination, libraries, launchScript);
        }
    }
    finally {
        if (!this.backupSource && !this.source.equals(workingSource)) {
            deleteFile(workingSource);
        }
    }
}
private static final String BOOT_VERSION_ATTRIBUTE = "Spring-Boot-Version";
private boolean alreadyRepackaged() throws IOException {
    try (JarFile jarFile = new JarFile(this.source)) {
        Manifest manifest = jarFile.getManifest();
        return (manifest != null && manifest.getMainAttributes().getValue(BOOT_VERSION_ATTRIBUTE) != null);
    }
}

public final File getBackupFile() {
    return new File(this.source.getParentFile(), this.source.getName() + ".original");
}

这里主要进行的历史文件的处理和重新命名,接下里看真正的打包过程:

//sourceJar:原始jar文件 destination:目标jar文件
private void repackage(JarFile sourceJar, File destination, Libraries libraries, LaunchScript launchScript)
throws IOException {
    WritableLibraries writeableLibraries = new WritableLibraries(libraries);
    try (JarWriter writer = new JarWriter(destination, launchScript)) {
        writer.writeManifest(buildManifest(sourceJar));
        writeLoaderClasses(writer);
        if (this.layout instanceof RepackagingLayout) {
            writer.writeEntries(sourceJar,
                                new RenamingEntryTransformer(((RepackagingLayout) this.layout).getRepackagedClassesLocation()),
                                writeableLibraries);
        }
        else {
            writer.writeEntries(sourceJar, writeableLibraries);
        }
        writeableLibraries.write(writer);
    }
}

private WritableLibraries(Libraries libraries) throws IOException {
    libraries.doWithLibraries((library) -> {
        if (isZip(library.getFile())) {
            //这里到对应的打包目标中寻找jar包 这里有jar和war2种不同的实现,对应不同的目录
            String libraryDestination = Repackager.this.layout.getLibraryDestination(library.getName(),
                    library.getScope());
            if (libraryDestination != null) {
                //这里建立一个索引,方便查找
                Library existing = this.libraryEntryNames.putIfAbsent(libraryDestination + library.getName(),
                        library);
                if (existing != null) {
                    throw new IllegalStateException("Duplicate library " + library.getName());
                }
            }
        }
    });
}

public JarWriter(File file, LaunchScript launchScript) throws FileNotFoundException, IOException {
    FileOutputStream fileOutputStream = new FileOutputStream(file);
    if (launchScript != null) {
        fileOutputStream.write(launchScript.toByteArray());
        setExecutableFilePermission(file);
    }
    this.jarOutput = new JarArchiveOutputStream(fileOutputStream);
    this.jarOutput.setEncoding("UTF-8");
}

private void setExecutableFilePermission(File file) {
    try {
        Path path = file.toPath();
        //获取文件权限
        Set<PosixFilePermission> permissions = new HashSet<>(Files.getPosixFilePermissions(path));
        permissions.add(PosixFilePermission.OWNER_EXECUTE);
        Files.setPosixFilePermissions(path, permissions);
    }
    catch (Throwable ex) {
        // Ignore and continue creating the jar
    }
}

这里先说下manifest文件的构建:

private static final String MAIN_CLASS_ATTRIBUTE = "Main-Class";
private static final String START_CLASS_ATTRIBUTE = "Start-Class";
private static final String BOOT_VERSION_ATTRIBUTE = "Spring-Boot-Version";
private static final String BOOT_LIB_ATTRIBUTE = "Spring-Boot-Lib";
private static final String BOOT_CLASSES_ATTRIBUTE = "Spring-Boot-Classes";

private Manifest buildManifest(JarFile source) throws IOException {
    Manifest manifest = source.getManifest();
    if (manifest == null) {
        manifest = new Manifest();
        manifest.getMainAttributes().putValue("Manifest-Version", "1.0");
    }
    manifest = new Manifest(manifest);
    String startClass = this.mainClass;
    if (startClass == null) {
        startClass = manifest.getMainAttributes().getValue(MAIN_CLASS_ATTRIBUTE);
    }
    if (startClass == null) {
        startClass = findMainMethodWithTimeoutWarning(source);
    }
    String launcherClassName = this.layout.getLauncherClassName();
    if (launcherClassName != null) {
        manifest.getMainAttributes().putValue(MAIN_CLASS_ATTRIBUTE, launcherClassName);
        if (startClass == null) {
            throw new IllegalStateException("Unable to find main class");
        }
        manifest.getMainAttributes().putValue(START_CLASS_ATTRIBUTE, startClass);
    }
    else if (startClass != null) {
        manifest.getMainAttributes().putValue(MAIN_CLASS_ATTRIBUTE, startClass);
    }
    String bootVersion = getClass().getPackage().getImplementationVersion();
    manifest.getMainAttributes().putValue(BOOT_VERSION_ATTRIBUTE, bootVersion);
    manifest.getMainAttributes().putValue(BOOT_CLASSES_ATTRIBUTE, (this.layout instanceof RepackagingLayout)
                                          ? ((RepackagingLayout) this.layout).getRepackagedClassesLocation() : this.layout.getClassesLocation());
    String lib = this.layout.getLibraryDestination("", LibraryScope.COMPILE);
    if (StringUtils.hasLength(lib)) {
        manifest.getMainAttributes().putValue(BOOT_LIB_ATTRIBUTE, lib);
    }
    return manifest;
}

private String findMainMethodWithTimeoutWarning(JarFile source) throws IOException {
    long startTime = System.currentTimeMillis();
    String mainMethod = findMainMethod(source);
    long duration = System.currentTimeMillis() - startTime;
    if (duration > FIND_WARNING_TIMEOUT) {
        for (MainClassTimeoutWarningListener listener : this.mainClassTimeoutListeners) {
            listener.handleTimeoutWarning(duration, mainMethod);
        }
    }
    return mainMethod;
}
private static final String SPRING_BOOT_APPLICATION_CLASS_NAME = "org.springframework.boot.autoconfigure.SpringBootApplication";
protected String findMainMethod(JarFile source) throws IOException {
    return MainClassFinder.findSingleMainClass(source, this.layout.getClassesLocation(),
            SPRING_BOOT_APPLICATION_CLASS_NAME);
}

这里有个查找MainMethod的寻找过程,可以看下findMainMethod:

public static String findSingleMainClass(JarFile jarFile, String classesLocation, String annotationName)
throws IOException {
    SingleMainClassCallback callback = new SingleMainClassCallback(annotationName);
    MainClassFinder.doWithMainClasses(jarFile, classesLocation, callback);
    return callback.getMainClassName();
}
static <T> T doWithMainClasses(JarFile jarFile, String classesLocation, MainClassCallback<T> callback)
        throws IOException {
    List<JarEntry> classEntries = getClassEntries(jarFile, classesLocation);
    classEntries.sort(new ClassEntryComparator());
    for (JarEntry entry : classEntries) {
        try (InputStream inputStream = new BufferedInputStream(jarFile.getInputStream(entry))) {
            ClassDescriptor classDescriptor = createClassDescriptor(inputStream);
            if (classDescriptor != null && classDescriptor.isMainMethodFound()) {
                String className = convertToClassName(entry.getName(), classesLocation);
                T result = callback.doWith(new MainClass(className, classDescriptor.getAnnotationNames()));
                if (result != null) {
                    return result;
                }
            }
        }
    }
    return null;
}
private static final String DOT_CLASS = ".class";
private static List<JarEntry> getClassEntries(JarFile source, String classesLocation) {
    classesLocation = (classesLocation != null) ? classesLocation : "";
    Enumeration<JarEntry> sourceEntries = source.entries();
    List<JarEntry> classEntries = new ArrayList<>();
    while (sourceEntries.hasMoreElements()) {
        JarEntry entry = sourceEntries.nextElement();
        if (entry.getName().startsWith(classesLocation) && entry.getName().endsWith(DOT_CLASS)) {
            classEntries.add(entry);
        }
    }
    return classEntries;
}

private static ClassDescriptor createClassDescriptor(InputStream inputStream) {
    try {
        ClassReader classReader = new ClassReader(inputStream);
        ClassDescriptor classDescriptor = new ClassDescriptor();
        //这里用asm读取处理封装成ClassDescriptor对象
        classReader.accept(classDescriptor, ClassReader.SKIP_CODE);
        return classDescriptor;
    }
    catch (IOException ex) {
        return null;
    }
}
private static String convertToClassName(String name, String prefix) {
    name = name.replace('/', '.');
    name = name.replace('\\', '.');
    name = name.substring(0, name.length() - DOT_CLASS.length());
    if (prefix != null) {
        name = name.substring(prefix.length());
    }
    return name;
}

private String getMainClassName() {
    Set<MainClass> matchingMainClasses = new LinkedHashSet<>();
    if (this.annotationName != null) {
        for (MainClass mainClass : this.mainClasses) {
            if (mainClass.getAnnotationNames().contains(this.annotationName)) {
                matchingMainClasses.add(mainClass);
            }
        }
    }
    if (matchingMainClasses.isEmpty()) {
        matchingMainClasses.addAll(this.mainClasses);
    }
    if (matchingMainClasses.size() > 1) {
        throw new IllegalStateException(
                "Unable to find a single main class from the following candidates " + matchingMainClasses);
    }
    return (matchingMainClasses.isEmpty() ? null : matchingMainClasses.iterator().next().getName());
}

这里读取原始jar包中的文件,用ASM解析class文件,这里查找到包含main方法的类直接返回,而在getMainClassName方法中,有个路径:

  1. 如果注解不为空,默认注解为@SpringBootApplication,则有对应注解的都是符合条件的matchingMainClasses,如果解析的类中均没有注解且matchingMainClasses为空的,采用ASM解析过程中国存在main方法的对应类
  2. 如果注解为空,则直接采用ASM解析过程中国存在main方法的对应类
  3. 存在main方法的类型即matchingMainClasses只能有1个,不然报错

接下来看MANIFEST如何写出到文件:

public void writeManifest(Manifest manifest) throws IOException {
    JarArchiveEntry entry = new JarArchiveEntry("META-INF/MANIFEST.MF");
    writeEntry(entry, manifest::write);
}
private void writeEntry(JarArchiveEntry entry, EntryWriter entryWriter, UnpackHandler unpackHandler)
        throws IOException {
    String parent = entry.getName();
    if (parent.endsWith("/")) {
        parent = parent.substring(0, parent.length() - 1);
        entry.setUnixMode(UnixStat.DIR_FLAG | UnixStat.DEFAULT_DIR_PERM);
    }
    else {
        entry.setUnixMode(UnixStat.FILE_FLAG | UnixStat.DEFAULT_FILE_PERM);
    }
    if (parent.lastIndexOf('/') != -1) {
        parent = parent.substring(0, parent.lastIndexOf('/') + 1);
        if (!parent.isEmpty()) {
            writeEntry(new JarArchiveEntry(parent), null, unpackHandler);
        }
    }

    if (this.writtenEntries.add(entry.getName())) {
        entryWriter = addUnpackCommentIfNecessary(entry, entryWriter, unpackHandler);
        this.jarOutput.putArchiveEntry(entry);
        if (entryWriter != null) {
            entryWriter.write(this.jarOutput);
        }
        this.jarOutput.closeArchiveEntry();
    }
}

工具类不多说

writeLoaderClasses这里默认将spring-boot-loader.jar打包到了jar文件中,spring-boot-loader.jar负责spring应用的启动引导
private void writeLoaderClasses(JarWriter writer) throws IOException {
    if (this.layout instanceof CustomLoaderLayout) {
        ((CustomLoaderLayout) this.layout).writeLoadedClasses(writer);
    }
    else if (this.layout.isExecutable()) {
        writer.writeLoaderClasses();
    }
}
private static final String NESTED_LOADER_JAR = "META-INF/loader/spring-boot-loader.jar";
public void writeLoaderClasses() throws IOException {
    writeLoaderClasses(NESTED_LOADER_JAR);
}
public void writeLoaderClasses(String loaderJarResourceName) throws IOException {
    URL loaderJar = getClass().getClassLoader().getResource(loaderJarResourceName);
    try (JarInputStream inputStream = new JarInputStream(new BufferedInputStream(loaderJar.openStream()))) {
        JarEntry entry;
        while ((entry = inputStream.getNextJarEntry()) != null) {
            if (entry.getName().endsWith(".class")) {
                writeEntry(new JarArchiveEntry(entry), new InputStreamEntryWriter(inputStream));
            }
        }
    }
}

这里注意spring-boot-loader.jar(该类存在于插件内部)是spring应用的引导类,在writeLoaderClasses方法中,将他打包到了最终的目标文件中,

最后看下writeEntries的实现:

void writeEntries(JarFile jarFile, UnpackHandler unpackHandler) throws IOException {
    this.writeEntries(jarFile, new IdentityEntryTransformer(), unpackHandler);
}

void writeEntries(JarFile jarFile, EntryTransformer entryTransformer, UnpackHandler unpackHandler)
        throws IOException {
    Enumeration<JarEntry> entries = jarFile.entries();
    while (entries.hasMoreElements()) {
        JarArchiveEntry entry = new JarArchiveEntry(entries.nextElement());
        setUpEntry(jarFile, entry);
        try (ZipHeaderPeekInputStream inputStream = new ZipHeaderPeekInputStream(jarFile.getInputStream(entry))) {
            EntryWriter entryWriter = new InputStreamEntryWriter(inputStream);
            JarArchiveEntry transformedEntry = entryTransformer.transform(entry);
            if (transformedEntry != null) {
                writeEntry(transformedEntry, entryWriter, unpackHandler);
            }
        }
    }
}

工具类写出,不多说

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【python实现通过复数进程互相检测防止关闭和删除】

python实现通过复数进程互相检测防止关闭和删除 要使用 Python 实现通过多个进程互相检测来防止关闭和删除&#xff0c;可以使用 multiprocessing 模块来创建多个进程&#xff0c;并通过进程间通信来实现心跳检测。以下是一个简单的示例代码&#xff0c;展示了如何使用两个进程…

【学术前沿】基于非易失性存储器硬件特性的存算一体神经网络设计方法

【学术前沿】基于非易失性存储器硬件特性的存算一体神经网络设计方法 Lixia HAN, Peng HUANG, Yijiao WANG, Zheng ZHOU, Haozhang YANG, Yiyang CHEN, Xiaoyan LIU & Jinfeng KANG, Mitigating Methodology of Hardware Non-ideal Characteristics for Non-volatile Memo…

经常打喷嚏和浮毛猫毛满天飞有关吗?能去浮毛的空气净化器推荐

近年来&#xff0c;随着生活品质的提升&#xff0c;越来越多的家庭选择养宠物&#xff0c;养宠物的同时也带来了不少的困扰。比如&#xff0c;下班归家&#xff0c;迎接的可能是满室的异味与漂浮的毛发&#xff0c;瞬间让好心情大打折扣。长期置身于这样的环境中还对健康有影响…

笔记本没电造成数据丢失怎么恢复?速览几种有效方法

在日常使用笔记本电脑的过程中&#xff0c;我们时常会遇到电量耗尽而突然关机的情况。这种突如其来的断电不仅可能打断我们的工作流程&#xff0c;更有可能导致未保存的数据丢失&#xff0c;给我们的生活和工作带来不小的困扰。面对这种情况&#xff0c;许多用户可能会感到手足…

Jellyfin10.9.x解决色调映射无法使用的问题

环境 物理机为12100的CPU、精粤 H610I的主板、16G2内存、闪迪1TB M.2固态、希捷酷鹰3.5寸4TB硬盘4。 物理机安装了PVE后在PVE安装了NAS、OP、Windows 10、HA、Ubuntu还利用LXC模板安装了Debian12 里面套娃了Jellyfin。不过我的Jellyfin不是部署在docker中的。 问题 但是当我…

NSSCTF练习记录:[SWPUCTF 2021 新生赛]ez_caesar

题目&#xff1a; import base64 def caesar(plaintext):str_list list(plaintext)i 0while i < len(plaintext):if not str_list[i].isalpha():str_list[i] str_list[i]else:a "A" if str_list[i].isupper() else "a"str_list[i] chr((ord(str_…

蓝队技能-应急响应篇勒索病毒系统样本家族解密渠道寄生入口异常定性处置封锁

知识点 1、应急响应-勒索病毒-定性&排查 2、应急响应-勒索病毒-应急&处置1、什么是勒索病毒&#xff1f; 勒索病毒是一种新型电脑病毒&#xff0c;主要以RDP爆破、邮件、程序木马、网页挂马的形式进行传播。该病毒性质恶劣、危害极大&#xff0c;一旦感染将给用户带来…

vue3 RouterLink路由跳转后RouterView组件未加载,页面未显示,且控制台无任何报错

在使用 vue3 开发项目过程中&#xff0c;组件之间使用 router-link 跳转&#xff0c;但是当我开发的组件跳转到其他组件时&#xff0c;其他组件的页面未加载&#xff0c;再跳转回自己的组件时&#xff0c;自己的组件也加载不出来了&#xff0c;浏览器刷新后页面可以加载出来。但…

点亮希望之灯:解决孩子自闭症的探索之路

在这个充满活力与挑战的世界里&#xff0c;有一群特殊的孩子&#xff0c;他们仿佛生活在自己的孤独星球上&#xff0c;难以与外界进行有效的沟通和互动。他们是自闭症儿童&#xff0c;也被称为 “星星的孩子”。面对这些孩子的自闭症问题&#xff0c;我们该如何寻找解决之道呢&…

C语言06--数组进阶

数组名含义 数组名有两个含义&#xff1a; 第一含义是&#xff1a;整个数组第二含义是&#xff1a;首元素的地址当出现以下情形时&#xff0c;那么数组名就代表整个数组&#xff1a; 在数组定义中在 sizeof 运算表达式中 &#xff0c;因此sizeof 计算的就是整个数组的大小。si…

《黑神话:悟空》风灵月影V1.0-35项修改器全面解析

《黑神话&#xff1a;悟空》作为一款备受瞩目的魂系动作角色扮演游戏&#xff0c;以其细腻的画面、流畅的操作和深厚的剧情吸引了众多玩家的关注。然而&#xff0c;对于不少玩家来说&#xff0c;游戏的高难度设置也带来了不小的挑战。为此&#xff0c;风灵月影工作室特别推出了…

[Spring] Spring原理(SpringBoot完结)

&#x1f338;个人主页:https://blog.csdn.net/2301_80050796?spm1000.2115.3001.5343 &#x1f3f5;️热门专栏: &#x1f9ca; Java基本语法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12615970.html?spm1001.2014.3001.5482 &#x1f355; Collection与…

【15】Java字节码

Java方法栈帧的组成&#xff1a;操作数栈局部变量表 操作数栈 Java字节码是Java虚拟机所使用的的指令集。它与JVM基于栈的计算模型是分不开的。 在解释执行过程中&#xff0c;每当为 Java 方法分配栈桢时&#xff0c;Java 虚拟机往往需要开辟一块额外的空间作为操作数栈&…

JavaEE 第16节 线程安全的集合类

目录 前言 顺序表 队列 哈希表 1、Hashtable 2、ConcurrentHashMap&#xff08;重点&#xff09; 前言 本文章主要介绍在多线程环境下&#xff0c;如何线程安全的使用一些常用的集合类&#xff08;顺序表和哈希表&#xff09;。 顺序表 1、自己使用同步锁机制&#xff…

模拟笔试:卡码网2023年快手笔试真题

1.158同余方程 思路 纯数学的思路&#xff0c;想不出来的话很难做。 欧几里得算法视频讲解 代码 #include <iostream> using namespace std;// 扩展欧几里得&#xff1a;计算 ax by gcd(a, b) 的解 long long extended_gcd(long long a, long long b, long long &a…

Java语言程序设计——篇十五(5)

&#x1f33f;&#x1f33f;&#x1f33f;跟随博主脚步&#xff0c;从这里开始→博主主页&#x1f33f;&#x1f33f;&#x1f33f; 欢迎大家&#xff1a;这里是我的学习笔记、总结知识的地方&#xff0c;喜欢的话请三连&#xff0c;有问题可以私信&#x1f333;&#x1f333;&…

【STM32嵌入式系统设计与开发拓展】——16_FreeRTOS操作系统

参考&#xff1a;链接: 正点原子 一、认识裸机和RTOS 裸机是无操作系统支持&#xff0c;程序直接运行在硬件上&#xff0c;开发者要自行处理硬件细节。早期单片机常采用&#xff0c;优点是性能和资源利用率高&#xff0c;缺点是开发难、可移植性差。RTOS 是实时操作系统&…

vscode导入的包裹代码名称没有颜色

问题描述:代码其他染色正常,但是例如import torchtorch没有颜色,虽然能够识别(ctrl左键能够点进去看到torch代码) 解决: 下载extention pylancefile->preferences->settings, 搜索Python: Language Server, 从default改成pylance

JAVA—IO流

存储数据的方案File和文件数据的操作IO流&#xff0c;学习字节流和字符流&#xff0c;了解框架和IO框架Commons IO 目录 1.File &#xff08;1&#xff09;创建对象 &#xff08;2&#xff09;常用方法 【1】判断类型&#xff0c;获取信息 【2】创建文件&#xff0c;删除…

LNMP学习

一、LNMP—web 1. 概述 LNMP/LAMP linux/windows/unix apache/nginx mysql/pgsql php/jsp 2. nginx部署及其平滑升级 实操 3. nginx七层负载均衡及算法 算法参考文档&#xff1a;https://docs.nginx.com/nginx/admin-guide/load-balancer/http-load-balancer/ 实操 4…