SpringBootCLI使用相关

介绍

Spring Boot CLI(Command Line Interface)是一个命令行工具,我们可以用它来快速构建Spring原型应用。通过Spring Boot CLI,我们可以通过编写Java脚本来快速的构建出Spring Boot应用,并通过命令行的方式将其运行起来。

下载工具包

下载地址

这里我们使用 2.0.1.RELEASE 版本,更高的版本可能需要更高的JDK版本来配合。我们也需要提前配置JAVA路径的环境变量,这里使用JDK11。

运行Java脚本

脚本如下

@RestController
public class HelloController {

    @RequestMapping("/")
    public String home() {
        return "Hello World!";
    }
}

运行命令

.\spring.bat run HelloController.java

运行结果

WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by org.codehaus.groovy.vmplugin.v7.Java7$1 (jar:file:/C:/Users/xxx/Downloads/spring-boot-cli-2.0.1.RELEASE-bin/spring-2.0.1.RELEASE/lib/spring-boot-cli-2.0.1.RELEASE.jar!/BOOT-INF/lib/groovy-2.4.15.jar!/) to constructor java.lang.invoke.MethodHandles$Lookup(java.lang.Class,int)
WARNING: Please consider reporting this to the maintainers of org.codehaus.groovy.vmplugin.v7.Java7$1
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
Resolving dependencies..............................................................................

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.0.1.RELEASE)

2023-01-30 14:35:29.867  INFO 14080 --- [       runner-0] o.s.boot.SpringApplication               : Starting application on SH-FM-NB-320 with PID 14080 (started by xxx in C:\Users\xxx\Downloads\spring-boot-cli-2.0.1.RELEASE-bin\spring-2.0.1.RELEASE\bin)
2023-01-30 14:35:29.881  INFO 14080 --- [       runner-0] o.s.boot.SpringApplication               : No active profile set, falling back to default profiles: default
2023-01-30 14:35:30.096  INFO 14080 --- [       runner-0] ConfigServletWebServerApplicationContext : Refreshing org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@1650a964: startup date [Mon Jan 30 14:35:30 CST 2023]; root of context hierarchy
2023-01-30 14:35:31.494  INFO 14080 --- [       runner-0] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2023-01-30 14:35:31.527  INFO 14080 --- [       runner-0] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2023-01-30 14:35:31.527  INFO 14080 --- [       runner-0] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/8.5.29
2023-01-30 14:35:31.540  INFO 14080 --- [ost-startStop-1] o.a.catalina.core.AprLifecycleListener   : The APR based Apache Tomcat Native library which allows optimal performance in production environments was not found on the java.library.path: [C:\D-myfiles\java\jdk\jdk-11\bin;C:\Windows\Sun\Java\bin;C:\Windows\system32;C:\Windows;C:\Python27\;C:\Python27\Scripts;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Windows\System32\OpenSSH\;C:\Program Files\PuTTY\;C:\Program Files (x86)\Tencent\微信web开发者工具\dll;C:\Users\xxx\AppData\Local\Microsoft\WindowsApps;C:\Users\xxx\AppData\Local\Programs\Git\cmd;C:\D-myfiles\java\jdk\jdk-11\bin;C:\D-myfiles\java\apache-maven-3.6.1\bin;C:\Users\xxx\AppData\Local\Programs\Microsoft VS Code\bin;.]
2023-01-30 14:35:31.599  INFO 14080 --- [ost-startStop-1] org.apache.catalina.loader.WebappLoader  : Unknown loader org.springframework.boot.cli.compiler.ExtendedGroovyClassLoader$DefaultScopeParentClassLoader@22eeefeb class org.springframework.boot.cli.compiler.ExtendedGroovyClassLoader$DefaultScopeParentClassLoader
2023-01-30 14:35:31.691  INFO 14080 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2023-01-30 14:35:31.691  INFO 14080 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 1596 ms
2023-01-30 14:35:31.910  INFO 14080 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean  : Servlet dispatcherServlet mapped to [/]
2023-01-30 14:35:31.921  INFO 14080 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'characterEncodingFilter' to: [/*]
2023-01-30 14:35:31.921  INFO 14080 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2023-01-30 14:35:31.922  INFO 14080 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'httpPutFormContentFilter' to: [/*]
2023-01-30 14:35:31.922  INFO 14080 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'requestContextFilter' to: [/*]
2023-01-30 14:35:32.150  INFO 14080 --- [       runner-0] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2023-01-30 14:35:32.460  INFO 14080 --- [       runner-0] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@1650a964: startup date [Mon Jan 30 14:35:30 CST 2023]; root of context hierarchy
2023-01-30 14:35:32.564  INFO 14080 --- [       runner-0] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/]}" onto public java.lang.String HelloController.home()
2023-01-30 14:35:32.569  INFO 14080 --- [       runner-0] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2023-01-30 14:35:32.570  INFO 14080 --- [       runner-0] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
2023-01-30 14:35:32.607  INFO 14080 --- [       runner-0] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2023-01-30 14:35:32.608  INFO 14080 --- [       runner-0] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2023-01-30 14:35:33.054  INFO 14080 --- [       runner-0] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2023-01-30 14:35:33.101  INFO 14080 --- [       runner-0] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2023-01-30 14:35:33.107  INFO 14080 --- [       runner-0] o.s.boot.SpringApplication               : Started application in 3.938 seconds (JVM running for 102.914)
2023-01-30 14:36:38.790  INFO 14080 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring FrameworkServlet 'dispatcherServlet'
2023-01-30 14:36:38.790  INFO 14080 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization started
2023-01-30 14:36:38.831  INFO 14080 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization completed in 41 ms

会先去maven中央仓库下载依赖,然后运行。

原理分析

在任意一个maven项目中添加此依赖,最好不要是SpringBoot项目,可能有未知的问题。

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-cli</artifactId>
  <version>2.0.1.RELEASE</version>
</dependency>

启动类为 org.springframework.boot.cli.SpringCli,这里我们以 spring run xxx.java 命令为例来分析原理。

  1. RunCommand的run()方法
  2. RunOptionHandler的run()方法
  3. SpringApplicationRunner的compileAndRun()方法,开始编译并运行
  4. GroovyCompiler的compile()方法,将java文件编译为class
  5. 开启一个守护线程RunThread
  6. 创建一个SpringApplicationLauncher来启动应用程序,简单来说就是通过反射来创建 org.springframework.boot.SpringApplication 对象并调用run()方法,和我们自己启动SpringBoot项目类似。
  7. 编译后得到的HelloController类通过构造器参数传入SpringApplication。

在创建GroovyCompiler对象的过程中也会处理maven依赖相关,这块逻辑比较复杂,核心为AetherGrapeEngine类。

顺便一提,.\spring.bat init --dependencies=web myproject 内部就是直接调用 https://start.spring.io/starter.zip 生成项目代码,接口返回为zip文件,再解压为文件夹,起始类为InitCommand。

源码运行遇到的问题

Unable to find groovy JAR

缩短命令行的方式,不能选择第三或第四种,可以选择第一或第二种。

maven依赖的处理

点击查看代码
import lombok.Data;
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.artifact.Artifact;
import org.eclipse.aether.artifact.DefaultArtifact;
import org.eclipse.aether.collection.CollectRequest;
import org.eclipse.aether.collection.DependencyCollectionException;
import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory;
import org.eclipse.aether.graph.Dependency;
import org.eclipse.aether.graph.DependencyNode;
import org.eclipse.aether.impl.DefaultServiceLocator;
import org.eclipse.aether.repository.Authentication;
import org.eclipse.aether.repository.LocalRepository;
import org.eclipse.aether.repository.RemoteRepository;
import org.eclipse.aether.resolution.ArtifactRequest;
import org.eclipse.aether.resolution.ArtifactResolutionException;
import org.eclipse.aether.resolution.ArtifactResult;
import org.eclipse.aether.resolution.DependencyRequest;
import org.eclipse.aether.resolution.DependencyResolutionException;
import org.eclipse.aether.resolution.DependencyResult;
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.repository.AuthenticationBuilder;
import org.springframework.util.StringUtils;

import java.io.File;

/**
 * 下载maven依赖,使用Aether框架,逻辑简化版
 * 参考 https://www.cnblogs.com/xiaosiyuan/articles/5887642.html
 * https://wusandi.github.io/2018/10/29/equinox-aether-introduction/
 * 核心类为 RepositorySystem,RepositorySystemSession,使用这两个类从指定的远程仓库下载指定的jar包
 */
public class TestMavenDependencyDownload2 {

    public static void main(String[] args) throws Exception {
        Params params = new Params();
        params.setGroupId("org.springframework.boot");
        params.setArtifactId("spring-boot-starter");
        params.setVersion("2.0.1.RELEASE");
        downLoadDependency(params);
    }

    private 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);
    }

    private static RepositorySystemSession newSession(RepositorySystem system, String target) {
        DefaultRepositorySystemSession session = MavenRepositorySystemUtils.newSession();
        LocalRepository localRepo = new LocalRepository( /*"target/local-repo" */target);
        session.setLocalRepositoryManager(system.newLocalRepositoryManager(session, localRepo));
        return session;
    }

    public static void downLoadArtifact(Params params) throws ArtifactResolutionException {
        String groupId = params.getGroupId();
        String artifactId = params.getArtifactId();
        String version = params.getVersion();
        String repositoryUrl = params.getRepository();
        String target = params.getTarget();
        String username = params.getUsername();
        String password = params.getPassword();
        RepositorySystem repoSystem = newRepositorySystem();
        RepositorySystemSession session = newSession(repoSystem, target);
        RemoteRepository central = null;
        if (username == null && password == null) {
            central = new RemoteRepository.Builder("central", "default", repositoryUrl).build();
        } else {
            Authentication authentication = new AuthenticationBuilder().addUsername(username).addPassword(password).build();
            central = new RemoteRepository.Builder("central", "default", repositoryUrl).setAuthentication(authentication).build();
        }
        /**
         * 下载一个jar包
         */
        Artifact artifact = new DefaultArtifact(groupId, artifactId, "jar", version);
        ArtifactRequest artifactRequest = new ArtifactRequest();
        artifactRequest.addRepository(central);
        artifactRequest.setArtifact(artifact);
        ArtifactResult artifactResult = repoSystem.resolveArtifact(session, artifactRequest);
        System.out.println("artifactResult: " + artifactResult.getArtifact().getFile());
    }

    public static void downLoadDependency(Params params) throws ArtifactResolutionException, DependencyCollectionException, DependencyResolutionException {
        String groupId = params.getGroupId();
        String artifactId = params.getArtifactId();
        String version = params.getVersion();
        String repositoryUrl = params.getRepository();
        String target = params.getTarget();
        String username = params.getUsername();
        String password = params.getPassword();

        RepositorySystem repoSystem = newRepositorySystem();
        RepositorySystemSession session = newSession(repoSystem, target);
        RemoteRepository central = null;
        if (username == null && password == null) {
            central = new RemoteRepository.Builder("central", "default", repositoryUrl).build();
        } else {
            Authentication authentication = new AuthenticationBuilder().addUsername(username).addPassword(password).build();
            central = new RemoteRepository.Builder("central", "default", repositoryUrl).setAuthentication(authentication).build();
        }
        Artifact artifact = new DefaultArtifact(groupId, artifactId, "jar", version);
        ArtifactRequest artifactRequest = new ArtifactRequest();
        artifactRequest.addRepository(central);
        artifactRequest.setArtifact(artifact);

        /**
         * 下载该jar包及其所有依赖jar包
         */
        Dependency dependency = new Dependency(artifact, null);
        CollectRequest collectRequest = new CollectRequest();
        collectRequest.setRoot(dependency);
        collectRequest.addRepository(central);
        DependencyNode node = repoSystem.collectDependencies(session, collectRequest).getRoot();

        DependencyRequest dependencyRequest = new DependencyRequest();
        dependencyRequest.setRoot(node);
        DependencyResult dependencyResult = repoSystem.resolveDependencies(session, dependencyRequest);
        for (ArtifactResult artifactResult : dependencyResult.getArtifactResults()) {
            System.out.println(artifactResult.getArtifact().getFile());
        }
    }

    private static File getM2RepoDirectory() {
        return new File(getDefaultM2HomeDirectory(), "repository");
    }

    private static File getDefaultM2HomeDirectory() {
        String mavenRoot = System.getProperty("maven.home");
        if (StringUtils.hasLength(mavenRoot)) {
            return new File(mavenRoot);
        }
        return new File(System.getProperty("user.home"), ".m2");
    }

    @Data
    static class Params {
        /**
         * jar包在maven仓库中的groupId
         */
        private String groupId;
        /**
         * jar包在maven仓库中的artifactId
         */
        private String artifactId;
        /**
         * jar包在maven仓库中的version
         */
        private String version;
        /**
         * 远程maven仓库的URL地址
         */
        private String repository = "https://repo.maven.apache.org/maven2/";
        /**
         * 下载的jar包存放的目标地址,默认为 ${user.home}/.m2/repository
         */
        private String target = getM2RepoDirectory().getAbsolutePath();
        /**
         * 登录远程maven仓库的用户名,若远程仓库不需要权限,设为null,默认为null
         */
        private String username = null;
        /**
         * 登录远程maven仓库的密码,若远程仓库不需要权限,设为null,默认为null
         */
        private String password = null;
    }

}

主要使用了aether框架,spring-boot-cli 也是在此基础上封装的。更多用法可以参考官网

参考

Spring Boot快速开发利器:Spring Boot CLI
Spring Cloud CLI-官方
利用aether api实现从指定maven仓库下载jar包
eclipse aether入门

posted @ 2023-08-09 07:22  strongmore  阅读(244)  评论(0编辑  收藏  举报