目录

· 概况

· 原理

· HDFS 架构

· 

· NameNode

· SecondaryNameNode

· fsimage与edits合并

· DataNode

· 数据读写

· 容错机制

· 数据完整性

· NameNode HA

· NameNode Federation

· HDFS Snapshots

· 操作

· API


概况

1. 文件系统抽象类FileSystem

a) 源码

 public abstract class FileSystem extends Configured implements Closeable {
// ...
}

b) 实现类

文件系统

URI方案

Java实现

定义

Local

file

org.apache.hadoop.fs.LocalFileSystem

已使用客户端校验的本地文件系统。未使用校验的本地磁盘文件系统由RawLocalFileSystem实现。

HDFS

hdfs

org.apache.hadoop.hdfs.DistributedFileSystem

Hadoop分布式文件系统

HFTP

hftp

org.apache.hadoop.hdfs.web.HftpFileSystem

支持通过HTTP方式以只读方式访问HDFS,通常和distcp命令结合使用。

HSFTP

hsftp

org.apache.hadoop.hdfs.web.HsftpFileSystem

支持通过HTTPS方式以只读方式访问HDFS。

HAR

har

org.apache.hadoop.fs.HarFileSystem

构建在Hadoop文件系统之上,对文件归档。Hadoop归档文件主要用来减少NameNode内存使用。

FTP

ftp

org.apache.hadoop.fs.ftp.FtpFileSystem

由FTP服务器支持的文件系统。

S3(原生)

s3n

org.apache.hadoop.fs.s3native.NativeS3FileSystem

基于Amazon S3的文件系统。

S3(基于块)

s3

org.apache.hadoop.fs.s3.S3FileSystem

基于Amazon S3的文件系统,解决S3的5GB文件大小限制。

2. HDFS特点

a) 适合存储超大文件:存储在HDFS的文件大多在GB、TB级别,甚至PB级别。

b) 运行于廉价硬件之上:设计时已考虑集群规模足够大时,节点故障是常态。HDFS无需运行在高可靠且昂贵的服务器,普通的PC服务器即可。

c) 流式数据访问:HDFS认为一次写入、多次读取时最高效的访问模式。数据集生成后,会长时间在此数据集上进行各种分析,每次分析都将设计该数据集的大部分甚至全部数据。

3. HDFS缺点

a) 实时数据访问弱:HDFS针对数据吞吐量做了优化,而牺牲了读取效率,无法做到秒级或毫秒级响应。

b) 大量小文件:HDFS启动时,NameNode将全部元数据加载到内存,而一般一个HDFS文件、目录和数据块的存储信息约150字节,因此文件个数受限于NameNode节点内存。过多小文件很快达到上限。

c) 多用户写入,任意修改文件:HDFS文件同时只能有一个写入者,且写操作总在文件末。

原理

HDFS 架构

1. 架构图

2. 守护进程

名称

集群中数目

作用

NameNode

1(默认)

存储文件系统元数据,存储文件与数据块映射,并提供文件系统全景图

SecondaryNameNode

1

备份NameNode数据,并负责镜像与NameNode日志数据合并

DataNode

多个(至少1个)

存储块数据

1. 文件系统块:块大小是磁盘块大小的整数倍,如ext3为4KB,NTFS为4KB。

2. HDFS块

a) HDFS文件:被划分为块大小的多个分块。

b) 默认大小:64MB。

c) 较大原因:最小化寻址开销(块足够大,从磁盘传输数据块的时间明显大于定位块开始位置所需的时间)。

d) 配置:hdfs-site.xml的参数“dfs.block.size”。

3. 副本

a) 含义:每个HDFS块在集群中保存的份数,默认为3。

b) 效果:值越高,冗余性越好,占用存储越多。

c) 配置:hdfs-site.xml的参数“dfs.replication”。

4. 块分布示例

a) 环境:文件大小150MB,块大小64MB,副本数2。

b) 分布:第1块64MB,第2块64MB,第3块22MB。

5. 块布局策略

a) 第1个副本:如果HDFS客户端在集群内,默认布局在客户端所在节点;否则随机选择一个节点,但会尽量避免存储太满或太忙的节点。

b) 第2个副本:与第1个副本不同且随机另外机架中的节点。

c) 第3个副本:与第2个副本相同机架且随机选择另外一个节点。

d) 其他副本(副本数>3):集群随机选择节点,但会尽量避免在相同机架上布局太多副本。

e) 由NameNode选择节点。

f) 示意图:副本数为3。

NameNode

1. 职责:HDFS主从(Master/Slave)架构的主角色。

2. NameNode存储的文件

a) fsimage:HDFS元数据的完整快照,每次NameNode启动时,默认加载最新的fsimage。

b) edits:fsimage的编辑日志。

SecondaryNameNode

1. 职责:定期合并fsimage和edits的辅助守护进程。

2. 部署:生产环境一般单独部署在一台服务器。

fsimage与edits合并

1. 不直接更新fsimage的原因:fsimage是一个大型文件,如果频繁执行写操作,会使系统运行极慢。

2. 定期合并的原因

a) 随时间推移,edits越来越大,一旦发生故障,回滚时间非常长。

b) 如果由NameNode合并,则NameNode可能无法提供足够资源为集群服务,所以由SecondaryNameNode合并。

3. 定期合并的过程

a) SecondaryNameNode通知NameNode准备提交edits文件,此时NameNode产生edits.new;

b) SecondaryNameNode通过HTTP GET方式获取NameNode的fsimage与edits文件(在SecondaryNameNode的current同级目录下可见 temp.check-point或者previous-checkpoint目录,这些目录中存储着从NameNode拷贝来的镜像文件);

c) SecondaryNameNode开始合并获取的上述两个文件,产生一个新的fsimage文件fsimage.ckpt;

d) SecondaryNameNode用HTTP POST方式发送fsimage.ckpt至NameNode;

e) NameNode将fsimage.ckpt与edits.new文件分别重命名为fsimage与edits,然后更新fstime,整个checkpoint过程结束。

4. 定期合并的默认时机

a) 每小时一次;

b) 或当NameNode edits文件达到默认的64MB时。

DataNode

1. 职责:HDFS主从(Master/Slave)架构的从角色。

2. 块文件:每个块都是一个文件,默认位于参数“dfs.data.dir”目录的current目录下,文件名blk_blkID。

3. 上报:DataNode启动时,向NameNode上报块信息(Block Report)。

数据读写

1. 文件读取过程

a) HDFS对客户端身份验证,两种方式:通过信任的客户端,由其指定用户名;诸如Kerberos等强制验证机制。

b) 客户端告知NameNode要读取的文件。当文件存在且用户有访问权限时,NameNode告知客户端该文件第1个块的标号以及保存该块的DataNode列表(按DataNode与客户端距离排序)。

c) 客户端直接访问最适合的DataNode读取块,该过程一直重复直到文件所有块读取完成或客户端主动关闭文件流。

d) 特殊情况,客户端是DataNode时,将从本地DataNode读取数据。

2. 文件写入过程

a) 客户端发送请求,打开一个要写入的文件。如果客户端有写入权限,则请求被送达NameNode,并建立该文件的元数据,但该文件元数据未和任何数据库关联。

b) 客户端收到“打开文件成功”响应后开始将数据写入流,数据被自动拆分成数据包,并将数据包保存在内存队列。

c) 客户端一个独立线程从队列读取数据包,并向NameNode请求一组DataNode列表,以便写入下一个块的多个副本。客户端直接连接到列表中第1个DataNode,而该DataNode又连接到第2个DataNode,第2个又连接到第3各,如此建立块的复制管道。各DataNode都会确认收到的数据包成功写入磁盘。客户端维护着一个列表,记录由尚未收到确认信息的数据包。

d) 当块被写入一组DataNode后,客户端重新向NameNode申请下一组DataNode。

e) 最终,全部数据包写入,关闭数据管道并通知NameNode写操作完成。

容错机制

1. 心跳机制:当NameNode未收到DataNode心跳包时,NameNode认为该DataNode上的数据无效。新的IO操作将不会派发给该DataNode;如果块副本数小于hdfs-site.xml的参数“dfs.replication.min”,则开始自动复制新副本到其他DataNode节点。

2. 块完整性校验:HDFS记录文件所有块的校验和,当确认校验和不一致时,会从其他DataNode节点获取块的副本。

3. 集群负载均衡:节点失效或增加可能导致数据分布不均,当某个DataNode空闲空间大于临界值时,HDFS自动从其他DataNode迁移数据保持平衡。

4. fsimage与edits:如果NameNode单点部署,fsimage、edits文件损坏时,可手工从SecondaryNameNode定期备份手工恢复。

5. 文件删除:删除并未从NameNode移除,而是存放在/trash目录可随时恢复,直到超过hdfs-site.xml的参数“fs.trash.interval”的值(秒)。

数据完整性

1. 写入时校验:客户端将数据及其校验和发送到一组DataNode组成复制管道,管道最后一个DataNode负责验证校验和,如果错误,客户端便收到ChecksumException。

2. 读取时校验:客户端对比读取数据与DataNode存储的校验和,验证成功后告知DataNode,DataNode记录到验证校验和日志(包括每个块的最后验证时间)。

3. 后台定期校验:每个DataNode后台有一个DataBlockScanner线程,定期验证Data上所有数据块。

4. 块修复:NameNode将块标记为已损坏,之后安排该块的一个副本复制到另一DataNode以达到预设副本数,最后删除已损坏的块。

NameNode HA

1. 场景

a) NameNode的热备(SecondaryNameNode是NameNode的冷备)。

b) 生产环境必备功能。

2. NameNode HA架构

a) 一个NameNode处于主状态(Active),处理客户端和DataNode请求,并把edits写入本地和共享编辑日志(NFS或QJM等)。

b) 另一个NameNode处于从状态(StandBy),启动时加载fsimage文件,然后周期性从共享编辑日志获取edits,保持与主NameNode同步。

c) 为实现主节点宕机后从节点迅速提供服务,DataNode需同时向两个NameNode汇报(Block Report)。

NameNode Federation

1. 解决问题:解决NameNode伸缩性、隔离性,以及单NameNode性能方面的问题。

2. 场景:集群规模在1000台以下时,几乎无需NameNode Federation。

HDFS Snapshots

1. 原理:HDFS Snapshots是一个只读的基于时间点的文件系统副本,快照可以时整个文件系统也可以是其中一部分。

2. 场景:常用来作为数据备份,防止用户误操作和容灾。

操作

1. HDFS文件系统命令

命令

功能

举例

hadoop dfs -ls <path>

列出文件或目录内容

hadoop dfs -ls /

hadoop dfs -lsr <path>

递归列出目录内容

hadoop dfs -lsr /

hadoop dfs -df <path>

查看目录使用情况

hadoop dfs -df /

hadoop dfs -du <path>

显示目录中所有文件及目录的大小

hadoop dfs -du /

hadoop dfs -dus <path>

显示目录总大小

hadoop dfs -dus /

hadoop dfs -count [-q] <path>

显示目录下目录数及文件数,格式为“目录数 文件数 大小 文件名”,-q指查看文件索引

hadoop dfs -count /

hadoop dfs -mv <src> <dst>

将HDFS文件移动到目标目录

hadoop dfs -mv /user/hadoop/a.txt /user/test

hadoop dfs -rm [-skipTrash] <path>

将HDFS文件移动到回收站,-skipTrash指直接删除

hadoop dfs -rm /text.txt

hadoop dfs -rmr [-skipTrash] <path>

将HDFS目录移动到回收站,-skipTrash指直接删除

hadoop dfs -rmr /text

hadoop dfs -expunge

清空回收站

hadoop dfs -expunge

hadoop dfs -put <localsrc> ... <dst>

将本地文件上传到HDFS目录

hadoop dfs -put /home/hadoop/test.txt /user/hadoop

hadoop dfs -copyFromLocal <localsrc> ... <dst>

类似-put

hadoop dfs -copyFromLocal /home/hadoop/test.txt /user/hadoop

hadoop dfs -moveFromLocal <localsrc> ... <dst>

将本地文件移动到HDFS目录

hadoop dfs -moveFromLocal /home/hadoop/test.txt /user/hadoop

hadoop dfs -get [-ignoreCrc] [-crc] <src> <localdst>

将HDFS文件下载到本地目录,-ignoreCrc指忽略CRC校验失败,-crc指下载文件及CRC信息

hadoop dfs -get /user/hadoop/a.txt /home/hadoop

hadoop dfs -getmerge <src> <localdst> [addnl]

将HDFS目录下所有文件按文件名排序合并成一个文件下载到本地目录,addnl指每个文件结尾添加一个换行符

hadoop dfs -getmerge /user/test /home/hadoop/o

hadoop dfs -cat <src>

浏览HDFS文件内容

hadoop dfs -cat /user/hadoop/test.txt

hadoop dfs -text <src>

以文本方式浏览HDFS文件

hadoop dfs -text /user/test.txt

hadoop dfs -copyToLocal [-ignoreCrc] [-crc] <src> <localdst>

类似-get

hadoop dfs -copyToLocal /user/hadoop/a.txt /home/hadoop

hadoop dfs -moveToLocal [-crc] <src> <localdst>

将HDFS文件移动到本地目录

hadoop dfs -moveToLocal /user/hadoop/a.txt /home/hadoop

hadoop dfs -mkdir <path>

创建HDFS目录

hadoop dfs -mkdir /user/test

hadoop dfs -setrep [-R] [-w] <rep> <path/file>

设置文件副本数,-R指递归执行

hadoop dfs -setrep 5 -R /user/test

hadoop dfs -touchz <path>

创建0字节的HDFS空文件

hadoop dfs -touchz /user/hadoop/test

hadoop dfs -test -[ezd] <path>

检查HDFS文件,-e指检查存在(存在返回0),-z指检查0字节(是返回0),-d指检查目录(是返回1,否返回0)

hadoop dfs -test -e /user/test.txt

hadoop dfs -stat [format] <path>

显示HDFS文件或目录统计信息

hadoop dfs -stat /user/test

hadoop dfs -tail [-f] <file>

浏览HDFS文件最后1KB内容,-f指随文件内容更新而更新

hadoop dfs -tail -f /user/test.txt

hadoop dfs -chmod [-R] <MODE[,MODE]... | OCTALMODE> PATH...

修改HDFS文件权限,-R指递归执行

hadoop dfs -chmod -R +r /user/test

hadoop dfs -chown [-R] [OWNER][:[GROUP]] PATH...

修改HDFS文件所属用户,-R指递归执行

hadoop dfs -chown -R hadoop:hadoop /user/test

hadoop dfs -chgrp [-R] GROUP PATH...

修改HDFS文件所属组别,-R指递归执行

hadoop dfs -chgrp -R hadoop /user/test

hadoop dfs -help

显示所有dfs命令帮助

hadoop dfs -help

2. HDFS其他命令参考官方文档

API

1. HDFS客户端要求:

a) 配置文件hdfs-site.xml;

b) Maven依赖

 <dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
<version>2.6.5</version>
</dependency>

2. 示例执行方法

hadoop jar hdfs-test.jar hdfs.ReadFile

3. 读取文件

 package hdfs;

 import java.io.IOException;
import java.io.InputStream; import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IOUtils; public class ReadFile { public static void main(String[] args) throws IOException {
String uri = "hdfs://centos1:9000/test/README.txt";
Configuration conf = new Configuration(); FileSystem fs = null;
InputStream in = null;
try {
fs = FileSystem.get(conf);
in = fs.open(new Path(uri));
IOUtils.copyBytes(in, System.out, 4096, false);
} finally {
IOUtils.closeStream(in);
if (fs != null) {
fs.close();
}
}
} }

4. 写入文件

 package hdfs;

 import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream; import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IOUtils; public class WriteFile { public static void main(String[] args) throws IOException {
String source = "/opt/app/hadoop-2.6.5/NOTICE.txt";
String destination = "hdfs://centos1:9000/test/NOTICE.txt";
Configuration conf = new Configuration(); InputStream in = null;
FileSystem fs = null;
OutputStream out = null;
try {
in = new BufferedInputStream(new FileInputStream(source));
fs = FileSystem.get(conf);
out = fs.create(new Path(destination));
IOUtils.copyBytes(in, out, 4096, true);
} finally {
IOUtils.closeStream(out);
if (fs != null) {
fs.close();
}
IOUtils.closeStream(in);
}
} }

5. 创建目录

 package hdfs;

 import java.io.IOException;

 import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path; public class CreateDirectory { public static void main(String[] args) throws IOException {
String uri = "hdfs://centos1:9000/test/testdir";
Configuration conf = new Configuration(); FileSystem fs = null;
try {
fs = FileSystem.get(conf);
Path dir = new Path(uri);
fs.mkdirs(dir);
} finally {
if (fs != null) {
fs.close();
}
}
} }

6. 删除目录

 package hdfs;

 import java.io.IOException;

 import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path; public class RemoveDirectory { public static void main(String[] args) throws IOException {
String uri = "hdfs://centos1:9000/test/testdir";
Configuration conf = new Configuration(); FileSystem fs = null;
try {
fs = FileSystem.get(conf);
Path dir = new Path(uri);
boolean deleted = fs.delete(dir, true); // 递归删除,也可删除文件
System.out.println(deleted);
} finally {
if (fs != null) {
fs.close();
}
}
} }

7. 检查文件存在

 package hdfs;

 import java.io.IOException;

 import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path; public class CheckFileExistence { public static void main(String[] args) throws IOException {
String uri = "hdfs://centos1:9000/test/NOTICE.txt";
Configuration conf = new Configuration(); FileSystem fs = null;
try {
fs = FileSystem.get(conf);
Path file = new Path(uri);
boolean exists = fs.exists(file);
System.out.println(exists);
} finally {
if (fs != null) {
fs.close();
}
}
} }

8. 列出子目录和文件

 package hdfs;

 import java.io.IOException;

 import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileStatus;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path; public class ListFiles { public static void main(String[] args) throws IOException {
String uri = "hdfs://centos1:9000/test";
Configuration conf = new Configuration(); FileSystem fs = null;
try {
fs = FileSystem.get(conf);
Path dir = new Path(uri);
FileStatus[] files = fs.listStatus(dir);
for (FileStatus file : files) {
System.out.println(file.getPath());
}
} finally {
if (fs != null) {
fs.close();
}
}
} }

9. 获取块位置信息

 package hdfs;

 import java.io.IOException;

 import org.apache.commons.lang.StringUtils;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.BlockLocation;
import org.apache.hadoop.fs.FileStatus;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path; public class ObtainFileInfo { public static void main(String[] args) throws IOException {
String uri = "hdfs://centos1:9000/test/NOTICE.txt";
Configuration conf = new Configuration(); FileSystem fs = null;
try {
fs = FileSystem.get(conf);
Path file = new Path(uri);
FileStatus fileStatus = fs.getFileStatus(file);
BlockLocation[] blockLocations = fs.getFileBlockLocations(fileStatus, 0, fileStatus.getLen());
for (int index = 0, length = blockLocations.length; index < length; index++) {
System.out.println("block" + index + " " + StringUtils.join(blockLocations[index].getHosts(), ","));
}
} finally {
if (fs != null) {
fs.close();
}
}
} }

作者:netoxi
出处:http://www.cnblogs.com/netoxi
本文版权归作者和博客园共有,欢迎转载,未经同意须保留此段声明,且在文章页面明显位置给出原文连接。欢迎指正与交流。

HDFS笔记——技术点汇总的更多相关文章

  1. Hadoop笔记——技术点汇总

    目录 · 概况 · Hadoop · 云计算 · 大数据 · 数据挖掘 · 手工搭建集群 · 引言 · 配置机器名 · 调整时间 · 创建用户 · 安装JDK · 配置文件 · 启动与测试 · Clo ...

  2. Spark SQL笔记——技术点汇总

    目录 概述 原理 组成 执行流程 性能 API 应用程序模板 通用读写方法 RDD转为DataFrame Parquet文件数据源 JSON文件数据源 Hive数据源 数据库JDBC数据源 DataF ...

  3. Storm笔记——技术点汇总

    目录 概况 手工搭建集群 引言 安装Python 配置文件 启动与测试 应用部署 参数配置 Storm命令 原理 Storm架构 Storm组件 Stream Grouping 守护进程容错性(Dae ...

  4. Kafka笔记——技术点汇总

    Table of contents Table of contents Overview Introduction Use cases Manual setup Assumption Configur ...

  5. Spark Streaming笔记——技术点汇总

    目录 目录 概况 原理 API DStream WordCount示例 Input DStream Transformation Operation Output Operation 缓存与持久化 C ...

  6. Spark笔记——技术点汇总

    目录 概况 手工搭建集群 引言 安装Scala 配置文件 启动与测试 应用部署 部署架构 应用程序部署 核心原理 RDD概念 RDD核心组成 RDD依赖关系 DAG图 RDD故障恢复机制 Standa ...

  7. Hive笔记——技术点汇总

    目录 · 概况 · 手工安装 · 引言 · 创建HDFS目录 · 创建元数据库 · 配置文件 · 测试 · 原理 · 架构 · 与关系型数据库对比 · API · WordCount · 命令 · 数 ...

  8. MapReduce笔记——技术点汇总

    目录 · 概况 · 原理 · MapReduce编程模型 · MapReduce过程 · 容错机制 · API · 概况 · WordCount示例 · Writable接口 · Mapper类 ·  ...

  9. YARN笔记——技术点汇总

    目录 · 概况 · 原理 · 资源调度器分类 · YARN架构 · ResourceManager · NodeManager · ApplicationMaster · Container · YA ...

随机推荐

  1. 关于Java常见的误解

    误解一:JavaScript是Java的简易版 JavaScript是一种在网页中使用的脚本语言,它的原名叫做LiveScript.JavaScript的语法与Java类似.除此之外,他们再无任何关系 ...

  2. 表连接查询的顺序和where子句条件的前后顺序会影响sql的性能么

    有好多时候,我们常听别人说大表在前,小表在后,包括现在好多百度出来的靠前的答案都有说数据库是从右到左加载的,所以from语句最后关联的那张表会先被处理.如果三表交叉,就选择交叉表来作为基础表.等等一些 ...

  3. Redis可视化工具Redis Desktop Manager使用

    Redis可视化工具,RedisDesktopManager 没错,它开源的,托管在github上:https://github.com/uglide/RedisDesktopManager 还不错, ...

  4. jQuery.validate 的form校验

    jQuery验证框架 : 基本html代码: <script src="js/jquery-1.9.1.js"></script> <script s ...

  5. R语言包翻译

    Shiny-cheatsheet 作者:周彦通 1.安装 install.packages("shinydashboard")  2.基础知识 仪表盘有三个部分:标题.侧边栏,身体 ...

  6. 使用websocket-sharp来创建c#版本的websocket服务

    当前有一个需求,需要网页端调用扫描仪,javascript不具备调用能力,因此需要在机器上提供一个ws服务给前端网页调用扫描仪.而扫描仪有一个c#版本的API,因此需要寻找一个c#的websocket ...

  7. mybatis介绍与环境搭建

    一.不用纯jdbc的原因,即缺点. 1.数据库理解,使用时创建,不用时释放,会对数据库进行频繁的链接开启和关闭,造成数据库的资源浪费,影响数据库的性能.设想:使用数据库的连接池.2.将sql语句硬编码 ...

  8. php产生随机字符串

    /** * 产生随机字符串 * * @param int $length 输出长度 * @param string $chars 可选的 ,默认为 0123456789 * @return strin ...

  9. linux下vim 查找命令

    在命令模式下输入/word 这个是查找文件中“word”这个单词,是从文件上面到下面查找?word 这个是查找文件中“word”这个单词,是从文件下上面到面查找

  10. Spring Boot 构建 WAR和JAR 文件

    原文:https://github.com/x113773/testall/issues/3 ## JAR文件方式一:1. 修改[pom.xml](https://github.com/x113773 ...