只使用写字板和java原生工具做Java开发
by bmwhero 2022.11
本文主旨在于通过只使用写字板、Java原生工具的方式,故意从轮子开始造起,探讨最本源的Java开发的样子,并以此讲清Java的编码、编译、打包和运行原理。为了用最少的篇幅讲全,本文将不涉及复杂的编码,而是以简单的package中的HelloWorld,加入配置文件、内部class调用、外部lib调用之后的HelloWorld,以及简单的web工程为三个例子。
作者并非杠精,深知从轮子造起没有意义,且长于后端开发的Java,在实际项目使用中涉及的外部库调用、人员分工、单元测试、版本管理、上线运行等复杂度很高,只用写字板和java进行全周期开发是不现实的。但这种造轮子的基本功,即搞清楚Java从编码到运行的基础概念和基本流程,对于程序员的知识积累有较大的帮助,因此特写出本文。
本文特别适合使用轮子多年或上来就在一个成熟框架上开发,但对基本原理没有过涉足的Java工程师,也可供Java初学者在学习中作为参考资料。
一、约定和示例需求
1 约定
约定以Windows为开发平台,以Linux为运行平台;约定开发中的JDK为17,运行中的JDK为11。
约定读者之前已经编写并成功运行过最简单的HelloWorld程序,了解CLASSPATH的配置和使用机制,可以较为熟练地操作Windows和Linux的命令行。
笔者以自己笔记本中D:\work\dev\workplace\xiaoche为基础目录,下设不相关的project01、project02、project03为工程目录,分别对应后面要讲的三个示例。
2 软件前提
所用写字板,可以是Windows自带notepad,也可以是Notepad++、VSCode等文本编辑器,不借助任何开发和打包插件。
所用的Java,为了故意区别开发和实际运行情况,Windows上的开发使用Java17,运行使用11,两个都是LTS版本。开发中,需要确保javac、jar、java三个命令可以在windows command控制台中直接使用。运行中,要确保java命令可以在Linux终端终直接使用。
所用的Linux,可以是近世的任何Linux分发版本,最简单的,使用虚拟机上安装的CentOS7.X即可,Linux上需要安装只用于例三的tomcat,版本为8或9都可以。
3 示例需求
(1)例一,简单HelloWorld,设为project001,为一个Java独立程序,只有一个.java文件,不做任何调用,在控制台打印Hello World即可。但相比很多百度出来的helloworld,这里故意设置了package,以讲清Java在编译中的要点。
(2)例二,复杂HelloWorld,设为project002,在简单HelloWorld的基础上,另起炉灶,给出两个不同package的.java文件,一个调用另一个,同时有log4j2的外部库调用,有配置文件的读取。在这种复杂情况下,讲清规划目录结构、如何做从编码到运行的全流程。
(3)例三,Web工程,设为project003,完全手工搭建一个最简的servlet web工程,使用servlet4.0。与例二类似,在web工程建立后,讲清楚目录结构、编码、编译、制作web工程、打包,并war包放置在Linux的tomcat中运行。
二、Package内的简单Hello World
例一:简单HelloWorld项目
根目录:D:\work\dev\workplace\xiaoche\project001,后面将用<根目录>作为对这个绝对路径的引用。
应用:app001
这个例子专注于讲清如何打出一个最简单的jar包,并分析.java、.class、和jar包之间的关系,并查看jar包内包含的内容。该例子作为例一,解释说明的文字较多,并且让读者看到,一个看似简单的HelloWorld,可以包含很多引申的java知识。
1 编码
.java是java的源代码文件,也是程序员直接编写的主要文件。
根目录下,建立com/xiaoche/app001三级目录,在app001中创建App001Main.java。目录结构如下所示。
project001
└─com
└─xiaoche
└─app001
App001Main.java
编写App001Main.java文件并保存
package com.xiaoche.app001;
public class App001Main{
public static void main(String[] args){
System.out.println("Xiaoche's Hello World!");
}
}
这里给出多层目录的简单解释。Java采用了package方式对代码文件做树形管理,不同的功能区分在不同的package中。这里设定的com/xiaoche/app001三级目录,对应App001Main.java中的package名称com.xiaoche.app001,这个唯一的java文件必须放在第三级目录中。而App001Main.java的文件名,必须和java文件中的class名保持一致。
设定package以后,后面的运行中,需要指定运行的是package下面,一个class中的main方法。即com.xiaoche.app001.App001Main。
之所以这么执着地要把一个helloworld放在package里,是因为这样才能看到javac和java执行时候的本质。而且实际使用中的所有类,都应该位于提前规划好的某个package中。
2 编译
在根目录中,使用下面的javac命令进行编译,编译成功将生成不可读的.class文件,即程序用来执行的文件。
cd <根目录>
javac -encoding UTF-8 --release 11 com/xiaoche/app001/*.java
这里使用了-encoding参数,即表示用UTF-8编码方式进行非西文字符的编译。如果程序后续需要在Linux上运行,该参数一定显性指定。
另外这里按照约定,为了能够在运行时兼容Java11,使用了--release参数。
编译后,在app001中,除了刚才自己写的.java文件,还生成了一个.class文件,这个就是运行时调用的class文件。我们可以先使用java命令来运行该class。
cd <根目录>
java com.xiaoche.app001.App001Main
控制台(windows中命令行)应该能够回显出程序代码中print的内容。
Xiaoche's Hello World!
3 打包
上面从编译后的运行可以看到,使用java命令已经可以执行.class文件了。但在实际运行环境的使用中,极少直接使用class文件,而是将一个项目中众多class文件打成一个jar包,将该jar包放入CLASSPATH中运行。
尽管只是一个HelloWorld程序,我们还是将它打包并在windows和linux上运行。打包使用java自带的jar命令。
cd <根目录>
mkdir target
jar cvfe ./target/app001.jar com.xiaoche.app001.App001Main com/xiaoche/app001/*.class
以下是执行正确的回显:
已添加清单
正在添加: com/xiaoche/app001/App001Main.class(输入 = 455) (输出 = 312)(压缩了 31%)
jar的参数说明:jar命令中,cvfe同时使用,表示c--创建,v--详细输出,f--输出到文件,e--指定执行的main方法在哪一个类中。
后面紧跟的第一项,对应f参数,是要打包的包名,这里加了一层target目录。第二项对应e参数,指定main class。最后一项是要打包的class集合。这样就在<根目录>\target\下面,生成了app001.jar。值得注意的是,用e参数指定main class并不是必须的,这样指定是为了后面的运行中,可以不再指定要执行的main方法。
.jar文件,实际就是zip压缩包,符合zip标准的所有规范,可以使用winrar、7-zip、unzip等工具打开查看。使用jar命令还可以查看生成的app001.jar。
cd <根目录>
jar -tvf ./target/app001.jar
以下是执行正确的回显:
0 Tue Nov 22 23:24:52 CST 2022 META-INF/
105 Tue Nov 22 23:24:52 CST 2022 META-INF/MANIFEST.MF
455 Tue Nov 22 22:52:06 CST 2022 com/xiaoche/app001/App001Main.class
可以看到,之前编译的.class文件已经放入jar,此外生成了一个元信息目录META-INF,下面有唯一的元数据文件MANIFEST.MF。这是一个可读的文件,可以从jar包中拿出并用文本编辑器打开。内容如下。
Manifest-Version: 1.0
Created-By: 17 (Oracle Corporation)
Main-Class: com.xiaoche.app001.App001Main
这里指明了Manifest的版本、创建者和Main-Class,如果打包时没有使用e参数,则没有Main-Class这一行。
4 运行
在编译完成的时候,已经使用java命令运行了main class,那时是在没有打包的情况下直接运行。这里将不使用class文件,转而用jar文件运行程序。
(1)在Windows中运行
方法一:jar包调用
cd <根目录>\target
java -jar app001.jar
输出:
Xiaoche's Hello World!
可以看到,在打包时,指定了main class以后,直接使用java的jar参数,调用jar包,即可运行程序,非常简单。
当然也可以直接调用类名,那么就需要将这个jar包放入CLASSPATH中,并在java命令中指定类名。
方法二:class调用
cd <根目录>\target
java -classpath ./app001.jar com.xiaoche.app001.App001Main
输出:
Xiaoche's Hello World!
如果在打包时没有指定main class,则只能使用方法二。这里多说一句,一个Java程序的项目中可以有多个class都有main方法,在使用中,可以通过不同的类名调用不同的main方法,因此方法二实际上更加灵活。
(2)在Linux中运行
由于Java的跨平台特性,Java程序在Linux中运行和在Windows中运行,本质都是运行在JVM虚拟机上,从里(JVM)到表(命令)没有差别。这里只给出使用方法一的例子。
cd /opt/xiaoche
mkdir app001
<这里将Windows上打包好的app001.jar放入/opt/xiaoche/app001中>
java -jar app001.jar
输出:
Xiaoche's Hello World!
三、复杂Hello World
例二:复杂HelloWorld项目
根目录:D:\work\dev\workplace\xiaoche\project002,后面将用<根目录>作为对这个绝对路径的引用。
应用:app002
这个例子将在例一的基础上做以下四个方面的扩展:
(1)使用Calendar获取当前格式化的日期和时间,加入Hello World打印中
(2)间隔一段时间,循环打印,不退出,保持进程常在
(3)除了打印日志,打印内容还使用log4j2输出到日志里
(4)打印内容、打印间隔可以通过配置文件进行配置
1 需求分析
有了这些新需求后,可以看出,我们在开发中需要引入外部的lib库sef4j.jar和log4j2.jar,需要设置配置文件app002.conf,需要创建DateHelper类来制作格式化的日期时间,需要把在main class中打印输出、控制台输出。
在运行时中,没法再像前面的例子那样简单,需要对目录做出规划,这里需要有conf目录存放配置文件,lib目录存放调用的外部库jar包,app目录放开发的程序jar包,logs目录存放日志。在这种情况下,编码前,需要规划好目录结构,在之后的编码和编译中,按照既定的目录进行编排。
2 目录结构
这里在<根目录>下,将开发用的dev和运行用的runtime做分离,分别按需创建各自的子目录,如下所示。
project002/
├── dev
│ ├── conf
│ ├── lib
│ ├── src
│ └── target
└── runtime
├── app
├── conf
├── lib
├── logs
对于开发时候使用的dev目录,里面src用来存放编码,target存放编译的class,lib用来放外部库,conf目录用来存放配置文件。
对于运行时候使用的runtime目录,里面app目录存放主程序的jar包,lib目录存放外部库(同开发),conf目录存放配置文件(同开发),logs目录存放生成的日志,在runtime根目录中,可以设置启动停止脚本。
这里故意将dev和runtime做严格的分离,是为了严格区分开发和运行,互不干涉。后面的编码、编译,将在dev中进行,而打包和Windows上的运行,将在runtime中进行。
3 编码
(1)编码的简单设计
<根目录>\dev\src目录中,继续按照package的分层使用,给出子目录结构
com/xiaoche/app002/App02Main.java
com/xiaoche/app002/util/DateUtil.java
com/xiaoche/app002/util/ConfigUtil.java
在例一的基础上,多了package com.xiaoche.app002.util,里面有DateUtil.java用来处理日期时间,ConfigUtil.java用来处理配置文件,ConfigUtil.java将读取配置文件app02.conf。同时相比例一,由于多了外部log4j2相关jar包的调用,需要配置log4j2的配置文件,并将log4j2的4个jar包作为外部库。
因此,除去放在<根目录>\dev\src目录中的java代码,在<根目录>\dev\conf中,放置app02.conf、log4j2.xml两个配置文件;在<根目录>\dev\lib中,放置log4j2的四个相关jar包。
(2)ConfigUtil.java
ConfigUtil.java中基于java.util包,用反射读取配置文件,支持动态reload。这里没有做过多的文件存在性校验,具备主程序调用配置文件这一复杂性即可。
代码清单:<根目录>\dev\src\com\xiaoche\app002\util\ConfigUtil.java
package com.xiaoche.app002.util;
import java.io.*;
import java.net.URL;
import java.util.Properties;
public class ConfigUtil {
private static Properties props = null;
private static File configFile = null;
private static long fileLastModified = 0L;
private static String configFileName = "app002.conf";
private static void init() {
URL url = ConfigUtil.class.getClassLoader().getResource(configFileName);
configFile = new File(url.getFile());
fileLastModified = configFile.lastModified();
props = new Properties();
load();
}
private static void load() {
try {
props.load(new InputStreamReader(new FileInputStream(configFile),"UTF-8"));
fileLastModified = configFile.lastModified();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static String getConfig(String key) {
if ((configFile == null) || (props == null)) init();
if (configFile.lastModified() > fileLastModified) load(); //当检测到文件被修改时重新加载配置文件
return props.getProperty(key);
}
}
同时给出conf目录中的主配置文件app002.conf
程序配置文件清单:<根目录>\dev\conf\app002.conf
#app002.conf
#输出内容
mesage.content=HelloWorld002
#输出间隔,单位ms
message.inteval=5000
(3)DateUtil.java
DateUtil.java实现将当前日期时间戳,使用YYYY-MM-DD HH:mm:DD的格式打印出来,供主程序调用。
代码清单:<根目录>\dev\src\com\xiaoche\app002\util\DateUtil.java
package com.xiaoche.app002.util;
import java.util.Calendar;
public class DateUtil {
public static String getFormattedDateTime() {
Calendar calendar = Calendar.getInstance();
String year = String.valueOf(calendar.get(Calendar.YEAR));
String month = String.valueOf(calendar.get(Calendar.MONTH) + 1);
String date = String.valueOf(calendar.get(Calendar.DAY_OF_MONTH));
String hour = String.valueOf(calendar.get(Calendar.HOUR_OF_DAY));
String minute = String.valueOf(calendar.get(Calendar.MINUTE));
String second = String.valueOf(calendar.get(Calendar.SECOND));
String formatted = year + "." + month + "." + date + " " + hour + ":" + minute + ":" + second;
return formatted;
}
}
(4)App02Main.java
最后给出主程序代码,相比例一,这里添加了log4j2实现的slf4j用于输出日志,添加了打印的循环,打印的内容则由前面Util中提供的日期时间戳+配置文件中读取的核心内容相加而得。同样的,对于输入参数没有做过多的校验,目的仅仅在于讲清使用了外部jar包、配置文件、内部Class调用。
代码清单:<根目录>\dev\src\com\xiaoche\app002\App02Main.java
package com.xiaoche.app002;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.xiaoche.app002.util.ConfigUtil;
import com.xiaoche.app002.util.DateUtil;
public class App002Main {
Logger logger = LoggerFactory.getLogger(this.getClass());
public void outPutMesages() {
int timewait = Integer.parseInt(ConfigUtil.getConfig("message.inteval"));
logger.info("内容输出间隔为:{}毫秒", timewait);
for(int i=0;i<1000;i++){
logger.debug("组包开始");
String timeMessage = DateUtil.getFormattedDateTime();
logger.debug("时间戳信息为:{}", timeMessage);
String coreMessage = ConfigUtil.getConfig("mesage.content");
logger.debug("核心信息为:{}", coreMessage);
String fullMessage = "时间为" + timeMessage + ", 情报为" + coreMessage;
logger.debug("组包结束");
//执行模拟业务代码,输出fullMessage
System.out.println(fullMessage);
logger.info("最终输出信息:{}",fullMessage);
try {
Thread.sleep(timewait);
}catch(InterruptedException ie) {
logger.error(ie.getMessage());
}
}
}
public static void main(String[] args) {
App002Main am = new App002Main();
am.outPutMesages();
}
}
这里的日志输出,用的是slf4j为框架,用log4j2实现的方式,需要引入4个jar文件,这4个文件都放在lib目录中:
外部库清单:<根目录>\dev\lib\中的jar包
slf4j-api-1.7.36.jar
log4j-slf4j-impl-2.17.2.jar
log4j-core-2.17.2.jar
log4j-api-2.17.2.jar
随主程序,给出log4j2.xml的基本配置,这里不做复杂的解释说明,能够对console和文件同时做输出即可。
log4j2配置文件清单:<根目录>\dev\conf\log4j2.conf
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xml>
<Configuration status="WARN" monitorInterval="30">
<Properties>
<Property name="logDir">./logs</Property>
</Properties>
<Appenders>
<!-- Console -->
<Console name="Console" target="SYSTEM_OUT">
<ThresholdFilter level="DEBUG" onMatch="ACCEPT" onMismatch="DENY"/>
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%-12.-12t] [%-5p] %c{10}-%M(%F:%L) - %m%n"/>
</Console>
<!-- RollingFile:Info -->
<RollingFile name="RollingFileInfo" fileName="${logDir}/info.log"
filePattern="${logDir}/info-%d{yyyy-MM-dd}-%i.log">
<ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY"/>
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"/>
<Policies>
<TimeBasedTriggeringPolicy/>
<SizeBasedTriggeringPolicy size="10 MB"/>
</Policies>
<DefaultRolloverStrategy max="3"/>
</RollingFile>
</Appenders>
<Loggers>
<Logger name="com.xiaoche" level="DEBUG" additivity="false">
<AppenderRef ref="Console"/>
<AppenderRef ref="RollingFileInfo"/>
</Logger>
<Root level="INFO">
<AppenderRef ref="Console"/>
<AppenderRef ref="RollingFileInfo"/>
</Root>
</Loggers>
</Configuration>
(5)编码后的目录结构
这里最后给出,编码后,目录结构以及里面包含的文件,相关文件在前面(1)-(4)中已全部完成。
project002
+---dev
| +---conf
| | app002.conf
| | log4j2.xml
| +---lib
| | log4j-api-2.17.2.jar
| | log4j-core-2.17.2.jar
| | log4j-slf4j-impl-2.17.2.jar
| | slf4j-api-1.7.36.jar
| +---src
| | \---com
| | \---xiaoche
| | \---app002
| | | App002Main.java
| | |
| | \---util
| | ConfigUtil.java
| | DateUtil.java
| \---target
\---runtime
+---app
+---conf
+---lib
\---logs
这时的根目录下,dev中,只有conf下有两个配置文件,lib下有4个调用的jar包,src下分两个package,有3个java代码文件,target目录没有内容,等待后面编译时,将src内的内容,编译成class放入target。且此时的runtime中,除了规划好的目录,还没有内容,目前还没到运行时。
可以看到,这是一个只使用了写字板做出来的非常干净的编码即编码相关文件的目录结构。
(6)开发中的测试
在开发的同时,可以随时做测试,测试需要用javac做临时编译,然后用java运行。这里给出基本命令,关于javac和java如何对这个比较复杂的HelloWorld做参数配置,后面的编译和运行的小节里会给出详细解释,这里先提前预热一下。
cd <根目录>\dev
临时编译:
javac -encoding UTF-8 ^
--release 11 ^
-cp "./lib/slf4j-api-1.7.36.jar;./lib/log4j-slf4j-impl-2.17.2.jar;./lib/log4j-core-2.17.2.jar;./lib/log4j-api-2.17.2.jar" ^
-s ./src src/com/xiaoche/app002/*.java src/com/xiaoche/app002/util/*.java
测试运行:
java -cp "./lib/slf4j-api-1.7.36.jar;./lib/log4j-slf4j-impl-2.17.2.jar;./lib/log4j-core-2.17.2.jar;./lib/log4j-api-2.17.2.jar;./conf;./src" com.xiaoche.app002.App002Main
javac的编译,这里前两个参数和例一一致,不再解释。
-cp是设置CLASSPATH,由于例一中自己的代码没有任何外部库的调用,因此不需要这个参数。但在例二中,自己的代码用到了log4j2的外部库,因此要用-cp来设置CLASSPATH。此外,在编码中还配置了两个配置文件,但配置文件只有在运行时才有用,编译时不需要,因此CLASSPATH只包含log4j2的4个jar即可。
-s是指定待编译的java文件的位置,在例一中,由于直接在package入口目录com的同级目录下进行编译,因此不需要指定-s。但在例二中,dev下,将代码放在了专用的src中,也就是com目录位于了<根目录>\dev\src中,而测试的临时编译,是在<根目录>\dev下进行的,相比例一,多出一层src目录,因此必须显性指定-s ./src,告诉javac,代码请到src下面去找。虽然指定-s,但后面列出java文件时,src还要带上,即src/com/xiaoche......,这一点要注意,这也是很多时候编译报找不到源文件的根本原因。
编译完成后,会发现在每个.java文件的同级目录下,都生成了.class文件,也就是这时源代码和编译好的文件处于同目录。这种编译方式没做到开发和运行分离,因此这里叫做临时编译。
使用java命令做临时运行的时候,类似也加了-cp,但这里的-cp中的CLASSPATH要多出两个目录,即./conf和./src。运行的时候会到./conf这里找两个配置文件;./src是告诉java,把编译好的class文件的目录也加入CLASSPATH,这个如果忘了,运行直接就会报找不到main class。执行开始后,运行如下,临时编译的程序,已经可以读取配置文件、调用log4j2打日志,且程序循环运行,不再退出。
2022-11-23 18:19:22.743 [main ] [INFO ] com.xiaoche.app002.App002Main-outPutMesages(App002Main.java:15) - 内容输 出间隔为:5000毫秒
2022-11-23 18:19:22.746 [main ] [DEBUG] com.xiaoche.app002.App002Main-outPutMesages(App002Main.java:17) - 组包开 始
2022-11-23 18:19:22.767 [main ] [DEBUG] com.xiaoche.app002.App002Main-outPutMesages(App002Main.java:19) - 时间戳 信息为:2022.11.23 18:19:22
2022-11-23 18:19:22.767 [main ] [DEBUG] com.xiaoche.app002.App002Main-outPutMesages(App002Main.java:21) - 核心信 息为:HelloWorld002
2022-11-23 18:19:22.768 [main ] [DEBUG] com.xiaoche.app002.App002Main-outPutMesages(App002Main.java:23) - 组包结 束
时间为2022.11.23 18:19:22, 情报为HelloWorld002
2022-11-23 18:19:22.769 [main ] [INFO ] com.xiaoche.app002.App002Main-outPutMesages(App002Main.java:26) - 最终输 出信息:时间为2022.11.23 18:19:22, 情报为HelloWorld002
2022-11-23 18:19:27.776 [main ] [DEBUG] com.xiaoche.app002.App002Main-outPutMesages(App002Main.java:17) - 组包开 始
2022-11-23 18:19:27.778 [main ] [DEBUG] com.xiaoche.app002.App002Main-outPutMesages(App002Main.java:19) - 时间戳 信息为:2022.11.23 18:19:27
2022-11-23 18:19:27.780 [main ] [DEBUG] com.xiaoche.app002.App002Main-outPutMesages(App002Main.java:21) - 核心信 息为:HelloWorld002
2022-11-23 18:19:27.781 [main ] [DEBUG] com.xiaoche.app002.App002Main-outPutMesages(App002Main.java:23) - 组包结 束
时间为2022.11.23 18:19:27, 情报为HelloWorld002
2022-11-23 18:19:27.782 [main ] [INFO ] com.xiaoche.app002.App002Main-outPutMesages(App002Main.java:26) - 最终输 出信息:时间为2022.11.23 18:19:27, 情报为HelloWorld002
且随着程序的成功启动运行,<根目录>\dev\下面,自动添加了logs目录,并生成了info.log,里面有编码中写的info级别日志。
2022-11-23 18:19:22.743 [main] INFO com.xiaoche.app002.App002Main - 内容输出间隔为:5000毫秒
2022-11-23 18:19:22.769 [main] INFO com.xiaoche.app002.App002Main - 最终输出信息:时间为2022.11.23 18:19:22, 情报为HelloWorld002
2022-11-23 18:19:27.782 [main] INFO com.xiaoche.app002.App002Main - 最终输出信息:时间为2022.11.23 18:19:27, 情报为HelloWorld002
2022-11-23 18:19:32.813 [main] INFO com.xiaoche.app002.App002Main - 最终输出信息:时间为2022.11.23 18:19:32, 情报为HelloWorld002
测试完成后,可以将<根目录>\dev\src下的所有.class文件递归地删除,把<根目录>\dev\logs目录删除,保持目录结构清爽。
4 编译
到<根目录>\dev中,在这里使用javac命令,将自己写的3个代码编译成.class文件,放入target目录中,与前面编码最后一步的临时编译非常类似,只是将生成class文件的目的地指定到了之前规划好的target目录里,不再和.java文件混编。同样地,因为main class中使用了log4j2,因此log4j2的相关jar文件,在用javac编译时,必须指定到CLASSPATH中。
与例一的编译类似,依然保留UTF-8和对java11的兼容这两个参数。具体命令如下:
javac -encoding UTF-8 --release 11 -cp "./lib/slf4j-api-1.7.36.jar;./lib/log4j-slf4j-impl-2.17.2.jar;./lib/log4j-core-2.17.2.jar;./lib/log4j-api-2.17.2.jar" -s ./src src/com/xiaoche/app002/*.java src/com/xiaoche/app002/util/*.java -d ./target
由于参数较多,在windows命令行中,可以分行写,每行行末用^符(键盘上6的上档字符)结尾,最后一行不用,这样和上面的命令效果一致。(在Linux终端中用\换行,也是同样的效果)
javac -encoding UTF-8 ^
--release 11 ^
-cp "./lib/slf4j-api-1.7.36.jar;./lib/log4j-slf4j-impl-2.17.2.jar;./lib/log4j-core-2.17.2.jar;./lib/log4j-api-2.17.2.jar" ^
-s ./src src/com/xiaoche/app002/*.java src/com/xiaoche/app002/util/*.java ^
-d ./target
javac的编译,这里前两个参数和例一一致,不再解释。
-cp是设置CLASSPATH,由于例一中自己的代码没有任何外部库的调用,因此不需要这个参数。但在例二中,自己的代码用到了log4j2的外部库,因此要用-cp来设置CLASSPATH。此外,在编码中还配置了两个配置文件,但配置文件只有在运行时才有用,编译时不需要,因此CLASSPATH只包含log4j2的4个jar即可。
-s是指定待编译的java文件的位置,在例一中,由于直接在package入口目录com的同级目录下进行编译,因此不需要指定-s。但在例二中,dev下,将代码放在了专用的src中,也就是com目录位于了<根目录>\dev\src中,而测试的临时编译,是在<根目录>\dev下进行的,相比例一,多出一层src目录,因此必须显性指定-s ./src,告诉javac,代码请到src下面去找。虽然指定-s,但后面列出java文件时,src还要带上,即src/com/xiaoche......,这一点要注意,这也是很多时候编译报找不到源文件的根本原因。
-d是指编好的class文件放在哪里,这里显性指定一个之前规划好、创建好,但没用过的目录target。
编译完成后,我们再看一下目录结构以及里面包含的文件。
project002
+---dev
| +---conf
| | app002.conf
| | log4j2.xml
| +---lib
| | log4j-api-2.17.2.jar
| | log4j-core-2.17.2.jar
| | log4j-slf4j-impl-2.17.2.jar
| | slf4j-api-1.7.36.jar
| +---src
| | \---com
| | \---xiaoche
| | \---app002
| | | App002Main.java
| | \---util
| | ConfigUtil.java
| | DateUtil.java
| \---target
| \---com
| \---xiaoche
| \---app002
| | App002Main.class
| |
| \---util
| ConfigUtil.class
| DateUtil.class
\---runtime
+---app
+---conf
+---lib
\---logs
相比编码完成时,只是在target目录中,多了一套与src目录中完全一样的目录结构,只是里面都是class文件。而runtime目录中只有之前规划的目录架构,下一步马上将要手工做build,即充实起runtime目录。
5 打包
这里的打包,相比例一中的打包,内容更加丰富,除了包含打jar包并放入指定目录外,还包含了做一个build,所需要的配置文件、外部jar,以及做出启动、停止脚本。
(1)程序执行jar包
首先,把上面编译好的class,连同目录结构,一起打包成一个jar文件,放到<根目录>\runtime\app中。
cd <根目录>
jar cvfe ./runtime/app/app002.jar com.xiaoche.app002.App002Main -C ./dev/target com
这里可以和例一中打包的命令做一个对比。
例一打包:jar cvfe ./target/app001.jar com.xiaoche.app001.App001Main com/xiaoche/app001/*.class
例一和例二分别指明了各自jar包的目标目录,都指定了main class是谁,这两点没什么可注意的。
例一在最后,直接给了class的路径com/xiaoche/app001/*.class,没有任何定向,这是因为,第一,当时运行jar命令的目录,和com目录处于同级目录,第二,显性指定到最内部的class文件,是因为当时例一中class和java处于同目录混编,不这么写会把java文件也打到包里,不利于知识产权。
而例二也就是本例中,用了-C参数,做待打包文件的重定向,是因为例二运行jar命令是在根目录中,而class文件在<根目录>\dev\target中,需要对class文件所在目录做重新指定。而且因为例二不再混编java文件和class文件,因此这里只需要在指向target目录后,跟要打包的class文件的头目录com即可。
打包完成后,还是可以用jar的t参数查看jar包包含的内容。
cd <根目录>
jar -tvf ./runtime/app/app002.jar
以下是执行正确的回显:
0 Wed Nov 23 19:40:16 CST 2022 META-INF/
105 Wed Nov 23 19:40:16 CST 2022 META-INF/MANIFEST.MF
0 Wed Nov 23 17:51:50 CST 2022 com/
0 Wed Nov 23 17:51:50 CST 2022 com/xiaoche/
0 Wed Nov 23 17:51:50 CST 2022 com/xiaoche/app002/
2237 Wed Nov 23 17:51:50 CST 2022 com/xiaoche/app002/App002Main.class
0 Wed Nov 23 17:51:50 CST 2022 com/xiaoche/app002/util/
1588 Wed Nov 23 17:51:50 CST 2022 com/xiaoche/app002/util/ConfigUtil.class
1124 Wed Nov 23 17:51:50 CST 2022 com/xiaoche/app002/util/DateUtil.class
jar包中实际上除了META-INF/MANIFEST.MF,只有我们自己编译的3个class,配置文件、外部jar包都没有包含在其中。
(2)拷贝配置文件和外部库
将dev中conf目录的内容照搬到runtime的conf目录中,lib目录同理。
cd <根目录>
copy dev\conf\* runtime\conf\
copy dev\lib\* runtime\lib\
(3)制作Linux程序启动和停止脚本
相比windows,Linux对使用shell脚本制作程序后台启动和停止的脚本更加友好,也更加标准。故这里先给出Linux的shell脚本写法。
cd <根目录>/runtime
==新建start_app002.sh,内容如下==
#!/bin/bash
nohup java -cp "./lib/slf4j-api-1.7.36.jar:./lib/log4j-slf4j-impl-2.17.2.jar:./lib/log4j-core-2.17.2.jar:./lib/log4j-api-2.17.2.jar:./conf:./app/app002.jar" com.xiaoche.app002.App002Main > /dev/null 2>&1 &
==新建stop_app02.sh,内容如下==
#!/bin/bash
ps -ef | grep java | grep com.xiaoche.app002.App002Main | awk '{print $2;}' | xargs kill -9
这里对启动中nohup一行做出解释。
与之前编码时候,执行的java相比,这里的-cp中,需要用冒号而非分号分隔,这是Linux对比Windows中CLASSPATH的区别。而且在cp中,在外部jar包引用、配置文件目录引用一致的情况下,这里没有./src,而是换成了./app/app002.jar,这是因为原来是在开发时对src下的class做调用,而现在已经打包,原来散碎的class已经被打成了app001.jar。
我们的程序默认会在前台执行,按下ctrl+C可以停止,当终端连接关闭时,程序也会退出。改为nohup方式,可以在后台运行。后面有个> /dev/null,表示后台运行时,输出到一个文件中,这里的文件故意指向/dev/null,也就是输出到这个特别文件中以后,什么都不会显示了。
考虑到app002本身做的就是输出字符串到文件的工作,因此启动脚本中的>,也可以选择不输出到/dev/null,而是输出到一个产出物文件中。
#!/bin/bash
nohup java -cp "./lib/slf4j-api-1.7.36.jar:./lib/log4j-slf4j-impl-2.17.2.jar:./lib/log4j-core-2.17.2.jar:./lib/log4j-api-2.17.2.jar:./conf:./app/app002.jar" com.xiaoche.app002.App002Main > ./harvest.txt 2>&1 &
(4)制作windows程序启动器
Windows中没有明确的后台运行的概念,因此这里只做Windows的程序前台启动器。
cd <根目录>/runtime
==新建start_app002.bat,内容如下==
@echo off
java -cp "./lib/slf4j-api-1.7.36.jar;./lib/log4j-slf4j-impl-2.17.2.jar;./lib/log4j-core-2.17.2.jar;./lib/log4j-api-2.17.2.jar;./conf;./app/app002.jar" com.xiaoche.app002.App002Main
(5)打包后的目录结构(最终)
project002
+---dev
| +---conf
| | app002.conf
| | log4j2.xml
| +---lib
| | log4j-api-2.17.2.jar
| | log4j-core-2.17.2.jar
| | log4j-slf4j-impl-2.17.2.jar
| | slf4j-api-1.7.36.jar
| +---src
| | \---com
| | \---xiaoche
| | \---app002
| | | App002Main.java
| | \---util
| | ConfigUtil.java
| | DateUtil.java
| \---target
| \---com
| \---xiaoche
| \---app002
| | App002Main.class
| \---util
| ConfigUtil.class
| DateUtil.class
\---runtime
| start_app02.bat
| start_app02.sh
| stop_app02.sh
+---app
| app002.jar
+---conf
| app002.conf
| log4j2.xml
+---lib
| log4j-api-2.17.2.jar
| log4j-core-2.17.2.jar
| log4j-slf4j-impl-2.17.2.jar
| slf4j-api-1.7.36.jar
\---logs
可以看到除了最后的runtime\logs目录,其他目录都已经按照规划填满。注意写的三个批处理,是在runtime下,而log4j2.xml中配置的日志路径刚好是./logs,因此在运行时,只要是在runtime这个目录下运行sh或者bat,这个空着的logs目录,将正好承载生成的日志。
6 运行
将runtime打成zip包,已经可以放到任意安装了Java11或以上版本的Windows或Linux下运行。相比例一,例二在打包阶段做了很多准备工作,产出了一套类似上线build的runtime程序,故运行就非常简单了。
(1)Windows
Windows下解包,比如解包到D:\work\tmp\try下,进入build目录,可以直接双击start_app02.bat文件,即可运行。可注意观察运行后,日志的输出。
(2)Linux
Linux下解包,注意查看两个shell文件是否有可执行权限,如果没有用chmod 744 *.sh添加。之后使用./start_app02.sh做后台启动,使用./stop_app01.sh做刹停。同Windows上一样,运行后可以到logs目录中查看日志。
7 总结
经过漫长的制作,经历需求分析、目录结构设计、编码、编译、打包、运行,一套完整的纯手工制作的build完成并通过了上线运行。可以看到,在这个例子中,增加了一些java程序都会有的最基本功能,外部jar调用、读配置文件、输出日志等,但手工编码和编译、打包的难度大了很多。试想一个真正的生产项目,要管理如此多的库、配置、代码等,几乎是一个不可能完成的任务。因此实际使用中,必须引入自动化的工具帮助做这一全流程的工作。
同时希望读者通过例一、例二看到,一个简单的HelloWorld,能够引申出很多概念和拓展,对于帮助理解程序开发,理解架构,有着很好的帮助。
四、Web工程
例三:简单的Web - Servlet实现
根目录:D:\work\dev\workplace\xiaoche\project003,后面将用<根目录>作为对这个绝对路径的引用。
应用:app003
部署目标:tomcat9.0,默认http监听端口8080
URL:http://hostname:8080/app003/myservlet
本例中,将继续采取完全手工的方式,制作一个最基本的servlet应用,打成war包,部署到tomcat中并验证可以提供访问服务。本例的主旨在于讲清Web应用在开发、部署时的本质,没有引用外部库,没有设置日志输出和配置文件,因此从功能上类似例一,比例二简单。
servlet的实现,需要在自己的代码中手写一个servlet,继承JavaEE中的javax.servlet.http.HttpServlet类,为了简便,只引用JavaEE的servlet-api.jar,该jar包可以从tomcat软件的lib目录中获得。因此,为了完成本例,需要在Linux上部署v8.0或v9.0版本的tomcat,并从该tomcat中拿到servlet-api.jar放到开发环境中。开发环境,依然只使用原生Java和写字板。
1 目录结构
开发和制作build的初始目录结构如下所示。
project003
+---build
\---dev
| servlet-api.jar
\---compiled
初始只设置两个目录,dev目录用于做编码和编译,该目录中提前准备好从tomcat中拿到的servlet-api.jar,作为实现servlet的唯一外部库依赖,dev里面还有一个叫做compiled的空目录,用于稍后编译的输出。build目录则用于制作war包,后面在制作web工程和打包环节将使用并丰富该目录。
我这里的servlet-api.jar是从tomcat9.0.68中拿到的,servlet版本为4.0。
2 编码
在<根目录>\dev下面,直接建立package的层级目录:com/xiaoche/app003,在app003下创建本例中唯一需要开发的MyServlet.java,代码如下。
代码清单:<根目录>\dev\com\xiaoche\app003\MyServlet.java
package com.xiaoche.app003;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException {
System.out.println("MyFirstServlet 在处理get()请求...");
PrintWriter out = response.getWriter();
response.setContentType("text/html;charset=utf-8");
out.println("<strong>My first servlet, doGet executed</strong><br>");
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException {
System.out.println("MyFirstServlet 在处理post()请求...");
PrintWriter out = response.getWriter();
response.setContentType("text/html;charset=utf-8");
out.println("<strong>My first servlet, doPost executed</strong><br>");
}
}
代码中,MyServlet类继承servlet-api.jar中的HttpServlet父类,并重写doGet和doPost两个方法。在doGet方法中,一旦客户端向MyServlet发起doGet的request,则在response页面中回显My first servlet, doGet executed。doPost类似。这里不对servlet更多的实现原理做解读,读者只需要了解,servlet作为web开发最基础的组件,今天流行的restful风格后端API、以前流行的jsp,都是在此基础上的实现。
3 编译
前面只开发了一个com.xiaoche.app003.MyServlet,这里对应地也只对MyServlet.java做编译。编译的方法和例一、例二完全类似。
cd <根目录>\dev
javac -encoding UTF-8 --release 11 -cp "./servlet-api.jar" com/xiaoche/app003/*.java -d ./compiled
相比例二,这里的-cp只需要调用一个外部库,就是前面讲的父类出处-servlet-api.jar,要编译的java所在的package;这回直接就在dev下面,所以不需要再用-s指定源目录;-d指定开始时就创建的compiled目录,进行class文件连同package层级目录的输出。
编译完成后,现在的目录结构呈现出以下的样子。
project003
+---build
\---dev
| servlet-api.jar
+---com
| \---xiaoche
| \---app003
| MyServlet.java
\---compiled
\---com
\---xiaoche
\---app003
MyServlet.class
相比开始时的目录结构,
(1)在dev中,开发时创建了程序的package层级目录和程序文件com\xiaoche\app003\MyServlet.java。
(2)在dev/compiled中,编译时创建了可执行文件的package层级目录和可执行文件com\xiaoche\app003\MyServlet.class。
(3)build目录还没用到,这个会在下面web工程制作中使用。
4 web工程制作
随着大约20年的Java Web技术的使用,现在很少有开发人员不借助IDE环境自己纯手工制作web工程了,但这种纯手工非常有利于开发人员看清web工程是如何一砖一瓦建立起来、每个目录和每个文件的作用是什么。
前面已经在<根目录>\dev\compiled中将servlet程序部分编译好,后续的web工程制作,实际只需要在任意目录中(这里使用<根目录>\build)加入标准的web工程目录结构、刚刚编译好的程序和一个在web容器中指导web容器做地址转化的web.xml即可。为了讲述清晰,这里先给出web工程制作完成的目录结构。
project003
+---build
| \---WEB-INF
| | web.xml
| \---classes
| \---com
| \---xiaoche
| \---app003
| MyServlet.class
\---dev
| servlet-api.jar
+---com
| \---xiaoche
| \---app003
| MyServlet.java
\---compiled
\---com
\---xiaoche
\---app003
MyServlet.class
dev目录没有变化,后续也不会再变动。
build目录中,只创建一个WEB-INF目录,里面创建classes目录,classes里面放入<根目录>\dev\compiled中的全部内容。此外,在WEB-INF中,创建一个web.xml文件,并做编辑。注意WEB-INF全大写,而classes目录和web.xml这个文件名全小写。下面编辑关键的web.xml
配置清单:<根目录>\build\WEB-INF\web.xml
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0"
metadata-complete="true">
<description>
Servlet Examples.
</description>
<display-name>Servlet Examples</display-name>
<request-character-encoding>UTF-8</request-character-encoding>
<servlet>
<servlet-name>myservlet</servlet-name>
<servlet-class>com.xiaoche.app003.MyServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>myservlet</servlet-name>
<url-pattern>/myservlet</url-pattern>
</servlet-mapping>
</web-app>
这个xml文件的内容,属于八股文写法,大多数内容都是固定的。web-app节点的各个属性,照搬即可。这里主要关注的是servlet和servlet-mapping两个段落。这两个段落里的servlet-name,值必须保持一致,该节点值类似一个servlet的入口ID号,servlet中的servlet-name类似主键,servlet-mapping中的servlet-name类似外键。在servlet段中的servlet-class,指向我们开发的MyServlet类,包含package名称。而servlet-mapping段中的url-pattern,是URL最后一个层级的字符串。后面对servlet的访问中,我们使用的URL类似下面的形式:
http://<1.tomcat_hostname>:<2.tomcat_prot>/❤️.appname>/<4.url-pattern>
对于已经准备好的Linux上的tomcat,1和2实际已经固定了,比如Linux的IP是192.168.172.11,端口是tomcat默认的8080端口。而4的url-pattern就是这里定义的字符串。到这里只有3还没有,3实际是webapp的root context,在下一步打包中会获得它的值。也就是说,这里URL已经可以写为:
http://192.168.172.11:8080/
总结一下,web工程在打入war包之前的准备工作里,主要是制作WEB-INF目录,里面包含web.xml和classes目录,其中web.xml负责做URL引导,而classes目录放置编译好的servlet实现类的class。
由于本例比较简单,制作web工程的方法也比较传统,这里扩展几个知识。
(1)本例在编译时用到了servlet-api.jar,但在制作web工程时却没有包含它,这是因为这个工程是一个没法自己借助java运行的工程,而是一个必须部署到web容器(如tomcat)中才能运行的工程,因此servlet-api.jar只在编译时借用一下,运行时会借助web容器的库做运行时调用。
(2)本例没有使用其他外部的库,如例二中的log4j2,如果需要使用,则在制作时,应在WEB-INF下创建lib目录,里面放入外部库的jar包。
(3)本例没有使用额外的配置文件,如例二中的app002.conf,如果需要使用,则在制作时,应放在WEB-INF/classes/中。
(4)本例中使用了web.xml做URL引导,实际上从当前流行的配置方法来说,不用web.xml而是用代码注解annotation的方式,更为主流。本例没有涉及这种用法。
(5)本例MyServlet.java中import的servlet-api.jar的HttpServlet,隶属于Java EE,package入口为javax。但目前Java EE已经因名称使用权问题,使得所有者eclipse基金会在2019年9月被迫将其改名为Jakarta(雅加达),并发布Jakarta EE8版本,2022年版本10发布。Jakarta EE的package入口为jakarta,从第二层开始名称保持不变。因此有可能在使用更高版本的servlet-api.jar时,需要在代码中import的类,改为jakarta.xxxx。
5 打包
在制作完Web工程后,已经可以将该工程直接放在tomcat中并运行,单为了交付时更加标准,这里还是使用jar命令,在build目录中将web工程打成war包,再在后面最后一步中在tomcat中直接部署war包。
cd <根目录>\build
jar cvf app003.war ./*
由于web工程不具备独立运行的能力,因此也就无法也无需指定main class了,这里只需要将build目录中的内容,也就是WEB-INF打入war包即可。
这里的war包名称值得一提,app003.war,等于基本认定了该应用的appname就是app003。在tomcat部署后,该名称一般没有机会再做更改,而在weblogic等商业化容器软件中,在部署时可能会有一次修改的机会。因此,这里的名字,起名要谨慎;而我们下面马上就要部署到tomcat中,因此就以app003作为appname。到此,可以完善web工程制作的时候,对于URL的解析:
http://192.168.172.11:8080/
http://192.168.172.11:8080/app003/myservlet
我们按照例一中的传统,用jar的-t参数,查看该war包的构成。
jar tvf app003.war
回显如下:
0 Thu Nov 24 16:49:06 CST 2022 META-INF/
62 Thu Nov 24 16:49:06 CST 2022 META-INF/MANIFEST.MF
0 Thu Nov 24 16:19:42 CST 2022 WEB-INF/
0 Thu Nov 24 16:19:42 CST 2022 WEB-INF/classes/
0 Thu Nov 24 16:01:14 CST 2022 WEB-INF/classes/com/
0 Thu Nov 24 16:01:14 CST 2022 WEB-INF/classes/com/xiaoche/
0 Thu Nov 24 16:01:14 CST 2022 WEB-INF/classes/com/xiaoche/app003/
1118 Thu Nov 24 15:47:36 CST 2022 WEB-INF/classes/com/xiaoche/app003/MyServlet.class
780 Thu Nov 24 16:01:34 CST 2022 WEB-INF/web.xml
可以清楚地看到,除了之前制作好的web工程WEB-INF的内容全部加入war包外,jar命令依然帮助建立了META-INF/MANIFEST.MF。由于没有main class需要指定,这个文件中只有MANIFEST版本和作者信息。
最后给出例三最终的完整目录结构。
+---build
| | app003.war
| \---WEB-INF
| | web.xml
| |
| \---classes
| \---com
| \---xiaoche
| \---app003
| MyServlet.class
\---dev
| servlet-api.jar
+---com
| \---xiaoche
| \---app003
| MyServlet.java
\---compiled
\---com
\---xiaoche
\---app003
MyServlet.class
6 在tomcat中运行
在Linux的tomcat中,将war包放入webapps目录。启动tomcat。在tomcat的catalina.out中,可以看到
Deploying web application archive [/opt/tomcat9.0.68/webapps/app003.war]
Deployment of web application archive [/opt/tomcat9.0.68/webapps/app003.war] has finished in [51] ms
这表明,启动时我们的app003已经被解包部署。使用浏览器访问我们的应用:
http://192.168.172.11:8080/app003/myservlet
界面中回显:My first servlet, doGet executed
表明上述URL已经在浏览器端,向app003发起了doGet请求,位于tomcat容器内的app003,已经给了回应。在catalina.out中,可以看到
MyFirstServlet 在处理get()请求...
这个输出,就是代码中doGet方法System.out.println的内容。doPost由于需要使用表单的方式提交,这里我们没有开发前端web,也没有使用jsp,就没法通过URL做直接发request请求了,因此不做演示。
至此,一个简单的servlet工程,成功开发、部署、制作、打包,并部署到tomcat中,且可以成功访问。例三全部内容结束。
五、高级工具简介
回到最初的话题,既然只用写字板和java进行全周期开发是不现实的,那么应该用什么工具来辅助开发、编译、打包呢?这里只列出几个名字,供读者进行下一步的学习。
1 Ant
2 Maven&Gradle
3 IDE环境
Eclipse、Intellij Idea
标签:xiaoche,Java,app002,写字板,jar,java,com,class From: https://www.cnblogs.com/bmwhero/p/16923831.html