在开发环境下,原有代码一直如常运行。最近要把它部署到云上。遇到了不少问题。毕竟在项目部署过程中,涉及到maven或者gradle之间的深刻理解,才能克服开发环境正常,部署出现各种问题的解决能力。
在此,以下文章权当记录,但是只是针对maven这块,gradle以后再去讨论。预防后期自己的遗忘。
目录如下:
- 打包文件的说明
- IDE开发工具打包的区别:微服务,eclipse和IDEA区别
- maven打包的三种方式:
maven-jar-plugin
maven-assembly
maven-shade - 微服务搭建架构注意点(以demo为例子)
微服务的springcloud大致说明
微服务的打包工具及指令(按照第二点的IDEA做好基本的配置)
微服务的内嵌容器Tomcat/Jetty/Undertow启动
微服务的外嵌容器TOMCAT启动(暂讨论tomcat)
微服务jar之传参启动jar
一、打包文件的说明
解压的cloud的demo。
eureka_center为包名。此处会根据包名的.分割,变成文件夹的层级结构。文件为class编译后的文件。
META-INF:相当于一个信息包,目录中的文件和目录获得Java 2平台的认可与解释,用来配置应用程序、扩展程序、类加载器和服务manifest.mf文件,还有项目的pom文件等,在用jar打包时自动生成。
里面的代码如下(讨论部分):
Manifest-Version: 1.0: 调用的服务manifest.mf版本
Main-Class: eureka_center.EurekaCenterStart :微服务的入口(格式为包名.类名,不需要类名的后缀)。如果不存在main的话,而是由外部class path执行入口,这边这个就可以忽略
Implementation-Title: eureka_center :项目名称
Implementation-Version: 1.0 :项目版本号
Archiver-Version: Plexus Archiver :此为maven骨架引用的项目。jar引用的,没有它会导致maven骨架异常,导致打包失败
Built-By: Administrator :电脑当前用户
Implementation-Vendor-Id: eureka_center :系统给项目创建的id
Created-By: Apache Maven 3.3.1 : 此骨架创建的maven版本号,非自己电脑配置的版本号
Build-Jdk: 1.8.0_161 :创建项目
Implementation-URL: http://www.example.com
Implementation-Vendor: Pivotal Software, Inc.
其中Main-Class:比较重要,涉及到你部署后,是否会出现 “找不到主清单的异常”,如图:
二、IDE开发工具打包的区别
环境要求:JDK1.8(微服务要求1.8,如果jdk是1.8以下的,重装吧)
maven:3.3.0以上。 (以下版本还没测试过)
2.1 eclipse打包
对于eclipse的打包,在maven环境变量配置好后。只需要注意项目的编译版本在1.8,jdk1.8.然后,去执行项目邮件的run as 即可。具体选择maven build install等,后面讨论部分。
2.2 IDEA 2018.2 版本 打包
增加微服务的项目后。最好增加如下操作。保证MF文件中,导出的文件信息正确。具备Main-Class的入口类信息。
选择idea-file-project structure
将项目增加进去。
Main—class:选择自己的入口类main函数
extract to the target jar :代表将项目的引用的所有jar包都打包成一个jar文件。这样导致打包的文件会比较大。但是里面会涵盖了所有的jar引用。
copy to the output directory and link :jar包就不打包在项目,需要增加外部引用。这部分最好使用脚本。优点就是,打包的项目极小,基本不过百K。甚至几KB。
三、MAVEN打包的方式
我们项目打包,有maven-jar-plugin和maven-dependency-plugin方式,有maven-assembly-plugin方式。甚至还有maven-shade-plugin方式。当项目是jar和war或者前端采用Framework形式的话,打包方式都不一样。还有打包指令。因为IDE开发工具,一般自带的指令是mvn install或者mvn package maven generate-source。需要严格意义的打包。保证打包在云上,远远不够的。在这方面。指令打包更加切合实际。
当然,外面公司打包还有很多种方式,比如平安公司内部,采用的工具是神兵工具,不开源。还有持续集成,热部署等等技术。这里谈的,更倾向原始,毕竟工具只是整合。对原始的打包理解的透彻,有助于项目的问题解决能力。
3.1 maven-jar-plugin和maven-dependency-plugin插件打包
POM的设置如下:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.6</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<classpathPrefix>lib/</classpathPrefix>
<mainClass>此处配置包名.类名的完整路径,作为入口函数。也可以直接在MF里面指定,万一MF失效,可以在POM这里指定,增加打包入口类的成功率</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>2.10</version>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>外部引用的jar包文件路径。这里就契合上图idea下的全量打包或者增量打包(全量打包:全部东西都包含,增量打包:仅仅代码本身的打包。)</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
上面关键字段的解释:
-
maven-jar-plugin: 生成META-INF/MANIFEST.MF文件的局部内容
-
mainClass:指定 MANIFEST.MF中的Main-Class
-
addClasspath:在MANIFEST.MF加上Class-Path项并配置依赖包。比如聚合项目,包之间存在父子关系,每个包不一定是完整的项目。
-
classpathPrefix:指定项目依赖包所在目录
-
outputDirectory:项目依赖包的指定拷贝目录
打包指令:
在ide下选择maven package或者install 。比如IDEA,如图:
但是,maven的install和package有什么区别呢?是否完全一致?不是的。在聚合项目下,必须maven install。 -
maven package:把jar打到本项目的target下
-
maven install: 把jar打到本项目的target下,并把target下的jar安装到本地仓库,供其他项目使用
3.2 maven-assembly-plugin插件打包
pom.xml的设置如下:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.5.5</version>
<configuration>
<archive>
<manifest>
<mainClass>包名.类名</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>打包的项目,追加的名字拼接</descriptorRef>
</descriptorRefs>
</configuration>
</plugin>
</plugins>
</build>
注意:此种方式,必须要求指令:
在当前项目下的cmd窗口下:
mvn package assembly:single
打包会生成项目名-jar- +(descriptorRef标签指定的拼接名字).jar
此种打包方式:包含项目中所有代码和资源,还包含了所有依赖包的内容。所以可以直接通过 java -jar 项目名.jar执行
如果需要使用mvn package打包呢?
需要在pom增加配置,如下:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.5.5</version>
<configuration>
<archive>
<manifest>
<mainClass>包名.类名</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
上面关键字段的解释:
-
executions: 执行executions下面申明的其中一个phase的时候在goal阶段执行这个插件
-
goal:代表一个特定任务。这里为single。单个意思
-
phase:代表maven的 build 过程。它的Build Lifecycle分为三种,分别为:
01、default(处理项目的部署)、02、clean(处理项目的清理)、03、site(处理项目的文档生成)。他们都包含不同的lifecycle。
Build Lifecycle是由phases构成的,default Build Lifecycle有几个重要的phase。
validate 验证项目是否正确以及必须的信息是否可用
compile 编译源代码
test 测试编译后的代码,即执行单元测试代码
package 打包编译后的代码,在target目录下生成package文件
integration-test 处理package以便需要时可以部署到集成测试环境
verify 检验package是否有效并且达到质量标准
install 打包编译后的代码,在target目录下生成package文件并安装package到本地仓库,方便本地其它项目使用
deploy 部署,拷贝最终的package到远程仓库和替他开发这或项目共享,在集成或发布环境完成
这里采用的是package方式
所以,增加如下配置后,打包就可以采用mvn package或者install打包。具体哪种方式,按需去修改如下标签内的内容:
<phase>package或者install</phase>
缺点:(详情可以查找spring源码里面的PlugableSchemaResolver,我就不黏贴了,会导致篇幅过长)
- spring在加载xsd文件时候,会 本地查找xsd文件 > 如果没有,就去URL指定的路径去下载xsd
- assembly打包方式所把jar包全部整合成一个jar包的方式。但是对于一个项目,必定是多个jar整合进去的。而被依赖的jar包又会依赖其他的jar包。当一些jar存在同名不同版本情况下,会去查找最高版本的xsd。这样,势必会遗漏了一些jar需要低版本的xsd。从而没有造成合并。导致打包错误。
下面,我会去讨论,那么有哪种方式,又可以打包成一个单独的jar,又可以解决不同版本的xsd文件之间的共存呢,要谈shade打包方式
3.3 maven-shade-plugin插件打包
shade,就是能解决assembly的同名jar包不同版本的xsd的共存问题。它在对spring。schemas文件处理上,会将所有的spring.schemas合并,包含了所有版本出现的集合。
好了,POM的配置如下:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>2.4.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>包名.类名</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
上面关键字段的解释:
- transformer implementation: 打包要使用的代理实现类
org.apache.maven.plugins.shade.resource.ManifestResourceTransformer
此类的作用,就是去合并相同版本的spring.handlers和spring.schemas,将多个版本号放置在一个集合里面。具体实现还需增加如下配置:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>2.4.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>包名.类名</mainClass>
</transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/spring.handlers</resource>
</transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/spring.schemas</resource>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
四、微服务搭建架构注意点
4.1 微服务的springcloud大致说明
**springboot:**其实就是spring+springmvc的叠加
**springcloud:**其实就是springboot和springcloud之间的叠加。至于cloud的内嵌boot版本。不一定和cloud版本号一致,详情看上图。所以,在构建cloud的时候,可以不需要增加boot
4.2 微服务的打包工具及指令
这里不谈第三方插件等的一键部署。毕竟一键部署,也是对于原始的理解,提高一键部署遇到问题的解决能力。
以下是cloud的打包plugin插件。配置如下:
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
完整POM配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>eureka_center</groupId>
<artifactId>eureka_center</artifactId>
<version>1.0</version>
<packaging>jar</packaging>
<name>eureka_center</name>
<!-- FIXME change it to the project's website -->
<url>http://www.example.com</url>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-parent</artifactId>
<version>Camden.SR7</version>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka-server</artifactId>
<version>1.4.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
mvn package spring-boot:repackage说明
Spring Boot Maven plugin的最重要的goal就是repackage,它在Maven的package生命周期构建阶段,能够将mvn package生成的jar包,再次打包为可执行的jar,并将mvn package生成的软件包重命名为*.original。
如下图:
所以,打包指令,基于上述的要求。指令如下:
最好在cmd下直接执行:
mvn package spring-boot:repackage
指令执行的时候,就会先去 执行 mvn package >> 使用spring-boot:repackage重新打包
对打包后的jar解压,获得里面的Manifest.MF文件如下:
Manifest-Version: 1.0
Implementation-Title: eureka_center
Implementation-Version: 0.0.1-SNAPSHOT
Archiver-Version: Plexus Archiver
Built-By: Administrator
Implementation-Vendor-Id: eureka_center
Spring-Boot-Version: 1.4.6.RELEASE
Implementation-Vendor: Pivotal Software, Inc.
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: eureka.center.EurekaCenterStart
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Created-By: Apache Maven 3.3.1
Build-Jdk: 1.8.0_161
Implementation-URL: http://maven.apache.org
如果在Start-Class里面失效的话,最好在打包过程中。自行指定main函数。
因为Spring Boot Maven plugin会在打包过程中自动为Manifest文件设置Main-Class属性,它还可以受Spring Boot Maven plugin的配置属性layout控制的,代码如下:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>${start-class}</mainClass>
<layout>ZIP</layout>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
4.3 微服务的内嵌容器Tomcat/Jetty/Undertow启动
采用内嵌tomcat容器的POM文件配置如下:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.4.RELEASE</version>
</parent>
<groupId>com.zzk</groupId>
<artifactId>zzk</artifactId>
<version>0.0.1-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
按照我上面的 4.1 分类里面的打包指令
mvn package spring-boot:repackage
那么项目就可以在云上执行起来。如图:
采用Jetty容器的POM文件配置如下:
为什么要用Jetty呢?Jetty更适合长连接。比如聊天、下载。需要连接一直保持。springboot默认为tomcat。如果要采用jetty的话,那么需要排除tomcat的内嵌容器。其他代码如常
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 使用Jetty -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
采用undertow容器的POM文件配置如下:
但是。jsp是tomcat里面独有的。改了undertow后,就不支持jsp格式了。
POM增加如下配置,去修改容器的:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<!-- 排除tomcat容器-->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 使用undertow容器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
至于springboot容器切换的源码。是取决于:
org.springframework.boot.autoconfigure.web.EmbeddedServletContainerAutoConfiguration
源码奉上:源码就可以看到。springboot是根据你导入的是tomcat或者jetty或者undertow的jar包,从而引用哪个容器的。
@AutoConfigureOrder(-2147483648)
@Configuration
@ConditionalOnWebApplication
@Import({EmbeddedServletContainerAutoConfiguration.BeanPostProcessorsRegistrar.class})
public class EmbeddedServletContainerAutoConfiguration {
public EmbeddedServletContainerAutoConfiguration() {
}
public static class BeanPostProcessorsRegistrar implements ImportBeanDefinitionRegistrar, BeanFactoryAware {
private ConfigurableListableBeanFactory beanFactory;
public BeanPostProcessorsRegistrar() {
}
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
if (beanFactory instanceof ConfigurableListableBeanFactory) {
this.beanFactory = (ConfigurableListableBeanFactory)beanFactory;
}
}
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
if (this.beanFactory != null) {
this.registerSyntheticBeanIfMissing(registry, "embeddedServletContainerCustomizerBeanPostProcessor", EmbeddedServletContainerCustomizerBeanPostProcessor.class);
this.registerSyntheticBeanIfMissing(registry, "errorPageRegistrarBeanPostProcessor", ErrorPageRegistrarBeanPostProcessor.class);
}
}
private void registerSyntheticBeanIfMissing(BeanDefinitionRegistry registry, String name, Class<?> beanClass) {
if (ObjectUtils.isEmpty(this.beanFactory.getBeanNamesForType(beanClass, true, false))) {
RootBeanDefinition beanDefinition = new RootBeanDefinition(beanClass);
beanDefinition.setSynthetic(true);
registry.registerBeanDefinition(name, beanDefinition);
}
}
}
@Configuration
@ConditionalOnClass({Servlet.class, Undertow.class, SslClientAuthMode.class})
@ConditionalOnMissingBean(
value = {EmbeddedServletContainerFactory.class},
search = SearchStrategy.CURRENT
)
public static class EmbeddedUndertow {
public EmbeddedUndertow() {
}
@Bean
public UndertowEmbeddedServletContainerFactory undertowEmbeddedServletContainerFactory() {
return new UndertowEmbeddedServletContainerFactory();
}
}
@Configuration
@ConditionalOnClass({Servlet.class, Server.class, Loader.class, WebAppContext.class})
@ConditionalOnMissingBean(
value = {EmbeddedServletContainerFactory.class},
search = SearchStrategy.CURRENT
)
public static class EmbeddedJetty {
public EmbeddedJetty() {
}
@Bean
public JettyEmbeddedServletContainerFactory jettyEmbeddedServletContainerFactory() {
return new JettyEmbeddedServletContainerFactory();
}
}
@Configuration
@ConditionalOnClass({Servlet.class, Tomcat.class})
@ConditionalOnMissingBean(
value = {EmbeddedServletContainerFactory.class},
search = SearchStrategy.CURRENT
)
public static class EmbeddedTomcat {
public EmbeddedTomcat() {
}
@Bean
public TomcatEmbeddedServletContainerFactory tomcatEmbeddedServletContainerFactory() {
return new TomcatEmbeddedServletContainerFactory();
}
}
}
关键源码如下:
tomcat容器:
Jetty容器:
undertow容器:
4.4 微服务的外嵌容器TOMCAT启动
**暂且讨论使用外置tomcat容器的打包发布。时间有限,不然写不完**
POM文件修改如下:
包类型修改为war。
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.zzk</groupId>
<artifactId>zzk</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.4.RELEASE</version>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
继承SpringBootServletInitializer覆盖基类的configure方法。使其基于外部容器,从而找到对应的入口类。
public class ZZKApplication extends SpringBootServletInitializer{
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(ZZKApplication.class);
}
public static void main(String[] args) {
SpringApplication.run(ZZKApplication.class, args);
}
}
4.5 微服务jar之传参启动jar
基于生产环境,微服务启动占用的堆栈等大小,是取决于执行命令的。不能单纯在IDE下面去增加。不然部署上去云。根本无效。
cloud的内存占用是比较高的。有时在公司,我们的开发测试环境,没必要需要那么高的内存占用。而且。高内存的占用,有时还会出现服务无法正常访问的问题。启动后,内存占用,在2G的云上,内存直接占用到了900M。内存利用太低了。
解决方案如下:
可以在jar启动的时候,增加参数的传参。
java -Xms128m -Xmx256m -jar xxx.xxxx-xxx-xxxxxx-0.0.1-SNAPSHOT.jar &
当然,在上线和测试之间,我更推荐采用脚本启动。做好测试启动脚本和生产启动脚本。这里就不讨论了。以后再谈。