美文网首页
Spring Boot 打包成windows服务的生命周期

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

作者: 兴厚 | 来源:发表于2019-04-19 18:08 被阅读0次

    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 的问题

    相关文章

      网友评论

          本文标题:Spring Boot 打包成windows服务的生命周期

          本文链接:https://www.haomeiwen.com/subject/pipcgqtx.html