Spring Boot 打包成windows服务的生命周期

springboot buildwindows 服务,使用的是官方推荐的 sample , 代码有点老,依赖改了改,贴在下面

  properties部分
  <properties>
      <dist.dir>${project.build.directory}/dist</dist.dir>
      <dist.project.id>${project.artifactId}</dist.project.id>
      <dist.project.name>Demo</dist.project.name>
      <dist.start.class>com.steve.MasterApplication</dist.start.class>  <!--springboot 项目的入口类-->
      <dist.project.description>
        Master service.
      </dist.project.description>
      <dist.jmx.port>30001</dist.jmx.port> <!-- 项目暴露的jmx端口号 -->
      <java.version>1.8</java.version>
      <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>
    
  新增依赖 部分
  
  <dependency>
      <groupId>com.sun.winsw</groupId>
      <artifactId>winsw</artifactId>
      <version>2.2.0</version>
      <classifier>bin</classifier>
      <type>exe</type>
  </dependency>
  <dependency>
      <groupId>commons-daemon</groupId>
      <artifactId>commons-daemon</artifactId>
      <version>1.0.15</version>
  </dependency>
  
  build 部分
  
  <build>
      <plugins>
          <plugin>
              <groupId>org.apache.maven.plugins</groupId>
              <artifactId>maven-dependency-plugin</artifactId>
              <version>2.10</version>
              <executions>
                  <execution>
                      <id>copy</id>
                      <phase>package</phase>
                      <goals>
                        <goal>copy</goal>
                      </goals>
                      <configuration>
                          <artifactItems>
                              <artifactItem>
                                  <groupId>com.sun.winsw</groupId>
                                  <artifactId>winsw</artifactId>
                                  <classifier>bin</classifier>
                                  <type>exe</type>
                                  <destFileName>service.exe</destFileName>
                              </artifactItem>
                          </artifactItems>
                          <outputDirectory>${dist.dir}</outputDirectory>
                      </configuration>
                  </execution>
              </executions>
          </plugin>
          <plugin>
              <groupId>org.apache.maven.plugins</groupId>
              <artifactId>maven-resources-plugin</artifactId>
              <version>2.7</version>
              <executions>
                  <execution>
                  <id>copy-resources</id>
                  <phase>process-resources</phase>
                  <goals>
                    <goal>copy-resources</goal>
                  </goals>
                  <configuration>
                      <outputDirectory>${dist.dir}</outputDirectory>
                      <resources>
                          <resource>
                              <directory>src/main/dist</directory>
                              <filtering>true</filtering>
                          </resource>
                      </resources>
                  </configuration>
                  </execution>
              </executions>
          </plugin>
          <plugin>
              <groupId>org.apache.maven.plugins</groupId>
              <artifactId>maven-assembly-plugin</artifactId>
              <version>2.5.5</version>
              <configuration>
              <descriptors>
                  <descriptor>src/main/assembly/unix.xml</descriptor>
                  <descriptor>src/main/assembly/windows.xml</descriptor>
              </descriptors>
              </configuration>
              <executions>
                  <execution>
                      <id>assembly</id>
                      <phase>package</phase>
                      <goals>
                        <goal>single</goal>
                      </goals>
                  </execution>
              </executions>
          </plugin>
      </plugins>
  </build>
  
repository (不可少)
   <repositories>
       <repository>
           <id>alimaven</id>
           <name>aliyun maven</name>
           <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
           <releases>
            <enabled>true</enabled>
           </releases>
           <snapshots>
            <enabled>false</enabled>
           </snapshots>
       </repository>
       <repository>
           <id>jenkins</id>
           <name>Jenkins Repository</name>
           <url>http://repo.jenkins-ci.org/releases</url>
           <snapshots>
            <enabled>false</enabled>
           </snapshots>
       </repository>
   </repositories>

这个项目是 windows 服务和 linux 服务都帮我们做了。所以需要引入 commons-daemon 这个依赖,改造我们的入口类.

@SpringBootApplication
public class MasterApplication implements Daemon{

    public static void main(String[] args) {
        SpringApplication sp = new SpringApplication(AgentApplication.class);
        Environment environment = sp.run(args).getEnvironment();
    }


    private Class<?> springBootApp;

    private ConfigurableApplicationContext content;

    @Override
    public void init(DaemonContext context) throws DaemonInitException, Exception {
        this.springBootApp = ClassUtils.resolveClassName(context.getArguments()[0],
                AgentApplication.class.getClassLoader());
    }

    @Override
    public void start() throws Exception {
        this.content = SpringApplication.run(springBootApp);
    }

    @Override
    public void stop() throws Exception {
        this.content.close();
    }

    @Override
    public void destroy() {

    }
}

src/mian 下面添加sample对应的文件,添加完成后项目结构图

1555666436049.png

添加关闭的执行方法,这里就不用官方样例的那个shutdown方法了,官方的关闭方法在SpringApplicationAdminMXBeanRegistrar#SpringApplicationAdminshutdown方法里面,但是实际测试发现当程序运行时间过久,就会出现关闭不了的情况,于是开始寻找另一种程序化的关闭springboot 项目方法,发现有个通过http post调用actuator/shutdown方法去关闭项目的方法,看源码(在ShutdownEndpoint类中)

@WriteOperation
public Map<String, String> shutdown() {
    if (this.context == null) {
        return NO_CONTEXT_MESSAGE;
    }
    try {
        return SHUTDOWN_MESSAGE;
    }
    finally {
        Thread thread = new Thread(this::performShutdown);  // 调用 performShutdown 方法
        thread.setContextClassLoader(getClass().getClassLoader());
        thread.start();
    }
}

private void performShutdown() {
    try {
        Thread.sleep(500L);   // 这个 sleep 在这里不知道有什么用,懂的评论区留言,谢谢指点。
    }
    catch (InterruptedException ex) {
        Thread.currentThread().interrupt();
    }
    this.context.close();
}

再来对比一下官方样例的SpringApplicationAdminshutdown方法

private class SpringApplicationAdmin implements SpringApplicationAdminMXBean {
    
    // ... 

    @Override
    public void shutdown() {
        logger.info("Application shutdown requested.");
        SpringApplicationAdminMXBeanRegistrar.this.applicationContext.close();
    }

}

可以发现最终都是通过关闭context来关闭应用,但是第一个方法就不会出现关闭不了程序的问题,即使项目中有线程池的存在,然后我们就自然而然想到了手写一个 MBean 来实现第一个方法的作用,开干

Shutdown.java

@Slf4j
public class Shutdown implements ApplicationContextAware {

    private ConfigurableApplicationContext applicationContext;
    public static final String DEFAULT_OBJECT_NAME="com.steve:type=Demo,name=Shutdown";  // MBean name

    public void shutdown() {
        Thread thread = new Thread(() -> performShutdown(applicationContext));
        thread.setContextClassLoader(LifeCycleService.class.getClassLoader());
        thread.start();
    }

    private void performShutdown(ConfigurableApplicationContext applicationContext) {
        try {
            Thread.sleep(500L);
        } catch (InterruptedException ex) {
            Thread.currentThread().interrupt();
        }
        if(applicationContext == null){
            log.error("shutdown failed, application context is null");
        }
        applicationContext.close();
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        if(applicationContext instanceof ConfigurableApplicationContext) {
            this.applicationContext = (ConfigurableApplicationContext) applicationContext;
        }
    }

}


MBean (样例,非官方的xxxMBean结尾命名)我们已经下好了,接下来要借助Spring的帮助将这个类实例转化为MBean

DemoApplicationConfiguration.java

@Configuration
public class DemoApplicationConfiguration {
     @Bean
    public Shutdown lifeCycleConfiguration(){
        return new Shutdown();
    }

    /*
     * 由MBeanExporte 管理需要转换成 JMX的 MBean
     */
    @Bean
    public MBeanExporter mBeanExporter(Shutdown shutdown){
        MBeanExporter mBeanExporter = new MBeanExporter();
        Map<String, Object> beans = new HashMap<>();
        beans.put(Shutdown.DEFAULT_OBJECT_NAME, shutdown);
        mBeanExporter.setBeans(beans);
        return mBeanExporter;
    }
}

更多关于 SpringJMX 的支持看这篇 博文, 这里我们的关闭方法就已经准备好了,然后就是调用了,我们这里就直接套用样例代码的部分就可以了
SpringApplicationAdminClient.java

public class SpringApplicationAdminClient {

    public static final String DEFAULT_OBJECT_NAME = Shutdown.DEFAULT_OBJECT_NAME; //这里只需要改变object_name的值就行了,其他代码不做改动
    
    // .....
    
}

另外三个类 [ SpringBootService.class, StartSpringbootService.class, StopSpringbootService.class ] 就不需要改动了,来看一下windows服务的启动的文件service.xml内容改动

<service>
    <id>@dist.project.id@</id>
    <name>@dist.project.name@</name>
    <description>@dist.project.description@</description>
    <workingdirectory>%BASE%\</workingdirectory>
    <logpath>%DATAMESH_HOME%/logs/@dist.project.id@</logpath>
    <logmode>rotate</logmode>

    <executable>%JAVA_PATH%/java</executable> <!--引用windows的环境变量-->
    <startargument>-Dcom.sun.management.jmxremote.port=@dist.jmx.port@</startargument>
    <startargument>-Dcom.sun.management.jmxremote.authenticate=false</startargument>
    <startargument>-Dcom.sun.management.jmxremote.ssl=false </startargument>
    <startargument>-cp</startargument>
    <startargument>lib/*</startargument>
    <startargument>com.steve.StartSpringbootService</startargument>  <!--入口类-->
    <startargument>@dist.start.class@</startargument> <!--pom文件里面配置的属性,用@包裹起来-->

    <stopexecutable>%JAVA_PATH%/java</stopexecutable>
    <stopargument>-cp</stopargument>
    <stopargument>lib/*</stopargument>
    <stopargument>com.steve.StopSpringbootService</stopargument>
    <stopargument>@dist.jmx.port@</stopargument>

</service>

然后就可以正常使用了,下面看一下最终打包的项目结构和操作指令

打包后target目录生成三个包,可以修改 assembly 文件夹中对应系统的xml文件更改对应系统的打包生成的压缩文件格式。建议linux改为tar.gz。 下面为打包好的文件图

1555666436049.png

一共有三个,一个是jar, 一个是linux下的,一个是windows下的。解压缩 windows系统的zip包,使用系统管理员打开cmd窗口

xxx.exe install    // 安装服务
net start xxx.exe  // 启动服务
net stop xxx.exe   // 关闭服务

sc delete xxx   // xxx 为安装的服务名, 删除对应的服务
sc query xxx    // 查询对应服务状态

踩坑:

  1. 这种启动方式当项目里引用了 spring-boot-devtools 这个依赖的时候启动会报异常:
Exception in thread "restartedMain" java.lang.reflect.InvocationTargetException
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)

去除这个依赖就好了,暂时还没找到解决方案。目测是 class loader 的问题

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容