非 WEB 环境下运行 SpringBootApplication

前言

有时候一些项目并不需要提供 Web 服务,例如跑定时任务的项目等。因为启动一个 Tomcat 这样的 WEB 服务器容器也比较消耗资源,浪费内存及算力。 非 WEB 项目可以修改 maven 依赖为:

<dependencies>
 <dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter</artifactId>
 </dependency>
</dependencies>

当然,不修改也是没有问题的,可以仍旧依赖:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>

首次尝试

不过 SpringBoot 的启动类的 main 方法需要做一些改变

package com.sample.api;

import com.sample.api.entity.User;
import com.sample.api.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.*;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.annotation.Resource;

@SpringBootApplication
@EnableTransactionManagement
@EnableCaching
@MapperScan("com.sample.api.mapper")
@Slf4j
public class CmdApplication implements ApplicationRunner {

    @Resource
    private UserService userService;

    public static void main(String[] args) {
        new SpringApplicationBuilder(CmdApplication.class)
                .web(WebApplicationType.NONE) // .REACTIVE, .SERVLET
                //.bannerMode(Banner.Mode.OFF)
                .run(args);
        log.info("运行完成!");
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {
        User user = userService.findById("1");
        log.info("user={}",user);
    }
}

重要的内容

  1. 要实现 ApplicationRunner 接口
  2. 要定制应用启动方式 WebApplicationType.NONE

注意: 此种方式运行完成后应用不会立即退出应用

这离我们的需求有一点差距,我们希望独立运行的非 WEB 环境下的 Application 能够在运行完成后自动退出,而不是一直在运行。

改进之法

将 SpringBootApplication 抽象出来 App.java

package com.sample.api;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@SpringBootApplication
@EnableTransactionManagement
@EnableCaching
@MapperScan("com.sample.api.mapper")
public class App {
}

之后,新增两个入口启动类: WebApplication.java 和 CmdApplication.java

WebApplication.java 用于启动带有 WEB Server 的微服务

package com.sample.api;
import org.springframework.boot.SpringApplication;

public class WebApplication {
public static void main(String[] args) {
  SpringApplication.run(App.class, args);
}

}

CmdApplication.java 用于启动命令行程序

package com.sample.api;

import com.sample.api.entity.User;
import com.sample.api.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.ConfigurableApplicationContext;

@Slf4j
public class CmdApplication {
    public static void main(String[] args) {
        ConfigurableApplicationContext context =  new SpringApplicationBuilder(App.class).web(WebApplicationType.NONE).run(args);
        UserService userService = context.getBean(UserService.class);
        User user = userService.findById("3");
        log.info("user={}",user);
        log.info("运行完成!");
        System.exit(0);//使用强制退出来终止SpringBootApplication应用
    }
}

如此调整之后,就完美无瑕了!^_^

进一步改良

App.java

在抽象出来的 App.java 增加一些辅助的命令行相关的静态方法,供命令行程序调用。

package com.sample.api;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import java.util.function.Consumer;

@SpringBootApplication
@EnableTransactionManagement
@EnableCaching
@MapperScan("com.sample.api.mapper")
public class App {
   private static ConfigurableApplicationContext context = null;

   public static synchronized ConfigurableApplicationContext start(String[] args) {
       if (context == null) {
           context = new SpringApplicationBuilder(App.class).web(WebApplicationType.NONE).run(args);
       }
       return context;
   }

   public static synchronized void stop() {
       System.exit(0);
   }

   public static <T> T getBean(Class<T> bean) {
       ConfigurableApplicationContext context = start(new String[]{});
       return context.getBean(bean);
   }

   public static void exec(String[] args, Consumer<ConfigurableApplicationContext> consumer) {
       ConfigurableApplicationContext context = start(args);
       consumer.accept(context);
       stop();
   }

   public static void exec(Consumer<ConfigurableApplicationContext> consumer) {
       exec(new String[]{}, consumer);
   }
}

WebApplication.java

启动 WEB 微服务的入口类就变成这个样子啦!

package com.sample.api;

import org.springframework.boot.SpringApplication;


public class WebApplication {

public static void main(String[] args) {
    SpringApplication.run(App.class, args);
}

}

CmdApplication.java

那么命令行的启动非 WEB 的入口类就是此类形式,应对一般需求的,例如本地运行导入数据等程序,就可以以此为样板代码进行。

package com.sample.api;

import com.sample.api.entity.User;
import com.sample.api.service.UserService;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class CmdApplication {
    // 进行测试,以后的单个应用,比如临时用于倒入数据,执行某个服务逻辑都可以以此为样板代码
    private static void test() {
        //App.exec((context)->{
        UserService userService = App.getBean(UserService.class);
        User user = userService.findById("3");
        String id="4";
        String name="新加入";
        userService.createNewUser(id,name);
        log.info("user={}", user);
        log.info("运行完成!");
        //});
    }

    public static void main(String[] args) {
        App.start(args);
        try {
            test();
        }finally {
            App.stop();
        }

    }
}