文章目录
- 一、开场白
- 1. 程序员打赌的故事
- 2. 目标: 尽量在不修改代码的情况下将springmvc框架以独立jar方式运行
- 二、出师不利
- 方案一、Spring Web工程转Spring Boot
- 方案二、引入内置服务器jetty
- 其余备用方案
- 三、柳暗花明
- 遇见jetty-runner
- 测试验证
- 准备工作:
- 部署工作
- 四、再接再厉
- 一、新建maven工程pom.xml
- 二、编写核心逻辑
- 三、测试
- 五、遗留难题
- 难难难!!! 小C突然发现这个代码在开发阶段竟然是无法调试的。
一、开场白
1. 程序员打赌的故事
小A、小B、小C都在一家初创公司工作,小A是系统运维,小B和小C都是后台开发,他们都是能力卓越的IT工程狮。
某日加班后,三人聚在一起闲聊。小A向其余2人抱怨说,咱们公司开发小组维护的那个历史比较悠久的B项目,在测试服务上部署太麻烦了,每次更新都需要先停Tomcat服务器,再删掉旧的war和目录,然后上传war包,最后再重启服务。你们看看能不能改造下,最好能象springboot工程一样,上传一个jar,一个命令启停就搞定了,能这样就谢天谢地了!
小B说,据我所知,那个B项目是springmvc框架开发的,历史悠久,想把他转换为springboot框架Jar运行,基本上不太可能,除非大动。
小C说,我看有一定可行性,只是需要时间做技术预研。
于是,3人约定将这件事情作为一个技术攻坚工作,谁能够先成功解决,另外2人请吃饭。而且可以上报本季度的效率提升之星(获得免费2天的调休时间以及物质奖励)
2. 目标: 尽量在不修改代码的情况下将springmvc框架以独立jar方式运行
二、出师不利
经过一段时间的技术预研,小B、小C他们提出了以下2个主要方案和备用方案
方案一、Spring Web工程转Spring Boot
步骤1:删除web.xml
步骤2:pom.xml导入springboot
步骤3:添加springboot 启动代码,保留springmvc工程xml配置文件,用ImportResource注解引入
参考案例: springmvc-dbutils-redis、 springmvc-dbutils-to-boot
方案二、引入内置服务器jetty
步骤1:pom.xml导入jetty相关组件,一般包含jetty-webapp、jetty-jsp、jetty-server等。
步骤2:编写启动类,设置jetty启动的各项参数。
参考案例:keta-custom
其余备用方案
自己实现一套web逻辑,可以参考的项目主要有:h2-database、Jenkins
不幸的是以上几种方案,均违背了以下前提:
- 代码修改变动太大,对原始代码侵入太强
- java web程序一般都是war,改造成boot工程未必就是Jar(其实war如果能独立运行的也是可以接受的)
事情似乎陷入了死胡同。。。
三、柳暗花明
遇见jetty-runner
大家继续寻找方案,这天小C,在查找资料时,忽然在jetty官网看到这样一句话:
Jetty Runner ,This chapter explains how to use the jetty-runner to run your webapps without needing an installation of Jetty.
Deploying a Simple Context Let’s assume we have a very simple webapp that does not need any resources from its environment, nor any configuration apart from the defaults. Starting it is as simple as performing the following:
java -jar jetty-runner.jar simple.war
官网文档:https://eclipse.dev/jetty/documentation/jetty-9/index.html#runner
测试验证
准备工作:
从maven中央仓库下载jetty-runner jar
因为本地开发环境是jdk8,所以下载了支持jdk版本为1.8的9.3或者9.4系列版本。
部署工作
小C急忙将之前开发的一个springmvc工程打成的dbtool_simple.war找出来,与jetty-runner.jar放在同一级目录
并敲下这个命令:java -jar jetty-runner-9.4.52.v20230823.jar dbtool_simple.war
后台打印出一段日志后,dbtool_simple.war竟然成功启动了
小C激动的都快喊出来了。
四、再接再厉
小C冷静下来之后,看着jar、war陷入了沉思,现在jar、war其实是分离的,其实可以将war打包到jar里面运行,经过一段思考之后,小C提出了如下优化方案:
暂时将此项目命名为jetty-runner-extra
,这样项目打出的jar为jetty-runner-extra.jar
我们需要完成如下优化:
- 执行
java -jar jetty-runner-extra.jar
搜索jar内存在的war运行,否则给出提示war不存在
,程序终止。 - 执行
java -jar jetty-runner-extra.jar --addwar
交互运行,列出当前目录(包含子目录)war供选择添加,添加后生成新的jar,jar执行逻辑同1。 - 执行
java -jar jetty-runner-extra.jar --addwar war/simple.war
如指定的war存在,直接添加,添加后生成新的jar,jar执行逻辑同1。否则给出提示war不存在
,程序终止。
说干就干!
一、新建maven工程pom.xml
pom.xml
<?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>com.fly</groupId>
<artifactId>jetty-runner-extra</artifactId>
<version>1.0.0</version>
<name>jetty-runner-extra</name>
<url>http://maven.apache.org</url>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<log4j.version>2.12.1</log4j.version>
<java.version>1.8</java.version>
<skipTests>true</skipTests>
</properties>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-runner</artifactId>
<version>9.4.52.v20230823</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.5</version>
</dependency>
<!-- slf4j + log4j2 begin -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>${log4j.version}</version>
</dependency>
<!-- log4j end -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.4.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<minimizeJar>false</minimizeJar>
<filters>
<filter>
<artifact>*:*</artifact>
</filter>
</filters>
<transformers>
<!-- 往MANIFEST文件中写入Main-Class是可执行包的必要条件。ManifestResourceTransformer可以轻松实现。 -->
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.fly.JettyExtraRunner</mainClass>
</transformer>
<!-- AppendingTransformer 用来处理多个jar包中存在重名的配置文件的合并,尤其是spring -->
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/services/javax.servlet.ServletContainerInitializer</resource>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
这里我们引入了jetty-runner、commons-io、log4j2、lombok还有maven-shade-plugin插件,插件主要实现可执行jar的配置。
二、编写核心逻辑
JettyExtraRunner.java
package com.fly;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Scanner;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.eclipse.jetty.runner.Runner;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@SuppressWarnings("deprecation")
public class JettyExtraRunner
{
private static URL url = JettyExtraRunner.class.getProtectionDomain().getCodeSource().getLocation();
/**
* 遍历文件或Jar寻找war运行
*
* @param args
* @throws IOException
*/
public static void main(String[] args)
throws IOException
{
log.info("类加载路径: {}", url.getPath());
boolean isJar = url.getPath().endsWith(".jar");
if (isJar)
{
processInJar(args);
}
}
/**
* Jar遍历-寻找war-拷贝-运行
*
* @param args
* @throws IOException
*/
private static void processInJar(String[] args)
throws IOException
{
// 调用方式1: java -jar jetty-runner-extra.jar 搜索jar内存在的war运行,否则给出提示 war不存在,程序终止
if (args.length == 0)
{
try (JarFile jarFile = new JarFile(url.getFile()))
{
int num = 0;
Enumeration<JarEntry> entrys = jarFile.entries();
while (entrys.hasMoreElements())
{
JarEntry jar = entrys.nextElement();
String name = jar.getName();
if (name.endsWith(".war"))
{
num++;
log.info("即将加载运行:{}", name);
try (InputStream is = JettyExtraRunner.class.getResourceAsStream("/" + name))
{
File file = new File(name);
FileUtils.copyInputStreamToFile(is, file);
Runner.main(new String[] {file.getCanonicalPath()});
return;
}
}
}
if (num == 0)
{
log.error("未发现war文件,程序终止");
}
}
return;
}
// 调用方式2: java -jar jetty-runner-extra.jar --addwar 交互运行,列出当前目录(包含子目录)war供选择添加
if (args.length == 1 && "--addwar".equals(args[0]))
{
Collection<File> files = FileUtils.listFiles(new File(url.getPath()).getParentFile(), new String[] {"war"}, true);
if (files.isEmpty())
{
log.error("未发现war文件,无法添加");
return;
}
File selected;
if (files.size() == 1)
{
selected = files.toArray(new File[0])[0];
}
else
{
// 列出->选择
try (Scanner sc = new Scanner(System.in))
{
int input;
do
{
int index = 1;
for (File file : files)
{
log.info("序号{}: {}", index++, file.getCanonicalPath());
}
log.info("请输入序号1-{}选择war文件", files.size());
input = sc.nextInt();
} while (input < 1 || input > files.size());
selected = files.toArray(new File[0])[input - 1];
log.info("你选择了war文件:{} ", selected.getCanonicalPath());
}
}
addWar(selected);
return;
}
// 调用方式3: java -jar jetty-runner-extra.jar --addwar war/simple.war 如指定的war存在,直接添加,否则给出提示 war不存在,程序终止
if (args.length == 2 && "--addwar".equals(args[0]))
{
String path = args[1];
File war = new File(path);
if (war.exists())
{
log.info("文件:{}", war.getCanonicalPath());
addWar(war);
}
else
{
log.error("{} 不存在,程序终止", path);
}
return;
}
}
/**
* 添加war到新jar中
*
* @param war
* @see [类、类#方法、类#成员]
*/
private static void addWar(File war)
{
try
{
File srcJar = new File(url.getPath());
String newJar = srcJar.getCanonicalPath().replace(".jar", DateFormatUtils.format(System.currentTimeMillis(), "_HHmmssSSS") + ".jar");
addWarToJar(war, srcJar, newJar);
}
catch (IOException e)
{
log.error(e.getMessage(), e);
}
}
/**
* 将war添加到srcJar并重命名为newJar
*
* @param war
* @param srcJar
* @param newJar
* @throws IOException
*/
private static void addWarToJar(File war, File srcJar, String newJar)
throws IOException
{
log.info("即将添加war文件:{} 到Jar中...", war.getCanonicalPath());
try (JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(newJar)); JarFile jarFile = new JarFile(srcJar))
{
// 遍历jar文件数据写入新jar
Enumeration<JarEntry> entrys = jarFile.entries();
while (entrys.hasMoreElements())
{
JarEntry jarEntry = entrys.nextElement();
if (jarEntry != null)
{
jarOutputStream.putNextEntry(jarEntry);
try (InputStream entryInputStream = jarFile.getInputStream(jarEntry))
{
IOUtils.copy(entryInputStream, jarOutputStream);
}
}
}
// 追加war写入数据
JarEntry warEntry = new JarEntry("war/" + war.getName());
jarOutputStream.putNextEntry(warEntry);
try (InputStream entryInputStream = new FileInputStream(war))
{
IOUtils.copy(entryInputStream, jarOutputStream);
}
}
}
}
三、测试
我们在项目的根目录执行mvn clean package
便生成了 jetty-runner-extra.jar
目录结构如下:
我们来实际运行下:
五、遗留难题
难难难!!! 小C突然发现这个代码在开发阶段竟然是无法调试的。
各位大佬,快来帮帮他,提供思路来解决他的难处!!
把这段代码变得聪明起来!!解决问题有帮助的网友,版主会点名感谢!!
俗话说得好: 军功章上有你的一半也有我的一半