一、创建非web项目
概述
从开始使用SpringBoot到现在,一直都是在用SpringBoot开发web服务(API服务)。直到前段时间,需要帮其他组的同事写一个非web的简单服务时,才想到SpringBoot是不是也支持非web项目。
答案是肯定的:spring诞生之初就不是为web项目定制的,SpringBoot无非是在spring核心项目的基础上添加了一些方便开发者使用的组件,所以使用SpringBoot开发非web项目也是可行的。
首先我们要弄明白常用的web项目和非web项目的区别在哪儿?私以为是服务启动和执行逻辑触发的方式:
- web项目需要依赖web容器来启动,通过http请求来触发相关的服务;
- 非web项目则不需要依赖web容器来启动,它可以是自启动的;
- 非web项的服务通常是主动触发的或者通过非http的方式被动触发的。
接下来详细介绍下如何使用SpringBoot构建非web项目。
依赖
创建web项目通常需要使用的依赖是spring-boot-starter-web:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
这个依赖间接引用了tomcat,spring-webmvc,spring-context,json-starter等依赖。这些在非web项目里基本上都用不到,非web项目可以直接使用spring-boot-starter:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency>
spring-boot-starter间接引用了spring-core、yaml、auto-configure等依赖,已经足够用来创建一个普通的spring项目了。
如果有特殊情况(说实话,我强烈怀疑)非要使用spring-boot-starter-web依赖来构建非web项目也不是不行,只需要加一些配置来避免启动web容器就行:
spring: main: web-application-type: none
或者在启动类中添加web配置并设置为NONE:
@SpringBootApplication public class MyApplication { public static void main(String[] args) { new SpringApplicationBuilder() .web(NONE) .main(MyApplication.class) .build(args); } }
启动
SpringBoot非web项目的启动类定义和web项目并无不同。如下:
@SpringBootApplication public class MyApplication { public static void main(String[] args) { SpringApplication.run(MyApplication.class, args); } }
但是执行启动类中的main方法以后呢?如果是web项目,在启动后等待HTTP请求调用就行了。不过这里是非web项目,我们得想办法执行我们定义好的业务逻辑。接下来按常见的业务逻辑特征分别介绍下。
1、任务需要定时执行
这种情况需要配置定时任务。SpringBoot对定时任务的支持还算可以。前段时间我写过关于《SpringBoot定时任务配置》这样的文章,有需要可以参考下,这里就不重复介绍了。
2、在某个类的实例注入后就立即执行
这种场景说实话不多见。通常是需要该类实现InitializingBean
接口,并在afterPropertiesSet方法中实现相关的逻辑。如下:
@Service public class MyService implements InitializingBean { @Override public void afterPropertiesSet() throws Exception { System.out.println("--------->>> start from my service"); }
还有一种方式是使用java提供的@PostConstruct
注解:
@Service public class MyService { @PostConstruct public void init() { System.out.println("--------->>> start from my service init method"); } }
建议最好使用前者。
3、使用SpringBoot的提供的主动执行接口
SpringBoot提供了两个接口ApplicationRunner
和CommandLineRunner
来支持主动执行业务逻辑。
比如我们可以直接实现ApplicationRunner
接口,并在run方法中添加业务逻辑:
@Component public class MyRunner1 implements ApplicationRunner { @Autowired private MyService myService; @Override public void run(ApplicationArguments args) { System.out.println("--------->>> my runner1 has started"); System.out.println(myService.getId()); } }
CommandLineRunner
接口和ApplicationRunner
接口的差别不大。从执行时机还是调用过程上来着,这两者几乎都是一样的。这唯一的区别在于他们提供的run方法的args参数类型上:
ApplicationRunner
的args参数是ApplicationArguments类型,对原始参数做了一层封装;CommandLineRunner
的args参数是字符串类型,取的是启动类收到的原始参数。
这种差别源于SpringBoot对二者的定位上:
ApplicationRunner
适用于启动即执行的场景,只需要读取一次参数信息即可。它的参数通常是“option=value”这种结构的,如:“--foo=bar --foo=baz” 。ApplicationArguments
中封装了一些对这种参数进行处理的方法,以便开发使用。
CommandLineRunner
从名字上看就是用来做命令行交互用的,所以它这里直接取了原始参数,看一个使用示例:
@Component public class MyRunner3 implements CommandLineRunner { @Override public void run(String... args) throws Exception { System.out.println("Please enter your name:"); try (Scanner scanner = new Scanner(System.in)) { String name = scanner.nextLine(); System.out.println("Hello " + name + "!"); System.out.println("Bye!"); } } }
如果在执行命令行交互之前也需要读取解析传入的参数,那么这里的MyRunner3
类完全也可以实现ApplicationRunner
接口。二者的差别几乎可以忽略。
在一个应用里面可以有多个ApplicationRunner
或CommandLineRunner
的实现。要调整两者的实现类之间的执行顺序可以使用@Order
注解。
就这样。这一节虽然介绍的是如何创建启动非web项目,但是在web项目中也会有主动触发一些执行逻辑的需要,上面介绍的这些方案也是完全可用的。
二、如何使用ApplicationRunner解析命令行中的参数?
使用Spring提供的CommandLineRunner接口可以实现了一个命令行应用程序。但是,参数/选项/参数处理却不是那么好。幸运的是,有一种更好的方法可以使用Spring Boot编写命令行应用程序,并且还可以使用ApplicationRunner接口进行解析。
在这两种情况下,无论是CommandLineRunner还是ApplicationRunner,都始终支持Spring的属性处理。我们可以像往常一样使用@Value注释注入值。
完整的工作源代码在 这里
首先,我们创建一个简单的Spring Boot应用程序并实现ApplicationRunner接口。现在,Spring可以获得我们的这个类并执行它。它类似于CommandLineRunner。
我们还必须实现一个方法run,但是我们将ApplicationArguments 作为入参数而不是String列表。
ApplicationArguments区分选项参数和非选项参数。选项参数是我们可以通过Spring Boot属性处理使用的(如 app.name = Myapp)。它们还可以通过传入逗号分隔列表或多次使用参数来为每个选项包含多个值。
非选项参数是我们在命令行传递除了VM参数的所有其他参数。
在示例中,我们将打印出我们收到的每种类型的参数,然后显示它们。
package de.codeboje.tutorials.cmdlineargs; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class CommandlineAppApplication implements ApplicationRunner{ public static void main(String[] args) { SpringApplication.run(CommandlineAppApplication.class, args); } @Override public void run(ApplicationArguments args) throws Exception { System.out.println("# NonOptionArgs: " + args.getNonOptionArgs().size()); System.out.println("NonOptionArgs:"); args.getNonOptionArgs().forEach(System.out::println); System.out.println("# OptionArgs: " + args.getOptionNames().size()); System.out.println("OptionArgs:"); args.getOptionNames().forEach(optionName -> { System.out.println(optionName + "=" + args.getOptionValues(optionName)); }); } }
非选项参数通过getNonOptionArgs()
作为字符串列表重新获得。
而对于选项参数,我们可以通过接收选项名称getOptionNames和实际值通过getOptionValues,它会返回一个字符串列表。
当我们现在启动应用程序并传递一些参数,如:
java -jar commandline-app-0.0.1-SNAPSHOT.jar nonoption --app.name=CmdRulez --app.hosts=abc,def,ghi --app.name=2
上述执行的输出:
#NonOptionArgs:1个 NonOptionArgs: nonoption #OptionArgs:2个 OptionArgs: app.hosts = [abc,def,ghi] app.name = [CmdRulez,2]
三、如何让非Web程序保持运行(不退出)?
按照原先的方式启动 SpringBootApplication 会发现启动加载完之后会立即退出,这时需要做点工作让主线程阻塞让程序不退出:
方式一:
@SpringBootApplication public class SampleApplication implements CommandLineRunner { public static void main(String[] args) throws Exception { SpringApplication.run(SampleApplication.class, args); } @Override public void run(String... args) throws Exception { Thread.currentThread().join(); } }
方式二:
@SpringBootApplication public class SampleApplication { public static void main(String[] args) throws Exception { SpringApplication.run(SampleApplication.class, args); new CountDownLatch(1).await(); } }
参考: