我正在通过在运行时下载依赖项而不是将它们打包在最终的 JAR 中来优化 Spring Boot 应用程序。虽然我已经成功实现了下载机制,但 Spring Boot 无法识别这些动态加载的依赖项,即使它们可以通过 Java ClassLoader 访问。
目标: 将最终 JAR 大小减少:
我的背景 我在网上搜索了可能的解决方案,但没有找到任何解决方案。我之前在使用 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>
文件夹结构
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,一旦启动它,它就会下载依赖项。