尝试使用 Apache Aether 在运行时动态下载和加载依赖项,但 Spring Boot 无法正确识别它们

问题描述 投票:0回答:1

我正在通过在运行时下载依赖项而不是将它们打包在最终的 JAR 中来优化 Spring Boot 应用程序。虽然我已经成功实现了下载机制,但 Spring Boot 无法识别这些动态加载的依赖项,即使它们可以通过 Java ClassLoader 访问。

目标: 将最终 JAR 大小减少:

  1. 从编译的 JAR 中排除依赖项
  2. 在运行时下载所需的依赖项
  3. 使用 URLClassLoader 动态加载它们

我的背景 我在网上搜索了可能的解决方案,但没有找到任何解决方案。我之前在使用 Libby 开发 Minecraft 插件时使用核心 Java 处理过类似的事情,但我不确定使用 Spring Boot 是否可行。虽然我使用 AI 工具解决了一些依赖性问题,但我现在陷入了这一点。

我当前的实施

DependencyLoader.java

package com.hapangama.sunlicense.boot;

import org.eclipse.aether.DefaultRepositorySystemSession;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.artifact.Artifact;
import org.eclipse.aether.artifact.DefaultArtifact;
import org.eclipse.aether.collection.CollectRequest;
import org.eclipse.aether.collection.CollectResult;
import org.eclipse.aether.collection.DependencyCollectionException;
import org.eclipse.aether.collection.DependencySelector;
import org.eclipse.aether.graph.Dependency;
import org.eclipse.aether.graph.DependencyFilter;
import org.eclipse.aether.graph.DependencyNode;
import org.eclipse.aether.repository.LocalRepository;
import org.eclipse.aether.repository.RemoteRepository;
import org.eclipse.aether.repository.RepositoryPolicy;
import org.eclipse.aether.resolution.DependencyRequest;
import org.eclipse.aether.resolution.DependencyResolutionException;
import org.eclipse.aether.resolution.DependencyResult;
import org.eclipse.aether.util.filter.DependencyFilterUtils;
import org.eclipse.aether.util.graph.selector.ScopeDependencySelector;
import org.eclipse.aether.util.graph.visitor.PreorderNodeListGenerator;
import org.springframework.stereotype.Service;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.logging.Logger;
import java.util.stream.Collectors;

@Service
public class DependencyLoader {
    private static final Logger LOGGER = Logger.getLogger(DependencyLoader.class.getName());
    private static final String DEPENDENCIES_DIR = "BOOT-INF/lib";

    public static void initializeDependencies() throws Exception {
        // Create libs directory if it doesn't exist
        File libsDir = new File(DEPENDENCIES_DIR);
        if (!libsDir.exists()) {
            libsDir.mkdirs();
        }

        // Initialize Maven components
        RepositorySystem system = Booter.newRepositorySystem();
        DefaultRepositorySystemSession session = Booter.newRepositorySystemSession(system);
        session.setLocalRepositoryManager(
                system.newLocalRepositoryManager(
                        session,
                        new LocalRepository(libsDir.getAbsolutePath())
                )
        );

        // Define repositories
        List<RemoteRepository> repositories = Arrays.asList(
                new RemoteRepository.Builder("central", "default", "https://repo.maven.apache.org/maven2/")
                        .setPolicy(new RepositoryPolicy(true, RepositoryPolicy.UPDATE_POLICY_DAILY, RepositoryPolicy.CHECKSUM_POLICY_WARN))
                        .build(),
                new RemoteRepository.Builder("spring-releases", "default", "https://repo.spring.io/release")
                        .setPolicy(new RepositoryPolicy(true, RepositoryPolicy.UPDATE_POLICY_DAILY, RepositoryPolicy.CHECKSUM_POLICY_WARN))
                        .build(),
                new RemoteRepository.Builder("jcenter", "default", "https://jcenter.bintray.com")
                        .setPolicy(new RepositoryPolicy(true, RepositoryPolicy.UPDATE_POLICY_DAILY, RepositoryPolicy.CHECKSUM_POLICY_WARN))
                        .build(),
                new RemoteRepository.Builder("vaadin-addons", "default", "https://maven.vaadin.com/vaadin-addons")
                        .setPolicy(new RepositoryPolicy(true, RepositoryPolicy.UPDATE_POLICY_DAILY, RepositoryPolicy.CHECKSUM_POLICY_WARN))
                        .build()
        );

        // Define dependencies
        List<Dependency> dependencies = Arrays.asList(
                // Spring Boot dependencies
                new Dependency(new DefaultArtifact("org.springframework.boot:spring-boot-starter-data-jpa:2.5.4"), "runtime"),
                new Dependency(new DefaultArtifact("org.springframework.boot:spring-boot-starter-security:2.5.4"), "runtime"),

                // Vaadin and related dependencies
                new Dependency(new DefaultArtifact("com.vaadin:vaadin-spring-boot-starter:24.0.0"), "runtime"),
                new Dependency(new DefaultArtifact("in.virit:viritin:2.10.1"), "runtime"),
                new Dependency(new DefaultArtifact("com.github.appreciated:apexcharts:24.0.1"), "runtime"),
                new Dependency(new DefaultArtifact("org.parttio:starpass-theme:1.0.4"), "runtime"),
                new Dependency(new DefaultArtifact("org.vaadin.crudui:crudui:7.1.2"), "runtime"),

                // Database
                new Dependency(new DefaultArtifact("com.h2database:h2:2.1.214"), "runtime"),

                // Utility libraries
                new Dependency(new DefaultArtifact("org.modelmapper:modelmapper:3.2.0"), "runtime"),

                // Discord integration
                new Dependency(new DefaultArtifact("net.dv8tion:JDA:5.2.1"), "runtime")
        );

        // Create collection request
        CollectRequest collectRequest = new CollectRequest();
        collectRequest.setRepositories(repositories);
        dependencies.forEach(collectRequest::addDependency);

        // Resolve dependencies
        DependencyResult result = resolveDependencies(system, session, collectRequest);

        // Get resolved artifact files
        List<File> artifactFiles = getArtifactFiles(result);

        // Create and set up the custom ClassLoader
        URL[] urls = artifactFiles.stream()
                .map(file -> {
                    try {
                        return file.toURI().toURL();
                    } catch (MalformedURLException e) {
                        LOGGER.warning("Failed to convert file to URL: " + file);
                        return null;
                    }
                })
                .filter(Objects::nonNull)
                .toArray(URL[]::new);


        URLClassLoader classLoader = new URLClassLoader(urls, DependencyLoader.class.getClassLoader());
        Thread.currentThread().setContextClassLoader(classLoader);

        // Verify critical dependencies
        verifyDependencies(classLoader);
    }

    private static DependencyResult resolveDependencies(RepositorySystem system,
                                                        RepositorySystemSession session,
                                                        CollectRequest collectRequest) throws Exception {
        CollectResult collectResult = system.collectDependencies(session, collectRequest);
        DependencyRequest dependencyRequest = new DependencyRequest(collectResult.getRoot(),
                DependencyFilterUtils.classpathFilter("compile", "runtime"));

        return system.resolveDependencies(session, dependencyRequest);
    }

    private static List<File> getArtifactFiles(DependencyResult result) {
        PreorderNodeListGenerator nlg = new PreorderNodeListGenerator();
        result.getRoot().accept(nlg);

        return nlg.getArtifacts(false).stream()
                .map(Artifact::getFile)
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
    }

    private static void verifyDependencies(ClassLoader classLoader) {
        try {
            // Verify JDA
            Class.forName("net.dv8tion.jda.api.hooks.ListenerAdapter", true, classLoader);
            LOGGER.info("JDA dependency loaded successfully");

            // Verify Spring Boot
            Class.forName("org.springframework.boot.SpringApplication", true, classLoader);
            LOGGER.info("Spring Boot dependency loaded successfully");

            // Add other critical dependency verifications as needed

        } catch (ClassNotFoundException e) {
            LOGGER.severe("Failed to verify critical dependency: " + e.getMessage());
            throw new RuntimeException("Critical dependency verification failed", e);
        }
    }
}

Booter.java

package com.hapangama.sunlicense.boot;
import org.apache.maven.repository.internal.MavenRepositorySystemUtils;
import org.eclipse.aether.DefaultRepositorySystemSession;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory;
import org.eclipse.aether.impl.DefaultServiceLocator;
import org.eclipse.aether.repository.LocalRepository;
import org.eclipse.aether.repository.RepositoryPolicy;
import org.eclipse.aether.spi.connector.RepositoryConnectorFactory;
import org.eclipse.aether.spi.connector.transport.TransporterFactory;
import org.eclipse.aether.transport.file.FileTransporterFactory;
import org.eclipse.aether.transport.http.HttpTransporterFactory;
import org.eclipse.aether.util.listener.ChainedRepositoryListener;
import org.eclipse.aether.util.listener.ChainedTransferListener;

import java.io.File;

// Booter.java
public class Booter {
    public static RepositorySystem newRepositorySystem() {
        DefaultServiceLocator locator = MavenRepositorySystemUtils.newServiceLocator();
        locator.addService(RepositoryConnectorFactory.class, BasicRepositoryConnectorFactory.class);
        locator.addService(TransporterFactory.class, FileTransporterFactory.class);
        locator.addService(TransporterFactory.class, HttpTransporterFactory.class);

        return locator.getService(RepositorySystem.class);
    }

    public static DefaultRepositorySystemSession newRepositorySystemSession(RepositorySystem system) {
        DefaultRepositorySystemSession session = MavenRepositorySystemUtils.newSession();

        LocalRepository localRepo = new LocalRepository("target/local-repo");
        session.setLocalRepositoryManager(system.newLocalRepositoryManager(session, localRepo));

        session.setTransferListener(new ChainedTransferListener());
        session.setRepositoryListener(new ChainedRepositoryListener());

        return session;
    }
}

主班

package com.hapangama.sunlicense;

import com.hapangama.sunlicense.boot.DependencyLoader;
import com.vaadin.flow.component.page.AppShellConfigurator;
import com.vaadin.flow.theme.Theme;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.io.DefaultResourceLoader;

import java.net.URLClassLoader;
import java.util.Arrays;
import java.util.Collections;
import java.util.Properties;

@Theme(value = "sun")
@SpringBootApplication(exclude = ErrorMvcAutoConfiguration.class)
public class SunLicenseApplication implements AppShellConfigurator {

    public static void main(String[] args) {
        try {
            // Initialize dependencies before starting Spring
            DependencyLoader.initializeDependencies();

            // Get the context class loader that has our dependencies
            ClassLoader customClassLoader = Thread.currentThread().getContextClassLoader();

            // Create Spring application
            SpringApplication app = new SpringApplication(SunLicenseApplication.class);

            Properties properties = new Properties();
            properties.put("spring.main.allow-bean-definition-overriding", "true");
            properties.put("spring.main.allow-circular-references", "true");

            app.setDefaultProperties(properties);

            // Important: Set both resource loader and context class loader
            app.setResourceLoader(new DefaultResourceLoader(customClassLoader));
            Thread.currentThread().setContextClassLoader(customClassLoader);

            ConfigurableApplicationContext context = app.run(args);

            if (context != null && context.isActive()) {
                System.out.println("Application started successfully");
                System.out.println("Active profiles: " + Arrays.toString(context.getEnvironment().getActiveProfiles()));
            }

        } catch (Exception e) {
            System.out.println("Failed to start application");
            e.printStackTrace();
            System.exit(1);
        }
    }
}

启动控制台日志

java成功加载JDA(成功的库之一)

INFO: JDA dependency loaded successfully

但由于某种原因它会抛出稍后找不到的类。

Caused by: java.lang.ClassNotFoundException: net.dv8tion.jda.api.hooks.ListenerAdapter

https://paste.hapangama.com/egewawovif.properties

Pom.xml

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.hapangama</groupId>
    <artifactId>SunLicense</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>SunLicense</name>
    <description>SunLicense</description>
    <url/>
    <licenses>
        <license/>
    </licenses>
    <developers>
        <developer/>
    </developers>
    <scm>
        <connection/>
        <developerConnection/>
        <tag/>
        <url/>
    </scm>
    <properties>
        <java.version>17</java.version>
        <vaadin.version>24.5.4</vaadin.version>
    </properties>

    <repositories>
        <repository>
            <id>Vaadin Directory</id>
            <url>https://maven.vaadin.com/vaadin-addons</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>

    <dependencies>
        <dependency> <!-- required  -->
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency> <!-- required  -->
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency> <!-- required  -->
            <groupId>com.vaadin</groupId>
            <artifactId>vaadin-spring-boot-starter</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

<!--        addons-->
        <dependency>
            <groupId>in.virit</groupId>
            <artifactId>viritin</artifactId>
            <version>2.10.1</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>com.github.appreciated</groupId>
            <artifactId>apexcharts</artifactId>
            <version>24.0.1</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.parttio</groupId>
            <artifactId>starpass-theme</artifactId>
            <version>1.0.4</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.vaadin.crudui</groupId>
            <artifactId>crudui</artifactId>
            <version>7.1.2</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.modelmapper</groupId>
            <artifactId>modelmapper</artifactId>
            <version>3.2.0</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>net.dv8tion</groupId>
            <artifactId>JDA</artifactId>
            <version>5.2.1</version>
        </dependency>

        <dependency>
            <groupId>org.eclipse.aether</groupId>
            <artifactId>aether-api</artifactId>
            <version>1.1.0</version>
        </dependency>
        <dependency>
            <groupId>org.eclipse.aether</groupId>
            <artifactId>aether-connector-basic</artifactId>
            <version>1.1.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.maven</groupId>
            <artifactId>maven-aether-provider</artifactId>
            <version>3.3.9</version>
        </dependency>
        <dependency>
            <groupId>org.apache.maven</groupId>
            <artifactId>maven-resolver-provider</artifactId>
            <version>3.8.4</version>
        </dependency>
        <dependency>
            <groupId>org.apache.maven.resolver</groupId>
            <artifactId>maven-resolver-connector-basic</artifactId>
            <version>1.7.3</version>
        </dependency>
        <dependency>
            <groupId>org.apache.maven.resolver</groupId>
            <artifactId>maven-resolver-transport-file</artifactId>
            <version>1.7.3</version>
        </dependency>
        <dependency>
            <groupId>org.apache.maven.resolver</groupId>
            <artifactId>maven-resolver-transport-http</artifactId>
            <version>1.7.3</version>
        </dependency>








    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.vaadin</groupId>
                <artifactId>vaadin-bom</artifactId>
                <version>${vaadin.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <pluginRepositories>

            <pluginRepository>
                <id>central</id>
                <url>https://repo.maven.apache.org/maven2</url>
                <snapshots>
                    <enabled>false</enabled>
                </snapshots>
            </pluginRepository>

    </pluginRepositories>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                        <exclude>
                            <groupId>org.springframework.boot</groupId>
                            <artifactId>spring-boot-starter-data-jpa</artifactId>
                        </exclude>
                        <exclude>
                            <groupId>org.springframework.boot</groupId>
                            <artifactId>spring-boot-starter-security</artifactId>
                        </exclude>
                        <exclude>
                            <groupId>com.vaadin</groupId>
                            <artifactId>vaadin-spring-boot-starter</artifactId>
                        </exclude>
                        <exclude>
                            <groupId>com.h2database</groupId>
                            <artifactId>h2</artifactId>
                        </exclude>
                        <exclude>
                            <groupId>in.virit</groupId>
                            <artifactId>viritin</artifactId>
                        </exclude>
                        <exclude>
                            <groupId>com.github.appreciated</groupId>
                            <artifactId>apexcharts</artifactId>
                        </exclude>
                        <exclude>
                            <groupId>org.parttio</groupId>
                            <artifactId>starpass-theme</artifactId>
                        </exclude>
                        <exclude>
                            <groupId>org.vaadin.crudui</groupId>
                            <artifactId>crudui</artifactId>
                        </exclude>
                        <exclude>
                            <groupId>org.modelmapper</groupId>
                            <artifactId>modelmapper</artifactId>
                        </exclude>
                        <exclude>
                            <groupId>net.dv8tion</groupId>
                            <artifactId>JDA</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <profiles>
        <profile>
            <id>production</id>
            <properties>
                <vaadin.productionMode>true</vaadin.productionMode>
            </properties>
            <dependencies>
                <dependency>
                    <groupId>com.vaadin</groupId>
                    <artifactId>vaadin-core</artifactId>
                    <exclusions>
                        <exclusion>
                            <groupId>com.vaadin</groupId>
                            <artifactId>vaadin-dev</artifactId>
                        </exclusion>
                    </exclusions>
                </dependency>

            </dependencies>
            <build>
                <plugins>
                    <plugin>
                        <groupId>com.vaadin</groupId>
                        <artifactId>vaadin-maven-plugin</artifactId>
                        <version>${vaadin.version}</version>
                        <executions>
                            <execution>
                                <id>frontend</id>
                                <phase>compile</phase>
                                <goals>
                                    <goal>prepare-frontend</goal>
                                    <goal>build-frontend</goal>
                                </goals>
                            </execution>
                        </executions>
                    </plugin>
                </plugins>
            </build>
        </profile>
    </profiles>

</project>

文件夹结构

my folder structure at runtime

java spring spring-boot maven dependency-management
1个回答
0
投票

Spring Boot 已经有了

Layout
的概念,它用于引导并确定应用程序的依赖关系和结构。您可以创建一个自定义的来下载依赖项,而不是将它们打包在 jar 中。

然而,您不需要自己发明,而是已经有 Spring Boot Thin Launcher 可以做到这一点。它将布局切换为在第一次启动时下载 jar 的布局。

它需要将额外的依赖项添加到

spring-boot-maven-plugin
中的
pom.xml
,并且不需要任何其他代码更改。

<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot.experimental</groupId>
            <artifactId>spring-boot-thin-layout</artifactId>
            <version>1.0.31.RELEASE</version>
        </dependency>
    </dependencies>
</plugin>

就是这样。现在它会创建一个没有所有依赖项的 jar,一旦启动它,它就会下载依赖项。

最新问题
© www.soinside.com 2019 - 2025. All rights reserved.