对现有Spring MVC + JSP项目部署方案的改造

对现有Spring MVC + JSP项目部署方案的改造

Scroll Down

对现有 Spring MVC + JSP 项目部署方案的改造

吐槽

虽然我以前见过手动替换 war 包的,但是当这边原来的人员离职,项目交由我,居然教我手动 copy classes 文件和 JSP 文件的时候,我是崩溃的。。

0202 年了,为什么还会有人这么做?而且,他们竟然能忍个一两年都是一直手动,这点我还是有、服气的。

总结一下他们的操作步骤:

1、使用 Eclipse 本地点击 clean

2、至少2、3分钟,等待 classes 和 JSP 更新到对应存放的目录

3、远程一台 windows 2012 服务器、登上之后再远程另一台 windows 2012 服务器

4、将这两个文件夹里的文件 copy 到远程服务器对应的目录,完成替换

5、手动重启服务器上的 tomcat

整个步骤下来十几二十分钟就没了,遇到一些其他的情况,走个神啥的,部署一下半小时起步。


V1.0 本地push + 服务器.bat

我无法忍受重复的机器人劳动,于是想了想有何解法,一开始想的是直接用个Jenkins 得了,然后顺便把版本管理工具换成 Git,我也尝试了把项目改换成 Spring Boot + Maven,但是折腾了一天,由于各种奇奇怪怪的问题,暂时失败了。

甚至激进的想过要不要给它重构掉,但是权衡了一波,考虑到种种原因,只好暂时放弃。

于是我申请要一个端口,说弄自动部署。。结果竟然不了了之,行,不给就不给,不给我也能想办法。

我的思路是,既然暂时没办法将编译源码的步骤放到服务器执行,那我就来想办法将他们手动执行的这些给自动化,分析一下这5个操作其实只做了两件事

1、本地代码编译

2、将编译之后的代码放到服务器,并使其运行

我的本地编译有热部署的插件帮我搞定,那么就只要保证到服务器上能直接获得到我本地的编译之后的文件就行了,于是事情变得简单:

将本地编译之后的代码上传到在线的代码仓库,然后在服务器上拉下来就好了。

其实按照规范来讲,这些操作都是要报备的,但是emmmm一言难尽,连测试服务器都不给。

考虑到网络原因,在线代码仓库我选了码云,建了个私有仓库,然后项目根目录底下建了个 git ignore文件:

README.md
README.en.md
config/
lib/
META-INF/
tld/
web.xml
classes/*.properties
classes/*.xml
classes/*.zip
classes/templates
classes/ROOT
classes/com/package/common/util/Const.class
.idea
.settings

把乱七八糟的有些不能删,还有些暂时不敢删的的文件排除出去,使最后提交上去的只有 classes/pages/ 这两个文件夹里的东西。

然后服务器上建了个文件夹从码云把编译之后的文件拉下来,又写了个bat,replace-classes-pages.bat:

@echo off
d:
cd /project-name-resources\project
git pull 
xcopy   classes\com\package\name  D:\web\WebRoot\WEB-INF\classes\com\package\name /s /y && xcopy   pages  D:\web\WebRoot\WEB-INF\pages /s  /y
net stop tomcat 
ping -n 20 127.1 >nul
net start tomcat 

这个写得很简陋,而且有改进的空间,写的时候也遇到了各种问题,比如重启 tomcat 一开始写的是net stop tomcat && net start tomcat

直接双击执行bat,一切执行正常,可是当我在程序里调用此bat的时候,只能关闭掉tomcat服务,死活不能给我启动。。巨坑,暂时没想到是为何。

但是还是很麻烦,我每次得在本地从 SVN 拉下来他们更新的代码,然后切换窗口使 IDEA 失去焦点触发热部署,接着到命令行执行 git push 那一套,再远程到服务器上双击bat。

由于远程服务器还不止一个组在用,所以连接还经常被人给挤掉。。。也不知是谁,我太难了。

呼~ 听起来就让人喘不过气来,不过那天晚上再搞就很太晚了,于是我就回去了,起码1.0 版本上了,再也不用我手动压缩文件夹,再拷贝过去解压替换了。


V2.0 WebHook + Node · JS/GO

由于是老项目,还没办法重构,所以整个过程十分的曲折。

一开始我直接在项目里用 Java 写了个接口,接收到请求就本地调用 replace-classes-pages.bat,但是老是各种原因失败。我就拉着我们前端用Node起了个服务,然后用shelljs去调用 bat。

java 代码就非常的简单,但是好用:

@Controller
@NoAuth
public class CI {
    private final Logger logger = LoggerFactory.getLogger(CI.class);
    @RequestMapping("/ci")
    @ResponseBody
    public String ci(HttpServletRequest request) {
        String password = "this is a secret...";
        String auth = request.getHeader("X-Gitee-Token");
        if (StringUtils.isNotEmpty(auth)) {
            if (password.equals(auth)) {
                String result = HttpUtil.get("http://127.0.0.1:3000");
                logger.info("请求ci,result:{}", result);
                return "okkkkk";
            }
        }
        return "wtf!";
    }
}

js代码如下:

var _path = _interopRequireDefault(require("path"));

var _fsExtra = _interopRequireDefault(require("fs-extra"));

var _express = _interopRequireDefault(require("express"));

var _shelljs = _interopRequireDefault(require("shelljs"));

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

const app = (0, _express.default)();
app.get('/', (req, res) => {
  const common_resources_path = "D:/project-name-resources/project";
  const common_target_path = "D:/web/WebRoot/WEB-INF";

  const class_resources = _path.default.join(common_resources_path, 'classes/com/package/name');

  const page_resources = _path.default.join(common_resources_path, 'pages');

  const class_target = _path.default.join(common_target_path, 'classes/com/package/name');

  const page_target = _path.default.join(common_target_path, 'pages');

  _shelljs.default.cd(`d:/project-name-resources/project`).exec(`git pull`);

  copyDir(class_resources, class_target);
  copyDir(page_resources, page_target);

  _shelljs.default.exec(`net stop tomcat && net start tomcat`);
 
  res.status(200).json({
    content: 'WOW Awesome!!!!!!'
  });
});

function copyDir(from, to) {
  _fsExtra.default.copy(from, to, err => {
    if (err) {
      console.log('An error occured while copying the folder.');
      return console.error(err);
    }

    console.log('Copy completed!');
  });
}

app.listen(3000, () => console.log(`Server running`));

这写得其实也是非常的简陋,然后 shelljs 也是调用 bat 会有问题,就是死活启动不了 tomcat,气得我不行,同时深感自己基础知识匮乏,心有余而力不足。

后又用 PM2 给 Node 项目起了个守护进程,防止挂掉。

于是我的部署流程就变成了,本地热更新代码之后,git push一下,那边就不用我操心了,码云的 Webhook 地址就填的我用 java 写的那个接口。

但这个方案还是存在问题,shelljs 去执行前半部分的流程都好好的,但重启不了 tomcat,出现过好几次,我这边 push 完代码,我就不管了,然后就有人找我说项目挂掉啦,咳咳。

我只好远程登上去,手动启动一下 tomcat 服务。

这不稳定的玩意,还得想办法。

然后我搜了一下如何用 Go 调用 bat 文件,写了这个:

package main

import (
    "fmt"
    "net/http"
	"os/exec"
	"time"
	"log"
	"os"
)

func main() {
    http.HandleFunc("/", execBat)
    http.ListenAndServe(":3000", nil)
}

func execBat(w http.ResponseWriter, r *http.Request) {
    // shellPath := "C:/Users/cat/Desktop/test.bat"
    shellPath := "D:/xxx/replace-classes-pages.bat"
    command := exec.Command(shellPath) 
    err := command.Start()
	if nil != err {
		fmt.Println(err)
	}
	fmt.Println("Process PID:", command.Process.Pid)
	err = command.Wait() 
	if nil != err {
		fmt.Println(err)
	}
	fmt.Println("ProcessState PID:", command.ProcessState.Pid())
	nowDateTime := time.Now().Format("2006-01-02 15:04:05")
	fmt.Println("deploy sucess!! at ", nowDateTime)

	logPath := "D:/xxx/xx-ci.log"
    logFile,err  :=  os.OpenFile(logPath,os.O_CREATE|os.O_WRONLY|os.O_APPEND, 7777)
    defer logFile.Close()
    if err != nil {
        log.Fatalln("open file error !")
    }
	debugLog := log.New(logFile,"[Debug]",log.Llongfile)
	debugLog.SetFlags(debugLog.Flags() | log.LstdFlags)
	debugLog.Println("deploy success! Save 10 minutes of your life!!")

这个 exec 执行 bat 文件比 shelljs 好用多了,完美的可以重启 tomcat,并且我加了一行简陋的日志,这样我就可以看到每次的部署记录了,其实 gitee 的WebHooks 后台也是有请求记录的,只是我 Java 写的那个服务没有好好给它返回值,所以得不到确切消息,这个也留待后续优化。

go build -ldflags "-H=windowsgui" example.go生成一个 exe程序,设置成开机自启,再把tomcat服务设置成自启动,这样也不用怕腊鸡 windows 哪天莫名奇妙的自动重启了。

现在就舒服多了,我只要从 SVN 拉一下代码,等 Jrebel 热部署一下,然后 git commit -am "update comment" , git push。就搞定了,整个部署过程从20多分钟,变成了1分钟。

不过目前仍然存在很大问题:由于编译放在了本地做,所以只能我自己的这台主机 push,其他小伙伴们没法部署。这个之后还是得想办法把编译放在服务器上,然后通过大家 push 代码或者 PR 的时候触发 WebHooks。

目前这个项目还存在着太多太多问题:

  • 没有包管理也没有分支管理
  • jsp 给页面逻辑写得稀碎
  • 代码和数据库丝毫不遵循命名规范
  • 老旧代码太多,焦油坑无人敢踩
  • 还包含了其他项目,剪不断理还乱
  • 权限角色菜单管理(旧版)的那个模块做的什么玩意,又慢又丑,设计也不合理
  • 代码里各种奇葩写法
  • 没有测试服务器,数据备份机制也近乎没有
  • 仅有我目前整理的很小一部分文档和需求
  • 客户可以随意变更需求,而我们这边还得照着做?
  • ...

可以说是非常难受了,而且最关键的是,做这个项目我毫无成就感,甚至想去转Go了,以后就可以再也不用碰这种无聊的业务项目了。

V3.0 打 war 包

V2.0 方案使用的这段时间,虽然时有问题,但总归是能用的,然后某天,公司突然宣布断网。。开发人员想要上网只能通过一台公共的可上网的机器,各自登录mstsc 账号。。。

那我的把 Git 当作临时网盘使用的方案就泡汤了,因为本地的主机没办法直连外网,而且后来的一个多月派我到客户现场驻场,只能通过公司的给的 vpn 连入公司内网,然后用笔记本 mstsc 我在公司的主机,这又增加了复杂度。

后来发现 Tomcat 配置文件 D:\apache-tomcat-8.5.40\conf\server.xml 的 Host 属性 有两个子配置 :

<Host appBase="webapps" autoDeploy="true" name="localhost" unpackWARs="true">

autoDeploy 和 unpackWARs 见名知意。就是直接把 war 包丢到 tomcat 的 webapps 目录底下,就可以自动解压部署。于是我就想着,要想办法把这个项目打成 war 包!这样就不用每次都上传和下载这么多细碎的小文件了,只要一个 war 包就搞定。

可实际上,我忽略了一个很致命的问题,那就是 血坑的 D:\web\WebRoot\attachment

不知道之前的哪个牲口,将这个项目的所有上传文件!竟然全部!存放在项目根目录下!项目附件虽然目前还没多少数据,9个G,但是持续在增长之中,最关键的是,Tomcat 的 unpackWARs 自动解压 war 包的时候,会用此 war 包中的目录,直接将原来的项目底下的同名目录覆盖掉,这意味着根本就不能将 attachment 文件夹放入 war 包。

而且堂堂的附件文件,这样打包搬运来搬运去,也太愚蠢了,更何况小文件的复制非常慢,无论是从哪个角度,都不能这样干。

那么问题变成了,我要如何在打成 war 包的同时排除一些文件夹,于是我想到了中古神器 Ant。兴冲冲的下载配置并运行,结果啪的一下就报错了,无法顺利的打造 war 包,行,那就复古呗。Ant 的实现方式不就是 用 xml 配置文件的方式来描述打包过程的吗,既然你报错那就不用了,自己实现得了,而且目前只我一人部署,所以并不需要考虑那么多。

还需要考虑的一个问题是,开发环境的 配置文件和服务器上的配置文件存在不一致的情况,这个可以通过更改 applicationContext-common.xml中PropertyPlaceholderConfigurer 的 locations 属性,将 jdbc.properties 等不一致的配置文件,重新指向到其他的目录,也就是说,不用项目里的这个配置文件了,用的是服务器上某个目录下的配置文件。

  <!--引入外部文件 -->
    <bean
            class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
        <property name="locations">
            <list>
                <value>classpath:jdbc.properties</value>
<!--   <value>file:${catalina.home}/jdbc.properties</value>-->
            </list>
        </property>
    </bean>

开发机和生产服务器使用不同配置文件的问题解决了,接下来解决打包时要把不需要的文件排除出去的问题。

于是我回归了世界的本源——Terminal 。写了以下的一个脚本 pck.bat:

B:
cd project/
svn up
choice /t 3 /d y /n >nul
explorer B:\project\project-name\classes\artifacts\nwyy
choice /t 60 /d y /n >nul
cd /project/project-name/classes/artifacts/out/
jar -cvf project.war  WEB-INF  js  css fonts highcharts html images META-INF sjdr index.jsp

既然无法排除某文件夹,那么我就只把需要的文件和文件夹打包好了。

非常的简单粗暴,最大化利用了我目前掌握的所有信息和手上的所有资源,诶,甚至有点暴力美学的感jio是怎么肥事?

有个需要注意的地方是,如果有些已经更改的文件所在的文件夹,不在上述脚本打包的目录列表之内,很简单,加一行就完事了,同时 undeploy.bat 解压之后记得复制一下该文件夹到 tomcat 指定的运行目录。

解释一下这个脚本的执行过程,首先改变目录进入项目编译之后的 out 目录,这里有打包所需的 class、page 文件等,然后执行 svn up 将其他人的代码拉下来,接下来 explorer 打开打包之后 war 包的存放目录,开始等待xx秒,此时由于多出了个窗口,Jrebel 检测到失去焦点,触发自动更新 classes 和 resources,预估等待 xx 秒之后,执行 jar --cvf xxx.war [dir1 dir2] 命令,打包 war 包。

打完 war 包之后又是需要手动操作的部分了,Ctrl C 一下 war 包,到 172.16.1.11 ,双击 backup-war.bat备份一下 上次的 war 包,自动打开 war 包存放目录,Ctrl V 粘贴。然后双击 deploy.bat ,就可以去倒杯茶喝喝了,等回来就发现部署完成了。

backup-war.bat的内容如下:

@echo off
explorer D:\project-resources\project-war
move D:\project-resources\project-war\project-backup.war  D:\project-resources\project-war\project-backup-backup.war
move D:\project-resources\project-war\project.war   D:\project-resources\project-war\project-backup.war

简单的备份了之前两个版本,如果本版部署出现了问题,需要快速回滚,这时候将 project-backup.war 重命名为 project.war , 然后再次执行 undeploy.bat 即可,这个过程其实也可以用脚本做掉。

deploy.bat 用来和 pck.bat 搭配,好家伙,好好的一个 war 包,被我当成了压缩包来用,内容如下:

deploy.bat:

@echo off
d:
cd /project-resources\project-war
jar -xvf nongwei.war
xcopy   WEB-INF\classes\com\dom  D:\web\WebRoot\WEB-INF\classes\com\dom /s /r /y^
 && xcopy   attachment\excel  D:\web\WebRoot\attachment\excel /s /r /y^
 && xcopy   css D:\web\WebRoot\css /s /r /y^
 && xcopy   js  D:\web\WebRoot\js /s /r /y^
 && xcopy  html\yzt D:\web\WebRoot\html\yzt /s /r /y^
 && xcopy   WEB-INF\pages  D:\web\WebRoot\WEB-INF\pages /s /r /y
net stop tomcat8 && net start tomcat8

就是无脑解压+复制+重启。

目前的方案基本就是这样,核心思想就是能用脚本做的事情,坚决不要用手工做。

其他

拼命的还技术债,却怎么也还不完。折腾到最后也没折腾出一个全套的方案。这就是不做好技术选型的后果,如果当初立项考虑到了这些,后期根本不用这么折腾,差点就要 自己写一个 版本管理工具和项目部署工具了。我在做的是 Git 和 Ant、Maven、Gradle 被发明之前的事情,这不合理。比直接看人家源码学习效率低多了。而且从生产角度来说,怎么也应该把先进的生产工具拿来使用,提高生产力啊。