为什么要把程序打包为系统服务?
通常启动java程序的方式是通过命令行,启动一个黑窗口放在桌面上。但是这种方式有一个非常大的缺点:当服务器关机后,需要用户手动再次打开。
我们更希望程序运行在系统后台,用户不会轻易的将其关闭,当服务器重启后,程序能够自动启动。
因此我们需要将程序打包为系统服务,Scaffold中通过Java Service Wrapper实现。
Java Service Wrapper 官网
Java Service Wrapper有三个版本:专业版、标准版、社区版。我们用的是社区版,其他两个是收费版本,社区版已经能够满足我们的需求。
社区版可以实现:
- 将程序包装为系统服务
- 检测异常并进行自动重启
- 将控制台日志写入文件
为了部署更加方便,我们会将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
使用步骤:
- 下载Ladderx Service Wrapper
- 将程序的配置文件application.properties、日志配置文件logback-spring.xml(Log_Home设置为../logs)存放在conf目录下
- 如果程序有使用文件模块,新建file文件夹,在application.properties中配置 file.folder=${user.dir}/../file/
- 将程序打包为可执行Jar,命名为app.jar,放在lib目录下
- 编辑conf/wrapper.conf文件,修改必要参数如服务名、显示名称以及描述(下面详细说明)
- 通过bin中脚本进行操作
- 在根目录下的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配置:
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);