java service wrapper带来的问题

为什么要把程序打包为系统服务?

通常启动java程序的方式是通过命令行,启动一个黑窗口放在桌面上。但是这种方式有一个非常大的缺点:当服务器关机后,需要用户手动再次打开。

我们更希望程序运行在系统后台,用户不会轻易的将其关闭,当服务器重启后,程序能够自动启动。

因此我们需要将程序打包为系统服务,Scaffold中通过Java Service Wrapper实现。
Java Service Wrapper 官网

Java Service Wrapper有三个版本:专业版、标准版、社区版。我们用的是社区版,其他两个是收费版本,社区版已经能够满足我们的需求。

社区版可以实现:

  1. 将程序包装为系统服务
  1. 检测异常并进行自动重启
  1. 将控制台日志写入文件

为了部署更加方便,我们会将Java Service Wrapper集成一个jre8,配置调整在最佳状态,程序直接放在里面,整个放到服务器上就可以运行,而不需要手动再去下载和安装jdk。我们为这个集成取个名字便于以后引用说明:Ladderx Service Wrapper。

Windows服务

Java Service Wrapper社区版仅支持32位Windows操作系统。我们也只需要32位,为什么?

因为Java 32位Tomcat占用内存通常在200~500M左右,而64位则占用较大的内存。

程序如果是部署在我们自己运营的服务器,通常都是使用Linux服务器。

而如果部署在客户的服务器上,使用Windows Server操作系统的项目一般性能要求都不高,服务器配置也不会很高,可能是4G/8G,所以32位是节约内存的好选择。

如果是需要高性能、高并发的应用场景,通常我们会建议客户使用Linux操作系统。

Ladderx Service Wrapper下载 提取码:vfq8

使用步骤:

  1. 下载Ladderx Service Wrapper
  1. 将程序的配置文件application.properties、日志配置文件logback-spring.xml(Log_Home设置为../logs)存放在conf目录下
  1. 如果程序有使用文件模块,新建file文件夹,在application.properties中配置 file.folder=${user.dir}/../file/
  1. 将程序打包为可执行Jar,命名为app.jar,放在lib目录下
  1. 编辑conf/wrapper.conf文件,修改必要参数如服务名、显示名称以及描述(下面详细说明)
  1. 通过bin中脚本进行操作
  1. 在根目录下的readme.txt编写项目说明,修改ladderx-service-wrapper文件夹名称为项目名称

集成后目录结构如下:

ladderx-service-wrapper
├── bin                                 可执行脚本目录
│   ├── Console.bat                     使用控制台的方式启动Java Service Wrapper,可用于验证配置是否正确
│   ├── Install.bat                     注册服务(不会自己启动服务)
│   ├── Uninstall.bat                   卸载服务
│   ├── Start.bat                       启动服务
│   ├── Stop.bat                        停止服务
│   ├── Install&Start.bat               注册服务并启动
│   ├── Status.bat                      查看服务注册状态,启动类型以及运行状态
│   └── wrapper.exe                     该文件不可直接双击打开,以上bat脚本最终调用的是此exe程序
│
├── conf                                配置文件目录
│   ├── wrapper.conf                    java service wrapper的日志配置文件
│   ├── application.properties          项目的配置文件(需要自行放入,文件命名根据实际情况)
│   └── logback-spring.xml              项目的日志配置文件(必须存在否则报错,内置有一个默认配置文件,但需要自行放入项目实际配置,注意配置日志文件Log_Home写入到../logs下)
│        
├── lib                                 依赖库目录
│   ├── wrapper.jar                     java service wrapper的组件
│   ├── wrapper.dll                     java service wrapper的组件
│   └── app.jar                         项目的可执行Jar(需要自行放入,文件命名必须是app.jar)
│
├── logs                                日志文件目录
│   ├── wrapper.log                     java service wrapper的日志文件
│   └── xxx.log                         项目的日志文件(由程序写入)
│
├── docs                                存放教程文档等资料
├── file                                存放程序使用的文件
├── jre                                 jre8运行环境
└── readme.txt                          项目说明

wrapper.conf关键配置说明:

# jre位置:如果有多个项目部署,可以共用jre,并修改jre位置的配置
set.JRE_HOME=../jre

# Tell the Wrapper to log the full generated Java command line.
#wrapper.java.command.loglevel=INFO

# 项目依赖的jar包,编号从1开始累加
wrapper.java.classpath.1=../lib/wrapper.jar
# wrapper.java.classpath.2=../lib/xxx.jar

# 如果有调用dll或so,可以放在此位置
wrapper.java.library.path.1=../lib

# Java Additional Parameters
wrapper.java.additional.1=-Dlogging.config=../conf/logback-spring.xml

# 内存调优,Java堆内存初始大小(MB)
#wrapper.java.initmemory=128

# 内存调优:最大Java堆内存大小(MB)
#wrapper.java.maxmemory=512

# 启动参数,编号从1开始,第1个是项目可执行jar,之后是参数
wrapper.app.parameter.1=../lib/app.jar
# 项目配置文件扫描位置,利用的是springboot的配置文件扫描规则
wrapper.app.parameter.2=--spring.config.location=classpath:/,classpath:/config/,../conf/

#********************************************************************
# java service wrapper日志相关配置
#********************************************************************
# Format of output for the console.  (See docs for formats)
wrapper.console.format=PM

# Log Level for console output.  (See docs for log levels)
wrapper.console.loglevel=INFO

# java service wrapper日志文件输出位置
wrapper.logfile=../logs/wrapper.log

# Format of output for the log file.  (See docs for formats)
wrapper.logfile.format=LPTM

# Log Level for log file output.  (See docs for log levels)
wrapper.logfile.loglevel=INFO

# 日志文件的大小,单位为k(KB)或者m(MB),当超出后将创建新的日志文件,值为0不限制
wrapper.logfile.maxsize=1m

# 日志文件最大文件数,多出的文件自动删除最早的,值为0表示不限制。
wrapper.logfile.maxfiles=10

# Log Level for sys/event log output.  (See docs for log levels)
wrapper.syslog.loglevel=NONE

#********************************************************************
# java service wrapper常规配置
#********************************************************************

# 调用Console.bat时,命令行窗口标题
wrapper.console.title=Ladderx Service Application

#********************************************************************
# JVM 异常检测配置
#********************************************************************

# 内存溢出检测
# (Ignore output from dumping the configuration to the console.  This is only needed by the TestWrapper sample application.)

wrapper.filter.trigger.999=wrapper.filter.trigger.*java.lang.OutOfMemoryError
wrapper.filter.allow_wildcards.999=TRUE
wrapper.filter.action.999=NONE

#  Ignore -verbose:class output to avoid false positives.
wrapper.filter.trigger.1000=[Loaded java.lang.OutOfMemoryError
wrapper.filter.action.1000=NONE

# (Simple match)
wrapper.filter.trigger.1001=java.lang.OutOfMemoryError

# (Only match text in stack traces if -XX:+PrintClassHistogram is being used.)
#wrapper.filter.trigger.1001=Exception in thread "*" java.lang.OutOfMemoryError
#wrapper.filter.allow_wildcards.1001=TRUE
wrapper.filter.action.1001=RESTART
wrapper.filter.message.1001=The JVM has run out of memory.

#********************************************************************
# java service wrapper系统服务配置(主要修改这一块配置,其他可以不改)
#********************************************************************

# 服务名称,用英文,在任务管理器-服务中会显示(必改)
wrapper.name=LadderxService

# 显示名称,可以用中文,在任务管理器-服务中会显示(必改)
wrapper.displayname=Ladderx Windows Service

# 描述,可以用中文,在任务管理器-服务中会显示(必改)
wrapper.description=Ladderx Windows Service

# Service dependencies.  Add dependencies as needed starting from 1
wrapper.ntservice.dependency.1=

# 服务的启动模式  AUTO_START 自动启动, DELAY_START 延迟启动 or DEMAND_START 手动启动
wrapper.ntservice.starttype=AUTO_START

Java Service Wrapper本身很小,不到1M,但是嵌入了JRE8有137M,因此总共约138M左右,Scaffold通常打出来的Jar约40至100M左右,
因此整个包部署大概有200至300M左右,压缩后100至150M左右。如果一个服务器上部署多个程序,可以将JRE8独立出来共用,修改wrapper.conf的JRE_HOME变量位置即可。

这里提供一份可用的wrapper.conf配置:

📎wrapper.conf

Linux服务

暂未整理,推荐centos7,使用docker。

WinSW

windows下使用WinSW将nginx打包为系统服务运行。

WinSW地址:https://github.com/winsw/winsw/tree/v2.11.0

ladderx-ticket-nginx示例下载:

链接:百度网盘 请输入提取码
提取码:6pd0

使用java service wrapper在实际项目中碰到一些问题。

一、https证书配置问题

由于java service wrapper的classloader经过定制,springboot配置https证书路径后,读取证书有一些问题。

目前通过覆盖底层CatalinaBaseConfigurationSource类解决。

在工程中创建包org.apache.catalina.startup,创建CatalinaBaseConfigurationSource类,将如下代码粘贴进去:

package org.apache.catalina.startup;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.InvalidPathException;

import org.apache.tomcat.util.file.ConfigurationSource;
import org.apache.tomcat.util.res.StringManager;

public class CatalinaBaseConfigurationSource implements ConfigurationSource {

    protected static final StringManager sm = StringManager.getManager(Constants.Package);

    private final String serverXmlPath;
    private final File catalinaBaseFile;
    private final URI catalinaBaseUri;

    public CatalinaBaseConfigurationSource(File catalinaBaseFile, String serverXmlPath) {
        this.catalinaBaseFile = catalinaBaseFile;
        catalinaBaseUri = catalinaBaseFile.toURI();
        this.serverXmlPath = serverXmlPath;
    }

    @Override
    public Resource getServerXml() throws IOException {
        IOException ioe = null;
        Resource result = null;
        try {
            if (serverXmlPath == null || serverXmlPath.equals(Catalina.SERVER_XML)) {
                result = ConfigurationSource.super.getServerXml();
            } else {
                result = getResource(serverXmlPath);
            }
        } catch (IOException e) {
            ioe = e;
        }
        if (result == null) {
            // Compatibility with legacy server-embed.xml location
            InputStream stream = getClass().getClassLoader().getResourceAsStream("server-embed.xml");
            if (stream != null) {
                try {
                    result = new Resource(stream, getClass().getClassLoader().getResource("server-embed.xml").toURI());
                } catch (URISyntaxException e) {
                    stream.close();
                }
            }
        }

        if (result == null && ioe != null) {
            throw ioe;
        } else {
            return result;
        }
    }

    @Override
    public Resource getResource(String name) throws IOException {
        // Location was originally always a file before URI support was added so
        // try file first.

        File f = new File(name);
        if (!f.isAbsolute()) {
            f = new File(catalinaBaseFile, name);
        }
        if (f.isFile()) {
            return new Resource(new FileInputStream(f), f.toURI());
        }

        //	注释了这一段代码,正常的classloader读取不到此文件,会通过URI读取。
        //	而java service wrapper读取到了,但stream又被close了,报错stream closed。注释掉后,强制采用URI读取
        // Try classloader
//        try(InputStream stream = getClass().getClassLoader().getResourceAsStream(name)) {
//            if (stream != null) {
//                return new Resource(stream, getClass().getClassLoader().getResource(name).toURI());
//            }
//        } catch (InvalidPathException e) {
//            // Ignore. Some valid file URIs can trigger this.
//        } catch (URISyntaxException e) {
//            throw new IOException(sm.getString("catalinaConfigurationSource.cannotObtainURL", name), e);
//        }

        // Then try URI.
        URI uri = getURI(name);

        // Obtain the input stream we need
        try {
            URL url = uri.toURL();
            return new Resource(url.openConnection().getInputStream(), uri);
        } catch (MalformedURLException e) {
            throw new IOException(sm.getString("catalinaConfigurationSource.cannotObtainURL", name), e);
        }
    }

    @Override
    public URI getURI(String name) {
        File f = new File(name);
        if (!f.isAbsolute()) {
            f = new File(catalinaBaseFile, name);
        }
        if (f.isFile()) {
            return f.toURI();
        }

        // Try classloader
        try {
            URL resource = getClass().getClassLoader().getResource(name);
            if (resource != null) {
                return resource.toURI();
            }
        } catch (Exception e) {
            // Ignore
        }

        // Then try URI.
        // Using resolve() enables the code to handle relative paths that did
        // not point to a file
        URI uri;
        if (catalinaBaseUri != null) {
            uri = catalinaBaseUri.resolve(name);
        } else {
            uri = URI.create(name);
        }
        return uri;
    }

}

二、JRE8安全限制

有项目集成了CAS,需要对外调用CAS授权服务器接口,为HTTPS协议,出现TLS握手断开问题。

目前ladderx service wrapper中集成的是JRE8,参考AesUtil文档中的JRE8解除限制说明。

oracle 官网下载对应jdk版本的 jce_policy ,JDK8 点此下载 http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html。

下载之后找到你电脑安装的路径 G:\jdk8\jre\lib\security(方法也适用Centos),将下载出来的两个jar包解压到里面把之前的jar包覆盖了,就OK。

此处为语雀内容卡片,点击链接查看:2.1. AES加解密-AesUtil ✅ · 语雀

三、根证书问题

java service wrapper似乎是独立的java环境,并非默认使用系统的根证书对HTTPS站点证书进行校验。

简单处理,可直接忽略对HTTPS证书的校验。

在项目初始化位置加入代码忽略校验:

// 创建一个 TrustManager 实现,用于忽略证书验证
TrustManager[] trustAllCerts = new TrustManager[]{
    new X509TrustManager() {
        public java.security.cert.X509Certificate[] getAcceptedIssuers() {
            return null;
        }
        public void checkClientTrusted(X509Certificate[] certs, String authType) {
        }
        public void checkServerTrusted(X509Certificate[] certs, String authType) {
        }
    }
};

// 获取默认的 SSLContext
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustAllCerts, new java.security.SecureRandom());

// 获取默认的 HostnameVerifier
HostnameVerifier hostnameVerifier = new HostnameVerifier() {
    public boolean verify(String hostname, SSLSession session) {
        return true; // 全部信任
    }
};

// 使用自定义的 SSLContext 和 HostnameVerifier 来创建 SSL Socket Factory
HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());
HttpsURLConnection.setDefaultHostnameVerifier(hostnameVerifier);

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值